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.
- package/README.md +19 -11
- package/dist/core/refs.js +39 -36
- package/dist/internal/e10/auth/commands.js +33 -18
- package/dist/internal/e10/auth/session.js +60 -2
- package/dist/internal/e10/auth/xiaoe.js +253 -53
- package/dist/internal/e10/context.js +18 -6
- package/dist/internal/e10/request-runtime.js +3 -1
- package/dist/shortcuts/caizhilian/operations/confirm.js +1 -1
- package/dist/shortcuts/caizhilian/operations/handlers/file-collection.js +901 -20
- package/dist/shortcuts/caizhilian/operations/handlers/folder.js +55 -0
- package/dist/shortcuts/caizhilian/operations/handlers/setting-attach.js +4 -4
- package/dist/shortcuts/caizhilian/operations/specs.js +75 -0
- package/dist/shortcuts/hrm/manifest.js +16 -9
- package/dist/shortcuts/hrm/operations/registry.js +2 -0
- package/dist/shortcuts/hrm/operations/resolve.js +449 -0
- package/dist/shortcuts/invoice/host.js +5 -4
- package/dist/shortcuts/meeting/operations/eb/association.js +151 -0
- package/dist/shortcuts/meeting/operations/eb/attendance-summary.js +153 -0
- package/dist/shortcuts/meeting/operations/eb/catalog.js +185 -13
- package/dist/shortcuts/meeting/operations/eb/conflict.js +17 -11
- package/dist/shortcuts/meeting/operations/eb/decision.js +385 -0
- package/dist/shortcuts/meeting/operations/eb/factory.js +222 -35
- package/dist/shortcuts/meeting/operations/eb/index.js +18 -1
- package/dist/shortcuts/meeting/operations/eb/link.js +212 -0
- package/dist/shortcuts/meeting/operations/eb/task-progress.js +123 -0
- package/dist/shortcuts/meeting/operations/env.js +78 -61
- package/dist/shortcuts/meeting/operations/link.js +125 -0
- package/dist/shortcuts/meeting/operations/registry.js +2 -0
- package/dist/shortcuts/meeting/operations/shared.js +13 -0
- package/dist/shortcuts/workflow/host.js +5 -1
- package/dist/shortcuts/workflow/operations/form.js +95 -11
- package/dist/shortcuts/workflow/operations/read.js +785 -26
- package/docs/e10-auth.md +8 -2
- package/docs/hrm.md +34 -2
- package/docs/invoice.md +3 -0
- package/package.json +2 -2
- package/scripts/xiaoe_env.py +118 -0
- package/skill-template/domains/hrm.md +2 -1
- package/skill-template/skill-template.md +1 -1
- package/skills/weaver-e10-blog/SKILL.md +1 -1
- package/skills/weaver-e10-caizhilian/SKILL.md +5 -4
- package/skills/weaver-e10-caizhilian/references/file-collection.md +37 -0
- package/skills/weaver-e10-caizhilian/references/folder.md +5 -1
- package/skills/weaver-e10-caizhilian/references/setting-attach.md +35 -5
- package/skills/weaver-e10-caizhilian/references/source-manifest.json +620 -5
- package/skills/weaver-e10-calendar/SKILL.md +1 -1
- package/skills/weaver-e10-ebuilder-form/SKILL.md +1 -1
- package/skills/weaver-e10-esb/SKILL.md +1 -1
- package/skills/weaver-e10-hrm/SKILL.md +52 -5
- package/skills/weaver-e10-hrm/references/org.md +107 -1
- package/skills/weaver-e10-jinchengda/SKILL.md +2 -1
- package/skills/weaver-e10-jiuchuanhui/SKILL.md +1 -1
- package/skills/weaver-e10-jucailin/SKILL.md +1 -1
- package/skills/weaver-e10-mail/SKILL.md +1 -1
- package/skills/weaver-e10-meeting/SKILL.md +27 -11
- package/skills/weaver-e10-meeting/references/meeting-eb.md +162 -7
- package/skills/weaver-e10-meeting/references/meeting-verification.md +271 -0
- package/skills/weaver-e10-meeting.zip +0 -0
- package/skills/weaver-e10-okr/SKILL.md +1 -1
- package/skills/weaver-e10-okr/references/source-manifest.json +3 -3
- package/skills/weaver-e10-plan/SKILL.md +1 -1
- package/skills/weaver-e10-qiyecheng/SKILL.md +1 -1
- package/skills/weaver-e10-shared/SKILL.md +10 -13
- package/skills/weaver-e10-shared/references/e10-auth-and-session.md +11 -17
- package/skills/weaver-e10-shared/references/non-interactive-environment.md +21 -14
- package/skills/weaver-e10-shijingran/SKILL.md +1 -1
- package/skills/weaver-e10-skill-maker/SKILL.md +1 -1
- package/skills/weaver-e10-skill-maker/references/weaver-skill-style.md +1 -1
- package/skills/weaver-e10-wenshuding/SKILL.md +1 -1
- package/skills/weaver-e10-workflow/SKILL.md +19 -19
- package/skills/weaver-e10-workflow/references/source-manifest.json +13 -13
- package/skills/weaver-e10-workflow/references/workflow-create.md +11 -7
- package/skills/weaver-e10-workflow/references/workflow-query.md +9 -14
- package/skills/weaver-e10-yepiaotong/SKILL.md +1 -2
- package/skills/weaver-e10-yimiaoban/SKILL.md +1 -1
- package/skills/weaver-e10-yimiaoban/references/source-manifest.json +3 -3
- 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];
|
|
@@ -135,10 +135,11 @@ export function createInvoiceHost(ctx, dependencies = {}) {
|
|
|
135
135
|
if (typeof token !== 'string' || !token.trim()) {
|
|
136
136
|
throw invoiceAuthError('invoice_token_missing', '未取得业票通业务 Token');
|
|
137
137
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
138
|
+
// Headless Web/Linux hosts may not provide a system keyring (for example,
|
|
139
|
+
// Secret Service over D-Bus). Persistence is only an optimization: keep
|
|
140
|
+
// the token in this process so the current business request can proceed,
|
|
141
|
+
// without falling back to an insecure plaintext cache.
|
|
142
|
+
await tokenStore.save(token);
|
|
142
143
|
invoiceToken = token;
|
|
143
144
|
return token;
|
|
144
145
|
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EB 建会后的**关联数据补建**(占用 / 回执 / 签到)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么必须补(2026-09-21 真机确认,应用设置 `open_mt_approve=0` + `only_flow_create=0`):
|
|
5
|
+
* eBuilder 的**自定义接口** `create_meeting` 只写会议主表,**不会**像页面建会那样
|
|
6
|
+
* 自动生成占用视图 / 回执 / 签到记录;而这三样恰好是后续能力的硬依赖:
|
|
7
|
+
*
|
|
8
|
+
* - 没有**占用**(roomusage)⇒ 会议室冲突检测(`query_roomusage_list`)查不到这场会,
|
|
9
|
+
* 以后别人在同一时段订同一间会议室不会被拦住;
|
|
10
|
+
* - 没有**回执**(receipt)⇒ 参会人冲突检测(以回执表 member_id 为准)对本场会议失效;
|
|
11
|
+
* - 没有**签到**记录(sign)⇒ `meeting.eb.sign.mark`(补签)报「未找到签到记录」。
|
|
12
|
+
*
|
|
13
|
+
* 因此建会成功后由本模块按「会议 + 参会人」逐条补齐,语义与页面建会一致。
|
|
14
|
+
*
|
|
15
|
+
* 失败语义(重要):主写入(建会)**已经成功**,这里任何一步失败都**不抛错**,
|
|
16
|
+
* 只把失败原因汇总进 `warnings` 并标明后果 —— 否则调用方会以为建会失败而重试,导致重复建会。
|
|
17
|
+
*/
|
|
18
|
+
import { EB_BASE, ebBusinessSuccess, normalizeEbWrite, writeBody } from './factory.js';
|
|
19
|
+
import { EB_ENDPOINTS } from './catalog.js';
|
|
20
|
+
/** 只在建会后补建关联数据;其他写操作直接跳过。 */
|
|
21
|
+
const CREATE_MEETING_PK = 'create_meeting';
|
|
22
|
+
/** 回执人员类型:1 = 参会人员(参会人冲突检测只统计该类型)。 */
|
|
23
|
+
const MEMBER_TYPE_ATTENDEE = '1';
|
|
24
|
+
function requireEndpoint(interfacePk) {
|
|
25
|
+
const endpoint = EB_ENDPOINTS.find((item) => item.interfacePk === interfacePk);
|
|
26
|
+
if (!endpoint)
|
|
27
|
+
throw new Error(`EB catalog 缺少 ${interfacePk},请检查 catalog.ts 声明`);
|
|
28
|
+
return endpoint;
|
|
29
|
+
}
|
|
30
|
+
/** 逗号分隔 id 串(`hrm_members` 等)→ 去重去空后的数组。 */
|
|
31
|
+
function splitIds(value) {
|
|
32
|
+
if (value === undefined || value === null)
|
|
33
|
+
return [];
|
|
34
|
+
const text = String(value).trim();
|
|
35
|
+
if (text === '')
|
|
36
|
+
return [];
|
|
37
|
+
const seen = new Set();
|
|
38
|
+
for (const part of text.split(',')) {
|
|
39
|
+
const id = part.trim();
|
|
40
|
+
if (id !== '')
|
|
41
|
+
seen.add(id);
|
|
42
|
+
}
|
|
43
|
+
return [...seen];
|
|
44
|
+
}
|
|
45
|
+
function errorText(error) {
|
|
46
|
+
return error instanceof Error ? error.message : String(error);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* 单条 EB 写入。**不抛错**:返回失败原因交由调用方汇总(主写入已成功,不可中断)。
|
|
50
|
+
* 复用 `normalizeEbWrite` 以保证字段白名单 / 必填 / 日期格式与手动调用完全一致。
|
|
51
|
+
*/
|
|
52
|
+
async function writeOne(env, interfacePk, args) {
|
|
53
|
+
try {
|
|
54
|
+
const session = await env.host.getSession();
|
|
55
|
+
const payload = normalizeEbWrite(requireEndpoint(interfacePk), args, session.userId);
|
|
56
|
+
const response = await env.host.request({
|
|
57
|
+
method: 'POST',
|
|
58
|
+
path: `${EB_BASE}/saveFormData/${interfacePk}`,
|
|
59
|
+
operation: 'meeting.eb.meeting.create.apply',
|
|
60
|
+
body: writeBody(session.userId, payload),
|
|
61
|
+
acceptBusiness: ebBusinessSuccess,
|
|
62
|
+
});
|
|
63
|
+
if (response?.datajson?.status === false) {
|
|
64
|
+
return { ok: false, error: `${interfacePk} 返回 status=false` };
|
|
65
|
+
}
|
|
66
|
+
return { ok: true };
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
return { ok: false, error: `${interfacePk}: ${errorText(error)}` };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export const ebMeetingAssociation = async (ctx) => {
|
|
73
|
+
if (ctx.endpoint.interfacePk !== CREATE_MEETING_PK)
|
|
74
|
+
return undefined;
|
|
75
|
+
const mtId = ctx.dataIds[0];
|
|
76
|
+
if (!mtId) {
|
|
77
|
+
return {
|
|
78
|
+
status: 'SKIPPED',
|
|
79
|
+
reason: 'create_meeting 未返回会议 id,无法补建占用/回执/签到,请到会议详情页确认。',
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const main = ctx.payload.mainTable;
|
|
83
|
+
const warnings = [];
|
|
84
|
+
const created = {};
|
|
85
|
+
// ① 会议室占用:仅在指定了会议室时写入。
|
|
86
|
+
const address = main.address === undefined || main.address === null ? '' : String(main.address).trim();
|
|
87
|
+
if (address === '') {
|
|
88
|
+
warnings.push('会议未指定会议室(address),已跳过占用写入。');
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
const result = await writeOne(ctx.env, 'create_roomusage', {
|
|
92
|
+
mt_id: mtId,
|
|
93
|
+
room_id: address,
|
|
94
|
+
startdate: main.begin_datetime,
|
|
95
|
+
enddate: main.end_datetime,
|
|
96
|
+
});
|
|
97
|
+
if (result.ok)
|
|
98
|
+
created.roomUsage = [address];
|
|
99
|
+
else {
|
|
100
|
+
warnings.push(`会议室占用写入失败(${result.error}):该会议不会被会议室冲突检测识别,`
|
|
101
|
+
+ '建议到会议详情页确认,或按需重新创建占用。');
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// ② 回执 + ③ 签到:按 hrm_members 逐人补齐。
|
|
105
|
+
const members = splitIds(main.hrm_members);
|
|
106
|
+
if (members.length === 0) {
|
|
107
|
+
warnings.push('会议未提供具体参会人员(hrm_members 为空),已跳过回执与签到写入:'
|
|
108
|
+
+ '参会人冲突检测与签到/补签都依赖这些记录;若通过 org_members 指定了部门,本 CLI 不展开部门成员,'
|
|
109
|
+
+ '请到会议详情页确认参会人后按需补建。');
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
const receiptFailed = [];
|
|
113
|
+
const signFailed = [];
|
|
114
|
+
for (const memberId of members) {
|
|
115
|
+
const receipt = await writeOne(ctx.env, 'create_meeting_receipt', {
|
|
116
|
+
mt_id: mtId,
|
|
117
|
+
member_type: MEMBER_TYPE_ATTENDEE,
|
|
118
|
+
member_id: memberId,
|
|
119
|
+
member_manager: memberId,
|
|
120
|
+
});
|
|
121
|
+
if (receipt.ok)
|
|
122
|
+
(created.receipt ??= []).push(memberId);
|
|
123
|
+
else
|
|
124
|
+
receiptFailed.push(`${memberId}(${receipt.error})`);
|
|
125
|
+
const sign = await writeOne(ctx.env, 'create_meeting_sign', {
|
|
126
|
+
mt_id: mtId,
|
|
127
|
+
user_id: memberId,
|
|
128
|
+
});
|
|
129
|
+
if (sign.ok)
|
|
130
|
+
(created.sign ??= []).push(memberId);
|
|
131
|
+
else
|
|
132
|
+
signFailed.push(`${memberId}(${sign.error})`);
|
|
133
|
+
}
|
|
134
|
+
if (receiptFailed.length > 0) {
|
|
135
|
+
warnings.push(`回执写入失败 ${receiptFailed.length}/${members.length} 人:${receiptFailed.join('、')}。`
|
|
136
|
+
+ '参会人冲突检测以回执表为准,缺失将导致该会议的参会人冲突漏检。');
|
|
137
|
+
}
|
|
138
|
+
if (signFailed.length > 0) {
|
|
139
|
+
warnings.push(`签到记录写入失败 ${signFailed.length}/${members.length} 人:${signFailed.join('、')}。`
|
|
140
|
+
+ '这些人将无法通过 meeting.eb.sign.mark 补签(无签到记录可定位)。');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
status: warnings.length === 0 ? 'COMPLETE' : 'PARTIAL',
|
|
145
|
+
mtId,
|
|
146
|
+
created,
|
|
147
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
148
|
+
note: 'eBuilder 自定义接口 create_meeting 不会像页面建会那样自动生成占用/回执/签到,'
|
|
149
|
+
+ '本步骤由 CLI 按「会议 + 参会人」补齐,供会议室/参会人冲突检测与签到补签使用。',
|
|
150
|
+
};
|
|
151
|
+
};
|