weaver-work-cli 0.1.36 → 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 (169) hide show
  1. package/README.md +4 -2
  2. package/dist/chunks/{archive-XBVH7JU7.js → archive-ENZYBBME.js} +559 -68
  3. package/dist/chunks/{archive-oa-MZM3EKFQ.js → archive-oa-YWLEC5V4.js} +4 -3
  4. package/dist/chunks/{asset-4XO7UOUW.js → asset-OOV6DWPA.js} +8 -7
  5. package/dist/chunks/asset-idle-RE632MY5.js +670 -0
  6. package/dist/chunks/{attachment-content-VXSWANAC.js → attachment-content-RWN2FCLL.js} +3 -3
  7. package/dist/chunks/{blog-PGZKOPSC.js → blog-PEPOJAL7.js} +5 -4
  8. package/dist/chunks/{caizhilian-BH4ZVGUN.js → caizhilian-WYIQDVPJ.js} +5 -4
  9. package/dist/chunks/{calendar-MXEMTIO4.js → calendar-CIARBAOX.js} +4 -3
  10. package/dist/chunks/{chunk-NCOVUAZK.js → chunk-36DIFZAH.js} +4 -3
  11. package/dist/chunks/{chunk-I6QGIBDM.js → chunk-BJGXMFGD.js} +4 -3
  12. package/dist/chunks/{chunk-TC6MZAO3.js → chunk-CQHGNCK2.js} +52 -2
  13. package/dist/chunks/{chunk-WXFPWCXG.js → chunk-EINRJHQ3.js} +3 -3
  14. package/dist/chunks/{chunk-JUXVYKII.js → chunk-EZGFHJFW.js} +201 -19
  15. package/dist/chunks/{chunk-VPUTVEMC.js → chunk-F2EQXUEE.js} +436 -290
  16. package/dist/chunks/{chunk-JYKABYCV.js → chunk-GVPJZXJ2.js} +3 -3
  17. package/dist/chunks/{chunk-6LLJFKJE.js → chunk-HJYVHXUQ.js} +1 -1
  18. package/dist/chunks/{chunk-T72N74ZT.js → chunk-I2HB737U.js} +5 -4
  19. package/dist/chunks/{chunk-VPXOPQJI.js → chunk-K3RUEEOL.js} +4 -2
  20. package/dist/chunks/{chunk-HXJQI5NM.js → chunk-KTCB5R5H.js} +3 -2
  21. package/dist/chunks/{chunk-XMGSYD6S.js → chunk-L4N7B7ZD.js} +4 -4
  22. package/dist/chunks/{chunk-QYGO5EE2.js → chunk-MUQUHKPG.js} +4 -3
  23. package/dist/chunks/{chunk-QHFANTX7.js → chunk-NLK2RZKG.js} +1348 -90
  24. package/dist/chunks/{chunk-F746NKNF.js → chunk-P2DAISYW.js} +1 -1
  25. package/dist/chunks/{chunk-BPE37FPB.js → chunk-T7JDJJ7H.js} +3 -3
  26. package/dist/chunks/{chunk-JNG42JKG.js → chunk-XXJOSZTQ.js} +6 -6
  27. package/dist/chunks/{chunk-5DQEEGCC.js → chunk-YS5DPN4T.js} +1 -1
  28. package/dist/chunks/{chunk-EG6ZU444.js → chunk-ZUX3P6BQ.js} +10 -5
  29. package/dist/chunks/{chunk-JCSN3JGM.js → chunk-ZXRNHZTE.js} +7 -6
  30. package/dist/chunks/{conflict-JAB2LJEZ.js → conflict-DRMDOL56.js} +2 -2
  31. package/dist/chunks/{contract-XI2EEIZU.js → contract-73MERZWN.js} +4 -3
  32. package/dist/chunks/{cowork-SCPGLBA4.js → cowork-HNGBPD7A.js} +4 -3
  33. package/dist/chunks/{ebuilder-form-EYLHNOTA.js → ebuilder-form-OY6DVNRK.js} +3 -3
  34. package/dist/chunks/{ehr-EAP77IMT.js → ehr-RAQNEQO7.js} +4 -3
  35. package/dist/chunks/{esb-2TGGXEE7.js → esb-6XV7C7YV.js} +4 -4
  36. package/dist/chunks/{fna-2X7ZE7BU.js → fna-XTSRB7G7.js} +4 -3
  37. package/dist/chunks/{host-Q4LZGCRU.js → host-3F5UM5GN.js} +3 -3
  38. package/dist/chunks/{host-XCLDDXKQ.js → host-A7J6GDUP.js} +3 -3
  39. package/dist/chunks/{host-I4NRK4MX.js → host-VGPVP7YF.js} +3 -3
  40. package/dist/chunks/{host-C6EL2RKL.js → host-YAQAPYNP.js} +3 -3
  41. package/dist/chunks/{host-UEI5FGDM.js → host-YMBXWFWX.js} +3 -3
  42. package/dist/chunks/{hrm-YXERJM76.js → hrm-TLHKC545.js} +3 -3
  43. package/dist/chunks/{im-A4VZN2TQ.js → im-YOUCFMUK.js} +3 -3
  44. package/dist/chunks/{invoice-2Z6YPMIS.js → invoice-WNBS2B4C.js} +3 -3
  45. package/dist/chunks/{jiuchuanhui-AR5GMKZB.js → jiuchuanhui-XABWS5RN.js} +4 -3
  46. package/dist/chunks/{lazy-dispatch-QXW7P4X3.js → lazy-dispatch-NLZ5UQQI.js} +5 -5
  47. package/dist/chunks/{mail-7GZ6CPBJ.js → mail-LRYMBFNL.js} +4 -3
  48. package/dist/chunks/{meeting-3NMGA3MB.js → meeting-ESXL6MFI.js} +13 -12
  49. package/dist/chunks/{okr-BZ6XUOAG.js → okr-EOV65SSF.js} +4 -3
  50. package/dist/chunks/{plan-DR5CMUYP.js → plan-YFPVKW6Y.js} +4 -3
  51. package/dist/chunks/{project-6J63MK33.js → project-MDVZUYQW.js} +4 -3
  52. package/dist/chunks/{registry-VRTNC6UR.js → registry-QCLZEDAP.js} +8 -8
  53. package/dist/chunks/{registry-KDD7DHNK.js → registry-VT5Y2HLA.js} +4 -4
  54. package/dist/chunks/{registry-TKX2E7C7.js → registry-YX4CSEMD.js} +3 -3
  55. package/dist/chunks/{room-recommend-XCWRMB5P.js → room-recommend-L3MWH4JW.js} +4 -4
  56. package/dist/chunks/{schedule-IVBICP32.js → schedule-UASAVDVD.js} +4 -4
  57. package/dist/chunks/{workflow-D74NFK3C.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 +127 -7
  62. package/dist/internal/e10/client.js +16 -2
  63. package/dist/internal/e10/context.js +16 -3
  64. package/dist/internal/e10/request-runtime.js +106 -8
  65. package/dist/shortcuts/archive/host.js +5 -2
  66. package/dist/shortcuts/archive/manifest.js +1 -1
  67. package/dist/shortcuts/archive/operations/borrow-list.js +13 -1
  68. package/dist/shortcuts/archive/operations/flow-url.js +1 -1
  69. package/dist/shortcuts/archive/operations/prelib.js +472 -0
  70. package/dist/shortcuts/archive/operations/registry.js +2 -0
  71. package/dist/shortcuts/archive/operations/upload.js +1 -1
  72. package/dist/shortcuts/archive-oa/host.js +2 -1
  73. package/dist/shortcuts/asset/host.js +6 -5
  74. package/dist/shortcuts/asset-idle/errors.js +44 -0
  75. package/dist/shortcuts/asset-idle/host.js +161 -0
  76. package/dist/shortcuts/asset-idle/index.js +114 -0
  77. package/dist/shortcuts/asset-idle/manifest.js +159 -0
  78. package/dist/shortcuts/asset-idle/operations/idle.js +47 -0
  79. package/dist/shortcuts/asset-idle/operations/registry.js +33 -0
  80. package/dist/shortcuts/asset-idle/operations/shared.js +142 -0
  81. package/dist/shortcuts/asset-idle/operations/types.js +9 -0
  82. package/dist/shortcuts/asset-idle/operations.js +15 -0
  83. package/dist/shortcuts/blog/host.js +3 -2
  84. package/dist/shortcuts/caizhilian/host.js +3 -2
  85. package/dist/shortcuts/calendar/host.js +2 -1
  86. package/dist/shortcuts/contract/host.js +2 -1
  87. package/dist/shortcuts/cowork/host.js +2 -1
  88. package/dist/shortcuts/ebuilder-form/host.js +3 -1
  89. package/dist/shortcuts/ehr/host.js +2 -1
  90. package/dist/shortcuts/esb/host.js +2 -1
  91. package/dist/shortcuts/fna/host.js +2 -1
  92. package/dist/shortcuts/hrm/host.js +2 -1
  93. package/dist/shortcuts/im/host.js +3 -2
  94. package/dist/shortcuts/index.js +5 -0
  95. package/dist/shortcuts/invoice/host.js +5 -4
  96. package/dist/shortcuts/jiuchuanhui/host.js +2 -1
  97. package/dist/shortcuts/mail/host.js +2 -1
  98. package/dist/shortcuts/meeting/host.js +3 -2
  99. package/dist/shortcuts/meeting/invitation/text.js +71 -0
  100. package/dist/shortcuts/meeting/operations/eb/create-flow.js +1073 -50
  101. package/dist/shortcuts/meeting/operations/eb/crm-search.js +478 -0
  102. package/dist/shortcuts/meeting/operations/eb/factory.js +60 -1
  103. package/dist/shortcuts/meeting/operations/eb/index.js +2 -0
  104. package/dist/shortcuts/meeting/operations/invitation.js +122 -9
  105. package/dist/shortcuts/okr/host.js +2 -1
  106. package/dist/shortcuts/plan/host.js +2 -1
  107. package/dist/shortcuts/project/host.js +2 -1
  108. package/dist/shortcuts/workflow/host.js +3 -0
  109. package/dist/shortcuts/workflow/index.js +1 -1
  110. package/dist/shortcuts/workflow/operations/form.js +44 -11
  111. package/dist/shortcuts/workflow/operations/todoSmartConvert/aigroupRules.js +77 -26
  112. package/dist/shortcuts/workflow/operations/todoSmartConvert/aigroupStore.js +6 -0
  113. package/dist/shortcuts/workflow/operations/todoSmartConvert/summaryCache.js +59 -117
  114. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoDetailCache.js +44 -7
  115. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoDetailFetcher.js +37 -29
  116. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.collect.js +20 -6
  117. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.detail.js +22 -18
  118. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.groupSummary.js +34 -10
  119. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.js +4 -2
  120. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.smartGroup.js +9 -2
  121. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.summary.js +110 -20
  122. package/docs/_catalog.md +3 -0
  123. package/docs/asset-idle.md +45 -0
  124. package/docs/e10-auth.md +1 -1
  125. package/docs/todo-smart-convert/subgroup-rules.md +1 -1
  126. package/package.json +1 -1
  127. package/scripts/package-skill.mjs +2 -0
  128. package/skill-template/business-info.json +14 -1
  129. package/skill-template/domains/asset-idle.md +13 -0
  130. package/skill-template/domains/wenshuding.md +3 -0
  131. package/skills/weaver-e10-archive-oa/SKILL.md +1 -0
  132. package/skills/weaver-e10-archive-oa/references/my-borrow.md +49 -0
  133. package/skills/weaver-e10-asset-idle/SKILL.md +73 -0
  134. package/skills/weaver-e10-asset-idle/references/idle-list.md +62 -0
  135. package/skills/weaver-e10-asset-idle/references/idle-stats-and-charts.md +65 -0
  136. package/skills/weaver-e10-asset-idle/references/source-manifest.json +193 -0
  137. package/skills/weaver-e10-asset-idle/references/widget-style.md +205 -0
  138. package/skills/weaver-e10-caizhilian/SKILL.md +29 -11
  139. package/skills/weaver-e10-caizhilian/references/auth-and-install.md +12 -0
  140. package/skills/weaver-e10-caizhilian/references/files.md +1 -1
  141. package/skills/weaver-e10-caizhilian/references/setting-encrypt.md +1 -1
  142. package/skills/weaver-e10-caizhilian/references/source-manifest.json +45 -45
  143. package/skills/weaver-e10-cowork/SKILL.md +2 -0
  144. package/skills/weaver-e10-jucailin/SKILL.md +2 -0
  145. package/skills/weaver-e10-jucailin/references/recruit-read.md +2 -0
  146. package/skills/weaver-e10-jucailin/references/recruit-write.md +1 -1
  147. package/skills/weaver-e10-mail/SKILL.md +3 -0
  148. package/skills/weaver-e10-meeting/SKILL.md +250 -301
  149. package/skills/weaver-e10-meeting/references/meeting-eb.md +203 -0
  150. package/skills/weaver-e10-meeting/references/meeting-invitation.md +37 -5
  151. package/skills/weaver-e10-meeting.zip +0 -0
  152. package/skills/weaver-e10-okr/references/source-manifest.json +3 -3
  153. package/skills/weaver-e10-shared/SKILL.md +5 -5
  154. package/skills/weaver-e10-shared/references/e10-auth-and-session.md +3 -3
  155. package/skills/weaver-e10-wenshuding/SKILL.md +29 -6
  156. package/skills/weaver-e10-wenshuding/references/archive-borrow-list.md +14 -0
  157. package/skills/weaver-e10-wenshuding/references/archive-download.md +2 -0
  158. package/skills/weaver-e10-wenshuding/references/archive-flow.md +1 -1
  159. package/skills/weaver-e10-wenshuding/references/archive-prelib-arrange.md +76 -0
  160. package/skills/weaver-e10-wenshuding/references/archive-prelib-list.md +107 -0
  161. package/skills/weaver-e10-wenshuding/references/archive-prelib-open.md +59 -0
  162. package/skills/weaver-e10-wenshuding/references/archive-upload.md +4 -1
  163. package/skills/weaver-e10-wenshuding/references/source-manifest.json +108 -65
  164. package/skills/weaver-e10-workflow/SKILL.md +5 -2
  165. package/skills/weaver-e10-workflow/aigroup/default.json +154 -19
  166. package/skills/weaver-e10-workflow/references/todo-smart-convert.md +59 -27
  167. package/skills/weaver-e10-workflow/tools/todo-serve.cjs +113 -25
  168. package/skills/weaver-e10-workflow/tools/todo-summary.cjs +292 -42
  169. package/skills/weaver-e10-yimiaoban/references/source-manifest.json +3 -3
@@ -20,6 +20,7 @@ import { fileURLToPath } from 'node:url';
20
20
  import { meetingValidationError } from '../errors.js';
21
21
  import { meetingStateDir } from './env.js';
22
22
  import { renderDocx } from '../invitation/docx.js';
23
+ import { renderInvitationMarkdown } from '../invitation/text.js';
23
24
  import { meetingSuccess, SCHEMA_ID } from './types.js';
24
25
  /** 模板随 CLI 包分发(`templates/` 已在 package.json 的 files 白名单内)。 */
25
26
  function templatePath() {
@@ -250,8 +251,80 @@ async function resolveIssuerOrganization(env, creatorId) {
250
251
  return null;
251
252
  }
252
253
  }
253
- /** 组装占位符解析器:派生字段 → 会议标准字段 → extra(租户自定义字段)→ 待补充。 */
254
- export function buildResolver(row, extra, issuerOrg) {
254
+ /**
255
+ * 取当前登录人的姓名 —— 会议既没填召集人、又没有创建人名时,用**发起人本人**兜底。
256
+ *
257
+ * 理由:邀请函里「XXX 诚邀您参加…」这句必须有个人名,否则通篇【待补充】没法看。
258
+ * 实际业务上发起建会的人就是召集人,比留空更接近事实(2026-10-31 用户对【待补充】有意见)。
259
+ */
260
+ async function resolveSessionUserName(env) {
261
+ try {
262
+ const session = await env.host.getSession();
263
+ const userId = String(session?.userId ?? '').trim();
264
+ if (!userId)
265
+ return '';
266
+ const { hrmEmployeeOperations } = await import('../../hrm/operations/employee.js');
267
+ const batch = hrmEmployeeOperations.find((operation) => operation.name === 'hrm.employee.batch');
268
+ if (!batch)
269
+ return '';
270
+ const result = await batch.handler({ ids: [userId] }, env);
271
+ const row = (result?.data?.rows ?? [])[0];
272
+ return String(row?.name ?? '').trim();
273
+ }
274
+ catch {
275
+ return '';
276
+ }
277
+ }
278
+ /**
279
+ * 用 `meeting.eb.meeting.detail` 补列表单查里**空着**的字段。
280
+ *
281
+ * 为什么需要(2026-10-31 真机取证):同一个会议
282
+ * · `meeting.eb.meeting.list`(单查)→ `hrm_members = ""`
283
+ * · `meeting.eb.meeting.detail` → `hrm_members = "王丹丹"`
284
+ * 邀请函原先只读 list,于是"明明有参会人却写成【待补充】"—— 这正是用户投诉过的
285
+ * 「参会人员没有带出来」。detail 的价值在于**字段更多且有值**,代价是 `address` 回的是
286
+ * 会议室 id(不是名称),所以这里**只补白名单字段、且只填空值**,绝不覆盖 list 已有的值。
287
+ */
288
+ const DETAIL_FALLBACK_KEYS = [
289
+ 'hrm_members', 'mt_desc', 'total_member', 'caller', 'contacter',
290
+ 'begin_datetime', 'end_datetime', 'other_members', 'mt_creator',
291
+ 'topic_presenter', 'mt_format', 'mt_type', 'video_url',
292
+ 'org_members', 'with_secauth',
293
+ ];
294
+ async function supplementRowFromDetail(row, mtId, env) {
295
+ const isBlank = (value) => {
296
+ if (value === undefined || value === null)
297
+ return true;
298
+ if (Array.isArray(value))
299
+ return value.length === 0;
300
+ return String(value).trim() === '';
301
+ };
302
+ const missing = DETAIL_FALLBACK_KEYS.filter((key) => isBlank(row[key]));
303
+ if (missing.length === 0)
304
+ return { row, filled: [] };
305
+ try {
306
+ const { getMeetingOperation } = await import('./registry.js');
307
+ const detailOperation = getMeetingOperation('meeting.eb.meeting.detail');
308
+ const result = await detailOperation.handler({ id: mtId }, env);
309
+ // detail 的字段是**扁平**挂在 `data.data` 下(不是 mainTable),两种都兜一下
310
+ const flat = (result?.data?.data ?? result?.data ?? {});
311
+ const merged = { ...row };
312
+ const filled = [];
313
+ for (const key of missing) {
314
+ const value = flat[key];
315
+ if (!isBlank(value)) {
316
+ merged[key] = value;
317
+ filled.push(key);
318
+ }
319
+ }
320
+ return { row: merged, filled };
321
+ }
322
+ catch {
323
+ // detail 读失败不能拖垮邀请函:列表数据照样能出一份(缺的字段仍标【待补充】)
324
+ return { row, filled: [] };
325
+ }
326
+ }
327
+ /** 组装占位符解析器:派生字段 → 会议标准字段 → extra(租户自定义字段)→ 待补充。 */ export function buildResolver(row, extra, issuerOrg) {
255
328
  const begin = parseDateTime(row.begin_datetime);
256
329
  const end = parseDateTime(row.end_datetime);
257
330
  const derived = {
@@ -297,6 +370,9 @@ export function buildResolver(row, extra, issuerOrg) {
297
370
  org_members: orgMembers,
298
371
  // 落款公司全称:`extra.company_name` 可覆盖(对外开放时客户传自己的公司名)
299
372
  company_name: String(extra.company_name ?? '').trim() || DEFAULT_COMPANY_NAME,
373
+ // 客户单位(「尊敬的 XXX」):先看 extra(建会时由 create.flow 把参会客户名带进来),
374
+ // 再看会议主表的 crm_members(部分租户会存),都没有才留【待补充】
375
+ guest_org: String(extra.guest_org ?? '').trim() || flattenFieldValue(row.crm_members) || '',
300
376
  // 单独暴露上级公司,模板需要「公司级落款」时可直接用
301
377
  issuer_company: issuerCompany,
302
378
  other_members: humanizeMembers(row.other_members),
@@ -320,8 +396,14 @@ export function buildResolver(row, extra, issuerOrg) {
320
396
  const lookup = (key) => {
321
397
  if (key in derived)
322
398
  return derived[key];
323
- if (key in standard)
324
- return standard[key];
399
+ if (key in standard) {
400
+ const standardValue = standard[key];
401
+ // ⛔ 会议里该字段**空着**时要继续往 extra 找(2026-10-31):
402
+ // 以前 `key in standard` 直接返回空串,导致「会议没填召集人 → extra.caller 也压不上」,
403
+ // 明明调用方补了值还是渲染成【待补充】。空值不算"有值",让 extra 有机会兜底。
404
+ if (String(standardValue ?? '').trim() !== '')
405
+ return standardValue;
406
+ }
325
407
  if (key.startsWith('extra.') && key.slice(6) in extra)
326
408
  return extra[key.slice(6)];
327
409
  if (key in extra)
@@ -353,7 +435,10 @@ const meetingInvitationRender = {
353
435
  resource: 'invitation',
354
436
  method: 'render',
355
437
  action: 'meeting.invitation.render',
356
- summary: '按会议真实数据生成邀请函 docx(只读,不发送):数据取 meeting.eb.meeting.list 单查;议程有议题就按议题自动填、没有则保留模板示例行;取不到的字段渲染成黄色高亮【待补充】;租户自定义字段用 extra 传入',
438
+ summary: '按会议真实数据生成邀请函(只读,不发送):**同时返回 docx 与对话内可展示的 Markdown 全文**(`text` 字段),'
439
+ + '会话里应直接把 `text` 贴出来,不要只给文件名让人自己下载;'
440
+ + '数据取 meeting.eb.meeting.list 单查;议程有议题就按议题自动填、没有则保留模板示例行;'
441
+ + '取不到的字段渲染成【待补充】(docx 里是黄色高亮);租户自定义字段用 extra 传入',
357
442
  capability: 'invitation',
358
443
  risk: 'read',
359
444
  inputSchema: {
@@ -395,6 +480,8 @@ const meetingInvitationRender = {
395
480
  let row;
396
481
  /** meeting=按真实会议数据;payload=按调用方传入的申请单字段(会议可能还没生成)。 */
397
482
  let dataSource = 'meeting';
483
+ /** 用 detail 补回来的字段名(列表单查会丢「参会人员」,见下方注释)。 */
484
+ let detailSupplementary = [];
398
485
  if (!mtId) {
399
486
  // ⛔ 邀请函本质是「预置函」:项目人员下载后自己改、自己发(2026-10-30 用户明确要求)。
400
487
  // 所以不该卡在"会议必须已生成"——流程还在审批时,用申请单已知信息先生成一份,
@@ -437,6 +524,21 @@ const meetingInvitationRender = {
437
524
  if (!row) {
438
525
  throw meetingValidationError('not_found', `未找到会议 id=${mtId},或当前账号无权限查看`, { mtId });
439
526
  }
527
+ // 列表单查在真机上会**丢「参会人员」**(2026-10-31 实测:同一会议 list 里 `hrm_members=""`,
528
+ // detail 里是「王丹丹」)⇒ 只对**空字段**用 detail 补,避免邀请函把明明有的人写成【待补充】。
529
+ const supplement = await supplementRowFromDetail(row, mtId, env);
530
+ row = supplement.row;
531
+ detailSupplementary = supplement.filled;
532
+ }
533
+ // 召集人再兜一层:会议没填 caller、也没有创建人名时,用**发起人本人**(当前登录人)。
534
+ // 否则邀请函开头「XXX 诚邀您参加…」整句就是【待补充】,对话里贴出来很难看。
535
+ let callerFromSession = false;
536
+ if (!String(row.caller ?? '').trim() && !String(row.mt_creator ?? '').trim()) {
537
+ const sessionName = await resolveSessionUserName(env);
538
+ if (sessionName) {
539
+ row = { ...row, caller: sessionName };
540
+ callerFromSession = true;
541
+ }
440
542
  }
441
543
  // 落款单位回落:会议 org_members 为空时,用「发起人(creator 人员 id)的所属组织」
442
544
  const creatorId = String(row.creator ?? '').trim();
@@ -463,6 +565,10 @@ const meetingInvitationRender = {
463
565
  const rendered = renderDocx(template, resolver, {
464
566
  agenda: { headerKeyword: AGENDA_HEADER_KEYWORD, rows: agendaRows },
465
567
  });
568
+ // 对话内文本版(2026-10-31 用户要求「直接展示在对话里」):
569
+ // docx 是给打印/外发的成品,但会话里用户看不见内容 ⇒ 同一份 resolver 再渲染一遍 Markdown,
570
+ // 上层(create.flow / 模型回复)可以直接把 text 贴出来,字段与 docx 完全一致。
571
+ const textVersion = renderInvitationMarkdown(resolver, agendaRows);
466
572
  const begin = parseDateTime(row.begin_datetime);
467
573
  const dateTag = begin ? `${begin.getFullYear()}${pad(begin.getMonth() + 1)}${pad(begin.getDate())}` : '未知日期';
468
574
  const fileName = `邀请函-${safeFileName(String(row.mt_name ?? ''))}-${dateTag}.docx`;
@@ -499,14 +605,21 @@ const meetingInvitationRender = {
499
605
  meetingName: String(row.mt_name ?? ''),
500
606
  // meeting=按真实会议数据;payload=按申请单字段预生成(会议可能还没生成,属预置函)
501
607
  dataSource,
608
+ // 列表单查丢字段时,用 detail 补回来的字段名(有值 = 邀请函用的是合并后的数据)
609
+ detailSupplemented: detailSupplementary,
610
+ // 对话内展示用的 Markdown 全文(与 docx 同源)。上层应直接把这段贴进回复正文,
611
+ // ⛔ 不要只丢一个 filePath 让用户自己去本地打开看内容。
612
+ text: textVersion.markdown,
502
613
  filledFields: rendered.filled,
503
614
  pendingFields: rendered.pending,
504
615
  // 落款单位的实际来源,便于调用方判断是否需要人工改写
505
616
  orgSource,
506
- // 召集人来源:meeting.caller=会议自己填的 / meeting.mt_creator=回落到创建人 / none=都没有
507
- callerSource: String(row.caller ?? '').trim()
508
- ? 'meeting.caller'
509
- : (String(row.mt_creator ?? '').trim() ? 'meeting.mt_creator' : 'none'),
617
+ // 召集人来源:meeting.caller=会议自己填的 / meeting.mt_creator=回落到创建人 / session.user=回落到发起人本人 / none=都没有
618
+ callerSource: callerFromSession
619
+ ? 'session.user'
620
+ : (String(row.caller ?? '').trim()
621
+ ? 'meeting.caller'
622
+ : (String(row.mt_creator ?? '').trim() ? 'meeting.mt_creator' : 'none')),
510
623
  // 议程来源:meeting.topic=按议题自动填 / extra=调用方传入 / template=保留模板示例行
511
624
  agendaSource,
512
625
  agendaRows: agendaRows.length,
@@ -6,6 +6,7 @@ import { E10_SESSION_EXPIRED_MESSAGE } from '../../internal/e10/auth/messages.js
6
6
  import { getActiveProfile } from '../../internal/e10/auth/session.js';
7
7
  import { requireE10Session } from '../../internal/e10/context.js';
8
8
  import { E10ApiError, createE10RequestRuntime } from '../../internal/e10/request-runtime.js';
9
+ import { isE10SessionExpiredResponse } from '../../internal/e10/auth-recovery.js';
9
10
  import { okrStateRoot } from './continuation.js';
10
11
  import { OkrCliError, okrAuthError, okrNetworkError, okrValidationError } from './errors.js';
11
12
  function inferProfile(ctx) {
@@ -143,7 +144,7 @@ export function createOkrHost(ctx, dependencies = {}) {
143
144
  catch (error) {
144
145
  mapRuntimeNetworkError(error);
145
146
  }
146
- if (response.status === 401 || response.status === 302) {
147
+ if (isE10SessionExpiredResponse(response)) {
147
148
  throw okrAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, { httpStatus: response.status });
148
149
  }
149
150
  if (response.parseError) {
@@ -6,6 +6,7 @@ import { E10_SESSION_EXPIRED_MESSAGE } from '../../internal/e10/auth/messages.js
6
6
  import { getActiveProfile } from '../../internal/e10/auth/session.js';
7
7
  import { requireE10Session } from '../../internal/e10/context.js';
8
8
  import { E10ApiError, createE10RequestRuntime } from '../../internal/e10/request-runtime.js';
9
+ import { isE10SessionExpiredResponse } from '../../internal/e10/auth-recovery.js';
9
10
  import { planStateRoot } from './continuation.js';
10
11
  import { PlanCliError, planAuthError, planNetworkError, planValidationError } from './errors.js';
11
12
  function inferProfile(ctx) {
@@ -150,7 +151,7 @@ export function createPlanHost(ctx, dependencies = {}) {
150
151
  catch (error) {
151
152
  mapRuntimeNetworkError(error);
152
153
  }
153
- if (response.status === 401 || response.status === 302) {
154
+ if (isE10SessionExpiredResponse(response)) {
154
155
  throw planAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, { httpStatus: response.status });
155
156
  }
156
157
  if (response.parseError) {
@@ -2,6 +2,7 @@ import { E10_SESSION_EXPIRED_MESSAGE } from '../../internal/e10/auth/messages.js
2
2
  import { getActiveProfile } from '../../internal/e10/auth/session.js';
3
3
  import { requireE10Session } from '../../internal/e10/context.js';
4
4
  import { E10ApiError, createE10RequestRuntime } from '../../internal/e10/request-runtime.js';
5
+ import { isE10SessionExpiredResponse } from '../../internal/e10/auth-recovery.js';
5
6
  import { ProjectCliError, projectApiError, projectAuthError, projectNetworkError, projectValidationError, } from './errors.js';
6
7
  const PROJECT_PATH_PREFIXES = [
7
8
  '/api/ebuilder/form/formdata/v2/',
@@ -133,7 +134,7 @@ export function createProjectHost(ctx, dependencies = {}) {
133
134
  catch (error) {
134
135
  mapRuntimeNetworkError(error);
135
136
  }
136
- if (response.status === 401 || response.status === 302) {
137
+ if (isE10SessionExpiredResponse(response)) {
137
138
  throw projectAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, {
138
139
  httpStatus: response.status,
139
140
  });
@@ -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',
@@ -4726,8 +4734,19 @@ const XIAOE_FILL_FORM_SDK_OPTIONS = {
4726
4734
  */
4727
4735
  const XIAOE_INITIAL_VALUES_MULTI_SEPARATOR = ',';
4728
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 = '##';
4729
4748
  /** 单个标量 → URL 传参里的那一段文本。对象按「id 优先」取标识(附件取到的就是 fileid)。 */
4730
- function xiaoEUrlParamScalar(value) {
4749
+ function xiaoEUrlParamScalar(value, prefixBrowserId = false) {
4731
4750
  if (value === null || value === undefined)
4732
4751
  return '';
4733
4752
  if (typeof value === 'object') {
@@ -4740,24 +4759,26 @@ function xiaoEUrlParamScalar(value) {
4740
4759
  if (picked === null || picked === undefined)
4741
4760
  continue;
4742
4761
  const text = String(picked).trim();
4743
- if (text)
4744
- return text;
4762
+ if (!text)
4763
+ continue;
4764
+ // 浏览按钮字段:这一段的来源是对象 id(不是用户给的名称),按 2026-11-02 口径加 `##`。
4765
+ return prefixBrowserId ? `${XIAOE_BROWSER_ID_PREFIX}${text}` : text;
4745
4766
  }
4746
4767
  return '';
4747
4768
  }
4748
4769
  return String(value).trim();
4749
4770
  }
4750
4771
  /** 单个字段的值 → URL 传参里的那一段文本(数组按「同字段多值」用 `,` 连接)。 */
4751
- function xiaoEUrlParamValue(value) {
4772
+ function xiaoEUrlParamValue(value, prefixBrowserId = false) {
4752
4773
  if (value === null || value === undefined)
4753
4774
  return '';
4754
4775
  if (Array.isArray(value)) {
4755
4776
  return value
4756
- .map((item) => xiaoEUrlParamScalar(item))
4777
+ .map((item) => xiaoEUrlParamScalar(item, prefixBrowserId))
4757
4778
  .filter(Boolean)
4758
4779
  .join(XIAOE_INITIAL_VALUES_MULTI_SEPARATOR);
4759
4780
  }
4760
- return xiaoEUrlParamScalar(value);
4781
+ return xiaoEUrlParamScalar(value, prefixBrowserId);
4761
4782
  }
4762
4783
  /**
4763
4784
  * `params.customParams` 里**字段值**那一份 = `{字段id: 值}`(用户裁定用**字段 id**,不用数据 key);
@@ -4768,6 +4789,10 @@ function xiaoEUrlParamValue(value) {
4768
4789
  *
4769
4790
  * 明细字段(`preview` 里带 `dataIndex`)按**行号升序**取值、用 `;` 连接;主表字段就是它自己的值。
4770
4791
  * 值为空(没解析出内容)的字段整个省略,不生成空参数。
4792
+ *
4793
+ * **浏览按钮字段的值加 `##` 前缀**(2026-11-02 用户裁定,见 `XIAOE_BROWSER_ID_PREFIX`):这类字段解析
4794
+ * 出来的每一段都是对象 id,写成 `##<id>`;名称(非浏览字段的字符串)原样不加。多值/多行逐个判定,
4795
+ * 因此 `##<id>,<名称>`、`##<id>;<名称>` 都是合法形态。`src` 末尾的字段参数与这里同源,一起带上。
4771
4796
  */
4772
4797
  /**
4773
4798
  * 小e 档交给浏览器页面的字段值:只留**可见且可编辑**的那些(2026-10-21 用户裁定)。
@@ -4810,15 +4835,23 @@ function editablePreviewForXiaoE(preview, fields, visibility) {
4810
4835
  }
4811
4836
  return kept;
4812
4837
  }
4813
- function xiaoECustomParams(preview) {
4838
+ function xiaoECustomParams(preview, fields) {
4814
4839
  const ordered = [];
4815
4840
  const mainValues = new Map();
4816
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
+ }
4817
4850
  for (const item of toArray(preview)) {
4818
4851
  const fieldId = String(item?.fieldId ?? '').trim();
4819
4852
  if (!fieldId)
4820
4853
  continue;
4821
- const text = xiaoEUrlParamValue(item?.value);
4854
+ const text = xiaoEUrlParamValue(item?.value, browserFieldIds.has(fieldId));
4822
4855
  if (!text)
4823
4856
  continue;
4824
4857
  const isDetail = item?.dataIndex !== undefined && item?.dataIndex !== null;
@@ -5234,7 +5267,7 @@ async function runCreateRequest(args, env, options = {}) {
5234
5267
  baseUrl: xiaoEBaseUrl,
5235
5268
  title: String(context.workflow?.name ?? ''),
5236
5269
  workflowId: String(context.workflow?.id ?? context.runtime?.workflow_id ?? ''),
5237
- customParams: xiaoECustomParams(fillPreview),
5270
+ customParams: xiaoECustomParams(fillPreview, fields),
5238
5271
  }),
5239
5272
  });
5240
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 });