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.
Files changed (119) hide show
  1. package/dist/packages/vps-work/src/bin/vps-work.js +271 -0
  2. package/dist/packages/vps-work/src/commands/agent/index.js +264 -0
  3. package/dist/packages/vps-work/src/commands/approval/index.js +7 -0
  4. package/dist/packages/vps-work/src/commands/approval/shortcuts.js +259 -0
  5. package/dist/packages/vps-work/src/commands/attendance/applications.js +21 -0
  6. package/dist/packages/vps-work/src/commands/attendance/index.js +9 -0
  7. package/dist/packages/vps-work/src/commands/attendance/scheduling.js +183 -0
  8. package/dist/packages/vps-work/src/commands/attendance/shortcuts.js +131 -0
  9. package/dist/packages/vps-work/src/commands/auth/check.js +34 -0
  10. package/dist/packages/vps-work/src/commands/auth/index.js +21 -0
  11. package/dist/packages/vps-work/src/commands/auth/list.js +50 -0
  12. package/dist/packages/vps-work/src/commands/auth/scopes.js +17 -0
  13. package/dist/packages/vps-work/src/commands/auth/status.js +127 -0
  14. package/dist/packages/vps-work/src/commands/auth/tenants.js +35 -0
  15. package/dist/packages/vps-work/src/commands/auth/use.js +40 -0
  16. package/dist/packages/vps-work/src/commands/calendar/index.js +7 -0
  17. package/dist/packages/vps-work/src/commands/calendar/shortcuts.js +598 -0
  18. package/dist/packages/vps-work/src/commands/catalog.js +29 -0
  19. package/dist/packages/vps-work/src/commands/cli-install.js +70 -0
  20. package/dist/packages/vps-work/src/commands/collab-invoke.js +19 -0
  21. package/dist/packages/vps-work/src/commands/corporate/index.js +204 -0
  22. package/dist/packages/vps-work/src/commands/creative/index.js +369 -0
  23. package/dist/packages/vps-work/src/commands/documents/index.js +991 -0
  24. package/dist/packages/vps-work/src/commands/drive/index.js +435 -0
  25. package/dist/packages/vps-work/src/commands/exec.js +46 -0
  26. package/dist/packages/vps-work/src/commands/hr/business.js +22 -0
  27. package/dist/packages/vps-work/src/commands/hr/data-view-shortcuts.js +519 -0
  28. package/dist/packages/vps-work/src/commands/hr/index.js +15 -0
  29. package/dist/packages/vps-work/src/commands/hr/job-responsibilities.js +266 -0
  30. package/dist/packages/vps-work/src/commands/hr/payslip.js +45 -0
  31. package/dist/packages/vps-work/src/commands/hr/shortcuts.js +146 -0
  32. package/dist/packages/vps-work/src/commands/im/im-message-payload.js +150 -0
  33. package/dist/packages/vps-work/src/commands/im/index.js +7 -0
  34. package/dist/packages/vps-work/src/commands/im/shortcuts.js +856 -0
  35. package/dist/packages/vps-work/src/commands/knowledge/index.js +36 -0
  36. package/dist/packages/vps-work/src/commands/login.js +52 -0
  37. package/dist/packages/vps-work/src/commands/logout.js +12 -0
  38. package/dist/packages/vps-work/src/commands/mail/email.js +132 -0
  39. package/dist/packages/vps-work/src/commands/mail/index.js +9 -0
  40. package/dist/packages/vps-work/src/commands/mail/shortcuts.js +43 -0
  41. package/dist/packages/vps-work/src/commands/meeting/index.js +7 -0
  42. package/dist/packages/vps-work/src/commands/meeting/shortcuts.js +120 -0
  43. package/dist/packages/vps-work/src/commands/odoo-invoke.js +21 -0
  44. package/dist/packages/vps-work/src/commands/odoo-leaf-registry.js +13 -0
  45. package/dist/packages/vps-work/src/commands/office/index.js +59 -0
  46. package/dist/packages/vps-work/src/commands/okr/index.js +7 -0
  47. package/dist/packages/vps-work/src/commands/okr/shortcuts.js +75 -0
  48. package/dist/packages/vps-work/src/commands/org-structure.js +347 -0
  49. package/dist/packages/vps-work/src/commands/popup/index.js +147 -0
  50. package/dist/packages/vps-work/src/commands/product/abnormal-statistics.js +250 -0
  51. package/dist/packages/vps-work/src/commands/product/data-view-shortcuts.js +253 -0
  52. package/dist/packages/vps-work/src/commands/product/index.js +11 -0
  53. package/dist/packages/vps-work/src/commands/purchase/index.js +45 -0
  54. package/dist/packages/vps-work/src/commands/reauth.js +127 -0
  55. package/dist/packages/vps-work/src/commands/report/index.js +7 -0
  56. package/dist/packages/vps-work/src/commands/report/missing-daily-reports.js +9 -0
  57. package/dist/packages/vps-work/src/commands/report/shortcuts.js +251 -0
  58. package/dist/packages/vps-work/src/commands/sales/index.js +11 -0
  59. package/dist/packages/vps-work/src/commands/sales/period-params.js +75 -0
  60. package/dist/packages/vps-work/src/commands/sales/shortcuts-personnel.js +124 -0
  61. package/dist/packages/vps-work/src/commands/sales/shortcuts.js +800 -0
  62. package/dist/packages/vps-work/src/commands/shortcut-common.js +88 -0
  63. package/dist/packages/vps-work/src/commands/shortcut-params.js +22 -0
  64. package/dist/packages/vps-work/src/commands/skill/index.js +136 -0
  65. package/dist/packages/vps-work/src/commands/skill/personal.js +167 -0
  66. package/dist/packages/vps-work/src/commands/store/index.js +82 -0
  67. package/dist/packages/vps-work/src/commands/task/index.js +7 -0
  68. package/dist/packages/vps-work/src/commands/task/task-center.js +595 -0
  69. package/dist/packages/vps-work/src/commands/tools/index.js +127 -0
  70. package/dist/packages/vps-work/src/commands/vemory/index.js +7 -0
  71. package/dist/packages/vps-work/src/commands/vemory/shortcuts.js +53 -0
  72. package/dist/packages/vps-work/src/commands/vertu-aisales.js +30 -0
  73. package/dist/packages/vps-work/src/commands/vertu-erp.js +30 -0
  74. package/dist/packages/vps-work/src/core/aisales-mcp-client.js +35 -0
  75. package/dist/packages/vps-work/src/core/auth.js +240 -0
  76. package/dist/packages/vps-work/src/core/cli-bin.js +11 -0
  77. package/dist/packages/vps-work/src/core/cli-root-help.js +190 -0
  78. package/dist/packages/vps-work/src/core/client.js +158 -0
  79. package/dist/packages/vps-work/src/core/config.js +183 -0
  80. package/dist/packages/vps-work/src/core/corporate-client.js +52 -0
  81. package/dist/packages/vps-work/src/core/creative-attachment-download.js +48 -0
  82. package/dist/packages/vps-work/src/core/documents-client.js +57 -0
  83. package/dist/packages/vps-work/src/core/endpoints.js +46 -0
  84. package/dist/packages/vps-work/src/core/erp-mcp-client.js +11 -0
  85. package/dist/packages/vps-work/src/core/errors.js +107 -0
  86. package/dist/packages/vps-work/src/core/execute-catalog-client.js +42 -0
  87. package/dist/packages/vps-work/src/core/hermes-client.js +70 -0
  88. package/dist/packages/vps-work/src/core/im-attachment-download.js +109 -0
  89. package/dist/packages/vps-work/src/core/im-client.js +64 -0
  90. package/dist/packages/vps-work/src/core/keychain.js +58 -0
  91. package/dist/packages/vps-work/src/core/mail-client.js +82 -0
  92. package/dist/packages/vps-work/src/core/mcp-client.js +129 -0
  93. package/dist/packages/vps-work/src/core/office-client.js +25 -0
  94. package/dist/packages/vps-work/src/core/office-files.js +82 -0
  95. package/dist/packages/vps-work/src/core/paths.js +70 -0
  96. package/dist/packages/vps-work/src/core/public-result.js +43 -0
  97. package/dist/packages/vps-work/src/core/sales-invoice-attachments-client.js +75 -0
  98. package/dist/packages/vps-work/src/core/sales-query-client.js +452 -0
  99. package/dist/packages/vps-work/src/core/skill-catalog.js +158 -0
  100. package/dist/packages/vps-work/src/core/skill-key.js +19 -0
  101. package/dist/packages/vps-work/src/core/v-drive-store.js +392 -0
  102. package/dist/packages/vps-work/src/core/vertu-agent-env.js +32 -0
  103. package/dist/packages/vps-work/src/core/vps-auth-scopes.js +73 -0
  104. package/dist/packages/vps-work/src/core/vps-service-auth.js +382 -0
  105. package/dist/packages/vps-work/src/core/vps-service-config.js +83 -0
  106. package/dist/packages/vps-work/src/core/vps-service-http.js +187 -0
  107. package/dist/packages/vps-work/src/utils/at-file.js +34 -0
  108. package/dist/packages/vps-work/src/utils/cli-option-helpers.js +52 -0
  109. package/dist/packages/vps-work/src/utils/frontmatter.js +26 -0
  110. package/dist/packages/vps-work/src/utils/normalize-argv.js +51 -0
  111. package/dist/packages/vps-work/src/utils/parse-json-arg.js +85 -0
  112. package/dist/packages/vps-work/src/utils/prompt.js +46 -0
  113. package/dist/packages/vps-work/src/utils/read-text.js +44 -0
  114. package/dist/packages/vps-work/src/utils/schema-to-options.js +307 -0
  115. package/dist/packages/vps-work/src/utils/unwrap-envelope.js +31 -0
  116. package/dist/shared/im-org-tree/index.js +200 -0
  117. package/dist/vps-work.cjs +3 -3
  118. package/package.json +1 -1
  119. package/skills/vertu-aisales/SKILL.md +4 -2
@@ -0,0 +1,250 @@
1
+ import { VertuError } from '../../core/errors.js';
2
+ import { vpsServiceGet, vpsServicePost } from '../../core/sales-query-client.js';
3
+ import { expandAtFileToken } from '../../utils/at-file.js';
4
+ import { attachSharedShortcutOptions, resolveShortcutVpsServiceUrl, } from '../shortcut-common.js';
5
+ const LOCATION_PATH = '/v1/product/abnormal/location';
6
+ const LOCATION_LIST_PATH = '/v1/product/abnormal/location-list';
7
+ const DETAILS_PATH = '/v1/product/abnormal/details';
8
+ function isPreview(opts) {
9
+ return opts.validateOnly === true || opts.dryRun === true;
10
+ }
11
+ function printResult(opts, value) {
12
+ console.log(JSON.stringify(value, null, opts.json === false ? undefined : 2));
13
+ }
14
+ function asRecord(value) {
15
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
16
+ ? value
17
+ : null;
18
+ }
19
+ function remoteMessage(value, fallback) {
20
+ const record = asRecord(value);
21
+ for (const key of ['message', 'error', 'statusText']) {
22
+ const candidate = record?.[key];
23
+ if (typeof candidate === 'string' && candidate.trim())
24
+ return candidate.trim();
25
+ }
26
+ return fallback;
27
+ }
28
+ function unwrapEnvelopeRows(value, label) {
29
+ const envelope = asRecord(value);
30
+ if (!envelope || Number(envelope.code) !== 200) {
31
+ throw new VertuError(remoteMessage(value, `${label}返回结构无效。`));
32
+ }
33
+ const payload = asRecord(envelope.data);
34
+ if (!payload || !Array.isArray(payload.data)) {
35
+ throw new VertuError(`${label}返回结构缺少 data.data 数组。`);
36
+ }
37
+ return {
38
+ rows: payload.data.filter((item) => asRecord(item) !== null),
39
+ reportedTotal: Number.isFinite(Number(payload.total)) ? Number(payload.total) : payload.data.length,
40
+ };
41
+ }
42
+ function parseLocationIds(raw) {
43
+ if (!raw?.trim())
44
+ return [];
45
+ const values = raw.split(/[\s,]+/).filter(Boolean);
46
+ const ids = values.map((value) => Number(value));
47
+ if (ids.some((value) => !Number.isInteger(value) || value <= 0)) {
48
+ throw new VertuError('--location-ids 必须是英文逗号分隔的正整数。');
49
+ }
50
+ return [...new Set(ids)];
51
+ }
52
+ async function parseImeis(raw) {
53
+ const expanded = (await expandAtFileToken(raw, '--imeis')).trim();
54
+ if (!expanded)
55
+ throw new VertuError('--imeis 不能为空。');
56
+ let values;
57
+ if (expanded.startsWith('[')) {
58
+ try {
59
+ const parsed = JSON.parse(expanded);
60
+ if (!Array.isArray(parsed))
61
+ throw new Error('not array');
62
+ values = parsed;
63
+ }
64
+ catch {
65
+ throw new VertuError('--imeis 的 JSON 输入必须是字符串或数字数组。');
66
+ }
67
+ }
68
+ else {
69
+ values = expanded.split(/[\s,]+/).filter(Boolean);
70
+ }
71
+ if (values.some((value) => typeof value !== 'string' && typeof value !== 'number')) {
72
+ throw new VertuError('--imeis 的每一项必须是字符串或数字。');
73
+ }
74
+ const imeis = [...new Set(values.map((value) => String(value).trim()).filter(Boolean))];
75
+ if (!imeis.length)
76
+ throw new VertuError('--imeis 不能为空。');
77
+ if (imeis.length > 5000)
78
+ throw new VertuError('--imeis 单次最多 5000 个。');
79
+ return imeis;
80
+ }
81
+ function numeric(row, key) {
82
+ const value = Number(row[key]);
83
+ return Number.isFinite(value) ? value : 0;
84
+ }
85
+ function summarizeLocations(rows) {
86
+ const locations = new Set();
87
+ const abnormalImeis = new Set();
88
+ let totalStock = 0;
89
+ let physicalStock = 0;
90
+ let inTransitStock = 0;
91
+ let storeStock = 0;
92
+ for (const row of rows) {
93
+ const location = typeof row.location_name === 'string' ? row.location_name.trim() : '';
94
+ if (location)
95
+ locations.add(location);
96
+ totalStock += numeric(row, 'total_stock');
97
+ physicalStock += numeric(row, 'physical_stock');
98
+ inTransitStock += numeric(row, 'in_transit_stock');
99
+ storeStock += numeric(row, 'store_stock');
100
+ if (Array.isArray(row.abnormal_imeis)) {
101
+ for (const imei of row.abnormal_imeis) {
102
+ const normalized = String(imei).trim();
103
+ if (normalized)
104
+ abnormalImeis.add(normalized);
105
+ }
106
+ }
107
+ }
108
+ return {
109
+ abnormal_imei_count: abnormalImeis.size,
110
+ in_transit_stock: inTransitStock,
111
+ location_count: locations.size,
112
+ physical_stock: physicalStock,
113
+ row_count: rows.length,
114
+ store_stock: storeStock,
115
+ total_stock: totalStock,
116
+ };
117
+ }
118
+ async function fetchLocationOptions(serviceBaseUrl, locationName) {
119
+ const response = await vpsServiceGet(LOCATION_LIST_PATH, { location_name: locationName?.trim() || undefined }, serviceBaseUrl, '设备异常仓位列表');
120
+ return unwrapEnvelopeRows(response, '仓位列表接口');
121
+ }
122
+ /** Add role-protected abnormal inventory/device statistics shortcuts to the product domain. */
123
+ export function attachProductAbnormalStatisticsShortcuts(parent) {
124
+ attachSharedShortcutOptions(parent
125
+ .command('+abnormal-location')
126
+ .description('按仓位查询库存、在途、门店库存和异常 IMEI(需设备异常分析角色)')
127
+ .option('--location-name <name>', '仓位名称模糊搜索')
128
+ .option('--location-ids <ids>', '仓位 ID,多个用英文逗号分隔')
129
+ .option('--default-selected', '复现 vps-ep 页面:先读取仓位列表并使用 is_selected=true 的仓位')).action(async (opts, cmd) => {
130
+ const serviceBaseUrl = resolveShortcutVpsServiceUrl(opts, cmd);
131
+ let locationIds = parseLocationIds(opts.locationIds);
132
+ if (opts.defaultSelected && locationIds.length) {
133
+ throw new VertuError('--default-selected 与 --location-ids 不能同时使用。');
134
+ }
135
+ const locationName = opts.locationName?.trim() || undefined;
136
+ if (isPreview(opts)) {
137
+ printResult(opts, {
138
+ validateOnly: true,
139
+ source: 'vps-service-device-abnormal-proxy',
140
+ endpoint: `${serviceBaseUrl}${LOCATION_PATH}`,
141
+ required_scope: 'product.device_abnormal:read',
142
+ query: {
143
+ ...(locationName ? { location_name: locationName } : {}),
144
+ ...(locationIds.length ? { location_ids: locationIds.join(',') } : {}),
145
+ ...(opts.defaultSelected ? { location_ids: `<is_selected=true from ${LOCATION_LIST_PATH}>` } : {}),
146
+ },
147
+ ...(opts.defaultSelected
148
+ ? { prerequisite_endpoint: `${serviceBaseUrl}${LOCATION_LIST_PATH}` }
149
+ : {}),
150
+ });
151
+ return;
152
+ }
153
+ if (opts.defaultSelected) {
154
+ const options = await fetchLocationOptions(serviceBaseUrl);
155
+ locationIds = options.rows
156
+ .filter((row) => row.is_selected === true)
157
+ .map((row) => Number(row.location_id))
158
+ .filter((id) => Number.isInteger(id) && id > 0);
159
+ if (!locationIds.length) {
160
+ throw new VertuError('仓位列表未返回 is_selected=true 的默认仓位,已停止查询,避免退化为全仓位。');
161
+ }
162
+ }
163
+ const response = await vpsServiceGet(LOCATION_PATH, {
164
+ location_name: locationName,
165
+ location_ids: locationIds.length ? locationIds.join(',') : undefined,
166
+ }, serviceBaseUrl, '设备异常仓位统计');
167
+ const result = unwrapEnvelopeRows(response, '仓位异常统计接口');
168
+ printResult(opts, {
169
+ source: 'vps-service-device-abnormal-proxy',
170
+ filter: {
171
+ location_name: locationName ?? null,
172
+ location_ids: locationIds,
173
+ default_selected: opts.defaultSelected === true,
174
+ },
175
+ summary: summarizeLocations(result.rows),
176
+ rows: result.rows,
177
+ reported_total: result.reportedTotal,
178
+ });
179
+ });
180
+ attachSharedShortcutOptions(parent
181
+ .command('+abnormal-location-list')
182
+ .description('查询异常统计可用仓位及 vps-ep 默认选中状态(需设备异常分析角色)')
183
+ .option('--location-name <name>', '仓位名称模糊搜索')).action(async (opts, cmd) => {
184
+ const serviceBaseUrl = resolveShortcutVpsServiceUrl(opts, cmd);
185
+ const locationName = opts.locationName?.trim() || undefined;
186
+ if (isPreview(opts)) {
187
+ printResult(opts, {
188
+ validateOnly: true,
189
+ source: 'vps-service-device-abnormal-proxy',
190
+ endpoint: `${serviceBaseUrl}${LOCATION_LIST_PATH}`,
191
+ required_scope: 'product.device_abnormal:read',
192
+ query: { ...(locationName ? { location_name: locationName } : {}) },
193
+ });
194
+ return;
195
+ }
196
+ const result = await fetchLocationOptions(serviceBaseUrl, locationName);
197
+ printResult(opts, {
198
+ source: 'vps-service-device-abnormal-proxy',
199
+ filter: { location_name: locationName ?? null },
200
+ rows: result.rows,
201
+ total: result.reportedTotal,
202
+ default_selected_ids: result.rows
203
+ .filter((row) => row.is_selected === true)
204
+ .map((row) => Number(row.location_id))
205
+ .filter((id) => Number.isInteger(id) && id > 0),
206
+ });
207
+ });
208
+ attachSharedShortcutOptions(parent
209
+ .command('+abnormal-details')
210
+ .description('按 IMEI 查询仍未处理的设备异常明细(需设备异常分析角色)')
211
+ .requiredOption('--imeis <values>', 'IMEI,逗号/空白分隔;支持 JSON 数组或 @file')
212
+ .option('--stat-date <date>', '统计开始时间,日期字符串或时间戳')
213
+ .option('--end-date <date>', '统计结束时间,日期字符串或时间戳')
214
+ .option('--exception-type <type>', '异常筛选类型,例如 address_exception')).action(async (opts, cmd) => {
215
+ const serviceBaseUrl = resolveShortcutVpsServiceUrl(opts, cmd);
216
+ const imeis = await parseImeis(opts.imeis);
217
+ const body = {
218
+ imeis,
219
+ ...(opts.statDate?.trim() ? { stat_date: opts.statDate.trim() } : {}),
220
+ ...(opts.endDate?.trim() ? { end_date: opts.endDate.trim() } : {}),
221
+ ...(opts.exceptionType?.trim() ? { exception_type: opts.exceptionType.trim() } : {}),
222
+ };
223
+ if (isPreview(opts)) {
224
+ printResult(opts, {
225
+ validateOnly: true,
226
+ source: 'vps-service-device-abnormal-proxy',
227
+ endpoint: `${serviceBaseUrl}${DETAILS_PATH}`,
228
+ required_scope: 'product.device_abnormal:read',
229
+ method: 'POST',
230
+ body,
231
+ });
232
+ return;
233
+ }
234
+ const response = await vpsServicePost(DETAILS_PATH, body, serviceBaseUrl, '设备异常明细');
235
+ if (!Array.isArray(response)) {
236
+ throw new VertuError('设备异常明细接口应返回数组,但实际响应结构不同。');
237
+ }
238
+ printResult(opts, {
239
+ source: 'vps-service-device-abnormal-proxy',
240
+ filter: {
241
+ imeis,
242
+ stat_date: body.stat_date ?? '2000-01-01',
243
+ end_date: body.end_date ?? '2035-01-01',
244
+ exception_type: body.exception_type ?? '',
245
+ },
246
+ rows: response,
247
+ total: response.length,
248
+ });
249
+ });
250
+ }
@@ -0,0 +1,253 @@
1
+ import { VertuError } from '../../core/errors.js';
2
+ import { runVpsServiceDataView, vpsServicePost } from '../../core/sales-query-client.js';
3
+ import { expandAtFileToken } from '../../utils/at-file.js';
4
+ import { parseJsonArg } from '../../utils/parse-json-arg.js';
5
+ import { attachSharedShortcutOptions, mergeShortcutParams, parseOptionalInt, resolveShortcutVpsServiceUrl, } from '../shortcut-common.js';
6
+ function printResult(opts, result) {
7
+ if (opts.json !== false) {
8
+ console.log(JSON.stringify(result, null, 2));
9
+ return;
10
+ }
11
+ console.log(typeof result === 'string' ? result : JSON.stringify(result));
12
+ }
13
+ function parsePositiveInt(raw, flag, fallback, max) {
14
+ const value = parseOptionalInt(raw, flag) ?? fallback;
15
+ if (value < 1) {
16
+ throw new VertuError(`Invalid ${flag}: expected a positive integer.`);
17
+ }
18
+ return max ? Math.min(value, max) : value;
19
+ }
20
+ function collectProductFilters(opts) {
21
+ const filters = {};
22
+ if (opts.productCode) {
23
+ filters.product_code = opts.productCode;
24
+ }
25
+ if (opts.productName) {
26
+ filters.product_name = opts.productName;
27
+ }
28
+ if (opts.productFamily) {
29
+ filters.product_family = opts.productFamily;
30
+ }
31
+ if (opts.productCategory) {
32
+ filters.product_category = opts.productCategory;
33
+ }
34
+ if (opts.productClass) {
35
+ filters.product_class = opts.productClass;
36
+ }
37
+ if (opts.productSubcategory) {
38
+ filters.product_subcategory = opts.productSubcategory;
39
+ }
40
+ if (opts.productColor) {
41
+ filters.product_color = opts.productColor;
42
+ }
43
+ if (opts.productMaterial) {
44
+ filters.product_material = opts.productMaterial;
45
+ }
46
+ if (opts.productColorMaterial) {
47
+ filters.product_color_material = opts.productColorMaterial;
48
+ }
49
+ if (opts.productPhoneVersion) {
50
+ filters.product_phone_version = opts.productPhoneVersion;
51
+ }
52
+ if (opts.priceRegion) {
53
+ filters.price_region = opts.priceRegion;
54
+ }
55
+ return filters;
56
+ }
57
+ async function resolveFiltersJson(raw) {
58
+ if (raw === undefined || raw.trim() === '') {
59
+ return {};
60
+ }
61
+ const expanded = await expandAtFileToken(raw, '--filters-json');
62
+ if (expanded.trim() === '') {
63
+ return {};
64
+ }
65
+ return parseJsonArg(expanded, '--filters-json', {
66
+ requireObject: true,
67
+ fileFlagSuggestion: '--filters-json @./filters.json',
68
+ });
69
+ }
70
+ /**
71
+ * 把 CLI flag 收敛为 data-view filters;空值不下发,交由服务端默认。
72
+ * filter key 必须与 vps-service `dataViews.views[].filters[].key` 完全一致。
73
+ */
74
+ async function runProductDataView(viewId, filters, opts, cmd) {
75
+ const filtersJson = await resolveFiltersJson(opts.filtersJson);
76
+ const merged = await mergeShortcutParams({ ...filters, ...filtersJson }, opts);
77
+ const page = parsePositiveInt(opts.page, '--page', 1);
78
+ const pageSize = parsePositiveInt(opts.pageSize ?? opts.limit, opts.pageSize ? '--page-size' : '--limit', 100, 1000);
79
+ const groupBy = opts.groupBy?.trim() || undefined;
80
+ const serviceBaseUrl = resolveShortcutVpsServiceUrl(opts, cmd);
81
+ // --validate-only / --dry-run:仅本地预览将提交的请求载荷,不打网络、不查库。
82
+ if (opts.validateOnly === true || opts.dryRun === true) {
83
+ printResult(opts, {
84
+ validateOnly: true,
85
+ endpoint: `${serviceBaseUrl}/v1/data-view/query`,
86
+ payload: {
87
+ view_id: viewId,
88
+ filters: merged,
89
+ page,
90
+ page_size: pageSize,
91
+ ...(groupBy ? { group_by: groupBy } : {}),
92
+ },
93
+ });
94
+ return;
95
+ }
96
+ printResult(opts, await runVpsServiceDataView(viewId, merged, { page, pageSize, groupBy }, serviceBaseUrl));
97
+ }
98
+ /** Features · 产品与供应链 — `vps-work product +…`(data-view:库存/发货/齐套/机头)。 */
99
+ export function attachProductDataViewShortcuts(parent) {
100
+ attachSharedShortcutOptions(parent
101
+ .command('+inventory-by-vsn')
102
+ .alias('+vsn-inventory')
103
+ .description('按 VSN 精确查询商品、当前内部/在途库存和存储位置')
104
+ .requiredOption('--vsn <values>', 'VSN;多个值用逗号或空白分隔,最多100个')).action(async (opts, cmd) => {
105
+ const vsns = [...new Set(String(opts.vsn ?? '').split(/[,,\s]+/).map((value) => value.trim()).filter(Boolean))];
106
+ if (!vsns.length)
107
+ throw new VertuError('--vsn 不能为空。');
108
+ if (vsns.length > 100)
109
+ throw new VertuError('--vsn 单次最多 100 个。');
110
+ const serviceBaseUrl = resolveShortcutVpsServiceUrl(opts, cmd);
111
+ const body = { vsns };
112
+ if (opts.validateOnly === true || opts.dryRun === true) {
113
+ printResult(opts, {
114
+ validateOnly: true,
115
+ endpoint: `${serviceBaseUrl}/v1/product/inventory-by-vsn`,
116
+ required_scope: 'product.device_abnormal:read',
117
+ method: 'POST',
118
+ body,
119
+ });
120
+ return;
121
+ }
122
+ const response = await vpsServicePost('/v1/product/inventory-by-vsn', body, serviceBaseUrl, 'VSN 库存查询');
123
+ const result = response && typeof response === 'object' && !Array.isArray(response) && 'result' in response
124
+ ? response.result
125
+ : response;
126
+ printResult(opts, result);
127
+ });
128
+ attachSharedShortcutOptions(parent
129
+ .command('+inventory')
130
+ .description('库存数据:分仓/门店库存、门店明细(inventory-data)')
131
+ .option('--product-code <code>', '产品编码包含')
132
+ .option('--product-name <name>', '商品名称包含')
133
+ .option('--product-family <name>', '产品包含(如 产成品 / 半成品)')
134
+ .option('--product-category <name>', '商品大类包含')
135
+ .option('--product-class <name>', '商品分类包含')
136
+ .option('--product-subcategory <name>', '商品细类包含')
137
+ .option('--product-color <name>', '颜色包含(如 黑色 / 棕色 / 蓝色)')
138
+ .option('--product-material <name>', '材质包含(如 小牛皮 / 鳄鱼皮)')
139
+ .option('--product-color-material <name>', '颜色材质包含')
140
+ .option('--product-phone-version <name>', '手机版本包含(如 国内 / 海外)')
141
+ .option('--store-detail <name>', '门店明细包含')
142
+ .option('--group-by <dim>', '聚合维度:product|product_category|product_class|product_subcategory|product_color|product_material|store_detail|department')
143
+ .option('--page <n>', '页码,从 1 开始', '1')
144
+ .option('--page-size <n>', '每页行数,最大 1000')
145
+ .option('--limit <n>', '返回行数', '100')
146
+ .option('--filters-json <json>', '合并高级过滤 JSON;支持 @file,例如 {"product_category":"手机"}')).action(async (opts, cmd) => {
147
+ const filters = collectProductFilters(opts);
148
+ if (opts.storeDetail) {
149
+ filters.store_detail = opts.storeDetail;
150
+ }
151
+ await runProductDataView('inventory-data', filters, opts, cmd);
152
+ });
153
+ attachSharedShortcutOptions(parent
154
+ .command('+price')
155
+ .description('商品价格:标准价/海外价/国内价(product-price,不依赖库存)')
156
+ .option('--product-code <code>', '产品编码包含')
157
+ .option('--product-name <name>', '商品名称包含')
158
+ .option('--product-family <name>', '产品包含(如 产成品 / 半成品)')
159
+ .option('--product-category <name>', '商品大类包含')
160
+ .option('--product-class <name>', '商品分类包含')
161
+ .option('--product-subcategory <name>', '商品细类包含')
162
+ .option('--product-color <name>', '颜色包含(如 黑色 / 棕色 / 蓝色)')
163
+ .option('--product-material <name>', '材质包含(如 小牛皮 / 鳄鱼皮)')
164
+ .option('--product-color-material <name>', '颜色材质包含')
165
+ .option('--product-phone-version <name>', '手机版本包含(如 国内 / 海外)')
166
+ .option('--price-region <region>', '价格区域包含(standard / domestic / overseas)')
167
+ .option('--page <n>', '页码,从 1 开始', '1')
168
+ .option('--page-size <n>', '每页行数,最大 1000')
169
+ .option('--limit <n>', '返回行数', '100')
170
+ .option('--filters-json <json>', '合并高级过滤 JSON;支持 @file,例如 {"product_category":"手机"}')).action(async (opts, cmd) => {
171
+ await runProductDataView('product-price', collectProductFilters(opts), opts, cmd);
172
+ });
173
+ attachSharedShortcutOptions(parent
174
+ .command('+delivery')
175
+ .description('发货数据:出库单、发货数量、收货部门(delivery-data)')
176
+ .option('--product-code <code>', '产品编码包含')
177
+ .option('--product-name <name>', '商品名称包含(如 ALPHAFOLD / FOLD)')
178
+ .option('--product-family <name>', '产品包含(如 产成品 / 半成品)')
179
+ .option('--product-category <name>', '商品大类包含(如 手机)')
180
+ .option('--product-class <name>', '商品分类包含')
181
+ .option('--product-subcategory <name>', '商品细类包含(如 VERTU ALPHAFOLD)')
182
+ .option('--product-color <name>', '颜色包含')
183
+ .option('--product-material <name>', '材质包含(如 小牛皮 / 鳄鱼皮)')
184
+ .option('--product-color-material <name>', '颜色材质包含')
185
+ .option('--product-phone-version <name>', '手机版本包含(如 国内 / 海外)')
186
+ .option('--warehouse <name>', '发货仓/库位完整名称包含(如 发货仓 / 成都总仓)')
187
+ .option('--department <name>', '部门包含')
188
+ .option('--start-date <date>', '发货起始日期 YYYY-MM-DD')
189
+ .option('--end-date <date>', '发货结束日期 YYYY-MM-DD')
190
+ .option('--launch-start-date <date>', '上市起始日期 YYYY-MM-DD')
191
+ .option('--launch-end-date <date>', '上市结束日期 YYYY-MM-DD')
192
+ .option('--page <n>', '页码,从 1 开始', '1')
193
+ .option('--page-size <n>', '每页行数,最大 1000')
194
+ .option('--limit <n>', '返回行数', '100')
195
+ .option('--filters-json <json>', '合并高级过滤 JSON;支持 @file,例如 {"product_category":"手机","warehouse":"发货仓"}')).action(async (opts, cmd) => {
196
+ const filters = collectProductFilters(opts);
197
+ if (opts.warehouse) {
198
+ filters.warehouse = opts.warehouse;
199
+ }
200
+ if (opts.department) {
201
+ filters.department = opts.department;
202
+ }
203
+ if (opts.startDate) {
204
+ filters.start_date = opts.startDate;
205
+ }
206
+ if (opts.endDate) {
207
+ filters.end_date = opts.endDate;
208
+ }
209
+ if (opts.launchStartDate) {
210
+ filters.launch_start_date = opts.launchStartDate;
211
+ }
212
+ if (opts.launchEndDate) {
213
+ filters.launch_end_date = opts.launchEndDate;
214
+ }
215
+ await runProductDataView('delivery-data', filters, opts, cmd);
216
+ });
217
+ attachSharedShortcutOptions(parent
218
+ .command('+kit-materials')
219
+ .description('齐套物料:BOM 可用库存与商品分类(kit-materials)')
220
+ .option('--product-name <name>', '商品名称包含')
221
+ .option('--product-code <code>', '产品编码包含')
222
+ .option('--page <n>', '页码,从 1 开始', '1')
223
+ .option('--page-size <n>', '每页行数,最大 1000')
224
+ .option('--limit <n>', '返回行数', '100')
225
+ .option('--filters-json <json>', '合并高级过滤 JSON;支持 @file')).action(async (opts, cmd) => {
226
+ const filters = {};
227
+ if (opts.productName) {
228
+ filters.product_name = opts.productName;
229
+ }
230
+ if (opts.productCode) {
231
+ filters.product_code = opts.productCode;
232
+ }
233
+ await runProductDataView('kit-materials', filters, opts, cmd);
234
+ });
235
+ attachSharedShortcutOptions(parent
236
+ .command('+phone-head-inventory')
237
+ .description('机头库存:机头良品、内存、颜色与库存部门(phone-head-inventory)')
238
+ .option('--product-name <name>', '商品名称包含')
239
+ .option('--department <name>', '部门包含')
240
+ .option('--page <n>', '页码,从 1 开始', '1')
241
+ .option('--page-size <n>', '每页行数,最大 1000')
242
+ .option('--limit <n>', '返回行数', '100')
243
+ .option('--filters-json <json>', '合并高级过滤 JSON;支持 @file')).action(async (opts, cmd) => {
244
+ const filters = {};
245
+ if (opts.productName) {
246
+ filters.product_name = opts.productName;
247
+ }
248
+ if (opts.department) {
249
+ filters.department = opts.department;
250
+ }
251
+ await runProductDataView('phone-head-inventory', filters, opts, cmd);
252
+ });
253
+ }
@@ -0,0 +1,11 @@
1
+ import { Command } from 'commander';
2
+ import { attachProductAbnormalStatisticsShortcuts } from './abnormal-statistics.js';
3
+ import { attachProductDataViewShortcuts } from './data-view-shortcuts.js';
4
+ /** Features · 产品与供应链 — `vps-work product +…`(库存、价格、发货、异常设备等)。 */
5
+ export function buildProductCommand() {
6
+ const product = new Command('product')
7
+ .description('Product shortcuts: inventory, VSN inventory, price, delivery, materials and abnormal devices。');
8
+ attachProductDataViewShortcuts(product);
9
+ attachProductAbnormalStatisticsShortcuts(product);
10
+ return product;
11
+ }
@@ -0,0 +1,45 @@
1
+ import { Command } from 'commander';
2
+ import { attachSharedShortcutOptions, mergeShortcutParams, isValidateOnly, resolveShortcutVpsServiceUrl } from '../shortcut-common.js';
3
+ import { executeVpsServiceScript } from '../../core/sales-query-client.js';
4
+ export function buildPurchaseCommand() {
5
+ const parent = new Command('purchase').description('采购申请、采购订单及明细只读查询(独立权限)');
6
+ for (const [name, script, operation, description] of [
7
+ ['+requests', 'requests', 'list', 'VPS 采购申请列表'],
8
+ ['+request-detail', 'requests', 'detail', 'VPS 采购申请详情(含明细)'],
9
+ ['+request-lines', 'requests', 'lines', 'VPS 采购申请明细分页'],
10
+ ['+orders', 'orders', 'list', 'ERP 采购订单列表'],
11
+ ['+order-detail', 'orders', 'detail', 'ERP 采购订单详情'],
12
+ ['+order-lines', 'orders', 'lines', 'ERP 采购订单明细分页'],
13
+ ]) {
14
+ const cmd = parent.command(name).description(description).option('--document-number <number>', '完整单号,精确匹配');
15
+ if (operation === 'list')
16
+ cmd.option('--status <status>', '来源系统状态')
17
+ .option('--keyword <text>', '单号、申请事由或供应商关键词')
18
+ .option('--company-id <id>', '公司 ID').option('--start-date <YYYY-MM-DD>', '起始日期(上海时区,包含)')
19
+ .option('--end-date <YYYY-MM-DD>', '结束日期(上海时区,包含)');
20
+ else
21
+ cmd.option('--id <id>', script === 'requests' ? 'VPS 采购申请 UUID(与完整单号二选一)' : 'ERP 采购订单整数 ID(与完整单号二选一)');
22
+ if (operation !== 'detail')
23
+ cmd.option('--limit <n>', '每页条数,1–100').option('--offset <n>', '分页偏移');
24
+ attachSharedShortcutOptions(cmd).action(async (opts, command) => {
25
+ const params = { operation };
26
+ for (const [key, value] of Object.entries({
27
+ [script === 'requests' ? 'submission_id' : 'id']: opts.id,
28
+ document_number: opts.documentNumber, status: opts.status, keyword: opts.keyword, company_id: opts.companyId,
29
+ start_date: opts.startDate, end_date: opts.endDate, limit: opts.limit, offset: opts.offset,
30
+ }))
31
+ if (value !== undefined)
32
+ params[key] = value;
33
+ // New catalog-native skills must bypass the legacy Odoo shortcut allowlist.
34
+ const payload = await mergeShortcutParams(params, opts);
35
+ const baseUrl = resolveShortcutVpsServiceUrl(opts, command);
36
+ if (isValidateOnly(opts)) {
37
+ console.log(JSON.stringify({ validateOnly: true, source: 'vps-service-execute', endpoint: `${baseUrl}/v1/execute`, skill_key: 'vps-purchase', script_key: script, params: payload }, null, 2));
38
+ return;
39
+ }
40
+ const result = await executeVpsServiceScript('vps-purchase', script, payload, baseUrl);
41
+ console.log(typeof result === 'string' && opts.json === false ? result : JSON.stringify(result, null, 2));
42
+ });
43
+ }
44
+ return parent;
45
+ }