openyida 2026.10.29-beta.2 → 2026.10.29-beta.3
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 +4 -0
- package/lib/auth/org.js +14 -3
- package/lib/auth/qwenwork-cloud.js +60 -24
- package/lib/auth/qwenwork-desktop.js +5 -2
- package/lib/auth/qwenwork-login.js +10 -6
- package/lib/auth/session-identity.js +23 -0
- package/lib/auth/token-auth.js +11 -2
- package/lib/auth/token-store.js +3 -0
- package/lib/core/locales/en.js +1 -0
- package/lib/core/locales/zh.js +1 -0
- package/lib/core/utils.js +4 -0
- package/package.json +5 -17
- package/scripts/install-qwenwork-pre-beta.js +0 -40
package/README.md
CHANGED
|
@@ -145,12 +145,16 @@ openyida auth status
|
|
|
145
145
|
|
|
146
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
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
|
+
|
|
148
150
|
For QwenWork cloud identity diagnostics, run inside the actual cloud execution environment:
|
|
149
151
|
|
|
150
152
|
```bash
|
|
151
153
|
openyida auth qwenwork user-info --json
|
|
152
154
|
```
|
|
153
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
|
+
|
|
154
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.
|
|
155
159
|
|
|
156
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.
|
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
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { sessionIdentity } = require('./session-identity');
|
|
4
|
+
|
|
3
5
|
const { CliError } = require('../core/cli-error');
|
|
4
6
|
const { t } = require('../core/i18n');
|
|
5
7
|
const { DINGTALK_OAUTH_CLIENT_ID } = require('../core/env-manager');
|
|
@@ -9,12 +11,30 @@ function failure(code, details = {}) {
|
|
|
9
11
|
return new CliError(t('qwenwork.login_check_failed'), { code: `QWENWORK_CLOUD_${code}`, details });
|
|
10
12
|
}
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
const ODS_CORP_ID = /^yida[0-9a-f]{32}$/;
|
|
15
|
+
const CORP_ID = /^(?:(?:ding|yida)[0-9a-f]{32}|dingd8e1123006514592)$/;
|
|
16
|
+
|
|
17
|
+
// Only bounded protocol identifiers are diagnostic data; never forward the response body or message.
|
|
18
|
+
function diagnostics(value, statusCode, credential = {}) {
|
|
19
|
+
const details = { stage: 'yida_exchange', status_code: statusCode };
|
|
20
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) { return details; }
|
|
21
|
+
const secrets = [credential.access_token, value.access_token, value.refresh_token, value.refreshToken].filter(token => typeof token === 'string' && token);
|
|
22
|
+
const publicValue = text => typeof text === 'string' && !secrets.some(token => text.includes(token));
|
|
23
|
+
if (publicValue(value.status) && value.status.length <= 64 && /^(?:ok|error|failure|[a-z][a-z0-9]*_[a-z0-9_]+)$/.test(value.status)
|
|
24
|
+
&& !/^(?:qwex|dws)_/.test(value.status)) { details.status = value.status; }
|
|
25
|
+
if (publicValue(value.stage) && ['request', 'qwen_verify', 'token_issue'].includes(value.stage)) { details.stage = value.stage; }
|
|
26
|
+
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)) {
|
|
27
|
+
details.exchange_id = value.exchange_id;
|
|
28
|
+
}
|
|
29
|
+
return details;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function origin(value, details = {}) {
|
|
13
33
|
try {
|
|
14
34
|
const url = new URL(value);
|
|
15
35
|
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(); }
|
|
16
36
|
return url.origin;
|
|
17
|
-
} catch { throw failure('ENDPOINT_INVALID'); }
|
|
37
|
+
} catch { throw failure('ENDPOINT_INVALID', details); }
|
|
18
38
|
}
|
|
19
39
|
|
|
20
40
|
async function exchangeSession(credential, baseUrl, options) {
|
|
@@ -26,27 +46,29 @@ async function exchangeSession(credential, baseUrl, options) {
|
|
|
26
46
|
const response = await (options.fetch || globalThis.fetch)(`${baseUrl}/openapi/cli/v1/auth/qwenwork/token`, {
|
|
27
47
|
method: 'POST', redirect: 'error', signal: controller.signal,
|
|
28
48
|
headers: { authorization: `Bearer ${credential.access_token}`, accept: 'application/json', 'content-type': 'application/json' },
|
|
29
|
-
|
|
49
|
+
// corp_id selects a DingTalk organization upstream; ODS identity is checked locally after exchange.
|
|
50
|
+
body: JSON.stringify({ gateway_url: credential.base_url, ...(options.corpId && !ODS_CORP_ID.test(options.corpId) ? { corp_id: options.corpId } : {}) }),
|
|
30
51
|
});
|
|
31
|
-
if (!response.ok) {
|
|
32
|
-
await response.body?.cancel();
|
|
33
|
-
throw failure('HTTP_ERROR', { stage: 'yida_exchange', status_code: response.status });
|
|
34
|
-
}
|
|
35
52
|
const reader = response.body?.getReader();
|
|
36
|
-
if (!reader) { throw failure('RESPONSE_INVALID'); }
|
|
53
|
+
if (!reader) { throw failure(response.ok ? 'RESPONSE_INVALID' : 'HTTP_ERROR', diagnostics(null, response.status)); }
|
|
37
54
|
const chunks = []; let bytes = 0;
|
|
38
55
|
try {
|
|
39
56
|
for (let part = await reader.read(); !part.done; part = await reader.read()) {
|
|
40
57
|
bytes += part.value.byteLength;
|
|
41
|
-
if (bytes > 65536) { await reader.cancel(); throw failure('RESPONSE_INVALID'); }
|
|
58
|
+
if (bytes > 65536) { await reader.cancel(); throw failure(response.ok ? 'RESPONSE_INVALID' : 'HTTP_ERROR', { ...diagnostics(null, response.status), reason: 'response_too_large' }); }
|
|
42
59
|
chunks.push(part.value);
|
|
43
60
|
}
|
|
44
61
|
} finally { reader.releaseLock(); }
|
|
45
|
-
|
|
62
|
+
let value;
|
|
63
|
+
try { value = JSON.parse(Buffer.concat(chunks).toString('utf8')); }
|
|
64
|
+
catch { throw failure(response.ok ? 'RESPONSE_INVALID' : 'HTTP_ERROR', { ...diagnostics(null, response.status), reason: 'invalid_json' }); }
|
|
65
|
+
const details = diagnostics(value, response.status, credential);
|
|
66
|
+
if (!response.ok) { throw failure('HTTP_ERROR', details); }
|
|
67
|
+
return { value, details };
|
|
46
68
|
} catch (error) {
|
|
47
69
|
// 上游正文和异常可能回显凭据,仅保留本模块生成的固定错误码。
|
|
48
70
|
if (error instanceof CliError && /^QWENWORK_CLOUD_/.test(error.code)) { throw error; }
|
|
49
|
-
throw failure(controller.signal.aborted ? 'REQUEST_TIMED_OUT' : 'REQUEST_FAILED');
|
|
71
|
+
throw failure(controller.signal.aborted ? 'REQUEST_TIMED_OUT' : 'REQUEST_FAILED', { stage: 'yida_exchange' });
|
|
50
72
|
} finally { clearTimeout(timer); }
|
|
51
73
|
}
|
|
52
74
|
|
|
@@ -67,24 +89,38 @@ async function exchangeQwenWorkCloudSession(options = {}) {
|
|
|
67
89
|
const credential = sandboxCredential(options);
|
|
68
90
|
try {
|
|
69
91
|
const startedAt = Date.now();
|
|
70
|
-
const value = await exchangeSession(credential, baseUrl, options);
|
|
71
|
-
if (!value || value
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
92
|
+
const { value, details } = await exchangeSession(credential, baseUrl, options);
|
|
93
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) { throw failure('RESPONSE_INVALID', { ...details, reason: 'response_shape' }); }
|
|
94
|
+
if (value.status !== 'ok') { throw failure('EXCHANGE_REJECTED', details); }
|
|
95
|
+
const env = options.env || process.env;
|
|
96
|
+
const hostTokens = ['QWENWORK_AGENT_TOKEN', 'MULE_AGENT_TOKEN', 'AGENT_TOKEN'].map(name => env[name]).filter(Boolean);
|
|
97
|
+
const validToken = token => typeof token === 'string' && !!token && !/\s/.test(token) && token.length <= 32768 && !hostTokens.includes(token);
|
|
98
|
+
// Older cloud deployments may return access-only sessions. Preserve a refresh token when supplied.
|
|
99
|
+
const refreshToken = value.refresh_token === undefined ? value.refreshToken : value.refresh_token;
|
|
100
|
+
const invalid = [
|
|
101
|
+
[value.auth_mode !== 'token', 'auth_mode'],
|
|
102
|
+
[value.can_auto_use !== true, 'can_auto_use'],
|
|
103
|
+
[value.token_type !== 'Bearer', 'token_type'],
|
|
104
|
+
[value.credential_source !== 'qwenwork_cloud', 'credential_source'],
|
|
105
|
+
[!validToken(value.access_token), 'access_token'],
|
|
106
|
+
[!Number.isInteger(value.expires_in) || value.expires_in <= 0 || value.expires_in > 1800, 'expires_in'],
|
|
107
|
+
[typeof value.corp_id !== 'string' || !CORP_ID.test(value.corp_id), 'corp_id'],
|
|
108
|
+
[typeof value.user_id !== 'string' || !value.user_id || value.user_id.trim() !== value.user_id || value.user_id.length > 256, 'user_id'],
|
|
109
|
+
[options.corpId && value.corp_id !== options.corpId, 'corp_id_mismatch'],
|
|
110
|
+
[options.userId && value.user_id !== options.userId, 'user_id_mismatch'],
|
|
111
|
+
['exchange_ticket' in value, 'exchange_ticket'],
|
|
112
|
+
[refreshToken !== undefined && !validToken(refreshToken), 'refresh_token'],
|
|
113
|
+
[value.client_id !== undefined && value.client_id !== (options.clientId || DINGTALK_OAUTH_CLIENT_ID), 'client_id'],
|
|
114
|
+
].find(([rejected]) => rejected);
|
|
115
|
+
if (invalid) { throw failure('RESPONSE_INVALID', { ...details, reason: invalid[1] }); }
|
|
82
116
|
return {
|
|
117
|
+
...sessionIdentity(value),
|
|
83
118
|
auth_mode: 'token', credential_source: 'qwenwork_cloud', token_type: 'Bearer',
|
|
84
119
|
// 可信宜搭签发服务决定业务域名;认证请求仍只发往最初选择的 origin。
|
|
85
|
-
access_token: value.access_token, base_url: value.base_url === undefined ? baseUrl : origin(value.base_url),
|
|
120
|
+
access_token: value.access_token, refresh_token: refreshToken, base_url: value.base_url === undefined ? baseUrl : origin(value.base_url, { ...details, reason: 'base_url' }),
|
|
86
121
|
client_id: options.clientId || DINGTALK_OAUTH_CLIENT_ID,
|
|
87
122
|
corp_id: value.corp_id, user_id: value.user_id,
|
|
123
|
+
user_name: typeof value.user_name === 'string' ? value.user_name : undefined,
|
|
88
124
|
expires_at: startedAt + value.expires_in * 1000,
|
|
89
125
|
};
|
|
90
126
|
} finally { credential.access_token = undefined; }
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { sessionIdentity } = require('./session-identity');
|
|
4
|
+
|
|
3
5
|
const { CliError } = require('../core/cli-error');
|
|
4
6
|
const { buildQwenWorkLoginInvocation } = require('../core/qwenwork-login-guidance');
|
|
5
7
|
const { t } = require('../core/i18n');
|
|
@@ -114,6 +116,7 @@ async function loginQwenWork(options = {}) {
|
|
|
114
116
|
if (verified.status !== 'ok' || verified.auth_mode !== 'token' || verified.can_auto_use !== true ||
|
|
115
117
|
verified.corp_id !== value.corp_id || verified.user_id !== value.user_id || expiresAt <= Date.now()) { throw fail('IDENTITY_MISMATCH'); }
|
|
116
118
|
const session = saveTokenSession({
|
|
119
|
+
...sessionIdentity(value),
|
|
117
120
|
auth_mode: 'token', token_type: 'Bearer', access_token: value.access_token, refresh_token: value.refresh_token,
|
|
118
121
|
expires_at: expiresAt, base_url: businessBaseUrl, client_id: DINGTALK_OAUTH_CLIENT_ID,
|
|
119
122
|
corp_id: value.corp_id, user_id: value.user_id, credential_source: 'qwenwork_desktop',
|
|
@@ -122,8 +125,8 @@ async function loginQwenWork(options = {}) {
|
|
|
122
125
|
if (!['user', 'project'].includes(session.persistence_scope) || !restored ||
|
|
123
126
|
restored.access_token !== value.access_token || restored.refresh_token !== value.refresh_token || restored.base_url !== businessBaseUrl ||
|
|
124
127
|
restored.corp_id !== value.corp_id || restored.user_id !== value.user_id) { throw fail('PERSISTENCE_FAILED'); }
|
|
125
|
-
//
|
|
126
|
-
return { ok: true, status: 'ok', can_auto_use: true };
|
|
128
|
+
// 只返回分类和登录结果;组织/用户标识、环境和存储细节不进入模型可见结果。
|
|
129
|
+
return { ok: true, status: 'ok', can_auto_use: true, ...sessionIdentity(session) };
|
|
127
130
|
}
|
|
128
131
|
|
|
129
132
|
async function run(args, options = {}) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
const { t } = require('../core/i18n');
|
|
3
|
+
const { sessionIdentity } = require('./session-identity');
|
|
3
4
|
const { CliError } = require('../core/cli-error');
|
|
4
5
|
const { loadTokenSession, resolveTokenSession, isEnvTokenAuthMode, hasEnvTokenCredential, saveTokenSession } = require('./token-store');
|
|
5
6
|
const { detectRuntimeCapabilities } = require('../core/utils');
|
|
@@ -10,7 +11,7 @@ const { tokenLogin, tokenRefresh, isRefreshAuthRequired } = require('./token-aut
|
|
|
10
11
|
function failure(code) {
|
|
11
12
|
return new CliError(t('qwenwork.login_check_failed'), { code });
|
|
12
13
|
}
|
|
13
|
-
function summary(ok) { return { ok, status: ok ? 'ok' : 'not_logged_in', can_auto_use: ok }; }
|
|
14
|
+
function summary(ok, session = {}) { return { ok, status: ok ? 'ok' : 'not_logged_in', can_auto_use: ok, ...(ok ? sessionIdentity(session) : {}) }; }
|
|
14
15
|
|
|
15
16
|
/** 以服务端状态为准;网络异常不能触发浏览器登录。 */
|
|
16
17
|
async function verify(session, options = {}) {
|
|
@@ -53,7 +54,10 @@ async function ensureQwenWorkLogin(options = {}, dependencies = {}) {
|
|
|
53
54
|
const env = options.env || process.env;
|
|
54
55
|
const runtime = options.runtime || detectRuntimeCapabilities({ env });
|
|
55
56
|
if (isQwenWorkWebRuntime(runtime) && !isEnvTokenAuthMode(env) && !hasEnvTokenCredential(env)) {
|
|
56
|
-
if (options.checkOnly) {
|
|
57
|
+
if (options.checkOnly) {
|
|
58
|
+
const session = loadTokenSession(options);
|
|
59
|
+
return summary(await verify(session, options), session || {});
|
|
60
|
+
}
|
|
57
61
|
const selectedProfile = options.authProfile || options.profile || env.OPENYIDA_AUTH_PROFILE;
|
|
58
62
|
const selectedSession = selectedProfile ? loadTokenSession(options) : null;
|
|
59
63
|
if (selectedProfile && !selectedSession) { throw failure('AUTH_PROFILE_REQUIRED'); }
|
|
@@ -66,7 +70,7 @@ async function ensureQwenWorkLogin(options = {}, dependencies = {}) {
|
|
|
66
70
|
const session = await exchangeQwenWorkCloudSession(cloudOptions);
|
|
67
71
|
if (!await verify(session, options)) { throw failure('QWENWORK_LOGIN_CHECK_FAILED'); }
|
|
68
72
|
saveTokenSession(session, options);
|
|
69
|
-
return summary(true);
|
|
73
|
+
return summary(true, session);
|
|
70
74
|
}
|
|
71
75
|
const resolution = resolveTokenSession(options);
|
|
72
76
|
if (resolution.status === 'profile_required') { throw failure('AUTH_PROFILE_REQUIRED'); }
|
|
@@ -74,12 +78,12 @@ async function ensureQwenWorkLogin(options = {}, dependencies = {}) {
|
|
|
74
78
|
if (session && options.endpoint && new URL(session.base_url).origin !== new URL(options.endpoint).origin) {
|
|
75
79
|
session = null;
|
|
76
80
|
}
|
|
77
|
-
if (await verify(session, options)) { return summary(true); }
|
|
81
|
+
if (await verify(session, options)) { return summary(true, session); }
|
|
78
82
|
if (session?.refresh_token) {
|
|
79
83
|
try {
|
|
80
84
|
const refreshed = await (dependencies.refresh || tokenRefresh)(options);
|
|
81
85
|
if (refreshed && refreshed.ok !== false && refreshed.access_token) {
|
|
82
|
-
if (await verify(refreshed, options)) { return summary(true); }
|
|
86
|
+
if (await verify(refreshed, options)) { return summary(true, refreshed); }
|
|
83
87
|
} else if (!isRefreshAuthRequired(refreshed)) { throw failure('QWENWORK_LOGIN_REFRESH_FAILED'); }
|
|
84
88
|
} catch (error) {
|
|
85
89
|
if (!isRefreshAuthRequired(error) && ![401, 403].includes(error.statusCode)) {
|
|
@@ -94,7 +98,7 @@ async function ensureQwenWorkLogin(options = {}, dependencies = {}) {
|
|
|
94
98
|
if (!result || result.ok === false || !result.access_token || !await verify(result, options)) {
|
|
95
99
|
throw failure('QWENWORK_LOGIN_FAILED');
|
|
96
100
|
}
|
|
97
|
-
return summary(true);
|
|
101
|
+
return summary(true, result);
|
|
98
102
|
}
|
|
99
103
|
|
|
100
104
|
async function run(args, options = {}) {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { CliError } = require('../core/cli-error');
|
|
4
|
+
const { t } = require('../core/i18n');
|
|
5
|
+
|
|
6
|
+
/** Explicit server classification wins; historical sessions without either field default to DingTalk organizations. */
|
|
7
|
+
function sessionIdentity(value = {}) {
|
|
8
|
+
const empty = field => field === undefined || field === null || field === '';
|
|
9
|
+
const identity = empty(value.identity_type) ? undefined : value.identity_type;
|
|
10
|
+
const organization = empty(value.organization_type) ? undefined : value.organization_type;
|
|
11
|
+
if (identity === undefined && organization === undefined) {
|
|
12
|
+
return { identity_type: 'ORGANIZATION', organization_type: 'DINGTALK' };
|
|
13
|
+
}
|
|
14
|
+
if ((identity === 'PERSONAL' && organization === undefined) ||
|
|
15
|
+
(identity === 'ORGANIZATION' && ['DINGTALK', 'QWENWORK', 'OTHER'].includes(organization))) {
|
|
16
|
+
return { identity_type: identity, ...(organization === undefined ? {} : { organization_type: organization }) };
|
|
17
|
+
}
|
|
18
|
+
throw new CliError(t('qwenwork.login_check_failed'), {
|
|
19
|
+
code: 'AUTH_IDENTITY_TYPE_INVALID', details: { reason: 'identity_classification' },
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = { sessionIdentity };
|
package/lib/auth/token-auth.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { sessionIdentity } = require('./session-identity');
|
|
4
|
+
|
|
3
5
|
const http = require('http');
|
|
4
6
|
const https = require('https');
|
|
5
7
|
|
|
@@ -174,6 +176,7 @@ function normalizeTokenResponse(response, baseUrl, clientId) {
|
|
|
174
176
|
baseUrl
|
|
175
177
|
);
|
|
176
178
|
return {
|
|
179
|
+
...sessionIdentity(data),
|
|
177
180
|
auth_mode: 'token',
|
|
178
181
|
credential_source: data.credential_source,
|
|
179
182
|
status: data.status || 'ok',
|
|
@@ -445,6 +448,7 @@ function tokenStatus(options = {}) {
|
|
|
445
448
|
refresh_token: maskToken(session.refresh_token),
|
|
446
449
|
expires_at: session.expires_at ? new Date(session.expires_at).toISOString() : undefined,
|
|
447
450
|
base_url: session.base_url,
|
|
451
|
+
...sessionIdentity(session),
|
|
448
452
|
corp_id: session.corp_id,
|
|
449
453
|
corp_name: session.corp_name,
|
|
450
454
|
user_id: session.user_id,
|
|
@@ -510,11 +514,16 @@ async function tokenRefreshUnlocked(options, session) {
|
|
|
510
514
|
} catch { throw createTokenAuthRequiredError('refresh_origin_mismatch'); }
|
|
511
515
|
}
|
|
512
516
|
const businessBaseUrl = session.base_url || authBaseUrlValue;
|
|
517
|
+
const corpId = data.corp_id || data.corpId || session.corp_id;
|
|
518
|
+
const userId = data.user_id || data.userId || session.user_id;
|
|
519
|
+
const inheritClassification = [data.identity_type, data.organization_type].every(value => value === undefined || value === null || value === '')
|
|
520
|
+
&& corpId === session.corp_id && userId === session.user_id;
|
|
513
521
|
const normalized = normalizeTokenResponse({
|
|
514
522
|
...data,
|
|
515
|
-
|
|
523
|
+
...(inheritClassification ? sessionIdentity(session) : {}),
|
|
524
|
+
corp_id: corpId,
|
|
516
525
|
corp_name: data.corp_name || data.corpName || data.name || session.corp_name,
|
|
517
|
-
user_id:
|
|
526
|
+
user_id: userId,
|
|
518
527
|
user_name: data.user_name || data.userName || session.user_name,
|
|
519
528
|
refresh_token: data.refresh_token || data.refreshToken || session.refresh_token,
|
|
520
529
|
credential_source: data.credential_source || session.credential_source,
|
package/lib/auth/token-store.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { sessionIdentity } = require('./session-identity');
|
|
4
|
+
|
|
3
5
|
const fs = require('fs');
|
|
4
6
|
const path = require('path');
|
|
5
7
|
const os = require('os');
|
|
@@ -275,6 +277,7 @@ function normalizeTokenSession(session = {}) {
|
|
|
275
277
|
session.tokenEndpoint
|
|
276
278
|
);
|
|
277
279
|
const normalized = {
|
|
280
|
+
...sessionIdentity(session),
|
|
278
281
|
auth_mode: 'token',
|
|
279
282
|
credential_source: session.credential_source,
|
|
280
283
|
token_type: session.token_type || session.tokenType || 'Bearer',
|
package/lib/core/locales/en.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
module.exports = {
|
|
7
7
|
qwenwork: {
|
|
8
|
+
organization_switch_success: 'Switched organization through QwenWork login.',
|
|
8
9
|
login_check_failed: 'Unable to verify the login session. Please retry.',
|
|
9
10
|
desktop_login_failed: 'QwenWork desktop login failed. Check the desktop identity and login handoff before retrying.',
|
|
10
11
|
desktop_login_usage: 'Usage: openyida login --qwenwork [--json] [--check-only]',
|
package/lib/core/locales/zh.js
CHANGED
package/lib/core/utils.js
CHANGED
|
@@ -829,11 +829,13 @@ function resolveTokenBaseUrl(root, fallbackBaseUrl, session) {
|
|
|
829
829
|
function loadTokenAuthData(projectRoot, defaultBaseUrl) {
|
|
830
830
|
try {
|
|
831
831
|
const { loadTokenSession } = require('../auth/token-store');
|
|
832
|
+
const { sessionIdentity } = require('../auth/session-identity');
|
|
832
833
|
const session = loadTokenSession({ projectRoot });
|
|
833
834
|
if (!session || (!session.access_token && !session.refresh_token)) {
|
|
834
835
|
return null;
|
|
835
836
|
}
|
|
836
837
|
return {
|
|
838
|
+
...sessionIdentity(session),
|
|
837
839
|
corp_id: session.corp_id,
|
|
838
840
|
corp_name: session.corp_name,
|
|
839
841
|
user_id: session.user_id,
|
|
@@ -1884,7 +1886,9 @@ function normalizeRefreshedTokenAuthData(refreshResult, fallbackBaseUrl) {
|
|
|
1884
1886
|
if (!refreshResult || !refreshResult.access_token) {
|
|
1885
1887
|
return null;
|
|
1886
1888
|
}
|
|
1889
|
+
const { sessionIdentity } = require('../auth/session-identity');
|
|
1887
1890
|
return {
|
|
1891
|
+
...sessionIdentity(refreshResult),
|
|
1888
1892
|
corp_id: refreshResult.corp_id,
|
|
1889
1893
|
corp_name: refreshResult.corp_name,
|
|
1890
1894
|
user_id: refreshResult.user_id,
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openyida",
|
|
3
|
-
"version": "2026.10.29-beta.
|
|
3
|
+
"version": "2026.10.29-beta.3",
|
|
4
4
|
"description": "OpenYida CLI - 宜搭低代码 AI 开发工具(安装即用,零配置)",
|
|
5
5
|
"bin": {
|
|
6
|
-
"openyida": "bin/yida
|
|
7
|
-
"yida": "bin/yida
|
|
6
|
+
"openyida": "bin/yida.js",
|
|
7
|
+
"yida": "bin/yida.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"bin/",
|
|
@@ -15,8 +15,7 @@
|
|
|
15
15
|
"!lib/samples/**/assets/**",
|
|
16
16
|
"scripts/postinstall.js",
|
|
17
17
|
"README.md",
|
|
18
|
-
"LICENSE"
|
|
19
|
-
"scripts/install-qwenwork-pre-beta.js"
|
|
18
|
+
"LICENSE"
|
|
20
19
|
],
|
|
21
20
|
"scripts": {
|
|
22
21
|
"test": "jest",
|
|
@@ -64,7 +63,7 @@
|
|
|
64
63
|
"check:package": "npm pack --dry-run",
|
|
65
64
|
"check:release": "npm run check:ci",
|
|
66
65
|
"check:ci": "bash scripts/validate-ci.sh",
|
|
67
|
-
"postinstall": "node scripts/
|
|
66
|
+
"postinstall": "node scripts/postinstall.js"
|
|
68
67
|
},
|
|
69
68
|
"keywords": [
|
|
70
69
|
"yida",
|
|
@@ -120,16 +119,5 @@
|
|
|
120
119
|
"@openyida/local-agent-runtime-darwin-x64": "0.1.4",
|
|
121
120
|
"@openyida/local-agent-runtime-win32-x64": "0.1.4",
|
|
122
121
|
"@openyida/local-agent-runtime-win32-arm64": "0.1.4"
|
|
123
|
-
},
|
|
124
|
-
"openyidaLocalBeta": {
|
|
125
|
-
"desktopTicketLogin": true
|
|
126
|
-
},
|
|
127
|
-
"openyidaBuild": {
|
|
128
|
-
"sourceCommit": "32eae192ec36e6764878e7123d2902170924911d",
|
|
129
|
-
"sourceBranch": "feature/qwenwork-cloud-auth",
|
|
130
|
-
"mergedMainCommit": "ab4ccf97e03a7469df7eee731b300c59289b8231",
|
|
131
|
-
"distTag": "beta",
|
|
132
|
-
"presetScope": "none",
|
|
133
|
-
"environmentRouting": "standard"
|
|
134
122
|
}
|
|
135
123
|
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
const fs = require('node:fs');
|
|
3
|
-
const os = require('node:os');
|
|
4
|
-
const path = require('node:path');
|
|
5
|
-
|
|
6
|
-
/** 技能复制属于可选安装步骤,权限不足时保留旧目录,CLI 仍可使用随包技能。 */
|
|
7
|
-
function install(options = {}) {
|
|
8
|
-
const io = options.fs || fs;
|
|
9
|
-
const home = options.home || os.homedir();
|
|
10
|
-
const root = path.join(home, '.qwenworkcn');
|
|
11
|
-
if (!options.target && !io.existsSync(root)) { return { status: 'skipped' }; }
|
|
12
|
-
const source = options.source || path.resolve(__dirname, '../yida-skills');
|
|
13
|
-
const target = options.target || path.join(root, 'skills', 'yida-skills');
|
|
14
|
-
const unique = `${Date.now()}-${process.pid}`;
|
|
15
|
-
const backup = path.join(options.backupRoot || path.join(options.target ? path.dirname(target) : root, '.openyida-beta-backups'), unique);
|
|
16
|
-
const stage = path.join(path.dirname(target), `.openyida-installing-${unique}`);
|
|
17
|
-
let moved = false;
|
|
18
|
-
try {
|
|
19
|
-
io.mkdirSync(path.dirname(target), { recursive: true });
|
|
20
|
-
io.cpSync(source, stage, { recursive: true });
|
|
21
|
-
if (io.existsSync(target)) {
|
|
22
|
-
io.mkdirSync(path.dirname(backup), { recursive: true });
|
|
23
|
-
io.renameSync(target, backup);
|
|
24
|
-
moved = true;
|
|
25
|
-
}
|
|
26
|
-
io.renameSync(stage, target);
|
|
27
|
-
return { status: 'updated' };
|
|
28
|
-
} catch {
|
|
29
|
-
// 可选复制失败不使 npm 回滚;旧技能已移动时先恢复,备份始终保留在扫描目录外。
|
|
30
|
-
try { if (moved && !io.existsSync(target)) { io.renameSync(backup, target); } } catch { /* 保留备份,避免覆盖现有内容。 */ }
|
|
31
|
-
try { io.rmSync(stage, { recursive: true, force: true }); } catch { /* 权限不足时不扩大清理范围。 */ }
|
|
32
|
-
return { status: 'skipped' };
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
if (require.main === module) {
|
|
37
|
-
const index = process.argv.indexOf('--target-dir');
|
|
38
|
-
install(index >= 0 && process.argv[index + 1] ? { target: path.resolve(process.argv[index + 1]) } : {});
|
|
39
|
-
}
|
|
40
|
-
module.exports = { install };
|