|
@@ -47,7 +47,6 @@
|
|
|
currentAccountId: localStorage.getItem(CURRENT_ACCOUNT_KEY) || null,
|
|
currentAccountId: localStorage.getItem(CURRENT_ACCOUNT_KEY) || null,
|
|
|
status: null,
|
|
status: null,
|
|
|
groups: [],
|
|
groups: [],
|
|
|
- groupSync: {},
|
|
|
|
|
groupAutoSyncStarted: false,
|
|
groupAutoSyncStarted: false,
|
|
|
groupFilters: loadFilters(GROUP_FILTERS_KEY, { keyword: '', status: '', showCustomerOnly: false }),
|
|
groupFilters: loadFilters(GROUP_FILTERS_KEY, { keyword: '', status: '', showCustomerOnly: false }),
|
|
|
groupKeywords: null,
|
|
groupKeywords: null,
|
|
@@ -143,10 +142,6 @@
|
|
|
localStorage.setItem(CURRENT_ACCOUNT_KEY, state.currentAccountId || '');
|
|
localStorage.setItem(CURRENT_ACCOUNT_KEY, state.currentAccountId || '');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- function isGroupSynced(roomId) {
|
|
|
|
|
- return !!(state.groupSync[roomId] && state.groupSync[roomId].lastSyncAt);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
function addAccount(account) {
|
|
function addAccount(account) {
|
|
|
const existing = state.accounts.find(a =>
|
|
const existing = state.accounts.find(a =>
|
|
|
(account.uid && a.uid === account.uid) ||
|
|
(account.uid && a.uid === account.uid) ||
|
|
@@ -1286,7 +1281,7 @@
|
|
|
actions: [
|
|
actions: [
|
|
|
{ label: '处理风险', description: '查看原消息证据并建立整改任务', hash: '#group-ops' },
|
|
{ label: '处理风险', description: '查看原消息证据并建立整改任务', hash: '#group-ops' },
|
|
|
{ label: '审核今日计划', description: '批准话术或退回修改', hash: '#group-ops' },
|
|
{ label: '审核今日计划', description: '批准话术或退回修改', hash: '#group-ops' },
|
|
|
- { label: '核对客户群', description: '确认群身份与消息同步状态', hash: '#groups' }
|
|
|
|
|
|
|
+ { label: '核对客户群', description: '确认群身份与回调接入范围', hash: '#groups' }
|
|
|
]
|
|
]
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|
|
@@ -2447,10 +2442,7 @@
|
|
|
const config = monitor.config || {};
|
|
const config = monitor.config || {};
|
|
|
const scope = monitor.scope || { customers: [], groups: [] };
|
|
const scope = monitor.scope || { customers: [], groups: [] };
|
|
|
const reminders = monitor.reminders || [];
|
|
const reminders = monitor.reminders || [];
|
|
|
- const scopeItems = [
|
|
|
|
|
- ...(scope.customers || []).map(item => ({ ...item, typeLabel: '客户' })),
|
|
|
|
|
- ...(scope.groups || []).map(item => ({ ...item, typeLabel: '客户群' })),
|
|
|
|
|
- ];
|
|
|
|
|
|
|
+ const scopeItems = (scope.customers || []).map(item => ({ ...item, typeLabel: '客户' }));
|
|
|
return `
|
|
return `
|
|
|
<section class="response-monitor ${summary.urgent ? 'has-urgent' : ''}">
|
|
<section class="response-monitor ${summary.urgent ? 'has-urgent' : ''}">
|
|
|
<header class="response-monitor-header">
|
|
<header class="response-monitor-header">
|
|
@@ -2461,7 +2453,6 @@
|
|
|
</div>
|
|
</div>
|
|
|
<div class="response-monitor-kpis">
|
|
<div class="response-monitor-kpis">
|
|
|
<span><b>${summary.monitoredCustomers || 0}</b> 个客户</span>
|
|
<span><b>${summary.monitoredCustomers || 0}</b> 个客户</span>
|
|
|
- <span><b>${summary.monitoredGroups || 0}</b> 个客户群</span>
|
|
|
|
|
<span class="${summary.overdue ? 'danger' : ''}"><b>${summary.overdue || 0}</b> 个超时</span>
|
|
<span class="${summary.overdue ? 'danger' : ''}"><b>${summary.overdue || 0}</b> 个超时</span>
|
|
|
</div>
|
|
</div>
|
|
|
</header>
|
|
</header>
|
|
@@ -2469,7 +2460,7 @@
|
|
|
<div class="response-monitor-scope">
|
|
<div class="response-monitor-scope">
|
|
|
<div class="response-monitor-section-title"><strong>当前监听范围</strong><span>${config.warningMinutes || 15} 分钟提醒 · ${config.urgentMinutes || 60} 分钟紧急</span></div>
|
|
<div class="response-monitor-section-title"><strong>当前监听范围</strong><span>${config.warningMinutes || 15} 分钟提醒 · ${config.urgentMinutes || 60} 分钟紧急</span></div>
|
|
|
<div class="response-scope-list">
|
|
<div class="response-scope-list">
|
|
|
- ${scopeItems.length ? scopeItems.map(item => `<span class="response-scope-chip ${escapeHtml(item.status || '')}"><i>${escapeHtml(item.typeLabel)}</i>${escapeHtml(item.name)}<em>${item.status === 'awaiting_sync' ? '待同步' : item.status === 'overdue' ? '待回复' : item.status === 'replied' ? '已回复' : item.status === 'no_reply_needed' ? '无需回复' : '监控中'}</em></span>`).join('') : '<span class="response-monitor-empty">尚未配置监听客户或客户群</span>'}
|
|
|
|
|
|
|
+ ${scopeItems.length ? scopeItems.map(item => `<span class="response-scope-chip ${escapeHtml(item.status || '')}"><i>${escapeHtml(item.typeLabel)}</i>${escapeHtml(item.name)}<em>${item.status === 'awaiting_sync' ? '待同步' : item.status === 'overdue' ? '待回复' : item.status === 'replied' ? '已回复' : item.status === 'no_reply_needed' ? '无需回复' : '监控中'}</em></span>`).join('') : '<span class="response-monitor-empty">尚未配置监听客户</span>'}
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
<div class="response-reminder-list">
|
|
<div class="response-reminder-list">
|
|
@@ -2541,7 +2532,7 @@
|
|
|
warning: '即将超时',
|
|
warning: '即将超时',
|
|
|
replied: '已回复',
|
|
replied: '已回复',
|
|
|
no_reply_needed: '无需回复',
|
|
no_reply_needed: '无需回复',
|
|
|
- awaiting_sync: '等待消息同步',
|
|
|
|
|
|
|
+ awaiting_sync: '等待回调新消息',
|
|
|
monitoring: '监控中',
|
|
monitoring: '监控中',
|
|
|
})[status] || '监控中';
|
|
})[status] || '监控中';
|
|
|
}
|
|
}
|
|
@@ -2566,7 +2557,7 @@
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function groupConversationFlow(group) {
|
|
function groupConversationFlow(group) {
|
|
|
- const synced = group?.status !== 'awaiting_sync';
|
|
|
|
|
|
|
+ const confirmed = Boolean(group?.roomId);
|
|
|
const hasMessage = Boolean(group?.messages?.length || group?.lastMessageAt);
|
|
const hasMessage = Boolean(group?.messages?.length || group?.lastMessageAt);
|
|
|
const pending = Boolean(group?.pendingReply);
|
|
const pending = Boolean(group?.pendingReply);
|
|
|
const failed = Boolean(group?.agentError);
|
|
const failed = Boolean(group?.agentError);
|
|
@@ -2575,8 +2566,8 @@
|
|
|
const auto = group?.mode === 'auto';
|
|
const auto = group?.mode === 'auto';
|
|
|
const sent = ['group_message_sent', 'group_message_auto_sent'].includes(group?.lastOutcome?.action) || group?.status === 'replied';
|
|
const sent = ['group_message_sent', 'group_message_auto_sent'].includes(group?.lastOutcome?.action) || group?.status === 'replied';
|
|
|
return [
|
|
return [
|
|
|
- { label: '群消息接入', detail: hasMessage ? '消息已同步' : '等待同步', state: hasMessage ? 'done' : 'current' },
|
|
|
|
|
- { label: '范围识别', detail: synced ? '已确认客户群' : '等待确认', state: synced ? 'done' : 'waiting' },
|
|
|
|
|
|
|
+ { label: '群消息接入', detail: hasMessage ? '已收到回调新消息' : '等待回调新消息', state: hasMessage ? 'done' : 'current' },
|
|
|
|
|
+ { label: '范围识别', detail: confirmed ? '已确认客户群' : '等待确认', state: confirmed ? 'done' : 'waiting' },
|
|
|
{ label: 'Agent 分析', detail: failed ? '生成失败' : pending || sent || noReply ? '分析完成' : '等待生成', state: failed ? 'error' : pending || sent || noReply ? 'done' : hasMessage ? 'current' : 'waiting' },
|
|
{ label: 'Agent 分析', detail: failed ? '生成失败' : pending || sent || noReply ? '分析完成' : '等待生成', state: failed ? 'error' : pending || sent || noReply ? 'done' : hasMessage ? 'current' : 'waiting' },
|
|
|
{ label: auto ? '自动放行' : '人工审核', detail: auto ? (sendFailed ? '发送失败待处理' : sent ? '已自动放行' : noReply ? '无需放行' : '等待 Agent 分析') : pending ? '草稿待审核' : sent || noReply ? '已完成' : '等待草稿', state: sendFailed ? 'error' : auto ? (sent || noReply ? 'done' : 'waiting') : pending ? 'current' : sent || noReply ? 'done' : 'waiting' },
|
|
{ label: auto ? '自动放行' : '人工审核', detail: auto ? (sendFailed ? '发送失败待处理' : sent ? '已自动放行' : noReply ? '无需放行' : '等待 Agent 分析') : pending ? '草稿待审核' : sent || noReply ? '已完成' : '等待草稿', state: sendFailed ? 'error' : auto ? (sent || noReply ? 'done' : 'waiting') : pending ? 'current' : sent || noReply ? 'done' : 'waiting' },
|
|
|
{ label: auto ? '自动发送' : '群内发送', detail: sent ? '回复已发送' : noReply ? '无需回复' : sendFailed ? '发送失败' : '尚未发送', state: sent || noReply ? 'done' : sendFailed ? 'error' : 'waiting' },
|
|
{ label: auto ? '自动发送' : '群内发送', detail: sent ? '回复已发送' : noReply ? '无需回复' : sendFailed ? '发送失败' : '尚未发送', state: sent || noReply ? 'done' : sendFailed ? 'error' : 'waiting' },
|
|
@@ -3170,16 +3161,12 @@
|
|
|
const agent = status.agent || {};
|
|
const agent = status.agent || {};
|
|
|
const knowledge = status.knowledge || {};
|
|
const knowledge = status.knowledge || {};
|
|
|
const conversations = state.agent.conversations || [];
|
|
const conversations = state.agent.conversations || [];
|
|
|
- const groupAgents = state.agent.groupAgents || [];
|
|
|
|
|
const privateConversations = conversations.filter(item => item.channelType !== 'group');
|
|
const privateConversations = conversations.filter(item => item.channelType !== 'group');
|
|
|
- const groupConversations = agentGroupMonitorItems(conversations, state.agent.responseMonitor || {}, groupAgents);
|
|
|
|
|
- const listMode = state.agent.listMode === 'group' ? 'group' : 'private';
|
|
|
|
|
- const selected = listMode === 'private'
|
|
|
|
|
- ? privateConversations.find(item => item.id === state.agent.selectedId) || privateConversations[0] || null
|
|
|
|
|
- : null;
|
|
|
|
|
- const selectedGroup = listMode === 'group'
|
|
|
|
|
- ? groupConversations.find(item => item.id === state.agent.selectedGroupId) || groupConversations[0] || null
|
|
|
|
|
- : null;
|
|
|
|
|
|
|
+ const groupAgents = [];
|
|
|
|
|
+ const groupConversations = [];
|
|
|
|
|
+ const listMode = 'private';
|
|
|
|
|
+ const selected = privateConversations.find(item => item.id === state.agent.selectedId) || privateConversations[0] || null;
|
|
|
|
|
+ const selectedGroup = null;
|
|
|
if (selected) state.agent.selectedId = selected.id;
|
|
if (selected) state.agent.selectedId = selected.id;
|
|
|
if (selectedGroup) state.agent.selectedGroupId = selectedGroup.id;
|
|
if (selectedGroup) state.agent.selectedGroupId = selectedGroup.id;
|
|
|
const sessionGuide = selected?.claudeSession || null;
|
|
const sessionGuide = selected?.claudeSession || null;
|
|
@@ -3239,7 +3226,7 @@
|
|
|
<button class="agent-mode-btn ${!globalPaused && globalMode === 'auto' ? 'active' : ''}" data-agent-mode="auto">高置信自动</button>
|
|
<button class="agent-mode-btn ${!globalPaused && globalMode === 'auto' ? 'active' : ''}" data-agent-mode="auto">高置信自动</button>
|
|
|
<button class="agent-mode-btn ${globalPaused ? 'active danger' : ''}" data-agent-mode="paused">全局暂停</button>
|
|
<button class="agent-mode-btn ${globalPaused ? 'active danger' : ''}" data-agent-mode="paused">全局暂停</button>
|
|
|
</div>
|
|
</div>
|
|
|
- <div class="agent-guard"><span>发送边界</span>私聊限 ${allowlistCount} 位白名单联系人 · 客户群按各群独立模式处理${groupAgents.some(item => item.mode === 'auto') ? ` · ${groupAgents.filter(item => item.mode === 'auto').length} 个群已开启全自动` : ''}</div>
|
|
|
|
|
|
|
+ <div class="agent-guard"><span>发送边界</span>客服工作台仅处理 ${allowlistCount} 位白名单客户私聊 · 群聊只采集消息并沉淀画像</div>
|
|
|
</section>
|
|
</section>
|
|
|
|
|
|
|
|
${renderResponseMonitor(state.agent.responseMonitor || {})}
|
|
${renderResponseMonitor(state.agent.responseMonitor || {})}
|
|
@@ -3252,10 +3239,9 @@
|
|
|
|
|
|
|
|
<section class="agent-workspace">
|
|
<section class="agent-workspace">
|
|
|
<aside class="agent-conversation-list">
|
|
<aside class="agent-conversation-list">
|
|
|
- <div class="agent-panel-heading"><div><strong>消息入口</strong><small>私聊与群聊智能回复已分开</small></div><span class="agent-count">${privateConversations.length + groupConversations.length}</span></div>
|
|
|
|
|
|
|
+ <div class="agent-panel-heading"><div><strong>消息入口</strong><small>仅展示白名单客户私聊</small></div><span class="agent-count">${privateConversations.length}</span></div>
|
|
|
<div class="agent-list-tabs" role="tablist" aria-label="消息类型">
|
|
<div class="agent-list-tabs" role="tablist" aria-label="消息类型">
|
|
|
- <button class="${listMode === 'private' ? 'active' : ''}" role="tab" aria-selected="${listMode === 'private'}" data-agent-list-mode="private"><span>客户私聊</span><b>${privateConversations.length}</b></button>
|
|
|
|
|
- <button class="${listMode === 'group' ? 'active' : ''}" role="tab" aria-selected="${listMode === 'group'}" data-agent-list-mode="group"><span>群聊智能回复</span><b>${groupConversations.length}</b></button>
|
|
|
|
|
|
|
+ <button class="active" role="tab" aria-selected="true" data-agent-list-mode="private"><span>客户私聊</span><b>${privateConversations.length}</b></button>
|
|
|
</div>
|
|
</div>
|
|
|
<div class="agent-list-scroll">
|
|
<div class="agent-list-scroll">
|
|
|
${listMode === 'private' && privateConversations.length ? privateConversations.map(item => {
|
|
${listMode === 'private' && privateConversations.length ? privateConversations.map(item => {
|
|
@@ -3267,7 +3253,7 @@
|
|
|
</button>`;
|
|
</button>`;
|
|
|
}).join('') : listMode === 'group' && groupConversations.length ? groupConversations.map(item => `<button class="agent-conversation-item group ${item.id === selectedGroup?.id ? 'active' : ''}" data-group-monitor-id="${escapeHtml(item.id)}">
|
|
}).join('') : listMode === 'group' && groupConversations.length ? groupConversations.map(item => `<button class="agent-conversation-item group ${item.id === selectedGroup?.id ? 'active' : ''}" data-group-monitor-id="${escapeHtml(item.id)}">
|
|
|
<span class="agent-avatar group">群</span>
|
|
<span class="agent-avatar group">群</span>
|
|
|
- <span class="agent-conversation-copy"><span class="agent-conversation-name"><strong>${escapeHtml(item.name || '客户群')}</strong><i class="agent-channel-badge group">群聊</i></span><small>${escapeHtml(item.lastMessagePreview || '等待群消息同步')}</small><em class="${escapeHtml(item.status || '')}">${item.mode === 'auto' ? '全自动' : item.pendingReply ? '草稿待审核' : item.agentError ? 'Agent 生成失败' : escapeHtml(agentMonitorStatusLabel(item.status))}${item.waitingMinutes ? ` · ${formatReplyWaiting(item.waitingMinutes)}` : ''}</em></span>
|
|
|
|
|
|
|
+ <span class="agent-conversation-copy"><span class="agent-conversation-name"><strong>${escapeHtml(item.name || '客户群')}</strong><i class="agent-channel-badge group">群聊</i></span><small>${escapeHtml(item.lastMessagePreview || '等待群回调新消息')}</small><em class="${escapeHtml(item.status || '')}">${item.mode === 'auto' ? '全自动' : item.pendingReply ? '草稿待审核' : item.agentError ? 'Agent 生成失败' : escapeHtml(agentMonitorStatusLabel(item.status))}${item.waitingMinutes ? ` · ${formatReplyWaiting(item.waitingMinutes)}` : ''}</em></span>
|
|
|
<time>${formatAgentTime(item.lastMessageAt)}</time>
|
|
<time>${formatAgentTime(item.lastMessageAt)}</time>
|
|
|
</button>`).join('') : `<div class="agent-empty"><strong>${listMode === 'group' ? '还没有已确认客户群' : allowlistReady ? '等待第一条真实消息' : '还没有选择白名单'}</strong><span>${listMode === 'group' ? '先在客户群管理中确认需要智能回复的客户群。' : allowlistReady ? (product.mode === 'enterprise' ? '公网回调会持续接收消息,请从已选企微客户发送测试内容。' : '启动监听后,请从已选企微客户发送测试内容。') : '点击上方“管理白名单”,直接从企微联系人中选择。'}</span></div>`}
|
|
</button>`).join('') : `<div class="agent-empty"><strong>${listMode === 'group' ? '还没有已确认客户群' : allowlistReady ? '等待第一条真实消息' : '还没有选择白名单'}</strong><span>${listMode === 'group' ? '先在客户群管理中确认需要智能回复的客户群。' : allowlistReady ? (product.mode === 'enterprise' ? '公网回调会持续接收消息,请从已选企微客户发送测试内容。' : '启动监听后,请从已选企微客户发送测试内容。') : '点击上方“管理白名单”,直接从企微联系人中选择。'}</span></div>`}
|
|
|
</div>
|
|
</div>
|
|
@@ -3408,16 +3394,20 @@
|
|
|
if (state.agent.loading) return;
|
|
if (state.agent.loading) return;
|
|
|
state.agent.loading = true;
|
|
state.agent.loading = true;
|
|
|
try {
|
|
try {
|
|
|
- const [statusRes, conversationsRes, responseMonitorRes, groupAgentsRes] = await Promise.all([
|
|
|
|
|
|
|
+ const [statusRes, conversationsRes, responseMonitorRes] = await Promise.all([
|
|
|
api('GET', '/api/agent/status'),
|
|
api('GET', '/api/agent/status'),
|
|
|
api('GET', '/api/agent/conversations'),
|
|
api('GET', '/api/agent/conversations'),
|
|
|
api('GET', '/api/agent/response-monitor'),
|
|
api('GET', '/api/agent/response-monitor'),
|
|
|
- api('GET', '/api/agent/groups'),
|
|
|
|
|
]);
|
|
]);
|
|
|
const nextStatus = statusRes.data || {};
|
|
const nextStatus = statusRes.data || {};
|
|
|
const nextConversations = conversationsRes.data?.conversations || [];
|
|
const nextConversations = conversationsRes.data?.conversations || [];
|
|
|
- const nextResponseMonitor = responseMonitorRes.data || {};
|
|
|
|
|
- const nextGroupAgents = groupAgentsRes.data?.groups || [];
|
|
|
|
|
|
|
+ const rawResponseMonitor = responseMonitorRes.data || {};
|
|
|
|
|
+ const nextResponseMonitor = {
|
|
|
|
|
+ ...rawResponseMonitor,
|
|
|
|
|
+ scope: { ...(rawResponseMonitor.scope || {}), groups: [] },
|
|
|
|
|
+ reminders: (rawResponseMonitor.reminders || []).filter(item => item.kind !== 'group'),
|
|
|
|
|
+ };
|
|
|
|
|
+ const nextGroupAgents = [];
|
|
|
const localAccount = currentAccount();
|
|
const localAccount = currentAccount();
|
|
|
const activeAccount = nextStatus.account || {};
|
|
const activeAccount = nextStatus.account || {};
|
|
|
if (localAccount && localAccount.uid && activeAccount.uid === localAccount.uid) {
|
|
if (localAccount && localAccount.uid && activeAccount.uid === localAccount.uid) {
|
|
@@ -4683,7 +4673,7 @@
|
|
|
<div>
|
|
<div>
|
|
|
<span>WECOM CUSTOMER GROUP OPERATIONS</span>
|
|
<span>WECOM CUSTOMER GROUP OPERATIONS</span>
|
|
|
<h2>客户群管理</h2>
|
|
<h2>客户群管理</h2>
|
|
|
- <p>识别真实客户群、同步历史消息,并集中完成人工确认与群运营准备。</p>
|
|
|
|
|
|
|
+ <p>识别并确认真实客户群,确认后自动接收公网回调中的群新消息。</p>
|
|
|
</div>
|
|
</div>
|
|
|
${renderGroupStatsCard(null)}
|
|
${renderGroupStatsCard(null)}
|
|
|
</section>
|
|
</section>
|
|
@@ -4692,7 +4682,7 @@
|
|
|
<main class="group-list-panel">
|
|
<main class="group-list-panel">
|
|
|
<div class="operation-note">
|
|
<div class="operation-note">
|
|
|
<svg viewBox="0 0 24 24"><path d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20Zm1 15h-2v-6h2v6Zm0-8h-2V7h2v2Z"/></svg>
|
|
<svg viewBox="0 0 24 24"><path d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20Zm1 15h-2v-6h2v6Zm0-8h-2V7h2v2Z"/></svg>
|
|
|
- <div><strong>先识别,再同步</strong><span>勾选目标群后可批量确认并保存历史消息;未确认的普通群不会进入客户运营流程。</span></div>
|
|
|
|
|
|
|
+ <div><strong>先识别,再确认</strong><span>确认后的客户群会自动接收回调新消息;未确认的普通群不会进入客户运营流程。</span></div>
|
|
|
</div>
|
|
</div>
|
|
|
${renderGroupFilters()}
|
|
${renderGroupFilters()}
|
|
|
<div id="groups-table-card"></div>
|
|
<div id="groups-table-card"></div>
|
|
@@ -4728,7 +4718,6 @@
|
|
|
<option value="all">全部群</option>
|
|
<option value="all">全部群</option>
|
|
|
<option value="self">我创建的群</option>
|
|
<option value="self">我创建的群</option>
|
|
|
<option value="session">最近聊天里的群</option>
|
|
<option value="session">最近聊天里的群</option>
|
|
|
- <option value="messages">从消息记录里找群</option>
|
|
|
|
|
</select>
|
|
</select>
|
|
|
</div>
|
|
</div>
|
|
|
<div class="form-group">
|
|
<div class="form-group">
|
|
@@ -4777,7 +4766,7 @@
|
|
|
return `
|
|
return `
|
|
|
<div class="card group-filter-card">
|
|
<div class="card group-filter-card">
|
|
|
<div class="card-header">
|
|
<div class="card-header">
|
|
|
- <div><h3 class="card-title">群列表</h3><div class="card-subtitle">识别结果与消息同步状态</div></div>
|
|
|
|
|
|
|
+ <div><h3 class="card-title">群列表</h3><div class="card-subtitle">识别结果与回调接入范围</div></div>
|
|
|
<div class="filter-bar">
|
|
<div class="filter-bar">
|
|
|
<input id="filter-keyword" type="text" placeholder="搜索群名" value="${escapeHtml(state.groupFilters.keyword)}" />
|
|
<input id="filter-keyword" type="text" placeholder="搜索群名" value="${escapeHtml(state.groupFilters.keyword)}" />
|
|
|
<select id="filter-status">
|
|
<select id="filter-status">
|
|
@@ -4834,9 +4823,6 @@
|
|
|
<button class="btn btn-sm" id="btn-batch-confirm" ${state.selectedGroupIds.size ? '' : 'disabled'}>
|
|
<button class="btn btn-sm" id="btn-batch-confirm" ${state.selectedGroupIds.size ? '' : 'disabled'}>
|
|
|
确认选中为客户群
|
|
确认选中为客户群
|
|
|
</button>
|
|
</button>
|
|
|
- <button class="btn btn-sm" id="btn-batch-sync-msg" ${state.selectedGroupIds.size ? '' : 'disabled'}>
|
|
|
|
|
- 同步选中群消息
|
|
|
|
|
- </button>
|
|
|
|
|
</div>
|
|
</div>
|
|
|
<div class="table-wrap">
|
|
<div class="table-wrap">
|
|
|
<table>
|
|
<table>
|
|
@@ -4847,14 +4833,12 @@
|
|
|
<th style="width:70px">人数</th>
|
|
<th style="width:70px">人数</th>
|
|
|
<th style="width:100px">识别结果</th>
|
|
<th style="width:100px">识别结果</th>
|
|
|
<th style="width:100px">置信度</th>
|
|
<th style="width:100px">置信度</th>
|
|
|
- <th style="width:100px">消息同步</th>
|
|
|
|
|
<th>命中关键词</th>
|
|
<th>命中关键词</th>
|
|
|
<th style="width:120px">操作</th>
|
|
<th style="width:120px">操作</th>
|
|
|
</tr>
|
|
</tr>
|
|
|
</thead>
|
|
</thead>
|
|
|
<tbody>
|
|
<tbody>
|
|
|
${filtered.map((g, i) => {
|
|
${filtered.map((g, i) => {
|
|
|
- const synced = isGroupSynced(g.roomId);
|
|
|
|
|
return `
|
|
return `
|
|
|
<tr style="animation:fadeInUp 0.3s ease ${i * 0.03}s both">
|
|
<tr style="animation:fadeInUp 0.3s ease ${i * 0.03}s both">
|
|
|
<td>
|
|
<td>
|
|
@@ -4872,14 +4856,12 @@
|
|
|
<span style="font-size:12px;color:var(--text-muted)">${Math.round((g.confidence || 0) * 100)}%</span>
|
|
<span style="font-size:12px;color:var(--text-muted)">${Math.round((g.confidence || 0) * 100)}%</span>
|
|
|
</div>
|
|
</div>
|
|
|
</td>
|
|
</td>
|
|
|
- <td>${synced ? `<span class="badge badge-success">已同步</span>` : `<span class="badge badge-default">未同步</span>`}</td>
|
|
|
|
|
<td>${(g.matchedKeywords || []).map(k => `<span class="tag" style="margin-bottom:4px">${escapeHtml(k)}</span>`).join(' ') || '-'}</td>
|
|
<td>${(g.matchedKeywords || []).map(k => `<span class="tag" style="margin-bottom:4px">${escapeHtml(k)}</span>`).join(' ') || '-'}</td>
|
|
|
<td>
|
|
<td>
|
|
|
<div style="display:flex;gap:6px;flex-wrap:wrap">
|
|
<div style="display:flex;gap:6px;flex-wrap:wrap">
|
|
|
${!['CONFIRMED', 'AUTO_CONFIRMED'].includes(g.reviewStatus) ? `<button class="btn btn-sm" data-action="confirm" data-room="${escapeHtml(g.roomId)}" data-name="${escapeHtml(g.roomName || '')}">确认</button>` : ''}
|
|
${!['CONFIRMED', 'AUTO_CONFIRMED'].includes(g.reviewStatus) ? `<button class="btn btn-sm" data-action="confirm" data-room="${escapeHtml(g.roomId)}" data-name="${escapeHtml(g.roomName || '')}">确认</button>` : ''}
|
|
|
${g.reviewStatus === 'CONFIRMED' ? `<button class="btn btn-sm" data-action="detail" data-room="${escapeHtml(g.roomId)}" data-name="${escapeHtml(g.roomName || '')}">群详情</button>` : ''}
|
|
${g.reviewStatus === 'CONFIRMED' ? `<button class="btn btn-sm" data-action="detail" data-room="${escapeHtml(g.roomId)}" data-name="${escapeHtml(g.roomName || '')}">群详情</button>` : ''}
|
|
|
${g.reviewStatus !== 'REJECTED' ? `<button class="btn btn-sm btn-secondary" data-action="reject" data-room="${escapeHtml(g.roomId)}" data-name="${escapeHtml(g.roomName || '')}">忽略</button>` : ''}
|
|
${g.reviewStatus !== 'REJECTED' ? `<button class="btn btn-sm btn-secondary" data-action="reject" data-room="${escapeHtml(g.roomId)}" data-name="${escapeHtml(g.roomName || '')}">忽略</button>` : ''}
|
|
|
- <button class="btn btn-sm btn-secondary" data-action="sync-msg" data-room="${escapeHtml(g.roomId)}">${synced ? '重新同步' : '同步消息'}</button>
|
|
|
|
|
</div>
|
|
</div>
|
|
|
</td>
|
|
</td>
|
|
|
</tr>`;
|
|
</tr>`;
|
|
@@ -4923,22 +4905,12 @@
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- const batchSyncBtn = card.querySelector('#btn-batch-sync-msg');
|
|
|
|
|
- if (batchSyncBtn) {
|
|
|
|
|
- batchSyncBtn.addEventListener('click', () => {
|
|
|
|
|
- const roomIds = Array.from(state.selectedGroupIds);
|
|
|
|
|
- if (!roomIds.length) return;
|
|
|
|
|
- syncGroupMessages(roomIds, batchSyncBtn);
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
card.querySelectorAll('[data-action]').forEach(btn => {
|
|
card.querySelectorAll('[data-action]').forEach(btn => {
|
|
|
btn.addEventListener('click', () => {
|
|
btn.addEventListener('click', () => {
|
|
|
const roomId = btn.dataset.room;
|
|
const roomId = btn.dataset.room;
|
|
|
const roomName = btn.dataset.name;
|
|
const roomName = btn.dataset.name;
|
|
|
if (btn.dataset.action === 'confirm') confirmGroup(roomId, roomName);
|
|
if (btn.dataset.action === 'confirm') confirmGroup(roomId, roomName);
|
|
|
if (btn.dataset.action === 'reject') openRejectModal(roomId, roomName);
|
|
if (btn.dataset.action === 'reject') openRejectModal(roomId, roomName);
|
|
|
- if (btn.dataset.action === 'sync-msg') syncGroupMessages([roomId], btn);
|
|
|
|
|
if (btn.dataset.action === 'detail') openGroupDetailModal(roomId, roomName);
|
|
if (btn.dataset.action === 'detail') openGroupDetailModal(roomId, roomName);
|
|
|
});
|
|
});
|
|
|
});
|
|
});
|
|
@@ -4984,12 +4956,8 @@
|
|
|
|
|
|
|
|
async function loadGroups() {
|
|
async function loadGroups() {
|
|
|
try {
|
|
try {
|
|
|
- const [groupsRes, syncRes] = await Promise.all([
|
|
|
|
|
- api('GET', '/api/groups/list'),
|
|
|
|
|
- api('GET', '/api/dashboard/groups')
|
|
|
|
|
- ]);
|
|
|
|
|
|
|
+ const groupsRes = await api('GET', '/api/groups/list');
|
|
|
state.groups = groupsRes.data?.groups || [];
|
|
state.groups = groupsRes.data?.groups || [];
|
|
|
- state.groupSync = syncRes.data || {};
|
|
|
|
|
updateGroupStats(groupsRes.summary);
|
|
updateGroupStats(groupsRes.summary);
|
|
|
renderGroupTable(state.groups);
|
|
renderGroupTable(state.groups);
|
|
|
} catch (err) {
|
|
} catch (err) {
|
|
@@ -5031,7 +4999,6 @@
|
|
|
customerKeywords: ['客户群', '服务群', '售后群', '咨询群', 'VIP群', '专属群', '业主群', '项目群', '订单群', '用户群'],
|
|
customerKeywords: ['客户群', '服务群', '售后群', '咨询群', 'VIP群', '专属群', '业主群', '项目群', '订单群', '用户群'],
|
|
|
highConfidenceTerms: ['客户群', '服务群'],
|
|
highConfidenceTerms: ['客户群', '服务群'],
|
|
|
autoClassify: true,
|
|
autoClassify: true,
|
|
|
- scanFromMessages: false,
|
|
|
|
|
}, result => {
|
|
}, result => {
|
|
|
if (tracker) tracker.innerHTML = '';
|
|
if (tracker) tracker.innerHTML = '';
|
|
|
updateGroupStats(result.summary);
|
|
updateGroupStats(result.summary);
|
|
@@ -5062,8 +5029,7 @@
|
|
|
maxPages: Number(document.getElementById('sync-max-pages').value) || 20,
|
|
maxPages: Number(document.getElementById('sync-max-pages').value) || 20,
|
|
|
customerKeywords: keywords,
|
|
customerKeywords: keywords,
|
|
|
highConfidenceTerms: highConfidence,
|
|
highConfidenceTerms: highConfidence,
|
|
|
- autoClassify: document.getElementById('sync-auto-classify').checked,
|
|
|
|
|
- scanFromMessages: document.getElementById('sync-scope').value === 'messages'
|
|
|
|
|
|
|
+ autoClassify: document.getElementById('sync-auto-classify').checked
|
|
|
};
|
|
};
|
|
|
const tracker = document.getElementById('sync-job-tracker');
|
|
const tracker = document.getElementById('sync-job-tracker');
|
|
|
const id = Date.now().toString(36);
|
|
const id = Date.now().toString(36);
|
|
@@ -5192,29 +5158,6 @@
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- async function syncGroupMessages(roomIds, triggerButton = null) {
|
|
|
|
|
- const guid = getGuid();
|
|
|
|
|
- if (!guid) {
|
|
|
|
|
- toast('请先登录企业微信账号', 'warning');
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- const acc = currentAccount();
|
|
|
|
|
- if (!acc || !acc.online) {
|
|
|
|
|
- toast('账号当前离线,请先恢复登录后再同步消息', 'warning');
|
|
|
|
|
- location.hash = '#status';
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- const body = { guid, roomIds, maxPages: 20, maxMessagesPerRoom: 500 };
|
|
|
|
|
- const name = 'sync-messages-' + roomIds[0];
|
|
|
|
|
- if (triggerButton) {
|
|
|
|
|
- triggerButton.dataset.job = name;
|
|
|
|
|
- }
|
|
|
|
|
- startJob(name, '/api/groups/messages/sync', body, result => {
|
|
|
|
|
- toast(`共同步 ${result.summary?.totalNew || 0} 条消息`);
|
|
|
|
|
- loadGroups();
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
// ==================== Helpers ====================
|
|
// ==================== Helpers ====================
|
|
|
function requireOnlineAccount() {
|
|
function requireOnlineAccount() {
|
|
|
const acc = currentAccount();
|
|
const acc = currentAccount();
|
|
@@ -6116,7 +6059,8 @@
|
|
|
</div>
|
|
</div>
|
|
|
<div class="operation-toolbar">
|
|
<div class="operation-toolbar">
|
|
|
<div class="toolbar-left">
|
|
<div class="toolbar-left">
|
|
|
- <button class="btn" id="btn-pt-batch" data-job="batch-portraits">批量生成画像</button>
|
|
|
|
|
|
|
+ <button class="btn" id="btn-pt-incremental" data-job="incremental-portraits">批量增量更新</button>
|
|
|
|
|
+ <button class="btn btn-secondary" id="btn-pt-full" data-job="full-portraits">批量全量更新</button>
|
|
|
<button class="btn btn-secondary" id="btn-pt-tag">为客户打标签</button>
|
|
<button class="btn btn-secondary" id="btn-pt-tag">为客户打标签</button>
|
|
|
<button class="btn btn-secondary" id="btn-pt-export">导出全部画像</button>
|
|
<button class="btn btn-secondary" id="btn-pt-export">导出全部画像</button>
|
|
|
</div>
|
|
</div>
|
|
@@ -6172,7 +6116,8 @@
|
|
|
page.querySelector('#pt-filter-source').addEventListener('change', () => loadPortraitsTable(page));
|
|
page.querySelector('#pt-filter-source').addEventListener('change', () => loadPortraitsTable(page));
|
|
|
page.querySelector('#pt-filter-tag').addEventListener('change', () => loadPortraitsTable(page));
|
|
page.querySelector('#pt-filter-tag').addEventListener('change', () => loadPortraitsTable(page));
|
|
|
|
|
|
|
|
- page.querySelector('#btn-pt-batch').addEventListener('click', () => openBatchPortraitSelector(page));
|
|
|
|
|
|
|
+ page.querySelector('#btn-pt-incremental').addEventListener('click', () => openBatchPortraitSelector(page, 'incremental'));
|
|
|
|
|
+ page.querySelector('#btn-pt-full').addEventListener('click', () => openBatchPortraitSelector(page, 'full'));
|
|
|
page.querySelector('#btn-pt-tag').addEventListener('click', () => openTagModal(page));
|
|
page.querySelector('#btn-pt-tag').addEventListener('click', () => openTagModal(page));
|
|
|
page.querySelector('#btn-pt-export').addEventListener('click', () => {
|
|
page.querySelector('#btn-pt-export').addEventListener('click', () => {
|
|
|
const portraits = state.portraits?.allPortraits || {};
|
|
const portraits = state.portraits?.allPortraits || {};
|
|
@@ -6568,11 +6513,11 @@
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- async function openBatchPortraitSelector(page) {
|
|
|
|
|
|
|
+ async function openBatchPortraitSelector(page, updateMode = 'incremental') {
|
|
|
const result = await openCustomerSelector({
|
|
const result = await openCustomerSelector({
|
|
|
multiple: true,
|
|
multiple: true,
|
|
|
- title: '选择要生成画像的客户',
|
|
|
|
|
- confirmText: '生成画像'
|
|
|
|
|
|
|
+ title: updateMode === 'full' ? '选择要全量更新画像的客户' : '选择要增量更新画像的客户',
|
|
|
|
|
+ confirmText: updateMode === 'full' ? '全量更新' : '增量更新'
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
const ids = result.externalUserIds || [];
|
|
const ids = result.externalUserIds || [];
|
|
@@ -6581,7 +6526,8 @@
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- startJob('batch-portraits', '/api/portraits/batch', { externalUserIds: ids, aiMode: 'ai', guid: getGuid() }, () => loadPortraitsDashboard(page), 'pt-job-tracker');
|
|
|
|
|
|
|
+ const jobName = updateMode === 'full' ? 'full-portraits' : 'incremental-portraits';
|
|
|
|
|
+ startJob(jobName, '/api/portraits/batch', { externalUserIds: ids, aiMode: 'ai', updateMode, guid: getGuid() }, () => loadPortraitsDashboard(page), 'pt-job-tracker');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function openTagModal(page) {
|
|
function openTagModal(page) {
|