@iamsamyiok/agents-chat 3.30.0 → 3.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +26 -0
- package/app/lib/agent.js +4 -1
- package/app/lib/cards.js +2 -0
- package/app/lib/notify.js +26 -0
- package/app/lib/oc.js +10 -3
- package/app/lib/orchestrator.js +31 -26
- package/app/lib/store.js +91 -1
- package/app/lib/worktree.js +192 -0
- package/app/mock/mock-agent.js +7 -2
- package/app/public/cards.html +20 -1
- package/app/public/icon.svg +6 -0
- package/app/public/index.html +259 -12
- package/app/public/manifest.json +17 -0
- package/app/server.js +119 -8
- package/bin/agents-chat.js +107 -0
- package/package.json +4 -3
- package/scripts/build-exe.js +13 -2
package/app/public/index.html
CHANGED
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
<html lang="zh-CN">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
|
6
|
+
<meta name="theme-color" content="#07c160">
|
|
7
|
+
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
8
|
+
<meta name="apple-mobile-web-app-title" content="AgentsChat">
|
|
9
|
+
<link rel="manifest" href="/manifest.json">
|
|
10
|
+
<link rel="icon" href="/static/icon.svg" type="image/svg+xml">
|
|
6
11
|
<title>Agents 群聊</title>
|
|
7
12
|
<style>
|
|
8
13
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
@@ -84,6 +89,11 @@
|
|
|
84
89
|
#jumpBtn { position: absolute; right: 18px; bottom: 100px; z-index: 5; display: none; border: 1px solid #dcdcdc; background: #fff; color: #333; border-radius: 16px; padding: 6px 14px; font-size: 12px; cursor: pointer; box-shadow: 0 2px 8px rgba(0,0,0,.15); animation: fade-in .2s ease; transition: color .15s, border-color .15s; }
|
|
85
90
|
#jumpBtn:hover { color: #07c160; border-color: #07c160; }
|
|
86
91
|
.sys-tip { text-align: center; margin: 10px 0; }
|
|
92
|
+
/* 历史弹窗:全历史搜索结果条目 */
|
|
93
|
+
.hist-hit { border: 1px solid #eee; border-radius: 8px; padding: 6px 10px; margin: 4px 0; cursor: pointer; background: #fafafa; }
|
|
94
|
+
.hist-hit:hover { border-color: #07c160; background: #f0f7f0; }
|
|
95
|
+
.hist-hit .hh-meta { font-size: 11px; color: #999; margin-bottom: 2px; }
|
|
96
|
+
.hist-hit .hh-snippet { font-size: 12px; color: #333; line-height: 1.5; word-break: break-all; }
|
|
87
97
|
/* 长会话增量渲染:加载更早入口 */
|
|
88
98
|
.load-earlier { display: block; margin: 4px auto 16px; border: 1px solid #dcdcdc; background: #fff; color: #888; border-radius: 14px; padding: 5px 14px; font-size: 12px; cursor: pointer; transition: color .15s, border-color .15s; }
|
|
89
99
|
.load-earlier:hover { color: #07c160; border-color: #07c160; }
|
|
@@ -205,14 +215,14 @@
|
|
|
205
215
|
.at-tx i { font-style: normal; color: #999; font-size: 11px; display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 380px; }
|
|
206
216
|
|
|
207
217
|
/* ---------- 弹窗 ---------- */
|
|
208
|
-
#modalMask, #batchMask, #agentsMask, #historyMask, #helpMask, #flowMask, #cardMask, #teamsMask, #memMask { position: fixed; inset: 0; background: rgba(0,0,0,.4); display: none; align-items: center; justify-content: center; z-index: 51; }
|
|
218
|
+
#modalMask, #batchMask, #agentsMask, #historyMask, #helpMask, #flowMask, #cardMask, #teamsMask, #memMask, #diffMask { position: fixed; inset: 0; background: rgba(0,0,0,.4); display: none; align-items: center; justify-content: center; z-index: 51; }
|
|
209
219
|
/* 弹窗出现动画:遮罩淡入(含毛玻璃)+ 内容上浮,全站弹窗统一节奏 */
|
|
210
220
|
div[id$="Mask"] { backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px); }
|
|
211
221
|
div[id$="Mask"].show { animation: fade-in .18s ease; }
|
|
212
222
|
.modal, .preview-card { animation: modal-in .24s cubic-bezier(.2,.7,.3,1); }
|
|
213
223
|
@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
|
|
214
224
|
@keyframes modal-in { from { opacity: 0; transform: translateY(14px) scale(.985); } to { opacity: 1; transform: none; } }
|
|
215
|
-
#modalMask.show, #batchMask.show, #agentsMask.show, #historyMask.show, #helpMask.show, #flowMask.show, #cardMask.show, #teamsMask.show, #memMask.show { display: flex; }
|
|
225
|
+
#modalMask.show, #batchMask.show, #agentsMask.show, #historyMask.show, #helpMask.show, #flowMask.show, #cardMask.show, #teamsMask.show, #memMask.show, #diffMask.show { display: flex; }
|
|
216
226
|
/* 团队仓库卡片 */
|
|
217
227
|
.team-card { border: 1px solid #e5e7eb; border-radius: 10px; padding: 14px 16px; margin-bottom: 12px; }
|
|
218
228
|
.team-card .tc-head { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
|
|
@@ -251,6 +261,12 @@
|
|
|
251
261
|
.flow-detail .fd-title { font-weight: 600; margin-bottom: 6px; }
|
|
252
262
|
.flow-detail .fd-files { color: #7d5ac8; }
|
|
253
263
|
.modal { background: #fff; border-radius: 10px; width: 560px; max-width: 92vw; max-height: 88vh; overflow-y: auto; padding: 20px; }
|
|
264
|
+
/* Git 改动视图:行级着色 */
|
|
265
|
+
.diff-pre { margin: 6px 0 0; padding: 8px; background: #fff; border-radius: 6px; font-size: 12px; line-height: 1.55; overflow-x: auto; white-space: pre; font-family: ui-monospace, Menlo, Consolas, monospace; }
|
|
266
|
+
.diff-pre .dl { padding: 0 4px; border-radius: 3px; }
|
|
267
|
+
.diff-pre .dl.add { background: #e6ffec; color: #116329; }
|
|
268
|
+
.diff-pre .dl.del { background: #ffebe9; color: #82071e; }
|
|
269
|
+
.diff-pre .dl.hun { color: #0550ae; background: #ddf4ff; }
|
|
254
270
|
.modal h3 { font-size: 15px; margin-bottom: 6px; }
|
|
255
271
|
.modal .tip { font-size: 12px; color: #999; line-height: 1.7; margin-bottom: 10px; }
|
|
256
272
|
.modal textarea { width: 100%; height: 200px; border: 1px solid #ddd; border-radius: 6px; padding: 10px; font-size: 13px; resize: vertical; outline: none; }
|
|
@@ -306,6 +322,19 @@
|
|
|
306
322
|
#menuBtn { display: block !important; }
|
|
307
323
|
.msg-body { max-width: 80%; }
|
|
308
324
|
}
|
|
325
|
+
/* 触屏/小屏增强:更大触控目标、安全区适配、顶栏按钮收纳 */
|
|
326
|
+
@media (max-width: 760px) {
|
|
327
|
+
.hdr-right { flex-wrap: wrap; gap: 4px; }
|
|
328
|
+
.hdr-right .cfg-btn { padding: 4px 8px; font-size: 11px; }
|
|
329
|
+
#messages { padding: 10px 8px; }
|
|
330
|
+
.bubble.left, .bubble.right { font-size: 14px; word-break: break-word; }
|
|
331
|
+
.composer { padding-bottom: calc(8px + env(safe-area-inset-bottom)); }
|
|
332
|
+
#input { font-size: 16px; } /* 防 iOS 聚焦自动放大 */
|
|
333
|
+
.jumpBtn { bottom: calc(84px + env(safe-area-inset-bottom)); }
|
|
334
|
+
#jumpBtn { bottom: calc(84px + env(safe-area-inset-bottom)); }
|
|
335
|
+
.modal.wide { width: 96vw; max-height: 92vh; }
|
|
336
|
+
#previewMask .preview-card { width: 96vw; height: 92vh; }
|
|
337
|
+
}
|
|
309
338
|
#menuBtn { display: none; background: none; border: none; font-size: 18px; cursor: pointer; color: #555; margin-right: 10px; }
|
|
310
339
|
|
|
311
340
|
/* ---------- 侧栏顶部:单聊/群聊开关 + 任务操作按钮 ---------- */
|
|
@@ -441,6 +470,7 @@
|
|
|
441
470
|
<button class="cfg-btn" id="cardsNavBtn" onclick="openCards()" title="多任务编排 · 卡牌:把任务写成卡牌,Agent 干完一件自动注入下一件">🗂️ 多任务编排</button>
|
|
442
471
|
<span class="runner-tag" id="runnerTag" style="display:none"></span>
|
|
443
472
|
<span class="ver-chip" id="verChip" title="当前版本" style="font-size:11px;color:#999;border:1px solid #e2e2e2;border-radius:10px;padding:2px 8px;white-space:nowrap"></span>
|
|
473
|
+
<button class="cfg-btn" id="notifyBtn" onclick="toggleSysNotify()" title="系统通知:页面在后台时任务完成弹系统级通知(浏览器授权后生效)">🔔 通知</button>
|
|
444
474
|
<button class="cfg-btn" id="helpBtn" onclick="openHelp()" title="使用帮助">❓ 帮助</button>
|
|
445
475
|
</div>
|
|
446
476
|
</header>
|
|
@@ -486,6 +516,20 @@
|
|
|
486
516
|
</div>
|
|
487
517
|
</div>
|
|
488
518
|
|
|
519
|
+
<!-- 任务 Git 改动弹窗(隔离区 diff / 合并 / 丢弃) -->
|
|
520
|
+
<div id="diffMask">
|
|
521
|
+
<div class="modal" style="max-width:860px">
|
|
522
|
+
<h3 id="diffTitle">🌿 任务改动</h3>
|
|
523
|
+
<div class="tip" id="diffMeta"></div>
|
|
524
|
+
<div id="diffBody" style="max-height:52vh;overflow:auto;border:1px solid #d8dee9;border-radius:8px;padding:8px;background:#fafbfc"></div>
|
|
525
|
+
<div class="modal-actions">
|
|
526
|
+
<button class="mini-btn danger" onclick="discardTaskDiff()">🗑 丢弃隔离区</button>
|
|
527
|
+
<button class="primary" onclick="mergeTaskDiff()">合并到主目录</button>
|
|
528
|
+
<button onclick="closeTaskDiff()">关闭</button>
|
|
529
|
+
</div>
|
|
530
|
+
</div>
|
|
531
|
+
</div>
|
|
532
|
+
|
|
489
533
|
<!-- 导入任务弹窗 -->
|
|
490
534
|
<div id="modalMask">
|
|
491
535
|
<div class="modal">
|
|
@@ -499,6 +543,10 @@
|
|
|
499
543
|
<textarea id="importText" onkeydown="importKeydown(event)"></textarea>
|
|
500
544
|
<div id="importAtMenu"></div>
|
|
501
545
|
</div>
|
|
546
|
+
<div class="ag-row" style="margin:6px 0 2px;align-items:center;gap:6px">
|
|
547
|
+
<input type="checkbox" id="importIsolated" style="width:auto">
|
|
548
|
+
<label for="importIsolated" style="flex:1;cursor:pointer">🌿 Git 隔离执行:每个任务在独立 Git worktree 分支中执行,互不污染;完成后可查看改动、合并回主目录或整体丢弃(统一工作目录需是 Git 仓库)</label>
|
|
549
|
+
</div>
|
|
502
550
|
<div class="modal-actions">
|
|
503
551
|
<button onclick="closeImport()">取消</button>
|
|
504
552
|
<button class="primary" onclick="doImport()">提取任务</button>
|
|
@@ -588,10 +636,16 @@
|
|
|
588
636
|
<div class="modal wide">
|
|
589
637
|
<h3>聊天历史(主会话)</h3>
|
|
590
638
|
<div class="tip">按时间正序展示全部主会话记录(含已开启新会话前的内容),仅供查阅;消息中的 .md 文件路径可点击预览</div>
|
|
639
|
+
<input id="histSearch" placeholder="🔍 搜索全部会话消息(含任务/单聊会话,关键词匹配)…" autocomplete="off"
|
|
640
|
+
style="width:100%;box-sizing:border-box;padding:7px 10px;border:1px solid #e2e2e2;border-radius:8px;font-size:12.5px;margin-bottom:6px">
|
|
641
|
+
<div id="histUsage" style="font-size:11.5px;color:#888;margin:0 0 6px"></div>
|
|
642
|
+
<div id="histSearchResult" style="display:none"></div>
|
|
591
643
|
<div id="historyList"></div>
|
|
592
644
|
<div class="modal-actions" style="justify-content:space-between">
|
|
593
|
-
<div style="display:flex;gap:8px">
|
|
645
|
+
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
|
594
646
|
<button class="mini-btn" onclick="exportHistory()" title="全部会话(含任务会话)导出为 Markdown 文件">⬇ 导出 sessions.md</button>
|
|
647
|
+
<button class="mini-btn" onclick="showDataStats()" title="查看数据目录磁盘占用分布">📊 空间占用</button>
|
|
648
|
+
<button class="mini-btn" onclick="pruneDataNow()" title="手动执行一次滚动清理:过期任务/会话/产出存档等(与每日自动清理同一套规则,默认保留 15 天)">🧹 清理旧数据</button>
|
|
595
649
|
<button class="mini-btn danger" onclick="clearAllHistory()" title="删除全部消息记录、会话产出文件与流转日志(任务列表保留)">🗑 清空全部会话</button>
|
|
596
650
|
</div>
|
|
597
651
|
<button class="primary" onclick="closeHistory()">关闭</button>
|
|
@@ -666,7 +720,7 @@
|
|
|
666
720
|
</div>
|
|
667
721
|
|
|
668
722
|
<script>
|
|
669
|
-
const PAGE_VERSION = '3.
|
|
723
|
+
const PAGE_VERSION = '3.32.0'; // 与服务端 /api/health.version 互检,不一致说明页面缓存过期
|
|
670
724
|
const AV_COLORS = ['#5b8def','#07c160','#fa9d3b','#10aeff','#8a6fe8','#fa5151','#e8a33d','#3aa7a3'];
|
|
671
725
|
const PHASE_LABEL = { plan: '调度规划', work: '执行', review: '验收', report: '汇总', talk: '圆桌发言', task: '任务执行' };
|
|
672
726
|
// 职业图标库(智能体配置可选)
|
|
@@ -703,7 +757,29 @@ function escAttr(s) { return String(s == null ? '' : s).replace(/&/g,'&').re
|
|
|
703
757
|
function fmtTime(ts) { const d = new Date(ts); return String(d.getHours()).padStart(2,'0') + ':' + String(d.getMinutes()).padStart(2,'0'); }
|
|
704
758
|
function fmtSched(ts) { const d = new Date(ts); const p = (n) => String(n).padStart(2,'0'); return `${d.getFullYear()}-${p(d.getMonth()+1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`; }
|
|
705
759
|
function fmtSchedNow() { const d = new Date(); const p = (n) => String(n).padStart(2,'0'); return `${d.getFullYear()}${p(d.getMonth()+1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}`; }
|
|
706
|
-
function toast(msg) { const t = document.getElementById('toast'); t.textContent = msg; t.style.display = 'block'; setTimeout(() => t.style.display = 'none', 2200); }
|
|
760
|
+
function toast(msg, ms) { const t = document.getElementById('toast'); t.textContent = msg; t.style.display = 'block'; setTimeout(() => t.style.display = 'none', ms || 2200); }
|
|
761
|
+
|
|
762
|
+
// ---------- 系统通知开关(浏览器 Notification API,状态记忆在 localStorage) ----------
|
|
763
|
+
function refreshNotifyBtn() {
|
|
764
|
+
const on = localStorage.getItem('sysNotify') === '1';
|
|
765
|
+
const b = document.getElementById('notifyBtn');
|
|
766
|
+
if (b) { b.style.color = on ? '#07c160' : ''; b.style.borderColor = on ? '#07c160' : ''; }
|
|
767
|
+
}
|
|
768
|
+
async function toggleSysNotify() {
|
|
769
|
+
const on = localStorage.getItem('sysNotify') === '1';
|
|
770
|
+
if (on) {
|
|
771
|
+
localStorage.setItem('sysNotify', '0');
|
|
772
|
+
toast('系统通知已关闭');
|
|
773
|
+
} else {
|
|
774
|
+
if (typeof Notification === 'undefined') { toast('当前浏览器不支持系统通知'); return; }
|
|
775
|
+
let perm = Notification.permission;
|
|
776
|
+
if (perm !== 'granted') perm = await Notification.requestPermission();
|
|
777
|
+
if (perm !== 'granted') { toast('浏览器未授权通知,请在地址栏权限设置中允许'); return; }
|
|
778
|
+
localStorage.setItem('sysNotify', '1');
|
|
779
|
+
toast('系统通知已开启:页面在后台时任务完成将弹系统通知');
|
|
780
|
+
}
|
|
781
|
+
refreshNotifyBtn();
|
|
782
|
+
}
|
|
707
783
|
function colorOf(id) { let h = 0; const s = String(id); for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) >>> 0; return AV_COLORS[h % AV_COLORS.length]; }
|
|
708
784
|
// 智能体显示图标:配置的职业图标 > 名称首字
|
|
709
785
|
function agentIcon(agentId, name) {
|
|
@@ -761,6 +837,7 @@ async function init() {
|
|
|
761
837
|
addSys(`⚠ 页面版本(${PAGE_VERSION})与服务端(${health.version})不一致:浏览器缓存了旧页面,请按 Ctrl+F5 强制刷新`);
|
|
762
838
|
}
|
|
763
839
|
renderUpdateInfo(health);
|
|
840
|
+
refreshNotifyBtn();
|
|
764
841
|
|
|
765
842
|
input.addEventListener('input', handleAtInput);
|
|
766
843
|
input.addEventListener('blur', () => setTimeout(hideAt, 150));
|
|
@@ -1349,6 +1426,15 @@ function linkBadge(t) {
|
|
|
1349
1426
|
? '<span style="color:#8a6fe8;font-size:11px;margin-right:4px" title="在上一任务的会话中继续执行(同进程续聊)">↪ 续聊</span>'
|
|
1350
1427
|
: '<span style="color:#0a742e;font-size:11px;margin-right:4px" title="与相邻并行任务同时执行(独立进程)">⚡ 并行</span>';
|
|
1351
1428
|
}
|
|
1429
|
+
// Git 隔离徽标与改动按钮
|
|
1430
|
+
function wtBadge(t) {
|
|
1431
|
+
return t.worktree ? '<span style="color:#0a742e;font-size:11px;margin-right:4px" title="Git 隔离执行(独立 worktree 分支)">🌿</span>' : '';
|
|
1432
|
+
}
|
|
1433
|
+
function wtBtn(t) {
|
|
1434
|
+
return (t.worktree && (t.status === 'done' || t.status === 'failed'))
|
|
1435
|
+
? `<button class="mini-btn" title="查看本任务在隔离区的 Git 改动(可合并/丢弃)" onclick="event.stopPropagation();openTaskDiff('${escAttr(t.id)}')">改动</button>`
|
|
1436
|
+
: '';
|
|
1437
|
+
}
|
|
1352
1438
|
function renderTasks() {
|
|
1353
1439
|
const el = document.getElementById('taskList');
|
|
1354
1440
|
const badge = document.getElementById('pendingBadge');
|
|
@@ -1371,10 +1457,11 @@ function renderTasks() {
|
|
|
1371
1457
|
<div class="task-avatar st-${t.status === 'pending' ? 'scheduled' : t.status}">${icon}</div>
|
|
1372
1458
|
<div class="task-main">
|
|
1373
1459
|
<div class="task-title">${esc(t.title)}</div>
|
|
1374
|
-
<div class="task-sub"><span>${linkBadge(t)}${st}</span></div>
|
|
1460
|
+
<div class="task-sub"><span>${linkBadge(t)}${wtBadge(t)}${st}</span></div>
|
|
1375
1461
|
</div>
|
|
1376
1462
|
${t.status === 'pending' ? `<button class="mini-btn" title="不再等到点,立即执行" onclick="event.stopPropagation();runSchedNow('${escAttr(t.id)}')">立即执行</button>` : ''}
|
|
1377
1463
|
${t.status === 'failed' ? `<button class="mini-btn" title="重新执行" onclick="event.stopPropagation();runSchedNow('${escAttr(t.id)}')">重跑</button>` : ''}
|
|
1464
|
+
${wtBtn(t)}
|
|
1378
1465
|
<button class="mini-btn danger" title="删除任务及其会话" onclick="event.stopPropagation();delTask('${escAttr(t.id)}')">✕</button>
|
|
1379
1466
|
</div>`;
|
|
1380
1467
|
}).join('') : '<div class="task-empty">暂无定时任务<br>点上方「导入任务」→「定时任务」标签添加</div>';
|
|
@@ -1390,9 +1477,10 @@ function renderTasks() {
|
|
|
1390
1477
|
<div class="task-avatar st-${t.status}">${icon}</div>
|
|
1391
1478
|
<div class="task-main">
|
|
1392
1479
|
<div class="task-title">${esc(t.title)}</div>
|
|
1393
|
-
<div class="task-sub"><span>${linkBadge(t)}${statusText(t.status)}</span><span>${fmtTime(t.createdAt)}</span></div>
|
|
1480
|
+
<div class="task-sub"><span>${linkBadge(t)}${wtBadge(t)}${statusText(t.status)}</span><span>${fmtTime(t.createdAt)}</span></div>
|
|
1394
1481
|
</div>
|
|
1395
1482
|
${t.status === 'pending' || t.status === 'failed' ? `<button class="mini-btn" onclick="event.stopPropagation();runTask('${escAttr(t.id)}')">执行</button>` : ''}
|
|
1483
|
+
${wtBtn(t)}
|
|
1396
1484
|
<button class="mini-btn danger" title="删除任务及其会话" onclick="event.stopPropagation();delTask('${escAttr(t.id)}')">✕</button>
|
|
1397
1485
|
</div>`;
|
|
1398
1486
|
}).join('') : '<div class="task-empty">暂无顺序任务<br>点上方「导入任务」添加<br>(单聊任务由 OpenCode 单体执行)</div>';
|
|
@@ -1422,10 +1510,11 @@ function renderTasks() {
|
|
|
1422
1510
|
<div class="task-avatar st-${t.status === 'pending' ? 'scheduled' : t.status}">${icon}</div>
|
|
1423
1511
|
<div class="task-main">
|
|
1424
1512
|
<div class="task-title">${esc(t.title)}</div>
|
|
1425
|
-
<div class="task-sub"><span>${st}${assign}</span></div>
|
|
1513
|
+
<div class="task-sub"><span>${wtBadge(t)}${st}${assign}</span></div>
|
|
1426
1514
|
</div>
|
|
1427
1515
|
${t.status === 'pending' ? `<button class="mini-btn" title="不再等到点,立即执行" onclick="event.stopPropagation();runSchedNow('${escAttr(t.id)}')">立即执行</button>` : ''}
|
|
1428
1516
|
${t.status === 'failed' ? `<button class="mini-btn" title="重新执行" onclick="event.stopPropagation();runSchedNow('${escAttr(t.id)}')">重跑</button>` : ''}
|
|
1517
|
+
${wtBtn(t)}
|
|
1429
1518
|
<button class="mini-btn danger" title="删除任务及其会话" onclick="event.stopPropagation();delTask('${escAttr(t.id)}')">✕</button>
|
|
1430
1519
|
</div>`;
|
|
1431
1520
|
}).join('');
|
|
@@ -1452,9 +1541,10 @@ function renderTasks() {
|
|
|
1452
1541
|
<div class="task-avatar st-${t.status}">${icon}</div>
|
|
1453
1542
|
<div class="task-main">
|
|
1454
1543
|
<div class="task-title">${esc(t.title)}</div>
|
|
1455
|
-
<div class="task-sub"><span>${statusText(t.status)}${assign}</span><span>${fmtTime(t.createdAt)}</span></div>
|
|
1544
|
+
<div class="task-sub"><span>${wtBadge(t)}${statusText(t.status)}${assign}</span><span>${fmtTime(t.createdAt)}</span></div>
|
|
1456
1545
|
</div>
|
|
1457
1546
|
${t.status === 'pending' || t.status === 'failed' ? `<button class="mini-btn" onclick="event.stopPropagation();runTask('${escAttr(t.id)}')">执行</button>` : ''}
|
|
1547
|
+
${wtBtn(t)}
|
|
1458
1548
|
<button class="mini-btn danger" title="删除任务及其会话" onclick="event.stopPropagation();delTask('${escAttr(t.id)}')">✕</button>
|
|
1459
1549
|
</div>`;
|
|
1460
1550
|
}).join('');
|
|
@@ -1554,6 +1644,16 @@ function lastReportText(taskId) {
|
|
|
1554
1644
|
|
|
1555
1645
|
// 右下角弹窗:显示最终回答部分内容,点击跳回原会话;不处理 3 秒后自动消失(保留绿点)
|
|
1556
1646
|
function showDoneToast(taskId, title) {
|
|
1647
|
+
// 系统级通知:开关开启且页面在后台时弹系统通知(人不在电脑前也能收到)
|
|
1648
|
+
try {
|
|
1649
|
+
if (localStorage.getItem('sysNotify') === '1' && document.hidden && typeof Notification !== 'undefined' && Notification.permission === 'granted') {
|
|
1650
|
+
const n = new Notification('✅ 任务完成:' + (title || ''), {
|
|
1651
|
+
body: (lastReportText(taskId) || '点击查看结果').slice(0, 120),
|
|
1652
|
+
tag: 'agents-chat-done'
|
|
1653
|
+
});
|
|
1654
|
+
n.onclick = () => { window.focus(); n.close(); };
|
|
1655
|
+
}
|
|
1656
|
+
} catch { /* ignore */ }
|
|
1557
1657
|
const key = sessKey(taskId || '');
|
|
1558
1658
|
if (key === curKey() || (chatMode === 'solo' && key === curOcId)) return; // 用户正在看该会话,无需提醒
|
|
1559
1659
|
markUnread(key);
|
|
@@ -2287,9 +2387,75 @@ async function openHistory() {
|
|
|
2287
2387
|
}
|
|
2288
2388
|
document.getElementById('historyMask').classList.add('show');
|
|
2289
2389
|
el.scrollTop = 0;
|
|
2390
|
+
loadUsageBar();
|
|
2290
2391
|
}
|
|
2291
2392
|
function closeHistory() { document.getElementById('historyMask').classList.remove('show'); }
|
|
2292
2393
|
|
|
2394
|
+
// ---------- 全历史搜索(服务端逐分片匹配,含任务/单聊会话) ----------
|
|
2395
|
+
let histSearchTimer = null;
|
|
2396
|
+
document.getElementById('histSearch').addEventListener('input', (e) => {
|
|
2397
|
+
clearTimeout(histSearchTimer);
|
|
2398
|
+
const q = e.target.value.trim();
|
|
2399
|
+
const box = document.getElementById('histSearchResult');
|
|
2400
|
+
if (!q) { box.style.display = 'none'; box.innerHTML = ''; return; }
|
|
2401
|
+
histSearchTimer = setTimeout(async () => {
|
|
2402
|
+
try {
|
|
2403
|
+
const data = await api('/api/search?q=' + encodeURIComponent(q) + '&limit=50');
|
|
2404
|
+
const list = data.results || [];
|
|
2405
|
+
box.style.display = 'block';
|
|
2406
|
+
box.innerHTML = list.length
|
|
2407
|
+
? `<div class="tip" style="margin:0 0 4px">找到 ${list.length} 条匹配${list.length >= 50 ? '(已达上限,可加精确关键词)' : ''}</div>` +
|
|
2408
|
+
list.map(r => {
|
|
2409
|
+
const sess = r.sessionId === '' ? '主会话' : `会话 ${esc(r.sessionId.slice(0, 12))}`;
|
|
2410
|
+
const who = r.role === 'user' ? '👤 我' : esc(r.agentName || 'AI');
|
|
2411
|
+
return `<div class="hist-hit" onclick="jumpToSearchSess('${esc(r.sessionId)}')" title="点击定位该会话">
|
|
2412
|
+
<div class="hh-meta">${esc((r.timestamp || '').replace('T', ' ').slice(0, 16))} · ${sess} · ${who}</div>
|
|
2413
|
+
<div class="hh-snippet">${esc(r.snippet)}</div></div>`;
|
|
2414
|
+
}).join('')
|
|
2415
|
+
: '<div class="tip">没有匹配的消息</div>';
|
|
2416
|
+
} catch { box.innerHTML = '<div class="tip">搜索失败</div>'; box.style.display = 'block'; }
|
|
2417
|
+
}, 300);
|
|
2418
|
+
});
|
|
2419
|
+
function jumpToSearchSess(sessId) {
|
|
2420
|
+
// 主会话结果留在历史弹窗(消息就在下方);其余会话提示定位方式
|
|
2421
|
+
if (!sessId) { toast('该消息在主会话中,见下方历史记录'); return; }
|
|
2422
|
+
toast('该消息位于会话 ' + sessId.slice(0, 16) + '…:任务消息在任务列表点击对应任务查看,单聊在左侧会话列表选择');
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
// ---------- 用量统计与空间管理 ----------
|
|
2426
|
+
async function loadUsageBar() {
|
|
2427
|
+
try {
|
|
2428
|
+
const d = await api('/api/usage');
|
|
2429
|
+
const u = d.usage || {};
|
|
2430
|
+
const fmt = (n) => n >= 10000 ? (n / 10000).toFixed(1) + '万' : String(n || 0);
|
|
2431
|
+
document.getElementById('histUsage').textContent =
|
|
2432
|
+
`📊 Token 用量:今日 ${fmt(u.today && u.today.tokens)}(${fmt(u.today && u.today.requests)} 次请求)· 累计 ${fmt(u.total && u.total.tokens)}${(!u.total || !u.total.tokens) ? '(内核输出未携带用量数据时不统计)' : ''}`;
|
|
2433
|
+
} catch { /* ignore */ }
|
|
2434
|
+
}
|
|
2435
|
+
function fmtSize(bytes) {
|
|
2436
|
+
const b = Number(bytes) || 0;
|
|
2437
|
+
if (b >= 1024 * 1024 * 1024) return (b / 1024 / 1024 / 1024).toFixed(2) + ' GB';
|
|
2438
|
+
if (b >= 1024 * 1024) return (b / 1024 / 1024).toFixed(1) + ' MB';
|
|
2439
|
+
if (b >= 1024) return (b / 1024).toFixed(1) + ' KB';
|
|
2440
|
+
return b + ' B';
|
|
2441
|
+
}
|
|
2442
|
+
async function showDataStats() {
|
|
2443
|
+
try {
|
|
2444
|
+
const d = await api('/api/data/stats');
|
|
2445
|
+
const s = d.stats || {};
|
|
2446
|
+
toast(`数据目录共 ${fmtSize(s.total)}:消息 ${fmtSize(s.messages)} · 产出存档 ${fmtSize(s.outputs)} · 工作区 ${fmtSize(s.workspace)} · 流转日志 ${fmtSize(s.flow)}`, 5000);
|
|
2447
|
+
} catch { toast('查询失败'); }
|
|
2448
|
+
}
|
|
2449
|
+
async function pruneDataNow() {
|
|
2450
|
+
if (!confirm('立即执行一次滚动清理?规则与每日自动清理相同:过期已完结任务、过期会话、超期产出存档等(默认保留 15 天,任务列表中的未执行任务不受影响)。')) return;
|
|
2451
|
+
try {
|
|
2452
|
+
const r = await api('/api/data/prune', { method: 'POST' });
|
|
2453
|
+
const s = r.stat || {};
|
|
2454
|
+
toast(`清理完成:任务 ${s.tasks || 0} · 会话 ${s.ocSessions || 0} · 消息 ${s.messages || 0} · 产出目录 ${s.outputs || 0} · 流转日志 ${s.flowEvents || 0}`, 5000);
|
|
2455
|
+
openHistory();
|
|
2456
|
+
} catch { toast('清理失败'); }
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2293
2459
|
// ---------- 历史管理:一键导出 sessions.md / 一键清空全部会话 ----------
|
|
2294
2460
|
async function exportHistory() {
|
|
2295
2461
|
try {
|
|
@@ -2442,11 +2608,17 @@ async function doImport() {
|
|
|
2442
2608
|
// 单聊导入时记录当前所选模型,定时触发时用它执行(页面关闭也不丢)
|
|
2443
2609
|
const data = await api('/api/tasks/import', {
|
|
2444
2610
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
2445
|
-
body: JSON.stringify({
|
|
2611
|
+
body: JSON.stringify({
|
|
2612
|
+
text, mode: importMode, runner: chatMode === 'solo' ? 'solo' : '', model: chatMode === 'solo' ? (curOcModel || '') : '',
|
|
2613
|
+
isolated: !!(document.getElementById('importIsolated') || {}).checked
|
|
2614
|
+
})
|
|
2446
2615
|
});
|
|
2447
2616
|
if (data.success) {
|
|
2448
2617
|
tasks = data.tasks || [];
|
|
2449
2618
|
renderSidebar();
|
|
2619
|
+
if (data.isolated > 0) {
|
|
2620
|
+
(chatMode === 'solo' ? addSoloSys : addSys)(`🌿 ${data.isolated} 个任务将在 Git 隔离区执行(独立分支,互不污染)`);
|
|
2621
|
+
}
|
|
2450
2622
|
if (chatMode === 'solo') {
|
|
2451
2623
|
addSoloSys(importMode === 'scheduled'
|
|
2452
2624
|
? `已添加 ${data.added} 个单聊定时任务(到点由 OpenCode 自动执行)`
|
|
@@ -2462,6 +2634,65 @@ async function doImport() {
|
|
|
2462
2634
|
} else toast(data.error || '导入失败');
|
|
2463
2635
|
}
|
|
2464
2636
|
|
|
2637
|
+
// ---------- Git 隔离区改动查看 / 合并 / 丢弃 ----------
|
|
2638
|
+
let diffTask = null; // 当前弹窗对应的任务
|
|
2639
|
+
function escDiffLine(l) {
|
|
2640
|
+
const e = esc(l);
|
|
2641
|
+
if (l.startsWith('+')) return `<div class="dl add">${e}</div>`;
|
|
2642
|
+
if (l.startsWith('-')) return `<div class="dl del">${e}</div>`;
|
|
2643
|
+
if (l.startsWith('@@')) return `<div class="dl hun">${e}</div>`;
|
|
2644
|
+
return `<div class="dl">${e}</div>`;
|
|
2645
|
+
}
|
|
2646
|
+
async function openTaskDiff(id) {
|
|
2647
|
+
const t = tasks.find(x => x.id === id);
|
|
2648
|
+
if (!t) return;
|
|
2649
|
+
document.getElementById('diffTitle').textContent = `🌿 改动:${t.title.slice(0, 30)}`;
|
|
2650
|
+
document.getElementById('diffMeta').textContent = '加载中…';
|
|
2651
|
+
document.getElementById('diffBody').innerHTML = '';
|
|
2652
|
+
document.getElementById('diffMask').classList.add('show');
|
|
2653
|
+
diffTask = t;
|
|
2654
|
+
const data = await api('/api/tasks/diff?id=' + encodeURIComponent(id));
|
|
2655
|
+
if (!data.success) { document.getElementById('diffMeta').textContent = data.error || '加载失败'; return; }
|
|
2656
|
+
const d = data.diff;
|
|
2657
|
+
const addAll = d.stat.reduce((a, s) => a + (s.add || 0), 0);
|
|
2658
|
+
const delAll = d.stat.reduce((a, s) => a + (s.del || 0), 0);
|
|
2659
|
+
const merged = t.mergedAt ? '(已合并过)' : '';
|
|
2660
|
+
document.getElementById('diffMeta').innerHTML =
|
|
2661
|
+
`分支 ${esc(d.branch)}${merged} · ${d.stat.length} 个文件 · <span style="color:#0a742e">+${addAll}</span> / <span style="color:#c0392b">-${delAll}</span>${d.truncated ? '(内容过长已截断)' : ''}`;
|
|
2662
|
+
if (!d.stat.length) {
|
|
2663
|
+
document.getElementById('diffBody').innerHTML = '<div style="color:#666;padding:12px">隔离区没有任何改动</div>';
|
|
2664
|
+
return;
|
|
2665
|
+
}
|
|
2666
|
+
const byFile = Object.fromEntries((d.files || []).map(f => [f.path, f.patch]));
|
|
2667
|
+
document.getElementById('diffBody').innerHTML = d.stat.map(s => `
|
|
2668
|
+
<details ${d.stat.length <= 5 ? 'open' : ''} style="margin-bottom:8px">
|
|
2669
|
+
<summary style="cursor:pointer;font-size:13px;padding:4px 6px;background:#eef1f5;border-radius:6px">
|
|
2670
|
+
<b>${esc(s.file)}</b>
|
|
2671
|
+
<span style="color:#0a742e">+${s.add == null ? '二进制' : s.add}</span>
|
|
2672
|
+
<span style="color:#c0392b">-${s.del == null ? '' : s.del}</span>
|
|
2673
|
+
</summary>
|
|
2674
|
+
<pre class="diff-pre">${(byFile[s.file] || '').split('\n').map(escDiffLine).join('')}</pre>
|
|
2675
|
+
</details>`).join('');
|
|
2676
|
+
}
|
|
2677
|
+
function closeTaskDiff() {
|
|
2678
|
+
document.getElementById('diffMask').classList.remove('show');
|
|
2679
|
+
diffTask = null;
|
|
2680
|
+
}
|
|
2681
|
+
async function mergeTaskDiff() {
|
|
2682
|
+
if (!diffTask) return;
|
|
2683
|
+
if (!confirm(`把「${diffTask.title.slice(0, 30)}」的隔离区改动合并到主工作目录?\n(未提交改动会先自动提交到隔离分支,再在主目录执行 git merge)`)) return;
|
|
2684
|
+
const data = await api('/api/tasks/merge', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: diffTask.id }) });
|
|
2685
|
+
if (data.success) { toast(data.already ? '隔离区改动此前已合并' : '已合并到主工作目录'); closeTaskDiff(); loadTasks(); }
|
|
2686
|
+
else toast(data.error || '合并失败');
|
|
2687
|
+
}
|
|
2688
|
+
async function discardTaskDiff() {
|
|
2689
|
+
if (!diffTask) return;
|
|
2690
|
+
if (!confirm(`丢弃「${diffTask.title.slice(0, 30)}」的隔离区?\n(删除独立 worktree 与分支,其中未合并的改动将永久丢失;任务本身保留,可重新执行)`)) return;
|
|
2691
|
+
const data = await api('/api/tasks/discard', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: diffTask.id }) });
|
|
2692
|
+
if (data.success) { toast('隔离区已丢弃'); closeTaskDiff(); loadTasks(); }
|
|
2693
|
+
else toast(data.error || '丢弃失败');
|
|
2694
|
+
}
|
|
2695
|
+
|
|
2465
2696
|
async function clearTasks() {
|
|
2466
2697
|
if (!confirm('确定清空全部任务?(任务会话的对话记录也会从列表移除)')) return;
|
|
2467
2698
|
await api('/api/tasks/clear', { method: 'POST' });
|
|
@@ -3378,8 +3609,24 @@ function openHelp() {
|
|
|
3378
3609
|
</div>
|
|
3379
3610
|
<div class="help-sec"><h4>🕘 历史与数据管理</h4>
|
|
3380
3611
|
<ul>
|
|
3612
|
+
<li>「🕘 历史」弹窗顶部可<b>全历史搜索</b>:关键词匹配全部会话消息(含任务/单聊会话),点击结果定位所属会话</li>
|
|
3613
|
+
<li>弹窗内实时显示 <b>Token 用量</b>(今日/累计,内核输出携带用量数据时统计);「📊 空间占用」查看数据目录分布,「🧹 清理旧数据」手动执行一次滚动清理</li>
|
|
3381
3614
|
<li>「🕘 历史」弹窗底部:<b>⬇ 导出 sessions.md</b> 把全部会话(含任务会话与单聊会话)导出为一个 Markdown 文件;<b>🗑 清空全部会话</b> 一键删除消息、单聊会话、会话产出文件与流转日志(任务列表保留)</li>
|
|
3382
|
-
<li>全部页面关闭且无任务执行/审批等待时,后台服务空闲约 1
|
|
3615
|
+
<li>全部页面关闭且无任务执行/审批等待时,后台服务空闲约 1 分钟自动退出(需要定时任务到点执行请保持页面打开,或运行 <code>agents-chat autostart on</code> 开机自启;<code>.env AGENTS_CHAT_AUTOSTOP=0</code> 可关闭),需要时重新运行 start 即可</li>
|
|
3616
|
+
</ul>
|
|
3617
|
+
</div>
|
|
3618
|
+
<div class="help-sec"><h4>🌿 Git 隔离执行(worktree)</h4>
|
|
3619
|
+
<ul>
|
|
3620
|
+
<li>导入任务时勾选「Git 隔离执行」:每个任务在<b>独立 Git worktree 分支</b>中执行,多任务并行互不污染,失败的尝试可整体丢弃(统一工作目录需是 Git 仓库且至少有一个提交)</li>
|
|
3621
|
+
<li>任务完成后任务条出现「改动」按钮:<b>逐文件查看 diff</b>(新增/删除/修改着色显示),确认后「合并到主目录」(自动提交隔离分支并 git merge,冲突会还原主目录并提示)或「丢弃隔离区」(删除该 worktree 与分支)</li>
|
|
3622
|
+
<li>隔离区随数据维护自动治理:任务删除或完结超 15 天的 worktree 自动清理</li>
|
|
3623
|
+
</ul>
|
|
3624
|
+
</div>
|
|
3625
|
+
<div class="help-sec"><h4>🔔 完成通知</h4>
|
|
3626
|
+
<ul>
|
|
3627
|
+
<li>顶栏「🔔 通知」开启后(需浏览器授权),页面在后台时任务/卡牌完成会弹<b>系统级通知</b>,人不在电脑前也不错过</li>
|
|
3628
|
+
<li>进阶:.env 配置 <code>AGENTS_CHAT_WEBHOOK_URL</code> 可把完成事件转发到钉钉/飞书/自建中转(POST JSON,含标题/状态/摘要)</li>
|
|
3629
|
+
<li>定时任务抗机器重启:命令行 <code>agents-chat autostart on</code> 设置开机自启;停机期间错过的定时任务会在下次启动时自动补跑(控制台有日志说明)</li>
|
|
3383
3630
|
</ul>
|
|
3384
3631
|
</div>
|
|
3385
3632
|
<div class="help-sec"><h4>🛂 协作通道(看板 / 委派 / 审批)</h4>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Agents Chat",
|
|
3
|
+
"short_name": "AgentsChat",
|
|
4
|
+
"description": "多智能体协作聊天与任务编排",
|
|
5
|
+
"start_url": "/",
|
|
6
|
+
"display": "standalone",
|
|
7
|
+
"background_color": "#f5f5f5",
|
|
8
|
+
"theme_color": "#07c160",
|
|
9
|
+
"icons": [
|
|
10
|
+
{
|
|
11
|
+
"src": "/static/icon.svg",
|
|
12
|
+
"sizes": "any",
|
|
13
|
+
"type": "image/svg+xml",
|
|
14
|
+
"purpose": "any"
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
}
|