feat(agent): implement streamdown for real-time message rendering in chat

This commit is contained in:
HouYunFei
2026-07-09 17:16:35 +08:00
parent 25e1d173e8
commit 864ce4101a
7 changed files with 22 additions and 24 deletions
+2 -15
View File
@@ -8,7 +8,7 @@
- 写代码保持最少行数,能简单实现就不要引入复杂抽象。
- 标准格式、协议、解析、压缩、加密、日期等通用能力优先使用成熟稳定的库,不要手写底层实现,除非用户明确要求或项目已有实现必须沿用。
- 不要为了“兼容更多场景”写大量分支,只实现当前明确需要的功能。
- 项目尚未上线,不需要兼容旧数据;表结构或字段调整时直接按新设计修改,不写旧字段兼容数据迁移兜底或删除旧表的清理逻辑,除非用户明确要求。
- 项目尚未上线,不需要兼容旧数据;本地存储结构调整时直接按新设计修改,不写旧字段兼容数据迁移兜底,除非用户明确要求。
- 每次写完代码,不需要检查语法,不需要执行构建,用户会自己做。
- 不要改无关文件,不要顺手重构。
- 如果工作区已有用户改动,不要回滚,不要覆盖;只在必要范围内追加修改。
@@ -19,22 +19,11 @@
- 补充时写成明确、可执行的规则,避免只写模糊描述。
- 新规则应放到最相关的章节;找不到合适章节时放到“项目注意事项”。
## 后端规范
- 后端使用 Go + Gin + GORM。
- `handler/` 只处理 HTTP 入参、调用 service、返回 `OK` / `Fail`
- `service/` 放业务逻辑、默认值、校验、时间、ID、鉴权等处理。
- `repository/` 只做数据库访问和 GORM 查询。
- `model/` 只定义数据结构、枚举和简单模型方法。
- 列表接口优先沿用 `model.Query``Normalize`、分页和标签筛选方式。
- 业务接口保持 `{ code, data, msg }` 的响应结构。
- 新增数据表时同步更新 `docs/backend-database.md`
## 前端规范
- 前端使用 Vite、React、React Router、TypeScript、Ant Design、Tailwind、Zustand。
- 编写 Ant Design 相关代码时,参考 https://ant.design/llms-full.txt 理解组件 API、示例和设计规范,并优先结合项目当前 antd 版本与既有写法。
- API 请求统一放在 `web/src/services/api/`
- 外部服务请求统一放在 `web/src/services/api/`,由浏览器前端直连,不假设存在项目后端
- 全局或跨页面状态优先放在 `web/src/stores/`
- 已经放在全局 store 或全局 hook 中的状态/动作,组件需要时直接使用对应 store/hook,不要为了“纯组件”层层透传 props;避免一个组件传递过多参数。
- 全局组件、全局常量、全局配置等全局性质的内容不要作为 props 或参数层层传递;哪里需要就在哪里直接从对应全局入口获取。
@@ -76,8 +65,6 @@
- 每次重大改动(新增/调整/删除功能、接口或工具,影响用户可感知行为)完成后,都要在 `CHANGELOG.md``Unreleased` 追加一条记录,按 `[新增]` / `[调整]` / `[修复]` / `[优化]` 前缀分类,用一句中文归纳;纯内部重构、格式化、无用户可感知影响的小改动可不记。
- 每次 todo 事项完成后,先从 `docs/content/docs/progress/todo.mdx` 移到 `docs/content/docs/progress/pending-test.mdx`,不要直接写进正式功能说明;用户确认测试通过后再更新 `docs/content/docs/overview/features.mdx`
- 每次任务完成前,都要根据实际变更检查并更新 `docs/content/docs/progress/todo.mdx``docs/content/docs/progress/pending-test.mdx`;如果功能或待办没有变化,也要确认无需修改。
- 接口响应规则写到 `docs/content/docs/backend/api-response.mdx`
- 数据库结构写到 `docs/content/docs/backend/backend-database.mdx`
- 文档不要写过期日期;除非用户明确要求记录具体时间。
## 发版本流程
+7 -6
View File
@@ -2,12 +2,13 @@
## Unreleased
+ [新增] Agent 新增站点级工具:`canvas_list_projects`(画布清单,仅标题/日期/节点数/连线数,配合 `site_navigate` 打开指定画布)、`workbench_image_get_config` / `workbench_image_generate`(生图工作台读取配置、填写提示词并触发生成)、`workbench_video_get_config` / `workbench_video_generate`(视频创作台同理)、`prompts_search`(分页搜索提示词库)、`assets_list` / `assets_add`(查看与新增「我的素材」)
+ [新增] Agent 面板全站化:从画布页抽离为全局常驻右侧面板,开/关整体推动顶栏和内容(类似浏览器侧边栏扩展),顶栏和画布工具栏各有一个独立开关按钮
+ [新增] Agent 新增 `site_navigate` 工具,Codex 可直接控制网站跳转至首页、画布、生图、视频、提示词库、素材、配置等页面
+ [新增] Agent 添加停止对话按钮:对话运行中发送按钮变为红色停止按钮,点击后向后端发送 SIGINT 中断当前 Codex turn
+ [调整] Codex 会话改为单一站点级连续线程,不再按画布 ID 分隔,跨页面和跨画布保持同一对话上下文;
+ [调整] 移除仅前端调用 OpenAI responses 接口的在线画布助手死代码,Agent 统一走 MCP + 本地 Codex 链路
+ [新增] Agent 对话消息改用 streamdown 流式渲染,提升长回复与 Markdown 内容展示效果
+ [新增] Agent 新增画布、工作台、提示词库和素材等站点级工具,支持读取配置、触发生成与新增素材
+ [新增] Agent 面板改为全站常驻右侧栏,开关时同步推动顶栏和页面内容,并保留独立入口按钮
+ [新增] Agent 新增 `site_navigate` 工具,支持从对话中直接跳转首页、画布、工作台、素材和配置等页面
+ [新增] Agent 对话运行中支持一键停止,发送按钮切换为红色停止按钮并中断当前 Codex turn。
+ [调整] Codex 会话改为站点级连续线程,不再按画布 ID 分隔,跨页面和跨画布保持同一上下文
+ [调整] 移除仅前端调用 OpenAI responses 接口的在线画布助手旧逻辑,统一走 MCP + 本地 Codex 链路。
+ [新增] 画布节点支持统一维护名称字段,默认显示在节点上方,并可直接双击名称编辑。
+ [新增] 画布新增组节点,支持节点拖入/拆出分组、拖拽高亮吸附和移动组时带动子节点。
+ [调整] 画布节点顶部工具条改为点击选中节点后显示,避免鼠标经过节点时频繁弹出。
+1 -1
View File
@@ -25,7 +25,7 @@
无限画布是一款面向图片创作的开源工作台。它把画布编排、AI 图片生成、参考图编辑、对话助手、提示词库和素材沉淀放在同一个界面里,适合用来探索视觉方案并连续迭代图片结果。
> [!CAUTION]
> 项目目前处于开发阶段,不保证历史数据兼容。各种数据库结构和存储格式都可能直接调整,欢迎关注后续更新,当前更适合个人/本地部署,不建议直接公网多人共用。
> 项目目前处于开发阶段,不保证历史数据兼容。各种本地存储格式都可能直接调整,欢迎关注后续更新,当前更适合个人/本地部署,不建议直接公网多人共用。
>
> 如果你需要稳定维护自己的分支,建议自行 fork 后独立开发。二次开发与 PR 请保留原作者信息和前端页面标识。
+2 -1
View File
@@ -35,6 +35,7 @@
"react-router": "^7.12.0",
"react-router-dom": "^7.12.0",
"shadcn": "^4.7.0",
"streamdown": "^2.5.0",
"tailwind-merge": "^3.6.0",
"tailwindcss": "^4",
"tw-animate-css": "^1.4.0",
@@ -42,11 +43,11 @@
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@vitejs/plugin-react": "^5.2.0",
"@types/file-saver": "^2.0.7",
"@types/node": "^20",
"@types/react": "19.1.12",
"@types/react-dom": "19.1.9",
"@vitejs/plugin-react": "^5.2.0",
"prettier": "^3.8.3",
"typescript": "^5",
"vite": "^7.3.0"
@@ -1,6 +1,7 @@
import { useEffect, useRef, useState, type ReactNode } from "react";
import { Button, Tooltip } from "antd";
import { ArrowUp, CheckCircle2, CircleAlert, ImagePlus, LoaderCircle, Square, UserRound, Wrench, X, XCircle } from "lucide-react";
import { Streamdown } from "streamdown";
import { isPlainEnterKey } from "@/lib/keyboard-event";
import { canvasThemes } from "@/lib/canvas-theme";
@@ -15,6 +16,8 @@ export type CanvasAgentChatMessage = {
meta?: string;
detail?: unknown;
attachments?: CanvasAgentChatAttachment[];
/** Present while the message is actively streaming; cleared on completion. */
streamId?: string;
};
const WORKING_TEXT = "working...";
@@ -46,7 +49,11 @@ export function AgentChatMessage({ item, theme, user, onRejectTool, onApproveToo
<div className={`flex items-start gap-3 ${isUser ? "justify-end" : "justify-start"}`}>
{!isUser ? <AgentAvatar theme={theme} /> : null}
<div className={`min-w-0 max-w-[82%] text-sm leading-6 ${isUser ? "text-right" : "text-left"}`} style={{ color }}>
<div className="whitespace-pre-wrap break-words text-left">{item.text}</div>
{isUser ? (
<div className="whitespace-pre-wrap break-words text-left">{item.text}</div>
) : (
<Streamdown animated isAnimating={!!item.streamId}>{item.text}</Streamdown>
)}
{item.attachments?.length ? <AgentMessageAttachments attachments={item.attachments} /> : null}
{item.meta ? <div className="mt-1 text-[11px] opacity-45">{item.meta}</div> : null}
</div>
+1
View File
@@ -1,6 +1,7 @@
import React from "react";
import { createRoot } from "react-dom/client";
import "antd/dist/reset.css";
import "streamdown/styles.css";
import "./styles/globals.css";
import { RouterProvider } from "react-router-dom";
+1
View File
@@ -1,5 +1,6 @@
@import "tailwindcss";
@import "tw-animate-css";
@source "../../../node_modules/streamdown/dist/*.js";
@import "shadcn/tailwind.css";
@custom-variant dark (&:is(.dark *));