openyida 2026.10.26-beta.2 → 2026.10.27-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -143,7 +143,7 @@ openyida login --check-only --json
143
143
  openyida auth status
144
144
  ```
145
145
 
146
- In QwenWork web, `openyida login --qwenwork --json` (or ordinary `login`) exchanges the cloud runtime credential for a verified YiDA session on the selected YiDA endpoint. Login preparation stays silent; failures stop without browser OAuth. Desktop and other hosts retain their existing authentication paths.
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
148
  For QwenWork cloud identity diagnostics, run inside the actual cloud execution environment:
149
149
 
@@ -4,7 +4,6 @@ const { CliError } = require('../core/cli-error');
4
4
  const { t } = require('../core/i18n');
5
5
  const { DINGTALK_OAUTH_CLIENT_ID } = require('../core/env-manager');
6
6
  const { resolveTokenBaseUrl } = require('./token-auth');
7
- const { exchangeQwenWorkAgentToken, getQwenWorkUserInfo } = require('./qwenwork-client');
8
7
 
9
8
  function failure(code, details = {}) {
10
9
  return new CliError(t('qwenwork.login_check_failed'), { code: `QWENWORK_CLOUD_${code}`, details });
@@ -13,7 +12,7 @@ function failure(code, details = {}) {
13
12
  function origin(value) {
14
13
  try {
15
14
  const url = new URL(value);
16
- if (url.protocol !== 'https:' || url.username || url.password || url.pathname !== '/' || url.search || url.hash) { throw new Error(); }
15
+ 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(); }
17
16
  return url.origin;
18
17
  } catch { throw failure('ENDPOINT_INVALID'); }
19
18
  }
@@ -27,7 +26,7 @@ async function exchangeSession(credential, baseUrl, options) {
27
26
  const response = await (options.fetch || globalThis.fetch)(`${baseUrl}/openapi/cli/v1/auth/qwenwork/token`, {
28
27
  method: 'POST', redirect: 'error', signal: controller.signal,
29
28
  headers: { authorization: `Bearer ${credential.access_token}`, accept: 'application/json', 'content-type': 'application/json' },
30
- body: JSON.stringify(options.corpId ? { corp_id: options.corpId } : {}),
29
+ body: JSON.stringify({ gateway_url: credential.base_url, ...(options.corpId ? { corp_id: options.corpId } : {}) }),
31
30
  });
32
31
  if (!response.ok) {
33
32
  await response.body?.cancel();
@@ -51,13 +50,22 @@ async function exchangeSession(credential, baseUrl, options) {
51
50
  } finally { clearTimeout(timer); }
52
51
  }
53
52
 
54
- /** 仅在内存中兑换千问凭据;返回宜搭会话供调用方校验后保存。 */
53
+ /** 只读取本次沙箱凭据,由宜搭服务端向受信任网关验证身份。 */
54
+ function sandboxCredential(options) {
55
+ const env = options.env || process.env;
56
+ const token = ['QWENWORK_AGENT_TOKEN', 'MULE_AGENT_TOKEN', 'AGENT_TOKEN'].map(name => env[name]).find(value => typeof value === 'string' && value);
57
+ if (!token) { throw new CliError(t('qwenwork.login_check_failed'), { code: 'QWENWORK_CREDENTIAL_MISSING' }); }
58
+ if (token.length > 4096 || /\s/.test(token)) { throw failure('CREDENTIAL_INVALID'); }
59
+ const gateway = env.QWENWORK_GATEWAY_URL || env.MULE_GATEWAY_URL;
60
+ if (!gateway) { throw new CliError(t('qwenwork.login_check_failed'), { code: 'QWENWORK_GATEWAY_MISSING' }); }
61
+ return { access_token: token, base_url: origin(gateway) };
62
+ }
63
+
64
+ /** 只向宜搭提交 Agent Token 和网关地址;返回会话供调用方校验后保存。 */
55
65
  async function exchangeQwenWorkCloudSession(options = {}) {
56
66
  const baseUrl = origin(options.endpoint || (options.env || process.env).OPENYIDA_ENDPOINT || resolveTokenBaseUrl(options));
57
- const credential = await exchangeQwenWorkAgentToken(options);
67
+ const credential = sandboxCredential(options);
58
68
  try {
59
- const profile = await getQwenWorkUserInfo(credential, options);
60
- if (profile.is_biz !== true || !profile.biz?.org_id) { throw failure('ENTERPRISE_REQUIRED'); }
61
69
  const startedAt = Date.now();
62
70
  const value = await exchangeSession(credential, baseUrl, options);
63
71
  if (!value || value.status !== 'ok' || value.auth_mode !== 'token' || value.can_auto_use !== true ||
@@ -66,15 +74,15 @@ async function exchangeQwenWorkCloudSession(options = {}) {
66
74
  value.access_token === credential.access_token ||
67
75
  ['QWENWORK_AGENT_TOKEN', 'MULE_AGENT_TOKEN', 'AGENT_TOKEN'].some(name => value.access_token === (options.env || process.env)[name]) ||
68
76
  !Number.isInteger(value.expires_in) || value.expires_in <= 0 || value.expires_in > 1800 ||
69
- typeof value.corp_id !== 'string' || !/^ding[0-9a-f]{32}$/.test(value.corp_id) ||
77
+ typeof value.corp_id !== 'string' || !/^(?:ding[0-9a-f]{32}|dingd8e1123006514592)$/.test(value.corp_id) ||
70
78
  typeof value.user_id !== 'string' || !value.user_id || value.user_id.trim() !== value.user_id || value.user_id.length > 256 ||
71
79
  (options.corpId && value.corp_id !== options.corpId) ||
72
80
  (options.userId && value.user_id !== options.userId) ||
73
- (value.base_url !== undefined && origin(value.base_url) !== baseUrl) ||
74
81
  'refresh_token' in value || 'refreshToken' in value || 'exchange_ticket' in value) { throw failure('RESPONSE_INVALID'); }
75
82
  return {
76
83
  auth_mode: 'token', credential_source: 'qwenwork_cloud', token_type: 'Bearer',
77
- access_token: value.access_token, base_url: baseUrl,
84
+ // 可信宜搭签发服务决定业务域名;认证请求仍只发往最初选择的 origin。
85
+ access_token: value.access_token, base_url: value.base_url === undefined ? baseUrl : origin(value.base_url),
78
86
  client_id: options.clientId || DINGTALK_OAUTH_CLIENT_ID,
79
87
  corp_id: value.corp_id, user_id: value.user_id,
80
88
  expires_at: startedAt + value.expires_in * 1000,
@@ -37,7 +37,7 @@ function parseTicket(raw, context) {
37
37
  function origin(value) {
38
38
  try {
39
39
  const url = new URL(value);
40
- if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash || url.pathname !== '/') { throw new Error(); }
40
+ if (typeof value !== 'string' || value.length > 2048 || /\s/.test(value) || url.protocol !== 'https:' || url.username || url.password || url.search || url.hash || url.pathname !== '/') { throw new Error(); }
41
41
  return url.origin;
42
42
  } catch {
43
43
  throw fail('ENDPOINT_INVALID');
@@ -105,21 +105,22 @@ async function loginQwenWork(options = {}) {
105
105
  typeof value.refresh_token !== 'string' || !value.refresh_token || /\s/.test(value.refresh_token) || value.refresh_token.length > 32768 ||
106
106
  !Number.isInteger(value.refresh_token_expires_in) || value.refresh_token_expires_in < 1 ||
107
107
  (value.client_id && value.client_id !== DINGTALK_OAUTH_CLIENT_ID) ||
108
- value.corp_id !== ticket.corp_id || value.user_id !== ticket.user_id || value.exchange_id !== ticket.exchange_id ||
109
- (value.base_url && origin(value.base_url) !== baseUrl)) { throw fail('CREDENTIAL_INVALID'); }
110
- const verified = await requestJson(`${baseUrl}/openapi/cli/v1/auth/status`, {
108
+ value.corp_id !== ticket.corp_id || value.user_id !== ticket.user_id || value.exchange_id !== ticket.exchange_id) { throw fail('CREDENTIAL_INVALID'); }
109
+ // 票据仍在原签票 origin 消费,业务域名仅来自受信的消费响应。
110
+ const businessBaseUrl = value.base_url === undefined ? baseUrl : origin(value.base_url);
111
+ const verified = await requestJson(`${businessBaseUrl}/openapi/cli/v1/auth/status`, {
111
112
  method: 'GET', headers: { authorization: `Bearer ${value.access_token}`, accept: 'application/json' },
112
113
  }, options);
113
114
  if (verified.status !== 'ok' || verified.auth_mode !== 'token' || verified.can_auto_use !== true ||
114
115
  verified.corp_id !== value.corp_id || verified.user_id !== value.user_id || expiresAt <= Date.now()) { throw fail('IDENTITY_MISMATCH'); }
115
116
  const session = saveTokenSession({
116
117
  auth_mode: 'token', token_type: 'Bearer', access_token: value.access_token, refresh_token: value.refresh_token,
117
- expires_at: expiresAt, base_url: baseUrl, client_id: DINGTALK_OAUTH_CLIENT_ID,
118
+ expires_at: expiresAt, base_url: businessBaseUrl, client_id: DINGTALK_OAUTH_CLIENT_ID,
118
119
  corp_id: value.corp_id, user_id: value.user_id, credential_source: 'qwenwork_desktop',
119
120
  }, options);
120
121
  const restored = loadTokenSession(options);
121
122
  if (!['user', 'project'].includes(session.persistence_scope) || !restored ||
122
- restored.access_token !== value.access_token || restored.refresh_token !== value.refresh_token || restored.base_url !== baseUrl ||
123
+ restored.access_token !== value.access_token || restored.refresh_token !== value.refresh_token || restored.base_url !== businessBaseUrl ||
123
124
  restored.corp_id !== value.corp_id || restored.user_id !== value.user_id) { throw fail('PERSISTENCE_FAILED'); }
124
125
  // 身份、环境和存储细节仅用于内部校验,不进入模型可见的登录结果。
125
126
  return { ok: true, status: 'ok', can_auto_use: true };
@@ -502,8 +502,12 @@ async function tokenRefreshUnlocked(options, session) {
502
502
  requireOkResponse(response);
503
503
  const data = getTokenPayload(response);
504
504
  const responseBusinessBaseUrl = data.base_url || data.baseUrl;
505
- if (desktopSession && responseBusinessBaseUrl && normalizeBaseUrl(responseBusinessBaseUrl, null) !== session.base_url) {
506
- throw createTokenAuthRequiredError('refresh_origin_mismatch');
505
+ if (desktopSession && responseBusinessBaseUrl !== undefined) {
506
+ try {
507
+ const value = new URL(responseBusinessBaseUrl);
508
+ if (typeof responseBusinessBaseUrl !== 'string' || responseBusinessBaseUrl.length > 2048 || /\s/.test(responseBusinessBaseUrl) ||
509
+ value.protocol !== 'https:' || value.username || value.password || value.pathname !== '/' || value.search || value.hash) { throw new Error(); }
510
+ } catch { throw createTokenAuthRequiredError('refresh_origin_mismatch'); }
507
511
  }
508
512
  const businessBaseUrl = session.base_url || authBaseUrlValue;
509
513
  const normalized = normalizeTokenResponse({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openyida",
3
- "version": "2026.10.26-beta.2",
3
+ "version": "2026.10.27-beta.1",
4
4
  "description": "OpenYida CLI - 宜搭低代码 AI 开发工具(安装即用,零配置)",
5
5
  "bin": {
6
6
  "openyida": "bin/yida-pre-beta.js",
@@ -127,5 +127,10 @@
127
127
  "description": "宜搭 PRE 千问办公免登联调",
128
128
  "authDirectory": "qwenwork-pre-beta",
129
129
  "desktopTicketLogin": true
130
+ },
131
+ "openyidaBuild": {
132
+ "sourceCommit": "bcad3d168e1537751e498bd2a18bdc83b9f5e5a4",
133
+ "sourceBranch": "feature/qwenwork-cloud-auth",
134
+ "distTag": "beta"
130
135
  }
131
136
  }
@@ -2,7 +2,20 @@
2
2
 
3
3
  1. 以当前能力快照的 `runtime.tool=qwenwork`、`runtime.subtype=qwenwork_web`、`builder_path.interactive_login.mode=cloud_token` 确认环境。
4
4
  2. 已登录直接继续业务;未登录时静默执行快照推荐的命令,通常为 `openyida login --qwenwork --json`,并等待结束。普通 `openyida login` 在该网页态也会自动走同一路径。
5
- 3. CLI 自行消费沙箱提供的 Agent 凭据,兑换千问用户凭据并校验身份,再向当前宜搭环境换取登录态。不要让模型读取、复制、打印环境变量或认证文件。
5
+ 3. CLI 自行消费沙箱提供的 Agent Token 与网关地址,直接交给当前宜搭环境验证并换取登录态。不要让模型读取、复制、打印环境变量或认证文件。
6
6
  4. 成功后直接继续创建应用等用户任务,只展示业务结果和链接;不展示安装、登录、Token、用户、组织、环境或凭据路径。
7
7
  5. 缺少凭据、个人身份无企业组织、接口拒绝或网络失败时停止,只简短说明阻塞;不回退 OAuth,不启动浏览器或 Playwright,不要求用户粘贴 Token。
8
8
  6. 桌面态仍走桌面换票;其他宿主继续采用其快照中的认证规则;已注入宜搭 Token 的运行环境保持原规则。
9
+
10
+ ## 云端身份协议
11
+
12
+ 登录命令直接从宿主读取 Agent Token 与网关 URL,经 HTTPS 提交给宜搭 `/openapi/cli/v1/auth/qwenwork/token`。由宜搭服务端调用该受信任网关的固定 `/api/agent-gw/yida/user_info`,取得钉钉 UID 和组织关联后签发宜搭会话。CLI 不再调用旧的 `/api/agent-gw/token/exchange` 或 `/user/info`;保留的 `auth qwenwork user-info` 是独立诊断命令,不用于本次自动登录。
13
+
14
+ 网关来源由宜搭服务端配置校验;失败即停止,不改 URL 猜环境、不回退旧兑换。安装、预检、登录仍静默执行,成功后继续用户业务。
15
+
16
+
17
+ ## 宜搭业务域名
18
+
19
+ 认证入口和业务域名可以不同。成功响应含 base_url 时,由宜搭服务端按当前环境 Diamond hosts 和集团专属域名规则决定;CLI 校验纯 HTTPS origin,在该域名用宜搭 Token 验证同一组织与用户后才保存登录态。缺少该字段则沿用原入口,不由模型猜域名。Agent Token 仅交给最初选定的宜搭认证入口。
20
+
21
+ 客户端在原签票地址消费兑换票,再从受信的消费响应取得业务域名;后续刷新沿用保存的业务地址,并消费服务端返回的合法域名更新。普通用户流程仍只展示业务结果。