weaver-work-cli 0.1.0
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/LICENSE +21 -0
- package/README.md +422 -0
- package/dist/cmd/action.js +35 -0
- package/dist/cmd/commands/index.js +15 -0
- package/dist/cmd/config/index.js +64 -0
- package/dist/cmd/docs/index.js +56 -0
- package/dist/cmd/doctor/index.js +45 -0
- package/dist/cmd/e10/index.js +25 -0
- package/dist/cmd/index.js +18 -0
- package/dist/cmd/register.js +19 -0
- package/dist/cmd/root.js +36 -0
- package/dist/cmd/run/index.js +38 -0
- package/dist/cmd/setup/index.js +77 -0
- package/dist/cmd/skills/index.js +72 -0
- package/dist/core/config-store.js +43 -0
- package/dist/core/context.js +31 -0
- package/dist/core/errors.js +60 -0
- package/dist/core/hooks.js +41 -0
- package/dist/core/logger.js +27 -0
- package/dist/core/output.js +25 -0
- package/dist/core/write-with-retry.js +21 -0
- package/dist/index.js +11 -0
- package/dist/internal/e10/auth/commands.js +594 -0
- package/dist/internal/e10/auth/crypto.js +126 -0
- package/dist/internal/e10/auth/paths.js +12 -0
- package/dist/internal/e10/auth/session.js +449 -0
- package/dist/internal/e10/client.js +104 -0
- package/dist/internal/e10/context.js +20 -0
- package/dist/internal/e10/index.js +2 -0
- package/dist/internal/e10/types.js +1 -0
- package/dist/internal/skills/install.js +99 -0
- package/dist/internal/skills/reader.js +119 -0
- package/dist/services/command-runner.js +44 -0
- package/dist/shortcuts/index.js +4 -0
- package/dist/shortcuts/invoice/continuation.js +70 -0
- package/dist/shortcuts/invoice/errors.js +59 -0
- package/dist/shortcuts/invoice/host.js +337 -0
- package/dist/shortcuts/invoice/index.js +301 -0
- package/dist/shortcuts/invoice/manifest.js +288 -0
- package/dist/shortcuts/invoice/operations/add.js +106 -0
- package/dist/shortcuts/invoice/operations/delete.js +92 -0
- package/dist/shortcuts/invoice/operations/download.js +16 -0
- package/dist/shortcuts/invoice/operations/get.js +7 -0
- package/dist/shortcuts/invoice/operations/import.js +128 -0
- package/dist/shortcuts/invoice/operations/list.js +12 -0
- package/dist/shortcuts/invoice/operations/ocr-preview.js +40 -0
- package/dist/shortcuts/invoice/operations/registry.js +50 -0
- package/dist/shortcuts/invoice/operations/shared.js +640 -0
- package/dist/shortcuts/invoice/operations/types.js +1 -0
- package/dist/shortcuts/invoice/operations/update.js +109 -0
- package/dist/shortcuts/invoice/operations/upload.js +8 -0
- package/dist/shortcuts/invoice/operations/validate-preview.js +39 -0
- package/dist/shortcuts/invoice/operations.js +8 -0
- package/dist/types.js +1 -0
- package/docs/SKILL.md +36 -0
- package/docs/_catalog.md +12 -0
- package/docs/agent-invoice.md +53 -0
- package/docs/e10-auth.md +51 -0
- package/docs/invoice.md +99 -0
- package/package.json +51 -0
- package/scripts/clean.mjs +4 -0
- package/scripts/package-skill.mjs +164 -0
- package/scripts/sync-docs.mjs +17 -0
- package/skill-template/business-info.json +4 -0
- package/skill-template/domains/invoice.md +23 -0
- package/skill-template/domains/shared.md +8 -0
- package/skill-template/master-skill-template.md +29 -0
- package/skill-template/skill-template.md +60 -0
- package/skills/weaver-work-cli-invoice/SKILL.md +103 -0
- package/skills/weaver-work-cli-invoice/product.json +8 -0
- package/skills/weaver-work-cli-invoice/references/invoice-add.md +18 -0
- package/skills/weaver-work-cli-invoice/references/invoice-agent-entry.md +38 -0
- package/skills/weaver-work-cli-invoice/references/invoice-delete.md +20 -0
- package/skills/weaver-work-cli-invoice/references/invoice-detail.md +29 -0
- package/skills/weaver-work-cli-invoice/references/invoice-disabled-capabilities.md +26 -0
- package/skills/weaver-work-cli-invoice/references/invoice-download.md +22 -0
- package/skills/weaver-work-cli-invoice/references/invoice-enterprise-list.md +35 -0
- package/skills/weaver-work-cli-invoice/references/invoice-file-upload.md +28 -0
- package/skills/weaver-work-cli-invoice/references/invoice-import.md +28 -0
- package/skills/weaver-work-cli-invoice/references/invoice-ocr-preview.md +33 -0
- package/skills/weaver-work-cli-invoice/references/invoice-personal-list.md +47 -0
- package/skills/weaver-work-cli-invoice/references/invoice-update.md +38 -0
- package/skills/weaver-work-cli-invoice/references/invoice-validation-preview.md +26 -0
- package/skills/weaver-work-cli-shared/SKILL.md +53 -0
- package/skills/weaver-work-cli-shared/references/e10-auth-and-session.md +83 -0
- package/skills/weaver-work-cli-shared/references/high-risk-write.md +36 -0
- package/skills/weaver-work-cli-shared/references/json-output-contract.md +61 -0
- package/skills/weaver-work-cli-shared/references/weaver-work-cli-installation.md +61 -0
- package/templates/shortcut/index.ts.tpl +19 -0
- package/templates/shortcut/operations/ping.ts.tpl +7 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { requireE10Session } from '../../internal/e10/context.js';
|
|
4
|
+
import { getActiveProfile } from '../../internal/e10/auth/session.js';
|
|
5
|
+
import { InvoiceCliError, invoiceAuthError, invoiceNetworkError, invoiceValidationError, } from './errors.js';
|
|
6
|
+
const MAX_DOWNLOAD_BYTES = 50 * 1024 * 1024;
|
|
7
|
+
const TOKEN_SERVICE = 'cn.weaver.weaver-work-cli.invoice-token';
|
|
8
|
+
function assertPath(requestPath) {
|
|
9
|
+
if (typeof requestPath !== 'string' || !requestPath.startsWith('/') || requestPath.startsWith('//')) {
|
|
10
|
+
throw invoiceValidationError('path_invalid', '请求路径必须是相对绝对路径');
|
|
11
|
+
}
|
|
12
|
+
if (/^https?:/i.test(requestPath) || /[\r\n]/.test(requestPath) || requestPath.includes('/../')) {
|
|
13
|
+
throw invoiceValidationError('path_invalid', '请求路径不允许跨域或路径逃逸');
|
|
14
|
+
}
|
|
15
|
+
if (!requestPath.startsWith('/api/')) {
|
|
16
|
+
throw invoiceValidationError('path_not_allowed', '业票通只能调用 /api/ 路径');
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function withQuery(baseUrl, requestPath, query = {}) {
|
|
20
|
+
const url = new URL(`${baseUrl}${requestPath}`);
|
|
21
|
+
for (const [key, value] of Object.entries(query)) {
|
|
22
|
+
if (value === undefined || value === null)
|
|
23
|
+
continue;
|
|
24
|
+
if (Array.isArray(value)) {
|
|
25
|
+
for (const item of value)
|
|
26
|
+
url.searchParams.append(key, String(item));
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
url.searchParams.set(key, String(value));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return url;
|
|
33
|
+
}
|
|
34
|
+
async function fetchWithTimeout(url, options, timeoutMs) {
|
|
35
|
+
const controller = new AbortController();
|
|
36
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
37
|
+
try {
|
|
38
|
+
return await fetch(url, { ...options, signal: controller.signal });
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
42
|
+
throw invoiceNetworkError('timeout', `请求超时(${timeoutMs}ms)`, { retryable: false });
|
|
43
|
+
}
|
|
44
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
45
|
+
throw invoiceNetworkError('transport', `请求失败:${message}`, { retryable: true });
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
clearTimeout(timer);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function responseRoot(value) {
|
|
52
|
+
return value?.res_base || value?.data?.res_base || value;
|
|
53
|
+
}
|
|
54
|
+
function responseMessage(value) {
|
|
55
|
+
const root = responseRoot(value);
|
|
56
|
+
return typeof root?.message === 'string'
|
|
57
|
+
? root.message
|
|
58
|
+
: typeof root?.msg === 'string'
|
|
59
|
+
? root.msg
|
|
60
|
+
: '泛微接口返回业务失败';
|
|
61
|
+
}
|
|
62
|
+
function responseCode(value) {
|
|
63
|
+
const root = responseRoot(value);
|
|
64
|
+
return root?.ret ?? root?.code;
|
|
65
|
+
}
|
|
66
|
+
function tokenAccount(session) {
|
|
67
|
+
return createHash('sha256')
|
|
68
|
+
.update(`${session.profile}\n${session.baseUrl}\n${session.userId}`)
|
|
69
|
+
.digest('hex');
|
|
70
|
+
}
|
|
71
|
+
function createInvoiceTokenStore(session) {
|
|
72
|
+
const account = tokenAccount(session);
|
|
73
|
+
let entryPromise = null;
|
|
74
|
+
async function entry() {
|
|
75
|
+
if (!entryPromise) {
|
|
76
|
+
entryPromise = import('@napi-rs/keyring').then(({ Entry }) => new Entry(TOKEN_SERVICE, account));
|
|
77
|
+
}
|
|
78
|
+
return entryPromise;
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
async load() {
|
|
82
|
+
try {
|
|
83
|
+
const value = await (await entry()).getPassword();
|
|
84
|
+
return typeof value === 'string' && value.trim() ? value : null;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
async save(token) {
|
|
91
|
+
if (!token.trim())
|
|
92
|
+
return false;
|
|
93
|
+
try {
|
|
94
|
+
await (await entry()).setPassword(token);
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
async clear() {
|
|
102
|
+
try {
|
|
103
|
+
await (await entry()).deletePassword();
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function inferProfile(ctx) {
|
|
113
|
+
const options = ctx.getGlobalOptions();
|
|
114
|
+
return options.profile || getActiveProfile();
|
|
115
|
+
}
|
|
116
|
+
function requestHeaders(session, invoiceToken) {
|
|
117
|
+
const headers = {
|
|
118
|
+
Accept: 'application/json, text/plain, */*',
|
|
119
|
+
Cookie: session.cookieHeader(),
|
|
120
|
+
eteamsid: session.eteamsId,
|
|
121
|
+
'User-Agent': session.uaHeader(),
|
|
122
|
+
Origin: session.getBaseUrl(),
|
|
123
|
+
};
|
|
124
|
+
if (invoiceToken)
|
|
125
|
+
headers.token = invoiceToken;
|
|
126
|
+
return headers;
|
|
127
|
+
}
|
|
128
|
+
export function createInvoiceHost(ctx) {
|
|
129
|
+
let e10Session = null;
|
|
130
|
+
let sessionContext = null;
|
|
131
|
+
let invoiceToken = null;
|
|
132
|
+
let tokenStore = null;
|
|
133
|
+
async function ensureSession() {
|
|
134
|
+
if (!e10Session) {
|
|
135
|
+
e10Session = await requireE10Session(ctx);
|
|
136
|
+
sessionContext = {
|
|
137
|
+
baseUrl: e10Session.getBaseUrl(),
|
|
138
|
+
profile: inferProfile(ctx),
|
|
139
|
+
userId: e10Session.userId,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
return e10Session;
|
|
143
|
+
}
|
|
144
|
+
async function getSession() {
|
|
145
|
+
await ensureSession();
|
|
146
|
+
return sessionContext;
|
|
147
|
+
}
|
|
148
|
+
async function ensureInvoiceToken(timeoutMs = 30_000) {
|
|
149
|
+
await ensureSession();
|
|
150
|
+
const context = await getSession();
|
|
151
|
+
tokenStore ||= createInvoiceTokenStore(context);
|
|
152
|
+
if (invoiceToken)
|
|
153
|
+
return invoiceToken;
|
|
154
|
+
invoiceToken = await tokenStore.load();
|
|
155
|
+
if (invoiceToken)
|
|
156
|
+
return invoiceToken;
|
|
157
|
+
const tokenResponse = await invoiceRequest({
|
|
158
|
+
method: 'GET',
|
|
159
|
+
path: '/api/inc/proxy/getToken',
|
|
160
|
+
query: { appType: 1 },
|
|
161
|
+
needsInvoiceToken: false,
|
|
162
|
+
timeoutMs,
|
|
163
|
+
});
|
|
164
|
+
const token = tokenResponse?.data?.token;
|
|
165
|
+
if (typeof token !== 'string' || !token.trim()) {
|
|
166
|
+
throw invoiceAuthError('invoice_token_missing', '未取得业票通业务 Token');
|
|
167
|
+
}
|
|
168
|
+
const saved = await tokenStore.save(token);
|
|
169
|
+
if (!saved) {
|
|
170
|
+
throw invoiceAuthError('invoice_token_cache_failed', '发票 Token 安全缓存写入失败,已停止业务请求');
|
|
171
|
+
}
|
|
172
|
+
invoiceToken = token;
|
|
173
|
+
return token;
|
|
174
|
+
}
|
|
175
|
+
async function ensureContext(needsInvoiceToken, timeoutMs, path) {
|
|
176
|
+
await ensureSession();
|
|
177
|
+
const tokenEndpoint = path === '/api/inc/proxy/getToken' || path === '/api/app/inc/proxy/getToken';
|
|
178
|
+
if (tokenEndpoint)
|
|
179
|
+
return undefined;
|
|
180
|
+
if (!needsInvoiceToken) {
|
|
181
|
+
// 兼容调用方参数,但业务路径仍强制补齐发票 Token。
|
|
182
|
+
}
|
|
183
|
+
return ensureInvoiceToken(timeoutMs);
|
|
184
|
+
}
|
|
185
|
+
async function invoiceRequest(request) {
|
|
186
|
+
const { method = 'POST', path, query, body, needsInvoiceToken = true, timeoutMs = 30_000, } = request;
|
|
187
|
+
assertPath(path);
|
|
188
|
+
const token = await ensureContext(needsInvoiceToken, timeoutMs, path);
|
|
189
|
+
const session = await ensureSession();
|
|
190
|
+
const url = withQuery(session.getBaseUrl(), path, query);
|
|
191
|
+
const headers = requestHeaders(session, token);
|
|
192
|
+
const init = { method, headers, redirect: 'manual' };
|
|
193
|
+
if (body !== undefined) {
|
|
194
|
+
headers['Content-Type'] = 'application/json;charset=UTF-8';
|
|
195
|
+
init.body = JSON.stringify(body);
|
|
196
|
+
}
|
|
197
|
+
const response = await fetchWithTimeout(url, init, timeoutMs);
|
|
198
|
+
const text = await response.text();
|
|
199
|
+
if (response.status === 401 || response.status === 302) {
|
|
200
|
+
throw invoiceAuthError('session_expired', 'E10 登录态已失效,请先向用户确认 E10 登录域名,再运行 weaver-work-cli auth login --base-url <域名> 完成登录', { httpStatus: response.status });
|
|
201
|
+
}
|
|
202
|
+
let value = {};
|
|
203
|
+
try {
|
|
204
|
+
value = text ? JSON.parse(text) : {};
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
if (response.status < 200 || response.status >= 300) {
|
|
208
|
+
throw new InvoiceCliError('api', 'http_error', `泛微接口 HTTP ${response.status}`, {
|
|
209
|
+
code: response.status,
|
|
210
|
+
details: { status: response.status },
|
|
211
|
+
exitCode: 1,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
throw invoiceNetworkError('protocol', `接口返回不是合法 JSON(HTTP ${response.status})`, { retryable: false });
|
|
215
|
+
}
|
|
216
|
+
if (response.status < 200 || response.status >= 300) {
|
|
217
|
+
throw new InvoiceCliError('api', 'http_error', `泛微接口 HTTP ${response.status}`, {
|
|
218
|
+
code: response.status,
|
|
219
|
+
details: { status: response.status, message: responseMessage(value) },
|
|
220
|
+
exitCode: 1,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
const code = responseCode(value);
|
|
224
|
+
if (code !== undefined && code !== 0 && code !== 200) {
|
|
225
|
+
throw new InvoiceCliError('api', 'business_error', responseMessage(value), {
|
|
226
|
+
code: String(code),
|
|
227
|
+
details: { ret: code },
|
|
228
|
+
exitCode: 1,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
return value;
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
invoiceRequest,
|
|
235
|
+
async invoiceMultipartRequest(request) {
|
|
236
|
+
const { method = 'POST', path, query, fields = {}, file, needsInvoiceToken = true, timeoutMs = 30_000, } = request;
|
|
237
|
+
if (!file && !fields.url) {
|
|
238
|
+
throw invoiceValidationError('input_invalid', 'multipart 请求需要文件或 URL');
|
|
239
|
+
}
|
|
240
|
+
assertPath(path);
|
|
241
|
+
const token = await ensureContext(needsInvoiceToken, timeoutMs, path);
|
|
242
|
+
const session = await ensureSession();
|
|
243
|
+
const form = new FormData();
|
|
244
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
245
|
+
if (value === undefined || value === null)
|
|
246
|
+
continue;
|
|
247
|
+
form.append(key, typeof value === 'string' ? value : JSON.stringify(value));
|
|
248
|
+
}
|
|
249
|
+
if (file) {
|
|
250
|
+
const content = await readFile(file.path);
|
|
251
|
+
form.append('file', new Blob([content], { type: file.mimeType || 'application/octet-stream' }), file.name);
|
|
252
|
+
}
|
|
253
|
+
const response = await fetchWithTimeout(withQuery(session.getBaseUrl(), path, query), {
|
|
254
|
+
method,
|
|
255
|
+
headers: requestHeaders(session, token),
|
|
256
|
+
body: form,
|
|
257
|
+
redirect: 'manual',
|
|
258
|
+
}, timeoutMs);
|
|
259
|
+
const text = await response.text();
|
|
260
|
+
if (response.status === 401 || response.status === 302) {
|
|
261
|
+
throw invoiceAuthError('session_expired', 'E10 登录态已失效,请先向用户确认 E10 登录域名,再运行 weaver-work-cli auth login --base-url <域名> 完成登录', { httpStatus: response.status });
|
|
262
|
+
}
|
|
263
|
+
let value = {};
|
|
264
|
+
try {
|
|
265
|
+
value = text ? JSON.parse(text) : {};
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
throw invoiceNetworkError('protocol', `接口返回不是合法 JSON(HTTP ${response.status})`, { retryable: false });
|
|
269
|
+
}
|
|
270
|
+
if (response.status < 200 || response.status >= 300) {
|
|
271
|
+
throw new InvoiceCliError('api', 'http_error', `泛微接口 HTTP ${response.status}`, {
|
|
272
|
+
code: response.status,
|
|
273
|
+
details: { status: response.status, message: responseMessage(value) },
|
|
274
|
+
exitCode: 1,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
const code = responseCode(value);
|
|
278
|
+
if (code !== undefined && code !== 0 && code !== 200) {
|
|
279
|
+
throw new InvoiceCliError('api', 'business_error', responseMessage(value), {
|
|
280
|
+
code: String(code),
|
|
281
|
+
details: { ret: code },
|
|
282
|
+
exitCode: 1,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
return value;
|
|
286
|
+
},
|
|
287
|
+
async invoiceDownload({ id, fid, needLog = false, timeoutMs = 30_000 }) {
|
|
288
|
+
const token = await ensureContext(true, timeoutMs, '/api/inc/file/download');
|
|
289
|
+
const session = await ensureSession();
|
|
290
|
+
const url = withQuery(session.getBaseUrl(), '/api/inc/file/download', { id, fid, needLog });
|
|
291
|
+
const response = await fetchWithTimeout(url, {
|
|
292
|
+
method: 'GET',
|
|
293
|
+
headers: {
|
|
294
|
+
...requestHeaders(session, token),
|
|
295
|
+
Accept: 'application/octet-stream, application/pdf, */*',
|
|
296
|
+
},
|
|
297
|
+
redirect: 'manual',
|
|
298
|
+
}, timeoutMs);
|
|
299
|
+
if (response.status === 401 || response.status === 302) {
|
|
300
|
+
throw invoiceAuthError('session_expired', 'E10 登录态已失效,请先向用户确认 E10 登录域名,再运行 weaver-work-cli auth login --base-url <域名> 完成登录', { httpStatus: response.status });
|
|
301
|
+
}
|
|
302
|
+
const contentType = response.headers.get('content-type') || 'application/octet-stream';
|
|
303
|
+
const declaredLength = Number(response.headers.get('content-length'));
|
|
304
|
+
if (Number.isFinite(declaredLength) && declaredLength > MAX_DOWNLOAD_BYTES) {
|
|
305
|
+
throw invoiceNetworkError('response_too_large', `下载文件超过 ${MAX_DOWNLOAD_BYTES} 字节限制`, { retryable: false });
|
|
306
|
+
}
|
|
307
|
+
const content = Buffer.from(await response.arrayBuffer());
|
|
308
|
+
const looksLikeJson = /^[\s\r\n]*[\[{]/u.test(content.subarray(0, 64).toString('utf8'));
|
|
309
|
+
if (response.status < 200 || response.status >= 300 || contentType.includes('json') || looksLikeJson) {
|
|
310
|
+
let value = {};
|
|
311
|
+
try {
|
|
312
|
+
value = JSON.parse(content.toString('utf8'));
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
value = {};
|
|
316
|
+
}
|
|
317
|
+
throw new InvoiceCliError('api', 'download_failed', responseMessage(value) || `下载失败(HTTP ${response.status})`, {
|
|
318
|
+
code: response.status,
|
|
319
|
+
details: { status: response.status, ret: responseCode(value) },
|
|
320
|
+
exitCode: 1,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
if (!content.length) {
|
|
324
|
+
throw invoiceNetworkError('protocol', '下载接口返回空文件', { retryable: false });
|
|
325
|
+
}
|
|
326
|
+
if (content.length > MAX_DOWNLOAD_BYTES) {
|
|
327
|
+
throw invoiceNetworkError('response_too_large', `下载文件超过 ${MAX_DOWNLOAD_BYTES} 字节限制`, { retryable: false });
|
|
328
|
+
}
|
|
329
|
+
return { content, contentType };
|
|
330
|
+
},
|
|
331
|
+
getSession,
|
|
332
|
+
invalidateToken() {
|
|
333
|
+
invoiceToken = null;
|
|
334
|
+
void tokenStore?.clear();
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { ExitCode, isCliError, toErrorEnvelope } from '../../core/errors.js';
|
|
3
|
+
import { createInvoiceContinuationSigner } from './continuation.js';
|
|
4
|
+
import { createInvoiceHost } from './host.js';
|
|
5
|
+
import { invoiceManifest } from './manifest.js';
|
|
6
|
+
import { executeInvoiceOperation } from './operations.js';
|
|
7
|
+
import { invoiceValidationError } from './errors.js';
|
|
8
|
+
function asOptions(value) {
|
|
9
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
10
|
+
}
|
|
11
|
+
async function readStdin() {
|
|
12
|
+
const chunks = [];
|
|
13
|
+
for await (const chunk of process.stdin) {
|
|
14
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
15
|
+
}
|
|
16
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
17
|
+
}
|
|
18
|
+
async function inputFrom(options, extra = {}) {
|
|
19
|
+
const opts = asOptions(options);
|
|
20
|
+
let input = {};
|
|
21
|
+
if (opts.input) {
|
|
22
|
+
let raw;
|
|
23
|
+
try {
|
|
24
|
+
raw = opts.input === '-' ? await readStdin() : await readFile(String(opts.input), 'utf8');
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
throw invoiceValidationError('input_unreadable', `无法读取 --input:${String(opts.input)}`);
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const parsed = JSON.parse(raw);
|
|
31
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
32
|
+
throw new Error('input must be an object');
|
|
33
|
+
}
|
|
34
|
+
input = parsed;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
throw invoiceValidationError('input_invalid', '--input 不是合法 JSON 对象');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
...input,
|
|
42
|
+
...Object.fromEntries(Object.entries(extra).filter(([, value]) => value !== undefined && value !== '')),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function intOption(value) {
|
|
46
|
+
if (value === undefined || value === null || value === '')
|
|
47
|
+
return undefined;
|
|
48
|
+
const parsed = Number(value);
|
|
49
|
+
return Number.isInteger(parsed) ? parsed : undefined;
|
|
50
|
+
}
|
|
51
|
+
function successEnvelope(operation, result) {
|
|
52
|
+
return {
|
|
53
|
+
schemaVersion: 1,
|
|
54
|
+
ok: true,
|
|
55
|
+
operation,
|
|
56
|
+
data: result.data,
|
|
57
|
+
meta: { status: result.status },
|
|
58
|
+
warnings: [],
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function failureEnvelope(error) {
|
|
62
|
+
const structured = error;
|
|
63
|
+
return {
|
|
64
|
+
schemaVersion: 1,
|
|
65
|
+
ok: false,
|
|
66
|
+
...(structured?.partialData === undefined ? {} : { data: structured.partialData }),
|
|
67
|
+
error: toErrorEnvelope(error),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
async function executeAndPrint(ctx, operation, input) {
|
|
71
|
+
try {
|
|
72
|
+
const result = await executeInvoiceOperation(operation, input, {
|
|
73
|
+
host: createInvoiceHost(ctx),
|
|
74
|
+
continuation: createInvoiceContinuationSigner(ctx),
|
|
75
|
+
});
|
|
76
|
+
ctx.output.write(successEnvelope(operation, result), { json: true });
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
console.error(JSON.stringify(failureEnvelope(error), null, 2));
|
|
80
|
+
process.exitCode = isCliError(error) ? error.exitCode : ExitCode.GeneralError;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
export const invoiceShortcut = {
|
|
84
|
+
name: 'invoice',
|
|
85
|
+
description: 'E10 业票通发票管理 CLI',
|
|
86
|
+
category: 'shortcut',
|
|
87
|
+
register({ program, action }) {
|
|
88
|
+
const invoice = program.command('invoice').description('E10 业票通发票管理');
|
|
89
|
+
invoice.command('schema')
|
|
90
|
+
.description('Print invoice operation manifest for agents')
|
|
91
|
+
.action(action((ctx) => {
|
|
92
|
+
ctx.output.write(invoiceManifest, { json: true });
|
|
93
|
+
}));
|
|
94
|
+
invoice.command('run')
|
|
95
|
+
.description('Run an invoice operation with JSON input')
|
|
96
|
+
.argument('<operation>', 'operation name, e.g. invoice.list')
|
|
97
|
+
.option('--input <path>', 'JSON input file, or - for stdin')
|
|
98
|
+
.action(action(async (ctx, operation, options) => {
|
|
99
|
+
await executeAndPrint(ctx, String(operation), await inputFrom(options));
|
|
100
|
+
}));
|
|
101
|
+
invoice.command('list')
|
|
102
|
+
.description('List personal invoice folder entries')
|
|
103
|
+
.option('--input <path>', 'JSON input file, or - for stdin')
|
|
104
|
+
.option('--content <keyword>', 'keyword search')
|
|
105
|
+
.option('--page-size <size>', 'page size')
|
|
106
|
+
.option('--start-pos <offset>', 'start position')
|
|
107
|
+
.option('--sreim <status>', 'reimbursement status: 0 all, 1 in reimbursement, 2 reimbursed, 3 unreimbursed, 4 non-reimbursable')
|
|
108
|
+
.action(action(async (ctx, options) => {
|
|
109
|
+
const opts = asOptions(options);
|
|
110
|
+
await executeAndPrint(ctx, 'invoice.list', await inputFrom(options, {
|
|
111
|
+
content: opts.content,
|
|
112
|
+
page_size: intOption(opts.pageSize),
|
|
113
|
+
start_pos: intOption(opts.startPos),
|
|
114
|
+
sreim: intOption(opts.sreim),
|
|
115
|
+
}));
|
|
116
|
+
}));
|
|
117
|
+
const enterprise = invoice.command('enterprise').description('Enterprise invoice folder commands');
|
|
118
|
+
enterprise.command('list')
|
|
119
|
+
.description('List enterprise invoice folder entries')
|
|
120
|
+
.option('--input <path>', 'JSON input file, or - for stdin')
|
|
121
|
+
.option('--content <keyword>', 'keyword search')
|
|
122
|
+
.option('--page-size <size>', 'page size')
|
|
123
|
+
.option('--start-pos <offset>', 'start position')
|
|
124
|
+
.option('--sreim <status>', 'reimbursement status: 0 all, 1 in reimbursement, 2 reimbursed, 3 unreimbursed, 4 non-reimbursable')
|
|
125
|
+
.action(action(async (ctx, options) => {
|
|
126
|
+
const opts = asOptions(options);
|
|
127
|
+
await executeAndPrint(ctx, 'invoice.enterprise.list', await inputFrom(options, {
|
|
128
|
+
content: opts.content,
|
|
129
|
+
page_size: intOption(opts.pageSize),
|
|
130
|
+
start_pos: intOption(opts.startPos),
|
|
131
|
+
sreim: intOption(opts.sreim),
|
|
132
|
+
}));
|
|
133
|
+
}));
|
|
134
|
+
invoice.command('get')
|
|
135
|
+
.description('Get invoice detail by fid or number')
|
|
136
|
+
.option('--input <path>', 'JSON input file, or - for stdin')
|
|
137
|
+
.option('--fid <fid>', 'invoice fid')
|
|
138
|
+
.option('--number <number>', 'invoice number')
|
|
139
|
+
.action(action(async (ctx, options) => {
|
|
140
|
+
const opts = asOptions(options);
|
|
141
|
+
await executeAndPrint(ctx, 'invoice.get', await inputFrom(options, {
|
|
142
|
+
fid: opts.fid,
|
|
143
|
+
number: opts.number,
|
|
144
|
+
}));
|
|
145
|
+
}));
|
|
146
|
+
invoice.command('upload')
|
|
147
|
+
.description('Upload an invoice file for OCR')
|
|
148
|
+
.option('--input <path>', 'JSON input file, or - for stdin')
|
|
149
|
+
.option('--file <path>', 'local invoice file')
|
|
150
|
+
.option('--url <url>', 'server-accessible file URL')
|
|
151
|
+
.action(action(async (ctx, options) => {
|
|
152
|
+
const opts = asOptions(options);
|
|
153
|
+
await executeAndPrint(ctx, 'invoice.upload', await inputFrom(options, {
|
|
154
|
+
file: opts.file,
|
|
155
|
+
url: opts.url,
|
|
156
|
+
}));
|
|
157
|
+
}));
|
|
158
|
+
const ocr = invoice.command('ocr').description('Invoice OCR commands');
|
|
159
|
+
ocr.command('preview')
|
|
160
|
+
.description('Preview OCR result without saving an invoice')
|
|
161
|
+
.option('--input <path>', 'JSON input file, or - for stdin')
|
|
162
|
+
.option('--file <path>', 'local invoice file')
|
|
163
|
+
.option('--url <url>', 'server-accessible file URL')
|
|
164
|
+
.action(action(async (ctx, options) => {
|
|
165
|
+
const opts = asOptions(options);
|
|
166
|
+
await executeAndPrint(ctx, 'invoice.ocr.preview', await inputFrom(options, {
|
|
167
|
+
file: opts.file,
|
|
168
|
+
url: opts.url,
|
|
169
|
+
}));
|
|
170
|
+
}));
|
|
171
|
+
const validate = invoice.command('validate').description('Invoice validation commands');
|
|
172
|
+
validate.command('preview')
|
|
173
|
+
.description('Validate an existing invoice without saving validation result')
|
|
174
|
+
.option('--input <path>', 'JSON input file, or - for stdin')
|
|
175
|
+
.option('--fid <fid>', 'invoice fid')
|
|
176
|
+
.option('--scope <scope>', 'personal or enterprise')
|
|
177
|
+
.action(action(async (ctx, options) => {
|
|
178
|
+
const opts = asOptions(options);
|
|
179
|
+
await executeAndPrint(ctx, 'invoice.validate.preview', await inputFrom(options, {
|
|
180
|
+
fid: opts.fid,
|
|
181
|
+
scope: opts.scope,
|
|
182
|
+
}));
|
|
183
|
+
}));
|
|
184
|
+
const importCommand = invoice.command('import').description('Invoice file import commands');
|
|
185
|
+
importCommand.command('prepare')
|
|
186
|
+
.description('Prepare a confirmed invoice file import')
|
|
187
|
+
.option('--input <path>', 'JSON input file, or - for stdin')
|
|
188
|
+
.option('--file <path>', 'local invoice file')
|
|
189
|
+
.option('--validate', 'request validation before import')
|
|
190
|
+
.option('--no-validate', 'skip validation before import')
|
|
191
|
+
.option('--sync-to-oa', 'sync imported invoice to OA where supported')
|
|
192
|
+
.option('--no-sync-to-oa', 'do not sync imported invoice to OA where supported')
|
|
193
|
+
.action(action(async (ctx, options) => {
|
|
194
|
+
const opts = asOptions(options);
|
|
195
|
+
await executeAndPrint(ctx, 'invoice.import.prepare', await inputFrom(options, {
|
|
196
|
+
file: opts.file,
|
|
197
|
+
validate: typeof opts.validate === 'boolean' ? opts.validate : undefined,
|
|
198
|
+
syncToOa: typeof opts.syncToOa === 'boolean' ? opts.syncToOa : undefined,
|
|
199
|
+
}));
|
|
200
|
+
}));
|
|
201
|
+
importCommand.command('apply')
|
|
202
|
+
.description('Apply a prepared invoice file import')
|
|
203
|
+
.option('--input <path>', 'JSON input file, or - for stdin')
|
|
204
|
+
.option('--file <path>', 'local invoice file')
|
|
205
|
+
.option('--continuation <token>', 'continuation token from prepare')
|
|
206
|
+
.option('--yes', 'confirm the write operation')
|
|
207
|
+
.action(action(async (ctx, options) => {
|
|
208
|
+
const opts = asOptions(options);
|
|
209
|
+
await executeAndPrint(ctx, 'invoice.import.apply', await inputFrom(options, {
|
|
210
|
+
file: opts.file,
|
|
211
|
+
continuation: opts.continuation,
|
|
212
|
+
confirm: opts.yes === true ? true : undefined,
|
|
213
|
+
}));
|
|
214
|
+
}));
|
|
215
|
+
const add = invoice.command('add').description('Manual invoice add commands');
|
|
216
|
+
add.command('prepare')
|
|
217
|
+
.description('Prepare a confirmed manual invoice add')
|
|
218
|
+
.option('--input <path>', 'JSON input file, or - for stdin')
|
|
219
|
+
.option('--scope <scope>', 'personal or enterprise')
|
|
220
|
+
.action(action(async (ctx, options) => {
|
|
221
|
+
const opts = asOptions(options);
|
|
222
|
+
await executeAndPrint(ctx, 'invoice.add.prepare', await inputFrom(options, { scope: opts.scope }));
|
|
223
|
+
}));
|
|
224
|
+
add.command('apply')
|
|
225
|
+
.description('Apply a prepared manual invoice add')
|
|
226
|
+
.option('--input <path>', 'JSON input file, or - for stdin')
|
|
227
|
+
.option('--continuation <token>', 'continuation token from prepare')
|
|
228
|
+
.option('--yes', 'confirm the write operation')
|
|
229
|
+
.action(action(async (ctx, options) => {
|
|
230
|
+
const opts = asOptions(options);
|
|
231
|
+
await executeAndPrint(ctx, 'invoice.add.apply', await inputFrom(options, {
|
|
232
|
+
continuation: opts.continuation,
|
|
233
|
+
confirm: opts.yes === true ? true : undefined,
|
|
234
|
+
}));
|
|
235
|
+
}));
|
|
236
|
+
const update = invoice.command('update').description('Invoice update commands');
|
|
237
|
+
update.command('prepare')
|
|
238
|
+
.description('Prepare an invoice update')
|
|
239
|
+
.option('--input <path>', 'JSON input file, or - for stdin')
|
|
240
|
+
.option('--fid <fid>', 'invoice fid')
|
|
241
|
+
.option('--scope <scope>', 'personal or enterprise')
|
|
242
|
+
.action(action(async (ctx, options) => {
|
|
243
|
+
const opts = asOptions(options);
|
|
244
|
+
await executeAndPrint(ctx, 'invoice.update.prepare', await inputFrom(options, {
|
|
245
|
+
fid: opts.fid,
|
|
246
|
+
scope: opts.scope,
|
|
247
|
+
}));
|
|
248
|
+
}));
|
|
249
|
+
update.command('apply')
|
|
250
|
+
.description('Apply a prepared invoice update')
|
|
251
|
+
.option('--input <path>', 'JSON input file, or - for stdin')
|
|
252
|
+
.option('--continuation <token>', 'continuation token from prepare')
|
|
253
|
+
.option('--yes', 'confirm the write operation')
|
|
254
|
+
.action(action(async (ctx, options) => {
|
|
255
|
+
const opts = asOptions(options);
|
|
256
|
+
await executeAndPrint(ctx, 'invoice.update.apply', await inputFrom(options, {
|
|
257
|
+
continuation: opts.continuation,
|
|
258
|
+
confirm: opts.yes === true ? true : undefined,
|
|
259
|
+
}));
|
|
260
|
+
}));
|
|
261
|
+
invoice.command('download')
|
|
262
|
+
.description('Download an invoice attachment after verifying ownership')
|
|
263
|
+
.option('--input <path>', 'JSON input file, or - for stdin')
|
|
264
|
+
.option('--fid <fid>', 'invoice fid')
|
|
265
|
+
.option('--file-id <id>', 'invoice file id')
|
|
266
|
+
.option('--output <path>', 'output file path')
|
|
267
|
+
.action(action(async (ctx, options) => {
|
|
268
|
+
const opts = asOptions(options);
|
|
269
|
+
await executeAndPrint(ctx, 'invoice.download', await inputFrom(options, {
|
|
270
|
+
fid: opts.fid,
|
|
271
|
+
fileId: opts.fileId,
|
|
272
|
+
output: opts.output,
|
|
273
|
+
}));
|
|
274
|
+
}));
|
|
275
|
+
const del = invoice.command('delete').description('Invoice delete commands');
|
|
276
|
+
del.command('prepare')
|
|
277
|
+
.description('Prepare an invoice soft delete')
|
|
278
|
+
.option('--input <path>', 'JSON input file, or - for stdin')
|
|
279
|
+
.option('--fid <fid>', 'invoice fid')
|
|
280
|
+
.option('--scope <scope>', 'personal or enterprise')
|
|
281
|
+
.action(action(async (ctx, options) => {
|
|
282
|
+
const opts = asOptions(options);
|
|
283
|
+
await executeAndPrint(ctx, 'invoice.delete.prepare', await inputFrom(options, {
|
|
284
|
+
fid: opts.fid,
|
|
285
|
+
scope: opts.scope,
|
|
286
|
+
}));
|
|
287
|
+
}));
|
|
288
|
+
del.command('apply')
|
|
289
|
+
.description('Apply a prepared invoice soft delete')
|
|
290
|
+
.option('--input <path>', 'JSON input file, or - for stdin')
|
|
291
|
+
.option('--continuation <token>', 'continuation token from prepare')
|
|
292
|
+
.option('--yes', 'confirm the write operation')
|
|
293
|
+
.action(action(async (ctx, options) => {
|
|
294
|
+
const opts = asOptions(options);
|
|
295
|
+
await executeAndPrint(ctx, 'invoice.delete.apply', await inputFrom(options, {
|
|
296
|
+
continuation: opts.continuation,
|
|
297
|
+
confirm: opts.yes === true ? true : undefined,
|
|
298
|
+
}));
|
|
299
|
+
}));
|
|
300
|
+
},
|
|
301
|
+
};
|