nodejs-store 2.6.0 → 3.0.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.
- package/README.md +157 -0
- package/README.zh-CN.md +147 -0
- package/llms-full.txt +71 -0
- package/llms.txt +1 -0
- package/package.json +2 -2
- package/src/ask.js +358 -0
- package/src/ask_knowledge.md +250 -0
- package/src/ddl.js +182 -1
- package/src/feedback.js +6 -1
- package/src/index.js +95 -25
- package/src/llm.js +157 -0
- package/src/permission.js +75 -0
- package/src/profile.js +35 -0
- package/src/schema.js +37 -2
- package/src/workflow.js +734 -0
package/src/workflow.js
ADDED
|
@@ -0,0 +1,734 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 工作流编排(首批:线性步骤 + when 守卫 + fail-fast)
|
|
5
|
+
*
|
|
6
|
+
* 设计见 `common-store/工作流编排设计文档.md`。两条铁律:
|
|
7
|
+
* - 铁律 A(定义即数据):Workflow defn 是与 schema defn 同构的纯 JSON——同注册模式、
|
|
8
|
+
* 同校验文化(白名单外显式 Err,文案含 `WORKFLOW_UNSUPPORTED`)、权限三级白名单内嵌 defn;
|
|
9
|
+
* - 铁律 B(运行即数据):run 记录落库为内建 schema `__workflowRun`,用户可用普通 GQL
|
|
10
|
+
* 查询失败/输入/步骤迹——可观测性零新接口。
|
|
11
|
+
*
|
|
12
|
+
* 引擎落在宿主层、core 零改动(执行全是 IO;校验与执行共用同一套步骤语义)。
|
|
13
|
+
* 首批明确不做(检出即 Err,不静默降级):循环 / 并行 / 子工作流 / 人工审批 / 自动重试 /
|
|
14
|
+
* 自动补偿 / 步骤级宿主回调 / gql 内嵌占位符(参数化请走 params 绑定)/ 数组下标路径。
|
|
15
|
+
* 对齐 `py-store/src/py_store/workflow.py`(双宿主输出逐字节一致由 parity 锚单测守护)。
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const {
|
|
19
|
+
core: _core,
|
|
20
|
+
has: _hasSchema,
|
|
21
|
+
register: _registerSchema,
|
|
22
|
+
requireContext: _requireContext,
|
|
23
|
+
} = require('./schema');
|
|
24
|
+
const crud = require('./crud');
|
|
25
|
+
const { emit: _emitFeedback } = require('./feedback');
|
|
26
|
+
const { getContext, _als } = require('./permission');
|
|
27
|
+
const { _call, sourcesOf: _sourcesOf, runAtomic } = require('./crud/exec');
|
|
28
|
+
const { _newIdPool } = require('./crud/id');
|
|
29
|
+
|
|
30
|
+
// ─── 白名单(首批边界;白名单外注册即 Err,不猜测语义) ──────────
|
|
31
|
+
|
|
32
|
+
// 步骤类型白名单(§4.2)
|
|
33
|
+
const _OPS = ['query', 'mutation', 'fail'];
|
|
34
|
+
// when 算子白名单(§4.2)→ 右操作数参数键(exists 的参数键 is;is/eq/ne/lte/gt/gte 统一 than)
|
|
35
|
+
const _WHEN_PARAM = {
|
|
36
|
+
exists: 'is', is: 'than', eq: 'than',
|
|
37
|
+
ne: 'than', lt: 'than', lte: 'than', gt: 'than', gte: 'than',
|
|
38
|
+
};
|
|
39
|
+
// defn 顶层字段白名单(§4.1)
|
|
40
|
+
const _TOP_FIELDS = new Set(['name', 'steps', 'read', 'write', 'run', 'description']);
|
|
41
|
+
// 步骤字段白名单(按 op;as 语义见 §4.2——query 必填,mutation 可选,fail 无)
|
|
42
|
+
const _STEP_FIELDS = {
|
|
43
|
+
query: new Set(['op', 'as', 'gql', 'params', 'when']),
|
|
44
|
+
mutation: new Set(['op', 'as', 'model', 'data', 'match', 'upsert', 'when']),
|
|
45
|
+
fail: new Set(['op', 'message', 'when']),
|
|
46
|
+
};
|
|
47
|
+
// 步骤必填字段
|
|
48
|
+
const _STEP_REQUIRED = { query: ['as', 'gql'], mutation: ['model', 'data'], fail: ['message'] };
|
|
49
|
+
|
|
50
|
+
// 占位符(§4.3):整值形态(允许一层嵌套——dec 参数;dec 嵌套 dec 注册即 Err)
|
|
51
|
+
const _FULL_PH = /^\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\}$/;
|
|
52
|
+
const _INNER_PH = /\{\{([^{}]*)\}\}/g;
|
|
53
|
+
// 外层完整占位符(含一层 dec 嵌套)——校验扫描用:dec 形态错误只有在外层可见时才拦得住
|
|
54
|
+
const _OUTER_PH = /\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\}/g;
|
|
55
|
+
const _GQL_PH = /\{\{/;
|
|
56
|
+
const _NUM_SEG = /^\d+$/;
|
|
57
|
+
const _NUM_LIT = /^-?\d+(\.\d+)?$/;
|
|
58
|
+
const _DEC = 'dec:';
|
|
59
|
+
const _INPUT = 'input.';
|
|
60
|
+
|
|
61
|
+
// run 终态状态机(§5):running → succeeded | failed | rejected;dry-run 终态 drySucceeded | dryFailed
|
|
62
|
+
const _BUILTIN_NAME = '__workflowRun';
|
|
63
|
+
|
|
64
|
+
// 内建 run 表 schema(§5)。write 显式空名单(R2:core 对显式 [] 拒绝一切写,
|
|
65
|
+
// super_admin/admin/internal 保留——实测空白名单写被普通角色放行,不收紧则 run 审计
|
|
66
|
+
// 可被 GQL 篡改);read 缺省 = 跟随既有全局语义(可读可观测,铁律 B 自举查询)。
|
|
67
|
+
// 模块内部写入走干净 internal 上下文(不含触发者 roles:core 对 guest 硬拒先于
|
|
68
|
+
// internal 放行——permission.rs can_write_schema,故不能复用保留 roles 的 runAsInternal)。
|
|
69
|
+
// now 为 number(毫秒,与 mutation 的 now 同型同源)——设计文档 §5 写 string,
|
|
70
|
+
// 实现按同构语义取 number,留痕见交付说明。
|
|
71
|
+
const _RUN_SCHEMA = {
|
|
72
|
+
name: _BUILTIN_NAME,
|
|
73
|
+
system: true,
|
|
74
|
+
collection: _BUILTIN_NAME,
|
|
75
|
+
idPrefix: 'wfrun',
|
|
76
|
+
write: [],
|
|
77
|
+
fields: {
|
|
78
|
+
_id: { type: 'string' },
|
|
79
|
+
workflow: { type: 'string' },
|
|
80
|
+
status: { type: 'string' },
|
|
81
|
+
input: { type: 'object' },
|
|
82
|
+
steps: { type: 'array' },
|
|
83
|
+
error: { type: 'string' },
|
|
84
|
+
stepIndex: { type: 'int' },
|
|
85
|
+
dryRun: { type: 'bool' },
|
|
86
|
+
now: { type: 'number' },
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/** 注册内建 `__workflowRun` schema(幂等;core 对重复三元组显式报错,故先 has 守卫) */
|
|
91
|
+
function ensureBuiltin() {
|
|
92
|
+
if (!_hasSchema(_BUILTIN_NAME)) _registerSchema(_RUN_SCHEMA);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ─── 错误 ────────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
/** Workflow defn 校验失败(白名单外 / 结构非法);文案含 WORKFLOW_UNSUPPORTED 前缀 */
|
|
98
|
+
class WorkflowError extends Error {}
|
|
99
|
+
|
|
100
|
+
/** 步骤失败内部信号:fail 步骤语义失败(message)或步骤执行 Err——fail-fast + 整体回滚 */
|
|
101
|
+
class _StepFailure extends Error {
|
|
102
|
+
constructor(message, index = null) {
|
|
103
|
+
super(message);
|
|
104
|
+
this.name = '_StepFailure';
|
|
105
|
+
this.index = index;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ─── 权限:三级白名单判定(§0.6;core permission.rs::evaluate 同构,doc=Missing 语义) ──
|
|
110
|
+
|
|
111
|
+
/** 类型名对齐 py type(v).__name__(错误文案 parity:NoneType/int/float/str/bool/list/dict) */
|
|
112
|
+
function _pyTypeName(v) {
|
|
113
|
+
if (v === null || v === undefined) return 'NoneType';
|
|
114
|
+
if (typeof v === 'boolean') return 'bool';
|
|
115
|
+
if (typeof v === 'number') return Number.isInteger(v) ? 'int' : 'float';
|
|
116
|
+
if (typeof v === 'string') return 'str';
|
|
117
|
+
if (Array.isArray(v)) return 'list';
|
|
118
|
+
if (typeof v === 'object') return 'dict';
|
|
119
|
+
return typeof v;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 角色白名单评估(与 core evaluate 同构;新建 run 无属主文档 → creator 按 Missing 通过)。
|
|
124
|
+
* roleList 传 null(缺省)而非 [] 时同样走「无配置」默认行为(py falsy 统一回退语义)。
|
|
125
|
+
*/
|
|
126
|
+
function _evaluate(roleList, ctx) {
|
|
127
|
+
const list = roleList ?? null;
|
|
128
|
+
if (!list || list.length === 0) {
|
|
129
|
+
// 空白名单 = 无权限配置 → 默认行为(ctx 缺失放行;internal 放行;guest 拒绝)
|
|
130
|
+
if (ctx == null) return true;
|
|
131
|
+
if (ctx.internal) return true;
|
|
132
|
+
return !(ctx.roles || []).includes('guest');
|
|
133
|
+
}
|
|
134
|
+
if (ctx == null) return true;
|
|
135
|
+
if (ctx.internal) return true;
|
|
136
|
+
const roles = ctx.roles || [];
|
|
137
|
+
if (roles.includes('super_admin') || roles.includes('admin')) return true;
|
|
138
|
+
const effective = roles.length ? roles : [ctx.role];
|
|
139
|
+
if (effective.some((r) => list.includes(r))) return true;
|
|
140
|
+
return list.includes('creator');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** run 白名单:显式声明优先;缺省回退 write(§0.6,falsy 统一回退) */
|
|
144
|
+
function _runWhitelist(defn) {
|
|
145
|
+
return defn.run || defn.write || null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ─── 校验器(注册即静态检查;纯函数,错误全量收集) ──────────────
|
|
149
|
+
|
|
150
|
+
/** 深扫结构中的全部占位符表达式(外层完整形态,dec 嵌套一并可见),返回 [{inner, whole}] */
|
|
151
|
+
function _scanPlaceholders(value) {
|
|
152
|
+
const out = [];
|
|
153
|
+
const walk = (v) => {
|
|
154
|
+
if (typeof v === 'string') {
|
|
155
|
+
for (const m of v.matchAll(_OUTER_PH)) out.push({ inner: m[0].slice(2, -2), whole: m[0] });
|
|
156
|
+
} else if (Array.isArray(v)) {
|
|
157
|
+
v.forEach(walk);
|
|
158
|
+
} else if (v && typeof v === 'object') {
|
|
159
|
+
Object.values(v).forEach(walk);
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
walk(value);
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** 校验单个占位符表达式:input.<path> / dec:<a>,<b> / <as>.<path>(前向) */
|
|
167
|
+
function _checkExpr(inner, definedAs, errors, where) {
|
|
168
|
+
if (inner.startsWith(_INPUT)) {
|
|
169
|
+
const path = inner.slice(_INPUT.length);
|
|
170
|
+
if (!path || path.split('.').some((s) => _NUM_SEG.test(s))) {
|
|
171
|
+
errors.push(`${where}: 占位符 {{${inner}}} 路径非法`
|
|
172
|
+
+ '(首批不支持数组下标段,需要逐行处理请走宿主代码编排)');
|
|
173
|
+
}
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (inner.startsWith(_DEC)) {
|
|
177
|
+
const parts = inner.slice(_DEC.length).split(',');
|
|
178
|
+
if (parts.length !== 2) {
|
|
179
|
+
errors.push(`${where}: 占位符 {{${inner}}} dec 须恰两个操作数 dec:<a>,<b>`);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
for (const raw of parts) {
|
|
183
|
+
const p = raw.trim();
|
|
184
|
+
if (p.startsWith('{{') && p.endsWith('}}')) {
|
|
185
|
+
_checkExpr(p.slice(2, -2), definedAs, errors, where);
|
|
186
|
+
} else if (!_NUM_LIT.test(p)) {
|
|
187
|
+
errors.push(`${where}: 占位符 {{${inner}}} 操作数 '${p}' 须为占位符或数字字面量`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
// <as>.<path>
|
|
193
|
+
const segs = inner.split('.');
|
|
194
|
+
const asName = segs[0];
|
|
195
|
+
if (!asName) {
|
|
196
|
+
errors.push(`${where}: 占位符 {{${inner}}} 缺少 as 名`);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (!definedAs.has(asName)) {
|
|
200
|
+
const avail = [...definedAs].sort();
|
|
201
|
+
errors.push(`${where}: 占位符 {{${inner}}} 引用了不存在的 as "${asName}"`
|
|
202
|
+
+ `(前向可用: ${avail.length ? JSON.stringify(avail) : '无'};后向引用不支持)`);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const path = segs.slice(1).join('.');
|
|
206
|
+
if (path.split('.').some((s) => _NUM_SEG.test(s))) {
|
|
207
|
+
errors.push(`${where}: 占位符 {{${inner}}} 路径非法(首批不支持数组下标段)`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Workflow defn 静态校验 → 错误列表(空 = 通过)。纯函数,白名单外全量收集。
|
|
213
|
+
*
|
|
214
|
+
* 检查项:顶层/步骤字段白名单、op 白名单、必填字段、as 唯一、占位符前向引用、
|
|
215
|
+
* when 结构(恰一个白名单算子 + 合法参数键)、upsert 必带 match、gql 禁占位符
|
|
216
|
+
* (参数化走 params 绑定,防注入)。
|
|
217
|
+
*/
|
|
218
|
+
function validateDefn(defn) {
|
|
219
|
+
const errors = [];
|
|
220
|
+
if (!defn || typeof defn !== 'object' || Array.isArray(defn)) return ['defn 须为 JSON 对象'];
|
|
221
|
+
const name = defn.name;
|
|
222
|
+
if (!name || typeof name !== 'string') {
|
|
223
|
+
errors.push('name 必填(非空字符串)');
|
|
224
|
+
} else if (name.startsWith('__')) {
|
|
225
|
+
errors.push(`name "${name}" 以 __ 开头(前缀保留给内建 schema,禁止用于工作流)`);
|
|
226
|
+
}
|
|
227
|
+
for (const k of Object.keys(defn)) {
|
|
228
|
+
if (!_TOP_FIELDS.has(k)) {
|
|
229
|
+
errors.push(`未知顶层字段 "${k}"(白名单: ${JSON.stringify([..._TOP_FIELDS].sort())})`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
for (const k of ['read', 'write', 'run']) {
|
|
233
|
+
const v = defn[k];
|
|
234
|
+
if (v != null && (!Array.isArray(v) || !v.every((r) => typeof r === 'string'))) {
|
|
235
|
+
errors.push(`${k} 白名单须为字符串数组`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const steps = defn.steps;
|
|
240
|
+
if (!Array.isArray(steps) || !steps.length) {
|
|
241
|
+
errors.push('steps 必填(非空数组,线性步骤序列)');
|
|
242
|
+
return errors;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const definedAs = new Set();
|
|
246
|
+
steps.forEach((step, i) => {
|
|
247
|
+
const where = `steps[${i}]`;
|
|
248
|
+
if (!step || typeof step !== 'object' || Array.isArray(step)) {
|
|
249
|
+
errors.push(`${where}: 步骤须为 JSON 对象`);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const op = step.op;
|
|
253
|
+
if (!_OPS.includes(op)) {
|
|
254
|
+
errors.push(`${where}: WORKFLOW_UNSUPPORTED 未知步骤类型 ${JSON.stringify(op)}`
|
|
255
|
+
+ `(首批白名单: ${JSON.stringify(_OPS)};循环/并行/子工作流/审批节点均不支持)`);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
for (const k of Object.keys(step)) {
|
|
259
|
+
if (!_STEP_FIELDS[op].has(k)) {
|
|
260
|
+
errors.push(`${where}: 未知步骤字段 "${k}"(${op} 白名单: `
|
|
261
|
+
+ `${JSON.stringify([..._STEP_FIELDS[op]].sort())})`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
for (const k of _STEP_REQUIRED[op]) {
|
|
265
|
+
if (step[k] == null || step[k] === '') {
|
|
266
|
+
errors.push(`${where}: ${op} 步骤缺少必填字段 "${k}"`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
const asName = step.as;
|
|
270
|
+
if ((op === 'query' || op === 'mutation') && asName && definedAs.has(asName)) {
|
|
271
|
+
errors.push(`${where}: as "${asName}" 重复(引用歧义,禁止覆盖)`);
|
|
272
|
+
}
|
|
273
|
+
// 前向引用集合:不含当前步骤自身的 as(自引用在执行期必然悬空——结果尚不存在);直接用 definedAs
|
|
274
|
+
if (op === 'mutation' && step.upsert && !step.match) {
|
|
275
|
+
errors.push(`${where}: upsert: true 须提供 match 条件`);
|
|
276
|
+
}
|
|
277
|
+
if (op === 'query' && typeof step.gql === 'string' && _GQL_PH.test(step.gql)) {
|
|
278
|
+
errors.push(`${where}: gql 内嵌占位符不支持(注入面);动态参数请走 params 绑定`);
|
|
279
|
+
}
|
|
280
|
+
// 占位符前向引用:扫描该步全部占位符字段(gql 除外——已禁)
|
|
281
|
+
const scanPool = {};
|
|
282
|
+
for (const k of Object.keys(step)) {
|
|
283
|
+
if (k !== 'op' && k !== 'gql') scanPool[k] = step[k];
|
|
284
|
+
}
|
|
285
|
+
for (const { inner } of _scanPlaceholders(scanPool)) {
|
|
286
|
+
_checkExpr(inner, definedAs, errors, where);
|
|
287
|
+
}
|
|
288
|
+
// when 结构
|
|
289
|
+
if (step.when != null) _validateWhen(step.when, errors, `${where}.when`);
|
|
290
|
+
if ((op === 'query' || op === 'mutation') && asName) definedAs.add(asName);
|
|
291
|
+
});
|
|
292
|
+
return errors;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* when → {op, paramKey};无歧义文法:含 exists 键即 op=exists(is 作参数键),
|
|
297
|
+
* 其余算子的右值键统一 than(is 键已被 exists 参数占用;is/eq 算子同用 than)
|
|
298
|
+
*/
|
|
299
|
+
function _splitWhen(when) {
|
|
300
|
+
if ('exists' in when) return { op: 'exists', paramKey: 'is' };
|
|
301
|
+
const op = Object.keys(when).find((k) => k in _WHEN_PARAM) || null;
|
|
302
|
+
return op ? { op, paramKey: _WHEN_PARAM[op] } : { op: null, paramKey: null };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** when 守卫结构:恰一个白名单算子键;右操作数参数键按算子定(is/than) */
|
|
306
|
+
function _validateWhen(when, errors, where) {
|
|
307
|
+
if (!when || typeof when !== 'object' || Array.isArray(when)) {
|
|
308
|
+
errors.push(`${where}: 须为 JSON 对象`);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const { op, paramKey } = _splitWhen(when);
|
|
312
|
+
if (op == null) {
|
|
313
|
+
const ops = Object.keys(when).filter((k) => k in _WHEN_PARAM);
|
|
314
|
+
errors.push(`${where}: 须恰一个白名单算子 ${JSON.stringify(Object.keys(_WHEN_PARAM))},收到 `
|
|
315
|
+
+ `${JSON.stringify(ops.length ? ops : '无')}`);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
const extra = Object.keys(when).filter((k) => k !== op && k !== paramKey);
|
|
319
|
+
if (extra.length) {
|
|
320
|
+
errors.push(`${where}: 未知参数键 ${JSON.stringify(extra)}(${op} 算子右值键为 "${paramKey}")`);
|
|
321
|
+
}
|
|
322
|
+
if (op !== 'exists' && !(paramKey in when)) {
|
|
323
|
+
errors.push(`${where}: ${op} 算子缺少右值键 "${paramKey}"`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ─── 注册表 ──────────────────────────────────────────────────
|
|
328
|
+
|
|
329
|
+
const _workflows = new Map();
|
|
330
|
+
|
|
331
|
+
/** 注册工作流定义(注册即静态校验,白名单外显式 Err;name 全局唯一)。
|
|
332
|
+
*
|
|
333
|
+
* 同名同形重复注册幂等通过(对齐 core schema.register 的复跑语义——场景 harness
|
|
334
|
+
* 每后端复跑同一批用例时必须可重入);同名异形显式 Err(禁止静默覆盖已注册定义)。
|
|
335
|
+
*/
|
|
336
|
+
function register(defn) {
|
|
337
|
+
const errors = validateDefn(defn);
|
|
338
|
+
if (errors.length) throw new WorkflowError(`WORKFLOW_UNSUPPORTED: ${errors.join(';')}`);
|
|
339
|
+
const name = defn.name;
|
|
340
|
+
if (_workflows.has(name)) {
|
|
341
|
+
if (_workflows.get(name) === defn) return defn;
|
|
342
|
+
throw new WorkflowError(
|
|
343
|
+
`WORKFLOW_UNSUPPORTED: Workflow 已注册且定义不同,禁止覆盖: ${name}`);
|
|
344
|
+
}
|
|
345
|
+
_workflows.set(name, defn);
|
|
346
|
+
return defn;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** 按名取 defn(read 白名单过滤;不可见与不存在同形——防枚举) */
|
|
350
|
+
function get(name, ctx = undefined) {
|
|
351
|
+
const d = _workflows.get(name);
|
|
352
|
+
if (!d) throw new Error(`Workflow 未注册: ${name}`);
|
|
353
|
+
const c = ctx === undefined ? getContext() ?? null : ctx;
|
|
354
|
+
if (!_evaluate(d.read ?? null, c)) throw new Error(`Workflow 未注册: ${name}`);
|
|
355
|
+
return d;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** 全部可见工作流名(read 白名单过滤) */
|
|
359
|
+
function list(ctx = undefined) {
|
|
360
|
+
const c = ctx === undefined ? getContext() ?? null : ctx;
|
|
361
|
+
return [..._workflows.values()]
|
|
362
|
+
.filter((d) => _evaluate(d.read ?? null, c))
|
|
363
|
+
.map((d) => d.name);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// ─── 占位符解析(§4.3;执行期) ───────────────────────────────
|
|
367
|
+
|
|
368
|
+
/** 点路径取值;strict(参数位)取不到显式 Err,非 strict(when 取值位)→ null(三态) */
|
|
369
|
+
function _dig(root, path, strict, whole) {
|
|
370
|
+
let cur = root;
|
|
371
|
+
for (const seg of path.split('.')) {
|
|
372
|
+
if (cur && typeof cur === 'object' && !Array.isArray(cur) && seg in cur) {
|
|
373
|
+
cur = cur[seg];
|
|
374
|
+
} else {
|
|
375
|
+
if (strict) throw new _StepFailure(`占位符 ${whole} 解析失败: 路径 "${path}" 不存在`);
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return cur;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** 解析单个占位符表达式为值(dec 递减;类型不合法显式 Err,不猜) */
|
|
383
|
+
function _resolveExpr(inner, inputMap, ctxMap, strict, whole) {
|
|
384
|
+
if (inner.startsWith(_INPUT)) {
|
|
385
|
+
return _dig(inputMap, inner.slice(_INPUT.length), strict, whole);
|
|
386
|
+
}
|
|
387
|
+
if (inner.startsWith(_DEC)) {
|
|
388
|
+
const parts = inner.slice(_DEC.length).split(',');
|
|
389
|
+
const vals = parts.map((raw) => {
|
|
390
|
+
const p = raw.trim();
|
|
391
|
+
if (p.startsWith('{{') && p.endsWith('}}')) {
|
|
392
|
+
return _resolveExpr(p.slice(2, -2), inputMap, ctxMap, strict, p);
|
|
393
|
+
}
|
|
394
|
+
return Number(p);
|
|
395
|
+
});
|
|
396
|
+
const [a, b] = vals;
|
|
397
|
+
vals.forEach((v, i) => {
|
|
398
|
+
if (typeof v !== 'number' || Number.isNaN(v)) {
|
|
399
|
+
throw new _StepFailure(
|
|
400
|
+
`占位符 ${whole} dec 操作数 ${i === 0 ? 'a' : 'b'} 须为数值,收到 ${_pyTypeName(v)}`);
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
return a - b;
|
|
404
|
+
}
|
|
405
|
+
const segs = inner.split('.');
|
|
406
|
+
const asName = segs[0];
|
|
407
|
+
const path = segs.slice(1).join('.');
|
|
408
|
+
if (!(asName in ctxMap)) {
|
|
409
|
+
if (strict) {
|
|
410
|
+
throw new _StepFailure(`占位符 ${whole} 引用的 as "${asName}" 无可用结果`
|
|
411
|
+
+ '(该步骤可能被 when 跳过或尚未执行)');
|
|
412
|
+
}
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
return _dig(ctxMap[asName], path, strict, whole);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/** 内嵌占位符值的字符串化(仅标量;null/复杂结构显式 Err,禁静默拼 'null') */
|
|
419
|
+
function _stringify(v) {
|
|
420
|
+
if (typeof v === 'string') return v;
|
|
421
|
+
if (typeof v === 'boolean') return v ? 'true' : 'false';
|
|
422
|
+
if (typeof v === 'number') return String(v);
|
|
423
|
+
throw new _StepFailure(`内嵌占位符仅支持标量值,收到 ${_pyTypeName(v)}`);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** 字符串中的占位符:整值形态 → 保类型替换;内嵌形态 → 字符串化拼接 */
|
|
427
|
+
function _resolveStr(s, inputMap, ctxMap, strict) {
|
|
428
|
+
if (_FULL_PH.test(s)) {
|
|
429
|
+
return _resolveExpr(s.slice(2, -2).trim(), inputMap, ctxMap, strict, s);
|
|
430
|
+
}
|
|
431
|
+
if (s.includes('{{')) {
|
|
432
|
+
return s.replace(_INNER_PH, (m, inner) => _stringify(_resolveExpr(inner, inputMap, ctxMap, strict, m)));
|
|
433
|
+
}
|
|
434
|
+
return s;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/** 深度替换结构中的占位符(object/array 递归;非字符串原样保留) */
|
|
438
|
+
function _resolveTree(value, inputMap, ctxMap, strict) {
|
|
439
|
+
if (typeof value === 'string') return _resolveStr(value, inputMap, ctxMap, strict);
|
|
440
|
+
if (Array.isArray(value)) return value.map((v) => _resolveTree(v, inputMap, ctxMap, strict));
|
|
441
|
+
if (value && typeof value === 'object') {
|
|
442
|
+
const out = {};
|
|
443
|
+
for (const k of Object.keys(value)) out[k] = _resolveTree(value[k], inputMap, ctxMap, strict);
|
|
444
|
+
return out;
|
|
445
|
+
}
|
|
446
|
+
return value;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/** JSON 语义相等:数值互比(NaN 不等),其余须严格同型同值(对齐 1 !== true) */
|
|
450
|
+
function _jsonEq(a, b) {
|
|
451
|
+
if (typeof a === 'number' && typeof b === 'number') return a === b;
|
|
452
|
+
return a === b;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// ─── when 守卫判定 ───────────────────────────────────────────
|
|
456
|
+
|
|
457
|
+
/** when 判定(取值位非 strict:路径取不到 → null 三态,服务 exists/is null 断言) */
|
|
458
|
+
function _evalWhen(when, inputMap, ctxMap) {
|
|
459
|
+
const { op, paramKey } = _splitWhen(when);
|
|
460
|
+
const left = _resolveTree(when[op], inputMap, ctxMap, false);
|
|
461
|
+
const right = paramKey in when
|
|
462
|
+
? _resolveTree(when[paramKey], inputMap, ctxMap, false)
|
|
463
|
+
: null;
|
|
464
|
+
if (op === 'exists') {
|
|
465
|
+
const present = left !== null && left !== undefined;
|
|
466
|
+
// is 键缺省 = 断言存在;显式 is(null/false → 期望不存在,其余 → 期望存在)
|
|
467
|
+
const want = paramKey in when
|
|
468
|
+
? (right !== null && right !== undefined && right !== false)
|
|
469
|
+
: true;
|
|
470
|
+
return present === want;
|
|
471
|
+
}
|
|
472
|
+
if (op === 'is' || op === 'eq') return _jsonEq(left, right);
|
|
473
|
+
if (op === 'ne') return !_jsonEq(left, right);
|
|
474
|
+
// lt / lte / gt / gte:数值比较;类型不可比显式 Err(不猜)
|
|
475
|
+
const cmp = { lt: (x, y) => x < y, lte: (x, y) => x <= y, gt: (x, y) => x > y, gte: (x, y) => x >= y }[op];
|
|
476
|
+
if (typeof left !== 'number' || typeof right !== 'number') {
|
|
477
|
+
throw new _StepFailure(
|
|
478
|
+
`when 算子 ${op} 的操作数类型不可比: ${_pyTypeName(left)} vs ${_pyTypeName(right)}`);
|
|
479
|
+
}
|
|
480
|
+
return cmp(left, right);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// ─── 执行器(线性步骤循环 + fail-fast;§2 生命线) ─────────────
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* 预扫全部 mutation 步骤的数据源集合(run 级原子包络依据;plan 是纯函数不执行)。
|
|
487
|
+
* 返回 null = 预扫失败 → 降级裸跑(单步骤内原子,emit 反馈事件,禁静默)。
|
|
488
|
+
* 占位符原样参与规划(plan 只看结构键,不校验占位符字符串值——实测)。
|
|
489
|
+
*/
|
|
490
|
+
function _prescanSources(defn, routeOverride, now, ctx) {
|
|
491
|
+
const sources = new Set();
|
|
492
|
+
for (const step of defn.steps) {
|
|
493
|
+
if (step.op !== 'mutation') continue;
|
|
494
|
+
try {
|
|
495
|
+
const pool = _newIdPool(step.model, step.data);
|
|
496
|
+
const plan = _call(() => _core.planMutation(step.model, step.data, now, pool, ctx, routeOverride));
|
|
497
|
+
for (const s of _sourcesOf(plan)) sources.add(s || 'default');
|
|
498
|
+
} catch (e) {
|
|
499
|
+
// 预扫失败降级裸跑(显式声明,禁静默)
|
|
500
|
+
_emitFeedback({
|
|
501
|
+
type: 'workflow_prescan_failed',
|
|
502
|
+
code: 'workflowPrescanFailed',
|
|
503
|
+
layer: 'workflow',
|
|
504
|
+
message: `run 源预扫失败(步骤 model=${step.model}):${e && e.message ? e.message : e};`
|
|
505
|
+
+ '本 run 降级裸跑(跨步骤非原子,单步骤内仍原子)',
|
|
506
|
+
hint: '检查该 mutation 步骤的 model/结构是否可规划;预扫仅提取数据源,不影响执行',
|
|
507
|
+
});
|
|
508
|
+
return null;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return sources;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/** 多源 run:无法原子 → 程序化声明(允许顺序执行,禁止静默;对齐 non_atomic_write 语义) */
|
|
515
|
+
function _warnNonAtomic(sources) {
|
|
516
|
+
const listed = [...sources].sort();
|
|
517
|
+
_emitFeedback({
|
|
518
|
+
type: 'workflow_non_atomic',
|
|
519
|
+
code: 'workflowNonAtomic',
|
|
520
|
+
layer: 'workflow',
|
|
521
|
+
message: `本 run 的写步骤跨 ${listed.length} 个数据源(${listed.join(', ')}):`
|
|
522
|
+
+ '无法原子,按顺序执行(跨步骤非原子)',
|
|
523
|
+
hint: '把写步骤收敛到单一数据源;跨源强一致首批请拆为宿主代码编排',
|
|
524
|
+
sources: listed,
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/** 执行单步骤(when 已判定通过):query / mutation / fail */
|
|
529
|
+
async function _execStep(step, inputMap, ctxMap, routeOverride, index) {
|
|
530
|
+
const op = step.op;
|
|
531
|
+
if (op === 'query') {
|
|
532
|
+
const params = _resolveTree(step.params ?? null, inputMap, ctxMap, true);
|
|
533
|
+
const rows = await crud.query(step.gql, params, routeOverride);
|
|
534
|
+
if (rows.length > 1) {
|
|
535
|
+
throw new _StepFailure(
|
|
536
|
+
`query 步骤 "${step.as}" 返回 ${rows.length} 行(期望 ≤1:占位符引用要求唯一结果;`
|
|
537
|
+
+ '请收紧条件或加 $limit:1)', index);
|
|
538
|
+
}
|
|
539
|
+
return rows.length ? rows[0] : null;
|
|
540
|
+
}
|
|
541
|
+
if (op === 'mutation') {
|
|
542
|
+
const data = _resolveTree(step.data, inputMap, ctxMap, true);
|
|
543
|
+
if (step.upsert) {
|
|
544
|
+
const match = _resolveTree(step.match, inputMap, ctxMap, true);
|
|
545
|
+
return crud.upsert(step.model, match, data, null, routeOverride);
|
|
546
|
+
}
|
|
547
|
+
return crud.mutation(step.model, data, routeOverride);
|
|
548
|
+
}
|
|
549
|
+
// fail:显式业务断言失败(§4.2)——message 支持内嵌占位符丰富错误文案
|
|
550
|
+
throw new _StepFailure(_resolveStr(step.message, inputMap, ctxMap, true), index);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* 线性步骤循环:迹与 defn.steps 一一对应(skipped/wouldRun/ran/failed 均留痕)。
|
|
555
|
+
* 失败抛 _StepFailure(带步骤下标)——外层收尾统一落 failed 终态 + 整体回滚。
|
|
556
|
+
*/
|
|
557
|
+
async function _runSteps(defn, inputMap, ctxMap, trace, dry, routeOverride) {
|
|
558
|
+
for (const [i, step] of defn.steps.entries()) {
|
|
559
|
+
const op = step.op;
|
|
560
|
+
const asName = step.as ?? null;
|
|
561
|
+
// a. when 判定 → 不满足记 skipped(显式留痕,绝不静默跳过),后续步骤照常
|
|
562
|
+
const when = step.when;
|
|
563
|
+
let ok = true;
|
|
564
|
+
if (when != null) {
|
|
565
|
+
try {
|
|
566
|
+
ok = _evalWhen(when, inputMap, ctxMap);
|
|
567
|
+
} catch (e) {
|
|
568
|
+
if (!(e instanceof _StepFailure)) throw e;
|
|
569
|
+
trace.push({ as: asName, op, state: 'failed', error: String(e.message) });
|
|
570
|
+
e.index = i;
|
|
571
|
+
throw e;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
if (!ok) {
|
|
575
|
+
trace.push({ as: asName, op, state: 'skipped' });
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
// b. dry-run:mutation / fail 不执行,记 wouldRun(§0.7)
|
|
579
|
+
if (dry && (op === 'mutation' || op === 'fail')) {
|
|
580
|
+
trace.push({ as: asName, op, state: 'wouldRun' });
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
// c/d. 解析占位符(步骤内)→ 执行 → 结果按 as 入上下文 → 迹追加
|
|
584
|
+
const entry = { as: asName, op, state: 'ran' };
|
|
585
|
+
let result;
|
|
586
|
+
try {
|
|
587
|
+
result = await _execStep(step, inputMap, ctxMap, routeOverride, i);
|
|
588
|
+
} catch (e) {
|
|
589
|
+
if (e instanceof _StepFailure) {
|
|
590
|
+
trace.push({ ...entry, state: 'failed', error: String(e.message) });
|
|
591
|
+
if (e.index == null) e.index = i;
|
|
592
|
+
throw e;
|
|
593
|
+
}
|
|
594
|
+
const msg = `步骤 ${i}(${op})执行失败: ${e && e.message ? e.message : e}`;
|
|
595
|
+
trace.push({ ...entry, state: 'failed', error: msg });
|
|
596
|
+
throw new _StepFailure(msg, i);
|
|
597
|
+
}
|
|
598
|
+
if (asName) ctxMap[asName] = result;
|
|
599
|
+
entry.result = result;
|
|
600
|
+
trace.push(entry);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* 干净 internal 上下文({internal: true},丢弃触发者 roles)
|
|
606
|
+
*
|
|
607
|
+
* 不用 permission.runAsInternal:它保留原 roles,而 core 对 guest **硬拒写**先于
|
|
608
|
+
* internal 放行(permission.rs::can_write_schema)——guest 触发的 rejected 落库会
|
|
609
|
+
* 被自己的 guest 角色挡住。审计通道的身份必须与触发者权限解耦。
|
|
610
|
+
*/
|
|
611
|
+
async function _runInternal(fn) {
|
|
612
|
+
return _als.run({ internal: true }, fn);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/** run 记录写入(internal 上下文——__workflowRun write 显式空名单仅放行 internal/admin) */
|
|
616
|
+
async function _persistWrite(doc) {
|
|
617
|
+
return _runInternal(() => crud.mutation(_BUILTIN_NAME, doc));
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/** run 记录收尾更新(事务外独立提交:业务事务回滚不影响失败 run 可查——铁律 B) */
|
|
621
|
+
async function _persistUpdate(rid, patch) {
|
|
622
|
+
return _runInternal(() => crud.update(_BUILTIN_NAME, { _id: rid }, patch));
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/** 内存 run 文档补落库元数据(_id / createdAt / updatedAt) */
|
|
626
|
+
function _withMeta(runDoc, saved) {
|
|
627
|
+
return {
|
|
628
|
+
...runDoc,
|
|
629
|
+
_id: saved ? saved._id ?? null : null,
|
|
630
|
+
createdAt: saved ? saved.createdAt ?? null : null,
|
|
631
|
+
updatedAt: saved ? saved.updatedAt ?? null : null,
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* 触发工作流 → 完整 run 文档(§3)
|
|
637
|
+
*
|
|
638
|
+
* 统一契约:执行期一切业务失败(步骤 Err / fail 步骤 / 权限拒绝)都表达为 run 终态
|
|
639
|
+
* (failed / rejected)+ error 字段返回,不抛异常;编程错误(未注册 / input 非法 /
|
|
640
|
+
* defn 校验失败)照常抛错。dry-run 下 query 真实执行(只读安全),mutation/fail 记
|
|
641
|
+
* wouldRun。跨步骤原子性:单源 run 整体原子(N1 实测可行——外层 runAtomic 包住整个
|
|
642
|
+
* 步骤循环,内层 mutation 嵌套并入);多源 / 预扫失败按顺序执行并发反馈事件(禁静默)。
|
|
643
|
+
* run 记录时序(§2 生命线):先落 running(独立提交,进程崩溃可见)→ 步骤事务 →
|
|
644
|
+
* 收尾 update 终态(事务外独立提交——业务回滚不影响失败 run 可查,铁律 B)。
|
|
645
|
+
*/
|
|
646
|
+
async function run(name, input = null, { dryRun = false, routeOverride = null } = {}) {
|
|
647
|
+
ensureBuiltin();
|
|
648
|
+
const d = _workflows.get(name);
|
|
649
|
+
if (!d) throw new Error(`Workflow 未注册: ${name}`);
|
|
650
|
+
if (input != null && (typeof input !== 'object' || Array.isArray(input))) {
|
|
651
|
+
throw new TypeError(`run(name, input) 的 input 须为 dict,收到 ${typeof input}`);
|
|
652
|
+
}
|
|
653
|
+
const inp = { ...(input || {}) };
|
|
654
|
+
const ctx = getContext() ?? null;
|
|
655
|
+
const now = Date.now();
|
|
656
|
+
const dry = Boolean(dryRun);
|
|
657
|
+
|
|
658
|
+
const runDoc = {
|
|
659
|
+
workflow: name, status: 'running', input: inp, steps: [],
|
|
660
|
+
error: null, stepIndex: null, dryRun: dry, now,
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
// 1. 权限(§0.6):fail-secure 优先于 dry-run(N3 实测裁决)——require_context 开启
|
|
664
|
+
// 且无 ctx → rejected 拒跑;run 白名单外 → rejected(落库可审计,一次落终态)
|
|
665
|
+
const require = _requireContext();
|
|
666
|
+
let rejected = null;
|
|
667
|
+
if (require && ctx == null) {
|
|
668
|
+
rejected = 'ERR_NO_CONTEXT: 上下文强制开启,无 ctx 拒跑(fail-secure 优先于 dry-run)';
|
|
669
|
+
} else if (!_evaluate(_runWhitelist(d), ctx)) {
|
|
670
|
+
rejected = `run 白名单拦截: 触发者角色不在 run 白名单内(${JSON.stringify(_runWhitelist(d) || [])})`;
|
|
671
|
+
}
|
|
672
|
+
if (rejected !== null) {
|
|
673
|
+
runDoc.status = 'rejected';
|
|
674
|
+
runDoc.error = rejected;
|
|
675
|
+
const saved = await _persistWrite(runDoc);
|
|
676
|
+
return _withMeta(runDoc, saved);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// 2. 状态落库 running(先于步骤:中断可观测)
|
|
680
|
+
const saved = await _persistWrite(runDoc);
|
|
681
|
+
const rid = saved ? saved._id ?? null : null;
|
|
682
|
+
|
|
683
|
+
// 3. 步骤循环(单源时整体原子;失败 _StepFailure 冒泡出事务 → 整体回滚)
|
|
684
|
+
const trace = runDoc.steps;
|
|
685
|
+
const ctxMap = Object.create(null);
|
|
686
|
+
const _execute = () => _runSteps(d, inp, ctxMap, trace, dry, routeOverride);
|
|
687
|
+
|
|
688
|
+
let status = dry ? 'drySucceeded' : 'succeeded';
|
|
689
|
+
let error = null;
|
|
690
|
+
let stepIndex = null;
|
|
691
|
+
try {
|
|
692
|
+
if (!dry) {
|
|
693
|
+
const sources = _prescanSources(d, routeOverride, now, ctx);
|
|
694
|
+
if (sources !== null && sources.size === 1) {
|
|
695
|
+
await runAtomic(sources, _execute);
|
|
696
|
+
} else {
|
|
697
|
+
if (sources !== null && sources.size > 1) _warnNonAtomic(sources);
|
|
698
|
+
await _execute();
|
|
699
|
+
}
|
|
700
|
+
} else {
|
|
701
|
+
await _execute();
|
|
702
|
+
}
|
|
703
|
+
} catch (e) {
|
|
704
|
+
if (!(e instanceof _StepFailure)) throw e;
|
|
705
|
+
status = dry ? 'dryFailed' : 'failed';
|
|
706
|
+
error = String(e.message);
|
|
707
|
+
stepIndex = e.index;
|
|
708
|
+
}
|
|
709
|
+
// 4. 收尾终态(事务外独立提交)
|
|
710
|
+
runDoc.status = status;
|
|
711
|
+
runDoc.error = error;
|
|
712
|
+
runDoc.stepIndex = stepIndex;
|
|
713
|
+
await _persistUpdate(rid, { status, steps: trace, error, stepIndex });
|
|
714
|
+
return _withMeta(runDoc, saved);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// 模块加载即自举内建 run 表(幂等;零配置——require 即可 run)
|
|
718
|
+
ensureBuiltin();
|
|
719
|
+
|
|
720
|
+
module.exports = {
|
|
721
|
+
WorkflowError,
|
|
722
|
+
ensureBuiltin,
|
|
723
|
+
validateDefn,
|
|
724
|
+
register,
|
|
725
|
+
get,
|
|
726
|
+
list,
|
|
727
|
+
run,
|
|
728
|
+
// parity 锚与单测用的内部件(下划线内部语义;对齐 py wf._workflows/_run_whitelist 可达性)
|
|
729
|
+
_workflows,
|
|
730
|
+
_runWhitelist,
|
|
731
|
+
_evaluate,
|
|
732
|
+
_evalWhen,
|
|
733
|
+
_resolveStr,
|
|
734
|
+
};
|