吸收数字员工长期记忆闭环

This commit is contained in:
baiyanyun
2026-06-09 13:49:11 +08:00
parent ef8252e2dd
commit ddd39396f7
12 changed files with 817 additions and 34 deletions

View File

@@ -439,6 +439,71 @@ describe("digital employee sync service", () => {
remote_id: "remote-schedule-run-1",
});
});
it("syncs memory business views and acknowledgements", async () => {
insertEntity("memory", "memory-1");
insertOutbox(
"memory:upsert:memory-1",
"memory",
"memory-1",
JSON.stringify({
key: "commit-language",
content: "用户要求提交信息使用中文,并保留相关上下文。",
category: "preference",
source: "qimingclaw-memory-service",
status: "active",
}),
);
mockState.fetch.mockResolvedValue(
jsonResponse({
success: true,
acked: [{ entity_type: "memory", entity_id: "memory-1", remote_id: "remote-memory-1" }],
}),
);
const { flushDigitalEmployeeSyncOutbox } = await import("./syncService");
const status = await flushDigitalEmployeeSyncOutbox({ force: true });
expect(status.failed).toBe(0);
expect(lastSyncRequestBody()).toMatchObject({
items: [
expect.objectContaining({
entity_type: "memory",
business_view: expect.objectContaining({
entity_type: "memory",
local_id: "memory-1",
key: "commit-language",
category: "preference",
source: "qimingclaw-memory-service",
status: "active",
content_preview: "用户要求提交信息使用中文,并保留相关上下文。",
}),
}),
],
});
expect(readEntity("memory", "memory-1")).toMatchObject({
sync_status: "synced",
remote_id: "remote-memory-1",
sync_error: null,
});
});
it("marks memory sync failures as failed", async () => {
insertEntity("memory", "memory-rejected");
insertOutbox("memory:upsert:memory-rejected", "memory", "memory-rejected");
mockState.fetch.mockResolvedValue(
jsonResponse({ code: "0001", message: "记忆同步被拒绝", data: null }),
);
const { flushDigitalEmployeeSyncOutbox } = await import("./syncService");
const status = await flushDigitalEmployeeSyncOutbox({ force: true });
expect(status.failed).toBe(1);
expect(readEntity("memory", "memory-rejected")).toMatchObject({
sync_status: "failed",
sync_error: "记忆同步被拒绝",
});
});
});
function insertPlan(id: string): void {
@@ -505,6 +570,7 @@ function entityMap(
if (entityType === "schedule_run") return mockState.db.scheduleRuns;
if (entityType === "artifact") return mockState.db.artifacts;
if (entityType === "approval") return mockState.db.approvals;
if (entityType === "memory") return mockState.db.memories;
return undefined;
}
@@ -573,6 +639,7 @@ class TestDb {
scheduleRuns = new Map<string, TestSyncEntityRow>();
artifacts = new Map<string, TestSyncEntityRow>();
approvals = new Map<string, TestSyncEntityRow>();
memories = new Map<string, TestSyncEntityRow>();
attempts: TestAttemptRow[] = [];
private nextAttemptId = 1;
@@ -673,6 +740,11 @@ class TestDb {
return this.approvals.get(id);
}
if (sql.startsWith("SELECT key AS title, status, content AS summary, payload FROM digital_memories")) {
const [id] = args as [string];
return this.memories.get(id);
}
if (sql.startsWith("SELECT title, status, NULL AS objective, payload FROM digital_plan_steps")) {
const [id] = args as [string];
return this.planSteps.get(id);
@@ -785,7 +857,7 @@ class TestDb {
return { changes: before - this.attempts.length };
}
if (/^UPDATE digital_(plans|plan_steps|schedules|schedule_runs|artifacts|approvals) SET sync_status = 'synced'/.test(sql)) {
if (/^UPDATE digital_(plans|plan_steps|schedules|schedule_runs|artifacts|approvals|memories) SET sync_status = 'synced'/.test(sql)) {
const [remoteId, lastSyncedAt, id] = args as [string | null, string, string];
const row = this.entityRowsForUpdate(sql).get(id);
if (row) {
@@ -797,7 +869,7 @@ class TestDb {
return { changes: row ? 1 : 0 };
}
if (/^UPDATE digital_(plans|plan_steps|schedules|schedule_runs|artifacts|approvals) SET sync_status = 'failed'/.test(sql)) {
if (/^UPDATE digital_(plans|plan_steps|schedules|schedule_runs|artifacts|approvals|memories) SET sync_status = 'failed'/.test(sql)) {
const [syncError, id] = args as [string, string];
const row = this.entityRowsForUpdate(sql).get(id);
if (row) {
@@ -813,6 +885,7 @@ class TestDb {
private entityRowsForUpdate(sql: string): Map<string, TestSyncEntityRow> {
if (sql.startsWith("UPDATE digital_artifacts")) return this.artifacts;
if (sql.startsWith("UPDATE digital_approvals")) return this.approvals;
if (sql.startsWith("UPDATE digital_memories")) return this.memories;
if (sql.startsWith("UPDATE digital_schedule_runs")) return this.scheduleRuns;
if (sql.startsWith("UPDATE digital_schedules")) return this.schedules;
if (sql.startsWith("UPDATE digital_plan_steps")) return this.planSteps;