Compare commits

..

219 Commits

Author SHA1 Message Date
HouYunFei 2002dff78a feat(agent): add support for image generation type in event handling and messaging 2026-07-29 17:32:06 +08:00
HouYunFei 2afe2f3898 feat(agent): implement local image handling and enhance generated image integration in canvas 2026-07-29 17:19:20 +08:00
HouYunFei 7a48f441cf feat(agent): optimize agent panel header and functionality layout for improved space efficiency 2026-07-29 17:08:17 +08:00
HouYunFei 8babf7faa0 feat(agent): add local file reveal functionality in system file manager 2026-07-29 17:03:41 +08:00
HouYunFei fe1ba781d1 feat(agent): standardize approval policy and sandbox naming conventions in Codex settings 2026-07-29 16:50:51 +08:00
HouYunFei b926feeb14 feat(agent): implement permission mode handling for Codex threads and approvals 2026-07-29 16:44:51 +08:00
HouYunFei bd8cf318f7 feat(agent): enhance Codex reasoning summary with Markdown rendering and improve message history merging 2026-07-29 16:34:44 +08:00
HouYunFei a9243b6c2e feat(agent): enhance reasoning and command summaries with compact collapsible design and real-time updates 2026-07-29 16:10:05 +08:00
HouYunFei b628b742d5 fix(agent): update button styles for tool confirmation mode in chat input 2026-07-29 15:50:45 +08:00
HouYunFei d76594a7e7 feat(agent): implement tool confirmation mode in chat input with automatic confirmation by default 2026-07-29 15:44:56 +08:00
HouYunFei fa58ec4715 feat(agent): update image message display to compact thumbnails with click-to-zoom preview 2026-07-29 15:26:26 +08:00
HouYunFei 5a607bea6c fix(agent): ensure image attachments retain thumbnails after history sync and prevent internal context from displaying in user messages 2026-07-29 15:17:10 +08:00
HouYunFei 88e86c0566 fix(agent): correct image attachment positioning and prevent disappearance after task completion 2026-07-29 14:33:45 +08:00
HouYunFei d2141d5f59 feat(agent): prioritize current canvas operations when receiving tasks, avoiding unnecessary project queries 2026-07-29 14:28:28 +08:00
HouYunFei eda3481533 feat(agent): update agent connection behavior to start with a blank conversation and allow user-initiated session restoration 2026-07-29 14:16:35 +08:00
HouYunFei 0526290c8f feat(agent): restructure task progress display in conversation flow to a collapsible format above the input area, ensuring real-time updates for pending, in-progress, and completed tasks 2026-07-29 13:36:34 +08:00
HouYunFei ed32e44e69 fix(agent): handle task failure by displaying user-friendly error messages in conversation flow 2026-07-29 13:20:31 +08:00
HouYunFei 0f5eb1fbcb feat(agent): enhance structured task progress display in conversation flow, ensuring real-time updates and fixed visibility during execution 2026-07-29 13:06:25 +08:00
HouYunFei 20844d5e99 feat(codex): add structured task progress support in conversation flow, displaying status updates for pending, in-progress, and completed tasks 2026-07-29 12:57:21 +08:00
HouYunFei ea67a5abf5 feat(agent): enhance conversation flow with incremental updates and user-friendly summaries for reasoning, plans, commands, searches, and file changes 2026-07-29 12:19:13 +08:00
HouYunFei c6f8b1e394 feat(codex): optimize streaming response handling by merging deltas and improving message rendering performance 2026-07-29 11:32:03 +08:00
HouYunFei 1f6a652799 feat(codex): upgrade Codex CLI to version 0.145.0 and refactor interrupt handling for improved turn management 2026-07-29 11:14:15 +08:00
HouYunFei 40c47dd7ff feat(canvas): restructure source directory for Canvas Agent, enhancing code maintainability 2026-07-29 10:47:06 +08:00
HouYunFei 92fd0ce129 feat(logging): update logging format to plain text and enhance HTTP diagnostics 2026-07-29 10:28:24 +08:00
HouYunFei b3b02f75a9 feat(logging): implement detailed logging for Codex operations and HTTP requests 2026-07-28 17:42:11 +08:00
HouYunFei b6e9954c0a chore(release): update version to v0.11.0 and add changelog entries for new features 2026-07-28 10:29:06 +08:00
HouYunFei c23a819f27 feat(canvas): add reasoning effort settings for text nodes in canvas configuration 2026-07-28 10:28:27 +08:00
HouYunFei 6aa9c8bddc fix(canvas): retain prompt visibility for selected canvas nodes after generation 2026-07-28 10:15:53 +08:00
HouYunFei bde33e7816 fix(canvas): restrict prompt layer height for long input and enable scrolling for node prompts 2026-07-28 10:10:31 +08:00
HouYunFei edc7667830 feat(drag-and-drop): implement file upload via drag-and-drop for reference assets in image and video workspaces 2026-07-28 10:03:23 +08:00
HouYunFei f76f81c4c7 feat(canvas): add image preview functionality in the left-side element list 2026-07-27 17:37:35 +08:00
HouYunFei 87ea3a3954 feat(config): add JSON import/export support for user preferences and configurations 2026-07-27 17:27:00 +08:00
HouYunFei ac126a61b9 feat(api): add support for 火山方舟 protocol and enhance video reference validation 2026-07-27 17:02:50 +08:00
HouYunFei d6d6b7bb8d Merge branch 'fork/chen3737/fix/seedream-error-and-edit' 2026-07-27 16:36:33 +08:00
HouYunFei 84fc159d4f Merge branch 'fork/chen3737/main' 2026-07-27 16:31:22 +08:00
HouYunFei 6c61081698 feat(canvas): enhance image editing interactions and fix flickering in high-resolution previews 2026-07-27 16:30:43 +08:00
HouYunFei f8191d13b2 Merge branch 'fork/kumu-ze/codex/upstream-image-editing'
# Conflicts:
#	CHANGELOG.md
#	docs/content/docs/progress/pending-test.mdx
2026-07-27 16:11:25 +08:00
HouYunFei a0287a5e34 chore: bump version to v0.10.0 2026-07-25 11:55:13 +08:00
HouYunFei 0179724467 feat(prompts): add atlascloud logo SVG and update prompt source handling 2026-07-25 11:55:00 +08:00
yu 49851f2b5f feat(prompts): unify sources and add personal library 2026-07-21 01:08:11 +08:00
yu c81bb8b651 style(agent): distinguish chat participants 2026-07-20 12:14:36 +08:00
yu b7d0fe10bc fix(agent): improve chat navigation 2026-07-20 12:13:33 +08:00
HouYunFei d91e67a5ad feat(canvas): retain prompt input after node generation completion 2026-07-20 11:34:46 +08:00
HouYunFei d566d5cc46 Merge remote-tracking branch 'origin/main'
# Conflicts:
#	CHANGELOG.md
2026-07-20 07:55:56 +08:00
yu 8fb8d0b224 fix(agent): add uploaded images to canvas flows 2026-07-19 18:17:57 +08:00
yuki 0ca35f46ca fix(api): 增强API错误信息展示并修复Seedream图生图接口兼容性 2026-07-19 15:01:37 +08:00
yuki ccf6412a7c feat(canvas): 画布工具栏上传和拖拽支持多选文件 2026-07-19 12:54:39 +08:00
枯木泽 4236af0c58 merge: 同步上游 Agent 会话更新 2026-07-18 20:23:04 +08:00
枯木泽 8bcedfe51b fix(canvas): 防止空格平移误触弹窗按钮 2026-07-18 20:21:18 +08:00
yu 5e1fd7a825 fix(agent): synchronize shared Codex sessions 2026-07-18 15:29:58 +08:00
yu 062e4569aa feat(agent): add task status and client-scoped operations 2026-07-18 15:29:58 +08:00
HouYunFei b5dc9c8a87 feat(canvas): optimize CanvasAssetsTab and CanvasPromptsTab with memoization to improve performance 2026-07-18 08:27:20 +08:00
HouYunFei 75c22c2f9f feat(canvas): add "prompts" tab to the left panel with collapsible prompt sources 2026-07-18 08:19:59 +08:00
枯木泽 97bb330de1 feat(canvas): 优化图片编辑交互 2026-07-17 21:32:17 +08:00
HouYunFei bdca6b0a5c feat(prompt-sources): add davidwu-gpt-image2-prompts to default prompt sources 2026-07-17 17:45:50 +08:00
HouYunFei d4130bbb79 feat(prompt-sources): add custom script fetching functionality for prompt sources 2026-07-17 17:36:42 +08:00
HouYunFei 0e1edd1484 chore(release): bump version to v0.9.0 and update changelog 2026-07-17 16:43:01 +08:00
HouYunFei 5107be0acf feat(canvas): 资产卡增删改重做 + 节点名称默认隐藏
- 资产 Tab 卡片重做:视频取首帧封面、文本铺内容、去标题成矩形,hover 居中显示插入/删除两个 icon 按钮
- 修复删除确认框「取消」经 React Portal 事件冒泡误触插入
- 资产 Tab 支持上传添加图片/视频、卡片移除资产
- 画布节点名称默认不再显示,仅在选中/悬停/编辑时出现
- 操作按钮改用扁平无灰底样式,并写入 AGENTS.md 规范

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 16:42:08 +08:00
HouYunFei 219b3e84db feat(canvas): 左侧面板可调宽收起 + 元素多选导出与跳转动画
- 左侧画布面板支持拖拽调整宽度、展开/收起(带动画),状态与宽度持久化
- 顶栏菜单左侧新增面板开关按钮,并缩小菜单/开关按钮
- 顶栏菜单新增「导出当前画布」为压缩包
- 画布元素列表支持多选并批量导出:图片/视频/音频为文件、文本为 txt、其余各自为 json
- 画布/资产切换改为滑动下划线动画,点击元素跳转带缓动动画
- 移除文本/配置/视频等非图片元素图标的灰色底色,并写入 AGENTS.md 规范

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 16:21:26 +08:00
HouYunFei a3411cf9d9 docs(changelog): note the drag-resize React #185 crash fix
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 15:17:57 +08:00
HouYunFei 430d1bd956 refactor(canvas-project): extract plugin host into usePluginHost hook
Move the plugin node host capabilities (pluginAi generation bridge, pluginHost
canvas accessors, renderPluginPanel, buildNodeToolbarItems) and the plugin-load
effect out of InfiniteCanvasPage into usePluginHost. The hook receives the
shared config/refs/setters explicitly and keeps identical dependency arrays.

This also relocates the AI-service and plugin imports (requestVideoGeneration,
selectableModelsByCapability, buildNodeContext, ensurePluginsLoaded, the
canvas-plugin types, etc.) that were only used by this cluster.

project.tsx is now ~2950 lines, down from 3536 at the start of the refactor.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 14:19:00 +08:00
HouYunFei cf0561d12a refactor(canvas-project): extract agent bridge into useAgentBridge hook
Move the canvas<->local-Agent bridge (agentSnapshot memo, applyAgentOps /
undoAgentOps, agentUndoSnapshot state, and the publish-to-agent-store effect)
out of InfiniteCanvasPage into a useAgentBridge hook. Only applyAgentOps is
consumed outside (by the plugin host); the rest is fully encapsulated.

The hook receives the shared canvas state/refs/setters explicitly and keeps the
exact same dependency arrays, so behavior is unchanged. project.tsx drops the
now-unused agent-ops imports and the setCanvasContext selector.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 14:14:10 +08:00
HouYunFei e6dcd7a832 refactor(canvas-project): extract CanvasTopBar into its own component
Move CanvasTopBar and its private helpers (MenuLabel, CompactAgentStatus,
Shortcut) out of project.tsx into canvas-top-bar.tsx. The component already took
all data via explicit props, so the move is mechanical with no behavior change.
Drop the imports (DOCS_URL, UserStatusActions, Dropdown, several icons) that
were only used by the top bar.

project.tsx is now ~3080 lines, down from 3536 at the start of the refactor.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 14:04:25 +08:00
HouYunFei 187c499ef9 refactor(canvas-project): extract refresh shell and create menus into components
Move the module-scope presentational subcomponents out of project.tsx:
- canvas-refresh-shell.tsx: CanvasRefreshShell (pre-load placeholder).
- canvas-create-menus.tsx: ConnectionCreateMenu, ConnectionCreateOption,
  NodeCreateMenu, plus the PendingConnectionCreate type they define.

They already communicated via explicit props, so the move is mechanical. No
behavior change; project.tsx imports them back and drops now-unused icon and
node-registry imports.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 14:00:53 +08:00
HouYunFei cf1ea2646f refactor(canvas-project): extract pure helpers into @/lib/canvas modules
Move the ~30 module-scope pure functions out of the 3.5k-line project.tsx into
three focused lib modules, with identical names and signatures. No behavior
change; project.tsx imports them back.

- canvas-node-factory.ts: createCanvasNode, *Metadata builders, referenceUrl,
  applyNodeConfigPatch.
- canvas-node-geometry.ts: nodeBounds, group drop/snap, connection anchors,
  normalizeConnection, hidden-batch checks.
- canvas-generation-helpers.ts: generation config/inputs, reference hydration,
  retry-source lookup, angle prompt builders, misc pure helpers.

project.tsx drops from 3536 to ~3230 lines. Also removes imports that were only
used by the moved helpers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 13:46:07 +08:00
HouYunFei 57db0fe752 feat(canvas-local-agent-panel): optimize state management and rendering performance 2026-07-17 13:38:55 +08:00
HouYunFei c5f53454aa feat(changelog): add support for Google Analytics 4 and Baidu statistics 2026-07-17 11:32:11 +08:00
HouYunFei ca6efdf0c3 feat(analytics): implement runtime configuration and analytics tracking for GA4 and Baidu 2026-07-17 11:24:22 +08:00
HouYunFei a2586ffe34 feat(canvas-node-manual): enhance input box to support resource references with thumbnail display 2026-07-17 11:19:34 +08:00
HouYunFei 5a27aae99e feat(changelog): update entries for canvas node enhancements and image reference display 2026-07-17 10:15:16 +08:00
HouYunFei 66961ce10f feat(canvas-prompt-chip-input): implement new component for enhanced prompt input with reference chips 2026-07-17 10:06:57 +08:00
HouYunFei 0c4288b832 chore: update changelog for v0.8.2 and bump version to v0.8.2 2026-07-16 17:44:20 +08:00
HouYunFei 093260f7b2 feat(image-settings-panel): clarify transparent background feature availability in tooltip 2026-07-16 17:44:05 +08:00
HouYunFei 805573b980 feat(prompt-chip-input): implement contentEditable input for image references with thumbnail display 2026-07-16 17:32:43 +08:00
HouYunFei e6829333c5 feat(canvas-node): enhance input area styling and improve cursor visibility in prompt panel 2026-07-16 17:28:30 +08:00
HouYunFei 2959b5d340 feat(image-settings): add transparent background toggle for image generation 2026-07-16 15:45:02 +08:00
HouYunFei 2f9776bee3 chore: bump version to v0.8.1 2026-07-16 15:12:02 +08:00
HouYunFei 4beadc6161 feat(markdown-node): improve rendering performance and user interaction by caching parsed Markdown and optimizing DOM updates 2026-07-16 15:11:48 +08:00
HouYunFei 13c508bc34 feat(svg-node): enhance SVG node with transparent background rendering and improved editing experience 2026-07-16 14:13:43 +08:00
HouYunFei 5f627d5baf feat(sticky-note): update sticky note plugin with customizable colors, improved editing experience, and enhanced drag-and-drop functionality 2026-07-16 13:57:47 +08:00
HouYunFei 3f179a607a feat(panorama): enhance 3D panorama node with AI generation support and unified interaction toggle 2026-07-16 13:21:25 +08:00
HouYunFei 2d2c7e3d5e feat(plugin-manager): add upgrade notification for installed plugins and improve version handling 2026-07-16 10:36:41 +08:00
HouYunFei 3f623ea70c feat(html-node): enhance HTML node plugin with interactive toolbar and improved editor functionality 2026-07-15 17:51:39 +08:00
HouYunFei 7ac2ab351d chore(release): bump version to v0.8.0 and update CHANGELOG 2026-07-15 16:00:17 +08:00
HouYunFei b6e75c225e Merge branch 'dev'
# Conflicts:
#	.gitignore
#	CHANGELOG.md
#	web/bun.lock
2026-07-15 15:32:32 +08:00
HouYunFei 75aafc115f feat(plugin-sdk): add TypeScript SDK for Infinite Canvas plugins with automatic JSX and build support 2026-07-15 15:31:48 +08:00
HouYunFei 8d3f244524 feat(toolbar): add extension node management and close on outside click 2026-07-15 13:10:53 +08:00
HouYunFei 88510223eb feat(plugin): add local plugin discovery and management with cache busting 2026-07-15 12:55:59 +08:00
HouYunFei 3a4a4a6772 fix(deps): sync bun.lock with package.json for streamdown transitive deps 2026-07-15 11:59:59 +08:00
HouYunFei ee8f126da7 feat(id): replace crypto.randomUUID with nanoid for unique ID generation 2026-07-15 11:55:36 +08:00
HouYunFei eef4d96787 feat(plugin): implement canvas node plugin system with dynamic registration and remote installation 2026-07-15 11:47:25 +08:00
HouYunFei f0db29d8e3 feat(channel): add customizable JS model-call scripts with split-panel editor
- Run user-authored async scripts per model with flat locals (prompt,
  images, messages, params, model, baseUrl, apiKey, http, request, poll,
  sleep, signal, onDelta); empty script falls back to system default.
- Wire image/video/audio/text request entrypoints through the plugin
  runtime, keeping default handlers unchanged.
- Split-panel script editor: variable/return docs on the left, CodeMirror
  JS editor on the right; title shows capability - model.
- Provide OpenAI and Gemini template buttons per capability; text template
  uses the /responses API; image template branches text2img vs img2img.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 11:24:34 +08:00
HouYunFei 3963c6eebf feat(model): enhance model configuration and script handling for audio and video capabilities 2026-07-15 10:13:37 +08:00
HouYunFei c57f7d61a7 feat(model): enhance model configuration and script handling for audio and video capabilities 2026-07-15 10:13:32 +08:00
HouYunFei a4dcc679c9 feat(version): bump version to 0.7.0 2026-07-14 17:37:57 +08:00
HouYunFei b16263eb43 feat(version): bump version to 0.2.0 and update image ratio parsing functions 2026-07-14 17:37:51 +08:00
HouYunFei a5824f509e feat(agent): enhance local Agent connection handling with silent mode and update UI instructions 2026-07-14 17:08:20 +08:00
HouYunFei 7e0288d461 feat(docs): refine Infinite Canvas usage instructions and clarify local Agent connection process 2026-07-14 16:24:22 +08:00
HouYunFei 228ca29c82 feat(docs): update Infinite Canvas usage instructions and streamline local Agent connection process 2026-07-13 17:23:36 +08:00
HouYunFei 301fbce472 feat(canvas): add double-click functionality to create nodes from blank canvas area 2026-07-13 10:43:37 +08:00
HouYunFei ebd8ae2de7 feat(agent): clarify local Agent connection instructions for Codex plugin and manual MCP usage 2026-07-09 17:49:40 +08:00
HouYunFei 864ce4101a feat(agent): implement streamdown for real-time message rendering in chat 2026-07-09 17:16:35 +08:00
HouYunFei 25e1d173e8 feat(docs): add local development and canvas data structure documentation 2026-07-09 17:14:46 +08:00
HouYunFei 52a5e9e33a feat(agent): update documentation for new site tools and changelog guidelines 2026-07-09 16:54:48 +08:00
HouYunFei 2adbe9e43a feat(agent): add site tools support for canvas project listing and media generation 2026-07-09 16:53:09 +08:00
HouYunFei 7b347729ec feat(agent): implement global Agent panel with site navigation and interrupt functionality 2026-07-09 16:19:32 +08:00
HouYunFei 387d8be961 feat(canvas): enhance group node functionality with improved click and drag behavior 2026-07-09 13:31:13 +08:00
HouYunFei ec0e8c4e24 feat(canvas): add group node support with drag-and-drop functionality 2026-07-09 12:54:36 +08:00
HouYunFei abe3be58a6 feat(canvas): add group node support with drag-and-drop functionality 2026-07-09 11:48:23 +08:00
HouYunFei 050b36fad1 chore(changelog): add release notes for v0.6.0 2026-07-09 10:42:13 +08:00
HouYunFei 668c3adb52 chore(changelog): add release notes for v0.6.0 2026-07-09 10:21:07 +08:00
HouYunFei f2a36e86b2 fix(video): enhance video generation error handling and URL retrieval 2026-07-08 22:23:18 +08:00
HouYunFei d747527e2a Merge branch 'fork/beetle0915/fix/ime-enter-composition' into dev 2026-07-08 16:30:38 +08:00
HouYunFei 798d06b662 Merge branch 'fork/1797127235/fix/canvas-mode-model-switch' into dev
# Conflicts:
#	CHANGELOG.md
#	docs/content/docs/progress/pending-test.mdx
#	web/src/components/canvas/canvas-config-node-panel.tsx
#	web/src/components/canvas/canvas-node-prompt-panel.tsx
2026-07-08 16:23:12 +08:00
HouYunFei 257dab5bac fix: resolve issue with Gemini image format not passing aspect ratio configuration 2026-07-08 16:18:12 +08:00
HouYunFei 81144ea009 fix: resolve issue with Gemini image format not passing aspect ratio configuration 2026-07-08 15:11:18 +08:00
beetle0915 bac571ef4e Fix Enter handling during IME composition 2026-07-07 08:24:34 +08:00
1797127235 965d416aeb fix(canvas): switch to correct default model when changing generation mode 2026-07-06 10:33:52 +08:00
HouYunFei 3080d6da10 feat: enhance Codex agent connection handling with automatic thread recovery and improved error logging 2026-07-05 23:40:54 +08:00
HouYunFei 6113986637 refactor: remove agent mode switch and streamline local agent usage in the canvas assistant panel 2026-07-05 17:04:02 +08:00
HouYunFei ca5faa1f4e feat: add GitHub Pages workflow for automatic deployment on version tag push 2026-07-05 16:51:27 +08:00
HouYunFei 0b69dfce46 feat: add support for draggable grid lines in image slicing, with options to add, delete, and reset horizontal/vertical lines 2026-07-05 16:08:23 +08:00
HouYunFei 65a91b22e1 feat: add Codex App plugin support with dedicated configuration page and connection settings 2026-07-05 15:12:42 +08:00
HouYunFei 9dd0dc28b2 refactor: remove credit cost calculations and related UI elements from canvas node panels 2026-07-05 13:44:42 +08:00
HouYunFei 522153b3e6 fix: resolve issue with image retry results not persisting after page refresh in the image workbench 2026-07-05 12:46:51 +08:00
HouYunFei 568f0f1838 feat: remove deprecated backend .env.example file and update deployment configurations 2026-07-05 11:41:20 +08:00
HouYunFei cebc3dc5c2 feat: update project structure for Vite and React Router, remove deprecated sources, and enhance Docker deployment 2026-07-05 11:38:42 +08:00
HouYunFei 443afab7bd refactor: remove "use client" directive from multiple files and update project structure to align with Vite and React Router 2026-06-26 17:24:48 +08:00
HouYunFei e635ec6908 feat: migrate from Next.js to Vite and React Router, updating routing and environment variable handling 2026-06-26 16:41:32 +08:00
HouYunFei ef7a218ba6 feat(docs): update documentation to reflect direct browser connections for third-party prompts and WebDAV 2026-06-26 15:56:55 +08:00
HouYunFei bd0ad0aebf feat(plugin): add Codex App plugin support and local agent integration 2026-06-23 17:35:43 +08:00
HouYunFei 8cbe00e348 feat(api): add support for Gemini API format and enhance channel configuration 2026-06-17 10:24:55 +08:00
soldier 6300f5fb26 Merge branch 'main' of github.com:basketikun/infinite-canvas 2026-06-16 18:46:34 +08:00
soldier 73090da95a feat: 生成增加停止功能 2026-06-16 18:44:20 +08:00
HouYunFei 973d14f4c3 feat(changelog): update to version 0.4.0 with new web-based Agent Loop mode and Vercel one-click deployment support 2026-06-16 17:44:13 +08:00
HouYunFei f038406070 feat(agent): enhance model resolution logic by normalizing model options and improving generation model selection 2026-06-16 17:25:45 +08:00
HouYunFei 1b289f6a77 feat(agent): align online Agent tools with local Agent capabilities and enhance node creation and generation configuration 2026-06-16 16:52:52 +08:00
HouYunFei 27bd6f004b feat(agent): implement streaming responses for online Agent and enhance tool execution feedback 2026-06-16 16:16:04 +08:00
HouYunFei 7893bd72dd feat(agent): refactor tool response handling and enhance online agent functionality 2026-06-16 15:54:20 +08:00
HouYunFei f7b030e209 feat(agent): enhance online Agent functionality with improved read-only responses and log view 2026-06-16 14:02:53 +08:00
HouYunFei bce9c2d897 feat(agent): improve Agent panel interactions and enhance text model selection UI 2026-06-16 13:44:33 +08:00
HouYunFei b4515ed85f feat(agent): enhance Codex thread management with workspace verification and improved thread loading 2026-06-16 13:22:15 +08:00
HouYunFei 7f0199da59 feat: transition to a fully frontend architecture by removing backend dependencies and updating configuration for local deployment 2026-06-16 12:56:49 +08:00
HouYunFei 8a66524ea7 feat: update admin navigation and configuration for local deployment, enhance user experience with direct API connections 2026-06-16 12:07:35 +08:00
soldier 0d90465339 feat: enhance MentionMenu with active item scrolling functionality 2026-06-16 11:00:32 +08:00
HouYunFei d3923e21e0 feat(agent): enhance Canvas Agent with new operations and improved message handling 2026-06-15 23:24:56 +08:00
HouYunFei b1164f9c12 chore: update changelog for v0.3.0 release and bump version to v0.3.0 2026-06-15 17:48:48 +08:00
HouYunFei b6d358d6f8 feat: rename package to @basketikun/canvas-agent and update related configurations 2026-06-15 17:16:27 +08:00
HouYunFei e422285c59 feat(agent): implement local Canvas Agent with MCP integration and HTTP server 2026-06-14 18:27:44 +08:00
HouYunFei 8aa3fad684 chore: update Docker image names to reflect new repository ownership 2026-06-08 15:30:08 +08:00
HouYunFei 22f256d1be chore: update changelog for v0.2.5 release and bump version to v0.2.5 2026-06-08 15:27:33 +08:00
HouYunFei 8d0b5ecdb8 feat(video): implement video generation task creation and polling mechanism 2026-06-08 15:26:35 +08:00
HouYunFei 373229a04a fix(canvas): resolve text node editing issues and improve textarea rendering 2026-06-08 15:11:16 +08:00
HouYunFei 39a1e859b1 feat(webdav): add WebDAV synchronization configuration and functionality 2026-06-08 14:59:30 +08:00
kunkun dd9347e85d Merge pull request #47 from YuSaZh/main
ci: 增加 ARM 架构 Docker 镜像自动构建
2026-06-08 11:30:16 +08:00
YuSaZh 13418a4d26 ci: build docker images on native runners 2026-06-07 22:25:30 +09:00
YuSaZh 506378c308 ci: publish multi-arch docker images 2026-06-07 21:46:01 +09:00
HouYunFei 81b9a207b9 feat(toolbar): add image splitting feature to toolbar for generating sub-images 2026-06-07 12:25:52 +08:00
kunkun 3667557dc2 Merge pull request #37 from 1847123733/main
添加图像节点双击放大预览功能
2026-06-04 17:41:54 +08:00
HouYunFei b05d759f20 chore: bump version from v0.2.3 to v0.2.4 2026-06-04 17:25:39 +08:00
HouYunFei 6d3b89c842 feat(toolbar): add reverse prompt feature to image node toolbar for generating prompt text 2026-06-04 17:25:30 +08:00
1847123733 eb050fc08a Merge branch 'basketikun:main' into main 2026-06-04 17:15:17 +08:00
王鑫凯 72024c8138 feat(canvas): 添加图像节点预览功能
- 在CanvasNode组件中添加onViewImage回调属性
- 实现图像节点点击时触发预览功能
- 修复事件冒泡问题,确保图像预览操作不会触发其他交互
2026-06-04 17:13:28 +08:00
HouYunFei 51df099936 chore: bump version from v0.2.2 to v0.2.3 2026-06-04 17:03:54 +08:00
HouYunFei 96cca4d97c feat(canvas): enhance configuration node with new input handling and reference selection features 2026-06-04 17:03:48 +08:00
kunkun 75d5af5d8c Bump version from v0.2.1 to v0.2.2 2026-06-04 16:30:40 +08:00
HouYunFei 8a51cc609f feat(toolbar): add mask editing feature to image node toolbar for localized modifications 2026-06-04 16:12:38 +08:00
HouYunFei 0adf547db9 feat(toolbar): update image toolbar tools visibility settings for better user experience 2026-06-04 15:19:05 +08:00
HouYunFei 163dc51203 feat(database): add automatic database creation for MySQL and PostgreSQL 2026-06-04 14:22:20 +08:00
HouYunFei 68aca6fa16 feat(toolbar): enhance image node hover toolbar with new tools and settings modal 2026-06-03 17:52:01 +08:00
HouYunFei ceb7605a1e feat(canvas): add image upscaling and super resolution features with UI enhancements 2026-06-03 16:05:35 +08:00
stupid-h4er 76a4e7c4a2 fix: prefer config resources for canvas mentions 2026-06-03 12:49:29 +08:00
stupid-h4er c70e7af14d feat: add canvas resource mention references 2026-06-03 11:26:59 +08:00
HouYunFei 72ed6a043c feat(changelog): update to version 0.2.1 and add new entries for documentation and UI improvements 2026-06-03 10:30:31 +08:00
HouYunFei 8f19a18c35 feat(audio): add audio generation capabilities with configurable settings and UI enhancements 2026-06-02 17:50:36 +08:00
HouYunFei 38ec654748 feat(config): enhance model configuration with support for audio models and improve UI for user preferences 2026-06-02 17:24:27 +08:00
HouYunFei 39793e890d feat(canvas): add canvas default image count configuration and update model picker capabilities 2026-06-02 17:07:31 +08:00
HouYunFei 7bfa5a2bd9 feat(env): add NEXT_PUBLIC_DOC_URL for documentation access 2026-06-02 16:48:44 +08:00
HouYunFei 98a229af5c feat(docs): add documentation link in top menu and user status actions 2026-06-02 16:22:40 +08:00
HouYunFei d12cceeecf feat(image): enhance image generation settings with dimension snapping and thumbnail filtering 2026-06-02 16:00:20 +08:00
HouYunFei 50176d8c7a feat(image): enhance action buttons with tooltips and improve layout 2026-06-02 15:06:50 +08:00
HouYunFei e926223385 feat(image): add output format for image processing and update routes type reference 2026-06-02 14:56:23 +08:00
HouYunFei 965270b6e3 feat(docker): update Dockerfile for standalone Next.js app and adjust build paths 2026-06-02 14:37:13 +08:00
HouYunFei 6413011e54 feat(docs): enhance documentation structure and add Docker support for standalone Next.js app 2026-06-02 13:23:11 +08:00
HouYunFei 7802771e56 Merge branch 'fork/stupid-h4er/feat/ark-agentplan-seedance-video'
# Conflicts:
#	docs/content/docs/progress/pending-test.mdx
2026-06-02 09:41:28 +08:00
HouYunFei b16251c1ed feat(docs): reorganize documentation structure and update navigation links 2026-06-01 17:50:53 +08:00
stupid-h4er c7e3e0af6a feat: improve canvas connection interactions 2026-06-01 15:26:24 +08:00
kunkun 5f59b4bed3 Merge pull request #20 from aaronjmars/security/open-redirect-guard
fix(security): block open redirect in login redirect target
2026-06-01 11:17:39 +08:00
HouYunFei e9fe5b0651 feat(video): 新增火山方舟 Agent Plan 接入 Seedance 2.0 视频生成功能
- 支持通过火山方舟 Agent Plan 接入 Seedance 2.0 视频生成,后端自动适配任务创建、轮询和结果回填接口
- 视频创作台和画布视频生成支持 Seedance 分辨率、比例、时长、生成声音、水印及参考图片、参考视频、参考音频输入
- 画布新增音频节点,并新增参考素材上传与公开访问接口,配合 PUBLIC_BASE_URL 供火山方舟拉取本地参考素材
- 管理后台模型渠道支持 Agent Plan Base URL 归一、Seedance 模型配置提示、渠道探测超时和已启用模型自动同步到公开可用模型
- 图片/视频参考素材按 图片1、视频1、音频1 编号注入提示词,图像请求尺寸和质量参数在请求前归一校验
- 修复 Docker 部署时 SQLite 相对路径可能写入错误目录的问题,并优化 AI 上游鉴权、限流和 Seedance 敏感参考视频错误提示
2026-06-01 11:03:46 +08:00
HouYunFei 3aee6d1eee Merge branch 'fork/stupid-h4er/feat/ark-agentplan-seedance-video'
# Conflicts:
#	docs/pending-test.md
2026-06-01 10:53:16 +08:00
stupid-h4er c3b1ba2943 feat: label image references in generation 2026-05-31 00:56:40 +08:00
HouYunFei c63b843ff1 chore(version): 更新版本号从 v0.1.0 到 v0.1.1
- 将 VERSION 文件中的版本号从 v0.1.0 更新为 v0.1.1
2026-05-30 13:13:13 +08:00
HouYunFei 50a3a94876 chore(version): 更新版本号从 v0.1.0 到 v0.1.1
- 将 VERSION 文件中的版本号从 v0.1.0 更新为 v0.1.1
2026-05-30 13:09:01 +08:00
HouYunFei 6867f3e1e6 feat(config): 支持通过URL参数自动配置API设置
- 新增通过URL查询参数baseUrl/baseurl和apiKey/apikey自动填充配置的功能
- 实现读取参数后从地址栏自动移除相关参数的安全机制
- 添加后台是否允许自定义渠道的检查逻辑
- 集成Ant Design的消息提示和对话框组件用于用户反馈
- 更新文档说明New API自动配置的使用方法和参数格式
- 优化配置初始化流程以处理外部软件跳转场景
2026-05-30 13:08:43 +08:00
haer 78738e060f Merge branch 'basketikun:main' into feat/ark-agentplan-seedance-video 2026-05-29 23:48:13 +08:00
stupid-h4er 923e21c581 fix: normalize gpt image request size 2026-05-28 14:28:53 +08:00
HouYunFei e740da6702 chore(Dockerfile): 将基础镜像从 Bun 切换到 Node.js 并更新启动命令 2026-05-28 10:36:27 +08:00
aeonframework 5ca5b72b01 fix(security): block open redirect in login redirect target
The login redirect accepted any value beginning with a single slash, so a
protocol-relative URL such as "//evil.com" (or a backslash variant) slipped
through and the browser resolved it to an external site. Both the Go OAuth
state decoder and the web login page used the same prefix-only check, so an
attacker could send a victim to /login?redirect=//evil.com — or supply it via
the Linux.do OAuth redirect param — and bounce them off-site after login.

Harden both layers: strip Tab/CR/LF (which browsers ignore inside URLs) and
reject protocol-relative and backslash-prefixed targets, allowing only genuine
same-site relative paths.

Detected by Aeon + semgrep (go.lang.security.injection.open-redirect).
Severity: medium
CWE-601 (URL Redirection to Untrusted Site)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 20:06:22 +00:00
stupid-h4er 76461e6dba fix: clarify seedance reference video failures 2026-05-27 22:34:46 +08:00
stupid-h4er 6d185a6d93 fix: persist docker sqlite settings path 2026-05-27 22:11:36 +08:00
stupid-h4er 74ea4b81f2 fix: improve seedance reference handling 2026-05-27 21:55:05 +08:00
stupid-h4er 4bffb55695 feat: add seedance media params and model visibility 2026-05-27 13:02:36 +08:00
stupid-h4er 871bedc072 fix: add timeout for admin channel probes 2026-05-27 11:00:20 +08:00
stupid-h4er 3be0df0df6 fix: clarify ark agent plan model discovery 2026-05-27 10:52:59 +08:00
stupid-h4er fa9a3c1920 fix: normalize ark agent plan base url 2026-05-27 10:08:01 +08:00
stupid-h4er 1b557a86d7 fix: skip text test for ark seedance channels 2026-05-27 10:01:15 +08:00
stupid-h4er e37597f432 docs: add ark agentplan setup notes 2026-05-26 23:10:57 +08:00
stupid-h4er 1b2d473d9c feat: add video node preview 2026-05-26 23:10:49 +08:00
stupid-h4er c9fea71821 feat: add ark seedance video generation 2026-05-26 23:10:41 +08:00
stupid-h4er 1493aed81d chore: inspect ai generation flow 2026-05-26 22:19:38 +08:00
HouYunFei fcde508482 chore(version): 更新版本号并完善变更日志
- 更新版本号从 v0.0.9 到 v0.1.0
- 在变更日志中添加 v0.1.0 版本记录
- 记录优化我的画布、我的素材导出功能
- 记录修复画布撤销、配置节点等 bug 问题
2026-05-26 16:05:40 +08:00
HouYunFei 7e5179c54f fix(canvas): 解决画布节点撤销操作后图片丢失问题
- 添加 cleanupCanvasFiles 回调函数用于清理画布资源
- 将 cleanupAssetImages 替换为 cleanupCanvasFiles 以统一资源清理逻辑
- 在节点变更和画布清空时使用新的清理函数
- 修复撤销操作后节点信息恢复但本地图片数据被清理的错误
2026-05-26 15:50:36 +08:00
HouYunFei fda8efb362 fix(canvas): 修复画布视频设置浮层遮挡和交互问题
- 将视频设置浮层改为挂载到页面根层级并使用自建浮层交互
- 避免被节点悬浮工具栏遮挡或点击面板内容时意外关闭
- 在画布配置节点组件中为视频设置浮层添加 placement 属性
- 更新文档描述以反映浮层交互方式的改进
2026-05-26 15:28:35 +08:00
HouYunFei 7ac1fea662 refactor(canvas): 重构画布图像设置弹窗组件
- 使用 Portal 重新实现弹窗渲染机制
- 移除 Ant Design Popover 组件依赖
- 添加手动定位逻辑替代自动溢出调整
- 优化外部点击关闭事件处理机制
- 修复弹窗位置计算和滚动同步问题
- 简化组件参数并改进 DOM 结构
2026-05-26 15:17:29 +08:00
HouYunFei 005c396660 feat(image): 优化图片生成记录持久化和导出功能
- 新增生成记录配置持久化功能,保存提示词、参考图、模型、质量、尺寸和张数配置
- 结果图写入本地图片存储后只保存 storageKey,减少存储空间占用
- 点击历史记录可回填生成配置并预览结果
- 优化我的画布、我的素材导出功能
- 新增视频创作台页面
- 修复图片节点size参数传递问题
- 添加完整的生成日志数据结构和序列化处理
- 实现图片存储和删除的批量操作支持
2026-05-26 14:44:06 +08:00
HouYunFei bf66a25936 feat(image): 优化图片生成记录持久化和导出功能
- 新增生成记录配置持久化功能,保存提示词、参考图、模型、质量、尺寸和张数配置
- 结果图写入本地图片存储后只保存 storageKey,减少存储空间占用
- 点击历史记录可回填生成配置并预览结果
- 优化我的画布、我的素材导出功能
- 新增视频创作台页面
- 修复图片节点size参数传递问题
- 添加完整的生成日志数据结构和序列化处理
- 实现图片存储和删除的批量操作支持
2026-05-26 14:09:10 +08:00
HouYunFei de25ef515e feat(assets): 使用 file-saver 库优化文件下载功能
- 引入 file-saver 库替代手动创建下载链接的方式
- 在 asset-transfer.ts 中使用 saveAs 替代 URL.createObjectURL 和手动点击链接
- 在 canvas-export.ts 中使用 saveAs 替代原有的文件下载逻辑
- 更新 package.json 添加 file-saver 依赖
- 更新 bun.lock 文件同步依赖变更
2026-05-26 13:21:52 +08:00
HouYunFei 9311b8a92b feat(asset): 添加素材导入导出功能
- 实现了素材批量导出为包含 assets.json 和媒体文件的压缩包
- 实现了从压缩包导入素材到本地存储的功能
- 在素材页面添加了导出素材和导入素材的按钮入口
- 创建了 asset-transfer 模块处理素材打包和解包逻辑
- 支持图片和视频文件的存储路径映射和恢复
- 添加了文件安全命名和 MIME 类型识别功能
2026-05-26 13:13:05 +08:00
HouYunFei 1fb782819b feat(canvas): 实现画布项目导出为ZIP压缩包功能
- 新增 canvas-export 工具模块,支持将画布项目打包为 ZIP 文件
- 修改导出功能,将原来的 JSON 格式改为 ZIP 压缩包,包含 projects.json 和相关媒体文件
- 添加对 fflate 库的依赖用于 ZIP 文件处理
- 更新画布库页面,支持多选项目批量导出
- 修改导入功能以支持读取新版 ZIP 格式的画布项目
- 新增 zip 处理工具库,封装压缩和解压操作
- 扩展文件存储服务,添加获取和设置 Blob 对象的方法
- 更新 AGENTS.md 文档,添加标准库使用原则
2026-05-26 13:00:16 +08:00
410 changed files with 51111 additions and 12293 deletions
+20
View File
@@ -0,0 +1,20 @@
{
"name": "infinite-canvas-local",
"interface": {
"displayName": "Infinite Canvas"
},
"plugins": [
{
"name": "infinite-canvas",
"source": {
"source": "local",
"path": "./plugins/infinite-canvas"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "Productivity"
}
]
}
+5 -1
View File
@@ -1,7 +1,11 @@
.git .git
.idea .idea
docs docs2
data data
docs/.next
docs/.source
docs/node_modules
docs/out
web/.next web/.next
web/node_modules web/node_modules
web/out web/out
-20
View File
@@ -1,20 +0,0 @@
# 管理员账号,首次启动会自动创建。
ADMIN_USERNAME=admin
ADMIN_PASSWORD=infinite-canvas
# JWT 登录密钥和过期时间,正式部署请修改 JWT_SECRET。
JWT_SECRET=infinite-canvas
JWT_EXPIRE_HOURS=168
# 后端监听端口
PORT=8080
# 前端开发代理默认使用 http://127.0.0.1:8080,如后端开发端口不同,启动前端时可单独设置 API_BASE_URL。
# API_BASE_URL=http://127.0.0.1:8080
# 数据库配置,默认使用本地 SQLite。
STORAGE_DRIVER=sqlite
# sqlite: DATABASE_DSN=data/infinite-canvas.db
# mysql: DATABASE_DSN=user:password@tcp(127.0.0.1:3306)/infinite_canvas?parseTime=true
# postgres: DATABASE_DSN=postgres://user:password@127.0.0.1:5432/infinite_canvas?sslmode=disable
DATABASE_DSN=data/infinite-canvas.db
+89 -15
View File
@@ -9,9 +9,37 @@ permissions:
contents: read contents: read
packages: write packages: write
env:
IMAGE_NAME: ghcr.io/basketikun/infinite-canvas
jobs: jobs:
build: meta:
runs-on: ubuntu-latest runs-on: ubuntu-latest
outputs:
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
steps:
- id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.IMAGE_NAME }}
tags: |
type=ref,event=tag
type=sha,prefix=
build:
needs: meta
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- platform: linux/amd64
arch: amd64
runner: ubuntu-latest
- platform: linux/arm64
arch: arm64
runner: ubuntu-24.04-arm
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -23,19 +51,65 @@ jobs:
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- id: meta - id: build
uses: docker/metadata-action@v5 uses: docker/build-push-action@v6
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=ref,event=tag
type=sha,prefix=
- uses: docker/build-push-action@v6
with: with:
context: . context: .
push: true platforms: ${{ matrix.platform }}
tags: ${{ steps.meta.outputs.tags }} labels: ${{ needs.meta.outputs.labels }}
labels: ${{ steps.meta.outputs.labels }} outputs: type=image,name=${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
cache-from: type=gha cache-from: type=gha,scope=app-${{ matrix.arch }}
cache-to: type=gha,mode=max cache-to: type=gha,mode=max,scope=app-${{ matrix.arch }}
- name: Export digest
shell: bash
run: |
mkdir -p "$RUNNER_TEMP/digests"
digest="${{ steps.build.outputs.digest }}"
touch "$RUNNER_TEMP/digests/${digest#sha256:}"
- uses: actions/upload-artifact@v4
with:
name: digests-${{ matrix.arch }}
path: ${{ runner.temp }}/digests/*
if-no-files-found: error
retention-days: 1
merge:
needs:
- meta
- build
runs-on: ubuntu-latest
steps:
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/download-artifact@v4
with:
path: ${{ runner.temp }}/digests
pattern: digests-*
merge-multiple: true
- name: Create multi-arch manifest
shell: bash
env:
TAGS: ${{ needs.meta.outputs.tags }}
run: |
tag_args=()
while IFS= read -r tag; do
if [ -n "$tag" ]; then
tag_args+=("-t" "$tag")
fi
done <<< "$TAGS"
digest_args=()
while IFS= read -r digest_file; do
digest_args+=("${IMAGE_NAME}@sha256:${digest_file}")
done < <(find "$RUNNER_TEMP/digests" -type f -printf '%f\n' | sort)
docker buildx imagetools create "${tag_args[@]}" "${digest_args[@]}"
+117
View File
@@ -0,0 +1,117 @@
name: Docs Docker image
on:
push:
tags: ["v*"]
workflow_dispatch:
permissions:
contents: read
packages: write
env:
IMAGE_NAME: ghcr.io/basketikun/infinite-canvas-docs
jobs:
meta:
runs-on: ubuntu-latest
outputs:
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
steps:
- id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.IMAGE_NAME }}
tags: |
type=raw,value=latest
type=ref,event=tag
type=sha,prefix=
build:
needs: meta
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- platform: linux/amd64
arch: amd64
runner: ubuntu-latest
- platform: linux/arm64
arch: arm64
runner: ubuntu-24.04-arm
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: build
uses: docker/build-push-action@v6
with:
context: .
file: ./docs/Dockerfile
platforms: ${{ matrix.platform }}
labels: ${{ needs.meta.outputs.labels }}
outputs: type=image,name=${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
cache-from: type=gha,scope=docs-${{ matrix.arch }}
cache-to: type=gha,mode=max,scope=docs-${{ matrix.arch }}
- name: Export digest
shell: bash
run: |
mkdir -p "$RUNNER_TEMP/digests"
digest="${{ steps.build.outputs.digest }}"
touch "$RUNNER_TEMP/digests/${digest#sha256:}"
- uses: actions/upload-artifact@v4
with:
name: docs-digests-${{ matrix.arch }}
path: ${{ runner.temp }}/digests/*
if-no-files-found: error
retention-days: 1
merge:
needs:
- meta
- build
runs-on: ubuntu-latest
steps:
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/download-artifact@v4
with:
path: ${{ runner.temp }}/digests
pattern: docs-digests-*
merge-multiple: true
- name: Create multi-arch manifest
shell: bash
env:
TAGS: ${{ needs.meta.outputs.tags }}
run: |
tag_args=()
while IFS= read -r tag; do
if [ -n "$tag" ]; then
tag_args+=("-t" "$tag")
fi
done <<< "$TAGS"
digest_args=()
while IFS= read -r digest_file; do
digest_args+=("${IMAGE_NAME}@sha256:${digest_file}")
done < <(find "$RUNNER_TEMP/digests" -type f -printf '%f\n' | sort)
docker buildx imagetools create "${tag_args[@]}" "${digest_args[@]}"
+52
View File
@@ -0,0 +1,52 @@
name: GitHub Pages
on:
push:
tags: ["v*"]
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: pages
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- name: Install dependencies
working-directory: web
run: bun install
- name: Build
working-directory: web
env:
VITE_BASE: /${{ github.event.repository.name }}/
run: bun run build
- name: Add SPA fallback
run: cp web/dist/index.html web/dist/404.html
- uses: actions/configure-pages@v5
- uses: actions/upload-pages-artifact@v3
with:
path: web/dist
deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- id: deployment
uses: actions/deploy-pages@v4
@@ -0,0 +1,59 @@
name: Publish Canvas Agent
on:
push:
branches:
- main
paths:
- "canvas-agent/**"
- ".github/workflows/publish-canvas-agent.yml"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: publish-canvas-agent
cancel-in-progress: false
jobs:
publish:
runs-on: ubuntu-latest
defaults:
run:
working-directory: canvas-agent
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
registry-url: https://registry.npmjs.org
- name: Install dependencies
run: npm install --no-audit --no-fund
- name: Check package version
id: package
run: |
name=$(node -p "require('./package.json').name")
version=$(node -p "require('./package.json').version")
published=$(npm view "$name@$version" version 2>/dev/null || true)
echo "name=$name" >> "$GITHUB_OUTPUT"
echo "version=$version" >> "$GITHUB_OUTPUT"
if [ -n "$published" ]; then
echo "should_publish=false" >> "$GITHUB_OUTPUT"
else
echo "should_publish=true" >> "$GITHUB_OUTPUT"
fi
- name: Publish to npm
if: steps.package.outputs.should_publish == 'true'
run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Skip existing version
if: steps.package.outputs.should_publish != 'true'
run: echo "${{ steps.package.outputs.name }}@${{ steps.package.outputs.version }} already exists on npm, skipping publish."
+47
View File
@@ -0,0 +1,47 @@
name: Publish Official Plugins
# 构建官方插件并把产物(bundle + 清单)强推到孤儿分支 plugins-dist。
# 与 github-pages.yml 一致,在打版本 tag 时触发 —— 发版时 web 与官方插件一起发布。
# 前端经 jsDelivr 从该分支远程拉取;构建产物不进 main、不进 git。
on:
push:
tags: ["v*"]
workflow_dispatch:
permissions:
contents: write
concurrency:
group: publish-plugins
cancel-in-progress: false
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Install build dependencies
working-directory: plugins/canvas/registry
run: npm install --no-audit --no-fund
- name: Build official plugins
working-directory: plugins/canvas/registry
run: npm run build
- name: Publish to plugins-dist branch
working-directory: plugins/canvas/registry/dist
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
git init -q
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git checkout -q -b plugins-dist
git add -A
git commit -q -m "publish official plugins @ ${GITHUB_SHA::7}"
git push -f "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" plugins-dist
+7
View File
@@ -8,3 +8,10 @@ data/
*.tsbuildinfo *.tsbuildinfo
.DS_Store .DS_Store
.idea .idea
web/dist
**/dist
web/public/plugins
plugins/canvas/*/dist
plugins/canvas/*/node_modules
web/public/plugins
+17 -24
View File
@@ -6,8 +6,9 @@
- 先读现有代码,再动手修改,优先沿用项目已有结构和写法。 - 先读现有代码,再动手修改,优先沿用项目已有结构和写法。
- 写代码保持最少行数,能简单实现就不要引入复杂抽象。 - 写代码保持最少行数,能简单实现就不要引入复杂抽象。
- 标准格式、协议、解析、压缩、加密、日期等通用能力优先使用成熟稳定的库,不要手写底层实现,除非用户明确要求或项目已有实现必须沿用。
- 不要为了“兼容更多场景”写大量分支,只实现当前明确需要的功能。 - 不要为了“兼容更多场景”写大量分支,只实现当前明确需要的功能。
- 项目尚未上线,不需要兼容旧数据;表结构或字段调整时直接按新设计修改,不写旧字段兼容数据迁移兜底或删除旧表的清理逻辑,除非用户明确要求。 - 项目尚未上线,不需要兼容旧数据;本地存储结构调整时直接按新设计修改,不写旧字段兼容数据迁移兜底,除非用户明确要求。
- 每次写完代码,不需要检查语法,不需要执行构建,用户会自己做。 - 每次写完代码,不需要检查语法,不需要执行构建,用户会自己做。
- 不要改无关文件,不要顺手重构。 - 不要改无关文件,不要顺手重构。
- 如果工作区已有用户改动,不要回滚,不要覆盖;只在必要范围内追加修改。 - 如果工作区已有用户改动,不要回滚,不要覆盖;只在必要范围内追加修改。
@@ -18,28 +19,18 @@
- 补充时写成明确、可执行的规则,避免只写模糊描述。 - 补充时写成明确、可执行的规则,避免只写模糊描述。
- 新规则应放到最相关的章节;找不到合适章节时放到“项目注意事项”。 - 新规则应放到最相关的章节;找不到合适章节时放到“项目注意事项”。
## 后端规范
- 后端使用 Go + Gin + GORM。
- `handler/` 只处理 HTTP 入参、调用 service、返回 `OK` / `Fail`
- `service/` 放业务逻辑、默认值、校验、时间、ID、鉴权等处理。
- `repository/` 只做数据库访问和 GORM 查询。
- `model/` 只定义数据结构、枚举和简单模型方法。
- 列表接口优先沿用 `model.Query``Normalize`、分页和标签筛选方式。
- 业务接口保持 `{ code, data, msg }` 的响应结构。
- 新增数据表时同步更新 `docs/backend-database.md`
## 前端规范 ## 前端规范
- 前端使用 Next.js App Router、React、TypeScript、Ant Design、Tailwind、Zustand。 - 前端使用 Vite、React、React Router、TypeScript、Ant Design、Tailwind、Zustand。
- 编写 Ant Design 相关代码时,参考 https://ant.design/llms-full.txt 理解组件 API、示例和设计规范,并优先结合项目当前 antd 版本与既有写法。 - 编写 Ant Design 相关代码时,参考 https://ant.design/llms-full.txt 理解组件 API、示例和设计规范,并优先结合项目当前 antd 版本与既有写法。
- API 请求统一放在 `web/src/services/api/` - 外部服务请求统一放在 `web/src/services/api/`,由浏览器前端直连,不假设存在项目后端
- 全局或跨页面状态优先放在 `web/src/stores/` - 全局或跨页面状态优先放在 `web/src/stores/`
- 已经放在全局 store 或全局 hook 中的状态/动作,组件需要时直接使用对应 store/hook,不要为了“纯组件”层层透传 props;避免一个组件传递过多参数。 - 已经放在全局 store 或全局 hook 中的状态/动作,组件需要时直接使用对应 store/hook,不要为了“纯组件”层层透传 props;避免一个组件传递过多参数。
- 全局组件、全局常量、全局配置等全局性质的内容不要作为 props 或参数层层传递;哪里需要就在哪里直接从对应全局入口获取。 - 全局组件、全局常量、全局配置等全局性质的内容不要作为 props 或参数层层传递;哪里需要就在哪里直接从对应全局入口获取。
- 多个页面重复出现的 UI 副作用动作,例如复制文本并提示、下载并提示、统一确认弹窗,优先抽成 `web/src/hooks/` 下的全局 hook;不要放进 store,除非它确实是需要共享/订阅的状态。 - 多个页面重复出现的 UI 副作用动作,例如复制文本并提示、下载并提示、统一确认弹窗,优先抽成 `web/src/hooks/` 下的全局 hook;不要放进 store,除非它确实是需要共享/订阅的状态。
- 画布相关状态和组件放在 `web/src/app/(user)/canvas/` 内部 - 路由页面放在 `web/src/pages/`,页面布局放在 `web/src/layouts/`,路由配置放在 `web/src/router.tsx`
- 页面里只有一个主业务组件时直接写在 `page.tsx`,不要单独拆 `Manager` 组件再传一堆 props - 画布页面放在 `web/src/pages/canvas/`,画布组件放在 `web/src/components/canvas/`,画布状态放在 `web/src/stores/canvas/`,画布工具函数放在 `web/src/lib/canvas/`
- 页面按目录组织,例如 `web/src/pages/image/index.tsx`;页面里只有一个主业务组件时直接写在对应页面入口中,不要单独拆 `Manager` 组件再传一堆 props。
- 不要新增只做简单转发的组件,例如只 `return <X>{children}</X>` 或只换个名字透传 props;直接在使用处使用真实组件或把逻辑写进当前文件。 - 不要新增只做简单转发的组件,例如只 `return <X>{children}</X>` 或只换个名字透传 props;直接在使用处使用真实组件或把逻辑写进当前文件。
- 页面私有 hook 放在对应页面目录下,例如 `admin/assets/use-admin-assets.ts`;只有多个页面真实复用的 hook 才放到外层 `hooks/` - 页面私有 hook 放在对应页面目录下,例如 `admin/assets/use-admin-assets.ts`;只有多个页面真实复用的 hook 才放到外层 `hooks/`
- 管理后台页面私有组件放到各自页面目录的 `components/` 下,例如 `admin/assets/components/``admin/prompts/components/`;不要为了单页面使用放到 `admin/components/` 共享目录。 - 管理后台页面私有组件放到各自页面目录的 `components/` 下,例如 `admin/assets/components/``admin/prompts/components/`;不要为了单页面使用放到 `admin/components/` 共享目录。
@@ -60,20 +51,22 @@
- 不要硬编码黑白、stone、slate 等颜色导致浅色/深色主题不一致。 - 不要硬编码黑白、stone、slate 等颜色导致浅色/深色主题不一致。
- 新增画布按钮、弹窗、浮层时,尽量复用已有工具栏、节点面板、Modal 的视觉风格。 - 新增画布按钮、弹窗、浮层时,尽量复用已有工具栏、节点面板、Modal 的视觉风格。
- 画布顶部工具栏和状态信息优先采用极简扁平风格:无边框、无阴影、无胶囊背景,融入整体背景,弱化按钮感,仅保留轻微 hover 反馈,保持简洁现代、低视觉重量。 - 画布顶部工具栏和状态信息优先采用极简扁平风格:无边框、无阴影、无胶囊背景,融入整体背景,弱化按钮感,仅保留轻微 hover 反馈,保持简洁现代、低视觉重量。
- 左侧画布面板等列表里的节点/元素缩略图容器,非图片类型(文本、配置、视频、音频等)不要使用 `theme.node.fill``#e7e5df`/`#292524`)这类灰色背景,图标直接无背景展示,尽量不要给多余底色,保持干净。
- 画布内的操作按钮(如面板里的「添加」「导出」「选择」等)默认用扁平无底色样式:透明背景、仅 `hover:bg-black/5 dark:hover:bg-white/10` 轻微反馈,靠图标+文字表达,不要用 `theme.toolbar.activeBg``#e7e5df`/`#3a3631`)或 `theme.node.fill` 之类的灰色作为按钮填充底色。灰色 `activeBg` 只允许用于「选中态」等需要表达状态的高亮,不要当普通装饰底色。
- 图片节点尺寸逻辑要尊重原始比例,除非功能明确要求自由变形。 - 图片节点尺寸逻辑要尊重原始比例,除非功能明确要求自由变形。
- 批量生成、多图展示、助手面板等画布交互要尽量简洁,不要占用过多画布空间。 - 批量生成、多图展示、助手面板等画布交互要尽量简洁,不要占用过多画布空间。
## 文档规范 ## 文档规范
- README 保持简洁,只放项目介绍、核心功能、快速开始和文档入口。 - README 保持简洁,只放项目介绍、核心功能、快速开始和文档入口。
- 详细功能介绍写到 `docs/features.md` - `docs/index.md` 放给 AI 使用的文档索引,不要再放到 `docs/content/docs/` 内容目录里
- 后续待办写到 `docs/todo.md` - 详细功能介绍写到 `docs/content/docs/overview/features.mdx`
- 已实现但还需要用户测试确认的事项写到 `docs/pending-test.md` - 后续待办写到 `docs/content/docs/progress/todo.mdx`
- `docs/pending-test.md` 用来记录这个版本实际做了哪些可测试变更;`CHANGELOG.md``Unreleased` 只保留对这些变更的版本级归纳,避免逐条照搬实现细节 - 已实现但还需要用户测试确认的事项写到 `docs/content/docs/progress/pending-test.mdx`
- 每次 todo 事项完成后,先从 `docs/todo.md` 移到 `docs/pending-test.md`,不要直接写进正式功能说明;用户确认测试通过后再更新 `docs/features.md` - `docs/content/docs/progress/pending-test.mdx` 用来记录这个版本实际做了哪些可测试变更;`CHANGELOG.md``Unreleased` 只保留对这些变更的版本级归纳,避免逐条照搬实现细节
- 每次任务完成,都要根据实际变更检查并更新 `docs/todo.md` `docs/pending-test.md`;如果功能或待办没有变化,也要确认无需修改 - 每次重大改动(新增/调整/删除功能、接口或工具,影响用户可感知行为)完成,都要`CHANGELOG.md` `Unreleased` 追加一条记录,按 `[新增]` / `[调整]` / `[修复]` / `[优化]` 前缀分类,用一句中文归纳;纯内部重构、格式化、无用户可感知影响的小改动可不记
- 接口响应规则写到 `docs/api-response.md` - 每次 todo 事项完成后,先从 `docs/content/docs/progress/todo.mdx` 移到 `docs/content/docs/progress/pending-test.mdx`,不要直接写进正式功能说明;用户确认测试通过后再更新 `docs/content/docs/overview/features.mdx`
- 数据库结构写到 `docs/backend-database.md` - 每次任务完成前,都要根据实际变更检查并更新 `docs/content/docs/progress/todo.mdx``docs/content/docs/progress/pending-test.mdx`;如果功能或待办没有变化,也要确认无需修改
- 文档不要写过期日期;除非用户明确要求记录具体时间。 - 文档不要写过期日期;除非用户明确要求记录具体时间。
## 发版本流程 ## 发版本流程
+194
View File
@@ -2,6 +2,200 @@
## Unreleased ## Unreleased
+ [修复] 画布 Agent 默认通过画布节点生成内容,仅在用户明确指定时使用 Codex 内置生图并将结果展示到对话和当前画布。
+ [优化] Agent 面板标题、功能标签与新对话操作合并为单行顶部栏,减少面板空间占用。
+ [新增] 画布 Agent 支持请求批准、自动审查和完全访问三档 Codex 权限,并可在对话中审批文件、命令与网络访问。
+ [修复] Agent 回复中的本地绝对文件路径改为在系统文件管理器中定位,不再被浏览器错误拼接为 localhost 地址。
+ [优化] Agent Markdown 代码块与外链确认弹窗改为紧凑中文样式,改善右侧面板阅读体验。
+ [优化] Agent 思考摘要支持 Markdown 渲染,改善强调、列表和代码内容的阅读效果。
+ [修复] Agent 对话启用 Codex 可读思考摘要,并修复一轮结束或刷新恢复后思考摘要、命令和工具过程记录缺失的问题。
+ [优化] Agent 思考摘要和命令执行改为无边框紧凑折叠行,并保留 Codex 实时返回的具体思考摘要。
+ [调整] Agent 工具确认模式移至对话输入框,并默认使用自动确认。
+ [优化] Agent 用户图片消息改为紧凑缩略图展示,并支持单击放大预览。
+ [修复] 修复 Agent 图片预览打开后在对话区域产生额外空行的问题。
+ [优化] Canvas Agent 内置 Codex CLI 升级至 0.145.0。
+ [修复] Canvas Agent 停止任务改为中断当前 Codex turn,避免终止整个 App Server 进程。
+ [新增] Canvas Agent 支持通过 `--debug` 输出详细日志并按日期保存到本地日志文件。
+ [优化] Canvas Agent Debug 日志改为简洁的纯文本单行格式。
+ [优化] 精简网页与本地 Agent 的 HTTP 诊断日志,过滤轮询和流式重复事件并改用任务摘要展示。
+ [优化] Agent 对话移除双方头像,用户消息改为右侧透明排版,工具活动改为图标与状态同行的紧凑卡片;输入框上方居中实时展示带平滑数字动画的最新一次模型调用 Token 用量。
+ [优化] Canvas Agent 流式回复改为增量合并传输并隔离消息列表渲染,减少长回复和长会话中的重复更新与界面卡顿。
+ [优化] Agent 对话新增思考摘要、计划、命令、搜索和文件变更时间线,工具卡片改为中文摘要与用户可读详情,不再展示原始 JSON。
+ [新增] Agent 对话支持 Codex 结构化任务进度,在输入区上方独立折叠展示并逐项实时更新待处理、进行中和已完成状态。
+ [优化] Agent 长时间等待时显示当前处理阶段、已等待时长和停止提示,并避免新建空会话反复记录历史读取失败。
+ [修复] 修复 Agent 发送后的运行提示闪退及回复需要切换标签页才显示的问题。
+ [修复] Agent 在模型繁忙等任务失败时会立即结束等待状态,并在对话和历史中展示中文错误原因与重试建议。
+ [优化] Agent 历史记录支持多选批量删除,点击记录可直接进入对话。
+ [调整] 进入画布并连接 Agent 时默认开启空白新对话,不再自动加载上一次会话,历史对话改为由用户主动选择恢复。
+ [优化] Agent 在画布内接收任务时优先直接操作当前画布,不再无故查询画布列表或重复进入画布。
+ [修复] 修复 Agent 用户图片附件位置偏移,历史同步或重新打开对话后仍保留本地缩略图,并且不再回显内部附件说明。
+ [修复] 修复画布全局复制快捷键拦截 Agent 对话和节点详情文本复制的问题。
+ [优化] Canvas Agent 改为从独立指令文件初始化当前工作目录的 AGENTS.md,不再为每轮消息重复拼接前置提示词。
+ [优化] 重构 Canvas Agent 源码目录,按 Agent、画布、服务与通用工具拆分模块职责,提升代码可维护性。
## v0.11.0 - 2026-07-28
+ [新增] 画布文本生成新增推理强度设置,支持默认调用与自定义调用脚本读取所选档位。
+ [新增] 生图与视频工作台的参考资产区域支持直接拖动文件上传。
+ [新增] 配置与用户偏好支持通过配置文件导入和导出。
+ [新增] 模型渠道新增火山方舟协议。
+ [优化] 画布左侧元素列表新增图片放大预览操作。
+ [优化] 图片遮罩、切图与裁剪编辑支持常用快捷操作并修复高分辨率图片预览闪烁。
+ [优化] 提示词详情弹窗改为上下布局并限制显示尺寸。
+ [修复] 画布组装提示词浮层限制正文高度,长提示词改为区域内滚动查看。
+ [修复] 修复画布节点提示词过长时无法使用鼠标滚轮滚动的问题。
+ [修复] 修复画布重试时组装提示词重复以及切换类型调用模型不一致的问题。
+ [修复] 修复生成完成后再次选中画布节点时提示词不再回显的问题。
## v0.10.0 - 2026-07-25
+ [新增] 提示词来源新增BananaPromptQuicker,并支持添加自定义标准 JSON 来源。
+ [新增] Agent 新增统一状态查询,支持查看画布、生图工作台和视频工作台任务进度。
+ [调整] 内置提示词来源改为读取ImagePrompts 统一JSON 数据,不再由画布端分别解析。
+ [调整] 提示词来源配置保留卡片式交互,并展示来源数量、同步状态和上次成功时间。
+ [优化] 提示词来源支持独立缓存和更新,更新失败时保留上一次成功内容。
+ [优化] Agent 对话区分用户与 AI 消息,并优化会话切换和回到最新消息的交互。
+ [优化] 画布生成完成后保留提示词输入,并改善提示词库和资产面板的交互性能。
+ [优化] 提示词中心标题居中、搜索输入增加防抖,并采用左侧筛选、右侧内容的双栏布局。
+ [修复] 本地 Agent 完善多标签页请求隔离、结果归属、焦点回退和 Codex 会话状态同步。
+ [修复] 本地 Agent 上传的图片附件可正确创建画布图片节点并连接生成流程。
+ [修复] 画布提示词库支持跨来源搜索,插入节点时保留提示词标题。
## v0.9.0 - 2026-07-17
+ [新增] 左侧面板「资产」Tab 支持上传添加图片/视频资产、卡片悬停移除资产。
+ [新增] 左侧画布面板支持拖拽调整宽度、展开/收起(带动画),顶栏菜单左侧新增面板开关按钮。
+ [新增] 顶栏菜单新增「导出当前画布」,导出为包含全部资源的压缩包。
+ [新增] 左侧画布元素列表支持多选并批量导出选中元素为压缩包。
+ [新增] 可选的网站统计分析:支持 Google Analytics 4 与百度统计。
+ [调整] 画布节点名称默认不再显示,仅在选中/悬停/编辑时出现,画布更简洁。
+ [优化] 左侧面板「画布/资产」切换改为带滑动下划线的动画,移除非图片元素图标的灰色底色。
+ [优化] 画布节点提示词面板 `@` 引用图片时,输入框内直接显示真实缩略图。
+ [优化] 移除画布节点右上角的「图片1/文本1」资源角标,引用改在对话面板 `@` 直接选取。
+ [修复] 连接本地 Codex Agent 后,拖拽画布节点边框缩放等高频编辑导致页面崩溃。
## v0.8.2 - 2026-07-16
+ [新增] 图像设置新增「透明背景」开关,开启后生成无背景的透明图像。
+ [优化] 画布节点输入区域移除灰色底色与边框、美化样式。
+ [修复] 画布节点提示词输入框补上悬停文本光标。
## v0.8.1 - 2026-07-16
+ [新增] 插件 SDK 扩展:AI 生成能力、面板控制能力。
+ [优化] 3D 全景(1.1.0)支持上传与 AI 生成并升级查看器;
+ [优化] HTML 节点(1.2.0)迁移到统一交互开关;
+ [优化] 便利贴(1.1.0)可拖动移动、自选颜色、移除资源角标与衍生功能;
+ [优化] SVG 节点(1.1.0)透明背景融入画布、可拖动、去除默认值;
+ [优化] 画布节点渲染性能:memo 化节点回调,交互时不再全量重渲染;
+ [修复] 修复 Markdown 节点在点击/移动视角时重复渲染导致图片反复请求的问题;
+ [修复] 修复插件版本号显示不更新,插件面板新增可升级绿点提醒。
## v0.8.0 - 2026-07-15
+ [新增] 画布节点插件系统:支持通过 URL 动态安装/启用/更新/卸载远程节点插件。
+ [新增] 插件开发 SDK,可用 TypeScript 开发画布节点插件。
+ [新增] 新增 Markdown、SVG、HTML、3D 全景、便利贴等示例插件。
+ [新增] 官方插件注册表:节点插件面板可从项目仓库读取官方插件列表并一键安装。
+ [新增] 在画布右上角工具栏新增「节点插件」入口。
+ [新增] 支持自定义生图/视频接口调用方式以适配不同中转站。
## v0.7.1 - 2026-07-15
+ [修复] 修复页面白屏报错的问题。
## v0.7.0 - 2026-07-14
+ [新增] Agent 对话消息改用 streamdown 流式渲染,提升Markdown 内容展示效果。
+ [新增] Agent 新增画布、工作台、提示词库和素材等站点级工具。
+ [新增] Agent 面板改为全站常驻右侧栏,开关时同步推动顶栏和页面内容。
+ [新增] Agent 新增 `site_navigate` 工具,支持页面跳转。
+ [新增] Agent 对话运行中支持一键停止,中断当前 Codex turn。
+ [新增] 画布节点支持统一维护名称字段,默认显示在节点上方,并可直接双击名称编辑。
+ [新增] 画布新增组节点,支持节点拖入/拆出分组、拖拽高亮吸附和移动组时带动子节点。
+ [新增] 画布空白区域支持双击打开节点选择菜单,并在点击位置创建节点。
+ [调整] Codex 会话改为站点级连续线程,跨页面和跨画布保持同一上下文。
+ [调整] 移除仅前端调用 OpenAI responses 接口,统一走 MCP + 本地 Codex 链路。
+ [调整] 画布节点顶部工具条改为点击选中节点后显示,避免鼠标经过节点时频繁弹出。
+ [优化] 本地 Agent 连接说明明确区分插件 / 手动 MCP 才会增加 Codex token 消耗。
+ [优化] 优化本地 Agent 连接说明,区分 Codex 插件启动和直接运行 Agent 两种方式。
+ [修复] 修复 Gemini 格式生图时因内置比例列表触发误报导致生成失败的问题。
## v0.6.0 - 2026-07-09
+ [新增] 新增Codex App插件支持。
+ [新增] 配置与用户偏好新增独立页面和 Codex 连接配置 Tab。
+ [新增] 新增GitHub Pages 前端静态站点发布 workflow。
+ [新增] 图片切图支持等分线直接拖拽调整,并可新增、删除和重置横向 / 纵向切图线。
+ [调整] Docker 运行镜像改为 nginx 静态托管。
+ [调整] 移除网站Agent模式,专注于连接Codex Agent操作画布
+ [修复] 修复生图工作台重试成功结果刷新后丢失的问题。
+ [修复] 修复 Gemini 调用格式生图未传递尺寸比例配置的问题。
+ [修复] 修复前端 TypeScript 构建报错。
+ [修复] 修复画布生成配置切换文本/视频/音频模式时模型仍显示为生图模型的问题。
+ [修复] 兼容中转站视频任务直接返回视频 URL 且没有 `/content` 接口的情况,并优化失败原因展示。
## v0.5.0 - 2026-07-05
+ [新增] 渠道兼容Gemini格式。
+ [调整] 前端从 Next.js 迁移到 Vite,项目改为静态前端构建。
+ [调整] 移除已 404 的 EvoLinkAI 提示词来源。
## v0.4.0 - 2026-06-16
+ [新增] 新增网页版Agent Loop模式。
+ [新增] 支持Vercel一键部署。
+ [调整] 移除后端,项目定位为个人画布工具。
## v0.3.0 - 2026-06-15
+ [新增] 新增canvas-agent通过codex操作画布。
## v0.2.5 - 2026-06-08
+ [新增] 新增图片切图功能。
+ [新增] 支持webdav同步数据。
+ [修复] 修复画布文字节点错误问题。
## v0.2.4 - 2026-06-04
+ [新增] 新增图片反推提示词功能。
## v0.2.3 - 2026-06-04
+ [新增] 新增图片蒙版局部修改功能。
+ [优化] 优化配置节点@图片功能
## v0.2.2 - 2026-06-04
+ [新增] 新增图片放大工具。
+ [优化] 优化图片工具条,增加自定义功能。
+ [修复] 修复端口冲突问题、pg/mysql未初始化问题。
## v0.2.1 - 2026-06-03
+ [新增] 新增文档站点页面。
+ [优化] 优化画布连线交互。
+ [优化] 优化模型选择用户偏好。
## v0.2.0 - 2026-06-01
+ [新增] 支持通过火山方舟AgentPlan接入。
+ [新增] 视频生成支持声音、水印及图片/视频/音频参考输入。
+ [新增] 画布新增音频节点。
+ [优化] 图片/视频素材支持 `图片1`编号注入提示词。
## v0.1.1 - 2026-05-30
+ [新增] 支持New API跳转并自动填入Base URL和API Key配置。
## v0.1.0 - 2026-05-26
+ [优化] 优化我的画布、我的素材导出功能
+ [修复] 修复画布撤销,配置节点等bug问题
## v0.0.9 - 2026-05-26
+ [新增] 新增视频创作台页面。 + [新增] 新增视频创作台页面。
+ [修复] 修复图片节点size参数传递问题。 + [修复] 修复图片节点size参数传递问题。
+91
View File
@@ -0,0 +1,91 @@
# Contributor License Agreement
This Contributor License Agreement ("Agreement") applies to any code,
documentation, design asset, issue text, pull request, patch, or other
material ("Contribution") that you submit to the infinite-canvas project
("Project").
By submitting a Contribution, you agree to the terms below.
## 1. Copyright License
You keep ownership of your Contributions.
You grant the Project maintainers, including the repository owner, a
perpetual, worldwide, non-exclusive, royalty-free, irrevocable license to use,
reproduce, modify, prepare derivative works of, publicly display, publicly
perform, sublicense, distribute, commercialize, and otherwise exploit your
Contributions as part of the Project and related distributions.
This license allows the maintainers to publish your Contributions under the
Project's open-source license, currently GNU Affero General Public License
v3.0, and to use them in commercial, proprietary, privately licensed,
source-available, or other versions, services, support offerings, or
derivatives related to the Project.
## 2. Relicensing
You grant the Project maintainers, including the repository owner, the
perpetual, worldwide, irrevocable right to relicense the Project, in whole or
in part, under any license terms, whether existing now or created in the
future.
You agree that your Contributions may be incorporated into and distributed as
part of versions of the Project released under open-source, source-available,
proprietary, commercial, private, dual-license, or other licensing models,
without requiring additional permission, notice, approval, or compensation.
This right includes distribution of the Project as software, hosted services,
Software-as-a-Service (SaaS), managed services, commercial offerings, OEM
products, embedded products, and any other form of distribution or use.
## 3. Patent License
If your Contribution is covered by patents that you own or control, you grant
the Project maintainers and recipients of the Project a perpetual, worldwide,
non-exclusive, royalty-free, irrevocable patent license to make, use, sell,
offer for sale, import, and otherwise run, modify, and distribute your
Contribution as part of the Project.
## 4. Right to Contribute
You represent that:
* You have the legal right to submit the Contribution.
* The Contribution is your original work, or you have permission to submit it
under this Agreement.
* Any third-party material included in the Contribution is clearly identified
and compatible with the Project's license and this Agreement.
* If you submit a Contribution on behalf of an employer or organization, you
are authorized to do so.
## 5. No Obligation
The maintainers are not required to accept, review, merge, publish, or keep any
Contribution. The maintainers may modify, reject, revert, or remove
Contributions at their sole discretion.
## 6. No Warranty
You provide your Contributions "as is", without warranties or conditions of any
kind, express or implied, including warranties of merchantability, fitness for
a particular purpose, title, and non-infringement.
## 7. How This Agreement Is Accepted
Submitting a pull request, patch, commit, issue attachment, design asset,
documentation update, or any other Contribution to this repository constitutes
acceptance of this Agreement.
Maintainers may request explicit confirmation by asking contributors to comment:
```text
I have read and agree to CLA.md.
```
or by requiring acceptance through an automated CLA workflow.
## 8. Contact
For questions about this Agreement, contact the maintainers through the
repository or email `1844025705@qq.com`.
+8 -29
View File
@@ -1,41 +1,20 @@
# 构建 Next.js 前端产物。 # 构建 Vite 前端产物。
FROM oven/bun:1.3.13 AS web-build FROM oven/bun:1.3.13 AS web-build
WORKDIR /app/web WORKDIR /app/web
COPY web/package.json web/bun.lock ./ COPY web/package.json web/bun.lock ./
RUN --mount=type=cache,target=/root/.bun/install/cache bun install --frozen-lockfile --registry=https://registry.npmmirror.com --cache-dir=/root/.bun/install/cache RUN --mount=type=cache,target=/root/.bun/install/cache bun install --cache-dir=/root/.bun/install/cache
COPY VERSION /app/VERSION COPY VERSION /app/VERSION
COPY CHANGELOG.md /app/CHANGELOG.md COPY CHANGELOG.md /app/CHANGELOG.md
COPY web ./ COPY web ./
RUN bun run build RUN bun run build
# 构建 Go 后端入口。 # 运行镜像:只启动静态前端,AI 请求由浏览器前台直连用户自己的接口。
FROM golang:1.25-alpine AS api-build FROM nginx:1.27-alpine
WORKDIR /app COPY --from=web-build /app/web/dist /usr/share/nginx/html
COPY go.mod go.sum ./ COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY config ./config COPY web/docker-entrypoint.sh /docker-entrypoint.d/40-runtime-config.sh
COPY handler ./handler RUN chmod +x /docker-entrypoint.d/40-runtime-config.sh
COPY middleware ./middleware
COPY model ./model
COPY repository ./repository
COPY router ./router
COPY service ./service
COPY main.go ./
RUN go build -o /server .
# 运行镜像:Next.js 对外监听 3000,Go 只在容器内部监听 8080。
FROM oven/bun:1.3.13
WORKDIR /app
COPY VERSION /app/VERSION
COPY CHANGELOG.md /app/CHANGELOG.md
COPY --from=api-build /server /app/server
COPY --from=web-build /app/web /app/web
ENV PROMPT_DATA_DIR=/app/data/prompts
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && rm -rf /var/lib/apt/lists/*
RUN mkdir -p /app/data/prompts
EXPOSE 3000 EXPOSE 3000
# 先启动内部 Go API,再由 Next.js 提供页面并代理 /api/*。
CMD ["sh", "-c", "PORT=8080 /app/server & cd /app/web && HOSTNAME=0.0.0.0 PORT=3000 bun run start"]
+69 -27
View File
@@ -4,52 +4,86 @@
<h1 align="center">无限画布 (infinite-canvas)</h1> <h1 align="center">无限画布 (infinite-canvas)</h1>
<p align="center">
<a href="https://linux.do/"><img src="https://img.shields.io/badge/Linux.do-Community-2b6de8?style=flat-square" alt="Linux.do"></a>
<a href="https://render.com/deploy?repo=https://github.com/basketikun/infinite-canvas"><img src="https://img.shields.io/badge/Render-Deploy-46e3b7?style=flat-square&logo=render&logoColor=111111" alt="Deploy to Render"></a>
<a href="https://github.com/basketikun/infinite-canvas"><img src="https://img.shields.io/github/stars/basketikun/infinite-canvas?style=flat-square&logo=github" alt="GitHub stars"></a>
<a href="https://github.com/basketikun/infinite-canvas/tags"><img src="https://img.shields.io/github/v/tag/basketikun/infinite-canvas?style=flat-square&label=version" alt="Version"></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/license-AGPL--3.0-f97316?style=flat-square" alt="License"></a>
<a href="https://vite.dev/"><img src="https://img.shields.io/badge/Vite-7-646cff?style=flat-square&logo=vite&logoColor=white" alt="Vite"></a>
<a href="https://reactrouter.com/"><img src="https://img.shields.io/badge/React_Router-7-ca4245?style=flat-square&logo=reactrouter&logoColor=white" alt="React Router"></a>
</p>
<p align="center">
<a href="https://trendshift.io/repositories/50077?utm_source=repository-badge&amp;utm_medium=badge&amp;utm_campaign=badge-repository-50077" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/repositories/50077" alt="basketikun%2Finfinite-canvas | Trendshift" width="250" height="55"/></a>
</p>
<p align="center">
<a href="docs/content/docs/overview/quick-start.mdx">快速开始</a> · <a href="docs/content/docs/overview/features.mdx">功能介绍</a> · <a href="docs/content/docs/overview/render.mdx">Render 部署</a> · <a href="docs/content/docs/overview/docker.mdx">Docker 部署</a> · <a href="docs/content/docs/canvas/canvas-node-manual.mdx">画布节点操作手册</a> · <a href="docs/content/docs/canvas/canvas-shortcuts.mdx">画布快捷键</a> · <a href="CLA.md">贡献者协议</a> · <a href="SECURITY.md">漏洞提交</a> · <a href="docs/content/docs/progress/todo.mdx">待办事项</a> · <a href="canvas-agent/README.md">本地 Canvas Agent</a> · <a href="plugins/infinite-canvas">Codex app 插件</a>
</p>
无限画布是一款面向图片创作的开源工作台。它把画布编排、AI 图片生成、参考图编辑、对话助手、提示词库和素材沉淀放在同一个界面里,适合用来探索视觉方案并连续迭代图片结果。 无限画布是一款面向图片创作的开源工作台。它把画布编排、AI 图片生成、参考图编辑、对话助手、提示词库和素材沉淀放在同一个界面里,适合用来探索视觉方案并连续迭代图片结果。
> [!CAUTION] > [!CAUTION]
> 项目目前处于开发阶段,不保证历史数据兼容。各种数据库结构和存储格式都可能直接调整,欢迎关注后续更新,当前更适合个人/本地部署,不建议直接公网多人共用 > 项目目前处于开发阶段,不保证历史数据兼容。各种本地存储格式都可能直接调整,欢迎关注后续更新。
> >
> 如果你需要稳定维护自己的分支,建议自行 fork 后独立开发。二次开发与 PR 请保留原作者信息和前端页面标识。 > 如果你需要稳定维护自己的分支,建议自行 fork 后独立开发。二次开发与 PR 请保留原作者信息和前端页面标识。
## 赞助商
<table>
<tr>
<td width="190" align="center">
<a href="https://www.atlascloud.ai/zh?utm_source=github&amp;utm_medium=link&amp;utm_campaign=infinite-canvas"><img src="assets/atlascloud.svg" width="163" alt="Atlas Cloud"></a>
</td>
<td>
<a href="https://www.atlascloud.ai/zh?utm_source=github&amp;utm_medium=link&amp;utm_campaign=infinite-canvas">Atlas Cloud</a> is a full-modal AI inference platform that gives developers a single AI API to access video generation, image generation, and LLM APIs. Instead of managing multiple vendor integrations, you connect once and get unified access to 300+ curated models across all modalities. Check out <a href="https://www.atlascloud.ai/console/coding-plan">Atlas Cloud's new coding plan promotion</a> for more budget-friendly API access.
</td>
</tr>
</table>
## 核心功能 ## 核心功能
- 无限画布:多画布项目、节点拖拽缩放、连线、小地图、撤销重做、导入导出。 - 无限画布:多画布项目、节点拖拽缩放、连线、小地图、撤销重做、导入导出。
- AI 创作:支持 OpenAI 兼容接口文生图、图生图、参考图编辑文本问答。 - AI 创作:浏览器前台直连你配置的 OpenAI 兼容接口,支持文生图、图生图、参考图编辑文本问答、音频和视频生成
- 画布助手:围绕选中节点和上游节点对话、生图,并把结果插回画布。 - 画布助手:围绕选中节点和上游节点对话、生图,并把结果插回画布。
- 提示词库:抓取多个 GitHub 开源项目,按案例整理数百个图片提示词。 - 本地 Agent:通过本机 Canvas Agent 连接 Codex / Claude Code,让 Agent 通过 MCP 操作当前画布;
- Codex App 插件:提供 Codex app 插件,安装后会自动注册 MCP 并尝试拉起本地 Agent。
- 插件系统:支持通过 URL 动态安装 / 启用 / 更新 / 卸载远程节点插件,并提供 TypeScript SDK 自行开发画布节点插件。
- 自定义接口调用:可自定义生图 / 视频接口的调用方式,灵活适配各类中转站与自建服务。
- 提示词库:浏览器前端直连多个 GitHub 开源项目,并缓存到 IndexedDB。
完整功能说明见 [docs/features.md](docs/features.md)。 完整功能说明见 [功能介绍](docs/content/docs/overview/features.mdx)。
如果你在为担心没有合适的生图API来发愁,可以查看该免费生图项目:[chatgpt2api](https://github.com/basketikun/chatgpt2api) 如果你在为担心没有合适的生图API来发愁,可以查看该免费生图项目:[chatgpt2api](https://github.com/basketikun/chatgpt2api)
## 技术栈
- 前端:Next.js、React、TypeScript、Tailwind CSS、Ant Design、Zustand、TanStack Query。
- 后端:Go、Gin、GORM。
- 部署:Docker。
## 快速开始 ## 快速开始
[![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/basketikun/infinite-canvas) AI API Key、Base URL、画布、素材和生成记录默认保存在浏览器本地。
### 本地开发
```bash ```bash
git clone git@github.com:basketikun/infinite-canvas.git git clone git@github.com:basketikun/infinite-canvas.git
cd infinite-canvas cd infinite-canvas
cp .env.example .env cd web
# 修改默认账号密码等信息 bun install
docker-compose up -d bun run dev
``` ```
本地源码构建运行: ### Docker 运行
```bash ```bash
cp .env.example .env git clone git@github.com:basketikun/infinite-canvas.git
docker compose -f docker-compose.local.yml up -d --build cd infinite-canvas
docker compose up -d
``` ```
运行后默认端口3000,可访问 `http://localhost:3000` 运行后默认端口3000,可访问 `http://localhost:3000`
如需要拉取提示词,可前往:`http://localhost:3000/admin/prompts` 首次打开后进入右上角配置,填入自己的 OpenAI 兼容 `Base URL``API Key`
如果默认的OpenAI接口调用方式与您的API不同,可自定义生图/视频脚本调用。
## 效果展示 ## 效果展示
@@ -66,18 +100,23 @@ docker compose -f docker-compose.local.yml up -d --build
<td width="50%"><img src="https://i.ibb.co/bj30FtS5/5.png" alt="5" border="0"></td> <td width="50%"><img src="https://i.ibb.co/bj30FtS5/5.png" alt="5" border="0"></td>
<td width="50%"><img src="https://i.ibb.co/hxRvjw51/image.png" alt="image" border="0"></td> <td width="50%"><img src="https://i.ibb.co/hxRvjw51/image.png" alt="image" border="0"></td>
</tr> </tr>
<tr>
<td width="50%"><img src="https://i.ibb.co/jkWsF8q1/image.png" alt="image" border="0"></td>
<td width="50%"><img src="https://i.ibb.co/XrnfXHx7/image.png" alt="image" border="0"></td>
</tr>
</table> </table>
## 文档 ## 联系方式
- [功能介绍](docs/features.md) 项目定制二次开发需求 / 生图 API 需求可联系。
- [部署说明](docs/deployment.md)
- [画布节点操作手册](docs/canvas-node-manual.md) 邮箱:1844025705@qq.com · QQ1844025705
- [画布快捷键](docs/canvas-shortcuts.md)
- [待办事项](docs/todo.md) ## 赞助支持
- [后端数据库说明](docs/backend-database.md)
- [系统配置数据结构](docs/system-settings.md) 本项目长期开放广告赞助合作,欢迎品牌 / 产品投放,你的支持是持续更新的动力!
- [接口响应约定](docs/api-response.md)
有广告赞助意向请通过上方联系方式沟通。
## 社区支持 ## 社区支持
@@ -89,6 +128,9 @@ docker compose -f docker-compose.local.yml up -d --build
本项目使用 GNU Affero General Public License v3.0,见 [LICENSE](LICENSE)。 本项目使用 GNU Affero General Public License v3.0,见 [LICENSE](LICENSE)。
你可以在遵守 AGPL-3.0 的前提下自由使用、复制、修改和分发本项目。如果你分发修改后的版本,或将其作为网站、SaaS 等网络服务提供给他人使用,需要向对应用户公开基于本项目形成的完整源代码,继续使用 AGPL-3.0 协议,并保留原项目的作者、版权、许可证和来源说明。
本项目禁止未经授权的闭源商用。如果你希望将无限画布用于商业项目,请尊重开源,遵循 AGPL-3.0 协议,继续开源基于本项目修改或开发的对应代码,回馈开源社区;如果无法公开对应代码,请联系作者购买商业授权后闭源使用。
## Star History ## Star History
+80
View File
@@ -0,0 +1,80 @@
# Security Policy
## Supported Versions
infinite-canvas is in active development. Security fixes are accepted for the
`main` branch and the latest tagged release. Older versions may be handled on a
best-effort basis.
## Reporting a Vulnerability
Please do not open a public issue with exploit details, credentials, private
API keys, proof-of-concept code, or screenshots that reveal sensitive data.
Preferred reporting channels:
1. Use GitHub private vulnerability reporting or a GitHub Security Advisory for
this repository, if available.
2. If a private GitHub report is not available, email `1844025705@qq.com` with
the subject `[infinite-canvas security]`.
3. If neither private channel is available, open a public issue that asks for a
private contact channel and does not include technical exploit details.
Please include:
- Affected version, commit, branch, or deployment mode.
- Clear reproduction steps.
- Impact and attack scenario.
- Any relevant logs, screenshots, or proof of concept, with secrets removed.
- Whether the issue affects local-only usage, hosted deployments, browser
storage, WebDAV sync, AI provider configuration, or API proxy behavior.
## Scope
### Canvas node plugins
The canvas supports third-party node plugins loaded from a remote URL. By
design, an installed plugin's code runs directly inside the web app with full
access to the page, including locally stored data such as AI API keys. This is
an intentional trade-off for extensibility, and the installer shows a warning
before installing. Therefore:
- Only install plugins from sources you trust.
- Reports that a *malicious plugin* can access page data or API keys are **out
of scope** — that is the documented behavior of the trust model.
- Reports **in scope** include: the app loading/executing plugin code without
the install confirmation, a plugin escaping its declared node type to break
core app integrity in ways not implied by "runs in the page", or the plugin
source cache being writable by an unrelated origin.
Examples of in-scope reports:
- Cross-site scripting or token exfiltration in the web app.
- Exposure of locally stored API keys or synced canvas data caused by project
code.
- Unsafe file handling, import/export behavior, or WebDAV proxy behavior.
- Authentication, authorization, or access-control flaws in project-managed
features.
- Supply-chain issues that are exploitable through this repository's shipped
code or default configuration.
Examples that are usually out of scope:
- Vulnerabilities in third-party AI providers, model APIs, hosting platforms,
or browser extensions outside this repository.
- Compromise of a user's own API key outside the app.
- Denial-of-service reports that require unrealistic traffic volume or physical
access to the user's device.
- Missing security headers without a demonstrated exploit path.
- Social engineering, phishing, spam, or account recovery requests.
- Dependency reports without a practical impact on this project.
## Disclosure
The maintainers aim to acknowledge valid reports within 7 days and coordinate a
fix before public disclosure. Response and fix timelines are best effort for
this community project.
Please allow time for investigation and remediation before publishing details.
Credit will be given on request unless you prefer to remain anonymous.
+1 -1
View File
@@ -1 +1 @@
v0.0.9 v0.11.0
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.3 KiB

+2
View File
@@ -0,0 +1,2 @@
dist
node_modules
+157
View File
@@ -0,0 +1,157 @@
# Infinite Canvas Agent
本地 Canvas Agent 用来连接画布网页和用户电脑上的 Codex / Claude Code。本地开发时优先连接 `http://localhost:3000`,不需要先使用线上站点。
## 启动
```bash
npx -y @basketikun/canvas-agent
```
需要排查连接、线程、Codex app-server 或工具调用问题时,可开启 Debug 模式:
```bash
npx -y @basketikun/canvas-agent --debug
```
Debug 日志会以 `[DEBUG][HH:mm:ss]` 等传统格式输出到终端,并按启动日期保存到 `~/.infinite-canvas/logs/canvas-agent-YYYY-MM-DD.log`。终端日志带级别颜色,文件日志为纯文本;日志包含 HTTP、SSE、线程、turn、Codex app-server 和工具调用事件,token 与图片 Data URL 会自动隐藏。
本仓库开发时也可以直接运行:
```bash
cd canvas-agent
npm install
npm run build
node dist/index.js
```
启动后会输出本机地址和 token
```txt
Local URL: http://127.0.0.1:17371
Connect token: xxxxxx
```
在画布右上角点击 `Agent`,填入地址和 token 后连接。
Codex app 插件会读取启动输出里的 Local URL 和 Connect token,并直接打开画布网页地址;Canvas Agent 不负责生成画布打开 URL。
Canvas Agent 默认只监听 `127.0.0.1`。网页第一次带正确 token 连接后,Canvas Agent 会记录该网页 Origin;之后其他 Origin 不能复用这个本地 Agent,除非用户清理 `~/.infinite-canvas/canvas-agent.json` 里的 `origins`
## 发布
`canvas-agent` 使用自己的 `package.json` 版本号,不跟仓库根目录 `VERSION` 绑定。推送到 `main` 后,GitHub Actions 会检查 npm 上是否已经存在当前包版本;不存在时才发布 `@basketikun/canvas-agent`
发布前需要在 GitHub 仓库 Secrets 中配置 `NPM_TOKEN`
## Codex MCP
如果希望 Codex 终端能直接操作画布,需要先把 Canvas Agent 注册成 Codex MCP。
直接运行 `npx -y @basketikun/canvas-agent` 只启动本地 Agent 服务,不会安装 MCP,也不会增加 Codex 工具上下文。只有安装 Codex app 插件,或手动执行 `codex mcp add` 后,`infinite-canvas` 工具才会进入 Codex 上下文;由于工具较多,不使用时建议移除。
通过插件安装时移除插件:
```bash
codex plugin remove infinite-canvas
```
手动添加 MCP 时移除 MCP
```bash
codex mcp remove infinite-canvas
```
### Codex app 插件
仓库内提供了 Codex app 插件:`plugins/infinite-canvas`。在 Codex app 中添加本仓库的 marketplace 后,可以安装 `Infinite Canvas` 插件;插件会注册同一个 `infinite-canvas` MCP,并带上画布操作说明。
添加本地 marketplace 时建议使用仓库绝对路径,避免 Codex 从其他工作目录解析失败:
```bash
cd /path/to/infinite-canvas
codex plugin marketplace add "$(pwd)"
codex plugin add infinite-canvas@infinite-canvas-local
```
插件默认通过 npm 启动 MCP;这个命令只提供 MCP 工具,不会把 MCP 写入全局配置,也不会在退出时自动卸载:
```bash
npx -y @basketikun/canvas-agent mcp
```
使用时可以直接在 Codex 里说“打开 Infinite Canvas”,插件会启动本地 Agent,读取 Local URL 和 Connect token,然后在右侧打开 `https://canvas.best/` 并自动新建、连接画布;只有明确要求使用本地项目时才会启动本地前端。
Canvas Agent 启动后,给 Codex 添加 MCP
```bash
codex mcp add infinite-canvas -- npx -y @basketikun/canvas-agent mcp
```
本仓库开发时可以改成,实际使用建议替换为本机绝对路径:
```bash
codex mcp add infinite-canvas -- node /path/to/infinite-canvas/canvas-agent/dist/index.js mcp
```
Canvas Agent 源码使用 TypeScript 编写,MCP 协议层使用官方 `@modelcontextprotocol/sdk`,工具入参使用 `zod` 描述。
如果希望终端里的 Codex 不被 MCP 审批卡住,可以在 `~/.codex/config.toml` 里给这个 MCP 设置自动放行:
```toml
[mcp_servers.infinite-canvas]
command = "npx"
args = ["-y", "@basketikun/canvas-agent", "mcp"]
default_tools_approval_mode = "approve"
```
可用工具:
- `canvas_get_state`
- `canvas_get_selection`
- `canvas_export_snapshot`
- `canvas_apply_ops`
- `canvas_create_text_node`
- `canvas_create_image_prompt_flow`
`canvas_apply_ops` 示例:
```json
{
"ops": [
{
"type": "add_node",
"nodeType": "text",
"title": "标题",
"position": { "x": 0, "y": 0 },
"metadata": { "content": "文本内容" }
}
]
}
```
## 侧边栏 Codex
本地面板会把提示词发送给 Canvas Agent。Canvas Agent 使用官方 `@openai/codex` CLI 的 `codex app-server --stdio` 启动并复用同一个 Codex thread,启动时会注入 `infinite-canvas` MCP 配置并自动放行 MCP 审批,真正执行画布修改前仍由网页侧边栏二次确认。
侧边栏会展示 Codex 返回的 `thread.started``turn.started``item.*``turn.completed` 等结构化事件;Canvas Agent 会合并短时间内的回复、思考摘要和命令输出增量,网页使用同一条消息持续更新,并把任务进度、计划、搜索、文件修改与工具操作整理为中文过程时间线。
侧边栏上传或粘贴的图片会先发到本机 Canvas Agent,再由 Canvas Agent 临时写入本机文件并作为 app-server `localImage` 输入传给 Codex;前端会提示附件体积,单次请求体限制为 30MB。
## Claude Code
Claude Code Adapter 代码暂时保留,但当前网页侧边栏只开放 Codex。后续开放 Claude 入口时,Canvas Agent 会调用本机 `claude -p --output-format stream-json` 并把流式 JSON 事件转发到侧边栏。
如果希望 Claude Code 也能操作画布,需要给 Claude Code 添加同一个 MCP。建议用 user scope,避免 Canvas Agent 从不同目录启动时找不到配置:
```bash
claude mcp add --scope user --transport stdio infinite-canvas -- npx -y @basketikun/canvas-agent mcp
```
本仓库开发时可以改成:
```bash
claude mcp add --scope user --transport stdio infinite-canvas -- node /path/to/infinite-canvas/canvas-agent/dist/index.js mcp
```
Canvas Agent 调用 Claude Code 时会默认带上 `--allowedTools mcp__infinite-canvas__*`,画布写操作仍由网页侧边栏确认。
+13
View File
@@ -0,0 +1,13 @@
# Infinite Canvas Agent
你正在帮助用户操作 Infinite Canvas 网站。
- 用户要求操作画布时,默认目标就是网页当前已经打开的画布。需要了解内容时先使用 `canvas_get_state`;读取成功后直接在该画布执行任务,不要调用 `canvas_list_projects`,也不要用 `site_navigate` 重复进入画布。
- 只有用户明确要求查看、选择或切换其他画布,或者 `canvas_get_state` 明确提示当前没有已连接画布时,才使用 `canvas_list_projects``site_navigate``site_navigate` 可跳转 `/``/canvas``/canvas/:id``/image``/video``/prompts``/assets``/config`
- 修改当前画布时根据任务使用已配置的 infinite-canvas MCP 工具;复杂批量改动使用 `canvas_apply_ops`
- 用户要求把上传附件放入画布或作为生成参考图时,必须先用 `canvas_create_attachment_nodes` 创建真实图片节点,再把节点 ID 传给生成流程,不要创建空图片占位节点。
- 生图与视频工作台分别使用 `workbench_image_*``workbench_video_*` 工具;提示词和素材分别使用 `prompts_search``assets_*` 工具。
- 用户要求生成图片、视频、音频或文本时,默认调用对应的 `canvas_generate_image``canvas_generate_video``canvas_generate_audio``canvas_generate_text`,通过当前画布的生成节点完成任务。
- 只有用户明确要求使用“Codex 内置生图”“ImageGen 技能”或意思明确相同的能力时,才使用 Codex 自带的 `imagegen`;不要因为用户只说“生成图片”就自行改用内置生图。内置生图完成后,其结果会由 Canvas Agent 自动展示到对话并插入当前画布,无需再创建空节点或重复生成。
- 只有用户明确说要在生图/视频工作台生成时,才使用 `workbench_image_*``workbench_video_*`。生成任务提交后应说明已经在画布或工作台开始生成,不要在实际没有结果时声称“已生成”。
- 需要生成内容时直接调用对应生成工具,不要绑定特定业务场景,不要模拟鼠标点击,不要要求用户手动复制 JSON。
+355
View File
@@ -0,0 +1,355 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "@basketikun/canvas-agent",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.12.1",
"@openai/codex": "0.145.0",
"express": "^5.1.0",
"winston": "^3.19.0",
"zod": "^3.25.0",
},
"devDependencies": {
"@types/express": "^5.0.0",
"@types/node": "^22.0.0",
"tsx": "^4.19.0",
"typescript": "^5.8.0",
},
},
},
"packages": {
"@colors/colors": ["@colors/colors@1.6.0", "", {}, "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA=="],
"@dabh/diagnostics": ["@dabh/diagnostics@2.0.8", "", { "dependencies": { "@so-ric/colorspace": "^1.1.6", "enabled": "2.0.x", "kuler": "^2.0.0" } }, "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.28.1.tgz", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.28.1.tgz", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="],
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="],
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="],
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="],
"@hono/node-server": ["@hono/node-server@1.19.14", "https://registry.npmmirror.com/@hono/node-server/-/node-server-1.19.14.tgz", { "peerDependencies": { "hono": "^4" } }, "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw=="],
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "https://registry.npmmirror.com/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="],
"@openai/codex": ["@openai/codex@0.145.0", "", { "optionalDependencies": { "@openai/codex-darwin-arm64": "npm:@openai/codex@0.145.0-darwin-arm64", "@openai/codex-darwin-x64": "npm:@openai/codex@0.145.0-darwin-x64", "@openai/codex-linux-arm64": "npm:@openai/codex@0.145.0-linux-arm64", "@openai/codex-linux-x64": "npm:@openai/codex@0.145.0-linux-x64", "@openai/codex-win32-arm64": "npm:@openai/codex@0.145.0-win32-arm64", "@openai/codex-win32-x64": "npm:@openai/codex@0.145.0-win32-x64" }, "bin": { "codex": "bin/codex.js" } }, "sha512-/PSPSFujjjmiyVFvG2yu/grOFhsWdokTH8t2KGWhXSo/M5n/dIDsnbsnO82/7bLtIoDuzQf7ATBUMWqPWQINlQ=="],
"@openai/codex-darwin-arm64": ["@openai/codex@0.145.0-darwin-arm64", "", { "os": "darwin", "cpu": "arm64" }, "sha512-h6aQ0UxnaP8mIM/9/qPAH9MNkRliJo88toq1T36IxNM2L5JSU0TFamu+MZn7YkFgDsrp0RfiI+97Tm8AVVxqtA=="],
"@openai/codex-darwin-x64": ["@openai/codex@0.145.0-darwin-x64", "", { "os": "darwin", "cpu": "x64" }, "sha512-FCYzVKCa9VoLtg9gVyzKpqylonfgZrfcWZN6HsXAZPeuo8CukdMqdgTUOhDn2V6h3MbqS0z6VqQVKUllN/yKhA=="],
"@openai/codex-linux-arm64": ["@openai/codex@0.145.0-linux-arm64", "", { "os": "linux", "cpu": "arm64" }, "sha512-8OLcPXaAol/FOrRoDxWhIiHIFa73KRsM41EKocjRZOwiT4TcelzJWn3dHyiuSb7teWF25rrslvSPyvhULYRRCQ=="],
"@openai/codex-linux-x64": ["@openai/codex@0.145.0-linux-x64", "", { "os": "linux", "cpu": "x64" }, "sha512-u8w8LLv3DvsfrDCoswLIemZ0SoNEXyi511WsfFsSiYUazk9qMsB/NtU8N9vhAfN7mZAxLFoMex4v66JjHuZWwA=="],
"@openai/codex-win32-arm64": ["@openai/codex@0.145.0-win32-arm64", "", { "os": "win32", "cpu": "arm64" }, "sha512-sub61rjEFevi1i3Zx7nAd4JM5XxoNFqMqFc5LfTo2xSI8ixHjFvEYDFDXwXOftT04n3Ht1Wh271ioUZpDiEjEg=="],
"@openai/codex-win32-x64": ["@openai/codex@0.145.0-win32-x64", "", { "os": "win32", "cpu": "x64" }, "sha512-u0h9lk094CaXRSqE34SBW2dRaQTPa6fASXqehczWH9QdsU62mBsiAgAdp6tCG4i+YzPmmhjD8FdXNnYGNmwuMg=="],
"@so-ric/colorspace": ["@so-ric/colorspace@1.1.6", "", { "dependencies": { "color": "^5.0.2", "text-hex": "1.0.x" } }, "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw=="],
"@types/body-parser": ["@types/body-parser@1.19.6", "https://registry.npmmirror.com/@types/body-parser/-/body-parser-1.19.6.tgz", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="],
"@types/connect": ["@types/connect@3.4.38", "https://registry.npmmirror.com/@types/connect/-/connect-3.4.38.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
"@types/express": ["@types/express@5.0.6", "https://registry.npmmirror.com/@types/express/-/express-5.0.6.tgz", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "^2" } }, "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA=="],
"@types/express-serve-static-core": ["@types/express-serve-static-core@5.1.1", "https://registry.npmmirror.com/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A=="],
"@types/http-errors": ["@types/http-errors@2.0.5", "https://registry.npmmirror.com/@types/http-errors/-/http-errors-2.0.5.tgz", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="],
"@types/node": ["@types/node@22.19.21", "https://registry.npmmirror.com/@types/node/-/node-22.19.21.tgz", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-VMeFBSCKQKmm2swI2kW51SFusDqekC6q9trBCvJ/JliDchFSuoYYKN7yVNjPthP1HKZcx3U1gI/wTcEBjEFKTA=="],
"@types/qs": ["@types/qs@6.15.1", "https://registry.npmmirror.com/@types/qs/-/qs-6.15.1.tgz", {}, "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw=="],
"@types/range-parser": ["@types/range-parser@1.2.7", "https://registry.npmmirror.com/@types/range-parser/-/range-parser-1.2.7.tgz", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="],
"@types/send": ["@types/send@1.2.1", "https://registry.npmmirror.com/@types/send/-/send-1.2.1.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ=="],
"@types/serve-static": ["@types/serve-static@2.2.0", "https://registry.npmmirror.com/@types/serve-static/-/serve-static-2.2.0.tgz", { "dependencies": { "@types/http-errors": "*", "@types/node": "*" } }, "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ=="],
"@types/triple-beam": ["@types/triple-beam@1.3.5", "", {}, "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="],
"accepts": ["accepts@2.0.0", "https://registry.npmmirror.com/accepts/-/accepts-2.0.0.tgz", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
"ajv": ["ajv@8.20.0", "https://registry.npmmirror.com/ajv/-/ajv-8.20.0.tgz", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="],
"ajv-formats": ["ajv-formats@3.0.1", "https://registry.npmmirror.com/ajv-formats/-/ajv-formats-3.0.1.tgz", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
"async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],
"body-parser": ["body-parser@2.2.2", "https://registry.npmmirror.com/body-parser/-/body-parser-2.2.2.tgz", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
"bytes": ["bytes@3.1.2", "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
"call-bound": ["call-bound@1.0.4", "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
"color": ["color@5.0.3", "", { "dependencies": { "color-convert": "^3.1.3", "color-string": "^2.1.3" } }, "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA=="],
"color-convert": ["color-convert@3.1.3", "", { "dependencies": { "color-name": "^2.0.0" } }, "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg=="],
"color-name": ["color-name@2.1.1", "", {}, "sha512-p2FdgwVx1a9yWBHP2wI0VgShkDpgN4kZISkxdNipGBJWpa5G6b04OINlVWCyJj0JmfvcPrgqt95E9k8yvaOJFg=="],
"color-string": ["color-string@2.1.4", "", { "dependencies": { "color-name": "^2.0.0" } }, "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg=="],
"content-disposition": ["content-disposition@1.1.0", "https://registry.npmmirror.com/content-disposition/-/content-disposition-1.1.0.tgz", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="],
"content-type": ["content-type@1.0.5", "https://registry.npmmirror.com/content-type/-/content-type-1.0.5.tgz", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
"cookie": ["cookie@0.7.2", "https://registry.npmmirror.com/cookie/-/cookie-0.7.2.tgz", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
"cookie-signature": ["cookie-signature@1.2.2", "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.2.2.tgz", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
"cors": ["cors@2.8.6", "https://registry.npmmirror.com/cors/-/cors-2.8.6.tgz", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="],
"cross-spawn": ["cross-spawn@7.0.6", "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"debug": ["debug@4.4.3", "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"depd": ["depd@2.0.0", "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
"dunder-proto": ["dunder-proto@1.0.1", "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"ee-first": ["ee-first@1.1.1", "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
"enabled": ["enabled@2.0.0", "", {}, "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="],
"encodeurl": ["encodeurl@2.0.0", "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
"es-define-property": ["es-define-property@1.0.1", "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
"es-errors": ["es-errors@1.3.0", "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
"es-object-atoms": ["es-object-atoms@1.1.2", "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="],
"esbuild": ["esbuild@0.28.1", "https://registry.npmmirror.com/esbuild/-/esbuild-0.28.1.tgz", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="],
"escape-html": ["escape-html@1.0.3", "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
"etag": ["etag@1.8.1", "https://registry.npmmirror.com/etag/-/etag-1.8.1.tgz", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
"eventsource": ["eventsource@3.0.7", "https://registry.npmmirror.com/eventsource/-/eventsource-3.0.7.tgz", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="],
"eventsource-parser": ["eventsource-parser@3.1.0", "https://registry.npmmirror.com/eventsource-parser/-/eventsource-parser-3.1.0.tgz", {}, "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg=="],
"express": ["express@5.2.1", "https://registry.npmmirror.com/express/-/express-5.2.1.tgz", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
"express-rate-limit": ["express-rate-limit@8.5.2", "https://registry.npmmirror.com/express-rate-limit/-/express-rate-limit-8.5.2.tgz", { "dependencies": { "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-uri": ["fast-uri@3.1.2", "https://registry.npmmirror.com/fast-uri/-/fast-uri-3.1.2.tgz", {}, "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ=="],
"fecha": ["fecha@4.2.3", "", {}, "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="],
"finalhandler": ["finalhandler@2.1.1", "https://registry.npmmirror.com/finalhandler/-/finalhandler-2.1.1.tgz", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
"fn.name": ["fn.name@1.1.0", "", {}, "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="],
"forwarded": ["forwarded@0.2.0", "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
"fresh": ["fresh@2.0.0", "https://registry.npmmirror.com/fresh/-/fresh-2.0.0.tgz", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
"fsevents": ["fsevents@2.3.3", "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"function-bind": ["function-bind@1.1.2", "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"get-intrinsic": ["get-intrinsic@1.3.0", "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
"get-proto": ["get-proto@1.0.1", "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
"gopd": ["gopd@1.2.0", "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
"has-symbols": ["has-symbols@1.1.0", "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
"hasown": ["hasown@2.0.4", "https://registry.npmmirror.com/hasown/-/hasown-2.0.4.tgz", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="],
"hono": ["hono@4.12.25", "https://registry.npmmirror.com/hono/-/hono-4.12.25.tgz", {}, "sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ=="],
"http-errors": ["http-errors@2.0.1", "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.1.tgz", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
"iconv-lite": ["iconv-lite@0.7.2", "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.7.2.tgz", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
"inherits": ["inherits@2.0.4", "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"ip-address": ["ip-address@10.2.0", "https://registry.npmmirror.com/ip-address/-/ip-address-10.2.0.tgz", {}, "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA=="],
"ipaddr.js": ["ipaddr.js@1.9.1", "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
"is-promise": ["is-promise@4.0.0", "https://registry.npmmirror.com/is-promise/-/is-promise-4.0.0.tgz", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
"is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
"isexe": ["isexe@2.0.0", "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"jose": ["jose@6.2.3", "https://registry.npmmirror.com/jose/-/jose-6.2.3.tgz", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="],
"json-schema-traverse": ["json-schema-traverse@1.0.0", "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"json-schema-typed": ["json-schema-typed@8.0.2", "https://registry.npmmirror.com/json-schema-typed/-/json-schema-typed-8.0.2.tgz", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="],
"kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="],
"logform": ["logform@2.7.0", "", { "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", "fecha": "^4.2.0", "ms": "^2.1.1", "safe-stable-stringify": "^2.3.1", "triple-beam": "^1.3.0" } }, "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"media-typer": ["media-typer@1.1.0", "https://registry.npmmirror.com/media-typer/-/media-typer-1.1.0.tgz", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
"merge-descriptors": ["merge-descriptors@2.0.0", "https://registry.npmmirror.com/merge-descriptors/-/merge-descriptors-2.0.0.tgz", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
"mime-db": ["mime-db@1.54.0", "https://registry.npmmirror.com/mime-db/-/mime-db-1.54.0.tgz", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
"mime-types": ["mime-types@3.0.2", "https://registry.npmmirror.com/mime-types/-/mime-types-3.0.2.tgz", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
"ms": ["ms@2.1.3", "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"negotiator": ["negotiator@1.0.0", "https://registry.npmmirror.com/negotiator/-/negotiator-1.0.0.tgz", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
"object-assign": ["object-assign@4.1.1", "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
"object-inspect": ["object-inspect@1.13.4", "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
"on-finished": ["on-finished@2.4.1", "https://registry.npmmirror.com/on-finished/-/on-finished-2.4.1.tgz", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
"once": ["once@1.4.0", "https://registry.npmmirror.com/once/-/once-1.4.0.tgz", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
"one-time": ["one-time@1.0.0", "", { "dependencies": { "fn.name": "1.x.x" } }, "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g=="],
"parseurl": ["parseurl@1.3.3", "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
"path-key": ["path-key@3.1.1", "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
"path-to-regexp": ["path-to-regexp@8.4.2", "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-8.4.2.tgz", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="],
"pkce-challenge": ["pkce-challenge@5.0.1", "https://registry.npmmirror.com/pkce-challenge/-/pkce-challenge-5.0.1.tgz", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="],
"proxy-addr": ["proxy-addr@2.0.7", "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
"qs": ["qs@6.15.2", "https://registry.npmmirror.com/qs/-/qs-6.15.2.tgz", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw=="],
"range-parser": ["range-parser@1.2.1", "https://registry.npmmirror.com/range-parser/-/range-parser-1.2.1.tgz", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
"raw-body": ["raw-body@3.0.2", "https://registry.npmmirror.com/raw-body/-/raw-body-3.0.2.tgz", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
"require-from-string": ["require-from-string@2.0.2", "https://registry.npmmirror.com/require-from-string/-/require-from-string-2.0.2.tgz", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
"router": ["router@2.2.0", "https://registry.npmmirror.com/router/-/router-2.2.0.tgz", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="],
"safer-buffer": ["safer-buffer@2.1.2", "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"send": ["send@1.2.1", "https://registry.npmmirror.com/send/-/send-1.2.1.tgz", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="],
"serve-static": ["serve-static@2.2.1", "https://registry.npmmirror.com/serve-static/-/serve-static-2.2.1.tgz", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="],
"setprototypeof": ["setprototypeof@1.2.0", "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
"shebang-command": ["shebang-command@2.0.0", "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
"shebang-regex": ["shebang-regex@3.0.0", "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
"side-channel": ["side-channel@1.1.1", "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.1.tgz", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4", "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ=="],
"side-channel-list": ["side-channel-list@1.0.1", "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.1.tgz", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="],
"side-channel-map": ["side-channel-map@1.0.1", "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
"stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="],
"statuses": ["statuses@2.0.2", "https://registry.npmmirror.com/statuses/-/statuses-2.0.2.tgz", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
"text-hex": ["text-hex@1.0.0", "", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="],
"toidentifier": ["toidentifier@1.0.1", "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
"triple-beam": ["triple-beam@1.4.1", "", {}, "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg=="],
"tsx": ["tsx@4.22.4", "https://registry.npmmirror.com/tsx/-/tsx-4.22.4.tgz", { "dependencies": { "esbuild": "~0.28.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg=="],
"type-is": ["type-is@2.1.0", "https://registry.npmmirror.com/type-is/-/type-is-2.1.0.tgz", { "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="],
"typescript": ["typescript@5.9.3", "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici-types": ["undici-types@6.21.0", "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
"unpipe": ["unpipe@1.0.0", "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"vary": ["vary@1.1.2", "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
"which": ["which@2.0.2", "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"winston": ["winston@3.19.0", "", { "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.8", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.7.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.9.0" } }, "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA=="],
"winston-transport": ["winston-transport@4.9.0", "", { "dependencies": { "logform": "^2.7.0", "readable-stream": "^3.6.2", "triple-beam": "^1.3.0" } }, "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A=="],
"wrappy": ["wrappy@1.0.2", "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
"zod": ["zod@3.25.76", "https://registry.npmmirror.com/zod/-/zod-3.25.76.tgz", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"zod-to-json-schema": ["zod-to-json-schema@3.25.2", "https://registry.npmmirror.com/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="],
"type-is/content-type": ["content-type@2.0.0", "https://registry.npmmirror.com/content-type/-/content-type-2.0.0.tgz", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="],
}
}
File diff suppressed because it is too large Load Diff
+42
View File
@@ -0,0 +1,42 @@
{
"name": "@basketikun/canvas-agent",
"version": "0.2.0",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"bin": {
"canvas-agent": "./dist/index.js"
},
"files": [
"dist",
"agent-instructions.md",
"README.md"
],
"scripts": {
"dev": "tsx src/index.ts",
"debug": "tsx src/index.ts --debug",
"test": "tsx --test src/canvas/session.test.ts",
"build": "tsc -p tsconfig.json",
"start": "node dist/index.js",
"prepack": "npm run build"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.12.1",
"@openai/codex": "0.145.0",
"express": "^5.1.0",
"winston": "^3.19.0",
"zod": "^3.25.0"
},
"devDependencies": {
"@types/express": "^5.0.0",
"@types/node": "^22.0.0",
"tsx": "^4.19.0",
"typescript": "^5.8.0"
},
"engines": {
"node": ">=18"
},
"publishConfig": {
"access": "public"
}
}
+48
View File
@@ -0,0 +1,48 @@
import { spawn } from "node:child_process";
import { AGENT_PROMPT } from "../config.js";
import { errorMessage } from "../utils/value.js";
import type { AgentEmit } from "./types.js";
/** 使用 Claude CLI 执行一次带 Canvas Agent 工具的任务。 */
export function runClaudeTurn(prompt: string, emit: AgentEmit) {
const fullPrompt = withAgentPrompt(prompt);
if (!fullPrompt) return;
const child = spawnAgent("claude", ["-p", "--output-format", "stream-json", "--verbose", "--include-partial-messages", "--allowedTools", "mcp__infinite-canvas__*", fullPrompt], emit);
if (child) pipeJsonLines(child, emit, "claude");
}
/** 为 Claude CLI 请求拼接 Canvas Agent 指令。 */
function withAgentPrompt(prompt: string) {
return prompt.trim() ? `${AGENT_PROMPT}\n\n用户请求:${prompt}` : "";
}
/** 将 Claude CLI 的 JSON Lines 输出转换为 Agent 事件。 */
function pipeJsonLines(child: ReturnType<typeof spawn>, emit: AgentEmit, agent: string) {
let out = "";
child.stdout?.on("data", (chunk) => {
out += chunk.toString();
const lines = out.split(/\r?\n/);
out = lines.pop() || "";
lines.filter(Boolean).forEach((line) => {
try {
emit("agent_event", { agent, ...JSON.parse(line) });
} catch {
emit("agent_event", { agent, type: "raw", text: line });
}
});
});
child.stderr?.on("data", (chunk) => emit("agent_log", { text: chunk.toString() }));
child.on("error", (error) => emit("agent_error", { message: error.message }));
child.on("close", (code) => emit("agent_done", { agent, code }));
}
/** 启动外部 Agent CLI,并将同步启动异常转换为事件。 */
function spawnAgent(name: string, args: string[], emit: AgentEmit) {
try {
return spawn(name, args, { stdio: ["ignore", "pipe", "pipe"], shell: process.platform === "win32", windowsHide: true });
} catch (error) {
emit("agent_error", { message: errorMessage(error) });
return null;
}
}
+426
View File
@@ -0,0 +1,426 @@
import { spawn, type ChildProcess } from "node:child_process";
import { createRequire } from "node:module";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { VERSION } from "../config.js";
import { logger } from "../utils/logger.js";
import { field, type JsonRecord } from "../utils/value.js";
import type { CodexNotificationParams, CodexPlanUpdate, CodexRequestMethod, CodexRequestParams, CodexRequestResult, CodexTurnInput } from "./codex-protocol.js";
import type { AgentEmit, AgentPermissionMode } from "./types.js";
type AgentEvent = JsonRecord & { type: string; usage?: unknown };
type PendingRequest = { resolve: (value: unknown) => void; reject: (error: Error) => void };
type ItemDeltaParams = { threadId: string; turnId: string; itemId: string; delta: string };
type PendingDelta = { delta: string; itemType: string; params: ItemDeltaParams; timer: ReturnType<typeof setTimeout> };
const canvasAgentMcp = canvasAgentMcpCommand();
const require = createRequire(import.meta.url);
const STREAM_UPDATE_INTERVAL_MS = 40;
/** 封装 Codex app-server 的 JSON-RPC 通信与事件转换。 */
export class CodexAppClient {
private nextId = 1;
private buffer = "";
private currentThreadId = "";
private currentTurnId = "";
private textByItem = new Map<string, string>();
private lastUsage: unknown = null;
private pending = new Map<number, PendingRequest>();
private activeTurns = new Map<string, PendingRequest>();
private completedTurns = new Map<string, Error | null>();
private pendingDeltas = new Map<string, PendingDelta>();
private plansByTurn = new Map<string, CodexPlanUpdate>();
private approvalRequests = new Map<string, { id: number; method: string; params: JsonRecord }>();
/** 保存 app-server 子进程和事件出口。 */
private constructor(private child: ChildProcess, private emit: AgentEmit) {}
/** 启动并初始化 Codex app-server。 */
static async start(emit: AgentEmit, onExit: () => void) {
logger.info("Starting Codex app-server", { executable: process.execPath, codex: codexBin() });
const child = spawn(process.execPath, [codexBin(), "app-server", "--stdio"], { stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
const client = new CodexAppClient(child, emit);
child.stdout?.on("data", (chunk) => client.read(chunk.toString()));
child.stderr?.on("data", (chunk) => {
const text = chunk.toString();
logger.warn("Codex app-server stderr", { text });
emit("agent_log", { text });
});
child.on("error", (error) => {
logger.error("Codex app-server process error", error);
emit("agent_error", { message: error.message });
});
child.on("exit", (code) => {
logger.warn("Codex app-server exited", { code });
client.failAll(`Codex app-server exited: ${code ?? 0}`);
onExit();
emit("agent_log", { text: `Codex app-server exited: ${code ?? 0}` });
});
await client.request("initialize", { clientInfo: { name: "canvas-agent", title: "Infinite Canvas Agent", version: VERSION }, capabilities: { experimentalApi: true, requestAttestation: false } });
client.notify("initialized");
return client;
}
/** 创建新的 Codex 线程。 */
async startThread(cwd?: string, permissionMode: AgentPermissionMode = "request") {
const { thread } = await this.request("thread/start", { ...threadSettings(permissionMode), ...(cwd ? { cwd } : {}), threadSource: "user" });
if (!thread.id) throw new Error("Codex app-server 没有返回 thread id");
return thread;
}
/** 恢复已有 Codex 线程。 */
async resumeThread(threadId: string, cwd?: string, permissionMode: AgentPermissionMode = "request") {
const { thread } = await this.request("thread/resume", { threadId, ...threadSettings(permissionMode), ...(cwd ? { cwd } : {}) });
if (!thread.id) throw new Error("Codex app-server 没有返回 thread id");
return thread;
}
/** 查询 Codex 线程列表。 */
listThreads(params: CodexRequestParams<"thread/list">) {
return this.request("thread/list", params);
}
/** 读取指定 Codex 线程。 */
readThread(threadId: string, includeTurns = true) {
return this.request("thread/read", { threadId, includeTurns });
}
/** 归档指定 Codex 线程。 */
archiveThread(threadId: string) {
return this.request("thread/archive", { threadId });
}
/** 返回指定线程在当前进程中收到的最新任务计划。 */
planUpdates(threadId: string) {
return [...this.plansByTurn.values()].filter((item) => item.threadId === threadId);
}
/** 清理已归档线程的任务计划缓存。 */
clearPlanUpdates(threadId: string) {
this.plansByTurn.forEach((item, turnId) => {
if (item.threadId === threadId) this.plansByTurn.delete(turnId);
});
}
/** 启动一个 Codex turn 并等待完成通知。 */
async startTurn(threadId: string, prompt: string, images: string[], permissionMode: AgentPermissionMode, onTurn?: (turnId: string) => void) {
this.currentThreadId = threadId;
const { turn } = await this.request("turn/start", { threadId, input: codexInput(prompt, images), ...turnSettings(permissionMode) });
const turnId = turn.id;
if (!turnId) throw new Error("Codex app-server 没有返回 turn id");
this.currentTurnId = turnId;
onTurn?.(turnId);
const completed = this.completedTurns.get(turnId);
if (this.completedTurns.has(turnId)) {
this.completedTurns.delete(turnId);
this.currentThreadId = "";
this.currentTurnId = "";
if (completed) throw completed;
return;
}
await new Promise((resolve, reject) => this.activeTurns.set(turnId, { resolve, reject }));
}
/** 中断当前正在运行的 Codex turn。 */
async interruptCurrentTurn() {
const threadId = this.currentThreadId;
const turnId = this.currentTurnId;
if (!threadId || !turnId) return false;
try {
logger.warn("Interrupting active Codex turn", { threadId, turnId });
await this.request("turn/interrupt", { threadId, turnId });
return true;
} catch (error) {
logger.warn("Failed to interrupt Codex turn", { error, threadId, turnId });
return false;
}
}
/** 回复网页端已经确认的 Codex 权限请求。 */
resolveApproval(requestId: string, decision: string) {
const request = this.approvalRequests.get(requestId);
if (!request) return false;
this.approvalRequests.delete(requestId);
const permissions = field(request.params, "permissions") || field(request.params, "requestedPermissions");
const result = request.method === "item/permissions/requestApproval"
? { permissions: decision === "decline" ? {} : permissions || {}, scope: decision === "acceptForSession" ? "session" : "turn" }
: { decision };
this.write({ id: request.id, result });
return true;
}
/** 发送 JSON-RPC 请求并保存待处理 Promise。 */
private request<Method extends CodexRequestMethod>(method: Method, params: CodexRequestParams<Method>) {
const id = this.nextId++;
this.write({ id, method, params });
return new Promise<CodexRequestResult<Method>>((resolve, reject) => this.pending.set(id, { resolve: (result) => resolve(result as CodexRequestResult<Method>), reject }));
}
/** 发送无需响应的 JSON-RPC 通知。 */
private notify(method: string, params?: unknown) {
this.write(params === undefined ? { method } : { method, params });
}
/** 将 JSON-RPC 消息写入 app-server 标准输入。 */
private write(value: unknown) {
const method = String(field(value, "method") || "");
const params = field(value, "params");
if (method) logger.debug(`Codex ${method}`, { id: field(value, "id"), threadId: field(params, "threadId") });
this.child.stdin?.write(`${JSON.stringify(value)}\n`);
}
/** 按行解析 app-server 标准输出。 */
private read(chunk: string) {
this.buffer += chunk;
const lines = this.buffer.split(/\r?\n/);
this.buffer = lines.pop() || "";
lines.filter(Boolean).forEach((line) => {
try {
this.handle(JSON.parse(line) as JsonRecord);
} catch (error) {
logger.warn("Invalid Codex app-server output", { error, line });
this.emit("agent_log", { text: line });
}
});
}
/** 分派单条 JSON-RPC 响应、请求或通知。 */
private handle(message: JsonRecord) {
const id = Number(message.id);
if (message.error && this.pending.has(id)) {
const error = String(field(message.error, "message") || "Codex request failed");
if (/not materialized yet.*includeTurns/i.test(error)) logger.debug("Codex thread has no messages yet", { id });
else logger.warn("Codex request failed", { id, error });
return this.reject(id, error);
}
if (this.pending.has(id)) return this.resolve(id, message.result);
if (typeof message.method === "string" && "id" in message) return this.answerServerRequest(message);
if (typeof message.method === "string") this.handleNotification(message.method, (message.params || {}) as JsonRecord);
}
/** 转换并广播 app-server 通知。 */
private handleNotification(method: string, params: JsonRecord) {
if (method === "serverRequest/resolved") {
const requestId = String(field(params, "requestId") || "");
if (requestId) this.approvalRequests.delete(requestId);
this.emit("codex_approval_resolved", { requestId, ...params });
return;
}
if (!field(params, "threadId") && this.currentThreadId && (method === "turn/started" || method === "turn/completed" || method === "turn/plan/updated")) params = { ...params, threadId: this.currentThreadId };
if (method === "item/agentMessage/delta") {
const value = params as unknown as CodexNotificationParams<"item/agentMessage/delta">;
this.textByItem.set(value.itemId, `${this.textByItem.get(value.itemId) || ""}${value.delta}`);
return this.emitDelta("agent_message", value);
}
if (method === "item/plan/delta") return this.emitDelta("plan", params as unknown as CodexNotificationParams<"item/plan/delta">);
if (method === "item/reasoning/summaryTextDelta") return this.emitDelta("reasoning", params as unknown as CodexNotificationParams<"item/reasoning/summaryTextDelta">);
if (method === "item/commandExecution/outputDelta") return this.emitDelta("command_execution", params as unknown as CodexNotificationParams<"item/commandExecution/outputDelta">);
if (method === "turn/plan/updated") {
const value = params as unknown as CodexNotificationParams<"turn/plan/updated">;
const update: CodexPlanUpdate = { ...value, threadId: value.threadId || "" };
if (update.threadId && update.turnId) this.plansByTurn.set(update.turnId, update);
params = update as unknown as JsonRecord;
}
if (method === "thread/tokenUsage/updated") {
this.lastUsage = normalizeUsage(params as unknown as CodexNotificationParams<"thread/tokenUsage/updated">);
this.emit("agent_event", { agent: "codex", type: "usage.updated", usage: this.lastUsage, ...codexEventScope(params) });
return;
}
const event = normalizeCodexNotification(method, params);
if (!event) return;
if (event.type === "item.completed") {
const item = field(event, "item") as JsonRecord | undefined;
const id = String(field(item, "id") || "");
this.flushDelta(id);
const streamedText = this.textByItem.get(id);
if (item?.type === "agent_message" && streamedText && !item.text) item.text = streamedText;
if (id) this.textByItem.delete(id);
}
if (event.type === "turn.completed") {
const turn = field(params, "turn");
const turnId = String(field(turn, "id") || field(params, "turnId") || "");
const plan = this.plansByTurn.get(turnId);
if (plan) this.plansByTurn.set(turnId, { ...plan, turnStatus: String(field(turn, "status") || "completed") });
}
if (event.type === "turn.completed") event.usage = this.lastUsage;
this.emit("agent_event", { agent: "codex", ...event });
if (event.type === "turn.completed") {
const turn = (params as unknown as CodexNotificationParams<"turn/completed">).turn;
const turnId = turn.id;
const pending = this.activeTurns.get(turnId);
const error = turn.error;
if (pending) {
this.activeTurns.delete(turnId);
error ? pending.reject(new Error(error.message || "Codex turn failed")) : pending.resolve(event);
} else if (turnId) {
this.completedTurns.set(turnId, error ? new Error(error.message || "Codex turn failed") : null);
}
if (turnId === this.currentTurnId) {
this.currentThreadId = "";
this.currentTurnId = "";
}
this.emit("agent_done", { agent: "codex", usage: event.usage, ...codexEventScope(params) });
}
}
/** 合并并广播 Agent 文本或执行输出增量。 */
private emitDelta(itemType: string, params: ItemDeltaParams) {
const id = params.itemId;
const pending = this.pendingDeltas.get(id);
if (pending) {
pending.delta += params.delta;
pending.itemType = itemType;
pending.params = params;
return;
}
this.pendingDeltas.set(id, {
delta: params.delta,
itemType,
params,
timer: setTimeout(() => this.flushDelta(id), STREAM_UPDATE_INTERVAL_MS),
});
}
/** 合并短时间内的文本增量,减少 SSE 传输和前端渲染次数。 */
private flushDelta(id: string) {
const pending = this.pendingDeltas.get(id);
if (!pending) return;
clearTimeout(pending.timer);
this.pendingDeltas.delete(id);
if (pending.delta) this.emit("agent_event", { agent: "codex", type: "item.updated", item: { id, type: pending.itemType, delta: pending.delta }, ...codexEventScope(pending.params as unknown as JsonRecord) });
}
/** 自动回复 app-server 发起的授权或交互请求。 */
private answerServerRequest(message: JsonRecord) {
const method = String(message.method);
const params = (field(message, "params") as JsonRecord) || {};
if (["item/commandExecution/requestApproval", "item/fileChange/requestApproval", "item/permissions/requestApproval"].includes(method)) {
const requestId = String(message.id);
this.approvalRequests.set(requestId, { id: Number(message.id), method, params });
this.emit("codex_approval", { requestId, method, ...params });
return;
}
const result = method === "mcpServer/elicitation/request" ? { action: "accept", content: {}, _meta: null } : { decision: "decline" };
this.write({ id: message.id, result });
this.emit("agent_event", { agent: "codex", type: "server.request", method, params, result });
}
/** 完成指定 JSON-RPC 请求。 */
private resolve(id: number, result: unknown) {
const pending = this.pending.get(id);
if (pending) (this.pending.delete(id), pending.resolve(result));
}
/** 拒绝指定 JSON-RPC 请求。 */
private reject(id: number, message: string) {
const pending = this.pending.get(id);
if (pending) (this.pending.delete(id), pending.reject(new Error(message)));
}
/** 拒绝进程退出时仍未完成的请求与 turn。 */
private failAll(message: string) {
[...this.pending.values(), ...this.activeTurns.values()].forEach((item) => item.reject(new Error(message)));
this.pendingDeltas.forEach((item) => clearTimeout(item.timer));
this.pending.clear();
this.activeTurns.clear();
this.pendingDeltas.clear();
this.textByItem.clear();
this.approvalRequests.clear();
this.currentThreadId = "";
this.currentTurnId = "";
}
}
/** 生成 Codex 调用 Canvas Agent MCP 的启动命令。 */
function canvasAgentMcpCommand() {
const current = process.argv.find((arg) => /index\.(t|j)s$/.test(arg)) || "";
const entry = path.resolve(current || fileURLToPath(new URL("../index.js", import.meta.url)));
const tsx = path.join(path.dirname(entry), "..", "node_modules", "tsx", "dist", "cli.mjs");
return entry.endsWith(".ts") ? { command: process.execPath, args: [tsx, entry, "mcp"] } : { command: process.execPath, args: [entry, "mcp"] };
}
/** 生成 Codex app-server 使用的 MCP 配置。 */
function codexConfig(permissionMode: AgentPermissionMode) {
return { model_reasoning_summary: "auto", ...(permissionMode === "automatic" ? { approvals_reviewer: "auto_review" } : {}), mcp_servers: { "infinite-canvas": { command: canvasAgentMcp.command, args: canvasAgentMcp.args, default_tools_approval_mode: "approve", startup_timeout_sec: 20, tool_timeout_sec: 90 } } };
}
function threadSettings(permissionMode: AgentPermissionMode) {
return { approvalPolicy: permissionMode === "full" ? "never" as const : "on-request" as const, sandbox: permissionMode === "full" ? "danger-full-access" as const : "workspace-write" as const, config: codexConfig(permissionMode) };
}
function turnSettings(permissionMode: AgentPermissionMode) {
return {
approvalPolicy: permissionMode === "full" ? "never" as const : "on-request" as const,
sandboxPolicy: permissionMode === "full" ? { type: "dangerFullAccess" as const } : { type: "workspaceWrite" as const, networkAccess: false },
};
}
/** 将文本和本地图片转换为 Codex turn 输入。 */
function codexInput(prompt: string, images: string[]): CodexTurnInput[] {
return [{ type: "text", text: prompt, text_elements: [] }, ...images.map<CodexTurnInput>((file) => ({ type: "localImage", path: file }))];
}
/** 将 app-server 通知转换为前端使用的 Agent 事件。 */
function normalizeCodexNotification(method: string, params: JsonRecord): AgentEvent | null {
const scope = codexEventScope(params);
if (method === "thread/started") return { type: "thread.started", ...scope };
if (method === "turn/started") return { type: "turn.started", ...scope };
if (method === "turn/completed") return { type: "turn.completed", status: field(field(params, "turn"), "status"), error: field(field(params, "turn"), "error"), usage: null, duration_ms: field(field(params, "turn"), "durationMs"), ...scope };
if (method === "turn/plan/updated") return { type: "plan.updated", explanation: field(params, "explanation"), plan: field(params, "plan"), ...scope };
if (method === "item/started") return { type: "item.started", item: normalizeItem(field(params, "item")), ...scope };
if (method === "item/completed") return { type: "item.completed", item: normalizeItem(field(params, "item")), ...scope };
if (method === "error") return { type: "error", message: field(field(params, "error"), "message"), ...scope };
return null;
}
/** 提取 Codex 事件所属的线程和 turn。 */
function codexEventScope(params: JsonRecord) {
const threadId = String(field(params, "threadId") || field(field(params, "thread"), "id") || "");
const turnId = String(field(params, "turnId") || field(field(params, "turn"), "id") || "");
return { ...(threadId ? { thread_id: threadId } : {}), ...(turnId ? { turn_id: turnId } : {}) };
}
/** 统一 app-server item 的类型和参数格式。 */
function normalizeItem(item: unknown) {
const value = item && typeof item === "object" ? { ...(item as JsonRecord) } : {};
if (value.type === "agentMessage") value.type = "agent_message";
if (value.type === "mcpToolCall") value.type = "mcp_tool_call";
if (value.type === "commandExecution") value.type = "command_execution";
if (value.type === "fileChange") value.type = "file_change";
if (value.type === "dynamicToolCall") value.type = "dynamic_tool_call";
if (value.type === "collabToolCall") value.type = "collab_tool_call";
if (value.type === "webSearch") value.type = "web_search";
if (value.type === "imageView") value.type = "image_view";
if (value.type === "imageGeneration") value.type = "image_generation";
if (value.type === "contextCompaction") value.type = "context_compaction";
if (value.type === "agent_message" && typeof value.id === "string") value.text = String(value.text || "");
if ("arguments" in value) value.arguments = parseMaybeJson(value.arguments);
return value;
}
/** 将 Codex token usage 转换为前端字段。 */
function normalizeUsage(params: CodexNotificationParams<"thread/tokenUsage/updated">) {
const last = params.tokenUsage.last;
return {
input_tokens: last.inputTokens,
cached_input_tokens: last.cachedInputTokens,
output_tokens: last.outputTokens,
reasoning_output_tokens: last.reasoningOutputTokens,
};
}
/** 尝试将字符串解析为 JSON,失败时保留原值。 */
function parseMaybeJson(value: unknown) {
if (typeof value !== "string") return value;
try {
return JSON.parse(value);
} catch {
return value;
}
}
/** 定位当前依赖中 Codex CLI 的执行文件。 */
function codexBin() {
return path.join(path.dirname(require.resolve("@openai/codex/package.json")), "bin", "codex.js");
}
+293
View File
@@ -0,0 +1,293 @@
import { field } from "../utils/value.js";
import type { CodexPlanUpdate } from "./codex-protocol.js";
type AgentHistoryMessage = { id: string; role: "user" | "assistant" | "tool" | "error"; title?: string; text: string; detail?: unknown; streamId?: string };
/** 将 Codex 线程转换为列表展示所需的摘要。 */
export function summarizeCodexThread(thread: unknown) {
return {
id: String(field(thread, "id") || ""),
sessionId: String(field(thread, "sessionId") || ""),
preview: displayUserText(String(field(thread, "preview") || "")),
name: stringOrNull(field(thread, "name")),
cwd: String(field(thread, "cwd") || ""),
status: String(field(thread, "status") || ""),
source: field(thread, "source"),
threadSource: field(thread, "threadSource"),
createdAt: Number(field(thread, "createdAt") || 0),
updatedAt: Number(field(thread, "updatedAt") || 0),
};
}
/** 将 Codex turn items 转换为网页聊天历史。 */
export function threadMessages(thread: unknown, planUpdates: CodexPlanUpdate[] = []): AgentHistoryMessage[] {
const turns = arrayValue(field(thread, "turns"));
const plansByTurn = new Map(planUpdates.map((item) => [item.turnId, item]));
const messages: AgentHistoryMessage[] = [];
turns.forEach((turn, turnIndex) => {
const turnId = String(field(turn, "id") || turnIndex);
const turnError = String(field(field(turn, "error"), "message") || "").trim();
const planMessage = structuredPlanMessage(plansByTurn.get(turnId) || { threadId: "", turnId, explanation: stringOrNull(field(turn, "explanation")), plan: arrayValue(field(turn, "plan")) as CodexPlanUpdate["plan"], turnStatus: String(field(turn, "status") || "") });
let planAdded = false;
arrayValue(field(turn, "items")).forEach((item, itemIndex) => {
const type = String(field(item, "type") || "");
const id = String(field(item, "id") || `${turnIndex}-${itemIndex}`);
if (type === "userMessage") {
const text = displayUserText(userInputText(field(item, "content")));
if (text) messages.push({ id, role: "user", text });
if (planMessage && !planAdded) {
messages.push(planMessage);
planAdded = true;
}
}
if (type === "agentMessage") {
const text = String(field(item, "text") || "").trim();
if (text) messages.push({ id, role: "assistant", title: "Codex", text });
}
if (type === "mcpToolCall") {
const tool = String(field(item, "tool") || "工具调用");
const error = String(field(field(item, "error"), "message") || "");
const input = toolArguments(field(item, "arguments"));
messages.push({ id, role: "tool", title: toolName(tool), text: error || toolHistorySummary(tool, input), detail: toolHistoryDetail(tool, item, input, error) });
}
if (type === "commandExecution") {
const command = String(field(item, "command") || "").trim();
if (command) messages.push({ id, role: "tool", title: "执行命令", text: command, detail: commandDetail(item) });
}
if (type === "fileChange") {
const changes = arrayValue(field(item, "changes"));
messages.push({ id, role: "tool", title: "修改文件", text: fileChangeSummary(changes), detail: { kind: "file", status: field(item, "status"), files: changes.map((change) => ({ path: String(field(change, "path") || "未知文件"), action: changeKind(field(change, "kind")) })) } });
}
if (type === "reasoning") {
const text = readableText(field(item, "summary"));
if (text) messages.push({ id, role: "tool", title: "思考摘要", text, detail: { kind: "reasoning", status: "completed" } });
}
if (type === "plan") {
const text = String(field(item, "text") || "").trim();
if (text) messages.push({ id, role: "tool", title: "执行计划", text, detail: { kind: "plan", status: "completed" } });
}
if (type === "webSearch") messages.push({ id, role: "tool", title: "搜索资料", text: webSearchSummary(item), detail: { kind: "search", status: "completed", rows: webSearchRows(item) } });
if (type === "imageView") messages.push({ id, role: "tool", title: "查看图片", text: String(field(item, "path") || "已查看图片"), detail: { kind: "image", status: "completed" } });
if (type === "imageGeneration") messages.push({ id, role: "tool", title: "内置生图", text: String(field(item, "savedPath") || "图片生成完成"), detail: { kind: "image", status: field(item, "status"), savedPath: field(item, "savedPath") } });
if (type === "contextCompaction") messages.push({ id, role: "tool", title: "整理上下文", text: "已整理当前对话,继续处理任务", detail: { kind: "context", status: "completed" } });
if (type === "dynamicToolCall") messages.push({ id, role: "tool", title: "使用工具", text: "已完成工具操作", detail: { kind: "tool", status: field(item, "status") } });
if (type === "collabToolCall") messages.push({ id, role: "tool", title: "协作处理", text: "已完成协作任务", detail: { kind: "tool", status: field(item, "status") } });
});
if (planMessage && !planAdded) messages.push(planMessage);
if (turnError) {
const error = userFacingCodexError(turnError);
messages.push({ id: `error-${turnId}`, role: "error", title: error.title, text: error.text });
}
});
return messages.filter((item) => item.text).slice(-120);
}
/** 将结构化任务计划转换为聊天进度卡片。 */
function structuredPlanMessage(update: CodexPlanUpdate): AgentHistoryMessage | null {
const tasks = arrayValue(update.plan).flatMap((item) => {
const step = String(field(item, "step") || "").trim();
return step ? [{ step, status: String(field(item, "status") || "pending") }] : [];
});
if (!tasks.length) return null;
const completed = tasks.filter((item) => item.status === "completed").length;
return {
id: `plan-${update.turnId}`,
role: "tool",
title: "任务进度",
text: `已完成 ${completed}/${tasks.length}`,
detail: { kind: "todo", status: planStatus(tasks, update.turnStatus), tasks, explanation: update.explanation || "" },
};
}
/** 根据步骤和 turn 状态生成任务卡片状态。 */
function planStatus(tasks: Array<{ status: string }>, turnStatus?: string) {
if (turnStatus === "failed") return "failed";
if (turnStatus === "interrupted") return "interrupted";
if (tasks.every((item) => item.status === "completed")) return "completed";
return turnStatus === "completed" ? "finished" : "inProgress";
}
/** 将常见 Codex 错误转换为普通用户可理解的提示。 */
function userFacingCodexError(message: string) {
if (/selected model is at capacity/i.test(message)) return { title: "模型暂时繁忙", text: "当前选择的模型请求量过大,暂时无法处理。请稍后重试,或切换其他模型后再试。" };
return { title: "任务失败", text: message || "Codex 未能完成本次任务,请稍后重试。" };
}
/** 提取用户输入条目中的文本与附件占位信息。 */
function userInputText(content: unknown) {
return arrayValue(content)
.map((item) => {
const type = String(field(item, "type") || "");
if (type === "text") return String(field(item, "text") || "");
if (type === "image" || type === "localImage") return "图片附件";
if (type === "mention") return `@${String(field(item, "name") || "文件")}`;
return "";
})
.filter(Boolean)
.join("\n");
}
/** 移除用户消息中由旧流程拼接的 Agent 前置提示词。 */
function displayUserText(text: string) {
const value = text.trim();
const marker = "用户请求:";
const index = value.lastIndexOf(marker);
const prompt = index >= 0 ? value.slice(index + marker.length) : value;
return prompt.split("\n\n本轮可用图片附件(顺序与图片输入一致):", 1)[0].trim();
}
/** 将未知值转换为数组。 */
function arrayValue(value: unknown) {
return Array.isArray(value) ? value : [];
}
/** 将非空字符串保留为字符串,否则返回 null。 */
function stringOrNull(value: unknown) {
return typeof value === "string" && value.trim() ? value : null;
}
/** 生成命令执行的用户可读详情。 */
function commandDetail(item: unknown) {
const rows = [
textRow("工作目录", field(item, "cwd")),
textRow("退出状态", field(item, "exitCode")),
durationRow(field(item, "durationMs")),
].filter(Boolean);
return { kind: "command", status: field(item, "status"), rows, output: String(field(item, "aggregatedOutput") || "").trim() };
}
/** 生成 MCP 工具的用户可读详情。 */
function toolHistoryDetail(tool: string, item: unknown, input: unknown, error: string) {
return { kind: "tool", status: error ? "failed" : field(item, "status"), rows: toolInputRows(tool, input), ...(error ? { output: error } : {}) };
}
/** 生成 MCP 工具在对话中的结果摘要。 */
function toolHistorySummary(tool: string, input: unknown) {
if (tool === "site_navigate") return `已打开${routeName(String(field(input, "path") || "/"))}`;
if (tool === "canvas_list_projects") return "已读取画布列表";
if (tool === "canvas_get_state") return "已读取当前画布内容";
if (tool === "canvas_get_selection") return "已读取当前选中内容";
if (tool === "prompts_search") return `已搜索提示词“${String(field(input, "query") || "") || "全部"}`;
if (tool === "assets_list") return "已读取我的素材";
if (tool === "generation_get_status") return "已检查生成任务状态";
return `${toolName(tool)}已完成`;
}
/** 提取工具参数中适合普通用户查看的信息。 */
function toolInputRows(tool: string, input: unknown) {
if (tool === "site_navigate") return [textRow("目标页面", routeName(String(field(input, "path") || "/")))].filter(Boolean);
if (tool === "prompts_search") return [textRow("搜索内容", field(input, "query"))].filter(Boolean);
if (tool === "canvas_create_text_node") return [textRow("文本内容", field(input, "text"))].filter(Boolean);
if (tool === "canvas_apply_ops") return [textRow("操作数量", arrayValue(field(input, "ops")).length)].filter(Boolean);
return [];
}
/** 生成人类可读的文件变更摘要。 */
function fileChangeSummary(changes: unknown[]) {
if (!changes.length) return "已完成文件修改";
const names = changes.slice(0, 3).map((change) => String(field(change, "path") || "未知文件"));
if (changes.length === 1) return `${changeKind(field(changes[0], "kind"))}${names[0]}`;
return `涉及 ${changes.length} 个文件:${names.join("、")}${changes.length > names.length ? " 等" : ""}`;
}
/** 生成网页搜索摘要。 */
function webSearchSummary(item: unknown) {
const action = field(item, "action");
const type = String(field(action, "type") || "");
if (type === "openPage") return `打开网页:${String(field(action, "url") || "")}`;
if (type === "findInPage") return `在网页中查找“${String(field(action, "pattern") || "内容")}`;
return `搜索:${String(field(item, "query") || field(action, "query") || "相关资料")}`;
}
/** 生成网页搜索详情行。 */
function webSearchRows(item: unknown) {
const action = field(item, "action");
return [textRow("关键词", field(item, "query") || field(action, "query")), textRow("网页", field(action, "url"))].filter(Boolean);
}
/** 从 reasoning 结构中提取可读文本。 */
function readableText(value: unknown): string {
if (typeof value === "string") return value.trim();
if (Array.isArray(value)) return value.map(readableText).filter(Boolean).join("\n");
return readableText(field(value, "text"));
}
/** 将历史工具参数解析为对象。 */
function toolArguments(value: unknown) {
if (typeof value !== "string") return value;
try {
return JSON.parse(value) as unknown;
} catch {
return {};
}
}
/** 创建非空详情行。 */
function textRow(label: string, value: unknown) {
return value === undefined || value === null || value === "" ? null : { label, value: String(value) };
}
/** 创建命令耗时详情行。 */
function durationRow(value: unknown) {
const duration = Number(value || 0);
return duration > 0 ? { label: "耗时", value: `${(duration / 1000).toFixed(1)}` } : null;
}
/** 将文件变更类型转换为中文。 */
function changeKind(value: unknown) {
if (value === "add") return "新增";
if (value === "delete") return "删除";
return "修改";
}
/** 将站点路由转换为中文页面名称。 */
function routeName(path: string) {
if (path === "/") return "首页";
if (path === "/canvas") return "画布页面";
if (path.startsWith("/canvas/")) return "指定画布";
if (path.startsWith("/image")) return "生图工作台";
if (path.startsWith("/video")) return "视频工作台";
if (path.startsWith("/prompts")) return "提示词中心";
if (path.startsWith("/assets")) return "我的素材";
if (path.startsWith("/config")) return "配置页面";
return path;
}
/** 将 MCP 工具名称转换为聊天记录中的中文标题。 */
function toolName(name: string) {
if (name === "site_navigate") return "打开页面";
if (name === "canvas_list_projects") return "查看画布列表";
if (name === "canvas_apply_ops") return "画布操作";
if (name === "canvas_get_state") return "读取画布";
if (name === "canvas_get_selection") return "读取选区";
if (name === "canvas_export_snapshot") return "导出快照";
if (name === "canvas_create_node") return "创建节点";
if (name === "canvas_create_attachment_nodes") return "添加附件图片";
if (name === "canvas_create_text_node") return "创建文本";
if (name === "canvas_create_text_nodes") return "批量创建文本";
if (name === "canvas_create_config_node") return "创建生成配置";
if (name === "canvas_create_image_prompt_flow") return "创建生图流程";
if (name === "canvas_create_generation_flow") return "创建生成流程";
if (name === "canvas_generate_text") return "生成文本";
if (name === "canvas_generate_image") return "生成图片";
if (name === "canvas_generate_video") return "生成视频";
if (name === "canvas_generate_audio") return "生成音频";
if (name === "canvas_update_node") return "更新节点";
if (name === "canvas_update_node_text") return "更新文本";
if (name === "canvas_move_nodes") return "移动节点";
if (name === "canvas_resize_node") return "调整节点尺寸";
if (name === "canvas_delete_nodes") return "删除节点";
if (name === "canvas_connect_nodes") return "连接节点";
if (name === "canvas_select_nodes") return "选择节点";
if (name === "canvas_set_viewport") return "调整视口";
if (name === "canvas_run_generation") return "触发生成";
if (name === "workbench_image_get_config") return "读取生图设置";
if (name === "workbench_image_generate") return "在生图工作台生成";
if (name === "workbench_video_get_config") return "读取视频设置";
if (name === "workbench_video_generate") return "在视频工作台生成";
if (name === "prompts_search") return "搜索提示词";
if (name === "assets_list") return "查看我的素材";
if (name === "assets_add") return "添加到我的素材";
if (name === "generation_get_status") return "查看生成状态";
return "工具操作";
}
+93
View File
@@ -0,0 +1,93 @@
import type { JsonRecord } from "../utils/value.js";
export type CodexThread = JsonRecord & { id: string; cwd: string; turns?: CodexTurn[] };
export type CodexTurn = JsonRecord & { id: string; error?: CodexTurnError | null; durationMs?: number | null };
export type CodexTurnError = JsonRecord & { message: string };
export type CodexItem = JsonRecord & { id: string; type: string; text?: string };
export type CodexPlanStep = { step: string; status: "pending" | "inProgress" | "completed" };
export type CodexPlanUpdate = { threadId: string; turnId: string; explanation?: string | null; plan: CodexPlanStep[]; turnStatus?: string };
export type CodexTurnInput =
| { type: "text"; text: string; text_elements: [] }
| { type: "localImage"; path: string };
type ThreadOptions = {
approvalPolicy: "never" | "on-request";
sandbox: "workspace-write" | "danger-full-access";
config: JsonRecord;
cwd?: string;
};
type CodexRequestSpec = {
initialize: {
params: {
clientInfo: { name: string; title: string; version: string };
capabilities: { experimentalApi: boolean; requestAttestation: boolean };
};
result: JsonRecord;
};
"thread/start": {
params: ThreadOptions & { threadSource: "user" };
result: { thread: CodexThread };
};
"thread/resume": {
params: ThreadOptions & { threadId: string };
result: { thread: CodexThread };
};
"thread/list": {
params: {
limit: number;
sortKey: "updated_at";
sortDirection: "desc";
sourceKinds: Array<"cli" | "vscode" | "appServer" | "exec">;
cwd: string;
searchTerm?: string;
};
result: { data: CodexThread[]; nextCursor: string | null; backwardsCursor: string | null };
};
"thread/read": {
params: { threadId: string; includeTurns: boolean };
result: { thread: CodexThread };
};
"thread/archive": {
params: { threadId: string };
result: Record<string, never>;
};
"turn/start": {
params: { threadId: string; input: CodexTurnInput[]; approvalPolicy: "never" | "on-request"; sandboxPolicy: { type: "workspaceWrite"; networkAccess: boolean } | { type: "dangerFullAccess" } };
result: { turn: CodexTurn };
};
"turn/interrupt": {
params: { threadId: string; turnId: string };
result: Record<string, never>;
};
};
export type CodexRequestMethod = keyof CodexRequestSpec;
export type CodexRequestParams<Method extends CodexRequestMethod> = CodexRequestSpec[Method]["params"];
export type CodexRequestResult<Method extends CodexRequestMethod> = CodexRequestSpec[Method]["result"];
type TokenUsageBreakdown = {
inputTokens: number;
cachedInputTokens: number;
outputTokens: number;
reasoningOutputTokens: number;
};
type CodexNotificationSpec = {
"thread/started": { thread: CodexThread };
"turn/started": { threadId?: string; turn: CodexTurn };
"turn/completed": { threadId?: string; turn: CodexTurn };
"turn/plan/updated": { threadId?: string; turnId: string; explanation?: string | null; plan: CodexPlanStep[] };
"item/started": { threadId: string; turnId: string; item: CodexItem };
"item/completed": { threadId: string; turnId: string; item: CodexItem };
"item/agentMessage/delta": { threadId: string; turnId: string; itemId: string; delta: string };
"item/plan/delta": { threadId: string; turnId: string; itemId: string; delta: string };
"item/reasoning/summaryTextDelta": { threadId: string; turnId: string; itemId: string; delta: string; summaryIndex: number };
"item/commandExecution/outputDelta": { threadId: string; turnId: string; itemId: string; delta: string };
"thread/tokenUsage/updated": { threadId: string; turnId: string; tokenUsage: { last: TokenUsageBreakdown } };
error: { threadId: string; turnId: string; error: CodexTurnError; willRetry: boolean };
};
export type CodexNotificationMethod = keyof CodexNotificationSpec;
export type CodexNotificationParams<Method extends CodexNotificationMethod> = CodexNotificationSpec[Method];
+216
View File
@@ -0,0 +1,216 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { logger } from "../utils/logger.js";
import { errorMessage, field } from "../utils/value.js";
import { CodexAppClient } from "./codex-client.js";
import { summarizeCodexThread, threadMessages } from "./codex-history.js";
import type { AgentAttachment, AgentEmit, AgentPermissionMode } from "./types.js";
type CodexRunOptions = { threadId?: string; cwd?: string; permissionMode?: AgentPermissionMode; appEmit?: AgentEmit; onStart?: () => void; onThread?: (threadId: string) => void; onTurn?: (turnId: string) => void; onFinish?: () => void };
let codexQueue: Promise<unknown> = Promise.resolve();
let codexApp: CodexAppClient | null = null;
let codexAppStart: Promise<CodexAppClient> | null = null;
let codexThreadId = "";
const unmaterializedThreadIds = new Set<string>();
export { summarizeCodexThread } from "./codex-history.js";
/** 将 Codex turn 加入串行队列并等待执行完成。 */
export async function runCodexTurn(prompt: string, emit: AgentEmit, attachments: AgentAttachment[] = [], options: CodexRunOptions = {}) {
if (!prompt.trim()) return;
codexQueue = codexQueue.catch(() => undefined).then(() => runCodexTurnNow(prompt, emit, attachments, options));
await codexQueue;
}
/** 中断当前线程正在执行的 Codex turn。 */
export async function interruptCodexTurn(threadId?: string) {
if (!codexApp || (threadId && threadId !== codexThreadId)) return false;
return await codexApp.interruptCurrentTurn();
}
/** 回复当前 app-server 的待处理权限请求。 */
export async function resolveCodexApproval(requestId: string, decision: string) {
return Boolean(codexApp?.resolveApproval(requestId, decision));
}
/** 创建新的 Codex 线程并记录当前线程 ID。 */
export async function startCodexThread(emit: AgentEmit, cwd?: string, permissionMode: AgentPermissionMode = "request") {
const app = await getCodexApp(emit);
const thread = await app.startThread(cwd, permissionMode);
codexThreadId = String(field(thread, "id") || "");
if (codexThreadId) unmaterializedThreadIds.add(codexThreadId);
return thread;
}
/** 恢复指定 Codex 线程并返回聊天历史。 */
export async function resumeCodexThread(emit: AgentEmit, threadId: string, cwd?: string, permissionMode: AgentPermissionMode = "request") {
const app = await getCodexApp(emit);
await loadCodexThread(emit, threadId, cwd, false);
const thread = await app.resumeThread(threadId, cwd, permissionMode);
assertThreadWorkspace(thread, cwd);
codexThreadId = String(field(thread, "id") || threadId);
const historyThread = await loadCodexThread(emit, codexThreadId, cwd, true);
return { thread, messages: threadMessages(historyThread, app.planUpdates(threadId)) };
}
/** 查询当前工作空间中的 Codex 线程。 */
export async function listCodexThreads(emit: AgentEmit, options: { cwd: string; searchTerm?: string; limit?: number }) {
const app = await getCodexApp(emit);
const result = await app.listThreads({
limit: options.limit || 40,
sortKey: "updated_at",
sortDirection: "desc",
sourceKinds: ["cli", "vscode", "appServer", "exec"],
cwd: options.cwd,
...(options.searchTerm ? { searchTerm: options.searchTerm } : {}),
});
const data = Array.isArray(field(result, "data")) ? (field(result, "data") as unknown[]).map(summarizeCodexThread).filter((thread) => threadInWorkspace(thread, options.cwd)) : [];
return { data, nextCursor: field(result, "nextCursor") || null, backwardsCursor: field(result, "backwardsCursor") || null };
}
/** 读取指定 Codex 线程及其聊天历史。 */
export async function readCodexThread(emit: AgentEmit, threadId: string, cwd?: string) {
const app = await getCodexApp(emit);
let thread: unknown;
try {
thread = await loadCodexThread(emit, threadId, cwd, !unmaterializedThreadIds.has(threadId));
} catch (error) {
if (!/not materialized yet.*includeTurns/i.test(errorMessage(error))) throw error;
unmaterializedThreadIds.add(threadId);
thread = await loadCodexThread(emit, threadId, cwd, false);
}
return { thread: summarizeCodexThread(thread), messages: threadMessages(thread, app.planUpdates(threadId)) };
}
/** 确认指定 Codex 线程属于当前工作空间。 */
export async function verifyCodexThreadWorkspace(emit: AgentEmit, threadId: string, cwd: string) {
await loadCodexThread(emit, threadId, cwd, false);
}
/** 归档指定 Codex 线程。 */
export async function archiveCodexThread(emit: AgentEmit, threadId: string, cwd?: string) {
const app = await getCodexApp(emit);
await loadCodexThread(emit, threadId, cwd, false);
await app.archiveThread(threadId);
app.clearPlanUpdates(threadId);
unmaterializedThreadIds.delete(threadId);
}
/** 判断线程异常是否允许自动新建线程后重试。 */
export function isRecoverableThreadError(error: unknown) {
return /thread not loaded|no rollout found/i.test(errorMessage(error));
}
/** 执行一次 Codex turn,并负责附件临时文件和线程恢复。 */
async function runCodexTurnNow(prompt: string, emit: AgentEmit, attachments: AgentAttachment[], options: CodexRunOptions) {
let files: string[] = [];
try {
options.onStart?.();
files = await writeAttachmentFiles(attachments);
const app = await getCodexApp(options.appEmit || emit);
let threadId = await ensureCodexThread(app, options, emit);
options.onThread?.(threadId);
unmaterializedThreadIds.delete(threadId);
try {
await app.startTurn(threadId, prompt, files, options.permissionMode || "request", options.onTurn);
} catch (error) {
if (!isRecoverableThreadError(error)) throw error;
emit("agent_log", { text: `Codex thread unavailable, starting a new thread: ${errorMessage(error)}` });
codexThreadId = "";
threadId = await ensureCodexThread(app, { cwd: options.cwd }, emit);
options.onThread?.(threadId);
unmaterializedThreadIds.delete(threadId);
await app.startTurn(threadId, prompt, files, options.permissionMode || "request", options.onTurn);
}
} catch (error) {
logger.error("Codex turn failed", error);
emit("agent_error", { message: errorMessage(error) });
} finally {
options.onFinish?.();
await Promise.all(files.map((file) => fs.unlink(file).catch(() => undefined)));
}
}
/** 恢复请求线程或创建新的 Codex 线程。 */
async function ensureCodexThread(app: CodexAppClient, options: CodexRunOptions, emit: AgentEmit) {
if (options.threadId) {
if (options.threadId === codexThreadId) return codexThreadId;
try {
const result = await app.readThread(options.threadId, false);
assertThreadWorkspace(field(result, "thread") || {}, options.cwd);
const thread = await app.resumeThread(options.threadId, options.cwd, options.permissionMode || "request");
assertThreadWorkspace(thread, options.cwd);
codexThreadId = String(field(thread, "id") || options.threadId);
return codexThreadId;
} catch (error) {
if (!isRecoverableThreadError(error)) throw error;
emit("agent_log", { text: `Codex thread unavailable, starting a new thread: ${errorMessage(error)}` });
}
}
if (!codexThreadId) {
const thread = await app.startThread(options.cwd, options.permissionMode || "request");
codexThreadId = String(field(thread, "id") || "");
if (codexThreadId) unmaterializedThreadIds.add(codexThreadId);
}
return codexThreadId;
}
/** 从 app-server 读取线程并校验工作空间。 */
async function loadCodexThread(emit: AgentEmit, threadId: string, cwd: string | undefined, includeTurns: boolean) {
const app = await getCodexApp(emit);
const result = await app.readThread(threadId, includeTurns);
const thread = field(result, "thread") || {};
assertThreadWorkspace(thread, cwd);
return thread;
}
/** 获取已启动的 Codex app-server 客户端。 */
async function getCodexApp(emit: AgentEmit) {
if (codexApp) return codexApp;
codexAppStart ||= CodexAppClient.start(emit, () => {
codexApp = null;
codexThreadId = "";
});
try {
codexApp = await codexAppStart;
return codexApp;
} finally {
codexAppStart = null;
}
}
/** 校验线程是否属于指定工作空间。 */
function assertThreadWorkspace(thread: unknown, cwd?: string) {
if (!cwd || threadInWorkspace(thread, cwd)) return;
throw new Error("该 Codex 会话不属于当前画布工作空间");
}
/** 判断线程工作目录是否与当前工作空间一致。 */
function threadInWorkspace(thread: unknown, cwd: string) {
const threadCwd = String(field(thread, "cwd") || "");
return Boolean(threadCwd && path.resolve(threadCwd) === path.resolve(cwd));
}
/** 将图片附件写入临时文件供 Codex 读取。 */
async function writeAttachmentFiles(attachments: AgentAttachment[]) {
return await Promise.all(attachments.filter((item) => item.dataUrl?.startsWith("data:image/")).map(writeAttachmentFile));
}
/** 将单个 Data URL 图片附件写入临时文件。 */
async function writeAttachmentFile(item: AgentAttachment) {
const [, meta = "", data = ""] = item.dataUrl?.match(/^data:([^;]+);base64,(.+)$/) || [];
if (!data) throw new Error(`图片附件无效:${item.name || "未命名图片"}`);
const file = path.join(os.tmpdir(), `infinite-canvas-${Date.now()}-${Math.random().toString(16).slice(2)}.${imageExt(meta || item.type)}`);
await fs.writeFile(file, Buffer.from(data, "base64"));
return file;
}
/** 根据图片 MIME 类型返回临时文件扩展名。 */
function imageExt(type = "") {
if (type.includes("png")) return "png";
if (type.includes("webp")) return "webp";
return "jpg";
}
+8
View File
@@ -0,0 +1,8 @@
/** Agent 向网页广播事件的函数类型。 */
export type AgentEmit = (type: string, payload: unknown) => void;
/** 用户随当前 Agent 消息上传的附件。 */
export type AgentAttachment = { id?: string; name?: string; type?: string; size?: number; width?: number; height?: number; dataUrl?: string };
/** Codex 文件、命令和网络权限模式。 */
export type AgentPermissionMode = "request" | "automatic" | "full";
+168
View File
@@ -0,0 +1,168 @@
import crypto from "node:crypto";
import type { ToolName } from "./schemas.js";
import { nextCanvasX } from "./tools.js";
import type { CanvasNode, CanvasNodeType, CanvasSnapshot } from "./types.js";
export type CanvasToolRequest = { name: "canvas_apply_ops"; input: Record<string, unknown> };
/** 将上层画布工具调用转换为前端可执行的批量操作。 */
export function buildCanvasToolRequest(name: ToolName, input: Record<string, unknown>, state: CanvasSnapshot | null): CanvasToolRequest {
if (name === "canvas_apply_ops") return { name, input };
if (name === "canvas_create_node") {
const data = input as { nodeType: CanvasNodeType; title?: string; x?: number; y?: number; width?: number; height?: number; metadata?: Record<string, unknown> };
return applyOps([{ type: "add_node", nodeType: data.nodeType, title: data.title, position: { x: data.x ?? nextCanvasX(state), y: data.y ?? 0 }, width: data.width, height: data.height, metadata: data.metadata }]);
}
if (name === "canvas_create_text_node") {
const data = input as { text?: string; x?: number; y?: number; title?: string; width?: number; height?: number };
return applyOps([textNodeOp(data, data.x ?? nextCanvasX(state), data.y ?? 0)]);
}
if (name === "canvas_create_text_nodes") {
const data = input as { items: Array<{ text: string; title?: string; x?: number; y?: number; width?: number; height?: number }>; x?: number; y?: number; gap?: number; direction?: "row" | "column" };
const x = Number(data.x ?? nextCanvasX(state));
const y = Number(data.y ?? 0);
const gap = Number(data.gap ?? 40);
return applyOps(data.items.map((item, index) => textNodeOp(item, item.x ?? (data.direction === "row" ? x + index * (340 + gap) : x), item.y ?? (data.direction === "row" ? y : y + index * (240 + gap)))));
}
if (name === "canvas_create_image_prompt_flow") return applyOps(generationFlowOps({ ...input, mode: "image" }, state));
if (name === "canvas_create_config_node") {
const x = Number(input.x ?? nextCanvasX(state));
const y = Number(input.y ?? 0);
const configId = `config-${crypto.randomUUID()}`;
const mode = generationMode(input.mode);
const prompt = String(input.prompt || "");
return applyOps([configNodeOp(configId, input, x, y), ...(input.autoRun ? [runGenerationOp(configId, mode, prompt)] : [])]);
}
if (name === "canvas_create_generation_flow") return applyOps(generationFlowOps(input, state));
if (name === "canvas_generate_text" || name === "canvas_generate_image" || name === "canvas_generate_video" || name === "canvas_generate_audio") {
return applyOps(generationFlowOps({ ...input, mode: name.replace("canvas_generate_", ""), autoRun: true }, state));
}
if (name === "canvas_update_node") {
const data = input as { id: string; patch?: Record<string, unknown>; metadata?: Record<string, unknown> };
return applyOps([{ type: "update_node", id: data.id, patch: data.patch, metadata: data.metadata }]);
}
if (name === "canvas_update_node_text") {
const data = input as { id: string; text: string; title?: string };
return applyOps([{ type: "update_node", id: data.id, patch: { ...(data.title ? { title: data.title } : {}) }, metadata: { content: data.text, status: "success" } }]);
}
if (name === "canvas_move_nodes") {
const data = input as { items: Array<{ id: string; x?: number; y?: number; dx?: number; dy?: number }> };
return applyOps(data.items.map((item) => {
const current = findNode(state, item.id);
return { type: "update_node", id: item.id, patch: { position: { x: item.x ?? ((current?.position.x || 0) + (item.dx || 0)), y: item.y ?? ((current?.position.y || 0) + (item.dy || 0)) } } };
}));
}
if (name === "canvas_resize_node") {
const data = input as { id: string; width: number; height: number; freeResize?: boolean };
return applyOps([{ type: "update_node", id: data.id, patch: { width: data.width, height: data.height }, metadata: data.freeResize === undefined ? undefined : { freeResize: data.freeResize } }]);
}
if (name === "canvas_delete_nodes") return applyOps([{ type: "delete_node", ids: (input as { ids: string[] }).ids }]);
if (name === "canvas_connect_nodes") {
const data = input as { connections: Array<{ fromNodeId: string; toNodeId: string }> };
return applyOps(data.connections.map((connection) => ({ type: "connect_nodes", ...connection })));
}
if (name === "canvas_select_nodes") return applyOps([{ type: "select_nodes", ids: (input as { ids: string[] }).ids }]);
if (name === "canvas_set_viewport") return applyOps([{ type: "set_viewport", viewport: (input as { viewport: unknown }).viewport }]);
if (name === "canvas_run_generation") {
const data = input as { nodeId: string; mode?: string; prompt?: string };
return applyOps([runGenerationOp(data.nodeId, generationMode(data.mode), data.prompt)]);
}
throw new Error(`未知工具:${name}`);
}
/** 按最大边限制计算附件图片节点尺寸,并保持原始比例。 */
export function fitAttachmentNodeSize(width: number, height: number) {
const scale = Math.min(1, 640 / width, 640 / height);
return { width: width * scale, height: height * scale };
}
/** 创建统一的批量画布操作请求。 */
function applyOps(ops: unknown[]): CanvasToolRequest {
return { name: "canvas_apply_ops", input: { ops } };
}
/** 创建文本节点操作。 */
function textNodeOp(input: { id?: string; text?: string; title?: string; width?: number; height?: number }, x: number, y: number) {
return { type: "add_node", id: input.id, nodeType: "text", title: input.title, position: { x, y }, width: input.width, height: input.height, metadata: { content: input.text || "", status: "success", fontSize: 14 } };
}
/** 创建生成配置节点操作。 */
function configNodeOp(id: string, input: Record<string, unknown>, x: number, y: number) {
const mode = generationMode(input.mode);
const prompt = String(input.prompt || "");
return {
type: "add_node",
id,
nodeType: "config",
title: String(input.title || generationTitle(mode)),
position: { x, y },
width: typeof input.width === "number" ? input.width : undefined,
height: typeof input.height === "number" ? input.height : undefined,
metadata: cleanRecord({
generationMode: mode,
composerContent: prompt,
prompt,
status: "idle",
model: input.model,
size: input.size,
quality: input.quality,
count: input.count,
seconds: input.seconds,
vquality: input.vquality,
generateAudio: input.generateAudio,
watermark: input.watermark,
audioVoice: input.audioVoice,
audioFormat: input.audioFormat,
audioSpeed: input.audioSpeed,
audioInstructions: input.audioInstructions,
}),
};
}
/** 创建包含提示词、配置节点和引用连线的生成流程。 */
function generationFlowOps(input: Record<string, unknown>, state: CanvasSnapshot | null) {
const mode = generationMode(input.mode);
const prompt = String(input.prompt || "");
const x = Number(input.x ?? nextCanvasX(state));
const y = Number(input.y ?? 0);
const textId = `text-${crypto.randomUUID()}`;
const configId = `config-${crypto.randomUUID()}`;
const referenceNodeIds = Array.isArray(input.referenceNodeIds) ? input.referenceNodeIds.filter((id): id is string => typeof id === "string") : [];
const tokens = [`@[node:${textId}]`, ...referenceNodeIds.map((id) => `@[node:${id}]`)];
return [
textNodeOp({ id: textId, text: prompt, title: String(input.title || "提示词") }, x, y),
configNodeOp(configId, { ...input, prompt: tokens.join("\n") }, x + 420, y),
{ type: "connect_nodes", fromNodeId: textId, toNodeId: configId },
...referenceNodeIds.map((fromNodeId) => ({ type: "connect_nodes", fromNodeId, toNodeId: configId })),
{ type: "select_nodes", ids: [configId] },
...(input.autoRun ? [runGenerationOp(configId, mode, tokens.join("\n"))] : []),
];
}
/** 创建触发节点生成的画布操作。 */
function runGenerationOp(nodeId: string, mode: "text" | "image" | "video" | "audio", prompt?: string) {
return { type: "run_generation", nodeId, mode, prompt };
}
/** 将未知生成模式归一为画布支持的模式。 */
function generationMode(value: unknown): "text" | "image" | "video" | "audio" {
return value === "text" || value === "video" || value === "audio" ? value : "image";
}
/** 获取生成模式对应的默认节点标题。 */
function generationTitle(mode: "text" | "image" | "video" | "audio") {
if (mode === "text") return "文本生成";
if (mode === "video") return "视频生成";
if (mode === "audio") return "音频生成";
return "图片生成";
}
/** 按节点 ID 查找当前画布节点。 */
function findNode(state: CanvasSnapshot | null, id: string): CanvasNode | undefined {
return (state?.nodes || []).find((node) => node.id === id);
}
/** 移除对象中未设置的生成参数。 */
function cleanRecord(value: Record<string, unknown>) {
return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined && item !== ""));
}
+163
View File
@@ -0,0 +1,163 @@
import { z } from "zod";
const recordSchema = z.record(z.unknown());
const positionSchema = z.object({ x: z.number(), y: z.number() });
const viewportSchema = z.object({ x: z.number(), y: z.number(), k: z.number() });
const nodeTypeSchema = z.enum(["image", "text", "config", "video", "audio"]);
const generationModeSchema = z.enum(["text", "image", "video", "audio"]);
/** Canvas Agent 对外提供的工具名称。 */
export const toolNames = [
"site_navigate",
"canvas_list_projects",
"canvas_get_state",
"canvas_get_selection",
"canvas_export_snapshot",
"canvas_apply_ops",
"canvas_create_node",
"canvas_create_attachment_nodes",
"canvas_create_text_node",
"canvas_create_text_nodes",
"canvas_create_config_node",
"canvas_create_image_prompt_flow",
"canvas_create_generation_flow",
"canvas_generate_text",
"canvas_generate_image",
"canvas_generate_video",
"canvas_generate_audio",
"canvas_update_node",
"canvas_update_node_text",
"canvas_move_nodes",
"canvas_resize_node",
"canvas_delete_nodes",
"canvas_connect_nodes",
"canvas_select_nodes",
"canvas_set_viewport",
"canvas_run_generation",
"generation_get_status",
"workbench_image_get_config",
"workbench_image_generate",
"workbench_video_get_config",
"workbench_video_generate",
"prompts_search",
"assets_list",
"assets_add",
] as const;
export type ToolName = (typeof toolNames)[number];
export const canvasOpSchema = z.discriminatedUnion("type", [
z.object({ type: z.literal("add_node"), nodeType: nodeTypeSchema.optional(), id: z.string().optional(), title: z.string().optional(), x: z.number().optional(), y: z.number().optional(), width: z.number().optional(), height: z.number().optional(), position: positionSchema.optional(), metadata: recordSchema.optional() }).passthrough(),
z.object({ type: z.literal("update_node"), id: z.string(), patch: recordSchema.optional(), metadata: recordSchema.optional() }).passthrough(),
z.object({ type: z.literal("delete_node"), id: z.string().optional(), ids: z.array(z.string()).optional() }).passthrough(),
z.object({ type: z.literal("delete_connections"), id: z.string().optional(), ids: z.array(z.string()).optional(), all: z.boolean().optional() }).passthrough(),
z.object({ type: z.literal("connect_nodes"), id: z.string().optional(), fromNodeId: z.string(), toNodeId: z.string() }).passthrough(),
z.object({ type: z.literal("set_viewport"), viewport: viewportSchema }).passthrough(),
z.object({ type: z.literal("select_nodes"), ids: z.array(z.string()) }).passthrough(),
z.object({ type: z.literal("run_generation"), nodeId: z.string(), mode: generationModeSchema.optional(), prompt: z.string().optional() }).passthrough(),
]);
const textNodeSchema = z.object({
text: z.string(),
title: z.string().optional(),
x: z.number().optional(),
y: z.number().optional(),
width: z.number().optional(),
height: z.number().optional(),
});
const generationOptionsSchema = z.object({
model: z.string().optional(),
size: z.string().optional(),
quality: z.string().optional(),
count: z.number().optional(),
seconds: z.string().optional(),
vquality: z.string().optional(),
generateAudio: z.string().optional(),
watermark: z.string().optional(),
audioVoice: z.string().optional(),
audioFormat: z.string().optional(),
audioSpeed: z.string().optional(),
audioInstructions: z.string().optional(),
});
const generationFlowSchema = z.object({
prompt: z.string(),
title: z.string().optional(),
x: z.number().optional(),
y: z.number().optional(),
referenceNodeIds: z.array(z.string()).optional(),
});
export const toolInputSchemas = {
site_navigate: z.object({ path: z.string() }),
canvas_list_projects: z.object({ keyword: z.string().optional(), page: z.number().optional(), pageSize: z.number().optional() }),
canvas_get_state: z.object({}).passthrough(),
canvas_get_selection: z.object({}).passthrough(),
canvas_export_snapshot: z.object({}).passthrough(),
canvas_apply_ops: z.object({ ops: z.array(canvasOpSchema) }),
canvas_create_node: z.object({ nodeType: nodeTypeSchema, title: z.string().optional(), x: z.number().optional(), y: z.number().optional(), width: z.number().optional(), height: z.number().optional(), metadata: recordSchema.optional() }),
canvas_create_attachment_nodes: z.object({ attachmentIds: z.array(z.string()).min(1), x: z.number().optional(), y: z.number().optional(), gap: z.number().optional(), direction: z.enum(["row", "column"]).optional() }),
canvas_create_text_node: z.object({ text: z.string().optional(), x: z.number().optional(), y: z.number().optional(), title: z.string().optional(), width: z.number().optional(), height: z.number().optional() }),
canvas_create_text_nodes: z.object({ items: z.array(textNodeSchema).min(1), x: z.number().optional(), y: z.number().optional(), gap: z.number().optional(), direction: z.enum(["row", "column"]).optional() }),
canvas_create_config_node: z.object({ prompt: z.string().optional(), mode: generationModeSchema.optional(), title: z.string().optional(), x: z.number().optional(), y: z.number().optional(), width: z.number().optional(), height: z.number().optional(), autoRun: z.boolean().optional() }).merge(generationOptionsSchema),
canvas_create_image_prompt_flow: z.object({ prompt: z.string(), x: z.number().optional(), y: z.number().optional(), autoRun: z.boolean().optional() }).merge(generationOptionsSchema),
canvas_create_generation_flow: generationFlowSchema.extend({ mode: generationModeSchema.optional(), autoRun: z.boolean().optional() }).merge(generationOptionsSchema),
canvas_generate_text: generationFlowSchema.merge(generationOptionsSchema),
canvas_generate_image: generationFlowSchema.merge(generationOptionsSchema),
canvas_generate_video: generationFlowSchema.merge(generationOptionsSchema),
canvas_generate_audio: generationFlowSchema.merge(generationOptionsSchema),
canvas_update_node: z.object({ id: z.string(), patch: recordSchema.optional(), metadata: recordSchema.optional() }),
canvas_update_node_text: z.object({ id: z.string(), text: z.string(), title: z.string().optional() }),
canvas_move_nodes: z.object({ items: z.array(z.object({ id: z.string(), x: z.number().optional(), y: z.number().optional(), dx: z.number().optional(), dy: z.number().optional() })).min(1) }),
canvas_resize_node: z.object({ id: z.string(), width: z.number(), height: z.number(), freeResize: z.boolean().optional() }),
canvas_delete_nodes: z.object({ ids: z.array(z.string()).min(1) }),
canvas_connect_nodes: z.object({ connections: z.array(z.object({ fromNodeId: z.string(), toNodeId: z.string() })).min(1) }),
canvas_select_nodes: z.object({ ids: z.array(z.string()) }),
canvas_set_viewport: z.object({ viewport: viewportSchema }),
canvas_run_generation: z.object({ nodeId: z.string(), mode: generationModeSchema.optional(), prompt: z.string().optional() }),
generation_get_status: z.object({ scope: z.enum(["all", "canvas", "image", "video"]).optional(), taskId: z.string().optional(), nodeIds: z.array(z.string()).optional(), limit: z.number().optional() }),
workbench_image_get_config: z.object({}).passthrough(),
workbench_image_generate: z.object({ prompt: z.string(), model: z.string().optional(), quality: z.string().optional(), size: z.string().optional(), count: z.number().optional(), run: z.boolean().optional() }),
workbench_video_get_config: z.object({}).passthrough(),
workbench_video_generate: z.object({ prompt: z.string(), model: z.string().optional(), size: z.string().optional(), seconds: z.string().optional(), resolution: z.string().optional(), generateAudio: z.boolean().optional(), watermark: z.boolean().optional(), run: z.boolean().optional() }),
prompts_search: z.object({ keyword: z.string().optional(), category: z.string().optional(), tags: z.array(z.string()).optional(), page: z.number().optional(), pageSize: z.number().optional() }),
assets_list: z.object({ kind: z.enum(["all", "text", "image", "video"]).optional(), keyword: z.string().optional(), page: z.number().optional(), pageSize: z.number().optional() }),
assets_add: z.object({ kind: z.enum(["text", "image"]), title: z.string(), content: z.string().optional(), imageUrl: z.string().optional(), tags: z.array(z.string()).optional(), source: z.string().optional(), note: z.string().optional() }),
} satisfies Record<ToolName, z.AnyZodObject>;
export const toolDescriptions: Record<ToolName, string> = {
site_navigate: "跳转网站页面。path 可为 / (首页)、/canvas (我的画布)、/canvas/:id (指定画布)、/image (生图工作台)、/video (视频创作台)、/prompts (提示词库)、/assets (我的素材)、/config (配置)。操作画布前若不在画布页,先用本工具打开画布。",
canvas_list_projects: "列出用户全部画布(仅标题、创建/更新时间、节点数、连线数,不含完整数据),支持 keyword 搜索和 page/pageSize 分页。返回的 id 可配合 site_navigate 跳转到 /canvas/:id 打开对应画布。",
canvas_get_state: "读取当前网页画布的节点、连线、选区和视口。",
canvas_get_selection: "读取当前网页画布选中的节点。",
canvas_export_snapshot: "导出当前画布快照,用于理解布局。",
canvas_apply_ops: "批量操作当前网页画布。ops 支持 add_node、update_node、delete_node、delete_connections、connect_nodes、set_viewport、select_nodes、run_generation。",
canvas_create_node: "创建任意类型节点:text、image、config、video、audio。适合创建占位图、媒体占位、配置节点或自定义 metadata 节点。",
canvas_create_attachment_nodes: "把当前对话中用户上传的图片附件创建成真实画布图片节点。attachmentIds 使用本轮附件清单中的 ID;返回的节点 ID 可传给 canvas_create_generation_flow.referenceNodeIds 作为生成参考图。",
canvas_create_text_node: "在当前画布创建单个文本节点。",
canvas_create_text_nodes: "批量创建文本节点,适合生成标题、段落、脚本、说明等内容块。",
canvas_create_config_node: "创建生成配置节点,可指定 text/image/video/audio 模式和生成参数,可选择立即触发生成。",
canvas_create_image_prompt_flow: "创建提示词文本节点和图片生成配置节点,并自动连线,可选择立即触发生图。",
canvas_create_generation_flow: "创建通用生成流程:提示词文本节点、生成配置节点、参考节点连线,可用于文案、生图、视频或音频。",
canvas_generate_text: "创建通用文本生成流程并立即触发生成。",
canvas_generate_image: "创建通用图片生成流程并立即触发生成。",
canvas_generate_video: "创建通用视频生成流程并立即触发生成。",
canvas_generate_audio: "创建通用音频生成流程并立即触发生成。",
canvas_update_node: "更新节点基础字段或 metadata。",
canvas_update_node_text: "更新文本节点内容和标题。",
canvas_move_nodes: "移动一个或多个节点,支持绝对坐标或 dx/dy 偏移。",
canvas_resize_node: "调整节点尺寸。",
canvas_delete_nodes: "删除指定节点及相关连线。",
canvas_connect_nodes: "批量连接节点。",
canvas_select_nodes: "设置当前选中节点。",
canvas_set_viewport: "调整画布视口。",
canvas_run_generation: "触发指定节点生成,通常用于配置节点或文本/图片/视频/音频节点。",
generation_get_status: "查询当前活动网页的生成任务状态。默认返回画布、生图工作台和视频工作台最近任务;可用 scope 过滤来源,用 taskId 查询工作台任务,用 nodeIds 查询画布节点。",
workbench_image_get_config: "读取生图工作台的当前参数和可选项(可用模型、质量、尺寸/宽高比、张数范围),在调用 workbench_image_generate 前先了解可选值。",
workbench_image_generate: "在生图工作台填入提示词并按需设置 model、quality、size(如 1:1 或 1024x1024)、countrun 默认 true 会自动点击生成按钮。会自动跳转到生图工作台。生成为异步过程,提交后返回 taskId,可用 generation_get_status 查询状态。",
workbench_video_get_config: "读取视频创作台的当前参数和可选项(可用模型、尺寸/比例、时长、清晰度/分辨率、是否生成声音与水印)。",
workbench_video_generate: "在视频创作台填入提示词并按需设置 model、size、seconds、resolution、generateAudio、watermarkrun 默认 true 会自动点击生成按钮。会自动跳转到视频创作台。生成为异步过程,提交后返回 taskId,可用 generation_get_status 查询状态。",
prompts_search: "搜索提示词库(第三方提示词合集),支持 keyword、category、tags 过滤和 page/pageSize 分页,返回标题、提示词、分类、标签、封面等。",
assets_list: "列出用户「我的素材」,支持 kindtext/image/video)过滤、keyword 搜索和 page/pageSize 分页。为控制体积不返回图片/视频原始 data,仅返回封面与元信息。",
assets_add: "向「我的素材」新增素材。kind=text 时用 content 传文本内容;kind=image 时用 imageUrl 传图片地址或 dataURL。可附带 title、tags、source、note。",
};
+297
View File
@@ -0,0 +1,297 @@
import { EventEmitter } from "node:events";
import type { ServerResponse } from "node:http";
import assert from "node:assert/strict";
import test from "node:test";
import { CanvasSession } from "./session.js";
test("MCP 读取当前激活网页的画布", async (t) => {
const session = new CanvasSession();
const first = connect(session, "first");
const second = connect(session, "second");
t.after(() => {
first.close();
second.close();
});
session.updateState(snapshot("canvas-first"), "first");
session.updateState(snapshot("canvas-second"), "second");
session.activateClient("first");
assert.equal(field(await session.callTool("canvas_get_state", {}), "projectId"), "canvas-first");
session.activateClient("second");
assert.equal(field(await session.callTool("canvas_get_state", {}), "projectId"), "canvas-second");
});
test("画布写操作只发送给当前激活网页", async (t) => {
const session = new CanvasSession();
const first = connect(session, "first");
const second = connect(session, "second");
t.after(() => {
first.close();
second.close();
});
session.updateState(snapshot("canvas-first"), "first");
session.updateState(snapshot("canvas-second"), "second");
session.activateClient("second");
const result = session.callTool("canvas_create_text_node", { text: "只写入第二个画布" });
const call = second.event("tool_call");
assert.equal(first.event("tool_call"), undefined);
assert.equal(field(call, "name"), "canvas_apply_ops");
session.resolveResult("second", { requestId: String(field(call, "requestId")), result: { ok: true } });
assert.deepEqual(await result, { ok: true });
});
test("当前 turn 的图片附件可在发起标签页画布创建图片节点", async (t) => {
const session = new CanvasSession();
const first = connect(session, "first");
t.after(() => first.close());
const dataUrl = "data:image/png;base64,aW1hZ2U=";
session.setTurnAttachments("first", [{ id: "attachment-1", name: "商品.png", type: "image/png", size: 5, width: 1200, height: 600, dataUrl }]);
session.bindClient("first");
const result = session.callTool("canvas_create_attachment_nodes", { attachmentIds: ["attachment-1"], x: 100, y: 200 });
const call = first.event("tool_call");
const input = field(call, "input") as Record<string, unknown>;
const nodes = input.nodes as Array<Record<string, unknown>>;
assert.equal(field(call, "name"), "canvas_create_attachment_nodes");
assert.equal(nodes.length, 1);
assert.equal(nodes[0].attachmentId, "attachment-1");
assert.equal(nodes[0].title, "商品.png");
assert.deepEqual(nodes[0].position, { x: 100, y: 200 });
assert.equal(nodes[0].width, 640);
assert.equal(nodes[0].height, 320);
assert.equal("dataUrl" in nodes[0], false);
assert.equal(session.getTurnAttachment("first", "attachment-1").dataUrl, dataUrl);
session.resolveResult("first", { requestId: String(field(call, "requestId")), result: { ok: true } });
const created = (await result) as { nodes: Array<{ id: string; attachmentId: string; title: string }> };
assert.equal(created.nodes[0].id, nodes[0].id);
assert.equal(created.nodes[0].attachmentId, "attachment-1");
session.clearTurnAttachments("first");
assert.throws(() => session.getTurnAttachment("first", "attachment-1"), /找不到/);
});
test("图片附件只允许发起 turn 的标签页读取和落入画布", async (t) => {
const session = new CanvasSession();
const first = connect(session, "first");
const second = connect(session, "second");
t.after(() => {
first.close();
second.close();
});
session.setTurnAttachments("first", [{ id: "attachment-1", name: "商品.png", type: "image/png", dataUrl: "data:image/png;base64,aW1hZ2U=" }]);
session.bindClient("second");
await assert.rejects(session.callTool("canvas_create_attachment_nodes", { attachmentIds: ["attachment-1"] }), /发起标签页/);
assert.throws(() => session.getTurnAttachment("second", "attachment-1"), /发起标签页/);
assert.equal(first.event("tool_call"), undefined);
assert.equal(second.event("tool_call"), undefined);
});
test("tool result is accepted only from the request client", async (t) => {
const session = new CanvasSession();
const first = connect(session, "first");
const second = connect(session, "second");
t.after(() => {
first.close();
second.close();
});
session.activateClient("first");
const result = session.callTool("canvas_create_text_node", { text: "first only" });
const call = first.event("tool_call");
const requestId = String(field(call, "requestId"));
assert.equal(session.resolveResult("second", { requestId, result: { client: "second" } }), false);
assert.equal(session.resolveResult("first", { requestId, result: { client: "first" } }), true);
assert.deepEqual(await result, { client: "first" });
});
test("生成状态查询由当前激活网页返回", async (t) => {
const session = new CanvasSession();
const first = connect(session, "first");
const second = connect(session, "second");
t.after(() => {
first.close();
second.close();
});
session.activateClient("second");
const result = session.callTool("generation_get_status", { scope: "all" });
const call = second.event("tool_call");
assert.equal(first.event("tool_call"), undefined);
assert.equal(field(call, "name"), "generation_get_status");
session.resolveResult("second", { requestId: String(field(call, "requestId")), result: { total: 1, tasks: [{ id: "image-1", status: "running" }] } });
assert.deepEqual(await result, { total: 1, tasks: [{ id: "image-1", status: "running" }] });
});
test("活动网页关闭后回退到仍连接的画布", async (t) => {
const session = new CanvasSession();
const first = connect(session, "first");
const second = connect(session, "second");
t.after(() => {
first.close();
second.close();
});
session.updateState(snapshot("canvas-first"), "first");
session.updateState(snapshot("canvas-second"), "second");
session.activateClient("second");
second.close();
assert.equal(field(await session.callTool("canvas_get_state", {}), "projectId"), "canvas-first");
});
test("closing the active client falls back to the most recently focused client", async (t) => {
const session = new CanvasSession();
const first = connect(session, "first");
const second = connect(session, "second");
const third = connect(session, "third");
t.after(() => {
first.close();
second.close();
third.close();
});
session.updateState(snapshot("canvas-first"), "first");
session.updateState(snapshot("canvas-second"), "second");
session.updateState(snapshot("canvas-third"), "third");
session.activateClient("third");
session.activateClient("second");
second.close();
assert.equal(field(await session.callTool("canvas_get_state", {}), "projectId"), "canvas-third");
});
test("closing a client rejects its pending tool requests", async () => {
const session = new CanvasSession();
const first = connect(session, "first");
const result = session.callTool("canvas_create_text_node", { text: "pending" });
const call = first.event("tool_call");
const requestId = String(field(call, "requestId"));
first.close();
const outcome = await Promise.race([
result.then(() => "resolved", (error) => error instanceof Error ? error.message : String(error)),
new Promise<string>((resolve) => setTimeout(() => resolve("pending"), 20)),
]);
if (outcome === "pending") session.resolveResult("first", { requestId, result: null });
assert.match(outcome, /断开/);
});
test("shared thread events are broadcast with the active thread id", (t) => {
const session = new CanvasSession();
const first = connect(session, "first");
const second = connect(session, "second");
t.after(() => {
first.close();
second.close();
});
session.emitThread("workspace_changed", "thread-2", { activeThreadId: "thread-2" });
assert.deepEqual(first.event("workspace_changed"), { activeThreadId: "thread-2", threadId: "thread-2" });
assert.deepEqual(second.event("workspace_changed"), { activeThreadId: "thread-2", threadId: "thread-2" });
});
test("new clients receive the current Codex state and later updates", (t) => {
const session = new CanvasSession();
session.setCodexState({ busy: true, threadId: "thread-2", turnId: "turn-1" });
const client = connect(session, "first");
t.after(() => client.close());
assert.deepEqual(field(client.event("hello"), "codex"), { busy: true, threadId: "thread-2", turnId: "turn-1" });
session.setCodexState({ busy: false });
assert.deepEqual(client.event("codex_state"), { busy: false, threadId: "thread-2", turnId: "turn-1" });
});
test("a bound client remains the tool target while focus changes", async (t) => {
const session = new CanvasSession();
const first = connect(session, "first");
const second = connect(session, "second");
t.after(() => {
first.close();
second.close();
});
session.updateState(snapshot("canvas-first"), "first");
session.updateState(snapshot("canvas-second"), "second");
session.bindClient("first");
session.activateClient("second");
assert.equal(field(await session.callTool("canvas_get_state", {}), "projectId"), "canvas-first");
const result = session.callTool("canvas_create_text_node", { text: "bound" });
const call = first.event("tool_call");
assert.equal(second.event("tool_call"), undefined);
session.resolveResult("first", { requestId: String(field(call, "requestId")), result: { ok: true } });
assert.deepEqual(await result, { ok: true });
session.releaseClient("first");
assert.equal(field(await session.callTool("canvas_get_state", {}), "projectId"), "canvas-second");
});
test("closing the bound client falls back to the active client", async (t) => {
const session = new CanvasSession();
const first = connect(session, "first");
const second = connect(session, "second");
t.after(() => {
first.close();
second.close();
});
session.updateState(snapshot("canvas-first"), "first");
session.updateState(snapshot("canvas-second"), "second");
session.bindClient("first");
session.activateClient("second");
first.close();
assert.equal(field(await session.callTool("canvas_get_state", {}), "projectId"), "canvas-second");
const result = session.callTool("canvas_create_text_node", { text: "fallback" });
const call = second.event("tool_call");
session.resolveResult("second", { requestId: String(field(call, "requestId")), result: { ok: true } });
assert.deepEqual(await result, { ok: true });
});
/** 创建用于测试的画布 SSE 连接。 */
function connect(session: CanvasSession, clientId: string) {
const response = new FakeSseResponse();
session.openEvents(new URL(`http://127.0.0.1/events?clientId=${clientId}`), response as unknown as ServerResponse);
return response;
}
/** 创建最小画布快照。 */
function snapshot(projectId: string) {
return { projectId, title: projectId, nodes: [], connections: [], selectedNodeIds: [], viewport: { x: 0, y: 0, k: 1 } };
}
/** 安全读取测试对象字段。 */
function field(value: unknown, key: string) {
return value && typeof value === "object" ? (value as Record<string, unknown>)[key] : undefined;
}
/** 模拟 Node SSE 响应并提供事件读取能力。 */
class FakeSseResponse extends EventEmitter {
private chunks: string[] = [];
/** 模拟写入响应头。 */
writeHead() {
return this;
}
/** 保存写入的 SSE 文本块。 */
write(chunk: string) {
this.chunks.push(chunk);
return true;
}
/** 读取指定类型的首个 SSE 事件数据。 */
event(type: string) {
const chunk = this.chunks.find((item) => item.startsWith(`event: ${type}\n`));
const data = chunk?.split("\n").find((line) => line.startsWith("data: "))?.slice(6);
return data ? (JSON.parse(data) as unknown) : undefined;
}
/** 触发连接关闭事件。 */
close() {
this.emit("close");
}
}
+263
View File
@@ -0,0 +1,263 @@
import crypto from "node:crypto";
import type { ServerResponse } from "node:http";
import type { AgentAttachment } from "../agent/types.js";
import { logger } from "../utils/logger.js";
import { buildCanvasToolRequest, fitAttachmentNodeSize } from "./operations.js";
import type { ToolName } from "./schemas.js";
import { compactCanvasState, compactNode, isToolName, nextCanvasX, parseToolInput } from "./tools.js";
import type { CanvasSnapshot } from "./types.js";
type PendingRequest = { clientId: string; resolve: (value: unknown) => void; reject: (error: Error) => void };
type TurnAttachment = { clientId: string; id: string; name: string; type: string; size: number; width: number; height: number; dataUrl: string };
export type CodexState = { busy: boolean; threadId: string; turnId: string };
const SITE_TOOLS = new Set<ToolName>([
"site_navigate",
"canvas_list_projects",
"workbench_image_get_config",
"workbench_image_generate",
"workbench_video_get_config",
"workbench_video_generate",
"prompts_search",
"assets_list",
"assets_add",
"generation_get_status",
]);
/** 管理网页画布连接、状态、附件和工具请求。 */
export class CanvasSession {
private clients = new Map<string, ServerResponse>();
private clientFocusOrder = new Map<string, number>();
private pending = new Map<string, PendingRequest>();
private canvasStates = new Map<string, CanvasSnapshot>();
private turnAttachments = new Map<string, TurnAttachment>();
private activeClientId = "";
private boundClientId = "";
private focusSequence = 0;
private codexState: CodexState = { busy: false, threadId: "", turnId: "" };
/** 获取当前目标网页的画布状态。 */
private get canvasState() {
return this.canvasStates.get(this.targetClientId) || null;
}
/** 获取当前 turn 绑定或最近激活的网页客户端。 */
private get targetClientId() {
return this.boundClientId || this.activeClientId;
}
/** 返回 Canvas Agent 当前连接状态。 */
health() {
return { ok: true, hasCanvas: Boolean(this.canvasState), clients: this.clients.size, codexBusy: this.codexState.busy };
}
/** 返回 Codex 是否正在执行任务。 */
get codexBusy() {
return this.codexState.busy;
}
/** 更新并广播 Codex 运行状态。 */
setCodexState(patch: Partial<CodexState>) {
const next = { ...this.codexState, ...patch };
if (next.busy === this.codexState.busy && next.threadId === this.codexState.threadId && next.turnId === this.codexState.turnId) return;
this.codexState = next;
logger.debug("Codex state changed", this.codexState);
this.emitAll("codex_state", this.codexState);
}
/** 建立网页与 Canvas Agent 之间的 SSE 连接。 */
openEvents(url: URL, res: ServerResponse) {
const clientId = url.searchParams.get("clientId") || crypto.randomUUID();
const statusOnly = url.searchParams.get("role") === "status";
logger.info("SSE client connected", { clientId, statusOnly });
res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" });
if (!statusOnly) {
this.clients.set(clientId, res);
if (!this.clientFocusOrder.has(clientId)) this.clientFocusOrder.set(clientId, 0);
if (!this.activeClientId) {
this.activeClientId = clientId;
this.clientFocusOrder.set(clientId, ++this.focusSequence);
}
}
sendEvent(res, "hello", { ok: true, clientId, codex: this.codexState });
const timer = setInterval(() => sendEvent(res, "ping", { time: Date.now() }), 15000);
res.on("close", () => {
clearInterval(timer);
logger.info("SSE client disconnected", { clientId, statusOnly });
if (statusOnly || this.clients.get(clientId) !== res) return;
this.clients.delete(clientId);
this.clientFocusOrder.delete(clientId);
this.canvasStates.delete(clientId);
if (this.boundClientId === clientId) this.boundClientId = "";
this.pending.forEach((item, requestId) => {
if (item.clientId !== clientId) return;
this.pending.delete(requestId);
item.reject(new Error("请求页面已断开"));
});
if (this.activeClientId === clientId) this.activeClientId = [...this.clients.keys()].sort((a, b) => (this.clientFocusOrder.get(b) || 0) - (this.clientFocusOrder.get(a) || 0))[0] || "";
});
}
/** 保存指定网页上报的最新画布快照。 */
updateState(body: unknown, clientId?: string) {
const targetClientId = clientId || this.activeClientId;
if (!targetClientId) return;
this.canvasStates.set(targetClientId, { ...((body && typeof body === "object" && !Array.isArray(body) ? body : {}) as Record<string, unknown>), clientId: targetClientId } as CanvasSnapshot);
logger.debug("Canvas state updated", { clientId: targetClientId, nodes: Array.isArray((body as CanvasSnapshot | null)?.nodes) ? (body as CanvasSnapshot).nodes.length : 0, connections: Array.isArray((body as CanvasSnapshot | null)?.connections) ? (body as CanvasSnapshot).connections.length : 0 });
}
/** 将指定网页设为最近激活的工具目标。 */
activateClient(clientId: string) {
if (!this.clients.has(clientId)) throw new Error("当前网页未连接");
this.activeClientId = clientId;
this.clientFocusOrder.set(clientId, ++this.focusSequence);
logger.debug("Canvas client activated", { clientId });
}
/** 将当前 Agent turn 固定绑定到指定网页。 */
bindClient(clientId: string) {
if (!this.clients.has(clientId)) throw new Error("当前网页未连接");
this.boundClientId = clientId;
logger.debug("Canvas client bound to turn", { clientId });
}
/** 解除当前 Agent turn 的网页绑定。 */
releaseClient(clientId: string) {
if (this.boundClientId === clientId) this.boundClientId = "";
logger.debug("Canvas client released from turn", { clientId });
}
/** 保存当前 turn 可用的图片附件并返回安全引用。 */
setTurnAttachments(clientId: string, attachments: AgentAttachment[]) {
this.turnAttachments.clear();
return attachments.flatMap((item, index) => {
if (!item.dataUrl?.startsWith("data:image/")) return [];
const id = item.id?.trim() || `attachment-${crypto.randomUUID()}`;
const attachment: TurnAttachment = {
clientId,
id,
name: item.name?.trim() || `图片 ${index + 1}`,
type: item.type?.startsWith("image/") ? item.type : item.dataUrl.match(/^data:([^;]+)/)?.[1] || "image/png",
size: positiveNumber(item.size, 0),
width: positiveNumber(item.width, 1024),
height: positiveNumber(item.height, 1024),
dataUrl: item.dataUrl,
};
this.turnAttachments.set(id, attachment);
return [{ id, name: attachment.name, type: attachment.type, size: attachment.size, width: attachment.width, height: attachment.height }];
});
}
/** 清理指定网页或全部 turn 附件。 */
clearTurnAttachments(clientId?: string) {
this.turnAttachments.forEach((item, id) => {
if (!clientId || item.clientId === clientId) this.turnAttachments.delete(id);
});
}
/** 获取属于指定网页 turn 的图片附件。 */
getTurnAttachment(clientId: string, attachmentId: string) {
const attachment = this.turnAttachments.get(attachmentId);
if (!attachment) throw new Error(`找不到本轮图片附件:${attachmentId}`);
if (attachment.clientId !== clientId) throw new Error("图片附件不属于当前 turn 的发起标签页");
return attachment;
}
/** 接收网页返回的工具调用结果。 */
resolveResult(clientId: string, body: { requestId?: string; error?: string; result?: unknown }) {
const item = body.requestId ? this.pending.get(body.requestId) : null;
if (!item || !body.requestId || item.clientId !== clientId) return false;
this.pending.delete(body.requestId);
logger.debug("Canvas tool result received", { clientId, requestId: body.requestId, error: body.error, result: body.result });
body.error ? item.reject(new Error(body.error)) : item.resolve(body.result);
return true;
}
/** 向全部已连接网页广播事件。 */
emitAll(type: string, payload: unknown) {
this.clients.forEach((client) => sendEvent(client, type, payload));
}
/** 向全部网页广播带线程归属的事件。 */
emitThread(type: string, threadId: string, payload: Record<string, unknown> = {}) {
this.emitAll(type, { ...payload, threadId });
}
/** 校验工具参数并将调用分派到当前目标网页。 */
async callTool(name: unknown, rawInput: unknown) {
if (!isToolName(name)) throw new Error(`未知工具:${String(name)}`);
logger.info("MCP tool called", { name, input: rawInput, targetClientId: this.targetClientId });
const input = parseToolInput(name, rawInput) as Record<string, unknown>;
if (SITE_TOOLS.has(name)) {
if (!this.clients.size) throw new Error("当前没有已连接网页");
return await this.requestCanvasTool(name, input);
}
const readTool = ["canvas_get_state", "canvas_get_selection", "canvas_export_snapshot"].includes(name);
if (readTool && (!this.clients.size || !this.canvasState)) throw new Error("当前没有已连接画布");
if (name === "canvas_get_state" || name === "canvas_export_snapshot") return compactCanvasState(this.canvasState);
if (name === "canvas_get_selection") {
const ids = new Set(this.canvasState?.selectedNodeIds || []);
return { nodes: (this.canvasState?.nodes || []).filter((node) => ids.has(node.id)).map(compactNode) };
}
if (name === "canvas_create_attachment_nodes") return await this.createAttachmentNodes(input as { attachmentIds: string[]; x?: number; y?: number; gap?: number; direction?: "row" | "column" });
if (!this.clients.size) throw new Error("当前没有已连接画布");
const request = buildCanvasToolRequest(name, input, this.canvasState);
return await this.requestCanvasTool(request.name, request.input);
}
/** 将当前 turn 的附件转换为画布图片节点。 */
private async createAttachmentNodes(input: { attachmentIds: string[]; x?: number; y?: number; gap?: number; direction?: "row" | "column" }) {
const clientId = this.targetClientId;
if (!this.clients.has(clientId)) throw new Error("当前没有已连接画布");
const attachments = input.attachmentIds.map((id) => this.getTurnAttachment(clientId, id));
const x = Number(input.x ?? nextCanvasX(this.canvasState));
const y = Number(input.y ?? 0);
const gap = Number(input.gap ?? 40);
const direction = input.direction || "row";
let offset = 0;
const nodes = attachments.map((attachment) => {
const size = fitAttachmentNodeSize(attachment.width, attachment.height);
const node = {
id: `image-${crypto.randomUUID()}`,
attachmentId: attachment.id,
title: attachment.name,
position: { x: direction === "row" ? x + offset : x, y: direction === "column" ? y + offset : y },
width: size.width,
height: size.height,
};
offset += (direction === "row" ? size.width : size.height) + gap;
return node;
});
await this.requestCanvasTool("canvas_create_attachment_nodes", { nodes });
return { nodes: nodes.map(({ id, attachmentId, title }) => ({ id, attachmentId, title })) };
}
/** 向目标网页发送工具请求并等待调用结果。 */
private async requestCanvasTool(name: ToolName, input: Record<string, unknown>) {
const requestId = crypto.randomUUID();
const clientId = this.targetClientId;
const client = this.clients.get(clientId);
if (!client) throw new Error("当前没有已连接画布");
sendEvent(client, "tool_call", { requestId, name, input });
logger.debug("Canvas tool request sent", { requestId, name, input, clientId });
return await new Promise((resolve, reject) => {
const timer = setTimeout(() => {
this.pending.delete(requestId);
logger.warn("Canvas tool request timed out", { requestId, name, clientId });
reject(new Error("画布操作超时"));
}, 30000);
this.pending.set(requestId, { clientId, resolve: (value) => (clearTimeout(timer), resolve(value)), reject: (error) => (clearTimeout(timer), reject(error)) });
});
}
}
/** 向 SSE 连接写入一个事件。 */
function sendEvent(res: ServerResponse, type: string, payload: unknown) {
res.write(`event: ${type}\ndata: ${JSON.stringify(payload)}\n\n`);
}
/** 将未知数值转换为正数,否则使用默认值。 */
function positiveNumber(value: unknown, fallback: number) {
const number = Number(value);
return Number.isFinite(number) && number > 0 ? number : fallback;
}
+31
View File
@@ -0,0 +1,31 @@
import { toolInputSchemas, toolNames, type ToolName } from "./schemas.js";
import type { CanvasNode, CanvasSnapshot } from "./types.js";
/** 判断传入名称是否为已注册的画布工具。 */
export function isToolName(name: unknown): name is ToolName {
return typeof name === "string" && toolNames.includes(name as ToolName);
}
/** 按工具名称校验并解析调用参数。 */
export function parseToolInput(name: ToolName, input: unknown) {
return toolInputSchemas[name].parse(input ?? {});
}
/** 压缩画布快照,避免向 Agent 返回过长的节点内容。 */
export function compactCanvasState(state: CanvasSnapshot | null) {
if (!state) throw new Error("当前没有已连接画布");
return { ...state, nodes: (state.nodes || []).map(compactNode) };
}
/** 压缩单个画布节点的元数据内容。 */
export function compactNode(node: CanvasNode) {
const metadata = { ...(node.metadata || {}) };
if (typeof metadata.content === "string" && metadata.content.length > 240) metadata.content = `${metadata.content.slice(0, 120)}...`;
return { id: node.id, type: node.type, title: node.title, position: node.position, width: node.width, height: node.height, metadata };
}
/** 计算新节点在当前画布右侧的默认横坐标。 */
export function nextCanvasX(state: CanvasSnapshot | null) {
const nodes = state?.nodes || [];
return nodes.length ? Math.max(...nodes.map((node) => node.position.x + node.width)) + 80 : 0;
}
+7
View File
@@ -0,0 +1,7 @@
/** 画布坐标。 */
export type Position = { x: number; y: number };
export type Viewport = { x: number; y: number; k: number };
export type CanvasNodeType = "image" | "text" | "config" | "video" | "audio";
export type CanvasNode = { id: string; type: CanvasNodeType; title?: string; position: Position; width: number; height: number; metadata?: Record<string, unknown> };
export type CanvasConnection = { id: string; fromNodeId: string; toNodeId: string };
export type CanvasSnapshot = { projectId?: string; title?: string; nodes?: CanvasNode[]; connections?: CanvasConnection[]; selectedNodeIds?: string[]; viewport?: Viewport; clientId?: string };
+84
View File
@@ -0,0 +1,84 @@
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
export const DEFAULT_PORT = 17371;
export const CONFIG_DIR = path.join(os.homedir(), ".infinite-canvas");
export const CONFIG_FILE = path.join(CONFIG_DIR, "canvas-agent.json");
export const VERSION = readPackageVersion();
export const AGENT_PROMPT = fs.readFileSync(new URL("../agent-instructions.md", import.meta.url), "utf8");
const initializedWorkspaces = new Set<string>();
export type SiteWorkspaceConfig = { workspacePath: string; activeThreadId?: string; pinnedThreadIds?: string[] };
export type CanvasAgentConfig = { url: string; token: string; origins?: string[]; workspace?: SiteWorkspaceConfig };
/** 读取本地 Canvas Agent 配置,不存在时生成默认配置。 */
export function loadConfig(create = false): CanvasAgentConfig {
try {
return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8")) as CanvasAgentConfig;
} catch {
const config = { url: `http://127.0.0.1:${Number(process.env.PORT) || DEFAULT_PORT}`, token: crypto.randomBytes(18).toString("hex") };
if (create) saveConfig(config);
return config;
}
}
/** 将 Canvas Agent 配置写入用户配置目录。 */
export function saveConfig(config: CanvasAgentConfig) {
fs.mkdirSync(CONFIG_DIR, { recursive: true });
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
}
/** 确保站点级 Codex 工作空间存在并已初始化。 */
export function ensureSiteWorkspace(config: CanvasAgentConfig) {
const current = config.workspace;
if (current?.workspacePath) {
const workspacePath = resolveWorkspacePath(current.workspacePath);
initializeWorkspace(workspacePath);
return { ...current, workspacePath };
}
const workspacePath = path.join(CONFIG_DIR, "codex-workspaces", "site");
config.workspace = { workspacePath };
initializeWorkspace(workspacePath);
saveConfig(config);
return { workspacePath };
}
/** 更新站点级 Codex 工作空间配置。 */
export function updateSiteWorkspace(config: CanvasAgentConfig, patch: Partial<SiteWorkspaceConfig>) {
const current = ensureSiteWorkspace(config);
const workspacePath = patch.workspacePath ? resolveWorkspacePath(patch.workspacePath) : current.workspacePath;
const next = { ...current, ...patch, workspacePath };
config.workspace = { workspacePath: next.workspacePath, activeThreadId: next.activeThreadId, pinnedThreadIds: next.pinnedThreadIds };
initializeWorkspace(workspacePath);
saveConfig(config);
return config.workspace;
}
/** 创建工作空间目录并写入默认 AGENTS.md。 */
function initializeWorkspace(workspacePath: string) {
if (initializedWorkspaces.has(workspacePath)) return;
fs.mkdirSync(workspacePath, { recursive: true });
const instructionsFile = path.join(workspacePath, "AGENTS.md");
const current = fs.existsSync(instructionsFile) ? fs.readFileSync(instructionsFile, "utf8") : "";
if (!current || current.startsWith("# Infinite Canvas Agent")) fs.writeFileSync(instructionsFile, AGENT_PROMPT);
initializedWorkspaces.add(workspacePath);
}
/** 将用户输入的工作空间路径解析为绝对路径。 */
function resolveWorkspacePath(value: string) {
if (value === "~") return os.homedir();
if (value.startsWith("~/")) return path.join(os.homedir(), value.slice(2));
return path.resolve(value);
}
/** 从当前包信息中读取 Canvas Agent 版本号。 */
function readPackageVersion() {
try {
const pkg = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8")) as { version?: string };
return pkg.version || "0.0.0";
} catch {
return "0.0.0";
}
}
+6
View File
@@ -0,0 +1,6 @@
#!/usr/bin/env node
import { startHttpServer } from "./server/http.js";
import { startMcpServer } from "./server/mcp.js";
if (process.argv[2] === "mcp") await startMcpServer();
else startHttpServer();
+297
View File
@@ -0,0 +1,297 @@
import { spawn } from "node:child_process";
import { readFile, stat } from "node:fs/promises";
import path from "node:path";
import express, { type NextFunction, type Request, type Response } from "express";
import { runClaudeTurn } from "../agent/claude.js";
import { archiveCodexThread, interruptCodexTurn, isRecoverableThreadError, listCodexThreads, readCodexThread, resolveCodexApproval, resumeCodexThread, runCodexTurn, startCodexThread, summarizeCodexThread, verifyCodexThreadWorkspace } from "../agent/codex.js";
import type { AgentAttachment, AgentPermissionMode } from "../agent/types.js";
import { CanvasSession } from "../canvas/session.js";
import { DEFAULT_PORT, ensureSiteWorkspace, loadConfig, saveConfig, updateSiteWorkspace, type CanvasAgentConfig } from "../config.js";
import { logger } from "../utils/logger.js";
/** 启动仅监听本机的 Canvas Agent HTTP 服务。 */
export function startHttpServer() {
const config = loadConfig(true);
const port = Number(process.env.PORT) || Number(new URL(config.url).port) || DEFAULT_PORT;
config.url = `http://127.0.0.1:${port}`;
saveConfig(config);
const session = new CanvasSession();
/** 将 Agent 事件广播到所属线程或全部网页。 */
const emit = (type: string, payload: unknown) => {
const data = payload && typeof payload === "object" && !Array.isArray(payload) ? payload as Record<string, unknown> : { value: payload };
const threadId = String(data.threadId || data.thread_id || ensureSiteWorkspace(config).activeThreadId || "");
threadId ? session.emitThread(type, threadId, data) : session.emitAll(type, data);
};
/** 保存并广播当前站点工作空间的活跃线程。 */
const setActiveThread = (activeThreadId: string, payload: Record<string, unknown> = {}) => {
const workspace = updateSiteWorkspace(config, { activeThreadId: activeThreadId || undefined });
session.emitThread("workspace_changed", activeThreadId, { ...payload, activeThreadId });
return workspace;
};
const app = express();
app.disable("x-powered-by");
app.use(express.json({ limit: "30mb" }));
app.use((req, res, next) => {
if (!logger.enabled) return next();
const startedAt = Date.now();
const url = requestUrl(req, config);
res.on("finish", () => {
if (req.method === "OPTIONS" || (res.statusCode < 400 && ["/health", "/canvas/state", "/canvas/activate"].includes(url.pathname))) return;
logger.debug(`HTTP ${req.method} ${url.pathname}`, { status: res.statusCode, durationMs: Date.now() - startedAt });
});
next();
});
app.use((req, res, next) => {
const url = requestUrl(req, config);
if (!setCors(req, res, url, config)) return void res.status(403).json({ ok: false, error: "origin not allowed" });
if (req.method === "OPTIONS") return void res.json({});
next();
});
app.get("/health", (_req, res) => res.json(session.health()));
app.get("/config", (_req, res) => res.json({ ok: true, url: config.url, hasToken: true }));
app.use((req, res, next) => {
if (validToken(req, requestUrl(req, config), config.token)) return next();
res.status(401).json({ ok: false, error: "invalid token" });
});
app.get("/events", (req, res) => session.openEvents(requestUrl(req, config), res));
app.post("/canvas/state", (req, res) => {
session.updateState(req.body, String(req.query.clientId || "") || undefined);
res.json({ ok: true });
});
app.post("/canvas/activate", (req, res) => {
session.activateClient(String(req.query.clientId || ""));
res.json({ ok: true });
});
app.post("/canvas/result", (req, res) => {
const ok = session.resolveResult(String(req.query.clientId || ""), req.body);
res.status(ok ? 200 : 409).json({ ok });
});
app.get("/agent/attachments/:attachmentId", route(async (req, res) => {
const attachment = session.getTurnAttachment(String(req.query.clientId || ""), routeParam(req.params.attachmentId));
const data = attachment.dataUrl.split(",", 2)[1];
if (!data) throw new Error("图片附件内容无效");
res.setHeader("Cache-Control", "no-store");
res.type(attachment.type).send(Buffer.from(data, "base64"));
}));
app.post("/agent/local-file/reveal", route(async (req, res) => {
const filePath = String(req.body?.path || "");
if (!path.isAbsolute(filePath)) return res.status(400).json({ ok: false, error: "文件路径必须是绝对路径" });
const file = await stat(filePath);
await revealLocalFile(filePath, file.isDirectory());
res.json({ ok: true });
}));
app.post("/agent/local-image", route(async (req, res) => {
const filePath = String(req.body?.path || "");
if (!path.isAbsolute(filePath) || !/\.(?:avif|gif|jpe?g|png|webp)$/i.test(filePath)) return res.status(400).json({ ok: false, error: "图片路径无效" });
const file = await stat(filePath);
if (!file.isFile()) return res.status(400).json({ ok: false, error: "图片文件无效" });
res.setHeader("Cache-Control", "no-store");
res.type(path.extname(filePath)).send(await readFile(filePath));
}));
app.post("/api/tools", route(async (req, res) => res.json({ ok: true, result: await session.callTool(req.body?.name, req.body?.input || {}) })));
app.get("/agent/codex/workspace", (_req, res) => {
const workspace = ensureSiteWorkspace(config);
res.json({ ok: true, workspace });
});
app.get("/agent/codex/threads", route(async (req, res) => {
const workspace = ensureSiteWorkspace(config);
const result = await listCodexThreads(emit, { cwd: workspace.workspacePath, searchTerm: String(req.query.searchTerm || "") });
res.json({ ok: true, workspace, ...result });
}));
app.post("/agent/codex/threads/new", route(async (req, res) => {
if (session.codexBusy) return res.status(409).json({ ok: false, error: "Codex 正在运行,请等待当前任务完成" });
const workspace = ensureSiteWorkspace(config);
const thread = await startCodexThread(emit, workspace.workspacePath, permissionMode(req.body?.permissionMode));
const activeThreadId = String((thread as Record<string, unknown>).id || "");
const nextWorkspace = setActiveThread(activeThreadId, { emptyThread: true });
res.json({ ok: true, workspace: nextWorkspace, thread: summarizeCodexThread(thread), messages: [] });
}));
app.get("/agent/codex/threads/:threadId", route(async (req, res) => {
const workspace = ensureSiteWorkspace(config);
const threadId = routeParam(req.params.threadId);
try {
res.json({ ok: true, workspace, ...(await readCodexThread(emit, threadId, workspace.workspacePath)) });
} catch (error) {
if (workspace.activeThreadId !== threadId || !isRecoverableThreadError(error)) throw error;
res.json({ ok: true, workspace, thread: { id: threadId, preview: "", cwd: workspace.workspacePath }, messages: [] });
}
}));
app.post("/agent/codex/threads/:threadId/resume", route(async (req, res) => {
if (session.codexBusy) return res.status(409).json({ ok: false, error: "Codex 正在运行,请等待当前任务完成" });
const workspace = ensureSiteWorkspace(config);
const threadId = routeParam(req.params.threadId);
const result = await resumeCodexThread(emit, threadId, workspace.workspacePath, permissionMode(req.body?.permissionMode));
const nextWorkspace = setActiveThread(threadId);
res.json({ ok: true, workspace: nextWorkspace, ...result });
}));
app.post("/agent/codex/threads/:threadId/delete", route(async (req, res) => {
if (session.codexBusy) return res.status(409).json({ ok: false, error: "Codex 正在运行,请等待当前任务完成" });
const workspace = ensureSiteWorkspace(config);
const threadId = routeParam(req.params.threadId);
await archiveCodexThread(emit, threadId, workspace.workspacePath);
setActiveThread(workspace.activeThreadId === threadId ? "" : workspace.activeThreadId || "");
res.json({ ok: true });
}));
app.post("/agent/codex/turn", route(async (req, res) => {
if (session.codexBusy) return res.status(409).json({ ok: false, error: "Codex 正在运行,请等待当前任务完成" });
const attachments = Array.isArray(req.body?.attachments) ? (req.body.attachments as AgentAttachment[]) : [];
const workspace = ensureSiteWorkspace(config);
const prompt = String(req.body?.prompt || "");
if (!prompt.trim()) return res.status(400).json({ ok: false, error: "请输入任务内容" });
const clientId = String(req.body?.clientId || "");
logger.info("Codex turn accepted", { threadId: req.body?.threadId, promptLength: prompt.length, attachmentCount: attachments.length });
session.setCodexState({ busy: true, threadId: String(req.body?.threadId || workspace.activeThreadId || ""), turnId: "" });
try {
let threadId = String(req.body?.threadId || workspace.activeThreadId || "");
let turnId = "";
if (!threadId) {
const thread = await startCodexThread(emit, workspace.workspacePath, permissionMode(req.body?.permissionMode));
threadId = String((thread as Record<string, unknown>).id || "");
setActiveThread(threadId, { emptyThread: true });
} else if (threadId !== workspace.activeThreadId) {
await verifyCodexThreadWorkspace(emit, threadId, workspace.workspacePath);
setActiveThread(threadId);
}
const attachmentRefs = session.setTurnAttachments(clientId, attachments);
const chatMessage = {
sourceClientId: clientId,
message: { id: String(req.body?.messageId || Date.now()), role: "user", text: String(req.body?.messageText || prompt || `发送了 ${attachments.length} 张图片`) },
};
let chatThreadId = "";
/** 将当前 turn 事件固定广播到实际线程。 */
const turnEmit = (type: string, payload: unknown) => {
const data = payload && typeof payload === "object" && !Array.isArray(payload) ? payload as Record<string, unknown> : { value: payload };
session.emitThread(type, threadId, { ...data, ...(turnId ? { turn_id: turnId } : {}) });
};
void runCodexTurn(withAttachmentContext(prompt, attachmentRefs), turnEmit, attachments, {
threadId,
cwd: workspace.workspacePath,
permissionMode: permissionMode(req.body?.permissionMode),
appEmit: emit,
onStart: clientId ? () => session.bindClient(clientId) : undefined,
onThread: (actualThreadId) => {
if (actualThreadId !== threadId) {
threadId = actualThreadId;
setActiveThread(threadId, { emptyThread: true });
}
session.setCodexState({ busy: true, threadId, turnId: "" });
if (chatThreadId !== threadId) {
chatThreadId = threadId;
session.emitThread("chat_message", threadId, chatMessage);
}
},
onTurn: (actualTurnId) => {
turnId = actualTurnId;
logger.info("Codex turn started", { threadId, turnId });
session.setCodexState({ busy: true, threadId, turnId });
},
onFinish: () => {
logger.info("Codex turn finished", { threadId, turnId });
session.clearTurnAttachments(clientId);
if (clientId) session.releaseClient(clientId);
session.setCodexState({ busy: false, threadId, turnId });
},
});
res.json({ ok: true, threadId });
} catch (error) {
session.setCodexState({ busy: false, threadId: String(req.body?.threadId || workspace.activeThreadId || ""), turnId: "" });
throw error;
}
}));
app.post("/agent/codex/approval", route(async (req, res) => {
const decision = String(req.body?.decision || "");
if (!["accept", "acceptForSession", "decline", "cancel"].includes(decision)) return res.status(400).json({ ok: false, error: "无效的审批决定" });
const ok = await resolveCodexApproval(String(req.body?.requestId || ""), decision);
res.status(ok ? 200 : 409).json({ ok, ...(ok ? {} : { error: "审批请求已失效" }) });
}));
app.post("/agent/codex/interrupt", route(async (req, res) => res.json({ ok: await interruptCodexTurn(String(req.body?.threadId || "")) })));
app.post("/agent/claude/turn", (req, res) => {
runClaudeTurn(String(req.body?.prompt || ""), emit);
res.json({ ok: true });
});
app.use((_req, res) => res.status(404).json({ ok: false, error: "not found" }));
app.use((error: Error, req: Request, res: Response, _next: NextFunction) => {
logger.error("HTTP request failed", { method: req.method, path: req.path, error });
res.status(500).json({ ok: false, error: error.message });
});
app.listen(port, "127.0.0.1", () => {
console.log("Infinite Canvas Agent");
console.log(`Local URL: ${config.url}`);
console.log(`Connect token: ${config.token}`);
console.log("Codex MCP is not installed by this command.");
console.log("Optional MCP add: codex mcp add infinite-canvas -- npx -y @basketikun/canvas-agent mcp");
console.log("Remove manually added MCP: codex mcp remove infinite-canvas");
if (logger.enabled) console.log(`Debug log: ${logger.filePath}`);
logger.info("Canvas Agent started", { url: config.url, workspace: ensureSiteWorkspace(config).workspacePath, debugLog: logger.filePath });
});
}
/** 将异步 Express 路由异常交给统一错误处理中间件。 */
function route(handler: (req: Request, res: Response) => Promise<unknown>) {
return (req: Request, res: Response, next: NextFunction) => void handler(req, res).catch(next);
}
/** 从 Express 路由参数中读取单个字符串。 */
function routeParam(value: string | string[]) {
return Array.isArray(value) ? value[0] || "" : value;
}
function permissionMode(value: unknown): AgentPermissionMode {
return value === "automatic" || value === "full" ? value : "request";
}
/** 使用当前操作系统的文件管理器定位本地文件。 */
function revealLocalFile(filePath: string, isDirectory: boolean) {
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer.exe" : "xdg-open";
const args = process.platform === "darwin"
? ["-R", filePath]
: process.platform === "win32"
? [isDirectory ? filePath : `/select,${filePath}`]
: [isDirectory ? filePath : path.dirname(filePath)];
return new Promise<void>((resolve, reject) => {
const child = spawn(command, args, { detached: true, stdio: "ignore" });
child.once("spawn", () => {
child.unref();
resolve();
});
child.once("error", reject);
});
}
/** 结合服务配置解析当前请求 URL。 */
function requestUrl(req: Request, config: CanvasAgentConfig) {
return new URL(req.originalUrl || req.url || "/", config.url);
}
/** 设置跨域响应头并记录通过 token 授权的来源。 */
function setCors(req: Request, res: Response, url: URL, config: CanvasAgentConfig) {
const origin = req.headers.origin;
res.setHeader("Access-Control-Allow-Origin", origin || "*");
res.setHeader("Access-Control-Allow-Headers", "content-type,x-canvas-agent-token");
res.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS");
res.setHeader("Access-Control-Allow-Private-Network", "true");
if (!origin || req.method === "OPTIONS" || url.pathname === "/health" || url.pathname === "/config") return true;
config.origins ||= [];
if (validToken(req, url, config.token) && !config.origins.includes(origin)) {
config.origins.push(origin);
saveConfig(config);
}
res.setHeader("Vary", "Origin");
return config.origins.includes(origin);
}
/** 校验请求查询参数或请求头中的连接 token。 */
function validToken(req: Request, url: URL, token: string) {
const header = req.headers["x-canvas-agent-token"];
return url.searchParams.get("token") === token || header === token || (Array.isArray(header) && header.includes(token));
}
/** 向 Agent 提示词追加本轮图片附件引用说明。 */
function withAttachmentContext(prompt: string, attachments: Array<{ id: string; name: string }>) {
if (!attachments.length) return prompt;
const list = attachments.map((item, index) => `${index + 1}. attachmentId=${item.id}, name=${JSON.stringify(item.name)}`).join("\n");
return `${prompt}\n\n本轮可用图片附件(顺序与图片输入一致):\n${list}\n需要把附件放入画布或作为生成参考图时,先调用 canvas_create_attachment_nodes,再使用返回的画布节点 ID 创建生成流程。`;
}
+32
View File
@@ -0,0 +1,32 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { toolDescriptions, toolInputSchemas, toolNames, type ToolName } from "../canvas/schemas.js";
import { AGENT_PROMPT, loadConfig, type CanvasAgentConfig, VERSION } from "../config.js";
type CanvasAgentToolResponse = { ok?: boolean; result?: unknown; error?: string };
/** 启动通过标准输入输出通信的 MCP 服务。 */
export async function startMcpServer() {
const config = loadConfig(true);
const server = new McpServer({ name: "canvas-agent", version: VERSION }, { instructions: AGENT_PROMPT });
toolNames.forEach((name) => registerCanvasTool(server, config, name));
await server.connect(new StdioServerTransport());
}
/** 向 MCP Server 注册单个 Canvas Agent 工具。 */
function registerCanvasTool(server: McpServer, config: CanvasAgentConfig, name: ToolName) {
const schema = toolInputSchemas[name];
server.registerTool(name, { description: toolDescriptions[name], inputSchema: schema.shape }, async (input: unknown) => {
const result = await postCanvasAgentTool(config, name, schema.parse(input));
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
});
}
/** 将 MCP 工具调用转发到本地 Canvas Agent HTTP 服务。 */
async function postCanvasAgentTool(config: CanvasAgentConfig, name: ToolName, input: unknown) {
const res = await fetch(`${config.url}/api/tools`, { method: "POST", headers: { "content-type": "application/json", "x-canvas-agent-token": config.token }, body: JSON.stringify({ name, input }) });
const body = (await res.json()) as CanvasAgentToolResponse;
if (!body.ok) throw new Error(body.error || "tool call failed");
return body.result;
}
+7
View File
@@ -0,0 +1,7 @@
/** 将日期格式化为适合文件名使用的本地日期字符串。 */
export function formatDateForFilename(date = new Date()) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
}
+78
View File
@@ -0,0 +1,78 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import {inspect} from "node:util";
import winston, {format, transports, type Logger as WinstonLogger} from "winston";
import {formatDateForFilename} from "./date.js";
/** 管理 Canvas Agent 的终端与文件 Debug 日志。 */
export class Logger {
readonly enabled = process.argv.includes("--debug");
readonly filePath = this.enabled ? path.join(os.homedir(), ".infinite-canvas", "logs", `canvas-agent-${formatDateForFilename()}.log`) : "";
private readonly logger: WinstonLogger | null;
/** 根据命令行 Debug 参数初始化日志输出。 */
constructor() {
if (!this.enabled) {
this.logger = null;
return;
}
fs.mkdirSync(path.dirname(this.filePath), {recursive: true});
const line = format.printf(({level, message, timestamp, details}) => `${timestamp} ${level.toUpperCase()} ${message}${formatDetails(details)}`);
this.logger = winston.createLogger({
level: "debug",
transports: [
new transports.Console({format: format.combine(format.timestamp({format: "HH:mm:ss"}), line)}),
new transports.File({filename: this.filePath, format: format.combine(format.timestamp({format: "HH:mm:ss"}), line)}),
],
});
}
/** 输出 Debug 级别日志。 */
debug(message: string, details?: unknown) {
if (details === undefined) this.logger?.debug(message);
else this.logger?.debug(message, {details: sanitize(details)});
}
/** 输出 Info 级别日志。 */
info(message: string, details?: unknown) {
if (details === undefined) this.logger?.info(message);
else this.logger?.info(message, {details: sanitize(details)});
}
/** 输出 Warn 级别日志。 */
warn(message: string, details?: unknown) {
if (details === undefined) this.logger?.warn(message);
else this.logger?.warn(message, {details: sanitize(details)});
}
/** 输出 Error 级别日志。 */
error(message: string, details?: unknown) {
if (details === undefined) this.logger?.error(message);
else this.logger?.error(message, {details: sanitize(details)});
}
}
/** 将日志详情格式化为紧凑的单行文本。 */
function formatDetails(details: unknown) {
if (details === undefined) return "";
if (!details || typeof details !== "object" || Array.isArray(details)) return ` ${inspect(details, {depth: null, breakLength: Infinity})}`;
const text = Object.entries(details).filter(([, value]) => value !== undefined).map(([key, value]) => `${key}=${inspect(value, {depth: null, breakLength: Infinity})}`).join(" ");
return text ? ` ${text}` : "";
}
/** 清理日志内容中的敏感数据和不可序列化引用。 */
function sanitize(value: unknown, key = "", seen = new WeakSet<object>()): unknown {
if (/token|authorization|api.?key|dataurl/i.test(key)) return "[REDACTED]";
if (typeof value === "string" && value.startsWith("data:")) return `[DATA URL ${value.length} chars]`;
if (value instanceof Error) return {name: value.name, message: value.message, stack: value.stack};
if (!value || typeof value !== "object") return value;
if (seen.has(value)) return "[CIRCULAR]";
seen.add(value);
if (Array.isArray(value)) return value.map((item) => sanitize(item, key, seen));
return Object.fromEntries(Object.entries(value as Record<string, unknown>).map(([field, item]) => [field, sanitize(item, field, seen)]));
}
export const logger = new Logger();
+11
View File
@@ -0,0 +1,11 @@
export type JsonRecord = Record<string, unknown>;
/** 安全读取未知对象中的指定字段。 */
export function field(value: unknown, key: string) {
return value && typeof value === "object" ? (value as JsonRecord)[key] : undefined;
}
/** 将未知异常转换为可展示的错误信息。 */
export function errorMessage(error: unknown) {
return error instanceof Error ? error.message : String(error);
}
+15
View File
@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"esModuleInterop": true,
"declaration": true,
"outDir": "dist",
"rootDir": "src",
"skipLibCheck": true
},
"include": ["src/**/*.ts"],
"exclude": ["src/**/*.test.ts"]
}
-48
View File
@@ -1,48 +0,0 @@
package config
import (
"crypto/rand"
"encoding/base64"
"strings"
"github.com/caarlos0/env/v11"
"github.com/joho/godotenv"
)
type Config struct {
Port string `env:"PORT" envDefault:"8080"`
AdminUsername string `env:"ADMIN_USERNAME" envDefault:"admin"`
AdminPassword string `env:"ADMIN_PASSWORD" envDefault:"infinite-canvas"`
JWTSecret string `env:"JWT_SECRET" envDefault:"infinite-canvas"`
JWTExpireHours int `env:"JWT_EXPIRE_HOURS" envDefault:"168"`
StorageDriver string `env:"STORAGE_DRIVER" envDefault:"sqlite"`
DatabaseDSN string `env:"DATABASE_DSN" envDefault:"data/infinite-canvas.db"`
LinuxDoAuthorizeURL string `env:"LINUX_DO_AUTHORIZE_URL" envDefault:"https://connect.linux.do/oauth2/authorize"`
LinuxDoTokenURL string `env:"LINUX_DO_TOKEN_URL" envDefault:"https://connect.linux.do/oauth2/token"`
LinuxDoUserInfoURL string `env:"LINUX_DO_USERINFO_URL" envDefault:"https://connect.linux.do/api/user"`
}
var Cfg Config
func Load() error {
_ = godotenv.Load()
if err := env.Parse(&Cfg); err != nil {
return err
}
if strings.TrimSpace(Cfg.JWTSecret) == "" || Cfg.JWTSecret == "infinite-canvas" {
secret, err := randomSecret()
if err != nil {
return err
}
Cfg.JWTSecret = secret
}
return nil
}
func randomSecret() (string, error) {
buf := make([]byte, 32)
if _, err := rand.Read(buf); err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(buf), nil
}
-4
View File
@@ -4,10 +4,6 @@ services:
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
env_file:
- .env
volumes:
- ./data:/app/data
ports: ports:
- "3000:3000" - "3000:3000"
restart: unless-stopped restart: unless-stopped
+4 -4
View File
@@ -2,10 +2,10 @@ services:
app: app:
image: ghcr.io/basketikun/infinite-canvas:latest image: ghcr.io/basketikun/infinite-canvas:latest
container_name: infinite-canvas container_name: infinite-canvas
env_file:
- .env
volumes:
- ./data:/app/data
ports: ports:
- "3000:3000" - "3000:3000"
restart: unless-stopped restart: unless-stopped
# 统计分析(可选,默认关闭)。每家一个独立变量,填了谁就启用谁,可同时启用多家:
# environment:
# ANALYTICS_GA4_ID: G-XXXXXXXXXX # Google Analytics 4 衡量 ID
# ANALYTICS_BAIDU_ID: xxxxxxxxxxxxxxxxxxxxxxxxxxxx # 百度统计站点 ID
+9
View File
@@ -0,0 +1,9 @@
node_modules
.next
out
.source
coverage
*.log
.env*
!.env.example
.DS_Store
+26
View File
@@ -0,0 +1,26 @@
# deps
/node_modules
# generated content
.source
# test & build
/coverage
/.next/
/out/
/build
*.tsbuildinfo
# misc
.DS_Store
*.pem
/.pnp
.pnp.js
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# others
.env*.local
.vercel
next-env.d.ts
+28
View File
@@ -0,0 +1,28 @@
FROM oven/bun:1.3.13 AS docs-build
WORKDIR /app
COPY docs/package.json docs/bun.lock ./
RUN --mount=type=cache,target=/root/.bun/install/cache bun install --frozen-lockfile --ignore-scripts --cache-dir=/root/.bun/install/cache
COPY docs ./
COPY CHANGELOG.md /CHANGELOG.md
RUN bun run postinstall
RUN bun run build
FROM node:22-bookworm-slim
WORKDIR /app
ENV NODE_ENV=production
ENV HOSTNAME=0.0.0.0
ENV PORT=3000
RUN groupadd --system --gid 1001 nodejs && useradd --system --uid 1001 --gid nodejs nextjs
COPY --from=docs-build --chown=nextjs:nodejs /app/public ./public
COPY --from=docs-build --chown=nextjs:nodejs /app/index.md ./index.md
COPY --from=docs-build --chown=nextjs:nodejs /app/content ./content
COPY --from=docs-build --chown=nextjs:nodejs /app/.source ./.source
COPY --from=docs-build --chown=nextjs:nodejs /app/source.config.ts ./source.config.ts
COPY --from=docs-build --chown=nextjs:nodejs /CHANGELOG.md /CHANGELOG.md
COPY --from=docs-build --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=docs-build --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
CMD ["node", "server.js"]
+52
View File
@@ -0,0 +1,52 @@
# docs
This is a Next.js application generated with
[Create Fumadocs](https://github.com/fuma-nama/fumadocs).
It runs as a server-backed Next.js docs site and is configured for standalone
output. Route handlers such as search and LLM text remain available at runtime.
Run development server:
```bash
bun run dev
```
Build and run local production server:
```bash
bun run build
bun run start
```
Run the published image with Docker Compose:
```bash
docker compose up -d
```
Or build locally with Docker Compose:
```bash
docker compose -f docker-compose.local.yml up -d --build
```
## Explore
In the project, you can see:
- `lib/source.ts`: Code for content source adapter, `loader()` provides the interface to access your content.
- `lib/layout.shared.tsx`: Shared options for layouts, optional but preferred to keep.
| Route | Description |
| ------------------------- | ------------------------------------------------------ |
| `app/(home)` | The route group for your landing page and other pages. |
| `app/docs` | The documentation layout and pages. |
| `app/api/search/route.ts` | The Route Handler for search. |
### Fumadocs MDX
A `source.config.ts` config file has been included, you can customise different
options like frontmatter schema.
Read the [Introduction](https://fumadocs.dev/docs/mdx) for further details.
-19
View File
@@ -1,19 +0,0 @@
# 前后端接口响应约定
后端业务接口统一返回 JSON
```json
{
"code": 0,
"data": {},
"msg": "ok"
}
```
- `code`: 业务状态码,`0` 表示成功,非 `0` 表示失败。
- `data`: 业务数据。失败时通常为 `null`
- `msg`: 响应消息。成功默认为 `ok`,失败时放错误原因。
前端请求逻辑以 `code` 判断业务是否成功。当前后端业务失败也会返回 HTTP 200,前端不要只依赖 HTTP 状态码判断结果。
接口连接失败、服务不可达、返回体不是约定 JSON 时,前端按网络或接口异常处理。
-195
View File
@@ -1,195 +0,0 @@
# 后端数据库说明
本文档只记录后端当前已经使用的主要数据表。
## 数据库
后端使用 GORM 管理数据库连接和表结构迁移。
支持的存储驱动:
- `sqlite`
- `mysql`
- `postgresql`
当前启动时执行 `AutoMigrate`,自动维护以下表:
- `users`
- `credit_logs`
- `prompts`
- `assets`
- `settings`
后续新增表时再同步补充本文档,未实际使用的规划表不提前写入。
### users
系统用户表。用户基础信息、角色、算力点余额和第三方登录标识放在该表中。
| 字段 | 类型 | 说明 |
|-----------------|--------|--------------------------|
| `id` | string | 主键 |
| `username` | string | 用户名,唯一索引 |
| `password` | string | 密码哈希 |
| `email` | string | 邮箱 |
| `display_name` | string | 昵称 |
| `avatar_url` | string | 头像地址 |
| `role` | string | 角色:`user``admin` |
| `credits` | number | 算力点余额 |
| `aff_code` | string | 用户自己的邀请码,唯一索引 |
| `aff_count` | number | 已邀请用户数量,冗余统计字段 |
| `inviter_id` | string | 邀请人用户 ID |
| `github_id` | string | GitHub 用户 ID |
| `linux_do_id` | string | Linux.do 用户 ID |
| `wechat_id` | string | 微信用户 ID |
| `status` | string | 用户状态:`active``ban` |
| `last_login_at` | string | 最近登录时间 |
| `extra` | json | 扩展信息,第三方资料按平台命名空间保存,如 `linuxDo` |
| `created_at` | string | 创建时间 |
| `updated_at` | string | 更新时间 |
### prompts
提示词表。用于保存公开提示词、内置 GitHub 系统提示词、分类和预览内容。
| 字段 | 类型 | 说明 |
|--------------|--------|------------------------------|
| `id` | string | 主键 |
| `title` | string | 标题 |
| `cover_url` | string | 封面图 |
| `prompt` | string | 提示词内容 |
| `tags` | json | 标签列表 |
| `category` | string | 分类标识 |
| `preview` | text | Markdown 展示内容,可包含文本、图片、视频链接等 |
| `created_at` | string | 创建时间 |
| `updated_at` | string | 更新时间 |
`github_url` 仅用于接口返回,不写入数据库。
### assets
素材表。当前用于后台素材库。
| 字段 | 类型 | 说明 |
|------------------|--------|-------------------------------|
| `id` | string | 主键 |
| `title` | string | 标题 |
| `type` | string | 素材类型:`text``image``video` 等 |
| `cover_url` | string | 封面图 |
| `tags` | json | 标签列表 |
| `category` | string | 分类标识 |
| `description` | string | 描述 |
| `content` | text | 文本或 Markdown 内容 |
| `url` | string | 图片、视频等媒体地址 |
| `created_at` | string | 创建时间 |
| `updated_at` | string | 更新时间 |
### settings
系统配置表,只保存两行数据:`public` 放前端可读取的公开配置,`private` 放仅后端和管理员可读取的私有配置,配置值都用 JSON。
| 字段 | 类型 | 说明 |
|--------------|--------|-----------------------|
| `key` | string | 主键:`public``private` |
| `value` | json | 配置内容 |
| `created_at` | string | 创建时间 |
| `updated_at` | string | 更新时间 |
`public.value` 常放前端展示和可公开读取的配置,例如模型列表、登录开关等。
`private.value` 常放渠道密钥、登录密钥、后台内部开关等。
当前系统设置接口会按后端结构体序列化和反序列化已知字段;数据库 JSON 中额外存在的旧字段会被忽略。
`public.value` 当前字段:
| 字段 | 类型 | 说明 |
|-------------------|----------|----------------|
| `modelChannel` | object | 模型渠道公开配置组 |
| `auth` | object | 公开登录配置 |
`modelChannel` 当前字段:
| 字段 | 类型 | 说明 |
|-------------------|----------|----------------|
| `availableModels` | string[] | 系统可用模型列表 |
| `modelCosts` | object[] | 模型算力点配置 |
| `defaultModel` | string | 默认模型 |
| `defaultImageModel` | string | 默认图片模型 |
| `defaultVideoModel` | string | 默认视频模型 |
| `defaultTextModel` | string | 默认文本模型 |
| `systemPrompt` | string | 系统提示词 |
| `allowCustomChannel` | bool | 是否允许用户自定义渠道,默认允许,关闭后前端只提供走后端渠道的模式 |
`modelCosts` 每项字段:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `model` | string | 模型名称 |
| `credits` | number | 每次后端模型接口调用前预扣的算力点,未配置默认不扣除 |
`auth.linuxDo` 当前字段:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `enabled` | bool | 是否开启 Linux.do 登录 |
`private.value` 当前字段:
| 字段 | 类型 | 说明 |
|------------|----------|----------|
| `channels` | object[] | 模型渠道配置列表 |
| `promptSync` | object | GitHub 远程提示词定时同步配置 |
| `auth` | object | 私有登录配置 |
`channels` 每项字段:
| 字段 | 类型 | 说明 |
|----------|----------|----------|
| `protocol` | string | 协议,当前支持 `openai` |
| `name` | string | 渠道名称 |
| `baseUrl` | string | 渠道接口地址 |
| `apiKey` | string | 渠道密钥 |
| `models` | string[] | 渠道可用模型列表 |
| `weight` | number | 渠道权重,同一模型命中多个渠道时按权重随机 |
| `enabled` | bool | 是否启用 |
| `remark` | string | 备注 |
`promptSync` 字段:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `enabled` | bool | 是否开启定时同步,默认开启 |
| `cron` | string | Cron 表达式,默认每 5 分钟 |
`auth.linuxDo` 当前字段:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `clientId` | string | Linux.do OAuth App Client ID |
| `clientSecret` | string | Linux.do OAuth App Client Secret,后台返回时隐藏 |
后端请求模型时,先按模型名筛选启用且包含该模型的渠道,再按 `weight` 加权随机选择一个渠道。
### credit_logs
用户算力点变更流水表。当前记录后台手动调整、模型调用预扣和模型调用失败返还。
| 字段 | 类型 | 说明 |
|--------------|--------|--------------------------|
| `id` | string | 主键 |
| `user_id` | string | 关联用户 ID |
| `type` | string | 类型:`admin_adjust``ai_consume``ai_refund` |
| `amount` | number | 本次变动数量,增加为正,扣减为负 |
| `balance` | number | 变动后的用户算力点余额 |
| `related_id` | string | 关联业务 ID,可为空 |
| `remark` | string | 备注 |
| `extra` | json | 扩展信息 |
| `created_at` | string | 创建时间 |
`type` 当前取值:
| 值 | 说明 |
| --- | --- |
| `admin_adjust` | 后台手动调整 |
| `ai_consume` | 调用后端模型接口消费 |
| `ai_refund` | 后端模型接口调用失败返还 |
+936
View File
@@ -0,0 +1,936 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "docs",
"dependencies": {
"@orama/orama": "^3.1.18",
"fumadocs-core": "16.9.3",
"fumadocs-mdx": "15.0.10",
"fumadocs-ui": "16.9.3",
"lucide-react": "^1.17.0",
"next": "16.2.6",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"tailwind-merge": "^3.6.0",
},
"devDependencies": {
"@tailwindcss/postcss": "^4.3.0",
"@types/mdx": "^2.0.13",
"@types/node": "^25.9.1",
"@types/react": "^19.2.15",
"@types/react-dom": "^19.2.3",
"postcss": "^8.5.15",
"serve": "^14.2.6",
"tailwindcss": "^4.3.0",
"typescript": "^6.0.3",
},
},
},
"packages": {
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
"@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.28.0", "", { "os": "android", "cpu": "arm" }, "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.0", "", { "os": "android", "cpu": "arm64" }, "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.28.0", "", { "os": "android", "cpu": "x64" }, "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.0", "", { "os": "linux", "cpu": "ia32" }, "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.0", "", { "os": "linux", "cpu": "none" }, "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.0", "", { "os": "linux", "cpu": "none" }, "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.0", "", { "os": "linux", "cpu": "none" }, "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.0", "", { "os": "linux", "cpu": "x64" }, "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ=="],
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.0", "", { "os": "none", "cpu": "arm64" }, "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.0", "", { "os": "none", "cpu": "x64" }, "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw=="],
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.0", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA=="],
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.0", "", { "os": "none", "cpu": "arm64" }, "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.0", "", { "os": "sunos", "cpu": "x64" }, "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.0", "", { "os": "win32", "cpu": "x64" }, "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw=="],
"@floating-ui/core": ["@floating-ui/core@1.7.5", "", { "dependencies": { "@floating-ui/utils": "^0.2.11" } }, "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ=="],
"@floating-ui/dom": ["@floating-ui/dom@1.7.6", "", { "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" } }, "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ=="],
"@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.8", "", { "dependencies": { "@floating-ui/dom": "^1.7.6" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A=="],
"@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="],
"@fumadocs/tailwind": ["@fumadocs/tailwind@0.0.5", "", { "peerDependencies": { "@tailwindcss/oxide": "^4.0.0", "tailwindcss": "^4.0.0" }, "optionalPeers": ["@tailwindcss/oxide", "tailwindcss"] }, "sha512-ENKPWUDRmriccsrUDE4bDBq3FNr/ms3BP2rWlsAEMV1yP23pcCaan+ceGfeBUsAQjw7sj9Q3R4Kl3g/TCStPzQ=="],
"@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="],
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="],
"@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="],
"@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="],
"@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="],
"@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="],
"@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="],
"@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="],
"@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="],
"@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="],
"@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="],
"@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="],
"@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="],
"@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="],
"@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="],
"@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="],
"@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="],
"@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="],
"@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="],
"@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="],
"@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="],
"@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="],
"@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="],
"@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="],
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="],
"@next/env": ["@next/env@16.2.6", "", {}, "sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw=="],
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.2.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg=="],
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.2.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ=="],
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.2.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w=="],
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.2.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA=="],
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.2.6", "", { "os": "linux", "cpu": "x64" }, "sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw=="],
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.2.6", "", { "os": "linux", "cpu": "x64" }, "sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g=="],
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.2.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg=="],
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.2.6", "", { "os": "win32", "cpu": "x64" }, "sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA=="],
"@orama/orama": ["@orama/orama@3.1.18", "", {}, "sha512-a61ljmRVVyG5MC/698C8/FfFDw5a8LOIvyOLW5fztgUXqUpc1jOfQzOitSCbge657OgXXThmY3Tk8fpiDb4UcA=="],
"@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="],
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
"@radix-ui/react-accordion": ["@radix-ui/react-accordion@1.2.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA=="],
"@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="],
"@radix-ui/react-collapsible": ["@radix-ui/react-collapsible@1.1.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA=="],
"@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="],
"@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
"@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
"@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="],
"@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="],
"@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="],
"@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="],
"@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="],
"@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
"@radix-ui/react-navigation-menu": ["@radix-ui/react-navigation-menu@1.2.14", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w=="],
"@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA=="],
"@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="],
"@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="],
"@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="],
"@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="],
"@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.10", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A=="],
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="],
"@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="],
"@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="],
"@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="],
"@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="],
"@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="],
"@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
"@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ=="],
"@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.1", "", { "dependencies": { "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w=="],
"@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="],
"@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.3", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug=="],
"@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="],
"@shikijs/core": ["@shikijs/core@4.1.0", "", { "dependencies": { "@shikijs/primitive": "4.1.0", "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-jLJtSJeuFffqX6/inRE1zqU5aFv2hrszvYgq3OjbAgFRZiWv7abKMDdQzYxuSDfmUPQozZvI/kuy6VMTvnvqTQ=="],
"@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.1.0", "", { "dependencies": { "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.6" } }, "sha512-YquhawCUgaBfhsS72e2Y/dI59gCBNPHu3fEO/tvLaXrTssxZrY5ddjtNLTwndrMgPo8b3IscE+xoICDzpTmlFQ=="],
"@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@4.1.0", "", { "dependencies": { "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-axLpjVs45YBvvINa+dJF+NPW+KtFkNXsFr4SDw2BMj9GdeMnGxVB9PQb2xXlJYovslt/nz6giedAyOANkfc7hg=="],
"@shikijs/langs": ["@shikijs/langs@4.1.0", "", { "dependencies": { "@shikijs/types": "4.1.0" } }, "sha512-nwOMruEkbgdZfQ/b8CgpNBVOpvG1k0N5tbmgiFeqsan401+x3ILqlzZJowSla4Agmq4hG2Uf2wh5jLTEhR8VSg=="],
"@shikijs/primitive": ["@shikijs/primitive@4.1.0", "", { "dependencies": { "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-zx2/2Uwj2q9X3KSyYREEhXO23xBw5WUhP4orK2lE4r+t9JGITmEe0JH+wPmJhqHpOT2bRRs6lAL945+LDvOAGw=="],
"@shikijs/themes": ["@shikijs/themes@4.1.0", "", { "dependencies": { "@shikijs/types": "4.1.0" } }, "sha512-emCcTnUM7yO2wltYbaxm+yLvcCI4+h8XBKc4KmJ7EZUXoSGjcCHifkI//R4OFit9ewpg7H2/9tjOuXrT2v/Knw=="],
"@shikijs/types": ["@shikijs/types@4.1.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3EQWX54fMpniOrDblzAhiwiJwpiTMW6+B9DWyUd9ska483tbayFYuw47UxwuPknI31bKnySfVQ/QW+jFL4rFdA=="],
"@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
"@tailwindcss/node": ["@tailwindcss/node@4.3.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.21.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.0" } }, "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g=="],
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.0", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.0", "@tailwindcss/oxide-darwin-arm64": "4.3.0", "@tailwindcss/oxide-darwin-x64": "4.3.0", "@tailwindcss/oxide-freebsd-x64": "4.3.0", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", "@tailwindcss/oxide-linux-x64-musl": "4.3.0", "@tailwindcss/oxide-wasm32-wasi": "4.3.0", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" } }, "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg=="],
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.0", "", { "os": "android", "cpu": "arm64" }, "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng=="],
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ=="],
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA=="],
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ=="],
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0", "", { "os": "linux", "cpu": "arm" }, "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA=="],
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg=="],
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ=="],
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ=="],
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg=="],
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.0", "", { "dependencies": { "@emnapi/core": "^1.10.0", "@emnapi/runtime": "^1.10.0", "@emnapi/wasi-threads": "^1.2.1", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA=="],
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ=="],
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA=="],
"@tailwindcss/postcss": ["@tailwindcss/postcss@4.3.0", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.3.0", "@tailwindcss/oxide": "4.3.0", "postcss": "^8.5.10", "tailwindcss": "4.3.0" } }, "sha512-Jm05Tjx+9yCLGv5qw1c+84Psds8MnyrEQYCB+FFk2lgGiUjlRqdxke4mVTuYrj2xnVZqKim2Apr5ySuQRYAw/w=="],
"@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="],
"@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
"@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="],
"@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
"@types/mdx": ["@types/mdx@2.0.13", "", {}, "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw=="],
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
"@types/node": ["@types/node@25.9.1", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg=="],
"@types/react": ["@types/react@19.2.15", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q=="],
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.1", "", {}, "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ=="],
"@zeit/schemas": ["@zeit/schemas@2.36.0", "", {}, "sha512-7kjMwcChYEzMKjeex9ZFXkt1AyNov9R5HZtjBKVsmVpw7pa7ZtlCGvCBC2vnnXctaYN+aRI61HjIqeetZW5ROg=="],
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
"ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
"ansi-align": ["ansi-align@3.0.1", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="],
"ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
"ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
"arch": ["arch@2.2.0", "", {}, "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ=="],
"arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="],
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
"aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="],
"astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="],
"bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.33", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw=="],
"boxen": ["boxen@7.0.0", "", { "dependencies": { "ansi-align": "^3.0.1", "camelcase": "^7.0.0", "chalk": "^5.0.1", "cli-boxes": "^3.0.0", "string-width": "^5.1.2", "type-fest": "^2.13.0", "widest-line": "^4.0.1", "wrap-ansi": "^8.0.1" } }, "sha512-j//dBVuyacJbvW+tvZ9HuH03fZ46QcaKvvhZickZqtB271DxJ7SNRSNxrV/dZX0085m7hISRZWbzWlJvx/rHSg=="],
"brace-expansion": ["brace-expansion@1.1.15", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg=="],
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
"camelcase": ["camelcase@7.0.1", "", {}, "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw=="],
"caniuse-lite": ["caniuse-lite@1.0.30001793", "", {}, "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA=="],
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
"chalk": ["chalk@5.0.1", "", {}, "sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w=="],
"chalk-template": ["chalk-template@0.4.0", "", { "dependencies": { "chalk": "^4.1.2" } }, "sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg=="],
"character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
"character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="],
"character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="],
"character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="],
"chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
"class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
"cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="],
"client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
"clipboardy": ["clipboardy@3.0.0", "", { "dependencies": { "arch": "^2.2.0", "execa": "^5.1.1", "is-wsl": "^2.2.0" } }, "sha512-Su+uU5sr1jkUy1sGRpLKjKrvEOVXgSgiSInwa/qeID6aJ07yh+5NWc3h2QfjHjBnfX4LhtFcuAWKUsJ3r+fjbg=="],
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
"collapse-white-space": ["collapse-white-space@2.1.0", "", {}, "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
"compressible": ["compressible@2.0.18", "", { "dependencies": { "mime-db": ">= 1.43.0 < 2" } }, "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg=="],
"compression": ["compression@1.8.1", "", { "dependencies": { "bytes": "3.1.2", "compressible": "~2.0.18", "debug": "2.6.9", "negotiator": "~0.6.4", "on-headers": "~1.1.0", "safe-buffer": "5.2.1", "vary": "~1.1.2" } }, "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w=="],
"compute-scroll-into-view": ["compute-scroll-into-view@3.1.1", "", {}, "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw=="],
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
"content-disposition": ["content-disposition@0.5.2", "", {}, "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA=="],
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
"debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
"decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="],
"deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="],
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
"eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
"emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
"enhanced-resolve": ["enhanced-resolve@5.22.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww=="],
"entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
"esast-util-from-estree": ["esast-util-from-estree@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "unist-util-position-from-estree": "^2.0.0" } }, "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ=="],
"esast-util-from-js": ["esast-util-from-js@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "acorn": "^8.0.0", "esast-util-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw=="],
"esbuild": ["esbuild@0.28.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.0", "@esbuild/android-arm": "0.28.0", "@esbuild/android-arm64": "0.28.0", "@esbuild/android-x64": "0.28.0", "@esbuild/darwin-arm64": "0.28.0", "@esbuild/darwin-x64": "0.28.0", "@esbuild/freebsd-arm64": "0.28.0", "@esbuild/freebsd-x64": "0.28.0", "@esbuild/linux-arm": "0.28.0", "@esbuild/linux-arm64": "0.28.0", "@esbuild/linux-ia32": "0.28.0", "@esbuild/linux-loong64": "0.28.0", "@esbuild/linux-mips64el": "0.28.0", "@esbuild/linux-ppc64": "0.28.0", "@esbuild/linux-riscv64": "0.28.0", "@esbuild/linux-s390x": "0.28.0", "@esbuild/linux-x64": "0.28.0", "@esbuild/netbsd-arm64": "0.28.0", "@esbuild/netbsd-x64": "0.28.0", "@esbuild/openbsd-arm64": "0.28.0", "@esbuild/openbsd-x64": "0.28.0", "@esbuild/openharmony-arm64": "0.28.0", "@esbuild/sunos-x64": "0.28.0", "@esbuild/win32-arm64": "0.28.0", "@esbuild/win32-ia32": "0.28.0", "@esbuild/win32-x64": "0.28.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw=="],
"escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
"estree-util-attach-comments": ["estree-util-attach-comments@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw=="],
"estree-util-build-jsx": ["estree-util-build-jsx@3.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-walker": "^3.0.0" } }, "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ=="],
"estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="],
"estree-util-scope": ["estree-util-scope@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0" } }, "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ=="],
"estree-util-to-js": ["estree-util-to-js@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "astring": "^1.8.0", "source-map": "^0.7.0" } }, "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg=="],
"estree-util-value-to-estree": ["estree-util-value-to-estree@3.5.0", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-aMV56R27Gv3QmfmF1MY12GWkGzzeAezAX+UplqHVASfjc9wNzI/X6hC0S9oxq61WT4aQesLGslWP9tKk6ghRZQ=="],
"estree-util-visit": ["estree-util-visit@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/unist": "^3.0.0" } }, "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww=="],
"estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
"execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="],
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-uri": ["fast-uri@3.1.2", "", {}, "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"framer-motion": ["framer-motion@12.40.0", "", { "dependencies": { "motion-dom": "^12.40.0", "motion-utils": "^12.39.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-uaBd3qC1v3KQqBEjwTUd183K6PbS+j0yR9w9VmEOLWA/tnUcSn8Xa3uck7t4dgpDoUss8xQTcj8W2L07lrnLFg=="],
"fumadocs-core": ["fumadocs-core@16.9.3", "", { "dependencies": { "@orama/orama": "^3.1.18", "estree-util-value-to-estree": "^3.5.0", "github-slugger": "^2.0.0", "hast-util-to-estree": "^3.1.3", "hast-util-to-jsx-runtime": "^2.3.6", "js-yaml": "^4.1.1", "mdast-util-mdx": "^3.0.0", "mdast-util-to-markdown": "^2.1.2", "remark": "^15.0.1", "remark-gfm": "^4.0.1", "remark-rehype": "^11.1.2", "scroll-into-view-if-needed": "^3.1.0", "shiki": "^4.1.0", "tinyglobby": "^0.2.16", "unified": "^11.0.5", "unist-util-visit": "^5.1.0", "vfile": "^6.0.3" }, "peerDependencies": { "@mdx-js/mdx": "*", "@mixedbread/sdk": "0.x.x", "@orama/core": "1.x.x", "@oramacloud/client": "2.x.x", "@tanstack/react-router": "1.x.x", "@types/estree-jsx": "*", "@types/hast": "*", "@types/mdast": "*", "@types/react": "*", "algoliasearch": "5.x.x", "flexsearch": "*", "lucide-react": "*", "next": "16.x.x", "react": "^19.2.0", "react-dom": "^19.2.0", "react-router": "7.x.x", "waku": "*", "zod": "4.x.x" }, "optionalPeers": ["@mdx-js/mdx", "@mixedbread/sdk", "@orama/core", "@oramacloud/client", "@tanstack/react-router", "@types/estree-jsx", "@types/hast", "@types/mdast", "@types/react", "algoliasearch", "flexsearch", "lucide-react", "next", "react", "react-dom", "react-router", "waku", "zod"] }, "sha512-8RVzKnzBJR5o+tJCccY28ntekfMQYBoYiz7alnYb/d9YJc+XpnsINzTl63lQ1eBMZ9gdhm2MqRtgUjh/8rUrbw=="],
"fumadocs-mdx": ["fumadocs-mdx@15.0.10", "", { "dependencies": { "@mdx-js/mdx": "^3.1.1", "@standard-schema/spec": "^1.1.0", "chokidar": "^5.0.0", "esbuild": "^0.28.0", "estree-util-value-to-estree": "^3.5.0", "js-yaml": "^4.1.1", "mdast-util-mdx": "^3.0.0", "picocolors": "^1.1.1", "picomatch": "^4.0.4", "tinyexec": "^1.2.2", "tinyglobby": "^0.2.16", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.1.0", "vfile": "^6.0.3", "zod": "^4.4.3" }, "peerDependencies": { "@types/mdast": "*", "@types/mdx": "*", "@types/react": "*", "fumadocs-core": "^16.7.0", "mdast-util-directive": "*", "next": "^15.3.0 || ^16.0.0", "react": "^19.2.0", "rolldown": "*", "vite": "7.x.x || 8.x.x" }, "optionalPeers": ["@types/mdast", "@types/mdx", "@types/react", "mdast-util-directive", "next", "react", "rolldown", "vite"], "bin": { "fumadocs-mdx": "./bin.js" } }, "sha512-kH3S7ESS9yXTAaCkA8dDugsCK/MbnpgyZ5qBEL7cWoavV0O/T4+4YTYFkvNknz7cw+T/r+OG0p2BvlVhkk4fww=="],
"fumadocs-ui": ["fumadocs-ui@16.9.3", "", { "dependencies": { "@fumadocs/tailwind": "0.0.5", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-direction": "^1.1.1", "@radix-ui/react-navigation-menu": "^1.2.14", "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-presence": "^1.1.5", "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-tabs": "^1.1.13", "class-variance-authority": "^0.7.1", "lucide-react": "^1.17.0", "motion": "^12.40.0", "next-themes": "^0.4.6", "react-remove-scroll": "^2.7.2", "rehype-raw": "^7.0.0", "scroll-into-view-if-needed": "^3.1.0", "shiki": "^4.1.0", "tailwind-merge": "^3.6.0", "unist-util-visit": "^5.1.0" }, "peerDependencies": { "@takumi-rs/image-response": "*", "@types/mdx": "*", "@types/react": "*", "fumadocs-core": "16.9.3", "next": "16.x.x", "react": "^19.2.0", "react-dom": "^19.2.0" }, "optionalPeers": ["@takumi-rs/image-response", "@types/mdx", "@types/react", "next"] }, "sha512-eoVKj1H+ATut0su+WIoPWBLRqzPMGD0hekIBr4GopWvUg1lS997HL4kP+Leyf+3CYlZtFgyXb6ylbvRLFtEj6Q=="],
"get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
"get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="],
"github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="],
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
"hast-util-from-parse5": ["hast-util-from-parse5@8.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "hastscript": "^9.0.0", "property-information": "^7.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" } }, "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg=="],
"hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="],
"hast-util-raw": ["hast-util-raw@9.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-from-parse5": "^8.0.0", "hast-util-to-parse5": "^8.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "parse5": "^7.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw=="],
"hast-util-to-estree": ["hast-util-to-estree@3.1.3", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-attach-comments": "^3.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w=="],
"hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="],
"hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="],
"hast-util-to-parse5": ["hast-util-to-parse5@8.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA=="],
"hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],
"hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="],
"html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],
"human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="],
"ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="],
"inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="],
"is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="],
"is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="],
"is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="],
"is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="],
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
"is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="],
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
"is-port-reachable": ["is-port-reachable@4.0.0", "", {}, "sha512-9UoipoxYmSk6Xy7QFgRv2HDyaysmgSG75TFQs6S+3pDM7ZhKTF/bskZV+0UlABHzKjNVhPjYCLfeZUEg1wXxig=="],
"is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
"is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="],
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="],
"js-yaml": ["js-yaml@4.2.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw=="],
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
"longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
"lucide-react": ["lucide-react@1.17.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-9FA9evdox/JQL5PT57fdA1x/yg8T7knJ98+zjTL3UfKza6pflQUUh3XtaQIHKvnsJw1lmsEyHVlt5jchYxOQ5w=="],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"markdown-extensions": ["markdown-extensions@2.0.0", "", {}, "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q=="],
"markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
"mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="],
"mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="],
"mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="],
"mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="],
"mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="],
"mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="],
"mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="],
"mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="],
"mdast-util-mdx": ["mdast-util-mdx@3.0.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w=="],
"mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="],
"mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.2.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q=="],
"mdast-util-mdxjs-esm": ["mdast-util-mdxjs-esm@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg=="],
"mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="],
"mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="],
"mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="],
"mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="],
"merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="],
"micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="],
"micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="],
"micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="],
"micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="],
"micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="],
"micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="],
"micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="],
"micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="],
"micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="],
"micromark-extension-mdx-expression": ["micromark-extension-mdx-expression@3.0.1", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q=="],
"micromark-extension-mdx-jsx": ["micromark-extension-mdx-jsx@3.0.2", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ=="],
"micromark-extension-mdx-md": ["micromark-extension-mdx-md@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ=="],
"micromark-extension-mdxjs": ["micromark-extension-mdxjs@3.0.0", "", { "dependencies": { "acorn": "^8.0.0", "acorn-jsx": "^5.0.0", "micromark-extension-mdx-expression": "^3.0.0", "micromark-extension-mdx-jsx": "^3.0.0", "micromark-extension-mdx-md": "^2.0.0", "micromark-extension-mdxjs-esm": "^3.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ=="],
"micromark-extension-mdxjs-esm": ["micromark-extension-mdxjs-esm@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-position-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A=="],
"micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="],
"micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="],
"micromark-factory-mdx-expression": ["micromark-factory-mdx-expression@2.0.3", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-position-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ=="],
"micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="],
"micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="],
"micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="],
"micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="],
"micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="],
"micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="],
"micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="],
"micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="],
"micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="],
"micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="],
"micromark-util-events-to-acorn": ["micromark-util-events-to-acorn@2.0.3", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg=="],
"micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="],
"micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="],
"micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="],
"micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="],
"micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="],
"micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="],
"micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
"mime-types": ["mime-types@2.1.18", "", { "dependencies": { "mime-db": "~1.33.0" } }, "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ=="],
"mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
"minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
"motion": ["motion@12.40.0", "", { "dependencies": { "framer-motion": "^12.40.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-yjrHUrBFW6kQvjJwRsoiPSAhC5tRwRqNGJWmiJ4CrGnbKp0V88AdzkhBmDoqIsIPfarOe0Uddd37Xq43/gIocA=="],
"motion-dom": ["motion-dom@12.40.0", "", { "dependencies": { "motion-utils": "^12.39.0" } }, "sha512-HxU3ZaBwNPVQUBQf1xxgq+7JrPNZvjLVxgbpEZL7RrWJnsxOf0/OM+yrHG9ogLQ31Do/r57Oz2gQWPK+6q62mg=="],
"motion-utils": ["motion-utils@12.39.0", "", {}, "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ=="],
"ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
"nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="],
"negotiator": ["negotiator@0.6.4", "", {}, "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w=="],
"next": ["next@16.2.6", "", { "dependencies": { "@next/env": "16.2.6", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.2.6", "@next/swc-darwin-x64": "16.2.6", "@next/swc-linux-arm64-gnu": "16.2.6", "@next/swc-linux-arm64-musl": "16.2.6", "@next/swc-linux-x64-gnu": "16.2.6", "@next/swc-linux-x64-musl": "16.2.6", "@next/swc-win32-arm64-msvc": "16.2.6", "@next/swc-win32-x64-msvc": "16.2.6", "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw=="],
"next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="],
"npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="],
"on-headers": ["on-headers@1.1.0", "", {}, "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A=="],
"onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
"oniguruma-parser": ["oniguruma-parser@0.12.2", "", {}, "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw=="],
"oniguruma-to-es": ["oniguruma-to-es@4.3.6", "", { "dependencies": { "oniguruma-parser": "^0.12.2", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA=="],
"parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="],
"parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
"path-is-inside": ["path-is-inside@1.0.2", "", {}, "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
"path-to-regexp": ["path-to-regexp@3.3.0", "", {}, "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
"postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="],
"property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
"range-parser": ["range-parser@1.2.0", "", {}, "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A=="],
"rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="],
"react": ["react@19.2.6", "", {}, "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q=="],
"react-dom": ["react-dom@19.2.6", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.6" } }, "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g=="],
"react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="],
"react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
"react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
"readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],
"recma-build-jsx": ["recma-build-jsx@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-build-jsx": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew=="],
"recma-jsx": ["recma-jsx@1.0.1", "", { "dependencies": { "acorn-jsx": "^5.0.0", "estree-util-to-js": "^2.0.0", "recma-parse": "^1.0.0", "recma-stringify": "^1.0.0", "unified": "^11.0.0" }, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w=="],
"recma-parse": ["recma-parse@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "esast-util-from-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ=="],
"recma-stringify": ["recma-stringify@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-to-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g=="],
"regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="],
"regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="],
"regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="],
"registry-auth-token": ["registry-auth-token@3.3.2", "", { "dependencies": { "rc": "^1.1.6", "safe-buffer": "^5.0.1" } }, "sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ=="],
"registry-url": ["registry-url@3.1.0", "", { "dependencies": { "rc": "^1.0.1" } }, "sha512-ZbgR5aZEdf4UKZVBPYIgaglBmSF2Hi94s2PcIHhRGFjKYu+chjJdYfHn4rt3hB6eCKLJ8giVIIfgMa1ehDfZKA=="],
"rehype-raw": ["rehype-raw@7.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-raw": "^9.0.0", "vfile": "^6.0.0" } }, "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww=="],
"rehype-recma": ["rehype-recma@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "hast-util-to-estree": "^3.0.0" } }, "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw=="],
"remark": ["remark@15.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A=="],
"remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="],
"remark-mdx": ["remark-mdx@3.1.1", "", { "dependencies": { "mdast-util-mdx": "^3.0.0", "micromark-extension-mdxjs": "^3.0.0" } }, "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg=="],
"remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="],
"remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="],
"remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="],
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
"scroll-into-view-if-needed": ["scroll-into-view-if-needed@3.1.0", "", { "dependencies": { "compute-scroll-into-view": "^3.0.2" } }, "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ=="],
"semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="],
"serve": ["serve@14.2.6", "", { "dependencies": { "@zeit/schemas": "2.36.0", "ajv": "8.18.0", "arg": "5.0.2", "boxen": "7.0.0", "chalk": "5.0.1", "chalk-template": "0.4.0", "clipboardy": "3.0.0", "compression": "1.8.1", "is-port-reachable": "4.0.0", "serve-handler": "6.1.7", "update-check": "1.5.4" }, "bin": { "serve": "build/main.js" } }, "sha512-QEjUSA+sD4Rotm1znR8s50YqA3kYpRGPmtd5GlFxbaL9n/FdUNbqMhxClqdditSk0LlZyA/dhud6XNRTOC9x2Q=="],
"serve-handler": ["serve-handler@6.1.7", "", { "dependencies": { "bytes": "3.0.0", "content-disposition": "0.5.2", "mime-types": "2.1.18", "minimatch": "3.1.5", "path-is-inside": "1.0.2", "path-to-regexp": "3.3.0", "range-parser": "1.2.0" } }, "sha512-CinAq1xWb0vR3twAv9evEU8cNWkXCb9kd5ePAHUKJBkOsUpR1wt/CvGdeca7vqumL1U5cSaeVQ6zZMxiJ3yWsg=="],
"sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="],
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
"shiki": ["shiki@4.1.0", "", { "dependencies": { "@shikijs/core": "4.1.0", "@shikijs/engine-javascript": "4.1.0", "@shikijs/engine-oniguruma": "4.1.0", "@shikijs/langs": "4.1.0", "@shikijs/themes": "4.1.0", "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-l/ABZPUR5v70jI10EzqfMS/I96vjSGv2y0ihUV+WYFzv0EfvW4s54m0Lg8wCrrL+2IkwBzFTuxkZjPf8b2NX9Q=="],
"signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
"source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
"string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
"strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
"strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="],
"strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="],
"style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="],
"style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="],
"styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="],
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"tailwind-merge": ["tailwind-merge@3.6.0", "", {}, "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w=="],
"tailwindcss": ["tailwindcss@4.3.0", "", {}, "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q=="],
"tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="],
"tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="],
"tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
"trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"type-fest": ["type-fest@2.19.0", "", {}, "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA=="],
"typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
"undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
"unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="],
"unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="],
"unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="],
"unist-util-position-from-estree": ["unist-util-position-from-estree@2.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ=="],
"unist-util-remove-position": ["unist-util-remove-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q=="],
"unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
"unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="],
"unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="],
"update-check": ["update-check@1.5.4", "", { "dependencies": { "registry-auth-token": "3.3.2", "registry-url": "3.1.0" } }, "sha512-5YHsflzHP4t1G+8WGPlvKbJEbAJGCgw+Em+dGR1KmBUbr1J36SJBqlHLjR7oob7sco5hWHGQVcr9B2poIVDDTQ=="],
"use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
"use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="],
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
"vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="],
"vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
"web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"widest-line": ["widest-line@4.0.1", "", { "dependencies": { "string-width": "^5.0.1" } }, "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig=="],
"wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
"zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
"@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-popover/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="],
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="],
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"chalk-template/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"micromark/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"mime-types/mime-db": ["mime-db@1.33.0", "", {}, "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ=="],
"next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
"serve-handler/bytes": ["bytes@3.0.0", "", {}, "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw=="],
"ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"ansi-align/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"chalk-template/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"micromark/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
}
}
+29
View File
@@ -0,0 +1,29 @@
---
title: 商务合作
description: 商务合作、定制开发和项目接入说明
---
# 商务合作
如果你希望围绕无限画布做商业合作、私有化部署、二次开发或模型渠道接入,可以直接通过邮箱联系项目维护者。
## 适合沟通的方向
- 私有化部署、内网部署和团队版落地。
- 定制画布节点、生成流程、素材管理或后台能力。
- 接入自有 OpenAI 兼容接口、模型渠道或提示词仓库。
- 围绕 AI 图片、视频、文本生成工作流做联合方案。
- 商业授权、技术支持、交付咨询或联合方案评估。
## 建议提供的信息
- 你的业务场景和预期用户规模。
- 需要部署的环境、数据保存方式和安全要求。
- 需要接入的模型、渠道、支付或账号体系。
- 期望的交付范围、时间和预算区间。
## 联系方式
请直接发送邮件至 [1844025705@qq.com](mailto:1844025705@qq.com),并在邮件标题中注明“商务合作”。
如果涉及商业授权或私有化交付,请在邮件中说明计划使用方式和预期交付范围。
+22
View File
@@ -0,0 +1,22 @@
---
title: 贡献者协议
description: 贡献代码、文档或素材前需要了解的 CLA
---
# 贡献者协议
Contributor License Agreement,见 [CLA.md](https://github.com/basketikun/infinite-canvas/blob/main/CLA.md)。
向本仓库提交 Pull Request、补丁、文档、素材或其他贡献,即表示你同意该协议。维护者也可能要求你在 PR 中明确回复:
```text
I have read and agree to CLA.md.
```
## 贡献前请确认
- 你有权提交相关代码、文档或素材。
- 第三方内容已明确标注来源,并且授权方式与本项目兼容。
- 提交内容可以随项目以 AGPL-3.0 以及项目相关商业授权方式发布。
- 安全漏洞不要通过公开 Issue 直接披露细节,请按[漏洞提交](/docs/support/security)流程处理。
+36
View File
@@ -0,0 +1,36 @@
---
title: 开源协议
description: 无限画布开源协议和使用边界说明
---
# 开源协议
无限画布采用 GNU Affero General Public License v3.0AGPL-3.0)开源协议。你可以在遵守协议要求的前提下自由使用、复制、修改和分发本项目。
## 项目初衷
无限画布保持开源,希望让更多人可以查看和修改代码,不被固定 API 渠道限制,接入自己的 API 与模型渠道,用画布组织提示词、参考图、素材和生成结果,搭建适合自己的 AI 创作工作流。
## 使用建议
如果你只是本地学习、研究或自用,可以直接按开源协议使用。
如果你计划将项目用于商业产品、私有化交付、SaaS 服务或长期团队内部系统,建议先确认自己的使用方式是否满足 AGPL-3.0 的开源要求。
如果你的使用场景无法继续开源修改后的代码,或需要闭源交付、商业授权、私有化部署支持、定制开发服务,请查看[商务合作](/docs/business/business)。
## 什么是 AGPL-3.0
AGPL-3.0 的核心要求可以通俗理解为:
- 开源义务:如果你使用了无限画布的 AGPL 代码,无论你或你的下游如何使用、修改、二次开发或分发,都必须把基于本项目形成的最终代码完整公开出来,不只是公开修改的部分,也不是换个框架重写一遍就能和原始代码脱离关系。
- 延续协议:基于本项目形成的衍生代码需要继续以 AGPL-3.0 协议开源,不能更换为其他协议,也不能把这部分代码改成闭源授权。
- 网络服务也要开源:即使你只是把基于本项目修改后的版本做成网站、SaaS 或其他网络服务,只要别人可以通过网络使用这个服务,也需要向这些用户提供对应源代码,并继续遵守 AGPL-3.0。
- 保留版权声明:不能删除原项目作者信息、版权声明、许可证声明和来源说明,需要让使用者知道代码从哪里来、遵循什么协议。
- 不能加额外限制:不能给基于本项目形成的 AGPL 代码增加额外限制,例如禁止别人再分发代码,或要求别人购买授权、服务、产品后才能使用这部分开源代码。
- 免责声明:项目按现状提供,作者不保证代码没有 bug,也不对使用本项目产生的结果、损失或风险负责。
本项目禁止闭源商用。如果你希望将无限画布用于商业项目,请尊重开源,严格遵循 AGPL-3.0 协议,继续开源对应代码,回馈开源社区。
+10
View File
@@ -0,0 +1,10 @@
{
"title": "商务合作",
"root": true,
"defaultOpen": true,
"pages": [
"license",
"cla",
"business"
]
}
@@ -1,3 +1,8 @@
---
title: 画布节点操作手册
description: 当前画布节点的主要用途与操作流程
---
# 画布节点操作手册 # 画布节点操作手册
本文档记录画布节点的主要用途和操作流程,方便使用和后续开发维护。当前先介绍文本节点。 本文档记录画布节点的主要用途和操作流程,方便使用和后续开发维护。当前先介绍文本节点。
@@ -19,8 +24,9 @@
- 当文本节点为空时,输入框用于填写想生成的文本内容;点击发送后,结果会回填到当前文本节点。 - 当文本节点为空时,输入框用于填写想生成的文本内容;点击发送后,结果会回填到当前文本节点。
- 当文本节点已有内容时,输入框用于填写想把本段文本修改成什么;点击发送后,会在右侧生成新的文本节点,并自动连接原节点和新节点。 - 当文本节点已有内容时,输入框用于填写想把本段文本修改成什么;点击发送后,会在右侧生成新的文本节点,并自动连接原节点和新节点。
- 输入内容可以手写,也可以从提示词库选择。 - 输入内容可以手写,也可以从提示词库选择。
- 输入框支持 `@` 引用已连接的图片、文本、视频、音频资源;`@` 引用图片时会在输入框内直接显示真实缩略图,发送时按当前连接自动编号交给模型。因此不需要再记忆「图片1 / 文本1」编号,画布节点右上角也不再显示资源角标。
- 对话框里的模型下拉来自全局配置里已拉取的模型列表;选择结果只作用于当前节点,不会修改其它节点或全局默认模型。 - 对话框里的模型下拉来自全局配置里已拉取的模型列表;选择结果只作用于当前节点,不会修改其它节点或全局默认模型。
- 如果下拉中没有模型,需要先打开配置弹窗拉取模型列表,并设置默认生图模型和默认文本模型。 - 如果下拉中没有模型,需要先打开配置弹窗拉取模型列表,并设置默认生图模型和默认文本模型。火山方舟 Agent Plan 若提示不支持 `/models`,请手动填写模型名。
### 用文本节点生成图片 ### 用文本节点生成图片
@@ -45,7 +51,11 @@
- 视频节点使用原生播放器展示内容,可在节点内直接播放、暂停和拖动进度。 - 视频节点使用原生播放器展示内容,可在节点内直接播放、暂停和拖动进度。
- 空视频节点下方对话框可输入提示词生成视频,结果会回填到当前节点。 - 空视频节点下方对话框可输入提示词生成视频,结果会回填到当前节点。
- 从文本、图片或配置节点创建视频生成时,会在右侧生成新的视频节点并自动连接。 - 从文本、图片或配置节点创建视频生成时,会在右侧生成新的视频节点并自动连接。
- 视频生成接口使用 OpenAI 兼容的 `POST /v1/videos`、`GET /v1/videos/{id}` 和 `GET /v1/videos/{id}/content` - 生成配置节点的视频模式会读取上游文本作为 prompt,读取上游图片作为参考图,读取上游视频作为参考视频,并在输入预览里显示参考视频
- 视频生成接口支持 OpenAI 风格的 `POST /v1/videos`、`GET /v1/videos/{id}` 和 `GET /v1/videos/{id}/content`。
- 使用火山方舟 Agent Plan / Seedance 2.0 时,Base URL 配置为 `https://ark.cn-beijing.volces.com/api/plan/v3`,模型名使用 Seedance 2.0 对应模型;系统会改用 `POST /contents/generations/tasks` 创建异步任务,并轮询 `GET /contents/generations/tasks/{id}`。
- Agent Plan 专属 `/api/plan/v3` 当前未提供 OpenAI `/models` 模型列表接口,配置弹窗不会伪造模型列表;请手动填写 `doubao-seedance-2.0` 或文档列出的其他可用模型。
- Seedance 参考视频更建议使用公网可访问 URL;本地素材由前端读取后传给兼容接口,是否可用取决于具体上游。
### 推荐流程 ### 推荐流程
@@ -1,3 +1,8 @@
---
title: 画布快捷键
description: 画布常用鼠标与键盘操作说明
---
# 画布快捷键 # 画布快捷键
本文档记录画布里常用的鼠标和键盘操作。 本文档记录画布里常用的鼠标和键盘操作。
@@ -29,7 +34,7 @@
- 拖入图片文件:上传图片到画布。 - 拖入图片文件:上传图片到画布。
- 导入图片按钮:从本地选择图片。 - 导入图片按钮:从本地选择图片。
- 素材库或我的素材:选择素材后插入画布。 - 我的素材:选择素材后插入画布。
## 撤销和重做范围 ## 撤销和重做范围
+9
View File
@@ -0,0 +1,9 @@
{
"title": "操作手册",
"root": true,
"defaultOpen": true,
"pages": [
"canvas-node-manual",
"canvas-shortcuts"
]
}
@@ -1,6 +1,11 @@
---
title: 画布数据结构
description: 画布本地存储、节点结构、媒体文件与清理机制
---
# 画布数据结构 # 画布数据结构
本文档说明当前画布在前端本地保存的数据结构、图片文件的存储和清理方式,以及后续接入后端存储时建议保持的兼容边界 本文档说明当前画布在前端本地保存的数据结构、图片文件的存储和清理方式。
## 当前存储位置 ## 当前存储位置
@@ -217,68 +222,3 @@ type UploadedImage = {
5. 删除时会同时 `URL.revokeObjectURL`,并从内存缓存 `objectUrls` 移除。 5. 删除时会同时 `URL.revokeObjectURL`,并从内存缓存 `objectUrls` 移除。
这套方式可以避免同一张图片被画布、素材或助手同时引用时误删。 这套方式可以避免同一张图片被画布、素材或助手同时引用时误删。
需要注意:
- 只要某个 JSON 结构里仍有 `storageKey`,清理逻辑就会认为图片仍被使用。
- `collectImageStorageKeys` 会递归扫描对象中的 `storageKey` 字段,字段值必须以 `image:` 开头才会被当成本地图片。
- 如果后续新增保存图片引用的数据结构,也要确保它能传入清理上下文,或者位于现有项目/素材结构内。
## 后端存储兼容建议
后续接入后端时,建议保持“画布 JSON”和“图片文件”分离:
- 画布表保存项目元信息和画布 JSON。
- 文件表保存图片文件、访问 URL、哈希、大小、MIME、宽高、归属用户等信息。
- 画布节点中继续保存轻量图片引用,不把图片二进制或 base64 写进画布 JSON。
建议图片引用逐步扩展为兼容本地和云端的结构:
```ts
type ImageRef = {
storageKey?: string;
fileId?: string;
url?: string;
width?: number;
height?: number;
bytes?: number;
mimeType?: string;
};
```
兼容规则:
- 本地旧数据:有 `storageKey`,无 `fileId`,通过 IndexedDB 读取。
- 已上传后端:有 `fileId`,展示时优先使用后端返回的签名 URL 或公开 URL。
- 迁移过渡期:可以同时保留 `storageKey` 和 `fileId`;确认云端文件可用后,再按清理策略删除本地 Blob。
- `content/dataUrl/coverUrl` 仍只作为当前可展示 URL,不作为稳定 ID。
建议读取优先级:
1. 有 `fileId`:向后端换取可访问 URL。
2. 有 `storageKey`:从本地 IndexedDB 生成 `blob:` URL。
3. 有旧 `data:image/...`:先写入本地图片存储,再视需要上传后端。
4. 只有普通 URL:直接展示,但不要假设可长期访问。
建议删除策略:
- 删除节点只删除画布 JSON 引用,不直接删除后端文件。
- 后端文件删除应按引用计数或定期扫描未引用文件处理。
- 保存到“我的素材”的图片,即使原画布节点删除,也应继续保留文件引用。
- 删除画布、删除素材、删除助手会话后,再由后端清理任务判断文件是否无人引用。
建议同步流程:
1. 前端保存画布 JSON 时,保持节点 ID、连线 ID、`storageKey/fileId` 不变。
2. 遇到只有 `storageKey` 的图片,后台同步前先上传 Blob,得到 `fileId`。
3. 上传成功后给对应图片引用补 `fileId` 和云端元信息。
4. 服务端保存更新后的画布 JSON。
5. 前端下次打开时优先走 `fileId`,本地 `storageKey` 只作为缓存或离线回退。
## 后续改动约束
- 不要把新生成的大图直接长期写入画布 JSON。
- 新增图片来源时统一走 `uploadImage` 或未来的文件上传服务。
- 新增图片引用字段时,应保留 `storageKey` 兼容旧本地数据。
- 新增清理入口时,要把仍需保留的画布、素材、助手数据传给 `cleanupUnusedImages`。
- 后端同步完成前,文档和 UI 不要写成已支持云同步。
@@ -0,0 +1,171 @@
---
title: 本地 Codex 连接画布原理
description: 说明 Canvas Agent、MCP、SSE 和浏览器画布之间如何交互
---
# 本地 Codex 连接画布原理
本地 Codex 连接画布的核心不是让 Codex 直接控制浏览器,也不是模拟鼠标点击或操作 DOM,而是在用户电脑上运行一个 `Canvas Agent` 作为桥接服务。
```txt
Codex / 浏览器侧边栏
|
v
本机 Canvas Agent
|
v
浏览器画布 React 状态
```
真正的画布数据仍然在浏览器里,节点、连线、选区和视口都由前端 React 状态维护。Codex 只能通过 `Canvas Agent` 暴露的工具协议发出“读取画布”或“执行画布操作”的请求。
## 两个使用方向
### 1. 本地 Codex 主动连接画布
这个方向的入口在 Codex。
```txt
本地 Codex
-> infinite-canvas MCP 工具
-> 本机 Canvas Agent
-> 浏览器画布
```
用户在本地 Codex 里发起请求,例如读取画布、创建文本节点、连接节点或触发生成。Codex 调用 `infinite-canvas` MCP 工具,MCP 工具再请求本机 `Canvas Agent` 的 `/api/tools` 接口。
常见工具包括:
- `canvas_get_state`:读取当前画布摘要。
- `canvas_get_selection`:读取当前选区。
- `canvas_apply_ops`:批量执行画布操作。
- `canvas_create_text_node`:创建文本节点。
- `canvas_generate_image` / `canvas_generate_text`:创建生成流程并触发生成。
- `canvas_update_node` / `canvas_connect_nodes`:更新节点或连接节点。
这条链路适合在 Codex 对话里直接让 Codex 操作当前打开的画布。
### 2. 浏览器右侧面板主动打开 Codex
这个方向的入口在浏览器。
```txt
浏览器右侧 Codex 面板
-> 本机 Canvas Agent
-> Codex app-server --stdio
-> infinite-canvas MCP 工具
-> 本机 Canvas Agent
-> 浏览器画布
```
用户在网页右侧面板输入提示词后,浏览器把请求发送到 `Canvas Agent` 的 `/agent/codex/turn` 接口。`Canvas Agent` 会在本机启动或复用 `codex app-server --stdio`,并给这个 Codex 会话注入同一套 `infinite-canvas` MCP 工具。
所以即使入口在浏览器右侧面板,真正改动画布时仍然会回到同一条工具链路:
```txt
Codex -> MCP -> Canvas Agent -> 浏览器画布
```
右侧面板只是把“打开 Codex 会话、发送 prompt、展示流式事件”这件事搬到了网页里。
## 浏览器和 Canvas Agent 如何连接
`Canvas Agent` 默认监听本机地址:
```txt
http://127.0.0.1:17371
```
启动后会生成 `Connect token`,网页连接时必须携带 token。浏览器连接后会建立一条 SSE 长连接:
```txt
GET /events?token=xxx&clientId=xxx
```
连接成功后,`Canvas Agent` 会发送 `hello` 事件,浏览器把状态更新为已连接。
## 画布状态如何同步给 Codex
浏览器会把当前画布快照主动同步给 `Canvas Agent`
```txt
POST /canvas/state?token=xxx&clientId=xxx
```
快照包含:
- 当前画布 ID 和标题;
- 节点列表;
- 连线列表;
- 当前选中的节点 ID
- 当前视口位置和缩放。
`Canvas Agent` 把最近一次快照保存在内存里。Codex 调用 `canvas_get_state` 时,读取的是这份由浏览器同步过来的最新快照,而不是直接读取浏览器 DOM。
## Codex 如何操作画布
以创建文本节点为例,流程如下:
```txt
Codex 调 canvas_create_text_node
-> MCP POST /api/tools
-> Canvas Agent 转成 canvas_apply_ops
-> Canvas Agent 通过 SSE 发送 tool_call
-> 浏览器执行 add_node
-> 浏览器 POST /canvas/result
-> Canvas Agent 返回结果给 Codex
```
`Canvas Agent` 会把一些高级工具转换成统一的画布操作:
```json
{
"ops": [
{
"type": "add_node",
"nodeType": "text",
"metadata": {
"content": "文本内容",
"status": "success"
}
}
]
}
```
浏览器收到 `tool_call` 后,调用前端的 `applyCanvasAgentOps`,在 React 状态中执行 `add_node`、`update_node`、`delete_node`、`connect_nodes`、`set_viewport`、`select_nodes` 等操作。执行完成后再把结果通过 `/canvas/result` 回传给 `Canvas Agent`。
## 工具确认和撤销
浏览器侧默认自动执行 `canvas_apply_ops` 写操作。需要逐次审核时,可在右侧对话输入框左下方把「自动确认」切换为「手动确认」;收到写操作后,对话中会展示待执行工具调用,用户确认后才真正应用到画布。
执行后,前端会保存最近一次操作前的画布快照,用于撤销上一轮 Agent 工具操作。
## 安全边界
- `Canvas Agent` 默认只监听 `127.0.0.1`。
- 网页连接必须携带本机生成的 `Connect token`。
- 第一次带正确 token 连接后,`Canvas Agent` 会记录网页 Origin,避免其他来源复用当前本机 Agent。
- Codex 不直接访问浏览器 DOM,也不直接持有画布内部状态。
- 线上网页不保存本机 Codex 登录态、API Key 或本地文件权限。
- 画布写操作最终由浏览器前端执行,因此可以在前端做确认、撤销和权限控制。
## 一句话总结
本地 Codex 连接画布的本质是:
```txt
Codex 调 MCP 工具,Canvas Agent 做本机桥接,浏览器通过 SSE 接收工具调用并在 React 状态中真正修改画布。
```
读状态是:
```txt
浏览器快照 -> Canvas Agent 内存 -> MCP 工具 -> Codex
```
写操作是:
```txt
Codex -> MCP 工具 -> Canvas Agent -> SSE tool_call -> 浏览器 applyOps -> /canvas/result -> Codex
```
@@ -0,0 +1,40 @@
---
title: 本地开发
description: 前端优先的本地开发方式
---
# 本地开发
当前主应用以 `web/` 前端为主,AI 请求由浏览器前台直连用户自己的 OpenAI 兼容接口。
## 1. 启动前端
```bash
cd web
bun install
bun run dev
```
前端默认访问:
```text
http://localhost:3000
```
## 2. 配置模型
打开右上角配置弹窗,填写自己的 `Base URL`、`API Key` 和模型名。第三方提示词和 WebDAV 都由浏览器前端直连。
## 3. 启动文档站
如果需要单独调整文档站,在 `docs` 目录执行:
```bash
bun run dev
```
## 常见场景
- 改画布、页面和交互:主要看 `web/`
- 改提示词缓存:主要看 `web/src/services/api/prompts.ts`
- 改文档站内容:主要看 `docs/content/docs/`
+10
View File
@@ -0,0 +1,10 @@
{
"title": "开发文档",
"root": true,
"defaultOpen": true,
"pages": [
"local-development",
"canvas-data-structure",
"local-codex-canvas"
]
}
+12
View File
@@ -0,0 +1,12 @@
{
"title": "文档",
"root": true,
"pages": [
"overview",
"canvas",
"development",
"progress",
"business",
"support"
]
}
@@ -0,0 +1,140 @@
---
title: Codex App 插件
description: 在 Codex App 中安装并使用 Infinite Canvas 插件
---
# Codex App 插件
Infinite Canvas 提供了一个 Codex App 插件,用来把 Codex 和本地 Canvas Agent 连接起来。安装后,你可以直接在 Codex 中说“打开 Infinite Canvas”,让 Codex 打开画布、读取节点、创建节点并触发生成流程。
插件源码位于仓库的 `plugins/infinite-canvas/` 目录。
## 安装方式
### 让 Codex 自动安装
把下面这段发给 Codex
```text
请从 https://github.com/basketikun/infinite-canvas.git 安装 Infinite Canvas Codex 插件。
请 clone 仓库到 ~/plugins/infinite-canvas,确认 plugins/infinite-canvas/.codex-plugin/plugin.json 存在,
把 plugins/infinite-canvas 加入 personal marketplace,先运行 codex plugin marketplace add ~
再运行 codex plugin add infinite-canvas@personal。
安装后请校验插件,并告诉我是否需要开启一个新对话来加载新技能和 MCP 工具。
```
这种方式适合不想手动改 marketplace 配置的用户。Codex 会完成 clone、写入 marketplace、安装插件和基础校验。
### 手动安装
推荐把仓库 clone 到 Codex personal marketplace 默认会引用的位置:
```bash
mkdir -p ~/plugins
git clone https://github.com/basketikun/infinite-canvas.git ~/plugins/infinite-canvas
```
确保 `~/.agents/plugins/marketplace.json` 中有 Infinite Canvas 条目。这里的 `path` 要指向仓库里的插件子目录:
```json
{
"name": "personal",
"interface": {
"displayName": "Personal"
},
"plugins": [
{
"name": "infinite-canvas",
"source": {
"source": "local",
"path": "./plugins/infinite-canvas/plugins/infinite-canvas"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "Productivity"
}
]
}
```
然后注册 personal marketplace 并安装插件:
```bash
codex plugin marketplace add ~
codex plugin add infinite-canvas@personal
```
安装完成后,建议新建一个 Codex 对话,让新的 skill 和 MCP 工具完整加载。
### 本仓库开发调试安装
如果你正在 Infinite Canvas 仓库中开发插件,可以直接使用仓库自带的 marketplace
```bash
cd /path/to/infinite-canvas
codex plugin marketplace add "$(pwd)"
codex plugin add infinite-canvas@infinite-canvas-local
```
仓库内的 `.agents/plugins/marketplace.json` 已经指向 `./plugins/infinite-canvas`,适合开发者本地调试。
## 使用插件
新建 Codex 对话后,可以直接说:
```text
打开 Infinite Canvas 新建画布 并连接好Codex
```
插件会启动本地 Canvas Agent,读取连接地址和 token,然后在右侧打开 `https://canvas.best/` 并自动新建、连接画布。只有明确要求使用本地项目时,插件才会启动本地前端。
画布打开后可以继续让 Codex 操作画布:
```text
读取当前画布并总结节点结构
根据选中节点创建一组生图提示词
把这些节点整理成一个生成流程
```
## 更新插件
普通业务代码变更通常不需要更新插件。只有改了下面这些内容时,才建议重新安装或刷新插件:
- `plugins/infinite-canvas/.codex-plugin/plugin.json`
- `plugins/infinite-canvas/.mcp.json`
- `plugins/infinite-canvas/skills/` 下的技能说明
- 插件暴露的 MCP、app 或入口脚本
重新安装后建议开启新对话,避免旧对话继续使用缓存中的技能和 MCP 工具。
## 手动排查
插件默认通过下面的 MCP 命令启动 Canvas Agent
```bash
npx -y @basketikun/canvas-agent mcp
```
如果需要手动排查,先启动画布:
```bash
cd web
bun install
bun run dev
```
然后启动本地 Agent
```bash
npx -y @basketikun/canvas-agent
```
从 Agent 输出或 `http://127.0.0.1:17371/config` 读取本地地址和 token 后,打开:
```text
<画布网页地址>/canvas?mode=new&agentUrl=<Local URL>&agentToken=<Connect token>
```
`mode=new` 会让网页自动创建具体画布并连接本地 Agent,不需要手动点击“新建画布”。
+91
View File
@@ -0,0 +1,91 @@
---
title: Docker 部署
description: 使用 Docker Compose 部署无限画布
---
# Docker 部署
如果你希望在自己的机器或服务器上运行项目,可以直接使用 Docker Compose。
## 使用发布镜像
```bash
git clone git@github.com:basketikun/infinite-canvas.git
cd infinite-canvas
docker compose up -d
```
启动后访问:
```text
http://localhost:3000
```
## 本地构建镜像
如果需要基于当前源码构建镜像:
```bash
docker compose -f docker-compose.local.yml up -d --build
```
## 文档站镜像
文档站位于 `docs/` 目录,按带服务端能力的 Next.js standalone 应用单独构建,不打进主应用镜像。
使用发布镜像:
```bash
cd docs
docker compose up -d
```
基于当前源码本地构建:
```bash
cd docs
docker compose -f docker-compose.local.yml up -d --build
```
## 数据说明
当前主应用镜像只启动 Next.js。画布、我的素材、生成记录和 AI API Key 默认保存在浏览器本地;第三方提示词由浏览器前端直连拉取,不需要额外挂载数据目录。
## 统计分析(可选)
项目**默认不接入任何统计**:不设置下列环境变量时,页面不会加载任何统计脚本,也不会发送任何外部请求。你在自己的部署上想查看流量时,设置对应环境变量即可,无需修改代码或重新构建镜像——镜像启动时会据此生成运行期配置。
统计按「每家一个独立变量」配置:**填了谁就启用谁,可同时启用多家**(例如 GA4 + 百度一起开)。仅支持 GA4 与百度,两者都只接受 ID,脚本地址由代码固定拼接,不接受任意脚本地址或内联 JS。
| 变量 | 说明 |
| --- | --- |
| `ANALYTICS_GA4_ID` | Google Analytics 4 衡量 ID`G-XXXX` |
| `ANALYTICS_BAIDU_ID` | 百度统计站点 ID`hm.js?` 后面那串 hash |
在 `docker-compose.yml` 的 `environment` 中配置(文件内已附注释示例),下例同时启用 GA4 与百度:
```yaml
services:
app:
image: ghcr.io/basketikun/infinite-canvas:latest
ports:
- "3000:3000"
environment:
ANALYTICS_GA4_ID: G-XXXXXXXXXX
ANALYTICS_BAIDU_ID: e8263761cc5c32f5f7a855b8203b965a
```
或使用 `docker run`
```bash
docker run -d -p 3000:3000 \
-e ANALYTICS_GA4_ID=G-XXXXXXXXXX \
-e ANALYTICS_BAIDU_ID=e8263761cc5c32f5f7a855b8203b965a \
ghcr.io/basketikun/infinite-canvas:latest
```
单页应用的路由切换会自动向所有已启用的统计上报 pageview。
<Callout type="warn">
请只在你自己的站点上配置统计 ID,不要把 ID 提交进仓库;fork 或自托管本项目的用户默认不会产生任何统计。
</Callout>
+172
View File
@@ -0,0 +1,172 @@
---
title: 功能介绍
description: 当前项目已实现的主要功能
---
# 功能介绍
本文档记录当前项目已经实现的主要功能。
## 画布项目
- 支持创建多个画布项目。
- 支持项目重命名、删除、批量选择和批量删除。
- 支持单个画布项目导出为 JSON,也支持从 JSON 导入画布。
- 画布项目保存在浏览器本地,登录账号后暂不会自动同步到服务器。
## 无限画布
- 支持拖动画布、滚轮缩放、缩放滑杆和重置视图。
- 支持小地图定位,可开关小地图。
- 支持点阵、网格线、空白三种背景。
- 支持浅色和深色主题。
- 支持框选、多选、全选、取消选择、删除选中。
- 支持复制粘贴节点和节点之间的连线。
- 支持撤销和重做节点、连线、视口、背景和助手会话变化。
- 支持节点连线,并高亮当前节点相关的上下游节点和连线。
- 支持快捷键帮助,覆盖缩放、框选、全选、复制粘贴、撤销重做、删除、退出选择和拖入图片。
## 节点
目前画布中有三类节点:
- 图片节点:展示上传图片、生成图片或我的素材图片。
- 文本节点:保存提示词、说明文案、AI 文字回答等文本内容。
- 生成配置节点:汇总上游文本和图片,统一配置模型、比例、数量后批量生成图片或文本。
节点支持:
- 拖拽移动。
- 四角缩放。
- 图片节点等比缩放或自由比例切换。
- 查看节点基础信息和 JSON。
- 删除、复制、粘贴。
- 通过左右连接点建立上下游关系。
## 图片工作流
- 支持上传图片到新节点。
- 支持拖拽图片文件到画布。
- 支持替换已有图片节点内容。
- 支持下载图片节点。
- 支持把图片节点保存到“我的素材”。
- 支持图片裁剪,并把裁剪结果生成为新的图片节点。
- 支持本地多角度变换,并把结果生成为新的图片节点。
- 支持生成失败后重试。
- 批量生成多张图片时会先展示为图片组节点,支持叠卡预览、展开查看全部结果并设置主图。
## AI 生成
项目默认使用前台直连:前端使用浏览器本地配置的 Base URL、API Key 和 Model 直接请求 OpenAI 兼容接口,不再通过项目服务端转发 AI 请求。
OpenAI 兼容图像和文本能力继续复用现有接口:
- `/v1/images/generations`:文生图。
- `/v1/images/edits`:图生图/参考图编辑。
- `/v1/responses`:文本问答、带图问答和在线 Agent 工具调用。
- `/v1/models`:读取模型列表;火山方舟 Agent Plan 专属 `/api/plan/v3` 当前未提供 OpenAI `/models` 模型列表接口,需要手动填写模型名。
视频能力支持两类接口:
- OpenAI 风格视频:`POST /v1/videos`、`GET /v1/videos/{id}`、`GET /v1/videos/{id}/content`。
- 火山方舟 Agent Plan / Seedance 2.0Base URL 使用 `https://ark.cn-beijing.volces.com/api/plan/v3`,创建任务为 `POST /contents/generations/tasks`,查询任务为 `GET /contents/generations/tasks/{id}`,成功结果读取 `content.video_url`。
Base URL 如果已经以 `/v1`、`/api/v3` 或 `/api/plan/v3` 结尾,系统不会再追加 `/v1`。因此 cpa 反代或火山方舟 Agent Plan 可以继续通过现有 Base URL + API Key + Model 方式配置,不需要新增火山生图 Provider。
配置弹窗里的“拉取模型列表”会尝试真实请求 OpenAI `/models`,不会为 Agent Plan 伪造模型结果。如果火山方舟 Agent Plan 返回 404,请手动增加 `doubao-seedance-2.0` 或文档列出的其他模型名。
可配置项:
- Base URL。
- API Key。
- 默认模型。
- 图片质量。
- 图片比例。
- 生成数量。
- 视频模型。
- 视频比例、清晰度、时长、生成声音和水印。
- 自定义生图 / 视频接口的调用方式,灵活适配不同中转站与自建服务。
普通图片/文本节点可以直接输入提示词生成结果。生成配置节点可以读取上游节点内容,并按节点自己的配置批量生成多个图片或文本结果。生成配置节点支持预览当前提示词和参考图输入,并调整输入顺序。
节点下方对话框和组装提示词输入框都支持 `@` 引用已连接的图片、文本、视频、音频等资源;`@` 引用图片时,输入框内会直接显示该图片的真实缩略图,而不再是「图片1」这类文字编号,发送时会按当前连接自动编号交给模型理解。由于引用改在对话框内直接 `@`,画布节点右上角不再显示「图片1 / 文本1」资源角标。
视频生成可从文本节点读取 prompt,从图片节点读取参考图,从视频节点读取参考视频,从音频节点读取参考音频。Seedance 2.0 支持最多 9 张参考图、3 个参考视频、3 个参考音频;分辨率支持 `480p`、`720p`、`1080p`fast 模型不支持 `1080p`),比例支持 `16:9`、`4:3`、`1:1`、`3:4`、`9:16`、`21:9`、`adaptive`,时长支持 4-15 秒或智能时长。生成成功后会把视频插入画布为视频节点并使用原生播放器预览。参考视频和参考音频优先使用公网可访问 URL;本地素材会以前端可读取的数据传给兼容接口,是否支持取决于具体上游。
## 画布助手
画布右侧助手面板支持:
- 文本问答。
- 生图。
- 读取当前选中节点作为引用。
- 自动把选中节点的上游节点也纳入引用。
- 粘贴图片到助手输入框并插入画布。
- 历史会话。
- 删除单条或多条会话。
- 重试回答。
- 把助手生成的文本插入画布。
- 把助手生成的图片插入画布。
- 折叠和展开助手面板。
## 节点插件系统
画布支持通过节点插件扩展新的节点类型,内置节点之外的能力都可以以插件形式加载:
- 支持通过 URL 动态安装、启用、更新和卸载远程节点插件。
- 提供官方插件注册表,节点插件面板可从项目仓库读取官方插件列表并一键安装。
- 提供 TypeScript 插件开发 SDK,可自行开发画布节点插件,并使用 AI 生成、面板控制等扩展能力。
- 内置 Markdown、SVG、HTML、3D 全景、便利贴等示例插件。
- 插件面板会显示插件版本,并对可升级的插件给出提醒。
Codex App 插件的安装与使用见 [Codex App 插件](/docs/overview/codex-app-plugin)。
## 提示词库
前台提示词库支持:
- 按标题搜索。
- 按标签筛选。
- 按来源筛选。
- 查看提示词详情。
- 查看封面和结果图。
- 复制提示词。
- 把提示词加入“我的素材”。
提示词管理支持:
- 查询提示词。
- 按分组和标签筛选。
- 查看远程提示词源。
- 触发读取内置远程提示词源。
当前内置远程源包括多个 GPT Image / GPT-4o / Nano Banana Pro 相关提示词仓库,由浏览器前端直连拉取并缓存到 IndexedDB。
## 素材
“我的素材”是浏览器本地素材库,支持:
- 新增文本素材和图片素材。
- 编辑素材标题、封面、标签、来源、备注和内容。
- 删除素材。
- 按关键词搜索。
- 按类型筛选。
- 分页浏览。
- 复制文本素材。
- 下载图片素材。
- 从提示词库和画布节点加入素材。
- 在画布中插入素材。
## 配置和同步
- 当前版本不需要账号登录,也不再提供后台管理页面。
- 配置与用户偏好弹窗支持多个 OpenAI 兼容渠道、默认模型、生成偏好和 WebDAV 同步设置。
- 第三方提示词、WebDAV 和 AI 接口都由浏览器前端直连,不经过项目后端代理。
## 当前限制
- 画布项目和“我的素材”目前只保存在浏览器本地,不会随账号同步。
- AI API Key 保存在浏览器本地,并由浏览器直接请求配置的 OpenAI 兼容接口;只适合个人或可信环境使用。
- Seedance 本地参考视频/音频更建议使用公网可访问 URL;上游是否接受前端传入的本地数据取决于具体兼容接口。
- Seedance 返回远程视频 URL 时,前端会尽量下载为本地 Blob 持久化;如果因 CORS 或网络限制无法下载,会保留远程 URL,后续是否可播放取决于上游 URL 的有效期。
- 画布更适合桌面端使用,移动端触控体验还未系统完善。
+14
View File
@@ -0,0 +1,14 @@
{
"title": "项目介绍",
"root": true,
"defaultOpen": true,
"pages": [
"quick-start",
"codex-app-plugin",
"features",
"render",
"docker",
"third-party-prompt-repositories",
"[在线体验](https://canvas.best/)"
]
}
@@ -0,0 +1,48 @@
---
title: 快速开始
description: 用最少步骤把无限画布跑起来
---
# 快速开始
如果你只是想先把项目跑起来,优先部署或启动 `web/` 前端。
## Vercel 部署
在 Vercel 中导入仓库即可,根目录 `vercel.json` 会构建 `web/`。当前版本的 AI 请求由浏览器前台直连用户自己的 OpenAI 兼容地址,不需要额外配置服务端。
## 本地启动
```bash
git clone git@github.com:basketikun/infinite-canvas.git
cd infinite-canvas
cd web
bun install
bun run dev
```
启动后访问:
```text
http://localhost:3000
```
## Docker 启动
如果你需要基于当前源码构建镜像:
```bash
docker build -t infinite-canvas .
docker run --rm -p 3000:3000 infinite-canvas
```
## 首次使用建议
- 先打开右上角配置弹窗,填入自己的 `Base URL`、`API Key` 和模型名。
- 如果需要提示词仓库内容,打开 `/prompts` 会由浏览器前端直连拉取并缓存到 IndexedDB。
- 如果需要跨设备同步画布、素材和生成记录,可在配置弹窗中填写 WebDAV。
## 说明
- 当前画布项目和“我的素材”主要保存在浏览器本地,WebDAV 同步需要用户自行配置。
- AI API Key 保存在浏览器本地,并由前端直接请求 OpenAI 兼容接口。
+28
View File
@@ -0,0 +1,28 @@
---
title: Render 部署
description: 使用 Render 部署无限画布
---
# Render 部署
点击下面链接即可部署到 Render:
[部署到 Render](https://render.com/deploy?repo=https://github.com/basketikun/infinite-canvas)
## 部署步骤
1. 点击 `Deploy to Render`。
2. 登录 Render,并按页面提示连接 GitHub。
3. 确认部署。
部署完成后,打开 Render 分配的 `.onrender.com` 域名即可访问。
## 免费版说明
默认使用 Render 免费 Web Service
- 空闲约 15 分钟后会休眠,下次访问会自动唤醒。
- 当前主应用数据默认保存在浏览器本地;第三方提示词由浏览器前端直连拉取。
- 适合体验和演示,不适合长期保存正式数据。
长期使用建议优先部署到 Vercel,或自行配置 WebDAV 同步浏览器本地数据。
@@ -1,8 +1,12 @@
---
title: 第三方 GitHub 提示词仓库
description: 当前已接入同步逻辑的第三方提示词仓库
---
# 第三方 GitHub 提示词仓库 # 第三方 GitHub 提示词仓库
| 地址 | 状态 | | 地址 | 状态 |
| --- | --- | | --- | --- |
| https://github.com/EvoLinkAI/awesome-gpt-image-2-API-and-Prompts | 已实现同步逻辑 |
| https://github.com/ZeroLu/awesome-gpt-image | 已实现同步逻辑 | | https://github.com/ZeroLu/awesome-gpt-image | 已实现同步逻辑 |
| https://github.com/ImgEdify/Awesome-GPT4o-Image-Prompts | 已实现同步逻辑 | | https://github.com/ImgEdify/Awesome-GPT4o-Image-Prompts | 已实现同步逻辑 |
| https://github.com/YouMind-OpenLab/awesome-gpt-image-2 | 已实现同步逻辑 | | https://github.com/YouMind-OpenLab/awesome-gpt-image-2 | 已实现同步逻辑 |
@@ -0,0 +1,248 @@
---
title: 本地 Agent 接入规划
description: 规划通过本地 Canvas Agent、MCP 和侧边栏助手连接 Codex / Claude Code 操作画布
---
# 本地 Agent 接入规划
本文档规划个人用户在访问线上画布网页时,如何连接自己电脑上的 Codex / Claude Code,并让 Agent 通过对话和工具调用操作当前画布。
## 目标
- 用户打开线上画布后,可以启动本机服务连接当前画布。
- 用户可以在 Codex 终端里通过 MCP 操作画布。
- 用户也可以在网页侧边栏里和本地 Codex / Claude Code 对话。
- 侧边栏需要展示普通消息、流式输出、工具调用、工具结果和错误提示。
- 线上服务不保存用户本地 Codex / Claude Code 登录态、API Key 或本地文件权限。
## 核心结论
浏览器页面不能直接启动本地进程,也不应该直接控制本机 Codex / Claude Code。推荐增加一个用户本机运行的 `canvas-agent` 服务:
```txt
线上画布网页 <-> 本机 canvas-agent <-> Codex / Claude Code
|
+-> MCP Server
+-> 画布连接
```
`canvas-agent` 是本地可信边界,负责启动或连接 Codex / Claude Code、暴露 MCP 工具、维护画布连接、转发侧边栏对话事件。
## 推荐目录
后续实现时建议在项目根目录新增独立 npm 包:
```txt
canvas-agent/
package.json
tsconfig.json
src/
index.ts
config.ts
http-server.ts
canvas-session.ts
mcp-server.ts
agents.ts
schemas.ts
tools.ts
types.ts
```
原因:
- 这是用户本机运行的 Node 服务,不属于线上服务端,也不属于纯前端页面。
- 后续可以发布成 npm 包,用户通过 `npx` 或全局安装启动。
- Codex SDK、Codex MCP、Claude SDK 都更适合在本地 Node 进程中接入。
- HTTP 路由使用 ExpressMCP 协议层使用官方 `@modelcontextprotocol/sdk`,工具入参使用 `zod`,避免手写协议和松散 JSON。
## 本机启动方式
MVP 阶段优先提供 `npx`
```bash
npx -y @basketikun/canvas-agent
```
在本仓库内开发调试时可直接运行:
```bash
cd canvas-agent
npm install
npm run build
node dist/index.js
```
启动后输出:
```txt
Infinite Canvas Agent
Local URL: http://127.0.0.1:17371
Connect token: xxxxxx
```
网页侧边栏填写或自动发现 `http://127.0.0.1:17371` 和 token 后连接本机服务。
## 前端需要增加的能力
前端不直接暴露 HTTP 接口给本机服务,优先由网页主动连接本机 Canvas Agent。当前 MVP 使用 SSE 接收本机事件,HTTP POST 上报画布状态和工具结果:
```txt
网页 -> http://127.0.0.1:17371/events?token=xxx
网页 -> http://127.0.0.1:17371/canvas/state?token=xxx
网页 -> http://127.0.0.1:17371/canvas/result?token=xxx
```
前端需要提供一个画布 Agent 控制层,内部调用现有 store / hook,不直接让外部操作 React 组件:
```ts
canvasAgent.getState()
canvasAgent.getSelection()
canvasAgent.applyOps(ops)
canvasAgent.focusNodes(ids)
canvasAgent.exportSnapshot()
```
建议先支持最小操作集:
- `add_node`:新增图片、文本、音频、视频、生成配置节点。
- `update_node`:更新节点位置、尺寸、内容和配置。
- `delete_node`:删除节点。
- `connect_nodes`:连接两个节点。
- `set_viewport`:移动或缩放当前视口。
- `select_nodes`:选中节点。
工具入参应使用画布业务 JSON,不使用模拟鼠标点击或屏幕坐标自动化。
MCP 读取画布状态时默认返回摘要,不直接把完整图片、视频、音频或超长 base64 内容塞给 Agent。
## MCP 工具设计
`canvas-agent` 内置 MCP Server,让 Codex CLI 可以连接:
```txt
Codex CLI <-> canvas-agent MCP <-> 当前网页画布
```
建议首批 MCP 工具:
- `canvas_get_state`:读取当前画布节点、连线、选区和视口摘要。
- `canvas_apply_ops`:批量执行画布操作。
- `canvas_get_selection`:读取当前选中的节点。
- `canvas_export_snapshot`:导出当前画布快照,用于让 Agent 理解布局。
- `canvas_create_text_node`:快捷创建文本节点。
- `canvas_create_image_prompt_flow`:快捷创建提示词文本节点和图片生成配置节点。
用户本机 Codex 配置示例:
```bash
codex mcp add infinite-canvas -- npx -y @basketikun/canvas-agent mcp
```
本仓库调试时可使用,实际配置建议替换为本机绝对路径:
```bash
codex mcp add infinite-canvas -- node /path/to/infinite-canvas/canvas-agent/dist/index.js mcp
```
网页侧边栏助手另走 Canvas Agent 的对话通道。Canvas Agent 使用官方 `@openai/codex` CLI 的 `codex app-server --stdio` 启动并续用同一个 thread,启动时会注入 `infinite-canvas` MCP 配置并把 `default_tools_approval_mode` 设为 `approve`,避免 Codex 自己的 MCP 审批卡住侧边栏;真正修改画布前由网页侧边栏做二次确认。
侧边栏图片附件通过 HTTP 发送到本机 Canvas AgentCanvas Agent 临时写入本机文件后作为 app-server `localImage` 输入传给 Codex;MVP 会在输入区提示附件体积,单次请求体限制为 30MB。
## 侧边栏助手设计
侧边栏助手连接 `canvas-agent` 后,由 Canvas Agent 选择 Agent Adapter
```txt
Sidebar -> canvas-agent -> Codex app-server stdio
Sidebar -> canvas-agent -> Claude Code CLI / Claude Agent SDK
```
侧边栏优先展示 Codex app-server 原生结构化事件。前端只消费必要事件:
```ts
type AgentEvent =
| { type: "thread.started"; thread_id: string }
| { type: "turn.started" }
| { type: "item.started" | "item.updated" | "item.completed"; item: ThreadItem }
| { type: "turn.completed"; usage: Usage }
| { type: "turn.failed"; error: { message: string } };
```
Claude 后续接入时再单独做 Claude Adapter;当前前端默认只展示 Codex。
## Codex 接入优先级
优先使用官方 `@openai/codex` CLI 的 `codex app-server --stdio`。
- Codex app-server 会输出 `item/agentMessage/delta`Canvas Agent 转成 `item.updated` 后,前端用同一条消息做真实流式渲染。
- 图片附件使用 app-server `localImage` 输入,不手写多模态协议。
参考:
- https://github.com/openai/codex/tree/main/codex-rs/app-server
- https://developers.openai.com/codex/codex-manual.md
## Claude Code 接入优先级
Claude Code 侧当前 MVP 先调用本机 Claude Code CLI 的流式 JSON 输出,由 `canvas-agent` 统一转换事件和工具调用;后续可升级为 Claude Agent SDK。
如果希望 Claude Code 也能操作当前画布,需要给 Claude Code 配置同一个 MCP
```bash
claude mcp add --scope user --transport stdio infinite-canvas -- npx -y @basketikun/canvas-agent mcp
```
Canvas Agent 调用 Claude Code 时默认允许 `mcp__infinite-canvas__*`,避免 print 模式被工具审批卡住;真正修改画布仍由网页侧边栏确认。
参考:
- https://docs.anthropic.com/en/docs/claude-code/sdk
- https://code.claude.com/docs/en/agent-sdk/typescript
## 安全边界
- Canvas Agent 默认只监听 `127.0.0.1`。
- Canvas Agent 启动时生成 token,网页连接必须携带 token。
- Canvas Agent 只接受允许的 Origin,默认允许用户当前打开的画布域名。
- 画布操作默认先在侧边栏展示工具调用,`canvas_apply_ops` 默认需要用户确认后执行,并保留最近一次工具操作撤销入口。
- 不把 Codex / Claude 登录态、API Key、用户本地文件路径上传到线上服务。
- 线上网页只保存 Canvas Agent 地址和必要偏好,不保存本地密钥。
## 实现阶段
### 第一阶段:Codex 终端操作画布
1. 新增 `canvas-agent/` npm 包。
2. Canvas Agent 提供 SSE/HTTP 连接,网页连接并注册当前画布。
3. 前端新增 `canvasAgent` 控制层。
4. Canvas Agent 内置 MCP Server。
5. Codex 通过 MCP 调用 `canvas_get_state` 和 `canvas_apply_ops`。
### 第二阶段:网页侧边栏连接 Codex
1. 前端新增本地 Agent 侧边栏。
2. Canvas Agent 通过官方 `@openai/codex` CLI 的 `codex app-server --stdio` 接入 Codex,复用同一个 thread,注入 `infinite-canvas` MCP,并展示结构化事件流。
3. 运行日志只保留关键 Codex 事件,避免把流式中间更新刷满日志。
4. 侧边栏展示消息流、工具调用、工具结果、错误和图片附件大小提示。
### 第三阶段:接入 Claude Code
1. Canvas Agent 新增 Claude Code CLI Adapter。
2. 统一 Claude Code 的消息和工具调用事件。
3. 前端侧边栏增加 Agent 类型选择。
### 第四阶段:体验完善
1. 增加本机连接状态、重连和 token 更新。
2. 优化工具调用确认、撤销和批量工具队列体验。
3. 增加常用画布动作模板。
4. 增加 npm 发布和用户安装文档。
## MVP 验收标准
- 用户运行 `npx -y @basketikun/canvas-agent` 后,线上画布能显示已连接。
- 用户在 Codex CLI 中可以创建文本节点、移动节点、连接节点。
- 网页侧边栏能发送一条消息给本地 Codex,并展示流式回复。
- 网页侧边栏默认只展示本地 Codex,并展示结构化回复。
- 侧边栏能展示一次 `canvas_apply_ops` 工具调用和结果。
- 断开 Canvas Agent 后,网页能显示清晰的本机连接失败提示。
+12
View File
@@ -0,0 +1,12 @@
{
"title": "项目进度",
"root": true,
"defaultOpen": true,
"pages": [
"[更新日志](/docs/progress/changelog)",
"local-agent-integration-plan",
"prompt-chip-input-plan",
"pending-test",
"todo"
]
}
@@ -0,0 +1,48 @@
---
title: 待测试
description: 当前版本已实现但仍需人工验证的变更项
---
# 待测试
- Agent 画布生图:重启 Canvas Agent 后,其自动生成的工作区 `AGENTS.md` 应同步为最新版指令(用户自行编写的其他 `AGENTS.md` 不应被覆盖);输入“生成一张图片”时应调用 `canvas_generate_image`,通过当前画布节点创建并运行生成流程;只有明确输入“使用 Codex 内置生图/ImageGen 技能生成”时才允许调用内置能力,Codex App Server 返回的 `imageGeneration` 结果应显示在对话中并作为保持原比例的图片节点添加到当前画布;Agent 不应在没有生成结果时提前声称“已生成”。
- Agent 顶部栏:标题只显示垂直居中的「Agent」,连接、对话、历史、日志、新对话和收起操作应位于同一行;面板较窄时标签仍可横向滚动且操作不应错位。
- Agent Markdown 样式:右侧 Agent 回复中的代码块应为横向占满消息区域、无语言标题和无双层边框的紧凑代码条,单行内容不应保留纵向大面积空白,复制操作仅在悬停时弱化显示;行内代码、链接在浅色和深色主题下应清晰;点击外部链接后应显示中文紧凑确认弹窗,长路径能够正常换行且复制、继续打开和关闭操作可用;点击 `/Users/`、`/home/` 等本地绝对文件路径时应改为提示在系统文件管理器中定位,支持复制路径,且浏览器地址不应跳转为 localhost 文件路径。
- Agent 工具确认模式:右侧面板标题栏不应再显示「工具确认」开关;对话输入框左下方应显示确认模式选择,默认选中「自动确认」,画布写入工具应直接执行;切换为「手动确认」后,画布写入工具应在对话中展示等待确认卡片,并支持批准或拒绝。
- Canvas Agent Codex 升级:启动 Canvas Agent 后应能正常连接 Codex、创建或恢复会话、发送消息并调用画布工具,实际运行的 Codex CLI 版本应为 0.145.0;运行中停止任务应只中断当前 turn,随后无需重启 Agent 即可继续发送新任务。
- Canvas Agent Debug:使用 `npx -y @basketikun/canvas-agent --debug` 启动后,终端应按“时间 级别 消息 详情”的纯文本单行格式输出日志并显示日志文件路径,`~/.infinite-canvas/logs/` 下应按启动日期生成相同格式的 `canvas-agent-YYYY-MM-DD.log`,同一天多次启动应追加到同一文件;普通启动保持原有简洁输出,日志中不应出现连接 token 或图片 Data URL 原文。
- Agent HTTP 诊断日志:网页发送一条普通消息后,本地 Debug 日志不应重复输出 `/health`、`/canvas/state`、`/canvas/activate` 成功请求、流式增量或完整会话响应,只保留 HTTP 请求与 Codex 生命周期摘要;右侧「日志」应以单行时间线展示发送、开始、回复、工具、完成用量和错误,不再输出 userMessage started/completed、流式摘要、重复 threadId 或大段原始 JSON。
- Agent 对话统计:用户消息应右对齐并使用透明无气泡的简洁排版,用户和 Codex 两侧均不显示人物头像,消息下方均不显示时间或 Token 信息;输入框上方应居中展示最新一次模型调用的输入、缓存、输出 Token 用量,不显示会话累计值,数值更新时应从旧值平滑滚动到新值而非突然跳变,新建、切换或删除当前会话后应清空旧统计。
- Agent 回复实时显示:在右侧 Agent 发送消息后,用户消息下方应立即出现“正在思考...”,任务运行期间不应闪退;工具完成后应显示 Codex 正在继续处理及已等待时长,等待超过 30 秒时提示可继续等待或停止本轮;Codex 的回复应在当前对话中持续显示,实时事件缺失时也应在任务完成后自动同步完整内容,无需切换到历史或日志再返回对话。模型繁忙等任务失败时应立即结束等待状态,在对话中显示中文错误原因和重试建议,诊断日志不应再把失败轮次记为“处理完成”,刷新历史后错误仍应保留。新建尚未发送首条消息的空会话不应反复出现历史读取失败。
- Agent 流式交互性能:发送长回复时文字应连续平滑出现,输入框、滚动和画布操作不应随回复变长而明显卡顿;长历史会话中只有当前流式消息持续更新,屏幕外消息不应造成明显布局压力;任务完成后应通过 SSE 自动同步完整历史,不再持续请求 `/health` 轮询状态。
- Agent 过程时间线:新建 Agent 对话后 Codex 应生成可读思考摘要,并在运行时依次显示中文的思考摘要、执行计划、命令执行、网页搜索、文件修改和画布工具活动;思考摘要的图标、标题和箭头应稳定保持在同一行,默认收起且无边框,点击箭头后才展开 Codex 实际返回的具体摘要,其中 Markdown 强调、列表和代码应正确渲染而非显示原始符号,完成事件不应再用「已完成分析」覆盖已经收到的摘要;命令执行也应使用无边框紧凑行,显示「已执行 1 条命令」和单行命令预览,点击后再展开工作目录、耗时、退出状态和运行输出;其他工具调用继续使用紧凑卡片排版,状态图标、标题和状态文字保持在同一行;结构化任务进度不应混在对话时间线中,而应独立放在对话区下方、Token 统计上方,支持展开和折叠,并逐项实时更新「待处理」「进行中」「已完成」状态;新任务生成时默认展开,任务结束后保留最新结果,同一计划更新不应生成重复内容;文件详情展示文件路径与新增/修改/删除动作,工具详情不应出现请求 ID、英文工具名或原始 JSON;一轮对话结束自动同步历史以及刷新页面后从历史中 resume 对话时,思考摘要、命令和工具过程记录仍应完整保留并保持相同展示。
- Agent 权限控制:输入框可选择「请求批准」「自动审查」「完全访问」;请求批准模式下,Codex 编辑工作区外文件、执行受限命令或访问网络时应在对话中显示审批卡片,支持拒绝、允许一次和本会话允许,并能依次处理多个并发请求;自动审查模式应仅将需要用户决定的风险操作送入审批;完全访问必须先显示风险确认,启用后可访问网络和本机文件且不再请求审批;选择应在刷新后保留。
- Agent 历史记录:点击记录卡片应直接进入对应对话,不再显示「进入」按钮;可勾选单条或全选多条记录并批量删除,删除当前对话后聊天内容应清空。
- Agent 默认新对话:每次进入任一画布并连接 Agent 后,对话区应保持空白且不自动恢复上一次会话;第一次发送消息时才创建新线程,不应产生未发送消息的空历史记录;需要继续旧对话时可在「历史」中主动选择恢复。
- Agent 当前画布优先:在已打开某个画布时要求 Agent 创建、修改、整理或生成内容,Agent 应直接读取并操作当前画布,不应先调用 `canvas_list_projects` 或使用 `site_navigate` 重复进入画布;只有明确要求查看或切换其他画布时才允许查询画布列表并导航。
- Agent 图片消息:发送一张或多张图片附件后,图片应紧跟用户文字并在消息右侧显示为约 40px 的紧凑缩略图,不再撑大消息区域,单击缩略图应打开大图预览,打开或关闭预览不应改变消息间距或产生额外空行;任务运行中、完成同步历史、切换页面及重新进入历史对话后都应显示浏览器本地保存的图片缩略图,不应消失或把 attachmentId、附件使用说明等内部上下文回显到用户消息中;删除历史会话后应同步清理对应缩略图。
- 画布文本复制:在 Agent 对话或节点信息详情中用鼠标选中文字后,按 `Ctrl/Cmd + C` 应复制所选文本,不应触发画布节点复制;未选中文字且焦点位于画布时,原有节点复制快捷键应保持可用。
- Agent 工作目录指令:`canvas-agent/agent-instructions.md` 应作为独立维护源;重启 Canvas Agent 后,当前工作目录应自动生成 `AGENTS.md`;新建对话发送消息时,Codex 日志中的用户消息只包含本轮请求和必要的附件上下文,不再重复整段 Infinite Canvas 前置提示词,画布及工作台工具仍可正常调用。
- 画布文本设置:文本节点和生成配置节点切换到文本模式后应显示推理强度设置,可选择自动、低、中、高、极高;选择自动时默认 OpenAI Responses 请求不应携带 `reasoning`,选择其他档位时应携带所选强度,刷新画布后节点设置应保留;文本模型自定义调用脚本应能读取 `reasoningEffort`OpenAI 模板应按自动或指定档位正确组装请求。
- 生图工作台参考图:将一张或多张图片拖入参考图区域后应直接上传并显示缩略图;拖入非图片文件应忽略,拖动过程中区域应显示高亮提示,放下文件不应导致浏览器打开或替换当前页面。
- 视频创作台参考资产:将图片、MP4/MOV 视频或 MP3/WAV 音频拖入任一参考资产区域后,应按文件类型自动上传到对应列表;当前拖入区域应显示高亮提示,原有数量、大小、时长和格式限制应继续生效。
- 画布组装提示词:输入或连接超长提示词后,浮层正文高度不应继续撑大,内容应可在正文区域内滚动查看和编辑,标题及关闭按钮保持可见。
- 画布节点提示词:图片等节点下方的提示词超过输入区域高度后,鼠标悬停在输入区域内滚动应查看提示词内容,不应缩放画布。
- 画布节点提示词回显:单图或多图生成完成后,切换选择生成结果的根节点及任一子节点,下方输入区域都应继续显示生成时的提示词;切换到其他节点再选回来后也不应清空。
- 画布生成配置:连接文本节点后连续点击生成(包含失败重试),组装提示词不应重复追加上游文本;在生图、文本、视频、音频之间切换时,实际请求应始终使用当前面板展示的对应类型模型。
- 画布左侧元素列表:点击元素整行应平滑定位并选中对应节点;有内容的图片元素应显示预览按钮,点击后打开大图弹窗且不触发画布定位。
- 配置与用户偏好:导出 JSON 后应包含渠道、默认模型、生成偏好、提示词来源和 WebDAV 配置;在修改当前配置后重新导入该文件,应恢复导出时的设置,错误 JSON 文件应提示格式不正确。配置文件包含 API Key 和 WebDAV 凭据,不应公开分享。
- 模型渠道协议:渠道编辑可选择「火山方舟」并自动填入方舟接口地址;任意名称的生图模型应按方舟 JSON 格式提交参考图,任意名称的视频模型应按方舟任务格式提交和查询,不再依赖模型名包含 `doubao`、`seedream` 或 `seedance`1080p 不应再因模型名包含 `fast` 被禁用,参考视频应允许最大 200MB、总像素 409600-8295044,并继续校验官方宽高、比例和时长限制。
- 图片编辑弹窗:遮罩、切图和裁剪连续滚轮缩放时,图片与遮罩应保持同步且不再闪烁、短暂消失或跳动;遮罩画笔圆心应始终固定在鼠标位置,仅直径随缩放变化,缩放后仍可准确涂抹、拖动切分线和调整裁剪框。
- 提示词中心布局:页面标题及提示词总数应居中;连续输入搜索文字时应在停止输入约 300ms 后再查询;桌面端分类与标签应在左侧独立滚动,右侧搜索框下直接展示提示词卡片;标签数量较多时不能继续向下挤压提示词,窄屏下应恢复上下排列且内容不溢出;不再显示「我的提示词」Tab,收藏提示词应直接加入我的资产。
- 提示词详情弹窗:封面和参考图应固定显示在上方,复制及加入资产操作栏固定在底部,只有中间的标签、描述及提示词内容区域可以滚动;弹窗宽高应受视口限制且不超出屏幕。
- 提示词来源:6 个内置来源应从 Image Prompts 统一仓库读取,更新后数量依次为 323、494、53、76、126、129;提示词仍按 6 个来源分组并可独立启用,来源内可继续按 `tags` 筛选。添加标准 JSON URL 后应能查看内容,填写非数组 JSON 或不可访问地址时应显示失败,并继续保留该来源上一次成功缓存的内容。
- 提示词来源界面:来源应以卡片列表展示,启用开关位于左侧,数量、同步状态和上次成功时间作为次级信息显示,查看、拉取及自定义来源编辑/删除操作使用带文字按钮;底部定时拉取区域应保持独立边框布局。
- 画布提示词库:不应再显示「我的提示词」分组;不展开任何公共来源直接搜索其中的提示词,匹配项应自动显示;点击「插入画布」后应创建正文正确且标题保持为提示词标题的文本节点。
- 全站 Agent:新增 `generation_get_status` 工具,画布生成节点可按 `nodeIds` 查询,生图和视频工作台提交后会返回 `taskId` 并可查询排队、运行、成功或失败状态;需验证查询只由当前活动标签页返回。
- 本地 Agent 多标签页隔离:同时打开两个不同画布并连接同一个 Agent,分别聚焦标签页后通过 MCP 读取和修改画布,操作应只落在当前聚焦页面;网页面板发起的整个 Codex turn 应固定操作发起页面,即使中途聚焦另一标签页也不能切换目标;关闭当前页面后应回退到最近聚焦且仍连接的页面,其他页面回传同一请求结果应被拒绝。
- 本地 Agent 多标签页会话同步:所有标签页共享同一个站点级 Codex 活跃线程;任一页面发送消息、新建、恢复或删除会话后,其他页面应同步活跃线程和聊天记录;Agent 输出仅显示在事件所属线程,运行中不能新建、恢复、删除或再次发送任务。
- 本地 Agent 运行状态同步:在一个标签页运行较长 Codex 任务,等待某张工具卡显示「工具完成」后再打开或刷新第二个标签页;第二个标签页应立即显示 Codex 正在运行并禁用发送,整轮结束后两个标签页同时恢复;工具卡只显示「工具完成」,整轮结束由「本轮完成」表示。
- 本地 Agent 图片附件落画布:在右侧 Agent 上传参考图并要求基于商品信息创建生图流程,附件应创建为保持原比例的真实图片节点,分析提示词应创建为文本节点,二者都应连接到生成配置节点;刷新页面后参考图仍可显示并参与生成。任务中途切换到其他标签页时,附件只能写入发起任务的标签页;若发起标签页关闭,附件读取应失败且不能落入其他画布。
- Agent 对话滚动:从历史或日志切回对话、恢复其他会话时应自动定位到最新消息;手动向上浏览后应显示向下箭头且新消息不强制打断阅读,点击箭头后平滑回到底部并继续跟随新消息。
- Agent 消息区分:用户消息应在右侧使用透明无气泡排版,AI 回复应在左侧使用无头像的开放式 Markdown 排版;长文本、多张附件、错误消息及浅色/深色主题下均应清晰且不溢出。
@@ -0,0 +1,66 @@
---
title: 提示词面板引用显示图片缩略图规划
description: 将提示词面板输入框从 textarea 改造为 contentEditable,让 @ 引用直接显示真实图片缩略图
---
# 提示词面板 @ 引用显示真实图片缩略图
## 背景 / 要解决的问题
提示词面板(`CanvasNodePromptPanel`)的输入框里,`@` 引用一张图片后,显示的是蓝色文字 chip「图片1」。期望**直接显示这张图片的真实缩略图**,而不是文字编号。
当前该输入框是原生 `<textarea>`,只能承载纯文本,无法内嵌图片。要显示缩略图,必须改用 `contentEditable`(可编辑 div),把引用做成带 `<img>` 的原子小卡片。项目里「组装提示词」组件(`CanvasConfigComposer`)已经是这套 contentEditable + 图片 chip 的成熟实现,可作蓝本。
## 关键约束(为什么不能直接改现有组件)
`CanvasResourceMentionTextarea` 被两处复用:
1. **提示词面板** `web/src/components/canvas/canvas-node-prompt-panel.tsx` —— 本次要改的对象。`highlightLabels` 默认 truevalue 是提示词字符串,引用以 label 文本「图片1」嵌在其中,**直接发给 AI**(配合 `web/src/lib/image-reference-prompt.ts` 的「参考图片编号:图片1」前缀)。
2. **节点内文本编辑** `web/src/components/canvas/canvas-node.tsx``isEditingContent` 分支)—— `highlightLabels={false}`,通过 `ref` 调用 `textarea.setSelectionRange(...)` 等 **textarea 专有 API**。若把组件改成 contentEditable,这处会直接崩。
> 结论:**新建一个 contentEditable chip 输入组件,只替换提示词面板那一处**`CanvasResourceMentionTextarea`textarea 版)保持不动,继续服务节点内文本编辑。
## 方案:新增 `CanvasPromptChipInput` 组件
新文件 `web/src/components/canvas/canvas-prompt-chip-input.tsx`,参照 `canvas-config-composer.tsx` 的 contentEditable 模式,但有一处**核心差异**:
- 「组装提示词」把引用序列化成 `@[node:UUID]` 标记;
- 本组件的 value 必须保持**引用的 label 文本**(如「图片1」),这样发给生成的提示词语义不变。因此 chip ↔ 文本的序列化用 **label 文本**,不是 node 标记。
### 组件契约(与现用法一致)
Props`value: string`、`references: CanvasResourceReference[]`、`onChange(value)`、`onSubmit?()`、`className`、`style`、`placeholder`。图片缩略图取 `reference.previewUrl`(即 `node.metadata.content`,已存在于 `CanvasResourceReference`)。
### 实现要点(大量复用 composer 的做法)
1. **DOM ← value(未聚焦时重建)**:仿 `CanvasConfigComposer` 的重建 effect。用 active label 列表(`references.filter(active).map(label)`,按长度降序)把 value 字符串切成「文本片段 + 命中 label」,命中处插入 chip。聚焦时不重建(`document.activeElement === editor` 直接 return),避免与输入 / IME 打架。
2. **chip 渲染**:仿 `createReferenceChip`。image 类型 → `size-6` 的 `<img src={reference.previewUrl}>`;其它类型 → 截断文字。chip 为 `contentEditable="false"` 的 `<span>`,用 `dataset.refLabel` 存 label 以便序列化。
3. **value ← DOMsyncFromEditor**:仿 `serializeNodes`,但 chip 序列化为 `dataset.refLabel`label 文本)而非 node 标记;文本节点原样拼接,`<br>` → `\n`。
4. **@ 菜单**:复用 `textBeforeCaret` + `/@([^\s@]*)$/` 检测;菜单定位改用 `getSelection().getRangeAt(0).getBoundingClientRect()`(比 textarea 的镜像 div 更简单、天然贴着光标)。菜单 UI 沿用现有 `MentionMenu` 的视觉(缩略图 + label + 副标题)。
5. **原子删除**:直接复用 composer 的 `deleteAdjacentReference` / `adjacentReferenceNode` / `findReferenceSibling`——contentEditable + `contentEditable="false"` chip 天然整体删除。可把这几个通用 DOM 助手连同 `textBeforeCaret` / `placeCaretAtEnd` / `closestEditor` 复制进新组件(保持 composer 不动,规避回归风险)。
6. **回车发送 + IME**:复用 `isPlainEnterKey` / `isImeComposing``@/lib/keyboard-event`)与 `onCompositionStart/End`,发送时调用 `onSubmit`。
7. **占位符 & 样式**:空值时显示 placeholderabsolute 占位层);容器套用面板传入的 `className/style``min-h-40`、透明底、无边框,与当前一致)。
### 替换点
`canvas-node-prompt-panel.tsx` 里把 `<CanvasResourceMentionTextarea .../>` 换成 `<CanvasPromptChipInput .../>`(同样的 value/references/onChange/onSubmit/className/style/placeholder)。其余(模型选择、设置、生成按钮)不动。
### 不改动
- `CanvasResourceMentionTextarea`textarea 版)保留,节点内文本编辑继续用它。
- 生成链路、`image-reference-prompt.ts` 前缀逻辑不变(value 仍是 label 文本)。
## 影响文件
- 新增:`web/src/components/canvas/canvas-prompt-chip-input.tsx`
- 修改:`web/src/components/canvas/canvas-node-prompt-panel.tsx`(仅替换输入框组件 + 相应 import)
## 验证
1. `npx tsc --noEmit` 通过。
2. dev server 刷新后:
- 连一张图片到图片 / 视频节点 → 面板输入框输入 `@` → 菜单贴着光标弹出 → 选中 → **输入框内显示该图的真实缩略图 chip**(不再是「图片1」文字)。
- 光标在 chip 后按一次 Backspace → 整个 chip 一次删除。
- 普通文字 + chip 混排,回车触发生成;确认发给生成的 prompt 里引用仍是「图片1」文本(生成结果正常引用到图)。
- 中文输入法输入不吞字、不误触发发送。
- 回归:双击文本节点进入内文本编辑仍正常(未受影响)。
+10
View File
@@ -0,0 +1,10 @@
---
title: TODO
description: 当前项目后续值得处理的事项
---
# TODO
本文档用来记录当前项目后续比较值得处理的事项。
- 本地 Agent 接入后续:按[本地 Agent 接入规划](/docs/progress/local-agent-integration-plan)把 Claude Code CLI Adapter 升级为 Claude Agent SDK Adapter,并完善工具队列体验。
+9
View File
@@ -0,0 +1,9 @@
{
"title": "支持与安全",
"root": true,
"defaultOpen": true,
"pages": [
"security",
"sponsor"
]
}
+34
View File
@@ -0,0 +1,34 @@
---
title: 漏洞提交
description: 安全漏洞提交和负责任披露说明
---
# 漏洞提交
安全策略英文原文见 [SECURITY.md](https://github.com/basketikun/infinite-canvas/blob/main/SECURITY.md)。
请不要在公开 Issue 中直接发布漏洞细节、可利用代码、私密 API Key、截图中的敏感信息或真实用户数据。
## 提交流程
优先使用 GitHub 的私密漏洞报告或 Security Advisory。如果仓库没有开启对应入口,请发送邮件至 [1844025705@qq.com](mailto:1844025705@qq.com),邮件标题建议使用:
```text
[infinite-canvas security]
```
如果暂时无法使用私密渠道,可以先开一个公开 Issue 请求维护者提供私密联系方式,但不要在 Issue 中写入漏洞细节。
## 建议提供的信息
- 受影响的版本、提交、分支或部署方式。
- 清晰的复现步骤。
- 漏洞影响、攻击场景和触发条件。
- 已脱敏的日志、截图或 PoC。
- 是否涉及浏览器本地存储、API Key、WebDAV 同步、AI 渠道配置或接口请求。
## 范围说明
项目会优先处理由本仓库代码或默认配置导致的 XSS、敏感信息泄露、文件处理、导入导出、WebDAV 同步、权限控制和供应链风险。
第三方模型服务、托管平台、浏览器插件、用户自行泄露的 API Key、没有实际利用路径的安全头建议、纯社工钓鱼或账号找回问题通常不属于本项目漏洞范围。
+41
View File
@@ -0,0 +1,41 @@
---
title: 赞助支持
description: 赞助支持、广告合作、项目露出和社区推广说明
---
# 赞助支持
如果本项目对你有帮助,欢迎通过 Star、广告赞助合作、提供 AI 订阅账号等方式支持项目继续维护。
项目开发大量依赖 Codex、Claude 等 AI 编程工具辅助写代码、排查问题和整理文档,这些工具需要持续付费。相关支持会优先用于购买 AI 开发工具、模型服务和项目维护相关资源,帮助项目继续迭代。
## 支持方式
支持方式包括但不限于:
- 给 GitHub 仓库点 Star,帮助项目被更多人看到。
- 通过广告赞助合作支持项目维护成本。
- Codex、Claude 等 AI 编程工具订阅账号支持,帮助维护者持续使用 AI 辅助开发。
## 广告赞助
如果你希望在项目文档、README 或相关页面获得赞助露出,可以直接通过邮箱联系项目维护者沟通位置、周期和展示形式。
### 适合的赞助内容
- AI 工具、模型服务、API 服务或开发者工具。
- 与图片、视频、文本生成工作流相关的产品。
- 适合开源项目用户和 AI 创作者使用的服务。
### 建议提供的信息
- 品牌或产品名称。
- 希望展示的位置和周期。
- 需要展示的链接、文案和素材。
- 预算区间和合作方式。
## 联系方式
请直接发送邮件至 [1844025705@qq.com](mailto:1844025705@qq.com),并在邮件标题中注明“广告赞助”。
如果已经有展示素材或投放计划,可以在邮件中一并说明。
-33
View File
@@ -1,33 +0,0 @@
# Render 部署
点击下面按钮即可部署到 Render:
[![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/basketikun/infinite-canvas)
## 部署步骤
1. 点击 `Deploy to Render`
2. 登录 Render,并按页面提示连接 GitHub。
3. 填写 `ADMIN_PASSWORD`,然后点击确认部署。
部署完成后,打开 Render 分配的 `.onrender.com` 域名即可访问。
## 免费版说明
默认使用 Render 免费 Web Service
- 空闲约 15 分钟后会休眠,下次访问会自动唤醒。
- 免费版本地文件不是持久化存储,SQLite 数据可能在重启、重新部署后丢失。
- 适合体验和演示,不适合长期保存正式数据。
如果要长期使用,建议升级 Render 付费实例并挂载 Persistent Disk,或改用 PostgreSQL。
## 管理员账号
默认管理员用户名:
```text
admin
```
管理员密码是在 Render 部署页面里填写的 `ADMIN_PASSWORD`
+8
View File
@@ -0,0 +1,8 @@
services:
docs:
build:
context: ..
dockerfile: docs/Dockerfile
ports:
- "3001:3000"
restart: unless-stopped
+6
View File
@@ -0,0 +1,6 @@
services:
docs:
image: ghcr.io/basketikun/infinite-canvas-docs:latest
ports:
- "3001:3000"
restart: unless-stopped
-166
View File
@@ -1,166 +0,0 @@
# 功能介绍
本文档记录当前项目已经实现的主要功能。
## 画布项目
- 支持创建多个画布项目。
- 支持项目重命名、删除、批量选择和批量删除。
- 支持单个画布项目导出为 JSON,也支持从 JSON 导入画布。
- 画布项目保存在浏览器本地,登录账号后暂不会自动同步到服务器。
## 无限画布
- 支持拖动画布、滚轮缩放、缩放滑杆和重置视图。
- 支持小地图定位,可开关小地图。
- 支持点阵、网格线、空白三种背景。
- 支持浅色和深色主题。
- 支持框选、多选、全选、取消选择、删除选中。
- 支持复制粘贴节点和节点之间的连线。
- 支持撤销和重做节点、连线、视口、背景和助手会话变化。
- 支持节点连线,并高亮当前节点相关的上下游节点和连线。
- 支持快捷键帮助,覆盖缩放、框选、全选、复制粘贴、撤销重做、删除、退出选择和拖入图片。
## 节点
目前画布中有三类节点:
- 图片节点:展示上传图片、生成图片或素材库图片。
- 文本节点:保存提示词、说明文案、AI 文字回答等文本内容。
- 生成配置节点:汇总上游文本和图片,统一配置模型、比例、数量后批量生成图片或文本。
节点支持:
- 拖拽移动。
- 四角缩放。
- 图片节点等比缩放或自由比例切换。
- 查看节点基础信息和 JSON。
- 删除、复制、粘贴。
- 通过左右连接点建立上下游关系。
## 图片工作流
- 支持上传图片到新节点。
- 支持拖拽图片文件到画布。
- 支持替换已有图片节点内容。
- 支持下载图片节点。
- 支持把图片节点保存到“我的素材”。
- 支持图片裁剪,并把裁剪结果生成为新的图片节点。
- 支持本地多角度变换,并把结果生成为新的图片节点。
- 支持生成失败后重试。
- 批量生成多张图片时会先展示为图片组节点,支持叠卡预览、展开查看全部结果并设置主图。
## AI 生成
项目通过前端直接请求 OpenAI 兼容接口:
- `/v1/images/generations`:文生图。
- `/v1/images/edits`:图生图/参考图编辑。
- `/v1/chat/completions`:文本问答和带图问答。
- `/v1/models`:读取模型列表。
可配置项:
- Base URL。
- API Key。
- 默认模型。
- 图片质量。
- 图片比例。
- 生成数量。
普通图片/文本节点可以直接输入提示词生成结果。生成配置节点可以读取上游节点内容,并按节点自己的配置批量生成多个图片或文本结果。生成配置节点支持预览当前提示词和参考图输入,并调整输入顺序。
## 画布助手
画布右侧助手面板支持:
- 文本问答。
- 生图。
- 读取当前选中节点作为引用。
- 自动把选中节点的上游节点也纳入引用。
- 粘贴图片到助手输入框并插入画布。
- 历史会话。
- 删除单条或多条会话。
- 重试回答。
- 把助手生成的文本插入画布。
- 把助手生成的图片插入画布。
- 折叠和展开助手面板。
## 提示词库
前台提示词库支持:
- 按标题搜索。
- 按标签筛选。
- 按来源筛选。
- 查看提示词详情。
- 查看封面和结果图。
- 复制提示词。
- 把提示词加入“我的素材”。
后台提示词管理支持:
- 查询提示词。
- 新增、编辑、删除提示词。
- 按分组和标签筛选。
- 查看远程提示词源。
- 同步内置远程提示词源。
当前内置远程源包括多个 GPT Image / GPT-4o / Nano Banana Pro 相关提示词仓库。
## 素材
“我的素材”是浏览器本地素材库,支持:
- 新增文本素材和图片素材。
- 编辑素材标题、封面、标签、来源、备注和内容。
- 删除素材。
- 按关键词搜索。
- 按类型筛选。
- 分页浏览。
- 复制文本素材。
- 下载图片素材。
- 从提示词库、画布节点和服务器素材库加入素材。
- 在画布中插入素材。
“素材库”是服务器素材库,支持:
- 按标题搜索。
- 按类型筛选。
- 按标签筛选。
- 查看素材详情。
- 复制文本或图片链接。
- 加入“我的素材”。
- 在画布中插入素材。
后台素材库管理支持:
- 查询素材。
- 新增、编辑、删除素材。
- 按类型和标签筛选。
## 账号和后台
- 注册功能暂时关闭。
- 仅允许管理员账号登录。
- 支持 JWT 会话。
- `/api/auth/me` 可读取当前用户,未登录时返回访客用户。
- 首次启动时可根据环境变量创建默认管理员。
- 管理员后台目前包含提示词管理和素材库管理。
- 后端已有用户管理接口,但前端暂未实现用户管理页面。
## 后端能力
- Gin 提供 API 服务。
- Docker 运行时由 Next.js 提供页面入口,`/api/*` 请求代理到内部 Go 服务。
- GORM 管理数据库连接和自动迁移。
- 支持 SQLite、MySQL、PostgreSQL。
- 数据库保存用户、提示词分组、提示词和服务器素材。
- 业务接口统一返回 `{ code, data, msg }`
## 当前限制
- 画布项目和“我的素材”目前只保存在浏览器本地,不会随账号同步。
- AI API Key 目前保存在浏览器本地,并由浏览器直接请求配置的 OpenAI 兼容接口。
- 服务器素材库目前主要保存 URL 或文本,暂未提供文件上传接口。
- 画布更适合桌面端使用,移动端触控体验还未系统完善。
+45
View File
@@ -0,0 +1,45 @@
# 无限画布文档索引
## 项目介绍
- [快速开始](/docs/overview/quick-start)
- [功能介绍](/docs/overview/features)
- [Render 部署](/docs/overview/render)
- [Docker 部署](/docs/overview/docker)
- [第三方 GitHub 提示词仓库](/docs/overview/third-party-prompt-repositories)
## 操作手册
- [画布节点操作手册](/docs/canvas/canvas-node-manual)
- [画布快捷键](/docs/canvas/canvas-shortcuts)
## 开发与数据
- [本地开发](/docs/development/local-development)
- [画布数据结构](/docs/development/canvas-data-structure)
## 商务合作
- [开源协议](/docs/business/license)
- [贡献者协议](/docs/business/cla)
- [商务合作](/docs/business/business)
## 支持与安全
- [漏洞提交](/docs/support/security)
- [赞助支持](/docs/support/sponsor)
## 项目进度
- [更新日志](/docs/progress/changelog)
- [待测试](/docs/progress/pending-test)
- [TODO](/docs/progress/todo)
## 说明
- 当前画布项目和“我的素材”主要保存在浏览器本地,跨设备可自行配置 WebDAV 同步。
- AI API Key 保存在浏览器本地,并由前端直接请求 OpenAI 兼容接口。
## 原理说明
- [本地 Codex 连接画布原理](/docs/development/local-codex-canvas)
+11
View File
@@ -0,0 +1,11 @@
import { createMDX } from 'fumadocs-mdx/next';
const withMDX = createMDX();
/** @type {import('next').NextConfig} */
const config = {
output: 'standalone',
reactStrictMode: true,
};
export default withMDX(config);
+34
View File
@@ -0,0 +1,34 @@
{
"name": "docs",
"version": "0.0.0",
"private": true,
"scripts": {
"build": "next build",
"dev": "next dev",
"start": "next start",
"types:check": "fumadocs-mdx && next typegen && tsc --noEmit",
"postinstall": "fumadocs-mdx"
},
"dependencies": {
"@orama/orama": "^3.1.18",
"fumadocs-core": "16.9.3",
"fumadocs-mdx": "15.0.10",
"fumadocs-ui": "16.9.3",
"lucide-react": "^1.17.0",
"next": "16.2.6",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"tailwind-merge": "^3.6.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.3.0",
"@types/mdx": "^2.0.13",
"@types/node": "^25.9.1",
"@types/react": "^19.2.15",
"@types/react-dom": "^19.2.3",
"postcss": "^8.5.15",
"serve": "^14.2.6",
"tailwindcss": "^4.3.0",
"typescript": "^6.0.3"
}
}
-16
View File
@@ -1,16 +0,0 @@
# 待测试
- 未登录状态下,画布右上角不再显示用户头像菜单、用户名称、算力点余额和退出登录入口,改为显示登录入口;快捷键入口仍可直接打开。
- 生图工作台的图片参数区复用画布里的紧凑版图像设置面板,尺寸、质量、生成张数的交互保持一致;工作台仍保留独立的模型选择。
- 视频设置抽成画布和视频创作台共用的紧凑面板,清晰度、尺寸、秒数按固定网格选择并支持手动输入;尺寸选择 `auto` 时请求不传 `size`
- 修复画布和生图工作台选择图片尺寸后,请求图片生成/编辑接口未携带 `size` 参数的问题;`auto` 不传,其余比例或像素尺寸会随请求发送。
- 修复生图工作台和画布生图请求中 `quality` 参数可能传入上游不支持值导致 400 的问题;请求前会归一化质量枚举,`auto` 或异常值不再发送给上游。
- 管理后台新增/编辑渠道时,渠道可用模型支持通过弹窗按“新获取、已有”分组选择,并可在弹窗内手动增加模型或拉取模型列表后再写回表单。
- 管理后台编辑渠道时,API Key 留空不再触发必填校验,表示沿用已保存的密钥;新增渠道仍要求填写 API Key。
- 管理后台公开配置里的系统可用模型候选项改为由已启用渠道中选择的模型合并去重生成,最终开放哪些模型仍由公开配置里手动勾选。
- 视频生成请求参数对齐 `grok-imagine-video` 接口:使用 `resolution_name``preset=normal``input_reference[]`,支持清晰度、尺寸、秒数快捷选择和手动输入,并支持最多 7 张参考图。
- 画布视频设置浮层改为挂载到页面根层级,避免被节点悬浮工具栏遮挡。
- 视频清晰度输入框改为只输入数字,提交请求时再拼接 `p` 单位。
- 视频生成前端会识别后端 `{ code, msg }` 错误响应,创建失败不再继续轮询 `undefined`
- 新增 `/video` 视频创作台页面,参考生图工作台布局,支持提示词、参考图、视频参数、生成结果、保存素材、下载和本地生成记录;清晰度和秒数均支持常用值选择与手动输入。
- 视频创作台生成前会把模型、尺寸、秒数、清晰度归一化为视频接口支持的参数,并展示后端返回的错误信息,避免页面侧残留的生图参数影响视频请求。
+7
View File
@@ -0,0 +1,7 @@
const config = {
plugins: {
'@tailwindcss/postcss': {},
},
};
export default config;
+1
View File
@@ -0,0 +1 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Github</title><path d="M12 0c6.63 0 12 5.276 12 11.79-.001 5.067-3.29 9.567-8.175 11.187-.6.118-.825-.25-.825-.56 0-.398.015-1.665.015-3.242 0-1.105-.375-1.813-.81-2.181 2.67-.295 5.475-1.297 5.475-5.822 0-1.297-.465-2.344-1.23-3.169.12-.295.54-1.503-.12-3.125 0 0-1.005-.324-3.3 1.209a11.32 11.32 0 00-3-.398c-1.02 0-2.04.133-3 .398-2.295-1.518-3.3-1.209-3.3-1.209-.66 1.622-.24 2.83-.12 3.125-.765.825-1.23 1.887-1.23 3.169 0 4.51 2.79 5.527 5.46 5.822-.345.294-.66.81-.765 1.577-.69.31-2.415.81-3.495-.973-.225-.354-.9-1.223-1.845-1.209-1.005.015-.405.56.015.781.51.28 1.095 1.327 1.23 1.666.24.663 1.02 1.93 4.035 1.385 0 .988.015 1.916.015 2.196 0 .31-.225.664-.825.56C3.303 21.374-.003 16.867 0 11.791 0 5.276 5.37 0 12 0z"></path></svg>

After

Width:  |  Height:  |  Size: 907 B

+4
View File
@@ -0,0 +1,4 @@
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M32 8L58 54H46L32 29L18 54H6L32 8Z" fill="currentColor"/>
<path d="M32 40L40 54H24L32 40Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 229 B

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1780302626117" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1702" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M824.8 613.2c-16-51.4-34.4-94.6-62.7-165.3C766.5 262.2 689.3 112 511.5 112 331.7 112 256.2 265.2 261 447.9c-28.4 70.8-46.7 113.7-62.7 165.3-34 109.5-23 154.8-14.6 155.8 18 2.2 70.1-82.4 70.1-82.4 0 49 25.2 112.9 79.8 159-26.4 8.1-85.7 29.9-71.6 53.8 11.4 19.3 196.2 12.3 249.5 6.3 53.3 6 238.1 13 249.5-6.3 14.1-23.8-45.3-45.7-71.6-53.8 54.6-46.2 79.8-110.1 79.8-159 0 0 52.1 84.6 70.1 82.4 8.5-1.1 19.5-46.4-14.5-155.8z" p-id="1703"></path></svg>

After

Width:  |  Height:  |  Size: 780 B

+23
View File
@@ -0,0 +1,23 @@
import { defineConfig, defineDocs } from 'fumadocs-mdx/config';
import { metaSchema, pageSchema } from 'fumadocs-core/source/schema';
// You can customize Zod schemas for frontmatter and `meta.json` here
// see https://fumadocs.dev/docs/mdx/collections
export const docs = defineDocs({
dir: 'content/docs',
docs: {
schema: pageSchema,
postprocess: {
includeProcessedMarkdown: true,
},
},
meta: {
schema: metaSchema,
},
});
export default defineConfig({
mdxOptions: {
// MDX options
},
});
+6
View File
@@ -0,0 +1,6 @@
import { HomeLayout } from 'fumadocs-ui/layouts/home';
import { baseOptions } from '@/lib/layout.shared';
export default function Layout({ children }: LayoutProps<'/'>) {
return <HomeLayout {...baseOptions()}>{children}</HomeLayout>;
}
+171
View File
@@ -0,0 +1,171 @@
import Link from 'next/link';
import { ArrowUpRight, BookOpen, Rocket } from 'lucide-react';
import { appName, gitConfig } from '@/lib/shared';
const githubUrl = `https://github.com/${gitConfig.user}/${gitConfig.repo}`;
const demoUrl = 'https://canvas.best/';
const starHistoryUrl = `https://www.star-history.com/?repos=${gitConfig.user}%2F${gitConfig.repo}&type=date`;
const starHistoryChart = `https://api.star-history.com/chart?repos=${gitConfig.user}/${gitConfig.repo}&type=date&transparent=true`;
const darkStarHistoryChart = `${starHistoryChart}&theme=dark`;
const previewImages = [
{
src: 'https://i.ibb.co/TDFvGWDT/image.png',
title: '画布编排',
},
{
src: 'https://i.ibb.co/zVwJq3YS/image.png',
title: '图片生成',
},
{
src: 'https://i.ibb.co/PvY3qhhK/image.png',
title: '参考图编辑',
},
{
src: 'https://i.ibb.co/7D04LwN/image.png',
title: '节点工作流',
},
];
export default function HomePage() {
return (
<main className="mx-auto flex w-full max-w-6xl flex-1 flex-col px-5 pb-16 pt-8 md:px-10 md:pt-14">
<section className="grid min-h-[520px] items-center gap-10 border-b border-zinc-200 pb-12 dark:border-zinc-800 lg:grid-cols-[0.88fr_1.12fr]">
<div>
<div className="inline-flex items-center gap-2 text-xs font-medium text-zinc-500 dark:text-zinc-400">
<Rocket className="size-3.5 text-emerald-600 dark:text-emerald-400" />
AI
</div>
<h1 className="mt-6 max-w-3xl text-4xl font-semibold leading-tight text-zinc-950 dark:text-zinc-50 md:text-6xl [font-family:var(--font-display)]">
{appName}
<span className="block text-zinc-500 dark:text-zinc-400"></span>
</h1>
<p className="mt-6 max-w-2xl text-base leading-8 text-zinc-600 dark:text-zinc-400">
AI
</p>
<div className="mt-8 flex flex-wrap gap-3">
<Link
href="/docs/overview/quick-start"
className="inline-flex items-center justify-center gap-2 rounded-full bg-zinc-950 px-5 py-3 text-sm font-medium text-white transition hover:bg-zinc-800 dark:bg-zinc-100 dark:text-zinc-950 dark:hover:bg-zinc-200"
>
<BookOpen className="size-4" />
</Link>
<a
href={githubUrl}
target="_blank"
rel="noreferrer noopener"
className="inline-flex items-center justify-center gap-2 rounded-full border border-zinc-300 px-5 py-3 text-sm font-medium text-zinc-900 transition hover:border-zinc-900 hover:bg-zinc-100 dark:border-zinc-700 dark:text-zinc-100 dark:hover:border-zinc-500 dark:hover:bg-zinc-900"
>
<img src="/github.svg" alt="" className="size-4" />
GitHub
</a>
<a
href={demoUrl}
target="_blank"
rel="noreferrer noopener"
className="inline-flex items-center justify-center gap-2 rounded-full border border-zinc-300 px-5 py-3 text-sm font-medium text-zinc-900 transition hover:border-zinc-900 hover:bg-zinc-100 dark:border-zinc-700 dark:text-zinc-100 dark:hover:border-zinc-500 dark:hover:bg-zinc-900"
>
线
<ArrowUpRight className="size-4" />
</a>
</div>
</div>
<div className="overflow-hidden rounded-2xl lg:w-[108%] lg:max-w-none">
<img
src={previewImages[3].src}
alt="无限画布效果图"
className="aspect-[16/10] w-full rounded-xl object-cover"
/>
</div>
</section>
<section className="mt-14">
<div className="flex flex-col gap-2 md:flex-row md:items-end md:justify-between">
<div>
<h2 className="text-2xl font-semibold text-zinc-950 dark:text-zinc-50 md:text-3xl">
</h2>
</div>
<Link
href="/docs/overview/features"
className="inline-flex w-fit items-center gap-1.5 text-sm font-medium text-zinc-800 transition hover:text-zinc-950 dark:text-zinc-200 dark:hover:text-white"
>
<ArrowUpRight className="size-4" />
</Link>
</div>
<div className="mt-6 grid gap-4 md:grid-cols-2">
{previewImages.map((item) => (
<img
key={item.src}
src={item.src}
alt={`${item.title}效果图`}
loading="lazy"
decoding="async"
className="aspect-[16/10] w-full rounded-2xl object-cover"
/>
))}
</div>
</section>
<section className="mx-auto mt-16 w-full max-w-4xl text-center">
<h2 className="text-2xl font-semibold text-zinc-950 dark:text-zinc-50 md:text-3xl">
</h2>
<p className="mt-2 text-sm text-zinc-500 dark:text-zinc-400">
</p>
<div className="mt-7 flex justify-center">
<a
href={`${githubUrl}/graphs/contributors`}
target="_blank"
rel="noreferrer noopener"
className="inline-flex max-w-full"
>
<img
src={`https://contrib.rocks/image?repo=${gitConfig.user}/${gitConfig.repo}`}
alt="开发贡献者头像"
loading="lazy"
decoding="async"
className="max-w-full"
/>
</a>
</div>
</section>
<section className="mx-auto mt-16 w-full max-w-5xl text-center">
<h2 className="text-2xl font-semibold text-zinc-950 dark:text-zinc-50 md:text-3xl">
Star History
</h2>
<div className="mt-7 flex justify-center">
<a
href={starHistoryUrl}
target="_blank"
rel="noreferrer noopener"
className="block w-full max-w-4xl"
>
<picture>
<source
media="(prefers-color-scheme: dark)"
srcSet={darkStarHistoryChart}
/>
<source
media="(prefers-color-scheme: light)"
srcSet={starHistoryChart}
/>
<img
src={starHistoryChart}
alt="Star History Chart"
loading="lazy"
decoding="async"
className="mx-auto w-full"
/>
</picture>
</a>
</div>
</section>
</main>
);
}
+11
View File
@@ -0,0 +1,11 @@
import { source } from '@/lib/source';
import { createDocsSearchTokenizer } from '@/lib/search-tokenizer';
import { createFromSource } from 'fumadocs-core/search/server';
export const revalidate = false;
export const { staticGET: GET } = createFromSource(source, {
components: {
tokenizer: createDocsSearchTokenizer(),
},
});
+20
View File
@@ -0,0 +1,20 @@
import { DocPageContent, getDocPageMetadata } from '@/lib/doc-page';
import { source } from '@/lib/source';
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
export default async function Page(props: PageProps<'/docs/[...slug]'>) {
const params = await props.params;
const page = source.getPage(params.slug);
if (!page) notFound();
return <DocPageContent page={page} />;
}
export async function generateMetadata(props: PageProps<'/docs/[...slug]'>): Promise<Metadata> {
const params = await props.params;
const page = source.getPage(params.slug);
if (!page) notFound();
return getDocPageMetadata(page);
}
+13
View File
@@ -0,0 +1,13 @@
import { source } from '@/lib/source';
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
import { baseOptions } from '@/lib/layout.shared';
import { DocsTopTabs } from '@/components/docs-top-tabs';
export default function Layout({ children }: LayoutProps<'/docs'>) {
return (
<DocsLayout {...baseOptions()} tree={source.getPageTree()} tabs={false}>
<DocsTopTabs />
{children}
</DocsLayout>
);
}
+39
View File
@@ -0,0 +1,39 @@
import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/layouts/docs/page';
import { Markdown } from 'fumadocs-core/content/md';
import { getTableOfContents } from 'fumadocs-core/content/toc';
import { remarkHeading } from 'fumadocs-core/mdx-plugins';
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import type { Metadata } from 'next';
import { getMDXComponents } from '@/components/mdx';
const title = '无限画布文档';
const description = '功能说明、操作手册、部署方式、开发文档、商务合作与赞助支持';
async function readDocsIndex() {
return readFile(join(process.cwd(), 'index.md'), 'utf8');
}
export default async function Page() {
const content = await readDocsIndex();
const toc = getTableOfContents(content);
return (
<DocsPage toc={toc}>
<DocsTitle>{title}</DocsTitle>
<DocsDescription>{description}</DocsDescription>
<DocsBody>
<Markdown components={getMDXComponents()} remarkPlugins={[remarkHeading]}>
{content}
</Markdown>
</DocsBody>
</DocsPage>
);
}
export function generateMetadata(): Metadata {
return {
title,
description,
};
}
@@ -0,0 +1,39 @@
import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/layouts/docs/page';
import { Markdown } from 'fumadocs-core/content/md';
import { getTableOfContents } from 'fumadocs-core/content/toc';
import { remarkHeading } from 'fumadocs-core/mdx-plugins';
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import type { Metadata } from 'next';
import { getMDXComponents } from '@/components/mdx';
const title = '更新日志';
const description = '项目版本变更记录';
async function readChangelog() {
return readFile(join(process.cwd(), '..', 'CHANGELOG.md'), 'utf8');
}
export default async function ChangelogPage() {
const changelog = await readChangelog();
const toc = getTableOfContents(changelog);
return (
<DocsPage toc={toc}>
<DocsTitle>{title}</DocsTitle>
<DocsDescription>{description}</DocsDescription>
<DocsBody>
<Markdown components={getMDXComponents()} remarkPlugins={[remarkHeading]}>
{changelog}
</Markdown>
</DocsBody>
</DocsPage>
);
}
export function generateMetadata(): Metadata {
return {
title,
description,
};
}

Some files were not shown because too many files have changed in this diff Show More