weaver-work-cli 0.1.38 → 0.1.39

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.
Files changed (136) hide show
  1. package/README.md +4 -2
  2. package/dist/chunks/{archive-PDV724YK.js → archive-ENZYBBME.js} +556 -66
  3. package/dist/chunks/{archive-oa-4XE4MNXQ.js → archive-oa-YWLEC5V4.js} +2 -2
  4. package/dist/chunks/{asset-PDL2QE2N.js → asset-OOV6DWPA.js} +2 -2
  5. package/dist/chunks/asset-idle-RE632MY5.js +670 -0
  6. package/dist/chunks/{attachment-content-MDDGOE5E.js → attachment-content-RWN2FCLL.js} +3 -3
  7. package/dist/chunks/{blog-UYR3EUXW.js → blog-PEPOJAL7.js} +2 -2
  8. package/dist/chunks/{caizhilian-LG2X5NMF.js → caizhilian-WYIQDVPJ.js} +2 -2
  9. package/dist/chunks/{calendar-FG4BAZHE.js → calendar-CIARBAOX.js} +2 -2
  10. package/dist/chunks/{chunk-6FTTSA45.js → chunk-36DIFZAH.js} +2 -2
  11. package/dist/chunks/{chunk-UJJD4QWF.js → chunk-BJGXMFGD.js} +2 -2
  12. package/dist/chunks/{chunk-ELYDWBZJ.js → chunk-CQHGNCK2.js} +1 -1
  13. package/dist/chunks/{chunk-XJY7G4DD.js → chunk-EINRJHQ3.js} +3 -3
  14. package/dist/chunks/{chunk-UZZBA6PV.js → chunk-EZGFHJFW.js} +28 -6
  15. package/dist/chunks/{chunk-NI6GIPIM.js → chunk-F2EQXUEE.js} +438 -387
  16. package/dist/chunks/{chunk-VRYHGG6O.js → chunk-GVPJZXJ2.js} +3 -3
  17. package/dist/chunks/{chunk-IZUDA353.js → chunk-HJYVHXUQ.js} +1 -1
  18. package/dist/chunks/{chunk-KKRL3REV.js → chunk-I2HB737U.js} +2 -2
  19. package/dist/chunks/{chunk-NR6RKXPA.js → chunk-K3RUEEOL.js} +4 -2
  20. package/dist/chunks/{chunk-6LTFIFLM.js → chunk-KTCB5R5H.js} +2 -2
  21. package/dist/chunks/{chunk-J7USS3ED.js → chunk-L4N7B7ZD.js} +4 -4
  22. package/dist/chunks/{chunk-CORFRD7Q.js → chunk-MUQUHKPG.js} +2 -2
  23. package/dist/chunks/{chunk-DMV2TOMH.js → chunk-NLK2RZKG.js} +46 -46
  24. package/dist/chunks/{chunk-JSMFPO43.js → chunk-P2DAISYW.js} +1 -1
  25. package/dist/chunks/{chunk-GGJFMNJM.js → chunk-T7JDJJ7H.js} +3 -3
  26. package/dist/chunks/{chunk-OJQUJZQF.js → chunk-XXJOSZTQ.js} +2 -0
  27. package/dist/chunks/{chunk-Z32KWY6G.js → chunk-YS5DPN4T.js} +1 -1
  28. package/dist/chunks/{chunk-EG6ZU444.js → chunk-ZUX3P6BQ.js} +10 -5
  29. package/dist/chunks/{chunk-FUWVHMVV.js → chunk-ZXRNHZTE.js} +2 -2
  30. package/dist/chunks/{conflict-BQZEQ4SP.js → conflict-DRMDOL56.js} +2 -2
  31. package/dist/chunks/{contract-XIRAFTEV.js → contract-73MERZWN.js} +2 -2
  32. package/dist/chunks/{cowork-BLG62BL2.js → cowork-HNGBPD7A.js} +2 -2
  33. package/dist/chunks/{ebuilder-form-NUKG5QW4.js → ebuilder-form-OY6DVNRK.js} +3 -3
  34. package/dist/chunks/{ehr-2STW5ILH.js → ehr-RAQNEQO7.js} +2 -2
  35. package/dist/chunks/{esb-UUDQUNKY.js → esb-6XV7C7YV.js} +4 -4
  36. package/dist/chunks/{fna-6RCY4NU5.js → fna-XTSRB7G7.js} +2 -2
  37. package/dist/chunks/{host-RHM3NPXV.js → host-3F5UM5GN.js} +3 -3
  38. package/dist/chunks/{host-EJZFROU3.js → host-A7J6GDUP.js} +3 -3
  39. package/dist/chunks/{host-FWDOSRH5.js → host-VGPVP7YF.js} +3 -3
  40. package/dist/chunks/{host-7RXEDMAH.js → host-YAQAPYNP.js} +3 -3
  41. package/dist/chunks/{host-UFF33TWJ.js → host-YMBXWFWX.js} +3 -3
  42. package/dist/chunks/{hrm-PKJ6ELSH.js → hrm-TLHKC545.js} +3 -3
  43. package/dist/chunks/{im-YJAXS7CO.js → im-YOUCFMUK.js} +3 -3
  44. package/dist/chunks/{invoice-HWVSXGX6.js → invoice-WNBS2B4C.js} +3 -3
  45. package/dist/chunks/{jiuchuanhui-L7VKILZB.js → jiuchuanhui-XABWS5RN.js} +2 -2
  46. package/dist/chunks/{lazy-dispatch-NMYEIHRH.js → lazy-dispatch-NLZ5UQQI.js} +5 -5
  47. package/dist/chunks/{mail-B42RRU7G.js → mail-LRYMBFNL.js} +2 -2
  48. package/dist/chunks/{meeting-CFYVBJSS.js → meeting-ESXL6MFI.js} +10 -10
  49. package/dist/chunks/{okr-GCLF6V7W.js → okr-EOV65SSF.js} +2 -2
  50. package/dist/chunks/{plan-ZVL7GBRI.js → plan-YFPVKW6Y.js} +2 -2
  51. package/dist/chunks/{project-4YWAAE2K.js → project-MDVZUYQW.js} +2 -2
  52. package/dist/chunks/{registry-EP4VTHZS.js → registry-QCLZEDAP.js} +8 -8
  53. package/dist/chunks/{registry-FRJ6H7BT.js → registry-VT5Y2HLA.js} +4 -4
  54. package/dist/chunks/{registry-SG2PGVCF.js → registry-YX4CSEMD.js} +3 -3
  55. package/dist/chunks/{room-recommend-R3JVUUAC.js → room-recommend-L3MWH4JW.js} +4 -4
  56. package/dist/chunks/{schedule-NIJBTYDO.js → schedule-UASAVDVD.js} +4 -4
  57. package/dist/chunks/{workflow-BB24DFBY.js → workflow-KDGIUCNW.js} +9 -6
  58. package/dist/core/output.js +16 -6
  59. package/dist/index.js +33 -28
  60. package/dist/internal/e10/auth/messages.js +1 -0
  61. package/dist/internal/e10/auth-recovery.js +13 -6
  62. package/dist/internal/e10/context.js +16 -3
  63. package/dist/internal/e10/request-runtime.js +3 -1
  64. package/dist/shortcuts/archive/host.js +2 -0
  65. package/dist/shortcuts/archive/manifest.js +1 -1
  66. package/dist/shortcuts/archive/operations/borrow-list.js +13 -1
  67. package/dist/shortcuts/archive/operations/flow-url.js +1 -1
  68. package/dist/shortcuts/archive/operations/prelib.js +472 -0
  69. package/dist/shortcuts/archive/operations/registry.js +2 -0
  70. package/dist/shortcuts/archive/operations/upload.js +1 -1
  71. package/dist/shortcuts/asset-idle/errors.js +44 -0
  72. package/dist/shortcuts/asset-idle/host.js +161 -0
  73. package/dist/shortcuts/asset-idle/index.js +114 -0
  74. package/dist/shortcuts/asset-idle/manifest.js +159 -0
  75. package/dist/shortcuts/asset-idle/operations/idle.js +47 -0
  76. package/dist/shortcuts/asset-idle/operations/registry.js +33 -0
  77. package/dist/shortcuts/asset-idle/operations/shared.js +142 -0
  78. package/dist/shortcuts/asset-idle/operations/types.js +9 -0
  79. package/dist/shortcuts/asset-idle/operations.js +15 -0
  80. package/dist/shortcuts/index.js +5 -0
  81. package/dist/shortcuts/workflow/host.js +3 -0
  82. package/dist/shortcuts/workflow/index.js +1 -1
  83. package/dist/shortcuts/workflow/operations/form.js +46 -171
  84. package/dist/shortcuts/workflow/operations/todoSmartConvert/aigroupRules.js +77 -26
  85. package/dist/shortcuts/workflow/operations/todoSmartConvert/aigroupStore.js +6 -0
  86. package/dist/shortcuts/workflow/operations/todoSmartConvert/summaryCache.js +59 -117
  87. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoDetailCache.js +44 -7
  88. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoDetailFetcher.js +37 -29
  89. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.collect.js +20 -6
  90. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.detail.js +22 -18
  91. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.groupSummary.js +34 -10
  92. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.js +4 -2
  93. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.smartGroup.js +9 -2
  94. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.summary.js +110 -20
  95. package/docs/_catalog.md +3 -0
  96. package/docs/asset-idle.md +45 -0
  97. package/docs/e10-auth.md +1 -1
  98. package/docs/todo-smart-convert/subgroup-rules.md +1 -1
  99. package/package.json +1 -1
  100. package/scripts/package-skill.mjs +2 -0
  101. package/skill-template/business-info.json +14 -1
  102. package/skill-template/domains/asset-idle.md +13 -0
  103. package/skill-template/domains/wenshuding.md +3 -0
  104. package/skills/weaver-e10-archive-oa/SKILL.md +1 -0
  105. package/skills/weaver-e10-archive-oa/references/my-borrow.md +49 -0
  106. package/skills/weaver-e10-asset-idle/SKILL.md +73 -0
  107. package/skills/weaver-e10-asset-idle/references/idle-list.md +62 -0
  108. package/skills/weaver-e10-asset-idle/references/idle-stats-and-charts.md +65 -0
  109. package/skills/weaver-e10-asset-idle/references/source-manifest.json +193 -0
  110. package/skills/weaver-e10-asset-idle/references/widget-style.md +205 -0
  111. package/skills/weaver-e10-caizhilian/SKILL.md +29 -11
  112. package/skills/weaver-e10-caizhilian/references/auth-and-install.md +12 -0
  113. package/skills/weaver-e10-caizhilian/references/files.md +1 -1
  114. package/skills/weaver-e10-caizhilian/references/setting-encrypt.md +1 -1
  115. package/skills/weaver-e10-caizhilian/references/source-manifest.json +45 -45
  116. package/skills/weaver-e10-cowork/SKILL.md +2 -0
  117. package/skills/weaver-e10-jucailin/SKILL.md +2 -0
  118. package/skills/weaver-e10-jucailin/references/recruit-read.md +2 -0
  119. package/skills/weaver-e10-jucailin/references/recruit-write.md +1 -1
  120. package/skills/weaver-e10-mail/SKILL.md +3 -0
  121. package/skills/weaver-e10-shared/SKILL.md +5 -5
  122. package/skills/weaver-e10-shared/references/e10-auth-and-session.md +3 -3
  123. package/skills/weaver-e10-wenshuding/SKILL.md +29 -6
  124. package/skills/weaver-e10-wenshuding/references/archive-borrow-list.md +14 -0
  125. package/skills/weaver-e10-wenshuding/references/archive-download.md +2 -0
  126. package/skills/weaver-e10-wenshuding/references/archive-flow.md +1 -1
  127. package/skills/weaver-e10-wenshuding/references/archive-prelib-arrange.md +76 -0
  128. package/skills/weaver-e10-wenshuding/references/archive-prelib-list.md +107 -0
  129. package/skills/weaver-e10-wenshuding/references/archive-prelib-open.md +59 -0
  130. package/skills/weaver-e10-wenshuding/references/archive-upload.md +4 -1
  131. package/skills/weaver-e10-wenshuding/references/source-manifest.json +108 -65
  132. package/skills/weaver-e10-workflow/SKILL.md +5 -2
  133. package/skills/weaver-e10-workflow/aigroup/default.json +154 -19
  134. package/skills/weaver-e10-workflow/references/todo-smart-convert.md +59 -27
  135. package/skills/weaver-e10-workflow/tools/todo-serve.cjs +113 -25
  136. package/skills/weaver-e10-workflow/tools/todo-summary.cjs +292 -42
@@ -0,0 +1,15 @@
1
+ import { assetIdleManifest } from './manifest.js';
2
+ import { getAssetIdleOperation } from './operations/registry.js';
3
+ import { assertKnownKeys, assertNoForbiddenKeys, asObject } from './operations/shared.js';
4
+ /** 顶层输入白名单直接取自 manifest:schema 即运行时约束,避免两处规则漂移。 */
5
+ const ALLOWED_KEYS = Object.fromEntries(assetIdleManifest.operations.map((operation) => [
6
+ operation.name,
7
+ Object.keys(operation.inputSchema.properties ?? {}),
8
+ ]));
9
+ export async function executeAssetIdleOperation(operation, rawInput, context) {
10
+ const definition = getAssetIdleOperation(operation);
11
+ const input = asObject(rawInput);
12
+ assertNoForbiddenKeys(input);
13
+ assertKnownKeys(input, ALLOWED_KEYS[operation] ?? [], operation);
14
+ return definition.handler(input, context);
15
+ }
@@ -105,6 +105,11 @@ export const builtinShortcuts = [
105
105
  description: 'E10 资产管理(资管家)CLI',
106
106
  load: async () => (await import('./asset/index.js')).assetShortcut,
107
107
  }),
108
+ lazyShortcut({
109
+ name: 'asset-idle',
110
+ description: 'E10 闲置资产判定结果查询 CLI(分页获取/聚合统计)',
111
+ load: async () => (await import('./asset-idle/index.js')).assetIdleShortcut,
112
+ }),
108
113
  lazyShortcut({
109
114
  name: 'im',
110
115
  description: '泛微E10易秒办(即时通讯)CLI',
@@ -2,6 +2,7 @@ import { E10_SESSION_EXPIRED_MESSAGE } from '../../internal/e10/auth/messages.js
2
2
  import { requireE10Session } from '../../internal/e10/context.js';
3
3
  import { getActiveProfile } from '../../internal/e10/auth/session.js';
4
4
  import { createE10RequestRuntime } from '../../internal/e10/request-runtime.js';
5
+ import { isE10AuthenticationRequired } from '../../internal/e10/auth-recovery.js';
5
6
  import { workflowApiError, workflowAuthError, workflowNetworkError } from './errors.js';
6
7
  const DEFAULT_TIMEOUT_MS = 30_000;
7
8
  /**
@@ -81,6 +82,8 @@ export function isAtustRedirect(status, value) {
81
82
  export function isLoginExpired(value, httpStatus) {
82
83
  if (httpStatus === 401 || httpStatus === 403)
83
84
  return true;
85
+ if (isE10AuthenticationRequired({ status: httpStatus ?? 200, value }))
86
+ return true;
84
87
  if (isAtustRedirect(httpStatus, value))
85
88
  return true;
86
89
  const joined = payloadTexts(value).join('\n').toLowerCase();
@@ -215,7 +215,7 @@ export const workflowShortcut = {
215
215
  const user = readUserIntents({ ctx: ctx });
216
216
  ctx.output.write({
217
217
  dir: loaded.dir,
218
- defaultFile: join(loaded.dir, 'default.json'),
218
+ defaultFile: loaded.dir ? join(loaded.dir, 'default.json') : null,
219
219
  userIntentFile: user.file,
220
220
  userIntentExists: user.exists,
221
221
  userIntentError: user.error,
@@ -34,8 +34,16 @@ import { buildChatSceneInput, formatLocalNow, requestFormFill } from './chatscen
34
34
  const TEXTAREA_COMPONENTS = new Set(['textarea', 'richtext', 'editor', 'html', 'htmltext']);
35
35
  const SELECT_COMPONENTS = new Set(['select', 'selectmultiple', 'radiobox', 'checkbox', 'radio', 'multiselect']);
36
36
  const BROWSER_COMPONENTS = new Set([
37
- 'employee', 'department', 'subcompany', 'jobtitle', 'job', 'relatebrowser',
38
- 'browser', 'workflow', 'ebuilder', 'customer', 'project',
37
+ // 人员 / 组织类
38
+ 'employee', 'department', 'subcompany', 'jobtitle', 'job',
39
+ 'employeeorganization', 'employeeshare', 'employeeauthshare', 'odocreceivecomponent',
40
+ // 关联类型(设计器「表单组件 / 关联类型」面板)
41
+ 'ebuilder', 'mainline', 'project', 'task', 'document', 'workflow', 'formcomponent',
42
+ 'customer', 'customercomponent', 'cluecomponent', 'ordercomponent', 'contactcomponent',
43
+ 'productioncomponent', 'contractcomponent', 'activitycomponent', 'competitorcomponent',
44
+ 'kpiflowcomponent', 'workreportcomponent', 'attendcomponent', 'agendacomponent', 'chancecomponent',
45
+ 'relatebrowser', 'relateodoc', 'relatetag', 'relateintegrate', 'modulecomponent', 'paas',
46
+ 'quotecomponent', 'drivingrecord', 'browser',
39
47
  ]);
40
48
  const NON_INPUT_COMPONENTS = new Set([
41
49
  'paragraph', 'label', 'divider', 'option', 'detailtable', 'group', 'layout', 'internaltitle', 'multilingualtag',
@@ -593,14 +601,7 @@ function extractFields(layoutData, requiredMap, disableEditMap, displayMap) {
593
601
  // (Python 的 `bool({})` 为 False)——直接照抄 Python 的 `bool(browser_type)` 会让**每个字段**
594
602
  // 都被判成浏览按钮字段,文本/富文本字段随之走 name→ID 解析并报 BROWSER NOT_FOUND,
595
603
  // 内容永远写不进去(2026-09-16「内部留言」留言内容字段实证)。必须判"非空对象"。
596
- // 组件名有两种写法:eBuilder 表单里常带 `Component` 后缀(`CustomerComponent` /
597
- // `EmployeeComponent`),标准流程表单里是不带的(`customer` / `employee`)。
598
- // ⛔ 只认前者会漏判:2026-10-31 会议申请流程的「参会客户」是 `CustomerComponent`,
599
- // 被当成普通文本字段 ⇒ 提交 `[{id,name}]` 报「文本字段只接受字符串、数字或布尔值」。
600
- const componentBase = normalized.replace(/component$/, '');
601
- const isBrowser = Object.keys(browserType).length > 0
602
- || BROWSER_COMPONENTS.has(normalized)
603
- || BROWSER_COMPONENTS.has(componentBase);
604
+ const isBrowser = Object.keys(browserType).length > 0 || BROWSER_COMPONENTS.has(normalized);
604
605
  const isSelect = SELECT_COMPONENTS.has(normalized) || options.length > 0;
605
606
  result.push({
606
607
  id: fieldId,
@@ -2147,31 +2148,6 @@ async function buildCreateFormData(workflowId, env, options = {}) {
2147
2148
  continue;
2148
2149
  visibleFields.push(f);
2149
2150
  }
2150
- /**
2151
- * 明细表**整表被判不可见**时的显式告警(2026-10-31)。
2152
- *
2153
- * 场景:明细表被摆在了页面网格的**隐藏行 / 隐藏列**上(`pageLayouts[0].config.workbook.Sheet1.rows[n].visible=false`),
2154
- * 于是它的列字段全部被 `hiddenCellIds` 过滤掉。此前是**静默丢弃** —— 模型只看到"表单里没有这张明细表",
2155
- * 分不清「表单确实没配」和「配了但摆在隐藏行上」,只能回一句"没有这张表",用户无从下手。
2156
- * 这里按 subFormId 汇总,整表字段全灭的才告警(部分列隐藏属正常设计)。
2157
- */
2158
- const hiddenBySubform = new Map();
2159
- for (const f of fields) {
2160
- const fid = String(f.id ?? '');
2161
- const sid = String(f.subFormId ?? '').trim();
2162
- if (!sid || !hiddenCellIds.has(fid))
2163
- continue;
2164
- hiddenBySubform.set(sid, (hiddenBySubform.get(sid) || 0) + 1);
2165
- }
2166
- const survivingSubforms = new Set(visibleFields.map((f) => String(f.subFormId ?? '').trim()).filter(Boolean));
2167
- const invisibleSubformWarnings = [];
2168
- for (const [sid, count] of hiddenBySubform) {
2169
- if (survivingSubforms.has(sid))
2170
- continue;
2171
- invisibleSubformWarnings.push(`明细表 ${sid} 的 ${count} 个字段全部被判为「不可见」(该明细表所在的行/列在表单布局里是隐藏的),`
2172
- + '因此没有出现在字段清单里。要么在表单设计器里把明细表挪到可见行,要么取消隐藏它所在的行/列,然后重新载入表单。');
2173
- }
2174
- let layoutWarning = invisibleSubformWarnings.length ? invisibleSubformWarnings.join(';') : null;
2175
2151
  const defaults = normalizeDefaults(layoutData.defaultDetails);
2176
2152
  // 计算必填/可编辑/明细必填(按默认值 + onLoadForm 联动)
2177
2153
  const systemFieldValues = _normalizeSystemFieldValues(layoutData.businessOperatorObj);
@@ -2236,52 +2212,6 @@ async function buildCreateFormData(workflowId, env, options = {}) {
2236
2212
  }
2237
2213
  }
2238
2214
  const folderOptionValues = _mainValueMap(defaults);
2239
- /**
2240
- * 「条件显示」的明细表(2026-10-31,会议申请流程「参会客户明细」实证)
2241
- *
2242
- * 表单可以给明细表配**显示联动**:触发字段有值时 `show`、为空时 `hideLine`。
2243
- * 会议申请流程的「参会客户明细」(subFormId `1332370499762806784`)就是这样 ——
2244
- * 触发字段是「参会客户」(`crm_members`),空着时整表隐藏。
2245
- *
2246
- * 旧行为把它与「明细表被摆在隐藏行」一视同仁 ⇒ 整表字段从 `fields` 里消失,
2247
- * 建会链路既解析不到明细表也写不进明细,模型只能回「表单里没有这张表」。
2248
- * 现在:布局上确实存在、且有成对 `show` 规则的表,按**条件可见**处理 ——
2249
- * 字段照常下发(标 `conditional: true`),并在 `layoutWarning` 里说明触发字段。
2250
- */
2251
- const conditionalHiddenSubforms = new Set();
2252
- const conditionalMainFields = new Set();
2253
- const conditionalHints = collectDetailShowLinkages(layoutData.viewLinkages, fields);
2254
- /**
2255
- * 主表字段同理:「参会客户」(`crm_members`) 自己也有成对的 `show` / `hideLine` 规则
2256
- * (空值时连自己都隐藏)。若不这样处理,建会链路的 `crm_members` 永远映射不到字段。
2257
- */
2258
- const fieldShowIds = collectFieldShowLinkageIds(layoutData.viewLinkages);
2259
- for (const fid of Array.from(mainHidden)) {
2260
- if (!fieldShowIds.has(fid))
2261
- continue;
2262
- if (hiddenCellIds.has(fid))
2263
- continue;
2264
- const target = visibleFields.find((f) => String(f.id ?? '') === fid);
2265
- if (!target)
2266
- continue;
2267
- conditionalMainFields.add(fid);
2268
- mainHidden.delete(fid);
2269
- invisibleSubformWarnings.push(`字段「${target.name}」(${fid}) 因显示联动隐藏(该字段本身是触发字段),已照常下发并标 conditional: true`);
2270
- }
2271
- for (const sid of Array.from(hiddenSubforms)) {
2272
- const hint = conditionalHints.get(sid);
2273
- if (!hint)
2274
- continue;
2275
- const usable = visibleFields.some((f) => String(f.subFormId ?? '') === sid && !hiddenCellIds.has(String(f.id ?? '')));
2276
- if (!usable)
2277
- continue;
2278
- conditionalHiddenSubforms.add(sid);
2279
- hiddenSubforms.delete(sid);
2280
- invisibleSubformWarnings.push(`明细表 ${sid}${hint.tableName ? `(${hint.tableName})` : ''} 因显示联动隐藏,触发字段:`
2281
- + `${hint.triggerNames.join('、') || hint.triggerIds.join('、')}。`
2282
- + '该表字段已照常下发(标 conditional: true),把触发字段填上后表单里就会显示;不要回「表单里没有这张明细表」。');
2283
- }
2284
- layoutWarning = invisibleSubformWarnings.length ? invisibleSubformWarnings.join(';') : null;
2285
2215
  for (const field of visibleFields) {
2286
2216
  const fieldId = String(field.id ?? '');
2287
2217
  const subFormId = String(field.subFormId ?? '');
@@ -2381,11 +2311,6 @@ async function buildCreateFormData(workflowId, env, options = {}) {
2381
2311
  else if (slimSubFormId) {
2382
2312
  item.subFormId = slimSubFormId;
2383
2313
  }
2384
- // 条件可见的明细表(见 conditionalHiddenSubforms 注释):字段可用,但当前触发字段没值时表单里不显示。
2385
- if (slimSubFormId && conditionalHiddenSubforms.has(slimSubFormId))
2386
- item.conditional = true;
2387
- if (!slimSubFormId && conditionalMainFields.has(String(field.id ?? '')))
2388
- item.conditional = true;
2389
2314
  item.componentKey = field.componentKey;
2390
2315
  // 表单初始化时就有的值(OA 预填 + onLoadForm 联动带出)——有才出现该键;**没有默认值的字段不带**。
2391
2316
  // CLI 拼 `data` 时会自动并入,模型**不必显式传**;用户本次明确改写了它才按用户说的覆盖(见
@@ -2544,8 +2469,6 @@ async function buildCreateFormData(workflowId, env, options = {}) {
2544
2469
  },
2545
2470
  systemFieldWarning: minorWarning,
2546
2471
  requiredInfoWarning: requiredInfoWarningText(context),
2547
- // 明细表整表不可见时的告警(见 layoutWarning 注释):有值时**不要**回"表单里没有这张明细表"。
2548
- layoutWarning,
2549
2472
  // 原 Python 把完整 context 落盘;此处通过 continuation 承载,返回结构保持文档契约。
2550
2473
  continuation: token,
2551
2474
  },
@@ -4047,74 +3970,6 @@ function mergeLinkageDetails(merged, linkageDetails) {
4047
3970
  function emptyLinkageVisibility() {
4048
3971
  return [new Set(), {}, {}, {}];
4049
3972
  }
4050
- /**
4051
- * 收集主表字段的 `show` 联动字段 id(`linkageObj=field` 且 `linkageProp=show`)。
4052
- * 这类字段的隐藏是**条件性**的,按条件可见处理,见 `buildCreateFormData` 的 `conditionalMainFields`。
4053
- */
4054
- export function collectFieldShowLinkageIds(linkages) {
4055
- const out = new Set();
4056
- if (!Array.isArray(linkages))
4057
- return out;
4058
- for (const rule of linkages) {
4059
- const details = Array.isArray(rule?.linkageDetails) ? rule.linkageDetails : [];
4060
- for (const detail of details) {
4061
- if (!detail || typeof detail !== 'object')
4062
- continue;
4063
- if (String(detail.linkageObj ?? '') !== 'field')
4064
- continue;
4065
- if (String(detail.linkageProp ?? '') !== 'show')
4066
- continue;
4067
- const fid = String(detail.linkageValue ?? '').trim();
4068
- if (fid)
4069
- out.add(fid);
4070
- }
4071
- }
4072
- return out;
4073
- }
4074
- /**
4075
- * 收集「明细表的显示联动」——`linkageObj=detail` 且 `linkageProp=show` 的规则。
4076
- *
4077
- * 用途见 `buildCreateFormData` 里 `conditionalHiddenSubforms` 的注释:配了 `show` 规则的明细表
4078
- * 是**条件可见**(另有配套的 `hideLine` 规则),不能因为当期条件不成立就把整表字段丢掉。
4079
- * 返回 `subFormId → { triggerIds, triggerNames, tableName }`,triggerNames 供告警文本直接引用。
4080
- */
4081
- export function collectDetailShowLinkages(linkages, fields) {
4082
- const out = new Map();
4083
- if (!Array.isArray(linkages))
4084
- return out;
4085
- const list = Array.isArray(fields) ? fields : [];
4086
- const nameOf = (id) => {
4087
- for (const f of list) {
4088
- if (String(f?.id ?? '') === id)
4089
- return String(f?.name ?? id);
4090
- }
4091
- return id;
4092
- };
4093
- for (const rule of linkages) {
4094
- const details = Array.isArray(rule?.linkageDetails) ? rule.linkageDetails : [];
4095
- for (const detail of details) {
4096
- if (!detail || typeof detail !== 'object')
4097
- continue;
4098
- if (String(detail.linkageObj ?? '') !== 'detail')
4099
- continue;
4100
- if (String(detail.linkageProp ?? '') !== 'show')
4101
- continue;
4102
- const sid = String(detail.linkageValue ?? '').trim();
4103
- if (!sid)
4104
- continue;
4105
- const conds = Array.isArray(rule?.condition?.conditionDetails) ? rule.condition.conditionDetails : [];
4106
- const triggerIds = conds
4107
- .map((c) => String(c?.targetId ?? '').trim())
4108
- .filter(Boolean);
4109
- out.set(sid, {
4110
- triggerIds,
4111
- triggerNames: triggerIds.map(nameOf),
4112
- tableName: nameOf(sid) === sid ? '' : nameOf(sid),
4113
- });
4114
- }
4115
- }
4116
- return out;
4117
- }
4118
3973
  function computeLinkageVisibility(linkages, entries, defaults, linkageDetails, systemFieldValues, fields) {
4119
3974
  const hiddenSubforms = new Set();
4120
3975
  const rowHidden = {};
@@ -4879,8 +4734,19 @@ const XIAOE_FILL_FORM_SDK_OPTIONS = {
4879
4734
  */
4880
4735
  const XIAOE_INITIAL_VALUES_MULTI_SEPARATOR = ',';
4881
4736
  const XIAOE_INITIAL_VALUES_ROW_SEPARATOR = ';';
4737
+ /**
4738
+ * 浏览按钮字段的值是**对象 id** 时加的前缀(2026-11-02 用户裁定)。
4739
+ *
4740
+ * 口径:`isBrowser === true` 的字段值来自 `[{id, name}]` 里的 `id`,落进 `customParams` / `src`
4741
+ * 时写成 `##<id>`,让页面能区分「这是记录 id」与「这是用户手打的名称」;而**名称不拼前缀**
4742
+ * (非浏览字段的值本来就是字符串,原样透传)。
4743
+ *
4744
+ * 多值逐个判定,所以 `##<id>,<名称>` 是合法形态(同一字段里既有解析出的 id、又有原样名称)。
4745
+ * 明细多行同口径:`##<id>;<名称>`。
4746
+ */
4747
+ const XIAOE_BROWSER_ID_PREFIX = '##';
4882
4748
  /** 单个标量 → URL 传参里的那一段文本。对象按「id 优先」取标识(附件取到的就是 fileid)。 */
4883
- function xiaoEUrlParamScalar(value) {
4749
+ function xiaoEUrlParamScalar(value, prefixBrowserId = false) {
4884
4750
  if (value === null || value === undefined)
4885
4751
  return '';
4886
4752
  if (typeof value === 'object') {
@@ -4893,24 +4759,26 @@ function xiaoEUrlParamScalar(value) {
4893
4759
  if (picked === null || picked === undefined)
4894
4760
  continue;
4895
4761
  const text = String(picked).trim();
4896
- if (text)
4897
- return text;
4762
+ if (!text)
4763
+ continue;
4764
+ // 浏览按钮字段:这一段的来源是对象 id(不是用户给的名称),按 2026-11-02 口径加 `##`。
4765
+ return prefixBrowserId ? `${XIAOE_BROWSER_ID_PREFIX}${text}` : text;
4898
4766
  }
4899
4767
  return '';
4900
4768
  }
4901
4769
  return String(value).trim();
4902
4770
  }
4903
4771
  /** 单个字段的值 → URL 传参里的那一段文本(数组按「同字段多值」用 `,` 连接)。 */
4904
- function xiaoEUrlParamValue(value) {
4772
+ function xiaoEUrlParamValue(value, prefixBrowserId = false) {
4905
4773
  if (value === null || value === undefined)
4906
4774
  return '';
4907
4775
  if (Array.isArray(value)) {
4908
4776
  return value
4909
- .map((item) => xiaoEUrlParamScalar(item))
4777
+ .map((item) => xiaoEUrlParamScalar(item, prefixBrowserId))
4910
4778
  .filter(Boolean)
4911
4779
  .join(XIAOE_INITIAL_VALUES_MULTI_SEPARATOR);
4912
4780
  }
4913
- return xiaoEUrlParamScalar(value);
4781
+ return xiaoEUrlParamScalar(value, prefixBrowserId);
4914
4782
  }
4915
4783
  /**
4916
4784
  * `params.customParams` 里**字段值**那一份 = `{字段id: 值}`(用户裁定用**字段 id**,不用数据 key);
@@ -4921,6 +4789,10 @@ function xiaoEUrlParamValue(value) {
4921
4789
  *
4922
4790
  * 明细字段(`preview` 里带 `dataIndex`)按**行号升序**取值、用 `;` 连接;主表字段就是它自己的值。
4923
4791
  * 值为空(没解析出内容)的字段整个省略,不生成空参数。
4792
+ *
4793
+ * **浏览按钮字段的值加 `##` 前缀**(2026-11-02 用户裁定,见 `XIAOE_BROWSER_ID_PREFIX`):这类字段解析
4794
+ * 出来的每一段都是对象 id,写成 `##<id>`;名称(非浏览字段的字符串)原样不加。多值/多行逐个判定,
4795
+ * 因此 `##<id>,<名称>`、`##<id>;<名称>` 都是合法形态。`src` 末尾的字段参数与这里同源,一起带上。
4924
4796
  */
4925
4797
  /**
4926
4798
  * 小e 档交给浏览器页面的字段值:只留**可见且可编辑**的那些(2026-10-21 用户裁定)。
@@ -4963,15 +4835,23 @@ function editablePreviewForXiaoE(preview, fields, visibility) {
4963
4835
  }
4964
4836
  return kept;
4965
4837
  }
4966
- function xiaoECustomParams(preview) {
4838
+ function xiaoECustomParams(preview, fields) {
4967
4839
  const ordered = [];
4968
4840
  const mainValues = new Map();
4969
4841
  const detailValues = new Map();
4842
+ // 「这个字段是不是浏览按钮」决定要不要给对象 id 拼 `##`(2026-11-02 用户裁定)——
4843
+ // 判据取 `extractFields` 算好的 `isBrowser`,与值解析走的是同一份结论,不在这里二次推断。
4844
+ const browserFieldIds = new Set();
4845
+ for (const field of toArray(fields)) {
4846
+ const id = String(field?.id ?? '').trim();
4847
+ if (id && field?.isBrowser)
4848
+ browserFieldIds.add(id);
4849
+ }
4970
4850
  for (const item of toArray(preview)) {
4971
4851
  const fieldId = String(item?.fieldId ?? '').trim();
4972
4852
  if (!fieldId)
4973
4853
  continue;
4974
- const text = xiaoEUrlParamValue(item?.value);
4854
+ const text = xiaoEUrlParamValue(item?.value, browserFieldIds.has(fieldId));
4975
4855
  if (!text)
4976
4856
  continue;
4977
4857
  const isDetail = item?.dataIndex !== undefined && item?.dataIndex !== null;
@@ -5220,12 +5100,7 @@ async function runCreateRequest(args, env, options = {}) {
5220
5100
  * (`buildCreateFormData`,那边的联动结果要用来算字段的必填 / 可编辑清单);这里两者都不跑 ——
5221
5101
  * 本档既不做必填判定、也不发写请求。非小e 环境行为不变。
5222
5102
  */
5223
- // ⛔ 2026-10-31 真机回归:调用方可以显式要求「小e 环境也走服务端提交」(meeting 建会直提用)。
5224
- // 起因:小e 的浏览器填单页只给**文本类**字段赋值,浏览按钮(参会人员 / 参会客户)传过去整段是空 ——
5225
- // 用户看到的就是"填了但表单里这两个字段没有值"。服务端提交这条路上这些字段走标准 JSON,
5226
- // 能完整落库,所以建会这类「字段必须完整」的场景应走直提,浏览器填单只留给需要人工核对/补填的场合。
5227
- const forceServerSubmit = args.forceServerSubmit === true;
5228
- const isXiaoE = forceServerSubmit ? false : hasXiaoEEnv(env.ctx.env, env.ctx.getGlobalOptions?.());
5103
+ const isXiaoE = hasXiaoEEnv(env.ctx.env, env.ctx.getGlobalOptions?.());
5229
5104
  const linkageDetails = isXiaoE
5230
5105
  ? []
5231
5106
  : await runFieldLinkage(env, linkageContextFromPayload(context), entries, null, merged, false, context.defaults || []);
@@ -5392,7 +5267,7 @@ async function runCreateRequest(args, env, options = {}) {
5392
5267
  baseUrl: xiaoEBaseUrl,
5393
5268
  title: String(context.workflow?.name ?? ''),
5394
5269
  workflowId: String(context.workflow?.id ?? context.runtime?.workflow_id ?? ''),
5395
- customParams: xiaoECustomParams(fillPreview),
5270
+ customParams: xiaoECustomParams(fillPreview, fields),
5396
5271
  }),
5397
5272
  });
5398
5273
  }
@@ -12,7 +12,7 @@
12
12
  import { existsSync, readFileSync } from 'node:fs';
13
13
  import { join } from 'node:path';
14
14
  import { stateRoot } from '../../../../core/state.js';
15
- import { cacheRoots } from './summaryCache.js';
15
+ import { resolveSkillDir } from './summaryCache.js';
16
16
  export const AIGROUP_DIR_NAME = 'aigroup';
17
17
  export const DEFAULT_RULE_FILE = 'default.json';
18
18
  export const USER_INTENT_RULE_FILE = 'user-intents.json';
@@ -30,6 +30,7 @@ export const FALLBACK_DEFAULTS = {
30
30
  summaryMaxChars: 150,
31
31
  groupSummaryMaxChars: 100,
32
32
  groupSummaryTail: '可展开逐条查看或直接汇总处理',
33
+ excludeOriginFormDataFields: [],
33
34
  };
34
35
  export function emptyRuleFile(error = null) {
35
36
  return {
@@ -66,16 +67,38 @@ function validateFocusKeyword(raw, index) {
66
67
  return `focusKeyword[${index}] 必须是 [指标, 算子, 值] 三元组`;
67
68
  return null;
68
69
  }
70
+ /** 校验排除字段清单:缺省放行;给了就必须是字符串或字符串数组(允许空数组)。 */
71
+ function validateExcludeFields(raw, where) {
72
+ if (raw === undefined)
73
+ return null;
74
+ if (typeof raw === 'string') {
75
+ return raw.trim() === '' ? `${where} 不能是空字符串(不排除就别给这个键)` : null;
76
+ }
77
+ if (!Array.isArray(raw))
78
+ return `${where} 必须是字符串数组`;
79
+ for (const item of raw) {
80
+ if (typeof item !== 'string' || item.trim() === '')
81
+ return `${where} 的每一项都必须是非空字符串`;
82
+ }
83
+ return null;
84
+ }
69
85
  /** 校验整份规则文件;返回错误列表(空数组 = 合法)。 */
70
86
  export function validateRuleFile(raw) {
71
87
  const errors = [];
72
88
  const file = raw;
73
89
  if (!file || typeof file !== 'object')
74
90
  return ['规则文件必须是 JSON 对象'];
91
+ // schemaVersion:不能是废字段。给值时必须受支持(2 = aigroup set 写入的意图文件,3 = default.json);缺省允许(规则级校验兼容)。
92
+ if (file.schemaVersion !== undefined && file.schemaVersion !== 2 && file.schemaVersion !== 3) {
93
+ errors.push(`schemaVersion 只支持 2 或 3,收到 ${JSON.stringify(file.schemaVersion ?? null)}`);
94
+ }
75
95
  if (!Array.isArray(file.rules)) {
76
96
  errors.push('rules 必须是数组');
77
97
  return errors;
78
98
  }
99
+ const defaultsExclude = validateExcludeFields(file.defaults?.excludeOriginFormDataFields, 'defaults.excludeOriginFormDataFields');
100
+ if (defaultsExclude)
101
+ errors.push(defaultsExclude);
79
102
  const seen = new Set();
80
103
  file.rules.forEach((rule, index) => {
81
104
  if (!rule || typeof rule !== 'object') {
@@ -98,6 +121,16 @@ export function validateRuleFile(raw) {
98
121
  if (rule.summaryMaxChars !== undefined && (typeof rule.summaryMaxChars !== 'number' || rule.summaryMaxChars <= 0)) {
99
122
  errors.push(`rules[${index}].summaryMaxChars 必须是正数`);
100
123
  }
124
+ const excludeReason = validateExcludeFields(rule.excludeOriginFormDataFields, `rules[${index}].excludeOriginFormDataFields`);
125
+ if (excludeReason)
126
+ errors.push(excludeReason);
127
+ if (rule.summaryRequiredFields !== undefined) {
128
+ if (!Array.isArray(rule.summaryRequiredFields) || rule.summaryRequiredFields.length === 0
129
+ || rule.summaryRequiredFields.some((f) => typeof f !== 'string' || !f.trim() || f.trim().length > 50)
130
+ || rule.summaryRequiredFields.length > 20) {
131
+ errors.push(`rules[${index}].summaryRequiredFields 必须是非空字符串数组(1~20 项、每项 1~50 字)`);
132
+ }
133
+ }
101
134
  const groupRule = rule.aiGroup?.groupRule;
102
135
  if (groupRule) {
103
136
  if (typeof groupRule.focus !== 'boolean')
@@ -162,47 +195,42 @@ export function loadRuleFile(file) {
162
195
  return emptyRuleFile(`规则文件校验失败:${errors.join(';')}`);
163
196
  return { file: parsed, error: null };
164
197
  }
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
198
  /**
174
- * 用户意图目录(**运行时产物,绝不写进 skill 目录**):`WEAVER_WORK_CLI_AIGROUP_DIR` → CLI 状态目录。
175
- *
176
- * 刻意与 `aigroupDir()` 分开:意图文件是本机产生的,一旦落在 skill 目录里,
177
- * 打包/安装时会跟着 skill 跑到别人机器上(既是泄漏,也会污染别人的分组口径)。
199
+ * 当前 skill 的 aigroup 目录(`default.json` 与 `user-intents.json` 同目录;
200
+ * 2026-11-01 用户定稿:规则与意图都锚定**当前 agent 的 skill 目录**)。
201
+ * 裸 CLI(无当前 skill 上下文)返回 null ⇒ 规则不加载、意图不可读写。
178
202
  */
203
+ export function aigroupDir(ctx) {
204
+ const skillDir = resolveSkillDir();
205
+ return skillDir ? join(skillDir, AIGROUP_DIR_NAME) : null;
206
+ }
207
+ /** 用户意图目录:与 `default.json` 同目录(见 `aigroupDir`)。 */
179
208
  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);
209
+ return aigroupDir(ctx);
184
210
  }
185
211
  /**
186
212
  * 加载并合并规则:默认文件 +(可选)用户意图文件(**同 workflowName 整条覆盖**,系统默认不被覆盖)。
187
213
  * 用户意图文件里的条目按 `workflowName` 粒度替换默认条目,缺字段由 defaults 补。
214
+ * 裸 CLI(无当前 skill 上下文)⇒ 按无规则处理 + warning(缓存与规则都拒绝,见 cacheRoots)。
188
215
  */
189
216
  export function loadRules(options = {}) {
190
- const dir = options.dir ?? aigroupDir(options.ctx);
191
- // 显式 `dir` 时(测试/现场)默认文件与意图文件放一起;否则意图文件只从状态目录读。
192
- const userDir = options.dir ?? aigroupUserDir(options.ctx);
193
217
  const warnings = [];
218
+ const dir = options.dir ?? aigroupDir(options.ctx);
219
+ if (!dir) {
220
+ warnings.push('无当前 skill 上下文(WEAVER_WORK_CLI_SKILL_DIR 未设置):aigroup 规则未加载,按无规则处理');
221
+ return { file: emptyRuleFile(null).file, warnings, dir: null };
222
+ }
194
223
  const base = loadRuleFile(join(dir, DEFAULT_RULE_FILE));
195
224
  if (base.error)
196
225
  warnings.push(base.error);
197
226
  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);
227
+ // 意图文件与 default.json 同目录;兼容迁移:老位置(CLI 状态目录)若存在且新位置没有 ⇒ 回读 + 提示
228
+ const userIntentFile = join(dir, USER_INTENT_RULE_FILE);
229
+ const legacyIntentFile = join(stateRoot(options.ctx), AIGROUP_DIR_NAME, USER_INTENT_RULE_FILE);
202
230
  let intentFile = userIntentFile;
203
231
  if (!options.dir && !existsSync(userIntentFile) && existsSync(legacyIntentFile)) {
204
232
  intentFile = legacyIntentFile;
205
- warnings.push(`用户意图仍读自 skill 目录(${legacyIntentFile}):新位置是 ${userIntentFile},重新执行 aigroup set 即可迁移`);
233
+ warnings.push(`用户意图仍读自 CLI 状态目录(${legacyIntentFile}):新位置是 ${userIntentFile},重新执行 aigroup set 即可迁移`);
206
234
  }
207
235
  const intent = loadRuleFile(intentFile);
208
236
  if (intent.error) {
@@ -274,13 +302,16 @@ export function resolveRule(file, workflowName) {
274
302
  summaryMaxChars: raw.summaryMaxChars ?? defaults.summaryMaxChars,
275
303
  groupSummaryMaxChars: defaults.groupSummaryMaxChars,
276
304
  groupSummaryTail: defaults.groupSummaryTail,
305
+ // 显式给 `[]` = 规则级覆盖(清空 defaults 的清单);未给才落 defaults
306
+ excludeOriginFormDataFields: raw.excludeOriginFormDataFields === undefined
307
+ ? defaults.excludeOriginFormDataFields
308
+ : asStringArray(raw.excludeOriginFormDataFields) ?? [],
277
309
  aiGroup: raw.aiGroup
278
310
  ? {
279
311
  groupSummary: {
280
312
  needAI: groupSummary?.needAI ?? false,
281
313
  ...(groupSummary?.prompt === undefined ? {} : { prompt: groupSummary.prompt }),
282
314
  ...(groupSummary?.template === undefined ? {} : { template: groupSummary.template }),
283
- maxChars: defaults.groupSummaryMaxChars,
284
315
  tail: defaults.groupSummaryTail,
285
316
  },
286
317
  groupRule: groupRule ?? EMPTY_GROUP_RULE,
@@ -446,3 +477,23 @@ export function resetActiveRules() {
446
477
  activeFile = null;
447
478
  activeDir = null;
448
479
  }
480
+ // ---------------------------------------------------------------------------
481
+ // formData 排除字段(2026-10-30 用户定稿:excludeOriginFormDataFields)
482
+ // ---------------------------------------------------------------------------
483
+ /**
484
+ * 当前生效规则的 formData 排除字段清单(空数组 = 不排除)。
485
+ *
486
+ * 消费方:`todoDetailFetcher.fetchOneDetail` —— 拿到接口 formData 后按 fieldName 精确匹配剔除;
487
+ * 详情缓存用 `excludeTagFor()` 做**口径失效**:清单一变,旧缓存条目整体 miss。
488
+ */
489
+ export function excludeFieldsFor(workflowName) {
490
+ return activeRuleFor(workflowName)?.excludeOriginFormDataFields ?? [];
491
+ }
492
+ /** 排除清单的特征串(原文拼接)。规则没配 = 空串,与「无口径」条目互相匹配。 */
493
+ export function excludeTagOf(fields) {
494
+ return [...fields].join('\u0001');
495
+ }
496
+ /** 便捷组合:某流程当前生效的排除口径特征串。 */
497
+ export function excludeTagFor(workflowName) {
498
+ return excludeTagOf(excludeFieldsFor(workflowName));
499
+ }
@@ -44,6 +44,8 @@ function namesOf(rule) {
44
44
  */
45
45
  export function saveUserIntents(payload, options = {}) {
46
46
  const dir = options.dir ?? aigroupUserDir(options.ctx);
47
+ if (!dir)
48
+ throw workflowValidationError('aigroup_no_skill_context', '无当前 skill 上下文(WEAVER_WORK_CLI_SKILL_DIR 未设置):用户意图不可读写');
47
49
  const warnings = [];
48
50
  const entries = normalizeIntentPayload(payload);
49
51
  if (entries.length === 0)
@@ -86,6 +88,8 @@ export function saveUserIntents(payload, options = {}) {
86
88
  /** 读用户意图文件(诊断/展示用)。 */
87
89
  export function readUserIntents(options = {}) {
88
90
  const dir = options.dir ?? aigroupUserDir(options.ctx);
91
+ if (!dir)
92
+ return { dir: null, file: '', exists: false, error: '无当前 skill 上下文(WEAVER_WORK_CLI_SKILL_DIR 未设置)', rules: [] };
89
93
  const file = join(dir, USER_INTENT_RULE_FILE);
90
94
  if (!existsSync(file))
91
95
  return { dir, file, exists: false, error: null, rules: [] };
@@ -95,6 +99,8 @@ export function readUserIntents(options = {}) {
95
99
  /** 清空用户意图(只删内容、不删文件,保持目录结构)。 */
96
100
  export function clearUserIntents(options = {}) {
97
101
  const dir = options.dir ?? aigroupUserDir(options.ctx);
102
+ if (!dir)
103
+ throw workflowValidationError('aigroup_no_skill_context', '无当前 skill 上下文(WEAVER_WORK_CLI_SKILL_DIR 未设置):用户意图不可读写');
98
104
  const file = join(dir, USER_INTENT_RULE_FILE);
99
105
  mkdirSync(dir, { recursive: true, mode: 0o700 });
100
106
  writeFileSync(file, `${JSON.stringify({ schemaVersion: 2, rules: [] }, null, 2)}\n`, { mode: 0o600 });