@xiaobuyu/nodesign 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +156 -106
- package/bin/nodesign.js +0 -0
- package/package.json +1 -1
- package/server/api/README.md +45 -45
- package/server/api/_guard.js +75 -75
- package/server/api/admin.js +187 -187
- package/server/api/assets/docx-page.js +103 -103
- package/server/api/assets/helpers.js +42 -22
- package/server/api/assets/notes.js +194 -144
- package/server/api/assets.js +891 -896
- package/server/api/board.js +134 -57
- package/server/api/browse.js +138 -138
- package/server/api/canvas.js +444 -444
- package/server/api/chatai.js +324 -324
- package/server/api/exports/build-standalone.js +980 -980
- package/server/api/exports/cards.js +118 -118
- package/server/api/exports/docx-pdf.js +46 -46
- package/server/api/exports/export-page.js +142 -142
- package/server/api/exports/handoff.js +147 -147
- package/server/api/exports.js +690 -690
- package/server/api/instruction.js +75 -75
- package/server/api/me.js +116 -116
- package/server/api/memory.js +185 -185
- package/server/api/pending-changes.js +366 -366
- package/server/api/plugins.js +164 -164
- package/server/api/projects.js +220 -220
- package/server/api/publish.js +64 -64
- package/server/api/recent.js +73 -73
- package/server/api/sessions.js +589 -589
- package/server/api/skills.js +77 -77
- package/server/api/standalone-fit.js +168 -168
- package/server/api/turn-compose.js +180 -180
- package/server/api/turn-inflight.js +44 -44
- package/server/auth/README.md +17 -17
- package/server/auth/middleware.js +138 -138
- package/server/auth/origin-guard.js +71 -71
- package/server/auth/session.js +108 -108
- package/server/auth/tier.js +113 -113
- package/server/auth/users-store.js +348 -348
- package/server/edit/README.md +29 -29
- package/server/engine/README.md +113 -113
- package/server/engine/agent/agent-shared.js +550 -550
- package/server/engine/agent/auto-mode-default-hard-deny.txt +4 -4
- package/server/engine/agent/auto-mode-rules.js +78 -78
- package/server/engine/agent/context.js +418 -418
- package/server/engine/agent/events.js +465 -464
- package/server/engine/agent/hooks/canvas-validate.js +199 -199
- package/server/engine/agent/hooks/failure.js +120 -120
- package/server/engine/agent/hooks/file-events.js +75 -75
- package/server/engine/agent/hooks/lifecycle.js +233 -233
- package/server/engine/agent/hooks/post-canvas-focus.js +80 -80
- package/server/engine/agent/hooks/post-guidance.js +206 -206
- package/server/engine/agent/hooks/post-subagent-report.js +104 -104
- package/server/engine/agent/hooks/post-trim.js +58 -58
- package/server/engine/agent/hooks/pre-board-neighborhood.js +34 -34
- package/server/engine/agent/hooks/pre-defaults.js +84 -84
- package/server/engine/agent/hooks/pre-injectors.js +277 -277
- package/server/engine/agent/hooks/pre-performance-log-guard.js +78 -78
- package/server/engine/agent/hooks/pre-starter-files.js +61 -61
- package/server/engine/agent/hooks/pre-workspace-scope-guard.js +78 -78
- package/server/engine/agent/hooks/site-validate.js +94 -94
- package/server/engine/agent/hooks/tool-prompts.js +31 -31
- package/server/engine/agent/hooks/turn-state-memory.js +56 -56
- package/server/engine/agent/hooks/user-prompt-submit.js +258 -215
- package/server/engine/agent/hooks.js +354 -354
- package/server/engine/agent/init-contract.js +69 -69
- package/server/engine/agent/isolation.js +159 -159
- package/server/engine/agent/model-table.js +292 -292
- package/server/engine/agent/plugin-loader.js +266 -266
- package/server/engine/agent/prompts/nodesign-prelude.md +388 -336
- package/server/engine/agent/prompts/tools/ask-user-question-protocol.md +71 -71
- package/server/engine/agent/prompts/tools/direct-edit-protocol.md +98 -98
- package/server/engine/agent/prompts/tools/generate-image-cookbook.gemini-gateway.md +583 -583
- package/server/engine/agent/prompts/tools/generate-image-cookbook.md +483 -483
- package/server/engine/agent/prompts/tools/hybrid-reference.md +100 -100
- package/server/engine/agent/prompts/tools/paint-still-cookbook.md +270 -270
- package/server/engine/agent/prompts/tools/roll-film-cookbook.md +70 -70
- package/server/engine/agent/prompts/tools/site-reference.md +106 -106
- package/server/engine/agent/prompts/tools/tweaks-syntax.md +125 -125
- package/server/engine/agent/prompts/tools/vision-checker-dispatch.md +48 -48
- package/server/engine/agent/session-loop.js +1035 -1024
- package/server/engine/agent/session-model.js +164 -164
- package/server/engine/agent/skill.js +204 -204
- package/server/engine/agent/task-events.js +78 -78
- package/server/engine/agents/ds-extractor.md +132 -132
- package/server/engine/agents/explorer.md +190 -190
- package/server/engine/agents/index.js +230 -230
- package/server/engine/agents/schemas/design-system.json +156 -156
- package/server/engine/agents/schemas/tweak-schema.json +113 -113
- package/server/engine/agents/tweak-proposer.md +127 -127
- package/server/engine/agents/vision-checker.md +254 -254
- package/server/engine/browse/capture.js +422 -422
- package/server/engine/browse/card.js +137 -137
- package/server/engine/browse/handover.js +59 -59
- package/server/engine/browse/page-digest.js +119 -119
- package/server/engine/browse/refs.js +196 -196
- package/server/engine/browse/registry.js +307 -307
- package/server/engine/browse/screencast.js +170 -170
- package/server/engine/browse/state.js +121 -121
- package/server/engine/chatai/chat-log.js +79 -79
- package/server/engine/chatai/index.js +193 -193
- package/server/engine/chatai/openai-compat.js +152 -152
- package/server/engine/chatai/orchestrate.js +309 -309
- package/server/engine/chatai/perform.js +40 -40
- package/server/engine/chatai/summarize.js +79 -79
- package/server/engine/mcp/capability-gate.js +52 -52
- package/server/engine/mcp/index.js +351 -339
- package/server/engine/mcp/param-sanitizer.js +142 -142
- package/server/engine/mcp/tools/arrange-on-board.js +124 -124
- package/server/engine/mcp/tools/artifact-session.js +278 -278
- package/server/engine/mcp/tools/browse-computer.js +421 -421
- package/server/engine/mcp/tools/browse-find-batch.js +183 -183
- package/server/engine/mcp/tools/browse-screenshot.js +157 -157
- package/server/engine/mcp/tools/browse.js +480 -480
- package/server/engine/mcp/tools/build-docx.js +101 -101
- package/server/engine/mcp/tools/clear-pending-changes.js +99 -99
- package/server/engine/mcp/tools/create-on-board.js +152 -155
- package/server/engine/mcp/tools/crystallize-skill.js +168 -168
- package/server/engine/mcp/tools/edit-sketch.js +167 -0
- package/server/engine/mcp/tools/explain-style.js +237 -237
- package/server/engine/mcp/tools/export-handoff.js +133 -133
- package/server/engine/mcp/tools/expose-tweaks.js +149 -149
- package/server/engine/mcp/tools/generate-image.js +842 -842
- package/server/engine/mcp/tools/get-computed-styles.js +164 -164
- package/server/engine/mcp/tools/get-pending-changes.js +205 -205
- package/server/engine/mcp/tools/h3box-ssh.js +84 -84
- package/server/engine/mcp/tools/helpers/acquire-page.js +82 -82
- package/server/engine/mcp/tools/helpers/motion-lab.js +554 -554
- package/server/engine/mcp/tools/helpers/motion-scroll.js +112 -112
- package/server/engine/mcp/tools/helpers/perception-page.js +215 -215
- package/server/engine/mcp/tools/helpers/reference-download.js +103 -103
- package/server/engine/mcp/tools/helpers/rembg-bridge.py +80 -80
- package/server/engine/mcp/tools/helpers/shot-pipeline.js +280 -280
- package/server/engine/mcp/tools/highlight.js +68 -68
- package/server/engine/mcp/tools/list-pages.js +222 -222
- package/server/engine/mcp/tools/look-at-board.js +124 -0
- package/server/engine/mcp/tools/lookup-tags.js +59 -59
- package/server/engine/mcp/tools/navigate-to-page.js +60 -60
- package/server/engine/mcp/tools/organize-board.js +64 -64
- package/server/engine/mcp/tools/paint-still.js +434 -434
- package/server/engine/mcp/tools/pin-to-board.js +130 -130
- package/server/engine/mcp/tools/preview-deck.js +130 -130
- package/server/engine/mcp/tools/profile-scroll.js +305 -305
- package/server/engine/mcp/tools/publish-site.js +102 -102
- package/server/engine/mcp/tools/query-elements.js +195 -195
- package/server/engine/mcp/tools/read-board.js +190 -103
- package/server/engine/mcp/tools/read-document.js +93 -93
- package/server/engine/mcp/tools/read-page.js +264 -264
- package/server/engine/mcp/tools/read-tavern-json.js +143 -143
- package/server/engine/mcp/tools/read-user-view.js +59 -0
- package/server/engine/mcp/tools/record-decision.js +140 -140
- package/server/engine/mcp/tools/relate-on-board.js +112 -110
- package/server/engine/mcp/tools/report-issue.js +115 -115
- package/server/engine/mcp/tools/roll-film.js +266 -266
- package/server/engine/mcp/tools/screenshot-docx.js +138 -138
- package/server/engine/mcp/tools/screenshot-url.js +195 -195
- package/server/engine/mcp/tools/screenshot.js +583 -583
- package/server/engine/mcp/tools/sketch-on-board.js +337 -0
- package/server/engine/mcp/tools/tier-gate.js +96 -96
- package/server/engine/mcp/tools/trace-motion.js +215 -215
- package/server/engine/mcp/tools/web-search.js +548 -548
- package/server/engine/mcp/tools/write-on-board.js +130 -0
- package/server/engine/motion/inventory.js +349 -349
- package/server/engine/perception/session.js +235 -235
- package/server/engine/plugins/nodesign/.claude-plugin/plugin.json +5 -5
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/SKILL.md +199 -192
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/canvas.template.html +667 -667
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/hybrid-grid.md +22 -22
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/image-led-cover.md +25 -25
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/portrait.md +23 -23
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/quote-backdrop.md +21 -21
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/section-divider.md +23 -23
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/text-led.md +21 -21
- package/server/engine/plugins/nodesign/skills/docx-craft/SKILL.md +169 -162
- package/server/engine/plugins/nodesign/skills/docx-craft/references/token-schema.md +407 -407
- package/server/engine/plugins/nodesign/skills/docx-craft//346/226/207/346/241/243.template.json +80 -80
- package/server/engine/plugins/nodesign/skills/rp-craft/SKILL.md +235 -228
- package/server/engine/plugins/nodesign/skills/rp-craft/patterns//346/274/224/345/207/272/351/241/265-/346/234/200/345/260/217/345/256/236/347/216/260.html +78 -78
- package/server/engine/plugins/nodesign/skills/rp-craft//346/274/224/345/207/272.template.js +268 -268
- package/server/engine/plugins/nodesign/skills/site-craft/SKILL.md +330 -323
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/build-lane.md +134 -134
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/cutout-collage.md +61 -61
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/mock-app.md +163 -163
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/page-transitions.md +86 -86
- package/server/engine/runs/active-runs.js +698 -698
- package/server/engine/runs/board-tasklist.js +130 -0
- package/server/engine/runs/live-turn.js +252 -252
- package/server/engine/runs/store.js +364 -364
- package/server/engine/runs/turn-relay.js +217 -217
- package/server/engine/runtime/workspace.js +164 -164
- package/server/index.js +3 -0
- package/server/lib/artifact-file-path.js +80 -80
- package/server/lib/artifact-target.js +284 -284
- package/server/lib/asset-refs.js +239 -239
- package/server/lib/async-queue.js +104 -104
- package/server/lib/auto-relations.js +130 -130
- package/server/lib/binding-types.js +72 -60
- package/server/lib/board-graph-export.js +191 -0
- package/server/lib/board-groups.js +167 -0
- package/server/lib/board-hero.js +76 -0
- package/server/lib/board-kind-sizes.js +85 -62
- package/server/lib/board-relations.js +133 -133
- package/server/lib/browse-proxy.js +180 -180
- package/server/lib/canvas-id.js +36 -36
- package/server/lib/chalk.js +121 -0
- package/server/lib/cover.js +227 -227
- package/server/lib/danbooru-tags.js +291 -291
- package/server/lib/doc-extract.js +156 -156
- package/server/lib/docx/build-from-source.js +260 -260
- package/server/lib/docx/build.js +490 -490
- package/server/lib/docx/dump-styles.js +287 -287
- package/server/lib/docx/fonts/nodesign-cjk.conf +164 -164
- package/server/lib/docx/merge-runs.js +129 -129
- package/server/lib/docx/numbering.js +218 -218
- package/server/lib/docx/order.js +152 -152
- package/server/lib/docx/rawzip.js +172 -172
- package/server/lib/docx/render.js +92 -92
- package/server/lib/docx/text-lint.js +225 -225
- package/server/lib/docx/tokens.js +335 -335
- package/server/lib/docx/units.js +29 -29
- package/server/lib/docx/xml.js +291 -291
- package/server/lib/docx-pages.js +187 -187
- package/server/lib/export-collect.js +289 -289
- package/server/lib/export-package.js +210 -210
- package/server/lib/html-srcset.js +145 -145
- package/server/lib/image-variant.js +460 -460
- package/server/lib/ingress/forward-openai-chat.js +305 -305
- package/server/lib/ingress/openai-chat.js +491 -491
- package/server/lib/ingress/session-notice.js +66 -66
- package/server/lib/ingress/session-routes.js +68 -68
- package/server/lib/ingress/slot-probe.js +130 -130
- package/server/lib/ingress/upstream-billing.js +52 -52
- package/server/lib/ingress/upstream-fail-streak.js +72 -72
- package/server/lib/ingress/upstream-truncation.js +48 -48
- package/server/lib/issues-store.js +182 -182
- package/server/lib/kinds/deck.js +72 -72
- package/server/lib/kinds/docx.js +283 -283
- package/server/lib/kinds/file-kinds.js +113 -113
- package/server/lib/kinds/index.js +202 -202
- package/server/lib/kinds/site.js +161 -161
- package/server/lib/model-ingress.js +543 -543
- package/server/lib/moderation.js +255 -255
- package/server/lib/notice-store.js +103 -103
- package/server/lib/plugin-install.js +104 -104
- package/server/lib/plugin-validator.js +721 -721
- package/server/lib/publish-store.js +115 -115
- package/server/lib/quick-summary.js +52 -52
- package/server/lib/quota.js +270 -270
- package/server/lib/rate-window.js +29 -29
- package/server/lib/reference-assets.js +92 -92
- package/server/lib/region-shot.js +135 -135
- package/server/lib/safe-path.js +73 -73
- package/server/lib/sdk-session.js +33 -33
- package/server/lib/showcase-store.js +92 -92
- package/server/lib/site-publish.js +465 -465
- package/server/lib/sketch-layout.js +277 -0
- package/server/lib/ssrf-guard.js +432 -432
- package/server/lib/task-scan.js +158 -158
- package/server/lib/tavern-json.js +154 -154
- package/server/lib/video-variant.js +237 -237
- package/server/lib/workspace-path.js +33 -33
- package/server/ops/fix-sdk-musl.mjs +40 -40
- package/server/ops/install-macos-fonts.sh +114 -114
- package/server/ops/macos-fonts.conf +336 -336
- package/server/ops/sandbox-shim/bwrap +57 -57
- package/server/projects/assets-summary.js +119 -119
- package/server/projects/auto-name.js +53 -53
- package/server/projects/board-limits.js +12 -0
- package/server/projects/board-sanitize.js +252 -0
- package/server/projects/board-store.js +507 -640
- package/server/projects/move-entry.js +103 -103
- package/server/projects/store.js +270 -270
- package/server/projects/ui-config.js +60 -54
- package/server/projects/viewpoint-store.js +78 -0
- package/server/projects/workspace-templates.js +53 -53
- package/server/projects/workspace.js +1025 -1025
- package/server/runtime/local-config.js +177 -177
- package/server/runtime/profile.js +72 -72
- package/server/services/rembg-service.py +334 -334
- package/server/shared/README.md +25 -25
- package/server/shared/deck.js +54 -54
- package/server/shared/time.js +49 -49
- package/server/style-pipeline/README.md +53 -53
- package/server/ws/broker.js +39 -36
- package/server/ws/browse-channel.js +177 -177
- package/server/ws/index.js +386 -386
- package/web/README.md +89 -89
- package/web/dist/assets/BrowserWindow-P_vIdh-G.js +11 -0
- package/web/dist/assets/{DeckWindow-4G0xX7P3.js → DeckWindow-BpLv5082.js} +3 -3
- package/web/dist/assets/{DocxWindow-BXQsoPkh.js → DocxWindow-DSAU1-tD.js} +4 -4
- package/web/dist/assets/MermaidBlock-DR1GikCv.js +313 -0
- package/web/dist/assets/SiteWindow-92uBuRRR.js +18 -0
- package/web/dist/assets/abnfDiagram-VCTEODGH-uA8Esix4.js +1 -0
- package/web/dist/assets/arc-D_khH_xN.js +1 -0
- package/web/dist/assets/architectureDiagram-5GKGNRK7-DydJHqOt.js +36 -0
- package/web/dist/assets/blockDiagram-NRAW4CY4-B7wBbKiW.js +129 -0
- package/web/dist/assets/c4Diagram-UCG6FXSJ-6mrH0x7Y.js +38 -0
- package/web/dist/assets/channel-DGkccMO1.js +1 -0
- package/web/dist/assets/chunk-2Q5K7J3B-Cuikho5Q.js +1 -0
- package/web/dist/assets/chunk-5VM5RSS4-3-KZ5AUz.js +15 -0
- package/web/dist/assets/chunk-F27PBJKO-Bkwq2gGU.js +1 -0
- package/web/dist/assets/chunk-G27WJ6UU-BLyQzdGE.js +231 -0
- package/web/dist/assets/chunk-JWPE2WC7-B7HxxQds.js +1 -0
- package/web/dist/assets/chunk-LCL6LL3I-CtXrnUwF.js +206 -0
- package/web/dist/assets/chunk-POPQ4Y6H-dxddZlXt.js +1 -0
- package/web/dist/assets/chunk-SVP7TREG-DpkmrgHn.js +88 -0
- package/web/dist/assets/chunk-XXDRQBXY-BwZ7RbpQ.js +1 -0
- package/web/dist/assets/classDiagram-DTDB5LWJ-BGscw3OD.js +1 -0
- package/web/dist/assets/classDiagram-v2-JRS7N3AN-BGscw3OD.js +1 -0
- package/web/dist/assets/cose-bilkent-JH36ORCC-DGFOsMUt.js +1 -0
- package/web/dist/assets/cynefin-OW5HDTMX-DUwn3ehS.js +178 -0
- package/web/dist/assets/cynefinDiagram-5FMLGOSQ-DlLAOZ9x.js +62 -0
- package/web/dist/assets/cytoscape.esm-BjAbmLdb.js +331 -0
- package/web/dist/assets/dagre-3AP2YEHR-CyourLfz.js +4 -0
- package/web/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/web/dist/assets/diagram-S7CK7UJ4-BTMbx2np.js +30 -0
- package/web/dist/assets/diagram-UQ7AKVKN-UK94_1-P.js +41 -0
- package/web/dist/assets/diagram-VSXAHHWV-C7zS-v9v.js +3 -0
- package/web/dist/assets/diagram-VX7I27RA-DKJRa6CK.js +24 -0
- package/web/dist/assets/diagram-Z3DM3KII-kUbAfGAp.js +24 -0
- package/web/dist/assets/ebnfDiagram-PWID7BFC-CVW16_vb.js +1 -0
- package/web/dist/assets/erDiagram-SSCWMZ5O-BRQMFQ5y.js +99 -0
- package/web/dist/assets/flowDiagram-A5DVABFB-S34LpLtK.js +168 -0
- package/web/dist/assets/ganttDiagram-EL5Y4UJY-BFiqk1-G.js +292 -0
- package/web/dist/assets/gitGraphDiagram-WWUBYQGX-LZsrdyyv.js +106 -0
- package/web/dist/assets/index-GAamh1p4.js +2152 -0
- package/web/dist/assets/infoDiagram-RXCK75RN-0DvN6KIp.js +2 -0
- package/web/dist/assets/init-Gi6I4Gst.js +1 -0
- package/web/dist/assets/ishikawaDiagram-5VMMS53U-CaY3A-Bx.js +70 -0
- package/web/dist/assets/journeyDiagram-EYS64GPL-BBeiaCrj.js +139 -0
- package/web/dist/assets/kanban-definition-3QL26DDD-ux42DRXQ.js +89 -0
- package/web/dist/assets/layout-CnXiF_NB.js +1 -0
- package/web/dist/assets/linear-BMW0u6G6.js +1 -0
- package/web/dist/assets/mindmap-definition-FBJOCRG2-hUxSpdKn.js +96 -0
- package/web/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/web/dist/assets/pegDiagram-XKGWAZYB-aCaQPH0w.js +1 -0
- package/web/dist/assets/{pending-edit-apply-DTjo2geW.js → pending-edit-apply-AcdOQ9Ij.js} +6 -6
- package/web/dist/assets/pieDiagram-E7YTZNPT-CmOEZ8zg.js +39 -0
- package/web/dist/assets/quadrantDiagram-AXDQQJYC-VW6fAhsV.js +7 -0
- package/web/dist/assets/railroadDiagram-O6MQD6OU-CKJ7Ba5K.js +1 -0
- package/web/dist/assets/requirementDiagram-EFPCY7ZU-pNGkq9Vw.js +84 -0
- package/web/dist/assets/{rotate-cw-LMT3x1Zj.js → rotate-cw-KAfChbGV.js} +1 -1
- package/web/dist/assets/sankeyDiagram-P5KCCOFB-BIMmDTfZ.js +40 -0
- package/web/dist/assets/sequenceDiagram-WJ2MYXX4-BBW7wzKY.js +162 -0
- package/web/dist/assets/sizeCapture-X5ZJPWSS-DdkXmLYR.js +1 -0
- package/web/dist/assets/{square-dashed-mouse-pointer-CBoAkfhi.js → square-dashed-mouse-pointer-CDUlhUP7.js} +1 -1
- package/web/dist/assets/stateDiagram-HBIQ2CUA-W313Uz16.js +1 -0
- package/web/dist/assets/stateDiagram-v2-4QOOHH4V-DDhJBZzf.js +1 -0
- package/web/dist/assets/swimlanes-XN3QIQJK-C29TL26K.js +1 -0
- package/web/dist/assets/swimlanesDiagram-VK2B7HYN-BfN-Xdmu.js +8 -0
- package/web/dist/assets/timeline-definition-24CTP7MA-CF0JMObX.js +120 -0
- package/web/dist/assets/vennDiagram-4TSXK5OY-DrDFsaH3.js +34 -0
- package/web/dist/assets/wardleyDiagram-VM6X3IG4-CVrnRIS_.js +78 -0
- package/web/dist/assets/xychartDiagram-S5SC5T6Z-BvkN1zsT.js +7 -0
- package/web/dist/index.html +17 -17
- package/web/dist/assets/BrowserWindow-Dc7J7CYc.js +0 -11
- package/web/dist/assets/SiteWindow-D6eS-2gM.js +0 -18
- package/web/dist/assets/index-Cbl3ATvp.js +0 -2125
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* server/engine/runs/board-tasklist.js —— 把 agent 的步骤清单镜像成板书(2026-08-23)
|
|
3
|
+
*
|
|
4
|
+
* 用户定的文化:板书是基础设施。agent 开工时用 TodoWrite 列的步骤不该只活在侧栏的计划
|
|
5
|
+
* 面板里 —— 它是这一轮的骨架,该写在画布上、每一步产出的东西连到对应步骤。
|
|
6
|
+
* 这件事**由 harness 做,不靠 agent 记得**:
|
|
7
|
+
* - run.todo.updated → 一条板书 `notes/板书/<stamp>-步骤.md`(一轮一条、原地更新),
|
|
8
|
+
* Markdown 任务列表:✓ 已做 / → 正在(加粗)/ 待做;落定不走 staging
|
|
9
|
+
* - run.file_changed(有"正在"的步骤时)→ 这条板书到那件产物的 annotates 线,label「第 N 步」,
|
|
10
|
+
* by:'auto'(不算进主角焦点分;同一轮同一件只连一次)
|
|
11
|
+
* - run.done/cancelled/error → 忘掉这一轮的状态(板书留着,它是过程记录)
|
|
12
|
+
* 挂在 project bus 上(broker.getProjectBus 建 bus 时调一次),与 live-turn 折叠器同级。
|
|
13
|
+
* 失败一律吞掉只 warn:镜像挂了不能影响 run。
|
|
14
|
+
*/
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { readBoard, patchBoard } from '../../projects/board-store.js';
|
|
17
|
+
import { getSharedDir } from '../../projects/workspace.js';
|
|
18
|
+
import { estimateSizeOn } from '../../lib/board-kind-sizes.js';
|
|
19
|
+
import { layerOf, normalizeCanvasId } from '../../lib/canvas-id.js';
|
|
20
|
+
import { textBox, findSpot } from '../../lib/sketch-layout.js';
|
|
21
|
+
import { getViewpoint } from '../../projects/viewpoint-store.js';
|
|
22
|
+
import { renderChalk, writeChalkFile, CHALK_DIR } from '../../lib/chalk.js';
|
|
23
|
+
|
|
24
|
+
const byRun = new Map(); // runId → { projectId, rel, fileName, items, linked:Set, session }
|
|
25
|
+
const MAX_LINKS_PER_RUN = 24;
|
|
26
|
+
|
|
27
|
+
function renderList(items) {
|
|
28
|
+
const lines = ['### 这一轮的步骤', ''];
|
|
29
|
+
items.forEach((it, i) => {
|
|
30
|
+
const text = String(it.content || it.activeForm || '').trim() || `步骤 ${i + 1}`;
|
|
31
|
+
if (it.status === 'completed') lines.push(`- [x] ${text}`);
|
|
32
|
+
else if (it.status === 'in_progress') lines.push(`- [ ] **→ ${it.activeForm || text}**`);
|
|
33
|
+
else lines.push(`- [ ] ${text}`);
|
|
34
|
+
});
|
|
35
|
+
return lines.join('\n');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** 工作区相对路径 → 画布 id(产物卡的地址):精确 > deck: > 站点/word 根 > 原样 */
|
|
39
|
+
export function canvasIdForRel(board, rel) {
|
|
40
|
+
const r = String(rel || '').replace(/\\/g, '/').replace(/^\.\//, '');
|
|
41
|
+
if (!r) return null;
|
|
42
|
+
const objs = board.objects || {};
|
|
43
|
+
if (objs[r]) return r;
|
|
44
|
+
const norm = normalizeCanvasId(r);
|
|
45
|
+
if (norm && objs[norm]) return norm;
|
|
46
|
+
for (const id of Object.keys(objs)) {
|
|
47
|
+
const m = /^(site|docx):(.+)$/.exec(id);
|
|
48
|
+
if (!m) continue;
|
|
49
|
+
const root = m[2];
|
|
50
|
+
if (r === root || r.startsWith(`${root}/`)) return id;
|
|
51
|
+
}
|
|
52
|
+
// 还没上墙:按形态猜(seated 之后线自然画出来)
|
|
53
|
+
return norm || r;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function upsert(st, items) {
|
|
57
|
+
st.items = items;
|
|
58
|
+
const body = renderList(items);
|
|
59
|
+
const content = renderChalk({ body, by: 'agent', tag: st.tag, sessionId: st.session || null });
|
|
60
|
+
const sharedRoot = getSharedDir(st.projectId);
|
|
61
|
+
if (!st.fileName) st.fileName = `${new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d+Z$/, '').replace('T', '-')}-步骤.md`;
|
|
62
|
+
st.rel = await writeChalkFile(sharedRoot, st.fileName, content, { overwrite: !!st.rel });
|
|
63
|
+
st.fileName = st.rel.split('/').pop(); // 首写可能被加了防撞序号,之后原地覆盖同一个文件
|
|
64
|
+
const board = await readBoard(st.projectId);
|
|
65
|
+
const box = textBox(body, 'md', { md: true, wUnits: 14 });
|
|
66
|
+
const prev = board.objects?.[st.rel];
|
|
67
|
+
if (prev && Number.isFinite(prev.x)) {
|
|
68
|
+
// 已有座位:只改高(行数变了)不碰宽 —— 宽由前端量过真值,回写会跟它来回抖(fable P2)
|
|
69
|
+
await patchBoard(st.projectId, { objects: { [st.rel]: { ...prev, h: box.h } } });
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
// 首次落位:用户视口的空地 > 内容底下(跟 write_on_board 同口径)
|
|
73
|
+
const known = new Set(Object.keys(board.zones || {}));
|
|
74
|
+
const obstacles = Object.entries(board.objects || {})
|
|
75
|
+
.filter(([id, e]) => Number.isFinite(e?.x) && layerOf(id, e, known) === '')
|
|
76
|
+
.map(([id, e]) => ({ x: e.x, y: e.y, ...estimateSizeOn(board, id, e) }));
|
|
77
|
+
let bottom = 0; for (const o of obstacles) bottom = Math.max(bottom, o.y + o.h);
|
|
78
|
+
for (const zz of Object.values(board.zones || {})) if (Number.isFinite(zz?.y)) bottom = Math.max(bottom, zz.y + 240);
|
|
79
|
+
const vp = getViewpoint(st.projectId);
|
|
80
|
+
const vpRect = (vp && !vp.layer && vp.camera) ? vp.camera : null;
|
|
81
|
+
const spot = findSpot({ w: box.w + 24, h: box.h + 24, obstacles, contentBottom: bottom, viewport: vpRect });
|
|
82
|
+
await patchBoard(st.projectId, { objects: { [st.rel]: { x: spot.x + 12, y: spot.y + 12, z: 1, w: box.w, h: box.h, by: 'agent', tag: st.tag } } });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function attachBoardTasklist(bus, projectId) {
|
|
86
|
+
const handle = async (evt) => {
|
|
87
|
+
if (!evt?.runId) return;
|
|
88
|
+
if (evt.type === 'run.todo.updated') {
|
|
89
|
+
const items = Array.isArray(evt.todos) ? evt.todos.filter(t => t && typeof t === 'object') : [];
|
|
90
|
+
if (!items.length) return;
|
|
91
|
+
let st = byRun.get(evt.runId);
|
|
92
|
+
if (!st) {
|
|
93
|
+
st = { projectId, rel: null, fileName: null, items: [], linked: new Set(), session: evt.sessionId || null, tag: `步骤-${String(evt.runId).slice(-6)}` };
|
|
94
|
+
byRun.set(evt.runId, st);
|
|
95
|
+
}
|
|
96
|
+
await upsert(st, items);
|
|
97
|
+
bus.publish({ type: 'board.updated', sessionId: null, runId: evt.runId, summary: '步骤清单上板' });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (evt.type === 'run.file_changed') {
|
|
101
|
+
const st = byRun.get(evt.runId);
|
|
102
|
+
if (!st?.rel || !evt.filePath) return;
|
|
103
|
+
const idx = st.items.findIndex(t => t.status === 'in_progress');
|
|
104
|
+
if (idx < 0) return;
|
|
105
|
+
// 只连工作区内、非隐藏目录的真产物;每轮封顶(否则 /tmp、.nd、.claude 里的写入会灌一堆
|
|
106
|
+
// 指向虚空的 auto 线,MAX_BINDINGS 一到用户自己的线都不收 —— fable 08-23 P2)
|
|
107
|
+
const rel = String(evt.filePath).replace(/\\/g, '/');
|
|
108
|
+
if (rel.startsWith('/') || rel.split('/').some(seg => seg === '..' || seg.startsWith('.') || seg === 'node_modules')) return;
|
|
109
|
+
if (rel.startsWith(`${CHALK_DIR}/`) || rel.startsWith('notes/')) return;
|
|
110
|
+
if (st.linked.size >= MAX_LINKS_PER_RUN) return;
|
|
111
|
+
const board = await readBoard(st.projectId);
|
|
112
|
+
const target = canvasIdForRel(board, evt.filePath);
|
|
113
|
+
if (!target || target === st.rel || target.startsWith(`${CHALK_DIR}/`)) return;
|
|
114
|
+
if (st.linked.has(target)) return;
|
|
115
|
+
st.linked.add(target);
|
|
116
|
+
const id = `b:auto:step:${String(evt.runId).slice(-6)}:${st.linked.size}`;
|
|
117
|
+
await patchBoard(st.projectId, { bindings: { [id]: { type: 'annotates', from: st.rel, to: target, by: 'auto', label: `第 ${idx + 1} 步`, tag: st.tag } } });
|
|
118
|
+
bus.publish({ type: 'board.updated', sessionId: null, runId: evt.runId, summary: `第 ${idx + 1} 步 → ${path.basename(target)}` });
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (evt.type === 'run.done' || evt.type === 'run.cancelled' || evt.type === 'run.error') {
|
|
122
|
+
byRun.delete(evt.runId);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
bus.subscribe('*', (evt) => {
|
|
126
|
+
handle(evt).catch(err => console.warn('[board-tasklist]', err?.message || err));
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function _resetBoardTasklist() { byRun.clear(); }
|
|
@@ -1,252 +1,252 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* server/engine/runs/live-turn.js — 进行中 turn 的物化快照(内存态)
|
|
3
|
-
*
|
|
4
|
-
* 解决的问题:EventBus 是 fire-and-forget + ring buffer(2000 条),token 级
|
|
5
|
-
* streaming 一个 turn 就上万条事件 —— 刷新 / 断线重连后"进行中的 turn"在任何
|
|
6
|
-
* 可恢复的地方都不存在(jsonl 只在 turn 边界 flush)。老架构靠 ?since= 游标 +
|
|
7
|
-
* ring 回放,重连几秒就 gap,gap 补 hydrate 又把已渲染内容反向覆盖。
|
|
8
|
-
*
|
|
9
|
-
* 新范式(快照 + 尾随,2026-07-27 重构):
|
|
10
|
-
* - 本模块订阅每个 project bus,把进行中 turn 的事件**同步折叠**成前端
|
|
11
|
-
* display-message 形态的物化状态(跟 ProjectWorkspace handleEvent 的累加
|
|
12
|
-
* 逻辑同构)
|
|
13
|
-
* - WS 连接时:jsonl hydrate(已完成 turn)→ ws.live_turn 快照(进行中 turn)
|
|
14
|
-
* → 从快照 seq 起订阅实时流。ring buffer 大小从此只影响极小的同步窗口,
|
|
15
|
-
* 不再影响正确性
|
|
16
|
-
* - turn 结束(done / error / cancelled)或 session 结束即清,内存占用 =
|
|
17
|
-
* 活跃 turn 数 × 单 turn 消息体量(tool output 截断到 16k)
|
|
18
|
-
*
|
|
19
|
-
* 折叠规则跟前端 handleEvent 对齐(改前端渲染逻辑时两边同步改):
|
|
20
|
-
* run.delta.text / thinking → 同 role 连续累加一条消息
|
|
21
|
-
* run.tool_use.started → push tool 消息 status='running'
|
|
22
|
-
* run.delta.tool_use → 补 toolInput
|
|
23
|
-
* run.delta.tool_result → 补 status / output / error(images 不进快照,太大)
|
|
24
|
-
* run.todo.updated → todos 覆盖
|
|
25
|
-
* run.context_usage → contextUsage 覆盖
|
|
26
|
-
* run.done / error / cancelled → 标记 ended(保留 GRACE 毫秒,见下),不再折叠
|
|
27
|
-
* run.query.end → 清
|
|
28
|
-
*
|
|
29
|
-
* turn 结束后为什么还留一小会(2026-07-28):jsonl 是 SDK 子进程按 block 逐条写的,
|
|
30
|
-
* 我们读 jsonl 与 turn 收尾之间有几百毫秒的错位窗口。这段时间内重连,若快照已经
|
|
31
|
-
* 没了,尾随订阅会把已经落进 jsonl 的那几条 delta 再放一遍 → 正文重复。留 GRACE
|
|
32
|
-
* 毫秒让"本轮由快照权威、hydrate 裁掉本轮"这条规则在收尾瞬间同样成立。
|
|
33
|
-
*/
|
|
34
|
-
|
|
35
|
-
const MAX_TOOL_TEXT = 16_000;
|
|
36
|
-
// 3s:够盖住"读 jsonl 的几百毫秒"这个错位窗口,又短到收尾后的重连基本不会
|
|
37
|
-
// 落在里面(快照不带 tool_result 的图片,长时间用快照顶替 hydrate 会丢缩略图)。
|
|
38
|
-
const ENDED_GRACE_MS = 3_000;
|
|
39
|
-
|
|
40
|
-
/** @type {Map<string, object>} sessionId → live turn state */
|
|
41
|
-
const liveTurns = new Map();
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* sessionId → 最近一次 run.context_usage 事件(2026-07-30)
|
|
45
|
-
*
|
|
46
|
-
* 跟 liveTurns 分开活着:turn 一结束 liveTurns 就清,但"这个会话现在装了多少上下文"
|
|
47
|
-
* 在两轮之间照样成立 —— 而那恰恰是用户最想看它的时刻(要不要先压缩再开新活)。
|
|
48
|
-
*
|
|
49
|
-
* query 结束也**不清**:关掉标签页就会触发 grace 超时关 session,而下次打开这个会话
|
|
50
|
-
* 时 SDK 从同一份 jsonl resume,上下文体量基本还是那么大。这时给个带"当前没有活跃
|
|
51
|
-
* 会话"标注的旧数字(live:false),比一句"还没开始对话"有用得多。只有 query 活着时才向 SDK 现问
|
|
52
|
-
* (见 GET /sessions/:sid/context-usage),所以准确的那条路一直优先。
|
|
53
|
-
*
|
|
54
|
-
* 只活在内存里:进程重启就没了,下一轮 turn 自然补上。
|
|
55
|
-
*/
|
|
56
|
-
const lastUsage = new Map();
|
|
57
|
-
const LAST_USAGE_CAP = 200;
|
|
58
|
-
|
|
59
|
-
export function getLastContextUsage(sessionId) {
|
|
60
|
-
return lastUsage.get(sessionId) || null;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* 不变量守卫:只记 Events.contextUsage 构造出来的那种形状。
|
|
65
|
-
*
|
|
66
|
-
* `run.context_usage` 曾经有两个生产者,字段名对不上:session-loop 每条 assistant
|
|
67
|
-
* message 后发 { totalTokens, maxTokens, percentage }(前端读这套),hooks.js 的 Stop
|
|
68
|
-
* handler 每轮收尾发 { used, max, percent, categories }。前端靠 store 的"不覆盖已有值"
|
|
69
|
-
* 侥幸没显形,但每轮的**最后**一条恰好总是后者,凡是"取最新一条"的下游拿到的都是
|
|
70
|
-
* 一片 undefined —— 这个记忆层就是这么栽的。两个生产者已经合并成一个构造函数
|
|
71
|
-
* (2026-07-30),这里留一道守卫:以后再冒出第三种形状,是它被丢掉,不是这里出错数。
|
|
72
|
-
*/
|
|
73
|
-
function rememberUsage(sid, evt) {
|
|
74
|
-
if (typeof evt.totalTokens !== 'number') return;
|
|
75
|
-
if (lastUsage.size >= LAST_USAGE_CAP && !lastUsage.has(sid)) {
|
|
76
|
-
// 先进先出丢一条:进程活得久了不至于攒着所有历史 session
|
|
77
|
-
const oldest = lastUsage.keys().next().value;
|
|
78
|
-
if (oldest) lastUsage.delete(oldest);
|
|
79
|
-
}
|
|
80
|
-
lastUsage.set(sid, evt);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function truncate(s) {
|
|
84
|
-
if (typeof s !== 'string') return s;
|
|
85
|
-
if (s.length <= MAX_TOOL_TEXT) return s;
|
|
86
|
-
return s.slice(0, MAX_TOOL_TEXT) + `\n…[截断,共 ${s.length} 字符]`;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* 给 project bus 挂折叠订阅(幂等 —— broker.getProjectBus 每次创建 bus 时调)。
|
|
91
|
-
*/
|
|
92
|
-
export function attachLiveTurnTracker(bus) {
|
|
93
|
-
if (bus.__liveTurnTracked) return;
|
|
94
|
-
bus.__liveTurnTracked = true;
|
|
95
|
-
bus.subscribe('*', fold);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* 取 session 当前 turn 的快照;没有在跑、且上一轮已过 grace 期 → null。
|
|
100
|
-
* 返回值直接可作 ws.live_turn 帧 payload(messages 是前端 display 形态)。
|
|
101
|
-
*
|
|
102
|
-
* running=false 表示这是刚结束那一轮的尾巴(grace 期内),前端据此**只合并消息、
|
|
103
|
-
* 不把自己切回 streaming 态**。
|
|
104
|
-
*/
|
|
105
|
-
export function getLiveTurnSnapshot(sessionId) {
|
|
106
|
-
const st = liveTurns.get(sessionId);
|
|
107
|
-
if (!st) return null;
|
|
108
|
-
if (st.endedAt && Date.now() - st.endedAt > ENDED_GRACE_MS) {
|
|
109
|
-
liveTurns.delete(sessionId);
|
|
110
|
-
return null;
|
|
111
|
-
}
|
|
112
|
-
return {
|
|
113
|
-
runId: st.runId,
|
|
114
|
-
seq: st.seq,
|
|
115
|
-
startedAt: st.startedAt,
|
|
116
|
-
running: !st.endedAt,
|
|
117
|
-
messages: st.messages,
|
|
118
|
-
todos: st.todos,
|
|
119
|
-
contextUsage: st.contextUsage,
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function fold(evt) {
|
|
124
|
-
const sid = evt.sessionId;
|
|
125
|
-
if (!sid) return;
|
|
126
|
-
|
|
127
|
-
// 记在 liveTurns 之外:turn 结束、快照删掉之后,这个数字还要继续给 composer
|
|
128
|
-
// 的 [+] 菜单用。放在 st 判空之前,快照已经清了也照记。
|
|
129
|
-
if (evt.type === 'run.context_usage') rememberUsage(sid, evt);
|
|
130
|
-
|
|
131
|
-
if (evt.type === 'run.start') {
|
|
132
|
-
liveTurns.set(sid, {
|
|
133
|
-
runId: evt.runId || null,
|
|
134
|
-
startedAt: evt.ts || new Date().toISOString(),
|
|
135
|
-
seq: evt.seq || 0,
|
|
136
|
-
messages: [],
|
|
137
|
-
todos: [],
|
|
138
|
-
contextUsage: null,
|
|
139
|
-
endedAt: null,
|
|
140
|
-
_msgCounter: 0,
|
|
141
|
-
});
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const st = liveTurns.get(sid);
|
|
146
|
-
if (!st) return;
|
|
147
|
-
|
|
148
|
-
// 已收尾的那一轮只留着给 grace 期内的重连当权威快照,不再吸新事件
|
|
149
|
-
// (新一轮由上面的 run.start 整块换掉)。seq 照常推进,免得尾随订阅从旧游标
|
|
150
|
-
// 起把这轮已经放过的事件再放一遍。
|
|
151
|
-
if (st.endedAt) {
|
|
152
|
-
if (evt.type === 'run.query.end' || Date.now() - st.endedAt > ENDED_GRACE_MS) {
|
|
153
|
-
liveTurns.delete(sid);
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
if (typeof evt.seq === 'number' && evt.seq > st.seq) st.seq = evt.seq;
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// stale 事件(老 turn 的尾巴)不折叠也不清新 turn 的状态
|
|
161
|
-
const runMatches = !evt.runId || !st.runId || evt.runId === st.runId;
|
|
162
|
-
|
|
163
|
-
switch (evt.type) {
|
|
164
|
-
case 'run.delta.text':
|
|
165
|
-
if (runMatches) appendText(st, 'assistant', evt.text, evt.parentToolUseId);
|
|
166
|
-
break;
|
|
167
|
-
case 'run.delta.thinking':
|
|
168
|
-
if (runMatches) appendText(st, 'thinking', evt.text, evt.parentToolUseId);
|
|
169
|
-
break;
|
|
170
|
-
case 'run.tool_use.started':
|
|
171
|
-
if (runMatches && evt.blockId && !st.messages.some(m => m.id === evt.blockId)) {
|
|
172
|
-
st.messages.push({
|
|
173
|
-
id: evt.blockId, role: 'tool', toolName: evt.name,
|
|
174
|
-
toolInput: undefined, status: 'running', runId: st.runId,
|
|
175
|
-
...(evt.parentToolUseId ? { parentToolUseId: evt.parentToolUseId } : {}),
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
break;
|
|
179
|
-
case 'run.delta.tool_use': {
|
|
180
|
-
if (!runMatches) break;
|
|
181
|
-
const existing = st.messages.find(m => m.role === 'tool' && m.id === evt.blockId);
|
|
182
|
-
if (existing) existing.toolInput = evt.input;
|
|
183
|
-
else if (evt.blockId) {
|
|
184
|
-
st.messages.push({
|
|
185
|
-
id: evt.blockId, role: 'tool', toolName: evt.name,
|
|
186
|
-
toolInput: evt.input, status: 'running', runId: st.runId,
|
|
187
|
-
...(evt.parentToolUseId ? { parentToolUseId: evt.parentToolUseId } : {}),
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
break;
|
|
191
|
-
}
|
|
192
|
-
case 'run.delta.tool_result': {
|
|
193
|
-
if (!runMatches) break;
|
|
194
|
-
const tool = st.messages.find(m => m.role === 'tool' && m.id === evt.blockId);
|
|
195
|
-
if (tool) {
|
|
196
|
-
tool.status = evt.ok ? 'success' : 'error';
|
|
197
|
-
if (evt.output !== undefined) tool.toolOutput = truncate(typeof evt.output === 'string' ? evt.output : JSON.stringify(evt.output));
|
|
198
|
-
if (evt.error) tool.toolError = truncate(typeof evt.error === 'string' ? evt.error : JSON.stringify(evt.error));
|
|
199
|
-
// images 不进快照:base64 体积大,turn 结束后 jsonl hydrate 会带回来
|
|
200
|
-
}
|
|
201
|
-
break;
|
|
202
|
-
}
|
|
203
|
-
case 'run.tool_use_summary': {
|
|
204
|
-
if (!runMatches || !evt.summary) break;
|
|
205
|
-
const ids = Array.isArray(evt.blockIds) ? evt.blockIds : [];
|
|
206
|
-
const hit = st.messages.find(m => m.role === 'tool' && ids.includes(m.id));
|
|
207
|
-
if (hit) hit.groupSummary = evt.summary;
|
|
208
|
-
break;
|
|
209
|
-
}
|
|
210
|
-
case 'run.todo.updated':
|
|
211
|
-
if (runMatches && Array.isArray(evt.todos)) st.todos = evt.todos;
|
|
212
|
-
break;
|
|
213
|
-
case 'run.context_usage':
|
|
214
|
-
if (runMatches) st.contextUsage = evt;
|
|
215
|
-
break;
|
|
216
|
-
case 'run.done':
|
|
217
|
-
case 'run.error':
|
|
218
|
-
case 'run.cancelled':
|
|
219
|
-
// 不立刻删:留 grace 期给"收尾瞬间重连"当权威快照(见文件头说明)
|
|
220
|
-
if (runMatches) {
|
|
221
|
-
st.endedAt = Date.now();
|
|
222
|
-
if (typeof evt.seq === 'number' && evt.seq > st.seq) st.seq = evt.seq;
|
|
223
|
-
}
|
|
224
|
-
return;
|
|
225
|
-
case 'run.query.end':
|
|
226
|
-
liveTurns.delete(sid);
|
|
227
|
-
return;
|
|
228
|
-
default:
|
|
229
|
-
break;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// 所有本 session 事件都推进快照游标(包括未折叠类型)——
|
|
233
|
-
// 重连订阅从 seq 起,不折叠的事件类型(queue depth 等)丢了也无碍(幂等 UI 状态)
|
|
234
|
-
if (typeof evt.seq === 'number' && evt.seq > st.seq) st.seq = evt.seq;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
function appendText(st, role, text, parentToolUseId = null) {
|
|
238
|
-
if (!text) return;
|
|
239
|
-
const last = st.messages[st.messages.length - 1];
|
|
240
|
-
// 子代理时间轴:不同 parent 的流不互吸(跟前端 lib/chat-stream.js 同构)
|
|
241
|
-
if (last && last.role === role && (last.parentToolUseId || null) === (parentToolUseId || null)) {
|
|
242
|
-
last.content = (last.content || '') + text;
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
245
|
-
st.messages.push({
|
|
246
|
-
id: `${st.runId || 'live'}:m${st._msgCounter++}`,
|
|
247
|
-
...(parentToolUseId ? { parentToolUseId } : {}),
|
|
248
|
-
role,
|
|
249
|
-
content: text,
|
|
250
|
-
runId: st.runId,
|
|
251
|
-
});
|
|
252
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* server/engine/runs/live-turn.js — 进行中 turn 的物化快照(内存态)
|
|
3
|
+
*
|
|
4
|
+
* 解决的问题:EventBus 是 fire-and-forget + ring buffer(2000 条),token 级
|
|
5
|
+
* streaming 一个 turn 就上万条事件 —— 刷新 / 断线重连后"进行中的 turn"在任何
|
|
6
|
+
* 可恢复的地方都不存在(jsonl 只在 turn 边界 flush)。老架构靠 ?since= 游标 +
|
|
7
|
+
* ring 回放,重连几秒就 gap,gap 补 hydrate 又把已渲染内容反向覆盖。
|
|
8
|
+
*
|
|
9
|
+
* 新范式(快照 + 尾随,2026-07-27 重构):
|
|
10
|
+
* - 本模块订阅每个 project bus,把进行中 turn 的事件**同步折叠**成前端
|
|
11
|
+
* display-message 形态的物化状态(跟 ProjectWorkspace handleEvent 的累加
|
|
12
|
+
* 逻辑同构)
|
|
13
|
+
* - WS 连接时:jsonl hydrate(已完成 turn)→ ws.live_turn 快照(进行中 turn)
|
|
14
|
+
* → 从快照 seq 起订阅实时流。ring buffer 大小从此只影响极小的同步窗口,
|
|
15
|
+
* 不再影响正确性
|
|
16
|
+
* - turn 结束(done / error / cancelled)或 session 结束即清,内存占用 =
|
|
17
|
+
* 活跃 turn 数 × 单 turn 消息体量(tool output 截断到 16k)
|
|
18
|
+
*
|
|
19
|
+
* 折叠规则跟前端 handleEvent 对齐(改前端渲染逻辑时两边同步改):
|
|
20
|
+
* run.delta.text / thinking → 同 role 连续累加一条消息
|
|
21
|
+
* run.tool_use.started → push tool 消息 status='running'
|
|
22
|
+
* run.delta.tool_use → 补 toolInput
|
|
23
|
+
* run.delta.tool_result → 补 status / output / error(images 不进快照,太大)
|
|
24
|
+
* run.todo.updated → todos 覆盖
|
|
25
|
+
* run.context_usage → contextUsage 覆盖
|
|
26
|
+
* run.done / error / cancelled → 标记 ended(保留 GRACE 毫秒,见下),不再折叠
|
|
27
|
+
* run.query.end → 清
|
|
28
|
+
*
|
|
29
|
+
* turn 结束后为什么还留一小会(2026-07-28):jsonl 是 SDK 子进程按 block 逐条写的,
|
|
30
|
+
* 我们读 jsonl 与 turn 收尾之间有几百毫秒的错位窗口。这段时间内重连,若快照已经
|
|
31
|
+
* 没了,尾随订阅会把已经落进 jsonl 的那几条 delta 再放一遍 → 正文重复。留 GRACE
|
|
32
|
+
* 毫秒让"本轮由快照权威、hydrate 裁掉本轮"这条规则在收尾瞬间同样成立。
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
const MAX_TOOL_TEXT = 16_000;
|
|
36
|
+
// 3s:够盖住"读 jsonl 的几百毫秒"这个错位窗口,又短到收尾后的重连基本不会
|
|
37
|
+
// 落在里面(快照不带 tool_result 的图片,长时间用快照顶替 hydrate 会丢缩略图)。
|
|
38
|
+
const ENDED_GRACE_MS = 3_000;
|
|
39
|
+
|
|
40
|
+
/** @type {Map<string, object>} sessionId → live turn state */
|
|
41
|
+
const liveTurns = new Map();
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* sessionId → 最近一次 run.context_usage 事件(2026-07-30)
|
|
45
|
+
*
|
|
46
|
+
* 跟 liveTurns 分开活着:turn 一结束 liveTurns 就清,但"这个会话现在装了多少上下文"
|
|
47
|
+
* 在两轮之间照样成立 —— 而那恰恰是用户最想看它的时刻(要不要先压缩再开新活)。
|
|
48
|
+
*
|
|
49
|
+
* query 结束也**不清**:关掉标签页就会触发 grace 超时关 session,而下次打开这个会话
|
|
50
|
+
* 时 SDK 从同一份 jsonl resume,上下文体量基本还是那么大。这时给个带"当前没有活跃
|
|
51
|
+
* 会话"标注的旧数字(live:false),比一句"还没开始对话"有用得多。只有 query 活着时才向 SDK 现问
|
|
52
|
+
* (见 GET /sessions/:sid/context-usage),所以准确的那条路一直优先。
|
|
53
|
+
*
|
|
54
|
+
* 只活在内存里:进程重启就没了,下一轮 turn 自然补上。
|
|
55
|
+
*/
|
|
56
|
+
const lastUsage = new Map();
|
|
57
|
+
const LAST_USAGE_CAP = 200;
|
|
58
|
+
|
|
59
|
+
export function getLastContextUsage(sessionId) {
|
|
60
|
+
return lastUsage.get(sessionId) || null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 不变量守卫:只记 Events.contextUsage 构造出来的那种形状。
|
|
65
|
+
*
|
|
66
|
+
* `run.context_usage` 曾经有两个生产者,字段名对不上:session-loop 每条 assistant
|
|
67
|
+
* message 后发 { totalTokens, maxTokens, percentage }(前端读这套),hooks.js 的 Stop
|
|
68
|
+
* handler 每轮收尾发 { used, max, percent, categories }。前端靠 store 的"不覆盖已有值"
|
|
69
|
+
* 侥幸没显形,但每轮的**最后**一条恰好总是后者,凡是"取最新一条"的下游拿到的都是
|
|
70
|
+
* 一片 undefined —— 这个记忆层就是这么栽的。两个生产者已经合并成一个构造函数
|
|
71
|
+
* (2026-07-30),这里留一道守卫:以后再冒出第三种形状,是它被丢掉,不是这里出错数。
|
|
72
|
+
*/
|
|
73
|
+
function rememberUsage(sid, evt) {
|
|
74
|
+
if (typeof evt.totalTokens !== 'number') return;
|
|
75
|
+
if (lastUsage.size >= LAST_USAGE_CAP && !lastUsage.has(sid)) {
|
|
76
|
+
// 先进先出丢一条:进程活得久了不至于攒着所有历史 session
|
|
77
|
+
const oldest = lastUsage.keys().next().value;
|
|
78
|
+
if (oldest) lastUsage.delete(oldest);
|
|
79
|
+
}
|
|
80
|
+
lastUsage.set(sid, evt);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function truncate(s) {
|
|
84
|
+
if (typeof s !== 'string') return s;
|
|
85
|
+
if (s.length <= MAX_TOOL_TEXT) return s;
|
|
86
|
+
return s.slice(0, MAX_TOOL_TEXT) + `\n…[截断,共 ${s.length} 字符]`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 给 project bus 挂折叠订阅(幂等 —— broker.getProjectBus 每次创建 bus 时调)。
|
|
91
|
+
*/
|
|
92
|
+
export function attachLiveTurnTracker(bus) {
|
|
93
|
+
if (bus.__liveTurnTracked) return;
|
|
94
|
+
bus.__liveTurnTracked = true;
|
|
95
|
+
bus.subscribe('*', fold);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 取 session 当前 turn 的快照;没有在跑、且上一轮已过 grace 期 → null。
|
|
100
|
+
* 返回值直接可作 ws.live_turn 帧 payload(messages 是前端 display 形态)。
|
|
101
|
+
*
|
|
102
|
+
* running=false 表示这是刚结束那一轮的尾巴(grace 期内),前端据此**只合并消息、
|
|
103
|
+
* 不把自己切回 streaming 态**。
|
|
104
|
+
*/
|
|
105
|
+
export function getLiveTurnSnapshot(sessionId) {
|
|
106
|
+
const st = liveTurns.get(sessionId);
|
|
107
|
+
if (!st) return null;
|
|
108
|
+
if (st.endedAt && Date.now() - st.endedAt > ENDED_GRACE_MS) {
|
|
109
|
+
liveTurns.delete(sessionId);
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
runId: st.runId,
|
|
114
|
+
seq: st.seq,
|
|
115
|
+
startedAt: st.startedAt,
|
|
116
|
+
running: !st.endedAt,
|
|
117
|
+
messages: st.messages,
|
|
118
|
+
todos: st.todos,
|
|
119
|
+
contextUsage: st.contextUsage,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function fold(evt) {
|
|
124
|
+
const sid = evt.sessionId;
|
|
125
|
+
if (!sid) return;
|
|
126
|
+
|
|
127
|
+
// 记在 liveTurns 之外:turn 结束、快照删掉之后,这个数字还要继续给 composer
|
|
128
|
+
// 的 [+] 菜单用。放在 st 判空之前,快照已经清了也照记。
|
|
129
|
+
if (evt.type === 'run.context_usage') rememberUsage(sid, evt);
|
|
130
|
+
|
|
131
|
+
if (evt.type === 'run.start') {
|
|
132
|
+
liveTurns.set(sid, {
|
|
133
|
+
runId: evt.runId || null,
|
|
134
|
+
startedAt: evt.ts || new Date().toISOString(),
|
|
135
|
+
seq: evt.seq || 0,
|
|
136
|
+
messages: [],
|
|
137
|
+
todos: [],
|
|
138
|
+
contextUsage: null,
|
|
139
|
+
endedAt: null,
|
|
140
|
+
_msgCounter: 0,
|
|
141
|
+
});
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const st = liveTurns.get(sid);
|
|
146
|
+
if (!st) return;
|
|
147
|
+
|
|
148
|
+
// 已收尾的那一轮只留着给 grace 期内的重连当权威快照,不再吸新事件
|
|
149
|
+
// (新一轮由上面的 run.start 整块换掉)。seq 照常推进,免得尾随订阅从旧游标
|
|
150
|
+
// 起把这轮已经放过的事件再放一遍。
|
|
151
|
+
if (st.endedAt) {
|
|
152
|
+
if (evt.type === 'run.query.end' || Date.now() - st.endedAt > ENDED_GRACE_MS) {
|
|
153
|
+
liveTurns.delete(sid);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (typeof evt.seq === 'number' && evt.seq > st.seq) st.seq = evt.seq;
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// stale 事件(老 turn 的尾巴)不折叠也不清新 turn 的状态
|
|
161
|
+
const runMatches = !evt.runId || !st.runId || evt.runId === st.runId;
|
|
162
|
+
|
|
163
|
+
switch (evt.type) {
|
|
164
|
+
case 'run.delta.text':
|
|
165
|
+
if (runMatches) appendText(st, 'assistant', evt.text, evt.parentToolUseId);
|
|
166
|
+
break;
|
|
167
|
+
case 'run.delta.thinking':
|
|
168
|
+
if (runMatches) appendText(st, 'thinking', evt.text, evt.parentToolUseId);
|
|
169
|
+
break;
|
|
170
|
+
case 'run.tool_use.started':
|
|
171
|
+
if (runMatches && evt.blockId && !st.messages.some(m => m.id === evt.blockId)) {
|
|
172
|
+
st.messages.push({
|
|
173
|
+
id: evt.blockId, role: 'tool', toolName: evt.name,
|
|
174
|
+
toolInput: undefined, status: 'running', runId: st.runId,
|
|
175
|
+
...(evt.parentToolUseId ? { parentToolUseId: evt.parentToolUseId } : {}),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
break;
|
|
179
|
+
case 'run.delta.tool_use': {
|
|
180
|
+
if (!runMatches) break;
|
|
181
|
+
const existing = st.messages.find(m => m.role === 'tool' && m.id === evt.blockId);
|
|
182
|
+
if (existing) existing.toolInput = evt.input;
|
|
183
|
+
else if (evt.blockId) {
|
|
184
|
+
st.messages.push({
|
|
185
|
+
id: evt.blockId, role: 'tool', toolName: evt.name,
|
|
186
|
+
toolInput: evt.input, status: 'running', runId: st.runId,
|
|
187
|
+
...(evt.parentToolUseId ? { parentToolUseId: evt.parentToolUseId } : {}),
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
case 'run.delta.tool_result': {
|
|
193
|
+
if (!runMatches) break;
|
|
194
|
+
const tool = st.messages.find(m => m.role === 'tool' && m.id === evt.blockId);
|
|
195
|
+
if (tool) {
|
|
196
|
+
tool.status = evt.ok ? 'success' : 'error';
|
|
197
|
+
if (evt.output !== undefined) tool.toolOutput = truncate(typeof evt.output === 'string' ? evt.output : JSON.stringify(evt.output));
|
|
198
|
+
if (evt.error) tool.toolError = truncate(typeof evt.error === 'string' ? evt.error : JSON.stringify(evt.error));
|
|
199
|
+
// images 不进快照:base64 体积大,turn 结束后 jsonl hydrate 会带回来
|
|
200
|
+
}
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
case 'run.tool_use_summary': {
|
|
204
|
+
if (!runMatches || !evt.summary) break;
|
|
205
|
+
const ids = Array.isArray(evt.blockIds) ? evt.blockIds : [];
|
|
206
|
+
const hit = st.messages.find(m => m.role === 'tool' && ids.includes(m.id));
|
|
207
|
+
if (hit) hit.groupSummary = evt.summary;
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
case 'run.todo.updated':
|
|
211
|
+
if (runMatches && Array.isArray(evt.todos)) st.todos = evt.todos;
|
|
212
|
+
break;
|
|
213
|
+
case 'run.context_usage':
|
|
214
|
+
if (runMatches) st.contextUsage = evt;
|
|
215
|
+
break;
|
|
216
|
+
case 'run.done':
|
|
217
|
+
case 'run.error':
|
|
218
|
+
case 'run.cancelled':
|
|
219
|
+
// 不立刻删:留 grace 期给"收尾瞬间重连"当权威快照(见文件头说明)
|
|
220
|
+
if (runMatches) {
|
|
221
|
+
st.endedAt = Date.now();
|
|
222
|
+
if (typeof evt.seq === 'number' && evt.seq > st.seq) st.seq = evt.seq;
|
|
223
|
+
}
|
|
224
|
+
return;
|
|
225
|
+
case 'run.query.end':
|
|
226
|
+
liveTurns.delete(sid);
|
|
227
|
+
return;
|
|
228
|
+
default:
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// 所有本 session 事件都推进快照游标(包括未折叠类型)——
|
|
233
|
+
// 重连订阅从 seq 起,不折叠的事件类型(queue depth 等)丢了也无碍(幂等 UI 状态)
|
|
234
|
+
if (typeof evt.seq === 'number' && evt.seq > st.seq) st.seq = evt.seq;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function appendText(st, role, text, parentToolUseId = null) {
|
|
238
|
+
if (!text) return;
|
|
239
|
+
const last = st.messages[st.messages.length - 1];
|
|
240
|
+
// 子代理时间轴:不同 parent 的流不互吸(跟前端 lib/chat-stream.js 同构)
|
|
241
|
+
if (last && last.role === role && (last.parentToolUseId || null) === (parentToolUseId || null)) {
|
|
242
|
+
last.content = (last.content || '') + text;
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
st.messages.push({
|
|
246
|
+
id: `${st.runId || 'live'}:m${st._msgCounter++}`,
|
|
247
|
+
...(parentToolUseId ? { parentToolUseId } : {}),
|
|
248
|
+
role,
|
|
249
|
+
content: text,
|
|
250
|
+
runId: st.runId,
|
|
251
|
+
});
|
|
252
|
+
}
|