@baishuyun/chat-backend 0.0.9 → 0.0.10
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 +8 -0
- package/package.json +4 -4
- package/src/controllers/report/query/createQueryTransformStream.ts +79 -75
- package/src/controllers/report/query/handler-helpers.ts +35 -0
- package/src/controllers/report/query/handler-registry.ts +46 -0
- package/src/controllers/report/query/types.ts +9 -0
- package/src/controllers/report/query/utils.ts +46 -16
- package/src/utils/createJsonStreamTransformer.ts +14 -5
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@baishuyun/chat-backend",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.10",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
"parse-sse": "^0.1.0",
|
|
23
23
|
"pino": "^10.1.0",
|
|
24
24
|
"zod": "^4.1.13",
|
|
25
|
-
"@baishuyun/coze-provider": "0.0.
|
|
26
|
-
"@baishuyun/types": "1.0.
|
|
25
|
+
"@baishuyun/coze-provider": "0.0.10",
|
|
26
|
+
"@baishuyun/types": "1.0.10"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@types/config": "^3.3.5",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"pm2": "^6.0.14",
|
|
35
35
|
"tsx": "^4.7.1",
|
|
36
36
|
"typescript": "^5.8.3",
|
|
37
|
-
"@baishuyun/typescript-config": "0.0.
|
|
37
|
+
"@baishuyun/typescript-config": "0.0.10"
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
|
40
40
|
"dev": "cross-env NODE_ENV=development tsx watch src/index.ts",
|
|
@@ -5,18 +5,20 @@ import {
|
|
|
5
5
|
createJsonStreamTransformer,
|
|
6
6
|
type IParserCtx,
|
|
7
7
|
} from '../../../utils/createJsonStreamTransformer.js';
|
|
8
|
-
import {
|
|
8
|
+
import { isTargetElement } from './utils.js';
|
|
9
|
+
import { logger } from '../../../logger/index.js';
|
|
10
|
+
import { fieldHandlers } from './handler-registry.js';
|
|
9
11
|
|
|
10
12
|
export const createQueryResultTransformer = (): TransformStream<LanguageModelV2StreamPart, any> => {
|
|
11
13
|
return createJsonStreamTransformer<IQueryResult>({
|
|
12
14
|
createJSONParser: createJSONParser,
|
|
13
15
|
onParseValue: handleParsedValue,
|
|
14
16
|
onParseError(error) {
|
|
15
|
-
|
|
17
|
+
logger.error('JSON parsing error in query transformer: ', error);
|
|
16
18
|
return '数据解析异常,请重试';
|
|
17
19
|
},
|
|
18
20
|
onErrorChunk(chunk) {
|
|
19
|
-
|
|
21
|
+
logger.error('Received error chunk in query transformer');
|
|
20
22
|
return '';
|
|
21
23
|
},
|
|
22
24
|
});
|
|
@@ -26,96 +28,98 @@ function createJSONParser(): JSONParser {
|
|
|
26
28
|
return new JSONParser({
|
|
27
29
|
stringBufferSize: undefined,
|
|
28
30
|
numberBufferSize: undefined,
|
|
29
|
-
separator: '',
|
|
31
|
+
separator: '',
|
|
30
32
|
paths: [
|
|
31
|
-
'$', //
|
|
32
|
-
'$.title',
|
|
33
|
-
'$.
|
|
34
|
-
'$.
|
|
35
|
-
'$.
|
|
36
|
-
'$.
|
|
37
|
-
'$.
|
|
38
|
-
'$.
|
|
39
|
-
'$.
|
|
40
|
-
'$.
|
|
41
|
-
'$.
|
|
42
|
-
'$.
|
|
33
|
+
'$', // 保留完整结果推送
|
|
34
|
+
'$.title',
|
|
35
|
+
'$.name',
|
|
36
|
+
'$.type',
|
|
37
|
+
'$.userID',
|
|
38
|
+
'$.forms',
|
|
39
|
+
'$.fields.*',
|
|
40
|
+
'$.xFields.*',
|
|
41
|
+
'$.yFields.*',
|
|
42
|
+
'$.metrics.*',
|
|
43
|
+
'$.formulas.*',
|
|
44
|
+
'$.filter.rel',
|
|
45
|
+
'$.filter.cond.*',
|
|
46
|
+
'$.limit',
|
|
43
47
|
],
|
|
44
|
-
keepStack: true,
|
|
48
|
+
keepStack: true,
|
|
45
49
|
});
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
function handleParsedValue(ctx: IParserCtx<IQueryResult>): void {
|
|
49
|
-
const { parsedInfo,
|
|
50
|
-
const {
|
|
51
|
-
|
|
52
|
-
// 快速提取顶层字段
|
|
53
|
-
// @ts-ignore
|
|
54
|
-
if (key === 'title') parsedResult.title = value;
|
|
55
|
-
// @ts-ignore
|
|
56
|
-
if (key === 'userID') parsedResult.userID = value;
|
|
57
|
-
// @ts-ignore
|
|
58
|
-
if (key === 'limit') parsedResult.limit = value;
|
|
59
|
-
// @ts-ignore
|
|
60
|
-
if (key === 'forms') parsedResult.formIds = [value];
|
|
61
|
-
|
|
62
|
-
// 处理数组类型
|
|
63
|
-
// @ts-ignore
|
|
64
|
-
if (key === 'rel' && stack.length >= 1 && stack[stack.length - 1]?.key === 'filter') {
|
|
65
|
-
if (!parsedResult.filter) parsedResult.filter = { rel: 'and', conds: [] };
|
|
66
|
-
parsedResult.filter.rel = value as any;
|
|
67
|
-
}
|
|
53
|
+
const { parsedInfo, getResult, currentChunkId, deltaChunkEnqueuer: enqueueTextDelta, ctrl } = ctx;
|
|
54
|
+
const { value } = parsedInfo;
|
|
68
55
|
|
|
69
|
-
|
|
70
|
-
if (!parsedResult.fields) {
|
|
71
|
-
parsedResult.fields = [];
|
|
72
|
-
}
|
|
73
|
-
// @ts-ignore
|
|
74
|
-
parsedResult.fields?.push(value);
|
|
75
|
-
}
|
|
56
|
+
logger.debug('Parsed JSON value: ' + JSON.stringify(value));
|
|
76
57
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
parsedResult.formulas.push(value);
|
|
58
|
+
// TODO: 使用约定的字段判断类型
|
|
59
|
+
// Phase 1: aggregate 类型 —— name 字段触发,独立处理后直接返回
|
|
60
|
+
if (isTargetElement('$.name', parsedInfo)) {
|
|
61
|
+
handleAggregateName(ctx);
|
|
62
|
+
return;
|
|
83
63
|
}
|
|
84
64
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
parsedResult.filter = { rel: 'and', conds: [] };
|
|
89
|
-
}
|
|
90
|
-
// @ts-ignore
|
|
91
|
-
parsedResult.filter.conds.push(value);
|
|
65
|
+
// Phase 2: 报表标题 —— 初始化结果并发送 text-start
|
|
66
|
+
if (isTargetElement('$.title', parsedInfo)) {
|
|
67
|
+
handleTitle(ctx);
|
|
92
68
|
}
|
|
93
69
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
parsedResult.metrics = [];
|
|
97
|
-
}
|
|
98
|
-
// @ts-ignore
|
|
99
|
-
parsedResult.metrics.push(value);
|
|
100
|
-
}
|
|
70
|
+
// Phase 3: aggregate 结果无需后续字段处理
|
|
71
|
+
if (getResult().isAggregate) return;
|
|
101
72
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
73
|
+
// Phase 4: 按注册的 handler 分发字段处理
|
|
74
|
+
for (const { path, handler } of fieldHandlers) {
|
|
75
|
+
if (isTargetElement(path, parsedInfo)) {
|
|
76
|
+
handler(ctx, value);
|
|
77
|
+
break;
|
|
105
78
|
}
|
|
106
|
-
// @ts-ignore
|
|
107
|
-
parsedResult.xFields.push(value);
|
|
108
79
|
}
|
|
109
80
|
|
|
110
|
-
|
|
111
|
-
|
|
81
|
+
// Phase 5: 向下游推送增量结果
|
|
112
82
|
enqueueTextDelta(
|
|
113
83
|
`${JSON.stringify(value)},`,
|
|
114
|
-
{
|
|
115
|
-
|
|
116
|
-
result: validResult,
|
|
117
|
-
},
|
|
118
|
-
currentChunkId,
|
|
84
|
+
{ type: 'query-stream-parsed-info', result: JSON.stringify(getResult()) },
|
|
85
|
+
currentChunkId + 2,
|
|
119
86
|
true
|
|
120
87
|
);
|
|
121
88
|
}
|
|
89
|
+
|
|
90
|
+
// ===================== Phase Handlers =====================
|
|
91
|
+
|
|
92
|
+
function handleAggregateName(ctx: IParserCtx<IQueryResult>): void {
|
|
93
|
+
const { parsedInfo, getResult, currentChunkId, deltaChunkEnqueuer: enqueueTextDelta } = ctx;
|
|
94
|
+
const value = parsedInfo.value;
|
|
95
|
+
|
|
96
|
+
ctx.setPartialResult({
|
|
97
|
+
title: value as string,
|
|
98
|
+
isAggregate: true,
|
|
99
|
+
type: 'data_table',
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
logger.debug('Parsed aggregate table title: ' + value);
|
|
103
|
+
|
|
104
|
+
enqueueTextDelta(
|
|
105
|
+
`${JSON.stringify(value)},`,
|
|
106
|
+
{ type: 'query-stream-parsed-info', result: JSON.stringify(getResult()) },
|
|
107
|
+
currentChunkId + 1,
|
|
108
|
+
false
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function handleTitle(ctx: IParserCtx<IQueryResult>): void {
|
|
113
|
+
const { parsedInfo, currentChunkId, ctrl } = ctx;
|
|
114
|
+
|
|
115
|
+
ctx.clearResult();
|
|
116
|
+
|
|
117
|
+
ctrl.enqueue({
|
|
118
|
+
type: 'text-start',
|
|
119
|
+
id: currentChunkId + 2,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
ctx.setPartialResult({
|
|
123
|
+
title: parsedInfo.value as string,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type IQueryResult } from '@baishuyun/types';
|
|
2
|
+
import { type IParserCtx } from '../../../utils/createJsonStreamTransformer.js';
|
|
3
|
+
import type { ValueHandler } from './types.js';
|
|
4
|
+
|
|
5
|
+
// ===================== Handler Helpers =====================
|
|
6
|
+
/** 创建一个将值推入指定数组字段的 handler */
|
|
7
|
+
export function pushToArray(field: keyof IQueryResult): ValueHandler {
|
|
8
|
+
return (ctx, value) => {
|
|
9
|
+
const result = ctx.getResult();
|
|
10
|
+
if (!result[field]) {
|
|
11
|
+
ctx.setPartialResult({ [field]: [] } as Partial<IQueryResult>);
|
|
12
|
+
}
|
|
13
|
+
(ctx.getResult()[field] as any[]).push(value);
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** 创建一个将值推入数组字段、并附加 tag/form 元信息的 handler */
|
|
18
|
+
export function pushToArrayWithMeta(field: keyof IQueryResult): ValueHandler {
|
|
19
|
+
return (ctx, value) => {
|
|
20
|
+
const result = ctx.getResult();
|
|
21
|
+
if (!result[field]) {
|
|
22
|
+
ctx.setPartialResult({ [field]: [] } as Partial<IQueryResult>);
|
|
23
|
+
}
|
|
24
|
+
value.tag = `f_${Date.now()}`;
|
|
25
|
+
value.form = result.formIds ? result.formIds[0] : '';
|
|
26
|
+
(ctx.getResult()[field] as any[]).push(value);
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 确保 filter 对象已初始化 */
|
|
31
|
+
export function ensureFilter(ctx: IParserCtx<IQueryResult>): void {
|
|
32
|
+
if (!ctx.getResult().filter) {
|
|
33
|
+
ctx.setPartialResult({ filter: { rel: 'and', conds: [] } });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type IDashWidgetType, type IQueryFilter } from '@baishuyun/types';
|
|
2
|
+
import type { PathHandler } from './types.js';
|
|
3
|
+
import { ensureFilter, pushToArray, pushToArrayWithMeta } from './handler-helpers.js';
|
|
4
|
+
|
|
5
|
+
export const fieldHandlers: PathHandler[] = [
|
|
6
|
+
{
|
|
7
|
+
path: '$.userID',
|
|
8
|
+
handler: (ctx, value) => ctx.setPartialResult({ userID: value as number }),
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
path: '$.limit',
|
|
12
|
+
handler: (ctx, value) => ctx.setPartialResult({ limit: value as number }),
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
path: '$.forms',
|
|
16
|
+
handler: (ctx, value) => ctx.setPartialResult({ formIds: [value as string] }),
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
path: '$.type',
|
|
20
|
+
handler: (ctx, value) => ctx.setPartialResult({ type: value as IDashWidgetType }),
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
path: '$.filter.rel',
|
|
24
|
+
handler: (ctx, value) => {
|
|
25
|
+
ensureFilter(ctx);
|
|
26
|
+
ctx.setPartialResult({
|
|
27
|
+
filter: { ...ctx.getResult().filter, rel: value } as IQueryFilter,
|
|
28
|
+
});
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
path: '$.filter.cond.*',
|
|
33
|
+
handler: (ctx, value) => {
|
|
34
|
+
ensureFilter(ctx);
|
|
35
|
+
const filter = ctx.getResult().filter!;
|
|
36
|
+
ctx.setPartialResult({
|
|
37
|
+
filter: { ...filter, conds: [...(filter.conds || []), value] } as IQueryFilter,
|
|
38
|
+
});
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
{ path: '$.fields.*', handler: pushToArray('fields') },
|
|
42
|
+
{ path: '$.formulas.*', handler: pushToArray('formulas') },
|
|
43
|
+
{ path: '$.metrics.*', handler: pushToArrayWithMeta('metrics') },
|
|
44
|
+
{ path: '$.xFields.*', handler: pushToArrayWithMeta('xFields') },
|
|
45
|
+
{ path: '$.yFields.*', handler: pushToArrayWithMeta('yFields') },
|
|
46
|
+
];
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type IQueryResult } from '@baishuyun/types';
|
|
2
|
+
import { type IParserCtx } from '../../../utils/createJsonStreamTransformer.js';
|
|
3
|
+
|
|
4
|
+
export type ValueHandler = (ctx: IParserCtx<IQueryResult>, value: any) => void;
|
|
5
|
+
|
|
6
|
+
export interface PathHandler {
|
|
7
|
+
path: string;
|
|
8
|
+
handler: ValueHandler;
|
|
9
|
+
}
|
|
@@ -35,20 +35,50 @@ export const queryChunkGuard = (chunk: LanguageModelV2StreamPart) => {
|
|
|
35
35
|
return true;
|
|
36
36
|
};
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
38
|
+
/**
|
|
39
|
+
* 通用路径匹配器:判断当前解析到的元素是否命中指定的 JSONParser 路径。
|
|
40
|
+
*
|
|
41
|
+
* 支持两种路径形式:
|
|
42
|
+
* - 精确路径:`$.title`、`$.filter.rel` — 匹配特定 key
|
|
43
|
+
* - 通配路径:`$.fields.*`、`$.filter.cond.*` — 匹配数组元素
|
|
44
|
+
*
|
|
45
|
+
* path 格式与 `JSONParser` 的 `paths` 配置完全一致,无需额外转换。
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* isTargetElement('$.title', parsedInfo)
|
|
49
|
+
* isTargetElement('$.filter.rel', parsedInfo)
|
|
50
|
+
* isTargetElement('$.fields.*', parsedInfo)
|
|
51
|
+
* isTargetElement('$.filter.cond.*', parsedInfo)
|
|
52
|
+
*/
|
|
53
|
+
export const isTargetElement = (path: string, parsedInfo: ParsedElementInfo): boolean => {
|
|
54
|
+
const { key, stack, parent } = parsedInfo;
|
|
55
|
+
|
|
56
|
+
// 去掉 '$.' 前缀,按 '.' 分割
|
|
57
|
+
const segments = path.replace(/^\$\./, '').split('.');
|
|
58
|
+
const endsWithWildcard = segments[segments.length - 1] === '*';
|
|
59
|
+
|
|
60
|
+
if (endsWithWildcard) {
|
|
61
|
+
// ---- 通配路径:匹配数组中的元素 ----
|
|
62
|
+
const parentKeys = segments.slice(0, -1);
|
|
63
|
+
|
|
64
|
+
// 当前 key 必须为数组索引,parent 必须为数组
|
|
65
|
+
if (typeof key !== 'number' || !Array.isArray(parent)) return false;
|
|
66
|
+
|
|
67
|
+
// stack 深度 = root(1) + parentKeys
|
|
68
|
+
if (stack.length !== parentKeys.length + 1) return false;
|
|
69
|
+
|
|
70
|
+
// 从 stack[1] 开始逐段匹配(跳过 root)
|
|
71
|
+
return parentKeys.every((seg, i) => stack[i + 1]?.key === seg);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ---- 精确路径:匹配特定 key ----
|
|
75
|
+
const leafKey = segments[segments.length - 1];
|
|
76
|
+
if (key !== leafKey) return false;
|
|
77
|
+
|
|
78
|
+
const parentKeys = segments.slice(0, -1);
|
|
79
|
+
|
|
80
|
+
// stack 深度 = root(1) + parentKeys(叶子节点自身不在 stack 中)
|
|
81
|
+
if (stack.length !== parentKeys.length + 1) return false;
|
|
82
|
+
|
|
83
|
+
return parentKeys.every((seg, i) => stack[i + 1]?.key === seg);
|
|
54
84
|
};
|
|
@@ -7,13 +7,16 @@ import { logger } from '../logger/index.js';
|
|
|
7
7
|
/** onParseValue 回调接收的上下文 */
|
|
8
8
|
export interface IParserCtx<T> {
|
|
9
9
|
/** 累积的解析结果,可直接修改 */
|
|
10
|
-
|
|
10
|
+
getResult: () => T;
|
|
11
|
+
setPartialResult: (partial: Partial<T>) => void;
|
|
12
|
+
clearResult: () => void;
|
|
11
13
|
/** 当前触发 onValue 的解析信息 */
|
|
12
14
|
parsedInfo: ParsedElementInfo;
|
|
13
15
|
/** 向下游推送 text-delta chunk 的工具函数 */
|
|
14
16
|
deltaChunkEnqueuer: ReturnType<typeof createTextInfoEnqueuer>;
|
|
15
17
|
/** 当前正在处理的 chunk id */
|
|
16
18
|
currentChunkId: string;
|
|
19
|
+
ctrl: Controller;
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
/** 创建 JSON 流转换器的选项 */
|
|
@@ -140,8 +143,6 @@ class JsonStreamProcessor<T> {
|
|
|
140
143
|
const enqueue = createTextInfoEnqueuer(controller);
|
|
141
144
|
|
|
142
145
|
this.parser.onValue = (parsedInfo: ParsedElementInfo) => {
|
|
143
|
-
logger.debug('enter onValue');
|
|
144
|
-
|
|
145
146
|
// 超时错误特殊处理
|
|
146
147
|
if (isTimeoutError(parsedInfo.value)) {
|
|
147
148
|
if (!this.terminated) {
|
|
@@ -155,15 +156,19 @@ class JsonStreamProcessor<T> {
|
|
|
155
156
|
}
|
|
156
157
|
|
|
157
158
|
this.options.onParseValue({
|
|
158
|
-
parsedResult: this.result,
|
|
159
159
|
parsedInfo,
|
|
160
160
|
deltaChunkEnqueuer: enqueue,
|
|
161
161
|
currentChunkId: this.currentChunkId,
|
|
162
|
+
ctrl: controller,
|
|
163
|
+
getResult: () => this.result,
|
|
164
|
+
clearResult: this.clearResult.bind(this),
|
|
165
|
+
setPartialResult: (partial) => {
|
|
166
|
+
this.result = { ...this.result, ...partial };
|
|
167
|
+
},
|
|
162
168
|
});
|
|
163
169
|
};
|
|
164
170
|
|
|
165
171
|
this.parser.onError = (err: any) => {
|
|
166
|
-
logger.debug('enter onError: ' + err);
|
|
167
172
|
if (!this.terminated) {
|
|
168
173
|
const msg = this.options.onParseError
|
|
169
174
|
? this.options.onParseError(err)
|
|
@@ -191,6 +196,10 @@ class JsonStreamProcessor<T> {
|
|
|
191
196
|
});
|
|
192
197
|
}
|
|
193
198
|
|
|
199
|
+
private clearResult(): void {
|
|
200
|
+
this.result = {} as T;
|
|
201
|
+
}
|
|
202
|
+
|
|
194
203
|
/** 标记解析完成,重置状态 */
|
|
195
204
|
private completeParsing(): void {
|
|
196
205
|
this.resolveParseCompleted?.();
|