openyida 2026.10.29-beta.2 → 2026.10.29-beta.4

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',
@@ -52,7 +52,7 @@ async function run(args) {
52
52
  console.log(`\n连接器: ${connector.displayName}`);
53
53
  console.log('\n⚠️ CLI 未执行删除操作:当前无法确定性证明该连接器未被表单、页面、流程或集成自动化引用。');
54
54
  console.log('请先确认并解除全部依赖,再前往宜搭平台管理后台手工删除:');
55
- console.log(' https://www.aliwork.com/platformManage/customConnectorFactory');
55
+ console.log(' https://davidtest.aliwork.com/platformManage/customConnectorFactory');
56
56
  }
57
57
 
58
58
  module.exports = { run };
@@ -52,7 +52,7 @@ async function run(args) {
52
52
  }
53
53
  console.log('📭 暂无鉴权账号');
54
54
  console.log('\n💡 提示: 需要在宜搭平台管理后台创建鉴权账号');
55
- console.log(' https://www.aliwork.com/platformManage/customConnectorFactory');
55
+ console.log(' https://davidtest.aliwork.com/platformManage/customConnectorFactory');
56
56
  return result;
57
57
  }
58
58
 
@@ -695,8 +695,8 @@ class FixEngine {
695
695
  case 'create-config': {
696
696
  const configPath = path.join(this.projectRoot, 'config.json');
697
697
  const template = {
698
- loginUrl: 'https://www.aliwork.com/workPlatform',
699
- defaultBaseUrl: 'https://www.aliwork.com',
698
+ loginUrl: 'https://davidtest.aliwork.com/workPlatform',
699
+ defaultBaseUrl: 'https://davidtest.aliwork.com',
700
700
  };
701
701
  fs.writeFileSync(configPath, JSON.stringify(template, null, 2), 'utf-8');
702
702
  return {
@@ -9,7 +9,7 @@
9
9
  * 2. authData.base_url(登录后实际跳转域名)
10
10
  * 3. 环境变量 OPENYIDA_ENV 指定的环境配置
11
11
  * 4. 当前激活的环境配置(openyida-envs.json current 字段)
12
- * 5. 默认公有云 https://www.aliwork.com
12
+ * 5. 默认公有云 https://davidtest.aliwork.com
13
13
  *
14
14
  * 导出函数:
15
15
  * loadEnvsConfig() - 读取环境配置文件(不存在则返回默认公有云配置)
@@ -25,8 +25,8 @@ const fs = require('fs');
25
25
  const path = require('path');
26
26
  const { findProjectRoot } = require('./utils');
27
27
 
28
- const DEFAULT_BASE_URL = 'https://www.aliwork.com';
29
- const DEFAULT_LOGIN_URL = 'https://www.aliwork.com/workPlatform';
28
+ const DEFAULT_BASE_URL = 'https://davidtest.aliwork.com';
29
+ const DEFAULT_LOGIN_URL = 'https://davidtest.aliwork.com/workPlatform';
30
30
  const INTERNATIONAL_BASE_URL = 'https://www.yidaapps.com';
31
31
  const DINGTALK_OAUTH_CLIENT_ID = 'suite9xvlxxerybljwheo';
32
32
  const DINGTALK_LOGIN_ORIGIN = 'https://login.dingtalk.com';
@@ -151,7 +151,7 @@ const ENV_ALIASES = {
151
151
  };
152
152
 
153
153
  const KNOWN_YIDA_HOSTS = new Set([
154
- 'www.aliwork.com',
154
+ 'davidtest.aliwork.com',
155
155
  'www.yidaapps.com',
156
156
  'yida-group.alibaba-inc.com',
157
157
  'www.yidaapps.com',
@@ -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
@@ -412,7 +412,7 @@ function isTokenAuthMode(_env = process.env) {
412
412
  }
413
413
 
414
414
  function normalizeEnvBaseUrl(baseUrl, fallbackBaseUrl) {
415
- const raw = String(baseUrl || fallbackBaseUrl || 'https://www.aliwork.com').trim();
415
+ const raw = String(baseUrl || fallbackBaseUrl || 'https://davidtest.aliwork.com').trim();
416
416
  try {
417
417
  const parsed = new URL(raw);
418
418
  if (!['http:', 'https:'].includes(parsed.protocol)) {
@@ -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,
@@ -916,7 +918,7 @@ function loadAuthData(projectRoot, defaultBaseUrl) {
916
918
  };
917
919
  }
918
920
  const root = projectRoot || findProjectRoot();
919
- const fallbackBaseUrl = defaultBaseUrl || 'https://www.aliwork.com';
921
+ const fallbackBaseUrl = defaultBaseUrl || 'https://davidtest.aliwork.com';
920
922
  return loadTokenAuthData(root, fallbackBaseUrl);
921
923
  }
922
924
 
@@ -937,7 +939,7 @@ function triggerLogin(options = {}) {
937
939
  if (isEnvAuthMode()) {
938
940
  const status = getAuthStatus({ projectRoot: options.projectRoot || findProjectRoot() });
939
941
  if (status && status.ok) {
940
- return loadTokenAuthData(options.projectRoot || findProjectRoot(), status.base_url || 'https://www.aliwork.com');
942
+ return loadTokenAuthData(options.projectRoot || findProjectRoot(), status.base_url || 'https://davidtest.aliwork.com');
941
943
  }
942
944
  const reason = status && status.failure_reason
943
945
  ? status.failure_reason
@@ -956,7 +958,7 @@ function triggerLogin(options = {}) {
956
958
  );
957
959
  }
958
960
 
959
- const tokenAuthData = loadTokenAuthData(options.projectRoot || findProjectRoot(), 'https://www.aliwork.com');
961
+ const tokenAuthData = loadTokenAuthData(options.projectRoot || findProjectRoot(), 'https://davidtest.aliwork.com');
960
962
  if (tokenAuthData) {
961
963
  return tokenAuthData;
962
964
  }
@@ -1072,7 +1074,7 @@ function resolveBaseUrl(authData, defaultBaseUrl) {
1072
1074
  return businessBaseUrl;
1073
1075
  }
1074
1076
  const resolved = resolveEndpoint(null, undefined);
1075
- if (defaultBaseUrl && resolved === 'https://www.aliwork.com' && (!authData || !authData.base_url)) {
1077
+ if (defaultBaseUrl && resolved === 'https://davidtest.aliwork.com' && (!authData || !authData.base_url)) {
1076
1078
  return defaultBaseUrl.replace(/\/+$/, '');
1077
1079
  }
1078
1080
  return resolved;
@@ -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,
@@ -3,7 +3,7 @@
3
3
  /**
4
4
  * corp-efficiency.js - 平台管理 / 企业效能
5
5
  *
6
- * 对应页面:https://www.aliwork.com/platformManage/corpEfficiency
6
+ * 对应页面:https://davidtest.aliwork.com/platformManage/corpEfficiency
7
7
  *
8
8
  * 用法:
9
9
  * openyida corp-efficiency [overview] [--locale zh_CN|en_US] [--raw]
@@ -28,7 +28,7 @@ const API = {
28
28
 
29
29
  const NOTIFY_TYPES = new Set(['noticeStudy', 'noticeCertify', 'completeStudy']);
30
30
  const DETAIL_DOC_URL = 'https://docs.aliwork.com/docs/yida_support/ifpp7g/wow0da';
31
- const DETAIL_CONSULT_URL = 'https://www.aliwork.com/hybrid_home';
31
+ const DETAIL_CONSULT_URL = 'https://davidtest.aliwork.com/hybrid_home';
32
32
  const API_MESSAGES = {
33
33
  loginMessage: '登录态已失效,请重新登录',
34
34
  };
@@ -143,7 +143,7 @@ function parseDetailReportInfo(detailReportUrl, baseUrl) {
143
143
  }
144
144
 
145
145
  try {
146
- const url = new URL(detailReportUrl, baseUrl || 'https://www.aliwork.com');
146
+ const url = new URL(detailReportUrl, baseUrl || 'https://davidtest.aliwork.com');
147
147
  const pathParts = url.pathname.split('/').filter(Boolean);
148
148
  const previewIndex = pathParts.indexOf('preview');
149
149
  const appType = previewIndex > 0 ? pathParts[previewIndex - 1] : '';
@@ -530,7 +530,7 @@ function inferBaseUrl(formResult, publicPath) {
530
530
  // ignore
531
531
  }
532
532
  }
533
- return publicPath && publicPath.startsWith('http') ? new URL(publicPath).origin : 'https://www.aliwork.com';
533
+ return publicPath && publicPath.startsWith('http') ? new URL(publicPath).origin : 'https://davidtest.aliwork.com';
534
534
  }
535
535
 
536
536
  function writeJson(filePath, value) {
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.4",
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",
@@ -113,23 +112,12 @@
113
112
  "node": ">=18"
114
113
  },
115
114
  "openyidaAgent": {
116
- "endpoint": "https://www.aliwork.com/openyida/agent-api"
115
+ "endpoint": "https://davidtest.aliwork.com/openyida/agent-api"
117
116
  },
118
117
  "optionalDependencies": {
119
118
  "@openyida/local-agent-runtime-darwin-arm64": "0.1.4",
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,4 +1,4 @@
1
1
  {
2
- "loginUrl": "https://www.aliwork.com/workPlatform",
3
- "defaultBaseUrl": "https://www.aliwork.com"
2
+ "loginUrl": "https://davidtest.aliwork.com/workPlatform",
3
+ "defaultBaseUrl": "https://davidtest.aliwork.com"
4
4
  }
@@ -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 };