@ian2018cs/agenthub 0.1.30 → 0.1.31

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.
@@ -0,0 +1,937 @@
1
+ /**
2
+ * card-builder.js — 飞书卡片消息构建
3
+ *
4
+ * 所有卡片均通过 JSON 直接构建(interactive 类型),无需飞书卡片模板 ID。
5
+ */
6
+
7
+ const MODE_LABELS = {
8
+ default: '默认模式',
9
+ acceptEdits: '接受编辑',
10
+ plan: '计划模式',
11
+ bypassPermissions: 'YOLO 模式',
12
+ };
13
+
14
+ /**
15
+ * 隐藏用户项目目录前缀,只保留相对项目路径。
16
+ * 例:/data/user-projects/597ef057-.../test → test
17
+ */
18
+ function stripProjectPrefix(fullPath) {
19
+ if (!fullPath) return fullPath;
20
+ return fullPath.replace(/^.*\/user-projects\/[0-9a-f-]{36}\//, '') || fullPath;
21
+ }
22
+
23
+ // ─── 各场景卡片构建函数 ────────────────────────────────────────────────────────
24
+
25
+ /**
26
+ * 工具审批卡片(直接 JSON)
27
+ */
28
+ function buildToolApprovalCardDirect(toolName, toolInput, requestId) {
29
+ const inputSummary = truncate(
30
+ typeof toolInput === 'object' ? JSON.stringify(toolInput, null, 2) : String(toolInput || ''),
31
+ 400
32
+ );
33
+ const card = {
34
+ schema: '2.0',
35
+ config: { update_multi: true },
36
+ body: {
37
+ direction: 'vertical',
38
+ elements: [
39
+ {
40
+ tag: 'column_set',
41
+ flex_mode: 'stretch',
42
+ horizontal_spacing: '12px',
43
+ horizontal_align: 'left',
44
+ columns: [
45
+ {
46
+ tag: 'column',
47
+ width: 'weighted',
48
+ weight: 1,
49
+ vertical_spacing: '8px',
50
+ horizontal_align: 'left',
51
+ vertical_align: 'top',
52
+ elements: [
53
+ { tag: 'markdown', content: `**工具名称**\n${toolName || ''}` },
54
+ { tag: 'markdown', content: `**请求ID**\n${requestId || ''}` },
55
+ ],
56
+ },
57
+ {
58
+ tag: 'column',
59
+ width: 'weighted',
60
+ weight: 1,
61
+ vertical_spacing: '8px',
62
+ horizontal_align: 'left',
63
+ vertical_align: 'top',
64
+ elements: [
65
+ { tag: 'markdown', content: '**审批状态**\n待处理' },
66
+ ],
67
+ },
68
+ ],
69
+ margin: '0px 0px 0px 0px',
70
+ },
71
+ {
72
+ tag: 'markdown',
73
+ content: `**工具输入摘要**\n\`\`\`json\n${inputSummary}\n\`\`\``,
74
+ margin: '0px 0px 0px 0px',
75
+ },
76
+ {
77
+ tag: 'form',
78
+ name: 'approval_form',
79
+ direction: 'vertical',
80
+ horizontal_align: 'left',
81
+ vertical_align: 'top',
82
+ padding: '0px 0px 0px 0px',
83
+ margin: '0px 0px 0px 0px',
84
+ elements: [
85
+ {
86
+ tag: 'column_set',
87
+ flex_mode: 'stretch',
88
+ horizontal_spacing: '8px',
89
+ horizontal_align: 'left',
90
+ margin: '0px 0px 0px 0px',
91
+ columns: [
92
+ {
93
+ tag: 'column',
94
+ width: 'auto',
95
+ vertical_spacing: '8px',
96
+ horizontal_align: 'left',
97
+ vertical_align: 'top',
98
+ elements: [{
99
+ tag: 'button',
100
+ name: 'approve_btn',
101
+ form_action_type: 'submit',
102
+ type: 'primary_filled',
103
+ width: 'default',
104
+ margin: '4px 0px 4px 0px',
105
+ text: { tag: 'plain_text', content: '同意' },
106
+ behaviors: [{ type: 'callback', value: { action: 'tool_approve', decision: 'allow' } }],
107
+ }],
108
+ },
109
+ {
110
+ tag: 'column',
111
+ width: 'auto',
112
+ vertical_spacing: '8px',
113
+ horizontal_align: 'left',
114
+ vertical_align: 'top',
115
+ elements: [{
116
+ tag: 'button',
117
+ name: 'reject_btn',
118
+ form_action_type: 'submit',
119
+ type: 'danger',
120
+ width: 'default',
121
+ margin: '4px 0px 4px 0px',
122
+ text: { tag: 'plain_text', content: '拒绝' },
123
+ behaviors: [{ type: 'callback', value: { action: 'tool_approve', decision: 'deny' } }],
124
+ }],
125
+ },
126
+ {
127
+ tag: 'column',
128
+ width: 'weighted',
129
+ weight: 1,
130
+ vertical_spacing: '8px',
131
+ horizontal_align: 'left',
132
+ vertical_align: 'center',
133
+ elements: [{
134
+ tag: 'button',
135
+ name: 'remember_btn',
136
+ form_action_type: 'submit',
137
+ type: 'primary',
138
+ width: 'default',
139
+ margin: '4px 0px 4px 0px',
140
+ text: { tag: 'plain_text', content: '记住并允许' },
141
+ behaviors: [{ type: 'callback', value: { action: 'tool_approve', decision: 'allow_remember' } }],
142
+ }],
143
+ },
144
+ ],
145
+ },
146
+ ],
147
+ },
148
+ ],
149
+ },
150
+ header: {
151
+ title: { tag: 'plain_text', content: '工具审批请求' },
152
+ subtitle: { tag: 'plain_text', content: '' },
153
+ text_tag_list: [{ tag: 'text_tag', text: { tag: 'plain_text', content: '待审批' }, color: 'blue' }],
154
+ template: 'blue',
155
+ icon: { tag: 'standard_icon', token: 'robot_outlined' },
156
+ padding: '12px 8px 12px 8px',
157
+ },
158
+ };
159
+ return JSON.stringify(card);
160
+ }
161
+
162
+ /**
163
+ * 工具审批结果卡片(直接 JSON)
164
+ * 审批决定后更新原卡片用
165
+ */
166
+ function buildToolApprovalResultCardDirect(decision) {
167
+ const label =
168
+ decision === 'allow' ? '✅ 已允许' :
169
+ decision === 'deny' ? '❌ 已拒绝' :
170
+ decision === 'timeout' ? '⏰ 已超时' : '已处理';
171
+ const template =
172
+ decision === 'allow' || decision === 'allow_remember' ? 'green' :
173
+ decision === 'deny' ? 'red' : 'grey';
174
+ const card = {
175
+ schema: '2.0',
176
+ config: { update_multi: true },
177
+ body: {
178
+ direction: 'vertical',
179
+ elements: [
180
+ { tag: 'markdown', content: `**审批结果**\n${label}` },
181
+ ],
182
+ },
183
+ header: {
184
+ title: { tag: 'plain_text', content: '工具审批请求' },
185
+ subtitle: { tag: 'plain_text', content: '' },
186
+ template,
187
+ icon: { tag: 'standard_icon', token: 'robot_outlined' },
188
+ padding: '12px 8px 12px 8px',
189
+ },
190
+ };
191
+ return JSON.stringify(card);
192
+ }
193
+
194
+ /**
195
+ * 模式选择卡片(直接 JSON)
196
+ * 每个模式渲染为一行:左侧显示名称与说明,右侧显示切换按钮
197
+ * 当前模式按钮高亮且禁用,其余按钮可点击触发 set_mode callback
198
+ */
199
+ function buildModeSelectCardDirect(currentMode) {
200
+ const current = currentMode || 'default';
201
+ const MODES = [
202
+ { key: 'default', label: '默认模式', desc: '每次操作需手动确认' },
203
+ { key: 'acceptEdits', label: '接受编辑', desc: '自动接受文件编辑,其余需确认' },
204
+ { key: 'plan', label: '计划模式', desc: '只规划,不执行任何操作' },
205
+ { key: 'bypassPermissions', label: 'YOLO 模式', desc: '跳过所有确认,自动执行' },
206
+ ];
207
+
208
+ const rows = [];
209
+ MODES.forEach((m, i) => {
210
+ const isCurrent = m.key === current;
211
+ rows.push({
212
+ tag: 'column_set',
213
+ flex_mode: 'stretch',
214
+ horizontal_spacing: '12px',
215
+ horizontal_align: 'left',
216
+ margin: '4px 0px 4px 0px',
217
+ columns: [
218
+ {
219
+ tag: 'column',
220
+ width: 'weighted',
221
+ weight: 1,
222
+ vertical_spacing: '4px',
223
+ horizontal_align: 'left',
224
+ vertical_align: 'center',
225
+ elements: [{
226
+ tag: 'markdown',
227
+ content: `${isCurrent ? '✅ ' : ''}**${m.label}**\n${m.desc}`,
228
+ }],
229
+ },
230
+ {
231
+ tag: 'column',
232
+ width: 'auto',
233
+ vertical_spacing: '4px',
234
+ horizontal_align: 'right',
235
+ vertical_align: 'center',
236
+ elements: [{
237
+ tag: 'button',
238
+ type: isCurrent ? 'primary_filled' : 'default',
239
+ width: 'default',
240
+ margin: '0px 0px 0px 0px',
241
+ text: { tag: 'plain_text', content: isCurrent ? '当前' : '切换' },
242
+ disabled: isCurrent,
243
+ behaviors: [{ type: 'callback', value: { action: 'set_mode', mode: m.key } }],
244
+ }],
245
+ },
246
+ ],
247
+ });
248
+ if (i < MODES.length - 1) rows.push({ tag: 'hr' });
249
+ });
250
+
251
+ const card = {
252
+ schema: '2.0',
253
+ config: { update_multi: true },
254
+ body: { direction: 'vertical', elements: rows },
255
+ header: {
256
+ title: { tag: 'plain_text', content: '权限模式选择' },
257
+ subtitle: { tag: 'plain_text', content: `当前:${MODE_LABELS[current] || current}` },
258
+ template: 'blue',
259
+ icon: { tag: 'standard_icon', token: 'robot_outlined' },
260
+ padding: '12px 8px 12px 8px',
261
+ },
262
+ };
263
+ return JSON.stringify(card);
264
+ }
265
+
266
+ /**
267
+ * 状态卡片(直接 JSON)
268
+ * 显示当前项目、路径、会话 ID、权限模式,底部提供「新建会话」和「切换模式」快捷按钮
269
+ */
270
+ function buildStatusCardDirect(state) {
271
+ const { cwd, claude_session_id, permission_mode } = state || {};
272
+ const projectPath = cwd || '';
273
+ const displayPath = stripProjectPrefix(projectPath);
274
+ const projectName = displayPath ? displayPath.split('/').filter(Boolean).pop() || displayPath : '未设置';
275
+ const sessionId = claude_session_id ? claude_session_id.slice(0, 8) : '新建';
276
+ const mode = permission_mode || 'default';
277
+ const modeLabel = MODE_LABELS[mode] || mode;
278
+
279
+ const card = {
280
+ schema: '2.0',
281
+ config: { update_multi: true },
282
+ body: {
283
+ direction: 'vertical',
284
+ elements: [
285
+ {
286
+ tag: 'column_set',
287
+ flex_mode: 'stretch',
288
+ horizontal_spacing: '12px',
289
+ horizontal_align: 'left',
290
+ margin: '0px 0px 0px 0px',
291
+ columns: [
292
+ {
293
+ tag: 'column',
294
+ width: 'weighted',
295
+ weight: 1,
296
+ vertical_spacing: '8px',
297
+ horizontal_align: 'left',
298
+ vertical_align: 'top',
299
+ elements: [
300
+ { tag: 'markdown', content: `**项目**\n${projectName}` },
301
+ { tag: 'markdown', content: `**路径**\n\`${displayPath || '未设置'}\`` },
302
+ ],
303
+ },
304
+ {
305
+ tag: 'column',
306
+ width: 'weighted',
307
+ weight: 1,
308
+ vertical_spacing: '8px',
309
+ horizontal_align: 'left',
310
+ vertical_align: 'top',
311
+ elements: [
312
+ { tag: 'markdown', content: `**会话**\n\`${sessionId}\`` },
313
+ { tag: 'markdown', content: `**模式**\n${modeLabel}` },
314
+ ],
315
+ },
316
+ ],
317
+ },
318
+ { tag: 'hr' },
319
+ {
320
+ tag: 'column_set',
321
+ flex_mode: 'stretch',
322
+ horizontal_spacing: '8px',
323
+ horizontal_align: 'left',
324
+ margin: '0px 0px 0px 0px',
325
+ columns: [
326
+ {
327
+ tag: 'column',
328
+ width: 'auto',
329
+ vertical_spacing: '4px',
330
+ horizontal_align: 'left',
331
+ vertical_align: 'center',
332
+ elements: [{
333
+ tag: 'button',
334
+ type: 'default',
335
+ width: 'default',
336
+ text: { tag: 'plain_text', content: '新建会话' },
337
+ behaviors: [{ type: 'callback', value: { action: 'new_session' } }],
338
+ }],
339
+ },
340
+ {
341
+ tag: 'column',
342
+ width: 'auto',
343
+ vertical_spacing: '4px',
344
+ horizontal_align: 'left',
345
+ vertical_align: 'center',
346
+ elements: [{
347
+ tag: 'button',
348
+ type: 'default',
349
+ width: 'default',
350
+ text: { tag: 'plain_text', content: '切换模式' },
351
+ behaviors: [{ type: 'callback', value: { action: 'show_modes' } }],
352
+ }],
353
+ },
354
+ ],
355
+ },
356
+ ],
357
+ },
358
+ header: {
359
+ title: { tag: 'plain_text', content: '📊 当前状态' },
360
+ subtitle: { tag: 'plain_text', content: '' },
361
+ template: 'green',
362
+ padding: '12px 8px 12px 8px',
363
+ },
364
+ };
365
+ return JSON.stringify(card);
366
+ }
367
+
368
+ /**
369
+ * 列表卡片(直接 JSON)
370
+ * 使用下拉选择组件(select_static),用户从下拉框选取后点击「确认」提交。
371
+ *
372
+ * @param {string} title 卡片标题
373
+ * @param {Array} items 条目数组,每项 { label, sub?, isCurrent? }
374
+ * @param {string} actionType 'project' | 'session'
375
+ */
376
+ function buildListCardDirect({ title, items, actionType }) {
377
+ const options = (items || []).map((item, i) => ({
378
+ text: {
379
+ tag: 'plain_text',
380
+ content: item.isCurrent ? `✅ ${item.label}` : item.label,
381
+ },
382
+ value: String(i + 1),
383
+ }));
384
+
385
+ return JSON.stringify({
386
+ schema: '2.0',
387
+ config: { update_multi: true },
388
+ header: {
389
+ title: { tag: 'plain_text', content: title },
390
+ template: 'blue',
391
+ },
392
+ body: {
393
+ direction: 'vertical',
394
+ elements: [
395
+ {
396
+ tag: 'form',
397
+ name: 'list_form',
398
+ elements: [
399
+ {
400
+ tag: 'select_static',
401
+ placeholder: { tag: 'plain_text', content: '请选择…' },
402
+ options,
403
+ name: 'list_select',
404
+ width: 'fill',
405
+ },
406
+ {
407
+ tag: 'button',
408
+ name: 'list_submit_btn',
409
+ text: { tag: 'plain_text', content: '确认' },
410
+ type: 'primary',
411
+ form_action_type: 'submit',
412
+ behaviors: [{ type: 'callback', value: { action: 'list_select', type: actionType } }],
413
+ },
414
+ ],
415
+ },
416
+ ],
417
+ },
418
+ });
419
+ }
420
+
421
+ /**
422
+ * 删除列表卡片(直接 JSON)
423
+ * 与列表卡片布局一致,但使用红色主题和「确认删除」危险按钮。
424
+ *
425
+ * @param {string} title 卡片标题
426
+ * @param {Array} items 条目数组,每项 { label, sub?, isCurrent? }
427
+ * @param {string} actionType 'project' | 'session'
428
+ */
429
+ function buildDeleteListCardDirect({ title, items, actionType }) {
430
+ const options = (items || []).map((item, i) => ({
431
+ text: {
432
+ tag: 'plain_text',
433
+ content: item.isCurrent ? `⚠️ ${item.label}(当前)` : item.label,
434
+ },
435
+ value: String(i + 1),
436
+ }));
437
+
438
+ const deleteButtons = actionType === 'project'
439
+ ? [
440
+ {
441
+ tag: 'column',
442
+ width: 'auto',
443
+ elements: [{
444
+ tag: 'button',
445
+ name: 'delete_with_folder_btn',
446
+ text: { tag: 'plain_text', content: '含文件夹删除' },
447
+ type: 'danger',
448
+ form_action_type: 'submit',
449
+ behaviors: [{ type: 'callback', value: { action: 'delete_project', delete_folder: 'true' } }],
450
+ }],
451
+ },
452
+ {
453
+ tag: 'column',
454
+ width: 'auto',
455
+ elements: [{
456
+ tag: 'button',
457
+ name: 'delete_record_btn',
458
+ text: { tag: 'plain_text', content: '仅删除记录' },
459
+ type: 'primary',
460
+ form_action_type: 'submit',
461
+ behaviors: [{ type: 'callback', value: { action: 'delete_project', delete_folder: 'false' } }],
462
+ }],
463
+ },
464
+ ]
465
+ : [
466
+ {
467
+ tag: 'column',
468
+ width: 'auto',
469
+ elements: [{
470
+ tag: 'button',
471
+ name: 'delete_submit_btn',
472
+ text: { tag: 'plain_text', content: '确认删除' },
473
+ type: 'danger',
474
+ form_action_type: 'submit',
475
+ behaviors: [{ type: 'callback', value: { action: `delete_${actionType}` } }],
476
+ }],
477
+ },
478
+ ];
479
+
480
+ return JSON.stringify({
481
+ schema: '2.0',
482
+ config: { update_multi: true },
483
+ header: {
484
+ title: { tag: 'plain_text', content: title },
485
+ template: 'red',
486
+ },
487
+ body: {
488
+ direction: 'vertical',
489
+ elements: [
490
+ {
491
+ tag: 'markdown',
492
+ content: '⚠️ **删除操作不可恢复**,请谨慎选择。',
493
+ },
494
+ {
495
+ tag: 'form',
496
+ name: 'delete_form',
497
+ elements: [
498
+ {
499
+ tag: 'select_static',
500
+ placeholder: { tag: 'plain_text', content: '请选择…' },
501
+ options,
502
+ name: 'delete_select',
503
+ width: 'fill',
504
+ },
505
+ {
506
+ tag: 'column_set',
507
+ flex_mode: 'left',
508
+ horizontal_spacing: '8px',
509
+ columns: [
510
+ ...deleteButtons,
511
+ {
512
+ tag: 'column',
513
+ width: 'auto',
514
+ elements: [{
515
+ tag: 'button',
516
+ name: 'delete_cancel_btn',
517
+ text: { tag: 'plain_text', content: '取消' },
518
+ type: 'default',
519
+ form_action_type: 'submit',
520
+ behaviors: [{ type: 'callback', value: { action: 'delete_cancel' } }],
521
+ }],
522
+ },
523
+ ],
524
+ },
525
+ ],
526
+ },
527
+ ],
528
+ },
529
+ });
530
+ }
531
+
532
+ /**
533
+ * 删除结果卡片(删除成功或失败后更新原卡片用)
534
+ *
535
+ * @param {boolean} success
536
+ * @param {string} message 结果描述
537
+ */
538
+ function buildDeleteResultCard(success, message) {
539
+ return JSON.stringify({
540
+ schema: '2.0',
541
+ config: { update_multi: true },
542
+ header: {
543
+ title: { tag: 'plain_text', content: success ? '✅ 删除成功' : '❌ 删除失败' },
544
+ template: success ? 'green' : 'red',
545
+ padding: '12px 8px 12px 8px',
546
+ },
547
+ body: {
548
+ direction: 'vertical',
549
+ elements: [
550
+ { tag: 'markdown', content: message || '' },
551
+ ],
552
+ },
553
+ });
554
+ }
555
+
556
+ /**
557
+ * 帮助卡片(直接 JSON)
558
+ */
559
+ function buildHelpCardDirect(boundEmail, state) {
560
+ const { cwd, permission_mode } = state || {};
561
+ const projectName = cwd ? (cwd.split('/').filter(Boolean).pop() || cwd) : '未设置';
562
+ const modeLabel = MODE_LABELS[permission_mode] || '默认模式';
563
+
564
+ return JSON.stringify({
565
+ config: { wide_screen_mode: true },
566
+ header: {
567
+ title: { tag: 'plain_text', content: '🤖 AgentHub' },
568
+ template: 'blue',
569
+ },
570
+ elements: [
571
+ {
572
+ tag: 'div',
573
+ text: {
574
+ tag: 'lark_md',
575
+ content: `**已绑定账号**:${boundEmail || '未绑定'}\n**当前项目**:${projectName}\n**当前模式**:${modeLabel}`,
576
+ },
577
+ },
578
+ { tag: 'hr' },
579
+ {
580
+ tag: 'div',
581
+ text: {
582
+ tag: 'lark_md',
583
+ content: [
584
+ '**命令列表**',
585
+ '`/auth <token>` 绑定账号',
586
+ '`/unbind` 解除绑定',
587
+ '`/new` 新建会话',
588
+ '`/list` 会话列表',
589
+ '`/switch <id>` 切换会话',
590
+ '`/project` 当前状态',
591
+ '`/project list` 项目列表',
592
+ '`/project create` 创建新项目',
593
+ '`/project use <路径>` 切换项目',
594
+ '`/project delete` 删除项目',
595
+ '`/delete` 删除当前项目的会话',
596
+ '`/mode` 切换权限模式',
597
+ '`/stop` 中止当前会话',
598
+ '`/help` 显示此帮助',
599
+ ].join('\n'),
600
+ },
601
+ },
602
+ { tag: 'hr' },
603
+ {
604
+ tag: 'div',
605
+ text: {
606
+ tag: 'lark_md',
607
+ content: [
608
+ '**权限模式**',
609
+ ...Object.entries(MODE_LABELS).map(([k, v]) => `\`${k}\` ${v}`),
610
+ ].join('\n'),
611
+ },
612
+ },
613
+ ],
614
+ });
615
+ }
616
+
617
+ /**
618
+ * AskUserQuestion 交互卡片(直接 JSON,无需模板 ID)
619
+ *
620
+ * 每道题渲染为一个下拉选择组件(单选 select_static / 多选 multi_select_static)
621
+ * 底部提供「提交答案」和「跳过」按钮
622
+ *
623
+ * @param {Array} questions SDK 传入的 questions 数组
624
+ * @param {string} requestId 用于按钮 value 中的 request_id
625
+ * @returns {string} JSON 字符串(msg_type: 'interactive' 的 content)
626
+ */
627
+ function buildAskUserQuestionCard(questions, requestId) {
628
+ const formElements = [];
629
+
630
+ questions.forEach((q, qi) => {
631
+ const multiNote = q.multiSelect ? ' 〔可多选〕' : '';
632
+ formElements.push({
633
+ tag: 'markdown',
634
+ content: `**${qi + 1}. ${q.question}**${multiNote}`,
635
+ });
636
+
637
+ const options = (q.options || []).map(opt => ({
638
+ text: {
639
+ tag: 'plain_text',
640
+ content: opt.description ? `${opt.label} — ${opt.description}` : opt.label,
641
+ },
642
+ value: opt.label,
643
+ }));
644
+
645
+ if (q.multiSelect) {
646
+ formElements.push({
647
+ tag: 'multi_select_static',
648
+ placeholder: { tag: 'plain_text', content: '请选择(可多选)…' },
649
+ options,
650
+ name: `q_${qi}`,
651
+ });
652
+ } else {
653
+ formElements.push({
654
+ tag: 'select_static',
655
+ placeholder: { tag: 'plain_text', content: '请选择…' },
656
+ options,
657
+ name: `q_${qi}`,
658
+ });
659
+ }
660
+
661
+ if (qi < questions.length - 1) {
662
+ formElements.push({ tag: 'hr' });
663
+ }
664
+ });
665
+
666
+ formElements.push({
667
+ tag: 'column_set',
668
+ flex_mode: 'left',
669
+ horizontal_spacing: '8px',
670
+ columns: [
671
+ {
672
+ tag: 'column',
673
+ width: 'auto',
674
+ elements: [{
675
+ tag: 'button',
676
+ text: { tag: 'plain_text', content: '提交答案' },
677
+ type: 'primary',
678
+ form_action_type: 'submit',
679
+ behaviors: [{ type: 'callback', value: { action: 'ask_user_submit', request_id: requestId } }],
680
+ }],
681
+ },
682
+ {
683
+ tag: 'column',
684
+ width: 'auto',
685
+ elements: [{
686
+ tag: 'button',
687
+ text: { tag: 'plain_text', content: '跳过' },
688
+ type: 'default',
689
+ form_action_type: 'submit',
690
+ behaviors: [{ type: 'callback', value: { action: 'ask_user_skip', request_id: requestId } }],
691
+ }],
692
+ },
693
+ ],
694
+ });
695
+
696
+ return JSON.stringify({
697
+ schema: '2.0',
698
+ config: { update_multi: true },
699
+ header: {
700
+ title: { tag: 'plain_text', content: '🤔 Claude 需要您的回答' },
701
+ template: 'blue',
702
+ },
703
+ body: {
704
+ direction: 'vertical',
705
+ elements: [
706
+ {
707
+ tag: 'form',
708
+ name: 'ask_user_form',
709
+ elements: formElements,
710
+ },
711
+ ],
712
+ },
713
+ });
714
+ }
715
+
716
+ /**
717
+ * AskUserQuestion 结果卡片(提交或跳过后更新原卡片用)
718
+ *
719
+ * @param {Object} answers { [questionText]: answerLabel }
720
+ * @param {boolean} skipped
721
+ */
722
+ function buildAskUserQuestionResultCard(answers, skipped = false) {
723
+ const content = skipped
724
+ ? '已跳过本次问卷。'
725
+ : Object.entries(answers)
726
+ .map(([q, a]) => `• **${q}**:${a || '(未填写)'}`)
727
+ .join('\n');
728
+
729
+ return JSON.stringify({
730
+ config: { wide_screen_mode: true },
731
+ header: {
732
+ title: { tag: 'plain_text', content: skipped ? '⏭️ 已跳过' : '✅ 已提交回答' },
733
+ template: skipped ? 'grey' : 'green',
734
+ },
735
+ elements: [
736
+ { tag: 'div', text: { tag: 'lark_md', content } },
737
+ ],
738
+ });
739
+ }
740
+
741
+ /**
742
+ * 创建项目表单卡片(直接 JSON)
743
+ * 用户填写项目名称和可选的 GitHub 地址,点击确认后触发 create_project_submit 回调
744
+ */
745
+ function buildCreateProjectCardDirect() {
746
+ return JSON.stringify({
747
+ schema: '2.0',
748
+ config: { update_multi: true },
749
+ header: {
750
+ title: { tag: 'plain_text', content: '创建新项目' },
751
+ template: 'green',
752
+ icon: { tag: 'standard_icon', token: 'folder_outlined' },
753
+ padding: '12px 8px 12px 8px',
754
+ },
755
+ body: {
756
+ direction: 'vertical',
757
+ elements: [
758
+ {
759
+ tag: 'form',
760
+ name: 'create_project_form',
761
+ elements: [
762
+ { tag: 'markdown', content: '**项目名称**(必填,仅支持字母、数字、`-`、`_`)' },
763
+ {
764
+ tag: 'input',
765
+ name: 'project_name',
766
+ placeholder: { tag: 'plain_text', content: '如:my-project' },
767
+ max_length: 100,
768
+ },
769
+ { tag: 'hr' },
770
+ { tag: 'markdown', content: '**GitHub 仓库地址**(选填,自动克隆公开仓库)' },
771
+ {
772
+ tag: 'input',
773
+ name: 'github_url',
774
+ placeholder: { tag: 'plain_text', content: '如:https://github.com/user/repo.git' },
775
+ max_length: 500,
776
+ },
777
+ { tag: 'hr' },
778
+ {
779
+ tag: 'column_set',
780
+ flex_mode: 'left',
781
+ horizontal_spacing: '8px',
782
+ columns: [
783
+ {
784
+ tag: 'column',
785
+ width: 'auto',
786
+ elements: [{
787
+ tag: 'button',
788
+ name: 'create_submit_btn',
789
+ text: { tag: 'plain_text', content: '确认创建' },
790
+ type: 'primary',
791
+ form_action_type: 'submit',
792
+ behaviors: [{ type: 'callback', value: { action: 'create_project_submit' } }],
793
+ }],
794
+ },
795
+ {
796
+ tag: 'column',
797
+ width: 'auto',
798
+ elements: [{
799
+ tag: 'button',
800
+ name: 'create_cancel_btn',
801
+ text: { tag: 'plain_text', content: '取消' },
802
+ type: 'default',
803
+ form_action_type: 'submit',
804
+ behaviors: [{ type: 'callback', value: { action: 'create_project_cancel' } }],
805
+ }],
806
+ },
807
+ ],
808
+ },
809
+ ],
810
+ },
811
+ ],
812
+ },
813
+ });
814
+ }
815
+
816
+ /**
817
+ * 创建项目结果卡片(创建成功或失败后更新原卡片用)
818
+ *
819
+ * @param {boolean} success
820
+ * @param {string} message 结果描述
821
+ */
822
+ function buildCreateProjectResultCard(success, message) {
823
+ return JSON.stringify({
824
+ schema: '2.0',
825
+ config: { update_multi: true },
826
+ header: {
827
+ title: { tag: 'plain_text', content: success ? '✅ 项目创建成功' : '❌ 创建失败' },
828
+ template: success ? 'green' : 'red',
829
+ padding: '12px 8px 12px 8px',
830
+ },
831
+ body: {
832
+ direction: 'vertical',
833
+ elements: [
834
+ { tag: 'markdown', content: message || '' },
835
+ ],
836
+ },
837
+ });
838
+ }
839
+
840
+ // ─── 工具函数 ─────────────────────────────────────────────────────────────────
841
+
842
+ function truncate(str, maxLen) {
843
+ if (!str) return '';
844
+ return str.length <= maxLen ? str : str.slice(0, maxLen) + '…';
845
+ }
846
+
847
+ /**
848
+ * 判断消息是否含有 Markdown 格式(决定是否使用富文本卡片)
849
+ */
850
+ const MARKDOWN_INDICATORS = ['```', '**', '~~', '\n- ', '\n* ', '\n1. ', '\n# ', '---'];
851
+
852
+ function containsMarkdown(text) {
853
+ return MARKDOWN_INDICATORS.some(ind => text.includes(ind));
854
+ }
855
+
856
+ /**
857
+ * 飞书卡片 Markdown 适配:
858
+ * # 标题 → **标题**(飞书不支持 # 标题)
859
+ * > 引用 → 缩进(飞书不支持 > 引用)
860
+ */
861
+ function adaptMarkdown(text) {
862
+ const lines = text.split('\n');
863
+ let inCodeBlock = false;
864
+ return lines.map(line => {
865
+ if (line.trimStart().startsWith('```')) { inCodeBlock = !inCodeBlock; return line; }
866
+ if (inCodeBlock) return line;
867
+ for (let lvl = 6; lvl >= 1; lvl--) {
868
+ const prefix = '#'.repeat(lvl) + ' ';
869
+ if (line.startsWith(prefix)) return `**${line.slice(prefix.length)}**`;
870
+ }
871
+ if (line.startsWith('> ')) return ' ' + line.slice(2);
872
+ return line;
873
+ }).join('\n');
874
+ }
875
+
876
+ /**
877
+ * 构建普通文本消息或 Markdown 富文本卡片消息体
878
+ * @returns {{ msgType: string, content: string }}
879
+ */
880
+ function buildTextOrMarkdownMessage(text) {
881
+ if (!containsMarkdown(text)) {
882
+ return {
883
+ msgType: 'text',
884
+ content: JSON.stringify({ text }),
885
+ };
886
+ }
887
+ const adapted = adaptMarkdown(text);
888
+ return {
889
+ msgType: 'interactive',
890
+ content: JSON.stringify({
891
+ config: { wide_screen_mode: true },
892
+ elements: [{
893
+ tag: 'div',
894
+ text: { tag: 'lark_md', content: adapted },
895
+ }],
896
+ }),
897
+ };
898
+ }
899
+
900
+ /**
901
+ * 长消息分块(飞书单消息建议不超过 4000 字符)
902
+ */
903
+ function splitMessage(text, maxLen = 4000) {
904
+ if (text.length <= maxLen) return [text];
905
+ const chunks = [];
906
+ let remaining = text;
907
+ while (remaining.length > 0) {
908
+ let end = Math.min(maxLen, remaining.length);
909
+ if (end < remaining.length) {
910
+ const lastNewline = remaining.lastIndexOf('\n', end);
911
+ if (lastNewline > maxLen * 0.5) end = lastNewline + 1;
912
+ }
913
+ chunks.push(remaining.slice(0, end));
914
+ remaining = remaining.slice(end);
915
+ }
916
+ return chunks;
917
+ }
918
+
919
+ export {
920
+ MODE_LABELS,
921
+ stripProjectPrefix,
922
+ buildToolApprovalCardDirect,
923
+ buildToolApprovalResultCardDirect,
924
+ buildModeSelectCardDirect,
925
+ buildStatusCardDirect,
926
+ buildListCardDirect,
927
+ buildDeleteListCardDirect,
928
+ buildDeleteResultCard,
929
+ buildHelpCardDirect,
930
+ buildAskUserQuestionCard,
931
+ buildAskUserQuestionResultCard,
932
+ buildCreateProjectCardDirect,
933
+ buildCreateProjectResultCard,
934
+ buildTextOrMarkdownMessage,
935
+ splitMessage,
936
+ truncate,
937
+ };