@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.
- package/README.md +156 -106
- package/bin/nodesign.js +0 -0
- package/package.json +1 -1
- package/server/api/README.md +45 -45
- package/server/api/_guard.js +75 -75
- package/server/api/admin.js +187 -187
- package/server/api/assets/docx-page.js +103 -103
- package/server/api/assets/helpers.js +22 -22
- package/server/api/assets/notes.js +144 -144
- package/server/api/assets.js +896 -896
- package/server/api/board.js +57 -57
- package/server/api/browse.js +138 -138
- package/server/api/canvas.js +444 -444
- package/server/api/chatai.js +324 -324
- package/server/api/exports/build-standalone.js +980 -980
- package/server/api/exports/cards.js +118 -118
- package/server/api/exports/docx-pdf.js +46 -46
- package/server/api/exports/export-page.js +142 -142
- package/server/api/exports/handoff.js +147 -147
- package/server/api/exports.js +690 -690
- package/server/api/instruction.js +75 -75
- package/server/api/me.js +116 -116
- package/server/api/memory.js +185 -185
- package/server/api/pending-changes.js +366 -366
- package/server/api/plugins.js +164 -164
- package/server/api/projects.js +220 -220
- package/server/api/publish.js +64 -64
- package/server/api/recent.js +73 -73
- package/server/api/sessions.js +589 -589
- package/server/api/skills.js +77 -77
- package/server/api/standalone-fit.js +168 -168
- package/server/api/turn-compose.js +180 -180
- package/server/api/turn-inflight.js +44 -44
- package/server/auth/README.md +17 -17
- package/server/auth/middleware.js +138 -138
- package/server/auth/origin-guard.js +71 -71
- package/server/auth/session.js +108 -108
- package/server/auth/tier.js +113 -113
- package/server/auth/users-store.js +348 -348
- package/server/edit/README.md +29 -29
- package/server/engine/README.md +113 -113
- package/server/engine/agent/agent-shared.js +550 -550
- package/server/engine/agent/auto-mode-default-hard-deny.txt +4 -4
- package/server/engine/agent/auto-mode-rules.js +78 -78
- package/server/engine/agent/context.js +418 -418
- package/server/engine/agent/events.js +464 -464
- package/server/engine/agent/hooks/canvas-validate.js +199 -199
- package/server/engine/agent/hooks/failure.js +120 -120
- package/server/engine/agent/hooks/file-events.js +75 -75
- package/server/engine/agent/hooks/lifecycle.js +233 -233
- package/server/engine/agent/hooks/post-canvas-focus.js +80 -80
- package/server/engine/agent/hooks/post-guidance.js +206 -206
- package/server/engine/agent/hooks/post-subagent-report.js +104 -104
- package/server/engine/agent/hooks/post-trim.js +58 -58
- package/server/engine/agent/hooks/pre-board-neighborhood.js +34 -34
- package/server/engine/agent/hooks/pre-defaults.js +84 -84
- package/server/engine/agent/hooks/pre-injectors.js +277 -277
- package/server/engine/agent/hooks/pre-performance-log-guard.js +78 -78
- package/server/engine/agent/hooks/pre-starter-files.js +61 -61
- package/server/engine/agent/hooks/pre-workspace-scope-guard.js +78 -78
- package/server/engine/agent/hooks/site-validate.js +94 -94
- package/server/engine/agent/hooks/tool-prompts.js +31 -31
- package/server/engine/agent/hooks/turn-state-memory.js +56 -56
- package/server/engine/agent/hooks/user-prompt-submit.js +215 -215
- package/server/engine/agent/hooks.js +354 -354
- package/server/engine/agent/init-contract.js +69 -69
- package/server/engine/agent/isolation.js +159 -159
- package/server/engine/agent/model-table.js +292 -292
- package/server/engine/agent/plugin-loader.js +266 -266
- package/server/engine/agent/prompts/nodesign-prelude.md +336 -336
- package/server/engine/agent/prompts/tools/ask-user-question-protocol.md +71 -71
- package/server/engine/agent/prompts/tools/direct-edit-protocol.md +98 -98
- package/server/engine/agent/prompts/tools/generate-image-cookbook.gemini-gateway.md +583 -583
- package/server/engine/agent/prompts/tools/generate-image-cookbook.md +483 -483
- package/server/engine/agent/prompts/tools/hybrid-reference.md +100 -100
- package/server/engine/agent/prompts/tools/paint-still-cookbook.md +270 -270
- package/server/engine/agent/prompts/tools/roll-film-cookbook.md +70 -70
- package/server/engine/agent/prompts/tools/site-reference.md +106 -106
- package/server/engine/agent/prompts/tools/tweaks-syntax.md +125 -125
- package/server/engine/agent/prompts/tools/vision-checker-dispatch.md +48 -48
- package/server/engine/agent/session-loop.js +1024 -1024
- package/server/engine/agent/session-model.js +164 -164
- package/server/engine/agent/skill.js +204 -204
- package/server/engine/agent/task-events.js +78 -78
- package/server/engine/agents/ds-extractor.md +132 -132
- package/server/engine/agents/explorer.md +190 -190
- package/server/engine/agents/index.js +230 -230
- package/server/engine/agents/schemas/design-system.json +156 -156
- package/server/engine/agents/schemas/tweak-schema.json +113 -113
- package/server/engine/agents/tweak-proposer.md +127 -127
- package/server/engine/agents/vision-checker.md +254 -254
- package/server/engine/browse/capture.js +422 -422
- package/server/engine/browse/card.js +137 -137
- package/server/engine/browse/handover.js +59 -59
- package/server/engine/browse/page-digest.js +119 -119
- package/server/engine/browse/refs.js +196 -196
- package/server/engine/browse/registry.js +307 -307
- package/server/engine/browse/screencast.js +170 -170
- package/server/engine/browse/state.js +121 -121
- package/server/engine/chatai/chat-log.js +79 -79
- package/server/engine/chatai/index.js +193 -193
- package/server/engine/chatai/openai-compat.js +152 -152
- package/server/engine/chatai/orchestrate.js +309 -309
- package/server/engine/chatai/perform.js +40 -40
- package/server/engine/chatai/summarize.js +79 -79
- package/server/engine/mcp/capability-gate.js +52 -52
- package/server/engine/mcp/index.js +339 -339
- package/server/engine/mcp/param-sanitizer.js +142 -142
- package/server/engine/mcp/tools/arrange-on-board.js +124 -124
- package/server/engine/mcp/tools/artifact-session.js +278 -278
- package/server/engine/mcp/tools/browse-computer.js +421 -421
- package/server/engine/mcp/tools/browse-find-batch.js +183 -183
- package/server/engine/mcp/tools/browse-screenshot.js +157 -157
- package/server/engine/mcp/tools/browse.js +480 -480
- package/server/engine/mcp/tools/build-docx.js +101 -101
- package/server/engine/mcp/tools/clear-pending-changes.js +99 -99
- package/server/engine/mcp/tools/create-on-board.js +155 -155
- package/server/engine/mcp/tools/crystallize-skill.js +168 -168
- package/server/engine/mcp/tools/explain-style.js +237 -237
- package/server/engine/mcp/tools/export-handoff.js +133 -133
- package/server/engine/mcp/tools/expose-tweaks.js +149 -149
- package/server/engine/mcp/tools/generate-image.js +842 -842
- package/server/engine/mcp/tools/get-computed-styles.js +164 -164
- package/server/engine/mcp/tools/get-pending-changes.js +205 -205
- package/server/engine/mcp/tools/h3box-ssh.js +84 -84
- package/server/engine/mcp/tools/helpers/acquire-page.js +82 -82
- package/server/engine/mcp/tools/helpers/motion-lab.js +554 -554
- package/server/engine/mcp/tools/helpers/motion-scroll.js +112 -112
- package/server/engine/mcp/tools/helpers/perception-page.js +215 -215
- package/server/engine/mcp/tools/helpers/reference-download.js +103 -103
- package/server/engine/mcp/tools/helpers/rembg-bridge.py +80 -80
- package/server/engine/mcp/tools/helpers/shot-pipeline.js +280 -280
- package/server/engine/mcp/tools/highlight.js +68 -68
- package/server/engine/mcp/tools/list-pages.js +222 -222
- package/server/engine/mcp/tools/lookup-tags.js +59 -59
- package/server/engine/mcp/tools/navigate-to-page.js +60 -60
- package/server/engine/mcp/tools/organize-board.js +64 -64
- package/server/engine/mcp/tools/paint-still.js +434 -434
- package/server/engine/mcp/tools/pin-to-board.js +130 -130
- package/server/engine/mcp/tools/preview-deck.js +130 -130
- package/server/engine/mcp/tools/profile-scroll.js +305 -305
- package/server/engine/mcp/tools/publish-site.js +102 -102
- package/server/engine/mcp/tools/query-elements.js +195 -195
- package/server/engine/mcp/tools/read-board.js +103 -103
- package/server/engine/mcp/tools/read-document.js +93 -93
- package/server/engine/mcp/tools/read-page.js +264 -264
- package/server/engine/mcp/tools/read-tavern-json.js +143 -143
- package/server/engine/mcp/tools/record-decision.js +140 -140
- package/server/engine/mcp/tools/relate-on-board.js +110 -110
- package/server/engine/mcp/tools/report-issue.js +115 -115
- package/server/engine/mcp/tools/roll-film.js +266 -266
- package/server/engine/mcp/tools/screenshot-docx.js +138 -138
- package/server/engine/mcp/tools/screenshot-url.js +195 -195
- package/server/engine/mcp/tools/screenshot.js +583 -583
- package/server/engine/mcp/tools/tier-gate.js +96 -96
- package/server/engine/mcp/tools/trace-motion.js +215 -215
- package/server/engine/mcp/tools/web-search.js +548 -548
- package/server/engine/motion/inventory.js +349 -349
- package/server/engine/perception/session.js +235 -235
- package/server/engine/plugins/nodesign/.claude-plugin/plugin.json +5 -5
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/SKILL.md +192 -192
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/canvas.template.html +667 -667
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/hybrid-grid.md +22 -22
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/image-led-cover.md +25 -25
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/portrait.md +23 -23
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/quote-backdrop.md +21 -21
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/section-divider.md +23 -23
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/text-led.md +21 -21
- package/server/engine/plugins/nodesign/skills/docx-craft/SKILL.md +162 -162
- package/server/engine/plugins/nodesign/skills/docx-craft/references/token-schema.md +407 -407
- package/server/engine/plugins/nodesign/skills/docx-craft//346/226/207/346/241/243.template.json +80 -80
- package/server/engine/plugins/nodesign/skills/rp-craft/SKILL.md +228 -228
- 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
- package/server/engine/plugins/nodesign/skills/rp-craft//346/274/224/345/207/272.template.js +268 -268
- package/server/engine/plugins/nodesign/skills/site-craft/SKILL.md +323 -323
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/build-lane.md +134 -134
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/cutout-collage.md +61 -61
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/mock-app.md +163 -163
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/page-transitions.md +86 -86
- package/server/engine/runs/active-runs.js +698 -698
- package/server/engine/runs/live-turn.js +252 -252
- package/server/engine/runs/store.js +364 -364
- package/server/engine/runs/turn-relay.js +217 -217
- package/server/engine/runtime/workspace.js +164 -164
- package/server/lib/artifact-file-path.js +80 -80
- package/server/lib/artifact-target.js +284 -284
- package/server/lib/asset-refs.js +239 -239
- package/server/lib/async-queue.js +104 -104
- package/server/lib/auto-relations.js +130 -130
- package/server/lib/binding-types.js +60 -60
- package/server/lib/board-kind-sizes.js +62 -62
- package/server/lib/board-relations.js +133 -133
- package/server/lib/browse-proxy.js +180 -180
- package/server/lib/canvas-id.js +36 -36
- package/server/lib/cover.js +227 -227
- package/server/lib/danbooru-tags.js +291 -291
- package/server/lib/doc-extract.js +156 -156
- package/server/lib/docx/build-from-source.js +260 -260
- package/server/lib/docx/build.js +490 -490
- package/server/lib/docx/dump-styles.js +287 -287
- package/server/lib/docx/fonts/nodesign-cjk.conf +164 -164
- package/server/lib/docx/merge-runs.js +129 -129
- package/server/lib/docx/numbering.js +218 -218
- package/server/lib/docx/order.js +152 -152
- package/server/lib/docx/rawzip.js +172 -172
- package/server/lib/docx/render.js +92 -92
- package/server/lib/docx/text-lint.js +225 -225
- package/server/lib/docx/tokens.js +335 -335
- package/server/lib/docx/units.js +29 -29
- package/server/lib/docx/xml.js +291 -291
- package/server/lib/docx-pages.js +187 -187
- package/server/lib/export-collect.js +289 -289
- package/server/lib/export-package.js +210 -210
- package/server/lib/html-srcset.js +145 -145
- package/server/lib/image-variant.js +460 -460
- package/server/lib/ingress/forward-openai-chat.js +305 -305
- package/server/lib/ingress/openai-chat.js +491 -491
- package/server/lib/ingress/session-notice.js +66 -66
- package/server/lib/ingress/session-routes.js +68 -68
- package/server/lib/ingress/slot-probe.js +130 -130
- package/server/lib/ingress/upstream-billing.js +52 -52
- package/server/lib/ingress/upstream-fail-streak.js +72 -72
- package/server/lib/ingress/upstream-truncation.js +48 -48
- package/server/lib/issues-store.js +182 -182
- package/server/lib/kinds/deck.js +72 -72
- package/server/lib/kinds/docx.js +283 -283
- package/server/lib/kinds/file-kinds.js +113 -113
- package/server/lib/kinds/index.js +202 -202
- package/server/lib/kinds/site.js +161 -161
- package/server/lib/model-ingress.js +543 -543
- package/server/lib/moderation.js +255 -255
- package/server/lib/notice-store.js +103 -103
- package/server/lib/plugin-install.js +104 -104
- package/server/lib/plugin-validator.js +721 -721
- package/server/lib/publish-store.js +115 -115
- package/server/lib/quick-summary.js +52 -52
- package/server/lib/quota.js +270 -270
- package/server/lib/rate-window.js +29 -29
- package/server/lib/reference-assets.js +92 -92
- package/server/lib/region-shot.js +135 -135
- package/server/lib/safe-path.js +73 -73
- package/server/lib/sdk-session.js +33 -33
- package/server/lib/showcase-store.js +92 -92
- package/server/lib/site-publish.js +465 -465
- package/server/lib/ssrf-guard.js +432 -432
- package/server/lib/task-scan.js +158 -158
- package/server/lib/tavern-json.js +154 -154
- package/server/lib/video-variant.js +237 -237
- package/server/lib/workspace-path.js +33 -33
- package/server/ops/fix-sdk-musl.mjs +40 -40
- package/server/ops/install-macos-fonts.sh +114 -114
- package/server/ops/macos-fonts.conf +336 -336
- package/server/ops/sandbox-shim/bwrap +57 -57
- package/server/projects/assets-summary.js +119 -119
- package/server/projects/auto-name.js +53 -53
- package/server/projects/board-store.js +640 -640
- package/server/projects/move-entry.js +103 -103
- package/server/projects/store.js +270 -270
- package/server/projects/ui-config.js +54 -54
- package/server/projects/workspace-templates.js +53 -53
- package/server/projects/workspace.js +1025 -1025
- package/server/runtime/local-config.js +177 -177
- package/server/runtime/profile.js +72 -72
- package/server/services/rembg-service.py +334 -334
- package/server/shared/README.md +25 -25
- package/server/shared/deck.js +54 -54
- package/server/shared/time.js +49 -49
- package/server/style-pipeline/README.md +53 -53
- package/server/ws/broker.js +36 -36
- package/server/ws/browse-channel.js +177 -177
- package/server/ws/index.js +386 -386
- package/web/README.md +89 -89
- package/web/dist/assets/BrowserWindow-BYCgizHw.js +11 -0
- package/web/dist/assets/{DeckWindow-4G0xX7P3.js → DeckWindow-pNydEM4L.js} +1 -1
- package/web/dist/assets/{DocxWindow-BXQsoPkh.js → DocxWindow-PPgzh3Gq.js} +1 -1
- package/web/dist/assets/{SiteWindow-D6eS-2gM.js → SiteWindow-DM0LQPCO.js} +1 -1
- package/web/dist/assets/{index-Cbl3ATvp.js → index-BHTBJlpY.js} +32 -32
- package/web/dist/assets/{pending-edit-apply-DTjo2geW.js → pending-edit-apply-C0I13CbD.js} +1 -1
- package/web/dist/assets/{rotate-cw-LMT3x1Zj.js → rotate-cw-B1UQ8Hld.js} +1 -1
- package/web/dist/assets/{square-dashed-mouse-pointer-CBoAkfhi.js → square-dashed-mouse-pointer-8lrgqmfv.js} +1 -1
- package/web/dist/index.html +17 -17
- 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
|
+
}
|