@xiaobuyu/nodesign 0.0.1 → 0.0.2

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 (282) hide show
  1. package/README.md +156 -106
  2. package/bin/nodesign.js +0 -0
  3. package/package.json +1 -1
  4. package/server/api/README.md +45 -45
  5. package/server/api/_guard.js +75 -75
  6. package/server/api/admin.js +187 -187
  7. package/server/api/assets/docx-page.js +103 -103
  8. package/server/api/assets/helpers.js +22 -22
  9. package/server/api/assets/notes.js +144 -144
  10. package/server/api/assets.js +896 -896
  11. package/server/api/board.js +57 -57
  12. package/server/api/browse.js +138 -138
  13. package/server/api/canvas.js +444 -444
  14. package/server/api/chatai.js +324 -324
  15. package/server/api/exports/build-standalone.js +980 -980
  16. package/server/api/exports/cards.js +118 -118
  17. package/server/api/exports/docx-pdf.js +46 -46
  18. package/server/api/exports/export-page.js +142 -142
  19. package/server/api/exports/handoff.js +147 -147
  20. package/server/api/exports.js +690 -690
  21. package/server/api/instruction.js +75 -75
  22. package/server/api/me.js +116 -116
  23. package/server/api/memory.js +185 -185
  24. package/server/api/pending-changes.js +366 -366
  25. package/server/api/plugins.js +164 -164
  26. package/server/api/projects.js +220 -220
  27. package/server/api/publish.js +64 -64
  28. package/server/api/recent.js +73 -73
  29. package/server/api/sessions.js +589 -589
  30. package/server/api/skills.js +77 -77
  31. package/server/api/standalone-fit.js +168 -168
  32. package/server/api/turn-compose.js +180 -180
  33. package/server/api/turn-inflight.js +44 -44
  34. package/server/auth/README.md +17 -17
  35. package/server/auth/middleware.js +138 -138
  36. package/server/auth/origin-guard.js +71 -71
  37. package/server/auth/session.js +108 -108
  38. package/server/auth/tier.js +113 -113
  39. package/server/auth/users-store.js +348 -348
  40. package/server/edit/README.md +29 -29
  41. package/server/engine/README.md +113 -113
  42. package/server/engine/agent/agent-shared.js +550 -550
  43. package/server/engine/agent/auto-mode-default-hard-deny.txt +4 -4
  44. package/server/engine/agent/auto-mode-rules.js +78 -78
  45. package/server/engine/agent/context.js +418 -418
  46. package/server/engine/agent/events.js +464 -464
  47. package/server/engine/agent/hooks/canvas-validate.js +199 -199
  48. package/server/engine/agent/hooks/failure.js +120 -120
  49. package/server/engine/agent/hooks/file-events.js +75 -75
  50. package/server/engine/agent/hooks/lifecycle.js +233 -233
  51. package/server/engine/agent/hooks/post-canvas-focus.js +80 -80
  52. package/server/engine/agent/hooks/post-guidance.js +206 -206
  53. package/server/engine/agent/hooks/post-subagent-report.js +104 -104
  54. package/server/engine/agent/hooks/post-trim.js +58 -58
  55. package/server/engine/agent/hooks/pre-board-neighborhood.js +34 -34
  56. package/server/engine/agent/hooks/pre-defaults.js +84 -84
  57. package/server/engine/agent/hooks/pre-injectors.js +277 -277
  58. package/server/engine/agent/hooks/pre-performance-log-guard.js +78 -78
  59. package/server/engine/agent/hooks/pre-starter-files.js +61 -61
  60. package/server/engine/agent/hooks/pre-workspace-scope-guard.js +78 -78
  61. package/server/engine/agent/hooks/site-validate.js +94 -94
  62. package/server/engine/agent/hooks/tool-prompts.js +31 -31
  63. package/server/engine/agent/hooks/turn-state-memory.js +56 -56
  64. package/server/engine/agent/hooks/user-prompt-submit.js +215 -215
  65. package/server/engine/agent/hooks.js +354 -354
  66. package/server/engine/agent/init-contract.js +69 -69
  67. package/server/engine/agent/isolation.js +159 -159
  68. package/server/engine/agent/model-table.js +292 -292
  69. package/server/engine/agent/plugin-loader.js +266 -266
  70. package/server/engine/agent/prompts/nodesign-prelude.md +336 -336
  71. package/server/engine/agent/prompts/tools/ask-user-question-protocol.md +71 -71
  72. package/server/engine/agent/prompts/tools/direct-edit-protocol.md +98 -98
  73. package/server/engine/agent/prompts/tools/generate-image-cookbook.gemini-gateway.md +583 -583
  74. package/server/engine/agent/prompts/tools/generate-image-cookbook.md +483 -483
  75. package/server/engine/agent/prompts/tools/hybrid-reference.md +100 -100
  76. package/server/engine/agent/prompts/tools/paint-still-cookbook.md +270 -270
  77. package/server/engine/agent/prompts/tools/roll-film-cookbook.md +70 -70
  78. package/server/engine/agent/prompts/tools/site-reference.md +106 -106
  79. package/server/engine/agent/prompts/tools/tweaks-syntax.md +125 -125
  80. package/server/engine/agent/prompts/tools/vision-checker-dispatch.md +48 -48
  81. package/server/engine/agent/session-loop.js +1024 -1024
  82. package/server/engine/agent/session-model.js +164 -164
  83. package/server/engine/agent/skill.js +204 -204
  84. package/server/engine/agent/task-events.js +78 -78
  85. package/server/engine/agents/ds-extractor.md +132 -132
  86. package/server/engine/agents/explorer.md +190 -190
  87. package/server/engine/agents/index.js +230 -230
  88. package/server/engine/agents/schemas/design-system.json +156 -156
  89. package/server/engine/agents/schemas/tweak-schema.json +113 -113
  90. package/server/engine/agents/tweak-proposer.md +127 -127
  91. package/server/engine/agents/vision-checker.md +254 -254
  92. package/server/engine/browse/capture.js +422 -422
  93. package/server/engine/browse/card.js +137 -137
  94. package/server/engine/browse/handover.js +59 -59
  95. package/server/engine/browse/page-digest.js +119 -119
  96. package/server/engine/browse/refs.js +196 -196
  97. package/server/engine/browse/registry.js +307 -307
  98. package/server/engine/browse/screencast.js +170 -170
  99. package/server/engine/browse/state.js +121 -121
  100. package/server/engine/chatai/chat-log.js +79 -79
  101. package/server/engine/chatai/index.js +193 -193
  102. package/server/engine/chatai/openai-compat.js +152 -152
  103. package/server/engine/chatai/orchestrate.js +309 -309
  104. package/server/engine/chatai/perform.js +40 -40
  105. package/server/engine/chatai/summarize.js +79 -79
  106. package/server/engine/mcp/capability-gate.js +52 -52
  107. package/server/engine/mcp/index.js +339 -339
  108. package/server/engine/mcp/param-sanitizer.js +142 -142
  109. package/server/engine/mcp/tools/arrange-on-board.js +124 -124
  110. package/server/engine/mcp/tools/artifact-session.js +278 -278
  111. package/server/engine/mcp/tools/browse-computer.js +421 -421
  112. package/server/engine/mcp/tools/browse-find-batch.js +183 -183
  113. package/server/engine/mcp/tools/browse-screenshot.js +157 -157
  114. package/server/engine/mcp/tools/browse.js +480 -480
  115. package/server/engine/mcp/tools/build-docx.js +101 -101
  116. package/server/engine/mcp/tools/clear-pending-changes.js +99 -99
  117. package/server/engine/mcp/tools/create-on-board.js +155 -155
  118. package/server/engine/mcp/tools/crystallize-skill.js +168 -168
  119. package/server/engine/mcp/tools/explain-style.js +237 -237
  120. package/server/engine/mcp/tools/export-handoff.js +133 -133
  121. package/server/engine/mcp/tools/expose-tweaks.js +149 -149
  122. package/server/engine/mcp/tools/generate-image.js +842 -842
  123. package/server/engine/mcp/tools/get-computed-styles.js +164 -164
  124. package/server/engine/mcp/tools/get-pending-changes.js +205 -205
  125. package/server/engine/mcp/tools/h3box-ssh.js +84 -84
  126. package/server/engine/mcp/tools/helpers/acquire-page.js +82 -82
  127. package/server/engine/mcp/tools/helpers/motion-lab.js +554 -554
  128. package/server/engine/mcp/tools/helpers/motion-scroll.js +112 -112
  129. package/server/engine/mcp/tools/helpers/perception-page.js +215 -215
  130. package/server/engine/mcp/tools/helpers/reference-download.js +103 -103
  131. package/server/engine/mcp/tools/helpers/rembg-bridge.py +80 -80
  132. package/server/engine/mcp/tools/helpers/shot-pipeline.js +280 -280
  133. package/server/engine/mcp/tools/highlight.js +68 -68
  134. package/server/engine/mcp/tools/list-pages.js +222 -222
  135. package/server/engine/mcp/tools/lookup-tags.js +59 -59
  136. package/server/engine/mcp/tools/navigate-to-page.js +60 -60
  137. package/server/engine/mcp/tools/organize-board.js +64 -64
  138. package/server/engine/mcp/tools/paint-still.js +434 -434
  139. package/server/engine/mcp/tools/pin-to-board.js +130 -130
  140. package/server/engine/mcp/tools/preview-deck.js +130 -130
  141. package/server/engine/mcp/tools/profile-scroll.js +305 -305
  142. package/server/engine/mcp/tools/publish-site.js +102 -102
  143. package/server/engine/mcp/tools/query-elements.js +195 -195
  144. package/server/engine/mcp/tools/read-board.js +103 -103
  145. package/server/engine/mcp/tools/read-document.js +93 -93
  146. package/server/engine/mcp/tools/read-page.js +264 -264
  147. package/server/engine/mcp/tools/read-tavern-json.js +143 -143
  148. package/server/engine/mcp/tools/record-decision.js +140 -140
  149. package/server/engine/mcp/tools/relate-on-board.js +110 -110
  150. package/server/engine/mcp/tools/report-issue.js +115 -115
  151. package/server/engine/mcp/tools/roll-film.js +266 -266
  152. package/server/engine/mcp/tools/screenshot-docx.js +138 -138
  153. package/server/engine/mcp/tools/screenshot-url.js +195 -195
  154. package/server/engine/mcp/tools/screenshot.js +583 -583
  155. package/server/engine/mcp/tools/tier-gate.js +96 -96
  156. package/server/engine/mcp/tools/trace-motion.js +215 -215
  157. package/server/engine/mcp/tools/web-search.js +548 -548
  158. package/server/engine/motion/inventory.js +349 -349
  159. package/server/engine/perception/session.js +235 -235
  160. package/server/engine/plugins/nodesign/.claude-plugin/plugin.json +5 -5
  161. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/SKILL.md +192 -192
  162. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/canvas.template.html +667 -667
  163. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/hybrid-grid.md +22 -22
  164. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/image-led-cover.md +25 -25
  165. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/portrait.md +23 -23
  166. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/quote-backdrop.md +21 -21
  167. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/section-divider.md +23 -23
  168. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/text-led.md +21 -21
  169. package/server/engine/plugins/nodesign/skills/docx-craft/SKILL.md +162 -162
  170. package/server/engine/plugins/nodesign/skills/docx-craft/references/token-schema.md +407 -407
  171. package/server/engine/plugins/nodesign/skills/docx-craft//346/226/207/346/241/243.template.json +80 -80
  172. package/server/engine/plugins/nodesign/skills/rp-craft/SKILL.md +228 -228
  173. package/server/engine/plugins/nodesign/skills/rp-craft/patterns//346/274/224/345/207/272/351/241/265-/346/234/200/345/260/217/345/256/236/347/216/260.html +78 -78
  174. package/server/engine/plugins/nodesign/skills/rp-craft//346/274/224/345/207/272.template.js +268 -268
  175. package/server/engine/plugins/nodesign/skills/site-craft/SKILL.md +323 -323
  176. package/server/engine/plugins/nodesign/skills/site-craft/patterns/build-lane.md +134 -134
  177. package/server/engine/plugins/nodesign/skills/site-craft/patterns/cutout-collage.md +61 -61
  178. package/server/engine/plugins/nodesign/skills/site-craft/patterns/mock-app.md +163 -163
  179. package/server/engine/plugins/nodesign/skills/site-craft/patterns/page-transitions.md +86 -86
  180. package/server/engine/runs/active-runs.js +698 -698
  181. package/server/engine/runs/live-turn.js +252 -252
  182. package/server/engine/runs/store.js +364 -364
  183. package/server/engine/runs/turn-relay.js +217 -217
  184. package/server/engine/runtime/workspace.js +164 -164
  185. package/server/lib/artifact-file-path.js +80 -80
  186. package/server/lib/artifact-target.js +284 -284
  187. package/server/lib/asset-refs.js +239 -239
  188. package/server/lib/async-queue.js +104 -104
  189. package/server/lib/auto-relations.js +130 -130
  190. package/server/lib/binding-types.js +60 -60
  191. package/server/lib/board-kind-sizes.js +62 -62
  192. package/server/lib/board-relations.js +133 -133
  193. package/server/lib/browse-proxy.js +180 -180
  194. package/server/lib/canvas-id.js +36 -36
  195. package/server/lib/cover.js +227 -227
  196. package/server/lib/danbooru-tags.js +291 -291
  197. package/server/lib/doc-extract.js +156 -156
  198. package/server/lib/docx/build-from-source.js +260 -260
  199. package/server/lib/docx/build.js +490 -490
  200. package/server/lib/docx/dump-styles.js +287 -287
  201. package/server/lib/docx/fonts/nodesign-cjk.conf +164 -164
  202. package/server/lib/docx/merge-runs.js +129 -129
  203. package/server/lib/docx/numbering.js +218 -218
  204. package/server/lib/docx/order.js +152 -152
  205. package/server/lib/docx/rawzip.js +172 -172
  206. package/server/lib/docx/render.js +92 -92
  207. package/server/lib/docx/text-lint.js +225 -225
  208. package/server/lib/docx/tokens.js +335 -335
  209. package/server/lib/docx/units.js +29 -29
  210. package/server/lib/docx/xml.js +291 -291
  211. package/server/lib/docx-pages.js +187 -187
  212. package/server/lib/export-collect.js +289 -289
  213. package/server/lib/export-package.js +210 -210
  214. package/server/lib/html-srcset.js +145 -145
  215. package/server/lib/image-variant.js +460 -460
  216. package/server/lib/ingress/forward-openai-chat.js +305 -305
  217. package/server/lib/ingress/openai-chat.js +491 -491
  218. package/server/lib/ingress/session-notice.js +66 -66
  219. package/server/lib/ingress/session-routes.js +68 -68
  220. package/server/lib/ingress/slot-probe.js +130 -130
  221. package/server/lib/ingress/upstream-billing.js +52 -52
  222. package/server/lib/ingress/upstream-fail-streak.js +72 -72
  223. package/server/lib/ingress/upstream-truncation.js +48 -48
  224. package/server/lib/issues-store.js +182 -182
  225. package/server/lib/kinds/deck.js +72 -72
  226. package/server/lib/kinds/docx.js +283 -283
  227. package/server/lib/kinds/file-kinds.js +113 -113
  228. package/server/lib/kinds/index.js +202 -202
  229. package/server/lib/kinds/site.js +161 -161
  230. package/server/lib/model-ingress.js +543 -543
  231. package/server/lib/moderation.js +255 -255
  232. package/server/lib/notice-store.js +103 -103
  233. package/server/lib/plugin-install.js +104 -104
  234. package/server/lib/plugin-validator.js +721 -721
  235. package/server/lib/publish-store.js +115 -115
  236. package/server/lib/quick-summary.js +52 -52
  237. package/server/lib/quota.js +270 -270
  238. package/server/lib/rate-window.js +29 -29
  239. package/server/lib/reference-assets.js +92 -92
  240. package/server/lib/region-shot.js +135 -135
  241. package/server/lib/safe-path.js +73 -73
  242. package/server/lib/sdk-session.js +33 -33
  243. package/server/lib/showcase-store.js +92 -92
  244. package/server/lib/site-publish.js +465 -465
  245. package/server/lib/ssrf-guard.js +432 -432
  246. package/server/lib/task-scan.js +158 -158
  247. package/server/lib/tavern-json.js +154 -154
  248. package/server/lib/video-variant.js +237 -237
  249. package/server/lib/workspace-path.js +33 -33
  250. package/server/ops/fix-sdk-musl.mjs +40 -40
  251. package/server/ops/install-macos-fonts.sh +114 -114
  252. package/server/ops/macos-fonts.conf +336 -336
  253. package/server/ops/sandbox-shim/bwrap +57 -57
  254. package/server/projects/assets-summary.js +119 -119
  255. package/server/projects/auto-name.js +53 -53
  256. package/server/projects/board-store.js +640 -640
  257. package/server/projects/move-entry.js +103 -103
  258. package/server/projects/store.js +270 -270
  259. package/server/projects/ui-config.js +54 -54
  260. package/server/projects/workspace-templates.js +53 -53
  261. package/server/projects/workspace.js +1025 -1025
  262. package/server/runtime/local-config.js +177 -177
  263. package/server/runtime/profile.js +72 -72
  264. package/server/services/rembg-service.py +334 -334
  265. package/server/shared/README.md +25 -25
  266. package/server/shared/deck.js +54 -54
  267. package/server/shared/time.js +49 -49
  268. package/server/style-pipeline/README.md +53 -53
  269. package/server/ws/broker.js +36 -36
  270. package/server/ws/browse-channel.js +177 -177
  271. package/server/ws/index.js +386 -386
  272. package/web/README.md +89 -89
  273. package/web/dist/assets/BrowserWindow-BYCgizHw.js +11 -0
  274. package/web/dist/assets/{DeckWindow-4G0xX7P3.js → DeckWindow-pNydEM4L.js} +1 -1
  275. package/web/dist/assets/{DocxWindow-BXQsoPkh.js → DocxWindow-PPgzh3Gq.js} +1 -1
  276. package/web/dist/assets/{SiteWindow-D6eS-2gM.js → SiteWindow-DM0LQPCO.js} +1 -1
  277. package/web/dist/assets/{index-Cbl3ATvp.js → index-BHTBJlpY.js} +32 -32
  278. package/web/dist/assets/{pending-edit-apply-DTjo2geW.js → pending-edit-apply-C0I13CbD.js} +1 -1
  279. package/web/dist/assets/{rotate-cw-LMT3x1Zj.js → rotate-cw-B1UQ8Hld.js} +1 -1
  280. package/web/dist/assets/{square-dashed-mouse-pointer-CBoAkfhi.js → square-dashed-mouse-pointer-8lrgqmfv.js} +1 -1
  281. package/web/dist/index.html +17 -17
  282. package/web/dist/assets/BrowserWindow-Dc7J7CYc.js +0 -11
@@ -1,130 +1,130 @@
1
- /**
2
- * mcp/tools/pin_to_board — 把一件东西摆到用户画布上的某个位置。
3
- *
4
- * 写 board.json(board-store 单锁原子操作,与前端 PATCH 互不覆盖),然后广播
5
- * board.updated(sessionId: null → project 全连接都收到,前端整份重拉)。
6
- *
7
- * 物件 id 约定(与前端 BoardCanvas 派生一致):**id = kind 前缀 + 工作区相对路径**
8
- * - 普通文件(图片 / 便签 / 数据):路径本身,`assets/generated/a.webp`
9
- * - deck:`deck:<路径>`,如 `deck:稿件/主稿.html`
10
- * - 站点:`site:<目录>`
11
- * - 项目文档:`doc:_root`(记忆)/ `doc:brand`(品牌档案)
12
- *
13
- * ## 2026-08-13:这个工具的职权范围缩小了
14
- *
15
- * 以前它有个 `zone` 参数,语义是"放进哪块工作区"。**在 id = 路径的模型下这件事
16
- * 不成立** —— 一个物件属于哪个文件夹,答案就写在它的路径里。让工具单独写一个
17
- * "显式归属"字段,等于允许画布说"它在 A 文件夹"而磁盘说"它在 B 文件夹",
18
- * 正是 2026-07-28 把「拖出工作区解绑」停用掉的那个理由(两边对不上,且很容易
19
- * 误触)。要换文件夹就 `mv` —— 那是真的搬,画布跟着走。
20
- *
21
- * 所以现在它只剩一件事:**给这件东西一个位置并置顶**("把它摆到用户眼前")。
22
- *
23
- * ⚠️ 顺带修掉的两个僵尸:`zone` 原来被校验成 `/^[A-Za-z0-9-]{8,64}$/`(= 一个
24
- * sessionId),于是任何中文文件夹名、任何带斜杠的嵌套路径都过不了,一律静默
25
- * 回落到 sessionId —— 然后在 board.json 里新建一块根本不存在的"工作区"。
26
- * agent 每帮一次忙就制造一条死数据。
27
- */
28
-
29
- import { tool } from '@anthropic-ai/claude-agent-sdk';
30
- import { z } from 'zod';
31
- import path from 'path';
32
- import { promises as fs } from 'fs';
33
- import { pinToZone } from '../../../projects/board-store.js';
34
-
35
- /** 物件 id → 它住在哪个文件夹(与前端 stage.js 的 zoneOfObjectId 同一套规则) */
36
- function folderOfObjectId(objectId) {
37
- if (objectId.startsWith('doc:')) return '';
38
- const c = objectId.indexOf(':');
39
- const p = (c > 0 && /^[a-z]+$/.test(objectId.slice(0, c))) ? objectId.slice(c + 1) : objectId;
40
- const i = p.lastIndexOf('/');
41
- return i > 0 ? p.slice(0, i) : '';
42
- }
43
-
44
- /**
45
- * @param {object} deps
46
- * @param {string} [deps.sharedRoot] 工作区根(校验文件存在用)
47
- * @param {string} [deps.projectId]
48
- * @param {import('../../agent/context.js').AgentContext} [deps.ctx]
49
- */
50
- export function makePinToBoardTool({ sharedRoot, projectId, ctx }) {
51
- return tool(
52
- 'pin_to_board',
53
- `Bring an item to the front of the user's canvas, at a free spot in whatever
54
- folder it lives in. The canvas is their desktop: whatever you write appears
55
- there automatically — you do NOT need this tool for your own outputs.
56
- Use it only to deliberately surface something:
57
-
58
- - Pull a reference (an uploaded asset, the brand doc, an older image) into view
59
- - Restore something the user dragged off-screen, when they ask for it back
60
-
61
- This does NOT change which folder the item belongs to — that is decided by
62
- where the file is on disk. To move it, \`mv\` the file; the canvas follows.
63
-
64
- Paths are workspace-relative, exactly as they are on disk. Accepted forms:
65
- - any file path: 'assets/generated/hero.webp', 'notes/灵感.md', '稿件/数据.csv'
66
- - a deck: 'deck:<path>.html' a site: 'site:<dir>'
67
- (a bare '<path>.html' is read as a deck)
68
- - '.claude/agent-memory/memory.md' (project memory) / '.../brand/memory.md' (brand doc)`,
69
- {
70
- path: z
71
- .string()
72
- .min(1)
73
- .max(300)
74
- .describe('Item to surface — see accepted forms in the tool description'),
75
- },
76
- async ({ path: rawPath }) => {
77
- try {
78
- if (!projectId) {
79
- return { content: [{ type: 'text', text: 'No project bound; cannot pin.' }], isError: true };
80
- }
81
-
82
- // 归一化成前端物件 id(id = kind 前缀 + 工作区相对路径)
83
- let objectId = String(rawPath).trim().replace(/\\/g, '/').replace(/^\.\//, '').replace(/^\/+|\/+$/g, '');
84
- if (!objectId || objectId.includes('..')) {
85
- return { content: [{ type: 'text', text: 'Invalid path.' }], isError: true };
86
- }
87
- if (objectId.endsWith('agent-memory/brand/memory.md')) objectId = 'doc:brand';
88
- else if (objectId.endsWith('agent-memory/memory.md')) objectId = 'doc:_root';
89
- else if (!/^(deck|site|doc):/.test(objectId)) {
90
- // 裸路径:.html 是一份 deck,其余(图片 / 便签 / 数据文件)id 就是路径。
91
- // 站点的目录要显式写 `site:` —— 光看路径分不出
92
- // "一个收纳文件夹"和"一件目录型产物",猜错了钉上去的是个不存在的 id。
93
- if (sharedRoot) {
94
- const abs = path.join(sharedRoot, objectId);
95
- try { await fs.access(abs); } catch {
96
- return {
97
- content: [{ type: 'text', text: `File not found: ${objectId}. Paths are workspace-relative, same as on disk.` }],
98
- isError: true,
99
- };
100
- }
101
- }
102
- if (/\.html?$/i.test(objectId)) objectId = `deck:${objectId}`;
103
- }
104
-
105
- const zoneId = folderOfObjectId(objectId);
106
- const { zone: placedZone, placed } = await pinToZone(projectId, { objectId, zoneId });
107
-
108
- try {
109
- ctx?.emit?.({
110
- type: 'board.updated',
111
- sessionId: null, // project 级广播:显式压掉 ctx 的 sessionId enrich
112
- objectId,
113
- zoneId,
114
- summary: zoneId ? `已把 ${objectId} 摆到「${zoneId}」里` : `已把 ${objectId} 摆到桌面上`,
115
- });
116
- } catch { /* emit fail-safe */ }
117
-
118
- const where = placedZone?.id ? `in ${placedZone.id}` : 'on the desktop';
119
- return {
120
- content: [{
121
- type: 'text',
122
- text: `Surfaced ${objectId} ${where} at (${placed.x}, ${placed.y}). The user's canvas updates live.`,
123
- }],
124
- };
125
- } catch (err) {
126
- return { content: [{ type: 'text', text: `pin_to_board failed: ${err.message}` }], isError: true };
127
- }
128
- },
129
- );
130
- }
1
+ /**
2
+ * mcp/tools/pin_to_board — 把一件东西摆到用户画布上的某个位置。
3
+ *
4
+ * 写 board.json(board-store 单锁原子操作,与前端 PATCH 互不覆盖),然后广播
5
+ * board.updated(sessionId: null → project 全连接都收到,前端整份重拉)。
6
+ *
7
+ * 物件 id 约定(与前端 BoardCanvas 派生一致):**id = kind 前缀 + 工作区相对路径**
8
+ * - 普通文件(图片 / 便签 / 数据):路径本身,`assets/generated/a.webp`
9
+ * - deck:`deck:<路径>`,如 `deck:稿件/主稿.html`
10
+ * - 站点:`site:<目录>`
11
+ * - 项目文档:`doc:_root`(记忆)/ `doc:brand`(品牌档案)
12
+ *
13
+ * ## 2026-08-13:这个工具的职权范围缩小了
14
+ *
15
+ * 以前它有个 `zone` 参数,语义是"放进哪块工作区"。**在 id = 路径的模型下这件事
16
+ * 不成立** —— 一个物件属于哪个文件夹,答案就写在它的路径里。让工具单独写一个
17
+ * "显式归属"字段,等于允许画布说"它在 A 文件夹"而磁盘说"它在 B 文件夹",
18
+ * 正是 2026-07-28 把「拖出工作区解绑」停用掉的那个理由(两边对不上,且很容易
19
+ * 误触)。要换文件夹就 `mv` —— 那是真的搬,画布跟着走。
20
+ *
21
+ * 所以现在它只剩一件事:**给这件东西一个位置并置顶**("把它摆到用户眼前")。
22
+ *
23
+ * ⚠️ 顺带修掉的两个僵尸:`zone` 原来被校验成 `/^[A-Za-z0-9-]{8,64}$/`(= 一个
24
+ * sessionId),于是任何中文文件夹名、任何带斜杠的嵌套路径都过不了,一律静默
25
+ * 回落到 sessionId —— 然后在 board.json 里新建一块根本不存在的"工作区"。
26
+ * agent 每帮一次忙就制造一条死数据。
27
+ */
28
+
29
+ import { tool } from '@anthropic-ai/claude-agent-sdk';
30
+ import { z } from 'zod';
31
+ import path from 'path';
32
+ import { promises as fs } from 'fs';
33
+ import { pinToZone } from '../../../projects/board-store.js';
34
+
35
+ /** 物件 id → 它住在哪个文件夹(与前端 stage.js 的 zoneOfObjectId 同一套规则) */
36
+ function folderOfObjectId(objectId) {
37
+ if (objectId.startsWith('doc:')) return '';
38
+ const c = objectId.indexOf(':');
39
+ const p = (c > 0 && /^[a-z]+$/.test(objectId.slice(0, c))) ? objectId.slice(c + 1) : objectId;
40
+ const i = p.lastIndexOf('/');
41
+ return i > 0 ? p.slice(0, i) : '';
42
+ }
43
+
44
+ /**
45
+ * @param {object} deps
46
+ * @param {string} [deps.sharedRoot] 工作区根(校验文件存在用)
47
+ * @param {string} [deps.projectId]
48
+ * @param {import('../../agent/context.js').AgentContext} [deps.ctx]
49
+ */
50
+ export function makePinToBoardTool({ sharedRoot, projectId, ctx }) {
51
+ return tool(
52
+ 'pin_to_board',
53
+ `Bring an item to the front of the user's canvas, at a free spot in whatever
54
+ folder it lives in. The canvas is their desktop: whatever you write appears
55
+ there automatically — you do NOT need this tool for your own outputs.
56
+ Use it only to deliberately surface something:
57
+
58
+ - Pull a reference (an uploaded asset, the brand doc, an older image) into view
59
+ - Restore something the user dragged off-screen, when they ask for it back
60
+
61
+ This does NOT change which folder the item belongs to — that is decided by
62
+ where the file is on disk. To move it, \`mv\` the file; the canvas follows.
63
+
64
+ Paths are workspace-relative, exactly as they are on disk. Accepted forms:
65
+ - any file path: 'assets/generated/hero.webp', 'notes/灵感.md', '稿件/数据.csv'
66
+ - a deck: 'deck:<path>.html' a site: 'site:<dir>'
67
+ (a bare '<path>.html' is read as a deck)
68
+ - '.claude/agent-memory/memory.md' (project memory) / '.../brand/memory.md' (brand doc)`,
69
+ {
70
+ path: z
71
+ .string()
72
+ .min(1)
73
+ .max(300)
74
+ .describe('Item to surface — see accepted forms in the tool description'),
75
+ },
76
+ async ({ path: rawPath }) => {
77
+ try {
78
+ if (!projectId) {
79
+ return { content: [{ type: 'text', text: 'No project bound; cannot pin.' }], isError: true };
80
+ }
81
+
82
+ // 归一化成前端物件 id(id = kind 前缀 + 工作区相对路径)
83
+ let objectId = String(rawPath).trim().replace(/\\/g, '/').replace(/^\.\//, '').replace(/^\/+|\/+$/g, '');
84
+ if (!objectId || objectId.includes('..')) {
85
+ return { content: [{ type: 'text', text: 'Invalid path.' }], isError: true };
86
+ }
87
+ if (objectId.endsWith('agent-memory/brand/memory.md')) objectId = 'doc:brand';
88
+ else if (objectId.endsWith('agent-memory/memory.md')) objectId = 'doc:_root';
89
+ else if (!/^(deck|site|doc):/.test(objectId)) {
90
+ // 裸路径:.html 是一份 deck,其余(图片 / 便签 / 数据文件)id 就是路径。
91
+ // 站点的目录要显式写 `site:` —— 光看路径分不出
92
+ // "一个收纳文件夹"和"一件目录型产物",猜错了钉上去的是个不存在的 id。
93
+ if (sharedRoot) {
94
+ const abs = path.join(sharedRoot, objectId);
95
+ try { await fs.access(abs); } catch {
96
+ return {
97
+ content: [{ type: 'text', text: `File not found: ${objectId}. Paths are workspace-relative, same as on disk.` }],
98
+ isError: true,
99
+ };
100
+ }
101
+ }
102
+ if (/\.html?$/i.test(objectId)) objectId = `deck:${objectId}`;
103
+ }
104
+
105
+ const zoneId = folderOfObjectId(objectId);
106
+ const { zone: placedZone, placed } = await pinToZone(projectId, { objectId, zoneId });
107
+
108
+ try {
109
+ ctx?.emit?.({
110
+ type: 'board.updated',
111
+ sessionId: null, // project 级广播:显式压掉 ctx 的 sessionId enrich
112
+ objectId,
113
+ zoneId,
114
+ summary: zoneId ? `已把 ${objectId} 摆到「${zoneId}」里` : `已把 ${objectId} 摆到桌面上`,
115
+ });
116
+ } catch { /* emit fail-safe */ }
117
+
118
+ const where = placedZone?.id ? `in ${placedZone.id}` : 'on the desktop';
119
+ return {
120
+ content: [{
121
+ type: 'text',
122
+ text: `Surfaced ${objectId} ${where} at (${placed.x}, ${placed.y}). The user's canvas updates live.`,
123
+ }],
124
+ };
125
+ } catch (err) {
126
+ return { content: [{ type: 'text', text: `pin_to_board failed: ${err.message}` }], isError: true };
127
+ }
128
+ },
129
+ );
130
+ }
@@ -1,130 +1,130 @@
1
- /**
2
- * mcp/tools/preview-deck.js — preview_deck MCP tool(2026-07-28)
3
- *
4
- * agent 主动把做好的 deck 摊到用户眼前:等价于用户在工作台上双击那张 deck 卡
5
- * ——收起态就展开成内嵌渲染,已经展开就开成画布内的最大化窗口。
6
- *
7
- * 实现:纯 emit run.deck_preview 事件(跟 navigate_to_page 同款控制层套路),
8
- * 路径归一在前端做(lib/stage.js 的 resolveObjectId 已经认得 tasks/<任务>/canvas.html
9
- * 和会话 cwd 的 canvas.html 两种形态)。
10
- */
11
-
12
- import { tool } from '@anthropic-ai/claude-agent-sdk';
13
- import { z } from 'zod';
14
- import { Events } from '../../agent/events.js';
15
- import { setActiveDeck } from '../../../lib/artifact-target.js';
16
- import fs from 'node:fs/promises';
17
- import nodePath from 'node:path';
18
- import { taskManifest } from '../../../lib/kinds/index.js';
19
-
20
- /**
21
- * @param {object} deps
22
- * @param {import('../../agent/context.js').AgentContext} [deps.ctx]
23
- */
24
- export function makePreviewDeckTool({ ctx, sessionId, workspaceRoot }) {
25
- return tool(
26
- 'preview_deck',
27
- `Show a deck to the user on their workbench — the same thing that happens
28
- when they double-click the deck card themselves: a collapsed card expands into
29
- a live embedded preview, an already-expanded one opens as a maximized window
30
- over the desktop.
31
-
32
- Use this when:
33
- - You just finished (or substantially changed) a deck and want the user to look
34
- - The user asks "show me" / "let me see it"
35
- - You're about to discuss a deck and want it on screen first
36
-
37
- Do NOT use it to spam attention mid-work — one call when the thing is worth
38
- looking at. This does not replace screenshot_canvas: that one is for **you** to
39
- see the render, this one is for **the user**.`,
40
- {
41
- path: z
42
- .string()
43
- .optional()
44
- .describe(
45
- "Deck to show, workspace-relative (e.g. 'canvas.html' or '稿件/主稿.html'). Omit for the active artifact.",
46
- ),
47
- },
48
- async ({ path: rawPath }) => {
49
- try {
50
- const p = typeof rawPath === 'string' ? rawPath.trim().replace(/^\.\//, '') : '';
51
- // 绝对路径同拒:这个 path 直发前端当寻址依据(home 幽灵族的家规)
52
- if (p.includes('..') || p.startsWith('/')) {
53
- return { content: [{ type: 'text', text: 'Invalid path (workspace-relative only).' }], isError: true };
54
- }
55
- // ⭐ 2026-08-18:先查这个文件在不在。
56
- // 以前这里只挡 `..` 和绝对路径,然后无条件 emit + 回一句 "Opened …"。
57
- // 传一个不存在的路径、或者传站点的子页时,用户桌面上打开的是**本会话的
58
- // 空 canvas.html**,而 agent 拿到的是成功 —— 它没有任何办法察觉用户
59
- // 正对着一块空白(问题库 iss_msr8oki7_z9su:「静默成功是最伤的部分」)。
60
- let siteNote = null;
61
- if (p) {
62
- const abs = nodePath.join(workspaceRoot || '', p);
63
- try {
64
- await fs.access(abs);
65
- } catch {
66
- return {
67
- content: [{ type: 'text', text:
68
- `path not found: ${p} —— 先写出这个文件,或者用 list_pages 看清楚现有产物。`
69
- + '(没有打开任何东西,用户桌面没变。)' }],
70
- isError: true,
71
- };
72
- }
73
- // 站点的子页:站点是整站一扇窗,打开的是站不是那一页 —— 如实说
74
- try {
75
- const manifest = await taskManifest(workspaceRoot);
76
- const arts = manifest?.artifacts || [];
77
- // ⛔ 原判据是 `(a.root ? p.startsWith(`${a.root}/`) : true)`。**根站的
78
- // root 是空字符串**(site.js 里 `out.push({ srcRoot:'', root, ... })`),
79
- // 于是三元退化成"任意路径都算这个站的一页"。后果是 `preview_deck("报告.docx")`
80
- // 和 `preview_deck("_drafts/试作.html")` 都被回一句自信的假话
81
- // 「还不在这个站的页面清单里…用户看到的是入口 index.html」——
82
- // 两句都假:docx 开在 DocxWindow,草稿开在自己那扇 single:true 的窗。
83
- // 改动本意是消灭"静默成功",结果换成了反向的自信错话。
84
- //
85
- // 先看有没有**别的产物正好就是这个路径**(单页站、docx、deck…)——
86
- // 有就是它自己开,跟根站无关。
87
- const ownedByOther = arts.some(a => a.entryRel === p || a.file === p
88
- || (a.single && (a.pages || []).includes(p)));
89
- const site = ownedByOther ? null : arts.find((a) => {
90
- if (a.kind !== 'site' || a.single) return false;
91
- if (a.root) return p.startsWith(`${a.root}/`); // 目录即边界,前缀够用
92
- // 根站没有目录边界可用,只能按"是不是一张网页"+页面清单判
93
- if (!/\.html?$/i.test(p)) return false;
94
- return true;
95
- });
96
- if (site && p !== (site.entryRel || 'index.html')) {
97
- // 2026-08-18 起子页**真的**开在那一页上(前端 entry 一路传到 SiteWindow)。
98
- // 但只有在产物清单认得这一页时才成立 —— 不在清单里(比如刚写出来还没
99
- // 重拉)就还是退回入口,所以这句话要如实分两种说。
100
- const known = (site.pages || []).includes(
101
- site.root ? p.slice(site.root.length + 1) : p);
102
- siteNote = known
103
- ? `打开的是站点「${site.root || '工作区根'}」并停在 ${p} 这一页(整站一扇窗,用户可以在窗里翻别的页)。`
104
- : `注意:${p} 还不在这个站的页面清单里(产物列表下次重拉才认它),`
105
- + `所以用户看到的是入口 ${site.entryRel || 'index.html'}。`
106
- + '站内给它加一条真链接,或者等一轮再试。';
107
- }
108
- } catch { /* manifest 读不出来不挡预览 */ }
109
- setActiveDeck(sessionId, p); // 摊给用户看的那份就是"当前 deck"
110
- }
111
- ctx?.emit?.(Events.deckPreview(p));
112
- return {
113
- content: [{
114
- type: 'text',
115
- text: [
116
- p ? `Opened ${p} on the user's workbench.`
117
- : "Opened this session's deck on the user's workbench.",
118
- siteNote,
119
- ].filter(Boolean).join('\n'),
120
- }],
121
- };
122
- } catch (err) {
123
- return {
124
- content: [{ type: 'text', text: `preview_deck failed: ${err?.message || String(err)}` }],
125
- isError: true,
126
- };
127
- }
128
- },
129
- );
130
- }
1
+ /**
2
+ * mcp/tools/preview-deck.js — preview_deck MCP tool(2026-07-28)
3
+ *
4
+ * agent 主动把做好的 deck 摊到用户眼前:等价于用户在工作台上双击那张 deck 卡
5
+ * ——收起态就展开成内嵌渲染,已经展开就开成画布内的最大化窗口。
6
+ *
7
+ * 实现:纯 emit run.deck_preview 事件(跟 navigate_to_page 同款控制层套路),
8
+ * 路径归一在前端做(lib/stage.js 的 resolveObjectId 已经认得 tasks/<任务>/canvas.html
9
+ * 和会话 cwd 的 canvas.html 两种形态)。
10
+ */
11
+
12
+ import { tool } from '@anthropic-ai/claude-agent-sdk';
13
+ import { z } from 'zod';
14
+ import { Events } from '../../agent/events.js';
15
+ import { setActiveDeck } from '../../../lib/artifact-target.js';
16
+ import fs from 'node:fs/promises';
17
+ import nodePath from 'node:path';
18
+ import { taskManifest } from '../../../lib/kinds/index.js';
19
+
20
+ /**
21
+ * @param {object} deps
22
+ * @param {import('../../agent/context.js').AgentContext} [deps.ctx]
23
+ */
24
+ export function makePreviewDeckTool({ ctx, sessionId, workspaceRoot }) {
25
+ return tool(
26
+ 'preview_deck',
27
+ `Show a deck to the user on their workbench — the same thing that happens
28
+ when they double-click the deck card themselves: a collapsed card expands into
29
+ a live embedded preview, an already-expanded one opens as a maximized window
30
+ over the desktop.
31
+
32
+ Use this when:
33
+ - You just finished (or substantially changed) a deck and want the user to look
34
+ - The user asks "show me" / "let me see it"
35
+ - You're about to discuss a deck and want it on screen first
36
+
37
+ Do NOT use it to spam attention mid-work — one call when the thing is worth
38
+ looking at. This does not replace screenshot_canvas: that one is for **you** to
39
+ see the render, this one is for **the user**.`,
40
+ {
41
+ path: z
42
+ .string()
43
+ .optional()
44
+ .describe(
45
+ "Deck to show, workspace-relative (e.g. 'canvas.html' or '稿件/主稿.html'). Omit for the active artifact.",
46
+ ),
47
+ },
48
+ async ({ path: rawPath }) => {
49
+ try {
50
+ const p = typeof rawPath === 'string' ? rawPath.trim().replace(/^\.\//, '') : '';
51
+ // 绝对路径同拒:这个 path 直发前端当寻址依据(home 幽灵族的家规)
52
+ if (p.includes('..') || p.startsWith('/')) {
53
+ return { content: [{ type: 'text', text: 'Invalid path (workspace-relative only).' }], isError: true };
54
+ }
55
+ // ⭐ 2026-08-18:先查这个文件在不在。
56
+ // 以前这里只挡 `..` 和绝对路径,然后无条件 emit + 回一句 "Opened …"。
57
+ // 传一个不存在的路径、或者传站点的子页时,用户桌面上打开的是**本会话的
58
+ // 空 canvas.html**,而 agent 拿到的是成功 —— 它没有任何办法察觉用户
59
+ // 正对着一块空白(问题库 iss_msr8oki7_z9su:「静默成功是最伤的部分」)。
60
+ let siteNote = null;
61
+ if (p) {
62
+ const abs = nodePath.join(workspaceRoot || '', p);
63
+ try {
64
+ await fs.access(abs);
65
+ } catch {
66
+ return {
67
+ content: [{ type: 'text', text:
68
+ `path not found: ${p} —— 先写出这个文件,或者用 list_pages 看清楚现有产物。`
69
+ + '(没有打开任何东西,用户桌面没变。)' }],
70
+ isError: true,
71
+ };
72
+ }
73
+ // 站点的子页:站点是整站一扇窗,打开的是站不是那一页 —— 如实说
74
+ try {
75
+ const manifest = await taskManifest(workspaceRoot);
76
+ const arts = manifest?.artifacts || [];
77
+ // ⛔ 原判据是 `(a.root ? p.startsWith(`${a.root}/`) : true)`。**根站的
78
+ // root 是空字符串**(site.js 里 `out.push({ srcRoot:'', root, ... })`),
79
+ // 于是三元退化成"任意路径都算这个站的一页"。后果是 `preview_deck("报告.docx")`
80
+ // 和 `preview_deck("_drafts/试作.html")` 都被回一句自信的假话
81
+ // 「还不在这个站的页面清单里…用户看到的是入口 index.html」——
82
+ // 两句都假:docx 开在 DocxWindow,草稿开在自己那扇 single:true 的窗。
83
+ // 改动本意是消灭"静默成功",结果换成了反向的自信错话。
84
+ //
85
+ // 先看有没有**别的产物正好就是这个路径**(单页站、docx、deck…)——
86
+ // 有就是它自己开,跟根站无关。
87
+ const ownedByOther = arts.some(a => a.entryRel === p || a.file === p
88
+ || (a.single && (a.pages || []).includes(p)));
89
+ const site = ownedByOther ? null : arts.find((a) => {
90
+ if (a.kind !== 'site' || a.single) return false;
91
+ if (a.root) return p.startsWith(`${a.root}/`); // 目录即边界,前缀够用
92
+ // 根站没有目录边界可用,只能按"是不是一张网页"+页面清单判
93
+ if (!/\.html?$/i.test(p)) return false;
94
+ return true;
95
+ });
96
+ if (site && p !== (site.entryRel || 'index.html')) {
97
+ // 2026-08-18 起子页**真的**开在那一页上(前端 entry 一路传到 SiteWindow)。
98
+ // 但只有在产物清单认得这一页时才成立 —— 不在清单里(比如刚写出来还没
99
+ // 重拉)就还是退回入口,所以这句话要如实分两种说。
100
+ const known = (site.pages || []).includes(
101
+ site.root ? p.slice(site.root.length + 1) : p);
102
+ siteNote = known
103
+ ? `打开的是站点「${site.root || '工作区根'}」并停在 ${p} 这一页(整站一扇窗,用户可以在窗里翻别的页)。`
104
+ : `注意:${p} 还不在这个站的页面清单里(产物列表下次重拉才认它),`
105
+ + `所以用户看到的是入口 ${site.entryRel || 'index.html'}。`
106
+ + '站内给它加一条真链接,或者等一轮再试。';
107
+ }
108
+ } catch { /* manifest 读不出来不挡预览 */ }
109
+ setActiveDeck(sessionId, p); // 摊给用户看的那份就是"当前 deck"
110
+ }
111
+ ctx?.emit?.(Events.deckPreview(p));
112
+ return {
113
+ content: [{
114
+ type: 'text',
115
+ text: [
116
+ p ? `Opened ${p} on the user's workbench.`
117
+ : "Opened this session's deck on the user's workbench.",
118
+ siteNote,
119
+ ].filter(Boolean).join('\n'),
120
+ }],
121
+ };
122
+ } catch (err) {
123
+ return {
124
+ content: [{ type: 'text', text: `preview_deck failed: ${err?.message || String(err)}` }],
125
+ isError: true,
126
+ };
127
+ }
128
+ },
129
+ );
130
+ }