@sanqianx/ai-coding-event-bridge-console 0.4.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.
@@ -0,0 +1,921 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>ai-coding-event-bridge · 会话浏览器</title>
7
+ <style>
8
+ /* 设计 token 与 vector-hub 控制台一致(ZCode 风格灰阶四层 + 单一蓝 + 语义红绿) */
9
+ :root {
10
+ --bg-side: #1b1c1f;
11
+ --bg-main: #212226;
12
+ --bg-card: #2a2b30;
13
+ --bg-input: #333439;
14
+ --bg-hover: #303237;
15
+ --bg-active: #36383e;
16
+ --line: #3a3b40;
17
+ --line-soft: #2e2f34;
18
+ --text: #e8e8ea;
19
+ --text-2: #a0a1a5;
20
+ --text-3: #6b6c70;
21
+ --label: #8a8b90;
22
+ --blue: #3b82f6;
23
+ --blue-soft: rgba(59,130,246,.12);
24
+ --green: #34d399;
25
+ --red: #e5695e;
26
+ --mono: Consolas, "Microsoft YaHei", monospace;
27
+ }
28
+ * { box-sizing: border-box; margin: 0; padding: 0; transition: background-color .12s ease, color .12s ease, border-color .12s ease, opacity .12s ease; }
29
+ html, body { height: 100%; }
30
+ body {
31
+ background: var(--bg-main); color: var(--text);
32
+ font: 13px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft YaHei", sans-serif;
33
+ overflow: hidden; display: flex; flex-direction: column;
34
+ color-scheme: dark;
35
+ }
36
+ ::-webkit-scrollbar { width: 10px; height: 10px; }
37
+ ::-webkit-scrollbar-track { background: transparent; }
38
+ ::-webkit-scrollbar-thumb { background: #45464b; border-radius: 99px; border: 3px solid transparent; background-clip: content-box; }
39
+ button { font: inherit; color: inherit; background: none; border: none; cursor: pointer; }
40
+ .svg-i { fill: none; stroke: currentColor; stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; vertical-align: -3px; }
41
+ *:focus-visible { outline: 1px solid var(--blue); outline-offset: 1px; border-radius: 6px; }
42
+ @media (prefers-reduced-motion: reduce) { * { transition: none !important; animation: none !important; } }
43
+
44
+ #app { flex: 1; display: flex; min-height: 0; }
45
+ aside { width: 248px; min-width: 248px; background: var(--bg-side); border-right: 1px solid var(--line-soft); display: flex; flex-direction: column; }
46
+ /* 嵌入壳(?embed=1)时由 kb:sidebar 控制项目侧栏的显隐 */
47
+ body.embed-collapse aside { display: none; }
48
+ .brand { display: flex; align-items: center; gap: 8px; padding: 14px 14px 10px; }
49
+ .brand .mark { width: 22px; height: 22px; border-radius: 6px; background: var(--text); color: #16171a; display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 12px; }
50
+ .brand .name { font-size: 13px; font-weight: 600; }
51
+ .brand .name em { font-style: normal; color: var(--text-3); font-weight: 400; }
52
+
53
+ .side-scroll { flex: 1; overflow-y: auto; padding: 0 8px 8px; }
54
+ .proj-filter { display: flex; align-items: center; gap: 6px; height: 28px; background: var(--bg-input); border-radius: 7px; padding: 0 8px; color: var(--text-3); margin: 2px 0 4px; }
55
+ .proj-filter input { flex: 1; border: none; background: none; outline: none; color: var(--text); font-size: 12px; min-width: 0; }
56
+ .proj-filter input::placeholder { color: var(--text-3); }
57
+
58
+ .group-label { font-size: 11px; color: var(--label); letter-spacing: .05em; padding: 8px 8px 6px; display: flex; align-items: center; gap: 6px; }
59
+ .group-label .sel { color: var(--text-3); font-family: var(--mono); font-weight: 400; }
60
+ .group-label .grow { flex: 1; }
61
+
62
+ .proj { display: flex; align-items: center; gap: 8px; height: 30px; padding: 0 8px; border-radius: 6px; color: var(--text-2); cursor: pointer; user-select: none; }
63
+ .proj:hover { background: var(--bg-hover); color: var(--text); }
64
+ .proj.on { background: var(--bg-active); color: var(--text); }
65
+ .proj .cb { width: 14px; height: 14px; border-radius: 4px; border: 1px solid var(--line); flex-shrink: 0; display: flex; align-items: center; justify-content: center; color: #16171a; }
66
+ .proj.on .cb { background: var(--text); border-color: var(--text); }
67
+ .proj .cb .svg-i { display: none; }
68
+ .proj.on .cb .svg-i { display: block; }
69
+ .proj .pn { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12.5px; }
70
+ .proj .auto-tag { font-size: 10px; font-family: var(--mono); color: var(--text-3); border: 1px solid var(--line); border-radius: 99px; padding: 0 5px; flex-shrink: 0; }
71
+ .proj .cnt { font-size: 11px; font-family: var(--mono); color: var(--text-3); flex-shrink: 0; }
72
+ .proj .del { opacity: 0; width: 22px; height: 22px; border-radius: 6px; display: flex; align-items: center; justify-content: center; color: var(--text-3); flex-shrink: 0; }
73
+ .proj:hover .del { opacity: 1; }
74
+ .proj .del:hover { color: var(--red); background: rgba(229,105,94,.1); }
75
+
76
+ .import-row { margin-top: 6px; height: 32px; border: 1px dashed var(--line); border-radius: 6px; display: flex; align-items: center; justify-content: center; gap: 6px; color: var(--text-3); font-size: 12px; cursor: pointer; width: 100%; }
77
+ .import-row:hover { color: var(--text); border-color: var(--text-3); }
78
+
79
+ .side-foot { padding: 10px 14px; display: flex; align-items: center; gap: 8px; }
80
+ .side-foot .dot { width: 6px; height: 6px; border-radius: 99px; background: var(--green); flex-shrink: 0; }
81
+ .side-foot .dot.off { background: var(--text-2); }
82
+ .side-foot .st { font-size: 11px; color: var(--text-3); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
83
+
84
+ main { flex: 1; display: flex; flex-direction: column; min-width: 0; }
85
+ .topbar { display: flex; align-items: center; gap: 10px; padding: 10px 20px; border-bottom: 1px solid var(--line-soft); flex-shrink: 0; }
86
+ .topbar h1 { font-size: 14px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
87
+ .topbar .grow { flex: 1; }
88
+ .icon-btn { width: 26px; height: 26px; border-radius: 6px; display: inline-flex; align-items: center; justify-content: center; color: var(--text-2); flex-shrink: 0; }
89
+ .icon-btn:hover { background: var(--bg-hover); color: var(--text); }
90
+ .back-btn { height: 26px; padding: 0 12px; border-radius: 99px; background: var(--bg-card); color: var(--text-2); font-size: 12px; display: none; align-items: center; gap: 6px; flex-shrink: 0; }
91
+ .back-btn:hover { color: var(--text); }
92
+ .back-btn.show { display: inline-flex; }
93
+
94
+ /* 顶栏右侧:折叠的历史入口 */
95
+ .hist-wrap { position: relative; flex-shrink: 0; }
96
+ .hist-btn { height: 28px; padding: 0 12px; border-radius: 99px; background: var(--bg-card); border: 1px solid var(--line-soft); color: var(--text-2); font-size: 12px; display: inline-flex; align-items: center; gap: 7px; }
97
+ .hist-btn:hover { color: var(--text); border-color: var(--line); }
98
+ .hist-btn.on { color: var(--text); border-color: var(--blue); background: var(--blue-soft); }
99
+ .hist-pop { display: none; position: absolute; right: 0; top: calc(100% + 8px); width: 400px; max-height: 420px; overflow-y: auto; background: var(--bg-card); border: 1px solid var(--line); border-radius: 12px; padding: 6px; z-index: 40; box-shadow: 0 16px 40px rgba(0,0,0,.5); }
100
+ .hist-pop.open { display: block; }
101
+ .hist-pop .ph { font-size: 11px; color: var(--label); letter-spacing: .05em; padding: 6px 10px 4px; }
102
+ .hist-pop .pnote { font-size: 10.5px; color: var(--text-3); padding: 6px 10px 8px; line-height: 1.5; }
103
+ .citem { width: 100%; display: flex; flex-direction: column; gap: 1px; padding: 7px 10px; border-radius: 8px; text-align: left; color: var(--text-2); }
104
+ .citem:hover { background: var(--bg-hover); }
105
+ .citem.on { background: var(--bg-active); color: var(--text); }
106
+ .citem .r1 { display: flex; align-items: center; gap: 6px; }
107
+ .citem .sha { font-family: var(--mono); font-size: 11.5px; color: var(--blue); }
108
+ .citem .dt { font-family: var(--mono); font-size: 10px; color: var(--text-3); margin-left: auto; }
109
+ .citem .sb { font-size: 11.5px; color: var(--text-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
110
+ .citem.on .sb, .citem:hover .sb { color: var(--text-2); }
111
+
112
+ /* 视图 */
113
+ .view { flex: 1; overflow-y: auto; padding: 16px 20px 40px; display: none; }
114
+ .view.on { display: block; }
115
+ .flow { max-width: 760px; margin: 0 auto; display: flex; flex-direction: column; }
116
+ .sep { display: flex; align-items: center; gap: 12px; margin: 10px 0 14px; }
117
+ .sep::before, .sep::after { content: ''; flex: 1; height: 1px; background: var(--line); }
118
+ .sep-capsule { display: inline-flex; align-items: center; padding: 3px 14px; border-radius: 99px; background: var(--bg-card); border: 1px solid var(--line); font-size: 11px; color: var(--text-3); }
119
+ .msg-u { align-self: flex-end; max-width: 76%; background: var(--bg-input); border: 1px solid var(--line-soft); border-radius: 14px 14px 4px 14px; padding: 9px 14px; font-size: 13px; font-weight: 500; white-space: pre-wrap; overflow-wrap: anywhere; margin: 3px 0; }
120
+ .msg-a-wrap { align-self: flex-start; width: 88%; margin: 3px 0; }
121
+ .msg-a-label { font-size: 10.5px; color: var(--text-3); margin: 0 0 3px 4px; font-family: var(--mono); }
122
+ .msg-a { background: var(--bg-card); border: 1px solid var(--line-soft); border-radius: 4px 14px 14px 14px; padding: 10px 14px; font-size: 12.5px; color: var(--text-2); line-height: 1.65; white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word; }
123
+ .sentinel { text-align: center; font-size: 11px; color: var(--text-3); padding: 12px 0; cursor: pointer; font-family: var(--mono); }
124
+ .sentinel:hover { color: var(--text-2); }
125
+
126
+ /* 历史文档视图 */
127
+ .doc-h { max-width: 760px; margin: 6px auto 4px; }
128
+ .doc-h h2 { font-size: 15px; font-weight: 600; line-height: 1.45; overflow-wrap: anywhere; }
129
+ .doc-h .sub { font-size: 11.5px; color: var(--text-3); font-family: var(--mono); margin-top: 3px; }
130
+ .doc-loading { max-width: 760px; margin: 30px auto; text-align: center; color: var(--text-3); font-family: var(--mono); font-size: 12px; }
131
+ .spin { display: inline-block; animation: sp 1s linear infinite; margin-right: 6px; }
132
+ @keyframes sp { to { transform: rotate(360deg); } }
133
+
134
+ .empty { max-width: 760px; margin: 40px auto 0; text-align: center; }
135
+ .empty .box { border: 1px dashed var(--line); border-radius: 10px; padding: 28px 24px; display: flex; flex-direction: column; align-items: center; gap: 6px; }
136
+ .empty .tile { width: 36px; height: 36px; border-radius: 9px; background: var(--bg-card); display: flex; align-items: center; justify-content: center; color: var(--text-3); }
137
+ .empty .t1 { font-size: 13px; }
138
+ .empty .t2 { font-size: 11px; color: var(--text-3); }
139
+
140
+ /* 导入项目弹窗 */
141
+ .modal-mask { display: none; position: fixed; inset: 0; background: rgba(0,0,0,.5); z-index: 50; align-items: center; justify-content: center; }
142
+ .modal-mask.open { display: flex; }
143
+ .modal { width: min(520px,92vw); max-height: calc(100vh - 80px); overflow-y: auto; border-radius: 14px; background: var(--bg-card); border: 1px solid var(--line); padding: 20px; }
144
+ .modal h3 { font-size: 14px; font-weight: 600; }
145
+ .modal .sub { font-size: 12px; color: var(--text-3); margin: 2px 0 14px; }
146
+ .field { margin-bottom: 12px; }
147
+ .field label { display: block; font-size: 12px; color: var(--text-2); margin-bottom: 5px; }
148
+ .field .fin { width: 100%; height: 32px; padding: 0 10px; font: inherit; font-family: var(--mono); font-size: 12px; background: var(--bg-input); border: none; border-radius: 8px; color: var(--text); }
149
+ .field .fin:focus { outline: 1px solid var(--blue); }
150
+ .field .note { font-size: 11px; color: var(--text-3); margin-top: 4px; line-height: 1.6; }
151
+ .field .note .mono { font-family: var(--mono); }
152
+ .input-with-btn { display: flex; gap: 6px; }
153
+ .input-with-btn .fin { flex: 1; min-width: 0; }
154
+ .modal-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 14px; }
155
+ .btn-sec { height: 30px; padding: 0 14px; border-radius: 8px; background: var(--bg-input); color: var(--text-2); font-size: 12.5px; flex-shrink: 0; }
156
+ .btn-sec:hover { color: var(--text); }
157
+ .btn-sec:disabled { opacity: .45; cursor: not-allowed; }
158
+ .btn-pri { height: 30px; padding: 0 16px; border-radius: 8px; background: var(--text); color: #16171a; font-size: 12.5px; font-weight: 600; }
159
+ .btn-pri:hover { background: #fff; }
160
+ .btn-pri:disabled { opacity: .45; cursor: not-allowed; }
161
+
162
+ /* toast */
163
+ #toasts { position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); display: flex; flex-direction: column; gap: 8px; z-index: 60; align-items: center; }
164
+ .toast { display: flex; align-items: center; gap: 8px; padding: 7px 14px; border-radius: 8px; font-size: 12.5px; background: var(--bg-card); border: 1px solid var(--line); color: var(--text-2); animation: t-in .14s ease; max-width: 80vw; }
165
+ .toast.out { animation: t-out .16s ease forwards; }
166
+ .toast .ok-ic { color: var(--green); flex-shrink: 0; }
167
+ .toast .bad-ic { color: var(--red); flex-shrink: 0; }
168
+ @keyframes t-in { from { opacity: 0; transform: translateY(6px); } }
169
+ @keyframes t-out { to { opacity: 0; transform: translateY(4px); } }
170
+
171
+ /* 设置弹窗 */
172
+ .set-section { font-size: 11px; color: var(--label); letter-spacing: .05em; border-top: 1px solid var(--line); padding-top: 12px; margin: 4px 0 10px; }
173
+ .modal .set-section:first-of-type { border-top: none; padding-top: 0; margin-top: 0; }
174
+ .agent-row { display: flex; align-items: center; gap: 8px; padding: 6px 0; }
175
+ .agent-row .an { font-size: 13px; font-weight: 500; min-width: 96px; }
176
+ .agent-row .note { font-size: 11px; color: var(--text-3); flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
177
+ .st-chip { font-size: 11px; padding: 1px 8px; border-radius: 99px; border: 1px solid var(--line); color: var(--text-2); flex-shrink: 0; }
178
+ .st-chip.ok { color: var(--green); border-color: rgba(52,211,153,.35); }
179
+ .st-chip.warn { color: var(--red); border-color: rgba(229,105,94,.35); }
180
+ .st-chip.off { color: var(--text-3); }
181
+ .kv-row { display: flex; align-items: center; gap: 8px; padding: 4px 0; font-size: 12.5px; color: var(--text-2); }
182
+ .kv-row .k { min-width: 96px; color: var(--text-3); font-size: 12px; }
183
+ .kv-row .v { font-family: var(--mono); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
184
+ .kv-row .grow { flex: 1; }
185
+ .btn-upg { height: 26px; padding: 0 12px; border-radius: 7px; background: var(--text); color: #16171a; font-size: 12px; font-weight: 600; flex-shrink: 0; }
186
+ .btn-upg:hover { background: #fff; }
187
+ .btn-upg:disabled { opacity: .45; cursor: not-allowed; }
188
+ </style>
189
+ </head>
190
+ <body>
191
+
192
+ <div id="app">
193
+ <aside>
194
+ <div class="brand">
195
+ <div class="mark">B</div>
196
+ <div class="name">event-bridge <em>会话浏览器</em></div>
197
+ </div>
198
+ <div class="side-scroll">
199
+ <div class="group-label">
200
+ <span>项目</span><span class="sel" id="proj-count"></span>
201
+ <span class="grow"></span>
202
+ </div>
203
+ <div class="proj-filter">
204
+ <span class="svg-i" data-i="search"></span>
205
+ <input id="proj-q" placeholder="过滤项目…">
206
+ </div>
207
+ <div id="proj-list"></div>
208
+ <div id="import-zone">
209
+ <button class="import-row" id="import-btn">
210
+ <span class="svg-i" data-i="plus"></span>导入项目
211
+ </button>
212
+ </div>
213
+ </div>
214
+ <div class="side-foot">
215
+ <span class="dot" id="foot-dot"></span>
216
+ <span class="st" id="foot-st">连接中…</span>
217
+ </div>
218
+ </aside>
219
+
220
+ <main>
221
+ <div class="topbar">
222
+ <button class="back-btn" id="back-btn">← 返回</button>
223
+ <h1 id="main-title">会话浏览器</h1>
224
+ <div class="grow"></div>
225
+ <button class="icon-btn" id="settings-btn" title="设置:本机 AI 工具与 Bridge 运行时">
226
+ <span class="svg-i" data-i="gear"></span>
227
+ </button>
228
+ <div class="hist-wrap">
229
+ <button class="hist-btn" id="hist-btn">🕘 历史</button>
230
+ <div class="hist-pop" id="hist-pop">
231
+ <div class="ph">历史对话</div>
232
+ <div id="hist-list"></div>
233
+ </div>
234
+ </div>
235
+ </div>
236
+
237
+ <div class="view on" id="view-live">
238
+ <div class="flow" id="live-flow"></div>
239
+ </div>
240
+
241
+ <div class="view" id="view-doc">
242
+ <div class="doc-loading" id="doc-loading" style="display:none"><span class="spin">◌</span>加载中…</div>
243
+ <div id="doc-content" style="display:none"></div>
244
+ </div>
245
+ </main>
246
+ </div>
247
+
248
+ <!-- 导入项目弹窗 -->
249
+ <div class="modal-mask" id="import-modal">
250
+ <div class="modal">
251
+ <h3>导入项目</h3>
252
+ <div class="sub">选择本地项目文件夹(Git 仓库),并指定该项目的对话记录保存地址</div>
253
+
254
+ <div class="field">
255
+ <label>项目文件夹</label>
256
+ <div class="input-with-btn">
257
+ <input class="fin" id="imp-path" placeholder="例如 D:\SanQian.Xu\my-project" spellcheck="false">
258
+ <button class="btn-sec" id="imp-path-browse">浏览…</button>
259
+ </div>
260
+ </div>
261
+
262
+ <div class="field">
263
+ <label>对话记录保存地址</label>
264
+ <div class="input-with-btn">
265
+ <input class="fin" id="imp-store" placeholder="留空使用默认地址(建议 knowledge\<项目>\dev-conversations)" spellcheck="false">
266
+ <button class="btn-sec" id="imp-store-browse">浏览…</button>
267
+ </div>
268
+ <div class="note">导入后,该项目里 Claude Code / Codex / OpenCode 的对话与提交边界会持久化到该地址;
269
+ 留空则使用默认 <span class="mono">&lt;bridge home&gt;/projects/&lt;项目名&gt;</span>。建议放到知识库目录下的
270
+ <span class="mono">dev-conversations</span> 文件夹,提交代码时会在此生成 <span class="mono">commits/&lt;sha&gt;.md</span> 封存文档。</div>
271
+ </div>
272
+
273
+ <div class="modal-actions">
274
+ <button class="btn-sec" id="imp-cancel">取消</button>
275
+ <button class="btn-pri" id="imp-ok">导入</button>
276
+ </div>
277
+ </div>
278
+ </div>
279
+
280
+ <div id="toasts"></div>
281
+
282
+ <!-- 设置弹窗 -->
283
+ <div class="modal-mask" id="settings-modal">
284
+ <div class="modal">
285
+ <h3>设置</h3>
286
+ <div class="sub">本机 AI 编程工具检测与 Bridge 运行时状态</div>
287
+ <div id="settings-body"><div class="t2" style="color:var(--text-3);font-size:12px">加载中…</div></div>
288
+ <div class="modal-actions">
289
+ <button class="btn-sec" id="settings-close">关闭</button>
290
+ </div>
291
+ </div>
292
+ </div>
293
+
294
+ <script>
295
+ 'use strict';
296
+
297
+ var ICONS = {
298
+ search: '<svg class="svg-i" width="12" height="12" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="M20 20l-4-4"/></svg>',
299
+ calendar: '<svg class="svg-i" width="15" height="15" viewBox="0 0 24 24"><rect x="3" y="5" width="18" height="16" rx="2"/><path d="M8 3v4M16 3v4M3 10h18"/></svg>',
300
+ check: '<svg class="svg-i" width="10" height="10" viewBox="0 0 24 24"><path d="M5 13l4 4L19 7"/></svg>',
301
+ inbox: '<svg class="svg-i" width="17" height="17" viewBox="0 0 24 24"><path d="M3 13h5l1 3h6l1-3h5"/><path d="M5 5h14l2 8v6H3v-6z"/></svg>',
302
+ plus: '<svg class="svg-i" width="13" height="13" viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></svg>',
303
+ gear: '<svg class="svg-i" width="15" height="15" viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33h.08a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51h.08a1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82v.08a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>',
304
+ x: '<svg class="svg-i" width="12" height="12" viewBox="0 0 24 24"><path d="M6 6l12 12M18 6L6 18"/></svg>',
305
+ ok: '<svg class="svg-i" width="14" height="14" viewBox="0 0 24 24"><path d="M5 13l4 4L19 7"/></svg>',
306
+ bad: '<svg class="svg-i" width="14" height="14" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 8v5M12 16v.5"/></svg>'
307
+ };
308
+ document.querySelectorAll('.svg-i[data-i]').forEach(function (el) {
309
+ el.outerHTML = ICONS[el.getAttribute('data-i')] || '';
310
+ });
311
+
312
+ /* ---------- 基础工具 ---------- */
313
+
314
+ function escapeText(value) {
315
+ return String(value === undefined || value === null ? '' : value)
316
+ .replace(/&/g, '&amp;')
317
+ .replace(/</g, '&lt;')
318
+ .replace(/>/g, '&gt;');
319
+ }
320
+ function escapeAttr(value) {
321
+ return escapeText(value).replace(/"/g, '&quot;');
322
+ }
323
+
324
+ function toast(ok, text) {
325
+ var el = document.createElement('div');
326
+ el.className = 'toast';
327
+ el.innerHTML = (ok ? '<span class="ok-ic">' + ICONS.ok + '</span>' : '<span class="bad-ic">' + ICONS.bad + '</span>')
328
+ + '<span>' + escapeText(text) + '</span>';
329
+ document.getElementById('toasts').appendChild(el);
330
+ setTimeout(function () {
331
+ el.classList.add('out');
332
+ setTimeout(function () { el.remove(); }, 200);
333
+ }, 3200);
334
+ }
335
+
336
+ function api(path, options) {
337
+ return fetch(path, options).then(function (res) {
338
+ return res.text().then(function (body) {
339
+ var payload = null;
340
+ try { payload = body ? JSON.parse(body) : null; } catch (_) { payload = null; }
341
+ if (!res.ok) {
342
+ var message = payload && payload.error && payload.error.message || ('HTTP ' + res.status);
343
+ var err = new Error(message);
344
+ err.status = res.status;
345
+ err.code = payload && payload.error && payload.error.code;
346
+ throw err;
347
+ }
348
+ return payload;
349
+ });
350
+ });
351
+ }
352
+
353
+ function todayStr(offsetDays) {
354
+ var d = new Date();
355
+ if (offsetDays) d.setDate(d.getDate() + offsetDays);
356
+ return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
357
+ }
358
+
359
+ function dayOf(iso) {
360
+ if (!iso) return '';
361
+ var d = new Date(iso);
362
+ if (Number.isNaN(d.getTime())) return '';
363
+ return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
364
+ }
365
+
366
+ function dayLabel(day) {
367
+ if (day === todayStr(0)) return '今天';
368
+ if (day === todayStr(-1)) return '昨天';
369
+ return day ? day.slice(5).replace('-', '/') : '';
370
+ }
371
+
372
+ function timeOf(iso) {
373
+ if (!iso) return '';
374
+ var d = new Date(iso);
375
+ if (Number.isNaN(d.getTime())) return '';
376
+ return String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0');
377
+ }
378
+
379
+ var SOURCE_LABELS = { 'claude-code': 'Claude', claude: 'Claude', codex: 'Codex', opencode: 'OpenCode' };
380
+
381
+ /* ---------- 状态 ---------- */
382
+
383
+ var state = {
384
+ projects: [],
385
+ project: null,
386
+ turns: [],
387
+ cursor: null,
388
+ totalTurns: 0,
389
+ loadingTurns: false,
390
+ filter: '',
391
+ commits: [],
392
+ currentDocFile: null
393
+ };
394
+
395
+ /* ---------- 渲染:侧栏 ---------- */
396
+
397
+ function visibleProjects() {
398
+ var q = state.filter.trim().toLowerCase();
399
+ return state.projects.filter(function (p) {
400
+ return !q || p.name.toLowerCase().indexOf(q) >= 0 || (p.path || '').toLowerCase().indexOf(q) >= 0;
401
+ });
402
+ }
403
+
404
+ function renderSidebar() {
405
+ var list = visibleProjects();
406
+ document.getElementById('proj-count').textContent = list.length ? '· ' + list.length : '';
407
+ document.getElementById('proj-list').innerHTML = list.map(function (p) {
408
+ var on = state.project && p.id === state.project.id;
409
+ var title = (p.path || p.id) + (p.auto ? '(未导入,仅浏览已捕获记录)' : '\n' + p.store);
410
+ return '<div class="proj' + (on ? ' on' : '') + '" data-proj="' + escapeAttr(p.id) + '" title="' + escapeAttr(title) + '">'
411
+ + '<span class="cb">' + ICONS.check + '</span>'
412
+ + '<span class="pn">' + escapeText(p.name) + '</span>'
413
+ + (p.auto ? '<span class="auto-tag">auto</span>' : '')
414
+ + '<span class="cnt">' + (p.turns || 0) + '</span>'
415
+ + (p.auto ? '' : '<button class="del" data-del="' + escapeAttr(p.id) + '" title="注销该项目(磁盘数据保留)">' + ICONS.x + '</button>')
416
+ + '</div>';
417
+ }).join('');
418
+ }
419
+
420
+ /* ---------- 渲染:当前对话(聊天流) ---------- */
421
+
422
+ function bubbleHtml(event, source) {
423
+ var label = SOURCE_LABELS[source] || source || 'AI';
424
+ var time = timeOf(event.capturedAt);
425
+ if (event.role === 'user') {
426
+ return '<div class="msg-u" title="' + escapeAttr(time) + '">' + escapeText(event.content) + '</div>';
427
+ }
428
+ return '<div class="msg-a-wrap"><div class="msg-a-label">' + escapeText(label) + (time ? ' · ' + escapeText(time) : '') + '</div>'
429
+ + '<div class="msg-a">' + escapeText(event.content) + '</div></div>';
430
+ }
431
+
432
+ function renderLive() {
433
+ var el = document.getElementById('live-flow');
434
+ if (!state.project) {
435
+ el.innerHTML = '<div class="empty"><div class="box"><div class="tile">' + ICONS.inbox + '</div>'
436
+ + '<div class="t1">先导入一个项目</div><div class="t2">左侧「导入项目」,选择本地 Git 仓库</div></div></div>';
437
+ return;
438
+ }
439
+ if (state.loadingTurns && !state.turns.length) {
440
+ el.innerHTML = '<div class="empty"><div class="t2" style="color:var(--text-3);font-size:12px">加载中…</div></div>';
441
+ return;
442
+ }
443
+ if (!state.turns.length) {
444
+ el.innerHTML = '<div class="empty"><div class="box"><div class="tile">' + ICONS.inbox + '</div>'
445
+ + '<div class="t1">还没有对话</div><div class="t2">在这个项目里和 AI 聊过后会出现在这里</div></div></div>';
446
+ return;
447
+ }
448
+ var html = '';
449
+ var prevDay = null;
450
+ state.turns.forEach(function (turn) {
451
+ var first = (turn.userEvents && turn.userEvents[0]) || (turn.assistantEvents && turn.assistantEvents[0]);
452
+ var day = dayOf(first && first.capturedAt);
453
+ if (day && day !== prevDay) {
454
+ html += '<div class="sep"><span class="sep-capsule">' + escapeText(dayLabel(day)) + '</span></div>';
455
+ prevDay = day;
456
+ }
457
+ var events = (turn.userEvents || []).concat(turn.assistantEvents || []);
458
+ events.sort(function (a, b) { return (a.sequence || 0) - (b.sequence || 0); });
459
+ events.forEach(function (event) { html += bubbleHtml(event, turn.source); });
460
+ });
461
+ if (state.cursor) html += '<div class="sentinel">继续滚动或点击加载更多</div>';
462
+ el.innerHTML = html;
463
+ observeSentinel();
464
+ }
465
+
466
+ function observeSentinel() {
467
+ var sentinel = document.querySelector('#view-live .sentinel');
468
+ if (sentinel && typeof IntersectionObserver === 'function') {
469
+ var io = new IntersectionObserver(function (entries) {
470
+ if (entries.some(function (e) { return e.isIntersecting; })) {
471
+ io.disconnect();
472
+ loadMoreTurns();
473
+ }
474
+ });
475
+ io.observe(sentinel);
476
+ }
477
+ }
478
+
479
+ function renderTopbar() {
480
+ var project = state.project;
481
+ document.getElementById('main-title').textContent = project ? project.name : '会话浏览器';
482
+ }
483
+
484
+ /* ---------- 数据加载:当前对话 ---------- */
485
+
486
+ function loadMoreTurns() {
487
+ if (!state.project || !state.cursor || state.loadingTurns) return;
488
+ fetchTurns({ append: true });
489
+ }
490
+
491
+ function fetchTurns(options) {
492
+ if (!state.project) return Promise.resolve();
493
+ var append = options && options.append;
494
+ state.loadingTurns = true;
495
+ if (!append) { state.turns = []; state.cursor = null; }
496
+ renderLive();
497
+ var query = '/api/turns?project=' + encodeURIComponent(state.project.id)
498
+ + (state.cursor ? '&cursor=' + encodeURIComponent(state.cursor) : '');
499
+ return api(query).then(function (payload) {
500
+ if (!state.project || payload.project !== state.project.id) return; // 切换已发生,丢弃过期响应
501
+ state.turns = state.turns.concat(payload.turns || []);
502
+ state.cursor = payload.nextCursor || null;
503
+ state.totalTurns = payload.totalTurns || state.turns.length;
504
+ state.loadingTurns = false;
505
+ renderLive();
506
+ }).catch(function (err) {
507
+ state.loadingTurns = false;
508
+ renderLive();
509
+ toast(false, '加载对话失败:' + err.message);
510
+ });
511
+ }
512
+
513
+ function selectProject(id, options) {
514
+ var project = state.projects.find(function (p) { return p.id === id; });
515
+ if (!project) return;
516
+ state.project = project;
517
+ state.turns = [];
518
+ state.cursor = null;
519
+ state.commits = [];
520
+ state.currentDocFile = null;
521
+ showLive();
522
+ renderTopbar();
523
+ renderSidebar();
524
+ renderHistList();
525
+ if (!(options && options.keepTurns)) fetchTurns();
526
+ }
527
+
528
+ function loadProjects(options) {
529
+ return api('/api/projects').then(function (payload) {
530
+ state.projects = payload.projects || [];
531
+ renderSidebar();
532
+ var currentStillExists = state.project && state.projects.some(function (p) { return p.id === state.project.id; });
533
+ if (!currentStillExists) {
534
+ if (state.projects.length) {
535
+ selectProject(state.projects[0].id, options);
536
+ } else {
537
+ state.project = null;
538
+ state.turns = [];
539
+ renderTopbar();
540
+ renderLive();
541
+ }
542
+ } else if (options && options.refreshSelected) {
543
+ selectProject(state.project.id, { keepTurns: false });
544
+ }
545
+ }).catch(function (err) {
546
+ toast(false, '加载项目失败:' + err.message);
547
+ });
548
+ }
549
+
550
+ function loadHealth() {
551
+ api('/api/health').then(function (payload) {
552
+ document.getElementById('foot-dot').classList.remove('off');
553
+ document.getElementById('foot-st').textContent = '已连接 · ' + payload.registeredProjects + ' 个已导入项目';
554
+ }).catch(function () {
555
+ document.getElementById('foot-dot').classList.add('off');
556
+ document.getElementById('foot-st').textContent = '无法连接';
557
+ });
558
+ }
559
+
560
+ /* ---------- 历史对话(封存文档,点击才加载) ---------- */
561
+
562
+ var histBtn = document.getElementById('hist-btn');
563
+ var histPop = document.getElementById('hist-pop');
564
+
565
+ function renderHistList() {
566
+ var el = document.getElementById('hist-list');
567
+ if (!state.project) { el.innerHTML = ''; return; }
568
+ if (state.project.auto) {
569
+ el.innerHTML = '<div class="pnote">该项目未导入,没有历史对话档案。</div>';
570
+ return;
571
+ }
572
+ if (!state.commits.length) {
573
+ el.innerHTML = '<div class="pnote">还没有历史对话。提交代码后,这段时间的对话会归档到这里。</div>';
574
+ return;
575
+ }
576
+ el.innerHTML = state.commits.map(function (doc) {
577
+ var label = doc.sha ? doc.sha.slice(0, 7) : '未提交';
578
+ var date = (doc.date || '').slice(0, 10);
579
+ return '<button class="citem' + (state.currentDocFile === doc.file ? ' on' : '') + '" data-doc="' + escapeAttr(doc.file) + '">'
580
+ + '<span class="r1"><span class="sha">' + escapeText(label) + '</span><span class="dt">' + escapeText(date) + '</span></span>'
581
+ + '<span class="sb">' + escapeText(doc.subject) + '</span></button>';
582
+ }).join('');
583
+ }
584
+
585
+ function refreshHist() {
586
+ if (!state.project || state.project.auto) { state.commits = []; renderHistList(); return; }
587
+ api('/api/commits?project=' + encodeURIComponent(state.project.id)).then(function (payload) {
588
+ if (!state.project || payload.project !== state.project.id) return;
589
+ state.commits = payload.commits || [];
590
+ renderHistList();
591
+ }).catch(function () {
592
+ state.commits = [];
593
+ renderHistList();
594
+ });
595
+ }
596
+
597
+ histBtn.addEventListener('click', function (ev) {
598
+ ev.stopPropagation();
599
+ var willOpen = !histPop.classList.contains('open');
600
+ histPop.classList.toggle('open', willOpen);
601
+ histBtn.classList.toggle('on', willOpen);
602
+ if (willOpen) refreshHist();
603
+ });
604
+ document.addEventListener('click', function (ev) {
605
+ if (!ev.target.closest('.hist-wrap')) {
606
+ histPop.classList.remove('open');
607
+ histBtn.classList.remove('on');
608
+ }
609
+ });
610
+
611
+ /* ---------- 封存文档解析(与 core 渲染规则对应:围栏感知) ---------- */
612
+
613
+ function splitSealedSections(body) {
614
+ var sections = [];
615
+ var current = null;
616
+ var fence = null;
617
+ body.split('\n').forEach(function (line) {
618
+ var fenceMatch = line.match(/^(`{3,})/);
619
+ if (fence) {
620
+ if (fenceMatch && fenceMatch[1].length === fence.length) fence = null;
621
+ } else if (fenceMatch) {
622
+ fence = fenceMatch[1];
623
+ } else if (line.indexOf('## ') === 0) {
624
+ current = [line];
625
+ sections.push(current);
626
+ return;
627
+ }
628
+ if (current) current.push(line);
629
+ });
630
+ return sections.map(function (lines) { return lines.join('\n'); }).filter(Boolean);
631
+ }
632
+
633
+ function parseSealedDoc(text) {
634
+ var fmMatch = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
635
+ var fm = {};
636
+ (fmMatch ? fmMatch[1] : '').split(/\r?\n/).forEach(function (line) {
637
+ var kv = line.match(/^([A-Za-z]+):\s*(.*)$/);
638
+ if (kv) fm[kv[1]] = kv[2].replace(/^'/, '').replace(/'$/, '');
639
+ });
640
+ var turns = [];
641
+ var appendix = [];
642
+ splitSealedSections(text.slice(fmMatch ? fmMatch[0].length : 0)).forEach(function (section) {
643
+ var isAppendix = section.indexOf('## 未配对事件') === 0;
644
+ var events = [];
645
+ var re = /^### (用户|助手)(?:[^\n]*)?\n\n(`{3,})\n([\s\S]*?)\n\2/gm;
646
+ var m;
647
+ while ((m = re.exec(section))) {
648
+ events.push({ role: m[1] === '用户' ? 'user' : 'assistant', content: m[3] });
649
+ }
650
+ if (isAppendix) appendix = events;
651
+ else turns.push({ events: events });
652
+ });
653
+ return { fm: fm, turns: turns, appendix: appendix };
654
+ }
655
+
656
+ /* ---------- 历史文档视图 ---------- */
657
+
658
+ function showLive() {
659
+ state.currentDocFile = null;
660
+ document.getElementById('view-doc').classList.remove('on');
661
+ document.getElementById('view-live').classList.add('on');
662
+ document.getElementById('back-btn').classList.remove('show');
663
+ renderHistList();
664
+ }
665
+
666
+ function showDocLoading(show) {
667
+ document.getElementById('doc-loading').style.display = show ? 'block' : 'none';
668
+ if (show) document.getElementById('doc-content').style.display = 'none';
669
+ }
670
+
671
+ document.getElementById('hist-list').addEventListener('click', function (ev) {
672
+ var item = ev.target.closest('[data-doc]');
673
+ if (!item || !state.project) return;
674
+ var file = item.getAttribute('data-doc');
675
+ if (state.currentDocFile === file && document.getElementById('view-doc').classList.contains('on')) {
676
+ histPop.classList.remove('open');
677
+ histBtn.classList.remove('on');
678
+ return;
679
+ }
680
+ state.currentDocFile = file;
681
+ histPop.classList.remove('open');
682
+ histBtn.classList.remove('on');
683
+ renderHistList();
684
+ var doc = state.commits.find(function (d) { return d.file === file; }) || { subject: file, sha: null, date: null };
685
+ document.getElementById('view-live').classList.remove('on');
686
+ document.getElementById('view-doc').classList.add('on');
687
+ document.getElementById('back-btn').classList.add('show');
688
+ showDocLoading(true);
689
+ // 懒加载:此刻才请求这一份文档,其余历史永不加载
690
+ fetch('/api/commits/' + encodeURIComponent(file) + '?project=' + encodeURIComponent(state.project.id))
691
+ .then(function (res) {
692
+ if (!res.ok) throw new Error('HTTP ' + res.status);
693
+ return res.text();
694
+ })
695
+ .then(function (text) {
696
+ if (state.currentDocFile !== file) return; // 已切换到别的文档/项目
697
+ var parsed = parseSealedDoc(text);
698
+ var title = doc.subject || parsed.fm.subject || (parsed.fm.commitSha || file).slice(0, 12);
699
+ var subParts = [];
700
+ var when = doc.date || parsed.fm.committedAt || parsed.fm.sealedAt || '';
701
+ if (when) subParts.push(when.slice(0, 16).replace('T', ' '));
702
+ var sha = doc.sha || parsed.fm.commitSha || '';
703
+ if (sha) subParts.push(String(sha).slice(0, 7));
704
+ var html = '<div class="doc-h"><h2>' + escapeText(title) + '</h2>'
705
+ + (subParts.length ? '<div class="sub">' + escapeText(subParts.join(' · ')) + '</div>' : '') + '</div>'
706
+ + '<div class="flow">';
707
+ parsed.turns.forEach(function (turn) {
708
+ turn.events.forEach(function (event) { html += bubbleHtml(event, 'claude-code'); });
709
+ });
710
+ parsed.appendix.forEach(function (event) { html += bubbleHtml(event, 'claude-code'); });
711
+ html += '</div>';
712
+ document.getElementById('doc-content').innerHTML = html;
713
+ showDocLoading(false);
714
+ document.getElementById('doc-content').style.display = 'block';
715
+ document.querySelector('#view-doc').scrollTop = 0;
716
+ })
717
+ .catch(function (err) {
718
+ if (state.currentDocFile !== file) return;
719
+ showDocLoading(false);
720
+ document.getElementById('doc-content').style.display = 'block';
721
+ document.getElementById('doc-content').innerHTML = '<div class="empty"><div class="box"><div class="t1">文档加载失败</div>'
722
+ + '<div class="t2">' + escapeText(err.message) + '</div></div></div>';
723
+ });
724
+ });
725
+
726
+ document.getElementById('back-btn').addEventListener('click', showLive);
727
+
728
+ /* ---------- 导入项目 ---------- */
729
+
730
+ var importModal = document.getElementById('import-modal');
731
+ var impPath = document.getElementById('imp-path');
732
+ var impStore = document.getElementById('imp-store');
733
+ var impOk = document.getElementById('imp-ok');
734
+ var impPathBrowse = document.getElementById('imp-path-browse');
735
+ var impStoreBrowse = document.getElementById('imp-store-browse');
736
+ var pickSupported = true;
737
+
738
+ function openImport() {
739
+ impPath.value = '';
740
+ impStore.value = '';
741
+ importModal.classList.add('open');
742
+ impPath.focus();
743
+ }
744
+ function closeImport() { importModal.classList.remove('open'); }
745
+
746
+ document.getElementById('import-btn').addEventListener('click', openImport);
747
+ document.getElementById('imp-cancel').addEventListener('click', closeImport);
748
+ importModal.addEventListener('click', function (ev) { if (ev.target === importModal) closeImport(); });
749
+
750
+ function browseInto(input) {
751
+ if (!pickSupported) { input.focus(); return; }
752
+ input.blur();
753
+ api('/api/system/pick-folder').then(function (payload) {
754
+ if (payload && payload.path) {
755
+ input.value = payload.path;
756
+ if (input === impPath) impStore.value = '';
757
+ }
758
+ }).catch(function (err) {
759
+ if (err.status === 501) {
760
+ pickSupported = false;
761
+ toast(false, '当前平台不支持文件夹选择框,请手动输入路径');
762
+ } else {
763
+ toast(false, '选择文件夹失败:' + err.message);
764
+ }
765
+ });
766
+ }
767
+
768
+ impPathBrowse.addEventListener('click', function () { browseInto(impPath); });
769
+ impStoreBrowse.addEventListener('click', function () { browseInto(impStore); });
770
+
771
+ impOk.addEventListener('click', function () {
772
+ var pathValue = impPath.value.trim();
773
+ if (!pathValue) { toast(false, '请先填写项目文件夹'); return; }
774
+ impOk.disabled = true;
775
+ api('/api/projects', {
776
+ method: 'POST',
777
+ headers: { 'content-type': 'application/json' },
778
+ body: JSON.stringify({ path: pathValue, store: impStore.value.trim() || undefined })
779
+ }).then(function (payload) {
780
+ impOk.disabled = false;
781
+ closeImport();
782
+ toast(true, '已导入 ' + payload.project.name);
783
+ return loadProjects();
784
+ }).catch(function (err) {
785
+ impOk.disabled = false;
786
+ var hint = err.code === 'PROJECT_NOT_GIT' ? '(该目录不是 Git 仓库)' : '';
787
+ toast(false, '导入失败:' + err.message + hint);
788
+ });
789
+ });
790
+
791
+ /* ---------- 项目注销 / 选择 ---------- */
792
+
793
+ document.getElementById('proj-list').addEventListener('click', function (ev) {
794
+ var del = ev.target.closest('[data-del]');
795
+ if (del) {
796
+ ev.stopPropagation();
797
+ var id = del.getAttribute('data-del');
798
+ var project = state.projects.find(function (p) { return p.id === id; });
799
+ if (!project) return;
800
+ if (!window.confirm('注销项目「' + project.name + '」?\n磁盘上的记录不会被删除;注销后该项目的新对话将写入全局记录。')) return;
801
+ api('/api/projects?id=' + encodeURIComponent(id), { method: 'DELETE' }).then(function () {
802
+ toast(true, '已注销 ' + project.name + '(磁盘数据保留)');
803
+ return loadProjects({ refreshSelected: true });
804
+ }).catch(function (err) {
805
+ toast(false, '注销失败:' + err.message);
806
+ });
807
+ return;
808
+ }
809
+ var item = ev.target.closest('[data-proj]');
810
+ if (!item) return;
811
+ selectProject(item.getAttribute('data-proj'));
812
+ });
813
+
814
+ document.getElementById('proj-q').addEventListener('input', function (ev) {
815
+ state.filter = ev.target.value;
816
+ renderSidebar();
817
+ });
818
+
819
+ document.getElementById('live-flow').addEventListener('click', function (ev) {
820
+ if (ev.target.closest('.sentinel')) loadMoreTurns();
821
+ });
822
+
823
+ /* ---------- 设置:本机工具检测 + 运行时 ---------- */
824
+
825
+ var settingsModal = document.getElementById('settings-modal');
826
+ document.getElementById('settings-btn').addEventListener('click', function () {
827
+ settingsModal.classList.add('open');
828
+ renderSettings();
829
+ });
830
+ document.getElementById('settings-close').addEventListener('click', function () {
831
+ settingsModal.classList.remove('open');
832
+ });
833
+ settingsModal.addEventListener('click', function (ev) {
834
+ if (ev.target === settingsModal) settingsModal.classList.remove('open');
835
+ });
836
+
837
+ function renderSettings() {
838
+ var body = document.getElementById('settings-body');
839
+ body.innerHTML = '<div style="color:var(--text-3);font-size:12px">加载中…</div>';
840
+ api('/api/system/agents').then(function (payload) {
841
+ var agentsHtml = payload.agents.map(function (a) {
842
+ var installed = a.cliFound || a.configFound;
843
+ var chips =
844
+ '<span class="st-chip ' + (installed ? 'ok' : 'off') + '">' + (installed ? '已安装' : '未安装') + '</span>'
845
+ + '<span class="st-chip ' + (a.hookInstalled ? 'ok' : 'warn') + '">' + (a.hookInstalled ? '已接入采集' : '未接入') + '</span>';
846
+ var note = a.detail || (a.hookInstalled ? '' : installed ? '需要通过安装器 / consumer 注册 hook 后才会采集' : '未检测到该工具');
847
+ return '<div class="agent-row"><span class="an">' + escapeText(a.label) + '</span>' + chips
848
+ + '<span class="note">' + escapeText(note) + '</span></div>';
849
+ }).join('');
850
+
851
+ var b = payload.bridge;
852
+ var runtimeHtml =
853
+ '<div class="kv-row"><span class="k">活跃运行时</span><span class="v">' + escapeText(b.activeRuntimeVersion || '无') + '</span><span class="grow"></span>'
854
+ + (b.upgradeAvailable ? '<button class="btn-upg" id="upgrade-btn">升级到 ' + escapeText(b.coreVersion) + '</button>' : '')
855
+ + '</div>'
856
+ + '<div class="kv-row"><span class="k">当前代码版本</span><span class="v">' + escapeText(b.coreVersion)
857
+ + (b.runtimeVersions.length ? '(已物化: ' + escapeText(b.runtimeVersions.join(', ')) + ')' : '') + '</span></div>'
858
+ + '<div class="kv-row"><span class="k">Bridge Home</span><span class="v">' + escapeText(b.home) + '</span></div>';
859
+
860
+ var consumersHtml = payload.consumers.length
861
+ ? payload.consumers.map(function (c) {
862
+ return '<div class="kv-row"><span class="k">' + escapeText(c.name) + '</span><span class="v">'
863
+ + escapeText(c.lastSeenAt || '从未活跃') + '</span></div>';
864
+ }).join('')
865
+ : '<div class="kv-row"><span class="v" style="color:var(--text-3)">无消费方注册</span></div>';
866
+
867
+ body.innerHTML =
868
+ '<div class="set-section">AI 编程工具</div>' + agentsHtml
869
+ + '<div class="set-section">Bridge 运行时</div>' + runtimeHtml
870
+ + '<div class="set-section">消费方</div>' + consumersHtml;
871
+
872
+ var upg = document.getElementById('upgrade-btn');
873
+ if (upg) {
874
+ upg.addEventListener('click', function () {
875
+ upg.disabled = true;
876
+ upg.textContent = '升级中…';
877
+ api('/api/system/upgrade-runtime', { method: 'POST' }).then(function (result) {
878
+ toast(true, '运行时已升级到 ' + result.activeVersion);
879
+ renderSettings();
880
+ }).catch(function (err) {
881
+ upg.disabled = false;
882
+ upg.textContent = '重试升级';
883
+ toast(false, '升级失败:' + err.message);
884
+ });
885
+ });
886
+ }
887
+ }).catch(function (err) {
888
+ body.innerHTML = '<div style="color:var(--red);font-size:12.5px">加载失败:' + escapeText(err.message) + '</div>';
889
+ });
890
+ }
891
+
892
+ /* ---------- 启动 ---------- */
893
+
894
+ loadHealth();
895
+ loadProjects();
896
+ setInterval(loadHealth, 30000);
897
+
898
+ /* ---------- embed bridge(被外部壳嵌入时受控) ----------
899
+ * 壳 postMessage:{type:'kb:sidebar', visible} 折叠/展开项目侧栏。
900
+ * URL 参数:?embed=1 进入嵌入态(默认折叠,sidebar=1 保持展开)。
901
+ * 就绪后回发 {type:'kb:ready', app:'event-bridge'};独立打开时不受控、侧栏恒显。 */
902
+ function embedAllowedOrigin(origin) {
903
+ if (origin === 'null') return true; // file:// 壳
904
+ try { const u = new URL(origin); return u.hostname === 'localhost' || u.hostname === '127.0.0.1' || u.hostname === '[::1]'; }
905
+ catch { return false; }
906
+ }
907
+ function setEmbedSideVisible(visible) { document.body.classList.toggle('embed-collapse', !visible); }
908
+ window.addEventListener('message', function (ev) {
909
+ if (!embedAllowedOrigin(ev.origin)) return;
910
+ const d = ev.data;
911
+ if (!d || typeof d !== 'object') return;
912
+ if (d.type === 'kb:sidebar') setEmbedSideVisible(!!d.visible);
913
+ });
914
+ (function () {
915
+ const q = new URLSearchParams(location.search);
916
+ if (q.get('embed') === '1') setEmbedSideVisible(q.get('sidebar') !== '1');
917
+ if (window.parent !== window) window.parent.postMessage({ type: 'kb:ready', app: 'event-bridge' }, '*');
918
+ })();
919
+ </script>
920
+ </body>
921
+ </html>