feat(client): persist digital governance events

This commit is contained in:
baiyanyun
2026-06-07 18:59:59 +08:00
parent 5e76828afe
commit e37024bdbe
14 changed files with 925 additions and 158 deletions

View File

@@ -13,31 +13,150 @@ const registeredHandlers: Array<{
}> = [];
function readEventField(data: unknown, keys: string[]): string | undefined {
if (!data || typeof data !== "object") return undefined;
const record = data as Record<string, unknown>;
for (const key of keys) {
const value = record[key];
if (typeof value === "string" && value.trim()) return value;
for (const record of eventRecords(data)) {
for (const key of keys) {
const value = record[key];
if (typeof value === "string" && value.trim()) return value;
}
}
return undefined;
}
function recordForwardedEvent(eventType: string, data: unknown): void {
recordDigitalEmployeeRuntimeEvent({
kind: eventType,
message:
readEventField(data, ["message", "description", "title", "subType"]) ||
eventType,
const normalized = normalizeForwardedEvent(eventType, data);
recordDigitalEmployeeRuntimeEvent(normalized);
}
function normalizeForwardedEvent(
eventType: string,
data: unknown,
): Parameters<typeof recordDigitalEmployeeRuntimeEvent>[0] {
const subtype = readEventField(data, ["subType", "sessionUpdate", "type", "messageType"]);
const toolName = readEventField(data, ["toolTitle", "title", "name", "kind"]);
const message = eventMessage(eventType, data, subtype, toolName);
return {
kind: eventKind(eventType, subtype),
message,
request_id: readEventField(data, ["requestId", "request_id"]),
session_id: readEventField(data, ["sessionId", "session_id", "id"]),
project_id: readEventField(data, ["projectId", "project_id"]),
status: eventType.includes("error")
? "failed"
: eventType.includes("idle") || eventType.includes("promptEnd")
? "completed"
: "running",
payload: data,
});
status: eventStatus(eventType, data),
payload: runtimePayload(eventType, data, subtype, toolName),
};
}
function eventKind(eventType: string, subtype?: string): string {
if (eventType === "computer:progress" && subtype) return `computer_${subtype}`;
return eventType.replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_+|_+$/g, "") || eventType;
}
function eventMessage(
eventType: string,
data: unknown,
subtype?: string,
toolName?: string,
): string {
if (subtype === "tool_call") return `工具调用开始:${toolName || "unknown"}`;
if (subtype === "tool_call_update") return `工具调用更新:${toolName || "unknown"}`;
if (subtype === "agent_message_chunk") return "Agent 输出内容";
if (subtype === "agent_thought_chunk") return "Agent 思考内容";
const explicit = readEventField(data, ["message", "description", "title"]);
if (explicit) return explicit;
if (eventType === "permission.updated") return `权限请求:${toolName || "待确认"}`;
if (eventType === "permission.replied") return `权限请求已回复:${toolName || "已处理"}`;
if (eventType === "file.edited") return `文件已编辑:${readEventField(data, ["filePath", "file_path", "path"]) || "unknown"}`;
if (eventType === "session.diff") return "会话产生文件差异";
return subtype || eventType;
}
function eventStatus(
eventType: string,
data: unknown,
): "running" | "completed" | "failed" | "pending" {
if (eventType.includes("error")) return "failed";
const status = (readEventField(data, ["status", "outcome", "reason"]) || "").toLowerCase();
if (["failed", "failure", "error", "rejected", "denied", "cancelled", "canceled"].includes(status)) return "failed";
if (["completed", "complete", "success", "succeeded", "done", "accepted", "approved", "end_turn"].includes(status)) return "completed";
if (eventType.includes("idle") || eventType.includes("promptEnd")) return "completed";
if (eventType.includes("permission")) return eventType.endsWith("replied") ? "completed" : "pending";
return "running";
}
function runtimePayload(
eventType: string,
data: unknown,
subtype?: string,
toolName?: string,
): Record<string, unknown> {
const primary = objectRecord(data) ?? {};
const nested = objectRecord(primary.data) ?? {};
const payload: Record<string, unknown> = {
...primary,
eventType,
subtype,
toolName,
};
const output = nested.rawOutput ?? nested.output ?? primary.output;
if (output !== undefined) payload.output = output;
const rawInput = nested.rawInput ?? nested.input ?? primary.input;
if (rawInput !== undefined) payload.input = rawInput;
const locations = Array.isArray(nested.locations) ? nested.locations : Array.isArray(primary.locations) ? primary.locations : [];
const artifacts = artifactHints(eventType, primary, nested, locations);
if (artifacts.length > 0) payload.artifacts = artifacts;
const approval = approvalHint(eventType, primary, nested, toolName);
if (approval) payload.approval_requests = [approval];
return payload;
}
function artifactHints(
eventType: string,
primary: Record<string, unknown>,
nested: Record<string, unknown>,
locations: unknown[],
): Array<Record<string, unknown>> {
const directPath = readEventField(primary, ["filePath", "file_path", "path", "uri"])
|| readEventField(nested, ["filePath", "file_path", "path", "uri"]);
const hints: Array<Record<string, unknown>> = locations
.map((location) => objectRecord(location))
.filter((location): location is Record<string, unknown> => Boolean(location?.path))
.map((location) => ({ kind: "file", path: location.path, payload: location }));
if (directPath && (eventType === "file.edited" || eventType === "session.diff")) {
hints.unshift({ kind: eventType === "file.edited" ? "file_edit" : "diff", path: directPath });
}
return hints;
}
function approvalHint(
eventType: string,
primary: Record<string, unknown>,
nested: Record<string, unknown>,
toolName?: string,
): Record<string, unknown> | null {
if (!eventType.includes("permission")) return null;
const id = readEventField(primary, ["permissionId", "permission_id", "id"])
|| readEventField(nested, ["permissionId", "permission_id", "id"])
|| toolName
|| eventType;
return {
id,
title: `权限确认:${toolName || readEventField(primary, ["title", "message"]) || "工具调用"}`,
status: eventType.endsWith("replied") ? "completed" : "pending",
source: "acp-permission",
payload: primary,
};
}
function eventRecords(data: unknown): Array<Record<string, unknown>> {
const primary = objectRecord(data);
if (!primary) return [];
return [primary, objectRecord(primary.data), objectRecord(primary.toolCall)]
.filter((record): record is Record<string, unknown> => Boolean(record));
}
function objectRecord(value: unknown): Record<string, unknown> | null {
return value && typeof value === "object" && !Array.isArray(value)
? value as Record<string, unknown>
: null;
}
/**