vps-work 2.4.1 → 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/.well-known/skills/index.json +11 -0
- package/README.md +10 -0
- 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 +246 -44
- package/package.json +1 -1
- package/scripts/package-desktop-runtime.mjs +43 -0
- package/skills/README.md +1 -0
- package/skills/vertu-aisales/SKILL.md +18 -0
- package/skills/vertu-erp/SKILL.md +17 -0
- package/skills/vps-dealer-ops/SKILL.md +92 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import { VertuError } from '../core/errors.js';
|
|
2
|
+
import { imRequest } from '../core/im-client.js';
|
|
3
|
+
import { attachSharedShortcutOptions, parseOptionalInt, } from './shortcut-common.js';
|
|
4
|
+
function baseUrl(opts, cmd) {
|
|
5
|
+
const merged = cmd.optsWithGlobals();
|
|
6
|
+
return opts.baseUrl ?? merged.baseUrl;
|
|
7
|
+
}
|
|
8
|
+
function asRecord(value) {
|
|
9
|
+
return typeof value === 'object' && value !== null ? value : null;
|
|
10
|
+
}
|
|
11
|
+
function numberField(row, key, label) {
|
|
12
|
+
const value = row[key];
|
|
13
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
throw new VertuError(`组织部门数据无效:${label} 缺失或不是数字。`);
|
|
17
|
+
}
|
|
18
|
+
function stringField(row, key, label) {
|
|
19
|
+
const value = row[key];
|
|
20
|
+
if (typeof value === 'string') {
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
throw new VertuError(`组织部门数据无效:${label} 缺失或不是字符串。`);
|
|
24
|
+
}
|
|
25
|
+
function nullableNumberField(row, key) {
|
|
26
|
+
const value = row[key];
|
|
27
|
+
if (value === null || value === undefined) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
throw new VertuError(`组织部门数据无效:${key} 不是数字。`);
|
|
34
|
+
}
|
|
35
|
+
function nullableStringField(row, key) {
|
|
36
|
+
const value = row[key];
|
|
37
|
+
if (value === null || value === undefined) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
if (typeof value === 'string') {
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
throw new VertuError(`组织部门数据无效:${key} 不是字符串。`);
|
|
44
|
+
}
|
|
45
|
+
export function parseOrgDepartments(payload) {
|
|
46
|
+
const data = asRecord(payload);
|
|
47
|
+
const rows = Array.isArray(data?.departments) ? data.departments : null;
|
|
48
|
+
if (!rows) {
|
|
49
|
+
throw new VertuError('Vertu 服务未返回 departments 数组。');
|
|
50
|
+
}
|
|
51
|
+
return rows.map((item) => {
|
|
52
|
+
const row = asRecord(item);
|
|
53
|
+
if (!row) {
|
|
54
|
+
throw new VertuError('组织部门数据无效:部门记录不是对象。');
|
|
55
|
+
}
|
|
56
|
+
const activeValue = row.active;
|
|
57
|
+
return {
|
|
58
|
+
department_id: numberField(row, 'department_id', 'department_id'),
|
|
59
|
+
parent_id: nullableNumberField(row, 'parent_id'),
|
|
60
|
+
name: stringField(row, 'name', 'name'),
|
|
61
|
+
complete_name: nullableStringField(row, 'complete_name'),
|
|
62
|
+
active: typeof activeValue === 'boolean' ? activeValue : true,
|
|
63
|
+
employee_count: numberField(row, 'employee_count', 'employee_count'),
|
|
64
|
+
};
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
export function parseOrgUsers(payload) {
|
|
68
|
+
const data = asRecord(payload);
|
|
69
|
+
const rows = Array.isArray(data?.users) ? data.users : null;
|
|
70
|
+
if (!rows) {
|
|
71
|
+
throw new VertuError('Vertu 服务未返回 users 数组。');
|
|
72
|
+
}
|
|
73
|
+
return rows.map((item) => {
|
|
74
|
+
const row = asRecord(item);
|
|
75
|
+
if (!row) {
|
|
76
|
+
throw new VertuError('组织人员数据无效:人员记录不是对象。');
|
|
77
|
+
}
|
|
78
|
+
const activeValue = row.is_active;
|
|
79
|
+
return {
|
|
80
|
+
user_id: numberField(row, 'user_id', 'user_id'),
|
|
81
|
+
login: stringField(row, 'login', 'login'),
|
|
82
|
+
employee_id: nullableNumberField(row, 'employee_id'),
|
|
83
|
+
employee_name: nullableStringField(row, 'employee_name'),
|
|
84
|
+
barcode: nullableStringField(row, 'barcode'),
|
|
85
|
+
job_title: nullableStringField(row, 'job_title'),
|
|
86
|
+
manager_user_id: nullableNumberField(row, 'manager_user_id'),
|
|
87
|
+
manager_name: nullableStringField(row, 'manager_name'),
|
|
88
|
+
department_id: nullableNumberField(row, 'department_id'),
|
|
89
|
+
department_name: nullableStringField(row, 'department_name'),
|
|
90
|
+
department_path: nullableStringField(row, 'department_path'),
|
|
91
|
+
employment_status: nullableStringField(row, 'employment_status'),
|
|
92
|
+
is_active: typeof activeValue === 'boolean' ? activeValue : true,
|
|
93
|
+
};
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
export function resolveOrgDepartmentPath(departments, input) {
|
|
97
|
+
const query = input.trim().toLocaleLowerCase();
|
|
98
|
+
if (!query) {
|
|
99
|
+
throw new VertuError('--department cannot be empty.');
|
|
100
|
+
}
|
|
101
|
+
const pathMatch = departments.find((department) => department.complete_name?.trim().toLocaleLowerCase() === query);
|
|
102
|
+
if (pathMatch) {
|
|
103
|
+
return pathMatch.complete_name?.trim() || pathMatch.name.trim();
|
|
104
|
+
}
|
|
105
|
+
const nameMatches = departments.filter((department) => department.name.trim().toLocaleLowerCase() === query);
|
|
106
|
+
if (nameMatches.length === 1) {
|
|
107
|
+
return nameMatches[0]?.complete_name?.trim() || nameMatches[0]?.name.trim() || input.trim();
|
|
108
|
+
}
|
|
109
|
+
if (nameMatches.length > 1) {
|
|
110
|
+
const paths = nameMatches.map((department) => department.complete_name || department.name).join('、');
|
|
111
|
+
throw new VertuError(`部门名称不唯一,请使用完整路径:${paths}`);
|
|
112
|
+
}
|
|
113
|
+
throw new VertuError(`未找到部门:${input.trim()}`);
|
|
114
|
+
}
|
|
115
|
+
function displayName(department) {
|
|
116
|
+
const name = department.name.trim();
|
|
117
|
+
if (name) {
|
|
118
|
+
return name;
|
|
119
|
+
}
|
|
120
|
+
return department.complete_name?.trim() || `部门 ${department.department_id}`;
|
|
121
|
+
}
|
|
122
|
+
function sortByDisplayName(a, b) {
|
|
123
|
+
return displayName(a).localeCompare(displayName(b), 'zh-CN');
|
|
124
|
+
}
|
|
125
|
+
export function buildOrgDepartmentForest(departments) {
|
|
126
|
+
const nodes = new Map();
|
|
127
|
+
for (const department of departments) {
|
|
128
|
+
nodes.set(department.department_id, {
|
|
129
|
+
...department,
|
|
130
|
+
level: 1,
|
|
131
|
+
subtree_employee_count: department.employee_count,
|
|
132
|
+
children: [],
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
const roots = [];
|
|
136
|
+
for (const node of nodes.values()) {
|
|
137
|
+
const parent = node.parent_id === null ? undefined : nodes.get(node.parent_id);
|
|
138
|
+
if (parent) {
|
|
139
|
+
parent.children.push(node);
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
roots.push(node);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const fill = (node, level) => {
|
|
146
|
+
node.level = level;
|
|
147
|
+
node.children.sort(sortByDisplayName);
|
|
148
|
+
let total = node.employee_count;
|
|
149
|
+
for (const child of node.children) {
|
|
150
|
+
total += fill(child, level + 1);
|
|
151
|
+
}
|
|
152
|
+
node.subtree_employee_count = total;
|
|
153
|
+
return total;
|
|
154
|
+
};
|
|
155
|
+
roots.sort(sortByDisplayName);
|
|
156
|
+
for (const root of roots) {
|
|
157
|
+
fill(root, 1);
|
|
158
|
+
}
|
|
159
|
+
return roots;
|
|
160
|
+
}
|
|
161
|
+
function cloneNode(node) {
|
|
162
|
+
return {
|
|
163
|
+
...node,
|
|
164
|
+
children: node.children.map(cloneNode),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function matchesQuery(node, query) {
|
|
168
|
+
const haystack = [
|
|
169
|
+
node.department_id,
|
|
170
|
+
node.parent_id,
|
|
171
|
+
node.name,
|
|
172
|
+
node.complete_name,
|
|
173
|
+
node.employee_count,
|
|
174
|
+
node.subtree_employee_count,
|
|
175
|
+
]
|
|
176
|
+
.join(' ')
|
|
177
|
+
.toLowerCase();
|
|
178
|
+
return haystack.includes(query);
|
|
179
|
+
}
|
|
180
|
+
function filterTree(nodes, query) {
|
|
181
|
+
if (!query) {
|
|
182
|
+
return nodes.map(cloneNode);
|
|
183
|
+
}
|
|
184
|
+
const filtered = [];
|
|
185
|
+
for (const node of nodes) {
|
|
186
|
+
const children = filterTree(node.children, query);
|
|
187
|
+
if (matchesQuery(node, query)) {
|
|
188
|
+
filtered.push(cloneNode(node));
|
|
189
|
+
}
|
|
190
|
+
else if (children.length > 0) {
|
|
191
|
+
filtered.push({ ...node, children });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return filtered;
|
|
195
|
+
}
|
|
196
|
+
function limitDepth(nodes, maxDepth) {
|
|
197
|
+
return nodes.map((node) => ({
|
|
198
|
+
...node,
|
|
199
|
+
children: maxDepth !== null && node.level >= maxDepth ? [] : limitDepth(node.children, maxDepth),
|
|
200
|
+
}));
|
|
201
|
+
}
|
|
202
|
+
function flattenTree(nodes) {
|
|
203
|
+
const rows = [];
|
|
204
|
+
const walk = (items) => {
|
|
205
|
+
for (const item of items) {
|
|
206
|
+
const { children: _children, ...row } = item;
|
|
207
|
+
rows.push(row);
|
|
208
|
+
walk(_children);
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
walk(nodes);
|
|
212
|
+
return rows;
|
|
213
|
+
}
|
|
214
|
+
function maxDepthValue(opts) {
|
|
215
|
+
const raw = opts.maxDepth ?? '2';
|
|
216
|
+
const parsed = parseOptionalInt(raw, '--max-depth') ?? 2;
|
|
217
|
+
if (parsed < 0) {
|
|
218
|
+
throw new VertuError('--max-depth must be >= 0.');
|
|
219
|
+
}
|
|
220
|
+
return parsed === 0 ? null : parsed;
|
|
221
|
+
}
|
|
222
|
+
export function buildOrgStructureOutput(departments, opts = {}) {
|
|
223
|
+
const maxDepth = maxDepthValue(opts);
|
|
224
|
+
const query = opts.query?.trim().toLowerCase() || '';
|
|
225
|
+
const forest = limitDepth(filterTree(buildOrgDepartmentForest(departments), query), maxDepth);
|
|
226
|
+
if (opts.flat) {
|
|
227
|
+
const rows = flattenTree(forest);
|
|
228
|
+
return {
|
|
229
|
+
ok: true,
|
|
230
|
+
count: rows.length,
|
|
231
|
+
query: query || null,
|
|
232
|
+
max_depth: maxDepth,
|
|
233
|
+
departments: rows,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
const rows = flattenTree(forest);
|
|
237
|
+
return {
|
|
238
|
+
ok: true,
|
|
239
|
+
count: rows.length,
|
|
240
|
+
query: query || null,
|
|
241
|
+
max_depth: maxDepth,
|
|
242
|
+
tree: forest,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
function printTreePlain(nodes) {
|
|
246
|
+
const lines = [];
|
|
247
|
+
const walk = (items) => {
|
|
248
|
+
for (const node of items) {
|
|
249
|
+
const indent = ' '.repeat(Math.max(0, node.level - 1));
|
|
250
|
+
const staff = `${node.employee_count}/${node.subtree_employee_count}`;
|
|
251
|
+
lines.push(`${indent}- ${displayName(node)} (#${node.department_id}, staff ${staff})`);
|
|
252
|
+
walk(node.children);
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
walk(nodes);
|
|
256
|
+
console.log(lines.join('\n'));
|
|
257
|
+
}
|
|
258
|
+
function printOrgStructure(opts, out) {
|
|
259
|
+
if (opts.json !== false) {
|
|
260
|
+
console.log(JSON.stringify(out, null, 2));
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if ('tree' in out) {
|
|
264
|
+
printTreePlain(out.tree);
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
for (const row of out.departments) {
|
|
268
|
+
console.log(`${' '.repeat(Math.max(0, row.level - 1))}${row.complete_name || row.name} (#${row.department_id})`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
printUsersPlain(out.users);
|
|
272
|
+
}
|
|
273
|
+
function printUsersPlain(users) {
|
|
274
|
+
if (!users) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
console.log(`\n员工名单(${users.length} 人)`);
|
|
278
|
+
const byDepartment = new Map();
|
|
279
|
+
for (const user of users) {
|
|
280
|
+
const path = user.department_path || user.department_name || '未分配部门';
|
|
281
|
+
const rows = byDepartment.get(path) ?? [];
|
|
282
|
+
rows.push(user);
|
|
283
|
+
byDepartment.set(path, rows);
|
|
284
|
+
}
|
|
285
|
+
for (const [path, rows] of [...byDepartment.entries()].sort(([a], [b]) => a.localeCompare(b, 'zh-CN'))) {
|
|
286
|
+
const names = rows
|
|
287
|
+
.map((user) => {
|
|
288
|
+
const name = user.employee_name || user.login;
|
|
289
|
+
return user.job_title ? `${name}(${user.job_title})` : name;
|
|
290
|
+
})
|
|
291
|
+
.sort((a, b) => a.localeCompare(b, 'zh-CN'));
|
|
292
|
+
console.log(`- ${path}(${rows.length}):${names.join('、')}`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
async function fetchOrgUsers(baseUrlValue, departmentPath) {
|
|
296
|
+
const users = new Map();
|
|
297
|
+
let page = 1;
|
|
298
|
+
let hasMore = true;
|
|
299
|
+
while (hasMore) {
|
|
300
|
+
const data = await imRequest('GET', '/v1/im/org/users', {
|
|
301
|
+
baseUrl: baseUrlValue,
|
|
302
|
+
query: {
|
|
303
|
+
department_path: departmentPath,
|
|
304
|
+
include_children: true,
|
|
305
|
+
page,
|
|
306
|
+
page_size: 500,
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
for (const user of parseOrgUsers(data)) {
|
|
310
|
+
users.set(user.user_id, user);
|
|
311
|
+
}
|
|
312
|
+
const pagination = asRecord(asRecord(data)?.pagination);
|
|
313
|
+
hasMore = pagination?.has_more === true;
|
|
314
|
+
page += 1;
|
|
315
|
+
}
|
|
316
|
+
return [...users.values()];
|
|
317
|
+
}
|
|
318
|
+
export function attachOrgStructureShortcut(parent, commandName, description) {
|
|
319
|
+
attachSharedShortcutOptions(parent
|
|
320
|
+
.command(commandName)
|
|
321
|
+
.description(description)
|
|
322
|
+
.option('--query <text>', '部门 ID / 名称 / 完整路径关键字')
|
|
323
|
+
.option('--department <path>', '限定部门子树;支持完整路径或唯一部门名称')
|
|
324
|
+
.option('--max-depth <n>', '最大部门层级;默认 2(一级/二级),0 表示不限', '2')
|
|
325
|
+
.option('--flat', '输出扁平部门列表而不是树', false)
|
|
326
|
+
.option('--include-users', '同时返回所选部门及下级部门的在职员工名单', false)).action(async (opts, cmd) => {
|
|
327
|
+
if (opts.query?.trim() && opts.department?.trim()) {
|
|
328
|
+
throw new VertuError('--query and --department cannot be used together.');
|
|
329
|
+
}
|
|
330
|
+
const serviceBaseUrl = baseUrl(opts, cmd);
|
|
331
|
+
const data = await imRequest('GET', '/v1/im/org/departments', { baseUrl: serviceBaseUrl });
|
|
332
|
+
const departments = parseOrgDepartments(data);
|
|
333
|
+
const departmentPath = opts.department
|
|
334
|
+
? resolveOrgDepartmentPath(departments, opts.department)
|
|
335
|
+
: undefined;
|
|
336
|
+
const out = buildOrgStructureOutput(departments, {
|
|
337
|
+
...opts,
|
|
338
|
+
query: departmentPath ?? opts.query,
|
|
339
|
+
});
|
|
340
|
+
if (opts.includeUsers) {
|
|
341
|
+
const users = await fetchOrgUsers(serviceBaseUrl, departmentPath);
|
|
342
|
+
out.user_count = users.length;
|
|
343
|
+
out.users = users;
|
|
344
|
+
}
|
|
345
|
+
printOrgStructure(opts, out);
|
|
346
|
+
});
|
|
347
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { VertuError } from '../../core/errors.js';
|
|
3
|
+
import { imRequest, printImResult } from '../../core/im-client.js';
|
|
4
|
+
import { readStdinText, readUtf8File } from '../../utils/read-text.js';
|
|
5
|
+
const HEX_COLOR = /^#[0-9a-f]{6}$/i;
|
|
6
|
+
function required(value, flag, max) {
|
|
7
|
+
const normalized = value?.trim();
|
|
8
|
+
if (!normalized)
|
|
9
|
+
throw new VertuError(`${flag} 为必填。`);
|
|
10
|
+
if (normalized.length > max)
|
|
11
|
+
throw new VertuError(`${flag} 最多 ${max} 字。`);
|
|
12
|
+
return normalized;
|
|
13
|
+
}
|
|
14
|
+
function optionalColor(value, flag) {
|
|
15
|
+
const normalized = value?.trim();
|
|
16
|
+
if (!normalized)
|
|
17
|
+
return undefined;
|
|
18
|
+
if (!HEX_COLOR.test(normalized)) {
|
|
19
|
+
throw new VertuError(`${flag} 必须是 #RRGGBB。`);
|
|
20
|
+
}
|
|
21
|
+
return normalized.toUpperCase();
|
|
22
|
+
}
|
|
23
|
+
function numberInRange(value, flag, fallback, min, max) {
|
|
24
|
+
const parsed = value === undefined ? fallback : Number(value);
|
|
25
|
+
if (!Number.isFinite(parsed) || parsed < min || parsed > max) {
|
|
26
|
+
throw new VertuError(`${flag} 必须在 ${min}–${max} 之间。`);
|
|
27
|
+
}
|
|
28
|
+
return parsed;
|
|
29
|
+
}
|
|
30
|
+
function optionalHttpsUrl(value, flag) {
|
|
31
|
+
const normalized = value?.trim();
|
|
32
|
+
if (!normalized)
|
|
33
|
+
return undefined;
|
|
34
|
+
try {
|
|
35
|
+
const url = new URL(normalized);
|
|
36
|
+
if (url.protocol !== 'https:')
|
|
37
|
+
throw new Error('protocol');
|
|
38
|
+
return url.toString();
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
throw new VertuError(`${flag} 必须是 HTTPS URL。`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export function buildPopupPayload(opts, content) {
|
|
45
|
+
if (opts.userId !== undefined && opts.login !== undefined) {
|
|
46
|
+
throw new VertuError('--user-id 和 --login 不能同时使用。');
|
|
47
|
+
}
|
|
48
|
+
let targetUserId;
|
|
49
|
+
if (opts.userId !== undefined) {
|
|
50
|
+
targetUserId = Number(opts.userId);
|
|
51
|
+
if (!Number.isSafeInteger(targetUserId) || targetUserId <= 0) {
|
|
52
|
+
throw new VertuError('--user-id 必须是正整数。');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const targetLogin = opts.login?.trim();
|
|
56
|
+
if (opts.login !== undefined && !targetLogin) {
|
|
57
|
+
throw new VertuError('--login 不能为空。');
|
|
58
|
+
}
|
|
59
|
+
const gradientStart = optionalColor(opts.gradientStart, '--gradient-start');
|
|
60
|
+
const gradientEnd = optionalColor(opts.gradientEnd, '--gradient-end');
|
|
61
|
+
if ((gradientStart === undefined) !== (gradientEnd === undefined)) {
|
|
62
|
+
throw new VertuError('--gradient-start 和 --gradient-end 必须同时提供。');
|
|
63
|
+
}
|
|
64
|
+
const buttonStyle = opts.buttonStyle?.trim().toLowerCase() || 'filled';
|
|
65
|
+
if (buttonStyle !== 'filled' && buttonStyle !== 'outlined') {
|
|
66
|
+
throw new VertuError('--button-style 只支持 filled 或 outlined。');
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
...(targetUserId === undefined ? {} : { target_user_id: targetUserId }),
|
|
70
|
+
...(targetLogin ? { target_login: targetLogin } : {}),
|
|
71
|
+
title: required(opts.title, '--title', 120),
|
|
72
|
+
content: required(content, '--content', 4000),
|
|
73
|
+
presentation: {
|
|
74
|
+
height_factor: numberInRange(opts.height, '--height', 0.62, 0.3, 0.95),
|
|
75
|
+
background_color: optionalColor(opts.backgroundColor, '--background-color'),
|
|
76
|
+
gradient_start: gradientStart,
|
|
77
|
+
gradient_end: gradientEnd,
|
|
78
|
+
background_image_url: optionalHttpsUrl(opts.backgroundImageUrl, '--background-image-url'),
|
|
79
|
+
title_color: optionalColor(opts.titleColor, '--title-color'),
|
|
80
|
+
content_color: optionalColor(opts.contentColor, '--content-color'),
|
|
81
|
+
},
|
|
82
|
+
button: {
|
|
83
|
+
text: opts.buttonText?.trim() || '我知道了',
|
|
84
|
+
style: buttonStyle,
|
|
85
|
+
background_color: optionalColor(opts.buttonBackgroundColor, '--button-background-color'),
|
|
86
|
+
text_color: optionalColor(opts.buttonTextColor, '--button-text-color'),
|
|
87
|
+
action_url: optionalHttpsUrl(opts.buttonUrl, '--button-url'),
|
|
88
|
+
},
|
|
89
|
+
expires_in_minutes: Math.round(numberInRange(opts.expiresInMinutes, '--expires-in-minutes', 1440, 1, 10080)),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
async function resolveContent(opts) {
|
|
93
|
+
if (opts.content !== undefined && opts.contentFile !== undefined) {
|
|
94
|
+
throw new VertuError('--content 和 --content-file 不能同时使用。');
|
|
95
|
+
}
|
|
96
|
+
if (opts.contentFile === '-')
|
|
97
|
+
return readStdinText();
|
|
98
|
+
if (opts.contentFile)
|
|
99
|
+
return readUtf8File(opts.contentFile);
|
|
100
|
+
return opts.content ?? '';
|
|
101
|
+
}
|
|
102
|
+
export function buildPopupCommand() {
|
|
103
|
+
const popup = new Command('popup').description('Agent 主动向 VPS 手机端投递一次性弹窗。');
|
|
104
|
+
popup
|
|
105
|
+
.command('+send')
|
|
106
|
+
.description('投递声明式安全弹窗;前台约 0–10 秒展示,离线时下次打开展示')
|
|
107
|
+
.option('--user-id <id>', '目标用户 ID;向他人推送需要管理员权限')
|
|
108
|
+
.option('--login <login>', '目标用户登录名;向他人推送需要管理员权限')
|
|
109
|
+
.requiredOption('--title <text>', '弹窗标题,最多 120 字')
|
|
110
|
+
.option('--content <text>', '弹窗正文,最多 4000 字')
|
|
111
|
+
.option('--content-file <path>', '从 UTF-8 文件读取正文;- 表示 stdin')
|
|
112
|
+
.option('--height <ratio>', '屏幕高度比例,0.3–0.95', '0.62')
|
|
113
|
+
.option('--background-color <#RRGGBB>', '纯色背景')
|
|
114
|
+
.option('--gradient-start <#RRGGBB>', '顶部渐变色,须与 --gradient-end 同时提供')
|
|
115
|
+
.option('--gradient-end <#RRGGBB>', '底部渐变色,须与 --gradient-start 同时提供')
|
|
116
|
+
.option('--background-image-url <https-url>', 'HTTPS 背景图地址')
|
|
117
|
+
.option('--title-color <#RRGGBB>', '标题颜色')
|
|
118
|
+
.option('--content-color <#RRGGBB>', '正文颜色')
|
|
119
|
+
.option('--button-text <text>', '按钮文字', '我知道了')
|
|
120
|
+
.option('--button-style <style>', 'filled 或 outlined', 'filled')
|
|
121
|
+
.option('--button-background-color <#RRGGBB>', '按钮背景/描边颜色')
|
|
122
|
+
.option('--button-text-color <#RRGGBB>', '按钮文字颜色')
|
|
123
|
+
.option('--button-url <https-url>', '点击按钮后打开的 HTTPS URL')
|
|
124
|
+
.option('--expires-in-minutes <number>', '未领取弹窗的有效分钟数,1–10080', '1440')
|
|
125
|
+
.option('--base-url <url>', 'Vertu 服务地址')
|
|
126
|
+
.option('--dry-run', '只打印请求,不投递')
|
|
127
|
+
.option('--no-json', '输出紧凑 JSON')
|
|
128
|
+
.action(async (opts, command) => {
|
|
129
|
+
const merged = command.optsWithGlobals();
|
|
130
|
+
const payload = buildPopupPayload(opts, await resolveContent(opts));
|
|
131
|
+
if (opts.dryRun) {
|
|
132
|
+
printImResult(opts.json, {
|
|
133
|
+
dry_run: true,
|
|
134
|
+
method: 'POST',
|
|
135
|
+
endpoint: '/v1/agent-popups',
|
|
136
|
+
body: payload,
|
|
137
|
+
});
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const result = await imRequest('POST', '/v1/agent-popups', {
|
|
141
|
+
baseUrl: opts.baseUrl ?? merged.baseUrl,
|
|
142
|
+
body: payload,
|
|
143
|
+
});
|
|
144
|
+
printImResult(opts.json, result);
|
|
145
|
+
});
|
|
146
|
+
return popup;
|
|
147
|
+
}
|