363 lines
11 KiB
TypeScript
363 lines
11 KiB
TypeScript
import type {
|
|
AgentChatMessage,
|
|
AgentChatProgress,
|
|
AgentInteractionToolRef,
|
|
AgentPermissionRequest,
|
|
AgentPermissionStatus,
|
|
AgentQuestionInfo,
|
|
AgentQuestionRequest,
|
|
AgentTodoItem,
|
|
AgentTodoUpdate
|
|
} from "./types";
|
|
|
|
type RecordValue = Record<string, unknown>;
|
|
|
|
export function upsertProgress(progress: AgentChatProgress[] | undefined, data: unknown) {
|
|
const payload = asRecord(data);
|
|
const id = readString(payload.id) ?? `progress-${Date.now().toString(36)}`;
|
|
const now = Date.now();
|
|
const index = progress?.findIndex((item) => item.id === id) ?? -1;
|
|
const existing = index >= 0 ? progress?.[index] : undefined;
|
|
const status = readProgressStatus(payload.status);
|
|
const startedAt = readNumber(payload.started_at) ?? readNumber(payload.startedAt) ?? existing?.startedAt;
|
|
const endedAt = readNumber(payload.ended_at) ?? readNumber(payload.endedAt);
|
|
const elapsedMs = readNumber(payload.elapsed_ms) ?? readNumber(payload.elapsedMs);
|
|
const durationMs = readNumber(payload.duration_ms) ?? readNumber(payload.durationMs);
|
|
const nextItem: AgentChatProgress = {
|
|
id,
|
|
phase: readString(payload.phase) ?? "progress",
|
|
status,
|
|
title: readString(payload.title) ?? "正在处理",
|
|
detail: readString(payload.detail),
|
|
startedAt,
|
|
endedAt,
|
|
elapsedMs: status === "running" ? elapsedMs : undefined,
|
|
elapsedSnapshotAt: status === "running" && elapsedMs !== undefined ? now : undefined,
|
|
durationMs: status === "running" ? undefined : durationMs
|
|
};
|
|
const next = [...(progress ?? [])];
|
|
|
|
if (index >= 0) {
|
|
next[index] = nextItem;
|
|
} else {
|
|
next.push(nextItem);
|
|
}
|
|
|
|
return next;
|
|
}
|
|
|
|
export function completeRunningProgress(progress: AgentChatProgress[] | undefined) {
|
|
return progress?.map((item) => {
|
|
if (item.status !== "running") {
|
|
return item;
|
|
}
|
|
|
|
const endedAt = Date.now();
|
|
return {
|
|
...item,
|
|
status: "completed" as const,
|
|
endedAt,
|
|
elapsedMs: undefined,
|
|
elapsedSnapshotAt: undefined,
|
|
durationMs:
|
|
item.durationMs ??
|
|
(item.startedAt !== undefined ? Math.max(0, endedAt - item.startedAt) : item.elapsedMs)
|
|
};
|
|
});
|
|
}
|
|
|
|
export function toTodoUpdate(data: unknown): AgentTodoUpdate | null {
|
|
const payload = asRecord(data);
|
|
const sessionId = readString(payload.session_id) ?? readString(payload.sessionId);
|
|
const rawTodos = Array.isArray(payload.todos) ? payload.todos : [];
|
|
|
|
if (!sessionId || rawTodos.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
sessionId,
|
|
messageId: readString(payload.message_id) ?? readString(payload.messageId),
|
|
todos: rawTodos.map(toTodoItem).filter((item): item is AgentTodoItem => Boolean(item)),
|
|
createdAt: readNumber(payload.created_at) ?? readNumber(payload.createdAt) ?? Date.now()
|
|
};
|
|
}
|
|
|
|
export function cancelRunningTodos(todoUpdate: AgentTodoUpdate | undefined) {
|
|
return todoUpdate
|
|
? {
|
|
...todoUpdate,
|
|
todos: todoUpdate.todos.map((todo) =>
|
|
todo.status === "pending" || todo.status === "in_progress"
|
|
? {
|
|
...todo,
|
|
status: "cancelled" as const,
|
|
updatedAt: Date.now()
|
|
}
|
|
: todo
|
|
)
|
|
}
|
|
: undefined;
|
|
}
|
|
|
|
export function upsertPermission(
|
|
permissions: AgentPermissionRequest[] | undefined,
|
|
data: unknown
|
|
) {
|
|
const payload = asRecord(data);
|
|
const requestId = readString(payload.request_id) ?? readString(payload.requestId);
|
|
const sessionId = readString(payload.session_id) ?? readString(payload.sessionId);
|
|
|
|
if (!requestId || !sessionId) {
|
|
return permissions;
|
|
}
|
|
|
|
const nextItem: AgentPermissionRequest = {
|
|
requestId,
|
|
sessionId,
|
|
permission: readString(payload.permission) ?? "unknown",
|
|
patterns: readStringArray(payload.patterns),
|
|
target: readString(payload.target),
|
|
always: readStringArray(payload.always),
|
|
tool: toToolRef(payload.tool),
|
|
createdAt: readNumber(payload.created_at) ?? readNumber(payload.createdAt) ?? Date.now(),
|
|
status: "pending"
|
|
};
|
|
const next = [...(permissions ?? [])];
|
|
const index = next.findIndex((permission) => permission.requestId === requestId);
|
|
|
|
if (index >= 0) {
|
|
next[index] = {
|
|
...next[index],
|
|
...nextItem,
|
|
status: next[index].status === "submitting" ? "submitting" : nextItem.status
|
|
};
|
|
} else {
|
|
next.push(nextItem);
|
|
}
|
|
|
|
return next;
|
|
}
|
|
|
|
export function applyPermissionResponse(
|
|
permissions: AgentPermissionRequest[] | undefined,
|
|
data: unknown
|
|
) {
|
|
const payload = asRecord(data);
|
|
const requestId = readString(payload.request_id) ?? readString(payload.requestId);
|
|
if (!requestId) {
|
|
return permissions;
|
|
}
|
|
|
|
return permissions?.map((permission) =>
|
|
permission.requestId === requestId
|
|
? {
|
|
...permission,
|
|
status: toPermissionStatus(readString(payload.reply)),
|
|
repliedAt: Date.now(),
|
|
error: undefined
|
|
}
|
|
: permission
|
|
);
|
|
}
|
|
|
|
export function upsertQuestion(questions: AgentQuestionRequest[] | undefined, data: unknown) {
|
|
const payload = asRecord(data);
|
|
const requestId = readString(payload.request_id) ?? readString(payload.requestId);
|
|
const sessionId = readString(payload.session_id) ?? readString(payload.sessionId);
|
|
const rawQuestions = Array.isArray(payload.questions) ? payload.questions : [];
|
|
|
|
if (!requestId || !sessionId || rawQuestions.length === 0) {
|
|
return questions;
|
|
}
|
|
|
|
const nextItem: AgentQuestionRequest = {
|
|
requestId,
|
|
sessionId,
|
|
questions: rawQuestions.map(toQuestionInfo).filter((item): item is AgentQuestionInfo => Boolean(item)),
|
|
tool: toToolRef(payload.tool),
|
|
createdAt: readNumber(payload.created_at) ?? readNumber(payload.createdAt) ?? Date.now(),
|
|
status: "pending"
|
|
};
|
|
const next = [...(questions ?? [])];
|
|
const index = next.findIndex((question) => isSameQuestion(question, nextItem));
|
|
|
|
if (index >= 0) {
|
|
next[index] = {
|
|
...next[index],
|
|
...nextItem,
|
|
status: next[index].status === "submitting" ? "submitting" : nextItem.status
|
|
};
|
|
} else {
|
|
next.push(nextItem);
|
|
}
|
|
|
|
return next;
|
|
}
|
|
|
|
export function applyQuestionResponse(questions: AgentQuestionRequest[] | undefined, data: unknown) {
|
|
const payload = asRecord(data);
|
|
const requestId = readString(payload.request_id) ?? readString(payload.requestId);
|
|
if (!requestId) {
|
|
return questions;
|
|
}
|
|
|
|
const answers = Array.isArray(payload.answers)
|
|
? payload.answers.map((answer) => (Array.isArray(answer) ? answer.map(String) : []))
|
|
: undefined;
|
|
const rejected = payload.rejected === true;
|
|
|
|
return questions?.map((question) =>
|
|
question.requestId === requestId
|
|
? {
|
|
...question,
|
|
status: rejected ? ("rejected" as const) : ("answered" as const),
|
|
answers: answers ?? question.answers,
|
|
repliedAt: Date.now(),
|
|
error: undefined
|
|
}
|
|
: question
|
|
);
|
|
}
|
|
|
|
export function finalizeAssistantMessageAfterAbort(message: AgentChatMessage): AgentChatMessage {
|
|
const progress = completeRunningProgress(message.progress);
|
|
const todos = cancelRunningTodos(message.todos);
|
|
|
|
return {
|
|
...message,
|
|
content: message.content || "请求已中止。",
|
|
progress,
|
|
todos,
|
|
permissions: message.permissions?.map((permission) =>
|
|
permission.status === "pending" || permission.status === "submitting" || permission.status === "error"
|
|
? {
|
|
...permission,
|
|
status: "aborted" as const,
|
|
repliedAt: Date.now(),
|
|
error: undefined
|
|
}
|
|
: permission
|
|
),
|
|
questions: message.questions?.map((question) =>
|
|
question.status === "pending" || question.status === "submitting" || question.status === "error"
|
|
? {
|
|
...question,
|
|
status: "rejected" as const,
|
|
repliedAt: Date.now(),
|
|
error: undefined
|
|
}
|
|
: question
|
|
)
|
|
};
|
|
}
|
|
|
|
function toPermissionStatus(reply: string | undefined): AgentPermissionStatus {
|
|
if (reply === "always") {
|
|
return "approved_always";
|
|
}
|
|
if (reply === "once") {
|
|
return "approved_once";
|
|
}
|
|
return "rejected";
|
|
}
|
|
|
|
function isSameQuestion(left: AgentQuestionRequest, right: AgentQuestionRequest) {
|
|
if (left.requestId === right.requestId) {
|
|
return true;
|
|
}
|
|
return Boolean(left.tool?.callID && right.tool?.callID && left.tool.callID === right.tool.callID);
|
|
}
|
|
|
|
function toQuestionInfo(value: unknown): AgentQuestionInfo | null {
|
|
const payload = asRecord(value);
|
|
const question = readString(payload.question);
|
|
if (!question) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
header: readString(payload.header) ?? "",
|
|
question,
|
|
options: Array.isArray(payload.options)
|
|
? payload.options
|
|
.map((option) => {
|
|
const item = asRecord(option);
|
|
const label = readString(item.label);
|
|
return label
|
|
? {
|
|
label,
|
|
description: readString(item.description) ?? ""
|
|
}
|
|
: null;
|
|
})
|
|
.filter((item): item is { label: string; description: string } => Boolean(item))
|
|
: [],
|
|
multiple: typeof payload.multiple === "boolean" ? payload.multiple : undefined,
|
|
custom: typeof payload.custom === "boolean" ? payload.custom : undefined
|
|
};
|
|
}
|
|
|
|
function toTodoItem(value: unknown): AgentTodoItem | null {
|
|
const payload = asRecord(value);
|
|
const id = readString(payload.id);
|
|
const content = readString(payload.content);
|
|
const status = readTodoStatus(payload.status);
|
|
|
|
if (!id || !content || !status) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
id,
|
|
content,
|
|
status,
|
|
priority: readTodoPriority(payload.priority),
|
|
createdAt: readNumber(payload.created_at) ?? readNumber(payload.createdAt),
|
|
updatedAt: readNumber(payload.updated_at) ?? readNumber(payload.updatedAt)
|
|
};
|
|
}
|
|
|
|
function toToolRef(value: unknown): AgentInteractionToolRef | undefined {
|
|
const payload = asRecord(value);
|
|
const messageID = readString(payload.messageID);
|
|
const callID = readString(payload.callID);
|
|
return messageID && callID ? { messageID, callID } : undefined;
|
|
}
|
|
|
|
function readProgressStatus(value: unknown): AgentChatProgress["status"] {
|
|
return value === "completed" || value === "error" ? value : "running";
|
|
}
|
|
|
|
function readTodoStatus(value: unknown): AgentTodoItem["status"] | undefined {
|
|
if (value === "pending" || value === "in_progress" || value === "completed" || value === "cancelled") {
|
|
return value;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function readTodoPriority(value: unknown): AgentTodoItem["priority"] | undefined {
|
|
if (value === "low" || value === "medium" || value === "high") {
|
|
return value;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function asRecord(value: unknown): RecordValue {
|
|
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
? (value as RecordValue)
|
|
: {};
|
|
}
|
|
|
|
function readString(value: unknown) {
|
|
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
}
|
|
|
|
function readNumber(value: unknown) {
|
|
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
}
|
|
|
|
function readStringArray(value: unknown) {
|
|
return Array.isArray(value) ? value.map((item) => String(item).trim()).filter(Boolean) : [];
|
|
}
|