Files
video-create/web/server/agent/pi-persist.ts
sion123 a6f2973f21 refactor(agent): 迁移AI会话引擎至pi-agent-core库
将原有基于Anthropic/OpenAI SDK的直播聊天代理重构为使用`@earendil-works/pi-agent-core`和`@earendil-works/pi-ai`库的统一API。

新增pi-bridge、pi-model、pi-persist、pi-tools四个模块,封装Agent路由、模型配置、消息持久化和工具适配逻辑。移除`chat.ts`中大量死代码,简化WebSocket处理流程。

BREAKING CHANGE: 移除`VideoAgent`类的`getAnthropicClient`、`getOpenAIClient`、`executeTool`等方法,外部调用需迁移至新pi-bridge API。`PROJECT_ROOT`路径计算方式变更,从`../../..`变为`../../`。
2026-05-08 01:43:33 +08:00

165 lines
5.1 KiB
TypeScript

import { randomUUID } from 'crypto';
import { getDb } from '../db';
import type { Message, AssistantMessage, TextContent, ToolCall } from '@earendil-works/pi-ai';
export interface DbMessage {
id: string;
conversation_id: string;
role: string;
content: string;
tool_calls: string | null;
created_at: string;
}
export function dbToPiMessages(dbMessages: DbMessage[]): Message[] {
const sanitized = sanitizeHistory(dbMessages);
return sanitized.map(dbToPiMessage);
}
function dbToPiMessage(msg: DbMessage): Message {
if (msg.role === 'user') {
return { role: 'user', content: msg.content, timestamp: Date.parse(msg.created_at) || Date.now() };
}
if (msg.role === 'assistant') {
const content: (TextContent | ToolCall)[] = [];
const textContent = msg.content && msg.content !== '(调用工具)' ? msg.content : '';
if (textContent) {
content.push({ type: 'text', text: textContent });
}
if (msg.tool_calls) {
try {
const parsed = JSON.parse(msg.tool_calls);
const blocks = Array.isArray(parsed) ? parsed : parsed.content_blocks;
if (Array.isArray(blocks)) {
for (const b of blocks) {
if (b.type === 'tool_use') {
content.push({
type: 'toolCall',
id: b.id,
name: b.name,
arguments: b.input || b.arguments || {},
});
}
}
}
} catch {}
}
return {
role: 'assistant',
content,
api: 'unknown' as any,
provider: 'unknown',
model: 'unknown',
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
stopReason: 'stop',
timestamp: Date.parse(msg.created_at) || Date.now(),
};
}
if (msg.role === 'tool') {
try {
const { tool_use_id, content: resultContent } = JSON.parse(msg.content);
return {
role: 'toolResult',
toolCallId: tool_use_id,
toolName: '',
content: [{ type: 'text', text: resultContent }],
isError: false,
timestamp: Date.parse(msg.created_at) || Date.now(),
};
} catch {
return {
role: 'toolResult',
toolCallId: 'unknown',
toolName: '',
content: [{ type: 'text', text: msg.content }],
isError: true,
timestamp: Date.parse(msg.created_at) || Date.now(),
};
}
}
return { role: 'user', content: msg.content, timestamp: Date.now() };
}
function sanitizeHistory(messages: DbMessage[]): DbMessage[] {
const cleaned = messages.filter((m) => {
if (m.role === 'assistant' && m.content.startsWith('抱歉,出错了:')) return false;
return true;
});
const result: DbMessage[] = [];
for (const msg of cleaned) {
if (msg.role === 'tool') {
let hasPrecedingToolCall = false;
for (let j = result.length - 1; j >= 0; j--) {
const prev = result[j];
if (prev.role === 'assistant') {
if (prev.tool_calls) {
try {
const parsed = JSON.parse(prev.tool_calls);
const blocks = Array.isArray(parsed) ? parsed : parsed.content_blocks;
if (blocks?.some((b: any) => b.type === 'tool_use')) {
hasPrecedingToolCall = true;
}
} catch {}
}
break;
}
if (prev.role === 'tool') continue;
break;
}
if (!hasPrecedingToolCall) continue;
}
result.push(msg);
}
return result;
}
export function saveUserMessage(convId: string, content: string): string {
const id = randomUUID();
getDb().prepare(
'INSERT INTO messages (id, conversation_id, role, content) VALUES (?, ?, ?, ?)'
).run(id, convId, 'user', content);
return id;
}
export function saveAssistantMessage(convId: string, msg: AssistantMessage): string {
const id = randomUUID();
const textParts = msg.content.filter((c): c is TextContent => c.type === 'text');
const text = textParts.map((c) => c.text).join('');
const toolCalls = msg.content.filter((c): c is ToolCall => c.type === 'toolCall');
if (toolCalls.length > 0) {
const dbToolCalls = toolCalls.map((tc) => ({
type: 'tool_use',
id: tc.id,
name: tc.name,
input: tc.arguments,
}));
const contentBlocks = text ? [{ type: 'text', text }, ...dbToolCalls] : dbToolCalls;
getDb().prepare(
'INSERT INTO messages (id, conversation_id, role, content, tool_calls) VALUES (?, ?, ?, ?, ?)'
).run(id, convId, 'assistant', text || '(调用工具)', JSON.stringify(contentBlocks));
} else {
getDb().prepare(
'INSERT INTO messages (id, conversation_id, role, content) VALUES (?, ?, ?, ?)'
).run(id, convId, 'assistant', text);
}
return id;
}
export function saveToolResult(convId: string, toolCallId: string, toolName: string, result: string, isError: boolean): string {
const id = randomUUID();
getDb().prepare(
'INSERT INTO messages (id, conversation_id, role, content) VALUES (?, ?, ?, ?)'
).run(id, convId, 'tool', JSON.stringify({ tool_use_id: toolCallId, tool_name: toolName, content: result }));
return id;
}