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
@@ -23,6 +23,7 @@ const binding = require('./binding.js');
23
23
  const pacing = require('./pacing.js');
24
24
  const routeHint = require('./route-hint.js');
25
25
  const knowledge = require('../phone-knowledge.js');
26
+ const access = require('./access.js');
26
27
 
27
28
  /** 设备标题:机型优先,没有机型才用 deviceId 的短尾巴。与凭据面板同一口径。 */
28
29
  function deviceLabel(row) {
@@ -177,7 +178,7 @@ async function handlePhoneKnowledge(deps, _ctx, req, res) {
177
178
  name: site.name,
178
179
  display: site.display,
179
180
  description: site.description,
180
- sites: site.sites,
181
+ sites: site.domains,
181
182
  aliases: site.aliases,
182
183
  }));
183
184
  sendJson(res, 200, { ok: true, apps, sites, matched: matched ? matched.name : '' });
@@ -219,6 +220,30 @@ async function handlePhonePacing(_deps, _ctx, req, res) {
219
220
  }
220
221
  }
221
222
 
223
+ /**
224
+ * 逐 App 访问控制:GET 读全部开关行,POST 改一个 App 的一行。
225
+ *
226
+ * 只是本地开关,不往手机上发动作;名单落 `$DSH_HOME/storages/mobile-plugin/phone-app-access.json`,
227
+ * 原生 `AppAccess.kt` 执行前按同一份名单判。包名形状在这里先校验,挡住把路径/命令塞进来。
228
+ */
229
+ async function handlePhoneAppAccess(_deps, _ctx, req, res) {
230
+ if (req.method === 'GET' || !req.method) {
231
+ return sendJson(res, 200, { ok: true, apps: access.rows() });
232
+ }
233
+ const body = await readJsonBody(req);
234
+ const packageName = body && typeof body.packageName === 'string' ? body.packageName : '';
235
+ const patch = {
236
+ blocked: typeof body.blocked === 'boolean' ? body.blocked : undefined,
237
+ shareAllowed: typeof body.shareAllowed === 'boolean' ? body.shareAllowed : undefined,
238
+ };
239
+ if (patch.blocked === undefined && patch.shareAllowed === undefined) {
240
+ return sendJson(res, 400, { ok: false, error: 'missing-flags' });
241
+ }
242
+ const result = access.update(packageName, patch);
243
+ if (!result.ok) return sendJson(res, 400, { ok: false, error: result.error });
244
+ sendJson(res, 200, { ok: true, apps: access.rows() });
245
+ }
246
+
222
247
  /**
223
248
  * 造出路由表要的那组 handler。
224
249
  *
@@ -235,6 +260,7 @@ function createPhoneRoutes(deps) {
235
260
  knowledge: (ctx, req, res) => handlePhoneKnowledge(deps, ctx, req, res),
236
261
  routes: (ctx, req, res) => handlePhoneRoutes(deps, ctx, req, res),
237
262
  pacing: (ctx, req, res) => handlePhonePacing(deps, ctx, req, res),
263
+ appAccess: (ctx, req, res) => handlePhoneAppAccess(deps, ctx, req, res),
238
264
  };
239
265
  }
240
266
 
@@ -246,6 +272,7 @@ module.exports = {
246
272
  handlePhoneKnowledge,
247
273
  handlePhoneRoutes,
248
274
  handlePhonePacing,
275
+ handlePhoneAppAccess,
249
276
  listDevices,
250
277
  describeDevice,
251
278
  deviceLabel,
@@ -75,6 +75,22 @@ const ACTIONS = new Map([
75
75
  ['screen.wait', 30_000],
76
76
  ['gesture', 20_000],
77
77
  ['app.link', 20_000],
78
+ // 结构化:提取 / 查询 / 滚动查找 / 多屏采集。
79
+ ['extract', 20_000],
80
+ ['query', 20_000],
81
+ ['find', 60_000],
82
+ ['collect', 60_000],
83
+ // 滚轮选择器一次转到位。
84
+ ['wheel.set', 25_000],
85
+ // 分享一段文字到指定 App。
86
+ ['share.text', 20_000],
87
+ // 系统动作:闹钟 / 计时器 / 看闹钟 / 日历插入(标准 Intent)。
88
+ ['system.alarm', 20_000],
89
+ ['system.timer', 20_000],
90
+ ['system.show_alarms', 20_000],
91
+ ['system.calendar_insert', 20_000],
92
+ // 任务结束时某个 App 的去留标记(只记下,不当场关)。
93
+ ['app.after_task', 20_000],
78
94
  ]);
79
95
 
80
96
  /**
@@ -173,6 +189,13 @@ function createPhoneService({ authorized, onLog } = {}) {
173
189
  }
174
190
  }
175
191
  total += rtt * 2 * steps.length;
192
+ // 手机侧还要按 pacing 在步与步之间停一下(standard 约每步 0.5–1.2s、新屏更久;careful/human 更久),
193
+ // 这段等待是在手机侧真睡掉的、要占同一笔预算,不给它留就会「批跑到一半被自己的停顿撞上超时」。
194
+ const pace = args && args.pacing;
195
+ if (pace && typeof pace === 'object') {
196
+ const careful = pace.place === 'careful' || pace.mode === 'human';
197
+ total += (careful ? 5_000 : 1_500) * steps.length;
198
+ }
176
199
  return Math.round(Math.min(Math.max(total, 5_000), 180_000));
177
200
  }
178
201
 
@@ -0,0 +1,271 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * 站点知识里的只读脚本:声明格式、清单与调用参数校验。
5
+ *
6
+ * 一条命令就是站点目录下的 `scripts/<name>.js`,文件以一段块注释开头(MARK 见下),后面是
7
+ * async 函数体,参数从 `args` 取。name 必须与文件名一致。头部一行一项:
8
+ *
9
+ * - `name: list_jobs`
10
+ * - `description: 列出当前搜索结果里的职位`
11
+ * - `arg: limit integer - 最多几条`
12
+ * - `arg: query string required - 只保留标题含这段字的`
13
+ * - `returns: [{title, company, salary, url}]`
14
+ * - `readOnly: true`(可选;站点命令只能是 true)
15
+ *
16
+ * 真正的页面内执行由原生侧做(真实页面、读取世界、同源只读),这个模块**不碰网络、不执行
17
+ * 任何代码**,只做两件事:把站点目录里的命令声明列出来,以及在调用前按声明校验参数。
18
+ *
19
+ * ## 执行契约(原生侧遵守,这里只定义,不在 JS 里强制执行)
20
+ *
21
+ * - 只读:脚本只能读 DOM 与页面已加载的数据,不得点击、输入、导航或改动页面状态。
22
+ * - 同源:只允许当前页面同源的 GET,禁止跨域请求。
23
+ * - 禁网:除同源只读外不得主动发起网络请求,不发 POST、不调站点的写接口。
24
+ * - 密码打码:返回值若来自密码 / 验证码等敏感输入,一律以 `***` 代替。
25
+ */
26
+
27
+ const fs = require('node:fs');
28
+ const path = require('node:path');
29
+
30
+ /** 命令头的起始标记。与 Mika 的 `/* mika-command` 同构,换成我们自己的名字。 */
31
+ const COMMAND_MARK = '/* dsh-command';
32
+ /** 命令名的字符集:小写字母开头,字母数字下划线,至多 40 个。 */
33
+ const NAME_RE = /^[a-z][a-z0-9_]{0,39}$/;
34
+ /** 参数类型白名单。 */
35
+ const TYPES = ['string', 'integer', 'number', 'boolean'];
36
+ /** 说明文字上限。 */
37
+ const DESCRIPTION_LIMIT = 120;
38
+ /** 脚本正文上限(字符)。太长说明该拆,不放进一块。 */
39
+ const BODY_LIMIT = 12_000;
40
+
41
+ /**
42
+ * 站点知识里 `commands` 的字段规格(给写知识的人看,也给校验用)。
43
+ * 每条命令恒定只读。
44
+ */
45
+ const COMMAND_SCHEMA = {
46
+ name: '小写字母开头,字母 / 数字 / 下划线,至多 40,必须与文件名 scripts/<name>.js 一致',
47
+ description: `一句话说清这条命令取什么,至多 ${DESCRIPTION_LIMIT} 字`,
48
+ readOnly: '恒为 true:站点命令只能读,不得写页面或改状态',
49
+ returns: '返回的字段说明,例如 [{title, url}]',
50
+ args: '[{ name, type: string|integer|number|boolean, required?, description? }]',
51
+ };
52
+
53
+ /** 执行契约的文字版(给知识注入与原生实现对齐用)。 */
54
+ const EXECUTION_CONTRACT = {
55
+ readOnly: true,
56
+ sameOrigin: true,
57
+ network: false,
58
+ redactPasswords: true,
59
+ summary:
60
+ '只读、同源、禁网、密码打码:脚本只读当前页面同源数据,不点击不输入不改页面,'
61
+ + '不主动发网络请求(含写接口),敏感输入一律打码后返回。',
62
+ };
63
+
64
+ const ARG_RE = /^([a-z][A-Za-z0-9_]{0,39})\s+(string|integer|number|boolean)(\s+required)?(?:\s+-\s+(.+))?$/;
65
+
66
+ /** 去掉 BOM、统一换行、去掉开头空白,便于认头部。 */
67
+ function clean(text) {
68
+ return String(text || '').replace(/^\uFEFF/, '').replace(/\r\n/g, '\n').replace(/^\s+/, '');
69
+ }
70
+
71
+ /** 这段文本是不是一条站点命令(以命令头开头)。 */
72
+ function isCommand(text) {
73
+ return clean(text).startsWith(COMMAND_MARK);
74
+ }
75
+
76
+ /**
77
+ * 解析一条站点命令文件。
78
+ *
79
+ * 返回 `{ decl, body, errors }`:`decl` 只在 `errors` 为空时给出,形状为
80
+ * `{ name, description, readOnly: true, returns, args, file }`;`body` 是头部之后的脚本正文。
81
+ *
82
+ * @param {string} file 站点目录下的相对路径,如 `scripts/list_jobs.js`
83
+ * @param {string} text 文件全文
84
+ */
85
+ function parseCommand(file, text) {
86
+ const t = clean(text);
87
+ if (!t.startsWith(COMMAND_MARK)) {
88
+ return { decl: null, body: '', errors: [`文件要以 \`${COMMAND_MARK}\` 开头`] };
89
+ }
90
+ const end = t.indexOf('*/');
91
+ if (end < 0) return { decl: null, body: '', errors: ['命令头没有用 `*/` 结束'] };
92
+
93
+ const body = t.slice(end + 2).trim();
94
+ const errors = [];
95
+ let name = null;
96
+ let description = null;
97
+ let returns = null;
98
+ let readOnly = null;
99
+ const args = [];
100
+
101
+ for (const raw of t.slice(COMMAND_MARK.length, end).split('\n')) {
102
+ // 头部每行可能带一个多余的 `*` 前缀,去掉它再按 key: value 解析。
103
+ const line = raw.trim().replace(/^\*\s?/, '');
104
+ if (!line) continue;
105
+ const idx = line.indexOf(':');
106
+ if (idx <= 0) {
107
+ errors.push(`头部这一行看不懂:${line}`);
108
+ continue;
109
+ }
110
+ const key = line.slice(0, idx).trim();
111
+ const value = line.slice(idx + 1).trim();
112
+ switch (key) {
113
+ case 'name':
114
+ name = value;
115
+ break;
116
+ case 'description':
117
+ description = value;
118
+ break;
119
+ case 'returns':
120
+ returns = value;
121
+ break;
122
+ case 'readOnly':
123
+ case 'readonly':
124
+ readOnly = value.toLowerCase() === 'true';
125
+ break;
126
+ case 'arg':
127
+ {
128
+ const m = ARG_RE.exec(value);
129
+ if (m) {
130
+ args.push({
131
+ name: m[1],
132
+ type: m[2],
133
+ required: Boolean(m[3]),
134
+ description: (m[4] || '').trim(),
135
+ });
136
+ } else {
137
+ errors.push(`arg 要写作「名字 类型 [required] [- 说明]」,类型是 ${TYPES.join('/')}:${value}`);
138
+ }
139
+ }
140
+ break;
141
+ default:
142
+ errors.push(`头部有不认得的键 ${key}`);
143
+ }
144
+ }
145
+
146
+ const base = path.basename(file).replace(/\.js$/i, '');
147
+ if (name == null) errors.push('缺少 name');
148
+ else if (!NAME_RE.test(name)) errors.push('name 只能用小写字母、数字与下划线,以字母开头,至多 40 个字符');
149
+ else if (name !== base) errors.push(`name(${name})必须与文件名(${base}.js)一致`);
150
+ if (description == null || description === '') errors.push('缺少 description');
151
+ else if ([...description].length > DESCRIPTION_LIMIT) errors.push(`description 超过 ${DESCRIPTION_LIMIT} 字`);
152
+ if (returns == null || returns === '') errors.push('缺少 returns');
153
+ if (readOnly === false) errors.push('站点命令只能只读,readOnly 不能是 false');
154
+ const dupNames = args.map((a) => a.name).filter((n, i, arr) => arr.indexOf(n) !== i);
155
+ for (const dup of new Set(dupNames)) errors.push(`arg ${dup} 重复`);
156
+ if (body === '') errors.push('脚本正文为空');
157
+ if (body.length > BODY_LIMIT) errors.push(`脚本正文 ${body.length} 字,超过 ${BODY_LIMIT}`);
158
+
159
+ const decl = errors.length === 0
160
+ ? { name, description, readOnly: true, returns, args, file }
161
+ : null;
162
+ return { decl, body, errors };
163
+ }
164
+
165
+ /** 值是否符合声明的类型。 */
166
+ function fitsType(value, type) {
167
+ switch (type) {
168
+ case 'string':
169
+ return typeof value === 'string';
170
+ case 'boolean':
171
+ return typeof value === 'boolean';
172
+ case 'integer':
173
+ return typeof value === 'number' && Number.isInteger(value);
174
+ case 'number':
175
+ return typeof value === 'number' && Number.isFinite(value);
176
+ default:
177
+ return false;
178
+ }
179
+ }
180
+
181
+ /**
182
+ * 按声明校验一次调用的参数。没问题返回 null,有问题返回一句中文说明。
183
+ *
184
+ * @param {object} decl parseCommand 给出的命令声明
185
+ * @param {object} args 模型 / 调用方给的参数(名字 → 值)
186
+ */
187
+ function validateCommand(decl, args) {
188
+ if (!decl || typeof decl !== 'object') return '命令声明无效';
189
+ const given = args && typeof args === 'object' && !Array.isArray(args) ? args : {};
190
+ const declared = new Map((decl.args || []).map((a) => [a.name, a]));
191
+ for (const key of Object.keys(given)) {
192
+ if (!declared.has(key)) {
193
+ const takes = (decl.args || []).map((a) => a.name).join('、') || '无参数';
194
+ return `'${key}' 不是 ${decl.name} 的参数;它接受 ${takes}`;
195
+ }
196
+ }
197
+ for (const arg of decl.args || []) {
198
+ const value = given[arg.name];
199
+ const missing = value === undefined || value === null;
200
+ if (missing) {
201
+ if (arg.required) return `${decl.name} 需要参数 '${arg.name}'(${arg.type})`;
202
+ continue;
203
+ }
204
+ if (!fitsType(value, arg.type)) return `参数 '${arg.name}' 必须是 ${arg.type}`;
205
+ }
206
+ return null;
207
+ }
208
+
209
+ /**
210
+ * 读一个站点目录下的 `scripts/*.js`,把带命令头的解析成声明清单。
211
+ *
212
+ * 返回 `{ commands, errors }`:`commands` 是合法声明(按名字排序),`errors` 收下坏掉的命令
213
+ * 及其具体问题。目录不存在或某文件读坏了都只跳过,不抛。
214
+ *
215
+ * @param {string} siteDir 站点知识目录,如 `knowledge/sites/github.com`
216
+ */
217
+ function listCommands(siteDir) {
218
+ const dir = path.join(String(siteDir || ''), 'scripts');
219
+ let entries;
220
+ try {
221
+ entries = fs.readdirSync(dir, { withFileTypes: true });
222
+ } catch {
223
+ return { commands: [], errors: [] };
224
+ }
225
+ const commands = [];
226
+ const errors = [];
227
+ for (const entry of entries) {
228
+ if (!entry.isFile() || !entry.name.endsWith('.js')) continue;
229
+ let text;
230
+ try {
231
+ text = fs.readFileSync(path.join(dir, entry.name), 'utf8');
232
+ } catch {
233
+ continue;
234
+ }
235
+ if (!isCommand(text)) continue;
236
+ const rel = `scripts/${entry.name}`;
237
+ const parsed = parseCommand(rel, text);
238
+ if (parsed.decl) commands.push(parsed.decl);
239
+ else errors.push({ file: rel, errors: parsed.errors });
240
+ }
241
+ commands.sort((a, b) => a.name.localeCompare(b.name));
242
+ return { commands, errors };
243
+ }
244
+
245
+ /** 把一条命令声明写成知识块里的一行(供站点正文注入)。 */
246
+ function formatCommandLine(decl) {
247
+ const sig = (decl.args || [])
248
+ .map((a) => `${a.name}${a.required ? '' : '?'}: ${a.type}`)
249
+ .join(', ');
250
+ const notes = (decl.args || [])
251
+ .filter((a) => a.description)
252
+ .map((a) => `${a.name} ${a.description}`)
253
+ .join('; ');
254
+ return `- ${decl.name}(${sig}): ${decl.description}。返回 ${decl.returns}`
255
+ + (notes ? `。参数:${notes}` : '');
256
+ }
257
+
258
+ module.exports = {
259
+ COMMAND_MARK,
260
+ NAME_RE,
261
+ TYPES,
262
+ DESCRIPTION_LIMIT,
263
+ BODY_LIMIT,
264
+ COMMAND_SCHEMA,
265
+ EXECUTION_CONTRACT,
266
+ isCommand,
267
+ parseCommand,
268
+ validateCommand,
269
+ listCommands,
270
+ formatCommandLine,
271
+ };
@@ -0,0 +1,201 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * 压缩时的**结构化步骤摘要**:目标 / 已确认事实(按种类去重)/ 进展行 / 更早结局计数 / 最后打算。
5
+ *
6
+ * 全从原话、工具结果与模型说过的话里**确定性**地取,**不调模型**。理由和 Mika 一样直白:
7
+ * 压缩每次都要重算摘要,调一次模型既多一次往返,同样的历史还可能得出不同的摘要——请求前缀
8
+ * 就不稳,缓存命中率跟着掉。摘要稳 = 前缀稳 = 省钱。
9
+ *
10
+ * 各段都有上限(事实每类 `MAX_FACT_VALUES` 个、进展 `MAX_PROGRESS` 行、更早结局 `MAX_EARLIER_KINDS`
11
+ * 种、单项截到 `CLIP` 字),步数再多摘要也不会无限变长。
12
+ *
13
+ * 输入沿用直播 / 历史清单的形状(`{ key, step }` 或裸 step 数组,step 里带
14
+ * `kind` / `name` / `summary` / `state`,可选 `args` / 结构化事实字段)。`render()` 产出的
15
+ * 是给模型读的确定性文本,可直接当压缩时折叠段的头。
16
+ */
17
+
18
+ const MAX_FACT_VALUES = 4;
19
+ const MAX_PROGRESS = 15;
20
+ const MAX_EARLIER_KINDS = 5;
21
+ const CLIP = 100;
22
+ const OTHER = '其他';
23
+
24
+ /** 结果里能当事实记下的短字段 → 事实种类(对齐 Mika StepSummary 的 FACT_KEYS)。 */
25
+ const FACT_KEYS = [
26
+ ['currentApp', '去过的应用'],
27
+ ['app', '去过的应用'],
28
+ ['packageName', '去过的应用'],
29
+ ['url', '打开的页面'],
30
+ ['path', '读写的文件'],
31
+ ['file_path', '读写的文件'],
32
+ ];
33
+
34
+ function clip(text, max = CLIP) {
35
+ const one = String(text == null ? '' : text).replace(/\s+/g, ' ').trim();
36
+ if (one.length <= max) return one;
37
+ return `${one.slice(0, max - 1)}…`;
38
+ }
39
+
40
+ function normalizeEntry(entry) {
41
+ if (!entry || typeof entry !== 'object') return {};
42
+ return entry.step && typeof entry.step === 'object'
43
+ ? { key: entry.key, ...entry.step }
44
+ : entry;
45
+ }
46
+
47
+ function toStepList(run) {
48
+ const list = Array.isArray(run)
49
+ ? run
50
+ : (run && typeof run === 'object' && Array.isArray(run.steps) ? run.steps : null);
51
+ return list ? list.map(normalizeEntry) : [];
52
+ }
53
+
54
+ function labelOf(step) {
55
+ if (!step || typeof step !== 'object') return '';
56
+ if (typeof step.summary === 'string' && step.summary.trim() !== '') return clip(step.summary);
57
+ const args = step.args && typeof step.args === 'object' ? step.args : {};
58
+ for (const key of ['target', 'text', 'description', 'query', 'command', 'file_path', 'path', 'ref', 'packageName', 'url']) {
59
+ if (typeof args[key] === 'string' && args[key].trim() !== '') return clip(args[key]);
60
+ }
61
+ return clip(step.name || step.kind || '');
62
+ }
63
+
64
+ /** 状态 → 进展行末尾的结局词。 */
65
+ function outcomeOf(step) {
66
+ const state = step && typeof step.state === 'string' ? step.state : '';
67
+ if (state === 'ok') return '成功';
68
+ if (state === 'error') return '失败';
69
+ if (state === 'stopped') return '中断';
70
+ if (state === 'running') return '未完成';
71
+ return '完成';
72
+ }
73
+
74
+ /** 工具名 → 从它这一步能确认的事实(路径 / 网址 / 应用)。 */
75
+ function factsFromStep(step, out) {
76
+ if (!step || typeof step !== 'object') return;
77
+ // 优先用步骤/结果里已经结构化好的字段。
78
+ for (const [key, kind] of FACT_KEYS) {
79
+ const value = step[key] != null ? step[key]
80
+ : (step.result && typeof step.result === 'object' ? step.result[key] : undefined);
81
+ if (typeof value === 'string' && value.trim() !== '') {
82
+ pushFact(out, kind, value.trim());
83
+ }
84
+ }
85
+ // 退一步:从工具摘要里认(读写/编辑给的是路径,web_fetch 给的是网址)。
86
+ const args = step.args && typeof step.args === 'object' ? step.args : {};
87
+ if (step.name === 'web_fetch' && typeof args.url === 'string') pushFact(out, '打开的页面', args.url.trim());
88
+ if (['read', 'read_image', 'write', 'edit', 'str_replace_editor'].includes(step.name)) {
89
+ const path = args.file_path || args.path;
90
+ if (typeof path === 'string' && path.trim() !== '') pushFact(out, '读写的文件', path.trim());
91
+ }
92
+ if (step.name === 'phone_open_app' && typeof args.packageName === 'string') pushFact(out, '去过的应用', args.packageName.trim());
93
+ }
94
+
95
+ function pushFact(out, kind, value) {
96
+ const clipped = clip(value);
97
+ if (!clipped) return;
98
+ const list = out[kind] || [];
99
+ const next = list.filter((v) => v !== clipped).concat(clipped).slice(-MAX_FACT_VALUES);
100
+ out[kind] = next;
101
+ }
102
+
103
+ /**
104
+ * 生成结构化步骤摘要。
105
+ *
106
+ * @param {object|Array} steps 步骤流(见文件头)
107
+ * @param {{goal?:string, intent?:string}} [opts] 目标;`intent` 不传就从最后一段有内容的思考取
108
+ * @returns {{
109
+ * goal:string, facts:Object<string,string[]>, progress:string[],
110
+ * earlier:Object<string,number>, intent:string,
111
+ * render:(header?:string)=>string
112
+ * }}
113
+ */
114
+ function summarizeSteps(steps, opts) {
115
+ const list = toStepList(steps);
116
+ const goal = opts && typeof opts.goal === 'string' ? clip(opts.goal, 200) : '';
117
+
118
+ const facts = {};
119
+ const progress = [];
120
+ let lastThought = '';
121
+
122
+ for (const step of list) {
123
+ if (!step || typeof step !== 'object') continue;
124
+ if (step.kind === 'think') {
125
+ const label = labelOf(step);
126
+ if (label) lastThought = label;
127
+ continue;
128
+ }
129
+ const label = labelOf(step);
130
+ if (!label && !step.name) continue;
131
+ factsFromStep(step, facts);
132
+ progress.push(`${label || step.name} → ${outcomeOf(step)}`);
133
+ }
134
+
135
+ // 进展行超上限:最早的几行并进「更早结局计数」。
136
+ const earlier = {};
137
+ let p = progress;
138
+ while (p.length > MAX_PROGRESS) {
139
+ const out = p[0].substring(p[0].lastIndexOf('→ ') + 2).trim() || OTHER;
140
+ const key = (out in earlier || Object.keys(earlier).length < MAX_EARLIER_KINDS) ? out : OTHER;
141
+ earlier[key] = (earlier[key] || 0) + 1;
142
+ p = p.slice(1);
143
+ }
144
+
145
+ const intent = opts && typeof opts.intent === 'string' && opts.intent.trim() !== ''
146
+ ? clip(opts.intent)
147
+ : lastThought;
148
+
149
+ const summary = {
150
+ goal,
151
+ facts,
152
+ progress: p,
153
+ earlier,
154
+ intent,
155
+ };
156
+ summary.render = (header) => render(summary, header);
157
+ return summary;
158
+ }
159
+
160
+ /** 把摘要渲染成给模型读的确定性文本(无时间戳,保证前缀稳定)。 */
161
+ function render(summary, header) {
162
+ const lines = [];
163
+ lines.push(typeof header === 'string' && header.trim() !== '' ? header.trim() : '【本轮进度摘要】');
164
+ lines.push('只有最新的用户消息是当前任务,工具仍然可用;更早的用户消息只作参考。');
165
+ lines.push(`目标:${summary.goal || '(未记录)'}`);
166
+ lines.push('已确认:');
167
+ const kinds = Object.keys(summary.facts);
168
+ if (kinds.length === 0) {
169
+ lines.push('- (暂无)');
170
+ } else {
171
+ for (const kind of kinds) lines.push(`- ${kind}:${summary.facts[kind].join('、')}`);
172
+ }
173
+ lines.push('进展:');
174
+ const earlierKeys = Object.keys(summary.earlier);
175
+ if (earlierKeys.length > 0) {
176
+ const total = earlierKeys.reduce((sum, k) => sum + summary.earlier[k], 0);
177
+ lines.push(`- 更早 ${total} 步:${earlierKeys.map((k) => `${k} ${summary.earlier[k]}`).join(',')}`);
178
+ }
179
+ if (summary.progress.length === 0) {
180
+ lines.push('- (暂无)');
181
+ } else {
182
+ for (const line of summary.progress) lines.push(`- ${line}`);
183
+ }
184
+ lines.push(summary.intent
185
+ ? `下一步:从下面保留的最近几步接着做(折叠前的最后打算:${summary.intent})`
186
+ : '下一步:从下面保留的最近几步接着做');
187
+ return lines.join('\n');
188
+ }
189
+
190
+ module.exports = {
191
+ summarizeSteps,
192
+ render,
193
+ // 纯函数与小常量:
194
+ labelOf,
195
+ outcomeOf,
196
+ factsFromStep,
197
+ MAX_FACT_VALUES,
198
+ MAX_PROGRESS,
199
+ MAX_EARLIER_KINDS,
200
+ CLIP,
201
+ };