Files
writing-agent/src/dictate/round.ts
T
chenmoranzhiandCursor 1cc4ae557a 创作一轮改为先选题再填写,减少工具空转和重复覆盖。
上下文按规则、历史、产物分段拼装,便于吃提示缓存并核对费用。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-07 23:48:20 +08:00

722 lines
25 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 一句用户话:先用 select_nodes 定目标节点,程序注入这些节点的写法,
* 再在同一条消息前缀上循环 insert。已通过的不再覆盖。
* 没有未通过项时,同一次请求里的 reply_user 才给用户。
* 填写步若未调用 reply_user,程序再打一轮瘦回复(只带本轮刚落盘产物);填写提示不提及此事。
*/
import { randomUUID } from "node:crypto";
import type { DriverStepOutcome, DriverToolCall } from "../runtime/driver.js";
import type { AgentDriver } from "../runtime/driver.js";
import type { ChatMessage, LlmProvider, UsageDetail, UsageSink } from "../llm/client.js";
import { DICTATE_REPLY_TOOL, DICTATE_SELECT_TOOL, DICTATE_WRITE_LOOP_TOOLS } from "./tools.js";
import { buildDictateContext, type DictateSegmentChars } from "./context.js";
import {
CREATION_PLAN_SCHEMA,
CREATION_PLAN_TAG,
formatCreationPlanCapabilityCatalog,
formatCreationPlanProgress,
parseCreationPlan,
} from "./creation-plan.js";
import { isDictateOpeningTag } from "./experience-anchor.js";
import {
buildDictateReplyPrompt,
buildDictateSelectPrompt,
buildDictateSystemPrompt,
formatDictateRoundTail,
type DictatePromptTailOpts,
} from "./prompt.js";
import {
parseDictateModuleReadState,
readLibraryEntries,
serializeDictateModuleReadState,
DICTATE_MODULE_READ_STATE_TAG,
type DictateModuleReadPayload,
} from "./read-module.js";
import { resolveModuleForInsertTag } from "./insert-feedback.js";
import type { DictateChatTurn, DictateProduct } from "./types.js";
import { isAllowedProductTag } from "./types.js";
import { VALUE_MAP_TAG } from "../skills/value-map.js";
import { VARIABLE_CATALOG_TAG } from "../skills/variable-catalog.js";
const READ_EXEMPT = new Set([
"narrative",
"variable-context",
"context-order",
"worker-spec",
"refine",
]);
type Apply = (calls: DriverToolCall[]) => DriverStepOutcome | Promise<DriverStepOutcome>;
export type PlannedRoundParams = {
llm: LlmProvider;
dialogue: DictateChatTurn[];
handlers: {
listProducts: () => DictateProduct[];
readTag: (tag: string) => string | undefined;
writeTag: (tag: string, content: string) => void;
prepareOpening?: () => void;
/** 本轮刚写入 tag 的能力回复口径(探测/原则/自评关注) */
lookupInsertFeedback?: (tag: string) => string | undefined;
onThinkingDelta?: (delta: string) => void;
onThinkingDone?: (text: string) => void;
};
moduleCatalog?: import("../skills/creation-flow.js").ModuleCatalog | null;
moduleReadIndex?: ReadonlyMap<string, DictateModuleReadPayload>;
moduleGuide?: string;
recipeExamples?: string;
recipeName?: string;
recipeBrief?: string;
systemPrompt?: string;
};
type PlanInsert = {
tag: string;
moduleId: string;
libraryId?: string;
entryIds: string[];
reason?: string;
};
type SelectPlan = {
clarify: string[];
inserts: PlanInsert[];
deletes: string[];
/** 模型对象上的字段名,空计划时写进回复,方便核对解析有没有对上 */
keys: string[];
};
type ItemFailure = { tag: string; error: string };
const INSERT_ATTEMPTS = 4;
export function createPlannedDictateDriver(params: PlannedRoundParams): AgentDriver {
return {
async run(input) {
const reply = await runPlannedDictateRound(params, input.handleStep);
return { iterations: reply.modelCalls, stop: { kind: "text", content: reply.text } };
},
};
}
async function runPlannedDictateRound(
params: PlannedRoundParams,
apply: Apply,
): Promise<{ text: string; modelCalls: number }> {
const turnId = randomUUID();
const fillSystem = params.systemPrompt?.trim() || buildDictateSystemPrompt();
const tailOpts = tailState(params);
const selectTail = formatDictateRoundTail({ ...tailOpts, phase: "select" });
const selectCtx = buildDictateContext({
systemPrompt: buildDictateSelectPrompt(),
products: params.handlers.listProducts(),
dialogue: params.dialogue,
phase: "select",
tail: selectTail,
});
const selected = await params.llm.completeWithTools(selectCtx.messages, {
caller: "dictate_agent",
label: "对话落盘 选题",
tools: [DICTATE_SELECT_TOOL],
toolChoice: { type: "function", function: { name: "select_nodes" } },
usageDetail: detail(turnId, "select", 0, selectCtx.segmentChars),
usageSink: sinkFor(turnId, "select"),
});
if (selected.reasoning?.trim()) params.handlers.onThinkingDone?.(selected.reasoning.trim());
const selectCall = selected.toolCalls.find((call) => call.name === "select_nodes") ?? selected.toolCalls[0];
const plan = parseSelect(selectCall ? safeJson(selectCall.arguments) : {}, params);
if (plan.clarify.length >= 2 && plan.inserts.length === 0 && plan.deletes.length === 0) {
return {
modelCalls: 1,
text: plan.clarify.map((item, index) => `${index + 1}. ${item}`).join("\n"),
};
}
if (plan.inserts.length === 0 && plan.deletes.length === 0) {
const fields = plan.keys.length ? plan.keys.join("、") : "空对象";
return {
modelCalls: 1,
text: `这一轮没有写入产物。选题返回的字段是 ${fields},没有可执行的写入项。可以说得更具体些,或点明先写哪一块。`,
};
}
const blocked = plan.inserts.filter((item) => item.tag.startsWith("!"));
const inserts = plan.inserts.filter((item) => !item.tag.startsWith("!"));
const openings = inserts.filter((item) => isDictateOpeningTag(item.tag));
const rest = inserts.filter((item) => !isDictateOpeningTag(item.tag));
const blockedLines = blocked.map(
(item) => `${item.tag.slice(1)}(${item.reason || "计划未通过程序核验"})`,
);
if (openings.length > 0) params.handlers.prepareOpening?.();
const injected = injectGroup(params, [...rest, ...openings]);
const tail = formatDictateRoundTail({
...tailOpts,
phase: "fill",
checklist: formatChecklist([...rest, ...openings], plan.deletes, []),
});
const ctx = buildDictateContext({
systemPrompt: fillSystem,
products: params.handlers.listProducts(),
dialogue: params.dialogue,
phase: "fill",
injected: injected.text,
tail,
});
const messages: ChatMessage[] = [...ctx.messages];
const passed = new Set<string>();
const pending = new Set([...rest, ...openings].map((item) => item.tag));
const pendingDeletes = new Set(plan.deletes);
const errors = new Map<string, string>();
let replyFromTool = "";
let lastProse = "";
let modelCalls = 1;
for (let attempt = 0; attempt < INSERT_ATTEMPTS && (pending.size > 0 || pendingDeletes.size > 0); attempt++) {
const step = attempt === 0 ? "fill" : "fill-retry";
const filled = await params.llm.completeWithTools(messages, {
caller: "dictate_agent",
label: attempt === 0 ? "对话落盘 填写" : "对话落盘 重写",
tools: DICTATE_WRITE_LOOP_TOOLS,
toolChoice: "required",
usageDetail: {
...detail(turnId, step, attempt, ctx.segmentChars),
plannedTags: [...pending, ...pendingDeletes],
},
usageSink: sinkFor(turnId, step),
});
modelCalls += 1;
if (filled.reasoning?.trim()) params.handlers.onThinkingDone?.(filled.reasoning.trim());
const prose = filled.content?.trim() ?? "";
if (prose) lastProse = prose;
const calls: DriverToolCall[] = filled.toolCalls.map((call, index) => ({
id: call.id || `call-${attempt}-${index}`,
name: call.name,
arguments: call.arguments,
}));
if (calls.length === 0) break;
const settled = await settleWriteStep(params, apply, calls, passed, pending, pendingDeletes, errors);
messages.push(assistantToolMessage(filled.content, calls));
for (const item of settled.results) {
messages.push({ role: "tool", content: item.content, tool_call_id: item.callId });
}
if (pending.size === 0 && pendingDeletes.size === 0) {
replyFromTool = settled.reply;
break;
}
}
let reply = replyFromTool;
if (
!replyFromTool &&
pending.size === 0 &&
pendingDeletes.size === 0 &&
passed.size > 0
) {
reply = await runLeanReply(params, turnId, passed, plan.deletes, lastProse);
modelCalls += 1;
} else if (!reply) {
reply = lastProse;
}
const failed = [...pending, ...pendingDeletes].map(
(tag) => `${tag}(${errors.get(tag) ?? "尚未通过"})`,
);
const pendingLines = [...blockedLines, ...failed];
const text = pendingLines.length
? [reply, `尚未完成:${pendingLines.join("、")}。`].filter(Boolean).join("\n")
: reply;
return { text, modelCalls };
}
async function runLeanReply(
params: PlannedRoundParams,
turnId: string,
passed: Set<string>,
plannedDeletes: string[],
fallbackProse: string,
): Promise<string> {
const writtenTags = [...passed].filter((tag) => !tag.startsWith("del:"));
const deletedTags = [...passed]
.filter((tag) => tag.startsWith("del:"))
.map((tag) => tag.slice(4))
.filter(Boolean);
const products = params.handlers
.listProducts()
.filter((product) => writtenTags.some((tag) => product.tag === tag || product.tag.startsWith(`${tag}#`)));
const feedbackBlocks = writtenTags
.map((tag) => params.handlers.lookupInsertFeedback?.(tag)?.trim())
.filter((text): text is string => Boolean(text));
if (writtenTags.length > 1 && feedbackBlocks.length > 1) {
feedbackBlocks.unshift(
"本轮写入了多个产物。用一段自然回复综合说明本轮变化、共同强项与关键缺口;不要逐模块套标题或重复模板。只有缺口确需用户决定时才提问。",
);
}
const tailOpts = tailState(params);
const deleteNote =
deletedTags.length || plannedDeletes.length
? `本轮已删除:${(deletedTags.length ? deletedTags : plannedDeletes).join("、")}`
: "";
const replyTail = [
formatDictateRoundTail({
...tailOpts,
phase: "reply",
checklist: [
writtenTags.length ? `本轮已写入:${writtenTags.join("、")}` : "",
deleteNote,
]
.filter(Boolean)
.join("\n"),
}),
feedbackBlocks.length ? `## 本轮关注(按能力回复口径)\n\n${feedbackBlocks.join("\n\n")}` : "",
]
.filter(Boolean)
.join("\n\n");
const replyCtx = buildDictateContext({
systemPrompt: buildDictateReplyPrompt(),
products,
dialogue: params.dialogue,
phase: "reply",
tail: replyTail,
});
const filled = await params.llm.completeWithTools(replyCtx.messages, {
caller: "dictate_agent",
label: "对话落盘 回复",
tools: [DICTATE_REPLY_TOOL],
toolChoice: { type: "function", function: { name: DICTATE_REPLY_TOOL.function.name } },
usageDetail: {
...detail(turnId, "reply", 0, replyCtx.segmentChars),
plannedTags: writtenTags,
},
usageSink: sinkFor(turnId, "reply"),
});
if (filled.reasoning?.trim()) params.handlers.onThinkingDone?.(filled.reasoning.trim());
const call = filled.toolCalls.find((item) => item.name === DICTATE_REPLY_TOOL.function.name);
if (call) {
const text = replyText({
id: call.id || "reply",
name: call.name,
arguments: call.arguments,
});
if (text) return text;
}
return filled.content?.trim() || fallbackProse;
}
function assistantToolMessage(content: string | null, calls: DriverToolCall[]): ChatMessage {
return {
role: "assistant",
content,
tool_calls: calls.map((call) => ({
id: call.id,
type: "function" as const,
function: { name: call.name, arguments: call.arguments },
})),
};
}
async function settleWriteStep(
params: PlannedRoundParams,
apply: Apply,
calls: DriverToolCall[],
passed: Set<string>,
pending: Set<string>,
pendingDeletes: Set<string>,
errors: Map<string, string>,
): Promise<{ results: Array<{ callId: string; content: string }>; reply: string }> {
const results: Array<{ callId: string; content: string }> = [];
const forward: DriverToolCall[] = [];
let reply = "";
for (const call of calls) {
if (call.name === DICTATE_REPLY_TOOL.function.name) continue;
const tag = callTag(call, params);
if (call.name === "insert" && tag && passed.has(tag)) {
results.push({
callId: call.id,
content: JSON.stringify({ error: "已通过,本步不要再写", tag }),
});
continue;
}
if (call.name === "delete" && tag && passed.has(`del:${tag}`)) {
results.push({
callId: call.id,
content: JSON.stringify({ error: "已通过,本步不要再删除", tag }),
});
continue;
}
if (call.name === "insert" && tag) {
const args = asRecord(safeJson(call.arguments));
forward.push({ ...call, arguments: JSON.stringify({ ...args, position: tag }) });
continue;
}
forward.push(call);
}
const outcome = await apply(forward);
if (outcome.kind === "continue") {
for (const result of outcome.results) {
results.push(result);
const call = forward.find((item) => item.id === result.callId);
const payload = asRecord(safeJson(result.content));
const tag = call ? callTag(call, params) : "";
if (payload.ok === true && tag) {
if (call?.name === "delete") {
passed.add(`del:${tag}`);
pendingDeletes.delete(tag);
} else {
passed.add(tag);
for (const planned of [...pending]) {
if (planned === tag || tag.startsWith(`${planned}#`)) pending.delete(planned);
}
}
errors.delete(tag);
} else if (typeof payload.error === "string" && tag) {
const planned = [...pending, ...pendingDeletes].find(
(item) => item === tag || tag.startsWith(`${item}#`),
);
errors.set(planned ?? tag, payload.error);
}
}
}
for (const call of calls) {
if (call.name !== DICTATE_REPLY_TOOL.function.name) continue;
const text = replyText(call);
if (pending.size > 0 || pendingDeletes.size > 0) {
const left = [...pending, ...pendingDeletes].join("、");
results.push({
callId: call.id,
content: JSON.stringify({
error: `还有未通过:${left}。这份回复先不给用户。只再写入这些。`,
}),
});
continue;
}
reply = text;
results.push({ callId: call.id, content: JSON.stringify({ ok: true }) });
}
return { results, reply };
}
function callTag(call: DriverToolCall, params: PlannedRoundParams): string {
const args = asRecord(safeJson(call.arguments));
const raw = String(args.position ?? args.tag ?? "").trim();
if (!raw) return "";
return canonicalProductTag(raw, "", params.moduleCatalog)?.tag ?? raw;
}
function replyText(call: DriverToolCall): string {
const args = asRecord(safeJson(call.arguments));
return String(args.text ?? args.reply ?? args.content ?? args.message ?? "").trim();
}
function sinkFor(_turnId: string, _step: string): UsageSink {
return {};
}
function detail(
turnId: string,
step: UsageDetail["step"],
retryIndex: number,
segmentChars: DictateSegmentChars,
): UsageDetail {
return { turnId, step, retryIndex, segmentChars };
}
function tailState(params: PlannedRoundParams): DictatePromptTailOpts {
const products = params.handlers.listProducts();
const plan = parseCreationPlan(
products.find((product) => product.tag === CREATION_PLAN_TAG)?.content,
);
if (plan) {
return {
creationPlanState: formatCreationPlanProgress({
plan,
catalog: params.moduleCatalog,
products,
}),
};
}
return {
capabilityCatalog:
formatCreationPlanCapabilityCatalog(params.moduleCatalog) || params.moduleGuide,
recipeExamples: params.recipeExamples,
recipeName: params.recipeName,
recipeBrief: params.recipeBrief,
moduleGuide: params.moduleGuide,
};
}
function parseSelect(raw: unknown, params: PlannedRoundParams): SelectPlan {
const row = asRecord(raw);
const clarify = stringList(row.clarify);
let deletes = stringList(row.deletes).filter((tag) => isAllowedProductTag(tag));
const inserts: PlanInsert[] = [];
let items = Array.isArray(row.nodes)
? row.nodes
: Array.isArray(row.inserts)
? row.inserts
: [];
if (items.length === 0 && clarify.length < 2 && deletes.length === 0) {
const recovered = recoverSelectWork(row);
items = recovered.items;
deletes = recovered.deletes.filter((tag) => isAllowedProductTag(tag));
}
for (const item of items) {
const rec = asRecord(item);
const moduleIdHint = String(rec.module_id ?? rec.moduleId ?? "").trim();
let tag = String(rec.tag ?? rec.position ?? "").trim();
if (!tag && moduleIdHint) {
tag =
params.moduleCatalog?.modules.find((module) => module.id === moduleIdHint)?.artifact?.trim() ??
"";
}
if (!tag) continue;
const canonical = canonicalProductTag(tag, moduleIdHint, params.moduleCatalog);
if (canonical) tag = canonical.tag;
const resolvedHint = canonical?.moduleId || moduleIdHint;
const module =
params.moduleReadIndex?.get(resolvedHint) ??
params.moduleReadIndex?.get(tag) ??
undefined;
const fromTag = resolveModuleForInsertTag(tag, params.moduleCatalog);
const moduleId = module?.module_id || canonical?.moduleId || fromTag?.id || moduleIdHint;
const entryIds = stringList(rec.entry_ids);
const reason = String(rec.reason ?? "").trim();
if (!isAllowedProductTag(tag) && tag !== VARIABLE_CATALOG_TAG && tag !== VALUE_MAP_TAG) {
inserts.push({ tag: `!${tag || "空"}`, moduleId, entryIds, reason: "产物 tag 不合法" });
continue;
}
const payload = moduleId ? params.moduleReadIndex?.get(moduleId) : undefined;
if (payload && !READ_EXEMPT.has(payload.module_id) && payload.libraries.length) {
const libraryId = String(rec.library_id ?? payload.libraries[0]?.library_id ?? "").trim();
if (entryIds.length > 2 && !reason) {
inserts.push({
tag: `!${tag}`,
moduleId,
entryIds,
reason: "默认最多两个库条目",
});
continue;
}
if (libraryId && entryIds.length) {
const bound = payload.libraries.some((library) => library.library_id === libraryId);
if (!bound) {
inserts.push({ tag: `!${tag}`, moduleId, entryIds, reason: `能力未绑定库 ${libraryId}` });
continue;
}
}
}
inserts.push({
tag,
moduleId,
libraryId: String(rec.library_id ?? "").trim() || undefined,
entryIds,
reason: reason || undefined,
});
}
return { clarify, inserts, deletes, keys: Object.keys(row) };
}
/** 选题没交 inserts 时,从方案正文、operations 或别名字段收出本轮要写的能力,再自动发填写。 */
function recoverSelectWork(row: Record<string, unknown>): {
items: unknown[];
deletes: string[];
} {
if (isCreationPlanShape(row)) {
return { items: [{ tag: CREATION_PLAN_TAG, module_id: "creation-plan" }], deletes: [] };
}
const items: unknown[] = [];
const deletes: string[] = [];
for (const list of [row.modules, row.selections, row.selected, row.writes, row.products, row.operations]) {
for (const item of asItemList(list)) {
const rec = flattenItem(item);
if (
typeof rec.tier === "string" &&
typeof rec.why === "string" &&
rec.tag == null &&
rec.position == null
) {
continue;
}
const op = operationName(rec);
if (op === "read_module" || op === "read_library_entry" || op === "prepare_opening") {
continue;
}
const moduleId = String(rec.module_id ?? rec.moduleId ?? "").trim();
const tag = String(rec.tag ?? rec.position ?? rec.artifact ?? "").trim();
if (op === "delete" || op === "remove") {
if (tag) deletes.push(tag);
continue;
}
if (
op === "declare_variable" ||
op === "undeclare_variable" ||
op === "declare_map" ||
op === "remove_map"
) {
continue;
}
if (!tag && !moduleId) continue;
items.push({
tag,
module_id: moduleId,
...(rec.library_id != null ? { library_id: rec.library_id } : {}),
...(rec.entry_ids != null ? { entry_ids: rec.entry_ids } : {}),
...(rec.reason != null ? { reason: rec.reason } : {}),
});
}
}
return { items, deletes };
}
function isCreationPlanShape(row: Record<string, unknown>): boolean {
if (row.schema === CREATION_PLAN_SCHEMA) return true;
if (!Array.isArray(row.items) || row.items.length === 0) return false;
return row.items.every((item) => {
const rec = asRecord(item);
return (
typeof rec.module_id === "string" &&
rec.module_id.trim().length > 0 &&
typeof rec.tier === "string" &&
rec.tag == null &&
rec.position == null
);
});
}
function injectGroup(
params: PlannedRoundParams,
group: PlanInsert[],
): { text: string } {
const blocks: string[] = [];
const state = parseDictateModuleReadState(
params.handlers.readTag(DICTATE_MODULE_READ_STATE_TAG),
);
let changed = false;
for (const item of group) {
const payload = item.moduleId ? params.moduleReadIndex?.get(item.moduleId) : undefined;
if (!payload) continue;
const catalogEntry = resolveModuleForInsertTag(item.tag, params.moduleCatalog);
if (!READ_EXEMPT.has(payload.module_id) && !catalogEntry?.auto) {
state.modules[payload.module_id] = payload.version;
state.active_module_id = payload.module_id;
changed = true;
}
const parts = [
`### ${payload.module_id} @ ${payload.version}`,
`产物 tag:${item.tag}`,
payload.score_dimensions.length
? `self_score 维度(这次写入必填,name 必须一致):${payload.score_dimensions.join("、")}`
: "",
`#### task\n${payload.task}`,
`#### principles\n${payload.principles}`,
`#### output\n${payload.output}`,
`#### score\n${payload.score}`,
];
if (item.libraryId && item.entryIds.length) {
const loaded = readLibraryEntries({
module: payload,
libraryId: item.libraryId,
entryIds: item.entryIds,
reason: item.reason,
});
if (loaded.entries) {
for (const entry of loaded.entries) {
const row = entry as { id?: string; content?: string; name?: string };
parts.push(`#### 库 ${item.libraryId} / ${row.id ?? ""}\n${row.content ?? ""}`);
}
} else if (loaded.error) {
parts.push(`#### 库读取失败\n${loaded.error}`);
}
}
blocks.push(parts.filter(Boolean).join("\n\n"));
}
if (changed) {
params.handlers.writeTag(DICTATE_MODULE_READ_STATE_TAG, serializeDictateModuleReadState(state));
}
return { text: blocks.join("\n\n") };
}
function canonicalProductTag(
tag: string,
moduleIdHint: string,
catalog: PlannedRoundParams["moduleCatalog"],
): { tag: string; moduleId: string } | null {
const bare = tag.replace(/^(?:用户|设计)\./, "").split("#")[0]?.trim() ?? "";
const entry =
catalog?.modules.find((module) => module.id === moduleIdHint) ??
resolveModuleForInsertTag(tag, catalog) ??
catalog?.modules.find(
(module) =>
module.name === bare ||
module.id === bare ||
module.artifact === tag ||
module.artifact.endsWith(`.${bare}`),
);
if (!entry?.artifact?.trim()) return null;
const hash = tag.indexOf("#");
const suffix = hash > 0 ? tag.slice(hash) : "";
const next =
entry.repeatable && suffix.startsWith("#") && suffix.length > 1
? `${entry.artifact.trim()}${suffix}`
: entry.artifact.trim();
return { tag: next, moduleId: entry.id };
}
function formatChecklist(group: PlanInsert[], deletes: string[], errors: ItemFailure[]): string {
const nodes = group.map((item) => item.moduleId || item.tag).filter(Boolean);
const lines = [
nodes.length ? `第一轮选定的节点:${nodes.join("、")}` : "",
group.length ? `本轮写入:${group.map((item) => item.tag).join("、")}` : "",
deletes.length ? `本轮删除:${deletes.join("、")}` : "",
errors.length
? `只重写未成功项:\n${errors.map((item) => `- ${item.tag}:${item.error}`).join("\n")}`
: "",
];
return lines.filter(Boolean).join("\n");
}
function asItemList(value: unknown): unknown[] {
if (Array.isArray(value)) return value;
const rec = asRecord(value);
const values = Object.values(rec);
if (values.length > 0 && values.every((item) => item && typeof item === "object")) {
return values;
}
return [];
}
function flattenItem(item: unknown): Record<string, unknown> {
if (typeof item === "string") {
const text = item.trim();
if (!text) return {};
if (text.startsWith("设计.") || text.startsWith("用户.")) return { tag: text };
return { module_id: text };
}
const rec = asRecord(item);
const nested = asRecord(rec.arguments ?? rec.args ?? rec.payload);
return { ...rec, ...nested };
}
function operationName(rec: Record<string, unknown>): string {
return String(rec.op ?? rec.name ?? rec.type ?? rec.action ?? "insert").trim();
}
function asRecord(value: unknown): Record<string, unknown> {
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
return value as Record<string, unknown>;
}
function stringList(value: unknown): string[] {
if (!Array.isArray(value)) return [];
return value.map((item) => String(item ?? "").trim()).filter(Boolean);
}
function safeJson(raw: string): unknown {
try {
return JSON.parse(raw);
} catch {
return {};
}
}