@shun-js/aibaiban-server 1.4.1 → 1.4.2
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/app.js +9 -9
- package/assets/sw.js +2 -2
- package/package.json +3 -2
- package/server/controller/IndexController.js +2 -2
- package/server/controller/LLMController.js +8 -2
- package/server/controller/SEOController.js +8 -8
- package/server/log-options.js +7 -7
- package/server/service/CCService.js +94 -0
- package/server/service/IndexService.js +1 -1
- package/server/service/LLMService.js +309 -135
- package/server/service/SEOService.js +10 -10
- package/server/util/check.js +2 -2
- package/server/util/feishu.js +10 -5
- package/server/util/prompt-agent.js +137 -93
- package/server/util/relay-client.js +70 -0
- package/views/index.html +51 -15
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// llm
|
|
2
|
-
const { OpenAIAPI, LibLibAPI, runAgents } = require(
|
|
3
|
-
const prompts = require(
|
|
2
|
+
const { OpenAIAPI, LibLibAPI, runAgents } = require("viho-llm");
|
|
3
|
+
const prompts = require("../util/prompt-agent.js");
|
|
4
4
|
|
|
5
5
|
// util
|
|
6
|
-
const { chatFeishuMsg, errorFeishuMsg } = require(
|
|
6
|
+
const { chatFeishuMsg, errorFeishuMsg } = require("../util/feishu.js");
|
|
7
|
+
const { sendPromptViaRelay } = require("../util/relay-client.js");
|
|
7
8
|
|
|
8
9
|
// LLM 配置
|
|
9
10
|
const llmConfig = global.QZ_CONFIG.llm;
|
|
@@ -20,9 +21,9 @@ const liblib = LibLibAPI(imageGenConfig);
|
|
|
20
21
|
* convertToolCallToSkeleton - 将单个 tool call 转为 ExcalidrawElementSkeleton
|
|
21
22
|
*/
|
|
22
23
|
function convertToolCallToSkeleton(name, args) {
|
|
23
|
-
if (name ===
|
|
24
|
+
if (name === "draw_shape") {
|
|
24
25
|
const skeleton = {
|
|
25
|
-
type: args.shape ||
|
|
26
|
+
type: args.shape || "rectangle",
|
|
26
27
|
x: args.x || 0,
|
|
27
28
|
y: args.y || 0,
|
|
28
29
|
};
|
|
@@ -43,8 +44,8 @@ function convertToolCallToSkeleton(name, args) {
|
|
|
43
44
|
return [skeleton];
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
if (name ===
|
|
47
|
-
const skeleton = { type:
|
|
47
|
+
if (name === "draw_arrow") {
|
|
48
|
+
const skeleton = { type: "arrow" };
|
|
48
49
|
// 绑定
|
|
49
50
|
if (args.startId) skeleton.start = { id: args.startId };
|
|
50
51
|
if (args.endId) skeleton.end = { id: args.endId };
|
|
@@ -57,9 +58,15 @@ function convertToolCallToSkeleton(name, args) {
|
|
|
57
58
|
if (args.label) skeleton.label = { text: args.label };
|
|
58
59
|
// 箭头样式
|
|
59
60
|
if (args.startArrowhead) skeleton.startArrowhead = args.startArrowhead;
|
|
60
|
-
if (args.endArrowhead !== undefined)
|
|
61
|
+
if (args.endArrowhead !== undefined)
|
|
62
|
+
skeleton.endArrowhead = args.endArrowhead;
|
|
61
63
|
// 如果没有绑定,用坐标计算 points
|
|
62
|
-
if (
|
|
64
|
+
if (
|
|
65
|
+
!args.startId &&
|
|
66
|
+
!args.endId &&
|
|
67
|
+
args.endX !== undefined &&
|
|
68
|
+
args.endY !== undefined
|
|
69
|
+
) {
|
|
63
70
|
skeleton.width = args.endX - skeleton.x;
|
|
64
71
|
skeleton.height = args.endY - skeleton.y;
|
|
65
72
|
}
|
|
@@ -71,13 +78,16 @@ function convertToolCallToSkeleton(name, args) {
|
|
|
71
78
|
return [skeleton];
|
|
72
79
|
}
|
|
73
80
|
|
|
74
|
-
if (name ===
|
|
81
|
+
if (name === "draw_line") {
|
|
75
82
|
if (!args.points || args.points.length < 2) return [];
|
|
76
83
|
const skeleton = {
|
|
77
|
-
type:
|
|
84
|
+
type: "line",
|
|
78
85
|
x: args.points[0][0] || 0,
|
|
79
86
|
y: args.points[0][1] || 0,
|
|
80
|
-
points: args.points.map((p) => [
|
|
87
|
+
points: args.points.map((p) => [
|
|
88
|
+
p[0] - (args.points[0][0] || 0),
|
|
89
|
+
p[1] - (args.points[0][1] || 0),
|
|
90
|
+
]),
|
|
81
91
|
};
|
|
82
92
|
if (args.strokeColor) skeleton.strokeColor = args.strokeColor;
|
|
83
93
|
if (args.strokeWidth) skeleton.strokeWidth = args.strokeWidth;
|
|
@@ -85,12 +95,12 @@ function convertToolCallToSkeleton(name, args) {
|
|
|
85
95
|
return [skeleton];
|
|
86
96
|
}
|
|
87
97
|
|
|
88
|
-
if (name ===
|
|
98
|
+
if (name === "draw_text") {
|
|
89
99
|
const skeleton = {
|
|
90
|
-
type:
|
|
100
|
+
type: "text",
|
|
91
101
|
x: args.x || 0,
|
|
92
102
|
y: args.y || 0,
|
|
93
|
-
text: args.text ||
|
|
103
|
+
text: args.text || "",
|
|
94
104
|
};
|
|
95
105
|
if (args.id) skeleton.id = args.id;
|
|
96
106
|
if (args.fontSize) skeleton.fontSize = args.fontSize;
|
|
@@ -100,9 +110,9 @@ function convertToolCallToSkeleton(name, args) {
|
|
|
100
110
|
return [skeleton];
|
|
101
111
|
}
|
|
102
112
|
|
|
103
|
-
if (name ===
|
|
113
|
+
if (name === "draw_frame") {
|
|
104
114
|
const skeleton = {
|
|
105
|
-
type:
|
|
115
|
+
type: "frame",
|
|
106
116
|
children: args.childIds || [],
|
|
107
117
|
};
|
|
108
118
|
if (args.name) skeleton.name = args.name;
|
|
@@ -122,16 +132,20 @@ function convertToolCallToSkeleton(name, args) {
|
|
|
122
132
|
function convertToolCallsToSkeletons(toolCalls) {
|
|
123
133
|
const skeletons = [];
|
|
124
134
|
for (const tc of toolCalls) {
|
|
125
|
-
if (tc.function.name ===
|
|
135
|
+
if (tc.function.name === "clear_canvas") continue;
|
|
126
136
|
try {
|
|
127
137
|
const args = JSON.parse(tc.function.arguments);
|
|
128
|
-
if (tc.function.name ===
|
|
138
|
+
if (tc.function.name === "draw_group") {
|
|
129
139
|
// draw_group: 递归转换子元素,统一加 groupIds
|
|
130
|
-
const groupId =
|
|
140
|
+
const groupId =
|
|
141
|
+
"group_" + Date.now() + "_" + Math.random().toString(36).slice(2, 8);
|
|
131
142
|
const children = args.elements || [];
|
|
132
143
|
for (const child of children) {
|
|
133
|
-
const toolName =
|
|
134
|
-
const childSkeletons = convertToolCallToSkeleton(
|
|
144
|
+
const toolName = "draw_" + child.tool;
|
|
145
|
+
const childSkeletons = convertToolCallToSkeleton(
|
|
146
|
+
toolName,
|
|
147
|
+
child.args || {},
|
|
148
|
+
);
|
|
135
149
|
for (const s of childSkeletons) {
|
|
136
150
|
s.groupIds = [groupId];
|
|
137
151
|
skeletons.push(s);
|
|
@@ -141,7 +155,7 @@ function convertToolCallsToSkeletons(toolCalls) {
|
|
|
141
155
|
const result = convertToolCallToSkeleton(tc.function.name, args);
|
|
142
156
|
skeletons.push(...result);
|
|
143
157
|
}
|
|
144
|
-
} catch
|
|
158
|
+
} catch {
|
|
145
159
|
// JSON 解析失败,跳过该 tool call
|
|
146
160
|
}
|
|
147
161
|
}
|
|
@@ -153,11 +167,11 @@ function convertToolCallsToSkeletons(toolCalls) {
|
|
|
153
167
|
* 1 次决策 LLM → reply / generate(elaborate+generate) / canvas / irrelevant
|
|
154
168
|
*/
|
|
155
169
|
exports.drawAgent = async (req, res) => {
|
|
156
|
-
const methodName =
|
|
170
|
+
const methodName = "drawAgent";
|
|
157
171
|
const messages = req.body.messages;
|
|
158
172
|
|
|
159
173
|
if (!messages?.length) {
|
|
160
|
-
const msg =
|
|
174
|
+
const msg = "need messages";
|
|
161
175
|
req.logger.error(methodName, msg);
|
|
162
176
|
res.jsonFail(msg);
|
|
163
177
|
return;
|
|
@@ -166,14 +180,15 @@ exports.drawAgent = async (req, res) => {
|
|
|
166
180
|
// 启动 SSE
|
|
167
181
|
res.streamingStart();
|
|
168
182
|
|
|
169
|
-
const lastUserMsg =
|
|
170
|
-
|
|
183
|
+
const lastUserMsg =
|
|
184
|
+
messages.filter((m) => m.role === "user").pop()?.content || "";
|
|
185
|
+
req.logger.info(methodName, "lastUserMsg", lastUserMsg);
|
|
171
186
|
chatFeishuMsg(req, `${lastUserMsg}`);
|
|
172
187
|
|
|
173
188
|
// 清洗对话历史:将前端确认文案替换为简短标记,避免 LLM 模仿自然语言回复
|
|
174
189
|
const cleanedMessages = messages.map((m) => {
|
|
175
|
-
if (m.role ===
|
|
176
|
-
return { ...m, content:
|
|
190
|
+
if (m.role === "assistant" && /^[✅❌]/.test(m.content)) {
|
|
191
|
+
return { ...m, content: "[已执行]" };
|
|
177
192
|
}
|
|
178
193
|
return m;
|
|
179
194
|
});
|
|
@@ -181,65 +196,18 @@ exports.drawAgent = async (req, res) => {
|
|
|
181
196
|
try {
|
|
182
197
|
const startTime = Date.now();
|
|
183
198
|
|
|
184
|
-
//
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
let decision = null;
|
|
188
|
-
try {
|
|
189
|
-
await runAgents([
|
|
190
|
-
{
|
|
191
|
-
agentStartCallback: () => {
|
|
192
|
-
req.logger.info(methodName, 'step: agent decision');
|
|
193
|
-
},
|
|
194
|
-
agentRequestOptions: {
|
|
195
|
-
llm,
|
|
196
|
-
modelName,
|
|
197
|
-
thinking,
|
|
198
|
-
isJson: true,
|
|
199
|
-
messages: [{ role: 'system', content: prompts.AGENT_PROMPT }, ...cleanedMessages],
|
|
200
|
-
},
|
|
201
|
-
agentEndCallback: (result) => {
|
|
202
|
-
decision = result;
|
|
203
|
-
const duration = Date.now() - startTime;
|
|
204
|
-
req.logger.info(methodName, 'decision', JSON.stringify(decision), `${duration}ms`);
|
|
205
|
-
chatFeishuMsg(req, `decision-${decision.action}`);
|
|
206
|
-
},
|
|
207
|
-
},
|
|
208
|
-
]);
|
|
209
|
-
} catch (jsonErr) {
|
|
210
|
-
// LLM 未返回 JSON,兜底为 reply
|
|
211
|
-
req.logger.warn(methodName, 'JSON parse fallback', jsonErr.message);
|
|
212
|
-
const rawText = jsonErr.message.replace('Cannot parse JSON from LLM response:', '').trim();
|
|
213
|
-
req.logger.info(methodName, 'LLM raw response', rawText);
|
|
214
|
-
|
|
215
|
-
// 尝试从原始文本中提取 JSON(LLM 可能在 JSON 前后加了多余文字)
|
|
216
|
-
const jsonMatch = rawText.match(/\{[\s\S]*\}/);
|
|
217
|
-
if (jsonMatch) {
|
|
218
|
-
try {
|
|
219
|
-
decision = JSON.parse(jsonMatch[0]);
|
|
220
|
-
req.logger.info(methodName, 'extracted JSON from raw text', JSON.stringify(decision));
|
|
221
|
-
} catch {
|
|
222
|
-
decision = { action: 'reply', message: '好的,请告诉我你想画什么图表?' };
|
|
223
|
-
}
|
|
224
|
-
} else {
|
|
225
|
-
decision = { action: 'reply', message: '好的,请告诉我你想画什么图表?' };
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// 2. 分发
|
|
230
|
-
if (decision.action === 'reply') {
|
|
231
|
-
res.streaming(`data: ${JSON.stringify({ type: 'message', content: decision.message })}\n\n`);
|
|
232
|
-
} else if (decision.action === 'irrelevant') {
|
|
233
|
-
res.streaming(`data: ${JSON.stringify({ type: 'welcome' })}\n\n`);
|
|
234
|
-
} else if (decision.action === 'generate') {
|
|
235
|
-
let elaboration = '';
|
|
199
|
+
// LLM Mermaid 管线函数
|
|
200
|
+
const runLLMMermaidPipeline = async () => {
|
|
201
|
+
let elaboration = "";
|
|
236
202
|
|
|
237
203
|
await runAgents([
|
|
238
204
|
// elaborate
|
|
239
205
|
{
|
|
240
206
|
agentStartCallback: () => {
|
|
241
|
-
res.streaming(
|
|
242
|
-
|
|
207
|
+
res.streaming(
|
|
208
|
+
`data: ${JSON.stringify({ type: "status", step: "elaborate" })}\n\n`,
|
|
209
|
+
);
|
|
210
|
+
req.logger.info(methodName, "step: elaborate");
|
|
243
211
|
},
|
|
244
212
|
agentRequestOptions: {
|
|
245
213
|
llm,
|
|
@@ -248,25 +216,31 @@ exports.drawAgent = async (req, res) => {
|
|
|
248
216
|
get messages() {
|
|
249
217
|
return [
|
|
250
218
|
{
|
|
251
|
-
role:
|
|
252
|
-
content: prompts.ELABORATE_PROMPT.replace(
|
|
253
|
-
|
|
254
|
-
decision.
|
|
255
|
-
),
|
|
219
|
+
role: "user",
|
|
220
|
+
content: prompts.ELABORATE_PROMPT.replace(
|
|
221
|
+
"{input}",
|
|
222
|
+
decision.description,
|
|
223
|
+
).replace("{diagramType}", decision.diagramType),
|
|
256
224
|
},
|
|
257
225
|
];
|
|
258
226
|
},
|
|
259
227
|
},
|
|
260
228
|
agentEndCallback: (result) => {
|
|
261
229
|
elaboration = result;
|
|
262
|
-
req.logger.info(
|
|
230
|
+
req.logger.info(
|
|
231
|
+
methodName,
|
|
232
|
+
"elaboration",
|
|
233
|
+
String(elaboration).slice(0, 100) + "...",
|
|
234
|
+
);
|
|
263
235
|
},
|
|
264
236
|
},
|
|
265
237
|
// generate
|
|
266
238
|
{
|
|
267
239
|
agentStartCallback: () => {
|
|
268
|
-
res.streaming(
|
|
269
|
-
|
|
240
|
+
res.streaming(
|
|
241
|
+
`data: ${JSON.stringify({ type: "status", step: "generate" })}\n\n`,
|
|
242
|
+
);
|
|
243
|
+
req.logger.info(methodName, "step: generate");
|
|
270
244
|
},
|
|
271
245
|
agentRequestOptions: {
|
|
272
246
|
llm,
|
|
@@ -275,11 +249,11 @@ exports.drawAgent = async (req, res) => {
|
|
|
275
249
|
get messages() {
|
|
276
250
|
return [
|
|
277
251
|
{
|
|
278
|
-
role:
|
|
279
|
-
content: prompts.GENERATE_PROMPT.replace(
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
),
|
|
252
|
+
role: "user",
|
|
253
|
+
content: prompts.GENERATE_PROMPT.replace(
|
|
254
|
+
"{diagramType}",
|
|
255
|
+
decision.diagramType,
|
|
256
|
+
).replace("{elaboration}", elaboration),
|
|
283
257
|
},
|
|
284
258
|
];
|
|
285
259
|
},
|
|
@@ -289,64 +263,251 @@ exports.drawAgent = async (req, res) => {
|
|
|
289
263
|
const duration = Date.now() - startTime;
|
|
290
264
|
req.logger.info(
|
|
291
265
|
methodName,
|
|
292
|
-
|
|
293
|
-
String(mermaidCode).slice(0, 100) +
|
|
266
|
+
"mermaidCode",
|
|
267
|
+
String(mermaidCode).slice(0, 100) + "...",
|
|
294
268
|
`total: ${duration}ms`,
|
|
295
269
|
);
|
|
296
270
|
chatFeishuMsg(req, `mermaid-${mermaidCode}`);
|
|
297
|
-
res.streaming(
|
|
271
|
+
res.streaming(
|
|
272
|
+
`data: ${JSON.stringify({ type: "mermaid", code: mermaidCode, duration })}\n\n`,
|
|
273
|
+
);
|
|
298
274
|
},
|
|
299
275
|
},
|
|
300
276
|
]);
|
|
301
|
-
}
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
// 1. Agent 决策
|
|
280
|
+
res.streaming(
|
|
281
|
+
`data: ${JSON.stringify({ type: "status", step: "thinking" })}\n\n`,
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
let decision = null;
|
|
285
|
+
try {
|
|
286
|
+
await runAgents([
|
|
287
|
+
{
|
|
288
|
+
agentStartCallback: () => {
|
|
289
|
+
req.logger.info(methodName, "step: agent decision");
|
|
290
|
+
},
|
|
291
|
+
agentRequestOptions: {
|
|
292
|
+
llm,
|
|
293
|
+
modelName,
|
|
294
|
+
thinking,
|
|
295
|
+
isJson: true,
|
|
296
|
+
messages: [
|
|
297
|
+
{ role: "system", content: prompts.AGENT_PROMPT },
|
|
298
|
+
...cleanedMessages,
|
|
299
|
+
],
|
|
300
|
+
},
|
|
301
|
+
agentEndCallback: (result) => {
|
|
302
|
+
decision = result;
|
|
303
|
+
const duration = Date.now() - startTime;
|
|
304
|
+
req.logger.info(
|
|
305
|
+
methodName,
|
|
306
|
+
"decision",
|
|
307
|
+
JSON.stringify(decision),
|
|
308
|
+
`${duration}ms`,
|
|
309
|
+
);
|
|
310
|
+
chatFeishuMsg(req, `decision-${decision.action}`);
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
]);
|
|
314
|
+
} catch (jsonErr) {
|
|
315
|
+
// LLM 未返回 JSON,兜底为 reply
|
|
316
|
+
req.logger.warn(methodName, "JSON parse fallback", jsonErr.message);
|
|
317
|
+
const rawText = jsonErr.message
|
|
318
|
+
.replace("Cannot parse JSON from LLM response:", "")
|
|
319
|
+
.trim();
|
|
320
|
+
req.logger.info(methodName, "LLM raw response", rawText);
|
|
321
|
+
|
|
322
|
+
// 尝试从原始文本中提取 JSON(LLM 可能在 JSON 前后加了多余文字)
|
|
323
|
+
const jsonMatch = rawText.match(/\{[\s\S]*\}/);
|
|
324
|
+
if (jsonMatch) {
|
|
325
|
+
try {
|
|
326
|
+
decision = JSON.parse(jsonMatch[0]);
|
|
327
|
+
req.logger.info(
|
|
328
|
+
methodName,
|
|
329
|
+
"extracted JSON from raw text",
|
|
330
|
+
JSON.stringify(decision),
|
|
331
|
+
);
|
|
332
|
+
} catch {
|
|
333
|
+
decision = {
|
|
334
|
+
action: "reply",
|
|
335
|
+
message: "好的,请告诉我你想画什么图表?",
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
} else {
|
|
339
|
+
decision = {
|
|
340
|
+
action: "reply",
|
|
341
|
+
message: "好的,请告诉我你想画什么图表?",
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// 2. 分发
|
|
347
|
+
if (decision.action === "reply") {
|
|
348
|
+
res.streaming(
|
|
349
|
+
`data: ${JSON.stringify({ type: "message", content: decision.message })}\n\n`,
|
|
350
|
+
);
|
|
351
|
+
} else if (decision.action === "irrelevant") {
|
|
352
|
+
res.streaming(`data: ${JSON.stringify({ type: "welcome" })}\n\n`);
|
|
353
|
+
} else if (decision.action === "generate") {
|
|
354
|
+
// Docker 模式优先,降级到 LLM Mermaid
|
|
355
|
+
const relayConfig = global.QZ_CONFIG.relay;
|
|
356
|
+
const useDocker = relayConfig?.wsUrl;
|
|
357
|
+
|
|
358
|
+
if (useDocker) {
|
|
359
|
+
try {
|
|
360
|
+
req.logger.info(methodName, "using Docker mode via relay");
|
|
361
|
+
res.streaming(
|
|
362
|
+
`data: ${JSON.stringify({ type: "status", step: "docker" })}\n\n`,
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
// SSE keepalive 防超时
|
|
366
|
+
const keepalive = setInterval(() => {
|
|
367
|
+
res.streaming(": keepalive\n\n");
|
|
368
|
+
}, 15000);
|
|
369
|
+
|
|
370
|
+
const result = await sendPromptViaRelay({
|
|
371
|
+
wsUrl: relayConfig.wsUrl,
|
|
372
|
+
authSecret: relayConfig.authSecret,
|
|
373
|
+
userId: req.user?.id || "guest",
|
|
374
|
+
prompt: JSON.stringify({ messages }),
|
|
375
|
+
timeout: relayConfig.timeout || 600000,
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
clearInterval(keepalive);
|
|
379
|
+
|
|
380
|
+
// 解析结果
|
|
381
|
+
if (result.startsWith("[IRRELEVANT]")) {
|
|
382
|
+
res.streaming(`data: ${JSON.stringify({ type: "welcome" })}\n\n`);
|
|
383
|
+
} else if (result.startsWith("[REPLY]")) {
|
|
384
|
+
const replyText = result.slice(7).trim();
|
|
385
|
+
res.streaming(
|
|
386
|
+
`data: ${JSON.stringify({ type: "message", content: replyText })}\n\n`,
|
|
387
|
+
);
|
|
388
|
+
} else {
|
|
389
|
+
// 尝试解析为 excalidraw JSON
|
|
390
|
+
try {
|
|
391
|
+
const data = JSON.parse(result);
|
|
392
|
+
if (data.type === "excalidraw" && data.elements) {
|
|
393
|
+
const duration = Date.now() - startTime;
|
|
394
|
+
req.logger.info(
|
|
395
|
+
methodName,
|
|
396
|
+
"excalidraw elements",
|
|
397
|
+
data.elements.length,
|
|
398
|
+
`${duration}ms`,
|
|
399
|
+
);
|
|
400
|
+
chatFeishuMsg(
|
|
401
|
+
req,
|
|
402
|
+
`docker-excalidraw-${data.elements.length}-elements`,
|
|
403
|
+
);
|
|
404
|
+
res.streaming(
|
|
405
|
+
`data: ${JSON.stringify({ type: "draw", elements: data.elements, duration })}\n\n`,
|
|
406
|
+
);
|
|
407
|
+
} else {
|
|
408
|
+
// 未知格式,当文字回复
|
|
409
|
+
res.streaming(
|
|
410
|
+
`data: ${JSON.stringify({ type: "message", content: result })}\n\n`,
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
} catch {
|
|
414
|
+
// JSON 解析失败,当文字回复
|
|
415
|
+
res.streaming(
|
|
416
|
+
`data: ${JSON.stringify({ type: "message", content: result })}\n\n`,
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
} catch (dockerErr) {
|
|
421
|
+
req.logger.error(
|
|
422
|
+
methodName,
|
|
423
|
+
"Docker mode failed, fallback to LLM",
|
|
424
|
+
dockerErr.message,
|
|
425
|
+
);
|
|
426
|
+
// 降级到 LLM Mermaid 管线
|
|
427
|
+
await runLLMMermaidPipeline();
|
|
428
|
+
}
|
|
429
|
+
} else {
|
|
430
|
+
// 无 relay 配置,直接走 LLM Mermaid
|
|
431
|
+
await runLLMMermaidPipeline();
|
|
432
|
+
}
|
|
433
|
+
} else if (decision.action === "canvas") {
|
|
302
434
|
// 3. Canvas - Function Calling 直接画图
|
|
303
|
-
res.streaming(
|
|
304
|
-
|
|
435
|
+
res.streaming(
|
|
436
|
+
`data: ${JSON.stringify({ type: "status", step: "drawing" })}\n\n`,
|
|
437
|
+
);
|
|
438
|
+
req.logger.info(methodName, "step: canvas function calling");
|
|
305
439
|
|
|
306
440
|
const canvasResponse = await llm.chat({
|
|
307
441
|
model: modelName,
|
|
308
442
|
messages: [
|
|
309
|
-
{ role:
|
|
310
|
-
{ role:
|
|
443
|
+
{ role: "system", content: prompts.CANVAS_SYSTEM_PROMPT },
|
|
444
|
+
{ role: "user", content: decision.description },
|
|
311
445
|
],
|
|
312
446
|
tools: prompts.CANVAS_TOOLS,
|
|
313
|
-
tool_choice:
|
|
447
|
+
tool_choice: "auto",
|
|
314
448
|
});
|
|
315
449
|
|
|
316
450
|
if (canvasResponse.tool_calls?.length) {
|
|
317
|
-
req.logger.info(
|
|
451
|
+
req.logger.info(
|
|
452
|
+
methodName,
|
|
453
|
+
"tool_calls",
|
|
454
|
+
canvasResponse.tool_calls.map((tc) => tc.function.name).join(", "),
|
|
455
|
+
);
|
|
318
456
|
|
|
319
457
|
// 检查是否有 clear_canvas
|
|
320
|
-
const hasClear = canvasResponse.tool_calls.some(
|
|
458
|
+
const hasClear = canvasResponse.tool_calls.some(
|
|
459
|
+
(tc) => tc.function.name === "clear_canvas",
|
|
460
|
+
);
|
|
321
461
|
if (hasClear) {
|
|
322
|
-
chatFeishuMsg(req,
|
|
323
|
-
res.streaming(`data: ${JSON.stringify({ type:
|
|
462
|
+
chatFeishuMsg(req, "canvas-clear");
|
|
463
|
+
res.streaming(`data: ${JSON.stringify({ type: "clear" })}\n\n`);
|
|
324
464
|
}
|
|
325
465
|
|
|
326
466
|
// 转换为 skeletons
|
|
327
|
-
const skeletons = convertToolCallsToSkeletons(
|
|
467
|
+
const skeletons = convertToolCallsToSkeletons(
|
|
468
|
+
canvasResponse.tool_calls,
|
|
469
|
+
);
|
|
328
470
|
if (skeletons.length > 0) {
|
|
329
471
|
const duration = Date.now() - startTime;
|
|
330
|
-
req.logger.info(
|
|
472
|
+
req.logger.info(
|
|
473
|
+
methodName,
|
|
474
|
+
"canvas elements",
|
|
475
|
+
skeletons.length,
|
|
476
|
+
`${duration}ms`,
|
|
477
|
+
);
|
|
331
478
|
chatFeishuMsg(req, `canvas-${skeletons.length}-elements`);
|
|
332
|
-
res.streaming(
|
|
479
|
+
res.streaming(
|
|
480
|
+
`data: ${JSON.stringify({ type: "draw", elements: skeletons, duration })}\n\n`,
|
|
481
|
+
);
|
|
333
482
|
}
|
|
334
483
|
} else if (canvasResponse.content) {
|
|
335
484
|
// LLM 选择文字回复而非调用函数
|
|
336
|
-
res.streaming(
|
|
485
|
+
res.streaming(
|
|
486
|
+
`data: ${JSON.stringify({ type: "message", content: canvasResponse.content })}\n\n`,
|
|
487
|
+
);
|
|
337
488
|
}
|
|
338
|
-
} else if (decision.action ===
|
|
489
|
+
} else if (decision.action === "image") {
|
|
339
490
|
// 4. Image - 调用 LibLib 图片生成 API
|
|
340
|
-
res.streaming(
|
|
341
|
-
|
|
491
|
+
res.streaming(
|
|
492
|
+
`data: ${JSON.stringify({ type: "status", step: "image" })}\n\n`,
|
|
493
|
+
);
|
|
494
|
+
req.logger.info(methodName, "step: image generation", decision.prompt);
|
|
342
495
|
chatFeishuMsg(req, `image-${decision.prompt}`);
|
|
343
496
|
|
|
344
|
-
const submitRes = await liblib.text2img(decision.prompt, {
|
|
497
|
+
const submitRes = await liblib.text2img(decision.prompt, {
|
|
498
|
+
aspectRatio: "square",
|
|
499
|
+
});
|
|
345
500
|
const generateUuid = submitRes.data?.generateUuid;
|
|
346
501
|
|
|
347
502
|
if (!generateUuid) {
|
|
348
|
-
req.logger.error(
|
|
349
|
-
|
|
503
|
+
req.logger.error(
|
|
504
|
+
methodName,
|
|
505
|
+
"liblib submit failed",
|
|
506
|
+
JSON.stringify(submitRes),
|
|
507
|
+
);
|
|
508
|
+
res.streaming(
|
|
509
|
+
`data: ${JSON.stringify({ type: "message", content: "图片生成失败,请稍后重试。" })}\n\n`,
|
|
510
|
+
);
|
|
350
511
|
} else {
|
|
351
512
|
const result = await liblib.waitForResult(generateUuid);
|
|
352
513
|
const imageUrl = result.images?.[0]?.imageUrl;
|
|
@@ -355,39 +516,52 @@ exports.drawAgent = async (req, res) => {
|
|
|
355
516
|
// 下载图片转 base64
|
|
356
517
|
const imgResponse = await fetch(imageUrl);
|
|
357
518
|
const imgBuffer = Buffer.from(await imgResponse.arrayBuffer());
|
|
358
|
-
const contentType =
|
|
359
|
-
|
|
519
|
+
const contentType =
|
|
520
|
+
imgResponse.headers.get("content-type") || "image/jpeg";
|
|
521
|
+
const dataURL = `data:${contentType};base64,${imgBuffer.toString("base64")}`;
|
|
360
522
|
|
|
361
523
|
const duration = Date.now() - startTime;
|
|
362
|
-
req.logger.info(methodName,
|
|
363
|
-
res.streaming(
|
|
524
|
+
req.logger.info(methodName, "image generated", `${duration}ms`);
|
|
525
|
+
res.streaming(
|
|
526
|
+
`data: ${JSON.stringify({ type: "image", dataURL, duration })}\n\n`,
|
|
527
|
+
);
|
|
364
528
|
} else {
|
|
365
|
-
req.logger.error(
|
|
366
|
-
|
|
529
|
+
req.logger.error(
|
|
530
|
+
methodName,
|
|
531
|
+
"liblib no image url",
|
|
532
|
+
JSON.stringify(result),
|
|
533
|
+
);
|
|
534
|
+
res.streaming(
|
|
535
|
+
`data: ${JSON.stringify({ type: "message", content: "图片生成失败,请稍后重试。" })}\n\n`,
|
|
536
|
+
);
|
|
367
537
|
}
|
|
368
538
|
}
|
|
369
|
-
} else if (decision.action ===
|
|
539
|
+
} else if (decision.action === "sketch") {
|
|
370
540
|
// 5. Sketch - 通知前端执行线稿提取(处理在前端完成)
|
|
371
541
|
const duration = Date.now() - startTime;
|
|
372
|
-
req.logger.info(methodName,
|
|
373
|
-
chatFeishuMsg(req,
|
|
374
|
-
res.streaming(
|
|
542
|
+
req.logger.info(methodName, "step: sketch", `${duration}ms`);
|
|
543
|
+
chatFeishuMsg(req, "sketch");
|
|
544
|
+
res.streaming(
|
|
545
|
+
`data: ${JSON.stringify({ type: "sketch", duration })}\n\n`,
|
|
546
|
+
);
|
|
375
547
|
} else {
|
|
376
548
|
// 未知 action fallback
|
|
377
549
|
res.streaming(
|
|
378
|
-
`data: ${JSON.stringify({ type:
|
|
550
|
+
`data: ${JSON.stringify({ type: "message", content: decision.message || "可以再说一次吗?" })}\n\n`,
|
|
379
551
|
);
|
|
380
552
|
}
|
|
381
553
|
|
|
382
554
|
res.streamingEnd();
|
|
383
555
|
} catch (error) {
|
|
384
|
-
req.logger.error(methodName,
|
|
556
|
+
req.logger.error(methodName, "error", error);
|
|
385
557
|
errorFeishuMsg(req, error.message);
|
|
386
558
|
// 对用户隐藏内部错误细节
|
|
387
|
-
const userMessage = error.message?.includes(
|
|
388
|
-
?
|
|
389
|
-
:
|
|
390
|
-
res.streaming(
|
|
559
|
+
const userMessage = error.message?.includes("Cannot parse JSON")
|
|
560
|
+
? "请求处理失败,请重新描述你的需求"
|
|
561
|
+
: "服务暂时出错,请稍后重试";
|
|
562
|
+
res.streaming(
|
|
563
|
+
`data: ${JSON.stringify({ type: "error", message: userMessage })}\n\n`,
|
|
564
|
+
);
|
|
391
565
|
res.streamingEnd();
|
|
392
566
|
}
|
|
393
567
|
};
|