Files
writing-agent/src/skills/settlement-packet.ts
moranzhi 94f67fa744 引入上下文片段、固定槽与投影排序,并完善机遇裁定与游玩期 UI。
把创作产物收敛为可挂载片段与 play_slots/context_order,同步修订世界模拟器模块与运行时拼装。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-03 01:36:32 +08:00

232 lines
7.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 主世界层 → 叙事转述的裁决包settlement / direction packet
* 存黑板 tag运行.本轮.裁决
*/
import { extractJsonObjectText } from "./worker-set-parse.js";
export const SETTLEMENT_SCHEMA = "settlement.v1" as const;
export const SETTLEMENT_TAG = "运行.本轮.裁决";
export type SettlementNpcMove = {
who: string;
move: string;
wants?: string;
refuses_to_say?: string;
};
export type SettlementVariableChange = {
key: string;
from?: unknown;
to?: unknown;
delta?: number;
note?: string;
};
export type SettlementPacket = {
schema: typeof SETTLEMENT_SCHEMA;
/** 玩家本轮行动摘要 */
player_action?: string;
/** 已落地的客观变化(短句列表) */
resolved?: string[];
/** 转述应呈现的场面现状(玩家安全) */
visible_now?: string;
/** NPC 主动动作binding move */
npc_moves?: SettlementNpcMove[];
/** 真值变更提议Runtime 合并进 变量.当前) */
variable_changes?: SettlementVariableChange[];
/** 禁止写入用户正文的内容 */
do_not_say?: string[];
/** 语气提示(通常来自 Progressive 投影) */
tone_hint?: string;
/** 可选:建议玩家选项(无主语动作短语) */
suggested_actions?: string[];
};
export type SettlementPacketView = {
ok: boolean;
parseError?: string;
packet?: SettlementPacket;
/** 用户友好分节,供 UI 渲染 */
sections: Array<{ title: string; lines: string[] }>;
};
function asString(v: unknown): string | undefined {
if (typeof v !== "string") return undefined;
const t = v.trim();
return t || undefined;
}
function asStringList(v: unknown): string[] {
if (!Array.isArray(v)) return [];
return v.map((x) => asString(x)).filter((x): x is string => Boolean(x));
}
function parseNpcMoves(raw: unknown): SettlementNpcMove[] | undefined {
if (!Array.isArray(raw)) return undefined;
const out: SettlementNpcMove[] = [];
for (const item of raw) {
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
const row = item as Record<string, unknown>;
const who = asString(row.who) ?? asString(row.actor) ?? asString(row.name);
const move = asString(row.move) ?? asString(row.action);
if (!who || !move) continue;
out.push({
who,
move,
wants: asString(row.wants),
refuses_to_say: asString(row.refuses_to_say) ?? asString(row.refusesToSay),
});
}
return out.length ? out : undefined;
}
function parseVariableChanges(
raw: unknown,
): SettlementVariableChange[] | undefined {
if (!Array.isArray(raw)) return undefined;
const out: SettlementVariableChange[] = [];
for (const item of raw) {
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
const row = item as Record<string, unknown>;
const key = asString(row.key) ?? asString(row.name) ?? asString(row.);
if (!key) continue;
const delta =
typeof row.delta === "number" && Number.isFinite(row.delta)
? row.delta
: undefined;
out.push({
key,
from: row.from ?? row.,
to: row.to ?? row. ?? row.value,
delta,
note: asString(row.note) ?? asString(row.),
});
}
return out.length ? out : undefined;
}
export function normalizeSettlementPacket(
row: Record<string, unknown>,
): SettlementPacket {
const resolved = asStringList(row.resolved);
const resolvedZh = asStringList(row.);
const doNot = asStringList(row.do_not_say);
const doNotZh = asStringList(row.);
const suggested = asStringList(row.suggested_actions);
const suggestedZh = asStringList(row.);
return {
schema: SETTLEMENT_SCHEMA,
player_action: asString(row.player_action) ?? asString(row.),
resolved: resolved.length ? resolved : resolvedZh.length ? resolvedZh : undefined,
visible_now: asString(row.visible_now) ?? asString(row.),
npc_moves: parseNpcMoves(row.npc_moves ?? row.npcMoves ?? row.),
variable_changes: parseVariableChanges(
row.variable_changes ?? row.variableChanges ?? row.,
),
do_not_say: doNot.length ? doNot : doNotZh.length ? doNotZh : undefined,
tone_hint: asString(row.tone_hint) ?? asString(row.),
suggested_actions: suggested.length
? suggested
: suggestedZh.length
? suggestedZh
: undefined,
};
}
export function parseSettlementPacket(raw: string | undefined | null): SettlementPacketView {
const text = raw?.trim();
if (!text) {
return { ok: false, parseError: "空裁决包", sections: [] };
}
const jsonText = extractJsonObjectText(text) ?? (text.startsWith("{") ? text : null);
if (!jsonText) {
// 兼容旧散文裁决:整段当作 visible_now
return {
ok: true,
packet: {
schema: SETTLEMENT_SCHEMA,
visible_now: text,
},
sections: [{ title: "场面(未结构化)", lines: [text] }],
};
}
try {
const doc = JSON.parse(jsonText) as unknown;
if (!doc || typeof doc !== "object" || Array.isArray(doc)) {
return { ok: false, parseError: "裁决包不是 JSON 对象", sections: [] };
}
const packet = normalizeSettlementPacket(doc as Record<string, unknown>);
return { ok: true, packet, sections: settlementSections(packet) };
} catch (err) {
return {
ok: false,
parseError: err instanceof Error ? err.message : "裁决包 JSON 解析失败",
sections: [],
};
}
}
export function settlementSections(packet: SettlementPacket): Array<{
title: string;
lines: string[];
}> {
const sections: Array<{ title: string; lines: string[] }> = [];
if (packet.player_action) {
sections.push({ title: "玩家行动", lines: [packet.player_action] });
}
if (packet.resolved?.length) {
sections.push({ title: "已落地", lines: packet.resolved });
}
if (packet.visible_now) {
sections.push({ title: "可见现状", lines: [packet.visible_now] });
}
if (packet.npc_moves?.length) {
sections.push({
title: "角色动作",
lines: packet.npc_moves.map((m) => {
const extra = [m.wants && `想要:${m.wants}`, m.refuses_to_say && `回避:${m.refuses_to_say}`]
.filter(Boolean)
.join("");
return extra ? `${m.who}${m.move}${extra}` : `${m.who}${m.move}`;
}),
});
}
if (packet.variable_changes?.length) {
sections.push({
title: "变量变更",
lines: packet.variable_changes.map((c) => {
if (c.delta != null) {
return `${c.key} ${c.delta >= 0 ? "+" : ""}${c.delta}` + (c.note ? `${c.note}` : "");
}
if (c.from !== undefined || c.to !== undefined) {
return `${c.key}${String(c.from ?? "?")}${String(c.to ?? "?")}`;
}
return c.key;
}),
});
}
if (packet.tone_hint) {
sections.push({ title: "语气", lines: [packet.tone_hint] });
}
if (packet.do_not_say?.length) {
sections.push({ title: "勿写入正文", lines: packet.do_not_say });
}
if (packet.suggested_actions?.length) {
sections.push({ title: "建议行动", lines: packet.suggested_actions });
}
return sections;
}
export function isSettlementLikeObject(doc: unknown): boolean {
if (!doc || typeof doc !== "object" || Array.isArray(doc)) return false;
const row = doc as Record<string, unknown>;
if (row.schema === SETTLEMENT_SCHEMA) return true;
// 启发式:有裁决包典型键
const keys = ["player_action", "visible_now", "resolved", "npc_moves", "variable_changes"];
let hits = 0;
for (const k of keys) if (k in row) hits += 1;
return hits >= 2;
}