magic-agent 2.3.2 → 2.3.9
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 +30 -4
- package/README.md +1 -1
- package/client-ui/shared/conv/07-stream-delegation.js +15 -0
- package/client-ui/shared/conversation.js +39 -0
- package/client-ui/shared/transport-http.js +10 -0
- package/client-ui-mobile/index.html +368 -44
- package/internal/agent/engine.go +2 -2
- package/internal/agent/llmengine.go +0 -8
- package/internal/agent/llmengine_test.go +0 -17
- package/internal/agent/llmmodels.go +0 -35
- package/internal/client/cors_test.go +32 -0
- package/internal/client/jobs.go +1160 -0
- package/internal/client/jobs_test.go +369 -0
- package/internal/client/service.go +235 -140
- package/internal/client/service_test.go +43 -0
- package/internal/session/session.go +15 -1
- package/internal/session/workspace_lock.go +165 -0
- package/internal/session/workspace_lock_test.go +90 -0
- package/internal/suggest/suggest.go +8 -25
- package/internal/suggest/suggest_test.go +0 -6
- package/npm/apk/magic-agent-mobile-release.apk +0 -0
- package/npm/apk/version.json +3 -3
- package/npm/dist/darwin-arm64/magic-agent +0 -0
- package/npm/dist/darwin-arm64/magic-client +0 -0
- package/npm/dist/darwin-x64/magic-agent +0 -0
- package/npm/dist/darwin-x64/magic-client +0 -0
- package/npm/dist/linux-arm64/magic-agent +0 -0
- package/npm/dist/linux-arm64/magic-client +0 -0
- package/npm/dist/linux-x64/magic-agent +0 -0
- package/npm/dist/linux-x64/magic-client +0 -0
- package/npm/dist/win32-x64/magic-agent.exe +0 -0
- package/npm/dist/win32-x64/magic-client.exe +0 -0
- package/package.json +1 -1
package/CLIENT-CONTRACT.md
CHANGED
|
@@ -351,7 +351,8 @@ notice {text:"团队模式:追加 N 个补充任务(a1、a2),继续
|
|
|
351
351
|
| 模块清单 | `GET /desk/modules`(从 profile 派生:id / 入口 / 声明的表) | 已实现 |
|
|
352
352
|
| 会话登记表 | `GET /desk/sessions`(core 的 `--sessions`,跨进程持久;**只回本客户端发起的会话**) | 已实现 |
|
|
353
353
|
| 项目业务接口 | `ANY /api/*` → 原样转发到 profile 的 `backend.url` | 已实现 |
|
|
354
|
-
| 发起一轮 | `POST /desk/ask`(SSE 中继 core 的 NDJSON,前置 `event: open`,收尾 `event: end
|
|
354
|
+
| 发起一轮 | `POST /desk/ask`(SSE 中继 core 的 NDJSON,前置 `event: open`,收尾 `event: end`)。**服务端会先入队**:同 workspace 串行 + `request_id` 幂等;真在等时先推 `event: queued`(`{job, state, position}`,客户端可忽略),准入后照旧 `open` | 已实现 |
|
|
355
|
+
| 作业队列(多客户端) | `POST /desk/jobs`:提交(不挂长连接,2xx 即可);`GET /desk/jobs[?workspace=][&limit=]`:队列快照(含他人任务与 `position`);`GET /desk/jobs/{id}`;`GET /desk/jobs/{id}/events?after=<seq>`:按 seq 回放本轮事件(形状同 `ask/{id}/events` + `job` 字段);`POST /desk/jobs/{id}/cancel`;`POST /desk/jobs/cancel {request_id}`(还没拿到 job id 时取消);`GET /desk/jobs/events?[workspace=]`:队列变更 SSE(每 2s 一帧 `event: jobs`)。持久化在 `<profile data dir>/jobs/`,服务重启后 queued 保留、running 残留标失败 | 已实现 |
|
|
355
356
|
| 控制与回审批 | `POST /desk/ask/{id}/control`(与 core 的 `--control` 同协议,插件不解释 op) | 已实现 |
|
|
356
357
|
| 断线续传 | `GET /desk/ask/{id}/events?after=<seq>`(按**本轮流 seq** 增量补拉;带 `done` / `code` / `stderr` / `snapshotRequired`;`after=0` 时首条是本轮**合成的用户提问**,见 2.4) | 已实现 |
|
|
357
358
|
| 运行中列表 | `GET /desk/asks`(**本进程**正在跑的轮次,每项 `{id, engine, started, session?, run?}`;与 sessions 的区别见下) | 已实现 |
|
|
@@ -447,6 +448,8 @@ notice {text:"团队模式:追加 N 个补充任务(a1、a2),继续
|
|
|
447
448
|
| `session` | 可选,续接一个已有会话(传 `/desk/sessions` 里的 id)→ core 的 `--session` |
|
|
448
449
|
| `idle` | 可选,常驻会话空闲收工时长;默认 `0` = 本轮结束就收工 |
|
|
449
450
|
| `team` | 可选,团队模式(见下)。**nil / 缺省 = 关闭**(默认关)|
|
|
451
|
+
| `request_id` | 可选(2026-10-23,只增字段),客户端生成的**幂等键**。同一个 id 重复提交(断线重连 / App 重启 / 两个标签页)只执行一次:服务端返回原作业并接回去。空 = 旧客户端,仍参与 workspace 串行,但没有幂等保护 |
|
|
452
|
+
| `serial` | 可选(2026-10-23,只增字段),默认 `true`:参与「同一 workspace 单槽」串行。客户端「并行模式」显式传 `false`(服务端仍套全局并发上限,`MAGIC_CLIENT_MAX_RUNNING`,默认 4)|
|
|
450
453
|
|
|
451
454
|
`team`(2026-10-21 只增字段,不升 `contractVersion`):
|
|
452
455
|
|
|
@@ -495,6 +498,10 @@ notice {text:"团队模式:追加 N 个补充任务(a1、a2),继续
|
|
|
495
498
|
用户要看的是「此刻连着的这份桌面端是哪一版」。老 core 没有该字段 → 空串,界面显示「—」,
|
|
496
499
|
客户端不写死版本号。
|
|
497
500
|
|
|
501
|
+
`/desk/health.jobs`(2026-10-23 只增字段,不升 `contractVersion`):插件是否提供
|
|
502
|
+
**服务端作业队列**(`true` / 缺失)。有它时客户端应把排队交给服务端(`/desk/jobs` 或带
|
|
503
|
+
`request_id` 的 `/desk/ask`),本地不再自行判忙;缺失时客户端退回本地串行门(老插件)。
|
|
504
|
+
|
|
498
505
|
⚠️ **`/desk/health.version` 是插件启动时缓存的 core 版本**:只升级 CLI、没重启插件时它会
|
|
499
506
|
一直报旧值(2026-10-22 实测:CLI 已 2.1.3,手机仍显示 1.0.111)。要显示「此刻真正的
|
|
500
507
|
core 版本」必须读 `/desk/contract.version`(每次都重新问 core);`health.version` 只适合
|
|
@@ -743,6 +750,25 @@ beforeAsk(info) // info: {isNew, session, workspace, engine, model, permission
|
|
|
743
750
|
|
|
744
751
|
掌天瓶那一侧的 transport 走 IPC → CLI;`transport-http.js` 是走 `/desk/*` 的现成实现。
|
|
745
752
|
|
|
753
|
+
### 3.2.1.1 transport 的可选能力:按会话停止(跨进程)
|
|
754
|
+
|
|
755
|
+
```js
|
|
756
|
+
stopSession(id) // id: session_id 或 run_id → Promise<{stopped, reason, record}>
|
|
757
|
+
// 对应 POST /desk/session/{id}/stop
|
|
758
|
+
```
|
|
759
|
+
|
|
760
|
+
`control({op:'interrupt'})` 需要 **ask id**,而 ask id 只存在于**拉起 core 的那个进程**里。
|
|
761
|
+
当本端只是尾随别人进程里跑的那一轮(桌面壳直接 exec core、另一台设备起的、宿主队列派发的),
|
|
762
|
+
`state.askId` 永远是空的 —— 没有这条可选能力,用户点「停止」只会被回一句
|
|
763
|
+
「这一轮还没拿到轮次 id」,按几次报几次,这一轮根本停不下来。
|
|
764
|
+
|
|
765
|
+
组件的兜底顺序(`control('interrupt')` 且没有 askId):① `onInterrupt(prompt)` 让宿主撤掉排队;
|
|
766
|
+
② `listAsks()` 里按 session_id / run_id 认出**这一轮还在跑**的那条 ask,用它打断
|
|
767
|
+
(跨端也是这条:悬浮球远程控制对面机器时,对面 `/desk/asks` 里就有它,对面不必升级);
|
|
768
|
+
③ `stopSession(state.session)`(**只在 `state.busy` 且有会话 id 时**);④ 才如实报「还没拿到轮次 id」。
|
|
769
|
+
服务端实现走 core 的 `--stop`(登记表 pid + SIGTERM 进程组),语义幂等:
|
|
770
|
+
`stopped:false` 只表示「本来就结束了 / 进程已不在」,不是失败。
|
|
771
|
+
|
|
746
772
|
### 3.2.2 transport 的可选能力:插件(加号 → 插件)
|
|
747
773
|
|
|
748
774
|
会话输入框的 + 弹层里,`插件` 这一行**只在 transport 提供下面两个方法时出现**
|
|
@@ -989,10 +1015,10 @@ core 侧只有一张空的 sid→title 覆盖表,于是:
|
|
|
989
1015
|
| 生成在 core | `magic-agent --suggest-followups <file\|->`(`internal/suggest`):提示词、截断、JSON 解析、去重与长度上限都只有一份;移动端 / PC 拿到的建议不会各说各话 |
|
|
990
1016
|
| **默认关闭** | 建议引擎**不配置 = 不开启建议功能**(2026-10-23 用户定调)。组件 `suggestEngine` 为空时不发 `/desk/suggest`、不画建议、不补历史建议、托管也不自动执行;老用户升级后默认没有建议,需去「我的」里显式选引擎 |
|
|
991
1017
|
| 配置入口 | 移动端「我的 → 建议引擎 / 建议模型」两行,点开的是与「默认引擎 / 模型」相同的**二列抽屉**:左列首项「不开启」(默认),其余是 `/desk/engines` 探测到的可用引擎;点左列只切右列,点右列才落定。引擎存 `magic.suggest.engine`、模型存 `magic.suggest.model`,都属于环境配置(`ENV_SCOPE_KEYS` + `/desk/prefs` 同步);改完立刻 `conv.setSuggestEngine(engine, model)` 生效,下一轮收尾开始生成 |
|
|
992
|
-
| 选引擎 / 模型 | `llm`
|
|
993
|
-
| **思考固定关闭** | 建议调用一律带 `NoThinking`,各引擎走官方开关:`claude` → `--thinking disabled`;`codex` → `model_reasoning_effort="none"`;`llm` →
|
|
1018
|
+
| 选引擎 / 模型 | `llm` 显式选中的模型只传 `reasoning_effort=none`,不换模型;`MiniMax-M3.1-Flash` 拒绝该值(要求 adaptive thinking,HTTP 400)。**建议模型留空**时用 `models.json` 第一条 `thinking.disabled` 条目作默认值(本机 `minimax-nothink`),避免默认首条 400。`claude` / `codex` 可用各自的关闭档 |
|
|
1019
|
+
| **思考固定关闭** | 建议调用一律带 `NoThinking`,各引擎走官方开关:`claude` → `--thinking disabled`;`codex` → `model_reasoning_effort="none"`;`llm` → `reasoning_effort="none"`(直连请求体;CLI 委托路径用 `-o reasoning_effort none`);显式模型不换模型,空建议模型用 `thinking.disabled` 变体作默认值;`codebuddy` → settings `alwaysThinkingEnabled=false`;`dsh` → SDK `InitializeParams.reasoningEffort="off"`。`trae`/`openclaw`/`arkclaw` 没有已知开关,仍保留 `NoThinking` 标记,失败后再降级 `llm` |
|
|
994
1020
|
| **不续接会话** | 独立进程、新会话、`ToolsOff`、不带 `SessionID` / `Continue` —— 「请生成建议」这类指令不会写进用户正在聊的上下文,也不进会话登记表 / 日志 |
|
|
995
|
-
| 引擎走「无工具 + JSON」通道 | core 的 `suggest.Generate` 固定 **`ToolsOff` + `JSONSchema` + `NoThinking`**,引擎以 `--print --output-format json` 返回一小段结果,不走流式、不带工具。默认选 `llm
|
|
1021
|
+
| 引擎走「无工具 + JSON」通道 | core 的 `suggest.Generate` 固定 **`ToolsOff` + `JSONSchema` + `NoThinking`**,引擎以 `--print --output-format json` 返回一小段结果,不走流式、不带工具。默认选 `llm`;建议模型留空时用 `models.json` 第一条 `thinking.disabled` 变体(本机 `minimax-nothink`),显式模型则只传 `reasoning_effort=none` |
|
|
996
1022
|
| 慢引擎不拖界面 | `claude` / `codex` / `codebuddy` / `dsh` 等非 `llm` 引擎给 10s 主引擎预算;失败/超时才降级 `llm`,并清掉当前引擎的 model。界面默认不传 engine,直接走快路径,20s 超时不会触发 |
|
|
997
1023
|
| **允许空数组** | 模型判断「没有下一步」时回 `[]`,这是**合法结果**:客户端不画任何按钮,不拿模板凑 |
|
|
998
1024
|
| 成功收尾后问 | 失败 / 被用户停止 / 还在跑的轮次都不问;本轮出现过提问卡(引擎在等作答)时也跳过,避免与作答卡抢注意力 |
|
package/README.md
CHANGED
|
@@ -201,7 +201,7 @@ magic-agent --append <run_id> -p "追加:顺便把 lint 也跑了"
|
|
|
201
201
|
| `-o, --output` | `json` | 输出格式:`json` \| `text` |
|
|
202
202
|
| `--stream` | 关 | 流式输出(text 模式思考走 stderr) |
|
|
203
203
|
| `--no-thinking` | 关 | 流式模式下不转发思考过程增量(只隐藏,不关闭模型思考) |
|
|
204
|
-
| `--disable-thinking` / `--no-think` | 关 | 请求引擎真正关闭模型思考:claude `--thinking disabled`、codex `effort=none`、codebuddy `alwaysThinkingEnabled=false`、dsh `reasoningEffort=off`、llm
|
|
204
|
+
| `--disable-thinking` / `--no-think` | 关 | 请求引擎真正关闭模型思考:claude `--thinking disabled`、codex `effort=none`、codebuddy `alwaysThinkingEnabled=false`、dsh `reasoningEffort=off`、llm 传 `reasoning_effort=none`(不自动换模型) |
|
|
205
205
|
| `--engines` | 关 | 列出引擎、可用性、模型、版本与一键安装命令 |
|
|
206
206
|
| `--no-models` | 关 | 配合 `--engines`:跳过模型与版本探测 |
|
|
207
207
|
| `--json` | 关 | 兼容保留(行为同 `--engines` 的默认 JSON 输出) |
|
|
@@ -125,6 +125,21 @@ function bindQaStreamDelegation(){
|
|
|
125
125
|
ts.classList.toggle("open");
|
|
126
126
|
return;
|
|
127
127
|
}
|
|
128
|
+
/* 长消息折叠的「展开全文 / 收起」(2026-10-23 用户报:「点击展开全文没效果」)。
|
|
129
|
+
按钮由 29-msg-variants 的 qaFoldMaybe 生成、**不带监听**,夹在被折叠正文 host
|
|
130
|
+
的紧后面(`previousElementSibling` 就是它)。此前委托迁进 07 时漏了这条分支,
|
|
131
|
+
按钮就成了死的(PC / 移动端同一份内核,两边一起哑)。
|
|
132
|
+
开合态只切 host 上的 `.on`(CSS 见 conv.css),文字在「展开全文(共 N 行)」与
|
|
133
|
+
「收起」之间来回;行数仍从 data-qa-lines 取回。 */
|
|
134
|
+
const foldBtn = e.target.closest && e.target.closest(".qa-fold-btn");
|
|
135
|
+
if(foldBtn){
|
|
136
|
+
const host = foldBtn.previousElementSibling;
|
|
137
|
+
if(!host) return;
|
|
138
|
+
const on = host.classList.toggle("on");
|
|
139
|
+
foldBtn.textContent = on ? "收起"
|
|
140
|
+
: "展开全文(共 " + (foldBtn.getAttribute("data-qa-lines") || "") + " 行)";
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
128
143
|
/* ⑥ 任务列表开合:面板是应答里的一块**常驻**节点(一条应答只有一块,每次 TodoWrite
|
|
129
144
|
原地更新它的内容,见 03-chat-message 的 todoList)—— 开合态只切类,点击必须走委托,
|
|
130
145
|
理由与工具卡 / 思考条同:快照恢复(`s.innerHTML = t.snapshot`)会把节点整个重建,
|
|
@@ -1440,6 +1440,45 @@
|
|
|
1440
1440
|
已经会说「已取消排队(串行模式)」—— 这里再说一遍就是两条一样的。 */
|
|
1441
1441
|
if (owned) return true;
|
|
1442
1442
|
}
|
|
1443
|
+
/* 跨进程 / 跨端:这一轮在**别的进程**里跑,本端只是尾随 —— 它的 ask id 不会回到这里,
|
|
1444
|
+
但那个进程的 /desk/asks 里还留着它。按会话 id 认领回来直接打断即可,
|
|
1445
|
+
不必等一个永远不来的 open 事件(2026-10-23 悬浮球「停不掉」就是这一条:
|
|
1446
|
+
面板远程看着家里那台机器上跑的一轮,点几次都只回「还没拿到轮次 id」)。 */
|
|
1447
|
+
if (op === 'interrupt' && state.busy && state.session && typeof transport.listAsks === 'function') {
|
|
1448
|
+
var live = null;
|
|
1449
|
+
try { live = await transport.listAsks(); } catch (e) { live = null; }
|
|
1450
|
+
if (Array.isArray(live)) {
|
|
1451
|
+
for (var i = live.length - 1; i >= 0; i--) {
|
|
1452
|
+
var a = live[i];
|
|
1453
|
+
if (!a || !a.id) continue;
|
|
1454
|
+
/* 与 runningAskOf 同口径:session_id 与 run_id 都算(新会话首轮只有 run_id)。 */
|
|
1455
|
+
if (a.session !== state.session && a.run !== state.session) continue;
|
|
1456
|
+
try {
|
|
1457
|
+
await transport.control({ op: 'interrupt', id: a.id });
|
|
1458
|
+
noticeItem('已请求打断');
|
|
1459
|
+
return true;
|
|
1460
|
+
} catch (e2) {
|
|
1461
|
+
noticeItem('控制失败:' + (e2.message || e2));
|
|
1462
|
+
return false;
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
/* 再兜一层:登记表里的进程 id 头(服务端走 core 的 `--stop`,见 /desk/session/{id}/stop)。
|
|
1468
|
+
既没有 ask 记录、又不在本进程时,这是唯一还停得下来的路。 */
|
|
1469
|
+
if (op === 'interrupt' && state.busy && state.session && typeof transport.stopSession === 'function') {
|
|
1470
|
+
try {
|
|
1471
|
+
var res = await transport.stopSession(state.session);
|
|
1472
|
+
/* 幂等:进程本来就没了 / 这一轮已经收尾 → 如实说已经结束,不当成失败。 */
|
|
1473
|
+
if (res && res.stopped) { noticeItem('已请求打断'); return true; }
|
|
1474
|
+
noticeItem('这一轮已经结束' + (res && res.reason ? '(' + res.reason + ')' : ''));
|
|
1475
|
+
return false;
|
|
1476
|
+
} catch (e) {
|
|
1477
|
+
/* 老服务端(404)或会话还没落定:把真实原因说出来,别假装停住了。 */
|
|
1478
|
+
noticeItem('控制失败:' + (e.message || e));
|
|
1479
|
+
return false;
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1443
1482
|
noticeItem('这一轮还没拿到轮次 id,稍等 open 事件回来再试');
|
|
1444
1483
|
return false;
|
|
1445
1484
|
}
|
|
@@ -313,6 +313,16 @@
|
|
|
313
313
|
.then(function () { return true; });
|
|
314
314
|
},
|
|
315
315
|
|
|
316
|
+
/* 按**会话 id** 停掉正在跑的那一轮(跨进程)。
|
|
317
|
+
与 control 的区别:control 要 ask id,而 ask id 只存在于拉起 core 的那个进程里;
|
|
318
|
+
手机 / 另一台设备 / 桌面壳 exec 的轮次只能读盘尾随,手里没有 id。
|
|
319
|
+
服务端走 core 的 `--stop`(登记表 pid + SIGTERM),返回 {stopped, reason, record}。
|
|
320
|
+
老服务端没有这条路由 → 404,调用方要如实说,不要假装停住了。 */
|
|
321
|
+
stopSession: function (id) {
|
|
322
|
+
return api('/desk/session/' + encodeURIComponent(id) + '/stop', { method: 'POST' })
|
|
323
|
+
.then(function (r) { return r.json(); });
|
|
324
|
+
},
|
|
325
|
+
|
|
316
326
|
/* 本进程**正在跑**的轮次(每项 {id, engine, started, session?})。
|
|
317
327
|
打开一条「还在跑」的会话时用它认出同一会话的运行中轮次,把画面接回实时流:
|
|
318
328
|
历史只重放到最后一条 user,其余从 ask 缓冲按 seq 补齐(见 conversation.js 的
|