feat(client): project digital employee runtime into UI

This commit is contained in:
baiyanyun
2026-06-07 17:46:07 +08:00
parent 8ff1c57b83
commit b07f133ed6
15 changed files with 2639 additions and 252 deletions

View File

@@ -0,0 +1,229 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const mockState = vi.hoisted(() => ({
db: null as TestDb | null,
settings: new Map<string, unknown>(),
}));
vi.mock("../../db", () => ({
getDb: () => mockState.db,
readSetting: (key: string) => mockState.settings.get(key) ?? null,
writeSetting: (key: string, value: unknown) => mockState.settings.set(key, value),
}));
describe("digital employee state service", () => {
beforeEach(() => {
vi.resetModules();
vi.setSystemTime(new Date("2026-06-07T08:00:00.000Z"));
mockState.db = new TestDb();
mockState.settings = new Map<string, unknown>();
});
it("extracts artifacts and approvals from runtime event payload into syncable records", async () => {
const { recordDigitalEmployeeRuntimeEvent } = await import("./stateService");
recordDigitalEmployeeRuntimeEvent({
kind: "tool_result",
request_id: "req-1",
message: "生成交付产物并等待确认",
status: "completed",
payload: {
artifacts: [
{
id: "summary-csv",
name: "summary.csv",
uri: "file:///tmp/summary.csv",
},
],
outputPath: "/tmp/final-report.txt",
approval_requests: [
{
id: "approval-1",
title: "确认发送日报给管理端",
},
],
},
});
expect(Array.from(mockState.db?.artifacts.values() ?? [])).toEqual([
expect.objectContaining({
id: "computer-chat-run-req-1:artifact:summary-csv",
name: "summary.csv",
uri: "file:///tmp/summary.csv",
}),
expect.objectContaining({
id: "computer-chat-run-req-1:artifact:tmp-final-report-txt",
name: "final-report.txt",
uri: "/tmp/final-report.txt",
}),
]);
expect(Array.from(mockState.db?.approvals.values() ?? [])).toEqual([
expect.objectContaining({
id: "computer-chat-run-req-1:approval:approval-1",
title: "确认发送日报给管理端",
status: "pending",
}),
]);
expect(mockState.db?.outbox.get("artifact:upsert:computer-chat-run-req-1:artifact:summary-csv")).toMatchObject({
entity_type: "artifact",
entity_id: "computer-chat-run-req-1:artifact:summary-csv",
status: "pending",
});
expect(mockState.db?.outbox.get("approval:upsert:computer-chat-run-req-1:approval:approval-1")).toMatchObject({
entity_type: "approval",
entity_id: "computer-chat-run-req-1:approval:approval-1",
status: "pending",
});
});
});
interface TestArtifactRow {
id: string;
plan_id: string;
task_id: string;
run_id: string;
kind: string;
name: string | null;
uri: string | null;
payload: string;
sync_status: string;
created_at: string;
}
interface TestApprovalRow {
id: string;
plan_id: string;
task_id: string;
run_id: string;
title: string;
status: string;
payload: string;
sync_status: string;
created_at: string;
updated_at: string;
}
interface TestOutboxRow {
id: string;
entity_type: string;
entity_id: string;
operation: string;
payload: string;
status: string;
attempts: number;
created_at: string;
updated_at: string;
error: string | null;
}
class TestDb {
artifacts = new Map<string, TestArtifactRow>();
approvals = new Map<string, TestApprovalRow>();
outbox = new Map<string, TestOutboxRow>();
transaction<T>(callback: () => T): () => T {
return callback;
}
prepare(sql: string): {
all: (...args: unknown[]) => unknown[];
get: (...args: unknown[]) => unknown;
run: (...args: unknown[]) => unknown;
} {
const normalized = sql.replace(/\s+/g, " ").trim();
return {
all: () => [],
get: () => ({ count: 0 }),
run: (...args: unknown[]) => this.run(normalized, args),
};
}
private run(sql: string, args: unknown[]): unknown {
if (sql.startsWith("INSERT INTO digital_plans")) return { changes: 1 };
if (sql.startsWith("INSERT INTO digital_tasks")) return { changes: 1 };
if (sql.startsWith("INSERT INTO digital_runs")) return { changes: 1 };
if (sql.startsWith("UPDATE digital_runs")) return { changes: 1 };
if (sql.startsWith("INSERT OR IGNORE INTO digital_events")) return { changes: 1 };
if (sql.startsWith("INSERT INTO digital_artifacts")) {
const [id, planId, taskId, runId, kind, name, uri, payload, createdAt] = args as [
string,
string,
string,
string,
string,
string | null,
string | null,
string,
string,
];
this.artifacts.set(id, {
id,
plan_id: planId,
task_id: taskId,
run_id: runId,
kind,
name,
uri,
payload,
sync_status: "pending",
created_at: createdAt,
});
return { changes: 1 };
}
if (sql.startsWith("INSERT INTO digital_approvals")) {
const [id, planId, taskId, runId, title, status, payload, createdAt, updatedAt] = args as [
string,
string,
string,
string,
string,
string,
string,
string,
string,
];
this.approvals.set(id, {
id,
plan_id: planId,
task_id: taskId,
run_id: runId,
title,
status,
payload,
sync_status: "pending",
created_at: createdAt,
updated_at: updatedAt,
});
return { changes: 1 };
}
if (sql.startsWith("INSERT INTO digital_sync_outbox")) {
const [id, entityType, entityId, operation, payload, createdAt, updatedAt] = args as [
string,
string,
string,
string,
string,
string,
string,
];
this.outbox.set(id, {
id,
entity_type: entityType,
entity_id: entityId,
operation,
payload,
status: "pending",
attempts: 0,
created_at: createdAt,
updated_at: updatedAt,
error: null,
});
return { changes: 1 };
}
throw new Error(`Unhandled run query: ${sql}`);
}
}