接通主角绑定、开场封印与游玩辅助,让世界模拟器从创作连续落到本局。
直聘插入按模块回馈,并记录请求骨架便于排查。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -19,6 +19,17 @@ export {
|
||||
} from "./context.js";
|
||||
export { DICTATE_TOOL_DEFINITIONS } from "./tools.js";
|
||||
export { runDictateTurn, type DictateTurnResult } from "./turn.js";
|
||||
export {
|
||||
DICTATE_INSERT_TAG_ALIASES,
|
||||
DICTATE_MULTI_MODULE_REPLY_HINT,
|
||||
buildDictateInsertFeedback,
|
||||
buildDictateInsertFeedbackIndex,
|
||||
formatDictateUserFacingBrief,
|
||||
lookupDictateInsertFeedback,
|
||||
resolveModuleForInsertTag,
|
||||
extractSelfScoreDimensionNames,
|
||||
type DictateInsertFeedback,
|
||||
} from "./insert-feedback.js";
|
||||
export {
|
||||
applyDictatePlayBind,
|
||||
bindDictateProductsToPlaySpec,
|
||||
@@ -26,6 +37,18 @@ export {
|
||||
collectDictateBindProducts,
|
||||
type DictatePlayBindResult,
|
||||
} from "./play-bind.js";
|
||||
export {
|
||||
REPEATABLE_TAG_SEP,
|
||||
buildRepeatableProductTag,
|
||||
collectFamilyContents,
|
||||
dictateProductFamily,
|
||||
dictateProductSlot,
|
||||
extractRepeatableSlotFromContent,
|
||||
isDictateProductFamily,
|
||||
mergeGenerationRulesArtifacts,
|
||||
resolveRepeatableInsertTag,
|
||||
sanitizeRepeatableSlot,
|
||||
} from "./repeatable-tags.js";
|
||||
export {
|
||||
VARIABLE_CATALOG_TAG,
|
||||
parseVariableCatalog,
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
/**
|
||||
* insert(tag) → 反查能力 → 独立「回复模块」规范,塞进 tool result。
|
||||
* 一轮可多次 insert:每个模块各自一份;末尾回复须分段全部附带。
|
||||
* 不照搬 schema / mount;只抽 probe、交互原则、自评关注点。
|
||||
*/
|
||||
|
||||
import {
|
||||
getModuleSection,
|
||||
loadModulePrompt,
|
||||
parseModulePromptSections,
|
||||
type ModuleCatalog,
|
||||
type ModuleCatalogEntry,
|
||||
} from "../skills/creation-flow.js";
|
||||
|
||||
/** 对话落盘常用简化 tag → 能力 id(catalog.artifact 之外) */
|
||||
export const DICTATE_INSERT_TAG_ALIASES: Readonly<Record<string, string>> = {
|
||||
"设计.开场白": "opening-setup",
|
||||
"设计.美学纲领": "aesthetics-interaction",
|
||||
"设计.文风": "narrative-guide",
|
||||
"设计.监控栏": "status-bar",
|
||||
};
|
||||
|
||||
/** 多模块同轮:写进每条 insert tool result 的 reply_hint */
|
||||
export const DICTATE_MULTI_MODULE_REPLY_HINT =
|
||||
"本轮带回了多个 reply_module。末尾可见回复:每个模块各写一块 markdown,标题用 ## 模块 · {名称}(与该 reply_module 一致);块间空一行;每块只写该模块的确认 / 薄弱点 / 追问。全部模块写完后,可另起一段写 1 条「建议下一刀」(条件须已接近成立)。缺某一块则补上后再结束本轮。";
|
||||
|
||||
const MAX_PROBE_CHARS = 1200;
|
||||
const MAX_PRINCIPLES_CHARS = 900;
|
||||
const MAX_BRIEF_CHARS = 2800;
|
||||
|
||||
export type DictateInsertFeedback = {
|
||||
moduleId: string;
|
||||
moduleName: string;
|
||||
tag: string;
|
||||
/** 给模型的独立模块 markdown */
|
||||
text: string;
|
||||
};
|
||||
|
||||
export function resolveModuleForInsertTag(
|
||||
tag: string,
|
||||
catalog: ModuleCatalog | null | undefined,
|
||||
): ModuleCatalogEntry | null {
|
||||
const t = tag.trim();
|
||||
if (!t || !catalog?.modules?.length) return null;
|
||||
|
||||
const byArtifact = catalog.modules.find((m) => m.artifact?.trim() === t);
|
||||
if (byArtifact) return byArtifact;
|
||||
|
||||
// 可增殖拆分:设计.生成规则#slug → 仍反查 设计.生成规则
|
||||
const hash = t.indexOf("#");
|
||||
if (hash > 0) {
|
||||
const family = t.slice(0, hash).trim();
|
||||
const byFamily = catalog.modules.find((m) => m.artifact?.trim() === family);
|
||||
if (byFamily) return byFamily;
|
||||
}
|
||||
|
||||
const aliasId = DICTATE_INSERT_TAG_ALIASES[t];
|
||||
if (aliasId) {
|
||||
return catalog.modules.find((m) => m.id === aliasId) ?? null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 从 output 块里抽自评维度名(用户向;不交 JSON) */
|
||||
export function extractSelfScoreDimensionNames(outputFence: string): string[] {
|
||||
const names: string[] = [];
|
||||
const re = /"名"\s*:\s*"([^"]+)"/g;
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = re.exec(outputFence)) !== null) {
|
||||
const name = m[1]!.trim();
|
||||
if (name && !names.includes(name)) names.push(name);
|
||||
}
|
||||
return names.slice(0, 8);
|
||||
}
|
||||
|
||||
function clip(text: string, max: number): string {
|
||||
const t = text.trim();
|
||||
if (t.length <= max) return t;
|
||||
return `${t.slice(0, max - 1)}…`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从能力 prompt.md 抽「回答时附带本模块」的 markdown(无匹配块则空串)。
|
||||
*/
|
||||
export function formatDictateUserFacingBrief(params: {
|
||||
tag: string;
|
||||
module: ModuleCatalogEntry;
|
||||
promptMd: string;
|
||||
}): string {
|
||||
const sections = parseModulePromptSections(params.promptMd);
|
||||
const probe = getModuleSection(sections, "probe");
|
||||
const principles = getModuleSection(sections, "principles");
|
||||
const output = getModuleSection(sections, "output");
|
||||
const dims = output ? extractSelfScoreDimensionNames(output) : [];
|
||||
|
||||
if (!probe && !principles && dims.length === 0) return "";
|
||||
|
||||
const includes: string[] = [];
|
||||
if (probe) includes.push("探测与追问口径");
|
||||
if (principles) includes.push("交互原则");
|
||||
if (dims.length) includes.push(`自评关注(${dims.join("、")})`);
|
||||
|
||||
const parts: string[] = [
|
||||
`# 模块 · ${params.module.name}`,
|
||||
"",
|
||||
`回答时请附带本模块(落盘 tag:\`${params.tag}\`)。`,
|
||||
"",
|
||||
"本模块内容包括:",
|
||||
...includes.map((x) => `- ${x}`),
|
||||
"",
|
||||
"## 写入用户回复时做什么",
|
||||
`1. 用二级标题 \`## 模块 · ${params.module.name}\` 开一段(多模块时本段只写本模块)。`,
|
||||
"2. 确认:2~4 句复述本模块已落要点,让用户能判断对不对。",
|
||||
"3. 薄弱点:对照下方「自评关注」口头点 1 处不够硬的地方(不必交打分表或 JSON)。",
|
||||
"4. 追问:1~2 点;每点给建议选项或短场景,用户可直接采用或改写。材料已够则追问可空,改写「建议下一刀」。",
|
||||
"5. 不粘贴产物全文;不复述 tool JSON。",
|
||||
];
|
||||
|
||||
if (probe) {
|
||||
parts.push("", "## 探测与追问(本模块口径)", clip(probe, MAX_PROBE_CHARS));
|
||||
}
|
||||
if (principles) {
|
||||
parts.push("", "## 交互原则(摘)", clip(principles, MAX_PRINCIPLES_CHARS));
|
||||
}
|
||||
if (dims.length) {
|
||||
parts.push("", "## 自评关注", dims.map((d) => `- ${d}`).join("\n"));
|
||||
}
|
||||
|
||||
return clip(parts.join("\n"), MAX_BRIEF_CHARS);
|
||||
}
|
||||
|
||||
export function buildDictateInsertFeedback(params: {
|
||||
tag: string;
|
||||
catalog: ModuleCatalog | null | undefined;
|
||||
promptMd: string | null | undefined;
|
||||
}): DictateInsertFeedback | null {
|
||||
const module = resolveModuleForInsertTag(params.tag, params.catalog);
|
||||
if (!module || !params.promptMd?.trim()) return null;
|
||||
const text = formatDictateUserFacingBrief({
|
||||
tag: params.tag.trim(),
|
||||
module,
|
||||
promptMd: params.promptMd,
|
||||
});
|
||||
if (!text.trim()) return null;
|
||||
return {
|
||||
moduleId: module.id,
|
||||
moduleName: module.name,
|
||||
tag: params.tag.trim(),
|
||||
text,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 预加载 catalog 全部能力的 insert 反馈索引(tag → 模块 markdown)。
|
||||
* 含 artifact 与别名 key。
|
||||
*/
|
||||
export async function buildDictateInsertFeedbackIndex(params: {
|
||||
catalog: ModuleCatalog;
|
||||
skillPackRoot: string;
|
||||
skillsRoot?: string;
|
||||
}): Promise<Map<string, string>> {
|
||||
const index = new Map<string, string>();
|
||||
const { catalog, skillPackRoot } = params;
|
||||
|
||||
await Promise.all(
|
||||
catalog.modules.map(async (module) => {
|
||||
const promptMd = await loadModulePrompt(
|
||||
skillPackRoot,
|
||||
module.id,
|
||||
params.skillsRoot,
|
||||
);
|
||||
if (!promptMd) return;
|
||||
const artifact = module.artifact?.trim();
|
||||
if (artifact) {
|
||||
const fb = buildDictateInsertFeedback({
|
||||
tag: artifact,
|
||||
catalog,
|
||||
promptMd,
|
||||
});
|
||||
if (fb) index.set(artifact, fb.text);
|
||||
}
|
||||
for (const [aliasTag, moduleId] of Object.entries(
|
||||
DICTATE_INSERT_TAG_ALIASES,
|
||||
)) {
|
||||
if (moduleId !== module.id) continue;
|
||||
const fb = buildDictateInsertFeedback({
|
||||
tag: aliasTag,
|
||||
catalog,
|
||||
promptMd,
|
||||
});
|
||||
if (fb) index.set(aliasTag, fb.text);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
export function lookupDictateInsertFeedback(
|
||||
index: Map<string, string> | null | undefined,
|
||||
tag: string,
|
||||
): string | undefined {
|
||||
const t = tag.trim();
|
||||
if (!t || !index?.size) return undefined;
|
||||
const direct = index.get(t);
|
||||
if (direct) return direct;
|
||||
const hash = t.indexOf("#");
|
||||
if (hash > 0) return index.get(t.slice(0, hash).trim());
|
||||
return undefined;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Boss 直聘式 · 落档/开玩时自动把产物 tag 挂到游玩 worker。
|
||||
* 对话落盘 · 落档/开玩时自动把产物 tag 挂到游玩 worker。
|
||||
* 只做「固定上下文绑定」;变量清单、可见性、分档映射仍靠对话产出,此处不发明。
|
||||
*/
|
||||
import type { Blackboard } from "../blackboard/blackboard.js";
|
||||
@@ -59,7 +59,7 @@ const SKIP_BIND_TAGS = new Set([
|
||||
|
||||
/** 偏文风/呈现:有叙事转述时挂转述;否则仍挂主世界层 */
|
||||
const STYLE_TAG_RE =
|
||||
/叙事指南|文风|美学|纲领|禁忌|示例|模仿|正文组成|回复格式|监控栏|篇幅|结构/;
|
||||
/叙事指南|故事推进|文风|美学|纲领|禁忌|示例|模仿|正文组成|回复格式|监控栏|篇幅|结构/;
|
||||
|
||||
/** 这些配方开玩时刚需叙事转述(主世界出内容、转述出文风) */
|
||||
const RECIPES_REQUIRE_NARRATOR = new Set(["文本生成器"]);
|
||||
@@ -85,7 +85,7 @@ function shouldSkipBindTag(tag: string): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 从黑板收集 Boss 直聘产物(有正文的 用户.* / 设计.*) */
|
||||
/** 从黑板收集对话落盘产物(有正文的 用户.* / 设计.*) */
|
||||
export function collectDictateBindProducts(board: Blackboard): DictateProduct[] {
|
||||
const products: DictateProduct[] = [];
|
||||
for (const e of board.listTagIndex()) {
|
||||
@@ -162,7 +162,7 @@ function buildGmInserts(params: {
|
||||
o++,
|
||||
p.tag,
|
||||
"full",
|
||||
`Boss直聘固定产物〔order=${effectiveDictateOrder(p)}〕`,
|
||||
`对话落盘固定产物〔order=${effectiveDictateOrder(p)}〕`,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -278,7 +278,7 @@ export function buildDictateContextOrder(params: {
|
||||
|
||||
return renumberContextOrder({
|
||||
schema: CONTEXT_ORDER_SCHEMA,
|
||||
brief: "Boss直聘落档自动挂载:固定产物 → 对话.历史 → 已有真值/投影 → 本轮输入",
|
||||
brief: "对话落盘落档自动挂载:固定产物 → 对话.历史 → 已有真值/投影 → 本轮输入",
|
||||
play_slots: params.playSlots,
|
||||
slots,
|
||||
});
|
||||
@@ -325,7 +325,7 @@ export function bindDictateProductsToPlaySpec(board: Blackboard): DictatePlayBin
|
||||
: JSON.stringify(
|
||||
{
|
||||
play_slots: playSlots,
|
||||
brief: "Boss直聘落档自动生成",
|
||||
brief: "对话落盘落档自动生成",
|
||||
},
|
||||
null,
|
||||
2,
|
||||
@@ -339,14 +339,14 @@ export function bindDictateProductsToPlaySpec(board: Blackboard): DictatePlayBin
|
||||
}
|
||||
row = parsed as Record<string, unknown>;
|
||||
} catch {
|
||||
row = { play_slots: playSlots, brief: "Boss直聘落档自动生成" };
|
||||
row = { play_slots: playSlots, brief: "对话落盘落档自动生成" };
|
||||
}
|
||||
row.play_slots = playSlots;
|
||||
if (playSlots.narrator) {
|
||||
row.brief =
|
||||
typeof row.brief === "string" && String(row.brief).includes("转述")
|
||||
? row.brief
|
||||
: "Boss直聘:主世界出内容,叙事转述出文风";
|
||||
: "对话落盘:主世界出内容,叙事转述出文风";
|
||||
}
|
||||
const withSlots = JSON.stringify(row, null, 2);
|
||||
const workerSetJson = mergeContextOrderIntoWorkerSetJson(withSlots, contextOrder);
|
||||
|
||||
+55
-20
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Boss 直聘式 · 创作进料专用系统提示
|
||||
* 对话落盘 · 创作进料专用系统提示
|
||||
*
|
||||
* 拼装:本提示(system)+ 当前产物(按相对序)+ 全量对话。
|
||||
* 仅用于创作层;开玩后由游玩管线接棒。
|
||||
@@ -28,57 +28,92 @@ ${opts.moduleGuide.trim()}
|
||||
`
|
||||
: "";
|
||||
|
||||
return `你是 Boss 直聘式创作助手:把用户原话整理成可落盘规格。作用对象是本局创作上下文(对话可清空,产物保留)。
|
||||
return `你是对话落盘创作助手:把用户原话整理成可落盘规格,并用可见回复让用户确认、补洞、选下一刀。作用对象是本局创作上下文(对话可清空,产物保留)。
|
||||
|
||||
**本提示只用于创作。** 开玩后叙事由游玩 worker 执行;不要假装在跑游玩回合。
|
||||
**本提示只用于创作。** 开玩后叙事由游玩 worker 执行;创作期只落规格与引导,不写游玩回合正文。
|
||||
|
||||
## 落盘节奏(先读)
|
||||
1. 对照【能力 · 何时落盘】与已有产物:条件成立才落对应能力;未成立 → 本轮不 insert / 不 declare 该能力产物。
|
||||
2. 每轮通常只推进一刀(一个能力或一组紧密相关的真值);用户一次说很多也不要批量抢跑下游。
|
||||
3. 配方顺序是骨架;具体能不能落,以各能力「何时用 / 何时不用」为准。
|
||||
1. 对照【能力 · 何时落盘】与已有产物:条件成立才落对应能力;未成立 → 本轮不 insert / 不 declare 该能力产物,改为短确认或问挡住的 1 点。
|
||||
2. **同轮可多次 insert / declare_***(一步并行或多个 tool 轮次都行):用户本轮材料已够、且多个能力的「何时用」都成立时,把这些都落掉;每个成功的 insert 各回一份 \`reply_module\`,可见回复按模块分段附带。
|
||||
3. **不抢跑未齐条件的下游**(这才是「别一口气铺满」的意思):条件未成立的能力本轮不落,写进「建议下一刀」等用户补料或上游齐了再落。配方顺序是骨架;能不能落以各能力「何时用 / 何时不用」为准。
|
||||
|
||||
## 落盘(仅允许 toolcall)
|
||||
禁止把设定/变量/映射全文只写在聊天里冒充已落盘。
|
||||
设定 / 变量 / 映射必须经 tool 写入;聊天只做确认与引导,不当落盘载体。
|
||||
|
||||
### insert — 不变型固定产物
|
||||
### insert — 写入/覆盖固定产物
|
||||
- position:\`用户.*\` / \`设计.*\`(优先用【能力 · 何时落盘】里的落盘 tag)
|
||||
- content:正文;order 可选(越小越靠前;越常改越大)
|
||||
- 勿用 insert 写变量目录或映射(用下面两个工具)
|
||||
- 同 position 再 insert = 整份覆盖改写;变量目录与映射改用 declare_variable / declare_map
|
||||
- **〔可反复〕须拆分**:生成规则 / 具体实例等每条增殖用 \`基名#唯一id\`(如 \`设计.生成规则#ex-girlfriend-card\`、\`设计.具体实例#lei-ying\`)。id 用 rule_id / batch_id / 姓名 slug。禁止反复 insert 基名覆盖旧条;改某一条才 insert 同一完整 tag。
|
||||
|
||||
### delete — 删除固定产物
|
||||
- position:已写入的 \`用户.*\` / \`设计.*\` tag,整份移除
|
||||
- 改内容用 insert,不要 delete 再重写除非用户要丢掉整块
|
||||
- 删单个变量字段 → undeclare_variable;删单条映射 → remove_map
|
||||
|
||||
### declare_variable — 跨轮真值
|
||||
- key / type / initial / user_visible(用户是否可见)
|
||||
- 只立必须跨轮记住的状态;派生文案不要立成可写变量
|
||||
- 时机对照「变量设计与更新规则」的何时用;美学未锚定前不要抢 declare
|
||||
- 时机对照「变量设计与更新规则」的何时用;美学未锚定前不 declare
|
||||
- 移除字段 → undeclare_variable
|
||||
|
||||
### declare_map — 真值→投影上下文
|
||||
- id / field(真值名)/ target_tag(\`上下文.*\` 或 \`大纲.*\`)/ bands[]
|
||||
- bands:\`{min,max,content}\` 区间(min 含 max 不含),或 \`{value,content}\`,或 \`{when:"default",content}\`
|
||||
- 槽位固定、内容随真值换档;旧档离开上下文
|
||||
- 移除一条 → remove_map(同 id 再 declare_map = 覆盖改写)
|
||||
|
||||
**自然语言回复**只写:短确认、是否这个、建议补充 A/B/C(须是条件已接近成立的下一刀)、提问。不要复述 tool 全文。
|
||||
## 对用户可见回复
|
||||
tool 跑完后写给用户看的 markdown。作用:确认本轮落了什么、点出薄弱处、用可选追问引导补洞或下一刀。
|
||||
|
||||
**分清:**
|
||||
执行顺序:
|
||||
1. 先完成本轮需要的 insert / declare_*。
|
||||
2. 收集各次 insert 成功返回的 \`reply_module\`(若有;每个模块一份独立 markdown)。
|
||||
3. 按下列规格写自然语言(粘贴产物全文或 tool JSON 都算失败):
|
||||
|
||||
**有 1 个 reply_module**
|
||||
- 用二级标题 \`## 模块 · {名称}\`(名称与该 reply_module 标题一致)
|
||||
- 段内按该模块的「写入用户回复时做什么」写满:确认要点 → 口头薄弱点 → 追问 1~2 点(每点含建议选项或短场景)
|
||||
|
||||
**有 N 个 reply_module(N≥2)**
|
||||
- 同轮多次 insert,或一步并行多个,都会各回一份 \`reply_module\`
|
||||
- 每个模块各写一块,标题互不相同的 \`## 模块 · {名称}\`;块与块之间空一行
|
||||
- 每一块只服务该模块;全部模块写完后,可另起一段写「建议下一刀」(仅 1 条、且条件已接近成立)
|
||||
|
||||
**没有 reply_module**
|
||||
- 用 2~4 句确认本轮写入的 tag 要点,并建议条件已近的下一刀;若落盘被挡住,只问挡住的 1 点
|
||||
|
||||
## 分清落盘目标
|
||||
- 正文组成(格式)→ insert \`设计.正文组成\`(须「回复呈现」等上游条件已满足,见能力表)
|
||||
- 开场白(内容)→ insert \`设计.开场白\`;落档/开玩同步 \`输出.开场白\`(收口;上游未齐不硬塞)
|
||||
- 多篇无关联短文(生产在游玩)→ 创作期钉 \`设计.模仿范例\` / \`设计.模仿要点\` / \`设计.叙事指南与故事推进\`(大纲扩写)
|
||||
- 好感分档性格等 → declare_variable + declare_map,不要 insert 一整张表进设定散文
|
||||
- 开场白(内容)→ insert \`设计.开场白\`;落档/开玩同步 \`输出.开场白\`(收口;上游未齐则先补上游,不硬塞开场)
|
||||
- 多篇无关联短文(生产在游玩)→ 创作期钉 \`设计.模仿范例\` / \`设计.模仿要点\` / \`设计.故事推进\`(大纲扩写);文风细则另 insert \`设计.叙事指南\`
|
||||
- 好感分档性格等 → declare_variable + declare_map;分档文案走映射,不 insert 整表散文
|
||||
- 代入名片(名字/身份/金手指)→ insert \`设计.主角设定\`(对照「主角设定」何时用;名字写成具体称呼,不用 @玩家 / 用户 / 主角)
|
||||
${recipeBlock}${moduleBlock}
|
||||
## 按用户本轮意图择一为主(仍须过「何时用」)
|
||||
落盘后的可见回复一律按「对用户可见回复」写;下列各节只约定本轮落什么。
|
||||
|
||||
### A · 钉体验 / 设定
|
||||
优先美学纲领与体验核心(条件总是先做);其它设定类 insert 仅当对应能力何时用成立。回复:确认本刀 + 建议条件已近的下一刀。
|
||||
优先美学纲领与体验核心(条件总是先做);其它设定类 insert 仅当对应能力何时用成立。
|
||||
|
||||
### B · 钉正文格式
|
||||
「回复呈现」等上游已齐时才 insert \`设计.正文组成\`。
|
||||
|
||||
### C · 生成开场白
|
||||
收口:先有正文组成且体验已锚定;insert \`设计.开场白\`(Markdown;岔路用 \`## 岔路\`)。
|
||||
**content 规格:** 正文必须含字面 \`@玩家\`(至少一处)作用户角色名位;禁止写死姓名;禁止全文只有「你」而零个 \`@玩家\`。缺则改写后再 insert。进游玩时程序替换 \`@玩家\`。
|
||||
收口:先有正文组成且体验已锚定;insert \`设计.开场白\`(岔路用 \`## 岔路\`)。
|
||||
**形状:** meta + 正文。有代入名片时,开头 YAML \`用户角色.名字\` / \`用户角色.简介\`,再 \`---\`,下面才是开场正文。不同开场可有不同用户角色。
|
||||
**正文格式:** 已有 \`设计.正文组成\` 时,正文必须是 present.v1 JSON(\`shell\` 与正文组成一致,主读写 \`blocks.body\`),不要只交裸 Markdown;尚未钉正文组成时才用 Markdown。
|
||||
**content 规格:** 正文至少一处字面 \`@玩家\` 作用户角色名位;姓名位用 \`@玩家\`,不写死具体姓名;全文不能只有「你」而零个 \`@玩家\`。缺则改写后再 insert。进游玩时程序替换 \`@玩家\`。
|
||||
进玩时若有用户角色,程序提示是否切换到这个预设。
|
||||
|
||||
### D · 钉变量与映射
|
||||
仅当「变量设计」何时用成立(体验须跨轮记住状态等):declare_variable → 有分档换文再 declare_map。勿因配方名叫数据化就抢跑 D。
|
||||
仅当「变量设计」何时用成立(体验须跨轮记住状态等):declare_variable → 有分档换文再 declare_map。配方名带「数据化」也不自动开跑 D。
|
||||
|
||||
### E · 文本生成器(创作收料 → 开玩挂载)
|
||||
必须用 insert 写齐游玩上下文:\`设计.模仿范例\`、\`设计.模仿要点\`、\`设计.叙事指南与故事推进\`(用户输入用法:大纲扩写;禁止扮演停笔)。缺任一就先补产物,不要说可以开玩。禁止在创作期写完整短文。开玩后程序只挂载这些产物:主世界出内容,转述出文风。
|
||||
必须用 insert 写齐游玩上下文:\`设计.模仿范例\`、\`设计.模仿要点\`、\`设计.故事推进\`(用户输入用法:大纲扩写;扮演停笔关掉)。文风细则对照「叙事指南」何时用再 insert \`设计.叙事指南\`。缺范例/要点/故事推进任一就先补产物,再说可以开玩。创作期不写完整短文。开玩后程序只挂载这些产物:主世界出内容,转述出文风。
|
||||
|
||||
### F · 快穿短局
|
||||
仅当本局配方是快穿短局,或用户只要一核短局。先 insert 美学纲领(这一核、怎么代入、何时等用户);代入有名字/简介则写进开场白的用户角色 meta(不单独 insert 主角设定)。核是变造法则再 insert 实现机制;有门控才 declare 至多三个真值。点名文风 → insert 叙事指南;点名推进/停笔 → insert 故事推进(条件独立)。核可玩即 insert 开场白:用户未点名分区/壳则跳过正文组成(覆盖 C),开玩吃默认散文。舞台骨架/生成规则/拓扑即使「何时用」成立也不 insert,把地点与规则折进美学核或开场白。用户改口要铺可经营世界 → 本局仍只收这一核,并告知改用「数据化跑团体验」或「回合推演」。
|
||||
|
||||
## 常用序感
|
||||
- 需求/美学等不变设定:靠前(负)
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
/**
|
||||
* 对话落盘 · 可增殖产物 tag 拆分
|
||||
*
|
||||
* catalog 标 repeatable 的能力(生成规则 / 具体实例)共用同一 artifact 基名。
|
||||
* 对话里若反复 insert 基 tag,后写会整份覆盖先写。
|
||||
* 约定:基名#槽位(如 设计.具体实例#lei-ying),每条增殖各占一 tag。
|
||||
*/
|
||||
|
||||
import { tryParseJsonDoc } from "../parse/json-doc.js";
|
||||
|
||||
export const REPEATABLE_TAG_SEP = "#";
|
||||
|
||||
/** 去掉 #槽位,得到能力 artifact 基名 */
|
||||
export function dictateProductFamily(tag: string): string {
|
||||
const t = tag.trim();
|
||||
if (!t) return "";
|
||||
const i = t.indexOf(REPEATABLE_TAG_SEP);
|
||||
return i >= 0 ? t.slice(0, i).trim() : t;
|
||||
}
|
||||
|
||||
/** 显式槽位;无 # 则空 */
|
||||
export function dictateProductSlot(tag: string): string {
|
||||
const t = tag.trim();
|
||||
const i = t.indexOf(REPEATABLE_TAG_SEP);
|
||||
if (i < 0) return "";
|
||||
return sanitizeRepeatableSlot(t.slice(i + 1));
|
||||
}
|
||||
|
||||
export function isDictateProductFamily(tag: string, family: string): boolean {
|
||||
return dictateProductFamily(tag) === family.trim();
|
||||
}
|
||||
|
||||
/** 槽位:英文 kebab / 短中文均可;去掉危险字符 */
|
||||
export function sanitizeRepeatableSlot(raw: string): string {
|
||||
return String(raw ?? "")
|
||||
.trim()
|
||||
.replace(/[\n\r\0#/\\]/g, "")
|
||||
.replace(/\s+/g, "-")
|
||||
.slice(0, 64);
|
||||
}
|
||||
|
||||
export function buildRepeatableProductTag(
|
||||
family: string,
|
||||
slot: string,
|
||||
): string {
|
||||
const base = family.trim();
|
||||
const s = sanitizeRepeatableSlot(slot);
|
||||
return s ? `${base}${REPEATABLE_TAG_SEP}${s}` : base;
|
||||
}
|
||||
|
||||
function asString(v: unknown): string | undefined {
|
||||
if (typeof v !== "string") return undefined;
|
||||
const t = v.trim();
|
||||
return t || undefined;
|
||||
}
|
||||
|
||||
function bodyOfFragment(content: string): Record<string, unknown> | null {
|
||||
const parsed = tryParseJsonDoc(content);
|
||||
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||
return null;
|
||||
}
|
||||
const body = (parsed as Record<string, unknown>).正文;
|
||||
if (!body || typeof body !== "object" || Array.isArray(body)) return null;
|
||||
return body as Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从正文抽出建议槽位:生成规则 → rule_id;具体实例 → batch_id / 首条姓名。
|
||||
*/
|
||||
export function extractRepeatableSlotFromContent(
|
||||
family: string,
|
||||
content: string,
|
||||
): string | undefined {
|
||||
const body = bodyOfFragment(content);
|
||||
if (!body) return undefined;
|
||||
const base = family.trim();
|
||||
|
||||
if (base === "设计.生成规则" || /生成规则$/.test(base)) {
|
||||
const top =
|
||||
asString(body.rule_id) ?? asString(body.id) ?? asString(body.规则id);
|
||||
if (top) return sanitizeRepeatableSlot(top) || undefined;
|
||||
const rules = body.rules;
|
||||
if (Array.isArray(rules)) {
|
||||
for (const row of rules) {
|
||||
if (!row || typeof row !== "object" || Array.isArray(row)) continue;
|
||||
const r = row as Record<string, unknown>;
|
||||
const id =
|
||||
asString(r.rule_id) ?? asString(r.id) ?? asString(r.规则id);
|
||||
if (id) return sanitizeRepeatableSlot(id) || undefined;
|
||||
}
|
||||
}
|
||||
const target = asString(body.target) ?? asString(body.对象) ?? asString(body.对象族);
|
||||
if (target) return sanitizeRepeatableSlot(target) || undefined;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (base === "设计.具体实例" || /具体实例$/.test(base)) {
|
||||
const batch =
|
||||
asString(body.batch_id) ??
|
||||
asString(body.批次id) ??
|
||||
asString(body.id);
|
||||
if (batch) return sanitizeRepeatableSlot(batch) || undefined;
|
||||
const records = body.records;
|
||||
if (Array.isArray(records)) {
|
||||
for (const row of records) {
|
||||
if (!row || typeof row !== "object" || Array.isArray(row)) continue;
|
||||
const r = row as Record<string, unknown>;
|
||||
const name =
|
||||
asString(r.姓名) ??
|
||||
asString(r.name) ??
|
||||
asString(r.名称) ??
|
||||
asString(r.id);
|
||||
if (name) return sanitizeRepeatableSlot(name) || undefined;
|
||||
}
|
||||
}
|
||||
const ruleId = asString(body.rule_id);
|
||||
if (ruleId) {
|
||||
return sanitizeRepeatableSlot(`${ruleId}-batch`) || undefined;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export type ResolveRepeatableInsertResult =
|
||||
| { tag: string; remapped?: boolean; note?: string }
|
||||
| { error: string };
|
||||
|
||||
/**
|
||||
* 可增殖 insert:有槽位则落到 基名#槽位;无槽位且基名已有产物 → 拒绝覆盖。
|
||||
*/
|
||||
export function resolveRepeatableInsertTag(params: {
|
||||
tag: string;
|
||||
content: string;
|
||||
existingTags: readonly string[];
|
||||
repeatableFamilies: ReadonlySet<string>;
|
||||
}): ResolveRepeatableInsertResult {
|
||||
const raw = params.tag.trim();
|
||||
if (!raw) return { error: "position 为空" };
|
||||
|
||||
const family = dictateProductFamily(raw);
|
||||
if (!params.repeatableFamilies.has(family)) {
|
||||
return { tag: raw };
|
||||
}
|
||||
|
||||
const explicit = dictateProductSlot(raw);
|
||||
const extracted = extractRepeatableSlotFromContent(family, params.content);
|
||||
const slot = explicit || extracted || "";
|
||||
|
||||
const siblings = params.existingTags
|
||||
.map((t) => t.trim())
|
||||
.filter((t) => t && dictateProductFamily(t) === family);
|
||||
|
||||
if (!slot) {
|
||||
if (siblings.length > 0 && raw === family) {
|
||||
return {
|
||||
error: `「${family}」是可增殖产物,追加请用「${family}#唯一id」(生成规则用 rule_id,具体实例用 batch_id/姓名),不要覆盖基 tag。改某一条则 insert 同一完整 tag。`,
|
||||
};
|
||||
}
|
||||
return { tag: family };
|
||||
}
|
||||
|
||||
const resolved = buildRepeatableProductTag(family, slot);
|
||||
if (raw === family || raw !== resolved) {
|
||||
return {
|
||||
tag: resolved,
|
||||
remapped: raw !== resolved,
|
||||
note: raw !== resolved ? `可增殖已拆到 ${resolved}` : undefined,
|
||||
};
|
||||
}
|
||||
return { tag: resolved };
|
||||
}
|
||||
|
||||
/** 收集同族全部正文(供旁观抽样等仍按「一份生成规则」读的逻辑) */
|
||||
export function collectFamilyContents(
|
||||
entries: ReadonlyArray<{ tag: string; content: string }>,
|
||||
family: string,
|
||||
): string[] {
|
||||
const base = family.trim();
|
||||
return entries
|
||||
.filter((e) => isDictateProductFamily(e.tag, base) && e.content.trim())
|
||||
.map((e) => e.content);
|
||||
}
|
||||
|
||||
/**
|
||||
* 把多份生成规则 context-fragment 合成一份(合并 rules[]),便于 findRuleBody。
|
||||
*/
|
||||
export function mergeGenerationRulesArtifacts(
|
||||
rawList: readonly string[],
|
||||
): string | undefined {
|
||||
const rules: unknown[] = [];
|
||||
let shell: Record<string, unknown> | null = null;
|
||||
for (const raw of rawList) {
|
||||
const parsed = tryParseJsonDoc(raw);
|
||||
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||
continue;
|
||||
}
|
||||
const doc = parsed as Record<string, unknown>;
|
||||
if (!shell) shell = { ...doc };
|
||||
const body = doc.正文;
|
||||
if (!body || typeof body !== "object" || Array.isArray(body)) continue;
|
||||
const rows = (body as Record<string, unknown>).rules;
|
||||
if (!Array.isArray(rows)) continue;
|
||||
for (const row of rows) rules.push(row);
|
||||
}
|
||||
if (!shell || rules.length === 0) {
|
||||
// 无法 JSON 合并时:按序拼接,抽样侧仍可能从单份命中
|
||||
const nonempty = rawList.map((s) => s.trim()).filter(Boolean);
|
||||
return nonempty.length ? nonempty.join("\n\n---\n\n") : undefined;
|
||||
}
|
||||
const body =
|
||||
shell.正文 && typeof shell.正文 === "object" && !Array.isArray(shell.正文)
|
||||
? { ...(shell.正文 as Record<string, unknown>), rules }
|
||||
: { rules };
|
||||
return JSON.stringify({ ...shell, 正文: body }, null, 2);
|
||||
}
|
||||
+66
-4
@@ -1,7 +1,7 @@
|
||||
import type { ToolDefinition } from "../llm/client.js";
|
||||
|
||||
/**
|
||||
* Boss 直聘创作期工具:产物 / 变量 / 映射均须 toolcall;聊天只做确认与建议。
|
||||
* 对话落盘创作期工具:产物 / 变量 / 映射均须 toolcall;聊天只做确认与建议。
|
||||
*/
|
||||
export const DICTATE_TOOL_DEFINITIONS: ToolDefinition[] = [
|
||||
{
|
||||
@@ -9,19 +9,19 @@ export const DICTATE_TOOL_DEFINITIONS: ToolDefinition[] = [
|
||||
function: {
|
||||
name: "insert",
|
||||
description:
|
||||
"写入不变型固定产物(设定/格式/开场等)。确认与建议只写在普通回复。用 order 标相对先后。",
|
||||
"写入或覆盖固定产物。同 position 再调用即整份改写。可增殖(生成规则/具体实例)须用 基名#唯一id 拆分,禁止反复写基名覆盖。成功时可能返回 reply_module:末尾可见回复须附带该模块;同轮多个则每个模块各写一块 ## 模块 · 名称。确认与追问写在普通回复,不写进本工具。order 标相对先后。",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
position: {
|
||||
type: "string",
|
||||
description:
|
||||
"产物 tag,须以「用户.」或「设计.」开头。例:用户.需求、设计.正文组成、设计.开场白。勿用本工具写变量目录/映射。",
|
||||
"产物 tag,须以「用户.」或「设计.」开头。例:用户.需求、设计.正文组成、设计.开场白;可增殖例:设计.生成规则#rule-id、设计.具体实例#batch-id。勿用本工具写变量目录/映射。",
|
||||
},
|
||||
content: {
|
||||
type: "string",
|
||||
description:
|
||||
"落入该位置的正文。设计.正文组成=格式 JSON;设计.开场白=可读开场 Markdown,必须含字面 @玩家(用户角色名位,禁止写死姓名)",
|
||||
"落入该位置的正文。设计.正文组成=格式 JSON;设计.开场白=meta + 正文(有正文组成时正文为 present.v1 JSON;否则 Markdown),必须含字面 @玩家(用户角色名位,禁止写死姓名)",
|
||||
},
|
||||
order: {
|
||||
type: "number",
|
||||
@@ -112,6 +112,68 @@ export const DICTATE_TOOL_DEFINITIONS: ToolDefinition[] = [
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "delete",
|
||||
description:
|
||||
"删除一个已落盘的固定产物 tag(整份移除)。改内容请用 insert 覆盖;删变量字段用 undeclare_variable;删映射条目用 remove_map。勿用本工具写空 content 假装删除。",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
position: {
|
||||
type: "string",
|
||||
description:
|
||||
"要删的产物 tag,须以「用户.」或「设计.」开头。例:设计.开场白。删整份变量目录/映射也可(设计.变量目录 / 设计.变量映射)。",
|
||||
},
|
||||
reason: {
|
||||
type: "string",
|
||||
description: "为何删除(一句话,可选)",
|
||||
},
|
||||
},
|
||||
required: ["position"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "undeclare_variable",
|
||||
description:
|
||||
"从设计.变量目录移除一个真值字段,并同步从变量.当前去掉该格。改初值/可见性请用 declare_variable。",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
key: {
|
||||
type: "string",
|
||||
description: "要移除的字段名",
|
||||
},
|
||||
},
|
||||
required: ["key"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "remove_map",
|
||||
description:
|
||||
"从设计.变量映射按 id 移除一条映射;若无其它映射共用其投影 tag,则一并删除该投影 tag。",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: {
|
||||
type: "string",
|
||||
description: "映射 id(与 declare_map 时一致)",
|
||||
},
|
||||
},
|
||||
required: ["id"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
|
||||
+185
-5
@@ -12,11 +12,14 @@ import {
|
||||
type DictateChatTurn,
|
||||
type DictateProduct,
|
||||
} from "./types.js";
|
||||
import { DICTATE_MULTI_MODULE_REPLY_HINT } from "./insert-feedback.js";
|
||||
import { resolveRepeatableInsertTag } from "./repeatable-tags.js";
|
||||
import {
|
||||
VARIABLE_CATALOG_TAG,
|
||||
parseVariableCatalog,
|
||||
serializeVariableCatalog,
|
||||
upsertVariableField,
|
||||
removeVariableField,
|
||||
type UpsertVariableInput,
|
||||
} from "../skills/variable-catalog.js";
|
||||
import {
|
||||
@@ -24,31 +27,46 @@ import {
|
||||
parseValueMapDoc,
|
||||
serializeValueMapDoc,
|
||||
upsertValueMapEntry,
|
||||
removeValueMapEntry,
|
||||
type UpsertMapInput,
|
||||
} from "../skills/value-map.js";
|
||||
import {
|
||||
parseTableDoc,
|
||||
stringifyTableDoc,
|
||||
} from "../blackboard/table-cells.js";
|
||||
|
||||
export type DictateTurnHandlers = {
|
||||
listProducts: () => DictateProduct[];
|
||||
/** order 省略时由实现沿用原序或默认 */
|
||||
writeProduct: (tag: string, content: string, order?: number) => void;
|
||||
/** 删除固定产物 tag;返回是否曾存在 */
|
||||
deleteProduct: (tag: string) => boolean;
|
||||
/** 读/写变量目录与映射(程序校验后的 JSON) */
|
||||
readTag: (tag: string) => string | undefined;
|
||||
writeTag: (tag: string, content: string) => void;
|
||||
/** 删除任意黑板 tag(投影 / 变量.当前 等) */
|
||||
deleteTag: (tag: string) => boolean;
|
||||
clearDialogue: () => void;
|
||||
onToolCall?: (name: string, detail: string) => void;
|
||||
onThinkingDelta?: (delta: string) => void;
|
||||
onThinkingDone?: (text: string) => void;
|
||||
/**
|
||||
* insert 成功后:按 tag 反查能力,返回独立「回复模块」markdown。
|
||||
* 程序写入 tool result 的 reply_module;同轮多个须在末尾回复中全部附带。
|
||||
*/
|
||||
lookupInsertFeedback?: (tag: string) => string | undefined;
|
||||
};
|
||||
|
||||
export type DictateTurnResult = {
|
||||
reply: string;
|
||||
iterations: number;
|
||||
wroteTags: string[];
|
||||
deletedTags: string[];
|
||||
clearedDialogue: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* 跑一轮 Boss 直聘整理:任务 messages 由本模块拼;preset 夹心由 LLM 包装层统一装配。
|
||||
* 跑一轮对话落盘整理:任务 messages 由本模块拼;preset 夹心由 LLM 包装层统一装配。
|
||||
*/
|
||||
export async function runDictateTurn(params: {
|
||||
llm: LlmProvider;
|
||||
@@ -59,12 +77,18 @@ export async function runDictateTurn(params: {
|
||||
recipeBrief?: string;
|
||||
/** modules catalog 的何时落盘块(formatModuleCatalogForDictate) */
|
||||
moduleGuide?: string;
|
||||
/** catalog 中 repeatable 能力的 artifact 基名(设计.生成规则 等) */
|
||||
repeatableFamilies?: ReadonlySet<string> | readonly string[];
|
||||
driver?: AgentDriver;
|
||||
}): Promise<DictateTurnResult> {
|
||||
const driver = params.driver ?? createLocalLlmDriver(params.llm);
|
||||
const wroteTags: string[] = [];
|
||||
const deletedTags: string[] = [];
|
||||
let clearedDialogue = false;
|
||||
let dialogue = [...params.dialogue];
|
||||
const repeatableFamilies = new Set(
|
||||
[...(params.repeatableFamilies ?? [])].map((t) => String(t).trim()).filter(Boolean),
|
||||
);
|
||||
|
||||
const systemPrompt =
|
||||
params.systemPrompt ??
|
||||
@@ -85,14 +109,15 @@ export async function runDictateTurn(params: {
|
||||
messages: userBlock,
|
||||
tools: DICTATE_TOOL_DEFINITIONS,
|
||||
caller: "dictate_agent",
|
||||
label: "Boss直聘",
|
||||
label: "对话落盘",
|
||||
onThinkingDelta: params.handlers.onThinkingDelta,
|
||||
onThinkingDone: params.handlers.onThinkingDone,
|
||||
handleStep: (calls) => {
|
||||
const moduleNamesThisStep: string[] = [];
|
||||
const results = calls.map((call) => {
|
||||
const args = safeParseArgs(call.arguments);
|
||||
if (call.name === "insert" || call.name === "write_product") {
|
||||
const tag = String(args.position ?? args.tag ?? "").trim();
|
||||
let tag = String(args.position ?? args.tag ?? "").trim();
|
||||
const content = String(args.content ?? "");
|
||||
if (tag === VARIABLE_CATALOG_TAG || tag === VALUE_MAP_TAG) {
|
||||
const err = `请用 declare_variable / declare_map,不要 insert ${tag}`;
|
||||
@@ -109,18 +134,47 @@ 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);
|
||||
return {
|
||||
callId: call.id,
|
||||
content: JSON.stringify({ error: resolved.error }),
|
||||
};
|
||||
}
|
||||
tag = resolved.tag;
|
||||
}
|
||||
const order = parseDictateOrder(args.order);
|
||||
params.handlers.writeProduct(tag, content, order);
|
||||
wroteTags.push(tag);
|
||||
const detail =
|
||||
order === undefined ? tag : `${tag} order=${order}`;
|
||||
params.handlers.onToolCall?.(call.name, detail);
|
||||
const replyModule =
|
||||
params.handlers.lookupInsertFeedback?.(tag)?.trim() || undefined;
|
||||
if (replyModule) {
|
||||
const title =
|
||||
replyModule.match(/^#\s*模块\s*·\s*(.+)$/m)?.[1]?.trim() || tag;
|
||||
moduleNamesThisStep.push(title);
|
||||
}
|
||||
return {
|
||||
callId: call.id,
|
||||
content: JSON.stringify({
|
||||
ok: true,
|
||||
position: tag,
|
||||
...(order !== undefined ? { order } : {}),
|
||||
...(replyModule
|
||||
? {
|
||||
reply_module: replyModule,
|
||||
reply_hint: DICTATE_MULTI_MODULE_REPLY_HINT,
|
||||
}
|
||||
: {}),
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -185,6 +239,108 @@ export async function runDictateTurn(params: {
|
||||
}),
|
||||
};
|
||||
}
|
||||
if (call.name === "delete") {
|
||||
const tag = String(args.position ?? args.tag ?? "").trim();
|
||||
const reason = String(args.reason ?? "").trim();
|
||||
if (!isAllowedProductTag(tag)) {
|
||||
const err = `拒绝删除:position 必须以「用户.」或「设计.」开头(收到:${tag || "空"})`;
|
||||
params.handlers.onToolCall?.(call.name, err);
|
||||
return { callId: call.id, content: JSON.stringify({ error: err }) };
|
||||
}
|
||||
const existed = params.handlers.deleteProduct(tag);
|
||||
if (!existed) {
|
||||
const err = `无产物 ${tag}`;
|
||||
params.handlers.onToolCall?.(call.name, err);
|
||||
return { callId: call.id, content: JSON.stringify({ error: err }) };
|
||||
}
|
||||
deletedTags.push(tag);
|
||||
const detail = reason ? `${tag}(${reason})` : tag;
|
||||
params.handlers.onToolCall?.(call.name, detail);
|
||||
return {
|
||||
callId: call.id,
|
||||
content: JSON.stringify({ ok: true, deleted: tag }),
|
||||
};
|
||||
}
|
||||
if (call.name === "undeclare_variable") {
|
||||
const key = String(args.key ?? "").trim();
|
||||
const current = parseVariableCatalog(
|
||||
params.handlers.readTag(VARIABLE_CATALOG_TAG),
|
||||
);
|
||||
const { doc, error } = removeVariableField(current, key);
|
||||
if (error) {
|
||||
params.handlers.onToolCall?.(call.name, error);
|
||||
return { callId: call.id, content: JSON.stringify({ error }) };
|
||||
}
|
||||
if (doc.fields.length === 0) {
|
||||
params.handlers.deleteTag(VARIABLE_CATALOG_TAG);
|
||||
} else {
|
||||
params.handlers.writeTag(
|
||||
VARIABLE_CATALOG_TAG,
|
||||
serializeVariableCatalog(doc),
|
||||
);
|
||||
}
|
||||
// 同步从 变量.当前 / 运行.初始变量 去掉该格
|
||||
for (const tableTag of ["变量.当前", "运行.初始变量"] as const) {
|
||||
const raw = params.handlers.readTag(tableTag);
|
||||
const table = parseTableDoc(raw);
|
||||
if (!table?.rows.some((r) => r.key === key)) continue;
|
||||
const next = {
|
||||
rows: table.rows.filter((r) => r.key !== key),
|
||||
};
|
||||
if (next.rows.length === 0) {
|
||||
params.handlers.deleteTag(tableTag);
|
||||
} else {
|
||||
params.handlers.writeTag(tableTag, stringifyTableDoc(next));
|
||||
}
|
||||
}
|
||||
deletedTags.push(`${VARIABLE_CATALOG_TAG}#${key}`);
|
||||
params.handlers.onToolCall?.(call.name, key);
|
||||
return {
|
||||
callId: call.id,
|
||||
content: JSON.stringify({
|
||||
ok: true,
|
||||
key,
|
||||
fields: doc.fields.length,
|
||||
}),
|
||||
};
|
||||
}
|
||||
if (call.name === "remove_map") {
|
||||
const id = String(args.id ?? "").trim();
|
||||
const current = parseValueMapDoc(
|
||||
params.handlers.readTag(VALUE_MAP_TAG),
|
||||
);
|
||||
const { doc, error, removed } = removeValueMapEntry(current, id);
|
||||
if (error || !removed) {
|
||||
const err = error ?? `映射无 id ${id}`;
|
||||
params.handlers.onToolCall?.(call.name, err);
|
||||
return { callId: call.id, content: JSON.stringify({ error: err }) };
|
||||
}
|
||||
if (doc.maps.length === 0) {
|
||||
params.handlers.deleteTag(VALUE_MAP_TAG);
|
||||
} else {
|
||||
params.handlers.writeTag(VALUE_MAP_TAG, serializeValueMapDoc(doc));
|
||||
}
|
||||
const stillUsed = doc.maps.some(
|
||||
(m) => m.target_tag === removed.target_tag,
|
||||
);
|
||||
if (!stillUsed) {
|
||||
params.handlers.deleteTag(removed.target_tag);
|
||||
}
|
||||
deletedTags.push(`${VALUE_MAP_TAG}#${id}`);
|
||||
params.handlers.onToolCall?.(
|
||||
call.name,
|
||||
`${id} → ${removed.target_tag}`,
|
||||
);
|
||||
return {
|
||||
callId: call.id,
|
||||
content: JSON.stringify({
|
||||
ok: true,
|
||||
id,
|
||||
cleared_target: stillUsed ? null : removed.target_tag,
|
||||
maps: doc.maps.length,
|
||||
}),
|
||||
};
|
||||
}
|
||||
if (call.name === "clear_dialogue") {
|
||||
const reason = String(args.reason ?? "").trim() || "上下文过长";
|
||||
params.handlers.clearDialogue();
|
||||
@@ -204,6 +360,22 @@ export async function runDictateTurn(params: {
|
||||
params.handlers.onToolCall?.(call.name, err);
|
||||
return { callId: call.id, content: JSON.stringify({ error: err }) };
|
||||
});
|
||||
|
||||
if (moduleNamesThisStep.length > 1) {
|
||||
const roster = moduleNamesThisStep.join("、");
|
||||
for (const r of results) {
|
||||
try {
|
||||
const parsed = JSON.parse(r.content) as Record<string, unknown>;
|
||||
if (typeof parsed.reply_module === "string") {
|
||||
parsed.reply_hint = `${DICTATE_MULTI_MODULE_REPLY_HINT} 本步已带回模块:${roster}。`;
|
||||
r.content = JSON.stringify(parsed);
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { kind: "continue" as const, results };
|
||||
},
|
||||
});
|
||||
@@ -213,8 +385,15 @@ export async function runDictateTurn(params: {
|
||||
reply = run.stop.content.trim();
|
||||
}
|
||||
if (!reply) {
|
||||
if (wroteTags.length) {
|
||||
reply = `已写入:${[...new Set(wroteTags)].join("、")}。还有要补充的吗?`;
|
||||
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 {
|
||||
@@ -226,6 +405,7 @@ export async function runDictateTurn(params: {
|
||||
reply,
|
||||
iterations: run.iterations,
|
||||
wroteTags: [...new Set(wroteTags)],
|
||||
deletedTags: [...new Set(deletedTags)],
|
||||
clearedDialogue,
|
||||
};
|
||||
}
|
||||
|
||||
+13
-8
@@ -33,17 +33,22 @@ export function isAllowedProductTag(tag: string): boolean {
|
||||
/**
|
||||
* 未显式给 order 时的默认相对序。
|
||||
* 越稳定/重要越靠前(更负);越常改越靠后(更正)。
|
||||
* 可增殖拆分 tag(基名#槽)按基名归类。
|
||||
*/
|
||||
export function defaultDictateOrder(tag: string): number {
|
||||
const t = tag.trim();
|
||||
if (t === "用户.需求") return -40;
|
||||
if (t === "设计.变量目录" || t === "设计.变量映射") return -25;
|
||||
if (t === "设计.模仿范例" || t === "设计.模仿要点") return -28;
|
||||
if (/美学|纲领|禁忌|示例|模仿/.test(t)) return -30;
|
||||
if (/文风|叙事指南|篇幅|结构/.test(t)) return -20;
|
||||
if (/短文集|短文\./.test(t)) return 10;
|
||||
if (/正文组成|回复格式/.test(t)) return 0;
|
||||
if (/开场白/.test(t)) return 20;
|
||||
const family = t.includes("#") ? t.slice(0, t.indexOf("#")).trim() : t;
|
||||
if (family === "用户.需求" || t === "用户.需求") return -40;
|
||||
if (family === "设计.变量目录" || family === "设计.变量映射") return -25;
|
||||
if (family === "设计.模仿范例" || family === "设计.模仿要点") return -28;
|
||||
if (/美学|纲领|禁忌|示例|模仿/.test(family)) return -30;
|
||||
if (/主角设定/.test(family)) return -26;
|
||||
if (/文风|叙事指南|故事推进|篇幅|结构/.test(family)) return -20;
|
||||
if (/生成规则/.test(family)) return -6;
|
||||
if (/具体实例/.test(family)) return -4;
|
||||
if (/短文集|短文\./.test(family)) return 10;
|
||||
if (/正文组成|回复格式/.test(family)) return 0;
|
||||
if (/开场白/.test(family)) return 20;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user