@xiaobuyu/nodesign 0.0.1
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/LICENSE +661 -0
- package/README.md +140 -0
- package/bin/nodesign.js +176 -0
- package/package.json +104 -0
- package/server/api/README.md +45 -0
- package/server/api/_guard.js +75 -0
- package/server/api/admin.js +187 -0
- package/server/api/assets/docx-page.js +103 -0
- package/server/api/assets/helpers.js +22 -0
- package/server/api/assets/notes.js +144 -0
- package/server/api/assets.js +896 -0
- package/server/api/board.js +57 -0
- package/server/api/browse.js +138 -0
- package/server/api/canvas.js +444 -0
- package/server/api/chatai.js +324 -0
- package/server/api/exports/build-standalone.js +980 -0
- package/server/api/exports/cards.js +118 -0
- package/server/api/exports/docx-pdf.js +46 -0
- package/server/api/exports/export-page.js +142 -0
- package/server/api/exports/handoff.js +147 -0
- package/server/api/exports.js +690 -0
- package/server/api/instruction.js +75 -0
- package/server/api/local.js +115 -0
- package/server/api/me.js +116 -0
- package/server/api/memory.js +185 -0
- package/server/api/pending-changes.js +366 -0
- package/server/api/plugins.js +164 -0
- package/server/api/projects.js +220 -0
- package/server/api/publish.js +64 -0
- package/server/api/recent.js +73 -0
- package/server/api/sessions.js +589 -0
- package/server/api/skills.js +77 -0
- package/server/api/standalone-fit.js +168 -0
- package/server/api/turn-compose.js +180 -0
- package/server/api/turn-inflight.js +44 -0
- package/server/api/turn.js +669 -0
- package/server/auth/README.md +17 -0
- package/server/auth/middleware.js +138 -0
- package/server/auth/origin-guard.js +71 -0
- package/server/auth/session.js +108 -0
- package/server/auth/tier.js +113 -0
- package/server/auth/users-store.js +348 -0
- package/server/edit/README.md +29 -0
- package/server/engine/README.md +113 -0
- package/server/engine/agent/agent-shared.js +550 -0
- package/server/engine/agent/auto-mode-default-hard-deny.txt +4 -0
- package/server/engine/agent/auto-mode-rules.js +78 -0
- package/server/engine/agent/context.js +418 -0
- package/server/engine/agent/events.js +464 -0
- package/server/engine/agent/hooks/canvas-validate.js +199 -0
- package/server/engine/agent/hooks/failure.js +120 -0
- package/server/engine/agent/hooks/file-events.js +75 -0
- package/server/engine/agent/hooks/lifecycle.js +233 -0
- package/server/engine/agent/hooks/post-canvas-focus.js +80 -0
- package/server/engine/agent/hooks/post-guidance.js +206 -0
- package/server/engine/agent/hooks/post-subagent-report.js +104 -0
- package/server/engine/agent/hooks/post-trim.js +58 -0
- package/server/engine/agent/hooks/pre-board-neighborhood.js +34 -0
- package/server/engine/agent/hooks/pre-defaults.js +84 -0
- package/server/engine/agent/hooks/pre-injectors.js +277 -0
- package/server/engine/agent/hooks/pre-performance-log-guard.js +78 -0
- package/server/engine/agent/hooks/pre-starter-files.js +61 -0
- package/server/engine/agent/hooks/pre-workspace-scope-guard.js +78 -0
- package/server/engine/agent/hooks/site-validate.js +94 -0
- package/server/engine/agent/hooks/tool-prompts.js +31 -0
- package/server/engine/agent/hooks/turn-state-memory.js +56 -0
- package/server/engine/agent/hooks/user-prompt-submit.js +215 -0
- package/server/engine/agent/hooks.js +354 -0
- package/server/engine/agent/init-contract.js +69 -0
- package/server/engine/agent/isolation.js +159 -0
- package/server/engine/agent/model-context.js +359 -0
- package/server/engine/agent/model-table.js +292 -0
- package/server/engine/agent/plugin-loader.js +266 -0
- package/server/engine/agent/prompts/nodesign-prelude.md +336 -0
- package/server/engine/agent/prompts/tools/ask-user-question-protocol.md +71 -0
- package/server/engine/agent/prompts/tools/direct-edit-protocol.md +98 -0
- package/server/engine/agent/prompts/tools/generate-image-cookbook.gemini-gateway.md +583 -0
- package/server/engine/agent/prompts/tools/generate-image-cookbook.md +483 -0
- package/server/engine/agent/prompts/tools/hybrid-reference.md +100 -0
- package/server/engine/agent/prompts/tools/paint-still-cookbook.md +270 -0
- package/server/engine/agent/prompts/tools/roll-film-cookbook.md +70 -0
- package/server/engine/agent/prompts/tools/site-reference.md +106 -0
- package/server/engine/agent/prompts/tools/tweaks-syntax.md +125 -0
- package/server/engine/agent/prompts/tools/vision-checker-dispatch.md +48 -0
- package/server/engine/agent/session-loop.js +1024 -0
- package/server/engine/agent/session-model.js +164 -0
- package/server/engine/agent/skill.js +204 -0
- package/server/engine/agent/system-prompts.js +86 -0
- package/server/engine/agent/task-events.js +78 -0
- package/server/engine/agents/ds-extractor.md +132 -0
- package/server/engine/agents/explorer.md +190 -0
- package/server/engine/agents/index.js +230 -0
- package/server/engine/agents/schemas/design-system.json +156 -0
- package/server/engine/agents/schemas/tweak-schema.json +113 -0
- package/server/engine/agents/tweak-proposer.md +127 -0
- package/server/engine/agents/vision-checker.md +254 -0
- package/server/engine/browse/capture.js +422 -0
- package/server/engine/browse/card.js +137 -0
- package/server/engine/browse/handover.js +59 -0
- package/server/engine/browse/page-digest.js +119 -0
- package/server/engine/browse/refs.js +196 -0
- package/server/engine/browse/registry.js +307 -0
- package/server/engine/browse/screencast.js +170 -0
- package/server/engine/browse/state.js +121 -0
- package/server/engine/chatai/chat-log.js +79 -0
- package/server/engine/chatai/index.js +193 -0
- package/server/engine/chatai/openai-compat.js +152 -0
- package/server/engine/chatai/orchestrate.js +309 -0
- package/server/engine/chatai/perform.js +40 -0
- package/server/engine/chatai/summarize.js +79 -0
- package/server/engine/mcp/capability-gate.js +52 -0
- package/server/engine/mcp/index.js +339 -0
- package/server/engine/mcp/param-sanitizer.js +142 -0
- package/server/engine/mcp/tools/arrange-on-board.js +124 -0
- package/server/engine/mcp/tools/artifact-session.js +278 -0
- package/server/engine/mcp/tools/browse-computer.js +421 -0
- package/server/engine/mcp/tools/browse-find-batch.js +183 -0
- package/server/engine/mcp/tools/browse-screenshot.js +157 -0
- package/server/engine/mcp/tools/browse.js +480 -0
- package/server/engine/mcp/tools/build-docx.js +101 -0
- package/server/engine/mcp/tools/clear-pending-changes.js +99 -0
- package/server/engine/mcp/tools/create-on-board.js +155 -0
- package/server/engine/mcp/tools/crystallize-skill.js +168 -0
- package/server/engine/mcp/tools/deliver-files.js +0 -0
- package/server/engine/mcp/tools/explain-style.js +237 -0
- package/server/engine/mcp/tools/export-handoff.js +133 -0
- package/server/engine/mcp/tools/expose-tweaks.js +149 -0
- package/server/engine/mcp/tools/generate-image.js +842 -0
- package/server/engine/mcp/tools/get-computed-styles.js +164 -0
- package/server/engine/mcp/tools/get-pending-changes.js +205 -0
- package/server/engine/mcp/tools/h3box-ssh.js +84 -0
- package/server/engine/mcp/tools/helpers/acquire-page.js +82 -0
- package/server/engine/mcp/tools/helpers/motion-lab.js +554 -0
- package/server/engine/mcp/tools/helpers/motion-scroll.js +112 -0
- package/server/engine/mcp/tools/helpers/perception-page.js +215 -0
- package/server/engine/mcp/tools/helpers/reference-download.js +103 -0
- package/server/engine/mcp/tools/helpers/rembg-bridge.py +80 -0
- package/server/engine/mcp/tools/helpers/rembg.js +273 -0
- package/server/engine/mcp/tools/helpers/shot-pipeline.js +280 -0
- package/server/engine/mcp/tools/highlight.js +68 -0
- package/server/engine/mcp/tools/list-pages.js +222 -0
- package/server/engine/mcp/tools/lookup-tags.js +59 -0
- package/server/engine/mcp/tools/navigate-to-page.js +60 -0
- package/server/engine/mcp/tools/organize-board.js +64 -0
- package/server/engine/mcp/tools/paint-still.js +434 -0
- package/server/engine/mcp/tools/pin-to-board.js +130 -0
- package/server/engine/mcp/tools/preview-deck.js +130 -0
- package/server/engine/mcp/tools/profile-scroll.js +305 -0
- package/server/engine/mcp/tools/publish-site.js +102 -0
- package/server/engine/mcp/tools/query-elements.js +195 -0
- package/server/engine/mcp/tools/read-board.js +103 -0
- package/server/engine/mcp/tools/read-document.js +93 -0
- package/server/engine/mcp/tools/read-page.js +264 -0
- package/server/engine/mcp/tools/read-tavern-json.js +143 -0
- package/server/engine/mcp/tools/record-decision.js +140 -0
- package/server/engine/mcp/tools/relate-on-board.js +110 -0
- package/server/engine/mcp/tools/remove-background.js +391 -0
- package/server/engine/mcp/tools/report-issue.js +115 -0
- package/server/engine/mcp/tools/roll-film.js +266 -0
- package/server/engine/mcp/tools/screenshot-docx.js +138 -0
- package/server/engine/mcp/tools/screenshot-url.js +195 -0
- package/server/engine/mcp/tools/screenshot.js +583 -0
- package/server/engine/mcp/tools/tier-gate.js +96 -0
- package/server/engine/mcp/tools/trace-motion.js +215 -0
- package/server/engine/mcp/tools/web-search.js +548 -0
- package/server/engine/motion/inventory.js +349 -0
- package/server/engine/perception/session.js +235 -0
- package/server/engine/plugins/nodesign/.claude-plugin/plugin.json +5 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/SKILL.md +192 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/canvas.template.html +667 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/hybrid-grid.md +22 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/image-led-cover.md +25 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/portrait.md +23 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/quote-backdrop.md +21 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/section-divider.md +23 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/text-led.md +21 -0
- package/server/engine/plugins/nodesign/skills/docx-craft/SKILL.md +162 -0
- package/server/engine/plugins/nodesign/skills/docx-craft/references/token-schema.md +407 -0
- package/server/engine/plugins/nodesign/skills/docx-craft//346/226/207/346/241/243.template.json +80 -0
- package/server/engine/plugins/nodesign/skills/rp-craft/SKILL.md +228 -0
- 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 -0
- package/server/engine/plugins/nodesign/skills/rp-craft//346/274/224/345/207/272.template.js +268 -0
- package/server/engine/plugins/nodesign/skills/site-craft/SKILL.md +323 -0
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/build-lane.md +134 -0
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/cutout-collage.md +61 -0
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/mock-app.md +163 -0
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/page-transitions.md +86 -0
- package/server/engine/runs/active-runs.js +698 -0
- package/server/engine/runs/live-turn.js +252 -0
- package/server/engine/runs/store.js +364 -0
- package/server/engine/runs/turn-relay.js +217 -0
- package/server/engine/runtime/workspace.js +164 -0
- package/server/index.js +235 -0
- package/server/lib/artifact-file-path.js +80 -0
- package/server/lib/artifact-target.js +284 -0
- package/server/lib/asset-refs.js +239 -0
- package/server/lib/async-queue.js +104 -0
- package/server/lib/auto-relations.js +130 -0
- package/server/lib/binding-types.js +60 -0
- package/server/lib/board-kind-sizes.js +62 -0
- package/server/lib/board-relations.js +133 -0
- package/server/lib/browse-proxy.js +180 -0
- package/server/lib/canvas-id.js +36 -0
- package/server/lib/cover.js +227 -0
- package/server/lib/danbooru-tags.js +291 -0
- package/server/lib/doc-extract.js +156 -0
- package/server/lib/docx/build-from-source.js +260 -0
- package/server/lib/docx/build.js +490 -0
- package/server/lib/docx/dump-styles.js +287 -0
- package/server/lib/docx/fonts/nodesign-cjk.conf +164 -0
- package/server/lib/docx/merge-runs.js +129 -0
- package/server/lib/docx/numbering.js +218 -0
- package/server/lib/docx/order.js +152 -0
- package/server/lib/docx/rawzip.js +172 -0
- package/server/lib/docx/render.js +92 -0
- package/server/lib/docx/text-lint.js +225 -0
- package/server/lib/docx/tokens.js +335 -0
- package/server/lib/docx/units.js +29 -0
- package/server/lib/docx/xml.js +291 -0
- package/server/lib/docx-pages.js +187 -0
- package/server/lib/export-collect.js +289 -0
- package/server/lib/export-package.js +210 -0
- package/server/lib/html-srcset.js +145 -0
- package/server/lib/image-variant.js +460 -0
- package/server/lib/ingress/forward-openai-chat.js +305 -0
- package/server/lib/ingress/openai-chat.js +491 -0
- package/server/lib/ingress/session-notice.js +66 -0
- package/server/lib/ingress/session-routes.js +68 -0
- package/server/lib/ingress/slot-probe.js +130 -0
- package/server/lib/ingress/upstream-billing.js +52 -0
- package/server/lib/ingress/upstream-fail-streak.js +72 -0
- package/server/lib/ingress/upstream-truncation.js +48 -0
- package/server/lib/issues-store.js +182 -0
- package/server/lib/kinds/deck.js +72 -0
- package/server/lib/kinds/docx.js +283 -0
- package/server/lib/kinds/file-kinds.js +113 -0
- package/server/lib/kinds/index.js +202 -0
- package/server/lib/kinds/site.js +161 -0
- package/server/lib/model-ingress.js +543 -0
- package/server/lib/moderation.js +255 -0
- package/server/lib/notice-store.js +103 -0
- package/server/lib/plugin-install.js +104 -0
- package/server/lib/plugin-validator.js +721 -0
- package/server/lib/publish-store.js +115 -0
- package/server/lib/quick-summary.js +52 -0
- package/server/lib/quota.js +270 -0
- package/server/lib/rate-window.js +29 -0
- package/server/lib/reference-assets.js +92 -0
- package/server/lib/region-shot.js +135 -0
- package/server/lib/safe-path.js +73 -0
- package/server/lib/sdk-session.js +33 -0
- package/server/lib/showcase-store.js +92 -0
- package/server/lib/site-publish.js +465 -0
- package/server/lib/ssrf-guard.js +432 -0
- package/server/lib/task-scan.js +158 -0
- package/server/lib/tavern-json.js +154 -0
- package/server/lib/video-variant.js +237 -0
- package/server/lib/workspace-path.js +33 -0
- package/server/ops/fix-sdk-musl.mjs +40 -0
- package/server/ops/install-macos-fonts.sh +114 -0
- package/server/ops/macos-fonts.conf +336 -0
- package/server/ops/sandbox-shim/bwrap +57 -0
- package/server/projects/assets-summary.js +119 -0
- package/server/projects/auto-name.js +53 -0
- package/server/projects/board-store.js +640 -0
- package/server/projects/move-entry.js +103 -0
- package/server/projects/store.js +270 -0
- package/server/projects/ui-config.js +54 -0
- package/server/projects/workspace-templates.js +53 -0
- package/server/projects/workspace.js +1025 -0
- package/server/runtime/capabilities.js +153 -0
- package/server/runtime/local-config.js +177 -0
- package/server/runtime/local-env.js +88 -0
- package/server/runtime/platform.js +342 -0
- package/server/runtime/profile.js +72 -0
- package/server/services/rembg-launcher.js +313 -0
- package/server/services/rembg-service.py +334 -0
- package/server/shared/README.md +25 -0
- package/server/shared/deck.js +54 -0
- package/server/shared/time.js +49 -0
- package/server/style-pipeline/README.md +53 -0
- package/server/ws/broker.js +36 -0
- package/server/ws/browse-channel.js +177 -0
- package/server/ws/index.js +386 -0
- package/web/README.md +89 -0
- package/web/dist/assets/BrowserWindow-Dc7J7CYc.js +11 -0
- package/web/dist/assets/DeckWindow-4G0xX7P3.js +16 -0
- package/web/dist/assets/DocxWindow-BXQsoPkh.js +16 -0
- package/web/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/web/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/web/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/web/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/web/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/web/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/web/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/web/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/web/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/web/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/web/dist/assets/SiteWindow-D6eS-2gM.js +18 -0
- package/web/dist/assets/again-D8tfgKKO.webp +0 -0
- package/web/dist/assets/caveat-nd-CP6HlsNg.woff2 +0 -0
- package/web/dist/assets/clock-xJhvfYpS.webp +0 -0
- package/web/dist/assets/film-cut-BBKMwH7q.webp +0 -0
- package/web/dist/assets/film-still-BZ_DL9Tt.webp +0 -0
- package/web/dist/assets/index-C0KUf6Wd.css +1 -0
- package/web/dist/assets/index-Cbl3ATvp.js +2125 -0
- package/web/dist/assets/ink-desk-BRbxeDsR.webp +0 -0
- package/web/dist/assets/ink-night-Bz4BT3XH.webp +0 -0
- package/web/dist/assets/ink-portrait-C0DM4V8Q.webp +0 -0
- package/web/dist/assets/longcang-regular-C0uvTuKa.woff2 +0 -0
- package/web/dist/assets/lxgw-nd-bold-DbRfiTgo.woff2 +0 -0
- package/web/dist/assets/lxgw-nd-regular-CpMw9Ehg.woff2 +0 -0
- package/web/dist/assets/pending-edit-apply-DTjo2geW.js +31 -0
- package/web/dist/assets/question-8H4HSqxh.webp +0 -0
- package/web/dist/assets/reject-BaPJqElj.webp +0 -0
- package/web/dist/assets/rotate-cw-LMT3x1Zj.js +11 -0
- package/web/dist/assets/rp-portrait-CbrGpew2.webp +0 -0
- package/web/dist/assets/rp-street-C4jVKjxx.webp +0 -0
- package/web/dist/assets/square-dashed-mouse-pointer-CBoAkfhi.js +11 -0
- package/web/dist/assets/tangle-DIxHVNuT.webp +0 -0
- package/web/dist/assets/thisone-BylZKglV.webp +0 -0
- package/web/dist/assets/thumb-DO1wfK1I.webp +0 -0
- package/web/dist/index.html +18 -0
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EventBus — agent 事件总线(标准化事件 schema)
|
|
3
|
+
*
|
|
4
|
+
* 设计:
|
|
5
|
+
* - 极简 EventEmitter(基于 Set<listener>),不依赖 node:events 避免 max listener warn
|
|
6
|
+
* - 每条事件至少含 { type, runId, ts }
|
|
7
|
+
* - 监听器订阅时可选择全订('*')或按前缀('run.delta')或按 type 精确匹配
|
|
8
|
+
*
|
|
9
|
+
* 标准事件 schema(type):
|
|
10
|
+
* ── 主流程 ──
|
|
11
|
+
* run.start 开始
|
|
12
|
+
* run.round.start { round }
|
|
13
|
+
* run.round.end { round, stopReason, usage? }
|
|
14
|
+
* run.delta.text { round, text }
|
|
15
|
+
* run.tool_use_summary { summary, blockIds } SDK helper 对一批工具调用的一句话总结
|
|
16
|
+
* run.delta.thinking { round, text }
|
|
17
|
+
* run.delta.tool_use { round, blockId, name, input }
|
|
18
|
+
* run.delta.tool_input { round, blockId, name, filePath?, append?, done? } Edit/Write 入参真流式(节流后的字段尾巴增量,工作台舞台层直播代码用)
|
|
19
|
+
* run.delta.tool_result { round, blockId, name, ok, output?, error? }
|
|
20
|
+
* run.todo.updated { todos }
|
|
21
|
+
* run.cancelled { reason }
|
|
22
|
+
* run.done { finalText, artifactPath?, snapshot }
|
|
23
|
+
* run.error { message, code?, stack? }
|
|
24
|
+
*
|
|
25
|
+
* ── P0+ stage 1 新增(SDK 28+ 种 message 类型映射)──
|
|
26
|
+
* run.tool_progress { blockId, toolName, elapsedSeconds } 工具执行中(>1s 才发)
|
|
27
|
+
* run.prompt_suggestion { suggestion } 每轮后预测下条 prompt
|
|
28
|
+
* run.task.started { taskId, description, subagentType, taskType } subagent 启动(只发真子代理,bash/workflow 任务被收口)
|
|
29
|
+
* run.task.progress { taskId, description, summary?, lastToolName? } subagent 30s 摘要(agentProgressSummaries)
|
|
30
|
+
* run.task.updated { taskId, patch } subagent 状态 patch
|
|
31
|
+
* run.task.notification { taskId, status, summary } subagent 完成/失败/停止
|
|
32
|
+
* run.files_persisted { files: [{ filename, file_id }], failed } agent 写完文件
|
|
33
|
+
* run.memory_recall { mode, memories } 自动 memory 召回
|
|
34
|
+
* run.notification { key, text, priority } 系统通知(弹 toast)
|
|
35
|
+
* run.session_state { state: 'idle' | 'running' | 'requires_action' }
|
|
36
|
+
* run.system_init { agents, tools, mcp_servers, model, ... } init 元信息
|
|
37
|
+
* run.hook.started { hookName, hookEvent } hook 生命周期(仅 includeHookEvents)
|
|
38
|
+
* run.hook.response { hookName, hookEvent, outcome }
|
|
39
|
+
* run.compact_boundary { compactMetadata } compact 边界
|
|
40
|
+
* run.status { status } compacting / requesting / null
|
|
41
|
+
* run.rate_limit { info } rate limit 变化
|
|
42
|
+
* run.image_generated { path, sizeBytes, prompt, assetRole, ... } generate_image 完成(Phase A)
|
|
43
|
+
* board.updated { objectId, zoneId, summary } pin_to_board 改画布布局(sessionId:null 广播,前端整份重拉 board.json)
|
|
44
|
+
* project.active_session { activeSessionId } 项目级会话指针变更(故意不带 sessionId —— 见构造器注释)
|
|
45
|
+
*
|
|
46
|
+
* 外层把 EventBus 桥接到:
|
|
47
|
+
* - WebSocket:subscribe('*') → ws.send(JSON.stringify(evt))
|
|
48
|
+
* - 文件审计:subscribe('*') → append events.jsonl
|
|
49
|
+
* - 内存测试:buffer.push(evt) 然后断言
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
import { resolveModelContextWindow, brandOfModel } from './model-context.js';
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Replay buffer 容量:单 project bus 保留最近 N 条事件供 WS 重连回放。
|
|
56
|
+
* 一次 turn 可能 emit 上千条 delta(thinking / text / tool);2000 够覆盖典型
|
|
57
|
+
* 几秒到几十秒的网络抖动重连窗口。FAR 旧的事件会被 client 通过 Sessions hydrate
|
|
58
|
+
* 重新拉,不依赖 buffer。
|
|
59
|
+
*/
|
|
60
|
+
const REPLAY_BUFFER_SIZE = 2000;
|
|
61
|
+
|
|
62
|
+
export class EventBus {
|
|
63
|
+
constructor() {
|
|
64
|
+
/** @type {Set<{ pattern: string, fn: (evt: object) => void }>} */
|
|
65
|
+
this._listeners = new Set();
|
|
66
|
+
/** 单调递增 seq,每个 publish 自增一;event.seq 已存在则不覆盖 */
|
|
67
|
+
this._seq = 0;
|
|
68
|
+
/** Ring buffer:保留最近 REPLAY_BUFFER_SIZE 条事件;FIFO 满了就 shift 最旧 */
|
|
69
|
+
this._buffer = [];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 订阅事件。
|
|
74
|
+
* @param {string} pattern - '*' 全订 / 'run.delta' 前缀 / 'run.done' 精确
|
|
75
|
+
* @param {(evt: object) => void} fn
|
|
76
|
+
* @returns {() => void} 取消订阅
|
|
77
|
+
*/
|
|
78
|
+
subscribe(pattern, fn) {
|
|
79
|
+
if (typeof pattern !== 'string') throw new Error('EventBus.subscribe: pattern must be string');
|
|
80
|
+
if (typeof fn !== 'function') throw new Error('EventBus.subscribe: fn must be function');
|
|
81
|
+
const entry = { pattern, fn };
|
|
82
|
+
this._listeners.add(entry);
|
|
83
|
+
return () => this._listeners.delete(entry);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 发布事件。listener 抛错会被吞 + 控制台 warn,不影响其他订阅者。
|
|
88
|
+
* 副作用:分配 event.seq(若调用方未显式带 seq)+ 进入 ring buffer。
|
|
89
|
+
*/
|
|
90
|
+
publish(event) {
|
|
91
|
+
if (event.seq == null) event.seq = ++this._seq;
|
|
92
|
+
this._buffer.push(event);
|
|
93
|
+
if (this._buffer.length > REPLAY_BUFFER_SIZE) this._buffer.shift();
|
|
94
|
+
|
|
95
|
+
for (const { pattern, fn } of this._listeners) {
|
|
96
|
+
if (matches(pattern, event.type)) {
|
|
97
|
+
try {
|
|
98
|
+
fn(event);
|
|
99
|
+
} catch (err) {
|
|
100
|
+
console.warn(`[EventBus] listener for "${pattern}" threw:`, err.message);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Replay-then-live 订阅 — WS 重连用。
|
|
108
|
+
*
|
|
109
|
+
* 流程(单线程 Node 保证原子,subscribe 与 buffer 快照间无 publish 触发):
|
|
110
|
+
* 1. subscribe('*') —— listener 把进来的事件先 push 到 queue(live=false)
|
|
111
|
+
* 2. 同步遍历 buffer,filter seq > since,对每条调 fn(dedupe 用 set)
|
|
112
|
+
* 3. drain queue —— 跳过 seq 已 replay 过的(buffer 和 queue 边界重叠)
|
|
113
|
+
* 4. 切 live,listener 直接 fn(evt)
|
|
114
|
+
*
|
|
115
|
+
* gap 判断:
|
|
116
|
+
* - 客户端 since=0 → 首次连,无 replay 需求 → gap=false
|
|
117
|
+
* - since > _seq → server 重启过,client 看过的 seq 现在不存在 → gap=true
|
|
118
|
+
* - buffer 最旧 seq > since+1 → 中间一段被 ring 挤掉了 → gap=true
|
|
119
|
+
* - 否则 buffer 完整覆盖 (since, _seq] → gap=false
|
|
120
|
+
*
|
|
121
|
+
* @param {number} since - 客户端最后看到的 seq(0 = 没看过任何事件)
|
|
122
|
+
* @param {(evt: object) => void} fn
|
|
123
|
+
* @returns {{ unsubscribe: () => void, replayed: number, gap: boolean }}
|
|
124
|
+
*/
|
|
125
|
+
subscribeFromSeq(since, fn) {
|
|
126
|
+
let live = false;
|
|
127
|
+
const queue = [];
|
|
128
|
+
const off = this.subscribe('*', (evt) => {
|
|
129
|
+
if (!live) queue.push(evt);
|
|
130
|
+
else fn(evt);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const needsReplay = since > 0 && since < this._seq;
|
|
134
|
+
const buffered = this._buffer.length > 0;
|
|
135
|
+
const restarted = since > this._seq;
|
|
136
|
+
const gap = restarted || (needsReplay && (!buffered || this._buffer[0].seq > since + 1));
|
|
137
|
+
|
|
138
|
+
// since=0 也 replay buffer 里 seq>0 的事件 —— ws/index.js 调用前先 await sendHydrate
|
|
139
|
+
// 是异步阻塞(拉 jsonl 100~数百 ms),期间若 agent emit 事件进 buffer 但 listener
|
|
140
|
+
// 还没 attach,老逻辑 `since > 0 ? ... : []` 让 since=0 时 replay=[] → 这些事件全丢,
|
|
141
|
+
// 用户报"发 chat 后 agent 不回应"。改成无条件 filter:since=0 + buffer 空(首连)→
|
|
142
|
+
// replay=[] 不变;since=0 + buffer 含 hydrate 期间事件 → 全 replay 推前端。
|
|
143
|
+
const replay = this._buffer.filter(e => e.seq > since);
|
|
144
|
+
const replayedSeqs = new Set();
|
|
145
|
+
for (const evt of replay) {
|
|
146
|
+
replayedSeqs.add(evt.seq);
|
|
147
|
+
try { fn(evt); }
|
|
148
|
+
catch (err) { console.warn(`[EventBus] replay listener threw:`, err.message); }
|
|
149
|
+
}
|
|
150
|
+
for (const evt of queue) {
|
|
151
|
+
if (replayedSeqs.has(evt.seq)) continue;
|
|
152
|
+
try { fn(evt); }
|
|
153
|
+
catch (err) { console.warn(`[EventBus] queue drain threw:`, err.message); }
|
|
154
|
+
}
|
|
155
|
+
live = true;
|
|
156
|
+
return { unsubscribe: off, replayed: replay.length, gap };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* 同步 collect 所有事件到一个数组(测试用)。返回数组 + 取消函数。
|
|
161
|
+
*/
|
|
162
|
+
collect(pattern = '*') {
|
|
163
|
+
const buffer = [];
|
|
164
|
+
const off = this.subscribe(pattern, (evt) => buffer.push(evt));
|
|
165
|
+
return { buffer, stop: off };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function matches(pattern, type) {
|
|
170
|
+
if (pattern === '*') return true;
|
|
171
|
+
if (pattern === type) return true;
|
|
172
|
+
// 前缀匹配:'run.delta' 匹配 'run.delta.text'
|
|
173
|
+
return type.startsWith(pattern + '.');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ── 事件构造器(type-safe helpers,调用方少写字面量)──
|
|
177
|
+
|
|
178
|
+
export const Events = {
|
|
179
|
+
start: () => ({ type: 'run.start' }),
|
|
180
|
+
roundStart: (round) => ({ type: 'run.round.start', round }),
|
|
181
|
+
roundEnd: (round, stopReason, usage) => ({ type: 'run.round.end', round, stopReason, usage }),
|
|
182
|
+
deltaText: (round, text) => ({ type: 'run.delta.text', round, text }),
|
|
183
|
+
// 铅笔精灵的手写短句(2026-08-14 日记本批):回合内回复的压缩旁白,
|
|
184
|
+
// 本地整形、一条 assistant 消息一发。⚠️ 2026-08-19 前这里同一 round 会来
|
|
185
|
+
// 两发(首句底稿 refined:false + haiku 精修 refined:true,前端后到覆盖)——
|
|
186
|
+
// 那发 haiku 写死走订阅、不跟随会话模型,整条线路已拆,`refined` 随之取消。
|
|
187
|
+
// 收场 recap(run.recap)是同一条 haiku 通道的另一个出口,同批退役。
|
|
188
|
+
spriteSummary: (round, text) => ({ type: 'run.sprite_summary', round, text }),
|
|
189
|
+
// SDK helper 生成的一句话工具批摘要(Claude Code 侧栏那种折叠标题)
|
|
190
|
+
toolUseSummary: (summary, blockIds) => ({ type: 'run.tool_use_summary', summary, blockIds }),
|
|
191
|
+
deltaThinking: (round, text) => ({ type: 'run.delta.thinking', round, text }),
|
|
192
|
+
deltaToolUse: (round, blockId, name, input) => ({ type: 'run.delta.tool_use', round, blockId, name, input }),
|
|
193
|
+
// 真流式工具入参(2026-07-28):patch = { filePath?, append?, done? }。
|
|
194
|
+
// append 是抽出字段(Edit.new_string / Write.content)相对上次的纯文本增量
|
|
195
|
+
deltaToolInput: (round, blockId, name, patch) => ({ type: 'run.delta.tool_input', round, blockId, name, ...patch }),
|
|
196
|
+
|
|
197
|
+
// 工具 streaming 起点 —— SDK content_block_start (type: tool_use) 触发,
|
|
198
|
+
// 只携 blockId + name,input 此时还没流完。让前端立刻显示 icon + 名字
|
|
199
|
+
// status='running',等后续 deltaToolUse 来时同 blockId update 完整 input。
|
|
200
|
+
toolUseStarted: (round, blockId, name) => ({ type: 'run.tool_use.started', round, blockId, name }),
|
|
201
|
+
deltaToolResult: (round, blockId, name, ok, output, error, images) => ({
|
|
202
|
+
type: 'run.delta.tool_result', round, blockId, name, ok,
|
|
203
|
+
...(output !== undefined ? { output } : {}),
|
|
204
|
+
...(error ? { error } : {}),
|
|
205
|
+
// C24:image content blocks(base64)单独传,让前端 <img src="data:..."> 渲染
|
|
206
|
+
...(images && images.length > 0 ? { images } : {}),
|
|
207
|
+
}),
|
|
208
|
+
todoUpdated: (todos) => ({ type: 'run.todo.updated', todos }),
|
|
209
|
+
cancelled: (reason) => ({ type: 'run.cancelled', reason }),
|
|
210
|
+
done: (finalText, artifactPath, snapshot) => ({
|
|
211
|
+
type: 'run.done', finalText, artifactPath, snapshot,
|
|
212
|
+
}),
|
|
213
|
+
error: (message, code, stack) => ({ type: 'run.error', message, code, stack }),
|
|
214
|
+
|
|
215
|
+
// streamInput 重构(Phase 2):session-level query 起停事件,跟 per-turn
|
|
216
|
+
// run.start/done 区分 —— 前端用它判断"会话还活着,能继续追加" vs "会话死了"
|
|
217
|
+
querySessionStart: (sessionId) => ({ type: 'run.query.start', sessionId }),
|
|
218
|
+
querySessionEnd: (sessionId, reason) => ({ type: 'run.query.end', sessionId, reason }),
|
|
219
|
+
// 排队提示:用户在 agent 跑时追加消息,UI 显示"已排队 N 条"
|
|
220
|
+
queueDepth: (sessionId, depth) => ({ type: 'run.queue.depth', sessionId, depth }),
|
|
221
|
+
|
|
222
|
+
// ── 会话收敛 E1a(2026-08-13):项目级 active session 指针变更广播 ──
|
|
223
|
+
//
|
|
224
|
+
// 会话真相源收敛到 projects.active_session_id(服务端指针)之后,指针一动
|
|
225
|
+
// 就广播这条,同项目的其他标签页靠它把自己对齐到服务端真相。
|
|
226
|
+
//
|
|
227
|
+
// ⚠️ 这条事件**绝对不能带 sessionId 字段**:ws/index.js 的下发过滤器是
|
|
228
|
+
// `event.sessionId && event.sessionId !== sid` —— 带上的话,正开着**别的**
|
|
229
|
+
// 会话(或还没有会话)的标签页会把它过滤掉,而那些标签页恰恰是最需要
|
|
230
|
+
// 知道"指针已经指向别处"的人。载荷字段叫 activeSessionId,绕开过滤器的
|
|
231
|
+
// 同时语义也更准:它是指针的新值,不是"这条事件属于哪个会话"。
|
|
232
|
+
// activeSessionId 可为 null(指针被清空,比如指向的会话被删)。
|
|
233
|
+
projectActiveSession: (activeSessionId) => ({ type: 'project.active_session', activeSessionId }),
|
|
234
|
+
|
|
235
|
+
// ── P0+ stage 1 新增(28+ 种 SDK message 翻译)──
|
|
236
|
+
toolProgress: (blockId, toolName, elapsedSeconds) => ({
|
|
237
|
+
type: 'run.tool_progress', blockId, toolName, elapsedSeconds,
|
|
238
|
+
}),
|
|
239
|
+
promptSuggestion: (suggestion) => ({ type: 'run.prompt_suggestion', suggestion }),
|
|
240
|
+
// toolUseId 关键 —— main agent 用 Task 工具调子代理时,SDK 推 task_*
|
|
241
|
+
// events 都带 tool_use_id,前端用它把 task 状态绑到对应的 Task tool message
|
|
242
|
+
// ⚠️ 只发真子代理(agent-shared.js isSubagentTask 收口):SDK 的统一任务
|
|
243
|
+
// 系统里后台 Bash / Workflow 也发 task_*,那些不进这条事件。
|
|
244
|
+
// subagentType = SDK task_started.subagent_type(explorer / vision-checker
|
|
245
|
+
// 这种真名);taskType 是 'local_agent' 这类泛名,只作兜底展示。
|
|
246
|
+
taskStarted: (taskId, description, subagentType, taskType, prompt, toolUseId) => ({
|
|
247
|
+
type: 'run.task.started', taskId, description, subagentType, taskType, prompt, toolUseId,
|
|
248
|
+
}),
|
|
249
|
+
taskProgress: (taskId, description, summary, lastToolName, usage, toolUseId) => ({
|
|
250
|
+
type: 'run.task.progress', taskId, description, summary, lastToolName, usage, toolUseId,
|
|
251
|
+
}),
|
|
252
|
+
taskUpdated: (taskId, patch, toolUseId) => ({
|
|
253
|
+
type: 'run.task.updated', taskId, patch, toolUseId,
|
|
254
|
+
}),
|
|
255
|
+
taskNotification: (taskId, status, summary, usage, toolUseId) => ({
|
|
256
|
+
type: 'run.task.notification', taskId, status, summary, usage, toolUseId,
|
|
257
|
+
}),
|
|
258
|
+
filesPersisted: (files, failed) => ({ type: 'run.files_persisted', files, failed }),
|
|
259
|
+
memoryRecall: (mode, memories) => ({ type: 'run.memory_recall', mode, memories }),
|
|
260
|
+
notification: (key, text, priority, color, timeoutMs) => ({
|
|
261
|
+
type: 'run.notification', key, text, priority, color, timeoutMs,
|
|
262
|
+
}),
|
|
263
|
+
sessionState: (state) => ({ type: 'run.session_state', state }),
|
|
264
|
+
systemInit: (info) => ({ type: 'run.system_init', info }),
|
|
265
|
+
hookStarted: (hookName, hookEvent) => ({ type: 'run.hook.started', hookName, hookEvent }),
|
|
266
|
+
hookResponse: (hookName, hookEvent, outcome, output, exitCode) => ({
|
|
267
|
+
type: 'run.hook.response', hookName, hookEvent, outcome, output, exitCode,
|
|
268
|
+
}),
|
|
269
|
+
|
|
270
|
+
// C4 FileChanged hook → 前端 reload iframe
|
|
271
|
+
fileChanged: (filePath, event) => ({ type: 'run.file_changed', filePath, event }),
|
|
272
|
+
|
|
273
|
+
// ── Phase 1 翻译补全 ──
|
|
274
|
+
|
|
275
|
+
// SDKAPIRetryMessage(sdk.d.ts:2322):API 请求失败可重试。
|
|
276
|
+
// 当前 handleSystemMessage 把 'api_retry' subtype 落进 default warn —— 改为 emit
|
|
277
|
+
// 让上层(前端 / 监控)能看到"还在重试"。
|
|
278
|
+
// error_status: HTTP 状态码,连接错误/超时时为 null(无 HTTP 响应)。
|
|
279
|
+
apiRetry: (attempt, maxRetries, retryDelayMs, errorStatus, errorKind) => ({
|
|
280
|
+
type: 'run.api_retry',
|
|
281
|
+
attempt,
|
|
282
|
+
maxRetries,
|
|
283
|
+
retryDelayMs,
|
|
284
|
+
errorStatus, // number | null
|
|
285
|
+
errorKind, // SDKAssistantMessageError union: rate_limit / server_error / ...
|
|
286
|
+
}),
|
|
287
|
+
|
|
288
|
+
// ── Phase 2 hooks 主动捕事件 ──
|
|
289
|
+
|
|
290
|
+
// SessionStart hook:source 'startup' | 'resume' | 'clear' | 'compact'
|
|
291
|
+
// 上层据此可决定要不要展示"接续上次会话"提示
|
|
292
|
+
sessionStart: (source, agentType, model) => ({
|
|
293
|
+
type: 'run.session_start', source, agentType, model,
|
|
294
|
+
}),
|
|
295
|
+
|
|
296
|
+
// SubagentStart hook(sdk.d.ts:5258):主动捕子代理启动。
|
|
297
|
+
// SDK system 'task_started' message 是间接路径(依赖 agentProgressSummaries),
|
|
298
|
+
// 这条 hook 是更可靠的主入口。
|
|
299
|
+
subagentStart: (agentId, agentType) => ({
|
|
300
|
+
type: 'run.subagent.start', agentId, agentType,
|
|
301
|
+
}),
|
|
302
|
+
|
|
303
|
+
// SubagentStop hook(sdk.d.ts:5269):子代理结束。带 last_assistant_message 字段
|
|
304
|
+
// 不用读 transcript 也能拿到收尾文本。
|
|
305
|
+
// toolUseId:SDK callback 第 2 参,是触发该 subagent 启动的 main agent
|
|
306
|
+
// Task tool_use_id —— 前端能用它把 critique 挂回对应的 Task message。
|
|
307
|
+
subagentStop: (agentId, agentType, lastAssistantMessage, transcriptPath, toolUseId) => ({
|
|
308
|
+
type: 'run.subagent.stop',
|
|
309
|
+
agentId, agentType,
|
|
310
|
+
lastAssistantMessage, // 可选;agent 收尾的最后一句
|
|
311
|
+
transcriptPath, // 可选;子代理转录文件路径,前端展开"完整对话"用
|
|
312
|
+
...(toolUseId ? { toolUseId } : {}),
|
|
313
|
+
}),
|
|
314
|
+
|
|
315
|
+
// PostToolUseFailure hook:工具失败,hook 已注入了恢复建议给 agent,
|
|
316
|
+
// 上层只需可见"哪个工具失败了"做监控/告警。
|
|
317
|
+
toolFailure: (toolName, error) => ({
|
|
318
|
+
type: 'run.tool_failure', toolName, error,
|
|
319
|
+
}),
|
|
320
|
+
|
|
321
|
+
// ── Canvas 焕新升级 S1(2026-05-02)──
|
|
322
|
+
|
|
323
|
+
// S1d PostToolUse(Edit|Write canvas.html) hook 自动检测改动的 page 号 emit。
|
|
324
|
+
// 前端 SlideNavigator 收到后自动 scrollIntoView + 1.5s pulse 高亮该 section。
|
|
325
|
+
// pages: number[] —— 一次 Edit 可能跨多页(罕见),通常单元素。
|
|
326
|
+
// anchor: 可选,data-anchor 值(如 'cover-title'),前端能精确高亮元素而非整页。
|
|
327
|
+
canvasFocusPage: (pages, anchor) => ({
|
|
328
|
+
type: 'run.canvas_focus_page',
|
|
329
|
+
pages,
|
|
330
|
+
...(anchor ? { anchor } : {}),
|
|
331
|
+
}),
|
|
332
|
+
|
|
333
|
+
// ── Canvas 焕新 C1(2026-05-02):MCP 工具反向通道 + tweaks/buffer 通知 ──
|
|
334
|
+
|
|
335
|
+
// navigate_to_page 工具触发,前端 ProjectWorkspace 收到后切到第 N 页
|
|
336
|
+
// (SlideNavigator setActivePage / iframe 内 scrollIntoView section[data-page="N"])
|
|
337
|
+
canvasNavigate: (page) => ({ type: 'run.canvas_navigate', page }),
|
|
338
|
+
|
|
339
|
+
// preview_deck 工具触发:前端把这张 deck 卡"替用户双击一下"
|
|
340
|
+
//(收起态→内嵌渲染;已展开→画布内最大化窗)。path 为 null = 当前会话的 deck
|
|
341
|
+
deckPreview: (filePath) => ({ type: 'run.deck_preview', path: filePath || null }),
|
|
342
|
+
|
|
343
|
+
// highlight 工具触发,前端在 InspectFloatingCard 同层挂 pulse overlay
|
|
344
|
+
canvasHighlight: (selector, durationMs = 1500) => ({
|
|
345
|
+
type: 'run.canvas_highlight', selector, durationMs,
|
|
346
|
+
}),
|
|
347
|
+
|
|
348
|
+
// expose_tweaks 工具完成,前端 TweaksPanel reload spec.tweaks
|
|
349
|
+
tweaksExposed: (count, added, replaced) => ({
|
|
350
|
+
type: 'run.tweaks_exposed', count, added, replaced,
|
|
351
|
+
}),
|
|
352
|
+
|
|
353
|
+
// clear_pending_changes 工具完成,前端可清相应本地 pending hint。
|
|
354
|
+
// clearedIds 让前端 comments state 精确 filter 出橙色 overlay(前后端 id 已统一)
|
|
355
|
+
pendingChangesCleared: (clearedIds, removed, remaining) => ({
|
|
356
|
+
type: 'run.pending_changes_cleared', clearedIds, removed, remaining,
|
|
357
|
+
}),
|
|
358
|
+
|
|
359
|
+
// ── A4.1(2026-05-02):AskUserQuestion 走 SDK canUseTool 路径 ──
|
|
360
|
+
//
|
|
361
|
+
// SDK binary 把 AskUserQuestion 设为 `shouldDefer: true` + `requiresUserInteraction: true`,
|
|
362
|
+
// 工具的 checkPermissions 总返 `behavior: 'ask', message: 'Answer questions?'`,
|
|
363
|
+
// 等 host 程序通过 canUseTool callback 返 `behavior: 'allow', updatedInput: {
|
|
364
|
+
// ...input, answers: {...} }`,binary 拿到带 answers 的 input → 调 tool.call →
|
|
365
|
+
// 工具直接返回 answers 作 result(cli.js:GR6.call 实现)。
|
|
366
|
+
//
|
|
367
|
+
// 我们的 canUseTool(session-loop.js)拦到此事件 → emit run.ask_user_question →
|
|
368
|
+
// 前端 AskUserQuestionView 卡片 → 用户点选项 → POST /answer → resolve
|
|
369
|
+
// canUseTool 的 await,返回 updatedInput。
|
|
370
|
+
//
|
|
371
|
+
// questions 是从工具 input 摘出的 `{ question, header, options, multiSelect }[]`
|
|
372
|
+
// 数组(详见 cli.js:zHK schema),前端按现有卡片字段直接消费。
|
|
373
|
+
askUserQuestion: (toolUseId, questions) => ({
|
|
374
|
+
type: 'run.ask_user_question',
|
|
375
|
+
toolUseId,
|
|
376
|
+
questions,
|
|
377
|
+
}),
|
|
378
|
+
|
|
379
|
+
// ── A1.2(2026-05-02):实时上下文用量 ──
|
|
380
|
+
// SDKControlGetContextUsageResponse(sdk.d.ts:2451-2541)由 query.getContextUsage()
|
|
381
|
+
// 返回。session-loop.js 在每个 assistant message 后 await 一次 emit,前端 ContextUsageBar
|
|
382
|
+
// 接事件渲进度条 + breakdown + autoCompact 阈值预警。
|
|
383
|
+
//
|
|
384
|
+
// 关键字段(为前端做轻量化封装,不全量透传以节省 ws 带宽):
|
|
385
|
+
// totalTokens / maxTokens / percentage — 主进度条数据
|
|
386
|
+
// autoCompactThreshold — 触发 autoCompact 的阈值(绝对 token)
|
|
387
|
+
// isAutoCompactEnabled — 当前开关状态(前端预警条件之一)
|
|
388
|
+
// model — 当前模型 id
|
|
389
|
+
// messageBreakdown.toolCallsByType — 工具消耗 token 排名(展开看)
|
|
390
|
+
// memoryFiles / mcpTools / agents / skills — 各类目消耗(展开看)
|
|
391
|
+
//
|
|
392
|
+
// categories / gridRows 不传 —— 前端不渲网格,只用 percentage + breakdown。
|
|
393
|
+
// ── 图片生成(Phase A,2026-05-06 后段)──
|
|
394
|
+
// generate_image MCP 工具调完后 emit。前端可在 chat / timeline 实时插
|
|
395
|
+
// thumbnail,并把 path 关联回 spec.json 的 record_decision 节点。
|
|
396
|
+
// 字段:
|
|
397
|
+
// path agent cwd 相对路径(assets/generated/<file>.png)
|
|
398
|
+
// absPath 绝对落地路径(前端不一定用,便于审计)
|
|
399
|
+
// sizeBytes png 字节数
|
|
400
|
+
// prompt 生成 prompt 全文
|
|
401
|
+
// assetRole 语义类(hero/cover/bg/.../pattern),可空
|
|
402
|
+
// aspectRatio / imageSize 生成参数
|
|
403
|
+
// referenceImageCount 喂进去的参考图数量
|
|
404
|
+
// accompanyText 模型可选的文字 commentary(responseModalities 含 TEXT 时才有)
|
|
405
|
+
imageGenerated: (info) => ({
|
|
406
|
+
type: 'run.image_generated',
|
|
407
|
+
path: info.path,
|
|
408
|
+
absPath: info.absPath,
|
|
409
|
+
sizeBytes: info.sizeBytes,
|
|
410
|
+
prompt: info.prompt,
|
|
411
|
+
assetRole: info.assetRole || null,
|
|
412
|
+
aspectRatio: info.aspectRatio,
|
|
413
|
+
imageSize: info.imageSize,
|
|
414
|
+
referenceImageCount: info.referenceImageCount || 0,
|
|
415
|
+
...(info.accompanyText ? { accompanyText: info.accompanyText } : {}),
|
|
416
|
+
}),
|
|
417
|
+
|
|
418
|
+
// appModel: NoDesign 上层真实 model(如 kimi-k2.6)。spoofing 后 SDK 的
|
|
419
|
+
// usage.model 是 alias(如 claude-opus-4-7[1m])不可信;调用方需把真 appModel
|
|
420
|
+
// 传进来。给定 appModel 时按真实容量算 maxTokens / percentage,前端 ContextUsageBar
|
|
421
|
+
// 显的就是 gateway 真正能扛的容量(不是 SDK 内部 compact 触发线)。
|
|
422
|
+
contextUsage: (usage, appModel = null) => {
|
|
423
|
+
// 这里 import 在 file 顶(已加),避免循环依赖问题
|
|
424
|
+
//
|
|
425
|
+
// 分母的兜底链只能有一条(2026-07-30):以前 hooks.js 那个生产者自己写了
|
|
426
|
+
// 一条更长的(?? rawMaxTokens ?? 256000),两条链在 appModel 缺失时给出不同
|
|
427
|
+
// 的分母,同一个会话前后两次百分比对不上。合并到这里,256000 那个 kimi 时代
|
|
428
|
+
// 的常量去掉 —— 拿它当 Claude 会话的分母只会算出个假的高百分比。
|
|
429
|
+
const realMax = (appModel ? resolveModelContextWindow(appModel) : null)
|
|
430
|
+
?? usage.maxTokens
|
|
431
|
+
?? usage.rawMaxTokens
|
|
432
|
+
?? null;
|
|
433
|
+
const totalTokens = usage.totalTokens;
|
|
434
|
+
const percentage = realMax > 0 ? Math.round((totalTokens / realMax) * 100) : (usage.percentage || 0);
|
|
435
|
+
return {
|
|
436
|
+
type: 'run.context_usage',
|
|
437
|
+
totalTokens,
|
|
438
|
+
maxTokens: realMax, // 真实容量(kimi=256k)—— 前端进度条分母
|
|
439
|
+
sdkMaxTokens: usage.maxTokens, // SDK 给的(compact 触发线 230k)—— debug
|
|
440
|
+
percentage,
|
|
441
|
+
autoCompactThreshold: usage.autoCompactThreshold,
|
|
442
|
+
isAutoCompactEnabled: usage.isAutoCompactEnabled,
|
|
443
|
+
model: appModel || usage.model,
|
|
444
|
+
// 出自谁家(BRANDS 之一)——画布精灵据此换身份标。⚠️ 不能让前端读 SDK 的 usage.model 去认牌子:
|
|
445
|
+
// spoofing 之后那是 alias(DeepSeek 行报 claude-opus-4-7[1m]),照它认牌子会把鲸画成星芒
|
|
446
|
+
brand: appModel ? brandOfModel(appModel) : null,
|
|
447
|
+
// 轻量化 breakdown — 前端 hover/expand 能看到细节,不渲细节时不占空间
|
|
448
|
+
messageBreakdown: usage.messageBreakdown ? {
|
|
449
|
+
toolCallTokens: usage.messageBreakdown.toolCallTokens,
|
|
450
|
+
toolResultTokens: usage.messageBreakdown.toolResultTokens,
|
|
451
|
+
attachmentTokens: usage.messageBreakdown.attachmentTokens,
|
|
452
|
+
assistantMessageTokens: usage.messageBreakdown.assistantMessageTokens,
|
|
453
|
+
userMessageTokens: usage.messageBreakdown.userMessageTokens,
|
|
454
|
+
// toolCallsByType 是排行(用于"哪个工具吃 token 最多"),保留 top 10
|
|
455
|
+
toolCallsByType: (usage.messageBreakdown.toolCallsByType || [])
|
|
456
|
+
.slice(0, 10),
|
|
457
|
+
} : null,
|
|
458
|
+
// 类目级别 token 数(聚合,不传完整数组)
|
|
459
|
+
memoryFilesTokens: (usage.memoryFiles || []).reduce((s, m) => s + (m.tokens || 0), 0),
|
|
460
|
+
mcpToolsTokens: (usage.mcpTools || []).reduce((s, t) => s + (t.tokens || 0), 0),
|
|
461
|
+
agentsTokens: (usage.agents || []).reduce((s, a) => s + (a.tokens || 0), 0),
|
|
462
|
+
};
|
|
463
|
+
},
|
|
464
|
+
};
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canvas 一致性校验(2026-05-08 范式重整 #1/#3/#5/#6 反馈通道)。
|
|
3
|
+
* (2026-08-14 可维护性行动:从 hooks.js 原样迁出,语义零改动)
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'node:fs/promises';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
|
|
8
|
+
/** 读 canvas.html 时的最大字节数(防大文件吞内存) */
|
|
9
|
+
const CANVAS_HTML_MAX_BYTES = 2 * 1024 * 1024;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 校验前预处理:strip HTML 注释 + CSS/JS block 注释
|
|
13
|
+
*
|
|
14
|
+
* 防止 false positive:模板里的 `<!-- ┄┄┄ 骨架范例 ... data-anchor="cover" ┄┄┄ -->`
|
|
15
|
+
* HTML 注释 + page-styles 里"取消注释切到 ppt mode"的 CSS 注释切片,原始 grep
|
|
16
|
+
* 都会误匹配。预先 strip 后再校验。
|
|
17
|
+
*
|
|
18
|
+
* 仅用于 validator 内部 regex 扫描;agent 看到的源文件不受影响。
|
|
19
|
+
*/
|
|
20
|
+
function stripCommentsForValidate(html) {
|
|
21
|
+
return html
|
|
22
|
+
.replace(/<!--[\s\S]*?-->/g, '') // HTML 注释
|
|
23
|
+
.replace(/\/\*[\s\S]*?\*\//g, ''); // CSS / JS block 注释(含 babel script)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* LAYOUT_COMPONENT_TRIGGERS — data-layout 值 → 推荐组件 import / detect 列表
|
|
28
|
+
*
|
|
29
|
+
* 校验项 4(#6)消费:data-layout ∈ keys + babel script 段所有 detect 都不命中
|
|
30
|
+
* → warn agent reach for 推荐组件(模板自带 inline 4 件 / 或 import @radix-ui)
|
|
31
|
+
*
|
|
32
|
+
* 形态:{ [layoutName]: { recommend: string[], detect: string[] regex sources } }
|
|
33
|
+
*/
|
|
34
|
+
const LAYOUT_COMPONENT_TRIGGERS = {
|
|
35
|
+
'comparison-table': { recommend: ['<Tabs>', '<Card>'], detect: ['\\bTabs\\s*[\\.<]', '<TabsList\\b', '<Card\\b', "import[^;]*['\"]@radix-ui/react-tabs['\"]"] },
|
|
36
|
+
'feature-cards': { recommend: ['<Card> 阵列'], detect: ['<Card\\b', '<CardHeader\\b', '<CardContent\\b', '<CardTitle\\b'] },
|
|
37
|
+
'use-cases': { recommend: ['<Tabs>', '<Card> 阵列'], detect: ['\\bTabs\\s*[\\.<]', '<Card\\b', "import[^;]*['\"]@radix-ui/react-tabs['\"]"] },
|
|
38
|
+
'core-products': { recommend: ['<Card> 阵列', '<Tabs>'], detect: ['<Card\\b', '\\bTabs\\s*[\\.<]'] },
|
|
39
|
+
'tech-highlights': { recommend: ['<Card> 阵列'], detect: ['<Card\\b', '<Badge\\b'] },
|
|
40
|
+
'feature-array': { recommend: ['<Tabs>', '<Card>'], detect: ['\\bTabs\\s*[\\.<]', '<Card\\b'] },
|
|
41
|
+
'variant-showcase': { recommend: ['<Tabs> (≤4) / embla-carousel-react (>4)'], detect: ['\\bTabs\\s*[\\.<]', 'embla-carousel-react', 'useEmblaCarousel'] },
|
|
42
|
+
'comparison': { recommend: ['<Tabs>', '<Card> 对比阵列'], detect: ['\\bTabs\\s*[\\.<]', '<Card\\b'] },
|
|
43
|
+
'step-switcher': { recommend: ['<Tabs>'], detect: ['\\bTabs\\s*[\\.<]', "import[^;]*['\"]@radix-ui/react-tabs['\"]"] },
|
|
44
|
+
'concept-vs-misconception': { recommend: ['<Tabs>', '<Card> 对照阵列'], detect: ['\\bTabs\\s*[\\.<]', '<Card\\b'] },
|
|
45
|
+
'config-switcher': { recommend: ['<Tabs>'], detect: ['\\bTabs\\s*[\\.<]'] },
|
|
46
|
+
'quadrant': { recommend: ['<Card> 4 格阵列'], detect: ['<Card\\b', 'grid-cols-2'] },
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 校验项 1:data-anchor 唯一性
|
|
51
|
+
*
|
|
52
|
+
* 全文 grep `data-anchor="X"`,按值分组,重名 → 报冲突 + 列页号
|
|
53
|
+
*/
|
|
54
|
+
function validateAnchorUniqueness(html) {
|
|
55
|
+
const matches = [...html.matchAll(/data-anchor\s*=\s*['"]([^'"]+)['"]/g)];
|
|
56
|
+
if (matches.length === 0) return null;
|
|
57
|
+
|
|
58
|
+
const groups = new Map();
|
|
59
|
+
for (const m of matches) {
|
|
60
|
+
const value = m[1];
|
|
61
|
+
const idx = m.index;
|
|
62
|
+
// 反推所在 page:往前找最近的 <section data-page="N">
|
|
63
|
+
const before = html.slice(0, idx);
|
|
64
|
+
const lastSection = [...before.matchAll(/<section\b[^>]*data-page\s*=\s*['"](\d+)['"]/g)].pop();
|
|
65
|
+
const page = lastSection ? lastSection[1] : '?';
|
|
66
|
+
if (!groups.has(value)) groups.set(value, []);
|
|
67
|
+
groups.get(value).push(page);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const conflicts = [];
|
|
71
|
+
for (const [value, pages] of groups) {
|
|
72
|
+
if (pages.length > 1) {
|
|
73
|
+
conflicts.push(`"${value}" → 出现在 page ${[...new Set(pages)].join(', ')} (${pages.length} 次)`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (conflicts.length === 0) return null;
|
|
77
|
+
return {
|
|
78
|
+
title: `data-anchor 重名 ${conflicts.length} 处`,
|
|
79
|
+
detail: conflicts.join('\n ') + '\n data-anchor 必须 deck 内唯一(重名加 -pN 页号或角色后缀,如 portrait-name-p3 / cover-sub-1)。findElementByAnchor 三层 fallback 第一层是按 data-anchor 查;重名时 querySelector 永远返第一个匹配,DirectEdit / 评论 pin 到错的元素。',
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 校验项 2:data-layout 推荐组件 reach for 检查(#6)
|
|
85
|
+
*
|
|
86
|
+
* data-layout ∈ LAYOUT_COMPONENT_TRIGGERS keys 且整文件 babel script 段所有
|
|
87
|
+
* detect[i] regex 都不命中 → warn agent 用 inline 4 件
|
|
88
|
+
*/
|
|
89
|
+
function validateLayoutComponents(html) {
|
|
90
|
+
const layoutMatches = [...html.matchAll(/data-layout\s*=\s*['"]([^'"]+)['"]/g)];
|
|
91
|
+
if (layoutMatches.length === 0) return null;
|
|
92
|
+
|
|
93
|
+
// 抽 babel script 段(多个)拼一起
|
|
94
|
+
const babelBlocks = [...html.matchAll(/<script[^>]*type=["']text\/babel["'][^>]*>([\s\S]*?)<\/script>/gi)]
|
|
95
|
+
.map(m => m[1]).join('\n');
|
|
96
|
+
|
|
97
|
+
const issues = [];
|
|
98
|
+
const seen = new Set();
|
|
99
|
+
for (const m of layoutMatches) {
|
|
100
|
+
const layoutName = m[1];
|
|
101
|
+
if (seen.has(layoutName)) continue;
|
|
102
|
+
seen.add(layoutName);
|
|
103
|
+
const trigger = LAYOUT_COMPONENT_TRIGGERS[layoutName];
|
|
104
|
+
if (!trigger) continue;
|
|
105
|
+
const anyHit = trigger.detect.some(src => {
|
|
106
|
+
try { return new RegExp(src).test(babelBlocks); } catch { return false; }
|
|
107
|
+
});
|
|
108
|
+
if (!anyHit) {
|
|
109
|
+
issues.push(`data-layout="${layoutName}" 适合 ${trigger.recommend.join(' / ')},但 babel script 段没检测到对应组件`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (issues.length === 0) return null;
|
|
113
|
+
return {
|
|
114
|
+
title: `${issues.length} 处 data-layout 漏用推荐组件`,
|
|
115
|
+
detail: issues.join('\n ') + '\n 模板 <script id="__nd-shadcn-lite"> 已自带 Card / Button / Badge / Tabs,0 import 直接 <Card> / <Tabs> 用即可。选型见首次写 deck 时注入的 hybrid 技术参考(inline shadcn 一节)。',
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 校验项 3:data-layout-role 必装
|
|
121
|
+
*
|
|
122
|
+
* 每个 <section data-page> 必标 data-layout-role
|
|
123
|
+
*/
|
|
124
|
+
function validateLayoutRolePresence(html) {
|
|
125
|
+
const sections = [...html.matchAll(/<section\b[^>]*data-page\s*=\s*['"](\d+)['"][^>]*>/g)];
|
|
126
|
+
if (sections.length === 0) return null;
|
|
127
|
+
const missing = [];
|
|
128
|
+
for (const m of sections) {
|
|
129
|
+
const tag = m[0];
|
|
130
|
+
if (!/data-layout-role\s*=/.test(tag)) {
|
|
131
|
+
missing.push(m[1]);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (missing.length === 0) return null;
|
|
135
|
+
return {
|
|
136
|
+
title: `${missing.length} 个 section 缺 data-layout-role`,
|
|
137
|
+
detail: `Page ${missing.join(', ')} 没标 data-layout-role(image-led / text-led / data-led / hybrid 必选其一)。这字段决定页型分布 + 视觉判断;缺它系统按"未知"处理,patterns/<role>.md 也无法对应。`,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Canvas validation 总入口(PostToolUse Edit|Write canvas.html 触发)
|
|
143
|
+
*
|
|
144
|
+
* matcher 第一行 path filter:仅对 canvas.html 跑校验,其他文件 noop。
|
|
145
|
+
* 单 hook 内 3 项串行校验(in-memory,~ms),有 issue 拼 systemMessage 注下一轮。
|
|
146
|
+
*
|
|
147
|
+
* 单 turn 反馈(不持久化):agent 不修则下次 Edit 自然惩罚再报,跨 turn 持续
|
|
148
|
+
* 延期下一轮(spec.json schema 扩展)。
|
|
149
|
+
*/
|
|
150
|
+
export function makePostToolUseCanvasValidationHandler({ ctx: _ctx, workspaceRoot }) {
|
|
151
|
+
return async (input, _toolUseId, _options) => {
|
|
152
|
+
try {
|
|
153
|
+
const fp = input?.tool_input?.file_path;
|
|
154
|
+
// deck 现在叫 <名>.html(canvas.html 只是常用名):先按扩展名放行,读到内容再按 deck wrap 标记认
|
|
155
|
+
if (!fp || !/\.html?$/i.test(fp)) return {};
|
|
156
|
+
if (!workspaceRoot) return {};
|
|
157
|
+
|
|
158
|
+
// 校验刚写的那份(2026-07-28):任务模型下 deck 在 tasks/<任务>/canvas.html,
|
|
159
|
+
// 这里以前固定读 cwd/canvas.html —— 文件不存在直接 ENOENT return,
|
|
160
|
+
// 于是整套一致性校验在任务模型下从来没跑过。
|
|
161
|
+
const canvasPath = path.resolve(workspaceRoot, fp);
|
|
162
|
+
let html;
|
|
163
|
+
try {
|
|
164
|
+
const stat = await fs.stat(canvasPath);
|
|
165
|
+
if (stat.size > CANVAS_HTML_MAX_BYTES) return {}; // 大文件 noop
|
|
166
|
+
html = await fs.readFile(canvasPath, 'utf8');
|
|
167
|
+
} catch (err) {
|
|
168
|
+
if (err.code === 'ENOENT') return {};
|
|
169
|
+
throw err;
|
|
170
|
+
}
|
|
171
|
+
if (!/canvas\.html$/i.test(fp) && !html.includes('__nd-deck-wrap')) return {}; // 不是 deck(站点页等)
|
|
172
|
+
|
|
173
|
+
// 预 strip 注释(HTML + CSS/JS block)防 false positive
|
|
174
|
+
const cleaned = stripCommentsForValidate(html);
|
|
175
|
+
// 依序调对应 validator。旧版含 mode-CSS / 必装 CSS 两项已删
|
|
176
|
+
// (2026-05-08 范式简化:不再有 deck-mode)
|
|
177
|
+
|
|
178
|
+
const issues = [
|
|
179
|
+
validateAnchorUniqueness(cleaned),
|
|
180
|
+
validateLayoutComponents(cleaned),
|
|
181
|
+
validateLayoutRolePresence(cleaned),
|
|
182
|
+
].filter(Boolean);
|
|
183
|
+
|
|
184
|
+
if (issues.length === 0) return {};
|
|
185
|
+
|
|
186
|
+
const body = issues.map((i, idx) => `${idx + 1}. ${i.title}\n ${i.detail}`).join('\n\n');
|
|
187
|
+
return {
|
|
188
|
+
systemMessage:
|
|
189
|
+
`<system-reminder>\n[canvas-validate] 你刚改完 ${fp},系统检测到 ${issues.length} 项可疑:\n\n`
|
|
190
|
+
+ body
|
|
191
|
+
+ `\n\n如果有意为之(custom mode / 故意命名重复 等)忽略;否则在下一轮主动修。\n`
|
|
192
|
+
+ `</system-reminder>`,
|
|
193
|
+
};
|
|
194
|
+
} catch (err) {
|
|
195
|
+
console.warn('[hooks/canvas-validate] threw:', err.message);
|
|
196
|
+
return {};
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
}
|