#!/usr/bin/env node const fs = require('fs'); const os = require('os'); const path = require('path'); const { DatabaseSync } = require('node:sqlite'); const spawn = require('cross-spawn'); const { applyWorkspaceContext } = require('../mcp/src/core/runtime-context'); const PACKAGE_ROOT = path.resolve(__dirname, '..'); const { workspaceRoot: PROJECT_ROOT } = applyWorkspaceContext({ packageRoot: PACKAGE_ROOT, changeCwd: false }); function loadEnvFile(filePath) { if (!fs.existsSync(filePath)) return; for (const line of fs.readFileSync(filePath, 'utf8').split(/\r?\n/)) { const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/); if (!match || process.env[match[1]] !== undefined) continue; let value = match[2].trim(); if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { value = value.slice(1, -1); } process.env[match[1]] = value; } } function maskContact(value) { const text = String(value || ''); if (text.length <= 4) return '***'; return `${text.slice(0, 2)}***${text.slice(-2)}`; } function safeLabel(value) { return String(value || '企微客户') .trim() .replace(/[\\/:*?"<>|\r\n]+/g, '-') .replace(/\s+/g, '-') .replace(/-+/g, '-') .replace(/^-|-$/g, '') .slice(0, 24) || '企微客户'; } function parseArgs(argv) { const args = { list: false, dryRun: false, customer: '', index: 0 }; for (let i = 0; i < argv.length; i += 1) { if (argv[i] === '--list') args.list = true; else if (argv[i] === '--dry-run') args.dryRun = true; else if (argv[i] === '--customer') args.customer = String(argv[++i] || '').trim(); else if (argv[i] === '--index') args.index = Number(argv[++i] || 0); else if (argv[i] === '--help' || argv[i] === '-h') args.help = true; } return args; } function readCustomerSessions({ sessionFile, dbPath }) { if (!fs.existsSync(sessionFile)) throw new Error('尚未创建客户 Claude Code Session'); if (!fs.existsSync(dbPath)) throw new Error('企微 Workbench 数据库不存在'); const state = JSON.parse(fs.readFileSync(sessionFile, 'utf8')); const db = new DatabaseSync(dbPath, { readOnly: true }); try { return Object.entries(state.sessions || {}) .filter(([, session]) => session.role === 'customer-agent') .map(([conversationId, session]) => { const conversation = db.prepare('SELECT contact_name, contact_id FROM conversations WHERE id=?').get(conversationId); return { conversationId, session, customerName: conversation?.contact_name || session.customerName || '未命名客户', maskedContact: maskContact(conversation?.contact_id), projectRoot: state.project?.projectRoot || PROJECT_ROOT, }; }) .sort((a, b) => String(a.customerName).localeCompare(String(b.customerName), 'zh-CN')); } finally { db.close(); } } function printList(rows) { if (!rows.length) { process.stdout.write('当前没有客户 Claude Code Session。\n'); return; } process.stdout.write('可查看的客户 Claude Code Session:\n'); rows.forEach((row, index) => { const status = row.session.initialized ? '已有对话' : '尚未初始化'; process.stdout.write(`${index + 1}. ${row.customerName}(${row.maskedContact})· ${status}\n`); }); } function selectSession(rows, args) { if (args.index) return rows[args.index - 1] || null; if (args.customer) { const exact = rows.filter(row => row.customerName === args.customer); if (exact.length === 1) return exact[0]; const fuzzy = rows.filter(row => row.customerName.includes(args.customer)); if (fuzzy.length === 1) return fuzzy[0]; return null; } return rows.length === 1 ? rows[0] : null; } function findTranscript(sessionId) { const projectsRoot = path.join(os.homedir(), '.claude', 'projects'); if (!fs.existsSync(projectsRoot)) return false; const target = `${sessionId}.jsonl`; const queue = [projectsRoot]; while (queue.length) { const current = queue.shift(); for (const entry of fs.readdirSync(current, { withFileTypes: true })) { const fullPath = path.join(current, entry.name); if (entry.isDirectory()) queue.push(fullPath); else if (entry.name === target) return true; } } return false; } function resolveClaudeCommand() { const candidates = [ process.env.CLAUDE_CODE_EXECUTABLE, path.join(process.env.APPDATA || '', 'npm', 'node_modules', '@anthropic-ai', 'claude-code', 'bin', 'claude.exe'), process.platform === 'win32' ? 'claude.cmd' : 'claude', ].filter(Boolean); return candidates.find(candidate => !path.isAbsolute(candidate) || fs.existsSync(candidate)) || candidates.at(-1); } async function main() { loadEnvFile(path.join(PROJECT_ROOT, '.env.local')); const args = parseArgs(process.argv.slice(2)); if (args.help) { process.stdout.write([ '查看客户 Claude Code Session:', ' npm run agent:session:list', ' npm run agent:session -- --customer <客户名称>', '', '打开时会自动 fork 一份审阅会话;查看和追问不会污染生产客户 Session。', '', ].join('\n')); return; } const outputsRoot = path.resolve(process.env.QIWEI_OUTPUTS_DIR || path.join(PROJECT_ROOT, 'outputs')); const sessionFile = path.resolve(process.env.CLAUDE_CODE_SESSION_FILE || path.join(outputsRoot, 'messages', 'claude-code-sessions.json')); const dbPath = path.resolve(process.env.QIWEI_AGENT_DB_PATH || path.join(outputsRoot, 'messages', 'agent-workbench.db')); const rows = readCustomerSessions({ sessionFile, dbPath }); if (args.list) { printList(rows); return; } const selected = selectSession(rows, args); if (!selected) { printList(rows); throw new Error('没有唯一匹配的客户,请通过 --customer <客户名称> 或 --index <序号> 选择'); } if (!selected.session.initialized) throw new Error('该客户 Session 尚未产生 Claude Code 对话'); const reviewName = `审阅-${safeLabel(selected.customerName)}`; const transcriptFound = findTranscript(selected.session.id); if (args.dryRun) { process.stdout.write(`${JSON.stringify({ status: 'ok', customer: selected.customerName, maskedContact: selected.maskedContact, transcriptFound, openMode: 'forked-review', productionSessionProtected: true, }, null, 2)}\n`); return; } if (!transcriptFound) throw new Error('没有找到该客户的本地 Claude Code 会话记录'); process.stdout.write(`正在打开 ${selected.customerName}(${selected.maskedContact})的 Claude Code 审阅会话。\n`); process.stdout.write('系统会先 fork 审阅副本;你在其中查看或追问,不会污染生产客户 Session。\n'); const child = spawn(resolveClaudeCommand(), [ '--resume', selected.session.id, '--fork-session', '--name', reviewName, ], { cwd: fs.existsSync(selected.projectRoot) ? selected.projectRoot : PROJECT_ROOT, stdio: 'inherit', }); child.on('error', error => { process.stderr.write(`无法打开 Claude Code:${error.message}\n`); process.exitCode = 1; }); child.on('exit', code => { process.exitCode = Number(code || 0); }); } if (require.main === module) { main().catch(error => { process.stderr.write(`${error.message}\n`); process.exitCode = 1; }); } module.exports = { maskContact, parseArgs, readCustomerSessions, selectSession };