openyida 2026.10.21 → 2026.10.22
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 +4 -2
- 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/core/command-manifest.js +15 -7
- package/lib/core/locales/en.js +50 -2
- package/lib/core/locales/zh.js +50 -2
- 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 +3 -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-prd/SKILL.md +1 -0
- package/yida-skills/skills-index.json +13 -4
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { spawnSync } = require('child_process');
|
|
4
|
+
const { t } = require('../core/i18n');
|
|
5
|
+
|
|
6
|
+
const INSTALL_URL = 'https://github.com/cli/cli#installation';
|
|
7
|
+
const LOGIN_COMMAND = 'gh auth login --hostname github.com --web --skip-ssh-key';
|
|
8
|
+
|
|
9
|
+
/** Fixed argv only. Capture diagnostics privately, including package-manager failures. */
|
|
10
|
+
function invoke(binary, args, options, installing = false) {
|
|
11
|
+
try {
|
|
12
|
+
return (options.spawnSyncFn || spawnSync)(binary, args, {
|
|
13
|
+
shell: false, encoding: 'utf8', timeout: installing ? 300000 : 15000,
|
|
14
|
+
maxBuffer: 1024 * 1024, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true,
|
|
15
|
+
env: { ...(options.env || process.env), GH_HOST: 'github.com', GH_PROMPT_DISABLED: '1',
|
|
16
|
+
GIT_TERMINAL_PROMPT: '0', HOMEBREW_NO_AUTO_UPDATE: '1', HOMEBREW_NO_INSTALL_CLEANUP: '1',
|
|
17
|
+
HOMEBREW_NO_INSTALL_UPGRADE: '1', HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: '1', HOMEBREW_NO_SUDO: '1',
|
|
18
|
+
...(installing ? { HOMEBREW_NO_ASK: '1' } : {}) },
|
|
19
|
+
});
|
|
20
|
+
} catch { return { status: null }; }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function toolState(options) {
|
|
24
|
+
const result = invoke('gh', ['--version'], options);
|
|
25
|
+
if (result.error?.code === 'ENOENT') { return { state: 'missing', version: null }; }
|
|
26
|
+
const version = String(result.stdout || '').match(/^gh version (\d+\.\d+\.\d+)\b/)?.[1];
|
|
27
|
+
return result.status === 0 && !result.error && version
|
|
28
|
+
? { state: 'available', version } : { state: 'unverified', version: null };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function installPlan(options) {
|
|
32
|
+
const platform = options.platform || process.platform;
|
|
33
|
+
const binary = platform === 'win32' ? 'winget' : ['darwin', 'linux'].includes(platform) ? 'brew' : null;
|
|
34
|
+
if (!binary) { return { available: false, reason: 'unsupported_platform', url: INSTALL_URL }; }
|
|
35
|
+
const probe = invoke(binary, ['--version'], options);
|
|
36
|
+
if (probe.error || probe.status !== 0) {
|
|
37
|
+
return { available: false, reason: probe.error?.code === 'ENOENT' ? 'manager_missing' : 'manager_unverified', url: INSTALL_URL };
|
|
38
|
+
}
|
|
39
|
+
const args = binary === 'brew' ? ['install', 'homebrew/core/gh']
|
|
40
|
+
: ['install', '--id', 'GitHub.cli', '--exact', '--source', 'winget', '--no-upgrade', '--disable-interactivity',
|
|
41
|
+
'--accept-source-agreements', '--accept-package-agreements'];
|
|
42
|
+
return { available: true, binary, args, url: INSTALL_URL, requiresConsent: true };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function accountState(options) {
|
|
46
|
+
const result = invoke('gh', ['api', '--hostname', 'github.com', '--method', 'GET', 'user'], options);
|
|
47
|
+
if (result.status === 4 && !result.error) { return { state: 'login_required', login: null }; }
|
|
48
|
+
let account;
|
|
49
|
+
try { account = JSON.parse(result.stdout); } catch { /* Unknown response stays unverified. */ }
|
|
50
|
+
if (result.status === 0 && !result.error && account?.type === 'User'
|
|
51
|
+
&& /^[A-Za-z0-9][A-Za-z0-9-]{0,38}$/.test(account.login || '')) {
|
|
52
|
+
return { state: 'verified', login: account.login };
|
|
53
|
+
}
|
|
54
|
+
return { state: 'unverified', login: null };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Called only after the router validates consent and the managed-run boundary. */
|
|
58
|
+
function setup(parsed, options) {
|
|
59
|
+
let tool = toolState(options);
|
|
60
|
+
const installation = { attempted: false, state: 'not_requested', plan: null };
|
|
61
|
+
if (tool.state === 'missing') {
|
|
62
|
+
installation.plan = installPlan(options);
|
|
63
|
+
if (parsed.install) {
|
|
64
|
+
installation.state = 'unavailable';
|
|
65
|
+
if (installation.plan.available) {
|
|
66
|
+
installation.attempted = true;
|
|
67
|
+
const result = invoke(installation.plan.binary, installation.plan.args, options, true);
|
|
68
|
+
tool = toolState(options);
|
|
69
|
+
installation.state = result.status === 0 && !result.error && tool.state === 'available'
|
|
70
|
+
? 'verified' : 'unverified';
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
} else if (parsed.install) {
|
|
74
|
+
installation.state = tool.state === 'available' ? 'already_available' : 'unavailable';
|
|
75
|
+
}
|
|
76
|
+
const account = tool.state === 'available' ? accountState(options) : { state: 'not_checked', login: null };
|
|
77
|
+
const ready = tool.state === 'available' && account.state === 'verified';
|
|
78
|
+
const nextStep = installation.state === 'unverified' ? 'inspect_installation'
|
|
79
|
+
: tool.state === 'unverified' ? 'check_execution'
|
|
80
|
+
: tool.state === 'missing' ? (installation.plan?.available ? 'confirm_install' : 'manual_install')
|
|
81
|
+
: ready ? 'ready' : account.state === 'login_required' ? 'browser_login' : 'check_account';
|
|
82
|
+
return {
|
|
83
|
+
success: !parsed.install || ['verified', 'already_available'].includes(installation.state),
|
|
84
|
+
action: 'setup', tool, account, ready, installation, nextStep,
|
|
85
|
+
message: t(`feedback_github.setup_${nextStep}`),
|
|
86
|
+
consentNotice: t('feedback_github.setup_consent'),
|
|
87
|
+
privacyNotice: t('feedback_github.setup_privacy'),
|
|
88
|
+
authorization: { performed: false, requiresSeparateConsent: true,
|
|
89
|
+
command: tool.state === 'available' && !ready ? LOGIN_COMMAND : null,
|
|
90
|
+
url: 'https://cli.github.com/manual/gh_auth_login' },
|
|
91
|
+
links: { install: INSTALL_URL, signup: 'https://github.com/signup', issue: 'https://github.com/openyida/openyida/issues/new' },
|
|
92
|
+
submissionPerformed: false, publicConfirmationRequired: true,
|
|
93
|
+
inspectBeforeRetry: installation.state === 'unverified',
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
module.exports = { setup };
|
package/lib/feedback/github.js
CHANGED
|
@@ -11,15 +11,18 @@ const links = Object.freeze({ repository: `https://github.com/${REPOSITORY}`,
|
|
|
11
11
|
/** Parse explicit account actions; submitting an Issue never implies Star or Fork consent. */
|
|
12
12
|
function parseArgs(args = []) {
|
|
13
13
|
const [action, ...flags] = args;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|| (
|
|
14
|
+
const install = flags.includes('--install');
|
|
15
|
+
if (!['status', 'star', 'fork', 'setup'].includes(action) || new Set(flags).size !== flags.length
|
|
16
|
+
|| flags.some(flag => !['--json', '--confirm', '--install'].includes(flag))
|
|
17
|
+
|| (install && action !== 'setup')
|
|
18
|
+
|| (action === 'status' && flags.includes('--confirm'))
|
|
19
|
+
|| (action === 'setup' && !install && flags.includes('--confirm'))) {
|
|
17
20
|
throw new CliError(t('feedback_github.usage'), { code: 'FEEDBACK_GITHUB_USAGE' });
|
|
18
21
|
}
|
|
19
|
-
if (
|
|
20
|
-
throw new CliError(t('feedback_github.confirmation'), { code: 'FEEDBACK_GITHUB_CONFIRMATION_REQUIRED' });
|
|
22
|
+
if ((['star', 'fork'].includes(action) || install) && !flags.includes('--confirm')) {
|
|
23
|
+
throw new CliError(t(install ? 'feedback_github.setup_consent' : 'feedback_github.confirmation'), { code: 'FEEDBACK_GITHUB_CONFIRMATION_REQUIRED' });
|
|
21
24
|
}
|
|
22
|
-
return { action, confirm: flags.includes('--confirm') };
|
|
25
|
+
return { action, confirm: flags.includes('--confirm'), ...(action === 'setup' ? { install } : {}) };
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
/** Run gh against github.com only. Never expose its raw output or credential diagnostics. */
|
|
@@ -90,10 +93,12 @@ function inspect(options) {
|
|
|
90
93
|
|
|
91
94
|
/** Execute one explicitly authorized action, then read it back. No retry, clone, remote change or Issue submission. */
|
|
92
95
|
function execute(parsed, options = {}) {
|
|
93
|
-
const
|
|
96
|
+
const validated = parseArgs([parsed.action, ...(parsed.confirm === true ? ['--confirm'] : []), ...(parsed.install === true ? ['--install'] : [])]);
|
|
97
|
+
const { action } = validated;
|
|
94
98
|
if ((options.env || process.env).OPENYIDA_MANAGED_RUN === '1') {
|
|
95
99
|
throw new CliError(t('feedback_github.failed'), { code: 'FEEDBACK_GITHUB_MANAGED_DENIED' });
|
|
96
100
|
}
|
|
101
|
+
if (action === 'setup') { return require('./github-setup').setup(validated, options); }
|
|
97
102
|
const result = inspect(options);
|
|
98
103
|
if (action === 'status') { return result; }
|
|
99
104
|
const fail = (stage, mutationPerformed = false) => {
|
|
@@ -124,7 +129,11 @@ function execute(parsed, options = {}) {
|
|
|
124
129
|
/** CLI entrypoint. The confirmation flag attests to the user's explicit choice of this action. */
|
|
125
130
|
function run(args = []) {
|
|
126
131
|
const parsed = parseArgs(args);
|
|
127
|
-
|
|
132
|
+
const result = execute(parsed);
|
|
133
|
+
if (result.success === false) {
|
|
134
|
+
throw new CliError(result.message, { code: 'FEEDBACK_GITHUB_SETUP_FAILED', details: result });
|
|
135
|
+
}
|
|
136
|
+
console.log(JSON.stringify(result, null, 2));
|
|
128
137
|
}
|
|
129
138
|
|
|
130
139
|
module.exports = { parseArgs, execute, run };
|
package/lib/feedback/report.js
CHANGED
|
@@ -13,6 +13,15 @@ const MAX_INPUT_BYTES = 1024 * 1024;
|
|
|
13
13
|
const REPOSITORY = 'openyida/openyida';
|
|
14
14
|
const TEXT_FIELDS = ['title', 'summary', 'command', 'expected', 'actual', 'impact', 'tool', 'model'];
|
|
15
15
|
const LIST_FIELDS = ['steps', 'evidence', 'attempts'];
|
|
16
|
+
const INCIDENT_FIELDS = ['openyida', 'node', 'platform', 'arch', 'installation', 'browser', 'deployment', 'organizationContext'];
|
|
17
|
+
|
|
18
|
+
function incidentEnvironment(value) {
|
|
19
|
+
if (value === undefined) {value = {};}
|
|
20
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {throw invalid('object');}
|
|
21
|
+
// Only explicit incident facts belong here. The reporting process may run
|
|
22
|
+
// after an upgrade or on another machine; never infer incident versions.
|
|
23
|
+
return Object.fromEntries(INCIDENT_FIELDS.map(key => [key, sanitizeText(value[key])]));
|
|
24
|
+
}
|
|
16
25
|
|
|
17
26
|
function comparisonRun(value) {
|
|
18
27
|
if (value === undefined) {return { status: 'not_run', evidence: [] };}
|
|
@@ -98,6 +107,8 @@ function buildReport(input) {
|
|
|
98
107
|
throw invalid('confirmed');
|
|
99
108
|
}
|
|
100
109
|
report.screenshots = [];
|
|
110
|
+
report.incidentEnvironment = incidentEnvironment(input.incidentEnvironment);
|
|
111
|
+
// Retain this output contract for existing consumers: generator context only.
|
|
101
112
|
report.environment = {
|
|
102
113
|
openyida: require('../../package.json').version,
|
|
103
114
|
node: process.version,
|
|
@@ -114,17 +125,28 @@ function buildReport(input) {
|
|
|
114
125
|
|
|
115
126
|
// Escape markup so copied diagnostics cannot inject images, links or HTML in an issue.
|
|
116
127
|
function markdownText(value) {
|
|
117
|
-
return String(value || '
|
|
128
|
+
return String(value || t('feedback_report.body.unknown')).replace(/[\\`*_{}[\]<>()#!|~]/g, '\\$&');
|
|
118
129
|
}
|
|
119
130
|
|
|
120
131
|
function renderMarkdown(report) {
|
|
132
|
+
const label = key => t('feedback_report.body.' + key);
|
|
133
|
+
const incident = report.incidentEnvironment || {};
|
|
134
|
+
const environmentLines = Object.entries({
|
|
135
|
+
openyida: incident.openyida, node: incident.node, platform: incident.platform, arch: incident.arch,
|
|
136
|
+
installation: incident.installation, tool: report.tool, model: report.model,
|
|
137
|
+
...Object.fromEntries(['browser', 'deployment', 'organizationContext']
|
|
138
|
+
.filter(key => incident[key]).map(key => [key, incident[key]])),
|
|
139
|
+
}).map(([key, value]) => `${key}: ${value || label('unknown')}`);
|
|
121
140
|
const sections = [
|
|
122
|
-
['
|
|
123
|
-
['
|
|
124
|
-
['
|
|
125
|
-
['
|
|
126
|
-
['
|
|
127
|
-
|
|
141
|
+
[label('summary'), report.summary || label('unknown')],
|
|
142
|
+
[label('actual'), report.actual], [label('expected'), report.expected || label('unknown')],
|
|
143
|
+
[label('incident'), environmentLines],
|
|
144
|
+
[label('command'), report.command], [label('steps'), report.steps],
|
|
145
|
+
[label('impact'), report.impact], [label('evidence'), report.evidence], [label('attempts'), report.attempts],
|
|
146
|
+
...(report.cause.summary || report.cause.status !== 'unknown' || report.cause.category !== 'unknown'
|
|
147
|
+
? [[label('cause'), `${report.cause.category} / ${report.cause.status}\n${report.cause.summary}`]] : []),
|
|
148
|
+
[label('cause_evidence'), report.cause.evidence], [label('next'), report.cause.nextSteps],
|
|
149
|
+
[label('screenshots'), report.screenshots.map(item => `${item.file}: ${item.description || label('review_image')}`)],
|
|
128
150
|
...(['before', 'after', 'regression'].some(key => report.comparison[key].status !== 'not_run') || report.comparison.limitations.length ? ['before', 'after', 'regression'] : []).map(key => [
|
|
129
151
|
`Test comparison: ${key}`, [
|
|
130
152
|
`Status: ${report.comparison[key].status}`,
|
|
@@ -134,19 +156,17 @@ function renderMarkdown(report) {
|
|
|
134
156
|
...(report.comparison[key].evidence || []),
|
|
135
157
|
],
|
|
136
158
|
]),
|
|
137
|
-
...(report.comparison.limitations.length ? [['
|
|
138
|
-
|
|
159
|
+
...(report.comparison.limitations.length ? [[label('limitations'), report.comparison.limitations]] : []),
|
|
160
|
+
[label('generator'), Object.entries(report.environment).map(([key, value]) => `${key}: ${value}`)],
|
|
161
|
+
].filter(([, value]) => Array.isArray(value) ? value.length : Boolean(value));
|
|
139
162
|
return [
|
|
140
163
|
`# ${markdownText(report.title)}`, '',
|
|
141
|
-
'
|
|
142
|
-
`Fingerprint: ${report.fingerprint}`, `Observed occurrences: ${report.occurrenceCount || 'Unknown'}`, '',
|
|
143
|
-
'## Environment', '',
|
|
144
|
-
...Object.entries({ ...report.environment, tool: report.tool, model: report.model })
|
|
145
|
-
.map(([key, value]) => `- ${key}: ${markdownText(value)}`), '',
|
|
164
|
+
label('draft'), '',
|
|
146
165
|
...sections.flatMap(([heading, value]) => [
|
|
147
166
|
`## ${heading}`, '',
|
|
148
|
-
Array.isArray(value) ?
|
|
167
|
+
Array.isArray(value) ? value.map(item => `- ${markdownText(item)}`).join('\n') : markdownText(value), '',
|
|
149
168
|
]),
|
|
169
|
+
`Fingerprint: ${report.fingerprint}`, `Observed occurrences: ${report.occurrenceCount || label('unknown')}`, '',
|
|
150
170
|
].join('\n');
|
|
151
171
|
}
|
|
152
172
|
|
|
@@ -18,11 +18,16 @@ function scheduleNodes(schedule, id, nextId) {
|
|
|
18
18
|
if (!/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:00$/.test(startDate || '')
|
|
19
19
|
|| !Number.isFinite(Date.parse(startDate.replace(' ', 'T') + '+08:00'))
|
|
20
20
|
|| !['ONCE', 'DAILY', 'WORKDAY', 'WEEKLY', 'BI_WEEKLY', 'MONTHLY', 'LAST_DAY_PER_MONTH'].includes(repeat)
|
|
21
|
-
|| maxCount !== undefined && (!Number.isInteger(maxCount) || maxCount < 1)
|
|
21
|
+
|| maxCount !== undefined && (!Number.isInteger(maxCount) || maxCount < 1 || maxCount > 100)
|
|
22
22
|
|| expireDate !== undefined && (!Number.isFinite(expireDate)
|
|
23
23
|
|| expireDate <= Date.parse(startDate.replace(' ', 'T') + '+08:00'))
|
|
24
24
|
|| maxCount !== undefined && expireDate !== undefined
|
|
25
25
|
|| repeat === 'ONCE' && (maxCount !== undefined || expireDate !== undefined)) { invalid('schedule values'); }
|
|
26
|
+
if (repeat !== 'ONCE' && maxCount === undefined && expireDate === undefined) {
|
|
27
|
+
throw new CliError(t('integration_triggers.end_required'), {
|
|
28
|
+
code: 'INTEGRATION_TRIGGER_INVALID', details: { reason: 'schedule end condition', remoteWrites: 0 },
|
|
29
|
+
});
|
|
30
|
+
}
|
|
26
31
|
const startTimestamp = Date.parse(startDate.replace(' ', 'T') + '+08:00');
|
|
27
32
|
if (new Date(startTimestamp + 8 * 3600000).toISOString().slice(0, 19).replace('T', ' ') !== startDate) {
|
|
28
33
|
invalid('invalid calendar date');
|
|
@@ -37,8 +42,8 @@ function scheduleNodes(schedule, id, nextId) {
|
|
|
37
42
|
viewNode: { componentName: 'StartNode', id, props: { nodeName: 'StartNode', name, nodeError: '',
|
|
38
43
|
start: { type: 'trigger', triggerType: 'TimerEvent', startDate: startTimestamp, repeatRuleType: repeat,
|
|
39
44
|
startTrigger: { startTriggerTime: 'specifiedTime' },
|
|
40
|
-
overTimeType:
|
|
41
|
-
...(
|
|
45
|
+
overTimeType: expireDate !== undefined ? 'time' : 'count',
|
|
46
|
+
...(expireDate === undefined ? { overCount: maxCount ?? 1 } : {}),
|
|
42
47
|
...(expireDate !== undefined ? { overTime: expireDate } : {}), rules: true } } },
|
|
43
48
|
formEventTypes: [],
|
|
44
49
|
};
|
|
@@ -19,6 +19,7 @@ const {
|
|
|
19
19
|
} = require('./services/process-compiler');
|
|
20
20
|
const { verifyPlatformView } = require('./services/process-view-verifier');
|
|
21
21
|
const { inspectProcessFields, assertPermissionFields } = require('./services/process-form-fields');
|
|
22
|
+
const { collectFormMemberIds, assertFormMemberFields } = require('./services/process-dynamic-actors');
|
|
22
23
|
const { completeSubtablePermissions } = require('./services/process-field-permissions');
|
|
23
24
|
const {
|
|
24
25
|
getProcessById,
|
|
@@ -365,6 +366,7 @@ async function run(args, options) {
|
|
|
365
366
|
'build_definition',
|
|
366
367
|
error,
|
|
367
368
|
{
|
|
369
|
+
...(error.code === 'PROCESS_DYNAMIC_ACTOR_INVALID' ? error.details : {}),
|
|
368
370
|
retryable: false,
|
|
369
371
|
nextAction: 'fix_definition_file',
|
|
370
372
|
diagnostics: [summarizeRemoteResult(error)],
|
|
@@ -417,13 +419,17 @@ async function run(args, options) {
|
|
|
417
419
|
}
|
|
418
420
|
stageTracker.complete('preflight_form_mode');
|
|
419
421
|
|
|
420
|
-
|
|
422
|
+
const memberFieldIds = collectFormMemberIds(compiled.viewJson);
|
|
423
|
+
if (Object.keys(compiled.processJson.flowConfig).length || memberFieldIds.length) {
|
|
421
424
|
try {
|
|
422
425
|
const fieldInspection = await inspectProcessFields(authRef, { appType, formUuid, processCode }, { binding: initialMode });
|
|
423
426
|
assertPermissionFields(compiled.processJson.flowConfig, fieldInspection.fields);
|
|
427
|
+
assertFormMemberFields(memberFieldIds, fieldInspection.fields);
|
|
424
428
|
completeSubtablePermissions(compiled, fieldInspection.fields);
|
|
425
429
|
} catch (error) {
|
|
426
|
-
failure(error.code || 'PROCESS_FORM_FIELDS_READ_FAILED', error.message, 'preflight_form_fields', error, {
|
|
430
|
+
failure(error.code || 'PROCESS_FORM_FIELDS_READ_FAILED', error.message, 'preflight_form_fields', error, {
|
|
431
|
+
...(error.code === 'PROCESS_DYNAMIC_ACTOR_INVALID' ? error.details : {}), remoteWrites: 0,
|
|
432
|
+
});
|
|
427
433
|
}
|
|
428
434
|
}
|
|
429
435
|
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { CliError } = require('../core/cli-error');
|
|
4
|
+
const { t } = require('../core/i18n');
|
|
5
|
+
|
|
6
|
+
function fail(code, key, details = {}) {
|
|
7
|
+
throw new CliError(t(`process_tasks.${key}`), {
|
|
8
|
+
code,
|
|
9
|
+
details: { retryable: false, retrySafe: true, sideEffectState: 'none', ...details },
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function taskId(value) {
|
|
14
|
+
if (typeof value === 'number' && (!Number.isSafeInteger(value) || value <= 0)) {return null;}
|
|
15
|
+
const id = typeof value === 'number' ? String(value) : value;
|
|
16
|
+
return typeof id === 'string' && /^[1-9][0-9]*$/.test(id) ? id : null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function validateArguments(positionals, options, transfer = false) {
|
|
20
|
+
const required = ['process_inst_id', ...(transfer
|
|
21
|
+
? ['to_user_id', 'form_uuid', 'expect_form_name', 'expect_form_type'] : [])];
|
|
22
|
+
const allowed = [...required, 'json', ...(transfer ? ['task_id', 'remark'] : [])];
|
|
23
|
+
if (positionals.length !== 1 || !/^APP_[A-Za-z0-9_-]+$/.test(positionals[0])) {
|
|
24
|
+
fail('DATA_INVALID_ARGUMENTS', 'arguments_invalid');
|
|
25
|
+
}
|
|
26
|
+
if (Object.keys(options).some(key => !allowed.includes(key))
|
|
27
|
+
|| required.some(key => typeof options[key] !== 'string' || !options[key].trim())
|
|
28
|
+
|| (options.remark !== undefined && (typeof options.remark !== 'string' || !options.remark.trim()))
|
|
29
|
+
|| !/^[A-Za-z0-9_-]+$/.test(options.process_inst_id)
|
|
30
|
+
|| (transfer && !/^FORM[-_][A-Za-z0-9_-]+$/.test(options.form_uuid))
|
|
31
|
+
|| ['process_inst_id', 'form_uuid', 'to_user_id'].some(key =>
|
|
32
|
+
typeof options[key] === 'string' && /\s/.test(options[key]))
|
|
33
|
+
|| (options.task_id !== undefined && !taskId(options.task_id))) {
|
|
34
|
+
fail('DATA_INVALID_ARGUMENTS', 'arguments_invalid');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function successful(response) {
|
|
39
|
+
return response && response.success === true && !response.__needLogin && !response.__csrfExpired
|
|
40
|
+
&& [undefined, null, '', 0, '0'].includes(response.errorCode);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function queryProcessTasks(client, appType, processInstanceId) {
|
|
44
|
+
const response = await client.get(
|
|
45
|
+
`/dingtalk/web/${appType}/query/formProcInstData/getInstTaskDatas.json`,
|
|
46
|
+
{
|
|
47
|
+
_api: 'DataManage.getInstTasks', _mock: 'false', _stamp: String(Date.now()),
|
|
48
|
+
_locale_time_zone_offset: '28800000', procInstId: processInstanceId,
|
|
49
|
+
}
|
|
50
|
+
);
|
|
51
|
+
if (!successful(response)) {
|
|
52
|
+
fail('DATA_PROCESS_TASK_QUERY_FAILED', 'query_failed', {
|
|
53
|
+
causeCode: response && response.errorCode,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
if (!Array.isArray(response.content)) {
|
|
57
|
+
fail('DATA_PROCESS_TASK_RESPONSE_INVALID', 'response_invalid');
|
|
58
|
+
}
|
|
59
|
+
const ids = new Set();
|
|
60
|
+
const tasks = response.content.map(task => {
|
|
61
|
+
const id = task && taskId(task.id);
|
|
62
|
+
if (!id || task.processInstanceId !== processInstanceId || ids.has(id)
|
|
63
|
+
|| typeof task.status !== 'string' || !task.status) {
|
|
64
|
+
fail('DATA_PROCESS_TASK_RESPONSE_INVALID', 'response_invalid');
|
|
65
|
+
}
|
|
66
|
+
ids.add(id);
|
|
67
|
+
return { ...task, taskId: id };
|
|
68
|
+
});
|
|
69
|
+
return { ...response, content: tasks };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function selectTask(tasks, requestedId) {
|
|
73
|
+
const running = tasks.filter(task => task.status === 'RUNNING');
|
|
74
|
+
if (requestedId !== undefined) {
|
|
75
|
+
const selected = running.find(task => task.taskId === requestedId);
|
|
76
|
+
if (!selected) {fail('DATA_PROCESS_TASK_NOT_RUNNING', 'not_running', { taskId: requestedId });}
|
|
77
|
+
return selected;
|
|
78
|
+
}
|
|
79
|
+
if (!running.length) {fail('DATA_PROCESS_TASK_NOT_RUNNING', 'not_running');}
|
|
80
|
+
if (running.length !== 1) {
|
|
81
|
+
fail('DATA_PROCESS_TASK_AMBIGUOUS', 'ambiguous', {
|
|
82
|
+
tasks: running.map(task => ({ taskId: task.taskId, groupName: task.groupName,
|
|
83
|
+
activityCode: task.activityCode, actualActionerId: task.actualActionerId })),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return running[0];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function verifiedTask(before, selected, after, toUserId) {
|
|
90
|
+
const original = after.find(task => task.taskId === selected.taskId);
|
|
91
|
+
if (original && original.status === 'RUNNING') {
|
|
92
|
+
return original.actualActionerId === toUserId ? original : null;
|
|
93
|
+
}
|
|
94
|
+
// A recipient's existing parallel task is not evidence of this transfer.
|
|
95
|
+
// Require the old task to close and a uniquely new task in the same node/group.
|
|
96
|
+
if (!selected.activityCode || !taskId(selected.taskGroupId)) {return null;}
|
|
97
|
+
const previousIds = new Set(before.map(task => task.taskId));
|
|
98
|
+
const candidates = after.filter(task => task.status === 'RUNNING'
|
|
99
|
+
&& !previousIds.has(task.taskId)
|
|
100
|
+
&& task.activityCode === selected.activityCode
|
|
101
|
+
&& String(task.taskGroupId) === String(selected.taskGroupId));
|
|
102
|
+
return candidates.length === 1 && candidates[0].actualActionerId === toUserId ? candidates[0] : null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function transferTask(client, input) {
|
|
106
|
+
const { appType, formUuid, processInstanceId, toUserId, remark = '转交' } = input;
|
|
107
|
+
const before = (await queryProcessTasks(client, appType, processInstanceId)).content;
|
|
108
|
+
const selected = selectTask(before, input.taskId);
|
|
109
|
+
if (typeof selected.actualActionerId !== 'string' || !selected.actualActionerId.trim()) {
|
|
110
|
+
fail('DATA_PROCESS_TASK_RESPONSE_INVALID', 'response_invalid');
|
|
111
|
+
}
|
|
112
|
+
const target = { appType, formUuid, processInstanceId, taskId: selected.taskId, toUserId };
|
|
113
|
+
const nextStep = `openyida data query process-tasks ${appType} --process-inst-id ${processInstanceId} --json`;
|
|
114
|
+
const result = {
|
|
115
|
+
...target, target, previousActionerId: selected.actualActionerId,
|
|
116
|
+
mutationPerformed: false, mutationAccepted: false, readbackVerified: false,
|
|
117
|
+
};
|
|
118
|
+
if (selected.actualActionerId === toUserId) {
|
|
119
|
+
return { ...result, success: true, status: 'ALREADY_ASSIGNED', alreadyAssigned: true,
|
|
120
|
+
transferred: false, readbackVerified: true, sideEffectState: 'none' };
|
|
121
|
+
}
|
|
122
|
+
let response;
|
|
123
|
+
try {
|
|
124
|
+
const query = new URLSearchParams({
|
|
125
|
+
_api: 'DataManage.redirectTask', _mock: 'false', _stamp: String(Date.now()),
|
|
126
|
+
});
|
|
127
|
+
response = await client.postFormOnce(
|
|
128
|
+
`/dingtalk/web/${appType}/query/instance/redirectTask.json?${query}`,
|
|
129
|
+
{ _locale_time_zone_offset: '28800000', taskId: selected.taskId,
|
|
130
|
+
procInstId: processInstanceId, nowActionerId: toUserId, remark, byManager: 'true' },
|
|
131
|
+
{ referer: `${client.getAuthRef().baseUrl}/${appType}/admin/${formUuid}/dataManage` }
|
|
132
|
+
);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
fail('DATA_PROCESS_TRANSFER_RESULT_UNKNOWN', 'result_unknown', {
|
|
135
|
+
...result, mutationPerformed: true, status: 'RESULT_UNKNOWN', mutationAccepted: null,
|
|
136
|
+
retrySafe: false, sideEffectState: 'unknown', readbackAllowed: true, nextStep,
|
|
137
|
+
causeCode: error.code || 'TRANSFER_TRANSPORT_FAILED',
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
if (!successful(response)) {
|
|
141
|
+
const rejected = response && response.success === false && !response.__needLogin
|
|
142
|
+
&& !response.__csrfExpired && !response.__nonJsonResponse;
|
|
143
|
+
fail(rejected ? 'DATA_PROCESS_TRANSFER_REJECTED' : 'DATA_PROCESS_TRANSFER_RESULT_UNKNOWN',
|
|
144
|
+
rejected ? 'rejected' : 'result_unknown', {
|
|
145
|
+
...result, mutationPerformed: true, mutationAccepted: rejected ? false : null,
|
|
146
|
+
status: rejected ? 'REJECTED' : 'RESULT_UNKNOWN', retrySafe: false,
|
|
147
|
+
sideEffectState: rejected ? 'none' : 'unknown', readbackAllowed: true, nextStep,
|
|
148
|
+
causeCode: response && response.errorCode,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
let verified;
|
|
152
|
+
try {
|
|
153
|
+
const after = (await queryProcessTasks(client, appType, processInstanceId)).content;
|
|
154
|
+
verified = verifiedTask(before, selected, after, toUserId);
|
|
155
|
+
} catch (error) {
|
|
156
|
+
fail('DATA_PROCESS_TRANSFER_UNVERIFIED', 'unverified', {
|
|
157
|
+
...result, mutationPerformed: true, mutationAccepted: true, status: 'UNVERIFIED',
|
|
158
|
+
retrySafe: false, sideEffectState: 'unknown', readbackAllowed: true, nextStep,
|
|
159
|
+
causeCode: error.code || 'TRANSFER_READBACK_FAILED',
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
if (!verified) {
|
|
163
|
+
fail('DATA_PROCESS_TRANSFER_UNVERIFIED', 'unverified', {
|
|
164
|
+
...result, mutationPerformed: true, mutationAccepted: true, status: 'UNVERIFIED',
|
|
165
|
+
retrySafe: false, sideEffectState: 'unknown', readbackAllowed: true, nextStep,
|
|
166
|
+
causeCode: 'TRANSFER_READBACK_MISMATCH',
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
return { ...result, success: true, transferred: true, status: 'TRANSFERRED',
|
|
170
|
+
mutationPerformed: true, mutationAccepted: true, readbackVerified: true,
|
|
171
|
+
currentTaskId: verified.taskId, actualActionerId: verified.actualActionerId,
|
|
172
|
+
retryable: false, sideEffectState: 'committed' };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
module.exports = { validateArguments, queryProcessTasks, transferTask };
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { verifyDynamicActors } = require('./process-dynamic-actors');
|
|
4
|
+
|
|
3
5
|
// The native MultiApproval setter stores flat UI rules, while runtime rules
|
|
4
6
|
// are grouped by status. Keep the two representations separate.
|
|
5
7
|
function buildMultiApproverRules(config, legacyRules) {
|
|
@@ -26,6 +28,7 @@ function verifyActorRoles(expectedNodes, actualNodes, errors) {
|
|
|
26
28
|
}
|
|
27
29
|
(expectedNodes || []).forEach((node, index) => {
|
|
28
30
|
const actual = (actualNodes || [])[index];
|
|
31
|
+
verifyDynamicActors(node, actual, errors);
|
|
29
32
|
const expectedConditional = node.props?.approverRules;
|
|
30
33
|
if (expectedConditional?.mode === 'ApprovalNode_rules_multiple'
|
|
31
34
|
&& JSON.stringify(expectedConditional) !== JSON.stringify(actual?.props?.approverRules)) {
|
|
@@ -11,6 +11,7 @@ const { t } = require('../../core/i18n');
|
|
|
11
11
|
const { buildNodeActions } = require('./process-actions');
|
|
12
12
|
const { buildFlowConfig, normalizeBehaviorList } = require('./process-field-permissions');
|
|
13
13
|
const { buildMultiApproverRules } = require('./process-actor-config');
|
|
14
|
+
const { buildDynamicActor, validateNativeActor, assertNativeActorTypes } = require('./process-dynamic-actors');
|
|
14
15
|
|
|
15
16
|
const KEY_PATTERN = /^[A-Za-z][A-Za-z0-9_]*$/;
|
|
16
17
|
|
|
@@ -369,6 +370,8 @@ function getFriendlyApproverKind(approver) {
|
|
|
369
370
|
}
|
|
370
371
|
const normalized = rawKind.toLowerCase().replace(/[-_\s]/g, '');
|
|
371
372
|
const kindMap = {
|
|
373
|
+
formfield: 'formField',
|
|
374
|
+
selfselect: 'selfSelect',
|
|
372
375
|
originator: 'originator',
|
|
373
376
|
self: 'originator',
|
|
374
377
|
starter: 'originator',
|
|
@@ -638,6 +641,11 @@ function buildFriendlyApproverConfig(node, approver) {
|
|
|
638
641
|
return null;
|
|
639
642
|
}
|
|
640
643
|
const raw = isPlainObject(approver) ? approver : {};
|
|
644
|
+
if (kind === 'formField' || kind === 'selfSelect') {
|
|
645
|
+
return buildDynamicActor(node, raw, kind, normalizeNodeType(node), {
|
|
646
|
+
createBaseApproverRules, normalizeApprovalMode, normalizeUserApprovers, normalizeRoleApprovers,
|
|
647
|
+
});
|
|
648
|
+
}
|
|
641
649
|
if (kind === 'originator') {
|
|
642
650
|
return buildOriginatorApproverConfig(node, raw);
|
|
643
651
|
}
|
|
@@ -1066,8 +1074,12 @@ function buildApproverConfig(node) {
|
|
|
1066
1074
|
return friendlyApprover;
|
|
1067
1075
|
}
|
|
1068
1076
|
|
|
1077
|
+
if (typeof approverInput === 'string') {
|
|
1078
|
+
validateNativeActor({ kind: approverInput }, nodeType);
|
|
1079
|
+
}
|
|
1069
1080
|
const rawApprover = node.approverConfig || (approverInput && typeof approverInput === 'object' ? approverInput : null);
|
|
1070
1081
|
if (rawApprover) {
|
|
1082
|
+
validateNativeActor(rawApprover, nodeType, node._nodeId);
|
|
1071
1083
|
return {
|
|
1072
1084
|
approvalType: rawApprover.approvalType || rawApprover.type || 'ext_target_approval_originator',
|
|
1073
1085
|
approvals: rawApprover.approvals || [['originator']],
|
|
@@ -1086,6 +1098,7 @@ function buildApproverConfig(node) {
|
|
|
1086
1098
|
}
|
|
1087
1099
|
|
|
1088
1100
|
if (node.approvalType || node.approvals || node.approverRules || node.processProps) {
|
|
1101
|
+
validateNativeActor({ ...node, type: node.approvalType }, nodeType, node._nodeId);
|
|
1089
1102
|
return {
|
|
1090
1103
|
approvalType: node.approvalType || 'ext_target_approval_originator',
|
|
1091
1104
|
approvals: node.approvals || [['originator']],
|
|
@@ -2053,6 +2066,8 @@ function buildProcessAndViewJson(definition, processCode, formUuid, baseUrl, app
|
|
|
2053
2066
|
},
|
|
2054
2067
|
};
|
|
2055
2068
|
|
|
2069
|
+
assertNativeActorTypes(processJson.nodes);
|
|
2070
|
+
assertNativeActorTypes(viewJson.schema.children);
|
|
2056
2071
|
processJson.flowConfig = buildFlowConfig(viewJson);
|
|
2057
2072
|
const result = {
|
|
2058
2073
|
processJson,
|