@shun-js/aibaiban-server 0.4.8 → 0.4.9
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/package.json +2 -2
- package/server/controller/IndexController.js +3 -0
- package/server/service/IndexService.js +64 -29
- package/server/util/llm.js +15 -149
- package/server/util/llm1.js +208 -0
- package/server/util/prompt-intent.md +72 -182
- package/server/util/llm-intent.js +0 -82
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shun-js/aibaiban-server",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.9",
|
|
4
4
|
"description": "aibaiban.com server",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai aibaiban"
|
|
@@ -44,5 +44,5 @@
|
|
|
44
44
|
"access": "public",
|
|
45
45
|
"registry": "https://registry.npmjs.org/"
|
|
46
46
|
},
|
|
47
|
-
"gitHead": "
|
|
47
|
+
"gitHead": "822aa69649e8feaff1bed2c0e02c2b527d432568"
|
|
48
48
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// llm
|
|
2
|
-
const {
|
|
2
|
+
const { llmParseIntent } = require('../util/llm');
|
|
3
3
|
|
|
4
4
|
// util
|
|
5
|
-
const { chatFeishuMsg, errorFeishuMsg } = require('../util/feishu.js');
|
|
5
|
+
const { chatFeishuMsg, chatResFeishuMsg, errorFeishuMsg } = require('../util/feishu.js');
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* index
|
|
@@ -22,13 +22,13 @@ exports.index = async (req, res) => {
|
|
|
22
22
|
};
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
|
-
*
|
|
25
|
+
* chat
|
|
26
26
|
* @param {*} req
|
|
27
27
|
* @param {*} res
|
|
28
28
|
* @returns
|
|
29
29
|
*/
|
|
30
|
-
exports.
|
|
31
|
-
const methodName = '
|
|
30
|
+
exports.chat = async (req, res) => {
|
|
31
|
+
const methodName = 'chat';
|
|
32
32
|
|
|
33
33
|
// check
|
|
34
34
|
if (!req.body.userPrompt) {
|
|
@@ -44,28 +44,63 @@ exports.chatWithStreaming = async (req, res) => {
|
|
|
44
44
|
chatFeishuMsg(req);
|
|
45
45
|
|
|
46
46
|
// go
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
47
|
+
try {
|
|
48
|
+
const llmParseIntentRes = await llmParseIntent(userPrompt);
|
|
49
|
+
req.logger.info(methodName, 'llmParseIntentRes', llmParseIntentRes);
|
|
50
|
+
|
|
51
|
+
// r
|
|
52
|
+
chatResFeishuMsg(req, JSON.stringify(llmParseIntentRes));
|
|
53
|
+
res.jsonSuccess('success', llmParseIntentRes);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
const msg = 'chat error';
|
|
56
|
+
errorFeishuMsg(req, msg);
|
|
57
|
+
req.logger.error(methodName, msg, error);
|
|
58
|
+
res.jsonFail(msg);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* chatWithStreaming
|
|
64
|
+
* @param {*} req
|
|
65
|
+
* @param {*} res
|
|
66
|
+
* @returns
|
|
67
|
+
*/
|
|
68
|
+
exports.chatWithStreaming = async () => {
|
|
69
|
+
// const methodName = 'chatWithStreaming';
|
|
70
|
+
// // check
|
|
71
|
+
// if (!req.body.userPrompt) {
|
|
72
|
+
// const msg = 'need userPrompt';
|
|
73
|
+
// req.logger.error(methodName, msg);
|
|
74
|
+
// res.jsonFail(msg);
|
|
75
|
+
// return;
|
|
76
|
+
// }
|
|
77
|
+
// // const
|
|
78
|
+
// const userPrompt = decodeURIComponent(req.body.userPrompt);
|
|
79
|
+
// req.logger.info(methodName, 'userPrompt', userPrompt);
|
|
80
|
+
// chatFeishuMsg(req);
|
|
81
|
+
// // go
|
|
82
|
+
// const userPrompts = [{ text: userPrompt }];
|
|
83
|
+
// chatWithStreaming(userPrompts, {
|
|
84
|
+
// beginCallback: () => {
|
|
85
|
+
// req.logger.info(methodName, 'beginCallback');
|
|
86
|
+
// res.streamingStart();
|
|
87
|
+
// },
|
|
88
|
+
// firstContentCallback: () => {
|
|
89
|
+
// req.logger.info(methodName, 'firstContentCallback');
|
|
90
|
+
// // res.streaming('First chunk received!');
|
|
91
|
+
// },
|
|
92
|
+
// contentCallback: (content) => {
|
|
93
|
+
// req.logger.info(methodName, 'contentCallback', content);
|
|
94
|
+
// res.streaming(content);
|
|
95
|
+
// },
|
|
96
|
+
// endCallback: () => {
|
|
97
|
+
// req.logger.info(methodName, 'endCallback');
|
|
98
|
+
// res.streamingEnd();
|
|
99
|
+
// },
|
|
100
|
+
// errorCallback: (error) => {
|
|
101
|
+
// errorFeishuMsg(req, 'errorCallback');
|
|
102
|
+
// req.logger.info(methodName, 'errorCallback', error);
|
|
103
|
+
// res.streamingEnd();
|
|
104
|
+
// },
|
|
105
|
+
// });
|
|
71
106
|
};
|
package/server/util/llm.js
CHANGED
|
@@ -8,172 +8,38 @@ const { zodToJsonSchema } = require('zod-to-json-schema');
|
|
|
8
8
|
|
|
9
9
|
// llm
|
|
10
10
|
const { GeminiVertex } = require('viho-llm');
|
|
11
|
-
|
|
12
|
-
// gemini
|
|
13
11
|
const gemini = GeminiVertex({
|
|
14
12
|
projectId: global.QZ_CONFIG.gemini.projectId,
|
|
15
13
|
location: global.QZ_CONFIG.gemini.location,
|
|
16
14
|
modelName: global.QZ_CONFIG.gemini.modelName,
|
|
17
15
|
});
|
|
18
16
|
|
|
19
|
-
//
|
|
20
|
-
|
|
17
|
+
// const
|
|
21
18
|
let cachedSystemPrompt = null;
|
|
22
|
-
let cachedCompiledSchema = null;
|
|
23
|
-
|
|
24
|
-
// ==================== JSON Schema 定义(避免联合类型)====================
|
|
25
|
-
|
|
26
19
|
const jsonSchema = (() => {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
.describe('形状类型,默认 rectangle'),
|
|
34
|
-
color: z
|
|
35
|
-
.enum(['blue', 'green', 'purple', 'orange', 'red', 'gray', 'yellow', 'pink', 'black'])
|
|
36
|
-
.optional()
|
|
37
|
-
.describe('节点颜色,默认 blue'),
|
|
38
|
-
x: z.number().optional().describe('X 坐标,不指定则自动布局'),
|
|
39
|
-
y: z.number().optional().describe('Y 坐标,不指定则自动布局'),
|
|
40
|
-
width: z.number().optional().describe('宽度,不指定则使用默认值'),
|
|
41
|
-
height: z.number().optional().describe('高度,不指定则使用默认值'),
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
const DiagramConnectionSchema = z.object({
|
|
45
|
-
from: z.string().describe('起始节点 id'),
|
|
46
|
-
to: z.string().describe('目标节点 id'),
|
|
47
|
-
label: z.string().optional().describe('连接线上的文字标签'),
|
|
48
|
-
type: z.enum(['arrow', 'line']).optional().describe('连接类型:arrow=箭头线,line=直线,默认 arrow'),
|
|
49
|
-
style: z
|
|
50
|
-
.enum(['solid', 'dashed', 'dotted'])
|
|
51
|
-
.optional()
|
|
52
|
-
.describe('线条样式:solid=实线,dashed=虚线,dotted=点线,默认 solid'),
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
const TextAnnotationSchema = z.object({
|
|
56
|
-
id: z.string().describe('标注唯一标识符'),
|
|
57
|
-
text: z.string().describe('标注文本内容'),
|
|
58
|
-
x: z.number().optional().describe('X 坐标'),
|
|
59
|
-
y: z.number().optional().describe('Y 坐标'),
|
|
60
|
-
fontSize: z.number().optional().describe('字体大小'),
|
|
61
|
-
color: z.string().optional().describe('文字颜色'),
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
const FrameSchema = z.object({
|
|
65
|
-
id: z.string().describe('框架唯一标识符'),
|
|
66
|
-
label: z.string().optional().describe('框架标题'),
|
|
67
|
-
children: z.array(z.string()).describe('包含的节点 id 列表'),
|
|
68
|
-
color: z.string().optional().describe('框架颜色'),
|
|
69
|
-
x: z.number().optional().describe('X 坐标'),
|
|
70
|
-
y: z.number().optional().describe('Y 坐标'),
|
|
71
|
-
width: z.number().optional().describe('宽度'),
|
|
72
|
-
height: z.number().optional().describe('高度'),
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
const ImageElementSchema = z.object({
|
|
76
|
-
id: z.string().describe('图片唯一标识符'),
|
|
77
|
-
imageUrl: z.string().url().describe('图片 URL'),
|
|
78
|
-
alt: z.string().optional().describe('图片描述'),
|
|
79
|
-
x: z.number().optional().describe('X 坐标'),
|
|
80
|
-
y: z.number().optional().describe('Y 坐标'),
|
|
81
|
-
width: z.number().optional().describe('宽度'),
|
|
82
|
-
height: z.number().optional().describe('高度'),
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
// 注意:freedraws 使用简化格式,避免 tuple 导致的兼容性问题
|
|
86
|
-
const FreedrawElementSchema = z.object({
|
|
87
|
-
id: z.string().describe('手绘元素唯一标识符'),
|
|
88
|
-
points: z.array(z.number()).describe('路径点坐标数组,格式:[x1,y1,x2,y2,...]'),
|
|
89
|
-
color: z.string().optional().describe('线条颜色'),
|
|
90
|
-
strokeWidth: z.number().optional().describe('线条宽度'),
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
// 统一的响应 Schema(包含图表和错误两种情况)
|
|
94
|
-
// 通过 type 字段区分:'architecture'|'flowchart'|'sequence'|'custom' = 图表,'error' = 错误
|
|
95
|
-
return z.object({
|
|
96
|
-
// 通用字段:type 用于区分响应类型
|
|
97
|
-
type: z
|
|
98
|
-
.enum(['architecture', 'flowchart', 'sequence', 'custom', 'error'])
|
|
99
|
-
.describe('响应类型:architecture/flowchart/sequence/custom=图表类型,error=错误响应'),
|
|
100
|
-
|
|
101
|
-
// ========== 图表相关字段(当 type != error 时使用)==========
|
|
102
|
-
title: z.string().optional().describe('图表标题'),
|
|
103
|
-
nodes: z.array(DiagramNodeSchema).optional().describe('节点列表'),
|
|
104
|
-
connections: z.array(DiagramConnectionSchema).optional().describe('连接关系列表'),
|
|
105
|
-
annotations: z.array(TextAnnotationSchema).optional().describe('独立文本标注列表(可选)'),
|
|
106
|
-
frames: z.array(FrameSchema).optional().describe('分组框架列表(可选)'),
|
|
107
|
-
images: z.array(ImageElementSchema).optional().describe('图片元素列表(可选)'),
|
|
108
|
-
freedraws: z.array(FreedrawElementSchema).optional().describe('手绘元素列表(可选)'),
|
|
109
|
-
|
|
110
|
-
// ========== 错误相关字段(当 type = error 时使用)==========
|
|
111
|
-
error: z
|
|
112
|
-
.enum(['NON_DRAWING_REQUEST', 'INVALID_REQUEST', 'UNKNOWN_ERROR'])
|
|
113
|
-
.optional()
|
|
114
|
-
.describe('错误类型(仅当 type=error 时需要)'),
|
|
115
|
-
message: z.string().optional().describe('错误提示消息(仅当 type=error 时需要)'),
|
|
116
|
-
});
|
|
20
|
+
return zodToJsonSchema(
|
|
21
|
+
z.object({
|
|
22
|
+
intent: z.enum(['DRAW', 'REJECT']).describe('用户意图:DRAW(绘图)或 REJECT(非绘图)'),
|
|
23
|
+
reason: z.string().describe('判断理由(一句话说明)'),
|
|
24
|
+
}),
|
|
25
|
+
);
|
|
117
26
|
})();
|
|
118
27
|
|
|
119
|
-
// ==================== 懒加载函数 ====================
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* 获取系统提示词(懒加载+缓存)
|
|
123
|
-
*/
|
|
124
|
-
async function getSystemPrompt() {
|
|
125
|
-
if (!cachedSystemPrompt) {
|
|
126
|
-
const promptPath = path.resolve(__dirname, './prompt.md');
|
|
127
|
-
cachedSystemPrompt = await readFile(promptPath);
|
|
128
|
-
}
|
|
129
|
-
return cachedSystemPrompt;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* 获取编译后的 JSON Schema(懒加载+缓存)
|
|
134
|
-
*/
|
|
135
|
-
function getCompiledSchema() {
|
|
136
|
-
if (!cachedCompiledSchema) {
|
|
137
|
-
const fullSchema = zodToJsonSchema(jsonSchema, {
|
|
138
|
-
name: 'AIResponse',
|
|
139
|
-
$refStrategy: 'none',
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
// 关键修复:Gemini 不支持顶层 $ref,直接使用 definitions 中的定义
|
|
143
|
-
if (fullSchema.definitions && fullSchema.definitions.AIResponse) {
|
|
144
|
-
cachedCompiledSchema = fullSchema.definitions.AIResponse;
|
|
145
|
-
} else if (fullSchema.$ref) {
|
|
146
|
-
// 如果顶层是 $ref,解析它
|
|
147
|
-
const refName = fullSchema.$ref.split('/').pop();
|
|
148
|
-
cachedCompiledSchema = fullSchema.definitions[refName];
|
|
149
|
-
} else {
|
|
150
|
-
cachedCompiledSchema = fullSchema;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
return cachedCompiledSchema;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// ==================== 主要功能 ====================
|
|
157
|
-
|
|
158
28
|
/**
|
|
159
|
-
*
|
|
29
|
+
* llmParseIntent
|
|
160
30
|
* @param {*} userPrompts
|
|
161
|
-
* @param {*} callbackOptions
|
|
162
31
|
*/
|
|
163
|
-
exports.
|
|
164
|
-
//
|
|
165
|
-
|
|
166
|
-
const compiledSchema = getCompiledSchema();
|
|
167
|
-
console.log('compiledSchema');
|
|
168
|
-
console.log(JSON.stringify(compiledSchema));
|
|
32
|
+
exports.llmParseIntent = async (userPrompts) => {
|
|
33
|
+
// cache
|
|
34
|
+
if (!cachedSystemPrompt) cachedSystemPrompt = await readFile(path.resolve(__dirname, './prompt-intent.md'));
|
|
169
35
|
|
|
170
36
|
// chat options
|
|
171
37
|
const chatOptions = {
|
|
172
38
|
contents: userPrompts,
|
|
173
|
-
systemInstruction:
|
|
39
|
+
systemInstruction: cachedSystemPrompt,
|
|
174
40
|
config: {
|
|
175
41
|
responseMimeType: 'application/json',
|
|
176
|
-
responseJsonSchema:
|
|
42
|
+
responseJsonSchema: jsonSchema,
|
|
177
43
|
temperature: 0.2,
|
|
178
44
|
topP: 0.9,
|
|
179
45
|
topK: 40,
|
|
@@ -203,6 +69,6 @@ exports.chatWithStreaming = async (userPrompts, callbackOptions) => {
|
|
|
203
69
|
},
|
|
204
70
|
};
|
|
205
71
|
|
|
206
|
-
//
|
|
207
|
-
await gemini.
|
|
72
|
+
// go
|
|
73
|
+
return await gemini.chat(chatOptions);
|
|
208
74
|
};
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
// path
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { readFile } = require('qiao-file');
|
|
4
|
+
|
|
5
|
+
// z
|
|
6
|
+
const { z } = require('zod');
|
|
7
|
+
const { zodToJsonSchema } = require('zod-to-json-schema');
|
|
8
|
+
|
|
9
|
+
// llm
|
|
10
|
+
const { GeminiVertex } = require('viho-llm');
|
|
11
|
+
|
|
12
|
+
// gemini
|
|
13
|
+
const gemini = GeminiVertex({
|
|
14
|
+
projectId: global.QZ_CONFIG.gemini.projectId,
|
|
15
|
+
location: global.QZ_CONFIG.gemini.location,
|
|
16
|
+
modelName: global.QZ_CONFIG.gemini.modelName,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// ==================== 缓存(懒加载) ====================
|
|
20
|
+
|
|
21
|
+
let cachedSystemPrompt = null;
|
|
22
|
+
let cachedCompiledSchema = null;
|
|
23
|
+
|
|
24
|
+
// ==================== JSON Schema 定义(避免联合类型)====================
|
|
25
|
+
|
|
26
|
+
const jsonSchema = (() => {
|
|
27
|
+
const DiagramNodeSchema = z.object({
|
|
28
|
+
id: z.string().describe('节点唯一标识符,使用驼峰命名法,如 userService, mysqlDB'),
|
|
29
|
+
label: z.string().describe('节点显示文本,支持 \\n 换行'),
|
|
30
|
+
type: z
|
|
31
|
+
.enum(['rectangle', 'ellipse', 'diamond', 'hexagon', 'cylinder', 'cloud'])
|
|
32
|
+
.optional()
|
|
33
|
+
.describe('形状类型,默认 rectangle'),
|
|
34
|
+
color: z
|
|
35
|
+
.enum(['blue', 'green', 'purple', 'orange', 'red', 'gray', 'yellow', 'pink', 'black'])
|
|
36
|
+
.optional()
|
|
37
|
+
.describe('节点颜色,默认 blue'),
|
|
38
|
+
x: z.number().optional().describe('X 坐标,不指定则自动布局'),
|
|
39
|
+
y: z.number().optional().describe('Y 坐标,不指定则自动布局'),
|
|
40
|
+
width: z.number().optional().describe('宽度,不指定则使用默认值'),
|
|
41
|
+
height: z.number().optional().describe('高度,不指定则使用默认值'),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const DiagramConnectionSchema = z.object({
|
|
45
|
+
from: z.string().describe('起始节点 id'),
|
|
46
|
+
to: z.string().describe('目标节点 id'),
|
|
47
|
+
label: z.string().optional().describe('连接线上的文字标签'),
|
|
48
|
+
type: z.enum(['arrow', 'line']).optional().describe('连接类型:arrow=箭头线,line=直线,默认 arrow'),
|
|
49
|
+
style: z
|
|
50
|
+
.enum(['solid', 'dashed', 'dotted'])
|
|
51
|
+
.optional()
|
|
52
|
+
.describe('线条样式:solid=实线,dashed=虚线,dotted=点线,默认 solid'),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const TextAnnotationSchema = z.object({
|
|
56
|
+
id: z.string().describe('标注唯一标识符'),
|
|
57
|
+
text: z.string().describe('标注文本内容'),
|
|
58
|
+
x: z.number().optional().describe('X 坐标'),
|
|
59
|
+
y: z.number().optional().describe('Y 坐标'),
|
|
60
|
+
fontSize: z.number().optional().describe('字体大小'),
|
|
61
|
+
color: z.string().optional().describe('文字颜色'),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const FrameSchema = z.object({
|
|
65
|
+
id: z.string().describe('框架唯一标识符'),
|
|
66
|
+
label: z.string().optional().describe('框架标题'),
|
|
67
|
+
children: z.array(z.string()).describe('包含的节点 id 列表'),
|
|
68
|
+
color: z.string().optional().describe('框架颜色'),
|
|
69
|
+
x: z.number().optional().describe('X 坐标'),
|
|
70
|
+
y: z.number().optional().describe('Y 坐标'),
|
|
71
|
+
width: z.number().optional().describe('宽度'),
|
|
72
|
+
height: z.number().optional().describe('高度'),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const ImageElementSchema = z.object({
|
|
76
|
+
id: z.string().describe('图片唯一标识符'),
|
|
77
|
+
imageUrl: z.string().url().describe('图片 URL'),
|
|
78
|
+
alt: z.string().optional().describe('图片描述'),
|
|
79
|
+
x: z.number().optional().describe('X 坐标'),
|
|
80
|
+
y: z.number().optional().describe('Y 坐标'),
|
|
81
|
+
width: z.number().optional().describe('宽度'),
|
|
82
|
+
height: z.number().optional().describe('高度'),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// 注意:freedraws 使用简化格式,避免 tuple 导致的兼容性问题
|
|
86
|
+
const FreedrawElementSchema = z.object({
|
|
87
|
+
id: z.string().describe('手绘元素唯一标识符'),
|
|
88
|
+
points: z.array(z.number()).describe('路径点坐标数组,格式:[x1,y1,x2,y2,...]'),
|
|
89
|
+
color: z.string().optional().describe('线条颜色'),
|
|
90
|
+
strokeWidth: z.number().optional().describe('线条宽度'),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// 统一的响应 Schema(包含图表和错误两种情况)
|
|
94
|
+
// 通过 type 字段区分:'architecture'|'flowchart'|'sequence'|'custom' = 图表,'error' = 错误
|
|
95
|
+
return z.object({
|
|
96
|
+
// 通用字段:type 用于区分响应类型
|
|
97
|
+
type: z
|
|
98
|
+
.enum(['architecture', 'flowchart', 'sequence', 'custom', 'error'])
|
|
99
|
+
.describe('响应类型:architecture/flowchart/sequence/custom=图表类型,error=错误响应'),
|
|
100
|
+
|
|
101
|
+
// ========== 图表相关字段(当 type != error 时使用)==========
|
|
102
|
+
title: z.string().optional().describe('图表标题'),
|
|
103
|
+
nodes: z.array(DiagramNodeSchema).optional().describe('节点列表'),
|
|
104
|
+
connections: z.array(DiagramConnectionSchema).optional().describe('连接关系列表'),
|
|
105
|
+
annotations: z.array(TextAnnotationSchema).optional().describe('独立文本标注列表(可选)'),
|
|
106
|
+
frames: z.array(FrameSchema).optional().describe('分组框架列表(可选)'),
|
|
107
|
+
images: z.array(ImageElementSchema).optional().describe('图片元素列表(可选)'),
|
|
108
|
+
freedraws: z.array(FreedrawElementSchema).optional().describe('手绘元素列表(可选)'),
|
|
109
|
+
|
|
110
|
+
// ========== 错误相关字段(当 type = error 时使用)==========
|
|
111
|
+
error: z
|
|
112
|
+
.enum(['NON_DRAWING_REQUEST', 'INVALID_REQUEST', 'UNKNOWN_ERROR'])
|
|
113
|
+
.optional()
|
|
114
|
+
.describe('错误类型(仅当 type=error 时需要)'),
|
|
115
|
+
message: z.string().optional().describe('错误提示消息(仅当 type=error 时需要)'),
|
|
116
|
+
});
|
|
117
|
+
})();
|
|
118
|
+
|
|
119
|
+
// ==================== 懒加载函数 ====================
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 获取系统提示词(懒加载+缓存)
|
|
123
|
+
*/
|
|
124
|
+
async function getSystemPrompt() {
|
|
125
|
+
if (!cachedSystemPrompt) {
|
|
126
|
+
const promptPath = path.resolve(__dirname, './prompt.md');
|
|
127
|
+
cachedSystemPrompt = await readFile(promptPath);
|
|
128
|
+
}
|
|
129
|
+
return cachedSystemPrompt;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* 获取编译后的 JSON Schema(懒加载+缓存)
|
|
134
|
+
*/
|
|
135
|
+
function getCompiledSchema() {
|
|
136
|
+
if (!cachedCompiledSchema) {
|
|
137
|
+
const fullSchema = zodToJsonSchema(jsonSchema, {
|
|
138
|
+
name: 'AIResponse',
|
|
139
|
+
$refStrategy: 'none',
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// 关键修复:Gemini 不支持顶层 $ref,直接使用 definitions 中的定义
|
|
143
|
+
if (fullSchema.definitions && fullSchema.definitions.AIResponse) {
|
|
144
|
+
cachedCompiledSchema = fullSchema.definitions.AIResponse;
|
|
145
|
+
} else if (fullSchema.$ref) {
|
|
146
|
+
// 如果顶层是 $ref,解析它
|
|
147
|
+
const refName = fullSchema.$ref.split('/').pop();
|
|
148
|
+
cachedCompiledSchema = fullSchema.definitions[refName];
|
|
149
|
+
} else {
|
|
150
|
+
cachedCompiledSchema = fullSchema;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return cachedCompiledSchema;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ==================== 主要功能 ====================
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* chatWithStreaming
|
|
160
|
+
* @param {*} userPrompts
|
|
161
|
+
* @param {*} callbackOptions
|
|
162
|
+
*/
|
|
163
|
+
exports.chatWithStreaming = async (userPrompts, callbackOptions) => {
|
|
164
|
+
// 懒加载:第一次使用时加载并缓存
|
|
165
|
+
const systemPrompt = await getSystemPrompt();
|
|
166
|
+
const compiledSchema = getCompiledSchema();
|
|
167
|
+
console.log('compiledSchema');
|
|
168
|
+
console.log(JSON.stringify(compiledSchema));
|
|
169
|
+
|
|
170
|
+
// chat options
|
|
171
|
+
const chatOptions = {
|
|
172
|
+
contents: userPrompts,
|
|
173
|
+
systemInstruction: systemPrompt,
|
|
174
|
+
config: {
|
|
175
|
+
responseMimeType: 'application/json',
|
|
176
|
+
responseJsonSchema: compiledSchema,
|
|
177
|
+
temperature: 0.2,
|
|
178
|
+
topP: 0.9,
|
|
179
|
+
topK: 40,
|
|
180
|
+
maxOutputTokens: 8192,
|
|
181
|
+
thinkingConfig: {
|
|
182
|
+
thinkingBudget: 0,
|
|
183
|
+
includeThoughts: false,
|
|
184
|
+
},
|
|
185
|
+
safetySettings: [
|
|
186
|
+
{
|
|
187
|
+
category: 'HARM_CATEGORY_HATE_SPEECH',
|
|
188
|
+
threshold: 'BLOCK_ONLY_HIGH',
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
category: 'HARM_CATEGORY_DANGEROUS_CONTENT',
|
|
192
|
+
threshold: 'BLOCK_ONLY_HIGH',
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT',
|
|
196
|
+
threshold: 'BLOCK_ONLY_HIGH',
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
category: 'HARM_CATEGORY_HARASSMENT',
|
|
200
|
+
threshold: 'BLOCK_ONLY_HIGH',
|
|
201
|
+
},
|
|
202
|
+
],
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// Send a chat message with streaming
|
|
207
|
+
await gemini.chatWithStreaming(chatOptions, callbackOptions);
|
|
208
|
+
};
|
|
@@ -1,217 +1,107 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 意图识别:判断用户是否要绘制图表
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
分析用户输入,判断是否为绘图请求。
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## 输出要求
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
2. **流式输出设计说明**:如果是绘图请求,实时输出设计思路
|
|
9
|
-
3. **友好拒绝**:如果不是绘图请求,礼貌说明能力范围
|
|
10
|
-
|
|
11
|
-
## 响应格式要求
|
|
12
|
-
|
|
13
|
-
你的回复必须以特殊标记开始,用于后续处理:
|
|
14
|
-
|
|
15
|
-
### 非绘图请求格式
|
|
16
|
-
|
|
17
|
-
```
|
|
18
|
-
[INTENT:REJECT]
|
|
19
|
-
|
|
20
|
-
抱歉,我是 AI 白板的绘图助手,我只能帮你绘制图表、流程图、架构图等。
|
|
21
|
-
|
|
22
|
-
我可以帮你:
|
|
23
|
-
• 绘制流程图和架构图
|
|
24
|
-
• 设计系统结构
|
|
25
|
-
• 创建各种图形和连接
|
|
26
|
-
|
|
27
|
-
如果你想画图,可以试试这样说:
|
|
28
|
-
「画一个登录流程图」
|
|
29
|
-
「设计一个微服务架构」
|
|
30
|
-
「画三个矩形连接起来」
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
### 绘图请求格式
|
|
7
|
+
以 JSON 格式输出:
|
|
34
8
|
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"intent": "DRAW" 或 "REJECT",
|
|
12
|
+
"reason": "判断理由(一句话说明)"
|
|
13
|
+
}
|
|
35
14
|
```
|
|
36
|
-
[INTENT:DRAW]
|
|
37
|
-
|
|
38
|
-
好的,我来帮你设计[图表类型]!
|
|
39
|
-
|
|
40
|
-
设计思路:
|
|
41
|
-
[流式输出你的设计思路,包括:]
|
|
42
|
-
1. 会使用哪些元素(矩形、圆形、箭头等)
|
|
43
|
-
2. 元素之间的关系和布局
|
|
44
|
-
3. 颜色和样式的选择理由
|
|
45
|
-
4. 整体的视觉效果说明
|
|
46
|
-
|
|
47
|
-
正在生成图表数据...
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
## 判断规则
|
|
51
|
-
|
|
52
|
-
### 属于绘图请求的情况:
|
|
53
|
-
|
|
54
|
-
- 明确要求绘制图表:「画一个流程图」「设计架构图」
|
|
55
|
-
- 描述需要可视化的内容:「用户登录的流程」「微服务架构」
|
|
56
|
-
- 要求创建图形:「画三个矩形」「连接这些模块」
|
|
57
|
-
- 使用图表相关词汇:流程图、架构图、组织架构、思维导图、ER图、时序图等
|
|
58
|
-
|
|
59
|
-
### 不属于绘图请求的情况:
|
|
60
|
-
|
|
61
|
-
- 闲聊和问候:「你好」「今天天气怎么样」
|
|
62
|
-
- 知识问答:「什么是微服务」「React 和 Vue 的区别」
|
|
63
|
-
- 代码相关:「帮我写一段代码」「这段代码有什么问题」
|
|
64
|
-
- 其他任务:「帮我写文章」「翻译这段话」「计算数学题」
|
|
65
|
-
|
|
66
|
-
## 设计说明输出指南
|
|
67
15
|
|
|
68
|
-
|
|
16
|
+
## 判断标准
|
|
69
17
|
|
|
70
|
-
|
|
71
|
-
2. **设计分析**:
|
|
72
|
-
- 识别出的关键元素和实体
|
|
73
|
-
- 元素之间的关系(顺序、包含、依赖等)
|
|
74
|
-
- 推荐的图表类型(流程图、架构图、关系图等)
|
|
18
|
+
### DRAW(绘图请求)
|
|
75
19
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
20
|
+
- 包含动作词:画、绘制、设计、生成、创建、制作
|
|
21
|
+
- 包含图表词:图、流程图、架构图、思维导图、关系图、结构图、示意图
|
|
22
|
+
- 描述可视化需求:「用户登录流程」「微服务架构」「数据库关系」
|
|
23
|
+
- 描述图形元素:「三个矩形」「圆形连接」
|
|
80
24
|
|
|
81
|
-
|
|
82
|
-
- 颜色方案的选择理由
|
|
83
|
-
- 形状类型的使用场景
|
|
84
|
-
- 箭头和连接线的样式
|
|
25
|
+
### REJECT(非绘图请求)
|
|
85
26
|
|
|
86
|
-
|
|
27
|
+
- 问候寒暄:你好、早上好、谢谢
|
|
28
|
+
- 知识问答:什么是、如何、为什么、介绍、解释
|
|
29
|
+
- 代码任务:写代码、debug、实现功能
|
|
30
|
+
- 文字任务:翻译、写文章、总结
|
|
31
|
+
- 其他任务:计算、推荐、建议
|
|
87
32
|
|
|
88
33
|
## 示例
|
|
89
34
|
|
|
90
|
-
|
|
35
|
+
输入:画一个用户登录流程图
|
|
36
|
+
输出:
|
|
91
37
|
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"intent": "DRAW",
|
|
41
|
+
"reason": "包含动作词'画'和图表词'流程图'"
|
|
42
|
+
}
|
|
92
43
|
```
|
|
93
|
-
用户:你好,今天天气怎么样?
|
|
94
44
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
抱歉,我是 AI 白板的绘图助手,我只能帮你绘制图表、流程图、架构图等。
|
|
99
|
-
|
|
100
|
-
我可以帮你:
|
|
101
|
-
• 绘制流程图和架构图
|
|
102
|
-
• 设计系统结构
|
|
103
|
-
• 创建各种图形和连接
|
|
104
|
-
|
|
105
|
-
如果你想画图,可以试试这样说:
|
|
106
|
-
「画一个登录流程图」
|
|
107
|
-
「设计一个微服务架构」
|
|
108
|
-
「画三个矩形连接起来」
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
### 示例 2:简单绘图请求
|
|
45
|
+
输入:设计微服务架构
|
|
46
|
+
输出:
|
|
112
47
|
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"intent": "DRAW",
|
|
51
|
+
"reason": "包含动作词'设计'和可视化需求'架构'"
|
|
52
|
+
}
|
|
113
53
|
```
|
|
114
|
-
用户:画三个矩形连接起来
|
|
115
54
|
|
|
116
|
-
|
|
117
|
-
|
|
55
|
+
输入:三个矩形连接起来
|
|
56
|
+
输出:
|
|
118
57
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
3. 使用箭头连接,表示从左到右的流动关系
|
|
125
|
-
4. 矩形大小统一,简洁清晰
|
|
126
|
-
|
|
127
|
-
正在生成图表数据...
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"intent": "DRAW",
|
|
61
|
+
"reason": "描述了具体的图形元素和连接关系"
|
|
62
|
+
}
|
|
128
63
|
```
|
|
129
64
|
|
|
130
|
-
|
|
65
|
+
输入:简单介绍一下上海市
|
|
66
|
+
输出:
|
|
131
67
|
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"intent": "REJECT",
|
|
71
|
+
"reason": "这是知识问答请求,不是绘图需求"
|
|
72
|
+
}
|
|
132
73
|
```
|
|
133
|
-
用户:设计一个用户登录的流程图
|
|
134
|
-
|
|
135
|
-
助手:
|
|
136
|
-
[INTENT:DRAW]
|
|
137
|
-
|
|
138
|
-
好的,我来帮你设计用户登录流程图!
|
|
139
|
-
|
|
140
|
-
设计思路:
|
|
141
74
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
- 输入用户名密码(矩形)
|
|
145
|
-
- 验证凭证(菱形决策节点)
|
|
146
|
-
- 成功/失败处理(矩形)
|
|
147
|
-
- 结束节点(椭圆形)
|
|
75
|
+
输入:什么是微服务架构?
|
|
76
|
+
输出:
|
|
148
77
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
- 采用纵向布局,从上到下展示流程
|
|
155
|
-
- 决策节点居中,两侧分出成功/失败分支
|
|
156
|
-
- 失败分支使用红色标注,成功路径使用绿色
|
|
157
|
-
|
|
158
|
-
4. **视觉效果**:
|
|
159
|
-
- 起止节点:绿色椭圆,突出流程边界
|
|
160
|
-
- 处理节点:蓝色矩形,表示操作步骤
|
|
161
|
-
- 决策节点:橙色菱形,强调判断点
|
|
162
|
-
- 错误提示:红色矩形,警示异常情况
|
|
163
|
-
|
|
164
|
-
正在生成图表数据...
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"intent": "REJECT",
|
|
81
|
+
"reason": "这是知识问答,用户在询问概念而非要求绘图"
|
|
82
|
+
}
|
|
165
83
|
```
|
|
166
84
|
|
|
167
|
-
|
|
85
|
+
输入:你好,今天天气怎么样
|
|
86
|
+
输出:
|
|
168
87
|
|
|
88
|
+
```json
|
|
89
|
+
{
|
|
90
|
+
"intent": "REJECT",
|
|
91
|
+
"reason": "这是闲聊问候,与绘图无关"
|
|
92
|
+
}
|
|
169
93
|
```
|
|
170
|
-
用户:画一个微服务架构图
|
|
171
|
-
|
|
172
|
-
助手:
|
|
173
|
-
[INTENT:DRAW]
|
|
174
|
-
|
|
175
|
-
好的,我来帮你设计微服务架构图!
|
|
176
|
-
|
|
177
|
-
设计思路:
|
|
178
|
-
|
|
179
|
-
1. **架构层次**:
|
|
180
|
-
- 前端层:客户端和 Web 应用
|
|
181
|
-
- 网关层:API Gateway
|
|
182
|
-
- 服务层:多个微服务(用户服务、订单服务、支付服务等)
|
|
183
|
-
- 数据层:各服务独立数据库
|
|
184
|
-
|
|
185
|
-
2. **元素选择**:
|
|
186
|
-
- 服务节点:使用圆角矩形(cylinder 样式)表示微服务
|
|
187
|
-
- 数据库:使用圆柱体(cylinder)图标
|
|
188
|
-
- 网关:使用六边形突出其特殊地位
|
|
189
|
-
- 客户端:使用矩形
|
|
190
|
-
|
|
191
|
-
3. **连接关系**:
|
|
192
|
-
- 客户端到网关:使用粗箭头表示请求流向
|
|
193
|
-
- 网关到服务:使用细箭头表示分发
|
|
194
|
-
- 服务到数据库:使用虚线表示数据访问
|
|
195
|
-
- 服务间调用:使用双向箭头
|
|
196
|
-
|
|
197
|
-
4. **颜色方案**:
|
|
198
|
-
- 前端层:蓝色系,表示用户界面
|
|
199
|
-
- 网关层:紫色,突出中间层地位
|
|
200
|
-
- 服务层:绿色系,表示业务逻辑
|
|
201
|
-
- 数据层:灰色,表示存储
|
|
202
94
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
- 同层服务水平排列,体现并行关系
|
|
206
|
-
- 使用 Frame 框将同层元素分组
|
|
95
|
+
输入:帮我写一段 Python 代码
|
|
96
|
+
输出:
|
|
207
97
|
|
|
208
|
-
|
|
98
|
+
```json
|
|
99
|
+
{
|
|
100
|
+
"intent": "REJECT",
|
|
101
|
+
"reason": "这是代码编写任务,不是绘图请求"
|
|
102
|
+
}
|
|
209
103
|
```
|
|
210
104
|
|
|
211
|
-
##
|
|
105
|
+
## 用户输入
|
|
212
106
|
|
|
213
|
-
|
|
214
|
-
2. **流式友好**:设计说明要分段输出,适合流式显示
|
|
215
|
-
3. **简洁清晰**:避免过度冗长,重点突出设计思路
|
|
216
|
-
4. **专业但友好**:使用专业术语但保持易懂
|
|
217
|
-
5. **匹配用户需求**:设计说明要紧密对应用户的具体请求
|
|
107
|
+
{USER_INPUT}
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
// path
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const { readFile } = require('qiao-file');
|
|
4
|
-
|
|
5
|
-
// z
|
|
6
|
-
// const { z } = require('zod');
|
|
7
|
-
// const { zodToJsonSchema } = require('zod-to-json-schema');
|
|
8
|
-
|
|
9
|
-
// llm
|
|
10
|
-
const { GeminiVertex } = require('viho-llm');
|
|
11
|
-
|
|
12
|
-
// gemini
|
|
13
|
-
const gemini = GeminiVertex({
|
|
14
|
-
projectId: global.QZ_CONFIG.gemini.projectId,
|
|
15
|
-
location: global.QZ_CONFIG.gemini.location,
|
|
16
|
-
modelName: global.QZ_CONFIG.gemini.modelName,
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
// ==================== 缓存(懒加载) ====================
|
|
20
|
-
|
|
21
|
-
let cachedSystemPrompt = null;
|
|
22
|
-
|
|
23
|
-
// ==================== 懒加载函数 ====================
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* 获取系统提示词(懒加载+缓存)
|
|
27
|
-
*/
|
|
28
|
-
async function getSystemPrompt(promptPath) {
|
|
29
|
-
if (!cachedSystemPrompt) cachedSystemPrompt = await readFile(path.resolve(__dirname, promptPath));
|
|
30
|
-
return cachedSystemPrompt;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// ==================== 主要功能 ====================
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* chatWithStreaming
|
|
37
|
-
* @param {*} userPrompts
|
|
38
|
-
* @param {*} callbackOptions
|
|
39
|
-
*/
|
|
40
|
-
exports.chatWithStreaming = async (userPrompts, callbackOptions) => {
|
|
41
|
-
// 懒加载:第一次使用时加载并缓存
|
|
42
|
-
const systemPrompt = await getSystemPrompt('./prompt-intent.md');
|
|
43
|
-
|
|
44
|
-
// chat options
|
|
45
|
-
const chatOptions = {
|
|
46
|
-
contents: userPrompts,
|
|
47
|
-
systemInstruction: systemPrompt,
|
|
48
|
-
config: {
|
|
49
|
-
// responseMimeType: 'application/json',
|
|
50
|
-
// responseJsonSchema: compiledSchema,
|
|
51
|
-
temperature: 0.2,
|
|
52
|
-
topP: 0.9,
|
|
53
|
-
topK: 40,
|
|
54
|
-
maxOutputTokens: 8192,
|
|
55
|
-
thinkingConfig: {
|
|
56
|
-
thinkingBudget: 0,
|
|
57
|
-
includeThoughts: false,
|
|
58
|
-
},
|
|
59
|
-
safetySettings: [
|
|
60
|
-
{
|
|
61
|
-
category: 'HARM_CATEGORY_HATE_SPEECH',
|
|
62
|
-
threshold: 'BLOCK_ONLY_HIGH',
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
category: 'HARM_CATEGORY_DANGEROUS_CONTENT',
|
|
66
|
-
threshold: 'BLOCK_ONLY_HIGH',
|
|
67
|
-
},
|
|
68
|
-
{
|
|
69
|
-
category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT',
|
|
70
|
-
threshold: 'BLOCK_ONLY_HIGH',
|
|
71
|
-
},
|
|
72
|
-
{
|
|
73
|
-
category: 'HARM_CATEGORY_HARASSMENT',
|
|
74
|
-
threshold: 'BLOCK_ONLY_HIGH',
|
|
75
|
-
},
|
|
76
|
-
],
|
|
77
|
-
},
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
// Send a chat message with streaming
|
|
81
|
-
await gemini.chatWithStreaming(chatOptions, callbackOptions);
|
|
82
|
-
};
|