magic-agent 1.0.0 → 1.0.2
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/CLIENT-CONTRACT.md +298 -0
- package/README.md +35 -0
- package/client-ui/assets/OFL-Caveat.txt +93 -0
- package/client-ui/assets/caveat.woff2 +0 -0
- package/client-ui/index.html +246 -0
- package/client-ui/pc/app.css +5110 -0
- package/client-ui/pc/assets/damiao-mascot.png +0 -0
- package/client-ui/pc/assets/icon.png +0 -0
- package/client-ui/pc/index.html +154 -0
- package/client-ui/pc/js/00-base.js +52 -0
- package/client-ui/pc/js/03-chat-message.js +880 -0
- package/client-ui/pc/js/04-task-list.js +430 -0
- package/client-ui/pc/js/05-task-list-render.js +812 -0
- package/client-ui/pc/js/06-ask-answer.js +204 -0
- package/client-ui/pc/js/07-chat-stream-render.js +493 -0
- package/client-ui/pc/js/08-ask-event-map.js +171 -0
- package/client-ui/pc/js/09-stream-apply.js +367 -0
- package/client-ui/pc/js/10-markdown.js +430 -0
- package/client-ui/pc/js/18-askbox.js +262 -0
- package/client-ui/pc/js/19-attachments.js +679 -0
- package/client-ui/pc/js/29-msg-variants.js +462 -0
- package/client-ui/pc/js/90-glue.js +212 -0
- package/client-ui/pc/js/91-workbench-stubs.js +134 -0
- package/client-ui/pc/js/92-composer.js +406 -0
- package/client-ui/pc/js/93-rail.js +362 -0
- package/client-ui/pc/vendor/highlight.min.js +1262 -0
- package/client-ui/pc/vendor/marked.min.js +6 -0
- package/client-ui/pc/vendor/purify.min.js +3 -0
- package/client-ui/shared/conversation.css +256 -0
- package/client-ui/shared/conversation.js +1225 -0
- package/client-ui/shared/transport-http.js +123 -0
- package/client-ui-mobile/assets/OFL-Caveat.txt +93 -0
- package/client-ui-mobile/assets/caveat.woff2 +0 -0
- package/client-ui-mobile/index.html +173 -0
- package/npm/bin/magic-agent.js +43 -0
- package/npm/dist/darwin-arm64/magic-agent +0 -0
- package/npm/dist/darwin-x64/magic-agent +0 -0
- package/npm/dist/linux-arm64/magic-agent +0 -0
- package/npm/dist/linux-x64/magic-agent +0 -0
- package/npm/dist/win32-x64/magic-agent.exe +0 -0
- package/package.json +4 -1
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/* 93-rail.js —— 左侧会话栏:**数据全部来自 magic-agent 自己的命令**(经宿主 desk 桥)。
|
|
2
|
+
*
|
|
3
|
+
* ── 数据来自哪两条命令 ──
|
|
4
|
+
* `--sessions` 会话登记表:run_id / session_id / engine / model / state / workspace /
|
|
5
|
+
* prompt_head / 起止时间 → 本文件的行
|
|
6
|
+
* `--session-log` 一条会话的**事件历史**:`{seq, at, kind, text, ...}`,
|
|
7
|
+
* kind ∈ user / thinking / text / tool_use / tool_result / ask /
|
|
8
|
+
* turn_end / error —— 与渲染层的扁平事件**同名词表**,所以点开一条会话
|
|
9
|
+
* 就是把历史事件喂进**与实时同一条**渲染管线(见 qaRailReplay)。
|
|
10
|
+
* 操作面只有 CLI 真有据可依的那几个:新建对话 / 打开(读历史)/ 停止(`--stop`)/
|
|
11
|
+
* 复制会话 id。「置顶 / 归档 / 删除」在 magic-agent 里没有对应概念 —— 本版**不做**,
|
|
12
|
+
* 免得长出点了没反应的按钮(旧工作台那三样建在掌天瓶自己的 tasks.db 上)。
|
|
13
|
+
*
|
|
14
|
+
* ── 为什么不用 05-task-list-render.js 那份左栏 ──
|
|
15
|
+
* 那份是给旧工作台的**本地任务表**写的(置顶 / 归档 / 需求绑定 / 画布快照全在 SQLite)。
|
|
16
|
+
* 这一版数据源换成了 CLI,硬套只会得到一半按钮是摆设。外观仍用 app.css 里那套 `.tk-*`
|
|
17
|
+
* 类,所以看上去与其余界面同调。
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const QA_RAIL_MAX = 60; // 一屏最多画多少行(CLI 的登记表可能有几百条)
|
|
21
|
+
const QA_RAIL_POLL_MS = 30000; // 有会话在跑时的兜底轮询(其余时候靠「一轮结束」触发)
|
|
22
|
+
|
|
23
|
+
let qaRailAll = []; // 最近一次拉到的会话(宿主归一后的行)
|
|
24
|
+
let qaRailSkipped = 0; // 被略掉的一次性调用条数(如实写在列表下方)
|
|
25
|
+
let qaRailNoLog = 0; // 列出来的会话里,有多少条没有可回放的事件日志
|
|
26
|
+
let qaRailQuery = "";
|
|
27
|
+
let qaRailTab = "all";
|
|
28
|
+
let qaRailErr = "";
|
|
29
|
+
let qaRailLoaded = false;
|
|
30
|
+
let qaRailTimer = null;
|
|
31
|
+
let qaRailMenuEl = null;
|
|
32
|
+
|
|
33
|
+
/** 会话落到哪一档(左栏分组)—— 判据全部来自 CLI 给的 state / alive。
|
|
34
|
+
* ⚠️ 状态词与 magic-agent 的记录、agents-anywhere 的 TimelineStatus 对齐(2026-09-29):
|
|
35
|
+
* running / waiting_approval / done / failed / cancelled / interrupted / gone。
|
|
36
|
+
* `waiting_approval` 必须算**进行中** —— 它没在跑,但也没结束;归到「已完成」的话,
|
|
37
|
+
* 用户会看到「我还在等它回话,它却已完成」。 */
|
|
38
|
+
function qaRailGroup(s){
|
|
39
|
+
if(!s) return "done";
|
|
40
|
+
const st = String(s.state || "");
|
|
41
|
+
if(s.alive || st === "running") return "live";
|
|
42
|
+
if(st === "waiting_approval") return "live";
|
|
43
|
+
if(st === "failed" || st === "gone") return "failed";
|
|
44
|
+
return "done";
|
|
45
|
+
}
|
|
46
|
+
function qaRailGroupLabel(s){
|
|
47
|
+
const st = String((s && s.state) || "");
|
|
48
|
+
const g = qaRailGroup(s);
|
|
49
|
+
if(g === "live") return st === "waiting_approval" ? "等审批" : "进行中";
|
|
50
|
+
if(g === "failed") return "失败";
|
|
51
|
+
if(st === "cancelled" || st === "stopped") return "已取消";
|
|
52
|
+
if(st === "interrupted") return "已打断";
|
|
53
|
+
return "已完成";
|
|
54
|
+
}
|
|
55
|
+
/** 时间列:今天只给 时:分,其余给 月/日(与旧左栏同一口径)。 */
|
|
56
|
+
function qaRailTime(ts){
|
|
57
|
+
const n = Number(ts);
|
|
58
|
+
if(!Number.isFinite(n) || !n) return "";
|
|
59
|
+
const d = new Date(n), now = new Date();
|
|
60
|
+
const hm = String(d.getHours()).padStart(2, "0") + ":" + String(d.getMinutes()).padStart(2, "0");
|
|
61
|
+
return d.toDateString() === now.toDateString() ? hm : (d.getMonth() + 1) + "/" + d.getDate();
|
|
62
|
+
}
|
|
63
|
+
function qaRailHit(s){
|
|
64
|
+
if(qaRailTab !== "all" && qaRailGroup(s) !== qaRailTab) return false;
|
|
65
|
+
const q = qaRailQuery.trim().toLowerCase();
|
|
66
|
+
if(!q) return true;
|
|
67
|
+
return [s.title, s.engine, s.model, s.workspace, s.sessionId, s.runId]
|
|
68
|
+
.some((v) => String(v || "").toLowerCase().includes(q));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* ---------- 取数 ---------- */
|
|
72
|
+
|
|
73
|
+
async function qaRailLoad(quiet){
|
|
74
|
+
const d = window.desk;
|
|
75
|
+
if(!d || typeof d.agentSessions !== "function"){
|
|
76
|
+
qaRailErr = "这一版没有会话清单通道(需要宿主提供 desk.agentSessions)";
|
|
77
|
+
qaRailLoaded = true; qaRailRender(); return;
|
|
78
|
+
}
|
|
79
|
+
try{
|
|
80
|
+
const r = await d.agentSessions();
|
|
81
|
+
if(!r || !r.ok){ qaRailErr = (r && r.error) || "会话清单读不到"; }
|
|
82
|
+
else {
|
|
83
|
+
const all = Array.isArray(r.sessions) ? r.sessions : [];
|
|
84
|
+
/* ⚠️ 判据只有一条:**有没有 `session_id`**(有 = 一条真会话,能续接、能回放)。
|
|
85
|
+
· 没有 `session_id` 的是一次性调用(归纳 / 判定 / 派发里的一轮),不是会话,不列;
|
|
86
|
+
· 有 `session_id` 但 `hasLog=false` 的**照样列** —— 引擎当时是非流式跑的,没留下事件
|
|
87
|
+
日志,可它仍然是一条 bot 会话(**审查轮就是这一类**:用户 2026-09-29「是审查也是
|
|
88
|
+
bot,所以应该显示在对话 ui 的左边」)。早先我按 hasLog 把它们筛掉了,结果是审查
|
|
89
|
+
在列表里整个消失。这类行会带「无历史」标记,点开也如实说清为什么没有。 */
|
|
90
|
+
qaRailAll = all.filter((s) => String(s.sessionId || "").trim());
|
|
91
|
+
qaRailSkipped = all.length - qaRailAll.length;
|
|
92
|
+
qaRailNoLog = qaRailAll.filter((s) => !s.alive && !s.hasLog).length;
|
|
93
|
+
qaRailErr = "";
|
|
94
|
+
}
|
|
95
|
+
}catch(e){ qaRailErr = String((e && e.message) || e); }
|
|
96
|
+
qaRailLoaded = true;
|
|
97
|
+
qaRailRender();
|
|
98
|
+
qaRailSchedule();
|
|
99
|
+
if(!quiet && qaRailErr) console.warn("[rail] " + qaRailErr);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** 有会话在跑时轻度轮询;没在跑就不轮询(一次 CLI 调用不贵,但没必要空转)。 */
|
|
103
|
+
function qaRailSchedule(){
|
|
104
|
+
const live = qaRailAll.some((s) => qaRailGroup(s) === "live");
|
|
105
|
+
if(live && !qaRailTimer){ qaRailTimer = setInterval(() => qaRailLoad(true), QA_RAIL_POLL_MS); }
|
|
106
|
+
if(!live && qaRailTimer){ clearInterval(qaRailTimer); qaRailTimer = null; }
|
|
107
|
+
}
|
|
108
|
+
/** 一轮结束后尽快刷新(由 92-composer 的收尾分支调)。 */
|
|
109
|
+
function qaRailRefreshSoon(){
|
|
110
|
+
setTimeout(() => qaRailLoad(true), 400);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* ---------- 渲染 ---------- */
|
|
114
|
+
|
|
115
|
+
function qaRailRender(){
|
|
116
|
+
const box = $("#rail-list"), tabs = $("#rail-tabs"), note = $("#rail-note");
|
|
117
|
+
if(!box) return;
|
|
118
|
+
if(tabs){
|
|
119
|
+
const counts = { all: qaRailAll.length, live: 0, done: 0, failed: 0 };
|
|
120
|
+
qaRailAll.forEach((s) => { counts[qaRailGroup(s)] += 1; });
|
|
121
|
+
const tab = (k, label) =>
|
|
122
|
+
`<button class="tk-tab${qaRailTab === k ? " on" : ""}" data-rail-tab="${k}" type="button"` +
|
|
123
|
+
` data-n="${counts[k]}" data-testid="rail-tab-${k}"><span>${label}</span>` +
|
|
124
|
+
`<span class="n">${counts[k]}</span></button>`;
|
|
125
|
+
tabs.innerHTML = tab("all", "全部") + tab("live", "进行中") + tab("done", "已完成") + tab("failed", "失败");
|
|
126
|
+
}
|
|
127
|
+
const rows = qaRailAll.filter(qaRailHit).slice(0, QA_RAIL_MAX);
|
|
128
|
+
if(!qaRailAll.length){
|
|
129
|
+
box.innerHTML = `<div class="tk-empty" data-testid="rail-empty">${qaRailLoaded
|
|
130
|
+
? (qaRailErr ? escHtml(qaRailErr) : "还没有会话 —— 在右边直接提问就会开一条")
|
|
131
|
+
: "正在读会话清单…"}</div>`;
|
|
132
|
+
}else if(!rows.length){
|
|
133
|
+
box.innerHTML = `<div class="tk-empty" data-testid="rail-empty">没有匹配的会话</div>`;
|
|
134
|
+
}else{
|
|
135
|
+
// ── 会话行 ──
|
|
136
|
+
// ⚠️ 「无历史」标记:有会话 id 但没有事件日志(引擎当时非流式跑)—— 这类行照样列,
|
|
137
|
+
// 但得**在点之前**告诉人一声,否则点开只看到一句说明会以为坏了。
|
|
138
|
+
const noLog = (s) => !s.alive && !s.hasLog;
|
|
139
|
+
box.innerHTML = rows.map((s) => {
|
|
140
|
+
const g = qaRailGroup(s);
|
|
141
|
+
const title = s.title || "(无标题会话)";
|
|
142
|
+
const sub = [s.engine, s.model].filter(Boolean).join(" · ");
|
|
143
|
+
const tip = [title, sub, s.workspace, s.sessionId || s.runId, s.updatedAt,
|
|
144
|
+
noLog(s) ? "这条会话没有留下事件日志(引擎当时是非流式跑的),点开看不到历史" : ""]
|
|
145
|
+
.filter(Boolean).join("\n");
|
|
146
|
+
return `<button class="tk-item${s.id === qaActiveId ? " active" : ""}" data-sid="${escHtml(s.id)}" type="button" title="${escHtml(tip)}" data-testid="rail-item">
|
|
147
|
+
<span class="tico"><i class="rail-dot ${g}" aria-hidden="true"></i></span>
|
|
148
|
+
<span class="tt">${escHtml(title)}</span>
|
|
149
|
+
${noLog(s) ? `<span class="rail-nolog" title="没有可回放的历史">无历史</span>` : ""}
|
|
150
|
+
<span class="tm">${escHtml(qaRailTime(s.ts))}</span>
|
|
151
|
+
<span class="tk-dots" data-rail-menu="${escHtml(s.id)}" role="button" tabindex="-1" data-testid="rail-dots"
|
|
152
|
+
title="更多">${'<svg viewBox="0 0 24 24"><circle cx="5" cy="12" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="19" cy="12" r="1.6"/></svg>'}</span>
|
|
153
|
+
</button>`;
|
|
154
|
+
}).join("");
|
|
155
|
+
}
|
|
156
|
+
if(note){
|
|
157
|
+
const hit = qaRailAll.filter(qaRailHit).length;
|
|
158
|
+
const bits = [];
|
|
159
|
+
if(hit > QA_RAIL_MAX) bits.push(`仅显示最近 ${QA_RAIL_MAX} 条(共 ${hit} 条 · 可用搜索缩小范围)`);
|
|
160
|
+
if(qaRailNoLog) bits.push(`标「无历史」的 ${qaRailNoLog} 条是引擎非流式跑的,没留下可回放的事件日志(会话本身还能继续)`);
|
|
161
|
+
if(qaRailSkipped) bits.push(`另有 ${qaRailSkipped} 条一次性调用(没有会话 id)未列出`);
|
|
162
|
+
note.textContent = bits.join(" · ");
|
|
163
|
+
}
|
|
164
|
+
const cnt = $("#rail-count");
|
|
165
|
+
if(cnt) cnt.textContent = qaRailAll.length ? String(qaRailAll.filter((s) => qaRailGroup(s) === "live").length || "") : "";
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/* ---------- 打开一条会话(读历史 + 回放) ---------- */
|
|
169
|
+
|
|
170
|
+
/** 把「正在跑的那一轮」的画面整份挪进它自己的 holder:节点不销毁 →
|
|
171
|
+
* 后台增量继续写进这些节点,切回来直接挂回 #qa-stream(与 05 的切会话同一套做法)。 */
|
|
172
|
+
function qaRailStowRun(){
|
|
173
|
+
const run = (typeof qaRunOfTask === "function") ? qaRunOfTask(qaActiveId) : null;
|
|
174
|
+
if(!run) return;
|
|
175
|
+
if(!run.holder) run.holder = document.createElement("div");
|
|
176
|
+
const prev = $("#qa-stream");
|
|
177
|
+
if(prev) while(prev.firstChild) run.holder.appendChild(prev.firstChild);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** 让 qaSend 认得这条会话:它按 `qaTasks.find(qaActiveId)` 取 sessionId,
|
|
181
|
+
* 取到了才会带上 `--session` 续接(否则引擎会新起一段上下文)。 */
|
|
182
|
+
function qaRailEnsureTask(s){
|
|
183
|
+
if(!s || !s.id) return;
|
|
184
|
+
let t = (Array.isArray(qaTasks) ? qaTasks : []).find((x) => x.id === s.id);
|
|
185
|
+
if(!t){ t = { id: s.id, ts: s.ts || Date.now() }; qaTasks.unshift(t); }
|
|
186
|
+
t.title = t.title || s.title || "新对话";
|
|
187
|
+
t.engine = s.engine || t.engine || qaEngine;
|
|
188
|
+
t.model = s.model || "";
|
|
189
|
+
t.sessionId = s.sessionId || "";
|
|
190
|
+
t.status = qaRailGroup(s) === "live" ? "running" : (s.state === "failed" ? "error" : "done");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** 历史回放:kind 与渲染层事件**同名词表**,所以逐条喂进 qaStreamApply 即可。
|
|
194
|
+
* `user` 那条不是渲染层事件(它是 core 补记的用户提问),走 qaRenderMine 画成用户气泡。 */
|
|
195
|
+
function qaRailReplay(events, s){
|
|
196
|
+
const list = Array.isArray(events) ? events : [];
|
|
197
|
+
let h = null;
|
|
198
|
+
for(const ev of list){
|
|
199
|
+
const kind = String((ev && ev.kind) || "");
|
|
200
|
+
if(!kind) continue;
|
|
201
|
+
if(kind === "user"){ qaRenderMine(String(ev.text || ""), []); continue; }
|
|
202
|
+
if(!h){ h = qaRenderAnswer(); if(!h) break; }
|
|
203
|
+
try{
|
|
204
|
+
qaStreamApply(Object.assign({}, ev, { type: kind }), {
|
|
205
|
+
h, tools: new Map(), agents: new Map(), rid: 0, taskId: qaActiveId,
|
|
206
|
+
/* 回放是**只读**的:台账 / 会话绑定 / 收尾动作一律不落笔(历史不该改写现在) */
|
|
207
|
+
setMs(){}, onText(){}, onStep(){}, onSession(){}, onError(){}, onDone(){},
|
|
208
|
+
});
|
|
209
|
+
}catch(e){ console.warn("[rail] 回放事件失败 " + kind + ":", (e && e.message) || e); }
|
|
210
|
+
}
|
|
211
|
+
if(!h && !list.some((e) => String(e.kind || "") === "user")){
|
|
212
|
+
qaRenderDone(true, "这条会话没有留下事件", $("#qa-stream"));
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if(h){
|
|
216
|
+
/* 收尾行按会话自己的状态给一句(历史是只读的,不显示「生成中」那种活状态) */
|
|
217
|
+
const g = qaRailGroup(s);
|
|
218
|
+
qaRenderDone(g !== "failed", g === "failed" ? "上次调用失败" : (g === "live" ? "这条会话当时还在跑" : "历史回放"), $("#qa-stream"));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function qaRailOpen(id){
|
|
223
|
+
const sid = String(id || "").trim();
|
|
224
|
+
if(!sid) return;
|
|
225
|
+
const s = qaRailAll.find((x) => x.id === sid) || null;
|
|
226
|
+
const d = window.desk;
|
|
227
|
+
if(!d || typeof d.agentSessionLog !== "function"){ toast("这一版没有会话历史通道"); return; }
|
|
228
|
+
const box = $("#qa-stream"); if(!box) return;
|
|
229
|
+
|
|
230
|
+
qaRailStowRun();
|
|
231
|
+
qaActiveId = sid;
|
|
232
|
+
qaRailEnsureTask(s);
|
|
233
|
+
if(typeof showView === "function") showView("chat");
|
|
234
|
+
box.innerHTML = qaConvNoticeHtml("正在读取这条会话的历史…", "wait");
|
|
235
|
+
box.hidden = false;
|
|
236
|
+
qaSetChatMode(true);
|
|
237
|
+
qaRailRender();
|
|
238
|
+
|
|
239
|
+
let r = null;
|
|
240
|
+
try{ r = await d.agentSessionLog(sid); }
|
|
241
|
+
catch(e){ r = { ok: false, error: String((e && e.message) || e) }; }
|
|
242
|
+
if(qaActiveId !== sid) return; // 用户已经切走 → 丢弃这次结果
|
|
243
|
+
box.innerHTML = "";
|
|
244
|
+
if(!r || !r.ok){
|
|
245
|
+
box.innerHTML = qaConvNoticeHtml("读会话历史失败:" + ((r && r.error) || "未知原因") +
|
|
246
|
+
"(老版 magic-agent 不认识 `--session-log`,设置里升级一下就好)", "lost");
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
/* 命令通了但一条事件都没有 —— 这**不是**「命令不认识」,别混成同一句话:
|
|
250
|
+
引擎当时是**非流式**跑的,magic-agent 的事件日志只在流式调用时写。行上那个
|
|
251
|
+
「无历史」标记说的就是这件事,这里把原因说得更细一点(会话本身还能继续追问)。 */
|
|
252
|
+
if(!Array.isArray(r.events) || !r.events.length){
|
|
253
|
+
box.innerHTML = qaConvNoticeHtml(
|
|
254
|
+
"这条会话没有留下事件日志(引擎当时是非流式跑的),所以看不到历史。" +
|
|
255
|
+
"会话本身还在 —— 直接在这里追问,引擎会接着原来的上下文继续。", "lost");
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
qaRailReplay(r.events, s);
|
|
259
|
+
qaScrollIntoView();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** 新建对话:把当前上下文清掉,下一句话就是**全新会话**(不带给引擎任何 --session)。 */
|
|
263
|
+
function qaRailNew(){
|
|
264
|
+
const box = $("#qa-stream");
|
|
265
|
+
qaRailStowRun();
|
|
266
|
+
qaActiveId = null;
|
|
267
|
+
if(box){ box.innerHTML = ""; box.hidden = true; }
|
|
268
|
+
if(typeof qaSetChatMode === "function") qaSetChatMode(false);
|
|
269
|
+
qaRailRender();
|
|
270
|
+
const inp = $("#home-input"); if(inp) inp.focus();
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/* ---------- 行尾菜单 ---------- */
|
|
274
|
+
|
|
275
|
+
function qaRailMenuClose(){
|
|
276
|
+
if(qaRailMenuEl){ qaRailMenuEl.remove(); qaRailMenuEl = null; }
|
|
277
|
+
}
|
|
278
|
+
function qaRailMenuOpen(id, anchor){
|
|
279
|
+
qaRailMenuClose();
|
|
280
|
+
const s = qaRailAll.find((x) => x.id === id) || null;
|
|
281
|
+
if(!s || !anchor) return;
|
|
282
|
+
const live = qaRailGroup(s) === "live";
|
|
283
|
+
const el = document.createElement("div");
|
|
284
|
+
el.className = "rail-menu";
|
|
285
|
+
el.innerHTML =
|
|
286
|
+
(live ? `<button type="button" data-act="stop">停止这条会话</button>` : "") +
|
|
287
|
+
`<button type="button" data-act="open">打开(读历史)</button>` +
|
|
288
|
+
`<button type="button" data-act="copy">复制会话 id</button>`;
|
|
289
|
+
document.body.appendChild(el);
|
|
290
|
+
const r = anchor.getBoundingClientRect();
|
|
291
|
+
el.style.left = Math.max(8, Math.min(r.left, window.innerWidth - el.offsetWidth - 8)) + "px";
|
|
292
|
+
el.style.top = Math.round(r.bottom + 4) + "px";
|
|
293
|
+
el.addEventListener("click", async (e) => {
|
|
294
|
+
const b = e.target.closest("[data-act]"); if(!b) return;
|
|
295
|
+
const act = b.dataset.act;
|
|
296
|
+
qaRailMenuClose();
|
|
297
|
+
if(act === "open") return qaRailOpen(id);
|
|
298
|
+
if(act === "copy"){
|
|
299
|
+
try{ await navigator.clipboard.writeText(s.sessionId || s.runId || id); toast("已复制会话 id"); }
|
|
300
|
+
catch(err){ toast("复制失败:" + ((err && err.message) || err)); }
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
if(act === "stop") return qaRailStop(s);
|
|
304
|
+
});
|
|
305
|
+
qaRailMenuEl = el;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async function qaRailStop(s){
|
|
309
|
+
const d = window.desk;
|
|
310
|
+
if(!d || typeof d.agentStop !== "function"){ toast("这一版没有停止会话的通道"); return; }
|
|
311
|
+
const id = s.sessionId || s.runId || s.id;
|
|
312
|
+
try{
|
|
313
|
+
const r = await d.agentStop(id);
|
|
314
|
+
if(r && r.ok) toast("已发出停止:" + id.slice(0, 8));
|
|
315
|
+
else toast("停止失败:" + ((r && r.error) || "未知原因"));
|
|
316
|
+
}catch(e){ toast("停止失败:" + ((e && e.message) || e)); }
|
|
317
|
+
qaRailRefreshSoon();
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/* ---------- 装配 ---------- */
|
|
321
|
+
|
|
322
|
+
function qaRailInit(){
|
|
323
|
+
const box = $("#rail-list");
|
|
324
|
+
if(!box) return; // 宿主没给左栏(例如窄屏)→ 不装
|
|
325
|
+
const tabs = $("#rail-tabs");
|
|
326
|
+
if(tabs) tabs.addEventListener("click", (e) => {
|
|
327
|
+
const b = e.target.closest("[data-rail-tab]"); if(!b) return;
|
|
328
|
+
qaRailTab = b.dataset.railTab || "all";
|
|
329
|
+
qaRailRender();
|
|
330
|
+
});
|
|
331
|
+
box.addEventListener("click", (e) => {
|
|
332
|
+
const dots = e.target.closest("[data-rail-menu]");
|
|
333
|
+
if(dots){ e.stopPropagation(); qaRailMenuOpen(dots.dataset.railMenu, dots); return; }
|
|
334
|
+
const row = e.target.closest("[data-sid]");
|
|
335
|
+
if(row) qaRailOpen(row.dataset.sid);
|
|
336
|
+
});
|
|
337
|
+
const sInp = $("#rail-search-input");
|
|
338
|
+
if(sInp){
|
|
339
|
+
sInp.addEventListener("input", () => { qaRailQuery = sInp.value; qaRailRender(); });
|
|
340
|
+
sInp.addEventListener("keydown", (e) => { if(e.key === "Escape"){ sInp.value = ""; qaRailQuery = ""; qaRailRender(); } });
|
|
341
|
+
}
|
|
342
|
+
const sClr = $("#rail-search-clear");
|
|
343
|
+
if(sClr) sClr.addEventListener("click", () => {
|
|
344
|
+
if(sInp) sInp.value = ""; qaRailQuery = "";
|
|
345
|
+
const c = $("#rail-search-clear"); if(c) c.hidden = true;
|
|
346
|
+
qaRailRender();
|
|
347
|
+
});
|
|
348
|
+
if(sInp) sInp.addEventListener("input", () => { const c = $("#rail-search-clear"); if(c) c.hidden = !sInp.value; });
|
|
349
|
+
const nw = $("#rail-new");
|
|
350
|
+
if(nw) nw.addEventListener("click", () => qaRailNew());
|
|
351
|
+
const rf = $("#rail-refresh");
|
|
352
|
+
if(rf) rf.addEventListener("click", () => qaRailLoad(false));
|
|
353
|
+
document.addEventListener("click", (e) => {
|
|
354
|
+
if(!qaRailMenuEl) return;
|
|
355
|
+
if(e.composedPath().some((n) => n === qaRailMenuEl)) return;
|
|
356
|
+
qaRailMenuClose();
|
|
357
|
+
});
|
|
358
|
+
document.addEventListener("keydown", (e) => { if(e.key === "Escape") qaRailMenuClose(); });
|
|
359
|
+
window.addEventListener("resize", () => qaRailMenuClose());
|
|
360
|
+
qaRailLoad(true);
|
|
361
|
+
qaRailRender();
|
|
362
|
+
}
|