weaver-work-cli 0.1.26 → 0.1.29
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 +7 -2
- package/dist/cmd/setup/index.js +1 -1
- package/dist/internal/e10/auth/commands.js +1 -6
- package/dist/internal/e10/auth/messages.js +3 -0
- package/dist/internal/e10/auth/session.js +8 -0
- package/dist/internal/e10/auth-recovery.js +148 -0
- package/dist/internal/e10/client.js +47 -15
- package/dist/internal/e10/context.js +8 -8
- package/dist/internal/e10/index.js +1 -0
- package/dist/internal/e10/request-runtime.js +51 -9
- package/dist/shortcuts/archive/host.js +3 -2
- package/dist/shortcuts/archive/manifest.js +1 -1
- package/dist/shortcuts/archive-oa/host.js +2 -1
- package/dist/shortcuts/archive-oa/manifest.js +1 -1
- package/dist/shortcuts/asset/host.js +7 -6
- package/dist/shortcuts/blog/host.js +4 -3
- package/dist/shortcuts/caizhilian/host.js +3 -2
- package/dist/shortcuts/caizhilian/manifest.js +1 -1
- package/dist/shortcuts/calendar/host.js +2 -1
- package/dist/shortcuts/contract/host.js +34 -59
- package/dist/shortcuts/ehr/host.js +2 -1
- package/dist/shortcuts/esb/host.js +2 -1
- package/dist/shortcuts/fna/host.js +6 -3
- package/dist/shortcuts/fna/manifest.js +10 -5
- package/dist/shortcuts/fna/operations/expense-chart.js +162 -0
- package/dist/shortcuts/fna/operations/registry.js +2 -0
- package/dist/shortcuts/hrm/host.js +2 -1
- package/dist/shortcuts/im/host.js +3 -2
- package/dist/shortcuts/invoice/host.js +9 -7
- package/dist/shortcuts/invoice/manifest.js +23 -142
- package/dist/shortcuts/invoice/operations/issuing-qys.js +266 -203
- package/dist/shortcuts/invoice/operations/registry.js +1 -6
- package/dist/shortcuts/jiuchuanhui/host.js +2 -1
- package/dist/shortcuts/mail/host.js +2 -1
- package/dist/shortcuts/meeting/host.js +2 -1
- package/dist/shortcuts/meeting/operations/eb/conflict.js +78 -12
- package/dist/shortcuts/okr/host.js +2 -1
- package/dist/shortcuts/plan/host.js +2 -1
- package/dist/shortcuts/project/host.js +2 -1
- package/dist/shortcuts/workflow/host.js +3 -5
- package/dist/shortcuts/workflow/index.js +4 -1
- package/dist/shortcuts/workflow/operations/chatscene.js +4 -22
- package/dist/shortcuts/workflow/operations/form.js +281 -166
- package/dist/shortcuts/workflow/operations/read.js +39 -8
- package/dist/shortcuts/workflow/operations/registry.js +2 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/businessRules.js +331 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/detailFieldRules.js +133 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/summaryCache.js +326 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoDetailCache.js +113 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoDetailFetcher.js +259 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoListRows.js +64 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoOutputPlanner.js +28 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.aggregate.js +70 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.collect.js +267 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.core.js +392 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.detail.js +165 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.group.js +448 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.groupSummary.js +348 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.js +222 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.pipeline.js +66 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.smartGroup.js +502 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.summary.js +540 -0
- package/dist/shortcuts/workflow/stdout-compact.js +7 -0
- package/docs/SKILL.md +1 -1
- package/docs/_catalog.md +2 -1
- package/docs/agent-invoice.md +1 -1
- package/docs/caizhilian.md +1 -1
- package/docs/e10-auth.md +1 -1
- package/docs/fna.md +7 -3
- package/docs/invoice.md +4 -6
- package/docs/todo-smart-convert/README.md +171 -0
- package/docs/todo-smart-convert/data-contract.md +236 -0
- package/docs/todo-smart-convert/mock/ai-todo-query-target.json +209 -0
- package/docs/todo-smart-convert/subgroup-rules.md +245 -0
- package/package.json +1 -1
- package/scripts/package-skill.mjs +1 -1
- package/skill-template/business-info.json +4 -1
- package/skill-template/domains/caizhilian.md +2 -1
- package/skill-template/domains/qiyecheng.md +1 -0
- package/skill-template/domains/yepiaotong.md +6 -2
- package/skill-template/skill-template.md +1 -0
- package/skills/weaver-e10-blog/SKILL.md +6 -1
- package/skills/weaver-e10-blog/references/blog-query.md +3 -1
- package/skills/weaver-e10-caizhilian/SKILL.md +18 -9
- package/skills/weaver-e10-caizhilian/references/action-flow.md +70 -0
- package/skills/weaver-e10-caizhilian/references/agent-entry.md +6 -2
- package/skills/weaver-e10-caizhilian/references/auth-and-install.md +207 -0
- package/skills/weaver-e10-caizhilian/references/file-collection.md +56 -54
- package/skills/weaver-e10-caizhilian/references/file-upload.md +4 -4
- package/skills/weaver-e10-caizhilian/references/host-executor-prompt.md +32 -0
- package/skills/weaver-e10-caizhilian/references/host-platform-integration.md +46 -0
- package/skills/weaver-e10-caizhilian/references/rule-index.md +56 -0
- package/skills/weaver-e10-caizhilian/references/setting-attach.md +5 -16
- package/skills/weaver-e10-caizhilian/references/setting-backup.md +4 -13
- package/skills/weaver-e10-caizhilian/references/setting-encrypt.md +6 -15
- package/skills/weaver-e10-caizhilian/references/setting-storage-docs-summary.md +126 -0
- package/skills/weaver-e10-caizhilian/references/setting-storage-field-map.md +57 -0
- package/skills/weaver-e10-caizhilian/references/setting-storage.md +142 -482
- package/skills/weaver-e10-caizhilian/references/source-manifest.json +7 -7
- package/skills/weaver-e10-caizhilian/references/task-workflow.md +87 -0
- package/skills/weaver-e10-caizhilian/references/withheld.md +2 -2
- package/skills/weaver-e10-caizhilian/references/write-protocol.md +7 -0
- package/skills/weaver-e10-calendar/SKILL.md +6 -1
- package/skills/weaver-e10-esb/SKILL.md +8 -0
- package/skills/weaver-e10-esb/references/input-format.md +3 -1
- package/skills/weaver-e10-esb/references/source-manifest.json +5 -5
- package/skills/weaver-e10-hrm/SKILL.md +6 -1
- package/skills/weaver-e10-hrm/references/employee.md +1 -1
- package/skills/weaver-e10-hrm/references/org.md +1 -1
- package/skills/weaver-e10-hrm/references/position.md +1 -1
- package/skills/weaver-e10-jinchengda/SKILL.md +2 -1
- package/skills/weaver-e10-jinchengda/references/contract-agent-entry.md +1 -1
- package/skills/weaver-e10-jiuchuanhui/SKILL.md +6 -1
- package/skills/weaver-e10-jucailin/SKILL.md +6 -1
- package/skills/weaver-e10-jucailin/references/hr-contact.md +1 -1
- package/skills/weaver-e10-jucailin/references/salary.md +1 -1
- package/skills/weaver-e10-mail/SKILL.md +6 -1
- package/skills/weaver-e10-meeting/SKILL.md +11 -6
- package/skills/weaver-e10-meeting/references/meeting-eb.md +7 -3
- package/skills/weaver-e10-meeting/references/meeting-queries.md +3 -1
- package/skills/weaver-e10-meeting/references/meeting-verification.md +4 -2
- package/skills/weaver-e10-okr/SKILL.md +6 -1
- package/skills/weaver-e10-okr/references/source-manifest.json +5 -5
- package/skills/weaver-e10-plan/SKILL.md +6 -1
- package/skills/weaver-e10-plan/references/plan-overview-and-routing.md +3 -1
- package/skills/weaver-e10-plan/references/plan-report-query.md +3 -1
- package/skills/weaver-e10-plan/references/source-manifest.json +7 -7
- package/skills/weaver-e10-qiyecheng/SKILL.md +16 -6
- package/skills/weaver-e10-qiyecheng/references/catalog.md +6 -3
- package/skills/weaver-e10-qiyecheng/references/expense-center.md +2 -0
- package/skills/weaver-e10-qiyecheng/references/expense-chart.md +78 -0
- package/skills/weaver-e10-qiyecheng/references/source-manifest.json +87 -33
- package/skills/weaver-e10-shared/SKILL.md +37 -14
- package/skills/weaver-e10-shared/references/e10-auth-and-session.md +11 -8
- package/skills/weaver-e10-shared/references/non-interactive-environment.md +4 -4
- package/skills/weaver-e10-shared/references/weaver-e10-installation.md +3 -1
- package/skills/weaver-e10-wenshuding/SKILL.md +6 -1
- package/skills/weaver-e10-wenshuding/references/source-manifest.json +3 -3
- package/skills/weaver-e10-workflow/SKILL.md +23 -1
- package/skills/weaver-e10-workflow/references/source-manifest.json +1 -2
- package/skills/weaver-e10-workflow/references/todo-smart-convert.md +282 -0
- package/skills/weaver-e10-workflow/references/workflow-create.md +30 -55
- package/skills/weaver-e10-workflow/scripts/todo-summary.cjs +695 -0
- package/skills/weaver-e10-yepiaotong/SKILL.md +19 -17
- package/skills/weaver-e10-yepiaotong/references/invoice-issuing-examples.md +75 -78
- package/skills/weaver-e10-yepiaotong/references/invoice-issuing-form-fill-rules.md +179 -0
- package/skills/weaver-e10-yepiaotong/references/invoice-issuing-workflow.md +238 -0
- package/skills/weaver-e10-yepiaotong/references/invoice-reim.md +7 -7
- package/skills/weaver-e10-yepiaotong/references/reim-api-reference.md +11 -5
- package/skills/weaver-e10-yepiaotong/references/reim-examples.md +55 -33
- package/skills/weaver-e10-yepiaotong/references/reim-form-fill-rules.md +102 -23
- package/skills/weaver-e10-yepiaotong/references/reim-phone-subsidy-rules.md +1 -1
- package/skills/weaver-e10-yepiaotong/references/reim-workflow.md +53 -42
- package/skills/weaver-e10-yepiaotong/references/source-manifest.json +94 -214
- package/skills/weaver-e10-yimiaoban/SKILL.md +9 -2
- package/skills/weaver-e10-yimiaoban/references/file-user-i18n.md +1 -1
- package/skills/weaver-e10-yimiaoban/references/msg-read.md +1 -1
- package/skills/weaver-e10-yimiaoban/references/source-manifest.json +9 -9
- package/skills/weaver-e10-ziguanjia/SKILL.md +6 -1
- package/dist/shortcuts/invoice/operations/issuing.js +0 -198
- package/skills/weaver-e10-yepiaotong/references/invoice-issuing-field-rules.md +0 -161
- package/skills/weaver-e10-yepiaotong/references/invoice-issuing-qys-ledger.md +0 -66
- package/skills/weaver-e10-yepiaotong/references/invoice-issuing.md +0 -107
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import { workflowValidationError } from '../../errors.js';
|
|
2
|
+
import { asRecord, readBoundedInt } from './todoSmartConvert.core.js';
|
|
3
|
+
import { detectBusinessKind, isDeterministicGroupKind, GROUP_SUMMARY_TAIL_TEXT, } from './businessRules.js';
|
|
4
|
+
import { fetchDetailFacts, stampsOfRows } from './todoSmartConvert.detail.js';
|
|
5
|
+
import { ctxEmptyOf, groupItemsOf, rowsOfGroup, totalTodoCount, } from './todoSmartConvert.smartGroup.js';
|
|
6
|
+
import { readFinalGroups, readSmartSummaryTitle } from './todoSmartConvert.summary.js';
|
|
7
|
+
const INTERNAL_MESSAGE_SUMMARY_HINT = '内部留言:只按原文直接概括「谁说了什么」,不要过度解读、不要推测动机或影响,严格按本批 suggestedMaxChars 控制字数';
|
|
8
|
+
/**
|
|
9
|
+
* 组摘要的固定尾巴一律用 `businessRules.GROUP_SUMMARY_TAIL_TEXT`(CLI 固定文案与喂模型的
|
|
10
|
+
* `stats.tail` 必须同一份,否则改一处就会两处不一致)。
|
|
11
|
+
*/
|
|
12
|
+
const GROUP_SUMMARY_TAIL = GROUP_SUMMARY_TAIL_TEXT;
|
|
13
|
+
/** 组级摘要建议长度上限。 */
|
|
14
|
+
const DEFAULT_GROUP_SUMMARY_MAX = 100;
|
|
15
|
+
/** `¥123` / `¥1,280.50` / `123元` 三种写法;每条只取第一个命中,避免把「14 小时」当金额。 */
|
|
16
|
+
const GROUP_AMOUNT_PATTERNS = [
|
|
17
|
+
/[¥¥]\s*(\d[\d,]*(?:\.\d{1,2})?)/u,
|
|
18
|
+
/(\d[\d,]*(?:\.\d{1,2})?)\s*元/u,
|
|
19
|
+
];
|
|
20
|
+
export function firstAmountOf(text) {
|
|
21
|
+
for (const pattern of GROUP_AMOUNT_PATTERNS) {
|
|
22
|
+
const matched = text.match(pattern);
|
|
23
|
+
if (!matched)
|
|
24
|
+
continue;
|
|
25
|
+
const value = Number(matched[1].replace(/,/gu, ''));
|
|
26
|
+
if (Number.isFinite(value) && value > 0)
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
/** `由 A、B、C 等 5 人发起`:≥4 人时只列前 3 个再补「等 N 人」。 */
|
|
32
|
+
export function createrPhraseOf(creators) {
|
|
33
|
+
if (creators.length === 0)
|
|
34
|
+
return '';
|
|
35
|
+
if (creators.length <= 3)
|
|
36
|
+
return `由${creators.join('、')} 发起`;
|
|
37
|
+
return `由${creators.slice(0, 3).join('、')} 等 ${creators.length} 人发起`;
|
|
38
|
+
}
|
|
39
|
+
/** 组摘要素材:只取普通组的**汇总项**(`type:"group"`),优先组与单独条目都跳过。
|
|
40
|
+
*
|
|
41
|
+
* `isCtxEmpty`:详情数据为空的条目不喂给 AI,从素材里剔除;
|
|
42
|
+
* 一个汇总项剔除后一条不剩 ⇒ 整个汇总项不产批次(该组不涉及 AI)。
|
|
43
|
+
*
|
|
44
|
+
* `smartGroup` 已为确定性路径的二次分组汇总项写入固定 `summary`;AI 路径和内部留言仍可进入
|
|
45
|
+
* `groupSummaryBatches`,由后续阶段补写组摘要。
|
|
46
|
+
*/
|
|
47
|
+
export function collectGroupSummaryBatches(groups, isCtxEmpty, maxChars = DEFAULT_GROUP_SUMMARY_MAX) {
|
|
48
|
+
const batches = [];
|
|
49
|
+
for (const group of groups) {
|
|
50
|
+
if (group.topPriority)
|
|
51
|
+
continue;
|
|
52
|
+
const kind = detectBusinessKind(group.title);
|
|
53
|
+
for (const sub of groupItemsOf(group)) {
|
|
54
|
+
// CLI 已经写好固定文案 ⇒ 不需要模型再写一遍
|
|
55
|
+
if (isDeterministicGroupKind(kind) && (sub.summary ?? '').trim() !== '')
|
|
56
|
+
continue;
|
|
57
|
+
const allRows = Array.isArray(sub.requestDatas) ? sub.requestDatas : [];
|
|
58
|
+
const rows = isCtxEmpty ? allRows.filter((row) => !isCtxEmpty(row.requestId)) : allRows;
|
|
59
|
+
const emptySkipped = allRows.length - rows.length;
|
|
60
|
+
if (rows.length === 0) {
|
|
61
|
+
// 整个汇总项没有可喂给模型的正文 ⇒ 不涉及 AI。
|
|
62
|
+
// 正常情况下这条走不到:单条摘要恒非空(详情为空时 CLI 用标题兜底),
|
|
63
|
+
// 而「有摘要」的条目在 `loadCtxEmptyPredicate` 里直接判为「有素材」。
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const items = rows.map((row) => ({
|
|
67
|
+
requestId: row.requestId,
|
|
68
|
+
requestNameTitle: row.requestNameTitle,
|
|
69
|
+
createrName: (row.createrName ?? '').trim(),
|
|
70
|
+
summary: row.summary ?? '',
|
|
71
|
+
}));
|
|
72
|
+
const creators = [];
|
|
73
|
+
for (const item of items) {
|
|
74
|
+
if (item.createrName && !creators.includes(item.createrName))
|
|
75
|
+
creators.push(item.createrName);
|
|
76
|
+
}
|
|
77
|
+
const amounts = [];
|
|
78
|
+
for (const item of items) {
|
|
79
|
+
const amount = firstAmountOf(`${item.summary} ${item.requestNameTitle}`);
|
|
80
|
+
if (amount !== null)
|
|
81
|
+
amounts.push(amount);
|
|
82
|
+
}
|
|
83
|
+
const amountReliable = items.length >= 2 && amounts.length === items.length;
|
|
84
|
+
batches.push({
|
|
85
|
+
groupId: sub.id,
|
|
86
|
+
parentGroupId: group.id,
|
|
87
|
+
parentTitle: group.title,
|
|
88
|
+
title: sub.title,
|
|
89
|
+
count: items.length,
|
|
90
|
+
totalCount: allRows.length,
|
|
91
|
+
kind,
|
|
92
|
+
...(kind === 'internal-message' ? { hint: INTERNAL_MESSAGE_SUMMARY_HINT } : {}),
|
|
93
|
+
suggestedMaxChars: maxChars,
|
|
94
|
+
...(emptySkipped > 0 ? { emptySkipped } : {}),
|
|
95
|
+
items,
|
|
96
|
+
stats: {
|
|
97
|
+
count: items.length,
|
|
98
|
+
/** 卡片上要显示的条数(含被剔除的条目)——「共 N 条」必须用它 */
|
|
99
|
+
totalCount: allRows.length,
|
|
100
|
+
creatorCount: creators.length,
|
|
101
|
+
creators,
|
|
102
|
+
createrPhrase: createrPhraseOf(creators),
|
|
103
|
+
amount: amountReliable
|
|
104
|
+
? {
|
|
105
|
+
count: amounts.length,
|
|
106
|
+
min: Math.min(...amounts),
|
|
107
|
+
max: Math.max(...amounts),
|
|
108
|
+
sum: Number(amounts.reduce((sum, value) => sum + value, 0).toFixed(2)),
|
|
109
|
+
}
|
|
110
|
+
: null,
|
|
111
|
+
tail: GROUP_SUMMARY_TAIL,
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return batches;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* 「没有 AI 素材」的判断器(按 requestId 现拉详情后判定)。
|
|
120
|
+
*
|
|
121
|
+
* 没有素材只有一种情形:详情本身就是空表单(`contextAvailable=false`)。
|
|
122
|
+
* 内部留言正文只做清洗,不因长度单独丢弃。
|
|
123
|
+
*
|
|
124
|
+
* **已有单条摘要的条目直接判「有素材」**(2026-10-26 用户定稿):摘要本身就是概括过的正文,
|
|
125
|
+
* 组摘要可以直接消费它,不需要再为它拉详情。这一条把 finish 链路里「每条都回查详情」的
|
|
126
|
+
* 请求量直接压到 0(77 条数据实测从 77 次降到 0 次)。
|
|
127
|
+
*
|
|
128
|
+
* 剩下的条目才有必要拉详情(`fetchDetailFacts`:缓存优先,键 `requestId__lastFeedBackDateTime`)。
|
|
129
|
+
* 只收集**非优先组**的条目:优先组不产组摘要(`collectGroupSummaryBatches` 直接跳过),
|
|
130
|
+
* 拉它的详情纯属浪费。
|
|
131
|
+
*
|
|
132
|
+
* 取数失败的条目(`fact === undefined`)当「有素材」:宁可把标题+空摘要喂进去,也不要因为
|
|
133
|
+
* 一次网络抖动就把整组素材剔空。
|
|
134
|
+
*/
|
|
135
|
+
export async function loadCtxEmptyPredicate(env, groups, args = {}) {
|
|
136
|
+
const rows = groups
|
|
137
|
+
.filter((group) => !group.topPriority)
|
|
138
|
+
.flatMap((group) => rowsOfGroup(group));
|
|
139
|
+
/** 已有摘要 ⇒ 有素材,不查缓存不拉网 */
|
|
140
|
+
const withSummary = new Set();
|
|
141
|
+
const pending = new Set();
|
|
142
|
+
for (const row of rows) {
|
|
143
|
+
const requestId = String(row.requestId ?? '').trim();
|
|
144
|
+
if (!requestId)
|
|
145
|
+
continue;
|
|
146
|
+
if ((row.summary ?? '').trim() !== '')
|
|
147
|
+
withSummary.add(requestId);
|
|
148
|
+
else
|
|
149
|
+
pending.add(requestId);
|
|
150
|
+
}
|
|
151
|
+
// 已经有摘要的条目不必再看详情:即便 pending 里也有它,也以「有摘要」为准。
|
|
152
|
+
for (const requestId of withSummary)
|
|
153
|
+
pending.delete(requestId);
|
|
154
|
+
if (pending.size === 0)
|
|
155
|
+
return { isCtxEmpty: () => false, warnings: [] };
|
|
156
|
+
const { facts, warnings } = await fetchDetailFacts(env, [...pending], args, stampsOfRows(rows));
|
|
157
|
+
const emptyOf = ctxEmptyOf(facts);
|
|
158
|
+
return {
|
|
159
|
+
isCtxEmpty: (requestId) => (withSummary.has(requestId) ? false : emptyOf(requestId)),
|
|
160
|
+
warnings,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* `stage=groupSummaryContext`:把每个汇总项的素材打包(一组一包,不切批)。
|
|
165
|
+
*
|
|
166
|
+
* 为什么**不切批**:组摘要要概括**整组**,切开就只能概括一半,模型会写出与 `count` 不符的句子。
|
|
167
|
+
* 组太大时宁可给一条 warning 提示「该组 N 条,上下文偏长」,也不拆。
|
|
168
|
+
*
|
|
169
|
+
* **详情全空的组不产批次**,直接保留现有分组结构;
|
|
170
|
+
* 混合组里详情为空的条目也从素材里剔除。
|
|
171
|
+
*/
|
|
172
|
+
export async function runGroupSummaryContext(args, env) {
|
|
173
|
+
const groups = await readFinalGroups(args, env);
|
|
174
|
+
const { isCtxEmpty, warnings: emptyWarnings } = await loadCtxEmptyPredicate(env, groups, args);
|
|
175
|
+
// 组摘要目标字数:用户给了就用用户的,没给才用默认 100。
|
|
176
|
+
const groupSummaryMaxChars = readBoundedInt(args.groupSummaryMaxChars, 'groupSummaryMaxChars', DEFAULT_GROUP_SUMMARY_MAX, 20, 1000);
|
|
177
|
+
const batches = collectGroupSummaryBatches(groups, isCtxEmpty, groupSummaryMaxChars);
|
|
178
|
+
const totalCount = totalTodoCount(groups);
|
|
179
|
+
const warnings = [...emptyWarnings];
|
|
180
|
+
const skippedEmpty = batches.reduce((sum, batch) => sum + (batch.emptySkipped ?? 0), 0);
|
|
181
|
+
if (skippedEmpty > 0) {
|
|
182
|
+
warnings.push(`组摘要素材里剔除了 ${skippedEmpty} 条详情为空的条目(它们的 summary 已用标题填充,不喂给 AI)`);
|
|
183
|
+
}
|
|
184
|
+
const missingSummary = batches.reduce((sum, batch) => sum + batch.items.filter((item) => !item.summary).length, 0);
|
|
185
|
+
if (missingSummary > 0) {
|
|
186
|
+
warnings.push(`有 ${missingSummary} 条单条摘要为空(先跑 stage=summary 效果更好);`
|
|
187
|
+
+ '组摘要会缺少这部分依据,请只按有摘要的条目概括,不要补写');
|
|
188
|
+
}
|
|
189
|
+
if (batches.length === 0) {
|
|
190
|
+
warnings.push('本次没有需要 AI 写组摘要的汇总项:所有二次分组汇总项的组摘要均由 CLI 固定生成');
|
|
191
|
+
}
|
|
192
|
+
const deterministicGroups = groups
|
|
193
|
+
.filter((group) => !group.topPriority
|
|
194
|
+
&& isDeterministicGroupKind(detectBusinessKind(group.title)))
|
|
195
|
+
.reduce((sum, group) => sum + groupItemsOf(group).length, 0);
|
|
196
|
+
return {
|
|
197
|
+
data: {
|
|
198
|
+
smartSummaryTitle: readSmartSummaryTitle(args, `您当前有 ${totalCount} 条待办,清单如下:`),
|
|
199
|
+
totalCount,
|
|
200
|
+
wfIdGroup: groups,
|
|
201
|
+
groupSummaryBatches: batches,
|
|
202
|
+
groupSummaryStats: {
|
|
203
|
+
groupCount: batches.length,
|
|
204
|
+
itemTotal: batches.reduce((sum, batch) => sum + batch.count, 0),
|
|
205
|
+
withAmount: batches.filter((batch) => batch.stats.amount !== null).length,
|
|
206
|
+
skippedEmpty,
|
|
207
|
+
/** CLI 固定文案的汇总项个数(不进批次,`smartGroup` 阶段已写好) */
|
|
208
|
+
deterministicGroups,
|
|
209
|
+
tail: GROUP_SUMMARY_TAIL,
|
|
210
|
+
suggestedMaxChars: groupSummaryMaxChars,
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
warnings,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
/** 校验并索引 `aiGroupSummaries`(`[{ groupId, summary }]`,与 `aiSummaries` 同风格)。 */
|
|
217
|
+
export function readAiGroupSummaries(value) {
|
|
218
|
+
if (!Array.isArray(value)) {
|
|
219
|
+
throw workflowValidationError('ai_group_summaries_invalid', 'aiGroupSummaries 必须是数组,每项形如 { groupId, summary };groupId 取 groupSummaryBatches[].groupId', { retryAllowed: false });
|
|
220
|
+
}
|
|
221
|
+
const map = new Map();
|
|
222
|
+
value.forEach((raw, index) => {
|
|
223
|
+
const label = `aiGroupSummaries[${index}]`;
|
|
224
|
+
const record = asRecord(raw);
|
|
225
|
+
if (!record) {
|
|
226
|
+
throw workflowValidationError('ai_group_summaries_invalid', `${label} 必须是对象 { groupId, summary }`, { retryAllowed: false });
|
|
227
|
+
}
|
|
228
|
+
const unknownKeys = Object.keys(record).filter((key) => !['groupId', 'summary'].includes(key));
|
|
229
|
+
if (unknownKeys.length > 0) {
|
|
230
|
+
throw workflowValidationError('ai_group_summaries_invalid', `${label} 不接受字段:${unknownKeys.join(', ')};只支持 groupId / summary`, { retryAllowed: false, details: { unknownKeys } });
|
|
231
|
+
}
|
|
232
|
+
const groupId = typeof record.groupId === 'string' ? record.groupId.trim() : '';
|
|
233
|
+
if (!groupId) {
|
|
234
|
+
throw workflowValidationError('ai_group_summaries_invalid', `${label}.groupId 必填且必须是字符串(形如 1003-1)`, { retryAllowed: false });
|
|
235
|
+
}
|
|
236
|
+
if (typeof record.summary !== 'string') {
|
|
237
|
+
throw workflowValidationError('ai_group_summaries_invalid', `${label}.summary 必须是字符串`, { retryAllowed: false });
|
|
238
|
+
}
|
|
239
|
+
if (map.has(groupId)) {
|
|
240
|
+
throw workflowValidationError('ai_group_summary_duplicate_group', `汇总项「${groupId}」出现了多次,一个汇总项只能有一个摘要`, { retryAllowed: true, details: { groupId } });
|
|
241
|
+
}
|
|
242
|
+
map.set(groupId, record.summary.trim());
|
|
243
|
+
});
|
|
244
|
+
return map;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* `stage=groupSummary`:把模型写的组摘要填进 `displayItems[].summary`(只填 `type:"group"`)。
|
|
248
|
+
*
|
|
249
|
+
* 校验口径与单条摘要一致:未知 / 重复 `groupId` 硬失败(说明结果与本轮结构对不上),
|
|
250
|
+
* 漏写只给 warning 并留空串(模型漏一个组不该让 300 条重跑)。
|
|
251
|
+
*
|
|
252
|
+
* 额外做一条**软校验**:组里有发起人时,摘要必须包含由 CLI 算出的 `由 … 发起` 短语。
|
|
253
|
+
* 不满足只 warning 不失败——文案可以换个说法,但「漏掉发起人」通常是模型偷懒,
|
|
254
|
+
* 值得点名让它知道。
|
|
255
|
+
*/
|
|
256
|
+
export async function runGroupSummary(args, env) {
|
|
257
|
+
const groups = await readFinalGroups(args, env);
|
|
258
|
+
const summaries = readAiGroupSummaries(args.aiGroupSummaries);
|
|
259
|
+
const { isCtxEmpty, warnings: emptyWarnings } = await loadCtxEmptyPredicate(env, groups, args);
|
|
260
|
+
// 组摘要目标字数:与 `runGroupSummaryContext` 保持同一口径(用户给了就用用户的)。
|
|
261
|
+
const groupSummaryMaxChars = readBoundedInt(args.groupSummaryMaxChars, 'groupSummaryMaxChars', DEFAULT_GROUP_SUMMARY_MAX, 20, 1000);
|
|
262
|
+
const batches = collectGroupSummaryBatches(groups, isCtxEmpty, groupSummaryMaxChars);
|
|
263
|
+
const knownGroupIds = new Set(batches.map((batch) => batch.groupId));
|
|
264
|
+
for (const groupId of summaries.keys()) {
|
|
265
|
+
if (!knownGroupIds.has(groupId)) {
|
|
266
|
+
throw workflowValidationError('ai_group_summary_unknown_group', `aiGroupSummaries 里的 groupId「${groupId}」不在本次结构的汇总项里,不需要 AI 写摘要`
|
|
267
|
+
+ '(请假/财务类固定文案,其他/内部留言汇总项交模型概括);'
|
|
268
|
+
+ `可回填的 groupId:${[...knownGroupIds].join(', ') || '(本次没有)'}`, { retryAllowed: true, details: { availableGroupIds: [...knownGroupIds] } });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
const warnings = [...emptyWarnings];
|
|
272
|
+
let filled = 0;
|
|
273
|
+
let empty = 0;
|
|
274
|
+
let missingCreater = 0;
|
|
275
|
+
let tooLong = 0;
|
|
276
|
+
/** 沿用 CLI 固定文案(请假/财务)的汇总项个数 */
|
|
277
|
+
let deterministic = 0;
|
|
278
|
+
/** 最终结构里已经有组摘要的汇总项数量;摘要正文只保留在 displayItems[].summary。 */
|
|
279
|
+
let summarizedGroupCount = 0;
|
|
280
|
+
for (const group of groups) {
|
|
281
|
+
if (group.topPriority)
|
|
282
|
+
continue;
|
|
283
|
+
const kind = detectBusinessKind(group.title);
|
|
284
|
+
for (const sub of groupItemsOf(group)) {
|
|
285
|
+
if (isDeterministicGroupKind(kind)) {
|
|
286
|
+
// ★ 固定文案:`smartGroup` 阶段已写好,这里**不覆盖**。
|
|
287
|
+
if ((sub.summary ?? '').trim() === '') {
|
|
288
|
+
const summary = summaries.get(sub.id) ?? '';
|
|
289
|
+
sub.summary = summary;
|
|
290
|
+
if (!summary) {
|
|
291
|
+
empty += 1;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
filled += 1;
|
|
295
|
+
summarizedGroupCount += 1;
|
|
296
|
+
if (summary.length > groupSummaryMaxChars)
|
|
297
|
+
tooLong += 1;
|
|
298
|
+
const batch = batches.find((item) => item.groupId === sub.id);
|
|
299
|
+
if (batch && batch.stats.createrPhrase && !summary.includes(batch.stats.createrPhrase)) {
|
|
300
|
+
missingCreater += 1;
|
|
301
|
+
}
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
deterministic += 1;
|
|
305
|
+
summarizedGroupCount += 1;
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
const summary = summaries.get(sub.id) ?? '';
|
|
309
|
+
sub.summary = summary;
|
|
310
|
+
if (!summary) {
|
|
311
|
+
empty += 1;
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
filled += 1;
|
|
315
|
+
summarizedGroupCount += 1;
|
|
316
|
+
if (summary.length > groupSummaryMaxChars)
|
|
317
|
+
tooLong += 1;
|
|
318
|
+
const batch = batches.find((item) => item.groupId === sub.id);
|
|
319
|
+
if (batch && batch.stats.createrPhrase && !summary.includes(batch.stats.createrPhrase)) {
|
|
320
|
+
missingCreater += 1;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (empty > 0)
|
|
325
|
+
warnings.push(`有 ${empty} 个汇总项没有拿到组摘要,summary 保持空串(未编造)`);
|
|
326
|
+
if (tooLong > 0) {
|
|
327
|
+
warnings.push(`有 ${tooLong} 个组摘要超过建议的 ${groupSummaryMaxChars} 字,已原样保留(CLI 不改写文案)`);
|
|
328
|
+
}
|
|
329
|
+
if (missingCreater > 0) {
|
|
330
|
+
warnings.push(`有 ${missingCreater} 个组摘要没有包含算好的「由 … 发起」短语(发起人由 CLI 统计,模型不该自己数),`
|
|
331
|
+
+ '如需统一口径可重跑模型侧');
|
|
332
|
+
}
|
|
333
|
+
return {
|
|
334
|
+
data: {
|
|
335
|
+
smartSummaryTitle: readSmartSummaryTitle(args, `您当前有 ${totalTodoCount(groups)} 条待办,清单如下:`),
|
|
336
|
+
totalCount: totalTodoCount(groups),
|
|
337
|
+
wfIdGroup: groups,
|
|
338
|
+
},
|
|
339
|
+
meta: {
|
|
340
|
+
groupSummarized: filled,
|
|
341
|
+
groupEmpty: empty,
|
|
342
|
+
/** 沿用 CLI 固定文案的汇总项个数(请假/财务,不经过模型) */
|
|
343
|
+
groupDeterministic: deterministic,
|
|
344
|
+
groupSummaryCount: summarizedGroupCount,
|
|
345
|
+
},
|
|
346
|
+
warnings,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { workflowValidationError } from '../../errors.js';
|
|
2
|
+
import { planTodoOutput } from './todoOutputPlanner.js';
|
|
3
|
+
import { assertKnownKeys, buildSkillTiming, CATEGORY_VALUES, DEFAULT_CATEGORY, DEFAULT_FETCH_LIMIT, DEFAULT_TOP_N, FETCH_PAGE_SIZE, GROUP_BY_VALUES, MAX_FETCH_LIMIT, MAX_FETCH_PAGES, STAGE_VALUES, workflowSuccess, } from './todoSmartConvert.core.js';
|
|
4
|
+
import { collectRawTodos } from './todoSmartConvert.collect.js';
|
|
5
|
+
import { runSmartConvert } from './todoSmartConvert.aggregate.js';
|
|
6
|
+
import { runGroup } from './todoSmartConvert.group.js';
|
|
7
|
+
import { inferStage, readStage } from './todoSmartConvert.pipeline.js';
|
|
8
|
+
import { runSmartGroup } from './todoSmartConvert.smartGroup.js';
|
|
9
|
+
import { runSummary, runSummaryContext } from './todoSmartConvert.summary.js';
|
|
10
|
+
import { runGroupSummary, runGroupSummaryContext } from './todoSmartConvert.groupSummary.js';
|
|
11
|
+
import { schemaOf, } from '../types.js';
|
|
12
|
+
const inputSchema = schemaOf({
|
|
13
|
+
stage: {
|
|
14
|
+
type: 'string',
|
|
15
|
+
enum: [...STAGE_VALUES],
|
|
16
|
+
default: 'auto',
|
|
17
|
+
description: '阶段:auto、collect、group、smartGroup、summaryContext、summary、groupSummaryContext、groupSummary 或 aggregate。',
|
|
18
|
+
},
|
|
19
|
+
items: {
|
|
20
|
+
type: 'array',
|
|
21
|
+
minItems: 1,
|
|
22
|
+
maxItems: MAX_FETCH_LIMIT,
|
|
23
|
+
description: '待办条目。未传时 collect 出网取数;传入后只做本地归一、分组或摘要回填。',
|
|
24
|
+
items: { type: 'object' },
|
|
25
|
+
},
|
|
26
|
+
aiGroups: {
|
|
27
|
+
type: 'array',
|
|
28
|
+
description: 'smartGroup 的二次分组计划。每项必须提供 groupId、title,以及 requestIds 与 seq 二选一。',
|
|
29
|
+
items: {
|
|
30
|
+
type: 'object',
|
|
31
|
+
additionalProperties: false,
|
|
32
|
+
properties: {
|
|
33
|
+
groupId: { type: 'string' },
|
|
34
|
+
title: { type: 'string' },
|
|
35
|
+
requestIds: { type: 'array', minItems: 1, items: { type: 'string' } },
|
|
36
|
+
seq: { type: 'array', minItems: 1, items: { type: 'integer', minimum: 1 } },
|
|
37
|
+
},
|
|
38
|
+
required: ['groupId', 'title'],
|
|
39
|
+
oneOf: [{ required: ['requestIds'] }, { required: ['seq'] }],
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
aiSummaries: {
|
|
43
|
+
type: 'array',
|
|
44
|
+
description: 'summary 阶段的单条摘要回填。',
|
|
45
|
+
items: {
|
|
46
|
+
type: 'object',
|
|
47
|
+
additionalProperties: false,
|
|
48
|
+
properties: { requestId: { type: 'string' }, summary: { type: 'string' } },
|
|
49
|
+
required: ['requestId', 'summary'],
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
aiGroupSummaries: {
|
|
53
|
+
type: 'array',
|
|
54
|
+
description: 'groupSummary 阶段的汇总项摘要回填。',
|
|
55
|
+
items: {
|
|
56
|
+
type: 'object',
|
|
57
|
+
additionalProperties: false,
|
|
58
|
+
properties: { groupId: { type: 'string' }, summary: { type: 'string' } },
|
|
59
|
+
required: ['groupId', 'summary'],
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
withDetailContext: { type: 'boolean', description: 'smartGroup 是否同进程生成单条摘要上下文。' },
|
|
63
|
+
withGroupSummaryContext: { type: 'boolean', description: 'summary 是否同进程生成组摘要上下文。' },
|
|
64
|
+
detailPrefetch: { type: 'boolean', description: '废弃兼容参数,已忽略;collect 只在给了 groupFocus 且意图适用于本组时按需预取分组字段(走详情缓存)。' },
|
|
65
|
+
groupBy: { type: 'string', enum: [...GROUP_BY_VALUES], default: 'workflow' },
|
|
66
|
+
groupOrder: { type: 'array', items: { type: 'string' } },
|
|
67
|
+
topN: { type: 'integer', minimum: 1, maximum: 50, default: DEFAULT_TOP_N },
|
|
68
|
+
limit: {
|
|
69
|
+
type: 'integer',
|
|
70
|
+
minimum: 1,
|
|
71
|
+
maximum: MAX_FETCH_LIMIT,
|
|
72
|
+
default: DEFAULT_FETCH_LIMIT,
|
|
73
|
+
description: 'collect 目标条数;单页最多 ' + FETCH_PAGE_SIZE + ' 条,最多 ' + MAX_FETCH_PAGES + ' 页。',
|
|
74
|
+
},
|
|
75
|
+
pageSize: {
|
|
76
|
+
type: 'integer',
|
|
77
|
+
minimum: 1,
|
|
78
|
+
maximum: FETCH_PAGE_SIZE,
|
|
79
|
+
description: '废弃兼容参数;页大小由 limit 推导,传入值只产生 warning。',
|
|
80
|
+
},
|
|
81
|
+
category: { type: 'string', enum: [...CATEGORY_VALUES], default: DEFAULT_CATEGORY },
|
|
82
|
+
tabid: { type: 'string' },
|
|
83
|
+
tabName: { type: 'string' },
|
|
84
|
+
subCategory: { type: 'string' },
|
|
85
|
+
workflow: { type: 'array', items: { type: 'string' } },
|
|
86
|
+
creator: { type: 'string' },
|
|
87
|
+
requestName: { type: 'string' },
|
|
88
|
+
receiveTime: { type: 'string' },
|
|
89
|
+
requestLevel: { type: 'string' },
|
|
90
|
+
groupFocus: { type: 'string' },
|
|
91
|
+
maxCharsPerBatch: { type: 'integer', minimum: 500, maximum: 200000, default: 24000 },
|
|
92
|
+
maxItemsPerBatch: { type: 'integer', minimum: 1, maximum: 500, default: 100 },
|
|
93
|
+
summaryMaxChars: { type: 'integer', minimum: 10, maximum: 500, default: 50 },
|
|
94
|
+
groupSummaryMaxChars: { type: 'integer', minimum: 20, maximum: 1000, default: 100 },
|
|
95
|
+
detailConcurrency: { type: 'integer', minimum: 1, maximum: 32, default: 10 },
|
|
96
|
+
detailTimeoutMs: { type: 'integer', minimum: 1000, maximum: 60000, default: 10000 },
|
|
97
|
+
skillTiming: { type: 'object' },
|
|
98
|
+
query: { type: 'object' },
|
|
99
|
+
fetched: { type: 'integer' },
|
|
100
|
+
warnings: { type: 'array', items: { type: 'string' } },
|
|
101
|
+
markdown: { type: 'string' },
|
|
102
|
+
smartSummaryTitle: { type: 'string' },
|
|
103
|
+
smartSmmaryTitle: { type: 'string', description: '兼容旧回灌数据;请使用 smartSummaryTitle。' },
|
|
104
|
+
totalCount: { type: 'integer' },
|
|
105
|
+
wfIdGroup: { type: 'array', items: { type: 'object' } },
|
|
106
|
+
summaryBatches: { type: 'array', items: { type: 'object' } },
|
|
107
|
+
summaryStats: { type: 'object' },
|
|
108
|
+
groupSummaryBatches: { type: 'array', items: { type: 'object' } },
|
|
109
|
+
groupSummaryStats: { type: 'object' },
|
|
110
|
+
groupInfo: { type: 'object' },
|
|
111
|
+
groupingBrief: { type: 'array', items: { type: 'object' } },
|
|
112
|
+
detailStats: { type: 'object' },
|
|
113
|
+
});
|
|
114
|
+
function finish(env, args, startedAt, stage, data, meta, warnings) {
|
|
115
|
+
const timing = buildSkillTiming(stage, args.skillTiming, startedAt);
|
|
116
|
+
const planned = planTodoOutput({
|
|
117
|
+
ctx: env.ctx,
|
|
118
|
+
operation: 'workflow.todoSmartConvert',
|
|
119
|
+
data,
|
|
120
|
+
meta: { ...(meta ?? {}), timing },
|
|
121
|
+
warnings,
|
|
122
|
+
});
|
|
123
|
+
return workflowSuccess('workflow.todoSmartConvert', planned.data, planned.meta, warnings);
|
|
124
|
+
}
|
|
125
|
+
export const workflowTodoSmartConvertOperations = [{
|
|
126
|
+
name: 'workflow.todoSmartConvert',
|
|
127
|
+
resource: 'workflow',
|
|
128
|
+
method: 'todoSmartConvert',
|
|
129
|
+
action: 'workflow.todoSmartConvert',
|
|
130
|
+
summary: '待办智能汇总:collect 取数,group 分组,smartGroup 组装分组,summary 回填摘要,aggregate 保留旧汇总行为。',
|
|
131
|
+
capability: 'search',
|
|
132
|
+
risk: 'read',
|
|
133
|
+
inputSchema,
|
|
134
|
+
handler: async (args, env) => {
|
|
135
|
+
assertKnownKeys(args);
|
|
136
|
+
const startedAt = Date.now();
|
|
137
|
+
const stage = args.stage === undefined || args.stage === null || args.stage === ''
|
|
138
|
+
? inferStage(args)
|
|
139
|
+
: readStage(args.stage);
|
|
140
|
+
const hasItems = Array.isArray(args.items);
|
|
141
|
+
const resumed = stage === 'collect' && hasItems;
|
|
142
|
+
if ((stage === 'group' || stage === 'smartGroup' || stage === 'aggregate') && !hasItems) {
|
|
143
|
+
throw workflowValidationError('items_required', 'stage=' + stage + ' 需要 items:先执行 stage=collect,再回灌 data.items', { retryAllowed: false });
|
|
144
|
+
}
|
|
145
|
+
if (stage === 'smartGroup') {
|
|
146
|
+
const result = await runSmartGroup(args, env);
|
|
147
|
+
if (args.withDetailContext === true) {
|
|
148
|
+
const context = await runSummaryContext({
|
|
149
|
+
...args,
|
|
150
|
+
wfIdGroup: result.data.wfIdGroup,
|
|
151
|
+
smartSummaryTitle: result.data.smartSummaryTitle,
|
|
152
|
+
totalCount: result.data.totalCount,
|
|
153
|
+
}, env, result.facts);
|
|
154
|
+
return finish(env, args, startedAt, stage, { ...result.data, summaryBatches: context.data.summaryBatches, summaryStats: context.data.summaryStats }, { branch: 'todo-smart-convert', stage, aiGrouped: Array.isArray(args.aiGroups), withDetailContext: true }, [...result.warnings, ...context.warnings]);
|
|
155
|
+
}
|
|
156
|
+
return finish(env, args, startedAt, stage, result.data, { branch: 'todo-smart-convert', stage, aiGrouped: Array.isArray(args.aiGroups) }, result.warnings);
|
|
157
|
+
}
|
|
158
|
+
if (stage === 'summaryContext') {
|
|
159
|
+
const result = await runSummaryContext(args, env);
|
|
160
|
+
return finish(env, args, startedAt, stage, result.data, {
|
|
161
|
+
branch: 'todo-smart-convert', stage, detailSource: 'REQUEST_DETAIL',
|
|
162
|
+
}, result.warnings);
|
|
163
|
+
}
|
|
164
|
+
if (stage === 'summary') {
|
|
165
|
+
if (!Array.isArray(args.aiSummaries)) {
|
|
166
|
+
throw workflowValidationError('ai_summaries_required', 'stage=summary 需要 aiSummaries:先执行 stage=summaryContext', { retryAllowed: false });
|
|
167
|
+
}
|
|
168
|
+
const result = await runSummary(args, env);
|
|
169
|
+
if (args.withGroupSummaryContext === true) {
|
|
170
|
+
const context = await runGroupSummaryContext({
|
|
171
|
+
wfIdGroup: result.data.wfIdGroup,
|
|
172
|
+
smartSummaryTitle: result.data.smartSummaryTitle,
|
|
173
|
+
totalCount: result.data.totalCount,
|
|
174
|
+
}, env);
|
|
175
|
+
if (Array.isArray(context.data.groupSummaryBatches) && context.data.groupSummaryBatches.length === 0) {
|
|
176
|
+
return finish(env, args, startedAt, stage, result.data, {
|
|
177
|
+
branch: 'todo-smart-convert', stage, aiSummarized: true, withGroupSummaryContext: true, ...result.meta,
|
|
178
|
+
}, result.warnings);
|
|
179
|
+
}
|
|
180
|
+
return finish(env, args, startedAt, stage, {
|
|
181
|
+
...result.data,
|
|
182
|
+
groupSummaryBatches: context.data.groupSummaryBatches,
|
|
183
|
+
groupSummaryStats: context.data.groupSummaryStats,
|
|
184
|
+
}, {
|
|
185
|
+
branch: 'todo-smart-convert', stage, aiSummarized: true, withGroupSummaryContext: true, ...result.meta,
|
|
186
|
+
}, [...result.warnings, ...context.warnings]);
|
|
187
|
+
}
|
|
188
|
+
return finish(env, args, startedAt, stage, result.data, {
|
|
189
|
+
branch: 'todo-smart-convert', stage, aiSummarized: true, ...result.meta,
|
|
190
|
+
}, result.warnings);
|
|
191
|
+
}
|
|
192
|
+
if (stage === 'groupSummaryContext') {
|
|
193
|
+
const result = await runGroupSummaryContext(args, env);
|
|
194
|
+
return finish(env, args, startedAt, stage, result.data, {
|
|
195
|
+
branch: 'todo-smart-convert', stage, deterministic: true,
|
|
196
|
+
}, result.warnings);
|
|
197
|
+
}
|
|
198
|
+
if (stage === 'groupSummary') {
|
|
199
|
+
if (!Array.isArray(args.aiGroupSummaries)) {
|
|
200
|
+
throw workflowValidationError('ai_group_summaries_required', 'stage=groupSummary 需要 aiGroupSummaries:先执行 stage=groupSummaryContext', { retryAllowed: false });
|
|
201
|
+
}
|
|
202
|
+
const result = await runGroupSummary(args, env);
|
|
203
|
+
return finish(env, args, startedAt, stage, result.data, {
|
|
204
|
+
branch: 'todo-smart-convert', stage, aiSummarized: true, ...result.meta,
|
|
205
|
+
}, result.warnings);
|
|
206
|
+
}
|
|
207
|
+
if (stage === 'group' || resumed) {
|
|
208
|
+
return finish(env, args, startedAt, 'group', runGroup(args), {
|
|
209
|
+
branch: 'todo-smart-convert', stage: 'group', deterministic: true, ...(resumed ? { resumed: true } : {}),
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
if (stage === 'collect' || !hasItems) {
|
|
213
|
+
const result = await collectRawTodos(args, env);
|
|
214
|
+
return finish(env, args, startedAt, 'collect', result, {
|
|
215
|
+
branch: 'todo-smart-convert', stage: 'collect',
|
|
216
|
+
}, result.warnings);
|
|
217
|
+
}
|
|
218
|
+
return finish(env, args, startedAt, 'aggregate', runSmartConvert(args), {
|
|
219
|
+
branch: 'todo-smart-convert', stage: 'aggregate', deterministic: true,
|
|
220
|
+
});
|
|
221
|
+
},
|
|
222
|
+
}];
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { workflowValidationError } from '../../errors.js';
|
|
2
|
+
import { STAGE_VALUES } from './todoSmartConvert.core.js';
|
|
3
|
+
export function readStage(value) {
|
|
4
|
+
if (value === undefined || value === null || value === '')
|
|
5
|
+
return 'auto';
|
|
6
|
+
const text = String(value);
|
|
7
|
+
if (STAGE_VALUES.includes(text))
|
|
8
|
+
return text;
|
|
9
|
+
throw workflowValidationError('stage_invalid', `stage 只支持 ${STAGE_VALUES.join(' / ')},收到 ${text}`, { retryAllowed: false, details: { allowed: STAGE_VALUES } });
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* 没显式给 `stage` 时,按**入参形态**推断下一步。
|
|
13
|
+
*
|
|
14
|
+
* 为什么必须推断:`--input-ref <refId>` 与 `--input-json` **不能同时传**(CLI 侧互斥),
|
|
15
|
+
* 而最终结构 `{ smartSummaryTitle, wfIdGroup }` 里**故意没有 `stage` 键**(多一个都破坏与
|
|
16
|
+
* demo 的同构)。若不推断,`--input-ref <smartGroup 的 ref>` 会被当成 `auto` + 无 items ⇒
|
|
17
|
+
* 又去取一次数,整条串接就断了。
|
|
18
|
+
*
|
|
19
|
+
* 规则(只认「带最终结构」这一种形态,其它一律回落到 `auto`,保持旧行为):
|
|
20
|
+
* - 有 `wfIdGroup` + 有 `aiGroupSummaries` ⇒ `groupSummary`(组摘要回填);
|
|
21
|
+
* - 有 `wfIdGroup` + 有 `aiSummaries` ⇒ `summary`(单条回填);
|
|
22
|
+
* - 有 `wfIdGroup`,且结构里**已经有非空单条摘要** ⇒ `groupSummaryContext`(下一步该写组摘要了);
|
|
23
|
+
* - 有 `wfIdGroup`,单条摘要还全空 ⇒ `summaryContext`(下一步该拉详情写单条摘要)。
|
|
24
|
+
*
|
|
25
|
+
* 最后两条靠「有没有填过单条摘要」区分:填过就说明已经走过 `summary`,不该再白拉一次详情。
|
|
26
|
+
* 想强制重跑某一步时显式传 `stage` 即可,显式永远优先。
|
|
27
|
+
*/
|
|
28
|
+
export function inferStage(args) {
|
|
29
|
+
if (!Array.isArray(args.wfIdGroup))
|
|
30
|
+
return 'auto';
|
|
31
|
+
if (Array.isArray(args.aiGroupSummaries))
|
|
32
|
+
return 'groupSummary';
|
|
33
|
+
if (Array.isArray(args.aiSummaries))
|
|
34
|
+
return 'summary';
|
|
35
|
+
return hasAnyRequestSummary(args.wfIdGroup) ? 'groupSummaryContext' : 'summaryContext';
|
|
36
|
+
}
|
|
37
|
+
/** 从**未经校验**的分组对象里摊平出条目(推断阶段用,同时兼容新旧两种结构)。 */
|
|
38
|
+
export function rowsOfRawGroup(group) {
|
|
39
|
+
if (!group)
|
|
40
|
+
return [];
|
|
41
|
+
if (Array.isArray(group.displayItems)) {
|
|
42
|
+
return group.displayItems.flatMap((item) => {
|
|
43
|
+
if (item?.type === 'group')
|
|
44
|
+
return Array.isArray(item.requestDatas) ? item.requestDatas : [];
|
|
45
|
+
if (item?.type === 'request')
|
|
46
|
+
return item.requestData ? [item.requestData] : [];
|
|
47
|
+
return [];
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
if (group.topPriority)
|
|
51
|
+
return Array.isArray(group.requestDatas) ? group.requestDatas : [];
|
|
52
|
+
return Array.isArray(group.smartGroup)
|
|
53
|
+
? group.smartGroup.flatMap((sub) => (Array.isArray(sub?.requestDatas) ? sub.requestDatas : []))
|
|
54
|
+
: [];
|
|
55
|
+
}
|
|
56
|
+
/** 结构里任意一条 `requestDatas[].summary` 非空 ⇒ 单条摘要阶段已经做过。 */
|
|
57
|
+
export function hasAnyRequestSummary(groups) {
|
|
58
|
+
for (const raw of groups) {
|
|
59
|
+
const rows = rowsOfRawGroup(raw);
|
|
60
|
+
if (rows.some((row) => typeof row?.summary === 'string' && row.summary.trim() !== '')) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
/** 只用于「接收时间区间」的比较与展示;识别不了的时间不参与区间,不猜。 */
|