mirror of
https://github.com/basketikun/infinite-canvas.git
synced 2026-08-06 01:14:36 +08:00
feat(i18n): add multilingual support for documentation with Chinese and English content
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
+ [新增] 文档站支持英文默认与简体中文切换,正文、导航、搜索及 LLM 文档接口按语言独立展示。
|
||||||
+ [新增] 前端搭建中英文国际化框架,支持持久化切换全局导航、首页、配置中心、Ant Design 和日期组件语言。
|
+ [新增] 前端搭建中英文国际化框架,支持持久化切换全局导航、首页、配置中心、Ant Design 和日期组件语言。
|
||||||
+ [新增] Agent 支持查看、创建、编辑、删除、启停和显式调用本地 Codex Skill,并同步多标签页变更。
|
+ [新增] Agent 支持查看、创建、编辑、删除、启停和显式调用本地 Codex Skill,并同步多标签页变更。
|
||||||
+ [新增] Agent 支持从当前对话或当前画布生成可编辑的 Skill 草稿,并在用户确认后保存。
|
+ [新增] Agent 支持从当前对话或当前画布生成可编辑的 Skill 草稿,并在用户确认后保存。
|
||||||
|
|||||||
@@ -1,29 +1,12 @@
|
|||||||
---
|
---
|
||||||
title: 商务合作
|
title: Business Cooperation
|
||||||
description: 商务合作、定制开发和项目接入说明
|
description: Commercial cooperation, custom development, and integration
|
||||||
---
|
---
|
||||||
|
|
||||||
# 商务合作
|
# Business Cooperation
|
||||||
|
|
||||||
如果你希望围绕无限画布做商业合作、私有化部署、二次开发或模型渠道接入,可以直接通过邮箱联系项目维护者。
|
Contact the maintainer if you need private deployment, custom development, model-provider integration, commercial licensing, technical support, or a joint AI image, video, or text workflow solution.
|
||||||
|
|
||||||
## 适合沟通的方向
|
Please include your use case, expected user scale, deployment environment, data and security requirements, integrations, expected delivery scope, timeline, and budget range.
|
||||||
|
|
||||||
- 私有化部署、内网部署和团队版落地。
|
Email [1844025705@qq.com](mailto:1844025705@qq.com) with **Business Cooperation** in the subject. For commercial licensing or private delivery, describe the intended usage and delivery scope.
|
||||||
- 定制画布节点、生成流程、素材管理或后台能力。
|
|
||||||
- 接入自有 OpenAI 兼容接口、模型渠道或提示词仓库。
|
|
||||||
- 围绕 AI 图片、视频、文本生成工作流做联合方案。
|
|
||||||
- 商业授权、技术支持、交付咨询或联合方案评估。
|
|
||||||
|
|
||||||
## 建议提供的信息
|
|
||||||
|
|
||||||
- 你的业务场景和预期用户规模。
|
|
||||||
- 需要部署的环境、数据保存方式和安全要求。
|
|
||||||
- 需要接入的模型、渠道、支付或账号体系。
|
|
||||||
- 期望的交付范围、时间和预算区间。
|
|
||||||
|
|
||||||
## 联系方式
|
|
||||||
|
|
||||||
请直接发送邮件至 [1844025705@qq.com](mailto:1844025705@qq.com),并在邮件标题中注明“商务合作”。
|
|
||||||
|
|
||||||
如果涉及商业授权或私有化交付,请在邮件中说明计划使用方式和预期交付范围。
|
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
title: 商务合作
|
||||||
|
description: 商务合作、定制开发和项目接入说明
|
||||||
|
---
|
||||||
|
|
||||||
|
# 商务合作
|
||||||
|
|
||||||
|
如果你希望围绕无限画布做商业合作、私有化部署、二次开发或模型渠道接入,可以直接通过邮箱联系项目维护者。
|
||||||
|
|
||||||
|
## 适合沟通的方向
|
||||||
|
|
||||||
|
- 私有化部署、内网部署和团队版落地。
|
||||||
|
- 定制画布节点、生成流程、素材管理或后台能力。
|
||||||
|
- 接入自有 OpenAI 兼容接口、模型渠道或提示词仓库。
|
||||||
|
- 围绕 AI 图片、视频、文本生成工作流做联合方案。
|
||||||
|
- 商业授权、技术支持、交付咨询或联合方案评估。
|
||||||
|
|
||||||
|
## 建议提供的信息
|
||||||
|
|
||||||
|
- 你的业务场景和预期用户规模。
|
||||||
|
- 需要部署的环境、数据保存方式和安全要求。
|
||||||
|
- 需要接入的模型、渠道、支付或账号体系。
|
||||||
|
- 期望的交付范围、时间和预算区间。
|
||||||
|
|
||||||
|
## 联系方式
|
||||||
|
|
||||||
|
请直接发送邮件至 [1844025705@qq.com](mailto:1844025705@qq.com),并在邮件标题中注明“商务合作”。
|
||||||
|
|
||||||
|
如果涉及商业授权或私有化交付,请在邮件中说明计划使用方式和预期交付范围。
|
||||||
@@ -1,22 +1,14 @@
|
|||||||
---
|
---
|
||||||
title: 贡献者协议
|
title: Contributor License Agreement
|
||||||
description: 贡献代码、文档或素材前需要了解的 CLA
|
description: The CLA for code, documentation, and asset contributions
|
||||||
---
|
---
|
||||||
|
|
||||||
# 贡献者协议
|
# Contributor License Agreement
|
||||||
|
|
||||||
Contributor License Agreement,见 [CLA.md](https://github.com/basketikun/infinite-canvas/blob/main/CLA.md)。
|
Read [CLA.md](https://github.com/basketikun/infinite-canvas/blob/main/CLA.md). By submitting a pull request, patch, document, asset, or other contribution, you agree to the CLA. A maintainer may ask you to reply:
|
||||||
|
|
||||||
向本仓库提交 Pull Request、补丁、文档、素材或其他贡献,即表示你同意该协议。维护者也可能要求你在 PR 中明确回复:
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
I have read and agree to CLA.md.
|
I have read and agree to CLA.md.
|
||||||
```
|
```
|
||||||
|
|
||||||
## 贡献前请确认
|
Before contributing, confirm that you have the right to submit the material, third-party content is attributed and compatibly licensed, and the contribution may be distributed under AGPL-3.0 and related commercial licensing. Report security vulnerabilities through the [security process](/docs/support/security), not a public issue.
|
||||||
|
|
||||||
- 你有权提交相关代码、文档或素材。
|
|
||||||
- 第三方内容已明确标注来源,并且授权方式与本项目兼容。
|
|
||||||
- 提交内容可以随项目以 AGPL-3.0 以及项目相关商业授权方式发布。
|
|
||||||
- 安全漏洞不要通过公开 Issue 直接披露细节,请按[漏洞提交](/docs/support/security)流程处理。
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
---
|
||||||
|
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 直接披露细节,请按[漏洞提交](/zh-CN/docs/support/security)流程处理。
|
||||||
@@ -1,36 +1,14 @@
|
|||||||
---
|
---
|
||||||
title: 开源协议
|
title: Open-source License
|
||||||
description: 无限画布开源协议和使用边界说明
|
description: Infinite Canvas licensing and usage boundaries
|
||||||
---
|
---
|
||||||
|
|
||||||
# 开源协议
|
# Open-source License
|
||||||
|
|
||||||
无限画布采用 GNU Affero General Public License v3.0(AGPL-3.0)开源协议。你可以在遵守协议要求的前提下自由使用、复制、修改和分发本项目。
|
Infinite Canvas is licensed under the GNU Affero General Public License v3.0 (AGPL-3.0). You may use, copy, modify, and distribute it while complying with the license.
|
||||||
|
|
||||||
## 项目初衷
|
The project remains open source so users can inspect and modify the code, connect their own APIs and models, and build AI creation workflows without being locked to a fixed provider.
|
||||||
|
|
||||||
无限画布保持开源,希望让更多人可以查看和修改代码,不被固定 API 渠道限制,接入自己的 API 与模型渠道,用画布组织提示词、参考图、素材和生成结果,搭建适合自己的 AI 创作工作流。
|
AGPL-3.0 generally requires derivative code to remain under AGPL-3.0, corresponding source to be offered to users of network services, copyright and license notices to remain intact, and no additional restrictions to be imposed on the covered code. The software is provided as-is without warranty.
|
||||||
|
|
||||||
|
If you need closed-source delivery, commercial licensing, private deployment support, or custom development, see [Business Cooperation](/docs/business/business).
|
||||||
## 使用建议
|
|
||||||
|
|
||||||
如果你只是本地学习、研究或自用,可以直接按开源协议使用。
|
|
||||||
|
|
||||||
如果你计划将项目用于商业产品、私有化交付、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 协议,继续开源对应代码,回馈开源社区。
|
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
title: 开源协议
|
||||||
|
description: 无限画布开源协议和使用边界说明
|
||||||
|
---
|
||||||
|
|
||||||
|
# 开源协议
|
||||||
|
|
||||||
|
无限画布采用 GNU Affero General Public License v3.0(AGPL-3.0)开源协议。你可以在遵守协议要求的前提下自由使用、复制、修改和分发本项目。
|
||||||
|
|
||||||
|
## 项目初衷
|
||||||
|
|
||||||
|
无限画布保持开源,希望让更多人可以查看和修改代码,不被固定 API 渠道限制,接入自己的 API 与模型渠道,用画布组织提示词、参考图、素材和生成结果,搭建适合自己的 AI 创作工作流。
|
||||||
|
|
||||||
|
|
||||||
|
## 使用建议
|
||||||
|
|
||||||
|
如果你只是本地学习、研究或自用,可以直接按开源协议使用。
|
||||||
|
|
||||||
|
如果你计划将项目用于商业产品、私有化交付、SaaS 服务或长期团队内部系统,建议先确认自己的使用方式是否满足 AGPL-3.0 的开源要求。
|
||||||
|
|
||||||
|
如果你的使用场景无法继续开源修改后的代码,或需要闭源交付、商业授权、私有化部署支持、定制开发服务,请查看[商务合作](/zh-CN/docs/business/business)。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 什么是 AGPL-3.0
|
||||||
|
|
||||||
|
AGPL-3.0 的核心要求可以通俗理解为:
|
||||||
|
|
||||||
|
- 开源义务:如果你使用了无限画布的 AGPL 代码,无论你或你的下游如何使用、修改、二次开发或分发,都必须把基于本项目形成的最终代码完整公开出来,不只是公开修改的部分,也不是换个框架重写一遍就能和原始代码脱离关系。
|
||||||
|
- 延续协议:基于本项目形成的衍生代码需要继续以 AGPL-3.0 协议开源,不能更换为其他协议,也不能把这部分代码改成闭源授权。
|
||||||
|
- 网络服务也要开源:即使你只是把基于本项目修改后的版本做成网站、SaaS 或其他网络服务,只要别人可以通过网络使用这个服务,也需要向这些用户提供对应源代码,并继续遵守 AGPL-3.0。
|
||||||
|
- 保留版权声明:不能删除原项目作者信息、版权声明、许可证声明和来源说明,需要让使用者知道代码从哪里来、遵循什么协议。
|
||||||
|
- 不能加额外限制:不能给基于本项目形成的 AGPL 代码增加额外限制,例如禁止别人再分发代码,或要求别人购买授权、服务、产品后才能使用这部分开源代码。
|
||||||
|
- 免责声明:项目按现状提供,作者不保证代码没有 bug,也不对使用本项目产生的结果、损失或风险负责。
|
||||||
|
|
||||||
|
本项目禁止闭源商用。如果你希望将无限画布用于商业项目,请尊重开源,严格遵循 AGPL-3.0 协议,继续开源对应代码,回馈开源社区。
|
||||||
@@ -1,10 +1,6 @@
|
|||||||
{
|
{
|
||||||
"title": "商务合作",
|
"title": "Business",
|
||||||
"root": true,
|
"root": true,
|
||||||
"defaultOpen": true,
|
"defaultOpen": true,
|
||||||
"pages": [
|
"pages": ["license", "cla", "business"]
|
||||||
"license",
|
|
||||||
"cla",
|
|
||||||
"business"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"title": "商务合作",
|
||||||
|
"root": true,
|
||||||
|
"defaultOpen": true,
|
||||||
|
"pages": [
|
||||||
|
"license",
|
||||||
|
"cla",
|
||||||
|
"business"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,66 +1,41 @@
|
|||||||
---
|
---
|
||||||
title: 画布节点操作手册
|
title: Canvas Node Guide
|
||||||
description: 当前画布节点的主要用途与操作流程
|
description: Main canvas node workflows and operations
|
||||||
---
|
---
|
||||||
|
|
||||||
# 画布节点操作手册
|
# Canvas Node Guide
|
||||||
|
|
||||||
本文档记录画布节点的主要用途和操作流程,方便使用和后续开发维护。当前先介绍文本节点。
|
## Text nodes
|
||||||
|
|
||||||
## 文本节点
|
Text nodes store prompts, drafts, descriptions, and AI-generated text. Double-click the content or select **Edit Text** in the toolbar to edit it. The toolbar also controls text size.
|
||||||
|
|
||||||
文本节点用于保存提示词、草稿、说明文案和 AI 生成的文字结果。它既可以作为独立文本内容,也可以作为下游生成配置节点的输入。
|
Select **Edit** to open the prompt panel below the node:
|
||||||
|
|
||||||
### 编辑文本内容
|
- An empty node generates text into itself.
|
||||||
|
- Editing an existing node creates a connected result node to the right, preserving the original.
|
||||||
|
- Use `@` to reference connected images, text, video, or audio. Image references appear as thumbnails and are numbered automatically when sent to the model.
|
||||||
|
- The model selection applies only to the current node. Fetch or enter model names in global Settings first.
|
||||||
|
|
||||||
- 双击文本节点内容区域,直接编辑节点里的文字。
|
To generate an image from text, prepare the prompt and select **Generate Image**. Infinite Canvas creates and connects a generation configuration node, starts generation, and lets you adjust the model, aspect ratio, and output count before retrying.
|
||||||
- 选中文本节点后,点击顶部工具栏的“编辑文字”,进入文字编辑状态。
|
|
||||||
- 顶部工具栏的“缩小”和“放大”用于调整文本节点字号。
|
|
||||||
|
|
||||||
### 用下方对话框生成或修改文本
|
## Image nodes
|
||||||
|
|
||||||
- 选中文本节点后,点击顶部工具栏的“编辑”,打开节点下方对话框。
|
- Generating from an empty image node writes the first result into that node.
|
||||||
- 文本节点下方对话框只用于生成或改写文本,不承担生图功能。
|
- A single result replaces the current empty node; multiple results use it as the primary node and add result nodes to the right.
|
||||||
- 当文本节点为空时,输入框用于填写想生成的文本内容;点击发送后,结果会回填到当前文本节点。
|
- A populated image node is reused as a reference image.
|
||||||
- 当文本节点已有内容时,输入框用于填写想把本段文本修改成什么;点击发送后,会在右侧生成新的文本节点,并自动连接原节点和新节点。
|
- Generated nodes record the prompt, generation type, model, size, quality, count, and references in `metadata`.
|
||||||
- 输入内容可以手写,也可以从提示词库选择。
|
- A retry stops with a clear warning if required references can no longer be restored.
|
||||||
- 输入框支持 `@` 引用已连接的图片、文本、视频、音频资源;`@` 引用图片时会在输入框内直接显示真实缩略图,发送时按当前连接自动编号交给模型。因此不需要再记忆「图片1 / 文本1」编号,画布节点右上角也不再显示资源角标。
|
|
||||||
- 对话框里的模型下拉来自全局配置里已拉取的模型列表;选择结果只作用于当前节点,不会修改其它节点或全局默认模型。
|
|
||||||
- 如果下拉中没有模型,需要先打开配置弹窗拉取模型列表,并设置默认生图模型和默认文本模型。火山方舟 Agent Plan 若提示不支持 `/models`,请手动填写模型名。
|
|
||||||
|
|
||||||
### 用文本节点生成图片
|
## Video nodes
|
||||||
|
|
||||||
- 先在文本节点中准备好要用于生图的文本内容。
|
Create a video node from the toolbar or upload a local video. Video nodes use the native player. An empty video node can generate into itself, while generation started from text, images, or configuration nodes creates a connected video node.
|
||||||
- 点击文本节点顶部工具栏的“生图”按钮。
|
|
||||||
- 系统会在文本节点右侧自动创建一个生成配置节点,并连接文本节点到生成配置节点。
|
|
||||||
- 生成配置节点会读取上游文本内容作为生图提示词,并立即开始生成图片。
|
|
||||||
- 后续需要调整模型、比例、数量时,可以在生成配置节点里修改后再次生成。
|
|
||||||
|
|
||||||
### 用图片节点继续生成图片
|
The OpenAI-compatible flow uses `POST /v1/videos`, `GET /v1/videos/{id}`, and `GET /v1/videos/{id}/content`. For Volcengine Ark Agent Plan and Seedance 2.0, use `https://ark.cn-beijing.volces.com/api/plan/v3` and enter the model name manually because this endpoint does not expose OpenAI's `/models` API.
|
||||||
|
|
||||||
- 选中空图片节点后点击“生成”,会直接在当前节点里生成结果,不再额外保留一个空框。
|
## Recommended workflow
|
||||||
- 当数量为 `1` 时,生成结果会直接回填到当前图片节点。
|
|
||||||
- 当数量大于 `1` 时,当前图片节点会作为主图节点,右侧继续生成对应数量的子图节点。
|
|
||||||
- 如果图片节点本身已经有内容,则会沿用已有图片作为参考图,再生成新的图片批次。
|
|
||||||
- 每个生成出来的图片节点都会在自身 `metadata` 下记录提示词、生成类型、模型、尺寸、质量、数量和参考图引用,方便后续重试。
|
|
||||||
- 如果重试时参考图已经丢失或无法恢复,系统会直接提示,避免误发空的图生图请求。
|
|
||||||
|
|
||||||
### 视频节点
|
1. Create a text node with the initial idea.
|
||||||
|
2. Ask AI to refine the prompt in the panel below it.
|
||||||
- 工具栏可以新建视频节点,也可以拖入或上传本地视频文件。
|
3. Compare the connected rewritten node with the original.
|
||||||
- 视频节点使用原生播放器展示内容,可在节点内直接播放、暂停和拖动进度。
|
4. Select **Generate Image** on the chosen text node.
|
||||||
- 空视频节点下方对话框可输入提示词生成视频,结果会回填到当前节点。
|
5. Adjust parameters or regenerate from the generated configuration node.
|
||||||
- 从文本、图片或配置节点创建视频生成时,会在右侧生成新的视频节点并自动连接。
|
|
||||||
- 生成配置节点的视频模式会读取上游文本作为 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. 新建文本节点,写入图片创作想法。
|
|
||||||
2. 打开文本节点下方对话框,让 AI 优化或扩写这段提示词。
|
|
||||||
3. 改写结果会生成到新的文本节点,保留原始文本方便对比。
|
|
||||||
4. 确认文本节点内容后,点击顶部工具栏“生图”。
|
|
||||||
5. 在自动创建的生成配置节点中继续调整图片参数或重新生成。
|
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
---
|
||||||
|
title: 画布节点操作手册
|
||||||
|
description: 当前画布节点的主要用途与操作流程
|
||||||
|
---
|
||||||
|
|
||||||
|
# 画布节点操作手册
|
||||||
|
|
||||||
|
本文档记录画布节点的主要用途和操作流程,方便使用和后续开发维护。当前先介绍文本节点。
|
||||||
|
|
||||||
|
## 文本节点
|
||||||
|
|
||||||
|
文本节点用于保存提示词、草稿、说明文案和 AI 生成的文字结果。它既可以作为独立文本内容,也可以作为下游生成配置节点的输入。
|
||||||
|
|
||||||
|
### 编辑文本内容
|
||||||
|
|
||||||
|
- 双击文本节点内容区域,直接编辑节点里的文字。
|
||||||
|
- 选中文本节点后,点击顶部工具栏的“编辑文字”,进入文字编辑状态。
|
||||||
|
- 顶部工具栏的“缩小”和“放大”用于调整文本节点字号。
|
||||||
|
|
||||||
|
### 用下方对话框生成或修改文本
|
||||||
|
|
||||||
|
- 选中文本节点后,点击顶部工具栏的“编辑”,打开节点下方对话框。
|
||||||
|
- 文本节点下方对话框只用于生成或改写文本,不承担生图功能。
|
||||||
|
- 当文本节点为空时,输入框用于填写想生成的文本内容;点击发送后,结果会回填到当前文本节点。
|
||||||
|
- 当文本节点已有内容时,输入框用于填写想把本段文本修改成什么;点击发送后,会在右侧生成新的文本节点,并自动连接原节点和新节点。
|
||||||
|
- 输入内容可以手写,也可以从提示词库选择。
|
||||||
|
- 输入框支持 `@` 引用已连接的图片、文本、视频、音频资源;`@` 引用图片时会在输入框内直接显示真实缩略图,发送时按当前连接自动编号交给模型。因此不需要再记忆「图片1 / 文本1」编号,画布节点右上角也不再显示资源角标。
|
||||||
|
- 对话框里的模型下拉来自全局配置里已拉取的模型列表;选择结果只作用于当前节点,不会修改其它节点或全局默认模型。
|
||||||
|
- 如果下拉中没有模型,需要先打开配置弹窗拉取模型列表,并设置默认生图模型和默认文本模型。火山方舟 Agent Plan 若提示不支持 `/models`,请手动填写模型名。
|
||||||
|
|
||||||
|
### 用文本节点生成图片
|
||||||
|
|
||||||
|
- 先在文本节点中准备好要用于生图的文本内容。
|
||||||
|
- 点击文本节点顶部工具栏的“生图”按钮。
|
||||||
|
- 系统会在文本节点右侧自动创建一个生成配置节点,并连接文本节点到生成配置节点。
|
||||||
|
- 生成配置节点会读取上游文本内容作为生图提示词,并立即开始生成图片。
|
||||||
|
- 后续需要调整模型、比例、数量时,可以在生成配置节点里修改后再次生成。
|
||||||
|
|
||||||
|
### 用图片节点继续生成图片
|
||||||
|
|
||||||
|
- 选中空图片节点后点击“生成”,会直接在当前节点里生成结果,不再额外保留一个空框。
|
||||||
|
- 当数量为 `1` 时,生成结果会直接回填到当前图片节点。
|
||||||
|
- 当数量大于 `1` 时,当前图片节点会作为主图节点,右侧继续生成对应数量的子图节点。
|
||||||
|
- 如果图片节点本身已经有内容,则会沿用已有图片作为参考图,再生成新的图片批次。
|
||||||
|
- 每个生成出来的图片节点都会在自身 `metadata` 下记录提示词、生成类型、模型、尺寸、质量、数量和参考图引用,方便后续重试。
|
||||||
|
- 如果重试时参考图已经丢失或无法恢复,系统会直接提示,避免误发空的图生图请求。
|
||||||
|
|
||||||
|
### 视频节点
|
||||||
|
|
||||||
|
- 工具栏可以新建视频节点,也可以拖入或上传本地视频文件。
|
||||||
|
- 视频节点使用原生播放器展示内容,可在节点内直接播放、暂停和拖动进度。
|
||||||
|
- 空视频节点下方对话框可输入提示词生成视频,结果会回填到当前节点。
|
||||||
|
- 从文本、图片或配置节点创建视频生成时,会在右侧生成新的视频节点并自动连接。
|
||||||
|
- 生成配置节点的视频模式会读取上游文本作为 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. 新建文本节点,写入图片创作想法。
|
||||||
|
2. 打开文本节点下方对话框,让 AI 优化或扩写这段提示词。
|
||||||
|
3. 改写结果会生成到新的文本节点,保留原始文本方便对比。
|
||||||
|
4. 确认文本节点内容后,点击顶部工具栏“生图”。
|
||||||
|
5. 在自动创建的生成配置节点中继续调整图片参数或重新生成。
|
||||||
@@ -1,41 +1,29 @@
|
|||||||
---
|
---
|
||||||
title: 画布快捷键
|
title: Canvas Shortcuts
|
||||||
description: 画布常用鼠标与键盘操作说明
|
description: Common mouse and keyboard controls
|
||||||
---
|
---
|
||||||
|
|
||||||
# 画布快捷键
|
# Canvas Shortcuts
|
||||||
|
|
||||||
本文档记录画布里常用的鼠标和键盘操作。
|
## View
|
||||||
|
|
||||||
## 视图
|
- Drag empty canvas space to pan.
|
||||||
|
- Use the mouse wheel or zoom slider to zoom.
|
||||||
|
- Use Reset View to restore the default zoom and centered position.
|
||||||
|
|
||||||
- 拖动画布空白处:平移视图。
|
## Selection
|
||||||
- 鼠标滚轮:缩放画布。
|
|
||||||
- 缩放滑杆:精确调整缩放。
|
|
||||||
- 重置视图按钮:回到默认缩放和居中位置。
|
|
||||||
|
|
||||||
## 选择
|
- `Ctrl / Cmd` + drag: box-select multiple nodes.
|
||||||
|
- `Shift / Ctrl / Cmd` + click: add or remove a node from the selection.
|
||||||
|
- `Ctrl / Cmd` + `A`: select all nodes.
|
||||||
|
- `Esc`: clear the selection and close the active overlay.
|
||||||
|
|
||||||
- `Ctrl / Cmd` + 拖动:框选多个节点。
|
## Editing
|
||||||
- `Shift / Ctrl / Cmd` + 点击节点:追加或取消选择节点。
|
|
||||||
- `Ctrl / Cmd` + `A`:全选画布节点。
|
|
||||||
- `Esc`:取消选择,并关闭当前浮层。
|
|
||||||
|
|
||||||
## 编辑
|
- `Ctrl / Cmd` + `C`: copy selected nodes.
|
||||||
|
- `Ctrl / Cmd` + `V`: paste nodes.
|
||||||
|
- `Delete / Backspace`: delete selected nodes or edges.
|
||||||
|
- `Ctrl / Cmd` + `Z`: undo.
|
||||||
|
- `Ctrl / Cmd` + `Shift` + `Z`, or `Ctrl / Cmd` + `Y`: redo.
|
||||||
|
|
||||||
- `Ctrl / Cmd` + `C`:复制选中节点。
|
Drag image files onto the canvas to upload them, or insert saved items from My Assets. Undo and redo cover nodes, edges, the viewport, background mode, and assistant sessions; they do not cover the project name, account state, global AI settings, or the My Assets library.
|
||||||
- `Ctrl / Cmd` + `V`:粘贴节点。
|
|
||||||
- `Delete / Backspace`:删除选中的节点或连线。
|
|
||||||
- `Ctrl / Cmd` + `Z`:撤销。
|
|
||||||
- `Ctrl / Cmd` + `Shift` + `Z`:重做。
|
|
||||||
- `Ctrl / Cmd` + `Y`:重做。
|
|
||||||
|
|
||||||
## 图片和素材
|
|
||||||
|
|
||||||
- 拖入图片文件:上传图片到画布。
|
|
||||||
- 导入图片按钮:从本地选择图片。
|
|
||||||
- 我的素材:选择素材后插入画布。
|
|
||||||
|
|
||||||
## 撤销和重做范围
|
|
||||||
|
|
||||||
撤销和重做会记录画布节点、连线、视口、背景模式和助手会话变化。画布项目名称、账号状态、全局 AI 配置和“我的素材”保存操作不属于当前画布历史。
|
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
---
|
||||||
|
title: 画布快捷键
|
||||||
|
description: 画布常用鼠标与键盘操作说明
|
||||||
|
---
|
||||||
|
|
||||||
|
# 画布快捷键
|
||||||
|
|
||||||
|
本文档记录画布里常用的鼠标和键盘操作。
|
||||||
|
|
||||||
|
## 视图
|
||||||
|
|
||||||
|
- 拖动画布空白处:平移视图。
|
||||||
|
- 鼠标滚轮:缩放画布。
|
||||||
|
- 缩放滑杆:精确调整缩放。
|
||||||
|
- 重置视图按钮:回到默认缩放和居中位置。
|
||||||
|
|
||||||
|
## 选择
|
||||||
|
|
||||||
|
- `Ctrl / Cmd` + 拖动:框选多个节点。
|
||||||
|
- `Shift / Ctrl / Cmd` + 点击节点:追加或取消选择节点。
|
||||||
|
- `Ctrl / Cmd` + `A`:全选画布节点。
|
||||||
|
- `Esc`:取消选择,并关闭当前浮层。
|
||||||
|
|
||||||
|
## 编辑
|
||||||
|
|
||||||
|
- `Ctrl / Cmd` + `C`:复制选中节点。
|
||||||
|
- `Ctrl / Cmd` + `V`:粘贴节点。
|
||||||
|
- `Delete / Backspace`:删除选中的节点或连线。
|
||||||
|
- `Ctrl / Cmd` + `Z`:撤销。
|
||||||
|
- `Ctrl / Cmd` + `Shift` + `Z`:重做。
|
||||||
|
- `Ctrl / Cmd` + `Y`:重做。
|
||||||
|
|
||||||
|
## 图片和素材
|
||||||
|
|
||||||
|
- 拖入图片文件:上传图片到画布。
|
||||||
|
- 导入图片按钮:从本地选择图片。
|
||||||
|
- 我的素材:选择素材后插入画布。
|
||||||
|
|
||||||
|
## 撤销和重做范围
|
||||||
|
|
||||||
|
撤销和重做会记录画布节点、连线、视口、背景模式和助手会话变化。画布项目名称、账号状态、全局 AI 配置和“我的素材”保存操作不属于当前画布历史。
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
{
|
{
|
||||||
"title": "操作手册",
|
"title": "Canvas Guide",
|
||||||
"root": true,
|
"root": true,
|
||||||
"defaultOpen": true,
|
"defaultOpen": true,
|
||||||
"pages": [
|
"pages": ["canvas-node-manual", "canvas-shortcuts"]
|
||||||
"canvas-node-manual",
|
|
||||||
"canvas-shortcuts"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"title": "操作手册",
|
||||||
|
"root": true,
|
||||||
|
"defaultOpen": true,
|
||||||
|
"pages": [
|
||||||
|
"canvas-node-manual",
|
||||||
|
"canvas-shortcuts"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,26 +1,15 @@
|
|||||||
---
|
---
|
||||||
title: 画布数据结构
|
title: Canvas Data Structure
|
||||||
description: 画布本地存储、节点结构、媒体文件与清理机制
|
description: Local canvas storage, nodes, media files, and cleanup
|
||||||
---
|
---
|
||||||
|
|
||||||
# 画布数据结构
|
# Canvas Data Structure
|
||||||
|
|
||||||
本文档说明当前画布在前端本地保存的数据结构、图片文件的存储和清理方式。
|
## Storage
|
||||||
|
|
||||||
## 当前存储位置
|
Canvas projects and My Assets are stored with localForage in the `infinite-canvas` database and `app_state` store. Image blobs use `image_files`; other media blobs use `media_files`. Project JSON stores display URLs, `storageKey`, and metadata rather than large base64 payloads.
|
||||||
|
|
||||||
当前画布项目主要保存在浏览器本地:
|
## Project
|
||||||
|
|
||||||
- 画布项目 JSON:`localForage`,数据库名 `infinite-canvas`,storeName `app_state`,key 为 `infinite-canvas:canvas_store`。
|
|
||||||
- 我的素材 JSON:`localForage`,数据库名 `infinite-canvas`,storeName `app_state`,key 为 `infinite-canvas:asset_store`。
|
|
||||||
- 图片 Blob:单独存到 `localForage` 实例,数据库名 `infinite-canvas`,storeName `image_files`。
|
|
||||||
- 视频等媒体 Blob:单独存到 `localForage` 实例,数据库名 `infinite-canvas`,storeName `media_files`。
|
|
||||||
|
|
||||||
画布 JSON 不直接长期保存大体积 base64 图片或视频。图片节点、视频节点、助手图片和素材媒体只保存展示 URL、`storageKey` 和元信息,真实 Blob 通过 `storageKey` 读取。
|
|
||||||
|
|
||||||
## 画布项目结构
|
|
||||||
|
|
||||||
每个画布项目是一个 `CanvasProject`:
|
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
type CanvasProject = {
|
type CanvasProject = {
|
||||||
@@ -37,21 +26,9 @@ type CanvasProject = {
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
字段说明:
|
The viewport stores screen translation in `x` and `y`, and scale in `k`.
|
||||||
|
|
||||||
- `id`:画布项目 ID,当前前端生成。
|
## Nodes and edges
|
||||||
- `title`:画布名称。
|
|
||||||
- `createdAt` / `updatedAt`:ISO 字符串。
|
|
||||||
- `nodes`:画布节点列表。
|
|
||||||
- `connections`:节点连线列表。
|
|
||||||
- `chatSessions`:右侧画布助手会话。
|
|
||||||
- `activeChatId`:当前选中的助手会话 ID。
|
|
||||||
- `backgroundMode`:画布背景模式。
|
|
||||||
- `viewport`:视口变换,`x/y` 是屏幕平移,`k` 是缩放比例。
|
|
||||||
|
|
||||||
## 节点结构
|
|
||||||
|
|
||||||
每个节点是一个 `CanvasNodeData`:
|
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
type CanvasNodeData = {
|
type CanvasNodeData = {
|
||||||
@@ -63,58 +40,7 @@ type CanvasNodeData = {
|
|||||||
height: number;
|
height: number;
|
||||||
metadata?: CanvasNodeMetadata;
|
metadata?: CanvasNodeMetadata;
|
||||||
};
|
};
|
||||||
```
|
|
||||||
|
|
||||||
通用字段:
|
|
||||||
|
|
||||||
- `id`:节点 ID。
|
|
||||||
- `type`:节点类型,当前有图片、文本、生成配置、视频四类。
|
|
||||||
- `title`:节点标题。
|
|
||||||
- `position`:画布世界坐标,不是屏幕坐标。
|
|
||||||
- `width` / `height`:画布世界坐标下的节点尺寸。
|
|
||||||
- `metadata`:节点内容和业务状态。
|
|
||||||
|
|
||||||
`metadata` 当前常用字段:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
type CanvasNodeMetadata = {
|
|
||||||
content?: string;
|
|
||||||
prompt?: string;
|
|
||||||
status?: "idle" | "success" | "loading" | "error";
|
|
||||||
errorDetails?: string;
|
|
||||||
fontSize?: number;
|
|
||||||
generationMode?: "text" | "image" | "video";
|
|
||||||
model?: string;
|
|
||||||
size?: string;
|
|
||||||
count?: number;
|
|
||||||
naturalWidth?: number;
|
|
||||||
naturalHeight?: number;
|
|
||||||
freeResize?: boolean;
|
|
||||||
isBatchRoot?: boolean;
|
|
||||||
batchRootId?: string;
|
|
||||||
batchChildIds?: string[];
|
|
||||||
primaryImageId?: string;
|
|
||||||
imageBatchExpanded?: boolean;
|
|
||||||
inputOrder?: string[];
|
|
||||||
storageKey?: string;
|
|
||||||
mimeType?: string;
|
|
||||||
bytes?: number;
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
不同节点的使用方式:
|
|
||||||
|
|
||||||
- 图片节点:`content` 是当前可展示的图片 URL,通常是 `blob:` URL;`storageKey` 指向本地图片 Blob;`naturalWidth/naturalHeight/bytes/mimeType` 保存原图信息。
|
|
||||||
- 视频节点:`content` 是当前可播放的视频 URL,通常是 `blob:` URL;`storageKey` 指向本地视频 Blob;`bytes/mimeType` 保存文件信息。
|
|
||||||
- 文本节点:`content` 保存文本内容;`fontSize` 保存字体大小;`prompt/status/errorDetails` 保存生成状态。
|
|
||||||
- 生成配置节点:`generationMode/model/size/count/inputOrder` 保存生成配置;`generationMode` 可选择文本、图片或视频;上游输入通过 `connections` 计算。
|
|
||||||
- 图片组节点:根节点用 `isBatchRoot/batchChildIds/primaryImageId/imageBatchExpanded` 记录批量生成结果;子图节点用 `batchRootId` 指回根节点。
|
|
||||||
|
|
||||||
## 连线结构
|
|
||||||
|
|
||||||
每条连线是一个 `CanvasConnection`:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
type CanvasConnection = {
|
type CanvasConnection = {
|
||||||
id: string;
|
id: string;
|
||||||
fromNodeId: string;
|
fromNodeId: string;
|
||||||
@@ -122,13 +48,11 @@ type CanvasConnection = {
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
连线只保存节点 ID,不保存端口坐标。渲染时根据节点位置和尺寸计算路径。
|
Node positions and sizes use canvas-world coordinates. Edges store only node IDs; their paths are calculated while rendering. Deleting a node removes connected edges, and deleting an image-batch root also removes its child nodes.
|
||||||
|
|
||||||
删除节点时会同步删除以该节点为起点或终点的连线。删除图片组根节点时,会把对应子节点一起删除。
|
Common metadata includes content, prompt, status, generation mode, model, size, count, natural dimensions, batch relationships, input order, `storageKey`, MIME type, and byte size. Image and video `content` values are normally session-scoped `blob:` URLs; `storageKey` is the persistent identity.
|
||||||
|
|
||||||
## 助手会话结构
|
## Assistant sessions
|
||||||
|
|
||||||
助手会话保存在画布项目内:
|
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
type CanvasAssistantSession = {
|
type CanvasAssistantSession = {
|
||||||
@@ -140,85 +64,10 @@ type CanvasAssistantSession = {
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
消息结构:
|
Message references and generated images use the same blob-storage rules. They are converted to data URLs temporarily only when an AI endpoint requires base64.
|
||||||
|
|
||||||
```ts
|
## Media lifecycle
|
||||||
type CanvasAssistantMessage = {
|
|
||||||
id: string;
|
|
||||||
role: "user" | "assistant";
|
|
||||||
mode: "ask" | "image";
|
|
||||||
text: string;
|
|
||||||
isLoading?: boolean;
|
|
||||||
references?: CanvasAssistantReference[];
|
|
||||||
images?: CanvasAssistantImage[];
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
图片引用和助手生成图片也遵循同一套图片存储规则:
|
`uploadImage()` converts a Blob or data URL to a Blob, creates an `image:<id>` storage key, stores it in `image_files`, creates an in-memory object URL, reads its dimensions, and returns the URL and metadata. Opening a project resolves each storage key into a fresh object URL.
|
||||||
|
|
||||||
- `dataUrl` 字段当前可能是 `blob:` URL,也可能是旧数据中的 `data:image/...`。
|
Cleanup is reference-based. After deleting nodes, canvases, assets, or assistant sessions, the application collects every still-referenced storage key, removes unreferenced blobs, revokes their object URLs, and removes them from the in-memory cache. This prevents deleting a file still shared by another canvas feature.
|
||||||
- `storageKey` 存在时,以 `storageKey` 为准读取图片 Blob。
|
|
||||||
- 发送到 AI 接口前,如果接口需要 base64,会通过 `imageToDataUrl` 临时把 Blob URL 转成 data URL。
|
|
||||||
|
|
||||||
## 图片写入流程
|
|
||||||
|
|
||||||
所有新增图片应通过 `uploadImage(input)` 写入:
|
|
||||||
|
|
||||||
1. 传入 `Blob` 或 data URL。
|
|
||||||
2. 内部转成 `Blob`。
|
|
||||||
3. 生成 `storageKey`,格式为 `image:<id>`。
|
|
||||||
4. 把 Blob 写入 `image_files`。
|
|
||||||
5. 创建 `blob:` URL,并缓存在内存 `objectUrls`。
|
|
||||||
6. 读取图片宽高,返回:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
type UploadedImage = {
|
|
||||||
url: string;
|
|
||||||
storageKey: string;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
bytes: number;
|
|
||||||
mimeType: string;
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
图片节点会通过 `imageMetadata(image)` 写入:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
{
|
|
||||||
content: image.url,
|
|
||||||
storageKey: image.storageKey,
|
|
||||||
status: "success",
|
|
||||||
naturalWidth: image.width,
|
|
||||||
naturalHeight: image.height,
|
|
||||||
bytes: image.bytes,
|
|
||||||
mimeType: image.mimeType
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
因此,`content` 只适合当前浏览器会话展示,不能作为长期文件标识;长期标识是 `storageKey`。
|
|
||||||
|
|
||||||
## 图片读取和旧数据迁移
|
|
||||||
|
|
||||||
打开画布时会执行图片补水:
|
|
||||||
|
|
||||||
- 如果图片节点有 `storageKey`,通过 `resolveImageUrl(storageKey, fallback)` 读取 Blob 并生成新的 `blob:` URL。
|
|
||||||
- 如果图片节点没有 `storageKey`,但 `content` 是旧的 `data:image/...`,会调用 `uploadImage(content)` 迁移到 `image_files`,并补上 `storageKey`。
|
|
||||||
- 助手消息里的引用图和生成图也会执行同类逻辑。
|
|
||||||
|
|
||||||
我的素材读取时也会做迁移:
|
|
||||||
|
|
||||||
- 有 `storageKey`:恢复 `coverUrl` 和 `data.dataUrl` 的可展示 URL。
|
|
||||||
- 无 `storageKey` 且保存了 base64:写入 `image_files`,然后更新素材里的 `storageKey`。
|
|
||||||
|
|
||||||
## 图片移除和清理
|
|
||||||
|
|
||||||
图片不是在删除节点时立即按节点逐张删除,而是做引用清理:
|
|
||||||
|
|
||||||
1. 删除节点、清空画布、删除画布、删除素材、删除助手会话时,会触发 `cleanupImages`。
|
|
||||||
2. `cleanupImages` 会收集当前仍被画布项目、素材和额外传入数据引用的所有 `storageKey`。
|
|
||||||
3. `cleanupUnusedImages` 遍历 `image_files` 中的全部图片。
|
|
||||||
4. 不在引用集合里的图片会被删除。
|
|
||||||
5. 删除时会同时 `URL.revokeObjectURL`,并从内存缓存 `objectUrls` 移除。
|
|
||||||
|
|
||||||
这套方式可以避免同一张图片被画布、素材或助手同时引用时误删。
|
|
||||||
|
|||||||
@@ -0,0 +1,224 @@
|
|||||||
|
---
|
||||||
|
title: 画布数据结构
|
||||||
|
description: 画布本地存储、节点结构、媒体文件与清理机制
|
||||||
|
---
|
||||||
|
|
||||||
|
# 画布数据结构
|
||||||
|
|
||||||
|
本文档说明当前画布在前端本地保存的数据结构、图片文件的存储和清理方式。
|
||||||
|
|
||||||
|
## 当前存储位置
|
||||||
|
|
||||||
|
当前画布项目主要保存在浏览器本地:
|
||||||
|
|
||||||
|
- 画布项目 JSON:`localForage`,数据库名 `infinite-canvas`,storeName `app_state`,key 为 `infinite-canvas:canvas_store`。
|
||||||
|
- 我的素材 JSON:`localForage`,数据库名 `infinite-canvas`,storeName `app_state`,key 为 `infinite-canvas:asset_store`。
|
||||||
|
- 图片 Blob:单独存到 `localForage` 实例,数据库名 `infinite-canvas`,storeName `image_files`。
|
||||||
|
- 视频等媒体 Blob:单独存到 `localForage` 实例,数据库名 `infinite-canvas`,storeName `media_files`。
|
||||||
|
|
||||||
|
画布 JSON 不直接长期保存大体积 base64 图片或视频。图片节点、视频节点、助手图片和素材媒体只保存展示 URL、`storageKey` 和元信息,真实 Blob 通过 `storageKey` 读取。
|
||||||
|
|
||||||
|
## 画布项目结构
|
||||||
|
|
||||||
|
每个画布项目是一个 `CanvasProject`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type CanvasProject = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
nodes: CanvasNodeData[];
|
||||||
|
connections: CanvasConnection[];
|
||||||
|
chatSessions: CanvasAssistantSession[];
|
||||||
|
activeChatId: string | null;
|
||||||
|
backgroundMode: "lines" | "dots" | "blank";
|
||||||
|
viewport: { x: number; y: number; k: number };
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
字段说明:
|
||||||
|
|
||||||
|
- `id`:画布项目 ID,当前前端生成。
|
||||||
|
- `title`:画布名称。
|
||||||
|
- `createdAt` / `updatedAt`:ISO 字符串。
|
||||||
|
- `nodes`:画布节点列表。
|
||||||
|
- `connections`:节点连线列表。
|
||||||
|
- `chatSessions`:右侧画布助手会话。
|
||||||
|
- `activeChatId`:当前选中的助手会话 ID。
|
||||||
|
- `backgroundMode`:画布背景模式。
|
||||||
|
- `viewport`:视口变换,`x/y` 是屏幕平移,`k` 是缩放比例。
|
||||||
|
|
||||||
|
## 节点结构
|
||||||
|
|
||||||
|
每个节点是一个 `CanvasNodeData`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type CanvasNodeData = {
|
||||||
|
id: string;
|
||||||
|
type: "image" | "text" | "config" | "video";
|
||||||
|
title: string;
|
||||||
|
position: { x: number; y: number };
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
metadata?: CanvasNodeMetadata;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
通用字段:
|
||||||
|
|
||||||
|
- `id`:节点 ID。
|
||||||
|
- `type`:节点类型,当前有图片、文本、生成配置、视频四类。
|
||||||
|
- `title`:节点标题。
|
||||||
|
- `position`:画布世界坐标,不是屏幕坐标。
|
||||||
|
- `width` / `height`:画布世界坐标下的节点尺寸。
|
||||||
|
- `metadata`:节点内容和业务状态。
|
||||||
|
|
||||||
|
`metadata` 当前常用字段:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type CanvasNodeMetadata = {
|
||||||
|
content?: string;
|
||||||
|
prompt?: string;
|
||||||
|
status?: "idle" | "success" | "loading" | "error";
|
||||||
|
errorDetails?: string;
|
||||||
|
fontSize?: number;
|
||||||
|
generationMode?: "text" | "image" | "video";
|
||||||
|
model?: string;
|
||||||
|
size?: string;
|
||||||
|
count?: number;
|
||||||
|
naturalWidth?: number;
|
||||||
|
naturalHeight?: number;
|
||||||
|
freeResize?: boolean;
|
||||||
|
isBatchRoot?: boolean;
|
||||||
|
batchRootId?: string;
|
||||||
|
batchChildIds?: string[];
|
||||||
|
primaryImageId?: string;
|
||||||
|
imageBatchExpanded?: boolean;
|
||||||
|
inputOrder?: string[];
|
||||||
|
storageKey?: string;
|
||||||
|
mimeType?: string;
|
||||||
|
bytes?: number;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
不同节点的使用方式:
|
||||||
|
|
||||||
|
- 图片节点:`content` 是当前可展示的图片 URL,通常是 `blob:` URL;`storageKey` 指向本地图片 Blob;`naturalWidth/naturalHeight/bytes/mimeType` 保存原图信息。
|
||||||
|
- 视频节点:`content` 是当前可播放的视频 URL,通常是 `blob:` URL;`storageKey` 指向本地视频 Blob;`bytes/mimeType` 保存文件信息。
|
||||||
|
- 文本节点:`content` 保存文本内容;`fontSize` 保存字体大小;`prompt/status/errorDetails` 保存生成状态。
|
||||||
|
- 生成配置节点:`generationMode/model/size/count/inputOrder` 保存生成配置;`generationMode` 可选择文本、图片或视频;上游输入通过 `connections` 计算。
|
||||||
|
- 图片组节点:根节点用 `isBatchRoot/batchChildIds/primaryImageId/imageBatchExpanded` 记录批量生成结果;子图节点用 `batchRootId` 指回根节点。
|
||||||
|
|
||||||
|
## 连线结构
|
||||||
|
|
||||||
|
每条连线是一个 `CanvasConnection`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type CanvasConnection = {
|
||||||
|
id: string;
|
||||||
|
fromNodeId: string;
|
||||||
|
toNodeId: string;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
连线只保存节点 ID,不保存端口坐标。渲染时根据节点位置和尺寸计算路径。
|
||||||
|
|
||||||
|
删除节点时会同步删除以该节点为起点或终点的连线。删除图片组根节点时,会把对应子节点一起删除。
|
||||||
|
|
||||||
|
## 助手会话结构
|
||||||
|
|
||||||
|
助手会话保存在画布项目内:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type CanvasAssistantSession = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
messages: CanvasAssistantMessage[];
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
消息结构:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type CanvasAssistantMessage = {
|
||||||
|
id: string;
|
||||||
|
role: "user" | "assistant";
|
||||||
|
mode: "ask" | "image";
|
||||||
|
text: string;
|
||||||
|
isLoading?: boolean;
|
||||||
|
references?: CanvasAssistantReference[];
|
||||||
|
images?: CanvasAssistantImage[];
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
图片引用和助手生成图片也遵循同一套图片存储规则:
|
||||||
|
|
||||||
|
- `dataUrl` 字段当前可能是 `blob:` URL,也可能是旧数据中的 `data:image/...`。
|
||||||
|
- `storageKey` 存在时,以 `storageKey` 为准读取图片 Blob。
|
||||||
|
- 发送到 AI 接口前,如果接口需要 base64,会通过 `imageToDataUrl` 临时把 Blob URL 转成 data URL。
|
||||||
|
|
||||||
|
## 图片写入流程
|
||||||
|
|
||||||
|
所有新增图片应通过 `uploadImage(input)` 写入:
|
||||||
|
|
||||||
|
1. 传入 `Blob` 或 data URL。
|
||||||
|
2. 内部转成 `Blob`。
|
||||||
|
3. 生成 `storageKey`,格式为 `image:<id>`。
|
||||||
|
4. 把 Blob 写入 `image_files`。
|
||||||
|
5. 创建 `blob:` URL,并缓存在内存 `objectUrls`。
|
||||||
|
6. 读取图片宽高,返回:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type UploadedImage = {
|
||||||
|
url: string;
|
||||||
|
storageKey: string;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
bytes: number;
|
||||||
|
mimeType: string;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
图片节点会通过 `imageMetadata(image)` 写入:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
{
|
||||||
|
content: image.url,
|
||||||
|
storageKey: image.storageKey,
|
||||||
|
status: "success",
|
||||||
|
naturalWidth: image.width,
|
||||||
|
naturalHeight: image.height,
|
||||||
|
bytes: image.bytes,
|
||||||
|
mimeType: image.mimeType
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
因此,`content` 只适合当前浏览器会话展示,不能作为长期文件标识;长期标识是 `storageKey`。
|
||||||
|
|
||||||
|
## 图片读取和旧数据迁移
|
||||||
|
|
||||||
|
打开画布时会执行图片补水:
|
||||||
|
|
||||||
|
- 如果图片节点有 `storageKey`,通过 `resolveImageUrl(storageKey, fallback)` 读取 Blob 并生成新的 `blob:` URL。
|
||||||
|
- 如果图片节点没有 `storageKey`,但 `content` 是旧的 `data:image/...`,会调用 `uploadImage(content)` 迁移到 `image_files`,并补上 `storageKey`。
|
||||||
|
- 助手消息里的引用图和生成图也会执行同类逻辑。
|
||||||
|
|
||||||
|
我的素材读取时也会做迁移:
|
||||||
|
|
||||||
|
- 有 `storageKey`:恢复 `coverUrl` 和 `data.dataUrl` 的可展示 URL。
|
||||||
|
- 无 `storageKey` 且保存了 base64:写入 `image_files`,然后更新素材里的 `storageKey`。
|
||||||
|
|
||||||
|
## 图片移除和清理
|
||||||
|
|
||||||
|
图片不是在删除节点时立即按节点逐张删除,而是做引用清理:
|
||||||
|
|
||||||
|
1. 删除节点、清空画布、删除画布、删除素材、删除助手会话时,会触发 `cleanupImages`。
|
||||||
|
2. `cleanupImages` 会收集当前仍被画布项目、素材和额外传入数据引用的所有 `storageKey`。
|
||||||
|
3. `cleanupUnusedImages` 遍历 `image_files` 中的全部图片。
|
||||||
|
4. 不在引用集合里的图片会被删除。
|
||||||
|
5. 删除时会同时 `URL.revokeObjectURL`,并从内存缓存 `objectUrls` 移除。
|
||||||
|
|
||||||
|
这套方式可以避免同一张图片被画布、素材或助手同时引用时误删。
|
||||||
@@ -1,171 +1,54 @@
|
|||||||
---
|
---
|
||||||
title: 本地 Codex 连接画布原理
|
title: How Local Codex Connects to the Canvas
|
||||||
description: 说明 Canvas Agent、MCP、SSE 和浏览器画布之间如何交互
|
description: Interaction between Canvas Agent, MCP, SSE, and browser canvas state
|
||||||
---
|
---
|
||||||
|
|
||||||
# 本地 Codex 连接画布原理
|
# How Local Codex Connects to the Canvas
|
||||||
|
|
||||||
本地 Codex 连接画布的核心不是让 Codex 直接控制浏览器,也不是模拟鼠标点击或操作 DOM,而是在用户电脑上运行一个 `Canvas Agent` 作为桥接服务。
|
Codex does not control the browser DOM or simulate mouse actions. A local `Canvas Agent` bridges Codex tools and the React state that owns the canvas.
|
||||||
|
|
||||||
```txt
|
```text
|
||||||
Codex / 浏览器侧边栏
|
Codex or browser side panel
|
||||||
|
|
↓
|
||||||
v
|
Local Canvas Agent
|
||||||
本机 Canvas Agent
|
↓
|
||||||
|
|
Browser canvas React state
|
||||||
v
|
|
||||||
浏览器画布 React 状态
|
|
||||||
```
|
```
|
||||||
|
|
||||||
真正的画布数据仍然在浏览器里,节点、连线、选区和视口都由前端 React 状态维护。Codex 只能通过 `Canvas Agent` 暴露的工具协议发出“读取画布”或“执行画布操作”的请求。
|
## Two entry points
|
||||||
|
|
||||||
## 两个使用方向
|
From a local Codex conversation, the `infinite-canvas` MCP tools call Canvas Agent's `/api/tools` endpoint. Common tools read canvas state and selection, create or update nodes, connect nodes, and start image or text generation.
|
||||||
|
|
||||||
### 1. 本地 Codex 主动连接画布
|
From the browser side panel, the browser sends a turn to `/agent/codex/turn`. Canvas Agent starts or reuses `codex app-server --stdio` and provides the same MCP tools to that conversation. Canvas writes still follow the same path: `Codex → MCP → Canvas Agent → browser`.
|
||||||
|
|
||||||
这个方向的入口在 Codex。
|
## Browser connection and state
|
||||||
|
|
||||||
```txt
|
Canvas Agent listens on `http://127.0.0.1:17371` by default and generates a connection token. The browser opens an authenticated SSE connection:
|
||||||
本地 Codex
|
|
||||||
-> infinite-canvas MCP 工具
|
|
||||||
-> 本机 Canvas Agent
|
|
||||||
-> 浏览器画布
|
|
||||||
```
|
|
||||||
|
|
||||||
用户在本地 Codex 里发起请求,例如读取画布、创建文本节点、连接节点或触发生成。Codex 调用 `infinite-canvas` MCP 工具,MCP 工具再请求本机 `Canvas Agent` 的 `/api/tools` 接口。
|
```text
|
||||||
|
|
||||||
常见工具包括:
|
|
||||||
|
|
||||||
- `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
|
GET /events?token=xxx&clientId=xxx
|
||||||
```
|
```
|
||||||
|
|
||||||
连接成功后,`Canvas Agent` 会发送 `hello` 事件,浏览器把状态更新为已连接。
|
The browser pushes snapshots to:
|
||||||
|
|
||||||
## 画布状态如何同步给 Codex
|
```text
|
||||||
|
|
||||||
浏览器会把当前画布快照主动同步给 `Canvas Agent`:
|
|
||||||
|
|
||||||
```txt
|
|
||||||
POST /canvas/state?token=xxx&clientId=xxx
|
POST /canvas/state?token=xxx&clientId=xxx
|
||||||
```
|
```
|
||||||
|
|
||||||
快照包含:
|
Snapshots contain the canvas ID and title, nodes, edges, selected node IDs, and viewport. Canvas Agent keeps the latest snapshot in memory, so tools read structured state rather than the DOM.
|
||||||
|
|
||||||
- 当前画布 ID 和标题;
|
## Applying operations
|
||||||
- 节点列表;
|
|
||||||
- 连线列表;
|
|
||||||
- 当前选中的节点 ID;
|
|
||||||
- 当前视口位置和缩放。
|
|
||||||
|
|
||||||
`Canvas Agent` 把最近一次快照保存在内存里。Codex 调用 `canvas_get_state` 时,读取的是这份由浏览器同步过来的最新快照,而不是直接读取浏览器 DOM。
|
For a node creation, Codex calls an MCP tool, Canvas Agent converts it to `canvas_apply_ops`, sends an SSE `tool_call`, and the browser applies operations such as `add_node`, `update_node`, `delete_node`, `connect_nodes`, `set_viewport`, and `select_nodes`. The browser posts the result to `/canvas/result`, which completes the MCP call.
|
||||||
|
|
||||||
## Codex 如何操作画布
|
Writes are automatically confirmed by default. Switch the side panel to manual confirmation to approve each write. The frontend also stores the snapshot before the latest Agent operation so it can be undone.
|
||||||
|
|
||||||
以创建文本节点为例,流程如下:
|
## Security boundary
|
||||||
|
|
||||||
```txt
|
- Canvas Agent binds to `127.0.0.1` by default.
|
||||||
Codex 调 canvas_create_text_node
|
- Browser connections require the generated token and are bound to the first accepted origin.
|
||||||
-> MCP POST /api/tools
|
- Codex does not directly access the DOM or own the canvas state.
|
||||||
-> Canvas Agent 转成 canvas_apply_ops
|
- The hosted website does not store the local Codex login, API keys, or local file permissions.
|
||||||
-> Canvas Agent 通过 SSE 发送 tool_call
|
- The browser remains responsible for executing writes, approvals, undo, and permission controls.
|
||||||
-> 浏览器执行 add_node
|
|
||||||
-> 浏览器 POST /canvas/result
|
|
||||||
-> Canvas Agent 返回结果给 Codex
|
|
||||||
```
|
|
||||||
|
|
||||||
`Canvas Agent` 会把一些高级工具转换成统一的画布操作:
|
In short, reads flow from browser snapshots through Canvas Agent and MCP to Codex; writes flow from Codex through MCP and SSE back to browser React state.
|
||||||
|
|
||||||
```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,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
|
||||||
|
```
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
---
|
---
|
||||||
title: 本地开发
|
title: Local Development
|
||||||
description: 前端优先的本地开发方式
|
description: Frontend-first local development
|
||||||
---
|
---
|
||||||
|
|
||||||
# 本地开发
|
# Local Development
|
||||||
|
|
||||||
当前主应用以 `web/` 前端为主,AI 请求由浏览器前台直连用户自己的 OpenAI 兼容接口。
|
The main application lives in `web/`. The browser sends AI requests directly to the user's OpenAI-compatible endpoint.
|
||||||
|
|
||||||
## 1. 启动前端
|
## Start the frontend
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd web
|
cd web
|
||||||
@@ -15,26 +15,17 @@ bun install
|
|||||||
bun run dev
|
bun run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
前端默认访问:
|
Open `http://localhost:3000`, then enter your `Base URL`, `API Key`, and model names in Settings. Prompt sources and WebDAV are also accessed directly by the browser.
|
||||||
|
|
||||||
```text
|
## Start the documentation site
|
||||||
http://localhost:3000
|
|
||||||
```
|
|
||||||
|
|
||||||
## 2. 配置模型
|
|
||||||
|
|
||||||
打开右上角配置弹窗,填写自己的 `Base URL`、`API Key` 和模型名。第三方提示词和 WebDAV 都由浏览器前端直连。
|
|
||||||
|
|
||||||
## 3. 启动文档站
|
|
||||||
|
|
||||||
如果需要单独调整文档站,在 `docs` 目录执行:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
cd docs
|
||||||
bun run dev
|
bun run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
## 常见场景
|
Common locations:
|
||||||
|
|
||||||
- 改画布、页面和交互:主要看 `web/`
|
- Canvas, pages, and interactions: `web/`
|
||||||
- 改提示词缓存:主要看 `web/src/services/api/prompts.ts`
|
- Prompt caching: `web/src/services/api/prompts.ts`
|
||||||
- 改文档站内容:主要看 `docs/content/docs/`
|
- Documentation content: `docs/content/docs/`
|
||||||
|
|||||||
@@ -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/`
|
||||||
@@ -1,10 +1,6 @@
|
|||||||
{
|
{
|
||||||
"title": "开发文档",
|
"title": "Development",
|
||||||
"root": true,
|
"root": true,
|
||||||
"defaultOpen": true,
|
"defaultOpen": true,
|
||||||
"pages": [
|
"pages": ["local-development", "canvas-data-structure", "local-codex-canvas"]
|
||||||
"local-development",
|
|
||||||
"canvas-data-structure",
|
|
||||||
"local-codex-canvas"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"title": "开发文档",
|
||||||
|
"root": true,
|
||||||
|
"defaultOpen": true,
|
||||||
|
"pages": [
|
||||||
|
"local-development",
|
||||||
|
"canvas-data-structure",
|
||||||
|
"local-codex-canvas"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,12 +1,5 @@
|
|||||||
{
|
{
|
||||||
"title": "文档",
|
"title": "Documentation",
|
||||||
"root": true,
|
"root": true,
|
||||||
"pages": [
|
"pages": ["overview", "canvas", "development", "progress", "business", "support"]
|
||||||
"overview",
|
|
||||||
"canvas",
|
|
||||||
"development",
|
|
||||||
"progress",
|
|
||||||
"business",
|
|
||||||
"support"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"title": "文档",
|
||||||
|
"root": true,
|
||||||
|
"pages": [
|
||||||
|
"overview",
|
||||||
|
"canvas",
|
||||||
|
"development",
|
||||||
|
"progress",
|
||||||
|
"business",
|
||||||
|
"support"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,76 +1,36 @@
|
|||||||
---
|
---
|
||||||
title: Codex App 插件
|
title: Codex App Plugin
|
||||||
description: 在 Codex App 中安装并使用 Infinite Canvas 插件
|
description: Install and use the Infinite Canvas plugin in Codex App
|
||||||
---
|
---
|
||||||
|
|
||||||
# Codex App 插件
|
# Codex App Plugin
|
||||||
|
|
||||||
Infinite Canvas 提供了一个 Codex App 插件,用来把 Codex 和本地 Canvas Agent 连接起来。安装后,你可以直接在 Codex 中说“打开 Infinite Canvas”,让 Codex 打开画布、读取节点、创建节点并触发生成流程。
|
The plugin connects Codex to a local Canvas Agent, allowing Codex to open Infinite Canvas, read and create nodes, connect nodes, and start generation workflows. Its source is in `plugins/infinite-canvas/`.
|
||||||
|
|
||||||
插件源码位于仓库的 `plugins/infinite-canvas/` 目录。
|
## Ask Codex to install it
|
||||||
|
|
||||||
## 安装方式
|
Send this request to Codex:
|
||||||
|
|
||||||
### 让 Codex 自动安装
|
|
||||||
|
|
||||||
把下面这段发给 Codex:
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
请从 https://github.com/basketikun/infinite-canvas.git 安装 Infinite Canvas Codex 插件。
|
Install the Infinite Canvas Codex plugin from https://github.com/basketikun/infinite-canvas.git.
|
||||||
请 clone 仓库到 ~/plugins/infinite-canvas,确认 plugins/infinite-canvas/.codex-plugin/plugin.json 存在,
|
Clone it to ~/plugins/infinite-canvas, verify plugins/infinite-canvas/.codex-plugin/plugin.json,
|
||||||
把 plugins/infinite-canvas 加入 personal marketplace,先运行 codex plugin marketplace add ~,
|
add the repository to my personal marketplace with `codex plugin marketplace add ~`,
|
||||||
再运行 codex plugin add infinite-canvas@personal。
|
then install `infinite-canvas@personal`. Validate the installation and tell me whether a new
|
||||||
安装后请校验插件,并告诉我是否需要开启一个新对话来加载新技能和 MCP 工具。
|
conversation is required to load the Skill and MCP tools.
|
||||||
```
|
```
|
||||||
|
|
||||||
这种方式适合不想手动改 marketplace 配置的用户。Codex 会完成 clone、写入 marketplace、安装插件和基础校验。
|
## Manual installation
|
||||||
|
|
||||||
### 手动安装
|
|
||||||
|
|
||||||
推荐把仓库 clone 到 Codex personal marketplace 默认会引用的位置:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mkdir -p ~/plugins
|
mkdir -p ~/plugins
|
||||||
git clone https://github.com/basketikun/infinite-canvas.git ~/plugins/infinite-canvas
|
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 marketplace add ~
|
||||||
codex plugin add infinite-canvas@personal
|
codex plugin add infinite-canvas@personal
|
||||||
```
|
```
|
||||||
|
|
||||||
安装完成后,建议新建一个 Codex 对话,让新的 skill 和 MCP 工具完整加载。
|
The personal marketplace entry must point to `./plugins/infinite-canvas/plugins/infinite-canvas`. Start a new Codex conversation after installation so the Skill and MCP tools load completely.
|
||||||
|
|
||||||
### 本仓库开发调试安装
|
For development from this repository:
|
||||||
|
|
||||||
如果你正在 Infinite Canvas 仓库中开发插件,可以直接使用仓库自带的 marketplace:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /path/to/infinite-canvas
|
cd /path/to/infinite-canvas
|
||||||
@@ -78,63 +38,14 @@ codex plugin marketplace add "$(pwd)"
|
|||||||
codex plugin add infinite-canvas@infinite-canvas-local
|
codex plugin add infinite-canvas@infinite-canvas-local
|
||||||
```
|
```
|
||||||
|
|
||||||
仓库内的 `.agents/plugins/marketplace.json` 已经指向 `./plugins/infinite-canvas`,适合开发者本地调试。
|
## Usage
|
||||||
|
|
||||||
## 使用插件
|
In a new Codex conversation, ask it to open Infinite Canvas, create a canvas, and connect Codex. The plugin starts Canvas Agent, reads its connection URL and token, opens `https://canvas.best/`, and creates and connects a canvas. It starts a local frontend only when explicitly requested.
|
||||||
|
|
||||||
新建 Codex 对话后,可以直接说:
|
You can then ask Codex to summarize the current canvas, create prompts from selected nodes, or organize nodes into a generation workflow.
|
||||||
|
|
||||||
```text
|
Reinstall or refresh the plugin after changing its manifest, MCP configuration, Skills, exposed MCP tools, app, or entry scripts. The agent's default MCP command is:
|
||||||
打开 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
|
```bash
|
||||||
npx -y @basketikun/canvas-agent mcp
|
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,不需要手动点击“新建画布”。
|
|
||||||
|
|||||||
@@ -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,不需要手动点击“新建画布”。
|
||||||
@@ -1,13 +1,11 @@
|
|||||||
---
|
---
|
||||||
title: Docker 部署
|
title: Docker Deployment
|
||||||
description: 使用 Docker Compose 部署无限画布
|
description: Deploy Infinite Canvas with Docker Compose
|
||||||
---
|
---
|
||||||
|
|
||||||
# Docker 部署
|
# Docker Deployment
|
||||||
|
|
||||||
如果你希望在自己的机器或服务器上运行项目,可以直接使用 Docker Compose。
|
## Use the published image
|
||||||
|
|
||||||
## 使用发布镜像
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone git@github.com:basketikun/infinite-canvas.git
|
git clone git@github.com:basketikun/infinite-canvas.git
|
||||||
@@ -15,54 +13,40 @@ cd infinite-canvas
|
|||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
启动后访问:
|
Open `http://localhost:3000`.
|
||||||
|
|
||||||
```text
|
## Build locally
|
||||||
http://localhost:3000
|
|
||||||
```
|
|
||||||
|
|
||||||
## 本地构建镜像
|
|
||||||
|
|
||||||
如果需要基于当前源码构建镜像:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose -f docker-compose.local.yml up -d --build
|
docker compose -f docker-compose.local.yml up -d --build
|
||||||
```
|
```
|
||||||
|
|
||||||
## 文档站镜像
|
## Documentation image
|
||||||
|
|
||||||
文档站位于 `docs/` 目录,按带服务端能力的 Next.js standalone 应用单独构建,不打进主应用镜像。
|
The documentation in `docs/` is a separate Next.js standalone application with server capabilities.
|
||||||
|
|
||||||
使用发布镜像:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd docs
|
cd docs
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
基于当前源码本地构建:
|
To build it from the current source:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd docs
|
cd docs
|
||||||
docker compose -f docker-compose.local.yml up -d --build
|
docker compose -f docker-compose.local.yml up -d --build
|
||||||
```
|
```
|
||||||
|
|
||||||
## 数据说明
|
The main application container only runs Next.js. Canvases, My Assets, generation history, and API keys are stored in the browser by default, while prompt repositories are fetched directly by the frontend. No persistent data volume is required for this data.
|
||||||
|
|
||||||
当前主应用镜像只启动 Next.js。画布、我的素材、生成记录和 AI API Key 默认保存在浏览器本地;第三方提示词由浏览器前端直连拉取,不需要额外挂载数据目录。
|
## Optional analytics
|
||||||
|
|
||||||
## 统计分析(可选)
|
No analytics are enabled by default. A deployment can enable GA4, Baidu Analytics, or both at runtime:
|
||||||
|
|
||||||
项目**默认不接入任何统计**:不设置下列环境变量时,页面不会加载任何统计脚本,也不会发送任何外部请求。你在自己的部署上想查看流量时,设置对应环境变量即可,无需修改代码或重新构建镜像——镜像启动时会据此生成运行期配置。
|
| Variable | Description |
|
||||||
|
|
||||||
统计按「每家一个独立变量」配置:**填了谁就启用谁,可同时启用多家**(例如 GA4 + 百度一起开)。仅支持 GA4 与百度,两者都只接受 ID,脚本地址由代码固定拼接,不接受任意脚本地址或内联 JS。
|
|
||||||
|
|
||||||
| 变量 | 说明 |
|
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `ANALYTICS_GA4_ID` | Google Analytics 4 衡量 ID(`G-XXXX`) |
|
| `ANALYTICS_GA4_ID` | Google Analytics 4 measurement ID (`G-XXXX`) |
|
||||||
| `ANALYTICS_BAIDU_ID` | 百度统计站点 ID(`hm.js?` 后面那串 hash) |
|
| `ANALYTICS_BAIDU_ID` | Baidu site ID, the hash after `hm.js?` |
|
||||||
|
|
||||||
在 `docker-compose.yml` 的 `environment` 中配置(文件内已附注释示例),下例同时启用 GA4 与百度:
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
@@ -75,17 +59,4 @@ services:
|
|||||||
ANALYTICS_BAIDU_ID: e8263761cc5c32f5f7a855b8203b965a
|
ANALYTICS_BAIDU_ID: e8263761cc5c32f5f7a855b8203b965a
|
||||||
```
|
```
|
||||||
|
|
||||||
或使用 `docker run`:
|
SPA route changes automatically report page views to enabled providers. Only configure analytics IDs on sites you control, and do not commit them to the repository.
|
||||||
|
|
||||||
```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>
|
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -1,172 +1,64 @@
|
|||||||
---
|
---
|
||||||
title: 功能介绍
|
title: Features
|
||||||
description: 当前项目已实现的主要功能
|
description: Major features available in the current project
|
||||||
---
|
---
|
||||||
|
|
||||||
# 功能介绍
|
# Features
|
||||||
|
|
||||||
本文档记录当前项目已经实现的主要功能。
|
## Canvas projects
|
||||||
|
|
||||||
## 画布项目
|
- Create, rename, duplicate, delete, import, and export canvas projects.
|
||||||
|
- Persist projects in the browser and optionally synchronize them with WebDAV.
|
||||||
|
- Keep viewport, background, nodes, connections, and assistant sessions with each project.
|
||||||
|
|
||||||
- 支持创建多个画布项目。
|
## Infinite canvas
|
||||||
- 支持项目重命名、删除、批量选择和批量删除。
|
|
||||||
- 支持单个画布项目导出为 JSON,也支持从 JSON 导入画布。
|
|
||||||
- 画布项目保存在浏览器本地,登录账号后暂不会自动同步到服务器。
|
|
||||||
|
|
||||||
## 无限画布
|
- Pan, zoom, reset the view, select multiple nodes, copy and paste, undo and redo.
|
||||||
|
- Drag files onto the canvas and use lines, dots, or a blank background.
|
||||||
|
- Export the entire canvas or selected elements with their resources.
|
||||||
|
|
||||||
- 支持拖动画布、滚轮缩放、缩放滑杆和重置视图。
|
## Nodes and workflows
|
||||||
- 支持小地图定位,可开关小地图。
|
|
||||||
- 支持点阵、网格线、空白三种背景。
|
|
||||||
- 支持浅色和深色主题。
|
|
||||||
- 支持框选、多选、全选、取消选择、删除选中。
|
|
||||||
- 支持复制粘贴节点和节点之间的连线。
|
|
||||||
- 支持撤销和重做节点、连线、视口、背景和助手会话变化。
|
|
||||||
- 支持节点连线,并高亮当前节点相关的上下游节点和连线。
|
|
||||||
- 支持快捷键帮助,覆盖缩放、框选、全选、复制粘贴、撤销重做、删除、退出选择和拖入图片。
|
|
||||||
|
|
||||||
## 节点
|
- Text, image, video, and generation-configuration nodes.
|
||||||
|
- Connect text prompts and reference media to generation nodes.
|
||||||
|
- Generate or rewrite text, create images, edit images from references, and create videos.
|
||||||
|
- Preserve original image ratios by default and organize batch results as connected nodes.
|
||||||
|
- Record prompts, models, dimensions, quality, and references on generated nodes for retries.
|
||||||
|
|
||||||
目前画布中有三类节点:
|
## AI generation
|
||||||
|
|
||||||
- 图片节点:展示上传图片、生成图片或我的素材图片。
|
- Connect to user-provided OpenAI-compatible endpoints directly from the browser.
|
||||||
- 文本节点:保存提示词、说明文案、AI 文字回答等文本内容。
|
- Configure multiple channels, fetch or enter model names, and choose default text, image, and video models.
|
||||||
- 生成配置节点:汇总上游文本和图片,统一配置模型、比例、数量后批量生成图片或文本。
|
- Support image count, aspect ratio, quality, transparent backgrounds, reasoning effort, and custom invocation scripts.
|
||||||
|
- Support OpenAI-style video endpoints and Volcengine Ark generation tasks.
|
||||||
|
|
||||||
节点支持:
|
## Canvas Agent
|
||||||
|
|
||||||
- 拖拽移动。
|
- Connect local Codex through Canvas Agent and MCP without giving a web server direct access to browser state.
|
||||||
- 四角缩放。
|
- Read the active canvas, selection, nodes, edges, generation status, and apply structured operations.
|
||||||
- 图片节点等比缩放或自由比例切换。
|
- Use Codex from the browser side panel with streaming responses, history, approvals, permissions, diagnostics, and local Skills.
|
||||||
- 查看节点基础信息和 JSON。
|
- Isolate active canvases and turns across multiple browser tabs.
|
||||||
- 删除、复制、粘贴。
|
|
||||||
- 通过左右连接点建立上下游关系。
|
|
||||||
|
|
||||||
## 图片工作流
|
See [Codex App Plugin](/docs/overview/codex-app-plugin) for installation and usage.
|
||||||
|
|
||||||
- 支持上传图片到新节点。
|
## Plugin system
|
||||||
- 支持拖拽图片文件到画布。
|
|
||||||
- 支持替换已有图片节点内容。
|
|
||||||
- 支持下载图片节点。
|
|
||||||
- 支持把图片节点保存到“我的素材”。
|
|
||||||
- 支持图片裁剪,并把裁剪结果生成为新的图片节点。
|
|
||||||
- 支持本地多角度变换,并把结果生成为新的图片节点。
|
|
||||||
- 支持生成失败后重试。
|
|
||||||
- 批量生成多张图片时会先展示为图片组节点,支持叠卡预览、展开查看全部结果并设置主图。
|
|
||||||
|
|
||||||
## AI 生成
|
Canvas plugins can contribute node types, rendering, inspectors, serialization, migration, toolbar actions, and optional AI generation capabilities. Built-in and local plugins are managed through the canvas UI.
|
||||||
|
|
||||||
项目默认使用前台直连:前端使用浏览器本地配置的 Base URL、API Key 和 Model 直接请求 OpenAI 兼容接口,不再通过项目服务端转发 AI 请求。
|
## Prompts and assets
|
||||||
|
|
||||||
OpenAI 兼容图像和文本能力继续复用现有接口:
|
- Synchronize supported public prompt repositories and add custom standard JSON sources.
|
||||||
|
- Search across enabled sources, filter by tags, inspect prompt details, copy prompts, and insert them into the canvas.
|
||||||
|
- Store reusable images, videos, audio, and prompt assets in the browser.
|
||||||
|
|
||||||
- `/v1/images/generations`:文生图。
|
## Configuration and synchronization
|
||||||
- `/v1/images/edits`:图生图/参考图编辑。
|
|
||||||
- `/v1/responses`:文本问答、带图问答和在线 Agent 工具调用。
|
|
||||||
- `/v1/models`:读取模型列表;火山方舟 Agent Plan 专属 `/api/plan/v3` 当前未提供 OpenAI `/models` 模型列表接口,需要手动填写模型名。
|
|
||||||
|
|
||||||
视频能力支持两类接口:
|
- Export and import channel, model, generation, prompt-source, and WebDAV settings.
|
||||||
|
- Store application data and API credentials locally in the browser.
|
||||||
|
- Use WebDAV for optional cross-device synchronization.
|
||||||
|
|
||||||
- OpenAI 风格视频:`POST /v1/videos`、`GET /v1/videos/{id}`、`GET /v1/videos/{id}/content`。
|
## Current limitations
|
||||||
- 火山方舟 Agent Plan / Seedance 2.0:Base 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。
|
- The project is frontend-first and does not provide built-in cloud accounts or cloud storage.
|
||||||
|
- API keys remain in browser storage and requests are sent directly to configured providers.
|
||||||
配置弹窗里的“拉取模型列表”会尝试真实请求 OpenAI `/models`,不会为 Agent Plan 伪造模型结果。如果火山方舟 Agent Plan 返回 404,请手动增加 `doubao-seedance-2.0` 或文档列出的其他模型名。
|
- Production Docker static-resource paths still require broader deployment verification.
|
||||||
|
|
||||||
可配置项:
|
|
||||||
|
|
||||||
- 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 的有效期。
|
|
||||||
- 画布更适合桌面端使用,移动端触控体验还未系统完善。
|
|
||||||
|
|||||||
@@ -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.0:Base 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 插件](/zh-CN/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 的有效期。
|
||||||
|
- 画布更适合桌面端使用,移动端触控体验还未系统完善。
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"title": "项目介绍",
|
"title": "Overview",
|
||||||
"root": true,
|
"root": true,
|
||||||
"defaultOpen": true,
|
"defaultOpen": true,
|
||||||
"pages": [
|
"pages": [
|
||||||
@@ -9,6 +9,6 @@
|
|||||||
"render",
|
"render",
|
||||||
"docker",
|
"docker",
|
||||||
"third-party-prompt-repositories",
|
"third-party-prompt-repositories",
|
||||||
"[在线体验](https://canvas.best/)"
|
"[Live Demo](https://canvas.best/)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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/)"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,48 +1,38 @@
|
|||||||
---
|
---
|
||||||
title: 快速开始
|
title: Quick Start
|
||||||
description: 用最少步骤把无限画布跑起来
|
description: Run Infinite Canvas in a few steps
|
||||||
---
|
---
|
||||||
|
|
||||||
# 快速开始
|
# Quick Start
|
||||||
|
|
||||||
如果你只是想先把项目跑起来,优先部署或启动 `web/` 前端。
|
To try the project, deploy or start the `web/` frontend.
|
||||||
|
|
||||||
## Vercel 部署
|
## Deploy with Vercel
|
||||||
|
|
||||||
在 Vercel 中导入仓库即可,根目录 `vercel.json` 会构建 `web/`。当前版本的 AI 请求由浏览器前台直连用户自己的 OpenAI 兼容地址,不需要额外配置服务端。
|
Import the repository into Vercel. The root `vercel.json` builds `web/`. AI requests are sent directly from the browser to your OpenAI-compatible endpoint, so no additional application server is required.
|
||||||
|
|
||||||
## 本地启动
|
## Run locally
|
||||||
|
|
||||||
```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/web
|
||||||
cd web
|
|
||||||
bun install
|
bun install
|
||||||
bun run dev
|
bun run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
启动后访问:
|
Open `http://localhost:3000`.
|
||||||
|
|
||||||
```text
|
## Run with Docker
|
||||||
http://localhost:3000
|
|
||||||
```
|
|
||||||
|
|
||||||
## Docker 启动
|
|
||||||
|
|
||||||
如果你需要基于当前源码构建镜像:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker build -t infinite-canvas .
|
docker build -t infinite-canvas .
|
||||||
docker run --rm -p 3000:3000 infinite-canvas
|
docker run --rm -p 3000:3000 infinite-canvas
|
||||||
```
|
```
|
||||||
|
|
||||||
## 首次使用建议
|
## First-time setup
|
||||||
|
|
||||||
- 先打开右上角配置弹窗,填入自己的 `Base URL`、`API Key` 和模型名。
|
- Open Settings and enter your `Base URL`, `API Key`, and model names.
|
||||||
- 如果需要提示词仓库内容,打开 `/prompts` 会由浏览器前端直连拉取并缓存到 IndexedDB。
|
- Open `/prompts` to fetch prompt repositories directly from the browser and cache them in IndexedDB.
|
||||||
- 如果需要跨设备同步画布、素材和生成记录,可在配置弹窗中填写 WebDAV。
|
- Configure WebDAV if you want to synchronize canvases, assets, and generation history across devices.
|
||||||
|
|
||||||
## 说明
|
Canvas projects and My Assets are primarily stored in the browser. The API key is also stored locally and used by the browser to call OpenAI-compatible endpoints directly.
|
||||||
|
|
||||||
- 当前画布项目和“我的素材”主要保存在浏览器本地,WebDAV 同步需要用户自行配置。
|
|
||||||
- AI API Key 保存在浏览器本地,并由前端直接请求 OpenAI 兼容接口。
|
|
||||||
|
|||||||
@@ -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 兼容接口。
|
||||||
@@ -1,28 +1,16 @@
|
|||||||
---
|
---
|
||||||
title: Render 部署
|
title: Deploy on Render
|
||||||
description: 使用 Render 部署无限画布
|
description: Deploy Infinite Canvas with Render
|
||||||
---
|
---
|
||||||
|
|
||||||
# Render 部署
|
# Deploy on Render
|
||||||
|
|
||||||
点击下面链接即可部署到 Render:
|
[Deploy to Render](https://render.com/deploy?repo=https://github.com/basketikun/infinite-canvas)
|
||||||
|
|
||||||
[部署到 Render](https://render.com/deploy?repo=https://github.com/basketikun/infinite-canvas)
|
1. Select **Deploy to Render**.
|
||||||
|
2. Sign in and connect GitHub as prompted.
|
||||||
|
3. Confirm the deployment.
|
||||||
|
|
||||||
## 部署步骤
|
When deployment finishes, open the assigned `.onrender.com` domain.
|
||||||
|
|
||||||
1. 点击 `Deploy to Render`。
|
The default configuration uses Render's free Web Service. It sleeps after a period of inactivity and wakes on the next request. Application data is primarily stored in the browser, and prompt repositories are fetched by the frontend. The free tier is suitable for evaluation and demos; use Vercel or configure WebDAV for longer-term use.
|
||||||
2. 登录 Render,并按页面提示连接 GitHub。
|
|
||||||
3. 确认部署。
|
|
||||||
|
|
||||||
部署完成后,打开 Render 分配的 `.onrender.com` 域名即可访问。
|
|
||||||
|
|
||||||
## 免费版说明
|
|
||||||
|
|
||||||
默认使用 Render 免费 Web Service:
|
|
||||||
|
|
||||||
- 空闲约 15 分钟后会休眠,下次访问会自动唤醒。
|
|
||||||
- 当前主应用数据默认保存在浏览器本地;第三方提示词由浏览器前端直连拉取。
|
|
||||||
- 适合体验和演示,不适合长期保存正式数据。
|
|
||||||
|
|
||||||
长期使用建议优先部署到 Vercel,或自行配置 WebDAV 同步浏览器本地数据。
|
|
||||||
|
|||||||
@@ -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,14 +1,14 @@
|
|||||||
---
|
---
|
||||||
title: 第三方 GitHub 提示词仓库
|
title: Third-party GitHub Prompt Repositories
|
||||||
description: 当前已接入同步逻辑的第三方提示词仓库
|
description: Third-party prompt repositories supported by the current synchronization logic
|
||||||
---
|
---
|
||||||
|
|
||||||
# 第三方 GitHub 提示词仓库
|
# Third-party GitHub Prompt Repositories
|
||||||
|
|
||||||
| 地址 | 状态 |
|
| Repository | Status |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| https://github.com/ZeroLu/awesome-gpt-image | 已实现同步逻辑 |
|
| https://github.com/ZeroLu/awesome-gpt-image | Supported |
|
||||||
| https://github.com/ImgEdify/Awesome-GPT4o-Image-Prompts | 已实现同步逻辑 |
|
| https://github.com/ImgEdify/Awesome-GPT4o-Image-Prompts | Supported |
|
||||||
| https://github.com/YouMind-OpenLab/awesome-gpt-image-2 | 已实现同步逻辑 |
|
| https://github.com/YouMind-OpenLab/awesome-gpt-image-2 | Supported |
|
||||||
| https://github.com/YouMind-OpenLab/awesome-nano-banana-pro-prompts | 已实现同步逻辑 |
|
| https://github.com/YouMind-OpenLab/awesome-nano-banana-pro-prompts | Supported |
|
||||||
| https://github.com/davidwuw0811-boop/awesome-gpt-image2-prompts | 已实现同步逻辑 |
|
| https://github.com/davidwuw0811-boop/awesome-gpt-image2-prompts | Supported |
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
title: 第三方 GitHub 提示词仓库
|
||||||
|
description: 当前已接入同步逻辑的第三方提示词仓库
|
||||||
|
---
|
||||||
|
|
||||||
|
# 第三方 GitHub 提示词仓库
|
||||||
|
|
||||||
|
| 地址 | 状态 |
|
||||||
|
| --- | --- |
|
||||||
|
| https://github.com/ZeroLu/awesome-gpt-image | 已实现同步逻辑 |
|
||||||
|
| https://github.com/ImgEdify/Awesome-GPT4o-Image-Prompts | 已实现同步逻辑 |
|
||||||
|
| https://github.com/YouMind-OpenLab/awesome-gpt-image-2 | 已实现同步逻辑 |
|
||||||
|
| https://github.com/YouMind-OpenLab/awesome-nano-banana-pro-prompts | 已实现同步逻辑 |
|
||||||
|
| https://github.com/davidwuw0811-boop/awesome-gpt-image2-prompts | 已实现同步逻辑 |
|
||||||
@@ -1,248 +1,41 @@
|
|||||||
---
|
---
|
||||||
title: 本地 Agent 接入规划
|
title: Local Agent Integration Plan
|
||||||
description: 规划通过本地 Canvas Agent、MCP 和侧边栏助手连接 Codex / Claude Code 操作画布
|
description: Plan for connecting Codex and Claude to the canvas through Canvas Agent and MCP
|
||||||
---
|
---
|
||||||
|
|
||||||
# 本地 Agent 接入规划
|
# Local Agent Integration Plan
|
||||||
|
|
||||||
本文档规划个人用户在访问线上画布网页时,如何连接自己电脑上的 Codex / Claude Code,并让 Agent 通过对话和工具调用操作当前画布。
|
## Goal
|
||||||
|
|
||||||
## 目标
|
Allow locally authenticated coding agents to read and modify the active browser canvas without moving canvas data, credentials, or local permissions to a hosted backend.
|
||||||
|
|
||||||
- 用户打开线上画布后,可以启动本机服务连接当前画布。
|
## Architecture
|
||||||
- 用户可以在 Codex 终端里通过 MCP 操作画布。
|
|
||||||
- 用户也可以在网页侧边栏里和本地 Codex / Claude Code 对话。
|
|
||||||
- 侧边栏需要展示普通消息、流式输出、工具调用、工具结果和错误提示。
|
|
||||||
- 线上服务不保存用户本地 Codex / Claude Code 登录态、API Key 或本地文件权限。
|
|
||||||
|
|
||||||
## 核心结论
|
Canvas Agent is the single local bridge. The browser synchronizes structured canvas state to it over authenticated HTTP and SSE. Codex and future Claude adapters use MCP tools exposed by Canvas Agent. Browser-side writes remain structured, reviewable, and undoable.
|
||||||
|
|
||||||
浏览器页面不能直接启动本地进程,也不应该直接控制本机 Codex / Claude Code。推荐增加一个用户本机运行的 `canvas-agent` 服务:
|
The service is divided into agent adapters, canvas state and tool handling, HTTP/SSE transport, and MCP exposure. Codex is the first adapter through `codex app-server`; Claude should use the Claude Agent SDK rather than duplicating canvas logic.
|
||||||
|
|
||||||
```txt
|
## Tool design
|
||||||
线上画布网页 <-> 本机 canvas-agent <-> Codex / Claude Code
|
|
||||||
|
|
|
||||||
+-> MCP Server
|
|
||||||
+-> 画布连接
|
|
||||||
```
|
|
||||||
|
|
||||||
`canvas-agent` 是本地可信边界,负责启动或连接 Codex / Claude Code、暴露 MCP 工具、维护画布连接、转发侧边栏对话事件。
|
Tools should cover state and selection reads, node creation and updates, connections, batch operations, viewport and selection changes, and text, image, or video generation. High-level tools convert to a small set of canvas operations before the browser applies them.
|
||||||
|
|
||||||
## 推荐目录
|
## Side panel
|
||||||
|
|
||||||
后续实现时建议在项目根目录新增独立 npm 包:
|
The browser panel manages connection state, conversations, streaming events, model and reasoning settings, permissions, approvals, local Skills, diagnostics, and task cancellation. A turn is bound to its initiating `clientId` so focusing another browser tab cannot redirect its canvas operations.
|
||||||
|
|
||||||
```txt
|
## Security
|
||||||
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
|
|
||||||
```
|
|
||||||
|
|
||||||
原因:
|
- Bind to localhost and require a generated connection token.
|
||||||
|
- Pin the accepted browser origin.
|
||||||
|
- Keep local agent login and file permissions out of the hosted site.
|
||||||
|
- Make writes confirmable and undoable in the browser.
|
||||||
|
- Never redirect an active turn to another tab when its initiating page disconnects.
|
||||||
|
|
||||||
- 这是用户本机运行的 Node 服务,不属于线上服务端,也不属于纯前端页面。
|
## Delivery stages
|
||||||
- 后续可以发布成 npm 包,用户通过 `npx` 或全局安装启动。
|
|
||||||
- Codex SDK、Codex MCP、Claude SDK 都更适合在本地 Node 进程中接入。
|
|
||||||
- HTTP 路由使用 Express,MCP 协议层使用官方 `@modelcontextprotocol/sdk`,工具入参使用 `zod`,避免手写协议和松散 JSON。
|
|
||||||
|
|
||||||
## 本机启动方式
|
1. Expose canvas state and operations to local Codex through MCP.
|
||||||
|
2. Add the browser Codex panel with streaming conversations and history.
|
||||||
|
3. Add a Claude Agent SDK adapter using the same canvas tools.
|
||||||
|
4. Improve approvals, diagnostics, multi-tab behavior, Skills, and recovery.
|
||||||
|
|
||||||
MVP 阶段优先提供 `npx`:
|
The MVP is complete when an agent can connect to the active canvas, read state and selection, apply visible node operations, start a generation workflow, return structured errors, and remain isolated across tabs.
|
||||||
|
|
||||||
```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 Agent,Canvas 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 后,网页能显示清晰的本机连接失败提示。
|
|
||||||
|
|||||||
@@ -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 路由使用 Express,MCP 协议层使用官方 `@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 Agent,Canvas 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 后,网页能显示清晰的本机连接失败提示。
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"title": "项目进度",
|
"title": "Project Progress",
|
||||||
"root": true,
|
"root": true,
|
||||||
"defaultOpen": true,
|
"defaultOpen": true,
|
||||||
"pages": [
|
"pages": [
|
||||||
"[更新日志](/docs/progress/changelog)",
|
"[Changelog](/docs/progress/changelog)",
|
||||||
"local-agent-integration-plan",
|
"local-agent-integration-plan",
|
||||||
"prompt-chip-input-plan",
|
"prompt-chip-input-plan",
|
||||||
"pending-test",
|
"pending-test",
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"title": "项目进度",
|
||||||
|
"root": true,
|
||||||
|
"defaultOpen": true,
|
||||||
|
"pages": [
|
||||||
|
"[更新日志](/zh-CN/docs/progress/changelog)",
|
||||||
|
"local-agent-integration-plan",
|
||||||
|
"prompt-chip-input-plan",
|
||||||
|
"pending-test",
|
||||||
|
"todo"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,65 +1,17 @@
|
|||||||
---
|
---
|
||||||
title: 待测试
|
title: Pending Tests
|
||||||
description: 当前版本已实现但仍需人工验证的变更项
|
description: Implemented changes that still need manual verification
|
||||||
---
|
---
|
||||||
|
|
||||||
# 待测试
|
# Pending Tests
|
||||||
|
|
||||||
- 国际化基础框架:进入「配置与用户偏好 → 偏好设置」切换简体中文或 English 后,首页、顶部及移动端导航、版本弹窗、配置主面板、渠道编辑、模型选择、页面标题、页面描述和 Ant Design 内置文案应立即切换,刷新页面后保持所选语言;WebDAV 同步错误、提示词来源及其他尚未接入翻译的业务模块暂时仍可能显示中文。
|
The current release needs manual verification in these areas:
|
||||||
- 画布节点缩放稳定性:选中任意节点并反复拖动四角缩放时,节点工具条应在缩放期间隐藏、松开后恢复,页面不应再出现 `Maximum update depth exceeded`。
|
|
||||||
- Agent MCP 初始化状态:首次进入空白对话、点击「新对话」或恢复历史对话后,无需先发送消息,对话区应立即显示 Codex 会话准备,并列出 `codex_apps`、`infinite-canvas`、`node_repl` 等完整 MCP 服务状态;全部服务进入终态前可输入并保留草稿,但发送按钮、上传和回车提交应禁用;可选 MCP 失败时应提示警告但允许发送,`infinite-canvas` 缺失或失败时应阻止发送并显示明确原因;首轮及后续任务运行期间再次收到的 MCP 状态只进入诊断日志,模型开始处理及回复完成后对话区不应残留初始化卡片。
|
- English and Simplified Chinese switching across navigation, settings, titles, descriptions, Ant Design components, and persisted preferences.
|
||||||
- Agent 模型设置:连接 Canvas Agent 后,输入框左下方应显示当前 Codex 模型与推理强度;模型列表应来自当前账号实际可用模型且不显示内部审查模型或重复项,切换模型后强度选项随模型能力更新且无空白选项,刷新页面后保留选择,发送任务时实际使用所选模型和强度;本地控制台与右侧「日志」应记录本轮使用的模型和推理强度。
|
- Canvas node resize stability, prompt scrolling, generated-prompt restoration, image editing, drag-and-drop references, and generation configuration.
|
||||||
- Agent 新对话响应:一轮对话完成后点击「新对话」,聊天内容应在服务端确认切换后清空并进入空白对话;后台应串行创建线程并预热 MCP,多个标签页应按同一会话版本同步,旧 HTTP 响应不得覆盖较新的 SSE 状态,初始化完成前不能发送到上一条会话或重复创建线程;重启 Agent 后应重新恢复并预热已有活动线程,无法恢复的失效空线程应自动替换为可用的新线程。
|
- Canvas Agent startup, MCP status, model and permission controls, approvals, version reporting, diagnostics, streaming responses, history consistency, multi-tab isolation, and local Skill management.
|
||||||
- Agent 读取画布卡片:读取当前画布完成后,卡片应按非零类型显示文本、图片、配置、视频、音频、分组、其他节点及连线数量,例如「3 个文本、5 张图片、2 个配置、4 条连线」;空画布应显示「当前画布为空」,执行失败时仍应显示错误信息,刷新恢复历史后统计保持一致。
|
- Agent message layout, Markdown, code blocks, attachments, token statistics, progress timelines, command groups, and scroll-follow behavior.
|
||||||
- Agent 首次发送响应:在空白新对话中输入内容并按回车后,输入框应立即清空、用户消息应立即出现在对话中,再显示「正在思考」;线程创建或发送失败时,原输入和附件应恢复;任务运行期间输入的新草稿不应在请求成功后被清空。
|
- Prompt Center layout, source synchronization, search, asset insertion, and prompt detail dialogs.
|
||||||
- Agent 动态工具信息:执行内置生图、查看图片、命令、文件修改或其他动态工具时,卡片标题应显示具体工具名称;执行失败时正文应显示真实错误原因,刷新并恢复历史对话后仍应保持一致,不再统一显示「工具操作已完成」。
|
- Settings import/export, Volcengine Ark protocol handling, and WebDAV-related configuration.
|
||||||
- Agent 画布生图:重启 Canvas Agent 后,其自动生成的工作区 `AGENTS.md` 应同步为最新版指令(用户自行编写的其他 `AGENTS.md` 不应被覆盖);输入“生成一张图片”时应调用 `canvas_generate_image`,通过当前画布节点创建并运行生成流程;只有明确输入“使用 Codex 内置生图/ImageGen 技能生成”时才允许调用内置能力,Codex App Server 返回的 `imageGeneration` 结果应显示在对话中并作为保持原比例的图片节点添加到当前画布;Agent 不应在没有生成结果时提前声称“已生成”。
|
|
||||||
- Agent 顶部栏:在对话、历史和日志之间新增「技能」标签并显示已加载数量;连接状态应位于左侧 Agent 标题旁,并与右侧新对话使用相同规格的文字按钮,只有内容标签显示选中线;标签图标始终显示,窄面板仅隐藏文字并保留小号数量,宽面板显示图标和文字,拖动宽度时不应换行、横向滚动或错位。
|
The Chinese version contains the complete item-by-item acceptance checklist.
|
||||||
- Agent 输入框窄屏布局:底部上传、Skill、确认模式、权限模式、模型和思考程度控件在窄面板下应只显示图标,宽面板下恢复文字;所有纯图标操作悬停时应使用统一样式显示名称或当前状态,任意宽度都不出现横向溢出。
|
|
||||||
- Agent 本地 Skill 管理:连接 Canvas Agent 后进入「技能」,应能读取当前 Codex 工作空间发现的项目、个人、系统和管理员 Skill,并支持搜索、按范围筛选、刷新及启停;未能加载的 Skill 应通过可点击、可滚动的详情查看原因;画布在站点工作空间 `.agents/skills` 下创建的 Skill 应可新建、编辑和删除,其他来源只读且不得被画布改写;编辑期间文件被其他窗口修改时应拒绝覆盖,清空显示名称、短说明和默认提示词后旧值应被删除,同时保留图标、品牌色等未由当前界面管理的元数据;多个标签页同时连接时,任一页面的 Skill 变更应自动同步,断开或切换 Agent 时旧请求不得重新写回列表或弹出编辑器。
|
|
||||||
- Agent Skill 草稿生成:创建 Skill 时应提供「空白创建」「从当前对话生成草稿」「从当前画布生成草稿」三种入口;空对话不得发起生成,取消时不得写入文件,确认后才写入站点工作空间 `.agents/skills`;Agent 运行中应禁用生成,多标签页下对话和画布来源必须分别严格绑定发起请求的 `threadId` 和 `clientId`,生成期间外部 MCP 仍应操作最近聚焦页面;草稿运行时创建、启停、删除和更新 Skill 应被统一拒绝,点击停止应真实中断临时草稿 turn;草稿不得泄露本地路径、密钥、媒体数据、真实节点 ID 或任务进度等临时状态,普通中文斜杠文本与 `/imagine` 等命令必须保持完整,生成草稿使用的临时 turn 不得出现在 Agent 对话、历史或日志中。
|
|
||||||
- Agent Skill 调用:在技能页点击「使用」或在输入框左下方选择 Skill 后,输入框上方应显示可移除标签;发送任务时应通过 Codex 原生结构化 Skill 输入执行,而不是只把名称拼进普通提示词,发送成功后清除选择,失败时保留;新建对话、断开连接、Skill 被停用或删除时也应清除失效选择,发送前、任务完成后和刷新历史后的用户消息文字应保持一致。
|
|
||||||
- Agent Markdown 样式:右侧 Agent 回复中的代码块应为横向占满消息区域、无语言标题和无双层边框的紧凑代码条,单行内容不应保留纵向大面积空白,复制操作仅在悬停时弱化显示;行内代码、链接在浅色和深色主题下应清晰;点击外部链接后应显示中文紧凑确认弹窗,长路径能够正常换行且复制、继续打开和关闭操作可用;点击 `/Users/`、`/home/` 等本地绝对文件路径时应改为提示在系统文件管理器中定位,支持复制路径,且浏览器地址不应跳转为 localhost 文件路径。
|
|
||||||
- Agent 工具确认模式:右侧面板标题栏不应再显示「工具确认」开关;对话输入框左下方应显示确认模式选择,默认选中「自动确认」,画布写入工具应直接执行;切换为「手动确认」后,画布写入工具应在对话中展示等待确认卡片,并支持批准或拒绝。
|
|
||||||
- Canvas Agent Codex 升级:启动 Canvas Agent 后应输出 Canvas Agent、内置 Codex 和本机 Codex 版本,实际运行的内置 Codex 应为 0.146.0;内置与本机版本不一致、未安装本机 Codex,或 npm 存在更新版本时应显示对应升级提醒,npm 检查失败不应阻止服务启动;Agent 应能正常连接 Codex、创建或恢复会话、发送消息并调用画布工具,运行中停止任务应只中断当前 turn,随后无需重启 Agent 即可继续发送新任务。
|
|
||||||
- Canvas Agent Debug:普通启动应通过公共 logger 输出 Info、Warn 和 Error 日志;使用 `npx -y @basketikun/canvas-agent --debug` 启动后还应输出 Debug 日志并显示日志文件路径,终端统一采用“`YYYY-MM-DD HH:mm:ss` 级别 消息 详情”的纯文本单行格式,`~/.infinite-canvas/logs/` 下应按启动日期生成相同格式的 `canvas-agent-YYYY-MM-DD.log`,同一天多次启动应追加到同一文件;日志中不应出现连接 token 或图片 Data URL 原文。
|
|
||||||
- Canvas Agent Codex 日志:Codex app-server 输出带颜色或样式控制符及 UTC 时间的 stderr 时,本地日志和网页诊断日志应只显示一份 `YYYY-MM-DD HH:mm:ss` 本地时间和干净文本,不应出现重复 ISO 时间或 `[2m`、`[31m` 等 ANSI 转义内容。
|
|
||||||
- Agent HTTP 诊断日志:网页发送一条普通消息后,本地 Debug 日志不应重复输出 `/health`、`/canvas/state`、`/canvas/activate` 成功请求、流式增量或完整会话响应,只保留 HTTP 请求与 Codex 生命周期摘要;右侧「日志」应以单行时间线展示发送、开始、回复、工具、完成用量和错误,不再输出 userMessage started/completed、流式摘要、重复 threadId 或大段原始 JSON。
|
|
||||||
- Agent 排查日志:右侧「日志」中的排查视图应显示连接摘要,并可按错误、警告和信息筛选;向下查看日志时仅列表内部滚动,标题、连接状态、筛选数量和操作按钮保持固定;连续相同事件应折叠计数,单条日志可展开查看完整详情;原始 JSON、复制全部、复制最近错误和清空功能应继续可用,浅色与深色主题下均应清晰。
|
|
||||||
- Agent 排查日志顺序与跟随:日志应按时间从旧到新排列,进入日志或切换筛选时定位到最新事件;停留底部时自动跟随新日志,展开最后一条后应继续停留在完整详情底部,向上浏览后暂停跟随并显示与对话区视觉高度一致的居中圆形向下箭头,新日志数量只在悬浮提示中显示;列表底部不应出现额外留白,点击按钮后回到底部并恢复跟随。
|
|
||||||
- Agent 对话统计:用户消息应右对齐并使用透明无气泡的简洁排版,用户和 Codex 两侧均不显示人物头像,消息下方均不显示时间或 Token 信息;输入框上方应居中展示最新一次模型调用的输入、缓存、输出 Token 用量,不显示会话累计值,数值更新时应从旧值平滑滚动到新值而非突然跳变,新建、切换或删除当前会话后应清空旧统计。
|
|
||||||
- Agent 回复实时显示:在右侧 Agent 发送消息后,用户消息下方应立即出现“正在思考...”,任务运行期间不应闪退;工具完成后应显示 Codex 正在继续处理及已等待时长,等待超过 30 秒时提示可继续等待或停止本轮;Codex 的回复应在当前对话中持续显示,实时事件缺失时也应在任务完成后自动同步完整内容,无需切换到历史或日志再返回对话。模型繁忙等任务失败时应立即结束等待状态,在对话中显示中文错误原因和重试建议,诊断日志不应再把失败轮次记为“处理完成”,刷新历史后错误仍应保留。新建尚未发送首条消息的空会话不应反复出现历史读取失败。
|
|
||||||
- Agent 对话实时与历史一致性:运行包含思考、命令及画布工具的较长任务,在执行中刷新发起页面或打开第二个页面时,已出现的用户消息、思考过程、工具卡和回复前缀均应保留并继续更新;同一思考或工具只显示一张卡片,任务结束及再次刷新后内容、顺序和状态应一致。切换页面焦点不能改变本轮画布工具目标,发起页面断开后不得把图片或写操作转交其他画布,使用相同页面身份重连后才能继续处理。
|
|
||||||
- Agent 流式交互性能:发送长回复时文字应连续平滑出现,输入框、滚动和画布操作不应随回复变长而明显卡顿;长历史会话中只有当前流式消息持续更新,屏幕外消息不应造成明显布局压力;任务完成后应通过 SSE 自动同步完整历史,不再持续请求 `/health` 轮询状态。
|
|
||||||
- Agent 过程时间线:新建 Agent 对话后 Codex 应生成可读思考摘要,并在运行时依次显示中文的思考摘要、执行计划、命令执行、网页搜索、文件修改和画布工具活动;思考摘要的图标、标题和箭头应稳定保持在同一行,默认收起且无边框,点击箭头后才展开 Codex 实际返回的具体摘要,其中 Markdown 强调、列表和代码应正确渲染而非显示原始符号,多行代码块和文本流程图应保留原始换行并允许横向滚动,完成事件不应再用「已完成分析」覆盖已经收到的摘要;同一轮连续命令应合并为显示数量的无边框折叠行,折叠时不显示命令预览,单条展开后直接显示详情,多条展开后可逐条查看工作目录、耗时、退出状态和运行输出;其他工具调用继续使用紧凑卡片排版,状态图标、标题和状态文字保持在同一行;结构化任务进度不应混在对话时间线中,而应独立放在对话区下方、Token 统计上方,支持展开和折叠,并逐项实时更新「待处理」「进行中」「已完成」状态;新任务生成时默认展开,任务结束后保留最新结果,同一计划更新不应生成重复内容;文件详情展示文件路径与新增/修改/删除动作,工具详情不应出现请求 ID、英文工具名或原始 JSON;一轮对话结束自动同步历史以及刷新页面后从历史中 resume 对话时,思考摘要、命令和工具过程记录仍应完整保留并保持相同展示。
|
|
||||||
- Agent 权限控制:输入框可选择「请求批准」「自动审查」「完全访问」;请求批准模式下,Codex 编辑工作区外文件、执行受限命令或访问网络时应在对话中显示审批卡片,支持拒绝、允许一次和本会话允许,并能依次处理多个并发请求;提交决定后卡片应保持禁用等待,只有 Codex 确认处理后才移除;等待审批时刷新页面,未处理的审批卡应自动恢复且不能让任务永久卡住;自动审查模式应仅将需要用户决定的风险操作送入审批;完全访问必须先显示风险确认,启用后可访问网络和本机文件且不再请求审批;选择应在刷新后保留。
|
|
||||||
- Agent 历史记录:点击记录卡片应直接进入对应对话,不再显示「进入」按钮;可勾选单条或全选多条记录并批量删除,删除当前对话后聊天内容应清空。
|
|
||||||
- Agent 默认新对话:每次进入任一画布并连接 Agent 后,对话区应保持空白且不自动恢复上一次会话,并在后台创建新线程、预热 MCP;需要继续旧对话时可在「历史」中主动选择恢复。
|
|
||||||
- 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 排版;长文本、多张附件、错误消息及浅色/深色主题下均应清晰且不溢出。
|
|
||||||
- Agent 命令记录:命令执行中应显示运行状态;完成后,同一轮相邻命令应合并为「已执行 N 条命令」折叠行且默认不显示原始命令,单条展开后直接显示完整详情,多条展开后可分别查看命令、输出和退出状态;对话结束或刷新后分组与记录不应消失。
|
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
---
|
||||||
|
title: 待测试
|
||||||
|
description: 当前版本已实现但仍需人工验证的变更项
|
||||||
|
---
|
||||||
|
|
||||||
|
# 待测试
|
||||||
|
|
||||||
|
- 文档站国际化:访问文档站首页及 `/docs` 应默认显示英文,切换简体中文后 URL 应带 `/zh-CN` 且首页、顶部导航、侧边栏、正文、搜索框和页面操作文案均显示中文;刷新、站内跳转和再次切回 English 后语言应保持正确,搜索结果与 Markdown/LLM 文档内容不得混入另一种语言。
|
||||||
|
- 国际化基础框架:进入「配置与用户偏好 → 偏好设置」切换简体中文或 English 后,首页、顶部及移动端导航、版本弹窗、配置主面板、渠道编辑、模型选择、页面标题、页面描述和 Ant Design 内置文案应立即切换,刷新页面后保持所选语言;WebDAV 同步错误、提示词来源及其他尚未接入翻译的业务模块暂时仍可能显示中文。
|
||||||
|
- 画布节点缩放稳定性:选中任意节点并反复拖动四角缩放时,节点工具条应在缩放期间隐藏、松开后恢复,页面不应再出现 `Maximum update depth exceeded`。
|
||||||
|
- Agent MCP 初始化状态:首次进入空白对话、点击「新对话」或恢复历史对话后,无需先发送消息,对话区应立即显示 Codex 会话准备,并列出 `codex_apps`、`infinite-canvas`、`node_repl` 等完整 MCP 服务状态;全部服务进入终态前可输入并保留草稿,但发送按钮、上传和回车提交应禁用;可选 MCP 失败时应提示警告但允许发送,`infinite-canvas` 缺失或失败时应阻止发送并显示明确原因;首轮及后续任务运行期间再次收到的 MCP 状态只进入诊断日志,模型开始处理及回复完成后对话区不应残留初始化卡片。
|
||||||
|
- Agent 模型设置:连接 Canvas Agent 后,输入框左下方应显示当前 Codex 模型与推理强度;模型列表应来自当前账号实际可用模型且不显示内部审查模型或重复项,切换模型后强度选项随模型能力更新且无空白选项,刷新页面后保留选择,发送任务时实际使用所选模型和强度;本地控制台与右侧「日志」应记录本轮使用的模型和推理强度。
|
||||||
|
- Agent 新对话响应:一轮对话完成后点击「新对话」,聊天内容应在服务端确认切换后清空并进入空白对话;后台应串行创建线程并预热 MCP,多个标签页应按同一会话版本同步,旧 HTTP 响应不得覆盖较新的 SSE 状态,初始化完成前不能发送到上一条会话或重复创建线程;重启 Agent 后应重新恢复并预热已有活动线程,无法恢复的失效空线程应自动替换为可用的新线程。
|
||||||
|
- Agent 读取画布卡片:读取当前画布完成后,卡片应按非零类型显示文本、图片、配置、视频、音频、分组、其他节点及连线数量,例如「3 个文本、5 张图片、2 个配置、4 条连线」;空画布应显示「当前画布为空」,执行失败时仍应显示错误信息,刷新恢复历史后统计保持一致。
|
||||||
|
- Agent 首次发送响应:在空白新对话中输入内容并按回车后,输入框应立即清空、用户消息应立即出现在对话中,再显示「正在思考」;线程创建或发送失败时,原输入和附件应恢复;任务运行期间输入的新草稿不应在请求成功后被清空。
|
||||||
|
- Agent 动态工具信息:执行内置生图、查看图片、命令、文件修改或其他动态工具时,卡片标题应显示具体工具名称;执行失败时正文应显示真实错误原因,刷新并恢复历史对话后仍应保持一致,不再统一显示「工具操作已完成」。
|
||||||
|
- Agent 画布生图:重启 Canvas Agent 后,其自动生成的工作区 `AGENTS.md` 应同步为最新版指令(用户自行编写的其他 `AGENTS.md` 不应被覆盖);输入“生成一张图片”时应调用 `canvas_generate_image`,通过当前画布节点创建并运行生成流程;只有明确输入“使用 Codex 内置生图/ImageGen 技能生成”时才允许调用内置能力,Codex App Server 返回的 `imageGeneration` 结果应显示在对话中并作为保持原比例的图片节点添加到当前画布;Agent 不应在没有生成结果时提前声称“已生成”。
|
||||||
|
- Agent 顶部栏:在对话、历史和日志之间新增「技能」标签并显示已加载数量;连接状态应位于左侧 Agent 标题旁,并与右侧新对话使用相同规格的文字按钮,只有内容标签显示选中线;标签图标始终显示,窄面板仅隐藏文字并保留小号数量,宽面板显示图标和文字,拖动宽度时不应换行、横向滚动或错位。
|
||||||
|
- Agent 输入框窄屏布局:底部上传、Skill、确认模式、权限模式、模型和思考程度控件在窄面板下应只显示图标,宽面板下恢复文字;所有纯图标操作悬停时应使用统一样式显示名称或当前状态,任意宽度都不出现横向溢出。
|
||||||
|
- Agent 本地 Skill 管理:连接 Canvas Agent 后进入「技能」,应能读取当前 Codex 工作空间发现的项目、个人、系统和管理员 Skill,并支持搜索、按范围筛选、刷新及启停;未能加载的 Skill 应通过可点击、可滚动的详情查看原因;画布在站点工作空间 `.agents/skills` 下创建的 Skill 应可新建、编辑和删除,其他来源只读且不得被画布改写;编辑期间文件被其他窗口修改时应拒绝覆盖,清空显示名称、短说明和默认提示词后旧值应被删除,同时保留图标、品牌色等未由当前界面管理的元数据;多个标签页同时连接时,任一页面的 Skill 变更应自动同步,断开或切换 Agent 时旧请求不得重新写回列表或弹出编辑器。
|
||||||
|
- Agent Skill 草稿生成:创建 Skill 时应提供「空白创建」「从当前对话生成草稿」「从当前画布生成草稿」三种入口;空对话不得发起生成,取消时不得写入文件,确认后才写入站点工作空间 `.agents/skills`;Agent 运行中应禁用生成,多标签页下对话和画布来源必须分别严格绑定发起请求的 `threadId` 和 `clientId`,生成期间外部 MCP 仍应操作最近聚焦页面;草稿运行时创建、启停、删除和更新 Skill 应被统一拒绝,点击停止应真实中断临时草稿 turn;草稿不得泄露本地路径、密钥、媒体数据、真实节点 ID 或任务进度等临时状态,普通中文斜杠文本与 `/imagine` 等命令必须保持完整,生成草稿使用的临时 turn 不得出现在 Agent 对话、历史或日志中。
|
||||||
|
- Agent Skill 调用:在技能页点击「使用」或在输入框左下方选择 Skill 后,输入框上方应显示可移除标签;发送任务时应通过 Codex 原生结构化 Skill 输入执行,而不是只把名称拼进普通提示词,发送成功后清除选择,失败时保留;新建对话、断开连接、Skill 被停用或删除时也应清除失效选择,发送前、任务完成后和刷新历史后的用户消息文字应保持一致。
|
||||||
|
- Agent Markdown 样式:右侧 Agent 回复中的代码块应为横向占满消息区域、无语言标题和无双层边框的紧凑代码条,单行内容不应保留纵向大面积空白,复制操作仅在悬停时弱化显示;行内代码、链接在浅色和深色主题下应清晰;点击外部链接后应显示中文紧凑确认弹窗,长路径能够正常换行且复制、继续打开和关闭操作可用;点击 `/Users/`、`/home/` 等本地绝对文件路径时应改为提示在系统文件管理器中定位,支持复制路径,且浏览器地址不应跳转为 localhost 文件路径。
|
||||||
|
- Agent 工具确认模式:右侧面板标题栏不应再显示「工具确认」开关;对话输入框左下方应显示确认模式选择,默认选中「自动确认」,画布写入工具应直接执行;切换为「手动确认」后,画布写入工具应在对话中展示等待确认卡片,并支持批准或拒绝。
|
||||||
|
- Canvas Agent Codex 升级:启动 Canvas Agent 后应输出 Canvas Agent、内置 Codex 和本机 Codex 版本,实际运行的内置 Codex 应为 0.146.0;内置与本机版本不一致、未安装本机 Codex,或 npm 存在更新版本时应显示对应升级提醒,npm 检查失败不应阻止服务启动;Agent 应能正常连接 Codex、创建或恢复会话、发送消息并调用画布工具,运行中停止任务应只中断当前 turn,随后无需重启 Agent 即可继续发送新任务。
|
||||||
|
- Canvas Agent Debug:普通启动应通过公共 logger 输出 Info、Warn 和 Error 日志;使用 `npx -y @basketikun/canvas-agent --debug` 启动后还应输出 Debug 日志并显示日志文件路径,终端统一采用“`YYYY-MM-DD HH:mm:ss` 级别 消息 详情”的纯文本单行格式,`~/.infinite-canvas/logs/` 下应按启动日期生成相同格式的 `canvas-agent-YYYY-MM-DD.log`,同一天多次启动应追加到同一文件;日志中不应出现连接 token 或图片 Data URL 原文。
|
||||||
|
- Canvas Agent Codex 日志:Codex app-server 输出带颜色或样式控制符及 UTC 时间的 stderr 时,本地日志和网页诊断日志应只显示一份 `YYYY-MM-DD HH:mm:ss` 本地时间和干净文本,不应出现重复 ISO 时间或 `[2m`、`[31m` 等 ANSI 转义内容。
|
||||||
|
- Agent HTTP 诊断日志:网页发送一条普通消息后,本地 Debug 日志不应重复输出 `/health`、`/canvas/state`、`/canvas/activate` 成功请求、流式增量或完整会话响应,只保留 HTTP 请求与 Codex 生命周期摘要;右侧「日志」应以单行时间线展示发送、开始、回复、工具、完成用量和错误,不再输出 userMessage started/completed、流式摘要、重复 threadId 或大段原始 JSON。
|
||||||
|
- Agent 排查日志:右侧「日志」中的排查视图应显示连接摘要,并可按错误、警告和信息筛选;向下查看日志时仅列表内部滚动,标题、连接状态、筛选数量和操作按钮保持固定;连续相同事件应折叠计数,单条日志可展开查看完整详情;原始 JSON、复制全部、复制最近错误和清空功能应继续可用,浅色与深色主题下均应清晰。
|
||||||
|
- Agent 排查日志顺序与跟随:日志应按时间从旧到新排列,进入日志或切换筛选时定位到最新事件;停留底部时自动跟随新日志,展开最后一条后应继续停留在完整详情底部,向上浏览后暂停跟随并显示与对话区视觉高度一致的居中圆形向下箭头,新日志数量只在悬浮提示中显示;列表底部不应出现额外留白,点击按钮后回到底部并恢复跟随。
|
||||||
|
- Agent 对话统计:用户消息应右对齐并使用透明无气泡的简洁排版,用户和 Codex 两侧均不显示人物头像,消息下方均不显示时间或 Token 信息;输入框上方应居中展示最新一次模型调用的输入、缓存、输出 Token 用量,不显示会话累计值,数值更新时应从旧值平滑滚动到新值而非突然跳变,新建、切换或删除当前会话后应清空旧统计。
|
||||||
|
- Agent 回复实时显示:在右侧 Agent 发送消息后,用户消息下方应立即出现“正在思考...”,任务运行期间不应闪退;工具完成后应显示 Codex 正在继续处理及已等待时长,等待超过 30 秒时提示可继续等待或停止本轮;Codex 的回复应在当前对话中持续显示,实时事件缺失时也应在任务完成后自动同步完整内容,无需切换到历史或日志再返回对话。模型繁忙等任务失败时应立即结束等待状态,在对话中显示中文错误原因和重试建议,诊断日志不应再把失败轮次记为“处理完成”,刷新历史后错误仍应保留。新建尚未发送首条消息的空会话不应反复出现历史读取失败。
|
||||||
|
- Agent 对话实时与历史一致性:运行包含思考、命令及画布工具的较长任务,在执行中刷新发起页面或打开第二个页面时,已出现的用户消息、思考过程、工具卡和回复前缀均应保留并继续更新;同一思考或工具只显示一张卡片,任务结束及再次刷新后内容、顺序和状态应一致。切换页面焦点不能改变本轮画布工具目标,发起页面断开后不得把图片或写操作转交其他画布,使用相同页面身份重连后才能继续处理。
|
||||||
|
- Agent 流式交互性能:发送长回复时文字应连续平滑出现,输入框、滚动和画布操作不应随回复变长而明显卡顿;长历史会话中只有当前流式消息持续更新,屏幕外消息不应造成明显布局压力;任务完成后应通过 SSE 自动同步完整历史,不再持续请求 `/health` 轮询状态。
|
||||||
|
- Agent 过程时间线:新建 Agent 对话后 Codex 应生成可读思考摘要,并在运行时依次显示中文的思考摘要、执行计划、命令执行、网页搜索、文件修改和画布工具活动;思考摘要的图标、标题和箭头应稳定保持在同一行,默认收起且无边框,点击箭头后才展开 Codex 实际返回的具体摘要,其中 Markdown 强调、列表和代码应正确渲染而非显示原始符号,多行代码块和文本流程图应保留原始换行并允许横向滚动,完成事件不应再用「已完成分析」覆盖已经收到的摘要;同一轮连续命令应合并为显示数量的无边框折叠行,折叠时不显示命令预览,单条展开后直接显示详情,多条展开后可逐条查看工作目录、耗时、退出状态和运行输出;其他工具调用继续使用紧凑卡片排版,状态图标、标题和状态文字保持在同一行;结构化任务进度不应混在对话时间线中,而应独立放在对话区下方、Token 统计上方,支持展开和折叠,并逐项实时更新「待处理」「进行中」「已完成」状态;新任务生成时默认展开,任务结束后保留最新结果,同一计划更新不应生成重复内容;文件详情展示文件路径与新增/修改/删除动作,工具详情不应出现请求 ID、英文工具名或原始 JSON;一轮对话结束自动同步历史以及刷新页面后从历史中 resume 对话时,思考摘要、命令和工具过程记录仍应完整保留并保持相同展示。
|
||||||
|
- Agent 权限控制:输入框可选择「请求批准」「自动审查」「完全访问」;请求批准模式下,Codex 编辑工作区外文件、执行受限命令或访问网络时应在对话中显示审批卡片,支持拒绝、允许一次和本会话允许,并能依次处理多个并发请求;提交决定后卡片应保持禁用等待,只有 Codex 确认处理后才移除;等待审批时刷新页面,未处理的审批卡应自动恢复且不能让任务永久卡住;自动审查模式应仅将需要用户决定的风险操作送入审批;完全访问必须先显示风险确认,启用后可访问网络和本机文件且不再请求审批;选择应在刷新后保留。
|
||||||
|
- Agent 历史记录:点击记录卡片应直接进入对应对话,不再显示「进入」按钮;可勾选单条或全选多条记录并批量删除,删除当前对话后聊天内容应清空。
|
||||||
|
- Agent 默认新对话:每次进入任一画布并连接 Agent 后,对话区应保持空白且不自动恢复上一次会话,并在后台创建新线程、预热 MCP;需要继续旧对话时可在「历史」中主动选择恢复。
|
||||||
|
- 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 排版;长文本、多张附件、错误消息及浅色/深色主题下均应清晰且不溢出。
|
||||||
|
- Agent 命令记录:命令执行中应显示运行状态;完成后,同一轮相邻命令应合并为「已执行 N 条命令」折叠行且默认不显示原始命令,单条展开后直接显示完整详情,多条展开后可分别查看命令、输出和退出状态;对话结束或刷新后分组与记录不应消失。
|
||||||
@@ -1,66 +1,24 @@
|
|||||||
---
|
---
|
||||||
title: 提示词面板引用显示图片缩略图规划
|
title: Prompt Reference Thumbnail Plan
|
||||||
description: 将提示词面板输入框从 textarea 改造为 contentEditable,让 @ 引用直接显示真实图片缩略图
|
description: Display real image thumbnails for @ references in the canvas prompt input
|
||||||
---
|
---
|
||||||
|
|
||||||
# 提示词面板 @ 引用显示真实图片缩略图
|
# Prompt Reference Thumbnail Plan
|
||||||
|
|
||||||
## 背景 / 要解决的问题
|
## Problem
|
||||||
|
|
||||||
提示词面板(`CanvasNodePromptPanel`)的输入框里,`@` 引用一张图片后,显示的是蓝色文字 chip「图片1」。期望**直接显示这张图片的真实缩略图**,而不是文字编号。
|
A plain textarea can display only text labels for `@` references. The canvas prompt panel needs real image thumbnails while preserving ordinary text editing, IME input, keyboard navigation, deletion, and the existing send contract.
|
||||||
|
|
||||||
当前该输入框是原生 `<textarea>`,只能承载纯文本,无法内嵌图片。要显示缩略图,必须改用 `contentEditable`(可编辑 div),把引用做成带 `<img>` 的原子小卡片。项目里「组装提示词」组件(`CanvasConfigComposer`)已经是这套 contentEditable + 图片 chip 的成熟实现,可作蓝本。
|
## Solution
|
||||||
|
|
||||||
## 关键约束(为什么不能直接改现有组件)
|
Introduce `CanvasPromptChipInput`, a focused `contentEditable` input that renders text and atomic reference chips. Keep the external value and callbacks compatible with the existing prompt panel so model invocation and connected-resource numbering do not change.
|
||||||
|
|
||||||
`CanvasResourceMentionTextarea` 被两处复用:
|
Implementation requirements:
|
||||||
|
|
||||||
1. **提示词面板** `web/src/components/canvas/canvas-node-prompt-panel.tsx` —— 本次要改的对象。`highlightLabels` 默认 true,value 是提示词字符串,引用以 label 文本「图片1」嵌在其中,**直接发给 AI**(配合 `web/src/lib/image-reference-prompt.ts` 的「参考图片编号:图片1」前缀)。
|
- Reuse the existing composer approach for selection restoration, composition events, paste, and keyboard handling.
|
||||||
2. **节点内文本编辑** `web/src/components/canvas/canvas-node.tsx`(`isEditingContent` 分支)—— `highlightLabels={false}`,通过 `ref` 调用 `textarea.setSelectionRange(...)` 等 **textarea 专有 API**。若把组件改成 contentEditable,这处会直接崩。
|
- Treat each reference chip as a non-editable unit and remove it with Backspace or Delete.
|
||||||
|
- Render image references with object URLs and accessible labels; keep text, video, and audio references compact.
|
||||||
|
- Serialize the editable DOM back to the current plain-text reference format before sending.
|
||||||
|
- Replace only the prompt panel input and keep the existing model selectors, prompt library, resource resolution, and generation requests unchanged.
|
||||||
|
|
||||||
> 结论:**新建一个 contentEditable chip 输入组件,只替换提示词面板那一处**;`CanvasResourceMentionTextarea`(textarea 版)保持不动,继续服务节点内文本编辑。
|
Manual verification should cover Chinese IME, ordinary slash text, multiline paste, adding and deleting multiple references, thumbnail rendering, selection movement, sending, and restoring the draft.
|
||||||
|
|
||||||
## 方案:新增 `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 ← DOM(syncFromEditor)**:仿 `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. **占位符 & 样式**:空值时显示 placeholder(absolute 占位层);容器套用面板传入的 `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」文本(生成结果正常引用到图)。
|
|
||||||
- 中文输入法输入不吞字、不误触发发送。
|
|
||||||
- 回归:双击文本节点进入内文本编辑仍正常(未受影响)。
|
|
||||||
|
|||||||
@@ -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` 默认 true,value 是提示词字符串,引用以 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 ← DOM(syncFromEditor)**:仿 `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. **占位符 & 样式**:空值时显示 placeholder(absolute 占位层);容器套用面板传入的 `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」文本(生成结果正常引用到图)。
|
||||||
|
- 中文输入法输入不吞字、不误触发发送。
|
||||||
|
- 回归:双击文本节点进入内文本编辑仍正常(未受影响)。
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
---
|
---
|
||||||
title: TODO
|
title: TODO
|
||||||
description: 当前项目后续值得处理的事项
|
description: Planned follow-up work
|
||||||
---
|
---
|
||||||
|
|
||||||
# TODO
|
# TODO
|
||||||
|
|
||||||
本文档用来记录当前项目后续比较值得处理的事项。
|
- Replace the Claude Code CLI adapter with a Claude Agent SDK adapter and improve the tool queue, following the [Local Agent Integration Plan](/docs/progress/local-agent-integration-plan).
|
||||||
|
- Add Skill installation from web search, Skill resource-file management, and controlled local-memory persistence.
|
||||||
- 本地 Agent 接入后续:按[本地 Agent 接入规划](/docs/progress/local-agent-integration-plan)把 Claude Code CLI Adapter 升级为 Claude Agent SDK Adapter,并完善工具队列体验。
|
|
||||||
- Agent Skill 后续:补充网络检索安装、Skill 资源文件管理和可控的本地记忆沉淀。
|
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
title: TODO
|
||||||
|
description: 当前项目后续值得处理的事项
|
||||||
|
---
|
||||||
|
|
||||||
|
# TODO
|
||||||
|
|
||||||
|
本文档用来记录当前项目后续比较值得处理的事项。
|
||||||
|
|
||||||
|
- 本地 Agent 接入后续:按[本地 Agent 接入规划](/zh-CN/docs/progress/local-agent-integration-plan)把 Claude Code CLI Adapter 升级为 Claude Agent SDK Adapter,并完善工具队列体验。
|
||||||
|
- Agent Skill 后续:补充网络检索安装、Skill 资源文件管理和可控的本地记忆沉淀。
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
{
|
{
|
||||||
"title": "支持与安全",
|
"title": "Support and Security",
|
||||||
"root": true,
|
"root": true,
|
||||||
"defaultOpen": true,
|
"defaultOpen": true,
|
||||||
"pages": [
|
"pages": ["security", "sponsor"]
|
||||||
"security",
|
|
||||||
"sponsor"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"title": "支持与安全",
|
||||||
|
"root": true,
|
||||||
|
"defaultOpen": true,
|
||||||
|
"pages": [
|
||||||
|
"security",
|
||||||
|
"sponsor"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,34 +1,14 @@
|
|||||||
---
|
---
|
||||||
title: 漏洞提交
|
title: Report a Vulnerability
|
||||||
description: 安全漏洞提交和负责任披露说明
|
description: Security reporting and responsible disclosure
|
||||||
---
|
---
|
||||||
|
|
||||||
# 漏洞提交
|
# Report a Vulnerability
|
||||||
|
|
||||||
安全策略英文原文见 [SECURITY.md](https://github.com/basketikun/infinite-canvas/blob/main/SECURITY.md)。
|
See [SECURITY.md](https://github.com/basketikun/infinite-canvas/blob/main/SECURITY.md). Do not post exploit details, private API keys, sensitive screenshots, or real user data in a public issue.
|
||||||
|
|
||||||
请不要在公开 Issue 中直接发布漏洞细节、可利用代码、私密 API Key、截图中的敏感信息或真实用户数据。
|
Use GitHub private vulnerability reporting or a Security Advisory. If unavailable, email [1844025705@qq.com](mailto:1844025705@qq.com) with `[infinite-canvas security]` in the subject.
|
||||||
|
|
||||||
## 提交流程
|
Include the affected version or commit, reproduction steps, impact and prerequisites, redacted logs or proof of concept, and whether browser storage, API keys, WebDAV, AI provider configuration, or network requests are involved.
|
||||||
|
|
||||||
优先使用 GitHub 的私密漏洞报告或 Security Advisory。如果仓库没有开启对应入口,请发送邮件至 [1844025705@qq.com](mailto:1844025705@qq.com),邮件标题建议使用:
|
The project prioritizes vulnerabilities caused by repository code or defaults, including XSS, sensitive-data exposure, file processing, import/export, WebDAV, access control, and supply-chain risks. Problems confined to third-party services, leaked user credentials, recommendations without an exploit path, phishing, and account recovery are normally outside scope.
|
||||||
|
|
||||||
```text
|
|
||||||
[infinite-canvas security]
|
|
||||||
```
|
|
||||||
|
|
||||||
如果暂时无法使用私密渠道,可以先开一个公开 Issue 请求维护者提供私密联系方式,但不要在 Issue 中写入漏洞细节。
|
|
||||||
|
|
||||||
## 建议提供的信息
|
|
||||||
|
|
||||||
- 受影响的版本、提交、分支或部署方式。
|
|
||||||
- 清晰的复现步骤。
|
|
||||||
- 漏洞影响、攻击场景和触发条件。
|
|
||||||
- 已脱敏的日志、截图或 PoC。
|
|
||||||
- 是否涉及浏览器本地存储、API Key、WebDAV 同步、AI 渠道配置或接口请求。
|
|
||||||
|
|
||||||
## 范围说明
|
|
||||||
|
|
||||||
项目会优先处理由本仓库代码或默认配置导致的 XSS、敏感信息泄露、文件处理、导入导出、WebDAV 同步、权限控制和供应链风险。
|
|
||||||
|
|
||||||
第三方模型服务、托管平台、浏览器插件、用户自行泄露的 API Key、没有实际利用路径的安全头建议、纯社工钓鱼或账号找回问题通常不属于本项目漏洞范围。
|
|
||||||
|
|||||||
@@ -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、没有实际利用路径的安全头建议、纯社工钓鱼或账号找回问题通常不属于本项目漏洞范围。
|
||||||
@@ -1,41 +1,12 @@
|
|||||||
---
|
---
|
||||||
title: 赞助支持
|
title: Sponsor the Project
|
||||||
description: 赞助支持、广告合作、项目露出和社区推广说明
|
description: Sponsorship, advertising, and community support
|
||||||
---
|
---
|
||||||
|
|
||||||
# 赞助支持
|
# Sponsor the Project
|
||||||
|
|
||||||
如果本项目对你有帮助,欢迎通过 Star、广告赞助合作、提供 AI 订阅账号等方式支持项目继续维护。
|
You can support Infinite Canvas by starring the repository, sponsoring project placement, or providing subscriptions to AI development tools such as Codex or Claude. Support is used for AI development tools, model services, and project maintenance.
|
||||||
|
|
||||||
项目开发大量依赖 Codex、Claude 等 AI 编程工具辅助写代码、排查问题和整理文档,这些工具需要持续付费。相关支持会优先用于购买 AI 开发工具、模型服务和项目维护相关资源,帮助项目继续迭代。
|
For sponsorship placement in the documentation, README, or related pages, email [1844025705@qq.com](mailto:1844025705@qq.com) with **Advertising Sponsorship** in the subject. Include the brand or product, desired placement and duration, link and creative materials, budget, and preferred cooperation model.
|
||||||
|
|
||||||
## 支持方式
|
Relevant sponsors include AI tools, model and API services, developer tools, creation workflows, and services useful to open-source users and AI creators.
|
||||||
|
|
||||||
支持方式包括但不限于:
|
|
||||||
|
|
||||||
- 给 GitHub 仓库点 Star,帮助项目被更多人看到。
|
|
||||||
- 通过广告赞助合作支持项目维护成本。
|
|
||||||
- Codex、Claude 等 AI 编程工具订阅账号支持,帮助维护者持续使用 AI 辅助开发。
|
|
||||||
|
|
||||||
## 广告赞助
|
|
||||||
|
|
||||||
如果你希望在项目文档、README 或相关页面获得赞助露出,可以直接通过邮箱联系项目维护者沟通位置、周期和展示形式。
|
|
||||||
|
|
||||||
### 适合的赞助内容
|
|
||||||
|
|
||||||
- AI 工具、模型服务、API 服务或开发者工具。
|
|
||||||
- 与图片、视频、文本生成工作流相关的产品。
|
|
||||||
- 适合开源项目用户和 AI 创作者使用的服务。
|
|
||||||
|
|
||||||
### 建议提供的信息
|
|
||||||
|
|
||||||
- 品牌或产品名称。
|
|
||||||
- 希望展示的位置和周期。
|
|
||||||
- 需要展示的链接、文案和素材。
|
|
||||||
- 预算区间和合作方式。
|
|
||||||
|
|
||||||
## 联系方式
|
|
||||||
|
|
||||||
请直接发送邮件至 [1844025705@qq.com](mailto:1844025705@qq.com),并在邮件标题中注明“广告赞助”。
|
|
||||||
|
|
||||||
如果已经有展示素材或投放计划,可以在邮件中一并说明。
|
|
||||||
|
|||||||
@@ -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),并在邮件标题中注明“广告赞助”。
|
||||||
|
|
||||||
|
如果已经有展示素材或投放计划,可以在邮件中一并说明。
|
||||||
+27
-30
@@ -1,45 +1,42 @@
|
|||||||
# 无限画布文档索引
|
# Infinite Canvas Documentation Index
|
||||||
|
|
||||||
## 项目介绍
|
## Overview
|
||||||
|
|
||||||
- [快速开始](/docs/overview/quick-start)
|
- [Quick Start](/docs/overview/quick-start)
|
||||||
- [功能介绍](/docs/overview/features)
|
- [Features](/docs/overview/features)
|
||||||
- [Render 部署](/docs/overview/render)
|
- [Deploy on Render](/docs/overview/render)
|
||||||
- [Docker 部署](/docs/overview/docker)
|
- [Docker Deployment](/docs/overview/docker)
|
||||||
- [第三方 GitHub 提示词仓库](/docs/overview/third-party-prompt-repositories)
|
- [Third-party GitHub Prompt Repositories](/docs/overview/third-party-prompt-repositories)
|
||||||
|
|
||||||
## 操作手册
|
## Canvas Guide
|
||||||
|
|
||||||
- [画布节点操作手册](/docs/canvas/canvas-node-manual)
|
- [Canvas Node Guide](/docs/canvas/canvas-node-manual)
|
||||||
- [画布快捷键](/docs/canvas/canvas-shortcuts)
|
- [Canvas Shortcuts](/docs/canvas/canvas-shortcuts)
|
||||||
|
|
||||||
## 开发与数据
|
## Development and Data
|
||||||
|
|
||||||
- [本地开发](/docs/development/local-development)
|
- [Local Development](/docs/development/local-development)
|
||||||
- [画布数据结构](/docs/development/canvas-data-structure)
|
- [Canvas Data Structure](/docs/development/canvas-data-structure)
|
||||||
|
- [How the Local Codex Connection Works](/docs/development/local-codex-canvas)
|
||||||
|
|
||||||
## 商务合作
|
## Business
|
||||||
|
|
||||||
- [开源协议](/docs/business/license)
|
- [Open-source License](/docs/business/license)
|
||||||
- [贡献者协议](/docs/business/cla)
|
- [Contributor License Agreement](/docs/business/cla)
|
||||||
- [商务合作](/docs/business/business)
|
- [Business Cooperation](/docs/business/business)
|
||||||
|
|
||||||
## 支持与安全
|
## Support and Security
|
||||||
|
|
||||||
- [漏洞提交](/docs/support/security)
|
- [Report a Vulnerability](/docs/support/security)
|
||||||
- [赞助支持](/docs/support/sponsor)
|
- [Sponsor the Project](/docs/support/sponsor)
|
||||||
|
|
||||||
## 项目进度
|
## Project Progress
|
||||||
|
|
||||||
- [更新日志](/docs/progress/changelog)
|
- [Changelog](/docs/progress/changelog)
|
||||||
- [待测试](/docs/progress/pending-test)
|
- [Pending Tests](/docs/progress/pending-test)
|
||||||
- [TODO](/docs/progress/todo)
|
- [TODO](/docs/progress/todo)
|
||||||
|
|
||||||
## 说明
|
## Notes
|
||||||
|
|
||||||
- 当前画布项目和“我的素材”主要保存在浏览器本地,跨设备可自行配置 WebDAV 同步。
|
- Canvas projects and My Assets are primarily stored in the browser. WebDAV can be configured for cross-device synchronization.
|
||||||
- AI API Key 保存在浏览器本地,并由前端直接请求 OpenAI 兼容接口。
|
- The AI API key is stored in the browser, which sends requests directly to OpenAI-compatible endpoints.
|
||||||
|
|
||||||
## 原理说明
|
|
||||||
|
|
||||||
- [本地 Codex 连接画布原理](/docs/development/local-codex-canvas)
|
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# 无限画布文档索引
|
||||||
|
|
||||||
|
## 项目介绍
|
||||||
|
|
||||||
|
- [快速开始](/zh-CN/docs/overview/quick-start)
|
||||||
|
- [功能介绍](/zh-CN/docs/overview/features)
|
||||||
|
- [Render 部署](/zh-CN/docs/overview/render)
|
||||||
|
- [Docker 部署](/zh-CN/docs/overview/docker)
|
||||||
|
- [第三方 GitHub 提示词仓库](/zh-CN/docs/overview/third-party-prompt-repositories)
|
||||||
|
|
||||||
|
## 操作手册
|
||||||
|
|
||||||
|
- [画布节点操作手册](/zh-CN/docs/canvas/canvas-node-manual)
|
||||||
|
- [画布快捷键](/zh-CN/docs/canvas/canvas-shortcuts)
|
||||||
|
|
||||||
|
## 开发与数据
|
||||||
|
|
||||||
|
- [本地开发](/zh-CN/docs/development/local-development)
|
||||||
|
- [画布数据结构](/zh-CN/docs/development/canvas-data-structure)
|
||||||
|
|
||||||
|
## 商务合作
|
||||||
|
|
||||||
|
- [开源协议](/zh-CN/docs/business/license)
|
||||||
|
- [贡献者协议](/zh-CN/docs/business/cla)
|
||||||
|
- [商务合作](/zh-CN/docs/business/business)
|
||||||
|
|
||||||
|
## 支持与安全
|
||||||
|
|
||||||
|
- [漏洞提交](/zh-CN/docs/support/security)
|
||||||
|
- [赞助支持](/zh-CN/docs/support/sponsor)
|
||||||
|
|
||||||
|
## 项目进度
|
||||||
|
|
||||||
|
- [更新日志](/zh-CN/docs/progress/changelog)
|
||||||
|
- [待测试](/zh-CN/docs/progress/pending-test)
|
||||||
|
- [TODO](/zh-CN/docs/progress/todo)
|
||||||
|
|
||||||
|
## 说明
|
||||||
|
|
||||||
|
- 当前画布项目和“我的素材”主要保存在浏览器本地,跨设备可自行配置 WebDAV 同步。
|
||||||
|
- AI API Key 保存在浏览器本地,并由前端直接请求 OpenAI 兼容接口。
|
||||||
|
|
||||||
|
## 原理说明
|
||||||
|
|
||||||
|
- [本地 Codex 连接画布原理](/zh-CN/docs/development/local-codex-canvas)
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { HomeLayout } from 'fumadocs-ui/layouts/home';
|
|
||||||
import { baseOptions } from '@/lib/layout.shared';
|
|
||||||
|
|
||||||
export default function Layout({ children }: LayoutProps<'/'>) {
|
|
||||||
return <HomeLayout {...baseOptions()}>{children}</HomeLayout>;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { HomeLayout } from 'fumadocs-ui/layouts/home';
|
||||||
|
import { baseOptions } from '@/lib/layout.shared';
|
||||||
|
|
||||||
|
export default async function Layout({ params, children }: LayoutProps<'/[lang]'>) {
|
||||||
|
const { lang } = await params;
|
||||||
|
return <HomeLayout {...baseOptions(lang)}>{children}</HomeLayout>;
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { ArrowUpRight, BookOpen, Rocket } from 'lucide-react';
|
import { ArrowUpRight, BookOpen, Rocket } from 'lucide-react';
|
||||||
import { appName, gitConfig } from '@/lib/shared';
|
import { appNames, gitConfig } from '@/lib/shared';
|
||||||
|
import { localizePath, type Locale } from '@/lib/i18n';
|
||||||
|
import type { Metadata } from 'next';
|
||||||
|
|
||||||
const githubUrl = `https://github.com/${gitConfig.user}/${gitConfig.repo}`;
|
const githubUrl = `https://github.com/${gitConfig.user}/${gitConfig.repo}`;
|
||||||
const demoUrl = 'https://canvas.best/';
|
const demoUrl = 'https://canvas.best/';
|
||||||
@@ -11,45 +13,79 @@ const darkStarHistoryChart = `${starHistoryChart}&theme=dark`;
|
|||||||
const previewImages = [
|
const previewImages = [
|
||||||
{
|
{
|
||||||
src: 'https://i.ibb.co/TDFvGWDT/image.png',
|
src: 'https://i.ibb.co/TDFvGWDT/image.png',
|
||||||
title: '画布编排',
|
title: { en: 'Canvas composition', 'zh-CN': '画布编排' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: 'https://i.ibb.co/zVwJq3YS/image.png',
|
src: 'https://i.ibb.co/zVwJq3YS/image.png',
|
||||||
title: '图片生成',
|
title: { en: 'Image generation', 'zh-CN': '图片生成' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: 'https://i.ibb.co/PvY3qhhK/image.png',
|
src: 'https://i.ibb.co/PvY3qhhK/image.png',
|
||||||
title: '参考图编辑',
|
title: { en: 'Reference editing', 'zh-CN': '参考图编辑' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: 'https://i.ibb.co/7D04LwN/image.png',
|
src: 'https://i.ibb.co/7D04LwN/image.png',
|
||||||
title: '节点工作流',
|
title: { en: 'Node workflow', 'zh-CN': '节点工作流' },
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function HomePage() {
|
const messages = {
|
||||||
|
en: {
|
||||||
|
eyebrow: 'Open-source AI image creation workspace',
|
||||||
|
center: 'Documentation',
|
||||||
|
description: 'An infinite canvas for image creation that brings canvas composition, AI generation, reference editing, prompt libraries, and reusable assets into one workflow.',
|
||||||
|
quickStart: 'Quick Start',
|
||||||
|
demo: 'Live Demo',
|
||||||
|
gallery: 'Gallery',
|
||||||
|
features: 'Explore Features',
|
||||||
|
previewAlt: 'Infinite Canvas preview',
|
||||||
|
contributors: 'Contributors',
|
||||||
|
contributorsDescription: 'Thank you to everyone who has contributed to this project',
|
||||||
|
contributorsAlt: 'Contributor avatars',
|
||||||
|
},
|
||||||
|
'zh-CN': {
|
||||||
|
eyebrow: '开源 AI 图片创作工作台',
|
||||||
|
center: '文档中心',
|
||||||
|
description: '面向图片创作的无限画布,把画布编排、AI 生成、参考图编辑、提示词库和素材沉淀放在同一个工作流里。',
|
||||||
|
quickStart: '快速开始',
|
||||||
|
demo: '在线体验',
|
||||||
|
gallery: '效果展示',
|
||||||
|
features: '功能介绍',
|
||||||
|
previewAlt: '无限画布效果图',
|
||||||
|
contributors: '开发贡献者',
|
||||||
|
contributorsDescription: '感谢所有为本项目做出贡献的开发者',
|
||||||
|
contributorsAlt: '开发贡献者头像',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function HomePage({ params }: PageProps<'/[lang]'>) {
|
||||||
|
const { lang } = await params;
|
||||||
|
const locale = lang as Locale;
|
||||||
|
const text = messages[locale];
|
||||||
|
const appName = appNames[locale];
|
||||||
|
|
||||||
return (
|
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">
|
<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]">
|
<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>
|
||||||
<div className="inline-flex items-center gap-2 text-xs font-medium text-zinc-500 dark:text-zinc-400">
|
<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" />
|
<Rocket className="size-3.5 text-emerald-600 dark:text-emerald-400" />
|
||||||
开源 AI 图片创作工作台
|
{text.eyebrow}
|
||||||
</div>
|
</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)]">
|
<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}
|
{appName}
|
||||||
<span className="block text-zinc-500 dark:text-zinc-400">文档中心</span>
|
<span className="block text-zinc-500 dark:text-zinc-400">{text.center}</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-6 max-w-2xl text-base leading-8 text-zinc-600 dark:text-zinc-400">
|
<p className="mt-6 max-w-2xl text-base leading-8 text-zinc-600 dark:text-zinc-400">
|
||||||
面向图片创作的无限画布,把画布编排、AI 生成、参考图编辑、提示词库和素材沉淀放在同一个工作流里。
|
{text.description}
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-8 flex flex-wrap gap-3">
|
<div className="mt-8 flex flex-wrap gap-3">
|
||||||
<Link
|
<Link
|
||||||
href="/docs/overview/quick-start"
|
href={localizePath(locale, '/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"
|
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" />
|
<BookOpen className="size-4" />
|
||||||
快速开始
|
{text.quickStart}
|
||||||
</Link>
|
</Link>
|
||||||
<a
|
<a
|
||||||
href={githubUrl}
|
href={githubUrl}
|
||||||
@@ -66,7 +102,7 @@ export default function HomePage() {
|
|||||||
rel="noreferrer noopener"
|
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"
|
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"
|
||||||
>
|
>
|
||||||
在线体验
|
{text.demo}
|
||||||
<ArrowUpRight className="size-4" />
|
<ArrowUpRight className="size-4" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -75,7 +111,7 @@ export default function HomePage() {
|
|||||||
<div className="overflow-hidden rounded-2xl lg:w-[108%] lg:max-w-none">
|
<div className="overflow-hidden rounded-2xl lg:w-[108%] lg:max-w-none">
|
||||||
<img
|
<img
|
||||||
src={previewImages[3].src}
|
src={previewImages[3].src}
|
||||||
alt="无限画布效果图"
|
alt={text.previewAlt}
|
||||||
className="aspect-[16/10] w-full rounded-xl object-cover"
|
className="aspect-[16/10] w-full rounded-xl object-cover"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -85,14 +121,14 @@ export default function HomePage() {
|
|||||||
<div className="flex flex-col gap-2 md:flex-row md:items-end md:justify-between">
|
<div className="flex flex-col gap-2 md:flex-row md:items-end md:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-semibold text-zinc-950 dark:text-zinc-50 md:text-3xl">
|
<h2 className="text-2xl font-semibold text-zinc-950 dark:text-zinc-50 md:text-3xl">
|
||||||
效果展示
|
{text.gallery}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<Link
|
<Link
|
||||||
href="/docs/overview/features"
|
href={localizePath(locale, '/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"
|
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"
|
||||||
>
|
>
|
||||||
功能介绍
|
{text.features}
|
||||||
<ArrowUpRight className="size-4" />
|
<ArrowUpRight className="size-4" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
@@ -101,7 +137,7 @@ export default function HomePage() {
|
|||||||
<img
|
<img
|
||||||
key={item.src}
|
key={item.src}
|
||||||
src={item.src}
|
src={item.src}
|
||||||
alt={`${item.title}效果图`}
|
alt={item.title[locale]}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
decoding="async"
|
decoding="async"
|
||||||
className="aspect-[16/10] w-full rounded-2xl object-cover"
|
className="aspect-[16/10] w-full rounded-2xl object-cover"
|
||||||
@@ -112,10 +148,10 @@ export default function HomePage() {
|
|||||||
|
|
||||||
<section className="mx-auto mt-16 w-full max-w-4xl text-center">
|
<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 className="text-2xl font-semibold text-zinc-950 dark:text-zinc-50 md:text-3xl">
|
||||||
开发贡献者
|
{text.contributors}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-2 text-sm text-zinc-500 dark:text-zinc-400">
|
<p className="mt-2 text-sm text-zinc-500 dark:text-zinc-400">
|
||||||
感谢所有为本项目做出贡献的开发者
|
{text.contributorsDescription}
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-7 flex justify-center">
|
<div className="mt-7 flex justify-center">
|
||||||
<a
|
<a
|
||||||
@@ -126,7 +162,7 @@ export default function HomePage() {
|
|||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={`https://contrib.rocks/image?repo=${gitConfig.user}/${gitConfig.repo}`}
|
src={`https://contrib.rocks/image?repo=${gitConfig.user}/${gitConfig.repo}`}
|
||||||
alt="开发贡献者头像"
|
alt={text.contributorsAlt}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
decoding="async"
|
decoding="async"
|
||||||
className="max-w-full"
|
className="max-w-full"
|
||||||
@@ -169,3 +205,20 @@ export default function HomePage() {
|
|||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function generateMetadata({ params }: PageProps<'/[lang]'>): Promise<Metadata> {
|
||||||
|
const { lang } = await params;
|
||||||
|
const locale = lang as Locale;
|
||||||
|
const text = messages[locale];
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: `${appNames[locale]} ${text.center}`,
|
||||||
|
description: text.description,
|
||||||
|
alternates: {
|
||||||
|
languages: {
|
||||||
|
en: '/',
|
||||||
|
'zh-CN': '/zh-CN',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -3,18 +3,22 @@ import { source } from '@/lib/source';
|
|||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
|
|
||||||
export default async function Page(props: PageProps<'/docs/[...slug]'>) {
|
export default async function Page(props: PageProps<'/[lang]/docs/[...slug]'>) {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
const page = source.getPage(params.slug);
|
const page = source.getPage(params.slug, params.lang);
|
||||||
if (!page) notFound();
|
if (!page) notFound();
|
||||||
|
|
||||||
return <DocPageContent page={page} />;
|
return <DocPageContent page={page} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateMetadata(props: PageProps<'/docs/[...slug]'>): Promise<Metadata> {
|
export async function generateMetadata(props: PageProps<'/[lang]/docs/[...slug]'>): Promise<Metadata> {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
const page = source.getPage(params.slug);
|
const page = source.getPage(params.slug, params.lang);
|
||||||
if (!page) notFound();
|
if (!page) notFound();
|
||||||
|
|
||||||
return getDocPageMetadata(page);
|
return getDocPageMetadata(page);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function generateStaticParams() {
|
||||||
|
return source.generateParams();
|
||||||
|
}
|
||||||
@@ -3,9 +3,11 @@ import { DocsLayout } from 'fumadocs-ui/layouts/docs';
|
|||||||
import { baseOptions } from '@/lib/layout.shared';
|
import { baseOptions } from '@/lib/layout.shared';
|
||||||
import { DocsTopTabs } from '@/components/docs-top-tabs';
|
import { DocsTopTabs } from '@/components/docs-top-tabs';
|
||||||
|
|
||||||
export default function Layout({ children }: LayoutProps<'/docs'>) {
|
export default async function Layout({ params, children }: LayoutProps<'/[lang]/docs'>) {
|
||||||
|
const { lang } = await params;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DocsLayout {...baseOptions()} tree={source.getPageTree()} tabs={false}>
|
<DocsLayout {...baseOptions(lang)} tree={source.getPageTree(lang)} tabs={false}>
|
||||||
<DocsTopTabs />
|
<DocsTopTabs />
|
||||||
{children}
|
{children}
|
||||||
</DocsLayout>
|
</DocsLayout>
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
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 messages = {
|
||||||
|
en: {
|
||||||
|
title: 'Infinite Canvas Documentation',
|
||||||
|
description: 'Features, canvas guides, deployment, development, business, and support',
|
||||||
|
index: 'index.md',
|
||||||
|
},
|
||||||
|
'zh-CN': {
|
||||||
|
title: '无限画布文档',
|
||||||
|
description: '功能说明、操作手册、部署方式、开发文档、商务合作与赞助支持',
|
||||||
|
index: 'index.zh-CN.md',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
async function readDocsIndex(locale: keyof typeof messages) {
|
||||||
|
return readFile(join(process.cwd(), messages[locale].index), 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function Page({ params }: PageProps<'/[lang]/docs'>) {
|
||||||
|
const { lang } = await params;
|
||||||
|
const locale = lang as keyof typeof messages;
|
||||||
|
const content = await readDocsIndex(locale);
|
||||||
|
const text = messages[locale];
|
||||||
|
const toc = getTableOfContents(content);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DocsPage toc={toc}>
|
||||||
|
<DocsTitle>{text.title}</DocsTitle>
|
||||||
|
<DocsDescription>{text.description}</DocsDescription>
|
||||||
|
<DocsBody>
|
||||||
|
<Markdown components={getMDXComponents()} remarkPlugins={[remarkHeading]}>
|
||||||
|
{content}
|
||||||
|
</Markdown>
|
||||||
|
</DocsBody>
|
||||||
|
</DocsPage>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateMetadata({ params }: PageProps<'/[lang]/docs'>): Promise<Metadata> {
|
||||||
|
const { lang } = await params;
|
||||||
|
const text = messages[lang as keyof typeof messages];
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: text.title,
|
||||||
|
description: text.description,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
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 messages = {
|
||||||
|
en: {
|
||||||
|
title: 'Changelog',
|
||||||
|
description: 'Project release history',
|
||||||
|
content: '# Changelog\n\nThe detailed changelog is currently maintained in Chinese. See the [source changelog](https://github.com/basketikun/infinite-canvas/blob/main/CHANGELOG.md) for all releases.',
|
||||||
|
},
|
||||||
|
'zh-CN': {
|
||||||
|
title: '更新日志',
|
||||||
|
description: '项目版本变更记录',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
async function readChangelog() {
|
||||||
|
return readFile(join(process.cwd(), '..', 'CHANGELOG.md'), 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function ChangelogPage({ params }: PageProps<'/[lang]/docs/progress/changelog'>) {
|
||||||
|
const { lang } = await params;
|
||||||
|
const text = messages[lang as keyof typeof messages];
|
||||||
|
const changelog = lang === 'zh-CN' ? await readChangelog() : messages.en.content;
|
||||||
|
const toc = getTableOfContents(changelog);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DocsPage toc={toc}>
|
||||||
|
<DocsTitle>{text.title}</DocsTitle>
|
||||||
|
<DocsDescription>{text.description}</DocsDescription>
|
||||||
|
<DocsBody>
|
||||||
|
<Markdown components={getMDXComponents()} remarkPlugins={[remarkHeading]}>
|
||||||
|
{changelog}
|
||||||
|
</Markdown>
|
||||||
|
</DocsBody>
|
||||||
|
</DocsPage>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateMetadata({ params }: PageProps<'/[lang]/docs/progress/changelog'>): Promise<Metadata> {
|
||||||
|
const { lang } = await params;
|
||||||
|
const text = messages[lang as keyof typeof messages];
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: text.title,
|
||||||
|
description: text.description,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { Provider } from '@/components/provider';
|
||||||
|
import '../global.css';
|
||||||
|
import { i18n } from '@/lib/i18n';
|
||||||
|
|
||||||
|
export default async function Layout({ params, children }: LayoutProps<'/[lang]'>) {
|
||||||
|
const { lang } = await params;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<html lang={lang} suppressHydrationWarning>
|
||||||
|
<body className="flex flex-col min-h-screen">
|
||||||
|
<Provider locale={lang}>{children}</Provider>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateStaticParams() {
|
||||||
|
return i18n.languages.map((lang) => ({ lang }));
|
||||||
|
}
|
||||||
@@ -5,7 +5,14 @@ import { createFromSource } from 'fumadocs-core/search/server';
|
|||||||
export const revalidate = false;
|
export const revalidate = false;
|
||||||
|
|
||||||
export const { staticGET: GET } = createFromSource(source, {
|
export const { staticGET: GET } = createFromSource(source, {
|
||||||
|
localeMap: {
|
||||||
|
en: {
|
||||||
|
language: 'english',
|
||||||
|
},
|
||||||
|
'zh-CN': {
|
||||||
components: {
|
components: {
|
||||||
tokenizer: createDocsSearchTokenizer(),
|
tokenizer: createDocsSearchTokenizer(),
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { Provider } from '@/components/provider';
|
|
||||||
import './global.css';
|
|
||||||
|
|
||||||
export default function Layout({ children }: LayoutProps<'/'>) {
|
|
||||||
return (
|
|
||||||
<html lang="zh-CN" suppressHydrationWarning>
|
|
||||||
<body className="flex flex-col min-h-screen">
|
|
||||||
<Provider>{children}</Provider>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -4,9 +4,10 @@ import { join } from 'node:path';
|
|||||||
|
|
||||||
export const revalidate = false;
|
export const revalidate = false;
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET(request: Request) {
|
||||||
const docsIndex = await readFile(join(process.cwd(), 'index.md'), 'utf8');
|
const locale = new URL(request.url).searchParams.get('locale') ?? 'en';
|
||||||
const scan = source.getPages().map(getLLMText);
|
const docsIndex = await readFile(join(process.cwd(), locale === 'zh-CN' ? 'index.zh-CN.md' : 'index.md'), 'utf8');
|
||||||
|
const scan = source.getPages(locale).map(getLLMText);
|
||||||
const scanned = await Promise.all(scan);
|
const scanned = await Promise.all(scan);
|
||||||
|
|
||||||
return new Response([docsIndex, ...scanned].join('\n\n'));
|
return new Response([docsIndex, ...scanned].join('\n\n'));
|
||||||
|
|||||||
@@ -3,10 +3,11 @@ import { notFound } from 'next/navigation';
|
|||||||
|
|
||||||
export const revalidate = false;
|
export const revalidate = false;
|
||||||
|
|
||||||
export async function GET(_req: Request, { params }: RouteContext<'/llms.mdx/docs/[[...slug]]'>) {
|
export async function GET(req: Request, { params }: RouteContext<'/llms.mdx/docs/[[...slug]]'>) {
|
||||||
const { slug } = await params;
|
const { slug } = await params;
|
||||||
|
const locale = new URL(req.url).searchParams.get('locale') ?? 'en';
|
||||||
// remove the appended "content.md"
|
// remove the appended "content.md"
|
||||||
const page = source.getPage(slug?.slice(0, -1));
|
const page = source.getPage(slug?.slice(0, -1), locale);
|
||||||
if (!page) notFound();
|
if (!page) notFound();
|
||||||
|
|
||||||
return new Response(await getLLMText(page), {
|
return new Response(await getLLMText(page), {
|
||||||
@@ -17,7 +18,7 @@ export async function GET(_req: Request, { params }: RouteContext<'/llms.mdx/doc
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function generateStaticParams() {
|
export function generateStaticParams() {
|
||||||
return source.getPages().map((page) => ({
|
return source.getPages('en').map((page) => ({
|
||||||
slug: getPageMarkdownUrl(page).segments,
|
slug: getPageMarkdownUrl(page).segments,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import { join } from 'node:path';
|
|||||||
|
|
||||||
export const revalidate = false;
|
export const revalidate = false;
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET(request: Request) {
|
||||||
const docsIndex = await readFile(join(process.cwd(), 'index.md'), 'utf8');
|
const locale = new URL(request.url).searchParams.get('locale') ?? 'en';
|
||||||
return new Response([docsIndex, llms(source).index()].join('\n\n'));
|
const docsIndex = await readFile(join(process.cwd(), locale === 'zh-CN' ? 'index.zh-CN.md' : 'index.md'), 'utf8');
|
||||||
|
return new Response([docsIndex, llms(source).index(locale)].join('\n\n'));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +1,41 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { usePathname } from 'next/navigation';
|
import { useParams, usePathname } from 'next/navigation';
|
||||||
import { cn } from '@/lib/cn';
|
import { cn } from '@/lib/cn';
|
||||||
|
import { localizePath, type Locale } from '@/lib/i18n';
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ title: '项目介绍', href: '/docs/overview/quick-start', prefix: '/docs/overview' },
|
{ title: { en: 'Overview', 'zh-CN': '项目介绍' }, href: '/docs/overview/quick-start', prefix: '/docs/overview' },
|
||||||
{ title: '操作手册', href: '/docs/canvas/canvas-node-manual', prefix: '/docs/canvas' },
|
{ title: { en: 'Canvas Guide', 'zh-CN': '操作手册' }, href: '/docs/canvas/canvas-node-manual', prefix: '/docs/canvas' },
|
||||||
{ title: '开发文档', href: '/docs/development/local-development', prefix: '/docs/development' },
|
{ title: { en: 'Development', 'zh-CN': '开发文档' }, href: '/docs/development/local-development', prefix: '/docs/development' },
|
||||||
{ title: '项目进度', href: '/docs/progress/changelog', prefix: '/docs/progress' },
|
{ title: { en: 'Progress', 'zh-CN': '项目进度' }, href: '/docs/progress/changelog', prefix: '/docs/progress' },
|
||||||
{ title: '商务合作', href: '/docs/business/business', prefix: '/docs/business' },
|
{ title: { en: 'Business', 'zh-CN': '商务合作' }, href: '/docs/business/business', prefix: '/docs/business' },
|
||||||
{ title: '赞助支持', href: '/docs/support/sponsor', prefix: '/docs/support' },
|
{ title: { en: 'Support', 'zh-CN': '赞助支持' }, href: '/docs/support/sponsor', prefix: '/docs/support' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export function DocsTopTabs() {
|
export function DocsTopTabs() {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
const { lang } = useParams<{ lang: Locale }>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="sticky top-0 z-30 hidden h-12 self-start overflow-x-auto border-b bg-fd-background/95 px-6 pt-3 backdrop-blur [grid-area:main] md:flex xl:px-8">
|
<nav className="sticky top-0 z-30 hidden h-12 self-start overflow-x-auto border-b bg-fd-background/95 px-6 pt-3 backdrop-blur [grid-area:main] md:flex xl:px-8">
|
||||||
<div className="flex flex-row items-end gap-6">
|
<div className="flex flex-row items-end gap-6">
|
||||||
{tabs.map((tab) => {
|
{tabs.map((tab) => {
|
||||||
const active = tab.prefix ? pathname === tab.href || pathname.startsWith(`${tab.prefix}/`) : pathname === tab.href;
|
const href = localizePath(lang, tab.href);
|
||||||
|
const prefix = localizePath(lang, tab.prefix);
|
||||||
|
const active = pathname === href || pathname.startsWith(`${prefix}/`);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
key={tab.href}
|
key={href}
|
||||||
href={tab.href}
|
href={href}
|
||||||
className={cn(
|
className={cn(
|
||||||
'inline-flex border-b-2 border-transparent pb-1.5 text-sm font-medium text-nowrap text-fd-muted-foreground transition-colors hover:text-fd-accent-foreground',
|
'inline-flex border-b-2 border-transparent pb-1.5 text-sm font-medium text-nowrap text-fd-muted-foreground transition-colors hover:text-fd-accent-foreground',
|
||||||
active && 'border-fd-primary text-fd-primary',
|
active && 'border-fd-primary text-fd-primary',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{tab.title}
|
{tab.title[lang]}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import SearchDialog from '@/components/search';
|
import SearchDialog from '@/components/search';
|
||||||
import { RootProvider } from 'fumadocs-ui/provider/next';
|
import { RootProvider } from 'fumadocs-ui/provider/next';
|
||||||
|
import { i18nProvider } from 'fumadocs-ui/i18n';
|
||||||
import { type ReactNode } from 'react';
|
import { type ReactNode } from 'react';
|
||||||
|
import { translations } from '@/lib/layout.shared';
|
||||||
|
|
||||||
export function Provider({ children }: { children: ReactNode }) {
|
export function Provider({ locale, children }: { locale: string; children: ReactNode }) {
|
||||||
return <RootProvider search={{ SearchDialog }}>{children}</RootProvider>;
|
return <RootProvider i18n={i18nProvider(translations, locale)} search={{ SearchDialog }}>{children}</RootProvider>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,14 @@ import { create } from '@orama/orama';
|
|||||||
import { useI18n } from 'fumadocs-ui/contexts/i18n';
|
import { useI18n } from 'fumadocs-ui/contexts/i18n';
|
||||||
import { createDocsSearchTokenizer } from '@/lib/search-tokenizer';
|
import { createDocsSearchTokenizer } from '@/lib/search-tokenizer';
|
||||||
|
|
||||||
function initOrama() {
|
function initOrama(locale?: string) {
|
||||||
|
if (locale !== 'zh-CN') {
|
||||||
|
return create({
|
||||||
|
schema: { _: 'string' },
|
||||||
|
language: 'english',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return create({
|
return create({
|
||||||
schema: { _: 'string' },
|
schema: { _: 'string' },
|
||||||
components: {
|
components: {
|
||||||
@@ -25,7 +32,7 @@ function initOrama() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function DefaultSearchDialog(props: SharedProps) {
|
export default function DefaultSearchDialog(props: SharedProps) {
|
||||||
const { locale } = useI18n(); // (optional) for i18n
|
const { locale } = useI18n();
|
||||||
const { search, setSearch, query } = useDocsSearch({
|
const { search, setSearch, query } = useDocsSearch({
|
||||||
type: 'static',
|
type: 'static',
|
||||||
initOrama,
|
initOrama,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
MarkdownCopyButton,
|
MarkdownCopyButton,
|
||||||
ViewOptionsPopover,
|
ViewOptionsPopover,
|
||||||
} from 'fumadocs-ui/layouts/docs/page';
|
} from 'fumadocs-ui/layouts/docs/page';
|
||||||
|
import { localizePath } from './i18n';
|
||||||
|
|
||||||
export type DocPageData = (typeof source)['$inferPage'];
|
export type DocPageData = (typeof source)['$inferPage'];
|
||||||
|
|
||||||
@@ -38,8 +39,16 @@ export function DocPageContent({ page }: { page: DocPageData }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getDocPageMetadata(page: DocPageData): Metadata {
|
export function getDocPageMetadata(page: DocPageData): Metadata {
|
||||||
|
const path = `/docs/${page.slugs.join('/')}`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: page.data.title,
|
title: page.data.title,
|
||||||
description: page.data.description,
|
description: page.data.description,
|
||||||
|
alternates: {
|
||||||
|
languages: {
|
||||||
|
en: localizePath('en', path),
|
||||||
|
'zh-CN': localizePath('zh-CN', path),
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { defineI18n } from 'fumadocs-core/i18n';
|
||||||
|
|
||||||
|
export type Locale = 'en' | 'zh-CN';
|
||||||
|
|
||||||
|
export const i18n = defineI18n({
|
||||||
|
defaultLanguage: 'en',
|
||||||
|
languages: ['en', 'zh-CN'],
|
||||||
|
parser: 'dot',
|
||||||
|
hideLocale: 'default-locale',
|
||||||
|
fallbackLanguage: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
export function localizePath(locale: string, path: string) {
|
||||||
|
return locale === i18n.defaultLanguage ? path : `/${locale}${path}`;
|
||||||
|
}
|
||||||
@@ -1,11 +1,52 @@
|
|||||||
import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
|
import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
|
||||||
import { appName, gitConfig } from './shared';
|
import { appNames, gitConfig } from './shared';
|
||||||
import { ArrowUpRight } from 'lucide-react';
|
import { ArrowUpRight } from 'lucide-react';
|
||||||
|
import { i18n } from './i18n';
|
||||||
|
import { uiTranslations } from 'fumadocs-ui/i18n';
|
||||||
|
|
||||||
const githubUrl = `https://github.com/${gitConfig.user}/${gitConfig.repo}`;
|
const githubUrl = `https://github.com/${gitConfig.user}/${gitConfig.repo}`;
|
||||||
const qqUrl = 'https://qm.qq.com/q/DFnKzZ807u';
|
const qqUrl = 'https://qm.qq.com/q/DFnKzZ807u';
|
||||||
|
|
||||||
export function baseOptions(): BaseLayoutProps {
|
export const translations = i18n.translations().extend(uiTranslations()).add('ui', {
|
||||||
|
en: {
|
||||||
|
displayName: 'English',
|
||||||
|
},
|
||||||
|
'zh-CN': {
|
||||||
|
displayName: '简体中文',
|
||||||
|
search: '搜索文档',
|
||||||
|
searchNoResult: '没有找到结果',
|
||||||
|
searchOpen: '打开搜索',
|
||||||
|
searchClose: '关闭搜索',
|
||||||
|
toc: '本页目录',
|
||||||
|
tocNoHeadings: '本页没有标题',
|
||||||
|
tocInline: '本页内容',
|
||||||
|
chooseLanguage: '选择语言',
|
||||||
|
nextPage: '下一页',
|
||||||
|
previousPage: '上一页',
|
||||||
|
chooseTheme: '选择主题',
|
||||||
|
themeToggle: '切换主题',
|
||||||
|
themeLight: '浅色',
|
||||||
|
themeDark: '深色',
|
||||||
|
themeSystem: '跟随系统',
|
||||||
|
codeBlockCopy: '复制代码',
|
||||||
|
codeBlockCopied: '已复制',
|
||||||
|
menuToggle: '切换菜单',
|
||||||
|
pageActionsCopyMarkdown: '复制 Markdown',
|
||||||
|
pageActionsOpen: '打开',
|
||||||
|
pageActionsOpenGitHub: '在 GitHub 中打开',
|
||||||
|
pageActionsViewMarkdown: '查看 Markdown',
|
||||||
|
sidebarOpen: '打开侧边栏',
|
||||||
|
sidebarCollapse: '收起侧边栏',
|
||||||
|
notFoundTitle: '页面不存在',
|
||||||
|
notFoundDescription: '你访问的页面不存在。',
|
||||||
|
notFoundLink: '返回首页',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export function baseOptions(locale: string): BaseLayoutProps {
|
||||||
|
const chinese = locale === 'zh-CN';
|
||||||
|
const appName = appNames[locale as keyof typeof appNames];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
nav: {
|
nav: {
|
||||||
title: (
|
title: (
|
||||||
@@ -17,14 +58,14 @@ export function baseOptions(): BaseLayoutProps {
|
|||||||
},
|
},
|
||||||
links: [
|
links: [
|
||||||
{
|
{
|
||||||
text: '文档导航',
|
text: chinese ? '文档导航' : 'Documentation',
|
||||||
url: '/docs/overview/quick-start',
|
url: `${chinese ? '/zh-CN' : ''}/docs/overview/quick-start`,
|
||||||
on: 'nav',
|
on: 'nav',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: (
|
text: (
|
||||||
<span className="inline-flex items-center gap-1.5">
|
<span className="inline-flex items-center gap-1.5">
|
||||||
<span>在线体验</span>
|
<span>{chinese ? '在线体验' : 'Live Demo'}</span>
|
||||||
<ArrowUpRight className="size-4" />
|
<ArrowUpRight className="size-4" />
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
export const appName = '无限画布';
|
export const appNames = {
|
||||||
|
en: 'Infinite Canvas',
|
||||||
|
'zh-CN': '无限画布',
|
||||||
|
};
|
||||||
export const docsRoute = '/docs';
|
export const docsRoute = '/docs';
|
||||||
export const docsContentRoute = '/llms.mdx/docs';
|
export const docsContentRoute = '/llms.mdx/docs';
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,23 @@
|
|||||||
import { docs } from 'collections/server';
|
import { docs } from 'collections/server';
|
||||||
import { loader } from 'fumadocs-core/source';
|
import { loader } from 'fumadocs-core/source';
|
||||||
import { docsContentRoute, docsRoute } from './shared';
|
import { docsContentRoute, docsRoute } from './shared';
|
||||||
|
import { i18n } from './i18n';
|
||||||
|
|
||||||
// See https://fumadocs.dev/docs/headless/source-api for more info
|
// See https://fumadocs.dev/docs/headless/source-api for more info
|
||||||
export const source = loader({
|
export const source = loader({
|
||||||
baseUrl: docsRoute,
|
baseUrl: docsRoute,
|
||||||
source: docs.toFumadocsSource(),
|
source: docs.toFumadocsSource(),
|
||||||
|
i18n,
|
||||||
plugins: [],
|
plugins: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
export function getPageMarkdownUrl(page: (typeof source)['$inferPage']) {
|
export function getPageMarkdownUrl(page: (typeof source)['$inferPage']) {
|
||||||
const segments = [...page.slugs, 'content.md'];
|
const segments = [...page.slugs, 'content.md'];
|
||||||
|
const locale = page.locale === i18n.defaultLanguage ? '' : `?locale=${page.locale}`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
segments,
|
segments,
|
||||||
url: `${docsContentRoute}/${segments.join('/')}`,
|
url: `${docsContentRoute}/${segments.join('/')}${locale}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { i18n } from '@/lib/i18n';
|
||||||
|
import { createI18nMiddleware } from 'fumadocs-core/i18n/middleware';
|
||||||
|
|
||||||
|
export default createI18nMiddleware(i18n);
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
matcher: ['/((?!api|_next/static|_next/image|favicon.ico|.*\\..*).*)'],
|
||||||
|
};
|
||||||
@@ -84,7 +84,7 @@ export function formatAgentActivity(event: AgentEventPayload): Omit<AgentChatIte
|
|||||||
if (item.type === "plan") {
|
if (item.type === "plan") {
|
||||||
const text = stringText(item.text);
|
const text = stringText(item.text);
|
||||||
if (completed && !text && !item.error?.message) return null;
|
if (completed && !text && !item.error?.message) return null;
|
||||||
return { role: "tool", title: tr("plan"), text: item.error?.message || text || activityPlaceholder(item.type), detail: { kind: "plan", status: itemStatus, ...(item.error?.message ? { output: item.error.message } : {}) };
|
return { role: "tool", title: tr("plan"), text: item.error?.message || text || activityPlaceholder(item.type), detail: { kind: "plan", status: itemStatus, ...(item.error?.message ? { output: item.error.message } : {}) } };
|
||||||
}
|
}
|
||||||
if (item.type === "command_execution") {
|
if (item.type === "command_execution") {
|
||||||
const command = stringText(item.command);
|
const command = stringText(item.command);
|
||||||
|
|||||||
Reference in New Issue
Block a user