@stalfh233/omc-cli 0.4.100 → 0.4.102
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 +13 -1
- package/coverage/m1-coverage-manifest-v1.json +9 -3
- package/dist/cli-contract.js +5 -1
- package/dist/cli.js +1 -1
- package/dist/compound-writes.js +16 -5
- package/dist/discovery.js +7 -0
- package/dist/field-change.js +11 -6
- package/dist/field-families.js +60 -2
- package/dist/goal-routes.js +69 -0
- package/dist/governed-execution.js +1 -1
- package/dist/lanes/b2-registration.js +20 -3
- package/dist/lanes/businessrule-contract.js +4 -0
- package/dist/lanes/businessrule-coverage.js +1 -1
- package/dist/lanes/businessrule.js +93 -0
- package/dist/lanes/datarule-protocol.js +100 -0
- package/dist/lanes/field.js +29 -3
- package/dist/lanes/form-contract.js +1 -1
- package/dist/lanes/form.js +2 -2
- package/dist/lanes/online-js.js +19 -1
- package/dist/lanes/workflow.js +41 -6
- package/dist/online-js-source.js +7 -2
- package/dist/recipes/b2-registration.js +8 -7
- package/dist/recipes/businessrule.js +59 -1
- package/dist/rule-support.js +96 -0
- package/docs/images/omc-nixi/README.md +41 -0
- package/docs/omc-introduction.md +851 -0
- package/package.json +4 -4
- package/skills/omc-capabilities.md +3 -3
- package/skills/omc-refusals.md +1 -1
- package/skills/omc-tool-selection.md +1 -1
- package/skills/omc-write-hazards.md +14 -0
|
@@ -126,6 +126,106 @@ function conditionShapeError(condition) {
|
|
|
126
126
|
* admits the literal `"true"` marker (unconditional REQUIRED, per the
|
|
127
127
|
* designer's `requiredType` check; only reproduced for type 4).
|
|
128
128
|
*/
|
|
129
|
+
/**
|
|
130
|
+
* r26 快速装载(2026-10-28):把 agent 最自然的符号式/裸值条件就地编译成
|
|
131
|
+
* 平台中文规范形状,替代「校验拒绝→读信封→重拼」的 4-6 轮往返。
|
|
132
|
+
*
|
|
133
|
+
* 触发边界(无歧义):先按 {@link conditionProtocolError} 校验——**已合规
|
|
134
|
+
* 的条件原样放行、零改写**;不合规(裸 `=`/`<>`、符号操作符配裸标量值、
|
|
135
|
+
* 中文操作符配裸词值)才尝试编译。编译不了的返回原校验错误(保留既有教学)。
|
|
136
|
+
*
|
|
137
|
+
* 值归一规则:JSON 值对象原样;裸数字/布尔/带引号字符串 → `custom` 值对象;
|
|
138
|
+
* 非裸引号的裸词(如 `{ctype} 等于 建设工程分包合同`)→ `custom` + 该词。
|
|
139
|
+
* 单选字段按选项 code 比较仍需 `specifyOption`(编译器无法推断选项语义,
|
|
140
|
+
* 回执 note 会提示)。
|
|
141
|
+
*/
|
|
142
|
+
const SYMBOL_TO_CHINESE_OPERATOR = Object.freeze({
|
|
143
|
+
"=": "等于", "==": "等于", "!=": "不等于", "<>": "不等于",
|
|
144
|
+
">": "大于", "<": "小于", ">=": "大于等于", "<=": "小于等于",
|
|
145
|
+
Contains: "包含", NotContains: "不包含", IsEmpty: "为空", IsNotEmpty: "不为空",
|
|
146
|
+
});
|
|
147
|
+
function compileSingleCondition(condition) {
|
|
148
|
+
const match = /^(\{[A-Za-z0-9_.]+\})\s+(\S+)(?:\s+([\s\S]+))?$/.exec(condition.trim());
|
|
149
|
+
if (match === null)
|
|
150
|
+
return { error: conditionShapeError(condition) ?? `条件「${condition}」无法解析` };
|
|
151
|
+
const field = match[1];
|
|
152
|
+
const rawOperator = match[2];
|
|
153
|
+
const rawValue = match[3];
|
|
154
|
+
const operator = rawOperator in SYMBOL_TO_CHINESE_OPERATOR ? SYMBOL_TO_CHINESE_OPERATOR[rawOperator] : rawOperator;
|
|
155
|
+
if (!CONDITION_OPERATORS.has(operator))
|
|
156
|
+
return { error: conditionShapeError(condition) ?? `操作符「${rawOperator}」不在平台条件词表内` };
|
|
157
|
+
if (CONDITION_UNARY_OPERATORS.has(operator)) {
|
|
158
|
+
if (rawValue === undefined)
|
|
159
|
+
return { condition: `${field} ${operator}`, changed: operator !== rawOperator || rawValue !== undefined };
|
|
160
|
+
return { error: `「${operator}」是不带值的操作符,不应带值` };
|
|
161
|
+
}
|
|
162
|
+
if (rawValue === undefined)
|
|
163
|
+
return { error: `「${operator}」需要值(裸值会被编译为 custom 值对象)` };
|
|
164
|
+
let valuePart;
|
|
165
|
+
let changed = operator !== rawOperator;
|
|
166
|
+
try {
|
|
167
|
+
const parsed = JSON.parse(rawValue);
|
|
168
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
169
|
+
valuePart = rawValue;
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
valuePart = JSON.stringify({ valueType: "custom", value: parsed });
|
|
173
|
+
changed = true;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
const word = rawValue.trim();
|
|
178
|
+
if (word.length === 0)
|
|
179
|
+
return { error: `「${operator}」需要值` };
|
|
180
|
+
// 成对的单/双引号包裹(如 'A')剥引号取内部——JSON.parse 失败只是因为引号不是 JSON 的。
|
|
181
|
+
const unquoted = word.length >= 2 && ((word.startsWith("'") && word.endsWith("'")) || (word.startsWith('"') && word.endsWith('"'))) ? word.slice(1, -1) : word;
|
|
182
|
+
valuePart = JSON.stringify({ valueType: "custom", value: unquoted });
|
|
183
|
+
changed = true;
|
|
184
|
+
}
|
|
185
|
+
return { condition: `${field} ${operator} ${valuePart}`, changed };
|
|
186
|
+
}
|
|
187
|
+
/** Compile one condition formula string (`&&`/`||` groups preserved). */
|
|
188
|
+
export function compileConditionFormula(formula) {
|
|
189
|
+
const groups = [];
|
|
190
|
+
let changed = false;
|
|
191
|
+
for (const group of formula.split(/\s*\|\|\s*/)) {
|
|
192
|
+
const conditions = [];
|
|
193
|
+
for (const condition of group.split(/\s*&&\s*/)) {
|
|
194
|
+
const result = compileSingleCondition(condition);
|
|
195
|
+
if ("error" in result)
|
|
196
|
+
return { error: `条件「${condition.trim()}」${result.error}` };
|
|
197
|
+
conditions.push(result.condition);
|
|
198
|
+
changed = changed || result.changed;
|
|
199
|
+
}
|
|
200
|
+
groups.push(conditions);
|
|
201
|
+
}
|
|
202
|
+
return { formula: groups.map((group) => group.join(" && ")).join(" || "), changed };
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Normalize the form-condition keys of a field-options object IN PLACE-free:
|
|
206
|
+
* returns a new options object when compilation changed anything, plus a note
|
|
207
|
+
* per compiled key (for the tool receipt). Compliant formulas pass through
|
|
208
|
+
* untouched.
|
|
209
|
+
*/
|
|
210
|
+
export function normalizeFormConditionOptions(options) {
|
|
211
|
+
const notes = [];
|
|
212
|
+
let next = options;
|
|
213
|
+
for (const key of ["requiredFormula", "readonlyCondition", "displayFormula"]) {
|
|
214
|
+
const formula = options[key];
|
|
215
|
+
if (typeof formula !== "string" || formula.trim() === "" || formula.trim() === "true")
|
|
216
|
+
continue;
|
|
217
|
+
if (conditionProtocolError(formula, { allowTrue: true }) === null)
|
|
218
|
+
continue;
|
|
219
|
+
const compiled = compileConditionFormula(formula);
|
|
220
|
+
if ("error" in compiled)
|
|
221
|
+
continue;
|
|
222
|
+
if (next === options)
|
|
223
|
+
next = { ...options };
|
|
224
|
+
next[key] = compiled.formula;
|
|
225
|
+
notes.push(`${key}: ${formula} → ${compiled.formula}`);
|
|
226
|
+
}
|
|
227
|
+
return { options: next, compiledNotes: notes };
|
|
228
|
+
}
|
|
129
229
|
export function conditionProtocolError(formula, options = {}) {
|
|
130
230
|
if (typeof formula !== "string" || formula.trim().length === 0)
|
|
131
231
|
return "必须是非空字符串";
|
package/dist/lanes/field.js
CHANGED
|
@@ -333,7 +333,16 @@ export function registerFieldLane(tools) {
|
|
|
333
333
|
...(selector.length > 0 ? { selected: selector } : {}),
|
|
334
334
|
...(args.all !== true ? { businessOnly: true } : {}),
|
|
335
335
|
fields: projected,
|
|
336
|
-
|
|
336
|
+
// 券商 issue 1.4(2026-10-27):默认面空清单曾被误读成「模型没有字段」。
|
|
337
|
+
// 空清单要自己解释三种可能,别让 agent 二次试错。
|
|
338
|
+
...(detailed
|
|
339
|
+
? {}
|
|
340
|
+
: {
|
|
341
|
+
inventory: true,
|
|
342
|
+
hint: projected.length === 0
|
|
343
|
+
? "字段详情(options/subSchema)用 code/fields 选择器或 full:true。当前 draft 面没有业务字段——字段可能都在 published 面(published:true)、尚未创建,或只有系统字段(all:true 核对)"
|
|
344
|
+
: "字段详情(options/subSchema)用 code/fields 选择器或 full:true",
|
|
345
|
+
}),
|
|
337
346
|
};
|
|
338
347
|
}, {
|
|
339
348
|
// Round 4: the default diet reduced the whole list to a count, so agents
|
|
@@ -387,12 +396,18 @@ export function registerFieldLane(tools) {
|
|
|
387
396
|
relativeCode: relativeCodeArg,
|
|
388
397
|
choices: choicesArg,
|
|
389
398
|
subFields: subFieldArg,
|
|
399
|
+
skipExisting: z.boolean().optional().describe("幂等重入:code 已存在于本模型时跳过并返回 existed:true(批量脚本重跑不再被 schema-code-taken 打断);跨模型占用仍拒绝"),
|
|
390
400
|
propertyEmpty: z.boolean().optional().describe("数据模型层是否声明该字段不可为空;默认 false(可为空)。必填/只读请在表单设计与流程节点权限配置,不要在模型层设置——平台会据此把流程各节点强制成可见+可编辑+必填"),
|
|
391
401
|
}, async (session, args) => {
|
|
392
402
|
const draft = wireRows((await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "false" } })).data);
|
|
393
403
|
const published = wireRows((await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "true" } })).data);
|
|
394
|
-
if ([...draft, ...published].some((row) => (row.code ?? row.propertyCode) === args.code))
|
|
395
|
-
|
|
404
|
+
if ([...draft, ...published].some((row) => (row.code ?? row.propertyCode) === args.code)) {
|
|
405
|
+
// r28b 账本(2026-10-28):批量脚本重入被重复拒绝打断 69 次——skipExisting
|
|
406
|
+
// 让幂等重跑返回 existed:true 而不是拒(跨模型占用仍走 wire 层教学拒绝)。
|
|
407
|
+
if (args.skipExisting === true)
|
|
408
|
+
return { performed: { errcode: 0, skipped: true, reason: "duplicate-field" }, readback: { draftFieldPresent: true, existed: true, note: "skipExisting:true 命中已存在字段,幂等跳过(无写入)" } };
|
|
409
|
+
throw new ToolRefusalError({ status: "refused", blocker: "duplicate-field", schemaCode: args.schemaCode, propertyCode: args.code, message: `字段 ${args.schemaCode}.${args.code} 已存在于草稿或发布面;拒绝重复 POST;批量重入加 skipExisting:true 幂等跳过`, nextCommand: `omc call field.list --args '${JSON.stringify({ schemaCode: args.schemaCode, published: true, code: args.code })}'` });
|
|
410
|
+
}
|
|
396
411
|
// One seam owns field-change normalization, every reproduced gate, and the
|
|
397
412
|
// wire body (src/field-change.ts), so configure_model and intent share it.
|
|
398
413
|
assertNoSelfSchemaAggregation(args, published);
|
|
@@ -411,6 +426,14 @@ export function registerFieldLane(tools) {
|
|
|
411
426
|
// collides across models, not just inside this one. The registry lookup is
|
|
412
427
|
// not exposed over REST, so the occupier cannot be listed here; say that
|
|
413
428
|
// instead of leaving a bare platform code.
|
|
429
|
+
if (error instanceof WireError && error.kind === "business" && error.errcode === 300005 && args.skipExisting === true) {
|
|
430
|
+
// r28b 账本(2026-10-28):批量重入 69 次 field.create 拒绝的大头是 code 已占——
|
|
431
|
+
// 幂等跳过让批量脚本可重跑,只有跨模型占用才走拒绝教学。
|
|
432
|
+
const existing = await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "false" } });
|
|
433
|
+
const present = wireRows(existing.data).some((row) => (row.code ?? row.propertyCode) === args.code);
|
|
434
|
+
if (present)
|
|
435
|
+
return { performed: { errcode: 0, skipped: true, reason: "schema-code-taken" }, readback: { draftFieldPresent: true, existed: true, note: "skipExisting:true 命中已存在字段,幂等跳过(无写入)" } };
|
|
436
|
+
}
|
|
414
437
|
if (error instanceof WireError && error.kind === "business" && error.errcode === 300005)
|
|
415
438
|
throw new ToolRefusalError({
|
|
416
439
|
status: "refused",
|
|
@@ -534,6 +557,9 @@ async function runFieldUpdate(session, args) {
|
|
|
534
557
|
const storedOptions = after !== undefined ? parseFieldOptionsForReadback(after.options) : undefined;
|
|
535
558
|
return {
|
|
536
559
|
performed: { errcode: performed.errcode },
|
|
560
|
+
...(prepared.conditionCompiled !== undefined && prepared.conditionCompiled.length > 0
|
|
561
|
+
? { note: { conditionCompiled: prepared.conditionCompiled, hint: "符号式/裸值条件已就地编译为平台中文规范形状;单选字段按选项 code 比较仍需 specifyOption;此配置是门户交互层,data.submit 不拦截" } }
|
|
562
|
+
: {}),
|
|
537
563
|
readback: {
|
|
538
564
|
fieldPresent: after !== undefined,
|
|
539
565
|
...(after?.name !== undefined ? { name: after.name } : {}),
|
|
@@ -21,7 +21,7 @@ export const FORM_WIRE = Object.freeze({
|
|
|
21
21
|
const FORM_GET_DESCRIPTION = '读取默认表单(bizsheet/get)的发布状态与控件清单(紧凑默认)。**发布态以返回的 `published` / `existDraft` 布尔为准**(`published:true`=已发布;`published:false,existDraft:true`=只有草稿,**未发布**)——不要用 `publishedControlCodes` 是否非空来推断(草稿字段会让它非空,误判为已发布)。**每个模型交付前其默认表单必须 `published:true`**:用 `configure_form` 或 `form.draft(rebuild:true)`+`form.publish` 发布,并逐模型 `form.get` 核对。`full:true` 还返回 `options` 与解析后的 `customButtonJson`(表单工具栏按钮)。传 `controls:["code",…]` 只取指定控件的 options(如关联控件的 mappings);传 `full:true` 才返回 draft/published 的完整 view/attributes/html/actions JSON(actions 列为设计器保存/发布的表单级按钮动作)。体积大,仅在需要整体布局时用。';
|
|
22
22
|
const FORM_DRAFT_DESCRIPTION = "Save the default-form DRAFT (bizsheet/update). Reads the current sheet, merges the caller's explicit draftViewJson/draftAttributesJson (or preserves the existing draft); when both are absent on a fresh default form, composes the designer-standard scaffold from the model's published fields. A redundant field (relation-mapping carry-through target) always stays bound; with no workflow it is hidden in the design (options.visible=false), with a workflow it stays visible for per-node propertyPermissions. Pass `draftHtmlJson` to thread custom online-script HTML through. Empty-layout guard: explicitly passing \"[]\"/\"{}\" view/attributes is refused (form-empty-layout) — the platform designer's Save is NOT lossless and clears drafts to exactly that shape; escape with allowEmptyLayout:true, and recover cleared drafts from the form version history (/api/api/app/bizsheet/history/list). **读回原样回写**:从 `form.get` 取回的 view/attributes 直接回传时加 `fromReadback:true`——工具会与已存草稿/发布面逐字比对,一致才放行(平台自身布局可能含子表列行/系统行默认宽度),被改动过的面照常按 validation 拒绝。**表单工具栏按钮**:`buttons`(数组)或 `customButtonJson`(原样字符串)写进 `biz_sheet.options.customButtonJson`;**要到 form.publish 才生成 h_biz_button(triggerType=FORM)**,且表单页是否显示还取决于流程节点 `buttonPermission.customBtns[].enabled`(设计器「表单操作」勾选)。`useLocation` 用 FORM/ROW/LIST,表单工具栏按钮用 FORM。 **表单控件属性(含关联填充 mappings)从字段行派生**:改关联映射的事实源在字段 options.mappings(field.update → field.publish),直接改这里的控件映射会被平台按字段重算覆盖(真机 2026-10-14 实证);字段发布后如需刷新已发布表单的控件映射,用本工具 rebuild:false 重存(保留现有布局与在线JS)再 form.publish。";
|
|
23
23
|
const FORM_PUBLISH_DESCRIPTION = "Publish the default form (bizsheet/publish, proven shape: draft*Json promoted to published*Json verbatim) with published-face readback. Degrade guard: refuses to promote a draft carrying FEWER controls than the published face (form-publish-degrade) — the designer's Save clears drafts non-losslessly; recover via the form version history (/api/api/app/bizsheet/history/list) and rewrite the draft first, or pass allowDegrade:true to force the reduction. rebuild:true is the one-call refresh: discard the existing draft, re-scaffold from the published fields (non-structural control options preserved), then force-promote — use it after a field shape change. **表单工具栏按钮**:传 `buttons`(或 `customButtonJson`)→ 写进 `options.customButtonJson` 并在本次 publish 生成 `h_biz_button(triggerType=FORM, triggerCode=<formCode>)`(平台 `publishBizForm:1022`);`options.customButtonJson` 必须是**字符串化数组**(对象会 50000 syntax error);表单页是否显示再由流程节点 `buttonPermission.customBtns[].enabled` 过滤。";
|
|
24
|
-
const FORM_RUN_BUTTON_DESCRIPTION = "执行表单工具栏按钮(平台同款面:`POST /api/api/runtime/form/executeCustomButton`,body `{bizButtonModel, bizObjectList:[<完整业务对象>], reacquireDataFromDb:true}`)。**与列表行按钮不同**:列表按钮发 `launcher/domain_service_runtime/<schema>/<rule>` + `ids[]`(用 `rule.run`),表单按钮带**整条记录**,不能互换。按钮来源 `GET /api/api/system/getCustomButtons`(只列 triggerType=FORM 的 h_biz_button)。前提:按钮已由 `form.publish` 的 `buttons`/`customButtonJson` 生成。返回是执行结果信封,规则副作用用 `data.load` 另读;被规则 UPDATE 的目标行须是 COMPLETED(引擎查不到 DRAFT)。`302045` =
|
|
24
|
+
const FORM_RUN_BUTTON_DESCRIPTION = "执行表单工具栏按钮(平台同款面:`POST /api/api/runtime/form/executeCustomButton`,body `{bizButtonModel, bizObjectList:[<完整业务对象>], reacquireDataFromDb:true}`)。**与列表行按钮不同**:列表按钮发 `launcher/domain_service_runtime/<schema>/<rule>` + `ids[]`(用 `rule.run`),表单按钮带**整条记录**,不能互换。按钮来源 `GET /api/api/system/getCustomButtons`(只列 triggerType=FORM 的 h_biz_button)。前提:按钮已由 `form.publish` 的 `buttons`/`customButtonJson` 生成。返回是执行结果信封,规则副作用用 `data.load` 另读;被规则 UPDATE 的目标行须是 COMPLETED(引擎查不到 DRAFT)。`302045` = 数据被占用。**`10005` 执行失败时优先换等价面 `rule.run`(launcher/domain_service_runtime/<schema>/<rule> + ids[])**——同一绑定规则在 rule.run 面已验证可用(r28b 实证),不要在 executeCustomButton 上反复重试。";
|
|
25
25
|
const CONFIGURE_FORM_DESCRIPTION = "High-level: bind fields into the default form and publish it (form.draft → form.publish), subject to the write permission gate. Delegates to the proven form.draft scaffold + form.publish paths so the real empty-form layout (and any caller-supplied draftHtmlJson) is persisted and promoted — never bypasses with a shortcut body. `sheetCode` is optional: omitted means the model's default sheet (sheetCode = schemaCode).";
|
|
26
26
|
export const FORM_LANE_CONTRACT = Object.freeze({
|
|
27
27
|
lane: LANES.form,
|
package/dist/lanes/form.js
CHANGED
|
@@ -50,7 +50,7 @@ export function registerFormLane(tools) {
|
|
|
50
50
|
controls: z
|
|
51
51
|
.union([z.boolean(), z.array(z.string().min(1))])
|
|
52
52
|
.optional()
|
|
53
|
-
.describe("
|
|
53
|
+
.describe("布尔或字符串数组:true=返回全部控件的 draft/published 定义;数组如 [\"controlA\",\"controlB\"]=只回这些控件码的定义;省略=只回控件码清单。注意:逗号拼出的单个字符串不是合法取值(invalid_union)——多控件用 JSON 数组。要控件的 options/subSchema 时改用 full:true 再 artifact get --jq <field> 取单字段"),
|
|
54
54
|
full: z.boolean().optional().describe("返回全部六份布局 JSON 原文(大体积)"),
|
|
55
55
|
buttonScripts: z.boolean().optional().describe("附带表单按钮的脚本原文(options.customButtonJson 里 SCRIPT 按钮的 actionConfig.scriptData)及 sha/字节数"),
|
|
56
56
|
capability: capabilityArg,
|
|
@@ -148,7 +148,7 @@ export function registerFormLane(tools) {
|
|
|
148
148
|
writeTool(tools, "form.draft", FORM_LANE, FORM_TOOLS["form.draft"].scope, FORM_TOOLS["form.draft"].description, {
|
|
149
149
|
schemaCode: z.string().min(1),
|
|
150
150
|
sheetCode: z.string().min(1).optional().describe("表单槽位码;省略时默认 = schemaCode(模型码即默认表单槽位)"),
|
|
151
|
-
draftViewJson: z.string().optional(),
|
|
151
|
+
draftViewJson: z.string().optional().describe("行分组:一行 N 个 code=N 列;改列数须改行分组,非改 span"),
|
|
152
152
|
draftAttributesJson: z.string().optional(),
|
|
153
153
|
draftHtmlJson: z.string().optional(),
|
|
154
154
|
fieldCodes: z.array(z.string().min(1)).optional().describe("脚手架绑定的已发布字段(按此顺序);由 configure_form/intent 显式传入"),
|
package/dist/lanes/online-js.js
CHANGED
|
@@ -216,7 +216,25 @@ export function registerOnlineJsLane(tools) {
|
|
|
216
216
|
// Refuse a source that cannot run in the script slot (export/import, unknown
|
|
217
217
|
// form.on event) BEFORE the write permission gate. This is the exact
|
|
218
218
|
// class of mistake that blanked a live form page with no harness signal.
|
|
219
|
-
(args) => lintOnlineJsSource(args.source),
|
|
219
|
+
(args) => lintOnlineJsSource(args.source),
|
|
220
|
+
// r23 F4:dry-run 必须走与真写相同的 compose 校验路径(含 replace/merge 的
|
|
221
|
+
// wrapper 判定),否则 dry-run 过、真写拒——探针按 dry-run 绿灯写正式调用被拒。
|
|
222
|
+
async (session, args) => {
|
|
223
|
+
const current = await wireCall(session, { path: ENDPOINTS.sheetGet, query: { schemaCode: args.schemaCode, sheetCode: args.sheetCode ?? args.schemaCode } });
|
|
224
|
+
const sheet = (current.data ?? null);
|
|
225
|
+
const raw = typeof sheet?.draftHtmlJson === "string" ? sheet.draftHtmlJson : "";
|
|
226
|
+
const compat = validateHtmlRender(raw);
|
|
227
|
+
const elements = compat.ok ? parseLayoutElements(raw) : [];
|
|
228
|
+
const existingScript = String(elements.find((element) => element.id === "customScript")?.innerHTML ?? "");
|
|
229
|
+
const composed = composeCustomScript(existingScript, args.source, args.replace === true ? "replace" : "merge");
|
|
230
|
+
return {
|
|
231
|
+
mode: args.replace === true ? "replace" : "merge",
|
|
232
|
+
wouldChange: composed !== existingScript,
|
|
233
|
+
composedSha256: createHash("sha256").update(composed, "utf8").digest("hex"),
|
|
234
|
+
composedBytes: Buffer.byteLength(composed, "utf8"),
|
|
235
|
+
hint: "dry-run 已执行与真写一致的 compose 校验;该结果即正式调用将落库的脚本形状",
|
|
236
|
+
};
|
|
237
|
+
}, {
|
|
220
238
|
target: (args) => `onlinejs:${String(args.schemaCode ?? "")}/${String(args.sheetCode ?? args.schemaCode ?? "")}`,
|
|
221
239
|
read: async (session, args) => {
|
|
222
240
|
const sheet = await readFormOrNull(session, String(args.schemaCode), String(args.sheetCode ?? args.schemaCode));
|
package/dist/lanes/workflow.js
CHANGED
|
@@ -1686,6 +1686,34 @@ function sanitizeWorkflowSnapshotForReplay(row) {
|
|
|
1686
1686
|
}),
|
|
1687
1687
|
};
|
|
1688
1688
|
}
|
|
1689
|
+
/**
|
|
1690
|
+
* 2026-10-27 用户需求:创建流程时流程名默认与表单名一致。
|
|
1691
|
+
*
|
|
1692
|
+
* 解析链(全部真实读回,不捏造):
|
|
1693
|
+
* ① 显式 name → "args"
|
|
1694
|
+
* ② 该模型默认表单的名字(bizsheet/get 的 name 字段)→ "form-name"
|
|
1695
|
+
* ③ 模型名(bizmodels/list_schema 行的 name)→ "model-name"
|
|
1696
|
+
* ④ 都读不到(模型不存在/无名字段)→ 拒绝:让调用方显式给 name,
|
|
1697
|
+
* 绝不拿 schemaCode 冒充流程名(占位符冒充合法值是形状红线)。
|
|
1698
|
+
*/
|
|
1699
|
+
async function resolveWorkflowName(session, explicitName, schemaCode) {
|
|
1700
|
+
if (typeof explicitName === "string" && explicitName.trim().length > 0)
|
|
1701
|
+
return { name: explicitName, resolvedFrom: "args" };
|
|
1702
|
+
const sheet = await wireCall(session, { path: ENDPOINTS.sheetGet, query: { schemaCode, sheetCode: schemaCode } }).catch(() => null);
|
|
1703
|
+
const sheetName = sheet?.data?.name;
|
|
1704
|
+
if (typeof sheetName === "string" && sheetName.trim().length > 0)
|
|
1705
|
+
return { name: sheetName, resolvedFrom: "form-name" };
|
|
1706
|
+
const listed = await wireCall(session, { path: ENDPOINTS.modelList, method: "POST", body: { schemaCodes: [schemaCode] } }).catch(() => null);
|
|
1707
|
+
const rows = wireRows(listed?.data);
|
|
1708
|
+
const modelName = rows.find((row) => String(row.schemaCode ?? "") === schemaCode)?.name;
|
|
1709
|
+
if (typeof modelName === "string" && modelName.trim().length > 0)
|
|
1710
|
+
return { name: modelName, resolvedFrom: "model-name" };
|
|
1711
|
+
throw new ToolRefusalError({
|
|
1712
|
+
status: "refused",
|
|
1713
|
+
blocker: "invalid-input",
|
|
1714
|
+
message: `流程 name 缺省解析失败:模型 ${schemaCode} 读不到表单名或模型名(模型不存在?)。显式传 name,或先确认模型存在(omc call model.list --args '{"filter":"<名或码>"}')`,
|
|
1715
|
+
});
|
|
1716
|
+
}
|
|
1689
1717
|
function workflowRollbackSpec(toolName) {
|
|
1690
1718
|
return {
|
|
1691
1719
|
target: (args) => `workflow:${String(args.schemaCode ?? "")}/${String(args.code ?? args.workflowCode ?? "")}`,
|
|
@@ -2121,7 +2149,13 @@ export function registerWorkflowLane(tools) {
|
|
|
2121
2149
|
return { workflowCode, valid: errors.length === 0, errorCount: errors.length, warningCount: warnings.length, errors, warnings };
|
|
2122
2150
|
}, { target: `workflow.validate:${args.workflowCode ?? args.schemaCode}` }),
|
|
2123
2151
|
}));
|
|
2124
|
-
writeTool(tools, "workflow.create", WORKFLOW_LANE, WORKFLOW_TOOLS["workflow.create"].scope, WORKFLOW_TOOLS["workflow.create"].description, {
|
|
2152
|
+
writeTool(tools, "workflow.create", WORKFLOW_LANE, WORKFLOW_TOOLS["workflow.create"].scope, WORKFLOW_TOOLS["workflow.create"].description, {
|
|
2153
|
+
schemaCode: z.string().min(1),
|
|
2154
|
+
// 2026-10-27 用户需求:流程名默认与表单名一致。省略时的解析链见
|
|
2155
|
+
// resolveWorkflowName——表单名 → 模型名 → 拒绝(不拿 schemaCode 冒充)。
|
|
2156
|
+
name: z.string().min(1).optional().describe("流程名称;省略时默认 = 该模型默认表单的名字(无表单草稿时用模型名),与设计器行为一致"),
|
|
2157
|
+
code: z.string().min(1),
|
|
2158
|
+
}, async (session, args) => {
|
|
2125
2159
|
// Ticket 76 idempotent retry: get_header FIRST — a post-timeout
|
|
2126
2160
|
// re-create of the same code short-circuits with the existing header
|
|
2127
2161
|
// and ZERO create wires. On the happy path this read REPLACES the
|
|
@@ -2131,12 +2165,13 @@ export function registerWorkflowLane(tools) {
|
|
|
2131
2165
|
if (existingHeader != null) {
|
|
2132
2166
|
return { performed: { skipped: "already-present" }, readback: { alreadyPresent: true, header: existingHeader } };
|
|
2133
2167
|
}
|
|
2168
|
+
const { name: workflowName, resolvedFrom } = await resolveWorkflowName(session, args.name, args.schemaCode);
|
|
2134
2169
|
let performed;
|
|
2135
2170
|
try {
|
|
2136
2171
|
performed = await wireCall(session, {
|
|
2137
2172
|
path: ENDPOINTS.workflowCreate,
|
|
2138
2173
|
method: "POST",
|
|
2139
|
-
body: { workflowCode: args.code, workflowName
|
|
2174
|
+
body: { workflowCode: args.code, workflowName, schemaCode: args.schemaCode, icon: "h-icon-all-logic" },
|
|
2140
2175
|
});
|
|
2141
2176
|
}
|
|
2142
2177
|
catch (error) {
|
|
@@ -2163,10 +2198,10 @@ export function registerWorkflowLane(tools) {
|
|
|
2163
2198
|
const draft = await getWorkflowDraftOrNull(session, args.code);
|
|
2164
2199
|
return {
|
|
2165
2200
|
performed: { errcode: performed.errcode },
|
|
2166
|
-
readback: { workflowCode: args.code, headerPresent: true, draftPresent: draft != null, templateId: draft?.id ?? null },
|
|
2201
|
+
readback: { workflowCode: args.code, name: workflowName, nameResolvedFrom: resolvedFrom, headerPresent: true, draftPresent: draft != null, templateId: draft?.id ?? null },
|
|
2167
2202
|
};
|
|
2168
2203
|
}, undefined, undefined, workflowRollbackSpec("workflow.create"));
|
|
2169
|
-
writeTool(tools, "workflow.update", WORKFLOW_LANE, WORKFLOW_TOOLS["workflow.update"].scope, WORKFLOW_TOOLS["workflow.update"].description, { schemaCode: z.string().min(1), code: z.string().min(1), template: workflowTemplateArg, merge: z.boolean().optional().describe("增量:只发改动/新增的活动与路由,工具按 activityCode 合并到草稿(草稿为基;新增节点自动接链),不必先读回整模板再重发。默认自动:模板缺 START 或 END 时按增量 merge(部分模板),给了完整 START..END 才整模板替换;显式 true 强制增量、false 强制替换。merge 是并集、绝不删边:结果比草稿少边会被拒(workflow-merge-would-drop-routes);--dry-run 会返回 routesBefore/routesAfter/routesAdded/routesDropped") }, async (session, args) => {
|
|
2204
|
+
writeTool(tools, "workflow.update", WORKFLOW_LANE, WORKFLOW_TOOLS["workflow.update"].scope, WORKFLOW_TOOLS["workflow.update"].description, { schemaCode: z.string().min(1), code: z.string().min(1).describe("流程编码——本参数名是 code,传 workflowCode 会被 schema 拒绝"), template: workflowTemplateArg, merge: z.boolean().optional().describe("增量:只发改动/新增的活动与路由,工具按 activityCode 合并到草稿(草稿为基;新增节点自动接链),不必先读回整模板再重发。默认自动:模板缺 START 或 END 时按增量 merge(部分模板),给了完整 START..END 才整模板替换;显式 true 强制增量、false 强制替换。merge 是并集、绝不删边:结果比草稿少边会被拒(workflow-merge-would-drop-routes);--dry-run 会返回 routesBefore/routesAfter/routesAdded/routesDropped") }, async (session, args) => {
|
|
2170
2205
|
const template = args.template ?? {};
|
|
2171
2206
|
validateCallerActivities(template, "workflow.update");
|
|
2172
2207
|
validateWorkflowRouteConditions(template, "workflow.update");
|
|
@@ -2279,7 +2314,7 @@ export function registerWorkflowLane(tools) {
|
|
|
2279
2314
|
: "本次未给 routes/edges/rules:草稿连线原样沿用(新增节点才会自动接链)",
|
|
2280
2315
|
};
|
|
2281
2316
|
}, workflowRollbackSpec("workflow.update"));
|
|
2282
|
-
writeTool(tools, "workflow.publish", WORKFLOW_LANE, WORKFLOW_TOOLS["workflow.publish"].scope, WORKFLOW_TOOLS["workflow.publish"].description, { schemaCode: z.string().min(1), code: z.string().min(1), template: workflowTemplateArg.optional() }, async (session, args) => {
|
|
2317
|
+
writeTool(tools, "workflow.publish", WORKFLOW_LANE, WORKFLOW_TOOLS["workflow.publish"].scope, WORKFLOW_TOOLS["workflow.publish"].description, { schemaCode: z.string().min(1), code: z.string().min(1).describe("流程编码——本参数名是 code,传 workflowCode 会被 schema 拒绝"), template: workflowTemplateArg.optional() }, async (session, args) => {
|
|
2283
2318
|
// An absent template publishes the stored draft as-is — the platform's own
|
|
2284
2319
|
// path. The schema is optional so the contract no longer contradicts it.
|
|
2285
2320
|
const template = args.template ?? {};
|
|
@@ -2618,7 +2653,7 @@ export function registerWorkflowLane(tools) {
|
|
|
2618
2653
|
},
|
|
2619
2654
|
};
|
|
2620
2655
|
}, undefined, undefined, workflowRollbackSpec("workflow.update"));
|
|
2621
|
-
writeTool(tools, "workflow.delete", WORKFLOW_LANE, WORKFLOW_TOOLS["workflow.delete"].scope, WORKFLOW_TOOLS["workflow.delete"].description, { schemaCode: z.string().min(1), code: z.string().min(1) }, async (session, args) => {
|
|
2656
|
+
writeTool(tools, "workflow.delete", WORKFLOW_LANE, WORKFLOW_TOOLS["workflow.delete"].scope, WORKFLOW_TOOLS["workflow.delete"].description, { schemaCode: z.string().min(1), code: z.string().min(1).describe("流程编码——本参数名是 code") }, async (session, args) => {
|
|
2622
2657
|
let performed;
|
|
2623
2658
|
try {
|
|
2624
2659
|
performed = await wireCall(session, { path: ENDPOINTS.workflowDelete, method: "POST", query: { workflowCode: args.code } });
|
package/dist/online-js-source.js
CHANGED
|
@@ -58,9 +58,14 @@ export const ONLINE_JS_EVENTS = new Set([
|
|
|
58
58
|
const WRAPPER_OPEN = /\(\s*function\s*\(\s*form\s*\)/;
|
|
59
59
|
const WRAPPER_CLOSE = "})";
|
|
60
60
|
const EVENT_AT = /form\.on\(\s*['"]([^'"]+)['"]/g;
|
|
61
|
-
/** True when the script already carries the platform's `(function(form){ … })` scaffold.
|
|
61
|
+
/** True when the script already carries the platform's `(function(form){ … })` scaffold.
|
|
62
|
+
* r23 F4/F5 实证:尾部必须容忍三种合法形式 —— 模板原生 `})`、IIFE 调用 `})(form);`/`})();`,
|
|
63
|
+
* 以及其后带注释/空白(中文注释曾被判成"裸代码"导致 replace 全拒)。 */
|
|
62
64
|
export function hasLifecycleWrapper(script) {
|
|
63
|
-
|
|
65
|
+
if (!WRAPPER_OPEN.test(script))
|
|
66
|
+
return false;
|
|
67
|
+
const tail = stripComments(script).trimEnd();
|
|
68
|
+
return /\}\s*\)\s*(?:\(\s*form\s*\))?\s*;?\s*$/.test(tail);
|
|
64
69
|
}
|
|
65
70
|
/**
|
|
66
71
|
* Refuse a source that cannot run in the form's script slot BEFORE any wire
|
|
@@ -16,14 +16,15 @@ const restfulQueryService = Object.freeze({
|
|
|
16
16
|
},
|
|
17
17
|
methodName: "querySupplier",
|
|
18
18
|
methodConfig: { httpType: "Post", requestBodyType: "JSON", url: "/supplier/query", frontEndExtensionEnable: true },
|
|
19
|
-
methodInputParameters: [{ code: "keyword", name: "关键字", required: true, configJson: { position: "REQUEST_BODY" } }],
|
|
20
|
-
methodOutputParameters: [{ code: "name", name: "供应商名称", configJson: { position: "REQUEST_BODY" } }],
|
|
19
|
+
methodInputParameters: [{ code: "keyword", name: "关键字", required: true, bizPropertyType: "SHORT_TEXT", configJson: { position: "REQUEST_BODY" } }],
|
|
20
|
+
methodOutputParameters: [{ code: "name", name: "供应商名称", bizPropertyType: "SHORT_TEXT", configJson: { position: "REQUEST_BODY" } }],
|
|
21
21
|
},
|
|
22
22
|
preconditions: ["serviceCategoryId 由 service.category.list 解析(「全部」是空 id 虚拟项不可用);RESTFUL 服务 configJson 需要 auth 与 commonlist[url];带 methodName 时必须有 methodConfig"],
|
|
23
23
|
conditionalRequirements: [
|
|
24
24
|
"RESTFUL auth.code ∈ customer(parameterlist=[]) / basic(client… username/password) / oauth_password / oauth_client;parameterlist 每项 {code,value} 且必须与 auth.code 期望参数完全一致",
|
|
25
25
|
"RESTFUL 方法 configJson 必需键:httpType(Post/Get/Put/Patch/Delete)、requestBodyType(Form-urlencoded/JSON/Text)、url、frontEndExtensionEnable(布尔)",
|
|
26
26
|
"RESTFUL 每个输入/输出参数必须带 configJson.position ∈ QUERY_PARAMS/REQUEST_HEADER/REQUEST_BODY/PATH_PARAMS(URL_VARIABLE 被运行时拒 501007)",
|
|
27
|
+
"每个输入/输出参数必须带 bizPropertyType(如 SHORT_TEXT/NUMERICAL)——缺它提交即被 bizservice-parameter-face-invalid 拒(r19-p7 首拒根因,2026-10-26 修正本配方)",
|
|
27
28
|
"一次写只建一个方法;给既有服务加方法走 method.create(escape hatch)",
|
|
28
29
|
],
|
|
29
30
|
priorToolReferences: [
|
|
@@ -92,10 +93,10 @@ const methodCreateRestfulGet = Object.freeze({
|
|
|
92
93
|
name: "供应商查询",
|
|
93
94
|
methodConfig: { httpType: "Get", requestBodyType: "JSON", url: "/openapi/supplier/query", frontEndExtensionEnable: true },
|
|
94
95
|
methodInputParameters: [
|
|
95
|
-
{ code: "keyword", name: "关键字", required: true, configJson: { position: "QUERY_PARAMS" } },
|
|
96
|
-
{ code: "x-lowcode-corpId", name: "企业ID(平台注入常量)", required: false, codeType: 0, bizCode: "<corpId>", configJson: { position: "REQUEST_HEADER" } },
|
|
96
|
+
{ code: "keyword", name: "关键字", required: true, bizPropertyType: "SHORT_TEXT", configJson: { position: "QUERY_PARAMS" } },
|
|
97
|
+
{ code: "x-lowcode-corpId", name: "企业ID(平台注入常量)", required: false, bizPropertyType: "SHORT_TEXT", codeType: 0, bizCode: "<corpId>", configJson: { position: "REQUEST_HEADER" } },
|
|
97
98
|
],
|
|
98
|
-
methodOutputParameters: [{ code: "name", name: "供应商名称", configJson: { position: "REQUEST_BODY" } }],
|
|
99
|
+
methodOutputParameters: [{ code: "name", name: "供应商名称", bizPropertyType: "SHORT_TEXT", configJson: { position: "REQUEST_BODY" } }],
|
|
99
100
|
},
|
|
100
101
|
preconditions: ["服务已存在(method.create 只加方法);methodCode 未存在;RESTFUL 每个入参与出参都带 configJson.position"],
|
|
101
102
|
conditionalRequirements: [
|
|
@@ -127,8 +128,8 @@ const methodCreateRestfulPost = Object.freeze({
|
|
|
127
128
|
methodCode: "saveSupplier",
|
|
128
129
|
name: "供应商保存",
|
|
129
130
|
methodConfig: { httpType: "Post", requestBodyType: "JSON", url: "/openapi/supplier/save", frontEndExtensionEnable: true },
|
|
130
|
-
methodInputParameters: [{ code: "body", name: "供应商对象", required: true, configJson: { position: "REQUEST_BODY" } }],
|
|
131
|
-
methodOutputParameters: [{ code: "id", name: "主键", configJson: { position: "REQUEST_BODY" } }],
|
|
131
|
+
methodInputParameters: [{ code: "body", name: "供应商对象", required: true, bizPropertyType: "SHORT_TEXT", configJson: { position: "REQUEST_BODY" } }],
|
|
132
|
+
methodOutputParameters: [{ code: "id", name: "主键", bizPropertyType: "SHORT_TEXT", configJson: { position: "REQUEST_BODY" } }],
|
|
132
133
|
},
|
|
133
134
|
preconditions: ["服务已存在;methodCode 未存在;POST 入参 position=REQUEST_BODY,requestBodyType=JSON"],
|
|
134
135
|
conditionalRequirements: [
|
|
@@ -255,6 +255,7 @@ const ruleCrossModelCreate = Object.freeze({
|
|
|
255
255
|
"上游是 QUERY 时 `inputParam=\"_<QUERY节点code>_output\"`(QUERY 查 N 行→CREATE 逐条生成即 N→N 扇出,真机验证支持,同/跨模型均可);否则 `\"_input\"`。",
|
|
256
256
|
"`QUERY` 节点只能查**主模型行**:查子表 schema(`parent.child` 或子 schema 名)空条件返回 0 行、带条件 550018(真机 2026-10-08)——逐子表明细扇出才需要二开。",
|
|
257
257
|
"跨模型 CREATE/UPDATE 真机验证支持(2026-09-28 / 2026-10-08);承载规则的触发时机要对(Create 在提交时触发、Update 只在 data.update 触发、TIMING 定时触发)。",
|
|
258
|
+
"**CREATE 目标是已发布流程的模型时**(2026-10-27 真机四格矩阵):不传 startWorkflow → 建行**不发起流程**;要发起必须 startWorkflow:true + targetWorkflowCode:<该流程> + originatorValue 用户对象数组 [{\"id\":\"<userId>\",\"name\":\"<名>\"}](传对象 {} → 触发提交 50000 expect '[';无效 id → 50000 发起人不存在);设计器手工编辑可打开该开关,API 与设计器配置会漂移,rule.get 回读核对。",
|
|
258
259
|
],
|
|
259
260
|
priorToolReferences: [
|
|
260
261
|
{ tool: "field.list", args: { environment, schemaCode: "<targetModel>", published: true }, purpose: "确认目标模型/字段已发布" },
|
|
@@ -269,7 +270,64 @@ const ruleCrossModelCreate = Object.freeze({
|
|
|
269
270
|
],
|
|
270
271
|
requiresEnvironmentDerivedIds: false,
|
|
271
272
|
});
|
|
273
|
+
/**
|
|
274
|
+
* r19 p7(2026-10-26):「Create 规则调用二开服务回写」在真机上全链路走通,但
|
|
275
|
+
* agent 为 methodMapping 形状 describe 了 rule.save 4 次(最大 16.8KB)——五条
|
|
276
|
+
* 既有配方里只有 data-duplicate-flag 带单向 inputMappings。本配方以 p7 的真实
|
|
277
|
+
* 成功参数为模板(输入 cname→来源字段、输出 level→目标字段,codeType:1),
|
|
278
|
+
* 双向映射一步可抄。
|
|
279
|
+
*/
|
|
280
|
+
const ruleBizactionInvokeService = Object.freeze({
|
|
281
|
+
id: "rule-bizaction-invoke-service",
|
|
282
|
+
title: "Create 规则调用服务方法并映射回写(BIZ_ACTION 双向)",
|
|
283
|
+
aliases: ["服务方法", "调用服务", "BIZ_ACTION", "回写服务", "查服务回写"],
|
|
284
|
+
status: "requires-prior-readback",
|
|
285
|
+
tool: "rule.save",
|
|
286
|
+
args: {
|
|
287
|
+
environment,
|
|
288
|
+
schemaCode: model,
|
|
289
|
+
ruleCode: "Create",
|
|
290
|
+
name: "数据新增",
|
|
291
|
+
nodeGraph: {
|
|
292
|
+
nodes: [
|
|
293
|
+
{ nodeCode: "Start", nodeName: "开始", nodeType: "START" },
|
|
294
|
+
{ nodeCode: "A1", nodeName: "调用服务并回写", nodeType: "BIZ_ACTION", inputParam: "_input", methodMapping: { schemaCode: model, serviceCode: "<svc>", serviceMethodCode: "<method>", inputMappings: [{ serviceMethodParameterCode: "<in_code>", codeType: 1, bizCode: "<src_field>", required: true }], outputMappings: [{ serviceMethodParameterCode: "<out_code>", codeType: 1, bizCode: "<dst_field>" }] } },
|
|
295
|
+
{ nodeCode: "Create", nodeName: "新增数据", nodeType: "SYSTEM_CREATE", isSystemNode: true },
|
|
296
|
+
{ nodeCode: "End", nodeName: "结束", nodeType: "END" },
|
|
297
|
+
],
|
|
298
|
+
routes: [
|
|
299
|
+
{ preNode: "Start", postNode: "A1", routeCondition: true },
|
|
300
|
+
{ preNode: "A1", postNode: "Create", routeCondition: true },
|
|
301
|
+
{ preNode: "Create", postNode: "End", routeCondition: true },
|
|
302
|
+
],
|
|
303
|
+
},
|
|
304
|
+
publish: true,
|
|
305
|
+
},
|
|
306
|
+
preconditions: [
|
|
307
|
+
"模型与来源/目标字段已发布;服务与方法已注册(service.register 的 readback.method 直接给参数 code)",
|
|
308
|
+
"方法出参类型(bizPropertyType)与目标字段类型匹配",
|
|
309
|
+
],
|
|
310
|
+
conditionalRequirements: [
|
|
311
|
+
"**出参回写三前提(r28b 8 组实验定案,缺一出参恒 null 且节点日志仍 successful)**:① 响应 JSON 根级平铺出参键(信封嵌套如 {data:{...}} 静默不回写)② 挂预置 Create/Update 规则上下文(自定义规则/rule.run 不回写业务字段)③ 用 data.submit/data.update 触发(data.save 存草稿不触发)。回写是否成功只认 data.load 读字段,不认调用日志。",
|
|
312
|
+
"codeType:1 = 映射模型字段(bizCode=字段 code);codeType:0 = 常量(bizCode=常量值);serviceMethodParameterCode 必须与方法声明的参数 code 逐字一致",
|
|
313
|
+
"**接口响应必须在 JSON 根级**返回与出参 code 同名的键——嵌套在信封里(如 {data:{level}})会静默映射不上(p7 实测;r28b 8 组实验全因信封端点失败,换平铺端点后 4 组一次全过)",
|
|
314
|
+
"**平台自调自己(127.0.0.1)会被运行时鉴权拦**:errcode 500012「绑定的API需要提供正确的身份验证或凭据」——需要带出站凭据的真实外部服务,或服务端配置鉴权放行;这不是 BIZ_ACTION 不支持(p7 定案)",
|
|
315
|
+
"BIZ_ACTION 放在 SYSTEM_CREATE **之前**(与自更新 UPDATE 相反:ASSIGN/BIZ_ACTION 在系统节点前,UPDATE 在系统节点后)",
|
|
316
|
+
],
|
|
317
|
+
priorToolReferences: [
|
|
318
|
+
{ tool: "service.register", args: { environment }, purpose: "注册服务+方法;readback.method 内联参数面(code/bizPropertyType),不必再 service.get" },
|
|
319
|
+
],
|
|
320
|
+
dryRun: { supported: true, command: `omc call rule.save --env ${environment} --dry-run --args '<recipe.args>'`, notes: "dry-run 跑图 lint + 映射键校验(schemaCode/serviceCode/serviceMethodCode 非空、codeType 枚举);服务参数面一致性真机 save 才核" },
|
|
321
|
+
expectedReadback: { readback: { published: true, enabled: false, next: "rule.enable → data.submit 触发 → data.load 读回目标字段" } },
|
|
322
|
+
refusalRecovery: [
|
|
323
|
+
{ code: "rule-graph-bizmethod-unresolved", action: "serviceCode/serviceMethodCode 必须是已注册服务(service.list);方法级详情 service.get" },
|
|
324
|
+
{ code: "rule-graph-bizaction-mapping-field-missing", action: "inputMappings 的 bizCode 必须是本模型已发布字段 code(field.list 核对)" },
|
|
325
|
+
...commonRecovery,
|
|
326
|
+
],
|
|
327
|
+
notes: "p7 实测链(sandbox-r19/.scratch/report-p7.md §4):注册查询服务 → 本配方建 Create 规则 → rule.enable → 提交触发。运行期若 500012 见 conditionalRequirements 第 3 条(出站凭据边界)",
|
|
328
|
+
requiresEnvironmentDerivedIds: true,
|
|
329
|
+
});
|
|
272
330
|
export const BUSINESSRULE_METADATA = Object.freeze({
|
|
273
|
-
"rule.save": Object.freeze({ recipes: [ruleGraph, ruleBranchGraph, ruleTimingTask, dataDuplicateFlag, ruleCrossModelCreate], relatedTools: ["field.list", "rule.get", "rule.enable"] }),
|
|
331
|
+
"rule.save": Object.freeze({ recipes: [ruleGraph, ruleBranchGraph, ruleTimingTask, dataDuplicateFlag, ruleCrossModelCreate, ruleBizactionInvokeService], relatedTools: ["field.list", "rule.get", "rule.enable", "service.get"] }),
|
|
274
332
|
});
|
|
275
333
|
//# sourceMappingURL=businessrule.js.map
|
package/dist/rule-support.js
CHANGED
|
@@ -446,6 +446,100 @@ export function isModernAssignVersion(version) {
|
|
|
446
446
|
* supported alternatives (BRANCH, or two rules).
|
|
447
447
|
*/
|
|
448
448
|
const CHOICE_CONDITION_TYPES = new Set([12, 13, 14, 15]);
|
|
449
|
+
/**
|
|
450
|
+
* 2026-10-27 用户反馈 + r24 沙盒四格矩阵(1.1.25 真机实测):
|
|
451
|
+
* CREATE(新增数据)节点打到**已发布流程的模型**时的流程发起开关语义——
|
|
452
|
+
*
|
|
453
|
+
* 不传 startWorkflow → 行落库、不发起流程(API 默认,实测)
|
|
454
|
+
* true + originatorValue "{}" → 触发表单提交 50000(fastjson expect '[', but {)
|
|
455
|
+
* true + 数组但 id 无效 → 50000「业务规则发起人不存在」
|
|
456
|
+
* true + [{id,name}] 真实用户 + targetWorkflowCode → 行落库并自动发起流程
|
|
457
|
+
*
|
|
458
|
+
* 契约:目标有已发布流程而未声明开关 → warning 教学显式做法(合法的两种选择
|
|
459
|
+
* 都要写明);开了开关但 originatorValue/targetWorkflowCode 不合法 → error
|
|
460
|
+
* 写前拒绝——那两种形状会把**触发表单的提交**整个打挂,正是 fail-closed 该拦的。
|
|
461
|
+
*/
|
|
462
|
+
async function createNodeFlowStartWarnings(session, nodes) {
|
|
463
|
+
const creates = nodes.filter((node) => String(node.nodeType ?? "") === "CREATE" && String(node.targetObjectCode ?? "") !== "");
|
|
464
|
+
if (creates.length === 0)
|
|
465
|
+
return [];
|
|
466
|
+
const flowByTarget = new Map();
|
|
467
|
+
for (const target of [...new Set(creates.map((node) => String(node.targetObjectCode)))]) {
|
|
468
|
+
try {
|
|
469
|
+
const isWorkflow = (await wireCall(session, { path: ENDPOINTS.workflowIsWorkflow, query: { schemaCode: target } })).data === true;
|
|
470
|
+
if (!isWorkflow)
|
|
471
|
+
continue;
|
|
472
|
+
const row = (await wireCall(session, { path: ENDPOINTS.workflowGetBySchema, query: { schemaCode: target } })).data;
|
|
473
|
+
const code = row && typeof row.workflowCode === "string" && row.workflowCode ? row.workflowCode : null;
|
|
474
|
+
if (!code)
|
|
475
|
+
continue;
|
|
476
|
+
// get_published 原样回模板(fake 保真:未发布也回 DRAFT 形状——Ticket 77,
|
|
477
|
+
// 发布证据是 templateType:PUBLISH 标记,不是有数据);流程码用已解析的 code。
|
|
478
|
+
const published = (await wireCall(session, { path: ENDPOINTS.workflowGetPublished, query: { workflowCode: code } })).data;
|
|
479
|
+
const publishMarker = published !== null && published !== undefined ? String(published.templateType ?? "").toUpperCase() : "ABSENT";
|
|
480
|
+
if (published && (publishMarker === "" || publishMarker === "PUBLISH"))
|
|
481
|
+
flowByTarget.set(target, code);
|
|
482
|
+
}
|
|
483
|
+
catch {
|
|
484
|
+
// 读面失败不阻塞写面(与 radioConditionWarnings 同策略:读不到就不评)
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
const findings = [];
|
|
488
|
+
for (const node of creates) {
|
|
489
|
+
const target = String(node.targetObjectCode);
|
|
490
|
+
const flowCode = flowByTarget.get(target);
|
|
491
|
+
if (flowCode === undefined)
|
|
492
|
+
continue;
|
|
493
|
+
const nodeCode = String(node.nodeCode ?? "");
|
|
494
|
+
if (node.startWorkflow === undefined || node.startWorkflow === false) {
|
|
495
|
+
findings.push({
|
|
496
|
+
code: "rule-create-target-has-flow",
|
|
497
|
+
nodeCode,
|
|
498
|
+
key: "startWorkflow",
|
|
499
|
+
severity: "warning",
|
|
500
|
+
message: `CREATE 目标模型 ${target} 已发布流程 ${flowCode}:不传 startWorkflow 时规则建行**不发起流程**(实测 2026-10-27,行落库即止)。` +
|
|
501
|
+
`要让规则新建的行直接进流程:显式 startWorkflow:true + targetWorkflowCode:"${flowCode}",且 originatorValue 必须是用户对象数组 [{"id":"<userId>","name":"<名>"}](id 用 org.user.search 取;传对象 {} 或无效 id 会让**触发表单的提交**直接 50000,实测)。` +
|
|
502
|
+
`注意:设计器里手工编辑该节点可打开此开关——API 与设计器两侧配置可能漂移,改完用 rule.get 回读核对 startWorkflow/targetWorkflowCode。`,
|
|
503
|
+
});
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
const raw = typeof node.originatorValue === "string" ? node.originatorValue.trim() : node.originatorValue;
|
|
507
|
+
let parsed = raw;
|
|
508
|
+
if (typeof raw === "string" && raw.length > 0) {
|
|
509
|
+
try {
|
|
510
|
+
parsed = JSON.parse(raw);
|
|
511
|
+
}
|
|
512
|
+
catch {
|
|
513
|
+
parsed = null;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
const originatorValid = Array.isArray(parsed)
|
|
517
|
+
&& parsed.length > 0
|
|
518
|
+
&& parsed.every((entry) => entry && typeof entry === "object" && typeof entry.id === "string" && entry.id.length > 0);
|
|
519
|
+
if (!originatorValid) {
|
|
520
|
+
findings.push({
|
|
521
|
+
code: "rule-create-startworkflow-originator-shape",
|
|
522
|
+
nodeCode,
|
|
523
|
+
key: "originatorValue",
|
|
524
|
+
message: `CREATE "${nodeCode}" 开了 startWorkflow 但 originatorValue 不是用户对象数组:期望 [{"id":"<userId>","name":"<名>"}]。` +
|
|
525
|
+
`实测(2026-10-27,1.1.25):传对象 {} → 触发表单提交 50000(fastjson "expect '[', but {");数组里 id 无效 → 50000「业务规则发起人不存在」。` +
|
|
526
|
+
`id 用 org.user.search 取真实用户后再写。`,
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
// 「startWorkflow:true 必须带 targetWorkflowCode」由既有的离线同步检查
|
|
530
|
+
// rule-graph-create-workflow-code 负责(rule-graph.ts:2055),这里不重复——
|
|
531
|
+
// 但流程码要能对上:给了但不是该模型的已发布流程时补一条 error。
|
|
532
|
+
if (typeof node.targetWorkflowCode === "string" && node.targetWorkflowCode.length > 0 && node.targetWorkflowCode !== flowCode) {
|
|
533
|
+
findings.push({
|
|
534
|
+
code: "rule-create-startworkflow-target-mismatch",
|
|
535
|
+
nodeCode,
|
|
536
|
+
key: "targetWorkflowCode",
|
|
537
|
+
message: `CREATE "${nodeCode}" 的 targetWorkflowCode="${node.targetWorkflowCode}" 不是目标模型 ${target} 的已发布流程(应为 "${flowCode}")——发到不匹配的流程会运行期失败。`,
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
return findings;
|
|
542
|
+
}
|
|
449
543
|
async function radioConditionWarnings(session, schemaCode, nodes) {
|
|
450
544
|
const sources = new Set();
|
|
451
545
|
const collect = (condition) => {
|
|
@@ -659,6 +753,8 @@ export async function executeRuleSaveWrite(session, args) {
|
|
|
659
753
|
const blockers = [...prepared.warnings, ...validateRuleNodeGraph(preparedGraph, args.ruleCode, typeof args.schemaCode === "string" ? args.schemaCode : undefined, carriedNodeCodes ? { carriedNodeCodes } : {})];
|
|
660
754
|
// 1.2: needs the published field types (pure lint cannot see them).
|
|
661
755
|
blockers.push(...(await radioConditionWarnings(session, args.schemaCode, Array.isArray(preparedGraph.nodes) ? preparedGraph.nodes : [])));
|
|
756
|
+
// r24 反馈(2026-10-27 真机四格矩阵):CREATE 目标带已发布流程时的发起开关教学/拦截。
|
|
757
|
+
blockers.push(...(await createNodeFlowStartWarnings(session, Array.isArray(preparedGraph.nodes) ? preparedGraph.nodes : [])));
|
|
662
758
|
// Ticket 113: warning-severity findings (e.g. unknown mapping keys) do not
|
|
663
759
|
// gate the write; they surface in the readback as lintWarnings.
|
|
664
760
|
const warningFindings = blockers.filter((blocker) => blocker.severity === "warning");
|