@xiaobuyu/nodesign 0.0.2 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/README.en.md +184 -0
  2. package/README.md +19 -25
  3. package/package.json +2 -1
  4. package/server/api/assets/helpers.js +20 -0
  5. package/server/api/assets/notes.js +50 -0
  6. package/server/api/assets.js +6 -11
  7. package/server/api/board.js +78 -1
  8. package/server/engine/agent/events.js +1 -0
  9. package/server/engine/agent/hooks/user-prompt-submit.js +43 -0
  10. package/server/engine/agent/prompts/nodesign-prelude.md +53 -1
  11. package/server/engine/agent/session-loop.js +11 -0
  12. package/server/engine/mcp/index.js +12 -0
  13. package/server/engine/mcp/tools/arrange-on-board.js +4 -4
  14. package/server/engine/mcp/tools/create-on-board.js +9 -12
  15. package/server/engine/mcp/tools/edit-sketch.js +167 -0
  16. package/server/engine/mcp/tools/look-at-board.js +124 -0
  17. package/server/engine/mcp/tools/read-board.js +121 -34
  18. package/server/engine/mcp/tools/read-user-view.js +59 -0
  19. package/server/engine/mcp/tools/relate-on-board.js +5 -3
  20. package/server/engine/mcp/tools/sketch-on-board.js +337 -0
  21. package/server/engine/mcp/tools/write-on-board.js +130 -0
  22. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/SKILL.md +7 -0
  23. package/server/engine/plugins/nodesign/skills/docx-craft/SKILL.md +7 -0
  24. package/server/engine/plugins/nodesign/skills/rp-craft/SKILL.md +7 -0
  25. package/server/engine/plugins/nodesign/skills/site-craft/SKILL.md +7 -0
  26. package/server/engine/runs/board-tasklist.js +130 -0
  27. package/server/index.js +3 -0
  28. package/server/lib/binding-types.js +12 -0
  29. package/server/lib/board-graph-export.js +191 -0
  30. package/server/lib/board-groups.js +167 -0
  31. package/server/lib/board-hero.js +76 -0
  32. package/server/lib/board-kind-sizes.js +23 -0
  33. package/server/lib/board-relations.js +1 -1
  34. package/server/lib/chalk.js +121 -0
  35. package/server/lib/sketch-layout.js +277 -0
  36. package/server/projects/board-limits.js +12 -0
  37. package/server/projects/board-sanitize.js +252 -0
  38. package/server/projects/board-store.js +72 -205
  39. package/server/projects/ui-config.js +6 -0
  40. package/server/projects/viewpoint-store.js +78 -0
  41. package/server/ws/broker.js +3 -0
  42. package/web/dist/assets/{BrowserWindow-BYCgizHw.js → BrowserWindow-P_vIdh-G.js} +2 -2
  43. package/web/dist/assets/{DeckWindow-pNydEM4L.js → DeckWindow-BpLv5082.js} +3 -3
  44. package/web/dist/assets/{DocxWindow-PPgzh3Gq.js → DocxWindow-DSAU1-tD.js} +4 -4
  45. package/web/dist/assets/MermaidBlock-DR1GikCv.js +313 -0
  46. package/web/dist/assets/SiteWindow-92uBuRRR.js +18 -0
  47. package/web/dist/assets/abnfDiagram-VCTEODGH-uA8Esix4.js +1 -0
  48. package/web/dist/assets/arc-D_khH_xN.js +1 -0
  49. package/web/dist/assets/architectureDiagram-5GKGNRK7-DydJHqOt.js +36 -0
  50. package/web/dist/assets/blockDiagram-NRAW4CY4-B7wBbKiW.js +129 -0
  51. package/web/dist/assets/c4Diagram-UCG6FXSJ-6mrH0x7Y.js +38 -0
  52. package/web/dist/assets/channel-DGkccMO1.js +1 -0
  53. package/web/dist/assets/chunk-2Q5K7J3B-Cuikho5Q.js +1 -0
  54. package/web/dist/assets/chunk-5VM5RSS4-3-KZ5AUz.js +15 -0
  55. package/web/dist/assets/chunk-F27PBJKO-Bkwq2gGU.js +1 -0
  56. package/web/dist/assets/chunk-G27WJ6UU-BLyQzdGE.js +231 -0
  57. package/web/dist/assets/chunk-JWPE2WC7-B7HxxQds.js +1 -0
  58. package/web/dist/assets/chunk-LCL6LL3I-CtXrnUwF.js +206 -0
  59. package/web/dist/assets/chunk-POPQ4Y6H-dxddZlXt.js +1 -0
  60. package/web/dist/assets/chunk-SVP7TREG-DpkmrgHn.js +88 -0
  61. package/web/dist/assets/chunk-XXDRQBXY-BwZ7RbpQ.js +1 -0
  62. package/web/dist/assets/classDiagram-DTDB5LWJ-BGscw3OD.js +1 -0
  63. package/web/dist/assets/classDiagram-v2-JRS7N3AN-BGscw3OD.js +1 -0
  64. package/web/dist/assets/cose-bilkent-JH36ORCC-DGFOsMUt.js +1 -0
  65. package/web/dist/assets/cynefin-OW5HDTMX-DUwn3ehS.js +178 -0
  66. package/web/dist/assets/cynefinDiagram-5FMLGOSQ-DlLAOZ9x.js +62 -0
  67. package/web/dist/assets/cytoscape.esm-BjAbmLdb.js +331 -0
  68. package/web/dist/assets/dagre-3AP2YEHR-CyourLfz.js +4 -0
  69. package/web/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  70. package/web/dist/assets/diagram-S7CK7UJ4-BTMbx2np.js +30 -0
  71. package/web/dist/assets/diagram-UQ7AKVKN-UK94_1-P.js +41 -0
  72. package/web/dist/assets/diagram-VSXAHHWV-C7zS-v9v.js +3 -0
  73. package/web/dist/assets/diagram-VX7I27RA-DKJRa6CK.js +24 -0
  74. package/web/dist/assets/diagram-Z3DM3KII-kUbAfGAp.js +24 -0
  75. package/web/dist/assets/ebnfDiagram-PWID7BFC-CVW16_vb.js +1 -0
  76. package/web/dist/assets/erDiagram-SSCWMZ5O-BRQMFQ5y.js +99 -0
  77. package/web/dist/assets/flowDiagram-A5DVABFB-S34LpLtK.js +168 -0
  78. package/web/dist/assets/ganttDiagram-EL5Y4UJY-BFiqk1-G.js +292 -0
  79. package/web/dist/assets/gitGraphDiagram-WWUBYQGX-LZsrdyyv.js +106 -0
  80. package/web/dist/assets/index-GAamh1p4.js +2152 -0
  81. package/web/dist/assets/infoDiagram-RXCK75RN-0DvN6KIp.js +2 -0
  82. package/web/dist/assets/init-Gi6I4Gst.js +1 -0
  83. package/web/dist/assets/ishikawaDiagram-5VMMS53U-CaY3A-Bx.js +70 -0
  84. package/web/dist/assets/journeyDiagram-EYS64GPL-BBeiaCrj.js +139 -0
  85. package/web/dist/assets/kanban-definition-3QL26DDD-ux42DRXQ.js +89 -0
  86. package/web/dist/assets/layout-CnXiF_NB.js +1 -0
  87. package/web/dist/assets/linear-BMW0u6G6.js +1 -0
  88. package/web/dist/assets/mindmap-definition-FBJOCRG2-hUxSpdKn.js +96 -0
  89. package/web/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  90. package/web/dist/assets/pegDiagram-XKGWAZYB-aCaQPH0w.js +1 -0
  91. package/web/dist/assets/{pending-edit-apply-C0I13CbD.js → pending-edit-apply-AcdOQ9Ij.js} +6 -6
  92. package/web/dist/assets/pieDiagram-E7YTZNPT-CmOEZ8zg.js +39 -0
  93. package/web/dist/assets/quadrantDiagram-AXDQQJYC-VW6fAhsV.js +7 -0
  94. package/web/dist/assets/railroadDiagram-O6MQD6OU-CKJ7Ba5K.js +1 -0
  95. package/web/dist/assets/requirementDiagram-EFPCY7ZU-pNGkq9Vw.js +84 -0
  96. package/web/dist/assets/{rotate-cw-B1UQ8Hld.js → rotate-cw-KAfChbGV.js} +1 -1
  97. package/web/dist/assets/sankeyDiagram-P5KCCOFB-BIMmDTfZ.js +40 -0
  98. package/web/dist/assets/sequenceDiagram-WJ2MYXX4-BBW7wzKY.js +162 -0
  99. package/web/dist/assets/sizeCapture-X5ZJPWSS-DdkXmLYR.js +1 -0
  100. package/web/dist/assets/{square-dashed-mouse-pointer-8lrgqmfv.js → square-dashed-mouse-pointer-CDUlhUP7.js} +1 -1
  101. package/web/dist/assets/stateDiagram-HBIQ2CUA-W313Uz16.js +1 -0
  102. package/web/dist/assets/stateDiagram-v2-4QOOHH4V-DDhJBZzf.js +1 -0
  103. package/web/dist/assets/swimlanes-XN3QIQJK-C29TL26K.js +1 -0
  104. package/web/dist/assets/swimlanesDiagram-VK2B7HYN-BfN-Xdmu.js +8 -0
  105. package/web/dist/assets/timeline-definition-24CTP7MA-CF0JMObX.js +120 -0
  106. package/web/dist/assets/vennDiagram-4TSXK5OY-DrDFsaH3.js +34 -0
  107. package/web/dist/assets/wardleyDiagram-VM6X3IG4-CVrnRIS_.js +78 -0
  108. package/web/dist/assets/xychartDiagram-S5SC5T6Z-BvkN1zsT.js +7 -0
  109. package/web/dist/index.html +1 -1
  110. package/web/dist/assets/SiteWindow-DM0LQPCO.js +0 -18
  111. package/web/dist/assets/index-BHTBJlpY.js +0 -2125
package/README.en.md ADDED
@@ -0,0 +1,184 @@
1
+ [简体中文](https://github.com/Xiaokebuyu/Nodesign/blob/main/README.md) | **English**
2
+
3
+ # NoDesign
4
+
5
+ An infinite canvas, and an agent that stays in the room.
6
+
7
+ Tell it what you want to make, and it creates websites, slide decks, documents, images and video right on the canvas. Everything it produces is saved as a real file you can keep editing, annotate, download and publish.
8
+
9
+ ![demo](https://raw.githubusercontent.com/Xiaokebuyu/Nodesign/main/docs/demo.gif)
10
+
11
+ ## Try it
12
+
13
+ ```bash
14
+ npx @xiaobuyu/nodesign
15
+ ```
16
+
17
+ Requires Node.js 20+ and at least one model provider:
18
+
19
+ - A Claude subscription or an Anthropic API key
20
+ - OpenAI, DeepSeek, Zhipu, Tongyi, OpenRouter
21
+ - Ollama or any other service that speaks the OpenAI API
22
+
23
+ NoDesign charges no subscription fee and takes no cut of model calls. Project files and configuration stay on your machine. When you use a cloud model, the context needed to finish the task is sent to the provider you configured. When you use a local model such as Ollama, nothing leaves your computer.
24
+
25
+ If you would rather not install anything yet, there is a hosted instance: [nodesign.xiaobuyu.trade](https://nodesign.xiaobuyu.trade)
26
+
27
+ The hosted version offers free models with a daily allowance. Some local capabilities are not open there.
28
+
29
+ ## What it can help you make
30
+
31
+ Three typical cases:
32
+
33
+ - **A portfolio page**: put your reference images and text on the canvas, tell the Agent what style you want, check how it looks at desktop, tablet and phone widths, then publish it to the public internet once Cloudflare is configured.
34
+ - **A slide deck**: put photos, text and a reference style on the canvas, the Agent generates paged slides, you annotate and revise directly on them, then export to PDF or PPTX.
35
+ - **A content project**: sites, images, video and documents live on one canvas and are handled by the same Agent, which can keep drawing material from one artifact into another.
36
+
37
+ ### Artifact types
38
+
39
+ | Artifact | Format | What you can do |
40
+ |---|---|---|
41
+ | Decks (slides, tall images, posters) | `.html` | Paged at 16:9, 9:16, 4:3 and other ratios; export to HTML, PDF, PPTX |
42
+ | Sites (portfolios, landing pages, small apps) | A folder with an `index.html` | Desktop, tablet and phone preview; whole-site zip export; one-click publish once Cloudflare is configured |
43
+ | Word documents | `.docx` | Real OOXML written directly, not HTML wrapped up as a Word file. Page-image preview, page flipping, download the original |
44
+ | Images | Common image formats | Generation, background removal |
45
+ | Video | Common video formats | Import, preview and transcoding |
46
+
47
+ ![Say one sentence to generate an image, then remove the background for a transparent PNG](https://raw.githubusercontent.com/Xiaokebuyu/Nodesign/main/docs/demo-image.gif)
48
+
49
+ ![The Agent produces a Word document, with the real layout visible on the canvas](https://raw.githubusercontent.com/Xiaokebuyu/Nodesign/main/docs/demo-docx.gif)
50
+
51
+ These files do not depend on any format private to NoDesign. You can keep editing them in other tools, and download them at any time.
52
+
53
+ Real projects made with NoDesign: a research site about a band, a 15-page Tibet travel deck with a companion site, a pixel-art page for a game server, a résumé `.docx` that went through six revisions. There are experimental ones too, such as an interactive visual novel where you pick the plot on the page and the text is generated in the chat. One published site: [spica-mix.share.xiaobuyu.trade](https://spica-mix.share.xiaobuyu.trade).
54
+
55
+ ## Why it is not just another AI chat box
56
+
57
+ ### The canvas is the workspace
58
+
59
+ Cards on the canvas are real files, and folders are real directories. Moving, editing and tidying cards changes what is on disk.
60
+
61
+ ![Open a folder, drag a card into it, draw a relation between two cards](https://raw.githubusercontent.com/Xiaokebuyu/Nodesign/main/docs/demo-desktop.gif)
62
+
63
+ ### The Agent is present
64
+
65
+ The Agent shows up next to whatever it is working on. You can see which file it is editing and how the content takes shape, instead of waiting for a chat box to return a finished answer.
66
+
67
+ ### The canvas is also a blackboard
68
+
69
+ The Agent does not only put finished artifacts on the canvas, it puts its thinking there too. It sketches, writes notes, and draws lines between related things, to show how it plans to work or to take something complicated apart.
70
+
71
+ ![Taking apart the character relationships in a play: eight people in two families, lines labelled with what each pair is to the other, portraits generated on the spot and wired back to the names](https://raw.githubusercontent.com/Xiaokebuyu/Nodesign/main/docs/demo-blackboard.gif)
72
+
73
+ Those notes are real files, kept under `notes/板书/` in the project. Sketch nodes, lines and notes can all be edited in place with a double click, and the Agent uses your edited version from the next turn on.
74
+
75
+ ![Double-click a note the Agent wrote and edit it in place on the canvas](https://raw.githubusercontent.com/Xiaokebuyu/Nodesign/main/docs/demo-chalk.gif)
76
+
77
+ ### Your actions become context for the next turn
78
+
79
+ You can edit text directly, circle a region, drag elements around, or draw relations such as "reference", "annotate" and "continue from" between artifacts. All of it is collected as context for the next task.
80
+
81
+ ![Circle a region of the cover, write one line, and the Agent picks it up and revises it](https://raw.githubusercontent.com/Xiaokebuyu/Nodesign/main/docs/demo-roundtrip.gif)
82
+
83
+ ### The Agent can check its own results
84
+
85
+ It can take screenshots at different screen widths and read computed styles, console errors, font loading state and animation frames, then keep revising based on what it finds. When it is done it screenshots the result and checks it. If you are still unhappy after two rounds, it asks a read-only review subagent to go through the pages.
86
+
87
+ ![A site previewed at desktop, tablet and phone widths](https://raw.githubusercontent.com/Xiaokebuyu/Nodesign/main/docs/demo-viewport.gif)
88
+
89
+ ### Memory is visible and editable
90
+
91
+ Project decisions, style notes and personal preferences do not stay buried in the conversation history. They live in the workspace as content you can see, and they can be crystallized into reusable Skills.
92
+
93
+ ## Local vs hosted
94
+
95
+ | | Local `npx @xiaobuyu/nodesign` | Hosted [nodesign.xiaobuyu.trade](https://nodesign.xiaobuyu.trade) |
96
+ |---|---|---|
97
+ | Models | The ones you configure (Claude subscription, API key, or any OpenAI-compatible service) | Free models with a daily allowance |
98
+ | Data | Kept in `~/.nodesign/`; the server listens on `127.0.0.1` only | Stored on the server, isolated per user |
99
+ | Account | None needed, open it and go | Open registration |
100
+ | Capabilities | All of them. Screenshots, Word, background removal, image generation and so on are detected from your machine. Publishing sites needs Cloudflare Pages | Screenshots, search and image generation are open. Site publishing and subscription models are not open to the public |
101
+ | Cost | No subscription, no markup. Model usage is billed by the provider you configured | Free |
102
+
103
+ The local version is the main one. The hosted one is there so you can try it without installing anything.
104
+
105
+ ## Configuration
106
+
107
+ Click the gear in the top right after starting. The settings page has four areas:
108
+
109
+ - **Models**: two cards side by side. "Claude official" takes an API key, or logs into a Claude subscription with `nodesign login`. "Custom endpoint" starts from a provider preset (DeepSeek, OpenAI, Zhipu, Tongyi, OpenRouter, Ollama and others) and takes a base URL and a key. Every model configuration can run a connection test that actually checks plain chat, streaming, tool calls, image understanding and token counting. With no model configured, the model picker is empty.
110
+ - **Local capabilities**: on startup it probes git, Chromium, LibreOffice, poppler, ffmpeg, rembg, image generation, search and publishing. Anything missing is listed with how to install it, and the tools that need it are shown as unavailable with the same instructions.
111
+ - **Other settings**: search (four providers, pick one), the image generation channel, Cloudflare publishing, sandbox and permission mode.
112
+ - **Status**: data directory, config file paths, restart.
113
+
114
+ Configuration lives in `~/.nodesign/.env` (secrets) and `~/.nodesign/config.json` (model slots). You can edit both by hand.
115
+
116
+ ## Security and privacy
117
+
118
+ Each project gets its own workspace directory. Local data is kept in `~/.nodesign/` by default, and the server listens on `127.0.0.1` only.
119
+
120
+ On supported platforms you can turn on an OS-level command sandbox:
121
+
122
+ - Linux: bubblewrap
123
+ - macOS: sandbox-exec
124
+ - Windows: no OS-level command sandbox at the moment
125
+
126
+ You can also turn on automatic permission checks, which add a second judgement on file uploads, outbound requests and other sensitive operations.
127
+
128
+ The local version ships with the command sandbox and the automatic permission checks off. Turn them on in settings according to how you use it. On Windows, open only projects you trust, and confirm the Agent's plan before it touches files outside the workspace, installs software or uploads anything.
129
+
130
+ ## Project status
131
+
132
+ A personal project, started at the end of April 2026 and still moving quickly. The current local release is `0.0.4`, and the core features have been in steady use during the private beta.
133
+
134
+ ### Features
135
+
136
+ | Capability | Current state |
137
+ |---|---|
138
+ | Canvas and project management | Running as the main build of the private beta |
139
+ | Site generation, preview and publishing | Stable |
140
+ | Deck generation and export | Stable |
141
+ | Word documents | Usable. Page breaks in the preview may differ from Microsoft Word |
142
+ | Image and video tools | Usable. What is available depends on local dependencies and service configuration |
143
+ | Interactive performance mode | Experimental, not fully in the public release yet |
144
+
145
+ ### Platform support
146
+
147
+ | Platform | State |
148
+ |---|---|
149
+ | Linux | Running in production |
150
+ | Windows | Installed and verified on real hardware |
151
+ | macOS | The code paths exist, not yet verified on real hardware |
152
+ | Mobile | You can browse and chat; organizing and editing are better on a computer |
153
+
154
+ ## Architecture
155
+
156
+ - **Frontend**: React + Vite. The infinite canvas camera, hit testing, relation layout and the artifact capability system are written from scratch (web/src/lib, 45 pure-function modules with unit tests).
157
+ - **Backend**: Node.js ESM. An Agent session runs with the project workspace as its working directory, and works through 56 in-process tools that operate on files, the browser and the different artifact types.
158
+ - **Session sync**: the server holds session state, with streaming output, reconnection after a drop, and multiple tabs staying in sync.
159
+ - **Model compatibility**: Claude is supported natively, and OpenAI-compatible services are reached through a translation layer. When a request cannot be matched to an upstream it fails outright, so nothing is quietly sent to the wrong model service.
160
+ - **Artifact system**: sites, decks and Word documents plug into preview, export and publishing through one registry. A new artifact type joins those flows through the same entry point.
161
+
162
+ ## Development
163
+
164
+ ```bash
165
+ npm install && cd web && npm install && cd ..
166
+ npm run dev # server, reads .env
167
+ cd web && npm run dev # frontend
168
+ npm test # server tests + web tests
169
+ ```
170
+
171
+ > Note: running the whole thing needs a model provider configured (a Claude subscription or an API key) and some local tool dependencies. The settings page lists what is missing and how to install it.
172
+
173
+ The Vitest suite is the gate before shipping: 1021 cases across server and web, covering the client/server contracts, module boundaries, the permission capability table and key user-facing copy. Some constraints are pinned by static tests rather than left to comments and habit. For example:
174
+
175
+ - The two capability tables, client and server, are reconciled item by item
176
+ - Permission decisions have to go through the capability table
177
+ - Wording checks on user-facing copy
178
+ - A line-count ratchet on source files
179
+
180
+ The frontend is deployed with `web/scripts/deploy.sh` (new chunks added, old chunks kept, `index.html` replaced atomically). Server changes need a process restart.
181
+
182
+ ## License
183
+
184
+ AGPL-3.0. Use it, change it, run your own instance. If you offer it to others as a service, your changes have to be open-sourced too.
package/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ **简体中文** | [English](https://github.com/Xiaokebuyu/Nodesign/blob/main/README.en.md)
2
+
1
3
  # NoDesign
2
4
 
3
5
  一张无限画布,一个始终在场的 Agent。
@@ -42,6 +44,10 @@ NoDesign 本身不收取订阅费,也不对模型调用加价。项目文件
42
44
  | 图片 | 常见图片格式 | 生图、抠图 |
43
45
  | 视频 | 常见视频格式 | 视频导入、预览与转码 |
44
46
 
47
+ ![说一句话生成图片,再抠掉背景,得到透明底 PNG](https://raw.githubusercontent.com/Xiaokebuyu/Nodesign/main/docs/demo-image.gif)
48
+
49
+ ![Agent 生成 Word 文档,在画布上直接看排版](https://raw.githubusercontent.com/Xiaokebuyu/Nodesign/main/docs/demo-docx.gif)
50
+
45
51
  这些文件不依赖 NoDesign 的私有格式,可以使用其他工具继续编辑和下载。
46
52
 
47
53
  使用 NoDesign 完成的实际项目:一个乐队的研究站、一份 15 页的西藏攻略幻灯配站点、一套像素风服务器宣传页、一份改了六版的简历 .docx。也有实验性项目,比如一个在页面上选择剧情、在聊天里生成内容的互动视觉小说。已发布的一个站点:[spica-mix.share.xiaobuyu.trade](https://spica-mix.share.xiaobuyu.trade)。
@@ -58,6 +64,16 @@ NoDesign 本身不收取订阅费,也不对模型调用加价。项目文件
58
64
 
59
65
  Agent 会出现在它正在处理的内容旁边。你可以看到它正在编辑哪个文件,以及内容如何逐步生成,而不是只能等待聊天框返回最终结果。
60
66
 
67
+ ### 画布也是黑板
68
+
69
+ Agent 不只是把做完的产物放上画布,也把想法放上去。它会画草图、写板书、在相关的东西之间拉线,说明自己打算怎么做,或者把一件复杂的事情拆开摆出来。
70
+
71
+ ![拆《雷雨》的人物关系:八个人分成两家,关系线标出谁和谁是什么,主要人物的肖像当场生成后连回名字](https://raw.githubusercontent.com/Xiaokebuyu/Nodesign/main/docs/demo-blackboard.gif)
72
+
73
+ 板书是真文件,保存在项目的 `notes/板书/` 目录下。草图节点、连线和板书都可以双击直接改,改完 Agent 接着用改过的版本。
74
+
75
+ ![双击 Agent 写的板书,在画布上原地改字](https://raw.githubusercontent.com/Xiaokebuyu/Nodesign/main/docs/demo-chalk.gif)
76
+
61
77
  ### 你的操作会成为下一轮上下文
62
78
 
63
79
  你可以直接修改文字、圈选区域、拖动元素,或者在不同产物之间建立「参考」「批注」「接着做」等关系。这些操作会被整理为后续任务的上下文。
@@ -113,7 +129,7 @@ Agent 会出现在它正在处理的内容旁边。你可以看到它正在编
113
129
 
114
130
  ## 项目状态
115
131
 
116
- 个人项目,2026 年 4 月底起步,目前仍在快速迭代。版本号 `0.0.1` 是本地发行版的首个公开版本,核心功能已在内测中稳定使用。
132
+ 个人项目,2026 年 4 月底起步,目前仍在快速迭代。本地发行版当前是 `0.0.4`,核心功能已在内测中稳定使用。
117
133
 
118
134
  ### 功能
119
135
 
@@ -138,7 +154,7 @@ Agent 会出现在它正在处理的内容旁边。你可以看到它正在编
138
154
  ## 技术架构
139
155
 
140
156
  - **前端**:React + Vite。无限画布的相机、命中检测、关系排布和产物能力系统为自研实现(web/src/lib,45 个纯函数模块,配有单测)。
141
- - **服务端**:Node.js ESM。Agent 会话以项目工作区作为执行目录,通过约 50 个进程内工具操作文件、浏览器和不同类型的产物。
157
+ - **服务端**:Node.js ESM。Agent 会话以项目工作区作为执行目录,通过 56 个进程内工具操作文件、浏览器和不同类型的产物。
142
158
  - **会话同步**:服务端维护会话状态,支持流式输出、断线恢复和多标签页同步。
143
159
  - **模型兼容**:原生支持 Claude,并通过格式转换接入兼容 OpenAI API 的模型服务。无法匹配上游时直接返回错误,避免请求被发送到错误的模型服务。
144
160
  - **产物系统**:网站、演示稿和 Word 文档通过统一的注册机制接入预览、导出和发布流程。新增产物类型时,通过同一注册入口接入相关能力。
@@ -154,7 +170,7 @@ npm test # server 测试 + web 测试
154
170
 
155
171
  > 注意:完整运行需要配置模型接入(Claude 订阅或 API Key)以及部分本地工具依赖。设置页会列出缺少的依赖和安装方法。
156
172
 
157
- 项目以 Vitest 测试套件作为发布前检查,server 和 web 合计 980 项用例,覆盖前后端契约、模块边界、权限能力表和关键用户文案。部分约束以静态测试固化,避免仅依赖注释和人工约定。例如:
173
+ 项目以 Vitest 测试套件作为发布前检查,server 和 web 合计 1021 项用例,覆盖前后端契约、模块边界、权限能力表和关键用户文案。部分约束以静态测试固化,避免仅依赖注释和人工约定。例如:
158
174
 
159
175
  - 前后端双份能力表逐项对账
160
176
  - 权限判断必须通过能力表查询
@@ -166,25 +182,3 @@ npm test # server 测试 + web 测试
166
182
  ## 许可
167
183
 
168
184
  AGPL-3.0。可以使用,可以修改,可以自行部署。对外提供服务时,修改需要一并开源。
169
-
170
- ---
171
-
172
- ## English overview
173
-
174
- **NoDesign** is an agent workbench for independent creators. An infinite canvas serves as your workspace: describe what you want, and the agent builds websites, slide decks, images, video and Word documents as real files on disk, right on the canvas. You can edit text in place, circle a region and annotate, drag elements around, and draw relations between cards. The agent picks all of that up on its next turn.
175
-
176
- ```bash
177
- npx @xiaobuyu/nodesign
178
- ```
179
-
180
- Requires Node.js 20+ and at least one model provider: a Claude subscription (`npx @xiaobuyu/nodesign login`), an Anthropic API key, or any OpenAI-compatible endpoint (DeepSeek, OpenAI, OpenRouter, local Ollama, etc.). Runs on your machine with your own key. No subscription fee, no markup on model calls. Project files stay local; when using cloud models, task context is sent to the provider you configured. A hosted instance is available at [nodesign.xiaobuyu.trade](https://nodesign.xiaobuyu.trade) for trying it without installing.
181
-
182
- What makes it different from a chat interface:
183
-
184
- - **The canvas is a real workspace.** Every card is a file, every folder is a directory. Dragging a card into a folder actually moves the file on disk.
185
- - **The agent is present.** It appears next to the card it's editing, and you can see code being written in real time.
186
- - **Your actions feed back.** Direct text edits, region selections, element dragging, and relation lines ("reference", "annotate", "continue") all become context for the next turn.
187
- - **The agent inspects its own work.** It captures screenshots at different viewport widths, reads computed styles, console errors, font loading status, and animation filmstrips, then continues editing based on what it finds.
188
- - **Memory is visible.** Decisions, style notes, and preferences live in the workspace as editable content, and can be crystallized into reusable Skills.
189
-
190
- Personal project, started late April 2026, actively developed. Version 0.0.1 is the first public local release. 980 Vitest cases as the deploy gate. Production runs on Linux; tested on Windows; macOS code paths exist but are not yet verified on real hardware. Licensed AGPL-3.0.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xiaobuyu/nodesign",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "NoDesign — 面向独立创作者的 agent 创作工作台(本地分发版:npx nodesign)",
@@ -12,6 +12,7 @@
12
12
  "server/",
13
13
  "web/dist/",
14
14
  "README.md",
15
+ "README.en.md",
15
16
  "LICENSE",
16
17
  "!server/**/*.test.js",
17
18
  "!server/**/_probe-*",
@@ -7,12 +7,32 @@
7
7
  * 不会再随下一次拆分复发(谁也不用记得"这个函数该跟着谁走")。
8
8
  */
9
9
 
10
+ import { promises as fs } from 'node:fs';
11
+ import { parseChalk } from '../../lib/chalk.js';
12
+
10
13
  /** 单层路径片段:不含分隔符、不含 `..`、不以点开头、长度可控 */
11
14
  export function safeSegment(s) {
12
15
  return typeof s === 'string' && !!s && s.length <= 200
13
16
  && !s.includes('/') && !s.includes('\\') && !s.includes('..') && !s.startsWith('.');
14
17
  }
15
18
 
19
+ /**
20
+ * 便签/板书正文上卡(2026-08-23 从 assets.js 抽出):frontmatter 带 nd: chalk 的是板书
21
+ * (agent/用户写在画布上的话,同一形态另一张脸),其余按便签头解析;正文 ≤4KB 截断。
22
+ * 读不到就静默(agent 正在写的文件可能半途消失)。
23
+ */
24
+ export async function decorateNoteText(item, absPath) {
25
+ try {
26
+ const raw = await fs.readFile(absPath, 'utf8');
27
+ const parsed = parseChalk(raw);
28
+ const { body, sessionId } = parsed.chalk ? parsed : parseNoteFrontmatter(raw);
29
+ item.text = body.length > 4096 ? body.slice(0, 4096) + '…' : body;
30
+ if (sessionId) item.sessionId = sessionId;
31
+ if (parsed.chalk) item.chalk = parsed.chalk;
32
+ } catch { /* */ }
33
+ return item;
34
+ }
35
+
16
36
  /** 便签 frontmatter:只认最简单的 `---\nsession: xxx\n---` 头,其余原样当正文 */
17
37
  export function parseNoteFrontmatter(raw) {
18
38
  const m = /^---\n([\s\S]{0,500}?)\n---\n?/.exec(raw);
@@ -13,6 +13,7 @@ import express from 'express';
13
13
  import path from 'node:path';
14
14
  import { promises as fs } from 'node:fs';
15
15
  import { safeSegment, parseNoteFrontmatter } from './helpers.js';
16
+ import { patchBoard } from '../../projects/board-store.js';
16
17
 
17
18
  /**
18
19
  * @param {object} deps
@@ -123,6 +124,55 @@ export function mountNotesRoutes({ router, guardProject, getSharedDir, ensurePro
123
124
  } catch (err) { next(err); }
124
125
  });
125
126
 
127
+ /**
128
+ * 板书(notes/板书/<name>.md,2026-08-23):用户侧改 / 删。名字是单段文件名,
129
+ * 目录固定 —— 跟便利贴同一套 safeNoteSegment 闸。
130
+ */
131
+ const resolveChalk = (pid, name) => {
132
+ const base = path.join(getSharedDir(pid), 'notes', '板书');
133
+ const file = path.resolve(base, name);
134
+ return file.startsWith(base + path.sep) ? file : null;
135
+ };
136
+ router.put('/:pid/chalk/:name', express.json(), async (req, res, next) => {
137
+ try {
138
+ if (!guardProject(req, res)) return;
139
+ const { name } = req.params;
140
+ if (!safeNoteSegment(name, { md: true })) return res.status(400).json({ error: 'invalid filename' });
141
+ const text = String(req.body?.text ?? '');
142
+ if (!text.trim()) return res.status(400).json({ error: 'text required' });
143
+ if (text.length > 20_000) return res.status(400).json({ error: 'too long (max 20k chars)' });
144
+ const file = resolveChalk(req.params.pid, name);
145
+ if (!file) return res.status(400).json({ error: 'invalid path' });
146
+ // frontmatter(nd:chalk/anchor/reply_to/tag)由服务端保住:正文只收正文。客户端拼头那条路
147
+ // 在 fetch 失败时会丢头、还会把 4KB 截断的展示副本写回(fable 08-23 P2)。
148
+ // 来的文本若自带头(老客户端拼过来的)剥掉,以磁盘上的头为准;磁盘上没有就保留来的头。
149
+ let head = '';
150
+ try { head = /^---\n[\s\S]{0,800}?\n---\n?/.exec(await fs.readFile(file, 'utf8'))?.[0] || ''; } catch { /* 新文件 */ }
151
+ const incomingHead = /^---\n[\s\S]{0,800}?\n---\n?/.exec(text)?.[0] || '';
152
+ const body = text.slice(incomingHead.length).replace(/^\n+/, '');
153
+ if (!body.trim()) return res.status(400).json({ error: 'text required' });
154
+ await fs.mkdir(path.dirname(file), { recursive: true });
155
+ await fs.writeFile(file, `${head || incomingHead}${head || incomingHead ? '\n' : ''}${body.replace(/\n+$/, '')}\n`, 'utf8');
156
+ res.json({ ok: true, path: `notes/板书/${name}` });
157
+ } catch (err) { next(err); }
158
+ });
159
+ router.delete('/:pid/chalk/:name', async (req, res, next) => {
160
+ try {
161
+ if (!guardProject(req, res)) return;
162
+ const { name } = req.params;
163
+ if (!safeNoteSegment(name, { md: true })) return res.status(400).json({ error: 'invalid filename' });
164
+ const file = resolveChalk(req.params.pid, name);
165
+ if (!file) return res.status(400).json({ error: 'invalid path' });
166
+ try { await fs.unlink(file); } catch (err) {
167
+ if (err.code === 'ENOENT') return res.status(404).json({ error: 'not found' });
168
+ throw err;
169
+ }
170
+ // 座位和线一起清,别留指向已删文件的幽灵条目(read_board 会把它当"摆过的"列出来)
171
+ try { await patchBoard(req.params.pid, { objects: { [`notes/板书/${name}`]: null } }); } catch { /* 清不掉不挡删除 */ }
172
+ res.status(204).end();
173
+ } catch (err) { next(err); }
174
+ });
175
+
126
176
  /** DELETE /:pid/task-notes/:filename — 删便利贴 */
127
177
  router.delete('/:pid/task-notes/:filename', async (req, res, next) => {
128
178
  try {
@@ -28,7 +28,8 @@ import { resolveArtifactFile, isServablePath } from '../lib/artifact-file-path.j
28
28
  import { getProjectCover } from '../lib/cover.js';
29
29
  import { makeDocxPageHandler, makeDocxPdfHandler } from './assets/docx-page.js';
30
30
  import { mountNotesRoutes } from './assets/notes.js';
31
- import { safeSegment, parseNoteFrontmatter } from './assets/helpers.js';
31
+ import { safeSegment, decorateNoteText } from './assets/helpers.js';
32
+ import { CHALK_DIR } from '../lib/chalk.js';
32
33
  import {
33
34
  sendImage, isThumbPath, findOriginalForThumbnail, imageCacheControl,
34
35
  THUMBNAIL_MAX_DIM, THUMBNAIL_QUALITY,
@@ -348,16 +349,8 @@ router.get('/:pid/artifacts', async (req, res, next) => {
348
349
  item.meta = JSON.parse(metaRaw);
349
350
  } catch { /* 无 sidecar(旧图)→ 无 meta */ }
350
351
  }
351
- // 便签直接把正文带给前端渲卡片(≤4KB 截断,完整内容走 artifact-file)。
352
- // frontmatter 里的 session 字段是分区归属(agent 写便签时按约定带上)。
353
- if (kind === 'note' && ext === '.md') {
354
- try {
355
- const raw = await fs.readFile(path.join(dir, e.name), 'utf8');
356
- const { body, sessionId } = parseNoteFrontmatter(raw);
357
- item.text = body.length > 4096 ? body.slice(0, 4096) + '…' : body;
358
- if (sessionId) item.sessionId = sessionId;
359
- } catch { /* */ }
360
- }
352
+ // 便签/板书把正文带给前端渲卡片(≤4KB 截断,完整内容走 artifact-file;见 helpers.decorateNoteText)
353
+ if (kind === 'note' && ext === '.md') await decorateNoteText(item, path.join(dir, e.name));
361
354
  artifacts.push(item);
362
355
  }
363
356
  };
@@ -497,6 +490,8 @@ router.get('/:pid/artifacts', async (req, res, next) => {
497
490
  // 24 条产物里 6 条重复,画布上就是一张卡叠着另一张同名卡。
498
491
  await scanDir(workspaceRoot, 'task-file', '', 0);
499
492
  await scanDir(path.join(workspaceRoot, 'notes'), 'note', 'notes');
493
+ // 板书住 notes/板书/(不进 notes/ 顶层:便利贴注入只列顶层,板书另有自己的注入)
494
+ await scanDir(path.join(workspaceRoot, CHALK_DIR), 'note', CHALK_DIR);
500
495
  for (const rel of folders) {
501
496
  await scanDir(path.join(workspaceRoot, rel), 'task-file', rel, 0);
502
497
  }
@@ -12,7 +12,10 @@
12
12
  import express from 'express';
13
13
  import { validateProjectId, getProject } from '../projects/store.js';
14
14
  import { guardProject } from './_guard.js';
15
- import { readBoard, replaceBoard, patchBoard } from '../projects/board-store.js';
15
+ import { readBoard, replaceBoard, patchBoard, commitStaging, removeByTag } from '../projects/board-store.js';
16
+ import { setViewpoint, getViewpoint } from '../projects/viewpoint-store.js';
17
+ import { exportGraph, exportGraphZip } from '../lib/board-graph-export.js';
18
+ import { getSharedDir } from '../projects/workspace.js';
16
19
 
17
20
  const router = express.Router();
18
21
 
@@ -54,4 +57,78 @@ router.put('/:pid/board', express.json({ limit: '600kb' }), async (req, res, nex
54
57
  }
55
58
  });
56
59
 
60
+ /**
61
+ * 用户视点上报(2026-08-23 黑板):前端节流 POST,服务端只留最近一份。
62
+ * 不广播、不落盘;失败也不该打扰用户(前端 fire-and-forget)。
63
+ */
64
+ router.post('/:pid/viewpoint', express.json({ limit: '16kb' }), async (req, res, next) => {
65
+ try {
66
+ if (!guard(req, res)) return;
67
+ const v = setViewpoint(req.params.pid, req.body?.viewpoint, req.user?.id || null);
68
+ res.json({ ok: !!v });
69
+ } catch (err) { next(err); }
70
+ });
71
+
72
+ /** 读最近一份视点(排障用:看看服务端眼里"用户在看哪";agent 走 read_user_view) */
73
+ router.get('/:pid/viewpoint', async (req, res, next) => {
74
+ try {
75
+ if (!guard(req, res)) return;
76
+ res.json({ viewpoint: getViewpoint(req.params.pid) });
77
+ } catch (err) { next(err); }
78
+ });
79
+
80
+ /** 草稿落定(用户也可以按组落定:黑板上 agent 的草稿半透明,点一下变实) */
81
+ router.post('/:pid/board/commit', express.json({ limit: '16kb' }), async (req, res, next) => {
82
+ try {
83
+ if (!guard(req, res)) return;
84
+ const tag = typeof req.body?.tag === 'string' ? req.body.tag : null;
85
+ const { board, committed } = await commitStaging(req.params.pid, { tag });
86
+ res.json({ ok: true, committed, board });
87
+ } catch (err) { next(err); }
88
+ });
89
+
90
+ /** 黑板擦:按 #tag 整组删(只删画布原生物件与线;产物卡只摘标签) */
91
+ router.post('/:pid/board/erase', express.json({ limit: '16kb' }), async (req, res, next) => {
92
+ try {
93
+ if (!guard(req, res)) return;
94
+ const tag = typeof req.body?.tag === 'string' ? req.body.tag : '';
95
+ if (!tag) return res.status(400).json({ error: 'tag required' });
96
+ const { board, removed } = await removeByTag(req.params.pid, tag);
97
+ res.json({ ok: true, removed, board });
98
+ } catch (err) { next(err); }
99
+ });
100
+
101
+ /**
102
+ * 连接图导出(2026-08-23 黑板):?format=json|mermaid|svg&tag=&layer=
103
+ * 真相是 board.json,这里只派生。download=1 时带附件头。
104
+ */
105
+ router.get('/:pid/board/graph', async (req, res, next) => {
106
+ try {
107
+ if (!guard(req, res)) return;
108
+ const format = String(req.query.format || 'json');
109
+ const tag = typeof req.query.tag === 'string' && req.query.tag ? req.query.tag.slice(0, 40) : null;
110
+ const layer = typeof req.query.layer === 'string' ? req.query.layer.slice(0, 300) : '';
111
+ const board = await readBoard(req.params.pid);
112
+ if (format === 'zip') {
113
+ const projectName = getProject(req.params.pid)?.name || '画布';
114
+ const { zip } = await exportGraphZip(board, { workspaceRoot: getSharedDir(req.params.pid), tag, layer, projectName });
115
+ const buf = await zip.generateAsync({ type: 'nodebuffer', compression: 'DEFLATE' });
116
+ res.setHeader('Content-Type', 'application/zip');
117
+ res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(`board${tag ? `-${tag}` : ''}.zip`)}`);
118
+ return res.send(buf);
119
+ }
120
+ const { mime, body } = exportGraph(board, { format, tag, layer });
121
+ res.setHeader('Content-Type', mime);
122
+ if (req.query.download) {
123
+ const ext = format === 'mermaid' ? 'mmd' : format;
124
+ const name = `board${tag ? `-${tag}` : ''}.${ext}`;
125
+ res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(name)}`);
126
+ }
127
+ res.send(body);
128
+ } catch (err) {
129
+ if (err?.status === 400) return res.status(400).json({ error: err.message });
130
+ next(err);
131
+ }
132
+ });
133
+
57
134
  export default router;
@@ -41,6 +41,7 @@
41
41
  * run.rate_limit { info } rate limit 变化
42
42
  * run.image_generated { path, sizeBytes, prompt, assetRole, ... } generate_image 完成(Phase A)
43
43
  * board.updated { objectId, zoneId, summary } pin_to_board 改画布布局(sessionId:null 广播,前端整份重拉 board.json)
44
+ * board.focus { rect, tag, layer } agent 落了一张草图(sessionId:null 广播;前端只在黑板模式跟镜头)
44
45
  * project.active_session { activeSessionId } 项目级会话指针变更(故意不带 sessionId —— 见构造器注释)
45
46
  *
46
47
  * 外层把 EventBus 桥接到:
@@ -26,6 +26,11 @@ import path from 'node:path';
26
26
  import { readUiConfigFile } from '../../../projects/ui-config.js';
27
27
  import { readAssetsSummary } from '../../../projects/assets-summary.js';
28
28
  import { relationsDigest } from '../../../lib/board-relations.js';
29
+ import { getViewpoint, describeViewpoint } from '../../../projects/viewpoint-store.js';
30
+ import { recentChalk, CHALK_DIR } from '../../../lib/chalk.js';
31
+ import { readBoard } from '../../../projects/board-store.js';
32
+ import { estimateSizeOn } from '../../../lib/board-kind-sizes.js';
33
+ import { layerOf } from '../../../lib/canvas-id.js';
29
34
  import {
30
35
  getActiveArtifact, listWorkspaceArtifacts, taskManifest, kindDef,
31
36
  KIND_DECK, KIND_SITE, ENTRY_FILE,
@@ -107,6 +112,25 @@ async function collectSections({ workspaceRoot, sessionId, projectId }) {
107
112
  } catch { /* notes/ 不存在:noop */ }
108
113
 
109
114
  // 画布关系线:用户画的排前面
115
+ // 用户视点(2026-08-23 黑板):一行,只在变化时进注入(renderTurnState 按 hash 判)。
116
+ // 视口矩形按 1/8 视口量化再进 hash,不然相机挪一像素就算"变了"。
117
+ try {
118
+ const vp = getViewpoint(projectId);
119
+ if (vp) {
120
+ const board = await readBoard(projectId);
121
+ const known = new Set(Object.keys(board.zones || {}));
122
+ const rects = Object.entries(board.objects || {})
123
+ .filter(([id, e]) => Number.isFinite(e?.x) && layerOf(id, e, known) === (vp.layer || ''))
124
+ .map(([id, e]) => ({ id, x: e.x, y: e.y, ...estimateSizeOn(board, id, e) }));
125
+ const q = vp.camera ? { ...vp, camera: {
126
+ x: Math.round(vp.camera.x / Math.max(1, vp.camera.w / 8)) * Math.round(vp.camera.w / 8),
127
+ y: Math.round(vp.camera.y / Math.max(1, vp.camera.h / 8)) * Math.round(vp.camera.h / 8),
128
+ w: vp.camera.w, h: vp.camera.h,
129
+ } } : vp;
130
+ const line = describeViewpoint(q, rects);
131
+ if (line) sections.push({ key: 'viewpoint', title: '用户视点', text: `用户此刻在画布上:${line}。他说「这个/这里/这张」多半指选中的 > 开着的窗 > 视口里的东西;要细节调 read_user_view。` });
132
+ }
133
+ } catch { /* 视点读不到就沉默 */ }
110
134
  try {
111
135
  const digest = await relationsDigest(projectId, { limit: 12 });
112
136
  if (digest) {
@@ -137,6 +161,25 @@ async function collectSections({ workspaceRoot, sessionId, projectId }) {
137
161
  } catch { /* 扫不动就不说 */ }
138
162
 
139
163
  // tweaks 开关:文件不存在 = 用户没碰过 = 沉默
164
+ // 最近板书(2026-08-23):你/用户在画布上说过的最近几句 —— 对话在板上,得记得板上说了什么
165
+ try {
166
+ const recent = await recentChalk(workspaceRoot, { limit: 8 });
167
+ if (recent.length) {
168
+ const lines = recent.map(c => ` ${c.path}(${c.by === 'user' ? '用户' : '你'}${c.anchor ? `,关于 ${c.anchor}` : ''}${c.replyTo ? `,回应 ${c.replyTo.replace(`${CHALK_DIR}/`, '')}` : ''})「${c.first}」`);
169
+ sections.push({ key: 'chalk', title: '最近板书', text: `画布上最近的板书(${CHALK_DIR}/,新在前;正文 Read 文件):\n${lines.join('\n')}`, items: recent.map(c => c.path) });
170
+ }
171
+ } catch { /* 板书读不到就沉默 */ }
172
+ // 黑板模式(2026-08-23):用户在画布上专注思考时开。这一节只在开着时注入,关着不提。
173
+ try {
174
+ const cfg = await readUiConfigFile(workspaceRoot);
175
+ if (cfg?.blackboard_mode === true) {
176
+ sections.push({ key: 'blackboard', title: '黑板模式', text:
177
+ '【黑板模式:开】用户此刻在画布上专注思考。这一轮默认这么做:想事情就画成图(sketch_on_board,'
178
+ + '小改动用 edit_sketch 原地改别重画);做完一件东西在它旁边写一条板书(write_on_board near=);'
179
+ + '用户标注了板上的东西就接在那条下面回(reply_to=)。侧栏照常回复,但板上已经写的别大段重复。'
180
+ + '尺寸守规范(0.8 倍一屏可读、正文 md 起、一条板书说一件事);画完 look_at_board 看一眼再收。' });
181
+ }
182
+ } catch { /* 读失败:不注入 */ }
140
183
  try {
141
184
  const cfg = await readUiConfigFile(workspaceRoot);
142
185
  if (cfg) {