放宽游玩正文判定,并允许错误阶段输入后恢复。
减少长稿被误丢,同时让失败会话可继续推进。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -367,8 +367,10 @@ export function getAllowedEvents(
|
||||
"runtime_failed",
|
||||
];
|
||||
case "done":
|
||||
case "error":
|
||||
return [];
|
||||
case "error":
|
||||
// UI 提供「说明后重试」:允许用户发言拉回 running
|
||||
return ["user_submitted_input"];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
@@ -393,6 +395,7 @@ export function canApplyEvent(
|
||||
return reason?.kind === "skill_selection";
|
||||
case "user_submitted_input":
|
||||
return (
|
||||
session.phase === "error" ||
|
||||
reason?.kind === "intake" ||
|
||||
reason?.kind === "input" ||
|
||||
reason?.kind === "worker_questions" ||
|
||||
|
||||
+13
-6
@@ -219,7 +219,7 @@ function existingOutputIsProduct(text: string | undefined): boolean {
|
||||
|
||||
function usablePlayFallbackText(raw?: string): string {
|
||||
if (!raw?.trim()) return "";
|
||||
if (isQuestionOnlyText(raw)) return "";
|
||||
if (isPlayMetaAsk(raw)) return "";
|
||||
const parsed = tryParseJsonDoc(raw);
|
||||
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
||||
return "";
|
||||
@@ -348,16 +348,26 @@ function looksLikeStructuredDraft(text: string): boolean {
|
||||
return Boolean(extractJsonObjectText(t));
|
||||
}
|
||||
|
||||
/** 几乎只有提问、没有稿。 */
|
||||
/** 几乎只有提问、没有稿。对话里夹问句不算。 */
|
||||
function isQuestionOnlyText(text: string): boolean {
|
||||
const t = text.trim();
|
||||
if (!t) return true;
|
||||
if (looksLikeStructuredDraft(t)) return false;
|
||||
const qs = extractQuestionsFromText(t);
|
||||
if (!qs.length) return false;
|
||||
const qChars = qs.join("").length;
|
||||
if (t.length >= 80 && qChars <= t.length * 0.55) return false;
|
||||
return looksLikeProseNotSpec(t) || /你的选择|请(?:描述|选择|确认|补充)/.test(t);
|
||||
}
|
||||
|
||||
/** 游玩正文:只把短追问口吻当成没写完,不因场面里有「?」丢掉整稿。 */
|
||||
function isPlayMetaAsk(text: string): boolean {
|
||||
const t = text.trim();
|
||||
if (!t) return true;
|
||||
if (t.length > 280) return false;
|
||||
return /可验收|追问|请再补|askUser/.test(t);
|
||||
}
|
||||
|
||||
function canonicalizeKnownOutputs(outputs: Record<string, string>): void {
|
||||
const raw = outputs[CONTEXT_ORDER_TAG];
|
||||
if (!raw?.trim()) return;
|
||||
@@ -408,10 +418,7 @@ function parseWorkerResponse(
|
||||
if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||
const target = playTarget ?? productOutputTags(outputTags)[0] ?? outputTags[0];
|
||||
if (playTarget) {
|
||||
const body =
|
||||
raw.trim() && !isQuestionOnlyText(raw)
|
||||
? raw
|
||||
: "";
|
||||
const body = raw.trim() && !isPlayMetaAsk(raw) ? raw : "";
|
||||
return finalizeParsedOutputs(
|
||||
{ [playTarget]: packBody(body) },
|
||||
{ summary: body.slice(0, 80) || "游玩正文" },
|
||||
|
||||
@@ -337,6 +337,18 @@ describe("phase runtime", () => {
|
||||
expect(runtime.getSession().waitingReason).toBeUndefined();
|
||||
});
|
||||
|
||||
it("submitInput recovers from error phase", async () => {
|
||||
const runtime = new PhaseRuntime({ autoStubWorker: true });
|
||||
await runtime.start();
|
||||
await runtime.submitInput("随便写点");
|
||||
await runtime.failRun("fetch failed");
|
||||
expect(runtime.getSession().phase).toBe("error");
|
||||
|
||||
await runtime.submitInput("重试一下");
|
||||
expect(runtime.getSession().phase).not.toBe("error");
|
||||
expect(runtime.getSession().slots.lastUserInput).toBe("重试一下");
|
||||
});
|
||||
|
||||
it("recoverOrphanedRun turns stuck running into node pick when DAG exists", async () => {
|
||||
const runtime = new PhaseRuntime();
|
||||
await runtime.start();
|
||||
|
||||
@@ -445,4 +445,27 @@ describe("sanitizeWorkerSetOutputs", () => {
|
||||
const view = parsePresentPacket(result.outputs["输出.用户展示"], "prose");
|
||||
expect(view.packet.shell).toBe("turn_panel");
|
||||
});
|
||||
|
||||
it("play gm body: keeps a scene that contains dialogue questions", () => {
|
||||
const scene = `神代流把耳机摘下,皱着眉。
|
||||
|
||||
「你在干什么?」她问。与他刚进监控室时不同,教练的吼声还在耳边。
|
||||
|
||||
只要玩家公开跪下认错,她就会放行。`;
|
||||
const result = parseWorkerResponseForTest(scene, ["输出.用户展示"], {
|
||||
preferPlainBody: true,
|
||||
});
|
||||
expect(result.askUser).toBeUndefined();
|
||||
expect(result.outputs["输出.用户展示"]).toContain("你在干什么");
|
||||
expect(result.outputs["输出.用户展示"]).not.toBe("(本轮场面未写完)");
|
||||
});
|
||||
|
||||
it("play gm body: short ask-only text still counts as unfinished", () => {
|
||||
const result = parseWorkerResponseForTest(
|
||||
"这一步还没写出可验收的产物。请再补一点你最在意的体验。",
|
||||
["输出.用户展示"],
|
||||
{ preferPlainBody: true },
|
||||
);
|
||||
expect(result.outputs["输出.用户展示"]).toBe("(本轮场面未写完)");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user