@stalfh233/omc-cli 0.4.99 → 0.4.100
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/call.js +8 -3
- package/dist/capability-overview.js +12 -0
- package/dist/cli.js +238 -12
- package/dist/closest-match.js +34 -0
- package/dist/compound-writes.js +310 -0
- package/dist/discovery.js +223 -9
- package/dist/goal-routes.js +127 -57
- package/dist/lanes/report.js +21 -5
- package/dist/refusal-guidance.js +207 -0
- package/dist/skills.js +89 -21
- package/dist/tools.js +35 -0
- package/dist/usage-log.js +21 -2
- package/dist/write.js +2 -2
- package/package.json +3 -3
- package/skills/omc-start-here.md +2 -1
package/dist/goal-routes.js
CHANGED
|
@@ -146,7 +146,13 @@ function bestRecipe(registry, capabilityId, lane, goal, minScore = 3) {
|
|
|
146
146
|
if (tool.lane !== lane)
|
|
147
147
|
continue;
|
|
148
148
|
for (const recipe of tool.executionMetadata?.recipes ?? []) {
|
|
149
|
-
|
|
149
|
+
// r19 ticket 04: an authored ALIAS phrase present in the goal is a much
|
|
150
|
+
// stronger signal than bigram overlap — "…汇总另一个模型的…" must route to
|
|
151
|
+
// formula-aggregation-cross-model, not the title-overlapping dropdown
|
|
152
|
+
// recipe. +20 per exact alias hit keeps authored intent decisive while
|
|
153
|
+
// overlap still ranks within equally aliased candidates.
|
|
154
|
+
const aliasBonus = (recipe.aliases ?? []).reduce((sum, alias) => (goal.includes(alias) ? sum + 20 : sum), 0);
|
|
155
|
+
const score = overlap(goal, `${recipe.title} ${recipe.id} ${(recipe.aliases ?? []).join(" ")}`) + aliasBonus;
|
|
150
156
|
if (score >= minScore && (best === undefined || score > best.score))
|
|
151
157
|
best = { tool: tool.name, recipe: recipe.id, title: recipe.title, score };
|
|
152
158
|
}
|
|
@@ -265,50 +271,39 @@ function sectionPointersFor(ids) {
|
|
|
265
271
|
return pointers;
|
|
266
272
|
}
|
|
267
273
|
/**
|
|
268
|
-
* Ready-to-run methodology reads.
|
|
274
|
+
* Ready-to-run methodology reads — r19 ticket 04: ONE pointer, not a shelf.
|
|
269
275
|
*
|
|
270
|
-
* Round 15
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
+
* Round 15 collapsed per-section reads; r10 F7 made the first section cheap.
|
|
277
|
+
* r18 then showed the residual cost is ATTENTION, not bytes: seven pointers
|
|
278
|
+
* made the agent crawl methodology before acting, and the generic draft
|
|
279
|
+
* template ("3. Draft template") was read and produced nothing on
|
|
280
|
+
* recipe-matched routes. So: a recipe-matched route carries NO pointer (the
|
|
281
|
+
* recipe command is the answer); otherwise exactly one — the first authored
|
|
282
|
+
* pointer that is not the omc-config-draft-review human-gate template (that
|
|
283
|
+
* gate is conversational, not a section to prefetch), falling back to the
|
|
284
|
+
* first pointer when every authored pointer is draft-review.
|
|
276
285
|
*/
|
|
277
|
-
function
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
const readMore = [];
|
|
283
|
-
for (const [topic, sections] of byTopic.entries()) {
|
|
284
|
-
readCommands.push(`omc help ${topic} --section "${sections[0]}"`);
|
|
285
|
-
for (const section of sections.slice(1))
|
|
286
|
-
readMore.push(`omc help ${topic} --section "${section}"`);
|
|
287
|
-
}
|
|
288
|
-
return { readCommands, readMore };
|
|
286
|
+
function pickSectionPointer(pointers, recipeMatched) {
|
|
287
|
+
if (recipeMatched || pointers.length === 0)
|
|
288
|
+
return [];
|
|
289
|
+
const specific = pointers.find((pointer) => pointer.topic !== "omc-config-draft-review");
|
|
290
|
+
return [specific ?? pointers[0]];
|
|
289
291
|
}
|
|
292
|
+
const FIRST_SECTION_INLINE_BUDGET_BYTES = 2048;
|
|
290
293
|
/**
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
* IMMEDIATELY by fetching the first readSection — one extra process +
|
|
296
|
-
* round-trip every task. The first section now rides inline when it fits the
|
|
297
|
-
* inline budget (~2 KB); larger sections stay pointer-only so `next` never
|
|
298
|
-
* balloons.
|
|
294
|
+
* r19 ticket 04: a multi-capability ladder already carries tool maps and
|
|
295
|
+
* recipes, so its inline section budget is tighter — the 4096B `next` ceiling
|
|
296
|
+
* must survive a ladder + a 2.5KB section (r18: "工作流审批流程" would have
|
|
297
|
+
* been 4729B). Single-capability routes keep the fuller budget (F9).
|
|
299
298
|
*/
|
|
300
|
-
const
|
|
301
|
-
function routeReads(readSections) {
|
|
302
|
-
|
|
303
|
-
//
|
|
304
|
-
//
|
|
305
|
-
const budgeted = readSections.map((pointer) => ({ ...pointer, bytes: sectionBytes(pointer) }));
|
|
306
|
-
// Inline the FIRST pointer that fits the budget (order = relevance); an
|
|
307
|
-
// oversized head section falls through to the next fitting one rather than
|
|
308
|
-
// dropping the inline content entirely.
|
|
299
|
+
const LADDER_INLINE_BUDGET_BYTES = 1200;
|
|
300
|
+
function routeReads(readSections, recipeMatched = false, inlineBudget = FIRST_SECTION_INLINE_BUDGET_BYTES) {
|
|
301
|
+
const budgeted = pickSectionPointer(readSections, recipeMatched).map((pointer) => ({ ...pointer, bytes: sectionBytes(pointer) }));
|
|
302
|
+
// Inline the ONE pointer's content when it fits the budget: the agent then
|
|
303
|
+
// needs zero extra calls for methodology (F9, r10-friction).
|
|
309
304
|
let firstSection;
|
|
310
|
-
for (const pointer of budgeted
|
|
311
|
-
if (pointer.bytes > 0 && pointer.bytes <=
|
|
305
|
+
for (const pointer of budgeted) {
|
|
306
|
+
if (pointer.bytes > 0 && pointer.bytes <= inlineBudget) {
|
|
312
307
|
const resolved = readSkillSection(pointer.topic, pointer.section);
|
|
313
308
|
if (resolved) {
|
|
314
309
|
firstSection = { topic: pointer.topic, section: pointer.section, bytes: pointer.bytes, content: resolved.section };
|
|
@@ -316,15 +311,19 @@ function routeReads(readSections) {
|
|
|
316
311
|
}
|
|
317
312
|
}
|
|
318
313
|
}
|
|
319
|
-
|
|
320
|
-
|
|
314
|
+
return {
|
|
315
|
+
readSections: budgeted,
|
|
316
|
+
readCommands: budgeted.map((pointer) => `omc help ${pointer.topic} --section "${pointer.section}"`),
|
|
317
|
+
readHint: READ_HINT,
|
|
318
|
+
...(firstSection !== undefined ? { firstSection } : {}),
|
|
319
|
+
};
|
|
321
320
|
}
|
|
322
321
|
/** UTF-8 byte length of one methodology section (best-effort; 0 when unreadable). */
|
|
323
322
|
function sectionBytes(pointer) {
|
|
324
323
|
const resolved = readSkillSection(pointer.topic, pointer.section);
|
|
325
324
|
return resolved ? Buffer.byteLength(resolved.section, "utf8") : 0;
|
|
326
325
|
}
|
|
327
|
-
const READ_HINT = '
|
|
326
|
+
const READ_HINT = '方法论只挂最相关的一节(readSections[0] 带字节数,`omc help <topic> --section "<节>"` 可回放,firstSection 已内联时就不用再取);其他具体问题(错误码/跨能力语义/某个词)用 `omc help --search "<词>"` 一步全文检索;不要 `--all` 读整篇。';
|
|
328
327
|
function orderedCapabilities(ids, index) {
|
|
329
328
|
const selected = new Set(ids);
|
|
330
329
|
const nodes = [...index.nodeById.values()];
|
|
@@ -434,6 +433,21 @@ const COMPOSER_ROUTES = Object.freeze([
|
|
|
434
433
|
]);
|
|
435
434
|
/** Test seam: the composer registry, so a template typo can be caught offline. */
|
|
436
435
|
export const COMPOSER_ROUTES_FOR_TEST = COMPOSER_ROUTES;
|
|
436
|
+
/**
|
|
437
|
+
* r19 ticket 04 — the ONE question that unlocks each composer. A composer
|
|
438
|
+
* skeleton always carries placeholders, so a composer match is a `needs-input`
|
|
439
|
+
* first answer by definition; the question names exactly what to fill in.
|
|
440
|
+
*/
|
|
441
|
+
const COMPOSER_QUESTIONS = Object.freeze({
|
|
442
|
+
"model-purge": "要清理哪个模型?给我 schemaCode(不确定就先跑下面的只读判别动作拿清单)",
|
|
443
|
+
"model-scaffold": "给我模型 code、显示名和字段清单(每字段 code/名称/类型;子表放 subFields),我一条命令建完",
|
|
444
|
+
"approval-flow": "目标模型 schemaCode 和每级审批节点(节点名@角色code,多级用重复 --steps)是什么?",
|
|
445
|
+
compute: "目标模型 schemaCode、结果字段 code、公式是什么?(子表条件聚合再加 --where \"子列 = 值\")",
|
|
446
|
+
"rule-when": "目标模型 schemaCode、触发时机(Create/Update…)、条件(--when)和动作(--then)各是什么?",
|
|
447
|
+
"form-default": "目标模型 schemaCode、字段 code 和默认值是什么?",
|
|
448
|
+
});
|
|
449
|
+
/** The read-only discriminator that supplies a missing schema code. */
|
|
450
|
+
const LIST_MODELS_ACTION = "omc call model.list --args '{}'";
|
|
437
451
|
function matchComposer(goal, mode) {
|
|
438
452
|
// A composer is a WRITE path: reads and scouting asks must keep their ladder.
|
|
439
453
|
if (mode !== "build" && mode !== "modify")
|
|
@@ -471,11 +485,19 @@ function wholePackageRoute(registry, goal, index) {
|
|
|
471
485
|
primary: "intent",
|
|
472
486
|
atomicSupplement: { note: "intent 已覆盖下列能力的创建;这些原子工具只用于 intent 未覆盖面(删除/生命周期/运行数据/精确 patch):", ladder },
|
|
473
487
|
...routeReads(sectionPointersFor(ordered)),
|
|
474
|
-
|
|
488
|
+
// r19 ticket 04: readiness (discover_environment) left firstCommands —
|
|
489
|
+
// the probe executed it 0 times in 184 calls; it rides its own field now.
|
|
490
|
+
readiness: DISCOVERY_AUTHORITY.readiness,
|
|
491
|
+
firstCommands: ["omc intent schema"],
|
|
475
492
|
write: true,
|
|
476
493
|
},
|
|
477
494
|
write: true,
|
|
478
495
|
intent: intentGuidance(),
|
|
496
|
+
firstAnswer: {
|
|
497
|
+
state: "ready",
|
|
498
|
+
action: "omc intent schema",
|
|
499
|
+
why: "整包需求走声明式意图:先看 intent YAML 的键位与可运行示例,再按 starter 写文件(validate → plan → apply)",
|
|
500
|
+
},
|
|
479
501
|
};
|
|
480
502
|
}
|
|
481
503
|
/** A configuration read-back ask that names no capability ("给我一份现状说明"). */
|
|
@@ -490,8 +512,13 @@ function readOverviewRoute(registry, goal, index) {
|
|
|
490
512
|
confident: true,
|
|
491
513
|
mode: "read",
|
|
492
514
|
match: { kind: "read-overview", ref: ordered.join("→"), capabilities: ordered },
|
|
493
|
-
route: { ladder, ...routeReads(sectionPointersFor(["environment"])),
|
|
515
|
+
route: { ladder, ...routeReads(sectionPointersFor(["environment"])), readiness: DISCOVERY_AUTHORITY.readiness, firstCommands: [LIST_MODELS_ACTION], write: false, readback: true },
|
|
494
516
|
write: false,
|
|
517
|
+
firstAnswer: {
|
|
518
|
+
state: "ready",
|
|
519
|
+
action: LIST_MODELS_ACTION,
|
|
520
|
+
why: "盘点从模型清单开始:一次调用拿到全部 schemaCode,后续读面(inspect/field.list)据此展开",
|
|
521
|
+
},
|
|
495
522
|
};
|
|
496
523
|
}
|
|
497
524
|
export function goalRoute(registry, goal) {
|
|
@@ -522,7 +549,8 @@ export function goalRoute(registry, goal) {
|
|
|
522
549
|
verify: "omc capabilities / omc capability <id>(运行时清单才是权威边界;能力表只是阅读辅助)",
|
|
523
550
|
topics: ["omc-requirement-analysis", "omc-capability-scouting"].filter((topic) => availableTopics.has(topic)),
|
|
524
551
|
},
|
|
525
|
-
route: { write: false, readback: false, firstCommands: ["omc capabilities", "omc capability <id>", "omc help omc-capability-scouting"] },
|
|
552
|
+
route: { write: false, readback: false, readiness: DISCOVERY_AUTHORITY.readiness, firstCommands: ["omc capabilities", "omc capability <id>", "omc help omc-capability-scouting"] },
|
|
553
|
+
firstAnswer: { state: "blocked", why: boundary.note },
|
|
526
554
|
};
|
|
527
555
|
}
|
|
528
556
|
// Composer before the capability solver: a recurring write SHAPE has a single
|
|
@@ -542,14 +570,22 @@ export function goalRoute(registry, goal) {
|
|
|
542
570
|
composer: composer.id,
|
|
543
571
|
command: composer.command,
|
|
544
572
|
summary: composer.summary,
|
|
545
|
-
|
|
546
|
-
// the
|
|
547
|
-
|
|
573
|
+
readiness: DISCOVERY_AUTHORITY.readiness,
|
|
574
|
+
// The composer IS the first step (its skeleton waits on the ONE
|
|
575
|
+
// question in firstAnswer; discover_environment no longer occupies
|
|
576
|
+
// the head slot — r18 executed it 0 times in 184 calls).
|
|
577
|
+
firstCommands: [composer.command, composer.usage],
|
|
548
578
|
write: true,
|
|
549
579
|
readback: true,
|
|
550
|
-
...(topics.length > 0 ? { readCommands: topics.map((topic) => `omc help ${topic} --section "<节>"`), readHint: "只在 composer 的 --help 不够用时再读这些 topic 的对应节" } : {}),
|
|
551
580
|
},
|
|
552
581
|
...(topics.length > 0 ? { topics } : {}),
|
|
582
|
+
firstAnswer: {
|
|
583
|
+
state: "needs-input",
|
|
584
|
+
question: COMPOSER_QUESTIONS[composer.id] ?? "补齐命令里的尖括号占位符后运行(形状见 pendingCommand)",
|
|
585
|
+
pendingCommand: composer.command,
|
|
586
|
+
...(composer.id === "model-purge" || composer.id === "compute" ? { action: LIST_MODELS_ACTION } : {}),
|
|
587
|
+
why: composer.summary,
|
|
588
|
+
},
|
|
553
589
|
};
|
|
554
590
|
}
|
|
555
591
|
const scores = [...capabilityScores(registry, goal).entries()].sort((left, right) => right[1] - left[1]);
|
|
@@ -562,6 +598,13 @@ export function goalRoute(registry, goal) {
|
|
|
562
598
|
topics: [...availableTopics],
|
|
563
599
|
write: false,
|
|
564
600
|
mode,
|
|
601
|
+
// Not confident → say so and hand ONE next move instead of a topic shelf.
|
|
602
|
+
firstAnswer: {
|
|
603
|
+
state: "needs-input",
|
|
604
|
+
question: "把目标说成一个具体动作短语(对象 + 动作,如「给 X 模型加一个汇总字段」),我就能给出一步可跑的命令",
|
|
605
|
+
action: "omc help omc-requirement-analysis",
|
|
606
|
+
why: "没命中任何能力线索;不猜路径(precision over recall)",
|
|
607
|
+
},
|
|
565
608
|
};
|
|
566
609
|
const best = scores[0];
|
|
567
610
|
if (best === undefined) {
|
|
@@ -601,6 +644,12 @@ export function goalRoute(registry, goal) {
|
|
|
601
644
|
const readSections = sectionPointersFor(ordered);
|
|
602
645
|
const write = readish ? false : ordered.some((id) => routeById.get(id)?.tools.some((name) => registryTools.get(name)?.kind === "write"));
|
|
603
646
|
const whole = isWholePackageWrite(mode, goal, ordered.length);
|
|
647
|
+
// r19 ticket 04: the ladder's first RECIPE command is the first answer
|
|
648
|
+
// when one exists (its describe carries the copyable firstCall shape);
|
|
649
|
+
// otherwise the only missing input is the target object code.
|
|
650
|
+
const recipeCommands = ladder.flatMap((step) => step.recipe?.command ? [step.recipe.command] : []);
|
|
651
|
+
const firstAction = recipeCommands[0];
|
|
652
|
+
const firstRecipeId = ladder.find((step) => step.recipe) && (ladder.find((step) => step.recipe).recipe.id);
|
|
604
653
|
// When intent is the primary path, the atomic ladder is only a POINTER to the
|
|
605
654
|
// capabilities (dogfood R1: repeating every tool map was ~60% unused bytes).
|
|
606
655
|
const atomicSupplement = { note: "intent 已覆盖这些能力的创建;原子工具只用于 intent 未覆盖面:", ladder: ladder.map((step) => ({ capability: step.capability, lane: step.lane, firstCommand: step.firstCommand ?? `omc commands --json --compact --lane ${step.lane}` })) };
|
|
@@ -612,13 +661,14 @@ export function goalRoute(registry, goal) {
|
|
|
612
661
|
match: { kind: readish ? "read-ladder" : "ladder", ref: ordered.join("→"), capabilities: ordered },
|
|
613
662
|
route: {
|
|
614
663
|
...(whole ? { primary: "intent", atomicSupplement } : { ladder }),
|
|
615
|
-
...routeReads(readSections),
|
|
664
|
+
...routeReads(readSections, recipeCommands.length > 0, LADDER_INLINE_BUDGET_BYTES),
|
|
665
|
+
readiness: DISCOVERY_AUTHORITY.readiness,
|
|
616
666
|
// r18: when a step already carries a goal-matched recipe, the generic
|
|
617
667
|
// first section ("3. Draft template") is noise — the probe read it and
|
|
618
668
|
// got nothing. Say that the recipe command is the answer and the
|
|
619
669
|
// sections are only a fallback.
|
|
620
|
-
...(
|
|
621
|
-
? { readHint: "已命中配方:先跑 firstCommands 里的 `omc describe … --example …`
|
|
670
|
+
...(recipeCommands.length > 0
|
|
671
|
+
? { readHint: "已命中配方:先跑 firstCommands 里的 `omc describe … --example …` 取形状(首答自带 firstCall 可照抄);方法论节已按需收缩,确实需要时用 `omc help <topic> --section \"<节>\"`。" }
|
|
622
672
|
: {}),
|
|
623
673
|
// Each ladder step already carried a runnable recipe command, but it sat
|
|
624
674
|
// buried at `ladder[i].recipe.command` behind a readiness line — r14's
|
|
@@ -626,8 +676,7 @@ export function goalRoute(registry, goal) {
|
|
|
626
676
|
// graph instead. Surface them at the top level where `firstCommands` is
|
|
627
677
|
// the thing an agent actually reads.
|
|
628
678
|
firstCommands: [
|
|
629
|
-
|
|
630
|
-
...ladder.flatMap((step) => step.recipe?.command ? [step.recipe.command] : []),
|
|
679
|
+
...recipeCommands,
|
|
631
680
|
// r13 #3: `capability workflow --executable` is 23 KB and still did not
|
|
632
681
|
// answer "how do I submit an approval" — the answer is a data.submit
|
|
633
682
|
// recipe, which lives in a DIFFERENT capability. When the goal is about
|
|
@@ -640,7 +689,17 @@ export function goalRoute(registry, goal) {
|
|
|
640
689
|
...(readish ? { readback: true } : {}),
|
|
641
690
|
},
|
|
642
691
|
write,
|
|
643
|
-
...(whole
|
|
692
|
+
...(whole
|
|
693
|
+
? { intent: intentGuidance(), firstAnswer: { state: "ready", action: "omc intent schema", why: "整包需求走声明式意图:先看 intent 的键位与示例,再按 starter 写文件" } }
|
|
694
|
+
: readish
|
|
695
|
+
// A read ask about the model inventory itself is answered BY the
|
|
696
|
+
// inventory call; any other read needs the target object code first.
|
|
697
|
+
? ordered.every((id) => id === "environment" || id === "model")
|
|
698
|
+
? { firstAnswer: { state: "ready", action: LIST_MODELS_ACTION, why: "问的就是模型清单本身:model.list 一次调用直接回答" } }
|
|
699
|
+
: { firstAnswer: { state: "needs-input", question: "要查看哪个对象?给我 schemaCode(或业务名,我列清单核对)", action: LIST_MODELS_ACTION, why: "读路径命中多个能力:readTools 已内联,只差目标对象 code" } }
|
|
700
|
+
: firstAction !== undefined
|
|
701
|
+
? { firstAnswer: { state: "ready", action: firstAction, why: `命中配方 ${firstRecipeId}:describe 首答自带 firstCall 形状,代入你的 schemaCode/字段 code 即可执行` } }
|
|
702
|
+
: { firstAnswer: { state: "needs-input", question: "第一步要操作哪个对象?给我 schemaCode(或模型业务名,我列清单核对)", action: LIST_MODELS_ACTION, why: "未命中配方:需要目标对象 code 才能给出一步可跑的写命令" } }),
|
|
644
703
|
};
|
|
645
704
|
}
|
|
646
705
|
// Single-capability mode: when several capabilities tie, a matching recipe
|
|
@@ -651,7 +710,7 @@ export function goalRoute(registry, goal) {
|
|
|
651
710
|
return fallback;
|
|
652
711
|
const write = readish ? false : route.tools.some((name) => registryTools.get(name)?.kind === "write");
|
|
653
712
|
const recipe = readish ? undefined : bestRecipe(registry, capabilityId, route.lane, goal);
|
|
654
|
-
const firstCommands = [
|
|
713
|
+
const firstCommands = [];
|
|
655
714
|
if (recipe)
|
|
656
715
|
firstCommands.push(`omc describe ${recipe.tool} --example ${recipe.recipe}`);
|
|
657
716
|
if (readish)
|
|
@@ -666,6 +725,7 @@ export function goalRoute(registry, goal) {
|
|
|
666
725
|
if (!readish && /审批|通过|驳回|提交|签收/.test(goal) && (capabilityId === "workflow" || capabilityId === "runtimeData"))
|
|
667
726
|
firstCommands.push("omc describe data.submit --example workflow-backed-data-submit");
|
|
668
727
|
const whole = isWholePackageWrite(mode, goal, 1);
|
|
728
|
+
const recipeCommand = recipe ? `omc describe ${recipe.tool} --example ${recipe.recipe}` : undefined;
|
|
669
729
|
return {
|
|
670
730
|
status: "ok",
|
|
671
731
|
goal,
|
|
@@ -683,13 +743,23 @@ export function goalRoute(registry, goal) {
|
|
|
683
743
|
lane: route.lane,
|
|
684
744
|
...(whole ? { primary: "intent" } : {}),
|
|
685
745
|
...(readish ? { readTools: readToolsInLane(registry, route.lane) } : { tools: route.tools }),
|
|
686
|
-
...routeReads(sectionPointersFor([capabilityId])),
|
|
746
|
+
...routeReads(sectionPointersFor([capabilityId]), recipe !== undefined),
|
|
747
|
+
readiness: DISCOVERY_AUTHORITY.readiness,
|
|
687
748
|
firstCommands,
|
|
688
749
|
write,
|
|
689
750
|
...(readish ? { readback: true } : {}),
|
|
690
751
|
},
|
|
691
752
|
write,
|
|
692
753
|
...(whole ? { intent: intentGuidance() } : {}),
|
|
754
|
+
...(whole
|
|
755
|
+
? { firstAnswer: { state: "ready", action: "omc intent schema", why: "整包需求走声明式意图:先看 intent 的键位与示例,再按 starter 写文件" } }
|
|
756
|
+
: readish
|
|
757
|
+
? capabilityId === "model"
|
|
758
|
+
? { firstAnswer: { state: "ready", action: LIST_MODELS_ACTION, why: "问的就是模型清单本身:model.list 一次调用直接回答" } }
|
|
759
|
+
: { firstAnswer: { state: "needs-input", question: "要查看哪个对象?给我 schemaCode(或业务名,我列清单核对)", action: LIST_MODELS_ACTION, why: `读路径命中能力 ${capabilityId}:readTools 已内联,只差目标对象 code` } }
|
|
760
|
+
: recipeCommand !== undefined
|
|
761
|
+
? { firstAnswer: { state: "ready", action: recipeCommand, why: `命中配方 ${recipe.recipe}:describe 首答自带 firstCall 形状,代入你的 schemaCode/字段 code 即可执行` } }
|
|
762
|
+
: { firstAnswer: { state: "needs-input", question: "要操作哪个对象?给我 schemaCode(或模型业务名,我列清单核对)", action: LIST_MODELS_ACTION, why: "未命中配方:需要目标对象 code 才能给出一步可跑的写命令" } }),
|
|
693
763
|
};
|
|
694
764
|
}
|
|
695
765
|
//# sourceMappingURL=goal-routes.js.map
|
package/dist/lanes/report.js
CHANGED
|
@@ -387,11 +387,27 @@ export function registerReportLane(tools) {
|
|
|
387
387
|
const page = (await wireCall(session, { path: ENDPOINTS.reportGet, query: { code } })).data;
|
|
388
388
|
const objectId = String(page.id ?? page.reportObjectId ?? "");
|
|
389
389
|
if (objectId) {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
390
|
+
try {
|
|
391
|
+
await biCall(session, {
|
|
392
|
+
path: ENDPOINTS.biReportPageSave,
|
|
393
|
+
body: { report: { objectId, title: args.name, attributes: "{}", global: JSON.stringify(DEFAULT_REPORT_GLOBAL) } },
|
|
394
|
+
auth: authOf(args.corpId, code, args.appCode),
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
catch (error) {
|
|
398
|
+
// r19 p14(p9 实证):报表服务未启动时 BI 初始化统一 502,但平台层的
|
|
399
|
+
// 报表页行**已经建了**。裸抛 uncertain 会掩盖这个残留——半成功必须
|
|
400
|
+
// 显式:回执带 halfSuccess 警示 + 清理命令,让 agent 下一调用就能收拾。
|
|
401
|
+
return {
|
|
402
|
+
performed: { errcode: 0, code, appCode: args.appCode, objectId, biInit: "failed" },
|
|
403
|
+
readback: { present: page.code === code, report: reportPageView(page), biInitialized: false },
|
|
404
|
+
halfSuccess: {
|
|
405
|
+
message: `平台层报表页已建(code=${code},objectId=${objectId}),但 BI 初始化失败(${error instanceof Error ? error.message : String(error)})——元数据已残留`,
|
|
406
|
+
cleanup: `omc call report.delete --args '{"code":"${code}"}'(或保留,待报表服务启动后重试 BI 配置)`,
|
|
407
|
+
boundary: "报表服务未启动时 BI 层(/dashboard/v1/*)统一 502——这是环境边界,不要原样重试",
|
|
408
|
+
},
|
|
409
|
+
};
|
|
410
|
+
}
|
|
395
411
|
}
|
|
396
412
|
void created;
|
|
397
413
|
return {
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shape-family refusals: the payload was malformed in a way the CLI can see
|
|
3
|
+
* offline. Each cites the recipe that carries the correct shape.
|
|
4
|
+
*/
|
|
5
|
+
const SHAPE_GATES = Object.freeze({
|
|
6
|
+
"relation-single-array-shape": {
|
|
7
|
+
whatWentWrong: "单选关联字段(propertyType 9)传了数组",
|
|
8
|
+
correctShape: "单选关联只接受 id 字符串或 {id} 对象:{\"字段\":\"<关联行id>\"} 或 {\"字段\":{\"id\":\"<id>\"}};多选关联(11)/人员(50)/组织(51)才用数组",
|
|
9
|
+
recipeCommand: { tool: "data.submit", recipe: "plain-data-submit" },
|
|
10
|
+
doNot: "不要对单选关联继续传数组重试——平台稳定回 50000,不是偶发",
|
|
11
|
+
},
|
|
12
|
+
"submit-date-shape": {
|
|
13
|
+
whatWentWrong: "日期字段的值形状不对(毫秒时间戳会落库 NULL)",
|
|
14
|
+
correctShape: "字符串且匹配字段 format:日期字段 \"YYYY-MM-DD\";日期时间字段 ISO8601 带时区 \"2026-01-01T10:00:00+08:00\"",
|
|
15
|
+
recipeCommand: { tool: "data.submit", recipe: "plain-data-submit" },
|
|
16
|
+
doNot: "不要传毫秒数(如 1790000000000)——不报错但落库 NULL,读回才发现",
|
|
17
|
+
},
|
|
18
|
+
"submit-child-rows-missing-id": {
|
|
19
|
+
whatWentWrong: "更新已有记录时,子表行没带 id",
|
|
20
|
+
correctShape: "更新时子表行必须带平台 id(新增行才省略);无 id 的行会被当**新增**插入,不判重",
|
|
21
|
+
command: undefined,
|
|
22
|
+
recipeCommand: { tool: "data.update", recipe: "plain-data-submit" },
|
|
23
|
+
doNot: "不要用同一批无 id 子行重试 update——每次都会再插一遍(r17 实测 2 行变 8 行)",
|
|
24
|
+
},
|
|
25
|
+
"submit-approval-agree-misplaced": {
|
|
26
|
+
whatWentWrong: "agree 放在了 approval 对象里,平台只读顶层 agree",
|
|
27
|
+
correctShape: "通过/驳回用顶层参数:{\"agree\": true|false, \"approvalNote\": \"意见\"};approval 对象只是评论载体(content/relUsers)",
|
|
28
|
+
recipeCommand: { tool: "data.submit", recipe: "workflow-backed-data-submit" },
|
|
29
|
+
doNot: "approval.agree 会被静默忽略且仍按通过处理(r18b 真机:agree:false 照样 APPROVE)——原地重试必然得到同样的错误结果",
|
|
30
|
+
},
|
|
31
|
+
"submit-approval-opinion-misplaced": {
|
|
32
|
+
whatWentWrong: "opinion 放在了 approval 对象里,平台不读它",
|
|
33
|
+
correctShape: "审批意见用顶层 approvalNote;approval.content 是评论内容,不是审批意见",
|
|
34
|
+
recipeCommand: { tool: "data.submit", recipe: "workflow-backed-data-submit" },
|
|
35
|
+
doNot: "改 approval.opinion 重试无意义——字段被静默忽略",
|
|
36
|
+
},
|
|
37
|
+
"aggregation-self-schema-cycle": {
|
|
38
|
+
whatWentWrong: "聚合目标是自己的子表——平台 publishFormula 必然判环(checkCycleOfAggregation:1032),无条件 305003",
|
|
39
|
+
correctShape: "聚合 schemaCode 指向**另一个模型**的主表标量列;要子表合计先物化到来源主表再聚合",
|
|
40
|
+
recipeCommand: { tool: "field.create", recipe: "formula-aggregation-cross-model" },
|
|
41
|
+
doNot: "同模型子表聚合是平台死路(8.6.26 与 1.1.25 逐行相同),改 payload 无用;也不要删平台自动生成的 t100 CALCULATE_RULE 镜像行",
|
|
42
|
+
},
|
|
43
|
+
"aggregation-target-is-child-schema": {
|
|
44
|
+
whatWentWrong: "聚合指向了子表 schema",
|
|
45
|
+
correctShape: "aggregation.schemaCode 必须是来源模型主表;数据项只能是来源主表的标量列",
|
|
46
|
+
recipeCommand: { tool: "field.create", recipe: "formula-aggregation-cross-model" },
|
|
47
|
+
doNot: "聚合指向子表会让目标模型整个不可写(40000 无明细)且匹配恒 0——先删这个字段配置再继续",
|
|
48
|
+
},
|
|
49
|
+
"aggregation-data-item-not-scalar": {
|
|
50
|
+
whatWentWrong: "聚合数据项不是来源主表的标量列",
|
|
51
|
+
correctShape: "跨模型汇总的数据项只能是来源主表标量列;子表列先在来源模型物化成主表字段",
|
|
52
|
+
recipeCommand: { tool: "field.create", recipe: "formula-aggregation-cross-model" },
|
|
53
|
+
doNot: "直接把子表列写进数据项不会报配置错误——回算静默恒 0,先回来源模型物化",
|
|
54
|
+
},
|
|
55
|
+
"aggregation-large-missing-k": {
|
|
56
|
+
whatWentWrong: "LARGE/SMALL 聚合缺 k(前 N 名的 N)",
|
|
57
|
+
correctShape: "algorithmOptions 里带 k(正整数);LARGE=前 k 大,SMALL=前 k 小",
|
|
58
|
+
recipeCommand: { tool: "field.create", recipe: "formula-aggregation-cross-model" },
|
|
59
|
+
doNot: "缺 k 时平台抛 NumberFormatException 掩码成 50000——重试同参数无用",
|
|
60
|
+
},
|
|
61
|
+
"aggregation-union-result-type": {
|
|
62
|
+
whatWentWrong: "UNION(文本拼接)的 resultType 越界",
|
|
63
|
+
correctShape: "UNION 的 resultType 只能 0 或 1(拼接分隔行为),不是数值型枚举",
|
|
64
|
+
recipeCommand: { tool: "field.create", recipe: "formula-aggregation-cross-model" },
|
|
65
|
+
doNot: "写 2 及以上平台直接 50000——改回 0/1 再发布",
|
|
66
|
+
},
|
|
67
|
+
"rule-graph-validation-failed": {
|
|
68
|
+
whatWentWrong: "规则图形状没过写前校验",
|
|
69
|
+
correctShape: "nodeGraph:nodes(Start/动作/End,系统节点标 isSystemNode)+ routes(preNode→postNode);条件分支用 BRANCH 节点",
|
|
70
|
+
recipeCommand: { tool: "rule.save", recipe: "business-rule-branch-conditional" },
|
|
71
|
+
},
|
|
72
|
+
"rule-graph-routes-empty": {
|
|
73
|
+
whatWentWrong: "规则图缺 routes(节点连线)",
|
|
74
|
+
correctShape: "每条 route {preNode, postNode, routeCondition:true};Start→首节点→…→End 必须连通",
|
|
75
|
+
recipeCommand: { tool: "rule.save", recipe: "business-rule-branch-conditional" },
|
|
76
|
+
},
|
|
77
|
+
"rule-graph-bizaction-mapping-field-missing": {
|
|
78
|
+
whatWentWrong: "BIZ_ACTION 节点的输入映射缺 serviceMethodParameterCode 或 bizCode",
|
|
79
|
+
correctShape: "inputMappings 每条:serviceMethodParameterCode(服务方法参数)↔ bizCode(模型字段 code),codeType 按类型",
|
|
80
|
+
recipeCommand: { tool: "rule.save", recipe: "rule-cross-model-create" },
|
|
81
|
+
missingInputs: [
|
|
82
|
+
{ name: "serviceCode/serviceMethodCode", source: "omc call bizservice.list --args '{}'(已注册的二开服务)" },
|
|
83
|
+
{ name: "bizCode(模型字段)", source: "omc call field.list --args '{\"schemaCode\":\"<模型>\",\"all\":true}'" },
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
"rule-graph-bizmethod-unresolved": {
|
|
87
|
+
whatWentWrong: "规则图引用的二开服务方法不存在或未注册",
|
|
88
|
+
correctShape: "methodMapping.schemaCode/serviceCode/serviceMethodCode 必须是已注册服务的真实 code",
|
|
89
|
+
recipeCommand: { tool: "rule.save", recipe: "rule-cross-model-create" },
|
|
90
|
+
missingInputs: [
|
|
91
|
+
{ name: "serviceCode/serviceMethodCode", source: "omc call bizservice.list --args '{}';方法级详情用 bizservice.get" },
|
|
92
|
+
],
|
|
93
|
+
doNot: "不要凭记忆拼写服务 code——静默不匹配时规则保存成功但运行为空",
|
|
94
|
+
},
|
|
95
|
+
"field-relation-target-missing": {
|
|
96
|
+
whatWentWrong: "关联字段的目标模型缺失或不可解析",
|
|
97
|
+
correctShape: "关联字段 options 里目标模型 schemaCode 必须已存在且已发布",
|
|
98
|
+
missingInputs: [
|
|
99
|
+
{ name: "目标模型 schemaCode", source: "omc call model.list --args '{}'" },
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
/**
|
|
104
|
+
* Platform/permission/environment families: honest "fix this first" with NO
|
|
105
|
+
* retry command — a command that would fail again is worse than none.
|
|
106
|
+
*/
|
|
107
|
+
const FIX_FIRST_GATES = Object.freeze({
|
|
108
|
+
"workflow-start-permission-denied": "40009 有三个独立同码原因:① 发起人角色没挂该流程的发起权限(ouScope ∩ 发起人部门链,全空会静默回退发起人);② 流程未启用/未发布;③ 模型未绑定流程。先 omc call workflow.get_published 核对 ②,再查角色 ouScope——不要反复换参数重试 data.submit",
|
|
109
|
+
"environment-write-disabled": "当前环境 profile 不可写:换可写环境,或让维护者在配置里开 writable——CLI 侧没有绕过开关",
|
|
110
|
+
"production-confirmation-required": "生产环境的写需要显式 confirmProduction:true——确认这次写入真的要落生产再加该参数,不要靠重试",
|
|
111
|
+
"app-not-published": "目标应用未发布:先 workflow/form/field 各面发布(或 app 级发布),再重试运行面操作",
|
|
112
|
+
"submit-workitem-not-mine": "该 workItem 不在当前登录人的待办里:用 omc call workitem.list --args '{\"status\":\"PENDING\"}' 拿**自己的** workItemId,不要用别人的 id 提交",
|
|
113
|
+
"capability-out-of-coverage": "需求不在覆盖面内:这份拒绝就是答案——用 omc capabilities 核对运行时清单,不要换相近工具伪造实现",
|
|
114
|
+
"version-family-evidence-missing": "平台版本证据缺失,写被关闭:先 omc doctor / discover_environment 取版本证据,非 8.6/1.1.25 家族按边界处理",
|
|
115
|
+
});
|
|
116
|
+
/** `tool-input-invalid` and friends: point at the refused tool's own describe. */
|
|
117
|
+
function inputInvalidGuidance(toolName) {
|
|
118
|
+
if (!toolName)
|
|
119
|
+
return undefined;
|
|
120
|
+
return {
|
|
121
|
+
whatWentWrong: "入参没过工具的 schema 校验(缺必填或类型不对)",
|
|
122
|
+
correctShape: "必填参数与类型见 command 指向的 describe(firstCall 自带最小可跑形状)",
|
|
123
|
+
command: `omc describe ${toolName}`,
|
|
124
|
+
doNot: "不要盲目重试——同参数必然同拒绝",
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* r19 p14 — the report BI boundary (`/dashboard/v1/*`). A deployment without
|
|
129
|
+
* the report service answers every BI call with a gateway 502 (live p9
|
|
130
|
+
* evidence); that is an ENVIRONMENT boundary, not a transient failure, so the
|
|
131
|
+
* teaching says which faces still work and to stop retrying. Matched from the
|
|
132
|
+
* message/detail text so both the refused and the frozen (uncertain) envelope
|
|
133
|
+
* paths can carry it.
|
|
134
|
+
*/
|
|
135
|
+
function dashboardBoundaryGuidance() {
|
|
136
|
+
return {
|
|
137
|
+
whatWentWrong: "报表 BI 通道(/dashboard/v1/*)不可达——报表服务未启动(本机有此服务;网关 502,p9 实证)",
|
|
138
|
+
fixFirst: "报表能力暂不优先支持(维护者裁决,先隐藏):不要重试同一子面。平台层仍可用:report.tree / report.update / report.get 存在性判定;确需 BI 详情/数据源/图表/配置时,先启动本机报表服务再深进",
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function mentionsDashboardChannel(payload) {
|
|
142
|
+
return JSON.stringify({ m: payload.message, d: payload.detail, r: payload.reason }).includes("/dashboard/v1/");
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Resolve the authored recipe command against the registry: the guidance must
|
|
146
|
+
* never cite a recipe the registry does not ship (that would be a second,
|
|
147
|
+
* contradictory shape source). Falls back to the tool's describe; if even the
|
|
148
|
+
* tool is unknown, the command is omitted.
|
|
149
|
+
*/
|
|
150
|
+
function resolveCommand(authored, registry) {
|
|
151
|
+
if (!authored.recipeCommand || !registry)
|
|
152
|
+
return undefined;
|
|
153
|
+
const tool = registry.find((entry) => entry.name === authored.recipeCommand.tool);
|
|
154
|
+
const recipe = tool?.executionMetadata?.recipes.find((entry) => entry.id === authored.recipeCommand.recipe);
|
|
155
|
+
if (tool && recipe)
|
|
156
|
+
return `omc describe ${tool.name} --example ${recipe.id}`;
|
|
157
|
+
if (tool)
|
|
158
|
+
return `omc describe ${tool.name}`;
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
export function refusalRecovery(blocker, payload, options = {}) {
|
|
162
|
+
// A refusal that already teaches (write-lock-held carries lock recovery) is
|
|
163
|
+
// not overwritten — single owner per envelope.
|
|
164
|
+
if (payload.recovery !== undefined)
|
|
165
|
+
return payload.recovery;
|
|
166
|
+
// r19 p14: the report BI boundary is matched from the wire path in the
|
|
167
|
+
// message/detail, so both refused and frozen (uncertain) envelopes teach it.
|
|
168
|
+
if (mentionsDashboardChannel(payload))
|
|
169
|
+
return dashboardBoundaryGuidance();
|
|
170
|
+
const shape = SHAPE_GATES[blocker];
|
|
171
|
+
if (shape) {
|
|
172
|
+
const command = resolveCommand(shape, options.registry);
|
|
173
|
+
const { recipeCommand: _drop, ...rest } = shape;
|
|
174
|
+
return { ...rest, ...(command !== undefined ? { command } : {}) };
|
|
175
|
+
}
|
|
176
|
+
const fixFirst = FIX_FIRST_GATES[blocker];
|
|
177
|
+
if (fixFirst)
|
|
178
|
+
return { whatWentWrong: String(payload.message ?? blocker).slice(0, 160), fixFirst };
|
|
179
|
+
if (blocker === "tool-input-invalid")
|
|
180
|
+
return inputInvalidGuidance(options.toolName);
|
|
181
|
+
return undefined;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Attach guidance to a refused result at the `runToolCall` choke point.
|
|
185
|
+
* Idempotent, refuses to overwrite, and leaves non-refused results untouched.
|
|
186
|
+
*/
|
|
187
|
+
export function attachRefusalRecovery(result, toolName, registry) {
|
|
188
|
+
// r19 p14: `/dashboard/v1/*` failures surface as UNCERTAIN (frozen) — the
|
|
189
|
+
// http-status wire kind is classified "uncertain" by design, and frozen
|
|
190
|
+
// envelopes carry `reason`/`detail` but NO `blocker` field. The dashboard
|
|
191
|
+
// boundary is definitive (gateway 502, report service not started), so those
|
|
192
|
+
// frozen envelopes get the teaching too; every other uncertain result stays
|
|
193
|
+
// untouched (never relabel network jitter as an environment boundary).
|
|
194
|
+
const frozenWithBoundary = result.status === "uncertain" && mentionsDashboardChannel(result);
|
|
195
|
+
if (result.status !== "refused" && !frozenWithBoundary)
|
|
196
|
+
return result;
|
|
197
|
+
if (result.recovery !== undefined)
|
|
198
|
+
return result;
|
|
199
|
+
const blocker = typeof result.blocker === "string" ? result.blocker : "";
|
|
200
|
+
if (!blocker && !frozenWithBoundary)
|
|
201
|
+
return result;
|
|
202
|
+
const recovery = refusalRecovery(blocker, result, { registry, toolName });
|
|
203
|
+
if (!recovery)
|
|
204
|
+
return result;
|
|
205
|
+
return { ...result, recovery };
|
|
206
|
+
}
|
|
207
|
+
//# sourceMappingURL=refusal-guidance.js.map
|