openyida 2026.10.23-2 → 2026.10.23-beta.2

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.
Files changed (43) hide show
  1. package/README.md +22 -3
  2. package/bin/yida-pre-beta.js +11 -0
  3. package/bin/yida.js +10 -5
  4. package/lib/app/services/canvas-page-schema-builder.js +1 -43
  5. package/lib/auth/qwenwork-client.js +190 -0
  6. package/lib/auth/qwenwork-desktop.js +140 -0
  7. package/lib/auth/qwenwork.js +37 -0
  8. package/lib/auth/token-store.js +1 -0
  9. package/lib/core/agent-capabilities.js +35 -5
  10. package/lib/core/command-manifest.js +9 -36
  11. package/lib/core/env-manager.js +3 -1
  12. package/lib/core/locales/en.js +16 -14
  13. package/lib/core/locales/zh.js +16 -14
  14. package/lib/core/task-center.js +4 -32
  15. package/lib/samples/openyida-scaffold/canvas-navigation.jsx +2 -6
  16. package/lib/task-center/contract.js +1 -26
  17. package/lib/task-center/loader.js +0 -5
  18. package/lib/task-center/scaffold.js +0 -9
  19. package/package.json +13 -5
  20. package/scripts/install-qwenwork-pre-beta.js +40 -0
  21. package/yida-skills/SKILL.md +7 -2
  22. package/yida-skills/references/qwenwork-desktop-login.md +33 -0
  23. package/yida-skills/references/setup-and-env.md +4 -0
  24. package/yida-skills/references/yida-api.md +1 -3
  25. package/yida-skills/skills/yida-canvas-custom-page/SKILL.md +2 -4
  26. package/yida-skills/skills/yida-canvas-custom-page/references/canvas-style-implementation-guide.md +0 -2
  27. package/yida-skills/skills/yida-canvas-custom-page/references/navigation-and-entry-guide.md +0 -2
  28. package/yida-skills/skills/yida-canvas-data-binding/SKILL.md +2 -2
  29. package/yida-skills/skills/yida-canvas-data-binding/references/workflow-capabilities.md +1 -29
  30. package/yida-skills/skills/yida-create-page/SKILL.md +0 -1
  31. package/yida-skills/skills/yida-design/SKILL.md +1 -1
  32. package/yida-skills/skills/yida-design/references/application-style-library.md +0 -2
  33. package/yida-skills/skills/yida-design/references/application-theme-consistency.md +0 -4
  34. package/yida-skills/skills/yida-design/sub_skill/page-design/SKILL.md +44 -27
  35. package/yida-skills/skills/yida-design/workflow/step-2-theme-system.md +1 -3
  36. package/yida-skills/skills/yida-login/SKILL.md +18 -0
  37. package/yida-skills/skills/yida-task-center/SKILL.md +1 -3
  38. package/yida-skills/skills-index.json +0 -13
  39. package/lib/app/form-message.js +0 -352
  40. package/lib/navigation/page-navigation.js +0 -62
  41. package/lib/task-center/navigation.js +0 -41
  42. package/yida-skills/skills/yida-form-message/SKILL.md +0 -53
  43. package/yida-skills/skills/yida-form-message/references/guide.md +0 -111
package/README.md CHANGED
@@ -143,6 +143,25 @@ openyida login --check-only --json
143
143
  openyida auth status
144
144
  ```
145
145
 
146
+ For QwenWork cloud identity diagnostics, run inside the actual cloud execution environment:
147
+
148
+ ```bash
149
+ openyida auth qwenwork user-info --json
150
+ ```
151
+
152
+ 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.
153
+
154
+ 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.
155
+
156
+ ```js
157
+ const { exchangeQwenWorkAgentToken, getQwenWorkUserInfo } = require('openyida/lib/auth/qwenwork-client');
158
+ async function readCloudUser() {
159
+ const credential = await exchangeQwenWorkAgentToken();
160
+ // The result contains QwenWork identifiers, not YiDA IDs.
161
+ return getQwenWorkUserInfo(credential);
162
+ }
163
+ ```
164
+
146
165
  To reuse an existing shared login profile for the current project, switch first instead of starting a new OAuth login:
147
166
 
148
167
  ```bash
@@ -458,8 +477,9 @@ When preparing a PR that needs a personal Fork, or when you ask how to support t
458
477
  | Command | Description |
459
478
  |---------|-------------|
460
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 |
461
481
  | `openyida logout` | Logout / unbind current project auth |
462
- | `openyida auth <status\|login\|refresh\|logout\|profiles\|profile switch>` | Token login state and profile management |
482
+ | `openyida auth <status\|login\|refresh\|logout\|profiles\|profile switch\|qwenwork user-info> [--json]` | Token login state and profile management |
463
483
  | `openyida org <list\|switch> [--json] [--corp-id <corpId>]` | Organization management (list / switch existing profiles first) |
464
484
  | `openyida env [--json\|setup\|list\|show\|switch\|add\|remove] [options]` | Detect AI tool environment & token login state |
465
485
 
@@ -516,7 +536,6 @@ When preparing a PR that needs a personal Fork, or when you ask how to support t
516
536
  | `openyida print-template <list\|inspect\|fields\|build\|create\|copy\|save\|restore\|settings\|status\|order\|render\|files> <appType> <formUuid> [templateId] [--config <file>] [--scene <order\|quotation\|delivery\|receipt\|approval\|codes>] [--bindings <file>] [--name <title>] [--expected-hash <hash>] [--backup <file>] [--out <file>] [--status START\|STOP] [--instance <id>] [--page <n>] [--size <n>] [--allow-permission-change] [--json] \| print-template <list\|render> --detail-url <url> [--template-id <id>] [--json]` | Manage native YiDA print templates, generate document layouts, back up, restore and request PDFs |
517
537
  | `openyida form-view <list\|inspect\|save\|create\|order> <appType> <formUuid> [viewUuid] [--config <file>] [--name <title>] [--with-config] [--expected-hash <hash>] [--backup <file>] [--replace] [--json]` | Native form views: list, inspect and configure columns, widths, frozen columns and filters |
518
538
  | `openyida custom-button <list\|inspect\|options\|create\|update\|delete> <appType> <formUuid> [buttonUuid] [--config <file>] [--expected-hash <hash>] [--backup <file>] [--yes] [--json]` | Native custom buttons: inspect and configure form edits, automation flows and page links |
519
- | `openyida form-message <list\|inspect\|options\|create\|update\|delete> <appType> <formUuid> [messageId] [--config <file>] [--expected-hash <hash>] [--backup <file>] [--yes] [--json]` | Native form notifications: inspect triggers, recipients and templates, then safely maintain bindings |
520
539
  | `openyida get-schema <appType> <formUuid\|--all> [--summary-json\|--field-map-json\|--analysis-json]` | Get one form Schema or all form Schemas |
521
540
  | `openyida check-prd-completeness <prd.md> --app-type <appType> [--build-manifest <file>] [--json]` | Check resources, frontend menu hiding and application theme delivery |
522
541
  | `openyida check-design <design.md> [--prd <prd.md>] [--base-dir <dir>] [--json]` | Validate design documents, theme tokens and PRD handoff |
@@ -534,7 +553,7 @@ When preparing a PR that needs a personal Fork, or when you ask how to support t
534
553
  | Command | Description |
535
554
  |---------|-------------|
536
555
  | `openyida data <query\|get\|create\|update\|transfer> <resource> ... \| delete form <appType> <formUuid> --inst-id <id> --expect-form-name <name> --expect-form-type receipt --confirm [--json]` | Unified data management (form/process/task/subform) |
537
- | `openyida task-center <todo\|created\|done\|cc\|proxy\|capabilities\|link\|scaffold\|options\|inspect\|count> [--spec <file.json> --output <file.canvas.jsx>] [options]` | Global task center (todo/processed/cc etc.) |
556
+ | `openyida task-center <todo\|created\|done\|cc\|proxy\|capabilities\|scaffold\|options\|inspect\|count> [--spec <file.json> --output <file.canvas.jsx>] [options]` | Global task center (todo/processed/cc etc.) |
538
557
  | `openyida basic-info <overview\|commodity\|grant\|capacity\|quota\|abs-path\|dataflow\|i18n\|domain>` | Query organization basic info, capacity, quotas, and domain settings |
539
558
  | `openyida read-dingtalk-doc <docUrl> [--output <file>] [--json]` | Fetch Markdown content from a DingTalk document |
540
559
  | `openyida read-dingtalk-tingji <taskUuid> [--json]` | Fetch DingTalk Tingji details by task UUID |
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ const os = require('node:os');
4
+ const path = require('node:path');
5
+ const config = require('../package.json').openyidaLocalBeta;
6
+ // PRE beta 消费包内测试默认值,真实凭据仍由原认证模块管理。
7
+ process.env.OPENYIDA_ENDPOINT ||= config.baseUrl;
8
+ process.env.OPENYIDA_ENV ||= config.environment;
9
+ process.env.OPENYIDA_AUTH_DIR ||= path.join(os.homedir(), '.openyida', config.authDirectory);
10
+ process.env.OPENYIDA_SKIP_UPDATE_CHECK = '1';
11
+ require('./yida.js');
package/bin/yida.js CHANGED
@@ -296,6 +296,7 @@ function getFirstPositionalArg(cliArgs, startIndex = 0) {
296
296
  }
297
297
 
298
298
  const UNSUPPORTED_LEGACY_LOGIN_FLAGS = new Set([
299
+ '--qwenwork',
299
300
  '--qr',
300
301
  '--qr-code',
301
302
  '--agent-qr',
@@ -594,6 +595,11 @@ async function main() {
594
595
  });
595
596
  assertManagedInvocation(command, rawArgs);
596
597
  applyQuietFlag();
598
+ // 一次性票据只在当前进程消费,不能先自更新并重启命令。
599
+ if (command === 'login-qwenwork') {
600
+ await require('../lib/auth/qwenwork-desktop').run(args);
601
+ return;
602
+ }
597
603
  applyGlobalEnvironmentFlags();
598
604
 
599
605
  const { maybeAutoUpdate } = require('../lib/core/update');
@@ -714,6 +720,10 @@ async function main() {
714
720
 
715
721
  case 'auth': {
716
722
  const subCommand = args[0];
723
+ if (subCommand === 'qwenwork') {
724
+ await require('../lib/auth/qwenwork').run(args.slice(1));
725
+ break;
726
+ }
717
727
  const authArgs = applyLoginEnvironmentFlags(args.slice(1), { inferTargetUrl: true });
718
728
  const { getAuthStatus } = require('../lib/core/utils');
719
729
  const { tokenLogin, tokenLogout, tokenRefresh } = require('../lib/auth/token-auth');
@@ -837,11 +847,6 @@ async function main() {
837
847
  await run(args);
838
848
  break;
839
849
  }
840
- case 'form-message': {
841
- const { run } = require('../lib/app/form-message');
842
- await run(args);
843
- break;
844
- }
845
850
  case 'print-template': {
846
851
  const { run } = require('../lib/print-template/print-template');
847
852
  await run(args);
@@ -10,16 +10,7 @@ const {
10
10
  findEmojiInValue,
11
11
  } = require('../../core/no-emoji-detector');
12
12
 
13
- const { createPageNavigation } = require('../../navigation/page-navigation');
14
- const { createTaskCenterContract } = require('../../task-center/contract');
15
- const { createTaskCenterLoader } = require('../../task-center/loader');
16
- const { createTaskCenterNavigation } = require('../../task-center/navigation');
17
-
18
- const CANVAS_YIDA_API_BRIDGE_SOURCE = `${createPageNavigation.toString()}
19
- ${createTaskCenterNavigation.toString()}
20
- ${createTaskCenterContract.toString()}
21
- ${createTaskCenterLoader.toString()}
22
- var OPENYIDA_YIDA_FORM_API_METHODS = [
13
+ const CANVAS_YIDA_API_BRIDGE_SOURCE = `var OPENYIDA_YIDA_FORM_API_METHODS = [
23
14
  'saveFormData',
24
15
  'updateFormData',
25
16
  'searchFormDataIds',
@@ -248,15 +239,8 @@ function openyidaCreateRouterBridge(utils) {
248
239
 
249
240
  function openyidaCreateUtilsBridge(context, yidaBridge) {
250
241
  var utils = context && context.utils;
251
- var navigation = createPageNavigation(window);
252
- var taskNavigation = createTaskCenterNavigation();
253
- var taskCounts = createTaskCenterLoader(createTaskCenterContract(), function() { return yidaBridge; });
254
242
  var methodNames = openyidaCollectUtilsMethodNames(utils);
255
243
  function invoke(methodName) {
256
- if (methodName === 'openPage') { return navigation.open.apply(null, Array.prototype.slice.call(arguments, 1)); }
257
- if (['taskCenterCount', 'taskCenterLink', 'openTaskCenter'].indexOf(methodName) !== -1) {
258
- return bridge[methodName].apply(null, Array.prototype.slice.call(arguments, 1));
259
- }
260
244
  if (!utils || typeof utils[methodName] !== 'function') {
261
245
  throw new Error('this.utils.' + methodName + ' is not available');
262
246
  }
@@ -267,8 +251,6 @@ function openyidaCreateUtilsBridge(context, yidaBridge) {
267
251
  apiGroups: {
268
252
  utility: OPENYIDA_UTIL_API_METHODS.slice(),
269
253
  router: ['push'],
270
- navigation: ['openPage', 'taskCenterLink', 'openTaskCenter'],
271
- tasks: ['taskCenterCount'],
272
254
  yida: OPENYIDA_KNOWN_YIDA_API_METHODS.slice()
273
255
  },
274
256
  knownMethods: OPENYIDA_UTIL_API_METHODS.slice(),
@@ -277,33 +259,9 @@ function openyidaCreateUtilsBridge(context, yidaBridge) {
277
259
  return !utils || typeof utils[methodName] !== 'function';
278
260
  }),
279
261
  invoke: invoke,
280
- openPage: navigation.open,
281
- taskCenterLink: function(options) {
282
- return taskNavigation.target(Object.assign({ baseUrl: window.location.origin }, options));
283
- },
284
- taskCenterCount: async function(options) {
285
- if (!options || typeof options !== 'object' || Array.isArray(options)
286
- || Object.keys(options).some(function(key) { return ['scope', 'appType', 'kind', 'filters'].indexOf(key) === -1; })) {
287
- var error = new Error('TASK_FILTER_INVALID: options');
288
- error.code = 'TASK_FILTER_INVALID';
289
- throw error;
290
- }
291
- var target = taskNavigation.target({ scope: options.scope, appType: options.appType, kind: options.kind, baseUrl: window.location.origin });
292
- var filters = createTaskCenterContract().normalizeFilters(target.kind, options.filters === undefined ? {} : options.filters, 'app-query');
293
- return taskCounts.count(target.kind, Object.assign({}, filters, { transport: 'app-query' },
294
- target.scope === 'corp' ? { scope: 'corp' } : { appType: target.appType }));
295
- },
296
- openTaskCenter: function(options) {
297
- var target = taskNavigation.target(Object.assign({ baseUrl: window.location.origin }, options));
298
- return navigation.open(target.pageUrl, { mode: target.mode });
299
- },
300
262
  router: openyidaCreateRouterBridge(utils),
301
263
  yida: yidaBridge
302
264
  };
303
- ['openPage', 'taskCenterLink', 'openTaskCenter', 'taskCenterCount'].forEach(function(methodName) {
304
- if (bridge.availableMethods.indexOf(methodName) === -1) { bridge.availableMethods.push(methodName); }
305
- });
306
- bridge.missingKnownMethods = bridge.missingKnownMethods.filter(function(name) { return name !== 'openPage'; });
307
265
  methodNames.forEach(function(methodName) {
308
266
  if (!bridge[methodName]) {
309
267
  bridge[methodName] = function() {
@@ -0,0 +1,190 @@
1
+ 'use strict';
2
+
3
+ const { CliError } = require('../core/cli-error');
4
+ const { t } = require('../core/i18n');
5
+
6
+ const DEFAULT_TIMEOUT_MS = 20000;
7
+ const MAX_RESPONSE_BYTES = 1024 * 1024;
8
+
9
+ function failure(code, message, stage, details = {}) {
10
+ return new CliError(t(`qwenwork.${message}`), {
11
+ code: `QWENWORK_${code}`,
12
+ details: { stage, ...details },
13
+ });
14
+ }
15
+
16
+ function pick(env, names) {
17
+ for (const name of names) {
18
+ const value = String(env[name] || '').trim();
19
+ if (value) {
20
+ return { name, value };
21
+ }
22
+ }
23
+ return null;
24
+ }
25
+
26
+ function normalizeBaseUrl(value) {
27
+ try {
28
+ const url = new URL(value);
29
+ const loopback = ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname);
30
+ if ((url.protocol !== 'https:' && !(url.protocol === 'http:' && loopback)) ||
31
+ url.username || url.password || url.search || url.hash) {
32
+ throw new Error('invalid_url');
33
+ }
34
+ return url.href.replace(/\/+$/, '');
35
+ } catch {
36
+ throw failure('INVALID_GATEWAY', 'invalid_url', 'resolve_credentials');
37
+ }
38
+ }
39
+
40
+ function isObject(value) {
41
+ return !!value && typeof value === 'object' && !Array.isArray(value);
42
+ }
43
+
44
+ // Do not include upstream bodies, headers or exception messages in diagnostics:
45
+ // an authentication endpoint may echo credentials even in an error response.
46
+ async function requestJson(method, url, accessToken, options, stage) {
47
+ const timeoutMs = options.timeoutMs === undefined ? DEFAULT_TIMEOUT_MS : options.timeoutMs;
48
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
49
+ throw failure('INVALID_TIMEOUT', 'request_failed', stage);
50
+ }
51
+ const controller = new globalThis.AbortController();
52
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
53
+ try {
54
+ const response = await globalThis.fetch(url, {
55
+ method,
56
+ headers: { accept: 'application/json', authorization: `Bearer ${accessToken}` },
57
+ redirect: 'manual',
58
+ signal: controller.signal,
59
+ });
60
+ if (!response.ok) {
61
+ if (response.body) {
62
+ await response.body.cancel();
63
+ }
64
+ throw failure('HTTP_ERROR', 'request_failed', stage, { status_code: response.status });
65
+ }
66
+ const chunks = [];
67
+ let size = 0;
68
+ if (response.body) {
69
+ const reader = response.body.getReader();
70
+ let next = await reader.read();
71
+ while (!next.done) {
72
+ const { value } = next;
73
+ size += value.byteLength;
74
+ if (size > MAX_RESPONSE_BYTES) {
75
+ await reader.cancel();
76
+ throw failure('RESPONSE_TOO_LARGE', 'invalid_response', stage);
77
+ }
78
+ chunks.push(Buffer.from(value));
79
+ next = await reader.read();
80
+ }
81
+ }
82
+ let payload;
83
+ try {
84
+ payload = JSON.parse(Buffer.concat(chunks).toString('utf8'));
85
+ } catch {
86
+ throw failure('INVALID_RESPONSE', 'invalid_response', stage);
87
+ }
88
+ if (!isObject(payload) || (payload.code !== undefined && payload.code !== 'ok')) {
89
+ throw failure('INVALID_RESPONSE', 'invalid_response', stage);
90
+ }
91
+ return payload;
92
+ } catch (error) {
93
+ if (error && error.isCliError) {
94
+ throw error;
95
+ }
96
+ throw controller.signal.aborted
97
+ ? failure('TIMEOUT', 'request_timeout', stage)
98
+ : failure('NETWORK_ERROR', 'request_failed', stage);
99
+ } finally {
100
+ clearTimeout(timer);
101
+ }
102
+ }
103
+
104
+ function decodeClaims(token) {
105
+ try {
106
+ const value = JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString('utf8'));
107
+ return isObject(value) ? value : {};
108
+ } catch {
109
+ return {};
110
+ }
111
+ }
112
+
113
+ /** Exchange a cloud Agent credential for an in-memory scoped user credential.
114
+ * This does not issue or persist an OpenYida session.
115
+ */
116
+ async function exchangeQwenWorkAgentToken(options = {}) {
117
+ const env = options.env || process.env;
118
+ const agent = pick(env, ['QWENWORK_AGENT_TOKEN', 'MULE_AGENT_TOKEN', 'AGENT_TOKEN']);
119
+ if (!agent || /[\r\n]/.test(agent.value)) {
120
+ throw failure('CREDENTIAL_MISSING', 'credential_missing', 'resolve_credentials');
121
+ }
122
+ const gateway = pick(env, ['QWENWORK_GATEWAY_URL', 'MULE_GATEWAY_URL']);
123
+ if (!gateway) {
124
+ throw failure('GATEWAY_MISSING', 'gateway_missing', 'resolve_credentials');
125
+ }
126
+ const baseUrl = normalizeBaseUrl(gateway.value);
127
+ const response = await requestJson('POST', `${baseUrl}/api/agent-gw/token/exchange`, agent.value, options, 'exchange');
128
+ // The current cloud endpoint returns {token, scope, ttl} without an envelope.
129
+ const payload = isObject(response.data) ? response.data : response;
130
+ if (typeof payload.token !== 'string' || !payload.token.trim() || /\s/.test(payload.token) ||
131
+ typeof payload.scope !== 'string' || !Number.isFinite(payload.ttl) || payload.ttl <= 0) {
132
+ throw failure('INVALID_RESPONSE', 'invalid_response', 'exchange');
133
+ }
134
+ const decoded = decodeClaims(payload.token);
135
+ if (typeof decoded.exp === 'number' && decoded.exp * 1000 <= Date.now()) {
136
+ throw failure('TOKEN_EXPIRED', 'expired_token', 'exchange');
137
+ }
138
+ return {
139
+ access_token: payload.token,
140
+ base_url: baseUrl,
141
+ credential_source: agent.name,
142
+ scope: payload.scope,
143
+ expires_in: payload.ttl,
144
+ expires_at: typeof decoded.exp === 'number' ? decoded.exp * 1000 : Date.now() + payload.ttl * 1000,
145
+ };
146
+ }
147
+
148
+ /** Query the credential's own origin; never infer a YiDA endpoint or organization. */
149
+ async function getQwenWorkUserInfo(credential, options = {}) {
150
+ const accessToken = credential && credential.access_token;
151
+ if (typeof accessToken !== 'string' || !accessToken || /[\r\n]/.test(accessToken)) {
152
+ throw failure('CREDENTIAL_MISSING', 'credential_missing', 'user_info');
153
+ }
154
+ const baseUrl = normalizeBaseUrl(credential.base_url);
155
+ const response = await requestJson('GET', `${baseUrl}/user/info`, accessToken, options, 'user_info');
156
+ if (response.code !== 'ok' || !isObject(response.data) ||
157
+ typeof response.data.user_id !== 'string' || !response.data.user_id.trim()) {
158
+ throw failure('INVALID_RESPONSE', 'invalid_response', 'user_info');
159
+ }
160
+ const env = options.env || process.env;
161
+ const decoded = decodeClaims(accessToken);
162
+ // Decoding is not local signature verification. The authenticated /user/info
163
+ // response establishes identity; these comparisons catch context drift.
164
+ for (const expected of [env.QWENWORK_USER_ID, env.MULE_USER_ID, decoded.user_id, decoded.sub]) {
165
+ if (expected && String(expected) !== response.data.user_id) {
166
+ throw failure('IDENTITY_MISMATCH', 'identity_mismatch', 'user_info');
167
+ }
168
+ }
169
+ const orgId = response.data.biz && response.data.biz.org_id;
170
+ if (orgId && decoded.biz_org_id && orgId !== decoded.biz_org_id) {
171
+ throw failure('IDENTITY_MISMATCH', 'identity_mismatch', 'user_info');
172
+ }
173
+ return response.data;
174
+ }
175
+
176
+ /** Safe composition for identity consumers: the returned object has no credential. */
177
+ async function getQwenWorkIdentity(options = {}) {
178
+ const credential = await exchangeQwenWorkAgentToken(options);
179
+ const profile = await getQwenWorkUserInfo(credential, options);
180
+ return {
181
+ base_url: credential.base_url,
182
+ credential_source: credential.credential_source,
183
+ scope: credential.scope,
184
+ expires_in: credential.expires_in,
185
+ expires_at: credential.expires_at,
186
+ profile,
187
+ };
188
+ }
189
+
190
+ module.exports = { exchangeQwenWorkAgentToken, getQwenWorkUserInfo, getQwenWorkIdentity };
@@ -0,0 +1,140 @@
1
+ 'use strict';
2
+
3
+ const { CliError } = require('../core/cli-error');
4
+ const { t } = require('../core/i18n');
5
+ const { DINGTALK_OAUTH_CLIENT_ID } = require('../core/env-manager');
6
+ const { saveTokenSession, loadTokenSession, isEnvTokenAuthMode } = require('./token-store');
7
+ const { readManagedContext } = require('../agent/managed-context');
8
+
9
+ const TICKET_ENV = 'OPENYIDA_QWENWORK_EXCHANGE_TICKET';
10
+ const TICKET_CONTEXT_ENV = 'OPENYIDA_QWENWORK_EXCHANGE_TICKET_CONTEXT';
11
+ const CONSUME_PATH = '/openapi/cli/v1/auth/qwenwork/desktop/consume';
12
+ const MAX_RESPONSE_BYTES = 65536;
13
+
14
+ function fail(code) {
15
+ return new CliError(t('qwenwork.desktop_login_failed'), { code: `QWENWORK_DESKTOP_${code}` });
16
+ }
17
+
18
+ function parseTicket(raw, context) {
19
+ try {
20
+ if (typeof raw !== 'string' || !raw || /\s/.test(raw) || raw.length > 32768 || typeof context !== 'string' || context.length > 8192) { throw new Error(); }
21
+ const value = JSON.parse(context);
22
+ if (!value || typeof value !== 'object' || Array.isArray(value) ||
23
+ !Number.isSafeInteger(value.expires_at) || value.expires_at <= Date.now() || value.expires_at > Date.now() + 300000 ||
24
+ typeof value.corp_id !== 'string' || !value.corp_id || typeof value.user_id !== 'string' || !value.user_id ||
25
+ typeof value.exchange_id !== 'string' || !value.exchange_id ||
26
+ 'access_token' in value || 'refresh_token' in value || 'accessToken' in value || 'refreshToken' in value) { throw new Error(); }
27
+ return { ...value, exchange_ticket: raw };
28
+ } catch {
29
+ throw fail('TICKET_MISSING_OR_INVALID');
30
+ }
31
+ }
32
+
33
+ function origin(value) {
34
+ try {
35
+ const url = new URL(value);
36
+ if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash || url.pathname !== '/') { throw new Error(); }
37
+ return url.origin;
38
+ } catch {
39
+ throw fail('ENDPOINT_INVALID');
40
+ }
41
+ }
42
+
43
+ async function requestJson(url, init, options) {
44
+ const controller = new AbortController();
45
+ const timer = setTimeout(() => controller.abort(), options.timeoutMs || 15000);
46
+ try {
47
+ const response = await (options.fetch || globalThis.fetch)(url, { ...init, redirect: 'error', signal: controller.signal });
48
+ if (!response.ok) {
49
+ if (response.body) { await response.body.cancel(); }
50
+ throw fail('REQUEST_REJECTED');
51
+ }
52
+ const reader = response.body && response.body.getReader();
53
+ if (!reader) { throw fail('RESPONSE_INVALID'); }
54
+ const chunks = [];
55
+ let bytes = 0;
56
+ try {
57
+ for (let next = await reader.read(); !next.done; next = await reader.read()) {
58
+ bytes += next.value.byteLength;
59
+ if (bytes > MAX_RESPONSE_BYTES) { await reader.cancel(); throw fail('RESPONSE_INVALID'); }
60
+ chunks.push(next.value);
61
+ }
62
+ } finally {
63
+ reader.releaseLock();
64
+ }
65
+ const value = JSON.parse(Buffer.concat(chunks).toString('utf8'));
66
+ if (!value || typeof value !== 'object' || Array.isArray(value)) { throw fail('RESPONSE_INVALID'); }
67
+ return value;
68
+ } catch (error) {
69
+ // 服务端和网络异常可能回显凭据,只透传本模块生成的固定错误。
70
+ if (error instanceof CliError && /^QWENWORK_DESKTOP_/.test(error.code)) { throw error; }
71
+ throw fail(controller.signal.aborted ? 'REQUEST_TIMED_OUT' : 'REQUEST_FAILED');
72
+ } finally {
73
+ clearTimeout(timer);
74
+ }
75
+ }
76
+
77
+ /** 专用免登入口:只消费环境中的一次性兑换票,不调用或重启普通 login。 */
78
+ async function loginQwenWork(options = {}) {
79
+ const env = options.env || process.env;
80
+ const raw = env[TICKET_ENV];
81
+ const context = env[TICKET_CONTEXT_ENV];
82
+ delete env[TICKET_ENV];
83
+ delete env[TICKET_CONTEXT_ENV];
84
+ if (isEnvTokenAuthMode(env) || readManagedContext(env)) { throw fail('MANAGED_AUTH_CONFLICT'); }
85
+ const ticket = parseTicket(raw, context);
86
+ const baseUrl = origin(ticket.base_url);
87
+ const expectedEndpoint = options.endpoint || env.OPENYIDA_ENDPOINT;
88
+ if (expectedEndpoint && origin(expectedEndpoint) !== baseUrl) { throw fail('ENDPOINT_MISMATCH'); }
89
+ if (options.corpId && options.corpId !== ticket.corp_id) { throw fail('IDENTITY_MISMATCH'); }
90
+ const startedAt = Date.now();
91
+ const value = await requestJson(`${baseUrl}${CONSUME_PATH}`, {
92
+ method: 'POST', headers: { accept: 'application/json', 'content-type': 'application/json', authorization: `Bearer ${ticket.exchange_ticket}` },
93
+ body: '{}',
94
+ }, options);
95
+ const expiresAt = startedAt + Number(value.expires_in) * 1000;
96
+ if (value.status !== 'ok' || value.auth_mode !== 'token' || value.can_auto_use !== true ||
97
+ value.token_type !== 'Bearer' || value.credential_source !== 'qwenwork_desktop' ||
98
+ 'refresh_token' in value || 'refreshToken' in value || 'exchange_ticket' in value ||
99
+ typeof value.access_token !== 'string' || !value.access_token || /\s/.test(value.access_token) || value.access_token.length > 32768 ||
100
+ !Number.isInteger(value.expires_in) || value.expires_in < 1 || value.expires_in > 1800 ||
101
+ value.corp_id !== ticket.corp_id || value.user_id !== ticket.user_id || value.exchange_id !== ticket.exchange_id ||
102
+ (value.base_url && origin(value.base_url) !== baseUrl)) { throw fail('CREDENTIAL_INVALID'); }
103
+ const verified = await requestJson(`${baseUrl}/openapi/cli/v1/auth/status`, {
104
+ method: 'GET', headers: { authorization: `Bearer ${value.access_token}`, accept: 'application/json' },
105
+ }, options);
106
+ if (verified.status !== 'ok' || verified.auth_mode !== 'token' || verified.can_auto_use !== true ||
107
+ verified.corp_id !== value.corp_id || verified.user_id !== value.user_id || expiresAt <= Date.now()) { throw fail('IDENTITY_MISMATCH'); }
108
+ const session = saveTokenSession({
109
+ auth_mode: 'token', token_type: 'Bearer', access_token: value.access_token,
110
+ expires_at: expiresAt, base_url: baseUrl, client_id: DINGTALK_OAUTH_CLIENT_ID,
111
+ corp_id: value.corp_id, user_id: value.user_id, credential_source: 'qwenwork_desktop',
112
+ }, options);
113
+ const restored = loadTokenSession(options);
114
+ if (!['user', 'project'].includes(session.persistence_scope) || !restored ||
115
+ restored.access_token !== value.access_token || restored.base_url !== baseUrl ||
116
+ restored.corp_id !== value.corp_id || restored.user_id !== value.user_id) { throw fail('PERSISTENCE_FAILED'); }
117
+ // 身份、环境和存储细节仅用于内部校验,不进入模型可见的登录结果。
118
+ return { ok: true, status: 'ok', can_auto_use: true };
119
+ }
120
+
121
+ async function run(args, options = {}) {
122
+ if (args.includes('--help') || args.includes('-h')) { console.log(t('qwenwork.desktop_login_usage')); return; }
123
+ const parsed = {};
124
+ const seen = new Set();
125
+ for (let index = 0; index < args.length; index++) {
126
+ const flag = args[index];
127
+ if (seen.has(flag)) { throw new CliError(t('qwenwork.desktop_login_usage'), { code: 'CLI_USAGE_ERROR', exitCode: 2 }); }
128
+ seen.add(flag);
129
+ if (['--json', '--quiet'].includes(flag)) { continue; }
130
+ const key = flag === '--corp-id' ? 'corpId' : flag === '--endpoint' ? 'endpoint' : null;
131
+ const value = args[++index];
132
+ if (!key || !value || value.startsWith('--')) { throw new CliError(t('qwenwork.desktop_login_usage'), { code: 'CLI_USAGE_ERROR', exitCode: 2 }); }
133
+ parsed[key] = value;
134
+ }
135
+ 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)); }
137
+ return result;
138
+ }
139
+
140
+ module.exports = { TICKET_ENV, TICKET_CONTEXT_ENV, loginQwenWork, run };
@@ -0,0 +1,37 @@
1
+ 'use strict';
2
+
3
+ const { CliError } = require('../core/cli-error');
4
+ const { t } = require('../core/i18n');
5
+ const { redactSensitive } = require('../core/redact');
6
+ const { getQwenWorkIdentity } = require('./qwenwork-client');
7
+
8
+ async function run(args, options = {}) {
9
+ if (args.includes('--help') || args.includes('-h')) {
10
+ console.log(t('qwenwork.usage'));
11
+ return;
12
+ }
13
+ if (args[0] !== 'user-info' || args.slice(1).some(arg => arg !== '--json')) {
14
+ throw new CliError(t('qwenwork.usage'), { code: 'CLI_USAGE_ERROR', exitCode: 2 });
15
+ }
16
+ const identity = await getQwenWorkIdentity(options);
17
+ const { profile, ...metadata } = identity;
18
+ const result = redactSensitive({
19
+ ok: true,
20
+ provider: 'qwenwork',
21
+ ...metadata,
22
+ user: {
23
+ user_id: profile.user_id,
24
+ nickname: profile.nickname,
25
+ username: profile.username,
26
+ email: profile.email,
27
+ is_biz: profile.is_biz,
28
+ biz_org_id: profile.biz && profile.biz.org_id,
29
+ biz_org_name: profile.biz && profile.biz.org_name,
30
+ },
31
+ yida_session_created: false,
32
+ });
33
+ console.log(JSON.stringify(result, null, 2));
34
+ return result;
35
+ }
36
+
37
+ module.exports = { run };
@@ -276,6 +276,7 @@ function normalizeTokenSession(session = {}) {
276
276
  );
277
277
  const normalized = {
278
278
  auth_mode: 'token',
279
+ credential_source: session.credential_source,
279
280
  token_type: session.token_type || session.tokenType || 'Bearer',
280
281
  access_token: session.access_token || session.accessToken,
281
282
  refresh_token: session.refresh_token || session.refreshToken,
@@ -3,7 +3,7 @@
3
3
  const crypto = require('crypto');
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
- const { version } = require('../../package.json');
6
+ const { version, openyidaLocalBeta: localBeta } = require('../../package.json');
7
7
  const { t } = require('./i18n');
8
8
  const { buildCommandManifest } = require('./command-manifest');
9
9
  const { buildEnvironmentSnapshot } = require('./env');
@@ -195,7 +195,7 @@ function forbiddenAliasBriefs(manifest) {
195
195
  }));
196
196
  }
197
197
 
198
- function buildAuthPath(login, env = process.env) {
198
+ function buildAuthPath(login, env = process.env, runtime = null) {
199
199
  const authSource = login.auth_source || 'token_session';
200
200
  const envTokenSession = authSource === 'env' || login.failure_reason === 'env_token_missing';
201
201
  const authPath = {
@@ -230,10 +230,30 @@ function buildAuthPath(login, env = process.env) {
230
230
  ? 'STOP_AND_REQUEST_ENV_TOKEN'
231
231
  : 'RUN_OPENYIDA_LOGIN_IF_USER_APPROVES',
232
232
  };
233
+ // 桌面换票仅由当前活跃宿主和发行配置共同启用,不影响其他宿主的认证路径。
234
+ if (localBeta?.desktopTicketLogin && runtime?.tool === 'qwenwork' && runtime?.subtype === 'qwenwork_desktop' && !envTokenSession) {
235
+ authPath.auth_runtime = 'qwenwork_desktop_exchange_ticket';
236
+ authPath.interactive_login_allowed = false;
237
+ authPath.missing_token_action = 'RUN_OPENYIDA_LOGIN_QWENWORK';
238
+ for (const key of ['auth_profile', 'corp_id', 'corp_name', 'user_id', 'user_name', 'store', 'persistence_scope', 'warning']) { delete authPath[key]; }
239
+ 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'];
243
+ }
233
244
  return authPath;
234
245
  }
235
246
 
236
247
  function buildInteractiveLogin(auth, runtime) {
248
+ if (auth.auth_runtime === 'qwenwork_desktop_exchange_ticket' && runtime.tool === 'qwenwork' && runtime.subtype === 'qwenwork_desktop') {
249
+ 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',
253
+ user_visible: false, success_action: 'continue_business_command_without_message',
254
+ reason: 'qwenwork_desktop_pre_beta', completion_signal: 'process_exit_and_final_json', playwright_required: false,
255
+ };
256
+ }
237
257
  const base = {
238
258
  suppress_flag: '--no-browser',
239
259
  suppress_env: 'OPENYIDA_NO_BROWSER',
@@ -293,9 +313,9 @@ function buildInteractiveLogin(auth, runtime) {
293
313
  }
294
314
 
295
315
  function buildBuilderPath(login, projectRoot, manifest, env = process.env, runtimeSnapshot = null) {
296
- const auth = buildAuthPath(login, env);
297
- const canTrustSummaryPreflight = auth.can_auto_use === true;
298
316
  const runtime = runtimeSnapshot || buildRuntimeSnapshot(projectRoot);
317
+ const auth = buildAuthPath(login, env, runtime);
318
+ const canTrustSummaryPreflight = auth.can_auto_use === true;
299
319
 
300
320
  return {
301
321
  schema_version: 1,
@@ -311,6 +331,13 @@ function buildBuilderPath(login, projectRoot, manifest, env = process.env, runti
311
331
  auth,
312
332
  runtime,
313
333
  interactive_login: buildInteractiveLogin(auth, runtime),
334
+ communication: runtime.tool === 'qwenwork' && runtime.subtype === 'qwenwork_desktop' ? {
335
+ preparation_visibility: 'silent',
336
+ completion_content: 'business_result_and_business_link_only',
337
+ disclose_authentication_identity_environment: false,
338
+ report_preparation_only_when_user_action_is_required: true,
339
+ bundled_skill_path: path.resolve(__dirname, '../../yida-skills'),
340
+ } : undefined,
314
341
  environment_check_simplification: {
315
342
  contract_role: 'machine_readable_builder_runtime_not_yida_agent_business_logic',
316
343
  minimal_probe_commands: [
@@ -432,6 +459,7 @@ function compactBuilderPath(builderPath) {
432
459
  auth,
433
460
  runtime: builderPath.runtime,
434
461
  interactive_login: builderPath.interactive_login,
462
+ communication: builderPath.communication,
435
463
  environment_check_simplification: {
436
464
  minimal_probe_commands: environment.minimal_probe_commands,
437
465
  can_skip_default_exploration_when_summary_ok: environment.can_skip_default_exploration_when_summary_ok,
@@ -601,7 +629,9 @@ function buildAgentCapabilitiesSummary() {
601
629
  schema_version: 1,
602
630
  name: 'openyida-agent-capabilities-summary',
603
631
  version,
604
- login: compactLogin(loginStatus),
632
+ login: builderPath.auth.auth_runtime === 'qwenwork_desktop_exchange_ticket'
633
+ ? { status: loginStatus.status, can_auto_use: loginStatus.can_auto_use === true }
634
+ : compactLogin(loginStatus),
605
635
  workdir: projectRoot,
606
636
  workdir_exists: !!projectResolution.exists,
607
637
  workdir_source: projectResolution.source,