|
@@ -15,7 +15,7 @@ const {
|
|
|
completeTodoKnowledge,
|
|
completeTodoKnowledge,
|
|
|
} = require('./official-office-knowledge-service');
|
|
} = require('./official-office-knowledge-service');
|
|
|
const { createCustomerTaskOfficialSync } = require('../core/customer-task-official-sync');
|
|
const { createCustomerTaskOfficialSync } = require('../core/customer-task-official-sync');
|
|
|
-const { messageTimestamp, roomIdOf, isGroupMessage, evaluatePolledMessage } = require('../core/agent-poller-policy');
|
|
|
|
|
|
|
+const { messageTimestamp, roomIdOf, isGroupMessage, messageContent, evaluatePolledMessage } = require('../core/agent-poller-policy');
|
|
|
const { setActiveQiweiContext } = require('../core/credentials');
|
|
const { setActiveQiweiContext } = require('../core/credentials');
|
|
|
const { FmodeQiweiClient } = require('../providers/fmode-agent-transport');
|
|
const { FmodeQiweiClient } = require('../providers/fmode-agent-transport');
|
|
|
const { responseMonitor } = require('./response-monitor-service');
|
|
const { responseMonitor } = require('./response-monitor-service');
|
|
@@ -140,13 +140,27 @@ function loadAgentConfig(overrides = {}) {
|
|
|
value('QIWEI_AGENT_KNOWLEDGE_DIR'),
|
|
value('QIWEI_AGENT_KNOWLEDGE_DIR'),
|
|
|
fs.existsSync(path.join(PROJECT_ROOT, 'knowledge')) ? path.join(PROJECT_ROOT, 'knowledge') : path.join(PACKAGE_ROOT, 'knowledge')
|
|
fs.existsSync(path.join(PROJECT_ROOT, 'knowledge')) ? path.join(PROJECT_ROOT, 'knowledge') : path.join(PACKAGE_ROOT, 'knowledge')
|
|
|
),
|
|
),
|
|
|
|
|
+ contextFiles: value('QIWEI_AGENT_CONTEXT_FILES', 'personality.md,context.md,rules.md').split(',').map(item => item.trim()).filter(Boolean),
|
|
|
|
|
+ contextCharLimit: number('QIWEI_AGENT_CONTEXT_CHAR_LIMIT', 6000, 1000, 20000),
|
|
|
propertyDataFile,
|
|
propertyDataFile,
|
|
|
|
|
+ memory: {
|
|
|
|
|
+ enabled: bool('QIWEI_AGENT_MEMORY_ENABLED', true),
|
|
|
|
|
+ coreCharLimit: number('QIWEI_AGENT_MEMORY_CORE_CHAR_LIMIT', 4000, 500, 8000),
|
|
|
|
|
+ recallLimit: number('QIWEI_AGENT_MEMORY_RECALL_LIMIT', 6, 0, 20),
|
|
|
|
|
+ recentMessageLimit: number('QIWEI_AGENT_MEMORY_RECENT_MESSAGES', 12, 4, 40),
|
|
|
|
|
+ historyScanLimit: number('QIWEI_AGENT_MEMORY_HISTORY_SCAN_LIMIT', 240, 20, 1000),
|
|
|
|
|
+ extractionIntervalMs: number('QIWEI_AGENT_MEMORY_EXTRACTION_INTERVAL_MS', 1000, 250, 60000),
|
|
|
|
|
+ extractionRetryBaseMs: number('QIWEI_AGENT_MEMORY_EXTRACTION_RETRY_BASE_MS', 1000, 250, 60000),
|
|
|
|
|
+ extractionMaxAttempts: number('QIWEI_AGENT_MEMORY_EXTRACTION_MAX_ATTEMPTS', 3, 1, 10),
|
|
|
|
|
+ },
|
|
|
agent: {
|
|
agent: {
|
|
|
provider,
|
|
provider,
|
|
|
apiKey: value('AGENT_API_KEY') || value(anthropic ? 'ANTHROPIC_AUTH_TOKEN' : 'OPENAI_API_KEY') || value('QIWEI_AUTO_REPLY_AI_KEY'),
|
|
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(/\/$/, ''),
|
|
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'),
|
|
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),
|
|
maxToolRounds: number('AGENT_MAX_TOOL_ROUNDS', 4, 1, 8),
|
|
|
|
|
+ promptCharLimit: number('QIWEI_AGENT_PROMPT_CHAR_LIMIT', 16000, 2000, 50000),
|
|
|
|
|
+ systemPromptCharLimit: number('QIWEI_AGENT_SYSTEM_PROMPT_CHAR_LIMIT', 12000, 2000, 50000),
|
|
|
claudeExecutable: value('CLAUDE_CODE_EXECUTABLE') || path.join(path.dirname(process.execPath), 'node_modules', '@anthropic-ai', 'claude-code', 'bin', 'claude.exe'),
|
|
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),
|
|
claudeWorkdir: resolvePath(value('CLAUDE_CODE_WORKDIR'), PROJECT_ROOT),
|
|
|
claudeSessionFile: resolvePath(value('CLAUDE_CODE_SESSION_FILE'), latestPath('messages', 'claude-code-sessions.json')),
|
|
claudeSessionFile: resolvePath(value('CLAUDE_CODE_SESSION_FILE'), latestPath('messages', 'claude-code-sessions.json')),
|
|
@@ -158,6 +172,8 @@ function loadAgentConfig(overrides = {}) {
|
|
|
claudeBare: bool('CLAUDE_CODE_BARE', true),
|
|
claudeBare: bool('CLAUDE_CODE_BARE', true),
|
|
|
claudeEffort: value('CLAUDE_CODE_EFFORT', 'low'),
|
|
claudeEffort: value('CLAUDE_CODE_EFFORT', 'low'),
|
|
|
claudeTools: value('CLAUDE_CODE_ALLOWED_TOOLS', 'Read,Glob,Grep'),
|
|
claudeTools: value('CLAUDE_CODE_ALLOWED_TOOLS', 'Read,Glob,Grep'),
|
|
|
|
|
+ claudeSessionMaxTurns: number('CLAUDE_CODE_SESSION_MAX_TURNS', 12, 1, 100),
|
|
|
|
|
+ claudeSessionMaxAgeMs: number('CLAUDE_CODE_SESSION_MAX_AGE_MS', 86400000, 60000, 604800000),
|
|
|
},
|
|
},
|
|
|
qiwei: {
|
|
qiwei: {
|
|
|
transport: 'fmode-gateway',
|
|
transport: 'fmode-gateway',
|
|
@@ -188,6 +204,7 @@ function loadAgentConfig(overrides = {}) {
|
|
|
const config = {
|
|
const config = {
|
|
|
...baseConfig,
|
|
...baseConfig,
|
|
|
...overrides,
|
|
...overrides,
|
|
|
|
|
+ memory: { ...baseConfig.memory, ...(overrides.memory || {}) },
|
|
|
agent: { ...baseConfig.agent, ...(overrides.agent || {}) },
|
|
agent: { ...baseConfig.agent, ...(overrides.agent || {}) },
|
|
|
qiwei: { ...baseConfig.qiwei, ...(overrides.qiwei || {}) },
|
|
qiwei: { ...baseConfig.qiwei, ...(overrides.qiwei || {}) },
|
|
|
voice: { ...baseConfig.voice, ...(overrides.voice || {}) },
|
|
voice: { ...baseConfig.voice, ...(overrides.voice || {}) },
|
|
@@ -256,6 +273,24 @@ function backfillCustomerIntelligence(db) {
|
|
|
return { version, removed, profileFields, taskCount, alertCount };
|
|
return { version, removed, profileFields, taskCount, alertCount };
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+function backfillCustomerMemory(service) {
|
|
|
|
|
+ const version = '2';
|
|
|
|
|
+ const db = service?.db;
|
|
|
|
|
+ if (!db || !service.memory || db.getSetting('customer_memory_backfill_version', '') === version) {
|
|
|
|
|
+ return { skipped: true, conversations: 0, captured: 0 };
|
|
|
|
|
+ }
|
|
|
|
|
+ let conversations = 0;
|
|
|
|
|
+ let captured = 0;
|
|
|
|
|
+ for (const conversation of db.listConversations()) {
|
|
|
|
|
+ const result = service.memory.backfillConversation(conversation);
|
|
|
|
|
+ conversations += 1;
|
|
|
|
|
+ captured += Number(result.captured || 0);
|
|
|
|
|
+ }
|
|
|
|
|
+ db.setSetting('customer_memory_backfill_version', version);
|
|
|
|
|
+ db.audit({ actor: 'migration', action: 'customer_memory_backfilled', detail: { version, conversations, captured } });
|
|
|
|
|
+ return { version, conversations, captured };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
|
|
|
|
|
|
class QiweiAgentPoller {
|
|
class QiweiAgentPoller {
|
|
@@ -360,14 +395,86 @@ class QiweiAgentPoller {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
async process(message) {
|
|
async process(message) {
|
|
|
- return ingestMessageForWorkbench({ config: this.config, db: this.db, service: this.service }, message, 'polling');
|
|
|
|
|
|
|
+ return ingestMessageForWorkbench({ config: this.config, db: this.db, service: this.service, qiwei: this.qiwei }, message, 'polling');
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const OUTBOUND_NOTIFICATION_MSG_TYPE = 2001;
|
|
|
|
|
+const OUTBOUND_CONTENT_MSG_TYPES = new Set([0, 1, 2]);
|
|
|
|
|
+
|
|
|
|
|
+function outboundContactId(message = {}, config = {}) {
|
|
|
|
|
+ const senderId = String(message.senderId || '');
|
|
|
|
|
+ const receiverId = String(message.receiverId || '');
|
|
|
|
|
+ const allowedSenders = new Set((config.allowedSenders || []).map(String));
|
|
|
|
|
+ const selfUserIds = new Set([
|
|
|
|
|
+ String(config.selfUserId || ''),
|
|
|
|
|
+ String(config.userId || ''),
|
|
|
|
|
+ String(message.userId || ''),
|
|
|
|
|
+ ].filter(Boolean));
|
|
|
|
|
+ return selfUserIds.has(senderId) && allowedSenders.has(receiverId) ? receiverId : '';
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function recordOutboundMessage(target, message, source, contactId) {
|
|
|
|
|
+ const content = messageContent(message);
|
|
|
|
|
+ if (!content) throw new Error('Outbound message content is empty');
|
|
|
|
|
+ const timestamp = messageTimestamp(message.timestamp);
|
|
|
|
|
+ if (!timestamp) throw new Error('Outbound message timestamp is invalid');
|
|
|
|
|
+ const existing = target.db.getConversationByContactId(contactId);
|
|
|
|
|
+ const conversation = target.db.ensureConversation(contactId, existing?.contact_name || message.receiverName || '白名单企微联系人');
|
|
|
|
|
+ const inserted = target.db.insertMessage({
|
|
|
|
|
+ conversationId: conversation.id,
|
|
|
|
|
+ externalId: String(message.msgServerId || message.msgUniqueIdentifier || `${message.senderId}:${message.seq}`),
|
|
|
|
|
+ direction: 'outbound',
|
|
|
|
|
+ senderType: 'human',
|
|
|
|
|
+ content,
|
|
|
|
|
+ status: 'sent',
|
|
|
|
|
+ createdAt: new Date(timestamp * 1000).toISOString(),
|
|
|
|
|
+ raw: { ...message, source },
|
|
|
|
|
+ });
|
|
|
|
|
+ if (inserted.created) {
|
|
|
|
|
+ target.db.audit({
|
|
|
|
|
+ actor: 'runtime',
|
|
|
|
|
+ action: 'outbound_message_captured',
|
|
|
|
|
+ conversationId: conversation.id,
|
|
|
|
|
+ entityId: inserted.message.id,
|
|
|
|
|
+ detail: { source, seq: Number(message.seq) || 0 },
|
|
|
|
|
+ });
|
|
|
|
|
+ target.service?.emit?.('change', { type: 'message', conversationId: conversation.id });
|
|
|
|
|
+ }
|
|
|
|
|
+ return { status: inserted.created ? 'outbound_recorded' : 'outbound_duplicate', message: inserted.message };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function syncOutboundNotification(target, notification, source, contactId) {
|
|
|
|
|
+ if (!target.qiwei?.syncMessages) throw new Error('Outbound message sync is unavailable');
|
|
|
|
|
+ const notificationSeq = Number(notification.seq) || 0;
|
|
|
|
|
+ const senderId = String(notification.senderId || '');
|
|
|
|
|
+ const delays = [0, 250, 750, 1500];
|
|
|
|
|
+ for (const delayMs of delays) {
|
|
|
|
|
+ if (delayMs) await new Promise(resolve => setTimeout(resolve, delayMs));
|
|
|
|
|
+ const result = await target.qiwei.syncMessages(Math.max(0, notificationSeq - 1), 10);
|
|
|
|
|
+ const message = (result.syncMsgList || []).find(item => (
|
|
|
|
|
+ Number(item.seq) >= notificationSeq
|
|
|
|
|
+ && String(item.senderId || '') === senderId
|
|
|
|
|
+ && String(item.receiverId || '') === contactId
|
|
|
|
|
+ && OUTBOUND_CONTENT_MSG_TYPES.has(Number(item.msgType))
|
|
|
|
|
+ && Boolean(messageContent(item))
|
|
|
|
|
+ ));
|
|
|
|
|
+ if (message) return recordOutboundMessage(target, message, `${source}:outbound-sync`, contactId);
|
|
|
}
|
|
}
|
|
|
|
|
+ throw new Error(`Outbound message content is not available after notification seq=${notificationSeq}`);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
async function ingestMessageForWorkbench(target, message = {}, source = 'callback') {
|
|
async function ingestMessageForWorkbench(target, message = {}, source = 'callback') {
|
|
|
if (isGroupMessage(message)) {
|
|
if (isGroupMessage(message)) {
|
|
|
return { status: 'ignored_group_reply_disabled' };
|
|
return { status: 'ignored_group_reply_disabled' };
|
|
|
}
|
|
}
|
|
|
|
|
+ const contactId = outboundContactId(message, target.config);
|
|
|
|
|
+ if (contactId && Number(message.msgType) === OUTBOUND_NOTIFICATION_MSG_TYPE) {
|
|
|
|
|
+ return syncOutboundNotification(target, message, source, contactId);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (contactId && OUTBOUND_CONTENT_MSG_TYPES.has(Number(message.msgType)) && messageContent(message)) {
|
|
|
|
|
+ return recordOutboundMessage(target, message, source, contactId);
|
|
|
|
|
+ }
|
|
|
const candidate = evaluatePolledMessage(message, target.config);
|
|
const candidate = evaluatePolledMessage(message, target.config);
|
|
|
if (!candidate.eligible) return { status: `ignored_${candidate.reason || 'ineligible'}` };
|
|
if (!candidate.eligible) return { status: `ignored_${candidate.reason || 'ineligible'}` };
|
|
|
const { content, senderId, timestamp } = candidate;
|
|
const { content, senderId, timestamp } = candidate;
|
|
@@ -403,6 +510,8 @@ function createWorkbench(overrides = {}) {
|
|
|
const knowledge = overrides.knowledge || new AgentKnowledgeStore({
|
|
const knowledge = overrides.knowledge || new AgentKnowledgeStore({
|
|
|
knowledgeDir: config.knowledgeDir,
|
|
knowledgeDir: config.knowledgeDir,
|
|
|
propertyDataFile: config.propertyDataFile,
|
|
propertyDataFile: config.propertyDataFile,
|
|
|
|
|
+ contextFiles: config.contextFiles,
|
|
|
|
|
+ contextCharLimit: config.contextCharLimit,
|
|
|
});
|
|
});
|
|
|
backfillPropertyRecommendations(db, knowledge);
|
|
backfillPropertyRecommendations(db, knowledge);
|
|
|
backfillSentVoiceAudioPaths(db);
|
|
backfillSentVoiceAudioPaths(db);
|
|
@@ -410,6 +519,7 @@ function createWorkbench(overrides = {}) {
|
|
|
const voice = overrides.voice || new VoiceCloneService({ config: config.voice, qiwei });
|
|
const voice = overrides.voice || new VoiceCloneService({ config: config.voice, qiwei });
|
|
|
const agent = overrides.agent || new QiweiAgentRuntime({ config: config.agent, knowledge });
|
|
const agent = overrides.agent || new QiweiAgentRuntime({ config: config.agent, knowledge });
|
|
|
const service = overrides.service || new AgentWorkbenchService({ db, agent, qiwei, config });
|
|
const service = overrides.service || new AgentWorkbenchService({ db, agent, qiwei, config });
|
|
|
|
|
+ backfillCustomerMemory(service);
|
|
|
const poller = overrides.poller || new QiweiAgentPoller({ config: config.qiwei, db, qiwei, service });
|
|
const poller = overrides.poller || new QiweiAgentPoller({ config: config.qiwei, db, qiwei, service });
|
|
|
return { config, db, knowledge, qiwei, voice, agent, service, poller };
|
|
return { config, db, knowledge, qiwei, voice, agent, service, poller };
|
|
|
}
|
|
}
|
|
@@ -858,12 +968,31 @@ function publicConversation(row) {
|
|
|
lastRecommendedAt: item.last_recommended_at,
|
|
lastRecommendedAt: item.last_recommended_at,
|
|
|
updatedAt: item.updated_at,
|
|
updatedAt: item.updated_at,
|
|
|
})),
|
|
})),
|
|
|
|
|
+ memories: (detail.memories || []).map(item => ({
|
|
|
|
|
+ id: item.id,
|
|
|
|
|
+ type: item.type,
|
|
|
|
|
+ content: item.content,
|
|
|
|
|
+ confidence: item.confidence,
|
|
|
|
|
+ importance: item.importance,
|
|
|
|
|
+ status: item.status,
|
|
|
|
|
+ sourceMessageIds: item.source_message_ids || [],
|
|
|
|
|
+ direction: item.direction,
|
|
|
|
|
+ createdBy: item.created_by,
|
|
|
|
|
+ expiresAt: item.expires_at,
|
|
|
|
|
+ updatedAt: item.updated_at,
|
|
|
|
|
+ })),
|
|
|
|
|
+ memorySnapshot: detail.memorySnapshot ? {
|
|
|
|
|
+ version: detail.memorySnapshot.version,
|
|
|
|
|
+ coreChars: String(detail.memorySnapshot.compact_text || '').length,
|
|
|
|
|
+ createdAt: detail.memorySnapshot.created_at,
|
|
|
|
|
+ } : null,
|
|
|
summary: {
|
|
summary: {
|
|
|
openTasks: customerTasks.filter(item => ['open', 'in_progress'].includes(item.status)).length,
|
|
openTasks: customerTasks.filter(item => ['open', 'in_progress'].includes(item.status)).length,
|
|
|
openAlerts: customerAlerts.filter(item => item.status === 'open').length,
|
|
openAlerts: customerAlerts.filter(item => item.status === 'open').length,
|
|
|
highAlerts: customerAlerts.filter(item => item.status === 'open' && ['high', 'critical'].includes(item.severity)).length,
|
|
highAlerts: customerAlerts.filter(item => item.status === 'open' && ['high', 'critical'].includes(item.severity)).length,
|
|
|
recommendationCount: customerRecommendations.length,
|
|
recommendationCount: customerRecommendations.length,
|
|
|
pendingFeedbackCount: customerRecommendations.filter(item => ['candidate', 'recommended'].includes(item.status)).length,
|
|
pendingFeedbackCount: customerRecommendations.filter(item => ['candidate', 'recommended'].includes(item.status)).length,
|
|
|
|
|
+ activeMemories: (detail.memories || []).filter(item => item.status === 'active').length,
|
|
|
},
|
|
},
|
|
|
},
|
|
},
|
|
|
pendingReply: pending ? {
|
|
pendingReply: pending ? {
|
|
@@ -943,6 +1072,7 @@ async function ingestWebhookMessage(message = {}) {
|
|
|
config: workbench.config.qiwei,
|
|
config: workbench.config.qiwei,
|
|
|
db: workbench.db,
|
|
db: workbench.db,
|
|
|
service: workbench.service,
|
|
service: workbench.service,
|
|
|
|
|
+ qiwei: workbench.qiwei,
|
|
|
}, message, 'relay_callback');
|
|
}, message, 'relay_callback');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -1101,6 +1231,7 @@ function updateCustomerProfile(conversationId, input = {}) {
|
|
|
? current.tags
|
|
? current.tags
|
|
|
: [...new Set((Array.isArray(input.tags) ? input.tags : String(input.tags || '').split(/[,,]/)).map(item => String(item).trim()).filter(Boolean))];
|
|
: [...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);
|
|
const updated = workbench.db.updateProfile(conversationId, nextProfile, tags);
|
|
|
|
|
+ const memory = workbench.service.memory.syncHumanProfile(conversationId, patch);
|
|
|
workbench.db.audit({
|
|
workbench.db.audit({
|
|
|
actor: 'human',
|
|
actor: 'human',
|
|
|
action: 'customer_profile_updated',
|
|
action: 'customer_profile_updated',
|
|
@@ -1113,7 +1244,7 @@ function updateCustomerProfile(conversationId, input = {}) {
|
|
|
status: 'ok',
|
|
status: 'ok',
|
|
|
assistantMessage: `客户“${conversation.contact_name || '未命名客户'}”的主档已更新。`,
|
|
assistantMessage: `客户“${conversation.contact_name || '未命名客户'}”的主档已更新。`,
|
|
|
summary: { changedFields, tagCount: tags.length },
|
|
summary: { changedFields, tagCount: tags.length },
|
|
|
- data: { profile: visibleProfile, profileEvidence: __evidence || {}, tags, updatedAt: updated.updatedAt },
|
|
|
|
|
|
|
+ data: { profile: visibleProfile, profileEvidence: __evidence || {}, tags, updatedAt: updated.updatedAt, memory },
|
|
|
warnings: [],
|
|
warnings: [],
|
|
|
errors: [],
|
|
errors: [],
|
|
|
};
|
|
};
|
|
@@ -1538,6 +1669,34 @@ function updateCustomerAlert(alertId, input = {}) {
|
|
|
return { status: 'ok', assistantMessage: `客户预警已更新为 ${alert.status}`, data: { alert } };
|
|
return { status: 'ok', assistantMessage: `客户预警已更新为 ${alert.status}`, data: { alert } };
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+function addCustomerMemory(conversationId, input = {}) {
|
|
|
|
|
+ const result = workbench.service.addCustomerMemory(conversationId, input, 'human');
|
|
|
|
|
+ return { status: 'ok', assistantMessage: '客户长期记忆已添加,并将在后续回复中生效', data: result };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function updateCustomerMemory(memoryId, input = {}) {
|
|
|
|
|
+ const action = String(input.action || 'update');
|
|
|
|
|
+ if (action === 'forget') {
|
|
|
|
|
+ const result = workbench.service.forgetCustomerMemory(memoryId, 'human');
|
|
|
|
|
+ return { status: 'ok', assistantMessage: '客户记忆已彻底遗忘', data: result };
|
|
|
|
|
+ }
|
|
|
|
|
+ const patch = action === 'confirm'
|
|
|
|
|
+ ? { type: 'fact', status: 'active', confidence: 1 }
|
|
|
|
|
+ : action === 'reject'
|
|
|
|
|
+ ? { status: 'rejected' }
|
|
|
|
|
+ : {
|
|
|
|
|
+ content: input.content,
|
|
|
|
|
+ type: input.type,
|
|
|
|
|
+ status: input.status,
|
|
|
|
|
+ importance: input.importance,
|
|
|
|
|
+ expiresAt: input.expiresAt,
|
|
|
|
|
+ };
|
|
|
|
|
+ const sanitized = Object.fromEntries(Object.entries(patch).filter(([, value]) => value !== undefined));
|
|
|
|
|
+ const result = workbench.service.updateCustomerMemory(memoryId, sanitized, 'human');
|
|
|
|
|
+ const message = action === 'confirm' ? '推断已人工确认为客户事实' : action === 'reject' ? '客户记忆已拒绝,不再进入上下文' : '客户记忆已更新';
|
|
|
|
|
+ return { status: 'ok', assistantMessage: message, data: result };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
function getAudit(limit = 200) {
|
|
function getAudit(limit = 200) {
|
|
|
return { status: 'ok', data: { audit: workbench.db.listAudit(Math.max(1, Math.min(500, Number(limit) || 200))) } };
|
|
return { status: 'ok', data: { audit: workbench.db.listAudit(Math.max(1, Math.min(500, Number(limit) || 200))) } };
|
|
|
}
|
|
}
|
|
@@ -1557,6 +1716,7 @@ async function ingestRuntimeMessage(message = {}, options = {}) {
|
|
|
config: workbench.config.qiwei,
|
|
config: workbench.config.qiwei,
|
|
|
db: workbench.db,
|
|
db: workbench.db,
|
|
|
service: workbench.service,
|
|
service: workbench.service,
|
|
|
|
|
+ qiwei: workbench.qiwei,
|
|
|
}, message, String(options.source || 'runtime'));
|
|
}, message, String(options.source || 'runtime'));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -1627,6 +1787,8 @@ module.exports = {
|
|
|
updateCustomerTask,
|
|
updateCustomerTask,
|
|
|
syncCustomerTaskToOfficialTodo,
|
|
syncCustomerTaskToOfficialTodo,
|
|
|
updateCustomerAlert,
|
|
updateCustomerAlert,
|
|
|
|
|
+ addCustomerMemory,
|
|
|
|
|
+ updateCustomerMemory,
|
|
|
getAudit,
|
|
getAudit,
|
|
|
ingestRuntimeMessage,
|
|
ingestRuntimeMessage,
|
|
|
ingestWebhookMessage,
|
|
ingestWebhookMessage,
|
|
@@ -1634,5 +1796,5 @@ module.exports = {
|
|
|
stopListener,
|
|
stopListener,
|
|
|
getAgentRuntimeConfig,
|
|
getAgentRuntimeConfig,
|
|
|
createWorkbench,
|
|
createWorkbench,
|
|
|
- __testing: { loadAgentConfig, normalizeAllowlistIds, normalizeAllowlistContact, accountRuntimeKey, accountWorkbenchOverrides, activeAccountMetadata, FmodeQiweiClient, QiweiAgentPoller, ingestMessageForWorkbench, conversationChannelInfo, publicConversation, backfillCustomerIntelligence, backfillPropertyRecommendations, backfillSentVoiceAudioPaths, sentVoiceRuns, pendingVoiceDraft, markVoiceDraftSent, detectedPropertiesInMessage, startListenerForWorkbench },
|
|
|
|
|
|
|
+ __testing: { loadAgentConfig, normalizeAllowlistIds, normalizeAllowlistContact, accountRuntimeKey, accountWorkbenchOverrides, activeAccountMetadata, FmodeQiweiClient, QiweiAgentPoller, outboundContactId, recordOutboundMessage, syncOutboundNotification, ingestMessageForWorkbench, conversationChannelInfo, publicConversation, backfillCustomerIntelligence, backfillCustomerMemory, backfillPropertyRecommendations, backfillSentVoiceAudioPaths, sentVoiceRuns, pendingVoiceDraft, markVoiceDraftSent, detectedPropertiesInMessage, startListenerForWorkbench },
|
|
|
};
|
|
};
|