创作一轮改为先选题再填写,减少工具空转和重复覆盖。
上下文按规则、历史、产物分段拼装,便于吃提示缓存并核对费用。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,721 @@
|
||||
/**
|
||||
* 一句用户话:先用 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 {};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user