openyida 2026.10.21 → 2026.10.22-snapshot.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.
- package/README.md +24 -3
- package/bin/yida.js +4 -0
- package/lib/app/canvas-compile.js +8 -0
- package/lib/app/page-linter.js +10 -0
- package/lib/app/responsive-layout-guard.js +82 -0
- package/lib/auth/qwenwork-client.js +190 -0
- package/lib/auth/qwenwork.js +37 -0
- package/lib/core/command-manifest.js +18 -10
- package/lib/core/locales/en.js +63 -4
- package/lib/core/locales/zh.js +62 -3
- package/lib/core/query-data.js +40 -2
- package/lib/core/sample.js +3 -0
- package/lib/feedback/feedback.js +2 -1
- package/lib/feedback/github-setup.js +97 -0
- package/lib/feedback/github.js +17 -8
- package/lib/feedback/report.js +35 -15
- package/lib/integration/integration-triggers.js +8 -3
- package/lib/process/configure-process.js +8 -2
- package/lib/process/process-tasks.js +175 -0
- package/lib/process/services/process-actor-config.js +3 -0
- package/lib/process/services/process-compiler.js +15 -0
- package/lib/process/services/process-dynamic-actors.js +165 -0
- package/lib/samples/openyida-scaffold/canvas-nav/mixed.jsx +1 -1
- package/lib/samples/openyida-scaffold/canvas-nav/shared.jsx +1 -1
- package/lib/samples/openyida-scaffold/canvas-nav/side.jsx +1 -1
- package/lib/samples/openyida-scaffold/canvas-nav/sidebar.jsx +36 -24
- package/lib/samples/openyida-scaffold/canvas-nav/top.jsx +1 -1
- package/lib/samples/openyida-scaffold/canvas-responsive-layout.jsx +104 -0
- package/package.json +1 -1
- package/scripts/postinstall.js +1 -0
- package/yida-skills/SKILL.md +5 -2
- package/yida-skills/skills/yida-canvas-custom-page/SKILL.md +5 -1
- package/yida-skills/skills/yida-custom-page/SKILL.md +6 -2
- package/yida-skills/skills/yida-data-management/SKILL.md +24 -3
- package/yida-skills/skills/yida-data-management/references/process-task-transfer.md +53 -0
- package/yida-skills/skills/yida-density/SKILL.md +6 -1
- package/yida-skills/skills/yida-design/SKILL.md +1 -1
- package/yida-skills/skills/yida-design/references/responsive-layout.md +69 -22
- package/yida-skills/skills/yida-design/references/task-driven-visual-design.md +2 -0
- package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/SKILL.md +1 -0
- package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/references/build-plan-compact-schema.md +1 -1
- package/yida-skills/skills/yida-design/workflow/output-design.md +2 -2
- package/yida-skills/skills/yida-design/workflow/step-1-read-brief.md +2 -0
- package/yida-skills/skills/yida-design/workflow/step-4-wireframe-interaction.md +10 -10
- package/yida-skills/skills/yida-feedback/SKILL.md +22 -5
- package/yida-skills/skills/yida-feedback/references/github-contribution.md +14 -8
- package/yida-skills/skills/yida-feedback/references/github-setup.md +74 -0
- package/yida-skills/skills/yida-feedback/references/report-input.md +10 -2
- package/yida-skills/skills/yida-feedback/references/user-interaction.md +19 -2
- package/yida-skills/skills/yida-flow-node-config/SKILL.md +1 -1
- package/yida-skills/skills/yida-flow-node-config/references/actors.md +4 -0
- package/yida-skills/skills/yida-flow-node-config/references/dynamic-actors.md +84 -0
- package/yida-skills/skills/yida-integration/SKILL.md +1 -0
- package/yida-skills/skills/yida-integration/references/triggers.md +14 -1
- package/yida-skills/skills/yida-login/SKILL.md +12 -0
- package/yida-skills/skills/yida-prd/SKILL.md +1 -0
- 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
|
-
|
|
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
|
|
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); }
|
package/lib/app/page-linter.js
CHANGED
|
@@ -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
|
-
|
|
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',
|