magic-agent 2.4.3 → 2.6.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.
Files changed (153) hide show
  1. package/CLIENT-CONTRACT.md +284 -30
  2. package/README.md +15 -14
  3. package/client-ui/pc/app.css +32 -0
  4. package/client-ui/pc/js/04-task-list.js +5 -1
  5. package/client-ui/pc/js/07-chat-stream-render.js +3 -3
  6. package/client-ui/pc/js/90-glue.js +37 -2
  7. package/client-ui/pc/js/92-composer.js +19 -4
  8. package/client-ui/pc/js/93-rail.js +16 -5
  9. package/client-ui/shared/conv/02-media-kinds.js +2 -2
  10. package/client-ui/shared/conv/03-chat-message.js +241 -23
  11. package/client-ui/shared/conv/06-ask-answer.js +75 -1
  12. package/client-ui/shared/conv/09-stream-apply.js +63 -7
  13. package/client-ui/shared/conv/19-attachments.js +157 -6
  14. package/client-ui/shared/conv/27-conv-ledger.js +5 -2
  15. package/client-ui/shared/conv/29-msg-variants.js +81 -0
  16. package/client-ui/shared/conv/conv.css +79 -19
  17. package/client-ui/shared/conv/dom-map.md +1 -0
  18. package/client-ui/shared/conversation.css +13 -2
  19. package/client-ui/shared/conversation.js +336 -50
  20. package/client-ui/shared/transport-http.js +14 -0
  21. package/client-ui-mobile/index.html +2932 -333
  22. package/client-ui-mobile/reqs.css +102 -0
  23. package/client-ui-mobile/reqs.js +485 -70
  24. package/client-ui-mobile/reqshot.js +505 -0
  25. package/client-ui-mobile/scripts.css +21 -0
  26. package/client-ui-mobile/scripts.js +539 -0
  27. package/client-ui-mobile/vendor/cose-base.js +3214 -0
  28. package/client-ui-mobile/vendor/cytoscape-fcose.js +1549 -0
  29. package/client-ui-mobile/vendor/cytoscape.min.js +32 -0
  30. package/client-ui-mobile/vendor/layout-base.js +5230 -0
  31. package/cmd/magic-client/browsectl.go +199 -0
  32. package/cmd/magic-client/clientctl.go +33 -9
  33. package/cmd/magic-client/main.go +57 -26
  34. package/internal/agent/ask.go +45 -0
  35. package/internal/agent/ask_test.go +39 -0
  36. package/internal/agent/attachments_test.go +6 -2
  37. package/internal/agent/capability.go +7 -1
  38. package/internal/agent/claude.go +20 -15
  39. package/internal/agent/codebuddy.go +125 -34
  40. package/internal/agent/codebuddy_account_test.go +54 -0
  41. package/internal/agent/codebuddy_stream_test.go +103 -0
  42. package/internal/agent/credits.go +713 -0
  43. package/internal/agent/credits_minimax_test.go +97 -0
  44. package/internal/agent/credits_test.go +211 -0
  45. package/internal/agent/custom_models.go +155 -0
  46. package/internal/agent/custom_models_test.go +125 -0
  47. package/internal/agent/dsh.go +123 -49
  48. package/internal/agent/dsh_resume.go +13 -2
  49. package/internal/agent/dsh_test.go +108 -6
  50. package/internal/agent/engine.go +37 -1
  51. package/internal/agent/engine_base.go +21 -17
  52. package/internal/agent/engine_base_test.go +2 -2
  53. package/internal/agent/engine_test.go +47 -86
  54. package/internal/agent/fallback.go +313 -0
  55. package/internal/agent/fallback_test.go +340 -0
  56. package/internal/agent/launch.go +43 -1
  57. package/internal/agent/llmhttp.go +46 -4
  58. package/internal/agent/llmhttp_test.go +157 -0
  59. package/internal/agent/models.go +53 -1
  60. package/internal/agent/models_test.go +20 -5
  61. package/internal/agent/pidalive_windows_test.go +40 -0
  62. package/internal/agent/runcmd.go +26 -0
  63. package/internal/agent/runcmd_windows.go +36 -5
  64. package/internal/agent/stream.go +136 -6
  65. package/internal/agent/stream_test.go +25 -38
  66. package/internal/agent/stream_timeline_test.go +2 -37
  67. package/internal/agent/streamjson.go +108 -6
  68. package/internal/agent/tools_suffix_test.go +6 -3
  69. package/internal/agent/trae.go +50 -243
  70. package/internal/agent/trae_exec.go +433 -0
  71. package/internal/agent/trae_exec_test.go +181 -0
  72. package/internal/agent/wraplaunch_test.go +113 -0
  73. package/internal/cli/ask.go +160 -37
  74. package/internal/cli/ask_test.go +1 -1
  75. package/internal/cli/checkin.go +77 -0
  76. package/internal/cli/clientcmd.go +3 -2
  77. package/internal/cli/control.go +46 -5
  78. package/internal/cli/control_test.go +65 -0
  79. package/internal/cli/events.go +4 -0
  80. package/internal/cli/quotas.go +125 -0
  81. package/internal/cli/requirements.go +51 -0
  82. package/internal/cli/root.go +122 -5
  83. package/internal/cli/session.go +6 -0
  84. package/internal/cli/suggest.go +7 -5
  85. package/internal/cli/team.go +37 -9
  86. package/internal/cli/team_test.go +39 -0
  87. package/internal/client/ab.go +372 -0
  88. package/internal/client/ab_test.go +287 -0
  89. package/internal/client/browse.go +84 -36
  90. package/internal/client/browse_test.go +107 -9
  91. package/internal/client/browseinput.go +481 -0
  92. package/internal/client/browseinput_test.go +216 -0
  93. package/internal/client/corecmd.go +23 -0
  94. package/internal/client/corecmd_other.go +8 -0
  95. package/internal/client/corecmd_windows.go +24 -0
  96. package/internal/client/daemon.go +90 -17
  97. package/internal/client/daemon_test.go +33 -0
  98. package/internal/client/engines_checkin_test.go +57 -0
  99. package/internal/client/fallback_flags_test.go +64 -0
  100. package/internal/client/jobs.go +294 -21
  101. package/internal/client/jobs_test.go +299 -2
  102. package/internal/client/knowledge.go +854 -0
  103. package/internal/client/knowledge_test.go +510 -0
  104. package/internal/client/lan.go +3 -3
  105. package/internal/client/quotas_merge_test.go +107 -0
  106. package/internal/client/requirements.go +198 -1
  107. package/internal/client/requirements_shots_test.go +200 -0
  108. package/internal/client/restart.go +134 -0
  109. package/internal/client/restart_test.go +151 -0
  110. package/internal/client/scripts.go +1047 -0
  111. package/internal/client/scripts_decode_other.go +27 -0
  112. package/internal/client/scripts_decode_test.go +125 -0
  113. package/internal/client/scripts_decode_windows.go +206 -0
  114. package/internal/client/scripts_test.go +726 -0
  115. package/internal/client/service.go +322 -18
  116. package/internal/client/service_test.go +51 -0
  117. package/internal/client/suggest.go +105 -2
  118. package/internal/client/suggest_test.go +86 -0
  119. package/internal/client/team_flags.go +25 -11
  120. package/internal/client/team_flags_test.go +39 -0
  121. package/internal/requirements/bootstrap.go +16 -0
  122. package/internal/requirements/consolidate.go +100 -9
  123. package/internal/requirements/doc.go +154 -3
  124. package/internal/requirements/graph.go +138 -0
  125. package/internal/requirements/memory.go +7 -0
  126. package/internal/requirements/modules.go +312 -0
  127. package/internal/requirements/modules_test.go +373 -0
  128. package/internal/requirements/relations.go +155 -0
  129. package/internal/requirements/relations_test.go +231 -0
  130. package/internal/requirements/requirements.go +10 -0
  131. package/internal/requirements/shots.go +418 -0
  132. package/internal/requirements/shots_test.go +387 -0
  133. package/internal/session/session.go +92 -0
  134. package/internal/suggest/suggest.go +23 -1
  135. package/internal/team/event.go +3 -1
  136. package/internal/team/execute.go +21 -6
  137. package/internal/team/fallback_route_test.go +80 -0
  138. package/internal/team/planner.go +15 -3
  139. package/internal/team/route.go +89 -17
  140. package/internal/team/team.go +18 -4
  141. package/npm/apk/magic-agent-mobile-release.apk +0 -0
  142. package/npm/apk/version.json +1 -5
  143. package/npm/dist/darwin-arm64/magic-agent +0 -0
  144. package/npm/dist/darwin-arm64/magic-client +0 -0
  145. package/npm/dist/darwin-x64/magic-agent +0 -0
  146. package/npm/dist/darwin-x64/magic-client +0 -0
  147. package/npm/dist/linux-arm64/magic-agent +0 -0
  148. package/npm/dist/linux-arm64/magic-client +0 -0
  149. package/npm/dist/linux-x64/magic-agent +0 -0
  150. package/npm/dist/linux-x64/magic-client +0 -0
  151. package/npm/dist/win32-x64/magic-agent.exe +0 -0
  152. package/npm/dist/win32-x64/magic-client.exe +0 -0
  153. package/package.json +8 -2
@@ -0,0 +1,505 @@
1
+ /* 需求页「页面完成情况」截图与标注(移动端专用子页面模块)。
2
+ *
3
+ * 需求(2026-10-25 用户):「需求文档整理时 如果设计页面改造 调用内置浏览器mcp
4
+ * 对完成情况截图标注并放在文档中」。
5
+ *
6
+ * ── 链路(谁做什么;2026-10-25 MCP 改基 agent-browser)──
7
+ * 1. 需求页功能点行上出「相机徽标」(有 page_url 才有;归纳时从对话里提取的页面地址)。
8
+ * 点徽标 → 本模块开一张抽屉:页面地址(可改)+ 标注说明(可改)。
9
+ * 2. 「打开页面并截图」→ 两步走:
10
+ * a) **强制走桌面端中转**(/b/)把页面开进手机浏览器:给用户看、滚到完成状态。
11
+ * 中转里注入的 relay 只负责「远程鼠标」(触控镜像),**不再承担截图**。
12
+ * b) **服务端 agent-browser**(真实 Chromium)按页面地址重新打开并截**全页图**:
13
+ * POST /desk/requirements/shot-remote {"url":…} → {data:"/desk/uploads/…png"}。
14
+ * 服务端没装 agent-browser 时端点回 501 + 安装提示(诚实报错,不假装能截)。
15
+ * 3. 拿到图片 → 开**标注浮层**(矩形 / 文字 / 三色 / 撤销 / 清除)。
16
+ * 4. 「完成」→ canvas.toBlob('image/png') → POST /desk/requirements/shots(**原始字节**
17
+ * 不是 multipart)→ core 落盘并把这一行追加进该功能点文档的「页面完成情况」段 →
18
+ * 刷新需求清单。
19
+ *
20
+ * ── 已知边界 ──
21
+ * · 服务端截的是 agent-browser 独立 profile 里的 page_url:需要登录的页面截出来
22
+ * 可能是登录页(不共享中转 cookie jar);截的是页面加载完的初始状态,不是
23
+ * 手机 iframe 里滚到的位置。
24
+ * · 标注只有矩形与文字(没有箭头 / 马赛克 / 裁剪)。
25
+ *
26
+ * 与宿主(index.html)的边界:只通过 mount(host) 注入的能力交互 ——
27
+ * getBase / getToken、toast、hEl / elSpan、openSheet / closeSheet、
28
+ * openBrowseProxy / proxyURL、setRelayOn、setShotBtn、refreshReqs。
29
+ * 截图链路的形状见 CLIENT-CONTRACT.md §3.4.1(shot-remote / shots 端点)与「浏览器 MCP」段。
30
+ */
31
+ (function () {
32
+ 'use strict';
33
+
34
+ var H = null; // 宿主(index.html 注入)
35
+ var TASK = null; // 当前截图任务 {ws, module, name, caption, url}
36
+ var SHOTTING = false; // 服务端截图进行中
37
+ var ANNO = null; // 标注浮层句柄
38
+ var SHOT_WAIT = 90000; // 服务端截图的上限(开页 + 冷启动 daemon 可能十几秒)
39
+ var RELAY_WAS = null; // 进来时中转开关的状态(结束时还原)
40
+
41
+ /* 标注颜色(三色:红/黄/绿)。深色底上的图也要看得清,所以文字额外描一圈深边。 */
42
+ var COLORS = ['#e5484d', '#f5a524', '#2ea043'];
43
+
44
+ function hEl(tag, cls, text) { return H.hEl(tag, cls, text); }
45
+ function elSpan(cls, text) { return H.elSpan(cls, text); }
46
+ function toast(msg) { return H.toast(msg); }
47
+
48
+ /* ── 抽屉:页面地址 + 标注说明 ──
49
+ URL 与说明都可改:归纳出来的页面地址可能提错或漏提(用户 2026-10-25 的原话
50
+ 就是「涉及页面改造」—— 具体哪个页面人来定更准)。 */
51
+ function openSheet(task) {
52
+ TASK = task;
53
+ H.openSheet('页面完成情况截图', function (body) {
54
+ var hint = hEl('div', 'reqshot__hint',
55
+ '点「打开页面并截图」:手机端开页面供你查看,同时服务端浏览器对页面截一张全页图,再由你标注。');
56
+ body.appendChild(hint);
57
+
58
+ var f1 = hEl('div', 'field');
59
+ var l1 = document.createElement('label');
60
+ l1.textContent = '页面地址';
61
+ f1.appendChild(l1);
62
+ var inpUrl = document.createElement('input');
63
+ inpUrl.type = 'text';
64
+ inpUrl.value = task.url || '';
65
+ inpUrl.placeholder = 'https://example.com/app';
66
+ inpUrl.setAttribute('autocapitalize', 'off');
67
+ inpUrl.setAttribute('autocorrect', 'off');
68
+ inpUrl.setAttribute('spellcheck', 'false');
69
+ f1.appendChild(inpUrl);
70
+ body.appendChild(f1);
71
+
72
+ var f2 = hEl('div', 'field');
73
+ var l2 = document.createElement('label');
74
+ l2.textContent = '标注说明(写进需求文档)';
75
+ f2.appendChild(l2);
76
+ var inpCap = document.createElement('input');
77
+ inpCap.type = 'text';
78
+ inpCap.value = task.caption || '';
79
+ inpCap.placeholder = '例如:双列布局已生效';
80
+ inpCap.setAttribute('autocapitalize', 'off');
81
+ inpCap.setAttribute('autocorrect', 'off');
82
+ inpCap.setAttribute('spellcheck', 'false');
83
+ f2.appendChild(inpCap);
84
+ body.appendChild(f2);
85
+
86
+ var err = hEl('div', 'field__err');
87
+ err.hidden = true;
88
+ body.appendChild(err);
89
+
90
+ var ok = hEl('button', 'btn btn--filled', '打开页面并截图');
91
+ ok.type = 'button';
92
+ body.appendChild(ok);
93
+
94
+ function fail(msg) { err.textContent = msg; err.hidden = false; }
95
+ function go() {
96
+ var u = String(inpUrl.value || '').trim();
97
+ if (!u) { fail('页面地址不能为空'); inpUrl.focus(); return; }
98
+ if (!/^https?:\/\//i.test(u)) { fail('页面地址要以 http:// 或 https:// 开头'); inpUrl.focus(); return; }
99
+ TASK.url = u;
100
+ TASK.caption = String(inpCap.value || '').trim();
101
+ H.closeSheet();
102
+ start(u);
103
+ }
104
+ ok.onclick = go;
105
+ inpUrl.onkeydown = function (e) { if (e.key === 'Enter') { e.preventDefault(); go(); } };
106
+ setTimeout(function () { try { inpUrl.focus(); inpUrl.select(); } catch (e) {} }, 30);
107
+ });
108
+ }
109
+
110
+ /* ── 打开页面 + 服务端截图 ──
111
+ 手机端走中转开页面(给用户看 + 触控);截图由服务端 agent-browser 完成。 */
112
+ function start(url) {
113
+ if (!H.getBase() || !H.getToken()) {
114
+ toast('未配对桌面端,截图不可用');
115
+ return;
116
+ }
117
+ /* 中转开关被我们强开(让用户能看着页面)—— 记下原值,流程结束时还原。 */
118
+ if (RELAY_WAS === null) RELAY_WAS = !!H.relayOn();
119
+ H.setRelayOn(true);
120
+ H.openBrowseProxy(H.proxyURL(url));
121
+ toast('页面已打开,服务端截图中…');
122
+ capture();
123
+ }
124
+
125
+ /* ── 发起截图(服务端 agent-browser)──
126
+ 抽屉的「打开页面并截图」直接走这里;浏览器工具栏的「截图」按钮同样。 */
127
+ function capture() {
128
+ if (!TASK || !TASK.url) { toast('先从需求页点功能点上的相机徽标'); return; }
129
+ if (SHOTTING) { toast('正在截图,请稍候'); return; }
130
+ var base = String(H.getBase() || '');
131
+ if (!base) { toast('未配对桌面端,截图不可用'); return; }
132
+ SHOTTING = true;
133
+ var hdr = { 'Content-Type': 'application/json' };
134
+ if (H.getToken()) hdr['X-Magic-Token'] = H.getToken();
135
+ fetch(base + '/desk/requirements/shot-remote', {
136
+ method: 'POST', headers: hdr,
137
+ body: JSON.stringify({ url: TASK.url, full_page: true })
138
+ })
139
+ .then(function (r) {
140
+ return r.text().then(function (t) {
141
+ var d = {};
142
+ try { d = JSON.parse(t); } catch (e) {}
143
+ if (!r.ok) throw new Error(d.error || ('HTTP ' + r.status));
144
+ return d;
145
+ });
146
+ })
147
+ .then(function (d) {
148
+ SHOTTING = false;
149
+ var rel = String(d.data || '');
150
+ if (!rel) { toast('截图失败:服务端没有回传图片地址'); return; }
151
+ openAnno(rel);
152
+ })
153
+ .catch(function (e) {
154
+ SHOTTING = false;
155
+ toast('截图失败:' + ((e && e.message) || e));
156
+ });
157
+ /* 兜底超时:服务端卡住时不至于永远转圈。 */
158
+ setTimeout(function () {
159
+ if (SHOTTING) {
160
+ SHOTTING = false;
161
+ toast('截图超时(服务端浏览器打开页面太慢)');
162
+ }
163
+ }, SHOT_WAIT);
164
+ }
165
+
166
+ /* ── 标注浮层 ── */
167
+ function openAnno(relURL) {
168
+ closeAnno();
169
+ var base = String(H.getBase() || '');
170
+ var tok = String(H.getToken() || '');
171
+ var imgURL = base + relURL + (relURL.indexOf('?') >= 0 ? '&' : '?') +
172
+ 'token=' + encodeURIComponent(tok);
173
+
174
+ var ov = hEl('div', 'reqshot-anno');
175
+ var bar = hEl('div', 'reqshot-anno__bar');
176
+ var stage = hEl('div', 'reqshot-anno__stage');
177
+ var cv = document.createElement('canvas');
178
+ cv.className = 'reqshot-anno__cv';
179
+ stage.appendChild(cv);
180
+ var hint = hEl('div', 'reqshot-anno__hint', '加载截图…');
181
+ stage.appendChild(hint);
182
+
183
+ var tool = 'rect';
184
+ var color = COLORS[0];
185
+
186
+ function mkBtn(cls, label, onClick) {
187
+ var b = hEl('button', cls, label);
188
+ b.type = 'button';
189
+ b.onclick = onClick;
190
+ return b;
191
+ }
192
+ var bRect = mkBtn('reqshot-anno__tbtn', '矩形', function () { setTool('rect'); });
193
+ var bText = mkBtn('reqshot-anno__tbtn', '文字', function () { setTool('text'); });
194
+ var bUndo = mkBtn('reqshot-anno__tbtn', '撤销', function () { undo(); });
195
+ var bClear = mkBtn('reqshot-anno__tbtn', '清除', function () { ANNO.ops = []; draw(); });
196
+ bar.appendChild(bRect);
197
+ bar.appendChild(bText);
198
+ bar.appendChild(bUndo);
199
+ bar.appendChild(bClear);
200
+
201
+ var colors = hEl('div', 'reqshot-anno__colors');
202
+ var cBtns = [];
203
+ COLORS.forEach(function (c) {
204
+ var b = hEl('button', 'reqshot-anno__c');
205
+ b.type = 'button';
206
+ b.style.background = c;
207
+ b.onclick = function () { color = c; paintC(); };
208
+ colors.appendChild(b);
209
+ cBtns.push(b);
210
+ });
211
+ bar.appendChild(colors);
212
+
213
+ var acts = hEl('div', 'reqshot-anno__acts');
214
+ var bCancel = mkBtn('reqshot-anno__tbtn reqshot-anno__tbtn--mute', '取消', function () { closeAnno(); });
215
+ var bDone = mkBtn('reqshot-anno__tbtn reqshot-anno__tbtn--ok', '完成', function () { done(); });
216
+ bDone.disabled = true;
217
+ acts.appendChild(bCancel);
218
+ acts.appendChild(bDone);
219
+ bar.appendChild(acts);
220
+
221
+ ov.appendChild(bar);
222
+ ov.appendChild(stage);
223
+ document.body.appendChild(ov);
224
+
225
+ var ctx = cv.getContext('2d');
226
+ ANNO = {
227
+ ov: ov, cv: cv, ctx: ctx, stage: stage, hint: hint,
228
+ base: null, ops: [], pending: null, tool: tool, color: color,
229
+ textInput: null
230
+ };
231
+
232
+ function setTool(t) {
233
+ tool = t;
234
+ ANNO.tool = t;
235
+ bRect.classList.toggle('is-on', t === 'rect');
236
+ bText.classList.toggle('is-on', t === 'text');
237
+ }
238
+ function paintC() {
239
+ cBtns.forEach(function (b, i) { b.classList.toggle('is-on', COLORS[i] === color); });
240
+ }
241
+ setTool('rect');
242
+ paintC();
243
+
244
+ /* 图片加载:relay 上传到 staging 的那张图(可能很大,等一会儿)。 */
245
+ var img = new Image();
246
+ img.onload = function () {
247
+ if (!ANNO || ANNO.cv !== cv) return;
248
+ cv.width = img.naturalWidth || img.width || 1;
249
+ cv.height = img.naturalHeight || img.height || 1;
250
+ ANNO.base = img;
251
+ hint.hidden = true;
252
+ bDone.disabled = false;
253
+ draw();
254
+ };
255
+ img.onerror = function () {
256
+ if (!ANNO || ANNO.cv !== cv) return;
257
+ hint.hidden = false;
258
+ hint.textContent = '截图读不出来(可能已过期),请重新截一张';
259
+ };
260
+ img.src = imgURL;
261
+
262
+ /* ── 绘制 ── */
263
+ function drawOp(ctx2, op) {
264
+ ctx2.save();
265
+ if (op.type === 'rect') {
266
+ ctx2.strokeStyle = op.color;
267
+ ctx2.lineWidth = op.lw;
268
+ ctx2.strokeRect(op.x, op.y, op.w, op.h);
269
+ } else if (op.type === 'text') {
270
+ ctx2.font = '600 ' + op.size + 'px system-ui, -apple-system, sans-serif';
271
+ ctx2.textBaseline = 'top';
272
+ ctx2.lineWidth = Math.max(2, op.size / 8);
273
+ ctx2.strokeStyle = 'rgba(0,0,0,.6)';
274
+ ctx2.strokeText(op.text, op.x, op.y);
275
+ ctx2.fillStyle = op.color;
276
+ ctx2.fillText(op.text, op.x, op.y);
277
+ }
278
+ ctx2.restore();
279
+ }
280
+ function draw() {
281
+ if (!ANNO) return;
282
+ var c = ANNO.ctx;
283
+ c.clearRect(0, 0, ANNO.cv.width, ANNO.cv.height);
284
+ if (ANNO.base) c.drawImage(ANNO.base, 0, 0);
285
+ ANNO.ops.forEach(function (op) { drawOp(c, op); });
286
+ if (ANNO.pending) drawOp(c, ANNO.pending);
287
+ }
288
+ function undo() {
289
+ if (!ANNO || !ANNO.ops.length) return;
290
+ ANNO.ops.pop();
291
+ draw();
292
+ }
293
+
294
+ /* 屏幕坐标 → 画布坐标(画布按原图分辨率,显示时缩放过)。 */
295
+ function toCanvas(clientX, clientY) {
296
+ var r = cv.getBoundingClientRect();
297
+ var k = r.width > 0 ? cv.width / r.width : 1;
298
+ return { x: (clientX - r.left) * k, y: (clientY - r.top) * k };
299
+ }
300
+ function strokeWidth() {
301
+ return Math.max(3, Math.round(cv.width / 300));
302
+ }
303
+ function fontSize() {
304
+ return Math.max(16, Math.round(cv.width / 28));
305
+ }
306
+
307
+ /* ── 手势 ── */
308
+ var dragging = false;
309
+ var startPt = null;
310
+
311
+ cv.addEventListener('pointerdown', function (e) {
312
+ if (!ANNO || !ANNO.base) return;
313
+ e.preventDefault();
314
+ if (ANNO.tool === 'text') {
315
+ openTextInput(toCanvas(e.clientX, e.clientY));
316
+ return;
317
+ }
318
+ dragging = true;
319
+ startPt = toCanvas(e.clientX, e.clientY);
320
+ ANNO.pending = { type: 'rect', x: startPt.x, y: startPt.y, w: 0, h: 0, color: color, lw: strokeWidth() };
321
+ try { cv.setPointerCapture(e.pointerId); } catch (err) {}
322
+ });
323
+ cv.addEventListener('pointermove', function (e) {
324
+ if (!dragging || !ANNO || !ANNO.pending) return;
325
+ e.preventDefault();
326
+ var p = toCanvas(e.clientX, e.clientY);
327
+ var x = Math.min(startPt.x, p.x), y = Math.min(startPt.y, p.y);
328
+ ANNO.pending.x = x;
329
+ ANNO.pending.y = y;
330
+ ANNO.pending.w = Math.abs(p.x - startPt.x);
331
+ ANNO.pending.h = Math.abs(p.y - startPt.y);
332
+ draw();
333
+ });
334
+ function endDrag(e) {
335
+ if (!dragging || !ANNO || !ANNO.pending) return;
336
+ dragging = false;
337
+ var op = ANNO.pending;
338
+ ANNO.pending = null;
339
+ /* 太小的框按误触处理:丢掉,不往文档里塞一个看不见的标记。 */
340
+ if (op.w > 4 && op.h > 4) ANNO.ops.push(op);
341
+ draw();
342
+ }
343
+ cv.addEventListener('pointerup', endDrag);
344
+ cv.addEventListener('pointercancel', endDrag);
345
+
346
+ /* 文字工具:**不用 window.prompt** —— Android WebView 与 Electron 小窗都没实现它,
347
+ 调用只会静默返回 null(表现是「点了没反应」)。这里自己放一个真输入框。 */
348
+ function openTextInput(pt) {
349
+ if (ANNO.textInput) return;
350
+ var box = hEl('div', 'reqshot-anno__textbox');
351
+ var inp = document.createElement('input');
352
+ inp.type = 'text';
353
+ inp.placeholder = '写点什么';
354
+ inp.setAttribute('autocapitalize', 'off');
355
+ inp.setAttribute('autocorrect', 'off');
356
+ inp.setAttribute('spellcheck', 'false');
357
+ var okBtn = hEl('button', 'reqshot-anno__tbtn reqshot-anno__tbtn--ok', '落字');
358
+ okBtn.type = 'button';
359
+ box.appendChild(inp);
360
+ box.appendChild(okBtn);
361
+ ANNO.ov.appendChild(box);
362
+ ANNO.textInput = box;
363
+ setTimeout(function () { try { inp.focus(); } catch (e) {} }, 20);
364
+
365
+ function commit() {
366
+ var t = String(inp.value || '').trim();
367
+ ANNO.textInput = null;
368
+ if (box.parentNode) box.parentNode.removeChild(box);
369
+ if (t) {
370
+ ANNO.ops.push({ type: 'text', x: pt.x, y: pt.y, text: t, color: color, size: fontSize() });
371
+ draw();
372
+ }
373
+ }
374
+ okBtn.onclick = commit;
375
+ inp.onkeydown = function (e) {
376
+ if (e.key === 'Enter') { e.preventDefault(); commit(); }
377
+ if (e.key === 'Escape') { e.preventDefault(); ANNO.textInput = null; if (box.parentNode) box.parentNode.removeChild(box); }
378
+ };
379
+ }
380
+
381
+ /* ── 完成:把画布导成 PNG 交给 core ── */
382
+ function done() {
383
+ if (!ANNO || !ANNO.base) return;
384
+ bDone.disabled = true;
385
+ try {
386
+ ANNO.cv.toBlob(function (b) {
387
+ if (!b) { bDone.disabled = false; toast('导出截图失败'); return; }
388
+ save(b);
389
+ }, 'image/png');
390
+ } catch (e) {
391
+ bDone.disabled = false;
392
+ toast('导出截图失败:' + ((e && e.message) || e));
393
+ }
394
+ }
395
+
396
+ function save(blob) {
397
+ if (!TASK) { toast('没有目标功能点'); return; }
398
+ var q = '?ws=' + encodeURIComponent(TASK.ws || '') +
399
+ '&module=' + encodeURIComponent(TASK.module || '') +
400
+ '&name=' + encodeURIComponent(TASK.name || '') +
401
+ '&caption=' + encodeURIComponent(TASK.caption || '');
402
+ var hdr = { 'Content-Type': 'image/png' };
403
+ if (H.getToken()) hdr['X-Magic-Token'] = H.getToken();
404
+ bDone.disabled = true;
405
+ fetch(H.getBase() + '/desk/requirements/shots' + q, { method: 'POST', headers: hdr, body: blob })
406
+ .then(function (r) {
407
+ return r.text().then(function (t) {
408
+ var d = {};
409
+ try { d = JSON.parse(t); } catch (e) {}
410
+ if (!r.ok) throw new Error(d.error || ('HTTP ' + r.status));
411
+ return d;
412
+ });
413
+ })
414
+ .then(function (d) {
415
+ var name = TASK ? TASK.name : '';
416
+ closeAnno();
417
+ H.setShotBtn(false);
418
+ /* 任务清掉:这次流程结束了。不清的话再点一次「截图」
419
+ 会把上一张再存一遍(同一个功能点里出现两张一样的图)。 */
420
+ TASK = null;
421
+ toast('已写入需求文档:' + (name || '') + '(第 ' + (d.shots || 1) + ' 张)');
422
+ if (H.refreshReqs) H.refreshReqs();
423
+ })
424
+ .catch(function (e) {
425
+ bDone.disabled = false;
426
+ toast('保存截图失败:' + ((e && e.message) || e));
427
+ });
428
+ }
429
+ }
430
+
431
+ function closeAnno() {
432
+ if (ANNO && ANNO.ov && ANNO.ov.parentNode) ANNO.ov.parentNode.removeChild(ANNO.ov);
433
+ ANNO = null;
434
+ }
435
+
436
+ /* ── 需求文档里的截图:markdown 源码预处理 ──
437
+ core 把截图写成 `![说明](<C:/…/1.png>)`(路径含空格/括号时用 <> 包)。
438
+ 但 **DOMPurify 的协议白名单会把这张图的 src 一起摘掉**(默认只放行
439
+ http/https/mailto/tel/cid/xmpp)—— 落进 DOM 的就是一枚没有地址的 <img>,
440
+ 界面上只剩一个破图占位(shared/conv/10-markdown-render.js 的注释里自证了这一点:
441
+ 它对链接用 data-local 绕开,图片同理)。
442
+ 所以这里在**送进 marked 之前**把本机绝对路径的图片改写成
443
+ `<img data-reqshot="绝对路径" alt="说明">`:data-* 属性消毒默认放行,
444
+ 渲染完再由宿主把 src 补成回读地址(/desk/media?path=…&w=480)。 */
445
+ var shotImgAngle = /!\[([^\]]*)\]\(\s*<([^>]+)>\s*\)/g;
446
+ /* 不带尖括号的本机路径:`X:/…`(Windows 盘符)或 `/…`(POSIX 根)。
447
+ ⚠️ `//host/path`(协议相对)**不能**算本机路径 —— 它是网页上的图。 */
448
+ var shotImgPlain = /!\[([^\]]*)\]\(\s*([A-Za-z]:\/[^\s)]+|\/(?!\/)[^\s)]+)\s*\)/g;
449
+
450
+ function isLocalPath(p) {
451
+ var s = String(p || '');
452
+ /* 本机绝对路径:Windows 盘符(D:/…)或 unix 根目录(/…)。
453
+ http(s) 那些是网页上的图,不归我们管(也不该改成 data-reqshot)。 */
454
+ if (/^[A-Za-z]:\//.test(s)) return true;
455
+ if (s.indexOf('//') === 0) return false; // 协议相对地址 = 网页上的图
456
+ return s.charAt(0) === '/';
457
+ }
458
+
459
+ function rewriteShotImages(md) {
460
+ var src = String(md == null ? '' : md);
461
+ if (!src) return '';
462
+ src = src.replace(shotImgAngle, function (all, alt, path) {
463
+ if (!isLocalPath(path)) return all;
464
+ return '<img data-reqshot="' + String(path).replace(/"/g, '%22') +
465
+ '" alt="' + String(alt || '').replace(/"/g, '%22') + '">';
466
+ });
467
+ src = src.replace(shotImgPlain, function (all, alt, path) {
468
+ if (!isLocalPath(path)) return all;
469
+ return '<img data-reqshot="' + String(path).replace(/"/g, '%22') +
470
+ '" alt="' + String(alt || '').replace(/"/g, '%22') + '">';
471
+ });
472
+ return src;
473
+ }
474
+
475
+ /* 浏览器被关掉时收尾:别留一个半途的截图任务。
476
+ 也把中转开关还原回用户自己的选择(我们是为了看页面才强开的)。 */
477
+ function reset() {
478
+ SHOTTING = false;
479
+ H.setShotBtn(false);
480
+ if (RELAY_WAS !== null) {
481
+ H.setRelayOn(RELAY_WAS);
482
+ RELAY_WAS = null;
483
+ }
484
+ TASK = null;
485
+ }
486
+
487
+ /* 挂载:index.html 在 DOM 就绪后调用一次。
488
+ host 里的 getter 必须保持为函数 —— 切环境后 BASE/TOKEN 会变。 */
489
+ function mount(host) {
490
+ H = host;
491
+ }
492
+
493
+ window.MagicReqShot = {
494
+ mount: mount,
495
+ open: openSheet,
496
+ capture: capture,
497
+ /* 需求页用:判断当前有没有待办任务(给徽标/按钮用)。 */
498
+ active: function () { return !!TASK; },
499
+ /* 浏览器被关掉时收尾:别留一个半开的截图通道。 */
500
+ reset: reset,
501
+ /* 需求文档的 markdown 预处理:把本机截图改写成 data-reqshot(见上面的注释)。 */
502
+ rewriteShotImages: rewriteShotImages,
503
+ isLocalPath: isLocalPath
504
+ };
505
+ })();
@@ -0,0 +1,21 @@
1
+ /* ══════════ 脚本页(「发现 → 脚本」子页面)══════════
2
+ 从 index.html 拆出:选择器只服务这一页(别的页面不引用)。
3
+ 一级清单(脚本 + 状态徽标)、二级详情(启停 + 日志区)。
4
+ CSS 变量(--ink / --muted / --faint / --border / --subtle / --primary-action /
5
+ --success / --warning / --fail / --stop / --tint-*)仍由 index.html 的 :root 统一提供。
6
+ ⚠️ 本页**不定义任何颜色令牌** —— 颜色全走 var(),
7
+ 否则 tests/check-ui-spec.mjs 的 ZCode 金标比对会红。 */
8
+
9
+ /* 状态徽标:四个状态四套色,语义与仓库既有状态色一致 ——
10
+ 运行中=绿(--running/--tint-run 一族)、已结束=绿(--tint-done)、
11
+ 已停止=石褐(--stop/--tint-stop,不是故障)、
12
+ 失败=红(--fail/--tint-fail,要人看一眼)。
13
+ ⚠️ 「已停止」不能用「已结束」的绿 —— 那是「正常跑完」,两件事不能同色。 */
14
+ .pbadge--run{color:var(--running);background:var(--tint-run)}
15
+ .pbadge--done{color:var(--success);background:var(--tint-done)}
16
+ .pbadge--stop{color:var(--stop);background:var(--tint-stop)}
17
+ .pbadge--fail{color:var(--fail);background:var(--tint-fail)}
18
+
19
+ /* 详情页的日志区:比插件页那条略高一点 —— 脚本日志是要盯着看的,
20
+ 170px 只够三四行,看不出「在动」。 */
21
+ .pd__log--script{max-height:260px}