@stalfh233/omc-cli 0.4.41 → 0.4.42
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/coverage/m1-coverage-manifest-v1.json +5 -3
- package/dist/call.js +11 -5
- package/dist/cli-contract.js +27 -2
- package/dist/cli.js +11 -0
- package/dist/form-support.js +21 -1
- package/dist/journal.js +73 -0
- package/dist/lanes/app.js +23 -3
- package/dist/lanes/businessrule.js +25 -2
- package/dist/lanes/datarule.js +24 -3
- package/dist/lanes/dictionary.js +34 -3
- package/dist/lanes/field-contract.js +1 -1
- package/dist/lanes/field.js +29 -3
- package/dist/lanes/form-contract.js +1 -1
- package/dist/lanes/form.js +19 -2
- package/dist/lanes/list-view.js +25 -3
- package/dist/lanes/model.js +32 -3
- package/dist/lanes/online-js-contract.js +6 -1
- package/dist/lanes/online-js-coverage.js +1 -1
- package/dist/lanes/online-js.js +79 -2
- package/dist/lanes/report.js +30 -4
- package/dist/lanes/rule-lifecycle.js +28 -3
- package/dist/lanes/workflow-contract.js +1 -1
- package/dist/lanes/workflow.js +241 -16
- package/dist/online-js-source.js +38 -1
- package/dist/rollback-spec.js +121 -0
- package/dist/rollback.js +178 -0
- package/dist/write.js +49 -2
- package/package.json +3 -3
- package/skills/omc-business-configuration.md +6 -4
- package/skills/omc-capabilities.md +3 -3
- package/skills/omc-write-hazards.md +1 -0
package/dist/rollback.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { basename } from "node:path";
|
|
3
|
+
import { ConfigStore, defaultPaths } from "./config-store.js";
|
|
4
|
+
import { SessionManager } from "./session-manager.js";
|
|
5
|
+
import { makeToolContext, buildToolRegistry } from "./tools.js";
|
|
6
|
+
import { runToolCall } from "./call.js";
|
|
7
|
+
import { readJournal, resolveJournalEntry, laterWritesOnTarget } from "./journal.js";
|
|
8
|
+
import { canonicalForVerify, deepDiff } from "./rollback-spec.js";
|
|
9
|
+
import { sha256 } from "./support.js";
|
|
10
|
+
import { emitTransportResult } from "./command-transport.js";
|
|
11
|
+
/** Pure runtime assembly: an injected context (tests) or the default one. */
|
|
12
|
+
function runtime(context) {
|
|
13
|
+
return context ?? makeToolContext({ sessions: new SessionManager(new ConfigStore(defaultPaths())) });
|
|
14
|
+
}
|
|
15
|
+
function resolvedEnvironment(environment) {
|
|
16
|
+
return new ConfigStore(defaultPaths()).resolveEnvironment(environment).name;
|
|
17
|
+
}
|
|
18
|
+
function loadArtifact(entry) {
|
|
19
|
+
if (!existsSync(entry.artifactFile))
|
|
20
|
+
throw new Error(`journal 指向的 artifact 不存在:${basename(entry.artifactFile)}(本机边界:journal 与快照不跨机器迁移)`);
|
|
21
|
+
return JSON.parse(readFileSync(entry.artifactFile, "utf8"));
|
|
22
|
+
}
|
|
23
|
+
function findSpec(toolName) {
|
|
24
|
+
const tool = buildToolRegistry().find((definition) => definition.name === toolName);
|
|
25
|
+
if (!tool)
|
|
26
|
+
throw new Error(`journal 记录的工具 ${toolName} 已不在当前注册表中`);
|
|
27
|
+
if (!tool.rollback)
|
|
28
|
+
throw new Error(`工具 ${toolName} 未声明回滚能力(旧 journal 记录)`);
|
|
29
|
+
return { tool: tool.name, spec: tool.rollback };
|
|
30
|
+
}
|
|
31
|
+
export async function listRollback(options, context) {
|
|
32
|
+
const ctx = runtime(context);
|
|
33
|
+
const environment = resolvedEnvironment(options.environment);
|
|
34
|
+
let entries = readJournal(environment, ctx.artifactsDir);
|
|
35
|
+
if (options.target)
|
|
36
|
+
entries = entries.filter((entry) => entry.target.includes(options.target));
|
|
37
|
+
if (options.tool)
|
|
38
|
+
entries = entries.filter((entry) => entry.tool === options.tool || entry.tool.startsWith(`${options.tool}.`));
|
|
39
|
+
const limit = options.limit ? Number(options.limit) : 40;
|
|
40
|
+
const timeline = [...entries].reverse().slice(0, Number.isSafeInteger(limit) && limit > 0 ? limit : 40);
|
|
41
|
+
return {
|
|
42
|
+
status: "ok",
|
|
43
|
+
environment,
|
|
44
|
+
journalFile: `.omc/journal/${environment}.ndjson(随本项目 artifacts 目录,本机边界:不跨机器)`,
|
|
45
|
+
entries: timeline.length,
|
|
46
|
+
writes: timeline.map((entry) => ({
|
|
47
|
+
seq: entry.seq,
|
|
48
|
+
ts: entry.ts,
|
|
49
|
+
tool: entry.tool,
|
|
50
|
+
target: entry.target,
|
|
51
|
+
rollbackable: entry.rollbackable,
|
|
52
|
+
...(entry.note ? { note: entry.note } : {}),
|
|
53
|
+
artifact: basename(entry.artifactFile),
|
|
54
|
+
})),
|
|
55
|
+
hint: "omc rollback plan <seq> 预览恢复(不写);omc rollback apply <seq> 执行恢复",
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
async function buildPlan(ref, environmentOption, context) {
|
|
59
|
+
const ctx = runtime(context);
|
|
60
|
+
const environment = resolvedEnvironment(environmentOption);
|
|
61
|
+
const entry = resolveJournalEntry(environment, ref, ctx.artifactsDir);
|
|
62
|
+
if (!entry)
|
|
63
|
+
throw new Error(`journal(${environment})中找不到 ${ref}:先 omc rollback list 核对 seq`);
|
|
64
|
+
const unrollbackable = (reason) => ({
|
|
65
|
+
entry,
|
|
66
|
+
spec: findSpec(entry.tool).spec,
|
|
67
|
+
before: undefined,
|
|
68
|
+
artifactArgs: {},
|
|
69
|
+
context: ctx,
|
|
70
|
+
plan: { environmentMatch: true, diff: { changes: [], total: 0 }, laterWrites: [], inverse: { kind: "irreversible", reason } },
|
|
71
|
+
});
|
|
72
|
+
if (!entry.rollbackable)
|
|
73
|
+
return unrollbackable(entry.note ?? "该写的 before 快照捕获失败,无法回滚");
|
|
74
|
+
const artifact = loadArtifact(entry);
|
|
75
|
+
const { spec } = findSpec(entry.tool);
|
|
76
|
+
const artifactArgs = (artifact.args ?? {});
|
|
77
|
+
const before = artifact.before ?? null;
|
|
78
|
+
// Environment fingerprint gate: the restore replays real writes, so the
|
|
79
|
+
// environment must be the SAME one the journal line was recorded against.
|
|
80
|
+
const session = await ctx.sessions.session(environment);
|
|
81
|
+
const currentFingerprint = session.environmentFingerprint();
|
|
82
|
+
if (currentFingerprint !== entry.environmentFingerprint)
|
|
83
|
+
throw new Error(`环境指纹已变化(记录 ${entry.environmentFingerprint.slice(0, 8)}… 当前 ${currentFingerprint.slice(0, 8)}…):底层环境不是写入时的环境,拒绝回滚;请人工核对`);
|
|
84
|
+
const current = await spec.read(session, artifactArgs);
|
|
85
|
+
const laterWrites = laterWritesOnTarget(environment, ctx.artifactsDir, entry.seq, entry.target);
|
|
86
|
+
return {
|
|
87
|
+
entry,
|
|
88
|
+
spec,
|
|
89
|
+
before,
|
|
90
|
+
artifactArgs,
|
|
91
|
+
context: ctx,
|
|
92
|
+
plan: {
|
|
93
|
+
environmentMatch: true,
|
|
94
|
+
currentFingerprint,
|
|
95
|
+
diff: deepDiff(before, current),
|
|
96
|
+
laterWrites,
|
|
97
|
+
inverse: spec.inverse({ before, args: artifactArgs, readback: (artifact.readback ?? {}) }),
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
export async function planRollback(ref, options, context) {
|
|
102
|
+
const loaded = await buildPlan(ref, options.environment, context);
|
|
103
|
+
const { entry, plan } = loaded;
|
|
104
|
+
return {
|
|
105
|
+
status: "ok",
|
|
106
|
+
rollbackOf: { seq: entry.seq, tool: entry.tool, target: entry.target, ts: entry.ts },
|
|
107
|
+
diff: { changedPaths: plan.diff.changes, total: plan.diff.total },
|
|
108
|
+
...(plan.laterWrites.length > 0
|
|
109
|
+
? { warning: `该目标在此写之后还有 ${plan.laterWrites.length} 次写(seq ${plan.laterWrites.map((w) => w.seq).join(", ")});本次回滚会用 before 参数覆盖这些后续变更`, laterWrites: plan.laterWrites.map((w) => ({ seq: w.seq, tool: w.tool, ts: w.ts })) }
|
|
110
|
+
: {}),
|
|
111
|
+
inverse: plan.inverse.kind === "calls"
|
|
112
|
+
? {
|
|
113
|
+
kind: "calls",
|
|
114
|
+
count: plan.inverse.calls.length,
|
|
115
|
+
...(plan.inverse.notes ? { notes: plan.inverse.notes } : {}),
|
|
116
|
+
script: plan.inverse.calls.map((call) => JSON.stringify({ tool: call.tool, args: { ...call.args, environment: entry.environment } })),
|
|
117
|
+
}
|
|
118
|
+
: { kind: "irreversible", reason: plan.inverse.reason, ...(plan.inverse.partial ? { partial: plan.inverse.partial } : {}) },
|
|
119
|
+
hint: "预览不写入;确认后 omc rollback apply <seq>(恢复走完整写闸门,回滚本身也会记 journal,可再回滚)",
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
export async function applyRollback(ref, options, context) {
|
|
123
|
+
const loaded = await buildPlan(ref, options.environment, context);
|
|
124
|
+
const { entry, plan, spec, artifactArgs, before, context: ctx } = loaded;
|
|
125
|
+
if (plan.inverse.kind !== "calls" || plan.inverse.calls.length === 0)
|
|
126
|
+
return {
|
|
127
|
+
status: plan.inverse.kind === "irreversible" ? "irreversible" : "ok",
|
|
128
|
+
rollbackOf: { seq: entry.seq, tool: entry.tool, target: entry.target },
|
|
129
|
+
inverse: plan.inverse,
|
|
130
|
+
message: plan.inverse.kind === "irreversible" ? plan.inverse.reason : "无可执行恢复调用",
|
|
131
|
+
};
|
|
132
|
+
const results = [];
|
|
133
|
+
for (const [index, call] of plan.inverse.calls.entries()) {
|
|
134
|
+
const { exitCode, result } = await runToolCall(call.tool, {
|
|
135
|
+
args: { ...call.args, environment: entry.environment, ...(options.confirmProduction ? { confirmProduction: true } : {}) },
|
|
136
|
+
requestId: `omc-rollback-${entry.seq}-${index + 1}`,
|
|
137
|
+
context: ctx,
|
|
138
|
+
print: () => undefined,
|
|
139
|
+
});
|
|
140
|
+
results.push({ tool: call.tool, exitCode, status: result.status, ...(result.deliveryState ? { deliveryState: result.deliveryState } : {}), ...(result.summary ? { summary: result.summary } : {}) });
|
|
141
|
+
if (exitCode !== 0)
|
|
142
|
+
return { status: "stopped", rollbackOf: { seq: entry.seq, tool: entry.tool }, step: index + 1, results, message: `恢复调用 ${call.tool} 未成功(status=${result.status});已执行的调用已各自记 journal,可对已完成步骤再 plan` };
|
|
143
|
+
}
|
|
144
|
+
// Authoritative verify: re-read the target and compare against the
|
|
145
|
+
// canonicalized before. "restored" tolerates the write pipeline
|
|
146
|
+
// EXPLICITIFYING platform defaults the bare auto-draft left absent
|
|
147
|
+
// (sheetCode backfill, designer-default permittedAction, canonical key
|
|
148
|
+
// order) — any CHANGED or DROPPED value is a mismatch.
|
|
149
|
+
const session = await ctx.sessions.session(entry.environment);
|
|
150
|
+
const restored = await spec.read(session, artifactArgs);
|
|
151
|
+
const beforeSha = before === null ? null : sha256(canonicalForVerify(before));
|
|
152
|
+
const restoredSha = restored === null ? null : sha256(canonicalForVerify(restored));
|
|
153
|
+
let verify;
|
|
154
|
+
if (beforeSha === restoredSha)
|
|
155
|
+
verify = "restored";
|
|
156
|
+
else {
|
|
157
|
+
const residual = deepDiff(canonicalForVerify(before), canonicalForVerify(restored));
|
|
158
|
+
verify = residual.changes.every((change) => change.before === "<absent>") ? "restored" : "mismatch";
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
status: "ok",
|
|
162
|
+
rollbackOf: { seq: entry.seq, tool: entry.tool, target: entry.target },
|
|
163
|
+
calls: results,
|
|
164
|
+
verify,
|
|
165
|
+
...(verify === "mismatch" ? { beforeSha, restoredSha, message: "恢复调用已完成但读回与 before 存在值冲突(非默认显式化):立即人工核对目标现态(omc rollback plan <seq> 的 diff 可复看差异)" } : {}),
|
|
166
|
+
hint: "本次恢复的每次调用都走了完整写闸门并已记 journal;恢复本身可再回滚(rollback plan <新 seq>)",
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
export async function cmdRollbackList(options, transport) {
|
|
170
|
+
return emitTransportResult(await listRollback(options), transport);
|
|
171
|
+
}
|
|
172
|
+
export async function cmdRollbackPlan(ref, options, transport) {
|
|
173
|
+
return emitTransportResult(await planRollback(ref, options), transport);
|
|
174
|
+
}
|
|
175
|
+
export async function cmdRollbackApply(ref, options, transport) {
|
|
176
|
+
return emitTransportResult(await applyRollback(ref, options), transport);
|
|
177
|
+
}
|
|
178
|
+
//# sourceMappingURL=rollback.js.map
|
package/dist/write.js
CHANGED
|
@@ -16,6 +16,7 @@ import { isPackageStopOutcome } from "./package.js";
|
|
|
16
16
|
import { writeSummary } from "./human-summary.js";
|
|
17
17
|
import { planFor } from "./plan.js";
|
|
18
18
|
import { isUncertainWireError, sha256, wireErrorKind, writeArtifact } from "./support.js";
|
|
19
|
+
import { appendJournalEntry } from "./journal.js";
|
|
19
20
|
import { capabilityArg, confirmProductionArg, environmentArg, WRITE_PROTOCOL } from "./args.js";
|
|
20
21
|
import { plannableArgsRaw } from "./plan.js";
|
|
21
22
|
import { defineTool, ToolRefusalError } from "./tool-types.js";
|
|
@@ -54,6 +55,21 @@ async function governedWrite(ctx, tool, args, execute, dryRunPayload) {
|
|
|
54
55
|
return outcome.result;
|
|
55
56
|
const { session, plan, productionConfirmed } = outcome.admission;
|
|
56
57
|
return ctx.lease.run(session.environmentName, `${tool.name}:${randomUUID().slice(0, 8)}`, async () => {
|
|
58
|
+
// Rollback before-capture: the pre-write state of the declared target is
|
|
59
|
+
// fetched INSIDE the lease (serialized with the write itself) and stored
|
|
60
|
+
// in the artifact. A failed pre-read must never block the wanted write —
|
|
61
|
+
// the journal line then records rollbackable:false with the reason.
|
|
62
|
+
let rollbackBefore = undefined;
|
|
63
|
+
let rollbackCaptureError;
|
|
64
|
+
if (tool.rollback) {
|
|
65
|
+
try {
|
|
66
|
+
rollbackBefore = await tool.rollback.read(session, args);
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
rollbackBefore = undefined;
|
|
70
|
+
rollbackCaptureError = error instanceof Error ? error.message : String(error);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
57
73
|
let executed;
|
|
58
74
|
try {
|
|
59
75
|
executed = await execute(session);
|
|
@@ -106,11 +122,41 @@ async function governedWrite(ctx, tool, args, execute, dryRunPayload) {
|
|
|
106
122
|
readback,
|
|
107
123
|
...(steps !== undefined ? { steps } : {}),
|
|
108
124
|
...(productionConfirmed ? { productionConfirmed: true } : {}),
|
|
125
|
+
...(tool.rollback
|
|
126
|
+
? {
|
|
127
|
+
rollback: {
|
|
128
|
+
target: tool.rollback.target(args),
|
|
129
|
+
beforeSha: rollbackBefore === null ? null : rollbackBefore === undefined ? null : sha256(rollbackBefore),
|
|
130
|
+
...(rollbackCaptureError ? { captureError: rollbackCaptureError } : {}),
|
|
131
|
+
},
|
|
132
|
+
args: plannableArgsRaw(args),
|
|
133
|
+
...(rollbackBefore !== undefined ? { before: rollbackBefore } : {}),
|
|
134
|
+
}
|
|
135
|
+
: {}),
|
|
109
136
|
deliveryState: "DELIVERED",
|
|
110
137
|
recordedAt: new Date().toISOString(),
|
|
111
138
|
};
|
|
112
139
|
const artifactRef = writeArtifact(ctx, `${tool.name}-${randomUUID().slice(0, 8)}.json`, artifact);
|
|
113
|
-
|
|
140
|
+
// Journal AFTER the artifact is durable: one append-only line per
|
|
141
|
+
// delivered governed config write, feeding `omc rollback list/plan/apply`.
|
|
142
|
+
let rollbackRef;
|
|
143
|
+
if (tool.rollback) {
|
|
144
|
+
const entry = appendJournalEntry(ctx.artifactsDir, {
|
|
145
|
+
v: 1,
|
|
146
|
+
environment: session.environmentName,
|
|
147
|
+
environmentFingerprint: session.environmentFingerprint(),
|
|
148
|
+
tool: tool.name,
|
|
149
|
+
target: tool.rollback.target(args),
|
|
150
|
+
beforeSha: artifact.rollback.beforeSha,
|
|
151
|
+
rollbackable: rollbackBefore !== undefined && !rollbackCaptureError,
|
|
152
|
+
...(rollbackCaptureError ? { note: `before-capture failed: ${rollbackCaptureError}` } : {}),
|
|
153
|
+
artifactFile: artifactRef.detailFile,
|
|
154
|
+
artifactId: artifactRef.detailFile.split("/").pop()?.replace(/\.json$/, "") ?? "",
|
|
155
|
+
artifactSha: artifactRef.detailSha256,
|
|
156
|
+
});
|
|
157
|
+
rollbackRef = { seq: entry.seq, hint: `omc rollback plan ${entry.seq}(--env ${entry.environment})` };
|
|
158
|
+
}
|
|
159
|
+
return { status: "ok", deliveryState: "DELIVERED", performed, readback, ...(steps !== undefined ? { steps } : {}), ...(args.requestId ? { requestId: args.requestId, idempotency: idempotencyDeclaration(tool.name) } : {}), ...(productionConfirmed ? { productionConfirmed: true } : {}), ...(rollbackRef ? { rollback: rollbackRef } : {}), summary: writeSummary(tool.name, args, { performed, readback }, "done"), detailFile: artifactRef.detailFile, detailSha256: artifactRef.detailSha256, artifactFingerprint: artifactRef.detailSha256 };
|
|
114
160
|
});
|
|
115
161
|
}
|
|
116
162
|
function idempotencyDeclaration(tool) {
|
|
@@ -138,7 +184,7 @@ function findCompleted(root, requestId, tool, environment, requestFingerprint) {
|
|
|
138
184
|
return undefined;
|
|
139
185
|
}
|
|
140
186
|
/** Low-level write registration factory: shared by every lane adapter. */
|
|
141
|
-
export function writeTool(tools, name, lane, scope, description, inputSchema, execute, preflight, dryRunPayload) {
|
|
187
|
+
export function writeTool(tools, name, lane, scope, description, inputSchema, execute, preflight, dryRunPayload, rollback) {
|
|
142
188
|
const rawExecute = execute;
|
|
143
189
|
const semanticGate = (args) => {
|
|
144
190
|
if (name === "field.create")
|
|
@@ -193,6 +239,7 @@ export function writeTool(tools, name, lane, scope, description, inputSchema, ex
|
|
|
193
239
|
// step can never bypass its lane's readback or semantic refusal.
|
|
194
240
|
writeExecute: rawExecute,
|
|
195
241
|
writePreflight: semanticGate,
|
|
242
|
+
...(rollback ? { rollback } : {}),
|
|
196
243
|
});
|
|
197
244
|
}
|
|
198
245
|
//# sourceMappingURL=write.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stalfh233/omc-cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.42",
|
|
4
4
|
"description": "CloudPivot 8.6 configuration capability CLI — governed, fail-closed, coverage-manifest bounded.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -50,9 +50,9 @@
|
|
|
50
50
|
"access": "public"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@stalfh233/omc-fake-platform": "0.4.
|
|
53
|
+
"@stalfh233/omc-fake-platform": "0.4.42",
|
|
54
54
|
"commander": "14.0.3",
|
|
55
|
-
"omc-core": "0.4.
|
|
55
|
+
"omc-core": "0.4.42",
|
|
56
56
|
"proper-lockfile": "4.1.2",
|
|
57
57
|
"write-file-atomic": "6.0.0",
|
|
58
58
|
"zod": "^3.23.8",
|
|
@@ -153,10 +153,12 @@ rules:
|
|
|
153
153
|
- 审批节点:通常 `visible:true, editable:false, required:false`。
|
|
154
154
|
- 不需要展示的字段明确 `visible:false`。
|
|
155
155
|
|
|
156
|
-
|
|
157
|
-
`
|
|
158
|
-
|
|
156
|
+
**怎么配**(没有单独的权限工具,别把「找不到入口」当不支持→假阻塞):**增量即可**——`workflow.update` 直接发
|
|
157
|
+
`activities:[{activityCode, propertyPermissions:[{propertyCode, visible, editable, required, subPropertyPermissions}]}]`
|
|
158
|
+
(**type 可省**:activityCode 命中草稿节点时类型/名称继承草稿、code 稳定不铸 `_2`;不必先读 `get_draft --full` 整回写。
|
|
159
|
+
`summary` 无需配,工具自动补 `true`),改完 `workflow.publish`。(`summary:true` 才进运行时 `activitySummaries`,工具已自动处理;
|
|
159
160
|
否则 `form/load` 的 `formPermission.dataPermissions` 回退默认,读不到你的 `editable:false`);子表列用 `subPropertyPermissions`。
|
|
161
|
+
**子表属性本身不可设 `required:true`**(接口接受但设计器节点字段配置不支持——工具拒 `workflow-child-table-not-required`):子表整行只配 visible/editable,字段级必填配到子表内部字段。**新增节点用语义 code**(如 `approval_finance`),不要给 `ActivityN` 加 `_1/_2` 后缀;改名才会铸新码(平台不能原地改名)。**子表操作按钮与驳回默认已由工具自动补齐**:填写节点自动配 `buttonPermission.childTableBtns`(每子表 新增/删除/导入/导出 启用、批量编辑禁用),`permittedAction` 按设计器默认归一(填写人不可驳回自己 `showReject:false`;审批节点驳回回发起 `rejectToStart:true`)——显式传 `permittedAction`/`buttonPermission` 可覆盖。
|
|
160
162
|
验收用 `npm run test:live-permission` + **非管理员参与者**(管理员会被后端强制 `e:true` 掩盖)。
|
|
161
163
|
|
|
162
164
|
声明式流程(intent)的 `workflow.template` 用 **`activities` + `routes`**(不是 `nodes`/`edges`):活动 `{id, type: START|END|PARTICIPANT|…, participant?}`,路由 `{preNode, postNode}` 必须指向已声明活动 code;`workflow.update` 只从 `activities` 读节点,写 `nodes` 会在 apply 时 `rules[0] references an unknown activity`(validate 会提前拦)。
|
|
@@ -238,7 +240,7 @@ BIZ_ACTION:
|
|
|
238
240
|
- 直接执行规则:`useLocation: ROW` + `operateType: WITH_OUT_VIEW`。
|
|
239
241
|
- `WITH_VIEW` 是打开表单。
|
|
240
242
|
- FLOW 按钮需要 workflowCode;CLI 会补安全的 actionConfig。
|
|
241
|
-
- **在线 JS 分两条**:本 CLI
|
|
243
|
+
- **在线 JS 分两条**:本 CLI 只做**读写与发布**——**读现存脚本用 `onlinejs.get`**(抽 customScript innerHTML 原文+SHA-256,不必拉 form.get --full 六份 JSON;据此决定 replace 还是默认 INCREMENTAL 并入),用 `onlinejs.compose` 把已定稿的 `source` 组合进平台 skeleton 的 `customScript` 再 publish,回读 `publishedHtmlJson`+SHA-256。**脚本内容的编写/增量合并不在本 CLI**:若当前项目自带在线 JS 编写规范(例如其 `.cpdocs`/技能或项目文档),遵循它;否则向用户/设计者索取已定稿的 `source`。不要在 omc 侧手写或臆造 JS 逻辑。
|
|
242
244
|
|
|
243
245
|
## 运行验收与清理
|
|
244
246
|
|
|
@@ -144,9 +144,9 @@ only the section for the capability you need —
|
|
|
144
144
|
|
|
145
145
|
## 表单在线 JS `onlinejs`
|
|
146
146
|
|
|
147
|
-
- **能做哪些事**:表单在线 JS 的 compose 六元素骨架、`draftHtmlJson.customScript`、发布与 SHA-256
|
|
148
|
-
- **工具**:`onlinejs.compose`
|
|
149
|
-
- **边界 / 陷阱**:仅表单在线 JS;后端在线开发(`/api/api/programming` Java 源码)用二开扩展包,不在 CLI 范围。
|
|
147
|
+
- **能做哪些事**:表单在线 JS 的 compose 六元素骨架、`draftHtmlJson.customScript`、发布与 SHA-256 回读;**读现存脚本**(`onlinejs.get` 抽 customScript innerHTML 原文+SHA-256,不必拉 form.get --full 六份 JSON)(脚本**编写**不在 CLI 范围,遵循项目在线 JS 规范或向用户索取定稿 source)。
|
|
148
|
+
- **工具**:`onlinejs.get`、`onlinejs.compose`
|
|
149
|
+
- **边界 / 陷阱**:仅表单在线 JS;后端在线开发(`/api/api/programming` Java 源码)用二开扩展包,不在 CLI 范围。INCREMENTAL 合并对同内容重发幂等;同头注释的整文件修订会被拒(`onlinejs-revision-replace-required`),走 `onlinejs.get` 取回 → 改 → `replace:true`。
|
|
150
150
|
|
|
151
151
|
## 业务集成 `bizservice`
|
|
152
152
|
|
|
@@ -30,6 +30,7 @@ omc help omc-write-hazards
|
|
|
30
30
|
- 有映射时传非空 JSON 数组字符串,并在 draft/published 两面读回。
|
|
31
31
|
- **运行时写关联值是另一种形状**(`data.save`/`data.update` 的 formData):单选关联(`propertyType 9`)传 **id 字符串或 `{ id }`**,传**数组会稳定 50000**;多选关联(11)才用数组;**人员/组织(50/51/60/61)传 `[{"id","type","name"}]`,`type` 必填**(缺它稳定 50000)——`type` 是 UnitType 索引:**用户=3、部门=1**。用错形状只会拿到一个不透明的 50000。
|
|
32
32
|
- **`mappings` 由表单运行时在用户选择时套用**,服务端 `data.save` **不会**自动回填映射目标字段。API 侧要自己写被映射字段,否则读回为空——别把它误判成“映射配置失败”。
|
|
33
|
+
- **关联映射的事实源在字段行 `options.mappings`,不在表单控件属性**(真机 2026-10-14 实证:改表单 attributes 里的映射会被平台按字段重算覆盖、回读不生效)。改映射走:`field.update` 按完整 options 提交(只改 mappings,其余键原值保留)→ `field.publish` → `form.draft`(**rebuild:false**,保留现有布局与在线JS)重存 → `form.publish` → 回读核对。**别用 field.update 的 cascadeRepublish 刷已带自定义布局/在线JS 的表单**(它会 rebuild 脚手架)。
|
|
33
34
|
- **运行时写值的最小形状**(组合出错只会得到一个不透明的 `50000`/NPE,逐一最小化再合):单选关联(9) → id 字符串 `"<id>"`;**人员/组织(50/51/60/61) → `[{"id":"<id>","type":3,"name":"<名>"}]`,`type` 必填且为整数**(缺/非整数 → `BizField.parseFromClient` 拆箱 NPE→50000;用户=3、部门=1);日期(3) → 字符串 `"2026-09-30 10:00:00"`;子表(8) → 行对象数组 `[{"<子列code>":<值>, ...}]`。先各写一条最小行通过,再组合。
|
|
34
35
|
- **DATE(3) 字段的 `options.format` 必须是平台大写枚举**(`YYYY-MM-DD`、`YYYY-MM-DD HH:mm:ss`、`YYYY年MM月DD日`…),**不是 Java 的 `yyyy-MM-dd`**:平台 `DateBizField` 按字面比较,Java 写法能创建字段但**首次运行时 save 稳定 `50000`(`不支持的format`)**(2026-09-30 R5 实测)。`field.create`/`intent` 会就地拒绝非枚举值。
|
|
35
36
|
|