|
@@ -17,7 +17,7 @@ const {
|
|
|
const { createCustomerTaskOfficialSync } = require('../core/customer-task-official-sync');
|
|
const { createCustomerTaskOfficialSync } = require('../core/customer-task-official-sync');
|
|
|
const { messageTimestamp, roomIdOf, isGroupMessage, messageContent, evaluatePolledMessage, normalizePersonalIntakeMode } = require('../core/agent-poller-policy');
|
|
const { messageTimestamp, roomIdOf, isGroupMessage, messageContent, evaluatePolledMessage, normalizePersonalIntakeMode } = require('../core/agent-poller-policy');
|
|
|
const { sortConversationsByRecency, maxConversationTimestamp } = require('../core/conversation-order');
|
|
const { sortConversationsByRecency, maxConversationTimestamp } = require('../core/conversation-order');
|
|
|
-const { saveQiweiClientConfig, setActiveQiweiContext } = require('../core/credentials');
|
|
|
|
|
|
|
+const { saveQiweiClientConfig, setActiveQiweiContext, readFmodeVoiceToken } = 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');
|
|
|
const { normalizeAllowlistIds, normalizeAllowlistContact } = require('../core/allowlist-config');
|
|
const { normalizeAllowlistIds, normalizeAllowlistContact } = require('../core/allowlist-config');
|
|
@@ -28,6 +28,12 @@ const PROJECT_ROOT = WORKSPACE_ROOT;
|
|
|
const ENV_FILE = path.join(PROJECT_ROOT, '.env.local');
|
|
const ENV_FILE = path.join(PROJECT_ROOT, '.env.local');
|
|
|
const INTAKE_AUTOPILOT_CONFIRMATION = 'ENABLE_AUTO_ENROLL_AUTOPILOT';
|
|
const INTAKE_AUTOPILOT_CONFIRMATION = 'ENABLE_AUTO_ENROLL_AUTOPILOT';
|
|
|
const INTAKE_AUTOPILOT_CONFIRMATION_VERSION = 'v1';
|
|
const INTAKE_AUTOPILOT_CONFIRMATION_VERSION = 'v1';
|
|
|
|
|
+const AUTOPILOT_CONFIRMATION = 'ENABLE_AUTOPILOT';
|
|
|
|
|
+
|
|
|
|
|
+function requireAutopilotConfirmation(mode, confirmation, scope = 'global') {
|
|
|
|
|
+ if (mode !== 'autopilot' || confirmation === AUTOPILOT_CONFIRMATION) return;
|
|
|
|
|
+ throw new Error(scope === 'conversation' ? '开启会话全自动接管需要二次确认' : '开启全自动接管需要二次确认');
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
function readEnvFile(filePath) {
|
|
function readEnvFile(filePath) {
|
|
|
try {
|
|
try {
|
|
@@ -178,7 +184,7 @@ function loadAgentConfig(overrides = {}) {
|
|
|
welcomeEnabled: bool('QIWEI_WELCOME_ENABLED', false),
|
|
welcomeEnabled: bool('QIWEI_WELCOME_ENABLED', false),
|
|
|
welcomeText: value('QIWEI_WELCOME_TEXT', '您好,已经收到您的消息,我会尽快为您处理。'),
|
|
welcomeText: value('QIWEI_WELCOME_TEXT', '您好,已经收到您的消息,我会尽快为您处理。'),
|
|
|
welcomeSendMode: value('QIWEI_WELCOME_SEND_MODE', 'draft') === 'send' ? 'send' : 'draft',
|
|
welcomeSendMode: value('QIWEI_WELCOME_SEND_MODE', 'draft') === 'send' ? 'send' : 'draft',
|
|
|
- effectiveAutopilotMode: 'auto',
|
|
|
|
|
|
|
+ effectiveAutopilotMode: 'autopilot',
|
|
|
},
|
|
},
|
|
|
knowledgeDir: resolvePath(
|
|
knowledgeDir: resolvePath(
|
|
|
value('QIWEI_AGENT_KNOWLEDGE_DIR'),
|
|
value('QIWEI_AGENT_KNOWLEDGE_DIR'),
|
|
@@ -231,7 +237,7 @@ function loadAgentConfig(overrides = {}) {
|
|
|
manualAllowedSenders: [...manualAllowedSenders],
|
|
manualAllowedSenders: [...manualAllowedSenders],
|
|
|
autoEnrolledSenders: [],
|
|
autoEnrolledSenders: [],
|
|
|
accountKey: '',
|
|
accountKey: '',
|
|
|
- intakeAutopilotConversationMode: 'auto',
|
|
|
|
|
|
|
+ intakeAutopilotConversationMode: 'autopilot',
|
|
|
selfUserId: value('QIWEI_AUTO_REPLY_SELF_USER_ID'),
|
|
selfUserId: value('QIWEI_AUTO_REPLY_SELF_USER_ID'),
|
|
|
intervalMs: number('QIWEI_AUTO_REPLY_INTERVAL_MS', 10000, 3000, 60000),
|
|
intervalMs: number('QIWEI_AUTO_REPLY_INTERVAL_MS', 10000, 3000, 60000),
|
|
|
initialSyncLimit: number('QIWEI_AGENT_INITIAL_SYNC_LIMIT', 5000, 100, 5000),
|
|
initialSyncLimit: number('QIWEI_AGENT_INITIAL_SYNC_LIMIT', 5000, 100, 5000),
|
|
@@ -243,7 +249,10 @@ function loadAgentConfig(overrides = {}) {
|
|
|
},
|
|
},
|
|
|
voice: {
|
|
voice: {
|
|
|
endpoint: value('QIWEI_VOICE_ENDPOINT', 'https://server.fmode.cn/api/voice/indextts2'),
|
|
endpoint: value('QIWEI_VOICE_ENDPOINT', 'https://server.fmode.cn/api/voice/indextts2'),
|
|
|
- authToken: value('QIWEI_VOICE_AUTH_TOKEN'),
|
|
|
|
|
|
|
+ authToken: readFmodeVoiceToken({
|
|
|
|
|
+ voiceAuthToken: value('QIWEI_VOICE_AUTH_TOKEN'),
|
|
|
|
|
+ endpoint: value('QIWEI_VOICE_ENDPOINT', 'https://server.fmode.cn/api/voice/indextts2'),
|
|
|
|
|
+ }),
|
|
|
model: 'fmode-voice',
|
|
model: 'fmode-voice',
|
|
|
requestTimeoutMs: number('QIWEI_TTS_TIMEOUT_MS', 180000, 15000, 300000),
|
|
requestTimeoutMs: number('QIWEI_TTS_TIMEOUT_MS', 180000, 15000, 300000),
|
|
|
},
|
|
},
|
|
@@ -543,7 +552,7 @@ async function ingestMessageForWorkbench(target, message = {}, source = 'callbac
|
|
|
target.config.autoEnrolledSenders = autoIds;
|
|
target.config.autoEnrolledSenders = autoIds;
|
|
|
target.config.allowedSenders = normalizeAllowlistIds([...(target.config.manualAllowedSenders || []), ...autoIds]);
|
|
target.config.allowedSenders = normalizeAllowlistIds([...(target.config.manualAllowedSenders || []), ...autoIds]);
|
|
|
const conversation = target.db.ensureConversation(senderId, message.senderName || '企微客户');
|
|
const conversation = target.db.ensureConversation(senderId, message.senderName || '企微客户');
|
|
|
- const effectiveMode = intakePolicy.mode === 'auto_enroll_review' ? 'review' : 'auto';
|
|
|
|
|
|
|
+ const effectiveMode = intakePolicy.mode === 'auto_enroll_review' ? 'review' : 'autopilot';
|
|
|
target.service.setConversationMode(conversation.id, effectiveMode, 'policy:auto-enroll');
|
|
target.service.setConversationMode(conversation.id, effectiveMode, 'policy:auto-enroll');
|
|
|
const welcomeState = !intakePolicy.welcomeEnabled
|
|
const welcomeState = !intakePolicy.welcomeEnabled
|
|
|
? 'skipped'
|
|
? 'skipped'
|
|
@@ -900,7 +909,7 @@ function publicConversation(row) {
|
|
|
const pending = currentDrafts.find(item => item.status === 'pending') || null;
|
|
const pending = currentDrafts.find(item => item.status === 'pending') || null;
|
|
|
const latestDraft = pending || currentDrafts.find(item => ['sent', 'approved'].includes(item.status)) || null;
|
|
const latestDraft = pending || currentDrafts.find(item => ['sent', 'approved'].includes(item.status)) || null;
|
|
|
const currentAgentOutcome = latestInbound && detail.agentOutcome?.entityId === latestInbound.id ? detail.agentOutcome : null;
|
|
const currentAgentOutcome = latestInbound && detail.agentOutcome?.entityId === latestInbound.id ? detail.agentOutcome : null;
|
|
|
- const agentError = ['agent_failed', 'agent_not_configured'].includes(currentAgentOutcome?.action)
|
|
|
|
|
|
|
+ const agentError = ['agent_failed', 'agent_not_configured', 'autopilot_send_failed'].includes(currentAgentOutcome?.action)
|
|
|
? { ...currentAgentOutcome, ...friendlyAgentError(currentAgentOutcome.message) }
|
|
? { ...currentAgentOutcome, ...friendlyAgentError(currentAgentOutcome.message) }
|
|
|
: null;
|
|
: null;
|
|
|
const agentNotice = currentAgentOutcome?.action === 'agent_no_reply_needed' ? currentAgentOutcome : null;
|
|
const agentNotice = currentAgentOutcome?.action === 'agent_no_reply_needed' ? currentAgentOutcome : null;
|
|
@@ -1213,7 +1222,7 @@ function intakePolicyPayload(target = workbench) {
|
|
|
welcomeEnabled: policy.welcomeEnabled,
|
|
welcomeEnabled: policy.welcomeEnabled,
|
|
|
welcomeText: policy.welcomeText,
|
|
welcomeText: policy.welcomeText,
|
|
|
welcomeSendMode: policy.welcomeSendMode,
|
|
welcomeSendMode: policy.welcomeSendMode,
|
|
|
- effectiveConversationMode: policy.mode === 'auto_enroll_autopilot' ? 'auto' : 'review',
|
|
|
|
|
|
|
+ effectiveConversationMode: policy.mode === 'auto_enroll_autopilot' ? 'autopilot' : 'review',
|
|
|
autopilotConfirmed: policy.autopilotConfirmation === INTAKE_AUTOPILOT_CONFIRMATION_VERSION,
|
|
autopilotConfirmed: policy.autopilotConfirmation === INTAKE_AUTOPILOT_CONFIRMATION_VERSION,
|
|
|
onboarding: {
|
|
onboarding: {
|
|
|
total: onboardings.length,
|
|
total: onboardings.length,
|
|
@@ -1257,7 +1266,7 @@ function updateIntakePolicyForWorkbench(target, input = {}, actor = 'human') {
|
|
|
target.db.audit({
|
|
target.db.audit({
|
|
|
actor,
|
|
actor,
|
|
|
action: 'personal_intake_policy_updated',
|
|
action: 'personal_intake_policy_updated',
|
|
|
- detail: { mode, welcomeEnabled, welcomeSendMode, effectiveConversationMode: 'auto' },
|
|
|
|
|
|
|
+ detail: { mode, welcomeEnabled, welcomeSendMode, effectiveConversationMode: mode === 'auto_enroll_autopilot' ? 'autopilot' : 'review' },
|
|
|
});
|
|
});
|
|
|
return intakePolicyPayload(target);
|
|
return intakePolicyPayload(target);
|
|
|
}
|
|
}
|
|
@@ -1510,12 +1519,14 @@ async function syncConversations(input = {}) {
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function changeGlobalMode(mode) {
|
|
|
|
|
|
|
+function changeGlobalMode(mode, confirmation = '') {
|
|
|
const selected = String(mode || '');
|
|
const selected = String(mode || '');
|
|
|
|
|
+ requireAutopilotConfirmation(selected, confirmation);
|
|
|
let update;
|
|
let update;
|
|
|
if (['paused', 'monitor'].includes(selected)) update = { paused: true };
|
|
if (['paused', 'monitor'].includes(selected)) update = { paused: true };
|
|
|
else if (['review', 'suggest'].includes(selected)) update = { paused: false, defaultMode: 'review' };
|
|
else if (['review', 'suggest'].includes(selected)) update = { paused: false, defaultMode: 'review' };
|
|
|
else if (selected === 'auto') update = { paused: false, defaultMode: 'auto' };
|
|
else if (selected === 'auto') update = { paused: false, defaultMode: 'auto' };
|
|
|
|
|
+ else if (selected === 'autopilot') update = { paused: false, defaultMode: 'autopilot' };
|
|
|
else if (selected === 'human') update = { paused: false, defaultMode: 'human' };
|
|
else if (selected === 'human') update = { paused: false, defaultMode: 'human' };
|
|
|
else throw new Error('不支持的 Agent 模式');
|
|
else throw new Error('不支持的 Agent 模式');
|
|
|
workbench.db.setSetting('listener_enabled', selected === 'paused' ? 'false' : 'true');
|
|
workbench.db.setSetting('listener_enabled', selected === 'paused' ? 'false' : 'true');
|
|
@@ -1527,10 +1538,11 @@ function changeGlobalMode(mode) {
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function changeConversationMode(id, mode) {
|
|
|
|
|
|
|
+function changeConversationMode(id, mode, confirmation = '') {
|
|
|
const mapped = mode === 'agent' ? 'review' : mode === 'manual' ? 'human' : mode;
|
|
const mapped = mode === 'agent' ? 'review' : mode === 'manual' ? 'human' : mode;
|
|
|
|
|
+ requireAutopilotConfirmation(mapped, confirmation, 'conversation');
|
|
|
const conversation = workbench.service.setConversationMode(id, mapped);
|
|
const conversation = workbench.service.setConversationMode(id, mapped);
|
|
|
- const labels = { review: '待审核', auto: '高置信自动', human: '人工接管', paused: '会话暂停' };
|
|
|
|
|
|
|
+ const labels = { review: '待审核', auto: '高置信自动', autopilot: '全自动接管', human: '人工接管', paused: '会话暂停' };
|
|
|
return { status: 'ok', assistantMessage: `会话已切换为${labels[mapped]}`, data: { conversation } };
|
|
return { status: 'ok', assistantMessage: `会话已切换为${labels[mapped]}`, data: { conversation } };
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -1953,5 +1965,5 @@ module.exports = {
|
|
|
stopListener,
|
|
stopListener,
|
|
|
getAgentRuntimeConfig,
|
|
getAgentRuntimeConfig,
|
|
|
createWorkbench,
|
|
createWorkbench,
|
|
|
- __testing: { loadAgentConfig, normalizeAllowlistIds, normalizeAllowlistContact, refreshAllowedSendersFromEnv, hydrateAccountAllowlist, updateAllowlistForWorkbench, resolveConversationSyncScope, applyManualTakeover, accountRuntimeKey, accountWorkbenchOverrides, activeAccountMetadata, FmodeQiweiClient, QiweiAgentPoller, outboundContactId, recordOutboundMessage, syncOutboundNotification, ingestMessageForWorkbench, conversationChannelInfo, publicConversation, backfillCustomerIntelligence, backfillCustomerMemory, backfillSentVoiceAudioPaths, sentVoiceRuns, pendingVoiceDraft, markVoiceDraftSent, startListenerForWorkbench, intakePolicyPayload, updateIntakePolicyForWorkbench },
|
|
|
|
|
|
|
+ __testing: { loadAgentConfig, normalizeAllowlistIds, normalizeAllowlistContact, refreshAllowedSendersFromEnv, hydrateAccountAllowlist, updateAllowlistForWorkbench, resolveConversationSyncScope, applyManualTakeover, requireAutopilotConfirmation, accountRuntimeKey, accountWorkbenchOverrides, activeAccountMetadata, FmodeQiweiClient, QiweiAgentPoller, outboundContactId, recordOutboundMessage, syncOutboundNotification, ingestMessageForWorkbench, conversationChannelInfo, publicConversation, backfillCustomerIntelligence, backfillCustomerMemory, backfillSentVoiceAudioPaths, sentVoiceRuns, pendingVoiceDraft, markVoiceDraftSent, startListenerForWorkbench, intakePolicyPayload, updateIntakePolicyForWorkbench },
|
|
|
};
|
|
};
|