foliko 1.1.1 → 1.1.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.
Files changed (57) hide show
  1. package/.agent/data/weixin-media/2026-04-08/img_1775618677512.jpg +0 -0
  2. package/.agent/data/weixin-media/2026-04-08/img_1775619073340.jpg +0 -0
  3. package/.agent/data/weixin-media/2026-04-08/img_1775619097536.jpg +0 -0
  4. package/.agent/data/weixin-media/2026-04-08/img_1775619209388.jpg +0 -0
  5. package/.agent/plugins/poster-plugin/package.json +2 -1
  6. package/.agent/plugins/poster-plugin/src/canvas.js +70 -7
  7. package/.agent/plugins/poster-plugin/src/components/barcode.js +120 -0
  8. package/.agent/plugins/poster-plugin/src/components/bubble.js +153 -0
  9. package/.agent/plugins/poster-plugin/src/components/button.js +124 -0
  10. package/.agent/plugins/poster-plugin/src/components/cta.js +26 -24
  11. package/.agent/plugins/poster-plugin/src/components/featureGrid.js +22 -17
  12. package/.agent/plugins/poster-plugin/src/components/frame.js +230 -0
  13. package/.agent/plugins/poster-plugin/src/components/highlightText.js +144 -0
  14. package/.agent/plugins/poster-plugin/src/components/icon.js +94 -0
  15. package/.agent/plugins/poster-plugin/src/components/index.js +19 -0
  16. package/.agent/plugins/poster-plugin/src/components/listItem.js +6 -5
  17. package/.agent/plugins/poster-plugin/src/components/qrcode.js +74 -0
  18. package/.agent/plugins/poster-plugin/src/components/ribbon.js +193 -0
  19. package/.agent/plugins/poster-plugin/src/components/seal.js +146 -0
  20. package/.agent/plugins/poster-plugin/src/components/table.js +17 -9
  21. package/.agent/plugins/poster-plugin/src/components/tagCloud.js +24 -17
  22. package/.agent/plugins/poster-plugin/src/components/timeline.js +24 -12
  23. package/.agent/plugins/poster-plugin/src/composer.js +392 -150
  24. package/.agent/plugins/poster-plugin/src/elements/background.js +36 -4
  25. package/.agent/plugins/poster-plugin/src/elements/image.js +4 -47
  26. package/.agent/plugins/poster-plugin/src/elements/index.js +2 -0
  27. package/.agent/plugins/poster-plugin/src/elements/richText.js +230 -0
  28. package/.agent/plugins/poster-plugin/src/elements/svg.js +35 -19
  29. package/.agent/plugins/poster-plugin/src/index.js +430 -7
  30. package/.agent/plugins/poster-plugin/src/utils/imageLoader.js +84 -0
  31. package/.agent/plugins/poster-plugin/test-background.svg +1 -0
  32. package/.agent/plugins/poster-plugin/test-full-poster.svg +2 -0
  33. package/.agent/plugins/poster-plugin/test-image.png +0 -0
  34. package/.agent/sessions/cli_default.json +1089 -145
  35. package/.agent/sessions/weixin_o9cq80zgZqKPA2-s59PN43GdDy1w@im.wechat.json +8902 -0
  36. package/.claude/settings.local.json +6 -1
  37. package/output/beef-love-poster.png +0 -0
  38. package/output/international-news-daily.png +0 -0
  39. package/package.json +2 -1
  40. package/plugins/extension-executor-plugin.js +33 -32
  41. package/plugins/file-system-plugin.js +4 -19
  42. package/plugins/subagent-plugin.js +37 -14
  43. package/plugins/weixin-plugin.js +167 -47
  44. package/poster-test-2.png +0 -0
  45. package/skills/poster-guide/SKILL.md +497 -5
  46. package/src/core/agent-chat.js +141 -8
  47. package/src/core/agent.js +6 -3
  48. package/calc_tokens_weixin.js +0 -81
  49. package/foliko-creative-3.png +0 -0
  50. package/foliko-creative-4.png +0 -0
  51. package/foliko-creative-5.png +0 -0
  52. package/story-cover-book-v2.png +0 -0
  53. package/story-cover-japanese-1.png +0 -0
  54. package/story-cover-japanese-2.png +0 -0
  55. package/story-cover-japanese-3.png +0 -0
  56. package/story-cover-moran.png +0 -0
  57. package/undefined.png +0 -0
@@ -1,11 +1,52 @@
1
1
  /**
2
2
  * 海报组件化生成器
3
- *
3
+ *
4
4
  * 支持 JSON 配置驱动,一次调用生成完整海报
5
5
  */
6
6
 
7
7
  const paper = require('paper')
8
8
 
9
+ // 导入新组件
10
+ const createButton = require('./components/button')
11
+ const createIcon = require('./components/icon')
12
+ const createQRCode = require('./components/qrcode')
13
+ const createFrame = require('./components/frame')
14
+ const createBubble = require('./components/bubble')
15
+ const createRibbon = require('./components/ribbon')
16
+ const createSeal = require('./components/seal')
17
+ const createHighlightText = require('./components/highlightText')
18
+ const createBarcode = require('./components/barcode')
19
+ const { loadImageAsRaster } = require('./utils/imageLoader')
20
+
21
+ // 组件包装函数
22
+ async function createButtonComponent(project, canvas, args) {
23
+ return await createButton(project, args)
24
+ }
25
+ async function createIconComponent(project, canvas, args) {
26
+ return await createIcon(project, args)
27
+ }
28
+ async function createQRCodeComponent(project, canvas, args) {
29
+ return await createQRCode(project, args)
30
+ }
31
+ async function createFrameComponent(project, canvas, args) {
32
+ return await createFrame(project, args)
33
+ }
34
+ async function createBubbleComponent(project, canvas, args) {
35
+ return await createBubble(project, args)
36
+ }
37
+ async function createRibbonComponent(project, canvas, args) {
38
+ return await createRibbon(project, args)
39
+ }
40
+ async function createSealComponent(project, canvas, args) {
41
+ return await createSeal(project, args)
42
+ }
43
+ async function createHighlightTextComponent(project, canvas, args) {
44
+ return await createHighlightText(project, args)
45
+ }
46
+ async function createBarcodeComponent(project, canvas, args) {
47
+ return await createBarcode(project, args)
48
+ }
49
+
9
50
  /**
10
51
  * 辅助函数:将元素添加到活跃层
11
52
  */
@@ -74,6 +115,16 @@ const COMPONENT_TYPES = {
74
115
  timeline: 'timeline',
75
116
  listItem: 'listItem',
76
117
  notification: 'notification',
118
+ // 设计组件
119
+ button: 'button',
120
+ icon: 'icon',
121
+ qrcode: 'qrcode',
122
+ frame: 'frame',
123
+ bubble: 'bubble',
124
+ ribbon: 'ribbon',
125
+ seal: 'seal',
126
+ highlightText: 'highlightText',
127
+ barcode: 'barcode',
77
128
  }
78
129
 
79
130
  /**
@@ -108,13 +159,13 @@ async function createFromConfig(project, canvas, config) {
108
159
  /**
109
160
  * 根据配置创建单个组件
110
161
  */
111
- function createComponent(project, canvas, config) {
162
+ async function createComponent(project, canvas, config) {
112
163
  const { type, ...args } = config
113
164
 
114
165
  switch (type) {
115
166
  // 基础元素
116
167
  case 'background':
117
- return createBackgroundElement(project, canvas, args)
168
+ return await createBackgroundElement(project, canvas, args)
118
169
  case 'rectangle':
119
170
  return createRectangleElement(project, args)
120
171
  case 'circle':
@@ -126,67 +177,87 @@ function createComponent(project, canvas, config) {
126
177
  case 'text':
127
178
  return createTextElement(project, args)
128
179
  case 'artText':
129
- return createArtTextElement(project, args)
180
+ return await createArtTextElement(project, args)
130
181
  case 'image':
131
- return createImageElement(project, args)
182
+ return await createImageElement(project, args)
132
183
  case 'svg':
133
- return createSVGElement(project, args)
184
+ return await createSVGElement(project, args)
134
185
  case 'imageFrame':
135
- return createImageFrameComponent(project, canvas, args)
186
+ return await createImageFrameComponent(project, canvas, args)
136
187
  case 'columns':
137
- return createColumnsComponent(project, canvas, args)
188
+ return await createColumnsComponent(project, canvas, args)
138
189
  case 'grid':
139
- return createGridComponent(project, canvas, args)
190
+ return await createGridComponent(project, canvas, args)
140
191
 
141
192
  // 装饰组件
142
193
  case 'star':
143
- return createStarComponent(project, canvas, args)
194
+ return await createStarComponent(project, canvas, args)
144
195
  case 'arrow':
145
- return createArrowComponent(project, canvas, args)
196
+ return await createArrowComponent(project, canvas, args)
146
197
  case 'progressCircle':
147
- return createProgressCircleComponent(project, canvas, args)
198
+ return await createProgressCircleComponent(project, canvas, args)
148
199
  case 'chip':
149
- return createChipComponent(project, canvas, args)
200
+ return await createChipComponent(project, canvas, args)
150
201
  case 'chart':
151
- return createChartComponent(project, canvas, args)
202
+ return await createChartComponent(project, canvas, args)
152
203
  case 'watermark':
153
- return createWatermarkComponent(project, canvas, args)
204
+ return await createWatermarkComponent(project, canvas, args)
154
205
  case 'table':
155
- return createTableComponent(project, canvas, args)
206
+ return await createTableComponent(project, canvas, args)
156
207
 
157
208
  // 高级组件
158
209
  case 'card':
159
- return createCardComponent(project, canvas, args)
210
+ return await createCardComponent(project, canvas, args)
160
211
  case 'badge':
161
- return createBadgeComponent(project, canvas, args)
212
+ return await createBadgeComponent(project, canvas, args)
162
213
  case 'cta':
163
- return createCTAComponent(project, canvas, args)
214
+ return await createCTAComponent(project, canvas, args)
164
215
  case 'feature':
165
- return createFeatureComponent(project, canvas, args)
216
+ return await createFeatureComponent(project, canvas, args)
166
217
  case 'featureGrid':
167
- return createFeatureGridComponent(project, canvas, args)
218
+ return await createFeatureGridComponent(project, canvas, args)
168
219
  case 'divider':
169
- return createDividerComponent(project, canvas, args)
220
+ return await createDividerComponent(project, canvas, args)
170
221
  case 'avatar':
171
- return createAvatarComponent(project, canvas, args)
222
+ return await createAvatarComponent(project, canvas, args)
172
223
  case 'progress':
173
- return createProgressComponent(project, canvas, args)
224
+ return await createProgressComponent(project, canvas, args)
174
225
  case 'rating':
175
- return createRatingComponent(project, canvas, args)
226
+ return await createRatingComponent(project, canvas, args)
176
227
  case 'quote':
177
- return createQuoteComponent(project, canvas, args)
228
+ return await createQuoteComponent(project, canvas, args)
178
229
  case 'statCard':
179
- return createStatCardComponent(project, canvas, args)
230
+ return await createStatCardComponent(project, canvas, args)
180
231
  case 'tagCloud':
181
- return createTagCloudComponent(project, canvas, args)
232
+ return await createTagCloudComponent(project, canvas, args)
182
233
  case 'stepper':
183
- return createStepperComponent(project, canvas, args)
234
+ return await createStepperComponent(project, canvas, args)
184
235
  case 'timeline':
185
- return createTimelineComponent(project, canvas, args)
236
+ return await createTimelineComponent(project, canvas, args)
186
237
  case 'listItem':
187
- return createListItemComponent(project, canvas, args)
238
+ return await createListItemComponent(project, canvas, args)
188
239
  case 'notification':
189
- return createNotificationComponent(project, canvas, args)
240
+ return await createNotificationComponent(project, canvas, args)
241
+
242
+ // 设计组件
243
+ case 'button':
244
+ return await createButtonComponent(project, canvas, args)
245
+ case 'icon':
246
+ return await createIconComponent(project, canvas, args)
247
+ case 'qrcode':
248
+ return await createQRCodeComponent(project, canvas, args)
249
+ case 'frame':
250
+ return await createFrameComponent(project, canvas, args)
251
+ case 'bubble':
252
+ return await createBubbleComponent(project, canvas, args)
253
+ case 'ribbon':
254
+ return await createRibbonComponent(project, canvas, args)
255
+ case 'seal':
256
+ return await createSealComponent(project, canvas, args)
257
+ case 'highlightText':
258
+ return await createHighlightTextComponent(project, canvas, args)
259
+ case 'barcode':
260
+ return createBarcodeComponent(project, canvas, args)
190
261
 
191
262
  default:
192
263
  return { success: false, error: `Unknown component type: ${type}` }
@@ -195,8 +266,71 @@ function createComponent(project, canvas, config) {
195
266
 
196
267
  // ============= 基础元素创建函数 =============
197
268
 
198
- function createBackgroundElement(project, canvas, { color, gradient }) {
199
- if (gradient) {
269
+ function createBackgroundElement(project, canvas, { color, gradient, image }) {
270
+ if (image) {
271
+ const fs = require('fs')
272
+ const path = require('path')
273
+
274
+ // 确保 image 是字符串
275
+ if (typeof image !== 'string') {
276
+ return { success: false, error: 'Background image must be a string' }
277
+ }
278
+
279
+ // 本地文件路径
280
+ let absolutePath = image
281
+ if (!path.isAbsolute(absolutePath)) {
282
+ absolutePath = path.join(process.cwd(), absolutePath)
283
+ }
284
+
285
+ if (!fs.existsSync(absolutePath)) {
286
+ throw new Error(`背景图片文件不存在: ${absolutePath}`)
287
+ }
288
+
289
+ const buffer = fs.readFileSync(absolutePath)
290
+ const ext = path.extname(absolutePath).toLowerCase()
291
+ const mimeTypes = {
292
+ '.png': 'image/png',
293
+ '.jpg': 'image/jpeg',
294
+ '.jpeg': 'image/jpeg',
295
+ '.gif': 'image/gif',
296
+ '.webp': 'image/webp',
297
+ '.bmp': 'image/bmp'
298
+ }
299
+ const mimeType = mimeTypes[ext] || 'image/png'
300
+ const imageUrl = `data:${mimeType};base64,${buffer.toString('base64')}`
301
+
302
+ const raster = new paper.Raster(imageUrl)
303
+
304
+ // 添加到项目活动层
305
+ if (project && project.activeLayer) {
306
+ project.activeLayer.addChild(raster)
307
+ }
308
+
309
+ raster.onLoad = () => {
310
+ // 计算缩放比例,使图片覆盖整个画布(cover 模式)
311
+ const canvasRatio = canvas.width / canvas.height
312
+ const imageRatio = raster.width / raster.height
313
+
314
+ let scaledWidth, scaledHeight, offsetX, offsetY
315
+
316
+ if (imageRatio > canvasRatio) {
317
+ // 图片更宽,以高度为基准缩放
318
+ scaledHeight = canvas.height
319
+ scaledWidth = raster.width * (canvas.height / raster.height)
320
+ offsetX = (canvas.width - scaledWidth) / 2
321
+ offsetY = 0
322
+ } else {
323
+ // 图片更高,以宽度为基准缩放
324
+ scaledWidth = canvas.width
325
+ scaledHeight = raster.height * (canvas.width / raster.width)
326
+ offsetX = 0
327
+ offsetY = (canvas.height - scaledHeight) / 2
328
+ }
329
+
330
+ raster.bounds = new paper.Rectangle(offsetX, offsetY, scaledWidth, scaledHeight)
331
+ raster.sendToBack()
332
+ }
333
+ } else if (gradient) {
200
334
  const paperColors = gradient.colors.map(c => new paper.Color(c))
201
335
  const { type, direction } = gradient
202
336
 
@@ -248,13 +382,23 @@ function createRectangleElement(project, { x, y, width, height, fill, stroke, st
248
382
  }
249
383
  if (opacity !== undefined) rect.opacity = opacity
250
384
 
385
+ if (project && project.activeLayer) {
386
+ project.activeLayer.addChild(rect)
387
+ }
388
+
251
389
  return { success: true, id: rect.id, type: 'rectangle' }
252
390
  }
253
391
 
254
- function createCircleElement(project, { cx, cy, rx, ry, fill, stroke, strokeWidth, opacity }) {
392
+ function createCircleElement(project, { x, y, cx, cy, radius, rx, ry, fill, stroke, strokeWidth, opacity }) {
393
+ // 兼容 x, y, radius 或 cx, cy, rx, ry 格式
394
+ const centerX = cx || x
395
+ const centerY = cy || y
396
+ const radiusX = rx || radius || 30
397
+ const radiusY = ry || radius || 30
398
+
255
399
  const circle = new paper.Path.Ellipse({
256
- center: [cx, cy],
257
- radius: [rx, ry || rx],
400
+ center: [centerX, centerY],
401
+ radius: [radiusX, radiusY],
258
402
  })
259
403
 
260
404
  if (fill) circle.fillColor = new paper.Color(fill)
@@ -264,6 +408,10 @@ function createCircleElement(project, { cx, cy, rx, ry, fill, stroke, strokeWidt
264
408
  }
265
409
  if (opacity !== undefined) circle.opacity = opacity
266
410
 
411
+ if (project && project.activeLayer) {
412
+ project.activeLayer.addChild(circle)
413
+ }
414
+
267
415
  return { success: true, id: circle.id, type: 'circle' }
268
416
  }
269
417
 
@@ -275,6 +423,10 @@ function createLineElement(project, { x1, y1, x2, y2, stroke, strokeWidth }) {
275
423
  strokeWidth: strokeWidth || 2,
276
424
  })
277
425
 
426
+ if (project && project.activeLayer) {
427
+ project.activeLayer.addChild(line)
428
+ }
429
+
278
430
  return { success: true, id: line.id, type: 'line' }
279
431
  }
280
432
 
@@ -292,6 +444,10 @@ function createPolygonElement(project, { cx, cy, radius, sides, fill, stroke, st
292
444
  }
293
445
  if (opacity !== undefined) polygon.opacity = opacity
294
446
 
447
+ if (project && project.activeLayer) {
448
+ project.activeLayer.addChild(polygon)
449
+ }
450
+
295
451
  return { success: true, id: polygon.id, type: 'polygon' }
296
452
  }
297
453
 
@@ -313,6 +469,10 @@ function createTextElement(project, { text, x, y, fontSize, fontFamily, color, a
313
469
  textItem.shadowOffset = new paper.Point(shadow.offsetX || 2, shadow.offsetY || 2)
314
470
  }
315
471
 
472
+ if (project && project.activeLayer) {
473
+ project.activeLayer.addChild(textItem)
474
+ }
475
+
316
476
  return { success: true, id: textItem.id, type: 'text' }
317
477
  }
318
478
 
@@ -348,100 +508,82 @@ function createArtTextElement(project, { text, x, y, fontSize, fontFamily, gradi
348
508
  textItem.shadowOffset = new paper.Point(shadow.offsetX || 3, shadow.offsetY || 3)
349
509
  }
350
510
 
511
+ if (project && project.activeLayer) {
512
+ project.activeLayer.addChild(textItem)
513
+ }
514
+
351
515
  return { success: true, id: textItem.id, type: 'artText' }
352
516
  }
353
517
 
354
- function createImageElement(project, { src, x, y, width, height, opacity }) {
355
- const fs = require('fs')
356
- const path = require('path')
357
-
358
- // 将图片转换为 Base64 data URL
359
- let imageUrl = src
360
-
361
- // 如果是本地文件,转换为 Base64
362
- if (!src.startsWith('data:') && !src.startsWith('http')) {
363
- let absolutePath = src
364
- if (!path.isAbsolute(absolutePath)) {
365
- absolutePath = path.join(process.cwd(), absolutePath)
366
- }
367
-
368
- if (!fs.existsSync(absolutePath)) {
369
- return { success: false, error: `文件不存在: ${absolutePath}` }
370
- }
518
+ async function createImageElement(project, { src, x = 0, y = 0, width, height, opacity = 1 }) {
519
+ try {
520
+ const { raster } = await loadImageAsRaster(project, src, { x, y, width, height }, opacity)
371
521
 
372
- const buffer = fs.readFileSync(absolutePath)
373
- const ext = path.extname(absolutePath).toLowerCase()
374
- const mimeTypes = {
375
- '.png': 'image/png',
376
- '.jpg': 'image/jpeg',
377
- '.jpeg': 'image/jpeg',
378
- '.gif': 'image/gif',
379
- '.webp': 'image/webp',
380
- '.bmp': 'image/bmp'
522
+ return {
523
+ success: true,
524
+ id: raster.id,
525
+ type: 'image',
381
526
  }
382
- const mimeType = mimeTypes[ext] || 'image/png'
383
- imageUrl = `data:${mimeType};base64,${buffer.toString('base64')}`
527
+ } catch (err) {
528
+ return { success: false, error: `Failed to load image: ${err.message}` }
384
529
  }
530
+ }
385
531
 
386
- const raster = new paper.Raster(imageUrl)
387
-
388
- raster.onLoad = () => {
389
- if (width && height) {
390
- raster.bounds = new paper.Rectangle(x, y, width, height)
391
- } else if (width) {
392
- const scale = width / raster.width
393
- raster.bounds = new paper.Rectangle(x, y, width, raster.height * scale)
394
- } else if (height) {
395
- const scale = height / raster.height
396
- raster.bounds = new paper.Rectangle(x, y, raster.width * scale, height)
397
- } else {
398
- raster.position = new paper.Point(x, y)
399
- }
532
+ const fs = require('fs')
400
533
 
401
- if (opacity !== undefined) raster.opacity = opacity
402
- }
534
+ async function createSVGElement(project, { src, x = 0, y = 0, width, height, opacity = 1 }) {
535
+ const fs = require('fs')
536
+ const path = require('path')
403
537
 
404
- return {
405
- success: true,
406
- id: raster.id,
407
- type: 'image',
408
- width: raster.width,
409
- height: raster.height,
538
+ // 确保 src 是字符串
539
+ if (typeof src !== 'string') {
540
+ return { success: false, error: 'SVG source must be a string' }
410
541
  }
411
- }
412
-
413
- const fs = require('fs')
414
542
 
415
- function createSVGElement(project, { src, x, y, width, height, opacity }) {
416
543
  let svgContent = src
417
544
 
418
545
  // 如果是文件路径,读取文件内容
419
546
  if (!src.startsWith('<') && !src.startsWith('<?xml')) {
420
547
  try {
421
- svgContent = fs.readFileSync(src, 'utf8')
548
+ let filePath = src
549
+ if (!path.isAbsolute(filePath)) {
550
+ filePath = path.join(process.cwd(), filePath)
551
+ }
552
+ svgContent = fs.readFileSync(filePath, 'utf8')
422
553
  } catch (e) {
423
554
  return { success: false, error: `Failed to read SVG file: ${e.message}` }
424
555
  }
425
556
  }
426
557
 
427
- // 导入 SVG
428
- const svg = project.importSVG(svgContent)
558
+ // 导入 SVG 到指定项目
559
+ let svg
560
+ try {
561
+ svg = project.importSVG(svgContent)
562
+ } catch (e) {
563
+ return { success: false, error: `Failed to import SVG: ${e.message}` }
564
+ }
429
565
 
430
566
  if (!svg) {
431
567
  return { success: false, error: 'Failed to import SVG' }
432
568
  }
433
569
 
570
+ // 确保 SVG 添加到活动层
571
+ if (project && project.activeLayer && svg.parent !== project.activeLayer) {
572
+ project.activeLayer.addChild(svg)
573
+ }
574
+
434
575
  // 设置位置
435
576
  svg.position = new paper.Point(x, y)
436
577
 
437
578
  // 设置尺寸
438
579
  if (width && height) {
439
- svg.bounds.width = width
440
- svg.bounds.height = height
580
+ const scaleX = width / svg.bounds.width
581
+ const scaleY = height / svg.bounds.height
582
+ svg.scale(Math.min(scaleX, scaleY), svg.bounds.center)
441
583
  } else if (width) {
442
- svg.scale(width / svg.bounds.width)
584
+ svg.scale(width / svg.bounds.width, svg.bounds.center)
443
585
  } else if (height) {
444
- svg.scale(height / svg.bounds.height)
586
+ svg.scale(height / svg.bounds.height, svg.bounds.center)
445
587
  }
446
588
 
447
589
  // 设置透明度
@@ -543,9 +685,15 @@ function createCTAComponent(project, canvas, {
543
685
  x, y, text,
544
686
  background = '#007bff', color = '#ffffff',
545
687
  border, fontSize = 20, padding = 25, radius = 8, shadow,
688
+ width: customWidth,
546
689
  }) {
547
- const textWidth = text.length * fontSize * 0.7
548
- const btnWidth = textWidth + padding * 2
690
+ // 确保 text 是字符串
691
+ const textStr = String(text || '')
692
+ // 使用更准确的字符宽度估算:中文约1.0,英文约0.5
693
+ const chineseChars = (textStr.match(/[\u4e00-\u9fa5]/g) || []).length
694
+ const otherChars = textStr.length - chineseChars
695
+ const textWidth = chineseChars * fontSize * 1.0 + otherChars * fontSize * 0.5
696
+ const btnWidth = customWidth || (textWidth + padding * 2)
549
697
  const btnHeight = fontSize + padding * 2
550
698
  const btnX = x - btnWidth / 2
551
699
 
@@ -565,12 +713,18 @@ function createCTAComponent(project, canvas, {
565
713
 
566
714
  const buttonText = new paper.PointText({
567
715
  point: [x, y + btnHeight / 2 + fontSize / 3],
568
- content: text,
716
+ content: textStr,
569
717
  fontSize: fontSize,
570
718
  fillColor: new paper.Color(color),
571
719
  justification: 'center',
572
720
  })
573
721
 
722
+ // 添加到项目
723
+ if (project && project.activeLayer) {
724
+ project.activeLayer.addChild(button)
725
+ project.activeLayer.addChild(buttonText)
726
+ }
727
+
574
728
  return { success: true, elements: [{ type: 'rectangle', id: button.id }, { type: 'text', id: buttonText.id }], type: 'cta' }
575
729
  }
576
730
 
@@ -627,6 +781,13 @@ function createFeatureGridComponent(project, canvas, {
627
781
  }) {
628
782
  const elements = []
629
783
 
784
+ // 确保 items 是数组
785
+ if (!Array.isArray(items)) {
786
+ items = []
787
+ }
788
+
789
+ const rows = items.length > 0 ? Math.ceil(items.length / columns) : 0
790
+
630
791
  for (let i = 0; i < items.length; i++) {
631
792
  const item = items[i]
632
793
  const col = i % columns
@@ -645,39 +806,56 @@ function createFeatureGridComponent(project, canvas, {
645
806
  bg.opacity = 0.8
646
807
  elements.push(bg)
647
808
 
809
+ // 添加到项目
810
+ if (project && project.activeLayer) {
811
+ project.activeLayer.addChild(bg)
812
+ }
813
+
648
814
  const padding = 15
649
815
  let offsetY = itemY + padding
650
816
 
651
817
  if (item.icon) {
652
- elements.push(new paper.PointText({
818
+ const iconText = new paper.PointText({
653
819
  point: [itemX + padding, offsetY + 24],
654
820
  content: item.icon,
655
821
  fontSize: 28,
656
822
  fillColor: new paper.Color(item.iconColor || '#00ff88'),
657
823
  justification: 'left',
658
- }))
824
+ })
825
+ elements.push(iconText)
826
+ if (project && project.activeLayer) {
827
+ project.activeLayer.addChild(iconText)
828
+ }
659
829
  offsetY += 35
660
830
  }
661
831
 
662
832
  if (item.title) {
663
- elements.push(new paper.PointText({
833
+ const titleText = new paper.PointText({
664
834
  point: [itemX + padding, offsetY + 18],
665
835
  content: item.title,
666
836
  fontSize: 16,
667
837
  fillColor: new paper.Color(item.titleColor || '#ffffff'),
668
838
  justification: 'left',
669
- }))
839
+ })
840
+ elements.push(titleText)
841
+ if (project && project.activeLayer) {
842
+ project.activeLayer.addChild(titleText)
843
+ }
670
844
  offsetY += 22
671
845
  }
672
846
 
673
847
  if (item.description) {
674
- elements.push(new paper.PointText({
848
+ const descText = new paper.PointText({
675
849
  point: [itemX + padding, offsetY + 14],
676
850
  content: item.description,
677
851
  fontSize: 12,
678
852
  fillColor: new paper.Color(item.descColor || '#888888'),
679
853
  justification: 'left',
680
- }))
854
+ })
855
+ elements.push(descText)
856
+ if (project && project.activeLayer) {
857
+ project.activeLayer.addChild(descText)
858
+ }
681
859
  }
682
860
  }
683
861
 
@@ -685,7 +863,9 @@ function createFeatureGridComponent(project, canvas, {
685
863
  success: true,
686
864
  elements,
687
865
  type: 'featureGrid',
688
- rows: Math.ceil(items.length / columns),
866
+ width: columns * itemWidth + (columns - 1) * gap,
867
+ height: rows * itemHeight + Math.max(0, rows - 1) * gap,
868
+ rows,
689
869
  cols: columns,
690
870
  }
691
871
  }
@@ -917,12 +1097,22 @@ function createStatCardComponent(project, canvas, { x, y, width = 200, height =
917
1097
 
918
1098
  function createTagCloudComponent(project, canvas, { x, y, tags = [], fontSize = 14, padding = 12, gap = 10, maxWidth = 400 }) {
919
1099
  const elements = []
1100
+
1101
+ // 确保 tags 是数组
1102
+ if (!Array.isArray(tags) || tags.length === 0) {
1103
+ return { success: true, elements: [], type: 'tagCloud', height: 0 }
1104
+ }
1105
+
920
1106
  let currentX = x
921
1107
  let currentY = y
922
1108
  let rowHeight = 0
923
1109
 
924
1110
  for (const tag of tags) {
925
- const textWidth = tag.text.length * fontSize * 0.6
1111
+ // 确保 tag.text 是字符串
1112
+ const tagText = String(tag.text || '')
1113
+ if (!tagText) continue
1114
+
1115
+ const textWidth = tagText.length * fontSize * 0.6
926
1116
  const tagWidth = textWidth + padding * 2
927
1117
  const tagHeight = fontSize + padding * 2
928
1118
 
@@ -940,13 +1130,22 @@ function createTagCloudComponent(project, canvas, { x, y, tags = [], fontSize =
940
1130
  tagBg.fillColor = new paper.Color(tag.bgColor || '#e0e7ff')
941
1131
  elements.push({ type: 'rectangle', id: tagBg.id })
942
1132
 
943
- elements.push(new paper.PointText({
1133
+ // 添加到项目
1134
+ if (project && project.activeLayer) {
1135
+ project.activeLayer.addChild(tagBg)
1136
+ }
1137
+
1138
+ const tagTextEl = new paper.PointText({
944
1139
  point: [currentX + tagWidth / 2, currentY + tagHeight / 2 + fontSize / 3],
945
- content: tag.text,
1140
+ content: tagText,
946
1141
  fontSize: fontSize,
947
1142
  fillColor: new paper.Color(tag.color || '#4338ca'),
948
1143
  justification: 'center',
949
- }))
1144
+ })
1145
+ elements.push(tagTextEl)
1146
+ if (project && project.activeLayer) {
1147
+ project.activeLayer.addChild(tagTextEl)
1148
+ }
950
1149
 
951
1150
  currentX += tagWidth + gap
952
1151
  rowHeight = Math.max(rowHeight, tagHeight)
@@ -1018,24 +1217,34 @@ function createStepperComponent(project, canvas, { x, y, width = 600, steps = []
1018
1217
 
1019
1218
  function createTimelineComponent(project, canvas, { x, y, width = 500, items = [], lineColor = '#e2e8f0', dotColor = '#6366f1', dotSize = 16, gap = 60 }) {
1020
1219
  const elements = []
1220
+
1221
+ // 确保 items 是数组
1222
+ if (!Array.isArray(items) || items.length === 0) {
1223
+ return { success: true, elements: [], type: 'timeline', height: 0 }
1224
+ }
1225
+
1021
1226
  const centerX = x + 80
1022
1227
  const contentX = x + 120
1023
1228
 
1024
1229
  if (items.length > 1) {
1025
- elements.push({
1026
- type: 'line',
1027
- id: new paper.Path.Line({
1028
- from: [centerX, y + dotSize / 2],
1029
- to: [centerX, y + (items.length - 1) * gap + dotSize / 2],
1030
- strokeColor: new paper.Color(lineColor),
1031
- strokeWidth: 2,
1032
- }).id
1230
+ const mainLine = new paper.Path.Line({
1231
+ from: [centerX, y + dotSize / 2],
1232
+ to: [centerX, y + (items.length - 1) * gap + dotSize / 2],
1233
+ strokeColor: new paper.Color(lineColor),
1234
+ strokeWidth: 2,
1033
1235
  })
1236
+ elements.push({ type: 'line', id: mainLine.id })
1237
+ if (project && project.activeLayer) {
1238
+ project.activeLayer.addChild(mainLine)
1239
+ }
1034
1240
  }
1035
1241
 
1036
1242
  for (let i = 0; i < items.length; i++) {
1243
+ const item = items[i]
1244
+ if (!item) continue
1245
+
1037
1246
  const itemY = y + i * gap
1038
- const isActive = items[i].active !== false
1247
+ const isActive = item.active !== false
1039
1248
 
1040
1249
  const dot = new paper.Path.Circle({
1041
1250
  center: [centerX, itemY + dotSize / 2],
@@ -1043,42 +1252,65 @@ function createTimelineComponent(project, canvas, { x, y, width = 500, items = [
1043
1252
  })
1044
1253
  dot.fillColor = new paper.Color(isActive ? dotColor : lineColor)
1045
1254
  elements.push({ type: 'circle', id: dot.id })
1255
+ if (project && project.activeLayer) {
1256
+ project.activeLayer.addChild(dot)
1257
+ }
1046
1258
 
1047
- if (items[i].date) {
1048
- elements.push(new paper.PointText({
1259
+ if (item.date) {
1260
+ const dateText = new paper.PointText({
1049
1261
  point: [x + 10, itemY + dotSize / 2 + 5],
1050
- content: items[i].date,
1262
+ content: item.date,
1051
1263
  fontSize: 12,
1052
1264
  fillColor: new paper.Color('#94a3b8'),
1053
1265
  justification: 'left',
1054
- }))
1266
+ })
1267
+ elements.push(dateText)
1268
+ if (project && project.activeLayer) {
1269
+ project.activeLayer.addChild(dateText)
1270
+ }
1055
1271
  }
1056
1272
 
1057
- elements.push(new paper.PointText({
1273
+ const titleText = new paper.PointText({
1058
1274
  point: [contentX, itemY + dotSize / 2 + 5],
1059
- content: items[i].title || `Event ${i + 1}`,
1275
+ content: item.title || `Event ${i + 1}`,
1060
1276
  fontSize: 16,
1061
1277
  fillColor: new paper.Color(isActive ? '#1e293b' : '#94a3b8'),
1062
1278
  justification: 'left',
1063
- }))
1279
+ })
1280
+ elements.push(titleText)
1281
+ if (project && project.activeLayer) {
1282
+ project.activeLayer.addChild(titleText)
1283
+ }
1064
1284
 
1065
- if (items[i].description) {
1066
- elements.push(new paper.PointText({
1285
+ if (item.description) {
1286
+ const descText = new paper.PointText({
1067
1287
  point: [contentX, itemY + dotSize / 2 + 28],
1068
- content: items[i].description,
1288
+ content: item.description,
1069
1289
  fontSize: 13,
1070
1290
  fillColor: new paper.Color('#64748b'),
1071
1291
  justification: 'left',
1072
- }))
1292
+ })
1293
+ elements.push(descText)
1294
+ if (project && project.activeLayer) {
1295
+ project.activeLayer.addChild(descText)
1296
+ }
1073
1297
  }
1074
1298
  }
1075
1299
 
1076
1300
  return { success: true, elements, type: 'timeline', height: items.length * gap }
1077
1301
  }
1078
1302
 
1079
- function createListItemComponent(project, canvas, { x, y, width = 400, icon = '→', title, description, badge, badgeColor = '#6366f1', iconColor = '#6366f1', background = '#ffffff', borderColor = '#e5e7eb', height = 60, radius = 8 }) {
1303
+ function createListItemComponent(project, canvas, { x = 0, y = 0, width = 400, icon = '→', title, description, badge, badgeColor = '#6366f1', iconColor = '#6366f1', background = '#ffffff', borderColor = '#e5e7eb', height = 60, radius = 8 }) {
1080
1304
  const elements = []
1081
1305
 
1306
+ // 添加到项目活动层
1307
+ const addToProject = (item) => {
1308
+ if (project && project.activeLayer) {
1309
+ project.activeLayer.addChild(item)
1310
+ }
1311
+ elements.push(item)
1312
+ }
1313
+
1082
1314
  const bg = new paper.Path.Rectangle({
1083
1315
  point: [x, y],
1084
1316
  size: [width, height],
@@ -1087,32 +1319,35 @@ function createListItemComponent(project, canvas, { x, y, width = 400, icon = '
1087
1319
  bg.fillColor = new paper.Color(background)
1088
1320
  bg.strokeColor = new paper.Color(borderColor)
1089
1321
  bg.strokeWidth = 1
1090
- elements.push({ type: 'rectangle', id: bg.id })
1322
+ addToProject(bg)
1091
1323
 
1092
- elements.push(new paper.PointText({
1324
+ const iconText = new paper.PointText({
1093
1325
  point: [x + 15, y + height / 2 + 6],
1094
1326
  content: icon,
1095
1327
  fontSize: 20,
1096
1328
  fillColor: new paper.Color(iconColor),
1097
1329
  justification: 'center',
1098
- }))
1330
+ })
1331
+ addToProject(iconText)
1099
1332
 
1100
- elements.push(new paper.PointText({
1333
+ const titleText = new paper.PointText({
1101
1334
  point: [x + 50, y + height / 2 - 5],
1102
1335
  content: title || 'List Item',
1103
1336
  fontSize: 16,
1104
1337
  fillColor: new paper.Color('#1e293b'),
1105
1338
  justification: 'left',
1106
- }))
1339
+ })
1340
+ addToProject(titleText)
1107
1341
 
1108
1342
  if (description) {
1109
- elements.push(new paper.PointText({
1343
+ const descText = new paper.PointText({
1110
1344
  point: [x + 50, y + height / 2 + 15],
1111
1345
  content: description,
1112
1346
  fontSize: 12,
1113
1347
  fillColor: new paper.Color('#64748b'),
1114
1348
  justification: 'left',
1115
- }))
1349
+ })
1350
+ addToProject(descText)
1116
1351
  }
1117
1352
 
1118
1353
  if (badge) {
@@ -1120,22 +1355,22 @@ function createListItemComponent(project, canvas, { x, y, width = 400, icon = '
1120
1355
  const badgeX = x + width - badgeWidth - 15
1121
1356
  const badgeY = y + (height - 24) / 2
1122
1357
 
1123
- elements.push({
1124
- type: 'rectangle',
1125
- id: new paper.Path.Rectangle({
1126
- point: [badgeX, badgeY],
1127
- size: [badgeWidth, 24],
1128
- radius: 12,
1129
- }).id
1358
+ const badgeRect = new paper.Path.Rectangle({
1359
+ point: [badgeX, badgeY],
1360
+ size: [badgeWidth, 24],
1361
+ radius: 12,
1130
1362
  })
1363
+ badgeRect.fillColor = new paper.Color(badgeColor)
1364
+ addToProject(badgeRect)
1131
1365
 
1132
- elements.push(new paper.PointText({
1366
+ const badgeText = new paper.PointText({
1133
1367
  point: [badgeX + badgeWidth / 2, badgeY + 16],
1134
1368
  content: badge,
1135
1369
  fontSize: 12,
1136
1370
  fillColor: new paper.Color('#ffffff'),
1137
1371
  justification: 'center',
1138
- }))
1372
+ })
1373
+ addToProject(badgeText)
1139
1374
  }
1140
1375
 
1141
1376
  return { success: true, elements, type: 'listItem' }
@@ -1833,7 +2068,14 @@ function createTableComponent(project, canvas, {
1833
2068
  cellColor = '#333333', fontSize = 12, headerFontSize = 13, striped = true, stripeColor = '#fafafa'
1834
2069
  }) {
1835
2070
  const elements = []
1836
- if (columns.length === 0) return { success: true, elements, type: 'table' }
2071
+ // 确保 columns 是数组
2072
+ if (!Array.isArray(columns) || columns.length === 0) {
2073
+ return { success: true, elements, type: 'table' }
2074
+ }
2075
+ // 确保 rows 是数组
2076
+ if (!Array.isArray(rows)) {
2077
+ rows = []
2078
+ }
1837
2079
 
1838
2080
  const totalHeight = rowHeight * (rows.length + 1)
1839
2081