@xiaobuyu/nodesign 0.0.6 → 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 +26 -0
- package/server/api/assets/notes.js +50 -0
- package/server/api/assets.js +31 -38
- 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/instruction.js +3 -3
- 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 +18 -1
- package/server/engine/agent/hooks/beat-gate.js +73 -0
- package/server/engine/agent/hooks/failure.js +11 -1
- package/server/engine/agent/hooks/lifecycle.js +16 -41
- package/server/engine/agent/hooks/post-guidance.js +4 -38
- 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 +208 -47
- package/server/engine/agent/hooks.js +80 -14
- package/server/engine/agent/isolation.js +25 -2
- package/server/engine/agent/memory-config.js +87 -0
- 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 +579 -70
- package/server/engine/agent/prompts/tools/direct-edit-protocol.md +1 -1
- package/server/engine/agent/prompts/tools/generate-image-cookbook.md +26 -9
- package/server/engine/agent/prompts/tools/paint-still-cookbook.md +1 -1
- package/server/engine/agent/prompts/tools/site-reference.md +15 -4
- 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 +55 -32
- 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/agents/explorer.md +1 -1
- package/server/engine/agents/index.js +1 -1
- package/server/engine/mcp/actor.js +59 -0
- package/server/engine/mcp/capability-gate.js +21 -2
- package/server/engine/mcp/index.js +118 -33
- 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-computer.js +2 -1
- package/server/engine/mcp/tools/browse-find-batch.js +95 -16
- package/server/engine/mcp/tools/browse.js +25 -3
- package/server/engine/mcp/tools/cast-role.js +167 -0
- package/server/engine/mcp/tools/crystallize-skill.js +1 -1
- 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/generate-image.js +50 -70
- package/server/engine/mcp/tools/helpers/codex-imagegen.js +139 -0
- package/server/engine/mcp/tools/helpers/rembg.js +7 -2
- package/server/engine/mcp/tools/helpers/shot-pipeline.js +109 -0
- package/server/engine/mcp/tools/list-pages.js +11 -5
- 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 +45 -6
- package/server/engine/mcp/tools/paint-still.js +1 -1
- package/server/engine/mcp/tools/pin-to-board.js +103 -19
- package/server/engine/mcp/tools/read-board.js +123 -16
- package/server/engine/mcp/tools/read-tavern-json.js +87 -19
- package/server/engine/mcp/tools/report-issue.js +15 -7
- 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 +19 -9
- 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 +12 -16
- 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 +568 -99
- package/server/engine/plugins/nodesign/skills/blackboard-rp/SKILL.md +215 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/SKILL.md +14 -7
- package/server/engine/plugins/nodesign/skills/docx-craft/SKILL.md +44 -5
- package/server/engine/plugins/nodesign/skills/docx-craft/references/token-schema.md +16 -0
- package/server/engine/plugins/nodesign/skills/site-craft/SKILL.md +24 -8
- 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 -2
- package/server/lib/artifact-target.js +31 -14
- 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 +38 -3
- 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 +46 -4
- 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/export-collect.js +3 -1
- 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/issues-store.js +3 -1
- package/server/lib/json-preview.js +58 -0
- package/server/lib/kinds/docx.js +11 -4
- package/server/lib/kinds/index.js +6 -16
- package/server/lib/kinds/site.js +40 -7
- 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/rewrite-refs.js +110 -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/ssrf-guard.js +9 -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/task-scan.js +18 -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 +137 -7
- package/server/projects/memory-migration.js +85 -0
- package/server/projects/move-entry.js +11 -1
- package/server/projects/session-jsonl.js +110 -0
- package/server/projects/store.js +56 -3
- package/server/projects/ui-config.js +2 -1
- package/server/projects/viewpoint-store.js +35 -3
- package/server/projects/workspace-slots.js +30 -0
- package/server/projects/workspace-templates.js +42 -10
- package/server/projects/workspace.js +25 -26
- package/server/runtime/capabilities.js +6 -2
- package/server/services/rembg-launcher.js +5 -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-P_vIdh-G.js → BrowserWindow-CcaGRWz5.js} +1 -1
- package/web/dist/assets/DeckWindow-BIr2KGFT.js +6 -0
- package/web/dist/assets/{DocxWindow-DSAU1-tD.js → DocxWindow-B9KZmU5z.js} +2 -2
- package/web/dist/assets/{MermaidBlock-DR1GikCv.js → MermaidBlock-DrfRnz8E.js} +43 -43
- package/web/dist/assets/{SiteWindow-92uBuRRR.js → SiteWindow-CJOjCF7d.js} +6 -6
- package/web/dist/assets/{abnfDiagram-VCTEODGH-uA8Esix4.js → abnfDiagram-VCTEODGH-CUZbQGIg.js} +1 -1
- package/web/dist/assets/{arc-D_khH_xN.js → arc-C1-HSUny.js} +1 -1
- package/web/dist/assets/{architectureDiagram-5GKGNRK7-DydJHqOt.js → architectureDiagram-5GKGNRK7-B5nwvu60.js} +2 -2
- package/web/dist/assets/{blockDiagram-NRAW4CY4-B7wBbKiW.js → blockDiagram-NRAW4CY4-uPPuFLwR.js} +1 -1
- package/web/dist/assets/{c4Diagram-UCG6FXSJ-6mrH0x7Y.js → c4Diagram-UCG6FXSJ-C0mKBFlz.js} +1 -1
- package/web/dist/assets/channel-BxpWNOGU.js +1 -0
- package/web/dist/assets/{chunk-2Q5K7J3B-Cuikho5Q.js → chunk-2Q5K7J3B-Cs0rlZ4q.js} +1 -1
- package/web/dist/assets/{chunk-5VM5RSS4-3-KZ5AUz.js → chunk-5VM5RSS4-DtR63oYb.js} +1 -1
- package/web/dist/assets/{chunk-F27PBJKO-Bkwq2gGU.js → chunk-F27PBJKO-B3MRdezJ.js} +1 -1
- package/web/dist/assets/{chunk-G27WJ6UU-BLyQzdGE.js → chunk-G27WJ6UU-BtwntwpS.js} +1 -1
- package/web/dist/assets/{chunk-JWPE2WC7-B7HxxQds.js → chunk-JWPE2WC7-BHsTe5PD.js} +1 -1
- package/web/dist/assets/{chunk-LCL6LL3I-CtXrnUwF.js → chunk-LCL6LL3I-knY15Qdm.js} +1 -1
- package/web/dist/assets/{chunk-POPQ4Y6H-dxddZlXt.js → chunk-POPQ4Y6H-C51tgK_I.js} +1 -1
- package/web/dist/assets/{chunk-SVP7TREG-DpkmrgHn.js → chunk-SVP7TREG-CnwRZmF_.js} +1 -1
- package/web/dist/assets/{chunk-XXDRQBXY-BwZ7RbpQ.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-DGFOsMUt.js → cose-bilkent-JH36ORCC-CzWlGP6i.js} +1 -1
- package/web/dist/assets/{cynefin-OW5HDTMX-DUwn3ehS.js → cynefin-OW5HDTMX-vK64EGSv.js} +1 -1
- package/web/dist/assets/{cynefinDiagram-5FMLGOSQ-DlLAOZ9x.js → cynefinDiagram-5FMLGOSQ-Bs3g1Gvh.js} +1 -1
- package/web/dist/assets/{dagre-3AP2YEHR-CyourLfz.js → dagre-3AP2YEHR-BhgkYe8V.js} +1 -1
- package/web/dist/assets/{diagram-S7CK7UJ4-BTMbx2np.js → diagram-S7CK7UJ4-C_6mvD2V.js} +1 -1
- package/web/dist/assets/{diagram-UQ7AKVKN-UK94_1-P.js → diagram-UQ7AKVKN-qW7tssUW.js} +1 -1
- package/web/dist/assets/{diagram-VSXAHHWV-C7zS-v9v.js → diagram-VSXAHHWV-txJPHpvG.js} +1 -1
- package/web/dist/assets/{diagram-VX7I27RA-DKJRa6CK.js → diagram-VX7I27RA-CQTMsCnL.js} +1 -1
- package/web/dist/assets/{diagram-Z3DM3KII-kUbAfGAp.js → diagram-Z3DM3KII--cb1hoE6.js} +1 -1
- package/web/dist/assets/{ebnfDiagram-PWID7BFC-CVW16_vb.js → ebnfDiagram-PWID7BFC-Da6lDU77.js} +1 -1
- package/web/dist/assets/{erDiagram-SSCWMZ5O-BRQMFQ5y.js → erDiagram-SSCWMZ5O-DGCMrsQn.js} +1 -1
- package/web/dist/assets/{flowDiagram-A5DVABFB-S34LpLtK.js → flowDiagram-A5DVABFB-GdOL_qno.js} +1 -1
- package/web/dist/assets/{ganttDiagram-EL5Y4UJY-BFiqk1-G.js → ganttDiagram-EL5Y4UJY-Cx1F_jBK.js} +4 -4
- package/web/dist/assets/{gitGraphDiagram-WWUBYQGX-LZsrdyyv.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-0DvN6KIp.js → infoDiagram-RXCK75RN-CjvKpSPA.js} +1 -1
- package/web/dist/assets/{ishikawaDiagram-5VMMS53U-CaY3A-Bx.js → ishikawaDiagram-5VMMS53U-CxLoAPhx.js} +1 -1
- package/web/dist/assets/{journeyDiagram-EYS64GPL-BBeiaCrj.js → journeyDiagram-EYS64GPL-BfvS1upJ.js} +1 -1
- package/web/dist/assets/{kanban-definition-3QL26DDD-ux42DRXQ.js → kanban-definition-3QL26DDD-DKPPu0w4.js} +1 -1
- package/web/dist/assets/{layout-CnXiF_NB.js → layout-Dhgck-Zo.js} +1 -1
- package/web/dist/assets/{linear-BMW0u6G6.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-hUxSpdKn.js → mindmap-definition-FBJOCRG2-CalfL_Kn.js} +1 -1
- package/web/dist/assets/{pegDiagram-XKGWAZYB-aCaQPH0w.js → pegDiagram-XKGWAZYB-D9mVWz7l.js} +1 -1
- package/web/dist/assets/pending-edit-apply-kwMFWsUe.js +31 -0
- package/web/dist/assets/{pieDiagram-E7YTZNPT-CmOEZ8zg.js → pieDiagram-E7YTZNPT-DELzbZNS.js} +1 -1
- package/web/dist/assets/{quadrantDiagram-AXDQQJYC-VW6fAhsV.js → quadrantDiagram-AXDQQJYC-TZqp4mfA.js} +1 -1
- package/web/dist/assets/{railroadDiagram-O6MQD6OU-CKJ7Ba5K.js → railroadDiagram-O6MQD6OU-DxX59f0i.js} +1 -1
- package/web/dist/assets/{requirementDiagram-EFPCY7ZU-pNGkq9Vw.js → requirementDiagram-EFPCY7ZU-DFdsJLIe.js} +1 -1
- package/web/dist/assets/{rotate-cw-KAfChbGV.js → rotate-cw-62liz7LX.js} +1 -1
- package/web/dist/assets/{sankeyDiagram-P5KCCOFB-BIMmDTfZ.js → sankeyDiagram-P5KCCOFB-ByYcd3fG.js} +1 -1
- package/web/dist/assets/{sequenceDiagram-WJ2MYXX4-BBW7wzKY.js → sequenceDiagram-WJ2MYXX4-Bj-ml1jS.js} +1 -1
- package/web/dist/assets/{sizeCapture-X5ZJPWSS-DdkXmLYR.js → sizeCapture-X5ZJPWSS-KhsXThyD.js} +1 -1
- package/web/dist/assets/{square-dashed-mouse-pointer-CDUlhUP7.js → square-dashed-mouse-pointer-D_VrqVpy.js} +1 -1
- package/web/dist/assets/{stateDiagram-HBIQ2CUA-W313Uz16.js → stateDiagram-HBIQ2CUA-BJlpHfWU.js} +1 -1
- package/web/dist/assets/stateDiagram-v2-4QOOHH4V-CR2kYAH2.js +1 -0
- package/web/dist/assets/{swimlanes-XN3QIQJK-C29TL26K.js → swimlanes-XN3QIQJK-DUB9OJVs.js} +1 -1
- package/web/dist/assets/swimlanesDiagram-VK2B7HYN-BitOxnG2.js +8 -0
- package/web/dist/assets/{timeline-definition-24CTP7MA-CF0JMObX.js → timeline-definition-24CTP7MA-C03NX82A.js} +1 -1
- package/web/dist/assets/{vennDiagram-4TSXK5OY-DrDFsaH3.js → vennDiagram-4TSXK5OY-B9pjzngp.js} +1 -1
- package/web/dist/assets/{wardleyDiagram-VM6X3IG4-CVrnRIS_.js → wardleyDiagram-VM6X3IG4-UqWq_mcT.js} +1 -1
- package/web/dist/assets/{xychartDiagram-S5SC5T6Z-BvkN1zsT.js → xychartDiagram-S5SC5T6Z-D2pDKtOv.js} +1 -1
- package/web/dist/index.html +2 -2
- package/server/api/memory.js +0 -185
- package/server/engine/agent/prompts/tools/generate-image-cookbook.gemini-gateway.md +0 -583
- package/server/engine/mcp/tools/arrange-on-board.js +0 -124
- 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/record-decision.js +0 -140
- package/server/engine/mcp/tools/relate-on-board.js +0 -112
- package/server/engine/mcp/tools/sketch-on-board.js +0 -337
- 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-BpLv5082.js +0 -16
- package/web/dist/assets/channel-DGkccMO1.js +0 -1
- package/web/dist/assets/classDiagram-DTDB5LWJ-BGscw3OD.js +0 -1
- package/web/dist/assets/classDiagram-v2-JRS7N3AN-BGscw3OD.js +0 -1
- package/web/dist/assets/index-C0KUf6Wd.css +0 -1
- package/web/dist/assets/index-GAamh1p4.js +0 -2152
- 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-AcdOQ9Ij.js +0 -31
- package/web/dist/assets/stateDiagram-v2-4QOOHH4V-DDhJBZzf.js +0 -1
- package/web/dist/assets/swimlanesDiagram-VK2B7HYN-BfN-Xdmu.js +0 -8
package/server/lib/canvas-id.js
CHANGED
|
@@ -10,21 +10,63 @@
|
|
|
10
10
|
export function normalizeCanvasId(raw) {
|
|
11
11
|
let id = String(raw || '').trim().replace(/\\/g, '/').replace(/^\.\//, '').replace(/^\/+|\/+$/g, '');
|
|
12
12
|
if (!id || id.includes('..')) return null;
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
// (doc:brand / doc:_root 映射 2026-08-24 拆除:项目文档并进根 CLAUDE.md,
|
|
14
|
+
// 记忆住 记忆/,都是普通画布文件,没有特殊 id 了)
|
|
15
15
|
if (!/^(deck|site|doc|text|scribble):/.test(id) && /\.html?$/i.test(id)) return `deck:${id}`;
|
|
16
16
|
return id;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* 一个 tag 下所有座位的包络(08-23 案:near 只认节点 id 不认 tag,可 tag 本来
|
|
21
|
+
* 就是"一片东西的名字"——刚用 tag 画完草图,把 tag 传给 near 是自然写法)。
|
|
22
|
+
* 返回 { x, y, w, h, anchorId, ids } 或 null;anchorId = 最右那个(贴右侧摆用)。
|
|
23
|
+
* sizeOf(id, entry) 由调用方给(estimateSizeOn 绑着 board,不在这层 import)。
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* 查 tag 时的归一:剥掉前导 `#`(2026-08-30)。
|
|
27
|
+
*
|
|
28
|
+
* ⛔ 我们在 write_on_board 的 `near`、read_board 的 `tag` 等好几处描述里明写
|
|
29
|
+
* 「Canvas id or **#tag**」,而查询侧一直是 `e.tag === raw` 精确比对,从来没剥过
|
|
30
|
+
* 这个井号 —— 全库统计:带 # 的写法 5 处,5 处全废;不带 # 的 2773 处全通。
|
|
31
|
+
* 「注释/描述声称做了、代码没做」的第三例。
|
|
32
|
+
*
|
|
33
|
+
* 只用在**查**的一侧。写的一侧(write_on_board 的 tag 参数)走 TAG_RE,本来就
|
|
34
|
+
* 收不下 # —— 那是 schema 报错,是响的,不用管。
|
|
35
|
+
*/
|
|
36
|
+
export function bareTag(raw) {
|
|
37
|
+
return String(raw ?? '').trim().replace(/^#+/, '');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function tagEnvelope(board, rawTag, sizeOf) {
|
|
41
|
+
const tag = bareTag(rawTag);
|
|
42
|
+
if (!tag) return null;
|
|
43
|
+
const hits = Object.entries(board?.objects || {})
|
|
44
|
+
.filter(([, e]) => e?.tag === tag && Number.isFinite(e?.x));
|
|
45
|
+
if (!hits.length) return null;
|
|
46
|
+
let x1 = Infinity; let y1 = Infinity; let x2 = -Infinity; let y2 = -Infinity;
|
|
47
|
+
let anchorId = null; let rightEdge = -Infinity;
|
|
48
|
+
for (const [id, e] of hits) {
|
|
49
|
+
const s = sizeOf(id, e) || { w: 0, h: 0 };
|
|
50
|
+
x1 = Math.min(x1, e.x); y1 = Math.min(y1, e.y);
|
|
51
|
+
x2 = Math.max(x2, e.x + s.w); y2 = Math.max(y2, e.y + s.h);
|
|
52
|
+
if (e.x + s.w > rightEdge) { rightEdge = e.x + s.w; anchorId = id; }
|
|
53
|
+
}
|
|
54
|
+
return { x: x1, y: y1, w: x2 - x1, h: y2 - y1, anchorId, ids: hits.map(([id]) => id) };
|
|
55
|
+
}
|
|
56
|
+
|
|
19
57
|
/**
|
|
20
58
|
* 它住在哪一层(服务端近似版):显式 zone 字段优先,其次沿路径往上找第一个
|
|
21
59
|
* 已知文件夹。已知集 = board.zones 的 key —— 比前端少了"服务端扫出来的任务
|
|
22
60
|
* 目录"这一路,没摆过的深层目录会归到根。read_board 的输出里写明这是近似。
|
|
23
61
|
*/
|
|
24
62
|
export function layerOf(id, entry, knownFolders) {
|
|
25
|
-
|
|
63
|
+
// 显式 zone 只认**真实存在的层**('' 或 zones 里有的文件夹)。历史上
|
|
64
|
+
// pin_to_board 往 zone 里写过 'assets/generated' 这类前端根本不当层渲染的
|
|
65
|
+
// 路径(iss_mt5t487g:pin 上来的图被判在"assets 层",arrange 跟根层的草图
|
|
66
|
+
// 节点一比就拒)——错标签直接落回按路径推,存量脏数据顺带自愈。
|
|
67
|
+
if (entry && typeof entry.zone === 'string'
|
|
68
|
+
&& (entry.zone === '' || knownFolders?.has?.(entry.zone))) return entry.zone;
|
|
26
69
|
const s = String(id);
|
|
27
|
-
if (s.startsWith('doc:')) return '';
|
|
28
70
|
const c = s.indexOf(':');
|
|
29
71
|
const p = (c > 0 && /^[a-z]+$/.test(s.slice(0, c))) ? s.slice(c + 1) : s;
|
|
30
72
|
let d = p.includes('/') ? p.slice(0, p.lastIndexOf('/')) : '';
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* server/lib/chalk-flow.js —— 长文自动分条(write_on_board 的 flow,2026-08-30)
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么要有这个文件
|
|
5
|
+
*
|
|
6
|
+
* 「剩 15 行、我这段是 14 行还是 16 行」这道算术模型做不准,也不该由它做 ——
|
|
7
|
+
* proj_mtfpehm3 首场真会话 14 发 board_batch 挂了 6 发,全是容量拒收,本质全是
|
|
8
|
+
* 这道算术。flow 把它拿走:agent 交整段内容,机器按**段落边界**拆成若干条
|
|
9
|
+
* ≤ 一张卡高的小板书,链着往版位/纸里排,装到哪儿是哪儿,装不下的**原样退回**
|
|
10
|
+
* (绝不静默丢、绝不硬挤、绝不替它翻页 —— 刀 F 的教义一条不破)。
|
|
11
|
+
*
|
|
12
|
+
* ## 拆分纪律
|
|
13
|
+
*
|
|
14
|
+
* 1. 只在**空行**处拆,且空行必须在围栏(``` fence)外 —— 代码块/nd:controls/
|
|
15
|
+
* 表格永远整块走,切开它们比写不下更坏。
|
|
16
|
+
* 2. 单块超高时降级到**句子边界**(。!?.!?; 换行也算),仍不切词。
|
|
17
|
+
* 3. 一句话就超高的(极少):整句放行,靠既有的折叠 + 如实报兜底。
|
|
18
|
+
* 4. 贪心装填:往当前条里续块,直到再续就超 maxH。
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { textBox } from './sketch-layout.js';
|
|
22
|
+
|
|
23
|
+
const FENCE_LINE = /^[ \t]*```/;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 按空行拆块(围栏内的空行不算边界)。
|
|
27
|
+
* @returns {string[]} 原文顺序的块,各自 trim 过、非空
|
|
28
|
+
*/
|
|
29
|
+
export function splitBlocks(body) {
|
|
30
|
+
const lines = String(body || '').split(/\r?\n/);
|
|
31
|
+
const blocks = [];
|
|
32
|
+
let cur = [];
|
|
33
|
+
let inFence = false;
|
|
34
|
+
const push = () => {
|
|
35
|
+
const t = cur.join('\n').trim();
|
|
36
|
+
if (t) blocks.push(t);
|
|
37
|
+
cur = [];
|
|
38
|
+
};
|
|
39
|
+
for (const l of lines) {
|
|
40
|
+
if (FENCE_LINE.test(l)) inFence = !inFence;
|
|
41
|
+
if (!inFence && !l.trim()) { push(); continue; }
|
|
42
|
+
cur.push(l);
|
|
43
|
+
}
|
|
44
|
+
push();
|
|
45
|
+
return blocks;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 单块超高时的降级:按句子边界拆(不切词;拆不动就原样一块) */
|
|
49
|
+
function splitSentences(block) {
|
|
50
|
+
const parts = String(block).split(/(?<=[。!?!?;;])\s*/).filter((s) => s.trim());
|
|
51
|
+
return parts.length > 1 ? parts : [block];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 把长文拆成若干条小板书的正文。
|
|
56
|
+
*
|
|
57
|
+
* @param {string} body
|
|
58
|
+
* @param {object} opts
|
|
59
|
+
* @param {number} opts.wUnits 目标宽(格数,24px 一格)—— 跟真落板同一把尺
|
|
60
|
+
* @param {string} [opts.size] 字号档(同 write_on_board 的 size)
|
|
61
|
+
* @param {number} opts.maxH 单条上限(px;通常 CARD_MAX_H)
|
|
62
|
+
* @returns {string[]} 每条的正文。measure 用的是 textBox —— 跟落位那一刻同一个
|
|
63
|
+
* 函数量出来的数,不存在"估的和真的不一样"这层误差。
|
|
64
|
+
*/
|
|
65
|
+
export function flowChunks(body, { wUnits, size = 'md', maxH }) {
|
|
66
|
+
const measure = (t) => textBox(t, size === 'sm' ? 'md' : size, { md: true, wUnits }).h;
|
|
67
|
+
const fits = (t) => measure(t) <= maxH;
|
|
68
|
+
|
|
69
|
+
// 先把超高的单块降级拆开,得到一串"每个都尽量 ≤ maxH"的原子
|
|
70
|
+
const atoms = [];
|
|
71
|
+
for (const b of splitBlocks(body)) {
|
|
72
|
+
if (fits(b)) { atoms.push(b); continue; }
|
|
73
|
+
for (const s of splitSentences(b)) atoms.push(s);
|
|
74
|
+
}
|
|
75
|
+
if (!atoms.length) return [];
|
|
76
|
+
|
|
77
|
+
// 贪心装填:能续就续。用 \n\n 拼回 —— 空行本来就是它们之间的边界
|
|
78
|
+
const chunks = [];
|
|
79
|
+
let cur = atoms[0];
|
|
80
|
+
for (const a of atoms.slice(1)) {
|
|
81
|
+
const joined = `${cur}\n\n${a}`;
|
|
82
|
+
if (fits(joined)) { cur = joined; continue; }
|
|
83
|
+
chunks.push(cur);
|
|
84
|
+
cur = a;
|
|
85
|
+
}
|
|
86
|
+
chunks.push(cur);
|
|
87
|
+
return chunks;
|
|
88
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* server/lib/chalk-rewrite.js —— 「重写一条板书的正文」这一件事,收成一份
|
|
3
|
+
* (2026-08-30)
|
|
4
|
+
*
|
|
5
|
+
* 在这之前只有 `edit_board` 的 set_text 会做这件事(edit-board.js 的板书文件分支),
|
|
6
|
+
* 而它做对了三件容易漏的:
|
|
7
|
+
* 1. 读原文 → 只换 body,frontmatter 逐字保留(by/at/anchor/reply_to/tag/session)
|
|
8
|
+
* —— `renderChalk` 只回写白名单键,拼错一个键就是整行静默丢失
|
|
9
|
+
* 2. 宽度沿用现有的(改正文不该改版心),高度按新正文重算
|
|
10
|
+
* 3. **用户亲手拖出来的留白留得住**(`sized:'user'` 时取重算高与现高的较大者)
|
|
11
|
+
* —— 他调的是「这一块留多少空」,重写一次正文就抹掉,是把他的排版意图当缓存
|
|
12
|
+
*
|
|
13
|
+
* `set_vars` 要做的是同一件事(改表里一格 = 重写这条板书的正文),所以这里抽出来
|
|
14
|
+
* 两处共用,而不是抄第二份。抄的话第 3 条几乎必然被漏掉 —— 它是三条里唯一
|
|
15
|
+
* 「漏了也不报错、只是用户的排版悄悄没了」的那条。
|
|
16
|
+
*
|
|
17
|
+
* ⚠️ 笔权(只有作者本人能改自己的话)**不在这里判** —— 那是调用方的事:
|
|
18
|
+
* edit_board 按 `by` 判,set_vars 有自己的规矩(状态表是场务件不是谁的台词)。
|
|
19
|
+
* 放进来会让两个调用方共享一条它们其实不共享的语义。
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { promises as fs } from 'node:fs';
|
|
23
|
+
import { parseChalk, renderChalk } from './chalk.js';
|
|
24
|
+
import { STATE_TABLE_TAG, parseStateTable } from './state-table.js';
|
|
25
|
+
import { UNIT, textBox } from './sketch-layout.js';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 重写一条板书文件的正文,并算出它在板上的新尺寸。
|
|
29
|
+
*
|
|
30
|
+
* @param {string} abs 板书文件的绝对路径
|
|
31
|
+
* @param {string} body 新正文
|
|
32
|
+
* @param {object} entry 这条板书在 board.json 里的条目(要 w / h / sized / by)
|
|
33
|
+
* @returns {Promise<{ w:number, h:number }>} 新的盒子(宽沿用、高按正文重算)
|
|
34
|
+
* @throws 文件读不到 / 写不进时抛,调用方自己决定怎么说
|
|
35
|
+
*/
|
|
36
|
+
export async function rewriteChalkBody(abs, body, entry = {}) {
|
|
37
|
+
const parsed = parseChalk(await fs.readFile(abs, 'utf8'));
|
|
38
|
+
const c = parsed.chalk || {};
|
|
39
|
+
// 状态表堵写口(2026-08-30):这条板书是载重的(set_vars/nd:triggers/draw_trend
|
|
40
|
+
// 全读它),一次"成功"的重写就能把它们全弄死 —— glm 真会话用 \r 系字面量重写,
|
|
41
|
+
// set_text 报 Applied 2/2、表却永久 broken。重写后表必须还在,不在就大声拒。
|
|
42
|
+
if (c.tag === STATE_TABLE_TAG) {
|
|
43
|
+
const t = parseStateTable(String(body).replace(/\r\n?/g, '\n'));
|
|
44
|
+
if (!t.ok) {
|
|
45
|
+
const ex = new Error(`这一改会把状态表写坏(${t.error === 'no-table' ? '「| 键 | 值 |」表没了' : t.error})—— 什么都没写。改值用 set_vars;重写正文要保证那张两列表还在。`);
|
|
46
|
+
ex.code = 'STATE_TABLE';
|
|
47
|
+
throw ex;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
await fs.writeFile(abs, renderChalk({
|
|
51
|
+
body,
|
|
52
|
+
by: c.by || entry.by || 'agent',
|
|
53
|
+
...(c.at ? { at: c.at } : {}),
|
|
54
|
+
anchor: c.anchor,
|
|
55
|
+
replyTo: c.replyTo,
|
|
56
|
+
tag: c.tag,
|
|
57
|
+
sessionId: parsed.sessionId,
|
|
58
|
+
}), 'utf8');
|
|
59
|
+
|
|
60
|
+
const box = textBox(body, 'md', { md: true, wUnits: Math.max(8, Math.round((entry.w || 432) / UNIT)) });
|
|
61
|
+
const h = entry.sized === 'user' ? Math.max(box.h, Number(entry.h) || 0) : box.h;
|
|
62
|
+
return { w: box.w, h };
|
|
63
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/chalk-size-pref.js —— 「用户喜欢多宽的板书」(2026-08-28)
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么学得到
|
|
5
|
+
*
|
|
6
|
+
* 板书的**宽是真排版**(前端 BoardObject 的 `width: sz.w` 决定折行),高只是脚印
|
|
7
|
+
* (盒子不设 height,由正文撑)。所以用户拖宽一块板书是一个有信息量的动作:他在说
|
|
8
|
+
* "这个宽度读着舒服"。以前这个动作留不下任何痕迹,下一拍 agent 又按正文估一遍宽,
|
|
9
|
+
* 用户就得反复调同一件事。
|
|
10
|
+
*
|
|
11
|
+
* 学习票源是 `sized: 'user'` —— 前端拖完手柄盖的章,**不是模型能写的东西**
|
|
12
|
+
* (sanitizeObject 白名单收,模型走 patchBoard 也盖不出别人的章)。
|
|
13
|
+
*
|
|
14
|
+
* ## 为什么只学宽不学高
|
|
15
|
+
*
|
|
16
|
+
* 高度跟着正文长短走,一段三行的话和一段三十行的话不该一样高。用户拖高度调的是
|
|
17
|
+
* "这一块留多少空",那是**这一块**的属性,不是可以套到下一块上的偏好。宽度不同:
|
|
18
|
+
* 它是版心,跨块稳定。硬把高度也套过去,结果是每块板书都拖着一截空白。
|
|
19
|
+
*
|
|
20
|
+
* ## 取最近三块的中位数
|
|
21
|
+
*
|
|
22
|
+
* 板书 id 是 `notes/板书/<时间戳>-<slug>.md`,时间戳可直接字典序排 —— 不用另存时间。
|
|
23
|
+
* 取最近三块:一块就听它的(他刚调完,下一拍就该照做),三块取中位数(一次手滑
|
|
24
|
+
* 拖出个极端值不至于带偏后面所有的)。
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** 网格单位(px)。跟 write_on_board 的 width 参数同一把尺 */
|
|
28
|
+
const GRID = 24;
|
|
29
|
+
/** 学习窗口:最近几块用户亲手调过的板书 */
|
|
30
|
+
const WINDOW = 3;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {object} board board.json(sanitize 过的)
|
|
34
|
+
* @returns {number|null} 网格单位的宽;没有票就 null(调用方回落按正文估)
|
|
35
|
+
*/
|
|
36
|
+
export function learnedChalkWidth(board) {
|
|
37
|
+
const votes = Object.entries(board?.objects || {})
|
|
38
|
+
.filter(([id, e]) => e?.sized === 'user' && Number(e.w) > 0 && id.includes('/板书/'))
|
|
39
|
+
.sort(([a], [b]) => (a < b ? 1 : -1)) // id 里的时间戳倒序 = 最近的在前
|
|
40
|
+
.slice(0, WINDOW)
|
|
41
|
+
.map(([, e]) => Math.round(Number(e.w) / GRID));
|
|
42
|
+
if (!votes.length) return null;
|
|
43
|
+
const sorted = [...votes].sort((a, b) => a - b);
|
|
44
|
+
const mid = sorted[Math.floor(sorted.length / 2)];
|
|
45
|
+
// 回到 write_on_board 的合法区间(schema 是 8..60);超出就当没学到,别拿一个
|
|
46
|
+
// 会被 schema 打回来的值去顶替一个能用的估算
|
|
47
|
+
return mid >= 8 && mid <= 60 ? mid : null;
|
|
48
|
+
}
|
package/server/lib/chalk.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*
|
|
12
12
|
* frontmatter(只认这几个键,其余原样当正文):
|
|
13
13
|
* nd: chalk 标记(没有它就是普通便利贴)
|
|
14
|
-
* by: agent|user
|
|
14
|
+
* by: agent | user | rp-<slug>(常驻角色,2026-08-26)
|
|
15
15
|
* at: ISO 时间
|
|
16
16
|
* anchor: <canvas id> 这段话关于谁(落盘时顺手画 annotates 线)
|
|
17
17
|
* reply_to: <path> 回应哪一条板书(线程)
|
|
@@ -19,11 +19,26 @@
|
|
|
19
19
|
* session: <sid> (沿用便利贴的归属字段)
|
|
20
20
|
*/
|
|
21
21
|
import path from 'node:path';
|
|
22
|
+
import { ROLE_SLUG_RE } from '../engine/agent/cast.js';
|
|
22
23
|
import { promises as fs } from 'node:fs';
|
|
23
24
|
|
|
24
25
|
export const CHALK_DIR = 'notes/板书';
|
|
25
26
|
const FM_RE = /^---\n([\s\S]{0,800}?)\n---\n?/;
|
|
26
27
|
|
|
28
|
+
/**
|
|
29
|
+
* 板书的署名收成三类:`user` / `agent` / 常驻角色的 slug(`rp-*`)。
|
|
30
|
+
*
|
|
31
|
+
* 为什么不原样全收:`by` 会被前端拿去显示、被 read_board 拿去分段、将来还要被
|
|
32
|
+
* 收件箱拿去路由。收任意字符串等于给这几个读者一起开口子(而写这个值的是模型)。
|
|
33
|
+
* 认不出的一律落回 'agent' —— 老板书没有这个字段,落回去正好是它本来的语义。
|
|
34
|
+
*/
|
|
35
|
+
export function normalizeBy(raw) {
|
|
36
|
+
const v = String(raw || '').trim();
|
|
37
|
+
if (v === 'user') return 'user';
|
|
38
|
+
if (ROLE_SLUG_RE.test(v)) return v;
|
|
39
|
+
return 'agent';
|
|
40
|
+
}
|
|
41
|
+
|
|
27
42
|
export function parseChalk(raw) {
|
|
28
43
|
const m = FM_RE.exec(raw);
|
|
29
44
|
if (!m) return { body: raw, chalk: null, sessionId: null };
|
|
@@ -35,7 +50,7 @@ export function parseChalk(raw) {
|
|
|
35
50
|
body,
|
|
36
51
|
sessionId: get('session'),
|
|
37
52
|
chalk: {
|
|
38
|
-
by: get('by')
|
|
53
|
+
by: normalizeBy(get('by')),
|
|
39
54
|
at: get('at'),
|
|
40
55
|
anchor: get('anchor'),
|
|
41
56
|
replyTo: get('reply_to'),
|
|
@@ -50,7 +65,9 @@ export function renderChalk({ body, by = 'agent', at = new Date().toISOString(),
|
|
|
50
65
|
if (replyTo) lines.push(`reply_to: ${replyTo}`);
|
|
51
66
|
if (tag) lines.push(`tag: ${tag}`);
|
|
52
67
|
if (sessionId) lines.push(`session: ${sessionId}`);
|
|
53
|
-
|
|
68
|
+
// CR 系换行在唯一写口归一(2026-08-30):glm 真会话把换行写成 \r 系字面量,
|
|
69
|
+
// 整张表被压成一行还落了盘 —— CR 在板书正文里没有合法用途,进门就换成 \n。
|
|
70
|
+
lines.push('---', '', String(body).replace(/\r\n?/g, '\n').trim(), '');
|
|
54
71
|
return lines.join('\n');
|
|
55
72
|
}
|
|
56
73
|
|
|
@@ -79,6 +96,28 @@ export async function writeChalkFile(sharedRoot, fileName, content, { overwrite
|
|
|
79
96
|
return `${CHALK_DIR}/${name}`;
|
|
80
97
|
}
|
|
81
98
|
|
|
99
|
+
/**
|
|
100
|
+
* 软删(2026-08-25,信箱 iss_mt8cvn16:板书 rm 后无法恢复):删除入口一律把文件
|
|
101
|
+
* 挪进 `.nd/trash/<YYYYMMDD>/` 而不是 unlink —— `.nd/` 不上画布不进 git,用户体感
|
|
102
|
+
* 就是删了,但捞得回来。挪不动(跨盘等)才退回真删。
|
|
103
|
+
*/
|
|
104
|
+
export async function trashChalkFile(sharedRoot, absPath) {
|
|
105
|
+
try {
|
|
106
|
+
const day = new Date().toISOString().slice(0, 10).replace(/-/g, '');
|
|
107
|
+
const dir = path.join(sharedRoot, '.nd', 'trash', day);
|
|
108
|
+
await fs.mkdir(dir, { recursive: true });
|
|
109
|
+
let dest = path.join(dir, path.basename(absPath));
|
|
110
|
+
for (let i = 2; i < 100; i += 1) {
|
|
111
|
+
try { await fs.access(dest); dest = path.join(dir, path.basename(absPath).replace(/\.md$/, `-${i}.md`)); } catch { break; }
|
|
112
|
+
}
|
|
113
|
+
await fs.rename(absPath, dest);
|
|
114
|
+
return dest;
|
|
115
|
+
} catch {
|
|
116
|
+
try { await fs.unlink(absPath); } catch { /* 已经没了 */ }
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
82
121
|
/** 最近的板书(给注入用):按文件名(=时间戳)倒序,读前几条的首行 */
|
|
83
122
|
export async function recentChalk(sharedRoot, { limit = 8 } = {}) {
|
|
84
123
|
const dir = path.join(sharedRoot, CHALK_DIR);
|
|
@@ -145,7 +145,9 @@ export async function collectCard({ workspaceRoot, cardId }) {
|
|
|
145
145
|
const m = await taskManifest(workspaceRoot).catch(() => null);
|
|
146
146
|
const rootSite = (m?.artifacts || []).find(a => a.kind === 'site' && !a.single && !a.srcRoot);
|
|
147
147
|
for (const pg of (rootSite?.pages || [])) {
|
|
148
|
-
|
|
148
|
+
// ⚠️ pages 是**产物根相对**(构建型根站 root='dist' 时是 'index.html' 不是
|
|
149
|
+
// 'dist/index.html')——对着工作区根解析会抓到源文件或落空(08-24 对齐)
|
|
150
|
+
const pRel = (rootSite.root ? `${rootSite.root}/` : '') + String(pg).replace(/\\/g, '/');
|
|
149
151
|
const pAbs = await safeResolve(workspaceRoot, pRel);
|
|
150
152
|
if (!pAbs) continue;
|
|
151
153
|
try { if ((await fs.stat(pAbs)).isFile()) files.push({ abs: pAbs, rel: pRel }); } catch { /* 页面刚被删 */ }
|
|
@@ -21,6 +21,9 @@
|
|
|
21
21
|
import http from 'node:http';
|
|
22
22
|
import https from 'node:https';
|
|
23
23
|
import { toOpenAIChatRequest, fromOpenAIChatResponse, toAnthropicError, OpenAIToAnthropicSSE, truncationOfChatResponse } from './openai-chat.js';
|
|
24
|
+
import { upstreamCostOf } from './upstream-billing.js';
|
|
25
|
+
import { upstreamErrorHint } from './upstream-error-hints.js';
|
|
26
|
+
import { armIdleWatchdog } from './stream-watchdog.js';
|
|
24
27
|
|
|
25
28
|
export const DEFAULT_EMPTY_RETRIES = 2;
|
|
26
29
|
export const DEFAULT_RETRY_BUDGET_MS = 120_000;
|
|
@@ -53,7 +56,7 @@ export function retryBudgetMs(env = process.env) {
|
|
|
53
56
|
export function forwardOpenAIChat({ parsed, wire, key, res, sidShort, target, path, agent, onOutcome = () => {}, onBilling = () => {}, onTruncated = () => {}, onNotice = () => {} }) {
|
|
54
57
|
const wantStream = !!parsed.stream;
|
|
55
58
|
const label = wire.upstream?.label || wire.upstreamId;
|
|
56
|
-
const body = toOpenAIChatRequest(parsed, { reasoningEffort: wire.reasoningEffort, maxOutput: wire.maxOutput });
|
|
59
|
+
const body = toOpenAIChatRequest(parsed, { reasoningEffort: wire.reasoningEffort, maxOutput: wire.maxOutput, bodyExtra: wire.bodyExtra });
|
|
57
60
|
const outBody = Buffer.from(JSON.stringify(body), 'utf8');
|
|
58
61
|
const useHttps = target.protocol === 'https:';
|
|
59
62
|
const headers = {
|
|
@@ -214,7 +217,8 @@ export function forwardOpenAIChat({ parsed, wire, key, res, sidShort, target, pa
|
|
|
214
217
|
if (settled || dead) return;
|
|
215
218
|
settled = true;
|
|
216
219
|
currentReq = null;
|
|
217
|
-
|
|
220
|
+
// 上游 4xx 的英文原文会原样落到用户的聊天框里 —— 能给出路的那几条翻成人话(原文保留在括号里)
|
|
221
|
+
const msg = upstreamErrorHint(text, wire) || (text?.trim() ? text : `${label} 上游返回 ${status}(模型暂时不可用,稍后再发一次)`);
|
|
218
222
|
console.warn(`[model-ingress] sid=${sidShort} upstream=${wire.upstreamId} ${status} model=${wire.wireModel} body=${String(text || '').slice(0, 200).replace(/\s+/g, ' ')}`);
|
|
219
223
|
failHard(status, msg, `HTTP ${status}${streaming ? ' on retry' : ''}`);
|
|
220
224
|
};
|
|
@@ -234,6 +238,12 @@ export function forwardOpenAIChat({ parsed, wire, key, res, sidShort, target, pa
|
|
|
234
238
|
proxyRes.on('aborted', () => attemptOver('aborted'));
|
|
235
239
|
proxyRes.on('error', (err) => attemptOver(err.code || err.message));
|
|
236
240
|
proxyRes.on('end', () => attemptOver(null));
|
|
241
|
+
// 断流看门狗(2026-08-29):destroy 触发上面的 'error' → attemptOver 走
|
|
242
|
+
// 现成的判决/重发机 —— 不新开收尾路,只补「静默也算死」这一种死法。
|
|
243
|
+
armIdleWatchdog(proxyRes, { onIdle: (silentMs) => {
|
|
244
|
+
console.warn(`[model-ingress] sid=${sidShort} upstream=${wire.upstreamId} 流静默 ${Math.round(silentMs / 1000)}s —— 看门狗掐断死流`);
|
|
245
|
+
try { proxyRes.destroy(new Error('stream idle watchdog')); } catch { /* 已经死了就算了 */ }
|
|
246
|
+
} });
|
|
237
247
|
proxyRes.pipe(xf, { end: false }); // end:false —— 这条 SSE 还要接着用(可能再打一发)
|
|
238
248
|
}, (detail) => attemptOver(detail, false)); // 连不上 / RST:同一条判决路,绝不自己碰 res
|
|
239
249
|
};
|
|
@@ -265,7 +275,8 @@ export function forwardOpenAIChat({ parsed, wire, key, res, sidShort, target, pa
|
|
|
265
275
|
const text = Buffer.concat(chunks).toString('utf8');
|
|
266
276
|
if (status >= 400) {
|
|
267
277
|
console.warn(`[model-ingress] sid=${sidShort} upstream=${wire.upstreamId} ${status} model=${wire.wireModel} body=${text.slice(0, 200).replace(/\s+/g, ' ')}`);
|
|
268
|
-
|
|
278
|
+
// ⚠️ 跟上面流式那条是**孪生的两条路**,翻译要两边都挂(生产走流式,探针走非流式,只补一边会以为修好了)
|
|
279
|
+
const msg = upstreamErrorHint(text, wire) || (text.trim() ? text : `${label} 上游返回 ${status}(模型暂时不可用,稍后再发一次)`);
|
|
269
280
|
onOutcome(false, `HTTP ${status}`);
|
|
270
281
|
const errBody = JSON.stringify(toAnthropicError(status, msg));
|
|
271
282
|
res.writeHead(status, { 'Content-Type': 'application/json', 'Content-Length': String(Buffer.byteLength(errBody)) });
|
|
@@ -292,7 +303,8 @@ export function forwardOpenAIChat({ parsed, wire, key, res, sidShort, target, pa
|
|
|
292
303
|
}
|
|
293
304
|
onOutcome(true);
|
|
294
305
|
onTruncated(truncationOfChatResponse(upstreamJson));
|
|
295
|
-
|
|
306
|
+
const costUsd = upstreamCostOf(upstreamJson); // Zen 放顶层、Merge 网关放 usage.cost
|
|
307
|
+
if (costUsd != null || upstreamJson.usage) onBilling({ costUsd, usage: upstreamJson.usage || null });
|
|
296
308
|
const respBody = JSON.stringify(out);
|
|
297
309
|
res.writeHead(200, { 'Content-Type': 'application/json', 'Content-Length': String(Buffer.byteLength(respBody)) });
|
|
298
310
|
res.end(respBody);
|
|
@@ -4,10 +4,12 @@
|
|
|
4
4
|
* ## 为什么有这层
|
|
5
5
|
*
|
|
6
6
|
* SDK binary 永远说 Anthropic Messages;model-ingress 以前只会**转发**(上游也说 Anthropic)。
|
|
7
|
-
* OpenCode Zen
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* OpenCode Zen 整家**只有 OpenAI chat 格式能用工具**(它给模型架的 /v1/messages 桥一带 tools
|
|
8
|
+
* 就 [1210],08-21 拿当时的 Ox Alpha 四种写法探死),newapi 中转站、NVIDIA build 同病。所以协议
|
|
9
|
+
* 映射得自己做 —— 不上 gproxy(外部守护进程 + 四个已知洞 + 第二个 quirk 真相源),在 ingress 里按
|
|
10
|
+
* 上游 `protocol: 'openai-chat'` 分岔,其余上游一字不动。
|
|
11
|
+
* ⭐ 逼出这层的 Ox 08-26 就下架了,这层照常服务着 zen/zenGo/nvidia 三家 —— **闸和转换层才是耐久
|
|
12
|
+
* 资产,模型行是插件**(08-21 加 Ox 时写下的判断,一周后应验)。
|
|
11
13
|
*
|
|
12
14
|
* ## 映射要点(都是探针实测逼出来的,不是抄规范)
|
|
13
15
|
*
|
|
@@ -16,7 +18,7 @@
|
|
|
16
18
|
* 文本混排 → 先吐 role:tool 条,再吐 role:user 条(文本 + 提出来的图)
|
|
17
19
|
* - thinking 块不回传(没有 signature 机制);assistant 历史里的 thinking 合成 reasoning_content
|
|
18
20
|
* (models.dev 标 interleaved.field=reasoning_content,回传给模型接着想)
|
|
19
|
-
* - Anthropic thinking 参数 → reasoning_effort(行内 reasoningEffort
|
|
21
|
+
* - Anthropic thinking 参数 → reasoning_effort(行内 reasoningEffort;zen 系普遍是 low|high|max 三档,**没有 medium**)
|
|
20
22
|
* - 流式:OpenAI chunk → 合成 message_start / content_block_* / message_delta / message_stop;
|
|
21
23
|
* usage 在最后一个 chunk(stream_options.include_usage),Anthropic 口径 input 不含 cache 命中
|
|
22
24
|
* - stop_reason:tool_calls→tool_use · stop→end_turn · length→max_tokens;有 tool_calls 但
|
|
@@ -34,6 +36,7 @@
|
|
|
34
36
|
* 会话直接判死 —— 现有的 content_filter→end_turn 在 Claude Code 语境下才是对的
|
|
35
37
|
*/
|
|
36
38
|
import { Transform } from 'node:stream';
|
|
39
|
+
import { upstreamCostOf } from './upstream-billing.js';
|
|
37
40
|
|
|
38
41
|
const STOP_MAP = { tool_calls: 'tool_use', stop: 'end_turn', length: 'max_tokens', content_filter: 'end_turn', function_call: 'tool_use' };
|
|
39
42
|
|
|
@@ -97,7 +100,7 @@ function splitToolResult(block) {
|
|
|
97
100
|
|
|
98
101
|
/**
|
|
99
102
|
* @param {object} parsed Anthropic Messages body(已过 transformForUpstream:model 已是 wireModel)
|
|
100
|
-
* @param {{ reasoningEffort?: string, maxOutput?: number }} opts
|
|
103
|
+
* @param {{ reasoningEffort?: string, maxOutput?: number, bodyExtra?: object|null }} opts
|
|
101
104
|
* @returns {object} OpenAI chat.completions body
|
|
102
105
|
*/
|
|
103
106
|
export function toOpenAIChatRequest(parsed, opts = {}) {
|
|
@@ -169,6 +172,9 @@ export function toOpenAIChatRequest(parsed, opts = {}) {
|
|
|
169
172
|
// 档位只看行内 reasoningEffort:Anthropic 的 thinking 字段在进到这里之前已被 transformForUpstream
|
|
170
173
|
// 按行内 thinking:'strip' 删掉(fable 评审抓的:以前以它存在为前提,档位从没发出去过)
|
|
171
174
|
if (opts.reasoningEffort && parsed.thinking?.type !== 'disabled') out.reasoning_effort = opts.reasoningEffort;
|
|
175
|
+
// 上游要的额外顶层字段(merge 的 `vendor` 点名)。最后合、且允许盖上面任何一个键:
|
|
176
|
+
// 它是行里明写的上游特配,比这里推导出来的默认值更该赢
|
|
177
|
+
if (opts.bodyExtra) Object.assign(out, opts.bodyExtra);
|
|
172
178
|
return out;
|
|
173
179
|
}
|
|
174
180
|
|
|
@@ -200,7 +206,10 @@ export function fromOpenAIChatResponse(json) {
|
|
|
200
206
|
const choice = json.choices[0] || {};
|
|
201
207
|
const m = choice.message || {};
|
|
202
208
|
const content = [];
|
|
203
|
-
|
|
209
|
+
// 思考文本的字段名各家不一样:zen 系是 reasoning_content(models.dev 的 interleaved.field),
|
|
210
|
+
// Merge 网关是 `thinking`(另带 thinking_signature,我们不回传签名所以不取)。先认前者,回退后者。
|
|
211
|
+
const reasoning = m.reasoning_content || m.thinking;
|
|
212
|
+
if (reasoning) content.push({ type: 'thinking', thinking: String(reasoning), signature: '' });
|
|
204
213
|
if (m.content) content.push({ type: 'text', text: String(m.content) });
|
|
205
214
|
if (m.refusal) content.push({ type: 'text', text: String(m.refusal) });
|
|
206
215
|
for (const c of m.tool_calls || []) {
|
|
@@ -270,7 +279,7 @@ export class OpenAIToAnthropicSSE extends Transform {
|
|
|
270
279
|
this.sawText = false; // 有过可见正文(区分"只想没说"的早断流)
|
|
271
280
|
this.failReason = null; // 本次以 error 事件收场的原因(forward 层据此记会话失败计数;null = 正常收尾)
|
|
272
281
|
this.truncated = null; // 本次「半截」收场的原因(见 truncationReason;forward 层据此报给 session-loop 续接)
|
|
273
|
-
this.cost = null; // 上游报的本次费用(美元,/zen/go 在 [DONE]
|
|
282
|
+
this.cost = null; // 上游报的本次费用(美元,/zen/go 在 [DONE] 后补顶层 cost、Merge 网关放 usage.cost;null = 上游没报)
|
|
274
283
|
this.doneSeen = false; // 见过 [DONE]:之后只收 cost,收尾等 _flush
|
|
275
284
|
this.attempts = 1; // 这条 SSE 一共打了几发上游(就地重发会 ++)
|
|
276
285
|
this.attemptUsage = null; // 这一发的 usage
|
|
@@ -305,13 +314,15 @@ export class OpenAIToAnthropicSSE extends Transform {
|
|
|
305
314
|
_handleChunk(chunk) {
|
|
306
315
|
this._ensureStart(chunk);
|
|
307
316
|
if (chunk.usage) this.attemptUsage = chunk.usage;
|
|
308
|
-
|
|
317
|
+
const chunkCost = upstreamCostOf(chunk); // 顶层 cost(Zen)或 usage.cost(Merge 网关)
|
|
318
|
+
if (chunkCost != null) this.attemptCost = chunkCost;
|
|
309
319
|
const choice = chunk.choices?.[0];
|
|
310
320
|
if (!choice) return;
|
|
311
321
|
const d = choice.delta || {};
|
|
312
|
-
|
|
322
|
+
const reasoningDelta = d.reasoning_content || d.thinking; // 同上:Merge 网关的增量字段叫 thinking
|
|
323
|
+
if (reasoningDelta) {
|
|
313
324
|
if (this.open?.kind !== 'thinking') this._openBlock('thinking', { type: 'thinking', thinking: '', signature: '' });
|
|
314
|
-
this._emit('content_block_delta', { type: 'content_block_delta', index: this.open.index, delta: { type: 'thinking_delta', thinking: String(
|
|
325
|
+
this._emit('content_block_delta', { type: 'content_block_delta', index: this.open.index, delta: { type: 'thinking_delta', thinking: String(reasoningDelta) } });
|
|
315
326
|
}
|
|
316
327
|
// content 与 refusal 都是"可见正文",进同一个 text 块(拒答也是模型说的话,原样吐给用户)
|
|
317
328
|
for (const piece of [d.content, d.refusal]) {
|
|
@@ -471,7 +482,7 @@ export class OpenAIToAnthropicSSE extends Transform {
|
|
|
471
482
|
if (payload === '[DONE]') { this.doneSeen = true; continue; }
|
|
472
483
|
let j;
|
|
473
484
|
try { j = JSON.parse(payload); } catch { continue; }
|
|
474
|
-
if (this.doneSeen || this.done) {
|
|
485
|
+
if (this.doneSeen || this.done) { const c = upstreamCostOf(j); if (c != null) this.attemptCost = c; continue; }
|
|
475
486
|
if (j?.error) { // 流中途的错误体:转成 Anthropic error 事件
|
|
476
487
|
this._ensureStart(j);
|
|
477
488
|
this._closeOpen(); // 先把开着的块闭合再发 error(顺序反了会出现 error 后面还跟 content_block_stop)
|
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
* 成功转发不留日志。表级断言封不住(SDK 内部会用哪些名字没法枚举),所以是**会话级路由**:一个会话只认
|
|
12
12
|
* 自己那行和自己的 fast 行,其它一概改道 fast 兜底,绝不跨行。
|
|
13
13
|
*
|
|
14
|
-
* 08-22
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* 08-22 起**会话优先**而不是全表优先:不写 sdkAlias 的行(外部插槽全部 + 08-25 起的内置新行)共用同一个
|
|
15
|
+
* spoof 名(model-table.js SHARED_SDK_ALIAS),全表反查分不出它们,只有会话知道自己是谁。先问会话、再查全表,
|
|
16
|
+
* 对独占别名的行结果不变(自己的 alias 反查到的就是自己)。
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import { resolveWireModel, resolveModelRoute, wireNamesOf } from '../../engine/agent/model-context.js';
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* server/lib/ingress/stream-watchdog.js —— 上游流断死看门狗(2026-08-29)
|
|
3
|
+
*
|
|
4
|
+
* 现场(proj_mtexu1kp 纸范式验收当晚):glm-5.3-flash-zai 在开始流 tool_use 入参时
|
|
5
|
+
* 断粮 —— 没有错误、没有 EOF、没有日志,TCP 就那么开着不再来字节。run 无限挂起,
|
|
6
|
+
* 前端还留着一张流到一半的工具卡,直到用户手动中断。**用户不该替我们当看门狗。**
|
|
7
|
+
*
|
|
8
|
+
* 判据:SSE 的正常节奏是亚秒级 chunk(thinking 长停顿也有事件/ping 垫着),静默
|
|
9
|
+
* 超过 IDLE_MS = 死流。掐掉(destroy)让下游走各自现成的错误路:
|
|
10
|
+
* - Anthropic 直连透传路:CLI 收到流错误 → 自己重试
|
|
11
|
+
* - openai-chat 转换路:proxyRes 'error' → attemptOver → 就地重发/判决机
|
|
12
|
+
*
|
|
13
|
+
* 只对流式响应上岗(非流式的整包响应可以长时间零字节,是合法的慢)。
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const DEFAULT_IDLE_MS = 180_000;
|
|
17
|
+
const CHECK_MS = 15_000;
|
|
18
|
+
|
|
19
|
+
export function idleMsFromEnv() {
|
|
20
|
+
const n = Number(process.env.NODESIGN_INGRESS_IDLE_MS);
|
|
21
|
+
return Number.isFinite(n) && n >= 1_000 ? n : DEFAULT_IDLE_MS;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 给一条上游响应流装看门狗。返回解除函数(幂等)。
|
|
26
|
+
* @param {import('http').IncomingMessage} stream 上游响应
|
|
27
|
+
* @param {{ idleMs?: number, checkMs?: number, onIdle: (silentMs: number) => void }} opts
|
|
28
|
+
* onIdle 只会被调用一次;由调用方决定怎么掐(destroy)和记账(noteOutcome/日志)。
|
|
29
|
+
*/
|
|
30
|
+
export function armIdleWatchdog(stream, { idleMs = idleMsFromEnv(), checkMs = CHECK_MS, onIdle }) {
|
|
31
|
+
let last = Date.now();
|
|
32
|
+
let fired = false;
|
|
33
|
+
const bump = () => { last = Date.now(); };
|
|
34
|
+
stream.on('data', bump);
|
|
35
|
+
const timer = setInterval(() => {
|
|
36
|
+
if (fired) return;
|
|
37
|
+
const silent = Date.now() - last;
|
|
38
|
+
if (silent > idleMs) {
|
|
39
|
+
fired = true;
|
|
40
|
+
stop();
|
|
41
|
+
try { onIdle(silent); } catch { /* 看门狗自己绝不抛 */ }
|
|
42
|
+
}
|
|
43
|
+
}, checkMs);
|
|
44
|
+
timer.unref?.();
|
|
45
|
+
function stop() {
|
|
46
|
+
clearInterval(timer);
|
|
47
|
+
stream.removeListener('data', bump);
|
|
48
|
+
}
|
|
49
|
+
stream.on('end', stop);
|
|
50
|
+
stream.on('close', stop);
|
|
51
|
+
stream.on('error', stop);
|
|
52
|
+
return stop;
|
|
53
|
+
}
|
|
@@ -12,6 +12,21 @@
|
|
|
12
12
|
* 上游没报 cost(cost 为 null)就不动任何东西 —— 假数据比没有更坏。
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* 上游把「这一发花了多少钱」放在哪儿,各家不一样:Zen(/zen/go)放在**顶层** `cost`
|
|
17
|
+
* (流式在 [DONE] 之后补一条 {"choices":[],"cost":"0.00123"}),Merge 网关放在 **usage.cost** 里
|
|
18
|
+
* (非流式在响应体的 usage 上,流式在 include_usage 那个末块的 usage 上)。两处都认,顶层优先。
|
|
19
|
+
* ⚠️ 缺席或不是数一律 null —— 记账那侧看见 null 就不动任何东西(假数据比没有更坏)。
|
|
20
|
+
*/
|
|
21
|
+
export function upstreamCostOf(obj) {
|
|
22
|
+
for (const v of [obj?.cost, obj?.usage?.cost]) {
|
|
23
|
+
if (v == null) continue;
|
|
24
|
+
const n = Number(v);
|
|
25
|
+
if (Number.isFinite(n)) return n;
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
15
30
|
export class UpstreamBilling {
|
|
16
31
|
constructor() { this.map = new Map(); } // sid → Map<appModel, acc>
|
|
17
32
|
/**
|