@spzxg/cli-ui 0.0.0-stage → 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 +163 -2
- package/dist/cli-ui-embed-hutDQhkT.js +37841 -0
- package/dist/cli-ui-index-C8TOk7M3.js +13516 -0
- package/dist/cli-ui.css +1 -0
- package/dist/cli-ui.js +17 -0
- package/package.json +49 -4
- package/src/board/BrowserPane.tsx +418 -0
- package/src/board/ChangesPane.tsx +142 -0
- package/src/board/CliBoard.tsx +246 -0
- package/src/board/FileViewer.tsx +257 -0
- package/src/board/FilesPane.tsx +109 -0
- package/src/board/blocks.tsx +96 -0
- package/src/board/fileKind.ts +81 -0
- package/src/chat/CliChat.tsx +639 -0
- package/src/chat/Composer.tsx +194 -0
- package/src/chat/export.ts +34 -0
- package/src/core/blocks.ts +132 -0
- package/src/core/client.ts +441 -0
- package/src/core/config.ts +222 -0
- package/src/core/conversation.ts +439 -0
- package/src/core/provider.tsx +103 -0
- package/src/core/sse.ts +51 -0
- package/src/core/store.ts +575 -0
- package/src/core/types.ts +385 -0
- package/src/elements.tsx +166 -0
- package/src/embed.ts +58 -0
- package/src/index.ts +40 -0
- package/src/msglist/CliMsgList.tsx +342 -0
- package/src/operator.css +1628 -0
- package/src/styles.css +174 -0
- package/src/ui/Markdown.tsx +55 -0
- package/src/ui/TextBlock.tsx +49 -0
- package/src/ui/Transcript.tsx +408 -0
- package/src/ui/fileLinks.ts +46 -0
- package/src/ui/format.ts +101 -0
- package/src/ui/icons.tsx +89 -0
- package/src/ui/shared.tsx +38 -0
- package/src/ui/transcriptModel.ts +94 -0
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 两套数据形状:
|
|
3
|
+
* · center 服务(cli-task-center)对话任务接口的返回(snake_case,原样保留,便于对照 API 文档);
|
|
4
|
+
* · Operator 节点转录的消息与事件(center 的实时流原样转发,界面按 Operator 的规则渲染)。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// ── center:对话任务 ────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
export type JobStatus = "queued" | "bound" | "idle" | "running" | "paused" | "succeeded" | "failed" | "cancelled" | string;
|
|
10
|
+
export type TurnStatus = "queued" | "dispatching" | "running" | "succeeded" | "failed" | "cancelled" | "merged" | string;
|
|
11
|
+
|
|
12
|
+
export interface CenterTurn {
|
|
13
|
+
turn_id: string;
|
|
14
|
+
job_id: string;
|
|
15
|
+
task_id: string;
|
|
16
|
+
seq: number;
|
|
17
|
+
business_id: string | null;
|
|
18
|
+
status: TurnStatus;
|
|
19
|
+
input: string;
|
|
20
|
+
/** 合并了后面排队的消息时,实际发给 Agent 的全文;否则为 null(= input)。 */
|
|
21
|
+
sent_input: string | null;
|
|
22
|
+
merged_into_turn_id: string | null;
|
|
23
|
+
reply: string | null;
|
|
24
|
+
/** true = 这一轮以 Agent 提问结束:reply 是问题,下一条消息就是回答。 */
|
|
25
|
+
asked: boolean;
|
|
26
|
+
last_error: string | null;
|
|
27
|
+
attempt: number;
|
|
28
|
+
agent: string | null;
|
|
29
|
+
resolved_model: string | null;
|
|
30
|
+
total_tokens: number;
|
|
31
|
+
input_tokens: number;
|
|
32
|
+
output_tokens: number;
|
|
33
|
+
cache_read_tokens: number;
|
|
34
|
+
cache_creation_tokens: number;
|
|
35
|
+
/** true = 节点上的会话重建了,这一轮起之前的对话没带过去(工作区文件还在)。 */
|
|
36
|
+
context_reset: boolean;
|
|
37
|
+
/** 这一轮的改动是否已合并回项目基线。 */
|
|
38
|
+
merged?: boolean;
|
|
39
|
+
merge_error?: string | null;
|
|
40
|
+
created_at: string;
|
|
41
|
+
started_at: string | null;
|
|
42
|
+
ended_at: string | null;
|
|
43
|
+
queue_position?: number | null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface CenterTask {
|
|
47
|
+
id: string;
|
|
48
|
+
job_id: string;
|
|
49
|
+
seq?: number;
|
|
50
|
+
title: string;
|
|
51
|
+
status: string;
|
|
52
|
+
agent: string | null;
|
|
53
|
+
resolved_model: string | null;
|
|
54
|
+
total_tokens: number;
|
|
55
|
+
input_tokens: number;
|
|
56
|
+
output_tokens: number;
|
|
57
|
+
cache_read_tokens: number;
|
|
58
|
+
cache_creation_tokens: number;
|
|
59
|
+
browser?: boolean;
|
|
60
|
+
last_error: string | null;
|
|
61
|
+
/** 接续出来的对话:指回旧对话任务里对应的那个对话。 */
|
|
62
|
+
continue_from_task_id?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 对话任务里的一个对话(一个子任务、一个 Agent 会话)。conversation.conversations 的一项。 */
|
|
66
|
+
export interface ConversationInfo {
|
|
67
|
+
task_id: string;
|
|
68
|
+
seq: number;
|
|
69
|
+
title: string;
|
|
70
|
+
status: string;
|
|
71
|
+
/** 关掉了(单独关闭这一个,或整个对话任务结束)。 */
|
|
72
|
+
closed: boolean;
|
|
73
|
+
turns: number;
|
|
74
|
+
queued_turns: number;
|
|
75
|
+
running_turn: boolean;
|
|
76
|
+
last_turn: CenterTurn | null;
|
|
77
|
+
remote_task_id: string | null;
|
|
78
|
+
continue_from_task_id: string | null;
|
|
79
|
+
created_at: string | null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface ConversationSummary {
|
|
83
|
+
/** 里面的每个对话(按开的先后)。旧版 center 没有这个字段:当作只有一个对话。 */
|
|
84
|
+
conversations?: ConversationInfo[];
|
|
85
|
+
turns: number;
|
|
86
|
+
queued_turns: number;
|
|
87
|
+
running_turn: boolean;
|
|
88
|
+
last_turn: CenterTurn | null;
|
|
89
|
+
last_activity_at: string | null;
|
|
90
|
+
expires_at: string | null;
|
|
91
|
+
closed_reason: string | null;
|
|
92
|
+
continued_from_job_id: string | null;
|
|
93
|
+
continued_by_job_id: string | null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** GET /api/jobs/:id */
|
|
97
|
+
export interface CenterJob {
|
|
98
|
+
id: string;
|
|
99
|
+
title: string;
|
|
100
|
+
status: JobStatus;
|
|
101
|
+
mode?: "conversation" | string;
|
|
102
|
+
conversation?: ConversationSummary;
|
|
103
|
+
tasks: CenterTask[];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** POST /api/jobs/:id/messages、POST /api/jobs/:id/conversations */
|
|
107
|
+
export interface SendResult {
|
|
108
|
+
job_id: string;
|
|
109
|
+
/** 实际接收这条消息的对话(对话任务过期接续后是新对话任务里对应的那个)。 */
|
|
110
|
+
task_id?: string;
|
|
111
|
+
status: JobStatus;
|
|
112
|
+
duplicate: boolean;
|
|
113
|
+
/** true = 原对话已结束,这条消息开了一个新对话(job_id 是新的)。 */
|
|
114
|
+
new_conversation: boolean;
|
|
115
|
+
previous_job_id: string | null;
|
|
116
|
+
queue_position: number | null;
|
|
117
|
+
turn: CenterTurn;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** 新建对话时可带的参数(POST /api/jobs,mode=conversation)。 */
|
|
121
|
+
export interface NewConversationOptions {
|
|
122
|
+
title?: string;
|
|
123
|
+
/** 业务自己的 ID(tasks[0].business_id):第一轮的回调里原样带回,用来和业务数据对应。 */
|
|
124
|
+
businessId?: string;
|
|
125
|
+
workspace?: { name?: string; context?: string; branch?: string };
|
|
126
|
+
callback?: { url: string; code: string };
|
|
127
|
+
agent?: string;
|
|
128
|
+
timeoutSeconds?: number;
|
|
129
|
+
browser?: boolean;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── Operator:节点转录 ─────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
export type MsgRole = "user" | "assistant" | "tool" | "system" | "session_break";
|
|
135
|
+
|
|
136
|
+
export interface Message {
|
|
137
|
+
id: string;
|
|
138
|
+
task_id?: string;
|
|
139
|
+
generation: number;
|
|
140
|
+
role: MsgRole;
|
|
141
|
+
content: string;
|
|
142
|
+
created_at: number;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export interface PendingMessage {
|
|
146
|
+
id: string;
|
|
147
|
+
generation: number;
|
|
148
|
+
content: string;
|
|
149
|
+
created_at: number;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export interface Msg {
|
|
153
|
+
id: string;
|
|
154
|
+
/** queued:中枢里还在排队、没送到节点的消息(界面上是可撤回的「排队中」气泡)。 */
|
|
155
|
+
role: MsgRole | "queued";
|
|
156
|
+
content: string;
|
|
157
|
+
generation: number;
|
|
158
|
+
toolId?: string;
|
|
159
|
+
ts?: number;
|
|
160
|
+
/** queued 气泡对应的中枢轮次,撤回时用。 */
|
|
161
|
+
turnId?: string;
|
|
162
|
+
/** queued 气泡:中枢给出的等待原因(节点连不上、没有可用节点……),正常排队时为空。 */
|
|
163
|
+
note?: string;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export interface AskOption { label: string; description?: string }
|
|
167
|
+
export interface AskQuestion { question: string; header: string; multiSelect?: boolean; options: AskOption[] }
|
|
168
|
+
export type AskAnswers = string[][];
|
|
169
|
+
|
|
170
|
+
export interface InputField { label: string; secret?: boolean; options?: string[]; placeholder?: string }
|
|
171
|
+
export interface InputRequest {
|
|
172
|
+
id: string;
|
|
173
|
+
message: string;
|
|
174
|
+
fields: InputField[];
|
|
175
|
+
status: "pending" | "answered" | "cancelled";
|
|
176
|
+
values?: string[];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export type DiffLine = { sign: "+" | "-" | " "; text: string };
|
|
180
|
+
|
|
181
|
+
export type ToolPeek =
|
|
182
|
+
| { kind: "count"; text: string }
|
|
183
|
+
| { kind: "diff"; added: number; removed: number; label?: string; lines: DiffLine[]; truncated?: number }
|
|
184
|
+
| { kind: "lines"; label?: string; lines: string[]; truncated?: number }
|
|
185
|
+
| { kind: "todos"; items: { text: string; status: string }[] };
|
|
186
|
+
|
|
187
|
+
export interface ToolData {
|
|
188
|
+
title: string;
|
|
189
|
+
detail?: string;
|
|
190
|
+
result?: string;
|
|
191
|
+
isError?: boolean;
|
|
192
|
+
peek?: ToolPeek;
|
|
193
|
+
diff?: DiffLine[];
|
|
194
|
+
ask?: { id: string; questions: AskQuestion[]; answers?: AskAnswers };
|
|
195
|
+
input?: InputRequest;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export interface TurnUsage {
|
|
199
|
+
cost_usd: number;
|
|
200
|
+
input_tokens: number;
|
|
201
|
+
output_tokens: number;
|
|
202
|
+
cache_read_tokens: number;
|
|
203
|
+
cache_creation_tokens: number;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** 节点回合事件(center 流里 `{type:"node", event}` 的 event)。 */
|
|
207
|
+
export type NodeEvent =
|
|
208
|
+
| { type: "user"; content: string; msgId: string; generation: number; ts?: number }
|
|
209
|
+
| { type: "queued"; msgId: string; content: string; generation: number; ts?: number }
|
|
210
|
+
| { type: "dequeued"; msgId: string }
|
|
211
|
+
| { type: "model"; model: string }
|
|
212
|
+
| { type: "assistant"; content: string; msgId?: string; generation?: number; ts?: number }
|
|
213
|
+
| { type: "tool"; id: string; title: string; detail: string; peek?: ToolPeek; diff?: DiffLine[]; msgId?: string; generation?: number }
|
|
214
|
+
| { type: "tool_result"; id: string; content: string; isError: boolean; peek?: ToolPeek; msgId?: string }
|
|
215
|
+
| { type: "ask"; id: string; questions: AskQuestion[]; msgId?: string; generation?: number }
|
|
216
|
+
| { type: "ask_answered"; id: string; answers: AskAnswers; msgId?: string }
|
|
217
|
+
| { type: "input"; input: InputRequest; msgId?: string; generation?: number }
|
|
218
|
+
| { type: "usage"; usage: TurnUsage }
|
|
219
|
+
| { type: "notice"; content: string; msgId?: string; generation?: number }
|
|
220
|
+
| { type: "error"; content: string; msgId?: string; generation?: number }
|
|
221
|
+
| { type: "turn_end" }
|
|
222
|
+
| { type: string; [key: string]: unknown };
|
|
223
|
+
|
|
224
|
+
/** center 实时流 chunk.events 里的一项。 */
|
|
225
|
+
export type StreamItem =
|
|
226
|
+
| { type: "transcript"; messages: Message[] | null; pending: PendingMessage[] | null; running: boolean | null }
|
|
227
|
+
| { type: "node"; event: NodeEvent }
|
|
228
|
+
| { type: "turn"; turn: CenterTurn }
|
|
229
|
+
| { type: "job"; job: { id: string; status: JobStatus; closed_reason: string | null; node_id: string | null } }
|
|
230
|
+
/** 正在看的这个对话的状态变化(关闭了:closed=true)。 */
|
|
231
|
+
| { type: "task"; task: StreamTask };
|
|
232
|
+
|
|
233
|
+
/** 实时流看的那个对话(GET /api/jobs/:id/stream?task=)。 */
|
|
234
|
+
export interface StreamTask { id: string; seq: number; title: string; status: string; closed: boolean; remote_task_id: string | null; continue_from_task_id: string | null }
|
|
235
|
+
|
|
236
|
+
export interface StreamSnapshot {
|
|
237
|
+
job: { id: string; status: JobStatus; closed_reason: string | null; node_id: string | null; mode: string; title: string; conversation: ConversationSummary };
|
|
238
|
+
/** 旧版 center 没有。 */
|
|
239
|
+
task?: StreamTask;
|
|
240
|
+
turns: CenterTurn[];
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export interface StreamEnd {
|
|
244
|
+
/** 对话任务的结束原因;只是这个对话被关闭时为 conversation_closed。 */
|
|
245
|
+
reason: string;
|
|
246
|
+
task_id?: string;
|
|
247
|
+
continued_by_job_id?: string | null;
|
|
248
|
+
/** 接续这个对话的新对话(新对话任务里的子任务);还没有为 null。 */
|
|
249
|
+
continued_by_task_id?: string | null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ── center 回调体(与中枢 conversation.ts turnCallbackPayload / job-callback.ts 的字段一一对应) ──
|
|
253
|
+
//
|
|
254
|
+
// 组件在前端发出的 conversation_turn / conversation_closed 事件,载荷与中枢投递给 callback.url 的请求体同名同义,
|
|
255
|
+
// 业务方可以用同一套代码处理「后端回调」和「前端事件」。前端拿不到的字段(event_id、callback_seq、cumulative、
|
|
256
|
+
// carried)不出现;以后端回调为准落库,前端事件只用来刷新界面。
|
|
257
|
+
|
|
258
|
+
/** 每轮一次:type = "conversation_turn"。 */
|
|
259
|
+
export interface ConversationTurnCallback {
|
|
260
|
+
type: "conversation_turn";
|
|
261
|
+
/** 投递顺序号(= seq)。同一对话里按它从小到大处理;结束事件是最后一轮 + 1。 */
|
|
262
|
+
callback_seq: number;
|
|
263
|
+
job_id: string;
|
|
264
|
+
task_id: string;
|
|
265
|
+
turn_id: string;
|
|
266
|
+
seq: number;
|
|
267
|
+
business_id: string | null;
|
|
268
|
+
/** succeeded / failed / cancelled(被合并的消息不单独回调,见 messages)。 */
|
|
269
|
+
status: TurnStatus;
|
|
270
|
+
occurred_at: string;
|
|
271
|
+
/** 实际发给 Agent 的正文(合并了排队消息时是拼起来的全文)。 */
|
|
272
|
+
input: string;
|
|
273
|
+
/** 这一轮包含的全部消息,按发送顺序(排队的消息会合并进来)。 */
|
|
274
|
+
messages: Array<{ turn_id: string; seq: number; business_id: string | null; input: string }>;
|
|
275
|
+
reply: string | null;
|
|
276
|
+
/** true = 这一轮以 Agent 提问结束(reply 是问题),status 仍是 succeeded;下一条消息就是回答。 */
|
|
277
|
+
asked: boolean;
|
|
278
|
+
/** 失败 / 取消原因(中枢原文)。 */
|
|
279
|
+
message: string | null;
|
|
280
|
+
continued_from_job_id: string | null;
|
|
281
|
+
context_reset: boolean;
|
|
282
|
+
merged: boolean;
|
|
283
|
+
merge_error: string | null;
|
|
284
|
+
agent: string | null;
|
|
285
|
+
resolved_model: string | null;
|
|
286
|
+
total_tokens: number;
|
|
287
|
+
input_tokens: number;
|
|
288
|
+
output_tokens: number;
|
|
289
|
+
cache_read_tokens: number;
|
|
290
|
+
cache_creation_tokens: number;
|
|
291
|
+
started_at: string | null;
|
|
292
|
+
ended_at: string | null;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** 对话结束一次:type = "conversation_closed"。 */
|
|
296
|
+
export interface ConversationClosedCallback {
|
|
297
|
+
type: "conversation_closed";
|
|
298
|
+
mode: "conversation";
|
|
299
|
+
/** 投递顺序号:最后一轮的 seq + 1,永远排在这个对话所有 conversation_turn 之后。 */
|
|
300
|
+
callback_seq: number;
|
|
301
|
+
job_id: string;
|
|
302
|
+
/** 只关掉了对话任务里的这一个对话时才有(前端事件;center 只在整个对话任务结束时回调)。 */
|
|
303
|
+
task_id?: string;
|
|
304
|
+
/** succeeded(关闭 / 过期 / 换线)/ cancelled / failed。 */
|
|
305
|
+
status: JobStatus;
|
|
306
|
+
/** closed / cancelled / expired / node_reassigned / node_lost。 */
|
|
307
|
+
closed_reason: string | null;
|
|
308
|
+
/** 一共几轮(= 最大的 seq,被合并的消息也算)。 */
|
|
309
|
+
turns: number;
|
|
310
|
+
/** 接续出来的新对话(过期、换线、节点丢失后再发消息时才有)。 */
|
|
311
|
+
continued_by_job_id: string | null;
|
|
312
|
+
occurred_at: string;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export type ConversationCallback = ConversationTurnCallback | ConversationClosedCallback;
|
|
316
|
+
|
|
317
|
+
/** 一轮结束时 onTurnEnd 收到的载荷(字段与 center 的 conversation_turn 回调体一致)。 */
|
|
318
|
+
export type TurnEndPayload = ConversationTurnCallback;
|
|
319
|
+
/** 对话结束时 onConversationEnd 收到的载荷(字段与 center 的 conversation_closed 回调体一致)。 */
|
|
320
|
+
export type ConversationEndPayload = ConversationClosedCallback;
|
|
321
|
+
|
|
322
|
+
// ── 右侧看板(cli-board):center 的 /api/jobs/:id/board、files、diff、browser ──
|
|
323
|
+
|
|
324
|
+
export interface BoardInfo {
|
|
325
|
+
job_id: string;
|
|
326
|
+
/** false = 还没派发到执行节点(文件和浏览器都还没有)。 */
|
|
327
|
+
dispatched: boolean;
|
|
328
|
+
detail?: string;
|
|
329
|
+
task_id?: string;
|
|
330
|
+
remote_task_id?: string;
|
|
331
|
+
node?: { id: string; health: string };
|
|
332
|
+
files: boolean;
|
|
333
|
+
/** 节点开了浏览器功能。 */
|
|
334
|
+
browser: boolean;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export interface FileEntry {
|
|
338
|
+
name: string;
|
|
339
|
+
/** 相对工作目录的路径,用 / 分隔。 */
|
|
340
|
+
path: string;
|
|
341
|
+
type: "dir" | "file";
|
|
342
|
+
size: number;
|
|
343
|
+
mtime: number;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export interface DirListing { type: "dir"; path: string; entries: FileEntry[]; truncated: boolean }
|
|
347
|
+
export interface FileInfo { type: "file"; path: string; name: string; size: number; mtime: number; contentType: string; inline: boolean }
|
|
348
|
+
|
|
349
|
+
export interface DiffFile { path: string; status: string; additions: number; deletions: number; binary: boolean; patch: string; truncated?: boolean }
|
|
350
|
+
/** Operator 的 /api/tasks/:id/diff(经 center 原样转发)。 */
|
|
351
|
+
export interface DiffResult {
|
|
352
|
+
isolated?: boolean;
|
|
353
|
+
reason?: string;
|
|
354
|
+
branch?: string;
|
|
355
|
+
baseLabel?: string;
|
|
356
|
+
files?: DiffFile[];
|
|
357
|
+
isDirty?: boolean;
|
|
358
|
+
ahead?: number;
|
|
359
|
+
alreadyMerged?: boolean;
|
|
360
|
+
error?: string;
|
|
361
|
+
/** 有的节点只给一整段 unified diff。 */
|
|
362
|
+
diff?: string;
|
|
363
|
+
patch?: string;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** Operator 的任务浏览器状态(/api/tasks/:id/browser)。 */
|
|
367
|
+
export interface BrowserStatus {
|
|
368
|
+
feature: boolean;
|
|
369
|
+
state: "none" | "opening" | "running" | "closing";
|
|
370
|
+
origin: "ai" | "manual" | "check" | null;
|
|
371
|
+
control: "ai" | "human";
|
|
372
|
+
handoff: { instruction: string; by: "ai" | "user" | "browser" | "kept" } | null;
|
|
373
|
+
capability: "config" | "manual" | "ai" | null;
|
|
374
|
+
viewers: number;
|
|
375
|
+
spaceName: string;
|
|
376
|
+
instance: { running: boolean; spaces: number };
|
|
377
|
+
shownLocally: boolean;
|
|
378
|
+
error: string | null;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** 浏览器画面流里的一条消息(节点原样转发)。 */
|
|
382
|
+
export type BrowserMessage =
|
|
383
|
+
| { type: "frame"; data: string; metadata?: { deviceWidth: number; deviceHeight: number } }
|
|
384
|
+
| { type: "page"; url?: string; title?: string }
|
|
385
|
+
| { type: "error"; message?: string };
|
package/src/elements.tsx
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web Component 版:任何页面(不用 React)都能直接放 <cli-msglist> / <cli-chat>。
|
|
3
|
+
*
|
|
4
|
+
* import "@spzxg/cli-ui/styles.css";
|
|
5
|
+
* import { defineCliElements } from "@spzxg/cli-ui/elements";
|
|
6
|
+
* defineCliElements();
|
|
7
|
+
*
|
|
8
|
+
* <cli-msglist group="a" base-url="/api/center" title="战略"></cli-msglist>
|
|
9
|
+
* <cli-chat group="a"></cli-chat>
|
|
10
|
+
* <cli-board group="a"></cli-board> (右侧看板:文件 / 改动 / 浏览器;对话里点文件会在这里打开)
|
|
11
|
+
* <cli-file-preview job-id="job_x" path="outputs/报告.md"></cli-file-preview> (单独的文件预览 + 下载)
|
|
12
|
+
* <cli-browser job-id="job_x"></cli-browser> (单独的浏览器实时画面)
|
|
13
|
+
*
|
|
14
|
+
* 同一个 group 的元素共享一个仓库:列表点哪个,对话就显示哪个;不写 group 各自独立。
|
|
15
|
+
* 属性:base-url、token、line、namespace、group、job-id、task-id(对话任务里的哪个对话,"new" = 新开一个;对话 / 看板)、
|
|
16
|
+
* title、color、theme、context、read-only、
|
|
17
|
+
* tabs(看板页签,逗号分隔:files,changes,browser)、default-tab、file-open(对话:auto / board / modal / link)、
|
|
18
|
+
* config(界面配置 JSON)、new-conversation(新建对话参数 JSON:title / businessId / workspace / callback / agent …)。
|
|
19
|
+
* 界面配置也可以直接赋对象:document.querySelector("cli-chat").config = { header: { export: false } }。
|
|
20
|
+
* 事件(全在浏览器里从实时流算出,和 center 的 callback.url 无关):
|
|
21
|
+
* cli-turn-end(一轮结束)、cli-conversation-end(对话结束),detail 字段与 center 回调体一致;
|
|
22
|
+
* cli-event(全部事件:select / conversation_turn / conversation_closed / job_change / open / error);
|
|
23
|
+
* cli-callback 是旧名字,等同于前两个合在一起,保留兼容。
|
|
24
|
+
* 渲染在 light DOM 里(不用 Shadow DOM),样式靠页面引入 styles.css,作用域都在 .cli-ui 内。
|
|
25
|
+
*/
|
|
26
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
27
|
+
import { CliMsgList } from "./msglist/CliMsgList.js";
|
|
28
|
+
import { CliChat } from "./chat/CliChat.js";
|
|
29
|
+
import { CliBoard, type BoardTab } from "./board/CliBoard.js";
|
|
30
|
+
import { CliBrowserView, CliFilePreview } from "./board/blocks.js";
|
|
31
|
+
import { createCliStore, type CliStore } from "./core/store.js";
|
|
32
|
+
|
|
33
|
+
const groups = new Map<string, CliStore>();
|
|
34
|
+
|
|
35
|
+
function storeFor(el: HTMLElement): CliStore | undefined {
|
|
36
|
+
const group = el.getAttribute("group");
|
|
37
|
+
if (!group) return undefined;
|
|
38
|
+
const baseUrl = el.getAttribute("base-url") ?? undefined;
|
|
39
|
+
const key = `${group}|${baseUrl ?? ""}`;
|
|
40
|
+
let store = groups.get(key);
|
|
41
|
+
if (!store) {
|
|
42
|
+
store = createCliStore(storeOptions(el));
|
|
43
|
+
groups.set(key, store);
|
|
44
|
+
}
|
|
45
|
+
return store;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const ATTRS = ["base-url", "token", "line", "namespace", "group", "job-id", "task-id", "title", "color", "theme", "context", "read-only", "config", "new-conversation", "tabs", "default-tab", "file-open", "path"];
|
|
49
|
+
|
|
50
|
+
function jsonAttr(el: HTMLElement, name: string): object | undefined {
|
|
51
|
+
const raw = el.getAttribute(name);
|
|
52
|
+
if (!raw) return undefined;
|
|
53
|
+
try { return JSON.parse(raw) as object; } catch { console.warn(`<${el.localName}> 的 ${name} 不是合法 JSON,已忽略`); return undefined; }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function storeOptions(el: HTMLElement) {
|
|
57
|
+
return {
|
|
58
|
+
baseUrl: el.getAttribute("base-url") ?? undefined,
|
|
59
|
+
token: el.getAttribute("token") ?? undefined,
|
|
60
|
+
line: el.getAttribute("line") ?? undefined,
|
|
61
|
+
namespace: el.getAttribute("namespace") ?? el.getAttribute("group") ?? undefined,
|
|
62
|
+
newConversation: jsonAttr(el, "new-conversation"),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
abstract class CliElement extends HTMLElement {
|
|
67
|
+
static observedAttributes = ATTRS;
|
|
68
|
+
private root: Root | null = null;
|
|
69
|
+
private ownStore: CliStore | undefined;
|
|
70
|
+
private configValue: object | undefined;
|
|
71
|
+
|
|
72
|
+
/** 界面配置(对象)。优先于 config 属性里的 JSON。 */
|
|
73
|
+
get config(): object | undefined {
|
|
74
|
+
return this.configValue ?? jsonAttr(this, "config");
|
|
75
|
+
}
|
|
76
|
+
set config(value: object | undefined) {
|
|
77
|
+
this.configValue = value;
|
|
78
|
+
if (this.root) this.render();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
connectedCallback() {
|
|
82
|
+
// 缺省撑满父容器;组件自己内部滚动(宿主给定高度即可)。
|
|
83
|
+
this.style.display ||= "block";
|
|
84
|
+
this.style.height ||= "100%";
|
|
85
|
+
this.style.minHeight ||= "0";
|
|
86
|
+
this.root ??= createRoot(this);
|
|
87
|
+
this.render();
|
|
88
|
+
}
|
|
89
|
+
disconnectedCallback() {
|
|
90
|
+
// 移动节点时会先断开再连上:推到下一轮再卸载。
|
|
91
|
+
queueMicrotask(() => { if (!this.isConnected) { this.root?.unmount(); this.root = null; } });
|
|
92
|
+
}
|
|
93
|
+
attributeChangedCallback() { if (this.root) this.render(); }
|
|
94
|
+
|
|
95
|
+
protected common() {
|
|
96
|
+
const store = storeFor(this) ?? (this.ownStore ??= createCliStore(storeOptions(this)));
|
|
97
|
+
return {
|
|
98
|
+
store,
|
|
99
|
+
title: this.getAttribute("title") ?? undefined,
|
|
100
|
+
color: this.getAttribute("color") ?? undefined,
|
|
101
|
+
theme: (this.getAttribute("theme") as "light" | "dark" | null) ?? undefined,
|
|
102
|
+
onEvent: (event: unknown) => this.dispatchEvent(new CustomEvent("cli-event", { detail: event, bubbles: true })),
|
|
103
|
+
onTurnEnd: (payload: unknown) => this.dispatchEvent(new CustomEvent("cli-turn-end", { detail: payload, bubbles: true })),
|
|
104
|
+
onConversationEnd: (payload: unknown) => this.dispatchEvent(new CustomEvent("cli-conversation-end", { detail: payload, bubbles: true })),
|
|
105
|
+
onCallback: (payload: unknown) => this.dispatchEvent(new CustomEvent("cli-callback", { detail: payload, bubbles: true })),
|
|
106
|
+
style: { height: "100%" },
|
|
107
|
+
config: this.config as never,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
protected abstract element(): React.ReactNode;
|
|
111
|
+
private render() { this.root?.render(this.element()); }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
class CliMsgListElement extends CliElement {
|
|
115
|
+
protected element() {
|
|
116
|
+
const context = this.getAttribute("context");
|
|
117
|
+
// 结果事件只由 <cli-chat> 发:同一个 group 里列表和对话共用一个仓库,两边都发的话 document 上会收到两遍。
|
|
118
|
+
const { onTurnEnd: _a, onConversationEnd: _b, onCallback: _c, ...common } = this.common();
|
|
119
|
+
return <CliMsgList {...common} {...(context !== null ? { context } : {})} />;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
class CliChatElement extends CliElement {
|
|
124
|
+
protected element() {
|
|
125
|
+
const jobId = this.getAttribute("job-id");
|
|
126
|
+
const taskId = this.getAttribute("task-id");
|
|
127
|
+
const fileOpen = this.getAttribute("file-open") as "auto" | "board" | "modal" | "link" | null;
|
|
128
|
+
return <CliChat {...this.common()} {...(jobId !== null ? { jobId: jobId || null } : {})} {...(taskId ? { taskId } : {})} readOnly={this.hasAttribute("read-only")} {...(fileOpen ? { fileOpen } : {})} />;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
class CliBoardElement extends CliElement {
|
|
133
|
+
protected element() {
|
|
134
|
+
const jobId = this.getAttribute("job-id");
|
|
135
|
+
const tabs = this.getAttribute("tabs")?.split(",").map((t) => t.trim()).filter(Boolean) as BoardTab[] | undefined;
|
|
136
|
+
const defaultTab = (this.getAttribute("default-tab") as BoardTab | null) ?? undefined;
|
|
137
|
+
// 结果事件只由 <cli-chat> 发(同上);看板只发 cli-event。
|
|
138
|
+
const { onTurnEnd: _a, onConversationEnd: _b, onCallback: _c, title: _t, color: _k, config: _g, ...common } = this.common();
|
|
139
|
+
const taskId = this.getAttribute("task-id");
|
|
140
|
+
return <CliBoard {...common} {...(jobId !== null ? { jobId: jobId || null } : {})} {...(taskId ? { taskId } : {})} tabs={tabs?.length ? tabs : undefined} defaultTab={defaultTab} readOnly={this.hasAttribute("read-only")} />;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
class CliFilePreviewElement extends CliElement {
|
|
145
|
+
protected element() {
|
|
146
|
+
const jobId = this.getAttribute("job-id");
|
|
147
|
+
const { onTurnEnd: _a, onConversationEnd: _b, onCallback: _c, title: _t, color: _k, config: _g, ...common } = this.common();
|
|
148
|
+
return <CliFilePreview {...common} {...(jobId !== null ? { jobId: jobId || null } : {})} path={this.getAttribute("path") ?? ""} />;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
class CliBrowserElement extends CliElement {
|
|
153
|
+
protected element() {
|
|
154
|
+
const jobId = this.getAttribute("job-id");
|
|
155
|
+
const { onTurnEnd: _a, onConversationEnd: _b, onCallback: _c, title: _t, color: _k, config: _g, ...common } = this.common();
|
|
156
|
+
return <CliBrowserView {...common} {...(jobId !== null ? { jobId: jobId || null } : {})} readOnly={this.hasAttribute("read-only")} />;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function defineCliElements() {
|
|
161
|
+
if (!customElements.get("cli-msglist")) customElements.define("cli-msglist", CliMsgListElement);
|
|
162
|
+
if (!customElements.get("cli-chat")) customElements.define("cli-chat", CliChatElement);
|
|
163
|
+
if (!customElements.get("cli-board")) customElements.define("cli-board", CliBoardElement);
|
|
164
|
+
if (!customElements.get("cli-file-preview")) customElements.define("cli-file-preview", CliFilePreviewElement);
|
|
165
|
+
if (!customElements.get("cli-browser")) customElements.define("cli-browser", CliBrowserElement);
|
|
166
|
+
}
|
package/src/embed.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 给第三方页面用的一体化入口:引入一个 JS 和一个 CSS 就能用,不需要 React、打包工具或 TypeScript。
|
|
3
|
+
*
|
|
4
|
+
* <link rel="stylesheet" href="/static/cli-ui/cli-ui.css">
|
|
5
|
+
* <script type="module" src="/static/cli-ui/cli-ui.js"></script>
|
|
6
|
+
* <cli-chat base-url="/api/center" job-id="job_xxx"></cli-chat>
|
|
7
|
+
*
|
|
8
|
+
* 引入后自动注册 <cli-chat> / <cli-msglist> / <cli-board>;想用 JS 控制(传回调函数、随时改参数)就用
|
|
9
|
+
* window.CliUI.mountChat / mountMsgList / mountBoard。几个 mount 要联动(对话里点文件在看板打开)时传同一个 store:
|
|
10
|
+
* const store = CliUI.createCliStore({ baseUrl: "/api/center" });
|
|
11
|
+
* CliUI.mountChat(a, { store }); CliUI.mountBoard(b, { store });
|
|
12
|
+
* 产物由 `npm run build:embed -w @spzxg/cli-ui` 生成在 dist/。
|
|
13
|
+
*/
|
|
14
|
+
import "./styles.css";
|
|
15
|
+
import { createElement } from "react";
|
|
16
|
+
import { createRoot } from "react-dom/client";
|
|
17
|
+
import { defineCliElements } from "./elements.js";
|
|
18
|
+
import { CliChat, type CliChatProps } from "./chat/CliChat.js";
|
|
19
|
+
import { CliMsgList, type CliMsgListProps } from "./msglist/CliMsgList.js";
|
|
20
|
+
import { CliBoard, type CliBoardProps } from "./board/CliBoard.js";
|
|
21
|
+
import { CliBrowserView, CliFilePreview, type CliBrowserViewProps, type CliFilePreviewProps } from "./board/blocks.js";
|
|
22
|
+
import { CliCenterProvider } from "./core/provider.js";
|
|
23
|
+
import { createCliStore } from "./core/store.js";
|
|
24
|
+
import { createCenterClient } from "./core/client.js";
|
|
25
|
+
|
|
26
|
+
export interface Mounted<P> {
|
|
27
|
+
/** 合并更新参数(只传要改的)。 */
|
|
28
|
+
update(next: Partial<P>): void;
|
|
29
|
+
unmount(): void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function mount<P extends object>(Component: (props: P) => unknown, el: Element, props: P): Mounted<P> {
|
|
33
|
+
const root = createRoot(el);
|
|
34
|
+
let current = props;
|
|
35
|
+
const render = () => root.render(createElement(Component as never, { style: { height: "100%" }, ...current }));
|
|
36
|
+
render();
|
|
37
|
+
return {
|
|
38
|
+
update(next) { current = { ...current, ...next }; render(); },
|
|
39
|
+
unmount() { root.unmount(); },
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** 在一个元素里挂对话组件。参数与 React 的 <CliChat> 相同(jobId、newConversation、onTurnEnd、config……)。 */
|
|
44
|
+
export const mountChat = (el: Element, props: CliChatProps = {}) => mount(CliChat, el, props);
|
|
45
|
+
/** 在一个元素里挂列表组件。参数与 React 的 <CliMsgList> 相同。 */
|
|
46
|
+
export const mountMsgList = (el: Element, props: CliMsgListProps = {}) => mount(CliMsgList, el, props);
|
|
47
|
+
/** 在一个元素里挂右侧看板。参数与 React 的 <CliBoard> 相同(jobId、tabs、fileActions……)。 */
|
|
48
|
+
export const mountBoard = (el: Element, props: CliBoardProps = {}) => mount(CliBoard, el, props);
|
|
49
|
+
/** 单独的文件预览 + 下载:{ jobId, path, baseUrl… }。 */
|
|
50
|
+
export const mountFilePreview = (el: Element, props: CliFilePreviewProps) => mount(CliFilePreview, el, props);
|
|
51
|
+
/** 单独的浏览器实时画面:{ jobId, readOnly, baseUrl… }。 */
|
|
52
|
+
export const mountBrowser = (el: Element, props: CliBrowserViewProps = {}) => mount(CliBrowserView, el, props);
|
|
53
|
+
|
|
54
|
+
export { defineCliElements, CliChat, CliMsgList, CliBoard, CliFilePreview, CliBrowserView, CliCenterProvider, createCliStore, createCenterClient };
|
|
55
|
+
|
|
56
|
+
defineCliElements();
|
|
57
|
+
const api = { mountChat, mountMsgList, mountBoard, mountFilePreview, mountBrowser, createCliStore, createCenterClient, defineCliElements };
|
|
58
|
+
(globalThis as { CliUI?: typeof api }).CliUI = api;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @spzxg/cli-ui —— 任务中心(cli-task-center)对话任务的前端积木,数据全部来自 center 服务。
|
|
3
|
+
*
|
|
4
|
+
* cli-msglist 消息对话列表(CliMsgList)
|
|
5
|
+
* cli-chat 对话(CliChat)
|
|
6
|
+
* cli-board 右侧看板:工作区文件(预览 / 下载)、改动、任务浏览器(CliBoard)
|
|
7
|
+
* 其中两个核心也能单独用:CliFilePreview(文件预览 + 下载)、CliBrowserView(浏览器实时画面)
|
|
8
|
+
*
|
|
9
|
+
* 单独用:
|
|
10
|
+
* <CliChat baseUrl="/api/center" jobId="job_xxx" />
|
|
11
|
+
* <CliChat baseUrl="/api/center" jobId="job_xxx" taskId="task_yyy" /> (对话任务里的某个对话;taskId="new" 新开一个)
|
|
12
|
+
* 组合用(同一个 Provider 里共享选中和实时流):
|
|
13
|
+
* <CliCenterProvider baseUrl="/api/center" namespace="strategy">
|
|
14
|
+
* <CliMsgList title="战略" />
|
|
15
|
+
* <CliChat />
|
|
16
|
+
* </CliCenterProvider>
|
|
17
|
+
*
|
|
18
|
+
* import "@spzxg/cli-ui/styles.css";
|
|
19
|
+
* 非 React 页面用 Web Component:import { defineCliElements } from "@spzxg/cli-ui/elements"。
|
|
20
|
+
*/
|
|
21
|
+
export { CliMsgList, type CliMsgListProps } from "./msglist/CliMsgList.js";
|
|
22
|
+
export { CliChat, type CliChatProps } from "./chat/CliChat.js";
|
|
23
|
+
export { CliBoard, type CliBoardProps, type BoardTab } from "./board/CliBoard.js";
|
|
24
|
+
export { CliFilePreview, CliBrowserView, type CliFilePreviewProps, type CliBrowserViewProps } from "./board/blocks.js";
|
|
25
|
+
export { FileViewer, downloadFile, type FileViewerProps, type FileAction } from "./board/FileViewer.js";
|
|
26
|
+
export { BrowserPane } from "./board/BrowserPane.js";
|
|
27
|
+
export { CliCenterProvider, useCliStore, useCliState, type CliCenterProviderProps, type ResultHandlers } from "./core/provider.js";
|
|
28
|
+
|
|
29
|
+
export {
|
|
30
|
+
resolveChatConfig, resolveMsgListConfig, DEFAULT_CHAT_CONFIG, DEFAULT_MSGLIST_CONFIG,
|
|
31
|
+
type CliChatConfig, type CliMsgListConfig, type ResolvedChatConfig, type ResolvedMsgListConfig,
|
|
32
|
+
} from "./core/config.js";
|
|
33
|
+
export { createCliStore, type CliStore, type CliStoreOptions, type CliState, type CliEvent, type JobEntry, type OpenTarget } from "./core/store.js";
|
|
34
|
+
export { createCenterClient, CenterError, boardFileUrl, type CenterClient, type CenterClientOptions, type StreamHandlers, type BoardBackend, type ConversationBackend } from "./core/client.js";
|
|
35
|
+
export { viewMessages, isRunning, runningTurn, convKey, parseConvKey, NEW_TASK, type ConvState } from "./core/conversation.js";
|
|
36
|
+
export {
|
|
37
|
+
composeMessage, splitMessage, serializeBlock, readFileText, isLongText, ACCEPTED_EXTENSIONS, MAX_MESSAGE_CHARS,
|
|
38
|
+
type TextBlock, type MessagePart,
|
|
39
|
+
} from "./core/blocks.js";
|
|
40
|
+
export type * from "./core/types.js";
|