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 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAC7E,OAAO,EACL,iBAAiB,EACjB,sBAAsB,EACtB,wBAAwB,GACzB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACvE,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,EACL,uBAAuB,EACvB,uBAAuB,GACxB,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EAAE,sBAAsB,EAAE,0BAA0B,EAAE,MAAM,sBAAsB,CAAC;AAE/F,YAAY,EACV,qBAAqB,EACrB,iBAAiB,EACjB,oBAAoB,EACpB,qBAAqB,EACrB,uBAAuB,EACvB,sBAAsB,EACtB,gBAAgB,EAChB,oBAAoB,EACpB,WAAW,EACX,gBAAgB,EAChB,cAAc,EACd,kBAAkB,EAClB,kBAAkB,EAClB,gBAAgB,EAChB,gBAAgB,EAChB,kBAAkB,EAClB,gBAAgB,EAChB,eAAe,EACf,UAAU,EACV,QAAQ,GACT,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { AgentChatClient } from './client.js';
|
|
2
|
+
export { parseSSELine, readSSEStream, isStreamEndSignal } from './parser.js';
|
|
3
|
+
export { normalizeRawChunk, createNormalizeContext, finalizeNormalizeContext, } from './normalizer.js';
|
|
4
|
+
export { createSessionState, applyEventToSession } from './session.js';
|
|
5
|
+
export { REACT_SSE_TYPE_MAP } from './react-flow.js';
|
|
6
|
+
export { parseKnowledgeSupLabels, stripKnowledgeSupLabels, } from './knowledge-label.js';
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 知识库引用标注(answer 正文中的 <sup> 标签)解析
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* `<sup score="0.2" fileid="doc_001" filename="FAQ.txt" chunkid="4191..." dsid="docstore_xxx">2</sup>`
|
|
6
|
+
*/
|
|
7
|
+
/** 单条知识库引用标注 */
|
|
8
|
+
export interface KnowledgeCitationLabel {
|
|
9
|
+
/** 原始 sup 标签全文 */
|
|
10
|
+
raw: string;
|
|
11
|
+
/** 标签内展示的引用序号(如 "2") */
|
|
12
|
+
index: string;
|
|
13
|
+
/** 相关度分数 */
|
|
14
|
+
score?: string;
|
|
15
|
+
/** 文档 ID(fileid) */
|
|
16
|
+
fileId?: string;
|
|
17
|
+
/** 文档名称(filename) */
|
|
18
|
+
filename?: string;
|
|
19
|
+
/** 切片 ID(chunkid) */
|
|
20
|
+
chunkId?: string;
|
|
21
|
+
/** 知识库 / 数据集 ID(dsid) */
|
|
22
|
+
datasetId?: string;
|
|
23
|
+
/** 标签上其余属性(小写 key) */
|
|
24
|
+
attributes: Record<string, string>;
|
|
25
|
+
}
|
|
26
|
+
/** 解析结果 */
|
|
27
|
+
export interface ParseKnowledgeLabelsResult {
|
|
28
|
+
/** 去掉 sup 标签后的正文 */
|
|
29
|
+
text: string;
|
|
30
|
+
/** 按出现顺序的引用标注 */
|
|
31
|
+
labels: KnowledgeCitationLabel[];
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* 解析正文中的知识库 <sup> 引用标签
|
|
35
|
+
*
|
|
36
|
+
* @param content - 含 sup 标签的回复正文
|
|
37
|
+
* @returns 剥离标签后的正文与标注列表
|
|
38
|
+
*/
|
|
39
|
+
export declare function parseKnowledgeSupLabels(content: string): ParseKnowledgeLabelsResult;
|
|
40
|
+
/**
|
|
41
|
+
* 去掉正文中的知识库 sup 标签(便于暂不渲染引用角标)
|
|
42
|
+
*
|
|
43
|
+
* @param content - 原始正文
|
|
44
|
+
* @param options.stripIncomplete - 是否去掉末尾未闭合的 `<sup`(流式场景建议 true)
|
|
45
|
+
*/
|
|
46
|
+
export declare function stripKnowledgeSupLabels(content: string, options?: {
|
|
47
|
+
stripIncomplete?: boolean;
|
|
48
|
+
}): string;
|
|
49
|
+
//# sourceMappingURL=knowledge-label.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"knowledge-label.d.ts","sourceRoot":"","sources":["../../src/knowledge-label.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,gBAAgB;AAChB,MAAM,WAAW,sBAAsB;IACrC,kBAAkB;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,wBAAwB;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,YAAY;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,oBAAoB;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,qBAAqB;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,qBAAqB;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yBAAyB;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sBAAsB;IACtB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACpC;AAED,WAAW;AACX,MAAM,WAAW,0BAA0B;IACzC,oBAAoB;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,iBAAiB;IACjB,MAAM,EAAE,sBAAsB,EAAE,CAAC;CAClC;AA8BD;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG,0BAA0B,CAOnF;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE;IAAE,eAAe,CAAC,EAAE,OAAO,CAAA;CAAE,GACtC,MAAM,CAIR"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 知识库引用标注(answer 正文中的 <sup> 标签)解析
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* `<sup score="0.2" fileid="doc_001" filename="FAQ.txt" chunkid="4191..." dsid="docstore_xxx">2</sup>`
|
|
6
|
+
*/
|
|
7
|
+
// \s* 兼容无属性写法 <sup>1</sup>
|
|
8
|
+
const SUP_TAG_REGEX = /<sup\s*([^>]*)>([\s\S]*?)<\/sup>/gi;
|
|
9
|
+
const ATTR_REGEX = /([\w-]+)\s*=\s*"([^"]*)"/g;
|
|
10
|
+
function parseSupAttributes(attrString) {
|
|
11
|
+
const attributes = {};
|
|
12
|
+
let match;
|
|
13
|
+
ATTR_REGEX.lastIndex = 0;
|
|
14
|
+
while ((match = ATTR_REGEX.exec(attrString)) !== null) {
|
|
15
|
+
attributes[match[1].toLowerCase()] = match[2];
|
|
16
|
+
}
|
|
17
|
+
return attributes;
|
|
18
|
+
}
|
|
19
|
+
function toCitationLabel(raw, attrString, inner) {
|
|
20
|
+
const attributes = parseSupAttributes(attrString);
|
|
21
|
+
return {
|
|
22
|
+
raw,
|
|
23
|
+
index: inner.trim(),
|
|
24
|
+
score: attributes.score,
|
|
25
|
+
fileId: attributes.fileid,
|
|
26
|
+
filename: attributes.filename,
|
|
27
|
+
chunkId: attributes.chunkid,
|
|
28
|
+
datasetId: attributes.dsid,
|
|
29
|
+
attributes,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 解析正文中的知识库 <sup> 引用标签
|
|
34
|
+
*
|
|
35
|
+
* @param content - 含 sup 标签的回复正文
|
|
36
|
+
* @returns 剥离标签后的正文与标注列表
|
|
37
|
+
*/
|
|
38
|
+
export function parseKnowledgeSupLabels(content) {
|
|
39
|
+
const labels = [];
|
|
40
|
+
const text = content.replace(SUP_TAG_REGEX, (match, attrPart, inner) => {
|
|
41
|
+
labels.push(toCitationLabel(match, attrPart, inner));
|
|
42
|
+
return '';
|
|
43
|
+
});
|
|
44
|
+
return { text, labels };
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* 去掉正文中的知识库 sup 标签(便于暂不渲染引用角标)
|
|
48
|
+
*
|
|
49
|
+
* @param content - 原始正文
|
|
50
|
+
* @param options.stripIncomplete - 是否去掉末尾未闭合的 `<sup`(流式场景建议 true)
|
|
51
|
+
*/
|
|
52
|
+
export function stripKnowledgeSupLabels(content, options) {
|
|
53
|
+
const { text } = parseKnowledgeSupLabels(content);
|
|
54
|
+
if (options?.stripIncomplete === false)
|
|
55
|
+
return text;
|
|
56
|
+
return stripIncompleteSupTail(text);
|
|
57
|
+
}
|
|
58
|
+
/** 去掉末尾未闭合的 sup 片段,避免流式过程中露出半截标签 */
|
|
59
|
+
function stripIncompleteSupTail(text) {
|
|
60
|
+
const openIdx = text.lastIndexOf('<sup');
|
|
61
|
+
if (openIdx === -1)
|
|
62
|
+
return text;
|
|
63
|
+
const tail = text.slice(openIdx);
|
|
64
|
+
if (/<\/sup>/i.test(tail))
|
|
65
|
+
return text;
|
|
66
|
+
return text.slice(0, openIdx);
|
|
67
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { AgentStreamEvent, RawSSEChunk } from './types.js';
|
|
2
|
+
export interface NormalizeContext {
|
|
3
|
+
thinkingContent: string;
|
|
4
|
+
answerContent: string;
|
|
5
|
+
isThinking: boolean;
|
|
6
|
+
thinkingRound: number;
|
|
7
|
+
thinkingRunningTime?: string;
|
|
8
|
+
activeToolId?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function createNormalizeContext(): NormalizeContext;
|
|
11
|
+
/**
|
|
12
|
+
* ReAct 流式事件归一化
|
|
13
|
+
*
|
|
14
|
+
* 交互时序:
|
|
15
|
+
* 1. thinking_start → thinking_delta* → thinking_end(进入工具/MCP/工作流,或开始输出 answer 时收尾)
|
|
16
|
+
* 2. tool_call_start → tool_call_end(工具调用中 → 完成)
|
|
17
|
+
* 3. 可重复 1-2 多轮
|
|
18
|
+
* 4. answer_delta* 流式输出最终结果(不触发 thinking_end,避免 answer-toolsnode 空包多计一轮)
|
|
19
|
+
*/
|
|
20
|
+
export declare function normalizeRawChunk(chunk: RawSSEChunk, ctx: NormalizeContext): AgentStreamEvent[];
|
|
21
|
+
export declare function finalizeNormalizeContext(ctx: NormalizeContext): AgentStreamEvent[];
|
|
22
|
+
//# sourceMappingURL=normalizer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"normalizer.d.ts","sourceRoot":"","sources":["../../src/normalizer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAmB,WAAW,EAAE,MAAM,YAAY,CAAC;AAa5E,MAAM,WAAW,gBAAgB;IAC/B,eAAe,EAAE,MAAM,CAAC;IACxB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,OAAO,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,wBAAgB,sBAAsB,IAAI,gBAAgB,CAOzD;AAoJD;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,WAAW,EAClB,GAAG,EAAE,gBAAgB,GACpB,gBAAgB,EAAE,CAoGpB;AAED,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,gBAAgB,GAAG,gBAAgB,EAAE,CAIlF"}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
const ANSWER_TYPES = new Set(['answer', 'text', 'message']);
|
|
2
|
+
const STREAM_END_TYPES = new Set(['done', 'end', 'finish', 'completed', 'stop']);
|
|
3
|
+
const TOOL_TYPES = new Set(['tool', 'knowledge', 'plugin', 'mcp', 'memory_var_update', 'workflow']);
|
|
4
|
+
let toolIdSeq = 0;
|
|
5
|
+
function nextToolId() {
|
|
6
|
+
toolIdSeq += 1;
|
|
7
|
+
return `tool_${toolIdSeq}_${Date.now()}`;
|
|
8
|
+
}
|
|
9
|
+
export function createNormalizeContext() {
|
|
10
|
+
return {
|
|
11
|
+
thinkingContent: '',
|
|
12
|
+
answerContent: '',
|
|
13
|
+
isThinking: false,
|
|
14
|
+
thinkingRound: 0,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function isToolType(type) {
|
|
18
|
+
return TOOL_TYPES.has(type);
|
|
19
|
+
}
|
|
20
|
+
function resolveCallKind(type) {
|
|
21
|
+
if (type === 'knowledge')
|
|
22
|
+
return 'knowledge';
|
|
23
|
+
if (type === 'plugin')
|
|
24
|
+
return 'plugin';
|
|
25
|
+
if (type === 'mcp')
|
|
26
|
+
return 'mcp';
|
|
27
|
+
if (type === 'memory_var_update')
|
|
28
|
+
return 'memory';
|
|
29
|
+
if (type === 'workflow')
|
|
30
|
+
return 'workflow';
|
|
31
|
+
return 'tool';
|
|
32
|
+
}
|
|
33
|
+
/** 规范化耗时(去掉末尾 s) */
|
|
34
|
+
function normalizeRunningTime(value) {
|
|
35
|
+
if (!value)
|
|
36
|
+
return undefined;
|
|
37
|
+
return String(value).replace(/s$/i, '').trim();
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* 拼接回答正文:兼容增量 delta 与服务端推送的累积全文
|
|
41
|
+
*/
|
|
42
|
+
function appendAnswerContent(ctx, incoming) {
|
|
43
|
+
if (!incoming)
|
|
44
|
+
return;
|
|
45
|
+
if (!ctx.answerContent) {
|
|
46
|
+
ctx.answerContent = incoming;
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (incoming.startsWith(ctx.answerContent)) {
|
|
50
|
+
ctx.answerContent = incoming;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (ctx.answerContent.endsWith(incoming))
|
|
54
|
+
return;
|
|
55
|
+
ctx.answerContent += incoming;
|
|
56
|
+
}
|
|
57
|
+
function resolveCallTitle(kind, raw) {
|
|
58
|
+
if (kind === 'knowledge')
|
|
59
|
+
return '知识库调用';
|
|
60
|
+
if (kind === 'plugin')
|
|
61
|
+
return '插件调用';
|
|
62
|
+
if (kind === 'mcp')
|
|
63
|
+
return 'MCP调用';
|
|
64
|
+
if (kind === 'memory') {
|
|
65
|
+
const name = raw?.name;
|
|
66
|
+
if (typeof name === 'string' && name)
|
|
67
|
+
return `记忆变量 ${name}`;
|
|
68
|
+
return '记忆变量更新';
|
|
69
|
+
}
|
|
70
|
+
if (kind === 'workflow') {
|
|
71
|
+
if (typeof raw?.tool_name === 'string' && raw.tool_name)
|
|
72
|
+
return String(raw.tool_name);
|
|
73
|
+
return '工作流调用';
|
|
74
|
+
}
|
|
75
|
+
if (typeof raw?.tool_name === 'string' && raw.tool_name)
|
|
76
|
+
return String(raw.tool_name);
|
|
77
|
+
return '工具调用';
|
|
78
|
+
}
|
|
79
|
+
function resolveToolId(chunk) {
|
|
80
|
+
if (chunk.type === 'workflow' && chunk.raw) {
|
|
81
|
+
if (typeof chunk.raw.tool_call_id === 'string')
|
|
82
|
+
return chunk.raw.tool_call_id;
|
|
83
|
+
if (typeof chunk.raw.workflow_id === 'string')
|
|
84
|
+
return chunk.raw.workflow_id;
|
|
85
|
+
if (typeof chunk.raw.tool_id === 'string')
|
|
86
|
+
return chunk.raw.tool_id;
|
|
87
|
+
}
|
|
88
|
+
if (chunk.type === 'memory_var_update' && chunk.raw) {
|
|
89
|
+
const name = typeof chunk.raw.name === 'string' ? chunk.raw.name : 'var';
|
|
90
|
+
const conv = typeof chunk.raw.conversation_id === 'string' ? chunk.raw.conversation_id : '';
|
|
91
|
+
return `memory_${name}_${conv}`.slice(0, 120);
|
|
92
|
+
}
|
|
93
|
+
if (typeof chunk.raw?.tool_call_id === 'string')
|
|
94
|
+
return chunk.raw.tool_call_id;
|
|
95
|
+
if (typeof chunk.raw?.chunk_id === 'string')
|
|
96
|
+
return chunk.raw.chunk_id;
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
/** 结束当前思考阶段,进入 ReAct 下一步(工具调用或回答) */
|
|
100
|
+
function endThinkingPhase(ctx, events, timestamp) {
|
|
101
|
+
if (!ctx.isThinking)
|
|
102
|
+
return;
|
|
103
|
+
const content = ctx.thinkingContent.trim();
|
|
104
|
+
ctx.isThinking = false;
|
|
105
|
+
if (content) {
|
|
106
|
+
events.push({
|
|
107
|
+
type: 'thinking_end',
|
|
108
|
+
timestamp,
|
|
109
|
+
content,
|
|
110
|
+
runningTime: normalizeRunningTime(ctx.thinkingRunningTime),
|
|
111
|
+
round: ctx.thinkingRound,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
ctx.thinkingContent = '';
|
|
115
|
+
ctx.thinkingRunningTime = undefined;
|
|
116
|
+
}
|
|
117
|
+
/** 开始新一轮思考(支持 ReAct 多轮:思考 → 工具 → 再思考) */
|
|
118
|
+
function startThinkingPhase(ctx, events, timestamp) {
|
|
119
|
+
if (ctx.isThinking)
|
|
120
|
+
return;
|
|
121
|
+
ctx.thinkingRound += 1;
|
|
122
|
+
ctx.isThinking = true;
|
|
123
|
+
ctx.thinkingContent = '';
|
|
124
|
+
ctx.thinkingRunningTime = undefined;
|
|
125
|
+
events.push({
|
|
126
|
+
type: 'thinking_start',
|
|
127
|
+
timestamp,
|
|
128
|
+
round: ctx.thinkingRound,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
function emitToolStart(ctx, events, timestamp, callKind, title, toolId) {
|
|
132
|
+
const id = toolId ?? nextToolId();
|
|
133
|
+
ctx.activeToolId = id;
|
|
134
|
+
events.push({
|
|
135
|
+
type: callKind === 'knowledge' ? 'knowledge_call_start' : 'tool_call_start',
|
|
136
|
+
timestamp,
|
|
137
|
+
toolId: id,
|
|
138
|
+
title,
|
|
139
|
+
callKind,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
function emitToolEnd(ctx, events, timestamp, callKind, title, content, runningTime, toolId) {
|
|
143
|
+
const id = toolId ?? ctx.activeToolId ?? nextToolId();
|
|
144
|
+
events.push({
|
|
145
|
+
type: callKind === 'knowledge' ? 'knowledge_call_end' : 'tool_call_end',
|
|
146
|
+
timestamp,
|
|
147
|
+
toolId: id,
|
|
148
|
+
title,
|
|
149
|
+
content,
|
|
150
|
+
runningTime: normalizeRunningTime(runningTime),
|
|
151
|
+
callKind,
|
|
152
|
+
});
|
|
153
|
+
if (ctx.activeToolId === id)
|
|
154
|
+
ctx.activeToolId = undefined;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* ReAct 流式事件归一化
|
|
158
|
+
*
|
|
159
|
+
* 交互时序:
|
|
160
|
+
* 1. thinking_start → thinking_delta* → thinking_end(进入工具/MCP/工作流,或开始输出 answer 时收尾)
|
|
161
|
+
* 2. tool_call_start → tool_call_end(工具调用中 → 完成)
|
|
162
|
+
* 3. 可重复 1-2 多轮
|
|
163
|
+
* 4. answer_delta* 流式输出最终结果(不触发 thinking_end,避免 answer-toolsnode 空包多计一轮)
|
|
164
|
+
*/
|
|
165
|
+
export function normalizeRawChunk(chunk, ctx) {
|
|
166
|
+
const timestamp = Date.now();
|
|
167
|
+
const events = [];
|
|
168
|
+
if (chunk.type === 'error') {
|
|
169
|
+
endThinkingPhase(ctx, events, timestamp);
|
|
170
|
+
events.push({
|
|
171
|
+
type: 'error',
|
|
172
|
+
timestamp,
|
|
173
|
+
message: chunk.error || chunk.content || '执行出错,请稍后重试',
|
|
174
|
+
detail: chunk.error_detail,
|
|
175
|
+
});
|
|
176
|
+
return events;
|
|
177
|
+
}
|
|
178
|
+
if (chunk.type === 'thinking') {
|
|
179
|
+
if (chunk.running_time) {
|
|
180
|
+
ctx.thinkingRunningTime = normalizeRunningTime(chunk.running_time);
|
|
181
|
+
}
|
|
182
|
+
startThinkingPhase(ctx, events, timestamp);
|
|
183
|
+
ctx.thinkingContent += chunk.content;
|
|
184
|
+
events.push({
|
|
185
|
+
type: 'thinking_delta',
|
|
186
|
+
timestamp,
|
|
187
|
+
delta: chunk.content,
|
|
188
|
+
content: ctx.thinkingContent,
|
|
189
|
+
round: ctx.thinkingRound,
|
|
190
|
+
});
|
|
191
|
+
return events;
|
|
192
|
+
}
|
|
193
|
+
if (isToolType(chunk.type)) {
|
|
194
|
+
endThinkingPhase(ctx, events, timestamp);
|
|
195
|
+
const callKind = resolveCallKind(chunk.type);
|
|
196
|
+
const title = resolveCallTitle(callKind, chunk.raw);
|
|
197
|
+
const rawId = resolveToolId(chunk);
|
|
198
|
+
if (chunk.running_time) {
|
|
199
|
+
if (!ctx.activeToolId) {
|
|
200
|
+
emitToolStart(ctx, events, timestamp, callKind, title, rawId);
|
|
201
|
+
}
|
|
202
|
+
emitToolEnd(ctx, events, timestamp, callKind, title, chunk.content, chunk.running_time, rawId);
|
|
203
|
+
}
|
|
204
|
+
else if (chunk.content) {
|
|
205
|
+
emitToolEnd(ctx, events, timestamp, callKind, title, chunk.content, undefined, rawId);
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
emitToolStart(ctx, events, timestamp, callKind, title, rawId);
|
|
209
|
+
}
|
|
210
|
+
return events;
|
|
211
|
+
}
|
|
212
|
+
if (ANSWER_TYPES.has(chunk.type)) {
|
|
213
|
+
// 思考阶段结束、开始输出最终回答时立即 thinking_end,避免 loading 持续到流结束
|
|
214
|
+
endThinkingPhase(ctx, events, timestamp);
|
|
215
|
+
const delta = chunk.content;
|
|
216
|
+
appendAnswerContent(ctx, delta);
|
|
217
|
+
events.push({
|
|
218
|
+
type: 'answer_delta',
|
|
219
|
+
timestamp,
|
|
220
|
+
delta,
|
|
221
|
+
content: ctx.answerContent,
|
|
222
|
+
});
|
|
223
|
+
return events;
|
|
224
|
+
}
|
|
225
|
+
if (STREAM_END_TYPES.has(chunk.type)) {
|
|
226
|
+
endThinkingPhase(ctx, events, timestamp);
|
|
227
|
+
events.push({ type: 'stream_end', timestamp });
|
|
228
|
+
return events;
|
|
229
|
+
}
|
|
230
|
+
if (chunk.content &&
|
|
231
|
+
chunk.type !== 'thinking' &&
|
|
232
|
+
!isToolType(chunk.type) &&
|
|
233
|
+
chunk.type !== 'error') {
|
|
234
|
+
endThinkingPhase(ctx, events, timestamp);
|
|
235
|
+
const delta = chunk.content;
|
|
236
|
+
appendAnswerContent(ctx, delta);
|
|
237
|
+
events.push({
|
|
238
|
+
type: 'answer_delta',
|
|
239
|
+
timestamp,
|
|
240
|
+
delta,
|
|
241
|
+
content: ctx.answerContent,
|
|
242
|
+
});
|
|
243
|
+
return events;
|
|
244
|
+
}
|
|
245
|
+
events.push({ type: 'raw', timestamp, chunk });
|
|
246
|
+
return events;
|
|
247
|
+
}
|
|
248
|
+
export function finalizeNormalizeContext(ctx) {
|
|
249
|
+
const events = [];
|
|
250
|
+
endThinkingPhase(ctx, events, Date.now());
|
|
251
|
+
return events;
|
|
252
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { RawSSEChunk } from './types.js';
|
|
2
|
+
export declare function isStreamEndSignal(type: string, dataStr: string): boolean;
|
|
3
|
+
/**
|
|
4
|
+
* 解析单行 SSE 文本
|
|
5
|
+
* @returns 原始分片 | 'end' 流结束 | null 跳过
|
|
6
|
+
*/
|
|
7
|
+
export declare function parseSSELine(line: string): RawSSEChunk | 'end' | null;
|
|
8
|
+
/**
|
|
9
|
+
* 从 ReadableStream 读取并解析 SSE,逐条回调
|
|
10
|
+
*/
|
|
11
|
+
export declare function readSSEStream(body: ReadableStream<Uint8Array>, onChunk: (chunk: RawSSEChunk) => void, signal?: AbortSignal): Promise<void>;
|
|
12
|
+
//# sourceMappingURL=parser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../../src/parser.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAsKzC,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAExE;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,KAAK,GAAG,IAAI,CAkFrE;AAED;;GAEG;AACH,wBAAsB,aAAa,CACjC,IAAI,EAAE,cAAc,CAAC,UAAU,CAAC,EAChC,OAAO,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,EACrC,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,IAAI,CAAC,CAwCf"}
|