完善创作节点循环与等待态交互,并大幅打磨 Web 壳与会话运行时。

- 节点循环:配方开局直坐 DAG 第一步;验收后先问下一步意向,再确认开干并可钉编排参数;「按意见修改」只重跑当前节点。
- 询问分流:能力 opening 走说话面引导,可跳过追问按题干去重;追问与产物同轮挂载,避免拆成两段历史。
- 运行时:补强 revision 重跑、创作流程合并/进度指针、工具循环与 worker 执行;新增运行日志与 web:watch。
- 前端:统一等待态文案与底栏主按钮,完善询问卡/产物验收/呈现壳样式与交互。
- 同步世界模拟器模块提示、编排文档与相关测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-17 01:28:12 +08:00
parent 20938b04d7
commit ec474cb946
56 changed files with 7460 additions and 1372 deletions

View File

@@ -1,8 +1,14 @@
import { describe, expect, it } from "vitest";
import {
appendAskedQuestions,
dropAlreadyAskedQuestions,
formatQuestionAnswersForAi,
formatQuestionAnswersForDisplay,
isModuleOpeningQuestions,
MODULE_OPENING_QUESTION_ID,
mergeQuestionsPreferFragment,
normalizeQuestions,
parseAskedQuestions,
} from "../src/skills/question-protocol.js";
describe("normalizeQuestions", () => {
@@ -28,6 +34,131 @@ describe("normalizeQuestions", () => {
});
});
describe("isModuleOpeningQuestions", () => {
it("recognizes the single module-opening question", () => {
const qs = normalizeQuestions([
{
id: MODULE_OPENING_QUESTION_ID,
prompt: "本轮生成对象已钉死。请直接谈…",
allowOther: true,
required: true,
},
]);
expect(isModuleOpeningQuestions(qs)).toBe(true);
});
it("rejects ordinary worker questions", () => {
const qs = normalizeQuestions([
{ id: "q1", prompt: "要不要池?" },
]);
expect(isModuleOpeningQuestions(qs)).toBe(false);
expect(isModuleOpeningQuestions([])).toBe(false);
expect(isModuleOpeningQuestions(null)).toBe(false);
});
});
describe("mergeQuestionsPreferFragment", () => {
it("keeps the 示例 version when askUser repeats the same options", () => {
const ask = normalizeQuestions([
{
prompt: "为贴近你要的质感:你最想反复感受到的是哪一种?",
options: [
"隐于都市的日常从容:所有人都只是凡人,而你心里清楚世界是你的",
"身份揭示的瞬间翻转:前一刻还被人平视甚至冒犯,后一刻万人噤声",
],
},
{
prompt: "在你亮明身份之前,你希望普通世界如何对待你?",
options: [
"基本礼貌但无特别敬畏,偶尔会有微小冒犯",
"因你气质不凡而隐隐客气,但没人知道原因",
],
},
]);
const frag = normalizeQuestions([
{
prompt:
"你最想反复感受到的是哪一种?\n示例比如走在现代街头有人撞了你一下。",
options: [
"隐于都市的日常从容:所有人都只是凡人,而你心里清楚世界是你的",
"身份揭示的瞬间翻转:前一刻还被人平视甚至冒犯,后一刻万人噤声",
],
},
{
prompt:
"在你亮明身份之前,你希望普通世界如何对待你?\n示例这样我才好确定叙事里普通人对你的默认距离。",
options: [
"基本礼貌但无特别敬畏,偶尔会有微小冒犯",
"因你气质不凡而隐隐客气,但没人知道原因",
],
},
]);
const merged = mergeQuestionsPreferFragment(ask, frag);
expect(merged).toHaveLength(2);
expect(merged[0]?.prompt).toContain("示例:");
expect(merged[1]?.prompt).toContain("示例:");
});
it("keeps askUser-only questions that are not in the fragment", () => {
const ask = normalizeQuestions([
{ prompt: "人称?", options: ["第二人称", "第一人称"] },
]);
const frag = normalizeQuestions([
{
prompt: "暴力尺度?\n示例锈迹即可",
options: ["少而锋利", "常态压抑"],
},
]);
const merged = mergeQuestionsPreferFragment(ask, frag);
expect(merged).toHaveLength(2);
expect(merged[0]?.prompt).toContain("暴力尺度");
expect(merged[1]?.prompt).toBe("人称?");
});
});
describe("已问追问去重", () => {
const aesthetics = normalizeQuestions([
{
id: "focus",
prompt: "「永远有 1 万元余额」这个设定,你希望体验更偏向哪种?",
options: [
"用这 1 万元在城市里精打细算地活下去",
"靠这 1 万元撬动关系、机会与影响力",
],
},
]);
it("编排层重抛上一步问过的题时整题丢掉", () => {
const history = appendAskedQuestions([], aesthetics);
const replanned = normalizeQuestions([
{
id: "focus",
prompt: "「永远有 1 万元余额」这个设定,你希望体验更偏向哪种?",
options: [
"用这 1 万元在城市里精打细算地活下去",
"靠这 1 万元撬动关系、机会与影响力",
],
},
]);
expect(dropAlreadyAskedQuestions(replanned, history)).toHaveLength(0);
});
it("新问题照常保留", () => {
const history = appendAskedQuestions([], aesthetics);
const fresh = normalizeQuestions([
{ prompt: "下一步先排实现机制还是叙事指南?", options: ["实现机制"] },
]);
expect(dropAlreadyAskedQuestions(fresh, history)).toHaveLength(1);
});
it("留痕可经 slots 往返,且同题不重复堆积", () => {
const once = appendAskedQuestions([], aesthetics);
const twice = appendAskedQuestions(once, aesthetics);
expect(twice).toHaveLength(1);
const roundTripped = parseAskedQuestions(JSON.stringify(twice));
expect(dropAlreadyAskedQuestions(aesthetics, roundTripped)).toHaveLength(0);
});
});
describe("formatQuestionAnswers", () => {
it("AI payload keeps Q+A; display omits prompts", () => {
const questions = normalizeQuestions([