Files
writing-agent/src/skills/gm-tools.ts
T
chenmoranzhiandCursor 3d720f23fe 主世界层直接写正文,转述与旁观改读用户展示输出。
去掉裁决包主路径,让无转述时可直出终稿、有转述时再改写镶壳。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-10 02:18:14 +08:00

120 lines
3.9 KiB
TypeScript

/**
* 主世界层 harness 可用工具(游玩期)。
* 仅挂载「本轮裁决缺了就没法写」的程序能力;转述/旁观/调度不在此列。
*/
import type { ToolDefinition } from "../llm/client.js";
import {
executeChanceBatch,
parseChanceBatchRequest,
type ChanceBatchResult,
} from "./chance-tools.js";
export const GM_CHANCE_TOOL_NAME = "chance" as const;
export const GM_TOOL_DEFINITIONS: ToolDefinition[] = [
{
type: "function",
function: {
name: GM_CHANCE_TOOL_NAME,
description:
"批量程序随机:掷骰、比点、抽签/加权抽取。需要多个随机时必须在 requests 数组中一次性全部提交;禁止口算或编造随机结果。",
parameters: {
type: "object",
properties: {
requests: {
type: "array",
description:
"本步所需的全部随机请求;每项必须有唯一 id,供后续 settlement 引用",
items: {
type: "object",
properties: {
id: {
type: "string",
description: "本批内唯一标识,如 attack-roll、loot-pick",
},
op: {
type: "string",
enum: ["roll", "compare", "draw", "pick"],
},
expression: {
type: "string",
description: "roll 时:如 1d20、2d6+3",
},
left: { type: "number", description: "compare 时:左值" },
right: { type: "number", description: "compare 时:右值" },
mode: {
type: "string",
enum: ["gt", "gte", "lt", "lte", "eq"],
},
pool: {
type: "array",
items: { type: "string" },
description: "draw 时:候选池",
},
count: { type: "number" },
unique: { type: "boolean" },
items: {
type: "array",
description: "pick 时:{ id, weight? }[]",
},
reason: { type: "string" },
},
required: ["id", "op"],
},
},
},
required: ["requests"],
additionalProperties: false,
},
},
},
];
export function executeGmChanceTool(argumentsJson: string): ChanceBatchResult {
let args: unknown;
try {
args = JSON.parse(argumentsJson || "{}");
} catch {
return {
schema: "chance.batch.v1",
ok: false,
results: [],
summary: "机遇失败:工具参数不是合法 JSON",
};
}
const items = parseChanceBatchRequest(args);
if (!items?.length) {
return {
schema: "chance.batch.v1",
ok: false,
results: [],
summary: "机遇失败:requests 须为非空数组,且每项含 id 与 op",
};
}
return executeChanceBatch(items);
}
export function handleGmToolCall(
name: string,
argumentsJson: string,
): string | null {
if (name !== GM_CHANCE_TOOL_NAME) return null;
return JSON.stringify(executeGmChanceTool(argumentsJson), null, 2);
}
/** 游玩拓扑 play_slots.chance:启用主世界层机遇工具 harness */
export function gmChanceToolsEnabled(
playSlots: { chance?: boolean } | undefined,
): boolean {
if (!playSlots) return true;
return playSlots.chance !== false;
}
export const GM_CHANCE_HARNESS_INSTRUCTION = `
## 机遇工具(游玩期)
需要真随机(掷骰/比点/抽签/加权抽取)时:调用工具 \`chance\`,在 \`requests\` 数组中**一次性**列出本步全部随机需求(每项必须有唯一 \`id\`)。
禁止口算或编造随机结果。收到工具结果后,在本轮 **Markdown 正文**里演绎对应 \`id\` 的后果(写进场面,不要交 JSON 裁决包)。
无随机需求时:直接输出故事正文,不要调用工具。
`;