openyida 2026.10.29 → 2026.10.30-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 +27 -2
- package/bin/yida-beta.js +5 -0
- package/bin/yida.js +20 -0
- package/lib/agent/provider-preflight.js +5 -3
- package/lib/auth/org.js +14 -3
- package/lib/auth/qwenwork-client.js +190 -0
- package/lib/auth/qwenwork-cloud.js +128 -0
- package/lib/auth/qwenwork-desktop.js +157 -0
- package/lib/auth/qwenwork-login.js +149 -0
- package/lib/auth/qwenwork.js +37 -0
- package/lib/auth/session-identity.js +23 -0
- package/lib/auth/token-auth.js +25 -4
- package/lib/auth/token-store.js +4 -0
- package/lib/core/agent-capabilities.js +61 -5
- package/lib/core/command-manifest.js +7 -4
- package/lib/core/locales/en.js +20 -3
- package/lib/core/locales/zh.js +19 -2
- package/lib/core/qwenwork-login-guidance.js +35 -0
- package/lib/core/utils.js +4 -0
- package/package.json +1 -1
- package/scripts/postinstall.js +3 -1
- package/yida-skills/SKILL.md +15 -1
- package/yida-skills/references/qwenwork-cloud-login.md +22 -0
- package/yida-skills/references/qwenwork-desktop-login.md +12 -0
- package/yida-skills/references/setup-and-env.md +12 -3
- package/yida-skills/references/source-repair.md +2 -2
- package/yida-skills/references/user-facing-summary.md +45 -0
- package/yida-skills/skills/yida-app/SKILL.md +1 -1
- package/yida-skills/skills/yida-app/workflow/step-7-page-code.md +1 -1
- package/yida-skills/skills/yida-app/workflow/step-8-publish-navigation.md +2 -0
- package/yida-skills/skills/yida-app/workflow/step-9-output-finish.md +7 -5
- package/yida-skills/skills/yida-canvas-custom-page/SKILL.md +3 -1
- package/yida-skills/skills/yida-canvas-upgrade/SKILL.md +6 -4
- package/yida-skills/skills/yida-canvas-upgrade/references/migration-examples.md +1 -1
- package/yida-skills/skills/yida-custom-page/SKILL.md +4 -2
- package/yida-skills/skills/yida-design/references/ask-human-interaction-contract.md +5 -1
- package/yida-skills/skills/yida-design/sub_skill/page-design/SKILL.md +2 -0
- package/yida-skills/skills/yida-design/sub_skill/ui-improve/SKILL.md +3 -1
- package/yida-skills/skills/yida-login/SKILL.md +24 -1
- package/yida-skills/skills/yida-openyida-publish-guard/SKILL.md +2 -0
- package/yida-skills/skills/yida-publish-page/SKILL.md +7 -5
- package/yida-skills/skills/yida-rechart/SKILL.md +1 -1
- package/yida-skills/skills/yida-upgrade-app-theme/SKILL.md +2 -0
- package/yida-skills/skills-index.json +4 -4
package/README.md
CHANGED
|
@@ -143,6 +143,31 @@ openyida login --check-only --json
|
|
|
143
143
|
openyida auth status
|
|
144
144
|
```
|
|
145
145
|
|
|
146
|
+
In QwenWork web, `openyida login --qwenwork --json` (or ordinary `login`) sends the sandbox Agent Token and gateway origin directly to the selected YiDA authentication endpoint. YiDA validates the identity through the trusted gateway’s fixed `/api/agent-gw/yida/user_info` endpoint, then issues its own short-lived session. This login flow no longer calls the legacy exchange or `/user/info` endpoints. Login preparation stays silent; failures stop without browser OAuth. Desktop and other hosts retain their existing authentication paths.
|
|
147
|
+
|
|
148
|
+
QwenWork login supports DingTalk organizations, QwenWork organizations and personal ODS identities. Local ODS tenant IDs retain their `yida` prefix. When the server supplies a refresh token, the CLI stores it for the standard refresh flow. Optional server fields `identity_type` (`PERSONAL` / `ORGANIZATION`) and `organization_type` (`DINGTALK` / `QWENWORK` / `OTHER`, organizations only) are preserved in the session and `auth status`; historical sessions with both fields absent, null or empty default to `ORGANIZATION` / `DINGTALK`. Authentication failures expose bounded `status`, `stage`, `exchange_id`, HTTP `status_code` and local validation `reason` in JSON diagnostics, without response bodies or credentials.
|
|
149
|
+
|
|
150
|
+
For QwenWork cloud identity diagnostics, run inside the actual cloud execution environment:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
openyida auth qwenwork user-info --json
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
This diagnostic command uses the legacy user-token protocol; it does not validate the current web Agent Token login flow. Use `openyida login --check-only --json` to check the YiDA session.
|
|
157
|
+
|
|
158
|
+
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.
|
|
159
|
+
|
|
160
|
+
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.
|
|
161
|
+
|
|
162
|
+
```js
|
|
163
|
+
const { exchangeQwenWorkAgentToken, getQwenWorkUserInfo } = require('openyida/lib/auth/qwenwork-client');
|
|
164
|
+
async function readCloudUser() {
|
|
165
|
+
const credential = await exchangeQwenWorkAgentToken();
|
|
166
|
+
// The result contains QwenWork identifiers, not YiDA IDs.
|
|
167
|
+
return getQwenWorkUserInfo(credential);
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
146
171
|
To reuse an existing shared login profile for the current project, switch first instead of starting a new OAuth login:
|
|
147
172
|
|
|
148
173
|
```bash
|
|
@@ -457,9 +482,9 @@ When preparing a PR that needs a personal Fork, or when you ask how to support t
|
|
|
457
482
|
|
|
458
483
|
| Command | Description |
|
|
459
484
|
|---------|-------------|
|
|
460
|
-
| `openyida login [target-url] [--env <name>\|--intl\|--overseas\|--global\|--yidaapps\|--alibaba] [--client-id <clientId>] [--endpoint <url>] [--no-browser]` | Login with OAuth token mode |
|
|
485
|
+
| `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
486
|
| `openyida logout` | Logout / unbind current project auth |
|
|
462
|
-
| `openyida auth <status\|login\|refresh\|logout\|profiles\|profile switch
|
|
487
|
+
| `openyida auth <status\|login\|refresh\|logout\|profiles\|profile switch\|qwenwork user-info> [--json]` | Token login state and profile management |
|
|
463
488
|
| `openyida org <list\|switch> [--json] [--corp-id <corpId>]` | Organization management (list / switch existing profiles first) |
|
|
464
489
|
| `openyida env [--json\|setup\|list\|show\|switch\|add\|remove] [options]` | Detect AI tool environment & token login state |
|
|
465
490
|
|
package/bin/yida-beta.js
ADDED
package/bin/yida.js
CHANGED
|
@@ -598,7 +598,23 @@ async function main() {
|
|
|
598
598
|
});
|
|
599
599
|
assertManagedInvocation(command, rawArgs);
|
|
600
600
|
applyQuietFlag();
|
|
601
|
+
// 一次性票据只在当前进程消费,不能先自更新并重启命令。
|
|
602
|
+
switch (command) {
|
|
603
|
+
case 'login-qwenwork':
|
|
604
|
+
await require('../lib/auth/qwenwork-desktop').run(args);
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
601
607
|
applyGlobalEnvironmentFlags();
|
|
608
|
+
// 网页态由 CLI 消费宿主凭据;桌面态继续消费宿主准备的登录态。
|
|
609
|
+
const webLogin = (command === 'login' || (command === 'auth' && args[0] === 'login')) &&
|
|
610
|
+
require('../lib/core/qwenwork-login-guidance').isQwenWorkWebRuntime(require('../lib/core/utils').detectRuntimeCapabilities());
|
|
611
|
+
if ((command === 'login' && args.includes('--qwenwork')) || webLogin) {
|
|
612
|
+
const loginArgs = applyLoginEnvironmentFlags(command === 'auth' ? args.slice(1) : args, { inferTargetUrl: true });
|
|
613
|
+
if (hasHelpFlag(loginArgs)) { printLoginHelp(); return; }
|
|
614
|
+
assertNoUnsupportedLegacyLoginFlags(rawArgs, loginArgs);
|
|
615
|
+
await require('../lib/auth/qwenwork-login').run(loginArgs, buildTokenLoginOptions(loginArgs));
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
602
618
|
|
|
603
619
|
const { maybeAutoUpdate } = require('../lib/core/update');
|
|
604
620
|
const updateResult = await maybeAutoUpdate({
|
|
@@ -718,6 +734,10 @@ async function main() {
|
|
|
718
734
|
|
|
719
735
|
case 'auth': {
|
|
720
736
|
const subCommand = args[0];
|
|
737
|
+
if (subCommand === 'qwenwork') {
|
|
738
|
+
await require('../lib/auth/qwenwork').run(args.slice(1));
|
|
739
|
+
break;
|
|
740
|
+
}
|
|
721
741
|
const authArgs = applyLoginEnvironmentFlags(args.slice(1), { inferTargetUrl: true });
|
|
722
742
|
const { getAuthStatus } = require('../lib/core/utils');
|
|
723
743
|
const { tokenLogin, tokenLogout, tokenRefresh } = require('../lib/auth/token-auth');
|
|
@@ -139,13 +139,15 @@ function probe(executable, args, rpc, options = {}) {
|
|
|
139
139
|
child = (options.probeSpawn || spawn)(command.executable, command.args, {
|
|
140
140
|
env, cwd: os.tmpdir(), shell: false, windowsHide: true,
|
|
141
141
|
windowsVerbatimArguments: command.windowsVerbatimArguments,
|
|
142
|
-
|
|
142
|
+
// Finite Windows probes do not consume input. Avoid an unused redirected
|
|
143
|
+
// stdin pipe during PowerShell startup; ACP still needs its live pipe.
|
|
144
|
+
detached: platform !== 'win32', stdio: [platform === 'win32' && !rpc ? 'ignore' : 'pipe', 'pipe', 'pipe'],
|
|
143
145
|
});
|
|
144
146
|
signals.once('SIGINT', interrupted);
|
|
145
147
|
signals.once('SIGTERM', interrupted);
|
|
146
148
|
timer = setTimeout(() => { stop(); finish({ status: 'probe_timeout' }); }, options.timeoutMs || 15000);
|
|
147
149
|
child.on('error', () => finish({ status: 'launch_failed' }));
|
|
148
|
-
child.stdin.on('error', () => {});
|
|
150
|
+
if (child.stdin) { child.stdin.on('error', () => {}); }
|
|
149
151
|
child.stderr.on('data', chunk => { output = (output + chunk.toString()).slice(-65536); });
|
|
150
152
|
child.stdout.on('data', chunk => {
|
|
151
153
|
output = (output + chunk.toString()).slice(-65536);
|
|
@@ -171,7 +173,7 @@ function probe(executable, args, rpc, options = {}) {
|
|
|
171
173
|
? { protocolVersion: 1, clientCapabilities: {}, clientInfo: { name: 'openyida-preflight', version: '1' } }
|
|
172
174
|
: { clientInfo: { name: 'openyida-preflight', version: '1' }, capabilities: {} };
|
|
173
175
|
child.stdin.write(JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize', params }) + '\n');
|
|
174
|
-
} else { child.stdin.end(); }
|
|
176
|
+
} else if (child.stdin) { child.stdin.end(); }
|
|
175
177
|
} catch { finish({ status: 'launch_failed' }); }
|
|
176
178
|
});
|
|
177
179
|
}
|
package/lib/auth/org.js
CHANGED
|
@@ -19,6 +19,10 @@ const {
|
|
|
19
19
|
saveTokenSession,
|
|
20
20
|
} = require('./token-store');
|
|
21
21
|
const { tokenLogin } = require('./token-auth');
|
|
22
|
+
const { sessionIdentity } = require('./session-identity');
|
|
23
|
+
const { detectRuntimeCapabilities } = require('../core/utils');
|
|
24
|
+
const { isQwenWorkWebRuntime } = require('../core/qwenwork-login-guidance');
|
|
25
|
+
const { t } = require('../core/i18n');
|
|
22
26
|
|
|
23
27
|
const LIST_CORP_INFOS_PATH = '/query/userservice/listCorpInfos.json';
|
|
24
28
|
|
|
@@ -146,6 +150,7 @@ function buildSwitchResult(status, currentSession, nextSession, message, extra =
|
|
|
146
150
|
auth_mode: 'token',
|
|
147
151
|
status,
|
|
148
152
|
can_auto_use: true,
|
|
153
|
+
...sessionIdentity(nextSession),
|
|
149
154
|
previous_corp_id: currentSession && currentSession.corp_id,
|
|
150
155
|
corp_id: nextSession.corp_id,
|
|
151
156
|
corp_name: nextSession.corp_name,
|
|
@@ -191,6 +196,7 @@ async function switchOrganization(targetCorpId, options = {}) {
|
|
|
191
196
|
ok: true,
|
|
192
197
|
auth_mode: 'token',
|
|
193
198
|
status: 'already_in_org',
|
|
199
|
+
...sessionIdentity(currentSession),
|
|
194
200
|
can_auto_use: true,
|
|
195
201
|
corp_id: currentSession.corp_id,
|
|
196
202
|
corp_name: currentSession.corp_name,
|
|
@@ -231,7 +237,12 @@ async function switchOrganization(targetCorpId, options = {}) {
|
|
|
231
237
|
corpId: targetCorpId,
|
|
232
238
|
};
|
|
233
239
|
|
|
234
|
-
const
|
|
240
|
+
const cloud = isQwenWorkWebRuntime(options.runtime || detectRuntimeCapabilities({ env: options.env || process.env }));
|
|
241
|
+
const login = options.tokenLogin || (cloud ? async opts => {
|
|
242
|
+
const { ensureQwenWorkLogin } = require('./qwenwork-login');
|
|
243
|
+
await ensureQwenWorkLogin(opts);
|
|
244
|
+
return loadTokenSession(opts);
|
|
245
|
+
} : tokenLogin);
|
|
235
246
|
const temporaryLogin = createTemporaryLoginOptions(loginOptions);
|
|
236
247
|
let loginResult;
|
|
237
248
|
try {
|
|
@@ -266,8 +277,8 @@ async function switchOrganization(targetCorpId, options = {}) {
|
|
|
266
277
|
'switched',
|
|
267
278
|
currentSession,
|
|
268
279
|
savedSession,
|
|
269
|
-
'switched organization by OAuth login',
|
|
270
|
-
{ switch_source: 'oauth_login' }
|
|
280
|
+
cloud ? t('qwenwork.organization_switch_success') : 'switched organization by OAuth login',
|
|
281
|
+
{ switch_source: cloud ? 'qwenwork_cloud' : 'oauth_login' }
|
|
271
282
|
);
|
|
272
283
|
}
|
|
273
284
|
|
|
@@ -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,128 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { sessionIdentity } = require('./session-identity');
|
|
4
|
+
|
|
5
|
+
const { CliError } = require('../core/cli-error');
|
|
6
|
+
const { t } = require('../core/i18n');
|
|
7
|
+
const { DINGTALK_OAUTH_CLIENT_ID } = require('../core/env-manager');
|
|
8
|
+
const { resolveTokenBaseUrl } = require('./token-auth');
|
|
9
|
+
|
|
10
|
+
function failure(code, details = {}) {
|
|
11
|
+
return new CliError(t('qwenwork.login_check_failed'), { code: `QWENWORK_CLOUD_${code}`, details });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const CORP_ID = /^(?:(?:ding|yida|ps)[0-9a-f]{32}|dingd8e1123006514592)$/;
|
|
15
|
+
|
|
16
|
+
// Only bounded protocol identifiers are diagnostic data; never forward the response body or message.
|
|
17
|
+
function diagnostics(value, statusCode, credential = {}) {
|
|
18
|
+
const details = { stage: 'yida_exchange', status_code: statusCode };
|
|
19
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) { return details; }
|
|
20
|
+
const secrets = [credential.access_token, value.access_token, value.refresh_token, value.refreshToken].filter(token => typeof token === 'string' && token);
|
|
21
|
+
const publicValue = text => typeof text === 'string' && !secrets.some(token => text.includes(token));
|
|
22
|
+
if (publicValue(value.status) && value.status.length <= 64 && /^(?:ok|error|failure|[a-z][a-z0-9]*_[a-z0-9_]+)$/.test(value.status)
|
|
23
|
+
&& !/^(?:qwex|dws)_/.test(value.status)) { details.status = value.status; }
|
|
24
|
+
if (publicValue(value.stage) && ['request', 'qwen_verify', 'token_issue'].includes(value.stage)) { details.stage = value.stage; }
|
|
25
|
+
if (publicValue(value.exchange_id) && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value.exchange_id)) {
|
|
26
|
+
details.exchange_id = value.exchange_id;
|
|
27
|
+
}
|
|
28
|
+
return details;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function origin(value, details = {}) {
|
|
32
|
+
try {
|
|
33
|
+
const url = new URL(value);
|
|
34
|
+
if (typeof value !== 'string' || value.length > 2048 || /\s/.test(value) || url.protocol !== 'https:' || url.username || url.password || url.pathname !== '/' || url.search || url.hash) { throw new Error(); }
|
|
35
|
+
return url.origin;
|
|
36
|
+
} catch { throw failure('ENDPOINT_INVALID', details); }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function exchangeSession(credential, baseUrl, options) {
|
|
40
|
+
const controller = new AbortController();
|
|
41
|
+
const timeoutMs = options.timeoutMs === undefined ? 15000 : options.timeoutMs;
|
|
42
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) { throw failure('INVALID_TIMEOUT'); }
|
|
43
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
44
|
+
try {
|
|
45
|
+
const response = await (options.fetch || globalThis.fetch)(`${baseUrl}/openapi/cli/v1/auth/qwenwork/token`, {
|
|
46
|
+
method: 'POST', redirect: 'error', signal: controller.signal,
|
|
47
|
+
headers: { authorization: `Bearer ${credential.access_token}`, accept: 'application/json', 'content-type': 'application/json' },
|
|
48
|
+
// corp_id constrains the resulting YiDA organization or personal space for every identity type.
|
|
49
|
+
body: JSON.stringify({ gateway_url: credential.base_url, ...(options.corpId ? { corp_id: options.corpId } : {}) }),
|
|
50
|
+
});
|
|
51
|
+
const reader = response.body?.getReader();
|
|
52
|
+
if (!reader) { throw failure(response.ok ? 'RESPONSE_INVALID' : 'HTTP_ERROR', diagnostics(null, response.status)); }
|
|
53
|
+
const chunks = []; let bytes = 0;
|
|
54
|
+
try {
|
|
55
|
+
for (let part = await reader.read(); !part.done; part = await reader.read()) {
|
|
56
|
+
bytes += part.value.byteLength;
|
|
57
|
+
if (bytes > 65536) { await reader.cancel(); throw failure(response.ok ? 'RESPONSE_INVALID' : 'HTTP_ERROR', { ...diagnostics(null, response.status), reason: 'response_too_large' }); }
|
|
58
|
+
chunks.push(part.value);
|
|
59
|
+
}
|
|
60
|
+
} finally { reader.releaseLock(); }
|
|
61
|
+
let value;
|
|
62
|
+
try { value = JSON.parse(Buffer.concat(chunks).toString('utf8')); }
|
|
63
|
+
catch { throw failure(response.ok ? 'RESPONSE_INVALID' : 'HTTP_ERROR', { ...diagnostics(null, response.status), reason: 'invalid_json' }); }
|
|
64
|
+
const details = diagnostics(value, response.status, credential);
|
|
65
|
+
if (!response.ok) { throw failure('HTTP_ERROR', details); }
|
|
66
|
+
return { value, details };
|
|
67
|
+
} catch (error) {
|
|
68
|
+
// 上游正文和异常可能回显凭据,仅保留本模块生成的固定错误码。
|
|
69
|
+
if (error instanceof CliError && /^QWENWORK_CLOUD_/.test(error.code)) { throw error; }
|
|
70
|
+
throw failure(controller.signal.aborted ? 'REQUEST_TIMED_OUT' : 'REQUEST_FAILED', { stage: 'yida_exchange' });
|
|
71
|
+
} finally { clearTimeout(timer); }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** 只读取本次沙箱凭据,由宜搭服务端向受信任网关验证身份。 */
|
|
75
|
+
function sandboxCredential(options) {
|
|
76
|
+
const env = options.env || process.env;
|
|
77
|
+
const token = ['QWENWORK_AGENT_TOKEN', 'MULE_AGENT_TOKEN', 'AGENT_TOKEN'].map(name => env[name]).find(value => typeof value === 'string' && value);
|
|
78
|
+
if (!token) { throw new CliError(t('qwenwork.login_check_failed'), { code: 'QWENWORK_CREDENTIAL_MISSING' }); }
|
|
79
|
+
if (token.length > 4096 || /\s/.test(token)) { throw failure('CREDENTIAL_INVALID'); }
|
|
80
|
+
const gateway = env.QWENWORK_GATEWAY_URL || env.MULE_GATEWAY_URL;
|
|
81
|
+
if (!gateway) { throw new CliError(t('qwenwork.login_check_failed'), { code: 'QWENWORK_GATEWAY_MISSING' }); }
|
|
82
|
+
return { access_token: token, base_url: origin(gateway) };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** 只向宜搭提交 Agent Token 和网关地址;返回会话供调用方校验后保存。 */
|
|
86
|
+
async function exchangeQwenWorkCloudSession(options = {}) {
|
|
87
|
+
const baseUrl = origin(options.endpoint || (options.env || process.env).OPENYIDA_ENDPOINT || resolveTokenBaseUrl(options));
|
|
88
|
+
const credential = sandboxCredential(options);
|
|
89
|
+
try {
|
|
90
|
+
const startedAt = Date.now();
|
|
91
|
+
const { value, details } = await exchangeSession(credential, baseUrl, options);
|
|
92
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) { throw failure('RESPONSE_INVALID', { ...details, reason: 'response_shape' }); }
|
|
93
|
+
if (value.status !== 'ok') { throw failure('EXCHANGE_REJECTED', details); }
|
|
94
|
+
const env = options.env || process.env;
|
|
95
|
+
const hostTokens = ['QWENWORK_AGENT_TOKEN', 'MULE_AGENT_TOKEN', 'AGENT_TOKEN'].map(name => env[name]).filter(Boolean);
|
|
96
|
+
const validToken = token => typeof token === 'string' && !!token && !/\s/.test(token) && token.length <= 32768 && !hostTokens.includes(token);
|
|
97
|
+
// Older cloud deployments may return access-only sessions. Preserve a refresh token when supplied.
|
|
98
|
+
const refreshToken = value.refresh_token === undefined ? value.refreshToken : value.refresh_token;
|
|
99
|
+
const invalid = [
|
|
100
|
+
[value.auth_mode !== 'token', 'auth_mode'],
|
|
101
|
+
[value.can_auto_use !== true, 'can_auto_use'],
|
|
102
|
+
[value.token_type !== 'Bearer', 'token_type'],
|
|
103
|
+
[value.credential_source !== 'qwenwork_cloud', 'credential_source'],
|
|
104
|
+
[!validToken(value.access_token), 'access_token'],
|
|
105
|
+
[!Number.isInteger(value.expires_in) || value.expires_in <= 0 || value.expires_in > 1800, 'expires_in'],
|
|
106
|
+
[typeof value.corp_id !== 'string' || !CORP_ID.test(value.corp_id), 'corp_id'],
|
|
107
|
+
[typeof value.user_id !== 'string' || !value.user_id || value.user_id.trim() !== value.user_id || value.user_id.length > 256, 'user_id'],
|
|
108
|
+
[options.corpId && value.corp_id !== options.corpId, 'corp_id_mismatch'],
|
|
109
|
+
[options.userId && value.user_id !== options.userId, 'user_id_mismatch'],
|
|
110
|
+
['exchange_ticket' in value, 'exchange_ticket'],
|
|
111
|
+
[refreshToken !== undefined && !validToken(refreshToken), 'refresh_token'],
|
|
112
|
+
[value.client_id !== undefined && value.client_id !== (options.clientId || DINGTALK_OAUTH_CLIENT_ID), 'client_id'],
|
|
113
|
+
].find(([rejected]) => rejected);
|
|
114
|
+
if (invalid) { throw failure('RESPONSE_INVALID', { ...details, reason: invalid[1] }); }
|
|
115
|
+
return {
|
|
116
|
+
...sessionIdentity(value),
|
|
117
|
+
auth_mode: 'token', credential_source: 'qwenwork_cloud', token_type: 'Bearer',
|
|
118
|
+
// 可信宜搭签发服务决定业务域名;认证请求仍只发往最初选择的 origin。
|
|
119
|
+
access_token: value.access_token, refresh_token: refreshToken, base_url: value.base_url === undefined ? baseUrl : origin(value.base_url, { ...details, reason: 'base_url' }),
|
|
120
|
+
client_id: options.clientId || DINGTALK_OAUTH_CLIENT_ID,
|
|
121
|
+
corp_id: value.corp_id, user_id: value.user_id,
|
|
122
|
+
user_name: typeof value.user_name === 'string' ? value.user_name : undefined,
|
|
123
|
+
expires_at: startedAt + value.expires_in * 1000,
|
|
124
|
+
};
|
|
125
|
+
} finally { credential.access_token = undefined; }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = { exchangeQwenWorkCloudSession };
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { sessionIdentity } = require('./session-identity');
|
|
4
|
+
|
|
5
|
+
const { CliError } = require('../core/cli-error');
|
|
6
|
+
const { buildQwenWorkLoginInvocation } = require('../core/qwenwork-login-guidance');
|
|
7
|
+
const { t } = require('../core/i18n');
|
|
8
|
+
const { DINGTALK_OAUTH_CLIENT_ID } = require('../core/env-manager');
|
|
9
|
+
const { saveTokenSession, loadTokenSession, isEnvTokenAuthMode } = require('./token-store');
|
|
10
|
+
const { readManagedContext } = require('../agent/managed-context');
|
|
11
|
+
|
|
12
|
+
const TICKET_ENV = 'OPENYIDA_QWENWORK_EXCHANGE_TICKET';
|
|
13
|
+
const TICKET_CONTEXT_ENV = 'OPENYIDA_QWENWORK_EXCHANGE_TICKET_CONTEXT';
|
|
14
|
+
const CONSUME_PATH = '/openapi/cli/v1/auth/qwenwork/desktop/consume';
|
|
15
|
+
const MAX_RESPONSE_BYTES = 65536;
|
|
16
|
+
|
|
17
|
+
function fail(code) {
|
|
18
|
+
return new CliError(t('qwenwork.desktop_login_failed'), {
|
|
19
|
+
code: `QWENWORK_DESKTOP_${code}`,
|
|
20
|
+
details: { invocation: buildQwenWorkLoginInvocation() },
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function parseTicket(raw, context) {
|
|
25
|
+
try {
|
|
26
|
+
if (typeof raw !== 'string' || !raw || /\s/.test(raw) || raw.length > 32768 || typeof context !== 'string' || context.length > 8192) { throw new Error(); }
|
|
27
|
+
const value = JSON.parse(context);
|
|
28
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) ||
|
|
29
|
+
!Number.isSafeInteger(value.expires_at) || value.expires_at <= Date.now() || value.expires_at > Date.now() + 300000 ||
|
|
30
|
+
typeof value.corp_id !== 'string' || !value.corp_id || typeof value.user_id !== 'string' || !value.user_id ||
|
|
31
|
+
typeof value.exchange_id !== 'string' || !value.exchange_id ||
|
|
32
|
+
'access_token' in value || 'refresh_token' in value || 'accessToken' in value || 'refreshToken' in value) { throw new Error(); }
|
|
33
|
+
return { ...value, exchange_ticket: raw };
|
|
34
|
+
} catch {
|
|
35
|
+
throw fail('TICKET_MISSING_OR_INVALID');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function origin(value) {
|
|
40
|
+
try {
|
|
41
|
+
const url = new URL(value);
|
|
42
|
+
if (typeof value !== 'string' || value.length > 2048 || /\s/.test(value) || url.protocol !== 'https:' || url.username || url.password || url.search || url.hash || url.pathname !== '/') { throw new Error(); }
|
|
43
|
+
return url.origin;
|
|
44
|
+
} catch {
|
|
45
|
+
throw fail('ENDPOINT_INVALID');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function requestJson(url, init, options) {
|
|
50
|
+
const controller = new AbortController();
|
|
51
|
+
const timer = setTimeout(() => controller.abort(), options.timeoutMs || 15000);
|
|
52
|
+
try {
|
|
53
|
+
const response = await (options.fetch || globalThis.fetch)(url, { ...init, redirect: 'error', signal: controller.signal });
|
|
54
|
+
if (!response.ok) {
|
|
55
|
+
if (response.body) { await response.body.cancel(); }
|
|
56
|
+
throw fail('REQUEST_REJECTED');
|
|
57
|
+
}
|
|
58
|
+
const reader = response.body && response.body.getReader();
|
|
59
|
+
if (!reader) { throw fail('RESPONSE_INVALID'); }
|
|
60
|
+
const chunks = [];
|
|
61
|
+
let bytes = 0;
|
|
62
|
+
try {
|
|
63
|
+
for (let next = await reader.read(); !next.done; next = await reader.read()) {
|
|
64
|
+
bytes += next.value.byteLength;
|
|
65
|
+
if (bytes > MAX_RESPONSE_BYTES) { await reader.cancel(); throw fail('RESPONSE_INVALID'); }
|
|
66
|
+
chunks.push(next.value);
|
|
67
|
+
}
|
|
68
|
+
} finally {
|
|
69
|
+
reader.releaseLock();
|
|
70
|
+
}
|
|
71
|
+
const value = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
72
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) { throw fail('RESPONSE_INVALID'); }
|
|
73
|
+
return value;
|
|
74
|
+
} catch (error) {
|
|
75
|
+
// 服务端和网络异常可能回显凭据,只透传本模块生成的固定错误。
|
|
76
|
+
if (error instanceof CliError && /^QWENWORK_DESKTOP_/.test(error.code)) { throw error; }
|
|
77
|
+
throw fail(controller.signal.aborted ? 'REQUEST_TIMED_OUT' : 'REQUEST_FAILED');
|
|
78
|
+
} finally {
|
|
79
|
+
clearTimeout(timer);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** 客户端内部入口:消费一次性兑换票并持久化双 Token,不调用或重启普通 login。 */
|
|
84
|
+
async function loginQwenWork(options = {}) {
|
|
85
|
+
const env = options.env || process.env;
|
|
86
|
+
const raw = options.exchangeTicket || env[TICKET_ENV];
|
|
87
|
+
const context = options.ticketContext || env[TICKET_CONTEXT_ENV];
|
|
88
|
+
delete env[TICKET_ENV];
|
|
89
|
+
delete env[TICKET_CONTEXT_ENV];
|
|
90
|
+
if (isEnvTokenAuthMode(env) || readManagedContext(env)) { throw fail('MANAGED_AUTH_CONFLICT'); }
|
|
91
|
+
const ticket = parseTicket(raw, context);
|
|
92
|
+
const baseUrl = origin(ticket.base_url);
|
|
93
|
+
const expectedEndpoint = options.endpoint || env.OPENYIDA_ENDPOINT;
|
|
94
|
+
if (expectedEndpoint && origin(expectedEndpoint) !== baseUrl) { throw fail('ENDPOINT_MISMATCH'); }
|
|
95
|
+
if (options.corpId && options.corpId !== ticket.corp_id) { throw fail('IDENTITY_MISMATCH'); }
|
|
96
|
+
const startedAt = Date.now();
|
|
97
|
+
const value = await requestJson(`${baseUrl}${CONSUME_PATH}`, {
|
|
98
|
+
method: 'POST', headers: { accept: 'application/json', 'content-type': 'application/json', authorization: `Bearer ${ticket.exchange_ticket}` },
|
|
99
|
+
body: '{}',
|
|
100
|
+
}, options);
|
|
101
|
+
const expiresAt = startedAt + Number(value.expires_in) * 1000;
|
|
102
|
+
if (value.status !== 'ok' || value.auth_mode !== 'token' || value.can_auto_use !== true ||
|
|
103
|
+
value.token_type !== 'Bearer' || value.credential_source !== 'qwenwork_desktop' ||
|
|
104
|
+
'refreshToken' in value || 'exchange_ticket' in value ||
|
|
105
|
+
typeof value.access_token !== 'string' || !value.access_token || /\s/.test(value.access_token) || value.access_token.length > 32768 ||
|
|
106
|
+
!Number.isInteger(value.expires_in) || value.expires_in < 1 || value.expires_in > 1800 ||
|
|
107
|
+
typeof value.refresh_token !== 'string' || !value.refresh_token || /\s/.test(value.refresh_token) || value.refresh_token.length > 32768 ||
|
|
108
|
+
!Number.isInteger(value.refresh_token_expires_in) || value.refresh_token_expires_in < 1 ||
|
|
109
|
+
(value.client_id && value.client_id !== DINGTALK_OAUTH_CLIENT_ID) ||
|
|
110
|
+
value.corp_id !== ticket.corp_id || value.user_id !== ticket.user_id || value.exchange_id !== ticket.exchange_id) { throw fail('CREDENTIAL_INVALID'); }
|
|
111
|
+
// 票据仍在原签票 origin 消费,业务域名仅来自受信的消费响应。
|
|
112
|
+
const businessBaseUrl = value.base_url === undefined ? baseUrl : origin(value.base_url);
|
|
113
|
+
const verified = await requestJson(`${businessBaseUrl}/openapi/cli/v1/auth/status`, {
|
|
114
|
+
method: 'GET', headers: { authorization: `Bearer ${value.access_token}`, accept: 'application/json' },
|
|
115
|
+
}, options);
|
|
116
|
+
if (verified.status !== 'ok' || verified.auth_mode !== 'token' || verified.can_auto_use !== true ||
|
|
117
|
+
verified.corp_id !== value.corp_id || verified.user_id !== value.user_id || expiresAt <= Date.now()) { throw fail('IDENTITY_MISMATCH'); }
|
|
118
|
+
const session = saveTokenSession({
|
|
119
|
+
...sessionIdentity(value),
|
|
120
|
+
auth_mode: 'token', token_type: 'Bearer', access_token: value.access_token, refresh_token: value.refresh_token,
|
|
121
|
+
expires_at: expiresAt, base_url: businessBaseUrl, client_id: DINGTALK_OAUTH_CLIENT_ID,
|
|
122
|
+
corp_id: value.corp_id, user_id: value.user_id, credential_source: 'qwenwork_desktop',
|
|
123
|
+
}, options);
|
|
124
|
+
const restored = loadTokenSession(options);
|
|
125
|
+
if (!['user', 'project'].includes(session.persistence_scope) || !restored ||
|
|
126
|
+
restored.access_token !== value.access_token || restored.refresh_token !== value.refresh_token || restored.base_url !== businessBaseUrl ||
|
|
127
|
+
restored.corp_id !== value.corp_id || restored.user_id !== value.user_id) { throw fail('PERSISTENCE_FAILED'); }
|
|
128
|
+
// 只返回分类和登录结果;组织/用户标识、环境和存储细节不进入模型可见结果。
|
|
129
|
+
return { ok: true, status: 'ok', can_auto_use: true, ...sessionIdentity(session) };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function run(args, options = {}) {
|
|
133
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
134
|
+
const invocation = buildQwenWorkLoginInvocation();
|
|
135
|
+
if (args.includes('--json')) { console.log(JSON.stringify({ usage: t('qwenwork.desktop_login_usage'), invocation })); }
|
|
136
|
+
else { console.log(`${t('qwenwork.desktop_login_usage')}\n${t('qwenwork.desktop_login_standalone', invocation.command)}`); }
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const parsed = {};
|
|
140
|
+
const seen = new Set();
|
|
141
|
+
for (let index = 0; index < args.length; index++) {
|
|
142
|
+
const flag = args[index];
|
|
143
|
+
if (seen.has(flag)) { throw new CliError(t('qwenwork.desktop_login_usage'), { code: 'CLI_USAGE_ERROR', exitCode: 2, details: { invocation: buildQwenWorkLoginInvocation() } }); }
|
|
144
|
+
seen.add(flag);
|
|
145
|
+
if (['--json', '--quiet'].includes(flag)) { continue; }
|
|
146
|
+
const key = flag === '--corp-id' ? 'corpId' : flag === '--endpoint' ? 'endpoint'
|
|
147
|
+
: flag === '--exchange-ticket' ? 'exchangeTicket' : flag === '--ticket-context' ? 'ticketContext' : null;
|
|
148
|
+
const value = args[++index];
|
|
149
|
+
if (!key || !value || value.startsWith('--')) { throw new CliError(t('qwenwork.desktop_login_usage'), { code: 'CLI_USAGE_ERROR', exitCode: 2, details: { invocation: buildQwenWorkLoginInvocation() } }); }
|
|
150
|
+
parsed[key] = value;
|
|
151
|
+
}
|
|
152
|
+
const result = await loginQwenWork({ ...options, ...parsed });
|
|
153
|
+
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 })); }
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
module.exports = { TICKET_ENV, TICKET_CONTEXT_ENV, loginQwenWork, run };
|