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.
Files changed (41) hide show
  1. package/CLIENT-CONTRACT.md +298 -0
  2. package/README.md +35 -0
  3. package/client-ui/assets/OFL-Caveat.txt +93 -0
  4. package/client-ui/assets/caveat.woff2 +0 -0
  5. package/client-ui/index.html +246 -0
  6. package/client-ui/pc/app.css +5110 -0
  7. package/client-ui/pc/assets/damiao-mascot.png +0 -0
  8. package/client-ui/pc/assets/icon.png +0 -0
  9. package/client-ui/pc/index.html +154 -0
  10. package/client-ui/pc/js/00-base.js +52 -0
  11. package/client-ui/pc/js/03-chat-message.js +880 -0
  12. package/client-ui/pc/js/04-task-list.js +430 -0
  13. package/client-ui/pc/js/05-task-list-render.js +812 -0
  14. package/client-ui/pc/js/06-ask-answer.js +204 -0
  15. package/client-ui/pc/js/07-chat-stream-render.js +493 -0
  16. package/client-ui/pc/js/08-ask-event-map.js +171 -0
  17. package/client-ui/pc/js/09-stream-apply.js +367 -0
  18. package/client-ui/pc/js/10-markdown.js +430 -0
  19. package/client-ui/pc/js/18-askbox.js +262 -0
  20. package/client-ui/pc/js/19-attachments.js +679 -0
  21. package/client-ui/pc/js/29-msg-variants.js +462 -0
  22. package/client-ui/pc/js/90-glue.js +212 -0
  23. package/client-ui/pc/js/91-workbench-stubs.js +134 -0
  24. package/client-ui/pc/js/92-composer.js +406 -0
  25. package/client-ui/pc/js/93-rail.js +362 -0
  26. package/client-ui/pc/vendor/highlight.min.js +1262 -0
  27. package/client-ui/pc/vendor/marked.min.js +6 -0
  28. package/client-ui/pc/vendor/purify.min.js +3 -0
  29. package/client-ui/shared/conversation.css +256 -0
  30. package/client-ui/shared/conversation.js +1225 -0
  31. package/client-ui/shared/transport-http.js +123 -0
  32. package/client-ui-mobile/assets/OFL-Caveat.txt +93 -0
  33. package/client-ui-mobile/assets/caveat.woff2 +0 -0
  34. package/client-ui-mobile/index.html +173 -0
  35. package/npm/bin/magic-agent.js +43 -0
  36. package/npm/dist/darwin-arm64/magic-agent +0 -0
  37. package/npm/dist/darwin-x64/magic-agent +0 -0
  38. package/npm/dist/linux-arm64/magic-agent +0 -0
  39. package/npm/dist/linux-x64/magic-agent +0 -0
  40. package/npm/dist/win32-x64/magic-agent.exe +0 -0
  41. package/package.json +4 -1
@@ -0,0 +1,212 @@
1
+ /* 适配层:让**逐字复制过来的** magic-test 对话框代码在两种宿主下都能跑起来。
2
+ *
3
+ * ── 只做两件事 ──
4
+ * ① 保证 `window.desk.*` 存在(上游代码只认这个接口):
5
+ * · 宿主已经注入了 desk(掌天瓶 Electron 壳的 preload)→ **直接用宿主的**;
6
+ * · 没有宿主桥(浏览器直开 / 静态预览)→ 这里装一个同名的适配,把通道接到 transport:
7
+ * desk.ask(payload) → transport.ask,事件按 rid 回推给 onAskEvent 订阅者
8
+ * desk.askStop(rid) → transport.control({op:'interrupt'})
9
+ * desk.askAppend({id,text}) → transport.control({op:'answer', text})
10
+ * desk.onAskEvent(cb) → 订阅(返回退订函数)
11
+ * desk.engines() → transport.listEngines(),并把 CLI 的 snake_case 归一成
12
+ * 上游约定的 {ok, engines:[{modelsNote, …}]}
13
+ * ② 挂载:askBoxMountAll(输入框)→ qaComposerInit(菜单 + 发送 + 流)。
14
+ *
15
+ * ── 关于「补丁函数」──
16
+ * 凡是**复制文件里已经带了实现**的,这里绝不重复声明(会直接撞成
17
+ * "Identifier has already been declared");只有真的没人定义的,才补最小实现,
18
+ * 且一律「如实说」——不假装成功、不静默失败。它们就是后面找增强点的清单。
19
+ *
20
+ * ⚠️ 维护提醒:往这儿加符号前,先确认复制文件里没有同名顶层声明。
21
+ */
22
+
23
+ /* ---------- 宿主桥优先 ----------
24
+ * ⚠️ 这一段是**集成时唯一要认的规矩**:
25
+ * · 页面被**宿主**打开(掌天瓶 Electron 壳就是),宿主 preload 已经注入了 `window.desk`
26
+ * → 直接用它。宿主那份是 IPC 直连 CLI,链路更短,而且有几个能力只有宿主给得了:
27
+ * 附件落盘 `desk.pasteImage`、按会话 id 追加 `desk.askAppend`、SQLite 任务库
28
+ * `desk.tasksList`。自己再包一层 transport 会把这些**顶掉**,症状是「附件贴不上、
29
+ * 作答变成新起一轮」。
30
+ * · 没有宿主桥(浏览器直接开 / 静态预览)→ 才自建:把 desk 的四个通道接到 transport。
31
+ * 两种情形下,下面的渲染层代码一个字都不用改(它只认 window.desk)。 */
32
+ const HOST_DESK = (window.desk && typeof window.desk.ask === "function") ? window.desk : null;
33
+
34
+ /* ---------- 传输层(仅无宿主桥时用) ---------- */
35
+ const MAGIC_TOKEN = window.MAGIC_TOKEN
36
+ || new URLSearchParams(location.search).get("token") || "";
37
+ const MAGIC_BASE = (window.MAGIC_BASE || "").replace(/\/+$/, "");
38
+ const transport = HOST_DESK ? null : window.MagicTransportHttp({ base: MAGIC_BASE, token: MAGIC_TOKEN });
39
+
40
+ /* ---------- 引擎行归一 ----------
41
+ 上游 magic-test 的 engines.cjs 会把 CLI 输出规范化;组件这一版直接吃 CLI 的原始行,
42
+ 所以在这里对齐字段名(`models_note` → `modelsNote` 等),其余原样透传。 */
43
+ function normalizeEngineRow(r){
44
+ const models = Array.isArray(r.models) ? r.models.map((m)=> (typeof m === "string" ? m : (m && m.id) || "" )).filter(Boolean) : [];
45
+ const labels = {};
46
+ (Array.isArray(r.models) ? r.models : []).forEach((m)=>{
47
+ if(m && typeof m === "object" && m.id) labels[m.id] = m.name || m.id;
48
+ });
49
+ return {
50
+ engine: r.engine,
51
+ ok: r.ok !== false,
52
+ bin: r.bin || "",
53
+ note: r.note || "",
54
+ models: models,
55
+ modelLabels: Object.assign(labels, r.modelLabels || {}),
56
+ modelCredits: r.model_credits || r.modelCredits || {},
57
+ modelsNote: r.models_note || r.modelsNote || "",
58
+ workspace: r.workspace || "",
59
+ streaming: r.streaming,
60
+ attachments: r.attachments || "",
61
+ permission: r.permission || "",
62
+ capabilities: r.capabilities || [],
63
+ };
64
+ }
65
+
66
+ /* ---------- desk 适配(只在没有宿主桥时安装) ---------- */
67
+ (function installDeskAdapter() {
68
+ if (HOST_DESK) return; // 宿主已经给了 desk,别覆盖它
69
+ const subs = [];
70
+ let ridSeq = 0;
71
+ /* ⚠️ 事件**延后一个微任务**再推给订阅者 —— 这不是洁癖,是修一个真实的竞态:
72
+ SSE 常把 `open` 与紧随其后的若干事件**放在同一个网络块**里回来(一次 write 合并成一段),
73
+ transport 会在一个 forEach 里把它们连着派给 onEvent。若在此**同步**转发,
74
+ 渲染层此时还在 `await desk.ask(...)` 里没恢复(run 尚未按 rid 登记),
75
+ `qaRuns.get(ev.rid)` 一律取不到 → **整轮事件被当过期事件丢光**:画面只剩用户气泡。
76
+ 推到微任务队列后,onOpen 触发的那个 Promise 续体(它才会 `qaRuns.set(rid, run)`)
77
+ 排在前面先跑,事件随后必被认领。顺序仍严格保持(逐个入队,FIFO)。 */
78
+ const emit = (rid, ev) => {
79
+ Promise.resolve().then(() => {
80
+ /* ⚠️ 订阅者抛错**要吭声**:静默 catch 会让「整轮画面不动」变成没有任何线索的哑故障
81
+ (事件明明推到了、渲染层却什么都没画 —— 排查时最费时间的一类)。 */
82
+ subs.forEach((fn) => {
83
+ try { fn(Object.assign({ rid: rid }, ev)); }
84
+ catch (e) { console.error("[desk] 事件订阅者处理失败", ev.type, e); }
85
+ });
86
+ });
87
+ };
88
+
89
+ window.desk = {
90
+ /* 起一轮。上游约定:**立即**回 {ok, rid}(渲染层据此登记 run),事件随后经 onAskEvent 推进来。
91
+ 所以这里等到 transport 的 onOpen(或失败)才 resolve —— 但**不阻塞**事件回推。 */
92
+ ask(payload) {
93
+ /* ⚠️ rid 必须是**数字**,不能图省事用 "r1" 这种字符串:上游代码拿它做 `Number.isFinite(rid)`
94
+ 判断(提问卡的答案要走 `--append` 追加回同一条会话,见 06-ask-answer.js 的 qaAskSend),
95
+ 字符串会判 false → 答案被降级成「排队,等这一轮结束再发」,而不是当场追加。
96
+ `0` 是上游的「占位 rid(尚未到手)」,所以从 1 起编号。 */
97
+ const rid = ++ridSeq;
98
+ return new Promise((resolve) => {
99
+ let settled = false;
100
+ const done = (r) => { if (!settled) { settled = true; resolve(r); } };
101
+ try {
102
+ transport.ask(payload, {
103
+ onOpen() { done({ ok: true, rid: rid }); },
104
+ onEvent(ev) { emit(rid, ev); },
105
+ onEnd(code) { emit(rid, { type: "end", code: code }); done({ ok: true, rid: rid }); },
106
+ onError(e) {
107
+ const msg = String((e && e.message) || e);
108
+ emit(rid, { type: "error", reason: msg });
109
+ done({ ok: false, error: msg });
110
+ },
111
+ });
112
+ } catch (e) {
113
+ const msg = String((e && e.message) || e);
114
+ emit(rid, { type: "error", reason: msg });
115
+ emit(rid, { type: "end", code: 1 });
116
+ done({ ok: false, error: msg });
117
+ }
118
+ /* 兜底:有些 transport 不回调 onOpen(一轮没有 open 帧)。给个上限,
119
+ 别让 qaSend 卡在 `await desk.ask` 上 —— 事件照旧按 rid 路由。 */
120
+ setTimeout(() => done({ ok: true, rid: rid }), 2000);
121
+ });
122
+ },
123
+ askStop(rid) { return transport.control({ id: rid, op: "interrupt" }); },
124
+ /* append = 往这条还活着的会话追加一条用户消息(引擎把它当下一轮)。
125
+ 在 core 那边就是 --control 的 answer。
126
+ ⚠️ 必须回 `{ok}` 形状:上游 qaSend 拿的是 `ar.ok`(见 07-chat-stream-render.js 的
127
+ `if(ar && ar.ok)`)。而 transport 的 control 只回一个裸 true —— 不归一的话,
128
+ 提问卡的答案会被判成「追加未送达」,白白回落成「新起一轮 + --session」,
129
+ 多一次冷启动(答案还是送得到,只是慢且费)。 */
130
+ askAppend(p) {
131
+ return Promise.resolve(transport.control({ id: p.id, op: "answer", text: p.text }))
132
+ .then(() => ({ ok: true }))
133
+ .catch((e) => ({ ok: false, error: String((e && e.message) || e) }));
134
+ },
135
+ onAskEvent(cb) { subs.push(cb); return () => { const i = subs.indexOf(cb); if (i >= 0) subs.splice(i, 1); }; },
136
+
137
+ /* 引擎清单:transport 给的是 CLI 原始行(可能带 snake_case)→ 归一成上游形状。 */
138
+ engines() {
139
+ return Promise.resolve()
140
+ .then(() => transport.listEngines())
141
+ .then((r) => {
142
+ const list = Array.isArray(r) ? r : (r && Array.isArray(r.engines) ? r.engines : null);
143
+ if (!list || !list.length) return { ok: false, engines: [] };
144
+ return { ok: true, engines: list.map(normalizeEngineRow) };
145
+ })
146
+ .catch(() => ({ ok: false, engines: [] }));
147
+ },
148
+
149
+ /* 附件落盘:上游是 Electron 的 pasteImage(把图写进磁盘并回路径)。
150
+ 这里没有本地壳,**不假装成功** —— 明说这一版还不支持粘贴落盘。 */
151
+ pasteImage() {
152
+ return Promise.reject(new Error("这一版(浏览器宿主)还不支持粘贴截图落盘:需要宿主提供 pasteImage 通道"));
153
+ },
154
+ /* 任务本地库(SQLite):magic-agent 没有本地库 —— 空清单 + 写入如实回「跳过」。
155
+ 04-task-list.js 的 qaDb* 会把这些结果当「桥不可用」,于是只走 localStorage。 */
156
+ tasksList() { return Promise.resolve({ ok: true, tasks: [] }); },
157
+ tasksCreate(t) { return Promise.resolve({ ok: true, task: t }); },
158
+ tasksUpdate() { return Promise.resolve({ ok: true }); },
159
+ tasksDelete() { return Promise.resolve({ ok: true }); },
160
+ convList() { return Promise.resolve({ ok: true, messages: [] }); },
161
+ convAppend() { return Promise.resolve({ ok: true }); },
162
+ settingsGet() { return Promise.resolve({ ok: true, config: {} }); },
163
+ /* 模型链(配置界面那份「主选 + 兜底」的有序链):这一版没有配置界面 → 空链。
164
+ ⚠️ 必须给这个通道:04-task-list.js 的 qaModelChainLoad 在没有它时会去 fetch
165
+ `/api/settings/model-chain`(那是 magic-test 内置后端的路),本版没有那个端点 →
166
+ 每次都留一条 404 噪音。空链的语义是「用引擎自带默认」,与 04 的注释一致,不编数据。 */
167
+ settingsModelChain() { return Promise.resolve({ ok: true, chain: [] }); },
168
+ };
169
+ })();
170
+
171
+ /* ---------- 工作台侧符号的最小实现 ----------
172
+ 只补「复制文件里确实没有」的那些。真被走到时如实说,不静默。 */
173
+ function notHere(what) {
174
+ return function () {
175
+ toast(what + ":这一版(magic-agent 基础对话 UI)还没接上");
176
+ console.warn("[conv] 未实现:" + what);
177
+ };
178
+ }
179
+ /* 视图切换:这里只有对话视图,所以是恒等操作(上游发送时会调它跳转到 chat) */
180
+ function showView(v) { state.view = v || "chat"; }
181
+ function createTask() { return { id: "conv-" + Date.now() }; }
182
+ function openPanel() { /* 上游:打开流水线督造面板;这一版没有 */ }
183
+ function syncWebviewViewport() { /* 上游:内嵌浏览器等比缩放;这一版没有 */ }
184
+ function isDockOpen() { return false; }
185
+ function openBrowserDock() { notHere("浏览器侧栏")(); }
186
+ function wsDirOf() { return ""; }
187
+ function genCasesFromDoc() { notHere("生成测试用例")(); }
188
+ function reqFeatPaneHint() { /* 上游:右栏文档阅读器的提示;无右栏 */ }
189
+ function reqShotsPaneHint() { /* 同上 */ }
190
+ function bindAsk() { /* 上游:提问卡选项的事件绑定;askCard 内部已自带 */ }
191
+ function agentCall() { notHere("模拟测试")(); }
192
+ function notify() { /* 上游:系统通知;无壳,忽略 */ }
193
+
194
+ /* ---------- 挂载 ---------- */
195
+ (function boot() {
196
+ try {
197
+ /* 输入框:上游的挂载入口,注入标记到 #ask-home */
198
+ if (typeof askBoxMountAll === "function") askBoxMountAll();
199
+ else throw new Error("askBoxMountAll 不存在(19 是否加载?)");
200
+ /* 菜单 + 发送 + 流:撰写区总装(92-composer.js) */
201
+ if (typeof qaComposerInit === "function") qaComposerInit();
202
+ else throw new Error("qaComposerInit 不存在(92 是否加载?)");
203
+ /* 左栏会话清单(93-rail.js):数据来自 magic-agent 的 --sessions / --session-log */
204
+ if (typeof qaRailInit === "function") qaRailInit();
205
+ } catch (e) {
206
+ document.body.insertAdjacentHTML("beforeend",
207
+ '<div style="position:fixed;left:16px;bottom:16px;padding:12px 14px;border:1px solid #F4C7C7;'
208
+ + 'background:#FFF4F4;color:#B42318;border-radius:10px;font:13px/1.5 system-ui">'
209
+ + "对话界面挂载失败:" + escHtml((e && e.message) || e) + "</div>");
210
+ throw e;
211
+ }
212
+ })();
@@ -0,0 +1,134 @@
1
+ /* 工作台侧符号 —— 对话渲染层会引用,但它们属于**工作台**(需求看板 / 文档阅读器 /
2
+ * 消息库 / 截图相册),magic-agent 这一版只有对话。
3
+ *
4
+ * ── 两类,别混 ──
5
+ * ① **纯函数**:与工作台无关、只跟对话状态有关的(wsMatch / qaReqOfId / qaTaskStatusOf /
6
+ * qaEngHintSync / qaRefreshRunning)—— 从 magic-test 原样搬来,语义一字不改。
7
+ * ② **诚实桩**:这条能力这一版没有 —— 一律**明说**(toast / console.warn),
8
+ * 不假装成功、不静默吞掉。这些桩就是「还没接上」的清单,也是后续增强点的来源。
9
+ */
10
+
11
+ /* ================= ① 纯函数(原样搬自 magic-test) ================= */
12
+
13
+ /* 02-docs-data.js:qaRunning 是派生量(由 qaRefreshRunning 重算),qaUnsub 是事件订阅句柄 */
14
+ let qaRunning = false, qaUnsub = null;
15
+ /* 引擎清单是否已从 CLI 取回(true = qaEngines 是真实清单;false = 还是内置兜底表)。
16
+ 跨文件共享状态:92-composer.js 的 qaEnginesFetch 写它,04-task-list.js 的 qaPermSupported 读它。
17
+ 两侧都只在**运行时**访问(不在脚本加载期),所以归到这份共享状态里最清楚。 */
18
+ let qaEnginesReal = false;
19
+ /** 07/03 都读它:当前有没有在跑的轮次。**唯一算法**,别各处自己数 qaRuns。 */
20
+ function qaRefreshRunning(){ qaRunning = qaRuns.size > 0; qaSyncBusy(); }
21
+
22
+ /* 02-docs-data.js:消息图标常量。**纯常量**(没有一条与工作台相关)—— 原样搬来。
23
+ 03-chat-message.js 的提问卡(askCard)读 QA_ICONS.ask;漏了它 → 审批卡整张抛
24
+ ReferenceError(画面表现:引擎问了、界面什么都没出现)。 */
25
+ const QA_ICONS = {
26
+ quote: '<svg viewBox="0 0 24 24"><path d="M4 6h16M4 11h10M4 16h13"/></svg>',
27
+ reply: '<svg viewBox="0 0 24 24"><path d="M21 12a8 8 0 01-8 8H5l-2 2V12a8 8 0 018-8h2a8 8 0 018 8z"/></svg>',
28
+ tool: '<svg viewBox="0 0 24 24"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg>',
29
+ agent: '<svg viewBox="0 0 24 24"><path d="M12 3v3M8 6h8a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H8a3 3 0 0 1-3-3V9a3 3 0 0 1 3-3z"/><path d="M9.5 11.5h.01M14.5 11.5h.01M9.5 15h5"/></svg>',
30
+ ask: '<svg viewBox="0 0 24 24"><path d="M21 11.5a8 8 0 0 1-8 8H7l-4 3v-9a8 8 0 0 1 8-8h2a8 8 0 0 1 8 6z"/><path d="M9.6 9.3a2.5 2.5 0 1 1 3.1 2.4c-.5.2-.8.6-.8 1.1v.5"/><path d="M12 16.2h.01"/></svg>',
31
+ };
32
+
33
+ /* 12-req-board.js:工作目录归属判定。本版没有「工作目录切换器」,wsDirOf() 恒为 ""
34
+ → 第一行 `if(!d) return true` 直接放行(= 不按目录过滤)。整段保留,语义与上游一致。 */
35
+ const WS_DEMO_PLACEHOLDER = "__DEFAULT__";
36
+ let qaWsDefaultDir = "";
37
+ function wsMatch(recWs){
38
+ const d = wsDirOf();
39
+ if(!d) return true; // 「不记录需求(仅浏览)」= 不按目录过滤
40
+ let rec = String(recWs || "");
41
+ if(rec === WS_DEMO_PLACEHOLDER) rec = qaWsDefaultDir || "";
42
+ if(rec) return rec === d;
43
+ return !!(qaWsDefaultDir && d === qaWsDefaultDir);
44
+ }
45
+
46
+ /* 12-req-board.js:会话 id ↔ 需求 id。本版不派发需求,但 id 前缀解析是通用的,原样保留。 */
47
+ const REQ_TASK_PREFIX = "qa-req-";
48
+ function qaReqOfId(id){
49
+ const s = String(id || "");
50
+ return s.startsWith(REQ_TASK_PREFIX) ? s.slice(REQ_TASK_PREFIX.length) : "";
51
+ }
52
+ /* 需求**侧**那条 —— 需要需求看板。本版没有看板 → 查不到(如实返回 null,
53
+ 于是 qaTaskStatusOf 走「普通对话」兜底分支,显示会话自己的 status)。 */
54
+ function qaReqItemOf(){ return null; }
55
+ function qaReqBoardLoaded(){ return false; }
56
+
57
+ /* 12-req-board.js:会话状态口径。因 qaReqItemOf 恒 null,实际走的是下面这段兜底
58
+ (与上游逐字一致 —— 兜底词表与需求侧同一批状态名)。 */
59
+ function qaTaskStatusOf(t){
60
+ const raw = String((t && t.status) || "done");
61
+ const st = raw === "queued" ? "pending" : raw; // 老值兼容
62
+ const it = qaReqItemOf(t && t.reqId);
63
+ if(it && typeof qaReqStatusOf === "function") return qaReqStatusOf(it);
64
+ const label = st === "running" ? "处理中"
65
+ : st === "verify" ? "待验证"
66
+ : st === "pending" ? "排队中"
67
+ : st === "stopped" ? "已停止"
68
+ : st === "error" ? "失败"
69
+ : "已完成";
70
+ const detail = st === "running" ? "流式输出,命令行退出即完成"
71
+ : st === "verify" ? "等审查结论"
72
+ : st === "pending" ? "等前一条结束(同一工作目录串行执行)"
73
+ : st === "stopped" ? "你自己喊的停"
74
+ : st === "error" ? "上次调用失败"
75
+ : "";
76
+ return { st, phase: "", label, detail, live: st === "pending" || st === "running", manual: false };
77
+ }
78
+
79
+ /* 12-req-board.js:+ 菜单底部「派发引擎 / 模型 / 工作目录」那行说明。原样保留。 */
80
+ function qaEngHintSync(){
81
+ const dir = wsDirOf();
82
+ const ws = !dir ? ""
83
+ : (qaWorkspaceSupported() ? ` · 工作目录 ${dir}` : " · 该引擎不支持指定工作目录");
84
+ ASK_INSTANCES.forEach(p=>{
85
+ const el = $("#" + p + "-eng-hint"); if(!el) return;
86
+ const mdl = (typeof qaModelFor === "function") ? qaModelFor(p, qaActiveId) : qaModel;
87
+ el.textContent = `派发引擎 ${qaEngine}${mdl ? " · " + qaModelName(mdl, qaEngine) : ""}${ws}`;
88
+ });
89
+ }
90
+
91
+ /* 27-conv-log.js:找不到历史时那句说明(如实)。原样保留。 */
92
+ function qaConvNoticeHtml(text, kind){
93
+ return `<div class="qa-hist-note${kind ? " " + kind : ""}">${escHtml(text)}</div>`;
94
+ }
95
+ function qaConvLostText(t){
96
+ const hasSid = !!(t && t.sessionId);
97
+ return "这条对话的历史没有留存下来(这一版还没有本地消息库)。"
98
+ + (hasSid ? "会话本身还在 —— 直接在这里追问,引擎会接着原来的上下文继续。" : "这一轮没有拿到会话 id,追问会新起一段上下文。");
99
+ }
100
+
101
+ /* ================= ② 诚实桩(这条能力这一版没有) ================= */
102
+
103
+ /* 内置后端基址:magic-test 靠它取引擎清单 / 上传截图。magic-agent 走 transport,
104
+ 不需要它 —— 声明成空串,所有 `if(prdBackendBase)` 分支自然短路到相对路径 / 直接失败。 */
105
+ var prdBackendBase = "";
106
+
107
+ /* 需求看板(12-req-board.js)—— 本版无看板 */
108
+ function reqHas(){ return false; }
109
+ function reqBoardFetch(){ return Promise.resolve(null); }
110
+ function reqBoardAddFromChat(){ return Promise.resolve(null); }
111
+ function qaReqConvMsg(){ return ""; }
112
+ function qaReqEngineMsg(){ return ""; }
113
+ function qaReqStreamApply(){ return false; }
114
+ function qaReqConvSnapshot(){ return ""; }
115
+ function qaReqConvSync(){ /* 无看板可同步 */ }
116
+ function qaReqConvDropMark(){ /* 无看板可标记 */ }
117
+ function qaTaskHistoryRecover(){ return Promise.resolve(""); }
118
+
119
+ /* 消息库 / 对话归档(27-conv-log.js)—— 本版无持久层。
120
+ ⚠️ 这意味着**刷新页面历史就没了**:左栏条目来自 localStorage 的 qaTasks(04-task-list.js),
121
+ 点开时靠 t.snapshot 还原当前会话画面;其余(逐轮消息表)没有。持久化 = 增强点之一。 */
122
+ function qaMsgLogLoad(){ return Promise.resolve([]); }
123
+ function qaConvTurnSync(){ /* 无消息库可写 */ }
124
+
125
+ /* 截图相册 / 文档阅读器(右栏)—— 本版无右栏 */
126
+ function reqShotUrl(){ return ""; }
127
+ function reqFeatOpenInDock(){ notHere("打开功能点文档")(); }
128
+ function reqShotsOpen(){ notHere("截图相册")(); }
129
+
130
+ /* 产品文档页的数据源(10-markdown.js 的文档阅读器读它们)—— 本版无文档页,留空即死路径。 */
131
+ let prdData = [];
132
+ let prdDetailIndex = {};
133
+ let prdRowsAll = [];
134
+ const prdStateText = {};