weaver-work-cli 0.1.32 → 0.1.33
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/README.md +2 -2
- package/dist/chunks/{archive-UMGX72K6.js → archive-FVFY6GZN.js} +2 -2
- package/dist/chunks/{archive-oa-N2IYBLF3.js → archive-oa-NW3OD4UV.js} +2 -2
- package/dist/chunks/{asset-GLEGKFG5.js → asset-7QJKMIDV.js} +2 -2
- package/dist/chunks/{attachment-content-AQNX5N3S.js → attachment-content-OSAVA6K3.js} +3 -2
- package/dist/chunks/{blog-E44OTVDA.js → blog-I7SDPNW4.js} +2 -2
- package/dist/chunks/{caizhilian-DQUFZXZL.js → caizhilian-O37ZIQNY.js} +2 -2
- package/dist/chunks/{calendar-JKPIRKFD.js → calendar-7KXE4ED3.js} +2 -2
- package/dist/chunks/{chunk-4YC43QQ4.js → chunk-2FLWXFVS.js} +4 -4
- package/dist/chunks/{chunk-QWY6PGL6.js → chunk-2LV74IWP.js} +361 -56
- package/dist/chunks/chunk-3SO6QL6U.js +73 -0
- package/dist/chunks/{chunk-DW6AUUKS.js → chunk-6ATGWRZM.js} +6 -4
- package/dist/chunks/chunk-BYATLLEF.js +68 -0
- package/dist/chunks/chunk-BYBSCN4X.js +932 -0
- package/dist/chunks/{chunk-2GA5ZIQL.js → chunk-D223PTXU.js} +1 -0
- package/dist/chunks/{chunk-H7J4DC5R.js → chunk-EEWPQE3O.js} +2 -2
- package/dist/chunks/{chunk-6RKD4DRZ.js → chunk-GFPXQIY2.js} +2 -2
- package/dist/chunks/{chunk-C6R3B2ML.js → chunk-HEL5XTYI.js} +12 -6
- package/dist/chunks/{chunk-CVZ2JOSW.js → chunk-IHUYP6LO.js} +1 -1
- package/dist/chunks/{chunk-YRVDW4WN.js → chunk-ILMBNVSG.js} +2 -2
- package/dist/chunks/{chunk-G3QJULO2.js → chunk-KAS45KNR.js} +1 -1
- package/dist/chunks/{chunk-H6XSTSLM.js → chunk-MEEFGWTM.js} +54 -196
- package/dist/chunks/{chunk-GCG2NMWR.js → chunk-MQ2I5AA3.js} +4 -2
- package/dist/chunks/chunk-NHA62QD7.js +774 -0
- package/dist/chunks/{chunk-WKI2FBIQ.js → chunk-OCU4RLHC.js} +641 -309
- package/dist/chunks/{chunk-WSKBSQ2U.js → chunk-PRHJGPMA.js} +2 -2
- package/dist/chunks/{chunk-6EFWDL7Y.js → chunk-Q6LWB5HX.js} +2 -2
- package/dist/chunks/{chunk-IURUFGZK.js → chunk-RBGOMK4Y.js} +50 -33
- package/dist/chunks/chunk-VBCFFFGT.js +226 -0
- package/dist/chunks/{ebuilder-form-V5FG2EHC.js → chunk-WRRJ4GG2.js} +22 -1148
- package/dist/chunks/{chunk-E4Z23GHM.js → chunk-WY74UBJ6.js} +264 -750
- package/dist/chunks/conflict-SFNEVJY5.js +21 -0
- package/dist/chunks/continuation-RN6PZN2M.js +11 -0
- package/dist/chunks/cowork-CKWYHXYB.js +2508 -0
- package/dist/chunks/ebuilder-form-KOLJRVZD.js +121 -0
- package/dist/chunks/{ehr-HMFY6VIL.js → ehr-WFOC3B4D.js} +1174 -20
- package/dist/chunks/{esb-ZYSVB7SG.js → esb-LJZTN663.js} +4 -4
- package/dist/chunks/{fna-B7RKPAZD.js → fna-F7QRIIZC.js} +2 -2
- package/dist/chunks/{host-45WED2KK.js → host-BCGUVEWX.js} +3 -3
- package/dist/chunks/host-BQBRHVR3.js +23 -0
- package/dist/chunks/{host-I4Z6BFYX.js → host-DGE2TZD4.js} +3 -3
- package/dist/chunks/{host-KNZ52LJZ.js → host-KXPTEWFS.js} +3 -3
- package/dist/chunks/{host-OCH2BCCV.js → host-VQ6JJXGQ.js} +3 -3
- package/dist/chunks/{hrm-2KAZGUHI.js → hrm-NI6XAKW5.js} +3 -3
- package/dist/chunks/{im-ACJJWH4U.js → im-7TRTF3KT.js} +88 -12
- package/dist/chunks/{invoice-ZK3WCX2U.js → invoice-AOXWJKYT.js} +3 -3
- package/dist/chunks/{jiuchuanhui-5PQD3IL2.js → jiuchuanhui-7NQWEZAW.js} +2 -2
- package/dist/chunks/{lazy-dispatch-JQXBW3GM.js → lazy-dispatch-RNS4EE4S.js} +4 -4
- package/dist/chunks/{mail-S2HDDXO2.js → mail-VDHJAB4O.js} +3 -3
- package/dist/chunks/{meeting-QVN3I37L.js → meeting-GA7XCNNP.js} +214 -43
- package/dist/chunks/{okr-QNG4VC2Q.js → okr-TQIRERPQ.js} +2 -2
- package/dist/chunks/operations-5QL3AECO.js +11 -0
- package/dist/chunks/{plan-A25EMW6U.js → plan-4RY4LN2P.js} +2 -2
- package/dist/chunks/{project-G6XV66DZ.js → project-D6XT2OC5.js} +2 -2
- package/dist/chunks/{registry-DC6KR3NH.js → registry-JHRIDQ3A.js} +4 -4
- package/dist/chunks/{registry-2VLUAESA.js → registry-OZSMDVPP.js} +2 -2
- package/dist/chunks/{registry-TI3CIUXF.js → registry-QKMI43Q6.js} +8 -6
- package/dist/chunks/{registry-7C5UMIPL.js → registry-VOGKFFTR.js} +2 -2
- package/dist/chunks/{room-recommend-WRTHO6Y2.js → room-recommend-D4EUOOWY.js} +4 -3
- package/dist/chunks/{schedule-KFIITGSK.js → schedule-XPFBHPCV.js} +4 -3
- package/dist/chunks/{workflow-DJ7SYWAF.js → workflow-HLBZRWIY.js} +172 -6
- package/dist/index.js +30 -25
- package/dist/internal/e10/auth-recovery.js +14 -4
- package/dist/shortcuts/contract/host.js +34 -59
- package/dist/shortcuts/cowork/continuation.js +70 -0
- package/dist/shortcuts/cowork/errors.js +59 -0
- package/dist/shortcuts/cowork/host.js +172 -0
- package/dist/shortcuts/cowork/index.js +166 -0
- package/dist/shortcuts/cowork/manifest.js +65 -0
- package/dist/shortcuts/cowork/operations/discuss-write.js +277 -0
- package/dist/shortcuts/cowork/operations/item-write.js +380 -0
- package/dist/shortcuts/cowork/operations/member-write.js +283 -0
- package/dist/shortcuts/cowork/operations/permission-write.js +286 -0
- package/dist/shortcuts/cowork/operations/read.js +423 -0
- package/dist/shortcuts/cowork/operations/registry.js +39 -0
- package/dist/shortcuts/cowork/operations/review-write.js +157 -0
- package/dist/shortcuts/cowork/operations/shared.js +215 -0
- package/dist/shortcuts/cowork/operations/types.js +24 -0
- package/dist/shortcuts/cowork/operations.js +19 -0
- package/dist/shortcuts/ehr/host.js +4 -1
- package/dist/shortcuts/ehr/operations/recruit-candidate-import.js +757 -0
- package/dist/shortcuts/ehr/operations/recruit-position.js +367 -0
- package/dist/shortcuts/ehr/operations/recruit-stats.js +133 -0
- package/dist/shortcuts/ehr/operations/recruit-write.js +8 -16
- package/dist/shortcuts/ehr/operations/recruit.js +35 -10
- package/dist/shortcuts/ehr/operations/registry.js +6 -0
- package/dist/shortcuts/im/diagnostics.js +97 -0
- package/dist/shortcuts/im/host.js +58 -31
- package/dist/shortcuts/im/index.js +7 -2
- package/dist/shortcuts/im/operations/at-mention.js +127 -0
- package/dist/shortcuts/im/operations/file-user.js +10 -2
- package/dist/shortcuts/im/operations/group-read.js +26 -5
- package/dist/shortcuts/im/operations/group-write.js +35 -28
- package/dist/shortcuts/im/operations/msg-sync.js +7 -2
- package/dist/shortcuts/im/operations/msg-write.js +76 -10
- package/dist/shortcuts/im/operations/quote-mention.js +119 -0
- package/dist/shortcuts/im/operations/session.js +1 -1
- package/dist/shortcuts/im/operations/shared.js +10 -0
- package/dist/shortcuts/im/operations/types.js +13 -0
- package/dist/shortcuts/index.js +5 -0
- package/dist/shortcuts/mail/host.js +4 -1
- package/dist/shortcuts/meeting/host.js +27 -34
- package/dist/shortcuts/meeting/operations/create.js +4 -1
- package/dist/shortcuts/meeting/operations/eb/catalog.js +5 -0
- package/dist/shortcuts/meeting/operations/eb/conflict.js +78 -7
- package/dist/shortcuts/meeting/operations/eb/create-flow.js +168 -26
- package/dist/shortcuts/meeting/operations/eb/factory.js +69 -1
- package/dist/shortcuts/meeting/operations/eb/index.js +16 -3
- package/dist/shortcuts/meeting/operations/eb/room-recommend.js +14 -3
- package/dist/shortcuts/meeting/operations/eb-fallback.js +159 -0
- package/dist/shortcuts/meeting/operations/env.js +93 -46
- package/dist/shortcuts/meeting/operations/form-fields.js +110 -0
- package/dist/shortcuts/meeting/operations/registry.js +2 -0
- package/dist/shortcuts/meeting/operations/standard-guard.js +71 -0
- package/dist/shortcuts/meeting/operations.js +16 -0
- package/dist/shortcuts/workflow/index.js +110 -1
- package/dist/shortcuts/workflow/operations/form.js +24 -3
- package/dist/shortcuts/workflow/operations/read.js +104 -23
- package/dist/shortcuts/workflow/operations/todoSmartConvert/aigroupRules.js +448 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/aigroupStore.js +106 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/businessRules.js +47 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.collect.js +30 -23
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.core.js +1 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.group.js +1 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.groupSummary.js +2 -1
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.js +2 -4
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.smartGroup.js +138 -181
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.summary.js +12 -7
- package/docs/e10-auth.md +1 -1
- package/docs/im.md +8 -1
- package/docs/todo-smart-convert/README.md +1 -1
- package/docs/todo-smart-convert/data-contract.md +2 -2
- package/docs/todo-smart-convert/subgroup-rules.md +6 -0
- package/package.json +1 -1
- package/skill-template/domains/yimiaoban.md +3 -3
- package/skill-template/skill-template.md +1 -1
- package/skills/weaver-e10-blog/SKILL.md +5 -5
- package/skills/weaver-e10-caizhilian/SKILL.md +5 -5
- package/skills/weaver-e10-calendar/SKILL.md +5 -5
- package/skills/weaver-e10-esb/SKILL.md +5 -5
- package/skills/weaver-e10-hrm/SKILL.md +5 -5
- package/skills/weaver-e10-jinchengda/SKILL.md +2 -2
- package/skills/weaver-e10-jiuchuanhui/SKILL.md +5 -5
- package/skills/weaver-e10-jucailin/SKILL.md +18 -12
- package/skills/weaver-e10-jucailin/references/recruit-create.md +104 -0
- package/skills/weaver-e10-mail/SKILL.md +5 -5
- package/skills/weaver-e10-meeting/SKILL.md +171 -25
- package/skills/weaver-e10-meeting/references/meeting-eb.md +4 -3
- package/skills/weaver-e10-meeting/references/meeting-queries.md +9 -6
- package/skills/weaver-e10-meeting/references/meeting-verification.md +8 -6
- package/skills/weaver-e10-meeting.zip +0 -0
- package/skills/weaver-e10-okr/SKILL.md +5 -5
- package/skills/weaver-e10-okr/references/source-manifest.json +3 -3
- package/skills/weaver-e10-plan/SKILL.md +5 -5
- package/skills/weaver-e10-shared/SKILL.md +12 -12
- package/skills/weaver-e10-shared/references/e10-auth-and-session.md +4 -4
- package/skills/weaver-e10-wenshuding/SKILL.md +5 -5
- package/skills/weaver-e10-workflow/SKILL.md +29 -5
- package/skills/weaver-e10-workflow/aigroup/default.json +88 -0
- package/skills/weaver-e10-workflow/references/todo-smart-convert.md +66 -14
- package/skills/weaver-e10-workflow/references/workflow-agent-entry.md +1 -1
- package/skills/weaver-e10-workflow/references/workflow-memory.md +1 -1
- package/skills/weaver-e10-workflow/references/workflow-query.md +12 -10
- package/skills/weaver-e10-workflow/references/workflow-view.md +2 -2
- package/skills/weaver-e10-workflow/tools/todo-serve.cjs +239 -0
- package/skills/weaver-e10-workflow/{scripts → tools}/todo-summary.cjs +218 -28
- package/skills/weaver-e10-yepiaotong/SKILL.md +10 -8
- package/skills/weaver-e10-yepiaotong/references/invoice-reim.md +7 -7
- package/skills/weaver-e10-yepiaotong/references/reim-api-reference.md +10 -10
- package/skills/weaver-e10-yepiaotong/references/reim-examples.md +40 -37
- package/skills/weaver-e10-yepiaotong/references/reim-expense-subject-rules.md +4 -4
- package/skills/weaver-e10-yepiaotong/references/reim-form-fill-rules.md +27 -38
- package/skills/weaver-e10-yepiaotong/references/reim-phone-subsidy-rules.md +4 -4
- package/skills/weaver-e10-yepiaotong/references/reim-workflow.md +37 -29
- package/skills/weaver-e10-yimiaoban/SKILL.md +156 -33
- package/skills/weaver-e10-yimiaoban/references/ding-write.md +1 -1
- package/skills/weaver-e10-yimiaoban/references/error-codes.md +5 -1
- package/skills/weaver-e10-yimiaoban/references/group-read.md +5 -1
- package/skills/weaver-e10-yimiaoban/references/group-write.md +6 -4
- package/skills/weaver-e10-yimiaoban/references/msg-read.md +1 -0
- package/skills/weaver-e10-yimiaoban/references/msg-write.md +34 -2
- package/skills/weaver-e10-yimiaoban/references/safety-boundaries.md +5 -2
- package/skills/weaver-e10-yimiaoban/references/source-manifest.json +79 -58
- package/skills/weaver-e10-yimiaoban/references/user-remind.md +2 -2
- package/skills/weaver-e10-ziguanjia/SKILL.md +5 -5
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `aigroup` 规则库:把「ai二次分组」的个性化规则从代码挪到 JSON(2026-10-28 用户定稿)。
|
|
3
|
+
*
|
|
4
|
+
* 目录与文件:
|
|
5
|
+
* - `<skill>/aigroup/default.json` —— 系统默认规则(随 skill 发布,**不允许被覆盖**)
|
|
6
|
+
* - `<state>/aigroup/user-intents.json` —— 用户意图规则(会话里由模型生成、CLI 校验落盘,整条覆盖)
|
|
7
|
+
* 刻意放 CLI 状态目录而不是 skill 目录:本机产生的意图不能跟着 skill 被打包/安装到别人机器上。
|
|
8
|
+
*
|
|
9
|
+
* 本模块只负责**读、校验、匹配、条件求值**,不负责分组算法本身;
|
|
10
|
+
* 门槛 / 子组解散 / 漏分平铺 / 排序编号 / 错误码仍固定在代码里(全局规则,不进 JSON)。
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import { stateRoot } from '../../../../core/state.js';
|
|
15
|
+
import { cacheRoots } from './summaryCache.js';
|
|
16
|
+
export const AIGROUP_DIR_NAME = 'aigroup';
|
|
17
|
+
export const DEFAULT_RULE_FILE = 'default.json';
|
|
18
|
+
export const USER_INTENT_RULE_FILE = 'user-intents.json';
|
|
19
|
+
/** 十种算子(2026-10-28 用户定稿;`介于` 左闭右闭)。 */
|
|
20
|
+
export const FOCUS_OPERATORS = [
|
|
21
|
+
'等于', '不等于', '是', '不是', '大于', '小于', '小于等于', '大于等于', '介于', '包含',
|
|
22
|
+
];
|
|
23
|
+
/** `groupingPrompt` 会原样进模型上下文,限长避免把规则文件当提示词工程仓库。 */
|
|
24
|
+
const MAX_GROUPING_PROMPT_CHARS = 500;
|
|
25
|
+
const EMPTY_GROUP_RULE = { focus: false };
|
|
26
|
+
export const FALLBACK_DEFAULTS = {
|
|
27
|
+
weight: 999,
|
|
28
|
+
needSummary: true,
|
|
29
|
+
requestSummaryPrompt: '',
|
|
30
|
+
summaryMaxChars: 150,
|
|
31
|
+
groupSummaryMaxChars: 100,
|
|
32
|
+
groupSummaryTail: '可展开逐条查看或直接汇总处理',
|
|
33
|
+
};
|
|
34
|
+
export function emptyRuleFile(error = null) {
|
|
35
|
+
return {
|
|
36
|
+
file: { schemaVersion: 2, defaults: FALLBACK_DEFAULTS, rules: [] },
|
|
37
|
+
error,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function asStringArray(value) {
|
|
41
|
+
if (typeof value === 'string')
|
|
42
|
+
return [value];
|
|
43
|
+
if (!Array.isArray(value))
|
|
44
|
+
return null;
|
|
45
|
+
const items = value.map((item) => String(item ?? '').trim()).filter((item) => item !== '');
|
|
46
|
+
return items.length > 0 ? items : null;
|
|
47
|
+
}
|
|
48
|
+
function isOperator(value) {
|
|
49
|
+
return typeof value === 'string' && FOCUS_OPERATORS.includes(value);
|
|
50
|
+
}
|
|
51
|
+
/** 校验一条 focusKeyword;不合法返回原因字符串。 */
|
|
52
|
+
function validateFocusKeyword(raw, index) {
|
|
53
|
+
if (!Array.isArray(raw))
|
|
54
|
+
return `focusKeyword[${index}] 必须是数组`;
|
|
55
|
+
const [metric, op] = raw;
|
|
56
|
+
if (typeof metric !== 'string' || !metric.trim())
|
|
57
|
+
return `focusKeyword[${index}] 的指标名必须是非空字符串`;
|
|
58
|
+
if (!isOperator(op))
|
|
59
|
+
return `focusKeyword[${index}] 的算子「${String(op)}」不支持(可用:${FOCUS_OPERATORS.join('/')})`;
|
|
60
|
+
if (op === '介于') {
|
|
61
|
+
if (raw.length !== 4)
|
|
62
|
+
return `focusKeyword[${index}] 用「介于」时必须给 [指标, 介于, 下界, 上界]`;
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
if (raw.length !== 3)
|
|
66
|
+
return `focusKeyword[${index}] 必须是 [指标, 算子, 值] 三元组`;
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
/** 校验整份规则文件;返回错误列表(空数组 = 合法)。 */
|
|
70
|
+
export function validateRuleFile(raw) {
|
|
71
|
+
const errors = [];
|
|
72
|
+
const file = raw;
|
|
73
|
+
if (!file || typeof file !== 'object')
|
|
74
|
+
return ['规则文件必须是 JSON 对象'];
|
|
75
|
+
if (!Array.isArray(file.rules)) {
|
|
76
|
+
errors.push('rules 必须是数组');
|
|
77
|
+
return errors;
|
|
78
|
+
}
|
|
79
|
+
const seen = new Set();
|
|
80
|
+
file.rules.forEach((rule, index) => {
|
|
81
|
+
if (!rule || typeof rule !== 'object') {
|
|
82
|
+
errors.push(`rules[${index}] 必须是对象`);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const names = asStringArray(rule.workflowName);
|
|
86
|
+
if (!names) {
|
|
87
|
+
errors.push(`rules[${index}].workflowName 必须是非空字符串或非空数组`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
for (const name of names) {
|
|
91
|
+
if (seen.has(name))
|
|
92
|
+
errors.push(`workflowName「${name}」在 rules[${index}] 重复出现`);
|
|
93
|
+
seen.add(name);
|
|
94
|
+
}
|
|
95
|
+
if (rule.weight !== undefined && (typeof rule.weight !== 'number' || !Number.isFinite(rule.weight))) {
|
|
96
|
+
errors.push(`rules[${index}].weight 必须是数字`);
|
|
97
|
+
}
|
|
98
|
+
if (rule.summaryMaxChars !== undefined && (typeof rule.summaryMaxChars !== 'number' || rule.summaryMaxChars <= 0)) {
|
|
99
|
+
errors.push(`rules[${index}].summaryMaxChars 必须是正数`);
|
|
100
|
+
}
|
|
101
|
+
const groupRule = rule.aiGroup?.groupRule;
|
|
102
|
+
if (groupRule) {
|
|
103
|
+
if (typeof groupRule.focus !== 'boolean')
|
|
104
|
+
errors.push(`rules[${index}].aiGroup.groupRule.focus 必须是布尔`);
|
|
105
|
+
if (groupRule.focus) {
|
|
106
|
+
const keywords = groupRule.focusKeyword;
|
|
107
|
+
if (!Array.isArray(keywords) || keywords.length === 0) {
|
|
108
|
+
errors.push(`rules[${index}] focus=true 时必须给 focusKeyword`);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
keywords.forEach((keyword, k) => {
|
|
112
|
+
const reason = validateFocusKeyword(keyword, k);
|
|
113
|
+
if (reason)
|
|
114
|
+
errors.push(`rules[${index}].${reason}`);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
else if (groupRule.focusKeyword !== undefined && groupRule.focusKeyword.length > 0) {
|
|
119
|
+
errors.push(`rules[${index}] focus=false 时不应给 focusKeyword`);
|
|
120
|
+
}
|
|
121
|
+
if (groupRule.focus === false && !groupRule.aiContext) {
|
|
122
|
+
errors.push(`rules[${index}] focus=false 时必须给 aiContext(流程标题 / 流程详情)`);
|
|
123
|
+
}
|
|
124
|
+
if (groupRule.groupingPrompt !== undefined) {
|
|
125
|
+
if (groupRule.focus === true) {
|
|
126
|
+
errors.push(`rules[${index}] focus=true 时不应给 groupingPrompt(它只作用于 focus=false 的语义分组)`);
|
|
127
|
+
}
|
|
128
|
+
else if (typeof groupRule.groupingPrompt !== 'string' || groupRule.groupingPrompt.trim() === '') {
|
|
129
|
+
errors.push(`rules[${index}].aiGroup.groupRule.groupingPrompt 必须是非空字符串`);
|
|
130
|
+
}
|
|
131
|
+
else if (groupRule.groupingPrompt.length > MAX_GROUPING_PROMPT_CHARS) {
|
|
132
|
+
errors.push(`rules[${index}].aiGroup.groupRule.groupingPrompt 超过 ${MAX_GROUPING_PROMPT_CHARS} 字`
|
|
133
|
+
+ '(它会原样进模型上下文,请精简)');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const groupSummary = rule.aiGroup?.groupSummary;
|
|
138
|
+
if (groupSummary) {
|
|
139
|
+
if (typeof groupSummary.needAI !== 'boolean')
|
|
140
|
+
errors.push(`rules[${index}].aiGroup.groupSummary.needAI 必须是布尔`);
|
|
141
|
+
if (groupSummary.needAI && !groupSummary.prompt)
|
|
142
|
+
errors.push(`rules[${index}] groupSummary.needAI=true 时必须给 prompt`);
|
|
143
|
+
if (!groupSummary.needAI && !groupSummary.template)
|
|
144
|
+
errors.push(`rules[${index}] groupSummary.needAI=false 时必须给 template`);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
return errors;
|
|
148
|
+
}
|
|
149
|
+
/** 读一份规则文件;文件不存在/坏 JSON/结构不合法都退回 `emptyRuleFile(原因)`。 */
|
|
150
|
+
export function loadRuleFile(file) {
|
|
151
|
+
if (!existsSync(file))
|
|
152
|
+
return emptyRuleFile(`规则文件不存在:${file}`);
|
|
153
|
+
let parsed;
|
|
154
|
+
try {
|
|
155
|
+
parsed = JSON.parse(readFileSync(file, 'utf8'));
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
return emptyRuleFile(`规则文件不是合法 JSON:${file}(${error.message})`);
|
|
159
|
+
}
|
|
160
|
+
const errors = validateRuleFile(parsed);
|
|
161
|
+
if (errors.length > 0)
|
|
162
|
+
return emptyRuleFile(`规则文件校验失败:${errors.join(';')}`);
|
|
163
|
+
return { file: parsed, error: null };
|
|
164
|
+
}
|
|
165
|
+
/** 默认规则目录(**随 skill 分发、只读**):`WEAVER_WORK_CLI_AIGROUP_DIR` → skill 目录下的 `aigroup/` → CLI 状态目录。 */
|
|
166
|
+
export function aigroupDir(ctx) {
|
|
167
|
+
const explicit = process.env.WEAVER_WORK_CLI_AIGROUP_DIR?.trim();
|
|
168
|
+
if (explicit)
|
|
169
|
+
return join(explicit);
|
|
170
|
+
const roots = cacheRoots(ctx);
|
|
171
|
+
return roots.skillDir ? join(roots.skillDir, AIGROUP_DIR_NAME) : join(stateRoot(ctx), AIGROUP_DIR_NAME);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* 用户意图目录(**运行时产物,绝不写进 skill 目录**):`WEAVER_WORK_CLI_AIGROUP_DIR` → CLI 状态目录。
|
|
175
|
+
*
|
|
176
|
+
* 刻意与 `aigroupDir()` 分开:意图文件是本机产生的,一旦落在 skill 目录里,
|
|
177
|
+
* 打包/安装时会跟着 skill 跑到别人机器上(既是泄漏,也会污染别人的分组口径)。
|
|
178
|
+
*/
|
|
179
|
+
export function aigroupUserDir(ctx) {
|
|
180
|
+
const explicit = process.env.WEAVER_WORK_CLI_AIGROUP_DIR?.trim();
|
|
181
|
+
if (explicit)
|
|
182
|
+
return join(explicit);
|
|
183
|
+
return join(stateRoot(ctx), AIGROUP_DIR_NAME);
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* 加载并合并规则:默认文件 +(可选)用户意图文件(**同 workflowName 整条覆盖**,系统默认不被覆盖)。
|
|
187
|
+
* 用户意图文件里的条目按 `workflowName` 粒度替换默认条目,缺字段由 defaults 补。
|
|
188
|
+
*/
|
|
189
|
+
export function loadRules(options = {}) {
|
|
190
|
+
const dir = options.dir ?? aigroupDir(options.ctx);
|
|
191
|
+
// 显式 `dir` 时(测试/现场)默认文件与意图文件放一起;否则意图文件只从状态目录读。
|
|
192
|
+
const userDir = options.dir ?? aigroupUserDir(options.ctx);
|
|
193
|
+
const warnings = [];
|
|
194
|
+
const base = loadRuleFile(join(dir, DEFAULT_RULE_FILE));
|
|
195
|
+
if (base.error)
|
|
196
|
+
warnings.push(base.error);
|
|
197
|
+
const rules = [...base.file.rules];
|
|
198
|
+
// 兼容迁移:意图文件原先写在 skill 目录里,改到状态目录后老文件不会被自动搬走;
|
|
199
|
+
// 状态目录没有时就回读老位置,避免用户已有的意图「凭空消失」。
|
|
200
|
+
const userIntentFile = join(userDir, USER_INTENT_RULE_FILE);
|
|
201
|
+
const legacyIntentFile = join(dir, USER_INTENT_RULE_FILE);
|
|
202
|
+
let intentFile = userIntentFile;
|
|
203
|
+
if (!options.dir && !existsSync(userIntentFile) && existsSync(legacyIntentFile)) {
|
|
204
|
+
intentFile = legacyIntentFile;
|
|
205
|
+
warnings.push(`用户意图仍读自 skill 目录(${legacyIntentFile}):新位置是 ${userIntentFile},重新执行 aigroup set 即可迁移`);
|
|
206
|
+
}
|
|
207
|
+
const intent = loadRuleFile(intentFile);
|
|
208
|
+
if (intent.error) {
|
|
209
|
+
// 用户意图文件缺失是常态(还没产生意图),只在结构坏掉时才告警
|
|
210
|
+
if (existsSync(intentFile))
|
|
211
|
+
warnings.push(intent.error);
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
for (const rule of intent.file.rules) {
|
|
215
|
+
const names = new Set(asStringArray(rule.workflowName) ?? []);
|
|
216
|
+
const at = rules.findIndex((item) => (asStringArray(item.workflowName) ?? []).some((name) => names.has(name)));
|
|
217
|
+
if (at >= 0 && (asStringArray(rules[at].workflowName) ?? []).includes('*')) {
|
|
218
|
+
warnings.push(`用户意图不能覆盖通配条目「*」,已忽略:${[...names].join(',')}`);
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (at >= 0)
|
|
222
|
+
rules[at] = rule;
|
|
223
|
+
else
|
|
224
|
+
rules.push(rule);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
file: { schemaVersion: base.file.schemaVersion, defaults: { ...FALLBACK_DEFAULTS, ...base.file.defaults }, rules },
|
|
229
|
+
warnings,
|
|
230
|
+
dir,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
/** 名称匹配:精确(别名全等)优先;否则模糊(包含)按权重升序、同权重按配置顺序。 */
|
|
234
|
+
export function matchRule(rules, workflowName) {
|
|
235
|
+
const name = String(workflowName ?? '').trim();
|
|
236
|
+
const aliasesOf = (rule) => asStringArray(rule.workflowName) ?? [];
|
|
237
|
+
const indexOf = new Map();
|
|
238
|
+
rules.forEach((rule, index) => indexOf.set(rule, index));
|
|
239
|
+
if (name) {
|
|
240
|
+
const exact = rules.filter((rule) => aliasesOf(rule).includes(name));
|
|
241
|
+
if (exact.length > 0) {
|
|
242
|
+
const [rule] = exact;
|
|
243
|
+
return { rule, matchedBy: 'exact', index: indexOf.get(rule) ?? 0 };
|
|
244
|
+
}
|
|
245
|
+
const fuzzy = rules.filter((rule) => aliasesOf(rule).some((alias) => alias !== '*' && name.includes(alias)));
|
|
246
|
+
if (fuzzy.length > 0) {
|
|
247
|
+
const sorted = [...fuzzy].sort((a, b) => (a.weight ?? FALLBACK_DEFAULTS.weight) - (b.weight ?? FALLBACK_DEFAULTS.weight)
|
|
248
|
+
|| (indexOf.get(a) ?? 0) - (indexOf.get(b) ?? 0));
|
|
249
|
+
const rule = sorted[0];
|
|
250
|
+
return { rule, matchedBy: 'fuzzy', index: indexOf.get(rule) ?? 0 };
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
const fallback = rules.find((rule) => aliasesOf(rule).includes('*'));
|
|
254
|
+
if (fallback)
|
|
255
|
+
return { rule: fallback, matchedBy: 'default', index: indexOf.get(fallback) ?? 0 };
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
/** 合并 defaults,得到生效规则。 */
|
|
259
|
+
export function resolveRule(file, workflowName) {
|
|
260
|
+
const matched = matchRule(file.rules, workflowName);
|
|
261
|
+
if (!matched)
|
|
262
|
+
return null;
|
|
263
|
+
const defaults = { ...FALLBACK_DEFAULTS, ...file.defaults };
|
|
264
|
+
const raw = matched.rule;
|
|
265
|
+
const groupSummary = raw.aiGroup?.groupSummary;
|
|
266
|
+
const groupRule = raw.aiGroup?.groupRule;
|
|
267
|
+
return {
|
|
268
|
+
matchedBy: matched.matchedBy,
|
|
269
|
+
rule: {
|
|
270
|
+
workflowName: asStringArray(raw.workflowName) ?? [],
|
|
271
|
+
weight: raw.weight ?? defaults.weight,
|
|
272
|
+
needSummary: raw.needSummary ?? defaults.needSummary,
|
|
273
|
+
requestSummaryPrompt: raw.requestSummaryPrompt ?? defaults.requestSummaryPrompt,
|
|
274
|
+
summaryMaxChars: raw.summaryMaxChars ?? defaults.summaryMaxChars,
|
|
275
|
+
groupSummaryMaxChars: defaults.groupSummaryMaxChars,
|
|
276
|
+
groupSummaryTail: defaults.groupSummaryTail,
|
|
277
|
+
aiGroup: raw.aiGroup
|
|
278
|
+
? {
|
|
279
|
+
groupSummary: {
|
|
280
|
+
needAI: groupSummary?.needAI ?? false,
|
|
281
|
+
...(groupSummary?.prompt === undefined ? {} : { prompt: groupSummary.prompt }),
|
|
282
|
+
...(groupSummary?.template === undefined ? {} : { template: groupSummary.template }),
|
|
283
|
+
maxChars: defaults.groupSummaryMaxChars,
|
|
284
|
+
tail: defaults.groupSummaryTail,
|
|
285
|
+
},
|
|
286
|
+
groupRule: groupRule ?? EMPTY_GROUP_RULE,
|
|
287
|
+
}
|
|
288
|
+
: null,
|
|
289
|
+
ruleIndex: matched.index,
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
// ---------------------------------------------------------------------------
|
|
294
|
+
// focusKeyword 求值
|
|
295
|
+
// ---------------------------------------------------------------------------
|
|
296
|
+
/** 日期口径:只比到「日」,用本地时区(`2026-10-28`)。 */
|
|
297
|
+
export function dateOnly(value) {
|
|
298
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
299
|
+
return `${value.getFullYear()}-${pad(value.getMonth() + 1)}-${pad(value.getDate())}`;
|
|
300
|
+
}
|
|
301
|
+
const RELATIVE_DAYS = { 今天: 0, 昨天: -1, 明天: 1 };
|
|
302
|
+
/** 把「今天/昨天/明天」或 `2026-10-28 …` 归一成 `YYYY-MM-DD`;不是日期返回 null。 */
|
|
303
|
+
export function normalizeDateLike(value, now = new Date()) {
|
|
304
|
+
const text = value.trim();
|
|
305
|
+
if (text in RELATIVE_DAYS) {
|
|
306
|
+
const date = new Date(now.getTime());
|
|
307
|
+
date.setDate(date.getDate() + RELATIVE_DAYS[text]);
|
|
308
|
+
return dateOnly(date);
|
|
309
|
+
}
|
|
310
|
+
const matched = /^(\d{4})[-/](\d{1,2})[-/](\d{1,2})/u.exec(text);
|
|
311
|
+
if (!matched)
|
|
312
|
+
return null;
|
|
313
|
+
const [, y, m, d] = matched;
|
|
314
|
+
return `${y}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}`;
|
|
315
|
+
}
|
|
316
|
+
function toNumber(value) {
|
|
317
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
318
|
+
return value;
|
|
319
|
+
const text = String(value ?? '').replace(/[,¥¥\s]/gu, '');
|
|
320
|
+
if (!text)
|
|
321
|
+
return null;
|
|
322
|
+
const parsed = Number(text);
|
|
323
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
324
|
+
}
|
|
325
|
+
function comparableOf(value, now) {
|
|
326
|
+
if (value === null || value === undefined || value === '')
|
|
327
|
+
return null;
|
|
328
|
+
if (typeof value === 'number')
|
|
329
|
+
return Number.isFinite(value) ? { kind: 'number', value } : null;
|
|
330
|
+
const text = String(value).trim();
|
|
331
|
+
if (!text)
|
|
332
|
+
return null;
|
|
333
|
+
const date = normalizeDateLike(text, now);
|
|
334
|
+
if (date)
|
|
335
|
+
return { kind: 'date', value: date };
|
|
336
|
+
const num = toNumber(text);
|
|
337
|
+
// 纯数字优先按数值比,避免「100」被当文本做字典序
|
|
338
|
+
if (num !== null && /^[¥¥]?\s*-?\d[\d,]*(?:\.\d+)?$/.test(text.replace(/\s/gu, ''))) {
|
|
339
|
+
return { kind: 'number', value: num };
|
|
340
|
+
}
|
|
341
|
+
return { kind: 'text', value: text };
|
|
342
|
+
}
|
|
343
|
+
function compare(a, b) {
|
|
344
|
+
if (a.kind === 'number' && b.kind === 'number')
|
|
345
|
+
return a.value - b.value;
|
|
346
|
+
if (a.kind === 'date' && b.kind === 'date')
|
|
347
|
+
return a.value < b.value ? -1 : a.value > b.value ? 1 : 0;
|
|
348
|
+
if (a.kind === 'text' || b.kind === 'text') {
|
|
349
|
+
const left = String(a.value);
|
|
350
|
+
const right = String(b.value);
|
|
351
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
352
|
+
}
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* 单个条件求值。**取不到值(null/undefined/空串)一律判为不命中**(2026-10-28 用户定稿:不猜)。
|
|
357
|
+
*/
|
|
358
|
+
export function evaluateCondition(condition, actual, now = new Date()) {
|
|
359
|
+
const [metric, op, expected, upper] = condition;
|
|
360
|
+
const left = comparableOf(actual, now);
|
|
361
|
+
if (!left)
|
|
362
|
+
return false;
|
|
363
|
+
const right = comparableOf(expected, now);
|
|
364
|
+
if (!right)
|
|
365
|
+
return false;
|
|
366
|
+
switch (op) {
|
|
367
|
+
case '等于':
|
|
368
|
+
case '是':
|
|
369
|
+
return compare(left, right) === 0;
|
|
370
|
+
case '不等于':
|
|
371
|
+
case '不是':
|
|
372
|
+
return compare(left, right) !== 0;
|
|
373
|
+
case '大于': {
|
|
374
|
+
const diff = compare(left, right);
|
|
375
|
+
return diff !== null && diff > 0;
|
|
376
|
+
}
|
|
377
|
+
case '小于': {
|
|
378
|
+
const diff = compare(left, right);
|
|
379
|
+
return diff !== null && diff < 0;
|
|
380
|
+
}
|
|
381
|
+
case '大于等于': {
|
|
382
|
+
const diff = compare(left, right);
|
|
383
|
+
return diff !== null && diff >= 0;
|
|
384
|
+
}
|
|
385
|
+
case '小于等于': {
|
|
386
|
+
const diff = compare(left, right);
|
|
387
|
+
return diff !== null && diff <= 0;
|
|
388
|
+
}
|
|
389
|
+
case '介于': {
|
|
390
|
+
const top = comparableOf(upper, now);
|
|
391
|
+
if (!top)
|
|
392
|
+
return false;
|
|
393
|
+
const low = compare(left, right);
|
|
394
|
+
const high = compare(left, top);
|
|
395
|
+
return low !== null && high !== null && low >= 0 && high <= 0;
|
|
396
|
+
}
|
|
397
|
+
case '包含':
|
|
398
|
+
return String(left.value).includes(String(right.value));
|
|
399
|
+
default:
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* 一条规则的全部条件(AND)。任一条件取不到值即不命中。
|
|
405
|
+
* `resolve` 由调用方提供:sysArg=true 从列表条目属性取,false 从流程详情字段取。
|
|
406
|
+
*/
|
|
407
|
+
export function evaluateFocusKeywords(conditions, resolve, now = new Date()) {
|
|
408
|
+
if (conditions.length === 0)
|
|
409
|
+
return false;
|
|
410
|
+
return conditions.every((condition) => evaluateCondition(condition, resolve(condition[0]), now));
|
|
411
|
+
}
|
|
412
|
+
// ---------------------------------------------------------------------------
|
|
413
|
+
// 进程内生效规则(一次 operation 只加载一次;纯读,不缓存到磁盘)
|
|
414
|
+
// ---------------------------------------------------------------------------
|
|
415
|
+
let activeFile = null;
|
|
416
|
+
let activeDir = null;
|
|
417
|
+
/** 加载并激活规则(每个 operation 开头调一次)。返回加载告警,由调用方并入 warnings。 */
|
|
418
|
+
export function primeRules(ctx) {
|
|
419
|
+
const loaded = loadRules({ ctx });
|
|
420
|
+
activeFile = loaded.file;
|
|
421
|
+
activeDir = loaded.dir;
|
|
422
|
+
return loaded;
|
|
423
|
+
}
|
|
424
|
+
/** 当前生效的规则文件(未 prime 时按默认目录懒加载)。 */
|
|
425
|
+
export function activeRules() {
|
|
426
|
+
if (!activeFile) {
|
|
427
|
+
const loaded = loadRules();
|
|
428
|
+
activeFile = loaded.file;
|
|
429
|
+
activeDir = loaded.dir;
|
|
430
|
+
}
|
|
431
|
+
return activeFile;
|
|
432
|
+
}
|
|
433
|
+
/** 当前规则目录(测试/诊断用)。 */
|
|
434
|
+
export function activeRulesDir() {
|
|
435
|
+
return activeDir;
|
|
436
|
+
}
|
|
437
|
+
/** 某工作流名对应的生效规则(找不到返回 null)。 */
|
|
438
|
+
export function activeRuleFor(workflowName) {
|
|
439
|
+
const name = String(workflowName ?? '').trim();
|
|
440
|
+
if (!name)
|
|
441
|
+
return null;
|
|
442
|
+
return resolveRule(activeRules(), name)?.rule ?? null;
|
|
443
|
+
}
|
|
444
|
+
/** 清空进程内缓存(单测用)。 */
|
|
445
|
+
export function resetActiveRules() {
|
|
446
|
+
activeFile = null;
|
|
447
|
+
activeDir = null;
|
|
448
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 用户意图规则库(`aigroup/user-intents.json`,落在 **CLI 状态目录**,不写进 skill 目录)的读写。
|
|
3
|
+
*
|
|
4
|
+
* 为什么需要 CLI 入口:意图是模型用自然语言理解出来的,但**落盘必须结构化**。
|
|
5
|
+
* 让模型直接写文件,一旦字段拼错(`focus: true` 却没给 `focusKeyword`、`workflowName` 用了 `*`),
|
|
6
|
+
* 后续整轮分组都会走错分支且很难定位;所以由 CLI 统一做「校验 → 按 workflowName 整条覆盖 → 落盘」。
|
|
7
|
+
*
|
|
8
|
+
* 覆盖规则(2026-10-28 用户定):
|
|
9
|
+
* - 同一 `workflowName` **整条覆盖**(不是字段合并),缺的字段由 `default.json` 的 defaults 补;
|
|
10
|
+
* - **不允许覆盖通配条目 `*`**(系统默认的兜底规则不可被意图改掉);
|
|
11
|
+
* - 系统默认文件 `default.json` 永远只读。
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { workflowValidationError } from '../../errors.js';
|
|
16
|
+
import { aigroupUserDir, emptyRuleFile, loadRuleFile, validateRuleFile, USER_INTENT_RULE_FILE, } from './aigroupRules.js';
|
|
17
|
+
/** 把入参归一成规则条目数组:允许单条、数组、或 `{ rules: [...] }`。 */
|
|
18
|
+
export function normalizeIntentPayload(payload) {
|
|
19
|
+
if (Array.isArray(payload))
|
|
20
|
+
return payload;
|
|
21
|
+
if (payload && typeof payload === 'object') {
|
|
22
|
+
const record = payload;
|
|
23
|
+
if (Array.isArray(record.rules))
|
|
24
|
+
return record.rules;
|
|
25
|
+
if (record.workflowName !== undefined)
|
|
26
|
+
return [record];
|
|
27
|
+
}
|
|
28
|
+
throw workflowValidationError('aigroup_invalid', '入参必须是规则对象、规则数组,或 { rules: [...] }');
|
|
29
|
+
}
|
|
30
|
+
function namesOf(rule) {
|
|
31
|
+
const raw = rule?.workflowName;
|
|
32
|
+
if (typeof raw === 'string')
|
|
33
|
+
return [raw.trim()].filter(Boolean);
|
|
34
|
+
if (Array.isArray(raw))
|
|
35
|
+
return raw.map((item) => String(item ?? '').trim()).filter(Boolean);
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* 校验并落盘用户意图规则。
|
|
40
|
+
*
|
|
41
|
+
* 校验直接复用 `validateRuleFile`(包一层 `{ defaults, rules }`),
|
|
42
|
+
* 所以「focus=true 必须给 focusKeyword」「focus=false 不给 focusKeyword」「groupSummary 必须二选一」等
|
|
43
|
+
* 全部与默认文件同一套规则,不会出现两套标准。
|
|
44
|
+
*/
|
|
45
|
+
export function saveUserIntents(payload, options = {}) {
|
|
46
|
+
const dir = options.dir ?? aigroupUserDir(options.ctx);
|
|
47
|
+
const warnings = [];
|
|
48
|
+
const entries = normalizeIntentPayload(payload);
|
|
49
|
+
if (entries.length === 0)
|
|
50
|
+
throw workflowValidationError('aigroup_invalid', '入参里没有规则条目');
|
|
51
|
+
const starred = entries.filter((rule) => namesOf(rule).includes('*'));
|
|
52
|
+
if (starred.length > 0) {
|
|
53
|
+
throw workflowValidationError('aigroup_wildcard_unsupported', '用户意图不能使用通配 workflowName「*」:兜底规则由 aigroup/default.json 决定');
|
|
54
|
+
}
|
|
55
|
+
const check = validateRuleFile({ schemaVersion: 2, defaults: {}, rules: entries });
|
|
56
|
+
if (check.length > 0) {
|
|
57
|
+
throw workflowValidationError('aigroup_invalid', `规则校验失败:${check.join(';')}`);
|
|
58
|
+
}
|
|
59
|
+
const file = join(dir, USER_INTENT_RULE_FILE);
|
|
60
|
+
const existing = existsSync(file) ? loadRuleFile(file) : emptyRuleFile();
|
|
61
|
+
if (existing.error) {
|
|
62
|
+
warnings.push(`${existing.error};本次将重建该文件`);
|
|
63
|
+
}
|
|
64
|
+
const byName = new Map();
|
|
65
|
+
const replaced = [];
|
|
66
|
+
for (const rule of existing.file.rules) {
|
|
67
|
+
for (const name of namesOf(rule))
|
|
68
|
+
byName.set(name, rule);
|
|
69
|
+
}
|
|
70
|
+
for (const rule of entries) {
|
|
71
|
+
const names = namesOf(rule);
|
|
72
|
+
for (const name of names) {
|
|
73
|
+
if (byName.has(name))
|
|
74
|
+
replaced.push(name);
|
|
75
|
+
byName.set(name, rule);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// 同一份规则可能有多个别名:按对象去重后落盘
|
|
79
|
+
const merged = [...new Set(byName.values())];
|
|
80
|
+
// 用户文件**不写 defaults**:缺省值统一由 default.json 提供,避免两份 defaults 打架
|
|
81
|
+
const text = `${JSON.stringify({ schemaVersion: 2, rules: merged }, null, 2)}\n`;
|
|
82
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
83
|
+
writeFileSync(file, text, { mode: 0o600 });
|
|
84
|
+
return { file, saved: entries.length, total: merged.length, replaced: [...new Set(replaced)], warnings };
|
|
85
|
+
}
|
|
86
|
+
/** 读用户意图文件(诊断/展示用)。 */
|
|
87
|
+
export function readUserIntents(options = {}) {
|
|
88
|
+
const dir = options.dir ?? aigroupUserDir(options.ctx);
|
|
89
|
+
const file = join(dir, USER_INTENT_RULE_FILE);
|
|
90
|
+
if (!existsSync(file))
|
|
91
|
+
return { dir, file, exists: false, error: null, rules: [] };
|
|
92
|
+
const loaded = loadRuleFile(file);
|
|
93
|
+
return { dir, file, exists: true, error: loaded.error, rules: loaded.file.rules };
|
|
94
|
+
}
|
|
95
|
+
/** 清空用户意图(只删内容、不删文件,保持目录结构)。 */
|
|
96
|
+
export function clearUserIntents(options = {}) {
|
|
97
|
+
const dir = options.dir ?? aigroupUserDir(options.ctx);
|
|
98
|
+
const file = join(dir, USER_INTENT_RULE_FILE);
|
|
99
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
100
|
+
writeFileSync(file, `${JSON.stringify({ schemaVersion: 2, rules: [] }, null, 2)}\n`, { mode: 0o600 });
|
|
101
|
+
return file;
|
|
102
|
+
}
|
|
103
|
+
/** 读原始文本(测试/排查用,避免被 loadRuleFile 的校验吞掉坏内容)。 */
|
|
104
|
+
export function readRawUserIntents(file) {
|
|
105
|
+
return readFileSync(file, 'utf8');
|
|
106
|
+
}
|
|
@@ -329,3 +329,50 @@ export function formatIssueSupportGroupSummary(input) {
|
|
|
329
329
|
const restText = input.restCount > 0 ? `其余 ${input.restCount} 条按单条展示,` : '';
|
|
330
330
|
return `含 ${input.count} 条已过期或今日到期的问题流程,${creatorText}${restText}${GROUP_SUMMARY_TAIL_TEXT}`;
|
|
331
331
|
}
|
|
332
|
+
/**
|
|
333
|
+
* 用 JSON 里的模板渲染组摘要(2026-10-28 用户定:文案从 `aigroup/default.json` 的 `template` 读)。
|
|
334
|
+
*
|
|
335
|
+
* 支持占位符:`{{count}}` `{{restCount}}` `{{categoryCount}}` `{{categories}}` `{{min}}` `{{max}}`
|
|
336
|
+
* `{{sum}}` `{{creators}}` `{{tail}}` `{{maxChars}}`。
|
|
337
|
+
*
|
|
338
|
+
* 两条防空规则(不产生 `()` / `发起。` 这类残句):
|
|
339
|
+
* - 没有类别 ⇒ 整段 `({{categories}})` 删掉,`{{categoryCount}}` 至少为 1;
|
|
340
|
+
* - 没有发起人 ⇒ 整段 `{{creators}}发起。` / `{{creators}}发起,` 删掉。
|
|
341
|
+
*/
|
|
342
|
+
export function renderGroupSummaryTemplate(input) {
|
|
343
|
+
let text = String(input.template ?? '');
|
|
344
|
+
if (!text.trim())
|
|
345
|
+
return '';
|
|
346
|
+
const allCategories = uniqueTexts(input.categories ?? []);
|
|
347
|
+
const shownCategories = allCategories.slice(0, 3);
|
|
348
|
+
const categoryCount = allCategories.length > 0 ? allCategories.length : 1;
|
|
349
|
+
const categories = shownCategories.length > 0
|
|
350
|
+
? `${shownCategories.join('、')}${allCategories.length > shownCategories.length ? '等' : ''}`
|
|
351
|
+
: '';
|
|
352
|
+
const allCreators = normalizeCreators(input.creators ?? []);
|
|
353
|
+
const shownCreators = allCreators.slice(0, 3);
|
|
354
|
+
const creators = shownCreators.length > 0
|
|
355
|
+
? `由${shownCreators.join('、')}${allCreators.length > 2 ? '等' : ''}`
|
|
356
|
+
: '';
|
|
357
|
+
if (!creators) {
|
|
358
|
+
text = text.replace(/\{\{creators\}\}发起[。,]/gu, '');
|
|
359
|
+
}
|
|
360
|
+
if (!categories) {
|
|
361
|
+
text = text.replace(/(\{\{categories\}\})/gu, '');
|
|
362
|
+
}
|
|
363
|
+
const amounts = (input.amounts ?? []).filter((value) => typeof value === 'number' && Number.isFinite(value));
|
|
364
|
+
const replaced = text
|
|
365
|
+
.replace(/\{\{count\}\}/gu, String(input.count))
|
|
366
|
+
.replace(/\{\{restCount\}\}/gu, String(input.restCount ?? 0))
|
|
367
|
+
.replace(/\{\{categoryCount\}\}/gu, String(categoryCount))
|
|
368
|
+
.replace(/\{\{categories\}\}/gu, categories)
|
|
369
|
+
.replace(/\{\{min\}\}/gu, amounts.length > 0 ? formatNumber(Math.min(...amounts)) : '')
|
|
370
|
+
.replace(/\{\{max\}\}/gu, amounts.length > 0 ? formatNumber(Math.max(...amounts)) : '')
|
|
371
|
+
.replace(/\{\{sum\}\}/gu, amounts.length > 0
|
|
372
|
+
? formatNumber(Number(amounts.reduce((total, value) => total + value, 0).toFixed(2)))
|
|
373
|
+
: '')
|
|
374
|
+
.replace(/\{\{creators\}\}/gu, creators)
|
|
375
|
+
.replace(/\{\{tail\}\}/gu, input.tail);
|
|
376
|
+
// 空占位没有可填内容时,别把 `¥` / 逗号留在句子里
|
|
377
|
+
return replaced.replace(/¥(\s*)/gu, '').replace(/\s*,\s*,/gu, ',').trim();
|
|
378
|
+
}
|