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,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">
@@ -5,15 +5,15 @@ function CanvasSidebar({ children, title, logo, actions, activeKey, defaultWidth
5
5
  const [collapsed, setCollapsed] = React.useState(defaultCollapsed);
6
6
  const [mobileOpen, setMobileOpen] = React.useState(false);
7
7
  const [width, setWidth] = React.useState(null);
8
- const [viewport, setViewport] = React.useState(() => window.innerWidth);
8
+ const [containerWidth, setContainerWidth] = React.useState(() => window.innerWidth);
9
9
  const [resizing, setResizing] = React.useState(false);
10
10
  const [measuredWidth, setMeasuredWidth] = React.useState(216);
11
11
  const sidebar = React.useRef(null);
12
12
  const drag = React.useRef(null);
13
13
  const menuId = React.useId();
14
- const mobile = viewport < 768;
14
+ const mobile = containerWidth < 768;
15
15
  const minWidth = 180;
16
- const maxWidth = Math.min(400, Math.floor(viewport * 0.45));
16
+ const maxWidth = Math.min(400, Math.floor(containerWidth * 0.45));
17
17
  const clamp = value => Math.max(minWidth, Math.min(maxWidth, value));
18
18
  const expanded = mobile ? mobileOpen : !collapsed;
19
19
  const ToggleIcon = mobile ? Menu : collapsed ? PanelLeftOpen : PanelLeftClose;
@@ -25,19 +25,35 @@ function CanvasSidebar({ children, title, logo, actions, activeKey, defaultWidth
25
25
  setResizing(false);
26
26
  }
27
27
  React.useEffect(() => {
28
- const resize = () => { finishResize(); setViewport(window.innerWidth); };
29
- window.addEventListener('resize', resize);
30
28
  window.addEventListener('blur', finishResize);
31
- return () => { window.removeEventListener('resize', resize); window.removeEventListener('blur', finishResize); finishResize(); };
29
+ return () => { window.removeEventListener('blur', finishResize); finishResize(); };
32
30
  }, []);
33
31
  React.useEffect(() => setMobileOpen(false), [activeKey]);
34
32
  React.useLayoutEffect(() => {
35
- const observer = new window.ResizeObserver(() => setMeasuredWidth(Math.round(sidebar.current.getBoundingClientRect().width)));
36
- observer.observe(sidebar.current);
37
- return () => observer.disconnect();
33
+ const aside = sidebar.current;
34
+ const container = aside?.parentElement || aside;
35
+ if (!aside || !container) return undefined;
36
+ const measure = () => {
37
+ const nextContainerWidth = Math.round(container.getBoundingClientRect().width);
38
+ const nextSidebarWidth = Math.round(aside.getBoundingClientRect().width);
39
+ setContainerWidth(current => current === nextContainerWidth ? current : nextContainerWidth);
40
+ setMeasuredWidth(current => current === nextSidebarWidth ? current : nextSidebarWidth);
41
+ };
42
+ measure();
43
+ const observer = window.ResizeObserver ? new window.ResizeObserver(measure) : null;
44
+ if (observer) {
45
+ observer.observe(container);
46
+ observer.observe(aside);
47
+ } else {
48
+ window.addEventListener('resize', measure);
49
+ }
50
+ return () => {
51
+ if (observer) observer.disconnect();
52
+ else window.removeEventListener('resize', measure);
53
+ };
38
54
  }, []);
39
55
 
40
- return <aside ref={sidebar} className="oy-nav-sidebar oy-nav-surface" data-collapsed={collapsed} data-mobile-open={mobileOpen}
56
+ return <aside ref={sidebar} className="oy-nav-sidebar oy-nav-surface" data-compact={mobile} data-collapsed={collapsed} data-mobile-open={mobileOpen}
41
57
  onClick={event => { if (mobile && event.target.closest('.oy-nav-menu .oy-nav-item')) setMobileOpen(false); }}
42
58
  style={{ '--oy-sidebar-width': width === null ? (typeof defaultWidth === 'number' ? `${defaultWidth}px` : defaultWidth) : `${clamp(width)}px`, '--oy-sidebar-max-width': `${maxWidth}px` }}>
43
59
  <style>{`
@@ -48,20 +64,16 @@ function CanvasSidebar({ children, title, logo, actions, activeKey, defaultWidth
48
64
  .oy-canvas-nav .oy-sidebar-resize { position: absolute; z-index: 2; top: 0; right: -4px; bottom: 0; width: 8px; cursor: col-resize; touch-action: none; }
49
65
  .oy-canvas-nav .oy-sidebar-resize:hover, .oy-canvas-nav .oy-sidebar-resize:focus-visible { background: var(--pod-nav-tab-line-selected-color, var(--color-brand1-6, #1677ff)); outline: 0; }
50
66
  .oy-canvas-nav .oy-sidebar-drag-cover { position: fixed; inset: 0; z-index: 9999; cursor: col-resize; user-select: none; }
51
- @media (min-width: 768px) {
52
- .oy-canvas-nav .oy-nav-sidebar[data-collapsed="true"] { width: var(--pod-nav-side-collapsed-width, 56px); }
53
- .oy-canvas-nav .oy-nav-sidebar[data-collapsed="true"] .oy-nav-brand { padding: 0; }
54
- .oy-canvas-nav .oy-nav-sidebar[data-collapsed="true"] .oy-nav-brand > :not(.oy-sidebar-toggle),
55
- .oy-canvas-nav .oy-nav-sidebar[data-collapsed="true"] .oy-sidebar-actions,
56
- .oy-canvas-nav .oy-nav-sidebar[data-collapsed="true"] .oy-nav-item[data-has-icon="true"] > span { display: none; }
57
- .oy-canvas-nav .oy-nav-sidebar[data-collapsed="true"] .oy-nav-item { justify-content: center; padding: 0 4px; }
58
- }
59
- @media (max-width: 767px) {
60
- .oy-canvas-nav .oy-nav-sidebar { width: 100%; border-right: 0; border-bottom: 1px solid var(--pod-nav-sub-divider-color, var(--color-line1-1, #ddd)); }
61
- .oy-canvas-nav .oy-nav-sidebar[data-mobile-open="false"] .oy-sidebar-items,
62
- .oy-canvas-nav .oy-nav-sidebar[data-mobile-open="false"] .oy-sidebar-actions { display: none; }
63
- .oy-canvas-nav .oy-sidebar-resize { display: none; }
64
- }
67
+ .oy-canvas-nav .oy-nav-sidebar[data-compact="false"][data-collapsed="true"] { width: var(--pod-nav-side-collapsed-width, 56px); }
68
+ .oy-canvas-nav .oy-nav-sidebar[data-compact="false"][data-collapsed="true"] .oy-nav-brand { padding: 0; }
69
+ .oy-canvas-nav .oy-nav-sidebar[data-compact="false"][data-collapsed="true"] .oy-nav-brand > :not(.oy-sidebar-toggle),
70
+ .oy-canvas-nav .oy-nav-sidebar[data-compact="false"][data-collapsed="true"] .oy-sidebar-actions,
71
+ .oy-canvas-nav .oy-nav-sidebar[data-compact="false"][data-collapsed="true"] .oy-nav-item[data-has-icon="true"] > span { display: none; }
72
+ .oy-canvas-nav .oy-nav-sidebar[data-compact="false"][data-collapsed="true"] .oy-nav-item { justify-content: center; padding: 0 4px; }
73
+ .oy-canvas-nav .oy-nav-sidebar[data-compact="true"] { width: 100%; border-right: 0; border-bottom: 1px solid var(--pod-nav-sub-divider-color, var(--color-line1-1, #ddd)); }
74
+ .oy-canvas-nav .oy-nav-sidebar[data-compact="true"][data-mobile-open="false"] .oy-sidebar-items,
75
+ .oy-canvas-nav .oy-nav-sidebar[data-compact="true"][data-mobile-open="false"] .oy-sidebar-actions { display: none; }
76
+ .oy-canvas-nav .oy-nav-sidebar[data-compact="true"] .oy-sidebar-resize { display: none; }
65
77
  `}</style>
66
78
  <div className="oy-nav-brand">{logo}{title ? <span>{title}</span> : null}
67
79
  <button className="oy-nav-item oy-sidebar-toggle" type="button" title={expanded ? '收起导航' : '展开导航'}
@@ -19,7 +19,7 @@ function CanvasNav({ items = [], activeKey, onSelect, title, logo, actions, chil
19
19
  .oy-nav-top .oy-nav-menu .oy-nav-item { min-height: var(--pod-nav-top-tab-height, 40px); padding: var(--pod-nav-top-tab-item-padding, 0 12px); max-width: var(--pod-nav-top-tab-item-max-width, 240px); }
20
20
  .oy-nav-top .oy-nav-menu .oy-nav-item > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
21
21
  .oy-nav-top .oy-nav-toggle { display: none; margin-left: auto; }
22
- @media (max-width: 767px) {
22
+ @container (max-width: 767px) {
23
23
  .oy-nav-top .oy-nav-header { gap: var(--pod-nav-logo-gap, 8px); }
24
24
  .oy-nav-top .oy-nav-toggle { display: flex; }
25
25
  .oy-nav-top .oy-nav-menu { order: 1; flex-basis: 100%; flex-direction: column; }
@@ -0,0 +1,104 @@
1
+ import React from 'react';
2
+
3
+ const DEFAULT_BREAKPOINTS = Object.freeze({ compact: 600, stacked: 768, wide: 1024 });
4
+
5
+ function getLayout(width, breakpoints) {
6
+ if (width < breakpoints.compact) return 'compact';
7
+ if (width < breakpoints.wide) return 'regular';
8
+ return 'wide';
9
+ }
10
+
11
+ /**
12
+ * Observe the actual content container instead of assuming it equals the browser viewport.
13
+ * Updates are coalesced with requestAnimationFrame and only committed when width/bucket changes.
14
+ */
15
+ function useContainerLayout(breakpoints = DEFAULT_BREAKPOINTS) {
16
+ const rootRef = React.useRef(null);
17
+ const frameRef = React.useRef(0);
18
+ const [measurement, setMeasurement] = React.useState({ width: 0, layout: 'regular' });
19
+
20
+ React.useLayoutEffect(() => {
21
+ const root = rootRef.current;
22
+ if (!root) return undefined;
23
+ let active = true;
24
+ const measure = () => {
25
+ if (!active) return;
26
+ if (frameRef.current) window.cancelAnimationFrame(frameRef.current);
27
+ frameRef.current = window.requestAnimationFrame(() => {
28
+ frameRef.current = 0;
29
+ if (!active) return;
30
+ const width = Math.round(root.getBoundingClientRect().width);
31
+ const layout = getLayout(width, breakpoints);
32
+ setMeasurement(current => current.width === width && current.layout === layout
33
+ ? current
34
+ : { width, layout });
35
+ });
36
+ };
37
+ measure();
38
+ const observer = window.ResizeObserver ? new window.ResizeObserver(measure) : null;
39
+ if (observer) observer.observe(root);
40
+ else window.addEventListener('resize', measure);
41
+ return () => {
42
+ active = false;
43
+ if (observer) observer.disconnect();
44
+ else window.removeEventListener('resize', measure);
45
+ if (frameRef.current) window.cancelAnimationFrame(frameRef.current);
46
+ };
47
+ }, [breakpoints.compact, breakpoints.wide]);
48
+
49
+ return { rootRef, ...measurement };
50
+ }
51
+
52
+ /**
53
+ * A reusable content shell for filters, primary/secondary panels, charts and wide tables.
54
+ * Business state stays above this shell, so crossing a breakpoint never remounts it.
55
+ */
56
+ function ResponsiveLayout({ filters, actions, primary, secondary, chart, table, breakpoints = DEFAULT_BREAKPOINTS }) {
57
+ const { rootRef, width, layout } = useContainerLayout(breakpoints);
58
+ return (
59
+ <section ref={rootRef} className="oy-responsive-root" data-layout={layout} data-container-width={width || undefined}>
60
+ <style>{`
61
+ .oy-responsive-root { container: oy-content / inline-size; min-width: 0; display: grid; gap: var(--oy-section-gap, 16px); color: var(--color-text1-4, #1f2329); }
62
+ .oy-responsive-root, .oy-responsive-root * { box-sizing: border-box; }
63
+ .oy-responsive-filters { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 220px), 1fr)); gap: 12px; align-items: end; min-width: 0; }
64
+ .oy-responsive-actions { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
65
+ .oy-responsive-panels { display: grid; grid-template-columns: minmax(0, 2fr) minmax(240px, 1fr); gap: 16px; align-items: stretch; min-width: 0; }
66
+ .oy-responsive-panel { min-width: 0; overflow-wrap: anywhere; }
67
+ .oy-responsive-chart { min-width: 0; min-height: 240px; }
68
+ .oy-responsive-table { min-width: 0; max-width: 100%; overflow-x: auto; overscroll-behavior-inline: contain; }
69
+ .oy-responsive-table > table { width: 100%; min-width: 720px; border-collapse: collapse; }
70
+ .oy-responsive-drawer-body { min-width: 0; container-type: inline-size; }
71
+ @container oy-content (max-width: ${breakpoints.stacked - 1}px) {
72
+ .oy-responsive-panels { grid-template-columns: minmax(0, 1fr); }
73
+ }
74
+ @container oy-content (max-width: ${breakpoints.compact - 1}px) {
75
+ .oy-responsive-filters { grid-template-columns: minmax(0, 1fr); }
76
+ .oy-responsive-actions > * { flex: 1 1 auto; min-width: 0; }
77
+ .oy-responsive-chart { min-height: 200px; }
78
+ }
79
+ `}</style>
80
+ <div className="oy-responsive-filters">{filters}<div className="oy-responsive-actions">{actions}</div></div>
81
+ <div className="oy-responsive-panels">
82
+ <div className="oy-responsive-panel">{primary}</div>
83
+ <aside className="oy-responsive-panel">{secondary}</aside>
84
+ </div>
85
+ <div className="oy-responsive-chart" data-chart-width={width || undefined}>{chart}</div>
86
+ <div className="oy-responsive-table" tabIndex={0} aria-label="可横向滚动的数据表格">{table}</div>
87
+ </section>
88
+ );
89
+ }
90
+
91
+ /** Example keeps filter state while the observed container crosses every breakpoint. */
92
+ export default function ResponsiveLayoutExample() {
93
+ const [keyword, setKeyword] = React.useState('');
94
+ return <ResponsiveLayout
95
+ filters={<label>关键词<input value={keyword} onChange={event => setKeyword(event.target.value)} /></label>}
96
+ actions={<><button type="button">查询</button><button type="button">清空</button></>}
97
+ primary={<div>主内容面板</div>}
98
+ secondary={<div>辅助信息与超长编号:OPENYIDA-RESPONSIVE-CONTAINER-CONTENT-IDENTIFIER</div>}
99
+ chart={<div role="img" aria-label="图表区域">图表应按 data-chart-width 调用自身 resize</div>}
100
+ table={<table><thead><tr><th>名称</th><th>状态</th><th>负责人</th><th>操作</th></tr></thead><tbody><tr><td>示例记录</td><td>进行中</td><td>当前用户</td><td><button type="button">查看</button></td></tr></tbody></table>}
101
+ />;
102
+ }
103
+
104
+ export { DEFAULT_BREAKPOINTS, ResponsiveLayout, getLayout, useContainerLayout };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openyida",
3
- "version": "2026.10.21",
3
+ "version": "2026.10.22",
4
4
  "description": "OpenYida CLI - 宜搭低代码 AI 开发工具(安装即用,零配置)",
5
5
  "bin": {
6
6
  "openyida": "bin/yida.js",
@@ -307,6 +307,7 @@ openyida copy
307
307
 
308
308
  ## 子技能目录
309
309
 
310
+ 已有流程的当前任务查询、流程审批转交或管理员转交,直接加载 \`yida-data-management\`,使用 \`data query process-tasks\` 和 \`data transfer task\`,并回读实际处理人。修改流程中的转交操作权限才使用 \`yida-process-rule\`。
310
311
  原生酷卡片仅在用户明确指定宜搭原生酷卡片(交互/置顶卡片)或继续已明确上下文时加载 \`yida-cool-card\`。它用 cardPageCode/cardTemplateId;CARD 资源和 dataCardDesigner 属于 \`yida-data-card\`,Canvas 展示卡片归页面。酷卡片 CLI 负责创建、读取与模板查询,返回官方入口由用户配置保存,再由 CLI 回读;不代替编辑器保存、发布或发送,不修改权益或官方卡片开关。
311
312
  原生数据卡片仅在用户显式指定宜搭原生数据卡片,或继续已明确的原生卡片操作时加载 \`yida-data-card\`。泛称卡片、指标卡、看板、投放或数据播报不触发;完整应用搭建也不自动添加该资源。自定义页面内的卡片随页面发布;页面虽有 CARD 引用,只改外层仍属页面任务,明确修改源卡片才进入原生卡片技能。
312
313
 
@@ -16,7 +16,7 @@ Fast / Plan 选定的主题和主色属于整个应用:导航、原生页、
16
16
 
17
17
  ## 本地使用问题与贡献反馈
18
18
 
19
- 用户遇到反复报错、不会提 Issue/PR、没有 GitHub 账号或没有 OpenYida 源码,先加载 `yida-feedback`。
19
+ 用户反馈 OpenYida 使用问题或体验建议、想贡献改进、不会提 Issue/PR,先加载 `yida-feedback`;没有 GitHub 账号或源码也可先整理材料。
20
20
  它负责基于当前证据分析根因、生成脱敏本地材料,并按用户授权提交 Issue 或个人 Fork PR。无账号先引导注册,实在不行提示“反馈到AI开发者群”。
21
21
  自动发现重复失败只建议并整理本地草稿,不自动公开提交。网页关联本地 Agent 异常先按下节诊断,需要反馈时再进入此技能。普通宜搭产品 VOC 清单仍用 `yida-voc`。
22
22
 
@@ -159,7 +159,7 @@ Fast / Plan 选定的主题和主色属于整个应用:导航、原生页、
159
159
  | --- | --- |
160
160
  | 从零搭一个完整应用/系统 | `yida-app`;先分析需求并完成首次搭建确认,再规划与搭建 |
161
161
  | 已有 app 但没有任何页面,需要补成完整系统 | `yida-app`;属于首次搭建,同样先询问搭建方式,复用已有 `appType` 补齐资源 |
162
- | OpenYida 反复报错、不会提 Issue/PR、没有账号或源码 | `yida-feedback`;先脱敏本地报告,个人 Fork 仅贡献公开 OpenYida 修复 |
162
+ | OpenYida 使用问题、体验建议、希望贡献改进或不会提 Issue/PR | `yida-feedback`;先整理脱敏材料,成功提交 Issue 后可选邀请 PR,贡献代码使用个人 Fork |
163
163
  | 读取钉钉在线文档正文 | `yida-document-markdown`,使用登录态接口获取 Markdown |
164
164
  | 按 taskUuid 读取钉钉听记 | `yida-tingji`,将听记任务 ID 原样传入命令 |
165
165
  | 将钉钉开放平台官方 API 接入宜搭或 Canvas | `yida-dingtalk-openapi`;密钥不进聊天,账号由用户在宜搭或本机 TTY 配置 |
@@ -180,6 +180,7 @@ Fast / Plan 选定的主题和主色属于整个应用:导航、原生页、
180
180
  | 流程设计器字段列表空白 | `yida-process-rule`,先用 `process fields` 检查字段来源 |
181
181
  | 查字段 ID / 保存 Schema 证据 | `yida-get-schema`;凡涉及 fieldId 的数据、流程、公式、页面代码先取证 |
182
182
  | 改表单数据记录 | `yida-data-management`,不是 `yida-create-form-page` |
183
+ | 查询流程当前任务、管理员转交已有流程任务 | `yida-data-management`;先查询 process-tasks,再 transfer task 并回读实际处理人 |
183
184
  | 查询指定应用/流程表单中当前审批人为某人的实例或条数 | `yida-data-management`;`data query process --current-approver <userId> [--count]`,仅统计当前登录人经服务端授权可见的实例 |
184
185
  | 配置或修改公式、字段计算、提交校验、节点筛选、报表计算 | 共用 `yida-formula`,按实际保存内容读取说明;查看字段公式、查参数和检查用 `yida-formula-evaluate` |
185
186
  | 提交后跨表写入/更新/删除 | 默认 `yida-integration`;用户明确要业务关联规则/高级函数时用 `yida-business-rule` |
@@ -11,7 +11,11 @@ description: 宜搭自定义页面开发规范,使用 `YidaCodeCanvas` 组件
11
11
 
12
12
  待办中心或审批任务区块只有在用户明确要求自定义实现,或经 ask_human 明确同意后才交接 `yida-task-center`;已有 Canvas、普通工作台需求或 Fast 模式不构成选择。未选择时保留原生能力,不直接手写任务中心。
13
13
 
14
- 布局实现读取 [共享响应式规则](../yida-design/references/responsive-layout.md):按实际容器宽度重排筛选栏和主辅面板,统一同组控件尺寸,宽屏按需等高、窄屏自然增长,宽表格局部滚动。断点变化保留原组件状态与业务行为,发布前按该文档验收。
14
+ 布局实现分三步,详细规则见 [共享响应式规则](../yida-design/references/responsive-layout.md):
15
+
16
+ 1. **读取设计**:从 `design.md` 读取当前页的首屏焦点、布局、主操作、响应式和验收要求。
17
+ 2. **实现布局**:优先用 CSS 按内容容器宽度重排。需要参考时执行 `openyida sample openyida-page-template canvas-responsive-layout`,按需复用与当前业务对应的布局片段;图表等组件需要读取实际宽度时,再复用测量 Hook。视口媒体查询用于浏览器视口或输入方式判断。
18
+ 3. **验证状态**:切换宽度前后保留原组件状态与业务行为,发布前按共享规则检查实际页面。
15
19
 
16
20
  已有页面的美化或 UI 优化,先进入 [ui-improve](../yida-design/sub_skill/ui-improve/SKILL.md)。只有当前视觉方案已通过 `ask_human` 确认且原状态备份可恢复,才修改源码;交接需包含获批 revision、范围、备份和功能契约。本轮仅按视觉差异修改,保留现有业务表达式、事件、数据绑定、URL、状态管理和打开方式,不套用下文的新页面默认规则改变业务。
17
21
 
@@ -5,7 +5,11 @@ description: JSX 自定义页面开发规范(React 16 平台 Jsx 组件、expo
5
5
 
6
6
  # JSX 自定义页面开发
7
7
 
8
- 布局实现读取 [共享响应式规则](../yida-design/references/responsive-layout.md):按实际容器宽度重排筛选栏和主辅面板,统一同组控件尺寸,宽屏按需等高、窄屏自然增长,宽表格局部滚动。使用原 JSX 运行时支持的样式能力,断点变化保留原组件状态与业务行为,发布前按该文档验收。
8
+ 布局实现分三步,详细规则见 [共享响应式规则](../yida-design/references/responsive-layout.md):
9
+
10
+ 1. **读取设计**:从 `design.md` 读取当前页的首屏焦点、布局、主操作、响应式和验收要求。
11
+ 2. **实现布局**:使用原 JSX 运行时支持的样式能力,按需复用与当前业务对应的布局片段,实现宽窄排列、控件对齐和局部滚动。
12
+ 3. **验证状态**:切换宽度前后保留原组件状态与业务行为,发布前按共享规则检查实际页面。
9
13
 
10
14
  已有页面美化先进入 [ui-improve](../yida-design/sub_skill/ui-improve/SKILL.md):视觉方案经 `ask_human` 确认、备份可恢复后才修改。沿用获批范围和功能契约,保留原 JSX 运行时、业务表达式、数据源、事件及入口行为;下文的新开发默认规则不用于顺手重构已有业务。不满意时由该流程恢复本轮前状态。
11
15
 
@@ -57,7 +61,7 @@ UI 组件及 JSX 展示结构允许替换,业务函数及业务状态实现不
57
61
  4. **默认 Tailwind 风格层 + native 控件 reset**:面向用户的自定义页面默认使用 Tailwind utility className 组织视觉层,并默认导入 Tailwind preflight;同时必须保留 `openyida-native-control-reset` 或等效页面级样式,覆盖 input/textarea/select/自定义下拉的 focus、appearance、font-weight 和 shadow,避免浏览器黑色粗边。reset 使用 `.oyd-page` 通用作用域时可以复用全局 id,但不能因为同名 style 已存在就跳过更新;如果页面使用 `.oyd-grade-page`、`.oyd-data-page` 等自定义作用域,style id 必须页面专属,避免多 native 页面切换时下拉选项和 SVG 勾选样式丢失。运行时脚本只允许使用已验证的 `g.alicdn.com` 或企业自托管地址,未配置有效地址时走内联兜底样式
58
62
  5. **DateField 时间戳格式**:日期字段值必须是时间戳(毫秒),不能是字符串
59
63
  6. **forceUpdate 后延迟操作 DOM**:`forceUpdate()` 后 DOM 不会立即更新,ECharts/Canvas/第三方组件初始化必须放入 `setTimeout` 或 `requestAnimationFrame`
60
- 7. **多端适配**:使用 `this.utils.isMobile()` 判断设备类型,适配 PC 和移动端
64
+ 7. **多端适配**:按实际内容容器宽度决定布局;`this.utils.isMobile()` 用于选择平台移动入口等设备相关行为。触控与键盘操作按当前设计及共享响应式规则实现。
61
65
  8. **输入法组合输入处理**:使用 `_isComposing` 标记配合 `compositionstart`/`compositionend` 事件,避免输入过程中触发提交
62
66
  9. **表单打开入口统一容器**:数据列表用 `workbench/{formUuid}?iframe=true`,禁止用 `formDetail` 冒充列表;新增/提交/查看详情入口统一封装为 `FormOpenContainer`。PC 端默认用半屏 `50vw` 抽屉 iframe 承载页面级隐藏导航的 `submission/{formUuid}?isRenderNav=false` 或 `formDetail/{formUuid}?formInstId=...&navConfig.layout=1180&isRenderNav=false`,提交页和详情页使用同一宽度规则;详情实例 ID 必须优先取 `row.formInstId`,缺失时禁用或提示,不打开空 `formInstId`;移动端才整页或新页打开原生表单页;不要在按钮里直接 `window.open`
63
67
  10. **Tabs 显隐控制**:下拉值变更后自动回退到第一个可见 Tab,内容区用 `display: none` 保留 DOM
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: yida-data-management
3
- description: 查询、新增、更新宜搭表单和流程数据,按当前审批人查询流程实例或统计条数,精确删除普通表单单条记录。支持日期为空、日期区间端点、多选数量、子表条件及服务端分页;表单使用 /v1/form/,流程使用 /v1/process/。
3
+ description: 查询、新增、更新宜搭表单和流程数据,按当前审批人查询流程实例或统计条数,精确删除普通表单单条记录,查询指定流程当前任务并执行管理员转交。支持日期为空、日期区间端点、多选数量、子表条件及服务端分页;表单使用 /v1/form/,流程使用 /v1/process/。
4
4
  ---
5
5
 
6
6
  # 数据管理
@@ -50,7 +50,7 @@ description: 查询、新增、更新宜搭表单和流程数据,按当前审
50
50
  ## 严格要求 (MUST DO)
51
51
 
52
52
  - 操作前先用 query 命令确认目标数据存在
53
- - 所有 create/update/delete/execute 写操作必须携带只读预检确认的资源名称和类型;CLI 校验不一致时停止,不得换目标重试
53
+ - 所有 create/update/delete/execute/transfer 写操作必须携带只读预检确认的资源名称和类型;CLI 校验不一致时停止,不得换目标重试
54
54
  - 新增记录、生成测试数据、批量导入或发起流程必须遵守上方“创建/录入数据强制闭环”
55
55
  - 批量操作单次不超过 30 条记录;多条实例必须逐条/分批执行 `openyida data create ...`
56
56
  - 删除普通表单记录前,必须先执行 `openyida data get form <appType> --inst-id <formInstId> --form-uuid <formUuid> --json`,核对返回实例 ID 与目标表单,并向用户展示记录数量、标题/关键字段和实例 ID。宿主支持并会在执行删除命令前触发 runtime permission/approval 时,直接发起一次带 `--confirm` 的受保护删除命令,由该唯一权限卡完成用户确认;无需且不得预先调用 `ask_human` 或其他业务确认工具。宿主不支持 runtime permission/approval 时,必须先通过 `ask_human` 或等价交互工具获得用户明确确认;只有用户确认后才能执行删除,用户拒绝或未确认时不得执行
@@ -98,6 +98,7 @@ description: 查询、新增、更新宜搭表单和流程数据,按当前审
98
98
  - "查询数据"、"新增记录"、"更新数据"
99
99
  - "查看表单实例"、"发起流程"
100
100
  - "录入数据"、"批量导入"、"查询待办任务"
101
+ - "查询流程当前任务"、"管理员转交"、"把这条流程转交给某人"
101
102
  - "按当前审批人统计"、"某人的待审批流程条数"、"查询指定审批人的流程实例"
102
103
 
103
104
  > ⚠️ 表单接口(`/v1/form/`)和流程接口(`/v1/process/`)不能混用。
@@ -216,6 +217,23 @@ openyida data query tasks <appType> --type todo|created|done|cc --normalized [--
216
217
 
217
218
  只读数量、流程详情和审批历史可独立使用 `task-center count / inspect`,无需选择自定义页面;返回含义与按需装配见[流程任务能力](../yida-canvas-data-binding/references/workflow-capabilities.md)。
218
219
 
220
+ ### 流程当前任务与管理员转交
221
+
222
+ ```bash
223
+ openyida data query process-tasks <appType> --process-inst-id <processInstanceId> --json
224
+ openyida data transfer task <appType> --process-inst-id <processInstanceId> --form-uuid <formUuid> --to-user-id <userId> --expect-form-name <真实表单名称> --expect-form-type process [--remark '用户说明的理由'] [--task-id <taskId>] --json
225
+ ```
226
+
227
+ - 这是管理员对已有流程任务的转交;审批同意/拒绝仍使用 `execute task`,配置转交操作权限使用 `yida-process-rule`,流程代理使用 `yida-agent-center`。
228
+ - 先查询流程确认实例及表单,再查询 `process-tasks` 展示任务、节点和实际处理人;明确接收人的真实 userId,名称有歧义时不能猜选。不要用“我的待办”代替指定流程的管理员任务查询。
229
+ - 用户未说明转交理由时,省略 `--remark`,CLI 固定写“转交”;用户说明了理由时,使用其原话传入 `--remark`。不要自行添加“按用户要求”等理由,也不要为缺少理由追问用户。
230
+ - `taskId` 来自任务接口 `content[].id`,CLI 同时输出字符串 `taskId`;禁止使用 `currentActivityInstances[].id`、`taskSequenceId`、`taskGroupId` 或从 URL 猜测任务 ID。
231
+ - CLI 会在写前重新查询任务:只有一个运行任务时可省略 `--task-id`,多个任务必须明确指定;任务已结束或不属于目标实例时停止。
232
+ - 完成以 `transferred=true && readbackVerified=true` 为准;`ALREADY_ASSIGNED` 表示任务已由接收人处理,本次没有转交写入。接口 `success=true/content=null` 仅代表受理,CLI 还必须回读确认实际处理人。
233
+ - 收到 `RESULT_UNKNOWN` / `UNVERIFIED`、提交超时或鉴权异常时,仅按错误 `details.nextStep` 只读回查,禁止自动重放转交。此规则优先于下方通用网络重试策略。
234
+
235
+ 接口、回读规则与错误恢复见 [流程任务查询与管理员转交](references/process-task-transfer.md)。
236
+
219
237
  ## 接口总览
220
238
 
221
239
  ### 表单实例
@@ -244,6 +262,8 @@ openyida data query tasks <appType> --type todo|created|done|cc --normalized [--
244
262
  | `updateInstance` | POST | 更新 |
245
263
  | `getOperationRecords` | GET | 审批记录 |
246
264
  | `executeTask` | POST | 执行任务 |
265
+ | `getInstTaskDatas` | GET | 管理员查询指定流程任务,使用 query/formProcInstData 路径 |
266
+ | `redirectTask` | POST | 管理员转交任务,使用 query/instance 路径,单次提交并回读 |
247
267
 
248
268
  ### 任务中心
249
269
 
@@ -396,11 +416,12 @@ openyida data create form APP_xxx FORM-商机表 --expect-form-name 商机表 --
396
416
  | 错误类型 | 默认处理策略 |
397
417
  |---------|-------------|
398
418
  | 命令执行失败 | 停止执行,向用户展示错误信息,询问是否重试或调整参数 |
419
+ | 管理员转交失败或结果未验证 | 禁止自动重放转交;按 details.nextStep 只读核查并报告 mutationAccepted/readbackVerified,后续写入须重新核对当前任务和用户意图 |
399
420
  | 参数缺失(appType/formUuid/fieldId 等) | 主动询问用户补充,或引导用户使用 `yida-get-schema` 获取 |
400
421
  | 权限不足 / 登录态失效 | 停止执行,提示用户执行 `openyida login` 重新登录 |
401
422
  | 字段 ID 无效 | 停止执行,引导用户执行 `openyida get-schema` 获取真实字段 ID |
402
423
  | 删除操作前 | 必须先展示操作摘要(数量 + 关键字段);宿主支持 runtime permission/approval 时直接发起一次受保护命令,不得预先调用 `ask_human`;宿主不支持时,必须先通过 `ask_human` 或等价交互工具获得用户明确确认 |
403
424
  | QPS 超限 | 降低请求频率,单次批量操作不超过 30 条,间隔 1 秒重试 |
404
425
  | 表单/流程接口混用 | 停止执行,提示用户检查接口类型(表单用 `/v1/form/`,流程用 `/v1/process/`) |
405
- | 网络超时 | 重试 1 次,仍失败则停止并提示用户检查网络 |
426
+ | 网络超时 | 查询可重试 1 次;管理员转交禁止自动重试提交,仅按 nextStep 只读回查,仍无法验证则报告结果未知 |
406
427
  | 未知错误 | 停止执行,完整展示错误信息,建议用户反馈问题 |
@@ -0,0 +1,53 @@
1
+ # 流程任务查询与管理员转交
2
+
3
+ 现有 `data query form/process` 列表接口保持不变。以下接口属于管理员任务操作,不使用浏览器 Cookie 或抓包中的 CSRF token;复用 CLI token session、环境 baseUrl 和受管任务授权。`byManager=true` 不授予管理员权限,平台仍校验当前身份。
4
+
5
+ ## 查询指定流程任务
6
+
7
+ `openyida data query process-tasks <appType> --process-inst-id <processInstanceId> --json`
8
+
9
+ - GET `/dingtalk/web/<appType>/query/formProcInstData/getInstTaskDatas.json`
10
+ - `_api=DataManage.getInstTasks`,`procInstId=<processInstanceId>`。
11
+ - 成功响应 `content` 为任务数组,空数组表示未查到任务;查询失败不能当成无任务。
12
+ - 每条任务 `processInstanceId` 必须等于目标实例,`id` 必须是正整数,重复 ID 或不合法结构停止操作。
13
+ - CLI 保留原始任务字段,并新增字符串 `taskId`,由 `id` 转换;不会解析任务 URL。
14
+
15
+ | 返回字段 | 含义 |
16
+ | --- | --- |
17
+ | `id` / `taskId` | 转交目标任务 ID |
18
+ | `status` | 仅 `RUNNING` 可转交 |
19
+ | `activityCode` / `groupName` | 所属节点 / 节点名称 |
20
+ | `actualActionerId` / `actualActionerDisplayName` | 当前实际处理人 |
21
+ | `originalActionerId` | 原始处理人,不用于验证转交后的接收人 |
22
+ | `taskGroupId` | 任务组标识,不是任务 ID |
23
+
24
+ ## 管理员转交
25
+
26
+ ```bash
27
+ openyida data transfer task APP_xxx --process-inst-id <processInstanceId> --form-uuid FORM-xxx --to-user-id <真实接收人userId> --expect-form-name <真实表单名称> --expect-form-type process --task-id <taskId> [--remark '用户说明的理由'] --json
28
+ ```
29
+
30
+ - POST `/dingtalk/web/<appType>/query/instance/redirectTask.json`。
31
+ - `_api=DataManage.redirectTask`;表单编码参数为 `taskId`、`procInstId`、`nowActionerId`(接收人)、`remark`、`byManager=true`。
32
+ - 用户未说明理由时,省略可选参数 `--remark`,CLI 默认备注为“转交”;用户说明理由时,原样传入 `--remark`,不添加前缀或自行编写理由。显式空备注或只有 `--remark` 没有值属于参数错误。
33
+ - 写前校验资源名称、类型及实例归属,再重查任务。只有一个运行任务时可省略 taskId;多个运行任务必须明确选择,禁止默认选择第一条。
34
+ - 不修改业务字段,不需要为转交准备 data-json/data-file 或字段映射;接收人必须来自用户明确指定或真实通讯录解析,不得编造。
35
+ - 接收人已是该任务的实际处理人时,返回 `ALREADY_ASSIGNED`、`mutationPerformed=false`,不提交转交。
36
+ - 单次提交,不因登录刷新、网络超时或空响应自动重放。`success=true/content=null` 是正常受理响应。
37
+
38
+ ## 回读与结果
39
+
40
+ 受理后立即重新查该流程任务。以下证据才算转交已验证:
41
+
42
+ 1. 原 taskId 仍为 RUNNING,且 `actualActionerId` 等于接收人;或
43
+ 2. 原任务不再运行,且同 `activityCode/taskGroupId` 出现唯一新增 RUNNING 任务,实际处理人为接收人。已有并行任务、仅 originalActionerId 匹配、旧任务消失但无新任务,都不足以证明成功。
44
+
45
+ | 结果 | 行为 |
46
+ | --- | --- |
47
+ | `TRANSFERRED` | `transferred=true`、`mutationAccepted=true`、`readbackVerified=true`;输出原 taskId 与 currentTaskId |
48
+ | `ALREADY_ASSIGNED` | `transferred=false`、`readbackVerified=true`,无需转交写入 |
49
+ | `REJECTED` | 平台明确拒绝,报告错误,核对权限与任务状态 |
50
+ | `RESULT_UNKNOWN` | 提交超时、鉴权异常、非 JSON 等;仅按 details.nextStep 只读回查,禁止自动重放 |
51
+ | `UNVERIFIED` | 已受理但回读失败或不能确认接收人;仅按 details.nextStep 只读回查,禁止自动重放 |
52
+
53
+ 不得把命令退出成功、旧任务消失、列表 actionerId 为空或仅接口受理当作转交验收通过。不能证明结果时如实报告,不改用一次性脚本或浏览器请求绕过 CLI。