@shun-js/aibaiban-server 1.2.1 → 1.2.4

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shun-js/aibaiban-server",
3
- "version": "1.2.1",
3
+ "version": "1.2.4",
4
4
  "description": "aibaiban.com server",
5
5
  "keywords": [
6
6
  "ai aibaiban"
@@ -45,5 +45,5 @@
45
45
  "access": "public",
46
46
  "registry": "https://registry.npmjs.org/"
47
47
  },
48
- "gitHead": "9f171abf1a9b47fa0c4d33d38852e1778c533fdb"
48
+ "gitHead": "d9255a37dbd25bf32a5c64b0230440eb34c6de2d"
49
49
  }
@@ -12,9 +12,139 @@ const llm = OpenAIAPI(finalLLMConfig);
12
12
  const modelName = finalLLMConfig.modelName;
13
13
  const thinking = finalLLMConfig.thinking;
14
14
 
15
+ /**
16
+ * convertToolCallToSkeleton - 将单个 tool call 转为 ExcalidrawElementSkeleton
17
+ */
18
+ function convertToolCallToSkeleton(name, args) {
19
+ if (name === 'draw_shape') {
20
+ const skeleton = {
21
+ type: args.shape || 'rectangle',
22
+ x: args.x || 0,
23
+ y: args.y || 0,
24
+ };
25
+ if (args.id) skeleton.id = args.id;
26
+ if (args.width) skeleton.width = args.width;
27
+ if (args.height) skeleton.height = args.height;
28
+ if (args.label) skeleton.label = { text: args.label };
29
+ if (args.strokeColor) skeleton.strokeColor = args.strokeColor;
30
+ if (args.backgroundColor) skeleton.backgroundColor = args.backgroundColor;
31
+ if (args.fillStyle) skeleton.fillStyle = args.fillStyle;
32
+ if (args.strokeWidth) skeleton.strokeWidth = args.strokeWidth;
33
+ if (args.strokeStyle) skeleton.strokeStyle = args.strokeStyle;
34
+ if (args.roughness !== undefined) skeleton.roughness = args.roughness;
35
+ if (args.opacity !== undefined) skeleton.opacity = args.opacity;
36
+ if (args.roundness !== undefined) {
37
+ skeleton.roundness = args.roundness ? { type: 3 } : null;
38
+ }
39
+ return [skeleton];
40
+ }
41
+
42
+ if (name === 'draw_arrow') {
43
+ const skeleton = { type: 'arrow' };
44
+ // 坐标:优先用起点坐标,否则默认 0
45
+ skeleton.x = args.startX || 0;
46
+ skeleton.y = args.startY || 0;
47
+ // 绑定
48
+ if (args.startId) skeleton.start = { id: args.startId };
49
+ if (args.endId) skeleton.end = { id: args.endId };
50
+ // 标签
51
+ if (args.label) skeleton.label = { text: args.label };
52
+ // 箭头样式
53
+ if (args.startArrowhead) skeleton.startArrowhead = args.startArrowhead;
54
+ if (args.endArrowhead !== undefined) skeleton.endArrowhead = args.endArrowhead;
55
+ // 如果没有绑定,用坐标计算 points
56
+ if (!args.startId && !args.endId && args.endX !== undefined && args.endY !== undefined) {
57
+ skeleton.width = args.endX - skeleton.x;
58
+ skeleton.height = args.endY - skeleton.y;
59
+ }
60
+ // 通用样式
61
+ if (args.strokeColor) skeleton.strokeColor = args.strokeColor;
62
+ if (args.strokeWidth) skeleton.strokeWidth = args.strokeWidth;
63
+ if (args.strokeStyle) skeleton.strokeStyle = args.strokeStyle;
64
+ if (args.elbowed) skeleton.elbowed = true;
65
+ return [skeleton];
66
+ }
67
+
68
+ if (name === 'draw_line') {
69
+ if (!args.points || args.points.length < 2) return [];
70
+ const skeleton = {
71
+ type: 'line',
72
+ x: args.points[0][0] || 0,
73
+ y: args.points[0][1] || 0,
74
+ points: args.points.map((p) => [p[0] - (args.points[0][0] || 0), p[1] - (args.points[0][1] || 0)]),
75
+ };
76
+ if (args.strokeColor) skeleton.strokeColor = args.strokeColor;
77
+ if (args.strokeWidth) skeleton.strokeWidth = args.strokeWidth;
78
+ if (args.strokeStyle) skeleton.strokeStyle = args.strokeStyle;
79
+ return [skeleton];
80
+ }
81
+
82
+ if (name === 'draw_text') {
83
+ const skeleton = {
84
+ type: 'text',
85
+ x: args.x || 0,
86
+ y: args.y || 0,
87
+ text: args.text || '',
88
+ };
89
+ if (args.id) skeleton.id = args.id;
90
+ if (args.fontSize) skeleton.fontSize = args.fontSize;
91
+ if (args.fontFamily) skeleton.fontFamily = args.fontFamily;
92
+ if (args.textAlign) skeleton.textAlign = args.textAlign;
93
+ if (args.strokeColor) skeleton.strokeColor = args.strokeColor;
94
+ return [skeleton];
95
+ }
96
+
97
+ if (name === 'draw_frame') {
98
+ const skeleton = {
99
+ type: 'frame',
100
+ children: args.childIds || [],
101
+ };
102
+ if (args.name) skeleton.name = args.name;
103
+ if (args.x !== undefined) skeleton.x = args.x;
104
+ if (args.y !== undefined) skeleton.y = args.y;
105
+ if (args.width) skeleton.width = args.width;
106
+ if (args.height) skeleton.height = args.height;
107
+ return [skeleton];
108
+ }
109
+
110
+ return [];
111
+ }
112
+
113
+ /**
114
+ * convertToolCallsToSkeletons - 将所有 tool calls 转为 skeleton 数组
115
+ */
116
+ function convertToolCallsToSkeletons(toolCalls) {
117
+ const skeletons = [];
118
+ for (const tc of toolCalls) {
119
+ if (tc.function.name === 'clear_canvas') continue;
120
+ try {
121
+ const args = JSON.parse(tc.function.arguments);
122
+ if (tc.function.name === 'draw_group') {
123
+ // draw_group: 递归转换子元素,统一加 groupIds
124
+ const groupId = 'group_' + Date.now() + '_' + Math.random().toString(36).slice(2, 8);
125
+ const children = args.elements || [];
126
+ for (const child of children) {
127
+ const toolName = 'draw_' + child.tool;
128
+ const childSkeletons = convertToolCallToSkeleton(toolName, child.args || {});
129
+ for (const s of childSkeletons) {
130
+ s.groupIds = [groupId];
131
+ skeletons.push(s);
132
+ }
133
+ }
134
+ } else {
135
+ const result = convertToolCallToSkeleton(tc.function.name, args);
136
+ skeletons.push(...result);
137
+ }
138
+ } catch (e) {
139
+ // JSON 解析失败,跳过该 tool call
140
+ }
141
+ }
142
+ return skeletons;
143
+ }
144
+
15
145
  /**
16
146
  * drawAgent - 对话式 Agent
17
- * 1 次决策 LLM → reply / generate(elaborate+generate) / irrelevant
147
+ * 1 次决策 LLM → reply / generate(elaborate+generate) / canvas / irrelevant
18
148
  */
19
149
  exports.drawAgent = async (req, res) => {
20
150
  const methodName = 'drawAgent';
@@ -134,6 +264,43 @@ exports.drawAgent = async (req, res) => {
134
264
  },
135
265
  },
136
266
  ]);
267
+ } else if (decision.action === 'canvas') {
268
+ // 3. Canvas - Function Calling 直接画图
269
+ res.streaming(`data: ${JSON.stringify({ type: 'status', step: 'drawing' })}\n\n`);
270
+ req.logger.info(methodName, 'step: canvas function calling');
271
+
272
+ const canvasResponse = await llm.chat({
273
+ model: modelName,
274
+ messages: [
275
+ { role: 'system', content: prompts.CANVAS_SYSTEM_PROMPT },
276
+ { role: 'user', content: decision.description },
277
+ ],
278
+ tools: prompts.CANVAS_TOOLS,
279
+ tool_choice: 'auto',
280
+ });
281
+
282
+ if (canvasResponse.tool_calls?.length) {
283
+ req.logger.info(methodName, 'tool_calls', canvasResponse.tool_calls.map((tc) => tc.function.name).join(', '));
284
+
285
+ // 检查是否有 clear_canvas
286
+ const hasClear = canvasResponse.tool_calls.some((tc) => tc.function.name === 'clear_canvas');
287
+ if (hasClear) {
288
+ chatFeishuMsg(req, 'canvas-clear');
289
+ res.streaming(`data: ${JSON.stringify({ type: 'clear' })}\n\n`);
290
+ }
291
+
292
+ // 转换为 skeletons
293
+ const skeletons = convertToolCallsToSkeletons(canvasResponse.tool_calls);
294
+ if (skeletons.length > 0) {
295
+ const duration = Date.now() - startTime;
296
+ req.logger.info(methodName, 'canvas elements', skeletons.length, `${duration}ms`);
297
+ chatFeishuMsg(req, `canvas-${skeletons.length}-elements`);
298
+ res.streaming(`data: ${JSON.stringify({ type: 'draw', elements: skeletons, duration })}\n\n`);
299
+ }
300
+ } else if (canvasResponse.content) {
301
+ // LLM 选择文字回复而非调用函数
302
+ res.streaming(`data: ${JSON.stringify({ type: 'message', content: canvasResponse.content })}\n\n`);
303
+ }
137
304
  } else {
138
305
  // 未知 action fallback
139
306
  res.streaming(
@@ -7,10 +7,11 @@ module.exports = {
7
7
  * Agent 决策 prompt
8
8
  * 一次 LLM 调用完成意图判断 + 决策
9
9
  */
10
- AGENT_PROMPT: `你是 AI 白板助手,帮用户在白板上画图表。
10
+ AGENT_PROMPT: `你是 AI 白板助手,帮用户在白板上画图表和图形。
11
11
 
12
12
  ## 能力
13
- 支持 4 种图表:flowchart(流程图)、sequence(时序图)、classDiagram(类图)、erDiagram(ER图)。
13
+ 1. 支持 4 种图表:flowchart(流程图)、sequence(时序图)、classDiagram(类图)、erDiagram(ER图)。
14
+ 2. 支持直接在白板上绘制形状、文字、箭头等基础图形。
14
15
 
15
16
  ## 决策规则
16
17
  收到用户消息后判断,只回复 JSON:
@@ -18,10 +19,13 @@ module.exports = {
18
19
  1. 回复文字(追问、确认、引导):
19
20
  {"action":"reply","message":"你的回复内容"}
20
21
 
21
- 2. 生成图表(信息已足够明确):
22
+ 2. 生成图表(用户要画流程图/时序图/类图/ER图,信息已足够明确):
22
23
  {"action":"generate","diagramType":"类型","description":"详细描述,包含所有节点、关系、步骤"}
23
24
 
24
- 3. 与画图完全无关:
25
+ 3. 直接画图(用户要求画简单形状、文字、箭头等,不属于四种图表):
26
+ {"action":"canvas","description":"用户想画什么的完整描述"}
27
+
28
+ 4. 与画图完全无关:
25
29
  {"action":"irrelevant"}
26
30
 
27
31
  ## 判断标准
@@ -30,6 +34,8 @@ module.exports = {
30
34
  - 用户只说了内容没说类型 → 你来判断最合适的类型,直接 generate
31
35
  - 用户说"帮我画个图" → reply 追问想画什么
32
36
  - 用户要修改已有图表 → 根据对话历史理解上下文,generate 完整新图表
37
+ - 用户要画简单形状(圆、矩形、箭头、文字等)→ canvas
38
+ - 用户说"清空白板" → canvas
33
39
  - 用户闲聊但话题相关 → reply 自然回应并引导回画图
34
40
  - 用户闲聊话题无关 → irrelevant
35
41
 
@@ -37,6 +43,7 @@ module.exports = {
37
43
  - 追问时友好自然,像朋友聊天,不要生硬
38
44
  - 可以给建议和示例帮助用户想清楚需求
39
45
  - generate 时 description 要尽可能详细完整
46
+ - canvas 时 description 要包含形状、颜色、位置等用户提到的所有细节
40
47
 
41
48
  只回复 JSON。`,
42
49
 
@@ -101,5 +108,199 @@ flowchart TD
101
108
  * 非白板请求的固定回复
102
109
  */
103
110
  FIXED_REPLY:
104
- '你好!👋 我是 AI 白板助手,支持绘制以下 4 种图表:\n\n📊 流程图 — 例如"画一个用户注册登录的流程图"\n🔄 时序图 — 例如"画一个用户下单支付的时序图"\n🏗️ 类图 — 例如"画一个电商系统的类图"\n🗄️ ER图 — 例如"画一个博客系统的ER图"\n\n💡 试试直接输入你的需求吧!',
111
+ '你好!👋 我是 AI 白板助手,支持以下功能:\n\n📊 流程图 — 例如"画一个用户注册登录的流程图"\n🔄 时序图 — 例如"画一个用户下单支付的时序图"\n🏗️ 类图 — 例如"画一个电商系统的类图"\n🗄️ ER图 — 例如"画一个博客系统的ER图"\n🎨 自由绘图 — 例如"画一个红色的圆"、"画两个矩形用箭头连接"\n\n💡 试试直接输入你的需求吧!',
112
+
113
+ /**
114
+ * Canvas Function Calling 系统提示词
115
+ */
116
+ CANVAS_SYSTEM_PROMPT: `你是 AI 白板助手,帮用户在白板上直接绘制形状、文字和箭头。
117
+
118
+ ## 坐标系
119
+ - 画布从左上角 (0, 0) 开始,x 向右增长,y 向下增长
120
+ - 建议将元素放在 (100-800, 100-600) 的范围内
121
+ - 默认形状大小约 150x80
122
+
123
+ ## 颜色建议
124
+ - 红: #ff6b6b 青: #4ecdc4 蓝: #45b7d1 黄: #ffeaa7 深蓝: #1971c2
125
+ - 绿: #51cf66 紫: #9775fa 橙: #ff922b 粉: #f06595 灰: #868e96
126
+
127
+ ## 注意事项
128
+ - 圆形用 draw_shape 的 ellipse 类型,宽高相等即可
129
+ - 需要箭头连接的形状必须指定 id
130
+ - 多个相关元素建议用 draw_group 编组
131
+ - 如果用户没指定位置/颜色/大小,选择合理的默认值
132
+ - 形状内文字通过 label 参数设置`,
133
+
134
+ /**
135
+ * Canvas Function Calling 工具定义
136
+ */
137
+ CANVAS_TOOLS: [
138
+ {
139
+ type: 'function',
140
+ function: {
141
+ name: 'draw_shape',
142
+ description: '在白板上绘制基础形状(矩形、椭圆、菱形),可带文字标签。需要被箭头连接时必须指定 id。',
143
+ parameters: {
144
+ type: 'object',
145
+ properties: {
146
+ id: { type: 'string', description: '元素ID,箭头绑定时需要引用' },
147
+ shape: { type: 'string', enum: ['rectangle', 'ellipse', 'diamond'] },
148
+ x: { type: 'number', description: '左上角X坐标' },
149
+ y: { type: 'number', description: '左上角Y坐标' },
150
+ width: { type: 'number', description: '宽度,默认100' },
151
+ height: { type: 'number', description: '高度,默认100' },
152
+ label: { type: 'string', description: '形状内的文字标签' },
153
+ strokeColor: { type: 'string', description: '边框颜色,默认#1e1e1e' },
154
+ backgroundColor: { type: 'string', description: '填充颜色,默认transparent' },
155
+ fillStyle: { type: 'string', enum: ['hachure', 'cross-hatch', 'solid', 'zigzag'] },
156
+ strokeWidth: { type: 'number', enum: [1, 2, 4], description: '1=细,2=中,4=粗' },
157
+ strokeStyle: { type: 'string', enum: ['solid', 'dashed', 'dotted'] },
158
+ roughness: { type: 'number', enum: [0, 1, 2], description: '0=精确,1=手绘,2=夸张' },
159
+ roundness: { type: 'boolean', description: '是否圆角,默认true' },
160
+ opacity: { type: 'number', description: '不透明度0-100,默认100' },
161
+ },
162
+ required: ['shape', 'x', 'y'],
163
+ },
164
+ },
165
+ },
166
+ {
167
+ type: 'function',
168
+ function: {
169
+ name: 'draw_arrow',
170
+ description: '绘制箭头。可通过 startId/endId 绑定到已有形状(箭头自动吸附),也可直接指定坐标点。',
171
+ parameters: {
172
+ type: 'object',
173
+ properties: {
174
+ startX: { type: 'number', description: '起点X(无startId时必填)' },
175
+ startY: { type: 'number', description: '起点Y(无startId时必填)' },
176
+ endX: { type: 'number', description: '终点X(无endId时必填)' },
177
+ endY: { type: 'number', description: '终点Y(无endId时必填)' },
178
+ startId: { type: 'string', description: '起点绑定的形状ID' },
179
+ endId: { type: 'string', description: '终点绑定的形状ID' },
180
+ label: { type: 'string', description: '箭头上的文字标签' },
181
+ startArrowhead: {
182
+ type: 'string',
183
+ enum: ['arrow', 'bar', 'dot', 'triangle', 'diamond'],
184
+ description: '起点箭头样式,默认无',
185
+ },
186
+ endArrowhead: {
187
+ type: 'string',
188
+ enum: ['arrow', 'bar', 'dot', 'triangle', 'diamond'],
189
+ description: '终点箭头样式,默认arrow',
190
+ },
191
+ strokeColor: { type: 'string' },
192
+ strokeWidth: { type: 'number', enum: [1, 2, 4] },
193
+ strokeStyle: { type: 'string', enum: ['solid', 'dashed', 'dotted'] },
194
+ elbowed: { type: 'boolean', description: '是否使用直角折线,默认false' },
195
+ },
196
+ required: [],
197
+ },
198
+ },
199
+ },
200
+ {
201
+ type: 'function',
202
+ function: {
203
+ name: 'draw_line',
204
+ description: '绘制线条(无箭头),支持多个折点',
205
+ parameters: {
206
+ type: 'object',
207
+ properties: {
208
+ points: {
209
+ type: 'array',
210
+ items: { type: 'array', items: { type: 'number' } },
211
+ description: '折点坐标数组,如 [[0,0],[100,50],[200,0]]',
212
+ },
213
+ strokeColor: { type: 'string' },
214
+ strokeWidth: { type: 'number', enum: [1, 2, 4] },
215
+ strokeStyle: { type: 'string', enum: ['solid', 'dashed', 'dotted'] },
216
+ },
217
+ required: ['points'],
218
+ },
219
+ },
220
+ },
221
+ {
222
+ type: 'function',
223
+ function: {
224
+ name: 'draw_text',
225
+ description: '在白板上添加独立文本',
226
+ parameters: {
227
+ type: 'object',
228
+ properties: {
229
+ id: { type: 'string' },
230
+ x: { type: 'number' },
231
+ y: { type: 'number' },
232
+ text: { type: 'string' },
233
+ fontSize: { type: 'number', description: '字号,默认20' },
234
+ fontFamily: {
235
+ type: 'number',
236
+ enum: [1, 2, 3, 5],
237
+ description: '1=手写,2=常规,3=等宽,5=圆体',
238
+ },
239
+ textAlign: { type: 'string', enum: ['left', 'center', 'right'] },
240
+ strokeColor: { type: 'string' },
241
+ },
242
+ required: ['x', 'y', 'text'],
243
+ },
244
+ },
245
+ },
246
+ {
247
+ type: 'function',
248
+ function: {
249
+ name: 'draw_group',
250
+ description: '批量创建多个元素并自动编组(移动时一起移动)。适合画一组相关图形。',
251
+ parameters: {
252
+ type: 'object',
253
+ properties: {
254
+ elements: {
255
+ type: 'array',
256
+ description: '元素数组,每个元素格式同其他 draw 工具的参数',
257
+ items: {
258
+ type: 'object',
259
+ properties: {
260
+ tool: { type: 'string', enum: ['shape', 'arrow', 'line', 'text'] },
261
+ args: {
262
+ type: 'object',
263
+ description: '对应 draw_shape/draw_arrow/draw_line/draw_text 的参数',
264
+ },
265
+ },
266
+ required: ['tool', 'args'],
267
+ },
268
+ },
269
+ },
270
+ required: ['elements'],
271
+ },
272
+ },
273
+ },
274
+ {
275
+ type: 'function',
276
+ function: {
277
+ name: 'draw_frame',
278
+ description: '创建一个 Frame 容器,包含指定的子元素(子元素会被框在一起)',
279
+ parameters: {
280
+ type: 'object',
281
+ properties: {
282
+ name: { type: 'string', description: 'Frame 名称' },
283
+ childIds: { type: 'array', items: { type: 'string' }, description: '子元素ID数组' },
284
+ x: { type: 'number' },
285
+ y: { type: 'number' },
286
+ width: { type: 'number' },
287
+ height: { type: 'number' },
288
+ },
289
+ required: ['childIds'],
290
+ },
291
+ },
292
+ },
293
+ {
294
+ type: 'function',
295
+ function: {
296
+ name: 'clear_canvas',
297
+ description: '清空白板上的所有内容',
298
+ parameters: {
299
+ type: 'object',
300
+ properties: {},
301
+ required: [],
302
+ },
303
+ },
304
+ },
305
+ ],
105
306
  };
package/views/index.html CHANGED
@@ -105,7 +105,7 @@
105
105
  <script
106
106
  type="module"
107
107
  crossorigin
108
- src="https://static-small.vincentqiao.com/aibaiban/static/index-BaMP5IJA.js"
108
+ src="https://static-small.vincentqiao.com/aibaiban/static/index-Fo5F7gnm.js"
109
109
  ></script>
110
110
  <link
111
111
  rel="modulepreload"
@@ -125,12 +125,12 @@
125
125
  <link
126
126
  rel="modulepreload"
127
127
  crossorigin
128
- href="https://static-small.vincentqiao.com/aibaiban/static/chunks/antd-x-2IS7LzuX.js"
128
+ href="https://static-small.vincentqiao.com/aibaiban/static/chunks/antd-x-xgDJhrnl.js"
129
129
  />
130
130
  <link
131
131
  rel="modulepreload"
132
132
  crossorigin
133
- href="https://static-small.vincentqiao.com/aibaiban/static/chunks/excalidraw-7ypewro4.js"
133
+ href="https://static-small.vincentqiao.com/aibaiban/static/chunks/excalidraw-XjoTMqq7.js"
134
134
  />
135
135
  <link
136
136
  rel="stylesheet"