mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
230 lines
8.0 KiB
TypeScript
230 lines
8.0 KiB
TypeScript
import type { PostgresPool } from '@qinglong/runtime-core';
|
|
import {
|
|
ApprovalUnavailableError,
|
|
approvalRequestDigest,
|
|
normalizeApprovalRequestRecord,
|
|
type ApprovalRequestRecord,
|
|
} from '@qinglong/runtime-core/approved-action';
|
|
import {
|
|
approvalRequestUpdatedAtMs,
|
|
assertApprovalDiscoveryProjectId,
|
|
assertApprovalDiscoveryRequestId,
|
|
assertApprovalRequestPageSize,
|
|
normalizeApprovalRequestCursor,
|
|
type ApprovalRequestDetail,
|
|
type ApprovalRequestDetailSource,
|
|
type ApprovalRequestPage,
|
|
type ApprovalRequestSource,
|
|
} from '@qinglong/runtime-core/approval-discovery';
|
|
import {
|
|
normalizeToolInvocationPreviewArtifact,
|
|
type ToolInvocationPreviewArtifact,
|
|
} from '@qinglong/runtime-core/tool-invocation-artifact';
|
|
|
|
import {
|
|
postgresRequiredInteger,
|
|
postgresRequiredJsonObject,
|
|
postgresRequiredString,
|
|
} from '../repository/definitionRepositorySupport';
|
|
|
|
type Row = Record<string, unknown>;
|
|
|
|
function unavailable(): ApprovalUnavailableError {
|
|
return new ApprovalUnavailableError();
|
|
}
|
|
|
|
function request(row: Row): Readonly<ApprovalRequestRecord> {
|
|
try {
|
|
const value = normalizeApprovalRequestRecord(
|
|
postgresRequiredJsonObject(
|
|
row.requestJson,
|
|
unavailable,
|
|
) as unknown as ApprovalRequestRecord,
|
|
);
|
|
if (
|
|
approvalRequestDigest(value) !==
|
|
postgresRequiredString(row.requestDigest, unavailable) ||
|
|
approvalRequestUpdatedAtMs(value) !==
|
|
postgresRequiredInteger(row.updatedAtMs, unavailable)
|
|
) {
|
|
throw unavailable();
|
|
}
|
|
return value;
|
|
} catch (error) {
|
|
if (error instanceof ApprovalUnavailableError) throw error;
|
|
throw unavailable();
|
|
}
|
|
}
|
|
|
|
function previewArtifact(
|
|
row: Row,
|
|
): Readonly<ToolInvocationPreviewArtifact> | null {
|
|
if (row.previewArtifactId === null) return null;
|
|
try {
|
|
const value = normalizeToolInvocationPreviewArtifact(
|
|
postgresRequiredJsonObject(
|
|
row.previewArtifactJson,
|
|
unavailable,
|
|
) as unknown as ToolInvocationPreviewArtifact,
|
|
);
|
|
if (
|
|
value.artifactId !== postgresRequiredString(row.previewArtifactId, unavailable) ||
|
|
value.projectId !== postgresRequiredString(row.previewProjectId, unavailable) ||
|
|
value.actionRef !== postgresRequiredString(row.previewActionRef, unavailable) ||
|
|
value.actionDigest !== postgresRequiredString(row.previewActionDigest, unavailable) ||
|
|
value.previewDigest !== postgresRequiredString(row.storedPreviewDigest, unavailable) ||
|
|
value.redactionContractDigest !==
|
|
postgresRequiredString(row.redactionContractDigest, unavailable) ||
|
|
value.artifactDigest !== postgresRequiredString(row.previewArtifactDigest, unavailable) ||
|
|
value.byteLength !== postgresRequiredInteger(row.previewByteLength, unavailable) ||
|
|
value.sealedAtMs !== postgresRequiredInteger(row.previewSealedAtMs, unavailable)
|
|
) {
|
|
throw unavailable();
|
|
}
|
|
return value;
|
|
} catch (error) {
|
|
if (error instanceof ApprovalUnavailableError) throw error;
|
|
throw unavailable();
|
|
}
|
|
}
|
|
|
|
/** Read-only Approval history using the existing Project keyset index. */
|
|
export class PostgresApprovalRequestSource
|
|
implements ApprovalRequestSource, ApprovalRequestDetailSource
|
|
{
|
|
constructor(private readonly pool: Pick<PostgresPool, 'query'>) {
|
|
if (!pool || typeof pool.query !== 'function') {
|
|
throw new TypeError('PostgreSQL Approval discovery pool is invalid');
|
|
}
|
|
}
|
|
|
|
async listApprovalRequests(options: {
|
|
readonly projectId: string;
|
|
readonly limit: number;
|
|
readonly after?: { readonly updatedAtMs: number; readonly requestId: string };
|
|
}): Promise<Readonly<ApprovalRequestPage>> {
|
|
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
|
throw new TypeError('Approval discovery options are invalid');
|
|
}
|
|
const keys = Object.keys(options);
|
|
if (
|
|
!keys.includes('projectId') ||
|
|
!keys.includes('limit') ||
|
|
keys.some((key) => !['projectId', 'limit', 'after'].includes(key))
|
|
) {
|
|
throw new TypeError('Approval discovery options shape is invalid');
|
|
}
|
|
assertApprovalDiscoveryProjectId(options.projectId);
|
|
assertApprovalRequestPageSize(options.limit);
|
|
const after = options.after
|
|
? normalizeApprovalRequestCursor(options.after)
|
|
: undefined;
|
|
try {
|
|
const result = await this.pool.query<Row>(
|
|
`SELECT request_json AS "requestJson",
|
|
request_digest AS "requestDigest",
|
|
updated_at_ms AS "updatedAtMs"
|
|
FROM "ql3"."approval_requests"
|
|
WHERE project_id = $1
|
|
AND (
|
|
$2::bigint IS NULL OR updated_at_ms < $2 OR
|
|
(updated_at_ms = $2 AND request_id < $3)
|
|
)
|
|
ORDER BY updated_at_ms DESC, request_id DESC
|
|
LIMIT $4`,
|
|
[
|
|
options.projectId,
|
|
after?.updatedAtMs ?? null,
|
|
after?.requestId ?? '',
|
|
options.limit + 1,
|
|
],
|
|
);
|
|
const truncated = result.rows.length > options.limit;
|
|
const requests = Object.freeze(
|
|
result.rows.slice(0, options.limit).map(request),
|
|
);
|
|
const last = requests.at(-1);
|
|
return Object.freeze({
|
|
requests,
|
|
truncated,
|
|
...(truncated && last
|
|
? {
|
|
next: Object.freeze({
|
|
updatedAtMs: approvalRequestUpdatedAtMs(last),
|
|
requestId: last.id,
|
|
}),
|
|
}
|
|
: {}),
|
|
});
|
|
} catch {
|
|
throw unavailable();
|
|
}
|
|
}
|
|
|
|
async getApprovalRequestDetail(options: {
|
|
readonly projectId: string;
|
|
readonly requestId: string;
|
|
}): Promise<Readonly<ApprovalRequestDetail> | null> {
|
|
if (
|
|
!options ||
|
|
typeof options !== 'object' ||
|
|
Array.isArray(options) ||
|
|
Object.keys(options).length !== 2 ||
|
|
!Object.hasOwn(options, 'projectId') ||
|
|
!Object.hasOwn(options, 'requestId')
|
|
) {
|
|
throw new TypeError('Approval detail options are invalid');
|
|
}
|
|
assertApprovalDiscoveryProjectId(options.projectId);
|
|
assertApprovalDiscoveryRequestId(options.requestId);
|
|
try {
|
|
const result = await this.pool.query<Row>(
|
|
`SELECT a.request_json AS "requestJson",
|
|
a.request_digest AS "requestDigest",
|
|
a.updated_at_ms AS "updatedAtMs",
|
|
p.artifact_id AS "previewArtifactId",
|
|
p.project_id AS "previewProjectId",
|
|
p.action_ref AS "previewActionRef",
|
|
p.action_digest AS "previewActionDigest",
|
|
p.preview_digest AS "storedPreviewDigest",
|
|
p.redaction_contract_digest AS "redactionContractDigest",
|
|
p.artifact_digest AS "previewArtifactDigest",
|
|
p.byte_length AS "previewByteLength",
|
|
p.sealed_at_ms AS "previewSealedAtMs",
|
|
p.artifact_json AS "previewArtifactJson"
|
|
FROM "ql3"."approval_requests" a
|
|
LEFT JOIN "ql3"."tool_invocation_preview_artifacts" p
|
|
ON a.action_type = 'tool.invoke'
|
|
AND p.project_id = a.project_id
|
|
AND p.action_ref = a.action_ref
|
|
AND p.action_digest = a.action_digest
|
|
AND p.preview_digest = a.preview_digest
|
|
WHERE a.project_id = $1 AND a.request_id = $2
|
|
LIMIT 2`,
|
|
[options.projectId, options.requestId],
|
|
);
|
|
if (result.rows.length > 1) throw unavailable();
|
|
if (!result.rows[0]) return null;
|
|
const approval = request(result.rows[0]);
|
|
const preview = previewArtifact(result.rows[0]);
|
|
if (
|
|
preview &&
|
|
(approval.action.actionType !== 'tool.invoke' ||
|
|
approval.projectId !== preview.projectId ||
|
|
approval.action.actionRef !== preview.actionRef ||
|
|
approval.action.actionDigest !== preview.actionDigest ||
|
|
approval.action.previewDigest !== preview.previewDigest)
|
|
) {
|
|
throw unavailable();
|
|
}
|
|
return Object.freeze({
|
|
request: approval,
|
|
preview: preview?.preview ?? null,
|
|
});
|
|
} catch (error) {
|
|
if (error instanceof ApprovalUnavailableError) throw error;
|
|
throw unavailable();
|
|
}
|
|
}
|
|
}
|