weaver-work-cli 0.1.20 → 0.1.22

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 (77) hide show
  1. package/README.md +19 -11
  2. package/dist/core/refs.js +39 -36
  3. package/dist/internal/e10/auth/commands.js +33 -18
  4. package/dist/internal/e10/auth/session.js +60 -2
  5. package/dist/internal/e10/auth/xiaoe.js +253 -53
  6. package/dist/internal/e10/context.js +18 -6
  7. package/dist/internal/e10/request-runtime.js +3 -1
  8. package/dist/shortcuts/caizhilian/operations/confirm.js +1 -1
  9. package/dist/shortcuts/caizhilian/operations/handlers/file-collection.js +901 -20
  10. package/dist/shortcuts/caizhilian/operations/handlers/folder.js +55 -0
  11. package/dist/shortcuts/caizhilian/operations/handlers/setting-attach.js +4 -4
  12. package/dist/shortcuts/caizhilian/operations/specs.js +75 -0
  13. package/dist/shortcuts/hrm/manifest.js +16 -9
  14. package/dist/shortcuts/hrm/operations/registry.js +2 -0
  15. package/dist/shortcuts/hrm/operations/resolve.js +449 -0
  16. package/dist/shortcuts/invoice/host.js +5 -4
  17. package/dist/shortcuts/meeting/operations/eb/association.js +151 -0
  18. package/dist/shortcuts/meeting/operations/eb/attendance-summary.js +153 -0
  19. package/dist/shortcuts/meeting/operations/eb/catalog.js +185 -13
  20. package/dist/shortcuts/meeting/operations/eb/conflict.js +17 -11
  21. package/dist/shortcuts/meeting/operations/eb/decision.js +385 -0
  22. package/dist/shortcuts/meeting/operations/eb/factory.js +222 -35
  23. package/dist/shortcuts/meeting/operations/eb/index.js +18 -1
  24. package/dist/shortcuts/meeting/operations/eb/link.js +212 -0
  25. package/dist/shortcuts/meeting/operations/eb/task-progress.js +123 -0
  26. package/dist/shortcuts/meeting/operations/env.js +78 -61
  27. package/dist/shortcuts/meeting/operations/link.js +125 -0
  28. package/dist/shortcuts/meeting/operations/registry.js +2 -0
  29. package/dist/shortcuts/meeting/operations/shared.js +13 -0
  30. package/dist/shortcuts/workflow/host.js +5 -1
  31. package/dist/shortcuts/workflow/operations/form.js +95 -11
  32. package/dist/shortcuts/workflow/operations/read.js +785 -26
  33. package/docs/e10-auth.md +8 -2
  34. package/docs/hrm.md +34 -2
  35. package/docs/invoice.md +3 -0
  36. package/package.json +2 -2
  37. package/scripts/xiaoe_env.py +118 -0
  38. package/skill-template/domains/hrm.md +2 -1
  39. package/skill-template/skill-template.md +1 -1
  40. package/skills/weaver-e10-blog/SKILL.md +1 -1
  41. package/skills/weaver-e10-caizhilian/SKILL.md +5 -4
  42. package/skills/weaver-e10-caizhilian/references/file-collection.md +37 -0
  43. package/skills/weaver-e10-caizhilian/references/folder.md +5 -1
  44. package/skills/weaver-e10-caizhilian/references/setting-attach.md +35 -5
  45. package/skills/weaver-e10-caizhilian/references/source-manifest.json +620 -5
  46. package/skills/weaver-e10-calendar/SKILL.md +1 -1
  47. package/skills/weaver-e10-ebuilder-form/SKILL.md +1 -1
  48. package/skills/weaver-e10-esb/SKILL.md +1 -1
  49. package/skills/weaver-e10-hrm/SKILL.md +52 -5
  50. package/skills/weaver-e10-hrm/references/org.md +107 -1
  51. package/skills/weaver-e10-jinchengda/SKILL.md +2 -1
  52. package/skills/weaver-e10-jiuchuanhui/SKILL.md +1 -1
  53. package/skills/weaver-e10-jucailin/SKILL.md +1 -1
  54. package/skills/weaver-e10-mail/SKILL.md +1 -1
  55. package/skills/weaver-e10-meeting/SKILL.md +27 -11
  56. package/skills/weaver-e10-meeting/references/meeting-eb.md +162 -7
  57. package/skills/weaver-e10-meeting/references/meeting-verification.md +271 -0
  58. package/skills/weaver-e10-meeting.zip +0 -0
  59. package/skills/weaver-e10-okr/SKILL.md +1 -1
  60. package/skills/weaver-e10-okr/references/source-manifest.json +3 -3
  61. package/skills/weaver-e10-plan/SKILL.md +1 -1
  62. package/skills/weaver-e10-qiyecheng/SKILL.md +1 -1
  63. package/skills/weaver-e10-shared/SKILL.md +10 -13
  64. package/skills/weaver-e10-shared/references/e10-auth-and-session.md +11 -17
  65. package/skills/weaver-e10-shared/references/non-interactive-environment.md +21 -14
  66. package/skills/weaver-e10-shijingran/SKILL.md +1 -1
  67. package/skills/weaver-e10-skill-maker/SKILL.md +1 -1
  68. package/skills/weaver-e10-skill-maker/references/weaver-skill-style.md +1 -1
  69. package/skills/weaver-e10-wenshuding/SKILL.md +1 -1
  70. package/skills/weaver-e10-workflow/SKILL.md +19 -19
  71. package/skills/weaver-e10-workflow/references/source-manifest.json +13 -13
  72. package/skills/weaver-e10-workflow/references/workflow-create.md +11 -7
  73. package/skills/weaver-e10-workflow/references/workflow-query.md +9 -14
  74. package/skills/weaver-e10-yepiaotong/SKILL.md +1 -2
  75. package/skills/weaver-e10-yimiaoban/SKILL.md +1 -1
  76. package/skills/weaver-e10-yimiaoban/references/source-manifest.json +3 -3
  77. package/skills/weaver-e10-ziguanjia/SKILL.md +1 -1
@@ -0,0 +1,123 @@
1
+ /**
2
+ * 决议任务的**进度汇总**(供 `meeting.eb.decisionTask.list` 声明式挂载,见 catalog 的 `summarizeRows`)。
3
+ *
4
+ * 为什么需要(2026-09-21/22 真机确立的口径,三个坑每次手算都容易踩):
5
+ * 1. **任务主表的 `decision_status` 恒为「未完成」** —— 既不随明细变化,也无法经接口写入,
6
+ * 所以"任务完成了没"**只能**从 `detail1`(执行人跟进记录)读;
7
+ * 2. **`executors` 可能是单对象也可能是数组**(1 人 = 对象,多人 = 数组),直接取 length 会得到 undefined;
8
+ * 3. **`detail1` 只有"已跟进过"的人才有记录** —— 没跟进的执行人不在里面,
9
+ * 若按 detail1 统计人数会漏人(表现为"1/1 完成",其实还有个人没跟进)。
10
+ *
11
+ * 故这里统一:**以 `executors` 为人员基准,用 `detail1` 覆盖其状态,缺失的人补 `pending`**。
12
+ * 状态归一化成英文枚举便于程序判断;中文展示交给调用方翻译。
13
+ */
14
+ function toText(value) {
15
+ return value === undefined || value === null ? '' : String(value);
16
+ }
17
+ /**
18
+ * `finish_status` 归一化。
19
+ *
20
+ * EB 在不同接口/配置下可能回**中文渲染值**(已完成 / 已延期 / 未完成)或**数字 id**(0/1/2),
21
+ * 两种都兼容;无法识别的一律按 `pending`(宁可少报完成,不夸大)。
22
+ */
23
+ function normalizeFinishStatus(raw) {
24
+ const text = toText(raw).trim();
25
+ if (text === '')
26
+ return 'pending';
27
+ if (text === '1' || text.includes('已完成') || text === '完成')
28
+ return 'done';
29
+ if (text === '2' || text.includes('延期'))
30
+ return 'overdue';
31
+ return 'pending';
32
+ }
33
+ /** 人员字段(`executors` / `executor`)兼容"单对象 | 数组 | 纯 id 串",统一成 `[{id,name}]`。 */
34
+ function toPeople(raw) {
35
+ const list = raw === undefined || raw === null ? [] : (Array.isArray(raw) ? raw : [raw]);
36
+ const out = [];
37
+ const seen = new Set();
38
+ for (const item of list) {
39
+ if (item === undefined || item === null)
40
+ continue;
41
+ if (typeof item === 'object') {
42
+ const id = toText(item.id).trim();
43
+ if (id === '' || seen.has(id))
44
+ continue;
45
+ seen.add(id);
46
+ out.push({ id, name: toText(item.name) });
47
+ }
48
+ else {
49
+ const id = toText(item).trim();
50
+ if (id === '' || seen.has(id))
51
+ continue;
52
+ seen.add(id);
53
+ out.push({ id, name: '' });
54
+ }
55
+ }
56
+ return out;
57
+ }
58
+ /** 单行 → 任务进度。 */
59
+ function summarizeRow(row) {
60
+ const main = (row?.mainTable ?? row ?? {});
61
+ const mt = (main.mt_id ?? {});
62
+ // detail1:只有已跟进的人才有记录 ⇒ 先建索引,再按 executors 对齐(缺的补 pending)。
63
+ const followed = new Map();
64
+ const details = Array.isArray(row?.detail1) ? row.detail1 : [];
65
+ for (const item of details) {
66
+ const who = toPeople(item?.executor)[0];
67
+ if (!who)
68
+ continue;
69
+ followed.set(who.id, {
70
+ status: normalizeFinishStatus(item?.finish_status),
71
+ finishDate: toText(item?.finish_date),
72
+ });
73
+ }
74
+ const members = toPeople(main.executors).map((person) => {
75
+ const hit = followed.get(person.id);
76
+ return {
77
+ executorId: person.id,
78
+ executorName: person.name,
79
+ status: hit ? hit.status : 'pending',
80
+ finishDate: hit ? hit.finishDate : '',
81
+ };
82
+ });
83
+ // 兜底:主表没带 executors 时退化为按跟进记录统计,避免显示 0/0 让人误判"没有执行人"。
84
+ if (members.length === 0) {
85
+ for (const [id, hit] of followed) {
86
+ members.push({ executorId: id, executorName: '', status: hit.status, finishDate: hit.finishDate });
87
+ }
88
+ }
89
+ const done = members.filter((item) => item.status === 'done').length;
90
+ const overdue = members.filter((item) => item.status === 'overdue').length;
91
+ const pending = members.length - done - overdue;
92
+ const status = members.length === 0
93
+ ? 'pending'
94
+ : done === members.length
95
+ ? 'done'
96
+ : overdue > 0
97
+ ? 'overdue'
98
+ : done > 0
99
+ ? 'inProgress'
100
+ : 'pending';
101
+ return {
102
+ id: toText(main.id),
103
+ title: toText(main.decision_title),
104
+ mtId: toText(mt.id),
105
+ mtName: toText(mt.name),
106
+ plan: { begin: toText(main.begin_datetime), end: toText(main.end_datetime) },
107
+ progress: { total: members.length, done, overdue, pending, status, members },
108
+ };
109
+ }
110
+ /** `decisionTask.list` 的结果后处理:原始 `rows` 原样保留,这里只额外给出 `progress`。 */
111
+ export function summarizeTaskProgress(rows) {
112
+ const items = rows.map((row) => summarizeRow(row ?? {}));
113
+ return {
114
+ summary: {
115
+ total: items.length,
116
+ done: items.filter((item) => item.progress.status === 'done').length,
117
+ overdue: items.filter((item) => item.progress.status === 'overdue').length,
118
+ inProgress: items.filter((item) => item.progress.status === 'inProgress').length,
119
+ pending: items.filter((item) => item.progress.status === 'pending').length,
120
+ },
121
+ items,
122
+ };
123
+ }
@@ -16,6 +16,13 @@ function stateRoot(ctx) {
16
16
  return resolve(configured, '..');
17
17
  return process.env.WEAVER_WORK_CLI_HOME || process.env.WORK_CLI_HOME || resolve(homedir(), '.weaver-work-cli');
18
18
  }
19
+ /**
20
+ * 会议模块状态目录(`<stateRoot>/meeting`):环境模式缓存、EB objid 缓存等落在这里。
21
+ * 导出供 link.ts 复用,避免各文件各自推导 stateRoot 导致缓存分裂。
22
+ */
23
+ export function meetingStateDir(ctx) {
24
+ return resolve(stateRoot(ctx), 'meeting');
25
+ }
19
26
  function cacheFile(ctx) {
20
27
  return resolve(stateRoot(ctx), 'meeting', 'env-mode.json');
21
28
  }
@@ -50,6 +57,74 @@ async function writeCache(ctx, baseUrl, mode, smokePath) {
50
57
  return `环境模式缓存写入失败(不影响本次结果,下次会重新冒烟):${error instanceof Error ? error.message : String(error)}`;
51
58
  }
52
59
  }
60
+ /**
61
+ * 解析当前环境的会议模式(EB / 标准版),供 `meeting.env.detect` 与 `meeting.link`
62
+ * 共用同一套缓存与冒烟逻辑(避免两处判定不一致导致链接模式选错)。
63
+ */
64
+ export async function resolveMeetingEnvMode(host, ctx, options = {}) {
65
+ const session = await host.getSession();
66
+ // ① 默认先读缓存(按 baseUrl 固化);refresh=true 才强制重新冒烟。
67
+ if (options.refresh !== true) {
68
+ const cache = await readCache(ctx);
69
+ const cached = cache.environments[session.baseUrl];
70
+ if (cached && (cached.mode === 'eb' || cached.mode === 'standard')) {
71
+ return {
72
+ mode: cached.mode,
73
+ smokePath: cached.smokePath,
74
+ fromCache: true,
75
+ detectedAt: cached.detectedAt,
76
+ };
77
+ }
78
+ }
79
+ // ② 第 1 步:EB 版冒烟(EB 优先)。接口 404/业务失败均视为「EB 未配置」。
80
+ let ebError;
81
+ try {
82
+ await host.request({
83
+ method: 'POST',
84
+ path: EB_SMOKE_PATH,
85
+ operation: 'meeting.env.detect',
86
+ // EB list 的 datajson 内必须出现 mainTable 节点(空对象即可):缺失时服务端报
87
+ // “mainTable解析异常,请参照文档修正数据格式”,冒烟会**假失败**并把 EB 环境误判为
88
+ // 标准版(2026-09-20 真机复现)。
89
+ body: { datajson: { mainTable: {}, header: { operator: session.userId } } },
90
+ // EB 响应不含 code/status(形如 {datajson:{datas:[...]}}),默认的 isBusinessSuccess
91
+ // 会判为 false → 冒烟假失败 → EB 环境被误判为标准版。
92
+ acceptBusiness: ebBusinessSuccess,
93
+ });
94
+ const warning = await writeCache(ctx, session.baseUrl, 'eb', EB_SMOKE_PATH);
95
+ return { mode: 'eb', smokePath: EB_SMOKE_PATH, fromCache: false, warning };
96
+ }
97
+ catch (error) {
98
+ ebError = error;
99
+ }
100
+ // ③ 第 2 步:标准版冒烟(仅当 EB 冒烟未通过)。
101
+ try {
102
+ await host.request({
103
+ method: 'GET',
104
+ path: STD_CALENDAR_PATH,
105
+ operation: 'meeting.env.detect',
106
+ });
107
+ const ebDetail = ebError instanceof Error
108
+ ? `${ebError.type ?? 'api'}/${ebError.subtype ?? 'error'}: ${ebError.message}`
109
+ : String(ebError);
110
+ const warning = await writeCache(ctx, session.baseUrl, 'standard', STD_CALENDAR_PATH);
111
+ return {
112
+ mode: 'standard',
113
+ smokePath: STD_CALENDAR_PATH,
114
+ fromCache: false,
115
+ ebSmokeFailed: true,
116
+ ebSmokeError: ebDetail,
117
+ warning,
118
+ };
119
+ }
120
+ catch (stdError) {
121
+ // 两步都不通 → 不写缓存,提示接口未配置完整。
122
+ throw meetingValidationError('env_unavailable', '当前环境 EB 版与标准版会议接口冒烟均未通过,请确认会议应用已安装且接口已配置完整', {
123
+ eb: ebError instanceof Error ? ebError.message : String(ebError),
124
+ standard: stdError instanceof Error ? stdError.message : String(stdError),
125
+ });
126
+ }
127
+ }
53
128
  const meetingEnvDetect = {
54
129
  name: 'meeting.env.detect',
55
130
  resource: 'env',
@@ -67,67 +142,9 @@ const meetingEnvDetect = {
67
142
  },
68
143
  },
69
144
  handler: async (args, { host, ctx }) => {
70
- const session = await host.getSession();
71
- // ① 默认先读缓存(按 baseUrl 固化);refresh=true 才强制重新冒烟。
72
- if (args.refresh !== true) {
73
- const cache = await readCache(ctx);
74
- const cached = cache.environments[session.baseUrl];
75
- if (cached && (cached.mode === 'eb' || cached.mode === 'standard')) {
76
- return meetingSuccess('meeting.env.detect', {
77
- mode: cached.mode,
78
- smokePath: cached.smokePath,
79
- fromCache: true,
80
- detectedAt: cached.detectedAt,
81
- });
82
- }
83
- }
84
- // ② 第 1 步:EB 版冒烟(EB 优先)。接口 404/业务失败均视为「EB 未配置」。
85
- let ebError;
86
- try {
87
- await host.request({
88
- method: 'POST',
89
- path: EB_SMOKE_PATH,
90
- operation: 'meeting.env.detect',
91
- // EB list 的 datajson 内必须出现 mainTable 节点(空对象即可):缺失时服务端报
92
- // “mainTable解析异常,请参照文档修正数据格式”,冒烟会**假失败**并把 EB 环境误判为
93
- // 标准版(2026-09-20 真机复现)。
94
- body: { datajson: { mainTable: {}, header: { operator: session.userId } } },
95
- // EB 响应不含 code/status(形如 {datajson:{datas:[...]}}),默认的 isBusinessSuccess
96
- // 会判为 false → 冒烟假失败 → EB 环境被误判为标准版。
97
- acceptBusiness: ebBusinessSuccess,
98
- });
99
- const warning = await writeCache(ctx, session.baseUrl, 'eb', EB_SMOKE_PATH);
100
- return meetingSuccess('meeting.env.detect', { mode: 'eb', smokePath: EB_SMOKE_PATH, fromCache: false }, warning ? { warnings: [warning] } : undefined);
101
- }
102
- catch (error) {
103
- ebError = error;
104
- }
105
- // ③ 第 2 步:标准版冒烟(仅当 EB 冒烟未通过)。
106
- try {
107
- await host.request({
108
- method: 'GET',
109
- path: STD_CALENDAR_PATH,
110
- operation: 'meeting.env.detect',
111
- });
112
- const ebDetail = ebError instanceof Error
113
- ? `${ebError.type ?? 'api'}/${ebError.subtype ?? 'error'}: ${ebError.message}`
114
- : String(ebError);
115
- const warning = await writeCache(ctx, session.baseUrl, 'standard', STD_CALENDAR_PATH);
116
- return meetingSuccess('meeting.env.detect', {
117
- mode: 'standard',
118
- smokePath: STD_CALENDAR_PATH,
119
- fromCache: false,
120
- ebSmokeFailed: true,
121
- ebSmokeError: ebDetail,
122
- }, warning ? { warnings: [warning] } : undefined);
123
- }
124
- catch (stdError) {
125
- // 两步都不通 → 不写缓存,提示接口未配置完整。
126
- throw meetingValidationError('env_unavailable', '当前环境 EB 版与标准版会议接口冒烟均未通过,请确认会议应用已安装且接口已配置完整', {
127
- eb: ebError instanceof Error ? ebError.message : String(ebError),
128
- standard: stdError instanceof Error ? stdError.message : String(stdError),
129
- });
130
- }
145
+ const result = await resolveMeetingEnvMode(host, ctx, { refresh: args.refresh === true });
146
+ const { warning, ...data } = result;
147
+ return meetingSuccess('meeting.env.detect', data, warning ? { warnings: [warning] } : undefined);
131
148
  },
132
149
  };
133
150
  // ---------------------------------------------------------------------------
@@ -0,0 +1,125 @@
1
+ import { meetingValidationError } from '../errors.js';
2
+ import { meetingSuccess, schemaOf, SCHEMA_ID, SCHEMA_ID_LIST } from './types.js';
3
+ import { resolveMeetingEnvMode } from './env.js';
4
+ import { MEETING_EB_TAGS, STANDARD_NO_PAGE_TYPES, buildEbCardPath, buildStandardMeetingPath, meetingAbsoluteUrl, meetingMarkdownLink, resolveMeetingEbObjs, } from './eb/link.js';
5
+ const EB_TYPES = Object.keys(MEETING_EB_TAGS);
6
+ /**
7
+ * `meeting.link` —— 会议对象「打开链接」生成器(EB 版 + 标准版)。
8
+ *
9
+ * 为什么要单独一个 operation:链接地址随模式不同、且 EB 版需要按表 tag 动态解析 objid
10
+ * (objid 随环境变化、禁止硬编码,也不等于 formId),不能让 Agent 手工拼 URL。
11
+ * 解析失败只降级为「不附链接」并写 warnings,**不影响**主业务结果。
12
+ */
13
+ const meetingLink = {
14
+ name: 'meeting.link',
15
+ resource: 'link',
16
+ method: 'resolve',
17
+ action: 'meeting.link',
18
+ summary: '生成对象详情页链接:EB 版 /sp/ebdfpage/card/0/{objid}/{dataid}(按 tag 解析 objid 并缓存);标准版会议 /sp/meeting/meetingSingle?meetingId=(会议室/会议类型无单页,不附链接)',
19
+ capability: 'link',
20
+ risk: 'read',
21
+ inputSchema: schemaOf({
22
+ dataId: { ...SCHEMA_ID, description: '对象 id:标准版=会议 id;EB 版=表单数据 id(单条查询用)' },
23
+ dataIds: { ...SCHEMA_ID_LIST, description: '批量:一次为多条数据生成链接(列表输出用)' },
24
+ type: {
25
+ type: 'string',
26
+ enum: [...EB_TYPES],
27
+ description: '对象类别(默认 meeting)。映射 EB 表 tag,详见 references/meeting-eb.md 的链接章节',
28
+ },
29
+ tag: { type: 'string', description: '直接指定 EB 表 tag(如 uf_mt_detail),与 type 二选一,优先于 type' },
30
+ mode: {
31
+ type: 'string',
32
+ enum: ['auto', 'eb', 'standard'],
33
+ description: 'auto(默认)=按当前环境模式自动判定;也可显式指定',
34
+ },
35
+ name: { type: 'string', description: '链接显示名;传入后同时返回 markdown 内联链接' },
36
+ refresh: { type: 'boolean', description: 'true=强制重新解析 EB objid(忽略缓存)' },
37
+ }, { required: [] }),
38
+ handler: async (args, { host, ctx }) => {
39
+ const single = args.dataId !== undefined && args.dataId !== null && String(args.dataId).trim() !== '';
40
+ const batch = Array.isArray(args.dataIds)
41
+ ? args.dataIds.map((item) => String(item)).filter((item) => item.trim() !== '')
42
+ : [];
43
+ const dataIds = single ? [String(args.dataId)] : batch;
44
+ if (dataIds.length === 0) {
45
+ throw meetingValidationError('link_data_id_required', '生成链接必须提供 dataId(单条)或 dataIds(批量)');
46
+ }
47
+ const requestedType = args.type === undefined ? 'meeting' : String(args.type);
48
+ const tag = typeof args.tag === 'string' && args.tag.trim() !== ''
49
+ ? args.tag.trim()
50
+ : MEETING_EB_TAGS[requestedType];
51
+ if (!tag) {
52
+ throw meetingValidationError('link_type_invalid', `未知对象类别:${requestedType}(可用:${EB_TYPES.join(' / ')},或用 tag 直接指定表 tag)`);
53
+ }
54
+ const session = await host.getSession();
55
+ const warnings = [];
56
+ const requestedMode = args.mode === undefined ? 'auto' : String(args.mode);
57
+ let mode;
58
+ if (requestedMode === 'eb' || requestedMode === 'standard') {
59
+ mode = requestedMode;
60
+ }
61
+ else {
62
+ const detected = await resolveMeetingEnvMode(host, ctx, {});
63
+ mode = detected.mode;
64
+ if (detected.warning)
65
+ warnings.push(detected.warning);
66
+ }
67
+ let objId = null;
68
+ let appId = null;
69
+ let fromCache = false;
70
+ if (mode === 'standard') {
71
+ // 标准版:只有会议详情有单页;会议室 / 会议类型明确无单页(standalone 口径)。
72
+ if (STANDARD_NO_PAGE_TYPES.includes(requestedType)) {
73
+ warnings.push(`标准版「${requestedType}」没有单页地址,按口径只输出数据、不附链接`);
74
+ }
75
+ else if (requestedType !== 'meeting') {
76
+ warnings.push(`标准版未记录「${requestedType}」的单页地址,已跳过链接生成`);
77
+ }
78
+ }
79
+ else {
80
+ const resolved = await resolveMeetingEbObjs(host, ctx, { tags: [tag], refresh: args.refresh === true });
81
+ objId = resolved.objs[tag]?.objId ?? null;
82
+ appId = resolved.appId;
83
+ fromCache = resolved.fromCache;
84
+ warnings.push(...resolved.warnings);
85
+ if (!objId)
86
+ warnings.push(`未解析到 tag=${tag} 的 objid,本次不附链接`);
87
+ }
88
+ const noStdPage = mode === 'standard' && requestedType !== 'meeting';
89
+ const items = dataIds.map((dataId) => {
90
+ const path = mode === 'eb'
91
+ ? (objId ? buildEbCardPath(objId, dataId) : null)
92
+ : (noStdPage ? null : buildStandardMeetingPath(dataId));
93
+ const url = meetingAbsoluteUrl(session.baseUrl, path);
94
+ const item = { dataId, path, url };
95
+ const markdown = typeof args.name === 'string' && args.name.trim() !== ''
96
+ ? meetingMarkdownLink(args.name, url)
97
+ : undefined;
98
+ if (markdown)
99
+ item.markdown = markdown;
100
+ return item;
101
+ });
102
+ // source:objid 的来源(cache/dynamic);标准版不需要 objid ⇒ none。
103
+ const source = mode === 'eb' ? (objId ? (fromCache ? 'cache' : 'dynamic') : 'none') : 'none';
104
+ const data = {
105
+ mode,
106
+ type: requestedType,
107
+ // 标准版不涉及 EB 表 tag,返回 null 避免误用。
108
+ tag: mode === 'eb' ? tag : null,
109
+ baseUrl: session.baseUrl,
110
+ appId,
111
+ objId,
112
+ source,
113
+ items,
114
+ };
115
+ if (single) {
116
+ const [first] = items;
117
+ data.path = first.path;
118
+ data.url = first.url;
119
+ if (first.markdown)
120
+ data.markdown = first.markdown;
121
+ }
122
+ return meetingSuccess('meeting.link', data, warnings.length ? { warnings } : {});
123
+ },
124
+ };
125
+ export const meetingLinkOperations = [meetingLink];
@@ -9,6 +9,7 @@ import { meetingCreateOperations } from './create.js';
9
9
  import { meetingLifecycleOperations } from './lifecycle.js';
10
10
  import { meetingReceiptOperations } from './receipts.js';
11
11
  import { meetingSignOperations } from './signs.js';
12
+ import { meetingLinkOperations } from './link.js';
12
13
  import { meetingEbOperations } from './eb/index.js';
13
14
  function buildRegistry() {
14
15
  const registry = new Map();
@@ -23,6 +24,7 @@ function buildRegistry() {
23
24
  ...meetingChangeOperations,
24
25
  ...meetingReceiptOperations,
25
26
  ...meetingSignOperations,
27
+ ...meetingLinkOperations,
26
28
  ...meetingEbOperations,
27
29
  ];
28
30
  for (const definition of definitions) {
@@ -29,6 +29,19 @@ export function assertDateTimeMinute(value, label) {
29
29
  }
30
30
  return value;
31
31
  }
32
+ /**
33
+ * 校验 `yyyy-MM-dd HH:mm:ss`(**带秒**)—— EB 部分字段要求带秒。
34
+ *
35
+ * 2026-09-21 真机实测:决议任务的 `finish_date` 只接受带秒格式,传 `2026-09-21 18:30`
36
+ * 会被服务端拒绝(「不满足传入格式yyyy-MM-dd HH:mm:ss」)。而决议明细的
37
+ * `begin_datetime`/`end_datetime` 是不带秒的 —— **同一应用里两种格式并存**,故分别校验。
38
+ */
39
+ export function assertDateTimeSecond(value, label) {
40
+ if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(value)) {
41
+ throw meetingValidationError('datetime_invalid', `${label} 必须是 yyyy-MM-dd HH:mm:ss 格式(带秒)`);
42
+ }
43
+ return value;
44
+ }
32
45
  /** 校验 yyyy-MM-dd,返回原串。 */
33
46
  export function assertDate(value, label) {
34
47
  if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(value)) {
@@ -124,7 +124,8 @@ function loginExpiredMessage() {
124
124
  export function createWorkflowHost(ctx, dependencies = {}) {
125
125
  let session = null;
126
126
  let sessionContext = null;
127
- const requireSession = dependencies.requireSession || requireE10Session;
127
+ const requireSession = dependencies.requireSession
128
+ || ((innerCtx) => requireE10Session(innerCtx, { skipProfileEnrichment: true }));
128
129
  async function ensureSession() {
129
130
  if (!session) {
130
131
  session = await requireSession(ctx);
@@ -153,6 +154,9 @@ export function createWorkflowHost(ctx, dependencies = {}) {
153
154
  const runtime = createE10RequestRuntime(ctx, await ensureSession(), {
154
155
  interceptors: dependencies.interceptors,
155
156
  fetch: dependencies.fetch,
157
+ // 流程侧是唯一开启"请求带上网关会话票据(ZT_SESSION)"的调用方:登录时已把票存进 auth
158
+ // (`ztSession`),带上可让网关不再为每个请求重发票。没有票或票已过期时不附加,请求头与从前一致。
159
+ attachSessionTicket: true,
156
160
  });
157
161
  let response;
158
162
  try {
@@ -1290,8 +1290,11 @@ async function resolveCreateWorkflow(rawWorkflow, env) {
1290
1290
  return { id: String(exact[0].id), name: String(exact[0].workflowname ?? ''), source: 'exactName' };
1291
1291
  }
1292
1292
  const list = exact.length ? exact : picked;
1293
- throw workflowClarificationError('workflow', queryText, `「${queryText}」匹配到 ${list.length} 个可发起的工作流,且其中没有与所写名称完全一致的,`
1294
- + '请确认要发起的具体工作流。', list.slice(0, 10).map((item, index) => ({
1293
+ // `clarificationQuestion` 是**用户口径**:它会被 `note` 要求「原样交给用户确认」,因此
1294
+ // 只能写"发生了什么 + 要用户做什么"。**禁止**把本机判定依据(如"其中没有与所写名称完全
1295
+ // 一致的")写进这句——那属于 CLI 内部实现,外传会让用户在确认卡片上看到与己无关的判据
1296
+ // 解释(2026-09-24 用户实拍)。模型侧的护栏是「不要再换词试探」,由 skill 的行为指令承担。
1297
+ throw workflowClarificationError('workflow', queryText, `「${queryText}」匹配到 ${list.length} 个可发起的工作流,请确认要发起的具体工作流。`, list.slice(0, 10).map((item, index) => ({
1295
1298
  index: index + 1,
1296
1299
  id: String(item.id),
1297
1300
  name: String(item.workflowname ?? ''),
@@ -3622,11 +3625,15 @@ async function commandCreateRequest(args, env) {
3622
3625
  });
3623
3626
  // parseCreateResponse 需读外层 status/code 并向下遍历 data/result,必须传原始响应
3624
3627
  created = parseCreateResponse(createResponse, context.runtime.request_id);
3625
- // flow/create 已返回成功:阶段推进到 committed。若进程在"回写成功结果"之前被中止,
3626
- // 重放可据此报幂等成功(ALREADY_APPLIED + requestId),而不是只报一句"已使用"让人反查。
3627
- await env.continuation
3628
- .markStage(token, 'committed', { requestId: context.runtime.request_id })
3629
- .catch(() => undefined);
3628
+ if (created.outcome === 'created') {
3629
+ // flow/create 已返回成功:阶段推进到 committed。若进程在"回写成功结果"之前被中止,
3630
+ // 重放可据此报幂等成功(ALREADY_APPLIED + requestId),而不是只报一句"已使用"让人反查。
3631
+ await env.continuation
3632
+ .markStage(token, 'committed', { requestId: context.runtime.request_id })
3633
+ .catch(() => undefined);
3634
+ }
3635
+ // outcome === 'rejected' 时**不**标 committed:服务端并没有给"成功回执",
3636
+ // 标了会让后续重放报出"上一次已成功"的假消息。结果照常由 settle() 收尾。
3630
3637
  }
3631
3638
  catch (error) {
3632
3639
  if (isNetworkError(error)) {
@@ -3643,8 +3650,8 @@ async function commandCreateRequest(args, env) {
3643
3650
  requestId,
3644
3651
  requestName: prepared.requestName,
3645
3652
  requestId_alt: requestId,
3646
- requestMark: created.requestMark || null,
3647
- requestName_alt: created.requestName || prepared.requestName,
3653
+ requestMark: (created.outcome === 'created' && created.requestMark) || null,
3654
+ requestName_alt: (created.outcome === 'created' && created.requestName) || prepared.requestName,
3648
3655
  workflow: context.workflow,
3649
3656
  url: `${baseUrl}/sp/workflow/flowpage/view/${requestId}?operResultType=SUCCESS_RELOAD_CURRENT_PAGE&userSpecifiedNodeId=0&autoMaxDisplay=0`,
3650
3657
  linkageResult,
@@ -3657,7 +3664,18 @@ async function commandCreateRequest(args, env) {
3657
3664
  previewDetails: prepared.previewDetails,
3658
3665
  };
3659
3666
  if (submitIntent) {
3660
- if (verification.blocked) {
3667
+ if (created.outcome === 'rejected' && createSrc === 'submit') {
3668
+ // 服务端明确拒绝提交,但**表单数据已落库、流程以草稿形态存在**(真机实测:被拒后该
3669
+ // requestId 在 workflow.detail 里查得到,flowStatus=草稿)。故按"完成 + 提交结果失败"
3670
+ // 返回,而不是抛错——抛错会让模型以为"什么都没做成"而重发,一路堆积重复草稿。
3671
+ // 所有"成功时会给的创建信息"这里一个都不少,只是提交结果是失败。
3672
+ result.status = 'CREATED_SUBMIT_FAILED';
3673
+ result.message = `流程已创建为草稿,但提交被服务端拒绝(未进入审批)。原因:${created.reason}`;
3674
+ result.submitResult = 'failed';
3675
+ result.submitFailReason = created.reason;
3676
+ result.verification = verification;
3677
+ }
3678
+ else if (verification.blocked) {
3661
3679
  result.message = '流程已创建,但因节点字段校验未通过未提交审批,请调整后重新提交。';
3662
3680
  result.verification = verification;
3663
3681
  }
@@ -3670,6 +3688,7 @@ async function commandCreateRequest(args, env) {
3670
3688
  else {
3671
3689
  result.status = 'CREATED_SUBMITTED';
3672
3690
  result.message = '流程已发起并提交审批。';
3691
+ result.submitResult = 'success';
3673
3692
  result.verification = verification;
3674
3693
  }
3675
3694
  }
@@ -3685,6 +3704,12 @@ async function commandCreateRequest(args, env) {
3685
3704
  : '。');
3686
3705
  }
3687
3706
  }
3707
+ // 非"本次真发起提交"的路径(action=draft,或被节点校验拦下、未真正提交)里 flow/create 也被拒:
3708
+ // 不改这些分支的状态语义(它们本就描述"未提交审批"),但把服务端给的原因附上,避免信息静默丢失。
3709
+ if (created.outcome === 'rejected' && !(submitIntent && createSrc === 'submit')) {
3710
+ result.createRejectedReason = created.reason;
3711
+ result.message = `${result.message}(服务端在创建时返回:${created.reason})`;
3712
+ }
3688
3713
  // 必填字段的本次取值 → 该工作流的发起偏好(CLI 内部完成、**静默**,见下方调用点注释)
3689
3714
  const requiredFieldIds = new Set();
3690
3715
  for (const field of fields) {
@@ -3824,11 +3849,69 @@ function parseSaveResponse(response) {
3824
3849
  }
3825
3850
  return values;
3826
3851
  }
3852
+ /**
3853
+ * `flow/create` 的**明确拒绝**判定——把「服务端说不」与「结果不确定」分开。
3854
+ *
3855
+ * 2026-09-24 真机实测(发起「泛微请假流程」提交):
3856
+ * `POST /api/workflow/core/flow/create` → HTTP 200
3857
+ * `{"status":false,"resultType":"FAIL_SHOW_ERROR_MSG","resultMessage":"监听事件触发异常",
3858
+ * "exceptionFlowLog":{"flowExMainPrompt":{"listenerOriginalMsg":"考勤校验失败:请假天数不能小于0"}}}`
3859
+ * 服务端**已经明确表态"不提交"并给出原因**,但 host 层的业务错误提取只认
3860
+ * `msg/message/error/errorMsg`(不认 `resultMessage`),漏判后落到本函数,
3861
+ * 原实现把这句"确定失败"抛成 `partial/write_uncertain`(语义="可能已生效、禁止重试、先回查")。
3862
+ * 模型读到 `write_uncertain` 无法分辨"被拒了"与"断线了",只能把草稿说成"可能已提交"。
3863
+ *
3864
+ * 因此按**失败信号是否显式**分流(V11.3 基线 `e10_workflow_create.py:4822` 同样只有
3865
+ * `PARTIAL_SAVED` 一档,属继承缺陷,此处为有意偏离):
3866
+ * - 显式拒绝(本函数命中)→ 确定失败,原因+草稿 requestId 原样带出;
3867
+ * - 形态不明(无任何成功/失败标志)→ 仍按 `partial/write_uncertain` 处理。
3868
+ */
3869
+ function isCreateRejected(response) {
3870
+ if (response.fail === true)
3871
+ return true;
3872
+ if (response.status === false)
3873
+ return true;
3874
+ if (response.flowIsCreateSuccess === false)
3875
+ return true;
3876
+ return String(response.resultType ?? '').trim().toUpperCase().startsWith('FAIL');
3877
+ }
3878
+ /**
3879
+ * 从 `flow/create` 的拒绝响应里挑出**最可执行**的原因文案。
3880
+ *
3881
+ * `resultMessage` 常是泛化壳文案(真机是「监听事件触发异常」),真正能指导用户的是
3882
+ * `exceptionFlowLog.flowExMainPrompt` 里的 `listenerOriginalMsg`/`msgTitle`
3883
+ * (真机是「考勤校验失败:请假天数不能小于0」)。故按"越具体越优先"排序取第一个非空值。
3884
+ */
3885
+ function pickCreateRejectReason(response) {
3886
+ const prompt = (response.exceptionFlowLog ?? {}).flowExMainPrompt ?? {};
3887
+ const candidates = [
3888
+ prompt.listenerOriginalMsg,
3889
+ response.msgTitle,
3890
+ prompt.listenerExMsgs,
3891
+ response.resultMessage,
3892
+ response.msg,
3893
+ response.message,
3894
+ ];
3895
+ for (const candidate of candidates) {
3896
+ const text = typeof candidate === 'string' ? candidate.trim() : '';
3897
+ if (text)
3898
+ return text;
3899
+ }
3900
+ return '服务端未给出原因';
3901
+ }
3827
3902
  function parseCreateResponse(response, expectedRequestId) {
3828
3903
  if (!response || typeof response !== 'object')
3829
3904
  throw workflowPartialError('flow/create 返回格式错误,禁止自动重试', { stage: 'flow/create' });
3905
+ if (isCreateRejected(response)) {
3906
+ return {
3907
+ outcome: 'rejected',
3908
+ reason: pickCreateRejectReason(response),
3909
+ resultType: String(response.resultType ?? '').trim(),
3910
+ requestId: String(response.requestId ?? response.requestid ?? expectedRequestId).trim() || expectedRequestId,
3911
+ };
3912
+ }
3830
3913
  const success = response.status === true || [0, 200, '0', '200'].includes(response.code) || [0, 200, '0', '200'].includes(response.status);
3831
- if (!success || response.flowIsCreateSuccess === false || response.fail === true) {
3914
+ if (!success) {
3832
3915
  throw workflowPartialError(String(response.resultMessage ?? response.msg ?? 'flow/create 未确认成功'), { stage: 'flow/create' });
3833
3916
  }
3834
3917
  const sources = [response];
@@ -3856,6 +3939,7 @@ function parseCreateResponse(response, expectedRequestId) {
3856
3939
  throw workflowPartialError('flow/create 返回了不同的 requestId,禁止自动重试', { stage: 'flow/create' });
3857
3940
  }
3858
3941
  return {
3942
+ outcome: 'created',
3859
3943
  requestId: returnedId || expectedRequestId,
3860
3944
  requestMark: firstText('requestMark', 'requestmark', 'requestCode'),
3861
3945
  requestName: firstText('requestName', 'requestname'),