吸收数字员工日报产物闭环
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 } {
|
||||
|
||||
Reference in New Issue
Block a user