mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-22 19:29:13 +08:00
261 lines
7.1 KiB
TypeScript
261 lines
7.1 KiB
TypeScript
import {
|
|
EXECUTION_ORIGINS,
|
|
RUN_STATUSES,
|
|
} from '@qinglong/runtime-core/run';
|
|
import {
|
|
type ProjectRunListCursor,
|
|
type ProjectRunListReader,
|
|
} from '@qinglong/runtime-core/project-run-list';
|
|
import {
|
|
BoundedRunListProjectionUnavailableError,
|
|
DEFAULT_BOUNDED_RUN_LIST_LIMIT,
|
|
InvalidBoundedRunListProjectionError,
|
|
MAX_BOUNDED_RUN_LIST_LIMIT,
|
|
executeBoundedRunListProjection,
|
|
} from '@qinglong/runtime-core/bounded-run-list-projection';
|
|
import {
|
|
normalizeToolDefinition,
|
|
type ToolJsonValue,
|
|
} from '@qinglong/runtime-core/tool-registry';
|
|
|
|
export const BUILTIN_RUN_LIST_TOOL = Object.freeze({
|
|
name: 'qinglong.run.list',
|
|
version: '1.0.0',
|
|
});
|
|
export const BUILTIN_RUN_LIST_TIMEOUT_SECONDS = 5;
|
|
export const BUILTIN_RUN_LIST_DEFAULT_LIMIT = DEFAULT_BOUNDED_RUN_LIST_LIMIT;
|
|
export const BUILTIN_RUN_LIST_MAX_LIMIT = MAX_BOUNDED_RUN_LIST_LIMIT;
|
|
|
|
const MAX_INT = 2_147_483_647;
|
|
const MIN_INT = -2_147_483_648;
|
|
const CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
|
|
|
|
const RUN_LIST_ITEM_SCHEMA = Object.freeze({
|
|
type: 'object',
|
|
properties: {
|
|
id: { type: 'string', minLength: 1, maxLength: 128 },
|
|
taskId: { type: 'string', minLength: 1, maxLength: 255 },
|
|
taskRevision: { type: 'string', minLength: 1, maxLength: 255 },
|
|
status: { type: 'string', maxLength: 32, enum: RUN_STATUSES },
|
|
version: { type: 'integer', minimum: 0, maximum: MAX_INT },
|
|
eventSequence: { type: 'integer', minimum: 0, maximum: MAX_INT },
|
|
priority: { type: 'integer', minimum: MIN_INT, maximum: MAX_INT },
|
|
executionOrigin: {
|
|
type: 'string',
|
|
maxLength: 32,
|
|
enum: EXECUTION_ORIGINS,
|
|
},
|
|
executionOwner: {
|
|
type: 'string',
|
|
maxLength: 16,
|
|
enum: ['legacy', 'runtime'],
|
|
},
|
|
createdAtMs: {
|
|
type: 'integer',
|
|
minimum: 0,
|
|
maximum: Number.MAX_SAFE_INTEGER,
|
|
},
|
|
queuedAtMs: {
|
|
type: 'integer',
|
|
minimum: 0,
|
|
maximum: Number.MAX_SAFE_INTEGER,
|
|
},
|
|
startedAtMs: {
|
|
type: 'integer',
|
|
minimum: 0,
|
|
maximum: Number.MAX_SAFE_INTEGER,
|
|
},
|
|
finishedAtMs: {
|
|
type: 'integer',
|
|
minimum: 0,
|
|
maximum: Number.MAX_SAFE_INTEGER,
|
|
},
|
|
},
|
|
required: [
|
|
'id',
|
|
'taskId',
|
|
'taskRevision',
|
|
'status',
|
|
'version',
|
|
'eventSequence',
|
|
'priority',
|
|
'executionOrigin',
|
|
'executionOwner',
|
|
'createdAtMs',
|
|
],
|
|
additionalProperties: false,
|
|
});
|
|
|
|
const RUN_LIST_CURSOR_SCHEMA = Object.freeze({
|
|
type: 'object',
|
|
properties: {
|
|
createdAtMs: {
|
|
type: 'integer',
|
|
minimum: 0,
|
|
maximum: Number.MAX_SAFE_INTEGER,
|
|
},
|
|
runId: { type: 'string', minLength: 1, maxLength: 128 },
|
|
},
|
|
required: ['createdAtMs', 'runId'],
|
|
additionalProperties: false,
|
|
});
|
|
|
|
export const BUILTIN_RUN_LIST_TOOL_DEFINITION = normalizeToolDefinition({
|
|
name: BUILTIN_RUN_LIST_TOOL.name,
|
|
version: BUILTIN_RUN_LIST_TOOL.version,
|
|
description: 'List recent low-sensitive Runs in the authenticated Project',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
after: RUN_LIST_CURSOR_SCHEMA,
|
|
limit: {
|
|
type: 'integer',
|
|
minimum: 1,
|
|
maximum: BUILTIN_RUN_LIST_MAX_LIMIT,
|
|
},
|
|
},
|
|
required: [],
|
|
additionalProperties: false,
|
|
},
|
|
outputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
runs: {
|
|
type: 'array',
|
|
maxItems: BUILTIN_RUN_LIST_MAX_LIMIT,
|
|
items: RUN_LIST_ITEM_SCHEMA,
|
|
},
|
|
hasMore: { type: 'boolean' },
|
|
next: RUN_LIST_CURSOR_SCHEMA,
|
|
},
|
|
required: ['runs', 'hasMore'],
|
|
additionalProperties: false,
|
|
},
|
|
effect: 'read',
|
|
risk: 'low',
|
|
requiredPermissions: ['run.read'],
|
|
timeoutSeconds: BUILTIN_RUN_LIST_TIMEOUT_SECONDS,
|
|
});
|
|
|
|
export class InvalidBuiltInRunListToolError extends TypeError {
|
|
readonly code = 'BUILTIN_RUN_LIST_TOOL_INVALID';
|
|
|
|
constructor(message: string) {
|
|
super(`Built-in Run list Tool is invalid: ${message}`);
|
|
this.name = 'InvalidBuiltInRunListToolError';
|
|
}
|
|
}
|
|
|
|
export class BuiltInRunListToolUnavailableError extends Error {
|
|
readonly code = 'BUILTIN_RUN_LIST_TOOL_UNAVAILABLE';
|
|
|
|
constructor() {
|
|
super('Built-in Run list Tool is unavailable');
|
|
this.name = 'BuiltInRunListToolUnavailableError';
|
|
}
|
|
}
|
|
|
|
function invalid(message: string): never {
|
|
throw new InvalidBuiltInRunListToolError(message);
|
|
}
|
|
|
|
function boundedText(value: unknown, maximum: number): value is string {
|
|
return (
|
|
typeof value === 'string' &&
|
|
value.length > 0 &&
|
|
value.length <= maximum &&
|
|
!CONTROL_PATTERN.test(value)
|
|
);
|
|
}
|
|
|
|
function integer(
|
|
value: unknown,
|
|
minimum: number,
|
|
maximum: number,
|
|
): value is number {
|
|
return (
|
|
Number.isSafeInteger(value) &&
|
|
Number(value) >= minimum &&
|
|
Number(value) <= maximum
|
|
);
|
|
}
|
|
|
|
function normalizeCursor(
|
|
value: ToolJsonValue | undefined,
|
|
): ProjectRunListCursor | undefined {
|
|
if (value === undefined) return undefined;
|
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
return invalid('cursor is invalid');
|
|
}
|
|
const cursor = value as Readonly<Record<string, ToolJsonValue>>;
|
|
if (
|
|
Reflect.ownKeys(cursor).length !== 2 ||
|
|
!Object.hasOwn(cursor, 'createdAtMs') ||
|
|
!Object.hasOwn(cursor, 'runId') ||
|
|
!integer(cursor.createdAtMs, 0, Number.MAX_SAFE_INTEGER) ||
|
|
!boundedText(cursor.runId, 128)
|
|
) {
|
|
return invalid('cursor is invalid');
|
|
}
|
|
return Object.freeze({
|
|
createdAtMs: cursor.createdAtMs,
|
|
runId: cursor.runId,
|
|
});
|
|
}
|
|
|
|
export async function executeBuiltInRunListTool(
|
|
runs: ProjectRunListReader,
|
|
projectId: string,
|
|
input: ToolJsonValue,
|
|
): Promise<Readonly<Record<string, ToolJsonValue>>> {
|
|
const inputRecord =
|
|
input && typeof input === 'object' && !Array.isArray(input)
|
|
? (input as Readonly<Record<string, ToolJsonValue>>)
|
|
: null;
|
|
const inputKeys = inputRecord ? Reflect.ownKeys(inputRecord) : [];
|
|
if (
|
|
!runs ||
|
|
typeof runs.listRunsByProject !== 'function' ||
|
|
!boundedText(projectId, 128) ||
|
|
!inputRecord ||
|
|
inputKeys.length > 2 ||
|
|
inputKeys.some((key) => key !== 'after' && key !== 'limit') ||
|
|
(inputRecord.limit !== undefined &&
|
|
!integer(inputRecord.limit, 1, BUILTIN_RUN_LIST_MAX_LIMIT))
|
|
) {
|
|
return invalid('execution context or input is invalid');
|
|
}
|
|
const after = normalizeCursor(inputRecord.after);
|
|
const limit = inputRecord.limit ?? BUILTIN_RUN_LIST_DEFAULT_LIMIT;
|
|
let result;
|
|
try {
|
|
result = await executeBoundedRunListProjection(runs, projectId, {
|
|
limit,
|
|
...(after === undefined ? {} : { after }),
|
|
});
|
|
} catch (error) {
|
|
if (error instanceof InvalidBoundedRunListProjectionError) {
|
|
return invalid('execution context or input is invalid');
|
|
}
|
|
if (!(error instanceof BoundedRunListProjectionUnavailableError)) {
|
|
throw error;
|
|
}
|
|
throw new BuiltInRunListToolUnavailableError();
|
|
}
|
|
const projected: readonly ToolJsonValue[] = result.runs.map((run) =>
|
|
Object.freeze({ ...run }),
|
|
);
|
|
return Object.freeze({
|
|
runs: Object.freeze(projected),
|
|
hasMore: result.hasMore,
|
|
...(result.next === undefined
|
|
? {}
|
|
: {
|
|
next: Object.freeze({
|
|
createdAtMs: result.next.createdAtMs,
|
|
runId: result.next.runId,
|
|
}),
|
|
}),
|
|
});
|
|
}
|