| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198 |
- #!/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})当前 Epoch 的 Claude Code 审阅会话。\n`);
- process.stdout.write('系统会先 fork 审阅副本;你在其中查看或追问,不会污染生产客户 Session。长期客户记忆保存在 Workbench DB 中。\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 };
|