feat(client): persist digital governance events
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user