replace_transfers.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. # -*- coding: utf-8 -*-
  2. new_transfers = ''' async function renderTransfersPage() {
  3. els.pageTitle.textContent = '客户交接看板';
  4. const page = document.createElement('div');
  5. page.className = 'page';
  6. page.innerHTML = `
  7. <div class="dashboard-grid" id="tf-dashboard">
  8. <div class="kpi-row" id="tf-kpi-row"></div>
  9. <div class="chart-card">
  10. <h3 class="chart-title"><span>交接漏斗</span></h3>
  11. <div class="chart-container" id="tf-funnel-chart"></div>
  12. </div>
  13. <div class="chart-card">
  14. <h3 class="chart-title"><span>交接结果分布</span></h3>
  15. <div class="chart-container" id="tf-result-chart"></div>
  16. </div>
  17. <div class="chart-card full">
  18. <h3 class="chart-title"><span>近7日交接执行趋势</span></h3>
  19. <div class="chart-container" id="tf-trend-chart"></div>
  20. </div>
  21. <div class="operation-toolbar">
  22. <div class="toolbar-left">
  23. <button class="btn" id="btn-new-transfer">新建交接预览</button>
  24. </div>
  25. <div class="toolbar-right">
  26. <button class="btn btn-sm btn-secondary" id="btn-tf-refresh">刷新</button>
  27. <button class="btn btn-sm btn-secondary" id="btn-tf-rebuild">重建索引</button>
  28. </div>
  29. </div>
  30. <div class="data-table-card">
  31. <div class="card-header">
  32. <div>
  33. <h3 class="card-title">交接包列表</h3>
  34. <div class="card-subtitle">预览包与执行记录</div>
  35. </div>
  36. </div>
  37. <div class="filter-bar" style="margin-bottom:16px">
  38. <select id="tf-filter-status">
  39. <option value="">全部状态</option>
  40. <option value="DRAFT">草稿</option>
  41. <option value="EXECUTED">已执行</option>
  42. </select>
  43. <input id="tf-filter-user" type="text" placeholder="按顾问 userId 搜索" style="width:200px" />
  44. </div>
  45. <div id="tf-transfer-table-wrap"></div>
  46. <div id="tf-pagination"></div>
  47. </div>
  48. </div>
  49. `;
  50. els.content.appendChild(page);
  51. await loadTransfersDashboard(page);
  52. dashboardRefreshTimer = setInterval(() => {
  53. if (state.page === 'transfers') loadTransfersDashboard(page, true);
  54. }, 30000);
  55. page.querySelector('#btn-tf-refresh').addEventListener('click', () => loadTransfersDashboard(page));
  56. page.querySelector('#btn-tf-rebuild').addEventListener('click', async () => {
  57. try {
  58. await api('POST', '/api/dashboard/rebuild');
  59. toast('索引已重建');
  60. loadTransfersDashboard(page);
  61. } catch (err) {
  62. toast(err.message || '重建失败', 'error');
  63. }
  64. });
  65. page.querySelector('#btn-new-transfer').addEventListener('click', () => openTransferModal(page));
  66. page.querySelector('#tf-filter-status').addEventListener('change', () => loadTransfersTable(page));
  67. page.querySelector('#tf-filter-user').addEventListener('input', () => loadTransfersTable(page));
  68. }
  69. async function loadTransfersDashboard(page, silent = false) {
  70. if (!silent) page.querySelector('#tf-transfer-table-wrap').innerHTML = '<div class="skeleton" style="height:200px"></div>';
  71. try {
  72. const [summaryRes, transfersRes] = await Promise.all([
  73. api('GET', '/api/dashboard/summary'),
  74. api('GET', '/api/dashboard/transfers')
  75. ]);
  76. const summary = summaryRes.data || {};
  77. const transfers = transfersRes.data || {};
  78. renderTransfersKpi(page, summary);
  79. renderTransfersCharts(page, summary, transfers);
  80. state.transfers = { ...state.transfers, ...transfers };
  81. loadTransfersTable(page);
  82. } catch (err) {
  83. if (!silent) toast(err.message || '加载看板失败', 'error');
  84. }
  85. }
  86. function renderTransfersKpi(page, summary) {
  87. const wrap = page.querySelector('#tf-kpi-row');
  88. const t = summary.transfers || {};
  89. const kpi = [
  90. { label: '交接预览包', value: formatNumber(t.previews) },
  91. { label: '已执行交接', value: formatNumber(t.executions) },
  92. { label: '交接成功率', value: (t.successRate || 0) + '%', sub: '成功记录 / 总记录' },
  93. { label: '近7日交接', value: formatNumber(t.recent7d), sub: '最近7天执行数' }
  94. ];
  95. wrap.innerHTML = renderKpiRow(kpi);
  96. }
  97. function renderTransfersCharts(page, summary, transfers) {
  98. const t = summary.transfers || {};
  99. initEChart('tf-funnel-chart', {
  100. tooltip: { trigger: 'item', formatter: '{b}: {c}' },
  101. series: [{
  102. type: 'funnel',
  103. left: '10%',
  104. width: '80%',
  105. label: { show: true, position: 'inside' },
  106. data: [
  107. { name: '预览包', value: t.previews || 0, itemStyle: { color: '#fa8c16' } },
  108. { name: '已执行', value: t.executions || 0, itemStyle: { color: '#1890ff' } },
  109. { name: '成功', value: Math.round(((t.successRate || 0) / 100) * (t.executions || 0)), itemStyle: { color: '#52c41a' } }
  110. ]
  111. }]
  112. });
  113. const executions = transfers.executions || [];
  114. const success = executions.reduce((sum, e) => sum + (e.success || 0), 0);
  115. const failed = executions.reduce((sum, e) => sum + (e.failed || 0), 0);
  116. initEChart('tf-result-chart', {
  117. tooltip: { trigger: 'item' },
  118. legend: { bottom: 0 },
  119. series: [{
  120. type: 'pie',
  121. radius: ['40%', '65%'],
  122. center: ['50%', '45%'],
  123. data: [
  124. { name: '成功', value: success, itemStyle: { color: '#52c41a' } },
  125. { name: '失败', value: failed, itemStyle: { color: '#ff4d4f' } }
  126. ]
  127. }]
  128. });
  129. const days = last7Days();
  130. const counts = countByDay(executions, 'executedAt');
  131. initEChart('tf-trend-chart', {
  132. tooltip: { trigger: 'axis' },
  133. xAxis: { type: 'category', data: days.map(d => formatDate(d)) },
  134. yAxis: { type: 'value' },
  135. series: [{ name: '交接执行', type: 'bar', data: counts, itemStyle: { color: '#fa8c16' } }]
  136. });
  137. }
  138. function loadTransfersTable(page) {
  139. const wrap = page.querySelector('#tf-transfer-table-wrap');
  140. const transfers = state.transfers || {};
  141. const statusFilter = page.querySelector('#tf-filter-status').value;
  142. const userFilter = page.querySelector('#tf-filter-user').value.trim().toLowerCase();
  143. let items = [
  144. ...(transfers.previews || []).map(p => ({ ...p, type: 'preview' })),
  145. ...(transfers.executions || []).map(e => ({ ...e, type: 'execution' }))
  146. ];
  147. let filtered = items.filter(item => {
  148. if (statusFilter && item.status !== statusFilter) return false;
  149. if (userFilter) {
  150. const text = `${item.fromUserId || ''} ${item.toUserId || ''}`.toLowerCase();
  151. if (!text.includes(userFilter)) return false;
  152. }
  153. return true;
  154. });
  155. filtered.sort((a, b) => (b.createdAt || b.executedAt || '').localeCompare(a.createdAt || a.executedAt || ''));
  156. const pageSize = 20;
  157. const currentPage = state.transfers.tablePage || 1;
  158. const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize));
  159. if (currentPage > totalPages) state.transfers.tablePage = totalPages;
  160. const start = (state.transfers.tablePage - 1) * pageSize;
  161. const pageItems = filtered.slice(start, start + pageSize);
  162. wrap.innerHTML = `
  163. <div class="table-wrap">
  164. <table>
  165. <thead>
  166. <tr>
  167. <th>类型</th>
  168. <th>原顾问</th>
  169. <th>新顾问</th>
  170. <th>记录数</th>
  171. <th>状态</th>
  172. <th>创建/执行时间</th>
  173. <th>操作</th>
  174. </tr>
  175. </thead>
  176. <tbody>
  177. ${pageItems.length ? pageItems.map(item => `
  178. <tr>
  179. <td>${item.type === 'preview' ? '<span class="badge badge-warning">预览</span>' : '<span class="badge badge-info">执行</span>'}</td>
  180. <td>${escapeHtml(item.fromUserId || '-')}</td>
  181. <td>${escapeHtml(item.toUserId || '-')}</td>
  182. <td>${formatNumber(item.itemCount || item.total || 0)}</td>
  183. <td>${item.status === 'EXECUTED' ? '<span class="badge badge-success">已执行</span>' : '<span class="badge badge-warning">草稿</span>'}</td>
  184. <td>${formatDateTime(item.createdAt || item.executedAt)}</td>
  185. <td>
  186. ${item.type === 'preview' ? `<button class="btn btn-sm" onclick="window.dashboardExecuteTransfer('${escapeHtml(item.filePath || '')}')">执行</button>` : `<button class="btn btn-sm btn-secondary" onclick="window.dashboardTransferDetail('${escapeHtml(item.filePath || '')}')">详情</button>`}
  187. </td>
  188. </tr>
  189. `).join('') : '<tr><td colspan="7" style="text-align:center;color:var(--text-muted);padding:40px">暂无交接记录</td></tr>'}
  190. </tbody>
  191. </table>
  192. </div>
  193. `;
  194. const pagination = page.querySelector('#tf-pagination');
  195. pagination.innerHTML = renderPagination(state.transfers.tablePage, filtered.length, pageSize);
  196. pagination.querySelectorAll('button').forEach(btn => {
  197. btn.addEventListener('click', () => {
  198. state.transfers.tablePage = Number(btn.dataset.page);
  199. loadTransfersTable(page);
  200. });
  201. });
  202. }
  203. function openTransferModal(page) {
  204. const body = `
  205. <div class="form-group" style="margin-bottom:16px">
  206. <label>原顾问 userId</label>
  207. <input id="new-tf-from" type="text" placeholder="fromUserId" />
  208. </div>
  209. <div class="form-group" style="margin-bottom:16px">
  210. <label>新顾问 userId</label>
  211. <input id="new-tf-to" type="text" placeholder="toUserId" />
  212. </div>
  213. <div class="form-group" style="margin-bottom:16px">
  214. <label>externalUserIds(每行一个,可选)</label>
  215. <textarea id="new-tf-external" rows="4" placeholder="externalUserId1\nexternalUserId2"></textarea>
  216. </div>
  217. <div class="form-group">
  218. <label>已确认群 roomIds(逗号分隔,可选)</label>
  219. <input id="new-tf-rooms" type="text" placeholder="roomId1,roomId2" />
  220. </div>
  221. `;
  222. openModal('新建交接预览', body, `
  223. <button class="btn btn-secondary" onclick="window.dashboardCloseModal()">取消</button>
  224. <button class="btn" id="btn-confirm-new-transfer">生成预览</button>
  225. `);
  226. document.getElementById('btn-confirm-new-transfer').addEventListener('click', async () => {
  227. const fromUserId = document.getElementById('new-tf-from').value.trim();
  228. const toUserId = document.getElementById('new-tf-to').value.trim();
  229. const externalUserIds = document.getElementById('new-tf-external').value.split(/\n|\r/).map(s => s.trim()).filter(Boolean);
  230. const roomIds = document.getElementById('new-tf-rooms').value.split(/[,,]/).map(s => s.trim()).filter(Boolean);
  231. if (!fromUserId || !toUserId) {
  232. toast('请输入原顾问和新顾问 userId', 'warning');
  233. return;
  234. }
  235. if (!externalUserIds.length && !roomIds.length) {
  236. toast('请输入 externalUserIds 或 roomIds', 'warning');
  237. return;
  238. }
  239. try {
  240. await api('POST', '/api/transfers/preview', { fromUserId, toUserId, externalUserIds, roomIds });
  241. toast('预览已生成');
  242. closeModal();
  243. loadTransfersDashboard(page);
  244. } catch (err) {
  245. toast(err.message || '生成失败', 'error');
  246. }
  247. });
  248. }
  249. window.dashboardExecuteTransfer = (filePath) => {
  250. const body = `
  251. <div class="form-group" style="margin-bottom:16px">
  252. <label>预览文件路径</label>
  253. <input id="exec-tf-file" type="text" value="${escapeHtml(filePath || '')}" placeholder="留空使用最新预览" />
  254. </div>
  255. <div class="form-group" style="margin-bottom:16px">
  256. <label>欢迎语</label>
  257. <input id="exec-tf-welcome" type="text" value="您好,我是您的新服务顾问,后续由我为您服务。" />
  258. </div>
  259. <div class="checkbox-row">
  260. <input id="exec-tf-remove" type="checkbox" />
  261. <label for="exec-tf-remove">交接后移除原顾问</label>
  262. </div>
  263. `;
  264. openModal('执行交接', body, `
  265. <button class="btn btn-secondary" onclick="window.dashboardCloseModal()">取消</button>
  266. <button class="btn" id="btn-confirm-exec-transfer">执行</button>
  267. `);
  268. document.getElementById('btn-confirm-exec-transfer').addEventListener('click', () => {
  269. const previewFile = document.getElementById('exec-tf-file').value.trim() || undefined;
  270. const welcomeText = document.getElementById('exec-tf-welcome').value.trim();
  271. const removeOldBroker = document.getElementById('exec-tf-remove').checked;
  272. closeModal();
  273. startJob('execute-transfer', '/api/transfers/execute', { guid: getGuid(), previewFile, welcomeText, removeOldBroker }, () => loadTransfersDashboard(document.querySelector('#tf-dashboard')?.closest('.page')));
  274. });
  275. };
  276. window.dashboardTransferDetail = async (filePath) => {
  277. if (!filePath) return;
  278. try {
  279. openModal('交接详情', `
  280. <div class="detail-panel">${escapeHtml(JSON.stringify({ filePath }, null, 2))}</div>
  281. <div style="margin-top:12px">${renderFileLink(filePath, '下载文件')}</div>
  282. `, `<button class="btn btn-secondary" onclick="window.dashboardCloseModal()">关闭</button>`);
  283. } catch (err) {
  284. toast(err.message || '查询失败', 'error');
  285. }
  286. };
  287. '''
  288. with open('d:/caidawork/openclaw-voc-skill/claude-code/claude-code-qiwe-assistant/mcp/src/dashboard/app.js', 'r', encoding='utf-8') as f:
  289. lines = f.readlines()
  290. start = 2156
  291. end = 2347
  292. new_lines = lines[:start] + [new_transfers + '\n'] + lines[end:]
  293. with open('d:/caidawork/openclaw-voc-skill/claude-code/claude-code-qiwe-assistant/mcp/src/dashboard/app.js', 'w', encoding='utf-8') as f:
  294. f.writelines(new_lines)
  295. print('renderTransfersPage replaced successfully')