@pactor-app/chat 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pactor Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,228 @@
1
+ # @pactor-app/chat
2
+
3
+ [Pactor](https://github.com/426-330/pactor) 的 Chat 包:AI Message Rendering Protocol(消息模型 / Streaming reducer / Safe Registry / capability 动作)与 Chat Runtime 视图(`<ChatView>` / `<MessageBubble>` / `<PartRenderer>` / `<Composer>` / chat 布局)。核心是"协议 + Runtime"而非单个组件;model / store / events / reducer / safe-registry / capability 零 React,可纯 Node 测试。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ npm install @pactor-app/chat @pactor-app/runtime @pactor-app/markdown react
9
+ # 或
10
+ pnpm add @pactor-app/chat @pactor-app/runtime @pactor-app/markdown react
11
+ ```
12
+
13
+ `react >= 18` 为 peer dependency。
14
+
15
+ 视图组件(ChatView / Chat / ChatShell 等)样式自持,使用前引入样式表并给宿主容器显式定高(组件填满父容器,`height: 100%`):
16
+
17
+ ```ts
18
+ import '@pactor-app/chat/styles.css';
19
+ ```
20
+
21
+ ## 示例
22
+
23
+ ```tsx
24
+ import { createShadcnComponentRegistry } from '@pactor-app/ui/components';
25
+ import {
26
+ ChatView,
27
+ ConversationStore,
28
+ createManualDriver,
29
+ createSafeRegistry,
30
+ } from '@pactor-app/chat';
31
+
32
+ const store = new ConversationStore();
33
+ // Safe Registry:LLM 生成的 DSL 只渲染白名单内的组件
34
+ const safeRegistry = createSafeRegistry(createShadcnComponentRegistry(), [
35
+ 'Statistic',
36
+ 'Card',
37
+ ]);
38
+
39
+ // 真实场景:transport 由业务按服务端协议实现(SSE / WebSocket);
40
+ // 这里用手动 driver 演示
41
+ const driver = createManualDriver();
42
+
43
+ <ChatView
44
+ store={store}
45
+ transport={driver.transport}
46
+ safeRegistry={safeRegistry}
47
+ conversationId="conv-1"
48
+ />;
49
+
50
+ // 服务端事件流(节选):
51
+ driver.push({ type: 'message.start', messageId: 'm1' });
52
+ driver.push({ type: 'thinking.delta', messageId: 'm1', delta: '先查数据…' });
53
+ driver.push({ type: 'text.delta', messageId: 'm1', delta: '根据当前数据' });
54
+ driver.push({
55
+ type: 'dsl.complete',
56
+ messageId: 'm1',
57
+ schema: { type: 'Statistic', props: { title: '总数', value: 42 } },
58
+ });
59
+ driver.push({ type: 'message.end', messageId: 'm1' });
60
+ driver.complete();
61
+ ```
62
+
63
+ ## API 一览
64
+
65
+ - **`ConversationStore`**(零 React):`appendMessage` / `updateMessage(id, patch)` / `appendPart(messageId, part)` / `updatePart(messageId, partRef, patch)`(partRef 为下标或 part id)/ `setStatus` / `removeMessagesFrom(messageId)`(不可变截断该消息及之后全部消息,编辑重发/重新生成的基础)/ `clear` / `subscribe(listener)`。全部更新不可变(新数组引用),兼容 `useSyncExternalStore`。`ChatMessage` 支持 `attachments?: ChatAttachment[]`(`id`/`name`/`mimeType`/`size?`/`url?`,上传与存储由调用方负责)。
66
+ - **`applyChatEvent(store, event)`**(零 React):Streaming 纯 reducer。事件:`message.start` / `text.delta`(`format: 'markdown'` 建 markdown part)/ `thinking.delta` / `tool.start`(可携带 `args`,供活动轨迹参数摘要)/ `tool.result` / `dsl.start` / `dsl.complete` / `message.end` / `message.error`。对乱序宽容(delta 先于 start 自动建占位)、未知事件忽略并告警;`dsl.patch` V1 不实现。
67
+ - **`ChatTransport` / `runChatTurn(store, transport, input)`**:`send({ text, conversationId? }) → AsyncIterable<ChatEvent>`;turn 先 append user 消息 + assistant 占位再逐事件 reduce,transport 抛错收敛为 `message.error`。`createManualDriver()` 提供代码控制推送事件的 transport(`push` / `complete` / `fail`),测试与手动驱动共用。
68
+ - **`runRegenerateTurn(store, transport, options?)`**(零 React):定位末条 user 消息 → `removeMessagesFrom` 截断其后消息 → 以同文本重跑 turn(`options.skipUserMessage`,不重复追加 user 消息);支持 `signal` 取消;无 user 消息时告警返回。消息操作条的"重新生成 / 错误重试"即接入此函数。
69
+ - **`detectStreamingMode(message)` / `pairToolTraces(parts)`**(零 React 纯函数):活动轨迹的推导层——前者返回 `reasoning | tool | responding | null`;后者把 tool-call 与 tool-result 按 name + 出现顺序配对为 `ToolTraceEntry`(名称 / 参数 / 状态 / part 下标)。
70
+ - **`createSafeRegistry(source, allowed)`**:白名单过滤的组件注册表(get / has / list 取 `allowed ∩ source`,实时代理)。LLM 生成的 DSL 必须经 Safe Registry 渲染;白名单外节点渲染受控"不允许"占位(`guardChatDsl` + `ChatDenied`)。
71
+ - **`registerCapabilityAction(actions, { capabilities, permission?, confirm? })`**:注册 `capability` 动作,执行链 registry → permission → confirm(`risk: 'high'` 且注入 confirm 时;取消返回 `{ ok: true, skipped: true }`,未注入 confirm 受控拒绝)→ execute。DSL 侧:`{ action: 'capability', params: { name, params } }`。
72
+ - **`createChatRuntime({ safeRegistry, capabilities?, permission?, permissionStore?, confirm?, user? })`**:组装 Chat DSL 渲染运行时(Safe Registry + 内置动作 + capability 动作 + 权限适配器);`buildChatScope({ conversation, message, params })` 组装 chat 作用域(`user` 经 RuntimeContext 注入)。
73
+ - **`<ChatView store transport? onSend? safeRegistry? capabilities? permission? confirm? conversationId? user? params? greeting? subtitle? suggestions?>`**:消息列表 + Composer。发送时 `onSend` 回调先行,注入 `transport` 时同时驱动一轮 turn。空会话渲染 `<EmptyState>`:问候语(`--pactor-chat-font-display` 衬线展示字体)+ 副标题 + `suggestions` 起始建议(字符串或 `{ label, prompt? }`,点击即发送,空数组不渲染建议区)。Composer 扩展 props(`attachments` / `onAttachmentsChange` / `onAttachFiles` / `onAttachmentClick` / `toolbarStart` / `toolbarEnd` / `onMentionTrigger` / `onPlusMenu`)原样透传给内建 Composer,语义见下方 `<Composer>`。
74
+ - **`<MessageBubble>` / `<PartRenderer>`**:按 role 区分气泡;parts 按类型渲染(text / markdown / dsl / thinking 折叠卡:流式展开 + 进行中指示、答案阶段自动折叠、支持 pin / error 附重试入口 / status);tool-call / tool-result 移交 `<ActivityTrace>` 配对渲染。
75
+ - **`<ActivityTrace>`**:流式状态头(动画图标 + 呼吸文案 + 秒级计时,结束定格)+ 配对工具轨迹条目(单行:名称 + 参数摘要 + 状态图标,展开显示参数与结果/错误);流式展开 → 答案阶段自动折叠 → 用户 pin 后不再自动变更(按消息记忆)。
76
+ - **`<MessageActions>`**:消息操作条(hover 显现),数据驱动渲染——`actions` 缺省为 `['copy', 'regenerate', 'retry', 'edit']`:复制(`collectMessageText` 汇总文本写入剪贴板)、末条 completed assistant 重新生成、user 消息气泡内联编辑重发(确认 → 截断 + 重跑,取消不改 store)、error 消息重试。传入数组时严格按数组渲染(数组即真相),内建动作可按字段覆盖 label / icon / roles / visibleWhen,自定义动作经 `callService(service, { actionId, message })` 派发;图标名走 `MESSAGE_ACTION_ICONS` 白名单(未知名渲染占位图标)。详见下文「消息操作条:messageActions」。
77
+ - **`<Composer>`**:自适应高度 textarea;IME 合成守卫(合成中 Enter 不发送);发送按钮状态机(disabled / ready / streaming 停止,同元素切换 + 纯 CSS 光环);受控附件 `attachments` + `onAttachmentsChange`(图片缩略图/文档卡片 + 移除),拖拽悬停态、粘贴与文件选择产出 `File[]` 经 `onAttachFiles` 交给调用方(包内不上传),点击卡片经 `onAttachmentClick` 通知;`toolbarStart` / `toolbarEnd` 渲染槽;词边界 `@` 触发 `onMentionTrigger(query, anchorRect)`,"+" 按钮触发 `onPlusMenu(anchorRect)`(未传回调不渲染入口,弹层由调用方实现)。
78
+ - **`<AppShell sidebar brand? locationKey? className?>`**:应用外壳——侧栏插槽 + 主区。桌面(≥768px)侧栏与主区 flex 同行;移动端(<768px)侧栏退出文档流变为覆盖抽屉(遮罩 + 顶栏菜单按钮 + `brand` 品牌区),断点由 CSS 媒体查询表达,JS 只管开合。抽屉打开时 Escape / 遮罩点击关闭;`locationKey` 变化(路由 / 会话切换)渲染期自动关闭;移动端关闭时侧栏容器 `inert`。侧栏内容经 `useSidebarDrawer()` 取 `{ close }`(壳外返回 null),导航后可关闭抽屉。
79
+ - **`registerChatLayout(layoutRegistry)`**:注册 `chat` 布局(header 标题区 + ChatView 区,V1 无会话历史侧栏),`resolveLayout(registry, 'chat')` 命中。
80
+ - **`Chat` / `registerChatComponent(registry)`**:一等 DSL 聊天工作区组件,见下节。
81
+ - **`createChatWorkspace(services)`**:React 直连路径的工作区工厂(内部与 Chat 共用同一组合),见下文「DSL 页面:createChatWorkspace」。
82
+
83
+ ## Chat:一等 DSL 组件
84
+
85
+ `Chat` 是与 Button / Input 同级的 DSL 组件:注册一次后,任意页面 DSL 里 `{ type: 'Chat', props, events }` 即可声明完整聊天工作区(AppShell 侧栏 + header + ChatView)。与 `createChatWorkspace` 的区别是**服务注入与组件声明解耦**——宿主在应用根部用 `ChatServicesProvider` 注入一次服务,`Chat` 组件就近从 context 读取;未注入时渲染受控「未配置聊天服务」提示并 `console.warn`,不抛错。
86
+
87
+ ```tsx
88
+ import { Registry } from '@pactor-app/core';
89
+ import { createPageRuntime } from '@pactor-app/runtime';
90
+ import {
91
+ ChatServicesProvider,
92
+ registerChatComponent,
93
+ } from '@pactor-app/chat';
94
+ import chatPageDsl from './chat.page.yaml';
95
+
96
+ const components = new Registry<unknown>('ComponentRegistry');
97
+ registerChatComponent(components); // 已存在 Chat 类型时不覆盖
98
+ const runtime = createPageRuntime(chatPageDsl, { components });
99
+
100
+ // 服务(ChatServices,与 ChatWorkspaceServices 同型:transport 必填,
101
+ // store / safeRegistry / capabilities / confirm / 附件与弹层回调等可选)
102
+ // 在根部注入一次
103
+ <ChatServicesProvider value={{ transport, safeRegistry, ... }}>
104
+ {runtime.render()}
105
+ </ChatServicesProvider>;
106
+ ```
107
+
108
+ ```yaml
109
+ # chat.page.yaml:菜单 / header / 问候建议均为 DSL 配置
110
+ dsl: '1.0'
111
+ page: { id: chat, title: 聊天页 }
112
+ state:
113
+ activeMenu: new
114
+ body:
115
+ type: Chat
116
+ props:
117
+ title: 助手工作区 # 内建 header 标题;showHeader: false 可关闭 header
118
+ greeting: 你好
119
+ suggestions:
120
+ - { label: 今天天气怎么样, prompt: 今天天气怎么样 }
121
+ activeMenuKey: ${state.activeMenu} # 受控高亮,可绑 DSL state
122
+ menu: # 内建侧栏(AppShell 承载,移动端为抽屉)
123
+ - { key: new, label: 新建对话, icon: plus }
124
+ - { key: s1, label: 本月报表分析, icon: history, group: 历史会话 }
125
+ events:
126
+ onMenuSelect: # event 作用域:event.key / event.item
127
+ - action: setState
128
+ params: { activeMenu: ${event.key} }
129
+ onSend: # event 作用域:event.text(与 transport turn 并行)
130
+ - action: setState
131
+ params: { lastSent: ${event.text} }
132
+ ```
133
+
134
+ - **菜单**:`menu: ChatMenuItem[]`(`key / label / icon? / badge? / disabled? / group?`)驱动内建侧栏;`icon` 为字符串名,映射内置 lucide 子集(plus / chat / history / settings / star / search / user / folder),未知名渲染占位圆点不抛错;`group` 渲染分组标题行;`activeMenuKey` 为受控 prop,典型用法是 `${state.x}` 绑定 + `events.onMenuSelect` 调 `setState` 闭环;disabled 项不触发 `onMenuSelect`。
135
+ - **Header 三层**:`title` 标量 → `headerExtra?: DslNode | ReactNode`(标题右侧槽)→ `header?: DslNode | ReactNode`(整体替换内建 header);`showHeader: false` 关闭。DslNode 槽仅在页面运行时(DslRenderer)上下文内经 `useRenderer().renderChildren` 渲染;纯 React 场景传 ReactNode,误传 DslNode 时告警并跳过(不抛错)。
136
+ - **侧栏槽**:`sidebar?: DslNode | ReactNode` 提供时整体替代内建菜单侧栏(仍以 AppShell 承载)。
137
+ - **React 直连**:`<ChatServicesProvider>` + `<Chat>` 同样可直接作为 React 组件使用(events 即普通函数 props:`onSend?(text)` / `onMenuSelect?(key, item)`)。`createChatWorkspace` 工厂保留为兼容路径,内部已改为 provider + 同一组合逻辑(`ChatComposition`),行为不变。
138
+
139
+ ### 消息操作条:messageActions
140
+
141
+ 消息气泡下方的操作条(复制 / 重新生成 / 重试 / 编辑)以可序列化 `messageActions` 数组完整声明(spec: chat-message-actions),`ChatView` / `ChatComposition` / `ChatWorkspace` / `Chat` 逐层透传:
142
+
143
+ ```yaml
144
+ body:
145
+ type: Chat
146
+ props:
147
+ # 数组即真相:顺序即渲染顺序,未列出的内建动作不出现;
148
+ # 缺省(不配置)等价于 ['copy', 'regenerate', 'retry', 'edit'],与历史行为一致
149
+ messageActions:
150
+ - copy # 字符串项等价于 { id: copy, builtin: copy }
151
+ - retry
152
+ - regenerate
153
+ - { id: edit, builtin: edit, label: 修改并重发 } # 内建动作按字段覆盖
154
+ - { id: quote, label: 引用回复, icon: quote, service: quoteMessage } # 自定义动作
155
+ events:
156
+ onMessageAction: # 任意动作(含内建)点击触发;event 作用域含 action 与 message
157
+ - action: message
158
+ params: { text: 触发消息操作:${event.action} }
159
+ ```
160
+
161
+ - **配置形状**:`Array<BuiltinMessageAction | ChatMessageAction>`;`ChatMessageAction = { id, builtin?, label?, icon?, roles?, visibleWhen?, service? }`(全部为 DSL 可表达数据)。`builtin` 指定时复用内建行为与默认描述(copy = assistant+completed;regenerate = assistant+completed 且仅末条且注入 transport;retry = assistant+error 且 transport;edit = user+completed),`label` / `icon` / `roles` / `visibleWhen` 按字段覆盖;「regenerate 仅末条 assistant」与 transport 依赖为不可覆盖的包内约束。
162
+ - **可见性**:`roles` 过滤角色后按 `visibleWhen` 匹配消息状态——`'completed'` 匹配非流式非错误终态(含 cancelled),`'error'` 匹配 error,`'always'` 含 streaming(自定义动作缺省 `roles: ['assistant']`、`visibleWhen: 'completed'`)。
163
+ - **自定义动作派发**:无 `builtin` 的动作点击时经 `ChatServices.callService(service, { actionId, message })` 派发(未注入 callService 时 console.warn 跳过,不抛错);内建与自定义动作点击均并行触发 `onMessageAction(actionId, message)` 通知(DSL 侧为 `events.onMessageAction`,event 作用域含 `action` 与 `message`),内建默认行为不受影响。
164
+ - **图标**:`icon` 为字符串名,走 `MESSAGE_ACTION_ICONS` 白名单(copy / check / rotate-ccw / pencil / quote / thumbs-up / thumbs-down / share / bookmark),未知名渲染占位图标,不抛错。
165
+
166
+ 完整参考见 `examples/chat`(`chat.page.yaml` 菜单 / state / events + `App.tsx` 宿主装配)。
167
+
168
+
169
+ ## DSL 页面:createChatWorkspace
170
+
171
+ 聊天页可以像其他 pactor 页面一样以 `*.page.yaml` 声明、经 `createPageRuntime` 渲染。表达式作用域只承载可序列化数据,函数 / 实例无法从 DSL 产生——因此运行时服务一律由宿主经工厂闭包注入,DSL 只携带标量配置:
172
+
173
+ ```tsx
174
+ import { Registry } from '@pactor-app/core';
175
+ import { createPageRuntime } from '@pactor-app/runtime';
176
+ import { createChatWorkspace } from '@pactor-app/chat';
177
+ import chatPageDsl from './chat.page.yaml'; // 经 @pactor-app/vite-plugin 编译
178
+
179
+ // 宿主装配:全部运行时服务与交互回调经闭包注入(只创建一次,
180
+ // 组件身份稳定,内部 store 才不会重建)
181
+ const ChatWorkspace = createChatWorkspace({
182
+ transport, // 必填:ChatTransport
183
+ store, // 可选:外部自持 store(另有 storeFactory)
184
+ safeRegistry, capabilities, confirm,
185
+ onAttachFiles: (files) => upload(files), // 回传 ChatAttachment[],发送后自动清空
186
+ onMentionTrigger, onPlusMenu,
187
+ toolbarStart: <ToolbarStart />, // ReactNode 闭包槽
188
+ });
189
+
190
+ const components = new Registry<unknown>('ComponentRegistry');
191
+ components.register('ChatWorkspace', ChatWorkspace);
192
+ const runtime = createPageRuntime(chatPageDsl, { components });
193
+ runtime.render(); // 或 <PageRuntimeView runtime={runtime} />
194
+ ```
195
+
196
+ ```yaml
197
+ # chat.page.yaml:DSL 只声明页面与标量配置
198
+ dsl: '1.0'
199
+ page: { id: chat, title: 聊天页 }
200
+ body:
201
+ type: ChatWorkspace
202
+ props:
203
+ title: 助手工作区 # 工作区头部标题(title 或 headerExtra 存在时渲染头部)
204
+ greeting: 你好
205
+ suggestions:
206
+ - { label: 今天天气怎么样, prompt: 今天天气怎么样 }
207
+ ```
208
+
209
+ - **DSL 安全 props**:`title?` / `greeting?` / `subtitle?` / `suggestions?` / `conversationId?` / `params?`——全部为标量 / 数组 / 普通对象;函数与实例一律不出现。
210
+ - **DslNode 槽**:`sidebar?: DslNode`(提供时以 AppShell 包裹)、`toolbarEnd?: DslNode`——renderer 对内嵌 DslNode prop 跳过提前求值,组件内经 `useRenderer().renderChildren` 渲染(Table `columns[].cell` 同款机制),仅页面运行时上下文内有效;纯 React 直连场景用服务的 `toolbarEnd` ReactNode 槽。
211
+ - **状态自持**:组件内部创建并持有 `ConversationStore`(外部 `store` > `storeFactory` > 默认构造,ref 保持——重渲染 / props 变化不重建)与附件受控状态(`onAttachFiles` 回传追加、发送后清空)。
212
+ - **React 直连 vs DSL 页面**:细粒度组装继续用 `<ChatView>`;整页 DSL 化用 `createChatWorkspace` + 页面运行时。完整参考见 `examples/chat`。
213
+
214
+ > **数据由调用方经接口注入**:包内不发起网络请求、不读取外部数据源。greeting / suggestions / attachments 等均为受控 props;文件上传、提及列表、预览等由调用方实现后经回调回写。`examples/chat` 提供内存级参考实现(data URL 伪上传、工具栏 chip、mention/+ 触发)。
215
+
216
+ ## 安全约定
217
+
218
+ - LLM 生成的 DSL 只经 Safe Registry 渲染(白名单外组件一律受控占位,props 内嵌 DSL 子树由 runtime 的 UnsupportedComponent 兜底)。
219
+ - AI Action 必须经 Capability → Permission → Confirmation → Execute 链;AI DSL 建议只配置 `capability` 动作(Safe Registry 只挡组件,动作侧由 capability 白名单兜底)。
220
+ - Markdown 渲染复用 @pactor-app/markdown 安全渲染器(不启用 raw HTML、URL 协议白名单)。
221
+
222
+ ## 文档
223
+
224
+ 完整文档见 <https://github.com/426-330/pactor/tree/main/docs>(`pnpm docs:dev` 本地启动文档站)。
225
+
226
+ ## License
227
+
228
+ [MIT](../../LICENSE)