@wecom/wecom-openclaw-plugin 2026.3.25-beta.2 → 2026.3.25-beta.3
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/dist/index.cjs.js +302 -7
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +302 -7
- package/dist/index.esm.js.map +1 -1
- package/package.json +1 -1
- package/skills/wecom-send-template-card/SKILL.md +102 -136
- package/skills/wecom-send-template-card/references/api-template-card-types.md +291 -574
package/dist/index.cjs.js
CHANGED
|
@@ -1220,24 +1220,31 @@ const MODE_ALIASES = {
|
|
|
1220
1220
|
};
|
|
1221
1221
|
/**
|
|
1222
1222
|
* 修正 checkbox.mode:
|
|
1223
|
-
* -
|
|
1223
|
+
* - 0 → 单选,1 → 多选,仅允许这两个值
|
|
1224
1224
|
* - 字符串数字 "0"/"1" → 整数
|
|
1225
1225
|
* - 语义别名 "multi"/"single" 等 → 对应整数
|
|
1226
|
+
* - 其他正整数(如 2)→ clamp 到 1(多选)
|
|
1226
1227
|
* - 无法识别 → 删除(让服务端使用默认值 0)
|
|
1227
1228
|
*/
|
|
1228
1229
|
function coerceCheckboxMode(value) {
|
|
1230
|
+
let num;
|
|
1229
1231
|
if (typeof value === "number" && Number.isFinite(value)) {
|
|
1230
|
-
|
|
1232
|
+
num = Math.round(value);
|
|
1231
1233
|
}
|
|
1232
|
-
if (typeof value === "string") {
|
|
1234
|
+
else if (typeof value === "string") {
|
|
1233
1235
|
const trimmed = value.trim().toLowerCase();
|
|
1234
1236
|
if (trimmed in MODE_ALIASES)
|
|
1235
1237
|
return MODE_ALIASES[trimmed];
|
|
1236
|
-
const
|
|
1237
|
-
if (!Number.isNaN(
|
|
1238
|
-
|
|
1238
|
+
const parsed = Number(trimmed);
|
|
1239
|
+
if (!Number.isNaN(parsed))
|
|
1240
|
+
num = Math.round(parsed);
|
|
1239
1241
|
}
|
|
1240
|
-
|
|
1242
|
+
if (num === undefined)
|
|
1243
|
+
return undefined;
|
|
1244
|
+
// mode 只允许 0(单选)或 1(多选),超出范围 clamp
|
|
1245
|
+
if (num <= 0)
|
|
1246
|
+
return 0;
|
|
1247
|
+
return 1;
|
|
1241
1248
|
}
|
|
1242
1249
|
/**
|
|
1243
1250
|
* 对 LLM 生成的模板卡片 JSON 做字段类型修正,确保符合企业微信 API 的类型要求。
|
|
@@ -1396,6 +1403,290 @@ function normalizeTemplateCardFields(card, log) {
|
|
|
1396
1403
|
}
|
|
1397
1404
|
return card;
|
|
1398
1405
|
}
|
|
1406
|
+
// ============================================================================
|
|
1407
|
+
// 必填字段校验与自动补全
|
|
1408
|
+
// ============================================================================
|
|
1409
|
+
/** task_id 合法字符:数字、字母、_-@ */
|
|
1410
|
+
const TASK_ID_RE = /^[a-zA-Z0-9_\-@]+$/;
|
|
1411
|
+
/**
|
|
1412
|
+
* 生成一个合法的 task_id。
|
|
1413
|
+
* 格式:task_{cardType}_{timestamp}_{随机4位},确保唯一且符合 API 要求。
|
|
1414
|
+
*/
|
|
1415
|
+
function generateTaskId(cardType) {
|
|
1416
|
+
const rand = Math.random().toString(36).slice(2, 6);
|
|
1417
|
+
return `task_${cardType}_${Date.now()}_${rand}`;
|
|
1418
|
+
}
|
|
1419
|
+
/**
|
|
1420
|
+
* 校验并补全模板卡片的必填字段。
|
|
1421
|
+
*
|
|
1422
|
+
* 在 normalizeTemplateCardFields(类型修正)之后调用,确保卡片结构满足企业微信 API 的必填要求。
|
|
1423
|
+
*
|
|
1424
|
+
* 补全策略:
|
|
1425
|
+
* - task_id:所有卡片统一自动补全(交互型 API 必填,通知型插件也需要用于缓存回写)
|
|
1426
|
+
* - main_title:除 text_notice 外的 4 种卡片 API 必填,自动补 { title: "通知" }
|
|
1427
|
+
* text_notice 要求 main_title.title 与 sub_title_text 至少填一个,缺两个时补 sub_title_text
|
|
1428
|
+
* - card_action:text_notice / news_notice API 必填,自动补 { type: 1, url: "https://work.weixin.qq.com" }
|
|
1429
|
+
* - checkbox:vote_interaction API 必填,无法凭空补全,仅记告警
|
|
1430
|
+
* - submit_button:vote_interaction / multiple_interaction API 必填,自动补 { text: "提交", key: "submit_default" }
|
|
1431
|
+
* - button_list:button_interaction API 必填,无法凭空补全,仅记告警
|
|
1432
|
+
* - select_list:multiple_interaction API 必填,无法凭空补全,仅记告警
|
|
1433
|
+
*/
|
|
1434
|
+
function validateAndFixRequiredFields(card, log) {
|
|
1435
|
+
const cardType = card.card_type;
|
|
1436
|
+
const fixes = [];
|
|
1437
|
+
const warnings = [];
|
|
1438
|
+
// ── task_id(所有卡片) ────────────────────────────────────────────
|
|
1439
|
+
if (!card.task_id || typeof card.task_id !== "string" || !card.task_id.trim()) {
|
|
1440
|
+
const generated = generateTaskId(cardType);
|
|
1441
|
+
fixes.push(`task_id: (missing) → "${generated}"`);
|
|
1442
|
+
card.task_id = generated;
|
|
1443
|
+
}
|
|
1444
|
+
else {
|
|
1445
|
+
// 校验合法性:只能由数字、字母和 "_-@" 组成
|
|
1446
|
+
const tid = card.task_id;
|
|
1447
|
+
if (!TASK_ID_RE.test(tid)) {
|
|
1448
|
+
// 尝试清理非法字符
|
|
1449
|
+
const cleaned = tid.replace(/[^a-zA-Z0-9_\-@]/g, "_").slice(0, 128);
|
|
1450
|
+
if (cleaned && TASK_ID_RE.test(cleaned)) {
|
|
1451
|
+
fixes.push(`task_id: "${tid}" → "${cleaned}" (cleaned illegal chars)`);
|
|
1452
|
+
card.task_id = cleaned;
|
|
1453
|
+
}
|
|
1454
|
+
else {
|
|
1455
|
+
const generated = generateTaskId(cardType);
|
|
1456
|
+
fixes.push(`task_id: "${tid}" → "${generated}" (replaced, too many illegal chars)`);
|
|
1457
|
+
card.task_id = generated;
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
// ── main_title ────────────────────────────────────────────────────────
|
|
1462
|
+
const mainTitle = card.main_title;
|
|
1463
|
+
const hasMainTitle = mainTitle && typeof mainTitle === "object" &&
|
|
1464
|
+
(typeof mainTitle.title === "string" && mainTitle.title.trim());
|
|
1465
|
+
const hasSubTitleText = typeof card.sub_title_text === "string" && card.sub_title_text.trim();
|
|
1466
|
+
switch (cardType) {
|
|
1467
|
+
case "text_notice":
|
|
1468
|
+
// text_notice: main_title.title 和 sub_title_text 至少一个
|
|
1469
|
+
if (!hasMainTitle && !hasSubTitleText) {
|
|
1470
|
+
card.sub_title_text = card.sub_title_text || "通知";
|
|
1471
|
+
fixes.push(`sub_title_text: (missing, no main_title either) → fallback "通知"`);
|
|
1472
|
+
}
|
|
1473
|
+
break;
|
|
1474
|
+
case "news_notice":
|
|
1475
|
+
case "button_interaction":
|
|
1476
|
+
case "vote_interaction":
|
|
1477
|
+
case "multiple_interaction":
|
|
1478
|
+
// 这四种 main_title 为必填
|
|
1479
|
+
if (!mainTitle || typeof mainTitle !== "object") {
|
|
1480
|
+
card.main_title = { title: "通知" };
|
|
1481
|
+
fixes.push(`main_title: (missing) → { title: "通知" }`);
|
|
1482
|
+
}
|
|
1483
|
+
else if (!hasMainTitle) {
|
|
1484
|
+
mainTitle.title = "通知";
|
|
1485
|
+
fixes.push(`main_title.title: (empty) → "通知"`);
|
|
1486
|
+
}
|
|
1487
|
+
break;
|
|
1488
|
+
}
|
|
1489
|
+
// ── card_action(text_notice / news_notice 必填) ──────────────────
|
|
1490
|
+
if (cardType === "text_notice" || cardType === "news_notice") {
|
|
1491
|
+
if (!card.card_action || typeof card.card_action !== "object") {
|
|
1492
|
+
card.card_action = { type: 1, url: "https://work.weixin.qq.com" };
|
|
1493
|
+
fixes.push(`card_action: (missing) → { type: 1, url: "https://work.weixin.qq.com" }`);
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
// ── submit_button(vote_interaction / multiple_interaction 必填) ──
|
|
1497
|
+
if (cardType === "vote_interaction" || cardType === "multiple_interaction") {
|
|
1498
|
+
if (!card.submit_button || typeof card.submit_button !== "object") {
|
|
1499
|
+
card.submit_button = { text: "提交", key: `submit_${cardType}_${Date.now()}` };
|
|
1500
|
+
fixes.push(`submit_button: (missing) → auto-generated`);
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
// ── 核心业务字段(无法凭空补全,仅告警) ────────────────────────────
|
|
1504
|
+
if (cardType === "button_interaction") {
|
|
1505
|
+
if (!Array.isArray(card.button_list) || card.button_list.length === 0) {
|
|
1506
|
+
warnings.push(`button_list is missing or empty (required for button_interaction)`);
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
if (cardType === "vote_interaction") {
|
|
1510
|
+
if (!card.checkbox || typeof card.checkbox !== "object") {
|
|
1511
|
+
warnings.push(`checkbox is missing (required for vote_interaction)`);
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
if (cardType === "multiple_interaction") {
|
|
1515
|
+
if (!Array.isArray(card.select_list) || card.select_list.length === 0) {
|
|
1516
|
+
warnings.push(`select_list is missing or empty (required for multiple_interaction)`);
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
if (fixes.length > 0) {
|
|
1520
|
+
log?.(`[template-card-parser] validateAndFixRequiredFields: ${fixes.length} fix(es): ${fixes.join("; ")}`);
|
|
1521
|
+
}
|
|
1522
|
+
if (warnings.length > 0) {
|
|
1523
|
+
log?.(`[template-card-parser] validateAndFixRequiredFields: ${warnings.length} warning(s): ${warnings.join("; ")}`);
|
|
1524
|
+
}
|
|
1525
|
+
return card;
|
|
1526
|
+
}
|
|
1527
|
+
// ============================================================================
|
|
1528
|
+
// 简化格式 → 企微 API 格式转换(vote_interaction / multiple_interaction)
|
|
1529
|
+
// ============================================================================
|
|
1530
|
+
/**
|
|
1531
|
+
* 生成唯一的 question_key / submit_button.key。
|
|
1532
|
+
*/
|
|
1533
|
+
function generateKey(prefix) {
|
|
1534
|
+
const rand = Math.random().toString(36).slice(2, 6);
|
|
1535
|
+
return `${prefix}_${Date.now()}_${rand}`;
|
|
1536
|
+
}
|
|
1537
|
+
/**
|
|
1538
|
+
* 将 vote_interaction 的简化格式转换为企微 API 格式。
|
|
1539
|
+
*
|
|
1540
|
+
* 简化格式字段:
|
|
1541
|
+
* title → main_title.title
|
|
1542
|
+
* description → main_title.desc
|
|
1543
|
+
* options → checkbox.option_list(每个 {id, text} 直接透传)
|
|
1544
|
+
* mode → checkbox.mode(0=单选 1=多选)
|
|
1545
|
+
* submit_text → submit_button.text
|
|
1546
|
+
*
|
|
1547
|
+
* 代码自动生成:checkbox.question_key, submit_button.key
|
|
1548
|
+
*
|
|
1549
|
+
* 如果 LLM 已输出了合法的 API 原始格式(有 checkbox.option_list),则跳过转换直接透传。
|
|
1550
|
+
*/
|
|
1551
|
+
function transformVoteInteraction(card, log) {
|
|
1552
|
+
// 防御性:如果已经是合法 API 格式,跳过
|
|
1553
|
+
const existingCheckbox = card.checkbox;
|
|
1554
|
+
if (existingCheckbox && typeof existingCheckbox === "object" && Array.isArray(existingCheckbox.option_list)) {
|
|
1555
|
+
log?.(`[template-card-parser] transformVoteInteraction: already has checkbox.option_list, skipping transform`);
|
|
1556
|
+
return card;
|
|
1557
|
+
}
|
|
1558
|
+
// 提取 options(简化格式的核心字段)
|
|
1559
|
+
const options = card.options;
|
|
1560
|
+
if (!Array.isArray(options) || options.length === 0) {
|
|
1561
|
+
log?.(`[template-card-parser] transformVoteInteraction: no "options" array found, skipping transform`);
|
|
1562
|
+
return card;
|
|
1563
|
+
}
|
|
1564
|
+
log?.(`[template-card-parser] transformVoteInteraction: transforming simplified format → API format`);
|
|
1565
|
+
log?.(`[template-card-parser] transformVoteInteraction: input=${JSON.stringify(card)}`);
|
|
1566
|
+
// ── 构建 main_title ──
|
|
1567
|
+
const title = card.title;
|
|
1568
|
+
const description = card.description;
|
|
1569
|
+
if (title || description) {
|
|
1570
|
+
card.main_title = {
|
|
1571
|
+
...(title ? { title } : {}),
|
|
1572
|
+
...(description ? { desc: description } : {}),
|
|
1573
|
+
};
|
|
1574
|
+
delete card.title;
|
|
1575
|
+
delete card.description;
|
|
1576
|
+
}
|
|
1577
|
+
// ── 构建 checkbox(最多 20 个选项) ──
|
|
1578
|
+
const mode = coerceCheckboxMode(card.mode) ?? 0;
|
|
1579
|
+
const questionKey = generateKey("vote");
|
|
1580
|
+
const clampedOptions = options.slice(0, 20);
|
|
1581
|
+
if (options.length > 20) {
|
|
1582
|
+
log?.(`[template-card-parser] transformVoteInteraction: options count ${options.length} exceeds max 20, clamped to 20`);
|
|
1583
|
+
}
|
|
1584
|
+
card.checkbox = {
|
|
1585
|
+
question_key: questionKey,
|
|
1586
|
+
mode,
|
|
1587
|
+
option_list: clampedOptions.map((opt) => ({
|
|
1588
|
+
id: String(opt.id ?? opt.value ?? `opt_${Math.random().toString(36).slice(2, 6)}`),
|
|
1589
|
+
text: String(opt.text ?? opt.label ?? opt.name ?? ""),
|
|
1590
|
+
})),
|
|
1591
|
+
};
|
|
1592
|
+
delete card.options;
|
|
1593
|
+
delete card.mode;
|
|
1594
|
+
// ── 构建 submit_button ──
|
|
1595
|
+
const submitText = card.submit_text || "提交";
|
|
1596
|
+
card.submit_button = {
|
|
1597
|
+
text: submitText,
|
|
1598
|
+
key: generateKey("submit_vote"),
|
|
1599
|
+
};
|
|
1600
|
+
delete card.submit_text;
|
|
1601
|
+
// ── 清理 LLM 可能杜撰的无效字段 ──
|
|
1602
|
+
delete card.vote_question;
|
|
1603
|
+
delete card.vote_option;
|
|
1604
|
+
delete card.vote_options;
|
|
1605
|
+
log?.(`[template-card-parser] transformVoteInteraction: output=${JSON.stringify(card)}`);
|
|
1606
|
+
return card;
|
|
1607
|
+
}
|
|
1608
|
+
/**
|
|
1609
|
+
* 将 multiple_interaction 的简化格式转换为企微 API 格式。
|
|
1610
|
+
*
|
|
1611
|
+
* 简化格式字段:
|
|
1612
|
+
* title → main_title.title
|
|
1613
|
+
* description → main_title.desc
|
|
1614
|
+
* selectors → select_list(每个 {title, options: [{id, text}]} → {question_key, title, option_list})
|
|
1615
|
+
* submit_text → submit_button.text
|
|
1616
|
+
*
|
|
1617
|
+
* 代码自动生成:select_list[].question_key, submit_button.key
|
|
1618
|
+
*
|
|
1619
|
+
* 如果 LLM 已输出了合法的 API 原始格式(有 select_list[0].option_list),则跳过转换直接透传。
|
|
1620
|
+
*/
|
|
1621
|
+
function transformMultipleInteraction(card, log) {
|
|
1622
|
+
// 防御性:如果已经是合法 API 格式,跳过
|
|
1623
|
+
const existingSelectList = card.select_list;
|
|
1624
|
+
if (Array.isArray(existingSelectList) &&
|
|
1625
|
+
existingSelectList.length > 0 &&
|
|
1626
|
+
Array.isArray(existingSelectList[0]?.option_list)) {
|
|
1627
|
+
log?.(`[template-card-parser] transformMultipleInteraction: already has select_list[].option_list, skipping transform`);
|
|
1628
|
+
return card;
|
|
1629
|
+
}
|
|
1630
|
+
// 提取 selectors(简化格式的核心字段)
|
|
1631
|
+
const selectors = card.selectors;
|
|
1632
|
+
if (!Array.isArray(selectors) || selectors.length === 0) {
|
|
1633
|
+
log?.(`[template-card-parser] transformMultipleInteraction: no "selectors" array found, skipping transform`);
|
|
1634
|
+
return card;
|
|
1635
|
+
}
|
|
1636
|
+
log?.(`[template-card-parser] transformMultipleInteraction: transforming simplified format → API format`);
|
|
1637
|
+
log?.(`[template-card-parser] transformMultipleInteraction: input=${JSON.stringify(card)}`);
|
|
1638
|
+
// ── 构建 main_title ──
|
|
1639
|
+
const title = card.title;
|
|
1640
|
+
const description = card.description;
|
|
1641
|
+
if (title || description) {
|
|
1642
|
+
card.main_title = {
|
|
1643
|
+
...(title ? { title } : {}),
|
|
1644
|
+
...(description ? { desc: description } : {}),
|
|
1645
|
+
};
|
|
1646
|
+
delete card.title;
|
|
1647
|
+
delete card.description;
|
|
1648
|
+
}
|
|
1649
|
+
// ── 构建 select_list(最多 3 个选择器,每个最多 10 个选项) ──
|
|
1650
|
+
const clampedSelectors = selectors.slice(0, 3);
|
|
1651
|
+
if (selectors.length > 3) {
|
|
1652
|
+
log?.(`[template-card-parser] transformMultipleInteraction: selectors count ${selectors.length} exceeds max 3, clamped to 3`);
|
|
1653
|
+
}
|
|
1654
|
+
card.select_list = clampedSelectors.map((sel, idx) => {
|
|
1655
|
+
const selectorOptions = (sel.options ?? []).slice(0, 10);
|
|
1656
|
+
return {
|
|
1657
|
+
question_key: generateKey(`sel_${idx}`),
|
|
1658
|
+
title: String(sel.title ?? sel.label ?? `选择${idx + 1}`),
|
|
1659
|
+
option_list: selectorOptions.map((opt) => ({
|
|
1660
|
+
id: String(opt.id ?? opt.value ?? `opt_${Math.random().toString(36).slice(2, 6)}`),
|
|
1661
|
+
text: String(opt.text ?? opt.label ?? opt.name ?? ""),
|
|
1662
|
+
})),
|
|
1663
|
+
};
|
|
1664
|
+
});
|
|
1665
|
+
delete card.selectors;
|
|
1666
|
+
// ── 构建 submit_button ──
|
|
1667
|
+
const submitText = card.submit_text || "提交";
|
|
1668
|
+
card.submit_button = {
|
|
1669
|
+
text: submitText,
|
|
1670
|
+
key: generateKey("submit_multi"),
|
|
1671
|
+
};
|
|
1672
|
+
delete card.submit_text;
|
|
1673
|
+
log?.(`[template-card-parser] transformMultipleInteraction: output=${JSON.stringify(card)}`);
|
|
1674
|
+
return card;
|
|
1675
|
+
}
|
|
1676
|
+
/**
|
|
1677
|
+
* 对 vote_interaction / multiple_interaction 执行简化格式转换。
|
|
1678
|
+
* 其他 card_type 直接跳过。
|
|
1679
|
+
*/
|
|
1680
|
+
function transformSimplifiedCard(card, log) {
|
|
1681
|
+
const cardType = card.card_type;
|
|
1682
|
+
if (cardType === "vote_interaction") {
|
|
1683
|
+
return transformVoteInteraction(card, log);
|
|
1684
|
+
}
|
|
1685
|
+
if (cardType === "multiple_interaction") {
|
|
1686
|
+
return transformMultipleInteraction(card, log);
|
|
1687
|
+
}
|
|
1688
|
+
return card;
|
|
1689
|
+
}
|
|
1399
1690
|
/**
|
|
1400
1691
|
* 匹配 markdown 代码块的正则表达式
|
|
1401
1692
|
* 支持 ```json ... ``` 和 ``` ... ``` 两种格式
|
|
@@ -1455,8 +1746,12 @@ function extractTemplateCards(text, log) {
|
|
|
1455
1746
|
continue;
|
|
1456
1747
|
}
|
|
1457
1748
|
log?.(`[template-card-parser] Code block #${blockIndex} is valid template card, card_type="${cardType}"`);
|
|
1749
|
+
// vote_interaction / multiple_interaction:简化格式 → API 格式转换
|
|
1750
|
+
transformSimplifiedCard(parsed, log);
|
|
1458
1751
|
// 修正 LLM 可能输出的错误字段类型(如 checkbox.mode: "multi" → 1)
|
|
1459
1752
|
normalizeTemplateCardFields(parsed, log);
|
|
1753
|
+
// 校验并补全必填字段(如缺失的 task_id、main_title、card_action)
|
|
1754
|
+
validateAndFixRequiredFields(parsed, log);
|
|
1460
1755
|
// 合法的模板卡片,收集并标记移除
|
|
1461
1756
|
cards.push({
|
|
1462
1757
|
cardJson: parsed,
|