openyida 2026.10.21 → 2026.10.22-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +24 -3
  2. package/bin/yida.js +4 -0
  3. package/lib/app/canvas-compile.js +8 -0
  4. package/lib/app/page-linter.js +10 -0
  5. package/lib/app/responsive-layout-guard.js +82 -0
  6. package/lib/auth/qwenwork-client.js +190 -0
  7. package/lib/auth/qwenwork.js +37 -0
  8. package/lib/core/command-manifest.js +18 -10
  9. package/lib/core/locales/en.js +63 -4
  10. package/lib/core/locales/zh.js +62 -3
  11. package/lib/core/query-data.js +40 -2
  12. package/lib/core/sample.js +3 -0
  13. package/lib/feedback/feedback.js +2 -1
  14. package/lib/feedback/github-setup.js +97 -0
  15. package/lib/feedback/github.js +17 -8
  16. package/lib/feedback/report.js +35 -15
  17. package/lib/integration/integration-triggers.js +8 -3
  18. package/lib/process/configure-process.js +8 -2
  19. package/lib/process/process-tasks.js +175 -0
  20. package/lib/process/services/process-actor-config.js +3 -0
  21. package/lib/process/services/process-compiler.js +15 -0
  22. package/lib/process/services/process-dynamic-actors.js +165 -0
  23. package/lib/samples/openyida-scaffold/canvas-nav/mixed.jsx +1 -1
  24. package/lib/samples/openyida-scaffold/canvas-nav/shared.jsx +1 -1
  25. package/lib/samples/openyida-scaffold/canvas-nav/side.jsx +1 -1
  26. package/lib/samples/openyida-scaffold/canvas-nav/sidebar.jsx +36 -24
  27. package/lib/samples/openyida-scaffold/canvas-nav/top.jsx +1 -1
  28. package/lib/samples/openyida-scaffold/canvas-responsive-layout.jsx +104 -0
  29. package/package.json +1 -1
  30. package/scripts/postinstall.js +1 -0
  31. package/yida-skills/SKILL.md +5 -2
  32. package/yida-skills/skills/yida-canvas-custom-page/SKILL.md +5 -1
  33. package/yida-skills/skills/yida-custom-page/SKILL.md +6 -2
  34. package/yida-skills/skills/yida-data-management/SKILL.md +24 -3
  35. package/yida-skills/skills/yida-data-management/references/process-task-transfer.md +53 -0
  36. package/yida-skills/skills/yida-density/SKILL.md +6 -1
  37. package/yida-skills/skills/yida-design/SKILL.md +1 -1
  38. package/yida-skills/skills/yida-design/references/responsive-layout.md +69 -22
  39. package/yida-skills/skills/yida-design/references/task-driven-visual-design.md +2 -0
  40. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/SKILL.md +1 -0
  41. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/references/build-plan-compact-schema.md +1 -1
  42. package/yida-skills/skills/yida-design/workflow/output-design.md +2 -2
  43. package/yida-skills/skills/yida-design/workflow/step-1-read-brief.md +2 -0
  44. package/yida-skills/skills/yida-design/workflow/step-4-wireframe-interaction.md +10 -10
  45. package/yida-skills/skills/yida-feedback/SKILL.md +22 -5
  46. package/yida-skills/skills/yida-feedback/references/github-contribution.md +14 -8
  47. package/yida-skills/skills/yida-feedback/references/github-setup.md +74 -0
  48. package/yida-skills/skills/yida-feedback/references/report-input.md +10 -2
  49. package/yida-skills/skills/yida-feedback/references/user-interaction.md +19 -2
  50. package/yida-skills/skills/yida-flow-node-config/SKILL.md +1 -1
  51. package/yida-skills/skills/yida-flow-node-config/references/actors.md +4 -0
  52. package/yida-skills/skills/yida-flow-node-config/references/dynamic-actors.md +84 -0
  53. package/yida-skills/skills/yida-integration/SKILL.md +1 -0
  54. package/yida-skills/skills/yida-integration/references/triggers.md +14 -1
  55. package/yida-skills/skills/yida-login/SKILL.md +12 -0
  56. package/yida-skills/skills/yida-prd/SKILL.md +1 -0
  57. package/yida-skills/skills-index.json +13 -4
@@ -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,
@@ -0,0 +1,165 @@
1
+ 'use strict';
2
+
3
+ const { CliError } = require('../../core/cli-error');
4
+ const { t } = require('../../core/i18n');
5
+
6
+ const FORM_MEMBER = 'ext_target_approval_variable';
7
+ const SELF_SELECT = 'ext_target_optional_approval';
8
+ // Native designer ApproverType, plus the existing conditional-actors adapter.
9
+ const NATIVE_TYPES = new Set(['ext_target_noset', 'ext_target_approval', 'ext_target_approval_role',
10
+ 'ext_target_approval_originator', 'ext_target_approval_liaison', 'ext_target_dept_leader',
11
+ 'ext_target_direct_leader', 'ext_target_multi_management', SELF_SELECT, FORM_MEMBER,
12
+ 'ext_target_approval_invoke', 'ext_target_approval_connector', 'ext_target_approval_matrix',
13
+ 'ext_target_approval_condition']);
14
+
15
+ function invalid(reason, details = {}) {
16
+ throw new CliError(t('process_dynamic_actors.invalid'), {
17
+ code: 'PROCESS_DYNAMIC_ACTOR_INVALID', details: { reason, ...details, remoteWrites: 0 },
18
+ });
19
+ }
20
+
21
+ function buildDynamicActor(node, input, kind, nodeType, helpers) {
22
+ const allowed = ['kind', 'type', 'approverType', 'approvalMode', 'multiApproverType', 'description'];
23
+ allowed.push(...(kind === 'formField' ? ['fieldId', 'fieldIds'] : ['range']));
24
+ if (Object.keys(input).some(key => !allowed.includes(key))) { invalid('unsupported property'); }
25
+ if (input.kind && input.type && input.kind !== input.type) { invalid('conflicting kind/type'); }
26
+ const mode = helpers.normalizeApprovalMode(input.approvalMode || input.multiApproverType || 'all');
27
+ if (!['all', 'or', 'oneByOne'].includes(mode)) { invalid('approvalMode'); }
28
+ const approvalType = kind === 'formField' ? FORM_MEMBER : SELF_SELECT;
29
+ const description = input.description || kind;
30
+ const approverRules = helpers.createBaseApproverRules(approvalType, description, {});
31
+ approverRules.multiApproverType = mode;
32
+ approverRules['approvalType_' + approvalType] = mode;
33
+ if (kind === 'formField') {
34
+ if (input.fieldId !== undefined && input.fieldIds !== undefined) { invalid('use fieldId or fieldIds'); }
35
+ const ids = input.fieldId === undefined ? input.fieldIds : [input.fieldId];
36
+ if (!Array.isArray(ids) || !ids.length || ids.some(id => typeof id !== 'string' || !id.trim() || id !== id.trim())
37
+ || new Set(ids).size !== ids.length) { invalid('fieldId/fieldIds'); }
38
+ approverRules.formMember = ids.map(value => ({ value, label: value }));
39
+ return { approvalType, approvals: [ids], approverRules, description,
40
+ processProps: { multiApprove: mode }, carbonReceiver: { type: 'VARIABLE', value: [ids] } };
41
+ }
42
+ // Carbon self-selection needs a separate non-free entitlement and feature flag.
43
+ // MultiApproval deliberately excludes this source in the native designer.
44
+ if (!['approval', 'operator'].includes(nodeType)) { invalid('selfSelect requires approval/operator', { nodeType }); }
45
+ const range = input.range === undefined ? { type: 'ALL' } : input.range;
46
+ if (!range || typeof range !== 'object' || Array.isArray(range)
47
+ || !['ALL', 'USER', 'ROLE'].includes(range.type)) { invalid('range.type'); }
48
+ const rangeKey = range.type === 'USER' ? 'users' : range.type === 'ROLE' ? 'roles' : null;
49
+ if (Object.keys(range).some(key => key !== 'type' && key !== rangeKey)) { invalid('range properties'); }
50
+ const runtimeRange = { type: range.type };
51
+ const viewRange = { type: range.type };
52
+ if (rangeKey) {
53
+ if (!Array.isArray(range[rangeKey]) || !range[rangeKey].length) { invalid('range must be nonempty'); }
54
+ let items;
55
+ try {
56
+ items = rangeKey === 'users' ? helpers.normalizeUserApprovers({ users: range.users })
57
+ : helpers.normalizeRoleApprovers({ roles: range.roles });
58
+ } catch { invalid('range member/role requires a real ID'); }
59
+ if (items.some(item => !item.id.trim() || !['YIDA', 'DINGTALK'].includes(item.roleType))) { invalid('range ID/roleType'); }
60
+ if (new Set(items.map(item => String(item.id))).size !== items.length) { invalid('duplicate range ID'); }
61
+ const key = rangeKey === 'users' ? 'approvalRange' : 'roleRange';
62
+ viewRange[key] = items;
63
+ runtimeRange[key] = rangeKey === 'users' ? items.map(item => item.id) : items;
64
+ }
65
+ approverRules.approvalOptionalRange = viewRange;
66
+ return { approvalType, approvals: ['__optionalApproval_' + node._nodeId], approverRules, description,
67
+ processProps: { multiApprove: mode, optionalApprovalRange: runtimeRange } };
68
+ }
69
+
70
+ function validateNativeActor(raw, nodeType, nodeId) {
71
+ if (Array.isArray(raw)) { invalid('actor must be an object'); }
72
+ if (raw.kind || raw.approverType) { invalid('unknown friendly actor kind', { kind: raw.kind || raw.approverType }); }
73
+ const type = raw.approvalType || raw.type || 'ext_target_approval_originator';
74
+ if (!NATIVE_TYPES.has(type)) { invalid('unknown native approvalType', { approvalType: type }); }
75
+ if (raw.approverRules?.type && raw.approverRules.type !== type) { invalid('runtime/view approvalType mismatch'); }
76
+ // Dynamic native payloads need both halves. Never default their runtime to originator.
77
+ if (type === FORM_MEMBER || type === SELF_SELECT) {
78
+ if (!raw.approvals || !raw.approverRules) { invalid('dynamic native actors require approvals and approverRules'); }
79
+ if (type === SELF_SELECT && !['approval', 'operator'].includes(nodeType)) {
80
+ invalid('selfSelect requires approval/operator', { nodeType });
81
+ }
82
+ if (type === FORM_MEMBER) {
83
+ const members = raw.approverRules.formMember;
84
+ const ids = Array.isArray(members) ? members.map(item => item?.value) : [];
85
+ if (!ids?.length || ids.some(id => typeof id !== 'string' || !id.trim())
86
+ || JSON.stringify(raw.approvals) !== JSON.stringify([ids])) { invalid('formMember/runtime mismatch'); }
87
+ if (nodeType === 'carbon' && (raw.carbonReceiver?.type !== 'VARIABLE'
88
+ || JSON.stringify(raw.carbonReceiver.value) !== JSON.stringify([ids]))) { invalid('carbonReceiver mismatch'); }
89
+ } else {
90
+ const view = raw.approverRules.approvalOptionalRange;
91
+ const runtime = (raw.processProps || raw.props || {}).optionalApprovalRange;
92
+ if (!view || !runtime || !['ALL', 'USER', 'ROLE'].includes(view.type) || view.type !== runtime.type
93
+ || JSON.stringify(raw.approvals) !== JSON.stringify(['__optionalApproval_' + nodeId])) {
94
+ invalid('selfSelect runtime/view range or generated node ID mismatch');
95
+ }
96
+ const key = view.type === 'USER' ? 'approvalRange' : view.type === 'ROLE' ? 'roleRange' : null;
97
+ if (key) {
98
+ const members = view[key];
99
+ if (!Array.isArray(members) || !members.length || members.some(item => !item?.id)) { invalid('selfSelect range IDs'); }
100
+ const ids = members.map(item => item.id);
101
+ const runtimeIds = view.type === 'USER' ? runtime[key] : runtime[key]?.map(item => item.id);
102
+ if (JSON.stringify(ids) !== JSON.stringify(runtimeIds)) { invalid('selfSelect runtime/view range IDs mismatch'); }
103
+ }
104
+ }
105
+ }
106
+ }
107
+
108
+ // Check both emitted contracts, including legacy multiRules and nested branches.
109
+ function assertNativeActorTypes(value) {
110
+ if (!value || typeof value !== 'object') { return; }
111
+ if (Array.isArray(value)) { value.forEach(assertNativeActorTypes); return; }
112
+ for (const [key, item] of Object.entries(value)) {
113
+ if (['type', 'approvalType'].includes(key) && typeof item === 'string' && item.startsWith('ext_target_')
114
+ && item !== 'ext_target_activity' && !NATIVE_TYPES.has(item)) {
115
+ invalid('unknown native approvalType', { approvalType: item });
116
+ }
117
+ if (item && typeof item === 'object') { assertNativeActorTypes(item); }
118
+ }
119
+ }
120
+
121
+ function actorRules(node) {
122
+ const props = node?.props || {};
123
+ return props.multiApproverRules ? props.multiApproverRules.multiRules || [] : [props.approverRules].filter(Boolean);
124
+ }
125
+
126
+ function collectFormMemberIds(view) {
127
+ const ids = new Set();
128
+ function visit(nodes) {
129
+ for (const node of nodes || []) {
130
+ for (const rule of actorRules(node)) {
131
+ if (rule.type === FORM_MEMBER) { (rule.formMember || []).forEach(item => ids.add(item.value)); }
132
+ }
133
+ visit(node.children);
134
+ }
135
+ }
136
+ visit(view?.schema?.children);
137
+ return [...ids];
138
+ }
139
+
140
+ function assertFormMemberFields(ids, fields) {
141
+ for (const fieldId of ids) {
142
+ const field = fields.find(item => item.fieldId === fieldId);
143
+ if (!field || field.componentName !== 'EmployeeField' || field.parentFieldId) {
144
+ invalid('formField requires an existing top-level EmployeeField', { fieldId, componentName: field?.componentName });
145
+ }
146
+ }
147
+ }
148
+
149
+ function verifyDynamicActors(expected, actual, errors) {
150
+ function signature(node) {
151
+ return actorRules(node).filter(rule => [FORM_MEMBER, SELF_SELECT].includes(rule.type)).map(rule => {
152
+ if (rule.type === FORM_MEMBER) { return { type: rule.type, mode: rule['approvalType_' + rule.type], ids: (rule.formMember || []).map(item => item.value) }; }
153
+ const range = rule.approvalOptionalRange || {};
154
+ return { type: rule.type, mode: rule['approvalType_' + rule.type], rangeType: range.type,
155
+ users: range.type === 'USER' ? (range.approvalRange || []).map(item => String(item.id)) : [],
156
+ roles: range.type === 'ROLE' ? (range.roleRange || []).map(item => ({ id: String(item.id), roleType: item.roleType || 'YIDA' })) : [] };
157
+ });
158
+ }
159
+ const wanted = signature(expected);
160
+ if (wanted.length && JSON.stringify(wanted) !== JSON.stringify(signature(actual))) {
161
+ errors.push({ code: 'PROCESS_PLATFORM_VIEW_DYNAMIC_ACTORS_MISMATCH', nodeId: expected.id });
162
+ }
163
+ }
164
+
165
+ module.exports = { assertNativeActorTypes, buildDynamicActor, validateNativeActor, collectFormMemberIds, assertFormMemberFields, verifyDynamicActors };
@@ -12,7 +12,7 @@ function CanvasNav({ items = [], activeKey, onSelect, title, logo, actions, chil
12
12
  .oy-canvas-nav.oy-nav-mixed .oy-nav-sidebar { padding: var(--pod-nav-l-aside-padding, 8px); }
13
13
  .oy-nav-mixed .oy-nav-content { flex: 1; }
14
14
  .oy-nav-mixed .oy-nav-menu { display: flex; flex-direction: column; gap: var(--pod-nav-menu-gap, 8px); }
15
- @media (max-width: 767px) {
15
+ @container (max-width: 767px) {
16
16
  .oy-nav-mixed .oy-nav-groups { flex-basis: 100%; }
17
17
  .oy-nav-mixed .oy-nav-body { display: block; }
18
18
  .oy-nav-mixed .oy-nav-menu { flex-direction: row; overflow-x: auto; border-right: 0; border-bottom: 1px solid var(--pod-nav-sub-divider-color, var(--color-line1-1, #ddd)); }
@@ -4,7 +4,7 @@ import React from 'react';
4
4
  // 这是可选实现示例;导航外观按 design.md 设计,已有 UI 无需替换成示例默认样式。
5
5
  // 选中菜单标识当前页面,main 用 aria-label 命名,children 直接承载业务内容。
6
6
  const CANVAS_NAV_CSS = `
7
- .oy-canvas-nav { display: flow-root; min-width: 0; min-height: 100vh; background: var(--pod-page-bg-color, var(--color-white, #fff)); }
7
+ .oy-canvas-nav { container-type: inline-size; display: flow-root; min-width: 0; min-height: 100vh; background: var(--pod-page-bg-color, var(--color-white, #fff)); }
8
8
  .oy-canvas-nav .oy-nav-surface {
9
9
  background: var(--pod-shell-theme-bg-color, var(--pod-nav-popup-bg-color, var(--color-white, #fff)));
10
10
  color: var(--pod-nav-item-text-color, var(--color-text1-4, #1f2329));
@@ -6,7 +6,7 @@ function CanvasNav({ items = [], activeKey, onSelect, title, logo, actions, chil
6
6
  .oy-nav-side .oy-nav-content { flex: 1; }
7
7
  .oy-nav-side .oy-nav-brand { min-height: var(--pod-nav-platform-header-height, 48px); }
8
8
  .oy-nav-side .oy-nav-menu { display: flex; flex-direction: column; gap: var(--pod-nav-menu-gap, 8px); }
9
- @media (max-width: 767px) { .oy-nav-side { display: block; } }
9
+ @container (max-width: 767px) { .oy-nav-side { display: block; } }
10
10
  `}</style>
11
11
  <CanvasSidebar title={title} logo={logo} actions={actions} activeKey={activeKey} defaultWidth={sidebarWidth} defaultCollapsed={defaultCollapsed}>
12
12
  <nav aria-label="应用导航" className="oy-nav-menu">