test: 暂存代码

This commit is contained in:
电摇小子
2026-08-06 20:29:25 +08:00
parent 307d247660
commit ad4b3a8074
43 changed files with 9978 additions and 512 deletions
+185
View File
@@ -0,0 +1,185 @@
import type { AiSearchAgentToolName, AiSearchAgentTraceItem } from '../../shared/ai-search'
export const MAX_AGENT_TOOL_CALLS = 5
export type AgentAction =
| { action: 'tool'; tool: AiSearchAgentToolName; arguments: Record<string, unknown> }
| { action: 'finalize'; reason: string }
export interface AgentToolResult {
summary: Record<string, unknown>
candidateCount: number
/** A host-owned coverage signal, never supplied by the model. */
finalizeReason?: string
}
export interface ControlledSearchAgentOptions {
question: string
scopeLabel: string
rangeLabel: string
maxToolCalls?: number
decide: (prompt: string) => Promise<string | undefined>
execute: (action: Extract<AgentAction, { action: 'tool' }>) => Promise<AgentToolResult>
onTrace: (item: Omit<AiSearchAgentTraceItem, 'sequence'>) => void
}
export interface ControlledSearchAgentResult {
status: 'finalized' | 'exhausted' | 'invalid'
toolCalls: number
reason: string
}
const TOOL_NAMES = new Set<AiSearchAgentToolName>([
'search_conversations',
'search_people',
'search_messages',
'get_conversation_messages',
'get_messages_by_time',
'get_message_context'
])
const parseAction = (value: string | undefined): AgentAction | null => {
if (!value) return null
const match = value.match(/\{[\s\S]*\}/)
if (!match) return null
try {
const parsed = JSON.parse(match[0]) as Record<string, unknown>
if (parsed.action === 'finalize' && typeof parsed.reason === 'string' && parsed.reason.trim()) {
return { action: 'finalize', reason: parsed.reason.trim().slice(0, 240) }
}
if (
parsed.action === 'tool' &&
typeof parsed.tool === 'string' &&
TOOL_NAMES.has(parsed.tool as AiSearchAgentToolName) &&
parsed.arguments &&
typeof parsed.arguments === 'object' &&
!Array.isArray(parsed.arguments)
) {
return {
action: 'tool',
tool: parsed.tool as AiSearchAgentToolName,
arguments: parsed.arguments as Record<string, unknown>
}
}
} catch {
// Invalid model output is rejected by the caller and triggers legacy fallback.
}
return null
}
const agentSystemPrompt = (
question: string,
scopeLabel: string,
rangeLabel: string
): string => `你是 WechatExplorer 的受控本地聊天搜索代理,只负责决定下一步检索,不回答用户问题。
用户问题:${question}
允许范围:${scopeLabel};时间范围:${rangeLabel}
你只能输出一个 JSON 对象,不能输出 Markdown、解释、代码、SQL、文件路径或任何系统操作。
唯一合法格式:
{"action":"tool","tool":"search_people|search_conversations|search_messages|get_conversation_messages|get_messages_by_time|get_message_context","arguments":{...}}
或:
{"action":"finalize","reason":"已有足够证据"}
规则:
- 只能使用此前 Tool 返回的 conversationRef/messageRef;不得猜测或创建引用。
- 问“我和某人最近聊了什么”时,优先 search_people 或 search_conversations,再 get_conversation_messages;不要把联系人名当消息关键词。
- 搜索会话没有结果时,可根据结果自行尝试更短或更自然的名称表达,但最多五次 Tool 调用。
- Tool 结果不足时可以改 Tool 或查询策略;结果充分时 finalize。
- 不要请求全部聊天记录;遵守 Tool 返回的受限结果。`
const traceArguments = (
argumentsValue: Record<string, unknown>
): Record<string, string | number | boolean> => {
const result: Record<string, string | number | boolean> = {}
if (typeof argumentsValue.query === 'string') result.query = argumentsValue.query.slice(0, 80)
if (typeof argumentsValue.limit === 'number') result.limit = argumentsValue.limit
if (typeof argumentsValue.startTime === 'number') result.startTime = argumentsValue.startTime
if (typeof argumentsValue.endTime === 'number') result.endTime = argumentsValue.endTime
if (typeof argumentsValue.conversationRef === 'string') result.target = '已选择会话'
if (typeof argumentsValue.messageRef === 'string') result.context = '已选择消息'
return result
}
export async function runControlledSearchAgent(
options: ControlledSearchAgentOptions
): Promise<ControlledSearchAgentResult> {
let toolCalls = 0
let previousResult = '尚未执行 Tool。'
options.onTrace({ event: 'agentStart', label: '开始规划本次本地检索' })
const maxToolCalls = options.maxToolCalls || MAX_AGENT_TOOL_CALLS
while (toolCalls < maxToolCalls) {
const decisionStartedAt = Date.now()
const decisionInput = `${agentSystemPrompt(options.question, options.scopeLabel, options.rangeLabel)}\n\n上一次 Tool 结果:${previousResult}`
const output = await options.decide(decisionInput)
const decisionElapsedMs = Date.now() - decisionStartedAt
const action = parseAction(output)
if (!action) return { status: 'invalid', toolCalls, reason: 'Agent 返回的控制协议无效' }
if (action.action === 'finalize') {
options.onTrace({
event: 'agentDecision',
label: 'Agent 判断现有结果足够',
decision: action.reason,
decisionInput: decisionInput.slice(0, 8_000),
elapsedMs: decisionElapsedMs
})
return { status: 'finalized', toolCalls, reason: action.reason }
}
options.onTrace({
event: 'agentDecision',
label: 'Agent 选择下一次检索',
toolName: action.tool,
elapsedMs: decisionElapsedMs,
decisionInput: decisionInput.slice(0, 8_000)
})
toolCalls += 1
options.onTrace({
event: 'toolCallStart',
label: '正在执行本地检索',
toolName: action.tool,
arguments: traceArguments(action.arguments)
})
const toolStartedAt = Date.now()
try {
const result = await options.execute(action)
const elapsedMs = Date.now() - toolStartedAt
options.onTrace({
event: 'toolCallEnd',
label: '本地检索完成',
toolName: action.tool,
resultCount: result.candidateCount,
elapsedMs
})
previousResult = JSON.stringify(result.summary)
if (result.finalizeReason) {
options.onTrace({
event: 'agentDecision',
label: '本地资料已覆盖所选时间范围,可直接整理回答',
decision: result.finalizeReason,
elapsedMs: 0
})
return { status: 'finalized', toolCalls, reason: result.finalizeReason }
}
} catch (error) {
const elapsedMs = Date.now() - toolStartedAt
const message = error instanceof Error ? error.message : '本次本地检索不可用'
options.onTrace({
event: 'toolCallEnd',
label: '本地检索未返回结果',
toolName: action.tool,
resultCount: 0,
elapsedMs,
decision: message.slice(0, 160)
})
previousResult = JSON.stringify({ error: message.slice(0, 160), results: [] })
}
}
options.onTrace({
event: 'agentDecision',
label: '已达到本次检索上限',
decision: `最多允许 ${maxToolCalls} 次本地检索`
})
return { status: 'exhausted', toolCalls, reason: '已达到本次检索上限' }
}