weaver-work-cli 0.1.40 → 0.1.42
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/chunks/{archive-JHTONRST.js → archive-J4I6MCKF.js} +2 -2
- package/dist/chunks/{archive-oa-FCHLCPHC.js → archive-oa-BRZKKTO4.js} +2 -2
- package/dist/chunks/{asset-RLIHC7DB.js → asset-L7D4FAOZ.js} +2 -2
- package/dist/chunks/{asset-idle-KVOFTLCP.js → asset-idle-DADLLV75.js} +2 -2
- package/dist/chunks/{attachment-content-RZYDB3MB.js → attachment-content-K4C7VLWQ.js} +3 -3
- package/dist/chunks/{blog-42OZTJNP.js → blog-BVGQ7NZL.js} +2 -2
- package/dist/chunks/{caizhilian-KRK547K5.js → caizhilian-SAADNFD6.js} +2 -2
- package/dist/chunks/{calendar-RTWAMKHD.js → calendar-Q7NQY5Z2.js} +61 -23
- package/dist/chunks/{chunk-LFMSDBZN.js → chunk-3KTFS4IA.js} +308 -153
- package/dist/chunks/{chunk-G2JSIUWF.js → chunk-4PD5NWAX.js} +89 -0
- package/dist/chunks/{chunk-T6EGVBKU.js → chunk-4RAE6JXP.js} +1 -1
- package/dist/chunks/{chunk-7BRU4EFH.js → chunk-72CKAUAG.js} +29 -2
- package/dist/chunks/{chunk-NETCCARD.js → chunk-APVFJUQH.js} +2 -2
- package/dist/chunks/{chunk-CAQNBBQA.js → chunk-BBKDXQEC.js} +2 -2
- package/dist/chunks/{chunk-57IECMWX.js → chunk-BIPYLLVS.js} +26 -12
- package/dist/chunks/{chunk-LN447PY2.js → chunk-EKWAQSLU.js} +3 -2
- package/dist/chunks/{chunk-4TEDNCON.js → chunk-FOAA27U5.js} +338 -52
- package/dist/chunks/{chunk-7U577Q7U.js → chunk-G4UR6NXN.js} +2 -2
- package/dist/chunks/{chunk-NJKREDA3.js → chunk-GEWYMQME.js} +1 -1
- package/dist/chunks/{chunk-I566ZYNF.js → chunk-GHXCDPTM.js} +1 -1
- package/dist/chunks/{chunk-4CTESSKI.js → chunk-IZ7S7QUS.js} +4 -4
- package/dist/chunks/{chunk-VNBU55SL.js → chunk-JPRO6ER2.js} +57 -2432
- package/dist/chunks/{chunk-IZSSVYAG.js → chunk-LADPYOWC.js} +2 -2
- package/dist/chunks/chunk-LDZQPUIA.js +1238 -0
- package/dist/chunks/chunk-NKZRQXST.js +2546 -0
- package/dist/chunks/{chunk-JWRZM2H2.js → chunk-QKNKR4RH.js} +3 -3
- package/dist/chunks/{chunk-FLS4DPGP.js → chunk-R4CHUSKC.js} +28 -15
- package/dist/chunks/{chunk-WFFNM65B.js → chunk-SA6VBIHY.js} +73 -19
- package/dist/chunks/{chunk-SPRHHSVC.js → chunk-TTPJJJC4.js} +2 -2
- package/dist/chunks/{chunk-7E6MUOEK.js → chunk-W7BEHOSR.js} +2 -2
- package/dist/chunks/{chunk-CXRQWOZ5.js → chunk-YRSZQUD2.js} +2 -2
- package/dist/chunks/{conflict-VR55AOOO.js → conflict-F2OMZRTO.js} +2 -2
- package/dist/chunks/{contract-HI4FNPYE.js → contract-5BETHRSG.js} +2 -2
- package/dist/chunks/{cowork-HJLI353D.js → cowork-3MN6EQQO.js} +22 -12
- package/dist/chunks/create-flow-5FQNNMVO.js +55 -0
- package/dist/chunks/{ebuilder-form-HTGI63UU.js → ebuilder-form-SBHOYBUD.js} +3 -3
- package/dist/chunks/{ehr-GYXIYH3A.js → ehr-KMOJULKG.js} +432 -1234
- package/dist/chunks/{env-DW3G4HUO.js → env-LB7NBL2O.js} +2 -2
- package/dist/chunks/{esb-I5INCEFE.js → esb-W2PJCFCX.js} +4 -4
- package/dist/chunks/{factory-TKXGXS6L.js → factory-6ZFQXQ46.js} +1 -1
- package/dist/chunks/{fna-K2MHTQ24.js → fna-ZQPCUV4R.js} +2 -2
- package/dist/chunks/{group-chat-VYBNMJLG.js → group-chat-2HHAJN46.js} +5 -3
- package/dist/chunks/{host-ZUQTJYYK.js → host-4CBHGOHT.js} +3 -3
- package/dist/chunks/{host-D5SERMAJ.js → host-ATKVNZAT.js} +3 -3
- package/dist/chunks/{host-JM4YVHFM.js → host-BQH22NTQ.js} +3 -3
- package/dist/chunks/{host-L3VLITMT.js → host-X2CSLFM6.js} +3 -3
- package/dist/chunks/{host-256GVCAH.js → host-YTODKMSL.js} +3 -3
- package/dist/chunks/{hrm-VLRBKTZB.js → hrm-FFM4UJFS.js} +4 -4
- package/dist/chunks/{im-QK3VTLXK.js → im-VEHKDQPB.js} +7 -7
- package/dist/chunks/{invoice-T5JTA32H.js → invoice-LVIKSIIF.js} +3 -3
- package/dist/chunks/{jiuchuanhui-P2FNEQVZ.js → jiuchuanhui-X6DINO5P.js} +2 -2
- package/dist/chunks/{lazy-dispatch-XRRI46M6.js → lazy-dispatch-KSWTN5L6.js} +4 -4
- package/dist/chunks/{mail-3Y7MJ6PS.js → mail-VDKRGUD3.js} +2 -2
- package/dist/chunks/{meeting-M7LTXGJE.js → meeting-CLC6HP3Z.js} +12 -11
- package/dist/chunks/{okr-OBC6S67L.js → okr-CHG4OO3U.js} +2 -2
- package/dist/chunks/{plan-5H6I53SK.js → plan-XWR75667.js} +2 -2
- package/dist/chunks/{project-L75QZV4S.js → project-5BXOOKMG.js} +40 -23
- package/dist/chunks/recruit-stage-DGMFNNDE.js +4507 -0
- package/dist/chunks/{registry-RXPN6BBT.js → registry-7FFOXIAW.js} +4 -4
- package/dist/chunks/{registry-KMHCPAAD.js → registry-BKBZPBAJ.js} +10 -9
- package/dist/chunks/{registry-QGHFNUWS.js → registry-OZSMDVPP.js} +2 -2
- package/dist/chunks/{registry-UGRBCBHO.js → registry-RHMW6ANW.js} +2 -2
- package/dist/chunks/{registry-NYPYMZXG.js → registry-Z43434VG.js} +1 -1
- package/dist/chunks/{room-recommend-A3UDC3CA.js → room-recommend-62H7ZQQ6.js} +4 -4
- package/dist/chunks/{schedule-5GCACCK7.js → schedule-FNXKH3DQ.js} +4 -4
- package/dist/chunks/{workflow-M5ZJ7DWF.js → workflow-ADHKCAJK.js} +6 -6
- package/dist/index.js +33 -28
- package/dist/internal/e10/auth/commands.js +1 -0
- package/dist/internal/e10/auth/messages.js +6 -0
- package/dist/internal/e10/auth/session.js +17 -0
- package/dist/internal/e10/auth/xiaoe.js +286 -44
- package/dist/internal/e10/auth-recovery.js +36 -5
- package/dist/internal/e10/context.js +56 -12
- package/dist/internal/e10/index.js +1 -1
- package/dist/internal/e10/network-error.js +93 -0
- package/dist/internal/e10/request-runtime.js +6 -0
- package/dist/shortcuts/calendar/operations/read.js +31 -22
- package/dist/shortcuts/calendar/operations/write.js +45 -1
- package/dist/shortcuts/cowork/operations/discuss-write.js +5 -1
- package/dist/shortcuts/cowork/operations/item-write.js +1 -7
- package/dist/shortcuts/cowork/operations/member-write.js +3 -1
- package/dist/shortcuts/cowork/operations/permission-write.js +5 -1
- package/dist/shortcuts/cowork/operations/shared.js +7 -0
- package/dist/shortcuts/ehr/operations/attend-write.js +43 -1
- package/dist/shortcuts/ehr/operations/hr-contact.js +7 -4
- package/dist/shortcuts/ehr/operations/hr-report.js +13 -7
- package/dist/shortcuts/ehr/operations/recruit-candidate-import.js +134 -59
- package/dist/shortcuts/ehr/operations/recruit-write.js +262 -17
- package/dist/shortcuts/ehr/operations/recruit.js +10 -4
- package/dist/shortcuts/ehr/operations/shared.js +47 -0
- package/dist/shortcuts/hrm/operations/org.js +118 -0
- package/dist/shortcuts/index.js +5 -0
- package/dist/shortcuts/meeting/operations/eb/catalog.js +4 -1
- package/dist/shortcuts/meeting/operations/eb/create-flow.js +85 -28
- package/dist/shortcuts/meeting/operations/eb/factory.js +33 -0
- package/dist/shortcuts/meeting/operations/eb/group-chat.js +25 -3
- package/dist/shortcuts/meeting/operations/eb/person-cache.js +96 -0
- package/dist/shortcuts/meeting/operations/eb/room-recommend.js +33 -11
- package/dist/shortcuts/project/operations/read.js +6 -25
- package/dist/shortcuts/project/operations/shared.js +22 -0
- package/dist/shortcuts/project/operations/write.js +28 -2
- package/dist/shortcuts/recruit-stage/context.js +119 -0
- package/dist/shortcuts/recruit-stage/continuation.js +73 -0
- package/dist/shortcuts/recruit-stage/errors.js +65 -0
- package/dist/shortcuts/recruit-stage/host.js +109 -0
- package/dist/shortcuts/recruit-stage/index.js +115 -0
- package/dist/shortcuts/recruit-stage/manifest.js +29 -0
- package/dist/shortcuts/recruit-stage/operations/interview-arrange.js +722 -0
- package/dist/shortcuts/recruit-stage/operations/interview-feedback.js +269 -0
- package/dist/shortcuts/recruit-stage/operations/jd-create.js +630 -0
- package/dist/shortcuts/recruit-stage/operations/list.js +431 -0
- package/dist/shortcuts/recruit-stage/operations/registry.js +38 -0
- package/dist/shortcuts/recruit-stage/operations/resume-import.js +1198 -0
- package/dist/shortcuts/recruit-stage/operations/search.js +158 -0
- package/dist/shortcuts/recruit-stage/operations/types.js +25 -0
- package/dist/shortcuts/recruit-stage/school-tiers.json +318 -0
- package/dist/shortcuts/recruit-stage/shared.js +608 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/detailFieldRules.js +4 -2
- package/dist/shortcuts/workflow/operations/todoSmartConvert/summaryCache.js +13 -6
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoDetailCache.js +17 -24
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoDetailFetcher.js +14 -9
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.collect.js +50 -8
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.core.js +2 -1
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.detail.js +17 -11
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.group.js +7 -39
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.groupSummary.js +3 -2
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.js +51 -15
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.pipeline.js +16 -1
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.smartGroup.js +151 -63
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.subGroup.js +56 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.summary.js +39 -14
- package/docs/e10-auth.md +7 -1
- package/docs/operation-manual.md +8 -0
- package/package.json +1 -1
- package/scripts/install-skill-to-xiaoe.mjs +105 -0
- package/scripts/package-skill.mjs +4 -2
- package/skill-template/business-info.json +24 -0
- package/skill-template/domains/recruit-stage.md +29 -0
- package/skills/weaver-e10-calendar/SKILL.md +2 -1
- package/skills/weaver-e10-calendar/references/calendar-write.md +54 -0
- package/skills/weaver-e10-cowork/SKILL.md +1 -1
- package/skills/weaver-e10-cowork/references/cowork-write.md +3 -1
- package/skills/weaver-e10-jucailin/SKILL.md +1 -1
- package/skills/weaver-e10-jucailin/references/attend-write.md +8 -0
- package/skills/weaver-e10-jucailin/references/hr-contact.md +8 -0
- package/skills/weaver-e10-jucailin/references/hr-report.md +8 -0
- package/skills/weaver-e10-jucailin/references/recruit-create.md +2 -2
- package/skills/weaver-e10-jucailin/references/recruit-read.md +1 -1
- package/skills/weaver-e10-jucailin/references/recruit-write.md +4 -3
- package/skills/weaver-e10-mail/SKILL.md +1 -1
- package/skills/weaver-e10-meeting/SKILL.md +32 -2
- package/skills/weaver-e10-meeting.zip +0 -0
- package/skills/weaver-e10-recruit-stage/SKILL.md +226 -0
- package/skills/weaver-e10-recruit-stage/agents/openai.yaml +119 -0
- package/skills/weaver-e10-recruit-stage/lib/recruit-xiaoe-file.mjs +188 -0
- package/skills/weaver-e10-recruit-stage/references/recruit-arrange-interview.md +322 -0
- package/skills/weaver-e10-recruit-stage/references/recruit-capability-check.md +127 -0
- package/skills/weaver-e10-recruit-stage/references/recruit-check-install.md +65 -0
- package/skills/weaver-e10-recruit-stage/references/recruit-detail-process.md +63 -0
- package/skills/weaver-e10-recruit-stage/references/recruit-edit-interview-feedback.md +142 -0
- package/skills/weaver-e10-recruit-stage/references/recruit-jd-create.md +73 -0
- package/skills/weaver-e10-recruit-stage/references/recruit-jd-template.md +56 -0
- package/skills/weaver-e10-recruit-stage/references/recruit-list-candidate.md +101 -0
- package/skills/weaver-e10-recruit-stage/references/recruit-list-interview.md +148 -0
- package/skills/weaver-e10-recruit-stage/references/recruit-match-candidates.md +77 -0
- package/skills/weaver-e10-recruit-stage/references/recruit-output-links.md +59 -0
- package/skills/weaver-e10-recruit-stage/references/recruit-resolve-creator.md +69 -0
- package/skills/weaver-e10-recruit-stage/references/recruit-resolve-metadata.md +241 -0
- package/skills/weaver-e10-recruit-stage/references/recruit-resume-import.md +438 -0
- package/skills/weaver-e10-recruit-stage/references/recruit-resume-parse.md +128 -0
- package/skills/weaver-e10-recruit-stage/references/recruit-search-candidate.md +146 -0
- package/skills/weaver-e10-recruit-stage/references/recruit-urge-feedback.md +69 -0
- package/skills/weaver-e10-shared/SKILL.md +1 -1
- package/skills/weaver-e10-shared/references/e10-auth-and-session.md +4 -2
- package/skills/weaver-e10-shijingran/SKILL.md +3 -2
- package/skills/weaver-e10-shijingran/references/operations.md +3 -1
- package/skills/weaver-e10-shijingran/references/source-manifest.json +41 -40
- package/skills/weaver-e10-shijingran/references/write-operations.md +2 -0
- package/skills/weaver-e10-workflow/SKILL.md +4 -2
- package/skills/weaver-e10-workflow/aigroup/default.json +11 -9
- package/skills/weaver-e10-workflow/references/todo-smart-convert.md +23 -13
- package/skills/weaver-e10-workflow/tools/todo-summary.cjs +62 -20
- package/skills/weaver-e10-yepiaotong/SKILL.md +9 -10
- package/skills/weaver-e10-yepiaotong/references/invoice-agent-entry.md +6 -3
- package/dist/chunks/{chunk-E7OK5WD5.js → chunk-2LV74IWP.js} +5 -5
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { workflowValidationError } from '../../errors.js';
|
|
2
2
|
import { renderGroupSummaryTemplate, } from './businessRules.js';
|
|
3
3
|
import { asRecord, readBoundedInt, } from './todoSmartConvert.core.js';
|
|
4
|
-
import { PRIORITY_GROUP_ID, PRIORITY_GROUP_TITLE, runGroup, GROUP_MIN_ITEMS_FOR_SUBGROUP, } from './todoSmartConvert.group.js';
|
|
4
|
+
import { PRIORITY_GROUP_ID, PRIORITY_GROUP_TITLE, runGroup, GROUP_MIN_ITEMS_FOR_SUBGROUP, selectPriorityRows, sortPriorityRows, } from './todoSmartConvert.group.js';
|
|
5
5
|
import { fetchDetailFacts, stampsOfRows } from './todoSmartConvert.detail.js';
|
|
6
|
-
import { activeRuleFor, evaluateFocusKeywords, primeRules } from './aigroupRules.js';
|
|
6
|
+
import { activeRuleFor, evaluateFocusKeywords, generalRules, primeRules } from './aigroupRules.js';
|
|
7
7
|
import { openSummaryCache, promptTagOf } from './summaryCache.js';
|
|
8
8
|
import { openDetailCache, detailMaterialTagOf } from './todoDetailCache.js';
|
|
9
9
|
const DEFAULT_REQUEST_SUMMARY_MAX = 50;
|
|
@@ -19,6 +19,54 @@ export function rowsOfGroup(group) {
|
|
|
19
19
|
export function groupItemsOf(group) {
|
|
20
20
|
return group.displayItems.filter((item) => item.type === 'group');
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* 最终结构上的「建议优先处理的流程」置顶视图(2026-11-03 用户定:改到**最后一步**生成)。
|
|
24
|
+
*
|
|
25
|
+
* - 输入是**成品结构**(单条摘要、组摘要都已回填):按同一条规则(`generalRules.priorityGroup`)
|
|
26
|
+
* 从各工作流分组的 `displayItems` 里摊平条目、筛出「超时 / 紧急程度」命中项,复制一份成
|
|
27
|
+
* `id=priority`、`unshift` 到首位;
|
|
28
|
+
* - **幂等**:输入里已有 priority 组时先剔除再重建,stage 串联多次调用结果一致;
|
|
29
|
+
* - 叠加视图语义不变:入选条目仍保留在原工作流分组里 ⇒ `Σcount` 仍大于 `totalCount`
|
|
30
|
+
* (`totalCount` 始终按去重条数计);
|
|
31
|
+
* - 判定字段(`overTime` / `userOverTime` / `requestLevel`)都在 `requestData` 上,不必回查列表;
|
|
32
|
+
* 原排序用的 `receivedTime` 不在 requestData 中,同档退化为按 `requestId` 稳定排序。
|
|
33
|
+
*/
|
|
34
|
+
export function assemblePriorityGroup(groups) {
|
|
35
|
+
const base = (groups ?? []).filter((group) => group.id !== PRIORITY_GROUP_ID && group.topPriority !== true);
|
|
36
|
+
const pgConfig = generalRules()?.priorityGroup ?? null;
|
|
37
|
+
const title = pgConfig?.title?.trim() || PRIORITY_GROUP_TITLE;
|
|
38
|
+
const rules = new Set(pgConfig?.rules ?? ['overdue', 'levelFallback']);
|
|
39
|
+
const rows = base.flatMap((group) => rowsOfGroup(group));
|
|
40
|
+
if (rows.length === 0)
|
|
41
|
+
return { groups: base };
|
|
42
|
+
const selection = selectPriorityRows(rows, rules);
|
|
43
|
+
const selected = sortPriorityRows(selection.rows);
|
|
44
|
+
if (selected.length === 0) {
|
|
45
|
+
return { groups: base, warning: `本次没有超时条目,也没有 requestLevel≥1 的条目,故不输出「${title}」分组` };
|
|
46
|
+
}
|
|
47
|
+
const levelText = selection.targetLevel === 2
|
|
48
|
+
? '紧急(requestLevel=2)'
|
|
49
|
+
: selection.targetLevel === 1
|
|
50
|
+
? '重要(requestLevel=1;本次无 2 级数据,已按规则降级取 1,0 级不取)'
|
|
51
|
+
: '无等级(本次整批没有 ≥1 级的条目,只按超时筛)';
|
|
52
|
+
const priorityGroup = {
|
|
53
|
+
id: PRIORITY_GROUP_ID,
|
|
54
|
+
topPriority: true,
|
|
55
|
+
title,
|
|
56
|
+
count: selected.length,
|
|
57
|
+
// 大分组(含优先组)默认展开;只有汇总项收缩
|
|
58
|
+
defaultExpanded: true,
|
|
59
|
+
// 复制一份:优先组是独立视图,确保两边互不影响
|
|
60
|
+
displayItems: requestItemsOf(selected.map((row) => ({ ...row }))),
|
|
61
|
+
};
|
|
62
|
+
return {
|
|
63
|
+
groups: [priorityGroup, ...base],
|
|
64
|
+
warning: `「${title}」筛出 ${selected.length} 条:超时 ${selection.overdueCount} 条 / `
|
|
65
|
+
+ `等级命中 ${selection.levelCount} 条(两者都满足 ${selection.bothCount} 条),等级口径=${levelText};`
|
|
66
|
+
+ `这 ${selected.length} 条同时保留在各自工作流分组中(叠加视图:Σcount 会大于 totalCount,`
|
|
67
|
+
+ 'totalCount 按去重条数计)',
|
|
68
|
+
};
|
|
69
|
+
}
|
|
22
70
|
/**
|
|
23
71
|
* 校验并索引 `aiGroups`。
|
|
24
72
|
*
|
|
@@ -40,10 +88,11 @@ export function readAiGroups(value) {
|
|
|
40
88
|
if (!record) {
|
|
41
89
|
throw workflowValidationError('ai_groups_invalid', `${label} 必须是对象 { groupId, title, requestIds }`, { retryAllowed: false });
|
|
42
90
|
}
|
|
43
|
-
const unknownKeys = Object.keys(record).filter((key) => !['groupId', 'title', 'requestIds'].includes(key));
|
|
91
|
+
const unknownKeys = Object.keys(record).filter((key) => !['groupId', 'title', 'requestIds', 'summary'].includes(key));
|
|
44
92
|
if (unknownKeys.length > 0) {
|
|
45
|
-
throw workflowValidationError('ai_groups_invalid', `${label} 不接受字段:${unknownKeys.join(', ')};只支持 groupId / title / requestIds`, { retryAllowed: false, details: { unknownKeys } });
|
|
93
|
+
throw workflowValidationError('ai_groups_invalid', `${label} 不接受字段:${unknownKeys.join(', ')};只支持 groupId / title / requestIds / summary`, { retryAllowed: false, details: { unknownKeys } });
|
|
46
94
|
}
|
|
95
|
+
const planSummary = typeof record.summary === 'string' ? record.summary.trim() : '';
|
|
47
96
|
const groupId = typeof record.groupId === 'string' ? record.groupId.trim() : '';
|
|
48
97
|
if (!groupId) {
|
|
49
98
|
throw workflowValidationError('ai_groups_invalid', `${label}.groupId 必填且必须是字符串(19 位 workflowId 用数字会失真;值取 stage=group 的 wfIdGroup[i].id)`, { retryAllowed: false });
|
|
@@ -65,7 +114,12 @@ export function readAiGroups(value) {
|
|
|
65
114
|
ids.push(id);
|
|
66
115
|
}
|
|
67
116
|
const bucket = map.get(groupId) ?? [];
|
|
68
|
-
bucket.push({
|
|
117
|
+
bucket.push({
|
|
118
|
+
groupId,
|
|
119
|
+
title,
|
|
120
|
+
requestIds: [...new Set(ids)],
|
|
121
|
+
...(planSummary ? { summary: planSummary } : {}),
|
|
122
|
+
});
|
|
69
123
|
map.set(groupId, bucket);
|
|
70
124
|
});
|
|
71
125
|
return map;
|
|
@@ -159,48 +213,29 @@ function rowKeyForCount(row) {
|
|
|
159
213
|
return `id:${id}`;
|
|
160
214
|
return `tx:${row.requestNameTitle ?? ''}|${row.creater ?? ''}|${row.nodeId ?? ''}`;
|
|
161
215
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
:
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
};
|
|
186
|
-
const cachedIds = new Set();
|
|
187
|
-
const allRows = [];
|
|
188
|
-
for (const group of groups) {
|
|
189
|
-
for (const row of group.requestDatas ?? []) {
|
|
190
|
-
allRows.push(row);
|
|
191
|
-
if (summaryCache.get(row.requestId, row.lastFeedBackDateTime, summaryMaxChars, promptTagOf(activeRuleFor(group.title)?.requestSummaryPrompt ?? ''), materialTagOfRow(row.requestId, row.lastFeedBackDateTime)))
|
|
192
|
-
cachedIds.add(row.requestId);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
// 详情缓存已经按 `requestId__lastFeedBackDateTime` 落盘,命中时零请求;
|
|
196
|
-
// 所以「摘要命中」不再等于「可以不拉详情」——否则阈值分组会因为没有指标而失效(回归 10.2)。
|
|
197
|
-
const detailIds = [...new Set(allRows.map((row) => row.requestId).filter(Boolean))];
|
|
198
|
-
// stamps 决定详情缓存的键:不传就永远 miss(缓存白建)。
|
|
199
|
-
const stamps = stampsOfRows(allRows);
|
|
200
|
-
const facts = skipDetail
|
|
201
|
-
? new Map()
|
|
202
|
-
: (options.facts ?? (await fetchDetailFacts(env, detailIds, args, stamps)).facts);
|
|
203
|
-
const metrics = skipDetail ? undefined : (options.metrics ?? metricsOfFacts(facts));
|
|
216
|
+
/** 平铺结构:不做 ai 二次分组,`displayItems` 全是单条。 */
|
|
217
|
+
export function flattenFinalGroups(groups) {
|
|
218
|
+
return groups.map((group) => ({
|
|
219
|
+
id: group.id,
|
|
220
|
+
topPriority: group.id === PRIORITY_GROUP_ID,
|
|
221
|
+
title: group.title,
|
|
222
|
+
count: group.count,
|
|
223
|
+
defaultExpanded: true,
|
|
224
|
+
displayItems: requestItemsOf(group.requestDatas),
|
|
225
|
+
}));
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* 在**已有结构**上应用 aigroup 规则的 ai 二次分组(2026-11-03 解耦)。
|
|
229
|
+
*
|
|
230
|
+
* 输入是首次 workflowName 分组产出的平铺结构(条目上可能已经带着单条摘要),
|
|
231
|
+
* 输出带汇总项的结构;既不重新分组、也不碰详情缓存,纯粹做「组内切子组」。
|
|
232
|
+
*/
|
|
233
|
+
export function applySubGroups(groups, options) {
|
|
234
|
+
const plans = options.plans;
|
|
235
|
+
const facts = options.facts;
|
|
236
|
+
const metrics = options.metrics;
|
|
237
|
+
const warnings = options.warnings;
|
|
238
|
+
// 计划本身的合法性:未知 groupId / 对优先组做二次分组,都要硬失败(2026-11-03 迁移到本阶段)。
|
|
204
239
|
const knownGroupIds = new Set(groups.map((group) => group.id));
|
|
205
240
|
for (const groupId of plans?.keys() ?? []) {
|
|
206
241
|
if (!knownGroupIds.has(groupId)) {
|
|
@@ -210,18 +245,6 @@ export async function runSmartGroup(args, env, options = {}) {
|
|
|
210
245
|
throw workflowValidationError('ai_group_priority_unsupported', `「${PRIORITY_GROUP_TITLE}」不做二次分组:目标结构里它只有 displayItems(全是单条展示),没有汇总项;请把它的条目从 aiGroups 里去掉`, { retryAllowed: true });
|
|
211
246
|
}
|
|
212
247
|
}
|
|
213
|
-
// markdown 是中间态的东西,最终结构里没有,相关提示一并丢掉,免得误导调用方。
|
|
214
|
-
const warnings = (base.warnings ?? [])
|
|
215
|
-
.filter((text) => !text.startsWith('markdown 只展示前'));
|
|
216
|
-
// aigroup 规则(default.json + user-intents.json):每个 operation 只加载一次。
|
|
217
|
-
warnings.push(...primeRules(env.ctx).warnings);
|
|
218
|
-
if (skipDetail) {
|
|
219
|
-
warnings.push('withDetailContext=false:本次未拉取任何详情——请假/财务阈值改为按标题识别、跳过整组空判定,也不产 summaryBatches');
|
|
220
|
-
}
|
|
221
|
-
else if (cachedIds.size > 0) {
|
|
222
|
-
warnings.push(`有 ${cachedIds.size} 条命中本地摘要缓存(这些条目不用再调 AI);`
|
|
223
|
-
+ '详情仍按缓存优先取一次——阈值指标只认流程详情数据,跳过详情会让阈值分组失效');
|
|
224
|
-
}
|
|
225
248
|
const finalGroups = groups.map((group) => {
|
|
226
249
|
if (group.id === PRIORITY_GROUP_ID) {
|
|
227
250
|
return {
|
|
@@ -358,7 +381,11 @@ export async function runSmartGroup(args, env, options = {}) {
|
|
|
358
381
|
}
|
|
359
382
|
groupRows = kept;
|
|
360
383
|
}
|
|
361
|
-
subGroups.push({
|
|
384
|
+
subGroups.push({
|
|
385
|
+
title: plan.title,
|
|
386
|
+
requestDatas: groupRows,
|
|
387
|
+
...(plan.summary ? { summary: plan.summary } : {}),
|
|
388
|
+
});
|
|
362
389
|
}
|
|
363
390
|
// 子组 <2 不成立(含有意图的子组):解散,条目平铺(Q5)。
|
|
364
391
|
const kept = subGroups.filter((sub) => sub.requestDatas.length >= 2);
|
|
@@ -400,6 +427,10 @@ export async function runSmartGroup(args, env, options = {}) {
|
|
|
400
427
|
}
|
|
401
428
|
/** 组摘要:规则 `needAI:false` 时用 JSON `template` 渲染,否则留空交 AI。 */
|
|
402
429
|
const renderSubGroupSummary = (sub) => {
|
|
430
|
+
// ① 模型随 aiGroups 一起给的摘要优先(同一次交互产出,见 AiSmartGroupPlan.summary)
|
|
431
|
+
if ((sub.summary ?? '').trim())
|
|
432
|
+
return sub.summary;
|
|
433
|
+
// ② 否则走规则里的固定模板(needAI:false 的确定性组)
|
|
403
434
|
const groupSummary = rule.aiGroup?.groupSummary;
|
|
404
435
|
if (!groupSummary || groupSummary.needAI || !groupSummary.template)
|
|
405
436
|
return '';
|
|
@@ -449,10 +480,67 @@ export async function runSmartGroup(args, env, options = {}) {
|
|
|
449
480
|
displayItems: [...groupItems, ...requestItemsOf(singles)],
|
|
450
481
|
};
|
|
451
482
|
});
|
|
483
|
+
return finalGroups;
|
|
484
|
+
}
|
|
485
|
+
export async function runSmartGroup(args, env, options = {}) {
|
|
486
|
+
const base = runGroup(args);
|
|
487
|
+
const groups = (base.wfIdGroup ?? []);
|
|
488
|
+
/** `null` = 确定性模式;数组(可能为空)= 兼容模式 */
|
|
489
|
+
const plans = args.aiGroups === undefined || args.aiGroups === null
|
|
490
|
+
? undefined
|
|
491
|
+
: readAiGroups(args.aiGroups);
|
|
492
|
+
// 注意:`aiGroups: []` 与「未传」现在等价(默认不分组),不再需要用空数组区分语义。
|
|
493
|
+
// 有侧重点时分组 = 标题语义 + 侧重点;子组顺序沿用模型返回顺序。
|
|
494
|
+
// 无侧重点时普通组按条数降序,请假/财务使用阈值规则。
|
|
495
|
+
const groupFocus = typeof args.groupFocus === 'string' ? args.groupFocus.trim() : '';
|
|
496
|
+
const hasFocus = groupFocus.length > 0;
|
|
497
|
+
// 摘要字数口径:缓存命中要按口径过滤(10 字要求不能吃 50 字的旧摘要)。
|
|
498
|
+
const summaryMaxChars = readBoundedInt(args.summaryMaxChars, "summaryMaxChars", DEFAULT_REQUEST_SUMMARY_MAX, 10, 500);
|
|
499
|
+
// 详情用来算阈值指标 + 判定空上下文;摘要缓存只省 AI,不省详情(阈值指标只认流程详情数据)。
|
|
500
|
+
// withDetailContext=false 时只生成结构、不获取详情(此时没有指标,阈值规则自然不生效)。
|
|
501
|
+
const skipDetail = args.withDetailContext === false;
|
|
502
|
+
const summaryCache = await openSummaryCache(env.ctx);
|
|
503
|
+
// 摘要条目的素材特征值:详情素材变了 ⇒ 旧摘要视为不存在(该条重新写摘要)
|
|
504
|
+
const detailCacheForTag = skipDetail ? null : await openDetailCache(env.ctx);
|
|
505
|
+
const materialTagOfRow = (requestId, stamp) => {
|
|
506
|
+
const entry = detailCacheForTag?.get(requestId, stamp);
|
|
507
|
+
return entry ? detailMaterialTagOf(entry) : undefined;
|
|
508
|
+
};
|
|
509
|
+
const cachedIds = new Set();
|
|
510
|
+
const allRows = [];
|
|
511
|
+
for (const group of groups) {
|
|
512
|
+
for (const row of group.requestDatas ?? []) {
|
|
513
|
+
allRows.push(row);
|
|
514
|
+
if (summaryCache.get(row.requestId, row.lastFeedBackDateTime, summaryMaxChars, promptTagOf(activeRuleFor(group.title)?.requestSummaryPrompt ?? ''), materialTagOfRow(row.requestId, row.lastFeedBackDateTime)))
|
|
515
|
+
cachedIds.add(row.requestId);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
// 详情缓存已经按 `requestId__lastFeedBackDateTime` 落盘,命中时零请求;
|
|
519
|
+
// 所以「摘要命中」不再等于「可以不拉详情」——否则阈值分组会因为没有指标而失效(回归 10.2)。
|
|
520
|
+
const detailIds = [...new Set(allRows.map((row) => row.requestId).filter(Boolean))];
|
|
521
|
+
// stamps 决定详情缓存的键:不传就永远 miss(缓存白建)。
|
|
522
|
+
const stamps = stampsOfRows(allRows);
|
|
523
|
+
const facts = skipDetail
|
|
524
|
+
? new Map()
|
|
525
|
+
: (options.facts ?? (await fetchDetailFacts(env, detailIds, args, stamps)).facts);
|
|
526
|
+
// markdown 是中间态的东西,最终结构里没有,相关提示一并丢掉,免得误导调用方。
|
|
527
|
+
const warnings = (base.warnings ?? [])
|
|
528
|
+
.filter((text) => !text.startsWith('markdown 只展示前'));
|
|
529
|
+
// aigroup 规则(default.json + user-intents.json):每个 operation 只加载一次。
|
|
530
|
+
warnings.push(...primeRules(env.ctx).warnings);
|
|
531
|
+
if (skipDetail) {
|
|
532
|
+
warnings.push('withDetailContext=false:本次未拉取任何详情——请假/财务阈值改为按标题识别、跳过整组空判定,也不产 summaryBatches');
|
|
533
|
+
}
|
|
534
|
+
else if (cachedIds.size > 0) {
|
|
535
|
+
warnings.push(`有 ${cachedIds.size} 条命中本地摘要缓存(这些条目不用再调 AI);`
|
|
536
|
+
+ '详情仍按缓存优先取一次——阈值指标只认流程详情数据,跳过详情会让阈值分组失效');
|
|
537
|
+
}
|
|
452
538
|
// 分组顺序完全沿用第一次 workflowName 分组顺序。
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
539
|
+
// 2026-11-03 用户定:本阶段**只做** workflowName 首次分组,产出平铺 displayItems。
|
|
540
|
+
// ai 二次分组一律由 `stage=subGroup` 调 `applySubGroups()` 完成(旧的就地分组分支已删除)。
|
|
541
|
+
const orderedGroups = flattenFinalGroups(groups);
|
|
542
|
+
if (plans !== undefined) {
|
|
543
|
+
warnings.push('aiGroups 已收到,但本阶段不做二次分组:留到 stage=subGroup 应用');
|
|
456
544
|
}
|
|
457
545
|
return {
|
|
458
546
|
data: {
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { applySubGroups, metricsOfFacts, readAiGroups, rowsOfGroup, totalTodoCount, } from './todoSmartConvert.smartGroup.js';
|
|
2
|
+
import { fetchDetailFacts, stampsOfRows } from './todoSmartConvert.detail.js';
|
|
3
|
+
import { readFinalGroups, readSmartSummaryTitle } from './todoSmartConvert.summary.js';
|
|
4
|
+
/**
|
|
5
|
+
* `stage=subGroup`:**ai 二次分组**(2026-11-03 从 `smartGroup` 拆出来独立成阶段)。
|
|
6
|
+
*
|
|
7
|
+
* 为什么要拆:二次分组曾经和「workflowName 首次分组」绑在同一个 stage 里,导致
|
|
8
|
+
* `focus:false` 的组在**写单条摘要之前**就强制要求模型给出 `aiGroups`——摘要与分组互相耦合。
|
|
9
|
+
* 现在的链路:
|
|
10
|
+
*
|
|
11
|
+
* ```
|
|
12
|
+
* collect → smartGroup(仅首次分组 + 平铺) → summaryContext → summary(单条摘要)
|
|
13
|
+
* → subGroup(本阶段:ai 二次分组) → groupSummaryContext → groupSummary
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* 本阶段只做「组内切子组」:不重新分组、不改摘要、详情全部走缓存(命中率 100%,
|
|
17
|
+
* 因为 collect 已经全量回填)。条目上已有的单条摘要随 `requestDatas` 一起带进子组。
|
|
18
|
+
*/
|
|
19
|
+
export async function runSubGroup(args, env) {
|
|
20
|
+
const groups = await readFinalGroups(args, env);
|
|
21
|
+
const warnings = [];
|
|
22
|
+
// 优先级组是最终结构上的叠加视图,不参与二次分组(导出前由 assemblePriorityGroup 重建)。
|
|
23
|
+
const wfGroups = groups
|
|
24
|
+
.filter((group) => !group.topPriority)
|
|
25
|
+
.map((group) => ({
|
|
26
|
+
id: group.id,
|
|
27
|
+
title: group.title,
|
|
28
|
+
count: group.count,
|
|
29
|
+
requestDatas: rowsOfGroup(group),
|
|
30
|
+
}));
|
|
31
|
+
const plans = args.aiGroups === undefined || args.aiGroups === null
|
|
32
|
+
? undefined
|
|
33
|
+
: readAiGroups(args.aiGroups);
|
|
34
|
+
// 阈值类二次分组(请假天数 / 报销金额 / 计划完成时间)要指标 ⇒ 仍要详情;
|
|
35
|
+
// collect 已全量回填,这里等价于纯缓存命中、零 HTTP。
|
|
36
|
+
const rows = wfGroups.flatMap((group) => group.requestDatas);
|
|
37
|
+
const detailIds = [...new Set(rows.map((row) => row.requestId).filter(Boolean))];
|
|
38
|
+
const facts = new Map();
|
|
39
|
+
let metrics;
|
|
40
|
+
if (args.withDetailContext !== false && detailIds.length > 0) {
|
|
41
|
+
const result = await fetchDetailFacts(env, detailIds, args, stampsOfRows(rows));
|
|
42
|
+
for (const [requestId, fact] of result.facts)
|
|
43
|
+
facts.set(requestId, fact);
|
|
44
|
+
metrics = metricsOfFacts(facts);
|
|
45
|
+
warnings.push(...result.warnings);
|
|
46
|
+
}
|
|
47
|
+
const finalGroups = applySubGroups(wfGroups, { plans, facts, metrics, warnings });
|
|
48
|
+
return {
|
|
49
|
+
data: {
|
|
50
|
+
smartSummaryTitle: readSmartSummaryTitle(args, `您当前有 ${totalTodoCount(groups)} 条待办,清单如下:`),
|
|
51
|
+
totalCount: totalTodoCount(groups),
|
|
52
|
+
wfIdGroup: finalGroups,
|
|
53
|
+
},
|
|
54
|
+
warnings,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -178,7 +178,10 @@ export function collectSummaryUnits(groups) {
|
|
|
178
178
|
continue;
|
|
179
179
|
}
|
|
180
180
|
const kind = detectBusinessKind(group.title);
|
|
181
|
-
|
|
181
|
+
// 规则 `needSummary:false`(default.json)⇒ 整组不进 AI 批次(标题兜底,见 runSummary)。
|
|
182
|
+
// 2026-11-03 用户定:此前这里只认硬编码业务类型(仅满意度被排除),导致规则里配了 false 的组
|
|
183
|
+
// 仍然生成素材、把「待写摘要」虚报成几百条。
|
|
184
|
+
if (!needAiSummary(group.title) || !needsRequestSummary(kind))
|
|
182
185
|
continue;
|
|
183
186
|
for (const item of group.displayItems) {
|
|
184
187
|
if (item.type === 'group') {
|
|
@@ -244,12 +247,14 @@ export async function runSummaryContext(args, env, prefetched) {
|
|
|
244
247
|
* 先建 requestId → 工作流组名 的映射,优先组的行查这张表。
|
|
245
248
|
*/
|
|
246
249
|
const workflowTitleByRequest = new Map();
|
|
247
|
-
|
|
248
|
-
|
|
250
|
+
// 从**全部工作流组**建映射(不能用 units:`needSummary:false` 的组不产 unit,但它们在优先组里
|
|
251
|
+
// 仍有副本,缺了映射就会被当成「建议优先处理的流程」⇒ 落 `*` 兜底 ⇒ 误判成需要 AI)。
|
|
252
|
+
for (const group of groups) {
|
|
253
|
+
if (group.topPriority)
|
|
249
254
|
continue;
|
|
250
|
-
for (const row of
|
|
255
|
+
for (const row of rowsOfGroup(group)) {
|
|
251
256
|
if (row.requestId && !workflowTitleByRequest.has(row.requestId)) {
|
|
252
|
-
workflowTitleByRequest.set(row.requestId,
|
|
257
|
+
workflowTitleByRequest.set(row.requestId, group.title);
|
|
253
258
|
}
|
|
254
259
|
}
|
|
255
260
|
}
|
|
@@ -259,6 +264,10 @@ export async function runSummaryContext(args, env, prefetched) {
|
|
|
259
264
|
for (const unit of units) {
|
|
260
265
|
for (const row of unit.rows) {
|
|
261
266
|
const name = ruleNameForRow(unit, row);
|
|
267
|
+
// 规则 `needSummary:false` ⇒ 不取详情、不进批次,摘要由 `runSummary` 用标题兜底
|
|
268
|
+
// (2026-11-03 用户定:优先组副本此前按组名判定,把这批条目也算成了待写)。
|
|
269
|
+
if (!needAiSummary(name))
|
|
270
|
+
continue;
|
|
262
271
|
const cached = summaryCache.get(row.requestId, row.lastFeedBackDateTime, explicitMaxCharsFor(args, name), promptTagOf(requestPromptFor(name)), materialTagOfRow(row.requestId, row.lastFeedBackDateTime));
|
|
263
272
|
if (cached) {
|
|
264
273
|
row.summary = cached;
|
|
@@ -321,6 +330,16 @@ export async function runSummaryContext(args, env, prefetched) {
|
|
|
321
330
|
// 摘要缓存已命中(上一步预填)⇒ 不再喂 AI
|
|
322
331
|
if (prefilledIds.has(row.requestId))
|
|
323
332
|
continue;
|
|
333
|
+
// 规则 `needSummary:false` ⇒ 标题兜底、不进批次(优先组副本按**原工作流**口径判定)。
|
|
334
|
+
// 2026-11-03:此前只在「详情为空」时才复制标题,规则豁免完全没接,规则 false 的条目照样进批次。
|
|
335
|
+
if (!needAiSummary(ruleNameForRow(unit, row))) {
|
|
336
|
+
if (!(row.summary ?? '').trim()) {
|
|
337
|
+
row.summary = row.requestNameTitle ?? '';
|
|
338
|
+
if ((row.summary ?? '').trim())
|
|
339
|
+
titleCopied += 1;
|
|
340
|
+
}
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
324
343
|
const { context, contextAvailable: available } = provider.get(row.requestId);
|
|
325
344
|
if (!available) {
|
|
326
345
|
// ★ 详情为空 ⇒ 不喂 AI,summary 直接复制标题(确定性操作,不走模型)。
|
|
@@ -487,21 +506,27 @@ export async function runSummary(args, env) {
|
|
|
487
506
|
}
|
|
488
507
|
}
|
|
489
508
|
const ruleNameForGroup = (group, row) => group.topPriority ? (wfTitleByRequest.get(row?.requestId ?? '') ?? group.title) : group.title;
|
|
509
|
+
/**
|
|
510
|
+
* 逐条按**原工作流口径**判定(2026-11-03 用户定:优先组副本空摘要修复)。
|
|
511
|
+
*
|
|
512
|
+
* 旧实现按「组」判定,且优先组被整组跳过标题兜底——但优先组的组名匹配不到规则(会落到 `*` 兜底,
|
|
513
|
+
* `needSummary` 被当成 true),于是 `needSummary:false` 的条目(如内部留言、安全问题工单)
|
|
514
|
+
* 在优先组里的副本既没走标题兜底、又没走 AI 批次,summary 恒为空 ⇒ 导出闸门拦截。
|
|
515
|
+
* 现在改为逐条解析口径:`needSummary:false` ⇒ 标题兜底(与普通组副本一致),否则读摘要缓存。
|
|
516
|
+
*/
|
|
490
517
|
for (const group of groups) {
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
518
|
+
for (const row of rowsOfGroup(group)) {
|
|
519
|
+
if ((row.summary ?? '').trim())
|
|
520
|
+
continue;
|
|
521
|
+
const name = ruleNameForGroup(group, row);
|
|
522
|
+
if (!needAiSummary(name)) {
|
|
495
523
|
row.summary = row.requestNameTitle ?? '';
|
|
496
524
|
if ((row.summary ?? '').trim())
|
|
497
525
|
titleSummary += 1;
|
|
526
|
+
continue;
|
|
498
527
|
}
|
|
499
|
-
|
|
500
|
-
}
|
|
501
|
-
for (const row of rowsOfGroup(group)) {
|
|
502
|
-
if (!row?.requestId || (row.summary ?? '').trim())
|
|
528
|
+
if (!row?.requestId)
|
|
503
529
|
continue;
|
|
504
|
-
const name = ruleNameForGroup(group, row);
|
|
505
530
|
const cached = summaryCache.get(row.requestId, row.lastFeedBackDateTime, explicitMaxCharsFor(args, name), promptTagOf(requestPromptFor(name)), materialTagOfRow(row.requestId, row.lastFeedBackDateTime));
|
|
506
531
|
if (cached) {
|
|
507
532
|
row.summary = cached;
|
package/docs/e10-auth.md
CHANGED
|
@@ -76,7 +76,7 @@ XiaoE 宿主身份有两种判定入口:桌面客户端由 Agent 根据“智
|
|
|
76
76
|
|
|
77
77
|
CLI 依次尝试成对的平台凭据、npm 包自带的 `scripts/xiaoe_env.py` 兼容路径、宿主 `xiaoeUserData/tokens.py`、桌面 XiaoE 客户端本地会话和 `WEAVER_XIAOE_*` ETEAMSID 映射。客户端会话优先读取 `XIAOE_USER_SETTINGS_PATH`,再从 `XIAOE_ROBOT_DATA_ROOT` / `XIAOE_ROBOT_ROOT` 查找,最后回退 macOS `~/Library/Application Support/XiaoE/Robots/<id>/user_settings.json` 或 Windows `%LOCALAPPDATA%/XiaoE/Robots/<id>/user_settings.json`。这些变量和文件只用于定位凭据,不能用于识别宿主。`XIAOE_DOMAIN` 是 XiaoE/erpa 换票服务地址,不是 E10 登录地址。
|
|
78
78
|
|
|
79
|
-
E10 目标地址按“显式 `--base-url` → 专用 `WEAVER_XIAOE_*` E10 配置/映射 → 当前 profile → `https://www.e-cology.com.cn`”解析。凭证可用时,CLI 直接保存登录态,保存的 `agentType` 固定为 `XiaoE`。统一登录失败时,所有操作系统都返回 `authentication/xiaoe_credential_unavailable
|
|
79
|
+
E10 目标地址按“显式 `--base-url` → 专用 `WEAVER_XIAOE_*` E10 配置/映射 → 当前 profile → `https://www.e-cology.com.cn`”解析。凭证可用时,CLI 直接保存登录态,保存的 `agentType` 固定为 `XiaoE`。统一登录失败时,所有操作系统都返回 `authentication/xiaoe_credential_unavailable`(若失败全程都是网络错误则返回 `authentication/xiaoe_network_unavailable`,提示“暂时无法连接 E10,请稍后重试”并标记为可重试),不启动浏览器 OAuth。需要浏览器授权时必须另行显式执行 `auth login`。
|
|
80
80
|
|
|
81
81
|
脚本兼容链路会先自动发现桌面 XiaoE 客户端内置 Python(macOS 应用包与 Windows 按用户/全局安装均支持),再回退到系统 `python3`、`/usr/bin/python3`、`python`;Windows 的系统回退顺序为 `python`、`python3`、`py`。可用 `WEAVER_XIAOE_TOKEN_SCRIPT_PYTHON` 精确指定解释器,用 `WEAVER_XIAOE_TOKEN_SCRIPT_PATH` 指定桌面宿主脚本路径(旧名 `XIAOE_TOKEN_SCRIPT_PYTHON` / `XIAOE_TOKEN_SCRIPT_PATH` 仍兼容)。CLI 自带的 `xiaoe_env.py` 位于 npm 包 `scripts/`,不会放入 `weaver-e10-shared` Skill;Skill 包禁止携带或直接执行 Token 脚本。`XIAOE_` 是平台保留前缀,本 CLI 的自定义变量统一使用 `WEAVER_XIAOE_*`。
|
|
82
82
|
|
|
@@ -84,12 +84,18 @@ E10 目标地址按“显式 `--base-url` → 专用 `WEAVER_XIAOE_*` E10 配置
|
|
|
84
84
|
|
|
85
85
|
XiaoE 换票结果中的 `expiryTimes` 按 Unix 毫秒时间戳持久化。宿主脚本未返回该字段时,CLI 与 XiaoE `tokens.py` 保持一致,按换票成功时间加 2 小时生成。业务命令加载会话后或长进程每次请求前发现当前时间达到 `expiryTimes` 时,会先静默重新换票;换票失败则停止本次业务请求,不会继续发送已知过期的 ETEAMSID。
|
|
86
86
|
|
|
87
|
+
小 e 换票接口、`teamsCheck` 与本地会话探测都带 15 秒超时(含读响应体)。超时按网络错误处理(`causeCode=ETIMEDOUT`),不会再出现某一步没有上限、把整条命令挂死的情况;`auth status` 之类的富化请求超时后只是放弃更新信息,不影响命令本身。
|
|
88
|
+
|
|
87
89
|
业务命令缺少可读取的本地会话时,若当前运行时已通过 `XIAOE_*`、`--runtime-agent xiaoe` 等标记识别为 XiaoE,公共会话加载层会先静默尝试统一登录,成功后继续业务操作。失败时返回 `authentication/xiaoe_credential_unavailable`,并明确提示已尝试自动获取。显式 `--auth` 不触发此恢复;未识别到 XiaoE 的首次认证仍需显式选择登录方式。`auth status --no-check` 等本地状态查询不会主动登录。已有 XiaoE 会话被 E10 接口明确拒绝为失效时,公共请求运行时会根据持久化会话的 `agentType` / `loginSource` 静默换票并重放原请求一次;这条路径不依赖 shared Skill、Agent、`XIAOE_*` 或 `--runtime-agent xiaoe`。它不会打印中间登录步骤,不会自动打开浏览器,也不会对流式请求、网络错误、`partial` / `write_uncertain` 做重放。`--runtime-agent xiaoe` 表示当前运行时身份,不携带凭据。WorkBuddy、Codex 及其他智能体首次认证使用 `auth login --base-url <url> --agent_type <agent_type>` 的浏览器 OAuth 流程。
|
|
88
90
|
|
|
89
91
|
无法使用浏览器回调时,可以用 `auth set` 直接写入浏览器中复制到的
|
|
90
92
|
`ETEAMSID`。命令会调用 `teamsCheck` 解析当前用户、租户和组织信息,并写入
|
|
91
93
|
E10 兼容的 auth 文件。
|
|
92
94
|
|
|
95
|
+
静默恢复失败时,`authentication/xiaoe_credential_unavailable` 会带 `details` 以便定位而不必复现:`attemptedSteps` 按序记录评估过的凭证通道及结果(如 `platform_token:missing`、`bundled_env_script:failed`、`token_script:disabled`、`environment_map:empty`、`e10_session:failed`),`failures` 按发生顺序保留每一次失败的阶段(`platform_token` / `bundled_env_script` / `token_script` / `client_session` / `teams_check` / `e10_session`)、类别(`network` / `http` / `credential`)以及网络错误的系统错误码 `causeCode`(取自 `error.cause.code`,如 `ECONNREFUSED`、`ETIMEDOUT`)和 HTTP 错误的状态码 `httpStatus`,`lastError` 是最后一条真实原因(已对已知凭据脱敏并截断到 300 字符),并附带解析出的 `baseUrl`、`clientType`、`platformCredentialDetected`、整体归类 `errorKind` 与最后失败阶段 `failureStage`;若票其实换到了、但按 `expiryTimes` 判定为已过期,会额外带 `resultSessionExpired: true`。当且仅当所有失败都是网络错误时,错误改为 `authentication/xiaoe_network_unavailable`、`retryable: true`,文案换成“暂时无法连接 E10,请稍后重试”(此时登录态并未被验证,提示用户重新登录属于误导)。这条路径默认依然完全无输出,需要看每一步的过程日志时给业务命令加 `--verbose`(经 stderr 以 `[debug]` 前缀输出;`--json` 下完全不输出)。
|
|
96
|
+
|
|
97
|
+
网页端若返回 `details.reason=host_credential_not_injected`,表示沙箱仍带 `XIAOE_CLIENT_TYPE=cloud` 标记,但宿主注入的平台凭据在启动子进程时丢失。这不是 E10 登录损坏,也不能通过重试或浏览器 OAuth 自愈;沙箱启动器必须保留宿主环境并只定向覆盖 `WEAVER_WORK_CLI_AUTH_ROOT` 等测试变量,不得用 `env -i`、空环境字典或不包含 XiaoE 平台凭据的白名单启动 CLI。凭据值不得写入命令、日志或错误详情。
|
|
98
|
+
|
|
93
99
|
`auth whoami` 默认读取本地缓存,不发网络请求;`auth whoami --live` 会调用
|
|
94
100
|
`teamsCheck` 获取实时信息。`auth export-weaver-env` 会输出
|
|
95
101
|
`WEAVER_BASE_URL`、`WEAVER_ETEAMSID`、`WEAVER_COOKIE`、`WEAVER_USER_AGENT` 和
|
package/docs/operation-manual.md
CHANGED
|
@@ -381,6 +381,14 @@ dist/weaver-e10-<module>-skill.zip
|
|
|
381
381
|
|
|
382
382
|
打包单个业务模块时会自动包含 `weaver-e10-shared`。zip 用于平台导入、离线分发或版本归档;本机日常安装优先使用 `weaver-work-cli skills install <module>`。
|
|
383
383
|
|
|
384
|
+
若目标环境已装有共享 skill、只需分发业务模块本身,加 `--no-shared`(脚本位置参数之后):
|
|
385
|
+
|
|
386
|
+
```bash
|
|
387
|
+
node scripts/package-skill.mjs yepiaotong dist/weaver-e10-yepiaotong-only.zip --no-shared
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
此时产物内只有业务模块目录,`../weaver-e10-shared/` 的相对引用需由目标环境自行提供。
|
|
391
|
+
|
|
384
392
|
## 10. 安装新生成的业务模块 Skill
|
|
385
393
|
|
|
386
394
|
检测通过并得到用户确认后安装:
|
package/package.json
CHANGED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 把仓库里的 weaver-e10-meeting 技能铺到小E 的 user-skills 目录(一键恢复)。
|
|
4
|
+
*
|
|
5
|
+
* 背景(2026-11-04):小E 的「更新 skill」会从平台技能库拉 2.11.0 覆盖本地,
|
|
6
|
+
* 而且有一次直接把 `user-skills/weaver-e10-meeting` 主目录删了(只剩 .backup-*),
|
|
7
|
+
* 导致模型读到旧版本、按旧规则把用户推去页面手工建会。
|
|
8
|
+
*
|
|
9
|
+
* 用法:node scripts/install-skill-to-xiaoe.mjs
|
|
10
|
+
* 做的事:
|
|
11
|
+
* 1. 找 AppData/Local/xiaoeUserData/userdata/<id>/user-skills;
|
|
12
|
+
* 2. 复制仓库 skills/weaver-e10-meeting 全部文件过去;
|
|
13
|
+
* 3. 保留/恢复 .skill-id(优先原文件 → 备份目录 → base.json 的 id);
|
|
14
|
+
* 4. 把 base.json 里该技能的 version 同步成 SKILL.md frontmatter 的版本。
|
|
15
|
+
*/
|
|
16
|
+
import { existsSync, readFileSync, readdirSync, statSync, copyFileSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
|
|
20
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const repoRoot = path.resolve(here, '..');
|
|
22
|
+
const skillSrc = path.join(repoRoot, 'skills', 'weaver-e10-meeting');
|
|
23
|
+
const SKILL_NAME = 'weaver-e10-meeting';
|
|
24
|
+
|
|
25
|
+
const localAppData = process.env.LOCALAPPDATA || path.join(process.env.USERPROFILE || '', 'AppData', 'Local');
|
|
26
|
+
const userdataRoot = path.join(localAppData, 'xiaoeUserData', 'userdata');
|
|
27
|
+
if (!existsSync(userdataRoot)) {
|
|
28
|
+
console.error(`找不到小E 数据目录:${userdataRoot}`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const version = (() => {
|
|
33
|
+
const text = readFileSync(path.join(skillSrc, 'SKILL.md'), 'utf8').replace(/\r\n/g, '\n');
|
|
34
|
+
const m = text.match(/^version:\s*(.+)$/m);
|
|
35
|
+
return m ? m[1].trim() : '';
|
|
36
|
+
})();
|
|
37
|
+
|
|
38
|
+
let installed = 0;
|
|
39
|
+
// 2026-11-04 实测:小E 真正加载的是**租户级** `tenant-skills`(优先级更高),
|
|
40
|
+
// `user-skills` 那份会被平台「更新 skill」重置回 2.11.0,所以两个目录都要铺。
|
|
41
|
+
for (const scope of ['tenant-skills', 'user-skills']) {
|
|
42
|
+
for (const userDir of readdirSync(userdataRoot)) {
|
|
43
|
+
const skillsRoot = path.join(userdataRoot, userDir, scope);
|
|
44
|
+
if (!existsSync(skillsRoot)) continue;
|
|
45
|
+
installInto(skillsRoot);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function installInto(skillsRoot) {
|
|
50
|
+
const target = path.join(skillsRoot, SKILL_NAME);
|
|
51
|
+
|
|
52
|
+
// 1) 保留原 .skill-id;没有就从备份目录找;再没有就从 base.json 的条目取
|
|
53
|
+
let skillId = '';
|
|
54
|
+
try {
|
|
55
|
+
skillId = readFileSync(path.join(target, '.skill-id'), 'utf8').trim();
|
|
56
|
+
} catch { /* 主目录被删过就没有 */ }
|
|
57
|
+
if (!skillId) {
|
|
58
|
+
const backup = readdirSync(skillsRoot).find((name) => name.startsWith('.backup') && name.includes('meeting'));
|
|
59
|
+
if (backup) {
|
|
60
|
+
try {
|
|
61
|
+
skillId = readFileSync(path.join(skillsRoot, backup, '.skill-id'), 'utf8').trim();
|
|
62
|
+
} catch { /* ignore */ }
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const baseJsonPath = path.join(skillsRoot, 'base.json');
|
|
66
|
+
let base = null;
|
|
67
|
+
if (existsSync(baseJsonPath)) {
|
|
68
|
+
try {
|
|
69
|
+
base = JSON.parse(readFileSync(baseJsonPath, 'utf8'));
|
|
70
|
+
const entry = (base.skills ?? []).find((s) => s.name === SKILL_NAME);
|
|
71
|
+
if (!skillId) skillId = String(entry?.id ?? '');
|
|
72
|
+
} catch { /* ignore */ }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 2) 复制技能文件
|
|
76
|
+
const copyDir = (rel) => {
|
|
77
|
+
const srcDir = path.join(skillSrc, rel);
|
|
78
|
+
const dstDir = path.join(target, rel);
|
|
79
|
+
mkdirSync(dstDir, { recursive: true });
|
|
80
|
+
for (const name of readdirSync(srcDir)) {
|
|
81
|
+
const src = path.join(srcDir, name);
|
|
82
|
+
const dst = path.join(dstDir, name);
|
|
83
|
+
if (statSync(src).isDirectory()) copyDir(rel ? `${rel}/${name}` : name);
|
|
84
|
+
else copyFileSync(src, dst);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
copyDir('');
|
|
88
|
+
|
|
89
|
+
// 3) 写回 .skill-id
|
|
90
|
+
if (skillId) writeFileSync(path.join(target, '.skill-id'), skillId, 'utf8');
|
|
91
|
+
|
|
92
|
+
// 4) 同步 base.json 版本
|
|
93
|
+
if (base) {
|
|
94
|
+
const entry = (base.skills ?? []).find((s) => s.name === SKILL_NAME);
|
|
95
|
+
if (entry) {
|
|
96
|
+
entry.version = version;
|
|
97
|
+
entry.updateTime = Date.now();
|
|
98
|
+
writeFileSync(baseJsonPath, JSON.stringify(base, null, 2), 'utf8');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
installed += 1;
|
|
103
|
+
console.log(`已铺装:${target}(version=${version},skillId=${skillId || '未取到'})`);
|
|
104
|
+
}
|
|
105
|
+
if (installed === 0) console.error('没有找到可铺装的 user-skills 目录');
|
|
@@ -15,7 +15,9 @@ const packageJsonPath = path.join(repositoryRoot, 'package.json')
|
|
|
15
15
|
const sharedSkillName = 'weaver-e10-shared'
|
|
16
16
|
const skillPrefix = 'weaver-e10-'
|
|
17
17
|
const legacySkillPrefix = 'weaver-work-cli-'
|
|
18
|
-
const
|
|
18
|
+
const rawArgs = process.argv.slice(2)
|
|
19
|
+
const includeShared = !rawArgs.includes('--no-shared')
|
|
20
|
+
const [requestedSkillArg, outputArg] = rawArgs.filter((arg) => !arg.startsWith('--'))
|
|
19
21
|
|
|
20
22
|
function sha256(value) {
|
|
21
23
|
return createHash('sha256').update(value).digest('hex')
|
|
@@ -59,7 +61,7 @@ async function resolveSkillNames() {
|
|
|
59
61
|
if (!requestedSkill) return allSkills
|
|
60
62
|
if (!allSkills.includes(requestedSkill)) throw new Error(`未知 Skill:${requestedSkill}`)
|
|
61
63
|
const names = []
|
|
62
|
-
if (requestedSkill !== sharedSkillName && allSkills.includes(sharedSkillName)) names.push(sharedSkillName)
|
|
64
|
+
if (includeShared && requestedSkill !== sharedSkillName && allSkills.includes(sharedSkillName)) names.push(sharedSkillName)
|
|
63
65
|
names.push(requestedSkill)
|
|
64
66
|
return names
|
|
65
67
|
}
|