将原有基于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`路径计算方式变更,从`../../..`变为`../../`。
165 lines
5.1 KiB
TypeScript
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;
|
|
}
|