吸收数字员工日报产物闭环

This commit is contained in:
baiyanyun
2026-06-09 22:16:03 +08:00
parent ba806f779e
commit 0d2008b35c
11 changed files with 588 additions and 180 deletions

View File

@@ -10,6 +10,7 @@ import { isWindows } from "../services/system/shellEnv";
import {
recordDigitalEmployeeSnapshot,
recordDigitalEmployeeGovernanceCommand,
recordDigitalEmployeeDailyReport,
readDigitalEmployeeRuntimeRecords,
readDigitalEmployeeState,
readDigitalEmployeeUiState,
@@ -25,6 +26,7 @@ import {
listDigitalEmployeeRouteDecisions,
type DigitalEmployeeManagedService,
type DigitalEmployeeGovernanceCommandRecord,
type DigitalEmployeeDailyReportRecordInput,
type DigitalEmployeeMemoryUpsertInput,
type DigitalEmployeeRouteDecisionInput,
type DigitalEmployeeServiceStatus,
@@ -179,6 +181,13 @@ export function registerDigitalEmployeeHandlers(ctx: HandlerContext): void {
return recordDigitalEmployeeGovernanceCommand(command);
},
);
ipcMain.handle(
"digitalEmployee:recordDailyReport",
async (_, input: DigitalEmployeeDailyReportRecordInput) => {
return recordDigitalEmployeeDailyReport(input);
},
);
}
function buildSnapshot(ctx: HandlerContext): DigitalEmployeeSnapshot {

View File

@@ -187,6 +187,83 @@ describe("digital employee state service", () => {
expect(Array.from(mockState.db?.outbox.values() ?? []).filter((row) => row.entity_type === "artifact")).toHaveLength(1);
});
it("records daily reports as syncable artifact and event history", async () => {
const { recordDigitalEmployeeDailyReport } = await import("./stateService");
const first = recordDigitalEmployeeDailyReport({
date: "2026-06-07",
title: "2026-06-07 数字员工日报",
summary: "今日完成任务汇总。",
totals: {
task_count: 2,
execution_count: 3,
success_count: 2,
failure_count: 1,
running_count: 0,
},
detailLines: ["09:00 任务 A完成", "10:00 任务 B异常"],
artifactSources: [{ id: "artifact-1", name: "summary.csv" }],
filename: "qimingclaw-digital-report-2026-06-07.txt",
textContent: "日报正文",
generatedAt: "2026-06-07T08:00:00.000Z",
reportScheduleTime: "18:00",
source: "qimingclaw",
model: "qimingclaw-adapter",
});
const second = recordDigitalEmployeeDailyReport({
date: "2026-06-07",
title: "2026-06-07 数字员工日报",
summary: "第二次生成。",
totals: { task_count: 2, execution_count: 4, success_count: 3, failure_count: 1 },
detailLines: ["18:00 重新生成日报"],
filename: "qimingclaw-digital-report-2026-06-07.txt",
textContent: "第二版日报正文",
generatedAt: "2026-06-07T09:00:00.000Z",
});
expect(first).toMatchObject({
reportId: "daily-report:2026-06-07:2026-06-07T08-00-00-000Z",
planId: "daily-report:2026-06-07",
taskId: "daily-report:2026-06-07:generate",
runId: "daily-report:2026-06-07:2026-06-07T08-00-00-000Z",
artifactId: "daily-report:2026-06-07:2026-06-07T08-00-00-000Z:artifact:text",
eventId: "daily-report:2026-06-07:2026-06-07T08-00-00-000Z:event:generated",
});
expect(second?.reportId).toBe("daily-report:2026-06-07:2026-06-07T09-00-00-000Z");
expect(mockState.db?.plans.get("daily-report:2026-06-07")).toMatchObject({ status: "completed" });
expect(mockState.db?.tasks.get("daily-report:2026-06-07:generate")).toMatchObject({
status: "completed",
assigned_skill_id: "qimingclaw-artifact-report",
});
expect(mockState.db?.runs.get(first!.runId)).toMatchObject({
status: "completed",
finished_at: "2026-06-07T08:00:00.000Z",
});
expect(Array.from(mockState.db?.artifacts.values() ?? []).filter((row) => row.kind === "daily_report")).toHaveLength(2);
expect(mockState.db?.events.get(first!.eventId)).toMatchObject({
kind: "daily_report_generated",
message: "数字员工日报已生成2026-06-07",
run_id: first!.runId,
});
expect(JSON.parse(mockState.db?.artifacts.get(first!.artifactId)?.payload ?? "{}")).toMatchObject({
report_id: first!.reportId,
date: "2026-06-07",
filename: "qimingclaw-digital-report-2026-06-07.txt",
format: "text",
text_content: "日报正文",
totals: { task_count: 2, execution_count: 3 },
artifact_sources: [{ id: "artifact-1", name: "summary.csv" }],
});
expect(mockState.db?.outbox.get(`artifact:upsert:${first!.artifactId}`)).toMatchObject({
entity_type: "artifact",
status: "pending",
});
expect(mockState.db?.outbox.get(`event:insert:${first!.eventId}`)).toMatchObject({
entity_type: "event",
status: "pending",
});
});
it("records and lists route decisions as syncable events", async () => {
const {
listDigitalEmployeeRouteDecisions,

View File

@@ -392,6 +392,31 @@ export interface DigitalEmployeeRuntimeEventRecord {
payload?: unknown;
}
export interface DigitalEmployeeDailyReportRecordInput {
reportId?: string | null;
date: string;
title: string;
summary: string;
totals: Record<string, unknown>;
detailLines: string[];
artifactSources?: unknown[];
filename: string;
textContent: string;
generatedAt?: string | null;
reportScheduleTime?: string | null;
source?: string | null;
model?: string | null;
}
export interface DigitalEmployeeDailyReportRecordResult {
reportId: string;
planId: string;
taskId: string;
runId: string;
artifactId: string;
eventId: string;
}
export interface DigitalEmployeeGovernanceCommandRecord {
commandKind: string;
commandId: string;
@@ -1451,6 +1476,89 @@ export function recordDigitalEmployeeRuntimeEvent(
tx();
}
export function recordDigitalEmployeeDailyReport(
input: DigitalEmployeeDailyReportRecordInput,
): DigitalEmployeeDailyReportRecordResult | null {
const db = getDigitalEmployeeDb();
if (!db) return null;
const now = input.generatedAt || new Date().toISOString();
const date = input.date || now.slice(0, 10);
const planId = `daily-report:${date}`;
const taskId = `${planId}:generate`;
const reportId = input.reportId || `${planId}:${safeId(now)}`;
const runId = reportId;
const artifactId = `${reportId}:artifact:text`;
const eventId = `${reportId}:event:generated`;
const payload = {
report_id: reportId,
date,
title: input.title,
summary: input.summary,
totals: input.totals ?? {},
detail_lines: input.detailLines ?? [],
artifact_sources: input.artifactSources ?? [],
filename: input.filename,
format: "text",
text_content: input.textContent,
generated_at: now,
report_schedule_time: input.reportScheduleTime ?? null,
source: input.source ?? "qimingclaw-adapter",
model: input.model ?? "qimingclaw-adapter",
};
const tx = db.transaction(() => {
upsertPlan({
id: planId,
title: `${date} 数字员工日报`,
objective: "沉淀数字员工日报产物与同步事实。",
status: "completed",
payload: { date, latest_report_id: reportId, source: "daily_report" },
now,
});
upsertTask({
id: taskId,
planId,
title: "生成数字员工日报",
status: "completed",
assignedSkillId: "qimingclaw-artifact-report",
payload: { date, latest_report_id: reportId, source: "daily_report" },
now,
});
upsertRun({
id: runId,
planId,
taskId,
status: "completed",
payload,
now,
});
finishRun(runId, "completed", now);
upsertArtifact({
id: artifactId,
planId,
taskId,
runId,
kind: "daily_report",
name: input.filename,
uri: null,
payload,
now,
});
insertEvent({
event_id: eventId,
kind: "daily_report_generated",
occurred_at: now,
message: `数字员工日报已生成:${date}`,
plan_id: planId,
task_id: taskId,
}, runId, payload);
});
tx();
return { reportId, planId, taskId, runId, artifactId, eventId };
}
export function recordDigitalEmployeeGovernanceCommand(
command: DigitalEmployeeGovernanceCommandRecord,
): { planId: string; taskId: string; runId: string } {

View File

@@ -152,6 +152,9 @@ contextBridge.exposeInMainWorld("QimingClawBridge", {
async recordGovernanceCommand(command: unknown) {
return ipcRenderer.invoke("digitalEmployee:recordGovernanceCommand", command);
},
async recordDailyReport(input: unknown) {
return ipcRenderer.invoke("digitalEmployee:recordDailyReport", input);
},
async respondPermission(sessionId: string, permissionId: string, response: "once" | "always" | "reject") {
return ipcRenderer.invoke("agent:respondPermission", sessionId, permissionId, response);
},