|
|
@@ -42,7 +42,7 @@
|
|
|
selectedGroupIds: new Set(),
|
|
|
customerOps: { customers: [], defaultGreeting: '您好 {{name}},我是您的专属顾问,方便加您企业微信沟通。', rateLimit: 12, maxAttempts: 2, lastResult: null, master: { hub: null, selectedId: null, keyword: '', loading: false }, ...loadFilters(CO_FILTERS_KEY, { tablePage: 1, keyword: '', status: '', portrait: '', tag: '' }) },
|
|
|
portraits: { lastPortrait: null, lastTags: [], personalLabels: [], lastTransferPreview: null, ...loadFilters(PT_FILTERS_KEY, { tablePage: 1, keyword: '', source: '', tag: '' }) },
|
|
|
- agent: { status: null, conversations: [], responseMonitor: null, selectedId: null, timer: null, loading: false, signature: null },
|
|
|
+ agent: { status: null, conversations: [], responseMonitor: null, groupAgents: [], selectedId: null, selectedGroupId: null, listMode: 'private', timer: null, loading: false, signature: null, replyEdits: {}, groupReplyEdits: {} },
|
|
|
skills: { registry: null, selectedPackageId: null, selectedSkillId: null, keyword: '' },
|
|
|
knowledge: { tree: null, selectedNodeId: null, file: null, properties: null, selectedProperty: null, expandedFolderIds: new Set(), foldersInitialized: false, filters: { q: '', district: '', layout: '', decoration: '', maxPrice: '' }, meetings: { hub: null, selectedId: null, loading: false }, documents: { hub: null, selectedId: null, loading: false }, todos: { hub: null, selectedId: null, loading: false, userResults: [] }, tasks: { hub: null, loading: false, filters: { q: '', source: '', status: 'active' } } },
|
|
|
transfers: { lastTransferPreview: null, ...loadFilters(TF_FILTERS_KEY, { tablePage: 1, status: '', user: '' }) }
|
|
|
@@ -145,6 +145,7 @@
|
|
|
state.agent.conversations = [];
|
|
|
state.agent.selectedId = null;
|
|
|
state.agent.signature = null;
|
|
|
+ state.agent.replyEdits = {};
|
|
|
saveAccounts();
|
|
|
updateAccountSwitcher();
|
|
|
renderPage();
|
|
|
@@ -1913,7 +1914,7 @@
|
|
|
}
|
|
|
|
|
|
function renderAgentMessage(message) {
|
|
|
- const roleLabel = message.role === 'customer' ? '客户' : message.role === 'human' ? '人工客服' : 'Agent 建议';
|
|
|
+ const roleLabel = message.senderName || (message.role === 'customer' ? '客户' : message.role === 'human' ? '人工客服' : 'Agent 建议');
|
|
|
const statusLabel = message.status === 'pending' ? '待确认' : message.status === 'sending' ? '发送中' : message.status === 'sent' ? '已发送' : message.status === 'submitted' ? '已提交企微' : '';
|
|
|
return `<div class="agent-message ${escapeHtml(message.role)}">
|
|
|
<div class="agent-message-meta"><span>${roleLabel}</span><span>${formatAgentTime(message.timestamp)}${statusLabel ? ` · ${statusLabel}` : ''}</span></div>
|
|
|
@@ -1976,6 +1977,110 @@
|
|
|
</section>`;
|
|
|
}
|
|
|
|
|
|
+ function agentGroupMonitorItems(conversations = [], monitor = {}, groupAgents = []) {
|
|
|
+ const groups = new Map();
|
|
|
+ for (const item of monitor.scope?.groups || []) {
|
|
|
+ groups.set(String(item.id), { ...item, id: String(item.id), channelType: 'group', channelLabel: '客户群聊' });
|
|
|
+ }
|
|
|
+ for (const conversation of conversations.filter(item => item.channelType === 'group')) {
|
|
|
+ const id = String(conversation.channelId || conversation.id);
|
|
|
+ const latest = conversation.messages?.[conversation.messages.length - 1] || null;
|
|
|
+ const current = groups.get(id) || {};
|
|
|
+ groups.set(id, {
|
|
|
+ ...current,
|
|
|
+ id,
|
|
|
+ name: current.name || conversation.displayName,
|
|
|
+ conversationId: conversation.id,
|
|
|
+ messages: conversation.messages || [],
|
|
|
+ lastMessageAt: current.lastMessageAt || conversation.lastMessageAt,
|
|
|
+ lastMessagePreview: current.lastMessagePreview || latest?.content || '',
|
|
|
+ status: current.status || 'monitoring',
|
|
|
+ channelType: 'group',
|
|
|
+ channelLabel: '客户群聊',
|
|
|
+ });
|
|
|
+ }
|
|
|
+ for (const groupAgent of groupAgents || []) {
|
|
|
+ const id = String(groupAgent.roomId || '');
|
|
|
+ if (!id) continue;
|
|
|
+ const latest = groupAgent.messages?.[groupAgent.messages.length - 1] || null;
|
|
|
+ const current = groups.get(id) || {};
|
|
|
+ groups.set(id, {
|
|
|
+ ...current,
|
|
|
+ id,
|
|
|
+ name: current.name || groupAgent.roomName || id,
|
|
|
+ messages: groupAgent.messages || [],
|
|
|
+ lastMessageAt: current.lastMessageAt || latest?.timestamp || null,
|
|
|
+ lastMessagePreview: current.lastMessagePreview || latest?.content || '',
|
|
|
+ lastSenderName: current.lastSenderName || latest?.senderName || '',
|
|
|
+ pendingReply: groupAgent.pendingReply || null,
|
|
|
+ mode: groupAgent.mode === 'auto' ? 'auto' : 'review',
|
|
|
+ agentError: groupAgent.agentError || null,
|
|
|
+ sendError: groupAgent.sendError || null,
|
|
|
+ agentNotice: groupAgent.agentNotice || null,
|
|
|
+ lastOutcome: groupAgent.lastOutcome || null,
|
|
|
+ audit: groupAgent.audit || [],
|
|
|
+ channelType: 'group',
|
|
|
+ channelLabel: '客户群聊',
|
|
|
+ });
|
|
|
+ }
|
|
|
+ return [...groups.values()].sort((a, b) => Date.parse(b.lastMessageAt || 0) - Date.parse(a.lastMessageAt || 0));
|
|
|
+ }
|
|
|
+
|
|
|
+ function agentMonitorStatusLabel(status) {
|
|
|
+ return ({
|
|
|
+ overdue: '待客服回复',
|
|
|
+ warning: '即将超时',
|
|
|
+ replied: '已回复',
|
|
|
+ no_reply_needed: '无需回复',
|
|
|
+ awaiting_sync: '等待消息同步',
|
|
|
+ monitoring: '监控中',
|
|
|
+ })[status] || '监控中';
|
|
|
+ }
|
|
|
+
|
|
|
+ function privateConversationFlow(conversation) {
|
|
|
+ const messages = conversation?.messages || [];
|
|
|
+ const inboundIndex = messages.findLastIndex(message => message.role === 'customer');
|
|
|
+ const outboundAfterInbound = inboundIndex >= 0 && messages.slice(inboundIndex + 1).some(message => ['agent', 'human'].includes(message.role));
|
|
|
+ const hasInbound = inboundIndex >= 0;
|
|
|
+ const hasDraft = Boolean(conversation?.pendingReply);
|
|
|
+ const failed = Boolean(conversation?.agentError);
|
|
|
+ const noReply = Boolean(conversation?.agentNotice);
|
|
|
+ const human = conversation?.mode === 'human';
|
|
|
+ const paused = conversation?.mode === 'paused';
|
|
|
+ return [
|
|
|
+ { label: '消息接入', detail: hasInbound ? '已收到企微消息' : '等待客户消息', state: hasInbound ? 'done' : 'current' },
|
|
|
+ { label: '范围校验', detail: hasInbound ? '白名单私聊' : '等待校验', state: hasInbound ? 'done' : 'waiting' },
|
|
|
+ { label: 'Agent 分析', detail: failed ? '处理失败' : noReply ? '判断无需回复' : hasDraft || outboundAfterInbound ? '分析完成' : human ? '人工路径' : paused ? '已暂停' : '等待处理', state: failed ? 'error' : noReply || hasDraft || outboundAfterInbound ? 'done' : hasInbound && !human && !paused ? 'current' : 'waiting' },
|
|
|
+ { label: '审核 / 接管', detail: hasDraft ? '草稿待审核' : human ? '人工处理中' : outboundAfterInbound || noReply ? '已完成' : '等待上一步', state: hasDraft || human ? 'current' : outboundAfterInbound || noReply ? 'done' : 'waiting' },
|
|
|
+ { label: '处理完成', detail: outboundAfterInbound ? '回复已发送' : noReply ? '无需外发' : '尚未完成', state: outboundAfterInbound || noReply ? 'done' : 'waiting' },
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ function groupConversationFlow(group) {
|
|
|
+ const synced = group?.status !== 'awaiting_sync';
|
|
|
+ const hasMessage = Boolean(group?.messages?.length || group?.lastMessageAt);
|
|
|
+ const pending = Boolean(group?.pendingReply);
|
|
|
+ const failed = Boolean(group?.agentError);
|
|
|
+ const sendFailed = Boolean(group?.sendError);
|
|
|
+ const noReply = Boolean(group?.agentNotice);
|
|
|
+ const auto = group?.mode === 'auto';
|
|
|
+ const sent = ['group_message_sent', 'group_message_auto_sent'].includes(group?.lastOutcome?.action) || group?.status === 'replied';
|
|
|
+ return [
|
|
|
+ { label: '群消息接入', detail: hasMessage ? '消息已同步' : '等待同步', state: hasMessage ? 'done' : 'current' },
|
|
|
+ { label: '范围识别', detail: synced ? '已确认客户群' : '等待确认', state: synced ? 'done' : '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: sent ? '回复已发送' : noReply ? '无需回复' : sendFailed ? '发送失败' : '尚未发送', state: sent || noReply ? 'done' : sendFailed ? 'error' : 'waiting' },
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ function renderAgentFlow(steps, type) {
|
|
|
+ return `<section class="agent-flow ${escapeHtml(type)}" aria-label="${type === 'group' ? '群聊监控流程' : '客户私聊处理流程'}">
|
|
|
+ <div class="agent-flow-heading"><strong>当前处理流程</strong><span>${type === 'group' ? '群聊按当前群策略处理,审核与全自动互不混用' : '客户私聊进入 Agent 与人工协作链路'}</span></div>
|
|
|
+ <ol>${steps.map((step, index) => `<li class="${escapeHtml(step.state)}"><i>${index + 1}</i><div><strong>${escapeHtml(step.label)}</strong><small>${escapeHtml(step.detail)}</small></div></li>`).join('')}</ol>
|
|
|
+ </section>`;
|
|
|
+ }
|
|
|
+
|
|
|
function agentAuditLabel(action) {
|
|
|
return ({
|
|
|
message_received: '收到真实企微消息',
|
|
|
@@ -2029,6 +2134,121 @@
|
|
|
});
|
|
|
}
|
|
|
|
|
|
+ function maskAllowlistId(value) {
|
|
|
+ const id = String(value || '');
|
|
|
+ if (id.length <= 8) return id;
|
|
|
+ return `${id.slice(0, 4)}…${id.slice(-4)}`;
|
|
|
+ }
|
|
|
+
|
|
|
+ async function openAllowlistModal(page) {
|
|
|
+ openModal(
|
|
|
+ '管理 AI 白名单',
|
|
|
+ `<div class="allowlist-intro"><strong>选择允许 AI 处理的客户</strong><span>未选中的联系人不会进入 Agent,也不会收到自动回复。</span></div>
|
|
|
+ <div class="allowlist-toolbar">
|
|
|
+ <input id="allowlist-search" type="search" placeholder="搜索客户姓名、备注或企业" autocomplete="off">
|
|
|
+ <button class="btn btn-secondary btn-sm" id="allowlist-refresh" type="button">刷新联系人</button>
|
|
|
+ </div>
|
|
|
+ <div class="allowlist-summary"><span id="allowlist-count">正在读取联系人…</span><em id="allowlist-warning"></em></div>
|
|
|
+ <div class="allowlist-contact-list" id="allowlist-contact-list"><div class="allowlist-loading">正在从企微读取联系人…</div></div>
|
|
|
+ <details class="allowlist-manual"><summary>找不到联系人?手工添加联系人 ID</summary><div><input id="allowlist-manual-id" placeholder="输入联系人 ID"><button class="btn btn-secondary btn-sm" id="allowlist-manual-add" type="button">添加</button></div><small id="allowlist-manual-error"></small></details>`,
|
|
|
+ `<button class="btn btn-secondary" onclick="window.dashboardCloseModal()">取消</button><button class="btn" id="allowlist-save" disabled>保存白名单</button>`,
|
|
|
+ 'allowlist-modal'
|
|
|
+ );
|
|
|
+ const modal = state.modal;
|
|
|
+ const previousCleanup = modal.__modalCleanup;
|
|
|
+ document.body.classList.add('allowlist-modal-open');
|
|
|
+ modal.__modalCleanup = () => {
|
|
|
+ previousCleanup?.();
|
|
|
+ document.body.classList.remove('allowlist-modal-open');
|
|
|
+ };
|
|
|
+ const listEl = modal.querySelector('#allowlist-contact-list');
|
|
|
+ const searchEl = modal.querySelector('#allowlist-search');
|
|
|
+ const countEl = modal.querySelector('#allowlist-count');
|
|
|
+ const warningEl = modal.querySelector('#allowlist-warning');
|
|
|
+ const saveButton = modal.querySelector('#allowlist-save');
|
|
|
+ let contacts = [];
|
|
|
+ let selected = new Set();
|
|
|
+
|
|
|
+ const updateCount = () => {
|
|
|
+ countEl.textContent = `已选择 ${selected.size} 位联系人`;
|
|
|
+ saveButton.textContent = `保存白名单${selected.size ? `(${selected.size})` : ''}`;
|
|
|
+ };
|
|
|
+ const renderContacts = () => {
|
|
|
+ const keyword = searchEl.value.trim().toLowerCase();
|
|
|
+ const visible = contacts.filter(contact => !keyword || [contact.displayName, contact.remark, contact.company, contact.id]
|
|
|
+ .some(value => String(value || '').toLowerCase().includes(keyword)));
|
|
|
+ listEl.innerHTML = visible.length ? visible.map(contact => {
|
|
|
+ const checked = selected.has(contact.id);
|
|
|
+ const meta = [contact.remark && contact.remark !== contact.displayName ? `备注:${contact.remark}` : '', contact.company, maskAllowlistId(contact.id)].filter(Boolean).join(' · ');
|
|
|
+ return `<label class="allowlist-contact ${checked ? 'selected' : ''}">
|
|
|
+ <input type="checkbox" data-allowlist-id="${escapeHtml(contact.id)}" ${checked ? 'checked' : ''}>
|
|
|
+ <span class="allowlist-avatar">${escapeHtml((contact.displayName || '客').slice(0, 1))}</span>
|
|
|
+ <span class="allowlist-contact-copy"><strong>${escapeHtml(contact.displayName || '企微客户')}</strong><small>${escapeHtml(meta)}</small></span>
|
|
|
+ <em>${checked ? '已选择' : '未选择'}</em>
|
|
|
+ </label>`;
|
|
|
+ }).join('') : `<div class="allowlist-empty"><strong>${contacts.length ? '没有匹配的联系人' : '暂未读取到联系人'}</strong><span>${contacts.length ? '换一个姓名或备注试试。' : '请确认企微在线,或使用下方联系人 ID 添加。'}</span></div>`;
|
|
|
+ updateCount();
|
|
|
+ };
|
|
|
+ const loadContacts = async () => {
|
|
|
+ listEl.innerHTML = '<div class="allowlist-loading">正在从企微读取联系人…</div>';
|
|
|
+ warningEl.textContent = '';
|
|
|
+ saveButton.disabled = true;
|
|
|
+ try {
|
|
|
+ const result = await api('GET', '/api/agent/allowlist', undefined, 35000);
|
|
|
+ if (!modal.isConnected) return;
|
|
|
+ contacts = result.data?.contacts || [];
|
|
|
+ selected = new Set(result.data?.selectedIds || []);
|
|
|
+ warningEl.textContent = result.warnings?.[0] || '';
|
|
|
+ renderContacts();
|
|
|
+ } catch (error) {
|
|
|
+ if (!modal.isConnected) return;
|
|
|
+ listEl.innerHTML = `<div class="allowlist-empty error"><strong>联系人读取失败</strong><span>${escapeHtml(error.message || '请稍后重试,或使用联系人 ID 添加。')}</span></div>`;
|
|
|
+ countEl.textContent = '尚未加载白名单';
|
|
|
+ } finally {
|
|
|
+ if (modal.isConnected) saveButton.disabled = false;
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ searchEl.addEventListener('input', renderContacts);
|
|
|
+ listEl.addEventListener('change', event => {
|
|
|
+ const checkbox = event.target.closest('[data-allowlist-id]');
|
|
|
+ if (!checkbox) return;
|
|
|
+ if (checkbox.checked) selected.add(checkbox.dataset.allowlistId);
|
|
|
+ else selected.delete(checkbox.dataset.allowlistId);
|
|
|
+ renderContacts();
|
|
|
+ });
|
|
|
+ modal.querySelector('#allowlist-refresh').addEventListener('click', loadContacts);
|
|
|
+ modal.querySelector('#allowlist-manual-add').addEventListener('click', () => {
|
|
|
+ const input = modal.querySelector('#allowlist-manual-id');
|
|
|
+ const errorEl = modal.querySelector('#allowlist-manual-error');
|
|
|
+ const id = input.value.trim();
|
|
|
+ if (!/^[A-Za-z0-9._:@-]{1,128}$/.test(id)) {
|
|
|
+ errorEl.textContent = '请输入有效的联系人 ID';
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ errorEl.textContent = '';
|
|
|
+ if (!contacts.some(contact => contact.id === id)) contacts.unshift({ id, displayName: '手工添加联系人', remark: '', company: '' });
|
|
|
+ selected.add(id);
|
|
|
+ input.value = '';
|
|
|
+ searchEl.value = '';
|
|
|
+ renderContacts();
|
|
|
+ });
|
|
|
+ saveButton.addEventListener('click', async () => {
|
|
|
+ if (!selected.size && !window.confirm('清空白名单会立即停止 AI 监听。确认清空吗?')) return;
|
|
|
+ saveButton.disabled = true;
|
|
|
+ try {
|
|
|
+ const result = await api('POST', '/api/agent/allowlist', { contactIds: [...selected] });
|
|
|
+ toast(result.assistantMessage || '白名单已保存');
|
|
|
+ closeModal();
|
|
|
+ await loadAgentData(page);
|
|
|
+ } catch (error) {
|
|
|
+ toast(error.message || '白名单保存失败', 'error');
|
|
|
+ saveButton.disabled = false;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ await loadContacts();
|
|
|
+ }
|
|
|
+
|
|
|
function renderAgentWorkspace(page) {
|
|
|
const status = state.agent.status || {};
|
|
|
const listener = status.listener || {};
|
|
|
@@ -2077,7 +2297,7 @@
|
|
|
<span class="agent-mode-btn ${!aiActive ? 'active' : ''}">人工回复</span>
|
|
|
<span class="agent-mode-btn ${aiActive ? 'active' : ''}">AI 自动回复</span>
|
|
|
</div>
|
|
|
- <div class="agent-guard"><span>安全边界</span>AI 只回复唯一白名单测试联系人</div>
|
|
|
+ <div class="agent-guard"><span>安全边界</span>仅处理 ${Number(config.allowedSenderCount) || 0} 位白名单联系人</div>
|
|
|
</section>
|
|
|
|
|
|
<section class="agent-workspace">
|
|
|
@@ -2147,12 +2367,28 @@
|
|
|
const globalPaused = global.paused !== undefined ? Boolean(global.paused) : status.globalMode === 'paused';
|
|
|
const globalMode = global.defaultMode || (status.globalMode === 'auto' ? 'auto' : 'review');
|
|
|
const listenerRunning = Boolean(listener.running);
|
|
|
+ const allowlistCount = Number(config.allowedSenderCount) || 0;
|
|
|
+ const allowlistReady = allowlistCount > 0;
|
|
|
const agent = status.agent || {};
|
|
|
const knowledge = status.knowledge || {};
|
|
|
const conversations = state.agent.conversations || [];
|
|
|
- const selected = conversations.find(item => item.id === state.agent.selectedId) || conversations[0] || null;
|
|
|
+ const groupAgents = state.agent.groupAgents || [];
|
|
|
+ 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;
|
|
|
if (selected) state.agent.selectedId = selected.id;
|
|
|
+ if (selectedGroup) state.agent.selectedGroupId = selectedGroup.id;
|
|
|
const sessionGuide = selected?.claudeSession || null;
|
|
|
+ const groupPending = selectedGroup?.pendingReply || null;
|
|
|
+ const groupReplyEditKey = selectedGroup && groupPending ? `${selectedGroup.id}:${groupPending.id}` : '';
|
|
|
+ const hasGroupReplyEdit = Boolean(groupReplyEditKey && Object.prototype.hasOwnProperty.call(state.agent.groupReplyEdits, groupReplyEditKey));
|
|
|
+ const groupReplyText = hasGroupReplyEdit ? state.agent.groupReplyEdits[groupReplyEditKey] : (groupPending?.content || '');
|
|
|
|
|
|
const analysis = selected?.analysis || {};
|
|
|
const customerIntelligence = selected?.customerIntelligence || {};
|
|
|
@@ -2164,9 +2400,13 @@
|
|
|
const pending = selected?.pendingReply || null;
|
|
|
const noReplyNotice = selected?.agentNotice || null;
|
|
|
const activeDraft = pending;
|
|
|
- const pendingText = pending?.content || '';
|
|
|
const manualActive = selected?.mode === 'human';
|
|
|
const conversationPaused = selected?.mode === 'paused';
|
|
|
+ const replyEditKey = selected ? `${selected.id}:${pending?.id || (manualActive ? 'manual' : 'composer')}` : '';
|
|
|
+ const hasReplyEdit = Boolean(replyEditKey && Object.prototype.hasOwnProperty.call(state.agent.replyEdits, replyEditKey));
|
|
|
+ const pendingText = hasReplyEdit
|
|
|
+ ? state.agent.replyEdits[replyEditKey]
|
|
|
+ : (pending?.content || '');
|
|
|
const audit = selected?.audit || [];
|
|
|
const toolTrace = activeDraft?.toolTrace || activeDraft?.tool_trace || [];
|
|
|
const citations = activeDraft?.citations || [];
|
|
|
@@ -2185,8 +2425,9 @@
|
|
|
<span class="agent-live-dot ${account.online ? 'online' : ''}"></span>
|
|
|
<div><strong>${escapeHtml(accountName)} · ${account.online ? '在线' : '离线'}</strong><small>${escapeHtml(account.corpName || config.transport || '企微通道')} · ${listenerRunning ? '真实消息监听中' : '监听已停止'}</small></div>
|
|
|
<div class="agent-safety-actions">
|
|
|
- <button class="btn btn-secondary" data-agent-action="sync-conversations" ${account.online ? '' : 'disabled'}>补采最近消息</button>
|
|
|
- <button class="btn ${listenerRunning ? 'btn-secondary' : ''}" data-agent-action="${listenerRunning ? 'stop-listener' : 'start-listener'}" ${account.online ? '' : 'disabled'}>${listenerRunning ? '关闭 AI 监听(转人工)' : '启动 AI 监听'}</button>
|
|
|
+ <button class="btn btn-secondary" data-agent-action="manage-allowlist">管理白名单</button>
|
|
|
+ <button class="btn btn-secondary" data-agent-action="sync-conversations" ${account.online && allowlistReady ? '' : 'disabled'}>补采最近消息</button>
|
|
|
+ <button class="btn ${listenerRunning ? 'btn-secondary' : ''}" data-agent-action="${allowlistReady ? (listenerRunning ? 'stop-listener' : 'start-listener') : 'manage-allowlist'}" ${account.online ? '' : 'disabled'}>${allowlistReady ? (listenerRunning ? '关闭 AI 监听(转人工)' : '启动 AI 监听') : '先选择白名单'}</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
</section>
|
|
|
@@ -2198,7 +2439,7 @@
|
|
|
<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>
|
|
|
</div>
|
|
|
- <div class="agent-guard"><span>安全边界</span>仅处理 1 个白名单联系人 · 演示回复已禁用</div>
|
|
|
+ <div class="agent-guard"><span>发送边界</span>私聊限 ${allowlistCount} 位白名单联系人 · 客户群按各群独立模式处理${groupAgents.some(item => item.mode === 'auto') ? ` · ${groupAgents.filter(item => item.mode === 'auto').length} 个群已开启全自动` : ''}</div>
|
|
|
</section>
|
|
|
|
|
|
${renderResponseMonitor(state.agent.responseMonitor || {})}
|
|
|
@@ -2211,21 +2452,49 @@
|
|
|
|
|
|
<section class="agent-workspace">
|
|
|
<aside class="agent-conversation-list">
|
|
|
- <div class="agent-panel-heading"><div><strong>真实客户会话</strong><small>${conversations.length} 个白名单会话</small></div><span class="agent-count">${conversations.length}</span></div>
|
|
|
+ <div class="agent-panel-heading"><div><strong>消息入口</strong><small>私聊与群聊智能回复已分开</small></div><span class="agent-count">${privateConversations.length + groupConversations.length}</span></div>
|
|
|
+ <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>
|
|
|
+ </div>
|
|
|
<div class="agent-list-scroll">
|
|
|
- ${conversations.length ? conversations.map(item => {
|
|
|
+ ${listMode === 'private' && privateConversations.length ? privateConversations.map(item => {
|
|
|
const last = [...(item.messages || [])].reverse().find(message => message.role === 'customer') || item.messages?.[item.messages.length - 1];
|
|
|
return `<button class="agent-conversation-item ${item.id === selected?.id ? 'active' : ''}" data-conversation-id="${item.id}">
|
|
|
<span class="agent-avatar">${escapeHtml((item.displayName || '客').slice(0, 1))}</span>
|
|
|
- <span class="agent-conversation-copy"><strong>${escapeHtml(item.displayName)}</strong><small>${escapeHtml(last?.content || '等待新消息')}</small><em>${agentModeLabel(item.mode)}${item.pendingReply ? ' · 有待审草稿' : ''}${item.customerIntelligence?.summary?.openTasks ? ` · ${item.customerIntelligence.summary.openTasks}待办` : ''}${item.customerIntelligence?.summary?.highAlerts ? ` · ${item.customerIntelligence.summary.highAlerts}预警` : ''}</em></span>
|
|
|
+ <span class="agent-conversation-copy"><span class="agent-conversation-name"><strong>${escapeHtml(item.displayName)}</strong><i class="agent-channel-badge private">私聊</i></span><small>${escapeHtml(last?.content || '等待新消息')}</small><em>${agentModeLabel(item.mode)}${item.pendingReply ? ' · 有待审草稿' : ''}${item.customerIntelligence?.summary?.openTasks ? ` · ${item.customerIntelligence.summary.openTasks}待办` : ''}${item.customerIntelligence?.summary?.highAlerts ? ` · ${item.customerIntelligence.summary.highAlerts}预警` : ''}</em></span>
|
|
|
<time>${formatAgentTime(item.lastMessageAt)}</time>
|
|
|
</button>`;
|
|
|
- }).join('') : `<div class="agent-empty"><strong>等待第一条真实消息</strong><span>开始接收后,用“王刚”的企微发送测试内容。</span></div>`}
|
|
|
+ }).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-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>
|
|
|
+ </button>`).join('') : `<div class="agent-empty"><strong>${listMode === 'group' ? '还没有已确认客户群' : allowlistReady ? '等待第一条真实消息' : '还没有选择白名单'}</strong><span>${listMode === 'group' ? '先在客户群管理中确认需要智能回复的客户群。' : allowlistReady ? '启动监听后,请从已选企微客户发送测试内容。' : '点击上方“管理白名单”,直接从企微联系人中选择。'}</span></div>`}
|
|
|
</div>
|
|
|
</aside>
|
|
|
|
|
|
- <main class="agent-chat-panel">
|
|
|
- ${selected ? `
|
|
|
+ <main class="agent-chat-panel ${selectedGroup ? 'group-mode' : ''}">
|
|
|
+ ${selectedGroup ? `
|
|
|
+ <header class="agent-chat-header group">
|
|
|
+ <div><strong>${escapeHtml(selectedGroup.name || '客户群')}</strong><span>客户群聊 · ${selectedGroup.mode === 'auto' ? '全自动(忽略风险)' : 'Agent 草稿 · 人工审核发送'}</span></div>
|
|
|
+ <div class="agent-conversation-modes group-modes">
|
|
|
+ <button class="btn btn-sm ${selectedGroup.mode === 'auto' ? 'btn-secondary' : ''}" data-agent-group-mode="review" data-room-id="${escapeHtml(selectedGroup.id)}">人工审核</button>
|
|
|
+ <button class="btn btn-sm agent-group-mode-danger ${selectedGroup.mode === 'auto' ? 'active' : 'btn-secondary'}" data-agent-group-mode="auto" data-room-id="${escapeHtml(selectedGroup.id)}">无视风险 · 全自动</button>
|
|
|
+ </div>
|
|
|
+ </header>
|
|
|
+ ${renderAgentFlow(groupConversationFlow(selectedGroup), 'group')}
|
|
|
+ <div class="agent-message-stream group">${selectedGroup.messages?.length ? selectedGroup.messages.map(renderAgentMessage).join('') : '<div class="agent-empty"><strong>等待群消息</strong><span>同步群消息后,Agent 可以生成待审核回复。</span></div>'}</div>
|
|
|
+ <div class="agent-composer group">
|
|
|
+ <div class="agent-composer-label"><strong>${groupPending ? (selectedGroup.sendError ? '全自动发送失败 · 草稿已保留' : '群聊 Agent 待审核草稿') : selectedGroup.agentNotice ? 'Agent 已处理' : selectedGroup.agentError ? 'Agent 生成失败' : selectedGroup.mode === 'auto' ? '群聊全自动回复' : '群聊智能回复'}</strong><span data-agent-group-edit-status>${groupPending ? (hasGroupReplyEdit ? '已人工修改 · 批准后发送当前内容' : `置信度 ${Math.round((groupPending.confidence || 0) * 100)}% · ${selectedGroup.mode === 'auto' ? '自动发送未完成,可人工处理' : '必须人工审核'}`) : selectedGroup.agentNotice ? '最新群消息无需回复' : selectedGroup.agentError ? '可重新生成或直接在企微回复' : selectedGroup.mode === 'auto' ? '忽略置信度、敏感判断和人工风险标记,生成后直接发送' : 'Agent 只生成草稿,不会自动外发'}</span></div>
|
|
|
+ <textarea id="agent-group-reply-editor" data-agent-group-edit-key="${escapeHtml(groupReplyEditKey)}" placeholder="${groupPending ? '可先编辑 Agent 群聊草稿…' : '点击下方按钮,让 Agent 分析最近一条群消息'}">${escapeHtml(groupReplyText)}</textarea>
|
|
|
+ <div class="agent-composer-actions">
|
|
|
+ <span>${groupPending ? escapeHtml(groupPending.reason || '请核对群成员、事实与表达后再发送') : `回复时效:${selectedGroup.waitingMinutes ? formatReplyWaiting(selectedGroup.waitingMinutes) : agentMonitorStatusLabel(selectedGroup.status)}`}</span>
|
|
|
+ <div class="agent-review-actions">
|
|
|
+ ${groupPending ? `<button class="btn btn-secondary" data-agent-action="group-reject" data-room-id="${escapeHtml(selectedGroup.id)}" data-draft-id="${escapeHtml(groupPending.id)}">驳回</button><button class="btn btn-secondary" data-agent-action="group-regenerate" data-room-id="${escapeHtml(selectedGroup.id)}" data-draft-id="${escapeHtml(groupPending.id)}">重新生成</button><button class="btn" data-agent-action="group-approve" data-room-id="${escapeHtml(selectedGroup.id)}" data-draft-id="${escapeHtml(groupPending.id)}">批准并发送到群</button>` : `<button class="btn ${selectedGroup.mode === 'auto' ? 'agent-group-auto-send' : ''}" data-agent-action="group-generate" data-room-id="${escapeHtml(selectedGroup.id)}">${selectedGroup.mode === 'auto' ? '立即生成并自动发送' : '让 Agent 生成草稿'}</button>`}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ ` : selected ? `
|
|
|
<header class="agent-chat-header">
|
|
|
<div><strong>${escapeHtml(selected.displayName)}</strong><span>${escapeHtml(selected.maskedId)} · 真实企微会话 · ${agentModeLabel(selected.mode)}</span></div>
|
|
|
<div class="agent-conversation-modes">
|
|
|
@@ -2235,6 +2504,7 @@
|
|
|
<button class="btn btn-sm ${selected.mode === 'paused' ? '' : 'btn-secondary'}" data-agent-conversation-mode="paused" data-id="${selected.id}">暂停</button>
|
|
|
</div>
|
|
|
</header>
|
|
|
+ ${renderAgentFlow(privateConversationFlow(selected), 'private')}
|
|
|
<div class="agent-session-strip ${sessionGuide?.ready ? 'ready' : ''}">
|
|
|
<span class="agent-session-dot"></span>
|
|
|
<div><strong>${sessionGuide?.ready ? 'Claude Code 客户 Session 已识别' : 'Claude Code 客户 Session 等待首次运行'}</strong><small>${escapeHtml(sessionGuide?.displayName || 'Agent 首次生成草稿后自动创建')} · ${sessionGuide?.ready ? '可安全打开审阅副本' : '不会与其他客户共用上下文'}</small></div>
|
|
|
@@ -2242,8 +2512,8 @@
|
|
|
</div>
|
|
|
<div class="agent-message-stream">${(selected.messages || []).map(renderAgentMessage).join('')}</div>
|
|
|
<div class="agent-composer ${manualActive ? 'manual' : ''}">
|
|
|
- <div class="agent-composer-label"><strong>${pending ? 'Agent 待审核草稿' : manualActive ? '人工回复' : conversationPaused ? '会话已暂停' : noReplyNotice ? 'Agent 已处理' : 'Agent 处理区'}</strong><span>${pending ? `置信度 ${Math.round((pending.confidence || 0) * 100)}% · ${pending.requiresHuman ? '必须人工审核' : '可审核发送'}` : manualActive ? 'Agent 不会生成或发送回复' : conversationPaused ? '消息保留,Agent 不处理' : noReplyNotice ? '最新消息无需回复' : '没有待审核草稿'}</span></div>
|
|
|
- <textarea id="agent-reply-editor" placeholder="${manualActive ? '输入人工回复内容…' : pending ? '可先编辑 Agent 草稿…' : noReplyNotice ? '最新客户消息无需回复' : '点击下方按钮,让 Agent 处理最近一条消息'}" ${conversationPaused ? 'disabled' : ''}>${escapeHtml(pendingText)}</textarea>
|
|
|
+ <div class="agent-composer-label"><strong>${pending ? 'Agent 待审核草稿' : manualActive ? '人工回复' : conversationPaused ? '会话已暂停' : noReplyNotice ? 'Agent 已处理' : 'Agent 处理区'}</strong><span data-agent-edit-status>${pending ? (hasReplyEdit ? '已人工修改 · 批准后发送当前内容' : `置信度 ${Math.round((pending.confidence || 0) * 100)}% · ${pending.requiresHuman ? '必须人工审核,可直接编辑' : '可直接编辑后发送'}`) : manualActive ? 'Agent 不会生成或发送回复' : conversationPaused ? '消息保留,Agent 不处理' : noReplyNotice ? '最新消息无需回复' : '没有待审核草稿'}</span></div>
|
|
|
+ <textarea id="agent-reply-editor" data-agent-edit-key="${escapeHtml(replyEditKey)}" placeholder="${manualActive ? '输入人工回复内容…' : pending ? '可先编辑 Agent 草稿…' : noReplyNotice ? '最新客户消息无需回复' : '点击下方按钮,让 Agent 处理最近一条消息'}" ${conversationPaused ? 'disabled' : ''}>${escapeHtml(pendingText)}</textarea>
|
|
|
<div class="agent-composer-actions">
|
|
|
<span>${pending ? escapeHtml(pending.reason || '请核对内容与依据后再发送') : `模型:${escapeHtml(agent.model || '未配置')} · ${agent.configured ? '已配置' : '未配置'}`}</span>
|
|
|
<div class="agent-review-actions">
|
|
|
@@ -2251,11 +2521,24 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
- ` : `<div class="agent-empty large"><strong>真实 Agent 会话准备就绪</strong><span>开始接收,然后从“王刚”的企微发送:“我想在新北区买婚房,预算150万,三室,最好精装”</span></div>`}
|
|
|
+ ` : `<div class="agent-empty large"><strong>真实 Agent 会话准备就绪</strong><span>开始接收后,请从已选白名单客户的企微发送测试消息。</span></div>`}
|
|
|
</main>
|
|
|
|
|
|
- <aside class="agent-insight-panel" data-conversation-id="${escapeHtml(selected?.id || '')}">
|
|
|
- ${selected ? `
|
|
|
+ <aside class="agent-insight-panel" data-conversation-id="${escapeHtml(selected?.id || selectedGroup?.id || '')}">
|
|
|
+ ${selectedGroup ? `
|
|
|
+ <div class="agent-insight-section">
|
|
|
+ <div class="agent-section-title"><span>01</span><div><strong>智能回复状态</strong><small>群聊独立上下文</small></div></div>
|
|
|
+ <div class="agent-intent-card group ${selectedGroup.mode === 'auto' ? 'auto' : ''}"><strong>${selectedGroup.mode === 'auto' ? '全自动 · 忽略风险' : groupPending ? '草稿待审核' : selectedGroup.agentError ? '生成失败' : selectedGroup.agentNotice ? '无需回复' : '等待 Agent'}</strong><span>${escapeHtml(agentMonitorStatusLabel(selectedGroup.status))}</span></div>
|
|
|
+ </div>
|
|
|
+ <div class="agent-insight-section">
|
|
|
+ <div class="agent-section-title"><span>02</span><div><strong>回复时效</strong><small>本地计时与提醒</small></div></div>
|
|
|
+ <div class="agent-group-sla"><strong>${selectedGroup.waitingMinutes ? formatReplyWaiting(selectedGroup.waitingMinutes) : '未开始计时'}</strong><span>${selectedGroup.status === 'overdue' ? '已超过紧急提醒阈值' : selectedGroup.status === 'warning' ? '已进入提醒区间' : '当前未超时'}</span></div>
|
|
|
+ </div>
|
|
|
+ <div class="agent-insight-section">
|
|
|
+ <div class="agent-section-title"><span>03</span><div><strong>处理边界</strong><small>不会串入客户私聊</small></div></div>
|
|
|
+ <ul class="agent-boundary-list ${selectedGroup.mode === 'auto' ? 'danger' : ''}">${selectedGroup.mode === 'auto' ? '<li>忽略置信度、敏感判断和人工风险标记</li><li>Agent 返回非空内容后立即真实群发</li><li>模型或发送失败时停止并保留草稿</li><li>不读取其他客户私聊与画像</li>' : '<li>已确认客户群自动生成回复草稿</li><li>每条草稿必须人工编辑或审核</li><li>不读取其他客户私聊与画像</li><li>确认后才通过 Fmode 网关真实群发</li>'}</ul>
|
|
|
+ </div>
|
|
|
+ ` : selected ? `
|
|
|
<div class="agent-insight-section">
|
|
|
<div class="agent-section-title"><span>01</span><div><strong>意图判断</strong><small>Agent 结构化理解</small></div></div>
|
|
|
<div class="agent-intent-card"><strong>${escapeHtml(analysis.intentLabel || analysis.intent || '待识别')}</strong><span>画像完整度 ${Math.round(analysis.completenessScore || 0)}%</span><div><i style="width:${Math.min(100, analysis.completenessScore || 0)}%"></i></div></div>
|
|
|
@@ -2293,7 +2576,7 @@
|
|
|
</aside>
|
|
|
</section>
|
|
|
`;
|
|
|
- restoreAgentInsightScroll(page, insightScroll, selected?.id);
|
|
|
+ restoreAgentInsightScroll(page, insightScroll, selected?.id || selectedGroup?.id);
|
|
|
scrollAgentMessagesToLatest(page);
|
|
|
}
|
|
|
|
|
|
@@ -2301,14 +2584,16 @@
|
|
|
if (state.agent.loading) return;
|
|
|
state.agent.loading = true;
|
|
|
try {
|
|
|
- const [statusRes, conversationsRes, responseMonitorRes] = await Promise.all([
|
|
|
+ const [statusRes, conversationsRes, responseMonitorRes, groupAgentsRes] = await Promise.all([
|
|
|
api('GET', '/api/agent/status'),
|
|
|
api('GET', '/api/agent/conversations'),
|
|
|
api('GET', '/api/agent/response-monitor'),
|
|
|
+ api('GET', '/api/agent/groups'),
|
|
|
]);
|
|
|
const nextStatus = statusRes.data || {};
|
|
|
const nextConversations = conversationsRes.data?.conversations || [];
|
|
|
const nextResponseMonitor = responseMonitorRes.data || {};
|
|
|
+ const nextGroupAgents = groupAgentsRes.data?.groups || [];
|
|
|
const localAccount = currentAccount();
|
|
|
const activeAccount = nextStatus.account || {};
|
|
|
if (localAccount && localAccount.uid && activeAccount.uid === localAccount.uid) {
|
|
|
@@ -2343,15 +2628,22 @@
|
|
|
config: nextStatus.config,
|
|
|
conversations: nextConversations,
|
|
|
responseMonitor: nextResponseMonitor,
|
|
|
+ groupAgents: nextGroupAgents,
|
|
|
});
|
|
|
const changed = nextSignature !== state.agent.signature;
|
|
|
state.agent.status = nextStatus;
|
|
|
state.agent.conversations = nextConversations;
|
|
|
state.agent.responseMonitor = nextResponseMonitor;
|
|
|
- state.agent.signature = nextSignature;
|
|
|
- if (!state.agent.selectedId && state.agent.conversations.length) state.agent.selectedId = state.agent.conversations[0].id;
|
|
|
+ state.agent.groupAgents = nextGroupAgents;
|
|
|
+ if (!state.agent.selectedId && state.agent.conversations.length) {
|
|
|
+ state.agent.selectedId = state.agent.conversations.find(item => item.channelType !== 'group')?.id || null;
|
|
|
+ }
|
|
|
const waitingForFirstRender = Boolean(page.querySelector('.agent-loading'));
|
|
|
- if ((changed || waitingForFirstRender) && state.page === 'agent' && page.isConnected) renderAgentWorkspaceV2(page);
|
|
|
+ const editorFocused = document.activeElement === page.querySelector('#agent-reply-editor') || document.activeElement === page.querySelector('#agent-group-reply-editor');
|
|
|
+ if (!editorFocused) {
|
|
|
+ state.agent.signature = nextSignature;
|
|
|
+ if ((changed || waitingForFirstRender) && state.page === 'agent' && page.isConnected) renderAgentWorkspaceV2(page);
|
|
|
+ }
|
|
|
} catch (error) {
|
|
|
if (!silent) toast(error.message || '智能会话加载失败', 'error');
|
|
|
} finally {
|
|
|
@@ -2371,19 +2663,51 @@
|
|
|
page.innerHTML = '<div class="agent-loading">正在读取企微智能会话…</div>';
|
|
|
els.content.appendChild(page);
|
|
|
|
|
|
+ page.addEventListener('input', event => {
|
|
|
+ const editor = event.target.closest('#agent-reply-editor');
|
|
|
+ if (editor?.dataset.agentEditKey) {
|
|
|
+ state.agent.replyEdits[editor.dataset.agentEditKey] = editor.value;
|
|
|
+ const status = page.querySelector('[data-agent-edit-status]');
|
|
|
+ if (status) status.textContent = '已人工修改 · 批准后发送当前内容';
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const groupEditor = event.target.closest('#agent-group-reply-editor');
|
|
|
+ if (!groupEditor?.dataset.agentGroupEditKey) return;
|
|
|
+ state.agent.groupReplyEdits[groupEditor.dataset.agentGroupEditKey] = groupEditor.value;
|
|
|
+ const status = page.querySelector('[data-agent-group-edit-status]');
|
|
|
+ if (status) status.textContent = '已人工修改 · 批准后发送当前内容';
|
|
|
+ });
|
|
|
+
|
|
|
page.addEventListener('click', async event => {
|
|
|
const responseMonitorButton = event.target.closest('[data-response-monitor-target]');
|
|
|
if (responseMonitorButton) {
|
|
|
if (responseMonitorButton.dataset.responseMonitorTarget === 'customer') {
|
|
|
+ state.agent.listMode = 'private';
|
|
|
state.agent.selectedId = responseMonitorButton.dataset.id;
|
|
|
- renderAgentWorkspaceV2(page);
|
|
|
} else {
|
|
|
- location.hash = '#groups';
|
|
|
+ state.agent.listMode = 'group';
|
|
|
+ state.agent.selectedGroupId = responseMonitorButton.dataset.id;
|
|
|
}
|
|
|
+ renderAgentWorkspaceV2(page);
|
|
|
+ page.querySelector('.agent-workspace')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const listModeButton = event.target.closest('[data-agent-list-mode]');
|
|
|
+ if (listModeButton) {
|
|
|
+ state.agent.listMode = listModeButton.dataset.agentListMode === 'group' ? 'group' : 'private';
|
|
|
+ renderAgentWorkspaceV2(page);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const groupMonitorButton = event.target.closest('[data-group-monitor-id]');
|
|
|
+ if (groupMonitorButton) {
|
|
|
+ state.agent.listMode = 'group';
|
|
|
+ state.agent.selectedGroupId = groupMonitorButton.dataset.groupMonitorId;
|
|
|
+ renderAgentWorkspaceV2(page);
|
|
|
return;
|
|
|
}
|
|
|
const conversationButton = event.target.closest('[data-conversation-id]');
|
|
|
if (conversationButton) {
|
|
|
+ state.agent.listMode = 'private';
|
|
|
state.agent.selectedId = conversationButton.dataset.conversationId;
|
|
|
renderAgentWorkspaceV2(page);
|
|
|
return;
|
|
|
@@ -2392,7 +2716,8 @@
|
|
|
const modeButton = event.target.closest('[data-agent-mode]');
|
|
|
if (modeButton) {
|
|
|
const mode = modeButton.dataset.agentMode;
|
|
|
- if (mode === 'auto' && !window.confirm('高置信自动模式会真实发送回复,仅对白名单联系人“王刚”生效。确认开启吗?')) return;
|
|
|
+ const allowlistCount = Number(state.agent.status?.config?.allowedSenderCount) || 0;
|
|
|
+ if (mode === 'auto' && !window.confirm(`高置信自动模式会向白名单中的 ${allowlistCount} 位联系人真实发送回复。确认开启吗?`)) return;
|
|
|
try {
|
|
|
const result = await api('POST', '/api/agent/mode', { mode });
|
|
|
toast(result.assistantMessage || '模式已更新');
|
|
|
@@ -2422,10 +2747,39 @@
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
+ const groupModeButton = event.target.closest('[data-agent-group-mode]');
|
|
|
+ if (groupModeButton) {
|
|
|
+ const roomId = groupModeButton.dataset.roomId;
|
|
|
+ const mode = groupModeButton.dataset.agentGroupMode;
|
|
|
+ if (mode === 'auto' && !window.confirm('全自动模式会忽略置信度、敏感判断和人工风险标记。Agent 只要返回非空内容,就会直接真实发送到该客户群。确认开启吗?')) return;
|
|
|
+ groupModeButton.disabled = true;
|
|
|
+ try {
|
|
|
+ const result = await api('POST', `/api/agent/groups/${encodeURIComponent(roomId)}/mode`, {
|
|
|
+ mode,
|
|
|
+ confirmation: mode === 'auto' ? 'AUTO_SEND_GROUP_MESSAGES' : '',
|
|
|
+ });
|
|
|
+ toast(result.assistantMessage || '群聊模式已更新');
|
|
|
+ await loadAgentData(page);
|
|
|
+ } catch (error) {
|
|
|
+ toast(error.message || '群聊模式切换失败', 'error');
|
|
|
+ } finally {
|
|
|
+ if (groupModeButton.isConnected) groupModeButton.disabled = false;
|
|
|
+ }
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
const actionButton = event.target.closest('[data-agent-action]');
|
|
|
if (!actionButton) return;
|
|
|
const action = actionButton.dataset.agentAction;
|
|
|
const id = actionButton.dataset.id;
|
|
|
+ const editor = page.querySelector('#agent-reply-editor');
|
|
|
+ const replyEditKey = editor?.dataset.agentEditKey || '';
|
|
|
+ const groupEditor = page.querySelector('#agent-group-reply-editor');
|
|
|
+ const groupReplyEditKey = groupEditor?.dataset.agentGroupEditKey || '';
|
|
|
+ if (action === 'manage-allowlist') {
|
|
|
+ await openAllowlistModal(page);
|
|
|
+ return;
|
|
|
+ }
|
|
|
if (action === 'session-guide') {
|
|
|
const conversation = (state.agent.conversations || []).find(item => item.id === id);
|
|
|
openAgentSessionGuide(conversation?.claudeSession || {});
|
|
|
@@ -2440,7 +2794,8 @@
|
|
|
const synced = result.data?.conversations || [];
|
|
|
if (synced.length) {
|
|
|
state.agent.conversations = synced;
|
|
|
- state.agent.selectedId = synced[0].id;
|
|
|
+ state.agent.listMode = 'private';
|
|
|
+ state.agent.selectedId = synced.find(item => item.channelType !== 'group')?.id || null;
|
|
|
}
|
|
|
} else if (action === 'customer-task-sync-official') {
|
|
|
const selected = (state.agent.conversations || []).find(item => item.id === state.agent.selectedId) || (state.agent.conversations || [])[0];
|
|
|
@@ -2460,12 +2815,35 @@
|
|
|
} else if (action === 'customer-alert-ack' || action === 'customer-alert-dismiss') {
|
|
|
result = await api('POST', `/api/agent/alerts/${id}`, { status: action === 'customer-alert-ack' ? 'acknowledged' : 'dismissed' });
|
|
|
} else if (action === 'start-listener' || action === 'stop-listener') {
|
|
|
- if (action === 'start-listener' && !window.confirm('启动后,白名单客户的新消息会由 AI 处理;高置信回复可能自动真实发送。人工可随时关闭监听或接管会话。确认启动吗?')) return;
|
|
|
+ if (action === 'start-listener') {
|
|
|
+ const autoGroups = (state.agent.groupAgents || []).filter(item => item.mode === 'auto');
|
|
|
+ const groupNotice = autoGroups.length
|
|
|
+ ? `其中 ${autoGroups.length} 个客户群已开启“无视风险 · 全自动”,Agent 非空回复会直接真实群发。`
|
|
|
+ : '当前所有客户群均为人工审核模式,不会自动群发。';
|
|
|
+ if (!window.confirm(`启动后,白名单私聊会按当前策略处理。${groupNotice}确认启动吗?`)) return;
|
|
|
+ }
|
|
|
result = await api('POST', `/api/agent/listener/${action === 'start-listener' ? 'start' : 'stop'}`, {});
|
|
|
+ } else if (action === 'group-generate') {
|
|
|
+ const group = (state.agent.groupAgents || []).find(item => item.roomId === actionButton.dataset.roomId);
|
|
|
+ if (group?.mode === 'auto' && !window.confirm(`当前为全自动模式。继续后,Agent 生成的非空内容会立即真实发送到“${group.roomName || group.roomId}”。确认继续吗?`)) return;
|
|
|
+ result = await api('POST', `/api/agent/groups/${encodeURIComponent(actionButton.dataset.roomId)}/generate`, {});
|
|
|
+ } else if (action === 'group-approve') {
|
|
|
+ const content = groupEditor?.value.trim();
|
|
|
+ if (!content) throw new Error('请先输入群聊回复内容');
|
|
|
+ const selectedGroup = (state.agent.groupAgents || []).find(item => item.roomId === actionButton.dataset.roomId);
|
|
|
+ if (!window.confirm(`确认审核通过,并真实发送到客户群“${selectedGroup?.roomName || actionButton.dataset.roomId}”?`)) return;
|
|
|
+ result = await api('POST', `/api/agent/groups/${encodeURIComponent(actionButton.dataset.roomId)}/drafts/${encodeURIComponent(actionButton.dataset.draftId)}/approve`, { content });
|
|
|
+ } else if (action === 'group-reject') {
|
|
|
+ const reason = window.prompt('驳回原因(可选)', '需要人工调整');
|
|
|
+ if (reason === null) return;
|
|
|
+ result = await api('POST', `/api/agent/groups/${encodeURIComponent(actionButton.dataset.roomId)}/drafts/${encodeURIComponent(actionButton.dataset.draftId)}/reject`, { reason });
|
|
|
+ } else if (action === 'group-regenerate') {
|
|
|
+ result = await api('POST', `/api/agent/groups/${encodeURIComponent(actionButton.dataset.roomId)}/drafts/${encodeURIComponent(actionButton.dataset.draftId)}/regenerate`, {});
|
|
|
} else if (action === 'approve') {
|
|
|
const content = page.querySelector('#agent-reply-editor')?.value.trim();
|
|
|
if (!content) throw new Error('请先输入回复内容');
|
|
|
- if (!window.confirm('确认审核通过,并真实发送给白名单联系人“王刚”?')) return;
|
|
|
+ const selected = (state.agent.conversations || []).find(item => item.id === state.agent.selectedId);
|
|
|
+ if (!window.confirm(`确认审核通过,并真实发送给白名单联系人“${selected?.displayName || '当前客户'}”?`)) return;
|
|
|
result = await api('POST', `/api/agent/drafts/${actionButton.dataset.draftId}/approve`, { content });
|
|
|
} else if (action === 'reject') {
|
|
|
const reason = window.prompt('驳回原因(可选)', '需要人工调整');
|
|
|
@@ -2478,10 +2856,17 @@
|
|
|
} else if (action === 'manual-send') {
|
|
|
const content = page.querySelector('#agent-reply-editor')?.value.trim();
|
|
|
if (!content) throw new Error('请先输入人工回复内容');
|
|
|
- if (!window.confirm('确认由人工真实发送给白名单联系人“王刚”?')) return;
|
|
|
+ const selected = (state.agent.conversations || []).find(item => item.id === state.agent.selectedId);
|
|
|
+ if (!window.confirm(`确认由人工真实发送给白名单联系人“${selected?.displayName || '当前客户'}”?`)) return;
|
|
|
result = await api('POST', `/api/agent/conversations/${id}/manual-send`, { content });
|
|
|
}
|
|
|
if (result) toast(result.assistantMessage || '操作成功');
|
|
|
+ if (result && replyEditKey && ['approve', 'reject', 'regenerate', 'generate', 'manual-send'].includes(action)) {
|
|
|
+ delete state.agent.replyEdits[replyEditKey];
|
|
|
+ }
|
|
|
+ if (result && groupReplyEditKey && ['group-approve', 'group-reject', 'group-regenerate', 'group-generate'].includes(action)) {
|
|
|
+ delete state.agent.groupReplyEdits[groupReplyEditKey];
|
|
|
+ }
|
|
|
await loadAgentData(page);
|
|
|
} catch (error) {
|
|
|
toast(error.message || '操作失败', 'error');
|