vps-work 2.5.0 → 2.5.1
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/dist/packages/vps-work/src/bin/vps-work.js +271 -0
- package/dist/packages/vps-work/src/commands/agent/index.js +264 -0
- package/dist/packages/vps-work/src/commands/approval/index.js +7 -0
- package/dist/packages/vps-work/src/commands/approval/shortcuts.js +259 -0
- package/dist/packages/vps-work/src/commands/attendance/applications.js +21 -0
- package/dist/packages/vps-work/src/commands/attendance/index.js +9 -0
- package/dist/packages/vps-work/src/commands/attendance/scheduling.js +183 -0
- package/dist/packages/vps-work/src/commands/attendance/shortcuts.js +131 -0
- package/dist/packages/vps-work/src/commands/auth/check.js +34 -0
- package/dist/packages/vps-work/src/commands/auth/index.js +21 -0
- package/dist/packages/vps-work/src/commands/auth/list.js +50 -0
- package/dist/packages/vps-work/src/commands/auth/scopes.js +17 -0
- package/dist/packages/vps-work/src/commands/auth/status.js +127 -0
- package/dist/packages/vps-work/src/commands/auth/tenants.js +35 -0
- package/dist/packages/vps-work/src/commands/auth/use.js +40 -0
- package/dist/packages/vps-work/src/commands/calendar/index.js +7 -0
- package/dist/packages/vps-work/src/commands/calendar/shortcuts.js +598 -0
- package/dist/packages/vps-work/src/commands/catalog.js +29 -0
- package/dist/packages/vps-work/src/commands/cli-install.js +70 -0
- package/dist/packages/vps-work/src/commands/collab-invoke.js +19 -0
- package/dist/packages/vps-work/src/commands/corporate/index.js +204 -0
- package/dist/packages/vps-work/src/commands/creative/index.js +369 -0
- package/dist/packages/vps-work/src/commands/documents/index.js +991 -0
- package/dist/packages/vps-work/src/commands/drive/index.js +435 -0
- package/dist/packages/vps-work/src/commands/exec.js +46 -0
- package/dist/packages/vps-work/src/commands/hr/business.js +22 -0
- package/dist/packages/vps-work/src/commands/hr/data-view-shortcuts.js +519 -0
- package/dist/packages/vps-work/src/commands/hr/index.js +15 -0
- package/dist/packages/vps-work/src/commands/hr/job-responsibilities.js +266 -0
- package/dist/packages/vps-work/src/commands/hr/payslip.js +45 -0
- package/dist/packages/vps-work/src/commands/hr/shortcuts.js +146 -0
- package/dist/packages/vps-work/src/commands/im/im-message-payload.js +150 -0
- package/dist/packages/vps-work/src/commands/im/index.js +7 -0
- package/dist/packages/vps-work/src/commands/im/shortcuts.js +856 -0
- package/dist/packages/vps-work/src/commands/knowledge/index.js +36 -0
- package/dist/packages/vps-work/src/commands/login.js +52 -0
- package/dist/packages/vps-work/src/commands/logout.js +12 -0
- package/dist/packages/vps-work/src/commands/mail/email.js +132 -0
- package/dist/packages/vps-work/src/commands/mail/index.js +9 -0
- package/dist/packages/vps-work/src/commands/mail/shortcuts.js +43 -0
- package/dist/packages/vps-work/src/commands/meeting/index.js +7 -0
- package/dist/packages/vps-work/src/commands/meeting/shortcuts.js +120 -0
- package/dist/packages/vps-work/src/commands/odoo-invoke.js +21 -0
- package/dist/packages/vps-work/src/commands/odoo-leaf-registry.js +13 -0
- package/dist/packages/vps-work/src/commands/office/index.js +59 -0
- package/dist/packages/vps-work/src/commands/okr/index.js +7 -0
- package/dist/packages/vps-work/src/commands/okr/shortcuts.js +75 -0
- package/dist/packages/vps-work/src/commands/org-structure.js +347 -0
- package/dist/packages/vps-work/src/commands/popup/index.js +147 -0
- package/dist/packages/vps-work/src/commands/product/abnormal-statistics.js +250 -0
- package/dist/packages/vps-work/src/commands/product/data-view-shortcuts.js +253 -0
- package/dist/packages/vps-work/src/commands/product/index.js +11 -0
- package/dist/packages/vps-work/src/commands/purchase/index.js +45 -0
- package/dist/packages/vps-work/src/commands/reauth.js +127 -0
- package/dist/packages/vps-work/src/commands/report/index.js +7 -0
- package/dist/packages/vps-work/src/commands/report/missing-daily-reports.js +9 -0
- package/dist/packages/vps-work/src/commands/report/shortcuts.js +251 -0
- package/dist/packages/vps-work/src/commands/sales/index.js +11 -0
- package/dist/packages/vps-work/src/commands/sales/period-params.js +75 -0
- package/dist/packages/vps-work/src/commands/sales/shortcuts-personnel.js +124 -0
- package/dist/packages/vps-work/src/commands/sales/shortcuts.js +800 -0
- package/dist/packages/vps-work/src/commands/shortcut-common.js +88 -0
- package/dist/packages/vps-work/src/commands/shortcut-params.js +22 -0
- package/dist/packages/vps-work/src/commands/skill/index.js +136 -0
- package/dist/packages/vps-work/src/commands/skill/personal.js +167 -0
- package/dist/packages/vps-work/src/commands/store/index.js +82 -0
- package/dist/packages/vps-work/src/commands/task/index.js +7 -0
- package/dist/packages/vps-work/src/commands/task/task-center.js +595 -0
- package/dist/packages/vps-work/src/commands/tools/index.js +127 -0
- package/dist/packages/vps-work/src/commands/vemory/index.js +7 -0
- package/dist/packages/vps-work/src/commands/vemory/shortcuts.js +53 -0
- package/dist/packages/vps-work/src/commands/vertu-aisales.js +30 -0
- package/dist/packages/vps-work/src/commands/vertu-erp.js +30 -0
- package/dist/packages/vps-work/src/core/aisales-mcp-client.js +35 -0
- package/dist/packages/vps-work/src/core/auth.js +240 -0
- package/dist/packages/vps-work/src/core/cli-bin.js +11 -0
- package/dist/packages/vps-work/src/core/cli-root-help.js +190 -0
- package/dist/packages/vps-work/src/core/client.js +158 -0
- package/dist/packages/vps-work/src/core/config.js +183 -0
- package/dist/packages/vps-work/src/core/corporate-client.js +52 -0
- package/dist/packages/vps-work/src/core/creative-attachment-download.js +48 -0
- package/dist/packages/vps-work/src/core/documents-client.js +57 -0
- package/dist/packages/vps-work/src/core/endpoints.js +46 -0
- package/dist/packages/vps-work/src/core/erp-mcp-client.js +11 -0
- package/dist/packages/vps-work/src/core/errors.js +107 -0
- package/dist/packages/vps-work/src/core/execute-catalog-client.js +42 -0
- package/dist/packages/vps-work/src/core/hermes-client.js +70 -0
- package/dist/packages/vps-work/src/core/im-attachment-download.js +109 -0
- package/dist/packages/vps-work/src/core/im-client.js +64 -0
- package/dist/packages/vps-work/src/core/keychain.js +58 -0
- package/dist/packages/vps-work/src/core/mail-client.js +82 -0
- package/dist/packages/vps-work/src/core/mcp-client.js +129 -0
- package/dist/packages/vps-work/src/core/office-client.js +25 -0
- package/dist/packages/vps-work/src/core/office-files.js +82 -0
- package/dist/packages/vps-work/src/core/paths.js +70 -0
- package/dist/packages/vps-work/src/core/public-result.js +43 -0
- package/dist/packages/vps-work/src/core/sales-invoice-attachments-client.js +75 -0
- package/dist/packages/vps-work/src/core/sales-query-client.js +452 -0
- package/dist/packages/vps-work/src/core/skill-catalog.js +158 -0
- package/dist/packages/vps-work/src/core/skill-key.js +19 -0
- package/dist/packages/vps-work/src/core/v-drive-store.js +392 -0
- package/dist/packages/vps-work/src/core/vertu-agent-env.js +32 -0
- package/dist/packages/vps-work/src/core/vps-auth-scopes.js +73 -0
- package/dist/packages/vps-work/src/core/vps-service-auth.js +382 -0
- package/dist/packages/vps-work/src/core/vps-service-config.js +83 -0
- package/dist/packages/vps-work/src/core/vps-service-http.js +187 -0
- package/dist/packages/vps-work/src/utils/at-file.js +34 -0
- package/dist/packages/vps-work/src/utils/cli-option-helpers.js +52 -0
- package/dist/packages/vps-work/src/utils/frontmatter.js +26 -0
- package/dist/packages/vps-work/src/utils/normalize-argv.js +51 -0
- package/dist/packages/vps-work/src/utils/parse-json-arg.js +85 -0
- package/dist/packages/vps-work/src/utils/prompt.js +46 -0
- package/dist/packages/vps-work/src/utils/read-text.js +44 -0
- package/dist/packages/vps-work/src/utils/schema-to-options.js +307 -0
- package/dist/packages/vps-work/src/utils/unwrap-envelope.js +31 -0
- package/dist/shared/im-org-tree/index.js +200 -0
- package/dist/vps-work.cjs +3 -3
- package/package.json +1 -1
- package/skills/vertu-aisales/SKILL.md +4 -2
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { VertuError } from '../../core/errors.js';
|
|
2
|
+
import { imRequest, printImResult } from '../../core/im-client.js';
|
|
3
|
+
import { attachSharedShortcutOptions, parseOptionalInt, resolveShortcutVpsServiceUrl, } from '../shortcut-common.js';
|
|
4
|
+
function requireDocArgs(opts) {
|
|
5
|
+
const resModel = opts.resModel?.trim();
|
|
6
|
+
if (!resModel) {
|
|
7
|
+
throw new VertuError('--res-model is required.');
|
|
8
|
+
}
|
|
9
|
+
const resId = parseOptionalInt(opts.resId, '--res-id');
|
|
10
|
+
if (resId === undefined) {
|
|
11
|
+
throw new VertuError('Invalid --res-id.');
|
|
12
|
+
}
|
|
13
|
+
return { resModel, resId };
|
|
14
|
+
}
|
|
15
|
+
function approvalCenterSource(raw) {
|
|
16
|
+
const source = raw?.trim() || 'all';
|
|
17
|
+
if (source !== 'all' && source !== 'business' && source !== 'attendance') {
|
|
18
|
+
throw new VertuError('--source must be one of: all, business, attendance.');
|
|
19
|
+
}
|
|
20
|
+
return source;
|
|
21
|
+
}
|
|
22
|
+
function approvalPendingScope(raw) {
|
|
23
|
+
const scope = raw?.trim() || 'mine';
|
|
24
|
+
if (scope !== 'mine' && scope !== 'department' && scope !== 'all') {
|
|
25
|
+
throw new VertuError('--scope must be one of: mine, department, all.');
|
|
26
|
+
}
|
|
27
|
+
return scope;
|
|
28
|
+
}
|
|
29
|
+
function csvPositiveIds(raw, flag) {
|
|
30
|
+
if (!raw?.trim())
|
|
31
|
+
return undefined;
|
|
32
|
+
const ids = raw.split(',').map((value) => Number(value.trim()));
|
|
33
|
+
if (ids.some((value) => !Number.isInteger(value) || value <= 0)) {
|
|
34
|
+
throw new VertuError(`${flag} must be comma-separated positive integers.`);
|
|
35
|
+
}
|
|
36
|
+
return [...new Set(ids)].join(',');
|
|
37
|
+
}
|
|
38
|
+
function eventDate(item) {
|
|
39
|
+
const value = item.event_date;
|
|
40
|
+
return typeof value === 'string' ? value : '';
|
|
41
|
+
}
|
|
42
|
+
async function listApprovalCenter(tab, opts, cmd) {
|
|
43
|
+
const limit = parseOptionalInt(opts.limit, '--limit') ?? 20;
|
|
44
|
+
const offset = parseOptionalInt(opts.offset, '--offset') ?? 0;
|
|
45
|
+
if (limit < 1)
|
|
46
|
+
throw new VertuError('--limit must be at least 1.');
|
|
47
|
+
if (offset < 0)
|
|
48
|
+
throw new VertuError('--offset must be at least 0.');
|
|
49
|
+
if (opts.all && offset !== 0) {
|
|
50
|
+
throw new VertuError('--all cannot be combined with a non-zero --offset.');
|
|
51
|
+
}
|
|
52
|
+
const source = approvalCenterSource(opts.source);
|
|
53
|
+
const merged = cmd.optsWithGlobals();
|
|
54
|
+
const baseUrl = opts.baseUrl ?? merged.baseUrl;
|
|
55
|
+
const page = async (pageSource, pageOffset) => (await imRequest('GET', '/v1/approvals/center', {
|
|
56
|
+
baseUrl,
|
|
57
|
+
query: { tab, limit, offset: pageOffset, source: pageSource === 'business' ? 'odoo' : pageSource },
|
|
58
|
+
}));
|
|
59
|
+
if (!opts.all)
|
|
60
|
+
return page(source, offset);
|
|
61
|
+
const sources = source === 'all' ? ['business', 'attendance'] : [source];
|
|
62
|
+
const items = [];
|
|
63
|
+
let firstPage;
|
|
64
|
+
for (const pageSource of sources) {
|
|
65
|
+
let pageOffset = 0;
|
|
66
|
+
while (true) {
|
|
67
|
+
const current = await page(pageSource, pageOffset);
|
|
68
|
+
firstPage ??= current;
|
|
69
|
+
items.push(...current.items);
|
|
70
|
+
if (!current.has_more)
|
|
71
|
+
break;
|
|
72
|
+
const nextOffset = current.next_offset;
|
|
73
|
+
if (typeof nextOffset !== 'number' || nextOffset <= pageOffset) {
|
|
74
|
+
throw new VertuError('Vertu 服务返回了无效的审批分页游标。');
|
|
75
|
+
}
|
|
76
|
+
pageOffset = nextOffset;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
items.sort((left, right) => eventDate(right).localeCompare(eventDate(left)));
|
|
80
|
+
return {
|
|
81
|
+
ok: firstPage?.ok ?? true,
|
|
82
|
+
tab,
|
|
83
|
+
count: items.length,
|
|
84
|
+
items,
|
|
85
|
+
has_more: false,
|
|
86
|
+
next_offset: null,
|
|
87
|
+
web_url: firstPage?.web_url,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
export function attachApprovalShortcuts(parent) {
|
|
91
|
+
const waitListOpts = (cmd) => cmd.option('--limit <n>', 'Max rows', '200');
|
|
92
|
+
const centerListOpts = (cmd) => cmd
|
|
93
|
+
.option('--limit <n>', 'Rows per page (server maximum: 20)', '20')
|
|
94
|
+
.option('--offset <n>', 'Starting row offset', '0')
|
|
95
|
+
.option('--source <source>', '审批来源:all、business 或 attendance', 'all')
|
|
96
|
+
.option('--all', 'Fetch all pages (separates sources to avoid skipped rows)', false);
|
|
97
|
+
attachSharedShortcutOptions(waitListOpts(parent
|
|
98
|
+
.command('+wait')
|
|
99
|
+
.description('待我审批(含单据标题与关键字段)'))).action(async (opts, cmd) => {
|
|
100
|
+
const limit = parseOptionalInt(opts.limit, '--limit') ?? 50;
|
|
101
|
+
const merged = cmd.optsWithGlobals();
|
|
102
|
+
const result = (await imRequest('GET', '/v1/approvals', {
|
|
103
|
+
baseUrl: opts.baseUrl ?? merged.baseUrl,
|
|
104
|
+
query: { limit },
|
|
105
|
+
}));
|
|
106
|
+
if (opts.json === false) {
|
|
107
|
+
if (!result.items.length) {
|
|
108
|
+
console.log('当前没有待你审批的单据。');
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
console.log(`待审批 ${result.count} 条:`);
|
|
112
|
+
result.items.forEach((item, index) => {
|
|
113
|
+
const head = `${index + 1}. ${item.title ?? item.flow_name} (${item.res_model}#${item.res_id})`;
|
|
114
|
+
const fields = (item.fields ?? [])
|
|
115
|
+
.map((field) => `${field.label}: ${field.value}`)
|
|
116
|
+
.join(';');
|
|
117
|
+
console.log(fields ? `${head} — ${fields}` : head);
|
|
118
|
+
});
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
printImResult(opts.json, result);
|
|
122
|
+
});
|
|
123
|
+
attachSharedShortcutOptions(parent
|
|
124
|
+
.command('+pending')
|
|
125
|
+
.description('按本人、负责部门或全公司查询待审批单')
|
|
126
|
+
.option('--scope <scope>', 'Query scope: mine, department, or all', 'mine')
|
|
127
|
+
.option('--department-ids <ids>', 'Comma-separated approver department IDs')
|
|
128
|
+
.option('--approver-user-ids <ids>', 'Comma-separated approver user IDs')
|
|
129
|
+
.option('--limit <n>', 'Rows per page (server maximum: 200)', '50')
|
|
130
|
+
.option('--offset <n>', 'Starting row offset', '0')
|
|
131
|
+
.option('--all', 'Fetch all pages', false)
|
|
132
|
+
.option('--all-pages', 'Alias for --all', false)).action(async (opts, cmd) => {
|
|
133
|
+
const scope = approvalPendingScope(opts.scope);
|
|
134
|
+
const limit = parseOptionalInt(opts.limit, '--limit') ?? 50;
|
|
135
|
+
let offset = parseOptionalInt(opts.offset, '--offset') ?? 0;
|
|
136
|
+
if (limit < 1 || limit > 200)
|
|
137
|
+
throw new VertuError('--limit must be between 1 and 200.');
|
|
138
|
+
if (offset < 0)
|
|
139
|
+
throw new VertuError('--offset must be at least 0.');
|
|
140
|
+
const fetchAll = opts.all === true || opts.allPages === true;
|
|
141
|
+
if (fetchAll && offset !== 0) {
|
|
142
|
+
throw new VertuError('--all cannot be combined with a non-zero --offset.');
|
|
143
|
+
}
|
|
144
|
+
const merged = cmd.optsWithGlobals();
|
|
145
|
+
const baseUrl = opts.baseUrl ?? merged.baseUrl;
|
|
146
|
+
const query = {
|
|
147
|
+
scope,
|
|
148
|
+
limit,
|
|
149
|
+
department_ids: csvPositiveIds(opts.departmentIds, '--department-ids'),
|
|
150
|
+
approver_user_ids: csvPositiveIds(opts.approverUserIds, '--approver-user-ids'),
|
|
151
|
+
};
|
|
152
|
+
if (opts.validateOnly === true || opts.dryRun === true) {
|
|
153
|
+
printImResult(opts.json, {
|
|
154
|
+
validateOnly: true,
|
|
155
|
+
endpoint: `${resolveShortcutVpsServiceUrl(opts, cmd)}/v1/approvals`,
|
|
156
|
+
query: { ...query, offset },
|
|
157
|
+
all: fetchAll,
|
|
158
|
+
});
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const items = [];
|
|
162
|
+
let first = null;
|
|
163
|
+
while (true) {
|
|
164
|
+
const page = await imRequest('GET', '/v1/approvals', {
|
|
165
|
+
baseUrl,
|
|
166
|
+
query: { ...query, offset },
|
|
167
|
+
});
|
|
168
|
+
first ??= page;
|
|
169
|
+
items.push(...(Array.isArray(page.items) ? page.items : []));
|
|
170
|
+
if (!fetchAll || page.has_more !== true)
|
|
171
|
+
break;
|
|
172
|
+
if (typeof page.next_offset !== 'number' || page.next_offset <= offset) {
|
|
173
|
+
throw new VertuError('Vertu 服务返回了无效的审批分页游标。');
|
|
174
|
+
}
|
|
175
|
+
offset = page.next_offset;
|
|
176
|
+
}
|
|
177
|
+
printImResult(opts.json, fetchAll ? { ...first, items, has_more: false, next_offset: null } : first);
|
|
178
|
+
});
|
|
179
|
+
attachSharedShortcutOptions(centerListOpts(parent
|
|
180
|
+
.command('+my-submitted')
|
|
181
|
+
.description('我提交的审批'))).action(async (opts, cmd) => {
|
|
182
|
+
const result = await listApprovalCenter('submitted', opts, cmd);
|
|
183
|
+
printImResult(opts.json, result);
|
|
184
|
+
});
|
|
185
|
+
attachSharedShortcutOptions(centerListOpts(parent
|
|
186
|
+
.command('+my-approved')
|
|
187
|
+
.description('我已审批的'))).action(async (opts, cmd) => {
|
|
188
|
+
const result = await listApprovalCenter('processed', opts, cmd);
|
|
189
|
+
printImResult(opts.json, result);
|
|
190
|
+
});
|
|
191
|
+
attachSharedShortcutOptions(centerListOpts(parent
|
|
192
|
+
.command('+cc')
|
|
193
|
+
.description('抄送我的审批'))).action(async (opts, cmd) => {
|
|
194
|
+
const result = await listApprovalCenter('cc', opts, cmd);
|
|
195
|
+
printImResult(opts.json, result);
|
|
196
|
+
});
|
|
197
|
+
const docAction = (cmd, description, requireProposal) => {
|
|
198
|
+
const c = cmd
|
|
199
|
+
.description(description)
|
|
200
|
+
.requiredOption('--res-model <name>', '业务模型 _name,如 sale.order')
|
|
201
|
+
.requiredOption('--res-id <id>', '业务记录 id');
|
|
202
|
+
if (requireProposal) {
|
|
203
|
+
return c.requiredOption('--proposal <text>', '驳回原因(不可为空)');
|
|
204
|
+
}
|
|
205
|
+
return c.option('--proposal <text>', '审批意见;省略等同空串');
|
|
206
|
+
};
|
|
207
|
+
attachSharedShortcutOptions(docAction(parent.command('+pass'), '审批通过当前待办步骤', false)).action(async (opts, cmd) => {
|
|
208
|
+
const { resModel, resId } = requireDocArgs(opts);
|
|
209
|
+
const merged = cmd.optsWithGlobals();
|
|
210
|
+
const result = await imRequest('POST', '/v1/approvals/pass', {
|
|
211
|
+
baseUrl: opts.baseUrl ?? merged.baseUrl,
|
|
212
|
+
body: { res_model: resModel, res_id: resId, proposal: opts.proposal ?? '' },
|
|
213
|
+
});
|
|
214
|
+
printImResult(opts.json, result);
|
|
215
|
+
});
|
|
216
|
+
attachSharedShortcutOptions(docAction(parent.command('+refuse'), '审批驳回', true)).action(async (opts, cmd) => {
|
|
217
|
+
const { resModel, resId } = requireDocArgs(opts);
|
|
218
|
+
const proposal = opts.proposal?.trim();
|
|
219
|
+
if (!proposal) {
|
|
220
|
+
throw new VertuError('--proposal is required for refuse.');
|
|
221
|
+
}
|
|
222
|
+
const merged = cmd.optsWithGlobals();
|
|
223
|
+
const result = await imRequest('POST', '/v1/approvals/refuse', {
|
|
224
|
+
baseUrl: opts.baseUrl ?? merged.baseUrl,
|
|
225
|
+
body: { res_model: resModel, res_id: resId, proposal },
|
|
226
|
+
});
|
|
227
|
+
printImResult(opts.json, result);
|
|
228
|
+
});
|
|
229
|
+
attachSharedShortcutOptions(parent
|
|
230
|
+
.command('+logs')
|
|
231
|
+
.description('单据审批轨迹')
|
|
232
|
+
.requiredOption('--res-model <name>', '业务模型 _name')
|
|
233
|
+
.requiredOption('--res-id <id>', '业务记录 id')).action(async (opts, cmd) => {
|
|
234
|
+
const { resModel, resId } = requireDocArgs(opts);
|
|
235
|
+
const merged = cmd.optsWithGlobals();
|
|
236
|
+
const result = (await imRequest('GET', '/v1/approvals/logs', {
|
|
237
|
+
baseUrl: opts.baseUrl ?? merged.baseUrl,
|
|
238
|
+
query: { res_model: resModel, res_id: resId },
|
|
239
|
+
}));
|
|
240
|
+
if (opts.json === false) {
|
|
241
|
+
if (!result.logs.length) {
|
|
242
|
+
console.log('该单据暂无审批轨迹。');
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
for (const log of result.logs) {
|
|
246
|
+
const segments = [
|
|
247
|
+
log.action_label,
|
|
248
|
+
log.employee_name ? `由 ${log.employee_name}` : '',
|
|
249
|
+
log.node_name ? `(${log.node_name})` : '',
|
|
250
|
+
new Date(log.created_at).toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }),
|
|
251
|
+
].filter(Boolean);
|
|
252
|
+
const line = segments.join(' ');
|
|
253
|
+
console.log(log.proposal ? `${line} — ${log.proposal}` : line);
|
|
254
|
+
}
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
printImResult(opts.json, result);
|
|
258
|
+
});
|
|
259
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { attachSharedShortcutOptions, runSkillShortcutExecute } from '../shortcut-common.js';
|
|
2
|
+
const SKILL = 'vps-attendance-applications';
|
|
3
|
+
export function attachAttendanceApplications(parent) {
|
|
4
|
+
const add = (name, description, script, configure) => {
|
|
5
|
+
let command = parent.command(name).description(description);
|
|
6
|
+
if (configure)
|
|
7
|
+
command = configure(command);
|
|
8
|
+
attachSharedShortcutOptions(command).action(async (opts, cmd) => {
|
|
9
|
+
await runSkillShortcutExecute(SKILL, script, opts.requestId ? { request_id: opts.requestId } : {}, opts, cmd);
|
|
10
|
+
});
|
|
11
|
+
};
|
|
12
|
+
add('+applications', '查询本人考勤申请;过滤条件通过 --params 传入', 'list_requests');
|
|
13
|
+
add('+application-preview', '预览申请审批流和服务端计算结果', 'request_preview');
|
|
14
|
+
add('+leave-types', '查询可用假期类型', 'leave_types');
|
|
15
|
+
add('+leave-options', '查询考勤申请选项', 'leave_options');
|
|
16
|
+
add('+leave-policy', '解析请假策略和可用余额', 'leave_policy');
|
|
17
|
+
add('+leave-balances', '查询本人假期余额', 'leave_balances');
|
|
18
|
+
add('+overtime-balances', '查询本人加班余额', 'overtime_balances');
|
|
19
|
+
add('+application-submit', '提交请假、出差、外勤、加班、补卡或换班申请;申请体通过 --params-file 传入', 'submit_request');
|
|
20
|
+
add('+application-cancel', '取消本人待审批考勤申请', 'cancel_request', (cmd) => cmd.requiredOption('--request-id <id>'));
|
|
21
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { attachAttendanceShortcuts } from './shortcuts.js';
|
|
3
|
+
import { attachAttendanceApplications } from './applications.js';
|
|
4
|
+
export function buildAttendanceCommand() {
|
|
5
|
+
const attendance = new Command('attendance').description('Query attendance reports and manage authorized employee schedules.');
|
|
6
|
+
attachAttendanceShortcuts(attendance);
|
|
7
|
+
attachAttendanceApplications(attendance);
|
|
8
|
+
return attendance;
|
|
9
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { VertuError } from '../../core/errors.js';
|
|
2
|
+
import { imRequest, printImResult } from '../../core/im-client.js';
|
|
3
|
+
import { resolveVpsServiceUrl } from '../../core/sales-query-client.js';
|
|
4
|
+
import { expandAtFileToken } from '../../utils/at-file.js';
|
|
5
|
+
import { parseJsonArg } from '../../utils/parse-json-arg.js';
|
|
6
|
+
import { attachSharedShortcutOptions, mergeShortcutParams } from '../shortcut-common.js';
|
|
7
|
+
function text(value, flag) {
|
|
8
|
+
if (typeof value !== 'string' || !value.trim())
|
|
9
|
+
throw new VertuError(`${flag} must not be empty.`);
|
|
10
|
+
return value.trim();
|
|
11
|
+
}
|
|
12
|
+
function date(value, flag) {
|
|
13
|
+
const result = text(value, flag);
|
|
14
|
+
const parsed = new Date(`${result}T00:00:00.000Z`);
|
|
15
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(result) || Number.isNaN(parsed.getTime()) || parsed.toISOString().slice(0, 10) !== result) {
|
|
16
|
+
throw new VertuError(`${flag} must be a valid YYYY-MM-DD date.`);
|
|
17
|
+
}
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
function positive(value, flag, fallback, max) {
|
|
21
|
+
const raw = value === undefined || value === null || value === '' ? fallback : Number(value);
|
|
22
|
+
if (!Number.isSafeInteger(raw) || Number(raw) <= 0 || (max !== undefined && Number(raw) > max)) {
|
|
23
|
+
throw new VertuError(`${flag} must be a positive integer${max ? ` no greater than ${max}` : ''}.`);
|
|
24
|
+
}
|
|
25
|
+
return Number(raw);
|
|
26
|
+
}
|
|
27
|
+
function normalizeRange(input) {
|
|
28
|
+
const from = date(input.from_date, 'from_date');
|
|
29
|
+
const to = date(input.to_date, 'to_date');
|
|
30
|
+
const days = (Date.parse(`${to}T00:00:00Z`) - Date.parse(`${from}T00:00:00Z`)) / 86_400_000 + 1;
|
|
31
|
+
if (days < 1 || days > 93)
|
|
32
|
+
throw new VertuError('The schedule date range must contain 1 to 93 days.');
|
|
33
|
+
const result = {
|
|
34
|
+
...input,
|
|
35
|
+
from_date: from,
|
|
36
|
+
to_date: to,
|
|
37
|
+
page: positive(input.page, 'page', 1),
|
|
38
|
+
page_size: positive(input.page_size, 'page_size', 100, 200),
|
|
39
|
+
};
|
|
40
|
+
if (input.employee_id !== undefined)
|
|
41
|
+
result.employee_id = positive(input.employee_id, 'employee_id');
|
|
42
|
+
if (input.department_id !== undefined)
|
|
43
|
+
result.department_id = positive(input.department_id, 'department_id');
|
|
44
|
+
if (input.query !== undefined && typeof input.query !== 'string')
|
|
45
|
+
throw new VertuError('query must be a string.');
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
function normalizePlan(input, apply) {
|
|
49
|
+
const ruleID = text(input.rule_id, 'rule_id');
|
|
50
|
+
const mode = input.mode === undefined ? 'fill_missing' : text(input.mode, 'mode');
|
|
51
|
+
if (!['fill_missing', 'replace'].includes(mode))
|
|
52
|
+
throw new VertuError('mode must be fill_missing or replace.');
|
|
53
|
+
if (!Array.isArray(input.items) || input.items.length < 1 || input.items.length > 5000)
|
|
54
|
+
throw new VertuError('items must contain 1 to 5000 schedule entries.');
|
|
55
|
+
const keys = new Set();
|
|
56
|
+
const employees = new Set();
|
|
57
|
+
const dates = [];
|
|
58
|
+
const items = input.items.map((raw, index) => {
|
|
59
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
60
|
+
throw new VertuError(`items[${index}] must be an object.`);
|
|
61
|
+
const item = raw;
|
|
62
|
+
const employeeID = positive(item.employee_id, `items[${index}].employee_id`);
|
|
63
|
+
const workDate = date(item.work_date, `items[${index}].work_date`);
|
|
64
|
+
const shiftTemplateID = text(item.shift_template_id, `items[${index}].shift_template_id`);
|
|
65
|
+
const remark = item.remark === undefined ? undefined : text(item.remark, `items[${index}].remark`);
|
|
66
|
+
const key = `${employeeID}:${workDate}`;
|
|
67
|
+
if (keys.has(key))
|
|
68
|
+
throw new VertuError(`Duplicate schedule entry for ${key}.`);
|
|
69
|
+
keys.add(key);
|
|
70
|
+
employees.add(employeeID);
|
|
71
|
+
dates.push(workDate);
|
|
72
|
+
return { employee_id: employeeID, work_date: workDate, shift_template_id: shiftTemplateID, ...(remark ? { remark } : {}) };
|
|
73
|
+
});
|
|
74
|
+
if (employees.size > 200)
|
|
75
|
+
throw new VertuError('A schedule plan cannot contain more than 200 employees.');
|
|
76
|
+
dates.sort();
|
|
77
|
+
const rangeDays = (Date.parse(`${dates.at(-1)}T00:00:00Z`) - Date.parse(`${dates[0]}T00:00:00Z`)) / 86_400_000 + 1;
|
|
78
|
+
if (rangeDays > 93)
|
|
79
|
+
throw new VertuError('A schedule plan cannot span more than 93 days.');
|
|
80
|
+
const result = { ...input, rule_id: ruleID, mode, items };
|
|
81
|
+
if (apply) {
|
|
82
|
+
result.preview_token = text(input.preview_token, 'preview_token');
|
|
83
|
+
result.client_request_id = text(input.client_request_id, 'client_request_id');
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
delete result.preview_token;
|
|
87
|
+
delete result.client_request_id;
|
|
88
|
+
}
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
async function basePlan(opts) {
|
|
92
|
+
if (opts.items !== undefined && opts.itemsFile !== undefined)
|
|
93
|
+
throw new VertuError('--items and --items-file are mutually exclusive.');
|
|
94
|
+
let items;
|
|
95
|
+
if (opts.items !== undefined)
|
|
96
|
+
items = parseJsonArg(opts.items, '--items');
|
|
97
|
+
if (opts.itemsFile !== undefined)
|
|
98
|
+
items = parseJsonArg(await expandAtFileToken(`@${opts.itemsFile}`, '--items-file'), '--items-file');
|
|
99
|
+
const shorthandUsed = Boolean(opts.employeeIds || opts.fromDate || opts.toDate || opts.shiftTemplateId);
|
|
100
|
+
if (items !== undefined && shorthandUsed)
|
|
101
|
+
throw new VertuError('--items/--items-file cannot be combined with range shorthand options.');
|
|
102
|
+
if (items === undefined && shorthandUsed) {
|
|
103
|
+
const employeeIDs = text(opts.employeeIds, '--employee-ids').split(',').map((value) => positive(value.trim(), '--employee-ids'));
|
|
104
|
+
const from = date(opts.fromDate, '--from-date');
|
|
105
|
+
const to = date(opts.toDate, '--to-date');
|
|
106
|
+
const shift = text(opts.shiftTemplateId, '--shift-template-id');
|
|
107
|
+
const days = (Date.parse(`${to}T00:00:00Z`) - Date.parse(`${from}T00:00:00Z`)) / 86_400_000 + 1;
|
|
108
|
+
if (days < 1 || days > 93)
|
|
109
|
+
throw new VertuError('The schedule date range must contain 1 to 93 days.');
|
|
110
|
+
items = [];
|
|
111
|
+
for (let offset = 0; offset < days; offset += 1) {
|
|
112
|
+
const current = new Date(`${from}T00:00:00Z`);
|
|
113
|
+
current.setUTCDate(current.getUTCDate() + offset);
|
|
114
|
+
for (const employeeID of employeeIDs) {
|
|
115
|
+
items.push({ employee_id: employeeID, work_date: current.toISOString().slice(0, 10), shift_template_id: shift, ...(opts.remark?.trim() ? { remark: opts.remark.trim() } : {}) });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return { rule_id: opts.ruleId, mode: opts.mode ?? 'fill_missing', ...(items === undefined ? {} : { items }), ...(opts.previewToken ? { preview_token: opts.previewToken } : {}), ...(opts.clientRequestId ? { client_request_id: opts.clientRequestId } : {}) };
|
|
120
|
+
}
|
|
121
|
+
function baseURL(opts, cmd) {
|
|
122
|
+
return opts.baseUrl ?? cmd.optsWithGlobals().baseUrl;
|
|
123
|
+
}
|
|
124
|
+
function preview(opts, cmd, method, path, payload) {
|
|
125
|
+
if (opts.validateOnly !== true && opts.dryRun !== true)
|
|
126
|
+
return false;
|
|
127
|
+
printImResult(opts.json, { validateOnly: true, method, endpoint: `${resolveVpsServiceUrl(baseURL(opts, cmd))}${path}`, ...payload });
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
export function attachAttendanceSchedulingShortcuts(parent) {
|
|
131
|
+
attachSharedShortcutOptions(parent.command('+schedule-rules').description('查询当前用户可执行排班的考勤规则'))
|
|
132
|
+
.action(async (opts, cmd) => {
|
|
133
|
+
const path = '/v1/attendance/scheduling/rules';
|
|
134
|
+
if (preview(opts, cmd, 'GET', path, {}))
|
|
135
|
+
return;
|
|
136
|
+
printImResult(opts.json, await imRequest('GET', path, { baseUrl: baseURL(opts, cmd) }));
|
|
137
|
+
});
|
|
138
|
+
attachSharedShortcutOptions(parent.command('+schedule-shifts').description('查询规则下可用和不可用班次').requiredOption('--rule-id <id>').option('--availability <value>', 'all|available|unavailable', 'all'))
|
|
139
|
+
.action(async (opts, cmd) => {
|
|
140
|
+
const params = await mergeShortcutParams({ availability: opts.availability }, opts);
|
|
141
|
+
const ruleID = text(params.rule_id ?? opts.ruleId, 'rule_id');
|
|
142
|
+
const availability = text(params.availability ?? 'all', 'availability');
|
|
143
|
+
if (!['all', 'available', 'unavailable'].includes(availability))
|
|
144
|
+
throw new VertuError('availability must be all, available, or unavailable.');
|
|
145
|
+
const path = `/v1/attendance/scheduling/rules/${encodeURIComponent(ruleID)}/shifts`;
|
|
146
|
+
const query = { availability };
|
|
147
|
+
if (preview(opts, cmd, 'GET', path, { query }))
|
|
148
|
+
return;
|
|
149
|
+
printImResult(opts.json, await imRequest('GET', path, { baseUrl: baseURL(opts, cmd), query }));
|
|
150
|
+
});
|
|
151
|
+
for (const [name, description] of [['+schedule-members', '查询规则下人员及排班事实'], ['+schedules', '查询规则下排班详情']]) {
|
|
152
|
+
attachSharedShortcutOptions(parent.command(name).description(description).requiredOption('--rule-id <id>').requiredOption('--from-date <ymd>').requiredOption('--to-date <ymd>')
|
|
153
|
+
.option('--employee-id <id>').option('--department-id <id>').option('--query <text>').option('--page <n>', '页码', '1').option('--page-size <n>', '每页人数,最大 200', '100'))
|
|
154
|
+
.action(async (opts, cmd) => {
|
|
155
|
+
const merged = await mergeShortcutParams({ rule_id: opts.ruleId, from_date: opts.fromDate, to_date: opts.toDate, employee_id: opts.employeeId, department_id: opts.departmentId, query: opts.query, page: opts.page, page_size: opts.pageSize }, opts);
|
|
156
|
+
const ruleID = text(merged.rule_id, 'rule_id');
|
|
157
|
+
delete merged.rule_id;
|
|
158
|
+
Object.keys(merged).forEach((key) => merged[key] === undefined && delete merged[key]);
|
|
159
|
+
const query = normalizeRange(merged);
|
|
160
|
+
const segment = name === '+schedules' ? 'schedules' : 'members';
|
|
161
|
+
const path = `/v1/attendance/scheduling/rules/${encodeURIComponent(ruleID)}/${segment}`;
|
|
162
|
+
if (preview(opts, cmd, 'GET', path, { query }))
|
|
163
|
+
return;
|
|
164
|
+
printImResult(opts.json, await imRequest('GET', path, { baseUrl: baseURL(opts, cmd), query }));
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
for (const [name, action] of [['+schedule-preview', 'preview'], ['+schedule-apply', 'apply']]) {
|
|
168
|
+
const command = parent.command(name).description(action === 'preview' ? '预览排班计划并获取短期令牌' : '使用预览令牌原子应用排班计划')
|
|
169
|
+
.requiredOption('--rule-id <id>').option('--mode <mode>', 'fill_missing|replace', 'fill_missing')
|
|
170
|
+
.option('--employee-ids <ids>', '逗号分隔员工 ID(范围简写)').option('--from-date <ymd>').option('--to-date <ymd>')
|
|
171
|
+
.option('--shift-template-id <id>').option('--remark <text>').option('--items <json>', '排班项目 JSON 数组')
|
|
172
|
+
.option('--items-file <path>', '从文件读取排班项目 JSON 数组');
|
|
173
|
+
if (action === 'apply')
|
|
174
|
+
command.requiredOption('--preview-token <token>').requiredOption('--client-request-id <id>');
|
|
175
|
+
attachSharedShortcutOptions(command).action(async (opts, cmd) => {
|
|
176
|
+
const body = normalizePlan(await mergeShortcutParams(await basePlan(opts), opts), action === 'apply');
|
|
177
|
+
const path = `/v1/attendance/scheduling/plans/${action}`;
|
|
178
|
+
if (preview(opts, cmd, 'POST', path, { body }))
|
|
179
|
+
return;
|
|
180
|
+
printImResult(opts.json, await imRequest('POST', path, { baseUrl: baseURL(opts, cmd), body }));
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { VertuError } from '../../core/errors.js';
|
|
2
|
+
import { imRequest, printImResult } from '../../core/im-client.js';
|
|
3
|
+
import { resolveVpsServiceUrl } from '../../core/sales-query-client.js';
|
|
4
|
+
import { attachSharedShortcutOptions, mergeShortcutParams, parseOptionalInt, } from '../shortcut-common.js';
|
|
5
|
+
import { attachAttendanceSchedulingShortcuts } from './scheduling.js';
|
|
6
|
+
function validDate(value, flag) {
|
|
7
|
+
const date = value?.trim() ?? '';
|
|
8
|
+
const parsed = new Date(`${date}T00:00:00.000Z`);
|
|
9
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(date) || Number.isNaN(parsed.getTime()) || parsed.toISOString().slice(0, 10) !== date) {
|
|
10
|
+
throw new VertuError(`${flag} must be a valid YYYY-MM-DD date.`);
|
|
11
|
+
}
|
|
12
|
+
return date;
|
|
13
|
+
}
|
|
14
|
+
function positiveInt(value, flag, fallback, max) {
|
|
15
|
+
const parsed = parseOptionalInt(value, flag) ?? fallback;
|
|
16
|
+
if (parsed <= 0 || (max !== undefined && parsed > max)) {
|
|
17
|
+
throw new VertuError(max === undefined ? `${flag} must be a positive integer.` : `${flag} must be between 1 and ${max}.`);
|
|
18
|
+
}
|
|
19
|
+
return parsed;
|
|
20
|
+
}
|
|
21
|
+
function queryText(value, flag) {
|
|
22
|
+
if (typeof value !== 'string')
|
|
23
|
+
throw new VertuError(`${flag} must be a string.`);
|
|
24
|
+
return value.trim();
|
|
25
|
+
}
|
|
26
|
+
function queryPositiveInt(value, flag, fallback, max) {
|
|
27
|
+
if (value === undefined || value === null)
|
|
28
|
+
return fallback;
|
|
29
|
+
if ((typeof value !== 'string' && typeof value !== 'number') || String(value).trim() === '') {
|
|
30
|
+
throw new VertuError(`${flag} must be a positive integer.`);
|
|
31
|
+
}
|
|
32
|
+
return positiveInt(String(value), flag, fallback, max);
|
|
33
|
+
}
|
|
34
|
+
export function normalizeAttendanceDailyQuery(input) {
|
|
35
|
+
const fromDate = validDate(queryText(input.from_date, 'from_date'), 'from_date');
|
|
36
|
+
const toDate = validDate(queryText(input.to_date, 'to_date'), 'to_date');
|
|
37
|
+
const days = Math.floor((Date.parse(`${toDate}T00:00:00Z`) - Date.parse(`${fromDate}T00:00:00Z`)) / 86_400_000) + 1;
|
|
38
|
+
if (days <= 0)
|
|
39
|
+
throw new VertuError('from_date cannot be later than to_date.');
|
|
40
|
+
if (days > 93)
|
|
41
|
+
throw new VertuError('The attendance daily query range cannot exceed 93 days.');
|
|
42
|
+
const scope = input.scope === undefined ? 'self' : queryText(input.scope, 'scope');
|
|
43
|
+
if (!['self', 'managed', 'employee', 'department', 'all'].includes(scope)) {
|
|
44
|
+
throw new VertuError('scope must be self, managed, employee, department, or all.');
|
|
45
|
+
}
|
|
46
|
+
const hasEmployeeId = input.employee_id !== undefined && input.employee_id !== null;
|
|
47
|
+
const hasDepartmentPath = input.department_path !== undefined && input.department_path !== null;
|
|
48
|
+
if ((scope === 'employee') !== hasEmployeeId) {
|
|
49
|
+
throw new VertuError('employee_id may only and must be provided when scope=employee.');
|
|
50
|
+
}
|
|
51
|
+
if ((scope === 'department') !== hasDepartmentPath) {
|
|
52
|
+
throw new VertuError('department_path may only and must be provided when scope=department.');
|
|
53
|
+
}
|
|
54
|
+
const query = {
|
|
55
|
+
...input,
|
|
56
|
+
from_date: fromDate,
|
|
57
|
+
to_date: toDate,
|
|
58
|
+
scope,
|
|
59
|
+
page: queryPositiveInt(input.page, 'page', 1),
|
|
60
|
+
page_size: queryPositiveInt(input.page_size, 'page_size', 50, 200),
|
|
61
|
+
};
|
|
62
|
+
if (hasEmployeeId)
|
|
63
|
+
query.employee_id = queryPositiveInt(input.employee_id, 'employee_id', 0);
|
|
64
|
+
if (hasDepartmentPath) {
|
|
65
|
+
const departmentPath = queryText(input.department_path, 'department_path');
|
|
66
|
+
if (!departmentPath)
|
|
67
|
+
throw new VertuError('department_path must not be empty.');
|
|
68
|
+
query.department_path = departmentPath;
|
|
69
|
+
}
|
|
70
|
+
return query;
|
|
71
|
+
}
|
|
72
|
+
export function buildAttendanceDailyQuery(opts) {
|
|
73
|
+
const fromDate = validDate(opts.fromDate, '--from-date');
|
|
74
|
+
const toDate = validDate(opts.toDate, '--to-date');
|
|
75
|
+
const days = Math.floor((Date.parse(`${toDate}T00:00:00Z`) - Date.parse(`${fromDate}T00:00:00Z`)) / 86_400_000) + 1;
|
|
76
|
+
if (days <= 0)
|
|
77
|
+
throw new VertuError('--from-date cannot be later than --to-date.');
|
|
78
|
+
if (days > 93)
|
|
79
|
+
throw new VertuError('The attendance daily query range cannot exceed 93 days.');
|
|
80
|
+
const selectors = [
|
|
81
|
+
opts.managedDepartments ? 'managed' : undefined,
|
|
82
|
+
opts.employeeId ? 'employee' : undefined,
|
|
83
|
+
opts.departmentPath?.trim() ? 'department' : undefined,
|
|
84
|
+
opts.all ? 'all' : undefined,
|
|
85
|
+
].filter((value) => value !== undefined);
|
|
86
|
+
if (selectors.length > 1) {
|
|
87
|
+
throw new VertuError('--managed-departments, --employee-id, --department-path, and --all are mutually exclusive.');
|
|
88
|
+
}
|
|
89
|
+
const scope = selectors[0] ?? 'self';
|
|
90
|
+
const query = {
|
|
91
|
+
from_date: fromDate,
|
|
92
|
+
to_date: toDate,
|
|
93
|
+
scope,
|
|
94
|
+
page: positiveInt(opts.page, '--page', 1),
|
|
95
|
+
page_size: positiveInt(opts.pageSize, '--page-size', 50, 200),
|
|
96
|
+
};
|
|
97
|
+
if (scope === 'employee')
|
|
98
|
+
query.employee_id = positiveInt(opts.employeeId, '--employee-id', 0);
|
|
99
|
+
if (scope === 'department')
|
|
100
|
+
query.department_path = opts.departmentPath?.trim();
|
|
101
|
+
return normalizeAttendanceDailyQuery(query);
|
|
102
|
+
}
|
|
103
|
+
export function attachAttendanceShortcuts(parent) {
|
|
104
|
+
attachSharedShortcutOptions(parent
|
|
105
|
+
.command('+daily')
|
|
106
|
+
.description('查询考勤日报及影响当日的申请;默认查询本人')
|
|
107
|
+
.requiredOption('--from-date <ymd>', '开始日期,YYYY-MM-DD(含)')
|
|
108
|
+
.requiredOption('--to-date <ymd>', '结束日期,YYYY-MM-DD(含)')
|
|
109
|
+
.option('--managed-departments', '查询当前有权管理的部门员工', false)
|
|
110
|
+
.option('--employee-id <id>', '查询指定员工;需要本人、部门或全员权限')
|
|
111
|
+
.option('--department-path <path>', '按部门完整路径查询该部门及子部门')
|
|
112
|
+
.option('--all', '查询全体在职考勤员工;需要全员权限', false)
|
|
113
|
+
.option('--page <n>', '页码', '1')
|
|
114
|
+
.option('--page-size <n>', '每页条数,最大 200', '50')).action(async (opts, cmd) => {
|
|
115
|
+
const query = normalizeAttendanceDailyQuery(await mergeShortcutParams(buildAttendanceDailyQuery(opts), opts));
|
|
116
|
+
const globals = cmd.optsWithGlobals();
|
|
117
|
+
const baseUrl = opts.baseUrl ?? globals.baseUrl;
|
|
118
|
+
if (opts.validateOnly === true || opts.dryRun === true) {
|
|
119
|
+
printImResult(opts.json, {
|
|
120
|
+
validateOnly: true,
|
|
121
|
+
method: 'GET',
|
|
122
|
+
endpoint: `${resolveVpsServiceUrl(baseUrl)}/v1/attendance/daily-reports`,
|
|
123
|
+
query,
|
|
124
|
+
});
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const result = await imRequest('GET', '/v1/attendance/daily-reports', { baseUrl, query });
|
|
128
|
+
printImResult(opts.json, result);
|
|
129
|
+
});
|
|
130
|
+
attachAttendanceSchedulingShortcuts(parent);
|
|
131
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { fetchVpsUserScopes, userHasVpsScope } from '../../core/vps-auth-scopes.js';
|
|
3
|
+
import { resolveVpsServiceUrl } from '../../core/sales-query-client.js';
|
|
4
|
+
export function buildAuthCheckCommand() {
|
|
5
|
+
return new Command('check')
|
|
6
|
+
.description('校验当前用户是否具备指定 scope(如 okr:structured:read)。exit 0 = 有权限,1 = 缺失。')
|
|
7
|
+
.argument('<scope>', 'resource:action,例如 execute:read')
|
|
8
|
+
.option('--base-url <url>', 'Vertu 服务地址')
|
|
9
|
+
.option('--json', '输出 JSON')
|
|
10
|
+
.action(async (scopeArg, opts, cmd) => {
|
|
11
|
+
const merged = cmd.optsWithGlobals();
|
|
12
|
+
const scope = scopeArg.trim();
|
|
13
|
+
const serviceUrl = resolveVpsServiceUrl(opts.baseUrl ?? merged.baseUrl);
|
|
14
|
+
const payload = await fetchVpsUserScopes(serviceUrl, scope);
|
|
15
|
+
const granted = payload.granted === true ||
|
|
16
|
+
userHasVpsScope(payload.userScopes ?? [], scope);
|
|
17
|
+
const out = {
|
|
18
|
+
appId: payload.appId,
|
|
19
|
+
scope,
|
|
20
|
+
granted,
|
|
21
|
+
login: payload.login,
|
|
22
|
+
};
|
|
23
|
+
if (opts.json) {
|
|
24
|
+
console.log(JSON.stringify(out, null, 2));
|
|
25
|
+
}
|
|
26
|
+
else if (granted) {
|
|
27
|
+
console.log(`scope 已授权:${scope}`);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
console.error(`scope 未授权:${scope}`);
|
|
31
|
+
}
|
|
32
|
+
process.exitCode = granted ? 0 : 1;
|
|
33
|
+
});
|
|
34
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { buildLoginCommand } from '../login.js';
|
|
3
|
+
import { buildLogoutCommand } from '../logout.js';
|
|
4
|
+
import { buildAuthCheckCommand } from './check.js';
|
|
5
|
+
import { buildAuthListCommand } from './list.js';
|
|
6
|
+
import { buildAuthScopesCommand } from './scopes.js';
|
|
7
|
+
import { buildAuthStatusCommand } from './status.js';
|
|
8
|
+
import { buildAuthTenantsCommand } from './tenants.js';
|
|
9
|
+
import { buildAuthUseCommand } from './use.js';
|
|
10
|
+
export function buildAuthCommand() {
|
|
11
|
+
const auth = new Command('auth').description('Sign-in, sessions, and API scopes (resource:action).');
|
|
12
|
+
auth.addCommand(buildLoginCommand());
|
|
13
|
+
auth.addCommand(buildLogoutCommand());
|
|
14
|
+
auth.addCommand(buildAuthStatusCommand());
|
|
15
|
+
auth.addCommand(buildAuthCheckCommand());
|
|
16
|
+
auth.addCommand(buildAuthScopesCommand());
|
|
17
|
+
auth.addCommand(buildAuthListCommand());
|
|
18
|
+
auth.addCommand(buildAuthTenantsCommand());
|
|
19
|
+
auth.addCommand(buildAuthUseCommand());
|
|
20
|
+
return auth;
|
|
21
|
+
}
|