openyida 2026.10.23-beta.2 → 2026.10.23-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.
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const { CliError } = require('../core/cli-error');
4
+ const { buildQwenWorkLoginInvocation } = require('../core/qwenwork-login-guidance');
4
5
  const { t } = require('../core/i18n');
5
6
  const { DINGTALK_OAUTH_CLIENT_ID } = require('../core/env-manager');
6
7
  const { saveTokenSession, loadTokenSession, isEnvTokenAuthMode } = require('./token-store');
@@ -12,7 +13,10 @@ const CONSUME_PATH = '/openapi/cli/v1/auth/qwenwork/desktop/consume';
12
13
  const MAX_RESPONSE_BYTES = 65536;
13
14
 
14
15
  function fail(code) {
15
- return new CliError(t('qwenwork.desktop_login_failed'), { code: `QWENWORK_DESKTOP_${code}` });
16
+ return new CliError(t('qwenwork.desktop_login_failed'), {
17
+ code: `QWENWORK_DESKTOP_${code}`,
18
+ details: { invocation: buildQwenWorkLoginInvocation() },
19
+ });
16
20
  }
17
21
 
18
22
  function parseTicket(raw, context) {
@@ -95,9 +99,12 @@ async function loginQwenWork(options = {}) {
95
99
  const expiresAt = startedAt + Number(value.expires_in) * 1000;
96
100
  if (value.status !== 'ok' || value.auth_mode !== 'token' || value.can_auto_use !== true ||
97
101
  value.token_type !== 'Bearer' || value.credential_source !== 'qwenwork_desktop' ||
98
- 'refresh_token' in value || 'refreshToken' in value || 'exchange_ticket' in value ||
102
+ 'refreshToken' in value || 'exchange_ticket' in value ||
99
103
  typeof value.access_token !== 'string' || !value.access_token || /\s/.test(value.access_token) || value.access_token.length > 32768 ||
100
104
  !Number.isInteger(value.expires_in) || value.expires_in < 1 || value.expires_in > 1800 ||
105
+ typeof value.refresh_token !== 'string' || !value.refresh_token || /\s/.test(value.refresh_token) || value.refresh_token.length > 32768 ||
106
+ !Number.isInteger(value.refresh_token_expires_in) || value.refresh_token_expires_in < 1 ||
107
+ (value.client_id && value.client_id !== DINGTALK_OAUTH_CLIENT_ID) ||
101
108
  value.corp_id !== ticket.corp_id || value.user_id !== ticket.user_id || value.exchange_id !== ticket.exchange_id ||
102
109
  (value.base_url && origin(value.base_url) !== baseUrl)) { throw fail('CREDENTIAL_INVALID'); }
103
110
  const verified = await requestJson(`${baseUrl}/openapi/cli/v1/auth/status`, {
@@ -106,34 +113,39 @@ async function loginQwenWork(options = {}) {
106
113
  if (verified.status !== 'ok' || verified.auth_mode !== 'token' || verified.can_auto_use !== true ||
107
114
  verified.corp_id !== value.corp_id || verified.user_id !== value.user_id || expiresAt <= Date.now()) { throw fail('IDENTITY_MISMATCH'); }
108
115
  const session = saveTokenSession({
109
- auth_mode: 'token', token_type: 'Bearer', access_token: value.access_token,
116
+ auth_mode: 'token', token_type: 'Bearer', access_token: value.access_token, refresh_token: value.refresh_token,
110
117
  expires_at: expiresAt, base_url: baseUrl, client_id: DINGTALK_OAUTH_CLIENT_ID,
111
118
  corp_id: value.corp_id, user_id: value.user_id, credential_source: 'qwenwork_desktop',
112
119
  }, options);
113
120
  const restored = loadTokenSession(options);
114
121
  if (!['user', 'project'].includes(session.persistence_scope) || !restored ||
115
- restored.access_token !== value.access_token || restored.base_url !== baseUrl ||
122
+ restored.access_token !== value.access_token || restored.refresh_token !== value.refresh_token || restored.base_url !== baseUrl ||
116
123
  restored.corp_id !== value.corp_id || restored.user_id !== value.user_id) { throw fail('PERSISTENCE_FAILED'); }
117
124
  // 身份、环境和存储细节仅用于内部校验,不进入模型可见的登录结果。
118
125
  return { ok: true, status: 'ok', can_auto_use: true };
119
126
  }
120
127
 
121
128
  async function run(args, options = {}) {
122
- if (args.includes('--help') || args.includes('-h')) { console.log(t('qwenwork.desktop_login_usage')); return; }
129
+ if (args.includes('--help') || args.includes('-h')) {
130
+ const invocation = buildQwenWorkLoginInvocation();
131
+ if (args.includes('--json')) { console.log(JSON.stringify({ usage: t('qwenwork.desktop_login_usage'), invocation })); }
132
+ else { console.log(`${t('qwenwork.desktop_login_usage')}\n${t('qwenwork.desktop_login_standalone', invocation.command)}`); }
133
+ return;
134
+ }
123
135
  const parsed = {};
124
136
  const seen = new Set();
125
137
  for (let index = 0; index < args.length; index++) {
126
138
  const flag = args[index];
127
- if (seen.has(flag)) { throw new CliError(t('qwenwork.desktop_login_usage'), { code: 'CLI_USAGE_ERROR', exitCode: 2 }); }
139
+ if (seen.has(flag)) { throw new CliError(t('qwenwork.desktop_login_usage'), { code: 'CLI_USAGE_ERROR', exitCode: 2, details: { invocation: buildQwenWorkLoginInvocation() } }); }
128
140
  seen.add(flag);
129
141
  if (['--json', '--quiet'].includes(flag)) { continue; }
130
142
  const key = flag === '--corp-id' ? 'corpId' : flag === '--endpoint' ? 'endpoint' : null;
131
143
  const value = args[++index];
132
- if (!key || !value || value.startsWith('--')) { throw new CliError(t('qwenwork.desktop_login_usage'), { code: 'CLI_USAGE_ERROR', exitCode: 2 }); }
144
+ if (!key || !value || value.startsWith('--')) { throw new CliError(t('qwenwork.desktop_login_usage'), { code: 'CLI_USAGE_ERROR', exitCode: 2, details: { invocation: buildQwenWorkLoginInvocation() } }); }
133
145
  parsed[key] = value;
134
146
  }
135
147
  const result = await loginQwenWork({ ...options, ...parsed });
136
- if (args.includes('--json') && !args.includes('--quiet') && (options.env || process.env).YIDA_QUIET !== '1') { console.log(JSON.stringify(result)); }
148
+ if (args.includes('--json') && !args.includes('--quiet') && (options.env || process.env).YIDA_QUIET !== '1') { console.log(JSON.stringify({ ...result, next_action: 'continue_business_operation_silently', user_visible: false })); }
137
149
  return result;
138
150
  }
139
151
 
@@ -175,6 +175,7 @@ function normalizeTokenResponse(response, baseUrl, clientId) {
175
175
  );
176
176
  return {
177
177
  auth_mode: 'token',
178
+ credential_source: data.credential_source,
178
179
  status: data.status || 'ok',
179
180
  can_auto_use: true,
180
181
  message: data.message || data.errorMsg || response.errorMsg,
@@ -486,7 +487,9 @@ async function tokenRefreshUnlocked(options, session) {
486
487
  // Token exchange follows the active environment endpoint, just like the
487
488
  // legacy login bootstrap. The response may return a different corp base_url
488
489
  // for business requests and generated application links.
489
- const authBaseUrlValue = resolveTokenBaseUrl({
490
+ // 桌面会话沿用首次换票的可信 origin;刷新不受后续全局环境切换影响。
491
+ const desktopSession = session.credential_source === 'qwenwork_desktop';
492
+ const authBaseUrlValue = desktopSession ? session.base_url : resolveTokenBaseUrl({
490
493
  projectRoot: options.projectRoot,
491
494
  endpoint: options.endpoint,
492
495
  });
@@ -499,6 +502,9 @@ async function tokenRefreshUnlocked(options, session) {
499
502
  requireOkResponse(response);
500
503
  const data = getTokenPayload(response);
501
504
  const responseBusinessBaseUrl = data.base_url || data.baseUrl;
505
+ if (desktopSession && responseBusinessBaseUrl && normalizeBaseUrl(responseBusinessBaseUrl, null) !== session.base_url) {
506
+ throw createTokenAuthRequiredError('refresh_origin_mismatch');
507
+ }
502
508
  const businessBaseUrl = session.base_url || authBaseUrlValue;
503
509
  const normalized = normalizeTokenResponse({
504
510
  ...data,
@@ -507,6 +513,7 @@ async function tokenRefreshUnlocked(options, session) {
507
513
  user_id: data.user_id || data.userId || session.user_id,
508
514
  user_name: data.user_name || data.userName || session.user_name,
509
515
  refresh_token: data.refresh_token || data.refreshToken || session.refresh_token,
516
+ credential_source: data.credential_source || session.credential_source,
510
517
  }, businessBaseUrl, clientId);
511
518
  if (!normalized.access_token) {
512
519
  return {
@@ -9,6 +9,7 @@ const { buildCommandManifest } = require('./command-manifest');
9
9
  const { buildEnvironmentSnapshot } = require('./env');
10
10
  const coreUtils = require('./utils');
11
11
  const { buildHostAssetCapabilities } = require('../asset/host-capabilities');
12
+ const { buildQwenWorkLoginInvocation } = require('./qwenwork-login-guidance');
12
13
  const { findProjectRoot, getAuthStatus } = coreUtils;
13
14
 
14
15
  function redactLogin(login) {
@@ -232,14 +233,15 @@ function buildAuthPath(login, env = process.env, runtime = null) {
232
233
  };
233
234
  // 桌面换票仅由当前活跃宿主和发行配置共同启用,不影响其他宿主的认证路径。
234
235
  if (localBeta?.desktopTicketLogin && runtime?.tool === 'qwenwork' && runtime?.subtype === 'qwenwork_desktop' && !envTokenSession) {
236
+ const invocation = buildQwenWorkLoginInvocation();
235
237
  authPath.auth_runtime = 'qwenwork_desktop_exchange_ticket';
236
238
  authPath.interactive_login_allowed = false;
237
239
  authPath.missing_token_action = 'RUN_OPENYIDA_LOGIN_QWENWORK';
238
240
  for (const key of ['auth_profile', 'corp_id', 'corp_name', 'user_id', 'user_name', 'store', 'persistence_scope', 'warning']) { delete authPath[key]; }
239
241
  authPath.next_step = authPath.can_auto_use
240
- ? 'Silently continue the requested business operation. Do not announce login or identity details.'
241
- : 'Silently run standalone openyida login-qwenwork --json, then continue the requested business operation. Do not narrate login, add shell operators, start OAuth, or edit environment files.';
242
- authPath.next_step_commands = authPath.can_auto_use ? [] : ['openyida login-qwenwork --json'];
242
+ ? 'Silently continue the requested business operation and report its business result.'
243
+ : `Run the recommended login command as returned: ${invocation.command}. No extra PATH or output setup is needed. After success silently continue the requested business operation. If it fails, follow the returned invocation guidance.`;
244
+ authPath.next_step_commands = authPath.can_auto_use ? [] : [invocation.command];
243
245
  }
244
246
  return authPath;
245
247
  }
@@ -248,7 +250,8 @@ function buildInteractiveLogin(auth, runtime) {
248
250
  if (auth.auth_runtime === 'qwenwork_desktop_exchange_ticket' && runtime.tool === 'qwenwork' && runtime.subtype === 'qwenwork_desktop') {
249
251
  return {
250
252
  mode: 'desktop_ticket', browser_default: 'not_required', browser_owner: 'none',
251
- recommended_command: auth.can_auto_use ? null : 'openyida login-qwenwork --json',
253
+ recommended_command: auth.can_auto_use ? null : buildQwenWorkLoginInvocation().command,
254
+ invocation: buildQwenWorkLoginInvocation(),
252
255
  agent_action: auth.can_auto_use ? 'continue_business_command' : 'run_standalone_login_qwenwork_and_wait',
253
256
  user_visible: false, success_action: 'continue_business_command_without_message',
254
257
  reason: 'qwenwork_desktop_pre_beta', completion_signal: 'process_exit_and_final_json', playwright_required: false,
@@ -24,7 +24,6 @@
24
24
  const fs = require('fs');
25
25
  const path = require('path');
26
26
  const { findProjectRoot } = require('./utils');
27
- const localBeta = require('../../package.json').openyidaLocalBeta;
28
27
 
29
28
  const DEFAULT_BASE_URL = 'https://www.aliwork.com';
30
29
  const DEFAULT_LOGIN_URL = 'https://www.aliwork.com/workPlatform';
@@ -108,7 +107,6 @@ const DEFAULT_INTERNATIONAL_ENV = {
108
107
  };
109
108
 
110
109
  const BUILTIN_ENVIRONMENTS = {
111
- ...(localBeta ? { [localBeta.environment]: { baseUrl: localBeta.baseUrl, loginUrl: localBeta.baseUrl + '/workPlatform', description: localBeta.description } } : {}),
112
110
  public: DEFAULT_PUBLIC_ENV,
113
111
  intl: DEFAULT_INTERNATIONAL_ENV,
114
112
  alibaba: DEFAULT_ALIBABA_INTERNAL_ENV,
@@ -167,7 +165,7 @@ function cloneBuiltinEnvironments() {
167
165
 
168
166
  function buildDefaultEnvsConfig() {
169
167
  return {
170
- current: localBeta?.environment || 'public',
168
+ current: 'public',
171
169
  environments: cloneBuiltinEnvironments(),
172
170
  };
173
171
  }
@@ -7,6 +7,7 @@ module.exports = {
7
7
  qwenwork: {
8
8
  desktop_login_failed: 'QwenWork desktop login failed. Check the desktop identity and login handoff before retrying.',
9
9
  desktop_login_usage: 'Usage: openyida login-qwenwork [--corp-id <corpId>] [--endpoint <url>] [--json]',
10
+ desktop_login_standalone: 'Run the recommended login command directly: {0}. No extra PATH or output setup is needed. After success continue the business operation silently; on failure follow the returned guidance.',
10
11
  credential_missing: 'No cloud Agent credential is available. Run this command inside the QwenWork cloud execution environment.',
11
12
  gateway_missing: 'QWENWORK_GATEWAY_URL or MULE_GATEWAY_URL is required.',
12
13
  invalid_url: 'The QwenWork gateway must use HTTPS (HTTP is allowed only for loopback) and contain no credentials, query or fragment.',
@@ -7,6 +7,7 @@ module.exports = {
7
7
  qwenwork: {
8
8
  desktop_login_failed: '千问办公桌面免登失败,请确认桌面登录身份和免登通道后重试。',
9
9
  desktop_login_usage: '用法:openyida login-qwenwork [--corp-id <corpId>] [--endpoint <url>] [--json]',
10
+ desktop_login_standalone: '请直接执行推荐的登录命令:{0}。无需额外处理 PATH 和输出;成功后静默继续业务,失败时按提示修正调用。',
10
11
  credential_missing: '未发现云端 Agent 凭证,请在千问办公云端执行环境中运行此命令。',
11
12
  gateway_missing: '缺少 QWENWORK_GATEWAY_URL 或 MULE_GATEWAY_URL。',
12
13
  invalid_url: '千问网关必须使用 HTTPS(仅回环地址允许 HTTP),且不能包含账号密码、查询参数或片段。',
@@ -0,0 +1,32 @@
1
+ 'use strict';
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+
5
+ /** 从当前调用或同一安装前缀定位入口,不依赖模型重写 PATH。 */
6
+ function resolveExecutable(invoked = process.argv[1], packageRoot = path.resolve(__dirname, '../..')) {
7
+ const usable = value => typeof value === 'string' && /^(?:\/|[a-z]:[\\/])/i.test(value)
8
+ && /^[\w./:\\-]+$/.test(value) && /^openyida(?:\.cmd|\.exe)?$/i.test(path.basename(value.replace(/\\/g, '/')));
9
+ if (usable(invoked)) { return invoked.replace(/\\/g, '/'); }
10
+ const candidates = [path.resolve(packageRoot, '../../../bin/openyida'), path.resolve(packageRoot, '../../openyida.cmd')];
11
+ for (const candidate of candidates) {
12
+ if (!usable(candidate) || !fs.existsSync(candidate)) { continue; }
13
+ try {
14
+ if (fs.realpathSync(candidate).startsWith(`${fs.realpathSync(packageRoot)}${path.sep}`)) { return candidate.replace(/\\/g, '/'); }
15
+ } catch { /* 不可用入口不扩展到其他安装目录。 */ }
16
+ }
17
+ return 'openyida';
18
+ }
19
+
20
+ function buildQwenWorkLoginInvocation(options = {}) {
21
+ const executable = resolveExecutable(options.executablePath, options.packageRoot);
22
+ return {
23
+ command: `${executable} login-qwenwork --json`,
24
+ required_runtime: { tool: 'qwenwork', subtype: 'qwenwork_desktop' },
25
+ execution: 'standalone_command',
26
+ preserve_command_verbatim: true,
27
+ output_visibility: 'internal',
28
+ success_next_step: 'continue_business_operation_silently',
29
+ failure_next_step: 'follow_returned_invocation_guidance',
30
+ };
31
+ }
32
+ module.exports = { buildQwenWorkLoginInvocation };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openyida",
3
- "version": "2026.10.23-beta.2",
3
+ "version": "2026.10.23-beta.3",
4
4
  "description": "OpenYida CLI - 宜搭低代码 AI 开发工具(安装即用,零配置)",
5
5
  "bin": {
6
6
  "openyida": "bin/yida-pre-beta.js",
@@ -18,13 +18,13 @@
18
18
 
19
19
  只以当前 `openyida agent-capabilities --summary-json` 的 `runtime.tool=qwenwork`、`runtime.subtype=qwenwork_desktop` 和 `builder_path.interactive_login.mode=desktop_ticket` 判断桌面换票能力。安装目录、产品名称和历史会话不能替代当前快照。
20
20
 
21
- 快照显示登录可用时直接执行用户请求;登录不可用且上述条件全部成立时,单独执行下面一条命令,成功后直接继续业务操作:
21
+ 快照显示登录可用时直接执行用户请求;登录不可用且上述条件全部成立时,原样执行 `builder_path.interactive_login.recommended_command`。该命令优先使用当前 CLI 的绝对路径,后续复用同一入口,不自行补 PATH。执行规则以 `builder_path.interactive_login.invocation` 为准;成功后直接继续业务操作。下面仅为命令形式示例:
22
22
 
23
23
  ```bash
24
24
  openyida login-qwenwork --json
25
25
  ```
26
26
 
27
- 这条命令不拼接 `cd`、`env`、重定向、管道、`head`、`echo` 或任何其他 Shell 命令;执行工具已提供退出状态,无需追加输出。专用命令只消费本次私有通道的短期一次性兑换票,不调用或递归进入普通 `login`。
27
+ 直接使用推荐的完整命令即可,无需额外处理 PATH 和输出;执行工具会提供结果和退出状态。调用格式不匹配时,按返回的推荐命令修正后继续。专用命令只消费本次私有通道的短期一次性兑换票,不调用或递归进入普通 `login`。
28
28
 
29
29
  真实宜搭凭据仅由 CLI 校验并持久化,不读取、填写或展示任何令牌、兑换票和认证文件。保持包内配置及实际认证 origin,不根据历史记忆猜测环境等价或切换到其他环境、组织。凭据过期时重新执行专用命令;票据缺失、过期、重放、签发或消费失败时停止,不回退 OAuth、直接凭据或手改环境文件。
30
30