openyida 2026.10.21 → 2026.10.22-beta.0

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 (57) hide show
  1. package/README.md +24 -3
  2. package/bin/yida.js +4 -0
  3. package/lib/app/canvas-compile.js +8 -0
  4. package/lib/app/page-linter.js +10 -0
  5. package/lib/app/responsive-layout-guard.js +82 -0
  6. package/lib/auth/qwenwork-client.js +190 -0
  7. package/lib/auth/qwenwork.js +37 -0
  8. package/lib/core/command-manifest.js +18 -10
  9. package/lib/core/locales/en.js +63 -4
  10. package/lib/core/locales/zh.js +62 -3
  11. package/lib/core/query-data.js +40 -2
  12. package/lib/core/sample.js +3 -0
  13. package/lib/feedback/feedback.js +2 -1
  14. package/lib/feedback/github-setup.js +97 -0
  15. package/lib/feedback/github.js +17 -8
  16. package/lib/feedback/report.js +35 -15
  17. package/lib/integration/integration-triggers.js +8 -3
  18. package/lib/process/configure-process.js +8 -2
  19. package/lib/process/process-tasks.js +175 -0
  20. package/lib/process/services/process-actor-config.js +3 -0
  21. package/lib/process/services/process-compiler.js +15 -0
  22. package/lib/process/services/process-dynamic-actors.js +165 -0
  23. package/lib/samples/openyida-scaffold/canvas-nav/mixed.jsx +1 -1
  24. package/lib/samples/openyida-scaffold/canvas-nav/shared.jsx +1 -1
  25. package/lib/samples/openyida-scaffold/canvas-nav/side.jsx +1 -1
  26. package/lib/samples/openyida-scaffold/canvas-nav/sidebar.jsx +36 -24
  27. package/lib/samples/openyida-scaffold/canvas-nav/top.jsx +1 -1
  28. package/lib/samples/openyida-scaffold/canvas-responsive-layout.jsx +104 -0
  29. package/package.json +1 -1
  30. package/scripts/postinstall.js +1 -0
  31. package/yida-skills/SKILL.md +5 -2
  32. package/yida-skills/skills/yida-canvas-custom-page/SKILL.md +5 -1
  33. package/yida-skills/skills/yida-custom-page/SKILL.md +6 -2
  34. package/yida-skills/skills/yida-data-management/SKILL.md +24 -3
  35. package/yida-skills/skills/yida-data-management/references/process-task-transfer.md +53 -0
  36. package/yida-skills/skills/yida-density/SKILL.md +6 -1
  37. package/yida-skills/skills/yida-design/SKILL.md +1 -1
  38. package/yida-skills/skills/yida-design/references/responsive-layout.md +69 -22
  39. package/yida-skills/skills/yida-design/references/task-driven-visual-design.md +2 -0
  40. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/SKILL.md +1 -0
  41. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/references/build-plan-compact-schema.md +1 -1
  42. package/yida-skills/skills/yida-design/workflow/output-design.md +2 -2
  43. package/yida-skills/skills/yida-design/workflow/step-1-read-brief.md +2 -0
  44. package/yida-skills/skills/yida-design/workflow/step-4-wireframe-interaction.md +10 -10
  45. package/yida-skills/skills/yida-feedback/SKILL.md +22 -5
  46. package/yida-skills/skills/yida-feedback/references/github-contribution.md +14 -8
  47. package/yida-skills/skills/yida-feedback/references/github-setup.md +74 -0
  48. package/yida-skills/skills/yida-feedback/references/report-input.md +10 -2
  49. package/yida-skills/skills/yida-feedback/references/user-interaction.md +19 -2
  50. package/yida-skills/skills/yida-flow-node-config/SKILL.md +1 -1
  51. package/yida-skills/skills/yida-flow-node-config/references/actors.md +4 -0
  52. package/yida-skills/skills/yida-flow-node-config/references/dynamic-actors.md +84 -0
  53. package/yida-skills/skills/yida-integration/SKILL.md +1 -0
  54. package/yida-skills/skills/yida-integration/references/triggers.md +14 -1
  55. package/yida-skills/skills/yida-login/SKILL.md +12 -0
  56. package/yida-skills/skills/yida-prd/SKILL.md +1 -0
  57. package/yida-skills/skills-index.json +13 -4
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
@@ -446,7 +465,9 @@ openyida feedback report --input summary.json --output-dir feedback-draft-001
446
465
 
447
466
  Your AI prepares the summary from observed evidence and selected reproduction screenshots. PRs include before/after test comparisons. Before anything is published, review the final text and images, confirm sensitive information, and remove anything you do not want to share. See the [report input format](yida-skills/skills/yida-feedback/references/report-input.md). Review business information before sharing. With a GitHub account, authorize your AI to submit an Issue or contribute a verified public OpenYida fix through your personal Fork. Without an account, create a GitHub account first. If that is not possible, share the report in the AI开发者群 (AI developer group).
448
467
 
449
- When you choose to submit an OpenYida Issue, the feedback Skill can check your signed-in GitHub account with `openyida feedback github status --json`. Star and a personal Fork are optional: `feedback github star --confirm` and `feedback github fork --confirm` run only after you choose them. Existing actions are reused. This check is separate from installation and automatic updates; neither action is required to submit an Issue.
468
+ To configure a submission tool, your AI can first run `openyida feedback github setup --json` to check gh and the current account. After you approve the installation plan, it can run `openyida feedback github setup --install --confirm --json`, using existing Homebrew on macOS/Linux or WinGet on Windows. Without a supported manager, official instructions or the web remain available. Browser authorization is a separate step performed by you; installation does not log you in, request passwords or tokens, or replace public-content approval. Resume the original draft after setup. See the [account and tool guide](yida-skills/skills/yida-feedback/references/github-setup.md).
469
+
470
+ When preparing a PR that needs a personal Fork, or when you ask how to support the project, the feedback Skill can inspect account support state with `openyida feedback github status --json`. Star and a personal Fork are optional: `feedback github star --confirm` and `feedback github fork --confirm` run only after you choose them. Existing actions are reused. This check is separate from installation and automatic updates; neither action is required to submit an Issue.
450
471
 
451
472
  <!-- OPENYIDA_COMMANDS_START -->
452
473
  <!-- This section is generated by `npm run docs:commands`. Do not edit command rows by hand. -->
@@ -457,7 +478,7 @@ When you choose to submit an OpenYida Issue, the feedback Skill can check your s
457
478
  |---------|-------------|
458
479
  | `openyida login [target-url] [--env <name>\|--intl\|--overseas\|--global\|--yidaapps\|--alibaba] [--client-id <clientId>] [--endpoint <url>] [--no-browser]` | Login with OAuth token mode |
459
480
  | `openyida logout` | Logout / unbind current project auth |
460
- | `openyida auth <status\|login\|refresh\|logout\|profiles\|profile switch>` | Token login state and profile management |
481
+ | `openyida auth <status\|login\|refresh\|logout\|profiles\|profile switch\|qwenwork user-info> [--json]` | Token login state and profile management |
461
482
  | `openyida org <list\|switch> [--json] [--corp-id <corpId>]` | Organization management (list / switch existing profiles first) |
462
483
  | `openyida env [--json\|setup\|list\|show\|switch\|add\|remove] [options]` | Detect AI tool environment & token login state |
463
484
 
@@ -530,7 +551,7 @@ When you choose to submit an OpenYida Issue, the feedback Skill can check your s
530
551
 
531
552
  | Command | Description |
532
553
  |---------|-------------|
533
- | `openyida data <query\|get\|create\|update> <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) |
554
+ | `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) |
534
555
  | `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.) |
535
556
  | `openyida basic-info <overview\|commodity\|grant\|capacity\|quota\|abs-path\|dataflow\|i18n\|domain>` | Query organization basic info, capacity, quotas, and domain settings |
536
557
  | `openyida read-dingtalk-doc <docUrl> [--output <file>] [--json]` | Fetch Markdown content from a DingTalk document |
package/bin/yida.js CHANGED
@@ -714,6 +714,10 @@ async function main() {
714
714
 
715
715
  case 'auth': {
716
716
  const subCommand = args[0];
717
+ if (subCommand === 'qwenwork') {
718
+ await require('../lib/auth/qwenwork').run(args.slice(1));
719
+ break;
720
+ }
717
721
  const authArgs = applyLoginEnvironmentFlags(args.slice(1), { inferTargetUrl: true });
718
722
  const { getAuthStatus } = require('../lib/core/utils');
719
723
  const { tokenLogin, tokenLogout, tokenRefresh } = require('../lib/auth/token-auth');
@@ -55,6 +55,7 @@ const { assertCanvasThemeStructure } = require('./canvas-theme-guard');
55
55
  const { assertCanvasIconExports } = require('./canvas-icon-guard');
56
56
  const { assertCanvasNavigationStructure } = require('./canvas-navigation-guard');
57
57
  const { assertCanvasNavigationPaths } = require('./canvas-path-guard');
58
+ const { findResponsiveLayoutIssues } = require('./responsive-layout-guard');
58
59
 
59
60
  /**
60
61
  * 依赖 → window 别名白名单,用于把 import 改写到运行时全局对象。
@@ -977,6 +978,13 @@ function compileCanvasLocal(source, options = {}) {
977
978
  assertCanvasNavigationPaths(source, options);
978
979
  assertInlineCssStructure(source, options);
979
980
  const warnings = [];
981
+ findResponsiveLayoutIssues(source).forEach((issue) => {
982
+ warnings.push({
983
+ line: issue.line,
984
+ rule: issue.rule,
985
+ message: t(issue.messageKey, ...(issue.params || [])),
986
+ });
987
+ });
980
988
  assertCanvasThemeStructure(source, { ...options, onThemeWarning(message, details) {
981
989
  warnings.push({ ...details, message });
982
990
  if (options.onThemeWarning) { options.onThemeWarning(message, details); }
@@ -9,6 +9,7 @@ const {
9
9
  const { findBareCjkJsxTextIdentifiers } = require('./jsx-text-guard');
10
10
  const { findDirectFormOpenIssues } = require('./form-open-guard');
11
11
  const { findFormDetailLinkIssues } = require('./form-detail-link-guard');
12
+ const { findResponsiveLayoutIssues } = require('./responsive-layout-guard');
12
13
  const Babel = require('@babel/standalone');
13
14
 
14
15
  const THEN_CALLBACK_LINE_LIMIT = 50;
@@ -1051,6 +1052,15 @@ function lintYidaSource(sourceCode, filePath) {
1051
1052
  findFormDetailLinkIssues(sourceCode, { parserOptions: PARSER_OPTIONS }).forEach((issue) => {
1052
1053
  pushIssue(errors, issue.line, 'form-detail-link', t('publish.lint_form_detail_link'), disableMap);
1053
1054
  });
1055
+ findResponsiveLayoutIssues(sourceCode).forEach((issue) => {
1056
+ pushIssue(
1057
+ warnings,
1058
+ issue.line,
1059
+ issue.rule,
1060
+ t(issue.messageKey, ...(issue.params || [])),
1061
+ disableMap
1062
+ );
1063
+ });
1054
1064
  return { errors, warnings };
1055
1065
  }
1056
1066
 
@@ -0,0 +1,82 @@
1
+ 'use strict';
2
+
3
+ const FIXED_WIDTH_THRESHOLD = 480;
4
+
5
+ function getLine(source, index) {
6
+ return source.slice(0, index).split('\n').length;
7
+ }
8
+
9
+ function lineHasOptOut(source, index, marker) {
10
+ const lineStart = source.lastIndexOf('\n', index) + 1;
11
+ const lineEnd = source.indexOf('\n', index);
12
+ const line = source.slice(lineStart, lineEnd < 0 ? source.length : lineEnd);
13
+ return line.includes(marker);
14
+ }
15
+
16
+ /**
17
+ * Find high-confidence responsive layout risks shared by Canvas and legacy JSX pages.
18
+ * The checks stay advisory because viewport-owned overlays and fixed-size visualizations
19
+ * can be intentional; authors can add the documented line marker after reviewing one.
20
+ * @param {string} source
21
+ * @returns {Array<{line: number, rule: string, messageKey: string, params?: Array<unknown>}>>}
22
+ */
23
+ function findResponsiveLayoutIssues(source) {
24
+ const text = String(source || '');
25
+ const issues = [];
26
+
27
+ const observerMatch = /new\s+(?:window\.)?ResizeObserver\s*\(/.exec(text);
28
+ if (observerMatch && !/\.\s*(?:disconnect|unobserve)\s*\(/.test(text)) {
29
+ issues.push({
30
+ line: getLine(text, observerMatch.index),
31
+ rule: 'resize-observer-cleanup',
32
+ messageKey: 'publish.lint_resize_observer_cleanup',
33
+ });
34
+ }
35
+
36
+ const viewportMatch = /(?:window\s*\.\s*innerWidth|matchMedia\s*\(\s*['"`]\s*\([^'"`]*(?:min|max)-width)/.exec(text);
37
+ const hasLayoutBranch = /(?:@media|grid-template-columns|gridTemplateColumns|flex-direction|flexDirection|\bmobile\b|\bisMobile\b)/.test(text);
38
+ const hasContainerSignal = /(?:@container|container-type|containerType|ResizeObserver|getBoundingClientRect\s*\(\s*\)\s*\.\s*width|\.\s*clientWidth\b)/.test(text);
39
+ if (
40
+ viewportMatch && hasLayoutBranch && !hasContainerSignal &&
41
+ !text.includes('openyida-responsive-viewport-ok')
42
+ ) {
43
+ issues.push({
44
+ line: getLine(text, viewportMatch.index),
45
+ rule: 'responsive-viewport-only',
46
+ messageKey: 'publish.lint_responsive_viewport_only',
47
+ });
48
+ }
49
+
50
+ const fixedWidthPattern = /(?:^|[\s{;,])width\s*:\s*['"]?(\d{3,})px?['"]?(?=\s*[,;}])/gm;
51
+ let fixedWidthMatch;
52
+ while ((fixedWidthMatch = fixedWidthPattern.exec(text)) !== null) {
53
+ const width = Number(fixedWidthMatch[1]);
54
+ if (
55
+ width >= FIXED_WIDTH_THRESHOLD &&
56
+ !lineHasOptOut(text, fixedWidthMatch.index, 'openyida-responsive-fixed-width-ok')
57
+ ) {
58
+ issues.push({
59
+ line: getLine(text, fixedWidthMatch.index),
60
+ rule: 'responsive-fixed-width',
61
+ messageKey: 'publish.lint_responsive_fixed_width',
62
+ params: [width],
63
+ });
64
+ }
65
+ }
66
+
67
+ const scaleMatch = /(?:transform\s*:\s*[^;\n}]*\bscale\s*\(|\bzoom\s*:)/.exec(text);
68
+ if (scaleMatch && !text.includes('openyida-responsive-scale-ok')) {
69
+ issues.push({
70
+ line: getLine(text, scaleMatch.index),
71
+ rule: 'responsive-page-scale',
72
+ messageKey: 'publish.lint_responsive_page_scale',
73
+ });
74
+ }
75
+
76
+ return issues;
77
+ }
78
+
79
+ module.exports = {
80
+ FIXED_WIDTH_THRESHOLD,
81
+ findResponsiveLayoutIssues,
82
+ };
@@ -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,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 };
@@ -574,7 +574,7 @@ const COMMAND_SIDE_EFFECTS = new Map([
574
574
  mutates_yida: false,
575
575
  mutates_local: true,
576
576
  read_actions: ['status', 'profiles', 'profile list'],
577
- mutating_actions: ['login', 'refresh', 'logout', 'profile switch'],
577
+ mutating_actions: ['login', 'refresh', 'logout', 'profile switch', 'qwenwork user-info'],
578
578
  })],
579
579
  ['batch', sideEffect('mixed', {
580
580
  mutates_yida: true,
@@ -612,7 +612,7 @@ const COMMAND_SIDE_EFFECTS = new Map([
612
612
  mutates_yida: true,
613
613
  mutates_local: false,
614
614
  read_actions: ['query', 'get'],
615
- mutating_actions: ['create', 'update', 'delete'],
615
+ mutating_actions: ['create', 'update', 'delete', 'transfer'],
616
616
  })],
617
617
  ['db-seq-fix', sideEffect('mixed', {
618
618
  mutates_yida: true,
@@ -648,7 +648,7 @@ const COMMAND_SIDE_EFFECTS = new Map([
648
648
  mutates_yida: true,
649
649
  mutates_local: true,
650
650
  read_actions: ['url', 'status', 'github status'],
651
- mutating_actions: ['setup', 'dismiss', 'report', 'github star', 'github fork'],
651
+ mutating_actions: ['setup', 'dismiss', 'report', 'github setup --install --confirm', 'github star', 'github fork'],
652
652
  })],
653
653
  ['i18n', sideEffect('mixed', {
654
654
  mutates_yida: true,
@@ -903,7 +903,7 @@ const COMMAND_PERMISSIONS = new Map([
903
903
  ...permissionEntries([
904
904
  'auth',
905
905
  ], actionDependentPermission({
906
- preauthorized_actions: ['login', 'refresh', 'logout', 'profile switch'],
906
+ preauthorized_actions: ['login', 'refresh', 'logout', 'profile switch', 'qwenwork user-info'],
907
907
  })),
908
908
 
909
909
  ...permissionEntries([
@@ -950,7 +950,7 @@ const COMMAND_PERMISSIONS = new Map([
950
950
  ...permissionEntries([
951
951
  'data',
952
952
  ], actionDependentPermission({
953
- preauthorized_actions: ['create', 'update'],
953
+ preauthorized_actions: ['create', 'update', 'transfer'],
954
954
  ask_actions: ['delete'],
955
955
  })),
956
956
 
@@ -1143,7 +1143,7 @@ const COMMAND_GROUPS = [
1143
1143
  output: 'json',
1144
1144
  }),
1145
1145
  command('logout', ['logout'], 'logout', 'help.cmd_logout', { managedRun: { scope: 'denied', reason: 'scope_not_audited' }, requiresLogin: false }),
1146
- command('auth', ['auth'], 'auth <status|login|refresh|logout|profiles|profile switch>', 'help.cmd_auth', { managedRun: { scope: 'actions', actions: { status: { scope: 'local', allowedFlags: ['--json', '--quiet'] } } }, requiresLogin: false }),
1146
+ 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 }),
1147
1147
  command('org', ['org'], 'org <list|switch> [--json] [--corp-id <corpId>]', 'help.cmd_org', { managedRun: { scope: 'denied', reason: 'scope_not_audited' } }),
1148
1148
  command('env', ['env'], 'env [--json|setup|list|show|switch|add|remove] [options]', 'help.cmd_env', { managedRun: { scope: 'local', allowedFlags: ['--json', '--quiet'] },
1149
1149
  requiresLogin: false,
@@ -1748,12 +1748,13 @@ const COMMAND_GROUPS = [
1748
1748
  id: 'data',
1749
1749
  titleKey: 'help.group_data',
1750
1750
  commands: [
1751
- command('data', ['data'], 'data <query|get|create|update> <resource> ... | delete form <appType> <formUuid> --inst-id <id> --expect-form-name <name> --expect-form-type receipt --confirm [--json]', 'help.cmd_data', {
1751
+ command('data', ['data'], 'data <query|get|create|update|transfer> <resource> ... | delete form <appType> <formUuid> --inst-id <id> --expect-form-name <name> --expect-form-type receipt --confirm [--json]', 'help.cmd_data', {
1752
1752
  managedRun: { scope: 'actions', actions: {
1753
1753
  query: { scope: 'actions', actions: {
1754
1754
  form: { scope: 'app', appPosition: 2 }, subform: { scope: 'app', appPosition: 2 },
1755
1755
  process: { scope: 'app', appPosition: 2 }, 'operation-records': { scope: 'app', appPosition: 2 },
1756
1756
  tasks: { scope: 'app', appPosition: 2 },
1757
+ 'process-tasks': { scope: 'app', appPosition: 2 },
1757
1758
  } },
1758
1759
  get: { scope: 'actions', actions: {
1759
1760
  form: { scope: 'app', appPosition: 2 }, subform: { scope: 'app', appPosition: 2 },
@@ -1773,6 +1774,9 @@ const COMMAND_GROUPS = [
1773
1774
  execute: { scope: 'actions', actions: {
1774
1775
  task: { scope: 'app', appPosition: 2, mutation: { operation: 'execute_task', formUuidOption: '--form-uuid' } },
1775
1776
  } },
1777
+ transfer: { scope: 'actions', actions: {
1778
+ task: { scope: 'app', appPosition: 2, mutation: { operation: 'execute_task', formUuidOption: '--form-uuid' } },
1779
+ } },
1776
1780
  } },
1777
1781
  output: 'json',
1778
1782
  noteKeys: ['help.data_notes', 'query_data.approver_query_help'],
@@ -1787,6 +1791,8 @@ const COMMAND_GROUPS = [
1787
1791
  'openyida data query form APP_XXX FORM_XXX --page 1 --size 20 --search-json \'[{"key":"dateField_due","componentName":"DateField","type":"DOUBLE","operator":"is","value":""}]\'',
1788
1792
  'openyida data create form APP_XXX FORM_XXX --expect-form-name 客户 --expect-form-type receipt --data-json \'{"textField_name":"客户 A","dateField_followUp":1787932800000}\'',
1789
1793
  'openyida data delete form APP_XXX FORM_XXX --inst-id FINST_XXX --expect-form-name 客户 --expect-form-type receipt --confirm --json',
1794
+ 'openyida data query process-tasks APP_XXX --process-inst-id PINST_XXX --json',
1795
+ 'openyida data transfer task APP_XXX --process-inst-id PINST_XXX --form-uuid FORM_XXX --to-user-id USER_ID --expect-form-name "Approval form" --expect-form-type process --json',
1790
1796
  ],
1791
1797
  }),
1792
1798
  command('task-center', ['task-center'], 'task-center <todo|created|done|cc|proxy|capabilities|scaffold|options|inspect|count> [--spec <file.json> --output <file.canvas.jsx>] [options]', 'help.cmd_task_center', {
@@ -2052,11 +2058,13 @@ const COMMAND_GROUPS = [
2052
2058
  requiresLogin: false,
2053
2059
  }),
2054
2060
  command('feedback', ['feedback'], 'feedback <report|github|setup|url|dismiss|status> [options]', 'help.cmd_feedback', { managedRun: { scope: 'actions', actions: { report: { scope: 'local' } } },
2055
- permission: actionDependentPermission({ preauthorized_actions: ['report', 'url', 'status', 'dismiss', 'github status'], ask_actions: ['setup', 'github star', 'github fork'] }),
2061
+ permission: actionDependentPermission({ preauthorized_actions: ['report', 'url', 'status', 'dismiss', 'github status'], ask_actions: ['setup', 'github setup', 'github setup --install --confirm', 'github star', 'github fork'] }),
2056
2062
  requiresLogin: false,
2057
2063
  output: 'text|json',
2058
- examples: ['openyida feedback report --input summary.json --output-dir feedback-draft-001'],
2059
- notes: ['report prepares local review drafts only; no login, source checkout, update check or network submission. github status [--json] checks the authenticated github.com user, Star and personal Fork through an existing gh installation; it performs no writes. github star|fork --confirm [--json] requires the user to explicitly choose that account action. Missing gh/login, unknown status and declined actions never block Issue submission. Already applied actions are reused; writes are single-attempt with readback. No auto-update/install hook, Issue submission, clone or git remote modification. Managed runs permit only offline report preparation.'],
2064
+ examples: ['openyida feedback report --input summary.json --output-dir feedback-draft-001', 'openyida feedback github setup --json', 'openyida feedback github setup --install --confirm --json'],
2065
+ // Keep setup conservative in permission metadata: --install changes the local machine.
2066
+ // The parser distinguishes read-only setup from a separately confirmed installation.
2067
+ notes: ['github setup [--json] detects gh and verifies the current account without installing or logging in. github setup --install --confirm performs one user-approved installation via existing Homebrew (macOS/Linux) or WinGet (Windows), then checks gh again. No manager bootstrap, source changes, automatic retry, upgrade, or automatic login; unsupported environments use official instructions or the web. Browser authorization requires separate consent and public submission still requires content review. report prepares local review drafts only; no login, source checkout, update check or network submission. github status [--json] checks the authenticated github.com user, Star and personal Fork through an existing gh installation; it performs no writes. github star|fork --confirm [--json] requires the user to explicitly choose that account action. Missing gh/login, unknown status and declined actions never block Issue submission. Already applied actions are reused; writes are single-attempt with readback. No auto-update/install hook, Issue submission, clone or git remote modification. Managed runs permit only offline report preparation.'],
2060
2068
  }),
2061
2069
  command('batch', ['batch'], 'batch <file>|--commands "cmd1 ; cmd2" [--stop-on-error] [--json]', 'help.cmd_batch', { managedRun: { scope: 'denied', reason: 'scope_not_audited' },
2062
2070
  output: 'text|json',