@xiaobuyu/nodesign 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +140 -0
- package/bin/nodesign.js +176 -0
- package/package.json +104 -0
- package/server/api/README.md +45 -0
- package/server/api/_guard.js +75 -0
- package/server/api/admin.js +187 -0
- package/server/api/assets/docx-page.js +103 -0
- package/server/api/assets/helpers.js +22 -0
- package/server/api/assets/notes.js +144 -0
- package/server/api/assets.js +896 -0
- package/server/api/board.js +57 -0
- package/server/api/browse.js +138 -0
- package/server/api/canvas.js +444 -0
- package/server/api/chatai.js +324 -0
- package/server/api/exports/build-standalone.js +980 -0
- package/server/api/exports/cards.js +118 -0
- package/server/api/exports/docx-pdf.js +46 -0
- package/server/api/exports/export-page.js +142 -0
- package/server/api/exports/handoff.js +147 -0
- package/server/api/exports.js +690 -0
- package/server/api/instruction.js +75 -0
- package/server/api/local.js +115 -0
- package/server/api/me.js +116 -0
- package/server/api/memory.js +185 -0
- package/server/api/pending-changes.js +366 -0
- package/server/api/plugins.js +164 -0
- package/server/api/projects.js +220 -0
- package/server/api/publish.js +64 -0
- package/server/api/recent.js +73 -0
- package/server/api/sessions.js +589 -0
- package/server/api/skills.js +77 -0
- package/server/api/standalone-fit.js +168 -0
- package/server/api/turn-compose.js +180 -0
- package/server/api/turn-inflight.js +44 -0
- package/server/api/turn.js +669 -0
- package/server/auth/README.md +17 -0
- package/server/auth/middleware.js +138 -0
- package/server/auth/origin-guard.js +71 -0
- package/server/auth/session.js +108 -0
- package/server/auth/tier.js +113 -0
- package/server/auth/users-store.js +348 -0
- package/server/edit/README.md +29 -0
- package/server/engine/README.md +113 -0
- package/server/engine/agent/agent-shared.js +550 -0
- package/server/engine/agent/auto-mode-default-hard-deny.txt +4 -0
- package/server/engine/agent/auto-mode-rules.js +78 -0
- package/server/engine/agent/context.js +418 -0
- package/server/engine/agent/events.js +464 -0
- package/server/engine/agent/hooks/canvas-validate.js +199 -0
- package/server/engine/agent/hooks/failure.js +120 -0
- package/server/engine/agent/hooks/file-events.js +75 -0
- package/server/engine/agent/hooks/lifecycle.js +233 -0
- package/server/engine/agent/hooks/post-canvas-focus.js +80 -0
- package/server/engine/agent/hooks/post-guidance.js +206 -0
- package/server/engine/agent/hooks/post-subagent-report.js +104 -0
- package/server/engine/agent/hooks/post-trim.js +58 -0
- package/server/engine/agent/hooks/pre-board-neighborhood.js +34 -0
- package/server/engine/agent/hooks/pre-defaults.js +84 -0
- package/server/engine/agent/hooks/pre-injectors.js +277 -0
- package/server/engine/agent/hooks/pre-performance-log-guard.js +78 -0
- package/server/engine/agent/hooks/pre-starter-files.js +61 -0
- package/server/engine/agent/hooks/pre-workspace-scope-guard.js +78 -0
- package/server/engine/agent/hooks/site-validate.js +94 -0
- package/server/engine/agent/hooks/tool-prompts.js +31 -0
- package/server/engine/agent/hooks/turn-state-memory.js +56 -0
- package/server/engine/agent/hooks/user-prompt-submit.js +215 -0
- package/server/engine/agent/hooks.js +354 -0
- package/server/engine/agent/init-contract.js +69 -0
- package/server/engine/agent/isolation.js +159 -0
- package/server/engine/agent/model-context.js +359 -0
- package/server/engine/agent/model-table.js +292 -0
- package/server/engine/agent/plugin-loader.js +266 -0
- package/server/engine/agent/prompts/nodesign-prelude.md +336 -0
- package/server/engine/agent/prompts/tools/ask-user-question-protocol.md +71 -0
- package/server/engine/agent/prompts/tools/direct-edit-protocol.md +98 -0
- package/server/engine/agent/prompts/tools/generate-image-cookbook.gemini-gateway.md +583 -0
- package/server/engine/agent/prompts/tools/generate-image-cookbook.md +483 -0
- package/server/engine/agent/prompts/tools/hybrid-reference.md +100 -0
- package/server/engine/agent/prompts/tools/paint-still-cookbook.md +270 -0
- package/server/engine/agent/prompts/tools/roll-film-cookbook.md +70 -0
- package/server/engine/agent/prompts/tools/site-reference.md +106 -0
- package/server/engine/agent/prompts/tools/tweaks-syntax.md +125 -0
- package/server/engine/agent/prompts/tools/vision-checker-dispatch.md +48 -0
- package/server/engine/agent/session-loop.js +1024 -0
- package/server/engine/agent/session-model.js +164 -0
- package/server/engine/agent/skill.js +204 -0
- package/server/engine/agent/system-prompts.js +86 -0
- package/server/engine/agent/task-events.js +78 -0
- package/server/engine/agents/ds-extractor.md +132 -0
- package/server/engine/agents/explorer.md +190 -0
- package/server/engine/agents/index.js +230 -0
- package/server/engine/agents/schemas/design-system.json +156 -0
- package/server/engine/agents/schemas/tweak-schema.json +113 -0
- package/server/engine/agents/tweak-proposer.md +127 -0
- package/server/engine/agents/vision-checker.md +254 -0
- package/server/engine/browse/capture.js +422 -0
- package/server/engine/browse/card.js +137 -0
- package/server/engine/browse/handover.js +59 -0
- package/server/engine/browse/page-digest.js +119 -0
- package/server/engine/browse/refs.js +196 -0
- package/server/engine/browse/registry.js +307 -0
- package/server/engine/browse/screencast.js +170 -0
- package/server/engine/browse/state.js +121 -0
- package/server/engine/chatai/chat-log.js +79 -0
- package/server/engine/chatai/index.js +193 -0
- package/server/engine/chatai/openai-compat.js +152 -0
- package/server/engine/chatai/orchestrate.js +309 -0
- package/server/engine/chatai/perform.js +40 -0
- package/server/engine/chatai/summarize.js +79 -0
- package/server/engine/mcp/capability-gate.js +52 -0
- package/server/engine/mcp/index.js +339 -0
- package/server/engine/mcp/param-sanitizer.js +142 -0
- package/server/engine/mcp/tools/arrange-on-board.js +124 -0
- package/server/engine/mcp/tools/artifact-session.js +278 -0
- package/server/engine/mcp/tools/browse-computer.js +421 -0
- package/server/engine/mcp/tools/browse-find-batch.js +183 -0
- package/server/engine/mcp/tools/browse-screenshot.js +157 -0
- package/server/engine/mcp/tools/browse.js +480 -0
- package/server/engine/mcp/tools/build-docx.js +101 -0
- package/server/engine/mcp/tools/clear-pending-changes.js +99 -0
- package/server/engine/mcp/tools/create-on-board.js +155 -0
- package/server/engine/mcp/tools/crystallize-skill.js +168 -0
- package/server/engine/mcp/tools/deliver-files.js +0 -0
- package/server/engine/mcp/tools/explain-style.js +237 -0
- package/server/engine/mcp/tools/export-handoff.js +133 -0
- package/server/engine/mcp/tools/expose-tweaks.js +149 -0
- package/server/engine/mcp/tools/generate-image.js +842 -0
- package/server/engine/mcp/tools/get-computed-styles.js +164 -0
- package/server/engine/mcp/tools/get-pending-changes.js +205 -0
- package/server/engine/mcp/tools/h3box-ssh.js +84 -0
- package/server/engine/mcp/tools/helpers/acquire-page.js +82 -0
- package/server/engine/mcp/tools/helpers/motion-lab.js +554 -0
- package/server/engine/mcp/tools/helpers/motion-scroll.js +112 -0
- package/server/engine/mcp/tools/helpers/perception-page.js +215 -0
- package/server/engine/mcp/tools/helpers/reference-download.js +103 -0
- package/server/engine/mcp/tools/helpers/rembg-bridge.py +80 -0
- package/server/engine/mcp/tools/helpers/rembg.js +273 -0
- package/server/engine/mcp/tools/helpers/shot-pipeline.js +280 -0
- package/server/engine/mcp/tools/highlight.js +68 -0
- package/server/engine/mcp/tools/list-pages.js +222 -0
- package/server/engine/mcp/tools/lookup-tags.js +59 -0
- package/server/engine/mcp/tools/navigate-to-page.js +60 -0
- package/server/engine/mcp/tools/organize-board.js +64 -0
- package/server/engine/mcp/tools/paint-still.js +434 -0
- package/server/engine/mcp/tools/pin-to-board.js +130 -0
- package/server/engine/mcp/tools/preview-deck.js +130 -0
- package/server/engine/mcp/tools/profile-scroll.js +305 -0
- package/server/engine/mcp/tools/publish-site.js +102 -0
- package/server/engine/mcp/tools/query-elements.js +195 -0
- package/server/engine/mcp/tools/read-board.js +103 -0
- package/server/engine/mcp/tools/read-document.js +93 -0
- package/server/engine/mcp/tools/read-page.js +264 -0
- package/server/engine/mcp/tools/read-tavern-json.js +143 -0
- package/server/engine/mcp/tools/record-decision.js +140 -0
- package/server/engine/mcp/tools/relate-on-board.js +110 -0
- package/server/engine/mcp/tools/remove-background.js +391 -0
- package/server/engine/mcp/tools/report-issue.js +115 -0
- package/server/engine/mcp/tools/roll-film.js +266 -0
- package/server/engine/mcp/tools/screenshot-docx.js +138 -0
- package/server/engine/mcp/tools/screenshot-url.js +195 -0
- package/server/engine/mcp/tools/screenshot.js +583 -0
- package/server/engine/mcp/tools/tier-gate.js +96 -0
- package/server/engine/mcp/tools/trace-motion.js +215 -0
- package/server/engine/mcp/tools/web-search.js +548 -0
- package/server/engine/motion/inventory.js +349 -0
- package/server/engine/perception/session.js +235 -0
- package/server/engine/plugins/nodesign/.claude-plugin/plugin.json +5 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/SKILL.md +192 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/canvas.template.html +667 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/hybrid-grid.md +22 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/image-led-cover.md +25 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/portrait.md +23 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/quote-backdrop.md +21 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/section-divider.md +23 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/text-led.md +21 -0
- package/server/engine/plugins/nodesign/skills/docx-craft/SKILL.md +162 -0
- package/server/engine/plugins/nodesign/skills/docx-craft/references/token-schema.md +407 -0
- package/server/engine/plugins/nodesign/skills/docx-craft//346/226/207/346/241/243.template.json +80 -0
- package/server/engine/plugins/nodesign/skills/rp-craft/SKILL.md +228 -0
- package/server/engine/plugins/nodesign/skills/rp-craft/patterns//346/274/224/345/207/272/351/241/265-/346/234/200/345/260/217/345/256/236/347/216/260.html +78 -0
- package/server/engine/plugins/nodesign/skills/rp-craft//346/274/224/345/207/272.template.js +268 -0
- package/server/engine/plugins/nodesign/skills/site-craft/SKILL.md +323 -0
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/build-lane.md +134 -0
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/cutout-collage.md +61 -0
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/mock-app.md +163 -0
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/page-transitions.md +86 -0
- package/server/engine/runs/active-runs.js +698 -0
- package/server/engine/runs/live-turn.js +252 -0
- package/server/engine/runs/store.js +364 -0
- package/server/engine/runs/turn-relay.js +217 -0
- package/server/engine/runtime/workspace.js +164 -0
- package/server/index.js +235 -0
- package/server/lib/artifact-file-path.js +80 -0
- package/server/lib/artifact-target.js +284 -0
- package/server/lib/asset-refs.js +239 -0
- package/server/lib/async-queue.js +104 -0
- package/server/lib/auto-relations.js +130 -0
- package/server/lib/binding-types.js +60 -0
- package/server/lib/board-kind-sizes.js +62 -0
- package/server/lib/board-relations.js +133 -0
- package/server/lib/browse-proxy.js +180 -0
- package/server/lib/canvas-id.js +36 -0
- package/server/lib/cover.js +227 -0
- package/server/lib/danbooru-tags.js +291 -0
- package/server/lib/doc-extract.js +156 -0
- package/server/lib/docx/build-from-source.js +260 -0
- package/server/lib/docx/build.js +490 -0
- package/server/lib/docx/dump-styles.js +287 -0
- package/server/lib/docx/fonts/nodesign-cjk.conf +164 -0
- package/server/lib/docx/merge-runs.js +129 -0
- package/server/lib/docx/numbering.js +218 -0
- package/server/lib/docx/order.js +152 -0
- package/server/lib/docx/rawzip.js +172 -0
- package/server/lib/docx/render.js +92 -0
- package/server/lib/docx/text-lint.js +225 -0
- package/server/lib/docx/tokens.js +335 -0
- package/server/lib/docx/units.js +29 -0
- package/server/lib/docx/xml.js +291 -0
- package/server/lib/docx-pages.js +187 -0
- package/server/lib/export-collect.js +289 -0
- package/server/lib/export-package.js +210 -0
- package/server/lib/html-srcset.js +145 -0
- package/server/lib/image-variant.js +460 -0
- package/server/lib/ingress/forward-openai-chat.js +305 -0
- package/server/lib/ingress/openai-chat.js +491 -0
- package/server/lib/ingress/session-notice.js +66 -0
- package/server/lib/ingress/session-routes.js +68 -0
- package/server/lib/ingress/slot-probe.js +130 -0
- package/server/lib/ingress/upstream-billing.js +52 -0
- package/server/lib/ingress/upstream-fail-streak.js +72 -0
- package/server/lib/ingress/upstream-truncation.js +48 -0
- package/server/lib/issues-store.js +182 -0
- package/server/lib/kinds/deck.js +72 -0
- package/server/lib/kinds/docx.js +283 -0
- package/server/lib/kinds/file-kinds.js +113 -0
- package/server/lib/kinds/index.js +202 -0
- package/server/lib/kinds/site.js +161 -0
- package/server/lib/model-ingress.js +543 -0
- package/server/lib/moderation.js +255 -0
- package/server/lib/notice-store.js +103 -0
- package/server/lib/plugin-install.js +104 -0
- package/server/lib/plugin-validator.js +721 -0
- package/server/lib/publish-store.js +115 -0
- package/server/lib/quick-summary.js +52 -0
- package/server/lib/quota.js +270 -0
- package/server/lib/rate-window.js +29 -0
- package/server/lib/reference-assets.js +92 -0
- package/server/lib/region-shot.js +135 -0
- package/server/lib/safe-path.js +73 -0
- package/server/lib/sdk-session.js +33 -0
- package/server/lib/showcase-store.js +92 -0
- package/server/lib/site-publish.js +465 -0
- package/server/lib/ssrf-guard.js +432 -0
- package/server/lib/task-scan.js +158 -0
- package/server/lib/tavern-json.js +154 -0
- package/server/lib/video-variant.js +237 -0
- package/server/lib/workspace-path.js +33 -0
- package/server/ops/fix-sdk-musl.mjs +40 -0
- package/server/ops/install-macos-fonts.sh +114 -0
- package/server/ops/macos-fonts.conf +336 -0
- package/server/ops/sandbox-shim/bwrap +57 -0
- package/server/projects/assets-summary.js +119 -0
- package/server/projects/auto-name.js +53 -0
- package/server/projects/board-store.js +640 -0
- package/server/projects/move-entry.js +103 -0
- package/server/projects/store.js +270 -0
- package/server/projects/ui-config.js +54 -0
- package/server/projects/workspace-templates.js +53 -0
- package/server/projects/workspace.js +1025 -0
- package/server/runtime/capabilities.js +153 -0
- package/server/runtime/local-config.js +177 -0
- package/server/runtime/local-env.js +88 -0
- package/server/runtime/platform.js +342 -0
- package/server/runtime/profile.js +72 -0
- package/server/services/rembg-launcher.js +313 -0
- package/server/services/rembg-service.py +334 -0
- package/server/shared/README.md +25 -0
- package/server/shared/deck.js +54 -0
- package/server/shared/time.js +49 -0
- package/server/style-pipeline/README.md +53 -0
- package/server/ws/broker.js +36 -0
- package/server/ws/browse-channel.js +177 -0
- package/server/ws/index.js +386 -0
- package/web/README.md +89 -0
- package/web/dist/assets/BrowserWindow-Dc7J7CYc.js +11 -0
- package/web/dist/assets/DeckWindow-4G0xX7P3.js +16 -0
- package/web/dist/assets/DocxWindow-BXQsoPkh.js +16 -0
- package/web/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/web/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/web/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/web/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/web/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/web/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/web/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/web/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/web/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/web/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/web/dist/assets/SiteWindow-D6eS-2gM.js +18 -0
- package/web/dist/assets/again-D8tfgKKO.webp +0 -0
- package/web/dist/assets/caveat-nd-CP6HlsNg.woff2 +0 -0
- package/web/dist/assets/clock-xJhvfYpS.webp +0 -0
- package/web/dist/assets/film-cut-BBKMwH7q.webp +0 -0
- package/web/dist/assets/film-still-BZ_DL9Tt.webp +0 -0
- package/web/dist/assets/index-C0KUf6Wd.css +1 -0
- package/web/dist/assets/index-Cbl3ATvp.js +2125 -0
- package/web/dist/assets/ink-desk-BRbxeDsR.webp +0 -0
- package/web/dist/assets/ink-night-Bz4BT3XH.webp +0 -0
- package/web/dist/assets/ink-portrait-C0DM4V8Q.webp +0 -0
- package/web/dist/assets/longcang-regular-C0uvTuKa.woff2 +0 -0
- package/web/dist/assets/lxgw-nd-bold-DbRfiTgo.woff2 +0 -0
- package/web/dist/assets/lxgw-nd-regular-CpMw9Ehg.woff2 +0 -0
- package/web/dist/assets/pending-edit-apply-DTjo2geW.js +31 -0
- package/web/dist/assets/question-8H4HSqxh.webp +0 -0
- package/web/dist/assets/reject-BaPJqElj.webp +0 -0
- package/web/dist/assets/rotate-cw-LMT3x1Zj.js +11 -0
- package/web/dist/assets/rp-portrait-CbrGpew2.webp +0 -0
- package/web/dist/assets/rp-street-C4jVKjxx.webp +0 -0
- package/web/dist/assets/square-dashed-mouse-pointer-CBoAkfhi.js +11 -0
- package/web/dist/assets/tangle-DIxHVNuT.webp +0 -0
- package/web/dist/assets/thisone-BylZKglV.webp +0 -0
- package/web/dist/assets/thumb-DO1wfK1I.webp +0 -0
- package/web/dist/index.html +18 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* server/engine/browse/screencast.js — 把 agent 的浏览器画面推给用户(2026-08-18)
|
|
3
|
+
*
|
|
4
|
+
* 用户要的形态:「**用通用显示器来播放 agent 当前浏览器操作画面,必要时人类可以
|
|
5
|
+
* 协助 agent**」。这个模块是那条像素通路加上行输入。
|
|
6
|
+
*
|
|
7
|
+
* ## 为什么是 CDP screencast(而不是轮询截图)
|
|
8
|
+
*
|
|
9
|
+
* ⭐ **screencast 是 damage-driven**(实测):页面静止时 30 秒只发 1 帧、0.1% 单核;
|
|
10
|
+
* 一次 DOM 改动 = 一帧。所以代价全在「页面在动 + 有人看」,静止时近乎免费。
|
|
11
|
+
* 周期性 `page.screenshot()` 轮询正好丢掉这个免费午餐 —— 页面动不动都要编码一张,
|
|
12
|
+
* 在 1 vCPU 上更差。
|
|
13
|
+
*
|
|
14
|
+
* ## 1 vCPU 上的账(⚠️ 实测纠正过一次归因)
|
|
15
|
+
*
|
|
16
|
+
* ⚠️ **这一节我写错过一次,纠正如下。**
|
|
17
|
+
*
|
|
18
|
+
* 第一版量的是「动画页 + 没人订阅 85.6% / 有人订阅 86.6%」,据此写了「贵的是页面
|
|
19
|
+
* 自己在动,画面流几乎免费」。两处都错:
|
|
20
|
+
* - 基线**本来就吃满核了**(只剩 ~14pp 余量),推流的成本被天花板挡住
|
|
21
|
+
* - 用的是 `ps` 的 `pcpu`,那是进程**生命周期平均值**,量不出增量
|
|
22
|
+
*
|
|
23
|
+
* 改用 `/proc` 的 utime+stime 差分 + 稳态等待,在**有余量**的页面上量两次:
|
|
24
|
+
*
|
|
25
|
+
* 小元素每 100ms 变一次:基线 1.3% → 订阅中 7.6% → 退订 1.8%(漂移 0.5pp)
|
|
26
|
+
* → 净成本 **+6.0pp @ 2.0 fps**
|
|
27
|
+
* 小元素 rAF 每帧变: 基线 4.0% → 订阅中 43.5% → 退订 4.1%(漂移 0.2pp)
|
|
28
|
+
* → 净成本 **+39.4pp @ 12.2 fps**
|
|
29
|
+
*
|
|
30
|
+
* 两点独立,每 fps 约 **3.1pp 单核**。所以:**满帧推流要吃掉约 40% 单核**,
|
|
31
|
+
* `MAX_ACTIVE = 1` 恰恰是唯一能把这个核还回来的刹车 —— 不是"便宜无害的装饰"。
|
|
32
|
+
* (待考虑没做:没人看时用 `Emulation.setVirtualTimePolicy` 把页面本身冻住,
|
|
33
|
+
* 能再省下页面动画那部分,但有些页面 resume 会坏,要真跑一批站才敢上。)
|
|
34
|
+
*
|
|
35
|
+
* 三道刹车:
|
|
36
|
+
* 1. **帧率硬压**:`everyNthFrame=5`(60fps 动画 → ~12fps)、`quality=45`、`maxWidth=1024`
|
|
37
|
+
* 2. ⭐ **ack 门控背压**:CDP 的 screencast 是 ack 驱动的 —— 只在 socket 没堵
|
|
38
|
+
* (`bufferedAmount` 低于阈值)时才 `screencastFrameAck`。socket 堵住就不 ack,
|
|
39
|
+
* chromium 自然停发下一帧。**把"没人接得住"直接变成"不生产"**,不丢帧也不排队膨胀。
|
|
40
|
+
* 3. **没人看就停**:最后一个订阅者断开 → 立刻 `stopScreencast`,不等浏览器空闲回收。
|
|
41
|
+
* 同时活跃画面流全局 ≤1(**这条是 CPU 定的,不是内存**)。
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/** 帧参数:1 vCPU 下的取舍,别调高(改这里前先拿 `top` 量一遍) */
|
|
45
|
+
const CAST = { format: 'jpeg', quality: 45, maxWidth: 1024, maxHeight: 700, everyNthFrame: 5 };
|
|
46
|
+
/** socket 里积压超过这个就不再 ack(= 让 chromium 停发) */
|
|
47
|
+
const BACKPRESSURE_BYTES = 256 * 1024;
|
|
48
|
+
/** 全局同时只允许一路活跃画面流。**这是 CPU 的主要保护**:满帧推流约 40% 单核
|
|
49
|
+
* (见文件头的实测),这台机器只有一个核。 */
|
|
50
|
+
const MAX_ACTIVE = 1;
|
|
51
|
+
|
|
52
|
+
/** projectId → { page, cdp, subs:Set<ws>, meta } */
|
|
53
|
+
const casts = new Map();
|
|
54
|
+
|
|
55
|
+
function activeCount() {
|
|
56
|
+
let n = 0;
|
|
57
|
+
for (const c of casts.values()) if (c.subs.size) n += 1;
|
|
58
|
+
return n;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 让某个 socket 订阅某项目的画面。
|
|
63
|
+
* @returns {Promise<{ok:true}|{ok:false, reason:string}>}
|
|
64
|
+
*/
|
|
65
|
+
let subscribing = false; // 绕过二的锁,见下
|
|
66
|
+
|
|
67
|
+
export async function subscribe(projectId, ws, page) {
|
|
68
|
+
let cast = casts.get(projectId);
|
|
69
|
+
// ⛔ **上限检查要在"这一路要不要开始编码"上判,不是在"有没有这个条目"上判**
|
|
70
|
+
// (审查攻出来的两个绕过):
|
|
71
|
+
// ① 退订只清 subs、**不删 casts 里的条目**,于是"再订一次"会走进 `cast` 已存在
|
|
72
|
+
// 那一支,整个跳过上限检查 —— 两路同时推流。
|
|
73
|
+
// ② 检查之后紧跟着 await(建 CDP 会话),两个并发 subscribe 都能过。
|
|
74
|
+
const willStartCasting = !cast || !cast.casting;
|
|
75
|
+
if (willStartCasting) {
|
|
76
|
+
if (subscribing) {
|
|
77
|
+
return { ok: false, reason: '另一路画面正在建立,稍等一下再打开这扇窗。' };
|
|
78
|
+
}
|
|
79
|
+
if (activeCount() >= MAX_ACTIVE) {
|
|
80
|
+
// ⚠️ 这个上限是**全机器**的(CPU 是全机器的),所以占着它的那一路很可能
|
|
81
|
+
// 属于**别的项目、甚至别的用户**。原来的话术是"先关掉另一扇浏览器窗",
|
|
82
|
+
// 那扇窗他既看不见也关不掉 —— 一句让人白忙的假指令。说实话就行。
|
|
83
|
+
const mine = casts.get(projectId)?.subs?.size ? '(就是这个项目自己的那扇)' : '(在别的会话里,你看不到也关不掉)';
|
|
84
|
+
return { ok: false, reason: `这台机器同时只能推一路浏览器画面,现在有一路在跑${mine}。`
|
|
85
|
+
+ '满帧推流约吃 40% 个核(实测),而这里只有 1 个核。等那边看完再打开,'
|
|
86
|
+
+ '或者让 agent 用截图代替实时画面。' };
|
|
87
|
+
}
|
|
88
|
+
subscribing = true;
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
if (!cast) {
|
|
92
|
+
const cdp = await page.context().newCDPSession(page);
|
|
93
|
+
cast = { projectId, page, cdp, subs: new Set(), meta: null, casting: false };
|
|
94
|
+
casts.set(projectId, cast);
|
|
95
|
+
|
|
96
|
+
cdp.on('Page.screencastFrame', (ev) => {
|
|
97
|
+
cast.meta = ev.metadata; // 里面有 deviceWidth/deviceHeight —— 接手时坐标换算要用
|
|
98
|
+
const buf = Buffer.from(ev.data, 'base64');
|
|
99
|
+
let anyoneKeepingUp = false;
|
|
100
|
+
for (const sock of cast.subs) {
|
|
101
|
+
if (sock.readyState !== 1) continue;
|
|
102
|
+
// ⭐ 堵住的 socket 直接跳过这一帧(画面会掉帧,但不会积压)
|
|
103
|
+
if (sock.bufferedAmount > BACKPRESSURE_BYTES) continue;
|
|
104
|
+
anyoneKeepingUp = true;
|
|
105
|
+
try { sock.send(buf, { binary: true, compress: false }); } catch { /* 下一帧再说 */ }
|
|
106
|
+
}
|
|
107
|
+
// ack 决定 chromium 发不发下一帧:没人接得住就不 ack = 不生产
|
|
108
|
+
if (anyoneKeepingUp) {
|
|
109
|
+
cdp.send('Page.screencastFrameAck', { sessionId: ev.sessionId }).catch(() => {});
|
|
110
|
+
} else {
|
|
111
|
+
// 留一条慢速心跳,免得所有人都堵住之后再也不恢复
|
|
112
|
+
setTimeout(() => {
|
|
113
|
+
cdp.send('Page.screencastFrameAck', { sessionId: ev.sessionId }).catch(() => {});
|
|
114
|
+
}, 500).unref?.();
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
cast.subs.add(ws);
|
|
120
|
+
if (!cast.casting) {
|
|
121
|
+
await cast.cdp.send('Page.startScreencast', CAST);
|
|
122
|
+
cast.casting = true;
|
|
123
|
+
// ⭐ 立刻补一帧。screencast 是 damage-driven —— 静止页面订阅之后可能**一帧都不发**,
|
|
124
|
+
// 用户看到的是一扇全白的窗,而它其实是"连上了但页面没动"。
|
|
125
|
+
// 这条是审查锐化出来的:接手功能在静止页面上由构造决定会失败(窗白着没法点)。
|
|
126
|
+
try {
|
|
127
|
+
const shot = await cast.cdp.send('Page.captureScreenshot', { format: 'jpeg', quality: CAST.quality });
|
|
128
|
+
const buf = Buffer.from(shot.data, 'base64');
|
|
129
|
+
for (const sock of cast.subs) {
|
|
130
|
+
if (sock.readyState === 1) sock.send(buf, { binary: true, compress: false });
|
|
131
|
+
}
|
|
132
|
+
} catch { /* 补不上就等页面自己动 */ }
|
|
133
|
+
}
|
|
134
|
+
return { ok: true };
|
|
135
|
+
} finally {
|
|
136
|
+
if (willStartCasting) subscribing = false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** 退订。最后一个人走了就停止编码。 */
|
|
141
|
+
export async function unsubscribe(projectId, ws) {
|
|
142
|
+
const cast = casts.get(projectId);
|
|
143
|
+
if (!cast) return;
|
|
144
|
+
cast.subs.delete(ws);
|
|
145
|
+
if (cast.subs.size) return;
|
|
146
|
+
if (cast.casting) {
|
|
147
|
+
await cast.cdp.send('Page.stopScreencast').catch(() => {});
|
|
148
|
+
cast.casting = false;
|
|
149
|
+
}
|
|
150
|
+
// 条目留着也没用(下次订阅要重新 startScreencast),而留着正好造出上面那个
|
|
151
|
+
// "跳过上限检查"的绕过。删掉 —— `activeCount()` 数的是"有订阅者的路",
|
|
152
|
+
// 但少一个空壳条目也少一处可以钻的缝。
|
|
153
|
+
casts.delete(projectId);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** 浏览器被关掉时(空闲回收 / LRU 淘汰)把这一路彻底忘掉 */
|
|
157
|
+
export function forget(projectId) {
|
|
158
|
+
const cast = casts.get(projectId);
|
|
159
|
+
if (!cast) return;
|
|
160
|
+
casts.delete(projectId);
|
|
161
|
+
for (const sock of cast.subs) {
|
|
162
|
+
try { sock.send(JSON.stringify({ type: 'closed', reason: 'browser was shut down' })); } catch { /* */ }
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** 当前帧的设备像素尺寸 —— 接手时前端坐标要按它换算 */
|
|
167
|
+
export function frameMeta(projectId) {
|
|
168
|
+
return casts.get(projectId)?.meta ?? null;
|
|
169
|
+
}
|
|
170
|
+
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* server/engine/browse/state.js — 浏览器在桌面上留下的那点持久痕迹(2026-08-18)
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么需要它
|
|
5
|
+
*
|
|
6
|
+
* 浏览器实例本身是**进程内的、易失的**(`registry.js` 的 Map,空闲 5 分钟就回收,
|
|
7
|
+
* pm2 重启就没了)。这是对的 —— 浏览器真的没了。
|
|
8
|
+
*
|
|
9
|
+
* 但用户要的是「**桌面上有一张浏览器卡片,随时能进去看和用**」(2026-08-18 拍板,
|
|
10
|
+
* 推翻了当初 BrowserWindow 里那句「你不会想在桌面上永久摆着一张某次浏览的卡片」)。
|
|
11
|
+
* 卡片要活得比实例长:agent 昨天逛过的站,今天打开项目还应该看得见那张卡,双击
|
|
12
|
+
* 就回到那一页。所以要有一点**落盘**的状态。
|
|
13
|
+
*
|
|
14
|
+
* ## 落哪儿
|
|
15
|
+
*
|
|
16
|
+
* `<pid>/.browser/`,跟 chromium 的 profile 同级、在 `shared/` **外面**。
|
|
17
|
+
* 理由跟 profile 一样(registry.js 里核过):`shared/` 是 agent 的 cwd 也是
|
|
18
|
+
* artifact-file 的服务根,放进去等于让 agent 能 Read 自己的浏览历史、
|
|
19
|
+
* 还会被当文件服出去、还进 per-project git。删项目时 `<pid>/` 整个删掉,
|
|
20
|
+
* 这里跟着走,不用另写清理。
|
|
21
|
+
*
|
|
22
|
+
* ## 两份东西,两种性质
|
|
23
|
+
*
|
|
24
|
+
* - `state.json` —— 上次在哪一页(`{url, title, at}`)。导航/点击后写,**必须极便宜
|
|
25
|
+
* 且永不抛**:它长在导航路径上,为了一张卡片让 agent 的导航失败是划不来的。
|
|
26
|
+
* - `last.webp` —— 上次看到的样子(卡片上那块预览)。
|
|
27
|
+
* ⭐ 只在**本来就截了图**的时候顺手存(`browser_screenshot` 的视口档),
|
|
28
|
+
* 或者用户正在看画布、preview 端点现截一张。**不为了刷新卡片主动截图** ——
|
|
29
|
+
* 实测视口截图在 stripe/小红书这种站上要 1.7~4.2 秒,而这台机器只有 1 个核。
|
|
30
|
+
*
|
|
31
|
+
* 卡上的预览因此是「**上次看到的样子**」,跟 word 卡是同一路数(那也是一张静态页图)。
|
|
32
|
+
* 活的画面流只在窗打开时给:实测每 fps 约 3.1pp 单核,满帧 40%。
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import path from 'node:path';
|
|
36
|
+
import fs from 'node:fs/promises';
|
|
37
|
+
import { getProjectWorkspace } from '../../projects/workspace.js';
|
|
38
|
+
|
|
39
|
+
const dirOf = (projectId) => path.join(getProjectWorkspace(projectId), '.browser');
|
|
40
|
+
const STATE_FILE = 'state.json';
|
|
41
|
+
const FRAME_FILE = 'last.webp';
|
|
42
|
+
|
|
43
|
+
/** 卡片预览的绝对路径(存在与否由调用方 stat) */
|
|
44
|
+
export function framePath(projectId) {
|
|
45
|
+
return path.join(dirOf(projectId), FRAME_FILE);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 记一笔「现在在这一页」。**永不抛** —— 它挂在导航路径上。
|
|
50
|
+
* @param {string} projectId
|
|
51
|
+
* @param {import('playwright').Page} page
|
|
52
|
+
*/
|
|
53
|
+
export async function recordVisit(projectId, page) {
|
|
54
|
+
try {
|
|
55
|
+
const url = page.url();
|
|
56
|
+
if (!/^https?:/.test(url)) return; // about:blank / chrome-error 不值得记
|
|
57
|
+
const title = await page.title().catch(() => '');
|
|
58
|
+
const dir = dirOf(projectId);
|
|
59
|
+
await fs.mkdir(dir, { recursive: true });
|
|
60
|
+
await fs.writeFile(path.join(dir, STATE_FILE), `${JSON.stringify({
|
|
61
|
+
url, title: title || null, at: new Date().toISOString(),
|
|
62
|
+
}, null, 2)}\n`, 'utf8');
|
|
63
|
+
} catch { /* 记不下来就算了,不能因此让导航失败 */ }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 上次在哪一页。没逛过 → null(桌面上就没有这张卡)。
|
|
68
|
+
* @returns {Promise<{url: string, title: string|null, at: string}|null>}
|
|
69
|
+
*/
|
|
70
|
+
export async function readVisit(projectId) {
|
|
71
|
+
try {
|
|
72
|
+
const raw = await fs.readFile(path.join(dirOf(projectId), STATE_FILE), 'utf8');
|
|
73
|
+
const v = JSON.parse(raw);
|
|
74
|
+
return (v && typeof v.url === 'string' && /^https?:/.test(v.url)) ? v : null;
|
|
75
|
+
} catch { return null; }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 存一帧当卡片预览。传的是原始截图 buffer,这里缩成卡片尺寸再落盘。
|
|
80
|
+
*
|
|
81
|
+
* 卡片预览区 640×400(`ARTIFACT_PREVIEW_H.browse`),主角档 ×1.5 = 960 宽,
|
|
82
|
+
* 所以存 1024 宽足够,再大是白占盘和白花编码时间。**永不抛。**
|
|
83
|
+
*/
|
|
84
|
+
export async function saveFrame(projectId, buf) {
|
|
85
|
+
try {
|
|
86
|
+
const { default: sharp } = await import('sharp');
|
|
87
|
+
const out = await sharp(buf).resize({ width: 1024, withoutEnlargement: true })
|
|
88
|
+
.webp({ quality: 72 }).toBuffer();
|
|
89
|
+
const dir = dirOf(projectId);
|
|
90
|
+
await fs.mkdir(dir, { recursive: true });
|
|
91
|
+
await fs.writeFile(path.join(dir, FRAME_FILE), out);
|
|
92
|
+
return out.length;
|
|
93
|
+
} catch { return 0; }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 忘掉这个项目的浏览痕迹 —— 桌面上那张卡随之消失。
|
|
98
|
+
*
|
|
99
|
+
* 为什么需要:卡片的存在判据是 `state.json` 在不在,而它一旦写下就永远在 ——
|
|
100
|
+
* 一个月前逛过一次的项目,桌面上会一直摆着那张卡赶不走。这是"卡活得比实例长"
|
|
101
|
+
* 的代价,得给个出口。
|
|
102
|
+
*
|
|
103
|
+
* ⚠️ **profile 不删**(`.browser/default/`):登录态是资产,用户点的是"这张卡
|
|
104
|
+
* 我不看了",不是"把我在那个站的登录清掉"。
|
|
105
|
+
*/
|
|
106
|
+
export async function forgetVisit(projectId) {
|
|
107
|
+
const dir = dirOf(projectId);
|
|
108
|
+
await Promise.all([
|
|
109
|
+
fs.unlink(path.join(dir, STATE_FILE)).catch(() => {}),
|
|
110
|
+
fs.unlink(path.join(dir, FRAME_FILE)).catch(() => {}),
|
|
111
|
+
]);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** 上次那张预览的字节 + mtime;没有 → null */
|
|
115
|
+
export async function readFrame(projectId) {
|
|
116
|
+
const file = framePath(projectId);
|
|
117
|
+
try {
|
|
118
|
+
const [buf, st] = await Promise.all([fs.readFile(file), fs.stat(file)]);
|
|
119
|
+
return { buf, mtime: st.mtime };
|
|
120
|
+
} catch { return null; }
|
|
121
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* chat-log.js —— 演出的对话记录与摘要落盘。
|
|
3
|
+
*
|
|
4
|
+
* 对话记录(默认 `对话.jsonl`,可在 编排.yaml 的 历史.文件 改):
|
|
5
|
+
* 一行一条 { seq, role: 'user'|'assistant', text, at }。
|
|
6
|
+
* ⭐ 只存双方真实发言,永远只追加、永远不删——摘要折叠掉的轮次仍然留在
|
|
7
|
+
* 文件里(编译时靠 摘要.json 的 `至` 跳过),全量记录随时可重放、可导出。
|
|
8
|
+
*
|
|
9
|
+
* 摘要(固定名 `摘要.json`,派生态不给改名旋钮):
|
|
10
|
+
* { 至, 内容, 时, 模型, 用量, 花费 } —— `至` 是被折叠的最后一条记录的 seq,
|
|
11
|
+
* 滚动覆盖写(旧摘要在生成新摘要时被并入,不单独存档)。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import fs from 'node:fs/promises';
|
|
16
|
+
|
|
17
|
+
export const SUMMARY_FILE = '摘要.json';
|
|
18
|
+
|
|
19
|
+
function logPath(dir, config) {
|
|
20
|
+
const rel = config?.历史?.文件 || '对话.jsonl';
|
|
21
|
+
const abs = path.resolve(dir, rel);
|
|
22
|
+
if (abs !== path.resolve(dir) && !abs.startsWith(path.resolve(dir) + path.sep)) {
|
|
23
|
+
throw Object.assign(new Error(`历史.文件「${rel}」跑出了演出文件夹`), { code: 'ORCH_INVALID' });
|
|
24
|
+
}
|
|
25
|
+
return abs;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** 读全量记录。文件不存在 = 空场;坏行跳过不炸(记录是追加型文件,尾行可能残缺)。 */
|
|
29
|
+
export async function readLog(dir, config) {
|
|
30
|
+
let raw;
|
|
31
|
+
try { raw = await fs.readFile(logPath(dir, config), 'utf8'); } catch { return []; }
|
|
32
|
+
const out = [];
|
|
33
|
+
for (const line of raw.split('\n')) {
|
|
34
|
+
const s = line.trim();
|
|
35
|
+
if (!s) continue;
|
|
36
|
+
try {
|
|
37
|
+
const r = JSON.parse(s);
|
|
38
|
+
if (Number.isInteger(r.seq) && (r.role === 'user' || r.role === 'assistant') && typeof r.text === 'string') {
|
|
39
|
+
out.push(r);
|
|
40
|
+
}
|
|
41
|
+
} catch { /* 残行跳过 */ }
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 追加一轮(user + assistant 成对写入,调用成功之后才落盘——失败的调用
|
|
48
|
+
* 不留半条记录,重试不会双写)。
|
|
49
|
+
*/
|
|
50
|
+
export async function appendTurn(dir, config, userText, assistantText) {
|
|
51
|
+
const records = await readLog(dir, config);
|
|
52
|
+
const last = records.length ? records[records.length - 1].seq : 0;
|
|
53
|
+
const at = new Date().toISOString();
|
|
54
|
+
const lines = [
|
|
55
|
+
{ seq: last + 1, role: 'user', text: String(userText), at },
|
|
56
|
+
{ seq: last + 2, role: 'assistant', text: String(assistantText), at },
|
|
57
|
+
];
|
|
58
|
+
await fs.appendFile(
|
|
59
|
+
logPath(dir, config),
|
|
60
|
+
lines.map(r => JSON.stringify(r)).join('\n') + '\n',
|
|
61
|
+
'utf8',
|
|
62
|
+
);
|
|
63
|
+
return lines;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function readSummary(dir) {
|
|
67
|
+
try {
|
|
68
|
+
const s = JSON.parse(await fs.readFile(path.join(dir, SUMMARY_FILE), 'utf8'));
|
|
69
|
+
return (s && Number.isInteger(s.至) && typeof s.内容 === 'string') ? s : null;
|
|
70
|
+
} catch { return null; }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** 原子写(tmp + rename):摘要生成要花一次真调用,别让并发读读到半个文件。 */
|
|
74
|
+
export async function writeSummary(dir, summary) {
|
|
75
|
+
const target = path.join(dir, SUMMARY_FILE);
|
|
76
|
+
const tmp = `${target}.tmp`;
|
|
77
|
+
await fs.writeFile(tmp, JSON.stringify(summary, null, 2), 'utf8');
|
|
78
|
+
await fs.rename(tmp, target);
|
|
79
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* chatai —— 第二条模型通路(跟 agent 会话完全无关)
|
|
3
|
+
*
|
|
4
|
+
* agent 走 Claude Agent SDK、有工具、会改文件;**chatai 只是一次补全调用**:
|
|
5
|
+
* 传进去 system + messages,回来一段文本和用量。它没有工具、没有会话、
|
|
6
|
+
* 改不了任何东西。
|
|
7
|
+
*
|
|
8
|
+
* 存在的理由不是省钱,是**别让高频生成挤掉设计会话**:订阅通路是全站共享的一个
|
|
9
|
+
* OAuth 额度加并发上限,而生成类动作频次高,一跑起来能把正经设计会话挤没。
|
|
10
|
+
*
|
|
11
|
+
* 通路配置走 env(`CHATAI_BASE_URL` / `CHATAI_API_KEY` / `CHATAI_MODEL`),
|
|
12
|
+
* 密钥不进代码不进 git。
|
|
13
|
+
*
|
|
14
|
+
* 计价:上游中转站的计量口径不透明,所以金额一律按**我们自己的单价表**算,
|
|
15
|
+
* 谁要拿它进配额闸门就用这个数。
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { callOpenAICompat } from './openai-compat.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 模型表 —— 单价、思考档位、中转站实名,一张表说完(2026-08-15 加思考档)。
|
|
22
|
+
*
|
|
23
|
+
* 单价 USD / 1M token。gemini-3.7-flash 是促销价,2027-01-01 起翻倍到 1.5 / 7.5。
|
|
24
|
+
* 思考 token 按输出计价(Gemini 的口径;Claude 的思考量直接混在 completion_tokens 里)。
|
|
25
|
+
*
|
|
26
|
+
* ⭐ **思考等级在中转站是模型名的一部分,不是请求参数**(2026-08-15 实测它家 100 个
|
|
27
|
+
* 模型:gemini 系是 -low/-medium/-high 三个独立模型名,claude 系是 base 与 -thinking
|
|
28
|
+
* 两个)。所以「档」不发 reasoning_effort,而是换实名。用户那边只看见标准名 + 一个
|
|
29
|
+
* 档位旋钮,换渠道时改这张表就行。
|
|
30
|
+
*
|
|
31
|
+
* 思耗 = 粗估的「思考 token ÷ 正文 token」倍数,只给设置页算预演帐目用,不进计费。
|
|
32
|
+
* 实测(演出类提示,2026-08-15):gemini flash 正文 234 / 思考 785 ≈ 3.4 倍,且
|
|
33
|
+
* 低/中/高 三档的思考量差异淹在噪声里(785/587/601),别把它当精确旋钮;
|
|
34
|
+
* claude 开思考后正文 363→408、输入 115→144(中转站往提示里塞了东西)。
|
|
35
|
+
*/
|
|
36
|
+
export const MODEL_SPECS = Object.freeze({
|
|
37
|
+
'gemini-3.7-flash': {
|
|
38
|
+
价: { in: 0.75, out: 3.75 }, 思耗: 3.4,
|
|
39
|
+
注: '$0.75 / $3.75 每百万 · 促销价,思考计入输出',
|
|
40
|
+
默认档: '低',
|
|
41
|
+
档: {
|
|
42
|
+
低: { 名: '反重力-流式抗截断/gemini-3.7-flash-low', 注: '最省,中转站的常规档' },
|
|
43
|
+
中: { 名: '反重力-流式抗截断/gemini-3.7-flash-medium', 注: '中档' },
|
|
44
|
+
高: { 名: '反重力-流式抗截断/gemini-3.7-flash-high', 注: '让它多想一会儿(实测三档差异有限,别指望它是精确旋钮)' },
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
'gemini-3.6-flash': {
|
|
48
|
+
价: { in: 0.75, out: 3.75 }, 思耗: 3.4,
|
|
49
|
+
注: '$0.75 / $3.75 每百万 · 上一版,行文更收敛',
|
|
50
|
+
默认档: '低',
|
|
51
|
+
档: {
|
|
52
|
+
低: { 名: '反重力-流式抗截断/gemini-3.6-flash-low', 注: '最省,中转站的常规档' },
|
|
53
|
+
中: { 名: '反重力-流式抗截断/gemini-3.6-flash-medium', 注: '中档' },
|
|
54
|
+
高: { 名: '反重力-流式抗截断/gemini-3.6-flash-high', 注: '让它多想一会儿' },
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
'gemini-3-flash': {
|
|
58
|
+
价: { in: 0.50, out: 2.50 }, 思耗: 3.0,
|
|
59
|
+
注: '$0.50 / $2.50 每百万 · 更便宜,思考档不可调',
|
|
60
|
+
实名: '反重力-流式抗截断/gemini-3-flash',
|
|
61
|
+
档: null,
|
|
62
|
+
},
|
|
63
|
+
'claude-sonnet-4-6': {
|
|
64
|
+
价: { in: 3.00, out: 15.00 }, 思耗: 1,
|
|
65
|
+
注: '$3 / $15 每百万 · 文笔更稳,一轮贵一个量级',
|
|
66
|
+
默认档: '关',
|
|
67
|
+
档: {
|
|
68
|
+
关: { 名: '反重力-流式抗截断/claude-sonnet-4-6', 思耗: 1, 注: '不开扩展思考' },
|
|
69
|
+
高: { 名: '反重力-流式抗截断/claude-sonnet-4-6-thinking', 思耗: 1.3, 注: '开扩展思考:思考走单独通道不进台词,但按输出计价' },
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
'claude-opus-4-6': {
|
|
73
|
+
价: { in: 15.00, out: 75.00 }, 思耗: 1,
|
|
74
|
+
注: '$15 / $75 每百万 · 只在真的在乎的场合;中转站常报没容量',
|
|
75
|
+
默认档: '关',
|
|
76
|
+
档: {
|
|
77
|
+
关: { 名: '反重力-claude-opus-4-6', 思耗: 1, 注: '不开扩展思考' },
|
|
78
|
+
高: { 名: '反重力-claude-opus-4-6-thinking', 思耗: 1.3, 注: '开扩展思考:思考不进台词,但按输出计价' },
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
/** 档位词表 —— 编排.yaml 只认这四个词,设置页只画模型支持的那几档 */
|
|
84
|
+
export const LEVELS = Object.freeze(['关', '低', '中', '高']);
|
|
85
|
+
|
|
86
|
+
/** 单价表(USD / 1M token),从模型表派生 —— 价格只有一处定义 */
|
|
87
|
+
export const PRICES = Object.freeze({
|
|
88
|
+
...Object.fromEntries(Object.entries(MODEL_SPECS).map(([k, v]) => [k, v.价])),
|
|
89
|
+
_default: { in: 1.00, out: 5.00 },
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
/** 从中转站的花式模型名里认出计价用的底模(`反重力-流式抗截断/gemini-3.7-flash-low`) */
|
|
93
|
+
export function priceKeyOf(model) {
|
|
94
|
+
const s = String(model || '').toLowerCase();
|
|
95
|
+
for (const k of Object.keys(PRICES)) {
|
|
96
|
+
if (k !== '_default' && s.includes(k)) return k;
|
|
97
|
+
}
|
|
98
|
+
return '_default';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** 按单价表估这次调用多少钱。缓存读按输入价一折(隐式缓存的通行口径) */
|
|
102
|
+
export function costOf(model, usage) {
|
|
103
|
+
const p = PRICES[priceKeyOf(model)];
|
|
104
|
+
const inTok = Math.max(0, (usage.inputTokens || 0) - (usage.cacheReadTokens || 0));
|
|
105
|
+
const cached = usage.cacheReadTokens || 0;
|
|
106
|
+
const outTok = (usage.outputTokens || 0) + (usage.reasoningTokens || 0);
|
|
107
|
+
return (inTok * p.in + cached * p.in * 0.1 + outTok * p.out) / 1e6;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function chataiConfig() {
|
|
111
|
+
const base = (process.env.CHATAI_BASE_URL || '').replace(/\/+$/, '');
|
|
112
|
+
const key = process.env.CHATAI_API_KEY || '';
|
|
113
|
+
const model = process.env.CHATAI_MODEL || '';
|
|
114
|
+
return { base, key, model, ok: !!(base && key && model) };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** 简写 → 标准名(历史遗留的手写值,别再往里加) */
|
|
118
|
+
const SHORTHAND = Object.freeze({ 'claude-sonnet': 'claude-sonnet-4-6' });
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 标准模型名 + 思考档 → 中转站实名(2026-08-15 时停之城 503 案 + 思考档)。
|
|
122
|
+
*
|
|
123
|
+
* 中转站的 100 个模型**没有一个裸标准名**,全是「反重力-流式抗截断/xxx」这类
|
|
124
|
+
* 前缀变体,思考等级也编在名字里。而 编排.yaml / 设置页 / skill 教的都是标准名
|
|
125
|
+
* (用户不该学中转站的命名黑话,换渠道也不用改配置)。这层在发请求前换成实名。
|
|
126
|
+
*
|
|
127
|
+
* 档给空或者这个模型不支持这档 → 落到模型的默认档(每个模型的默认档都是最便宜
|
|
128
|
+
* 那档),并把降级理由报回去:设置页只画支持的档,但 编排.yaml 是 agent 和人手写的,
|
|
129
|
+
* 写岔了不该拒开演出,只该说清楚跑的是哪档。
|
|
130
|
+
* 表里没有的模型名原样放行(直接写实名也合法)。env `CHATAI_MODEL_ALIASES`(JSON)
|
|
131
|
+
* 覆盖标准名→实名,写了就绕开档位(中转站哪天改名,改 env 不用动代码)。
|
|
132
|
+
*
|
|
133
|
+
* @returns {{ 名: string, 档: string|null, 降级: string|null, 思耗: number }}
|
|
134
|
+
*/
|
|
135
|
+
export function resolveModelVariant(model, 档) {
|
|
136
|
+
const empty = { 名: model, 档: null, 降级: null, 思耗: 1 };
|
|
137
|
+
if (!model) return empty;
|
|
138
|
+
let extra = {};
|
|
139
|
+
try { extra = JSON.parse(process.env.CHATAI_MODEL_ALIASES || '{}'); } catch { /* 坏 JSON 当没配 */ }
|
|
140
|
+
if (extra[model]) return { ...empty, 名: extra[model] };
|
|
141
|
+
|
|
142
|
+
const canonical = SHORTHAND[model] || model;
|
|
143
|
+
const spec = MODEL_SPECS[canonical];
|
|
144
|
+
if (!spec) return empty;
|
|
145
|
+
if (!spec.档) {
|
|
146
|
+
return {
|
|
147
|
+
名: spec.实名, 档: null, 思耗: spec.思耗,
|
|
148
|
+
降级: 档 ? `${canonical} 的思考档不可调,「${档}」没生效` : null,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
const 生效 = spec.档[档] ? 档 : spec.默认档;
|
|
152
|
+
const v = spec.档[生效];
|
|
153
|
+
return {
|
|
154
|
+
名: v.名, 档: 生效, 思耗: v.思耗 ?? spec.思耗,
|
|
155
|
+
降级: 档 && 生效 !== 档 ? `${canonical} 没有「${档}」档,按默认档「${生效}」跑` : null,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** 只要实名的老口径(默认档) */
|
|
160
|
+
export function resolveModelAlias(model) {
|
|
161
|
+
return resolveModelVariant(model).名;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** 给设置页的模型目录:单价、档位、注释都从这一张表来,前端不再各存一份 */
|
|
165
|
+
export function modelCatalog() {
|
|
166
|
+
return Object.entries(MODEL_SPECS).map(([id, s]) => ({
|
|
167
|
+
id, 入: s.价.in, 出: s.价.out, 注: s.注, 思耗: s.思耗,
|
|
168
|
+
档: s.档 ? Object.entries(s.档).map(([名, v]) => ({ 名, 思耗: v.思耗 ?? s.思耗, 注: v.注 ?? null })) : [],
|
|
169
|
+
默认档: s.默认档 ?? null,
|
|
170
|
+
}));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* 跑一次 chatai。
|
|
175
|
+
* @param {object} opts system / messages / model / 思考 / maxTokens / signal / onDelta
|
|
176
|
+
*/
|
|
177
|
+
export async function runChatai({
|
|
178
|
+
system = '', messages = [], model, 思考 = null, maxTokens = 2000, signal, onDelta,
|
|
179
|
+
} = {}) {
|
|
180
|
+
const cfg = chataiConfig();
|
|
181
|
+
if (!cfg.ok) {
|
|
182
|
+
throw Object.assign(
|
|
183
|
+
new Error('chatai 通路没配置(CHATAI_BASE_URL / CHATAI_API_KEY / CHATAI_MODEL)'),
|
|
184
|
+
{ code: 'CHATAI_UNCONFIGURED' },
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
const v = resolveModelVariant(model || cfg.model, 思考);
|
|
188
|
+
const out = await callOpenAICompat({
|
|
189
|
+
base: cfg.base, key: cfg.key, model: v.名,
|
|
190
|
+
system, messages, maxTokens, signal, onDelta,
|
|
191
|
+
});
|
|
192
|
+
return { ...out, model: v.名, 档: v.档, 降级: v.降级, costUsd: costOf(v.名, out.usage) };
|
|
193
|
+
}
|