magic-agent 1.0.49 → 1.0.108

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 (94) hide show
  1. package/CLIENT-CONTRACT.md +514 -7
  2. package/README.md +31 -2
  3. package/client-ui/pc/app.css +3 -41
  4. package/client-ui/pc/js/04-task-list.js +6 -2
  5. package/client-ui/pc/js/90-glue.js +23 -4
  6. package/client-ui/pc/js/92-composer.js +11 -4
  7. package/client-ui/pc/js/93-rail.js +178 -19
  8. package/client-ui/shared/conv/03-chat-message.js +145 -25
  9. package/client-ui/shared/conv/09-stream-apply.js +55 -6
  10. package/client-ui/shared/conv/19-attachments.js +70 -6
  11. package/client-ui/shared/conv/27-conv-ledger.js +18 -0
  12. package/client-ui/shared/conv/29-msg-variants.js +10 -0
  13. package/client-ui/shared/conv/README.md +6 -0
  14. package/client-ui/shared/conv/conv.css +77 -0
  15. package/client-ui/shared/conversation.css +229 -43
  16. package/client-ui/shared/conversation.js +1316 -131
  17. package/client-ui/shared/transport-http.js +164 -12
  18. package/client-ui-mobile/index.html +2168 -143
  19. package/internal/agent/capability.go +16 -2
  20. package/internal/agent/claude.go +3 -2
  21. package/internal/agent/codebuddy.go +7 -1
  22. package/internal/agent/codex_test.go +3 -0
  23. package/internal/agent/custom_models_test.go +5 -0
  24. package/internal/agent/dsh.go +18 -2
  25. package/internal/agent/dsh_test.go +53 -2
  26. package/internal/agent/engine_test.go +34 -19
  27. package/internal/agent/env_test.go +14 -9
  28. package/internal/agent/models_test.go +9 -0
  29. package/internal/agent/runcmd_test.go +24 -3
  30. package/internal/agent/stream.go +33 -0
  31. package/internal/cli/ask.go +94 -4
  32. package/internal/cli/contract_test.go +26 -0
  33. package/internal/cli/events.go +18 -7
  34. package/internal/cli/keepalive_test.go +69 -0
  35. package/internal/cli/requirements.go +119 -0
  36. package/internal/cli/requirements_test.go +36 -0
  37. package/internal/cli/root.go +56 -4
  38. package/internal/cli/session.go +64 -25
  39. package/internal/cli/sessionlog.go +76 -7
  40. package/internal/cli/sessionlog_test.go +58 -0
  41. package/internal/cli/team.go +374 -0
  42. package/internal/cli/team_session.go +156 -0
  43. package/internal/cli/team_test.go +360 -0
  44. package/internal/client/plugins.go +7 -4
  45. package/internal/client/requirements.go +309 -0
  46. package/internal/client/requirements_test.go +239 -0
  47. package/internal/client/service.go +545 -19
  48. package/internal/client/service_test.go +47 -2
  49. package/internal/client/session_events_test.go +320 -0
  50. package/internal/client/team_flags.go +204 -0
  51. package/internal/client/team_flags_test.go +254 -0
  52. package/internal/requirements/bootstrap.go +423 -0
  53. package/internal/requirements/consolidate.go +908 -0
  54. package/internal/requirements/doc.go +473 -0
  55. package/internal/requirements/memory.go +324 -0
  56. package/internal/requirements/requirements.go +169 -0
  57. package/internal/requirements/requirements_test.go +530 -0
  58. package/internal/requirements/store.go +289 -0
  59. package/internal/session/delete.go +8 -4
  60. package/internal/session/session.go +194 -17
  61. package/internal/session/session_append.go +4 -1
  62. package/internal/session/session_append_test.go +19 -0
  63. package/internal/session/session_mark_test.go +42 -3
  64. package/internal/session/session_test.go +43 -0
  65. package/internal/session/sessionlog.go +50 -34
  66. package/internal/session/sessionlog_read_test.go +69 -11
  67. package/internal/session/status.go +205 -0
  68. package/internal/session/status_test.go +144 -0
  69. package/internal/team/adaptive.go +293 -0
  70. package/internal/team/board.go +216 -0
  71. package/internal/team/event.go +28 -0
  72. package/internal/team/execute.go +648 -0
  73. package/internal/team/plan.go +903 -0
  74. package/internal/team/planner.go +294 -0
  75. package/internal/team/reduce.go +192 -0
  76. package/internal/team/role.go +334 -0
  77. package/internal/team/route.go +206 -0
  78. package/internal/team/schedule.go +241 -0
  79. package/internal/team/schedule_test.go +286 -0
  80. package/internal/team/subsession.go +27 -0
  81. package/internal/team/team.go +401 -0
  82. package/internal/team/team_test.go +1997 -0
  83. package/npm/bin/magic-client.js +0 -0
  84. package/npm/dist/darwin-arm64/magic-agent +0 -0
  85. package/npm/dist/darwin-arm64/magic-client +0 -0
  86. package/npm/dist/darwin-x64/magic-agent +0 -0
  87. package/npm/dist/darwin-x64/magic-client +0 -0
  88. package/npm/dist/linux-arm64/magic-agent +0 -0
  89. package/npm/dist/linux-arm64/magic-client +0 -0
  90. package/npm/dist/linux-x64/magic-agent +0 -0
  91. package/npm/dist/linux-x64/magic-client +0 -0
  92. package/npm/dist/win32-x64/magic-agent.exe +0 -0
  93. package/npm/dist/win32-x64/magic-client.exe +0 -0
  94. package/package.json +1 -1
@@ -14,6 +14,7 @@
14
14
  * GET /desk/ask/{id}/events?after=<seq> 断线续传:按 seq 增量补拉本轮事件
15
15
  * GET /desk/session/{id}/messages 全量历史
16
16
  * GET /desk/session/{id}/events?after=<seq> 增量续读
17
+ * GET /desk/session/{id}/follow 持续尾随(SSE,跨进程看别人在跑的轮次)
17
18
  * POST /desk/upload 原子化上传附件(返回本机路径 + 回读 url)
18
19
  * GET /desk/uploads/{name}?token=… 回读附件(缩略图;<img> 只能走 ?token=)
19
20
  * GET /desk/media?path=<abs>&w=<px> 按绝对路径回读媒体(w = 移动端缩略图宽度)
@@ -27,6 +28,13 @@
27
28
  (function () {
28
29
  'use strict';
29
30
 
31
+ /* 引擎清单缓存(2026-10-21):`/desk/engines?models=1` 在 core 侧要逐个引擎探测
32
+ (实测 ~1s;`--no-models` 才 <10ms),而结果只随「本机装了 / 卸了哪个引擎」变化。
33
+ 以前每挂载一次对话组件就重拉一次 —— 移动端「返回列表 → 再点进来」每次白等 1s。
34
+ 缓存 5 分钟,按端点(base + token)区分;`listEngines(true)` 强制重探。 */
35
+ var ENGINES_TTL = 5 * 60 * 1000;
36
+ var enginesCache = { key: '', at: 0, list: null, p: null };
37
+
30
38
  function MagicTransportHttp(opts) {
31
39
  opts = opts || {};
32
40
  var BASE = String(opts.base || '').replace(/\/+$/, '');
@@ -41,12 +49,15 @@
41
49
  return h;
42
50
  }
43
51
 
44
- /* 移动端慢网取图统一走 720 宽缩略图(服务端按需缩放;解不了 / 非图片自动回原文件)。
45
- 返回 '' 或 '<sep>w=720':由调用方拼在已有 query 之后。 */
52
+ /* 移动端慢网取图统一走 480 宽缩略图(服务端按需缩放;解不了 / 非图片自动回原文件)。
53
+ 2026-10-21 用户:「再压缩一下 可打开看大图」—— 缩略图只负责「看清是什么」,
54
+ 宽取 480 已够产物卡(200px 宽)在 2~2.4 倍屏上不糊,流量再砍一半;
55
+ 要看细节走大图浮层(移动端 imgview 打开时把 w 去掉,服务端就发原文件)。
56
+ 返回 '' 或 '<sep>w=480':由调用方拼在已有 query 之后。 */
46
57
  function thumbQuery(path, sep) {
47
58
  if (CLIENT !== 'mobile') return '';
48
59
  if (!/\.(?:png|jpe?g|gif|bmp|webp)$/i.test(String(path))) return '';
49
- return sep + 'w=720';
60
+ return sep + 'w=480';
50
61
  }
51
62
  function api(path, o) {
52
63
  o = o || {};
@@ -58,18 +69,46 @@
58
69
  }
59
70
  return fetch(BASE + path, o).then(function (r) {
60
71
  if (!r.ok) {
61
- var err = new Error(r.status + ' ' + r.statusText);
62
- err.status = r.status; // 调用方要区分 404(无法续传)和网络抖动
63
- throw err;
72
+ /* 非 2xx 的正文是服务端的真实原因(writeErr → {"error":"中文原因"}):
73
+ 400 团队路由非法、501 core 不支持团队模式、503 引擎不可用都在这里。
74
+ 只抛状态码会把原因彻底丢掉(「未能启动 为什么」问不出来就是这个坑),
75
+ 所以读一次正文,取 error 字段挂到 err.detail 并拼进 message。 */
76
+ return r.text().then(function (txt) {
77
+ var detail = '';
78
+ try { var j = JSON.parse(txt); detail = (j && (j.error || j.message)) || ''; } catch (e) {}
79
+ if (!detail) detail = String(txt || '').trim().slice(0, 300);
80
+ var err = new Error(r.status + ' ' + r.statusText + (detail ? ':' + detail : ''));
81
+ err.status = r.status; // 调用方要区分 404(无法续传)和网络抖动
82
+ err.detail = detail;
83
+ throw err;
84
+ });
64
85
  }
65
86
  return r;
66
87
  });
67
88
  }
68
89
 
69
90
  return {
70
- /* 引擎清单:带模型(选择器要用),所以固定 ?models=1。 */
71
- listEngines: function () {
72
- return api('/desk/engines?models=1').then(function (r) { return r.json(); });
91
+ /* 引擎清单:带模型(选择器要用),所以固定 ?models=1。
92
+ `force` = 跳过缓存重新探测(见模块顶部的 enginesCache)。 */
93
+ listEngines: function (force) {
94
+ var key = BASE + '|' + TOKEN;
95
+ if (!force && enginesCache.key === key) {
96
+ if (enginesCache.list && Date.now() - enginesCache.at < ENGINES_TTL) {
97
+ return Promise.resolve(enginesCache.list);
98
+ }
99
+ /* 同一端点正在探测(宿主连开两次组件)→ 共用同一个请求,别叠加 1s。 */
100
+ if (enginesCache.p) return enginesCache.p;
101
+ }
102
+ var p = api('/desk/engines?models=1').then(function (r) { return r.json(); })
103
+ .then(function (list) {
104
+ enginesCache = { key: key, at: Date.now(), list: list, p: null };
105
+ return list;
106
+ }, function (e) {
107
+ if (enginesCache.p === p) enginesCache.p = null; // 失败不缓存,下次重探
108
+ throw e;
109
+ });
110
+ enginesCache = { key: key, at: 0, list: null, p: p };
111
+ return p;
73
112
  },
74
113
 
75
114
  /* 上传附件:返回 {path, name, size, mime, url}。path 是 core 所在机器的绝对路径,
@@ -134,6 +173,61 @@
134
173
  return api('/desk/path-read', { json: body }).then(function (r) { return r.json(); });
135
174
  },
136
175
 
176
+ /* 插件目录(magic-shorts / magic-video 两个工程 CLI 的介绍、安装状态、安装/升级命令)。
177
+ 返回 {plugins:[{id,name,cli,tagline,description,requirements,homepage,
178
+ installed,version,bin,sourcePath,sourceExists,
179
+ install,upgrade,examples,usePrompt,note}]}。
180
+ ⚠️ 命令串只用于**展示与复制**;真正执行要调 pluginAction(服务端按固定表执行)。 */
181
+ listPlugins: function () {
182
+ return api('/desk/plugins').then(function (r) { return r.json(); });
183
+ },
184
+
185
+ /* 在**桌面端**执行某个插件的安装/升级:SSE 逐行回,形状与 ask 同构。
186
+ 入参:{id, action:'install'|'upgrade', onOpen(cmds), onLine(line), onStep(index,cmd), onEnd(result)}
187
+ onEnd 的那个 result 就是 SSE 里 end 事件的整份负载(ok/code/error/plugin)。
188
+ 老服务端没有这个接口 → 404,调用方要如实说「需要较新版本」,不要假装成功。 */
189
+ pluginAction: function (req, h) {
190
+ var r = req || {};
191
+ var hs = h || {};
192
+ var ended = false;
193
+ var done = function (out) {
194
+ if (ended) return;
195
+ ended = true;
196
+ if (hs.onEnd) { try { hs.onEnd(out); } catch (e) {} }
197
+ };
198
+ return api('/desk/plugins/' + encodeURIComponent(r.id) + '/' + encodeURIComponent(r.action || 'install'),
199
+ { method: 'POST' })
200
+ .then(function (resp) {
201
+ if (!resp.body) throw new Error('服务端没有返回流');
202
+ var reader = resp.body.getReader(), dec = new TextDecoder(), buf = '';
203
+ return (function pump() {
204
+ return reader.read().then(function (chunk) {
205
+ if (chunk.done) { done({ ok: false, code: null, error: '连接提前结束' }); return; }
206
+ buf += dec.decode(chunk.value, { stream: true });
207
+ var blocks = buf.split('\n\n');
208
+ buf = blocks.pop();
209
+ blocks.forEach(function (b) {
210
+ var name = '', data = '';
211
+ b.split('\n').forEach(function (ln) {
212
+ if (ln.indexOf('event: ') === 0) name = ln.slice(7).trim();
213
+ else if (ln.indexOf('data: ') === 0) data += ln.slice(6);
214
+ });
215
+ if (!data) return;
216
+ var ev; try { ev = JSON.parse(data); } catch (e) { return; }
217
+ try {
218
+ if (name === 'open' && hs.onOpen) hs.onOpen(ev);
219
+ else if (name === 'step' && hs.onStep) hs.onStep(ev);
220
+ else if (name === 'line' && hs.onLine) hs.onLine(ev);
221
+ else if (name === 'end') done(ev);
222
+ } catch (e) {}
223
+ });
224
+ return pump();
225
+ });
226
+ })();
227
+ })
228
+ .catch(function (e) { done({ ok: false, code: null, error: (e && e.message) || String(e) }); });
229
+ },
230
+
137
231
  /* 会话标题覆盖表 {sid: title}(插件侧持久化,重命名用)。 */
138
232
  listTitles: function () {
139
233
  return api('/desk/titles').then(function (r) { return r.json(); });
@@ -150,10 +244,13 @@
150
244
  var aborted = false;
151
245
  var ctl = typeof AbortController !== 'undefined' ? new AbortController() : null;
152
246
  var ended = false;
153
- var finish = function (code) {
247
+ /* ev = 收尾那一帧的原文。end 帧带 code + stderr(core 的真实失败原因,
248
+ 见 internal/client/service.go 的 send("end", …)),调用方要拿它解释失败,
249
+ 所以原样透传;不给就只有数字 code,用户看不懂也查不到。 */
250
+ var finish = function (code, ev) {
154
251
  if (ended) return;
155
252
  ended = true;
156
- if (h && h.onEnd) h.onEnd(code);
253
+ if (h && h.onEnd) h.onEnd(code, ev);
157
254
  };
158
255
  api('/desk/ask', { json: body, signal: ctl ? ctl.signal : undefined })
159
256
  .then(function (resp) {
@@ -176,7 +273,7 @@
176
273
  if (h && h.onOpen) h.onOpen(ev && (ev.id || ev.ask || ev.run));
177
274
  return;
178
275
  }
179
- if (name === 'end') { finish(ev && ev.code); return; }
276
+ if (name === 'end') { finish(ev && ev.code, ev); return; }
180
277
  if (h && h.onEvent) h.onEvent(ev);
181
278
  });
182
279
  if (ended) return;
@@ -239,6 +336,61 @@
239
336
  }
240
337
  return api('/desk/session/' + encodeURIComponent(id) + '/messages?lean=1')
241
338
  .then(function (r) { return r.json(); });
339
+ },
340
+
341
+ /* 持续尾随一条会话的进度(SSE,跨进程)。
342
+ `/desk/asks` 只认**本插件进程里**跑起来的轮次;掌天瓶桌面壳直接 exec core,
343
+ 它起的一轮在这里从来不存在。这个接口读的是磁盘上那条会话日志(谁跑的都一样),
344
+ 服务端按 ~500ms 增量读盘推送,客户端一次连接跟到终态。
345
+ 帧形状与 /desk/ask 同一套(各端已有解析器):
346
+ event: open data: {session, after}
347
+ data: <事件> 增量事件,一条一帧(与 readSession 的 events 同形)
348
+ event: snapshot data: {session, after, lastSeq} 游标超前 → 重拉历史
349
+ event: end data: {session, after, state, reason} reason ∈ terminal / max_duration
350
+ h.onOpen(open) / h.onEvent(ev) / h.onSnapshot(snap) / h.onEnd(end) / h.onError(err)。
351
+ 返回 {cancel}:换会话 / 组件销毁时调它断流。老服务端没有这个路由 → onError(404)。 */
352
+ followSession: function (id, after, h) {
353
+ var ctl = typeof AbortController !== 'undefined' ? new AbortController() : null;
354
+ var ended = false;
355
+ var finish = function (info) {
356
+ if (ended) return;
357
+ ended = true;
358
+ if (h && h.onEnd) h.onEnd(info || {});
359
+ };
360
+ var q = '?lean=1' + (after == null ? '' : '&after=' + encodeURIComponent(after));
361
+ api('/desk/session/' + encodeURIComponent(id) + '/follow' + q, { signal: ctl ? ctl.signal : undefined })
362
+ .then(function (resp) {
363
+ var reader = resp.body.getReader(), dec = new TextDecoder(), buf = '';
364
+ return (function pump() {
365
+ return reader.read().then(function (chunk) {
366
+ if (chunk.done) return finish({ reason: 'closed' });
367
+ buf += dec.decode(chunk.value, { stream: true });
368
+ var blocks = buf.split('\n\n');
369
+ buf = blocks.pop();
370
+ blocks.forEach(function (b) {
371
+ var name = '', data = '';
372
+ b.split('\n').forEach(function (ln) {
373
+ if (ln.indexOf('event: ') === 0) name = ln.slice(7).trim();
374
+ else if (ln.indexOf('data: ') === 0) data += ln.slice(6);
375
+ });
376
+ if (!data) return; // `: ping` 心跳注释帧
377
+ var ev; try { ev = JSON.parse(data); } catch (e) { return; }
378
+ if (name === 'open') { if (h && h.onOpen) h.onOpen(ev); return; }
379
+ if (name === 'snapshot') { if (h && h.onSnapshot) h.onSnapshot(ev); return; }
380
+ if (name === 'end') { finish(ev); return; }
381
+ if (h && h.onEvent) h.onEvent(ev);
382
+ });
383
+ if (ended) return;
384
+ return pump();
385
+ });
386
+ })();
387
+ })
388
+ .catch(function (e) {
389
+ if (ended) return;
390
+ if (h && h.onError) h.onError(e);
391
+ finish({ reason: 'error' });
392
+ });
393
+ return { cancel: function () { if (ctl) ctl.abort(); } };
242
394
  }
243
395
  };
244
396
  }