新建作品改为直接进入对话落盘,按本局创作方案写能力,并把楼层呈现和零层卡分开。
输入栏同时可以按分组切换模型。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+556
-44
@@ -1,6 +1,7 @@
|
||||
import type { LlmProvider } from "../llm/client.js";
|
||||
import {
|
||||
createLocalLlmDriver,
|
||||
DriverIterationLimitError,
|
||||
type AgentDriver,
|
||||
} from "../runtime/driver.js";
|
||||
import { buildDictateMessages } from "./context.js";
|
||||
@@ -9,11 +10,31 @@ import { DICTATE_TOOL_DEFINITIONS } from "./tools.js";
|
||||
import {
|
||||
isAllowedProductTag,
|
||||
parseDictateOrder,
|
||||
type DictateSelfScore,
|
||||
type DictateChatTurn,
|
||||
type DictateProduct,
|
||||
} from "./types.js";
|
||||
import { DICTATE_MULTI_MODULE_REPLY_HINT } from "./insert-feedback.js";
|
||||
import { resolveRepeatableInsertTag } from "./repeatable-tags.js";
|
||||
import {
|
||||
DICTATE_MULTI_MODULE_REPLY_HINT,
|
||||
resolveModuleForInsertTag,
|
||||
} from "./insert-feedback.js";
|
||||
import { dictateProductFamily, resolveRepeatableInsertTag, splitConcreteInstanceContents } from "./repeatable-tags.js";
|
||||
import type { ModuleCatalog } from "../skills/creation-flow.js";
|
||||
import {
|
||||
CREATION_PLAN_TAG,
|
||||
formatCreationPlanCapabilityCatalog,
|
||||
formatCreationPlanProgress,
|
||||
parseCreationPlan,
|
||||
requiredCreationPlanGaps,
|
||||
validateCreationPlan,
|
||||
} from "./creation-plan.js";
|
||||
import {
|
||||
DICTATE_MODULE_READ_STATE_TAG,
|
||||
parseDictateModuleReadState,
|
||||
readLibraryEntries,
|
||||
serializeDictateModuleReadState,
|
||||
type DictateModuleReadPayload,
|
||||
} from "./read-module.js";
|
||||
import {
|
||||
VARIABLE_CATALOG_TAG,
|
||||
parseVariableCatalog,
|
||||
@@ -34,11 +55,18 @@ import {
|
||||
parseTableDoc,
|
||||
stringifyTableDoc,
|
||||
} from "../blackboard/table-cells.js";
|
||||
import { isDictateOpeningTag } from "./experience-anchor.js";
|
||||
import { canonicalizeInsertedContent } from "../parse/json-doc.js";
|
||||
|
||||
export type DictateTurnHandlers = {
|
||||
listProducts: () => DictateProduct[];
|
||||
/** order 省略时由实现沿用原序或默认 */
|
||||
writeProduct: (tag: string, content: string, order?: number) => void;
|
||||
writeProduct: (
|
||||
tag: string,
|
||||
content: string,
|
||||
order?: number,
|
||||
metadata?: { layer?: "intermediate" | "final"; selfScore?: DictateSelfScore },
|
||||
) => void;
|
||||
/** 删除固定产物 tag;返回是否曾存在 */
|
||||
deleteProduct: (tag: string) => boolean;
|
||||
/** 读/写变量目录与映射(程序校验后的 JSON) */
|
||||
@@ -46,6 +74,8 @@ export type DictateTurnHandlers = {
|
||||
writeTag: (tag: string, content: string) => void;
|
||||
/** 删除任意黑板 tag(投影 / 变量.当前 等) */
|
||||
deleteTag: (tag: string) => boolean;
|
||||
/** 写入开场白前运行确定性收口工具,保证运行配置先于开场落盘。 */
|
||||
prepareOpening?: () => void;
|
||||
clearDialogue: () => void;
|
||||
onToolCall?: (name: string, detail: string) => void;
|
||||
onThinkingDelta?: (delta: string) => void;
|
||||
@@ -65,6 +95,16 @@ export type DictateTurnResult = {
|
||||
clearedDialogue: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* 一轮对话落盘的工具上限。
|
||||
* 串行模型往往一轮只调一个工具:读能力再写入,方案里十余项,再加上收尾回复。
|
||||
*/
|
||||
export const DICTATE_DRIVER_MAX_ITERATIONS = 36;
|
||||
|
||||
/** 接近上限时写进工具结果,让模型停下来回复用户。 */
|
||||
export const DICTATE_TURN_LIMIT_HINT =
|
||||
"本轮工具次数已接近上限。停止调用工具,直接回复用户。已写入的保留;未写的能力下一轮继续。";
|
||||
|
||||
/**
|
||||
* 跑一轮对话落盘整理:任务 messages 由本模块拼;preset 夹心由 LLM 包装层统一装配。
|
||||
*/
|
||||
@@ -77,6 +117,12 @@ export async function runDictateTurn(params: {
|
||||
recipeBrief?: string;
|
||||
/** modules catalog 的何时落盘块(formatModuleCatalogForDictate) */
|
||||
moduleGuide?: string;
|
||||
/** 尚无本局方案时显示的旧配方案例目录 */
|
||||
recipeExamples?: string;
|
||||
/** 用于本局方案覆盖校验及方案态能力状态 */
|
||||
moduleCatalog?: ModuleCatalog | null;
|
||||
/** 当前能力提示的可读内容与版本;存在时启用写前读取门禁 */
|
||||
moduleReadIndex?: ReadonlyMap<string, DictateModuleReadPayload>;
|
||||
/** catalog 中 repeatable 能力的 artifact 基名(设计.生成规则 等) */
|
||||
repeatableFamilies?: ReadonlySet<string> | readonly string[];
|
||||
driver?: AgentDriver;
|
||||
@@ -85,6 +131,7 @@ export async function runDictateTurn(params: {
|
||||
const wroteTags: string[] = [];
|
||||
const deletedTags: string[] = [];
|
||||
let clearedDialogue = false;
|
||||
let openingPrepared = false;
|
||||
let dialogue = [...params.dialogue];
|
||||
const repeatableFamilies = new Set(
|
||||
[...(params.repeatableFamilies ?? [])].map((t) => String(t).trim()).filter(Boolean),
|
||||
@@ -92,33 +139,146 @@ export async function runDictateTurn(params: {
|
||||
|
||||
const systemPrompt =
|
||||
params.systemPrompt ??
|
||||
buildDictateSystemPrompt({
|
||||
recipeName: params.recipeName,
|
||||
recipeBrief: params.recipeBrief,
|
||||
moduleGuide: params.moduleGuide,
|
||||
});
|
||||
(() => {
|
||||
const products = params.handlers.listProducts();
|
||||
const plan = parseCreationPlan(
|
||||
products.find((product) => product.tag === CREATION_PLAN_TAG)?.content,
|
||||
);
|
||||
return buildDictateSystemPrompt({
|
||||
...(plan
|
||||
? {
|
||||
creationPlanState: formatCreationPlanProgress({
|
||||
plan,
|
||||
catalog: params.moduleCatalog,
|
||||
products,
|
||||
}),
|
||||
}
|
||||
: {
|
||||
capabilityCatalog:
|
||||
formatCreationPlanCapabilityCatalog(params.moduleCatalog) ||
|
||||
params.moduleGuide,
|
||||
recipeExamples: params.recipeExamples,
|
||||
recipeName: params.recipeName,
|
||||
recipeBrief: params.recipeBrief,
|
||||
}),
|
||||
});
|
||||
})();
|
||||
const assembled = buildDictateMessages({
|
||||
systemPrompt,
|
||||
products: params.handlers.listProducts(),
|
||||
dialogue,
|
||||
});
|
||||
const userBlock = assembled.filter((m) => m.role !== "system");
|
||||
let toolSteps = 0;
|
||||
|
||||
const run = await driver.run({
|
||||
let run;
|
||||
try {
|
||||
run = await driver.run({
|
||||
system: systemPrompt,
|
||||
messages: userBlock,
|
||||
tools: DICTATE_TOOL_DEFINITIONS,
|
||||
caller: "dictate_agent",
|
||||
label: "对话落盘",
|
||||
maxIterations: DICTATE_DRIVER_MAX_ITERATIONS,
|
||||
onThinkingDelta: params.handlers.onThinkingDelta,
|
||||
onThinkingDone: params.handlers.onThinkingDone,
|
||||
handleStep: (calls) => {
|
||||
toolSteps += 1;
|
||||
const moduleNamesThisStep: string[] = [];
|
||||
const results = calls.map((call) => {
|
||||
const args = safeParseArgs(call.arguments);
|
||||
if (
|
||||
call.name === "declare_variable" ||
|
||||
call.name === "declare_map" ||
|
||||
call.name === "delete" ||
|
||||
call.name === "undeclare_variable" ||
|
||||
call.name === "remove_map"
|
||||
) {
|
||||
openingPrepared = false;
|
||||
}
|
||||
if (call.name === "read_module") {
|
||||
const ref = String(args.module_id ?? "").trim();
|
||||
const module = params.moduleReadIndex?.get(ref);
|
||||
if (!module) {
|
||||
const err = `未找到能力:${ref || "空"}`;
|
||||
params.handlers.onToolCall?.(call.name, err);
|
||||
return { callId: call.id, content: JSON.stringify({ error: err }) };
|
||||
}
|
||||
const state = parseDictateModuleReadState(
|
||||
params.handlers.readTag(DICTATE_MODULE_READ_STATE_TAG),
|
||||
);
|
||||
state.modules[module.module_id] = module.version;
|
||||
state.active_module_id = module.module_id;
|
||||
params.handlers.writeTag(
|
||||
DICTATE_MODULE_READ_STATE_TAG,
|
||||
serializeDictateModuleReadState(state),
|
||||
);
|
||||
params.handlers.onToolCall?.(
|
||||
call.name,
|
||||
`${module.module_id}@${module.version}`,
|
||||
);
|
||||
return {
|
||||
callId: call.id,
|
||||
content: JSON.stringify({ ok: true, ...module }),
|
||||
};
|
||||
}
|
||||
if (call.name === "read_library_entry") {
|
||||
const state = parseDictateModuleReadState(
|
||||
params.handlers.readTag(DICTATE_MODULE_READ_STATE_TAG),
|
||||
);
|
||||
const module = state.active_module_id
|
||||
? params.moduleReadIndex?.get(state.active_module_id)
|
||||
: undefined;
|
||||
if (!module) {
|
||||
const err = "请先 read_module 读取要使用该库的能力";
|
||||
params.handlers.onToolCall?.(call.name, err);
|
||||
return { callId: call.id, content: JSON.stringify({ error: err }) };
|
||||
}
|
||||
const result = readLibraryEntries({
|
||||
module,
|
||||
libraryId: String(args.library_id ?? ""),
|
||||
entryIds: Array.isArray(args.entry_ids)
|
||||
? args.entry_ids.map(String)
|
||||
: [],
|
||||
reason: typeof args.reason === "string" ? args.reason : undefined,
|
||||
});
|
||||
params.handlers.onToolCall?.(
|
||||
call.name,
|
||||
result.error ?? `${args.library_id}: ${result.entries?.length ?? 0}`,
|
||||
);
|
||||
return {
|
||||
callId: call.id,
|
||||
content: JSON.stringify(
|
||||
result.error
|
||||
? { error: result.error }
|
||||
: {
|
||||
ok: true,
|
||||
module_id: module.module_id,
|
||||
library_id: String(args.library_id ?? ""),
|
||||
entries: result.entries,
|
||||
},
|
||||
),
|
||||
};
|
||||
}
|
||||
if (call.name === "prepare_opening") {
|
||||
params.handlers.prepareOpening?.();
|
||||
openingPrepared = true;
|
||||
params.handlers.onToolCall?.(
|
||||
call.name,
|
||||
"信息可见范围 → 游玩执行方式 → 上下文排序 → 运行配置组装",
|
||||
);
|
||||
return {
|
||||
callId: call.id,
|
||||
content: JSON.stringify({
|
||||
ok: true,
|
||||
runtime_prepared: true,
|
||||
next: "现在生成并 insert 开场白",
|
||||
}),
|
||||
};
|
||||
}
|
||||
if (call.name === "insert" || call.name === "write_product") {
|
||||
let tag = String(args.position ?? args.tag ?? "").trim();
|
||||
const content = String(args.content ?? "");
|
||||
const content = canonicalizeInsertedContent(String(args.content ?? ""));
|
||||
if (tag === VARIABLE_CATALOG_TAG || tag === VALUE_MAP_TAG) {
|
||||
const err = `请用 declare_variable / declare_map,不要 insert ${tag}`;
|
||||
params.handlers.onToolCall?.(call.name, err);
|
||||
@@ -134,24 +294,193 @@ export async function runDictateTurn(params: {
|
||||
params.handlers.onToolCall?.(call.name, err);
|
||||
return { callId: call.id, content: JSON.stringify({ error: err }) };
|
||||
}
|
||||
if (repeatableFamilies.size) {
|
||||
const resolved = resolveRepeatableInsertTag({
|
||||
tag,
|
||||
content,
|
||||
existingTags: params.handlers.listProducts().map((p) => p.tag),
|
||||
repeatableFamilies,
|
||||
});
|
||||
if ("error" in resolved) {
|
||||
params.handlers.onToolCall?.(call.name, resolved.error);
|
||||
const targetModule = resolveModuleForInsertTag(
|
||||
tag,
|
||||
params.moduleCatalog,
|
||||
);
|
||||
if (
|
||||
targetModule &&
|
||||
params.moduleReadIndex &&
|
||||
requiresModuleRead(targetModule.id, targetModule.auto)
|
||||
) {
|
||||
const current = params.moduleReadIndex.get(targetModule.id);
|
||||
const state = parseDictateModuleReadState(
|
||||
params.handlers.readTag(DICTATE_MODULE_READ_STATE_TAG),
|
||||
);
|
||||
if (!current || state.modules[targetModule.id] !== current.version) {
|
||||
const err = `写入 ${tag} 前必须先 read_module("${targetModule.id}") 读取当前提示版本`;
|
||||
params.handlers.onToolCall?.(call.name, err);
|
||||
return {
|
||||
callId: call.id,
|
||||
content: JSON.stringify({ error: resolved.error }),
|
||||
content: JSON.stringify({
|
||||
error: err,
|
||||
required_module_id: targetModule.id,
|
||||
required_version: current?.version,
|
||||
}),
|
||||
};
|
||||
}
|
||||
tag = resolved.tag;
|
||||
}
|
||||
const currentModule = targetModule
|
||||
? params.moduleReadIndex?.get(targetModule.id)
|
||||
: undefined;
|
||||
const selfScore = parseAndValidateSelfScore(
|
||||
args.self_score,
|
||||
currentModule?.score_dimensions ?? [],
|
||||
);
|
||||
if ("error" in selfScore) {
|
||||
params.handlers.onToolCall?.(call.name, selfScore.error);
|
||||
return {
|
||||
callId: call.id,
|
||||
content: JSON.stringify({ error: selfScore.error }),
|
||||
};
|
||||
}
|
||||
if (tag === CREATION_PLAN_TAG) {
|
||||
const validation = validateCreationPlan(content, params.moduleCatalog);
|
||||
if (!validation.ok) {
|
||||
const err = validation.error ?? "本局创作方案校验失败";
|
||||
params.handlers.onToolCall?.(call.name, err);
|
||||
return {
|
||||
callId: call.id,
|
||||
content: JSON.stringify({
|
||||
error: err,
|
||||
missing_ids: validation.missingIds,
|
||||
duplicate_ids: validation.duplicateIds,
|
||||
unknown_ids: validation.unknownIds,
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
if (repeatableFamilies.size) {
|
||||
const family = dictateProductFamily(tag);
|
||||
const pieces =
|
||||
targetModule?.id === "concrete-instances" ||
|
||||
family === "设计.具体实例"
|
||||
? splitConcreteInstanceContents(content)
|
||||
: [content];
|
||||
const baseTag = pieces.length > 1 ? "设计.具体实例" : tag;
|
||||
const positions: string[] = [];
|
||||
const resolvedPieces: Array<{ tag: string; content: string }> = [];
|
||||
for (const piece of pieces) {
|
||||
const resolved = resolveRepeatableInsertTag({
|
||||
tag: baseTag,
|
||||
content: piece,
|
||||
existingTags: [
|
||||
...params.handlers.listProducts().map((p) => p.tag),
|
||||
...resolvedPieces.map((item) => item.tag),
|
||||
],
|
||||
repeatableFamilies,
|
||||
});
|
||||
if ("error" in resolved) {
|
||||
params.handlers.onToolCall?.(call.name, resolved.error);
|
||||
return {
|
||||
callId: call.id,
|
||||
content: JSON.stringify({ error: resolved.error }),
|
||||
};
|
||||
}
|
||||
resolvedPieces.push({ tag: resolved.tag, content: piece });
|
||||
}
|
||||
let replyModule: string | undefined;
|
||||
for (const [index, piece] of resolvedPieces.entries()) {
|
||||
const pieceTag = piece.tag;
|
||||
const order = parseDictateOrder(args.order);
|
||||
const productsBeforeWrite = params.handlers.listProducts();
|
||||
const isOpening = isDictateOpeningTag(pieceTag);
|
||||
const explicitlyPrepared = openingPrepared;
|
||||
if (isOpening && !explicitlyPrepared) {
|
||||
params.handlers.prepareOpening?.();
|
||||
}
|
||||
params.handlers.writeProduct(pieceTag, piece.content, order, {
|
||||
...(targetModule
|
||||
? {
|
||||
layer: resolveInsertLayer({
|
||||
moduleId: targetModule.id,
|
||||
catalogLayer: targetModule.layer,
|
||||
content: piece.content,
|
||||
}),
|
||||
}
|
||||
: {}),
|
||||
...(selfScore.value ? { selfScore: selfScore.value } : {}),
|
||||
});
|
||||
if (!isOpening) openingPrepared = false;
|
||||
wroteTags.push(pieceTag);
|
||||
positions.push(pieceTag);
|
||||
const detail =
|
||||
order === undefined ? pieceTag : `${pieceTag} order=${order}`;
|
||||
params.handlers.onToolCall?.(call.name, detail);
|
||||
replyModule =
|
||||
params.handlers.lookupInsertFeedback?.(pieceTag)?.trim() ||
|
||||
replyModule;
|
||||
const requiredGaps = isDictateOpeningTag(pieceTag)
|
||||
? requiredCreationPlanGaps({
|
||||
planRaw: productsBeforeWrite.find(
|
||||
(product) => product.tag === CREATION_PLAN_TAG,
|
||||
)?.content,
|
||||
catalog: params.moduleCatalog,
|
||||
products: [
|
||||
...productsBeforeWrite.filter((product) => product.tag !== pieceTag),
|
||||
{ tag: pieceTag, content: piece.content, order },
|
||||
],
|
||||
})
|
||||
: [];
|
||||
if (index === resolvedPieces.length - 1) {
|
||||
if (replyModule) {
|
||||
const title =
|
||||
replyModule.match(/^#\s*(?:模块|本轮关注)\s*·\s*(.+)$/m)?.[1]?.trim() ||
|
||||
pieceTag;
|
||||
moduleNamesThisStep.push(title);
|
||||
}
|
||||
return {
|
||||
callId: call.id,
|
||||
content: JSON.stringify({
|
||||
ok: true,
|
||||
position: positions[0],
|
||||
...(positions.length > 1 ? { positions, split: positions.length } : {}),
|
||||
...(isOpening
|
||||
? {
|
||||
runtime_prepared_before_opening: true,
|
||||
preparation_mode: explicitlyPrepared
|
||||
? "prepare_opening"
|
||||
: "implicit_fallback",
|
||||
}
|
||||
: {}),
|
||||
...(order !== undefined ? { order } : {}),
|
||||
...(requiredGaps.length
|
||||
? {
|
||||
warning: `仍有“必须”能力未落:${requiredGaps.join("、")}。开场白已写入,请在准备开玩前提醒用户补齐或调整方案。`,
|
||||
missing_required_module_ids: requiredGaps,
|
||||
}
|
||||
: {}),
|
||||
...(replyModule
|
||||
? {
|
||||
reply_module: replyModule,
|
||||
reply_hint: DICTATE_MULTI_MODULE_REPLY_HINT,
|
||||
}
|
||||
: {}),
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
const order = parseDictateOrder(args.order);
|
||||
params.handlers.writeProduct(tag, content, order);
|
||||
const productsBeforeWrite = params.handlers.listProducts();
|
||||
const isOpening = isDictateOpeningTag(tag);
|
||||
const explicitlyPrepared = openingPrepared;
|
||||
if (isOpening && !explicitlyPrepared) {
|
||||
params.handlers.prepareOpening?.();
|
||||
}
|
||||
params.handlers.writeProduct(tag, content, order, {
|
||||
...(targetModule
|
||||
? {
|
||||
layer: resolveInsertLayer({
|
||||
moduleId: targetModule.id,
|
||||
catalogLayer: targetModule.layer,
|
||||
content,
|
||||
}),
|
||||
}
|
||||
: {}),
|
||||
...(selfScore.value ? { selfScore: selfScore.value } : {}),
|
||||
});
|
||||
if (!isOpening) openingPrepared = false;
|
||||
wroteTags.push(tag);
|
||||
const detail =
|
||||
order === undefined ? tag : `${tag} order=${order}`;
|
||||
@@ -160,15 +489,41 @@ export async function runDictateTurn(params: {
|
||||
params.handlers.lookupInsertFeedback?.(tag)?.trim() || undefined;
|
||||
if (replyModule) {
|
||||
const title =
|
||||
replyModule.match(/^#\s*模块\s*·\s*(.+)$/m)?.[1]?.trim() || tag;
|
||||
replyModule.match(/^#\s*(?:模块|本轮关注)\s*·\s*(.+)$/m)?.[1]?.trim() || tag;
|
||||
moduleNamesThisStep.push(title);
|
||||
}
|
||||
const requiredGaps = isDictateOpeningTag(tag)
|
||||
? requiredCreationPlanGaps({
|
||||
planRaw: productsBeforeWrite.find(
|
||||
(product) => product.tag === CREATION_PLAN_TAG,
|
||||
)?.content,
|
||||
catalog: params.moduleCatalog,
|
||||
products: [
|
||||
...productsBeforeWrite.filter((product) => product.tag !== tag),
|
||||
{ tag, content, order },
|
||||
],
|
||||
})
|
||||
: [];
|
||||
return {
|
||||
callId: call.id,
|
||||
content: JSON.stringify({
|
||||
ok: true,
|
||||
position: tag,
|
||||
...(isOpening
|
||||
? {
|
||||
runtime_prepared_before_opening: true,
|
||||
preparation_mode: explicitlyPrepared
|
||||
? "prepare_opening"
|
||||
: "implicit_fallback",
|
||||
}
|
||||
: {}),
|
||||
...(order !== undefined ? { order } : {}),
|
||||
...(requiredGaps.length
|
||||
? {
|
||||
warning: `仍有“必须”能力未落:${requiredGaps.join("、")}。开场白已写入,请在准备开玩前提醒用户补齐或调整方案。`,
|
||||
missing_required_module_ids: requiredGaps,
|
||||
}
|
||||
: {}),
|
||||
...(replyModule
|
||||
? {
|
||||
reply_module: replyModule,
|
||||
@@ -376,33 +731,36 @@ export async function runDictateTurn(params: {
|
||||
}
|
||||
}
|
||||
|
||||
if (toolSteps >= DICTATE_DRIVER_MAX_ITERATIONS - 2) {
|
||||
noteTurnLimit(results);
|
||||
}
|
||||
|
||||
return { kind: "continue" as const, results };
|
||||
},
|
||||
});
|
||||
|
||||
let reply = "";
|
||||
if (run.stop.kind === "text") {
|
||||
reply = run.stop.content.trim();
|
||||
}
|
||||
if (!reply) {
|
||||
if (wroteTags.length || deletedTags.length) {
|
||||
const parts: string[] = [];
|
||||
if (wroteTags.length) {
|
||||
parts.push(`已写入:${[...new Set(wroteTags)].join("、")}`);
|
||||
}
|
||||
if (deletedTags.length) {
|
||||
parts.push(`已删除:${[...new Set(deletedTags)].join("、")}`);
|
||||
}
|
||||
reply = `${parts.join(";")}。还有要补充的吗?`;
|
||||
} else if (clearedDialogue) {
|
||||
reply = "对话已清空,产物都还在。可以继续说。";
|
||||
} else {
|
||||
reply = "我在听,请继续说你想要的体验或设定。";
|
||||
}
|
||||
} catch (err) {
|
||||
if (!(err instanceof DriverIterationLimitError)) throw err;
|
||||
return {
|
||||
reply: composeDictateReply({
|
||||
wroteTags,
|
||||
deletedTags,
|
||||
clearedDialogue,
|
||||
capped: true,
|
||||
}),
|
||||
iterations: err.iterations,
|
||||
wroteTags: [...new Set(wroteTags)],
|
||||
deletedTags: [...new Set(deletedTags)],
|
||||
clearedDialogue,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
reply,
|
||||
reply: composeDictateReply({
|
||||
stopText: run.stop.kind === "text" ? run.stop.content : "",
|
||||
wroteTags,
|
||||
deletedTags,
|
||||
clearedDialogue,
|
||||
}),
|
||||
iterations: run.iterations,
|
||||
wroteTags: [...new Set(wroteTags)],
|
||||
deletedTags: [...new Set(deletedTags)],
|
||||
@@ -410,6 +768,52 @@ export async function runDictateTurn(params: {
|
||||
};
|
||||
}
|
||||
|
||||
function noteTurnLimit(
|
||||
results: Array<{ callId: string; content: string }>,
|
||||
): void {
|
||||
const last = results.at(-1);
|
||||
if (!last) return;
|
||||
try {
|
||||
const parsed = JSON.parse(last.content) as Record<string, unknown>;
|
||||
parsed.turn_limit = DICTATE_TURN_LIMIT_HINT;
|
||||
last.content = JSON.stringify(parsed);
|
||||
} catch {
|
||||
last.content = `${last.content}\n${DICTATE_TURN_LIMIT_HINT}`;
|
||||
}
|
||||
}
|
||||
|
||||
function composeDictateReply(params: {
|
||||
stopText?: string;
|
||||
wroteTags: string[];
|
||||
deletedTags: string[];
|
||||
clearedDialogue: boolean;
|
||||
capped?: boolean;
|
||||
}): string {
|
||||
let reply = params.stopText?.trim() ?? "";
|
||||
if (!reply) {
|
||||
if (params.wroteTags.length || params.deletedTags.length) {
|
||||
const parts: string[] = [];
|
||||
if (params.wroteTags.length) {
|
||||
parts.push(`已写入:${[...new Set(params.wroteTags)].join("、")}`);
|
||||
}
|
||||
if (params.deletedTags.length) {
|
||||
parts.push(`已删除:${[...new Set(params.deletedTags)].join("、")}`);
|
||||
}
|
||||
reply = `${parts.join(";")}。还有要补充的吗?`;
|
||||
} else if (params.clearedDialogue) {
|
||||
reply = "对话已清空,产物都还在。可以继续说。";
|
||||
} else if (params.capped) {
|
||||
reply = "这轮工具次数已用完,还没有新的产物。可以说要先补哪一块。";
|
||||
} else {
|
||||
reply = "我在听,请继续说你想要的体验或设定。";
|
||||
}
|
||||
}
|
||||
if (params.capped && !reply.includes("工具次数已用完")) {
|
||||
reply = `${reply} 这轮工具次数已用完,其余下一轮继续。`;
|
||||
}
|
||||
return reply;
|
||||
}
|
||||
|
||||
function safeParseArgs(raw: string): Record<string, unknown> {
|
||||
try {
|
||||
const parsed = JSON.parse(raw || "{}");
|
||||
@@ -421,3 +825,111 @@ function safeParseArgs(raw: string): Record<string, unknown> {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function requiresModuleRead(moduleId: string, auto?: boolean): boolean {
|
||||
if (auto) return false;
|
||||
return !new Set([
|
||||
"narrative",
|
||||
"variable-context",
|
||||
"context-order",
|
||||
"worker-spec",
|
||||
"refine",
|
||||
]).has(moduleId);
|
||||
}
|
||||
|
||||
function resolveInsertLayer(params: {
|
||||
moduleId: string;
|
||||
catalogLayer?: "intermediate" | "final";
|
||||
content: string;
|
||||
}): "intermediate" | "final" {
|
||||
if (params.moduleId === "generation-rules") {
|
||||
const lifecycle = normalizeGenerationRuleLifecycle(
|
||||
extractGenerationRuleLifecycle(params.content),
|
||||
);
|
||||
if (lifecycle === "seed_only") return "intermediate";
|
||||
if (lifecycle === "runtime_only" || lifecycle === "seed_and_runtime") {
|
||||
return "final";
|
||||
}
|
||||
}
|
||||
return params.catalogLayer ?? "final";
|
||||
}
|
||||
|
||||
function extractGenerationRuleLifecycle(
|
||||
content: string,
|
||||
): string | undefined {
|
||||
try {
|
||||
const parsed = JSON.parse(content) as {
|
||||
正文?: { 生命周期?: unknown };
|
||||
生命周期?: unknown;
|
||||
};
|
||||
const raw = parsed.正文?.生命周期 ?? parsed.生命周期;
|
||||
return typeof raw === "string" ? raw.trim() : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeGenerationRuleLifecycle(
|
||||
raw: string | undefined,
|
||||
): string | undefined {
|
||||
if (!raw) return undefined;
|
||||
if (raw === "只预生成" || raw === "seed_only") return "seed_only";
|
||||
if (raw === "仅游玩期" || raw === "runtime_only") return "runtime_only";
|
||||
if (raw === "预生成并游玩期" || raw === "seed_and_runtime") {
|
||||
return "seed_and_runtime";
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
|
||||
function parseAndValidateSelfScore(
|
||||
raw: unknown,
|
||||
expectedNames: readonly string[],
|
||||
): { value?: DictateSelfScore } | { error: string } {
|
||||
if (expectedNames.length === 0) return {};
|
||||
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
||||
return { error: `self_score 必填;维度:${expectedNames.join("、")}` };
|
||||
}
|
||||
const dims = (raw as { dims?: unknown }).dims;
|
||||
if (!Array.isArray(dims)) {
|
||||
return { error: "self_score.dims 必须是数组" };
|
||||
}
|
||||
const parsed: DictateSelfScore["dims"] = [];
|
||||
for (const item of dims) {
|
||||
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
||||
return { error: "self_score.dims 含无效条目" };
|
||||
}
|
||||
const row = item as Record<string, unknown>;
|
||||
const name = typeof row.name === "string" ? row.name.trim() : "";
|
||||
const score = Number(row.score);
|
||||
const gap = typeof row.gap === "string" ? row.gap.trim() : "";
|
||||
const options = Array.isArray(row.options)
|
||||
? row.options.map(String).map((option) => option.trim()).filter(Boolean)
|
||||
: [];
|
||||
if (!name || !Number.isInteger(score) || score < 1 || score > 5) {
|
||||
return { error: "self_score 每项须含合法 name 与 1~5 整数 score" };
|
||||
}
|
||||
if (score <= 3 && !gap) {
|
||||
return { error: `self_score「${name}」不高于 3 分时必须写 gap` };
|
||||
}
|
||||
if (options.length > 0 && (options.length < 2 || options.length > 3)) {
|
||||
return { error: `self_score「${name}」的 options 必须为 2~3 项` };
|
||||
}
|
||||
parsed.push({
|
||||
name,
|
||||
score,
|
||||
...(gap ? { gap } : {}),
|
||||
...(options.length ? { options } : {}),
|
||||
});
|
||||
}
|
||||
const actual = parsed.map((dim) => dim.name);
|
||||
if (
|
||||
actual.length !== expectedNames.length ||
|
||||
expectedNames.some((name) => !actual.includes(name)) ||
|
||||
actual.some((name) => !expectedNames.includes(name))
|
||||
) {
|
||||
return {
|
||||
error: `self_score 维度必须与能力 score 一致:${expectedNames.join("、")}`,
|
||||
};
|
||||
}
|
||||
return { value: { dims: parsed } };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user