weaver-work-cli 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/internal/e10/auth/commands.js +23 -2
- package/dist/shortcuts/invoice/host.js +129 -103
- package/docs/e10-auth.md +7 -3
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
2
|
import { createServer } from 'node:http';
|
|
3
3
|
import { CliError, ExitCode } from '../../../core/errors.js';
|
|
4
|
+
import { clearInvoiceTokenForSession } from '../../../shortcuts/invoice/host.js';
|
|
4
5
|
import { buildE10CookieHeader, E10AuthSession, clearProfileAuthFile, extractRawSetCookie, findAuthFile, getActiveProfile, getProfileAuthPath, getProfileDir, listProfiles, profileNameFromUrl, readProfileConfig, readTeamsCheckUserInfo, saveAuthFile, saveProfileConfig, setActiveProfile, } from './session.js';
|
|
5
6
|
import { getE10AuthRoot } from './paths.js';
|
|
6
7
|
const DEFAULT_AGENT_TYPE = 'weaver-work-cli';
|
|
@@ -83,6 +84,15 @@ function formatLoginResult(result) {
|
|
|
83
84
|
'authFile: ' + result.authFile,
|
|
84
85
|
].join('\n');
|
|
85
86
|
}
|
|
87
|
+
async function clearInvoiceTokenCache(profile, authData) {
|
|
88
|
+
if (!authData.baseUrl || !authData.userId)
|
|
89
|
+
return;
|
|
90
|
+
await clearInvoiceTokenForSession({
|
|
91
|
+
baseUrl: normalizeUrl(authData.baseUrl),
|
|
92
|
+
profile,
|
|
93
|
+
userId: authData.userId,
|
|
94
|
+
}).catch(() => false);
|
|
95
|
+
}
|
|
86
96
|
function shellQuote(value) {
|
|
87
97
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
88
98
|
}
|
|
@@ -201,6 +211,7 @@ async function oidcExchange(code, redirectUri, baseUrl, tenantKeyFallback, agent
|
|
|
201
211
|
};
|
|
202
212
|
const profile = profileHint || profileNameFromUrl(base);
|
|
203
213
|
setActiveProfile(profile);
|
|
214
|
+
await clearInvoiceTokenCache(profile, authData);
|
|
204
215
|
const authFile = saveAuthFile(authData, profile);
|
|
205
216
|
saveProfileConfig(base, base, profile, tenantKey);
|
|
206
217
|
return {
|
|
@@ -455,6 +466,7 @@ export function registerE10AuthCommands(parent, action) {
|
|
|
455
466
|
passportUrl,
|
|
456
467
|
cookieSavedAt: Date.now(),
|
|
457
468
|
};
|
|
469
|
+
await clearInvoiceTokenCache(profile, authData);
|
|
458
470
|
const authFile = saveAuthFile(authData, profile);
|
|
459
471
|
saveProfileConfig(baseUrl, passportUrl, profile, tenantKey);
|
|
460
472
|
const result = {
|
|
@@ -475,9 +487,18 @@ export function registerE10AuthCommands(parent, action) {
|
|
|
475
487
|
auth.command('logout')
|
|
476
488
|
.alias('clear')
|
|
477
489
|
.description('Clear saved E10 auth for the current or selected profile')
|
|
478
|
-
.action(action((ctx) => {
|
|
490
|
+
.action(action(async (ctx) => {
|
|
479
491
|
const opts = ctx.getGlobalOptions();
|
|
480
|
-
const
|
|
492
|
+
const profile = opts.profile || getActiveProfile();
|
|
493
|
+
const authFile = getProfileAuthPath(profile);
|
|
494
|
+
const session = E10AuthSession.fromFile(authFile, opts.baseUrl, opts.passportUrl);
|
|
495
|
+
if (session) {
|
|
496
|
+
await clearInvoiceTokenCache(profile, {
|
|
497
|
+
baseUrl: session.getBaseUrl(),
|
|
498
|
+
userId: session.userId,
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
const result = clearProfileAuthFile(profile);
|
|
481
502
|
const text = result.cleared
|
|
482
503
|
? 'Cleared E10 auth.\nprofile: ' + result.profile + '\nauthFile: ' + result.authFile
|
|
483
504
|
: 'No E10 auth to clear.\nprofile: ' + result.profile + '\nauthFile: ' + result.authFile;
|
|
@@ -5,6 +5,7 @@ import { getActiveProfile } from '../../internal/e10/auth/session.js';
|
|
|
5
5
|
import { InvoiceCliError, invoiceAuthError, invoiceNetworkError, invoiceValidationError, } from './errors.js';
|
|
6
6
|
const MAX_DOWNLOAD_BYTES = 50 * 1024 * 1024;
|
|
7
7
|
const TOKEN_SERVICE = 'cn.weaver.weaver-work-cli.invoice-token';
|
|
8
|
+
const INVOICE_SESSION_EXPIRED_CODE = '-2';
|
|
8
9
|
function assertPath(requestPath) {
|
|
9
10
|
if (typeof requestPath !== 'string' || !requestPath.startsWith('/') || requestPath.startsWith('//')) {
|
|
10
11
|
throw invoiceValidationError('path_invalid', '请求路径必须是相对绝对路径');
|
|
@@ -63,6 +64,10 @@ function responseCode(value) {
|
|
|
63
64
|
const root = responseRoot(value);
|
|
64
65
|
return root?.ret ?? root?.code;
|
|
65
66
|
}
|
|
67
|
+
function isInvoiceSessionExpired(value) {
|
|
68
|
+
const code = responseCode(value);
|
|
69
|
+
return code !== undefined && String(code) === INVOICE_SESSION_EXPIRED_CODE;
|
|
70
|
+
}
|
|
66
71
|
function tokenAccount(session) {
|
|
67
72
|
return createHash('sha256')
|
|
68
73
|
.update(`${session.profile}\n${session.baseUrl}\n${session.userId}`)
|
|
@@ -109,6 +114,9 @@ function createInvoiceTokenStore(session) {
|
|
|
109
114
|
},
|
|
110
115
|
};
|
|
111
116
|
}
|
|
117
|
+
export async function clearInvoiceTokenForSession(session) {
|
|
118
|
+
return createInvoiceTokenStore(session).clear();
|
|
119
|
+
}
|
|
112
120
|
function inferProfile(ctx) {
|
|
113
121
|
const options = ctx.getGlobalOptions();
|
|
114
122
|
return options.profile || getActiveProfile();
|
|
@@ -125,14 +133,16 @@ function requestHeaders(session, invoiceToken) {
|
|
|
125
133
|
headers.token = invoiceToken;
|
|
126
134
|
return headers;
|
|
127
135
|
}
|
|
128
|
-
export function createInvoiceHost(ctx) {
|
|
136
|
+
export function createInvoiceHost(ctx, dependencies = {}) {
|
|
129
137
|
let e10Session = null;
|
|
130
138
|
let sessionContext = null;
|
|
131
139
|
let invoiceToken = null;
|
|
132
140
|
let tokenStore = null;
|
|
141
|
+
const requireSession = dependencies.requireSession || requireE10Session;
|
|
142
|
+
const tokenStoreFactory = dependencies.tokenStoreFactory || createInvoiceTokenStore;
|
|
133
143
|
async function ensureSession() {
|
|
134
144
|
if (!e10Session) {
|
|
135
|
-
e10Session = await
|
|
145
|
+
e10Session = await requireSession(ctx);
|
|
136
146
|
sessionContext = {
|
|
137
147
|
baseUrl: e10Session.getBaseUrl(),
|
|
138
148
|
profile: inferProfile(ctx),
|
|
@@ -148,7 +158,7 @@ export function createInvoiceHost(ctx) {
|
|
|
148
158
|
async function ensureInvoiceToken(timeoutMs = 30_000) {
|
|
149
159
|
await ensureSession();
|
|
150
160
|
const context = await getSession();
|
|
151
|
-
tokenStore ||=
|
|
161
|
+
tokenStore ||= tokenStoreFactory(context);
|
|
152
162
|
if (invoiceToken)
|
|
153
163
|
return invoiceToken;
|
|
154
164
|
invoiceToken = await tokenStore.load();
|
|
@@ -182,7 +192,31 @@ export function createInvoiceHost(ctx) {
|
|
|
182
192
|
}
|
|
183
193
|
return ensureInvoiceToken(timeoutMs);
|
|
184
194
|
}
|
|
185
|
-
async function
|
|
195
|
+
async function clearCachedInvoiceToken() {
|
|
196
|
+
invoiceToken = null;
|
|
197
|
+
await tokenStore?.clear().catch(() => false);
|
|
198
|
+
}
|
|
199
|
+
function businessError(value) {
|
|
200
|
+
const code = responseCode(value);
|
|
201
|
+
return new InvoiceCliError('api', 'business_error', responseMessage(value), {
|
|
202
|
+
code: String(code),
|
|
203
|
+
details: { ret: code },
|
|
204
|
+
exitCode: 1,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
async function handleBusinessResponse(value, retry, canRetry) {
|
|
208
|
+
const code = responseCode(value);
|
|
209
|
+
if (code === undefined || code === 0 || code === 200)
|
|
210
|
+
return value;
|
|
211
|
+
if (isInvoiceSessionExpired(value)) {
|
|
212
|
+
await clearCachedInvoiceToken();
|
|
213
|
+
if (canRetry)
|
|
214
|
+
return retry();
|
|
215
|
+
throw invoiceAuthError('invoice_session_expired', '发票业务登录态已失效,已清理本地发票 Token;请重新运行 weaver-work-cli auth login --base-url <域名> 后再试', { ret: code });
|
|
216
|
+
}
|
|
217
|
+
throw businessError(value);
|
|
218
|
+
}
|
|
219
|
+
async function invoiceRequest(request, canRetryExpiredToken = true) {
|
|
186
220
|
const { method = 'POST', path, query, body, needsInvoiceToken = true, timeoutMs = 30_000, } = request;
|
|
187
221
|
assertPath(path);
|
|
188
222
|
const token = await ensureContext(needsInvoiceToken, timeoutMs, path);
|
|
@@ -220,118 +254,110 @@ export function createInvoiceHost(ctx) {
|
|
|
220
254
|
exitCode: 1,
|
|
221
255
|
});
|
|
222
256
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
257
|
+
return handleBusinessResponse(value, () => invoiceRequest(request, false), canRetryExpiredToken && Boolean(token));
|
|
258
|
+
}
|
|
259
|
+
async function invoiceMultipartRequest(request, canRetryExpiredToken = true) {
|
|
260
|
+
const { method = 'POST', path, query, fields = {}, file, needsInvoiceToken = true, timeoutMs = 30_000, } = request;
|
|
261
|
+
if (!file && !fields.url) {
|
|
262
|
+
throw invoiceValidationError('input_invalid', 'multipart 请求需要文件或 URL');
|
|
263
|
+
}
|
|
264
|
+
assertPath(path);
|
|
265
|
+
const token = await ensureContext(needsInvoiceToken, timeoutMs, path);
|
|
266
|
+
const session = await ensureSession();
|
|
267
|
+
const form = new FormData();
|
|
268
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
269
|
+
if (value === undefined || value === null)
|
|
270
|
+
continue;
|
|
271
|
+
form.append(key, typeof value === 'string' ? value : JSON.stringify(value));
|
|
272
|
+
}
|
|
273
|
+
if (file) {
|
|
274
|
+
const content = await readFile(file.path);
|
|
275
|
+
form.append('file', new Blob([content], { type: file.mimeType || 'application/octet-stream' }), file.name);
|
|
276
|
+
}
|
|
277
|
+
const response = await fetchWithTimeout(withQuery(session.getBaseUrl(), path, query), {
|
|
278
|
+
method,
|
|
279
|
+
headers: requestHeaders(session, token),
|
|
280
|
+
body: form,
|
|
281
|
+
redirect: 'manual',
|
|
282
|
+
}, timeoutMs);
|
|
283
|
+
const text = await response.text();
|
|
284
|
+
if (response.status === 401 || response.status === 302) {
|
|
285
|
+
throw invoiceAuthError('session_expired', 'E10 登录态已失效,请先向用户确认 E10 登录域名,再运行 weaver-work-cli auth login --base-url <域名> 完成登录', { httpStatus: response.status });
|
|
286
|
+
}
|
|
287
|
+
let value = {};
|
|
288
|
+
try {
|
|
289
|
+
value = text ? JSON.parse(text) : {};
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
throw invoiceNetworkError('protocol', `接口返回不是合法 JSON(HTTP ${response.status})`, { retryable: false });
|
|
293
|
+
}
|
|
294
|
+
if (response.status < 200 || response.status >= 300) {
|
|
295
|
+
throw new InvoiceCliError('api', 'http_error', `泛微接口 HTTP ${response.status}`, {
|
|
296
|
+
code: response.status,
|
|
297
|
+
details: { status: response.status, message: responseMessage(value) },
|
|
228
298
|
exitCode: 1,
|
|
229
299
|
});
|
|
230
300
|
}
|
|
231
|
-
return value;
|
|
301
|
+
return handleBusinessResponse(value, () => invoiceMultipartRequest(request, false), canRetryExpiredToken && Boolean(token));
|
|
232
302
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
}
|
|
303
|
+
async function invoiceDownload({ id, fid, needLog = false, timeoutMs = 30_000 }, canRetryExpiredToken = true) {
|
|
304
|
+
const token = await ensureContext(true, timeoutMs, '/api/inc/file/download');
|
|
305
|
+
const session = await ensureSession();
|
|
306
|
+
const url = withQuery(session.getBaseUrl(), '/api/inc/file/download', { id, fid, needLog });
|
|
307
|
+
const response = await fetchWithTimeout(url, {
|
|
308
|
+
method: 'GET',
|
|
309
|
+
headers: {
|
|
310
|
+
...requestHeaders(session, token),
|
|
311
|
+
Accept: 'application/octet-stream, application/pdf, */*',
|
|
312
|
+
},
|
|
313
|
+
redirect: 'manual',
|
|
314
|
+
}, timeoutMs);
|
|
315
|
+
if (response.status === 401 || response.status === 302) {
|
|
316
|
+
throw invoiceAuthError('session_expired', 'E10 登录态已失效,请先向用户确认 E10 登录域名,再运行 weaver-work-cli auth login --base-url <域名> 完成登录', { httpStatus: response.status });
|
|
317
|
+
}
|
|
318
|
+
const contentType = response.headers.get('content-type') || 'application/octet-stream';
|
|
319
|
+
const declaredLength = Number(response.headers.get('content-length'));
|
|
320
|
+
if (Number.isFinite(declaredLength) && declaredLength > MAX_DOWNLOAD_BYTES) {
|
|
321
|
+
throw invoiceNetworkError('response_too_large', `下载文件超过 ${MAX_DOWNLOAD_BYTES} 字节限制`, { retryable: false });
|
|
322
|
+
}
|
|
323
|
+
const content = Buffer.from(await response.arrayBuffer());
|
|
324
|
+
const looksLikeJson = /^[\s\r\n]*[\[{]/u.test(content.subarray(0, 64).toString('utf8'));
|
|
325
|
+
if (response.status < 200 || response.status >= 300 || contentType.includes('json') || looksLikeJson) {
|
|
263
326
|
let value = {};
|
|
264
327
|
try {
|
|
265
|
-
value =
|
|
328
|
+
value = JSON.parse(content.toString('utf8'));
|
|
266
329
|
}
|
|
267
330
|
catch {
|
|
268
|
-
|
|
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 });
|
|
331
|
+
value = {};
|
|
301
332
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
333
|
+
if (isInvoiceSessionExpired(value)) {
|
|
334
|
+
await clearCachedInvoiceToken();
|
|
335
|
+
if (canRetryExpiredToken && token)
|
|
336
|
+
return invoiceDownload({ id, fid, needLog, timeoutMs }, false);
|
|
337
|
+
throw invoiceAuthError('invoice_session_expired', '发票业务登录态已失效,已清理本地发票 Token;请重新运行 weaver-work-cli auth login --base-url <域名> 后再试', { ret: responseCode(value), status: response.status });
|
|
306
338
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
-
},
|
|
339
|
+
throw new InvoiceCliError('api', 'download_failed', responseMessage(value) || `下载失败(HTTP ${response.status})`, {
|
|
340
|
+
code: response.status,
|
|
341
|
+
details: { status: response.status, ret: responseCode(value) },
|
|
342
|
+
exitCode: 1,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
if (!content.length) {
|
|
346
|
+
throw invoiceNetworkError('protocol', '下载接口返回空文件', { retryable: false });
|
|
347
|
+
}
|
|
348
|
+
if (content.length > MAX_DOWNLOAD_BYTES) {
|
|
349
|
+
throw invoiceNetworkError('response_too_large', `下载文件超过 ${MAX_DOWNLOAD_BYTES} 字节限制`, { retryable: false });
|
|
350
|
+
}
|
|
351
|
+
return { content, contentType };
|
|
352
|
+
}
|
|
353
|
+
return {
|
|
354
|
+
invoiceRequest,
|
|
355
|
+
invoiceMultipartRequest,
|
|
356
|
+
invoiceDownload,
|
|
331
357
|
getSession,
|
|
332
358
|
invalidateToken() {
|
|
333
359
|
invoiceToken = null;
|
|
334
|
-
void
|
|
360
|
+
void clearCachedInvoiceToken();
|
|
335
361
|
},
|
|
336
362
|
};
|
|
337
363
|
}
|
package/docs/e10-auth.md
CHANGED
|
@@ -8,6 +8,7 @@ E10 适配模块刻意与核心 CLI 框架隔离,避免平台逻辑污染通
|
|
|
8
8
|
- Profile 目录:`~/.weaver-work-cli/e10/profiles/<profile>`
|
|
9
9
|
- 当前激活 profile:`~/.weaver-work-cli/e10/profile`
|
|
10
10
|
- Keychain service/account:`weaver-work-cli` / `auth-key`
|
|
11
|
+
- 发票业务 Token Keychain service:`cn.weaver.weaver-work-cli.invoice-token`
|
|
11
12
|
|
|
12
13
|
`weaver-work-cli` 默认不读取、不迁移 `e10-login` / `e10-cli` 的 `~/.e10-cli`
|
|
13
14
|
登录态,避免用户安装本插件后因为历史登录状态被自动授权。确需复用外部 auth
|
|
@@ -42,9 +43,12 @@ E10 兼容的 auth 文件。
|
|
|
42
43
|
`WEAVER_BASE_URL`、`WEAVER_ETEAMSID`、`WEAVER_COOKIE`、`WEAVER_USER_AGENT` 和
|
|
43
44
|
`WEAVER_AGENT_TYPE`,可通过下面方式注入到当前 shell:
|
|
44
45
|
|
|
45
|
-
`auth
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
`auth login`、`auth oidc` 和 `auth set` 保存新登录态前,会清理同一
|
|
47
|
+
profile/baseUrl/userId 下缓存的发票业务 Token,避免重登后继续复用旧 Token。
|
|
48
|
+
|
|
49
|
+
`auth logout` 会清理当前 profile 的本地登录凭据和对应的发票业务 Token;也可配合
|
|
50
|
+
`--profile <name>` 清理指定 profile。该命令不删除 profile 配置和登录加密用
|
|
51
|
+
keychain 密钥。
|
|
48
52
|
|
|
49
53
|
```bash
|
|
50
54
|
eval "$(weaver-work-cli auth export-weaver-env)"
|