|
|
@@ -16,7 +16,7 @@ const {
|
|
|
} = require('./official-office-knowledge-service');
|
|
|
const { createCustomerTaskOfficialSync } = require('../core/customer-task-official-sync');
|
|
|
const { messageTimestamp, roomIdOf, isGroupMessage, messageContent, evaluatePolledMessage } = require('../core/agent-poller-policy');
|
|
|
-const { setActiveQiweiContext } = require('../core/credentials');
|
|
|
+const { saveQiweiClientConfig, setActiveQiweiContext } = require('../core/credentials');
|
|
|
const { FmodeQiweiClient } = require('../providers/fmode-agent-transport');
|
|
|
const { responseMonitor } = require('./response-monitor-service');
|
|
|
const { normalizeAllowlistIds, normalizeAllowlistContact, writeEnvValue } = require('../core/allowlist-config');
|
|
|
@@ -44,6 +44,18 @@ function readEnvFile(filePath) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+function refreshAllowedSendersFromEnv(config = {}, envFile = ENV_FILE) {
|
|
|
+ const env = readEnvFile(envFile);
|
|
|
+ if (!Object.prototype.hasOwnProperty.call(env, 'QIWEI_AUTO_REPLY_ALLOWED_SENDERS')) {
|
|
|
+ return { changed: false, count: Array.isArray(config.allowedSenders) ? config.allowedSenders.length : 0 };
|
|
|
+ }
|
|
|
+ const next = normalizeAllowlistIds(env.QIWEI_AUTO_REPLY_ALLOWED_SENDERS);
|
|
|
+ const current = Array.isArray(config.allowedSenders) ? config.allowedSenders.map(String) : [];
|
|
|
+ const changed = next.length !== current.length || next.some((id, index) => id !== current[index]);
|
|
|
+ if (changed) config.allowedSenders = next;
|
|
|
+ return { changed, count: next.length };
|
|
|
+}
|
|
|
+
|
|
|
function readRuntimeState() {
|
|
|
try {
|
|
|
const filePath = path.join(outputsRoot(), 'runtime', 'qiwei-runtime.json');
|
|
|
@@ -133,7 +145,7 @@ function loadAgentConfig(overrides = {}) {
|
|
|
const baseConfig = {
|
|
|
dbPath: resolvePath(value('QIWEI_AGENT_DB_PATH'), latestPath('messages', 'agent-workbench.db')),
|
|
|
legacyDbPath: path.resolve(PACKAGE_ROOT, '..', '..', 'qiwei-agent-workbench', 'data', 'workbench.db'),
|
|
|
- globalDefaultPaused: bool('QIWEI_AGENT_GLOBAL_DEFAULT_PAUSED', true),
|
|
|
+ globalDefaultPaused: bool('QIWEI_AGENT_GLOBAL_DEFAULT_PAUSED', false),
|
|
|
conversationDefaultMode: value('QIWEI_AGENT_DEFAULT_MODE', 'review'),
|
|
|
autoSendConfidence: number('QIWEI_AGENT_AUTO_SEND_CONFIDENCE', 0.88, 0, 1),
|
|
|
knowledgeDir: resolvePath(
|
|
|
@@ -395,6 +407,10 @@ class QiweiAgentPoller {
|
|
|
}
|
|
|
|
|
|
async process(message) {
|
|
|
+ const allowlist = refreshAllowedSendersFromEnv(this.config);
|
|
|
+ if (allowlist.changed) {
|
|
|
+ this.db.audit({ actor: 'runtime', action: 'poller_allowlist_reloaded', detail: { count: allowlist.count } });
|
|
|
+ }
|
|
|
return ingestMessageForWorkbench({ config: this.config, db: this.db, service: this.service, qiwei: this.qiwei }, message, 'polling');
|
|
|
}
|
|
|
}
|
|
|
@@ -644,9 +660,15 @@ async function switchActiveAccount(input = {}) {
|
|
|
const accountPatch = {
|
|
|
...account,
|
|
|
apiBase: account.apiBase || currentAccount.apiBase,
|
|
|
+ guid: account.guid || (accountChanged ? '' : currentAccount.guid),
|
|
|
};
|
|
|
- if (!accountPatch.guid) delete accountPatch.guid;
|
|
|
applyActiveAccountContext({ ...currentAccount, ...accountPatch });
|
|
|
+ saveQiweiClientConfig({
|
|
|
+ uid: accountPatch.uid,
|
|
|
+ guid: accountPatch.guid,
|
|
|
+ apiBase: accountPatch.apiBase,
|
|
|
+ envRoot: PROJECT_ROOT,
|
|
|
+ });
|
|
|
const status = provisionalAccountStatus();
|
|
|
accountStatusCache = { checkedAt: Date.now(), value: status };
|
|
|
void refreshAccountStatus();
|
|
|
@@ -1120,6 +1142,8 @@ async function getAllowlistCandidates() {
|
|
|
}
|
|
|
|
|
|
function updateAllowlist(input = {}) {
|
|
|
+ refreshAllowedSendersFromEnv(workbench.config.qiwei);
|
|
|
+ const previousIds = new Set(workbench.config.qiwei.allowedSenders.map(String));
|
|
|
const ids = normalizeAllowlistIds(input.contactIds || input.allowedSenders || []);
|
|
|
const serialized = ids.join(',');
|
|
|
writeEnvValue(ENV_FILE, 'QIWEI_AUTO_REPLY_ALLOWED_SENDERS', serialized);
|
|
|
@@ -1127,32 +1151,54 @@ function updateAllowlist(input = {}) {
|
|
|
process.env.QIWEI_AUTO_REPLY_ALLOWED_SENDERS = serialized;
|
|
|
|
|
|
let listenerStopped = false;
|
|
|
+ const contactNames = input.contactNames || {};
|
|
|
+ const addedIds = ids.filter(id => !previousIds.has(id));
|
|
|
for (const target of workbenches.values()) {
|
|
|
target.config.qiwei.allowedSenders = [...ids];
|
|
|
+ for (const contactId of addedIds) {
|
|
|
+ target.db.ensureConversation(contactId, String(contactNames[contactId] || ''));
|
|
|
+ }
|
|
|
+ if (ids.length && input.autoStart !== false) {
|
|
|
+ target.db.setSetting('listener_enabled', 'true');
|
|
|
+ target.service.setGlobal({ paused: false }, input.actor || 'allowlist');
|
|
|
+ } else if (!ids.length) {
|
|
|
+ target.db.setSetting('listener_enabled', 'false');
|
|
|
+ }
|
|
|
if (!ids.length && target.poller.status().running) {
|
|
|
target.poller.stop();
|
|
|
listenerStopped = true;
|
|
|
}
|
|
|
}
|
|
|
workbench.db.audit({
|
|
|
- actor: 'human',
|
|
|
+ actor: input.actor || 'human',
|
|
|
action: 'allowlist_updated',
|
|
|
- detail: { count: ids.length, listenerStopped },
|
|
|
+ detail: { count: ids.length, addedCount: addedIds.length, listenerStopped, autoStart: ids.length > 0 && input.autoStart !== false },
|
|
|
});
|
|
|
return {
|
|
|
status: 'ok',
|
|
|
assistantMessage: ids.length
|
|
|
- ? `白名单已保存,共 ${ids.length} 位联系人,立即生效`
|
|
|
+ ? `白名单已保存,共 ${ids.length} 位联系人;AI 监听将自动保持开启并按当前审核策略处理`
|
|
|
: '白名单已清空,AI 监听已停止',
|
|
|
- summary: { selectedCount: ids.length, listenerStopped },
|
|
|
- data: { selectedIds: ids, selectedCount: ids.length },
|
|
|
+ summary: { selectedCount: ids.length, addedCount: addedIds.length, listenerStopped, autoStart: ids.length > 0 && input.autoStart !== false },
|
|
|
+ data: { selectedIds: ids, selectedCount: ids.length, addedIds },
|
|
|
warnings: [],
|
|
|
errors: [],
|
|
|
};
|
|
|
}
|
|
|
|
|
|
+function addAllowlistContacts(input = {}) {
|
|
|
+ refreshAllowedSendersFromEnv(workbench.config.qiwei);
|
|
|
+ const contactIds = normalizeAllowlistIds(input.contactIds || []);
|
|
|
+ return updateAllowlist({
|
|
|
+ ...input,
|
|
|
+ contactIds: [...workbench.config.qiwei.allowedSenders, ...contactIds],
|
|
|
+ autoStart: input.autoStart !== false,
|
|
|
+ actor: input.actor || 'agent:test-contact',
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
function displayableConversations() {
|
|
|
- return workbench.db.listConversations().filter(item => item.last_message_at || item.last_content || Number(item.pending_count || 0) > 0);
|
|
|
+ return workbench.db.listConversations();
|
|
|
}
|
|
|
|
|
|
function getConversations() {
|
|
|
@@ -1280,9 +1326,30 @@ function updateCustomerRecommendation(conversationId, recommendationId, input =
|
|
|
};
|
|
|
}
|
|
|
|
|
|
-async function syncConversations() {
|
|
|
+function resolveConversationSyncScope(input = {}, allowlist = new Set(), db = workbench.db) {
|
|
|
+ const conversationId = String(input.conversationId || '').trim();
|
|
|
+ let contactId = String(input.contactId || '').trim();
|
|
|
+ if (conversationId) {
|
|
|
+ const conversation = db.getConversation(conversationId);
|
|
|
+ if (!conversation) throw new Error('当前会话不存在,请刷新后重试');
|
|
|
+ if (conversationChannelInfo(conversation, db.listMessages(conversation.id, 100)).channelType !== 'private') {
|
|
|
+ throw new Error('当前采集仅支持个人聊天会话');
|
|
|
+ }
|
|
|
+ contactId = String(conversation.contact_id || '').trim();
|
|
|
+ }
|
|
|
+ if (contactId && !allowlist.has(contactId)) throw new Error('当前联系人不在个人消息白名单中');
|
|
|
+ return {
|
|
|
+ conversationId,
|
|
|
+ contactId,
|
|
|
+ contacts: contactId ? new Set([contactId]) : allowlist,
|
|
|
+ scope: contactId ? 'conversation' : 'allowlist',
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+async function syncConversations(input = {}) {
|
|
|
const allowlist = new Set(workbench.config.qiwei.allowedSenders.map(String));
|
|
|
if (!allowlist.size) throw new Error('测试联系人白名单为空,无法同步会话');
|
|
|
+ const syncScope = resolveConversationSyncScope(input, allowlist);
|
|
|
const account = await detectAccountStatus(true);
|
|
|
if (!account.online) throw new Error('测试账号当前不在线,无法同步企微会话');
|
|
|
|
|
|
@@ -1302,7 +1369,7 @@ async function syncConversations() {
|
|
|
if (isGroupMessage(message)) continue;
|
|
|
const senderId = String(message.senderId || '');
|
|
|
const receiverId = String(message.receiverId || '');
|
|
|
- const contactId = allowlist.has(senderId) ? senderId : allowlist.has(receiverId) ? receiverId : '';
|
|
|
+ const contactId = syncScope.contacts.has(senderId) ? senderId : syncScope.contacts.has(receiverId) ? receiverId : '';
|
|
|
const content = String(message.msgData?.content || '').trim();
|
|
|
if (!contactId || !content || ![0, 1, 2].includes(Number(message.msgType))) continue;
|
|
|
|
|
|
@@ -1363,15 +1430,21 @@ async function syncConversations() {
|
|
|
workbench.db.audit({
|
|
|
actor: 'human',
|
|
|
action: 'conversation_history_synced',
|
|
|
- detail: { conversations: grouped.size, insertedMessages: syncedMessages, removedImportedMessages, scannedMessages, duplicatesRemoved },
|
|
|
+ detail: { scope: syncScope.scope, contactId: syncScope.contactId || null, conversations: grouped.size, insertedMessages: syncedMessages, removedImportedMessages, scannedMessages, duplicatesRemoved },
|
|
|
});
|
|
|
|
|
|
return {
|
|
|
status: 'ok',
|
|
|
assistantMessage: grouped.size
|
|
|
- ? `已补采 ${grouped.size} 个白名单真实会话的最近消息;只入库,不运行 Agent、不发送回复`
|
|
|
- : '未读取到白名单联系人的文字会话,请先在企微中与该联系人收发一条消息',
|
|
|
+ ? syncScope.scope === 'conversation'
|
|
|
+ ? '已采集当前个人会话的最近消息;只入库,不运行 Agent、不发送回复'
|
|
|
+ : `已补采 ${grouped.size} 个白名单真实会话的最近消息;只入库,不运行 Agent、不发送回复`
|
|
|
+ : syncScope.scope === 'conversation'
|
|
|
+ ? '当前个人会话暂无可采集的文字消息,请先在企微中收发一条消息'
|
|
|
+ : '未读取到白名单联系人的文字会话,请先在企微中与该联系人收发一条消息',
|
|
|
data: {
|
|
|
+ scope: syncScope.scope,
|
|
|
+ conversationId: syncScope.conversationId || null,
|
|
|
syncedConversationCount: grouped.size,
|
|
|
syncedMessageCount: syncedMessages,
|
|
|
scannedMessageCount: scannedMessages,
|
|
|
@@ -1388,6 +1461,7 @@ function changeGlobalMode(mode) {
|
|
|
else if (selected === 'auto') update = { paused: false, defaultMode: 'auto' };
|
|
|
else if (selected === 'human') update = { paused: false, defaultMode: 'human' };
|
|
|
else throw new Error('不支持的 Agent 模式');
|
|
|
+ workbench.db.setSetting('listener_enabled', selected === 'paused' ? 'false' : 'true');
|
|
|
const global = workbench.service.setGlobal(update);
|
|
|
return {
|
|
|
status: 'ok',
|
|
|
@@ -1701,8 +1775,14 @@ function getAudit(limit = 200) {
|
|
|
return { status: 'ok', data: { audit: workbench.db.listAudit(Math.max(1, Math.min(500, Number(limit) || 200))) } };
|
|
|
}
|
|
|
|
|
|
-async function startListenerForWorkbench(target, account) {
|
|
|
+async function startListenerForWorkbench(target, account, options = {}) {
|
|
|
if (!account.online) throw new Error(`${account.nickname || '当前账号'}不在线,无法启动真实消息监听`);
|
|
|
+ refreshAllowedSendersFromEnv(target.config.qiwei, options.envFile || ENV_FILE);
|
|
|
+ if (options.automatic === true && target.db?.getSetting?.('listener_enabled', 'true') === 'false') {
|
|
|
+ return { status: 'ok', assistantMessage: 'AI 监听保持人工关闭状态', data: { running: false, disabled: true } };
|
|
|
+ }
|
|
|
+ target.db?.setSetting?.('listener_enabled', 'true');
|
|
|
+ target.service?.setGlobal?.({ paused: false }, options.automatic ? 'runtime:auto-start' : 'human');
|
|
|
const status = await target.poller.start();
|
|
|
return {
|
|
|
status: 'ok',
|
|
|
@@ -1720,7 +1800,7 @@ async function ingestRuntimeMessage(message = {}, options = {}) {
|
|
|
}, message, String(options.source || 'runtime'));
|
|
|
}
|
|
|
|
|
|
-async function startListener() {
|
|
|
+async function startListener(options = {}) {
|
|
|
const product = getProductMode();
|
|
|
if (product.mode === 'enterprise') {
|
|
|
const runtime = listenerStateWithRuntime(product, workbench.poller.status());
|
|
|
@@ -1733,28 +1813,45 @@ async function startListener() {
|
|
|
};
|
|
|
}
|
|
|
const account = await detectAccountStatus(true);
|
|
|
- return startListenerForWorkbench(workbench, account);
|
|
|
+ return startListenerForWorkbench(workbench, account, options);
|
|
|
}
|
|
|
|
|
|
-function stopListener() {
|
|
|
+function applyManualTakeover(target) {
|
|
|
+ target.service.setGlobal({ paused: true, defaultMode: 'review' });
|
|
|
+ for (const conversation of target.db.listConversations()) {
|
|
|
+ target.service.setConversationMode(conversation.id, 'human');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function stopListener({ preserveAgentState = false } = {}) {
|
|
|
const product = getProductMode();
|
|
|
if (product.mode === 'enterprise') {
|
|
|
- workbench.service.setGlobal({ paused: true, defaultMode: 'review' });
|
|
|
- for (const conversation of workbench.db.listConversations()) {
|
|
|
- workbench.service.setConversationMode(conversation.id, 'human');
|
|
|
+ if (!preserveAgentState) {
|
|
|
+ workbench.service.setGlobal({ paused: true, defaultMode: 'review' });
|
|
|
+ for (const conversation of workbench.db.listConversations()) {
|
|
|
+ workbench.service.setConversationMode(conversation.id, 'human');
|
|
|
+ }
|
|
|
}
|
|
|
return {
|
|
|
status: 'ok',
|
|
|
- assistantMessage: 'Enterprise Relay continues collecting messages; AI replies are paused and conversations are in human mode.',
|
|
|
+ assistantMessage: preserveAgentState
|
|
|
+ ? 'Enterprise Relay runtime stopped; Agent modes were preserved.'
|
|
|
+ : 'Enterprise Relay continues collecting messages; AI replies are paused and conversations are in human mode.',
|
|
|
data: { running: false, relayRunning: true, transport: 'server_relay' },
|
|
|
};
|
|
|
}
|
|
|
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');
|
|
|
+ if (!preserveAgentState) {
|
|
|
+ workbench.db.setSetting('listener_enabled', 'false');
|
|
|
+ applyManualTakeover(workbench);
|
|
|
}
|
|
|
- return { status: 'ok', assistantMessage: 'AI 监听已关闭,现有会话已切换为人工接管', data: status };
|
|
|
+ return {
|
|
|
+ status: 'ok',
|
|
|
+ assistantMessage: preserveAgentState
|
|
|
+ ? 'Qiwei polling runtime stopped; Agent modes were preserved.'
|
|
|
+ : 'AI 监听已关闭,现有会话已切换为人工接管',
|
|
|
+ data: status,
|
|
|
+ };
|
|
|
}
|
|
|
|
|
|
function getAgentRuntimeConfig() {
|
|
|
@@ -1766,6 +1863,7 @@ module.exports = {
|
|
|
getAgentStatus,
|
|
|
getAllowlistCandidates,
|
|
|
updateAllowlist,
|
|
|
+ addAllowlistContacts,
|
|
|
getConversations,
|
|
|
getResponseMonitor,
|
|
|
updateCustomerProfile,
|
|
|
@@ -1796,5 +1894,5 @@ module.exports = {
|
|
|
stopListener,
|
|
|
getAgentRuntimeConfig,
|
|
|
createWorkbench,
|
|
|
- __testing: { loadAgentConfig, normalizeAllowlistIds, normalizeAllowlistContact, accountRuntimeKey, accountWorkbenchOverrides, activeAccountMetadata, FmodeQiweiClient, QiweiAgentPoller, outboundContactId, recordOutboundMessage, syncOutboundNotification, ingestMessageForWorkbench, conversationChannelInfo, publicConversation, backfillCustomerIntelligence, backfillCustomerMemory, backfillPropertyRecommendations, backfillSentVoiceAudioPaths, sentVoiceRuns, pendingVoiceDraft, markVoiceDraftSent, detectedPropertiesInMessage, startListenerForWorkbench },
|
|
|
+ __testing: { loadAgentConfig, normalizeAllowlistIds, normalizeAllowlistContact, refreshAllowedSendersFromEnv, resolveConversationSyncScope, applyManualTakeover, accountRuntimeKey, accountWorkbenchOverrides, activeAccountMetadata, FmodeQiweiClient, QiweiAgentPoller, outboundContactId, recordOutboundMessage, syncOutboundNotification, ingestMessageForWorkbench, conversationChannelInfo, publicConversation, backfillCustomerIntelligence, backfillCustomerMemory, backfillPropertyRecommendations, backfillSentVoiceAudioPaths, sentVoiceRuns, pendingVoiceDraft, markVoiceDraftSent, detectedPropertiesInMessage, startListenerForWorkbench },
|
|
|
};
|