@spzxg/cli-ui 0.1.0 → 0.3.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 CHANGED
@@ -6,9 +6,9 @@
6
6
  | --- | --- |
7
7
  | `<CliChat>` / `<cli-chat>` | 对话:实时显示执行过程,发消息、排队、停止、回答提问、结束对话 |
8
8
  | `<CliMsgList>` / `<cli-msglist>` | 对话列表,按「需要你处理 / 进行中 / 已结束」分组 |
9
- | cli-board | 右侧看板(文件、预览、下载):规划中,见 [PLAN.md](PLAN.md) |
9
+ | `<CliBoard>` / `<cli-board>` | 右侧看板:工作区文件(预览、下载)、改动、任务浏览器(实时画面、接管 / 交还) |
10
10
 
11
- 两个组件**相互独立**:传入参数就能单独运行,之后由组件接管。完整说明见中枢 API 文档的「前端组件」页签(`/api-docs#fe-start`)。
11
+ 三个组件**相互独立**:传入参数就能单独运行,之后由组件接管。完整说明见中枢 API 文档的「前端组件」页签(`/api-docs#fe-start`)。
12
12
 
13
13
  ## 快速开始
14
14
 
@@ -16,7 +16,8 @@
16
16
  import { CliChat, CliMsgList } from "@spzxg/cli-ui";
17
17
  import "@spzxg/cli-ui/styles.css";
18
18
 
19
- <CliChat jobId="job_xxx" /> // 打开已有对话
19
+ <CliChat jobId="job_xxx" /> // 打开已有对话(对话任务的第一个对话)
20
+ <CliChat jobId="job_xxx" taskId="task_yyy" /> // 对话任务里的某个对话;taskId="new" 新开一个
20
21
  <CliChat jobId={null} newConversation={{ title: "周报", businessId: "biz-1" }} /> // 新对话
21
22
  <CliMsgList jobIds={["job_xxx"]} onSelect={setJobId} /> // 列表
22
23
  ```
@@ -40,30 +41,71 @@ location /api/center/ {
40
41
  <link rel="stylesheet" href="/static/cli-ui/cli-ui.css">
41
42
  <script type="module" src="/static/cli-ui/cli-ui.js"></script>
42
43
  <cli-chat base-url="/api/center" job-id="job_xxx"></cli-chat>
43
- <!-- 或者 JS 挂载:CliUI.mountChat(el, { jobId, newConversation, onCallback }) → { update, unmount } -->
44
+ <!-- 或者 JS 挂载:CliUI.mountChat(el, { jobId, newConversation, onTurnEnd }) → { update, unmount } -->
44
45
  ```
45
46
 
46
47
  示例见 `demo/embed.html`:先 `npm run build:embed`,再 `npm run demo`,然后打开 http://127.0.0.1:12791/embed.html。
47
48
 
48
49
  ## 数据怎么流转
49
50
 
50
- - **组件 ⇄ 中枢**:显示对话、发消息。页面打开时才连接。
51
- - **中枢 → 业务后端 `callback.url`**:每轮一次 `conversation_turn`,结束时一次 `conversation_closed`。**落库以它为准。**
52
- - **组件 → 页面 `onCallback`**:内容与中枢回调体字段相同,只用来即时刷新页面。
51
+ **组件不依赖 callback,也不依赖业务系统。** 它在浏览器里直接和中枢收发:
53
52
 
54
- 推荐由业务后端建对话(`POST /api/jobs`,带 `mode: "conversation"`、`business_id`、`callback`),把 `job_id` 存进业务库,前端只传 `jobId`。
53
+ - **发消息**:新对话 `POST /api/jobs`(`mode: "conversation"`),之后每条 `POST /api/jobs/:id/messages`。
54
+ - **收消息**:`GET /api/jobs/:id/stream`(SSE 长连接)推送 Agent 的输出、工具调用、提问和状态变化。
55
+ - **只依赖两样东西**:能访问中枢,以及带上产品线令牌(经同源代理)。
56
+
57
+ `callback.url` 是中枢推给**业务后端**的,浏览器收不到,组件也用不到。只有后端想把每轮结果存下来、即使没人开着页面也要存时才需要配。页面上想在一轮结束时做点什么,用组件的 `onTurnEnd` / `onConversationEnd`:它们是组件从实时流里算出来的,不配 callback 也会触发,载荷字段与中枢回调体相同。
58
+
59
+ 谁来建对话:
60
+ - **组件自己建**:`<CliChat jobId={null} newConversation={{ title, businessId }} />`,建好后通过 `job_change` 事件把 job_id 交给页面。
61
+ - **业务后端建**(要带 callback 时):后端调 `POST /api/jobs`,前端只传 `jobId`,这样 callback 的密钥留在服务端。
55
62
 
56
63
  ## 和列表一起用
57
64
 
58
65
  ```tsx
59
66
  // a. 页面自己串:两个组件互不依赖
60
67
  <CliMsgList selectedId={jobId} onSelect={setJobId} onNew={() => setJobId(null)} />
61
- <CliChat jobId={jobId} onEvent={(e) => e.type === "job_change" && setJobId(e.jobId)} />
68
+ <CliChat jobId={jobId} onEvent={(e) => e.type === "job_change" && setJobId(e.key)} />
62
69
 
63
70
  // b. 放进同一个 Provider:自动联动
64
71
  <CliCenterProvider><CliMsgList /><CliChat /></CliCenterProvider>
65
72
  ```
66
73
 
74
+ ## 右侧看板
75
+
76
+ 核心是两块,都能单独放到任何地方(React、Web Component、无需构建的一体化 JS):
77
+
78
+ ```tsx
79
+ <CliFilePreview jobId="job_xxx" path="outputs/报告.md" /> // 文件预览 + 下载
80
+ <CliBrowserView jobId="job_xxx" /> // 浏览器实时画面 + 接管
81
+ ```
82
+
83
+ ```html
84
+ <cli-file-preview base-url="/api/center" job-id="job_xxx" path="outputs/报告.md"></cli-file-preview>
85
+ <cli-browser base-url="/api/center" job-id="job_xxx"></cli-browser>
86
+ <!-- 或 CliUI.mountFilePreview(el, { jobId, path }) / CliUI.mountBrowser(el, { jobId }) -->
87
+ ```
88
+
89
+ 宿主页面 CSP、触屏、窄容器等集成约定见 [BOARD-DESIGN.md](BOARD-DESIGN.md) 第 0 节。整个看板:
90
+
91
+ ```tsx
92
+ <CliBoard jobId="job_xxx" /> // 单独用
93
+
94
+ <CliCenterProvider> // 和对话联动:对话里点文件 / 「查看浏览器」在看板里打开
95
+ <CliChat />
96
+ <CliBoard collapsed={!open} onExpand={() => setOpen(true)} onCollapse={() => setOpen(false)}
97
+ fileActions={[{ label: "引用", onClick: (file) => quote(file.path) }]} />
98
+ </CliCenterProvider>
99
+ ```
100
+
101
+ - 页签:文件(目录、预览、下载)、改动(diff,只读)、浏览器(实时画面,接管后可直接操作)。`tabs={["files","changes"]}` 选页签。
102
+ - 预览:Markdown、代码、文本、CSV、图片、PDF、HTML(沙箱 iframe)、Word(.docx,mammoth 转 HTML 放进无脚本沙箱)。其他类型下载。
103
+ - 自动同步:实时流有进展就刷新(节流 1.5 秒),运行中每 4 秒(`pollMs`)再刷一次;改过的文件标「有更新」,打开着的文件改了自动重读。
104
+ - 没有看板时,对话自己弹窗预览(`<CliChat fileOpen="auto|board|modal|link">`)。宿主也可以 `store.openInBoard(jobId, { kind: "file", path })`。
105
+ - 数据都经中枢:`/api/jobs/:id/board`、`/files`、`/files/raw/<路径>`、`/diff`、`/browser`(+ `events` / `frames` / `input`)。
106
+ - 链路、交互、故障与重试的设计见 [BOARD-DESIGN.md](BOARD-DESIGN.md)。
107
+ - 浏览器里带令牌时(传了 `token` / `headers` / `line`),图片、PDF、HTML、下载先 fetch 成 blob 再显示;走同源代理时直接用地址。
108
+
67
109
  ## 界面配置
68
110
 
69
111
  ```tsx
@@ -78,25 +120,45 @@ location /api/center/ {
78
120
 
79
121
  ## 事件
80
122
 
81
- - `onCallback(payload)`:`payload.type` 为 `conversation_turn` / `conversation_closed`,字段与中枢回调体相同。
82
- - `onEvent(event)`:另外还有 `job_change`(新建了对话,或接续出了新 job_id,要存起来)、`select`、`error`。
83
- - Web Component 上监听 `cli-callback` / `cli-event` 事件。
123
+ 都在浏览器里从实时流算出,不需要配 callback:
124
+
125
+ - `onTurnEnd(payload)`:一轮结束。字段与中枢的 `conversation_turn` 回调体相同(`seq`、`status`、`reply`、`asked`、`business_id`……)。
126
+ - `onConversationEnd(payload)`:对话结束,总在最后一个 `onTurnEnd` 之后。字段与 `conversation_closed` 相同。
127
+ - `onEvent(event)`:全部事件,另外还有 `job_change`(新建了对话,或接续出了新 job_id;`key` 是新对话的对话键)、`select`、`error`。
128
+
129
+ ### 一个对话任务里的几个对话
130
+
131
+ 对话任务(`job_id`)里可以开几个对话(各是一个子任务、一个 Agent 会话,共用同一节点和工作区,可以同时跑)。组件用**对话键**标识一个对话:
132
+
133
+ - `"job_xxx"`:对话任务的第一个对话(只有一个对话时就是它,和以前一样);
134
+ - `"job_xxx#task_yyy"`:里面指定的那个对话;
135
+ - `"job_xxx#new"`:在这个对话任务里新开一个对话(还没发第一条消息)。
136
+
137
+ `<CliChat jobId taskId>` 两个参数,或者把对话键整个交给 `jobId` 都可以。cli-msglist 里开了几个对话的任务按对话展开,卡片上的「+」新开对话;`onSelect(id, { jobId, taskId })` 的 `id` 就是对话键,直接交给 CliChat 即可。指定了对话时,「结束对话」只关这一个(`POST /api/jobs/:id/conversations/:taskId/close`),没指定时结束整个对话任务。工具函数 `convKey` / `parseConvKey` 从包里导出。
138
+ - Web Component 上监听 `cli-turn-end` / `cli-conversation-end` / `cli-event` 事件。
139
+ - 旧名字 `onCallback` / `cli-callback` 仍然可用,等同于前两个合在一起。
84
140
 
85
141
  ## 顺序与容错
86
142
 
87
143
  - 同一对话的消息按发出顺序送到 center(一条一条发);新建对话只发一次(center 建对话不去重)。
88
- - 回调带 `callback_seq`,`conversation_closed` 总在最后一个 `conversation_turn` 之后,与 center 回调一致。
144
+ - 结果通知带 `callback_seq`,对话结束总在最后一轮之后,顺序与 center 回调一致。
89
145
  - 发送失败时内容留在输入框;撤回、停止、回答、结束失败时在顶部提示;排队卡住时气泡下写明原因。
90
146
  - 实时流 45 秒没数据就重连,网络恢复或切回页面时立即重连;404 / 401 停止重连,并给出提示。
91
147
 
148
+ ## 本地文件链接
149
+
150
+ Agent 回复里指向本地文件的链接(`report.md`、`src/a.ts:12`、`file:///…`),识别规则与 Operator 相同。缺省点了在看板(或弹窗)里打开,按住 ⌘/Ctrl 点按地址在新窗口打开;`fileUrl={(path, jobId) => 地址}` 可以自己给地址。
151
+
92
152
  ## 开发
93
153
 
94
154
  ```bash
95
- npm run demo -w @spzxg/cli-ui # 假中枢 + 演示页 http://localhost:12790(?mode=chat / ?mode=wired / elements.html)
155
+ npm run demo -w @spzxg/cli-ui # 假中枢 + 演示页 http://localhost:12790(?mode=chat / ?mode=wired / elements.html;看板用 e2e-local.sh 的 ?mode=board,单独集成看 blocks.html?job=…,先 build:embed)
96
156
  npm test -w @spzxg/cli-ui
97
157
  npm run lint -w @spzxg/cli-ui
98
158
  npm run css:sync -w @spzxg/cli-ui # Operator 改了样式后重新同步
99
159
  npm run build:embed -w @spzxg/cli-ui # 第三方一体化产物 dist/cli-ui.js + cli-ui.css
160
+ bash demo/e2e-local.sh # 连真实 center 联调:本地 center + 假节点 + 一次性本地库,演示页代理过去
161
+ bash demo/e2e-local.sh stop
100
162
  ```
101
163
 
102
164
  演示用的是 `demo/mock-center.mjs` 里的内存数据,不要把演示页指向生产中枢。