@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,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* server/api/admin.js — 内测管理接口(2026-07-30)
|
|
3
|
+
*
|
|
4
|
+
* 全部挂 adminGuard(authGuard 已在外层挂好 req.user)。用户/邀请码还是走
|
|
5
|
+
* curl / server/scripts/invite.mjs;问题库有页面(/admin/issues)。
|
|
6
|
+
*
|
|
7
|
+
* POST /api/admin/invites {maxUses?, expiresInDays?} → 生成邀请码
|
|
8
|
+
* GET /api/admin/invites 邀请码列表(含用量)
|
|
9
|
+
* PATCH /api/admin/invites/:code {maxUses} 改总次数(0 = 封死)
|
|
10
|
+
* GET /api/admin/users 用户列表 + 今日用量
|
|
11
|
+
* PATCH /api/admin/users/:id {disabled?, dailyCostLimitUsd?, lifetimeCostLimitUsd?} 封禁/调限额
|
|
12
|
+
* GET /api/admin/issues harness 问题库(按次数降序)+ 按工具聚合
|
|
13
|
+
* PATCH /api/admin/issues/:id {status} open|ack|ignored|closed
|
|
14
|
+
* DELETE /api/admin/issues/:id 删掉一条
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import express from 'express';
|
|
18
|
+
import { adminGuard } from '../auth/middleware.js';
|
|
19
|
+
import { createInvite, listInvites, getInvite, updateInvite, listUsers, getUserById, updateUser } from '../auth/users-store.js';
|
|
20
|
+
import { tierOf, PLANS } from '../auth/tier.js';
|
|
21
|
+
import { usedCostToday, usedCostTotal, usedTokensToday, limitFor } from '../lib/quota.js';
|
|
22
|
+
import { listIssues, setIssueStatus, removeIssue, issueStats } from '../lib/issues-store.js';
|
|
23
|
+
import { createNotice, listNotices, getActiveNotice, retireNotice, retireAllNotices } from '../lib/notice-store.js';
|
|
24
|
+
import { flagCounts, listFlags, removeFlag, levelForKnob, LEVELS } from '../lib/moderation.js';
|
|
25
|
+
|
|
26
|
+
const router = express.Router();
|
|
27
|
+
router.use(adminGuard);
|
|
28
|
+
|
|
29
|
+
router.post('/invites', (req, res) => {
|
|
30
|
+
const maxUses = Number(req.body?.maxUses) || 1;
|
|
31
|
+
const days = Number(req.body?.expiresInDays);
|
|
32
|
+
const expiresAt = Number.isFinite(days) && days > 0
|
|
33
|
+
? new Date(Date.now() + days * 86400_000).toISOString() : null;
|
|
34
|
+
// grantLifetimeUsd:该码注册的号走终身额度(试用口径,不刷新)
|
|
35
|
+
const grant = Number(req.body?.grantLifetimeUsd);
|
|
36
|
+
const invite = createInvite({
|
|
37
|
+
createdBy: req.user.id,
|
|
38
|
+
maxUses: Math.max(1, Math.min(100, maxUses)),
|
|
39
|
+
expiresAt,
|
|
40
|
+
grantLifetimeUsd: Number.isFinite(grant) && grant > 0 ? grant : null,
|
|
41
|
+
});
|
|
42
|
+
res.status(201).json({ invite });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
router.get('/invites', (_req, res) => {
|
|
46
|
+
res.json({ invites: listInvites() });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// 改可用次数(总次数口径)。0 或 ≤ 已用数 = 封死该码 —— 泄漏时的止血阀,
|
|
50
|
+
// 所以这里不设下限 1,也不 clamp 到 100(长期公开码就是要给大数的)
|
|
51
|
+
router.patch('/invites/:code', (req, res) => {
|
|
52
|
+
if (!getInvite(req.params.code)) return res.status(404).json({ error: 'invite not found' });
|
|
53
|
+
const maxUses = Number(req.body?.maxUses);
|
|
54
|
+
if (!Number.isInteger(maxUses) || maxUses < 0 || maxUses > 100000) {
|
|
55
|
+
return res.status(400).json({ error: 'maxUses 需为 0-100000 的整数(0 = 封死)' });
|
|
56
|
+
}
|
|
57
|
+
res.json({ invite: updateInvite(req.params.code, { maxUses }) });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
router.get('/users', (_req, res) => {
|
|
61
|
+
const flags = flagCounts(); // 内容外审标记(lib/moderation.js)
|
|
62
|
+
const users = listUsers().map(u => ({
|
|
63
|
+
...u,
|
|
64
|
+
tier: tierOf(u), // admin | pro | basic(auth/tier.js 派生;前端章只认这个)
|
|
65
|
+
costToday: usedCostToday(u.id), // 美元,闸门真口径
|
|
66
|
+
costTotal: usedCostTotal(u.id), // 全史;试用号(lifetimeCostLimitUsd 非空)拿它对限额
|
|
67
|
+
tokensToday: usedTokensToday(u.id), // 参考
|
|
68
|
+
effectiveDailyLimitUsd: limitFor(u),
|
|
69
|
+
flagsCount: flags.get(u.id) || 0,
|
|
70
|
+
effectiveModerationLevel: levelForKnob(u, 'subscription'), // 订阅模型:默认档算完的实际生效值
|
|
71
|
+
effectiveModerationLevelApi: levelForKnob(u, 'api'), // 本地/中转旋钮的生效值
|
|
72
|
+
}));
|
|
73
|
+
res.json({ users });
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
router.patch('/users/:id', (req, res) => {
|
|
77
|
+
const user = getUserById(req.params.id);
|
|
78
|
+
if (!user) return res.status(404).json({ error: 'user not found' });
|
|
79
|
+
const patch = {};
|
|
80
|
+
if (typeof req.body?.disabled === 'boolean') patch.disabled = req.body.disabled;
|
|
81
|
+
// 外审强度:null = 跟随默认档(按档位:basic strict / pro loose / admin off,auth/tier.js)
|
|
82
|
+
// 两个旋钮(08-20):订阅模型 / 本地与中转(见 lib/moderation.js 文件头)
|
|
83
|
+
for (const key of ['moderationLevel', 'moderationLevelApi']) {
|
|
84
|
+
if (!(key in (req.body || {}))) continue;
|
|
85
|
+
const lv = req.body[key];
|
|
86
|
+
if (lv !== null && !LEVELS.includes(lv)) {
|
|
87
|
+
return res.status(400).json({ error: `${key} 需为 ${LEVELS.join('/')} 或 null` });
|
|
88
|
+
}
|
|
89
|
+
patch[key] = lv;
|
|
90
|
+
}
|
|
91
|
+
if ('localGen' in (req.body || {})) {
|
|
92
|
+
patch.localGen = !!req.body.localGen;
|
|
93
|
+
}
|
|
94
|
+
if ('plan' in (req.body || {})) {
|
|
95
|
+
// 08-21 晚:档位真相源(auth/tier.js)。订阅/生图/发布/外审默认档全从它派生;admin 的 plan 不可改(role 派生)
|
|
96
|
+
if (user.role === 'admin') return res.status(400).json({ error: 'admin 的档位由 role 派生,不可改' });
|
|
97
|
+
if (!PLANS.includes(req.body.plan)) return res.status(400).json({ error: `plan 需为 ${PLANS.join('/')}` });
|
|
98
|
+
patch.plan = req.body.plan;
|
|
99
|
+
}
|
|
100
|
+
// 07-31 起限额单位是美元。老字段 dailyTokenLimit 仍收(存量数据能改回去),
|
|
101
|
+
// 但它已经不参与闸门判断了 —— 真正生效的是 dailyCostLimitUsd。
|
|
102
|
+
for (const [key, label] of [['dailyCostLimitUsd', '美元'], ['lifetimeCostLimitUsd', '美元'], ['dailyTokenLimit', 'token']]) {
|
|
103
|
+
if (!(key in (req.body || {}))) continue;
|
|
104
|
+
const v = req.body[key];
|
|
105
|
+
if (v !== null && (!Number.isFinite(Number(v)) || Number(v) < 0)) {
|
|
106
|
+
return res.status(400).json({ error: `${key} 需为非负数(${label})或 null` });
|
|
107
|
+
}
|
|
108
|
+
patch[key] = v === null ? null : Number(v);
|
|
109
|
+
}
|
|
110
|
+
res.json({ user: updateUser(user.id, patch) });
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// ── 站内公告(2026-07-31)──
|
|
114
|
+
// 一次只有一条生效(取最新)。发新的等于覆盖旧的,不用先下架。
|
|
115
|
+
|
|
116
|
+
router.get('/notices', (_req, res) => {
|
|
117
|
+
res.json({ notices: listNotices(), active: getActiveNotice() });
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
router.post('/notices', (req, res) => {
|
|
121
|
+
try {
|
|
122
|
+
const notice = createNotice({
|
|
123
|
+
body: req.body?.body,
|
|
124
|
+
level: req.body?.level || 'info',
|
|
125
|
+
expiresInHours: req.body?.expiresInHours,
|
|
126
|
+
});
|
|
127
|
+
res.status(201).json({ notice });
|
|
128
|
+
} catch (err) {
|
|
129
|
+
res.status(400).json({ error: err.message });
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
router.delete('/notices/:id', (req, res) => {
|
|
134
|
+
if (req.params.id === 'all') return res.json({ retired: retireAllNotices() });
|
|
135
|
+
if (!retireNotice(req.params.id)) return res.status(404).json({ error: 'notice not found' });
|
|
136
|
+
res.status(204).end();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// ── 内容外审留证(2026-08-02)──
|
|
140
|
+
// 拦截发生在 turn.js 闸门;这里只读账。连坐封禁自动发生(lib/moderation.js),
|
|
141
|
+
// 解封走既有 PATCH /users/:id {disabled:false}。
|
|
142
|
+
|
|
143
|
+
router.get('/moderation', (req, res) => {
|
|
144
|
+
const userId = typeof req.query.userId === 'string' && req.query.userId ? req.query.userId : null;
|
|
145
|
+
const limit = Math.min(500, Number(req.query.limit) || 100);
|
|
146
|
+
res.json({ flags: listFlags({ userId, limit }) });
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
router.delete('/moderation/:id', (req, res) => {
|
|
150
|
+
if (!removeFlag(req.params.id)) return res.status(404).json({ error: 'flag not found' });
|
|
151
|
+
res.status(204).end();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// ── harness 问题库(2026-07-30)──
|
|
155
|
+
// 两个来源写同一张表:auto = PostToolUseFailure 自动记的工具失败;
|
|
156
|
+
// agent = report_friction 主动报的摩擦。默认按次数降序 —— 一眼看到最该修的。
|
|
157
|
+
|
|
158
|
+
router.get('/issues', (req, res) => {
|
|
159
|
+
const { status, source, kind } = req.query;
|
|
160
|
+
const limit = Math.min(500, Number(req.query.limit) || 200);
|
|
161
|
+
res.json({
|
|
162
|
+
issues: listIssues({
|
|
163
|
+
status: typeof status === 'string' && status !== 'all' ? status : undefined,
|
|
164
|
+
source: typeof source === 'string' && source !== 'all' ? source : undefined,
|
|
165
|
+
kind: typeof kind === 'string' && kind !== 'all' ? kind : undefined,
|
|
166
|
+
limit,
|
|
167
|
+
}),
|
|
168
|
+
stats: issueStats(),
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
router.patch('/issues/:id', (req, res) => {
|
|
173
|
+
try {
|
|
174
|
+
const issue = setIssueStatus(req.params.id, String(req.body?.status || ''));
|
|
175
|
+
if (!issue) return res.status(404).json({ error: 'issue not found' });
|
|
176
|
+
res.json({ issue });
|
|
177
|
+
} catch (err) {
|
|
178
|
+
res.status(400).json({ error: err.message });
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
router.delete('/issues/:id', (req, res) => {
|
|
183
|
+
if (!removeIssue(req.params.id)) return res.status(404).json({ error: 'issue not found' });
|
|
184
|
+
res.status(204).end();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
export default router;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* assets/docx-page.js — `GET /:pid/docx-page` 的处理器。
|
|
3
|
+
*
|
|
4
|
+
* 画布上的缩略图和产物窗里的翻页吃的是同一份缓存:一次 LibreOffice 出整份,
|
|
5
|
+
* 翻页零成本(详见 lib/docx-pages.js)。给 `w` 就缩到那个宽度出 webp,
|
|
6
|
+
* 缩略图走这条 —— 不为小一号再起一次 soffice。
|
|
7
|
+
*
|
|
8
|
+
* 单开一个文件是因为它跟 assets.js 其余路由一行代码都不共享,而 assets.js
|
|
9
|
+
* 已经压在行数棘轮的上限上。(依赖用注入的,不去 import assets.js 的内部件 ——
|
|
10
|
+
* 那会绕成一个环。)
|
|
11
|
+
*
|
|
12
|
+
* ⚠️ 冷启第一次会真跑渲染(几百毫秒到两秒),前端要能等 —— **别在这条路上加
|
|
13
|
+
* 超时重试**,重试只会把同一份文档排进闸门两次。
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { promises as fs } from 'node:fs';
|
|
18
|
+
import { pageImage, docPdf } from '../../lib/docx-pages.js';
|
|
19
|
+
import { safeResolveRead } from '../../lib/safe-path.js';
|
|
20
|
+
|
|
21
|
+
/** 缩略图宽度上限:再大就不是缩略图了,纯粹是让服务端白干 */
|
|
22
|
+
const MAX_WIDTH = 2000;
|
|
23
|
+
|
|
24
|
+
export function makeDocxPageHandler({ getSharedDir, guardProject }) {
|
|
25
|
+
return async (req, res, next) => {
|
|
26
|
+
try {
|
|
27
|
+
if (!guardProject(req, res)) return;
|
|
28
|
+
const rel = String(req.query.path || '').replace(/\\/g, '/');
|
|
29
|
+
if (!rel || !/\.docx$/i.test(rel)) return res.status(400).json({ error: 'path 得是一个 .docx' });
|
|
30
|
+
|
|
31
|
+
// ⚠️ 只做词法检查是不够的:工作区里一个指向 .env 的软链,路径逐字看都在
|
|
32
|
+
// 工作区内,soffice 会老老实实把明文渲成一张 PNG 发出去。而这条路走的是
|
|
33
|
+
// **没有沙盒的 server 进程**,绕开了给 agent 设的 permissions.deny。
|
|
34
|
+
// 判据收在 lib/safe-path.js,别在这儿再抄一遍。
|
|
35
|
+
const root = getSharedDir(req.params.pid);
|
|
36
|
+
const abs = await safeResolveRead(root, rel);
|
|
37
|
+
if (!abs) return res.status(403).json({ error: 'path escapes workspace' });
|
|
38
|
+
|
|
39
|
+
let stat;
|
|
40
|
+
try { stat = await fs.stat(abs); } catch { return res.status(404).json({ error: '找不到这份文档' }); }
|
|
41
|
+
|
|
42
|
+
// ETag 带 mtime + size:agent 一 rebuild 就换 key,浏览器自然重取
|
|
43
|
+
const w = Math.min(Math.max(0, Number(req.query.w) || 0), MAX_WIDTH);
|
|
44
|
+
const pageNo = Math.max(1, Number(req.query.page) || 1);
|
|
45
|
+
const etag = `"${stat.mtimeMs}-${stat.size}-${pageNo}-${w}"`;
|
|
46
|
+
if (req.headers['if-none-match'] === etag) return res.status(304).end();
|
|
47
|
+
|
|
48
|
+
let out;
|
|
49
|
+
try {
|
|
50
|
+
out = await pageImage(abs, pageNo, w ? { width: w } : {});
|
|
51
|
+
} catch (err) {
|
|
52
|
+
console.warn('[docx-page] render failed:', err.message);
|
|
53
|
+
return res.status(err.status || 500).json({
|
|
54
|
+
error: '渲染失败',
|
|
55
|
+
details: String(err.message || err).slice(0, 300),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
res.set('ETag', etag);
|
|
59
|
+
res.set('Cache-Control', 'private, max-age=60');
|
|
60
|
+
// 页数给前端画翻页控件,省一次单独的请求
|
|
61
|
+
res.set('X-Docx-Pages', String(out.count));
|
|
62
|
+
res.type(out.mime).send(out.buf);
|
|
63
|
+
} catch (err) { next(err); }
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* `GET /:pid/docx-pdf` —— 整份 PDF,产物窗「PDF 视图」的 iframe 吃它。
|
|
69
|
+
* 跟页图同一份渲染缓存(docx → PDF 本来就是页图链路的中间站),同一套
|
|
70
|
+
* mtime ETag:agent 一 rebuild 浏览器自然重取,视图永不陈旧。
|
|
71
|
+
*/
|
|
72
|
+
export function makeDocxPdfHandler({ getSharedDir, guardProject }) {
|
|
73
|
+
return async (req, res, next) => {
|
|
74
|
+
try {
|
|
75
|
+
if (!guardProject(req, res)) return;
|
|
76
|
+
const rel = String(req.query.path || '').replace(/\\/g, '/');
|
|
77
|
+
if (!rel || !/\.docx$/i.test(rel)) return res.status(400).json({ error: 'path 得是一个 .docx' });
|
|
78
|
+
// 软链判据同上:收在 lib/safe-path.js,别抄
|
|
79
|
+
const root = getSharedDir(req.params.pid);
|
|
80
|
+
const abs = await safeResolveRead(root, rel);
|
|
81
|
+
if (!abs) return res.status(403).json({ error: 'path escapes workspace' });
|
|
82
|
+
let stat;
|
|
83
|
+
try { stat = await fs.stat(abs); } catch { return res.status(404).json({ error: '找不到这份文档' }); }
|
|
84
|
+
const etag = `"${stat.mtimeMs}-${stat.size}-pdf"`;
|
|
85
|
+
if (req.headers['if-none-match'] === etag) return res.status(304).end();
|
|
86
|
+
let out;
|
|
87
|
+
try {
|
|
88
|
+
out = await docPdf(abs);
|
|
89
|
+
} catch (err) {
|
|
90
|
+
console.warn('[docx-pdf] render failed:', err.message);
|
|
91
|
+
return res.status(err.status || 500).json({
|
|
92
|
+
error: '渲染失败',
|
|
93
|
+
details: String(err.message || err).slice(0, 300),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
res.set('ETag', etag);
|
|
97
|
+
res.set('Cache-Control', 'private, max-age=60');
|
|
98
|
+
// inline:在浏览器的 PDF 阅读器里看,不触发下载(要文件走导出)
|
|
99
|
+
res.set('Content-Disposition', `inline; filename="${encodeURIComponent(path.basename(rel))}"`);
|
|
100
|
+
res.type('application/pdf').send(out.buf);
|
|
101
|
+
} catch (err) { next(err); }
|
|
102
|
+
};
|
|
103
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* assets/helpers.js — assets 这一族路由共用的小纯函数。
|
|
3
|
+
*
|
|
4
|
+
* 为什么单独一个文件:2026-08-17 把便签路由拆出去时,这两个函数一个被落下、
|
|
5
|
+
* 一个被误带走 —— `node --check` 不报、vite build 不报、单测也不报,只有
|
|
6
|
+
* `no-undef.lint` 抓到了。给它们一个**双方都 import 的家**,这一类事故就
|
|
7
|
+
* 不会再随下一次拆分复发(谁也不用记得"这个函数该跟着谁走")。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** 单层路径片段:不含分隔符、不含 `..`、不以点开头、长度可控 */
|
|
11
|
+
export function safeSegment(s) {
|
|
12
|
+
return typeof s === 'string' && !!s && s.length <= 200
|
|
13
|
+
&& !s.includes('/') && !s.includes('\\') && !s.includes('..') && !s.startsWith('.');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** 便签 frontmatter:只认最简单的 `---\nsession: xxx\n---` 头,其余原样当正文 */
|
|
17
|
+
export function parseNoteFrontmatter(raw) {
|
|
18
|
+
const m = /^---\n([\s\S]{0,500}?)\n---\n?/.exec(raw);
|
|
19
|
+
if (!m) return { body: raw, sessionId: null };
|
|
20
|
+
const sm = /(?:^|\n)session:\s*([A-Za-z0-9-]{8,64})\s*(?:\n|$)/.exec(m[1]);
|
|
21
|
+
return { body: raw.slice(m[0].length).replace(/^\n+/, ''), sessionId: sm ? sm[1] : null };
|
|
22
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* assets/notes.js — 便签(灵感便利贴)的增删改。
|
|
3
|
+
*
|
|
4
|
+
* 2026-08-17 从 assets.js 搬出来。这一族自成一体:只碰 `assets/notes/` 和
|
|
5
|
+
* `notes/` 两个目录,跟 assets.js 其余路由(产物清单、文件服务、文件夹操作)
|
|
6
|
+
* 不共享任何状态。搬它是因为 assets.js 压在行数棘轮上限上 ——
|
|
7
|
+
* 「想给胖文件加功能,先拆出去一块」,这是那条规矩兑现的样子。
|
|
8
|
+
*
|
|
9
|
+
* ⚠️ 依赖走**注入**不走 import:反过来 import assets.js 会绕成环。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import express from 'express';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { promises as fs } from 'node:fs';
|
|
15
|
+
import { safeSegment, parseNoteFrontmatter } from './helpers.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {object} deps
|
|
19
|
+
* @param {import('express').Router} deps.router
|
|
20
|
+
*/
|
|
21
|
+
export function mountNotesRoutes({ router, guardProject, getSharedDir, ensureProjectWorkspace, sanitizeFilename }) {
|
|
22
|
+
/**
|
|
23
|
+
* POST /:pid/notes — 新建灵感便签(第一个非文件上传类产物 kind)。
|
|
24
|
+
* body: { text, title? } → 写 shared/assets/notes/<ts>-<slug>.md。
|
|
25
|
+
* 便签就是 markdown 文件:agent 可 Read(assets/notes/ 在 cwd 软链下),
|
|
26
|
+
* 加入上下文托盘走和图片相同的 attachment 管道。
|
|
27
|
+
*/
|
|
28
|
+
router.post('/:pid/notes', express.json(), async (req, res, next) => {
|
|
29
|
+
try {
|
|
30
|
+
if (!guardProject(req, res)) return;
|
|
31
|
+
const text = String(req.body?.text || '').trim();
|
|
32
|
+
if (!text) return res.status(400).json({ error: 'text required' });
|
|
33
|
+
if (text.length > 20_000) return res.status(400).json({ error: 'note too long (max 20k chars)' });
|
|
34
|
+
const sessionId = typeof req.body?.sessionId === 'string' && /^[A-Za-z0-9-]{8,64}$/.test(req.body.sessionId)
|
|
35
|
+
? req.body.sessionId : null;
|
|
36
|
+
|
|
37
|
+
await ensureProjectWorkspace(req.params.pid);
|
|
38
|
+
const notesDir = path.join(getSharedDir(req.params.pid), 'assets', 'notes');
|
|
39
|
+
await fs.mkdir(notesDir, { recursive: true });
|
|
40
|
+
|
|
41
|
+
// slug:CJK 标题 sanitize 后是一串下划线,折叠+去边;没剩下有效字符就叫 note
|
|
42
|
+
const slug = sanitizeFilename(String(req.body?.title || text.slice(0, 24)))
|
|
43
|
+
.replace(/\.+$/, '').replace(/_+/g, '_').replace(/^_|_$/g, '') || 'note';
|
|
44
|
+
const filename = `${Date.now().toString(36)}-${slug}.md`;
|
|
45
|
+
// session 归属写进 frontmatter —— 前端按它把便签自动摆进对应工作区
|
|
46
|
+
const content = sessionId ? `---\nsession: ${sessionId}\n---\n\n${text}` : text;
|
|
47
|
+
await fs.writeFile(path.join(notesDir, filename), content, 'utf8');
|
|
48
|
+
|
|
49
|
+
res.status(201).json({
|
|
50
|
+
artifact: {
|
|
51
|
+
kind: 'note', name: filename, path: `assets/notes/${filename}`,
|
|
52
|
+
size: Buffer.byteLength(content), mtime: new Date().toISOString(),
|
|
53
|
+
ext: '.md', isImage: false, text,
|
|
54
|
+
...(sessionId ? { sessionId } : {}),
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
} catch (err) { next(err); }
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
/** DELETE /:pid/notes/:filename — 删便签(仅 notes/ 目录,单层名 + 落点校验) */
|
|
62
|
+
router.delete('/:pid/notes/:filename', async (req, res, next) => {
|
|
63
|
+
try {
|
|
64
|
+
if (!guardProject(req, res)) return;
|
|
65
|
+
const filename = req.params.filename;
|
|
66
|
+
if (!safeSegment(filename) || !filename.endsWith('.md')) {
|
|
67
|
+
return res.status(400).json({ error: 'invalid note filename' });
|
|
68
|
+
}
|
|
69
|
+
const notesDir = path.join(getSharedDir(req.params.pid), 'assets', 'notes');
|
|
70
|
+
const filePath = path.resolve(notesDir, filename);
|
|
71
|
+
if (!filePath.startsWith(notesDir + path.sep)) {
|
|
72
|
+
return res.status(400).json({ error: 'invalid note filename' });
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
await fs.unlink(filePath);
|
|
76
|
+
} catch (err) {
|
|
77
|
+
if (err.code === 'ENOENT') return res.status(404).json({ error: 'note not found' });
|
|
78
|
+
throw err;
|
|
79
|
+
}
|
|
80
|
+
res.status(204).end();
|
|
81
|
+
} catch (err) { next(err); }
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 任务便利贴路由(2026-07-30)—— tasks/<任务>/notes/*.md 的用户侧写入口。
|
|
86
|
+
* agent 侧不走这里(它直接 Write 文件);这两条给前端"共享头脑风暴"用:
|
|
87
|
+
* 用户在贴纸阅读浮层里改内容 / 删贴。
|
|
88
|
+
*
|
|
89
|
+
* 校验:任务名和文件名都可能是 CJK(决策.md),不能套 assets/notes 那条
|
|
90
|
+
* `[A-Za-z0-9._-]` 正则。改为否定式(禁路径分隔符 / .. / 隐藏文件)+
|
|
91
|
+
* resolve 后必须留在 shared/tasks 下的双保险。
|
|
92
|
+
*/
|
|
93
|
+
function safeNoteSegment(s, { md = false } = {}) {
|
|
94
|
+
if (typeof s !== 'string' || !s || s.length > 200) return false;
|
|
95
|
+
if (s.includes('/') || s.includes('\\') || s.includes('..') || s.startsWith('.')) return false;
|
|
96
|
+
if (md && !s.endsWith('.md')) return false;
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function resolveTaskNote(pid, filename) {
|
|
101
|
+
const base = path.join(getSharedDir(pid), 'notes');
|
|
102
|
+
const file = path.resolve(base, filename);
|
|
103
|
+
if (!file.startsWith(base + path.sep)) return null;
|
|
104
|
+
return file;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** PUT /:pid/task-notes/:filename — 写/改便利贴(用户侧编辑) */
|
|
108
|
+
router.put('/:pid/task-notes/:filename', express.json(), async (req, res, next) => {
|
|
109
|
+
try {
|
|
110
|
+
if (!guardProject(req, res)) return;
|
|
111
|
+
const { filename } = req.params;
|
|
112
|
+
if (!safeNoteSegment(filename, { md: true })) {
|
|
113
|
+
return res.status(400).json({ error: 'invalid filename' });
|
|
114
|
+
}
|
|
115
|
+
const text = String(req.body?.text ?? '');
|
|
116
|
+
if (!text.trim()) return res.status(400).json({ error: 'text required' });
|
|
117
|
+
if (text.length > 20_000) return res.status(400).json({ error: 'note too long (max 20k chars)' });
|
|
118
|
+
const file = resolveTaskNote(req.params.pid, filename);
|
|
119
|
+
if (!file) return res.status(400).json({ error: 'invalid path' });
|
|
120
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
121
|
+
await fs.writeFile(file, text, 'utf8');
|
|
122
|
+
res.json({ ok: true, path: `notes/${filename}` });
|
|
123
|
+
} catch (err) { next(err); }
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
/** DELETE /:pid/task-notes/:filename — 删便利贴 */
|
|
127
|
+
router.delete('/:pid/task-notes/:filename', async (req, res, next) => {
|
|
128
|
+
try {
|
|
129
|
+
if (!guardProject(req, res)) return;
|
|
130
|
+
const { filename } = req.params;
|
|
131
|
+
if (!safeNoteSegment(filename, { md: true })) {
|
|
132
|
+
return res.status(400).json({ error: 'invalid filename' });
|
|
133
|
+
}
|
|
134
|
+
const file = resolveTaskNote(req.params.pid, filename);
|
|
135
|
+
if (!file) return res.status(400).json({ error: 'invalid path' });
|
|
136
|
+
try {
|
|
137
|
+
await fs.unlink(file);
|
|
138
|
+
} catch (err) {
|
|
139
|
+
if (err.code === 'ENOENT') return res.status(404).json({ error: 'note not found' });
|
|
140
|
+
throw err;
|
|
141
|
+
}
|
|
142
|
+
res.status(204).end();
|
|
143
|
+
} catch (err) { next(err); }
|
|
144
|
+
});}
|