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.
Files changed (53) hide show
  1. package/README.md +4 -2
  2. package/lib/app/canvas-compile.js +8 -0
  3. package/lib/app/page-linter.js +10 -0
  4. package/lib/app/responsive-layout-guard.js +82 -0
  5. package/lib/core/command-manifest.js +15 -7
  6. package/lib/core/locales/en.js +50 -2
  7. package/lib/core/locales/zh.js +50 -2
  8. package/lib/core/query-data.js +40 -2
  9. package/lib/core/sample.js +3 -0
  10. package/lib/feedback/feedback.js +2 -1
  11. package/lib/feedback/github-setup.js +97 -0
  12. package/lib/feedback/github.js +17 -8
  13. package/lib/feedback/report.js +35 -15
  14. package/lib/integration/integration-triggers.js +8 -3
  15. package/lib/process/configure-process.js +8 -2
  16. package/lib/process/process-tasks.js +175 -0
  17. package/lib/process/services/process-actor-config.js +3 -0
  18. package/lib/process/services/process-compiler.js +15 -0
  19. package/lib/process/services/process-dynamic-actors.js +165 -0
  20. package/lib/samples/openyida-scaffold/canvas-nav/mixed.jsx +1 -1
  21. package/lib/samples/openyida-scaffold/canvas-nav/shared.jsx +1 -1
  22. package/lib/samples/openyida-scaffold/canvas-nav/side.jsx +1 -1
  23. package/lib/samples/openyida-scaffold/canvas-nav/sidebar.jsx +36 -24
  24. package/lib/samples/openyida-scaffold/canvas-nav/top.jsx +1 -1
  25. package/lib/samples/openyida-scaffold/canvas-responsive-layout.jsx +104 -0
  26. package/package.json +1 -1
  27. package/scripts/postinstall.js +1 -0
  28. package/yida-skills/SKILL.md +3 -2
  29. package/yida-skills/skills/yida-canvas-custom-page/SKILL.md +5 -1
  30. package/yida-skills/skills/yida-custom-page/SKILL.md +6 -2
  31. package/yida-skills/skills/yida-data-management/SKILL.md +24 -3
  32. package/yida-skills/skills/yida-data-management/references/process-task-transfer.md +53 -0
  33. package/yida-skills/skills/yida-density/SKILL.md +6 -1
  34. package/yida-skills/skills/yida-design/SKILL.md +1 -1
  35. package/yida-skills/skills/yida-design/references/responsive-layout.md +69 -22
  36. package/yida-skills/skills/yida-design/references/task-driven-visual-design.md +2 -0
  37. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/SKILL.md +1 -0
  38. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/references/build-plan-compact-schema.md +1 -1
  39. package/yida-skills/skills/yida-design/workflow/output-design.md +2 -2
  40. package/yida-skills/skills/yida-design/workflow/step-1-read-brief.md +2 -0
  41. package/yida-skills/skills/yida-design/workflow/step-4-wireframe-interaction.md +10 -10
  42. package/yida-skills/skills/yida-feedback/SKILL.md +22 -5
  43. package/yida-skills/skills/yida-feedback/references/github-contribution.md +14 -8
  44. package/yida-skills/skills/yida-feedback/references/github-setup.md +74 -0
  45. package/yida-skills/skills/yida-feedback/references/report-input.md +10 -2
  46. package/yida-skills/skills/yida-feedback/references/user-interaction.md +19 -2
  47. package/yida-skills/skills/yida-flow-node-config/SKILL.md +1 -1
  48. package/yida-skills/skills/yida-flow-node-config/references/actors.md +4 -0
  49. package/yida-skills/skills/yida-flow-node-config/references/dynamic-actors.md +84 -0
  50. package/yida-skills/skills/yida-integration/SKILL.md +1 -0
  51. package/yida-skills/skills/yida-integration/references/triggers.md +14 -1
  52. package/yida-skills/skills/yida-prd/SKILL.md +1 -0
  53. 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 };
@@ -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
- if (!['status', 'star', 'fork'].includes(action) || new Set(flags).size !== flags.length
15
- || flags.some(flag => !['--json', '--confirm'].includes(flag))
16
- || (action === 'status' && flags.includes('--confirm'))) {
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 (action !== 'status' && !flags.includes('--confirm')) {
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 { action } = parseArgs([parsed.action, ...(parsed.confirm === true ? ['--confirm'] : [])]);
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
- console.log(JSON.stringify(execute(parsed), null, 2));
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 };
@@ -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 || 'Unknown / not verified').replace(/[\\`*_{}[\]<>()#!|~]/g, '\\$&');
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
- ['Summary', report.summary], ['Command scenario', report.command],
123
- ['Steps to reproduce', report.steps], ['Expected', report.expected], ['Actual', report.actual],
124
- ['Impact', report.impact], ['Evidence (observed)', report.evidence], ['Attempts and results', report.attempts],
125
- ['Root cause assessment', `${report.cause.category} / ${report.cause.status}\n${report.cause.summary || 'Not established'}`],
126
- ['Evidence for assessment', report.cause.evidence], ['Next verification', report.cause.nextSteps],
127
- ['Reproduction screenshots', report.screenshots.map(item => `${item.file}: ${item.description || 'Review image before sharing'}`)],
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 ? [['Test limitations', report.comparison.limitations]] : []),
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
- 'Local draft — not submitted. Review business data and private repository information before sharing.', '',
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) ? (value.length ? value.map(item => `- ${markdownText(item)}`).join('\n') : 'Unknown / not verified') : markdownText(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: maxCount !== undefined ? 'count' : expireDate !== undefined ? 'time' : 'never',
41
- ...(maxCount !== undefined ? { overCount: maxCount } : {}),
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
- if (Object.keys(compiled.processJson.flowConfig).length) {
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, { remoteWrites: 0 });
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,