dsh-mobile-plugin 0.5.28 → 0.5.29

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 (350) hide show
  1. package/extensions/catalog.json +10 -1
  2. package/extensions/magpie/agentcompact.js +104 -0
  3. package/extensions/magpie/app.css +4902 -0
  4. package/extensions/magpie/app.js +24474 -0
  5. package/extensions/magpie/autobackup.js +185 -0
  6. package/extensions/magpie/autoresetown.js +151 -0
  7. package/extensions/magpie/balfound.js +38 -0
  8. package/extensions/magpie/boot.js +23 -0
  9. package/extensions/magpie/cfgblocked.js +39 -0
  10. package/extensions/magpie/checkinnow.js +111 -0
  11. package/extensions/magpie/claudemode.js +92 -0
  12. package/extensions/magpie/compat.js +41 -0
  13. package/extensions/magpie/context.css +433 -0
  14. package/extensions/magpie/context.js +1576 -0
  15. package/extensions/magpie/ctxtext.js +223 -0
  16. package/extensions/magpie/fonts.js +42 -0
  17. package/extensions/magpie/i18n.js +18942 -0
  18. package/extensions/magpie/icons/ai21.svg +1 -0
  19. package/extensions/magpie/icons/ai302-color.svg +1 -0
  20. package/extensions/magpie/icons/ai360-color.svg +1 -0
  21. package/extensions/magpie/icons/aihubmix-color.svg +1 -0
  22. package/extensions/magpie/icons/aionui.svg +1 -0
  23. package/extensions/magpie/icons/air.svg +4 -0
  24. package/extensions/magpie/icons/alibaba-color.svg +1 -0
  25. package/extensions/magpie/icons/alibabacloud-color.svg +1 -0
  26. package/extensions/magpie/icons/alma.png +0 -0
  27. package/extensions/magpie/icons/anthropic.svg +1 -0
  28. package/extensions/magpie/icons/antigravity-color.svg +1 -0
  29. package/extensions/magpie/icons/anyscale-color.svg +1 -0
  30. package/extensions/magpie/icons/app-alma.png +0 -0
  31. package/extensions/magpie/icons/app-cc-switch.png +0 -0
  32. package/extensions/magpie/icons/apple-touch-icon.png +0 -0
  33. package/extensions/magpie/icons/aside.svg +28 -0
  34. package/extensions/magpie/icons/astrbot.svg +1 -0
  35. package/extensions/magpie/icons/atomcode.png +0 -0
  36. package/extensions/magpie/icons/aws.svg +1 -0
  37. package/extensions/magpie/icons/azure-color.svg +1 -0
  38. package/extensions/magpie/icons/azureai-color.svg +1 -0
  39. package/extensions/magpie/icons/baichuan-color.svg +1 -0
  40. package/extensions/magpie/icons/baidu-color.svg +1 -0
  41. package/extensions/magpie/icons/baiducloud-color.svg +1 -0
  42. package/extensions/magpie/icons/bailian-color.svg +1 -0
  43. package/extensions/magpie/icons/baseten.svg +1 -0
  44. package/extensions/magpie/icons/bedrock-color.svg +1 -0
  45. package/extensions/magpie/icons/burncloud-color.svg +1 -0
  46. package/extensions/magpie/icons/bytedance-color.svg +1 -0
  47. package/extensions/magpie/icons/cerebras.svg +1 -0
  48. package/extensions/magpie/icons/chatglm-color.svg +1 -0
  49. package/extensions/magpie/icons/cherryin-color.svg +1 -0
  50. package/extensions/magpie/icons/chutes.svg +1 -0
  51. package/extensions/magpie/icons/cindy.png +0 -0
  52. package/extensions/magpie/icons/claude-color.svg +1 -0
  53. package/extensions/magpie/icons/claudecode-color.svg +1 -0
  54. package/extensions/magpie/icons/cline.svg +1 -0
  55. package/extensions/magpie/icons/cloudflare-color.svg +1 -0
  56. package/extensions/magpie/icons/codebuddy-color.svg +1 -0
  57. package/extensions/magpie/icons/codex-color.svg +1 -0
  58. package/extensions/magpie/icons/cohere-color.svg +1 -0
  59. package/extensions/magpie/icons/cometapi-color.svg +1 -0
  60. package/extensions/magpie/icons/commandcode.svg +1 -0
  61. package/extensions/magpie/icons/copilot-color.svg +1 -0
  62. package/extensions/magpie/icons/coze.svg +1 -0
  63. package/extensions/magpie/icons/crush.png +0 -0
  64. package/extensions/magpie/icons/curl.svg +1 -0
  65. package/extensions/magpie/icons/cursor.svg +1 -0
  66. package/extensions/magpie/icons/deepinfra-color.svg +1 -0
  67. package/extensions/magpie/icons/deepmind-color.svg +1 -0
  68. package/extensions/magpie/icons/deepseek-color.svg +1 -0
  69. package/extensions/magpie/icons/deveco-color.svg +12 -0
  70. package/extensions/magpie/icons/devin.svg +1 -0
  71. package/extensions/magpie/icons/doubao-color.svg +1 -0
  72. package/extensions/magpie/icons/empryo-color.svg +1 -0
  73. package/extensions/magpie/icons/factory.svg +1 -0
  74. package/extensions/magpie/icons/favicon.svg +1 -0
  75. package/extensions/magpie/icons/featherless-color.svg +1 -0
  76. package/extensions/magpie/icons/fireworks-color.svg +1 -0
  77. package/extensions/magpie/icons/friendli.svg +1 -0
  78. package/extensions/magpie/icons/fx.svg +1 -0
  79. package/extensions/magpie/icons/gemini-color.svg +1 -0
  80. package/extensions/magpie/icons/geminicli-color.svg +1 -0
  81. package/extensions/magpie/icons/gemma-color.svg +1 -0
  82. package/extensions/magpie/icons/giteeai.svg +1 -0
  83. package/extensions/magpie/icons/github.svg +1 -0
  84. package/extensions/magpie/icons/githubcopilot.svg +1 -0
  85. package/extensions/magpie/icons/glama.svg +1 -0
  86. package/extensions/magpie/icons/gmicloud.svg +1 -0
  87. package/extensions/magpie/icons/google-color.svg +1 -0
  88. package/extensions/magpie/icons/googlecloud-color.svg +1 -0
  89. package/extensions/magpie/icons/goose.svg +1 -0
  90. package/extensions/magpie/icons/grok.svg +1 -0
  91. package/extensions/magpie/icons/groq.svg +1 -0
  92. package/extensions/magpie/icons/hanako.png +0 -0
  93. package/extensions/magpie/icons/hermes.svg +1 -0
  94. package/extensions/magpie/icons/huawei-color.svg +1 -0
  95. package/extensions/magpie/icons/huaweicloud-color.svg +1 -0
  96. package/extensions/magpie/icons/huggingface-color.svg +1 -0
  97. package/extensions/magpie/icons/hunyuan-color.svg +1 -0
  98. package/extensions/magpie/icons/hyperbolic-color.svg +1 -0
  99. package/extensions/magpie/icons/icon-512.png +0 -0
  100. package/extensions/magpie/icons/iflytekcloud-color.svg +1 -0
  101. package/extensions/magpie/icons/inference.svg +1 -0
  102. package/extensions/magpie/icons/infinigence-color.svg +1 -0
  103. package/extensions/magpie/icons/internlm-color.svg +1 -0
  104. package/extensions/magpie/icons/jcode.svg +1 -0
  105. package/extensions/magpie/icons/jina.svg +1 -0
  106. package/extensions/magpie/icons/kilo.svg +1 -0
  107. package/extensions/magpie/icons/kilocode.svg +1 -0
  108. package/extensions/magpie/icons/kimi-code-desktop.png +0 -0
  109. package/extensions/magpie/icons/kimi.svg +1 -0
  110. package/extensions/magpie/icons/kiro-color.svg +1 -0
  111. package/extensions/magpie/icons/kling-color.svg +1 -0
  112. package/extensions/magpie/icons/lambda.svg +1 -0
  113. package/extensions/magpie/icons/leptonai-color.svg +1 -0
  114. package/extensions/magpie/icons/liquid.svg +1 -0
  115. package/extensions/magpie/icons/liveagent-color.svg +1 -0
  116. package/extensions/magpie/icons/llmapi.svg +1 -0
  117. package/extensions/magpie/icons/lmstudio.svg +1 -0
  118. package/extensions/magpie/icons/lody.png +0 -0
  119. package/extensions/magpie/icons/longcat.svg +1 -0
  120. package/extensions/magpie/icons/magpie.svg +1 -0
  121. package/extensions/magpie/icons/meta-color.svg +1 -0
  122. package/extensions/magpie/icons/metaai-color.svg +1 -0
  123. package/extensions/magpie/icons/microsoft-color.svg +1 -0
  124. package/extensions/magpie/icons/mimocode.svg +1 -0
  125. package/extensions/magpie/icons/minimax-color.svg +1 -0
  126. package/extensions/magpie/icons/mistermorph-color.svg +1 -0
  127. package/extensions/magpie/icons/mistral-color.svg +1 -0
  128. package/extensions/magpie/icons/mlx-serve.png +0 -0
  129. package/extensions/magpie/icons/modelscope-color.svg +1 -0
  130. package/extensions/magpie/icons/monica-color.svg +1 -0
  131. package/extensions/magpie/icons/moonshot.svg +1 -0
  132. package/extensions/magpie/icons/morph-color.svg +1 -0
  133. package/extensions/magpie/icons/mothx.png +0 -0
  134. package/extensions/magpie/icons/nebius.svg +1 -0
  135. package/extensions/magpie/icons/newapi-color.svg +1 -0
  136. package/extensions/magpie/icons/nousresearch.svg +1 -0
  137. package/extensions/magpie/icons/nova-color.svg +1 -0
  138. package/extensions/magpie/icons/novita-color.svg +1 -0
  139. package/extensions/magpie/icons/nvidia-color.svg +1 -0
  140. package/extensions/magpie/icons/ollama.svg +1 -0
  141. package/extensions/magpie/icons/omlx.svg +1 -0
  142. package/extensions/magpie/icons/omo.svg +1 -0
  143. package/extensions/magpie/icons/omp.svg +1 -0
  144. package/extensions/magpie/icons/openai.svg +1 -0
  145. package/extensions/magpie/icons/openchamber.svg +11 -0
  146. package/extensions/magpie/icons/openclaw.svg +1 -0
  147. package/extensions/magpie/icons/opencode.svg +1 -0
  148. package/extensions/magpie/icons/openmausbot.svg +1 -0
  149. package/extensions/magpie/icons/openrouter.svg +1 -0
  150. package/extensions/magpie/icons/openwebui.svg +1 -0
  151. package/extensions/magpie/icons/parasail.svg +1 -0
  152. package/extensions/magpie/icons/penguin-color.svg +1 -0
  153. package/extensions/magpie/icons/perplexity-color.svg +1 -0
  154. package/extensions/magpie/icons/phala-color.svg +1 -0
  155. package/extensions/magpie/icons/pi.svg +1 -0
  156. package/extensions/magpie/icons/pipellm-color.svg +5 -0
  157. package/extensions/magpie/icons/poe.svg +1 -0
  158. package/extensions/magpie/icons/ppio-color.svg +1 -0
  159. package/extensions/magpie/icons/prime-agent.svg +1 -0
  160. package/extensions/magpie/icons/qiniu-color.svg +1 -0
  161. package/extensions/magpie/icons/qoder.svg +1 -0
  162. package/extensions/magpie/icons/qwen-color.svg +1 -0
  163. package/extensions/magpie/icons/qwenpaw.svg +1 -0
  164. package/extensions/magpie/icons/raycast.svg +1 -0
  165. package/extensions/magpie/icons/reasonix-color.svg +48 -0
  166. package/extensions/magpie/icons/reka.svg +1 -0
  167. package/extensions/magpie/icons/replicate.svg +1 -0
  168. package/extensions/magpie/icons/roocode.svg +1 -0
  169. package/extensions/magpie/icons/rtk.png +0 -0
  170. package/extensions/magpie/icons/sambanova-color.svg +1 -0
  171. package/extensions/magpie/icons/sensenova-color.svg +1 -0
  172. package/extensions/magpie/icons/siliconcloud-color.svg +1 -0
  173. package/extensions/magpie/icons/snow.png +0 -0
  174. package/extensions/magpie/icons/snowflake-color.svg +1 -0
  175. package/extensions/magpie/icons/sophnet-color.svg +1 -0
  176. package/extensions/magpie/icons/spark-color.svg +1 -0
  177. package/extensions/magpie/icons/stepfun-color.svg +1 -0
  178. package/extensions/magpie/icons/straico-color.svg +1 -0
  179. package/extensions/magpie/icons/submodel-color.svg +1 -0
  180. package/extensions/magpie/icons/t3code.svg +4 -0
  181. package/extensions/magpie/icons/targon-color.svg +1 -0
  182. package/extensions/magpie/icons/tencent-color.svg +1 -0
  183. package/extensions/magpie/icons/tencentcloud-color.svg +1 -0
  184. package/extensions/magpie/icons/tiangong-color.svg +1 -0
  185. package/extensions/magpie/icons/together-color.svg +1 -0
  186. package/extensions/magpie/icons/trae.svg +1 -0
  187. package/extensions/magpie/icons/typesafe.png +0 -0
  188. package/extensions/magpie/icons/upstage-color.svg +1 -0
  189. package/extensions/magpie/icons/venice-color.svg +1 -0
  190. package/extensions/magpie/icons/vercel.svg +1 -0
  191. package/extensions/magpie/icons/vertexai-color.svg +1 -0
  192. package/extensions/magpie/icons/vllm-color.svg +1 -0
  193. package/extensions/magpie/icons/volcengine-color.svg +1 -0
  194. package/extensions/magpie/icons/voyage-color.svg +1 -0
  195. package/extensions/magpie/icons/vscode.svg +1 -0
  196. package/extensions/magpie/icons/wenxin-color.svg +1 -0
  197. package/extensions/magpie/icons/windsurf.svg +1 -0
  198. package/extensions/magpie/icons/workbuddy-color.svg +1 -0
  199. package/extensions/magpie/icons/workersai-color.svg +1 -0
  200. package/extensions/magpie/icons/xai.svg +1 -0
  201. package/extensions/magpie/icons/xiaomimimo.svg +1 -0
  202. package/extensions/magpie/icons/xinference-color.svg +1 -0
  203. package/extensions/magpie/icons/yi.svg +1 -0
  204. package/extensions/magpie/icons/yuanbao-color.svg +1 -0
  205. package/extensions/magpie/icons/yylx.svg +5 -0
  206. package/extensions/magpie/icons/zai.svg +1 -0
  207. package/extensions/magpie/icons/zcode.png +0 -0
  208. package/extensions/magpie/icons/zed.svg +1 -0
  209. package/extensions/magpie/icons/zenmux.svg +1 -0
  210. package/extensions/magpie/icons/zeroone.svg +1 -0
  211. package/extensions/magpie/icons/zhipu-color.svg +1 -0
  212. package/extensions/magpie/icons/zoo-code.png +0 -0
  213. package/extensions/magpie/index.html +572 -0
  214. package/extensions/magpie/kernel.js +274 -0
  215. package/extensions/magpie/keyspend.js +63 -0
  216. package/extensions/magpie/layout.js +26 -0
  217. package/extensions/magpie/manifest.json +37 -0
  218. package/extensions/magpie/navlamp.js +47 -0
  219. package/extensions/magpie/omarchy.css +72 -0
  220. package/extensions/magpie/omarchy.js +46 -0
  221. package/extensions/magpie/passerrors.js +21 -0
  222. package/extensions/magpie/pluginpages.js +405 -0
  223. package/extensions/magpie/plugins/gateway/client.js +1194 -0
  224. package/extensions/magpie/plugins/gateway/keys.js +972 -0
  225. package/extensions/magpie/plugins/gateway/settings.js +459 -0
  226. package/extensions/magpie/plugins/library/client.js +6788 -0
  227. package/extensions/magpie/plugins/library/i18n.js +3228 -0
  228. package/extensions/magpie/plugins/library/library.css +848 -0
  229. package/extensions/magpie/plugins/otel/i18n.js +48 -0
  230. package/extensions/magpie/plugins/otel/page.js +189 -0
  231. package/extensions/magpie/plugins/redact/client.js +46 -0
  232. package/extensions/magpie/plugins/redact/demo.js +461 -0
  233. package/extensions/magpie/plugins/redact/i18n.js +432 -0
  234. package/extensions/magpie/plugins/redact/page.js +985 -0
  235. package/extensions/magpie/plugins/redact/redact.css +385 -0
  236. package/extensions/magpie/plugins/redact/settings.js +248 -0
  237. package/extensions/magpie/plugins/redact/xq_dark.jpg +0 -0
  238. package/extensions/magpie/plugins/redact/xq_dark.webm +0 -0
  239. package/extensions/magpie/plugins/redact/xq_light.jpg +0 -0
  240. package/extensions/magpie/plugins/redact/xq_light.webm +0 -0
  241. package/extensions/magpie/plugins/routing/client.js +8593 -0
  242. package/extensions/magpie/plugins/routing/i18n.js +2696 -0
  243. package/extensions/magpie/plugins/routing/routing.css +1111 -0
  244. package/extensions/magpie/plugins/search/settings.js +460 -0
  245. package/extensions/magpie/plugins/sessions/client.js +1622 -0
  246. package/extensions/magpie/plugins/sessions/i18n.js +360 -0
  247. package/extensions/magpie/plugins/sessions/sessions.css +181 -0
  248. package/extensions/magpie/plugins/sessions/settings.js +100 -0
  249. package/extensions/magpie/plugins/usage/client.js +150 -0
  250. package/extensions/magpie/plugins/usage/i18n.js +984 -0
  251. package/extensions/magpie/plugins/usage/page.js +4307 -0
  252. package/extensions/magpie/plugins.css +319 -0
  253. package/extensions/magpie/plugins.js +1926 -0
  254. package/extensions/magpie/profilerename.js +27 -0
  255. package/extensions/magpie/purposes.js +2 -0
  256. package/extensions/magpie/quitask.js +79 -0
  257. package/extensions/magpie/quotaall.js +142 -0
  258. package/extensions/magpie/ratewindows.js +123 -0
  259. package/extensions/magpie/registry.js +87 -0
  260. package/extensions/magpie/resetask.js +137 -0
  261. package/extensions/magpie/ruleedit.js +53 -0
  262. package/extensions/magpie/scrollends.js +91 -0
  263. package/extensions/magpie/settingshints.js +75 -0
  264. package/extensions/magpie/signsheets.js +86 -0
  265. package/extensions/magpie/start.js +26 -0
  266. package/extensions/magpie/syncfiles.js +216 -0
  267. package/extensions/magpie/theme.js +29 -0
  268. package/extensions/magpie/traywindows.js +41 -0
  269. package/extensions/magpie/webfonts.js +130 -0
  270. package/extensions/magpie/whatsnew.js +252 -0
  271. package/extensions/magpie.zip +0 -0
  272. package/knowledge/apps/baijiahao.md +30 -0
  273. package/knowledge/apps/bilibili.md +1 -0
  274. package/knowledge/apps/dedao.md +33 -0
  275. package/knowledge/apps/feishu.md +35 -0
  276. package/knowledge/apps/gmail.md +33 -0
  277. package/knowledge/apps/jike.md +31 -0
  278. package/knowledge/apps/kuaishou.md +34 -0
  279. package/knowledge/apps/linkedin.md +33 -0
  280. package/knowledge/apps/qqmail.md +30 -0
  281. package/knowledge/apps/telegram.md +27 -0
  282. package/knowledge/apps/toutiao.md +32 -0
  283. package/knowledge/apps/uber-eats.md +26 -0
  284. package/knowledge/apps/uber.md +27 -0
  285. package/knowledge/apps/whatsapp.md +27 -0
  286. package/knowledge/apps/x.md +32 -0
  287. package/knowledge/apps/xiaohongshu.md +1 -0
  288. package/knowledge/apps/zhihu.md +1 -0
  289. package/knowledge/sites/bilibili.com/SITE.md +50 -0
  290. package/knowledge/sites/bilibili.com/references/view-fields.md +17 -0
  291. package/knowledge/sites/github.com/SITE.md +51 -0
  292. package/knowledge/sites/github.com/references/repo-fields.md +13 -0
  293. package/knowledge/sites/x.com/SITE.md +47 -0
  294. package/knowledge/sites/xiaohongshu.com/SITE.md +48 -0
  295. package/knowledge/sites/xiaohongshu.com/references/note-fields.md +18 -0
  296. package/knowledge/sites/youtube.com/SITE.md +49 -0
  297. package/knowledge/sites/youtube.com/references/innertube-fields.md +14 -0
  298. package/knowledge/sites/zhihu.com/SITE.md +47 -0
  299. package/knowledge/sites/zhihu.com/references/api-fields.md +21 -0
  300. package/lib/mobile/extension-catalog.js +41 -1
  301. package/lib/mobile/extension-data.js +224 -0
  302. package/lib/mobile/extension-dev.js +3 -1
  303. package/lib/mobile/file-ops.js +252 -0
  304. package/lib/mobile/history-derive.js +80 -4
  305. package/lib/mobile/host-bridge/endpoints.js +48 -6
  306. package/lib/mobile/host-bridge/event-synthesis.js +33 -6
  307. package/lib/mobile/jobs-control.js +118 -0
  308. package/lib/mobile/memory.js +635 -0
  309. package/lib/mobile/model-badges.js +77 -0
  310. package/lib/mobile/pet-tools.js +50 -4
  311. package/lib/mobile/pets.js +214 -18
  312. package/lib/mobile/phone/access.js +85 -0
  313. package/lib/mobile/phone/evidence.js +228 -0
  314. package/lib/mobile/phone/gate.js +290 -0
  315. package/lib/mobile/phone/grounding.js +314 -0
  316. package/lib/mobile/phone/guard.js +465 -0
  317. package/lib/mobile/phone/irreversible.js +88 -8
  318. package/lib/mobile/phone/knowledge-tools.js +665 -0
  319. package/lib/mobile/phone/learn.js +329 -0
  320. package/lib/mobile/phone/redact.js +237 -0
  321. package/lib/mobile/phone/risk.js +95 -0
  322. package/lib/mobile/phone/routes.js +28 -1
  323. package/lib/mobile/phone/service.js +23 -0
  324. package/lib/mobile/phone/site-commands.js +271 -0
  325. package/lib/mobile/phone/step-summary.js +201 -0
  326. package/lib/mobile/phone/stop-summary.js +235 -0
  327. package/lib/mobile/phone/tool-groups.js +125 -0
  328. package/lib/mobile/phone/tools.js +589 -0
  329. package/lib/mobile/phone/untrusted.js +65 -0
  330. package/lib/mobile/phone-knowledge.js +20 -4
  331. package/lib/mobile/schedule-edit.js +500 -0
  332. package/lib/mobile/schedule.js +5 -0
  333. package/lib/mobile/send-continuation.js +52 -0
  334. package/lib/mobile/session-models.js +27 -31
  335. package/lib/mobile/session-send.js +12 -12
  336. package/lib/mobile/skill-import.js +338 -0
  337. package/lib/mobile/skill-store.js +524 -0
  338. package/lib/routes/mobile-api.js +42 -0
  339. package/package.json +7 -6
  340. package/skills/skill-authoring/SKILL.md +46 -0
  341. package/skills/skill-authoring/references/examples.md +82 -0
  342. package/skills/skill-authoring/references/frontmatter-and-fields.md +46 -0
  343. package/skills/skill-authoring/scripts/check_skill.py +167 -0
  344. package/skills/skill-authoring/scripts/manifest.json +3 -0
  345. package/knowledge/sites/bilibili.com.md +0 -27
  346. package/knowledge/sites/github.com.md +0 -27
  347. package/knowledge/sites/x.com.md +0 -28
  348. package/knowledge/sites/xiaohongshu.com.md +0 -27
  349. package/knowledge/sites/youtube.com.md +0 -28
  350. package/knowledge/sites/zhihu.com.md +0 -27
@@ -27,7 +27,9 @@ const CATALOG_FILE = 'catalog.json';
27
27
  const SLUG_PATTERN = /^[A-Za-z0-9._-]{1,64}$/;
28
28
  /** 整包上限:与开发包同一档,太重的包手机也不该整份拉。 */
29
29
  const MAX_PACKAGE_BYTES = 20 * 1024 * 1024;
30
- const MAX_PACKAGE_FILES = 200;
30
+ // 2026-11-08 从 200 抬到 400:整份网页前端做成的扩展(例如 magpie)光图标就一两百个、
31
+ // 按名字动态引用删不得,260 多个文件是常态;400 仍挡得住离谱的大包。
32
+ const MAX_PACKAGE_FILES = 400;
31
33
  /** 单个文件上限:一条 base64 响应要塞进手机内存,再大就不该走这条路。 */
32
34
  const MAX_FILE_BYTES = 8 * 1024 * 1024;
33
35
 
@@ -158,6 +160,12 @@ function listPackages(root) {
158
160
  apiVersion: textField(manifest.apiVersion, textField(manifest.api_version, '1')),
159
161
  releaseNotes: entry.releaseNotes,
160
162
  capabilities: capabilitiesOf(manifest.capabilities),
163
+ // 取数声明一起下发:手机上「已装记录」里若缺这一项(早期版本装下的包),App 用当前电脑上
164
+ // 这一份清单把老记录补上,页面才不会把 /api/… 解析成 file://… 而永远转圈。只下发声明,
165
+ // 不在这里做任何放行判断(真正的边界在 extension-data.js 的服务登记表里)。
166
+ hostData: manifest.hostData && typeof manifest.hostData === 'object' && !Array.isArray(manifest.hostData)
167
+ ? manifest.hostData
168
+ : null,
161
169
  size: acc.bytes,
162
170
  fileCount: acc.files.length,
163
171
  files: acc.files,
@@ -176,6 +184,37 @@ async function handleCatalog(_ctx, _req, res, root = catalogRoot()) {
176
184
  sendJson(res, 200, { ok: true, packages: listPackages(root) });
177
185
  }
178
186
 
187
+ /**
188
+ * GET /api/mobile/v1/extension/catalog/archive?slug=…(token)
189
+ *
190
+ * 整包一次下发:把一个已审核扩展的 **zip** 整个回成 base64。手机下载一个包、本地解包,
191
+ * 不再按文件逐个拉几百次(那正是「269 次往返、两三百 KB/s」的病根)。zip 由产包脚本放在
192
+ * `<root>/<slug>.zip`(与 slug 目录平级,不属于任何一包的文件清单)。
193
+ *
194
+ * 审核闸门与取单文件一致:没登记进 catalog.json 的 slug 一律 404。zip 体积按包上限兜住。
195
+ */
196
+ async function handleCatalogArchive(_ctx, req, res, root = catalogRoot()) {
197
+ const url = new URL(req.url || '/', 'http://x');
198
+ const slug = (url.searchParams.get('slug') || '').trim();
199
+ if (!slug || !SLUG_PATTERN.test(slug)) {
200
+ return fail(res, 400, 'bad-request', '需要合法的 slug');
201
+ }
202
+ if (!readCatalog(root).some((entry) => entry.slug === slug)) {
203
+ return fail(res, 404, 'package-missing', `审核清单里没有 ${slug} 这个包`);
204
+ }
205
+ const zipPath = path.join(root, `${slug}.zip`);
206
+ let raw;
207
+ try {
208
+ const stat = fs.statSync(zipPath);
209
+ if (stat.size > MAX_PACKAGE_BYTES) return fail(res, 413, 'package-too-large', `${slug}.zip 超过整包上限`);
210
+ raw = fs.readFileSync(zipPath);
211
+ } catch {
212
+ // 没有 zip 不算错误:老插件 / 没打 zip 的包照旧走逐文件那条(App 侧会回退)。
213
+ return fail(res, 404, 'archive-missing', `${slug} 没有整包 zip,走逐文件下载`);
214
+ }
215
+ sendJson(res, 200, { ok: true, slug, size: raw.length, sha256: sha256Hex(raw), content: raw.toString('base64') });
216
+ }
217
+
179
218
  /**
180
219
  * GET /api/mobile/v1/extension/catalog/file?slug=…&path=…(token)
181
220
  *
@@ -238,6 +277,7 @@ module.exports = {
238
277
  listPackages,
239
278
  handleCatalog,
240
279
  handleCatalogFile,
280
+ handleCatalogArchive,
241
281
  MAX_PACKAGE_BYTES,
242
282
  MAX_PACKAGE_FILES,
243
283
  MAX_FILE_BYTES,
@@ -0,0 +1,224 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * 扩展页「读宿主数据」那条通道的宿主侧(能力 `host.data`)。
5
+ *
6
+ * **为什么需要这一层**:扩展页是从手机本地 `file://` 加载的网页,它自己的 `fetch` 够不到
7
+ * 这台电脑上的服务——跨源、还要凭据。声明了 `hostData` 的扩展把命中的请求(例如 `/api/`)
8
+ * 交给 App,App 转到这里,由宿主**以自己人身份**去问目标服务,再把结果原样递回去。
9
+ *
10
+ * **这是通用通道,不是给 magpie 开的专口子**:`SERVICES` 是一张登记表,键是扩展包里声明的
11
+ * 服务名;要接一个新后台,在这一张表里登记一个服务即可,App 侧与协议一个字都不用改。
12
+ * 每个服务自己负责「怎么找到它、怎么带凭据」,以及「允许访问哪些路径前缀」(`allow`)——
13
+ * 一个服务只该被够到它自己的那部分路径,别拿它当跳板去够同端口的别处。
14
+ *
15
+ * **边界仍然收着**:只有配好对的手机(这条路由在 token 守卫之后)能调;只能到登记过的服务;
16
+ * 路径必须落在该服务的 `allow` 前缀内;不暴露 shell、系统设置、任意文件读写。请求体与响应体
17
+ * 都有限长,超了如实回错,不硬吃进内存。
18
+ *
19
+ * 纯 Node(http + 文件),无 cordis 依赖,测试可以直接 require 走假服务。
20
+ */
21
+
22
+ const http = require('node:http');
23
+
24
+ const { sendJson, readJsonBody } = require('../shared/http-util');
25
+ const { ensureWeb } = require('./magpie-web.js');
26
+
27
+ /** 请求体的上限:扩展页发来的是「一条请求」,几个 KB 的 JSON 足够,2MB 已经很宽松。 */
28
+ const MAX_REQUEST_BYTES = 2 * 1024 * 1024;
29
+ /** 响应体的上限:几百 KB 是常态(magpie 供应商列表就 324KB),给到 16MB 兜住异常大包。 */
30
+ const MAX_RESPONSE_BYTES = 16 * 1024 * 1024;
31
+ /** 一条代理请求的总时限。目标都是本机服务,慢到这份上就是它自己卡住了。 */
32
+ const REQUEST_TIMEOUT_MS = 20000;
33
+ /** 只放行这几种方法:读、写、删各一个,别的一律当非法。 */
34
+ const METHODS = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']);
35
+
36
+ /** 拿 cookie 的结果缓存(端口 + key → { cookie, at }),同一把 key 不必每发一个请求都换一次。 */
37
+ const cookieCache = new Map();
38
+ const COOKIE_TTL_MS = 60 * 1000;
39
+
40
+ /**
41
+ * 服务登记表。键=扩展包里 `hostData.service` 写的名字。
42
+ *
43
+ * 每个服务:
44
+ * - `allow`:允许被够到的路径前缀(都相对该服务的 origin);
45
+ * - `resolve()`:返回 `{ port, key }`,即这个服务此刻在哪、用什么凭据;
46
+ * - `cookie(port, key)`:把凭据换成请求要带的 `Cookie`(没有就返回空串)。
47
+ *
48
+ * magpie 先登记在这儿。以后接别的后台,在这里加一项即可。
49
+ */
50
+ const SERVICES = {
51
+ magpie: {
52
+ allow: ['/api/'],
53
+ async resolve() {
54
+ const entry = await ensureWeb({});
55
+ if (!entry || !entry.available) {
56
+ const error = new Error('service-unavailable');
57
+ error.code = 'service-unavailable';
58
+ error.reason = entry && entry.reason ? entry.reason : 'unavailable';
59
+ throw error;
60
+ }
61
+ return { port: entry.port, key: entry.key };
62
+ },
63
+ async cookie(port, key) {
64
+ return magpieCookie(port, key);
65
+ },
66
+ },
67
+ };
68
+
69
+ /**
70
+ * magpie 网页版的登录那一跳:`GET /?k=<key>` 会 303 并 `Set-Cookie`,把 key 换成一张 cookie。
71
+ * 后面每条 `/api/*` 都靠这张 cookie 认证。取不到就回空串(各接口会按 401 如实回,页面能看到)。
72
+ */
73
+ function magpieCookie(port, key) {
74
+ const cacheKey = `${port}:${key}`;
75
+ const hit = cookieCache.get(cacheKey);
76
+ if (hit && Date.now() - hit.at < COOKIE_TTL_MS) return Promise.resolve(hit.cookie);
77
+ return new Promise((resolve) => {
78
+ let done = false;
79
+ const finish = (cookie) => {
80
+ if (done) return;
81
+ done = true;
82
+ try { req.destroy(); } catch { /* 已经断了 */ }
83
+ if (cookie) cookieCache.set(cacheKey, { cookie, at: Date.now() });
84
+ resolve(cookie);
85
+ };
86
+ const req = http.request({
87
+ host: '127.0.0.1',
88
+ port,
89
+ path: `/?k=${encodeURIComponent(key)}`,
90
+ method: 'GET',
91
+ timeout: 5000,
92
+ }, (res) => {
93
+ const raw = res.headers['set-cookie'];
94
+ res.resume();
95
+ if (!raw || !raw.length) return finish('');
96
+ // 只取 `name=value` 那一段,属性(Path/HttpOnly…)不进请求头。
97
+ finish(String(raw[0]).split(';')[0]);
98
+ });
99
+ req.on('timeout', () => finish(''));
100
+ req.on('error', () => finish(''));
101
+ req.end();
102
+ });
103
+ }
104
+
105
+ /** 只要这几项响应头:页面解析正文够用了,别把一整套头也搬来搬去。 */
106
+ function pickHeaders(headers) {
107
+ const out = {};
108
+ if (headers && typeof headers['content-type'] === 'string') out['content-type'] = headers['content-type'];
109
+ return out;
110
+ }
111
+
112
+ /** 把一条请求原样代发到目标服务,收集状态码 / 头 / 正文(utf8)。失败回 0 + 一段错误正文。 */
113
+ function proxyRequest(port, cookie, method, path, body, contentType) {
114
+ return new Promise((resolve) => {
115
+ const headers = { accept: 'application/json, */*' };
116
+ if (cookie) headers.cookie = cookie;
117
+ const hasBody = typeof body === 'string' && body.length > 0;
118
+ if (hasBody) {
119
+ headers['content-type'] = contentType || 'application/json';
120
+ headers['content-length'] = Buffer.byteLength(body);
121
+ }
122
+ let done = false;
123
+ const finish = (result) => {
124
+ if (done) return;
125
+ done = true;
126
+ resolve(result);
127
+ };
128
+ const req = http.request({
129
+ host: '127.0.0.1',
130
+ port,
131
+ path,
132
+ method,
133
+ headers,
134
+ timeout: REQUEST_TIMEOUT_MS,
135
+ }, (res) => {
136
+ const chunks = [];
137
+ let total = 0;
138
+ res.on('data', (chunk) => {
139
+ total += chunk.length;
140
+ if (total > MAX_RESPONSE_BYTES) {
141
+ req.destroy();
142
+ finish({ status: 0, headers: {}, body: JSON.stringify({ code: 'response-too-large' }) });
143
+ return;
144
+ }
145
+ chunks.push(chunk);
146
+ });
147
+ res.on('end', () => finish({
148
+ status: res.statusCode || 0,
149
+ headers: pickHeaders(res.headers),
150
+ body: Buffer.concat(chunks).toString('utf8'),
151
+ }));
152
+ res.on('error', (error) => finish({ status: 0, headers: {}, body: JSON.stringify({ code: 'response-failed', message: String(error.message || error) }) }));
153
+ });
154
+ req.on('timeout', () => {
155
+ req.destroy();
156
+ finish({ status: 0, headers: {}, body: JSON.stringify({ code: 'timeout' }) });
157
+ });
158
+ req.on('error', (error) => finish({ status: 0, headers: {}, body: JSON.stringify({ code: 'request-failed', message: String(error.message || error) }) }));
159
+ if (hasBody) req.write(body);
160
+ req.end();
161
+ });
162
+ }
163
+
164
+ /** 一个失败但形状正常的回包:App 侧永远读 `{ status, headers, body }` 这套字段。 */
165
+ function failure(res, status, code, message) {
166
+ return sendJson(res, 200, {
167
+ ok: false,
168
+ status,
169
+ headers: {},
170
+ body: JSON.stringify({ code, message }),
171
+ code,
172
+ ...(message === undefined ? {} : { message }),
173
+ });
174
+ }
175
+
176
+ /**
177
+ * POST /api/mobile/v1/extension/data(token)
178
+ * body: { service, path, method?, body?, contentType? }
179
+ *
180
+ * 只有登记过的服务、落在它 `allow` 前缀内的路径才放行。目标服务的状态码 / 头 / 正文原样带回,
181
+ * 让扩展页像对待一个普通 Response 一样对待它(4xx / 5xx 也照原样,页面自己判断)。
182
+ */
183
+ async function handleExtensionData(_ctx, req, res) {
184
+ const body = await readJsonBody(req, MAX_REQUEST_BYTES);
185
+ if (!body || typeof body.service !== 'string' || typeof body.path !== 'string') {
186
+ return failure(res, 400, 'bad-request', '需要 { service, path }');
187
+ }
188
+ const service = body.service.trim();
189
+ const spec = SERVICES[service];
190
+ if (!spec) return failure(res, 404, 'service-unknown', `没有登记这个服务:${service}`);
191
+
192
+ const method = typeof body.method === 'string' ? body.method.trim().toUpperCase() : 'GET';
193
+ if (!METHODS.has(method)) return failure(res, 400, 'bad-method', `不支持的方法:${method}`);
194
+
195
+ const path = body.path.trim();
196
+ if (!path.startsWith('/') || path.startsWith('//')) return failure(res, 400, 'bad-path', '路径必须是本源内的绝对路径');
197
+ const pathname = path.split('?')[0];
198
+ if (spec.allow.length > 0 && !spec.allow.some((prefix) => pathname.startsWith(prefix))) {
199
+ return failure(res, 403, 'path-not-allowed', `这个服务不开放该路径:${pathname}`);
200
+ }
201
+
202
+ let target;
203
+ try {
204
+ target = await spec.resolve();
205
+ } catch (error) {
206
+ return failure(res, 503, 'service-unavailable', `${service} 现在不可用:${(error && error.reason) || 'unavailable'}`);
207
+ }
208
+
209
+ let cookie = '';
210
+ if (typeof spec.cookie === 'function') {
211
+ try { cookie = await spec.cookie(target.port, target.key); } catch { cookie = ''; }
212
+ }
213
+ const reqBody = typeof body.body === 'string' ? body.body : undefined;
214
+ const contentType = typeof body.contentType === 'string' ? body.contentType : undefined;
215
+ const result = await proxyRequest(target.port, cookie, method, path, reqBody, contentType);
216
+ sendJson(res, 200, { ok: true, status: result.status, headers: result.headers, body: result.body });
217
+ }
218
+
219
+ module.exports = {
220
+ handleExtensionData,
221
+ SERVICES,
222
+ MAX_REQUEST_BYTES,
223
+ MAX_RESPONSE_BYTES,
224
+ };
@@ -28,7 +28,9 @@ const DEV_ROOT_NAME = 'extension-dev';
28
28
  const SLUG_PATTERN = /^[A-Za-z0-9._-]{1,64}$/;
29
29
  /** 整包上限:这是「现读现看」的通道,不是分发管道,太重的包手机也不该整份拉。 */
30
30
  const MAX_PACKAGE_BYTES = 20 * 1024 * 1024;
31
- const MAX_PACKAGE_FILES = 200;
31
+ // 2026-11-08 从 200 抬到 400:整份网页前端做成的扩展(例如 magpie)光图标就一两百个、
32
+ // 按名字动态引用删不得,260 多个文件是常态;400 仍挡得住离谱的大包。
33
+ const MAX_PACKAGE_FILES = 400;
32
34
  /** 单个文件上限:一条 base64 响应要塞进手机内存,再大就该走别的路(发布到分发系统)。 */
33
35
  const MAX_FILE_BYTES = 8 * 1024 * 1024;
34
36
 
@@ -0,0 +1,252 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * 右栏文件浏览器的写操作:新建、删除、重命名。三条都只在「允许区」里动 —— 与
5
+ * `file-read.js` 同一条判据(宿主已登记的工作区 ∪ 当前会话的项目目录),不碰整台电脑。
6
+ *
7
+ * 与 `uploads.js` 的关系:那条负责「把手机上一份文件传进来」(分片、续传、加密信封),
8
+ * 落点同样受 `allowed-zone` 约束;这里是「已经在右栏里了,就地新建一个空文件 / 改个名字 /
9
+ * 删掉」。两者都不提供「任意路径写」,能力面没有放宽。
10
+ *
11
+ * 四条边界:
12
+ * 1. `path` 必须是绝对路径,且 `resolve` 之后的落点在允许区内。`..` 爬出去按最终落点算,
13
+ * 一律 403。判定与 `file-read.js` 共用 `allowed-zone.js`,不另抄一份。
14
+ * 2. **符号链接不逃逸**:只比字符串不够 —— `工作区/软链目录/x` 字面上在工作区里,物理上可能
15
+ * 指到工作区外。所以写之前先 `realpath` 目标所在的父目录,要求它落在某个「realpath 之后
16
+ * 的根」里。删除时先 `lstat`:目标是符号链接就拒绝(既不删链接本身、也绝不顺着删链接指向
17
+ * 的文件)。
18
+ * 3. 新建不能凭空替换别人的文件:目标已存在时默认 409,要 `overwrite: true` 才覆盖;覆盖前
19
+ * 仍然拒绝符号链接与目录。父目录必须已经存在(建目录走 `directory-browse.js` 的
20
+ * `directory/create`),这条不顺手递归 `mkdir` —— 少一层就少一处能爬出允许区的路径。
21
+ * 4. 不写 shell,全程 `node:fs/promises`。
22
+ */
23
+
24
+ const { writeFile, unlink, rename, lstat, realpath } = require('node:fs/promises');
25
+ const path = require('node:path');
26
+
27
+ const { sendJson, readJsonBody } = require('../shared/http-util');
28
+ const { allowedRoots, inAllowedZone } = require('./allowed-zone.js');
29
+
30
+ const FILE_PATH_MAX = 4096; // 绝对路径长度上限
31
+ const FILE_NAME_MAX = 255; // 单个文件名上限(多数文件系统同值)
32
+ const FILE_MAX_CONTENT_BYTES = 256 * 1024; // 新建时正文上限(文本文件;更大的走上传)
33
+
34
+ /** 400 / 403 / 404 / 409 等统一错误体。 */
35
+ function sendError(res, status, code, message) {
36
+ sendJson(res, status, { ok: false, error: { code, message } });
37
+ }
38
+
39
+ /** 解析并按允许区校验请求里的 `path`,同时确认「父目录的物理落点」也在允许区里。 */
40
+ async function preparePath(ctx, body, res) {
41
+ const declared = body && typeof body.path === 'string' ? body.path.trim() : '';
42
+ const sessionId = body && typeof body.sessionId === 'string' ? body.sessionId.trim() : '';
43
+
44
+ if (!declared) {
45
+ sendError(res, 400, 'bad-request', '需要 { path }(绝对路径)');
46
+ return null;
47
+ }
48
+ if (!path.isAbsolute(declared)) {
49
+ sendError(res, 400, 'bad-request', 'path 必须是绝对路径');
50
+ return null;
51
+ }
52
+ if (declared.length > FILE_PATH_MAX) {
53
+ sendError(res, 400, 'bad-request', 'path 太长');
54
+ return null;
55
+ }
56
+
57
+ const target = path.resolve(declared);
58
+
59
+ let roots;
60
+ try {
61
+ roots = await allowedRoots(ctx, sessionId);
62
+ } catch (error) {
63
+ // 宿主那句原话回给手机:这条路上「没取到」与「你没有这个目录」必须分得开。
64
+ sendFailure(res, error);
65
+ return null;
66
+ }
67
+ if (!inAllowedZone(roots, target)) {
68
+ sendError(res, 403, 'outside-allowed', '这个位置不在能改的范围里(只放行工作区目录和这条会话的项目目录)');
69
+ return null;
70
+ }
71
+
72
+ const parent = path.dirname(target);
73
+ let realParent;
74
+ try {
75
+ realParent = await realpath(parent);
76
+ } catch {
77
+ sendError(res, 404, 'parent-missing', '上级目录不在这台电脑上');
78
+ return null;
79
+ }
80
+ const realRoots = await realRootsOf(roots);
81
+ if (!inAllowedZone(realRoots, realParent)) {
82
+ sendError(res, 403, 'outside-allowed', '上级目录的物理位置不在允许区里(可能经过了符号链接)');
83
+ return null;
84
+ }
85
+ return { target, parent, roots };
86
+ }
87
+
88
+ /** 允许区每个根 realpath 之后的样子;解析不了的根直接跳过(跳过是缩小范围)。 */
89
+ async function realRootsOf(roots) {
90
+ const resolved = await Promise.all(roots.map((root) => realpath(root).catch(() => null)));
91
+ return resolved.filter((root) => typeof root === 'string');
92
+ }
93
+
94
+ /** 502:宿主工作区清单读取失败,把宿主原话转给手机。 */
95
+ function sendFailure(res, error) {
96
+ sendJson(res, 502, {
97
+ ok: false,
98
+ error: {
99
+ code: (error && error.code) || 'workspace-list-failed',
100
+ message: String((error && error.message) || error),
101
+ },
102
+ });
103
+ }
104
+
105
+ /**
106
+ * POST /api/mobile/v1/file/create
107
+ * body { path(绝对), sessionId?, content?, overwrite? }
108
+ */
109
+ async function handleFileCreate(ctx, req, res) {
110
+ const body = (await readJsonBody(req)) || {};
111
+ const prepared = await preparePath(ctx, body, res);
112
+ if (!prepared) return;
113
+ const { target } = prepared;
114
+
115
+ const content = typeof body.content === 'string' ? body.content : (body.content == null ? '' : null);
116
+ if (content === null) return sendError(res, 400, 'invalid-content', 'content 必须是字符串');
117
+ const bytes = Buffer.byteLength(content, 'utf8');
118
+ if (bytes > FILE_MAX_CONTENT_BYTES) {
119
+ return sendError(res, 413, 'too-large', '文件太大,手机这边不支持一次性新建这么大的文件');
120
+ }
121
+
122
+ let existing = null;
123
+ try {
124
+ existing = await lstat(target);
125
+ } catch {
126
+ existing = null;
127
+ }
128
+ if (existing) {
129
+ if (existing.isSymbolicLink()) return sendError(res, 400, 'symlink-refused', '这是一个符号链接,不在这里新建或覆盖');
130
+ if (existing.isDirectory()) return sendError(res, 400, 'not-a-file', '这条路径已经是一个目录');
131
+ if (!body.overwrite) return sendError(res, 409, 'file-exists', '同名文件已经存在');
132
+ }
133
+
134
+ try {
135
+ await writeFile(target, content, { encoding: 'utf8', flag: existing ? 'w' : 'wx' });
136
+ } catch (error) {
137
+ if (error && error.code === 'EEXIST') return sendError(res, 409, 'file-exists', '同名文件已经存在');
138
+ return sendError(res, 502, 'write-failed', `写文件失败:${String((error && error.message) || error)}`);
139
+ }
140
+
141
+ sendJson(res, 200, {
142
+ ok: true,
143
+ path: target,
144
+ created: !existing,
145
+ overwritten: !!existing,
146
+ bytes,
147
+ });
148
+ }
149
+
150
+ /**
151
+ * POST /api/mobile/v1/file/delete
152
+ * body { path(绝对), sessionId? }
153
+ */
154
+ async function handleFileDelete(ctx, req, res) {
155
+ const body = (await readJsonBody(req)) || {};
156
+ const prepared = await preparePath(ctx, body, res);
157
+ if (!prepared) return;
158
+ const { target } = prepared;
159
+
160
+ let info;
161
+ try {
162
+ info = await lstat(target);
163
+ } catch {
164
+ return sendError(res, 404, 'missing', '这个文件已经不在这台电脑上了');
165
+ }
166
+ if (info.isSymbolicLink()) {
167
+ // 既不移除链接、也绝不顺着链接去删它指向的文件。
168
+ return sendError(res, 400, 'symlink-refused', '这是一个符号链接,不删除');
169
+ }
170
+ if (!info.isFile()) {
171
+ return sendError(res, 400, 'not-a-file', '这条路径不是一个文件');
172
+ }
173
+
174
+ try {
175
+ await unlink(target);
176
+ } catch (error) {
177
+ return sendError(res, 502, 'delete-failed', `删除失败:${String((error && error.message) || error)}`);
178
+ }
179
+ sendJson(res, 200, { ok: true, path: target, deleted: true });
180
+ }
181
+
182
+ /** 单个路径段:非空、不是 . / ..、不含分隔符、不过长。 */
183
+ function validSegment(name) {
184
+ if (typeof name !== 'string') return '';
185
+ const text = name.trim();
186
+ if (!text || text === '.' || text === '..' || /[/\\]/.test(text)) return '';
187
+ if (text.length > FILE_NAME_MAX) return '';
188
+ return text;
189
+ }
190
+
191
+ /**
192
+ * POST /api/mobile/v1/file/rename
193
+ * body { path(绝对,原路径), name(新文件名,单段), sessionId?, overwrite? }
194
+ *
195
+ * 只做同目录改名(`name` 是单个路径段),不实现跨目录移动 —— 移动会把「源」和「目标」两个
196
+ * 落点都变成要各自校验的东西,改名只是在同一个已校验的父目录里换一个名字。
197
+ */
198
+ async function handleFileRename(ctx, req, res) {
199
+ const body = (await readJsonBody(req)) || {};
200
+ const prepared = await preparePath(ctx, body, res);
201
+ if (!prepared) return;
202
+ const { target, parent, roots } = prepared;
203
+
204
+ const name = validSegment(body.name);
205
+ if (!name) return sendError(res, 400, 'invalid-name', 'name 必须是单个文件名(不含路径分隔符)');
206
+
207
+ let source;
208
+ try {
209
+ source = await lstat(target);
210
+ } catch {
211
+ return sendError(res, 404, 'missing', '这个文件已经不在这台电脑上了');
212
+ }
213
+ if (source.isDirectory()) return sendError(res, 400, 'not-a-file', '这条路径是一个目录,不在这里改名');
214
+
215
+ const destination = path.join(parent, name);
216
+ if (!inAllowedZone(roots, destination)) {
217
+ // 正常到不了:name 是单段、parent 已校验。留着是防「以后有人把 name 放宽成相对路径」。
218
+ return sendError(res, 403, 'outside-allowed', '改名后的位置不在允许区里');
219
+ }
220
+ if (destination === target) {
221
+ return sendJson(res, 200, { ok: true, path: target, from: target, renamed: false });
222
+ }
223
+
224
+ let destinationInfo = null;
225
+ try {
226
+ destinationInfo = await lstat(destination);
227
+ } catch {
228
+ destinationInfo = null;
229
+ }
230
+ if (destinationInfo) {
231
+ if (!body.overwrite) return sendError(res, 409, 'file-exists', '目标名字已经有文件了');
232
+ if (destinationInfo.isSymbolicLink()) return sendError(res, 400, 'symlink-refused', '目标是一个符号链接,不覆盖');
233
+ if (destinationInfo.isDirectory()) return sendError(res, 400, 'not-a-file', '目标名字已经是一个目录');
234
+ }
235
+
236
+ try {
237
+ await rename(target, destination);
238
+ } catch (error) {
239
+ return sendError(res, 502, 'rename-failed', `改名失败:${String((error && error.message) || error)}`);
240
+ }
241
+ sendJson(res, 200, { ok: true, path: destination, from: target, renamed: true });
242
+ }
243
+
244
+ module.exports = {
245
+ FILE_PATH_MAX,
246
+ FILE_NAME_MAX,
247
+ FILE_MAX_CONTENT_BYTES,
248
+ validSegment,
249
+ handleFileCreate,
250
+ handleFileDelete,
251
+ handleFileRename,
252
+ };
@@ -296,6 +296,73 @@ function writtenPaths(content, toolResults) {
296
296
  }
297
297
 
298
298
  /** 从结构块里拼接纯文本(列表预览/标题回退用,仍只取 text 块)。 */
299
+ /**
300
+ * 系统内部注入的 source.kind:不画成「已注入上下文」行(与 App 侧 structuralItems 同一张名单)。
301
+ * 这些不是用户可读的普通上下文,一直隐藏;撤销「隐藏普通 Context 注入」不该把它们翻出来。
302
+ */
303
+ const HIDDEN_CONTEXT_SOURCE_KINDS = new Set(['tool-jobs', 'time-context', 'subagent-settled', 'runtime-context']);
304
+
305
+ /** 上下文注入的来源展示名(桌面 `ContextMessageNode.producer.label` 的等价读取)。 */
306
+ function contextProducerLabel(source) {
307
+ const stringsOf = (list, field) => {
308
+ if (!Array.isArray(list)) return '';
309
+ const out = [];
310
+ for (const entry of list) {
311
+ if (!entry || typeof entry !== 'object') continue;
312
+ const value = entry[field];
313
+ if (typeof value === 'string' && value !== '') out.push(value);
314
+ }
315
+ return out.join(', ');
316
+ };
317
+ if (source.kind === 'session-reference') return stringsOf(source.references, 'label');
318
+ if (source.kind === 'agent-instructions') return stringsOf(source.changes, 'path');
319
+ if (source.kind === 'skill-invocation' && typeof source.name === 'string') return source.name;
320
+ return typeof source.kind === 'string' ? source.kind : '';
321
+ }
322
+
323
+ /**
324
+ * 把一条 `user/message`(source.kind 非 user / 非 schedule / 非压缩检查点)折成上下文注入行。
325
+ * 不是这类消息、或没内容、或是内部系统注入时返回 null。
326
+ *
327
+ * 与 App 侧 `structuralItems.contextItemFromEvent` 同一判据、同一组字段 —— 直播与历史两侧口径
328
+ * 必须一致,否则同一条注入在重进会话后消失或换样。
329
+ */
330
+ function contextItemOf(msg, e) {
331
+ const src = msg.source;
332
+ if (!src || typeof src !== 'object') return null;
333
+ const kind = src.kind;
334
+ if (kind === undefined || kind === 'user') return null;
335
+ if (kind === 'schedule') return null;
336
+ if (kind === 'plugin' && src.plugin === 'compact') return null;
337
+ if (typeof kind === 'string' && HIDDEN_CONTEXT_SOURCE_KINDS.has(kind)) return null;
338
+ const content = Array.isArray(msg.content) ? msg.content : [];
339
+ const texts = [];
340
+ const added = [];
341
+ const removed = [];
342
+ for (const block of content) {
343
+ if (!block || typeof block !== 'object') continue;
344
+ if (block.type === 'text' && typeof block.text === 'string') texts.push(block.text);
345
+ else if (block.type === 'tool-addition' && typeof block.toolName === 'string') added.push(block.toolName);
346
+ else if (block.type === 'tool-removal' && typeof block.toolName === 'string') removed.push(block.toolName);
347
+ }
348
+ const text = texts.join('');
349
+ const hasTools = added.length > 0 || removed.length > 0;
350
+ if (text.trim() === '' && !hasTools) return null;
351
+ const label = contextProducerLabel(src);
352
+ return {
353
+ id: String(msg.id),
354
+ kind: 'context',
355
+ text: '',
356
+ seq: e.seq,
357
+ time: e.time,
358
+ ...(text !== '' ? { contextText: text } : {}),
359
+ ...(label !== '' ? { contextLabel: label } : {}),
360
+ ...(kind === 'session-reference' ? { contextRecall: true } : {}),
361
+ ...(hasTools ? { contextTools: { added, removed } } : {}),
362
+ ...(e.data && typeof e.data.turn === 'number' ? { turn: e.data.turn } : {}),
363
+ };
364
+ }
365
+
299
366
  function blocksToText(blocks) {
300
367
  return blocks.filter((b) => b.type === 'text' && typeof b.text === 'string').map((b) => b.text).join('\n');
301
368
  }
@@ -728,8 +795,11 @@ function deriveMessages(events, sessionId, lookbackEvents) {
728
795
  continue;
729
796
  }
730
797
  if (src && typeof src === 'object' && src.kind !== 'user') {
731
- // 上下文注入/召回(session-reference→召回,其余 kind→注入):桌面 `isVisibleChatNode`
732
- // 只放行携带 tool-addition/tool-removal 的上下文节点,这类提示一律不显示 —— 不下发。
798
+ // 上下文注入 / 召回(session-reference→召回,其余 kind→注入)。上游 2026-11-03 撤销了
799
+ // 「隐藏普通 Context 注入」,这类节点现在要**下发**成一条 kind:'context' 的折叠行;
800
+ // 系统内部注入(tool-jobs / time-context 等)仍由 contextItemOf 挡掉,别漏成用户气泡。
801
+ const contextItem = contextItemOf(msg, e);
802
+ if (contextItem) out.push(contextItem);
733
803
  continue;
734
804
  }
735
805
  // 普通用户消息(source 缺失或 kind==='user')沿用原折叠。
@@ -1023,10 +1093,16 @@ function deriveMessages(events, sessionId, lookbackEvents) {
1023
1093
  */
1024
1094
  const ENTRY_OUTCOME_TOOLS = new Set(['schedule_create', 'exit_plan_mode', 'extension_present']);
1025
1095
 
1026
- /** 一轮的起点:用户自己发的消息,或定时任务唤醒的那一行。 */
1096
+ /**
1097
+ * 一轮的起点:用户自己发的消息、定时任务唤醒的那一行,或一条上下文注入行。
1098
+ *
1099
+ * 上下文注入也算起点:会话开头那串 agent-instructions 之类就发生在第一条用户消息之前,
1100
+ * 不认它当起点时入口窗口会把这「半组」当成没有起点的残段整组裁掉(thinning 那条「凭空冒出
1101
+ * 的发言」规则),注入行就永远看不到。认了之后它自带起点、照常保留。
1102
+ */
1027
1103
  function isEntryTurnOpener(item) {
1028
1104
  return Boolean(item) && typeof item === 'object'
1029
- && (item.role === 'user' || item.kind === 'wake');
1105
+ && (item.role === 'user' || item.kind === 'wake' || item.kind === 'context');
1030
1106
  }
1031
1107
 
1032
1108
  function thinEntryMessages(items, options = {}) {