@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.
- package/README.en.md +184 -0
- package/README.md +19 -25
- package/package.json +2 -1
- package/server/api/assets/helpers.js +20 -0
- package/server/api/assets/notes.js +50 -0
- package/server/api/assets.js +6 -11
- package/server/api/board.js +78 -1
- package/server/engine/agent/events.js +1 -0
- package/server/engine/agent/hooks/user-prompt-submit.js +43 -0
- package/server/engine/agent/prompts/nodesign-prelude.md +53 -1
- package/server/engine/agent/session-loop.js +11 -0
- package/server/engine/mcp/index.js +12 -0
- package/server/engine/mcp/tools/arrange-on-board.js +4 -4
- package/server/engine/mcp/tools/create-on-board.js +9 -12
- package/server/engine/mcp/tools/edit-sketch.js +167 -0
- package/server/engine/mcp/tools/look-at-board.js +124 -0
- package/server/engine/mcp/tools/read-board.js +121 -34
- package/server/engine/mcp/tools/read-user-view.js +59 -0
- package/server/engine/mcp/tools/relate-on-board.js +5 -3
- package/server/engine/mcp/tools/sketch-on-board.js +337 -0
- package/server/engine/mcp/tools/write-on-board.js +130 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/SKILL.md +7 -0
- package/server/engine/plugins/nodesign/skills/docx-craft/SKILL.md +7 -0
- package/server/engine/plugins/nodesign/skills/rp-craft/SKILL.md +7 -0
- package/server/engine/plugins/nodesign/skills/site-craft/SKILL.md +7 -0
- package/server/engine/runs/board-tasklist.js +130 -0
- package/server/index.js +3 -0
- package/server/lib/binding-types.js +12 -0
- package/server/lib/board-graph-export.js +191 -0
- package/server/lib/board-groups.js +167 -0
- package/server/lib/board-hero.js +76 -0
- package/server/lib/board-kind-sizes.js +23 -0
- package/server/lib/board-relations.js +1 -1
- package/server/lib/chalk.js +121 -0
- package/server/lib/sketch-layout.js +277 -0
- package/server/projects/board-limits.js +12 -0
- package/server/projects/board-sanitize.js +252 -0
- package/server/projects/board-store.js +72 -205
- package/server/projects/ui-config.js +6 -0
- package/server/projects/viewpoint-store.js +78 -0
- package/server/ws/broker.js +3 -0
- package/web/dist/assets/{BrowserWindow-BYCgizHw.js → BrowserWindow-P_vIdh-G.js} +2 -2
- package/web/dist/assets/{DeckWindow-pNydEM4L.js → DeckWindow-BpLv5082.js} +3 -3
- package/web/dist/assets/{DocxWindow-PPgzh3Gq.js → DocxWindow-DSAU1-tD.js} +4 -4
- package/web/dist/assets/MermaidBlock-DR1GikCv.js +313 -0
- package/web/dist/assets/SiteWindow-92uBuRRR.js +18 -0
- package/web/dist/assets/abnfDiagram-VCTEODGH-uA8Esix4.js +1 -0
- package/web/dist/assets/arc-D_khH_xN.js +1 -0
- package/web/dist/assets/architectureDiagram-5GKGNRK7-DydJHqOt.js +36 -0
- package/web/dist/assets/blockDiagram-NRAW4CY4-B7wBbKiW.js +129 -0
- package/web/dist/assets/c4Diagram-UCG6FXSJ-6mrH0x7Y.js +38 -0
- package/web/dist/assets/channel-DGkccMO1.js +1 -0
- package/web/dist/assets/chunk-2Q5K7J3B-Cuikho5Q.js +1 -0
- package/web/dist/assets/chunk-5VM5RSS4-3-KZ5AUz.js +15 -0
- package/web/dist/assets/chunk-F27PBJKO-Bkwq2gGU.js +1 -0
- package/web/dist/assets/chunk-G27WJ6UU-BLyQzdGE.js +231 -0
- package/web/dist/assets/chunk-JWPE2WC7-B7HxxQds.js +1 -0
- package/web/dist/assets/chunk-LCL6LL3I-CtXrnUwF.js +206 -0
- package/web/dist/assets/chunk-POPQ4Y6H-dxddZlXt.js +1 -0
- package/web/dist/assets/chunk-SVP7TREG-DpkmrgHn.js +88 -0
- package/web/dist/assets/chunk-XXDRQBXY-BwZ7RbpQ.js +1 -0
- package/web/dist/assets/classDiagram-DTDB5LWJ-BGscw3OD.js +1 -0
- package/web/dist/assets/classDiagram-v2-JRS7N3AN-BGscw3OD.js +1 -0
- package/web/dist/assets/cose-bilkent-JH36ORCC-DGFOsMUt.js +1 -0
- package/web/dist/assets/cynefin-OW5HDTMX-DUwn3ehS.js +178 -0
- package/web/dist/assets/cynefinDiagram-5FMLGOSQ-DlLAOZ9x.js +62 -0
- package/web/dist/assets/cytoscape.esm-BjAbmLdb.js +331 -0
- package/web/dist/assets/dagre-3AP2YEHR-CyourLfz.js +4 -0
- package/web/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/web/dist/assets/diagram-S7CK7UJ4-BTMbx2np.js +30 -0
- package/web/dist/assets/diagram-UQ7AKVKN-UK94_1-P.js +41 -0
- package/web/dist/assets/diagram-VSXAHHWV-C7zS-v9v.js +3 -0
- package/web/dist/assets/diagram-VX7I27RA-DKJRa6CK.js +24 -0
- package/web/dist/assets/diagram-Z3DM3KII-kUbAfGAp.js +24 -0
- package/web/dist/assets/ebnfDiagram-PWID7BFC-CVW16_vb.js +1 -0
- package/web/dist/assets/erDiagram-SSCWMZ5O-BRQMFQ5y.js +99 -0
- package/web/dist/assets/flowDiagram-A5DVABFB-S34LpLtK.js +168 -0
- package/web/dist/assets/ganttDiagram-EL5Y4UJY-BFiqk1-G.js +292 -0
- package/web/dist/assets/gitGraphDiagram-WWUBYQGX-LZsrdyyv.js +106 -0
- package/web/dist/assets/index-GAamh1p4.js +2152 -0
- package/web/dist/assets/infoDiagram-RXCK75RN-0DvN6KIp.js +2 -0
- package/web/dist/assets/init-Gi6I4Gst.js +1 -0
- package/web/dist/assets/ishikawaDiagram-5VMMS53U-CaY3A-Bx.js +70 -0
- package/web/dist/assets/journeyDiagram-EYS64GPL-BBeiaCrj.js +139 -0
- package/web/dist/assets/kanban-definition-3QL26DDD-ux42DRXQ.js +89 -0
- package/web/dist/assets/layout-CnXiF_NB.js +1 -0
- package/web/dist/assets/linear-BMW0u6G6.js +1 -0
- package/web/dist/assets/mindmap-definition-FBJOCRG2-hUxSpdKn.js +96 -0
- package/web/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/web/dist/assets/pegDiagram-XKGWAZYB-aCaQPH0w.js +1 -0
- package/web/dist/assets/{pending-edit-apply-C0I13CbD.js → pending-edit-apply-AcdOQ9Ij.js} +6 -6
- package/web/dist/assets/pieDiagram-E7YTZNPT-CmOEZ8zg.js +39 -0
- package/web/dist/assets/quadrantDiagram-AXDQQJYC-VW6fAhsV.js +7 -0
- package/web/dist/assets/railroadDiagram-O6MQD6OU-CKJ7Ba5K.js +1 -0
- package/web/dist/assets/requirementDiagram-EFPCY7ZU-pNGkq9Vw.js +84 -0
- package/web/dist/assets/{rotate-cw-B1UQ8Hld.js → rotate-cw-KAfChbGV.js} +1 -1
- package/web/dist/assets/sankeyDiagram-P5KCCOFB-BIMmDTfZ.js +40 -0
- package/web/dist/assets/sequenceDiagram-WJ2MYXX4-BBW7wzKY.js +162 -0
- package/web/dist/assets/sizeCapture-X5ZJPWSS-DdkXmLYR.js +1 -0
- package/web/dist/assets/{square-dashed-mouse-pointer-8lrgqmfv.js → square-dashed-mouse-pointer-CDUlhUP7.js} +1 -1
- package/web/dist/assets/stateDiagram-HBIQ2CUA-W313Uz16.js +1 -0
- package/web/dist/assets/stateDiagram-v2-4QOOHH4V-DDhJBZzf.js +1 -0
- package/web/dist/assets/swimlanes-XN3QIQJK-C29TL26K.js +1 -0
- package/web/dist/assets/swimlanesDiagram-VK2B7HYN-BfN-Xdmu.js +8 -0
- package/web/dist/assets/timeline-definition-24CTP7MA-CF0JMObX.js +120 -0
- package/web/dist/assets/vennDiagram-4TSXK5OY-DrDFsaH3.js +34 -0
- package/web/dist/assets/wardleyDiagram-VM6X3IG4-CVrnRIS_.js +78 -0
- package/web/dist/assets/xychartDiagram-S5SC5T6Z-BvkN1zsT.js +7 -0
- package/web/dist/index.html +1 -1
- package/web/dist/assets/SiteWindow-DM0LQPCO.js +0 -18
- 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
|
+

|
|
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
|
+

|
|
48
|
+
|
|
49
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
48
|
+
|
|
49
|
+

|
|
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
|
+

|
|
72
|
+
|
|
73
|
+
板书是真文件,保存在项目的 `notes/板书/` 目录下。草图节点、连线和板书都可以双击直接改,改完 Agent 接着用改过的版本。
|
|
74
|
+
|
|
75
|
+

|
|
76
|
+
|
|
61
77
|
### 你的操作会成为下一轮上下文
|
|
62
78
|
|
|
63
79
|
你可以直接修改文字、圈选区域、拖动元素,或者在不同产物之间建立「参考」「批注」「接着做」等关系。这些操作会被整理为后续任务的上下文。
|
|
@@ -113,7 +129,7 @@ Agent 会出现在它正在处理的内容旁边。你可以看到它正在编
|
|
|
113
129
|
|
|
114
130
|
## 项目状态
|
|
115
131
|
|
|
116
|
-
个人项目,2026 年 4
|
|
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
|
|
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 合计
|
|
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.
|
|
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 {
|
package/server/api/assets.js
CHANGED
|
@@ -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,
|
|
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
|
-
//
|
|
352
|
-
|
|
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
|
}
|
package/server/api/board.js
CHANGED
|
@@ -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) {
|