| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138 |
- const fs = require('fs');
- const path = require('path');
- const crypto = require('crypto');
- const { latestPath } = require('../core/output-paths');
- const { AgentWorkbenchDb } = require('../core/agent-workbench-db');
- const { AgentKnowledgeStore } = require('../core/agent-knowledge');
- const { QiweiAgentRuntime, extractExplicitCustomerIntelligence } = require('../core/agent-runtime');
- const { getCustomerSessionGuide } = require('../core/agent-session-guide');
- const { AgentWorkbenchService } = require('../core/agent-workbench-service');
- const {
- searchTodoUsers,
- createTodoKnowledge,
- completeTodoKnowledge,
- } = require('./official-office-knowledge-service');
- const { createCustomerTaskOfficialSync } = require('../core/customer-task-official-sync');
- const { messageTimestamp, evaluatePolledMessage } = require('../core/agent-poller-policy');
- const { setActiveQiweiContext } = require('../core/credentials');
- const { FmodeQiweiClient } = require('../providers/fmode-agent-transport');
- const PROJECT_ROOT = path.resolve(__dirname, '..', '..', '..');
- const ENV_FILE = path.join(PROJECT_ROOT, '.env.local');
- function readEnvFile(filePath) {
- try {
- const env = {};
- for (const rawLine of fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, '').split(/\r?\n/)) {
- const line = rawLine.trim();
- if (!line || line.startsWith('#') || !line.includes('=')) continue;
- const index = line.indexOf('=');
- const key = line.slice(0, index).trim();
- let value = line.slice(index + 1).trim();
- if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) value = value.slice(1, -1);
- env[key] = value;
- }
- return env;
- } catch {
- return {};
- }
- }
- function readClaudeSettingsEnv() {
- const result = {};
- const home = process.env.USERPROFILE || process.env.HOME || '';
- for (const filePath of [path.join(home, '.claude', 'settings.json'), path.join(home, '.claude', 'settings.local.json')]) {
- try {
- const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, ''));
- for (const [key, item] of Object.entries(parsed.env || {})) {
- if (!result[key] && typeof item === 'string' && item.trim()) result[key] = item.trim();
- }
- } catch {}
- }
- return result;
- }
- const fileEnv = readEnvFile(ENV_FILE);
- const claudeEnv = readClaudeSettingsEnv();
- function value(name, fallback = '') {
- const candidates = [process.env[name], fileEnv[name], claudeEnv[name], fallback];
- return String(candidates.find(item => typeof item === 'string' && item.trim()) ?? '').trim();
- }
- function bool(name, fallback = false) {
- return /^(1|true|yes|on)$/i.test(value(name, fallback ? 'true' : 'false'));
- }
- function number(name, fallback, min = -Infinity, max = Infinity) {
- const parsed = Number(value(name, String(fallback)));
- return Math.min(max, Math.max(min, Number.isFinite(parsed) ? parsed : fallback));
- }
- function resolvePath(input, fallback) {
- const selected = input || fallback;
- return selected ? path.resolve(PROJECT_ROOT, selected) : '';
- }
- function loadAgentConfig(overrides = {}) {
- const provider = value('AGENT_PROVIDER', 'claude-code');
- const anthropic = provider === 'anthropic';
- const claudeCode = provider === 'claude-code';
- const bundledPropertyFile = path.join(PROJECT_ROOT, 'knowledge-base', 'property-data', 'properties.json');
- const workspacePropertyFile = path.resolve(PROJECT_ROOT, '..', '..', 'huaxiangpipei', 'src', 'assets', 'data', 'properties.json');
- const configuredPropertyFile = value('QIWEI_AGENT_PROPERTY_DATA_FILE');
- const propertyDataFile = configuredPropertyFile
- ? resolvePath(configuredPropertyFile)
- : (fs.existsSync(bundledPropertyFile) ? bundledPropertyFile : (fs.existsSync(workspacePropertyFile) ? workspacePropertyFile : ''));
- const baseConfig = {
- dbPath: resolvePath(value('QIWEI_AGENT_DB_PATH'), latestPath('messages', 'agent-workbench.db')),
- legacyDbPath: path.resolve(PROJECT_ROOT, '..', '..', 'qiwei-agent-workbench', 'data', 'workbench.db'),
- globalDefaultPaused: bool('QIWEI_AGENT_GLOBAL_DEFAULT_PAUSED', true),
- conversationDefaultMode: value('QIWEI_AGENT_DEFAULT_MODE', 'review'),
- autoSendConfidence: number('QIWEI_AGENT_AUTO_SEND_CONFIDENCE', 0.88, 0, 1),
- knowledgeDir: resolvePath(value('QIWEI_AGENT_KNOWLEDGE_DIR'), path.join(PROJECT_ROOT, 'knowledge')),
- propertyDataFile,
- agent: {
- provider,
- apiKey: value('AGENT_API_KEY') || value(anthropic ? 'ANTHROPIC_AUTH_TOKEN' : 'OPENAI_API_KEY') || value('QIWEI_AUTO_REPLY_AI_KEY'),
- baseUrl: (value('AGENT_BASE_URL') || value(anthropic ? 'ANTHROPIC_BASE_URL' : 'OPENAI_BASE_URL') || value('QIWEI_AUTO_REPLY_AI_BASE_URL') || (anthropic ? 'https://api.anthropic.com' : 'https://api.openai.com/v1')).replace(/\/$/, ''),
- model: value('AGENT_MODEL') || value(anthropic || claudeCode ? 'ANTHROPIC_MODEL' : 'OPENAI_MODEL') || value('QIWEI_AUTO_REPLY_AI_MODEL') || (anthropic || claudeCode ? 'sonnet' : 'gpt-4.1-mini'),
- maxToolRounds: number('AGENT_MAX_TOOL_ROUNDS', 4, 1, 8),
- claudeExecutable: value('CLAUDE_CODE_EXECUTABLE') || path.join(path.dirname(process.execPath), 'node_modules', '@anthropic-ai', 'claude-code', 'bin', 'claude.exe'),
- claudeWorkdir: resolvePath(value('CLAUDE_CODE_WORKDIR'), PROJECT_ROOT),
- claudeSessionFile: resolvePath(value('CLAUDE_CODE_SESSION_FILE'), latestPath('messages', 'claude-code-sessions.json')),
- claudeProjectId: value('QIWEI_AGENT_PROJECT_ID') || crypto.createHash('sha256').update(PROJECT_ROOT).digest('hex').slice(0, 16),
- claudeMainSessionId: value('QIWEI_AGENT_MAIN_SESSION_ID'),
- claudeTimeoutMs: number('CLAUDE_CODE_TIMEOUT_MS', 120000, 15000, 300000),
- claudeMaxBudgetUsd: number('CLAUDE_CODE_MAX_BUDGET_USD', 0.35, 0.05, 5),
- claudeTools: value('CLAUDE_CODE_ALLOWED_TOOLS', 'Read,Glob,Grep'),
- },
- qiwei: {
- transport: 'fmode-gateway',
- authToken: value('QIWEI_AUTH_TOKEN'),
- uid: value('QIWEI_UID') || value('QIWE_UID'),
- guid: value('QIWEI_GUID') || value('QIWE_GUID'),
- apiBase: value('QIWEI_API_BASE') || value('QIWE_API_BASE'),
- userId: '',
- nickname: '',
- corpName: '',
- allowedSenders: value('QIWEI_AUTO_REPLY_ALLOWED_SENDERS').split(',').map(item => item.trim()).filter(Boolean),
- selfUserId: value('QIWEI_AUTO_REPLY_SELF_USER_ID'),
- intervalMs: number('QIWEI_AUTO_REPLY_INTERVAL_MS', 10000, 3000, 60000),
- initialSyncLimit: number('QIWEI_AGENT_INITIAL_SYNC_LIMIT', 5000, 100, 5000),
- initialSyncMaxPages: number('QIWEI_AGENT_INITIAL_SYNC_MAX_PAGES', 200, 10, 500),
- startupGraceSeconds: number('QIWEI_AGENT_STARTUP_GRACE_SECONDS', 10, 0, 60),
- },
- };
- const config = {
- ...baseConfig,
- ...overrides,
- agent: { ...baseConfig.agent, ...(overrides.agent || {}) },
- qiwei: { ...baseConfig.qiwei, ...(overrides.qiwei || {}) },
- };
- config.agent.claudeAddDirs = [config.knowledgeDir, config.propertyDataFile ? path.dirname(config.propertyDataFile) : ''].filter(Boolean);
- return config;
- }
- function accountRuntimeKey(input = {}) {
- const source = String(input.uid || input.guid || input.userId || 'default').trim();
- return crypto.createHash('sha256').update(source || 'default').digest('hex').slice(0, 16);
- }
- function accountWorkbenchOverrides(input = {}) {
- const storageKey = accountRuntimeKey(input);
- return {
- dbPath: latestPath('messages', `agent-workbench-${storageKey}.db`),
- agent: {
- claudeSessionFile: latestPath('messages', `claude-code-sessions-${storageKey}.json`),
- claudeProjectId: `${crypto.createHash('sha256').update(PROJECT_ROOT).digest('hex').slice(0, 12)}-${storageKey.slice(0, 8)}`,
- },
- qiwei: {
- uid: String(input.uid || '').trim(),
- guid: String(input.guid || '').trim(),
- userId: String(input.userId || '').trim(),
- nickname: String(input.nickname || '').trim(),
- corpName: String(input.corpName || '').trim(),
- apiBase: String(input.apiBase || '').trim(),
- },
- };
- }
- function backfillCustomerIntelligence(db) {
- const version = '4';
- if (db.getSetting('customer_intelligence_backfill_version', '') === version) return { skipped: true, profileFields: 0, taskCount: 0, alertCount: 0 };
- const removed = db.lastIntelligenceMigration || { tasks: { removed: 0 }, alerts: { removed: 0 } };
- let profileFields = 0;
- let taskCount = 0;
- let alertCount = 0;
- for (const conversation of db.listConversations()) {
- const existing = db.getProfile(conversation.id);
- let current = { profile: { ...existing.profile }, tags: existing.tags || [] };
- for (const message of db.listMessages(conversation.id, 200).filter(item => item.direction === 'inbound')) {
- const intelligence = extractExplicitCustomerIntelligence(message.content, current.profile || {}, {});
- if (Object.keys(intelligence.profileUpdates).length) {
- current = { profile: { ...current.profile, ...intelligence.profileUpdates }, tags: current.tags };
- profileFields += Object.keys(intelligence.profileUpdates).length;
- }
- const changed = Object.keys(intelligence.profileUpdates).length > 0;
- const tasks = intelligence.tasks.map(item => ({ ...item, sourceMessageId: changed ? message.id : null }));
- const alerts = intelligence.alerts.map(item => ({ ...item, sourceMessageId: changed || item.managedBy === 'event' ? message.id : null }));
- taskCount += db.reconcileCustomerTasks(conversation.id, tasks, message.id).tasks.length;
- alertCount += db.reconcileCustomerAlerts(conversation.id, alerts, message.id).alerts.length;
- }
- if (Object.keys(current.profile).length) db.updateProfile(conversation.id, { ...existing.profile, ...current.profile }, existing.tags);
- const recommendationSent = db.listMessages(conversation.id, 200).some(message =>
- message.direction === 'outbound' && /(房源|方案|重点|推荐).{0,20}(套|房)|(套|房).{0,20}(房源|方案|推荐)/.test(String(message.content || ''))
- );
- if (recommendationSent) db.completeCustomerTaskByBusinessKey(conversation.id, 'recommendation:shortlist', 'historical_recommendation_sent');
- }
- db.setSetting('customer_intelligence_backfill_version', version);
- if (profileFields || taskCount || alertCount) {
- db.audit({ actor: 'migration', action: 'customer_intelligence_backfilled', detail: { version, removed, profileFields, taskCount, alertCount } });
- }
- return { version, removed, profileFields, taskCount, alertCount };
- }
- const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
- class QiweiAgentPoller {
- constructor({ config, db, qiwei, service }) {
- this.config = config;
- this.db = db;
- this.qiwei = qiwei;
- this.service = service;
- this.running = false;
- this.loopPromise = null;
- this.startedAt = 0;
- this.lastError = '';
- }
- status() {
- return {
- running: this.running,
- syncKey: Number(this.db.getPollState('sync_key', '0')),
- lastError: this.lastError,
- startedAt: this.startedAt || null,
- };
- }
- async start() {
- if (this.running) return this.status();
- if (!this.qiwei.isConfigured()) throw new Error('Fmode 企微网关尚未配置,请先完成 Fmode 鉴权和企微扫码登录');
- if (this.config.allowedSenders.length === 0) throw new Error('企微联系人白名单为空,拒绝启动');
- await this.qiwei.checkLogin();
- this.running = true;
- this.startedAt = Math.floor(Date.now() / 1000);
- this.lastError = '';
- let syncKey = Number(this.db.getPollState('sync_key', '0')) || 0;
- if (syncKey === 0) syncKey = await this.establishBaseline();
- this.db.audit({ actor: 'runtime', action: 'poller_started', detail: { syncKey, allowlistCount: this.config.allowedSenders.length } });
- this.loopPromise = this.loop(syncKey);
- return this.status();
- }
- stop() {
- if (!this.running) return this.status();
- this.running = false;
- this.db.audit({ actor: 'runtime', action: 'poller_stopped', detail: this.status() });
- return this.status();
- }
- async establishBaseline() {
- let cursor = 0;
- let reachedEnd = false;
- let pages = 0;
- let count = 0;
- while (pages < this.config.initialSyncMaxPages) {
- const result = await this.qiwei.syncMessages(cursor, this.config.initialSyncLimit);
- const list = result.syncMsgList || [];
- pages += 1;
- count += list.length;
- const seqs = list.map(item => Number(item.seq)).filter(Number.isFinite);
- const next = Math.max(cursor, Number(result.travelSyncKey) || 0, seqs.length ? Math.max(...seqs) : 0);
- if (list.length === 0) { reachedEnd = true; break; }
- if (next <= cursor) throw new Error(`历史消息游标没有前进(seq=${cursor})`);
- cursor = next;
- }
- if (!reachedEnd) throw new Error(`历史消息超过 ${this.config.initialSyncMaxPages} 页,拒绝启动自动处理`);
- this.db.setPollState('sync_key', cursor);
- this.db.audit({ actor: 'runtime', action: 'poller_baseline_established', detail: { cursor, pages, skippedHistory: count } });
- return cursor;
- }
- async loop(initialSyncKey) {
- let syncKey = initialSyncKey;
- while (this.running) {
- try {
- const result = await this.qiwei.syncMessages(syncKey, 50);
- const list = result.syncMsgList || [];
- const seqs = list.map(item => Number(item.seq)).filter(Number.isFinite);
- const next = Math.max(syncKey, Number(result.travelSyncKey) || 0, seqs.length ? Math.max(...seqs) : 0);
- for (const message of list) await this.process(message);
- syncKey = next;
- this.db.setPollState('sync_key', syncKey);
- this.lastError = '';
- } catch (error) {
- this.lastError = error.message;
- this.db.audit({ actor: 'runtime', action: 'poller_error', detail: { message: error.message } });
- }
- if (this.running) await delay(this.config.intervalMs);
- }
- }
- async process(message) {
- const candidate = evaluatePolledMessage(message, this.config);
- if (!candidate.eligible) return;
- const { content, senderId, timestamp } = candidate;
- await this.service.ingestInbound({
- externalId: String(message.msgServerId || message.msgUniqueIdentifier || `${senderId}:${message.seq}`),
- contactId: senderId,
- contactName: message.senderName || '王刚',
- content,
- timestamp: new Date(timestamp * 1000).toISOString(),
- raw: { seq: message.seq, msgType: message.msgType, timestamp: message.timestamp },
- });
- }
- }
- function createWorkbench(overrides = {}) {
- const config = loadAgentConfig(overrides.config || {});
- const db = overrides.db || new AgentWorkbenchDb(config.dbPath, {
- globalPaused: config.globalDefaultPaused,
- defaultMode: config.conversationDefaultMode,
- autoSendConfidence: config.autoSendConfidence,
- });
- if (!overrides.db && !overrides.skipLegacyImport) {
- try { db.importCompatibleDatabase(config.legacyDbPath); } catch (error) {
- db.audit({ actor: 'migration', action: 'legacy_workbench_import_failed', detail: { message: error.message } });
- }
- const removed = db.cleanupInboundContentDuplicates(60);
- if (removed) db.audit({ actor: 'migration', action: 'duplicate_messages_cleaned', detail: { removed } });
- backfillCustomerIntelligence(db);
- }
- const knowledge = overrides.knowledge || new AgentKnowledgeStore({
- knowledgeDir: config.knowledgeDir,
- propertyDataFile: config.propertyDataFile,
- });
- backfillPropertyRecommendations(db, knowledge);
- const qiwei = overrides.qiwei || new FmodeQiweiClient(config.qiwei);
- const agent = overrides.agent || new QiweiAgentRuntime({ config: config.agent, knowledge });
- const service = overrides.service || new AgentWorkbenchService({ db, agent, qiwei, config });
- const poller = overrides.poller || new QiweiAgentPoller({ config: config.qiwei, db, qiwei, service });
- return { config, db, knowledge, qiwei, agent, service, poller };
- }
- function configuredStartupAccount() {
- return {
- uid: value('QIWEI_UID') || value('QIWE_UID'),
- guid: value('QIWEI_GUID') || value('QIWE_GUID'),
- apiBase: value('QIWEI_API_BASE') || value('QIWE_API_BASE'),
- userId: '',
- nickname: '',
- corpName: '',
- };
- }
- const startupAccount = configuredStartupAccount();
- let workbench = createWorkbench(startupAccount.uid ? {
- config: accountWorkbenchOverrides(startupAccount),
- skipLegacyImport: true,
- } : {});
- function migrateStartupWorkbench(target) {
- if (!startupAccount.uid || target.db.listConversations().length) return { imported: false, reason: 'target_not_empty' };
- const candidates = [latestPath('messages', 'agent-workbench.db'), target.config.legacyDbPath];
- for (const sourcePath of candidates) {
- try {
- const result = target.db.importCompatibleDatabase(sourcePath);
- if (!result.imported) continue;
- const removed = target.db.cleanupInboundContentDuplicates(60);
- backfillCustomerIntelligence(target.db);
- backfillPropertyRecommendations(target.db, target.knowledge);
- target.db.audit({
- actor: 'migration',
- action: 'account_workbench_migration_completed',
- detail: { source: path.basename(sourcePath), removedDuplicates: removed },
- });
- return result;
- } catch (error) {
- target.db.audit({
- actor: 'migration',
- action: 'account_workbench_migration_failed',
- detail: { source: path.basename(sourcePath), message: error.message },
- });
- }
- }
- return { imported: false, reason: 'source_missing_or_incompatible' };
- }
- migrateStartupWorkbench(workbench);
- let accountStatusCache = { checkedAt: 0, value: null };
- let accountStatusRefresh = null;
- let accountStatusRefreshKey = '';
- const accountLastOnlineAt = new Map();
- const accountOfflineChecks = new Map();
- const ONLINE_STATUS_GRACE_MS = 60000;
- const workbenches = new Map();
- function activeAccountMetadata() {
- const context = workbench.qiwei.context();
- return {
- uid: String(context.uid || workbench.config.qiwei.uid || '').trim(),
- guid: String(context.guid || workbench.config.qiwei.guid || '').trim(),
- apiBase: String(context.apiBase || workbench.config.qiwei.apiBase || '').trim(),
- userId: String(workbench.config.qiwei.userId || '').trim(),
- nickname: String(workbench.config.qiwei.nickname || '').trim(),
- corpName: String(workbench.config.qiwei.corpName || '').trim(),
- };
- }
- function applyActiveAccountContext(account) {
- setActiveQiweiContext(account);
- Object.assign(workbench.config.qiwei, account);
- if (workbench.qiwei && workbench.qiwei.config) Object.assign(workbench.qiwei.config, account);
- }
- function provisionalAccountStatus(selected = activeAccountMetadata(), statusText = '正在检测账号状态') {
- return {
- uid: selected.uid,
- guid: selected.guid,
- userId: selected.userId,
- configured: workbench.qiwei.isConfigured(),
- online: false,
- nickname: selected.nickname || selected.userId || '当前企微账号',
- corpName: selected.corpName || '',
- statusCode: null,
- statusText,
- };
- }
- const initialAccount = activeAccountMetadata();
- workbenches.set(accountRuntimeKey(initialAccount), workbench);
- applyActiveAccountContext(initialAccount);
- async function switchActiveAccount(input = {}) {
- const account = {
- uid: String(input.uid || '').trim(),
- guid: String(input.guid || '').trim(),
- apiBase: String(input.apiBase || '').trim(),
- userId: String(input.userId || '').trim(),
- nickname: String(input.nickname || input.userId || '').trim(),
- corpName: String(input.corpName || '').trim(),
- };
- if (!account.uid) throw new Error('该账号缺少 Fmode 设备 uid,请重新扫码绑定后再切换');
- const current = activeAccountMetadata();
- const currentKey = accountRuntimeKey(current);
- const nextKey = accountRuntimeKey(account);
- const accountChanged = currentKey !== nextKey;
- if (accountChanged && workbench.poller.status().running) workbench.poller.stop();
- let nextWorkbench = workbenches.get(nextKey);
- if (!nextWorkbench) {
- nextWorkbench = createWorkbench({
- config: accountWorkbenchOverrides(account),
- skipLegacyImport: true,
- });
- workbenches.set(nextKey, nextWorkbench);
- }
- workbench = nextWorkbench;
- applyActiveAccountContext({
- ...activeAccountMetadata(),
- ...account,
- apiBase: account.apiBase || activeAccountMetadata().apiBase,
- });
- const status = provisionalAccountStatus();
- accountStatusCache = { checkedAt: Date.now(), value: status };
- void refreshAccountStatus();
- return {
- status: 'ok',
- assistantMessage: `已切换到账号:${status.nickname || account.nickname || account.userId || account.uid}`,
- summary: {
- switched: accountChanged,
- storageKey: nextKey,
- online: status.online,
- listenerStopped: accountChanged,
- },
- data: { account: status },
- };
- }
- async function refreshAccountStatus() {
- const selected = activeAccountMetadata();
- const targetWorkbench = workbench;
- const refreshKey = accountRuntimeKey(selected);
- if (accountStatusRefresh && accountStatusRefreshKey === refreshKey) return accountStatusRefresh;
- accountStatusRefreshKey = refreshKey;
- accountStatusRefresh = (async () => {
- let next;
- try {
- const data = await targetWorkbench.qiwei.checkLogin();
- const online = Number(data.userOnlineStatus) === 2 && Number(data.errorCode || 0) === 0;
- next = {
- uid: selected.uid,
- guid: selected.guid,
- userId: data.userId || selected.userId,
- configured: data.configured !== false,
- online,
- nickname: data.nickname || selected.nickname || selected.userId || '当前企微账号',
- corpName: data.corpName || selected.corpName || '',
- statusCode: data.userOnlineStatus ?? null,
- statusText: online ? '账号在线' : '账号离线',
- };
- if (online) {
- accountLastOnlineAt.set(refreshKey, Date.now());
- accountOfflineChecks.set(refreshKey, 0);
- } else {
- const offlineChecks = Number(accountOfflineChecks.get(refreshKey) || 0) + 1;
- accountOfflineChecks.set(refreshKey, offlineChecks);
- const lastOnlineAt = Number(accountLastOnlineAt.get(refreshKey) || 0);
- if (offlineChecks < 2 && lastOnlineAt && Date.now() - lastOnlineAt < ONLINE_STATUS_GRACE_MS) {
- next.online = true;
- next.statusCode = 2;
- next.statusText = '账号在线(正在复核)';
- }
- }
- } catch {
- next = {
- ...provisionalAccountStatus(selected, '状态检测失败'),
- configured: targetWorkbench.qiwei.isConfigured(),
- };
- const lastOnlineAt = Number(accountLastOnlineAt.get(refreshKey) || 0);
- if (lastOnlineAt && Date.now() - lastOnlineAt < ONLINE_STATUS_GRACE_MS) {
- next.online = true;
- next.statusCode = 2;
- next.statusText = '账号在线(状态刷新中)';
- }
- }
- if (accountRuntimeKey(activeAccountMetadata()) === refreshKey) {
- accountStatusCache = { checkedAt: Date.now(), value: next };
- }
- return next;
- })().finally(() => {
- if (accountStatusRefreshKey === refreshKey) {
- accountStatusRefresh = null;
- accountStatusRefreshKey = '';
- }
- });
- return accountStatusRefresh;
- }
- async function detectAccountStatus(force = false) {
- const selected = activeAccountMetadata();
- const selectedKey = accountRuntimeKey(selected);
- const cacheMatches = accountStatusCache.value && accountRuntimeKey(accountStatusCache.value) === selectedKey;
- if (force) return refreshAccountStatus();
- if (cacheMatches) {
- if (Date.now() - accountStatusCache.checkedAt >= 8000) void refreshAccountStatus();
- return accountStatusCache.value;
- }
- const provisional = provisionalAccountStatus(selected);
- accountStatusCache = { checkedAt: Date.now(), value: provisional };
- void refreshAccountStatus();
- return provisional;
- }
- function maskedId(value) {
- const text = String(value || '');
- if (text.length <= 4) return '测试联系人';
- return `${text.slice(0, 2)}***${text.slice(-2)}`;
- }
- function completeness(profile = {}) {
- const values = [
- profile.preferredRegion || profile.region || profile.district || profile.intent_area || profile.districts,
- profile.budgetWan || profile.budget || profile.budgetMax || profile.budget_max,
- profile.layout || profile.rooms || profile.house_type,
- profile.area || profile.areaMin || profile.area_min,
- profile.decoration,
- profile.timeline || profile.urgency,
- ];
- return Math.round(values.filter(value => Array.isArray(value) ? value.length : Boolean(value)).length / values.length * 100);
- }
- function parseJson(value, fallback) {
- try { return value ? JSON.parse(value) : fallback; }
- catch { return fallback; }
- }
- function propertyMatches(toolTrace = []) {
- const call = [...toolTrace].reverse().find(item => item.tool === 'search_properties');
- return (call?.result?.items || []).map(item => ({
- community: item.community,
- title: item.layout,
- price: item.totalPrice,
- layout: item.layout,
- area: item.area,
- score: null,
- level: call.result.warning || '',
- highlights: item.highlights || [],
- }));
- }
- function propertyRecommendationsFromTrace(toolTrace = []) {
- return [...new Map((toolTrace || []).filter(item => item?.tool === 'search_properties').flatMap(item => item.result?.items || []).filter(item => item?.id).map(item => [String(item.id), item])).values()];
- }
- function detectedPropertiesInMessage(content, properties = []) {
- const text = String(content || '');
- if (!text) return [];
- return properties.filter(property => {
- const id = String(property.id || '').trim();
- if (id && text.includes(id)) return true;
- const community = String(property.community || '').trim();
- const price = Number(property.totalPrice || 0);
- if (!community || !price || !text.includes(community)) return false;
- return new RegExp(`${price}(?:\\.0+)?\\s*万`).test(text);
- });
- }
- function backfillPropertyRecommendations(db, knowledge) {
- if (!db || typeof db.upsertCustomerRecommendations !== 'function') return 0;
- const properties = Array.isArray(knowledge?.properties) ? knowledge.properties : [];
- let count = 0;
- for (const conversation of db.listConversations()) {
- for (const draft of db.listDrafts({ conversationId: conversation.id, limit: 500 })) {
- const items = propertyRecommendationsFromTrace(draft.tool_trace);
- if (!items.length || draft.status === 'rejected') continue;
- db.upsertCustomerRecommendations(conversation.id, items, {
- type: 'agent-tool',
- entityId: draft.id,
- status: ['sent', 'approved'].includes(draft.status) ? 'recommended' : 'candidate',
- evidence: draft.status === 'sent' ? String(draft.content || '').slice(0, 500) : 'Agent 房源工具查询结果,尚未确认已发送给客户',
- createdAt: draft.created_at,
- });
- count += items.length;
- }
- if (!properties.length) continue;
- for (const message of db.listMessages(conversation.id, 500).filter(item => item.direction === 'outbound')) {
- const items = detectedPropertiesInMessage(message.content, properties);
- if (!items.length) continue;
- db.upsertCustomerRecommendations(conversation.id, items, {
- type: 'outbound-message-detected',
- entityId: message.id,
- status: 'recommended',
- evidence: String(message.content || '').slice(0, 500),
- createdAt: message.created_at,
- });
- count += items.length;
- }
- }
- return count;
- }
- function publicConversation(row) {
- const detail = workbench.service.conversationDetail(row.id);
- const claudeSession = getCustomerSessionGuide(row, {
- sessionFile: workbench.config.agent.claudeSessionFile,
- });
- const drafts = detail.drafts || [];
- const latestInbound = [...(detail.messages || [])].reverse().find(message => message.direction === 'inbound') || null;
- const currentDrafts = latestInbound
- ? drafts.filter(item => item.inbound_message_id === latestInbound.id)
- : [];
- const pending = currentDrafts.find(item => item.status === 'pending') || null;
- const latestDraft = pending || currentDrafts.find(item => ['sent', 'approved'].includes(item.status)) || null;
- const rawProfile = detail.profile?.profile || {};
- const { __evidence: profileEvidence = {}, ...profile } = rawProfile;
- const customerTasks = detail.tasks || [];
- const customerAlerts = detail.alerts || [];
- const customerRecommendations = detail.recommendations || [];
- const citations = latestDraft?.citations || [];
- const cutoverAt = Date.parse(workbench.db.getSetting('agent_cutover_at', '')) || Date.now();
- const displayMessages = [...new Map((detail.messages || []).map(message => [message.id, message])).values()]
- .sort((a, b) => Date.parse(a.created_at) - Date.parse(b.created_at))
- .slice(-60);
- const visibleEntityIds = new Set(displayMessages.map(message => message.id));
- const visibleAudit = (detail.audit || []).filter(item =>
- Date.parse(item.created_at) >= cutoverAt || visibleEntityIds.has(item.entity_id)
- ).slice(0, 40);
- return {
- id: row.id,
- displayName: row.contact_name || '白名单测试联系人',
- maskedId: maskedId(row.contact_id),
- mode: row.mode,
- source: 'live',
- claudeSession,
- messages: displayMessages.map(message => ({
- id: message.id,
- role: message.direction === 'inbound' ? 'customer' : message.sender_type,
- content: message.content,
- timestamp: message.created_at,
- status: message.status,
- source: message.direction === 'inbound' ? 'live' : message.sender_type,
- })),
- analysis: {
- intent: latestDraft?.intent || '',
- intentLabel: latestDraft?.intent || (detail.agentError ? 'Agent 上游不可用' : '待 Agent 处理'),
- demand: profile,
- completenessScore: completeness(profile),
- matches: propertyMatches(latestDraft?.tool_trace || []),
- knowledgeSources: citations.length
- ? citations.map(item => `${item.heading || item.source}${item.source ? ` · ${item.source}` : ''}`)
- : ['真实企微消息', '客户画像', '企业规则库与知识库'],
- reasoning: latestDraft?.reason || detail.agentError?.message || '消息已进入真实企微链路,等待 Agent 生成可审核草稿。',
- },
- customerIntelligence: {
- profile,
- profileEvidence,
- profileUpdatedAt: detail.profile?.updatedAt || null,
- tags: detail.profile?.tags || [],
- tasks: customerTasks.map(item => ({
- id: item.id,
- businessKey: item.business_key,
- type: item.type,
- title: item.title,
- owner: item.owner,
- dueAt: item.due_at,
- priority: item.priority,
- status: item.status,
- reason: item.reason,
- evidence: item.evidence,
- evidenceItems: parseJson(item.evidence_json, []),
- resolutionReason: item.resolution_reason,
- officialTodoId: item.official_todo_id,
- officialSyncStatus: item.official_sync_status,
- officialSyncedAt: item.official_synced_at,
- updatedAt: item.updated_at,
- })),
- alerts: customerAlerts.map(item => ({
- id: item.id,
- businessKey: item.business_key,
- type: item.type,
- severity: item.severity,
- title: item.title,
- detail: item.detail,
- evidence: item.evidence,
- evidenceItems: parseJson(item.evidence_json, []),
- resolutionReason: item.resolution_reason,
- recommendedAction: item.recommended_action,
- status: item.status,
- updatedAt: item.updated_at,
- })),
- recommendations: customerRecommendations.map(item => ({
- id: item.id,
- propertyId: item.property_id,
- property: item.property_snapshot,
- status: item.status,
- feedbackReason: item.feedback_reason,
- recommendCount: item.recommend_count,
- sources: item.sources,
- firstRecommendedAt: item.first_recommended_at,
- lastRecommendedAt: item.last_recommended_at,
- updatedAt: item.updated_at,
- })),
- summary: {
- openTasks: customerTasks.filter(item => ['open', 'in_progress'].includes(item.status)).length,
- openAlerts: customerAlerts.filter(item => item.status === 'open').length,
- highAlerts: customerAlerts.filter(item => item.status === 'open' && ['high', 'critical'].includes(item.severity)).length,
- recommendationCount: customerRecommendations.length,
- pendingFeedbackCount: customerRecommendations.filter(item => ['candidate', 'recommended'].includes(item.status)).length,
- },
- },
- pendingReply: pending ? {
- id: pending.id,
- content: pending.content,
- status: pending.status,
- confidence: pending.confidence,
- reason: pending.reason,
- requiresHuman: pending.requires_human,
- citations: pending.citations,
- toolTrace: pending.tool_trace,
- createdAt: pending.created_at,
- } : null,
- drafts,
- audit: visibleAudit,
- agentError: detail.agentError,
- lastMessageAt: displayMessages.at(-1)?.created_at || row.last_message_at,
- updatedAt: row.updated_at,
- };
- }
- async function getAgentStatus() {
- const account = await detectAccountStatus();
- const state = workbench.service.state(workbench.poller.status());
- return {
- status: 'ok',
- data: {
- globalMode: state.global.paused ? 'paused' : state.global.defaultMode,
- global: state.global,
- listener: state.poller,
- account,
- agent: state.agent,
- knowledge: workbench.knowledge.stats(),
- config: {
- allowedSenderCount: state.qiwei.allowlistCount,
- testMode: false,
- demoMode: false,
- transport: state.qiwei.transport,
- pollIntervalMs: workbench.config.qiwei.intervalMs,
- },
- safety: {
- whitelistEnabled: state.qiwei.allowlistCount > 0,
- defaultReviewMode: true,
- globalPauseSupported: true,
- messageSendRequiresWhitelist: true,
- demoReplyDisabled: true,
- },
- },
- };
- }
- function displayableConversations() {
- return workbench.db.listConversations().filter(item => item.last_message_at || item.last_content || Number(item.pending_count || 0) > 0);
- }
- function getConversations() {
- return { status: 'ok', data: { conversations: displayableConversations().map(publicConversation) } };
- }
- function updateCustomerProfile(conversationId, input = {}) {
- const conversation = workbench.db.getConversation(conversationId);
- if (!conversation) throw new Error('客户会话不存在');
- const current = workbench.db.getProfile(conversationId);
- const nextProfile = { ...(current.profile || {}) };
- const evidence = { ...(nextProfile.__evidence || {}) };
- const patch = input.profile && typeof input.profile === 'object' ? input.profile : {};
- const changedFields = [];
- for (const [field, rawValue] of Object.entries(patch)) {
- if (!field || field.startsWith('__')) continue;
- const value = typeof rawValue === 'string' ? rawValue.trim() : rawValue;
- if (value === '' || value === null || value === undefined) delete nextProfile[field];
- else nextProfile[field] = value;
- evidence[field] = {
- text: String(input.reason || '客户管理人工核对').trim(),
- sourceMessageId: null,
- source: 'human',
- updatedAt: new Date().toISOString(),
- };
- changedFields.push(field);
- }
- nextProfile.__evidence = evidence;
- const tags = input.tags === undefined
- ? current.tags
- : [...new Set((Array.isArray(input.tags) ? input.tags : String(input.tags || '').split(/[,,]/)).map(item => String(item).trim()).filter(Boolean))];
- const updated = workbench.db.updateProfile(conversationId, nextProfile, tags);
- workbench.db.audit({
- actor: 'human',
- action: 'customer_profile_updated',
- conversationId,
- entityId: conversationId,
- detail: { fields: changedFields, tagCount: tags.length, reason: String(input.reason || '').trim() },
- });
- const { __evidence, ...visibleProfile } = updated.profile || {};
- return {
- status: 'ok',
- assistantMessage: `客户“${conversation.contact_name || '未命名客户'}”的主档已更新。`,
- summary: { changedFields, tagCount: tags.length },
- data: { profile: visibleProfile, profileEvidence: __evidence || {}, tags, updatedAt: updated.updatedAt },
- warnings: [],
- errors: [],
- };
- }
- function updateCustomerRecommendation(conversationId, recommendationId, input = {}) {
- const recommendation = workbench.db.updateCustomerRecommendation(conversationId, recommendationId, {
- status: input.status,
- feedbackReason: input.feedbackReason,
- });
- if (!recommendation) throw new Error('房源推荐记录不存在');
- const current = workbench.db.getProfile(conversationId);
- const existingFeedback = Array.isArray(current.profile?.propertyFeedback) ? current.profile.propertyFeedback : [];
- const feedback = {
- propertyId: recommendation.property_id,
- status: recommendation.status,
- reason: recommendation.feedback_reason,
- recordedAt: new Date().toISOString(),
- };
- const byProperty = new Map(existingFeedback.map(item => [String(item.propertyId || ''), item]));
- byProperty.set(String(feedback.propertyId), feedback);
- const evidence = { ...(current.profile?.__evidence || {}) };
- evidence.propertyFeedback = { text: recommendation.feedback_reason || `人工标记为${recommendation.status}`, sourceMessageId: null, source: 'human', updatedAt: feedback.recordedAt };
- workbench.db.updateProfile(conversationId, { ...current.profile, propertyFeedback: [...byProperty.values()].slice(-50), __evidence: evidence }, current.tags);
- workbench.db.audit({ actor: 'human', action: 'property_recommendation_feedback', conversationId, entityId: recommendation.id, detail: { propertyId: recommendation.property_id, status: recommendation.status, reason: recommendation.feedback_reason } });
- return {
- status: 'ok',
- assistantMessage: `房源“${recommendation.property_snapshot?.community || recommendation.property_id}”反馈已记录为 ${recommendation.status}。`,
- summary: { recommendationId: recommendation.id, status: recommendation.status },
- data: { recommendation },
- warnings: [],
- errors: [],
- };
- }
- async function syncConversations() {
- const allowlist = new Set(workbench.config.qiwei.allowedSenders.map(String));
- if (!allowlist.size) throw new Error('测试联系人白名单为空,无法同步会话');
- const account = await detectAccountStatus(true);
- if (!account.online) throw new Error('测试账号当前不在线,无法同步企微会话');
- const grouped = new Map();
- const seen = new Set();
- const seenSemantic = new Set();
- let cursor = 0;
- let pages = 0;
- let scannedMessages = 0;
- while (pages < workbench.config.qiwei.initialSyncMaxPages) {
- const result = await workbench.qiwei.syncMessages(cursor, workbench.config.qiwei.initialSyncLimit);
- const list = Array.isArray(result.syncMsgList) ? result.syncMsgList : [];
- scannedMessages += list.length;
- for (const message of list) {
- const senderId = String(message.senderId || '');
- const receiverId = String(message.receiverId || '');
- const contactId = allowlist.has(senderId) ? senderId : allowlist.has(receiverId) ? receiverId : '';
- const content = String(message.msgData?.content || '').trim();
- if (!contactId || !content || ![0, 1, 2].includes(Number(message.msgType))) continue;
- const inbound = senderId === contactId;
- const timestampSeconds = messageTimestamp(message.timestamp);
- if (!timestampSeconds) continue;
- const timestamp = new Date(timestampSeconds * 1000).toISOString();
- const externalId = String(message.msgServerId || message.msgUniqueIdentifier || '');
- const dedupeKey = externalId || `${contactId}|${inbound ? 'in' : 'out'}|${timestamp}|${content}`;
- if (seen.has(dedupeKey)) continue;
- seen.add(dedupeKey);
- const semanticKey = `${contactId}|${inbound ? 'in' : 'out'}|${timestamp}|${content}`;
- if (seenSemantic.has(semanticKey)) continue;
- seenSemantic.add(semanticKey);
- if (!grouped.has(contactId)) grouped.set(contactId, { contactName: '', messages: [] });
- const group = grouped.get(contactId);
- if (inbound && message.senderName) group.contactName = String(message.senderName);
- group.messages.push({
- externalId: externalId || null,
- inbound,
- content,
- timestamp,
- raw: { seq: message.seq, msgType: message.msgType, timestamp: message.timestamp, source: 'manual_sync' },
- });
- }
- const seqs = list.map(item => Number(item.seq)).filter(Number.isFinite);
- const next = Math.max(cursor, Number(result.travelSyncKey) || 0, seqs.length ? Math.max(...seqs) : 0);
- pages += 1;
- if (!list.length || next <= cursor) break;
- cursor = next;
- }
- let syncedMessages = 0;
- let removedImportedMessages = 0;
- for (const [contactId, group] of grouped.entries()) {
- const conversation = workbench.db.ensureConversation(contactId, group.contactName || '白名单企微联系人');
- removedImportedMessages += workbench.db.deleteImportedMessages(conversation.id, 'manual_sync');
- const ordered = group.messages.sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp));
- const recent = ordered.slice(-20);
- for (const message of recent) {
- const inserted = workbench.db.insertMessage({
- conversationId: conversation.id,
- externalId: message.externalId,
- direction: message.inbound ? 'inbound' : 'outbound',
- senderType: message.inbound ? 'customer' : 'human',
- content: message.content,
- status: message.inbound ? 'received' : 'sent',
- createdAt: message.timestamp,
- raw: message.raw,
- });
- if (inserted.created) syncedMessages += 1;
- }
- }
- const duplicatesRemoved = workbench.db.cleanupInboundContentDuplicates(60);
- workbench.db.audit({
- actor: 'human',
- action: 'conversation_history_synced',
- detail: { conversations: grouped.size, insertedMessages: syncedMessages, removedImportedMessages, scannedMessages, duplicatesRemoved },
- });
- return {
- status: 'ok',
- assistantMessage: grouped.size
- ? `已补采 ${grouped.size} 个白名单真实会话的最近消息;只入库,不运行 Agent、不发送回复`
- : '未读取到白名单联系人的文字会话,请先在企微中与该联系人收发一条消息',
- data: {
- syncedConversationCount: grouped.size,
- syncedMessageCount: syncedMessages,
- scannedMessageCount: scannedMessages,
- conversations: displayableConversations().map(publicConversation),
- },
- };
- }
- function changeGlobalMode(mode) {
- const selected = String(mode || '');
- let update;
- if (['paused', 'monitor'].includes(selected)) update = { paused: true };
- else if (['review', 'suggest'].includes(selected)) update = { paused: false, defaultMode: 'review' };
- else if (selected === 'auto') update = { paused: false, defaultMode: 'auto' };
- else if (selected === 'human') update = { paused: false, defaultMode: 'human' };
- else throw new Error('不支持的 Agent 模式');
- const global = workbench.service.setGlobal(update);
- return {
- status: 'ok',
- assistantMessage: global.paused ? 'Agent 已全局暂停;仍可接收消息,但不会生成或发送回复' : `全局策略已切换为 ${global.defaultMode}`,
- data: { global, globalMode: global.paused ? 'paused' : global.defaultMode },
- };
- }
- function changeConversationMode(id, mode) {
- const mapped = mode === 'agent' ? 'review' : mode === 'manual' ? 'human' : mode;
- const conversation = workbench.service.setConversationMode(id, mapped);
- const labels = { review: '待审核', auto: '高置信自动', human: '人工接管', paused: '会话暂停' };
- return { status: 'ok', assistantMessage: `会话已切换为${labels[mapped]}`, data: { conversation } };
- }
- async function approveReply(id, content) {
- const detail = workbench.service.conversationDetail(id);
- if (!detail) throw new Error('会话不存在');
- const pending = detail.drafts.find(item => item.status === 'pending');
- const result = pending
- ? await workbench.service.approveDraft(pending.id, { content, actor: 'human' })
- : { status: 'sent', message: await workbench.service.manualSend(id, content, 'human') };
- return { status: 'ok', assistantMessage: '回复已真实发送给白名单测试联系人', data: result };
- }
- async function approveDraft(draftId, content) {
- const result = await workbench.service.approveDraft(draftId, { content, actor: 'human' });
- return { status: 'ok', assistantMessage: '审核通过,回复已真实发送且只发送一次', data: result };
- }
- function rejectDraft(draftId, reason) {
- const result = workbench.service.rejectDraft(draftId, { reason, actor: 'human' });
- return { status: 'ok', assistantMessage: '草稿已驳回,不会发送给客户', data: result };
- }
- async function regenerateDraft(draftId) {
- const result = await workbench.service.regenerateDraft(draftId, 'human');
- return {
- status: 'ok',
- assistantMessage: result.status === 'pending_review' ? 'Agent 已重新生成待审核草稿' : (result.error || 'Agent 重新生成未完成'),
- data: result,
- };
- }
- async function generateLatestDraft(conversationId) {
- const result = await workbench.service.generateLatestDraft(conversationId, 'human');
- return {
- status: 'ok',
- assistantMessage: result.status === 'pending_review' ? 'Agent 已生成待审核草稿' : (result.error || 'Agent 未生成草稿'),
- data: result,
- };
- }
- async function manualSend(conversationId, content) {
- const message = await workbench.service.manualSend(conversationId, content, 'human');
- return { status: 'ok', assistantMessage: '人工回复已真实发送给白名单测试联系人', data: { message } };
- }
- async function updateCustomerTask(taskId, input = {}) {
- if (!['open', 'in_progress', 'done', 'dismissed'].includes(String(input.status || ''))) throw new Error('不支持的客户待办状态');
- const existing = workbench.db.getCustomerTask(taskId);
- if (!existing) throw new Error('客户待办不存在');
- let officialResult = null;
- if (input.status === 'done' && existing.official_todo_id) {
- officialResult = await completeTodoKnowledge(existing.official_todo_id);
- }
- const officialOk = !officialResult || officialResult.status === 'ok';
- const task = workbench.db.updateCustomerTask(taskId, {
- status: input.status,
- resolution_reason: input.status === 'done' ? 'human_completed' : input.status === 'dismissed' ? 'human_dismissed' : '',
- ...(existing.official_todo_id ? { official_sync_status: officialOk ? input.status : 'error' } : {}),
- });
- if (!task) throw new Error('客户待办不存在');
- workbench.db.audit({ actor: 'human', action: 'customer_task_updated', conversationId: task.conversation_id, entityId: task.id, detail: { status: task.status, officialSynced: Boolean(existing.official_todo_id), officialOk } });
- return {
- status: 'ok',
- assistantMessage: existing.official_todo_id && !officialOk
- ? `本地待办已更新为 ${task.status},但企微官方待办同步失败,请稍后重试`
- : `客户待办已更新为 ${task.status}${existing.official_todo_id ? ',企微官方待办已同步' : ''}`,
- data: { task, officialResult },
- warnings: existing.official_todo_id && !officialOk ? ['企微官方待办状态尚未同步'] : [],
- };
- }
- async function syncCustomerTaskToOfficialTodo(taskId, input = {}) {
- const syncCurrentAccountTask = createCustomerTaskOfficialSync({
- db: workbench.db,
- searchTodoUsers,
- createTodoKnowledge,
- });
- return syncCurrentAccountTask(taskId, input);
- }
- function updateCustomerAlert(alertId, input = {}) {
- if (!['open', 'acknowledged', 'resolved', 'dismissed'].includes(String(input.status || ''))) throw new Error('不支持的客户预警状态');
- const alert = workbench.db.updateCustomerAlert(alertId, { status: input.status });
- if (!alert) throw new Error('客户预警不存在');
- workbench.db.audit({ actor: 'human', action: 'customer_alert_updated', conversationId: alert.conversation_id, entityId: alert.id, detail: { status: alert.status } });
- return { status: 'ok', assistantMessage: `客户预警已更新为 ${alert.status}`, data: { alert } };
- }
- function getAudit(limit = 200) {
- return { status: 'ok', data: { audit: workbench.db.listAudit(Math.max(1, Math.min(500, Number(limit) || 200))) } };
- }
- async function startListener() {
- const account = await detectAccountStatus(true);
- if (!account.online) throw new Error(`${account.nickname || '当前账号'}不在线,无法启动真实消息监听`);
- workbench.service.setGlobal({ paused: false, defaultMode: 'auto' });
- for (const conversation of workbench.db.listConversations()) {
- workbench.service.setConversationMode(conversation.id, 'auto');
- }
- let status;
- try {
- status = await workbench.poller.start();
- } catch (error) {
- workbench.service.setGlobal({ paused: false, defaultMode: 'review' });
- for (const conversation of workbench.db.listConversations()) {
- workbench.service.setConversationMode(conversation.id, 'human');
- }
- throw error;
- }
- return {
- status: 'ok',
- assistantMessage: 'AI 监听已启动:仅处理白名单联系人,高置信回复可自动发送,人工可随时接管',
- data: status,
- };
- }
- function stopListener() {
- const status = workbench.poller.stop();
- workbench.service.setGlobal({ paused: false, defaultMode: 'review' });
- for (const conversation of workbench.db.listConversations()) {
- workbench.service.setConversationMode(conversation.id, 'human');
- }
- return { status: 'ok', assistantMessage: 'AI 监听已关闭,现有会话已切换为人工接管', data: status };
- }
- function getAgentRuntimeConfig() {
- return { ...workbench.config.agent };
- }
- module.exports = {
- switchActiveAccount,
- getAgentStatus,
- getConversations,
- updateCustomerProfile,
- updateCustomerRecommendation,
- syncConversations,
- changeGlobalMode,
- changeConversationMode,
- approveReply,
- approveDraft,
- rejectDraft,
- regenerateDraft,
- generateLatestDraft,
- manualSend,
- updateCustomerTask,
- syncCustomerTaskToOfficialTodo,
- updateCustomerAlert,
- getAudit,
- startListener,
- stopListener,
- getAgentRuntimeConfig,
- createWorkbench,
- __testing: { loadAgentConfig, accountRuntimeKey, accountWorkbenchOverrides, activeAccountMetadata, FmodeQiweiClient, QiweiAgentPoller, publicConversation, backfillCustomerIntelligence, backfillPropertyRecommendations, detectedPropertiesInMessage },
- };
|