agent-runtime.js 48 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179
  1. const crypto = require('crypto');
  2. const fs = require('fs');
  3. const path = require('path');
  4. const { spawn } = require('child_process');
  5. const { AgentContextBuilder, selectAuthoritativeHistory } = require('./agent-context-builder');
  6. const { planResponse, plannerInstruction } = require('./response-quality-planner');
  7. const { buildRewriteInstruction, verifyResponseQuality } = require('./response-quality-verifier');
  8. const { normalizeQualityChecks, normalizeQualityScore, qualityFallbackReason } = require('./response-quality-contract');
  9. const { planSalesMove } = require('./agent-sales-move-planner');
  10. const {
  11. buildResponseGenerationStrategy,
  12. responseGenerationInstruction,
  13. } = require('./response-generation-strategy');
  14. const RISK_PATTERN = /合同|签约|产权|权属|资质|资格|政策|学区|入学|学位|合规|保证|承诺|最低价|成交价|报价|优惠|贷款|利率|税费|投诉|退款|赔偿|看房|预约|出价|面谈|付款|下单|发票|身份证|银行卡|法律|违约/;
  15. const NO_REPLY_NEEDED_PATTERN = /^(?:收到|行|行的|好|好的|好嘞|明白|明白了|知道了|了解|了解了|谢谢|谢谢你|谢谢您|感谢|ok|okay|嗯+|哦+)$/i;
  16. function knownProfileFields(profile = {}) {
  17. return Object.entries(profile || {})
  18. .filter(([key, value]) => key !== '__evidence' && value !== undefined && value !== null && value !== '')
  19. .map(([key]) => key);
  20. }
  21. function salesMoveInstruction(plan = {}) {
  22. const instructions = {
  23. answer: '先实质回答当前问题,再补必要依据。',
  24. acknowledge_objection: '先接住并回应当前顾虑,不要直接跳到追问。',
  25. clarify: '只追问一个信息增益最高且尚未回答的字段。',
  26. recommend: '基于已知事实给出一个可解释建议,并说明关键限制。',
  27. advance: '给出一个低成本、可执行的下一步,不额外堆叠问题。',
  28. handoff: '明确需要哪类人工角色核验;没有真实交接证据时,不得声称已经转交或正在处理。',
  29. };
  30. return [
  31. `本轮唯一主要沟通动作:${String(plan.primaryMove || 'answer')}`,
  32. instructions[plan.primaryMove] || instructions.answer,
  33. '不要在同一回复中并列多个销售动作;已完成动作和已确认字段不得换一种说法重复执行。',
  34. ].join('\n');
  35. }
  36. function isNoReplyNeededMessage(input = '') {
  37. const parts = String(input || '')
  38. .trim()
  39. .split(/[,,。;;、!!??~~\s]+/)
  40. .map(part => part.trim())
  41. .filter(Boolean);
  42. return Boolean(parts.length && parts.length <= 3 && parts.every(part => NO_REPLY_NEEDED_PATTERN.test(part)));
  43. }
  44. function claudeSessionResetReason(error) {
  45. const detail = `${error?.message || ''} ${error?.subtype || ''}`;
  46. if (/error_max_budget_usd|max[-_\s]?budget[-_\s]?usd/i.test(detail)) return 'budget_exceeded';
  47. if (/session|conversation|resume/i.test(detail)) return 'session_invalid';
  48. return '';
  49. }
  50. function parseClaudeProcessResult(stdout = '', stderr = '', code = 0) {
  51. let payload = null;
  52. try { payload = JSON.parse(String(stdout || '').trim()); } catch {}
  53. if (code === 0) {
  54. return payload
  55. ? { payload }
  56. : { error: `Claude Code 未返回有效 JSON${stderr ? ',请检查 Fmode 配置' : ''}` };
  57. }
  58. const structured = payload?.structured_output;
  59. const usableStructuredOutput = structured
  60. && typeof structured === 'object'
  61. && !Array.isArray(structured)
  62. && Object.prototype.hasOwnProperty.call(structured, 'reply')
  63. && Object.prototype.hasOwnProperty.call(structured, 'confidence')
  64. && Object.prototype.hasOwnProperty.call(structured, 'intent');
  65. const safeError = String(stderr || '')
  66. .replace(/Bearer\s+\S+/gi, 'Bearer [REDACTED]')
  67. .replace(/\bsk-[A-Za-z0-9_-]{6,}\b/g, 'sk-[REDACTED]')
  68. .trim()
  69. .slice(0, 300);
  70. const structuredError = String(payload?.error || payload?.message || payload?.result || payload?.subtype || '').trim().slice(0, 300);
  71. const detail = safeError || structuredError || `退出码 ${code}`;
  72. if (usableStructuredOutput) {
  73. return {
  74. payload: {
  75. ...payload,
  76. is_error: false,
  77. process_warning: { exitCode: code, detail },
  78. },
  79. };
  80. }
  81. return { error: `Claude Code 调用失败(退出码 ${code})${detail ? `:${detail}` : ''}` };
  82. }
  83. class AgentNotConfiguredError extends Error {
  84. constructor() {
  85. super('Agent 模型尚未配置,消息已保留但不会生成伪造回复');
  86. this.name = 'AgentNotConfiguredError';
  87. }
  88. }
  89. class OpenAICompatibleClient {
  90. constructor(config) { this.config = config; }
  91. async complete(messages, tools) {
  92. if (!this.config.apiKey) throw new AgentNotConfiguredError();
  93. const body = {
  94. model: this.config.model,
  95. temperature: 0.2,
  96. messages,
  97. };
  98. if (Array.isArray(tools) && tools.length) {
  99. body.tools = tools;
  100. body.tool_choice = 'auto';
  101. }
  102. const response = await fetch(`${this.config.baseUrl}/chat/completions`, {
  103. method: 'POST',
  104. headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this.config.apiKey}` },
  105. body: JSON.stringify(body),
  106. signal: AbortSignal.timeout(30000),
  107. });
  108. const payload = await response.json().catch(() => ({}));
  109. if (!response.ok) throw new Error(`Agent 上游暂时不可用(HTTP ${response.status})`);
  110. const message = payload.choices?.[0]?.message;
  111. if (!message) throw new Error('Agent 上游没有返回有效消息');
  112. return message;
  113. }
  114. }
  115. class AnthropicCompatibleClient {
  116. constructor(config) { this.config = config; }
  117. toAnthropicMessages(messages) {
  118. return messages.filter(message => message.role !== 'system').map(message => {
  119. if (message.role === 'tool') {
  120. return { role: 'user', content: [{ type: 'tool_result', tool_use_id: message.tool_call_id, content: message.content }] };
  121. }
  122. if (message.role === 'assistant' && message.tool_calls?.length) {
  123. const content = [];
  124. if (message.content) content.push({ type: 'text', text: message.content });
  125. for (const call of message.tool_calls) {
  126. let input = {};
  127. try { input = JSON.parse(call.function.arguments || '{}'); } catch {}
  128. content.push({ type: 'tool_use', id: call.id, name: call.function.name, input });
  129. }
  130. return { role: 'assistant', content };
  131. }
  132. return { role: message.role, content: message.content };
  133. });
  134. }
  135. async complete(messages, tools) {
  136. if (!this.config.apiKey) throw new AgentNotConfiguredError();
  137. const system = messages.find(message => message.role === 'system')?.content || '';
  138. const body = {
  139. model: this.config.model,
  140. max_tokens: 1400,
  141. temperature: 0.2,
  142. system,
  143. messages: this.toAnthropicMessages(messages),
  144. };
  145. if (Array.isArray(tools) && tools.length) {
  146. body.tools = tools.map(tool => ({
  147. name: tool.function.name,
  148. description: tool.function.description,
  149. input_schema: tool.function.parameters,
  150. }));
  151. }
  152. const response = await fetch(`${this.config.baseUrl}/v1/messages`, {
  153. method: 'POST',
  154. headers: {
  155. 'Content-Type': 'application/json',
  156. 'anthropic-version': '2023-06-01',
  157. 'x-api-key': this.config.apiKey,
  158. Authorization: `Bearer ${this.config.apiKey}`,
  159. },
  160. body: JSON.stringify(body),
  161. signal: AbortSignal.timeout(45000),
  162. });
  163. const payload = await response.json().catch(() => ({}));
  164. if (!response.ok) throw new Error(`Agent 上游暂时不可用(HTTP ${response.status})`);
  165. const blocks = Array.isArray(payload.content) ? payload.content : [];
  166. const toolCalls = blocks.filter(block => block.type === 'tool_use').map(block => ({
  167. id: block.id,
  168. function: { name: block.name, arguments: JSON.stringify(block.input || {}) },
  169. }));
  170. return {
  171. content: blocks.filter(block => block.type === 'text').map(block => block.text).join('\n'),
  172. ...(toolCalls.length ? { tool_calls: toolCalls } : {}),
  173. };
  174. }
  175. }
  176. function readJson(filePath, fallback = {}) {
  177. try { return JSON.parse(fs.readFileSync(filePath, 'utf8')); }
  178. catch { return fallback; }
  179. }
  180. function writeJsonAtomic(filePath, value) {
  181. fs.mkdirSync(path.dirname(filePath), { recursive: true });
  182. const tempPath = `${filePath}.${process.pid}.tmp`;
  183. fs.writeFileSync(tempPath, JSON.stringify(value, null, 2), 'utf8');
  184. fs.renameSync(tempPath, filePath);
  185. }
  186. function resolveClaudeExecutable(config = {}) {
  187. const pathCandidates = String(process.env.PATH || '')
  188. .split(path.delimiter)
  189. .map(item => item.trim().replace(/^"|"$/g, ''))
  190. .filter(Boolean)
  191. .flatMap(dir => process.platform === 'win32'
  192. ? [
  193. path.join(dir, 'claude.exe'),
  194. path.join(dir, 'node_modules', '@anthropic-ai', 'claude-code', 'bin', 'claude.exe'),
  195. ]
  196. : [path.join(dir, 'claude')]);
  197. const candidates = [
  198. config.claudeExecutable,
  199. process.env.CLAUDE_CODE_EXECUTABLE,
  200. ...pathCandidates,
  201. path.join(path.dirname(process.execPath), 'node_modules', '@anthropic-ai', 'claude-code', 'bin', 'claude.exe'),
  202. path.join(process.env.APPDATA || '', 'npm', 'node_modules', '@anthropic-ai', 'claude-code', 'bin', 'claude.exe'),
  203. ].filter(Boolean);
  204. return candidates.find(candidate => fs.existsSync(candidate)) || '';
  205. }
  206. function normalizeSessionLabel(value) {
  207. return String(value || '企微客户')
  208. .trim()
  209. .replace(/[\\/:*?"<>|\r\n]+/g, '-')
  210. .replace(/\s+/g, '-')
  211. .replace(/-+/g, '-')
  212. .replace(/^-|-$/g, '')
  213. .slice(0, 24) || '企微客户';
  214. }
  215. function buildClaudeSessionName(context = {}, key = '') {
  216. const customerName = normalizeSessionLabel(
  217. context.conversation?.contact_name || context.conversation?.displayName || '企微客户'
  218. );
  219. const reference = crypto.createHash('sha256').update(String(key || 'qiwei-default')).digest('hex').slice(0, 4);
  220. return `企微客户-${customerName}-${reference}`;
  221. }
  222. function uniqueIntelligence(items = [], keyFn) {
  223. const seen = new Set();
  224. return items.filter(item => {
  225. const key = keyFn(item);
  226. if (!key || seen.has(key)) return false;
  227. seen.add(key);
  228. return true;
  229. });
  230. }
  231. function normalizedEvidence(value) {
  232. return String(value || '').toLowerCase().replace(/[\s,。!?;:、,.!?;:'"“”‘’()()【】\[\]-]+/g, '');
  233. }
  234. function evidenceIsSupported(evidence, authoritativeText) {
  235. const source = normalizedEvidence(authoritativeText);
  236. const claim = normalizedEvidence(evidence);
  237. if (!source || !claim) return false;
  238. return source.includes(claim) || (source.length >= 4 && claim.includes(source));
  239. }
  240. function supportedModelProfileUpdates(updates = {}, authoritativeText = '') {
  241. if (!updates || typeof updates !== 'object' || Array.isArray(updates)) return {};
  242. const explicitNumbers = new Set(String(authoritativeText || '').match(/\d+(?:\.\d+)?/g) || []);
  243. return Object.fromEntries(Object.entries(updates).filter(([, value]) => {
  244. if (value === undefined || value === null || value === '') return false;
  245. const values = Array.isArray(value) ? value : [value];
  246. return values.every(item => typeof item === 'number'
  247. ? explicitNumbers.has(String(item))
  248. : evidenceIsSupported(String(item), authoritativeText));
  249. }));
  250. }
  251. function unsupportedAttributedClaims(final = {}, authoritativeHistory = []) {
  252. const output = `${String(final.reply || '')}\n${String(final.reason || '')}`;
  253. const source = authoritativeHistory.map(item => item.content || '').join('\n');
  254. const claims = [];
  255. const marker = /(?:您|客户)(?:之前|此前|刚才)?(?:曾经)?(?:提到|说过|说|表示)/g;
  256. for (const match of output.matchAll(marker)) {
  257. const tail = output.slice((match.index || 0) + match[0].length, (match.index || 0) + match[0].length + 100);
  258. const quoted = tail.match(/^\s*[::]?\s*["“‘']([^"”’'\r\n]{2,60})["”’']/);
  259. if (quoted && !evidenceIsSupported(quoted[1], source)) claims.push(quoted[1].trim());
  260. }
  261. return [...new Set(claims)];
  262. }
  263. function groundedConfirmationReply(profile = {}, inboundContent = '') {
  264. const budget = Number(profile.budgetWan || profile.budget || 0);
  265. const intent = profile.intent || profile.purpose || profile.need || '';
  266. const confirmed = [intent, budget ? `预算 ${budget}` : '', profile.timeline || ''].filter(Boolean).join('、');
  267. const opening = confirmed ? `收到,我先按${confirmed}继续整理。` : '收到,您刚才的信息我已经记录。';
  268. const questions = [];
  269. if (!intent) questions.push('这次最希望解决的核心问题是什么');
  270. if (budget && (!profile.budgetType || profile.budgetType === '待确认')) questions.push(`预算 ${budget} 是目标值还是上限`);
  271. if (!profile.timeline) questions.push('希望什么时候推进');
  272. if (!questions.length) return `${opening}我会先核对可用方案,再给您准确回复。`;
  273. return `${opening}为了避免理解偏差,想再确认一下:${questions.join(';')}?`;
  274. }
  275. function enforceAuthoritativeGrounding(final = {}, authoritativeHistory = [], profile = {}, inboundContent = '') {
  276. const unsupported = unsupportedAttributedClaims(final, authoritativeHistory);
  277. if (!unsupported.length) return { ...final, groundingWarnings: [] };
  278. return {
  279. ...final,
  280. reply: groundedConfirmationReply(profile, inboundContent),
  281. reason: `检测到模型引用了本轮有效会话中不存在的客户原话,已降级为确认式草稿。未支持内容:${unsupported.join('、')}`,
  282. confidence: Math.min(clamp(final.confidence), 0.68),
  283. requiresHuman: true,
  284. groundingWarnings: unsupported,
  285. };
  286. }
  287. function extractExplicitCustomerIntelligence(content, currentProfile = {}, modelOutput = {}) {
  288. const text = String(content || '').trim();
  289. const profileUpdates = supportedModelProfileUpdates(modelOutput.profileUpdates, text);
  290. const amount = text.match(/(\d+(?:\.\d+)?)\s*(?:万|元)?/);
  291. if (amount && (/预算|费用|报价|价格/.test(text))) {
  292. profileUpdates.budgetWan = Number(amount[1]);
  293. profileUpdates.budgetType = /上限|最多/.test(text) ? '上限' : /目标|大概/.test(text) ? '目标' : (currentProfile.budgetType || '待确认');
  294. }
  295. if (/今天|明天|本周|这周|尽快|马上|急/.test(text)) profileUpdates.urgency = '高';
  296. const mergedProfile = { ...currentProfile, ...profileUpdates };
  297. const factEvidence = [
  298. mergedProfile.intent || mergedProfile.purpose ? `意图${mergedProfile.intent || mergedProfile.purpose}` : '',
  299. mergedProfile.need || mergedProfile.needs ? `需求${mergedProfile.need || mergedProfile.needs}` : '',
  300. mergedProfile.budgetWan || mergedProfile.budget ? `预算${mergedProfile.budgetWan || mergedProfile.budget}` : '',
  301. mergedProfile.timeline ? `计划时间${mergedProfile.timeline}` : '',
  302. mergedProfile.decisionMaker ? `决策人${mergedProfile.decisionMaker}` : '',
  303. ].filter(Boolean).join(',').slice(0, 240);
  304. const evidence = text.slice(0, 240);
  305. const tasks = Array.isArray(modelOutput.tasks)
  306. ? modelOutput.tasks.filter(item => evidenceIsSupported(item?.evidence, text)).map(item => ({
  307. ...item,
  308. businessKey: item.businessKey || item.key,
  309. managedBy: 'agent',
  310. }))
  311. : [];
  312. const alerts = Array.isArray(modelOutput.alerts)
  313. ? modelOutput.alerts.filter(item => evidenceIsSupported(item?.evidence, text)).map(item => ({
  314. ...item,
  315. businessKey: item.businessKey || item.key,
  316. managedBy: 'agent',
  317. }))
  318. : [];
  319. const hasBudget = Number(mergedProfile.budgetWan || mergedProfile.budget || 0) > 0;
  320. const hasPurpose = Boolean(mergedProfile.intent || mergedProfile.purpose || mergedProfile.need || mergedProfile.needs);
  321. const hasTimeline = Boolean(mergedProfile.timeline);
  322. if ((hasBudget || hasPurpose) && (!hasPurpose || !hasTimeline)) {
  323. tasks.push({
  324. businessKey: 'qualification:purpose_and_timeline',
  325. managedBy: 'rule',
  326. type: 'qualification',
  327. title: '确认客户目标与计划时间',
  328. owner: '待分配',
  329. dueAt: '',
  330. priority: 'high',
  331. reason: `${[!hasPurpose ? '客户目标' : '', !hasTimeline ? '计划时间' : ''].filter(Boolean).join('和')}仍不明确。`,
  332. evidence: factEvidence || evidence,
  333. });
  334. }
  335. if (hasPurpose && hasTimeline) {
  336. alerts.push({
  337. businessKey: 'high_intent:core_demand_ready',
  338. managedBy: 'rule',
  339. type: 'high_intent',
  340. severity: 'high',
  341. title: '客户核心需求已基本成形',
  342. detail: '客户目标和计划时间已具备,可以从泛咨询进入下一步服务安排。',
  343. evidence: factEvidence || evidence,
  344. recommendedAction: '核对关键约束并确认下一步动作。',
  345. });
  346. }
  347. if (/投诉|不满意|骗人|退款|举报|再也不|太差|生气/.test(text)) {
  348. alerts.push({
  349. businessKey: 'complaint:manual_takeover',
  350. managedBy: 'event',
  351. type: 'complaint',
  352. severity: 'critical',
  353. title: '检测到投诉或强烈负面情绪',
  354. detail: '该消息不应由自动回复独立处理。',
  355. evidence,
  356. recommendedAction: '立即人工接管,先确认事实和客户诉求。',
  357. });
  358. } else if (/今天|明天|本周|这周|尽快|马上|急/.test(text)) {
  359. alerts.push({
  360. businessKey: 'time_sensitive:follow_up',
  361. managedBy: 'event',
  362. type: 'time_sensitive',
  363. severity: 'high',
  364. title: '客户表达了明确时效要求',
  365. detail: '消息中包含较紧迫的时间表达。',
  366. evidence,
  367. recommendedAction: '优先确认具体截止时间并安排跟进。',
  368. });
  369. }
  370. return {
  371. profileUpdates,
  372. tasks: uniqueIntelligence(tasks, item => `${item?.businessKey || item?.key || ''}|${item?.type || ''}|${item?.title || ''}`.toLowerCase()).slice(0, 8),
  373. alerts: uniqueIntelligence(alerts, item => `${item?.businessKey || item?.key || ''}|${item?.type || ''}|${item?.title || ''}`.toLowerCase()).slice(0, 6),
  374. };
  375. }
  376. class ClaudeCodeSessionStore {
  377. constructor(filePath, project = {}) {
  378. this.filePath = filePath;
  379. this.project = project;
  380. }
  381. loadState() {
  382. const state = readJson(this.filePath, { version: 1, project: {}, sessions: {} });
  383. state.version = 1;
  384. state.sessions ||= {};
  385. state.project ||= {};
  386. state.project.projectId = this.project.projectId || state.project.projectId || 'qiwei-project';
  387. state.project.projectRoot = this.project.projectRoot || state.project.projectRoot || '';
  388. state.project.controllerSessionId = this.project.mainSessionId
  389. || state.project.controllerSessionId
  390. || crypto.randomUUID();
  391. state.project.boundMainSessionId = this.project.mainSessionId || state.project.boundMainSessionId || null;
  392. return state;
  393. }
  394. get(key) {
  395. return this.loadState().sessions?.[key] || null;
  396. }
  397. ensure(key, metadata = {}) {
  398. const state = this.loadState();
  399. if (!state.sessions[key]) {
  400. state.sessions[key] = {
  401. id: crypto.randomUUID(),
  402. initialized: false,
  403. role: 'customer-agent',
  404. projectId: state.project.projectId,
  405. parentControllerSessionId: state.project.controllerSessionId,
  406. epochStartedAt: new Date().toISOString(),
  407. epochTurnCount: 0,
  408. memoryVersion: Number(metadata.memoryVersion || 0),
  409. createdAt: new Date().toISOString(),
  410. };
  411. }
  412. const session = state.sessions[key];
  413. session.role ||= 'customer-agent';
  414. session.projectId ||= state.project.projectId;
  415. session.parentControllerSessionId ||= state.project.controllerSessionId;
  416. if (metadata.customerName) session.customerName = String(metadata.customerName).trim().slice(0, 80);
  417. if (metadata.displayName) session.displayName = String(metadata.displayName).trim().slice(0, 80);
  418. if (metadata.memoryVersion !== undefined) session.latestMemoryVersion = Number(metadata.memoryVersion || 0);
  419. writeJsonAtomic(this.filePath, state);
  420. return session;
  421. }
  422. markInitialized(key, metadata = {}) {
  423. const state = this.loadState();
  424. if (!state.sessions?.[key]) return;
  425. state.sessions[key].initialized = true;
  426. state.sessions[key].epochTurnCount = Number(state.sessions[key].epochTurnCount || 0) + 1;
  427. if (metadata.memoryVersion !== undefined) state.sessions[key].latestMemoryVersion = Number(metadata.memoryVersion || 0);
  428. state.sessions[key].updatedAt = new Date().toISOString();
  429. writeJsonAtomic(this.filePath, state);
  430. }
  431. setRole(key, role) {
  432. const state = this.loadState();
  433. if (!state.sessions?.[key]) return false;
  434. state.sessions[key].role = String(role || 'customer-agent');
  435. state.sessions[key].updatedAt = new Date().toISOString();
  436. writeJsonAtomic(this.filePath, state);
  437. return true;
  438. }
  439. reset(key, metadata = {}) {
  440. const state = this.loadState();
  441. const previous = state.sessions[key] || null;
  442. const epochHistory = previous ? [
  443. ...(previous.epochHistory || []),
  444. {
  445. id: previous.id,
  446. startedAt: previous.epochStartedAt || previous.createdAt,
  447. endedAt: new Date().toISOString(),
  448. turnCount: Number(previous.epochTurnCount || 0),
  449. memoryVersion: Number(previous.memoryVersion || 0),
  450. latestMemoryVersion: Number(previous.latestMemoryVersion || previous.memoryVersion || 0),
  451. closedReason: metadata.closedReason || 'manual_reset',
  452. },
  453. ].slice(-50) : [];
  454. state.sessions[key] = {
  455. id: crypto.randomUUID(),
  456. initialized: false,
  457. role: 'customer-agent',
  458. projectId: state.project.projectId,
  459. parentControllerSessionId: state.project.controllerSessionId,
  460. parentSessionId: metadata.parentSessionId || previous?.id || undefined,
  461. closedReason: metadata.closedReason || undefined,
  462. epochStartedAt: new Date().toISOString(),
  463. epochTurnCount: 0,
  464. memoryVersion: Number(metadata.memoryVersion || 0),
  465. epochHistory,
  466. customerName: metadata.customerName || undefined,
  467. displayName: metadata.displayName || undefined,
  468. createdAt: new Date().toISOString(),
  469. };
  470. writeJsonAtomic(this.filePath, state);
  471. return state.sessions[key];
  472. }
  473. rotateIfNeeded(key, metadata = {}, policy = {}) {
  474. const session = this.ensure(key, metadata);
  475. if (!session.initialized) return { session, reason: '' };
  476. const maxTurns = Number(policy.maxTurns || 0);
  477. const maxAgeMs = Number(policy.maxAgeMs || 0);
  478. const ageMs = Date.now() - Date.parse(session.epochStartedAt || session.createdAt || 0);
  479. const reason = maxTurns > 0 && Number(session.epochTurnCount || 0) >= maxTurns
  480. ? 'epoch_turn_limit'
  481. : maxAgeMs > 0 && Number.isFinite(ageMs) && ageMs >= maxAgeMs
  482. ? 'epoch_age_limit'
  483. : '';
  484. if (!reason) return { session, reason: '' };
  485. return {
  486. session: this.reset(key, { ...metadata, parentSessionId: session.id, closedReason: reason }),
  487. reason,
  488. };
  489. }
  490. }
  491. class ClaudeCodeClient {
  492. constructor(config, contextBuilder = null) {
  493. this.config = config;
  494. this.executable = resolveClaudeExecutable(config);
  495. this.workdir = path.resolve(config.claudeWorkdir || process.cwd());
  496. this.sessionStore = new ClaudeCodeSessionStore(config.claudeSessionFile, {
  497. projectId: config.claudeProjectId,
  498. projectRoot: this.workdir,
  499. mainSessionId: config.claudeMainSessionId,
  500. });
  501. this.queues = new Map();
  502. this.contextBuilder = contextBuilder || new AgentContextBuilder({ config });
  503. }
  504. isConfigured() {
  505. return Boolean(this.executable && fs.existsSync(this.workdir));
  506. }
  507. outputSchema() {
  508. return this.config.outputSchema || {
  509. type: 'object',
  510. additionalProperties: false,
  511. properties: {
  512. reply: { type: 'string' },
  513. confidence: { type: 'number', minimum: 0, maximum: 1 },
  514. intent: { type: 'string' },
  515. reason: { type: 'string' },
  516. requiresHuman: { type: 'boolean' },
  517. profileUpdates: { type: 'object' },
  518. tasks: {
  519. type: 'array',
  520. maxItems: 6,
  521. items: {
  522. type: 'object',
  523. additionalProperties: false,
  524. properties: {
  525. key: { type: 'string' },
  526. type: { type: 'string' },
  527. title: { type: 'string' },
  528. owner: { type: 'string' },
  529. dueAt: { type: 'string' },
  530. priority: { type: 'string', enum: ['low', 'medium', 'high', 'urgent'] },
  531. reason: { type: 'string' },
  532. evidence: { type: 'string' },
  533. },
  534. required: ['type', 'title', 'owner', 'dueAt', 'priority', 'reason', 'evidence'],
  535. },
  536. },
  537. alerts: {
  538. type: 'array',
  539. maxItems: 4,
  540. items: {
  541. type: 'object',
  542. additionalProperties: false,
  543. properties: {
  544. key: { type: 'string' },
  545. type: { type: 'string' },
  546. severity: { type: 'string', enum: ['low', 'medium', 'high', 'critical'] },
  547. title: { type: 'string' },
  548. detail: { type: 'string' },
  549. evidence: { type: 'string' },
  550. recommendedAction: { type: 'string' },
  551. },
  552. required: ['type', 'severity', 'title', 'detail', 'evidence', 'recommendedAction'],
  553. },
  554. },
  555. },
  556. required: ['reply', 'confidence', 'intent', 'reason', 'requiresHuman', 'profileUpdates', 'tasks', 'alerts'],
  557. };
  558. }
  559. sessionKey(context = {}) {
  560. return String(context.conversation?.id || context.conversation?.contact_id || 'qiwei-default');
  561. }
  562. buildPrompt(messages, context = {}) {
  563. return this.contextBuilder.buildClaudePrompt(messages, context);
  564. }
  565. runProcess(args, input = '') {
  566. if (!this.isConfigured()) throw new AgentNotConfiguredError();
  567. return new Promise((resolve, reject) => {
  568. const childEnv = { ...process.env, NO_COLOR: '1' };
  569. if (this.config.claudeBare !== false && childEnv.ANTHROPIC_AUTH_TOKEN) {
  570. childEnv.ANTHROPIC_API_KEY = childEnv.ANTHROPIC_AUTH_TOKEN;
  571. }
  572. const child = spawn(this.executable, args, {
  573. cwd: this.workdir,
  574. env: childEnv,
  575. windowsHide: true,
  576. stdio: ['pipe', 'pipe', 'pipe'],
  577. });
  578. let stdout = '';
  579. let stderr = '';
  580. let finished = false;
  581. const maxBuffer = 8 * 1024 * 1024;
  582. const timer = setTimeout(() => {
  583. if (finished) return;
  584. child.kill('SIGTERM');
  585. reject(new Error('Claude Code 处理超时,已转人工审核'));
  586. }, Number(this.config.claudeTimeoutMs || 120000));
  587. child.stdout.on('data', chunk => {
  588. stdout += chunk.toString('utf8');
  589. if (stdout.length > maxBuffer) child.kill('SIGTERM');
  590. });
  591. child.stderr.on('data', chunk => {
  592. stderr += chunk.toString('utf8');
  593. if (stderr.length > maxBuffer) child.kill('SIGTERM');
  594. });
  595. child.stdin.on('error', () => {});
  596. child.stdin.end(String(input || ''), 'utf8');
  597. child.once('error', error => {
  598. if (finished) return;
  599. finished = true;
  600. clearTimeout(timer);
  601. reject(error);
  602. });
  603. child.once('close', code => {
  604. if (finished) return;
  605. finished = true;
  606. clearTimeout(timer);
  607. const parsed = parseClaudeProcessResult(stdout, stderr, code);
  608. if (parsed.error) reject(new Error(parsed.error));
  609. else resolve(parsed.payload);
  610. });
  611. });
  612. }
  613. async invoke(messages, context, session, options = {}) {
  614. const system = messages.find(message => message.role === 'system')?.content || '';
  615. const businessPrompt = this.buildPrompt(messages, context);
  616. const prompt = `${system}\n\n${businessPrompt}`;
  617. const budgetLimitUsd = Number(options.maxBudgetUsd || this.config.claudeMaxBudgetUsd || 1);
  618. const args = [
  619. '--print',
  620. '--output-format', 'json',
  621. '--permission-mode', 'dontAsk',
  622. ...(this.config.claudeBare !== false ? ['--bare'] : []),
  623. ...(this.config.claudeEffort ? ['--effort', String(this.config.claudeEffort)] : []),
  624. '--tools', String(this.config.claudeTools || 'Read,Glob,Grep'),
  625. '--model', String(this.config.model || 'sonnet'),
  626. '--max-budget-usd', String(budgetLimitUsd),
  627. '--json-schema', JSON.stringify(this.outputSchema()),
  628. '--name', session.displayName || buildClaudeSessionName(context, this.sessionKey(context)),
  629. ];
  630. for (const dir of this.config.claudeAddDirs || []) {
  631. if (dir && fs.existsSync(dir)) args.push('--add-dir', path.resolve(dir));
  632. }
  633. if (session.initialized) args.push('--resume', session.id);
  634. else args.push('--session-id', session.id);
  635. const payload = await this.runProcess(args, prompt);
  636. if (payload.is_error) {
  637. const subtype = String(payload.subtype || 'unknown');
  638. const error = new Error(`Claude Code/Fmode 暂时不可用(${subtype})`);
  639. error.subtype = subtype;
  640. throw error;
  641. }
  642. const structured = payload.structured_output ?? payload.result;
  643. if (structured === undefined || structured === null || structured === '') {
  644. throw new Error('Claude Code 没有返回客服草稿');
  645. }
  646. return {
  647. content: typeof structured === 'string' ? structured : JSON.stringify(structured),
  648. claudeCode: {
  649. model: this.config.model,
  650. sessionName: session.displayName || buildClaudeSessionName(context, this.sessionKey(context)),
  651. durationMs: Number(payload.duration_ms || 0),
  652. costUsd: Number(payload.total_cost_usd || 0),
  653. budgetLimitUsd,
  654. resumed: Boolean(session.initialized),
  655. systemChars: system.length,
  656. businessPromptChars: businessPrompt.length,
  657. promptChars: prompt.length,
  658. ...(payload.process_warning ? { processWarning: payload.process_warning } : {}),
  659. },
  660. };
  661. }
  662. async complete(messages, tools, context = {}) {
  663. const key = this.sessionKey(context);
  664. const metadata = {
  665. customerName: context.conversation?.contact_name || context.conversation?.displayName || '',
  666. displayName: buildClaudeSessionName(context, key),
  667. memoryVersion: Number(context.memoryContext?.snapshot?.version || 0),
  668. };
  669. const previous = this.queues.get(key) || Promise.resolve();
  670. const current = previous.catch(() => {}).then(async () => {
  671. const rotated = this.sessionStore.rotateIfNeeded(key, metadata, {
  672. maxTurns: this.config.claudeSessionMaxTurns,
  673. maxAgeMs: this.config.claudeSessionMaxAgeMs,
  674. });
  675. let session = rotated.session;
  676. try {
  677. const result = await this.invoke(messages, context, session);
  678. this.sessionStore.markInitialized(key, metadata);
  679. if (rotated.reason && result.claudeCode) result.claudeCode.sessionResetReason = rotated.reason;
  680. return result;
  681. } catch (error) {
  682. const resetReason = claudeSessionResetReason(error);
  683. if (resetReason) {
  684. session = this.sessionStore.reset(key, { ...metadata, parentSessionId: session.id, closedReason: resetReason });
  685. const retryMaxBudgetUsd = resetReason === 'budget_exceeded'
  686. ? Number(this.config.claudeRetryMaxBudgetUsd || Math.max(Number(this.config.claudeMaxBudgetUsd || 1) * 2, 3))
  687. : undefined;
  688. const result = await this.invoke(messages, context, session, { maxBudgetUsd: retryMaxBudgetUsd });
  689. this.sessionStore.markInitialized(key, metadata);
  690. if (result.claudeCode) result.claudeCode.sessionResetReason = resetReason;
  691. return result;
  692. }
  693. throw error;
  694. }
  695. });
  696. this.queues.set(key, current);
  697. try { return await current; }
  698. finally { if (this.queues.get(key) === current) this.queues.delete(key); }
  699. }
  700. }
  701. function parseJsonCandidate(value) {
  702. if (value && typeof value === 'object' && !Array.isArray(value)) return value;
  703. const text = String(value || '').trim();
  704. if (!text) return null;
  705. try {
  706. const parsed = JSON.parse(text);
  707. return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
  708. } catch {}
  709. const fenced = [...text.matchAll(/```(?:json)?\s*([\s\S]*?)```/gi)];
  710. for (const match of fenced) {
  711. try {
  712. const parsed = JSON.parse(match[1].trim());
  713. if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) return parsed;
  714. } catch {}
  715. }
  716. for (let start = text.indexOf('{'); start >= 0; start = text.indexOf('{', start + 1)) {
  717. let depth = 0;
  718. let inString = false;
  719. let escaped = false;
  720. for (let index = start; index < text.length; index += 1) {
  721. const char = text[index];
  722. if (inString) {
  723. if (escaped) escaped = false;
  724. else if (char === '\\') escaped = true;
  725. else if (char === '"') inString = false;
  726. continue;
  727. }
  728. if (char === '"') inString = true;
  729. else if (char === '{') depth += 1;
  730. else if (char === '}') {
  731. depth -= 1;
  732. if (depth === 0) {
  733. try {
  734. const parsed = JSON.parse(text.slice(start, index + 1));
  735. if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) return parsed;
  736. } catch {}
  737. break;
  738. }
  739. }
  740. }
  741. }
  742. return null;
  743. }
  744. function parseFinal(content) {
  745. const text = String(content || '').trim();
  746. let parsed = parseJsonCandidate(text);
  747. for (let depth = 0; parsed && depth < 3; depth += 1) {
  748. const wrapped = !Object.prototype.hasOwnProperty.call(parsed, 'reply')
  749. ? parseJsonCandidate(parsed.structured_output) || parseJsonCandidate(parsed.result)
  750. : null;
  751. if (wrapped) {
  752. parsed = wrapped;
  753. continue;
  754. }
  755. const nestedReply = parseJsonCandidate(parsed.reply);
  756. if (!nestedReply || !Object.prototype.hasOwnProperty.call(nestedReply, 'reply')) break;
  757. parsed = { ...parsed, ...nestedReply };
  758. }
  759. if (parsed && Object.prototype.hasOwnProperty.call(parsed, 'reply')) return parsed;
  760. const looksStructured = /^\s*[\[{]/.test(text) || /```(?:json)?/i.test(text) || /["']reply["']\s*:/.test(text);
  761. return {
  762. reply: looksStructured ? '' : text,
  763. confidence: 0.5,
  764. intent: 'unknown',
  765. reason: looksStructured
  766. ? '模型返回的结构化结果无法安全解析,已阻止 JSON 进入客户回复'
  767. : '模型未返回结构化结果,必须人工审核',
  768. requiresHuman: true,
  769. profileUpdates: {},
  770. };
  771. }
  772. function clamp(value) { return Math.max(0, Math.min(1, Number(value) || 0)); }
  773. class QiweiAgentRuntime {
  774. constructor({ config, knowledge, modelClient = null }) {
  775. this.config = config;
  776. this.knowledge = knowledge;
  777. this.contextBuilder = new AgentContextBuilder({ config, knowledge });
  778. this.modelClient = modelClient || (config.provider === 'claude-code'
  779. ? new ClaudeCodeClient(config, this.contextBuilder)
  780. : config.provider === 'anthropic'
  781. ? new AnthropicCompatibleClient(config)
  782. : new OpenAICompatibleClient(config));
  783. }
  784. tools() {
  785. return [
  786. {
  787. type: 'function',
  788. function: {
  789. name: 'search_knowledge',
  790. description: '检索企业规则、FAQ 和沟通 Playbook。',
  791. parameters: {
  792. type: 'object',
  793. properties: { query: { type: 'string' }, limit: { type: 'integer' } },
  794. required: ['query'],
  795. },
  796. },
  797. },
  798. {
  799. type: 'function',
  800. function: {
  801. name: 'get_customer_profile',
  802. description: '读取当前客户画像与标签。',
  803. parameters: { type: 'object', properties: {} },
  804. },
  805. },
  806. ];
  807. }
  808. async executeTool(name, args, context) {
  809. if (name === 'search_knowledge') return this.knowledge.search(args.query, args.limit || 5);
  810. if (name === 'get_customer_profile') return context.profile || { profile: {}, tags: [] };
  811. return { error: `未知工具 ${name}` };
  812. }
  813. async run({ conversation, messages, profile, customerIntelligence = {}, inboundContent, channelType = 'private', directPrompt = '', memoryContext = {}, journeyState = null, completedActions = [] }) {
  814. if (isNoReplyNeededMessage(inboundContent)) {
  815. return {
  816. status: 'no_reply_needed',
  817. noReplyNeeded: true,
  818. content: '',
  819. confidence: 1,
  820. intent: 'acknowledgement',
  821. reason: '客户消息仅包含确认或致谢',
  822. requiresHuman: false,
  823. profileUpdates: {},
  824. tasks: [],
  825. alerts: [],
  826. citations: [],
  827. toolTrace: [],
  828. qualityScore: null,
  829. qualityChecks: [],
  830. qualityPassed: true,
  831. qualityPlan: null,
  832. rewriteCount: 0,
  833. fallbackReason: null,
  834. salesMovePlan: null,
  835. responseGenerationPlan: null,
  836. };
  837. }
  838. const history = messages.slice(-16).map(message => ({
  839. role: message.direction === 'inbound' ? 'user' : 'assistant',
  840. content: message.content,
  841. }));
  842. const baseSystem = this.contextBuilder.buildSystemContext({ channelType, customerIntelligence, memoryContext });
  843. const llmMessages = [{ role: 'system', content: baseSystem }, ...history];
  844. const toolTrace = [];
  845. const citations = [];
  846. const currentProfile = profile?.profile || profile || {};
  847. let responsePlan = null;
  848. let salesMovePlan = null;
  849. let responseGenerationPlan = null;
  850. const refreshTurnPlan = () => {
  851. responsePlan = planResponse({
  852. inboundContent,
  853. messages: history,
  854. profile: currentProfile,
  855. customerIntelligence,
  856. toolTrace,
  857. citations,
  858. businessGoal: this.config.businessGoal,
  859. stagePlaybook: this.config.stagePlaybook,
  860. });
  861. const requiredFields = Array.isArray(this.config.requiredProfileFields) ? this.config.requiredProfileFields : [];
  862. const knownFields = knownProfileFields(currentProfile);
  863. const known = new Set(knownFields.map(String));
  864. const confirmedJourneyActions = (journeyState?.appliedActions || []).filter(action => action.status === 'confirmed');
  865. salesMovePlan = planSalesMove({
  866. scenario: responsePlan.scenario,
  867. stage: journeyState?.currentStageId || '',
  868. riskLevel: responsePlan.requireHumanHandoff ? 'high' : 'none',
  869. riskTypes: responsePlan.riskTypes,
  870. requiresHuman: responsePlan.requireHumanHandoff,
  871. knownFields,
  872. missingFields: requiredFields.filter(field => !known.has(String(field))),
  873. completedActions: [
  874. ...completedActions,
  875. ...confirmedJourneyActions.flatMap(action => [action.key, ...(action.payload?.guidelineIds || [])]),
  876. ],
  877. guidelines: this.config.salesGuidelines || [],
  878. });
  879. responseGenerationPlan = buildResponseGenerationStrategy({
  880. inboundContent,
  881. responsePlan,
  882. salesMovePlan,
  883. profile: currentProfile,
  884. citations,
  885. toolTrace,
  886. });
  887. const moveInstruction = salesMoveInstruction(salesMovePlan);
  888. llmMessages[0] = {
  889. role: 'system',
  890. content: `${baseSystem}\n\n【本轮回复计划】\n${plannerInstruction(responsePlan)}\n${responseGenerationInstruction(responseGenerationPlan)}\n${moveInstruction}`,
  891. };
  892. return moveInstruction;
  893. };
  894. for (let round = 0; round < this.config.maxToolRounds; round += 1) {
  895. const moveInstruction = refreshTurnPlan();
  896. const assistant = await this.modelClient.complete(llmMessages, this.tools(), {
  897. conversation,
  898. profile,
  899. customerIntelligence,
  900. inboundContent,
  901. channelType,
  902. directPrompt,
  903. memoryContext,
  904. businessGoal: this.config.businessGoal,
  905. stagePlaybook: this.config.stagePlaybook,
  906. salesMoveInstruction: moveInstruction,
  907. });
  908. if (assistant.claudeCode) {
  909. toolTrace.push({
  910. tool: 'claude_code_session',
  911. args: { provider: 'Fmode Studio', model: assistant.claudeCode.model },
  912. result: {
  913. sessionName: assistant.claudeCode.sessionName,
  914. durationMs: assistant.claudeCode.durationMs,
  915. costUsd: assistant.claudeCode.costUsd,
  916. budgetLimitUsd: assistant.claudeCode.budgetLimitUsd,
  917. resumed: assistant.claudeCode.resumed,
  918. systemChars: assistant.claudeCode.systemChars,
  919. businessPromptChars: assistant.claudeCode.businessPromptChars,
  920. promptChars: assistant.claudeCode.promptChars,
  921. ...(assistant.claudeCode.processWarning ? { processWarning: assistant.claudeCode.processWarning } : {}),
  922. ...(assistant.claudeCode.sessionResetReason ? { sessionResetReason: assistant.claudeCode.sessionResetReason } : {}),
  923. },
  924. });
  925. }
  926. if (assistant.tool_calls?.length) {
  927. llmMessages.push({ role: 'assistant', content: assistant.content || '', tool_calls: assistant.tool_calls });
  928. for (const call of assistant.tool_calls) {
  929. let args = {};
  930. try { args = JSON.parse(call.function.arguments || '{}'); } catch {}
  931. const result = await this.executeTool(call.function.name, args, { conversation, profile, customerIntelligence });
  932. toolTrace.push({ tool: call.function.name, args, result });
  933. if (call.function.name === 'search_knowledge') {
  934. for (const item of result) citations.push({ id: item.id, source: item.source, heading: item.heading });
  935. }
  936. llmMessages.push({ role: 'tool', tool_call_id: call.id, content: JSON.stringify(result) });
  937. }
  938. continue;
  939. }
  940. const authoritativeHistory = selectAuthoritativeHistory(history, 10);
  941. const assess = content => {
  942. const parsed = parseFinal(content);
  943. const grounded = enforceAuthoritativeGrounding(parsed, authoritativeHistory, currentProfile, inboundContent);
  944. const quality = verifyResponseQuality({
  945. reply: grounded.reply,
  946. inboundContent,
  947. messages: history,
  948. profile: currentProfile,
  949. customerIntelligence,
  950. citations,
  951. toolTrace,
  952. plan: responsePlan,
  953. requiresHuman: Boolean(grounded.requiresHuman),
  954. passScore: this.config.qualityPassScore,
  955. });
  956. return { final: grounded, quality };
  957. };
  958. let candidate = assess(assistant.content);
  959. let rewriteCount = 0;
  960. toolTrace.push({
  961. tool: 'response_quality_gate',
  962. args: { phase: 'evaluate', version: candidate.quality.version },
  963. result: {
  964. passed: candidate.quality.passed,
  965. score: candidate.quality.score,
  966. failedCheckIds: candidate.quality.failedCheckIds,
  967. hardFailureIds: candidate.quality.hardFailureIds,
  968. },
  969. });
  970. if (!candidate.quality.passed) {
  971. rewriteCount = 1;
  972. const qualityRepair = buildRewriteInstruction(candidate.quality);
  973. const repairMessages = llmMessages.map((message, index) => index === 0 && message.role === 'system'
  974. ? { ...message, content: `${message.content}\n\n【生成后质量修订】\n${qualityRepair}` }
  975. : message);
  976. try {
  977. const revised = await this.modelClient.complete(repairMessages, [], {
  978. conversation,
  979. profile,
  980. customerIntelligence,
  981. inboundContent,
  982. channelType,
  983. directPrompt,
  984. memoryContext,
  985. qualityRepair,
  986. rewriteAttempt: 1,
  987. businessGoal: this.config.businessGoal,
  988. stagePlaybook: this.config.stagePlaybook,
  989. salesMoveInstruction: salesMoveInstruction(salesMovePlan),
  990. });
  991. if (revised.claudeCode) {
  992. toolTrace.push({
  993. tool: 'claude_code_session',
  994. args: { provider: 'Fmode Studio', model: revised.claudeCode.model, phase: 'quality_rewrite' },
  995. result: {
  996. sessionName: revised.claudeCode.sessionName,
  997. durationMs: revised.claudeCode.durationMs,
  998. costUsd: revised.claudeCode.costUsd,
  999. budgetLimitUsd: revised.claudeCode.budgetLimitUsd,
  1000. resumed: revised.claudeCode.resumed,
  1001. systemChars: revised.claudeCode.systemChars,
  1002. businessPromptChars: revised.claudeCode.businessPromptChars,
  1003. promptChars: revised.claudeCode.promptChars,
  1004. },
  1005. });
  1006. }
  1007. candidate = assess(revised.content);
  1008. if (!candidate.quality.passed) {
  1009. candidate.quality.fallbackReason = qualityFallbackReason({
  1010. qualityPassed: false,
  1011. rewriteCount,
  1012. });
  1013. }
  1014. } catch (error) {
  1015. const rewriteCheck = {
  1016. id: 'rewrite_failed',
  1017. passed: false,
  1018. weight: 0,
  1019. hard: true,
  1020. detail: '质量重写执行失败',
  1021. };
  1022. candidate.quality = {
  1023. ...candidate.quality,
  1024. passed: false,
  1025. failedCheckIds: [...new Set([...candidate.quality.failedCheckIds, 'rewrite_failed'])],
  1026. hardFailureIds: [...new Set([...(candidate.quality.hardFailureIds || []), 'rewrite_failed'])],
  1027. checks: [...(candidate.quality.checks || []), rewriteCheck],
  1028. fallbackReason: qualityFallbackReason({ qualityPassed: false, rewriteCount }),
  1029. };
  1030. toolTrace.push({
  1031. tool: 'response_quality_rewrite',
  1032. args: { phase: 'recover' },
  1033. result: { status: 'failed', errorType: error?.name || 'Error' },
  1034. });
  1035. }
  1036. toolTrace.push({
  1037. tool: 'response_quality_gate',
  1038. args: { phase: 'rewrite_evaluate', version: candidate.quality.version },
  1039. result: {
  1040. passed: candidate.quality.passed,
  1041. score: candidate.quality.score,
  1042. failedCheckIds: candidate.quality.failedCheckIds,
  1043. hardFailureIds: candidate.quality.hardFailureIds,
  1044. },
  1045. });
  1046. }
  1047. const final = candidate.final;
  1048. toolTrace.push({
  1049. tool: 'sales_move_planner',
  1050. args: { version: salesMovePlan.version, stage: journeyState?.currentStageId || null },
  1051. result: {
  1052. primaryMove: salesMovePlan.primaryMove,
  1053. reason: salesMovePlan.reason,
  1054. appliedGuidelineIds: salesMovePlan.appliedGuidelines.map(item => item.id),
  1055. },
  1056. });
  1057. toolTrace.push({
  1058. tool: 'response_generation_plan',
  1059. args: { version: responseGenerationPlan.version },
  1060. result: {
  1061. directAnswer: responseGenerationPlan.directAnswer,
  1062. judgment: responseGenerationPlan.judgment,
  1063. singleNextStep: responseGenerationPlan.singleNextStep,
  1064. },
  1065. });
  1066. const intelligence = extractExplicitCustomerIntelligence(inboundContent, currentProfile, final);
  1067. const risky = RISK_PATTERN.test(inboundContent) || RISK_PATTERN.test(final.reply || '');
  1068. const confidence = risky ? Math.min(clamp(final.confidence), 0.75) : clamp(final.confidence);
  1069. const requiresHuman = Boolean(final.requiresHuman || responsePlan.requireHumanHandoff || risky || confidence < 0.7 || !candidate.quality.passed);
  1070. return {
  1071. content: String(final.reply || '').trim(),
  1072. confidence,
  1073. intent: String(final.intent || 'unknown'),
  1074. reason: String(final.reason || 'Agent 未提供说明'),
  1075. requiresHuman,
  1076. profileUpdates: intelligence.profileUpdates,
  1077. tasks: intelligence.tasks,
  1078. alerts: intelligence.alerts,
  1079. citations: [...new Map(citations.map(item => [item.id, item])).values()],
  1080. toolTrace,
  1081. qualityScore: normalizeQualityScore(candidate.quality.score),
  1082. qualityChecks: normalizeQualityChecks(candidate.quality.checks),
  1083. qualityPassed: candidate.quality.passed,
  1084. qualityPlan: candidate.quality.plan,
  1085. rewriteCount,
  1086. fallbackReason: qualityFallbackReason({
  1087. qualityAvailable: candidate.quality?.score !== null && candidate.quality?.score !== undefined,
  1088. qualityPassed: candidate.quality.passed,
  1089. rewriteCount,
  1090. requiresHuman,
  1091. }),
  1092. salesMovePlan,
  1093. responseGenerationPlan,
  1094. };
  1095. }
  1096. throw new Error('Agent 工具调用轮次超过上限,已转人工处理');
  1097. }
  1098. }
  1099. module.exports = {
  1100. AgentNotConfiguredError,
  1101. OpenAICompatibleClient,
  1102. AnthropicCompatibleClient,
  1103. ClaudeCodeClient,
  1104. ClaudeCodeSessionStore,
  1105. buildClaudeSessionName,
  1106. claudeSessionResetReason,
  1107. parseClaudeProcessResult,
  1108. parseFinal,
  1109. selectAuthoritativeHistory,
  1110. enforceAuthoritativeGrounding,
  1111. extractExplicitCustomerIntelligence,
  1112. isNoReplyNeededMessage,
  1113. resolveClaudeExecutable,
  1114. QiweiAgentRuntime,
  1115. };