@dsh-workbench/shared 1.1.8 → 1.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/blocker-table.d.ts +1 -0
- package/dist/api/blocker-table.js +1 -1
- package/dist/components/SelectField.css +0 -6
- package/dist/components/SelectField.d.ts +2 -3
- package/dist/components/SelectField.js +9 -30
- package/dist/components/SelectFieldNodes.d.ts +3 -1
- package/dist/components/SelectFieldNodes.js +2 -1
- package/dist/components/SelectFieldWheel.d.ts +9 -0
- package/dist/components/SelectFieldWheel.js +35 -0
- package/dist/config/constants.d.ts +4 -4
- package/dist/config/constants.js +4 -4
- package/dist/contract/capabilities.d.ts +29 -0
- package/dist/contract/capabilities.js +1 -0
- package/dist/contract/needs.d.ts +3 -3
- package/dist/contract/needs.js +1 -1
- package/dist/contract/plugin-source.d.ts +19 -0
- package/dist/contract/protocol/bundle-install.d.ts +3 -1
- package/dist/contract/protocol/bundle-install.js +3 -0
- package/dist/contract/protocol/market-index.d.ts +1 -1
- package/dist/contract/protocol/platform.d.ts +1 -1
- package/dist/contract/runner.d.ts +6 -0
- package/dist/contract/runner.js +1 -0
- package/dist/contract/unit.d.ts +4 -4
- package/dist/host/adapter-contract.d.ts +3 -0
- package/dist/host/adapter-contract.js +93 -0
- package/dist/host/adapter-set.js +6 -4
- package/dist/host/adapter-types.d.ts +1 -1
- package/dist/host/bundle-manager.d.ts +3 -0
- package/dist/host/bundle-manager.js +181 -0
- package/dist/host/cli.js +3 -1
- package/dist/host/config-sync.d.ts +48 -0
- package/dist/host/config-sync.js +342 -0
- package/dist/host/dsh-plugins.d.ts +5 -0
- package/dist/host/dsh-plugins.js +51 -0
- package/dist/host/extension.js +1 -1
- package/dist/host/flow/check.d.ts +9 -0
- package/dist/host/flow/check.js +179 -0
- package/dist/host/flow/expr.d.ts +30 -0
- package/dist/host/flow/expr.js +99 -0
- package/dist/host/flow/kit.d.ts +14 -0
- package/dist/host/flow/kit.js +13 -0
- package/dist/host/flow/run.d.ts +13 -0
- package/dist/host/flow/run.js +287 -0
- package/dist/host/flow/types.d.ts +188 -0
- package/dist/host/flow/types.js +1 -0
- package/dist/host/host-actions.d.ts +2 -0
- package/dist/host/host-actions.js +5 -0
- package/dist/host/host-kit.d.ts +19 -0
- package/dist/host/host-kit.js +14 -0
- package/dist/host/http.d.ts +2 -6
- package/dist/host/http.js +2 -2
- package/dist/host/installed-module.d.ts +23 -0
- package/dist/host/installed-module.js +264 -0
- package/dist/host/lib-install.d.ts +7 -9
- package/dist/host/lib-install.js +9 -12
- package/dist/host/lib-runners.d.ts +3 -5
- package/dist/host/lib-runners.js +2 -2
- package/dist/host/local-declaration.d.ts +7 -0
- package/dist/host/local-declaration.js +33 -0
- package/dist/host/module-root.d.ts +1 -0
- package/dist/host/module-root.js +5 -0
- package/dist/host/npm-registry.d.ts +10 -0
- package/dist/host/npm-registry.js +16 -0
- package/dist/host/opencli-runner.d.ts +2 -1
- package/dist/host/package-content.d.ts +7 -0
- package/dist/host/package-content.js +69 -0
- package/dist/host/package-kind.d.ts +13 -0
- package/dist/host/package-kind.js +51 -0
- package/dist/host/package-route.d.ts +31 -0
- package/dist/host/package-route.js +57 -0
- package/dist/host/paths.d.ts +11 -19
- package/dist/host/paths.js +16 -43
- package/dist/host/platform.js +1 -1
- package/dist/host/plugin-ledger.d.ts +8 -8
- package/dist/host/plugin-ledger.js +23 -16
- package/dist/host/plugin.d.ts +1 -2
- package/dist/host/plugin.js +4 -5
- package/dist/host/port-types.d.ts +42 -9
- package/dist/host/preflight.d.ts +3 -2
- package/dist/host/profile-create.d.ts +18 -0
- package/dist/host/profile-create.js +118 -0
- package/dist/host/profile-registries.d.ts +10 -0
- package/dist/host/profile-registries.js +150 -0
- package/dist/host/registry-declaration.d.ts +21 -0
- package/dist/host/registry-declaration.js +33 -0
- package/dist/host/release-probe.d.ts +34 -0
- package/dist/host/release-probe.js +88 -0
- package/dist/host/runner.d.ts +26 -0
- package/dist/host/runner.js +50 -0
- package/dist/host/shell.d.ts +0 -2
- package/dist/host/shell.js +0 -7
- package/dist/host/spec.js +1 -1
- package/dist/host/supply-index.d.ts +1 -0
- package/dist/host/supply-index.js +8 -7
- package/dist/host/supply-port.js +13 -3
- package/dist/host/unit-face.d.ts +2 -1
- package/dist/host/version.js +11 -6
- package/dist/host.d.ts +24 -6
- package/dist/host.js +23 -6
- package/dist/i18n/en.js +23 -24
- package/dist/i18n/index.d.ts +23 -24
- package/dist/i18n/runtime-terms.d.ts +15 -0
- package/dist/i18n/runtime-terms.js +22 -0
- package/dist/i18n/zh.d.ts +22 -23
- package/dist/i18n/zh.js +22 -23
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/testing/fake-adapter-set.d.ts +5 -0
- package/dist/testing/fake-adapter-set.js +150 -0
- package/dist/testing/needs.js +1 -1
- package/dist/testing.d.ts +1 -0
- package/dist/testing.js +1 -0
- package/package.json +1 -1
- package/src/api/__tests__/blocker-table.test.ts +25 -0
- package/src/api/{workbench-client.test.ts → __tests__/workbench-client.test.ts} +2 -2
- package/src/api/blocker-table.ts +6 -2
- package/src/components/SelectField.css +0 -6
- package/src/components/SelectField.tsx +7 -36
- package/src/components/SelectFieldNodes.tsx +4 -1
- package/src/components/{Card.test.tsx → __tests__/Card.test.tsx} +2 -2
- package/src/components/{CardAnchor.test.tsx → __tests__/CardAnchor.test.tsx} +3 -3
- package/src/components/{CardHead.test.tsx → __tests__/CardHead.test.tsx} +1 -1
- package/src/components/{ChoiceGroup.test.tsx → __tests__/ChoiceGroup.test.tsx} +2 -2
- package/src/components/{ConfirmBar.test.tsx → __tests__/ConfirmBar.test.tsx} +5 -5
- package/src/components/{Field.test.tsx → __tests__/Field.test.tsx} +1 -1
- package/src/components/{Form.test.tsx → __tests__/Form.test.tsx} +5 -5
- package/src/components/{FormActions.test.tsx → __tests__/FormActions.test.tsx} +3 -3
- package/src/components/{FormRows.test.tsx → __tests__/FormRows.test.tsx} +2 -2
- package/src/components/{FormTypes.test.ts → __tests__/FormTypes.test.ts} +1 -1
- package/src/components/{FormWiring.test.tsx → __tests__/FormWiring.test.tsx} +4 -4
- package/src/components/{Link.test.tsx → __tests__/Link.test.tsx} +1 -1
- package/src/components/{Rows.test.tsx → __tests__/Rows.test.tsx} +2 -2
- package/src/components/{SelectField.test.tsx → __tests__/SelectField.test.tsx} +7 -37
- package/src/components/{Spinner.test.tsx → __tests__/Spinner.test.tsx} +4 -4
- package/src/config/{constants.test.ts → __tests__/constants.test.ts} +1 -1
- package/src/config/{storage.test.ts → __tests__/storage.test.ts} +1 -1
- package/src/config/constants.ts +4 -4
- package/src/contract/capabilities.ts +30 -0
- package/src/contract/needs.ts +4 -4
- package/src/contract/plugin-source.ts +24 -0
- package/src/contract/protocol/bundle-install.ts +7 -1
- package/src/contract/protocol/market-index.ts +1 -0
- package/src/contract/protocol/platform.ts +1 -1
- package/src/contract/runner.ts +6 -0
- package/src/contract/unit.ts +4 -4
- package/src/host/{extension.test.ts → __tests__/extension.test.ts} +4 -4
- package/src/host/{install-failure.test.ts → __tests__/install-failure.test.ts} +1 -1
- package/src/host/{installed-sources.test.ts → __tests__/installed-sources.test.ts} +1 -1
- package/src/host/__tests__/lib-runners.test.ts +11 -2
- package/src/host/__tests__/plugin-ledger.test.ts +19 -6
- package/src/host/{recipe.test.ts → __tests__/recipe.test.ts} +3 -3
- package/src/host/__tests__/runtime-layout.test.ts +14 -0
- package/src/host/__tests__/supply-index.test.ts +2 -2
- package/src/host/__tests__/supply-port.test.ts +32 -0
- package/src/host/adapter-contract.ts +93 -0
- package/src/host/adapter-set.ts +6 -4
- package/src/host/adapter-types.ts +1 -1
- package/src/host/bundle-manager.ts +195 -0
- package/src/host/cli.ts +3 -1
- package/src/host/config-sync.ts +386 -0
- package/src/host/dsh-plugins.ts +60 -0
- package/src/host/extension.ts +1 -1
- package/src/host/flow/__tests__/advanced.test.ts +302 -0
- package/src/host/flow/__tests__/check.test.ts +169 -0
- package/src/host/flow/__tests__/run.test.ts +301 -0
- package/src/host/flow/check.ts +216 -0
- package/src/host/flow/expr.ts +94 -0
- package/src/host/flow/kit.ts +30 -0
- package/src/host/flow/run.ts +325 -0
- package/src/host/flow/types.ts +199 -0
- package/src/host/host-actions.ts +7 -0
- package/src/host/host-kit.ts +26 -0
- package/src/host/http.ts +6 -8
- package/src/host/installed-module.ts +272 -0
- package/src/host/lib-install.ts +14 -12
- package/src/host/lib-runners.ts +6 -8
- package/src/host/local-declaration.ts +37 -0
- package/src/host/module-root.ts +6 -0
- package/src/host/npm-registry.ts +21 -0
- package/src/host/package-content.ts +76 -0
- package/src/host/package-kind.ts +63 -0
- package/src/host/package-route.ts +93 -0
- package/src/host/paths.ts +27 -52
- package/src/host/platform.ts +1 -1
- package/src/host/plugin-ledger.ts +25 -18
- package/src/host/plugin.ts +4 -5
- package/src/host/port-types.ts +43 -9
- package/src/host/preflight.ts +3 -2
- package/src/host/profile-create.ts +133 -0
- package/src/host/profile-registries.ts +145 -0
- package/src/host/registry-declaration.ts +70 -0
- package/src/host/release-probe.ts +202 -0
- package/src/host/shell.ts +0 -9
- package/src/host/spec.ts +1 -1
- package/src/host/supply-index.ts +9 -7
- package/src/host/supply-port.ts +13 -3
- package/src/host/unit-face.ts +2 -1
- package/src/host/version.ts +12 -7
- package/src/host.ts +34 -6
- package/src/i18n/{index.test.ts → __tests__/index.test.ts} +3 -3
- package/src/i18n/{merge.test.ts → __tests__/merge.test.ts} +1 -1
- package/src/i18n/{zh.test.ts → __tests__/zh.test.ts} +2 -2
- package/src/i18n/en.ts +23 -25
- package/src/i18n/runtime-terms.ts +26 -0
- package/src/i18n/zh.ts +22 -24
- package/src/index.ts +4 -0
- package/src/lib/{board-fact.test.ts → __tests__/board-fact.test.ts} +1 -1
- package/src/lib/{busy.test.ts → __tests__/busy.test.ts} +1 -1
- package/src/lib/{form-problem.test.ts → __tests__/form-problem.test.ts} +1 -1
- package/src/lib/{git-address.test.ts → __tests__/git-address.test.ts} +1 -1
- package/src/lib/{react.test.ts → __tests__/react.test.ts} +1 -1
- package/src/lib/{text.test.ts → __tests__/text.test.ts} +1 -1
- package/src/lib/{ui.test.ts → __tests__/ui.test.ts} +1 -1
- package/src/testing/__tests__/fake-adapter-set.test.ts +28 -0
- package/src/testing/fake-adapter-set.ts +173 -0
- package/src/testing/needs.ts +1 -1
- package/src/testing.ts +1 -0
- package/src/host/__tests__/command-name.test.ts +0 -16
- package/src/host/__tests__/opencli-runner.test.ts +0 -120
- package/src/host/channel-plugin.ts +0 -55
- package/src/host/command-name.ts +0 -15
- package/src/host/opencli-runner.ts +0 -76
- package/src/host/paths.test.ts +0 -26
- package/src/host/plugin-source.ts +0 -137
- package/src/host/runtime-links.test.ts +0 -31
- package/src/host/runtime-links.ts +0 -23
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
function checkValue(value, path, context, add) {
|
|
2
|
+
if ('$lit' in value)
|
|
3
|
+
return;
|
|
4
|
+
if ('$default' in value) {
|
|
5
|
+
checkValue(value.$default[0], `${path}.$default[0]`, context, add);
|
|
6
|
+
checkValue(value.$default[1], `${path}.$default[1]`, context, add);
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
if (!('$ref' in value)) {
|
|
10
|
+
add(path, '不是合法的取值表达式');
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const ref = value.$ref.trim();
|
|
14
|
+
if (ref === '') {
|
|
15
|
+
add(path, '$ref 不能为空');
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const parts = ref.split('.').filter((part) => part !== '');
|
|
19
|
+
const root = parts[0];
|
|
20
|
+
if (root === undefined) {
|
|
21
|
+
add(path, '$ref 不能为空');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (root === 'in')
|
|
25
|
+
return;
|
|
26
|
+
if (root === 'steps') {
|
|
27
|
+
const id = parts[1];
|
|
28
|
+
if (id === undefined) {
|
|
29
|
+
add(path, '$ref 指向 steps 时要写到步骤 id');
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (!context.seen.has(id))
|
|
33
|
+
add(path, `$ref 引用了还没出现的步骤:${id}`);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (root === 'item') {
|
|
37
|
+
if (!context.item)
|
|
38
|
+
add(path, '$ref 的 item 只在 forEach 步骤里可用');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (root === 'value') {
|
|
42
|
+
if (!context.value)
|
|
43
|
+
add(path, '$ref 的 value 只在 poll.until 里可用');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
add(path, `$ref 只能从 in / steps / item / value 起头:${ref}`);
|
|
47
|
+
}
|
|
48
|
+
function checkPredicate(predicate, path, context, add) {
|
|
49
|
+
if ('$eq' in predicate) {
|
|
50
|
+
checkValue(predicate.$eq[0], `${path}.$eq[0]`, context, add);
|
|
51
|
+
checkValue(predicate.$eq[1], `${path}.$eq[1]`, context, add);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if ('$in' in predicate) {
|
|
55
|
+
checkValue(predicate.$in[0], `${path}.$in[0]`, context, add);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if ('$exists' in predicate) {
|
|
59
|
+
checkValue(predicate.$exists, `${path}.$exists`, context, add);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if ('$and' in predicate) {
|
|
63
|
+
predicate.$and.forEach((item, index) => {
|
|
64
|
+
checkPredicate(item, `${path}.$and[${index}]`, context, add);
|
|
65
|
+
});
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if ('$not' in predicate) {
|
|
69
|
+
checkPredicate(predicate.$not, `${path}.$not`, context, add);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
add(path, '不是合法的谓词');
|
|
73
|
+
}
|
|
74
|
+
function checkFlowAt(flow, actions, prefix, add) {
|
|
75
|
+
if (flow.id.trim() === '')
|
|
76
|
+
add(`${prefix}id`, '流程 id 不能为空');
|
|
77
|
+
if (flow.title.trim() === '')
|
|
78
|
+
add(`${prefix}title`, '流程标题不能为空');
|
|
79
|
+
if (flow.steps.length === 0)
|
|
80
|
+
add(`${prefix}steps`, '至少要有一步');
|
|
81
|
+
if (flow.target !== undefined) {
|
|
82
|
+
checkValue(flow.target, `${prefix}target`, { seen: new Set(), item: false, value: false }, add);
|
|
83
|
+
}
|
|
84
|
+
if (flow.stopWhen !== undefined) {
|
|
85
|
+
const all = new Set(flow.steps.map((step) => step.id));
|
|
86
|
+
checkPredicate(flow.stopWhen, `${prefix}stopWhen`, { seen: all, item: false, value: false }, add);
|
|
87
|
+
}
|
|
88
|
+
const seen = new Set();
|
|
89
|
+
flow.steps.forEach((step, index) => {
|
|
90
|
+
const at = `${prefix}steps[${index}]`;
|
|
91
|
+
if (step.id.trim() === '')
|
|
92
|
+
add(`${at}.id`, '步骤 id 不能为空');
|
|
93
|
+
if (seen.has(step.id))
|
|
94
|
+
add(`${at}.id`, `步骤 id 重复:${step.id}`);
|
|
95
|
+
if ('flow' in step) {
|
|
96
|
+
if (step.when !== undefined) {
|
|
97
|
+
checkPredicate(step.when, `${at}.when`, { seen, item: false, value: false }, add);
|
|
98
|
+
}
|
|
99
|
+
if (step.with !== undefined) {
|
|
100
|
+
for (const [key, value] of Object.entries(step.with)) {
|
|
101
|
+
checkValue(value, `${at}.with.${key}`, { seen, item: false, value: false }, add);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
checkFlowAt(step.flow, actions, `${at}.flow.`, add);
|
|
105
|
+
seen.add(step.id);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (step.action.trim() === '')
|
|
109
|
+
add(`${at}.action`, '动作名不能为空');
|
|
110
|
+
else if (!actions.has(step.action))
|
|
111
|
+
add(`${at}.action`, `注册表里没有这个动作:${step.action}`);
|
|
112
|
+
const item = step.forEach !== undefined;
|
|
113
|
+
if (step.when !== undefined) {
|
|
114
|
+
checkPredicate(step.when, `${at}.when`, { seen, item: false, value: false }, add);
|
|
115
|
+
}
|
|
116
|
+
if (step.with !== undefined) {
|
|
117
|
+
for (const [key, value] of Object.entries(step.with)) {
|
|
118
|
+
checkValue(value, `${at}.with.${key}`, { seen, item, value: false }, add);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (step.forEach !== undefined) {
|
|
122
|
+
checkValue(step.forEach.over, `${at}.forEach.over`, { seen, item: false, value: false }, add);
|
|
123
|
+
if (step.forEach.as.trim() === '')
|
|
124
|
+
add(`${at}.forEach.as`, 'forEach.as 不能为空');
|
|
125
|
+
if (!Number.isInteger(step.forEach.max) || step.forEach.max < 1 || step.forEach.max > 50) {
|
|
126
|
+
add(`${at}.forEach.max`, 'forEach.max 要在 1..50');
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (step.retry !== undefined) {
|
|
130
|
+
if (!Number.isInteger(step.retry.attempts) ||
|
|
131
|
+
step.retry.attempts < 0 ||
|
|
132
|
+
step.retry.attempts > 5) {
|
|
133
|
+
add(`${at}.retry.attempts`, 'retry.attempts 要在 0..5');
|
|
134
|
+
}
|
|
135
|
+
if (step.retry.delayMs !== undefined &&
|
|
136
|
+
(!Number.isInteger(step.retry.delayMs) ||
|
|
137
|
+
step.retry.delayMs < 0 ||
|
|
138
|
+
step.retry.delayMs > 60_000)) {
|
|
139
|
+
add(`${at}.retry.delayMs`, 'retry.delayMs 要在 0..60000');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (step.poll !== undefined) {
|
|
143
|
+
if (!Number.isInteger(step.poll.everyMs) ||
|
|
144
|
+
step.poll.everyMs < 100 ||
|
|
145
|
+
step.poll.everyMs > 60_000) {
|
|
146
|
+
add(`${at}.poll.everyMs`, 'poll.everyMs 要在 100..60000');
|
|
147
|
+
}
|
|
148
|
+
if (!Number.isInteger(step.poll.maxMs) ||
|
|
149
|
+
step.poll.maxMs < 0 ||
|
|
150
|
+
step.poll.maxMs > 1_800_000) {
|
|
151
|
+
add(`${at}.poll.maxMs`, 'poll.maxMs 要在 0..1800000');
|
|
152
|
+
}
|
|
153
|
+
else if (step.poll.maxMs !== 0 && step.poll.maxMs < step.poll.everyMs) {
|
|
154
|
+
add(`${at}.poll.maxMs`, 'poll.maxMs 不能小于 everyMs');
|
|
155
|
+
}
|
|
156
|
+
if (step.poll.onTimeout !== undefined &&
|
|
157
|
+
step.poll.onTimeout !== 'fail' &&
|
|
158
|
+
step.poll.onTimeout !== 'continue') {
|
|
159
|
+
add(`${at}.poll.onTimeout`, 'poll.onTimeout 只能是 fail 或 continue');
|
|
160
|
+
}
|
|
161
|
+
checkPredicate(step.poll.until, `${at}.poll.until`, { seen, item, value: true }, add);
|
|
162
|
+
}
|
|
163
|
+
seen.add(step.id);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* 校验一条流程:id / 步骤、动作存在、引用只指前面、重试与轮询在边界内、子流程也照查。
|
|
168
|
+
* 只做静态检查,不碰平台、不跑动作。
|
|
169
|
+
* @param flow 流程定义。
|
|
170
|
+
* @param actions 动作表(只问 `has`)。
|
|
171
|
+
* @returns 逐条问题;`ok` 为真表示没有。
|
|
172
|
+
*/
|
|
173
|
+
export function checkFlow(flow, actions) {
|
|
174
|
+
const problems = [];
|
|
175
|
+
checkFlowAt(flow, actions, '', (path, problem) => {
|
|
176
|
+
problems.push({ path, problem });
|
|
177
|
+
});
|
|
178
|
+
return { ok: problems.length === 0, problems };
|
|
179
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { FlowPredicate, FlowScope, FlowValue } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* 读一个引用路径:`in.env`、`steps.builds.rows.0.id`、`item.id`、`value.status`。
|
|
4
|
+
* 中间缺一段就给 `undefined`,由调用方决定这是不是错。
|
|
5
|
+
* @param path 点分路径。
|
|
6
|
+
* @param scope 求值现场。
|
|
7
|
+
* @returns 取到的值;路径不成立给 `undefined`。
|
|
8
|
+
*/
|
|
9
|
+
export declare function readRef(path: string, scope: FlowScope): unknown;
|
|
10
|
+
/**
|
|
11
|
+
* 求一个取值表达式:字面量、引用,或二选一。
|
|
12
|
+
* @param value 表达式。
|
|
13
|
+
* @param scope 求值现场。
|
|
14
|
+
* @returns 求出来的值;引用不成立给 `undefined`。
|
|
15
|
+
*/
|
|
16
|
+
export declare function resolveValue(value: FlowValue, scope: FlowScope): unknown;
|
|
17
|
+
/**
|
|
18
|
+
* 判一个条件表达式。只认五个操作符;未知形状给 `false`。
|
|
19
|
+
* @param predicate 条件表达式。
|
|
20
|
+
* @param scope 求值现场。
|
|
21
|
+
* @returns 真假。
|
|
22
|
+
*/
|
|
23
|
+
export declare function evalPredicate(predicate: FlowPredicate, scope: FlowScope): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* 把步骤的 `with` 映射求成动作入参。
|
|
26
|
+
* @param values 字段到表达式的映射;没给就是空对象。
|
|
27
|
+
* @param scope 求值现场。
|
|
28
|
+
* @returns 动作入参。
|
|
29
|
+
*/
|
|
30
|
+
export declare function resolveInput(values: Readonly<Record<string, FlowValue>> | undefined, scope: FlowScope): Readonly<Record<string, unknown>>;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
2
|
+
function fieldOf(value, key) {
|
|
3
|
+
if (Array.isArray(value)) {
|
|
4
|
+
const index = Number.parseInt(key, 10);
|
|
5
|
+
return Number.isInteger(index) && index >= 0 ? value[index] : undefined;
|
|
6
|
+
}
|
|
7
|
+
if (typeof value !== 'object' || value === null)
|
|
8
|
+
return undefined;
|
|
9
|
+
return value[key];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* 读一个引用路径:`in.env`、`steps.builds.rows.0.id`、`item.id`、`value.status`。
|
|
13
|
+
* 中间缺一段就给 `undefined`,由调用方决定这是不是错。
|
|
14
|
+
* @param path 点分路径。
|
|
15
|
+
* @param scope 求值现场。
|
|
16
|
+
* @returns 取到的值;路径不成立给 `undefined`。
|
|
17
|
+
*/
|
|
18
|
+
export function readRef(path, scope) {
|
|
19
|
+
const parts = path.split('.').filter((part) => part !== '');
|
|
20
|
+
const [root, ...rest] = parts;
|
|
21
|
+
if (root === undefined)
|
|
22
|
+
return undefined;
|
|
23
|
+
let current;
|
|
24
|
+
if (root === 'in')
|
|
25
|
+
current = scope.in;
|
|
26
|
+
else if (root === 'item')
|
|
27
|
+
current = scope.item;
|
|
28
|
+
else if (root === 'value')
|
|
29
|
+
current = scope.value;
|
|
30
|
+
else if (root === 'steps') {
|
|
31
|
+
const id = rest.shift();
|
|
32
|
+
if (id === undefined)
|
|
33
|
+
return undefined;
|
|
34
|
+
current = scope.steps[id];
|
|
35
|
+
}
|
|
36
|
+
else
|
|
37
|
+
return undefined;
|
|
38
|
+
for (const part of rest)
|
|
39
|
+
current = fieldOf(current, part);
|
|
40
|
+
return current;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 求一个取值表达式:字面量、引用,或二选一。
|
|
44
|
+
* @param value 表达式。
|
|
45
|
+
* @param scope 求值现场。
|
|
46
|
+
* @returns 求出来的值;引用不成立给 `undefined`。
|
|
47
|
+
*/
|
|
48
|
+
export function resolveValue(value, scope) {
|
|
49
|
+
if ('$lit' in value)
|
|
50
|
+
return value.$lit;
|
|
51
|
+
if ('$ref' in value)
|
|
52
|
+
return readRef(value.$ref, scope);
|
|
53
|
+
if ('$default' in value) {
|
|
54
|
+
const first = resolveValue(value.$default[0], scope);
|
|
55
|
+
return first === undefined ? resolveValue(value.$default[1], scope) : first;
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
function sameValue(left, right) {
|
|
60
|
+
return isDeepStrictEqual(left, right);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* 判一个条件表达式。只认五个操作符;未知形状给 `false`。
|
|
64
|
+
* @param predicate 条件表达式。
|
|
65
|
+
* @param scope 求值现场。
|
|
66
|
+
* @returns 真假。
|
|
67
|
+
*/
|
|
68
|
+
export function evalPredicate(predicate, scope) {
|
|
69
|
+
if ('$eq' in predicate) {
|
|
70
|
+
return sameValue(resolveValue(predicate.$eq[0], scope), resolveValue(predicate.$eq[1], scope));
|
|
71
|
+
}
|
|
72
|
+
if ('$in' in predicate) {
|
|
73
|
+
const needle = resolveValue(predicate.$in[0], scope);
|
|
74
|
+
return predicate.$in[1].some((candidate) => sameValue(candidate, needle));
|
|
75
|
+
}
|
|
76
|
+
if ('$exists' in predicate) {
|
|
77
|
+
const value = resolveValue(predicate.$exists, scope);
|
|
78
|
+
return value !== undefined && value !== null;
|
|
79
|
+
}
|
|
80
|
+
if ('$and' in predicate)
|
|
81
|
+
return predicate.$and.every((item) => evalPredicate(item, scope));
|
|
82
|
+
if ('$not' in predicate)
|
|
83
|
+
return !evalPredicate(predicate.$not, scope);
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* 把步骤的 `with` 映射求成动作入参。
|
|
88
|
+
* @param values 字段到表达式的映射;没给就是空对象。
|
|
89
|
+
* @param scope 求值现场。
|
|
90
|
+
* @returns 动作入参。
|
|
91
|
+
*/
|
|
92
|
+
export function resolveInput(values, scope) {
|
|
93
|
+
if (values === undefined)
|
|
94
|
+
return {};
|
|
95
|
+
const input = {};
|
|
96
|
+
for (const [key, value] of Object.entries(values))
|
|
97
|
+
input[key] = resolveValue(value, scope);
|
|
98
|
+
return input;
|
|
99
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { FlowActionRegistry, FlowCheck, FlowDefinition, FlowResult, FlowRunOptions } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* 流程引擎的宿主 kit:模块从 `mount(deps).flow` 拿,不直接 import shared。
|
|
4
|
+
*/
|
|
5
|
+
export interface FlowHost {
|
|
6
|
+
createActions(): FlowActionRegistry;
|
|
7
|
+
check(flow: FlowDefinition, actions: Pick<FlowActionRegistry, 'has'>): FlowCheck;
|
|
8
|
+
run(options: FlowRunOptions): Promise<FlowResult>;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* 建一份无状态的流程宿主 kit。
|
|
12
|
+
* @returns `createActions` / `check` / `run`。
|
|
13
|
+
*/
|
|
14
|
+
export declare function createFlowHost(): FlowHost;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { checkFlow } from './check.js';
|
|
2
|
+
import { createFlowActionRegistry, runFlow } from './run.js';
|
|
3
|
+
/**
|
|
4
|
+
* 建一份无状态的流程宿主 kit。
|
|
5
|
+
* @returns `createActions` / `check` / `run`。
|
|
6
|
+
*/
|
|
7
|
+
export function createFlowHost() {
|
|
8
|
+
return {
|
|
9
|
+
createActions: () => createFlowActionRegistry(),
|
|
10
|
+
check: (flow, actions) => checkFlow(flow, actions),
|
|
11
|
+
run: (options) => runFlow(options),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { FlowActionRegistry, FlowResult, FlowRunOptions } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* 建一个内存里的动作注册表;每个流程运行时各有一份,没有模块级状态。
|
|
4
|
+
* @returns 注册表。
|
|
5
|
+
*/
|
|
6
|
+
export declare function createFlowActionRegistry(): FlowActionRegistry;
|
|
7
|
+
/**
|
|
8
|
+
* 顺序跑一条流程:条件跳过、重试、轮询、逐项展开、子流程、取消;失败停在具体步骤。
|
|
9
|
+
* 不碰平台、不落盘;动作怎么实现由注册表决定。
|
|
10
|
+
* @param options 流程、输入、动作表、取消信号、身份与进度回调。
|
|
11
|
+
* @returns 成功 / 失败 / 停止;失败带 `failedStep`。
|
|
12
|
+
*/
|
|
13
|
+
export declare function runFlow(options: FlowRunOptions): Promise<FlowResult>;
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { evalPredicate, resolveInput, resolveValue } from './expr.js';
|
|
2
|
+
class StepFailure extends Error {
|
|
3
|
+
stepId;
|
|
4
|
+
constructor(stepId, cause) {
|
|
5
|
+
super(cause instanceof Error ? cause.message : String(cause));
|
|
6
|
+
this.name = 'StepFailure';
|
|
7
|
+
this.stepId = stepId;
|
|
8
|
+
this.cause = cause;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function abortError() {
|
|
12
|
+
const error = new Error('流程已停止');
|
|
13
|
+
error.name = 'AbortError';
|
|
14
|
+
return error;
|
|
15
|
+
}
|
|
16
|
+
function isAbortError(error) {
|
|
17
|
+
return error instanceof Error && error.name === 'AbortError';
|
|
18
|
+
}
|
|
19
|
+
function defaultSleep(ms, signal) {
|
|
20
|
+
return new Promise((resolve) => {
|
|
21
|
+
if (signal.aborted) {
|
|
22
|
+
resolve();
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const timer = setTimeout(() => {
|
|
26
|
+
signal.removeEventListener('abort', onAbort);
|
|
27
|
+
resolve();
|
|
28
|
+
}, ms);
|
|
29
|
+
function onAbort() {
|
|
30
|
+
clearTimeout(timer);
|
|
31
|
+
resolve();
|
|
32
|
+
}
|
|
33
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
function targetOf(flow, scope) {
|
|
37
|
+
if (flow.target === undefined)
|
|
38
|
+
return flow.id;
|
|
39
|
+
const value = resolveValue(flow.target, scope);
|
|
40
|
+
return typeof value === 'string' && value.trim() !== '' ? value.trim() : flow.id;
|
|
41
|
+
}
|
|
42
|
+
function stopped(steps, target) {
|
|
43
|
+
return { status: 'stopped', target, steps };
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* 建一个内存里的动作注册表;每个流程运行时各有一份,没有模块级状态。
|
|
47
|
+
* @returns 注册表。
|
|
48
|
+
*/
|
|
49
|
+
export function createFlowActionRegistry() {
|
|
50
|
+
const entries = new Map();
|
|
51
|
+
return {
|
|
52
|
+
register(name, action, meta) {
|
|
53
|
+
const trimmed = name.trim();
|
|
54
|
+
if (trimmed === '')
|
|
55
|
+
throw new Error('动作名不能为空');
|
|
56
|
+
entries.set(trimmed, { name: trimmed, action, meta: meta ?? {} });
|
|
57
|
+
},
|
|
58
|
+
get(name) {
|
|
59
|
+
return entries.get(name);
|
|
60
|
+
},
|
|
61
|
+
has(name) {
|
|
62
|
+
return entries.has(name);
|
|
63
|
+
},
|
|
64
|
+
list() {
|
|
65
|
+
return [...entries.values()];
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
async function callWithRetry(action, input, context, retry, sleep, signal, onAttempt) {
|
|
70
|
+
const attempts = Math.max(0, Math.min(5, Math.trunc(retry?.attempts ?? 0)));
|
|
71
|
+
const delayMs = Math.max(0, Math.min(60_000, Math.trunc(retry?.delayMs ?? 0)));
|
|
72
|
+
let last;
|
|
73
|
+
for (let attempt = 1; attempt <= attempts + 1; attempt += 1) {
|
|
74
|
+
if (signal.aborted)
|
|
75
|
+
throw abortError();
|
|
76
|
+
onAttempt?.(attempt);
|
|
77
|
+
try {
|
|
78
|
+
return await action(input, context);
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
last = error;
|
|
82
|
+
if (signal.aborted)
|
|
83
|
+
throw abortError();
|
|
84
|
+
if (attempt > attempts)
|
|
85
|
+
break;
|
|
86
|
+
if (delayMs > 0)
|
|
87
|
+
await sleep(delayMs, signal);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
throw last;
|
|
91
|
+
}
|
|
92
|
+
async function runOneInner(options) {
|
|
93
|
+
const { step, title, stepId, scope, actions, signal, runId, workspace, sleep, now, emit } = options;
|
|
94
|
+
const entry = actions.get(step.action);
|
|
95
|
+
if (entry === undefined)
|
|
96
|
+
throw new Error(`注册表里没有这个动作:${step.action}`);
|
|
97
|
+
const input = resolveInput(step.with, scope);
|
|
98
|
+
const context = {
|
|
99
|
+
signal,
|
|
100
|
+
runId,
|
|
101
|
+
workspace,
|
|
102
|
+
stepId,
|
|
103
|
+
log: (text) => {
|
|
104
|
+
emit({ stepId, title, status: 'running', message: text });
|
|
105
|
+
},
|
|
106
|
+
login: (url) => {
|
|
107
|
+
emit({ stepId, title, status: 'needs-login', loginUrl: url });
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
if (step.poll !== undefined) {
|
|
111
|
+
const everyMs = Math.max(100, Math.min(60_000, Math.trunc(step.poll.everyMs)));
|
|
112
|
+
const rawMax = Math.trunc(step.poll.maxMs);
|
|
113
|
+
const maxMs = rawMax === 0 ? 0 : Math.max(everyMs, Math.min(1_800_000, rawMax));
|
|
114
|
+
const startedAt = now();
|
|
115
|
+
let pollAttempt = 0;
|
|
116
|
+
for (;;) {
|
|
117
|
+
if (signal.aborted)
|
|
118
|
+
throw abortError();
|
|
119
|
+
pollAttempt += 1;
|
|
120
|
+
emit({
|
|
121
|
+
stepId,
|
|
122
|
+
title,
|
|
123
|
+
status: 'running',
|
|
124
|
+
attempt: pollAttempt,
|
|
125
|
+
message: `第 ${pollAttempt} 次`,
|
|
126
|
+
});
|
|
127
|
+
const value = await callWithRetry(entry.action, input, context, step.retry, sleep, signal);
|
|
128
|
+
if (evalPredicate(step.poll.until, { ...scope, value }))
|
|
129
|
+
return value;
|
|
130
|
+
if (maxMs > 0 && now() - startedAt >= maxMs) {
|
|
131
|
+
if (step.poll.onTimeout === 'continue')
|
|
132
|
+
return undefined;
|
|
133
|
+
throw new Error(`步骤 ${step.id} 轮询超时(${maxMs}ms)`);
|
|
134
|
+
}
|
|
135
|
+
await sleep(everyMs, signal);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return callWithRetry(entry.action, input, context, step.retry, sleep, signal, (attempt) => {
|
|
139
|
+
emit({ stepId, title, status: 'running', attempt });
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
async function runOne(options) {
|
|
143
|
+
try {
|
|
144
|
+
return await runOneInner(options);
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
if (error instanceof StepFailure || isAbortError(error))
|
|
148
|
+
throw error;
|
|
149
|
+
throw new StepFailure(options.stepId, error);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* 顺序跑一条流程:条件跳过、重试、轮询、逐项展开、子流程、取消;失败停在具体步骤。
|
|
154
|
+
* 不碰平台、不落盘;动作怎么实现由注册表决定。
|
|
155
|
+
* @param options 流程、输入、动作表、取消信号、身份与进度回调。
|
|
156
|
+
* @returns 成功 / 失败 / 停止;失败带 `failedStep`。
|
|
157
|
+
*/
|
|
158
|
+
export async function runFlow(options) {
|
|
159
|
+
const { flow, input, actions, signal, runId, workspace, onStep } = options;
|
|
160
|
+
const sleep = options.sleep ?? defaultSleep;
|
|
161
|
+
const now = options.now ?? (() => Date.now());
|
|
162
|
+
const steps = {};
|
|
163
|
+
const emit = (event) => {
|
|
164
|
+
try {
|
|
165
|
+
onStep(event);
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
const target = targetOf(flow, { in: input, steps });
|
|
171
|
+
const shouldStop = () => flow.stopWhen !== undefined && evalPredicate(flow.stopWhen, { in: input, steps });
|
|
172
|
+
if (shouldStop())
|
|
173
|
+
return { status: 'ok', target, steps };
|
|
174
|
+
for (const step of flow.steps) {
|
|
175
|
+
if (signal.aborted)
|
|
176
|
+
return stopped(steps, target);
|
|
177
|
+
const title = step.title ?? step.id;
|
|
178
|
+
try {
|
|
179
|
+
if (shouldStop())
|
|
180
|
+
return { status: 'ok', target, steps };
|
|
181
|
+
if (step.when !== undefined && !evalPredicate(step.when, { in: input, steps })) {
|
|
182
|
+
emit({ stepId: step.id, title, status: 'skipped' });
|
|
183
|
+
if (shouldStop())
|
|
184
|
+
return { status: 'ok', target, steps };
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if ('flow' in step) {
|
|
188
|
+
emit({ stepId: step.id, title, status: 'running' });
|
|
189
|
+
const subInput = resolveInput(step.with, { in: input, steps });
|
|
190
|
+
const nested = await runFlow({
|
|
191
|
+
flow: step.flow,
|
|
192
|
+
input: subInput,
|
|
193
|
+
actions,
|
|
194
|
+
signal,
|
|
195
|
+
runId: `${runId}/${step.id}`,
|
|
196
|
+
workspace,
|
|
197
|
+
onStep: (event) => {
|
|
198
|
+
emit({
|
|
199
|
+
...event,
|
|
200
|
+
stepId: `${step.id}.${event.stepId}`,
|
|
201
|
+
title: `${title} / ${event.title}`,
|
|
202
|
+
});
|
|
203
|
+
},
|
|
204
|
+
sleep,
|
|
205
|
+
now,
|
|
206
|
+
});
|
|
207
|
+
if (nested.status === 'failed') {
|
|
208
|
+
const failed = nested.failedStep === undefined ? step.id : `${step.id}.${nested.failedStep}`;
|
|
209
|
+
throw new StepFailure(failed, nested.error ?? new Error(`子流程失败在 ${failed}`));
|
|
210
|
+
}
|
|
211
|
+
steps[step.id] = nested.steps;
|
|
212
|
+
if (nested.status === 'stopped')
|
|
213
|
+
return stopped(steps, target);
|
|
214
|
+
emit({ stepId: step.id, title, status: 'ok' });
|
|
215
|
+
if (shouldStop())
|
|
216
|
+
return { status: 'ok', target, steps };
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (step.forEach !== undefined) {
|
|
220
|
+
const over = resolveValue(step.forEach.over, { in: input, steps });
|
|
221
|
+
if (!Array.isArray(over)) {
|
|
222
|
+
throw new Error(`步骤 ${step.id} 的 forEach.over 不是数组`);
|
|
223
|
+
}
|
|
224
|
+
const max = Math.max(1, Math.min(50, Math.trunc(step.forEach.max)));
|
|
225
|
+
if (over.length > max) {
|
|
226
|
+
throw new Error(`步骤 ${step.id} 的 forEach 有 ${over.length} 项,超过上限 ${max}`);
|
|
227
|
+
}
|
|
228
|
+
const output = [];
|
|
229
|
+
for (const [index, item] of over.entries()) {
|
|
230
|
+
if (signal.aborted)
|
|
231
|
+
return stopped(steps, target);
|
|
232
|
+
output.push(await runOne({
|
|
233
|
+
step,
|
|
234
|
+
title,
|
|
235
|
+
stepId: `${step.id}[${index}]`,
|
|
236
|
+
scope: { in: input, steps, item },
|
|
237
|
+
actions,
|
|
238
|
+
signal,
|
|
239
|
+
runId,
|
|
240
|
+
workspace,
|
|
241
|
+
sleep,
|
|
242
|
+
now,
|
|
243
|
+
emit,
|
|
244
|
+
}));
|
|
245
|
+
}
|
|
246
|
+
steps[step.id] = output;
|
|
247
|
+
emit({ stepId: step.id, title, status: 'ok' });
|
|
248
|
+
if (shouldStop())
|
|
249
|
+
return { status: 'ok', target, steps };
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
steps[step.id] = await runOne({
|
|
253
|
+
step,
|
|
254
|
+
title,
|
|
255
|
+
stepId: step.id,
|
|
256
|
+
scope: { in: input, steps },
|
|
257
|
+
actions,
|
|
258
|
+
signal,
|
|
259
|
+
runId,
|
|
260
|
+
workspace,
|
|
261
|
+
sleep,
|
|
262
|
+
now,
|
|
263
|
+
emit,
|
|
264
|
+
});
|
|
265
|
+
emit({ stepId: step.id, title, status: 'ok' });
|
|
266
|
+
if (shouldStop())
|
|
267
|
+
return { status: 'ok', target, steps };
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
if (signal.aborted || isAbortError(error))
|
|
271
|
+
return stopped(steps, target);
|
|
272
|
+
if (error instanceof StepFailure) {
|
|
273
|
+
emit({ stepId: error.stepId, title, status: 'failed', message: error.message });
|
|
274
|
+
return {
|
|
275
|
+
status: 'failed',
|
|
276
|
+
target,
|
|
277
|
+
steps,
|
|
278
|
+
failedStep: error.stepId,
|
|
279
|
+
error: error.cause ?? error,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
emit({ stepId: step.id, title, status: 'failed', message: String(error) });
|
|
283
|
+
return { status: 'failed', target, steps, failedStep: step.id, error };
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return { status: 'ok', target, steps };
|
|
287
|
+
}
|