| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151 |
- 'use strict';
- const assert = require('assert/strict');
- const fs = require('fs');
- const os = require('os');
- const path = require('path');
- const {
- detectThinRedLines,
- detectClaimedPastAction,
- } = require('../mcp/src/core/thin-red-lines');
- const {
- ClaudeCodeClient,
- claudeSessionResetReason,
- sessionFirstMode,
- sessionFirstSystemPrompt,
- applyThinRedLines,
- QiweiAgentRuntime,
- } = require('../mcp/src/core/agent-runtime');
- const { enforceHumanReplyStyle } = require('../mcp/src/core/response-human-style');
- function fakeClient(reply) {
- return {
- async complete() {
- return {
- content: JSON.stringify({
- reply,
- confidence: 0.9,
- intent: 'consult',
- reason: 'fixture',
- requiresHuman: false,
- profileUpdates: {},
- tasks: [],
- alerts: [],
- }),
- };
- },
- };
- }
- async function runOnce(conversationMode, reply, inbound = '怎么收费') {
- const runtime = new QiweiAgentRuntime({
- config: {
- conversationMode,
- maxToolRounds: 2,
- qualityPassScore: 82,
- },
- knowledge: { search: () => [], knowledgeIndex: () => 'knowledge/faq.md(收费)' },
- modelClient: fakeClient(reply),
- });
- return runtime.run({
- conversation: { id: 'c1', contact_id: 'u1' },
- messages: [{ direction: 'inbound', content: inbound }],
- profile: {},
- inboundContent: inbound,
- });
- }
- async function main() {
- assert.equal(sessionFirstMode({ conversationMode: 'session' }), true);
- assert.equal(sessionFirstMode({}), false);
- const system = sessionFirstSystemPrompt({ knowledge: { knowledgeIndex: () => 'knowledge/faq.md' } });
- assert.match(system, /行业通用客服/);
- assert.doesNotMatch(system, /22 问|房源|小牛看房|【本轮回复计划】/);
- const promised = '费用要按服务范围确认。我先帮您核实好再回复您。';
- const thickRewrite = enforceHumanReplyStyle({ reply: promised, reason: 'fixture' }, '怎么收费', {}, {});
- assert.notEqual(thickRewrite.reply, promised, '厚路径应当改写无凭据承诺');
- const sessionOut = await runOnce('session', promised);
- assert.equal(sessionOut.content, promised, 'session 模式不得改稿');
- assert.equal(sessionOut.rewriteCount, 0);
- assert.equal(sessionOut.qualityPassed, true);
- assert.equal(sessionOut.qualityPlan.version, 'session-thin-1.0');
- assert.equal(sessionOut.toolTrace.some(item => item.tool === 'response_quality_gate'), false);
- assert.equal(sessionOut.toolTrace.some(item => item.tool === 'sales_move_planner'), false);
- const claimed = '资料已经发给您了,您先看。';
- const flagged = applyThinRedLines({ reply: claimed, requiresHuman: false, reason: 'ok' }, {});
- assert.equal(flagged.reply, claimed);
- assert.equal(flagged.requiresHuman, true);
- assert.match(flagged.reason, /thin_redline:claimed_past_action/);
- const claimedOut = await runOnce('session', claimed, '资料发了吗');
- assert.equal(claimedOut.content, claimed);
- assert.equal(claimedOut.requiresHuman, true);
- const negated = '我不能空口说已经办好了,得核一下进度再回你。';
- assert.equal(detectThinRedLines(negated, {}).length, 0);
- assert.equal(detectClaimedPastAction(negated, {}).length, 0);
- const questioned = '已经发货了吗?';
- assert.equal(detectThinRedLines(questioned, {}).length, 0);
- const withTool = detectThinRedLines('系统里看了,流程在 knowledge/faq.md。', {
- toolTrace: [{ tool: 'search_knowledge', executed: true }],
- });
- assert.equal(withTool.length, 0);
- const thickOut = await runOnce('', promised);
- assert.notEqual(thickOut.content, promised, '未开 session 时厚路径仍应改写');
- assert.equal(thickOut.toolTrace.some(item => item.tool === 'response_quality_gate'), true);
- const sessionDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'qiwei-session-reset-'));
- const sessionFile = path.join(sessionDirectory, 'sessions.json');
- try {
- const client = new ClaudeCodeClient({
- claudeExecutable: process.execPath,
- claudeWorkdir: process.cwd(),
- claudeSessionFile: sessionFile,
- claudeProjectId: 'session-reset-smoke',
- claudeSessionMaxTurns: 12,
- claudeSessionMaxAgeMs: 86400000,
- });
- const key = 'session-reset-contact';
- client.sessionStore.ensure(key, {});
- client.sessionStore.markInitialized(key, {});
- const originalId = client.sessionStore.get(key).id;
- const invokedSessionIds = [];
- client.invoke = async (_messages, _context, session) => {
- invokedSessionIds.push(session.id);
- if (invokedSessionIds.length === 1) throw new Error('Claude Code 处理超时');
- return { content: '{}', claudeCode: {} };
- };
- await assert.rejects(
- client.complete([], [], { conversation: { id: key } }),
- /处理超时/,
- );
- const reset = client.sessionStore.get(key);
- assert.equal(claudeSessionResetReason(new Error('Claude Code 处理超时')), 'session_timeout');
- assert.equal(invokedSessionIds.length, 1, '超时会话应立即结束本轮,避免重复等待');
- assert.equal(invokedSessionIds[0], originalId);
- assert.notEqual(reset.id, originalId);
- assert.equal(reset.initialized, false);
- assert.equal(reset.epochHistory.at(-1).closedReason, 'session_timeout');
- await client.complete([], [], { conversation: { id: key } });
- assert.equal(invokedSessionIds.length, 2, '下一条消息必须使用重建后的新会话');
- assert.equal(invokedSessionIds[1], reset.id);
- const repaired = client.sessionStore.get(key);
- assert.equal(repaired.initialized, true);
- } finally {
- fs.rmSync(sessionDirectory, { recursive: true, force: true });
- }
- process.stdout.write('session-first smoke ok\n');
- }
- main().catch((error) => {
- process.stderr.write(`${error.stack || error.message}\n`);
- process.exit(1);
- });
|