吸收数字员工产物交付状态
This commit is contained in:
@@ -83,6 +83,110 @@ describe("digital employee state service", () => {
|
||||
expect(mockState.ensureSchemaCalls).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("normalizes file server workspace events into delivery artifact payload", async () => {
|
||||
const { recordDigitalEmployeeRuntimeEvent } = await import("./stateService");
|
||||
|
||||
recordDigitalEmployeeRuntimeEvent({
|
||||
kind: "file_server.workspace.created",
|
||||
request_id: "file-workspace-1",
|
||||
message: "create-workspace 完成",
|
||||
status: "completed",
|
||||
payload: {
|
||||
source: "qiming-file-server",
|
||||
toolName: "create_workspace",
|
||||
workspaceId: "workspace-123",
|
||||
projectId: "project-file",
|
||||
path: "/tmp/qiming/workspace-123",
|
||||
status: "completed",
|
||||
},
|
||||
});
|
||||
|
||||
const artifacts = Array.from(mockState.db?.artifacts.values() ?? []);
|
||||
expect(artifacts).toHaveLength(1);
|
||||
expect(artifacts[0]).toMatchObject({
|
||||
kind: "workspace",
|
||||
name: "workspace-123",
|
||||
uri: "/tmp/qiming/workspace-123",
|
||||
});
|
||||
expect(JSON.parse(artifacts[0]?.payload ?? "{}")).toMatchObject({
|
||||
delivery: {
|
||||
stage: "workspace",
|
||||
status: "completed",
|
||||
workspace_id: "workspace-123",
|
||||
project_id: "project-file",
|
||||
uri: "/tmp/qiming/workspace-123",
|
||||
source_tool: "create_workspace",
|
||||
source_event_kind: "file_server.workspace.created",
|
||||
},
|
||||
});
|
||||
expect(mockState.db?.outbox.get(`artifact:upsert:${artifacts[0]?.id}`)).toMatchObject({
|
||||
entity_type: "artifact",
|
||||
status: "pending",
|
||||
});
|
||||
});
|
||||
|
||||
it("upserts file server file delivery status without duplicating artifacts", async () => {
|
||||
const { recordDigitalEmployeeRuntimeEvent } = await import("./stateService");
|
||||
|
||||
const basePayload = {
|
||||
source: "qiming-file-server",
|
||||
workspaceId: "workspace-123",
|
||||
projectId: "project-file",
|
||||
fileId: "file-1",
|
||||
filename: "report.pdf",
|
||||
version: "v2",
|
||||
size: 2048,
|
||||
path: "/tmp/qiming/workspace-123/report.pdf",
|
||||
};
|
||||
recordDigitalEmployeeRuntimeEvent({
|
||||
kind: "file_server.upload.completed",
|
||||
request_id: "file-delivery-1",
|
||||
message: "upload-file 完成",
|
||||
status: "completed",
|
||||
payload: {
|
||||
...basePayload,
|
||||
toolName: "upload_file",
|
||||
status: "uploaded",
|
||||
},
|
||||
});
|
||||
recordDigitalEmployeeRuntimeEvent({
|
||||
kind: "file_server.download.completed",
|
||||
request_id: "file-delivery-1",
|
||||
message: "download-all-files 完成",
|
||||
status: "completed",
|
||||
payload: {
|
||||
...basePayload,
|
||||
toolName: "download_all_files",
|
||||
status: "downloaded",
|
||||
downloadUrl: "file:///tmp/qiming/workspace-123/report.pdf",
|
||||
},
|
||||
});
|
||||
|
||||
const artifacts = Array.from(mockState.db?.artifacts.values() ?? []);
|
||||
expect(artifacts).toHaveLength(1);
|
||||
expect(artifacts[0]).toMatchObject({
|
||||
kind: "download",
|
||||
name: "report.pdf",
|
||||
uri: "file:///tmp/qiming/workspace-123/report.pdf",
|
||||
});
|
||||
expect(JSON.parse(artifacts[0]?.payload ?? "{}")).toMatchObject({
|
||||
delivery: {
|
||||
stage: "download",
|
||||
status: "downloaded",
|
||||
workspace_id: "workspace-123",
|
||||
project_id: "project-file",
|
||||
file_id: "file-1",
|
||||
version: "v2",
|
||||
filename: "report.pdf",
|
||||
size: 2048,
|
||||
uri: "file:///tmp/qiming/workspace-123/report.pdf",
|
||||
source_tool: "download_all_files",
|
||||
source_event_kind: "file_server.download.completed",
|
||||
},
|
||||
});
|
||||
expect(Array.from(mockState.db?.outbox.values() ?? []).filter((row) => row.entity_type === "artifact")).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("records and lists route decisions as syncable events", async () => {
|
||||
const {
|
||||
listDigitalEmployeeRouteDecisions,
|
||||
|
||||
@@ -1402,7 +1402,9 @@ export function recordDigitalEmployeeRuntimeEvent(
|
||||
if (status === "completed" || status === "failed") {
|
||||
finishRun(ids.runId, status, now);
|
||||
}
|
||||
persistPayloadArtifactsAndApprovals(ids, event.payload ?? event, now);
|
||||
persistPayloadArtifactsAndApprovals(ids, event.payload ?? event, now, {
|
||||
sourceEventKind: event.kind,
|
||||
});
|
||||
insertEvent(
|
||||
{
|
||||
event_id: `${ids.runId}:${event.kind}:${Date.now()}`,
|
||||
@@ -3592,9 +3594,10 @@ function persistPayloadArtifactsAndApprovals(
|
||||
ids: { planId: string; taskId: string; runId: string },
|
||||
payload: unknown,
|
||||
now: string,
|
||||
context: ArtifactExtractionContext = {},
|
||||
): void {
|
||||
const sources = payloadSources(payload);
|
||||
const artifacts = sources.flatMap((source) => extractArtifacts(source));
|
||||
const artifacts = sources.flatMap((source) => extractArtifacts(source, context));
|
||||
const approvals = sources.flatMap((source) => extractApprovals(source));
|
||||
|
||||
dedupeByKey(artifacts, (artifact) => artifact.key).slice(0, 20).forEach((artifact, index) => {
|
||||
@@ -3645,12 +3648,34 @@ interface ExtractedArtifact {
|
||||
payload: unknown;
|
||||
}
|
||||
|
||||
function extractArtifacts(payload: unknown): ExtractedArtifact[] {
|
||||
interface ArtifactExtractionContext {
|
||||
sourceEventKind?: string;
|
||||
}
|
||||
|
||||
interface DeliveryMetadata {
|
||||
stage: string;
|
||||
status: string | null;
|
||||
workspace_id: string | null;
|
||||
project_id: string | null;
|
||||
file_id: string | null;
|
||||
version: string | null;
|
||||
filename: string | null;
|
||||
size: number | null;
|
||||
uri: string | null;
|
||||
error: string | null;
|
||||
source_tool: string | null;
|
||||
source_event_kind: string | null;
|
||||
}
|
||||
|
||||
function extractArtifacts(payload: unknown, context: ArtifactExtractionContext = {}): ExtractedArtifact[] {
|
||||
const record = objectRecord(payload);
|
||||
if (!record) return typeof payload === "string" && looksLikeArtifactUri(payload)
|
||||
? [artifactFromValue(payload, 0)]
|
||||
: [];
|
||||
|
||||
const fileServerArtifacts = extractFileServerArtifacts(record, context);
|
||||
if (fileServerArtifacts.length > 0) return fileServerArtifacts;
|
||||
|
||||
const values = [
|
||||
...unknownArray(record.artifacts),
|
||||
...unknownArray(record.outputs),
|
||||
@@ -3691,6 +3716,152 @@ function artifactFromValue(value: unknown, index: number): ExtractedArtifact {
|
||||
};
|
||||
}
|
||||
|
||||
const FILE_SERVER_TOOLS = new Set([
|
||||
"create_workspace",
|
||||
"create_workspace_v2",
|
||||
"push_skills_to_workspace",
|
||||
"push_skills_to_workspace_v2",
|
||||
"get_file_list",
|
||||
"files_update",
|
||||
"upload_file",
|
||||
"upload_files",
|
||||
"download_all_files",
|
||||
"create_project",
|
||||
"upload_project",
|
||||
"get_project_content",
|
||||
"get_project_content_by_version",
|
||||
"backup_current_version",
|
||||
"export_project",
|
||||
"delete_project",
|
||||
"upload_attachment_file",
|
||||
"copy_project",
|
||||
]);
|
||||
|
||||
function extractFileServerArtifacts(
|
||||
record: Record<string, unknown>,
|
||||
context: ArtifactExtractionContext,
|
||||
): ExtractedArtifact[] {
|
||||
const sourceTool = normalizeFileServerTool(record);
|
||||
if (!isFileServerPayload(record, sourceTool, context)) return [];
|
||||
|
||||
const values = [
|
||||
...unknownArray(record.artifacts),
|
||||
...unknownArray(record.outputs),
|
||||
...unknownArray(record.files),
|
||||
...unknownArray(record.attachments),
|
||||
];
|
||||
if (values.length === 0) values.push(record);
|
||||
|
||||
return values.map((value, index) => {
|
||||
const item = objectRecord(value);
|
||||
return fileServerArtifactFromRecord(item ? { ...record, ...item } : record, sourceTool, context, index);
|
||||
}).filter((artifact) => artifact.name || artifact.uri);
|
||||
}
|
||||
|
||||
function fileServerArtifactFromRecord(
|
||||
record: Record<string, unknown>,
|
||||
sourceTool: string,
|
||||
context: ArtifactExtractionContext,
|
||||
index: number,
|
||||
): ExtractedArtifact {
|
||||
const delivery = buildFileServerDelivery(record, sourceTool, context);
|
||||
const name = delivery.filename
|
||||
|| stringValue(record.name ?? record.title ?? record.projectName ?? record.project_name)
|
||||
|| delivery.workspace_id
|
||||
|| artifactName(delivery.uri || "")
|
||||
|| `File Server 产物 ${index + 1}`;
|
||||
return {
|
||||
key: delivery.file_id
|
||||
|| stringValue(record.id ?? record.artifact_id)
|
||||
|| delivery.uri
|
||||
|| `${delivery.stage}:${delivery.workspace_id || delivery.project_id || name}`,
|
||||
kind: delivery.stage,
|
||||
name,
|
||||
uri: delivery.uri,
|
||||
payload: {
|
||||
...record,
|
||||
delivery,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function buildFileServerDelivery(
|
||||
record: Record<string, unknown>,
|
||||
sourceTool: string,
|
||||
context: ArtifactExtractionContext,
|
||||
): DeliveryMetadata {
|
||||
const uri = stringValue(
|
||||
record.downloadUrl
|
||||
?? record.download_url
|
||||
?? record.exportPath
|
||||
?? record.export_path
|
||||
?? record.uri
|
||||
?? record.url
|
||||
?? record.path
|
||||
?? record.filePath
|
||||
?? record.file_path
|
||||
?? record.outputPath
|
||||
?? record.output_path,
|
||||
);
|
||||
return {
|
||||
stage: fileServerDeliveryStage(sourceTool),
|
||||
status: stringValue(record.status ?? record.deliveryStatus ?? record.delivery_status) || null,
|
||||
workspace_id: stringValue(record.workspaceId ?? record.workspace_id ?? record.workspace) || null,
|
||||
project_id: stringValue(record.projectId ?? record.project_id ?? record.project) || null,
|
||||
file_id: stringValue(record.fileId ?? record.file_id ?? record.id ?? record.artifact_id) || null,
|
||||
version: stringValue(record.version ?? record.fileVersion ?? record.file_version) || null,
|
||||
filename: stringValue(record.filename ?? record.fileName ?? record.file_name ?? record.name) || artifactName(uri) || null,
|
||||
size: numberValue(record.size ?? record.fileSize ?? record.file_size ?? record.bytes),
|
||||
uri: uri || null,
|
||||
error: stringValue(record.error ?? record.message_error ?? record.lastError ?? record.last_error) || null,
|
||||
source_tool: sourceTool || null,
|
||||
source_event_kind: context.sourceEventKind || null,
|
||||
};
|
||||
}
|
||||
|
||||
function isFileServerPayload(
|
||||
record: Record<string, unknown>,
|
||||
sourceTool: string,
|
||||
context: ArtifactExtractionContext,
|
||||
): boolean {
|
||||
if (sourceTool && FILE_SERVER_TOOLS.has(sourceTool)) return true;
|
||||
const text = [
|
||||
record.source,
|
||||
record.service,
|
||||
record.provider,
|
||||
record.server,
|
||||
context.sourceEventKind,
|
||||
].map((value) => stringValue(value).toLowerCase()).join(" ");
|
||||
return text.includes("qiming-file-server")
|
||||
|| text.includes("qiming_file_server")
|
||||
|| text.includes("file_server")
|
||||
|| text.includes("file-server");
|
||||
}
|
||||
|
||||
function normalizeFileServerTool(record: Record<string, unknown>): string {
|
||||
return stringValue(record.toolName ?? record.tool_name ?? record.tool ?? record.action ?? record.command)
|
||||
.toLowerCase()
|
||||
.replace(/-/g, "_");
|
||||
}
|
||||
|
||||
function fileServerDeliveryStage(tool: string): string {
|
||||
if (tool.includes("workspace")) return "workspace";
|
||||
if (tool.includes("download")) return "download";
|
||||
if (tool.includes("export")) return "export";
|
||||
if (tool.includes("upload") || tool.includes("push_skills")) return "upload";
|
||||
if (tool.includes("files_update") || tool.includes("file_list") || tool.includes("project_content")) return "file_update";
|
||||
return "artifact";
|
||||
}
|
||||
|
||||
function numberValue(value: unknown): number | null {
|
||||
if (typeof value === "number" && Number.isFinite(value)) return value;
|
||||
if (typeof value === "string" && value.trim()) {
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
interface ExtractedApproval {
|
||||
key: string;
|
||||
title: string;
|
||||
|
||||
Reference in New Issue
Block a user