@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.
@@ -1,9 +1,10 @@
1
1
  // llm
2
- const { OpenAIAPI, LibLibAPI, runAgents } = require('viho-llm');
3
- const prompts = require('../util/prompt-agent.js');
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('../util/feishu.js');
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 === 'draw_shape') {
24
+ if (name === "draw_shape") {
24
25
  const skeleton = {
25
- type: args.shape || 'rectangle',
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 === 'draw_arrow') {
47
- const skeleton = { type: 'arrow' };
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) skeleton.endArrowhead = args.endArrowhead;
61
+ if (args.endArrowhead !== undefined)
62
+ skeleton.endArrowhead = args.endArrowhead;
61
63
  // 如果没有绑定,用坐标计算 points
62
- if (!args.startId && !args.endId && args.endX !== undefined && args.endY !== undefined) {
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 === 'draw_line') {
81
+ if (name === "draw_line") {
75
82
  if (!args.points || args.points.length < 2) return [];
76
83
  const skeleton = {
77
- type: 'line',
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) => [p[0] - (args.points[0][0] || 0), p[1] - (args.points[0][1] || 0)]),
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 === 'draw_text') {
98
+ if (name === "draw_text") {
89
99
  const skeleton = {
90
- type: 'text',
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 === 'draw_frame') {
113
+ if (name === "draw_frame") {
104
114
  const skeleton = {
105
- type: 'frame',
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 === 'clear_canvas') continue;
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 === 'draw_group') {
138
+ if (tc.function.name === "draw_group") {
129
139
  // draw_group: 递归转换子元素,统一加 groupIds
130
- const groupId = 'group_' + Date.now() + '_' + Math.random().toString(36).slice(2, 8);
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 = 'draw_' + child.tool;
134
- const childSkeletons = convertToolCallToSkeleton(toolName, child.args || {});
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 (e) {
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 = 'drawAgent';
170
+ const methodName = "drawAgent";
157
171
  const messages = req.body.messages;
158
172
 
159
173
  if (!messages?.length) {
160
- const msg = 'need messages';
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 = messages.filter((m) => m.role === 'user').pop()?.content || '';
170
- req.logger.info(methodName, 'lastUserMsg', lastUserMsg);
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 === 'assistant' && /^[✅❌]/.test(m.content)) {
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
- // 1. Agent 决策
185
- res.streaming(`data: ${JSON.stringify({ type: 'status', step: 'thinking' })}\n\n`);
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(`data: ${JSON.stringify({ type: 'status', step: 'elaborate' })}\n\n`);
242
- req.logger.info(methodName, 'step: elaborate');
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: 'user',
252
- content: prompts.ELABORATE_PROMPT.replace('{input}', decision.description).replace(
253
- '{diagramType}',
254
- decision.diagramType,
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(methodName, 'elaboration', String(elaboration).slice(0, 100) + '...');
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(`data: ${JSON.stringify({ type: 'status', step: 'generate' })}\n\n`);
269
- req.logger.info(methodName, 'step: generate');
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: 'user',
279
- content: prompts.GENERATE_PROMPT.replace('{diagramType}', decision.diagramType).replace(
280
- '{elaboration}',
281
- elaboration,
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
- 'mermaidCode',
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(`data: ${JSON.stringify({ type: 'mermaid', code: mermaidCode, duration })}\n\n`);
271
+ res.streaming(
272
+ `data: ${JSON.stringify({ type: "mermaid", code: mermaidCode, duration })}\n\n`,
273
+ );
298
274
  },
299
275
  },
300
276
  ]);
301
- } else if (decision.action === 'canvas') {
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(`data: ${JSON.stringify({ type: 'status', step: 'drawing' })}\n\n`);
304
- req.logger.info(methodName, 'step: canvas function calling');
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: 'system', content: prompts.CANVAS_SYSTEM_PROMPT },
310
- { role: 'user', content: decision.description },
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: 'auto',
447
+ tool_choice: "auto",
314
448
  });
315
449
 
316
450
  if (canvasResponse.tool_calls?.length) {
317
- req.logger.info(methodName, 'tool_calls', canvasResponse.tool_calls.map((tc) => tc.function.name).join(', '));
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((tc) => tc.function.name === 'clear_canvas');
458
+ const hasClear = canvasResponse.tool_calls.some(
459
+ (tc) => tc.function.name === "clear_canvas",
460
+ );
321
461
  if (hasClear) {
322
- chatFeishuMsg(req, 'canvas-clear');
323
- res.streaming(`data: ${JSON.stringify({ type: 'clear' })}\n\n`);
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(canvasResponse.tool_calls);
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(methodName, 'canvas elements', skeletons.length, `${duration}ms`);
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(`data: ${JSON.stringify({ type: 'draw', elements: skeletons, duration })}\n\n`);
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(`data: ${JSON.stringify({ type: 'message', content: canvasResponse.content })}\n\n`);
485
+ res.streaming(
486
+ `data: ${JSON.stringify({ type: "message", content: canvasResponse.content })}\n\n`,
487
+ );
337
488
  }
338
- } else if (decision.action === 'image') {
489
+ } else if (decision.action === "image") {
339
490
  // 4. Image - 调用 LibLib 图片生成 API
340
- res.streaming(`data: ${JSON.stringify({ type: 'status', step: 'image' })}\n\n`);
341
- req.logger.info(methodName, 'step: image generation', decision.prompt);
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, { aspectRatio: 'square' });
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(methodName, 'liblib submit failed', JSON.stringify(submitRes));
349
- res.streaming(`data: ${JSON.stringify({ type: 'message', content: '图片生成失败,请稍后重试。' })}\n\n`);
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 = imgResponse.headers.get('content-type') || 'image/jpeg';
359
- const dataURL = `data:${contentType};base64,${imgBuffer.toString('base64')}`;
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, 'image generated', `${duration}ms`);
363
- res.streaming(`data: ${JSON.stringify({ type: 'image', dataURL, duration })}\n\n`);
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(methodName, 'liblib no image url', JSON.stringify(result));
366
- res.streaming(`data: ${JSON.stringify({ type: 'message', content: '图片生成失败,请稍后重试。' })}\n\n`);
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 === 'sketch') {
539
+ } else if (decision.action === "sketch") {
370
540
  // 5. Sketch - 通知前端执行线稿提取(处理在前端完成)
371
541
  const duration = Date.now() - startTime;
372
- req.logger.info(methodName, 'step: sketch', `${duration}ms`);
373
- chatFeishuMsg(req, 'sketch');
374
- res.streaming(`data: ${JSON.stringify({ type: 'sketch', duration })}\n\n`);
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: 'message', content: decision.message || '可以再说一次吗?' })}\n\n`,
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, 'error', error);
556
+ req.logger.error(methodName, "error", error);
385
557
  errorFeishuMsg(req, error.message);
386
558
  // 对用户隐藏内部错误细节
387
- const userMessage = error.message?.includes('Cannot parse JSON')
388
- ? '请求处理失败,请重新描述你的需求'
389
- : '服务暂时出错,请稍后重试';
390
- res.streaming(`data: ${JSON.stringify({ type: 'error', message: userMessage })}\n\n`);
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
  };