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,266 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, resolve } from 'node:path';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { VertuError } from '../../core/errors.js';
|
|
5
|
+
import { vpsServiceGet } from '../../core/sales-query-client.js';
|
|
6
|
+
import { attachSharedShortcutOptions, mergeShortcutParams, resolveShortcutVpsServiceUrl, } from '../shortcut-common.js';
|
|
7
|
+
const POSITION_DOCS_PATH = '/v1/workbench/position-docs';
|
|
8
|
+
const summarySchema = z.object({
|
|
9
|
+
id: z.number().int().positive(),
|
|
10
|
+
title: z.string(),
|
|
11
|
+
updated_at: z.string().nullable(),
|
|
12
|
+
}).passthrough();
|
|
13
|
+
const employeeSchema = z.object({
|
|
14
|
+
user_id: z.number().int().positive(),
|
|
15
|
+
barcode: z.string(),
|
|
16
|
+
name: z.string(),
|
|
17
|
+
position: z.string(),
|
|
18
|
+
department_id: z.number().int().nullable(),
|
|
19
|
+
department_name: z.string(),
|
|
20
|
+
department_path: z.string(),
|
|
21
|
+
profile_doc: summarySchema.nullable(),
|
|
22
|
+
sop_doc: summarySchema.nullable(),
|
|
23
|
+
});
|
|
24
|
+
const documentSchema = summarySchema.extend({
|
|
25
|
+
odoo_user_id: z.number().int().positive(),
|
|
26
|
+
doc_type: z.enum(['profile', 'sop']),
|
|
27
|
+
content_md: z.string().nullable().optional(),
|
|
28
|
+
content_json: z.unknown(),
|
|
29
|
+
}).passthrough();
|
|
30
|
+
const responseSchema = z.object({
|
|
31
|
+
ok: z.literal(true),
|
|
32
|
+
can_manage_all: z.boolean(),
|
|
33
|
+
viewer_user_id: z.number().int().positive(),
|
|
34
|
+
selected_user_id: z.number().int().positive(),
|
|
35
|
+
selected_employee: employeeSchema.nullable(),
|
|
36
|
+
employees: z.array(employeeSchema),
|
|
37
|
+
counts: z.object({ employees: z.number().int().nonnegative() }),
|
|
38
|
+
docs: z.object({ profile: documentSchema.nullable(), sop: documentSchema.nullable() }),
|
|
39
|
+
});
|
|
40
|
+
const optionalText = z.string().trim().min(1).optional();
|
|
41
|
+
const positiveInteger = z.union([z.string().regex(/^[1-9]\d*$/).transform(Number), z.number()])
|
|
42
|
+
.pipe(z.number().int().positive().max(Number.MAX_SAFE_INTEGER));
|
|
43
|
+
const querySchema = z.object({
|
|
44
|
+
department: optionalText,
|
|
45
|
+
include_children: z.boolean().default(true),
|
|
46
|
+
job: optionalText,
|
|
47
|
+
name: optionalText,
|
|
48
|
+
barcode: optionalText,
|
|
49
|
+
user_id: positiveInteger.optional(),
|
|
50
|
+
my: z.boolean().default(false),
|
|
51
|
+
all: z.boolean().default(false),
|
|
52
|
+
page: positiveInteger.default(1),
|
|
53
|
+
limit: positiveInteger.pipe(z.number().max(100)).default(20),
|
|
54
|
+
doc_type: z.enum(['profile', 'sop', 'all']).default('profile'),
|
|
55
|
+
summary_only: z.boolean().default(false),
|
|
56
|
+
}).strict();
|
|
57
|
+
export function normalizeJobResponsibilitiesQuery(input) {
|
|
58
|
+
const result = querySchema.safeParse(input);
|
|
59
|
+
if (!result.success) {
|
|
60
|
+
const issue = result.error.issues[0];
|
|
61
|
+
throw new VertuError(`岗位职责查询参数无效:${issue?.path.join('.') || 'params'} ${issue?.message ?? ''}`);
|
|
62
|
+
}
|
|
63
|
+
const query = result.data;
|
|
64
|
+
const hasFilter = [query.department, query.job, query.name, query.barcode, query.user_id].some(v => v !== undefined);
|
|
65
|
+
if (query.my && hasFilter)
|
|
66
|
+
throw new VertuError('--my 不能与部门、岗位或其他个人筛选同时使用。');
|
|
67
|
+
if (query.all && query.page !== 1)
|
|
68
|
+
throw new VertuError('--all 不能与大于 1 的 --page 同时使用。');
|
|
69
|
+
if (!Number.isSafeInteger((query.page - 1) * query.limit))
|
|
70
|
+
throw new VertuError('--page 超出有效范围。');
|
|
71
|
+
if (query.department && !departmentParts(query.department).length)
|
|
72
|
+
throw new VertuError('--department 不能为空部门路径。');
|
|
73
|
+
if (!hasFilter && !query.all)
|
|
74
|
+
query.my = true;
|
|
75
|
+
return query;
|
|
76
|
+
}
|
|
77
|
+
function departmentParts(value) {
|
|
78
|
+
return value.split('/').map(v => v.trim().toLocaleLowerCase()).filter(Boolean);
|
|
79
|
+
}
|
|
80
|
+
function matchesEmployee(employee, query, viewerUserId) {
|
|
81
|
+
if (query.my && employee.user_id !== viewerUserId)
|
|
82
|
+
return false;
|
|
83
|
+
if (query.user_id !== undefined && employee.user_id !== query.user_id)
|
|
84
|
+
return false;
|
|
85
|
+
if (query.name && employee.name.trim() !== query.name)
|
|
86
|
+
return false;
|
|
87
|
+
if (query.barcode && employee.barcode.trim() !== query.barcode)
|
|
88
|
+
return false;
|
|
89
|
+
if (query.job && !employee.position.toLocaleLowerCase().includes(query.job.toLocaleLowerCase()))
|
|
90
|
+
return false;
|
|
91
|
+
if (query.department) {
|
|
92
|
+
const wanted = departmentParts(query.department);
|
|
93
|
+
const actual = departmentParts(employee.department_path || employee.department_name);
|
|
94
|
+
// Match complete path segments, so AI业务 never includes sibling AI业务中台.
|
|
95
|
+
const found = actual.some((_, start) => wanted.every((part, offset) => part === actual[start + offset])
|
|
96
|
+
&& (query.include_children || start + wanted.length === actual.length));
|
|
97
|
+
if (!found)
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
function parseResponse(payload) {
|
|
103
|
+
const parsed = responseSchema.safeParse(payload);
|
|
104
|
+
if (!parsed.success)
|
|
105
|
+
throw new VertuError('岗位文档接口返回的数据结构无效,未输出查询结果。');
|
|
106
|
+
const data = parsed.data;
|
|
107
|
+
if (data.employees.length >= 5000 || data.counts.employees !== data.employees.length) {
|
|
108
|
+
throw new VertuError('岗位人员列表可能被截断,无法确认完整查询范围。');
|
|
109
|
+
}
|
|
110
|
+
if (new Set(data.employees.map(e => e.user_id)).size !== data.employees.length) {
|
|
111
|
+
throw new VertuError('岗位人员列表存在重复用户编号。');
|
|
112
|
+
}
|
|
113
|
+
if (!data.can_manage_all && data.employees.some(e => e.user_id !== data.viewer_user_id)) {
|
|
114
|
+
throw new VertuError('岗位人员列表与当前会话的本人范围不一致。');
|
|
115
|
+
}
|
|
116
|
+
return data;
|
|
117
|
+
}
|
|
118
|
+
function checkedDocuments(data, initial, employee) {
|
|
119
|
+
const selected = data.selected_employee;
|
|
120
|
+
if (data.viewer_user_id !== initial.viewer_user_id || data.can_manage_all !== initial.can_manage_all
|
|
121
|
+
|| data.selected_user_id !== employee.user_id || selected?.user_id !== employee.user_id) {
|
|
122
|
+
throw new VertuError(`岗位文档归属不一致(用户 ${employee.user_id}),已停止查询。`);
|
|
123
|
+
}
|
|
124
|
+
for (const key of ['name', 'barcode', 'position', 'department_id', 'department_path']) {
|
|
125
|
+
if (selected[key] !== employee[key])
|
|
126
|
+
throw new VertuError('查询期间人员档案发生变化,请重试。');
|
|
127
|
+
}
|
|
128
|
+
for (const type of ['profile', 'sop']) {
|
|
129
|
+
const doc = data.docs[type];
|
|
130
|
+
const summary = employee[`${type}_doc`];
|
|
131
|
+
if ((doc?.id ?? null) !== (summary?.id ?? null)
|
|
132
|
+
|| (doc?.updated_at ?? null) !== (summary?.updated_at ?? null)
|
|
133
|
+
|| (doc?.id ?? null) !== (selected[`${type}_doc`]?.id ?? null)) {
|
|
134
|
+
throw new VertuError(`查询期间岗位文档发生变化(用户 ${employee.user_id}),请重试。`);
|
|
135
|
+
}
|
|
136
|
+
if (doc && (doc.odoo_user_id !== employee.user_id || doc.doc_type !== type)) {
|
|
137
|
+
throw new VertuError(`岗位文档归属不一致(用户 ${employee.user_id}),已停止查询。`);
|
|
138
|
+
}
|
|
139
|
+
if (doc && !doc.content_md?.trim() && doc.content_json == null) {
|
|
140
|
+
throw new VertuError(`岗位文档 ${doc.id} 内容为空,无法导出完整职责。`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return data.docs;
|
|
144
|
+
}
|
|
145
|
+
export async function queryJobResponsibilities(query, baseUrl) {
|
|
146
|
+
const fetchDocs = async (userId) => parseResponse(await vpsServiceGet(POSITION_DOCS_PATH, { odoo_user_id: userId }, baseUrl, '岗位职责查询'));
|
|
147
|
+
const initial = await fetchDocs();
|
|
148
|
+
const matches = initial.employees.filter(e => matchesEmployee(e, query, initial.viewer_user_id))
|
|
149
|
+
.sort((a, b) => a.department_path.localeCompare(b.department_path, 'zh-CN')
|
|
150
|
+
|| a.position.localeCompare(b.position, 'zh-CN') || a.name.localeCompare(b.name, 'zh-CN') || a.user_id - b.user_id);
|
|
151
|
+
if (query.name && matches.length > 1) {
|
|
152
|
+
throw new VertuError(`姓名“${query.name}”匹配到 ${matches.length} 人,请用 --user-id、--barcode 或部门条件区分。`);
|
|
153
|
+
}
|
|
154
|
+
if (query.barcode && matches.length > 1)
|
|
155
|
+
throw new VertuError('工号匹配到多个人员,请用 --user-id 区分。');
|
|
156
|
+
const offset = query.all ? 0 : (query.page - 1) * query.limit;
|
|
157
|
+
const selected = query.all ? matches : matches.slice(offset, offset + query.limit);
|
|
158
|
+
const types = query.doc_type === 'all' ? ['profile', 'sop'] : [query.doc_type];
|
|
159
|
+
const rows = [];
|
|
160
|
+
// Bound detail requests; fetch only the chosen page and never fetch bodies in summary mode.
|
|
161
|
+
for (let start = 0; start < selected.length; start += 4) {
|
|
162
|
+
const batch = await Promise.allSettled(selected.slice(start, start + 4).map(async (employee) => {
|
|
163
|
+
if (query.summary_only)
|
|
164
|
+
return employee;
|
|
165
|
+
const response = initial.selected_user_id === employee.user_id ? initial : await fetchDocs(employee.user_id);
|
|
166
|
+
const docs = checkedDocuments(response, initial, employee);
|
|
167
|
+
return { ...employee, documents: Object.fromEntries(types.map(type => [type, docs[type]])) };
|
|
168
|
+
}));
|
|
169
|
+
for (const result of batch) {
|
|
170
|
+
if (result.status === 'rejected')
|
|
171
|
+
throw result.reason;
|
|
172
|
+
rows.push(result.value);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const profileCount = matches.filter(e => e.profile_doc).length;
|
|
176
|
+
const sopCount = matches.filter(e => e.sop_doc).length;
|
|
177
|
+
return {
|
|
178
|
+
ok: true,
|
|
179
|
+
source: 'vps-position-documents',
|
|
180
|
+
queried_at: new Date().toISOString(),
|
|
181
|
+
scope: {
|
|
182
|
+
viewer_user_id: initial.viewer_user_id,
|
|
183
|
+
can_manage_all: initial.can_manage_all,
|
|
184
|
+
description: initial.can_manage_all ? '岗位文档页面可见的当前在职人员' : '岗位文档页面可见的本人在职档案',
|
|
185
|
+
},
|
|
186
|
+
filters: query,
|
|
187
|
+
summary: {
|
|
188
|
+
employees: matches.length,
|
|
189
|
+
profile_documents: profileCount,
|
|
190
|
+
missing_profile_documents: matches.length - profileCount,
|
|
191
|
+
sop_documents: sopCount,
|
|
192
|
+
missing_sop_documents: matches.length - sopCount,
|
|
193
|
+
},
|
|
194
|
+
pagination: {
|
|
195
|
+
page: query.all ? 1 : query.page,
|
|
196
|
+
page_size: query.all ? matches.length : query.limit,
|
|
197
|
+
total_rows: matches.length,
|
|
198
|
+
returned_rows: rows.length,
|
|
199
|
+
has_more: offset + rows.length < matches.length,
|
|
200
|
+
},
|
|
201
|
+
rows,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
function plainResult(result) {
|
|
205
|
+
const lines = [
|
|
206
|
+
'# 岗位职责查询', '',
|
|
207
|
+
`范围:${result.scope.description}。匹配 ${result.pagination.total_rows} 人,本次返回 ${result.pagination.returned_rows} 人。`,
|
|
208
|
+
`岗位说明 ${result.summary.profile_documents} 份,缺失 ${result.summary.missing_profile_documents} 份;SOP ${result.summary.sop_documents} 份,缺失 ${result.summary.missing_sop_documents} 份。`,
|
|
209
|
+
result.pagination.has_more ? '还有后续结果,请使用 --page 翻页或 --all 查询全部匹配人员。' : '',
|
|
210
|
+
];
|
|
211
|
+
for (const row of result.rows) {
|
|
212
|
+
lines.push('', `## ${row.name} — ${row.position}`, `部门:${row.department_path};用户编号:${row.user_id};工号:${row.barcode}`);
|
|
213
|
+
if (!row.documents) {
|
|
214
|
+
lines.push(`岗位说明:${row.profile_doc ? '有文档' : '缺失'};SOP:${row.sop_doc ? '有文档' : '缺失'}(仅返回清单,未读取正文)。`);
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
for (const [type, doc] of Object.entries(row.documents)) {
|
|
218
|
+
lines.push('', `### ${type === 'profile' ? '岗位说明' : 'SOP'}`);
|
|
219
|
+
if (!doc)
|
|
220
|
+
lines.push('系统当前未提供此文档。');
|
|
221
|
+
else
|
|
222
|
+
lines.push(`文档编号:${doc.id};更新时间:${doc.updated_at ?? '未知'}`, '', doc.content_md?.trim() || JSON.stringify(doc.content_json, null, 2));
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return lines.join('\n');
|
|
226
|
+
}
|
|
227
|
+
export function attachJobResponsibilitiesShortcut(parent) {
|
|
228
|
+
attachSharedShortcutOptions(parent.command('+job-responsibilities')
|
|
229
|
+
.alias('+position-docs')
|
|
230
|
+
.description('岗位职责/岗位说明及 SOP;按部门、岗位或个人查询,默认本人')
|
|
231
|
+
.option('--department <name-or-path>', '部门名称或完整路径(按路径段精确匹配)')
|
|
232
|
+
.option('--no-include-children', '部门仅匹配本层,不含下属部门')
|
|
233
|
+
.option('--job <keyword>', '人员档案岗位关键词,包含匹配')
|
|
234
|
+
.option('--name <name>', '员工姓名精确匹配;重名需用部门或用户编号区分')
|
|
235
|
+
.option('--barcode <code>', '员工工号精确匹配')
|
|
236
|
+
.option('--user-id <id>', '用户编号(非 hr.employee 编号)')
|
|
237
|
+
.option('--my', '仅查询本人', false)
|
|
238
|
+
.option('--doc-type <type>', 'profile(岗位说明)、sop 或 all', 'profile')
|
|
239
|
+
.option('--summary-only', '仅返回文档清单及缺失状态,不读取正文', false)
|
|
240
|
+
.option('--page <n>', '页码,从 1 开始', '1')
|
|
241
|
+
.option('--limit <n>', '每页人数,1–100', '20')
|
|
242
|
+
.option('--all', '返回全部匹配人员,仍受当前账号可见范围限制', false)
|
|
243
|
+
.option('--output <path>', '保存为 UTF-8 文件;默认 JSON,--no-json 输出 Markdown'))
|
|
244
|
+
.action(async (opts, cmd) => {
|
|
245
|
+
const baseUrl = resolveShortcutVpsServiceUrl(opts, cmd);
|
|
246
|
+
const query = normalizeJobResponsibilitiesQuery(await mergeShortcutParams({
|
|
247
|
+
department: opts.department, include_children: opts.includeChildren, job: opts.job,
|
|
248
|
+
name: opts.name, barcode: opts.barcode, user_id: opts.userId, my: opts.my, all: opts.all,
|
|
249
|
+
page: opts.page, limit: opts.limit, doc_type: opts.docType, summary_only: opts.summaryOnly,
|
|
250
|
+
}, opts));
|
|
251
|
+
if (opts.validateOnly || opts.dryRun) {
|
|
252
|
+
console.log(JSON.stringify({ validateOnly: true, source: 'vps-position-documents', endpoint: `${baseUrl}${POSITION_DOCS_PATH}`, filters: query }, null, 2));
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const result = await queryJobResponsibilities(query, baseUrl);
|
|
256
|
+
const content = opts.json === false ? plainResult(result) : JSON.stringify(result, null, 2);
|
|
257
|
+
if (opts.output !== undefined) {
|
|
258
|
+
if (!opts.output.trim())
|
|
259
|
+
throw new VertuError('--output 不能为空路径。');
|
|
260
|
+
const output = resolve(opts.output);
|
|
261
|
+
await mkdir(dirname(output), { recursive: true });
|
|
262
|
+
await writeFile(output, `${content}\n`, 'utf8');
|
|
263
|
+
}
|
|
264
|
+
console.log(content);
|
|
265
|
+
});
|
|
266
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { VertuError } from '../../core/errors.js';
|
|
2
|
+
import { trySalesQueryExecute } from '../../core/sales-query-client.js';
|
|
3
|
+
/** 规范化工资月份:接受 YYYY-MM 或 YYYY-MM-DD,统一为当月 1 日。 */
|
|
4
|
+
export function normalizeSalaryMonth(raw) {
|
|
5
|
+
const t = raw.trim();
|
|
6
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(t)) {
|
|
7
|
+
return t;
|
|
8
|
+
}
|
|
9
|
+
if (/^\d{4}-\d{2}$/.test(t)) {
|
|
10
|
+
return `${t}-01`;
|
|
11
|
+
}
|
|
12
|
+
throw new VertuError(`Invalid salary month "${raw}": use YYYY-MM or YYYY-MM-DD (e.g. 2026-04 or 2026-04-01).`);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* 查询工资条。工资条已迁到 VPS 原生 HR(vps_tenant_payslips),
|
|
16
|
+
* 只经 vps-service `execute(vps-hr-compensation-documents/payslip)` 读取 VPS 工资条。
|
|
17
|
+
* 身份解析与访问控制均在服务端完成:本人可查,他人需薪酬管理组及有效薪酬访问凭证。
|
|
18
|
+
* 管理员身份及 Agent Key 不豁免授权;无凭证时请在人事助手中解锁查看,不在对话中输入密码。
|
|
19
|
+
*/
|
|
20
|
+
export async function fetchPayslip(opts) {
|
|
21
|
+
const params = {
|
|
22
|
+
salary_month: normalizeSalaryMonth(opts.salaryMonth),
|
|
23
|
+
limit: opts.limit ?? 5,
|
|
24
|
+
};
|
|
25
|
+
if (opts.useMyEmployee) {
|
|
26
|
+
params.my = true;
|
|
27
|
+
}
|
|
28
|
+
if (opts.employeeId?.trim()) {
|
|
29
|
+
const id = Number.parseInt(opts.employeeId, 10);
|
|
30
|
+
if (!Number.isFinite(id)) {
|
|
31
|
+
throw new VertuError('Invalid --employee-id: expected an integer.');
|
|
32
|
+
}
|
|
33
|
+
params.employee_id = id;
|
|
34
|
+
}
|
|
35
|
+
if (opts.name?.trim()) {
|
|
36
|
+
params.name = opts.name.trim();
|
|
37
|
+
}
|
|
38
|
+
const result = await trySalesQueryExecute('vps-hr-compensation-documents', 'payslip', params, opts.baseUrl);
|
|
39
|
+
if (result === null) {
|
|
40
|
+
throw new VertuError('工资条查询需要 vps-service 地址', {
|
|
41
|
+
hint: '请先 vps-work login,或配置 ~/.vps-work/vps-service.json / VPS_SERVICE_URL。',
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { VertuError } from '../../core/errors.js';
|
|
2
|
+
import { publicBusinessResult } from '../../core/public-result.js';
|
|
3
|
+
import { runVpsServiceDataView, vpsServiceGet } from '../../core/sales-query-client.js';
|
|
4
|
+
import { attachSharedShortcutOptions, parseOptionalInt, resolveShortcutVpsServiceUrl, } from '../shortcut-common.js';
|
|
5
|
+
import { attachOrgStructureShortcut } from '../org-structure.js';
|
|
6
|
+
import { applyHrDirectoryScope, assertHrDirectoryScopeApplied, } from './data-view-shortcuts.js';
|
|
7
|
+
import { fetchPayslip } from './payslip.js';
|
|
8
|
+
function printShortcutJson(opts, out) {
|
|
9
|
+
out = publicBusinessResult(out);
|
|
10
|
+
if (opts.json !== false) {
|
|
11
|
+
console.log(JSON.stringify(out, null, 2));
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
console.log(typeof out === 'string' ? out : JSON.stringify(out));
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function asRecord(value) {
|
|
18
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
19
|
+
? value
|
|
20
|
+
: null;
|
|
21
|
+
}
|
|
22
|
+
function rows(value) {
|
|
23
|
+
const record = asRecord(value);
|
|
24
|
+
const raw = record ? record.rows : undefined;
|
|
25
|
+
return Array.isArray(raw)
|
|
26
|
+
? raw.filter((row) => asRecord(row) !== null)
|
|
27
|
+
: [];
|
|
28
|
+
}
|
|
29
|
+
function num(value) {
|
|
30
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
if (typeof value === 'string' && value.trim()) {
|
|
34
|
+
const n = Number(value);
|
|
35
|
+
return Number.isFinite(n) ? n : null;
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
function str(value) {
|
|
40
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
41
|
+
}
|
|
42
|
+
async function currentHrIdentity(opts, cmd) {
|
|
43
|
+
const serviceBaseUrl = resolveShortcutVpsServiceUrl(opts, cmd);
|
|
44
|
+
const payload = asRecord(await vpsServiceGet('/v1/auth/me', {}, serviceBaseUrl, 'Vertu 身份')) ?? {};
|
|
45
|
+
const user = asRecord(payload.user) ?? {};
|
|
46
|
+
const userinfo = asRecord(payload.userinfo) ?? {};
|
|
47
|
+
const employeeId = num(user.employeeId) ?? num(userinfo.employee_id);
|
|
48
|
+
let profile = {};
|
|
49
|
+
let profileUnavailable;
|
|
50
|
+
if (employeeId) {
|
|
51
|
+
try {
|
|
52
|
+
const profileResult = await runVpsServiceDataView('hr-personnel-directory', applyHrDirectoryScope({ employee_id: employeeId }, opts.includeNonDirectory), 1, serviceBaseUrl);
|
|
53
|
+
assertHrDirectoryScopeApplied(profileResult, opts.includeNonDirectory);
|
|
54
|
+
profile = rows(profileResult)[0] ?? {};
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
// HR 档案视图返回 503 表示当前环境该能力不可用,
|
|
58
|
+
// 身份主体仍来自平台会话,只降级掉档案补充字段,不让 +me 整体失败。
|
|
59
|
+
if (!(error instanceof VertuError) || error.statusCode !== 503) {
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
profileUnavailable = error.message;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
...(profileUnavailable ? { profile_unavailable: profileUnavailable } : {}),
|
|
67
|
+
user_id: num(user.userId) ?? num(userinfo.user_id),
|
|
68
|
+
login: str(user.login) ?? str(userinfo.login),
|
|
69
|
+
name: str(user.name) ?? str(userinfo.name) ?? str(user.employeeName) ?? str(userinfo.employee_name),
|
|
70
|
+
employee_id: employeeId,
|
|
71
|
+
employee_name: str(user.employeeName) ?? str(userinfo.employee_name) ?? str(profile.name),
|
|
72
|
+
barcode: str(user.barcode) ?? str(user.employeeNo) ?? str(userinfo.barcode) ?? str(profile.barcode),
|
|
73
|
+
job_title: str(user.jobTitle) ?? str(userinfo.job_title) ?? str(profile['工作岗位']),
|
|
74
|
+
job_id: num(profile.job_id),
|
|
75
|
+
job_name: str(profile['岗位名称']),
|
|
76
|
+
department_id: num(user.departmentId) ?? num(userinfo.department_id) ?? num(profile.department_id),
|
|
77
|
+
department_name: str(user.departmentName) ?? str(userinfo.department_name) ?? str(profile.current_name),
|
|
78
|
+
department_path: str(user.departmentPath) ?? str(userinfo.department_path),
|
|
79
|
+
department_chain: departmentChain(profile, str(user.departmentPath) ?? str(userinfo.department_path)),
|
|
80
|
+
manager_id: num(profile.manager_employee_id),
|
|
81
|
+
manager_name: str(profile['上级姓名']),
|
|
82
|
+
manager_user_id: num(profile.manager_user_id),
|
|
83
|
+
employment_started_at: str(userinfo.employment_started_at) ?? str(profile['入职日期']),
|
|
84
|
+
employment_ended_at: str(userinfo.employment_ended_at) ?? str(profile['离职日期']),
|
|
85
|
+
role: str(user.role) ?? str(userinfo.role),
|
|
86
|
+
active: typeof user.active === 'boolean' ? user.active : userinfo.active,
|
|
87
|
+
source: 'vertu',
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/** 部门链优先取 HR 档案的四级部门;档案不可用时按平台 department_path("A / B / C")拆分。 */
|
|
91
|
+
function departmentChain(profile, departmentPath) {
|
|
92
|
+
const fromProfile = [profile.one_name, profile.two_name, profile.three_name, profile.current_name]
|
|
93
|
+
.map((item) => str(item))
|
|
94
|
+
.filter((item, index, arr) => Boolean(item) && arr.indexOf(item) === index);
|
|
95
|
+
if (fromProfile.length) {
|
|
96
|
+
return fromProfile;
|
|
97
|
+
}
|
|
98
|
+
return (departmentPath ?? '').split('/').map((item) => item.trim()).filter(Boolean);
|
|
99
|
+
}
|
|
100
|
+
function employeeLineOptions(cmd, limitDefault = '20') {
|
|
101
|
+
return cmd
|
|
102
|
+
.option('--name <name>', 'Employee name (unique match)')
|
|
103
|
+
.option('--employee-id <id>', 'hr.employee id')
|
|
104
|
+
.option('--my', 'Current user (@my_employee)', false)
|
|
105
|
+
.option('--limit <n>', 'Max rows', limitDefault);
|
|
106
|
+
}
|
|
107
|
+
function requireEmployeeTarget(opts) {
|
|
108
|
+
if (!opts.my && !opts.employeeId && !opts.name?.trim()) {
|
|
109
|
+
throw new VertuError('Pass --name <姓名>, --employee-id <id>, or --my.');
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function invokeOpts(opts, cmd) {
|
|
113
|
+
const merged = cmd.optsWithGlobals();
|
|
114
|
+
return {
|
|
115
|
+
baseUrl: opts.baseUrl ?? merged.baseUrl,
|
|
116
|
+
endpoint: opts.endpoint ?? merged.endpoint,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/** Features · 人力资源 — `vps-work hr +…`(vps-service:身份与工资条) */
|
|
120
|
+
export function attachHrShortcuts(parent) {
|
|
121
|
+
attachSharedShortcutOptions(parent
|
|
122
|
+
.command('+me')
|
|
123
|
+
.description('当前用户身份(user/employee/department/manager)')
|
|
124
|
+
.option('--include-non-directory', '允许补充通讯录不可见的 HR 档案', false)).action(async (opts, cmd) => {
|
|
125
|
+
printShortcutJson(opts, await currentHrIdentity(opts, cmd));
|
|
126
|
+
});
|
|
127
|
+
attachOrgStructureShortcut(parent, '+org-tree', '组织部门树(默认一级/二级;用 --max-depth 0 查看全量)');
|
|
128
|
+
attachSharedShortcutOptions(employeeLineOptions(parent
|
|
129
|
+
.command('+payslip')
|
|
130
|
+
.description('月度工资条(VPS 原生 HR,本人可查,查他人需企业管理员)')
|
|
131
|
+
.requiredOption('--salary-month <ym>', 'YYYY-MM or YYYY-MM-DD'), '5')).action(async (opts, cmd) => {
|
|
132
|
+
requireEmployeeTarget(opts);
|
|
133
|
+
const month = opts.salaryMonth;
|
|
134
|
+
if (!month) {
|
|
135
|
+
throw new VertuError('--salary-month is required.');
|
|
136
|
+
}
|
|
137
|
+
printShortcutJson(opts, await fetchPayslip({
|
|
138
|
+
baseUrl: invokeOpts(opts, cmd).baseUrl,
|
|
139
|
+
salaryMonth: month,
|
|
140
|
+
name: opts.name,
|
|
141
|
+
employeeId: opts.employeeId,
|
|
142
|
+
useMyEmployee: !!opts.my,
|
|
143
|
+
limit: parseOptionalInt(opts.limit, '--limit') ?? 5,
|
|
144
|
+
}));
|
|
145
|
+
});
|
|
146
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { VertuError } from '../../core/errors.js';
|
|
5
|
+
import { imRequest } from '../../core/im-client.js';
|
|
6
|
+
import { expandAtFileToken } from '../../utils/at-file.js';
|
|
7
|
+
import { parseJsonArg } from '../../utils/parse-json-arg.js';
|
|
8
|
+
import { readStdinText, readUtf8File } from '../../utils/read-text.js';
|
|
9
|
+
const EXT_MIME = {
|
|
10
|
+
'.png': 'image/png',
|
|
11
|
+
'.jpg': 'image/jpeg',
|
|
12
|
+
'.jpeg': 'image/jpeg',
|
|
13
|
+
'.gif': 'image/gif',
|
|
14
|
+
'.webp': 'image/webp',
|
|
15
|
+
'.svg': 'image/svg+xml',
|
|
16
|
+
'.pdf': 'application/pdf',
|
|
17
|
+
'.html': 'text/html',
|
|
18
|
+
'.htm': 'text/html',
|
|
19
|
+
'.md': 'text/markdown',
|
|
20
|
+
'.txt': 'text/plain',
|
|
21
|
+
'.csv': 'text/csv',
|
|
22
|
+
'.json': 'application/json',
|
|
23
|
+
'.doc': 'application/msword',
|
|
24
|
+
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
25
|
+
'.xls': 'application/vnd.ms-excel',
|
|
26
|
+
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
27
|
+
'.ppt': 'application/vnd.ms-powerpoint',
|
|
28
|
+
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
29
|
+
'.zip': 'application/zip',
|
|
30
|
+
'.mp3': 'audio/mpeg',
|
|
31
|
+
'.wav': 'audio/wav',
|
|
32
|
+
'.mp4': 'video/mp4',
|
|
33
|
+
'.mov': 'video/quicktime',
|
|
34
|
+
'.webm': 'video/webm',
|
|
35
|
+
};
|
|
36
|
+
function guessMimetype(filePath) {
|
|
37
|
+
return EXT_MIME[path.extname(filePath).toLowerCase()] ?? 'application/octet-stream';
|
|
38
|
+
}
|
|
39
|
+
async function readBodyFromFileToken(token) {
|
|
40
|
+
if (token === '-') {
|
|
41
|
+
return readStdinText();
|
|
42
|
+
}
|
|
43
|
+
if (token.startsWith('@')) {
|
|
44
|
+
return expandAtFileToken(token, '--body-file');
|
|
45
|
+
}
|
|
46
|
+
return readUtf8File(path.resolve(token));
|
|
47
|
+
}
|
|
48
|
+
export async function resolveImMessageBody(opts) {
|
|
49
|
+
if (opts.bodyFile?.trim()) {
|
|
50
|
+
return (await readBodyFromFileToken(opts.bodyFile.trim())).trimEnd();
|
|
51
|
+
}
|
|
52
|
+
return opts.body?.trim() ?? '';
|
|
53
|
+
}
|
|
54
|
+
function normalizeAttachmentItem(item, index) {
|
|
55
|
+
if (typeof item !== 'object' || item === null || Array.isArray(item)) {
|
|
56
|
+
throw new VertuError(`attachments[${index}] 必须为对象。`);
|
|
57
|
+
}
|
|
58
|
+
const row = item;
|
|
59
|
+
const name = typeof row.name === 'string' ? row.name.trim() : '';
|
|
60
|
+
if (!name) {
|
|
61
|
+
throw new VertuError(`attachments[${index}].name 为必填。`);
|
|
62
|
+
}
|
|
63
|
+
const rawType = typeof row.attachment_type === 'string' ? row.attachment_type.trim() : 'file';
|
|
64
|
+
const allowed = ['image', 'file', 'audio', 'video', 'link', 'emoji', 'other'];
|
|
65
|
+
if (!allowed.includes(rawType)) {
|
|
66
|
+
throw new VertuError(`attachments[${index}].attachment_type 不支持: ${rawType}`);
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
attachment_type: rawType,
|
|
70
|
+
name,
|
|
71
|
+
mime_type: typeof row.mime_type === 'string' ? row.mime_type.trim() : null,
|
|
72
|
+
size_bytes: typeof row.size_bytes === 'number' ? row.size_bytes : null,
|
|
73
|
+
url: typeof row.url === 'string' ? row.url.trim() : null,
|
|
74
|
+
storage_key: typeof row.storage_key === 'string' ? row.storage_key.trim() : null,
|
|
75
|
+
sha256: typeof row.sha256 === 'string' ? row.sha256.trim() : null,
|
|
76
|
+
metadata: typeof row.metadata === 'object' && row.metadata !== null && !Array.isArray(row.metadata)
|
|
77
|
+
? row.metadata
|
|
78
|
+
: {},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
export async function resolveImAttachments(attachPaths, attachmentsJson, baseUrl) {
|
|
82
|
+
const fromFiles = [];
|
|
83
|
+
for (const raw of attachPaths ?? []) {
|
|
84
|
+
const token = raw.trim();
|
|
85
|
+
if (!token)
|
|
86
|
+
continue;
|
|
87
|
+
const filePath = path.resolve(token.startsWith('@') ? token.slice(1) : token);
|
|
88
|
+
const buf = await readFile(filePath);
|
|
89
|
+
const mime = guessMimetype(filePath);
|
|
90
|
+
const name = path.basename(filePath);
|
|
91
|
+
const uploaded = await imRequest('POST', '/v1/im/files', {
|
|
92
|
+
baseUrl,
|
|
93
|
+
body: {
|
|
94
|
+
action: 'upload',
|
|
95
|
+
name,
|
|
96
|
+
mime_type: mime,
|
|
97
|
+
data_base64: buf.toString('base64'),
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
const file = typeof uploaded === 'object' && uploaded !== null && 'file' in uploaded
|
|
101
|
+
? uploaded.file
|
|
102
|
+
: null;
|
|
103
|
+
if (typeof file !== 'object' || file === null || Array.isArray(file)) {
|
|
104
|
+
throw new VertuError(`文件上传成功但未返回文件信息:${name}`);
|
|
105
|
+
}
|
|
106
|
+
const attachment = normalizeAttachmentItem(file, fromFiles.length);
|
|
107
|
+
if (!attachment.url || !attachment.storage_key) {
|
|
108
|
+
throw new VertuError(`文件上传成功但缺少 url/storage_key:${name}`);
|
|
109
|
+
}
|
|
110
|
+
fromFiles.push({
|
|
111
|
+
...attachment,
|
|
112
|
+
sha256: attachment.sha256 ||
|
|
113
|
+
createHash('sha256').update(buf).digest('hex'),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
let fromJson = [];
|
|
117
|
+
if (attachmentsJson?.trim()) {
|
|
118
|
+
const parsed = parseJsonArg(attachmentsJson.trim(), '--attachments-json');
|
|
119
|
+
if (!Array.isArray(parsed)) {
|
|
120
|
+
throw new VertuError('--attachments-json 必须为 JSON 数组。');
|
|
121
|
+
}
|
|
122
|
+
fromJson = parsed.map((item, index) => normalizeAttachmentItem(item, index));
|
|
123
|
+
}
|
|
124
|
+
const merged = [...fromFiles, ...fromJson];
|
|
125
|
+
return merged.length ? merged : undefined;
|
|
126
|
+
}
|
|
127
|
+
export async function buildImMessagePayload(opts) {
|
|
128
|
+
const body = await resolveImMessageBody(opts);
|
|
129
|
+
const attachments = await resolveImAttachments(opts.attach, opts.attachmentsJson, opts.baseUrl);
|
|
130
|
+
if (!body && !attachments?.length) {
|
|
131
|
+
throw new VertuError('请提供 --body / --body-file,或至少一个 --attach / --attachments-json。');
|
|
132
|
+
}
|
|
133
|
+
return attachments?.length ? { body, attachments } : { body };
|
|
134
|
+
}
|
|
135
|
+
export function parseUserIdList(raw, label) {
|
|
136
|
+
if (!raw?.trim()) {
|
|
137
|
+
throw new VertuError(`${label} 为必填。`);
|
|
138
|
+
}
|
|
139
|
+
const trimmed = raw.trim();
|
|
140
|
+
const parts = trimmed.startsWith('[')
|
|
141
|
+
? parseJsonArg(trimmed, label).map((item) => String(item))
|
|
142
|
+
: trimmed.split(/[,,\s]+/).filter(Boolean);
|
|
143
|
+
return parts.map((part, index) => {
|
|
144
|
+
const n = Number.parseInt(part, 10);
|
|
145
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
146
|
+
throw new VertuError(`Invalid ${label}[${index}]: expected a positive integer.`);
|
|
147
|
+
}
|
|
148
|
+
return n;
|
|
149
|
+
});
|
|
150
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { attachImShortcuts } from './shortcuts.js';
|
|
3
|
+
export function buildImCommand() {
|
|
4
|
+
const im = new Command('im').description('Vertu IM shortcuts: private chat, groups, org channels, broadcast, bots.');
|
|
5
|
+
attachImShortcuts(im);
|
|
6
|
+
return im;
|
|
7
|
+
}
|