|
@@ -84,10 +84,16 @@
|
|
|
voiceTone: 'auto',
|
|
voiceTone: 'auto',
|
|
|
renderPending: false,
|
|
renderPending: false,
|
|
|
insightInteractionUntil: 0,
|
|
insightInteractionUntil: 0,
|
|
|
|
|
+ openDrawer: null,
|
|
|
|
|
+ activityOpen: false,
|
|
|
|
|
+ intakePolicy: { mode: 'allowlist_only', welcomeEnabled: false, welcomeText: '', welcomeSendMode: 'draft' },
|
|
|
|
|
+ intakePolicyLoaded: false,
|
|
|
|
|
+ intakePolicyLoading: false,
|
|
|
|
|
+ intakePolicyError: '',
|
|
|
cachedAccountKey: initialAgentSnapshot.accountKey || null
|
|
cachedAccountKey: initialAgentSnapshot.accountKey || null
|
|
|
},
|
|
},
|
|
|
skills: { registry: null, selectedPackageId: null, selectedSkillId: null, keyword: '' },
|
|
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' } } },
|
|
|
|
|
|
|
+ knowledge: { tree: null, selectedNodeId: null, file: null, expandedFolderIds: new Set(), foldersInitialized: false, 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, candidates: [], groups: [], dashboardSnapshot: null, loadingPromise: null, ...loadFilters(TF_FILTERS_KEY, { tablePage: 1, status: '', user: '' }) }
|
|
transfers: { lastTransferPreview: null, candidates: [], groups: [], dashboardSnapshot: null, loadingPromise: null, ...loadFilters(TF_FILTERS_KEY, { tablePage: 1, status: '', user: '' }) }
|
|
|
};
|
|
};
|
|
|
|
|
|
|
@@ -260,6 +266,11 @@
|
|
|
state.agent.voiceTone = 'auto';
|
|
state.agent.voiceTone = 'auto';
|
|
|
state.agent.renderPending = false;
|
|
state.agent.renderPending = false;
|
|
|
state.agent.insightInteractionUntil = 0;
|
|
state.agent.insightInteractionUntil = 0;
|
|
|
|
|
+ state.agent.openDrawer = null;
|
|
|
|
|
+ state.agent.activityOpen = false;
|
|
|
|
|
+ state.agent.intakePolicy = { mode: 'allowlist_only', welcomeEnabled: false, welcomeText: '', welcomeSendMode: 'draft' };
|
|
|
|
|
+ state.agent.intakePolicyLoaded = false;
|
|
|
|
|
+ state.agent.intakePolicyError = '';
|
|
|
state.agent.cachedAccountKey = null;
|
|
state.agent.cachedAccountKey = null;
|
|
|
try { sessionStorage.removeItem(AGENT_SNAPSHOT_KEY); } catch {}
|
|
try { sessionStorage.removeItem(AGENT_SNAPSHOT_KEY); } catch {}
|
|
|
}
|
|
}
|
|
@@ -1690,7 +1701,7 @@
|
|
|
|
|
|
|
|
page.innerHTML = `
|
|
page.innerHTML = `
|
|
|
<section class="workspace-hero skills-hero">
|
|
<section class="workspace-hero skills-hero">
|
|
|
- <div><span>UNIFIED QIWEI CAPABILITY HUB</span><h2>技能中心</h2><p>把源码版、OpenClaw、Agent Workbench 和花巷房源匹配统一到一个 4320 工作台。</p></div>
|
|
|
|
|
|
|
+ <div><span>UNIFIED QIWEI CAPABILITY HUB</span><h2>技能中心</h2><p>把通用企微 Skills、Agent Workbench 和运行工具统一到一个 4320 工作台。</p></div>
|
|
|
<div class="workspace-stat-row">
|
|
<div class="workspace-stat-row">
|
|
|
<div><strong>${registry.summary.packageCount || 0}</strong><span>能力来源</span></div>
|
|
<div><strong>${registry.summary.packageCount || 0}</strong><span>能力来源</span></div>
|
|
|
<div><strong>${registry.summary.skillCount || 0}</strong><span>技能实例</span></div>
|
|
<div><strong>${registry.summary.skillCount || 0}</strong><span>技能实例</span></div>
|
|
@@ -1766,7 +1777,7 @@
|
|
|
|
|
|
|
|
function knowledgeIcon(node) {
|
|
function knowledgeIcon(node) {
|
|
|
if (node.type === 'folder') return '▸';
|
|
if (node.type === 'folder') return '▸';
|
|
|
- return ({ 'property-dataset': '⌂', 'meeting-dashboard': '会', 'doc-dashboard': '文', 'todo-dashboard': '办', 'task-dashboard': '任', markdown: 'M', 'skill-document': 'S', json: '{ }', code: '</>', csv: '▦' })[node.kind] || '•';
|
|
|
|
|
|
|
+ return ({ 'meeting-dashboard': '会', 'doc-dashboard': '文', 'todo-dashboard': '办', 'task-dashboard': '任', markdown: 'M', 'skill-document': 'S', json: '{ }', code: '</>', csv: '▦' })[node.kind] || '•';
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function renderKnowledgeTreeNode(node, depth = 0) {
|
|
function renderKnowledgeTreeNode(node, depth = 0) {
|
|
@@ -1784,34 +1795,6 @@
|
|
|
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- function propertyMeta(property) {
|
|
|
|
|
- return [property.district, property.layout, property.area ? `${property.area}㎡` : '', property.decoration].filter(Boolean).join(' · ');
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- function renderPropertyDetail(property) {
|
|
|
|
|
- if (!property) return '<div class="property-detail-empty"><strong>选择一套房源</strong><span>点击左侧房源卡片查看全部字段与数据来源。</span></div>';
|
|
|
|
|
- const rows = [
|
|
|
|
|
- ['区域', property.district], ['户型', property.layout], ['面积', property.area ? `${property.area}㎡` : ''],
|
|
|
|
|
- ['楼层', property.floor], ['朝向', property.orientation], ['装修', property.decoration],
|
|
|
|
|
- ['单价', property.unitPrice ? `${property.unitPrice} 元/㎡` : ''], ['学区', property.isSchoolDistrict ? property.schoolName || '是' : '否'],
|
|
|
|
|
- ['停车', property.parking], ['房龄', property.buildingAge ? `${property.buildingAge} 年` : ''],
|
|
|
|
|
- ['满五唯一', property.isFiveYearOnly ? '是' : '否'], ['议价空间', property.priceDropSpace ? `${property.priceDropSpace} 万` : ''],
|
|
|
|
|
- ].filter(([, value]) => value !== '' && value !== null && value !== undefined);
|
|
|
|
|
- return `<div class="property-detail"><header><span>${escapeHtml(property.id)}</span><h3>${escapeHtml(property.community)}</h3><strong>${property.totalPrice || '-'}<small>万</small></strong><p>${escapeHtml(propertyMeta(property))}</p></header><div class="property-detail-grid">${rows.map(([label, value]) => `<div><small>${escapeHtml(label)}</small><strong>${escapeHtml(value)}</strong></div>`).join('')}</div>${property.highlights?.length ? `<section><h4>房源亮点</h4><div class="property-tags">${property.highlights.map(tag => `<span>${escapeHtml(tag)}</span>`).join('')}</div></section>` : ''}${property.surrounding ? `<section><h4>周边描述</h4><p>${escapeHtml(property.surrounding)}</p></section>` : ''}${property.ownerSituation ? `<section><h4>业主情况</h4><p>${escapeHtml(property.ownerSituation)}</p></section>` : ''}<section><h4>匹配维度</h4><div class="property-score-list">${Object.entries(property.scores || {}).map(([key, value]) => `<div><span>${escapeHtml(({ community: '小区品质', transport: '交通', surrounding: '配套', priceAdvantage: '价格优势' })[key] || key)}</span><i><b style="width:${Math.min(100, Number(value || 0) * 10)}%"></b></i><em>${value || 0}</em></div>`).join('')}</div></section></div>`;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- function renderPropertiesView() {
|
|
|
|
|
- const data = state.knowledge.properties || { items: [], stats: {}, total: 0, page: 1, pageCount: 1 };
|
|
|
|
|
- const filters = state.knowledge.filters;
|
|
|
|
|
- const selected = state.knowledge.selectedProperty;
|
|
|
|
|
- return `<div class="property-library-view">
|
|
|
|
|
- <header class="knowledge-view-header"><div><span>PROPERTY DATASET</span><h3>房源数据列表</h3><p>${escapeHtml(data.sourceLabel || '房源数据')} · ${data.total || 0} 条当前结果</p></div><code>${escapeHtml(data.source || '')}</code></header>
|
|
|
|
|
- <div class="property-stats"><div><strong>${data.stats.total || 0}</strong><span>总房源</span></div><div><strong>${data.stats.averagePrice || 0}<small>万</small></strong><span>平均总价</span></div><div><strong>${data.stats.districts?.length || 0}</strong><span>区域</span></div><div><strong>${data.stats.schoolDistrictCount || 0}</strong><span>学区房</span></div></div>
|
|
|
|
|
- <form class="property-filterbar" id="property-filter-form"><input name="q" value="${escapeHtml(filters.q)}" placeholder="搜索小区、编号、标签" /><select name="district"><option value="">全部区域</option>${(data.stats.districts || []).map(item => `<option ${filters.district === item ? 'selected' : ''}>${escapeHtml(item)}</option>`).join('')}</select><select name="layout"><option value="">全部户型</option>${(data.stats.layouts || []).map(item => `<option ${filters.layout === item ? 'selected' : ''}>${escapeHtml(item)}</option>`).join('')}</select><select name="decoration"><option value="">全部装修</option>${(data.stats.decorations || []).map(item => `<option ${filters.decoration === item ? 'selected' : ''}>${escapeHtml(item)}</option>`).join('')}</select><input name="maxPrice" type="number" value="${escapeHtml(filters.maxPrice)}" placeholder="最高总价/万" /><button class="btn" type="submit">筛选</button></form>
|
|
|
|
|
- <div class="property-browser-layout"><div class="property-card-grid">${(data.items || []).map(property => `<button class="property-card ${selected?.id === property.id ? 'active' : ''}" data-property-id="${escapeHtml(property.id)}"><div><span>${escapeHtml(property.id)}</span><em>${property.isSchoolDistrict ? '学区' : property.decoration || '房源'}</em></div><h4>${escapeHtml(property.community)}</h4><p>${escapeHtml(propertyMeta(property))}</p><strong>${property.totalPrice || '-'}<small>万</small></strong><footer>${(property.highlights || []).slice(0, 3).map(tag => `<i>${escapeHtml(tag)}</i>`).join('')}</footer></button>`).join('') || '<div class="library-empty">没有符合条件的房源</div>'}</div><aside class="property-detail-panel">${renderPropertyDetail(selected)}</aside></div>
|
|
|
|
|
- </div>`;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
function meetingInputValue(value) {
|
|
function meetingInputValue(value) {
|
|
|
return String(value || '').replace(' ', 'T').slice(0, 16);
|
|
return String(value || '').replace(' ', 'T').slice(0, 16);
|
|
|
}
|
|
}
|
|
@@ -2029,9 +2012,7 @@
|
|
|
function renderKnowledgeWorkspace(page) {
|
|
function renderKnowledgeWorkspace(page) {
|
|
|
const tree = state.knowledge.tree || { roots: [], summary: {} };
|
|
const tree = state.knowledge.tree || { roots: [], summary: {} };
|
|
|
const selectedNode = flattenKnowledgeNodes(tree.roots).find(item => item.id === state.knowledge.selectedNodeId);
|
|
const selectedNode = flattenKnowledgeNodes(tree.roots).find(item => item.id === state.knowledge.selectedNodeId);
|
|
|
- const viewer = selectedNode?.kind === 'property-dataset'
|
|
|
|
|
- ? renderPropertiesView()
|
|
|
|
|
- : selectedNode?.kind === 'meeting-dashboard'
|
|
|
|
|
|
|
+ const viewer = selectedNode?.kind === 'meeting-dashboard'
|
|
|
? renderMeetingKnowledgeView()
|
|
? renderMeetingKnowledgeView()
|
|
|
: selectedNode?.kind === 'doc-dashboard'
|
|
: selectedNode?.kind === 'doc-dashboard'
|
|
|
? renderDocumentKnowledgeView()
|
|
? renderDocumentKnowledgeView()
|
|
@@ -2041,22 +2022,11 @@
|
|
|
? renderUnifiedTaskCenter()
|
|
? renderUnifiedTaskCenter()
|
|
|
: renderKnowledgeFileView(state.knowledge.file);
|
|
: renderKnowledgeFileView(state.knowledge.file);
|
|
|
page.innerHTML = `
|
|
page.innerHTML = `
|
|
|
- <section class="workspace-hero knowledge-hero"><div><span>LOCAL KNOWLEDGE & FILE LIBRARY</span><h2>知识库</h2><p>用文件夹方式统一管理规则、技能说明、房源数据和运行输出,点击文件即可可视化查看。</p></div><div class="workspace-stat-row"><div><strong>${tree.summary.libraryCount || 0}</strong><span>目录来源</span></div><div><strong>${tree.summary.fileCount || 0}</strong><span>可预览文件</span></div><div><strong>${tree.summary.propertyCount || 0}</strong><span>房源记录</span></div></div></section>
|
|
|
|
|
|
|
+ <section class="workspace-hero knowledge-hero"><div><span>LOCAL KNOWLEDGE & FILE LIBRARY</span><h2>知识库</h2><p>用文件夹方式统一管理规则、技能说明和运行输出,点击文件即可可视化查看。</p></div><div class="workspace-stat-row"><div><strong>${tree.summary.libraryCount || 0}</strong><span>目录来源</span></div><div><strong>${tree.summary.fileCount || 0}</strong><span>可预览文件</span></div><div><strong>${tree.summary.skillCount || 0}</strong><span>通用 Skills</span></div></div></section>
|
|
|
<section class="knowledge-layout"><aside class="knowledge-tree-panel"><header><strong>知识库目录</strong><span>本地文件 · 只读预览</span></header><div class="knowledge-tree">${(tree.roots || []).map(root => renderKnowledgeTreeNode(root)).join('')}</div></aside><main class="knowledge-viewer">${viewer}</main></section>
|
|
<section class="knowledge-layout"><aside class="knowledge-tree-panel"><header><strong>知识库目录</strong><span>本地文件 · 只读预览</span></header><div class="knowledge-tree">${(tree.roots || []).map(root => renderKnowledgeTreeNode(root)).join('')}</div></aside><main class="knowledge-viewer">${viewer}</main></section>
|
|
|
`;
|
|
`;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- async function loadKnowledgeProperties(page) {
|
|
|
|
|
- const params = new URLSearchParams({ pageSize: '30' });
|
|
|
|
|
- for (const [key, value] of Object.entries(state.knowledge.filters)) if (String(value || '').trim()) params.set(key, value);
|
|
|
|
|
- const result = await api('GET', `/api/knowledge/properties?${params}`);
|
|
|
|
|
- state.knowledge.properties = result.data;
|
|
|
|
|
- if (state.knowledge.selectedProperty) {
|
|
|
|
|
- state.knowledge.selectedProperty = result.data.items.find(item => item.id === state.knowledge.selectedProperty.id) || null;
|
|
|
|
|
- }
|
|
|
|
|
- renderKnowledgeWorkspace(page);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
async function loadMeetingKnowledgeHub(page) {
|
|
async function loadMeetingKnowledgeHub(page) {
|
|
|
const result = await api('GET', '/api/knowledge/meetings');
|
|
const result = await api('GET', '/api/knowledge/meetings');
|
|
|
state.knowledge.meetings.hub = result;
|
|
state.knowledge.meetings.hub = result;
|
|
@@ -2094,12 +2064,10 @@
|
|
|
if (!node || node.type !== 'file') return;
|
|
if (!node || node.type !== 'file') return;
|
|
|
state.knowledge.selectedNodeId = nodeId;
|
|
state.knowledge.selectedNodeId = nodeId;
|
|
|
state.knowledge.file = null;
|
|
state.knowledge.file = null;
|
|
|
- state.knowledge.selectedProperty = null;
|
|
|
|
|
if (node.kind === 'meeting-dashboard') await loadMeetingKnowledgeHub(page);
|
|
if (node.kind === 'meeting-dashboard') await loadMeetingKnowledgeHub(page);
|
|
|
else if (node.kind === 'doc-dashboard') await loadDocumentKnowledgeHub(page);
|
|
else if (node.kind === 'doc-dashboard') await loadDocumentKnowledgeHub(page);
|
|
|
else if (node.kind === 'todo-dashboard') await loadTodoKnowledgeHub(page);
|
|
else if (node.kind === 'todo-dashboard') await loadTodoKnowledgeHub(page);
|
|
|
else if (node.kind === 'task-dashboard') await loadUnifiedTaskHub(page);
|
|
else if (node.kind === 'task-dashboard') await loadUnifiedTaskHub(page);
|
|
|
- else if (node.kind === 'property-dataset') await loadKnowledgeProperties(page);
|
|
|
|
|
else {
|
|
else {
|
|
|
const result = await api('GET', `/api/knowledge/file?id=${encodeURIComponent(nodeId)}`);
|
|
const result = await api('GET', `/api/knowledge/file?id=${encodeURIComponent(nodeId)}`);
|
|
|
state.knowledge.file = result.data;
|
|
state.knowledge.file = result.data;
|
|
@@ -2245,14 +2213,6 @@
|
|
|
finally { state.knowledge.tasks.loading = false; renderKnowledgeWorkspace(page); }
|
|
finally { state.knowledge.tasks.loading = false; renderKnowledgeWorkspace(page); }
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
- const propertyButton = event.target.closest('[data-property-id]');
|
|
|
|
|
- if (propertyButton) {
|
|
|
|
|
- try {
|
|
|
|
|
- const result = await api('GET', `/api/knowledge/properties/${encodeURIComponent(propertyButton.dataset.propertyId)}`);
|
|
|
|
|
- state.knowledge.selectedProperty = result.data.property;
|
|
|
|
|
- renderKnowledgeWorkspace(page);
|
|
|
|
|
- } catch (error) { toast(error.message || '房源读取失败', 'error'); }
|
|
|
|
|
- }
|
|
|
|
|
});
|
|
});
|
|
|
page.addEventListener('submit', async event => {
|
|
page.addEventListener('submit', async event => {
|
|
|
const diagnosisFeedbackForm = event.target.closest('[data-diagnosis-feedback-form]');
|
|
const diagnosisFeedbackForm = event.target.closest('[data-diagnosis-feedback-form]');
|
|
@@ -2380,13 +2340,6 @@
|
|
|
}
|
|
}
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
- if (event.target.id === 'property-filter-form') {
|
|
|
|
|
- event.preventDefault();
|
|
|
|
|
- const form = new FormData(event.target);
|
|
|
|
|
- for (const key of Object.keys(state.knowledge.filters)) state.knowledge.filters[key] = String(form.get(key) || '').trim();
|
|
|
|
|
- try { await loadKnowledgeProperties(page); }
|
|
|
|
|
- catch (error) { toast(error.message || '房源筛选失败', 'error'); }
|
|
|
|
|
- }
|
|
|
|
|
});
|
|
});
|
|
|
page.addEventListener('toggle', event => {
|
|
page.addEventListener('toggle', event => {
|
|
|
const folder = event.target.closest?.('[data-knowledge-folder]');
|
|
const folder = event.target.closest?.('[data-knowledge-folder]');
|
|
@@ -2399,14 +2352,14 @@
|
|
|
state.knowledge.tree = result.data;
|
|
state.knowledge.tree = result.data;
|
|
|
if (!state.knowledge.foldersInitialized) {
|
|
if (!state.knowledge.foldersInitialized) {
|
|
|
for (const root of result.data.roots) {
|
|
for (const root of result.data.roots) {
|
|
|
- if (['agent-knowledge', 'property-data', 'meeting-knowledge', 'doc-knowledge', 'todo-knowledge', 'task-knowledge'].includes(root.libraryId)) {
|
|
|
|
|
|
|
+ if (['agent-knowledge', 'meeting-knowledge', 'doc-knowledge', 'todo-knowledge', 'task-knowledge'].includes(root.libraryId)) {
|
|
|
state.knowledge.expandedFolderIds.add(root.id);
|
|
state.knowledge.expandedFolderIds.add(root.id);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
state.knowledge.foldersInitialized = true;
|
|
state.knowledge.foldersInitialized = true;
|
|
|
}
|
|
}
|
|
|
const nodes = flattenKnowledgeNodes(result.data.roots);
|
|
const nodes = flattenKnowledgeNodes(result.data.roots);
|
|
|
- const defaultNode = nodes.find(item => item.kind === 'task-dashboard') || nodes.find(item => item.kind === 'doc-dashboard') || nodes.find(item => item.kind === 'todo-dashboard') || nodes.find(item => item.kind === 'meeting-dashboard') || nodes.find(item => item.kind === 'property-dataset') || nodes.find(item => item.type === 'file');
|
|
|
|
|
|
|
+ const defaultNode = nodes.find(item => item.kind === 'task-dashboard') || nodes.find(item => item.kind === 'doc-dashboard') || nodes.find(item => item.kind === 'todo-dashboard') || nodes.find(item => item.kind === 'meeting-dashboard') || nodes.find(item => item.type === 'file');
|
|
|
if (defaultNode) await openKnowledgeNode(page, state.knowledge.selectedNodeId || defaultNode.id);
|
|
if (defaultNode) await openKnowledgeNode(page, state.knowledge.selectedNodeId || defaultNode.id);
|
|
|
else renderKnowledgeWorkspace(page);
|
|
else renderKnowledgeWorkspace(page);
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
@@ -2421,6 +2374,26 @@
|
|
|
return date.toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' });
|
|
return date.toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' });
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ function agentTimestamp(value) {
|
|
|
|
|
+ const timestamp = Date.parse(value || '');
|
|
|
|
|
+ return Number.isFinite(timestamp) ? timestamp : Number.NEGATIVE_INFINITY;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function sortAgentItemsByLatest(items = [], value = item => item?.lastMessageAt) {
|
|
|
|
|
+ return items
|
|
|
|
|
+ .map((item, index) => ({ item, index, timestamp: agentTimestamp(value(item)) }))
|
|
|
|
|
+ .sort((a, b) => b.timestamp - a.timestamp || a.index - b.index)
|
|
|
|
|
+ .map(entry => entry.item);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function latestAgentMessageSource(sources = []) {
|
|
|
|
|
+ return sources.reduce((latest, source, index) => {
|
|
|
|
|
+ const timestamp = agentTimestamp(source?.time);
|
|
|
|
|
+ if (!latest || timestamp > latest.timestamp) return { ...source, timestamp, index };
|
|
|
|
|
+ return latest;
|
|
|
|
|
+ }, null);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
function agentTaskStatusLabel(status) {
|
|
function agentTaskStatusLabel(status) {
|
|
|
return ({ open: '待处理', in_progress: '处理中', done: '已完成', dismissed: '已忽略' })[status] || status || '待处理';
|
|
return ({ open: '待处理', in_progress: '处理中', done: '已完成', dismissed: '已忽略' })[status] || status || '待处理';
|
|
|
}
|
|
}
|
|
@@ -2451,20 +2424,16 @@
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function demandItems(demand = {}) {
|
|
function demandItems(demand = {}) {
|
|
|
- const districts = Array.isArray(demand.districts) ? demand.districts.join('/') : (demand.preferredRegion || demand.region || demand.intent_area || '');
|
|
|
|
|
const min = demand.budgetMin ?? demand.budget_min;
|
|
const min = demand.budgetMin ?? demand.budget_min;
|
|
|
const max = demand.budgetMax ?? demand.budget_max;
|
|
const max = demand.budgetMax ?? demand.budget_max;
|
|
|
- const rooms = demand.rooms || (demand.house_type ? String(demand.house_type).replace(/[^0-9]/g, '') : '');
|
|
|
|
|
const values = [
|
|
const values = [
|
|
|
- ['区域', districts],
|
|
|
|
|
- ['预算', demand.budgetWan ? `${demand.budgetWan}万${demand.budgetType && demand.budgetType !== '待确认' ? `(${demand.budgetType})` : ''}` : (min && max ? `${min}-${max}万` : (max ? `${max}万内` : ''))],
|
|
|
|
|
- ['户型', demand.layout || (rooms ? `${rooms}室` : (demand.house_type || ''))],
|
|
|
|
|
- ['面积', demand.areaMin && demand.areaMax ? `${demand.areaMin}-${demand.areaMax}㎡` : ''],
|
|
|
|
|
- ['装修', demand.decoration],
|
|
|
|
|
- ['用途', demand.purpose || demand.buyerType || demand.purchase_purpose],
|
|
|
|
|
- ['偏好', [demand.floor, demand.orientation].filter(Boolean).join(' / ')],
|
|
|
|
|
- ['学区', demand.schoolRequired ? '需要' : ''],
|
|
|
|
|
- ['时间', demand.timeline],
|
|
|
|
|
|
|
+ ['客户意图', demand.intent || demand.purpose],
|
|
|
|
|
+ ['核心需求', demand.need || demand.needs || demand.keyConcerns],
|
|
|
|
|
+ ['预算', demand.budgetWan ? `${demand.budgetWan}${demand.budgetType && demand.budgetType !== '待确认' ? `(${demand.budgetType})` : ''}` : (min && max ? `${min}-${max}` : (max ? `${max}内` : ''))],
|
|
|
|
|
+ ['计划时间', demand.timeline],
|
|
|
|
|
+ ['决策人', demand.decisionMaker],
|
|
|
|
|
+ ['沟通偏好', demand.contactPreference],
|
|
|
|
|
+ ['服务阶段', demand.serviceStage || demand.negotiationStage],
|
|
|
['紧迫度', demand.urgency],
|
|
['紧迫度', demand.urgency],
|
|
|
];
|
|
];
|
|
|
return values.filter(([, value]) => value !== undefined && value !== null && String(value).trim());
|
|
return values.filter(([, value]) => value !== undefined && value !== null && String(value).trim());
|
|
@@ -2494,7 +2463,7 @@
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function agentModeLabel(mode) {
|
|
function agentModeLabel(mode) {
|
|
|
- return ({ review: '待审核', auto: '高置信自动', human: '人工接管', paused: '会话暂停' })[mode] || mode || '待审核';
|
|
|
|
|
|
|
+ return ({ review: '待审核', auto: '高置信自动', autopilot: '全自动接管', human: '人工接管', paused: '会话暂停' })[mode] || mode || '待审核';
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function formatReplyWaiting(minutes) {
|
|
function formatReplyWaiting(minutes) {
|
|
@@ -2553,14 +2522,19 @@
|
|
|
const id = String(conversation.channelId || conversation.id);
|
|
const id = String(conversation.channelId || conversation.id);
|
|
|
const latest = conversation.messages?.[conversation.messages.length - 1] || null;
|
|
const latest = conversation.messages?.[conversation.messages.length - 1] || null;
|
|
|
const current = groups.get(id) || {};
|
|
const current = groups.get(id) || {};
|
|
|
|
|
+ const latestSource = latestAgentMessageSource([
|
|
|
|
|
+ { time: current.lastMessageAt, preview: current.lastMessagePreview, senderName: current.lastSenderName },
|
|
|
|
|
+ { time: conversation.lastMessageAt || latest?.timestamp, preview: latest?.content, senderName: latest?.senderName },
|
|
|
|
|
+ ]);
|
|
|
groups.set(id, {
|
|
groups.set(id, {
|
|
|
...current,
|
|
...current,
|
|
|
id,
|
|
id,
|
|
|
name: current.name || conversation.displayName,
|
|
name: current.name || conversation.displayName,
|
|
|
conversationId: conversation.id,
|
|
conversationId: conversation.id,
|
|
|
messages: conversation.messages || [],
|
|
messages: conversation.messages || [],
|
|
|
- lastMessageAt: current.lastMessageAt || conversation.lastMessageAt,
|
|
|
|
|
- lastMessagePreview: current.lastMessagePreview || latest?.content || '',
|
|
|
|
|
|
|
+ lastMessageAt: latestSource?.timestamp > Number.NEGATIVE_INFINITY ? latestSource.time : null,
|
|
|
|
|
+ lastMessagePreview: latestSource?.preview || '',
|
|
|
|
|
+ lastSenderName: latestSource?.senderName || '',
|
|
|
status: current.status || 'monitoring',
|
|
status: current.status || 'monitoring',
|
|
|
channelType: 'group',
|
|
channelType: 'group',
|
|
|
channelLabel: '客户群聊',
|
|
channelLabel: '客户群聊',
|
|
@@ -2571,14 +2545,18 @@
|
|
|
if (!id) continue;
|
|
if (!id) continue;
|
|
|
const latest = groupAgent.messages?.[groupAgent.messages.length - 1] || null;
|
|
const latest = groupAgent.messages?.[groupAgent.messages.length - 1] || null;
|
|
|
const current = groups.get(id) || {};
|
|
const current = groups.get(id) || {};
|
|
|
|
|
+ const latestSource = latestAgentMessageSource([
|
|
|
|
|
+ { time: current.lastMessageAt, preview: current.lastMessagePreview, senderName: current.lastSenderName },
|
|
|
|
|
+ { time: latest?.timestamp, preview: latest?.content, senderName: latest?.senderName },
|
|
|
|
|
+ ]);
|
|
|
groups.set(id, {
|
|
groups.set(id, {
|
|
|
...current,
|
|
...current,
|
|
|
id,
|
|
id,
|
|
|
name: current.name || groupAgent.roomName || id,
|
|
name: current.name || groupAgent.roomName || id,
|
|
|
messages: groupAgent.messages || [],
|
|
messages: groupAgent.messages || [],
|
|
|
- lastMessageAt: current.lastMessageAt || latest?.timestamp || null,
|
|
|
|
|
- lastMessagePreview: current.lastMessagePreview || latest?.content || '',
|
|
|
|
|
- lastSenderName: current.lastSenderName || latest?.senderName || '',
|
|
|
|
|
|
|
+ lastMessageAt: latestSource?.timestamp > Number.NEGATIVE_INFINITY ? latestSource.time : null,
|
|
|
|
|
+ lastMessagePreview: latestSource?.preview || '',
|
|
|
|
|
+ lastSenderName: latestSource?.senderName || '',
|
|
|
pendingReply: groupAgent.pendingReply || null,
|
|
pendingReply: groupAgent.pendingReply || null,
|
|
|
mode: groupAgent.mode === 'auto' ? 'auto' : 'review',
|
|
mode: groupAgent.mode === 'auto' ? 'auto' : 'review',
|
|
|
agentError: groupAgent.agentError || null,
|
|
agentError: groupAgent.agentError || null,
|
|
@@ -2590,7 +2568,7 @@
|
|
|
channelLabel: '客户群聊',
|
|
channelLabel: '客户群聊',
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
- return [...groups.values()].sort((a, b) => Date.parse(b.lastMessageAt || 0) - Date.parse(a.lastMessageAt || 0));
|
|
|
|
|
|
|
+ return sortAgentItemsByLatest([...groups.values()]);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function agentMonitorStatusLabel(status) {
|
|
function agentMonitorStatusLabel(status) {
|
|
@@ -2681,6 +2659,72 @@
|
|
|
})[action] || action;
|
|
})[action] || action;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ function agentTraceText(value, maxLength = 96) {
|
|
|
|
|
+ const text = String(value ?? '')
|
|
|
|
|
+ .replace(/\b1\d{10}\b/g, match => `${match.slice(0, 3)}****${match.slice(-4)}`)
|
|
|
|
|
+ .replace(/([?&](?:token|key|secret|password)=)[^&\s]+/gi, '$1[已脱敏]');
|
|
|
|
|
+ return text.length > maxLength ? `${text.slice(0, maxLength)}...` : text;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function agentTraceSummary(value, key = '', depth = 0) {
|
|
|
|
|
+ if (/token|secret|password|authorization|cookie|masterkey|session/i.test(key)) return '[已脱敏]';
|
|
|
|
|
+ if (value === null || value === undefined || value === '') return '';
|
|
|
|
|
+ if (Array.isArray(value)) return `${value.length} 项`;
|
|
|
|
|
+ if (typeof value === 'object') {
|
|
|
|
|
+ if (depth >= 1) return `${Object.keys(value).length} 个字段`;
|
|
|
|
|
+ return Object.fromEntries(Object.entries(value).slice(0, 6).map(([childKey, childValue]) => [
|
|
|
|
|
+ childKey,
|
|
|
|
|
+ agentTraceSummary(childValue, childKey, depth + 1),
|
|
|
|
|
+ ]));
|
|
|
|
|
+ }
|
|
|
|
|
+ return agentTraceText(value);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function agentToolStatus(call = {}) {
|
|
|
|
|
+ const result = call.result || {};
|
|
|
|
|
+ if (call.error || result.error || result.ok === false || result.success === false) return 'failed';
|
|
|
|
|
+ if (call.status === 'running') return 'running';
|
|
|
|
|
+ return 'completed';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function renderAgentActivityTimeline(viewModel = {}) {
|
|
|
|
|
+ const traces = Array.isArray(viewModel.toolTrace) ? viewModel.toolTrace : [];
|
|
|
|
|
+ const audit = Array.isArray(viewModel.audit) ? viewModel.audit : [];
|
|
|
|
|
+ const planAudit = audit.filter(item => ['message_received', 'manual_agent_run_requested', 'conversation_history_synced'].includes(item.action)).slice(-2);
|
|
|
|
|
+ const evaluateAudit = audit.filter(item => ['draft_created', 'agent_failed', 'agent_no_reply_needed', 'message_send_failed', 'draft_approved', 'message_sent'].includes(item.action)).slice(-3);
|
|
|
|
|
+ const phase = (code, label, items, empty) => `
|
|
|
|
|
+ <section class="agent-activity-phase ${code}">
|
|
|
|
|
+ <header><i>${code.slice(0, 1).toUpperCase()}</i><div><strong>${label}</strong><small>${items.length} 条证据</small></div></header>
|
|
|
|
|
+ <div class="agent-activity-items">${items.length ? items.join('') : `<span class="agent-activity-empty">${empty}</span>`}</div>
|
|
|
|
|
+ </section>`;
|
|
|
|
|
+ const auditCard = item => `<article class="agent-activity-item"><div><strong>${escapeHtml(agentAuditLabel(item.action))}</strong><em>${escapeHtml(formatAgentTime(item.created_at || item.createdAt || item.at || item.timestamp))}</em></div><small>${escapeHtml(agentTraceText(item.detail || item.message || item.reason || '已记录到会话审计'))}</small></article>`;
|
|
|
|
|
+ const actItems = traces.map(call => {
|
|
|
|
|
+ const status = agentToolStatus(call);
|
|
|
|
|
+ const args = agentTraceSummary(call.args || call.params || {});
|
|
|
|
|
+ const argsText = typeof args === 'object'
|
|
|
|
|
+ ? Object.entries(args).map(([key, value]) => `${key}: ${typeof value === 'object' ? '结构化参数' : value}`).join(' · ')
|
|
|
|
|
+ : args;
|
|
|
|
|
+ return `<article class="agent-activity-item tool ${status}"><div><strong>${escapeHtml(call.tool || call.name || 'Agent 工具')}</strong><em>${status === 'failed' ? '失败' : status === 'running' ? '执行中' : '已执行'}</em></div><small>${escapeHtml(argsText || '无公开参数')}</small></article>`;
|
|
|
|
|
+ });
|
|
|
|
|
+ const observeItems = traces.map(call => {
|
|
|
|
|
+ const result = call.result || {};
|
|
|
|
|
+ const status = agentToolStatus(call);
|
|
|
|
|
+ const duration = Number(call.durationMs ?? result.durationMs ?? call.elapsedMs ?? result.elapsedMs);
|
|
|
|
|
+ const retries = Number(call.retryCount ?? result.retryCount ?? call.retries ?? result.retries);
|
|
|
|
|
+ const resultSummary = agentTraceSummary(call.error || result.error || result.message || result.summary || result);
|
|
|
|
|
+ const summaryText = typeof resultSummary === 'object'
|
|
|
|
|
+ ? Object.entries(resultSummary).map(([key, value]) => `${key}: ${typeof value === 'object' ? '结构化结果' : value}`).join(' · ')
|
|
|
|
|
+ : resultSummary;
|
|
|
|
|
+ return `<article class="agent-activity-item observe ${status}"><div><strong>${escapeHtml(call.tool || call.name || 'Agent 工具')} 结果</strong><em>${Number.isFinite(duration) ? `${Math.max(0, Math.round(duration))} ms` : status === 'failed' ? '失败' : '完成'}${Number.isFinite(retries) && retries > 0 ? ` · 重试 ${retries}` : ''}</em></div><small>${escapeHtml(summaryText || (status === 'failed' ? '执行失败,详情已进入审计' : '工具返回成功'))}</small></article>`;
|
|
|
|
|
+ });
|
|
|
|
|
+ return `<div class="agent-activity-timeline">
|
|
|
|
|
+ ${phase('plan', 'Plan · 任务计划', planAudit.map(auditCard), '等待真实消息或人工运行请求')}
|
|
|
|
|
+ ${phase('act', 'Act · 工具执行', actItems, '本轮没有工具调用')}
|
|
|
|
|
+ ${phase('observe', 'Observe · 结果观察', observeItems, '暂无工具结果')}
|
|
|
|
|
+ ${phase('evaluate', 'Evaluate · 评估决策', evaluateAudit.map(auditCard), '等待 Agent 形成处理结论')}
|
|
|
|
|
+ </div>`;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
function scrollAgentMessagesToLatest(page) {
|
|
function scrollAgentMessagesToLatest(page) {
|
|
|
requestAnimationFrame(() => {
|
|
requestAnimationFrame(() => {
|
|
|
const stream = page.querySelector('.agent-message-stream');
|
|
const stream = page.querySelector('.agent-message-stream');
|
|
@@ -2954,7 +2998,6 @@
|
|
|
const customerAlerts = customerIntelligence.alerts || [];
|
|
const customerAlerts = customerIntelligence.alerts || [];
|
|
|
const intelligenceSummary = customerIntelligence.summary || {};
|
|
const intelligenceSummary = customerIntelligence.summary || {};
|
|
|
const demands = demandItems(analysis.demand || {});
|
|
const demands = demandItems(analysis.demand || {});
|
|
|
- const matches = analysis.matches || [];
|
|
|
|
|
const pendingText = selected?.pendingReply?.content || '';
|
|
const pendingText = selected?.pendingReply?.content || '';
|
|
|
const isHistorical = selected?.source === 'verified-history';
|
|
const isHistorical = selected?.source === 'verified-history';
|
|
|
const manualActive = Boolean(!aiActive || selected?.mode === 'manual');
|
|
const manualActive = Boolean(!aiActive || selected?.mode === 'manual');
|
|
@@ -3014,7 +3057,7 @@
|
|
|
<textarea id="agent-reply-editor" placeholder="${manualActive ? '输入人工回复内容…' : 'AI 自动回复运行中…'}">${escapeHtml(pendingText)}</textarea>
|
|
<textarea id="agent-reply-editor" placeholder="${manualActive ? '输入人工回复内容…' : 'AI 自动回复运行中…'}">${escapeHtml(pendingText)}</textarea>
|
|
|
<div class="agent-composer-actions"><span>${manualActive ? '也可以直接在企业微信中人工回复' : '新消息将由 AI 自动回复'}</span><button class="btn" data-agent-action="approve" data-id="${selected.id}" ${pendingText || manualActive ? '' : 'disabled'}>${manualActive ? '人工发送' : '发送建议'}</button></div>
|
|
<div class="agent-composer-actions"><span>${manualActive ? '也可以直接在企业微信中人工回复' : '新消息将由 AI 自动回复'}</span><button class="btn" data-agent-action="approve" data-id="${selected.id}" ${pendingText || manualActive ? '' : 'disabled'}>${manualActive ? '人工发送' : '发送建议'}</button></div>
|
|
|
</div>
|
|
</div>
|
|
|
- ` : `<div class="agent-empty large"><strong>智能会话准备就绪</strong><span>启动监听,然后从白名单企微发送:“我想在新北区买婚房,预算150万,三室,最好精装”</span></div>`}
|
|
|
|
|
|
|
+ ` : `<div class="agent-empty large"><strong>智能会话准备就绪</strong><span>启动监听,然后从白名单企微发送一条真实业务咨询。</span></div>`}
|
|
|
</main>
|
|
</main>
|
|
|
|
|
|
|
|
<aside class="agent-insight-panel">
|
|
<aside class="agent-insight-panel">
|
|
@@ -3028,12 +3071,8 @@
|
|
|
<div class="agent-section-title"><span>02</span><div><strong>需求画像</strong><small>从多轮会话持续归集</small></div></div>
|
|
<div class="agent-section-title"><span>02</span><div><strong>需求画像</strong><small>从多轮会话持续归集</small></div></div>
|
|
|
<div class="agent-demand-grid">${demands.length ? demands.map(([label, value]) => `<div><small>${escapeHtml(label)}</small><strong>${escapeHtml(value)}</strong></div>`).join('') : '<p class="agent-muted">暂无明确需求</p>'}</div>
|
|
<div class="agent-demand-grid">${demands.length ? demands.map(([label, value]) => `<div><small>${escapeHtml(label)}</small><strong>${escapeHtml(value)}</strong></div>`).join('') : '<p class="agent-muted">暂无明确需求</p>'}</div>
|
|
|
</div>
|
|
</div>
|
|
|
- <div class="agent-insight-section">
|
|
|
|
|
- <div class="agent-section-title"><span>03</span><div><strong>匹配建议</strong><small>本地房源数据与评分模型</small></div></div>
|
|
|
|
|
- <div class="agent-match-list">${matches.length ? matches.map((match, index) => `<div class="agent-match-card"><span>${index + 1}</span><div><strong>${escapeHtml(match.community || match.title || '匹配结果')}</strong><small>${escapeHtml([match.price ? `${match.price}万` : '', match.layout, match.area ? `${match.area}㎡` : ''].filter(Boolean).join(' · '))}</small><em>${escapeHtml((match.highlights || []).join(' / ') || match.level || '')}</em></div>${match.score !== null && match.score !== undefined ? `<b>${match.score}</b>` : ''}</div>`).join('') : '<p class="agent-muted">当前消息未触发房源匹配</p>'}</div>
|
|
|
|
|
- </div>
|
|
|
|
|
<div class="agent-insight-section knowledge">
|
|
<div class="agent-insight-section knowledge">
|
|
|
- <div class="agent-section-title"><span>04</span><div><strong>依据与边界</strong><small>让建议可解释、可追溯</small></div></div>
|
|
|
|
|
|
|
+ <div class="agent-section-title"><span>03</span><div><strong>依据与边界</strong><small>让建议可解释、可追溯</small></div></div>
|
|
|
<ul>${(analysis.knowledgeSources || []).map(source => `<li>${escapeHtml(source)}</li>`).join('')}</ul>
|
|
<ul>${(analysis.knowledgeSources || []).map(source => `<li>${escapeHtml(source)}</li>`).join('')}</ul>
|
|
|
</div>
|
|
</div>
|
|
|
` : '<div class="agent-empty"><strong>暂无分析</strong><span>选择会话后显示 Agent 工作过程。</span></div>'}
|
|
` : '<div class="agent-empty"><strong>暂无分析</strong><span>选择会话后显示 Agent 工作过程。</span></div>'}
|
|
@@ -3219,6 +3258,35 @@
|
|
|
})[error.code] || { title: 'Claude Code 暂时不可用', next: '请稍后重试;详细原因已记录在审计日志中。' };
|
|
})[error.code] || { title: 'Claude Code 暂时不可用', next: '请稍后重试;详细原因已记录在审计日志中。' };
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ function renderAgentIntakeSettings() {
|
|
|
|
|
+ const policy = state.agent.intakePolicy || {};
|
|
|
|
|
+ const mode = policy.mode || 'allowlist_only';
|
|
|
|
|
+ const sendMode = policy.welcomeSendMode || 'draft';
|
|
|
|
|
+ return `
|
|
|
|
|
+ <form class="agent-intake-form" data-agent-intake-form>
|
|
|
|
|
+ <div class="agent-drawer-section-heading"><strong>个人消息接入</strong><span>未知群聊始终不会自动纳入</span></div>
|
|
|
|
|
+ ${state.agent.intakePolicyError ? `<div class="agent-drawer-error">${escapeHtml(state.agent.intakePolicyError)}</div>` : ''}
|
|
|
|
|
+ <label class="agent-field"><span>接入策略</span><select name="mode">
|
|
|
|
|
+ <option value="allowlist_only" ${mode === 'allowlist_only' ? 'selected' : ''}>仅白名单(默认)</option>
|
|
|
|
|
+ <option value="auto_enroll_review" ${mode === 'auto_enroll_review' ? 'selected' : ''}>新私聊自动纳入 · 强制待审核</option>
|
|
|
|
|
+ <option value="auto_enroll_autopilot" ${mode === 'auto_enroll_autopilot' ? 'selected' : ''}>新私聊自动纳入 · 全自动接管</option>
|
|
|
|
|
+ </select></label>
|
|
|
|
|
+ <label class="agent-toggle-field"><input type="checkbox" name="welcomeEnabled" ${policy.welcomeEnabled ? 'checked' : ''}><span><strong>启用欢迎语</strong><small>默认只生成草稿,避免未经审核外发</small></span></label>
|
|
|
|
|
+ <label class="agent-field"><span>欢迎语</span><textarea name="welcomeText" rows="4" placeholder="输入首次联系时的欢迎内容">${escapeHtml(policy.welcomeText || '')}</textarea></label>
|
|
|
|
|
+ <label class="agent-field"><span>欢迎语处理</span><select name="welcomeSendMode">
|
|
|
|
|
+ <option value="draft" ${sendMode === 'draft' ? 'selected' : ''}>生成待审草稿</option>
|
|
|
|
|
+ <option value="send" ${sendMode === 'send' ? 'selected' : ''}>直接发送(仅全自动接管策略)</option>
|
|
|
|
|
+ </select></label>
|
|
|
|
|
+ <div class="agent-policy-warning ${mode === 'auto_enroll_autopilot' ? 'danger' : ''}">${mode === 'auto_enroll_autopilot' ? '全自动接入会把新私聊加入当前账号白名单,欢迎语和后续非空 Agent 回复都可能直接发送。保存时必须再次确认。' : '通用默认仅处理人工选择的白名单联系人;待审核模式不会自动外发。'}</div>
|
|
|
|
|
+ <button class="btn" type="submit" ${state.agent.intakePolicyLoading ? 'disabled' : ''}>${state.agent.intakePolicyLoading ? '正在保存…' : '保存接入设置'}</button>
|
|
|
|
|
+ </form>`;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function agentReminderForConversation(conversationId, monitor = {}) {
|
|
|
|
|
+ const id = String(conversationId || '');
|
|
|
|
|
+ return (monitor.reminders || []).find(item => String(item.conversationId || item.id || '') === id) || null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
function renderAgentWorkspaceV2(page) {
|
|
function renderAgentWorkspaceV2(page) {
|
|
|
const insightScroll = captureAgentInsightScroll(page);
|
|
const insightScroll = captureAgentInsightScroll(page);
|
|
|
const workspaceScroll = captureAgentWorkspaceScroll(page);
|
|
const workspaceScroll = captureAgentWorkspaceScroll(page);
|
|
@@ -3240,9 +3308,14 @@
|
|
|
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 privateConversations = conversations.filter(item => item.channelType !== 'group');
|
|
|
|
|
|
|
+ const monitor = state.agent.responseMonitor || {};
|
|
|
|
|
+ const monitorSummary = monitor.summary || {};
|
|
|
|
|
+ const privateConversations = sortAgentItemsByLatest(conversations.filter(item => item.channelType !== 'group'));
|
|
|
const groupAgents = [];
|
|
const groupAgents = [];
|
|
|
const groupConversations = [];
|
|
const groupConversations = [];
|
|
|
|
|
+ const pendingReplyCount = privateConversations.filter(item => item.pendingReply).length;
|
|
|
|
|
+ const reminderCount = (monitor.reminders || []).length;
|
|
|
|
|
+ const overdueCount = Number(monitorSummary.overdue) || (monitor.reminders || []).filter(item => item.severity === 'urgent').length;
|
|
|
const listMode = 'private';
|
|
const listMode = 'private';
|
|
|
const selected = privateConversations.find(item => item.id === state.agent.selectedId) || privateConversations[0] || null;
|
|
const selected = privateConversations.find(item => item.id === state.agent.selectedId) || privateConversations[0] || null;
|
|
|
const selectedGroup = null;
|
|
const selectedGroup = null;
|
|
@@ -3260,11 +3333,11 @@
|
|
|
const customerAlerts = customerIntelligence.alerts || [];
|
|
const customerAlerts = customerIntelligence.alerts || [];
|
|
|
const intelligenceSummary = customerIntelligence.summary || {};
|
|
const intelligenceSummary = customerIntelligence.summary || {};
|
|
|
const demands = demandItems(analysis.demand || {});
|
|
const demands = demandItems(analysis.demand || {});
|
|
|
- const matches = analysis.matches || [];
|
|
|
|
|
const pending = selected?.pendingReply || null;
|
|
const pending = selected?.pendingReply || null;
|
|
|
const noReplyNotice = selected?.agentNotice || null;
|
|
const noReplyNotice = selected?.agentNotice || null;
|
|
|
const activeDraft = pending;
|
|
const activeDraft = pending;
|
|
|
const manualActive = selected?.mode === 'human';
|
|
const manualActive = selected?.mode === 'human';
|
|
|
|
|
+ const autopilotActive = selected?.mode === 'autopilot';
|
|
|
const conversationPaused = selected?.mode === 'paused';
|
|
const conversationPaused = selected?.mode === 'paused';
|
|
|
const replyEditKey = selected ? `${selected.id}:${pending?.id || (manualActive ? 'manual' : 'composer')}` : '';
|
|
const replyEditKey = selected ? `${selected.id}:${pending?.id || (manualActive ? 'manual' : 'composer')}` : '';
|
|
|
const hasReplyEdit = Boolean(replyEditKey && Object.prototype.hasOwnProperty.call(state.agent.replyEdits, replyEditKey));
|
|
const hasReplyEdit = Boolean(replyEditKey && Object.prototype.hasOwnProperty.call(state.agent.replyEdits, replyEditKey));
|
|
@@ -3277,44 +3350,37 @@
|
|
|
|
|
|
|
|
els.accountName.textContent = accountName;
|
|
els.accountName.textContent = accountName;
|
|
|
els.accountDot.className = `account-dot ${account.online ? 'online' : 'offline'}`;
|
|
els.accountDot.className = `account-dot ${account.online ? 'online' : 'offline'}`;
|
|
|
|
|
+ page.dataset.theme = String(config.theme || status.tenant?.theme || 'fmode').toLowerCase() === 'xiaoniu' ? 'xiaoniu' : 'fmode';
|
|
|
|
|
|
|
|
const nextHtml = `
|
|
const nextHtml = `
|
|
|
- <section class="agent-hero">
|
|
|
|
|
- <div>
|
|
|
|
|
- <div class="agent-eyebrow">WECOM · REAL AGENT · HUMAN IN THE LOOP</div>
|
|
|
|
|
- <h2>真实企微消息,由 Agent 思考,人工可随时监管</h2>
|
|
|
|
|
- <p>消息进入后,Agent 自主检索规则库、知识库、画像和房源工具;草稿、依据、置信度和工具轨迹全部留痕。</p>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="agent-safety-card">
|
|
|
|
|
- <span class="agent-live-dot ${account.online ? 'online' : ''}"></span>
|
|
|
|
|
- <div><strong>${escapeHtml(accountName)} · ${account.online ? '在线' : '离线'}</strong><small>${escapeHtml(product.label)} · ${escapeHtml(product.collectionLabel)} · ${messageIngressRunning ? '回调接收中' : '回调待恢复'}</small></div>
|
|
|
|
|
- <div class="agent-safety-actions">
|
|
|
|
|
- <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>
|
|
|
|
|
- ${product.mode === 'personal'
|
|
|
|
|
- ? `<button class="btn ${aiListenerActive ? 'btn-secondary' : ''}" data-agent-action="${allowlistReady ? (aiListenerActive ? 'stop-listener' : 'start-listener') : 'manage-allowlist'}" ${account.online ? '' : 'disabled'}>${allowlistReady ? (aiListenerActive ? '关闭 AI 监听(转人工)' : '启动 AI 监听') : '先选择白名单'}</button>`
|
|
|
|
|
- : ''}
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- </section>
|
|
|
|
|
-
|
|
|
|
|
- <section class="agent-modebar">
|
|
|
|
|
- <div class="agent-mode-copy"><strong>全局 Agent 策略</strong><span>${escapeHtml(product.label)} · ${product.mode === 'enterprise' ? (messageIngressRunning ? '公网回调常驻' : '公网回调正在恢复') : '数据保存在当前项目'} · 暂停时仍接收并保存消息</span></div>
|
|
|
|
|
|
|
+ <header class="agent-commandbar">
|
|
|
|
|
+ <div class="agent-command-account"><span class="agent-live-dot ${account.online ? 'online' : ''}"></span><div><strong>${escapeHtml(accountName)}</strong><small>${account.online ? '账号在线' : '账号离线'} · ${messageIngressRunning ? '监听运行中' : '监听待恢复'}</small></div></div>
|
|
|
<div class="agent-mode-switch">
|
|
<div class="agent-mode-switch">
|
|
|
<button class="agent-mode-btn ${!globalPaused && globalMode === 'review' ? 'active' : ''}" data-agent-mode="review">待审核</button>
|
|
<button class="agent-mode-btn ${!globalPaused && globalMode === 'review' ? 'active' : ''}" data-agent-mode="review">待审核</button>
|
|
|
<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 && globalMode === 'autopilot' ? 'active danger' : ''}" data-agent-mode="autopilot">全自动接管</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} 位白名单客户私聊 · 群聊只采集消息并沉淀画像</div>
|
|
|
|
|
- </section>
|
|
|
|
|
-
|
|
|
|
|
- ${renderResponseMonitor(state.agent.responseMonitor || {})}
|
|
|
|
|
|
|
+ <div class="agent-command-kpis" aria-label="工作台摘要"><span><b>${allowlistCount}</b> 白名单</span><span class="${pendingReplyCount ? 'attention' : ''}"><b>${pendingReplyCount}</b> 待审核</span><span class="${overdueCount ? 'danger' : ''}"><b>${reminderCount}</b> 待回复</span></div>
|
|
|
|
|
+ <div class="agent-command-actions">
|
|
|
|
|
+ <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>
|
|
|
|
|
+ ${product.mode === 'personal' ? `<button class="btn ${aiListenerActive ? 'btn-secondary' : ''}" data-agent-action="${allowlistReady ? (aiListenerActive ? 'stop-listener' : 'start-listener') : 'manage-allowlist'}" ${account.online ? '' : 'disabled'}>${allowlistReady ? (aiListenerActive ? '关闭监听' : '启动监听') : '选择白名单'}</button>` : ''}
|
|
|
|
|
+ <button class="btn btn-secondary" data-agent-drawer="settings" aria-expanded="${state.agent.openDrawer === 'settings'}">设置</button>
|
|
|
|
|
+ <button class="btn btn-secondary ${reminderCount ? 'has-alert' : ''}" data-agent-drawer="reminders" aria-expanded="${state.agent.openDrawer === 'reminders'}">提醒${reminderCount ? ` ${reminderCount}` : ''}</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </header>
|
|
|
|
|
|
|
|
${selected?.agentError
|
|
${selected?.agentError
|
|
|
? (() => { const errorUi = agentErrorPresentation(selected.agentError); return `<div class="agent-runtime-alert"><strong>${escapeHtml(errorUi.title)}</strong><span>${escapeHtml(selected.agentError.message)} ${escapeHtml(errorUi.next)} 真实消息已保存,系统没有生成或发送 Mock 回复。</span></div>`; })()
|
|
? (() => { const errorUi = agentErrorPresentation(selected.agentError); return `<div class="agent-runtime-alert"><strong>${escapeHtml(errorUi.title)}</strong><span>${escapeHtml(selected.agentError.message)} ${escapeHtml(errorUi.next)} 真实消息已保存,系统没有生成或发送 Mock 回复。</span></div>`; })()
|
|
|
: noReplyNotice
|
|
: noReplyNotice
|
|
|
? `<div class="agent-runtime-alert notice"><strong>Agent 已处理</strong><span>${escapeHtml(noReplyNotice.message)},系统没有向客户发送消息。</span></div>`
|
|
? `<div class="agent-runtime-alert notice"><strong>Agent 已处理</strong><span>${escapeHtml(noReplyNotice.message)},系统没有向客户发送消息。</span></div>`
|
|
|
: ''}
|
|
: ''}
|
|
|
|
|
+ ${selected?.onboarding?.state === 'failed'
|
|
|
|
|
+ ? `<div class="agent-runtime-alert"><strong>欢迎语发送失败</strong><span>${escapeHtml(selected.onboarding.lastError || '可在确认企微连接后重试。')}</span><button class="btn btn-sm" data-agent-action="retry-welcome" data-id="${escapeHtml(selected.id)}">重试欢迎语</button></div>`
|
|
|
|
|
+ : selected?.onboarding?.state === 'delivery_unknown'
|
|
|
|
|
+ ? '<div class="agent-runtime-alert notice"><strong>欢迎语发送结果待确认</strong><span>为避免重复打扰,系统不会自动重发,请先在企微聊天中核对。</span></div>'
|
|
|
|
|
+ : ''}
|
|
|
|
|
|
|
|
<section class="agent-workspace">
|
|
<section class="agent-workspace">
|
|
|
<aside class="agent-conversation-list">
|
|
<aside class="agent-conversation-list">
|
|
@@ -3325,10 +3391,13 @@
|
|
|
<div class="agent-list-scroll">
|
|
<div class="agent-list-scroll">
|
|
|
${listMode === 'private' && privateConversations.length ? privateConversations.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];
|
|
const last = [...(item.messages || [])].reverse().find(message => message.role === 'customer') || item.messages?.[item.messages.length - 1];
|
|
|
|
|
+ const reminder = agentReminderForConversation(item.id, monitor);
|
|
|
|
|
+ const reminderStatus = reminder?.severity === 'urgent' || reminder?.status === 'overdue' ? 'overdue' : reminder ? 'warning' : item.pendingReply ? 'pending' : '';
|
|
|
|
|
+ const reminderLabel = reminderStatus === 'overdue' ? '已超时' : reminderStatus === 'warning' ? '待回复' : reminderStatus === 'pending' ? '待审核' : '';
|
|
|
return `<button class="agent-conversation-item ${item.id === selected?.id ? 'active' : ''}" data-conversation-id="${item.id}">
|
|
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-avatar">${escapeHtml((item.displayName || '客').slice(0, 1))}</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>
|
|
<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>
|
|
|
|
|
|
|
+ <span class="agent-conversation-meta"><time>${formatAgentTime(item.lastMessageAt)}</time>${reminderLabel ? `<i class="${reminderStatus}">${reminderLabel}</i>` : ''}</span>
|
|
|
</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>
|
|
@@ -3338,7 +3407,7 @@
|
|
|
</div>
|
|
</div>
|
|
|
</aside>
|
|
</aside>
|
|
|
|
|
|
|
|
- <main class="agent-chat-panel ${selectedGroup ? 'group-mode' : ''}">
|
|
|
|
|
|
|
+ <main class="agent-chat-panel ${selectedGroup ? 'group-mode' : ''}" tabindex="-1">
|
|
|
${selectedGroup ? `
|
|
${selectedGroup ? `
|
|
|
<header class="agent-chat-header group">
|
|
<header class="agent-chat-header group">
|
|
|
<div><strong>${escapeHtml(selectedGroup.name || '客户群')}</strong><span>客户群聊 · ${selectedGroup.mode === 'auto' ? '全自动(忽略风险)' : 'Agent 草稿 · 人工审核发送'}</span></div>
|
|
<div><strong>${escapeHtml(selectedGroup.name || '客户群')}</strong><span>客户群聊 · ${selectedGroup.mode === 'auto' ? '全自动(忽略风险)' : 'Agent 草稿 · 人工审核发送'}</span></div>
|
|
@@ -3365,7 +3434,8 @@
|
|
|
<div class="agent-conversation-modes">
|
|
<div class="agent-conversation-modes">
|
|
|
<button class="btn btn-sm btn-secondary" data-agent-action="sync-current-conversation" data-id="${selected.id}" ${account.online ? '' : 'disabled'}>采集当前会话</button>
|
|
<button class="btn btn-sm btn-secondary" data-agent-action="sync-current-conversation" data-id="${selected.id}" ${account.online ? '' : 'disabled'}>采集当前会话</button>
|
|
|
<button class="btn btn-sm ${selected.mode === 'review' ? '' : 'btn-secondary'}" data-agent-conversation-mode="review" data-id="${selected.id}">待审核</button>
|
|
<button class="btn btn-sm ${selected.mode === 'review' ? '' : 'btn-secondary'}" data-agent-conversation-mode="review" data-id="${selected.id}">待审核</button>
|
|
|
- <button class="btn btn-sm ${selected.mode === 'auto' ? '' : 'btn-secondary'}" data-agent-conversation-mode="auto" data-id="${selected.id}">自动</button>
|
|
|
|
|
|
|
+ <button class="btn btn-sm ${selected.mode === 'auto' ? '' : 'btn-secondary'}" data-agent-conversation-mode="auto" data-id="${selected.id}">高置信自动</button>
|
|
|
|
|
+ <button class="btn btn-sm ${selected.mode === 'autopilot' ? 'agent-mode-danger active' : 'btn-secondary'}" data-agent-conversation-mode="autopilot" data-id="${selected.id}">全自动接管</button>
|
|
|
<button class="btn btn-sm ${selected.mode === 'human' ? '' : 'btn-secondary'}" data-agent-conversation-mode="human" data-id="${selected.id}">人工接管</button>
|
|
<button class="btn btn-sm ${selected.mode === 'human' ? '' : 'btn-secondary'}" data-agent-conversation-mode="human" data-id="${selected.id}">人工接管</button>
|
|
|
<button class="btn btn-sm ${selected.mode === 'paused' ? '' : 'btn-secondary'}" data-agent-conversation-mode="paused" data-id="${selected.id}">暂停</button>
|
|
<button class="btn btn-sm ${selected.mode === 'paused' ? '' : 'btn-secondary'}" data-agent-conversation-mode="paused" data-id="${selected.id}">暂停</button>
|
|
|
</div>
|
|
</div>
|
|
@@ -3377,14 +3447,14 @@
|
|
|
<button class="btn btn-sm btn-secondary agent-session-action" data-agent-action="session-guide" data-id="${selected.id}">${sessionGuide?.ready ? '查看会话与打开方式' : '初始化 Session'}</button>
|
|
<button class="btn btn-sm btn-secondary agent-session-action" data-agent-action="session-guide" data-id="${selected.id}">${sessionGuide?.ready ? '查看会话与打开方式' : '初始化 Session'}</button>
|
|
|
</div>
|
|
</div>
|
|
|
<div class="agent-message-stream">${(selected.messages || []).map(renderAgentMessage).join('')}</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 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>
|
|
|
|
|
- ${voiceComposerToolbar({ selected, content: pendingText, draftId: pending?.id, disabled: conversationPaused })}
|
|
|
|
|
|
|
+ <div class="agent-composer ${manualActive ? 'manual' : autopilotActive ? 'autopilot' : ''}">
|
|
|
|
|
+ <div class="agent-composer-label"><strong>${pending ? 'Agent 待审核草稿' : manualActive ? '人工回复' : autopilotActive ? '全自动接管运行中' : conversationPaused ? '会话已暂停' : noReplyNotice ? 'Agent 已处理' : 'Agent 处理区'}</strong><span data-agent-edit-status>${pending ? (hasReplyEdit ? '已人工修改 · 批准后发送当前内容' : `置信度 ${Math.round((pending.confidence || 0) * 100)}% · ${pending.requiresHuman ? '必须人工审核,可直接编辑' : '可直接编辑后发送'}`) : manualActive ? 'Agent 不会生成或发送回复' : autopilotActive ? '新消息的非空 Agent 回复将直接发送,不创建待审核草稿' : conversationPaused ? '消息保留,Agent 不处理' : noReplyNotice ? '最新消息无需回复' : '没有待审核草稿'}</span></div>
|
|
|
|
|
+ <textarea id="agent-reply-editor" data-agent-edit-key="${escapeHtml(replyEditKey)}" placeholder="${manualActive ? '输入人工回复内容…' : pending ? '可先编辑 Agent 草稿…' : autopilotActive ? '全自动接管运行中…' : noReplyNotice ? '最新客户消息无需回复' : '点击下方按钮,让 Agent 处理最近一条消息'}" ${conversationPaused || (autopilotActive && !pending) ? 'disabled' : ''}>${escapeHtml(pendingText)}</textarea>
|
|
|
|
|
+ ${voiceComposerToolbar({ selected, content: pendingText, draftId: pending?.id, disabled: conversationPaused || (autopilotActive && !pending) })}
|
|
|
<div class="agent-composer-actions">
|
|
<div class="agent-composer-actions">
|
|
|
<span>${pending ? escapeHtml(pending.reason || '请核对内容与依据后再发送') : `模型:${escapeHtml(agent.model || '未配置')} · ${agent.configured ? '已配置' : '未配置'}`}</span>
|
|
<span>${pending ? escapeHtml(pending.reason || '请核对内容与依据后再发送') : `模型:${escapeHtml(agent.model || '未配置')} · ${agent.configured ? '已配置' : '未配置'}`}</span>
|
|
|
<div class="agent-review-actions">
|
|
<div class="agent-review-actions">
|
|
|
- ${pending ? `<button class="btn btn-secondary" data-agent-action="reject" data-id="${selected.id}" data-draft-id="${pending.id}">驳回</button><button class="btn btn-secondary" data-agent-action="regenerate" data-id="${selected.id}" data-draft-id="${pending.id}">重新生成</button><button class="btn" data-agent-action="approve" data-id="${selected.id}" data-draft-id="${pending.id}">批准并发送</button>` : manualActive ? `<button class="btn" data-agent-action="manual-send" data-id="${selected.id}">人工发送</button>` : conversationPaused ? '' : `<button class="btn" data-agent-action="generate" data-id="${selected.id}">${noReplyNotice ? '重新处理' : '让 Agent 处理'}</button>`}
|
|
|
|
|
|
|
+ ${pending ? `<button class="btn btn-secondary" data-agent-action="reject" data-id="${selected.id}" data-draft-id="${pending.id}">驳回</button><button class="btn btn-secondary" data-agent-action="regenerate" data-id="${selected.id}" data-draft-id="${pending.id}">重新生成</button><button class="btn" data-agent-action="approve" data-id="${selected.id}" data-draft-id="${pending.id}">批准并发送</button>` : manualActive ? `<button class="btn" data-agent-action="manual-send" data-id="${selected.id}">人工发送</button>` : conversationPaused || autopilotActive ? '' : `<button class="btn" data-agent-action="generate" data-id="${selected.id}">${noReplyNotice ? '重新处理' : '让 Agent 处理'}</button>`}
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
@@ -3423,25 +3493,38 @@
|
|
|
<div class="agent-section-title"><span>04</span><div><strong>客户预警</strong><small>${intelligenceSummary.openAlerts || 0} 项未处理 · ${intelligenceSummary.highAlerts || 0} 项高优先级</small></div></div>
|
|
<div class="agent-section-title"><span>04</span><div><strong>客户预警</strong><small>${intelligenceSummary.openAlerts || 0} 项未处理 · ${intelligenceSummary.highAlerts || 0} 项高优先级</small></div></div>
|
|
|
<div class="agent-customer-alert-list">${customerAlerts.length ? customerAlerts.slice(0, 6).map(alert => `<div class="agent-customer-alert ${escapeHtml(alert.severity)} ${escapeHtml(alert.status)}"><div class="agent-alert-heading"><strong>${escapeHtml(alert.title)}</strong><em>${escapeHtml(alert.severity)}</em></div><p>${escapeHtml(alert.detail || '')}</p><small>依据:${escapeHtml(alert.evidence || '待补充')}</small><div class="agent-intelligence-actions">${alert.status === 'open' ? `<button class="btn btn-sm btn-secondary" data-agent-action="customer-alert-dismiss" data-id="${alert.id}">忽略</button><button class="btn btn-sm" data-agent-action="customer-alert-ack" data-id="${alert.id}">已知悉</button>` : `<span>${alert.status === 'acknowledged' ? '已知悉' : alert.status === 'resolved' ? '已解决' : '已忽略'}</span>`}</div></div>`).join('') : '<p class="agent-muted">当前没有高意向、投诉、时效或矛盾预警</p>'}</div>
|
|
<div class="agent-customer-alert-list">${customerAlerts.length ? customerAlerts.slice(0, 6).map(alert => `<div class="agent-customer-alert ${escapeHtml(alert.severity)} ${escapeHtml(alert.status)}"><div class="agent-alert-heading"><strong>${escapeHtml(alert.title)}</strong><em>${escapeHtml(alert.severity)}</em></div><p>${escapeHtml(alert.detail || '')}</p><small>依据:${escapeHtml(alert.evidence || '待补充')}</small><div class="agent-intelligence-actions">${alert.status === 'open' ? `<button class="btn btn-sm btn-secondary" data-agent-action="customer-alert-dismiss" data-id="${alert.id}">忽略</button><button class="btn btn-sm" data-agent-action="customer-alert-ack" data-id="${alert.id}">已知悉</button>` : `<span>${alert.status === 'acknowledged' ? '已知悉' : alert.status === 'resolved' ? '已解决' : '已忽略'}</span>`}</div></div>`).join('') : '<p class="agent-muted">当前没有高意向、投诉、时效或矛盾预警</p>'}</div>
|
|
|
</div>
|
|
</div>
|
|
|
- <div class="agent-insight-section">
|
|
|
|
|
- <div class="agent-section-title"><span>05</span><div><strong>匹配建议</strong><small>已接入房源工具的返回结果</small></div></div>
|
|
|
|
|
- <div class="agent-match-list">${matches.length ? matches.map((match, index) => `<div class="agent-match-card"><span>${index + 1}</span><div><strong>${escapeHtml(match.community || match.title || '匹配结果')}</strong><small>${escapeHtml([match.price ? `${match.price}万` : '', match.layout, match.area ? `${match.area}㎡` : ''].filter(Boolean).join(' · '))}</small><em>${escapeHtml((match.highlights || []).join(' / ') || match.level || '')}</em></div></div>`).join('') : '<p class="agent-muted">当前消息未触发房源工具</p>'}</div>
|
|
|
|
|
- </div>
|
|
|
|
|
<div class="agent-insight-section knowledge">
|
|
<div class="agent-insight-section knowledge">
|
|
|
- <div class="agent-section-title"><span>06</span><div><strong>依据与引用</strong><small>${knowledge.knowledgeChunks || 0} 个知识块 · ${knowledge.propertyDataLabel || '房源未接入'}</small></div></div>
|
|
|
|
|
|
|
+ <div class="agent-section-title"><span>05</span><div><strong>依据与引用</strong><small>${knowledge.knowledgeChunks || 0} 个知识块 · 通用知识库</small></div></div>
|
|
|
<ul>${(citations.length ? citations.map(item => `${item.heading || item.source} · ${item.source || ''}`) : analysis.knowledgeSources || []).map(source => `<li>${escapeHtml(source)}</li>`).join('')}</ul>
|
|
<ul>${(citations.length ? citations.map(item => `${item.heading || item.source} · ${item.source || ''}`) : analysis.knowledgeSources || []).map(source => `<li>${escapeHtml(source)}</li>`).join('')}</ul>
|
|
|
</div>
|
|
</div>
|
|
|
<div class="agent-insight-section">
|
|
<div class="agent-insight-section">
|
|
|
- <div class="agent-section-title"><span>07</span><div><strong>工具调用轨迹</strong><small>Agent 自主检索过程</small></div></div>
|
|
|
|
|
- <div class="agent-tool-trace">${toolTrace.length ? toolTrace.map((item, index) => `<details ${index === 0 ? 'open' : ''}><summary>${escapeHtml(item.tool)}</summary><pre>${escapeHtml(JSON.stringify({ input: item.args || {}, result: item.result || {} }, null, 2))}</pre></details>`).join('') : '<p class="agent-muted">当前没有工具调用记录</p>'}</div>
|
|
|
|
|
|
|
+ <div class="agent-section-title"><span>06</span><div><strong>活动与工具轨迹</strong><small>Plan / Act / Observe / Evaluate</small></div></div>
|
|
|
|
|
+ <details class="agent-activity-disclosure" data-agent-activity ${state.agent.activityOpen ? 'open' : ''}><summary>查看执行证据与脱敏参数</summary>${renderAgentActivityTimeline({ toolTrace, audit })}</details>
|
|
|
</div>
|
|
</div>
|
|
|
<div class="agent-insight-section">
|
|
<div class="agent-insight-section">
|
|
|
- <div class="agent-section-title"><span>08</span><div><strong>审计日志</strong><small>接收、决策、审核和发送全留痕</small></div></div>
|
|
|
|
|
|
|
+ <div class="agent-section-title"><span>07</span><div><strong>审计日志</strong><small>接收、决策、审核和发送全留痕</small></div></div>
|
|
|
<div class="agent-audit-list">${audit.length ? audit.slice(0, 10).map(item => `<div><span>${escapeHtml(agentAuditLabel(item.action))}</span><time>${formatAgentTime(item.created_at)}</time></div>`).join('') : '<p class="agent-muted">暂无审计记录</p>'}</div>
|
|
<div class="agent-audit-list">${audit.length ? audit.slice(0, 10).map(item => `<div><span>${escapeHtml(agentAuditLabel(item.action))}</span><time>${formatAgentTime(item.created_at)}</time></div>`).join('') : '<p class="agent-muted">暂无审计记录</p>'}</div>
|
|
|
</div>
|
|
</div>
|
|
|
` : '<div class="agent-empty"><strong>暂无分析</strong><span>选择会话后显示 Agent 工作过程。</span></div>'}
|
|
` : '<div class="agent-empty"><strong>暂无分析</strong><span>选择会话后显示 Agent 工作过程。</span></div>'}
|
|
|
</aside>
|
|
</aside>
|
|
|
</section>
|
|
</section>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="agent-drawer-backdrop ${state.agent.openDrawer ? 'open' : ''}" data-agent-drawer-close></div>
|
|
|
|
|
+ <aside class="agent-drawer settings ${state.agent.openDrawer === 'settings' ? 'open' : ''}" aria-hidden="${state.agent.openDrawer !== 'settings'}" aria-label="Agent 设置">
|
|
|
|
|
+ <header><div><strong>Agent 设置</strong><span>接入、欢迎语与发送边界</span></div><button type="button" data-agent-drawer-close>关闭</button></header>
|
|
|
|
|
+ <div class="agent-drawer-body">
|
|
|
|
|
+ <section class="agent-connection-summary">
|
|
|
|
|
+ <div><span class="agent-live-dot ${account.online ? 'online' : ''}"></span><strong>${escapeHtml(accountName)}</strong></div>
|
|
|
|
|
+ <dl><div><dt>产品模式</dt><dd>${escapeHtml(product.label)}</dd></div><div><dt>消息接收</dt><dd>${messageIngressRunning ? '运行中' : '待恢复'}</dd></div><div><dt>默认模式</dt><dd>${escapeHtml(agentModeLabel(globalPaused ? 'paused' : globalMode))}</dd></div><div><dt>白名单</dt><dd>${allowlistCount} 人</dd></div></dl>
|
|
|
|
|
+ </section>
|
|
|
|
|
+ ${renderAgentIntakeSettings()}
|
|
|
|
|
+ <section class="agent-drawer-boundary"><strong>当前发送边界</strong><p>${globalMode === 'autopilot' ? '全自动接管会直接发送白名单私聊中的非空 Agent 回复,不创建待审核草稿。' : '未知群聊始终拒绝;自动纳入联系人按当前账号隔离;高置信自动仍受置信度与人工风险门禁。'}</p></section>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </aside>
|
|
|
|
|
+ <aside class="agent-drawer reminders ${state.agent.openDrawer === 'reminders' ? 'open' : ''}" aria-hidden="${state.agent.openDrawer !== 'reminders'}" aria-label="回复提醒">
|
|
|
|
|
+ <header><div><strong>回复提醒</strong><span>${reminderCount ? `${reminderCount} 个会话需要关注` : '当前没有待处理提醒'}</span></div><button type="button" data-agent-drawer-close>关闭</button></header>
|
|
|
|
|
+ <div class="agent-drawer-body">${renderResponseMonitor(monitor)}</div>
|
|
|
|
|
+ </aside>
|
|
|
`;
|
|
`;
|
|
|
const viewKey = `${listMode}:${selected?.id || selectedGroup?.id || 'empty'}`;
|
|
const viewKey = `${listMode}:${selected?.id || selectedGroup?.id || 'empty'}`;
|
|
|
const incremental = page.dataset.agentViewKey === viewKey && Boolean(page.querySelector('.agent-workspace'));
|
|
const incremental = page.dataset.agentViewKey === viewKey && Boolean(page.querySelector('.agent-workspace'));
|
|
@@ -3470,6 +3553,35 @@
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ function normalizeAgentIntakePolicy(payload = {}) {
|
|
|
|
|
+ const policy = payload.policy || payload;
|
|
|
|
|
+ return {
|
|
|
|
|
+ mode: ['allowlist_only', 'auto_enroll_review', 'auto_enroll_autopilot'].includes(policy.mode) ? policy.mode : 'allowlist_only',
|
|
|
|
|
+ welcomeEnabled: Boolean(policy.welcomeEnabled),
|
|
|
|
|
+ welcomeText: String(policy.welcomeText || ''),
|
|
|
|
|
+ welcomeSendMode: policy.welcomeSendMode === 'send' ? 'send' : 'draft',
|
|
|
|
|
+ effectiveConversationMode: policy.effectiveConversationMode || 'review',
|
|
|
|
|
+ onboarding: policy.onboarding || null,
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async function loadAgentIntakePolicy(page, silent = false) {
|
|
|
|
|
+ if (state.agent.intakePolicyLoading) return;
|
|
|
|
|
+ state.agent.intakePolicyLoading = true;
|
|
|
|
|
+ try {
|
|
|
|
|
+ const result = await api('GET', '/api/agent/intake-policy');
|
|
|
|
|
+ state.agent.intakePolicy = normalizeAgentIntakePolicy(result.data || result);
|
|
|
|
|
+ state.agent.intakePolicyLoaded = true;
|
|
|
|
|
+ state.agent.intakePolicyError = '';
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ state.agent.intakePolicyError = error.message || '接入策略读取失败,当前展示安全默认值';
|
|
|
|
|
+ if (!silent) toast(state.agent.intakePolicyError, 'error');
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ state.agent.intakePolicyLoading = false;
|
|
|
|
|
+ if (page?.isConnected && state.page === 'agent') renderAgentWorkspaceV2(page);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
async function loadAgentData(page, silent = false) {
|
|
async function loadAgentData(page, silent = false) {
|
|
|
if (state.agent.loading) return;
|
|
if (state.agent.loading) return;
|
|
|
state.agent.loading = true;
|
|
state.agent.loading = true;
|
|
@@ -3597,6 +3709,49 @@
|
|
|
};
|
|
};
|
|
|
page.addEventListener('pointerdown', markInsightInteraction, true);
|
|
page.addEventListener('pointerdown', markInsightInteraction, true);
|
|
|
page.addEventListener('scroll', markInsightInteraction, true);
|
|
page.addEventListener('scroll', markInsightInteraction, true);
|
|
|
|
|
+ page.addEventListener('toggle', event => {
|
|
|
|
|
+ if (event.target?.matches?.('[data-agent-activity]')) state.agent.activityOpen = event.target.open;
|
|
|
|
|
+ }, true);
|
|
|
|
|
+
|
|
|
|
|
+ page.addEventListener('submit', async event => {
|
|
|
|
|
+ const form = event.target.closest('[data-agent-intake-form]');
|
|
|
|
|
+ if (!form) return;
|
|
|
|
|
+ event.preventDefault();
|
|
|
|
|
+ const formData = new FormData(form);
|
|
|
|
|
+ const policy = {
|
|
|
|
|
+ mode: String(formData.get('mode') || 'allowlist_only'),
|
|
|
|
|
+ welcomeEnabled: formData.get('welcomeEnabled') === 'on',
|
|
|
|
|
+ welcomeText: String(formData.get('welcomeText') || '').trim(),
|
|
|
|
|
+ welcomeSendMode: String(formData.get('welcomeSendMode') || 'draft') === 'send' ? 'send' : 'draft',
|
|
|
|
|
+ };
|
|
|
|
|
+ if (policy.welcomeSendMode === 'send' && policy.mode !== 'auto_enroll_autopilot') {
|
|
|
|
|
+ toast('欢迎语直接发送仅可用于已明确确认的全自动接管策略', 'error');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (policy.mode === 'auto_enroll_autopilot' && !await confirmModal('新私聊会自动加入当前账号白名单,欢迎语和后续非空 Agent 回复可能直接真实发送。', {
|
|
|
|
|
+ title: '确认开启全自动新联系人接入',
|
|
|
|
|
+ confirmText: '确认全自动接入',
|
|
|
|
|
+ warning: '所有非空 Agent 回复都会直接发送,请确认当前账号的白名单范围。',
|
|
|
|
|
+ danger: true,
|
|
|
|
|
+ })) return;
|
|
|
|
|
+ state.agent.intakePolicy = policy;
|
|
|
|
|
+ state.agent.intakePolicyLoading = true;
|
|
|
|
|
+ renderAgentWorkspaceV2(page);
|
|
|
|
|
+ try {
|
|
|
|
|
+ const body = { ...policy, ...(policy.mode === 'auto_enroll_autopilot' ? { confirmation: 'ENABLE_AUTO_ENROLL_AUTOPILOT' } : {}) };
|
|
|
|
|
+ const result = await api('POST', '/api/agent/intake-policy', body);
|
|
|
|
|
+ state.agent.intakePolicy = normalizeAgentIntakePolicy(result.data || result);
|
|
|
|
|
+ state.agent.intakePolicyLoaded = true;
|
|
|
|
|
+ state.agent.intakePolicyError = '';
|
|
|
|
|
+ toast(result.assistantMessage || '接入策略已保存');
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ state.agent.intakePolicyError = error.message || '接入策略保存失败';
|
|
|
|
|
+ toast(state.agent.intakePolicyError, 'error');
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ state.agent.intakePolicyLoading = false;
|
|
|
|
|
+ if (page.isConnected) renderAgentWorkspaceV2(page);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
|
|
|
page.addEventListener('input', event => {
|
|
page.addEventListener('input', event => {
|
|
|
const editor = event.target.closest('#agent-reply-editor');
|
|
const editor = event.target.closest('#agent-reply-editor');
|
|
@@ -3644,6 +3799,20 @@
|
|
|
}, true);
|
|
}, true);
|
|
|
|
|
|
|
|
page.addEventListener('click', async event => {
|
|
page.addEventListener('click', async event => {
|
|
|
|
|
+ const drawerClose = event.target.closest('[data-agent-drawer-close]');
|
|
|
|
|
+ if (drawerClose) {
|
|
|
|
|
+ state.agent.openDrawer = null;
|
|
|
|
|
+ renderAgentWorkspaceV2(page);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ const drawerButton = event.target.closest('[data-agent-drawer]');
|
|
|
|
|
+ if (drawerButton) {
|
|
|
|
|
+ const drawer = drawerButton.dataset.agentDrawer;
|
|
|
|
|
+ state.agent.openDrawer = state.agent.openDrawer === drawer ? null : drawer;
|
|
|
|
|
+ renderAgentWorkspaceV2(page);
|
|
|
|
|
+ if (state.agent.openDrawer === 'settings' && !state.agent.intakePolicyLoaded) await loadAgentIntakePolicy(page, true);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
const transcriptButton = event.target.closest('[data-voice-transcript-toggle]');
|
|
const transcriptButton = event.target.closest('[data-voice-transcript-toggle]');
|
|
|
if (transcriptButton) {
|
|
if (transcriptButton) {
|
|
|
const transcript = document.getElementById(transcriptButton.getAttribute('aria-controls'));
|
|
const transcript = document.getElementById(transcriptButton.getAttribute('aria-controls'));
|
|
@@ -3678,8 +3847,9 @@
|
|
|
state.agent.listMode = 'group';
|
|
state.agent.listMode = 'group';
|
|
|
state.agent.selectedGroupId = responseMonitorButton.dataset.id;
|
|
state.agent.selectedGroupId = responseMonitorButton.dataset.id;
|
|
|
}
|
|
}
|
|
|
|
|
+ state.agent.openDrawer = null;
|
|
|
renderAgentWorkspaceV2(page);
|
|
renderAgentWorkspaceV2(page);
|
|
|
- page.querySelector('.agent-workspace')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
|
|
|
|
|
+ page.querySelector('.agent-chat-panel')?.focus({ preventScroll: true });
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
const listModeButton = event.target.closest('[data-agent-list-mode]');
|
|
const listModeButton = event.target.closest('[data-agent-list-mode]');
|
|
@@ -3713,8 +3883,14 @@
|
|
|
warning: '符合条件的回复将不经人工审核直接发送。',
|
|
warning: '符合条件的回复将不经人工审核直接发送。',
|
|
|
danger: true,
|
|
danger: true,
|
|
|
})) return;
|
|
})) return;
|
|
|
|
|
+ if (mode === 'autopilot' && !await confirmModal(`全自动接管会向白名单中的 ${allowlistCount} 位联系人直接发送 Agent 回复,且不创建待审核草稿。`, {
|
|
|
|
|
+ title: '开启全自动接管',
|
|
|
|
|
+ confirmText: '确认开启全自动',
|
|
|
|
|
+ warning: '所有非空 Agent 回复都会直接发送,请确认当前白名单范围。',
|
|
|
|
|
+ danger: true,
|
|
|
|
|
+ })) return;
|
|
|
try {
|
|
try {
|
|
|
- const result = await api('POST', '/api/agent/mode', { mode });
|
|
|
|
|
|
|
+ const result = await api('POST', '/api/agent/mode', { mode, ...(mode === 'autopilot' ? { confirmation: 'ENABLE_AUTOPILOT' } : {}) });
|
|
|
toast(result.assistantMessage || '模式已更新');
|
|
toast(result.assistantMessage || '模式已更新');
|
|
|
await loadAgentData(page);
|
|
await loadAgentData(page);
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
@@ -3733,10 +3909,16 @@
|
|
|
warning: '符合条件的回复将不经人工审核直接发送给当前联系人。',
|
|
warning: '符合条件的回复将不经人工审核直接发送给当前联系人。',
|
|
|
danger: true,
|
|
danger: true,
|
|
|
})) return;
|
|
})) return;
|
|
|
- const action = ({ review: 'resume', auto: 'auto', human: 'takeover', paused: 'pause' })[mode];
|
|
|
|
|
|
|
+ if (mode === 'autopilot' && !await confirmModal('该会话将进入全自动接管,Agent 回复会直接发送且不创建待审核草稿。', {
|
|
|
|
|
+ title: '开启会话全自动接管',
|
|
|
|
|
+ confirmText: '确认开启全自动',
|
|
|
|
|
+ warning: '所有非空 Agent 回复都会直接发送给当前联系人。',
|
|
|
|
|
+ danger: true,
|
|
|
|
|
+ })) return;
|
|
|
|
|
+ const action = ({ review: 'resume', auto: 'auto', autopilot: 'autopilot', human: 'takeover', paused: 'pause' })[mode];
|
|
|
conversationModeButton.disabled = true;
|
|
conversationModeButton.disabled = true;
|
|
|
try {
|
|
try {
|
|
|
- const result = await api('POST', `/api/agent/conversations/${id}/${action}`, {});
|
|
|
|
|
|
|
+ const result = await api('POST', `/api/agent/conversations/${id}/${action}`, mode === 'autopilot' ? { confirmation: 'ENABLE_AUTOPILOT' } : {});
|
|
|
toast(result.assistantMessage || '会话模式已更新');
|
|
toast(result.assistantMessage || '会话模式已更新');
|
|
|
await loadAgentData(page);
|
|
await loadAgentData(page);
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
@@ -3892,6 +4074,9 @@
|
|
|
result = await api('POST', `/api/agent/drafts/${actionButton.dataset.draftId}/reject`, { reason });
|
|
result = await api('POST', `/api/agent/drafts/${actionButton.dataset.draftId}/reject`, { reason });
|
|
|
} else if (action === 'regenerate') {
|
|
} else if (action === 'regenerate') {
|
|
|
result = await api('POST', `/api/agent/drafts/${actionButton.dataset.draftId}/regenerate`, {});
|
|
result = await api('POST', `/api/agent/drafts/${actionButton.dataset.draftId}/regenerate`, {});
|
|
|
|
|
+ } else if (action === 'retry-welcome') {
|
|
|
|
|
+ if (!await confirmModal('确认重试这位新联系人的欢迎语?', { title: '重试欢迎语', confirmText: '确认重试', warning: '该操作会立即发送真实私聊消息。', danger: true })) return;
|
|
|
|
|
+ result = await api('POST', `/api/agent/onboarding/${encodeURIComponent(id)}/retry-welcome`, {});
|
|
|
} else if (action === 'generate') {
|
|
} else if (action === 'generate') {
|
|
|
result = await api('POST', `/api/agent/conversations/${id}/generate`, {});
|
|
result = await api('POST', `/api/agent/conversations/${id}/generate`, {});
|
|
|
} else if (action === 'manual-send') {
|
|
} else if (action === 'manual-send') {
|
|
@@ -5326,7 +5511,6 @@
|
|
|
const openTasks = (selected?.tasks || []).filter(task => ['open', 'in_progress'].includes(task.status));
|
|
const openTasks = (selected?.tasks || []).filter(task => ['open', 'in_progress'].includes(task.status));
|
|
|
const openAlerts = (selected?.alerts || []).filter(alert => alert.status === 'open');
|
|
const openAlerts = (selected?.alerts || []).filter(alert => alert.status === 'open');
|
|
|
const nextActions = selected?.nextActions || [];
|
|
const nextActions = selected?.nextActions || [];
|
|
|
- const recommendations = selected?.recommendations || [];
|
|
|
|
|
const timeline = selected?.timeline || [];
|
|
const timeline = selected?.timeline || [];
|
|
|
const intelligenceItems = `${openTasks.map(task => `<div class="customer-master-action-item"><strong>${escapeHtml(task.title)}</strong><p>${escapeHtml(task.evidence || task.reason || '')}</p><small>内部任务 · ${escapeHtml(agentTaskStatusLabel(task.status))}</small></div>`).join('')}${openAlerts.map(alert => `<div class="customer-master-action-item alert"><strong>${escapeHtml(alert.title)}</strong><p>${escapeHtml(alert.evidence || alert.detail || '')}</p><small>客户预警 · ${escapeHtml(alert.severity)}</small></div>`).join('')}`;
|
|
const intelligenceItems = `${openTasks.map(task => `<div class="customer-master-action-item"><strong>${escapeHtml(task.title)}</strong><p>${escapeHtml(task.evidence || task.reason || '')}</p><small>内部任务 · ${escapeHtml(agentTaskStatusLabel(task.status))}</small></div>`).join('')}${openAlerts.map(alert => `<div class="customer-master-action-item alert"><strong>${escapeHtml(alert.title)}</strong><p>${escapeHtml(alert.evidence || alert.detail || '')}</p><small>客户预警 · ${escapeHtml(alert.severity)}</small></div>`).join('')}`;
|
|
|
const isFallback = Boolean(summary.fallback);
|
|
const isFallback = Boolean(summary.fallback);
|
|
@@ -5336,14 +5520,13 @@
|
|
|
<div class="customer-master-score-row"><div><strong>${selected.completenessScore}%</strong><span>画像完整度</span></div><div><strong>${selected.evidenceCoverage}%</strong><span>字段证据覆盖</span></div><div><strong>${selected.messageCount}</strong><span>已纳入消息</span></div><div><strong>${selected.openTaskCount}</strong><span>待跟进任务</span></div></div>
|
|
<div class="customer-master-score-row"><div><strong>${selected.completenessScore}%</strong><span>画像完整度</span></div><div><strong>${selected.evidenceCoverage}%</strong><span>字段证据覆盖</span></div><div><strong>${selected.messageCount}</strong><span>已纳入消息</span></div><div><strong>${selected.openTaskCount}</strong><span>待跟进任务</span></div></div>
|
|
|
<section class="customer-profile-fields"><header><strong>自动沉淀画像</strong><span>${escapeHtml(formatAgentTime(selected.profileUpdatedAt))} 更新</span></header><div>${(selected.fields || []).map(field => `<div><small>${escapeHtml(field.label)}</small><strong>${escapeHtml(field.displayValue || '-')}</strong><em>${field.evidence ? field.evidence.source === 'human' ? '人工核对' : '有原话依据' : '历史字段 · 待补依据'}</em></div>`).join('') || '<p class="agent-muted">还没有形成结构化画像,后续真实对话会持续补齐。</p>'}</div>${selected.tags?.length ? `<footer>${selected.tags.map(tag => `<span>${escapeHtml(tag)}</span>`).join('')}</footer>` : ''}</section>
|
|
<section class="customer-profile-fields"><header><strong>自动沉淀画像</strong><span>${escapeHtml(formatAgentTime(selected.profileUpdatedAt))} 更新</span></header><div>${(selected.fields || []).map(field => `<div><small>${escapeHtml(field.label)}</small><strong>${escapeHtml(field.displayValue || '-')}</strong><em>${field.evidence ? field.evidence.source === 'human' ? '人工核对' : '有原话依据' : '历史字段 · 待补依据'}</em></div>`).join('') || '<p class="agent-muted">还没有形成结构化画像,后续真实对话会持续补齐。</p>'}</div>${selected.tags?.length ? `<footer>${selected.tags.map(tag => `<span>${escapeHtml(tag)}</span>`).join('')}</footer>` : ''}</section>
|
|
|
<section class="customer-next-action-section"><header><div><strong>下一步行动工作台</strong><span>规则建议 · 需人工判断 · 不自动外发</span></div><em>${nextActions.length} 项建议</em></header><div>${nextActions.map(action => `<article class="customer-next-action-card ${escapeHtml(action.priority)}"><div><span>${action.confidence === 'rule' ? '规则建议' : 'AI 建议'}</span><strong>${escapeHtml(action.title)}</strong><p>${escapeHtml(action.description)}</p></div><ul>${(action.evidence || []).slice(0, 3).map(item => `<li>${escapeHtml(item)}</li>`).join('') || '<li>当前依据待补充</li>'}</ul>${selected.fallback ? '' : `<button class="btn btn-sm btn-secondary" data-customer-master-open="${escapeHtml(selected.id)}">去智能会话处理</button>`}</article>`).join('') || '<p class="agent-muted">当前没有新的行动建议。</p>'}</div></section>
|
|
<section class="customer-next-action-section"><header><div><strong>下一步行动工作台</strong><span>规则建议 · 需人工判断 · 不自动外发</span></div><em>${nextActions.length} 项建议</em></header><div>${nextActions.map(action => `<article class="customer-next-action-card ${escapeHtml(action.priority)}"><div><span>${action.confidence === 'rule' ? '规则建议' : 'AI 建议'}</span><strong>${escapeHtml(action.title)}</strong><p>${escapeHtml(action.description)}</p></div><ul>${(action.evidence || []).slice(0, 3).map(item => `<li>${escapeHtml(item)}</li>`).join('') || '<li>当前依据待补充</li>'}</ul>${selected.fallback ? '' : `<button class="btn btn-sm btn-secondary" data-customer-master-open="${escapeHtml(selected.id)}">去智能会话处理</button>`}</article>`).join('') || '<p class="agent-muted">当前没有新的行动建议。</p>'}</div></section>
|
|
|
- <section class="customer-recommendation-section"><header><div><strong>房源推荐历史</strong><span>保存已推荐内容和客户反馈,避免重复推荐</span></div><em>${recommendations.length} 套 · ${selected.pendingFeedbackCount || 0} 套待反馈</em></header><div>${recommendations.map(item => `<article class="customer-recommendation-card"><header><div><span>${escapeHtml(item.propertyId)}</span><strong>${escapeHtml(item.property?.community || '未命名房源')}</strong></div><em class="${escapeHtml(item.status)}">${escapeHtml(item.statusLabel)}</em></header><p>${[item.property?.district, item.property?.layout, item.property?.area ? `${item.property.area}㎡` : '', item.property?.decoration].filter(Boolean).map(escapeHtml).join(' · ') || '房源快照字段待补充'}</p><div><strong>${item.property?.totalPrice || '-'}<small>万</small></strong><span>累计推荐 ${item.recommendCount || 1} 次</span><span>最近 ${escapeHtml(formatAgentTime(item.lastRecommendedAt))}</span></div>${item.feedbackReason ? `<blockquote>${escapeHtml(item.feedbackReason)}</blockquote>` : ''}<footer>${selected.fallback ? '' : `<button class="btn btn-sm btn-secondary" data-recommendation-feedback="${escapeHtml(item.id)}" data-feedback-status="interested">感兴趣</button><button class="btn btn-sm btn-secondary" data-recommendation-feedback="${escapeHtml(item.id)}" data-feedback-status="rejected">不合适</button><button class="btn btn-sm btn-secondary" data-recommendation-feedback="${escapeHtml(item.id)}" data-feedback-status="viewing">已约带看</button><button class="btn btn-sm" data-recommendation-feedback="${escapeHtml(item.id)}" data-feedback-status="viewed">已带看</button>`}</footer></article>`).join('') || '<p class="agent-muted">还没有可验证的房源推荐记录。后续房源工具调用或明确发送的房源方案会自动进入这里。</p>'}</div></section>
|
|
|
|
|
<div class="customer-master-intelligence-grid">
|
|
<div class="customer-master-intelligence-grid">
|
|
|
<section><header><strong>字段依据</strong><span>${evidenceItems.length} 条</span></header>${evidenceItems.map(field => `<div class="customer-evidence-item"><strong>${escapeHtml(field.label)}</strong><p>${escapeHtml(field.evidence.text || '人工核对')}</p><small>${field.evidence.source === 'human' ? '人工修正记录' : '真实企微消息'} · ${escapeHtml(formatAgentTime(field.evidence.updatedAt))}</small></div>`).join('') || '<p class="agent-muted">现有历史画像没有逐字段原话定位;新消息产生的字段会自动记录证据。</p>'}</section>
|
|
<section><header><strong>字段依据</strong><span>${evidenceItems.length} 条</span></header>${evidenceItems.map(field => `<div class="customer-evidence-item"><strong>${escapeHtml(field.label)}</strong><p>${escapeHtml(field.evidence.text || '人工核对')}</p><small>${field.evidence.source === 'human' ? '人工修正记录' : '真实企微消息'} · ${escapeHtml(formatAgentTime(field.evidence.updatedAt))}</small></div>`).join('') || '<p class="agent-muted">现有历史画像没有逐字段原话定位;新消息产生的字段会自动记录证据。</p>'}</section>
|
|
|
<section><header><strong>跟进任务与预警</strong><span>${openTasks.length + openAlerts.length} 项</span></header>${intelligenceItems || '<p class="agent-muted">当前没有待处理任务或预警。</p>'}</section>
|
|
<section><header><strong>跟进任务与预警</strong><span>${openTasks.length + openAlerts.length} 项</span></header>${intelligenceItems || '<p class="agent-muted">当前没有待处理任务或预警。</p>'}</section>
|
|
|
</div>
|
|
</div>
|
|
|
${selected.latestMessage ? `<section class="customer-latest-message"><strong>最近纳入主档的消息</strong><p>${escapeHtml(selected.latestMessage.content)}</p><small>${selected.latestMessage.role === 'customer' ? '客户' : '内部回复'} · ${escapeHtml(formatAgentTime(selected.latestMessage.timestamp))}</small></section>` : ''}
|
|
${selected.latestMessage ? `<section class="customer-latest-message"><strong>最近纳入主档的消息</strong><p>${escapeHtml(selected.latestMessage.content)}</p><small>${selected.latestMessage.role === 'customer' ? '客户' : '内部回复'} · ${escapeHtml(formatAgentTime(selected.latestMessage.timestamp))}</small></section>` : ''}
|
|
|
- <section class="customer-timeline-section"><header><div><strong>客户时间线</strong><span>消息、画像、任务、预警和房源反馈统一留痕</span></div><em>${timeline.length} 个事件</em></header><div>${timeline.slice(0, 20).map(event => `<article class="customer-timeline-item ${escapeHtml(event.type)}"><i></i><time>${escapeHtml(formatAgentTime(event.occurredAt))}</time><div><strong>${escapeHtml(event.title)}</strong><p>${escapeHtml(event.description || '')}</p><small>${escapeHtml(event.source || '内部主账')}${event.status ? ` · ${escapeHtml(event.status)}` : ''}</small></div></article>`).join('') || '<p class="agent-muted">暂无客户事件。</p>'}</div></section>
|
|
|
|
|
- ${selected.fallback ? `<div class="customer-master-fallback-note"><strong>参考数据</strong><span>该客户来自客户运营导入或群成员发现,尚未建立真实企微会话。同步会话后,画像、任务和证据会自动沉淀到这里。</span></div>` : `<form class="customer-master-edit-form" id="customer-master-profile-form" data-customer-id="${escapeHtml(selected.id)}"><header><div><strong>人工核对画像</strong><span>只修正主档,不会向客户发送消息</span></div><button class="btn btn-sm" type="submit" ${state.customerOps.master.loading ? 'disabled' : ''}>保存主档</button></header><div><label><span>意向区域</span><input name="preferredRegion" value="${escapeHtml(customerProfileInput(profile, 'preferredRegion', 'region', 'district'))}" /></label><label><span>预算/万</span><input name="budgetWan" type="number" value="${escapeHtml(customerProfileInput(profile, 'budgetWan'))}" /></label><label><span>户型</span><input name="layout" value="${escapeHtml(customerProfileInput(profile, 'layout', 'houseType'))}" /></label><label><span>购置用途</span><input name="intent" value="${escapeHtml(customerProfileInput(profile, 'intent', 'purpose'))}" placeholder="自住 / 投资 / 待确认" /></label><label><span>购置时间</span><input name="timeline" value="${escapeHtml(customerProfileInput(profile, 'timeline'))}" /></label><label><span>标签</span><input name="tags" value="${escapeHtml((selected.tags || []).join(','))}" /></label></div><label class="customer-master-reason"><span>修正依据(可选)</span><input name="reason" placeholder="例如:已电话确认;仅用于审计留痕" /></label></form>`}
|
|
|
|
|
|
|
+ <section class="customer-timeline-section"><header><div><strong>客户时间线</strong><span>消息、画像、任务和预警统一留痕</span></div><em>${timeline.length} 个事件</em></header><div>${timeline.slice(0, 20).map(event => `<article class="customer-timeline-item ${escapeHtml(event.type)}"><i></i><time>${escapeHtml(formatAgentTime(event.occurredAt))}</time><div><strong>${escapeHtml(event.title)}</strong><p>${escapeHtml(event.description || '')}</p><small>${escapeHtml(event.source || '内部主账')}${event.status ? ` · ${escapeHtml(event.status)}` : ''}</small></div></article>`).join('') || '<p class="agent-muted">暂无客户事件。</p>'}</div></section>
|
|
|
|
|
+ ${selected.fallback ? `<div class="customer-master-fallback-note"><strong>参考数据</strong><span>该客户来自客户运营导入或群成员发现,尚未建立真实企微会话。同步会话后,画像、任务和证据会自动沉淀到这里。</span></div>` : `<form class="customer-master-edit-form" id="customer-master-profile-form" data-customer-id="${escapeHtml(selected.id)}"><header><div><strong>人工核对画像</strong><span>只修正主档,不会向客户发送消息</span></div><button class="btn btn-sm" type="submit" ${state.customerOps.master.loading ? 'disabled' : ''}>保存主档</button></header><div><label><span>客户意图</span><input name="intent" value="${escapeHtml(customerProfileInput(profile, 'intent', 'purpose'))}" /></label><label><span>核心需求</span><input name="need" value="${escapeHtml(customerProfileInput(profile, 'need', 'needs', 'keyConcerns'))}" /></label><label><span>预算</span><input name="budgetWan" type="number" value="${escapeHtml(customerProfileInput(profile, 'budgetWan', 'budget'))}" /></label><label><span>计划时间</span><input name="timeline" value="${escapeHtml(customerProfileInput(profile, 'timeline'))}" /></label><label><span>决策人</span><input name="decisionMaker" value="${escapeHtml(customerProfileInput(profile, 'decisionMaker'))}" /></label><label><span>标签</span><input name="tags" value="${escapeHtml((selected.tags || []).join(','))}" /></label></div><label class="customer-master-reason"><span>修正依据(可选)</span><input name="reason" placeholder="例如:已电话确认;仅用于审计留痕" /></label></form>`}
|
|
|
</article>` : '<div class="library-empty large"><strong>暂无已同步客户</strong><span>从真实企微会话同步消息后,画像会自动沉淀到这里。</span></div>';
|
|
</article>` : '<div class="library-empty large"><strong>暂无已同步客户</strong><span>从真实企微会话同步消息后,画像会自动沉淀到这里。</span></div>';
|
|
|
return `<section class="customer-master-shell">
|
|
return `<section class="customer-master-shell">
|
|
|
<header class="customer-master-hero"><div><span>CUSTOMER 360 · CANONICAL PROFILE</span><h2>客户管理</h2><p>${isFallback ? '当前展示客户运营导入/群成员发现数据作为参考;同步真实企微会话后会自动替换为画像主档。' : '真实企微消息持续生成客户主档,画像、标签、任务、预警和证据统一归档。'}</p></div><button class="btn secondary" id="btn-customer-master-refresh" ${state.customerOps.master.loading ? 'disabled' : ''}>刷新主档</button></header>
|
|
<header class="customer-master-hero"><div><span>CUSTOMER 360 · CANONICAL PROFILE</span><h2>客户管理</h2><p>${isFallback ? '当前展示客户运营导入/群成员发现数据作为参考;同步真实企微会话后会自动替换为画像主档。' : '真实企微消息持续生成客户主档,画像、标签、任务、预警和证据统一归档。'}</p></div><button class="btn secondary" id="btn-customer-master-refresh" ${state.customerOps.master.loading ? 'disabled' : ''}>刷新主档</button></header>
|
|
@@ -5474,30 +5657,6 @@
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
if (event.target.closest('#btn-customer-master-refresh')) void reloadCustomerMaster('客户主档已刷新');
|
|
if (event.target.closest('#btn-customer-master-refresh')) void reloadCustomerMaster('客户主档已刷新');
|
|
|
- const feedbackButton = event.target.closest('[data-recommendation-feedback]');
|
|
|
|
|
- if (feedbackButton) {
|
|
|
|
|
- const status = feedbackButton.dataset.feedbackStatus;
|
|
|
|
|
- let feedbackReason = '';
|
|
|
|
|
- if (status === 'rejected') {
|
|
|
|
|
- const answer = await promptModal('请记录该房源不合适的原因,便于后续优化推荐。', '', { title: '记录不感兴趣原因', label: '原因', textarea: true, required: false, confirmText: '保存反馈' });
|
|
|
|
|
- if (answer === null) return;
|
|
|
|
|
- feedbackReason = answer.trim();
|
|
|
|
|
- } else if (status === 'interested') {
|
|
|
|
|
- const answer = await promptModal('可记录客户感兴趣的具体原因,便于后续跟进。', '', { title: '记录感兴趣原因', label: '原因', textarea: true, required: false, confirmText: '保存反馈' });
|
|
|
|
|
- if (answer === null) return;
|
|
|
|
|
- feedbackReason = answer.trim();
|
|
|
|
|
- }
|
|
|
|
|
- state.customerOps.master.loading = true;
|
|
|
|
|
- rerenderCustomerMaster();
|
|
|
|
|
- api('POST', `/api/customers/${encodeURIComponent(state.customerOps.master.selectedId)}/recommendations/${encodeURIComponent(feedbackButton.dataset.recommendationFeedback)}`, { status, feedbackReason })
|
|
|
|
|
- .then(async result => {
|
|
|
|
|
- if (result.status !== 'ok') throw result;
|
|
|
|
|
- state.customerOps.master.hub = await api('GET', '/api/customers');
|
|
|
|
|
- toast(result.assistantMessage || '房源反馈已记录');
|
|
|
|
|
- })
|
|
|
|
|
- .catch(error => toast(error.assistantMessage || error.message || '房源反馈记录失败', 'error'))
|
|
|
|
|
- .finally(() => { state.customerOps.master.loading = false; rerenderCustomerMaster(); });
|
|
|
|
|
- }
|
|
|
|
|
});
|
|
});
|
|
|
masterRoot?.addEventListener('change', event => {
|
|
masterRoot?.addEventListener('change', event => {
|
|
|
if (event.target.id !== 'customer-master-search') return;
|
|
if (event.target.id !== 'customer-master-search') return;
|
|
@@ -5510,11 +5669,11 @@
|
|
|
const form = new FormData(event.target);
|
|
const form = new FormData(event.target);
|
|
|
const budgetText = String(form.get('budgetWan') || '').trim();
|
|
const budgetText = String(form.get('budgetWan') || '').trim();
|
|
|
const profile = {
|
|
const profile = {
|
|
|
- preferredRegion: String(form.get('preferredRegion') || '').trim(),
|
|
|
|
|
- budgetWan: budgetText ? Number(budgetText) : '',
|
|
|
|
|
- layout: String(form.get('layout') || '').trim(),
|
|
|
|
|
intent: String(form.get('intent') || '').trim(),
|
|
intent: String(form.get('intent') || '').trim(),
|
|
|
|
|
+ need: String(form.get('need') || '').trim(),
|
|
|
|
|
+ budgetWan: budgetText ? Number(budgetText) : '',
|
|
|
timeline: String(form.get('timeline') || '').trim(),
|
|
timeline: String(form.get('timeline') || '').trim(),
|
|
|
|
|
+ decisionMaker: String(form.get('decisionMaker') || '').trim(),
|
|
|
};
|
|
};
|
|
|
state.customerOps.master.loading = true;
|
|
state.customerOps.master.loading = true;
|
|
|
rerenderCustomerMaster();
|
|
rerenderCustomerMaster();
|
|
@@ -6640,7 +6799,7 @@
|
|
|
</div>
|
|
</div>
|
|
|
<div class="form-group" style="margin-bottom:16px">
|
|
<div class="form-group" style="margin-bottom:16px">
|
|
|
<label>标签(逗号分隔)</label>
|
|
<label>标签(逗号分隔)</label>
|
|
|
- <input id="tag-input" type="text" placeholder="高意向,学区房" />
|
|
|
|
|
|
|
+ <input id="tag-input" type="text" placeholder="高意向,重点客户" />
|
|
|
</div>
|
|
</div>
|
|
|
<div class="form-group">
|
|
<div class="form-group">
|
|
|
<label>操作</label>
|
|
<label>操作</label>
|