fix(core): let short-fiction output language follow the user's request instead of the session language

修复 issue #345 残留缺口:用户用中文对话要求"写一篇英文短篇"时,产出仍然是中文。

根因:
- Studio 用 inferLanguage 按 CJK/拉丁字符比例推断会话语言,中文指令推断为 zh;
- zh 分支短篇系统提示词硬编码"shortRun 填 language=zh"(en 分支同样硬编码 language=en),
  即使用户明确要求另一种产出语言,模型也照抄硬编码值;
- propose_action 的 shortRun.language schema description 只写了
  "Confirmed short-fiction language.",没有告诉模型产出语言可以和对话语言不同。

修法:
- agent-system-prompt.ts 短篇 zh/en 两个分支对称改为:language 填用户要求的产出语言,
  用户没提时跟对话语言一致;同时把 charsPerChapter 指引改为按 language 分语言说明
  (zh 每章 900-1200 字默认 1000,en 每章 600-800 个英文单词默认 650);
- agent-tools.ts 的 shortRun.language description 改为说明按用户要求的产出语言填,
  可以和对话语言不同,未指定时默认对话语言。

传参链路本身无需改动:proposedActionPayload 里 { language, ...shortRun } 的展开顺序
保证模型显式填的 language 覆盖会话默认值,且会话默认只注入 language 不注入字数,
runner 在未填 charsPerChapter 时按 language 取对应默认(en 650 词),不会把 zh 字数
带给 en 任务。新增测试固定以上行为和提示词/schema 文案。
This commit is contained in:
Ma
2026-07-15 01:01:03 +08:00
parent 26e659d0ab
commit a662224308
4 changed files with 75 additions and 3 deletions
@@ -214,6 +214,20 @@ describe("buildAgentSystemPrompt", () => {
expect(prompt).not.toContain("sub_agent");
expect(prompt).not.toContain("architect");
});
it("fills shortRun.language from the user's requested output language instead of hardcoding the session language", () => {
const zhPrompt = buildAgentSystemPrompt(null, "zh", "short");
expect(zhPrompt).not.toContain("language=zh、chapters");
expect(zhPrompt).toContain("language 填用户要求的产出语言");
expect(zhPrompt).toContain("900-1200");
expect(zhPrompt).toContain("600-800");
const enPrompt = buildAgentSystemPrompt(null, "en", "short");
expect(enPrompt).not.toContain("language=en, chapters");
expect(enPrompt).toContain("the output language the user asked for");
expect(enPrompt).toContain("900-1200");
expect(enPrompt).toContain("600-800");
});
});
describe("script and storyboard modes", () => {
@@ -195,6 +195,64 @@ describe("agent tools language wiring (en parity)", () => {
});
});
it("lets an explicit shortRun.language=en override the zh session default in the confirmation payload", async () => {
const result = await createProposeActionTool("zh").execute("propose-short-zh-en", {
action: "short_run",
instruction: "用户在中文对话里要求写一篇英文办公室悬疑短篇",
shortRun: {
direction: "an English office suspense story about forged expense records",
language: "en",
chapters: 12,
charsPerChapter: 650,
cover: false,
},
} as any);
expect(result.details).toMatchObject({
kind: "proposed_action",
actionPayload: {
shortRun: {
language: "en",
charsPerChapter: 650,
},
},
});
});
it("does not inject a zh charsPerChapter default when a zh session confirms an en short", async () => {
const result = await createProposeActionTool("zh").execute("propose-short-zh-en-no-length", {
action: "short_run",
instruction: "用户在中文对话里要求写一篇英文短篇,未指定每章字数",
shortRun: {
direction: "an English office suspense story",
language: "en",
cover: false,
},
} as any);
const shortRun = (result.details as any).actionPayload.shortRun;
expect(shortRun.language).toBe("en");
expect(shortRun.charsPerChapter).toBeUndefined();
const pipeline = { createAgentContext: vi.fn(() => ({})) };
const tool = createShortFictionRunTool(pipeline as never, root, {
language: "zh",
actionPayload: { shortRun } as any,
});
await tool.execute("short-zh-en-no-length", { direction: "fallback direction" } as any);
const runnerOptions = runShortFictionProductionMock.mock.calls[0]![0] as any;
expect(runnerOptions.language).toBe("en");
expect(runnerOptions.charsPerChapter).toBeUndefined();
});
it("documents in the shortRun.language schema that the output language may differ from the conversation language", () => {
const parameters = createProposeActionTool("zh").parameters as any;
const description = parameters.properties.shortRun.properties.language.description as string;
expect(description).toMatch(/output language/i);
expect(description).toMatch(/differ from the conversation language/i);
});
it("lets the confirmed short payload override the project language", async () => {
const pipeline = { createAgentContext: vi.fn(() => ({})) };
const tool = createShortFictionRunTool(pipeline as never, root, {
@@ -192,14 +192,14 @@ ${commonOutputRules(false)}`;
? `你是 InkOS Short 助手。当前入口只负责把独立短篇或短篇封面需求聊清楚,然后让用户确认。
可用工具:propose_action、ingest_material、retrieve_material。短篇成品用 action=short_run;只做封面用 action=generate_cover。用户上传或提供参考资料时先归档/召回相关资料,但不要直接生成成品。核心冲突和主角压力明确时必须调用 propose_action,不要用普通文字手写确认卡。用户说“先确认/确认后再写”时,propose_action 就是确认卡,仍然调用它,不要先用普通文字整理一遍再等用户二次确认。
instruction 必须自包含:题材方向、标题/暂定名、主角压力、核心冲突、情绪回报、封面视觉方向或目标短篇路径。生成完整短篇时同时填 shortRundirection、language=zh、chapters、charsPerChapter、covercharsPerChapter 只能是每章 900-1200 字,不是整篇总字数
instruction 必须自包含:题材方向、标题/暂定名、主角压力、核心冲突、情绪回报、封面视觉方向或目标短篇路径。生成完整短篇时同时填 shortRundirection、language、chapters、charsPerChapter、cover。language 填用户要求的产出语言,可以和对话语言不同:用户没提产出语言时跟对话语言一致(本会话填 zh);用户明确要求用英文写作时填 en。charsPerChapter 是每章篇幅,不是整篇总字数:zh 是每章 900-1200 字(默认 1000),en 是每章 600-800 个英文单词(默认 650
标题或封面视觉缺失时可以自行拟一个工作版本写进 instruction;只有题材、主角压力或核心冲突太空时才问一个关键问题。不要创建长篇 books/ 项目,不要启动互动世界,不要把短篇转成长篇建书。
${commonOutputRules(true)}`
: `You are the InkOS Short assistant. This surface clarifies standalone short-fiction or cover requests and asks for confirmation before production.
Available tools: propose_action, ingest_material, retrieve_material. Use action=short_run for full short production; action=generate_cover for cover-only work. Archive/retrieve user-provided references when needed, but do not generate finished content directly. When the core conflict and protagonist pressure are clear, you must call propose_action; do not hand-write the confirmation card as plain text. If the user says "confirm first" or "write after confirmation", propose_action is that confirmation card; still call it instead of summarizing in plain text and waiting for a second confirmation.
instruction must be self-contained: genre direction, title/working title, protagonist pressure, core conflict, emotional payoff, cover direction, or target short path. For full short production, also fill shortRun: direction, language=en, chapters, charsPerChapter, cover; charsPerChapter is 600-800 English words per chapter (default 650), not total story length.
instruction must be self-contained: genre direction, title/working title, protagonist pressure, core conflict, emotional payoff, cover direction, or target short path. For full short production, also fill shortRun: direction, language, chapters, charsPerChapter, cover. Set language to the output language the user asked for; it may differ from the conversation language: keep the conversation language (en here) when the user does not name one, and fill zh when the user explicitly asks for a Chinese short. charsPerChapter is per-chapter length, not total story length: 900-1200 Chinese characters (default 1000) for zh, or 600-800 English words (default 650) for en.
If title or cover direction is missing, invent a working version inside instruction; ask one key question only when genre, protagonist pressure, or core conflict is too vague. Do not create books/ projects, start play worlds, or route short-fiction requests to book creation.
${commonOutputRules(false)}`;
+1 -1
View File
@@ -182,7 +182,7 @@ const ProposeActionParams = Type.Object({
language: Type.Optional(Type.Union([
Type.Literal("zh"),
Type.Literal("en"),
], { description: "Confirmed short-fiction language." })),
], { description: "Output language of the short fiction. Fill the language the user asked the story to be written in; it may differ from the conversation language (e.g. a Chinese chat asking for an English short => en). When the user does not name one, it defaults to the conversation language." })),
chapters: Type.Optional(Type.Number({
description: "Confirmed complete short chapter count, 12-18.",
})),