weifuwu 0.78.0 → 0.80.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/README.md +12 -11
  2. package/dist/ai/client.d.ts +1 -1
  3. package/dist/ai/sse.d.ts +1 -1
  4. package/dist/ai/types.d.ts +2 -2
  5. package/dist/components/AlertGroup/AlertGroup.d.ts +1 -1
  6. package/dist/components/Anchor/Anchor.d.ts +1 -1
  7. package/dist/components/AuthPage/AuthPage.d.ts +36 -0
  8. package/dist/components/AutoComplete/AutoComplete.d.ts +3 -1
  9. package/dist/components/AvatarGroup/AvatarGroup.d.ts +1 -1
  10. package/dist/components/Calendar/Calendar.d.ts +1 -1
  11. package/dist/components/Carousel/Carousel.d.ts +1 -1
  12. package/dist/components/Cascader/Cascader.d.ts +1 -1
  13. package/dist/components/ChatInput/ChatInput.d.ts +61 -0
  14. package/dist/components/ColorPicker/ColorPicker.d.ts +1 -1
  15. package/dist/components/DatePicker/DatePicker.d.ts +2 -0
  16. package/dist/components/FloatButton/FloatButton.d.ts +1 -1
  17. package/dist/components/Grid/Grid.d.ts +1 -1
  18. package/dist/components/Input/Input.d.ts +2 -0
  19. package/dist/components/InputNumber/InputNumber.d.ts +1 -1
  20. package/dist/components/JSONViewer/JSONViewer.d.ts +1 -1
  21. package/dist/components/Link/Link.d.ts +1 -1
  22. package/dist/components/LogViewer/LogViewer.d.ts +1 -1
  23. package/dist/components/Markdown/parser.d.ts +1 -1
  24. package/dist/components/Mentions/Mentions.d.ts +1 -1
  25. package/dist/components/Menu/Menu.d.ts +1 -1
  26. package/dist/components/Menubar/Menubar.d.ts +1 -1
  27. package/dist/components/MessageBubble/MessageBubble.d.ts +1 -1
  28. package/dist/components/NavMenu/NavMenu.d.ts +1 -1
  29. package/dist/components/Popconfirm/Popconfirm.d.ts +1 -1
  30. package/dist/components/Result/Result.d.ts +1 -1
  31. package/dist/components/Scrollbar/Scrollbar.d.ts +1 -1
  32. package/dist/components/SearchInput/SearchInput.d.ts +1 -0
  33. package/dist/components/Slider/Slider.d.ts +1 -0
  34. package/dist/components/TagsInput/TagsInput.d.ts +1 -1
  35. package/dist/components/Timeline/Timeline.d.ts +1 -1
  36. package/dist/components/Transfer/Transfer.d.ts +1 -1
  37. package/dist/components/TreeSelect/TreeSelect.d.ts +2 -0
  38. package/dist/components/VirtualList/VirtualList.d.ts +3 -1
  39. package/dist/components/VirtualTable/VirtualTable.d.ts +1 -1
  40. package/dist/components/Watermark/Watermark.d.ts +1 -1
  41. package/dist/components/index.d.ts +4 -0
  42. package/dist/components/index.js +12 -12
  43. package/dist/components/style.css +77 -23
  44. package/dist/index.js +166 -149
  45. package/dist/ui-dom/ai.d.ts +1 -1
  46. package/dist/ui-dom/context.d.ts +32 -0
  47. package/dist/ui-dom/index.d.ts +9 -9
  48. package/dist/ui-dom/index.js +11 -11
  49. package/dist/ui-dom/jsx-runtime.js +1 -1
  50. package/dist/ui-dom/{vdom → middleware}/serve.d.ts +5 -8
  51. package/dist/ui-dom/testing.js +1 -1
  52. package/dist/ui-dom/use-chat.d.ts +1 -1
  53. package/dist/ui-dom/{vdom → vdom2}/audit.d.ts +5 -5
  54. package/dist/ui-dom/{vdom → vdom2}/build.d.ts +5 -5
  55. package/dist/ui-dom/vdom2/ctx.d.ts +32 -0
  56. package/dist/ui-dom/vdom2/hydrate.d.ts +13 -0
  57. package/dist/ui-dom/vdom2/index.d.ts +13 -0
  58. package/dist/ui-dom/vdom2/kind.d.ts +35 -0
  59. package/dist/ui-dom/vdom2/mount.d.ts +63 -0
  60. package/dist/ui-dom/vdom2/patch.d.ts +47 -0
  61. package/dist/ui-dom/vdom2/render.d.ts +19 -0
  62. package/dist/ui-dom/vdom2/ssr.d.ts +32 -0
  63. package/dist/ui-dom/vdom2/trace.d.ts +52 -0
  64. package/dist/ui-dom/{vdom → vdom2}/transform.d.ts +22 -1
  65. package/dist/ui-dom/vdom2/transitions.d.ts +23 -0
  66. package/dist/ui-dom/vdom2/x2html.d.ts +23 -0
  67. package/dist/ui-dom/vnode.d.ts +64 -67
  68. package/docs/ai-contract.md +418 -0
  69. package/docs/components-map.md +7 -5
  70. package/docs/components.md +6 -3
  71. package/docs/frontend-ui-dom.md +2 -2
  72. package/docs/frontend.md +2 -2
  73. package/docs/layout.md +1 -1
  74. package/docs/mobile.md +1 -1
  75. package/docs/saas.md +2 -2
  76. package/docs/style-guide.md +182 -0
  77. package/package.json +1 -1
  78. package/dist/ui-dom/vdom/diff.d.ts +0 -33
  79. package/dist/ui-dom/vdom/hydration.d.ts +0 -17
  80. package/dist/ui-dom/vdom/index.d.ts +0 -23
  81. package/dist/ui-dom/vdom/mount.d.ts +0 -58
  82. package/dist/ui-dom/vdom/render.d.ts +0 -17
  83. package/dist/ui-dom/vdom/ssr.d.ts +0 -44
  84. /package/dist/ui-dom/{vdom → vdom2}/registry.d.ts +0 -0
@@ -0,0 +1,23 @@
1
+ /**
2
+ * vdom2 x2html — vnode → HTML 字符串(SSR)
3
+ *
4
+ * 与 renderValue(客户端 DOM)同一**类型遍历**——TO_HTML[classifyKind(v)] 分派:
5
+ * 每个类型一个渲染实现,SSR/客户端结构同构(数组边界标记/占位注释/属性规则同一单一规则源
6
+ * transform.ts),hydration 不 mismatch。
7
+ *
8
+ * 与客户端的差异(SSR 语义):
9
+ * - 组件:现场执行工厂 + renderFn(无预构建——SSR 服务端每次渲染)
10
+ * - Portal:就地内联子节点(客户端 portal 渲染到 #__wf_portal——SSR 内联保留内容/SEO)
11
+ * - 事件 props 剥离(hydration 接线);ref 剥离
12
+ */
13
+ import type { VNodeChild } from '../vnode.ts';
14
+ import { isFrag, isComp, isPortal, isNative, Fragment, Portal } from '../vnode.ts';
15
+ export declare function escape(s: string): string;
16
+ type HtmlCtx = {
17
+ _fidPath?: string;
18
+ [key: string]: any;
19
+ };
20
+ /** vnode → HTML(SSR)——类型分派主入口 */
21
+ export declare function x2html(input: VNodeChild, ctx: HtmlCtx): Promise<string>;
22
+ export { Fragment, Portal };
23
+ export { isFrag, isComp, isPortal, isNative };
@@ -1,82 +1,78 @@
1
1
  /**
2
- * weifuwu/ui-dom VNode — 虚拟 DOM 节点
2
+ * vnode — VNode 强类型判别联合(vdom2 方案——vdom1 退役后唯一类型源)
3
3
  *
4
- * VNode 是纯 JS 对象,不依赖 DOM。组件返回 VNode。
4
+ * 与全局 vnode.ts(vdom1 JSX 运行时)的关系:
5
+ * - Fragment/Portal **symbol 复用全局**(全局唯一协议——JSX 编译产物用全局 Fragment,
6
+ * vdom2 引擎处理全局 h() 产物必须同 symbol 判别)
7
+ * - 类型(VNode/VNodeChild/Component)**独立**(vdom2 强类型;vdom1 替换后本文件成唯一类型源)
5
8
  *
6
- * h/jsx 由 esbuild JSX 编译调用:
7
- * --jsxImportSource=weifuwu/ui-dom
9
+ * 访问模式(强类型约束):
10
+ * if (isFrag(vnode)) { vnode._childNodes ... } // type === Fragment → TS 收窄为 FragVNode
11
+ * if (isComp(vnode)) { vnode._render ... } // type 为函数 → 收窄为 CompVNode
12
+ * —— 无散落 cast;字段访问由类型系统强制。
8
13
  */
9
- import type { WfuiContext } from './types.ts';
10
14
  export type VNodeType = string | Component<any, any> | typeof Fragment | typeof Portal;
11
- /**
12
- * VNode 子节点合法值——组件可返回/渲染的多态内容。
13
- * 递归联合:string/number/VNode/array/null/boolean 任意组合。
14
- */
15
15
  export type VNodeChild = VNode | string | number | boolean | null | undefined | VNodeChild[];
16
- export interface VNode {
16
+ export declare const Fragment: unique symbol;
17
+ export declare const Portal: unique symbol;
18
+ /** 通用字段(所有 VNode 共有——构建元数据;渲染前为 null 的显式初始化) */
19
+ interface VNodeBase {
17
20
  type: VNodeType;
18
21
  props: Record<string, any>;
19
- key?: string;
20
- el?: Node;
21
- /** 子 VNode 缓存(用于 patchValue diff,避免重复执行组件) */
22
- _child?: VNode | VNode[] | null;
23
- /** 远程 DOM 容器(Portal 等 remote VNode 的 DOM 所在处) */
24
- _remoteEl?: HTMLElement | undefined;
25
- /** VNode 的 DOM 归属:'local' 在父 DOM 树下,'remote' 在别处 */
26
- _placement?: 'local' | 'remote';
27
- /** 两阶段组件的 render 函数(mount 返回的函数——强制异步:props 变化时可 await 数据) */
28
- _render?: (props: Record<string, unknown>) => Promise<VNode | null>;
29
- /** 组件实例 ID(如 '_wf_0') */
30
- _id?: string;
31
- /** 自定义组件 ID(ctx.ui.selfId() 注册,跨组件精准刷新) */
32
- _customId?: string;
33
- /** 组件输出的 DOM 父节点 */
34
- _parentNode?: Node;
35
- /** 父 vnode 引用(动态挂载补全向上找持有组件) */
36
- _parentVNode?: VNode;
37
- /** 组件输出的第一个 DOM 节点 */
38
- _refNode?: Node | null;
39
- /** 阶段 B:children 每位置的首 DOM 节点(规则表 §5 锚点优先——替代 source[i] 下标猜测,
40
- * fragment/数组项多节点展开后相邻项不错位)。renderValue 记录,patchChildren 读取 + 回写 */
41
- _childAnchors?: (Node | null)[];
42
- /** Fragment 展开后的多个直属 DOM 节点范围(diff 对齐用,见 diff.ts) */
43
- _childNodes?: Node[];
44
- /** 组件 renderFn 上次执行时的 ctx 版本号(buildVNode 剪枝 + diff 三态 skip 的版本比较——
45
- * bumpCtxVersion 递增后版本不同 → 强制重跑 renderFn,如 i18n 切换语言) */
46
- _ctxVersion?: number;
22
+ /** key(数组项身份——无 key 为 null;显式 null 非可选 undefined) */
23
+ key: string | null;
24
+ /** 子 vnode 缓存(buildVNode 构建——patchValue diff 对照用;渲染前 null) */
25
+ _child: VNode | VNode[] | null;
26
+ /** 结构父节点(输出范围坐标系——范围定位不需要外部传 parent) */
27
+ _parentNode: Node | null;
28
+ /** 输出锚点 = 输出范围**首 DOM 节点**(多节点输出时必须是真实节点而非 DocumentFragment) */
29
+ _refNode: Node | null;
30
+ /** 组件实例 ID / 自定义 ID(buildVNode 分配) */
31
+ _id: string | null;
32
+ _customId: string | null;
33
+ /** 父 vnode 引用 */
34
+ _parentVNode: VNode | null;
35
+ /** 组件 renderFn 上次执行时的 ctx 版本号(buildVNode 剪枝 + diff 三态 skip) */
36
+ _ctxVersion: number | null;
47
37
  }
48
- /**
49
- * 两阶段异步组件(weifuwu 唯一组件形态):
50
- * async (initProps, ctx) => Promise<renderFn>
51
- * 外层 = mount(一次,可 await 数据),内层 = renderFn(每次 dirty/props 变化——**强制异步**,
52
- * 可 await 数据;统一异步心智:两阶段都可 await,无「同步组件 vs 异步组件」二元形态)。
53
- * P = props 类型(JSX 自动推断),C = 组件依赖的 ctx 注入(如 ApiInjected & RouteInjected)
54
- *
55
- * renderFn 签名:async (props) => Promise<VNode | null>——同步 renderFn 是类型错误
56
- * (diff 永不执行 renderFn——渲染器在 buildVNode 阶段 await,同步上下文拿不到 vnode)。
57
- * 渲染器按「返回值是 Promise」判别(主路径 buildVNode await 全部工厂 + renderFn)。
58
- */
59
- export type RenderFn<P> = (props: P) => Promise<VNode | null>;
60
- export type Component<P = {}, C extends object = {}> = (initProps: P, ctx: WfuiContext & C) => Promise<RenderFn<P> | null>;
61
- export declare const Fragment: unique symbol;
62
- /** Portal — 将子 VNode 渲染到 document.body 下的独立容器 */
63
- export declare const Portal: unique symbol;
64
- /** JSX 类型声明 — 使 TypeScript 理解自定义 JSX 运行时 */
38
+ /** 原生元素——type: string;特有:el(元素引用——锚点统一用 _refNode) */
39
+ export interface NativeVNode extends VNodeBase {
40
+ type: string;
41
+ el: Node | null;
42
+ }
43
+ /** Fragment——type: typeof Fragment(多节点输出边界 = fragment-start/end 标记——DOM 持久化) */
44
+ export interface FragVNode extends VNodeBase {
45
+ type: typeof Fragment;
46
+ }
47
+ /** 组件——type: Component;特有:_render(两阶段 renderFn)/ _outputChild(输出引用) */
48
+ export interface CompVNode extends VNodeBase {
49
+ type: Component;
50
+ _render: ((props: Record<string, unknown>) => Promise<VNode | null>) | null;
51
+ /** 输出 vnode 引用(dispose 清 _child/_id/_render 后仍可取输出范围——getOutputRange 递归终点) */
52
+ _outputChild: VNodeChild | null;
53
+ }
54
+ /** Portal——type: typeof Portal;特有:_remoteEl / _placement(固定 'remote') */
55
+ export interface PortalVNode extends VNodeBase {
56
+ type: typeof Portal;
57
+ _remoteEl: HTMLElement | null;
58
+ _placement: 'remote';
59
+ }
60
+ /** VNode 判别联合——type 为判别式 */
61
+ export type VNode = NativeVNode | FragVNode | CompVNode | PortalVNode;
62
+ export declare function isNative(v: unknown): v is NativeVNode;
63
+ export declare function isFrag(v: unknown): v is FragVNode;
64
+ export declare function isComp(v: unknown): v is CompVNode;
65
+ export declare function isPortal(v: unknown): v is PortalVNode;
66
+ export type Component<P = {}, C extends object = {}> = (initProps: P, ctx: import('./types.ts').WfuiContext & C) => Promise<((props: P) => Promise<VNode | null>) | null>;
67
+ /** 按类型构造强类型 VNode(每类初始化特有必填字段) */
68
+ export declare function createVNode(type: VNodeType, props: Record<string, any>, key?: string | null): VNode;
69
+ export declare function h(type: VNodeType, props: Record<string, any> | null, ...children: VNodeChild[]): VNode;
65
70
  export declare function jsx(type: VNodeType, props: Record<string, any> | null, key?: string | null): VNode;
66
71
  export declare const jsxs: typeof jsx;
67
72
  export declare function jsxDEV(type: VNodeType, props: Record<string, any> | null, key?: string | null): VNode;
68
- /** `h`(hyperscript)支持 variadic children: `h('div', {class:'x'}, child1, child2)` */
69
- export declare function h(type: VNodeType, props: Record<string, any> | null, ...children: VNodeChild[]): VNode;
70
- export declare function isNative(vnode: VNode): boolean;
71
- /** 递归文本/数组归一化(children 数组展开——嵌套数组扁平化,DOM 范围对齐)。
72
- * 栈展开(索引遍历替代 shift/unshift 头部操作——长数组 O(n) 而非 O(n²));逆序入栈 + pop 保持原顺序 */
73
- export declare function normalizeChildren(c: VNodeChild | undefined | null): VNodeChild[];
74
- export declare function isComponent(vnode: VNode): boolean;
75
- export declare function isFragment(vnode: VNode): boolean;
76
- export declare function isPortal(vnode: VNode): boolean;
77
- /** Portal VNode — 子节点渲染到 document.body#__wf_portal 中 */
73
+ export declare function arrayChildren(c: VNodeChild | undefined | null): VNodeChild[];
78
74
  export declare function createPortal(children: VNodeChild, portalKey?: string): VNode;
79
- /** JSX 类型声明 — ui-dom 是 jsxImportSource(client 壳不再声明) */
75
+ /** JSX 类型声明(jsxImportSource: weifuwu/ui-dom——组件/用户 JSX 编译产物类型) */
80
76
  declare global {
81
77
  namespace JSX {
82
78
  type Element = import('./vnode.ts').VNode | null;
@@ -85,7 +81,8 @@ declare global {
85
81
  [tag: string]: any;
86
82
  }
87
83
  interface IntrinsicAttributes {
88
- key?: string | number;
84
+ key?: string | number | null;
89
85
  }
90
86
  }
91
87
  }
88
+ export {};
@@ -0,0 +1,418 @@
1
+ # Weifuwu AI Stream Protocol (v1)
2
+
3
+ > **weifuwu 前后端之间的 LLM/agent 对话协议。** 定义"后端如何把一次对话/一次 agent run 流式地告诉前端,前端如何回传人工决策"。
4
+ >
5
+ > - **协议是 weifuwu 自己的**(`wf:` 命名空间),不依赖任何 provider 的 wire format——前端只见 `wf:` 事件,换模型/换提供商前端零改动。
6
+ > - **实现可换**:后端可以用自研 OpenAI 兼容客户端(`weifuwu/src/ai/`)、raw fetch 或任何库,只要输出 `wf:` 事件即可。协议不绑定实现。
7
+ > - **错误即值**:`wf:error` 是正常协议消息,不是断流异常(对齐自研 DB 客户端 RESP `-ERR` 精神)。
8
+ > - **前端参考实现**(`weifuwu/ui-dom`):`aiStream()` = 传输解码(POST + SSE 解析 + trace + abort);`ctx.ui.useChat()` = 会话语义层(消息累积、工具调用内嵌、HITL 审批、stop/retry,协议对页面透明)。
9
+ > - **版本**:本文档为 v1。非破坏性演进(新事件)直接追加;破坏性变更升版本号,两端随 weifuwu 单包原子发布同步升级。
10
+
11
+ ---
12
+
13
+ ## 1. 传输
14
+
15
+ ### 1.1 下行:SSE(`text/event-stream`)
16
+
17
+ - 一次 `POST` = 一次对话 / 一次 agent run
18
+ - 请求头:`Accept: text/event-stream`;认证与业务参数走 app 的既有中间件(auth / rateLimit 等)
19
+ - 响应头:`Content-Type: text/event-stream`、`Cache-Control: no-cache`、`Connection: keep-alive`
20
+ - 事件格式:`event: <名称>\ndata: <JSON>\n\n`
21
+
22
+ ```
23
+ event: wf:message_start
24
+ data: {"id":"9f3a"}
25
+
26
+ event: wf:token
27
+ data: {"text":"你好"}
28
+
29
+ event: wf:done
30
+ data: {"content":"你好,有什么可以帮你?","usage":{"prompt_tokens":512,"completion_tokens":384}}
31
+ ```
32
+
33
+ 调试:`curl -N -X POST <url> -d '{"messages":[...]}'` 直接看裸事件。
34
+
35
+ ### 1.2 上行:独立 POST
36
+
37
+ SSE 单向,人工决策回传走独立 POST(低频、可鉴权、可审计):
38
+
39
+ ```
40
+ POST /api/ai/approve
41
+ Content-Type: application/json
42
+
43
+ {"id":"ap_01","decision":"approved","note":"OK"}
44
+ // modified 决策:携带修改后参数(use-chat approve('modified', note, modifiedArgs) 自动带上)
45
+ // {"id":"ap_01","decision":"modified","note":"数量改为 5","modifiedArgs":{"qty":5}}
46
+ ```
47
+
48
+ - 回传载荷形状由协议定义(见 §4.5);**路由路径是 app 的**(app 知道自己的 run 生命周期)。
49
+ - 回传端点应挂 auth / rateLimit,审批记录带 `ctx.user` 进审计。
50
+
51
+ ### 1.3 生命周期
52
+
53
+ | 语义 | 规则 |
54
+ |---|---|
55
+ | 请求即会话 | 一次 POST + 一条 SSE 连接 = 一个会话;无连接池、无会话管理 |
56
+ | 断开 = abort | 客户端断开 → `req.signal` → 取消 provider 请求(省 token) |
57
+ | 无 token 超时 | 60s 内无任何事件 → `wf:error { code: 'timeout' }`,随后关闭连接 |
58
+ | 审批超时 | `wf:approval_request.expiresAt` 到期无人批 → 工具以 `tool_result { error: { code: 'timeout' } }` 结束(见 §4.5) |
59
+
60
+ ---
61
+
62
+ ## 2. 事件集总表
63
+
64
+ | 事件 | 层 | 方向 | 前端处理 | v1 状态 |
65
+ |---|---|---|---|---|
66
+ | `wf:message_start` | 核心 | 下行 | 创建会话/消息 | ✅ 实现 |
67
+ | `wf:token` | 核心 | 下行 | **append** 文本 | ✅ 实现 |
68
+ | `wf:usage` | 核心 | 下行 | 更新 token 计数 | ✅ 实现 |
69
+ | `wf:done` | 核心 | 下行 | 收尾(内容 + usage + 可选 reasoning) | ✅ 实现 |
70
+ | `wf:error` | 核心 | 下行 | 结构化降级(重试/提示) | ✅ 实现 |
71
+ | `wf:tool_call` | 工具 | 下行 | 渲染工具卡片 | ✅ 实现 |
72
+ | `wf:tool_result` | 工具 | 下行 | 卡片 → 结果态 | ✅ 实现 |
73
+ | `wf:tool_progress` | 工具 | 下行 | 卡片 → 进度态 | ✅ 实现 |
74
+ | `wf:step` | agent | 下行 | 步骤可视化 | ✅ 实现(agent 引擎) |
75
+ | `wf:approval_request` | agent | 下行 | 渲染审批卡片(待批态) | ✅ 实现(agent 引擎) |
76
+ | `wf:approval_response` | agent | **上行 POST** | 用户决策回传 | ✅ 实现(ctx.ai.approve) |
77
+ | `x:*` | 自定义 | 双向 | **透传不解释** | ✅ 规则生效 |
78
+
79
+ **层规则**:只要 chat 的 app 永远不接触工具/agent 事件;前端解码器按层订阅,未订阅的事件跳过不报错。
80
+
81
+ ---
82
+
83
+ ## 3. 核心事件(chat 必需)
84
+
85
+ ### 3.1 `wf:message_start`
86
+
87
+ ```jsonc
88
+ { "id": "9f3a" } // 会话/消息 id
89
+ ```
90
+
91
+ - 一条 SSE 流的第一个事件。
92
+ - **`id` 应取 `X-Trace-Id` 请求头**(见 §7 追踪关联),无则后端生成。
93
+ - 前端以 `id` 关联后续所有事件。
94
+
95
+ ### 3.2 `wf:token`
96
+
97
+ ```jsonc
98
+ { "text": "你好" } // 增量文本,直接 append
99
+ ```
100
+
101
+ - **纯增量**:前端把 `text` 追加到当前消息尾部,不做 diff/合并。
102
+ - 一个 provider chunk 的 content delta → 一个 `wf:token`(后端不做聚合)。
103
+
104
+ ### 3.3 `wf:usage`
105
+
106
+ ```jsonc
107
+ { "prompt_tokens": 512, "completion_tokens": 384 }
108
+ ```
109
+
110
+ - provider 返回 usage 时即发(可能出现在流中最后一 chunk,或聚合后一次发)。
111
+ - 前端只更新计数,不改变消息内容。
112
+
113
+ ### 3.4 `wf:done`
114
+
115
+ ```jsonc
116
+ {
117
+ "content": "你好,有什么可以帮你?",
118
+ "usage": { "prompt_tokens": 512, "completion_tokens": 384 },
119
+ "reasoning": "先分析用户意图:……" // 可选:thinking 模式推理过程(reasoning_content)
120
+ }
121
+ ```
122
+
123
+ - 正常收尾事件:完整内容 + 最终 usage。
124
+ - **`reasoning`(可选,additive)**:thinking 模式(DeepSeek 等)的推理过程,
125
+ 收尾时一次性下发——**v1 不进流式**(`wf:token` 只承载正文增量);
126
+ 前端以 ReasoningBlock 折叠展示,下一轮回传(`toChatMessages` 带 `reasoning_content`)。
127
+ - 前端标记会话完成(停止打字指示、启用输入框)。
128
+
129
+ ### 3.5 `wf:error`
130
+
131
+ ```jsonc
132
+ { "code": "rate_limited", "message": "请求过于频繁,请稍后再试" }
133
+ ```
134
+
135
+ - **正常协议消息,连接保持**(除非 code 为致命错误如 `auth_failed`,后端可随后关闭)。
136
+ - 前端按 `code` 分类降级:展示错误、给重试按钮、允许继续会话。
137
+
138
+ **错误码表(v1 定稿)**:
139
+
140
+ | code | 含义 | 前端建议 |
141
+ |---|---|---|
142
+ | `auth_failed` | API key 无效/未配置 | 引导配置 |
143
+ | `rate_limited` | provider 限流 | 显示 + 延迟重试 |
144
+ | `context_length` | 上下文超长 | 提示截断/新会话 |
145
+ | `timeout` | 无 token 超时 / 审批超时 | 提示重试 |
146
+ | `provider_error` | provider 返回错误(详情在 message) | 显示 message |
147
+ | `invalid_request` | 请求参数错误 | 修复请求 |
148
+ | `unsupported` | 能力不支持(诚实裁剪,CS-05) | 提示不可用 |
149
+ | `aborted` | 服务端侧主动取消 | 静默 |
150
+
151
+ ---
152
+
153
+ ## 4. 工具事件(chat + tools)
154
+
155
+ ### 4.1 `wf:tool_call`
156
+
157
+ ```jsonc
158
+ {
159
+ "id": "tc_01", // 工具调用 id(provider 给 / 后端生成)
160
+ "name": "query_orders", // 工具名(app 定义的业务语义,协议不解释)
161
+ "args": { "userId": "u1" } // 完整参数
162
+ }
163
+ ```
164
+
165
+ - **后端聚合完成后才发出**:provider 流式 chunk 中 `tool_calls` 的 id 可能只在首个 chunk(DeepSeek 如此),后端负责聚合出完整的 `{ id, name, args }` 再发。前端不接触增量。
166
+ - 并行工具调用 = 连续多条 `wf:tool_call`(各带独立 id),前端可渲染多张卡片。
167
+ - 前端按 `name` 分发渲染(`SearchCard` / `ProgressBar` / …),工具名是 app 的扩展面。
168
+
169
+ ### 4.2 `wf:tool_result`
170
+
171
+ ```jsonc
172
+ { "id": "tc_01", "ok": true, "output": { "rows": 3 } }
173
+ ```
174
+
175
+ 失败形态:
176
+
177
+ ```jsonc
178
+ { "id": "tc_01", "ok": false, "error": { "code": "rejected", "message": "预算不够" } }
179
+ ```
180
+
181
+ - `error.code`:`rejected`(人工拒绝)/ `timeout`(审批超时)/ `tool_error`(执行异常)/ app 自定义。
182
+ - **`ok: false` 不代表对话结束**——agent 读到 result 后可换方案重试(HITL 核心语义)。
183
+
184
+ ### 4.3 `wf:tool_progress`
185
+
186
+ ```jsonc
187
+ {
188
+ "toolCallId": "tc_01",
189
+ "step": 2,
190
+ "total": 5,
191
+ "message": "生成第 2 页",
192
+ "status": "running" // running | error | done
193
+ }
194
+ ```
195
+
196
+ - 长任务(PPT 生成、委派子 agent、深度搜索)执行期间的进度汇报,前端更新卡片进度条。
197
+ - 秒级任务用此事件;分钟级任务应入队(`ctx.queue`)+ 独立进度通道,见 §9。
198
+
199
+ ### 4.4 工具执行模型
200
+
201
+ ```ts
202
+ // 工具 = 普通对象;run 收到 emit(汇报进度/自定义事件)与 signal(取消)
203
+ tools: [{
204
+ name: 'generate_ppt',
205
+ run: async (args, { emit, signal }) => {
206
+ emit('wf:tool_progress', { step: 1, total: 5, message: '生成大纲' })
207
+ emit('x:ppt_page_done', { page: 2 }) // 自定义事件
208
+ return { fileId: 'ppt_01' } // → wf:tool_result
209
+ }
210
+ }]
211
+ ```
212
+
213
+ - `emit` 是工具的执行声道:可发 `wf:tool_progress` 与任意 `x:*` 事件。
214
+ - `signal`:用户取消 → abort → 中断长任务(与 §1.3 生命周期闭环)。
215
+ - 工具名/args 语义是 app 的,协议只定义"调用怎么流动"。
216
+
217
+ ### 4.5 人工审批(HITL,agent 扩展 schema)
218
+
219
+ **下行** `wf:approval_request`:
220
+
221
+ ```jsonc
222
+ {
223
+ "id": "ap_01",
224
+ "toolCallId": "tc_02",
225
+ "name": "send_email",
226
+ "args": { "to": "boss@x.com", "subject": "方案" },
227
+ "reason": "发送前需要确认收件人",
228
+ "expiresAt": 1735689600000 // 审批超时
229
+ }
230
+ ```
231
+
232
+ 发出后**后端挂起该工具执行**,等待上行回传。
233
+
234
+ **上行** `POST`(`wf:approval_response` 载荷):
235
+
236
+ ```jsonc
237
+ {
238
+ "id": "ap_01",
239
+ "decision": "approved", // approved | rejected | modified
240
+ "modifiedArgs": { "to": "me@x.com" }, // 仅 modified
241
+ "note": "不要群发,只发给我"
242
+ }
243
+ ```
244
+
245
+ **决策语义**:
246
+
247
+ | decision | 工具行为 | 后续事件 |
248
+ |---|---|---|
249
+ | `approved` | 按原 args 执行 | `wf:tool_result` |
250
+ | `modified` | 按 `modifiedArgs` 执行 | `wf:tool_result` |
251
+ | `rejected` | 不执行 | `wf:tool_result { ok:false, error:{ code:'rejected', message: note } }` → **agent 读 reason 换方案** |
252
+
253
+ - 审批请求带独立 `id`:并发多卡片各自挂起/恢复;id 一次性(防重放)。
254
+ - 回传端点挂 auth + rateLimit,审批人 `ctx.user` 进审计(app 责任,协议给 id 语义)。
255
+ - 超时:`expiresAt` 到期 → 按 rejected 处理(`error.code: 'timeout'`)。
256
+
257
+ ---
258
+
259
+ ## 5. agent 扩展事件(已实现:src/ai/agent.ts)
260
+
261
+ ### 5.1 `wf:step`
262
+
263
+ ```jsonc
264
+ { "type": "llm", "content": "正在查询订单…" }
265
+ { "type": "tool", "toolCallId": "tc_01", "name": "query_orders" }
266
+ ```
267
+
268
+ - 供前端做步骤可视化(思考中/工具执行中/完成)。
269
+ - 由 agent 引擎(`a.agent()`)在每个 LLM 轮次前与每个工具执行前发出。
270
+
271
+ ### 5.2 agent 引擎(`a.agent({ systemPrompt, tools, humanInTheLoop })`)
272
+
273
+ 工具循环:LLM 流式(emit `wf:token`)→ tool_calls → 执行工具 → 结果回喂 → 重复,直到无工具调用或 maxSteps 耗尽。
274
+
275
+ - 事件序列:`message_start → (step:llm → token* → tool_call → step:tool → [approval_request → approve] → tool_result)* → usage → done`
276
+ - **工具执行上下文**:`run(args, { emit, signal })`——emit `wf:tool_progress` / `x:*` 自定义事件;signal 接收用户取消
277
+ - **HITL 审批**:`humanInTheLoop` 时每个工具执行前挂起等 `ctx.ai.approve()` 响应(见 §4.5)
278
+ - **多轮消息纪律**(真实 DeepSeek 验证):带 tool_calls 的 assistant 消息必须入上下文;thinking 模式 `reasoning_content` 必须回传(陷阱清单 #4)
279
+
280
+ ### 5.3 子 agent = 工具
281
+
282
+ 多 agent 沟通不新增协议事件:子 agent 通过 `delegate` 工具承载(工具 run 内调 `a.chat()` 或另一个 agent 的循环),其最终输出 = 该工具的 `tool_result`。编排逻辑(委派给谁、何时、如何聚合)是 app 在工具 handler 里的业务。
283
+
284
+ ---
285
+
286
+ ## 6. 扩展机制(协议不是紧身衣)
287
+
288
+ | 机制 | 规则 |
289
+ |---|---|
290
+ | `x:` 命名空间 | `event: x:any_name`,解码器**透传不解释、不校验、不转换**,前端经 `onEvent` 自处理 |
291
+ | 未知字段透传 | `data` 中未知字段必须原样保留(解码器不得丢弃)——新旧客户端双向兼容 |
292
+ | 未知事件 | 前端**不得抛错**:未订阅的事件跳过(老前端接新后端安全) |
293
+ | 晋升路径 | 2+ app 收敛的 `x:` 事件 → 晋升为 `wf:` 事件 + 协议版本小升 + 前端原语跟进,单包原子发布 |
294
+
295
+ **规则一句话:`wf:` 是框架的、版本化的、有前端原语覆盖的;其余全是 app 的。**
296
+
297
+ ---
298
+
299
+ ## 7. 追踪关联(trace)
300
+
301
+ - 前端流式请求应携带 `X-Trace-Id` 请求头(ctx.api 同源生成,一行钩子)。
302
+ - 后端以 `X-Trace-Id` 作为 `wf:message_start.id`(serve.ts 已内置 traceId 机制,接受 `x-trace-id` / `traceparent`,兜底 `randomUUID`,响应头回显)。
303
+ - **工具内发起的后端请求继承同一 traceId** → 整个 agent run(对话 + provider 调用 + 工具内请求)挂在同一 id 下,日志一次搜完。
304
+
305
+ ```
306
+ 用户输入 → POST /api/chat (X-Trace-Id: 9f3a)
307
+ → wf:message_start { id: "9f3a" }
308
+ → provider 调用(日志带 9f3a)
309
+ → wf:tool_call query_orders
310
+ → GET /api/orders(继承 X-Trace-Id: 9f3a)
311
+ → wf:done
312
+ → 日志搜 "9f3a" = 整个会话全链路
313
+ ```
314
+
315
+ ---
316
+
317
+ ## 8. 共享类型(规范型,`src/ai/types.ts` 据此实现)
318
+
319
+ ```ts
320
+ // ── 事件 ──────────────────────────────────────────────
321
+
322
+ export interface WfMessageStart { id: string }
323
+ export interface WfToken { text: string }
324
+ export interface WfUsage { prompt_tokens: number; completion_tokens: number; total_tokens?: number }
325
+ export interface WfDone { content: string; usage?: WfUsage; reasoning?: string }
326
+
327
+ export type WfErrorCode =
328
+ | 'auth_failed' | 'rate_limited' | 'context_length' | 'timeout'
329
+ | 'provider_error' | 'invalid_request' | 'unsupported' | 'aborted'
330
+
331
+ export interface WfError { code: WfErrorCode; message: string }
332
+
333
+ export interface WfToolCall { id: string; name: string; args: Record<string, unknown> }
334
+ export interface WfToolResult {
335
+ id: string
336
+ ok: boolean
337
+ output?: unknown
338
+ error?: { code: string; message: string } // rejected | timeout | tool_error | app 自定义
339
+ }
340
+ export interface WfToolProgress {
341
+ toolCallId: string
342
+ step: number
343
+ total: number
344
+ message?: string
345
+ status: 'running' | 'error' | 'done'
346
+ }
347
+
348
+ export interface WfStep { type: 'llm' | 'tool'; content?: string; toolCallId?: string; name?: string }
349
+ export interface WfApprovalRequest {
350
+ id: string
351
+ toolCallId: string
352
+ name: string
353
+ args: Record<string, unknown>
354
+ reason?: string
355
+ expiresAt?: number
356
+ }
357
+ export type WfApprovalDecision = 'approved' | 'rejected' | 'modified'
358
+ export interface WfApprovalResponse {
359
+ id: string
360
+ decision: WfApprovalDecision
361
+ modifiedArgs?: Record<string, unknown>
362
+ note?: string
363
+ }
364
+
365
+ /** 所有框架事件的联合类型(前端 switch 收窄用) */
366
+ export type WfStreamEvent =
367
+ | { name: 'wf:message_start'; data: WfMessageStart }
368
+ | { name: 'wf:token'; data: WfToken }
369
+ | { name: 'wf:usage'; data: WfUsage }
370
+ | { name: 'wf:done'; data: WfDone }
371
+ | { name: 'wf:error'; data: WfError }
372
+ | { name: 'wf:tool_call'; data: WfToolCall }
373
+ | { name: 'wf:tool_result'; data: WfToolResult }
374
+ | { name: 'wf:tool_progress'; data: WfToolProgress }
375
+ | { name: 'wf:step'; data: WfStep }
376
+ | { name: 'wf:approval_request'; data: WfApprovalRequest }
377
+
378
+ // ── 对话消息(回传 provider 的形状,与 wf: 事件无关)────
379
+
380
+ export type MessageRole = 'system' | 'user' | 'assistant' | 'tool'
381
+ export interface ChatMessage {
382
+ role: MessageRole
383
+ content: string
384
+ /** DeepSeek thinking mode:前一轮的 reasoning_content 必须回传 */
385
+ reasoning_content?: string
386
+ tool_call_id?: string
387
+ tool_calls?: ToolCall[]
388
+ name?: string
389
+ }
390
+ export interface ToolCall { id: string; type: 'function'; function: { name: string; arguments: string } }
391
+ ```
392
+
393
+ ---
394
+
395
+ ## 9. 诚实裁剪与边界
396
+
397
+ | 范围 | 状态 |
398
+ |---|---|
399
+ | chat / stream / tools / progress / error 模型 | ✅ v1 实现 |
400
+ | `x:*` 透传、未知事件/字段兼容 | ✅ v1 规则生效 |
401
+ | agent 引擎(`a.agent()`)、`wf:step`、审批事件 | ✅ 已实现 |
402
+ | embeddings | ❌ 不做(DeepSeek 无此 API) |
403
+ | Anthropic/OpenAI 原生协议 | ❌ 不做(OpenAI 兼容已覆盖,baseUrl 可换) |
404
+ | 多 agent 编排 | ❌ 不承诺(子 agent = 工具已覆盖) |
405
+ | 审批持久化 | ❌ 不做(连接断 = 会话亡;持久化等 agent 长任务化 + queue) |
406
+ | 分钟级长任务 | ⚠️ 工具入队即返回(`ctx.queue`),进度走独立通道(WS/SSE 订阅)——app 编排 |
407
+ | 前端通用 HTTP 追踪/时间线面板 | ⏸ 信号(浏览器 DevTools 已覆盖前端部分) |
408
+
409
+ ## 10. 陷阱清单(来自真实实现)
410
+
411
+ 1. **partial chunk / UTF-8 边界**:SSE 解析必须 buffer + `TextDecoder(stream: true)`,`\n` 切行,末段保留到下次。
412
+ 2. **`[DONE]` 终止符**:读到即结束,不当作 JSON 解析。
413
+ 3. **tool_calls id 只在首个 chunk**(DeepSeek):后端必须聚合完整 tool_call 再发 `wf:tool_call`;无 id 的后续 chunk 追加到最后一个。
414
+ 4. **reasoning_content 必须回传**(thinking 模式):`ChatMessage.reasoning_content` 随消息往返,否则推理断档。
415
+ 5. **SSE 注释行 `:` 与空行**:跳过。
416
+ 6. **非 JSON 行**:忽略不抛错。
417
+ 7. **usage 可能只在最后一 chunk**(DeepSeek):聚合后发 `wf:usage`,`wf:done` 再带最终值。
418
+ 8. **审批回传 id 一次性**:防重放,用后即焚。
@@ -111,6 +111,8 @@
111
111
  | 组件 | 说明 |
112
112
  |------|------|
113
113
  | `<AiChat>` | 完整 AI 对话界面(流式 token/工具卡/审批卡/自动滚动),配 `ctx.ui.useChat()`;移动端 `raiseOnKeyboard` |
114
+ | `<ChatInput>` | 独立聊天输入条(AiChat 抽取):单行/多行 + streaming 停止 + IME 安全——纯输入层(useChat 组合在消费方) |
115
+ | `<AuthPage>` | 认证页骨架:居中卡片 + logo + 表单插槽 + 错误条 + 提交 loading(登录/注册复用) |
114
116
  | `<Markdown content>` | 零依赖安全子集 parser(无 raw HTML 注入,VNode 渲染天然转义) |
115
117
  | `<CodeBlock code lang>` | 代码块 + 语言标签 + 复制按钮 |
116
118
  | `<MessageBubble role status>` | 聊天气泡(独立复用,AiChat 抽取) |
@@ -129,7 +131,7 @@
129
131
  ## 第九批:AI 差异化(109 → 111 组件,2026-08)
130
132
 
131
133
  > 三库业务组件覆盖 ~100% 后的差异化期——补 AI **输入层 + 推理展示层**。
132
- > 完整设计/裁剪见 `design/ai-differentiation-plan.md`。
134
+ > 完整设计/裁剪见组件裁剪登记(内部文档,仓库内可查)。
133
135
 
134
136
  | 组件 | 类型 | 三库等价 | 要点 |
135
137
  |------|------|---------|------|
@@ -137,7 +139,7 @@
137
139
  | **ReasoningBlock** | 新增 | 三库无 | CoT 推理折叠展示:aria-expanded + 键盘可达 + 流式脉冲 |
138
140
  | **AiChat 集成** | 增强 | — | `WfDone.reasoning`(additive,收尾一次性下发)+ use-chat 聚合 + toChatMessages 回传 `reasoning_content`(thinking 模式闭环) |
139
141
 
140
- > 协议变更:`wf:done` 新增可选 `reasoning` 字段(向后兼容),见 `design/ai-contract.md` §3.4。
142
+ > 协议变更:`wf:done` 新增可选 `reasoning` 字段(向后兼容),见 `docs/ai-contract.md` §3.4。
141
143
 
142
144
  ## 快速迁移路径(三库 → weifuwu)
143
145
 
@@ -171,7 +173,7 @@
171
173
  | **LogViewer** | 新增 | ANSI 着色 + 虚拟滚动 + follow 自动跟随 + maxLines |
172
174
  | **JSONViewer** | 新增 | 递归折叠 + 类型色 + 路径复制 + 懒展开(ToolCallCard 已接入) |
173
175
 
174
- > 三库共识覆盖度 ~100%(剩余均为已声明裁剪项,见 `design/components-roadmap.md` 第六批裁剪清单)。
176
+ > 三库共识覆盖度 ~100%(剩余均为已声明裁剪项,见组件裁剪登记)。
175
177
 
176
178
  ## 第七批:AI 开发者工具深化(96 → 102 组件)
177
179
 
@@ -188,7 +190,7 @@
188
190
 
189
191
  > AI 开发者工具链成型:AiChat / Command / JSONViewer / LogViewer / DiffView /
190
192
  > Pipeline——三库差异化最深的完整工具线。
191
- > 裁剪声明见 `design/components-roadmap.md` 第七批。
193
+ > 裁剪声明见组件裁剪登记(内部文档)。
192
194
 
193
195
  ## 第八批:三库并集缺口清零(102 → 113,2026-08)
194
196
 
@@ -199,7 +201,7 @@
199
201
  |------|------|---------|------|
200
202
  | **Layout**(+LayoutHeader/Sider/Content/Footer) | 新增 | antd Layout / EP Container / shadcn Sidebar | 复合子组件双模式;含 Sider → row 布局;Sider 折叠受控/非受控 + trigger |
201
203
  | **Popconfirm** | 新增 | antd / EP Popconfirm | 复用 usePopup 基座(弹层组合性);danger 危险色;确认后自动关闭 |
202
- | **AutoComplete** | 新增 | antd+EP Autocomplete / shadcn Combobox | 自由输入联想;包含过滤(纯函数);键盘 ↓↑/Enter/Escape;选中回填 |
204
+ | **AutoComplete** | 新增 | antd+EP Autocomplete / shadcn Combobox | 自由输入联想;包含过滤(纯函数);键盘 ↓↑/Enter/Escape;选中回填;error 错误态 |
203
205
  | **Link** | 新增 | EP Link / antd Typography.Link | 语义色/下划线/disabled/new window/icon |
204
206
  | **FloatButton** | 新增 | antd FloatButton(特有) | fixed 定位 + badge + 组展开状态机 |
205
207
  | **NavMenu** | 新增 | shadcn NavigationMenu(特有) | 顶部多级 hover 弹出 + 键盘 →/Escape |