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 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 login = options.tokenLogin || tokenLogin;
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
- function origin(value) {
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
- body: JSON.stringify({ gateway_url: credential.base_url, ...(options.corpId ? { corp_id: options.corpId } : {}) }),
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
- return JSON.parse(Buffer.concat(chunks).toString('utf8'));
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.status !== 'ok' || value.auth_mode !== 'token' || value.can_auto_use !== true ||
72
- value.token_type !== 'Bearer' || value.credential_source !== 'qwenwork_cloud' ||
73
- typeof value.access_token !== 'string' || !value.access_token || /\s/.test(value.access_token) || value.access_token.length > 32768 ||
74
- value.access_token === credential.access_token ||
75
- ['QWENWORK_AGENT_TOKEN', 'MULE_AGENT_TOKEN', 'AGENT_TOKEN'].some(name => value.access_token === (options.env || process.env)[name]) ||
76
- !Number.isInteger(value.expires_in) || value.expires_in <= 0 || value.expires_in > 1800 ||
77
- typeof value.corp_id !== 'string' || !/^(?:ding[0-9a-f]{32}|dingd8e1123006514592)$/.test(value.corp_id) ||
78
- typeof value.user_id !== 'string' || !value.user_id || value.user_id.trim() !== value.user_id || value.user_id.length > 256 ||
79
- (options.corpId && value.corp_id !== options.corpId) ||
80
- (options.userId && value.user_id !== options.userId) ||
81
- 'exchange_ticket' in value) { throw failure('RESPONSE_INVALID'); }
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) { return summary(await verify(loadTokenSession(options), options)); }
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 };
@@ -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
- corp_id: data.corp_id || data.corpId || session.corp_id,
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: data.user_id || data.userId || session.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,
@@ -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',
@@ -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]',
@@ -5,6 +5,7 @@
5
5
 
6
6
  module.exports = {
7
7
  qwenwork: {
8
+ organization_switch_success: '已通过千问办公登录切换组织。',
8
9
  login_check_failed: '登录状态暂时无法确认,请重试。',
9
10
  desktop_login_failed: '千问办公桌面免登失败,请确认桌面登录身份和免登通道后重试。',
10
11
  desktop_login_usage: '用法:openyida login --qwenwork [--json] [--check-only]',
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.2",
3
+ "version": "2026.10.29-beta.3",
4
4
  "description": "OpenYida CLI - 宜搭低代码 AI 开发工具(安装即用,零配置)",
5
5
  "bin": {
6
- "openyida": "bin/yida-beta.js",
7
- "yida": "bin/yida-beta.js"
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/install-qwenwork-pre-beta.js"
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 };