agent-chat-sdk-core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/LICENSE +21 -0
- package/README.md +492 -0
- package/dist/cjs/client.js +199 -0
- package/dist/cjs/index.js +21 -0
- package/dist/cjs/knowledge-label.js +71 -0
- package/dist/cjs/normalizer.js +257 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/parser.js +294 -0
- package/dist/cjs/react-flow.js +42 -0
- package/dist/cjs/session.js +100 -0
- package/dist/cjs/types.js +2 -0
- package/dist/esm/client.d.ts +45 -0
- package/dist/esm/client.d.ts.map +1 -0
- package/dist/esm/client.js +195 -0
- package/dist/esm/index.d.ts +9 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +6 -0
- package/dist/esm/knowledge-label.d.ts +49 -0
- package/dist/esm/knowledge-label.d.ts.map +1 -0
- package/dist/esm/knowledge-label.js +67 -0
- package/dist/esm/normalizer.d.ts +22 -0
- package/dist/esm/normalizer.d.ts.map +1 -0
- package/dist/esm/normalizer.js +252 -0
- package/dist/esm/parser.d.ts +12 -0
- package/dist/esm/parser.d.ts.map +1 -0
- package/dist/esm/parser.js +289 -0
- package/dist/esm/react-flow.d.ts +40 -0
- package/dist/esm/react-flow.d.ts.map +1 -0
- package/dist/esm/react-flow.js +39 -0
- package/dist/esm/session.d.ts +7 -0
- package/dist/esm/session.d.ts.map +1 -0
- package/dist/esm/session.js +96 -0
- package/dist/esm/types.d.ts +151 -0
- package/dist/esm/types.d.ts.map +1 -0
- package/dist/esm/types.js +1 -0
- package/package.json +52 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AgentChatClient = void 0;
|
|
4
|
+
const parser_js_1 = require("./parser.js");
|
|
5
|
+
const normalizer_js_1 = require("./normalizer.js");
|
|
6
|
+
const DEFAULT_BUILD_BODY = (query, config) => ({
|
|
7
|
+
id: config.appId,
|
|
8
|
+
query,
|
|
9
|
+
});
|
|
10
|
+
const DEFAULT_BUILD_HEADERS = (config) => {
|
|
11
|
+
const headers = {
|
|
12
|
+
'Content-Type': 'application/json',
|
|
13
|
+
Accept: 'text/event-stream',
|
|
14
|
+
};
|
|
15
|
+
if (config.appKey) {
|
|
16
|
+
headers.Authorization = `Bearer ${config.appKey}`;
|
|
17
|
+
}
|
|
18
|
+
return headers;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* 智能体流式对话客户端
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* const client = new AgentChatClient({
|
|
26
|
+
* url: 'https://your-host/appforge/openapi/v1/InvokeApp/app-xxx',
|
|
27
|
+
* appId: 'app-xxx',
|
|
28
|
+
* appKey: 'your-key',
|
|
29
|
+
* });
|
|
30
|
+
*
|
|
31
|
+
* await client.chatStream(
|
|
32
|
+
* { query: '你好', dataMode: 'normalized' },
|
|
33
|
+
* {
|
|
34
|
+
* onEvent: (event) => {
|
|
35
|
+
* if (event.type === 'answer_delta') {
|
|
36
|
+
* console.log(event.content);
|
|
37
|
+
* }
|
|
38
|
+
* },
|
|
39
|
+
* },
|
|
40
|
+
* );
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
class AgentChatClient {
|
|
44
|
+
constructor(config) {
|
|
45
|
+
this.abortController = null;
|
|
46
|
+
this.config = config;
|
|
47
|
+
}
|
|
48
|
+
/** 更新配置 */
|
|
49
|
+
updateConfig(config) {
|
|
50
|
+
this.config = { ...this.config, ...config };
|
|
51
|
+
}
|
|
52
|
+
getConfig() {
|
|
53
|
+
return { ...this.config };
|
|
54
|
+
}
|
|
55
|
+
/** 中止当前流式请求 */
|
|
56
|
+
abort() {
|
|
57
|
+
this.abortController?.abort();
|
|
58
|
+
this.abortController = null;
|
|
59
|
+
}
|
|
60
|
+
/** 当前配置的请求 URL */
|
|
61
|
+
getUrl() {
|
|
62
|
+
return this.config.url;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* 发起流式对话
|
|
66
|
+
*/
|
|
67
|
+
async chatStream(options, handlers = {}) {
|
|
68
|
+
const { query, signal, dataMode = 'normalized', extraBody = {}, extraHeaders = {}, } = options;
|
|
69
|
+
this.abort();
|
|
70
|
+
const controller = new AbortController();
|
|
71
|
+
this.abortController = controller;
|
|
72
|
+
const combinedSignal = signal
|
|
73
|
+
? combineAbortSignals(signal, controller.signal)
|
|
74
|
+
: controller.signal;
|
|
75
|
+
const emit = (event) => handlers.onEvent?.(event);
|
|
76
|
+
const emitRaw = (chunk) => handlers.onRawChunk?.(chunk);
|
|
77
|
+
const shouldEmitNormalized = dataMode === 'normalized' || dataMode === 'both';
|
|
78
|
+
const shouldEmitRaw = dataMode === 'raw' || dataMode === 'both';
|
|
79
|
+
if (shouldEmitNormalized) {
|
|
80
|
+
emit({ type: 'stream_start', timestamp: Date.now(), query });
|
|
81
|
+
}
|
|
82
|
+
const normCtx = (0, normalizer_js_1.createNormalizeContext)();
|
|
83
|
+
try {
|
|
84
|
+
const fetchImpl = this.config.fetch ?? globalThis.fetch;
|
|
85
|
+
const url = this.config.url;
|
|
86
|
+
const buildBody = this.config.buildRequestBody ?? DEFAULT_BUILD_BODY;
|
|
87
|
+
const buildHeaders = this.config.buildHeaders ?? DEFAULT_BUILD_HEADERS;
|
|
88
|
+
const response = await fetchImpl(url, {
|
|
89
|
+
method: 'POST',
|
|
90
|
+
headers: { ...buildHeaders(this.config), ...extraHeaders },
|
|
91
|
+
body: JSON.stringify({ ...buildBody(query, this.config), ...extraBody }),
|
|
92
|
+
signal: combinedSignal,
|
|
93
|
+
cache: 'no-store',
|
|
94
|
+
});
|
|
95
|
+
if (!response.ok) {
|
|
96
|
+
const errorText = await response.text();
|
|
97
|
+
throw new Error(`HTTP ${response.status}: ${errorText || response.statusText}`);
|
|
98
|
+
}
|
|
99
|
+
if (!response.body) {
|
|
100
|
+
throw new Error('Response body is empty');
|
|
101
|
+
}
|
|
102
|
+
await (0, parser_js_1.readSSEStream)(response.body, (rawChunk) => {
|
|
103
|
+
if (shouldEmitRaw)
|
|
104
|
+
emitRaw(rawChunk);
|
|
105
|
+
if (shouldEmitNormalized) {
|
|
106
|
+
const events = (0, normalizer_js_1.normalizeRawChunk)(rawChunk, normCtx);
|
|
107
|
+
events.forEach(emit);
|
|
108
|
+
}
|
|
109
|
+
}, combinedSignal);
|
|
110
|
+
if (shouldEmitNormalized) {
|
|
111
|
+
(0, normalizer_js_1.finalizeNormalizeContext)(normCtx).forEach(emit);
|
|
112
|
+
emit({ type: 'stream_end', timestamp: Date.now() });
|
|
113
|
+
}
|
|
114
|
+
handlers.onComplete?.();
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
if (combinedSignal.aborted)
|
|
118
|
+
return;
|
|
119
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
120
|
+
handlers.onError?.(err);
|
|
121
|
+
if (shouldEmitNormalized) {
|
|
122
|
+
emit({
|
|
123
|
+
type: 'error',
|
|
124
|
+
timestamp: Date.now(),
|
|
125
|
+
message: err.message,
|
|
126
|
+
});
|
|
127
|
+
emit({ type: 'stream_end', timestamp: Date.now() });
|
|
128
|
+
}
|
|
129
|
+
throw err;
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
if (this.abortController === controller) {
|
|
133
|
+
this.abortController = null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* 以 AsyncIterable 消费流式事件(便于 for-await)
|
|
139
|
+
*/
|
|
140
|
+
async *streamEvents(options) {
|
|
141
|
+
const queue = [];
|
|
142
|
+
let resolveWait = null;
|
|
143
|
+
let done = false;
|
|
144
|
+
let streamError = null;
|
|
145
|
+
const notify = () => {
|
|
146
|
+
resolveWait?.();
|
|
147
|
+
resolveWait = null;
|
|
148
|
+
};
|
|
149
|
+
const runPromise = this.chatStream({ ...options, dataMode: options.dataMode ?? 'normalized' }, {
|
|
150
|
+
onEvent: (event) => {
|
|
151
|
+
queue.push(event);
|
|
152
|
+
notify();
|
|
153
|
+
},
|
|
154
|
+
onError: (err) => {
|
|
155
|
+
streamError = err;
|
|
156
|
+
done = true;
|
|
157
|
+
notify();
|
|
158
|
+
},
|
|
159
|
+
onComplete: () => {
|
|
160
|
+
done = true;
|
|
161
|
+
notify();
|
|
162
|
+
},
|
|
163
|
+
}).catch((err) => {
|
|
164
|
+
streamError = err;
|
|
165
|
+
done = true;
|
|
166
|
+
notify();
|
|
167
|
+
});
|
|
168
|
+
try {
|
|
169
|
+
while (true) {
|
|
170
|
+
if (queue.length > 0) {
|
|
171
|
+
yield queue.shift();
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (done)
|
|
175
|
+
break;
|
|
176
|
+
await new Promise((resolve) => {
|
|
177
|
+
resolveWait = resolve;
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
if (streamError)
|
|
181
|
+
throw streamError;
|
|
182
|
+
}
|
|
183
|
+
finally {
|
|
184
|
+
await runPromise.catch(() => undefined);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
exports.AgentChatClient = AgentChatClient;
|
|
189
|
+
function combineAbortSignals(...signals) {
|
|
190
|
+
const controller = new AbortController();
|
|
191
|
+
const onAbort = () => controller.abort();
|
|
192
|
+
signals.forEach((s) => {
|
|
193
|
+
if (s.aborted)
|
|
194
|
+
controller.abort();
|
|
195
|
+
else
|
|
196
|
+
s.addEventListener('abort', onAbort, { once: true });
|
|
197
|
+
});
|
|
198
|
+
return controller.signal;
|
|
199
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.stripKnowledgeSupLabels = exports.parseKnowledgeSupLabels = exports.REACT_SSE_TYPE_MAP = exports.applyEventToSession = exports.createSessionState = exports.finalizeNormalizeContext = exports.createNormalizeContext = exports.normalizeRawChunk = exports.isStreamEndSignal = exports.readSSEStream = exports.parseSSELine = exports.AgentChatClient = void 0;
|
|
4
|
+
var client_js_1 = require("./client.js");
|
|
5
|
+
Object.defineProperty(exports, "AgentChatClient", { enumerable: true, get: function () { return client_js_1.AgentChatClient; } });
|
|
6
|
+
var parser_js_1 = require("./parser.js");
|
|
7
|
+
Object.defineProperty(exports, "parseSSELine", { enumerable: true, get: function () { return parser_js_1.parseSSELine; } });
|
|
8
|
+
Object.defineProperty(exports, "readSSEStream", { enumerable: true, get: function () { return parser_js_1.readSSEStream; } });
|
|
9
|
+
Object.defineProperty(exports, "isStreamEndSignal", { enumerable: true, get: function () { return parser_js_1.isStreamEndSignal; } });
|
|
10
|
+
var normalizer_js_1 = require("./normalizer.js");
|
|
11
|
+
Object.defineProperty(exports, "normalizeRawChunk", { enumerable: true, get: function () { return normalizer_js_1.normalizeRawChunk; } });
|
|
12
|
+
Object.defineProperty(exports, "createNormalizeContext", { enumerable: true, get: function () { return normalizer_js_1.createNormalizeContext; } });
|
|
13
|
+
Object.defineProperty(exports, "finalizeNormalizeContext", { enumerable: true, get: function () { return normalizer_js_1.finalizeNormalizeContext; } });
|
|
14
|
+
var session_js_1 = require("./session.js");
|
|
15
|
+
Object.defineProperty(exports, "createSessionState", { enumerable: true, get: function () { return session_js_1.createSessionState; } });
|
|
16
|
+
Object.defineProperty(exports, "applyEventToSession", { enumerable: true, get: function () { return session_js_1.applyEventToSession; } });
|
|
17
|
+
var react_flow_js_1 = require("./react-flow.js");
|
|
18
|
+
Object.defineProperty(exports, "REACT_SSE_TYPE_MAP", { enumerable: true, get: function () { return react_flow_js_1.REACT_SSE_TYPE_MAP; } });
|
|
19
|
+
var knowledge_label_js_1 = require("./knowledge-label.js");
|
|
20
|
+
Object.defineProperty(exports, "parseKnowledgeSupLabels", { enumerable: true, get: function () { return knowledge_label_js_1.parseKnowledgeSupLabels; } });
|
|
21
|
+
Object.defineProperty(exports, "stripKnowledgeSupLabels", { enumerable: true, get: function () { return knowledge_label_js_1.stripKnowledgeSupLabels; } });
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* 知识库引用标注(answer 正文中的 <sup> 标签)解析
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* `<sup score="0.2" fileid="doc_001" filename="FAQ.txt" chunkid="4191..." dsid="docstore_xxx">2</sup>`
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.parseKnowledgeSupLabels = parseKnowledgeSupLabels;
|
|
10
|
+
exports.stripKnowledgeSupLabels = stripKnowledgeSupLabels;
|
|
11
|
+
// \s* 兼容无属性写法 <sup>1</sup>
|
|
12
|
+
const SUP_TAG_REGEX = /<sup\s*([^>]*)>([\s\S]*?)<\/sup>/gi;
|
|
13
|
+
const ATTR_REGEX = /([\w-]+)\s*=\s*"([^"]*)"/g;
|
|
14
|
+
function parseSupAttributes(attrString) {
|
|
15
|
+
const attributes = {};
|
|
16
|
+
let match;
|
|
17
|
+
ATTR_REGEX.lastIndex = 0;
|
|
18
|
+
while ((match = ATTR_REGEX.exec(attrString)) !== null) {
|
|
19
|
+
attributes[match[1].toLowerCase()] = match[2];
|
|
20
|
+
}
|
|
21
|
+
return attributes;
|
|
22
|
+
}
|
|
23
|
+
function toCitationLabel(raw, attrString, inner) {
|
|
24
|
+
const attributes = parseSupAttributes(attrString);
|
|
25
|
+
return {
|
|
26
|
+
raw,
|
|
27
|
+
index: inner.trim(),
|
|
28
|
+
score: attributes.score,
|
|
29
|
+
fileId: attributes.fileid,
|
|
30
|
+
filename: attributes.filename,
|
|
31
|
+
chunkId: attributes.chunkid,
|
|
32
|
+
datasetId: attributes.dsid,
|
|
33
|
+
attributes,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* 解析正文中的知识库 <sup> 引用标签
|
|
38
|
+
*
|
|
39
|
+
* @param content - 含 sup 标签的回复正文
|
|
40
|
+
* @returns 剥离标签后的正文与标注列表
|
|
41
|
+
*/
|
|
42
|
+
function parseKnowledgeSupLabels(content) {
|
|
43
|
+
const labels = [];
|
|
44
|
+
const text = content.replace(SUP_TAG_REGEX, (match, attrPart, inner) => {
|
|
45
|
+
labels.push(toCitationLabel(match, attrPart, inner));
|
|
46
|
+
return '';
|
|
47
|
+
});
|
|
48
|
+
return { text, labels };
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* 去掉正文中的知识库 sup 标签(便于暂不渲染引用角标)
|
|
52
|
+
*
|
|
53
|
+
* @param content - 原始正文
|
|
54
|
+
* @param options.stripIncomplete - 是否去掉末尾未闭合的 `<sup`(流式场景建议 true)
|
|
55
|
+
*/
|
|
56
|
+
function stripKnowledgeSupLabels(content, options) {
|
|
57
|
+
const { text } = parseKnowledgeSupLabels(content);
|
|
58
|
+
if (options?.stripIncomplete === false)
|
|
59
|
+
return text;
|
|
60
|
+
return stripIncompleteSupTail(text);
|
|
61
|
+
}
|
|
62
|
+
/** 去掉末尾未闭合的 sup 片段,避免流式过程中露出半截标签 */
|
|
63
|
+
function stripIncompleteSupTail(text) {
|
|
64
|
+
const openIdx = text.lastIndexOf('<sup');
|
|
65
|
+
if (openIdx === -1)
|
|
66
|
+
return text;
|
|
67
|
+
const tail = text.slice(openIdx);
|
|
68
|
+
if (/<\/sup>/i.test(tail))
|
|
69
|
+
return text;
|
|
70
|
+
return text.slice(0, openIdx);
|
|
71
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createNormalizeContext = createNormalizeContext;
|
|
4
|
+
exports.normalizeRawChunk = normalizeRawChunk;
|
|
5
|
+
exports.finalizeNormalizeContext = finalizeNormalizeContext;
|
|
6
|
+
const ANSWER_TYPES = new Set(['answer', 'text', 'message']);
|
|
7
|
+
const STREAM_END_TYPES = new Set(['done', 'end', 'finish', 'completed', 'stop']);
|
|
8
|
+
const TOOL_TYPES = new Set(['tool', 'knowledge', 'plugin', 'mcp', 'memory_var_update', 'workflow']);
|
|
9
|
+
let toolIdSeq = 0;
|
|
10
|
+
function nextToolId() {
|
|
11
|
+
toolIdSeq += 1;
|
|
12
|
+
return `tool_${toolIdSeq}_${Date.now()}`;
|
|
13
|
+
}
|
|
14
|
+
function createNormalizeContext() {
|
|
15
|
+
return {
|
|
16
|
+
thinkingContent: '',
|
|
17
|
+
answerContent: '',
|
|
18
|
+
isThinking: false,
|
|
19
|
+
thinkingRound: 0,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function isToolType(type) {
|
|
23
|
+
return TOOL_TYPES.has(type);
|
|
24
|
+
}
|
|
25
|
+
function resolveCallKind(type) {
|
|
26
|
+
if (type === 'knowledge')
|
|
27
|
+
return 'knowledge';
|
|
28
|
+
if (type === 'plugin')
|
|
29
|
+
return 'plugin';
|
|
30
|
+
if (type === 'mcp')
|
|
31
|
+
return 'mcp';
|
|
32
|
+
if (type === 'memory_var_update')
|
|
33
|
+
return 'memory';
|
|
34
|
+
if (type === 'workflow')
|
|
35
|
+
return 'workflow';
|
|
36
|
+
return 'tool';
|
|
37
|
+
}
|
|
38
|
+
/** 规范化耗时(去掉末尾 s) */
|
|
39
|
+
function normalizeRunningTime(value) {
|
|
40
|
+
if (!value)
|
|
41
|
+
return undefined;
|
|
42
|
+
return String(value).replace(/s$/i, '').trim();
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* 拼接回答正文:兼容增量 delta 与服务端推送的累积全文
|
|
46
|
+
*/
|
|
47
|
+
function appendAnswerContent(ctx, incoming) {
|
|
48
|
+
if (!incoming)
|
|
49
|
+
return;
|
|
50
|
+
if (!ctx.answerContent) {
|
|
51
|
+
ctx.answerContent = incoming;
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (incoming.startsWith(ctx.answerContent)) {
|
|
55
|
+
ctx.answerContent = incoming;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (ctx.answerContent.endsWith(incoming))
|
|
59
|
+
return;
|
|
60
|
+
ctx.answerContent += incoming;
|
|
61
|
+
}
|
|
62
|
+
function resolveCallTitle(kind, raw) {
|
|
63
|
+
if (kind === 'knowledge')
|
|
64
|
+
return '知识库调用';
|
|
65
|
+
if (kind === 'plugin')
|
|
66
|
+
return '插件调用';
|
|
67
|
+
if (kind === 'mcp')
|
|
68
|
+
return 'MCP调用';
|
|
69
|
+
if (kind === 'memory') {
|
|
70
|
+
const name = raw?.name;
|
|
71
|
+
if (typeof name === 'string' && name)
|
|
72
|
+
return `记忆变量 ${name}`;
|
|
73
|
+
return '记忆变量更新';
|
|
74
|
+
}
|
|
75
|
+
if (kind === 'workflow') {
|
|
76
|
+
if (typeof raw?.tool_name === 'string' && raw.tool_name)
|
|
77
|
+
return String(raw.tool_name);
|
|
78
|
+
return '工作流调用';
|
|
79
|
+
}
|
|
80
|
+
if (typeof raw?.tool_name === 'string' && raw.tool_name)
|
|
81
|
+
return String(raw.tool_name);
|
|
82
|
+
return '工具调用';
|
|
83
|
+
}
|
|
84
|
+
function resolveToolId(chunk) {
|
|
85
|
+
if (chunk.type === 'workflow' && chunk.raw) {
|
|
86
|
+
if (typeof chunk.raw.tool_call_id === 'string')
|
|
87
|
+
return chunk.raw.tool_call_id;
|
|
88
|
+
if (typeof chunk.raw.workflow_id === 'string')
|
|
89
|
+
return chunk.raw.workflow_id;
|
|
90
|
+
if (typeof chunk.raw.tool_id === 'string')
|
|
91
|
+
return chunk.raw.tool_id;
|
|
92
|
+
}
|
|
93
|
+
if (chunk.type === 'memory_var_update' && chunk.raw) {
|
|
94
|
+
const name = typeof chunk.raw.name === 'string' ? chunk.raw.name : 'var';
|
|
95
|
+
const conv = typeof chunk.raw.conversation_id === 'string' ? chunk.raw.conversation_id : '';
|
|
96
|
+
return `memory_${name}_${conv}`.slice(0, 120);
|
|
97
|
+
}
|
|
98
|
+
if (typeof chunk.raw?.tool_call_id === 'string')
|
|
99
|
+
return chunk.raw.tool_call_id;
|
|
100
|
+
if (typeof chunk.raw?.chunk_id === 'string')
|
|
101
|
+
return chunk.raw.chunk_id;
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
/** 结束当前思考阶段,进入 ReAct 下一步(工具调用或回答) */
|
|
105
|
+
function endThinkingPhase(ctx, events, timestamp) {
|
|
106
|
+
if (!ctx.isThinking)
|
|
107
|
+
return;
|
|
108
|
+
const content = ctx.thinkingContent.trim();
|
|
109
|
+
ctx.isThinking = false;
|
|
110
|
+
if (content) {
|
|
111
|
+
events.push({
|
|
112
|
+
type: 'thinking_end',
|
|
113
|
+
timestamp,
|
|
114
|
+
content,
|
|
115
|
+
runningTime: normalizeRunningTime(ctx.thinkingRunningTime),
|
|
116
|
+
round: ctx.thinkingRound,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
ctx.thinkingContent = '';
|
|
120
|
+
ctx.thinkingRunningTime = undefined;
|
|
121
|
+
}
|
|
122
|
+
/** 开始新一轮思考(支持 ReAct 多轮:思考 → 工具 → 再思考) */
|
|
123
|
+
function startThinkingPhase(ctx, events, timestamp) {
|
|
124
|
+
if (ctx.isThinking)
|
|
125
|
+
return;
|
|
126
|
+
ctx.thinkingRound += 1;
|
|
127
|
+
ctx.isThinking = true;
|
|
128
|
+
ctx.thinkingContent = '';
|
|
129
|
+
ctx.thinkingRunningTime = undefined;
|
|
130
|
+
events.push({
|
|
131
|
+
type: 'thinking_start',
|
|
132
|
+
timestamp,
|
|
133
|
+
round: ctx.thinkingRound,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
function emitToolStart(ctx, events, timestamp, callKind, title, toolId) {
|
|
137
|
+
const id = toolId ?? nextToolId();
|
|
138
|
+
ctx.activeToolId = id;
|
|
139
|
+
events.push({
|
|
140
|
+
type: callKind === 'knowledge' ? 'knowledge_call_start' : 'tool_call_start',
|
|
141
|
+
timestamp,
|
|
142
|
+
toolId: id,
|
|
143
|
+
title,
|
|
144
|
+
callKind,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
function emitToolEnd(ctx, events, timestamp, callKind, title, content, runningTime, toolId) {
|
|
148
|
+
const id = toolId ?? ctx.activeToolId ?? nextToolId();
|
|
149
|
+
events.push({
|
|
150
|
+
type: callKind === 'knowledge' ? 'knowledge_call_end' : 'tool_call_end',
|
|
151
|
+
timestamp,
|
|
152
|
+
toolId: id,
|
|
153
|
+
title,
|
|
154
|
+
content,
|
|
155
|
+
runningTime: normalizeRunningTime(runningTime),
|
|
156
|
+
callKind,
|
|
157
|
+
});
|
|
158
|
+
if (ctx.activeToolId === id)
|
|
159
|
+
ctx.activeToolId = undefined;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* ReAct 流式事件归一化
|
|
163
|
+
*
|
|
164
|
+
* 交互时序:
|
|
165
|
+
* 1. thinking_start → thinking_delta* → thinking_end(进入工具/MCP/工作流,或开始输出 answer 时收尾)
|
|
166
|
+
* 2. tool_call_start → tool_call_end(工具调用中 → 完成)
|
|
167
|
+
* 3. 可重复 1-2 多轮
|
|
168
|
+
* 4. answer_delta* 流式输出最终结果(不触发 thinking_end,避免 answer-toolsnode 空包多计一轮)
|
|
169
|
+
*/
|
|
170
|
+
function normalizeRawChunk(chunk, ctx) {
|
|
171
|
+
const timestamp = Date.now();
|
|
172
|
+
const events = [];
|
|
173
|
+
if (chunk.type === 'error') {
|
|
174
|
+
endThinkingPhase(ctx, events, timestamp);
|
|
175
|
+
events.push({
|
|
176
|
+
type: 'error',
|
|
177
|
+
timestamp,
|
|
178
|
+
message: chunk.error || chunk.content || '执行出错,请稍后重试',
|
|
179
|
+
detail: chunk.error_detail,
|
|
180
|
+
});
|
|
181
|
+
return events;
|
|
182
|
+
}
|
|
183
|
+
if (chunk.type === 'thinking') {
|
|
184
|
+
if (chunk.running_time) {
|
|
185
|
+
ctx.thinkingRunningTime = normalizeRunningTime(chunk.running_time);
|
|
186
|
+
}
|
|
187
|
+
startThinkingPhase(ctx, events, timestamp);
|
|
188
|
+
ctx.thinkingContent += chunk.content;
|
|
189
|
+
events.push({
|
|
190
|
+
type: 'thinking_delta',
|
|
191
|
+
timestamp,
|
|
192
|
+
delta: chunk.content,
|
|
193
|
+
content: ctx.thinkingContent,
|
|
194
|
+
round: ctx.thinkingRound,
|
|
195
|
+
});
|
|
196
|
+
return events;
|
|
197
|
+
}
|
|
198
|
+
if (isToolType(chunk.type)) {
|
|
199
|
+
endThinkingPhase(ctx, events, timestamp);
|
|
200
|
+
const callKind = resolveCallKind(chunk.type);
|
|
201
|
+
const title = resolveCallTitle(callKind, chunk.raw);
|
|
202
|
+
const rawId = resolveToolId(chunk);
|
|
203
|
+
if (chunk.running_time) {
|
|
204
|
+
if (!ctx.activeToolId) {
|
|
205
|
+
emitToolStart(ctx, events, timestamp, callKind, title, rawId);
|
|
206
|
+
}
|
|
207
|
+
emitToolEnd(ctx, events, timestamp, callKind, title, chunk.content, chunk.running_time, rawId);
|
|
208
|
+
}
|
|
209
|
+
else if (chunk.content) {
|
|
210
|
+
emitToolEnd(ctx, events, timestamp, callKind, title, chunk.content, undefined, rawId);
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
emitToolStart(ctx, events, timestamp, callKind, title, rawId);
|
|
214
|
+
}
|
|
215
|
+
return events;
|
|
216
|
+
}
|
|
217
|
+
if (ANSWER_TYPES.has(chunk.type)) {
|
|
218
|
+
// 思考阶段结束、开始输出最终回答时立即 thinking_end,避免 loading 持续到流结束
|
|
219
|
+
endThinkingPhase(ctx, events, timestamp);
|
|
220
|
+
const delta = chunk.content;
|
|
221
|
+
appendAnswerContent(ctx, delta);
|
|
222
|
+
events.push({
|
|
223
|
+
type: 'answer_delta',
|
|
224
|
+
timestamp,
|
|
225
|
+
delta,
|
|
226
|
+
content: ctx.answerContent,
|
|
227
|
+
});
|
|
228
|
+
return events;
|
|
229
|
+
}
|
|
230
|
+
if (STREAM_END_TYPES.has(chunk.type)) {
|
|
231
|
+
endThinkingPhase(ctx, events, timestamp);
|
|
232
|
+
events.push({ type: 'stream_end', timestamp });
|
|
233
|
+
return events;
|
|
234
|
+
}
|
|
235
|
+
if (chunk.content &&
|
|
236
|
+
chunk.type !== 'thinking' &&
|
|
237
|
+
!isToolType(chunk.type) &&
|
|
238
|
+
chunk.type !== 'error') {
|
|
239
|
+
endThinkingPhase(ctx, events, timestamp);
|
|
240
|
+
const delta = chunk.content;
|
|
241
|
+
appendAnswerContent(ctx, delta);
|
|
242
|
+
events.push({
|
|
243
|
+
type: 'answer_delta',
|
|
244
|
+
timestamp,
|
|
245
|
+
delta,
|
|
246
|
+
content: ctx.answerContent,
|
|
247
|
+
});
|
|
248
|
+
return events;
|
|
249
|
+
}
|
|
250
|
+
events.push({ type: 'raw', timestamp, chunk });
|
|
251
|
+
return events;
|
|
252
|
+
}
|
|
253
|
+
function finalizeNormalizeContext(ctx) {
|
|
254
|
+
const events = [];
|
|
255
|
+
endThinkingPhase(ctx, events, Date.now());
|
|
256
|
+
return events;
|
|
257
|
+
}
|