magic-agent 2.3.0 → 2.3.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 (52) hide show
  1. package/CLIENT-CONTRACT.md +51 -12
  2. package/README.md +10 -4
  3. package/client-ui/pc/app.css +14 -0
  4. package/client-ui/pc/js/93-rail.js +3 -1
  5. package/client-ui/shared/conv/03-chat-message.js +20 -3
  6. package/client-ui/shared/conv/27-conv-ledger.js +1 -1
  7. package/client-ui/shared/conv/conv.css +17 -0
  8. package/client-ui/shared/conversation.js +170 -39
  9. package/client-ui/shared/transport-http.js +183 -38
  10. package/client-ui-mobile/index.html +370 -68
  11. package/client-ui-mobile/reqs.js +42 -29
  12. package/internal/agent/capability.go +8 -1
  13. package/internal/agent/claude.go +5 -3
  14. package/internal/agent/codebuddy.go +6 -2
  15. package/internal/agent/codex.go +7 -2
  16. package/internal/agent/dsh_sdk.go +8 -0
  17. package/internal/agent/dsh_sdk_test.go +17 -0
  18. package/internal/agent/engine.go +20 -0
  19. package/internal/agent/engine_test.go +52 -0
  20. package/internal/agent/llmengine.go +14 -0
  21. package/internal/agent/llmengine_test.go +24 -0
  22. package/internal/agent/llmhttp.go +6 -0
  23. package/internal/agent/llmhttp_test.go +27 -0
  24. package/internal/agent/llmmodels.go +48 -0
  25. package/internal/agent/permission.go +5 -0
  26. package/internal/cli/ask.go +32 -17
  27. package/internal/cli/requirements.go +2 -0
  28. package/internal/cli/sessionlog.go +5 -0
  29. package/internal/cli/sessionlog_user_test.go +49 -0
  30. package/internal/client/requirements.go +27 -7
  31. package/internal/client/requirements_test.go +51 -0
  32. package/internal/client/service.go +24 -2
  33. package/internal/client/service_test.go +35 -0
  34. package/internal/requirements/consolidate.go +86 -24
  35. package/internal/requirements/requirements.go +9 -0
  36. package/internal/requirements/requirements_test.go +129 -0
  37. package/internal/requirements/store.go +11 -3
  38. package/internal/suggest/suggest.go +118 -7
  39. package/internal/suggest/suggest_test.go +50 -0
  40. package/npm/apk/magic-agent-mobile-release.apk +0 -0
  41. package/npm/apk/version.json +3 -3
  42. package/npm/dist/darwin-arm64/magic-agent +0 -0
  43. package/npm/dist/darwin-arm64/magic-client +0 -0
  44. package/npm/dist/darwin-x64/magic-agent +0 -0
  45. package/npm/dist/darwin-x64/magic-client +0 -0
  46. package/npm/dist/linux-arm64/magic-agent +0 -0
  47. package/npm/dist/linux-arm64/magic-client +0 -0
  48. package/npm/dist/linux-x64/magic-agent +0 -0
  49. package/npm/dist/linux-x64/magic-client +0 -0
  50. package/npm/dist/win32-x64/magic-agent.exe +0 -0
  51. package/npm/dist/win32-x64/magic-client.exe +0 -0
  52. package/package.json +3 -2
@@ -165,7 +165,7 @@ codex / dsh(+sdk) / llm / openclaw 暂不带 `item_id`,
165
165
 
166
166
  | 集合 | 位置 | 回答的问题 |
167
167
  | --- | --- | --- |
168
- | `features[]` | `--contract` 顶层 | 这台 core 支不支持某条**通道**(`feature.session.events` / `feature.session.control` / `feature.session.team` / `feature.requirements`) |
168
+ | `features[]` | `--contract` 顶层 | 这台 core 支不支持某条**通道**(`feature.session.events` / `feature.session.control` / `feature.session.hosted` / `feature.session.team` / `feature.requirements`) |
169
169
  | `engines[].capabilities[]` | 每个引擎 | 这个**引擎**能做什么(`session.stream` / `attachment.native` / `engine.install` …) |
170
170
 
171
171
  静态与运行态严格分开:`capabilities` / `features` 是静态声明,`ok` / `streaming` / `models` /
@@ -175,6 +175,11 @@ codex / dsh(+sdk) / llm / openclaw 暂不带 `item_id`,
175
175
  `--team`。客户端据它决定**要不要显示团队模式开关** —— 没有就整个不显示(点了必然 exit 2 的
176
176
  开关比没有更糟)。插件对带 `team` 的请求也先查这个位,缺了回 **501 + 中文原因**。
177
177
 
178
+ `feature.session.hosted`(2026-10-23 只增量,**不**进 `requiredFeatures`):这台 core 认
179
+ `--hosted`。它只给会话历史里的 `kind:"user"` 事件加 `hosted:true`(客户端回放画成
180
+ 「托管发送」),**不改变执行语义**。插件在能力位缺失时忽略请求里的 `hosted`、照常发送 ——
181
+ 这是纯显示字段,不能因为老 core 不认识就阻断用户的任务;实时气泡仍由客户端自己标。
182
+
178
183
  `feature.requirements`(2026-10-22 只增量,**不**进 `requiredFeatures`):这台 core 认
179
184
  `--requirements` / `--consolidate-requirements`(见 §3.4.1)。插件对 `GET /desk/requirements`
180
185
  与 `POST /desk/requirements/consolidate` 都先查这个位,缺了回 **501 + 中文原因**。
@@ -802,6 +807,9 @@ magic-agent 把会话的查询与修改都做成 CLI 命令,调用方 exec 即
802
807
  | **归纳需求** | `magic-agent --consolidate-requirements <ws_id\|目录\|.>` → 一轮归纳结果(**会调模型**) |
803
808
 
804
809
  `--session-log` 的输出形状与插件同名的 HTTP 接口**逐字段对齐**,两条路可互换。
810
+ 其中 `kind:"user"` 事件新增可选 `hosted:true`(2026-10-23 只增字段,`omitempty`):
811
+ true = 这条提问由客户端「会话托管」自动发出,客户端回放时画成机器人头像 + 「托管发送」小标;
812
+ 缺省 = 用户亲手发的。它只影响画法,不参与执行;老日志 / 老 core 没有该字段时行为不变。
805
813
  HTTP 侧的精简开关是**查询参数**(2026-10-20,只增参数,不传则与旧版一致):
806
814
  `GET /desk/session/{id}/messages?lean=1` 与 `GET /desk/session/{id}/events?after=<seq>&lean=1`
807
815
  会删掉每条事件的 `snapshot` 键(与 `--session-log-lean` 同一口径;CLI 侧该字段 `omitempty`,
@@ -875,8 +883,17 @@ event: end data: {"session":"<id>","after":<末位置>,"state":"done|faile
875
883
  丢掉当前画面,按 `messages` 重拉一遍全量,再用新游标重新 follow。不要在半截上续写。
876
884
  4. `event: end` 一定收摊:`reason=terminal` 按 `state` 落收尾(`failed`/`cancelled`/
877
885
  `interrupted`/`gone` 不写作「已完成」,`done`/`idle` 写作「已完成」);
878
- `reason=max_duration` 是服务端 30 分钟的上限(不是这一轮结束),提示用户重开再接。
879
- 5. 连接断开(网络 / 换会话 / 组件销毁)不是终态,别据此判定任务失败;重开一次即可续上。
886
+ `reason=max_duration` 是服务端 30 分钟的单连接寿命(**不是这一轮结束**)——
887
+ 客户端保留游标、自动重新 follow 续上,不要提示用户「重开会话」。
888
+ 5. 连接断开(网络 / WebView 切后台 / 反向代理空闲超时 / 换会话 / 组件销毁)**不是终态**,
889
+ 别据此判定任务失败。除主动收摊(换会话 / 销毁)外,客户端必须**自动重连**:
890
+ - 游标 = 最后一条已收到事件的 `seq`,重连请求带 `?after=<游标>`(断点续传,不重放已画过的帧);
891
+ - 指数退避:0.8s 起、翻倍、单次 15s 封顶;连续 12 次或 6 分钟仍连不上才按失败收尾;
892
+ - 重连期间那一轮**不封口**(不调 `finish` / `qaRunEnd`),底部继续显示运行态;
893
+ 界面插一行「连接中断,正在自动重连…」,恢复后就地改成「已重新连接,继续同步」;
894
+ - 4xx(404 老网关 / 401 令牌失效等)是确定性失败,**不重试**,立即 fallback / 如实报错。
895
+ 参考实现:`client-ui/shared/transport-http.js` 的 `followSession`(`onRetry` / `onResume`
896
+ 可选回调,老宿主不实现也不影响续流);闸门 `npm run check:follow`。
880
897
  6. 服务端没有这个路由(老版本,404)→ 退回旧行为(一次性快照),不要报错弹窗。
881
898
 
882
899
  服务端行为:`?lean=1` 与 `messages/events` 同一口径;`after` 缺省 = 从文件头读。
@@ -970,11 +987,16 @@ core 侧只有一张空的 sid→title 覆盖表,于是:
970
987
  | 事实 | 说明 |
971
988
  | --- | --- |
972
989
  | 生成在 core | `magic-agent --suggest-followups <file\|->`(`internal/suggest`):提示词、截断、JSON 解析、去重与长度上限都只有一份;移动端 / PC 拿到的建议不会各说各话 |
990
+ | **默认关闭** | 建议引擎**不配置 = 不开启建议功能**(2026-10-23 用户定调)。组件 `suggestEngine` 为空时不发 `/desk/suggest`、不画建议、不补历史建议、托管也不自动执行;老用户升级后默认没有建议,需去「我的」里显式选引擎 |
991
+ | 配置入口 | 移动端「我的 → 建议引擎 / 建议模型」两行,点开的是与「默认引擎 / 模型」相同的**二列抽屉**:左列首项「不开启」(默认),其余是 `/desk/engines` 探测到的可用引擎;点左列只切右列,点右列才落定。引擎存 `magic.suggest.engine`、模型存 `magic.suggest.model`,都属于环境配置(`ENV_SCOPE_KEYS` + `/desk/prefs` 同步);改完立刻 `conv.setSuggestEngine(engine, model)` 生效,下一轮收尾开始生成 |
992
+ | 选引擎 / 模型 | `llm` 最快(**模型留空 / 选「引擎默认」**时 core 自动挑 `models.json` 里 `thinking.disabled` 的变体,实测 0.9-2.3s);`claude` / `codex` 可用各自的关闭档。显式选中会思考的 `llm` 模型(如 `MiniMax-M3.1-Flash`)时,建议生成会自动换同文件的 no-think 变体;配置里没有变体才保留原模型 |
993
+ | **思考固定关闭** | 建议调用一律带 `NoThinking`,各引擎走官方开关:`claude` → `--thinking disabled`;`codex` → `model_reasoning_effort="none"`;`llm` → 同线 `thinking.disabled` 变体,没有变体时 `reasoning_effort="none"`(CLI 委托路径用 `-o reasoning_effort none`);`codebuddy` → settings `alwaysThinkingEnabled=false`;`dsh` → SDK `InitializeParams.reasoningEffort="off"`。`trae`/`openclaw`/`arkclaw` 没有已知开关,仍保留 `NoThinking` 标记,失败后再降级 `llm` |
973
994
  | **不续接会话** | 独立进程、新会话、`ToolsOff`、不带 `SessionID` / `Continue` —— 「请生成建议」这类指令不会写进用户正在聊的上下文,也不进会话登记表 / 日志 |
974
- | 引擎固定 `llm` | 与标题归纳同一档(纯文本、无工具副作用)。界面**不再传当前编码引擎**:建议是旁路,不能排进 codebuddy / claude 的队列 —— 主会话一轮没退干净时,`/desk/suggest` 会跟着 22s 超时(2026-10-23 实测);core 侧仍允许 `-e` 覆盖 |
995
+ | 引擎走「无工具 + JSON」通道 | core 的 `suggest.Generate` 固定 **`ToolsOff` + `JSONSchema` + `NoThinking`**,引擎以 `--print --output-format json` 返回一小段结果,不走流式、不带工具。默认选 `llm`,优先用 `models.json` 里第一条 `extraBody.thinking.type=disabled` 的变体(本机 `minimax-nothink`,实测 0.9-2.3s) |
996
+ | 慢引擎不拖界面 | `claude` / `codex` / `codebuddy` / `dsh` 等非 `llm` 引擎给 10s 主引擎预算;失败/超时才降级 `llm`,并清掉当前引擎的 model。界面默认不传 engine,直接走快路径,20s 超时不会触发 |
975
997
  | **允许空数组** | 模型判断「没有下一步」时回 `[]`,这是**合法结果**:客户端不画任何按钮,不拿模板凑 |
976
998
  | 成功收尾后问 | 失败 / 被用户停止 / 还在跑的轮次都不问;本轮出现过提问卡(引擎在等作答)时也跳过,避免与作答卡抢注意力 |
977
- | 历史最后一轮补一次 | 打开一条**已收尾、不在跑**的历史会话时,回放完对最后一轮补一次建议;更早的轮次不问(它的「下一步」早已被后续对话覆盖)。缓存按 `(sid, 最后一条 user 的 at/seq)` 放 `localStorage`,30 分钟内重开不重复调模型;空数组也缓存 |
999
+ | 历史最后一轮补一次 | 打开一条**已收尾、不在跑**的历史会话时,回放完对最后一轮补一次建议;更早的轮次不问(它的「下一步」早已被后续对话覆盖)。缓存按 `(sid, 最后一条 user 的 at/seq, 建议引擎, 建议模型)` 放 `localStorage`,30 分钟内重开不重复调模型;空数组也缓存 |
978
1000
  | 历史建议只展示 | 历史补出来的建议**不排托管计时器、不自动发送**,只有用户亲手点才走正常发送路径;否则一打开旧会话,托管会立刻把几天前的一句建议发出去 |
979
1001
  | 输入是这一轮 | `user` = 用户上一句,`reply` = 本轮回答正文(core 侧再截断:user 1500 / reply 6000 runes,取回答**尾部**) |
980
1002
  | 点击 = 发送 | 界面把建议文本交给正常发送路径(用户气泡、排队、附件闸门、串行门全部一致),**不是**只填进输入框 |
@@ -987,28 +1009,34 @@ core 侧只有一张空的 sid→title 覆盖表,于是:
987
1009
 
988
1010
  #### 会话托管:在「建议」之上自动发第一条(2026-10-23,纯客户端策略)
989
1011
 
990
- 用户定调:「托管模式下自动按照最佳建议提交到会话中执行,是否开启在我的 tab 中可配置」。
1012
+ 用户定调:「托管模式下自动按照最佳建议提交到会话中执行」;后续把「建议引擎」配置放到「我的」tab(默认不配置 = 建议功能关闭),托管开关仍在会话页运行设置、按会话记。
1013
+ 2026-10-23 再定:「跟团队模式一样改成挂在会话上,子会话继承,不再是全局」。(前者已被后者取代:
1014
+ 开关不再是「我的」里的设备级全局项,入口与团队模式并排放进会话页的「任务运行设置」。)
991
1015
 
992
1016
  这**不是**新的 wire 契约:没有新接口、没有新字段,core / 插件完全不需要知道托管是否开启。
993
1017
  它只是组件在「拿到建议」与「用户点建议」之间加的一条本地策略:
994
1018
 
995
1019
  | 事实 | 说明 |
996
1020
  | --- | --- |
997
- | 开关位置 | 移动端「我的」→「会话托管」;设备级(`localStorage`,随环境分桶),默认关。开关的权威值在宿主,组件只执行 |
1021
+ | 开关位置 | 移动端会话页右上角 →「任务运行设置」→「会话托管」;**按会话**记在 `magic.hosting.sessions.v1`(`sid → 开/关`),默认关。与团队模式同一套形状,归属这个环境(`ENV_SCOPE_KEYS` + `/desk/prefs` 同步),「我的」里不再有全局开关 |
1022
+ | 启用条件 | 必须先有**建议引擎**:未配置时运行设置那行显示「未配置建议引擎」,点击只提示去「我的」配置;组件 `setHosting(true)` 也会拒绝,避免没有「最佳建议」却自己跑 |
1023
+ | 子会话继承 | 子会话(团队模式拆出的子任务)**没有自己的记录**时,沿 `parent_session_id` / `parent_run_id` 向上找最近一条显式记录;都没有才算关。主会话改开关,没单独设过的子会话跟着变;在子会话里手动改一次 = 写自己的记录,从此显式覆盖(不再继承) |
998
1024
  | 自动发哪条 | 建议数组的**第一条**(模型排序即优先级);不拼接、不改写、不自己生成 |
999
1025
  | 发送路径 | 与用户点建议同一条:内核画用户气泡、串行门 `beforeAsk`、权限、排队、附件闸门全部照旧;不经过输入框 |
1026
+ | 发送标识 | 托管自动发的那条**不是「我」**(2026-10-23 用户:「是托管发送的要标识 显示时区别于“我”发送」):实时气泡用机器人头像 + 虚线气泡 + 「托管发送」小标;ask 请求带 `hosted:true`,core 写进 `kind:"user"` 历史事件,重开会话回放仍是同一标识。标识只影响画法,不改提示词 / 引擎 / 权限 |
1000
1027
  | 先给一眼 | 建议画出来后等 1.2s 再发;这 1.2s 内输入框有字 / 有附件 / 已开新轮 / 已切会话 / 已关托管 → 放弃本次自动发送 |
1001
- | 连续上限 | 12 轮。到顶 / 本轮失败 / 本轮被停止 → **自动暂停**并回调宿主把「我的」开关关掉;回放历史里的旧失败不算 |
1028
+ | 连续上限 | 12 轮。到顶 / 本轮失败 / 本轮被停止 → **自动暂停**并回调宿主把**这条会话**的开关关掉;回放历史里的旧失败不算 |
1002
1029
  | 空建议 | 不自动发,也不拿模板凑;托管保持开启,等下一轮(用户手动发的那句)结束后再看 |
1003
1030
  | 历史建议 | 打开历史会话补出来的「最后一轮」建议**只展示**,不进托管;托管只对本次实时新收尾的轮次生效 |
1004
1031
  | 交付完成 | 小说 / 故事 / 文章 / 报告等**完整成品**交付完、AI 没有提问也没列下一步 → 建议必须为空;「再来一篇 / 换个题材 / 再写一个 / 继续写」由服务端确定性过滤(除非 AI 明确邀请),托管不得自动续写 |
1005
1032
  | 提问卡 | 托管开着时,卡出现 1.2s 后自动选**第一项**交卷(优先「继续 / 下一步 / 开始」措辞);用户碰卡、关闭卡、自己输入 → 取消这次自动答,不抢人的操作。只处理 `kind:"question"`,待授权(permission)绝不自动点,避免代用户授权 |
1006
1033
  | 成功判据 | 不只看收尾回调的 `ok`:`llm` 这类**不发 `turn_end`** 的引擎走兜底 `hardFinish(0,'已完成')`,也必须算成功。否则建议永远不请求、托管第一轮就误暂停(2026-10-23 实测修复) |
1007
- | 停止 | 「我的」开关、建议行上的「托管中」标记、或正常的中断按钮(本轮被停 → 自动暂停) |
1034
+ | 停止 | 运行设置里的按会话开关、建议行上的「托管中」标记、或正常的中断按钮(本轮被停 → 自动暂停) |
1008
1035
  | 可见性 | 待发时建议行前有「托管中」标记、第一条建议加描边;发送后建议行收起。不做「静默自己跑」 |
1009
1036
 
1010
1037
  ⚠️ 托管是**宿主级**能力(`client-ui/shared/conversation.js` 的 `hosting` / `setHosting`),
1011
- 不是内核渲染的一部分;magic-test 桌面端那份内联 `app.js` 不加载这个组件,因此不会出现托管开关。
1038
+ 不是内核渲染的一部分;**开关按会话**,组件只执行宿主导出的当前值(子会话的继承也在宿主侧算)。
1039
+ magic-test 桌面端那份内联 `app.js` 不加载这个组件,因此不会出现托管开关。
1012
1040
 
1013
1041
  #### 会话状态:与标题同一套存法,客户端只读(2026-10-21)
1014
1042
 
@@ -1064,9 +1092,16 @@ magic-agent --consolidate-requirements ~/work/proj # 手动整理一轮(**会
1064
1092
  magic-agent --consolidate-requirements . --requirement-force # 没有新记忆也跑一轮
1065
1093
  magic-agent --consolidate-requirements . --requirement-all # 全量重放(回填/校正)
1066
1094
  magic-agent --consolidate-requirements . --requirement-auto # 自动归纳:只增量,绝不初始化
1095
+ magic-agent --consolidate-requirements . --requirement-module 登录 # 只整理「登录」这个模块
1067
1096
  magic-agent --consolidate-requirements . --requirement-engine claude # 用配置的默认引擎整理
1068
1097
  ```
1069
1098
 
1099
+ **模块级整理(2026-10-23)**:`--requirement-module <模块>` 只把增量写回该模块,
1100
+ **不推进空间游标** —— 其它模块的记忆仍留在 `pending`,留给空间级整理 / 自动归纳;
1101
+ 因此可以重复点、不会吞掉别的模块的需求。它**不触发首次目录初始化**(目录基线是空间级的),
1102
+ 模块不存在会明确报错。HTTP 侧对应 `POST /desk/requirements/consolidate` 的
1103
+ `{"workspace":"…","module":"登录"}`;`module` 留空 = 整理整个空间。
1104
+
1070
1105
  **引擎口径(2026-10-22 用户)**:整理的引擎就是**客户端配置的默认引擎**。移动端/PC 点「整理」
1071
1106
  时把「我的 → 默认引擎/模型」放进 `POST /desk/requirements/consolidate` 的
1072
1107
  `{"engine":"claude","model":"…"}`,插件原样组成 `--requirement-engine` / `--requirement-model`。
@@ -1104,8 +1139,9 @@ CLI)才按 `claude → codebuddy → codebuddy-ai → codex → trae → dsh`
1104
1139
  "final_preview":"用户用手机号登录,验证码 60 秒可重发","updated_at":1761090000000}]}]}]}
1105
1140
  ```
1106
1141
 
1107
- `--consolidate-requirements` 的输出:`{ok, workspace, dir, noop?, bootstrap?, bootstrap_engine?,
1108
- scanned?, updated, created, points, conclusions, consumed, replayed?, calls, pending, error?}`。
1142
+ `--consolidate-requirements` 的输出:`{ok, workspace, dir, module?, noop?, bootstrap?, bootstrap_engine?,
1143
+ scanned?, updated, created, points, conclusions, consumed, replayed?, calls, pending, error?}`
1144
+ (`module` 只在模块级整理时出现;此时 `consumed` 恒为 0)。
1109
1145
  `noop=true` 是「没有未归纳记忆、也没 `--requirement-force`」的正常结果,不是错;
1110
1146
  `bootstrap=true` 表示这一轮做了首次目录初始化(`bootstrap_engine` 是实际读目录的引擎,
1111
1147
  `scanned` 是扫描到的条目数,排障用)。
@@ -1124,8 +1160,11 @@ scanned?, updated, created, points, conclusions, consumed, replayed?, calls, pen
1124
1160
  **客户端要做的事**:列表页只调 `--requirements`(每刷一次都调,绝不顺手触发归纳);
1125
1161
  「整理」按钮调 `--consolidate-requirements`,并**传它要整理的那个空间 + 当前配置的默认引擎**
1126
1162
  (移动端需求页同时列出多个空间时,组头各给一枚「整理」,不要把「当前会话的空间」当成用户点的那个);
1163
+ 二级页(点进某个模块)的「整理」必须带 `module`,**只整理当前模块** —— 不带就会退化成整空间,
1164
+ 用户点一个模块却看到所有模块都变了(2026-10-23 报障)。
1127
1165
  顶栏另给「全部整理」:按空间**串行**逐个调同一个接口(有待整理的 → 未初始化的 → 其它),
1128
1166
  一个空间失败不阻断后面的,全部结束后汇总「成功/失败/新建/更新」。
1167
+ 忙态只标**本次真正在整理**的那一个入口;其余按钮只置灰、保持原文案(否则看起来像全部在跑)。
1129
1168
  按钮在首次初始化时要允许几分钟等待(文案「初始化中…」),完成后再 `--requirements` 刷新。
1130
1169
  打开一篇文档读 `file` 绝对路径的正文即可(普通 markdown,走既有的阅读器)。插件在每轮 `/desk/ask` 收尾后**防抖 12s 自动归纳**
1131
1170
  该轮所在工作区(`MAGIC_CLIENT_NO_REQ_AUTO=1` 关闭),所以客户端不必自己轮询触发。
package/README.md CHANGED
@@ -173,7 +173,8 @@ magic-agent -p "对比两张图" -a a.png -a b.png
173
173
 
174
174
  # 流式输出
175
175
  magic-agent --stream -e claude -o text "复杂问题"
176
- magic-agent --stream --no-thinking -e claude "问题"
176
+ magic-agent --stream --no-thinking -e claude "问题" # 只隐藏思考事件
177
+ magic-agent --disable-thinking -e claude "问题" # 请求模型真正关闭思考
177
178
 
178
179
  # 会话
179
180
  magic-agent --sessions # 会话登记表
@@ -199,7 +200,8 @@ magic-agent --append <run_id> -p "追加:顺便把 lint 也跑了"
199
200
  | `--backoff` | `2s` | 首次重试退避(指数翻倍,上限 30s,带抖动) |
200
201
  | `-o, --output` | `json` | 输出格式:`json` \| `text` |
201
202
  | `--stream` | 关 | 流式输出(text 模式思考走 stderr) |
202
- | `--no-thinking` | 关 | 流式模式下不转发思考过程增量 |
203
+ | `--no-thinking` | 关 | 流式模式下不转发思考过程增量(只隐藏,不关闭模型思考) |
204
+ | `--disable-thinking` / `--no-think` | 关 | 请求引擎真正关闭模型思考:claude `--thinking disabled`、codex `effort=none`、codebuddy `alwaysThinkingEnabled=false`、dsh `reasoningEffort=off`、llm 选 no-think 变体 / `reasoning_effort=none` |
203
205
  | `--engines` | 关 | 列出引擎、可用性、模型、版本与一键安装命令 |
204
206
  | `--no-models` | 关 | 配合 `--engines`:跳过模型与版本探测 |
205
207
  | `--json` | 关 | 兼容保留(行为同 `--engines` 的默认 JSON 输出) |
@@ -255,6 +257,7 @@ magic-agent --requirements all # 列全部工作区(只读
255
257
  magic-agent --requirements ~/work/proj # 只列这个目录(也认 ws id)
256
258
  magic-agent --consolidate-requirements ~/work/proj # 手动整理:首次=按目录初始化,之后只处理新会话
257
259
  magic-agent --consolidate-requirements . --requirement-all # 全量重放(回填/校正)
260
+ magic-agent --consolidate-requirements . --requirement-module 登录 # 只整理「登录」这个模块
258
261
  ```
259
262
 
260
263
  - **第一次整理 = 初始化**:工作区还没初始化过时,先有界扫描空间目录,用**客户端配置的默认
@@ -263,8 +266,11 @@ magic-agent --consolidate-requirements . --requirement-all # 全量重放(
263
266
  默认引擎不能读目录(如 `llm`)会明确报错并提示换引擎。目录为空不算失败,会回落到会话增量。
264
267
  - **自动归纳只增量**:插件在每轮对话收尾后防抖 12s 自动归纳(`MAGIC_CLIENT_NO_REQ_AUTO=1`
265
268
  关闭),它带 `--requirement-auto`,**绝不触发首次目录初始化**。
266
- - 需求页每个空间组头有独立「整理」,只整理那个空间;顶栏「全部整理」按空间串行跑一遍,
267
- 一键触发全部(某个空间失败不影响后面的)。没有新会话时返回 `noop`,不会空跑一次模型。
269
+ - **模块级整理**:`--requirement-module <模块>` 只把这个模块的增量写回该模块,
270
+ **不推进空间游标**(别的模块的记忆仍留在待整理),可以重复点;它不触发首次目录初始化。
271
+ - 需求页每个空间组头有独立「整理」,只整理那个空间;点进某个模块后,二级页的「整理」
272
+ 只整理当前模块;顶栏「全部整理」按空间串行跑一遍,一键触发全部(某个空间失败不影响后面的)。
273
+ 没有新会话时返回 `noop`,不会空跑一次模型。
268
274
 
269
275
  详细契约见 [CLIENT-CONTRACT.md](CLIENT-CONTRACT.md) §3.4.1。
270
276
 
@@ -1540,6 +1540,18 @@ body::after{content:"";position:fixed;inset:0;z-index:60;pointer-events:none;opa
1540
1540
  .qa-msg.qa-mine .qa-bd{padding:10px 14px;
1541
1541
  border:1px solid var(--line);border-radius:14px 5px 14px 14px;
1542
1542
  background:linear-gradient(180deg,#FBF9F7,#F5F1ED)}
1543
+ /* 托管自动发送(2026-10-23):PC 没有托管开关,但同一条历史可能在手机端被托管发出。
1544
+ 回放时按 `hosted:true` 画成机器人头像 + 「托管发送」小标。
1545
+ ⚠️ 与 client-ui/shared/conv/conv.css 的 `.qa-hosted` 那段同步改(两份皮肤目前各存一份)。 */
1546
+ .qa-msg.qa-hosted .qa-bd{border-style:dashed;border-color:var(--cinnabar-hi);
1547
+ background:linear-gradient(180deg,#FFF8F5,#FDF0EA)}
1548
+ .qa-msg.qa-hosted .qa-ico{color:var(--cinnabar-hi);--h:var(--halo-c-red)}
1549
+ .qa-hosted-tag{display:inline-flex;align-items:center;gap:4px;margin:0 0 6px;
1550
+ padding:2px 7px;border-radius:999px;background:rgba(240,105,83,.10);
1551
+ color:var(--cinnabar-hi);font-size:10.5px;line-height:15px;font-weight:600;
1552
+ letter-spacing:.02em;user-select:none;white-space:nowrap}
1553
+ .qa-hosted-tag::before{content:"";width:4px;height:4px;border-radius:50%;
1554
+ background:currentColor;flex:0 0 auto}
1543
1555
  /* ===== 审核用户的回复(2026-09-20 加审查轮;2026-09-22 **去卡片**)=====
1544
1556
  用户原话(2026-09-20):「验证不通过的返回建议 再代表审核用户发送到对话中」——
1545
1557
  它不是需求作者、也不是引擎,所以既不能复用用户气泡(朱砂「超」字),也不能复用 bot 气泡(大喵)。
@@ -3582,6 +3594,8 @@ body::after{content:"";position:fixed;inset:0;z-index:60;pointer-events:none;opa
3582
3594
  [data-theme="dark"] .qa-t.md-body blockquote{background:rgba(245,244,243,.04);border-color:#3B3835}
3583
3595
  [data-theme="dark"] .qa-t.md-body blockquote p{color:#A8A29E}
3584
3596
  [data-theme="dark"] .qa-msg.qa-mine .qa-bd{background:#1C1C1E;border-color:#3B3835}
3597
+ [data-theme="dark"] .qa-msg.qa-hosted .qa-bd{background:#241B18;border-color:#8A5144}
3598
+ [data-theme="dark"] .qa-hosted-tag{background:rgba(240,105,83,.16);color:#F2A08E}
3585
3599
  [data-theme="dark"] .qa-av-txt{background:linear-gradient(180deg,#2B2B2E,#1C1C1E)}
3586
3600
  /* 审核用户的回复(深色):**不需要另给一套** —— 它已经与 bot 答复同形(无底无框),
3587
3601
  与下面「审核引擎的回复」同一条口径。原来那条 `background:#241F16;border-color:琥珀` 的
@@ -258,7 +258,9 @@ function qaRailApply(ctx, events){
258
258
  if(Number.isFinite(sq) && sq > ctx.seq) ctx.seq = sq;
259
259
  if(kind === "user"){
260
260
  ctx.h = null;
261
- qaRenderMine(String(ev.text || ""), []);
261
+ /* 移动端「会话托管」自动发的提问带 hosted:true:PC 回放同一条历史时也画成
262
+ 「托管发送」,否则同一会话在两端会长成两张脸。 */
263
+ qaRenderMine(String(ev.text || ""), [], { hosted: !!ev.hosted });
262
264
  continue;
263
265
  }
264
266
  if(!ctx.h){ ctx.h = qaRenderAnswer(); if(!ctx.h) break; }
@@ -460,9 +460,16 @@ function qaMsgRetry(msg){
460
460
  toast("正在重试 · " + escHtml(text.slice(0, 24)) + (text.length > 24 ? "…" : ""));
461
461
  qaSend(text, { followup: true, echo: false });
462
462
  }
463
- function qaRenderMine(text, atts){
463
+ /* opts(可选,2026-10-23 只增参数):
464
+ · hosted=true —— 这条是「会话托管」自动选的第一条建议,不是用户亲手打的。
465
+ 仍然是右侧的用户轮次(留 `.qa-mine`,排队 / 重试 / 取文都照旧),但头像换成
466
+ 数字分身的机器人、气泡虚线描边,并在正文上方挂一枚「托管发送」小标 ——
467
+ 用户 2026-10-23 报:「是托管发送的要标识 显示时区别于“我”发送」。
468
+ 老调用方一律不传第三参,画出来与以前逐像素一致。 */
469
+ function qaRenderMine(text, atts, opts){
464
470
  const s = $("#qa-stream"); if(!s) return;
465
471
  s.hidden = false;
472
+ const hosted = !!(opts && opts.hosted);
466
473
  /* 空态那句「还没有事件」得先让位:它只在画布一条都还没有时该在。
467
474
  ⚠️ 以前只有**事件流**那条路径会摘它(conversation.js 的 ingest),而用户气泡
468
475
  是宿主直接调这里画的、不经过事件流 —— 于是发出第一句之后,画布上同时挂着
@@ -471,8 +478,18 @@ function qaRenderMine(text, atts){
471
478
  const empty = s.querySelector(".conv__empty");
472
479
  if(empty) empty.remove();
473
480
  const div = document.createElement("div");
474
- div.className = "qa-msg qa-mine";
475
- div.innerHTML = `<div class="qa-ico">${qaAvatarHtml("mine")}</div><div class="qa-bd"><div class="qa-t md-body"></div></div>`;
481
+ div.className = "qa-msg qa-mine" + (hosted ? " qa-hosted" : "");
482
+ div.innerHTML = `<div class="qa-ico">${qaAvatarHtml(hosted ? "sim" : "mine")}</div><div class="qa-bd"><div class="qa-t md-body"></div></div>`;
483
+ /* 小标放在 .qa-t **前面**(正文之外):取文 / 重试只看 .qa-t,不会把这四个字
484
+ 当成用户说的话再发一遍。 */
485
+ if(hosted){
486
+ const bd0 = div.querySelector(".qa-bd");
487
+ const tag = document.createElement("div");
488
+ tag.className = "qa-hosted-tag";
489
+ tag.textContent = "托管发送";
490
+ tag.title = "会话托管自动选择的第一条建议,不是你亲手输入的";
491
+ bd0.insertBefore(tag, bd0.firstChild);
492
+ }
476
493
  // 用户消息按 markdown 渲染:marked + DOMPurify(沿用产品文档同一管线,输入是用户可控,
477
494
  // 但消毒仍保留以防恶意粘贴)。
478
495
  const host = div.querySelector(".qa-t");
@@ -313,7 +313,7 @@ function qaSessionPlayer(){
313
313
  }catch(e){ /* 收尾失败不影响回放 */ }
314
314
  h = null;
315
315
  }else if(arts){ arts.flush(null); } // 没有答复卡的一轮:清掉残留,别串到下一轮
316
- qaRenderMine(String(ev.text || ""), []);
316
+ qaRenderMine(String(ev.text || ""), [], { hosted: !!ev.hosted });
317
317
  t0 = atMs(ev);
318
318
  continue;
319
319
  }
@@ -446,6 +446,21 @@
446
446
  .qa-msg.qa-mine .qa-bd{padding:10px 14px;
447
447
  border:1px solid var(--line);border-radius:var(--qa-bubble-r,14px 5px 14px 14px);
448
448
  background:linear-gradient(180deg,#FBF9F7,#F5F1ED)}
449
+ /* 托管自动发送的用户气泡(2026-10-23 用户:「是托管发送的要标识 显示时区别于“我”发送」):
450
+ 还是右侧的用户轮次,但头像换成数字分身的机器人(qaRenderMine 的 opts.hosted),
451
+ 气泡虚线描边 + 朱砂淡底,正文上方一枚「托管发送」小标 —— 一眼能看出这句不是手打的。 */
452
+ .qa-msg.qa-hosted .qa-bd{border-style:dashed;border-color:var(--cinnabar-hi);
453
+ background:linear-gradient(180deg,#FFF8F5,#FDF0EA)}
454
+ .qa-msg.qa-hosted .qa-ico{color:var(--cinnabar-hi);--h:var(--halo-c-red)}
455
+ .qa-hosted-tag{display:inline-flex;align-items:center;gap:4px;margin:0 0 6px;
456
+ padding:2px 7px;border-radius:var(--r-full,999px);
457
+ background:rgba(240,105,83,.10);color:var(--cinnabar-hi);
458
+ font-size:10.5px;line-height:15px;font-weight:600;letter-spacing:.02em;
459
+ user-select:none;white-space:nowrap}
460
+ .qa-hosted-tag::before{content:"";width:4px;height:4px;border-radius:50%;
461
+ background:currentColor;flex:0 0 auto}
462
+ [data-theme="dark"] .qa-msg.qa-hosted .qa-bd{background:#241B18;border-color:#8A5144}
463
+ [data-theme="dark"] .qa-hosted-tag{background:rgba(240,105,83,.16);color:#F2A08E}
449
464
  /* 排队中的用户气泡(`is-queued`):已经发出去、但还在等前一轮跑完。
450
465
  气泡照常可读(用户要能确认自己写的是什么),只把底色压淡 + 虚线边 + 底下补一行
451
466
  「排队中 · 上一轮结束后自动发送」—— **不隐藏、不挪走**,否则就是这次报障的
@@ -1022,6 +1037,8 @@
1022
1037
  [data-theme="dark"] .qa-t.md-body blockquote p{color:#A8A29E}
1023
1038
 
1024
1039
  [data-theme="dark"] .qa-msg.qa-mine .qa-bd{background:#1C1C1E;border-color:#3B3835}
1040
+ /* 放在上面那条**之后**:同为 4 个类级选择器,靠后才会赢(托管气泡的暗色底不被盖回灰)。 */
1041
+ [data-theme="dark"] .qa-msg.qa-hosted .qa-bd{background:#241B18;border-color:#8A5144}
1025
1042
 
1026
1043
  [data-theme="dark"] .qa-av-txt{background:linear-gradient(180deg,#2B2B2E,#1C1C1E)}
1027
1044