@spzxg/cli-ui 0.3.1 → 0.6.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/README.md +101 -18
- package/dist/cli-ui-embed-CXdmrwo1.js +40896 -0
- package/dist/cli-ui-index-BZppwn-v.js +13528 -0
- package/dist/cli-ui.css +1 -1
- package/dist/cli-ui.js +1 -1
- package/package.json +2 -1
- package/src/board/BrowserPane.tsx +396 -96
- package/src/board/ChangesPane.tsx +13 -4
- package/src/board/CliBoard.tsx +219 -37
- package/src/board/FileViewer.tsx +244 -63
- package/src/board/FilesPane.tsx +51 -4
- package/src/board/blocks.tsx +3 -2
- package/src/board/fileOps.ts +55 -0
- package/src/board/folderDownload.ts +161 -0
- package/src/board/workspace.ts +129 -0
- package/src/board/zip.ts +85 -0
- package/src/chat/CliChat.tsx +126 -29
- package/src/core/blocks.ts +21 -2
- package/src/core/client.ts +270 -41
- package/src/core/config.ts +28 -11
- package/src/core/conversation.ts +35 -12
- package/src/core/provider.tsx +27 -6
- package/src/core/store.ts +354 -69
- package/src/core/textDecode.ts +24 -0
- package/src/core/types.ts +28 -6
- package/src/elements.tsx +113 -9
- package/src/index.ts +7 -2
- package/src/layout/CliSplit.tsx +53 -0
- package/src/layout/split.ts +214 -0
- package/src/msglist/CliMsgList.tsx +64 -26
- package/src/styles.css +101 -0
- package/src/ui/ErrorBoundary.tsx +38 -0
- package/src/ui/TextBlock.tsx +1 -1
- package/src/ui/transcriptModel.ts +66 -9
- package/dist/cli-ui-embed-C0F64Ln7.js +0 -37922
- package/dist/cli-ui-index--RVPCGIc.js +0 -13516
package/README.md
CHANGED
|
@@ -18,11 +18,25 @@ import "@spzxg/cli-ui/styles.css";
|
|
|
18
18
|
|
|
19
19
|
<CliChat jobId="job_xxx" /> // 打开已有对话(对话任务的第一个对话)
|
|
20
20
|
<CliChat jobId="job_xxx" taskId="task_yyy" /> // 对话任务里的某个对话;taskId="new" 新开一个
|
|
21
|
-
<CliChat jobId="job_batch" taskId="task_zzz" /> // 一次性任务 /
|
|
21
|
+
<CliChat jobId="job_batch" taskId="task_zzz" /> // 一次性任务 / 续派的子任务:运行中只能看,结束后可以接着它聊
|
|
22
22
|
<CliChat jobId={null} newConversation={{ title: "周报", businessId: "biz-1" }} /> // 新对话
|
|
23
23
|
<CliMsgList jobIds={["job_xxx"]} onSelect={setJobId} /> // 列表
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
+
列表只显示 `jobIds` 里的任务(加上本页新建的对话)。不传 `jobIds` 就是空列表:组件不会去中枢列出这个产品线令牌下的全部对话,哪些任务给哪个用户看由宿主决定。
|
|
27
|
+
|
|
28
|
+
三栏想左右拖动改宽度,用 `CliSplit`(Web Component:`<cli-split>`)包起来:
|
|
29
|
+
|
|
30
|
+
```tsx
|
|
31
|
+
<CliCenterProvider baseUrl="/api/center">
|
|
32
|
+
<CliSplit storageKey="strategy"> {/* 记住拖好的宽度;panes 可改每栏初始 / 最小 / 最大宽度 */}
|
|
33
|
+
<CliMsgList jobIds={ids} />
|
|
34
|
+
<CliChat />
|
|
35
|
+
<CliBoard />
|
|
36
|
+
</CliSplit>
|
|
37
|
+
</CliCenterProvider>
|
|
38
|
+
```
|
|
39
|
+
|
|
26
40
|
组件请求同源的 `/api/center`(可以用 `baseUrl` 改),由代理转到中枢并注入令牌:
|
|
27
41
|
|
|
28
42
|
```nginx
|
|
@@ -61,15 +75,28 @@ location /api/center/ {
|
|
|
61
75
|
- **组件自己建**:`<CliChat jobId={null} newConversation={{ title, businessId }} />`,建好后通过 `job_change` 事件把 job_id 交给页面。
|
|
62
76
|
- **业务后端建**(要带 callback 时):后端调 `POST /api/jobs`,前端只传 `jobId`,这样 callback 的密钥留在服务端。
|
|
63
77
|
|
|
64
|
-
##
|
|
78
|
+
## 组合使用(列表 + 对话 + 看板联动)
|
|
79
|
+
|
|
80
|
+
放进同一个 `CliCenterProvider`(Web Component 写同一个 `group`),三个组件互相通信、切换时一起变:列表点哪个对话,对话和看板就切到哪个;对话里新建 / 接续出的新对话,列表跟着加上并选中、看板跟过去;对话里点文件在看板里打开。
|
|
65
81
|
|
|
66
82
|
```tsx
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
83
|
+
<CliCenterProvider baseUrl="/api/center"
|
|
84
|
+
selectedId={key} // 打开 / 切换到哪个对话("job_x" 或 "job_x#task_y");不传由列表点击决定
|
|
85
|
+
onEvent={(e) => { if (e.type === "select") setKey(e.jobId); if (e.type === "job_change") setKey(e.key); }}>
|
|
86
|
+
<CliMsgList jobIds={ids} />
|
|
87
|
+
<CliChat />
|
|
88
|
+
<CliBoard />
|
|
89
|
+
</CliCenterProvider>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
- Provider 里的组件不要再传 `jobId` / `taskId`(列表不传 `selectedId`):传了的组件只看自己那个,不跟着切。
|
|
93
|
+
- Web Component:同一个 `group`,不写 `job-id`;从外面打开某个对话给任意一个元素写 `selected-id="job_x"`,改它就一起切。
|
|
94
|
+
- 不用 Provider 也行:页面自己串,每个组件传 `jobId` / `selectedId`,在 `onSelect`、`job_change` 里同步:
|
|
70
95
|
|
|
71
|
-
|
|
72
|
-
<
|
|
96
|
+
```tsx
|
|
97
|
+
<CliMsgList selectedId={key} onSelect={setKey} onNew={() => setKey(null)} />
|
|
98
|
+
<CliChat jobId={key} onEvent={(e) => e.type === "job_change" && setKey(e.key)} />
|
|
99
|
+
<CliBoard jobId={key} />
|
|
73
100
|
```
|
|
74
101
|
|
|
75
102
|
## 右侧看板
|
|
@@ -95,29 +122,66 @@ location /api/center/ {
|
|
|
95
122
|
<CliCenterProvider> // 和对话联动:对话里点文件 / 「查看浏览器」在看板里打开
|
|
96
123
|
<CliChat />
|
|
97
124
|
<CliBoard collapsed={!open} onExpand={() => setOpen(true)} onCollapse={() => setOpen(false)}
|
|
98
|
-
|
|
125
|
+
extraTabs={[{ id: "term", label: "终端", render: ({ jobId, quote }) => <MyTerminal jobId={jobId} onQuote={(t) => quote({ kind: "terminal", label: "终端输出", text: t })} /> }]} />
|
|
99
126
|
</CliCenterProvider>
|
|
100
127
|
```
|
|
101
128
|
|
|
102
129
|
- 页签:文件(目录、预览、下载)、改动(diff,只读)、浏览器(实时画面,接管后可直接操作)。`tabs={["files","changes"]}` 选页签。
|
|
103
130
|
- 预览:Markdown、代码、文本、CSV、图片、PDF、HTML(沙箱 iframe)、Word(.docx,mammoth 转 HTML 放进无脚本沙箱)。其他类型下载。
|
|
104
|
-
- 自动同步:实时流有进展就刷新(节流 1.5 秒),运行中每
|
|
131
|
+
- 自动同步:实时流有进展就刷新(节流 1.5 秒),运行中每 15 秒(`pollMs`)兜底再刷一次;改过的文件标「有更新」,打开着的文件改了自动重读。
|
|
105
132
|
- 没有看板时,对话自己弹窗预览(`<CliChat fileOpen="auto|board|modal|link">`)。宿主也可以 `store.openInBoard(jobId, { kind: "file", path })`。
|
|
133
|
+
- 下载:单个文件;文件夹(页签右上角 / 文件夹行尾)在浏览器里打包成 ZIP,最多 2000 个文件、共 300 MB。
|
|
134
|
+
- 弱网 / 节点忙:读文件 30 秒没响应、或传输中 60 秒没收到数据就报错,预览里有「重试」;读超过 8 秒提示「网络较慢」。打包下载时每个文件失败会自动重试 3 次,一直读不到的跳过,其余照样打包,失败清单在压缩包里的 `_下载失败的文件.txt`。
|
|
135
|
+
- 放大:文件预览、浏览器画面都能放大到整页(Esc 还原);接管浏览器时自动放大。图片可缩放(Ctrl / ⌘ + 滚轮)。
|
|
136
|
+
- 引用到对话:文件预览、浏览器的「引用到对话」把它带进下一条消息(`store.addRef`),对话输入框上方显示、可去掉;发出时合成「看板上下文」附件。自定义页签(`extraTabs`)的 `quote` 同理——终端之类的交互以后都走这条路进对话。
|
|
137
|
+
- 多个文件页签:点开的文件各占一个页签(最多 8 个,再开关掉最早的),可关闭(× 或鼠标中键),按对话记在 sessionStorage,刷新后还在。
|
|
138
|
+
- 选中引用:在预览里选中文字浮出「引用到对话」;在浏览器网页里复制(⌘C / Ctrl+C)后提示里也有「引用到对话」。
|
|
139
|
+
- 正在看:打开着的文件 / 浏览器页面缺省随下一条消息带上一次(输入框上方虚线卡片「正在看」,× 这次不带;`autoQuoteOpenFile={false}` 关掉)。状态在 `store.getState().viewing`。
|
|
140
|
+
- 浏览器弱网:操作没送达时提示(不自动重发,免得点两次);接管时发了操作 8 秒画面没变化提示并给「重连画面」;按住鼠标切走窗口自动补发松开;地址栏自动补 `https://`。
|
|
106
141
|
- 数据都经中枢:`/api/jobs/:id/board`、`/files`、`/files/raw/<路径>`、`/diff`、`/browser`(+ `events` / `frames` / `input`)。
|
|
107
142
|
- 链路、交互、故障与重试的设计见 [BOARD-DESIGN.md](BOARD-DESIGN.md)。
|
|
108
|
-
- 浏览器里带令牌时(传了 `token` / `headers` / `line
|
|
143
|
+
- 浏览器里带令牌时(传了 `token` / `headers` / `line`,或代理跨域),图片、PDF、HTML、下载用中枢发的 30 分钟短期链接(令牌在路径里,HTML 的相对资源也能加载,大文件流式下载、不进内存);走同源代理时直接用地址。中枢太旧发不了短期链接时退回先 fetch 成 blob(超过 50 MB 的不预览)。
|
|
144
|
+
|
|
145
|
+
## 对接业务数据
|
|
146
|
+
|
|
147
|
+
页面通过 `store` 拿文件、追加背景、发消息;看板可以不显示(不放 `CliBoard`,或 `<cli-board hidden>`)。
|
|
148
|
+
|
|
149
|
+
```tsx
|
|
150
|
+
const store = createCliStore({ baseUrl: "/api/center" }); // 或 Provider 里 useCliStore();Web Component:el.store
|
|
151
|
+
<CliCenterProvider store={store} onTurnEnd={async (p) => { // 每轮结束拿产出
|
|
152
|
+
const files = await store.workspace(p.job_id).listAll();
|
|
153
|
+
}}>…</CliCenterProvider>
|
|
154
|
+
|
|
155
|
+
const ws = store.workspace(); // 当前选中的对话;或 store.workspace("job_xxx");Web Component:el.workspace()
|
|
156
|
+
await ws.list(dir); await ws.listAll(dir);
|
|
157
|
+
await ws.readText(path); // ≤ 20 MB,GBK 自动转码
|
|
158
|
+
await ws.readFile(path); // Blob,≤ 500 MB(maxBytes 可调),更大的用 fileUrl / download
|
|
159
|
+
await ws.zip(dir); // { blob, name, files, failed }
|
|
160
|
+
await ws.download(path); await ws.downloadFolder(dir);
|
|
161
|
+
await ws.fileUrl(path, { download: true });
|
|
162
|
+
ws.openFile(); ws.openFiles(); ws.open(path); await ws.info();
|
|
163
|
+
|
|
164
|
+
store.appendContext(jobId, text, { title, id }); // 随下一条消息带给智能体;null = 新对话的第一条
|
|
165
|
+
store.removeContext(jobId, id);
|
|
166
|
+
await store.send(jobId, text);
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
- 追加的背景显示在输入框上方(「补充背景」),发出后在这个对话里一直有效;同一任务里新开的对话不会自动带上。创建时的项目背景用 `newConversation.workspace.context`。每条最长 20000 字,同 `id` 替换。
|
|
170
|
+
- 事件:`board_change`(看板切换文件)、`context`(补充背景变化 / 发出)、`board_ref`。
|
|
171
|
+
- Web Component 的方法在元素注册后才有:`await customElements.whenDefined("cli-chat")`。元素上还有 `store`、`appendContext`、`removeContext`,TypeScript 里 `document.querySelector("cli-board")` 自带类型。
|
|
172
|
+
- 服务端渲染:组件和 `@spzxg/cli-ui/elements` 都能在服务端导入 / 渲染(`defineCliElements()` 在服务端什么都不做);Next.js 在用到组件的文件写 `"use client"`。
|
|
109
173
|
|
|
110
174
|
## 界面配置
|
|
111
175
|
|
|
112
176
|
```tsx
|
|
113
177
|
<CliChat config={{
|
|
114
|
-
header: {
|
|
178
|
+
header: { title: true, status: true }, // 顶部栏缺省隐藏;header: true 全部显示
|
|
115
179
|
composer: { placeholder: "给 {agent} 发消息…" }, // composer: false 只读
|
|
116
180
|
transcript: { tools: false },
|
|
117
181
|
}} />
|
|
118
182
|
```
|
|
119
183
|
|
|
120
|
-
|
|
184
|
+
cli-chat 的顶部栏(面包屑、标题、智能体、用量、模型、状态、结束对话)缺省全部隐藏,需要哪项写哪项。各种状态(未开始、进行中、已完成……)缺省都不显示:要显示打开 chat 的 `header.status`、`transcript.status`,list 的 `card.status`(卡片状态)、`grouped`(按状态分组)。所有配置项和缺省值都在 [`src/core/config.ts`](src/core/config.ts) 里,每一项都有注释。Web Component 上用 `config='{"header":{"title":true}}'`。
|
|
121
185
|
|
|
122
186
|
## 事件
|
|
123
187
|
|
|
@@ -138,6 +202,8 @@ location /api/center/ {
|
|
|
138
202
|
`<CliChat jobId taskId>` 两个参数,或者把对话键整个交给 `jobId` 都可以。cli-msglist 里开了几个对话的任务按对话展开,卡片上的「+」新开对话;`onSelect(id, { jobId, taskId })` 的 `id` 就是对话键,直接交给 CliChat 即可。指定了对话时,「结束对话」只关这一个(`POST /api/jobs/:id/conversations/:taskId/close`),没指定时结束整个对话任务。工具函数 `convKey` / `parseConvKey` 从包里导出。
|
|
139
203
|
|
|
140
204
|
一次性任务(含续派,`mode` 不是 `conversation`)也能交给 CliChat:只读显示一个子任务的执行过程(没有输入框和「结束对话」,状态是子任务的状态)。`taskId` 指定看哪个子任务,不给看最近派发的那个(续派开出新的子任务后自动跟过去)。转录里中枢的派发标记行(`[CENTER-ATTEMPT…]`)去掉,完成核对(`[CENTER-CONFIRM]`)合成一行说明。需要 0.3.1 + 中枢实时流支持 `?task=`;0.3.0 打开这类任务会一直「正在重连」。
|
|
205
|
+
|
|
206
|
+
例外是对话式续派的子任务(续派时 `mode: "conversation"`):用 `taskId` 打开它就是一个能一直聊下去的对话——有输入框,不显示「子任务已完成」之类的状态;每发一条 = 续派一条 `continue_from` 这个子任务的子任务(接在同一个 Agent 会话里),对话键不变。需要 0.4.0 + 同版本中枢。
|
|
141
207
|
- Web Component 上监听 `cli-turn-end` / `cli-conversation-end` / `cli-event` 事件。
|
|
142
208
|
- 旧名字 `onCallback` / `cli-callback` 仍然可用,等同于前两个合在一起。
|
|
143
209
|
|
|
@@ -154,14 +220,31 @@ Agent 回复里指向本地文件的链接(`report.md`、`src/a.ts:12`、`file
|
|
|
154
220
|
|
|
155
221
|
## 开发
|
|
156
222
|
|
|
223
|
+
源码在 **operator-oss/packages/cli-ui**(这里)。三个工程的关系:
|
|
224
|
+
|
|
225
|
+
| 工程 | 放什么 | 和组件的关系 |
|
|
226
|
+
| --- | --- | --- |
|
|
227
|
+
| `operator-oss` | 执行节点(Operator)+ **组件源码** `packages/cli-ui` | 改组件都在这里改;组件的样式从同仓库的 `app/globals.css` 同步(`css:sync`) |
|
|
228
|
+
| `clawbizx` | 业务协同工程,统一发布 `@spzxg/*` 包 | `packages/cli-ui` 是指向 `../../operator-oss/packages/cli-ui` 的链接,`scripts/publish-shared-packages.sh` 从这里发布到 npm |
|
|
229
|
+
| `cli-task-center` | 中枢(组件的数据都来自它) | 组件的使用文档在中枢 API 文档的「前端组件」页签(`backend/public/api.html`) |
|
|
230
|
+
|
|
231
|
+
两个仓库要并排放在同一个目录下(`~/operator-oss`、`~/clawbizx`),链接才有效。组件有自己的依赖,不和 Operator 共用:
|
|
232
|
+
|
|
157
233
|
```bash
|
|
158
|
-
|
|
159
|
-
npm
|
|
160
|
-
npm
|
|
161
|
-
npm run
|
|
162
|
-
npm run
|
|
234
|
+
cd operator-oss/packages/cli-ui
|
|
235
|
+
npm install # 第一次
|
|
236
|
+
npm test # 测试
|
|
237
|
+
npm run lint # 类型检查
|
|
238
|
+
npm run demo # 假中枢 + 演示页 http://localhost:12790(?mode=chat / ?mode=wired / elements.html;看板用 e2e-local.sh 的 ?mode=board,单独集成看 blocks.html?job=…,先 build:embed)
|
|
239
|
+
npm run css:sync # Operator 改了样式后重新同步
|
|
240
|
+
npm run build:embed # 第三方一体化产物 dist/cli-ui.js + cli-ui.css
|
|
163
241
|
bash demo/e2e-local.sh # 连真实 center 联调:本地 center + 假节点 + 一次性本地库,演示页代理过去
|
|
164
242
|
bash demo/e2e-local.sh stop
|
|
243
|
+
bash demo/e2e-real.sh # 真实整条链路:本地 center + 源码跑的真实节点 + 本机 ego lite(看板浏览器、故障演练;不跑智能体)
|
|
244
|
+
bash demo/e2e-real.sh stop
|
|
245
|
+
|
|
246
|
+
# 发布(在 clawbizx 里)
|
|
247
|
+
cd clawbizx && bash scripts/publish-shared-packages.sh
|
|
165
248
|
```
|
|
166
249
|
|
|
167
|
-
演示用的是 `demo/mock-center.mjs` 里的内存数据,不要把演示页指向生产中枢。
|
|
250
|
+
演示用的是 `demo/mock-center.mjs` 里的内存数据,不要把演示页指向生产中枢。Operator 的 `tsconfig.json`、`.dockerignore` 排除了 `packages/`,组件不进 Operator 的构建和镜像。
|