magic-agent 2.4.3 → 2.6.3

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 (207) hide show
  1. package/CLIENT-CONTRACT.md +467 -41
  2. package/README.md +15 -14
  3. package/client-ui/pc/app.css +74 -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 +40 -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 +247 -23
  11. package/client-ui/shared/conv/06-ask-answer.js +75 -1
  12. package/client-ui/shared/conv/09-stream-apply.js +72 -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 +217 -0
  16. package/client-ui/shared/conv/conv.css +329 -174
  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 +373 -50
  20. package/client-ui/shared/transport-http.js +14 -0
  21. package/client-ui-mobile/apps.css +16 -0
  22. package/client-ui-mobile/apps.js +406 -0
  23. package/client-ui-mobile/index.html +5554 -639
  24. package/client-ui-mobile/reqs.css +110 -1
  25. package/client-ui-mobile/reqs.js +533 -74
  26. package/client-ui-mobile/reqshot.js +505 -0
  27. package/client-ui-mobile/scripts.css +67 -0
  28. package/client-ui-mobile/scripts.js +599 -0
  29. package/client-ui-mobile/vendor/cose-base.js +3214 -0
  30. package/client-ui-mobile/vendor/cytoscape-fcose.js +1549 -0
  31. package/client-ui-mobile/vendor/cytoscape.min.js +32 -0
  32. package/client-ui-mobile/vendor/layout-base.js +5230 -0
  33. package/cmd/magic-client/browsectl.go +199 -0
  34. package/cmd/magic-client/clientctl.go +69 -10
  35. package/cmd/magic-client/main.go +210 -32
  36. package/internal/agent/ask.go +45 -0
  37. package/internal/agent/ask_test.go +39 -0
  38. package/internal/agent/attachments_test.go +6 -2
  39. package/internal/agent/capability.go +7 -1
  40. package/internal/agent/claude.go +20 -15
  41. package/internal/agent/codebuddy.go +125 -34
  42. package/internal/agent/codebuddy_account_test.go +54 -0
  43. package/internal/agent/codebuddy_stream_test.go +103 -0
  44. package/internal/agent/credits.go +713 -0
  45. package/internal/agent/credits_minimax_test.go +97 -0
  46. package/internal/agent/credits_test.go +211 -0
  47. package/internal/agent/custom_models.go +155 -0
  48. package/internal/agent/custom_models_test.go +125 -0
  49. package/internal/agent/dsh.go +123 -49
  50. package/internal/agent/dsh_caps_cache_test.go +184 -0
  51. package/internal/agent/dsh_resume.go +157 -7
  52. package/internal/agent/dsh_test.go +108 -6
  53. package/internal/agent/engine.go +37 -1
  54. package/internal/agent/engine_base.go +21 -17
  55. package/internal/agent/engine_base_test.go +2 -2
  56. package/internal/agent/engine_test.go +47 -86
  57. package/internal/agent/env.go +173 -1
  58. package/internal/agent/env_test.go +114 -0
  59. package/internal/agent/fallback.go +313 -0
  60. package/internal/agent/fallback_test.go +340 -0
  61. package/internal/agent/launch.go +43 -1
  62. package/internal/agent/limitnotice.go +207 -1
  63. package/internal/agent/limitnotice_test.go +140 -0
  64. package/internal/agent/llmhttp.go +46 -4
  65. package/internal/agent/llmhttp_test.go +157 -0
  66. package/internal/agent/models.go +53 -1
  67. package/internal/agent/models_test.go +20 -5
  68. package/internal/agent/pidalive_windows_test.go +40 -0
  69. package/internal/agent/runcmd.go +26 -0
  70. package/internal/agent/runcmd_windows.go +36 -5
  71. package/internal/agent/stream.go +166 -6
  72. package/internal/agent/stream_test.go +25 -38
  73. package/internal/agent/stream_timeline_test.go +2 -37
  74. package/internal/agent/streamjson.go +119 -7
  75. package/internal/agent/tools_suffix_test.go +6 -3
  76. package/internal/agent/trae.go +50 -243
  77. package/internal/agent/trae_exec.go +433 -0
  78. package/internal/agent/trae_exec_test.go +181 -0
  79. package/internal/agent/wraplaunch_test.go +113 -0
  80. package/internal/cli/ask.go +168 -37
  81. package/internal/cli/ask_test.go +1 -1
  82. package/internal/cli/checkin.go +77 -0
  83. package/internal/cli/clientcmd.go +3 -2
  84. package/internal/cli/control.go +46 -5
  85. package/internal/cli/control_test.go +65 -0
  86. package/internal/cli/engines_test.go +103 -0
  87. package/internal/cli/events.go +15 -0
  88. package/internal/cli/events_test.go +50 -0
  89. package/internal/cli/judge.go +104 -0
  90. package/internal/cli/quotas.go +125 -0
  91. package/internal/cli/requirements.go +51 -0
  92. package/internal/cli/root.go +157 -11
  93. package/internal/cli/session.go +75 -3
  94. package/internal/cli/session_test.go +82 -3
  95. package/internal/cli/sessionlog.go +6 -0
  96. package/internal/cli/sessionlog_test.go +22 -2
  97. package/internal/cli/suggest.go +7 -5
  98. package/internal/cli/team.go +37 -9
  99. package/internal/cli/team_test.go +39 -0
  100. package/internal/client/ab.go +372 -0
  101. package/internal/client/ab_test.go +287 -0
  102. package/internal/client/apk.go +8 -0
  103. package/internal/client/apk_test.go +59 -0
  104. package/internal/client/apps.go +1078 -0
  105. package/internal/client/apps_test.go +516 -0
  106. package/internal/client/browse.go +84 -36
  107. package/internal/client/browse_test.go +107 -9
  108. package/internal/client/browseinput.go +481 -0
  109. package/internal/client/browseinput_test.go +216 -0
  110. package/internal/client/corecmd.go +23 -0
  111. package/internal/client/corecmd_other.go +8 -0
  112. package/internal/client/corecmd_windows.go +24 -0
  113. package/internal/client/coreexe.go +196 -0
  114. package/internal/client/coreexe_test.go +161 -0
  115. package/internal/client/coreupgrade.go +866 -0
  116. package/internal/client/coreupgrade_test.go +561 -0
  117. package/internal/client/daemon.go +297 -33
  118. package/internal/client/daemon_test.go +33 -0
  119. package/internal/client/engines_checkin_test.go +57 -0
  120. package/internal/client/fallback_flags_test.go +64 -0
  121. package/internal/client/feishu.go +1259 -0
  122. package/internal/client/feishu_test.go +649 -0
  123. package/internal/client/health_identity_test.go +84 -0
  124. package/internal/client/hostenv.go +68 -0
  125. package/internal/client/hostenv_test.go +37 -0
  126. package/internal/client/jobs.go +518 -24
  127. package/internal/client/jobs_auto.go +571 -0
  128. package/internal/client/jobs_auto_test.go +431 -0
  129. package/internal/client/jobs_test.go +299 -2
  130. package/internal/client/knowledge.go +854 -0
  131. package/internal/client/knowledge_test.go +510 -0
  132. package/internal/client/lan.go +3 -3
  133. package/internal/client/plugins.go +5 -1
  134. package/internal/client/prefs.go +67 -0
  135. package/internal/client/procstart_linux.go +59 -0
  136. package/internal/client/publicurl.go +82 -0
  137. package/internal/client/quotas_merge_test.go +107 -0
  138. package/internal/client/requirements.go +207 -2
  139. package/internal/client/requirements_shots_test.go +200 -0
  140. package/internal/client/requirements_test.go +103 -0
  141. package/internal/client/restart.go +288 -0
  142. package/internal/client/restart_handoff_test.go +230 -0
  143. package/internal/client/restart_test.go +290 -0
  144. package/internal/client/scripts.go +1047 -0
  145. package/internal/client/scripts_decode_implausible.go +38 -0
  146. package/internal/client/scripts_decode_other.go +31 -0
  147. package/internal/client/scripts_decode_test.go +130 -0
  148. package/internal/client/scripts_decode_windows.go +182 -0
  149. package/internal/client/scripts_test.go +739 -0
  150. package/internal/client/service.go +717 -97
  151. package/internal/client/service_test.go +217 -1
  152. package/internal/client/sessions_cache.go +148 -0
  153. package/internal/client/sessions_cache_test.go +163 -0
  154. package/internal/client/sessions_fold.go +165 -0
  155. package/internal/client/sessions_fold_test.go +204 -0
  156. package/internal/client/singleinstance.go +470 -0
  157. package/internal/client/singleinstance_test.go +607 -0
  158. package/internal/client/suggest.go +105 -2
  159. package/internal/client/suggest_test.go +86 -0
  160. package/internal/client/team_flags.go +25 -11
  161. package/internal/client/team_flags_test.go +39 -0
  162. package/internal/client/testmain_test.go +88 -0
  163. package/internal/client/workspace_default_test.go +158 -0
  164. package/internal/client/workspacelockenv_test.go +54 -0
  165. package/internal/config/config.go +48 -0
  166. package/internal/judge/judge.go +315 -0
  167. package/internal/requirements/bootstrap.go +16 -0
  168. package/internal/requirements/consolidate.go +100 -9
  169. package/internal/requirements/doc.go +154 -3
  170. package/internal/requirements/graph.go +138 -0
  171. package/internal/requirements/memory.go +7 -0
  172. package/internal/requirements/modules.go +312 -0
  173. package/internal/requirements/modules_test.go +373 -0
  174. package/internal/requirements/relations.go +155 -0
  175. package/internal/requirements/relations_test.go +231 -0
  176. package/internal/requirements/requirements.go +10 -0
  177. package/internal/requirements/shots.go +418 -0
  178. package/internal/requirements/shots_test.go +387 -0
  179. package/internal/session/multiclient_test.go +368 -0
  180. package/internal/session/session.go +175 -11
  181. package/internal/session/session_test.go +7 -0
  182. package/internal/session/status.go +21 -0
  183. package/internal/session/status_test.go +28 -0
  184. package/internal/session/title.go +16 -0
  185. package/internal/session/title_test.go +26 -0
  186. package/internal/session/workspace_lock.go +265 -28
  187. package/internal/suggest/suggest.go +23 -1
  188. package/internal/team/event.go +3 -1
  189. package/internal/team/execute.go +21 -6
  190. package/internal/team/fallback_route_test.go +80 -0
  191. package/internal/team/planner.go +15 -3
  192. package/internal/team/route.go +89 -17
  193. package/internal/team/team.go +18 -4
  194. package/npm/apk/magic-agent-mobile-release.apk +0 -0
  195. package/npm/apk/version.json +6 -3
  196. package/npm/dist/darwin-arm64/magic-agent +0 -0
  197. package/npm/dist/darwin-arm64/magic-client +0 -0
  198. package/npm/dist/darwin-x64/magic-agent +0 -0
  199. package/npm/dist/darwin-x64/magic-client +0 -0
  200. package/npm/dist/linux-arm64/magic-agent +0 -0
  201. package/npm/dist/linux-arm64/magic-client +0 -0
  202. package/npm/dist/linux-x64/magic-agent +0 -0
  203. package/npm/dist/linux-x64/magic-client +0 -0
  204. package/npm/dist/win32-x64/magic-agent.exe +0 -0
  205. package/npm/dist/win32-x64/magic-client.exe +0 -0
  206. package/npm/install.js +11 -0
  207. package/package.json +14 -3
package/README.md CHANGED
@@ -17,7 +17,7 @@
17
17
  ```bash
18
18
  npm install -g magic-agent # 从 npmjs 全局安装(无需 Go)
19
19
  npm install -g ./magic-agent-<version>.tgz # 从本地 tgz 安装(离线 / 内网)
20
- go build -o bin/magic-agent ./cmd/magic-agent # 从源码构建(Go 1.26+)
20
+ npm run build:current # 从源码构建当前平台(core + client,Go 1.26+)
21
21
  ```
22
22
 
23
23
  > ⚠️ 截至 2026-09-28,npmjs 上还查不到 `magic-agent`(首次正式发布尚未发生),
@@ -36,7 +36,7 @@ go build -o bin/magic-agent ./cmd/magic-agent # 从源码构建(Go 1.26+)
36
36
  | claude | Claude Code | `MAGIC_AGENT_CLAUDE_BIN` → `/opt/homebrew/bin/claude` → PATH |
37
37
  | codebuddy | CodeBuddy Code CLI(`npm i -g @tencent-ai/codebuddy-code`) | `MAGIC_AGENT_CODEBUDDY_BIN` → PATH → npm 全局前缀 |
38
38
  | codebuddy-ai | 同上(**同一个 CLI 的第二个账号**) | `MAGIC_AGENT_CODEBUDDY_AI_BIN` → PATH → npm 全局前缀 |
39
- | trae | trae-cli | `MAGIC_AGENT_TRAE_BIN` → `~/.local/bin/trae-cli` → PATH |
39
+ | trae | TRAE CLI 2.0(`traex`) | `MAGIC_AGENT_TRAE_BIN` → `%LOCALAPPDATA%\Programs\TraeCLI\bin\traex.exe` / `~/.local/bin/traex` → PATH |
40
40
  | llm | [simonw/LLM](https://github.com/simonw/LLM) | `MAGIC_AGENT_LLM_BIN` → `~/.llm-venv/bin/llm` → `/opt/homebrew/bin/llm` → PATH |
41
41
  | codex | `@openai/codex` | `MAGIC_AGENT_CODEX_BIN` → `/opt/homebrew/bin/codex` → PATH |
42
42
  | openclaw | OpenClaw | `MAGIC_AGENT_OPENCLAW_BIN` → `/opt/homebrew/bin/openclaw` → PATH |
@@ -96,7 +96,7 @@ magic-agent --engines --no-models # 跳过模型与版本探测(不启动
96
96
  | codex | `npm install -g @openai/codex` |
97
97
  | openclaw | `npm install -g openclaw@latest` |
98
98
  | dsh | `npm i -g @deepseek-ai/dsh` |
99
- | trae | `sh -c "$(curl -L https://trae.cn/trae-cli/install.sh)"` |
99
+ | trae | `sh -c "$(curl -fsSL https://trae.cn/trae-cli/install_v2.sh)"`(Windows: `irm https://trae.cn/trae-cli/install_v2.ps1 | iex`) |
100
100
  | llm | `python3 -m venv ~/.llm-venv && ~/.llm-venv/bin/pip install llm` |
101
101
  | codebuddy / codebuddy-ai / arkclaw / codebuddy-gateway / 具名 A2A agent | **无该字段**(桌面端 GUI 应用 / 网关,没有可执行的安装命令),怎么才能用见同行 `note` |
102
102
 
@@ -116,7 +116,7 @@ magic-agent --engines --no-models | jq -r '.[] | select(.install) | "\(.engine)\
116
116
  | claude | `~/.claude/settings.json`(顶层 `model` + env 里 `ANTHROPIC_*MODEL[*_NAME]`) |
117
117
  | codebuddy | 客户端模型选择器清单 ∩ 客户端合并配置缓存;回退 `codebuddy --help` 的 `Currently supported` |
118
118
  | codebuddy-ai | 同上(独立配置目录 `~/.workbuddy-ai`) |
119
- | trae | `trae-cli models --json` |
119
+ | trae | `traex models --json` |
120
120
  | llm | `llm models` |
121
121
  | codex | `codex debug models` |
122
122
  | openclaw | `openclaw models list --json` |
@@ -151,7 +151,7 @@ magic-agent -o text -p "用一句话解释什么是熵"
151
151
  # 切引擎、切模型
152
152
  magic-agent -e codebuddy "写一首俳句" # codebuddy 默认 hy3
153
153
  magic-agent -e codebuddy -m glm-5.3 "写一首俳句"
154
- magic-agent -e trae -m My-MiniMax-M3 "..." # trae 无 --model,内部转 -c model.name=
154
+ magic-agent -e trae -m My-MiniMax-M3 "..." # 内部转 traex exec -m <model>
155
155
  magic-agent -e llm -m minimax-m3 "问题" # llm 用自己注册的模型
156
156
 
157
157
  # 工具与权限(--tools 默认 off = 纯 chat)
@@ -387,12 +387,13 @@ npm 包**同时装着** core(`magic-agent`)和客户端插件(`magic-clien
387
387
  与 `/desk/*` 接口。手机要用它,就在装包的那台电脑上把插件跑成后台服务:
388
388
 
389
389
  ```bash
390
- magic-agent client start --lan # 后台常驻(关掉终端也还在),打印手机可用的地址
391
- magic-agent client status # 在不在、端口、手机地址、健康检查(--json 给脚本)
392
- magic-agent client stop # 停掉(本来没跑也算成功)
390
+ magic-agent client start # 后台常驻(关掉终端也还在),默认手机可连
391
+ magic-agent client status # 在不在、端口、手机地址、健康检查(--json 给脚本)
392
+ magic-agent client stop # 停掉(本来没跑也算成功)
393
+ magic-agent client start --local # 只给本机用(绑 127.0.0.1)
393
394
  ```
394
395
 
395
- `start --lan` 会打印每个网卡一条 `http://<局域网IP>:<端口>/?token=…`,以及 `/pair`
396
+ `start` 默认绑 `0.0.0.0:9100`,会打印每个网卡一条 `http://<局域网IP>:9100/?token=…`,以及 `/pair`
396
397
  配对二维码页 —— 那条**在电脑上**打开,手机扫屏幕上的码(或把地址整行复制到手机 App)。
397
398
 
398
399
  | 事实 | 位置 / 说明 |
@@ -405,12 +406,12 @@ magic-agent client stop # 停掉(本来没跑也算成功)
405
406
 
406
407
  要点:
407
408
 
408
- - **必须同一 Wi-Fi**。`--lan` 只是把监听从 `127.0.0.1` 改成 `0.0.0.0`,跨网不在本命令范围内。
409
- - `--lan` 时令牌是**唯一门锁**,短于 24 字符会被直接拒绝启动;默认生成的 32 位十六进制远超这条线。
409
+ - **默认就绑 `0.0.0.0:9100`**,手机在同一 Wi-Fi 下可直接连;只想本机访问加 `--local`(绑 `127.0.0.1`)。跨网不在本命令范围内。
410
+ - 局域网模式下令牌是**唯一门锁**,短于 24 字符会被直接拒绝启动;默认生成的 32 位十六进制远超这条线。
410
411
  - 令牌跨重启稳定是刻意的:手机里存的是「地址 + 令牌」,每次 `start` 换一个就等于配对作废。
411
412
  - `status` 的判据是「pid 活着 **且** `/desk/health` 答得上」,不是只看进程;不健康时退出码 3。
412
- - 端口是系统分配的(`0`),因为**每次 start 会变**,所以别把端口写死进任何配置 —— 以打印的地址为准。
413
- - 排障要看前台日志就跑 `magic-client serve --lan`(同一个二进制,`serve` 不脱离终端)。
413
+ - 默认端口固定 `9100`(`--addr` 可覆盖),跨重启稳定;被占用时启动会明确报错,不会悄悄换端口。
414
+ - 排障要看前台日志就跑 `magic-client serve`(同一个二进制,`serve` 不脱离终端)。
414
415
  - `magic-agent client …` 只是转发层:core **不依赖**插件(`internal/boundary` 钉着这条),
415
416
  所以参数集合由 `magic-client start --help` 定义。
416
417
 
@@ -471,7 +472,7 @@ magic-client serve --ui-dir magic-h5
471
472
  手机访问启动时打印的 `http://<局域网IP>/?token=...`。嵌进自己的服务同理:按上面的
472
473
  布局静态托管,令牌走 `?token=`。
473
474
 
474
- (`magic-client` 随 npm 包安装,也可以只跑 `magic-agent client start --lan` 把插件后台拉起来,
475
+ (`magic-client` 随 npm 包安装,也可以只跑 `magic-agent client start` 把插件后台拉起来,
475
476
  界面资源会自动按包布局找到。)
476
477
 
477
478
  **② 拷进自己的构建**(App/工程内嵌):把 `client-ui-mobile/` 内容与 `client-ui/shared/`
@@ -80,6 +80,14 @@
80
80
  --halo-jade:rgba(15,118,110,.07);
81
81
  --halo-amber:rgba(183,121,31,.085);
82
82
  --halo-indigo:rgba(79,70,229,.07);
83
+
84
+ /* 对话内核(shared/conv/conv.css)深色覆盖里那些半透明叠加底走四条「rgb 三通道」令牌,
85
+ 于是「琥珀 14%」「玉绿 20%」这类底色永远跟着本壳的 --amber / --jade 走,而不是在
86
+ conv.css 里再写死一份灰(2026-10-25 深色配色收口)。暗色那一份在 [data-theme="dark"] 里。 */
87
+ --warning-rgb:183,121,31; /* --amber 亮色 */
88
+ --queued-rgb:183,121,31; /* 本壳没有独立排队金:排队态用琥珀 */
89
+ --success-rgb:13,138,106; /* --jade 亮色 */
90
+ --fail-rgb:214,64,42; /* --cinnabar 亮色 */
83
91
  }
84
92
 
85
93
  /* 暗色主题(2026-09-19):覆盖亮色变量,深色背景 + 浅色文字 */
@@ -96,6 +104,14 @@
96
104
  --halo-jade:rgba(52,211,153,.09);
97
105
  --halo-amber:rgba(251,191,36,.10);
98
106
  --halo-indigo:rgba(129,140,248,.09);
107
+ /* 对话内核(shared/conv/conv.css)深色覆盖里那些半透明叠加底走四条「rgb 三通道」令牌,
108
+ 于是「琥珀 14%」「玉绿 20%」这类底色永远跟着本壳的 --amber / --jade 走,而不是在
109
+ conv.css 里再写死一份灰(2026-10-25 深色配色收口)。⚠️ 必须写在这里::where(:root)
110
+ 的优先级是 0,跟着本主题块一起切换。亮色那份在文件顶部的 :root 里。 */
111
+ --warning-rgb:251,191,36; /* --amber 暗色 */
112
+ --queued-rgb:251,191,36; /* 本壳没有独立排队金:排队态用琥珀 */
113
+ --success-rgb:52,211,153; /* --jade 暗色 */
114
+ --fail-rgb:240,105,83; /* --cinnabar 暗色 */
99
115
  /* 层级刻度在暗色下要**整体换一套**(2026-09-28),这是修一个真实缺陷:
100
116
  :root 里的 --shadow-* 是带蓝调(rgba(15,23,42))的,为白底调的。暗色主题过去只在
101
117
  **4 处**内联覆盖成纯黑(#stage / .card / …),其余任何用 var(--shadow-1) 的组件
@@ -1921,6 +1937,62 @@ body::after{content:"";position:fixed;inset:0;z-index:60;pointer-events:none;opa
1921
1937
  .qa-done .d{width:6px;height:6px;border-radius:50%;background:var(--jade)}
1922
1938
  .qa-done.err .d{background:var(--cinnabar-hi)}
1923
1939
  .qa-done.err{color:var(--cinnabar-hi)}
1940
+
1941
+ /* 错误显示卡(终态失败):朱砂描边 + 淡红底的卡片面(见 29-msg-variants 的 qaErrorCardNode)。
1942
+ ⚠️ 根节点带 `.qa-done` 类(收尾行契约要数它),这里把它从「等宽灰字行」翻成「块级卡片」:
1943
+ display:block 盖掉 `.qa-done` 的 flex 行布局;颜色交给子元素,根只留卡片面。 */
1944
+ .qa-errcard{display:block;margin:8px 0 4px;padding:10px 12px;
1945
+ border:1px solid rgba(214,64,42,.38);border-radius:var(--r-md);background:rgba(214,64,42,.06)}
1946
+
1947
+ .qa-errcard + .qa-errcard{margin-top:0}
1948
+
1949
+ .qa-ec-hd{display:flex;align-items:center;gap:8px}
1950
+
1951
+ .qa-ec-ico{width:16px;height:16px;flex:none;display:grid;place-items:center;color:var(--cinnabar-hi)}
1952
+
1953
+ .qa-ec-ico svg{width:15px;height:15px;display:block;stroke:currentColor;fill:none;
1954
+ stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}
1955
+
1956
+ .qa-ec-tt{flex:1;min-width:0;font-family:var(--font);font-size:13px;font-weight:600;color:var(--cinnabar-hi)}
1957
+
1958
+ .qa-ec-bd{margin-top:8px}
1959
+
1960
+ .qa-ec-reason{margin:0;font-size:12.5px;line-height:1.6;color:var(--paper-dim);
1961
+ white-space:pre-wrap;word-break:break-word}
1962
+
1963
+ .qa-ec-meta{margin-top:6px;font-family:var(--mono);font-size:10px;letter-spacing:.04em;color:var(--paper-mute)}
1964
+
1965
+ /* 卡片里的可展开详情(复用 .qa-mk 词汇);把它的左缩进让出来,贴着卡片内边距更顺 */
1966
+ .qa-errcard .qa-mk{margin:8px 0 0}
1967
+
1968
+ .qa-errcard .qa-mk-bd{padding-left:2px}
1969
+
1970
+ /* 额度 / 限流提示卡(2026-10-25,见 29-msg-variants 的 qaQuotaCardNode)----------
1971
+ ⚠️ 与 client-ui/shared/conv/conv.css 里同名那一段**必须逐字一致**:PC 工作台只引本文件、
1972
+ 不引 conv.css(两边各存一份是既有事实)。只改了那边不改这边 = PC 上这张卡没样式。 */
1973
+ .qa-errcard.qa-quota{
1974
+ border-color:color-mix(in srgb,var(--amber) 42%,transparent);
1975
+ border-left:3px solid color-mix(in srgb,var(--amber) 70%,transparent);
1976
+ padding-left:10px;
1977
+ background:color-mix(in srgb,var(--amber) 8%,transparent)}
1978
+
1979
+ .qa-quota .qa-ec-ico{color:var(--amber)}
1980
+
1981
+ .qa-quota .qa-ec-tt{color:color-mix(in srgb,var(--amber) 72%,var(--paper))}
1982
+
1983
+ .qa-qc-badge{flex:none;display:inline-flex;align-items:center;height:15px;padding:0 5px;
1984
+ border:1px solid color-mix(in srgb,var(--amber) 42%,transparent);border-radius:var(--r-xs);
1985
+ background:color-mix(in srgb,var(--amber) 12%,transparent);
1986
+ font-family:var(--mono);font-size:9px;letter-spacing:.06em;white-space:nowrap;color:var(--amber)}
1987
+
1988
+ .qa-qc-reset{margin:6px 0 0;font-size:12.5px;line-height:1.6;color:var(--paper-dim)}
1989
+
1990
+ .qa-qc-reset b{font-family:var(--mono);font-size:13px;font-weight:600;color:var(--amber)}
1991
+
1992
+ .qa-qc-hint{margin:4px 0 0;font-size:12px;line-height:1.55;color:var(--paper-dim)}
1993
+
1994
+ [data-theme="dark"] .qa-errcard.qa-quota{background:color-mix(in srgb,var(--amber) 14%,transparent)}
1995
+
1924
1996
  /* 待验证那条对话末尾的**出路按钮**(人工确认完成 / 按审核意见返工重做,见 qaReqConvSnapshot):
1925
1997
  与卡片脚注同一套颜色语言(玉色 = 就这么定了,朱砂 = 再跑一轮),但对话里宽度充足、用全称。
1926
1998
  **贴右**(justify-content:flex-end):它作用于上面那一列审查内容(结论 + 报告都在右侧),
@@ -3912,6 +3984,8 @@ body::after{content:"";position:fixed;inset:0;z-index:60;pointer-events:none;opa
3912
3984
  [data-theme="dark"] .qa-done .d{background:#34D399}
3913
3985
  [data-theme="dark"] .qa-done.err{color:#F87171}
3914
3986
  [data-theme="dark"] .qa-done.err .d{background:#F87171}
3987
+ [data-theme="dark"] .qa-errcard{background:rgba(248,113,113,.09);border-color:rgba(248,113,113,.42)}
3988
+ [data-theme="dark"] .qa-ec-reason{color:#E7E5E4}
3915
3989
 
3916
3990
  /* 委派卡 */
3917
3991
  [data-theme="dark"] .qa-agent-chip{color:#A8A29E;border-color:#3B3835;background:rgba(245,244,243,.05)}
@@ -129,10 +129,14 @@ function qaPermOf(id){ return QA_PERMS.find(x=>x.id === id) || QA_PERMS.find(x=>
129
129
  function qaModelName(id, engine){
130
130
  const key = String(id || "").replace(/^custom-local:/, "");
131
131
  if(!key) return "";
132
+ const eng = String(engine || "").trim().toLowerCase();
133
+ /* dsh 的模型 id 是 `route/model`:路由前缀只在 dsh 配置内部有区分意义,显示时只留
134
+ 后面的模型名(`-m` 与落库仍用完整 id,见 internal/agent/dsh.go::dshResolveRoute)。 */
135
+ const bare = (eng === "dsh") ? key.replace(/^[^/]+\//, "") : key;
132
136
  const hit = (e)=> (e && e.modelLabels && e.modelLabels[key]) || "";
133
137
  return hit(qaEngines.find(x=>x.engine === (engine || qaEngine)))
134
138
  || qaEngines.map(hit).find(Boolean)
135
- || key;
139
+ || bare;
136
140
  }
137
141
  /* ---------- 模型:链来自配置界面,选不选**两个实例都可以** ----------
138
142
  用户 2026-09-21 四次改口,**这一版是定稿**(前面几版都写在这里,免得下次又来回改):
@@ -304,7 +304,7 @@ if(askAttBusy(from)){ toast("截图还在保存,稍等一下再发"); return f
304
304
  taskTitle: (curTask && curTask.title) || "" });
305
305
  if(!r || !r.ok){
306
306
  qaRunEnd(h);
307
- qaRenderDone(false, "调用失败 · " + ((r && r.error) || "桥不可用"), qaRunHost(run));
307
+ qaRenderDone(false, "调用失败 · " + ((r && r.error) || "桥不可用"), qaRunHost(run), null, r);
308
308
  /* 失败原因也进消息台账(2026-09-22):持久层只存消息,若这一轮一个字都没吐、
309
309
  失败原因又不记,点开历史就是「只有我说了话、引擎没反应」—— 与需求派发那边
310
310
  把失败原因当一条回答渲染是同一条口径(见 qaReqConvSnapshot)。 */
@@ -316,9 +316,9 @@ if(askAttBusy(from)){ toast("截图还在保存,稍等一下再发"); return f
316
316
  }
317
317
  run.rid = r.rid;
318
318
  qaRuns.delete(0); qaRuns.set(r.rid, run); // 用真实 rid 换掉占位(end 事件按 rid 路由)
319
- }catch(e){
319
+ }catch(e){
320
320
  qaRunEnd(h);
321
- qaRenderDone(false, "调用失败 · " + String(e && e.message || e).slice(0, 80), qaRunHost(run));
321
+ qaRenderDone(false, "调用失败 · " + String(e && e.message || e).slice(0, 80), qaRunHost(run), null, e);
322
322
  run.msgText = "运行失败\n" + String((e && e.message) || e);
323
323
  qaSaveSnapshotFor(run.taskId, "error");
324
324
  qaRuns.delete(run.rid || 0);
@@ -54,7 +54,22 @@ function normalizeEngineRow(r){
54
54
  models: models,
55
55
  modelLabels: Object.assign(labels, r.modelLabels || {}),
56
56
  modelCredits: r.model_credits || r.modelCredits || {},
57
+ modelVendors: r.model_vendors || r.modelVendors || {},
58
+ /* 账号额度 / 每日签到 / 模型级套餐额度:必须在这里显式带过去,否则组件拿不到
59
+ (normalize 只透传白名单字段)。 */
60
+ creditLeft: r.credit_left || r.creditLeft || "",
61
+ creditSuffix: r.credit_suffix || r.creditSuffix || "",
62
+ modelQuota: r.model_quota || r.modelQuota || {},
63
+ checkinToday: (typeof r.checkin_today === "boolean") ? r.checkin_today : r.checkinToday,
64
+ checkinCredit: r.checkin_credit || r.checkinCredit || "",
65
+ /* 多账号引擎(codebuddy 系):没登录的槽位不显示 —— 「登录几个账号就显示几个」。 */
66
+ loggedIn: (typeof r.logged_in === "boolean") ? r.logged_in : r.loggedIn,
67
+ /* 登录的账号昵称(如 Darren / 玲),选择器画在引擎名下面。 */
68
+ account: r.account || "",
57
69
  modelsNote: r.models_note || r.modelsNote || "",
70
+ /* 模型来源语义(none / failed / dynamic,见 core --engines):选择器用它区分
71
+ 「引擎本身不提供模型」与「探测失败」—— 前者要保留(arkclaw / codebuddy-gateway)。 */
72
+ modelsSource: r.models_source || r.modelsSource || "",
58
73
  workspace: r.workspace || "",
59
74
  streaming: r.streaming,
60
75
  attachments: r.attachments || "",
@@ -68,6 +83,9 @@ function normalizeEngineRow(r){
68
83
  if (HOST_DESK) return; // 宿主已经给了 desk,别覆盖它
69
84
  const subs = [];
70
85
  let ridSeq = 0;
86
+ /* 服务端 ask id(= SSE open 帧里的 id)。审批回 control 必须用它:
87
+ 客户端自己的 rid 只是渲染编号,/desk/ask/{id}/control 认不出(2026-10-25)。 */
88
+ const askIds = new Map(); // rid → server ask id
71
89
  /* ⚠️ 事件**延后一个微任务**再推给订阅者 —— 这不是洁癖,是修一个真实的竞态:
72
90
  SSE 常把 `open` 与紧随其后的若干事件**放在同一个网络块**里回来(一次 write 合并成一段),
73
91
  transport 会在一个 forEach 里把它们连着派给 onEvent。若在此**同步**转发,
@@ -100,9 +118,9 @@ function normalizeEngineRow(r){
100
118
  const done = (r) => { if (!settled) { settled = true; resolve(r); } };
101
119
  try {
102
120
  transport.ask(payload, {
103
- onOpen() { done({ ok: true, rid: rid }); },
121
+ onOpen(id) { if(id) askIds.set(rid, String(id)); done({ ok: true, rid: rid }); },
104
122
  onEvent(ev) { emit(rid, ev); },
105
- onEnd(code) { emit(rid, { type: "end", code: code }); done({ ok: true, rid: rid }); },
123
+ onEnd(code) { askIds.delete(rid); emit(rid, { type: "end", code: code }); done({ ok: true, rid: rid }); },
106
124
  onError(e) {
107
125
  const msg = String((e && e.message) || e);
108
126
  emit(rid, { type: "error", reason: msg });
@@ -132,6 +150,19 @@ function normalizeEngineRow(r){
132
150
  .then(() => ({ ok: true }))
133
151
  .catch((e) => ({ ok: false, error: String((e && e.message) || e) }));
134
152
  },
153
+ /* 待授权(计划审批 / 工具授权)的裁决(2026-10-25):引擎正阻塞在 control_response 上,
154
+ 必须走 control 的 ask_answer 原地写回;当成一条新消息发进去它不会读。
155
+ id 要用**服务端 ask id**(open 帧带回的),客户端 rid 只是渲染编号。 */
156
+ askApprove(p) {
157
+ const q = p || {};
158
+ const id = (q.rid != null && askIds.get(q.rid)) || askIds.get(Number(q.rid)) || "";
159
+ if (!id) return Promise.resolve({ ok: false, error: "这一轮还没拿到轮次 id(open 事件未到)" });
160
+ return Promise.resolve(transport.control({
161
+ id: id, op: "ask_answer",
162
+ request_id: q.request_id || "", allowed: !!q.allowed,
163
+ message: q.message || "", tool_name: q.tool_name || "",
164
+ })).then(() => ({ ok: true }), (e) => ({ ok: false, error: String((e && e.message) || e) }));
165
+ },
135
166
  onAskEvent(cb) { subs.push(cb); return () => { const i = subs.indexOf(cb); if (i >= 0) subs.splice(i, 1); }; },
136
167
 
137
168
  /* 引擎清单:transport 给的是 CLI 原始行(可能带 snake_case)→ 归一成上游形状。 */
@@ -172,6 +203,13 @@ function normalizeEngineRow(r){
172
203
  },
173
204
  /* 任务本地库(SQLite):magic-agent 没有本地库 —— 空清单 + 写入如实回「跳过」。
174
205
  04-task-list.js 的 qaDb* 会把这些结果当「桥不可用」,于是只走 localStorage。 */
206
+ /* 历史附件回读(2026-10-25):历史会话里的参考图只有**绝对路径**(core 落盘 {path, mime},
207
+ 没有回读地址)—— 内核画历史缩略图要当场按路径换 URL(attHistUrl,19-attachments)。
208
+ fileUrl 按绝对路径回读本机媒体文件(/desk/media?path=…),上传目录内外的都通;
209
+ mediaUrl 只对上传目录内文件有效(/desk/uploads/<名字>)。与组件宿主的
210
+ installDeskProbe(shared/conversation.js)同一口径。 */
211
+ fileUrl(path) { return (transport && transport.fileUrl) ? transport.fileUrl(path) : ""; },
212
+ mediaUrl(path) { return (transport && transport.mediaUrl) ? transport.mediaUrl(path) : ""; },
175
213
  tasksList() { return Promise.resolve({ ok: true, tasks: [] }); },
176
214
  tasksCreate(t) { return Promise.resolve({ ok: true, task: t }); },
177
215
  tasksUpdate() { return Promise.resolve({ ok: true }); },
@@ -119,13 +119,27 @@ function qaMdlMenuBuild(){
119
119
  const e = qaEngines.find(x=>x.engine === qaEngine) || null;
120
120
  const credits = (e && e.modelCredits) || {};
121
121
  const crOf = (id)=>{ const v = String(credits[qaModelText(id)] || "").trim(); return v ? "x" + v : ""; };
122
- const ids = (e && Array.isArray(e.models) ? e.models : []).map(m=>String(m||"")).filter(Boolean);
122
+ const ids = qaModelsOrdered(qaEngines, qaEngine,
123
+ (e && Array.isArray(e.models) ? e.models : []).map(m=>String(m||"")).filter(Boolean));
124
+ /* dsh 的模型 id 是 `route/model`:选择器里补一层路由名(同名模型可能挂在多条路由下,
125
+ 只给末段分不出来)—— 与移动端 `.pick__cd` 同一口径(见 shared/conv/03-chat-message.js)。 */
126
+ const routeOf = (id)=> (String(qaEngine).trim().toLowerCase() === "dsh" && String(id).indexOf("/") > 0)
127
+ ? String(id).split("/")[0] : "";
123
128
  const autoName = auto ? qaModelName(auto, qaEngine) : "引擎默认";
124
129
  const autoCr = auto ? crOf(auto) : "";
125
- const opts = [{ id:"", name:"自动", note: autoCr ? autoName + " · " + autoCr : autoName }]
130
+ /* 第二层:dsh 是路由名,其余引擎是 core 给的供应商名(model_vendors);
131
+ 套餐额度(MiniMax token plan)与它们同一行(note 本来就是一行 · 拼接)。 */
132
+ const subOf = (id)=> routeOf(id) || qaModelVendor(qaEngines, qaEngine, id);
133
+ const quotaOf = (id)=> qaModelQuota(qaEngines, qaEngine, id);
134
+ const autoNote = [autoName, subOf(auto), quotaOf(auto), autoCr].filter(Boolean).join(" · ");
135
+ const opts = [{ id:"", name:"自动", note: autoNote }]
126
136
  .concat(ids.map(id=>{
127
137
  const bits = [];
128
138
  if(auto === id) bits.push("链首");
139
+ const sub = subOf(id);
140
+ if(sub) bits.push(sub);
141
+ const quota = quotaOf(id);
142
+ if(quota) bits.push(quota);
129
143
  const cr = crOf(id);
130
144
  if(cr) bits.push(cr);
131
145
  return { id, name: qaModelName(id, qaEngine), note: bits.join(" · ") };
@@ -354,8 +368,9 @@ function qaStreamBind(){
354
368
  },
355
369
  onSession: (sid)=>{ qaSessionBindFor(run.taskId, sid); },
356
370
  onError: (e)=>{
357
- /* 显示哪一句:**优先根因**(reason),退回完整错误链(error)——理由同上游 */
358
- qaRenderDone(false, "调用失败 · " + String(e.reason || e.error || "未知错误").slice(0, 80), qaRunHost(run));
371
+ /* 显示哪一句:**优先根因**(reason),退回完整错误链(error)——理由同上游。
372
+ 把整份错误信封(e)作为 detail 传给收尾卡(见 29-msg-variants 的 qaErrorCardNode)。 */
373
+ qaRenderDone(false, "调用失败 · " + String(e.reason || e.error || "未知错误").slice(0, 80), qaRunHost(run), null, e);
359
374
  qaTaskMarkSidLostFor(run.taskId);
360
375
  qaSaveSnapshotFor(run.taskId, "error", { holder: run.holder });
361
376
  qaRuns.delete(run.rid);
@@ -53,13 +53,15 @@ function qaRailState(s){
53
53
  * `waiting_approval` 必须算**进行中** —— 它没在跑,但也没结束;归到「已完成」的话,
54
54
  * 用户会看到「我还在等它回话,它却已完成」。
55
55
  * `idle`(常驻会话本轮已结束、进程还在等追加)**不算进行中**,落到「已完成」—— 与
56
- * PC 按事件流显示的口径一致(2026-10-21)。 */
56
+ * PC 按事件流显示的口径一致(2026-10-21)。
57
+ * `gone`(记录写着在跑、进程已不在)**不算失败**:服务重启 / 终端关闭把进程带走
58
+ * 就是这一档,与移动端同一口径(2026-10-24 用户:「会话全部断连 红色」)。 */
57
59
  function qaRailGroup(s){
58
60
  if(!s) return "done";
59
61
  const st = qaRailState(s);
60
62
  if(s.alive || st === "running") return "live";
61
63
  if(st === "waiting_approval") return "live";
62
- if(st === "failed" || st === "gone") return "failed";
64
+ if(st === "failed") return "failed";
63
65
  return "done";
64
66
  }
65
67
  function qaRailGroupLabel(s){
@@ -68,7 +70,7 @@ function qaRailGroupLabel(s){
68
70
  if(g === "live") return st === "waiting_approval" ? "等审批" : "进行中";
69
71
  if(g === "failed") return "失败";
70
72
  if(st === "cancelled" || st === "stopped") return "已取消";
71
- if(st === "interrupted") return "已打断";
73
+ if(st === "interrupted" || st === "gone") return "已打断";
72
74
  return "已完成";
73
75
  }
74
76
  /* 运行中的会话**自动临时置顶**(2026-10-22 用户:「运行中的会话自动临时置顶」):
@@ -260,7 +262,11 @@ function qaRailApply(ctx, events){
260
262
  ctx.h = null;
261
263
  /* 移动端「会话托管」自动发的提问带 hosted:true:PC 回放同一条历史时也画成
262
264
  「托管发送」,否则同一会话在两端会长成两张脸。 */
263
- qaRenderMine(String(ev.text || ""), [], { hosted: !!ev.hosted });
265
+ /* 历史附件(参考图)随 user 事件落盘({path, mime},见 internal/agent/attachments.go):
266
+ 透传下去由内核的 attHistUrl 按路径换回读地址 —— 硬编码 [] 的话点开历史会话
267
+ 参考图整排消失(2026-10-25 用户报障)。 */
268
+ qaRenderMine(String(ev.text || ""),
269
+ Array.isArray(ev.attachments) ? ev.attachments : [], { hosted: !!ev.hosted });
264
270
  continue;
265
271
  }
266
272
  if(!ctx.h){ ctx.h = qaRenderAnswer(); if(!ctx.h) break; }
@@ -443,7 +449,12 @@ async function qaRailOpen(id){
443
449
  }
444
450
  const ctx = qaRailReplay(r.events, s, live);
445
451
  if(live) qaRailTailStart(s, ctx, null);
446
- qaScrollIntoView();
452
+ /* 打开历史 = 从底部开始看(2026-10-25 用户:「历史会话也应该默认到最底部」)。
453
+ 换 qaHistoryStickBottom(不再是裸 qaScrollIntoView):回放那次贴底在缩略图 /
454
+ 字体加载完成之前,图一到位画布就被撑高、停在半空 —— 三重兜底见内核;
455
+ 它同时置 qaUserAway=false,切会话残留的「用户离开底部」也一并复位。 */
456
+ if(typeof qaHistoryStickBottom === "function") qaHistoryStickBottom();
457
+ else qaScrollIntoView();
447
458
  }
448
459
 
449
460
  /** 新建对话:把当前上下文清掉,下一句话就是**全新会话**(不带给引擎任何 --session)。 */
@@ -21,8 +21,8 @@ const QA_IMG_LINE = /^\[图片\]\s*(\S.*?)\s*$/; // 飞书侧:一行一
21
21
  清单漂移的后果是「同一条路径在 PC 有缩略图、在手机没有」,最难查)。 */
22
22
  const QA_IMG_EXT_SRC = "png|jpe?g|gif|webp|bmp|svg|heic|avif";
23
23
  const QA_IMG_EXT = new RegExp("\\.(?:" + QA_IMG_EXT_SRC + ")$", "i");
24
- const QA_ART_EXT = /\.(?:png|jpe?g|gif|webp|bmp|svg|heic|avif|mp4|mov|m4v|webm|mkv|avi|mp3|wav|m4a|aac|ogg|flac|md|markdown|txt|pdf|docx?|xlsx?|pptx?|csv|zip|glb|gltf|psd|fig)$/i;
25
- const QA_ART_EXT_SRC = "png|jpe?g|gif|webp|bmp|svg|heic|avif|mp4|mov|m4v|webm|mkv|avi|mp3|wav|m4a|aac|ogg|flac|md|markdown|txt|pdf|docx?|xlsx?|pptx?|csv|zip|glb|gltf|psd|fig";
24
+ const QA_ART_EXT = /\.(?:png|jpe?g|gif|webp|bmp|svg|heic|avif|mp4|mov|m4v|webm|mkv|avi|mp3|wav|m4a|aac|ogg|flac|md|markdown|txt|pdf|docx?|xlsx?|pptx?|csv|zip|glb|gltf|psd|fig|html?|xhtml)$/i;
25
+ const QA_ART_EXT_SRC = "png|jpe?g|gif|webp|bmp|svg|heic|avif|mp4|mov|m4v|webm|mkv|avi|mp3|wav|m4a|aac|ogg|flac|md|markdown|txt|pdf|docx?|xlsx?|pptx?|csv|zip|glb|gltf|psd|fig|html?|xhtml";
26
26
  /** 整行就是一个产物路径(可带 `- ` 列表前缀)→ 摘成卡。
27
27
  * 相对路径只在这一档认(必须带目录分隔符,光一个 `report.md` 太像正文了)。 */
28
28
  const QA_ART_PATH_SRC = "(?:file://|/|~/|[A-Za-z0-9_.@-]+/)"; // 绝对 / `~/` / `file://` / 相对(带目录)