@done-coding/forge-agent
v0.5.0
Published
AI 对话 / 智能体 UI 库
Readme
@done-coding/forge-agent
AI 对话 / 智能体 UI 库:OpenAI 协议 SSE 对话驱动的组件 + hooks(AiDialogue 全流程对话 / AiMessageList 受控展示 / AiSender 受控输入 / 会话持久化 / 移动端适配),经 vue-element-plus-x 封装,让 AI 对话更容易接入。
协议层(C 阶段已交付,2026-08-02):SSE 分两块——传输层为 OpenAI Chat Completions 协议(POST
{model, messages, stream:true}+ SSE 帧解析 +delta.content累积 +[DONE]),业务数据经 转换器((frame: AiSSEFrame) => AiMessagePart[])由业务方注入(缺省恒等)。function call 仅立结构(AiToolPart v5 状态机 +delta.tool_calls增量解析),不实现调用逻辑。历史 Dify 契约(response_mode:"streaming"+ workflow 事件序)已移除。
安装
pnpm add @done-coding/forge-agent样式必须显式引入(SFC scoped CSS 被抽取到独立 es/style.css,漏引会导致 scoped 选择器失效、布局塌陷):
import "@done-coding/forge-agent/style.css";peer 依赖需消费方自装:vue / element-plus / @element-plus/icons-vue / lodash。
快速开始(最小可用)
① 受控展示:零后端依赖,接入最快
静态消息展示(markdown 渲染 / 业务 data part),结构对齐业界 UIMessage:
// 参考 apps/reference/src/pages/integration/agent/Index.vue
import { AiConfigProvider, AiMessageList, type AiMessage } from "@done-coding/forge-agent";
import "@done-coding/forge-agent/style.css";
const messages: AiMessage[] = [
{ id: "m1", role: "user", content: "你好" },
{ id: "m2", role: "assistant", content: "**你好!** 我是 forge-agent。" },
];<AiConfigProvider :is-dark-theme="false">
<AiMessageList :messages="messages" />
</AiConfigProvider>要点:
- 消息形态:
{ id, role: "user"|"assistant", parts | content }——content是纯文本 shorthand,等价parts:[{type:"text",text}];parts存在时以parts为准 - 业务数据走
data-<类型>part({ type:"data-card", data: {...} }),经#part插槽由消费方渲染(缺省平台占位兜底) AiConfigProvider提供主题变量(--dc-agent-*)与 expose 通道,[MUST] 包裹对话组件
② 完整对话:自管全流程(持久化 + SSE + 输入)
// 参考 demo-src/demos/DialogueDemo.vue
import { AiDialogue, AiDialogueGroup } from "@done-coding/forge-agent";
// AiDialogue:单会话对话(userName 必填——派生命名空间化的 IndexedDB 库)
<AiDialogue userName="demo-user" />
// AiDialogueGroup:多会话 + 会话列表操作条(操作条默认折叠)
<AiDialogueGroup :user-name="'demo-user'" />要点:
userName必填:派生createAiDb(userName)(IndexedDB 持久化会话 + 消息)——不传dbprop 即自动派生- SSE 端点:
sseUrl[MUST] 显式传入(OpenAI Chat Completions 协议;库不读宿主 env,未传时发送提示「未配置 SSE 端点」) AiDialogue自带:打字机流式 / markdown(含代码块复制)/ 思考折叠 / 中止 / 复制 / 历史记录加载 / 键盘避让- 业务自定义协议数据:传
sseConverter(转换器)把帧映射到AiMessagePart[](如自定义事件 / delta 扩展字段 / tool_calls → tool-call part)
③ 浮层形态:Teleport 悬浮面板
// AiDialogueWrap:fixed 右下角浮层(拉手开合 + 移动端全屏),AI 模块挂载点
<AiDialogueWrap v-model:show="show" density="auto">
<AiDialogueGroup :user-name="'demo-user'" />
</AiDialogueWrap>组件族索引
| 族 | 组件 | 文档 | | --- | --- | --- | | dialogue 对话族 | AiDialogue(全流程单会话)/ AiDialogueGroup(多会话+操作条)/ AiDialogueWrap(浮层)/ AiMessageList(受控展示) | README | | sender 输入族 | AiSender(受控输入原语)|内部件 AiSenderHeader | README | | config 配置族 | AiConfigProvider(主题 + expose 通道根) | README | | operation 操作族 | 内部件:AiOperationBar / AiOperationBtns | README | | bubble 气泡族 | 内部件:AiBubbleContent / AiBubbleFooter / AiCodeHeaderControl / AiContentSplit / AiThinkBox / AiTypewriter | README |
hooks 模块索引
全部经顶层 barrel 导出(import { useAiExposeInject } from "@done-coding/forge-agent"):
| hook | 职责 |
| --- | --- |
| useAiExposeProvide / useAiExposeInject | 对话 expose 通道(AiConfigProvider provide / 消费方 inject:sender/bubbleList/ask/sessionOperate/sessionExposeMap)。ask({ value?, parts? }) 两者至少给一个——纯 parts = 以用户名义发数据卡 |
| useSessionExpose / useSessionOperateMount | 单会话 expose 注册 / 会话级操作挂载 |
| useSenderScoped | 输入框全流程作用域(提交 → SSE → 气泡追加 → 中止),AiDialogue 内部消费 |
| useBubbleListScoped / useSessionListScoped / createNewSessionInfo | 气泡 / 会话列表状态机(内存 + IndexedDB 分页) |
| useAiModuleTheme | 主题变量挂值:--dc-agent-* 三级 fallback 链(var(--dc-core-*, var(--el-*, 默认))) |
| useCodeXSlot | XMarkdown 代码块头部控制(复制 + 折叠) |
| useFixBubbleListBug | BubbleList 滚动动画屏蔽(历史加载 / 首问) |
| useAiSSE + 协议层工具 | OpenAI 协议 SSE 对话流 + 传输层/转换器(parseOpenAIChunk / parseSSEEventLine / mergeToolCallsDeltas / defaultSSEConverter) |
详情见 hooks 引导。
主题与移动端
- 主题变量域:
--dc-agent-*(色 / 间距 / 字体 / 圆角 / 面板尺寸 / 安全区 token)。值 =var(--dc-core-*, var(--el-*, 默认))三级 fallback——宿主已接入 admin-core 主题时随主题风格 + 亮暗自适配,未接入时退 element-plus 色系。useAiModuleTheme(getIsDark)把默认值挂到:root body(AiConfigProvider已内置)。 - 移动端适配:
AiDialogueWrap内置useAgentUiDensity(scoped-rem 密度缩放 + 面板全屏形态)+useKeyboardInset(visualViewport 键盘避让);消费方唯一必做 = 入口 HTML 加viewport-fit=covermeta(详见 docs/mobile-adaptation.md)。
协议契约(C 阶段已落地,2026-08-02 拍板方案)
AiMessage 结构(对齐 AI SDK v5 状态机 + Anthropic content blocks 收敛集)
type AiMessageRole = "system" | "user" | "assistant";
interface AiMessage {
id: string;
role: AiMessageRole;
parts?: AiMessagePart[]; // parts 存在则以其为准(content 忽略)
content?: string; // shorthand = [{type:"text",text:content}],兼容 OpenAI
}
type AiMessagePart =
| AiTextPart // { type:"text", text }——markdown 渲染
| AiReasoningPart // { type:"reasoning", text }——thinking 痕迹
| AiToolPart // 状态机:input-streaming → input-available → output-available/error/denied
| AiFilePart // { type:"file", mediaType, filename?, url? }——图片/附件
| AiDataPart // { type:`data-${string}`, data: unknown }——业务卡片(现状保留)AiToolPart:{ type:"tool-call", id(增量合并锚), name, state, input?, output?, errorText? }——result 并入 output(v5 风格,不分离 tool-result);Anthropictool_result由转换器映射为 output-available 态AiDataPart保持data-${string}+ unknown(AI SDKdata-{typeName}同款;UI 兜底占位 +#part插槽业务自渲染)contentshorthand 保留(OpenAI content 兼容)
SSE 分两块(传输协议层 + 业务转换层)
- 传输协议层(通用 · 业务无关):OpenAI Chat Completions——POST body 构造(
{ model, messages, stream:true })+ SSE 帧解析(data: JSON+[DONE])+choices[0].delta.content累积 +finish_reason;delta.tool_calls增量解析类型预留(function call 只立结构,不做调用逻辑) - 业务转换层(业务方注入):转换器签名
(frame: 归一化帧) => AiMessagePart[]——逐帧增量调用支撑流式渲染;输入 = 解析后的协议帧对象(含标准字段 + 原始自定义字段);缺省恒等(content → AiTextPart)。业务数据通道:content 混 markdown / 自定义 SSE 事件 / delta 扩展字段 - 移除 Dify 事件序(workflow_started / node_started / text_chunk / node_finished / workflow_finished)
出站序列化(AiMessage[] → wire messages[])
与入站转换器成对的另一半:气泡历史 → AiMessage[](正文做首个 text part,data-* 卡片随后)
→ 序列化器 → body.messages[]。默认 toOpenAIMessages(公开纯函数)是有损映射:
| part | 默认映射 |
| --- | --- |
| text | 拼进 content |
| data-* | 序列化成 [<type>]\n<JSON> 文本段,并入同一条 content(多段以空行分隔) |
| reasoning | 丢弃(thinking 痕迹不回发,业界通例) |
| tool-call / file | 不映射(前者的回发由 agent 循环在本轮内自持 tool_calls + role:"tool";后者需多模态 content 数组) |
映射后 content 为空串的消息整条剔除。形状不合意就整条换掉——AiDialogue 的
sseMessagesSerializer prop 一次性拿到「历史 + 本轮」全部消息,返回什么就发什么;
aiIsDataPart / aiDataPartToWireText 随包导出,自写序列化器时当积木用。
[MUST NOT] 去改 toOpenAIMessages 的输出再回填。
文档约定
- 组件文档
README-<PascalName>.md位于各族目录docs/;内部件仅 API 段(不导出,无消费向叙事) - API 以
src/types/index.ts与组件源码为真相源;文档与其冲突时以源码为准 - 迭代惯例(与 core 同款):README API 模块先行 → 实现 → demo/单测/e2e → 非 API 文档
