openyida 2026.10.25 → 2026.10.26-beta.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/README.md +22 -5
- package/bin/yida-pre-beta.js +11 -0
- package/bin/yida.js +18 -15
- package/lib/app/services/canvas-page-schema-builder.js +1 -43
- package/lib/auth/qwenwork-client.js +190 -0
- package/lib/auth/qwenwork-desktop.js +153 -0
- package/lib/auth/qwenwork-login.js +87 -0
- package/lib/auth/qwenwork.js +37 -0
- package/lib/auth/token-auth.js +8 -1
- package/lib/auth/token-store.js +1 -0
- package/lib/core/agent-capabilities.js +38 -5
- package/lib/core/command-manifest.js +10 -73
- package/lib/core/locales/en.js +19 -30
- package/lib/core/locales/zh.js +18 -29
- package/lib/core/qwenwork-login-guidance.js +32 -0
- package/lib/core/task-center.js +4 -32
- package/lib/samples/openyida-scaffold/canvas-navigation.jsx +2 -6
- package/lib/task-center/contract.js +1 -26
- package/lib/task-center/loader.js +0 -5
- package/lib/task-center/scaffold.js +0 -9
- package/package.json +13 -5
- package/scripts/install-qwenwork-pre-beta.js +40 -0
- package/yida-skills/SKILL.md +8 -7
- package/yida-skills/references/qwenwork-desktop-login.md +12 -0
- package/yida-skills/references/setup-and-env.md +4 -0
- package/yida-skills/references/yida-api.md +1 -3
- package/yida-skills/skills/yida-canvas-custom-page/SKILL.md +2 -4
- package/yida-skills/skills/yida-canvas-custom-page/references/canvas-style-implementation-guide.md +0 -2
- package/yida-skills/skills/yida-canvas-custom-page/references/navigation-and-entry-guide.md +0 -2
- package/yida-skills/skills/yida-canvas-data-binding/SKILL.md +2 -2
- package/yida-skills/skills/yida-canvas-data-binding/references/workflow-capabilities.md +1 -29
- package/yida-skills/skills/yida-create-page/SKILL.md +0 -1
- package/yida-skills/skills/yida-design/SKILL.md +1 -1
- package/yida-skills/skills/yida-design/references/application-style-library.md +0 -2
- package/yida-skills/skills/yida-design/references/application-theme-consistency.md +0 -4
- package/yida-skills/skills/yida-design/sub_skill/page-design/SKILL.md +44 -27
- package/yida-skills/skills/yida-design/workflow/step-2-theme-system.md +1 -3
- package/yida-skills/skills/yida-login/SKILL.md +18 -0
- package/yida-skills/skills/yida-task-center/SKILL.md +1 -3
- package/yida-skills/skills-index.json +0 -26
- package/lib/app/form-message.js +0 -352
- package/lib/navigation/page-navigation.js +0 -62
- package/lib/solution-template/solution-template.js +0 -666
- package/lib/task-center/navigation.js +0 -41
- package/yida-skills/skills/yida-form-message/SKILL.md +0 -53
- package/yida-skills/skills/yida-form-message/references/guide.md +0 -111
- package/yida-skills/skills/yida-solution-template/SKILL.md +0 -98
package/README.md
CHANGED
|
@@ -143,6 +143,25 @@ openyida login --check-only --json
|
|
|
143
143
|
openyida auth status
|
|
144
144
|
```
|
|
145
145
|
|
|
146
|
+
For QwenWork cloud identity diagnostics, run inside the actual cloud execution environment:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
openyida auth qwenwork user-info --json
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
This command exchanges the injected `QWENWORK_AGENT_TOKEN` (legacy: `MULE_AGENT_TOKEN` / `AGENT_TOKEN`) at `QWENWORK_GATEWAY_URL` (legacy: `MULE_GATEWAY_URL`), then queries `/user/info` on the same origin. It reports identity and scope metadata without printing or persisting either credential. It does not create an OpenYida login session, and QwenWork organization IDs are not YiDA `corpId` values. Existing OAuth/profile behavior is unchanged.
|
|
153
|
+
|
|
154
|
+
Programmatic callers can use `exchangeQwenWorkAgentToken`, `getQwenWorkUserInfo` and `getQwenWorkIdentity` from `lib/auth/qwenwork-client.js`. The exchange function returns an in-memory `access_token` credential for the identity query or a future server-side login exchange; callers must not log or persist it.
|
|
155
|
+
|
|
156
|
+
```js
|
|
157
|
+
const { exchangeQwenWorkAgentToken, getQwenWorkUserInfo } = require('openyida/lib/auth/qwenwork-client');
|
|
158
|
+
async function readCloudUser() {
|
|
159
|
+
const credential = await exchangeQwenWorkAgentToken();
|
|
160
|
+
// The result contains QwenWork identifiers, not YiDA IDs.
|
|
161
|
+
return getQwenWorkUserInfo(credential);
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
146
165
|
To reuse an existing shared login profile for the current project, switch first instead of starting a new OAuth login:
|
|
147
166
|
|
|
148
167
|
```bash
|
|
@@ -457,9 +476,9 @@ When preparing a PR that needs a personal Fork, or when you ask how to support t
|
|
|
457
476
|
|
|
458
477
|
| Command | Description |
|
|
459
478
|
|---------|-------------|
|
|
460
|
-
| `openyida login [target-url] [--env <name>\|--intl\|--overseas\|--global\|--yidaapps\|--alibaba] [--client-id <clientId>] [--endpoint <url>] [--no-browser]` | Login with OAuth token mode |
|
|
479
|
+
| `openyida login [--qwenwork] [target-url] [--env <name>\|--intl\|--overseas\|--global\|--yidaapps\|--alibaba] [--client-id <clientId>] [--endpoint <url>] [--no-browser]` | Login with OAuth token mode |
|
|
461
480
|
| `openyida logout` | Logout / unbind current project auth |
|
|
462
|
-
| `openyida auth <status\|login\|refresh\|logout\|profiles\|profile switch
|
|
481
|
+
| `openyida auth <status\|login\|refresh\|logout\|profiles\|profile switch\|qwenwork user-info> [--json]` | Token login state and profile management |
|
|
463
482
|
| `openyida org <list\|switch> [--json] [--corp-id <corpId>]` | Organization management (list / switch existing profiles first) |
|
|
464
483
|
| `openyida env [--json\|setup\|list\|show\|switch\|add\|remove] [options]` | Detect AI tool environment & token login state |
|
|
465
484
|
|
|
@@ -473,7 +492,6 @@ When preparing a PR that needs a personal Fork, or when you ask how to support t
|
|
|
473
492
|
| `openyida app-entry get <appType> [--json]` | Read frontend and management runtime entries |
|
|
474
493
|
| `openyida app-entry set <appType> [--frontend <url>\|--frontend-page <formUuid>] [--management <url>\|--management-page <formUuid>] [--clear-frontend] [--clear-management] [--json]` | Register application entries with partial update and readback |
|
|
475
494
|
| `openyida app-list [--type managed\|created] [--page N] [--size N]` | Page through apps I manage or created |
|
|
476
|
-
| `openyida solution-template <search\|detail\|prepare\|ai-build\|enable\|status> [templateId\|requestId] [options] [--json]` | Solution templates for independent local agents: discovery, cloud AI handoff and direct enable |
|
|
477
495
|
| `openyida corp-efficiency [overview\|details\|detail\|groups\|notify] [options] [--open\|--no-open]` | Query enterprise efficiency overview and detail reports |
|
|
478
496
|
| `openyida create-app "<name>"\|--name <name> [options] [--locale zh_CN\|en_US\|ja_JP] [--open\|--no-open]` | Create a Yida app |
|
|
479
497
|
| `openyida design-plan catalog [--json]` | List available planning themes and page patterns |
|
|
@@ -517,7 +535,6 @@ When preparing a PR that needs a personal Fork, or when you ask how to support t
|
|
|
517
535
|
| `openyida print-template <list\|inspect\|fields\|build\|create\|copy\|save\|restore\|settings\|status\|order\|render\|files> <appType> <formUuid> [templateId] [--config <file>] [--scene <order\|quotation\|delivery\|receipt\|approval\|codes>] [--bindings <file>] [--name <title>] [--expected-hash <hash>] [--backup <file>] [--out <file>] [--status START\|STOP] [--instance <id>] [--page <n>] [--size <n>] [--allow-permission-change] [--json] \| print-template <list\|render> --detail-url <url> [--template-id <id>] [--json]` | Manage native YiDA print templates, generate document layouts, back up, restore and request PDFs |
|
|
518
536
|
| `openyida form-view <list\|inspect\|save\|create\|order> <appType> <formUuid> [viewUuid] [--config <file>] [--name <title>] [--with-config] [--expected-hash <hash>] [--backup <file>] [--replace] [--json]` | Native form views: list, inspect and configure columns, widths, frozen columns and filters |
|
|
519
537
|
| `openyida custom-button <list\|inspect\|options\|create\|update\|delete> <appType> <formUuid> [buttonUuid] [--config <file>] [--expected-hash <hash>] [--backup <file>] [--yes] [--json]` | Native custom buttons: inspect and configure form edits, automation flows and page links |
|
|
520
|
-
| `openyida form-message <list\|inspect\|options\|create\|update\|delete> <appType> <formUuid> [messageId] [--config <file>] [--expected-hash <hash>] [--backup <file>] [--yes] [--json]` | Native form notifications: inspect triggers, recipients and templates, then safely maintain bindings |
|
|
521
538
|
| `openyida get-schema <appType> <formUuid\|--all> [--summary-json\|--field-map-json\|--analysis-json]` | Get one form Schema or all form Schemas |
|
|
522
539
|
| `openyida check-prd-completeness <prd.md> --app-type <appType> [--build-manifest <file>] [--json]` | Check resources, frontend menu hiding and application theme delivery |
|
|
523
540
|
| `openyida check-design <design.md> [--prd <prd.md>] [--base-dir <dir>] [--json]` | Validate design documents, theme tokens and PRD handoff |
|
|
@@ -535,7 +552,7 @@ When preparing a PR that needs a personal Fork, or when you ask how to support t
|
|
|
535
552
|
| Command | Description |
|
|
536
553
|
|---------|-------------|
|
|
537
554
|
| `openyida data <query\|get\|create\|update\|transfer> <resource> ... \| delete form <appType> <formUuid> --inst-id <id> --expect-form-name <name> --expect-form-type receipt --confirm [--json]` | Unified data management (form/process/task/subform) |
|
|
538
|
-
| `openyida task-center <todo\|created\|done\|cc\|proxy\|capabilities\|
|
|
555
|
+
| `openyida task-center <todo\|created\|done\|cc\|proxy\|capabilities\|scaffold\|options\|inspect\|count> [--spec <file.json> --output <file.canvas.jsx>] [options]` | Global task center (todo/processed/cc etc.) |
|
|
539
556
|
| `openyida basic-info <overview\|commodity\|grant\|capacity\|quota\|abs-path\|dataflow\|i18n\|domain>` | Query organization basic info, capacity, quotas, and domain settings |
|
|
540
557
|
| `openyida read-dingtalk-doc <docUrl> [--output <file>] [--json]` | Fetch Markdown content from a DingTalk document |
|
|
541
558
|
| `openyida read-dingtalk-tingji <taskUuid> [--json]` | Fetch DingTalk Tingji details by task UUID |
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
const os = require('node:os');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const config = require('../package.json').openyidaLocalBeta;
|
|
6
|
+
// PRE beta 消费包内测试默认值,真实凭据仍由原认证模块管理。
|
|
7
|
+
process.env.OPENYIDA_ENDPOINT ||= config.baseUrl;
|
|
8
|
+
process.env.OPENYIDA_ENV ||= config.environment;
|
|
9
|
+
process.env.OPENYIDA_AUTH_DIR ||= path.join(os.homedir(), '.openyida', config.authDirectory);
|
|
10
|
+
process.env.OPENYIDA_SKIP_UPDATE_CHECK = '1';
|
|
11
|
+
require('./yida.js');
|
package/bin/yida.js
CHANGED
|
@@ -588,17 +588,27 @@ async function main() {
|
|
|
588
588
|
}
|
|
589
589
|
return;
|
|
590
590
|
}
|
|
591
|
-
// Template-only preflight; managed runs retain their existing guard contract.
|
|
592
|
-
if (command === 'solution-template' && !hasHelpFlag(rawArgs) && !readManagedContext()) {
|
|
593
|
-
require('../lib/solution-template/solution-template').assertIndependentLocalAgent();
|
|
594
|
-
}
|
|
595
591
|
commandReceipt = beginCommandReceipt({ command, args: rawArgs });
|
|
596
592
|
process.once('exit', code => {
|
|
597
593
|
try {commandReceipt.finish(code);} catch {process.exitCode = 1;}
|
|
598
594
|
});
|
|
599
595
|
assertManagedInvocation(command, rawArgs);
|
|
600
596
|
applyQuietFlag();
|
|
597
|
+
// 一次性票据只在当前进程消费,不能先自更新并重启命令。
|
|
598
|
+
switch (command) {
|
|
599
|
+
case 'login-qwenwork':
|
|
600
|
+
await require('../lib/auth/qwenwork-desktop').run(args);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
601
603
|
applyGlobalEnvironmentFlags();
|
|
604
|
+
// 桌面准备完成后继续执行公开命令,由真实登录态决定结果。
|
|
605
|
+
if (command === 'login' && args.includes('--qwenwork')) {
|
|
606
|
+
const loginArgs = applyLoginEnvironmentFlags(args, { inferTargetUrl: true });
|
|
607
|
+
if (hasHelpFlag(loginArgs)) { printLoginHelp(); return; }
|
|
608
|
+
assertNoUnsupportedLegacyLoginFlags(rawArgs, loginArgs);
|
|
609
|
+
await require('../lib/auth/qwenwork-login').run(loginArgs, buildTokenLoginOptions(loginArgs));
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
602
612
|
|
|
603
613
|
const { maybeAutoUpdate } = require('../lib/core/update');
|
|
604
614
|
const updateResult = await maybeAutoUpdate({
|
|
@@ -718,6 +728,10 @@ async function main() {
|
|
|
718
728
|
|
|
719
729
|
case 'auth': {
|
|
720
730
|
const subCommand = args[0];
|
|
731
|
+
if (subCommand === 'qwenwork') {
|
|
732
|
+
await require('../lib/auth/qwenwork').run(args.slice(1));
|
|
733
|
+
break;
|
|
734
|
+
}
|
|
721
735
|
const authArgs = applyLoginEnvironmentFlags(args.slice(1), { inferTargetUrl: true });
|
|
722
736
|
const { getAuthStatus } = require('../lib/core/utils');
|
|
723
737
|
const { tokenLogin, tokenLogout, tokenRefresh } = require('../lib/auth/token-auth');
|
|
@@ -794,12 +808,6 @@ async function main() {
|
|
|
794
808
|
break;
|
|
795
809
|
}
|
|
796
810
|
|
|
797
|
-
case 'solution-template': {
|
|
798
|
-
const { run } = require('../lib/solution-template/solution-template');
|
|
799
|
-
await run(args);
|
|
800
|
-
break;
|
|
801
|
-
}
|
|
802
|
-
|
|
803
811
|
case 'corp-efficiency': {
|
|
804
812
|
const { run } = require('../lib/corp-efficiency/corp-efficiency');
|
|
805
813
|
await run(args);
|
|
@@ -847,11 +855,6 @@ async function main() {
|
|
|
847
855
|
await run(args);
|
|
848
856
|
break;
|
|
849
857
|
}
|
|
850
|
-
case 'form-message': {
|
|
851
|
-
const { run } = require('../lib/app/form-message');
|
|
852
|
-
await run(args);
|
|
853
|
-
break;
|
|
854
|
-
}
|
|
855
858
|
case 'print-template': {
|
|
856
859
|
const { run } = require('../lib/print-template/print-template');
|
|
857
860
|
await run(args);
|
|
@@ -10,16 +10,7 @@ const {
|
|
|
10
10
|
findEmojiInValue,
|
|
11
11
|
} = require('../../core/no-emoji-detector');
|
|
12
12
|
|
|
13
|
-
const
|
|
14
|
-
const { createTaskCenterContract } = require('../../task-center/contract');
|
|
15
|
-
const { createTaskCenterLoader } = require('../../task-center/loader');
|
|
16
|
-
const { createTaskCenterNavigation } = require('../../task-center/navigation');
|
|
17
|
-
|
|
18
|
-
const CANVAS_YIDA_API_BRIDGE_SOURCE = `${createPageNavigation.toString()}
|
|
19
|
-
${createTaskCenterNavigation.toString()}
|
|
20
|
-
${createTaskCenterContract.toString()}
|
|
21
|
-
${createTaskCenterLoader.toString()}
|
|
22
|
-
var OPENYIDA_YIDA_FORM_API_METHODS = [
|
|
13
|
+
const CANVAS_YIDA_API_BRIDGE_SOURCE = `var OPENYIDA_YIDA_FORM_API_METHODS = [
|
|
23
14
|
'saveFormData',
|
|
24
15
|
'updateFormData',
|
|
25
16
|
'searchFormDataIds',
|
|
@@ -248,15 +239,8 @@ function openyidaCreateRouterBridge(utils) {
|
|
|
248
239
|
|
|
249
240
|
function openyidaCreateUtilsBridge(context, yidaBridge) {
|
|
250
241
|
var utils = context && context.utils;
|
|
251
|
-
var navigation = createPageNavigation(window);
|
|
252
|
-
var taskNavigation = createTaskCenterNavigation();
|
|
253
|
-
var taskCounts = createTaskCenterLoader(createTaskCenterContract(), function() { return yidaBridge; });
|
|
254
242
|
var methodNames = openyidaCollectUtilsMethodNames(utils);
|
|
255
243
|
function invoke(methodName) {
|
|
256
|
-
if (methodName === 'openPage') { return navigation.open.apply(null, Array.prototype.slice.call(arguments, 1)); }
|
|
257
|
-
if (['taskCenterCount', 'taskCenterLink', 'openTaskCenter'].indexOf(methodName) !== -1) {
|
|
258
|
-
return bridge[methodName].apply(null, Array.prototype.slice.call(arguments, 1));
|
|
259
|
-
}
|
|
260
244
|
if (!utils || typeof utils[methodName] !== 'function') {
|
|
261
245
|
throw new Error('this.utils.' + methodName + ' is not available');
|
|
262
246
|
}
|
|
@@ -267,8 +251,6 @@ function openyidaCreateUtilsBridge(context, yidaBridge) {
|
|
|
267
251
|
apiGroups: {
|
|
268
252
|
utility: OPENYIDA_UTIL_API_METHODS.slice(),
|
|
269
253
|
router: ['push'],
|
|
270
|
-
navigation: ['openPage', 'taskCenterLink', 'openTaskCenter'],
|
|
271
|
-
tasks: ['taskCenterCount'],
|
|
272
254
|
yida: OPENYIDA_KNOWN_YIDA_API_METHODS.slice()
|
|
273
255
|
},
|
|
274
256
|
knownMethods: OPENYIDA_UTIL_API_METHODS.slice(),
|
|
@@ -277,33 +259,9 @@ function openyidaCreateUtilsBridge(context, yidaBridge) {
|
|
|
277
259
|
return !utils || typeof utils[methodName] !== 'function';
|
|
278
260
|
}),
|
|
279
261
|
invoke: invoke,
|
|
280
|
-
openPage: navigation.open,
|
|
281
|
-
taskCenterLink: function(options) {
|
|
282
|
-
return taskNavigation.target(Object.assign({ baseUrl: window.location.origin }, options));
|
|
283
|
-
},
|
|
284
|
-
taskCenterCount: async function(options) {
|
|
285
|
-
if (!options || typeof options !== 'object' || Array.isArray(options)
|
|
286
|
-
|| Object.keys(options).some(function(key) { return ['scope', 'appType', 'kind', 'filters'].indexOf(key) === -1; })) {
|
|
287
|
-
var error = new Error('TASK_FILTER_INVALID: options');
|
|
288
|
-
error.code = 'TASK_FILTER_INVALID';
|
|
289
|
-
throw error;
|
|
290
|
-
}
|
|
291
|
-
var target = taskNavigation.target({ scope: options.scope, appType: options.appType, kind: options.kind, baseUrl: window.location.origin });
|
|
292
|
-
var filters = createTaskCenterContract().normalizeFilters(target.kind, options.filters === undefined ? {} : options.filters, 'app-query');
|
|
293
|
-
return taskCounts.count(target.kind, Object.assign({}, filters, { transport: 'app-query' },
|
|
294
|
-
target.scope === 'corp' ? { scope: 'corp' } : { appType: target.appType }));
|
|
295
|
-
},
|
|
296
|
-
openTaskCenter: function(options) {
|
|
297
|
-
var target = taskNavigation.target(Object.assign({ baseUrl: window.location.origin }, options));
|
|
298
|
-
return navigation.open(target.pageUrl, { mode: target.mode });
|
|
299
|
-
},
|
|
300
262
|
router: openyidaCreateRouterBridge(utils),
|
|
301
263
|
yida: yidaBridge
|
|
302
264
|
};
|
|
303
|
-
['openPage', 'taskCenterLink', 'openTaskCenter', 'taskCenterCount'].forEach(function(methodName) {
|
|
304
|
-
if (bridge.availableMethods.indexOf(methodName) === -1) { bridge.availableMethods.push(methodName); }
|
|
305
|
-
});
|
|
306
|
-
bridge.missingKnownMethods = bridge.missingKnownMethods.filter(function(name) { return name !== 'openPage'; });
|
|
307
265
|
methodNames.forEach(function(methodName) {
|
|
308
266
|
if (!bridge[methodName]) {
|
|
309
267
|
bridge[methodName] = function() {
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { CliError } = require('../core/cli-error');
|
|
4
|
+
const { t } = require('../core/i18n');
|
|
5
|
+
|
|
6
|
+
const DEFAULT_TIMEOUT_MS = 20000;
|
|
7
|
+
const MAX_RESPONSE_BYTES = 1024 * 1024;
|
|
8
|
+
|
|
9
|
+
function failure(code, message, stage, details = {}) {
|
|
10
|
+
return new CliError(t(`qwenwork.${message}`), {
|
|
11
|
+
code: `QWENWORK_${code}`,
|
|
12
|
+
details: { stage, ...details },
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function pick(env, names) {
|
|
17
|
+
for (const name of names) {
|
|
18
|
+
const value = String(env[name] || '').trim();
|
|
19
|
+
if (value) {
|
|
20
|
+
return { name, value };
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function normalizeBaseUrl(value) {
|
|
27
|
+
try {
|
|
28
|
+
const url = new URL(value);
|
|
29
|
+
const loopback = ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname);
|
|
30
|
+
if ((url.protocol !== 'https:' && !(url.protocol === 'http:' && loopback)) ||
|
|
31
|
+
url.username || url.password || url.search || url.hash) {
|
|
32
|
+
throw new Error('invalid_url');
|
|
33
|
+
}
|
|
34
|
+
return url.href.replace(/\/+$/, '');
|
|
35
|
+
} catch {
|
|
36
|
+
throw failure('INVALID_GATEWAY', 'invalid_url', 'resolve_credentials');
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isObject(value) {
|
|
41
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Do not include upstream bodies, headers or exception messages in diagnostics:
|
|
45
|
+
// an authentication endpoint may echo credentials even in an error response.
|
|
46
|
+
async function requestJson(method, url, accessToken, options, stage) {
|
|
47
|
+
const timeoutMs = options.timeoutMs === undefined ? DEFAULT_TIMEOUT_MS : options.timeoutMs;
|
|
48
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
49
|
+
throw failure('INVALID_TIMEOUT', 'request_failed', stage);
|
|
50
|
+
}
|
|
51
|
+
const controller = new globalThis.AbortController();
|
|
52
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
53
|
+
try {
|
|
54
|
+
const response = await globalThis.fetch(url, {
|
|
55
|
+
method,
|
|
56
|
+
headers: { accept: 'application/json', authorization: `Bearer ${accessToken}` },
|
|
57
|
+
redirect: 'manual',
|
|
58
|
+
signal: controller.signal,
|
|
59
|
+
});
|
|
60
|
+
if (!response.ok) {
|
|
61
|
+
if (response.body) {
|
|
62
|
+
await response.body.cancel();
|
|
63
|
+
}
|
|
64
|
+
throw failure('HTTP_ERROR', 'request_failed', stage, { status_code: response.status });
|
|
65
|
+
}
|
|
66
|
+
const chunks = [];
|
|
67
|
+
let size = 0;
|
|
68
|
+
if (response.body) {
|
|
69
|
+
const reader = response.body.getReader();
|
|
70
|
+
let next = await reader.read();
|
|
71
|
+
while (!next.done) {
|
|
72
|
+
const { value } = next;
|
|
73
|
+
size += value.byteLength;
|
|
74
|
+
if (size > MAX_RESPONSE_BYTES) {
|
|
75
|
+
await reader.cancel();
|
|
76
|
+
throw failure('RESPONSE_TOO_LARGE', 'invalid_response', stage);
|
|
77
|
+
}
|
|
78
|
+
chunks.push(Buffer.from(value));
|
|
79
|
+
next = await reader.read();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
let payload;
|
|
83
|
+
try {
|
|
84
|
+
payload = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
85
|
+
} catch {
|
|
86
|
+
throw failure('INVALID_RESPONSE', 'invalid_response', stage);
|
|
87
|
+
}
|
|
88
|
+
if (!isObject(payload) || (payload.code !== undefined && payload.code !== 'ok')) {
|
|
89
|
+
throw failure('INVALID_RESPONSE', 'invalid_response', stage);
|
|
90
|
+
}
|
|
91
|
+
return payload;
|
|
92
|
+
} catch (error) {
|
|
93
|
+
if (error && error.isCliError) {
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
throw controller.signal.aborted
|
|
97
|
+
? failure('TIMEOUT', 'request_timeout', stage)
|
|
98
|
+
: failure('NETWORK_ERROR', 'request_failed', stage);
|
|
99
|
+
} finally {
|
|
100
|
+
clearTimeout(timer);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function decodeClaims(token) {
|
|
105
|
+
try {
|
|
106
|
+
const value = JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString('utf8'));
|
|
107
|
+
return isObject(value) ? value : {};
|
|
108
|
+
} catch {
|
|
109
|
+
return {};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Exchange a cloud Agent credential for an in-memory scoped user credential.
|
|
114
|
+
* This does not issue or persist an OpenYida session.
|
|
115
|
+
*/
|
|
116
|
+
async function exchangeQwenWorkAgentToken(options = {}) {
|
|
117
|
+
const env = options.env || process.env;
|
|
118
|
+
const agent = pick(env, ['QWENWORK_AGENT_TOKEN', 'MULE_AGENT_TOKEN', 'AGENT_TOKEN']);
|
|
119
|
+
if (!agent || /[\r\n]/.test(agent.value)) {
|
|
120
|
+
throw failure('CREDENTIAL_MISSING', 'credential_missing', 'resolve_credentials');
|
|
121
|
+
}
|
|
122
|
+
const gateway = pick(env, ['QWENWORK_GATEWAY_URL', 'MULE_GATEWAY_URL']);
|
|
123
|
+
if (!gateway) {
|
|
124
|
+
throw failure('GATEWAY_MISSING', 'gateway_missing', 'resolve_credentials');
|
|
125
|
+
}
|
|
126
|
+
const baseUrl = normalizeBaseUrl(gateway.value);
|
|
127
|
+
const response = await requestJson('POST', `${baseUrl}/api/agent-gw/token/exchange`, agent.value, options, 'exchange');
|
|
128
|
+
// The current cloud endpoint returns {token, scope, ttl} without an envelope.
|
|
129
|
+
const payload = isObject(response.data) ? response.data : response;
|
|
130
|
+
if (typeof payload.token !== 'string' || !payload.token.trim() || /\s/.test(payload.token) ||
|
|
131
|
+
typeof payload.scope !== 'string' || !Number.isFinite(payload.ttl) || payload.ttl <= 0) {
|
|
132
|
+
throw failure('INVALID_RESPONSE', 'invalid_response', 'exchange');
|
|
133
|
+
}
|
|
134
|
+
const decoded = decodeClaims(payload.token);
|
|
135
|
+
if (typeof decoded.exp === 'number' && decoded.exp * 1000 <= Date.now()) {
|
|
136
|
+
throw failure('TOKEN_EXPIRED', 'expired_token', 'exchange');
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
access_token: payload.token,
|
|
140
|
+
base_url: baseUrl,
|
|
141
|
+
credential_source: agent.name,
|
|
142
|
+
scope: payload.scope,
|
|
143
|
+
expires_in: payload.ttl,
|
|
144
|
+
expires_at: typeof decoded.exp === 'number' ? decoded.exp * 1000 : Date.now() + payload.ttl * 1000,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Query the credential's own origin; never infer a YiDA endpoint or organization. */
|
|
149
|
+
async function getQwenWorkUserInfo(credential, options = {}) {
|
|
150
|
+
const accessToken = credential && credential.access_token;
|
|
151
|
+
if (typeof accessToken !== 'string' || !accessToken || /[\r\n]/.test(accessToken)) {
|
|
152
|
+
throw failure('CREDENTIAL_MISSING', 'credential_missing', 'user_info');
|
|
153
|
+
}
|
|
154
|
+
const baseUrl = normalizeBaseUrl(credential.base_url);
|
|
155
|
+
const response = await requestJson('GET', `${baseUrl}/user/info`, accessToken, options, 'user_info');
|
|
156
|
+
if (response.code !== 'ok' || !isObject(response.data) ||
|
|
157
|
+
typeof response.data.user_id !== 'string' || !response.data.user_id.trim()) {
|
|
158
|
+
throw failure('INVALID_RESPONSE', 'invalid_response', 'user_info');
|
|
159
|
+
}
|
|
160
|
+
const env = options.env || process.env;
|
|
161
|
+
const decoded = decodeClaims(accessToken);
|
|
162
|
+
// Decoding is not local signature verification. The authenticated /user/info
|
|
163
|
+
// response establishes identity; these comparisons catch context drift.
|
|
164
|
+
for (const expected of [env.QWENWORK_USER_ID, env.MULE_USER_ID, decoded.user_id, decoded.sub]) {
|
|
165
|
+
if (expected && String(expected) !== response.data.user_id) {
|
|
166
|
+
throw failure('IDENTITY_MISMATCH', 'identity_mismatch', 'user_info');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const orgId = response.data.biz && response.data.biz.org_id;
|
|
170
|
+
if (orgId && decoded.biz_org_id && orgId !== decoded.biz_org_id) {
|
|
171
|
+
throw failure('IDENTITY_MISMATCH', 'identity_mismatch', 'user_info');
|
|
172
|
+
}
|
|
173
|
+
return response.data;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Safe composition for identity consumers: the returned object has no credential. */
|
|
177
|
+
async function getQwenWorkIdentity(options = {}) {
|
|
178
|
+
const credential = await exchangeQwenWorkAgentToken(options);
|
|
179
|
+
const profile = await getQwenWorkUserInfo(credential, options);
|
|
180
|
+
return {
|
|
181
|
+
base_url: credential.base_url,
|
|
182
|
+
credential_source: credential.credential_source,
|
|
183
|
+
scope: credential.scope,
|
|
184
|
+
expires_in: credential.expires_in,
|
|
185
|
+
expires_at: credential.expires_at,
|
|
186
|
+
profile,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
module.exports = { exchangeQwenWorkAgentToken, getQwenWorkUserInfo, getQwenWorkIdentity };
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { CliError } = require('../core/cli-error');
|
|
4
|
+
const { buildQwenWorkLoginInvocation } = require('../core/qwenwork-login-guidance');
|
|
5
|
+
const { t } = require('../core/i18n');
|
|
6
|
+
const { DINGTALK_OAUTH_CLIENT_ID } = require('../core/env-manager');
|
|
7
|
+
const { saveTokenSession, loadTokenSession, isEnvTokenAuthMode } = require('./token-store');
|
|
8
|
+
const { readManagedContext } = require('../agent/managed-context');
|
|
9
|
+
|
|
10
|
+
const TICKET_ENV = 'OPENYIDA_QWENWORK_EXCHANGE_TICKET';
|
|
11
|
+
const TICKET_CONTEXT_ENV = 'OPENYIDA_QWENWORK_EXCHANGE_TICKET_CONTEXT';
|
|
12
|
+
const CONSUME_PATH = '/openapi/cli/v1/auth/qwenwork/desktop/consume';
|
|
13
|
+
const MAX_RESPONSE_BYTES = 65536;
|
|
14
|
+
|
|
15
|
+
function fail(code) {
|
|
16
|
+
return new CliError(t('qwenwork.desktop_login_failed'), {
|
|
17
|
+
code: `QWENWORK_DESKTOP_${code}`,
|
|
18
|
+
details: { invocation: buildQwenWorkLoginInvocation() },
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function parseTicket(raw, context) {
|
|
23
|
+
try {
|
|
24
|
+
if (typeof raw !== 'string' || !raw || /\s/.test(raw) || raw.length > 32768 || typeof context !== 'string' || context.length > 8192) { throw new Error(); }
|
|
25
|
+
const value = JSON.parse(context);
|
|
26
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) ||
|
|
27
|
+
!Number.isSafeInteger(value.expires_at) || value.expires_at <= Date.now() || value.expires_at > Date.now() + 300000 ||
|
|
28
|
+
typeof value.corp_id !== 'string' || !value.corp_id || typeof value.user_id !== 'string' || !value.user_id ||
|
|
29
|
+
typeof value.exchange_id !== 'string' || !value.exchange_id ||
|
|
30
|
+
'access_token' in value || 'refresh_token' in value || 'accessToken' in value || 'refreshToken' in value) { throw new Error(); }
|
|
31
|
+
return { ...value, exchange_ticket: raw };
|
|
32
|
+
} catch {
|
|
33
|
+
throw fail('TICKET_MISSING_OR_INVALID');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function origin(value) {
|
|
38
|
+
try {
|
|
39
|
+
const url = new URL(value);
|
|
40
|
+
if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash || url.pathname !== '/') { throw new Error(); }
|
|
41
|
+
return url.origin;
|
|
42
|
+
} catch {
|
|
43
|
+
throw fail('ENDPOINT_INVALID');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function requestJson(url, init, options) {
|
|
48
|
+
const controller = new AbortController();
|
|
49
|
+
const timer = setTimeout(() => controller.abort(), options.timeoutMs || 15000);
|
|
50
|
+
try {
|
|
51
|
+
const response = await (options.fetch || globalThis.fetch)(url, { ...init, redirect: 'error', signal: controller.signal });
|
|
52
|
+
if (!response.ok) {
|
|
53
|
+
if (response.body) { await response.body.cancel(); }
|
|
54
|
+
throw fail('REQUEST_REJECTED');
|
|
55
|
+
}
|
|
56
|
+
const reader = response.body && response.body.getReader();
|
|
57
|
+
if (!reader) { throw fail('RESPONSE_INVALID'); }
|
|
58
|
+
const chunks = [];
|
|
59
|
+
let bytes = 0;
|
|
60
|
+
try {
|
|
61
|
+
for (let next = await reader.read(); !next.done; next = await reader.read()) {
|
|
62
|
+
bytes += next.value.byteLength;
|
|
63
|
+
if (bytes > MAX_RESPONSE_BYTES) { await reader.cancel(); throw fail('RESPONSE_INVALID'); }
|
|
64
|
+
chunks.push(next.value);
|
|
65
|
+
}
|
|
66
|
+
} finally {
|
|
67
|
+
reader.releaseLock();
|
|
68
|
+
}
|
|
69
|
+
const value = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
70
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) { throw fail('RESPONSE_INVALID'); }
|
|
71
|
+
return value;
|
|
72
|
+
} catch (error) {
|
|
73
|
+
// 服务端和网络异常可能回显凭据,只透传本模块生成的固定错误。
|
|
74
|
+
if (error instanceof CliError && /^QWENWORK_DESKTOP_/.test(error.code)) { throw error; }
|
|
75
|
+
throw fail(controller.signal.aborted ? 'REQUEST_TIMED_OUT' : 'REQUEST_FAILED');
|
|
76
|
+
} finally {
|
|
77
|
+
clearTimeout(timer);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** 客户端内部入口:消费一次性兑换票并持久化双 Token,不调用或重启普通 login。 */
|
|
82
|
+
async function loginQwenWork(options = {}) {
|
|
83
|
+
const env = options.env || process.env;
|
|
84
|
+
const raw = options.exchangeTicket || env[TICKET_ENV];
|
|
85
|
+
const context = options.ticketContext || env[TICKET_CONTEXT_ENV];
|
|
86
|
+
delete env[TICKET_ENV];
|
|
87
|
+
delete env[TICKET_CONTEXT_ENV];
|
|
88
|
+
if (isEnvTokenAuthMode(env) || readManagedContext(env)) { throw fail('MANAGED_AUTH_CONFLICT'); }
|
|
89
|
+
const ticket = parseTicket(raw, context);
|
|
90
|
+
const baseUrl = origin(ticket.base_url);
|
|
91
|
+
const expectedEndpoint = options.endpoint || env.OPENYIDA_ENDPOINT;
|
|
92
|
+
if (expectedEndpoint && origin(expectedEndpoint) !== baseUrl) { throw fail('ENDPOINT_MISMATCH'); }
|
|
93
|
+
if (options.corpId && options.corpId !== ticket.corp_id) { throw fail('IDENTITY_MISMATCH'); }
|
|
94
|
+
const startedAt = Date.now();
|
|
95
|
+
const value = await requestJson(`${baseUrl}${CONSUME_PATH}`, {
|
|
96
|
+
method: 'POST', headers: { accept: 'application/json', 'content-type': 'application/json', authorization: `Bearer ${ticket.exchange_ticket}` },
|
|
97
|
+
body: '{}',
|
|
98
|
+
}, options);
|
|
99
|
+
const expiresAt = startedAt + Number(value.expires_in) * 1000;
|
|
100
|
+
if (value.status !== 'ok' || value.auth_mode !== 'token' || value.can_auto_use !== true ||
|
|
101
|
+
value.token_type !== 'Bearer' || value.credential_source !== 'qwenwork_desktop' ||
|
|
102
|
+
'refreshToken' in value || 'exchange_ticket' in value ||
|
|
103
|
+
typeof value.access_token !== 'string' || !value.access_token || /\s/.test(value.access_token) || value.access_token.length > 32768 ||
|
|
104
|
+
!Number.isInteger(value.expires_in) || value.expires_in < 1 || value.expires_in > 1800 ||
|
|
105
|
+
typeof value.refresh_token !== 'string' || !value.refresh_token || /\s/.test(value.refresh_token) || value.refresh_token.length > 32768 ||
|
|
106
|
+
!Number.isInteger(value.refresh_token_expires_in) || value.refresh_token_expires_in < 1 ||
|
|
107
|
+
(value.client_id && value.client_id !== DINGTALK_OAUTH_CLIENT_ID) ||
|
|
108
|
+
value.corp_id !== ticket.corp_id || value.user_id !== ticket.user_id || value.exchange_id !== ticket.exchange_id ||
|
|
109
|
+
(value.base_url && origin(value.base_url) !== baseUrl)) { throw fail('CREDENTIAL_INVALID'); }
|
|
110
|
+
const verified = await requestJson(`${baseUrl}/openapi/cli/v1/auth/status`, {
|
|
111
|
+
method: 'GET', headers: { authorization: `Bearer ${value.access_token}`, accept: 'application/json' },
|
|
112
|
+
}, options);
|
|
113
|
+
if (verified.status !== 'ok' || verified.auth_mode !== 'token' || verified.can_auto_use !== true ||
|
|
114
|
+
verified.corp_id !== value.corp_id || verified.user_id !== value.user_id || expiresAt <= Date.now()) { throw fail('IDENTITY_MISMATCH'); }
|
|
115
|
+
const session = saveTokenSession({
|
|
116
|
+
auth_mode: 'token', token_type: 'Bearer', access_token: value.access_token, refresh_token: value.refresh_token,
|
|
117
|
+
expires_at: expiresAt, base_url: baseUrl, client_id: DINGTALK_OAUTH_CLIENT_ID,
|
|
118
|
+
corp_id: value.corp_id, user_id: value.user_id, credential_source: 'qwenwork_desktop',
|
|
119
|
+
}, options);
|
|
120
|
+
const restored = loadTokenSession(options);
|
|
121
|
+
if (!['user', 'project'].includes(session.persistence_scope) || !restored ||
|
|
122
|
+
restored.access_token !== value.access_token || restored.refresh_token !== value.refresh_token || restored.base_url !== baseUrl ||
|
|
123
|
+
restored.corp_id !== value.corp_id || restored.user_id !== value.user_id) { throw fail('PERSISTENCE_FAILED'); }
|
|
124
|
+
// 身份、环境和存储细节仅用于内部校验,不进入模型可见的登录结果。
|
|
125
|
+
return { ok: true, status: 'ok', can_auto_use: true };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function run(args, options = {}) {
|
|
129
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
130
|
+
const invocation = buildQwenWorkLoginInvocation();
|
|
131
|
+
if (args.includes('--json')) { console.log(JSON.stringify({ usage: t('qwenwork.desktop_login_usage'), invocation })); }
|
|
132
|
+
else { console.log(`${t('qwenwork.desktop_login_usage')}\n${t('qwenwork.desktop_login_standalone', invocation.command)}`); }
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const parsed = {};
|
|
136
|
+
const seen = new Set();
|
|
137
|
+
for (let index = 0; index < args.length; index++) {
|
|
138
|
+
const flag = args[index];
|
|
139
|
+
if (seen.has(flag)) { throw new CliError(t('qwenwork.desktop_login_usage'), { code: 'CLI_USAGE_ERROR', exitCode: 2, details: { invocation: buildQwenWorkLoginInvocation() } }); }
|
|
140
|
+
seen.add(flag);
|
|
141
|
+
if (['--json', '--quiet'].includes(flag)) { continue; }
|
|
142
|
+
const key = flag === '--corp-id' ? 'corpId' : flag === '--endpoint' ? 'endpoint'
|
|
143
|
+
: flag === '--exchange-ticket' ? 'exchangeTicket' : flag === '--ticket-context' ? 'ticketContext' : null;
|
|
144
|
+
const value = args[++index];
|
|
145
|
+
if (!key || !value || value.startsWith('--')) { throw new CliError(t('qwenwork.desktop_login_usage'), { code: 'CLI_USAGE_ERROR', exitCode: 2, details: { invocation: buildQwenWorkLoginInvocation() } }); }
|
|
146
|
+
parsed[key] = value;
|
|
147
|
+
}
|
|
148
|
+
const result = await loginQwenWork({ ...options, ...parsed });
|
|
149
|
+
if (args.includes('--json') && !args.includes('--quiet') && (options.env || process.env).YIDA_QUIET !== '1') { console.log(JSON.stringify({ ...result, next_action: 'continue_business_operation_silently', user_visible: false })); }
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
module.exports = { TICKET_ENV, TICKET_CONTEXT_ENV, loginQwenWork, run };
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const { t } = require('../core/i18n');
|
|
3
|
+
const { CliError } = require('../core/cli-error');
|
|
4
|
+
const { loadTokenSession, resolveTokenSession, isEnvTokenAuthMode } = require('./token-store');
|
|
5
|
+
const { tokenLogin, tokenRefresh, isRefreshAuthRequired } = require('./token-auth');
|
|
6
|
+
|
|
7
|
+
function failure(code) {
|
|
8
|
+
return new CliError(t('qwenwork.login_check_failed'), { code });
|
|
9
|
+
}
|
|
10
|
+
function summary(ok) { return { ok, status: ok ? 'ok' : 'not_logged_in', can_auto_use: ok }; }
|
|
11
|
+
|
|
12
|
+
/** 以服务端状态为准;网络异常不能触发浏览器登录。 */
|
|
13
|
+
async function verify(session, options = {}) {
|
|
14
|
+
if (!session || !session.access_token) { return false; }
|
|
15
|
+
const controller = new AbortController();
|
|
16
|
+
const timer = setTimeout(() => controller.abort(), options.timeoutMs || 15000);
|
|
17
|
+
try {
|
|
18
|
+
const response = await (options.fetch || globalThis.fetch)(`${session.base_url.replace(/\/$/, '')}/openapi/cli/v1/auth/status`, {
|
|
19
|
+
headers: { authorization: `Bearer ${session.access_token}`, accept: 'application/json' },
|
|
20
|
+
redirect: 'error', signal: controller.signal,
|
|
21
|
+
});
|
|
22
|
+
if ([401, 403].includes(response.status)) { await response.body?.cancel(); return false; }
|
|
23
|
+
if (!response.ok) { await response.body?.cancel(); throw failure('QWENWORK_LOGIN_CHECK_FAILED'); }
|
|
24
|
+
const reader = response.body?.getReader();
|
|
25
|
+
if (!reader) { throw failure('QWENWORK_LOGIN_CHECK_FAILED'); }
|
|
26
|
+
const chunks = []; let size = 0;
|
|
27
|
+
try {
|
|
28
|
+
for (let part = await reader.read(); !part.done; part = await reader.read()) {
|
|
29
|
+
size += part.value.byteLength;
|
|
30
|
+
if (size > 65536) { await reader.cancel(); throw failure('QWENWORK_LOGIN_CHECK_FAILED'); }
|
|
31
|
+
chunks.push(part.value);
|
|
32
|
+
}
|
|
33
|
+
} finally { reader.releaseLock(); }
|
|
34
|
+
const value = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
35
|
+
if (value.status !== 'ok' || value.can_auto_use !== true || value.auth_mode !== 'token') {
|
|
36
|
+
if (['not_logged_in', 'invalid_access_token', 'token_expired', 'auth_required'].includes(value.status)) { return false; }
|
|
37
|
+
throw failure('QWENWORK_LOGIN_CHECK_FAILED');
|
|
38
|
+
}
|
|
39
|
+
if (value.corp_id !== session.corp_id || value.user_id !== session.user_id ||
|
|
40
|
+
(options.corpId && value.corp_id !== options.corpId)) { throw failure('QWENWORK_LOGIN_IDENTITY_MISMATCH'); }
|
|
41
|
+
return true;
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if (error instanceof CliError) { throw error; }
|
|
44
|
+
throw failure('QWENWORK_LOGIN_CHECK_FAILED');
|
|
45
|
+
} finally { clearTimeout(timer); }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 公开入口复用、刷新登录态;确需授权时复用普通 login 的前台 OAuth 流程。 */
|
|
49
|
+
async function ensureQwenWorkLogin(options = {}, dependencies = {}) {
|
|
50
|
+
const resolution = resolveTokenSession(options);
|
|
51
|
+
if (resolution.status === 'profile_required') { throw failure('AUTH_PROFILE_REQUIRED'); }
|
|
52
|
+
let session = loadTokenSession(options);
|
|
53
|
+
if (session && options.endpoint && new URL(session.base_url).origin !== new URL(options.endpoint).origin) {
|
|
54
|
+
session = null;
|
|
55
|
+
}
|
|
56
|
+
if (await verify(session, options)) { return summary(true); }
|
|
57
|
+
if (session?.refresh_token) {
|
|
58
|
+
try {
|
|
59
|
+
const refreshed = await (dependencies.refresh || tokenRefresh)(options);
|
|
60
|
+
if (refreshed && refreshed.ok !== false && refreshed.access_token) {
|
|
61
|
+
if (await verify(refreshed, options)) { return summary(true); }
|
|
62
|
+
} else if (!isRefreshAuthRequired(refreshed)) { throw failure('QWENWORK_LOGIN_REFRESH_FAILED'); }
|
|
63
|
+
} catch (error) {
|
|
64
|
+
if (!isRefreshAuthRequired(error) && ![401, 403].includes(error.statusCode)) {
|
|
65
|
+
if (error instanceof CliError) { throw error; }
|
|
66
|
+
throw failure('QWENWORK_LOGIN_REFRESH_FAILED');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (options.checkOnly) { return summary(false); }
|
|
71
|
+
if (isEnvTokenAuthMode(options.env || process.env)) { throw failure('QWENWORK_LOGIN_FAILED'); }
|
|
72
|
+
const result = await (dependencies.login || tokenLogin)(options);
|
|
73
|
+
if (!result || result.ok === false || !result.access_token || !await verify(result, options)) {
|
|
74
|
+
throw failure('QWENWORK_LOGIN_FAILED');
|
|
75
|
+
}
|
|
76
|
+
return summary(true);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function run(args, options = {}) {
|
|
80
|
+
const result = await ensureQwenWorkLogin({ ...options, checkOnly: args.includes('--check-only') });
|
|
81
|
+
if (args.includes('--json') && !args.includes('--quiet') && process.env.YIDA_QUIET !== '1') {
|
|
82
|
+
console.log(JSON.stringify(result));
|
|
83
|
+
}
|
|
84
|
+
if (!result.ok) { process.exitCode = 1; }
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
module.exports = { ensureQwenWorkLogin, run };
|