@veltra/ai 2.1.6 → 2.1.8
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 +42 -0
- package/dist/chat/fold.d.ts +41 -0
- package/dist/chat/fold.js +208 -0
- package/dist/chat/fold.js.map +1 -0
- package/dist/chat/index.d.ts +4 -2
- package/dist/chat/session.d.ts +123 -0
- package/dist/chat/session.js +79 -0
- package/dist/chat/session.js.map +1 -0
- package/dist/chat/types.d.ts +38 -1
- package/dist/chat/use-chat-helpers.js +39 -0
- package/dist/chat/use-chat-helpers.js.map +1 -0
- package/dist/chat/use-chat-session.js +68 -0
- package/dist/chat/use-chat-session.js.map +1 -0
- package/dist/chat/use-chat.d.ts +50 -2
- package/dist/chat/use-chat.js +73 -39
- package/dist/chat/use-chat.js.map +1 -1
- package/dist/components/ai-chat/ai-chat.js +49 -14
- package/dist/components/ai-chat/ai-chat.js.map +1 -1
- package/dist/components/ai-chat/approval-banner.js +54 -0
- package/dist/components/ai-chat/approval-banner.js.map +1 -0
- package/dist/components/ai-chat/ask-question.js +32 -51
- package/dist/components/ai-chat/ask-question.js.map +1 -1
- package/dist/components/ai-chat/di.js.map +1 -1
- package/dist/components/ai-chat/job-bar.js +48 -0
- package/dist/components/ai-chat/job-bar.js.map +1 -0
- package/dist/components/ai-chat/message-list.js +5 -2
- package/dist/components/ai-chat/message-list.js.map +1 -1
- package/dist/components/ai-chat/queue-list.js +9 -3
- package/dist/components/ai-chat/queue-list.js.map +1 -1
- package/dist/components/ai-chat/side-panel.js +12 -30
- package/dist/components/ai-chat/side-panel.js.map +1 -1
- package/dist/components/ai-chat/style2.css +146 -8
- package/dist/components/ai-chat/tool-call.js +51 -40
- package/dist/components/ai-chat/tool-call.js.map +1 -1
- package/dist/components/ai-chat/tool-icons.js +31 -0
- package/dist/components/ai-chat/tool-icons.js.map +1 -0
- package/dist/components/ai-chat/tool-json.js +40 -0
- package/dist/components/ai-chat/tool-json.js.map +1 -0
- package/dist/components/ai-orb/style2.css +1 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.js +3 -1
- package/dist/tools/ask-question/ask-question.js +48 -2
- package/dist/tools/ask-question/ask-question.js.map +1 -1
- package/dist/types/ai-chat.d.ts +17 -6
- package/package.json +7 -7
- package/src/chat/__test__/fold.test.ts +136 -0
- package/src/chat/__test__/session.test.ts +191 -0
- package/src/chat/__test__/use-chat.test.ts +268 -1
- package/src/chat/fold.ts +260 -0
- package/src/chat/index.ts +2 -0
- package/src/chat/session.ts +168 -0
- package/src/chat/types.ts +40 -0
- package/src/chat/use-chat-helpers.ts +39 -0
- package/src/chat/use-chat-session.ts +87 -0
- package/src/chat/use-chat.ts +90 -44
- package/src/components/ai-chat/__test__/ai-chat.test.ts +405 -7
- package/src/components/ai-chat/__test__/tool-icons.test.ts +33 -0
- package/src/components/ai-chat/ai-chat.vue +39 -4
- package/src/components/ai-chat/approval-banner.vue +37 -0
- package/src/components/ai-chat/ask-question.vue +38 -54
- package/src/components/ai-chat/di.ts +6 -4
- package/src/components/ai-chat/job-bar.vue +41 -0
- package/src/components/ai-chat/message-list.vue +4 -1
- package/src/components/ai-chat/queue-list.vue +3 -1
- package/src/components/ai-chat/side-panel.vue +15 -13
- package/src/components/ai-chat/style.scss +158 -6
- package/src/components/ai-chat/tool-call.vue +46 -34
- package/src/components/ai-chat/tool-icons.ts +33 -0
- package/src/components/ai-chat/tool-json.vue +52 -0
- package/src/tools/ask-question/ask-question.ts +53 -4
- package/src/types/ai-chat.ts +17 -5
package/README.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# @veltra/ai
|
|
2
|
+
|
|
3
|
+
对话 UI(`UAiChat`)与 UI 无关的 `useChat` 状态机。按 transport 形态接入两种工具来源。
|
|
4
|
+
|
|
5
|
+
## 客户端驱动
|
|
6
|
+
|
|
7
|
+
传入函数型 `ChatTransport`(通常 `createOpenAITransport`)和带 `execute` 的 `ChatTool[]`。前端持有历史,每轮把全量消息发给 OpenAI 兼容 SSE 端点,并在本地执行工具、维护队列与 `needsConfirm`。
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { createOpenAITransport, type ChatTool } from '@veltra/ai'
|
|
11
|
+
|
|
12
|
+
const transport = createOpenAITransport({
|
|
13
|
+
providers: [{ id: 'openai', label: 'OpenAI', endpoint: '/ai/chat/completions', models: [...] }]
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
const tools: ChatTool[] = [
|
|
17
|
+
{
|
|
18
|
+
name: 'ping',
|
|
19
|
+
description: '探测',
|
|
20
|
+
parameters: { type: 'object', properties: {} },
|
|
21
|
+
execute: async () => ({ ok: true })
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
已有调用方无需改代码:`ChatTransport` / `ChatTool.execute` 必填签名不变。
|
|
27
|
+
|
|
28
|
+
## 服务端驱动
|
|
29
|
+
|
|
30
|
+
传入 `createServerTransport(adapter)`。`adapter` 只做协议翻译(订阅事件流、把 `send` / `cancel` / `respond` / `selectModel` / `fetchHistory` 打到远端)。包内完成 seq 校验、断线补拉、动作去重,再 fold 成消息 / 队列 / 作业。
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import { createServerTransport, type ChatSessionAdapter } from '@veltra/ai'
|
|
34
|
+
|
|
35
|
+
const transport = createServerTransport(adapter)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
此模式下 `tools` 只作按 `name` 的可选渲染元信息(`ChatToolMeta`),前端不定义、不执行工具。未注册的工具名走通用工具卡片。
|
|
39
|
+
|
|
40
|
+
## 本包不含 bedrock / DSH 桥
|
|
41
|
+
|
|
42
|
+
不实现、不导出 `createBedrockTransport`。DeepSeek harness(DSH)的 HTTP/WS 与 bedrock 适配由其它仓库提供 `ChatSessionAdapter`。
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { ChatJob, ChatMessage, ChatQueuedMessage, ChatTokenUsage } from "./types.js";
|
|
2
|
+
import { AskQuestionItem } from "../tools/ask-question/ask-question.js";
|
|
3
|
+
import "../tools/index.js";
|
|
4
|
+
import { ChatSessionEvent } from "./session.js";
|
|
5
|
+
//#region src/chat/fold.d.ts
|
|
6
|
+
interface ChatPendingApproval {
|
|
7
|
+
approvalId: string;
|
|
8
|
+
toolName: string;
|
|
9
|
+
callId?: string;
|
|
10
|
+
reason?: string;
|
|
11
|
+
rpcId: string;
|
|
12
|
+
}
|
|
13
|
+
/** 历史回放与实时 onEvent 共用的折叠状态 */
|
|
14
|
+
interface ChatFoldState {
|
|
15
|
+
lastSeq: number | null;
|
|
16
|
+
messages: ChatMessage[];
|
|
17
|
+
queue: ChatQueuedMessage[];
|
|
18
|
+
jobs: ChatJob[];
|
|
19
|
+
tokenUsage: ChatTokenUsage | null;
|
|
20
|
+
projections: Record<string, unknown>;
|
|
21
|
+
title: string | null;
|
|
22
|
+
running: boolean;
|
|
23
|
+
pendingApprovals: ChatPendingApproval[];
|
|
24
|
+
pendingQuestion: {
|
|
25
|
+
questions: AskQuestionItem[];
|
|
26
|
+
rpcId: string;
|
|
27
|
+
} | null;
|
|
28
|
+
error: {
|
|
29
|
+
code: string;
|
|
30
|
+
message: string;
|
|
31
|
+
} | null;
|
|
32
|
+
}
|
|
33
|
+
declare function createFoldState(): ChatFoldState;
|
|
34
|
+
/**
|
|
35
|
+
* 纯函数 fold:历史回放与实时 onEvent 走同一套。
|
|
36
|
+
* 后到的更小/重复 seq 不覆盖已应用事件。
|
|
37
|
+
*/
|
|
38
|
+
declare function foldSessionEvent(state: ChatFoldState, event: ChatSessionEvent): ChatFoldState;
|
|
39
|
+
//#endregion
|
|
40
|
+
export { ChatFoldState, ChatPendingApproval, createFoldState, foldSessionEvent };
|
|
41
|
+
//# sourceMappingURL=fold.d.ts.map
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
//#region src/chat/fold.ts
|
|
2
|
+
function eventSeq(event) {
|
|
3
|
+
return "seq" in event ? event.seq : void 0;
|
|
4
|
+
}
|
|
5
|
+
function createFoldState() {
|
|
6
|
+
return {
|
|
7
|
+
lastSeq: null,
|
|
8
|
+
messages: [],
|
|
9
|
+
queue: [],
|
|
10
|
+
jobs: [],
|
|
11
|
+
tokenUsage: null,
|
|
12
|
+
projections: {},
|
|
13
|
+
title: null,
|
|
14
|
+
running: false,
|
|
15
|
+
pendingApprovals: [],
|
|
16
|
+
pendingQuestion: null,
|
|
17
|
+
error: null
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function mergeOptionalCount(a, b) {
|
|
21
|
+
if (a == null && b == null) return void 0;
|
|
22
|
+
return (a ?? 0) + (b ?? 0);
|
|
23
|
+
}
|
|
24
|
+
function addTokenUsage(a, b) {
|
|
25
|
+
const usage = {
|
|
26
|
+
promptTokens: a.promptTokens + b.promptTokens,
|
|
27
|
+
completionTokens: a.completionTokens + b.completionTokens,
|
|
28
|
+
totalTokens: a.totalTokens + b.totalTokens
|
|
29
|
+
};
|
|
30
|
+
const cacheHitTokens = mergeOptionalCount(a.cacheHitTokens, b.cacheHitTokens);
|
|
31
|
+
if (cacheHitTokens != null) usage.cacheHitTokens = cacheHitTokens;
|
|
32
|
+
const cacheMissTokens = mergeOptionalCount(a.cacheMissTokens, b.cacheMissTokens);
|
|
33
|
+
if (cacheMissTokens != null) usage.cacheMissTokens = cacheMissTokens;
|
|
34
|
+
const cacheCreationTokens = mergeOptionalCount(a.cacheCreationTokens, b.cacheCreationTokens);
|
|
35
|
+
if (cacheCreationTokens != null) usage.cacheCreationTokens = cacheCreationTokens;
|
|
36
|
+
return usage;
|
|
37
|
+
}
|
|
38
|
+
function isTokenUsage(value) {
|
|
39
|
+
if (value == null || typeof value !== "object") return false;
|
|
40
|
+
const usage = value;
|
|
41
|
+
return typeof usage.promptTokens === "number" && typeof usage.completionTokens === "number" && typeof usage.totalTokens === "number";
|
|
42
|
+
}
|
|
43
|
+
function replaceAt(list, idx, item) {
|
|
44
|
+
const next = list.slice();
|
|
45
|
+
next[idx] = item;
|
|
46
|
+
return next;
|
|
47
|
+
}
|
|
48
|
+
function patchMessage(messages, id, patch) {
|
|
49
|
+
const idx = messages.findIndex((m) => m.id === id);
|
|
50
|
+
if (idx < 0) return [...messages, {
|
|
51
|
+
id,
|
|
52
|
+
role: "assistant",
|
|
53
|
+
content: "",
|
|
54
|
+
status: "streaming",
|
|
55
|
+
...patch
|
|
56
|
+
}];
|
|
57
|
+
return replaceAt(messages, idx, {
|
|
58
|
+
...messages[idx],
|
|
59
|
+
...patch
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function appendToolCall(messages, call) {
|
|
63
|
+
const idx = messages.findLastIndex((m) => m.role === "assistant");
|
|
64
|
+
if (idx < 0) return [...messages, {
|
|
65
|
+
id: `tool-host:${call.id}`,
|
|
66
|
+
role: "assistant",
|
|
67
|
+
content: "",
|
|
68
|
+
status: "streaming",
|
|
69
|
+
toolCalls: [call]
|
|
70
|
+
}];
|
|
71
|
+
const target = messages[idx];
|
|
72
|
+
return replaceAt(messages, idx, {
|
|
73
|
+
...target,
|
|
74
|
+
toolCalls: [...target.toolCalls ?? [], call]
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
function updateToolCall(messages, callId, patch) {
|
|
78
|
+
const idx = messages.findIndex((m) => m.toolCalls?.some((c) => c.id === callId));
|
|
79
|
+
if (idx < 0) return messages;
|
|
80
|
+
const msg = messages[idx];
|
|
81
|
+
const calls = (msg.toolCalls ?? []).slice();
|
|
82
|
+
const callIdx = calls.findIndex((c) => c.id === callId);
|
|
83
|
+
if (callIdx < 0) return messages;
|
|
84
|
+
calls[callIdx] = {
|
|
85
|
+
...calls[callIdx],
|
|
86
|
+
...patch
|
|
87
|
+
};
|
|
88
|
+
return replaceAt(messages, idx, {
|
|
89
|
+
...msg,
|
|
90
|
+
toolCalls: calls
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* 纯函数 fold:历史回放与实时 onEvent 走同一套。
|
|
95
|
+
* 后到的更小/重复 seq 不覆盖已应用事件。
|
|
96
|
+
*/
|
|
97
|
+
function foldSessionEvent(state, event) {
|
|
98
|
+
const seq = eventSeq(event);
|
|
99
|
+
if (seq != null && state.lastSeq != null && seq <= state.lastSeq) return state;
|
|
100
|
+
const next = {
|
|
101
|
+
...state,
|
|
102
|
+
lastSeq: seq ?? state.lastSeq,
|
|
103
|
+
projections: { ...state.projections }
|
|
104
|
+
};
|
|
105
|
+
switch (event.type) {
|
|
106
|
+
case "user/message": {
|
|
107
|
+
if (next.messages.some((m) => m.id === event.messageId)) break;
|
|
108
|
+
const userMessage = {
|
|
109
|
+
id: event.messageId,
|
|
110
|
+
role: "user",
|
|
111
|
+
content: event.content
|
|
112
|
+
};
|
|
113
|
+
if (event.attachments) userMessage.attachments = event.attachments;
|
|
114
|
+
next.messages = [...next.messages, userMessage];
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
case "assistant/chunk": {
|
|
118
|
+
const current = next.messages.find((m) => m.id === event.messageId);
|
|
119
|
+
next.messages = patchMessage(next.messages, event.messageId, {
|
|
120
|
+
role: "assistant",
|
|
121
|
+
status: "streaming",
|
|
122
|
+
content: (current?.content ?? "") + event.delta,
|
|
123
|
+
reasoning: event.reasoningDelta ? (current?.reasoning ?? "") + event.reasoningDelta : current?.reasoning
|
|
124
|
+
});
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
case "assistant/message":
|
|
128
|
+
next.messages = patchMessage(next.messages, event.messageId, {
|
|
129
|
+
role: "assistant",
|
|
130
|
+
status: "done",
|
|
131
|
+
content: event.content,
|
|
132
|
+
reasoning: event.reasoning,
|
|
133
|
+
toolCalls: event.toolCalls ?? next.messages.find((m) => m.id === event.messageId)?.toolCalls
|
|
134
|
+
});
|
|
135
|
+
break;
|
|
136
|
+
case "tool/call": {
|
|
137
|
+
const call = {
|
|
138
|
+
id: event.callId,
|
|
139
|
+
name: event.name,
|
|
140
|
+
arguments: event.arguments,
|
|
141
|
+
status: "pending"
|
|
142
|
+
};
|
|
143
|
+
if (event.view !== void 0) call.view = event.view;
|
|
144
|
+
next.messages = appendToolCall(next.messages, call);
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
case "tool/result": {
|
|
148
|
+
const patch = { status: event.status };
|
|
149
|
+
if (event.result !== void 0) patch.result = event.result;
|
|
150
|
+
if (event.error !== void 0) patch.error = event.error;
|
|
151
|
+
if (event.view !== void 0) patch.view = event.view;
|
|
152
|
+
next.messages = updateToolCall(next.messages, event.callId, patch);
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
case "queue/snapshot":
|
|
156
|
+
next.queue = event.items.slice();
|
|
157
|
+
break;
|
|
158
|
+
case "jobs/snapshot":
|
|
159
|
+
next.jobs = event.jobs.slice();
|
|
160
|
+
break;
|
|
161
|
+
case "projection":
|
|
162
|
+
if (event.key === "tokenUsage" && isTokenUsage(event.value)) next.tokenUsage = next.tokenUsage ? addTokenUsage(next.tokenUsage, event.value) : event.value;
|
|
163
|
+
else if (event.key === "title" && typeof event.value === "string") next.title = event.value;
|
|
164
|
+
else next.projections[event.key] = event.value;
|
|
165
|
+
break;
|
|
166
|
+
case "running":
|
|
167
|
+
next.running = event.running;
|
|
168
|
+
break;
|
|
169
|
+
case "finish":
|
|
170
|
+
next.running = false;
|
|
171
|
+
break;
|
|
172
|
+
case "error":
|
|
173
|
+
next.running = false;
|
|
174
|
+
next.error = {
|
|
175
|
+
code: event.code,
|
|
176
|
+
message: event.message
|
|
177
|
+
};
|
|
178
|
+
break;
|
|
179
|
+
case "approval/requested":
|
|
180
|
+
if (event.callId) next.messages = updateToolCall(next.messages, event.callId, { status: "awaiting-confirm" });
|
|
181
|
+
next.pendingApprovals = [...next.pendingApprovals, {
|
|
182
|
+
approvalId: event.approvalId,
|
|
183
|
+
toolName: event.toolName,
|
|
184
|
+
callId: event.callId,
|
|
185
|
+
reason: event.reason,
|
|
186
|
+
rpcId: event.rpcId
|
|
187
|
+
}];
|
|
188
|
+
break;
|
|
189
|
+
case "approval/resolved": {
|
|
190
|
+
const hit = next.pendingApprovals.find((a) => a.approvalId === event.approvalId);
|
|
191
|
+
next.pendingApprovals = next.pendingApprovals.filter((a) => a.approvalId !== event.approvalId);
|
|
192
|
+
if (hit?.callId && event.outcome === "rejected") next.messages = updateToolCall(next.messages, hit.callId, { status: "rejected" });
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
case "question/requested":
|
|
196
|
+
next.pendingQuestion = {
|
|
197
|
+
questions: event.questions,
|
|
198
|
+
rpcId: event.rpcId
|
|
199
|
+
};
|
|
200
|
+
break;
|
|
201
|
+
case "question/resolved": if (next.pendingQuestion?.rpcId === event.questionRpcId) next.pendingQuestion = null;
|
|
202
|
+
}
|
|
203
|
+
return next;
|
|
204
|
+
}
|
|
205
|
+
//#endregion
|
|
206
|
+
export { createFoldState, foldSessionEvent };
|
|
207
|
+
|
|
208
|
+
//# sourceMappingURL=fold.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fold.js","names":[],"sources":["../../src/chat/fold.ts"],"sourcesContent":["import type { AskQuestionItem } from '../tools'\nimport type { ChatSessionEvent } from './session'\nimport type { ChatJob, ChatMessage, ChatQueuedMessage, ChatTokenUsage, ChatToolCall } from './types'\n\nfunction eventSeq(event: ChatSessionEvent): number | undefined {\n return 'seq' in event ? event.seq : undefined\n}\n\nexport interface ChatPendingApproval {\n approvalId: string\n toolName: string\n callId?: string\n reason?: string\n rpcId: string\n}\n\n/** 历史回放与实时 onEvent 共用的折叠状态 */\nexport interface ChatFoldState {\n lastSeq: number | null\n messages: ChatMessage[]\n queue: ChatQueuedMessage[]\n jobs: ChatJob[]\n tokenUsage: ChatTokenUsage | null\n projections: Record<string, unknown>\n title: string | null\n running: boolean\n pendingApprovals: ChatPendingApproval[]\n pendingQuestion: { questions: AskQuestionItem[]; rpcId: string } | null\n error: { code: string; message: string } | null\n}\n\nexport function createFoldState(): ChatFoldState {\n return {\n lastSeq: null,\n messages: [],\n queue: [],\n jobs: [],\n tokenUsage: null,\n projections: {},\n title: null,\n running: false,\n pendingApprovals: [],\n pendingQuestion: null,\n error: null\n }\n}\n\nfunction mergeOptionalCount(a?: number, b?: number): number | undefined {\n if (a == null && b == null) return undefined\n return (a ?? 0) + (b ?? 0)\n}\n\nfunction addTokenUsage(a: ChatTokenUsage, b: ChatTokenUsage): ChatTokenUsage {\n const usage: ChatTokenUsage = {\n promptTokens: a.promptTokens + b.promptTokens,\n completionTokens: a.completionTokens + b.completionTokens,\n totalTokens: a.totalTokens + b.totalTokens\n }\n const cacheHitTokens = mergeOptionalCount(a.cacheHitTokens, b.cacheHitTokens)\n if (cacheHitTokens != null) usage.cacheHitTokens = cacheHitTokens\n const cacheMissTokens = mergeOptionalCount(a.cacheMissTokens, b.cacheMissTokens)\n if (cacheMissTokens != null) usage.cacheMissTokens = cacheMissTokens\n const cacheCreationTokens = mergeOptionalCount(a.cacheCreationTokens, b.cacheCreationTokens)\n if (cacheCreationTokens != null) usage.cacheCreationTokens = cacheCreationTokens\n return usage\n}\n\nfunction isTokenUsage(value: unknown): value is ChatTokenUsage {\n if (value == null || typeof value !== 'object') return false\n const usage = value as ChatTokenUsage\n return (\n typeof usage.promptTokens === 'number' &&\n typeof usage.completionTokens === 'number' &&\n typeof usage.totalTokens === 'number'\n )\n}\n\nfunction replaceAt<T>(list: T[], idx: number, item: T): T[] {\n const next = list.slice()\n next[idx] = item\n return next\n}\n\nfunction patchMessage(\n messages: ChatMessage[],\n id: string,\n patch: Partial<ChatMessage>\n): ChatMessage[] {\n const idx = messages.findIndex((m) => m.id === id)\n if (idx < 0) {\n return [...messages, { id, role: 'assistant', content: '', status: 'streaming', ...patch }]\n }\n return replaceAt(messages, idx, { ...messages[idx]!, ...patch })\n}\n\nfunction appendToolCall(messages: ChatMessage[], call: ChatToolCall): ChatMessage[] {\n const idx = messages.findLastIndex((m) => m.role === 'assistant')\n if (idx < 0) {\n return [\n ...messages,\n {\n id: `tool-host:${call.id}`,\n role: 'assistant',\n content: '',\n status: 'streaming',\n toolCalls: [call]\n }\n ]\n }\n const target = messages[idx]!\n return replaceAt(messages, idx, { ...target, toolCalls: [...(target.toolCalls ?? []), call] })\n}\n\nfunction updateToolCall(\n messages: ChatMessage[],\n callId: string,\n patch: Partial<ChatToolCall>\n): ChatMessage[] {\n const idx = messages.findIndex((m) => m.toolCalls?.some((c) => c.id === callId))\n if (idx < 0) return messages\n const msg = messages[idx]!\n const calls = (msg.toolCalls ?? []).slice()\n const callIdx = calls.findIndex((c) => c.id === callId)\n if (callIdx < 0) return messages\n calls[callIdx] = { ...calls[callIdx]!, ...patch }\n return replaceAt(messages, idx, { ...msg, toolCalls: calls })\n}\n\n/**\n * 纯函数 fold:历史回放与实时 onEvent 走同一套。\n * 后到的更小/重复 seq 不覆盖已应用事件。\n */\nexport function foldSessionEvent(state: ChatFoldState, event: ChatSessionEvent): ChatFoldState {\n const seq = eventSeq(event)\n if (seq != null && state.lastSeq != null && seq <= state.lastSeq) return state\n\n const next: ChatFoldState = {\n ...state,\n lastSeq: seq ?? state.lastSeq,\n projections: { ...state.projections }\n }\n\n switch (event.type) {\n case 'user/message': {\n if (next.messages.some((m) => m.id === event.messageId)) break\n const userMessage: ChatMessage = { id: event.messageId, role: 'user', content: event.content }\n if (event.attachments) userMessage.attachments = event.attachments\n next.messages = [...next.messages, userMessage]\n break\n }\n case 'assistant/chunk': {\n const current = next.messages.find((m) => m.id === event.messageId)\n next.messages = patchMessage(next.messages, event.messageId, {\n role: 'assistant',\n status: 'streaming',\n content: (current?.content ?? '') + event.delta,\n reasoning: event.reasoningDelta\n ? (current?.reasoning ?? '') + event.reasoningDelta\n : current?.reasoning\n })\n break\n }\n case 'assistant/message': {\n next.messages = patchMessage(next.messages, event.messageId, {\n role: 'assistant',\n status: 'done',\n content: event.content,\n reasoning: event.reasoning,\n toolCalls: event.toolCalls ?? next.messages.find((m) => m.id === event.messageId)?.toolCalls\n })\n break\n }\n case 'tool/call': {\n const call: ChatToolCall = {\n id: event.callId,\n name: event.name,\n arguments: event.arguments,\n status: 'pending'\n }\n if (event.view !== undefined) call.view = event.view\n next.messages = appendToolCall(next.messages, call)\n break\n }\n case 'tool/result': {\n const patch: Partial<ChatToolCall> = { status: event.status }\n if (event.result !== undefined) patch.result = event.result\n if (event.error !== undefined) patch.error = event.error\n if (event.view !== undefined) patch.view = event.view\n next.messages = updateToolCall(next.messages, event.callId, patch)\n break\n }\n case 'queue/snapshot': {\n next.queue = event.items.slice()\n break\n }\n case 'jobs/snapshot': {\n next.jobs = event.jobs.slice()\n break\n }\n case 'projection': {\n if (event.key === 'tokenUsage' && isTokenUsage(event.value)) {\n next.tokenUsage = next.tokenUsage\n ? addTokenUsage(next.tokenUsage, event.value)\n : event.value\n } else if (event.key === 'title' && typeof event.value === 'string') {\n next.title = event.value\n } else {\n next.projections[event.key] = event.value\n }\n break\n }\n case 'running': {\n next.running = event.running\n break\n }\n case 'finish': {\n next.running = false\n break\n }\n case 'error': {\n next.running = false\n next.error = { code: event.code, message: event.message }\n break\n }\n case 'approval/requested': {\n if (event.callId) {\n next.messages = updateToolCall(next.messages, event.callId, { status: 'awaiting-confirm' })\n }\n next.pendingApprovals = [\n ...next.pendingApprovals,\n {\n approvalId: event.approvalId,\n toolName: event.toolName,\n callId: event.callId,\n reason: event.reason,\n rpcId: event.rpcId\n }\n ]\n break\n }\n case 'approval/resolved': {\n const hit = next.pendingApprovals.find((a) => a.approvalId === event.approvalId)\n next.pendingApprovals = next.pendingApprovals.filter((a) => a.approvalId !== event.approvalId)\n if (hit?.callId && event.outcome === 'rejected') {\n next.messages = updateToolCall(next.messages, hit.callId, { status: 'rejected' })\n }\n break\n }\n case 'question/requested': {\n next.pendingQuestion = { questions: event.questions, rpcId: event.rpcId }\n break\n }\n case 'question/resolved': {\n if (next.pendingQuestion?.rpcId === event.questionRpcId) next.pendingQuestion = null\n break\n }\n }\n\n return next\n}\n"],"mappings":"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"}
|
package/dist/chat/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { ChatAttachment, ChatMessage, ChatQueuedMessage, ChatTokenUsage, ChatTool, ChatToolCall, ChatToolContext, ChatToolRenderProps, ChatTransport, ChatTransportHandlers, ChatTransportRequest, ToolCallStatus } from "./types.js";
|
|
1
|
+
import { ChatAttachment, ChatJob, ChatJobStatus, ChatMessage, ChatQueuedMessage, ChatTokenUsage, ChatTool, ChatToolCall, ChatToolContext, ChatToolMeta, ChatToolRenderProps, ChatTransport, ChatTransportHandlers, ChatTransportRequest, ToolCallStatus } from "./types.js";
|
|
2
|
+
import { ChatSessionAdapter, ChatSessionEvent, ChatSessionTransport, createServerTransport, isServerTransport } from "./session.js";
|
|
3
|
+
import { ChatFoldState, ChatPendingApproval, createFoldState, foldSessionEvent } from "./fold.js";
|
|
2
4
|
import { UseChatOptions, useChat } from "./use-chat.js";
|
|
3
5
|
import { OpenAITransport, OpenAITransportOptions, createOpenAITransport } from "./transports/openai.js";
|
|
4
6
|
import "./transports/index.js";
|
|
5
|
-
export { ChatAttachment, ChatMessage, ChatQueuedMessage, ChatTokenUsage, ChatTool, ChatToolCall, ChatToolContext, ChatToolRenderProps, ChatTransport, ChatTransportHandlers, ChatTransportRequest, OpenAITransport, OpenAITransportOptions, ToolCallStatus, UseChatOptions, createOpenAITransport, useChat };
|
|
7
|
+
export { ChatAttachment, ChatFoldState, ChatJob, ChatJobStatus, ChatMessage, ChatPendingApproval, ChatQueuedMessage, ChatSessionAdapter, ChatSessionEvent, ChatSessionTransport, ChatTokenUsage, ChatTool, ChatToolCall, ChatToolContext, ChatToolMeta, ChatToolRenderProps, ChatTransport, ChatTransportHandlers, ChatTransportRequest, OpenAITransport, OpenAITransportOptions, ToolCallStatus, UseChatOptions, createFoldState, createOpenAITransport, createServerTransport, foldSessionEvent, isServerTransport, useChat };
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { ChatAttachment, ChatJob, ChatQueuedMessage, ChatToolCall } from "./types.js";
|
|
2
|
+
import { AskQuestionItem } from "../tools/ask-question/ask-question.js";
|
|
3
|
+
import "../tools/index.js";
|
|
4
|
+
//#region src/chat/session.d.ts
|
|
5
|
+
/** 归一化后的服务端会话事件(协议无关) */
|
|
6
|
+
type ChatSessionEvent = {
|
|
7
|
+
type: 'user/message';
|
|
8
|
+
messageId: string;
|
|
9
|
+
seq: number;
|
|
10
|
+
content: string;
|
|
11
|
+
attachments?: ChatAttachment[];
|
|
12
|
+
} | {
|
|
13
|
+
type: 'assistant/chunk';
|
|
14
|
+
messageId: string;
|
|
15
|
+
seq: number;
|
|
16
|
+
delta: string;
|
|
17
|
+
reasoningDelta?: string;
|
|
18
|
+
} | {
|
|
19
|
+
type: 'assistant/message';
|
|
20
|
+
messageId: string;
|
|
21
|
+
seq: number;
|
|
22
|
+
content: string;
|
|
23
|
+
reasoning?: string;
|
|
24
|
+
toolCalls?: ChatToolCall[];
|
|
25
|
+
} | {
|
|
26
|
+
type: 'tool/call';
|
|
27
|
+
callId: string;
|
|
28
|
+
name: string;
|
|
29
|
+
arguments: string;
|
|
30
|
+
seq: number;
|
|
31
|
+
view?: unknown;
|
|
32
|
+
} | {
|
|
33
|
+
type: 'tool/result';
|
|
34
|
+
callId: string;
|
|
35
|
+
status: 'success' | 'error' | 'rejected';
|
|
36
|
+
result?: string;
|
|
37
|
+
error?: string;
|
|
38
|
+
seq: number;
|
|
39
|
+
view?: unknown;
|
|
40
|
+
} | {
|
|
41
|
+
type: 'approval/requested';
|
|
42
|
+
approvalId: string;
|
|
43
|
+
toolName: string;
|
|
44
|
+
callId?: string;
|
|
45
|
+
reason?: string;
|
|
46
|
+
rpcId: string;
|
|
47
|
+
} | {
|
|
48
|
+
type: 'approval/resolved';
|
|
49
|
+
approvalId: string;
|
|
50
|
+
outcome: string;
|
|
51
|
+
} | {
|
|
52
|
+
type: 'question/requested';
|
|
53
|
+
questions: AskQuestionItem[];
|
|
54
|
+
rpcId: string;
|
|
55
|
+
} | {
|
|
56
|
+
type: 'question/resolved';
|
|
57
|
+
questionRpcId: string;
|
|
58
|
+
outcome: 'answered' | 'cancelled';
|
|
59
|
+
} | {
|
|
60
|
+
type: 'queue/snapshot';
|
|
61
|
+
items: ChatQueuedMessage[];
|
|
62
|
+
} | {
|
|
63
|
+
type: 'jobs/snapshot';
|
|
64
|
+
jobs: ChatJob[];
|
|
65
|
+
} | {
|
|
66
|
+
type: 'projection';
|
|
67
|
+
key: string;
|
|
68
|
+
value: unknown;
|
|
69
|
+
seq: number;
|
|
70
|
+
} | {
|
|
71
|
+
type: 'running';
|
|
72
|
+
running: boolean;
|
|
73
|
+
} | {
|
|
74
|
+
type: 'finish';
|
|
75
|
+
} | {
|
|
76
|
+
type: 'error';
|
|
77
|
+
code: string;
|
|
78
|
+
message: string;
|
|
79
|
+
};
|
|
80
|
+
/** 服务端会话 transport:对象形态,与函数型 ChatTransport 互斥 */
|
|
81
|
+
interface ChatSessionTransport {
|
|
82
|
+
readonly kind: 'session';
|
|
83
|
+
open(handlers: {
|
|
84
|
+
onEvent(event: ChatSessionEvent): void;
|
|
85
|
+
onDisconnect?(): void;
|
|
86
|
+
}): () => void;
|
|
87
|
+
send(content: string, attachments?: ChatAttachment[]): Promise<void>;
|
|
88
|
+
cancel(): Promise<void>;
|
|
89
|
+
respond(rpcId: string, ok: boolean, value?: unknown): Promise<void>;
|
|
90
|
+
fetchHistory(beforeSeq?: number): Promise<{
|
|
91
|
+
events: ChatSessionEvent[];
|
|
92
|
+
hasMore: boolean;
|
|
93
|
+
}>;
|
|
94
|
+
selectModel(provider: string, model: string): Promise<void>;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* 协议翻译层:订阅事件流、把动作打到远端、拉历史。
|
|
98
|
+
* 时序校验 / dispose / 断线补拉 / in-flight 去重由 createServerTransport 完成。
|
|
99
|
+
*/
|
|
100
|
+
interface ChatSessionAdapter {
|
|
101
|
+
subscribe(handlers: {
|
|
102
|
+
onEvent(event: ChatSessionEvent): void;
|
|
103
|
+
onDisconnect?(): void;
|
|
104
|
+
}): () => void;
|
|
105
|
+
send(content: string, attachments?: ChatAttachment[]): Promise<void>;
|
|
106
|
+
cancel(): Promise<void>;
|
|
107
|
+
respond(rpcId: string, ok: boolean, value?: unknown): Promise<void>;
|
|
108
|
+
fetchHistory(beforeSeq?: number): Promise<{
|
|
109
|
+
events: ChatSessionEvent[];
|
|
110
|
+
hasMore: boolean;
|
|
111
|
+
}>;
|
|
112
|
+
selectModel(provider: string, model: string): Promise<void>;
|
|
113
|
+
}
|
|
114
|
+
/** 仅带 kind: 'session' 的对象为 true;函数 / null / 无 kind 均为 false */
|
|
115
|
+
declare function isServerTransport(t: unknown): t is ChatSessionTransport;
|
|
116
|
+
/**
|
|
117
|
+
* 用 adapter 包出 ChatSessionTransport。
|
|
118
|
+
* 包内完成 seq 校验、disposer、断线补拉、动作 in-flight 去重。
|
|
119
|
+
*/
|
|
120
|
+
declare function createServerTransport(adapter: ChatSessionAdapter): ChatSessionTransport;
|
|
121
|
+
//#endregion
|
|
122
|
+
export { ChatSessionAdapter, ChatSessionEvent, ChatSessionTransport, createServerTransport, isServerTransport };
|
|
123
|
+
//# sourceMappingURL=session.d.ts.map
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
//#region src/chat/session.ts
|
|
2
|
+
function eventSeq(event) {
|
|
3
|
+
return "seq" in event ? event.seq : void 0;
|
|
4
|
+
}
|
|
5
|
+
/** 仅带 kind: 'session' 的对象为 true;函数 / null / 无 kind 均为 false */
|
|
6
|
+
function isServerTransport(t) {
|
|
7
|
+
return typeof t === "object" && t !== null && t.kind === "session";
|
|
8
|
+
}
|
|
9
|
+
function runExclusive(gate, action) {
|
|
10
|
+
if (gate.busy) return Promise.resolve();
|
|
11
|
+
gate.busy = true;
|
|
12
|
+
return action().finally(() => {
|
|
13
|
+
gate.busy = false;
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* 用 adapter 包出 ChatSessionTransport。
|
|
18
|
+
* 包内完成 seq 校验、disposer、断线补拉、动作 in-flight 去重。
|
|
19
|
+
*/
|
|
20
|
+
function createServerTransport(adapter) {
|
|
21
|
+
let lastSeq;
|
|
22
|
+
const sendGate = { busy: false };
|
|
23
|
+
const cancelGate = { busy: false };
|
|
24
|
+
const respondGate = { busy: false };
|
|
25
|
+
const selectModelGate = { busy: false };
|
|
26
|
+
const deliver = (event, onEvent) => {
|
|
27
|
+
const seq = eventSeq(event);
|
|
28
|
+
if (seq != null && lastSeq != null && seq <= lastSeq) {
|
|
29
|
+
console.warn(`[ChatSessionTransport] 丢弃乱序事件 seq=${seq} lastSeq=${lastSeq}`);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (seq != null) lastSeq = seq;
|
|
33
|
+
onEvent(event);
|
|
34
|
+
};
|
|
35
|
+
return {
|
|
36
|
+
kind: "session",
|
|
37
|
+
open(handlers) {
|
|
38
|
+
let disposed = false;
|
|
39
|
+
const unsubscribe = adapter.subscribe({
|
|
40
|
+
onEvent(event) {
|
|
41
|
+
if (!disposed) deliver(event, handlers.onEvent);
|
|
42
|
+
},
|
|
43
|
+
onDisconnect() {
|
|
44
|
+
if (disposed) return;
|
|
45
|
+
adapter.fetchHistory(lastSeq).then(({ events }) => {
|
|
46
|
+
if (disposed) return;
|
|
47
|
+
for (const event of events) deliver(event, handlers.onEvent);
|
|
48
|
+
handlers.onDisconnect?.();
|
|
49
|
+
}, () => {
|
|
50
|
+
if (!disposed) handlers.onDisconnect?.();
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
return () => {
|
|
55
|
+
disposed = true;
|
|
56
|
+
unsubscribe();
|
|
57
|
+
};
|
|
58
|
+
},
|
|
59
|
+
send(content, attachments) {
|
|
60
|
+
return runExclusive(sendGate, () => adapter.send(content, attachments));
|
|
61
|
+
},
|
|
62
|
+
cancel() {
|
|
63
|
+
return runExclusive(cancelGate, () => adapter.cancel());
|
|
64
|
+
},
|
|
65
|
+
respond(rpcId, ok, value) {
|
|
66
|
+
return runExclusive(respondGate, () => adapter.respond(rpcId, ok, value));
|
|
67
|
+
},
|
|
68
|
+
fetchHistory(beforeSeq) {
|
|
69
|
+
return adapter.fetchHistory(beforeSeq);
|
|
70
|
+
},
|
|
71
|
+
selectModel(provider, model) {
|
|
72
|
+
return runExclusive(selectModelGate, () => adapter.selectModel(provider, model));
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
//#endregion
|
|
77
|
+
export { createServerTransport, isServerTransport };
|
|
78
|
+
|
|
79
|
+
//# sourceMappingURL=session.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.js","names":[],"sources":["../../src/chat/session.ts"],"sourcesContent":["import type { AskQuestionItem } from '../tools'\nimport type { ChatAttachment, ChatJob, ChatQueuedMessage, ChatToolCall } from './types'\n\n/** 归一化后的服务端会话事件(协议无关) */\nexport type ChatSessionEvent =\n | {\n type: 'user/message'\n messageId: string\n seq: number\n content: string\n attachments?: ChatAttachment[]\n }\n | {\n type: 'assistant/chunk'\n messageId: string\n seq: number\n delta: string\n reasoningDelta?: string\n }\n | {\n type: 'assistant/message'\n messageId: string\n seq: number\n content: string\n reasoning?: string\n toolCalls?: ChatToolCall[]\n }\n | {\n type: 'tool/call'\n callId: string\n name: string\n arguments: string\n seq: number\n view?: unknown\n }\n | {\n type: 'tool/result'\n callId: string\n status: 'success' | 'error' | 'rejected'\n result?: string\n error?: string\n seq: number\n view?: unknown\n }\n | {\n type: 'approval/requested'\n approvalId: string\n toolName: string\n callId?: string\n reason?: string\n rpcId: string\n }\n | { type: 'approval/resolved'; approvalId: string; outcome: string }\n | { type: 'question/requested'; questions: AskQuestionItem[]; rpcId: string }\n | { type: 'question/resolved'; questionRpcId: string; outcome: 'answered' | 'cancelled' }\n | { type: 'queue/snapshot'; items: ChatQueuedMessage[] }\n | { type: 'jobs/snapshot'; jobs: ChatJob[] }\n | { type: 'projection'; key: string; value: unknown; seq: number }\n | { type: 'running'; running: boolean }\n | { type: 'finish' }\n | { type: 'error'; code: string; message: string }\n\n/** 服务端会话 transport:对象形态,与函数型 ChatTransport 互斥 */\nexport interface ChatSessionTransport {\n readonly kind: 'session'\n open(handlers: { onEvent(event: ChatSessionEvent): void; onDisconnect?(): void }): () => void\n send(content: string, attachments?: ChatAttachment[]): Promise<void>\n cancel(): Promise<void>\n respond(rpcId: string, ok: boolean, value?: unknown): Promise<void>\n fetchHistory(beforeSeq?: number): Promise<{ events: ChatSessionEvent[]; hasMore: boolean }>\n selectModel(provider: string, model: string): Promise<void>\n}\n\n/**\n * 协议翻译层:订阅事件流、把动作打到远端、拉历史。\n * 时序校验 / dispose / 断线补拉 / in-flight 去重由 createServerTransport 完成。\n */\nexport interface ChatSessionAdapter {\n subscribe(handlers: { onEvent(event: ChatSessionEvent): void; onDisconnect?(): void }): () => void\n send(content: string, attachments?: ChatAttachment[]): Promise<void>\n cancel(): Promise<void>\n respond(rpcId: string, ok: boolean, value?: unknown): Promise<void>\n fetchHistory(beforeSeq?: number): Promise<{ events: ChatSessionEvent[]; hasMore: boolean }>\n selectModel(provider: string, model: string): Promise<void>\n}\n\nfunction eventSeq(event: ChatSessionEvent): number | undefined {\n return 'seq' in event ? event.seq : undefined\n}\n\n/** 仅带 kind: 'session' 的对象为 true;函数 / null / 无 kind 均为 false */\nexport function isServerTransport(t: unknown): t is ChatSessionTransport {\n return typeof t === 'object' && t !== null && (t as { kind?: unknown }).kind === 'session'\n}\n\nfunction runExclusive(gate: { busy: boolean }, action: () => Promise<void>): Promise<void> {\n if (gate.busy) return Promise.resolve()\n gate.busy = true\n return action().finally(() => {\n gate.busy = false\n })\n}\n\n/**\n * 用 adapter 包出 ChatSessionTransport。\n * 包内完成 seq 校验、disposer、断线补拉、动作 in-flight 去重。\n */\nexport function createServerTransport(adapter: ChatSessionAdapter): ChatSessionTransport {\n let lastSeq: number | undefined\n const sendGate = { busy: false }\n const cancelGate = { busy: false }\n const respondGate = { busy: false }\n const selectModelGate = { busy: false }\n\n const deliver = (event: ChatSessionEvent, onEvent: (e: ChatSessionEvent) => void) => {\n const seq = eventSeq(event)\n if (seq != null && lastSeq != null && seq <= lastSeq) {\n console.warn(`[ChatSessionTransport] 丢弃乱序事件 seq=${seq} lastSeq=${lastSeq}`)\n return\n }\n if (seq != null) lastSeq = seq\n onEvent(event)\n }\n\n return {\n kind: 'session',\n open(handlers) {\n let disposed = false\n const unsubscribe = adapter.subscribe({\n onEvent(event) {\n if (!disposed) deliver(event, handlers.onEvent)\n },\n onDisconnect() {\n if (disposed) return\n void adapter.fetchHistory(lastSeq).then(\n ({ events }) => {\n if (disposed) return\n for (const event of events) deliver(event, handlers.onEvent)\n handlers.onDisconnect?.()\n },\n () => {\n if (!disposed) handlers.onDisconnect?.()\n }\n )\n }\n })\n return () => {\n disposed = true\n unsubscribe()\n }\n },\n send(content, attachments) {\n return runExclusive(sendGate, () => adapter.send(content, attachments))\n },\n cancel() {\n return runExclusive(cancelGate, () => adapter.cancel())\n },\n respond(rpcId, ok, value) {\n return runExclusive(respondGate, () => adapter.respond(rpcId, ok, value))\n },\n fetchHistory(beforeSeq) {\n return adapter.fetchHistory(beforeSeq)\n },\n selectModel(provider, model) {\n return runExclusive(selectModelGate, () => adapter.selectModel(provider, model))\n }\n }\n}\n"],"mappings":";AAsFA,SAAS,SAAS,OAA6C;CAC7D,OAAO,SAAS,QAAQ,MAAM,MAAM,KAAA;AACtC;;AAGA,SAAgB,kBAAkB,GAAuC;CACvE,OAAO,OAAO,MAAM,YAAY,MAAM,QAAS,EAAyB,SAAS;AACnF;AAEA,SAAS,aAAa,MAAyB,QAA4C;CACzF,IAAI,KAAK,MAAM,OAAO,QAAQ,QAAQ;CACtC,KAAK,OAAO;CACZ,OAAO,OAAO,CAAC,CAAC,cAAc;EAC5B,KAAK,OAAO;CACd,CAAC;AACH;;;;;AAMA,SAAgB,sBAAsB,SAAmD;CACvF,IAAI;CACJ,MAAM,WAAW,EAAE,MAAM,MAAM;CAC/B,MAAM,aAAa,EAAE,MAAM,MAAM;CACjC,MAAM,cAAc,EAAE,MAAM,MAAM;CAClC,MAAM,kBAAkB,EAAE,MAAM,MAAM;CAEtC,MAAM,WAAW,OAAyB,YAA2C;EACnF,MAAM,MAAM,SAAS,KAAK;EAC1B,IAAI,OAAO,QAAQ,WAAW,QAAQ,OAAO,SAAS;GACpD,QAAQ,KAAK,qCAAqC,IAAI,WAAW,SAAS;GAC1E;EACF;EACA,IAAI,OAAO,MAAM,UAAU;EAC3B,QAAQ,KAAK;CACf;CAEA,OAAO;EACL,MAAM;EACN,KAAK,UAAU;GACb,IAAI,WAAW;GACf,MAAM,cAAc,QAAQ,UAAU;IACpC,QAAQ,OAAO;KACb,IAAI,CAAC,UAAU,QAAQ,OAAO,SAAS,OAAO;IAChD;IACA,eAAe;KACb,IAAI,UAAU;KACd,QAAa,aAAa,OAAO,CAAC,CAAC,MAChC,EAAE,aAAa;MACd,IAAI,UAAU;MACd,KAAK,MAAM,SAAS,QAAQ,QAAQ,OAAO,SAAS,OAAO;MAC3D,SAAS,eAAe;KAC1B,SACM;MACJ,IAAI,CAAC,UAAU,SAAS,eAAe;KACzC,CACF;IACF;GACF,CAAC;GACD,aAAa;IACX,WAAW;IACX,YAAY;GACd;EACF;EACA,KAAK,SAAS,aAAa;GACzB,OAAO,aAAa,gBAAgB,QAAQ,KAAK,SAAS,WAAW,CAAC;EACxE;EACA,SAAS;GACP,OAAO,aAAa,kBAAkB,QAAQ,OAAO,CAAC;EACxD;EACA,QAAQ,OAAO,IAAI,OAAO;GACxB,OAAO,aAAa,mBAAmB,QAAQ,QAAQ,OAAO,IAAI,KAAK,CAAC;EAC1E;EACA,aAAa,WAAW;GACtB,OAAO,QAAQ,aAAa,SAAS;EACvC;EACA,YAAY,UAAU,OAAO;GAC3B,OAAO,aAAa,uBAAuB,QAAQ,YAAY,UAAU,KAAK,CAAC;EACjF;CACF;AACF"}
|
package/dist/chat/types.d.ts
CHANGED
|
@@ -27,6 +27,8 @@ interface ChatToolCall {
|
|
|
27
27
|
result?: string;
|
|
28
28
|
/** 执行失败信息 */
|
|
29
29
|
error?: string;
|
|
30
|
+
/** 服务端下发的展示视图原文(session 模式可选) */
|
|
31
|
+
view?: unknown;
|
|
30
32
|
}
|
|
31
33
|
/** 聊天消息 */
|
|
32
34
|
interface ChatMessage {
|
|
@@ -116,6 +118,41 @@ interface ChatTool<A = any> {
|
|
|
116
118
|
/** 工具实现,返回值(或 Promise 返回值)会被 JSON 序列化后回灌给模型 */
|
|
117
119
|
execute: (args: A, ctx: ChatToolContext) => unknown;
|
|
118
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* 服务端驱动模式下 tools 的合法形态:纯渲染元信息(执行在服务端)。
|
|
123
|
+
* 没有 execute / description / parameters / needsConfirm / terminal。
|
|
124
|
+
*/
|
|
125
|
+
interface ChatToolMeta {
|
|
126
|
+
/** 与服务端 tool/call.name 匹配 */
|
|
127
|
+
name: string;
|
|
128
|
+
/** 覆盖通用图标 */
|
|
129
|
+
icon?: Component;
|
|
130
|
+
/** 工具显示名,缺省取 name */
|
|
131
|
+
label?: string;
|
|
132
|
+
/** 自定义卡片/面板渲染,props 为 ChatToolRenderProps */
|
|
133
|
+
render?: Component;
|
|
134
|
+
/** 渲染位置,语义同 ChatTool.renderTo */
|
|
135
|
+
renderTo?: 'inline' | 'panel';
|
|
136
|
+
/** 侧边面板默认宽度,语义同 ChatTool.panelWidth */
|
|
137
|
+
panelWidth?: number;
|
|
138
|
+
/** 侧边面板标题,语义同 ChatTool.panelTitle */
|
|
139
|
+
panelTitle?: string | ((toolCall: ChatToolCall) => string);
|
|
140
|
+
/** 执行完成后是否自动折叠 */
|
|
141
|
+
autoCollapse?: boolean;
|
|
142
|
+
}
|
|
143
|
+
/** 作业条项状态:至少区分进行中与终态 */
|
|
144
|
+
type ChatJobStatus = 'running' | 'done' | 'error';
|
|
145
|
+
/** 服务端下发的作业条项 */
|
|
146
|
+
interface ChatJob {
|
|
147
|
+
/** 稳定 id */
|
|
148
|
+
id: string;
|
|
149
|
+
/** 作业类型(用于图标映射) */
|
|
150
|
+
kind: string;
|
|
151
|
+
/** 展示文案 */
|
|
152
|
+
label: string;
|
|
153
|
+
/** 进行中或终态 */
|
|
154
|
+
status: ChatJobStatus;
|
|
155
|
+
}
|
|
119
156
|
/** 工具自定义渲染组件 / 渲染函数的 props */
|
|
120
157
|
interface ChatToolRenderProps {
|
|
121
158
|
/** 本次工具调用(含 status/arguments/result/error,可自行渲染进度与错误) */
|
|
@@ -190,5 +227,5 @@ interface ChatTransportHandlers {
|
|
|
190
227
|
*/
|
|
191
228
|
type ChatTransport = (request: ChatTransportRequest, handlers: ChatTransportHandlers) => Promise<void> | void;
|
|
192
229
|
//#endregion
|
|
193
|
-
export { ChatAttachment, ChatMessage, ChatQueuedMessage, ChatTokenUsage, ChatTool, ChatToolCall, ChatToolContext, ChatToolRenderProps, ChatTransport, ChatTransportHandlers, ChatTransportRequest, ToolCallStatus };
|
|
230
|
+
export { ChatAttachment, ChatJob, ChatJobStatus, ChatMessage, ChatQueuedMessage, ChatTokenUsage, ChatTool, ChatToolCall, ChatToolContext, ChatToolMeta, ChatToolRenderProps, ChatTransport, ChatTransportHandlers, ChatTransportRequest, ToolCallStatus };
|
|
194
231
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { createBuiltinTools } from "../tools/index.js";
|
|
2
|
+
//#region src/chat/use-chat-helpers.ts
|
|
3
|
+
function serializeResult(result) {
|
|
4
|
+
if (typeof result === "string") return result;
|
|
5
|
+
try {
|
|
6
|
+
return JSON.stringify(result) ?? "null";
|
|
7
|
+
} catch {
|
|
8
|
+
return String(result);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/** 内置工具 + 用户工具,同名时内置优先 */
|
|
12
|
+
function resolveTools(userTools) {
|
|
13
|
+
const builtins = createBuiltinTools();
|
|
14
|
+
const names = new Set(builtins.map((t) => t.name));
|
|
15
|
+
return [...builtins, ...(userTools ?? []).filter((t) => !names.has(t.name))];
|
|
16
|
+
}
|
|
17
|
+
function mergeOptionalCount(a, b) {
|
|
18
|
+
if (a == null && b == null) return void 0;
|
|
19
|
+
return (a ?? 0) + (b ?? 0);
|
|
20
|
+
}
|
|
21
|
+
/** 累加两次 usage;缓存字段只在至少一侧有值时保留 */
|
|
22
|
+
function addTokenUsage(a, b) {
|
|
23
|
+
const usage = {
|
|
24
|
+
promptTokens: a.promptTokens + b.promptTokens,
|
|
25
|
+
completionTokens: a.completionTokens + b.completionTokens,
|
|
26
|
+
totalTokens: a.totalTokens + b.totalTokens
|
|
27
|
+
};
|
|
28
|
+
const cacheHitTokens = mergeOptionalCount(a.cacheHitTokens, b.cacheHitTokens);
|
|
29
|
+
if (cacheHitTokens != null) usage.cacheHitTokens = cacheHitTokens;
|
|
30
|
+
const cacheMissTokens = mergeOptionalCount(a.cacheMissTokens, b.cacheMissTokens);
|
|
31
|
+
if (cacheMissTokens != null) usage.cacheMissTokens = cacheMissTokens;
|
|
32
|
+
const cacheCreationTokens = mergeOptionalCount(a.cacheCreationTokens, b.cacheCreationTokens);
|
|
33
|
+
if (cacheCreationTokens != null) usage.cacheCreationTokens = cacheCreationTokens;
|
|
34
|
+
return usage;
|
|
35
|
+
}
|
|
36
|
+
//#endregion
|
|
37
|
+
export { addTokenUsage, resolveTools, serializeResult };
|
|
38
|
+
|
|
39
|
+
//# sourceMappingURL=use-chat-helpers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-chat-helpers.js","names":[],"sources":["../../src/chat/use-chat-helpers.ts"],"sourcesContent":["import { createBuiltinTools } from '../tools'\nimport type { ChatTokenUsage, ChatTool } from './types'\n\nexport function serializeResult(result: unknown): string {\n if (typeof result === 'string') return result\n try {\n return JSON.stringify(result) ?? 'null'\n } catch {\n return String(result)\n }\n}\n\n/** 内置工具 + 用户工具,同名时内置优先 */\nexport function resolveTools(userTools?: ChatTool[]): ChatTool[] {\n const builtins = createBuiltinTools()\n const names = new Set(builtins.map((t) => t.name))\n return [...builtins, ...(userTools ?? []).filter((t) => !names.has(t.name))]\n}\n\nfunction mergeOptionalCount(a?: number, b?: number): number | undefined {\n if (a == null && b == null) return undefined\n return (a ?? 0) + (b ?? 0)\n}\n\n/** 累加两次 usage;缓存字段只在至少一侧有值时保留 */\nexport function addTokenUsage(a: ChatTokenUsage, b: ChatTokenUsage): ChatTokenUsage {\n const usage: ChatTokenUsage = {\n promptTokens: a.promptTokens + b.promptTokens,\n completionTokens: a.completionTokens + b.completionTokens,\n totalTokens: a.totalTokens + b.totalTokens\n }\n const cacheHitTokens = mergeOptionalCount(a.cacheHitTokens, b.cacheHitTokens)\n if (cacheHitTokens != null) usage.cacheHitTokens = cacheHitTokens\n const cacheMissTokens = mergeOptionalCount(a.cacheMissTokens, b.cacheMissTokens)\n if (cacheMissTokens != null) usage.cacheMissTokens = cacheMissTokens\n const cacheCreationTokens = mergeOptionalCount(a.cacheCreationTokens, b.cacheCreationTokens)\n if (cacheCreationTokens != null) usage.cacheCreationTokens = cacheCreationTokens\n return usage\n}\n"],"mappings":";;AAGA,SAAgB,gBAAgB,QAAyB;CACvD,IAAI,OAAO,WAAW,UAAU,OAAO;CACvC,IAAI;EACF,OAAO,KAAK,UAAU,MAAM,KAAK;CACnC,QAAQ;EACN,OAAO,OAAO,MAAM;CACtB;AACF;;AAGA,SAAgB,aAAa,WAAoC;CAC/D,MAAM,WAAW,mBAAmB;CACpC,MAAM,QAAQ,IAAI,IAAI,SAAS,KAAK,MAAM,EAAE,IAAI,CAAC;CACjD,OAAO,CAAC,GAAG,UAAU,IAAI,aAAa,CAAC,EAAA,CAAG,QAAQ,MAAM,CAAC,MAAM,IAAI,EAAE,IAAI,CAAC,CAAC;AAC7E;AAEA,SAAS,mBAAmB,GAAY,GAAgC;CACtE,IAAI,KAAK,QAAQ,KAAK,MAAM,OAAO,KAAA;CACnC,QAAQ,KAAK,MAAM,KAAK;AAC1B;;AAGA,SAAgB,cAAc,GAAmB,GAAmC;CAClF,MAAM,QAAwB;EAC5B,cAAc,EAAE,eAAe,EAAE;EACjC,kBAAkB,EAAE,mBAAmB,EAAE;EACzC,aAAa,EAAE,cAAc,EAAE;CACjC;CACA,MAAM,iBAAiB,mBAAmB,EAAE,gBAAgB,EAAE,cAAc;CAC5E,IAAI,kBAAkB,MAAM,MAAM,iBAAiB;CACnD,MAAM,kBAAkB,mBAAmB,EAAE,iBAAiB,EAAE,eAAe;CAC/E,IAAI,mBAAmB,MAAM,MAAM,kBAAkB;CACrD,MAAM,sBAAsB,mBAAmB,EAAE,qBAAqB,EAAE,mBAAmB;CAC3F,IAAI,uBAAuB,MAAM,MAAM,sBAAsB;CAC7D,OAAO;AACT"}
|