@xiaobuyu/nodesign 0.0.7 → 0.0.8
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 +266 -79
- package/README.zh-CN.md +254 -67
- package/package.json +2 -2
- package/server/api/admin.js +7 -0
- package/server/api/assets/docx-page.js +5 -4
- package/server/api/assets/helpers.js +8 -0
- package/server/api/assets.js +21 -33
- package/server/api/board.js +29 -1
- package/server/api/browse.js +3 -2
- package/server/api/chatai.js +9 -8
- package/server/api/exports/cards.js +7 -6
- package/server/api/exports/docx-pdf.js +4 -1
- package/server/api/local.js +10 -8
- package/server/api/projects.js +19 -3
- package/server/api/publish.js +3 -2
- package/server/api/sessions-rewind.js +220 -0
- package/server/api/sessions.js +58 -194
- package/server/api/stage.js +35 -0
- package/server/api/turn-model-switch.js +109 -0
- package/server/api/turn.js +52 -88
- package/server/auth/middleware.js +29 -6
- package/server/auth/tier.js +27 -7
- package/server/auth/users-store.js +14 -1
- package/server/engine/agent/actor-trail.js +110 -0
- package/server/engine/agent/agent-shared.js +22 -52
- package/server/engine/agent/beat-state.js +61 -0
- package/server/engine/agent/cast.js +322 -0
- package/server/engine/agent/events.js +9 -2
- package/server/engine/agent/hooks/beat-gate.js +73 -0
- package/server/engine/agent/hooks/lifecycle.js +6 -2
- package/server/engine/agent/hooks/post-trim.js +1 -0
- package/server/engine/agent/hooks/pre-board-dirty.js +29 -0
- package/server/engine/agent/hooks/pre-defaults.js +206 -2
- package/server/engine/agent/hooks/pre-injectors.js +19 -2
- package/server/engine/agent/hooks/pre-peer-guard.js +88 -0
- package/server/engine/agent/hooks/pre-unknown-params.js +82 -0
- package/server/engine/agent/hooks/pre-workspace-scope-guard.js +33 -0
- package/server/engine/agent/hooks/resident-role-lifecycle.js +100 -0
- package/server/engine/agent/hooks/slot-alias.js +30 -0
- package/server/engine/agent/hooks/user-prompt-submit.js +197 -7
- package/server/engine/agent/hooks.js +76 -6
- package/server/engine/agent/isolation.js +25 -2
- package/server/engine/agent/memory-config.js +27 -1
- package/server/engine/agent/model-context.js +102 -18
- package/server/engine/agent/model-table.js +361 -58
- package/server/engine/agent/prompts/nodesign-prelude.md +566 -59
- package/server/engine/agent/prompts/tools/direct-edit-protocol.md +1 -1
- package/server/engine/agent/prompts/tools/generate-image-cookbook.md +2 -2
- package/server/engine/agent/prompts/tools/paint-still-cookbook.md +1 -1
- package/server/engine/agent/prompts/tools/site-reference.md +2 -1
- package/server/engine/agent/role-card.js +175 -0
- package/server/engine/agent/rp-mode.js +17 -0
- package/server/engine/agent/session-loop.js +26 -14
- package/server/engine/agent/stage-status.js +73 -0
- package/server/engine/agent/subagent-flight.js +73 -0
- package/server/engine/agent/system-prompts.js +41 -0
- package/server/engine/agent/tool-input-stream.js +165 -0
- package/server/engine/mcp/actor.js +59 -0
- package/server/engine/mcp/capability-gate.js +21 -2
- package/server/engine/mcp/index.js +116 -24
- package/server/engine/mcp/mode-profile.js +139 -0
- package/server/engine/mcp/server-name.js +19 -0
- package/server/engine/mcp/tools/artifact-session.js +2 -1
- package/server/engine/mcp/tools/browse-find-batch.js +76 -14
- package/server/engine/mcp/tools/browse.js +1 -1
- package/server/engine/mcp/tools/cast-role.js +167 -0
- package/server/engine/mcp/tools/draw-trend.js +104 -0
- package/server/engine/mcp/tools/edit-board-schema.js +56 -0
- package/server/engine/mcp/tools/edit-board.js +569 -0
- package/server/engine/mcp/tools/look-at-board.js +4 -3
- package/server/engine/mcp/tools/open-sheet.js +242 -0
- package/server/engine/mcp/tools/organize-board.js +19 -5
- package/server/engine/mcp/tools/paint-still.js +1 -1
- package/server/engine/mcp/tools/pin-to-board.js +78 -3
- package/server/engine/mcp/tools/read-board.js +109 -16
- package/server/engine/mcp/tools/read-tavern-json.js +87 -19
- package/server/engine/mcp/tools/report-issue.js +1 -0
- package/server/engine/mcp/tools/role-memory.js +83 -0
- package/server/engine/mcp/tools/roll-dice.js +82 -0
- package/server/engine/mcp/tools/roll-film.js +1 -1
- package/server/engine/mcp/tools/screenshot.js +2 -2
- package/server/engine/mcp/tools/set-vars.js +150 -0
- package/server/engine/mcp/tools/sheet-replan.js +46 -0
- package/server/engine/mcp/tools/web-search.js +1 -1
- package/server/engine/mcp/tools/write-on-board-flow.js +108 -0
- package/server/engine/mcp/tools/write-on-board-place.js +211 -0
- package/server/engine/mcp/tools/write-on-board-role-anchor.js +48 -0
- package/server/engine/mcp/tools/write-on-board-schema.js +125 -0
- package/server/engine/mcp/tools/write-on-board.js +567 -107
- package/server/engine/plugins/nodesign/skills/blackboard-rp/SKILL.md +215 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/SKILL.md +11 -3
- package/server/engine/plugins/nodesign/skills/docx-craft/SKILL.md +34 -4
- package/server/engine/plugins/nodesign/skills/site-craft/SKILL.md +20 -4
- package/server/engine/plugins/nodesign/skills/story-craft/SKILL.md +94 -0
- package/server/engine/plugins/nodesign/skills/story-import/SKILL.md +112 -0
- package/server/engine/plugins/nodesign/skills/story-intimacy/SKILL.md +72 -0
- package/server/engine/plugins/nodesign/skills/story-voice/SKILL.md +66 -0
- package/server/engine/plugins/nodesign/skills/story-voice/voices//345/206/267/347/241/254.md +33 -0
- package/server/engine/plugins/nodesign/skills/story-voice/voices//346/203/205/346/204/237/347/233/264/347/273/231.md +42 -0
- package/server/engine/plugins/nodesign/skills/story-voice/voices//346/204/217/350/257/206/346/265/201.md +42 -0
- package/server/engine/plugins/nodesign/skills/story-voice/voices//346/255/246/344/276/240/347/231/275/350/257/235.md +34 -0
- package/server/engine/plugins/nodesign/skills/story-voice/voices//347/224/237/346/264/273/346/265/201.md +35 -0
- package/server/engine/plugins/nodesign/skills/story-voice/voices//347/231/275/346/217/217.md +37 -0
- package/server/engine/plugins/nodesign/skills/story-voice/voices//350/210/236/345/217/260/345/211/247.md +32 -0
- package/server/engine/plugins/nodesign/skills/story-voice/voices//351/230/266/346/256/265/345/210/207/346/215/242.md +42 -0
- package/server/engine/runs/board-seater.js +272 -0
- package/server/engine/runs/board-tasklist.js +90 -48
- package/server/engine/runs/turn-relay.js +24 -2
- package/server/engine/runs/user-chalk-echo.js +44 -0
- package/server/index.js +2 -0
- package/server/lib/board-anchor.js +49 -0
- package/server/lib/board-dirty.js +93 -0
- package/server/lib/board-excerpt.js +80 -0
- package/server/lib/board-follow.js +111 -0
- package/server/lib/board-groups.js +1 -5
- package/server/lib/board-hero.js +3 -1
- package/server/lib/board-kind-sizes.js +36 -0
- package/server/lib/board-lanes.js +58 -0
- package/server/lib/board-obstacles.js +80 -0
- package/server/lib/board-relations.js +5 -1
- package/server/lib/board-sheets.js +467 -0
- package/server/lib/board-shelf.js +86 -0
- package/server/lib/board-transform.js +62 -0
- package/server/lib/canvas-id.js +18 -1
- package/server/lib/chalk-flow.js +88 -0
- package/server/lib/chalk-rewrite.js +63 -0
- package/server/lib/chalk-size-pref.js +48 -0
- package/server/lib/chalk.js +42 -3
- package/server/lib/ingress/forward-openai-chat.js +16 -4
- package/server/lib/ingress/openai-chat.js +23 -12
- package/server/lib/ingress/session-routes.js +3 -3
- package/server/lib/ingress/stream-watchdog.js +53 -0
- package/server/lib/ingress/upstream-billing.js +15 -0
- package/server/lib/ingress/upstream-error-hints.js +36 -0
- package/server/lib/json-preview.js +58 -0
- package/server/lib/model-ingress.js +26 -1
- package/server/lib/moderation.js +10 -5
- package/server/lib/quota.js +26 -6
- package/server/lib/rect.js +50 -0
- package/server/lib/screen.js +36 -0
- package/server/lib/sheet-state.js +19 -0
- package/server/lib/sketch-array.js +118 -0
- package/server/lib/sketch-hatch.js +99 -0
- package/server/lib/sketch-layout.js +269 -69
- package/server/lib/sketch-shapes.js +152 -0
- package/server/lib/sketch-stencils.js +170 -0
- package/server/lib/state-table.js +237 -0
- package/server/lib/state-trend.js +94 -0
- package/server/lib/state-triggers.js +199 -0
- package/server/lib/tavern-json.js +48 -0
- package/server/lib/user-uploads.js +88 -0
- package/server/projects/board-limits.js +5 -3
- package/server/projects/board-sanitize.js +220 -10
- package/server/projects/board-store.js +95 -6
- package/server/projects/session-jsonl.js +110 -0
- package/server/projects/store.js +56 -3
- package/server/projects/viewpoint-store.js +35 -3
- package/server/projects/workspace-slots.js +30 -0
- package/server/projects/workspace-templates.js +29 -0
- package/server/projects/workspace.js +16 -17
- package/server/runtime/capabilities.js +6 -2
- package/server/shared/locales.js +70 -0
- package/server/shared/messages-en.js +69 -0
- package/server/shared/messages.js +65 -0
- package/server/ws/broker.js +3 -0
- package/server/ws/index.js +43 -3
- package/web/README.md +1 -1
- package/web/dist/assets/{BrowserWindow-BoOBaIyz.js → BrowserWindow-CcaGRWz5.js} +1 -1
- package/web/dist/assets/DeckWindow-BIr2KGFT.js +6 -0
- package/web/dist/assets/{DocxWindow-B_u6aX9o.js → DocxWindow-B9KZmU5z.js} +2 -2
- package/web/dist/assets/{MermaidBlock-BO-46Du5.js → MermaidBlock-DrfRnz8E.js} +43 -43
- package/web/dist/assets/{SiteWindow-ConPmzdM.js → SiteWindow-CJOjCF7d.js} +6 -6
- package/web/dist/assets/{abnfDiagram-VCTEODGH-FCO9yf-n.js → abnfDiagram-VCTEODGH-CUZbQGIg.js} +1 -1
- package/web/dist/assets/{arc-BLcEOSRX.js → arc-C1-HSUny.js} +1 -1
- package/web/dist/assets/{architectureDiagram-5GKGNRK7-kxiX4TeN.js → architectureDiagram-5GKGNRK7-B5nwvu60.js} +2 -2
- package/web/dist/assets/{blockDiagram-NRAW4CY4-CptFksix.js → blockDiagram-NRAW4CY4-uPPuFLwR.js} +1 -1
- package/web/dist/assets/{c4Diagram-UCG6FXSJ-Dxmxj34F.js → c4Diagram-UCG6FXSJ-C0mKBFlz.js} +1 -1
- package/web/dist/assets/channel-BxpWNOGU.js +1 -0
- package/web/dist/assets/{chunk-2Q5K7J3B-D9inx5oo.js → chunk-2Q5K7J3B-Cs0rlZ4q.js} +1 -1
- package/web/dist/assets/{chunk-5VM5RSS4-DydfHlEc.js → chunk-5VM5RSS4-DtR63oYb.js} +1 -1
- package/web/dist/assets/{chunk-F27PBJKO-BO7GxhSv.js → chunk-F27PBJKO-B3MRdezJ.js} +1 -1
- package/web/dist/assets/{chunk-G27WJ6UU-BzP6YiQ_.js → chunk-G27WJ6UU-BtwntwpS.js} +1 -1
- package/web/dist/assets/{chunk-JWPE2WC7-CHcgcd0s.js → chunk-JWPE2WC7-BHsTe5PD.js} +1 -1
- package/web/dist/assets/{chunk-LCL6LL3I-D2Wg5JtX.js → chunk-LCL6LL3I-knY15Qdm.js} +1 -1
- package/web/dist/assets/{chunk-POPQ4Y6H-B2fCRfpM.js → chunk-POPQ4Y6H-C51tgK_I.js} +1 -1
- package/web/dist/assets/{chunk-SVP7TREG-JSLcsRlZ.js → chunk-SVP7TREG-CnwRZmF_.js} +1 -1
- package/web/dist/assets/{chunk-XXDRQBXY-BCjvaeHm.js → chunk-XXDRQBXY-BtXzVF-_.js} +1 -1
- package/web/dist/assets/classDiagram-DTDB5LWJ-DUEKmDfo.js +1 -0
- package/web/dist/assets/classDiagram-v2-JRS7N3AN-DUEKmDfo.js +1 -0
- package/web/dist/assets/{cose-bilkent-JH36ORCC-0uKv6oJ7.js → cose-bilkent-JH36ORCC-CzWlGP6i.js} +1 -1
- package/web/dist/assets/{cynefin-OW5HDTMX-Bvt-gEmA.js → cynefin-OW5HDTMX-vK64EGSv.js} +1 -1
- package/web/dist/assets/{cynefinDiagram-5FMLGOSQ-C9Ht02qC.js → cynefinDiagram-5FMLGOSQ-Bs3g1Gvh.js} +1 -1
- package/web/dist/assets/{dagre-3AP2YEHR-BfxZny4s.js → dagre-3AP2YEHR-BhgkYe8V.js} +1 -1
- package/web/dist/assets/{diagram-S7CK7UJ4-CrE0ZETE.js → diagram-S7CK7UJ4-C_6mvD2V.js} +1 -1
- package/web/dist/assets/{diagram-UQ7AKVKN-BO8VQ4Fx.js → diagram-UQ7AKVKN-qW7tssUW.js} +1 -1
- package/web/dist/assets/{diagram-VSXAHHWV-CHUZnR9F.js → diagram-VSXAHHWV-txJPHpvG.js} +1 -1
- package/web/dist/assets/{diagram-VX7I27RA-DYQAz1Vu.js → diagram-VX7I27RA-CQTMsCnL.js} +1 -1
- package/web/dist/assets/{diagram-Z3DM3KII-BDjivgLr.js → diagram-Z3DM3KII--cb1hoE6.js} +1 -1
- package/web/dist/assets/{ebnfDiagram-PWID7BFC-Bpe_QVbH.js → ebnfDiagram-PWID7BFC-Da6lDU77.js} +1 -1
- package/web/dist/assets/{erDiagram-SSCWMZ5O-BxhwuAu1.js → erDiagram-SSCWMZ5O-DGCMrsQn.js} +1 -1
- package/web/dist/assets/{flowDiagram-A5DVABFB-CCw2H08-.js → flowDiagram-A5DVABFB-GdOL_qno.js} +1 -1
- package/web/dist/assets/{ganttDiagram-EL5Y4UJY-DtTc7MyI.js → ganttDiagram-EL5Y4UJY-Cx1F_jBK.js} +4 -4
- package/web/dist/assets/{gitGraphDiagram-WWUBYQGX-BvORxMd2.js → gitGraphDiagram-WWUBYQGX-B5_6JhR3.js} +1 -1
- package/web/dist/assets/hand-CuvZZQGW.webp +0 -0
- package/web/dist/assets/index-BYKfMnmv.js +2759 -0
- package/web/dist/assets/index-CW7VsrVg.css +1 -0
- package/web/dist/assets/{infoDiagram-RXCK75RN-DMcrkypD.js → infoDiagram-RXCK75RN-CjvKpSPA.js} +1 -1
- package/web/dist/assets/{ishikawaDiagram-5VMMS53U-CZkTIbe-.js → ishikawaDiagram-5VMMS53U-CxLoAPhx.js} +1 -1
- package/web/dist/assets/{journeyDiagram-EYS64GPL-DR0XazdE.js → journeyDiagram-EYS64GPL-BfvS1upJ.js} +1 -1
- package/web/dist/assets/{kanban-definition-3QL26DDD-DN8VbwVu.js → kanban-definition-3QL26DDD-DKPPu0w4.js} +1 -1
- package/web/dist/assets/{layout-9_paCbyt.js → layout-Dhgck-Zo.js} +1 -1
- package/web/dist/assets/{linear-g6Ct012e.js → linear-DMSQ-aUw.js} +1 -1
- package/web/dist/assets/lxgw-nd-app-bold-D1pKA1bR.woff2 +0 -0
- package/web/dist/assets/lxgw-nd-app-regular-BDe3d26v.woff2 +0 -0
- package/web/dist/assets/lxgw-nd-bold-Bufvlg03.woff2 +0 -0
- package/web/dist/assets/lxgw-nd-regular-DQwoI4OP.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-100-DLPe7tEt.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-101-d3VsZKWT.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-102-ClzJuLq6.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-103-DuDnQ6K-.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-104-kPGW3Eem.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-105-BzD54h-0.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-106-BQ7fIzlx.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-107-D8tDfFHI.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-108-tGhkpwep.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-109-7SMVSrly.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-110-_uDKrFed.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-111-Bio825RE.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-112-D2ee0i-C.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-113-JPFVDrga.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-114-BcfxYA4w.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-115-B1HeeOsd.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-116-hY12tXpq.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-117-BvuZwmre.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-118-tzvs_H7j.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-119-J2DWuYDN.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-21-pDubCJV5.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-22-ChAPMpqp.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-23-BBx3oNpT.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-24-CYt-Oagk.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-25-Cx3j-ZUv.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-26-B9AfZ3LL.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-27-Bh2s9MNE.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-28-C02JPoKP.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-29-T_tbUGhk.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-30-DMfyNzhz.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-31-DFfdzbkg.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-32-Bs1fz2iW.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-33-cpB96OkZ.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-34-BhinPYWX.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-35-D_njRQqA.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-36-DqXlLE3t.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-37-j5LbvMph.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-38-oO4jWdKm.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-39-CabsX4rw.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-4-PatS3YHh.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-40-yrLg3C3e.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-41-DBXmCQhc.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-42-Bqij_UiQ.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-43-D3kp0Ntc.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-44-B_BNGvK4.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-45-CZc0cQWs.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-46-CHQX5Kd9.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-47-Ci8gLrHA.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-48-run3ugD3.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-49-DgfiUb6u.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-5-B0CtGbX_.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-50-CdCG-POr.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-51-L2okeQZ2.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-52-BZAqyGJW.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-53-B0xWqLeU.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-54-BZVQX-EA.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-55-093mkIUJ.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-56-CbhWG8G-.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-57-pLQuvo6c.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-58-BHhoY50x.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-59-D1_6DuIQ.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-6-BnAelmYU.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-60-HDBIuLTZ.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-61-C-c13SeU.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-62-kHRVOhZ2.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-63-C0MCsvFv.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-64-DZekguCj.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-65-CtP6c3IU.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-66-DLVWoMSf.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-67-BVuOnayH.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-68-DpxQ6Mnx.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-69-qesBxLmC.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-70-BBt1xN7h.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-71-BWqZacwB.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-72-B1wP_Yst.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-73-C99T7jNZ.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-74-DrUTsWBe.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-75-C4M5XXQe.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-76-Bq8spVWF.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-77-D9vNupE2.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-78-D6th7Gnu.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-79-bezM85cS.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-80-DD8pnhG2.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-81-N6jtSyBs.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-82-D6c180bZ.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-83-em17AadM.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-84-CJ8gdrMK.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-85-9PPGO1eB.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-86-CjWPf-u5.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-87-8D_X_yOC.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-88-9vtKFPBx.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-89-BhE5jAKX.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-90-D2Npfm5E.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-91-C6Ft1-Eu.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-97-CaExyQP9.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-98-CPoVbsUd.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-99-ZUQZGVgH.woff2 +0 -0
- package/web/dist/assets/{mindmap-definition-FBJOCRG2-BGt4omYC.js → mindmap-definition-FBJOCRG2-CalfL_Kn.js} +1 -1
- package/web/dist/assets/{pegDiagram-XKGWAZYB-uSfS-PBE.js → pegDiagram-XKGWAZYB-D9mVWz7l.js} +1 -1
- package/web/dist/assets/pending-edit-apply-kwMFWsUe.js +31 -0
- package/web/dist/assets/{pieDiagram-E7YTZNPT-DpeI935H.js → pieDiagram-E7YTZNPT-DELzbZNS.js} +1 -1
- package/web/dist/assets/{quadrantDiagram-AXDQQJYC-B7IuNvWa.js → quadrantDiagram-AXDQQJYC-TZqp4mfA.js} +1 -1
- package/web/dist/assets/{railroadDiagram-O6MQD6OU-ARzYpPQI.js → railroadDiagram-O6MQD6OU-DxX59f0i.js} +1 -1
- package/web/dist/assets/{requirementDiagram-EFPCY7ZU-Cj4upkAo.js → requirementDiagram-EFPCY7ZU-DFdsJLIe.js} +1 -1
- package/web/dist/assets/{rotate-cw-CXJcyHWk.js → rotate-cw-62liz7LX.js} +1 -1
- package/web/dist/assets/{sankeyDiagram-P5KCCOFB-OaVwSnLP.js → sankeyDiagram-P5KCCOFB-ByYcd3fG.js} +1 -1
- package/web/dist/assets/{sequenceDiagram-WJ2MYXX4-1yrUbq4t.js → sequenceDiagram-WJ2MYXX4-Bj-ml1jS.js} +1 -1
- package/web/dist/assets/{sizeCapture-X5ZJPWSS-nYH5p9D6.js → sizeCapture-X5ZJPWSS-KhsXThyD.js} +1 -1
- package/web/dist/assets/{square-dashed-mouse-pointer-CTtTVKoH.js → square-dashed-mouse-pointer-D_VrqVpy.js} +1 -1
- package/web/dist/assets/{stateDiagram-HBIQ2CUA-Dj5HvyNq.js → stateDiagram-HBIQ2CUA-BJlpHfWU.js} +1 -1
- package/web/dist/assets/stateDiagram-v2-4QOOHH4V-CR2kYAH2.js +1 -0
- package/web/dist/assets/{swimlanes-XN3QIQJK-umeT0HpB.js → swimlanes-XN3QIQJK-DUB9OJVs.js} +1 -1
- package/web/dist/assets/swimlanesDiagram-VK2B7HYN-BitOxnG2.js +8 -0
- package/web/dist/assets/{timeline-definition-24CTP7MA-MqnQfHKB.js → timeline-definition-24CTP7MA-C03NX82A.js} +1 -1
- package/web/dist/assets/{vennDiagram-4TSXK5OY-b2liyZra.js → vennDiagram-4TSXK5OY-B9pjzngp.js} +1 -1
- package/web/dist/assets/{wardleyDiagram-VM6X3IG4-QMK-zNh1.js → wardleyDiagram-VM6X3IG4-UqWq_mcT.js} +1 -1
- package/web/dist/assets/{xychartDiagram-S5SC5T6Z-D5Mc6tfA.js → xychartDiagram-S5SC5T6Z-D2pDKtOv.js} +1 -1
- package/web/dist/index.html +2 -2
- package/server/engine/mcp/tools/arrange-on-board.js +0 -126
- package/server/engine/mcp/tools/create-on-board.js +0 -152
- package/server/engine/mcp/tools/edit-sketch.js +0 -167
- package/server/engine/mcp/tools/relate-on-board.js +0 -150
- package/server/engine/mcp/tools/sketch-on-board.js +0 -345
- package/server/engine/plugins/nodesign/skills/rp-craft/SKILL.md +0 -235
- 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 +0 -78
- package/server/engine/plugins/nodesign/skills/rp-craft//346/274/224/345/207/272.template.js +0 -268
- package/web/dist/assets/DeckWindow-Bg3rgkvi.js +0 -11
- package/web/dist/assets/channel-DFyiuGL0.js +0 -1
- package/web/dist/assets/classDiagram-DTDB5LWJ-Aw1f8z-b.js +0 -1
- package/web/dist/assets/classDiagram-v2-JRS7N3AN-Aw1f8z-b.js +0 -1
- package/web/dist/assets/index-C0KUf6Wd.css +0 -1
- package/web/dist/assets/index-DGG5wIZs.js +0 -2118
- 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-uP4uvrOD.js +0 -31
- package/web/dist/assets/stateDiagram-v2-4QOOHH4V-kJ1m_zJT.js +0 -1
- package/web/dist/assets/swimlanesDiagram-VK2B7HYN-DPSkMbbl.js +0 -8
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* server/lib/state-triggers.js —— 状态表的条件触发器(2026-08-30)
|
|
3
|
+
*
|
|
4
|
+
* 「把会变的东西写在板上,设条件,需要的时候被动拿回来」的后半截。
|
|
5
|
+
* 前半截(值住在一张 markdown 表里)见 state-table.js。
|
|
6
|
+
*
|
|
7
|
+
* ## 声明住在正文里,不住在第二个存储里
|
|
8
|
+
*
|
|
9
|
+
* 触发器写成一段 ```nd:triggers 围栏,就在状态表那条板书的正文里 ——
|
|
10
|
+
* 跟 `nd:controls`(画布控件的声明)是同一个范式:**机器可读的声明住在用户看得见、
|
|
11
|
+
* 编得动的散文里**。这样用户能看到「现在挂着哪些条件」,也能自己删掉一条。
|
|
12
|
+
*
|
|
13
|
+
* 语法(一行一条,故意窄):
|
|
14
|
+
*
|
|
15
|
+
* ```nd:triggers
|
|
16
|
+
* - [好感度_苏绵 >= 5] once -> 她开始主动找你说话了,这一拍给她一个开口的机会
|
|
17
|
+
* - [体力 <= 2 && 时间 == 夜] on_cross -> 体力见底又是夜里:这一拍必须有代价
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* `&&` 是唯一的连接词(全都成立才算命中)。没有 `||`、没有括号 —— 那两样一进来
|
|
21
|
+
* 就得写真正的表达式解析器,而**模型拼错一个变量名时静默求值成 false** 是这条线
|
|
22
|
+
* 最怕的那种失效。窄语法换来的是:每一种写错都能当场指出来。
|
|
23
|
+
*
|
|
24
|
+
* ## 触发的是「穿越」,不是「为真」
|
|
25
|
+
*
|
|
26
|
+
* `好感度 >= 5` 一旦满足,之后每一拍都满足。按「为真就触发」写,它会每拍注一遍,
|
|
27
|
+
* 上线当天就变成噪音源。所以只有两档:
|
|
28
|
+
* - `once` 命中一次就退休(一次性的剧情钩子)
|
|
29
|
+
* - `on_cross` 每次**从假变真**才触发(会来回摆的条件,比如昼夜、体力)
|
|
30
|
+
*
|
|
31
|
+
* ## 沿状态(上一次是真是假)放 `.nd/`,这次是对的
|
|
32
|
+
*
|
|
33
|
+
* `.nd/` 在 gitignore 里,所以**玩法真相不能放那儿**(那是 state-table.js 头注
|
|
34
|
+
* 否掉 `.nd/vars.json` 的理由)。但沿状态是**派生记账**:丢了的代价只是一次
|
|
35
|
+
* 「上膛不击发」,不是丢故事状态。语义写死:
|
|
36
|
+
* - 文件缺失 / 解析失败 → 按当前值重新上膛、**不击发**、log 一行
|
|
37
|
+
* - `revertWorkspace` 不回滚它(gitignore),后果是回滚后已触发过的不会重触发 —— 可接受
|
|
38
|
+
*
|
|
39
|
+
* ## 求值点 = 注入点
|
|
40
|
+
*
|
|
41
|
+
* 只在 UserPromptSubmit 里求值(读盘上的表 + 盘上的沿状态,命中就写进这一轮的
|
|
42
|
+
* 状态块)。不在 set_vars 里求值攒到下一轮 —— 那样中间一次进程重启就静默吞掉一次
|
|
43
|
+
* 触发,正是「哑掉的机制不报错」那族病。
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
import path from 'node:path';
|
|
47
|
+
import { promises as fs } from 'node:fs';
|
|
48
|
+
import { fingerprint } from '../engine/agent/hooks/turn-state-memory.js';
|
|
49
|
+
|
|
50
|
+
const FENCE_RE = /^[ \t]*```[ \t]*nd:triggers[ \t]*$/;
|
|
51
|
+
const FENCE_END_RE = /^[ \t]*```[ \t]*$/;
|
|
52
|
+
/** - [<条件>] <mode> -> <话> */
|
|
53
|
+
const LINE_RE = /^\s*-\s*\[([^\]]+)\]\s*(once|on_cross)\s*->\s*(.+?)\s*$/;
|
|
54
|
+
const OPS = ['>=', '<=', '!=', '==', '>', '<'];
|
|
55
|
+
|
|
56
|
+
export const LATCH_REL = '.nd/vars-latch.json';
|
|
57
|
+
/** 一条触发器的话最多这么长:它是注给模型的一句提醒,不是一段剧本 */
|
|
58
|
+
export const MESSAGE_MAX = 200;
|
|
59
|
+
export const MAX_TRIGGERS = 24;
|
|
60
|
+
|
|
61
|
+
/** 把一条条件文本拆成若干个比较(`&&` 连接,全都成立才算命中) */
|
|
62
|
+
function parseCond(text) {
|
|
63
|
+
const parts = String(text).split('&&').map((p) => p.trim()).filter(Boolean);
|
|
64
|
+
if (!parts.length) return { error: '条件是空的' };
|
|
65
|
+
const cmps = [];
|
|
66
|
+
for (const p of parts) {
|
|
67
|
+
const op = OPS.find((o) => p.includes(o));
|
|
68
|
+
if (!op) return { error: `「${p}」里没有比较符(用 >= <= > < == != 其中之一)` };
|
|
69
|
+
const idx = p.indexOf(op);
|
|
70
|
+
const key = p.slice(0, idx).trim();
|
|
71
|
+
const value = p.slice(idx + op.length).trim();
|
|
72
|
+
if (!key) return { error: `「${p}」缺左边的键名` };
|
|
73
|
+
if (!value) return { error: `「${p}」缺右边的值` };
|
|
74
|
+
cmps.push({ key, op, value });
|
|
75
|
+
}
|
|
76
|
+
return { cmps };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 从正文里解析 ```nd:triggers 围栏。
|
|
81
|
+
* @returns {{ triggers: Array<{id,cmps,mode,message,raw}>, errors: string[] }}
|
|
82
|
+
* errors 里每条都指得出是哪一行错在哪 —— 写错要能被看见,不能静默丢。
|
|
83
|
+
*/
|
|
84
|
+
export function parseTriggers(body) {
|
|
85
|
+
const lines = String(body || '').split(/\r?\n/);
|
|
86
|
+
const triggers = []; const errors = [];
|
|
87
|
+
let inFence = false;
|
|
88
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
89
|
+
const l = lines[i];
|
|
90
|
+
if (!inFence) { if (FENCE_RE.test(l)) inFence = true; continue; }
|
|
91
|
+
if (FENCE_END_RE.test(l)) { inFence = false; continue; }
|
|
92
|
+
if (!l.trim()) continue;
|
|
93
|
+
const m = LINE_RE.exec(l);
|
|
94
|
+
if (!m) {
|
|
95
|
+
errors.push(`第 ${i + 1} 行看不懂:${l.trim().slice(0, 60)}\n (格式是 \`- [键 >= 值] once -> 要说的话\`,mode 只能是 once 或 on_cross)`);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const [, condText, mode, messageRaw] = m;
|
|
99
|
+
const { cmps, error } = parseCond(condText);
|
|
100
|
+
if (error) { errors.push(`第 ${i + 1} 行条件写错了:${error}`); continue; }
|
|
101
|
+
const message = messageRaw.slice(0, MESSAGE_MAX);
|
|
102
|
+
// id 只由「条件 + 档」决定:改那句提醒的措辞不该把沿状态重置掉
|
|
103
|
+
const id = fingerprint(`${cmps.map((c) => `${c.key}${c.op}${c.value}`).join('&&')}|${mode}`);
|
|
104
|
+
triggers.push({ id, cmps, mode, message, raw: l.trim() });
|
|
105
|
+
if (triggers.length >= MAX_TRIGGERS) {
|
|
106
|
+
errors.push(`触发器超过 ${MAX_TRIGGERS} 条,后面的没读 —— 挂太多等于没挂。`);
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (inFence) errors.push('```nd:triggers 围栏没闭合,后面的行都没读。');
|
|
111
|
+
return { triggers, errors };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const NUM_RE = /^-?\d+(\.\d+)?$/;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 求一个比较的值。
|
|
118
|
+
* @returns {{ v:boolean } | { error:string }}
|
|
119
|
+
*
|
|
120
|
+
* ⛔ 跨类型的大小比较**大声拒绝**,不隐式转换:`时间 >= 戌时` 按字典序比出来的
|
|
121
|
+
* 结果看着像那么回事,但它不是任何人想要的语义。钳住但要说 —— 这是 B+ 没有类型
|
|
122
|
+
* 系统换来的代价,只能在求值时付。
|
|
123
|
+
*/
|
|
124
|
+
function compare(cmp, rows) {
|
|
125
|
+
const row = rows.find((r) => r.key === cmp.key);
|
|
126
|
+
if (!row) return { error: `表里没有「${cmp.key}」这个键` };
|
|
127
|
+
const l = String(row.value).trim(); const r = String(cmp.value).trim();
|
|
128
|
+
const bothNum = NUM_RE.test(l) && NUM_RE.test(r);
|
|
129
|
+
if (cmp.op === '==') return { v: bothNum ? Number(l) === Number(r) : l === r };
|
|
130
|
+
if (cmp.op === '!=') return { v: bothNum ? Number(l) !== Number(r) : l !== r };
|
|
131
|
+
if (!bothNum) {
|
|
132
|
+
return { error: `「${cmp.key} ${cmp.op} ${cmp.value}」比不了:${NUM_RE.test(l) ? '右边' : '左边'}不是数字(大小比较只对数字生效;想比文字用 == 或 !=)` };
|
|
133
|
+
}
|
|
134
|
+
const a = Number(l); const b = Number(r);
|
|
135
|
+
if (cmp.op === '>=') return { v: a >= b };
|
|
136
|
+
if (cmp.op === '<=') return { v: a <= b };
|
|
137
|
+
if (cmp.op === '>') return { v: a > b };
|
|
138
|
+
return { v: a < b };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* 求值一批触发器。**纯函数** —— 落盘由调用方做,好测。
|
|
143
|
+
*
|
|
144
|
+
* @param triggers parseTriggers 的产物
|
|
145
|
+
* @param rows 状态表的现值
|
|
146
|
+
* @param latch 上一次的沿状态 { [id]: { last:boolean, fired:number } }
|
|
147
|
+
* @param opts.fresh true = 这是本项目第一次求值(沿状态文件不存在/坏了)
|
|
148
|
+
* → **上膛不击发**:按当前值记下来,一条都不触发
|
|
149
|
+
* @returns {{ fired:Array<{id,message,raw}>, latch:object, errors:string[], armed:number, retired:number }}
|
|
150
|
+
*/
|
|
151
|
+
export function evalTriggers(triggers, rows, latch = {}, { fresh = false } = {}) {
|
|
152
|
+
const next = {}; const fired = []; const errors = [];
|
|
153
|
+
let armed = 0; let retired = 0;
|
|
154
|
+
for (const t of triggers) {
|
|
155
|
+
const prev = latch[t.id] || null;
|
|
156
|
+
const firedCount = prev?.fired || 0;
|
|
157
|
+
if (t.mode === 'once' && firedCount > 0) { next[t.id] = prev; retired += 1; continue; }
|
|
158
|
+
|
|
159
|
+
let v = true; let bad = null;
|
|
160
|
+
for (const c of t.cmps) {
|
|
161
|
+
const r = compare(c, rows);
|
|
162
|
+
if (r.error) { bad = r.error; break; }
|
|
163
|
+
if (!r.v) { v = false; break; }
|
|
164
|
+
}
|
|
165
|
+
if (bad) {
|
|
166
|
+
errors.push(`${t.raw}\n → ${bad}`);
|
|
167
|
+
next[t.id] = prev || { last: false, fired: firedCount }; // 求不出来不动沿状态
|
|
168
|
+
armed += 1;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const was = prev ? !!prev.last : false;
|
|
173
|
+
// 上膛不击发:第一次见到这条触发器(或沿状态丢了)只记录,不触发 ——
|
|
174
|
+
// 否则每次进程重启都会把所有当前为真的条件重放一遍
|
|
175
|
+
const shouldFire = !fresh && !!prev && v && !was;
|
|
176
|
+
if (shouldFire) fired.push({ id: t.id, message: t.message, raw: t.raw });
|
|
177
|
+
next[t.id] = { last: v, fired: firedCount + (shouldFire ? 1 : 0) };
|
|
178
|
+
if (t.mode === 'once' && next[t.id].fired > 0) retired += 1; else armed += 1;
|
|
179
|
+
}
|
|
180
|
+
return { fired, latch: next, errors, armed, retired };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export async function readLatch(sharedRoot) {
|
|
184
|
+
try {
|
|
185
|
+
const raw = await fs.readFile(path.join(sharedRoot, LATCH_REL), 'utf8');
|
|
186
|
+
const j = JSON.parse(raw);
|
|
187
|
+
if (!j || typeof j !== 'object') return { latch: {}, fresh: true };
|
|
188
|
+
return { latch: j, fresh: false };
|
|
189
|
+
} catch {
|
|
190
|
+
// 缺失或坏了都走 fresh:按当前值重新上膛、不击发
|
|
191
|
+
return { latch: {}, fresh: true };
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export async function writeLatch(sharedRoot, latch) {
|
|
196
|
+
const abs = path.join(sharedRoot, LATCH_REL);
|
|
197
|
+
await fs.mkdir(path.dirname(abs), { recursive: true });
|
|
198
|
+
await fs.writeFile(abs, JSON.stringify(latch, null, 2), 'utf8');
|
|
199
|
+
}
|
|
@@ -152,3 +152,51 @@ export function fetchEntries(doc, 选) {
|
|
|
152
152
|
}
|
|
153
153
|
return 出;
|
|
154
154
|
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* PNG 角色卡(2026-08-25,四方世界卡案:iss 上报「不是合法 JSON:�PNG」)。
|
|
158
|
+
* 酒馆的卡就是一张 PNG,角色数据 base64 后塞在 tEXt 块里:V3 用键 `ccv3`,
|
|
159
|
+
* V2 用键 `chara`。两个都在时 ccv3 赢(V3 是超集)。不是卡的普通 PNG 返回 null。
|
|
160
|
+
*/
|
|
161
|
+
export function extractCardFromPng(buf) {
|
|
162
|
+
if (!Buffer.isBuffer(buf) || buf.length < 16) return null;
|
|
163
|
+
if (buf.readUInt32BE(0) !== 0x89504e47) return null; // \x89PNG
|
|
164
|
+
let off = 8;
|
|
165
|
+
const found = {};
|
|
166
|
+
while (off + 12 <= buf.length) {
|
|
167
|
+
const len = buf.readUInt32BE(off);
|
|
168
|
+
const type = buf.toString('latin1', off + 4, off + 8);
|
|
169
|
+
if (type === 'tEXt' && len > 0 && off + 8 + len <= buf.length) {
|
|
170
|
+
const data = buf.subarray(off + 8, off + 8 + len);
|
|
171
|
+
const nul = data.indexOf(0);
|
|
172
|
+
if (nul > 0) {
|
|
173
|
+
const key = data.toString('latin1', 0, nul);
|
|
174
|
+
if (key === 'ccv3' || key === 'chara') found[key] = data.subarray(nul + 1).toString('latin1');
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
off += 12 + len;
|
|
178
|
+
if (type === 'IEND') break;
|
|
179
|
+
}
|
|
180
|
+
const payload = found.ccv3 || found.chara;
|
|
181
|
+
if (!payload) return null;
|
|
182
|
+
try { return JSON.parse(Buffer.from(payload, 'base64').toString('utf8')); } catch { return null; }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* 世界书条目全量(带正文)——给 export_book 机械搬运用。判断归 agent(常驻挑选、
|
|
187
|
+
* 引擎条目丢弃),搬运归机器(375 条 61.8 万字符不该流经上下文)。
|
|
188
|
+
*/
|
|
189
|
+
export function listBookEntries(doc) {
|
|
190
|
+
const kind = detectKind(doc);
|
|
191
|
+
const book = kind === 'lorebook' ? doc : (doc?.data?.character_book || doc?.character_book);
|
|
192
|
+
if (!book) return [];
|
|
193
|
+
const list = Array.isArray(book.entries) ? book.entries : Object.values(book.entries || {});
|
|
194
|
+
return list.map((e, i) => ({
|
|
195
|
+
id: String(e.uid ?? e.id ?? i),
|
|
196
|
+
名字: e.comment || e.name || (Array.isArray(e.key) ? e.key.join('/') : '') || `条目${i + 1}`,
|
|
197
|
+
触发: Array.isArray(e.key) ? e.key : (Array.isArray(e.keys) ? e.keys : []),
|
|
198
|
+
常驻: !!(e.constant),
|
|
199
|
+
停用: e.enabled === false || e.disable === true,
|
|
200
|
+
正文: String(e.content || ''),
|
|
201
|
+
}));
|
|
202
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/user-uploads.js —— 用户上传件的落点与增删查(2026-08-28)
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么单独挪出来
|
|
5
|
+
*
|
|
6
|
+
* 以前上传一律落 `shared/assets/`,而 `assets/` 在画布的文件夹清单里是**基础设施
|
|
7
|
+
* 目录**(RESERVED_DIRS / NOT_A_FOLDER),于是每个上传件都 homeOf 到根 —— 用户拖
|
|
8
|
+
* 一张参考图进来,它就直接摊在桌面上跟产物混作一堆。用户原话:「有一件事很膈应」。
|
|
9
|
+
*
|
|
10
|
+
* ## 修法:给它一个真目录,不是在画布上造一个虚拟分组
|
|
11
|
+
*
|
|
12
|
+
* 文件夹在这套体系里从来就等于磁盘上的真目录(见 api/assets.js 的收纳文件夹注释)。
|
|
13
|
+
* 凭空多一种只存在于画布上的分组,「文件系统即真相」这条就破了。真目录还白拿了
|
|
14
|
+
* 整套现成能力:文件夹卡、拖进拖出、改名、MoveTo、agent 的 mv。
|
|
15
|
+
*
|
|
16
|
+
* ⚠️ 老项目 `assets/` 里已有的上传件**不迁**:物件 id 就是路径,搬家等于改 id,
|
|
17
|
+
* 得连 board.json 绑定一起转发。它们照旧扫成 upload 摊在根上 —— 所以这里的列举和
|
|
18
|
+
* 删除都**两个落点都认**,新的优先。
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import fs from 'fs/promises';
|
|
22
|
+
import path from 'path';
|
|
23
|
+
|
|
24
|
+
/** 上传落点(工作区相对)。画布上就显示成这个名字的文件夹 */
|
|
25
|
+
export const USER_UPLOAD_DIR = '用户内容';
|
|
26
|
+
|
|
27
|
+
/** 两个落点,新的在前 —— 列举顺序、删除尝试顺序都按它 */
|
|
28
|
+
const LOOKUP_DIRS = [USER_UPLOAD_DIR, 'assets'];
|
|
29
|
+
|
|
30
|
+
/** 上传写到哪(不存在就建) */
|
|
31
|
+
export async function ensureUploadDir(sharedRoot) {
|
|
32
|
+
const dir = path.join(sharedRoot, USER_UPLOAD_DIR);
|
|
33
|
+
await fs.mkdir(dir, { recursive: true });
|
|
34
|
+
return dir;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** agent / 前端拿到的引用路径。cwd 是 sessions/<sid>/,所以 `../../shared/<dir>/<name>` */
|
|
38
|
+
export function uploadRefPath(dirRel, filename) {
|
|
39
|
+
return `../../shared/${dirRel}/${filename}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 列两个落点下的上传件(只看顶层文件,子目录归画布的文件夹机制管)。
|
|
44
|
+
* @returns {Promise<Array<{path, name, dir, size, mtime}>>} 按 mtime 新→旧
|
|
45
|
+
*/
|
|
46
|
+
export async function listUploadedAssets(sharedRoot) {
|
|
47
|
+
const out = [];
|
|
48
|
+
for (const dirRel of LOOKUP_DIRS) {
|
|
49
|
+
let entries;
|
|
50
|
+
try {
|
|
51
|
+
entries = await fs.readdir(path.join(sharedRoot, dirRel), { withFileTypes: true });
|
|
52
|
+
} catch (err) {
|
|
53
|
+
if (err.code === 'ENOENT') continue;
|
|
54
|
+
throw err;
|
|
55
|
+
}
|
|
56
|
+
for (const e of entries) {
|
|
57
|
+
if (!e.isFile()) continue;
|
|
58
|
+
let stat;
|
|
59
|
+
try { stat = await fs.stat(path.join(sharedRoot, dirRel, e.name)); } catch { continue; }
|
|
60
|
+
out.push({
|
|
61
|
+
path: uploadRefPath(dirRel, e.name), name: e.name, dir: dirRel,
|
|
62
|
+
size: stat.size, mtime: stat.mtime.toISOString(),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
out.sort((a, b) => (a.mtime < b.mtime ? 1 : -1));
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 删一个上传件。两个落点依次试,**每个目录各自 resolve 复核**(别拿一个目录的
|
|
72
|
+
* 判据去删另一个目录的文件)。
|
|
73
|
+
* @returns {Promise<'deleted'|'not_found'|'invalid'>}
|
|
74
|
+
*/
|
|
75
|
+
export async function deleteUploadedAsset(sharedRoot, filename) {
|
|
76
|
+
for (const dirRel of LOOKUP_DIRS) {
|
|
77
|
+
const dirAbs = path.join(sharedRoot, dirRel);
|
|
78
|
+
const filePath = path.resolve(dirAbs, filename);
|
|
79
|
+
if (!filePath.startsWith(dirAbs + path.sep)) return 'invalid';
|
|
80
|
+
try {
|
|
81
|
+
await fs.unlink(filePath);
|
|
82
|
+
return 'deleted';
|
|
83
|
+
} catch (err) {
|
|
84
|
+
if (err.code !== 'ENOENT') throw err;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return 'not_found';
|
|
88
|
+
}
|
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
* board-store(写盘前查字节数)和 board-sanitize(逐类计数)都要,单独一份免循环依赖。
|
|
4
4
|
*/
|
|
5
5
|
export const DEFAULT_BOARD_SIZE = { w: 4000, h: 2600 };
|
|
6
|
-
export const MAX_BOARD_BYTES =
|
|
7
|
-
export const MAX_OBJECTS =
|
|
6
|
+
export const MAX_BOARD_BYTES = 2 * 1024 * 1024; // 08-25 放大(板是整读整写 JSON,这是失控兜底不是风格闸)
|
|
7
|
+
export const MAX_OBJECTS = 8000;
|
|
8
8
|
export const MAX_ZONES = 200;
|
|
9
9
|
// 关系线上限。取值理由:一块板上人能看懂的线远少于这个数,1000 是防脱缰
|
|
10
10
|
// (agent 循环里连画)的闸门,不是设计目标。超了直接不收,不做淘汰 ——
|
|
11
11
|
// 静默丢最旧的会让"我明明画了"变成玄学。
|
|
12
|
-
export const MAX_BINDINGS =
|
|
12
|
+
export const MAX_BINDINGS = 4000;
|
|
13
|
+
export const MAX_LANES = 60; // 线(lane 注册表):几十条线的脑图已是极限读感
|
|
14
|
+
export const MAX_SHEETS = 200; // 纸(sheet 注册表,2026-08-29 纸范式):一纸一屏,200 张已是长篇连载量级
|
|
@@ -6,12 +6,29 @@
|
|
|
6
6
|
* 行数棘轮,语义一个字没改。
|
|
7
7
|
*
|
|
8
8
|
* schema 见 board-store 头注释。黑板字段(2026-08-23):
|
|
9
|
-
* object.tag / object.staging / text.data.format
|
|
9
|
+
* object.tag / object.staging / text.data.format / text.data.lid
|
|
10
10
|
* binding.material / binding.tag / binding.staging
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { isBindingType, isBindingMaterial } from '../lib/binding-types.js';
|
|
14
|
-
import {
|
|
14
|
+
import { ROLE_SLUG_RE } from '../engine/agent/cast.js';
|
|
15
|
+
import { DEFAULT_BOARD_SIZE, MAX_OBJECTS, MAX_ZONES, MAX_BINDINGS, MAX_LANES, MAX_SHEETS } from './board-limits.js';
|
|
16
|
+
import { ONE_SCREEN } from '../lib/screen.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 板上署名的白名单:'user' / 'agent' / 常驻角色 slug(`rp-*`,见 engine/agent/cast.js)。
|
|
20
|
+
* 认不出的一律丢掉(落回无归属),**不透传** —— 这个值由模型写,而它的读者有一串:
|
|
21
|
+
* 前端标题与线标签、read_board 的分段、板书注入、将来的收件箱路由。
|
|
22
|
+
*
|
|
23
|
+
* ⚠️ **有东西寄生在这个值域上**:`lib/board-hero.js` 与 `web/src/lib/hero.js`(parity 对)
|
|
24
|
+
* 把「手画的线」判成 `by && by !== 'auto'` —— 那是个黑名单,只在「值域被这里钉死」的
|
|
25
|
+
* 前提下才与白名单等价。放宽这里的值域前,先去看那两处。
|
|
26
|
+
*/
|
|
27
|
+
function sanitizeBy(v) {
|
|
28
|
+
const s = typeof v === 'string' ? v.trim() : '';
|
|
29
|
+
if (s === 'user' || s === 'agent') return s;
|
|
30
|
+
return ROLE_SLUG_RE.test(s) ? s : null;
|
|
31
|
+
}
|
|
15
32
|
|
|
16
33
|
export function clampNum(v, min, max, fallback) {
|
|
17
34
|
const n = Number(v);
|
|
@@ -48,19 +65,19 @@ const CANVAS_NATIVE_KINDS = new Set(['scribble', 'text']);
|
|
|
48
65
|
const COORD_LIMIT = 1e6;
|
|
49
66
|
|
|
50
67
|
/** 涂鸦路径串上限。一条随手画的线约 300~800 字符,8000 够长且撑不爆 board.json */
|
|
51
|
-
const MAX_SCRIBBLE_PATH =
|
|
68
|
+
const MAX_SCRIBBLE_PATH = 24000;
|
|
52
69
|
/**
|
|
53
70
|
* 画布文字的字数上限。
|
|
54
71
|
*
|
|
55
72
|
* 它是"写在白板上的一句话",不是文档 —— 长东西该写成 .md(那是便利贴,
|
|
56
73
|
* agent 读得到)。2000 字够写一段说明,也撑不爆 board.json。
|
|
57
74
|
*/
|
|
58
|
-
const MAX_TEXT_LEN =
|
|
75
|
+
const MAX_TEXT_LEN = 8000;
|
|
59
76
|
/**
|
|
60
77
|
* md 档文字的上限(2026-08-23 黑板)。画布上的 markdown/KaTeX/mermaid 节点比手写
|
|
61
78
|
* 一句话长,但它仍是"黑板上的一块"不是文档 —— 想写长的走 .md。
|
|
62
79
|
*/
|
|
63
|
-
const MAX_MD_TEXT_LEN =
|
|
80
|
+
const MAX_MD_TEXT_LEN = 8000;
|
|
64
81
|
/** 文字的排版格式:plain = 原样手写;md = Markdown(含 KaTeX 与 mermaid 围栏) */
|
|
65
82
|
const TEXT_FORMATS = ['plain', 'md'];
|
|
66
83
|
/**
|
|
@@ -83,6 +100,7 @@ export function isSafeCanvasId(id) {
|
|
|
83
100
|
return !p.split('/').some(seg => seg === '..');
|
|
84
101
|
}
|
|
85
102
|
|
|
103
|
+
export { TAG_RE };
|
|
86
104
|
export function sanitizeTag(v) {
|
|
87
105
|
return typeof v === 'string' && TAG_RE.test(v) ? v : null;
|
|
88
106
|
}
|
|
@@ -117,6 +135,10 @@ function sanitizeCanvasData(kind, data) {
|
|
|
117
135
|
font: TEXT_FONTS.includes(data?.font) ? data.font : 'kai',
|
|
118
136
|
size: TEXT_SIZES.includes(data?.size) ? data.size : 'md',
|
|
119
137
|
color: ['ink', 'red', 'pencil', 'brass'].includes(data?.color) ? data.color : 'ink',
|
|
138
|
+
// lid = sketch_on_board 里那个局部 id(linfan / zhangwei…)。留着它,
|
|
139
|
+
// edit_sketch 才能按 agent 当初起的名字找回这个节点 —— 否则局部 id 一落定
|
|
140
|
+
// 就作废,加条线得先去翻上一次调用的返回。这层是白名单重建,不列就丢。
|
|
141
|
+
...(typeof data?.lid === 'string' && /^[A-Za-z0-9_-]{1,24}$/.test(data.lid) ? { lid: data.lid } : {}),
|
|
120
142
|
...sanitizeTransform(data),
|
|
121
143
|
};
|
|
122
144
|
}
|
|
@@ -151,9 +173,31 @@ export function sanitizeObject(o, size) {
|
|
|
151
173
|
...(o.expanded ? { expanded: true } : {}),
|
|
152
174
|
// 显式归属:'' = 明确无归属(覆盖 sid 派生),非空 = 所属工作区 id
|
|
153
175
|
...(typeof o.zone === 'string' && o.zone.length <= 300 ? { zone: o.zone } : {}),
|
|
154
|
-
// 出处(2026-08-14 agent
|
|
155
|
-
//
|
|
156
|
-
|
|
176
|
+
// 出处(2026-08-14 agent 摆位/建元素):谁**造**的这件东西。08-25 起也收
|
|
177
|
+
// 'user'(原来只认 agent,跟 bindings 三值不对称是口径病);08-26 起再收
|
|
178
|
+
// 常驻角色的 slug(rp-*)—— RP 场里板上大半东西是角色写的,全落回 'agent'
|
|
179
|
+
// 就丢了归属。⚠️ 这里是白名单,不是透传:写这个值的是模型,而 by 有一串读者
|
|
180
|
+
// (前端显示 / read_board 分段 / 板书注入 / 将来的收件箱路由)。
|
|
181
|
+
...(sanitizeBy(o.by) ? { by: sanitizeBy(o.by) } : {}),
|
|
182
|
+
// 座位出处(2026-08-25 范式重做):谁**摆**的这个座。三值:
|
|
183
|
+
// user = 用户亲手拖的 —— 出处记号+学习票源(inferFlowDir 认它学摆放方向)。
|
|
184
|
+
// 08-28 起**不再是禁令**(用户拍板全放开):move/reflow/组操作都挪得动,
|
|
185
|
+
// 但工具返回必须注明"原是用户亲手摆的",且他拖回去就是定案。
|
|
186
|
+
// auto = 入座算法排的 —— 可以被重排
|
|
187
|
+
// agent = agent 显式摆的 —— 用户可拖走(拖走后变 user)
|
|
188
|
+
// 没有这个字段,用户拖的和自动排的在数据上分不清
|
|
189
|
+
// (08-25 体检结论:老数据 by 被前端回写抹掉)。
|
|
190
|
+
// shelf = 暂存架上等安置(2026-08-30)—— 机器到货默认座。pin_to_board /
|
|
191
|
+
// edit_board move / 用户拖拽一改写 seat 就自然离架(lib/board-shelf.js)
|
|
192
|
+
...(o.seat === 'user' || o.seat === 'auto' || o.seat === 'agent' || o.seat === 'shelf' ? { seat: o.seat } : {}),
|
|
193
|
+
// 尺寸出处(2026-08-28):'user' = 这块板书的宽高是用户亲手拖出来的。
|
|
194
|
+
// 两个用途:① 重排/估算别拿内容宽度盖掉他调过的宽 ② 写入端拿它当**学习票源**
|
|
195
|
+
// 推断"他喜欢多宽的板书"(lib/chalk-size-pref.js)。跟 seat 是两件事 ——
|
|
196
|
+
// seat 说"谁摆的位置",sized 说"谁定的大小",用户可以只调一个。
|
|
197
|
+
...(o.sized === 'user' ? { sized: 'user' } : {}),
|
|
198
|
+
// 贴身跟随(2026-08-27 shapes 编辑面):这个涂鸦是"圈住 hug 那件东西"的记号,
|
|
199
|
+
// 挪那件东西时它跟着走(edit_board move/move_group/reflow;前端拖拽同口径)
|
|
200
|
+
...(typeof o.hug === 'string' && o.hug.length <= 300 ? { hug: o.hug } : {}),
|
|
157
201
|
// 分组标签 + 草稿位(2026-08-23 黑板)。staging = agent 这一轮还在打草稿:
|
|
158
202
|
// 入座不看它、read_board 默认不列它、画面上半透明;落定(commitStaging)清位。
|
|
159
203
|
...(sanitizeTag(o.tag) ? { tag: sanitizeTag(o.tag) } : {}),
|
|
@@ -188,12 +232,17 @@ export function sanitizeBinding(b) {
|
|
|
188
232
|
: {}),
|
|
189
233
|
// 谁画的。用户画的线 agent 不该擅自删,反过来也一样;auto = 机器可证的
|
|
190
234
|
// 引用关系(auto-relations.js 对账层专属,只有它增删自家 b:auto:* 条目)。
|
|
191
|
-
...(b.by === '
|
|
235
|
+
...(b.by === 'auto' ? { by: 'auto' } : sanitizeBy(b.by) ? { by: sanitizeBy(b.by) } : {}),
|
|
192
236
|
// 材质(2026-08-23 黑板):语义之外的第二个轴 —— 墨线/手绘/丝线。不落默认值,
|
|
193
237
|
// 渲染侧按语义给缺省材质;存了默认值以后改缺省就改不动存量。
|
|
194
238
|
...(isBindingMaterial(b.material) && b.material !== 'ink' ? { material: b.material } : {}),
|
|
195
239
|
...(sanitizeTag(b.tag) ? { tag: sanitizeTag(b.tag) } : {}),
|
|
196
240
|
...(b.staging === true ? { staging: true } : {}),
|
|
241
|
+
// 跟随线(2026-08-25 范式重做,RP「状态板重锚」案):follow = 目标 tag ——
|
|
242
|
+
// 这条线的 to 端永远指向该 tag 最新落板的那件,服务端在新件落板时自动重指
|
|
243
|
+
// 并把 from 端所在的组挪过去(board-follow.js)。followSide = 挪到哪一侧。
|
|
244
|
+
...(sanitizeTag(b.follow) ? { follow: sanitizeTag(b.follow) } : {}),
|
|
245
|
+
...(['right', 'left', 'above', 'below'].includes(b.followSide) ? { followSide: b.followSide } : {}),
|
|
197
246
|
};
|
|
198
247
|
}
|
|
199
248
|
|
|
@@ -218,6 +267,94 @@ export function sanitizeZone(z) {
|
|
|
218
267
|
};
|
|
219
268
|
}
|
|
220
269
|
|
|
270
|
+
/**
|
|
271
|
+
* 线(lane)注册表条目(2026-08-27 空间规划):{x,y,w,parent}。
|
|
272
|
+
* 键 = tag(线就是 tag,见 lib/board-lanes.js 头注);frontier 不存 —— 从成员现算。
|
|
273
|
+
*/
|
|
274
|
+
export function sanitizeLane(l) {
|
|
275
|
+
if (!l || typeof l !== 'object') return null;
|
|
276
|
+
const x = Number(l.x); const y = Number(l.y);
|
|
277
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) return null;
|
|
278
|
+
const parent = typeof l.parent === 'string' && l.parent.length <= 300 ? l.parent : null;
|
|
279
|
+
return {
|
|
280
|
+
x: Math.round(x), y: Math.round(y),
|
|
281
|
+
w: clampNum(l.w, 96, 2400, 480),
|
|
282
|
+
...(parent ? { parent } : {}),
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* 卷(2026-08-27 收纳器):{ at, by, label? }。有条目 = 这个 tag 收着。
|
|
288
|
+
* 只是视觉收纳的状态位 —— 成员对象的座位原样留在 objects 里(展开即归位),
|
|
289
|
+
* 落位引擎照旧把它们当障碍。
|
|
290
|
+
*/
|
|
291
|
+
export function sanitizeRoll(r) {
|
|
292
|
+
if (!r || typeof r !== 'object') return null;
|
|
293
|
+
const at = typeof r.at === 'string' && r.at.length <= 40 ? r.at : new Date().toISOString();
|
|
294
|
+
const by = typeof r.by === 'string' && r.by.length <= 40 ? r.by : 'user';
|
|
295
|
+
const label = typeof r.label === 'string' && r.label.trim() ? r.label.trim().slice(0, 60) : null;
|
|
296
|
+
return { at, by, ...(label ? { label } : {}) };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* 纸(sheet 注册表,2026-08-29 纸范式):{ x, y, w, h, by?, at?, title? }。
|
|
301
|
+
* 键 = 纸名(TAG_RE 字符集)。纸是**分配纪律不是本体容器**:成员按几何派生
|
|
302
|
+
* (中心点落在纸内),这里只登记「这块地是谁在什么时候铺的、多大」。
|
|
303
|
+
* w/h 显式存 —— 纸尺寸取决于铺纸那一刻的设备档(手机纸小、桌面纸大),
|
|
304
|
+
* 不能全局常量化。
|
|
305
|
+
*/
|
|
306
|
+
/** 一张纸上最多规划几块地(版面切太碎就不是版面了) */
|
|
307
|
+
export const MAX_SLOTS = 24;
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* 版位(slot,2026-08-29 占位契约刀 E):开工时先把这一屏切成几块地,各自起名。
|
|
311
|
+
*
|
|
312
|
+
* ⚠️ 坐标**跟 at 同一套**:纸内局部像素、原点在版心左上角。不用格数 —— 同一组
|
|
313
|
+
* 字段两种单位是站主点过名的老坑("同字段按数值切单位=中间那段必错且不出声")。
|
|
314
|
+
*/
|
|
315
|
+
export function sanitizeSlot(s) {
|
|
316
|
+
if (!s || typeof s !== 'object') return null;
|
|
317
|
+
const x = Number(s.x); const y = Number(s.y);
|
|
318
|
+
const w = Number(s.w); const h = Number(s.h);
|
|
319
|
+
if (![x, y, w, h].every(Number.isFinite)) return null;
|
|
320
|
+
if (w < 48 || h < 24) return null; // 比这还小的地放不下一行字
|
|
321
|
+
return {
|
|
322
|
+
x: Math.round(Math.min(Math.max(0, x), 12000)),
|
|
323
|
+
y: Math.round(Math.min(Math.max(0, y), 12000)),
|
|
324
|
+
w: Math.round(Math.min(w, 12000)),
|
|
325
|
+
h: Math.round(Math.min(h, 12000)),
|
|
326
|
+
...(typeof s.about === 'string' && s.about.trim() ? { about: s.about.trim().slice(0, 60) } : {}),
|
|
327
|
+
// 收产物的那块地(2026-08-30 刀 G):`for:'artifacts'` 一张纸上最多一块。
|
|
328
|
+
// 产物(生成的图、写出来的文件、目录型产物)是 agent 干活的**副产品**,
|
|
329
|
+
// 它没法在写之前一件件点名落位 —— 但它可以提前说「这一页的产物都放这儿」。
|
|
330
|
+
// 没有这块地,机器就只能自己决定,那就又回到「机器定版面」了。
|
|
331
|
+
...(s.for === 'artifacts' ? { for: 'artifacts' } : {}),
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export function sanitizeSheet(s) {
|
|
336
|
+
if (!s || typeof s !== 'object') return null;
|
|
337
|
+
const x = Number(s.x); const y = Number(s.y);
|
|
338
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) return null;
|
|
339
|
+
const slots = {};
|
|
340
|
+
let slotN = 0;
|
|
341
|
+
for (const [nm, v] of Object.entries(s?.slots && typeof s.slots === 'object' ? s.slots : {})) {
|
|
342
|
+
if (slotN >= MAX_SLOTS) break;
|
|
343
|
+
if (!TAG_RE.test(nm)) continue;
|
|
344
|
+
const sl = sanitizeSlot(v);
|
|
345
|
+
if (sl) { slots[nm] = sl; slotN += 1; }
|
|
346
|
+
}
|
|
347
|
+
return {
|
|
348
|
+
x: Math.round(x), y: Math.round(y),
|
|
349
|
+
w: clampNum(s.w, 240, 8000, ONE_SCREEN.w),
|
|
350
|
+
h: clampNum(s.h, 240, 12000, ONE_SCREEN.h),
|
|
351
|
+
...(sanitizeBy(s.by) ? { by: sanitizeBy(s.by) } : {}),
|
|
352
|
+
...(typeof s.at === 'string' && s.at.length <= 40 ? { at: s.at } : {}),
|
|
353
|
+
...(typeof s.title === 'string' && s.title.trim() ? { title: s.title.trim().slice(0, 60) } : {}),
|
|
354
|
+
...(slotN ? { slots } : {}),
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
221
358
|
export function sanitizeBoard(raw) {
|
|
222
359
|
const size = sanitizeSize(raw?.size);
|
|
223
360
|
const objects = {};
|
|
@@ -247,6 +384,79 @@ export function sanitizeBoard(raw) {
|
|
|
247
384
|
// 主角覆盖(2026-08-14 agent 摆位):显式立的主角压过前端 pickHero 的推断。
|
|
248
385
|
// 存 id 不存理由 —— 理由在会话里,画布只要知道谁站 C 位
|
|
249
386
|
const hero = typeof raw?.hero === 'string' && raw.hero.length <= 300 ? raw.hero : null;
|
|
250
|
-
|
|
387
|
+
const lanes = {};
|
|
388
|
+
let lCount = 0;
|
|
389
|
+
for (const [name, l] of Object.entries(raw?.lanes && typeof raw.lanes === 'object' ? raw.lanes : {})) {
|
|
390
|
+
if (lCount >= MAX_LANES) break;
|
|
391
|
+
const tag = sanitizeTag(name);
|
|
392
|
+
if (!tag) continue;
|
|
393
|
+
const s = sanitizeLane(l);
|
|
394
|
+
if (s) { lanes[tag] = s; lCount += 1; }
|
|
395
|
+
}
|
|
396
|
+
const rolls = {};
|
|
397
|
+
let rCount = 0;
|
|
398
|
+
for (const [name, r] of Object.entries(raw?.rolls && typeof raw.rolls === 'object' ? raw.rolls : {})) {
|
|
399
|
+
if (rCount >= MAX_LANES) break; // 卷跟线同量级:一线一卷
|
|
400
|
+
const tag = sanitizeTag(name);
|
|
401
|
+
if (!tag) continue;
|
|
402
|
+
const s = sanitizeRoll(r);
|
|
403
|
+
if (s) { rolls[tag] = s; rCount += 1; }
|
|
404
|
+
}
|
|
405
|
+
const sheets = {};
|
|
406
|
+
let sCount = 0;
|
|
407
|
+
for (const [name, s0] of Object.entries(raw?.sheets && typeof raw.sheets === 'object' ? raw.sheets : {})) {
|
|
408
|
+
if (sCount >= MAX_SHEETS) break;
|
|
409
|
+
const nm = sanitizeTag(name);
|
|
410
|
+
if (!nm) continue;
|
|
411
|
+
const s = sanitizeSheet(s0);
|
|
412
|
+
if (s) { sheets[nm] = s; sCount += 1; }
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* 跟随规则(2026-08-30):`{ 组tag: { target, side?, label? } }`。
|
|
416
|
+
*
|
|
417
|
+
* 以前 follow 只有「线」这一种形态,而线要求两端**此刻都在板上**。真会话里最常见的
|
|
418
|
+
* 写法是开场先立规则再写第一章(skill 的原话就是「开场画一次状态板,然后立一条跟随
|
|
419
|
+
* 规则」)—— 那一刻目标 tag 还是空的,于是必炸。全库 5 次、跨 4 个项目,形态一模一样。
|
|
420
|
+
* 规则和线分开存之后,规则可以先立着,目标一出现 applyFollows 顺手把线接上。
|
|
421
|
+
*/
|
|
422
|
+
const follows = {};
|
|
423
|
+
let fCount = 0;
|
|
424
|
+
for (const [g, r] of Object.entries(raw?.follows && typeof raw.follows === 'object' ? raw.follows : {})) {
|
|
425
|
+
if (fCount >= 12) break;
|
|
426
|
+
const gt = sanitizeTag(g);
|
|
427
|
+
const target = sanitizeTag(r?.target);
|
|
428
|
+
if (!gt || !target || gt === target) continue;
|
|
429
|
+
follows[gt] = {
|
|
430
|
+
target,
|
|
431
|
+
...(['right', 'left', 'above', 'below'].includes(r?.side) ? { side: r.side } : {}),
|
|
432
|
+
...(typeof r?.label === 'string' && r.label.trim() ? { label: r.label.trim().slice(0, 60) } : {}),
|
|
433
|
+
};
|
|
434
|
+
fCount += 1;
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* 待摆产物(2026-08-30 刀 G):磁盘上有、但板上还没地方放的工作区相对路径。
|
|
438
|
+
* 入座不再自己铺纸(那是「机器替 agent 定版面」),排不下就进这条队列,
|
|
439
|
+
* 每回合状态块点名,等 agent 规划出地方再落座。
|
|
440
|
+
*/
|
|
441
|
+
const pending = [];
|
|
442
|
+
for (const v of Array.isArray(raw?.pending) ? raw.pending : []) {
|
|
443
|
+
if (pending.length >= 50) break;
|
|
444
|
+
if (typeof v !== 'string') continue;
|
|
445
|
+
const t = v.trim();
|
|
446
|
+
if (!t || t.length > 300 || t.includes('..') || t.startsWith('/')) continue;
|
|
447
|
+
if (!pending.includes(t)) pending.push(t);
|
|
448
|
+
}
|
|
449
|
+
// 暂存架原点(2026-08-30,lib/board-shelf.js):机器到货码放的竖带左上角
|
|
450
|
+
const shelf = (raw?.shelf && Number.isFinite(Number(raw.shelf.x)) && Number.isFinite(Number(raw.shelf.y)))
|
|
451
|
+
? { x: clampNum(raw.shelf.x, -COORD_LIMIT, COORD_LIMIT, 0), y: clampNum(raw.shelf.y, -COORD_LIMIT, COORD_LIMIT, 0) }
|
|
452
|
+
: null;
|
|
453
|
+
return {
|
|
454
|
+
size, zones, objects, bindings,
|
|
455
|
+
...(hero ? { hero } : {}), ...(lCount ? { lanes } : {}), ...(rCount ? { rolls } : {}),
|
|
456
|
+
...(sCount ? { sheets } : {}),
|
|
457
|
+
...(fCount ? { follows } : {}),
|
|
458
|
+
...(pending.length ? { pending } : {}),
|
|
459
|
+
...(shelf ? { shelf } : {}),
|
|
460
|
+
};
|
|
251
461
|
}
|
|
252
462
|
|