openyida 2026.10.23-beta.2 → 2026.10.23-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
@@ -476,8 +476,7 @@ When preparing a PR that needs a personal Fork, or when you ask how to support t
476
476
 
477
477
  | Command | Description |
478
478
  |---------|-------------|
479
- | `openyida login [target-url] [--env <name>\|--intl\|--overseas\|--global\|--yidaapps\|--alibaba] [--client-id <clientId>] [--endpoint <url>] [--no-browser]` | Login with OAuth token mode |
480
- | `openyida login-qwenwork [--corp-id <corpId>] [--endpoint <url>] [--json]` | Log in to YiDA with a one-time QwenWork exchange ticket |
479
+ | `openyida login [--qwenwork] [target-url] [--env <name>\|--intl\|--overseas\|--global\|--yidaapps\|--alibaba] [--client-id <clientId>] [--endpoint <url>] [--no-browser]` | Login with OAuth token mode |
481
480
  | `openyida logout` | Logout / unbind current project auth |
482
481
  | `openyida auth <status\|login\|refresh\|logout\|profiles\|profile switch\|qwenwork user-info> [--json]` | Token login state and profile management |
483
482
  | `openyida org <list\|switch> [--json] [--corp-id <corpId>]` | Organization management (list / switch existing profiles first) |
package/bin/yida.js CHANGED
@@ -296,7 +296,6 @@ function getFirstPositionalArg(cliArgs, startIndex = 0) {
296
296
  }
297
297
 
298
298
  const UNSUPPORTED_LEGACY_LOGIN_FLAGS = new Set([
299
- '--qwenwork',
300
299
  '--qr',
301
300
  '--qr-code',
302
301
  '--agent-qr',
@@ -596,11 +595,20 @@ async function main() {
596
595
  assertManagedInvocation(command, rawArgs);
597
596
  applyQuietFlag();
598
597
  // 一次性票据只在当前进程消费,不能先自更新并重启命令。
599
- if (command === 'login-qwenwork') {
600
- await require('../lib/auth/qwenwork-desktop').run(args);
601
- return;
598
+ switch (command) {
599
+ case 'login-qwenwork':
600
+ await require('../lib/auth/qwenwork-desktop').run(args);
601
+ return;
602
602
  }
603
603
  applyGlobalEnvironmentFlags();
604
+ // 桌面准备完成后继续执行公开命令,由真实登录态决定结果。
605
+ if (command === 'login' && args.includes('--qwenwork')) {
606
+ const loginArgs = applyLoginEnvironmentFlags(args, { inferTargetUrl: true });
607
+ if (hasHelpFlag(loginArgs)) { printLoginHelp(); return; }
608
+ assertNoUnsupportedLegacyLoginFlags(rawArgs, loginArgs);
609
+ await require('../lib/auth/qwenwork-login').run(loginArgs, buildTokenLoginOptions(loginArgs));
610
+ return;
611
+ }
604
612
 
605
613
  const { maybeAutoUpdate } = require('../lib/core/update');
606
614
  const updateResult = await maybeAutoUpdate({
@@ -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) {
@@ -74,11 +78,11 @@ async function requestJson(url, init, options) {
74
78
  }
75
79
  }
76
80
 
77
- /** 专用免登入口:只消费环境中的一次性兑换票,不调用或重启普通 login。 */
81
+ /** 客户端内部入口:消费一次性兑换票并持久化双 Token,不调用或重启普通 login。 */
78
82
  async function loginQwenWork(options = {}) {
79
83
  const env = options.env || process.env;
80
- const raw = env[TICKET_ENV];
81
- const context = env[TICKET_CONTEXT_ENV];
84
+ const raw = options.exchangeTicket || env[TICKET_ENV];
85
+ const context = options.ticketContext || env[TICKET_CONTEXT_ENV];
82
86
  delete env[TICKET_ENV];
83
87
  delete env[TICKET_CONTEXT_ENV];
84
88
  if (isEnvTokenAuthMode(env) || readManagedContext(env)) { throw fail('MANAGED_AUTH_CONFLICT'); }
@@ -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,40 @@ 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
- const key = flag === '--corp-id' ? 'corpId' : flag === '--endpoint' ? 'endpoint' : null;
142
+ const key = flag === '--corp-id' ? 'corpId' : flag === '--endpoint' ? 'endpoint'
143
+ : flag === '--exchange-ticket' ? 'exchangeTicket' : flag === '--ticket-context' ? 'ticketContext' : null;
131
144
  const value = args[++index];
132
- if (!key || !value || value.startsWith('--')) { throw new CliError(t('qwenwork.desktop_login_usage'), { code: 'CLI_USAGE_ERROR', exitCode: 2 }); }
145
+ if (!key || !value || value.startsWith('--')) { throw new CliError(t('qwenwork.desktop_login_usage'), { code: 'CLI_USAGE_ERROR', exitCode: 2, details: { invocation: buildQwenWorkLoginInvocation() } }); }
133
146
  parsed[key] = value;
134
147
  }
135
148
  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)); }
149
+ 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
150
  return result;
138
151
  }
139
152
 
@@ -0,0 +1,86 @@
1
+ 'use strict';
2
+ const { t } = require('../core/i18n');
3
+ const { CliError } = require('../core/cli-error');
4
+ const { loadTokenSession, resolveTokenSession } = require('./token-store');
5
+ const { tokenLogin, tokenRefresh, isRefreshAuthRequired } = require('./token-auth');
6
+
7
+ function failure(code) {
8
+ return new CliError(t('qwenwork.login_check_failed'), { code });
9
+ }
10
+ function summary(ok) { return { ok, status: ok ? 'ok' : 'not_logged_in', can_auto_use: ok }; }
11
+
12
+ /** 以服务端状态为准;网络异常不能触发浏览器登录。 */
13
+ async function verify(session, options = {}) {
14
+ if (!session || !session.access_token) { return false; }
15
+ const controller = new AbortController();
16
+ const timer = setTimeout(() => controller.abort(), options.timeoutMs || 15000);
17
+ try {
18
+ const response = await (options.fetch || globalThis.fetch)(`${session.base_url.replace(/\/$/, '')}/openapi/cli/v1/auth/status`, {
19
+ headers: { authorization: `Bearer ${session.access_token}`, accept: 'application/json' },
20
+ redirect: 'error', signal: controller.signal,
21
+ });
22
+ if ([401, 403].includes(response.status)) { await response.body?.cancel(); return false; }
23
+ if (!response.ok) { await response.body?.cancel(); throw failure('QWENWORK_LOGIN_CHECK_FAILED'); }
24
+ const reader = response.body?.getReader();
25
+ if (!reader) { throw failure('QWENWORK_LOGIN_CHECK_FAILED'); }
26
+ const chunks = []; let size = 0;
27
+ try {
28
+ for (let part = await reader.read(); !part.done; part = await reader.read()) {
29
+ size += part.value.byteLength;
30
+ if (size > 65536) { await reader.cancel(); throw failure('QWENWORK_LOGIN_CHECK_FAILED'); }
31
+ chunks.push(part.value);
32
+ }
33
+ } finally { reader.releaseLock(); }
34
+ const value = JSON.parse(Buffer.concat(chunks).toString('utf8'));
35
+ if (value.status !== 'ok' || value.can_auto_use !== true || value.auth_mode !== 'token') {
36
+ if (['not_logged_in', 'invalid_access_token', 'token_expired', 'auth_required'].includes(value.status)) { return false; }
37
+ throw failure('QWENWORK_LOGIN_CHECK_FAILED');
38
+ }
39
+ if (value.corp_id !== session.corp_id || value.user_id !== session.user_id ||
40
+ (options.corpId && value.corp_id !== options.corpId)) { throw failure('QWENWORK_LOGIN_IDENTITY_MISMATCH'); }
41
+ return true;
42
+ } catch (error) {
43
+ if (error instanceof CliError) { throw error; }
44
+ throw failure('QWENWORK_LOGIN_CHECK_FAILED');
45
+ } finally { clearTimeout(timer); }
46
+ }
47
+
48
+ /** 公开入口只复用、刷新或按需发起 OAuth,不接收客户端的一次性票据。 */
49
+ async function ensureQwenWorkLogin(options = {}, dependencies = {}) {
50
+ const resolution = resolveTokenSession(options);
51
+ if (resolution.status === 'profile_required') { throw failure('AUTH_PROFILE_REQUIRED'); }
52
+ let session = loadTokenSession(options);
53
+ if (session && options.endpoint && new URL(session.base_url).origin !== new URL(options.endpoint).origin) {
54
+ session = null;
55
+ }
56
+ if (await verify(session, options)) { return summary(true); }
57
+ if (session?.refresh_token) {
58
+ try {
59
+ const refreshed = await (dependencies.refresh || tokenRefresh)(options);
60
+ if (refreshed && refreshed.ok !== false && refreshed.access_token) {
61
+ if (await verify(refreshed, options)) { return summary(true); }
62
+ } else if (!isRefreshAuthRequired(refreshed)) { throw failure('QWENWORK_LOGIN_REFRESH_FAILED'); }
63
+ } catch (error) {
64
+ if (!isRefreshAuthRequired(error) && ![401, 403].includes(error.statusCode)) {
65
+ if (error instanceof CliError) { throw error; }
66
+ throw failure('QWENWORK_LOGIN_REFRESH_FAILED');
67
+ }
68
+ }
69
+ }
70
+ if (options.checkOnly) { return summary(false); }
71
+ const result = await (dependencies.login || tokenLogin)(options);
72
+ if (!result || result.ok === false || !result.access_token || !await verify(result, options)) {
73
+ throw failure('QWENWORK_LOGIN_FAILED');
74
+ }
75
+ return summary(true);
76
+ }
77
+
78
+ async function run(args, options = {}) {
79
+ const result = await ensureQwenWorkLogin({ ...options, checkOnly: args.includes('--check-only') });
80
+ if (args.includes('--json') && !args.includes('--quiet') && process.env.YIDA_QUIET !== '1') {
81
+ console.log(JSON.stringify(result));
82
+ }
83
+ if (!result.ok) { process.exitCode = 1; }
84
+ return result;
85
+ }
86
+ module.exports = { ensureQwenWorkLogin, run };
@@ -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
- authPath.interactive_login_allowed = false;
238
+ authPath.interactive_login_allowed = true;
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
  }
@@ -247,9 +249,10 @@ function buildAuthPath(login, env = process.env, runtime = null) {
247
249
  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
- mode: 'desktop_ticket', browser_default: 'not_required', browser_owner: 'none',
251
- recommended_command: auth.can_auto_use ? null : 'openyida login-qwenwork --json',
252
- agent_action: auth.can_auto_use ? 'continue_business_command' : 'run_standalone_login_qwenwork_and_wait',
252
+ mode: 'desktop_ticket', browser_default: 'not_required', browser_owner: 'cli_if_auth_required',
253
+ recommended_command: auth.can_auto_use ? null : buildQwenWorkLoginInvocation().command,
254
+ invocation: buildQwenWorkLoginInvocation(),
255
+ agent_action: auth.can_auto_use ? 'continue_business_command' : 'run_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,
255
258
  };
@@ -1140,11 +1140,11 @@ const COMMAND_GROUPS = [
1140
1140
  id: 'auth',
1141
1141
  titleKey: 'help.group_auth',
1142
1142
  commands: [
1143
- command('login', ['login'], 'login [target-url] [--env <name>|--intl|--overseas|--global|--yidaapps|--alibaba] [--client-id <clientId>] [--endpoint <url>] [--no-browser]', 'help.cmd_login', { managedRun: { scope: 'local', allowedFlags: ['--check-only', '--json', '--quiet'], requiredFlags: ['--check-only'] },
1143
+ command('login', ['login'], 'login [--qwenwork] [target-url] [--env <name>|--intl|--overseas|--global|--yidaapps|--alibaba] [--client-id <clientId>] [--endpoint <url>] [--no-browser]', 'help.cmd_login', { managedRun: { scope: 'local', allowedFlags: ['--check-only', '--json', '--quiet'], requiredFlags: ['--check-only'] },
1144
1144
  requiresLogin: false,
1145
1145
  output: 'json',
1146
1146
  }),
1147
- command('login-qwenwork', ['login-qwenwork'], 'login-qwenwork [--corp-id <corpId>] [--endpoint <url>] [--json]', 'help.cmd_login_qwenwork', { managedRun: { scope: 'denied', reason: 'identity_exchange' }, requiresLogin: false, output: 'json' }),
1147
+ command('login-qwenwork', ['login-qwenwork'], 'login-qwenwork --exchange-ticket <ticket> --ticket-context <json> [--json]', 'help.cmd_login_qwenwork', { managedRun: { scope: 'denied', reason: 'identity_exchange' }, requiresLogin: false, output: 'json', hidden: true }),
1148
1148
  command('logout', ['logout'], 'logout', 'help.cmd_logout', { managedRun: { scope: 'denied', reason: 'scope_not_audited' }, requiresLogin: false }),
1149
1149
  command('auth', ['auth'], 'auth <status|login|refresh|logout|profiles|profile switch|qwenwork user-info> [--json]', 'help.cmd_auth', { managedRun: { scope: 'actions', actions: { status: { scope: 'local', allowedFlags: ['--json', '--quiet'] }, qwenwork: { scope: 'denied', reason: 'identity_exchange' } } }, requiresLogin: false }),
1150
1150
  command('org', ['org'], 'org <list|switch> [--json] [--corp-id <corpId>]', 'help.cmd_org', { managedRun: { scope: 'denied', reason: 'scope_not_audited' } }),
@@ -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
  }
@@ -5,8 +5,10 @@
5
5
 
6
6
  module.exports = {
7
7
  qwenwork: {
8
+ login_check_failed: 'Unable to verify the login session. Please retry.',
8
9
  desktop_login_failed: 'QwenWork desktop login failed. Check the desktop identity and login handoff before retrying.',
9
- desktop_login_usage: 'Usage: openyida login-qwenwork [--corp-id <corpId>] [--endpoint <url>] [--json]',
10
+ desktop_login_usage: 'Usage: openyida login --qwenwork [--json] [--check-only]',
11
+ 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
12
  credential_missing: 'No cloud Agent credential is available. Run this command inside the QwenWork cloud execution environment.',
11
13
  gateway_missing: 'QWENWORK_GATEWAY_URL or MULE_GATEWAY_URL is required.',
12
14
  invalid_url: 'The QwenWork gateway must use HTTPS (HTTP is allowed only for loopback) and contain no credentials, query or fragment.',
@@ -491,7 +493,7 @@ Examples:
491
493
  save_permission_usage: 'Usage: openyida save-permission <appType> <formUuid> [--package-uuid <packageUuid>] [--data-permission <json>] [--action-permission <json>] [--package-type <FORM_PACKAGE_VIEW|FORM_PACKAGE_START>]',
492
494
  save_permission_example: "Example: openyida save-permission APP_XXX FORM-XXX --data-permission '{\"role\":\"DEFAULT\",\"dataRange\":\"SELF\"}'",
493
495
  exec_failed: '\n❌ Execution failed: {0}',
494
- login_usage: 'Usage: openyida login [entryUrl|--public|--alibaba|--intl] [--no-browser] [--check-only] [--json] [--client-id <clientId>]',
496
+ login_usage: 'Usage: openyida login [--qwenwork] [entryUrl|--public|--alibaba|--intl] [--no-browser] [--check-only] [--json] [--client-id <clientId>]',
495
497
  login_example: 'Examples:\n openyida login # Automatically open the browser via OAuth loopback login\n openyida login --no-browser # Let the caller handle the authorization URL\n openyida login --check-only --json # Check token auth status only\n openyida login --intl # Login against the international environment\n OPENYIDA_NO_BROWSER=1 openyida login # Suppress auto-opening the browser with an environment variable\n openyida auth login # Login alias',
496
498
  login_unsupported_option: 'Removed legacy login option is no longer supported: {0}. Use token/OAuth login; env token mode reads only OPENYIDA_* tokens.',
497
499
  auth_usage: 'Usage: openyida auth <status|login|refresh|logout|profiles|profile switch|qwenwork user-info> [--json]',
@@ -5,8 +5,10 @@
5
5
 
6
6
  module.exports = {
7
7
  qwenwork: {
8
+ login_check_failed: '登录状态暂时无法确认,请重试。',
8
9
  desktop_login_failed: '千问办公桌面免登失败,请确认桌面登录身份和免登通道后重试。',
9
- desktop_login_usage: '用法:openyida login-qwenwork [--corp-id <corpId>] [--endpoint <url>] [--json]',
10
+ desktop_login_usage: '用法:openyida login --qwenwork [--json] [--check-only]',
11
+ desktop_login_standalone: '请直接执行推荐的登录命令:{0}。无需额外处理 PATH 和输出;成功后静默继续业务,失败时按提示修正调用。',
10
12
  credential_missing: '未发现云端 Agent 凭证,请在千问办公云端执行环境中运行此命令。',
11
13
  gateway_missing: '缺少 QWENWORK_GATEWAY_URL 或 MULE_GATEWAY_URL。',
12
14
  invalid_url: '千问网关必须使用 HTTPS(仅回环地址允许 HTTP),且不能包含账号密码、查询参数或片段。',
@@ -491,7 +493,7 @@ openyida - 宜搭命令行工具
491
493
  save_permission_usage: '用法: openyida save-permission <appType> <formUuid> [--package-uuid <packageUuid>] [--data-permission <json>] [--action-permission <json>] [--package-type <FORM_PACKAGE_VIEW|FORM_PACKAGE_START>]',
492
494
  save_permission_example: "示例: openyida save-permission APP_XXX FORM-XXX --data-permission '{\"role\":\"DEFAULT\",\"dataRange\":\"SELF\"}'",
493
495
  exec_failed: '\n❌ 执行失败: {0}',
494
- login_usage: '用法: openyida login [entryUrl|--public|--alibaba|--intl] [--no-browser] [--check-only] [--json] [--client-id <clientId>]',
496
+ login_usage: '用法: openyida login [--qwenwork] [entryUrl|--public|--alibaba|--intl] [--no-browser] [--check-only] [--json] [--client-id <clientId>]',
495
497
  login_example: '示例:\n openyida login # 通过 OAuth loopback 自动打开浏览器登录\n openyida login --no-browser # 不自动打开浏览器,由调用方接管授权链接\n openyida login --check-only --json # 只检查 token 登录态\n openyida login --intl # 使用国际站环境登录\n OPENYIDA_NO_BROWSER=1 openyida login # 通过环境变量抑制自动打开浏览器\n openyida auth login # 登录入口别名',
496
498
  login_unsupported_option: '已删除的旧登录参数不再支持: {0}。请使用 token/OAuth 登录;env token 模式下只读取 OPENYIDA_* token。',
497
499
  first_run_title: ' 🤖 OpenYida - AI 问答模式已开启! ',
@@ -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: '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.4",
4
4
  "description": "OpenYida CLI - 宜搭低代码 AI 开发工具(安装即用,零配置)",
5
5
  "bin": {
6
6
  "openyida": "bin/yida-pre-beta.js",
@@ -1,33 +1,10 @@
1
- # 千问办公桌面端后台准备
1
+ # 千问办公登录
2
2
 
3
- ## 用户沟通
3
+ 仅在运行环境为千问办公桌面端时,使用 `openyida login --qwenwork`。
4
+ 优先使用能力摘要返回的当前版本入口;成功后静默继续用户请求的业务操作,只汇报业务结果。
4
5
 
5
- 安装、版本核验、环境检查、自动登录和登录恢复均为内部准备,不加入用户可见的步骤、待办或完成总结,也不播报「免登成功」「正在换票」等状态。准备完成后直接继续用户请求的业务操作。
6
+ 客户端在执行公开登录命令前,内部调用 `login-qwenwork` 消费短期一次性票据并保存登录态。
7
+ 公开命令回查服务端,已有登录态可用则直接成功,失效时尝试 refresh token,确需重新授权时复用浏览器登录流程。
8
+ 网络错误不会触发浏览器登录。安装、认证准备、组织和用户标识不作为面向用户的业务结果。
6
9
 
7
- 登录命令和能力快照的 JSON 仅用于内部判断。不向用户展示或解释认证方式、环境名、后端域名、用户 ID、组织 ID、profile、凭据路径、有效期和令牌,即使掩码后也不展示。最终只交付用户需要的业务结果、应用名称和应用访问链接;应用链接属于业务交付,可正常展示。
8
-
9
- 仅在确实需要用户操作才能继续时,用一句普通语言说明阻塞和所需操作,不展开协议、接口、路径或内部错误细节。用户明确要求排障或查看认证信息时,再按其指定范围说明。
10
-
11
- ## 安装与技能
12
-
13
- 用户指定版本时先安装该版本,再做一次能力检查。镜像尚未同步时,直接使用官方 npm 源安装;短暂的下载 404 先重试核验,不据此断言包已损坏或擅自换成正式版。随包技能通过能力快照的 `builder_path.communication.bundled_skill_path` 定位,安装后优先读取该版本技能。
14
-
15
- 配套技能复制是可选步骤。宿主技能目录写入受限时保留旧目录,CLI 安装继续完成,使用包内技能;不要求用户修改沙箱规则,不反复申请宿主目录权限。
16
-
17
- ## 自动登录
18
-
19
- 只以当前 `openyida agent-capabilities --summary-json` 的 `runtime.tool=qwenwork`、`runtime.subtype=qwenwork_desktop` 和 `builder_path.interactive_login.mode=desktop_ticket` 判断桌面换票能力。安装目录、产品名称和历史会话不能替代当前快照。
20
-
21
- 快照显示登录可用时直接执行用户请求;登录不可用且上述条件全部成立时,单独执行下面一条命令,成功后直接继续业务操作:
22
-
23
- ```bash
24
- openyida login-qwenwork --json
25
- ```
26
-
27
- 这条命令不拼接 `cd`、`env`、重定向、管道、`head`、`echo` 或任何其他 Shell 命令;执行工具已提供退出状态,无需追加输出。专用命令只消费本次私有通道的短期一次性兑换票,不调用或递归进入普通 `login`。
28
-
29
- 真实宜搭凭据仅由 CLI 校验并持久化,不读取、填写或展示任何令牌、兑换票和认证文件。保持包内配置及实际认证 origin,不根据历史记忆猜测环境等价或切换到其他环境、组织。凭据过期时重新执行专用命令;票据缺失、过期、重放、签发或消费失败时停止,不回退 OAuth、直接凭据或手改环境文件。
30
-
31
- 多组织确实需要用户选择时,仅询问完成业务操作所需的组织,不由模型猜测。其他宿主继续遵循原认证快照。
32
-
33
- 用户只要求新建空白应用时,直接创建该应用,无需展开 PRD、表单、页面或主题设计。创建命令以当前版本契约为准,成功后只交付应用名称和访问链接。
10
+ 内部命令及其票据参数由客户端管理,模型无需了解或生成。