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
@@ -112,6 +112,14 @@
112
112
  return p;
113
113
  },
114
114
 
115
+ /* 领取某引擎账号的每日积分(codebuddy 系):core --checkin <engine>。
116
+ 返回 {engine, ok, credit, streak_days, checked_today, message};
117
+ 业务失败(今天已领)也是 200,ok:false + message,界面直接显示原因。 */
118
+ claimCheckin: function (engine) {
119
+ return api('/desk/checkin', { json: { engine: engine } })
120
+ .then(function (r) { return r.json(); });
121
+ },
122
+
115
123
  /* 上传附件:返回 {path, name, size, mime, url}。path 是 core 所在机器的绝对路径,
116
124
  由 ask 的 payload.attachments 透传给 -a/--attach;url 是回读地址(缩略图用,
117
125
  这里补成绝对地址并带上令牌)。multipart 的 Content-Type 交给浏览器带 boundary,不要自己设。 */
@@ -309,6 +317,12 @@
309
317
  control: function (p) {
310
318
  var body = { op: p.op };
311
319
  if (p.text != null) body.text = p.text;
320
+ /* 原地回审批(2026-10-25):ask_answer 的这几项要原样送到 core 的 --control,
321
+ 少一个 request_id core 就只能回 control_error(服务端也不再裁剪字段)。 */
322
+ if (p.request_id != null) body.request_id = p.request_id;
323
+ if (p.allowed != null) body.allowed = !!p.allowed;
324
+ if (p.message != null) body.message = p.message;
325
+ if (p.tool_name != null) body.tool_name = p.tool_name;
312
326
  return api('/desk/ask/' + encodeURIComponent(p.id) + '/control', { json: body })
313
327
  .then(function () { return true; });
314
328
  },
@@ -0,0 +1,16 @@
1
+ /* ══════════ 应用页(「发现 → 应用」子页面)══════════
2
+ 从 index.html 拆出:选择器只服务这一页(别的页面不引用)。
3
+ 一级清单(应用 + 启停徽标)、二级详情(启动 / 停止 / 在内置浏览器打开)。
4
+ 清单行沿用 `.plugin*` 那一套(与「插件」「脚本」两页同形),这里只补本页特有的东西。
5
+ CSS 变量(--ink / --muted / --faint / --border / --subtle / --primary-action /
6
+ --running / --tint-run / --stop / --tint-stop / --error-text)仍由 index.html 的 :root 统一提供。
7
+ ⚠️ 本页**不定义任何颜色令牌** —— 颜色全走 var(),
8
+ 否则 tests/check-ui-spec.mjs 的金标比对会红。 */
9
+
10
+ /* 地址那一行:等宽字、一行省略(长路径不该把整行撑成两行)。 */
11
+ .app__url{font:12px/1.5 var(--mono);color:var(--ink-soft);white-space:nowrap;overflow:hidden;
12
+ text-overflow:ellipsis}
13
+ /* 主按钮一排三个时的次级动作:同样吃 .btn,只调宽度,保证小屏上也能并排放。 */
14
+ .app__acts{display:flex;flex-wrap:wrap;gap:8px;margin-top:14px}
15
+ .app__acts .btn{flex:1;min-width:96px;margin-top:0}
16
+ /* 进行中(启动 / 停止)时按钮上的文字由 JS 换,禁用态由 .btn[disabled] 管。 */
@@ -0,0 +1,406 @@
1
+ /* 应用页(移动端「发现 → 应用」)—— 独立子页面模块。
2
+ *
3
+ * 为什么单独一个文件:跟脚本页(scripts.js)、需求页(reqs.js)同一条纪律 ——
4
+ * 这一页的 DOM、渲染、启停、状态轮询全在一处,不往 index.html 里塞。
5
+ *
6
+ * 这一页回答的是「magic-shorts / magic-video 的工作台,现在跑着没有、能不能打开」。
7
+ * 工作台跑在**这台桌面**上(不是手机上):服务端负责起进程 / 杀进程组,
8
+ * 手机只发起,然后在**内置浏览器**里打开(走桌面端的网络,见 apps.go 文件头)。
9
+ *
10
+ * ⚠️ 不假装成功:状态一律以服务端探活的结果为准(state.running / state.managed),
11
+ * 本页不根据「我点过启动」自己推断「在跑」。
12
+ *
13
+ * 与宿主(index.html)的边界:只通过 mount(host) 注入的能力交互 ——
14
+ * getBase / getToken(切环境后是活的,必须 getter,不能传快照)、
15
+ * hEl / elSpan / svgUse / toast / openSheet / closeSheet /
16
+ * showRoot(回清单)/ showDetail(进详情)/ backToDiscover / openInBrowser。
17
+ *
18
+ * 数据形状见 CLIENT-CONTRACT §2(/desk/apps):
19
+ * GET /desk/apps → {apps:[{id,name,tagline,description?,homepage?,cli,command,port,
20
+ * installed,sourcePath?,note?,openUrl?,
21
+ * state:{state,running,managed,pid?,port?,url,localUrl?,
22
+ * logTail?,startedAt?,error?}}], notes:[…]}
23
+ * POST /desk/apps/{id}/{start|stop} → SSE
24
+ * event: open {id,action}
25
+ * event: end {ok,code,error?,app}
26
+ */
27
+ (function () {
28
+ 'use strict';
29
+
30
+ var H = null; // 宿主(index.html 注入)
31
+ var els = {}; // 本页内部元素
32
+ var ITEMS = []; // 最近一次清单
33
+ var DETAIL = null; // 二级视图:{id}
34
+ var BUSY = {}; // id → 正在做的动作(start / stop),防重复点
35
+ var LAST_LOG = {}; // id → 上次失败的输出尾巴(收尾重画后仍看得到)
36
+ var POLL = null; // 详情页的轮询定时器
37
+
38
+ /* 状态徽标:与脚本页同一套语义(运行中=绿、已停止=石褐)。
39
+ 「运行中但不可托管」(桌面端起的)另给一个主色徽标:能打开、但停不了。 */
40
+ function stateText(st) {
41
+ var s = (st && st.state) || 'stopped';
42
+ if (s === 'running') return st && st.managed ? '运行中' : '运行中 · 外部';
43
+ if (s === 'starting') return '启动中';
44
+ return '已停止';
45
+ }
46
+ function stateBadge(st) {
47
+ var s = (st && st.state) || 'stopped';
48
+ if (s === 'running') return st && st.managed ? 'pbadge--run' : 'pbadge--new';
49
+ if (s === 'starting') return 'pbadge--new';
50
+ return 'pbadge--stop';
51
+ }
52
+
53
+ function hEl(tag, cls, text) { return H.hEl(tag, cls, text); }
54
+ function elSpan(cls, text) { return H.elSpan(cls, text); }
55
+ function svgUse(id, size) { return H.svgUse(id, size); }
56
+ function toast(msg) { return H.toast(msg); }
57
+
58
+ function api(path) { return H.getBase() + path; }
59
+ function headers() {
60
+ return H.getToken() ? { 'X-Magic-Token': H.getToken() } : {};
61
+ }
62
+
63
+ function byId(id) {
64
+ for (var i = 0; i < ITEMS.length; i++) if (ITEMS[i].id === id) return ITEMS[i];
65
+ return null;
66
+ }
67
+
68
+ /* ── 一级:应用清单 ── */
69
+ function renderList() {
70
+ if (!els.list) return;
71
+ var frag = document.createDocumentFragment();
72
+ if (!ITEMS.length) {
73
+ els.list.replaceChildren(hEl('div', 'history__hint',
74
+ '桌面端没有返回任何应用。'));
75
+ return;
76
+ }
77
+ frag.appendChild(hEl('div', 'history__group', '工作台'));
78
+ ITEMS.forEach(function (it) {
79
+ var st = it.state || {};
80
+ var row = hEl('button', 'plugin'); row.type = 'button';
81
+ var ico = hEl('span', 'plugin__ico' + (st.running ? ' is-on' : ''));
82
+ ico.appendChild(svgUse('#i-grid', 22));
83
+ var col = hEl('span', 'plugin__col');
84
+ col.appendChild(hEl('span', 'plugin__t', it.name));
85
+ col.appendChild(hEl('span', 'plugin__s', it.tagline || ''));
86
+ row.appendChild(ico); row.appendChild(col);
87
+ row.appendChild(elSpan('pbadge ' + stateBadge(st),
88
+ BUSY[it.id] ? (BUSY[it.id] === 'start' ? '启动中…' : '停止中…') : stateText(st)));
89
+ row.onclick = function () { openDetail(it.id); };
90
+ frag.appendChild(row);
91
+ });
92
+ frag.appendChild(hEl('div', 'pd__note',
93
+ '工作台在「这台桌面」上跑(不是手机上);打开时用内置浏览器,走桌面端的网络。'));
94
+ els.list.replaceChildren(frag);
95
+ }
96
+
97
+ function loadList(quiet) {
98
+ if (!els.list) return;
99
+ if (!quiet) els.list.innerHTML = '<div class="history__hint">读取中…</div>';
100
+ return fetch(api('/desk/apps'), { headers: headers() })
101
+ .then(function (r) {
102
+ if (r.status === 404) throw new Error('当前桌面端没有应用能力(需要较新版本)');
103
+ if (!r.ok) throw new Error('HTTP ' + r.status);
104
+ return r.json();
105
+ })
106
+ .then(function (j) {
107
+ ITEMS = (j && j.apps) || [];
108
+ renderList();
109
+ /* 详情页开着时,顺手刷一次(从启动状态变成运行中要靠它)。 */
110
+ if (DETAIL) renderDetail(true);
111
+ })
112
+ .catch(function (e) {
113
+ if (quiet && ITEMS.length) return;
114
+ els.list.innerHTML = '';
115
+ els.list.appendChild(hEl('div', 'history__hint',
116
+ '读取应用列表失败:' + ((e && e.message) || e)));
117
+ });
118
+ }
119
+
120
+ /* ── 二级:详情(启动 / 停止 / 在内置浏览器打开)── */
121
+ function openDetail(id) {
122
+ DETAIL = { id: id };
123
+ H.showDetail();
124
+ renderDetail(false);
125
+ loadList(true);
126
+ startPoll();
127
+ }
128
+
129
+ function backToList() {
130
+ stopPoll();
131
+ DETAIL = null;
132
+ H.showRoot();
133
+ loadList(true);
134
+ }
135
+
136
+ /* 启动中 / 运行中的时候轮询一次清单:启动是服务端等端口应答,手机这边
137
+ 只需要看它什么时候变过来(以及外部实例是否自己退了)。 */
138
+ function startPoll() {
139
+ stopPoll();
140
+ POLL = setInterval(function () { if (DETAIL) loadList(true); }, 2500);
141
+ }
142
+ function stopPoll() {
143
+ if (POLL) { clearInterval(POLL); POLL = null; }
144
+ }
145
+
146
+ function renderDetail(keepScroll) {
147
+ if (!els.detailBody || !DETAIL) return;
148
+ var it = byId(DETAIL.id);
149
+ var box = els.detailBody;
150
+ var logBox = box.querySelector('#appLog');
151
+ var keepLog = keepScroll && logBox ? logBox.scrollTop : 0;
152
+ box.innerHTML = '';
153
+ if (!it) {
154
+ box.appendChild(hEl('div', 'history__hint', '这个应用不在清单里了(可能桌面端换了环境)。'));
155
+ return;
156
+ }
157
+ var st = it.state || {};
158
+ var wrap = hEl('div', 'pd');
159
+
160
+ var head = hEl('div', 'pd__head');
161
+ head.appendChild(hEl('span', 'pd__name', it.name));
162
+ head.appendChild(elSpan('pbadge ' + stateBadge(st), stateText(st)));
163
+ wrap.appendChild(head);
164
+ wrap.appendChild(hEl('p', 'pd__tag', it.tagline || ''));
165
+
166
+ /* 状态说明:服务端说的原话(为什么是这个状态),本页不自己编。 */
167
+ if (it.note) wrap.appendChild(hEl('div', 'pd__note', it.note));
168
+ if (st.running && st.localUrl) {
169
+ wrap.appendChild(hEl('div', 'pd__note', '桌面端地址:' + st.localUrl));
170
+ }
171
+ if (st.running && st.managed && st.pid) {
172
+ wrap.appendChild(hEl('div', 'pd__note', '进程 PID ' + st.pid + '(随工作台起的子进程一起走)'));
173
+ }
174
+
175
+ /* 动作:运行中主角是「在内置浏览器打开」,没跑时主角是「启动」。 */
176
+ var acts = hEl('div', 'app__acts');
177
+ var busy = BUSY[it.id];
178
+ if (st.running) {
179
+ var openBtn = hEl('button', 'btn btn--filled', '在内置浏览器打开');
180
+ openBtn.type = 'button';
181
+ openBtn.onclick = function () { openApp(it); };
182
+ acts.appendChild(openBtn);
183
+
184
+ var stopBtn = hEl('button', 'btn btn--outlined',
185
+ busy === 'stop' ? '正在停止…' : (st.managed ? '停止' : '停止(到桌面端)'));
186
+ stopBtn.type = 'button';
187
+ stopBtn.disabled = !!busy || !st.managed;
188
+ if (!st.managed) stopBtn.title = '这个工作台不是从手机启动的,请到桌面端停止';
189
+ stopBtn.onclick = function () { if (st.managed) runAction(it, 'stop'); };
190
+ acts.appendChild(stopBtn);
191
+ } else {
192
+ var startBtn = hEl('button', 'btn btn--filled',
193
+ busy === 'start' ? '正在启动…' : (st.state === 'starting' ? '启动中…' : '启动'));
194
+ startBtn.type = 'button';
195
+ startBtn.disabled = !!busy || st.state === 'starting';
196
+ startBtn.onclick = function () { runAction(it, 'start'); };
197
+ acts.appendChild(startBtn);
198
+ }
199
+ wrap.appendChild(acts);
200
+
201
+ /* 输出尾巴:起不来时原因就在这几行里(stderr 原样),不让人去桌面翻日志。 */
202
+ var logLines = (LAST_LOG[it.id] && LAST_LOG[it.id].length) ? LAST_LOG[it.id] : (st.logTail || []);
203
+ if (logLines.length) {
204
+ var logSec = hEl('div', 'pd__sec');
205
+ logSec.appendChild(hEl('div', 'pd__h', '最近输出'));
206
+ var pre = hEl('pre', 'pd__log pd__log--script', logLines.join('\n'));
207
+ pre.id = 'appLog';
208
+ pre.hidden = false;
209
+ pre.scrollTop = keepLog;
210
+ logSec.appendChild(pre);
211
+ wrap.appendChild(logSec);
212
+ }
213
+
214
+ /* 介绍 / 怎么起 / 主页。 */
215
+ wrap.appendChild(pdSection('介绍', hEl('p', 'pd__p', it.description || '')));
216
+
217
+ var cmdSec = hEl('div', 'pd__sec');
218
+ cmdSec.appendChild(hEl('div', 'pd__h', '等价命令(在桌面端执行)'));
219
+ cmdSec.appendChild(hEl('pre', 'pd__code', it.command || ''));
220
+ var copyBtn = hEl('button', 'btn btn--outlined', '复制命令');
221
+ copyBtn.type = 'button';
222
+ copyBtn.onclick = function () { copyText(it.command || '', '命令已复制'); };
223
+ cmdSec.appendChild(copyBtn);
224
+ wrap.appendChild(cmdSec);
225
+
226
+ if (!it.installed) {
227
+ var miss = hEl('div', 'pd__note',
228
+ 'PATH 上没有 ' + it.cli + '。装法:' + (it.install || '见项目主页') +
229
+ '(「发现 → 插件」那一页可以直接点安装)。');
230
+ missingStyle(miss);
231
+ wrap.appendChild(miss);
232
+ }
233
+
234
+ if (it.homepage) {
235
+ var home = hEl('button', 'setrow');
236
+ home.appendChild(hEl('span', 'setrow__l', '项目主页'));
237
+ home.appendChild(hEl('span', 'setrow__v', it.homepage));
238
+ home.onclick = function () { H.openInBrowser(it.homepage); };
239
+ var card = hEl('div', 'setcard'); card.appendChild(home);
240
+ wrap.appendChild(card);
241
+ }
242
+
243
+ box.appendChild(wrap);
244
+ if (els.detailTitle) els.detailTitle.textContent = it.name;
245
+ }
246
+
247
+ function missingStyle(node) { node.style.color = 'var(--error-text)'; }
248
+
249
+ function pdSection(title, node) {
250
+ var sec = hEl('div', 'pd__sec');
251
+ sec.appendChild(hEl('div', 'pd__h', title));
252
+ sec.appendChild(node);
253
+ return sec;
254
+ }
255
+
256
+ /* 打开工作台:交给宿主的内置浏览器。
257
+ openUrl 是**相对地址**(/workbench/{id}/):先到入口探活 + 跳转,再由服务端
258
+ 把根路径请求转给工作台(页面用 '/api/…',必须在根下跑)。 */
259
+ function openApp(it) {
260
+ var u = (it && it.openUrl) || ((it && it.state && it.state.url) || '');
261
+ if (!u) { toast('这个应用没有给出地址'); return; }
262
+ if (!H.openInBrowser) { toast('当前界面不支持内置浏览器'); return; }
263
+ /* landscape:工作台自己声明「按桌面宽屏排版」(magic-video 是 true)。
264
+ 宿主据此在竖屏下先转横屏 —— 让浏览器去量页面宽度是量不出来的(它是响应式的,
265
+ 只溢出十几个像素,恰好切掉顶栏那排 tab)。 */
266
+ H.openInBrowser(u, { id: it.id, landscape: !!it.landscape });
267
+ }
268
+
269
+ /* ── 启停(SSE)──
270
+ 手解 SSE(跟脚本页、插件页同一套写法):fetch + ReadableStream,
271
+ 因为要带自定义头 X-Magic-Token,EventSource 带不了。 */
272
+ function runAction(it, action) {
273
+ if (BUSY[it.id]) return;
274
+ BUSY[it.id] = action;
275
+ delete LAST_LOG[it.id];
276
+ renderDetail(false);
277
+ renderList();
278
+
279
+ var ctrl = (typeof AbortController !== 'undefined') ? new AbortController() : null;
280
+ var opts = { method: 'POST', headers: headers() };
281
+ if (ctrl) opts.signal = ctrl.signal;
282
+ /* 等端口应答最多 20 秒(服务端侧),这里给足,避免长轮询被当成失败。 */
283
+ var timer = setTimeout(function () { if (ctrl) ctrl.abort(); }, 45000);
284
+
285
+ function finish(errText) {
286
+ clearTimeout(timer);
287
+ delete BUSY[it.id];
288
+ if (errText) {
289
+ toast((action === 'start' ? '启动失败:' : '停止失败:') + errText);
290
+ } else {
291
+ toast(action === 'start' ? '已启动' : '已停止');
292
+ }
293
+ loadList(true);
294
+ if (DETAIL) renderDetail(true);
295
+ }
296
+
297
+ fetch(api('/desk/apps/' + encodeURIComponent(it.id) + '/' + action), opts)
298
+ .then(function (resp) {
299
+ if (!resp.ok || !resp.body) {
300
+ return resp.text().then(function (t) {
301
+ throw new Error((t || ('HTTP ' + resp.status)).slice(0, 300));
302
+ });
303
+ }
304
+ var reader = resp.body.getReader(), dec = new TextDecoder(), buf = '';
305
+ var ended = false;
306
+ return (function pump() {
307
+ return reader.read().then(function (chunk) {
308
+ if (chunk.done) return null;
309
+ buf += dec.decode(chunk.value, { stream: true });
310
+ var blocks = buf.split('\n\n');
311
+ buf = blocks.pop();
312
+ blocks.forEach(function (b) { if (!ended) ended = handleBlock(it.id, b, finish); });
313
+ return ended ? null : pump();
314
+ });
315
+ })();
316
+ })
317
+ .then(function () { if (!ended) finish(''); })
318
+ .catch(function (e) {
319
+ if (e && e.name === 'AbortError') {
320
+ /* 超时多半是工作台在起(20 秒 + 预热);不报失败 —— 让轮询去拿真实状态。 */
321
+ finish('');
322
+ loadList(true);
323
+ return;
324
+ }
325
+ finish((e && e.message) || String(e));
326
+ });
327
+ }
328
+
329
+ /* 解析一个 SSE 块;收到 end 帧 → 收尾并返回 true(调用方停止继续读)。 */
330
+ function handleBlock(id, block, finish) {
331
+ var name = '', data = '';
332
+ block.split('\n').forEach(function (ln) {
333
+ if (ln.indexOf('event: ') === 0) name = ln.slice(7).trim();
334
+ else if (ln.indexOf('data: ') === 0) data += ln.slice(6);
335
+ });
336
+ if (!data) return false;
337
+ var ev; try { ev = JSON.parse(data); } catch (e) { return false; }
338
+ if (name !== 'end') return false;
339
+ /* end 帧带完整的最新状态:先落到 ITEMS 里,再收尾重画。 */
340
+ if (ev && ev.app && ev.app.id) {
341
+ var replaced = false;
342
+ for (var i = 0; i < ITEMS.length; i++) {
343
+ if (ITEMS[i].id === ev.app.id) { ITEMS[i] = ev.app; replaced = true; break; }
344
+ }
345
+ if (!replaced) ITEMS.push(ev.app);
346
+ var tail = (ev.app.state && ev.app.state.logTail) || [];
347
+ if (tail.length) LAST_LOG[ev.app.id] = tail;
348
+ }
349
+ if (ev && ev.ok) { finish(''); return true; }
350
+ var msg = (ev && ev.error) ? String(ev.error) : '服务端没有说明原因';
351
+ /* 409(已在启动 / 外部实例)不是故障,原样提示即可。 */
352
+ finish(msg);
353
+ return true;
354
+ }
355
+
356
+ function copyText(text, okMsg) {
357
+ try {
358
+ if (navigator.clipboard && navigator.clipboard.writeText) {
359
+ navigator.clipboard.writeText(text).then(function () { toast(okMsg); },
360
+ function () { toast('复制失败'); });
361
+ return;
362
+ }
363
+ } catch (e) {}
364
+ toast('当前环境不支持复制');
365
+ }
366
+
367
+ /* ── 挂载 ── */
368
+ function mount(host) {
369
+ H = host;
370
+ var root = H.rootEl, detail = H.detailEl;
371
+ if (!root || !detail) return;
372
+ root.innerHTML =
373
+ '<div class="history__top">' +
374
+ '<button class="topbar__icon" id="btnAppsBack" aria-label="返回"><svg width="20" height="20"><use href="#i-back"/></svg></button>' +
375
+ '<span class="history__title">应用</span>' +
376
+ '</div>' +
377
+ '<div class="history__list" id="appList"></div>';
378
+ detail.innerHTML =
379
+ '<div class="history__top">' +
380
+ '<button class="topbar__icon" id="btnAppDetailBack" aria-label="返回"><svg width="20" height="20"><use href="#i-back"/></svg></button>' +
381
+ '<span class="history__title" id="appDetailTitle">应用</span>' +
382
+ '</div>' +
383
+ '<div class="history__list" id="appDetailBody"></div>';
384
+ els.list = root.querySelector('#appList');
385
+ els.detailBody = detail.querySelector('#appDetailBody');
386
+ els.detailTitle = detail.querySelector('#appDetailTitle');
387
+ var back = root.querySelector('#btnAppsBack');
388
+ if (back) back.onclick = function () { H.backToDiscover(); };
389
+ var dback = detail.querySelector('#btnAppDetailBack');
390
+ if (dback) dback.onclick = function () { backToList(); };
391
+ }
392
+
393
+ /* 进入「发现 → 应用」:拉一次清单(只读,不启动任何东西)。 */
394
+ function show() {
395
+ DETAIL = null;
396
+ stopPoll();
397
+ loadList(false);
398
+ }
399
+
400
+ window.MagicApps = {
401
+ mount: mount,
402
+ show: show,
403
+ /* 给测试与调试用:当前清单快照(只读)。 */
404
+ items: function () { return ITEMS.slice(); }
405
+ };
406
+ })();