weaver-work-cli 0.1.20 → 0.1.21

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 (59) 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/workflow/host.js +5 -1
  17. package/dist/shortcuts/workflow/operations/form.js +95 -11
  18. package/dist/shortcuts/workflow/operations/read.js +785 -26
  19. package/docs/e10-auth.md +4 -2
  20. package/docs/hrm.md +34 -2
  21. package/package.json +2 -2
  22. package/scripts/xiaoe_env.py +118 -0
  23. package/skill-template/domains/hrm.md +2 -1
  24. package/skill-template/skill-template.md +1 -1
  25. package/skills/weaver-e10-blog/SKILL.md +1 -1
  26. package/skills/weaver-e10-caizhilian/SKILL.md +5 -4
  27. package/skills/weaver-e10-caizhilian/references/file-collection.md +37 -0
  28. package/skills/weaver-e10-caizhilian/references/folder.md +5 -1
  29. package/skills/weaver-e10-caizhilian/references/setting-attach.md +35 -5
  30. package/skills/weaver-e10-caizhilian/references/source-manifest.json +620 -5
  31. package/skills/weaver-e10-calendar/SKILL.md +1 -1
  32. package/skills/weaver-e10-ebuilder-form/SKILL.md +1 -1
  33. package/skills/weaver-e10-esb/SKILL.md +1 -1
  34. package/skills/weaver-e10-hrm/SKILL.md +52 -5
  35. package/skills/weaver-e10-hrm/references/org.md +107 -1
  36. package/skills/weaver-e10-jinchengda/SKILL.md +2 -1
  37. package/skills/weaver-e10-jiuchuanhui/SKILL.md +1 -1
  38. package/skills/weaver-e10-jucailin/SKILL.md +1 -1
  39. package/skills/weaver-e10-mail/SKILL.md +1 -1
  40. package/skills/weaver-e10-meeting/SKILL.md +1 -1
  41. package/skills/weaver-e10-okr/SKILL.md +1 -1
  42. package/skills/weaver-e10-okr/references/source-manifest.json +3 -3
  43. package/skills/weaver-e10-plan/SKILL.md +1 -1
  44. package/skills/weaver-e10-qiyecheng/SKILL.md +1 -1
  45. package/skills/weaver-e10-shared/SKILL.md +10 -13
  46. package/skills/weaver-e10-shared/references/e10-auth-and-session.md +11 -17
  47. package/skills/weaver-e10-shared/references/non-interactive-environment.md +21 -14
  48. package/skills/weaver-e10-shijingran/SKILL.md +1 -1
  49. package/skills/weaver-e10-skill-maker/SKILL.md +1 -1
  50. package/skills/weaver-e10-skill-maker/references/weaver-skill-style.md +1 -1
  51. package/skills/weaver-e10-wenshuding/SKILL.md +1 -1
  52. package/skills/weaver-e10-workflow/SKILL.md +19 -19
  53. package/skills/weaver-e10-workflow/references/source-manifest.json +13 -13
  54. package/skills/weaver-e10-workflow/references/workflow-create.md +11 -7
  55. package/skills/weaver-e10-workflow/references/workflow-query.md +9 -14
  56. package/skills/weaver-e10-yepiaotong/SKILL.md +1 -2
  57. package/skills/weaver-e10-yimiaoban/SKILL.md +1 -1
  58. package/skills/weaver-e10-yimiaoban/references/source-manifest.json +3 -3
  59. package/skills/weaver-e10-ziguanjia/SKILL.md +1 -1
@@ -0,0 +1,449 @@
1
+ import { hrmValidationError } from '../errors.js';
2
+ import { browserBaseBody, orgLink, } from './shared.js';
3
+ import { READ, SCHEMA_ID, hrmSuccess, schemaOf, } from './types.js';
4
+ const VIRTUAL_PATH = '/api/bs/hrm/companyVirtual/listCompanyVirtual';
5
+ const ORG_DETAIL_PATH = '/api/bs/hrm/organization/getForm';
6
+ const ORG_TYPES = ['department', 'subcompany'];
7
+ const DEFAULT_MAX_CANDIDATES = 20;
8
+ const MAX_CANDIDATES = 200;
9
+ const MAX_SUGGESTIONS = 5;
10
+ function text(value) {
11
+ return value === undefined || value === null ? '' : String(value).trim();
12
+ }
13
+ /** 名称匹配忽略大小写、全半角差异和多余空白。 */
14
+ function normalizedName(value) {
15
+ return text(value).normalize('NFKC').replace(/\s+/gu, '').toLocaleLowerCase();
16
+ }
17
+ function normalizePath(value) {
18
+ return text(value)
19
+ .split(/[|/]+/u)
20
+ .map((part) => part.trim())
21
+ .filter(Boolean)
22
+ .join('/');
23
+ }
24
+ function idValue(value, field) {
25
+ const valueText = text(value);
26
+ if (!/^\d+$/u.test(valueText)) {
27
+ throw hrmValidationError('id_invalid', `${field} 必须是数字 ID`);
28
+ }
29
+ return valueText;
30
+ }
31
+ function maxCandidatesValue(value) {
32
+ if (value === undefined || value === null || value === '')
33
+ return DEFAULT_MAX_CANDIDATES;
34
+ const numeric = Number(value);
35
+ if (!Number.isInteger(numeric) || numeric < 1 || numeric > MAX_CANDIDATES) {
36
+ throw hrmValidationError('number_invalid', `maxCandidates 必须是 1~${MAX_CANDIDATES} 的整数`);
37
+ }
38
+ return numeric;
39
+ }
40
+ function uniqueById(items) {
41
+ const seen = new Set();
42
+ return items.filter((item) => {
43
+ if (!item.id || seen.has(item.id))
44
+ return false;
45
+ seen.add(item.id);
46
+ return true;
47
+ });
48
+ }
49
+ function readVirtuals(response) {
50
+ const root = response?.data;
51
+ const rows = Array.isArray(root) ? root : Array.isArray(root?.data) ? root.data : [];
52
+ return uniqueById(rows
53
+ .map((row) => ({
54
+ id: text(row?.id ?? row?.value),
55
+ name: text(row?.content ?? row?.name ?? row?.label),
56
+ }))
57
+ .filter((item) => item.id && item.name));
58
+ }
59
+ function levenshtein(left, right) {
60
+ const a = [...left];
61
+ const b = [...right];
62
+ let previous = Array.from({ length: b.length + 1 }, (_, index) => index);
63
+ for (let row = 1; row <= a.length; row += 1) {
64
+ const current = [row];
65
+ for (let column = 1; column <= b.length; column += 1) {
66
+ current[column] = Math.min(current[column - 1] + 1, previous[column] + 1, previous[column - 1] + (a[row - 1] === b[column - 1] ? 0 : 1));
67
+ }
68
+ previous = current;
69
+ }
70
+ return previous[b.length];
71
+ }
72
+ function similarity(left, right) {
73
+ const a = normalizedName(left);
74
+ const b = normalizedName(right);
75
+ const longest = Math.max([...a].length, [...b].length);
76
+ return longest === 0 ? 1 : 1 - levenshtein(a, b) / longest;
77
+ }
78
+ function nearVirtuals(keyword, virtuals) {
79
+ return virtuals
80
+ .map((item) => ({ item, score: similarity(keyword, item.name) }))
81
+ .filter(({ score }) => score >= 0.6 && score < 1)
82
+ .sort((left, right) => right.score - left.score)
83
+ .map(({ item }) => item)
84
+ .slice(0, MAX_SUGGESTIONS);
85
+ }
86
+ function orgPath(row, type) {
87
+ const direct = type === 'department' ? row.supDepartment : row.supSubDepartment;
88
+ if (text(direct))
89
+ return normalizePath(direct);
90
+ const title = text(row.title);
91
+ const name = text(row.name);
92
+ if (title && name && title.startsWith(`${name}|`)) {
93
+ return normalizePath(title.slice(name.length + 1));
94
+ }
95
+ return '';
96
+ }
97
+ function fullOrg(row, type, baseUrl, virtual) {
98
+ const id = text(row.id);
99
+ const name = text(row.name);
100
+ const parentPath = orgPath(row, type);
101
+ const rawFullPath = normalizePath(row.fullPath);
102
+ const fullPath = rawFullPath || (parentPath ? `${parentPath}/${name}` : name);
103
+ const virtualName = parentPath.split('/')[0] || virtual.name;
104
+ const parentId = text(row.parent?.id ?? row.parent);
105
+ const subcompanyId = text(row.subcompanyid ?? row.subcompanyId);
106
+ const virtualId = text(row.virtualid ?? row.virtualId) || virtual.id;
107
+ return {
108
+ id,
109
+ name,
110
+ type: text(row.type) || type,
111
+ ...(text(row.code) ? { code: text(row.code) } : {}),
112
+ fullPath,
113
+ ...(parentPath ? { parentPath } : {}),
114
+ ...(parentId ? { parentId } : {}),
115
+ ...(subcompanyId ? { subcompanyId } : {}),
116
+ virtualId,
117
+ virtualName,
118
+ ...(row.status !== undefined ? { status: row.status } : {}),
119
+ link: orgLink(baseUrl, id),
120
+ };
121
+ }
122
+ function orgCandidate(row, type, baseUrl, virtual) {
123
+ const item = fullOrg(row, type, baseUrl, virtual);
124
+ return {
125
+ id: item.id,
126
+ name: item.name,
127
+ type: item.type,
128
+ fullPath: item.fullPath,
129
+ ...(item.code ? { code: item.code } : {}),
130
+ link: item.link,
131
+ };
132
+ }
133
+ function browsePath(type) {
134
+ return type === 'department'
135
+ ? '/api/hrm/common/browser/data/department'
136
+ : '/api/hrm/common/browser/data/subcompany';
137
+ }
138
+ async function browseOrg(host, type, virtualId, keyword, condition, pageSize) {
139
+ const response = await host.request({
140
+ method: 'POST',
141
+ path: browsePath(type),
142
+ body: {
143
+ ...browserBaseBody(),
144
+ virtualType: 'ALL',
145
+ cmd: 'list',
146
+ current: 1,
147
+ pageSize,
148
+ companyId: virtualId,
149
+ formDatas: {
150
+ name: [{
151
+ nameCondition: {
152
+ id: condition,
153
+ content: condition === 'eq' ? '等于' : '包含',
154
+ disabled: false,
155
+ },
156
+ name: keyword,
157
+ }],
158
+ },
159
+ quickSearchDatas: {},
160
+ type: 'and',
161
+ stopDepartment: false,
162
+ },
163
+ });
164
+ const data = response?.data ?? {};
165
+ const rows = Array.isArray(data.data) ? data.data : [];
166
+ const reportedTotal = Number(data.total);
167
+ return {
168
+ type,
169
+ rows,
170
+ total: Number.isFinite(reportedTotal) ? reportedTotal : rows.length,
171
+ };
172
+ }
173
+ async function browseTypes(host, types, virtualId, keyword, condition, pageSize) {
174
+ return Promise.all(types.map((type) => browseOrg(host, type, virtualId, keyword, condition, pageSize)));
175
+ }
176
+ function exactRows(results, keyword) {
177
+ const normalized = normalizedName(keyword);
178
+ return results.map((result) => {
179
+ const rows = result.rows.filter((row) => normalizedName(row.name) === normalized);
180
+ return { ...result, rows, total: rows.length };
181
+ });
182
+ }
183
+ function totalMatches(results) {
184
+ return results.reduce((sum, result) => sum + Math.max(result.total, result.rows.length), 0);
185
+ }
186
+ function allRows(results) {
187
+ return results.flatMap((result) => result.rows.map((row) => ({ type: result.type, row })));
188
+ }
189
+ function resolvedFromMatches(results, explicitType) {
190
+ if (explicitType) {
191
+ const result = results[0];
192
+ const count = Math.max(result.total, result.rows.length);
193
+ return {
194
+ selected: count === 1 && result.rows[0] ? { type: result.type, row: result.rows[0] } : undefined,
195
+ alternatives: [],
196
+ ambiguous: count > 1,
197
+ };
198
+ }
199
+ const department = results.find((result) => result.type === 'department');
200
+ const subcompany = results.find((result) => result.type === 'subcompany');
201
+ const departmentCount = department ? Math.max(department.total, department.rows.length) : 0;
202
+ const subcompanyCount = subcompany ? Math.max(subcompany.total, subcompany.rows.length) : 0;
203
+ if (departmentCount === 1 && department?.rows[0]) {
204
+ return {
205
+ selected: { type: 'department', row: department.rows[0] },
206
+ alternatives: (subcompany?.rows ?? []).map((row) => ({ type: 'subcompany', row })),
207
+ ambiguous: false,
208
+ };
209
+ }
210
+ if (departmentCount > 1)
211
+ return { alternatives: [], ambiguous: true };
212
+ if (subcompanyCount === 1 && subcompany?.rows[0]) {
213
+ return { selected: { type: 'subcompany', row: subcompany.rows[0] }, alternatives: [], ambiguous: false };
214
+ }
215
+ return { alternatives: [], ambiguous: subcompanyCount > 1 };
216
+ }
217
+ function truncatedCandidates(results, maxCandidates, baseUrl, virtual) {
218
+ const candidateTotal = totalMatches(results);
219
+ const candidates = allRows(results)
220
+ .slice(0, maxCandidates)
221
+ .map(({ type, row }) => orgCandidate(row, type, baseUrl, virtual));
222
+ return {
223
+ candidates,
224
+ candidateTotal,
225
+ truncated: candidateTotal > candidates.length,
226
+ };
227
+ }
228
+ async function resolveOrgId(host, orgId, baseUrl, virtual) {
229
+ const response = await host.request({
230
+ method: 'GET',
231
+ path: ORG_DETAIL_PATH,
232
+ query: { id: orgId, readOnly: true, includeSub: true, quote: true, isMobile: false },
233
+ headers: { Referer: `${baseUrl}/sp/hrm/orgCard/${orgId}` },
234
+ });
235
+ const raw = response?.data?.data ?? {};
236
+ const type = raw.type === 'subcompany' ? 'subcompany' : 'department';
237
+ const parentName = Array.isArray(raw.parent) ? text(raw.parent[0]?.name) : '';
238
+ const subcompanyName = Array.isArray(raw.subcompany) ? text(raw.subcompany[0]?.name) : '';
239
+ const fullPath = normalizePath(raw.fullPath);
240
+ const pathParts = fullPath.split('/').filter(Boolean);
241
+ return {
242
+ id: text(raw.id) || orgId,
243
+ name: text(raw.name),
244
+ type,
245
+ ...(text(raw.code) ? { code: text(raw.code) } : {}),
246
+ ...(fullPath ? { fullPath } : {}),
247
+ ...(pathParts.length > 1 ? { parentPath: pathParts.slice(0, -1).join('/') } : {}),
248
+ ...(parentName ? { parentName } : {}),
249
+ ...(subcompanyName ? { subcompanyName } : {}),
250
+ virtualId: virtual.id,
251
+ virtualName: virtual.name,
252
+ ...(raw.status !== undefined ? { status: raw.status } : {}),
253
+ link: orgLink(baseUrl, orgId),
254
+ };
255
+ }
256
+ function relaxedKeywords(name) {
257
+ const characters = [...text(name)];
258
+ if (characters.length <= 2)
259
+ return [characters.join('')].filter(Boolean);
260
+ const suffix = characters.slice(-2).join('');
261
+ const prefix = characters.slice(0, 2).join('');
262
+ return suffix === prefix ? [suffix] : [suffix, prefix];
263
+ }
264
+ async function nearOrgSuggestions(host, type, virtual, name, pageSize, baseUrl) {
265
+ const results = [];
266
+ const seen = new Set();
267
+ for (const keyword of relaxedKeywords(name)) {
268
+ const result = await browseOrg(host, type, virtual.id, keyword, 'in', pageSize);
269
+ for (const row of result.rows) {
270
+ const id = text(row.id);
271
+ if (!id || seen.has(id))
272
+ continue;
273
+ seen.add(id);
274
+ results.push(orgCandidate(row, type, baseUrl, virtual));
275
+ }
276
+ }
277
+ return results;
278
+ }
279
+ const orgResolve = {
280
+ name: 'hrm.org.resolve',
281
+ resource: 'org',
282
+ method: 'org.resolve',
283
+ action: 'hrm.org.resolve',
284
+ summary: '按多维名称与组织名称解析唯一部门/分部;歧义或近似名称只返回候选,不代替用户选择',
285
+ risk: READ,
286
+ inputSchema: schemaOf({
287
+ virtual: { type: 'string', minLength: 1, description: '多维名称;与 virtualId 至少传一个' },
288
+ virtualId: SCHEMA_ID,
289
+ name: { type: 'string', minLength: 1, description: '组织名称;与 orgId 至少传一个' },
290
+ orgId: SCHEMA_ID,
291
+ type: { type: 'string', enum: [...ORG_TYPES] },
292
+ maxCandidates: { type: 'integer', minimum: 1, maximum: MAX_CANDIDATES, default: DEFAULT_MAX_CANDIDATES },
293
+ }),
294
+ handler: async (args, { host }) => {
295
+ const virtualKeyword = text(args.virtual);
296
+ const name = text(args.name);
297
+ const virtualId = args.virtualId === undefined ? '' : idValue(args.virtualId, 'virtualId');
298
+ const orgId = args.orgId === undefined ? '' : idValue(args.orgId, 'orgId');
299
+ if (!virtualKeyword && !virtualId) {
300
+ throw hrmValidationError('virtual_required', 'virtual 或 virtualId 至少传一个');
301
+ }
302
+ if (!name && !orgId) {
303
+ throw hrmValidationError('name_required', 'name 或 orgId 至少传一个');
304
+ }
305
+ const explicitType = args.type !== undefined;
306
+ const requestedType = explicitType ? text(args.type) : '';
307
+ if (explicitType && !ORG_TYPES.includes(requestedType)) {
308
+ throw hrmValidationError('enum_invalid', `type 取值必须是 ${ORG_TYPES.join(' / ')} 之一`);
309
+ }
310
+ const types = explicitType
311
+ ? [requestedType]
312
+ : ['department', 'subcompany'];
313
+ const maxCandidates = maxCandidatesValue(args.maxCandidates);
314
+ const pageSize = Math.max(100, maxCandidates);
315
+ const virtualResponse = await host.request({ method: 'GET', path: VIRTUAL_PATH });
316
+ const virtuals = readVirtuals(virtualResponse);
317
+ let virtual;
318
+ let virtualMatchType = 'exact';
319
+ if (virtualId) {
320
+ virtual = virtuals.find((item) => item.id === virtualId);
321
+ virtualMatchType = 'id';
322
+ }
323
+ else {
324
+ const keyword = normalizedName(virtualKeyword);
325
+ const exact = virtuals.filter((item) => normalizedName(item.name) === keyword);
326
+ const matches = exact.length > 0
327
+ ? exact
328
+ : virtuals.filter((item) => normalizedName(item.name).includes(keyword));
329
+ virtualMatchType = exact.length > 0 ? 'exact' : 'fuzzy';
330
+ if (matches.length > 1) {
331
+ const candidates = matches.slice(0, maxCandidates);
332
+ return hrmSuccess('hrm.org.resolve', {
333
+ status: 'ambiguous',
334
+ stage: 'virtual',
335
+ virtual: null,
336
+ matchType: virtualMatchType,
337
+ candidates,
338
+ candidateTotal: matches.length,
339
+ truncated: matches.length > candidates.length,
340
+ message: `多维「${virtualKeyword}」匹配到 ${matches.length} 个候选,请确认后把候选 id 作为 virtualId 复调。`,
341
+ });
342
+ }
343
+ virtual = matches[0];
344
+ }
345
+ if (!virtual) {
346
+ const suggestions = virtualKeyword ? nearVirtuals(virtualKeyword, virtuals) : [];
347
+ const available = virtuals.map((item) => item.name).join('、');
348
+ const target = virtualId ? `ID ${virtualId}` : `「${virtualKeyword}」`;
349
+ const suggestionText = suggestions.length > 0
350
+ ? `;近似名称:${suggestions.map((item) => item.name).join('、')}。不要自行替换名称,请先让用户确认`
351
+ : '';
352
+ return hrmSuccess('hrm.org.resolve', {
353
+ status: 'not_found',
354
+ stage: 'virtual',
355
+ virtual: null,
356
+ matchType: 'none',
357
+ candidates: [],
358
+ candidateTotal: 0,
359
+ suggestions,
360
+ suggestionTotal: suggestions.length,
361
+ availableVirtuals: virtuals,
362
+ message: `多维${target}不存在${suggestionText}。当前可用多维:${available || '无'}。`,
363
+ });
364
+ }
365
+ const virtualResult = {
366
+ id: virtual.id,
367
+ name: virtual.name,
368
+ keyword: virtualKeyword,
369
+ matchType: virtualMatchType,
370
+ };
371
+ const session = await host.getSession();
372
+ const baseUrl = session.baseUrl.replace(/\/+$/u, '');
373
+ if (orgId) {
374
+ const org = await resolveOrgId(host, orgId, baseUrl, virtual);
375
+ return hrmSuccess('hrm.org.resolve', {
376
+ status: 'resolved',
377
+ stage: 'org',
378
+ matchType: 'id',
379
+ type: org.type,
380
+ virtual: virtualResult,
381
+ org,
382
+ message: `已按 orgId 定位组织「${String(org.name || orgId)}」。`,
383
+ });
384
+ }
385
+ let matchType = 'exact';
386
+ let matches = await browseTypes(host, types, virtual.id, name, 'eq', pageSize);
387
+ let choice = resolvedFromMatches(matches, explicitType);
388
+ if (!choice.selected && !choice.ambiguous) {
389
+ const fuzzyResults = await browseTypes(host, types, virtual.id, name, 'in', pageSize);
390
+ const normalizedExact = exactRows(fuzzyResults, name);
391
+ if (totalMatches(normalizedExact) > 0) {
392
+ matches = normalizedExact;
393
+ matchType = 'exact';
394
+ }
395
+ else {
396
+ matches = fuzzyResults;
397
+ matchType = 'fuzzy';
398
+ }
399
+ choice = resolvedFromMatches(matches, explicitType);
400
+ }
401
+ if (choice.selected) {
402
+ const org = fullOrg(choice.selected.row, choice.selected.type, baseUrl, virtual);
403
+ const alsoMatched = choice.alternatives.map(({ type, row }) => orgCandidate(row, type, baseUrl, virtual));
404
+ const fuzzyMessage = matchType === 'fuzzy' ? '(模糊匹配)' : '';
405
+ const alternativeMessage = alsoMatched.length > 0
406
+ ? `;另有同名节点 ${alsoMatched.length} 个,请一并告知用户`
407
+ : '';
408
+ return hrmSuccess('hrm.org.resolve', {
409
+ status: 'resolved',
410
+ stage: 'org',
411
+ matchType,
412
+ type: choice.selected.type,
413
+ virtual: virtualResult,
414
+ org,
415
+ ...(alsoMatched.length > 0 ? { alsoMatched } : {}),
416
+ message: `已${fuzzyMessage}定位${choice.selected.type === 'department' ? '部门' : '分部'}「${name}」${alternativeMessage}。`,
417
+ });
418
+ }
419
+ if (choice.ambiguous) {
420
+ const candidateData = truncatedCandidates(matches, maxCandidates, baseUrl, virtual);
421
+ const truncatedMessage = candidateData.truncated ? `,仅列出前 ${maxCandidates} 个` : '';
422
+ return hrmSuccess('hrm.org.resolve', {
423
+ status: 'ambiguous',
424
+ stage: 'org',
425
+ matchType,
426
+ virtual: virtualResult,
427
+ ...candidateData,
428
+ message: `组织「${name}」匹配到 ${candidateData.candidateTotal} 个候选${truncatedMessage},请确认后把候选 id 作为 orgId 复调。`,
429
+ });
430
+ }
431
+ // 名称的精确/包含查询均未命中。近似探测只用于提示,绝不自动解析。
432
+ const suggestionType = (requestedType || 'department');
433
+ const allSuggestions = await nearOrgSuggestions(host, suggestionType, virtual, name, pageSize, baseUrl);
434
+ const suggestions = allSuggestions.slice(0, MAX_SUGGESTIONS);
435
+ const suggestionMessage = suggestions.length > 0
436
+ ? `;找到近似名称 ${suggestions.map((item) => item.name).join('、')},不要自行替换名称,请先让用户确认`
437
+ : '';
438
+ return hrmSuccess('hrm.org.resolve', {
439
+ status: 'not_found',
440
+ stage: 'org',
441
+ matchType: 'none',
442
+ virtual: virtualResult,
443
+ suggestions,
444
+ suggestionTotal: allSuggestions.length,
445
+ message: `在多维「${virtual.name}」中未找到组织「${name}」${suggestionMessage}。`,
446
+ });
447
+ },
448
+ };
449
+ export const hrmResolveOperations = [orgResolve];
@@ -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'),