dsh-mobile-plugin 0.4.0 → 0.4.4

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 (111) hide show
  1. package/README.md +71 -74
  2. package/dsh-mobile-credentials/lib/credentials.js +47 -24
  3. package/host.js +22 -36
  4. package/lib/client.js +29 -82
  5. package/lib/desktop-view.cjs +5 -10
  6. package/lib/git/read.js +6 -8
  7. package/lib/host-compat.cjs +13 -25
  8. package/lib/host-guard.cjs +9 -39
  9. package/lib/host-patches.cjs +5 -16
  10. package/lib/http/routing.js +7 -27
  11. package/lib/impl.js +6 -12
  12. package/lib/index.js +1 -1
  13. package/lib/mirror/pusher.cjs +33 -85
  14. package/lib/mirror-snapshot.cjs +6 -24
  15. package/lib/mobile/agent-presets.js +6 -26
  16. package/lib/mobile/allowed-zone.js +100 -0
  17. package/lib/mobile/approvals.js +18 -44
  18. package/lib/mobile/archive-clean.js +13 -21
  19. package/lib/mobile/attachments.js +16 -37
  20. package/lib/mobile/channels.js +15 -41
  21. package/lib/mobile/credential-upgrade.js +7 -16
  22. package/lib/mobile/credentials.js +5 -21
  23. package/lib/mobile/directory-browse.js +8 -27
  24. package/lib/mobile/directory-listing.js +10 -23
  25. package/lib/mobile/drive-roots.js +59 -0
  26. package/lib/mobile/envelope.js +4 -15
  27. package/lib/mobile/events.js +35 -65
  28. package/lib/mobile/extensions.js +5 -8
  29. package/lib/mobile/file-read.js +168 -0
  30. package/lib/mobile/git-repos.js +6 -15
  31. package/lib/mobile/git.js +11 -19
  32. package/lib/mobile/history-derive.js +85 -134
  33. package/lib/mobile/host-bridge/endpoints.js +4 -6
  34. package/lib/mobile/host-bridge/event-synthesis.js +21 -35
  35. package/lib/mobile/host-bridge/shims.js +12 -22
  36. package/lib/mobile/host-bridge.js +8 -20
  37. package/lib/mobile/host-settings.js +9 -19
  38. package/lib/mobile/inbox-clean.js +7 -24
  39. package/lib/mobile/local-images.js +48 -23
  40. package/lib/mobile/model-vendors.js +105 -0
  41. package/lib/mobile/monitor.js +8 -17
  42. package/lib/mobile/pairing.js +7 -19
  43. package/lib/mobile/plugin-market.js +475 -0
  44. package/lib/mobile/plugin-self.js +6 -23
  45. package/lib/mobile/plugin-update.js +8 -27
  46. package/lib/mobile/plugins.js +6 -24
  47. package/lib/mobile/questions.js +15 -40
  48. package/lib/mobile/references.js +11 -31
  49. package/lib/mobile/relay-control.js +136 -33
  50. package/lib/mobile/rpc.js +5 -13
  51. package/lib/mobile/schedule.js +8 -33
  52. package/lib/mobile/self-check.cjs +9 -36
  53. package/lib/mobile/session-cache.js +13 -19
  54. package/lib/mobile/session-cwd.js +28 -0
  55. package/lib/mobile/session-file.js +9 -33
  56. package/lib/mobile/session-flags.js +7 -13
  57. package/lib/mobile/session-mirror.js +8 -15
  58. package/lib/mobile/session-models.js +3 -3
  59. package/lib/mobile/session-schedule-mark.js +8 -14
  60. package/lib/mobile/session-send.js +13 -21
  61. package/lib/mobile/session-snapshot.js +4 -6
  62. package/lib/mobile/sessions.js +7 -23
  63. package/lib/mobile/stats-collect.cjs +154 -37
  64. package/lib/mobile/stats-ledger.cjs +258 -75
  65. package/lib/mobile/stats-scheduler.cjs +41 -0
  66. package/lib/mobile/stream.js +39 -130
  67. package/lib/mobile/subagents.js +5 -18
  68. package/lib/mobile/uploads.js +75 -21
  69. package/lib/mobile/vendor-logos.cjs +30 -0
  70. package/lib/mobile/wire-cut.js +212 -0
  71. package/lib/mobile/workspaces.js +20 -8
  72. package/lib/panel/index.html +7 -7
  73. package/lib/read-receipt.cjs +10 -21
  74. package/lib/relay/apply-ledger.cjs +52 -0
  75. package/lib/relay/core.js +14 -3
  76. package/lib/relay/lan-network.cjs +14 -28
  77. package/lib/relay/lan-proxy.js +14 -21
  78. package/lib/relay/migrate.js +90 -0
  79. package/lib/relay/pairing-policy.js +6 -9
  80. package/lib/relay/preview.js +8 -25
  81. package/lib/relay/protocol.js +9 -12
  82. package/lib/relay/status.js +5 -8
  83. package/lib/relay/tunnel.js +9 -14
  84. package/lib/relay/verify.js +52 -0
  85. package/lib/routes/control-plane.js +39 -47
  86. package/lib/routes/mobile-api.js +47 -34
  87. package/lib/session-cleanup.cjs +26 -27
  88. package/lib/shared/credentials-entry.js +4 -11
  89. package/lib/shared/device-headers.js +12 -4
  90. package/lib/shared/dshtoken.js +4 -4
  91. package/lib/shared/e2ee-request.js +1 -1
  92. package/lib/shared/e2ee.js +8 -14
  93. package/lib/shared/fresh-request.js +6 -22
  94. package/lib/shared/http-util.js +3 -4
  95. package/lib/shared/list-revision.js +5 -14
  96. package/lib/shared/session-signal.js +7 -16
  97. package/lib/shared/sse-write.js +14 -20
  98. package/lib/shared/workspace-guard.js +7 -15
  99. package/lib/shell/device-ota.js +4 -6
  100. package/lib/shell/manager.js +12 -27
  101. package/lib/shell/ota-package.js +7 -12
  102. package/lib/shell/runtime.js +8 -16
  103. package/lib/shell/self-check-after-swap.js +9 -23
  104. package/lib/shell/updater.js +15 -28
  105. package/lib/sidecar-facade-worker.cjs +75 -41
  106. package/lib/sidecar-facade.cjs +18 -8
  107. package/lib/sidecar-lifecycle.cjs +21 -10
  108. package/lib/update-install.cjs +24 -62
  109. package/lib/update-registry.cjs +6 -18
  110. package/package.json +1 -1
  111. package/lib/auto-archive.cjs +0 -98
@@ -3,32 +3,21 @@
3
3
  /**
4
4
  * dsh-mobile 流式:SSE 转发目标会话的增量事件。
5
5
  *
6
- * 订阅宿主事件流,仅转发目标会话的 session/event 帧(增量事件),
7
- * 每 15s 发一行注释 + 一帧 `session/heartbeat` 数据帧;客户端断开即取消订阅。
6
+ * 订阅宿主事件流,只转发目标会话的 session/event 帧,每 15s 发一行注释加一帧
7
+ * `session/heartbeat`;客户端断开即取消订阅。心跳那帧不能省:注释行只是保活,客户端一个
8
+ * 事件都收不到,而「流被正常收尾」在 App 那边看起来和「还开着」一样,它需要一个自己数得到
9
+ * 的活口。心跳不带 seq、不进消息模型、不写缓存。
8
10
  *
9
- * 为什么要那帧心跳:注释行只是保活,客户端一个事件都收不到。而「流被正常收尾」
10
- * (中转重启、隧道掉了、宿主自己结束响应)在 App 那边看起来和「这条流还开着」一模一样
11
- * —— react-native-sse 在 readyState 走到 DONE 时既不派发 error 也不派发 close。所以
12
- * App 需要一个**自己数得到**的活口,两拍没到就主动断开重连(见 app 侧
13
- * services/viewmodels/stream-liveness.ts)。这帧不带 seq、不进消息模型、不写缓存。
11
+ * 断线恢复:客户端重连带 `Last-Event-ID: <sessionId>:<seq>`(或 `lastSeq`),服务端先回放
12
+ * `seq > lastSeq` 的事件再接入实时流,并用 seq 去重。回放受窗口与字节数上限约束,超出的
13
+ * 缺口让客户端去拉权威快照。
14
14
  *
15
- * 断线恢复:SSE 客户端重连时携带 `Last-Event-ID: <sessionId>:<seq>`(或查询参数
16
- * `lastSeq=<seq>`)。服务端先在 history 中回放 `seq > lastSeq` 的事件(收敛状态),
17
- * 再无缝接入实时流,并用 seq 去重避免回放/实时交界处重复交付。由于宿主 history
18
- * 只支持窗口回放,若断线窗口超过回放上限,客户端仍应在其 SSRD 重连补偿刷新
19
- * 权威快照(/session/messages、/workspaces)兜底。
15
+ * 生命周期:所有失败路径都必须经 logStream 留痕(守卫逐条钉着字面量),收尾一定给客户端一帧
16
+ * `session/resync-required`;keepAlive 定时器要 unref。
20
17
  *
21
- * 生命周期:所有失败路径都必须经 logStream 留痕(守卫 test/stream-liveness.test.cjs
22
- * 逐条钉着字面量),收尾一定给客户端一帧 `session/resync-required`;keepAlive 定时器
23
- * 要 unref —— 它只是保活,不该把进程退出按在 15s 一拍上。
24
- *
25
- * 出流量的三个口子(2026-10-25,都在这一层收):
26
- * - 草稿增量按 200ms 合并成一帧(`DRAFT_FLUSH_MS`):一帧固定成本两三百字节,而一个
27
- * token 只有十几字节,逐帧发等于按帧收费;
28
- * - 草稿的填充窗口 250ms(`DRAFT_PAD_INTERVAL_MS`):填充是按时间补的,120ms 就是
29
- * 16KB/s 的底线,与正文多少无关;
30
- * - 推理只推每一步的开头 600 字、工具结果只推前 2000 字:运行中用户不读它们,完整内容
31
- * 仍在 `assistant/message` 与 `/session/messages` 里,一个字都不丢。
18
+ * 出流量收在两处:草稿增量按 200ms 合并成一帧(一帧固定成本两三百字节,token 才十几字节),
19
+ * 草稿填充窗口 250ms。过程明细(推理、工具参数、工具结果正文与 diff)一律不下发手机,直播间
20
+ * 与历史两条路走同一份实现,见 `wire-cut.js`。
32
21
  */
33
22
 
34
23
  const { sendJson } = require('../shared/http-util');
@@ -46,6 +35,7 @@ const { createTextImageBudget } = require('./local-images.js');
46
35
  const { callSessions } = require('./rpc.js');
47
36
  const { unwrap } = require('./envelope.js');
48
37
  const { normalizeJobs } = require('./jobs.js');
38
+ const { cutEvent, createToolNameIndex } = require('./wire-cut.js');
49
39
  const approvals = require('./approvals.js');
50
40
  const questions = require('./questions.js');
51
41
  const streamRegistry = require('./stream-registry.js');
@@ -99,7 +89,13 @@ const REPLAY_MAX_GAP = 200;
99
89
  */
100
90
  const DRAFT_FLUSH_MS = 200;
101
91
  const DRAFT_FLUSH_BYTES = 1024;
102
- /** 可合并的增量类型:文本与工具参数都是「按块序号累积」的拼接语义。 */
92
+ /**
93
+ * 可合并的增量类型。
94
+ *
95
+ * 文本按块序号拼接;工具参数增量也在这里并,但它的参数正文不下发(见 wire-cut.js),并帧
96
+ * 只为把「有工具开始了」那一串帧按规定窗口压成一条。别因为参数是空的就把这类帧整条丢掉:
97
+ * App 靠它建流式工具块、靠 `id` 与 tool/result 的 callId 配对。
98
+ */
103
99
  const MERGEABLE_CHUNK_TYPES = new Set(['text-delta', 'tool-call-delta']);
104
100
 
105
101
  /**
@@ -112,24 +108,6 @@ const MERGEABLE_CHUNK_TYPES = new Set(['text-delta', 'tool-call-delta']);
112
108
  */
113
109
  const DRAFT_PAD_INTERVAL_MS = 250;
114
110
 
115
- /**
116
- * 推理(思考)增量在实时流里只推开头这么多字,之后改推轻量进度帧。
117
- *
118
- * 运行中那段思考正文用户基本不读(它自己滚、而且长得飞快),却按 token 一帧地发,是
119
- * 这条流里帧数最多的一类。完整思考仍然在每一步落定的 `assistant/message` 里跟着来,
120
- * 也仍然在 `/session/messages` 的历史里 —— 这里省的只是「逐字推」那一段。
121
- */
122
- const REASONING_LIVE_CHARS = 600;
123
-
124
- /**
125
- * 工具结果在实时流里最多带这么长的可见输出。
126
- *
127
- * 工具输出(读文件、跑命令、构建日志)是这条流里单帧最大的一类,几十到几百 KB 很常见,
128
- * 而运行中那几秒用户看的是工具卡的状态与一两行摘要;要看全文时走历史那一条
129
- * (`/session/messages` 的块与补丁),那里给的是全文。
130
- */
131
- const TOOL_RESULT_LIVE_CHARS = 2000;
132
-
133
111
  /**
134
112
  * keepalive 节奏,同时也是 `session/heartbeat` 的间隔。
135
113
  *
@@ -302,9 +280,6 @@ function createFrameSealer(req) {
302
280
  };
303
281
  }
304
282
 
305
- /** 工具结果被截断时留在正文末尾的说明(客户端原样显示,不参与解析)。 */
306
- const TOOL_RESULT_LIVE_ELIDED = '\n…(运行中只推前一段,展开时取全文)';
307
-
308
283
  /** 草稿增量的合并键:同一步、同一块、同一类型才可并。 */
309
284
  function chunkKeyOf(event) {
310
285
  const data = event && event.data;
@@ -316,8 +291,8 @@ function chunkKeyOf(event) {
316
291
  /**
317
292
  * 把后到的增量并进已经攒着的那一帧。
318
293
  *
319
- * 只并「客户端按块序号累积」的两类:文本(拼接 `text`)与工具参数(拼接 `argumentsDelta`)。
320
- * 其余类型(block-start/end、推理、协议 v2 的其它通知)一律不并 —— 它们带着语义边界,
294
+ * 只并「客户端按块序号累积」的两类:文本(拼接 `text`)与工具参数(正文不下发,并只为省帧)。
295
+ * 其余类型(block-start/end、协议 v2 的其它通知)一律不并 —— 它们带着语义边界,
321
296
  * 合并等于把边界抹掉。
322
297
  */
323
298
  function mergeDraft(into, from) {
@@ -329,11 +304,8 @@ function mergeDraft(into, from) {
329
304
  return { ...into, time: from.time, data: { ...into.data, chunk: { ...a, text: a.text + b.text } } };
330
305
  }
331
306
  if (a.type === 'tool-call-delta') {
332
- const chunk = { ...a };
333
- if (typeof b.argumentsDelta === 'string' && b.argumentsDelta) {
334
- chunk.argumentsDelta = (typeof a.argumentsDelta === 'string' ? a.argumentsDelta : '') + b.argumentsDelta;
335
- }
336
- return { ...into, time: from.time, data: { ...into.data, chunk } };
307
+ // 参数正文已经被 wire-cut.js 那条规则丢掉了,这一帧只剩身份字段,带上后一帧的时间即可。
308
+ return { ...into, time: from.time, data: { ...into.data, chunk: a } };
337
309
  }
338
310
  return null;
339
311
  }
@@ -413,73 +385,6 @@ function createDraftBuffer({ push }) {
413
385
  return { accept, flush, flushAll, isClosed: () => closed };
414
386
  }
415
387
 
416
- /**
417
- * 推理增量的实时截断(见 `REASONING_LIVE_CHARS`)。
418
- *
419
- * 每一步只推开头那一段,之后整段丢掉 —— 不新增帧类型,也就不动 App:组头那句「正在思考」
420
- * 本身一直在闪,用户看到的就是「它还在想」;完整正文在 `assistant/message` 与历史里,
421
- * 一个字都不丢。返回 `null` 表示这一帧不写。
422
- */
423
- function createReasoningGate() {
424
- const state = { step: null, sent: 0 };
425
- return (event) => {
426
- const data = event && event.data;
427
- const chunk = data && data.chunk;
428
- if (!chunk || typeof chunk.index !== 'number' || typeof data.turn !== 'number') return event;
429
- const step = `${data.turn}:${data.step}`;
430
- if (step !== state.step) {
431
- state.step = step;
432
- state.sent = 0;
433
- }
434
- const remaining = REASONING_LIVE_CHARS - state.sent;
435
- if (remaining <= 0) return null;
436
- if (chunk.text.length <= remaining) {
437
- state.sent += chunk.text.length;
438
- return event;
439
- }
440
- const text = chunk.text.slice(0, remaining);
441
- state.sent += text.length;
442
- return { ...event, data: { ...data, chunk: { ...chunk, text } } };
443
- };
444
- }
445
-
446
- /** 实时流里把工具结果的可见输出截到 `TOOL_RESULT_LIVE_CHARS`(两种内容形状都认)。 */
447
- function trimTextParts(parts, state) {
448
- let changed = false;
449
- const next = parts.map((part) => {
450
- if (!part || typeof part !== 'object') return part;
451
- if (Array.isArray(part.content)) {
452
- const nested = trimTextParts(part.content, state);
453
- if (nested === part.content) return part;
454
- changed = true;
455
- return { ...part, content: nested };
456
- }
457
- if (typeof part.text !== 'string' || !part.text) return part;
458
- if (state.budget <= 0) {
459
- changed = true;
460
- return { ...part, text: TOOL_RESULT_LIVE_ELIDED.trimStart() };
461
- }
462
- if (part.text.length <= state.budget) {
463
- state.budget -= part.text.length;
464
- return part;
465
- }
466
- const text = `${part.text.slice(0, state.budget)}${TOOL_RESULT_LIVE_ELIDED}`;
467
- state.budget = 0;
468
- changed = true;
469
- return { ...part, text };
470
- });
471
- return changed ? next : parts;
472
- }
473
-
474
- function truncateToolResultOutput(event) {
475
- const data = event && event.data;
476
- const message = data && data.message;
477
- if (!message || !Array.isArray(message.content)) return event;
478
- const content = trimTextParts(message.content, { budget: TOOL_RESULT_LIVE_CHARS });
479
- if (content === message.content) return event;
480
- return { ...event, data: { ...data, message: { ...message, content } } };
481
- }
482
-
483
388
  async function handleStream(ctx, req, res) {
484
389
  const url = new URL(req.url, 'http://x');
485
390
  const sessionId = url.searchParams.get('sessionId');
@@ -568,6 +473,8 @@ async function handleStream(ctx, req, res) {
568
473
  // 本机路径图片的内联预算:**一条流共用一份**,与历史那一页共用一份同口径。
569
474
  // 此前实时帧是每帧一预算,一条消息里几张截图就能把几百 KB 到几 MB 的 base64 推过去。
570
475
  const imageBudget = createTextImageBudget();
476
+ // 过程明细的减法(wire-cut.js)与它要的工具名索引:整条流共用一份,回放与实时帧都往里吸。
477
+ const toolNames = createToolNameIndex();
571
478
 
572
479
  try {
573
480
  // **先开实时流、再回放**:之前是先 replay 再 mux,两步之间的 follow 还没订阅,
@@ -583,7 +490,7 @@ async function handleStream(ctx, req, res) {
583
490
 
584
491
  // 断线续传:回放 history 中 seq > resumeSeq 的事件(收敛状态)。
585
492
  if (resumeSeq != null && resumeSeq > 0) {
586
- const replay = await replaySince(ctx, sessionId, resumeSeq, res, ac.signal, (seq) => { lastDeliveredSeq = Math.max(lastDeliveredSeq, seq); }, streamAddress, sealFrame, imageBudget);
493
+ const replay = await replaySince(ctx, sessionId, resumeSeq, res, ac.signal, (seq) => { lastDeliveredSeq = Math.max(lastDeliveredSeq, seq); }, streamAddress, sealFrame, imageBudget, toolNames);
587
494
  if (replay.gap && !ac.signal.aborted) {
588
495
  const value = { type: 'session/resync-required', sessionId, resumeSeq, oldestSeq: replay.oldestSeq };
589
496
  // 旧实现在这里直接 `return`:既不清 keepAlive 也不 end,漏出一条永不收尾的连接。
@@ -605,10 +512,8 @@ async function handleStream(ctx, req, res) {
605
512
  }
606
513
  return 'sent';
607
514
  };
608
- // 草稿合并 + 推理限流 + 整条流共用一份内联图片预算(见上面三组常量与
609
- // `createDraftBuffer` / `createReasoningGate` 的注释)。
515
+ // 草稿合并 + 整条流共用一份内联图片预算(见上面两组常量与 `createDraftBuffer` 的注释)。
610
516
  const drafts = createDraftBuffer({ push });
611
- const reasoningGate = createReasoningGate();
612
517
  /**
613
518
  * 非草稿帧写之前先把攒着的草稿写出去:草稿是按发生顺序攒的,晚写一步就会变成
614
519
  * 「收尾的 assistant/message 先到、被并过的正文后到」,界面上同一段话画两遍。
@@ -630,7 +535,9 @@ async function handleStream(ctx, req, res) {
630
535
  // 预算按**整条流**共享:本机路径的图片内联(local-images.js)此前是每帧一份,
631
536
  // 一条消息里几张截图就能把几百 KB 到几 MB 的 base64 推进去。
632
537
  let event = await resolveEventAttachments(ctx, sessionId, p.event, { referencesOnly: true, budget: imageBudget });
633
- if (event.type === 'tool/result') event = truncateToolResultOutput(event);
538
+ // 先吸工具名再减:结果自己不带工具名,正文留不留取决于它(见 wire-cut.js)。
539
+ toolNames.absorb(event);
540
+ event = cutEvent(event, toolNames);
634
541
  const chunk = event.data && event.data.chunk;
635
542
  if (seq !== null || !chunk || typeof chunk.type !== 'string') {
636
543
  // 持久事件(或不是增量的会话事件):先把攒着的草稿写掉,再按原样写这一帧。
@@ -639,10 +546,8 @@ async function handleStream(ctx, req, res) {
639
546
  if (result === 'closed') { clientGone = true; break; }
640
547
  if (seq !== null) lastDeliveredSeq = seq;
641
548
  } else if (chunk.type === 'reasoning-delta') {
642
- const kept = reasoningGate(event);
643
- if (kept === null) continue;
644
- await drafts.accept(kept, { ...frame, payload: { ...p, event: kept } });
645
- if (drafts.isClosed()) { clientGone = true; break; }
549
+ // 推理不出本机:整帧丢掉(不新增帧类型,也就不动 App 的帧裁决)。
550
+ continue;
646
551
  } else if (MERGEABLE_CHUNK_TYPES.has(chunk.type)) {
647
552
  // 被 shed 的只可能是没有 seq 的增量(见 writeSse 的软水位闸);它不推进水位,
648
553
  // 同一轮收尾的 assistant/message 会把完整正文补上。
@@ -707,7 +612,7 @@ async function handleStream(ctx, req, res) {
707
612
  * 从会话 history 回放 seq > sinceSeq 的事件(升序),逐个经附件解析后写 SSE。
708
613
  * 通过 onDelivered(seq) 回报已交付的最大 seq,供上层去重。
709
614
  */
710
- async function replaySince(ctx, sessionId, sinceSeq, res, signal, onDelivered, address, sealFrame = null, imageBudget = null) {
615
+ async function replaySince(ctx, sessionId, sinceSeq, res, signal, onDelivered, address, sealFrame = null, imageBudget = null, toolNames = null) {
711
616
  try {
712
617
  // 子会话的重放也要带父会话(同 historyAddress 的口径),否则宿主回 agent-busy,
713
618
  // 这条断线续传静默退化成「后面缺一段」。
@@ -738,10 +643,14 @@ async function replaySince(ctx, sessionId, sinceSeq, res, signal, onDelivered, a
738
643
  return { gap: true, oldestSeq: oldestSeq ?? sinceSeq + 1 };
739
644
  }
740
645
  let written = 0;
646
+ // 回放戴着与实时流同一副眼镜:这一段里同样有推理、工具参数与工具结果正文(见 wire-cut.js)。
647
+ // 工具名先从整窗吸一遍 —— 窗口里那条 `tool/call` 可能排在游标之前、不在候选里。
648
+ if (toolNames) toolNames.absorbAll(events);
741
649
  for (const entry of candidates) {
742
650
  if (signal.aborted) break;
743
651
  const e = entry?.event || entry;
744
- const event = await resolveEventAttachments(ctx, sessionId, e, { referencesOnly: true });
652
+ const resolved = await resolveEventAttachments(ctx, sessionId, e, { referencesOnly: true });
653
+ const event = cutEvent(resolved, toolNames);
745
654
  // Keep replay data identical to live SSE data: the client reducer consumes
746
655
  // the event's own type (turn/start, assistant/message, etc.).
747
656
  const payload = { type: 'session/event', sessionId, event };
@@ -3,25 +3,12 @@
3
3
  /**
4
4
  * 移动端「子代理」API:把某个会话派出去的子代理目录转给手机。
5
5
  *
6
- * 桌面端会话标题栏里挂着一个「N 个子代理」下拉(@deepseek-ai/dsh-client-ui-subagent
7
- * 的 SubagentHeaderLineage):父会话头部按 `origin === "subagent" && parentId ===
8
- * 当前会话` 把子代理收成一棵树,点进去能读子会话的完整记录。手机端此前把
9
- * `origin === 'subagent'` 的行在侧栏里直接 continue 掉(app/src/drawerGroups.ts:122),
10
- * 于是用户既不知道这个会话派过子代理,也没有任何入口能打开它 —— 子代理在手机上
11
- * 等于不存在。
6
+ * 手机侧栏把 `origin === 'subagent'` 的行直接跳过,于是用户既不知道会话派过子代理,也没有
7
+ * 入口打开它。子会话在普通消息接口里只有带 durable parent address(parentSessionId + mode)
8
+ * 才能读,而 /sessions 只给 origin/parentSessionId,mode 来自这里的子代理目录。
12
9
  *
13
- * 子会话在普通消息接口中只有携带 durable parent address 才能读取(parentSessionId + mode);
14
- * /sessions 只给 origin/parentSessionId,mode 则来自这里的子代理目录。缺的只是
15
- * 「这个会话有哪些子代理」——
16
- * 那正是 subagent.list 的返回值(entries + parentAvailable),这里只做形状收敛、
17
- * 限长与限条数,避免异常宿主把手机端列表撑爆。
18
- *
19
- * 宿主 subagent.list 的第二个形参是取消信号(内部 listChildren 会读 signal),
20
- * 故与 session.search 一样显式传一个 AbortController 的 signal,不裸传 undefined。
21
- *
22
- * DSH 0.2.0 没有 subagent.list 这个 RPC 了,目录改由 host-bridge.js 从会话投影的
23
- * subagentCatalog 重建(形状仍是 entries + parentAvailable),本文件对它的形状收敛、
24
- * 限长与限条数一字未动。
10
+ * 0.2.0 没有 subagent.list 这个 RPC,目录由 host-bridge.js 从会话投影的 subagentCatalog
11
+ * 重建(形状仍是 entries + parentAvailable);本文件只做形状收敛、限长与限条数。
25
12
  */
26
13
 
27
14
  const { sendJson, readJsonBody } = require('../shared/http-util');
@@ -1,28 +1,24 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * 移动端文件分片上传:把手机分享进来的文件落到宿主机固定收件目录。
4
+ * 移动端文件分片上传:把手机分享进来的文件落到宿主机的收件目录,或落到指定的那个目录。
5
5
  *
6
- * 四步:begin(登记文件名,拿 uploadId 与片大小)→ chunk(按 index 顺序追加)→
7
- * finish(落盘成 `<收件根>/<日期>/<文件名>`,重名自动加后缀)→ abort(放弃)。
6
+ * 四步:begin(登记文件名,拿 uploadId 与片大小)→ chunk(按 index 顺序追加)→ finish
7
+ * (落盘成 `<收件根>/<日期>/<文件名>`,重名自动加后缀)→ abort(放弃)。
8
8
  *
9
- * 为什么分片,而不是一次 POST 传完:
10
- * - 一次传完会把「能传多大的文件」变成一条运维配置 —— 中转的
11
- * `MAX_BODY_BYTES`(dsh-mobile-server/lib/config.js)默认 50MB,插件自己的
12
- * readJsonBody 默认只有 256KB。分片后单请求最大只有 2MB,与这些上限无关。
13
- * - 手机侧网络不稳,分片能只重传坏掉的那一片,而不是整个文件。
9
+ * 落点两条路:不带 `destPath` 时仍进固定收件箱(分享进来的文件本来就没有归处,攥在
10
+ * `~/.dsh/mobile-inbox` 里等用户自己搬);带 `destPath` 时落进那个目录 —— 右栏文件浏览器
11
+ * 「传到这里」要的就是它。后者必须在 `allowed-zone.js` 的允许区内(工作区 ∪ 当前会话项目
12
+ * 目录),能力面与按路径读文件同一份判据,写入不可能比读更宽。
14
13
  *
15
- * 为什么片内容走 base64 而不是原始字节:隧道协议本身支持原始字节
16
- * (lib/relay/protocol.js 的 httpBody 帧),但 App 侧没有任何「发二进制 body」的
17
- * 先例,RN 的 fetch 对 ArrayBuffer body 的支持也随版本变;图片通道本来就以 base64
18
- * 走 JSON,这里沿用同一条确定能用的路。代价是 33% 膨胀(单片 2MB → 约 2.73MB),
19
- * 仍远低于任何一层上限。
14
+ * 分片而不是一次传完:单请求因此与中转的 `MAX_BODY_BYTES`、readJsonBody 的 256KB 上限无关,
15
+ * 手机网络不稳时也能只重传坏掉的那一片。片内容走 base64 而不是原始字节:App 侧没有发二进制
16
+ * body 的先例,图片通道本来就以 base64 走 JSON;代价是 33% 膨胀,仍远低于各层上限。
20
17
  *
21
- * 顺序与幂等:chunk 只接受「下一片」,重复送达同一片按重传处理(回当前 received,
22
- * 不再追加),跳号直接 409 —— 客户端因此不需要在重试前先问进度。
18
+ * 顺序与幂等:chunk 只接受下一片,重复送达同一片按重传处理,跳号直接 409。
23
19
  *
24
- * 落盘根目录固定 `~/.dsh/mobile-inbox`(收件专目录,与工作区解耦,工作区被删也不
25
- * 影响已收到的文件)。`DSH_MOBILE_INBOX` 可覆盖,只给测试用。
20
+ * 收件根固定 `~/.dsh/mobile-inbox`,与工作区解耦(工作区被删不影响已收到的文件);
21
+ * `DSH_MOBILE_INBOX` 可覆盖,只给测试用。
26
22
  */
27
23
 
28
24
  const fsp = require('node:fs/promises');
@@ -31,6 +27,7 @@ const path = require('node:path');
31
27
  const crypto = require('node:crypto');
32
28
 
33
29
  const { sendJson, readJsonBody } = require('../shared/http-util');
30
+ const { allowedRoots, inAllowedZone } = require('./allowed-zone.js');
34
31
 
35
32
  // 片大小的**起步值** 256KB(base64 后约 341KB):客户端一次请求的硬预算是 8s
36
33
  // (`app/src/api/core.ts` 的 REQUEST_TIMEOUT_MS),一上来就给大片的机器会在慢速上行
@@ -212,12 +209,65 @@ function fail(res, status, code, message) {
212
209
  return sendJson(res, status, { ok: false, error: { code, message } });
213
210
  }
214
211
 
215
- /** POST /api/mobile/v1/upload/begin → { ok, uploadId, chunkSize }。 */
216
- async function handleUploadBegin(_ctx, req, res) {
212
+ /**
213
+ * begin 里那个可选落点:解析、判允许区、确认它是个**已经存在的目录**。
214
+ *
215
+ * 返回 `{ dir }`(字符串,未指定时为空串)或 `{ status, code, message }`。目录不存在与
216
+ * 「不是目录」都按 400 报:这条路上 404 只留给 uploadId,混用会让 App 把「目录没了」
217
+ * 当成「上传丢了」,从而把一个还在的上传重投一遍。
218
+ *
219
+ * `sessionId` 与 `/file/read` 同义:给上它,这条会话的项目目录也算在允许区里(右栏那份
220
+ * 「当前目录」未必被登记成了工作区)。
221
+ */
222
+ async function resolveDestDir(ctx, body) {
223
+ if (body.destPath === undefined || body.destPath === null) return { dir: '' };
224
+ if (typeof body.destPath !== 'string' || !body.destPath.trim()) {
225
+ return { status: 400, code: 'bad-request', message: 'destPath 必须是绝对目录路径' };
226
+ }
227
+ const asked = body.destPath.trim();
228
+ if (!path.isAbsolute(asked)) {
229
+ return { status: 400, code: 'bad-request', message: 'destPath 必须是绝对路径' };
230
+ }
231
+ const dir = path.resolve(asked);
232
+ const sessionId = typeof body.sessionId === 'string' ? body.sessionId.trim() : '';
233
+ let roots;
234
+ try {
235
+ roots = await allowedRoots(ctx, sessionId);
236
+ } catch (error) {
237
+ return {
238
+ status: 502,
239
+ code: (error && error.code) || 'workspace-list-failed',
240
+ message: String((error && error.message) || error),
241
+ };
242
+ }
243
+ if (!inAllowedZone(roots, dir)) {
244
+ return {
245
+ status: 403,
246
+ code: 'outside-allowed',
247
+ message: '这个目录不在能写的范围里(只放行工作区目录和这条会话的项目目录)',
248
+ };
249
+ }
250
+ let info;
251
+ try {
252
+ info = await fsp.stat(dir);
253
+ } catch {
254
+ info = null;
255
+ }
256
+ if (!info || !info.isDirectory()) {
257
+ return { status: 400, code: 'not-a-directory', message: 'destPath 不是一个已经存在的目录' };
258
+ }
259
+ return { dir };
260
+ }
261
+
262
+ /** POST /api/mobile/v1/upload/begin { name, destPath?, sessionId? } → { ok, uploadId, chunkSize }。 */
263
+ async function handleUploadBegin(ctx, req, res) {
217
264
  const body = await readJsonBody(req);
218
265
  if (!body || typeof body.name !== 'string' || !body.name.trim()) {
219
266
  return fail(res, 400, 'bad-request', '需要 { name }');
220
267
  }
268
+ // 落点先判:越界 / 不是目录在这一步就回,不先建暂存再报错 —— 残片要躺满 24 小时才清。
269
+ const destination = await resolveDestDir(ctx, body);
270
+ if (destination.status) return fail(res, destination.status, destination.code, destination.message);
221
271
  const stagingDir = path.join(inboxRoot(), '.staging');
222
272
  try {
223
273
  await fsp.mkdir(stagingDir, { recursive: true });
@@ -236,6 +286,8 @@ async function handleUploadBegin(_ctx, req, res) {
236
286
  }
237
287
  const job = {
238
288
  name: sanitizeName(body.name),
289
+ // 指定落点就记在这里,finish 才用得上(中途不再判一次范围:begin 已经认过)。
290
+ destDir: destination.dir,
239
291
  stagingPath,
240
292
  received: 0,
241
293
  bytes: 0,
@@ -299,9 +351,11 @@ async function handleUploadFinish(_ctx, req, res) {
299
351
  }
300
352
  removeUpload(uploadId, job);
301
353
 
302
- const dir = path.join(inboxRoot(), today());
354
+ const dir = job.destDir || path.join(inboxRoot(), today());
303
355
  try {
304
- await fsp.mkdir(dir, { recursive: true });
356
+ // 指定落点时目录在 begin 已确认存在:中途被删了就该按落盘失败报,不能悄悄重建一个
357
+ // 同名目录替用户做主。
358
+ if (!job.destDir) await fsp.mkdir(dir, { recursive: true });
305
359
  const target = await uniquePath(dir, job.name);
306
360
  await fsp.rename(job.stagingPath, target);
307
361
  // 记住这一份:发送图片时用它把「分片上传的字节」换回内联块,见 resolveFinishedUpload。
@@ -0,0 +1,30 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * 厂商品牌图标(统计页「模型用量」那一行前面那一枚)。
5
+ *
6
+ * 来源:Simple Icons(<https://simple-icons.org>,图标数据 CC0,商标归各自厂商)。
7
+ * App 侧用 `SvgXml` 渲染,颜色由主题给 —— 所以每个图标里**不许带 fill / style / defs / 渐变**,
8
+ * 只留 `<path>`(可以在 path 上保留 fill-rule / clip-rule)。
9
+ * 一段图标就几百字节到一两 KB;随 /stats/overview 只下发用到的那几个。
10
+ *
11
+ * openai / microsoft 取自仍带这两枚的最后一个 Simple Icons 版本(15.22.0 / 11.15.0)。
12
+ */
13
+ const VENDOR_LOGOS = {
14
+ deepseek: { name: 'DeepSeek', icon: '<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M23.748 4.651c-.254-.124-.364.113-.512.233-.051.04-.094.09-.137.137-.372.397-.806.657-1.373.626-.829-.046-1.537.214-2.163.848-.133-.782-.575-1.248-1.247-1.548-.352-.155-.708-.311-.955-.65-.172-.24-.219-.509-.305-.774-.055-.16-.11-.323-.293-.35-.2-.031-.278.136-.356.276-.313.572-.434 1.202-.422 1.84.027 1.436.633 2.58 1.838 3.393.137.094.172.187.129.323-.082.28-.18.553-.266.833-.055.179-.137.218-.328.14a5.5 5.5 0 0 1-1.737-1.179c-.857-.828-1.631-1.743-2.597-2.46a12 12 0 0 0-.689-.47c-.985-.957.13-1.743.387-1.836.27-.098.094-.433-.778-.428-.872.003-1.67.295-2.687.685a3 3 0 0 1-.465.136 9.6 9.6 0 0 0-2.883-.101c-1.885.21-3.39 1.1-4.497 2.622C.082 8.776-.231 10.854.152 13.02c.403 2.284 1.568 4.175 3.36 5.653 1.857 1.533 3.997 2.284 6.438 2.14 1.482-.085 3.132-.284 4.994-1.86.47.234.962.328 1.78.398.629.058 1.235-.031 1.705-.129.735-.155.684-.836.418-.961-2.155-1.004-1.682-.595-2.112-.926 1.095-1.295 2.768-3.598 3.284-6.733.05-.346.115-.834.108-1.114-.004-.171.035-.238.23-.257a4.2 4.2 0 0 0 1.545-.475c1.397-.763 1.96-2.016 2.093-3.517.02-.23-.004-.467-.247-.588M11.58 18.168c-2.088-1.642-3.101-2.183-3.52-2.16-.39.024-.32.472-.234.763.09.288.207.487.371.74.114.167.192.416-.113.603-.673.416-1.842-.14-1.897-.168-1.361-.801-2.5-1.86-3.301-3.306-.775-1.393-1.225-2.888-1.299-4.482-.02-.385.094-.522.477-.592a4.7 4.7 0 0 1 1.53-.038c2.131.311 3.946 1.264 5.467 2.774.868.86 1.525 1.887 2.202 2.89.72 1.066 1.494 2.082 2.48 2.915.348.291.626.513.892.677-.802.09-2.14.109-3.055-.615zm1.001-6.44a.306.306 0 0 1 .415-.287.3.3 0 0 1 .113.074.3.3 0 0 1 .086.214c0 .17-.136.307-.308.307a.303.303 0 0 1-.306-.307m3.11 1.596c-.2.081-.4.151-.591.16a1.25 1.25 0 0 1-.798-.254c-.274-.23-.47-.358-.551-.758a1.7 1.7 0 0 1 .015-.588c.07-.327-.007-.537-.238-.727-.188-.156-.426-.199-.689-.199a.6.6 0 0 1-.254-.078.253.253 0 0 1-.114-.358 1 1 0 0 1 .192-.21c.356-.202.767-.136 1.146.016.352.144.618.408 1.001.782.392.451.462.576.685.915.176.264.336.536.446.848.066.194-.02.353-.25.45"/></svg>' },
15
+ anthropic: { name: 'Anthropic', icon: '<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M17.3041 3.541h-3.6718l6.696 16.918H24Zm-10.6082 0L0 20.459h3.7442l1.3693-3.5527h7.0052l1.3693 3.5528h3.7442L10.5363 3.5409Zm-.3712 10.2232 2.2914-5.9456 2.2914 5.9456Z"/></svg>' },
16
+ openai: { name: 'OpenAI', icon: '<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z"/></svg>' },
17
+ google: { name: 'Google', icon: '<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M11.04 19.32Q12 21.51 12 24q0-2.49.93-4.68.96-2.19 2.58-3.81t3.81-2.55Q21.51 12 24 12q-2.49 0-4.68-.93a12.3 12.3 0 0 1-3.81-2.58 12.3 12.3 0 0 1-2.58-3.81Q12 2.49 12 0q0 2.49-.96 4.68-.93 2.19-2.55 3.81a12.3 12.3 0 0 1-3.81 2.58Q2.49 12 0 12q2.49 0 4.68.96 2.19.93 3.81 2.55t2.55 3.81"/></svg>' },
18
+ alibaba: { name: 'Alibaba', icon: '<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M23.919 14.545 20.817 9.17l1.47-2.544a.56.56 0 0 0 0-.566l-1.633-2.83a.57.57 0 0 0-.49-.283h-6.207L12.487.402a.57.57 0 0 0-.49-.284H8.732a.56.56 0 0 0-.49.284L5.139 5.775h-2.94a.56.56 0 0 0-.49.284L.077 8.887a.56.56 0 0 0 0 .567L3.18 14.83l-1.47 2.545a.56.56 0 0 0 0 .566l1.634 2.83a.57.57 0 0 0 .49.283h6.205l1.47 2.545a.57.57 0 0 0 .49.284h3.266a.57.57 0 0 0 .49-.284l3.104-5.375h2.94a.57.57 0 0 0 .49-.283l1.634-2.828a.55.55 0 0 0-.004-.568M8.733.686l1.634 2.828-1.634 2.828H21.8L20.164 9.17H7.425L5.63 6.06Zm1.306 19.801-6.205-.002 1.634-2.83h3.265L2.201 6.344h3.267q3.182 5.517 6.367 11.032zm10.124-5.66L18.53 12l-6.532 11.315-1.634-2.83c2.129-3.673 4.25-7.351 6.373-11.028h3.592l3.102 5.374z"/></svg>' },
19
+ moonshot: { name: 'Moonshot', icon: '<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m1.053 16.91 9.538 2.55a21 20.981 0 0 0 .06 2.031l5.956 1.592a12 11.99 0 0 1-15.554-6.172m-1.02-5.79 11.352 3.035a21 20.981 0 0 0-.469 2.01l10.817 2.89a12 11.99 0 0 1-1.845 2.004L.658 15.918a12 11.99 0 0 1-.625-4.796m1.593-5.146L13.573 9.17a21 20.981 0 0 0-1.01 1.874l11.297 3.02a21 20.981 0 0 1-.67 2.362l-11.55-3.087L.125 10.26a12 11.99 0 0 1 1.499-4.285ZM6.067 1.58l11.285 3.016a21 20.981 0 0 0-1.688 1.719l7.824 2.091a21 20.981 0 0 1 .513 2.664L2.107 5.218a12 11.99 0 0 1 3.96-3.638M21.68 4.866 7.222 1.003A12 11.99 0 0 1 21.68 4.866"/></svg>' },
20
+ meta: { name: 'Meta', icon: '<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M6.915 4.03c-1.968 0-3.683 1.28-4.871 3.113C.704 9.208 0 11.883 0 14.449c0 .706.07 1.369.21 1.973a6.624 6.624 0 0 0 .265.86 5.297 5.297 0 0 0 .371.761c.696 1.159 1.818 1.927 3.593 1.927 1.497 0 2.633-.671 3.965-2.444.76-1.012 1.144-1.626 2.663-4.32l.756-1.339.186-.325c.061.1.121.196.183.3l2.152 3.595c.724 1.21 1.665 2.556 2.47 3.314 1.046.987 1.992 1.22 3.06 1.22 1.075 0 1.876-.355 2.455-.843a3.743 3.743 0 0 0 .81-.973c.542-.939.861-2.127.861-3.745 0-2.72-.681-5.357-2.084-7.45-1.282-1.912-2.957-2.93-4.716-2.93-1.047 0-2.088.467-3.053 1.308-.652.57-1.257 1.29-1.82 2.05-.69-.875-1.335-1.547-1.958-2.056-1.182-.966-2.315-1.303-3.454-1.303zm10.16 2.053c1.147 0 2.188.758 2.992 1.999 1.132 1.748 1.647 4.195 1.647 6.4 0 1.548-.368 2.9-1.839 2.9-.58 0-1.027-.23-1.664-1.004-.496-.601-1.343-1.878-2.832-4.358l-.617-1.028a44.908 44.908 0 0 0-1.255-1.98c.07-.109.141-.224.211-.327 1.12-1.667 2.118-2.602 3.358-2.602zm-10.201.553c1.265 0 2.058.791 2.675 1.446.307.327.737.871 1.234 1.579l-1.02 1.566c-.757 1.163-1.882 3.017-2.837 4.338-1.191 1.649-1.81 1.817-2.486 1.817-.524 0-1.038-.237-1.383-.794-.263-.426-.464-1.13-.464-2.046 0-2.221.63-4.535 1.66-6.088.454-.687.964-1.226 1.533-1.533a2.264 2.264 0 0 1 1.088-.285z"/></svg>' },
21
+ mistral: { name: 'Mistral', icon: '<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M17.143 3.429v3.428h-3.429v3.429h-3.428V6.857H6.857V3.43H3.43v13.714H0v3.428h10.286v-3.428H6.857v-3.429h3.429v3.429h3.429v-3.429h3.428v3.429h-3.428v3.428H24v-3.428h-3.43V3.429z"/></svg>' },
22
+ bytedance: { name: 'ByteDance', icon: '<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M19.8772 1.4685L24 2.5326v18.9426l-4.1228 1.0563V1.4685zm-13.3481 9.428l4.115 1.0641v8.9786l-4.115 1.0642v-11.107zM0 2.572l4.115 1.0642v16.7354L0 21.428V2.572zm17.4553 5.6205v11.107l-4.1228-1.0642V9.2568l4.1228-1.0642z"/></svg>' },
23
+ baidu: { name: 'Baidu', icon: '<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M9.154 0C7.71 0 6.54 1.658 6.54 3.707c0 2.051 1.171 3.71 2.615 3.71 1.446 0 2.614-1.659 2.614-3.71C11.768 1.658 10.6 0 9.154 0zm7.025.594C14.86.58 13.347 2.589 13.2 3.927c-.187 1.745.25 3.487 2.179 3.735 1.933.25 3.175-1.806 3.422-3.364.252-1.555-.995-3.364-2.362-3.674a1.218 1.218 0 0 0-.261-.03zM3.582 5.535a2.811 2.811 0 0 0-.156.008c-2.118.19-2.428 3.24-2.428 3.24-.287 1.41.686 4.425 3.297 3.864 2.617-.561 2.262-3.68 2.183-4.362-.125-1.018-1.292-2.773-2.896-2.75zm16.534 1.753c-2.308 0-2.617 2.119-2.617 3.616 0 1.43.121 3.425 2.988 3.362 2.867-.063 2.553-3.238 2.553-3.988 0-.745-.62-2.99-2.924-2.99zm-8.264 2.478c-1.424.014-2.708.925-3.323 1.947-1.118 1.868-2.863 3.05-3.112 3.363-.25.309-3.61 2.116-2.864 5.42.746 3.301 3.365 3.237 3.365 3.237s1.93.19 4.171-.31c2.24-.495 4.17.123 4.17.123s5.233 1.748 6.665-1.616c1.43-3.364-.808-5.109-.808-5.109s-2.99-2.306-4.736-4.798c-1.072-1.665-2.348-2.268-3.528-2.257zm-2.234 3.84l1.542.024v8.197H7.758c-1.47-.291-2.055-1.292-2.13-1.462-.072-.173-.488-.976-.268-2.343.635-2.049 2.447-2.196 2.447-2.196h1.81zm3.964 2.39v3.881c.096.413.612.488.612.488h1.614v-4.343h1.689v5.782h-3.915c-1.517-.39-1.59-1.465-1.59-1.465v-4.317zm-5.458 1.147c-.66.197-.978.708-1.05.928-.076.22-.247.78-.1 1.269.294 1.095 1.248 1.144 1.248 1.144h1.37v-3.34z"/></svg>' },
24
+ tencent: { name: 'Tencent', icon: '<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12 0a1 1 0 0 1 0 24 1 1 0 0 1 0-24m1.65 1.12c.533.097 1.023.233 1.41.404 6.086 2.686 7.398 9.217 1.603 14.341a3.782 3.782 0 0 1-6.126-1.75 3.66 3.66 0 0 1-.095-1.622c-1.934.6-3.295 2.305-3.524 4.45-.204 1.912.324 4.044 2.056 5.634A11 11 0 1 0 13.65 1.12M2.748 6.21A11 11 0 0 0 8.9 22.55c-5.377-4.022-6.06-8.4-2.43-13.87a2.234 2.234 0 1 0-3.722-2.47m10.157-5.172c4.408 1.33 3.61 5.41 2.447 6.924-.86 1.117-2.922 1.46-3.708 2.238a4.18 4.18 0 0 0-1.212 2.292A5.3 5.3 0 0 1 12 12.258a5.672 5.672 0 0 0 1.404-11.169 11 11 0 0 0-.5-.052"/></svg>' },
25
+ minimax: { name: 'MiniMax', icon: '<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M11.43 3.92a.86.86 0 1 0-1.718 0v14.236a1.999 1.999 0 0 1-3.997 0V9.022a.86.86 0 1 0-1.718 0v3.87a1.999 1.999 0 0 1-3.997 0V11.49a.57.57 0 0 1 1.139 0v1.404a.86.86 0 0 0 1.719 0V9.022a1.999 1.999 0 0 1 3.997 0v9.134a.86.86 0 0 0 1.719 0V3.92a1.998 1.998 0 1 1 3.996 0v11.788a.57.57 0 1 1-1.139 0zm10.572 3.105a2 2 0 0 0-1.999 1.997v7.63a.86.86 0 0 1-1.718 0V3.923a1.999 1.999 0 0 0-3.997 0v16.16a.86.86 0 0 1-1.719 0V18.08a.57.57 0 1 0-1.138 0v2a1.998 1.998 0 0 0 3.996 0V3.92a.86.86 0 0 1 1.719 0v12.73a1.999 1.999 0 0 0 3.996 0V9.023a.86.86 0 1 1 1.72 0v6.686a.57.57 0 0 0 1.138 0V9.022a2 2 0 0 0-1.998-1.997"/></svg>' },
26
+ nvidia: { name: 'NVIDIA', icon: '<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M8.948 8.798v-1.43a6.7 6.7 0 0 1 .424-.018c3.922-.124 6.493 3.374 6.493 3.374s-2.774 3.851-5.75 3.851c-.398 0-.787-.062-1.158-.185v-4.346c1.528.185 1.837.857 2.747 2.385l2.04-1.714s-1.492-1.952-4-1.952a6.016 6.016 0 0 0-.796.035m0-4.735v2.138l.424-.027c5.45-.185 9.01 4.47 9.01 4.47s-4.08 4.964-8.33 4.964c-.37 0-.733-.035-1.095-.097v1.325c.3.035.61.062.91.062 3.957 0 6.82-2.023 9.593-4.408.459.371 2.34 1.263 2.73 1.652-2.633 2.208-8.772 3.984-12.253 3.984-.335 0-.653-.018-.971-.053v1.864H24V4.063zm0 10.326v1.131c-3.657-.654-4.673-4.46-4.673-4.46s1.758-1.944 4.673-2.262v1.237H8.94c-1.528-.186-2.73 1.245-2.73 1.245s.68 2.412 2.739 3.11M2.456 10.9s2.164-3.197 6.5-3.533V6.201C4.153 6.59 0 10.653 0 10.653s2.35 6.802 8.948 7.42v-1.237c-4.84-.6-6.492-5.936-6.492-5.936z"/></svg>' },
27
+ microsoft: { name: 'Microsoft', icon: '<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M0 0v11.408h11.408V0zm12.594 0v11.408H24V0zM0 12.594V24h11.408V12.594zm12.594 0V24H24V12.594z"/></svg>' },
28
+ };
29
+
30
+ module.exports = { VENDOR_LOGOS };