|
|
@@ -146,7 +146,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(
|
|
|
@@ -1023,6 +1023,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);
|
|
|
@@ -1030,32 +1032,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() {
|
|
|
@@ -1249,9 +1273,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('测试账号当前不在线,无法同步企微会话');
|
|
|
|
|
|
@@ -1271,7 +1316,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;
|
|
|
|
|
|
@@ -1332,15 +1377,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,
|
|
|
@@ -1357,6 +1408,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',
|
|
|
@@ -1642,8 +1694,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',
|
|
|
@@ -1660,7 +1718,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());
|
|
|
@@ -1673,11 +1731,11 @@ async function startListener() {
|
|
|
};
|
|
|
}
|
|
|
const account = await detectAccountStatus(true);
|
|
|
- return startListenerForWorkbench(workbench, account);
|
|
|
+ return startListenerForWorkbench(workbench, account, options);
|
|
|
}
|
|
|
|
|
|
function applyManualTakeover(target) {
|
|
|
- target.service.setGlobal({ paused: false, defaultMode: 'review' });
|
|
|
+ target.service.setGlobal({ paused: true, defaultMode: 'review' });
|
|
|
for (const conversation of target.db.listConversations()) {
|
|
|
target.service.setConversationMode(conversation.id, 'human');
|
|
|
}
|
|
|
@@ -1701,7 +1759,10 @@ function stopListener({ preserveAgentState = false } = {}) {
|
|
|
};
|
|
|
}
|
|
|
const status = workbench.poller.stop();
|
|
|
- if (!preserveAgentState) applyManualTakeover(workbench);
|
|
|
+ if (!preserveAgentState) {
|
|
|
+ workbench.db.setSetting('listener_enabled', 'false');
|
|
|
+ applyManualTakeover(workbench);
|
|
|
+ }
|
|
|
return {
|
|
|
status: 'ok',
|
|
|
assistantMessage: preserveAgentState
|
|
|
@@ -1720,6 +1781,7 @@ module.exports = {
|
|
|
getAgentStatus,
|
|
|
getAllowlistCandidates,
|
|
|
updateAllowlist,
|
|
|
+ addAllowlistContacts,
|
|
|
getConversations,
|
|
|
getResponseMonitor,
|
|
|
getGroupAgents,
|
|
|
@@ -1754,5 +1816,5 @@ module.exports = {
|
|
|
stopListener,
|
|
|
getAgentRuntimeConfig,
|
|
|
createWorkbench,
|
|
|
- __testing: { loadAgentConfig, normalizeAllowlistIds, normalizeAllowlistContact, refreshAllowedSendersFromEnv, applyManualTakeover, accountRuntimeKey, accountWorkbenchOverrides, activeAccountMetadata, FmodeQiweiClient, QiweiAgentPoller, ingestMessageForWorkbench, conversationChannelInfo, publicConversation, backfillCustomerIntelligence, backfillPropertyRecommendations, backfillSentVoiceAudioPaths, sentVoiceRuns, pendingVoiceDraft, markVoiceDraftSent, detectedPropertiesInMessage, groupAgentService, startListenerForWorkbench },
|
|
|
+ __testing: { loadAgentConfig, normalizeAllowlistIds, normalizeAllowlistContact, refreshAllowedSendersFromEnv, resolveConversationSyncScope, applyManualTakeover, accountRuntimeKey, accountWorkbenchOverrides, activeAccountMetadata, FmodeQiweiClient, QiweiAgentPoller, ingestMessageForWorkbench, conversationChannelInfo, publicConversation, backfillCustomerIntelligence, backfillPropertyRecommendations, backfillSentVoiceAudioPaths, sentVoiceRuns, pendingVoiceDraft, markVoiceDraftSent, detectedPropertiesInMessage, groupAgentService, startListenerForWorkbench },
|
|
|
};
|