cortico-world-simple-trpg-check 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Phantivia
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,38 +1,114 @@
1
- <!-- Owner: src/definition.ts, src/world.ts, src/config.ts -->
2
-
3
- # cortico-world-simple-trpg-check
4
-
5
- `simple_trpg_check_roll` 接收 `scenario` 和 `skill_lists`。`scenario` 写明角色使用技能的方法、目标和相关情境,可写熟练程度或身体条件。模型一次评估所有技能的成功把握;World 将每项 `0–1` 数值乘 100、向下取整,限制在 1–99,回执中称为“难度”。每项技能各掷一次 1–100,按输入顺序返回固定判定文本。
6
-
7
- 1 是大成功,100 是大失败。其余骰值不超过难度的四分之一为极难成功,不超过二分之一为困难成功,不超过难度为普通成功;否则为普通失败。模型不可用或任何一项答案无效时,整次调用返回失败回执,不掷骰。
8
-
9
- ## 判定模型
10
-
11
- `worlds.simple-trpg-check.backend` 可选 `laya-multilingual`(默认)、`laya` 和 `jev`。
12
-
13
- - `laya-multilingual`:在“多语言 Laya Python 环境”下拉选单中选择已安装 `laya` 的 Conda 环境,使用 `convaiinnovations/laya` 的 `multilingual` 检查点。选项来自本机 `~/.conda/environments.txt`,缺失或不可读时只显示当前配置值。首次运行可能由 Laya 下载权重;后续使用本地缓存。
14
- - `laya`:通过可选依赖 `@receptron/laya` 在本机运行英文检查点。
15
- - `jev`:按 `worlds.simple-trpg-check.jevSource` 选择 TypeSafe 或 OpenRouter 的 System One API。工具调用会将 `scenario` 和技能名称发送给所选服务。配置页的“Jev 来源”下方显示“打开密钥文件”按钮;点击后打开部署目录的 `.env`,缺少当前来源密钥时补入空的 `CORTICO_JEV_TYPESAFE_API_KEY=` 或 `CORTICO_JEV_OPENROUTER_API_KEY=` 行。填写并保存文件后,下一次模型请求读取密钥。旧的 World 专用变量名仍可读取。
16
-
17
- World 左栏的状态灯在首次成功完成模型评估后变绿;配置改变或评估失败后更新状态。
18
- “判定模型”下方的“测试连接”会发送一项英文技能评分请求,不掷骰;结果显示在按钮旁,并更新左栏状态灯。
19
-
20
- 同一 Cortico 进程中,若 continuity 与此 World 使用相同的 Laya 类型(multilingual 还需同一 Python 路径),两者共用已加载的模型或 worker。请求依次执行;模型在所有使用者空闲后按其中最长的 TTL 回收。`layaIdleTtlMinutes` 默认 10;设为 `0` 且无其他使用者需要保留模型时,每次评分后释放。不同进程不共用运行实例。
21
-
22
- “强制适配多语言”勾选后,World 提示 agent 用英文填写 `scenario` 和 `skill_lists`;它不翻译输入,也不切换判定模型。
23
-
24
- 环境提示词让 agent 只在结果不确定且具有戏剧性的情境中酌情投骰,并在面向用户的文本中写出每项骰值、难度和结果。
25
-
26
- ## 开发与安装
27
-
28
- ```sh
29
- corepack pnpm install
30
- corepack pnpm typecheck
31
- corepack pnpm typecheck:web
32
- corepack pnpm test
33
- corepack pnpm build:console
34
- ```
35
-
36
- 包内的 `pnpm-workspace.yaml` 已允许 `esbuild` 与 `onnxruntime-node` 的安装脚本。multilingual 模式需要在下拉选单中选择已安装 `laya` 的 Python 环境。
37
-
38
- 在 Cortico 仓库根目录运行 `corepack pnpm check:extension <本包绝对路径>`。然后在目标实例的“扩展”页手动安装此目录,并在 World 配置中启用。安装到运行中的实例需要重启进程;本包的测试和装载检查不会启动 bot。
1
+ <!-- Owner: src/definition.ts, src/world.ts, src/config.ts, src/request.ts, src/readout.ts, src/level-readout.ts, src/console/client.ts -->
2
+
3
+ # cortico-world-simple-trpg-check
4
+
5
+ `cortico-world-simple-trpg-check` 是一个面向 Cortico 的 World 扩展。agent 调用 `simple_trpg_check_roll` 提交角色、情境与逐项技能,插件给出每项技能的难度并逐项掷骰;两个智能体互相比试时改用 `simple_trpg_check_roll_contest`。回执末尾自带面向用户的成品格式,agent 照抄即可。
6
+
7
+ ## 功能
8
+
9
+ - `simple_trpg_check_roll` 接收结构化的 `character`、`situation` 与 `checks`;`situation` 的五个字段加上 `character.condition` 是必填的情境六维,没有影响也要写「无」。
10
+ - 每项技能的难度就是角色在该技能上的水平,也就是这次行动的达成概率百分数;可表达区间是 [1%, 99%]。
11
+ - 1 是大成功,100 是大失败;其余骰值不超过难度的 ¼ 为极难成功、½ 为困难成功、全值为普通成功,否则普通失败。
12
+ - 判定模型只从 `evidence` 读出角色属于哪一档(十档水平表),读出把档位分布映射成难度;运行时只查表,不训练。
13
+ - 情景代偿不进模型:`checks[i].bonus` 是 −2…2 的奖励/惩罚骰合计值(包含本项花掉的 banked 奖励骰),掷 1+|bonus| 颗取极值;`|bonus| ≥ 3` 时直接自动成功/失败,不调用判定模型也不掷骰。`checks[i].grade` 取 1(困难)或 2(极难),只把档位标签整体下压,阈值不动。
14
+ - 顶层 `joint`(`and`/`or`)把一次调用里的多项技能合成一次投掷:难度与奖惩骰都按 `and` 取最小、`or` 取最大。
15
+ - `simple_trpg_check_roll_contest` 对抗检定:两侧各写一份完整请求、各判一次,比成功等级定胜负;同级比难度数值,相等判玩家胜。任一侧失败则整次失败、不掷骰;对抗不触发奖励骰,也不接受 `grade`。
16
+ - 奖励骰的授予、记账与落盘由 agent 在外部工具里完成,本插件只在回执里给建议,不保存任何跨调用状态。
17
+ - 请求按 token 估算设三条上限(总 1000 / 情境 768 / 单问 256,判定时留一成余量);对抗检定把两侧当作两份请求、各自检测。模型不可用、答案无效或超限时返回失败回执,不掷骰。
18
+ - 支持 Jev(TypeSafe / OpenRouter / 自定义)与本地 Laya;控制台提供配置页、模型状态灯和「测试连接」。
19
+
20
+ ## 安装
21
+
22
+ 要求 Node.js 22 或更新版本,以及 Cortico World API 5。
23
+
24
+ 在 Cortico 仓库的 `extensions/` 目录安装:
25
+
26
+ ```powershell
27
+ Set-Location "<Cortico 仓库路径>\extensions"
28
+ corepack pnpm add --ignore-workspace cortico-world-simple-trpg-check
29
+ ```
30
+
31
+ 然后在目标实例的“扩展”页启用本 World,并重启进程。默认判定模型是本地多语言 Laya;实测它读不出角色水平(难度几乎恒定在 60 上下),要拿到可用的难度请在配置页把“判定模型”改成 Jev。
32
+
33
+ ## 判定模型
34
+
35
+ `worlds.simple-trpg-check.backend` 可选 `laya-multilingual`(默认)、`laya` 和 `jev`。
36
+
37
+ - `laya-multilingual`:在“多语言 Laya Python 环境”下拉选单中选择已安装 `laya` 的 Conda 环境,使用 `convaiinnovations/laya` 的 `multilingual` 检查点。选项来自本机 `~/.conda/environments.txt`,缺失或不可读时只显示当前配置值。首次运行可能由 Laya 下载权重;后续使用本地缓存。
38
+ - `laya`:通过可选依赖 `@receptron/laya` 在本机运行英文检查点。
39
+ - `jev`:按 `worlds.simple-trpg-check.jevSource` 选择 TypeSafe、OpenRouter 或自定义 System One API。自定义来源使用 `jevEndpoint` 地址和 `CORTICO_JEV_API_KEY`。工具调用会把序列化成一个 `state.message` 的情境与逐项技能问题发送给所选服务。配置页在“Jev 来源”旁显示密钥状态和“打开密钥文件”按钮;点击后打开部署目录的 `.env`,缺少当前来源密钥时补入空的密钥行。填写并保存文件后,下一次模型请求读取密钥。旧的 World 专用变量名仍可读取。
40
+
41
+ World 左栏的状态灯在首次成功完成模型评估后变绿;配置改变或评估失败后更新状态。
42
+ 配置页顶部的“测试连接”会发送一项英文技能评分请求,不掷骰;结果显示在按钮旁,并更新左栏状态灯。
43
+
44
+ 同一 Cortico 进程中,若 continuity 与此 World 使用相同的 Laya 类型(multilingual 还需同一 Python 路径),两者共用已加载的模型或 worker。请求依次执行;模型在所有使用者空闲后按其中最长的 TTL 回收。`layaIdleTtlMinutes` 默认 10;设为 `0` 且无其他使用者需要保留模型时,每次评分后释放。不同进程不共用运行实例。
45
+
46
+ “强制适配多语言”勾选后,World 提示 agent 用英文填写 `character`、`situation` 与 `checks` 的取值;它不翻译输入,也不切换判定模型。问题文本里技能与目标照抄输入,因此问题语言跟随开关。
47
+
48
+ 环境提示词让 agent 只在结果不确定且具有戏剧性的情境中酌情投骰、在投骰前提醒明显不奏效的方法(不强迫),并把回执末尾的「显示:」块原样拄成面向用户的文本。
49
+
50
+ ## 判定规则
51
+
52
+ `checks` 里的 `evidence` 是角色的水平证据,也是唯一直接决定难度的字段:不写数字与档位名,也不把情境写进去,只写是否以此谋生、训练与实操年限、学历或认证、同行中的相对地位、过往成败。`skill` 用环境提示词里的常用技能表(力量、体质、敏捷、智力…信用),有子分类的写具体子项;表外的技能名也能用。同一调用内多项技能默认是同一时刻的独立检定,顺序只决定回执排列;需要它们共同决定一个 `goal` 时用 `joint`;「先 A 再 B」的链条分两次调用,第二段要重写变化后的六维。
53
+
54
+ 难度是角色水平的百分数。没有代偿时,在 1–99 内成功条件等价于 `骰值 ≤ 难度`,所以 P(成功) = 难度/100 精确成立,大成功与大失败不改变这个概率;钳位不是可选项——允许 0 时掷出的 1 会把概率抬到 1%,允许 100 时掷出的 100 会把它压到 99%。加上代偿后这条不再成立:`grade` 1 把成功率压到约难度/200、`grade` 2 压到约难度/400(阈值不动,只覆盖标签,大成功与大失败免疫);`bonus ≠ 0` 则掷 1+|bonus| 颗取极值,优势取最低、劣势取最高,暴击只判选中的那一颗。
55
+
56
+ ### 情景代偿
57
+
58
+ `bonus` 的标尺是 −2…2(−2 巨大劣势、−1 劣势、0 基准、1 优势、2 巨大优势),优势与劣势 1:1 抵消,agent 只填合计值;超出 ±2 合法,而且是用奖励骰跳过检定的正路。`grade` 只用于「任务本身更难」的场合,它不改变难度数值也不改变 `evidence`,只把显示出来的档位往下压一档(1)或两档(2)。`grade` 与 `bonus` 可以同时用,`grade` 与 `joint` 不能。自动判定(`|bonus| ≥ 3`)压倒一切:不调模型、不掷骰,`grade` 被静默忽略。
59
+
60
+ ### 组合检定
61
+
62
+ `joint: "and"` 要求列表里每项都成功,`joint: "or"` 只要任意一项成功。整组只掷一次骰,按有效难度判:`and` 取最小、`or` 取最大;有效 `bonus` 同构。出现 `joint` 时 `checks` 至少两项,且各项不得带 `grade`。自动判定按有效 `bonus` 判。
63
+
64
+ ### 对抗检定
65
+
66
+ 两侧各写一份完整请求(`actor` / `opponent`,含 `name`、`traits`、`condition`、各自的五维 `situation` 与 `checks`),各发一次判定请求。`bonus` 与 `joint` 挂在侧上,因为每侧只掷一次骰;某侧 `checks` 多于一项时必须给这一侧指定 `joint`。比档序列是 大成功 > 极难成功 > 困难成功 > 普通成功 > 普通失败 > 大失败;同级比难度数值,**数值高者胜**,相等判玩家胜,所以对抗永远有一个赢家。任一侧失败(模型失败、答案无效、超预算)则整次失败且不掷任何骰。某一侧 `|bonus| ≥ 3` 时按序短路定胜负(我方 ≥ 3 → 我方胜;对方 ≤ −3 → 我方胜;我方 ≤ −3 → 对方胜;对方 ≥ 3 → 对方胜),命中时两侧都不调模型、不掷骰,未定调的一侧显示「跳过检定」。对抗不触发奖励骰。
67
+
68
+ ### 回执
69
+
70
+ 每项两行诊断(档位、骰值/难度、一句结果陈述与固定建议),其后追加一个「显示:」块——那就是面向用户的成品格式,标记顺序固定为组合 → 难度 → 奖惩,普通成功与普通失败简写为「成功」「失败」。模型请求失败或输入超限时整次调用失败,回执只有 `技能判定失败:<原因>`,不附显示块。
71
+
72
+ ## 水平读出
73
+
74
+ 模型对每项技能提一个问题:「在技能 X 上,角色处于哪一档」,选项是十档水平描述:六个语义锚点(外行 / 初学者 / 业余者 / 职业=学士 / 专家=硕士博士 / 世界顶尖)相邻之间各插一档,键用中性字母 A–J。World 把回答分布过一遍**水平读出**再四舍五入、限制在 1–99。
75
+
76
+ 读出是离线训练的产物,写在生成文件 `src/level-readout.ts`(一条单调分段线性表,字段与运行时行为见 `src/readout.ts`)。**发布包只带拟合好的读出,运行时不做任何训练**;本地 Laya 的检查点读不出水平证据,保持恒等读出。
77
+
78
+ 配置页在“判定模型”下拉选单下方会提示:本地 Laya 检查点实测读不出角色水平(难度几乎恒定在 60 上下),建议改用 Jev。该提示不阻止选择,Laya 仍可用。
79
+
80
+ 训练与实测留在本仓库的 `evals/`,不随包发布:
81
+
82
+ ```sh
83
+ corepack pnpm eval:train -- --collect # 调 Jev 收集语料,写 evals/dataset-readout-<档位数>.json
84
+ corepack pnpm eval:train -- --fit # 拟合读出,重写 src/level-readout.ts 与报告
85
+ ```
86
+
87
+ 2026-09-28(十档,44 条语料:4 技能 × 5 锚点 + 2 技能 × 12 个细分水平):留一平均绝对误差 恒等 4.0 / 等渗 1.5(选中),全量平均绝对误差 0.33、带符号偏差 0.00。同日端到端 37 条:3 / 62 / 82 三档完全归位,12 与 35 各差 1 点,技能层平均绝对误差 0.25。信用评级沿用技能尺,仍是已知近似(CR 70 → 53、CR 94 → 34)。
88
+
89
+ 读出默认完全采用拟合值;`--shrinkage=<γ>` 可把它往未训练基线收缩(实测更保守的设置误差更大,故默认 γ = 1)。
90
+
91
+ ## 目录结构
92
+
93
+ - `src/`(随包发布):运行时核心;`src/level-readout.ts` 是训练好的读出,属生成文件,不要手改。
94
+ - `python/`(随包发布):多语言 Laya 的 worker。
95
+ - `dist/`(随包发布):控制台面板产物。
96
+ - `evals/`、`tests/`(仅本地):语料、训练与实测脚本、测试,不随包发布。
97
+
98
+ ## 本地开发
99
+
100
+ ```sh
101
+ corepack pnpm install
102
+ corepack pnpm typecheck
103
+ corepack pnpm typecheck:web
104
+ corepack pnpm test
105
+ corepack pnpm build:console
106
+ ```
107
+
108
+ 包内的 `pnpm-workspace.yaml` 已允许 `esbuild` 与 `onnxruntime-node` 的安装脚本。multilingual 模式需要在下拉选单中选择已安装 `laya` 的 Python 环境。
109
+
110
+ `evals/` 下的脚本走 `tsx`,不参与 `vitest`;重量级实测需要 Python 环境或 Jev 密钥,因此不放在 `pnpm test` 里。
111
+
112
+ 在 Cortico 仓库根目录运行 `corepack pnpm check:extension <本包绝对路径>`。安装到运行中的实例需要重启进程;本包的测试和装载检查不会启动 bot。
113
+
114
+ 项目采用 MIT License,详见 [LICENSE](LICENSE)。
package/dist/console.js CHANGED
@@ -1,69 +1,174 @@
1
+ // src/config.ts
2
+ var SIMPLE_TRPG_CHECK_CONFIG_GROUP = {
3
+ id: "world:simple-trpg-check",
4
+ owner: "world:simple-trpg-check",
5
+ schema: {
6
+ type: "object",
7
+ title: "Simple TRPG Check",
8
+ properties: {
9
+ "worlds.simple-trpg-check.backend": {
10
+ type: "string",
11
+ title: "\u5224\u5B9A\u6A21\u578B",
12
+ enum: ["laya-multilingual", "laya", "jev"],
13
+ "x-hot": false
14
+ },
15
+ "worlds.simple-trpg-check.jevSource": {
16
+ type: "string",
17
+ title: "Jev \u6765\u6E90",
18
+ enum: ["typesafe", "openrouter", "custom"],
19
+ "x-hot": true
20
+ },
21
+ "worlds.simple-trpg-check.jevEndpoint": {
22
+ type: "string",
23
+ title: "\u81EA\u5B9A\u4E49 Jev \u670D\u52A1\u5730\u5740",
24
+ "x-hot": true
25
+ },
26
+ "worlds.simple-trpg-check.forceMultilingual": {
27
+ type: "boolean",
28
+ title: "\u5F3A\u5236\u9002\u914D\u591A\u8BED\u8A00",
29
+ description: "\u8981\u6C42 agent \u7528\u82F1\u6587\u586B\u5199 scenario \u4E0E skill_lists\uFF1B\u4E0D\u7FFB\u8BD1\u8F93\u5165\uFF0C\u4E5F\u4E0D\u5207\u6362\u6A21\u578B\u3002",
30
+ "x-hot": true
31
+ },
32
+ "worlds.simple-trpg-check.pythonExecutable": {
33
+ type: "string",
34
+ title: "\u591A\u8BED\u8A00 Laya Python \u73AF\u5883",
35
+ "x-options": "simple-trpg-check-conda-python",
36
+ description: "\u4ECE\u672C\u673A Conda \u73AF\u5883\u4E2D\u9009\u62E9\uFF1B\u8BE5\u73AF\u5883\u9700\u5B89\u88C5 laya\u3002",
37
+ "x-hot": false
38
+ },
39
+ "worlds.simple-trpg-check.layaIdleTtlMinutes": {
40
+ type: "integer",
41
+ title: "Laya \u7A7A\u95F2\u91CA\u653E\u65F6\u95F4",
42
+ minimum: 0,
43
+ multipleOf: 1,
44
+ "x-suffix": "min",
45
+ "x-hot": true,
46
+ description: "\u6700\u540E\u4E00\u6B21\u672C\u5730\u8BC4\u5206\u540E\u4FDD\u7559\u6A21\u578B\u7684\u5206\u949F\u6570\uFF1B\u4E0E\u5176\u4ED6\u4F7F\u7528\u8005\u5171\u7528\u65F6\u53D6\u6700\u957F TTL\u3002"
47
+ }
48
+ }
49
+ }
50
+ };
51
+
1
52
  // src/console/client.ts
2
- async function mountCredentials(ctx) {
53
+ async function mountConfig(ctx) {
54
+ const { ui } = ctx;
55
+ const options = await ctx.invoke("options");
3
56
  let state = await ctx.invoke("state");
4
- render(state);
57
+ let status = "";
58
+ let failed = false;
59
+ render();
5
60
  ctx.interval(() => {
6
61
  void ctx.invoke("state").then((next) => {
7
- if (next.backend !== state.backend || next.source !== state.source || next.keySet !== state.keySet) {
62
+ if (JSON.stringify(next) !== JSON.stringify(state)) {
8
63
  state = next;
9
- render(next);
64
+ render();
10
65
  }
11
66
  }).catch(() => void 0);
12
67
  }, 2e3);
13
- function render(current) {
14
- const { ui } = ctx;
15
- if (current.backend !== "jev") {
16
- ctx.root.replaceChildren();
17
- return;
68
+ async function save(key, value) {
69
+ try {
70
+ state = await ctx.invoke("save", [key, value]);
71
+ status = "\u5DF2\u4FDD\u5B58";
72
+ failed = false;
73
+ } catch (error) {
74
+ status = `\u4FDD\u5B58\u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}`;
75
+ failed = true;
18
76
  }
19
- const sourceName = current.source === "openrouter" ? "OpenRouter" : "TypeSafe";
20
- const message = ui.msgline(current.keySet ? "\u5BC6\u94A5\u5DF2\u914D\u7F6E" : "\u5BC6\u94A5\u672A\u914D\u7F6E");
21
- const open = ui.button(`\u6253\u5F00 ${sourceName} \u5BC6\u94A5\u6587\u4EF6`, {
77
+ render();
78
+ void ctx.refresh().catch(() => void 0);
79
+ }
80
+ function render() {
81
+ const sheet = ui.sheet({ title: "\u914D\u7F6E" });
82
+ const fields = ui.h("div");
83
+ const config = state.config;
84
+ const result = ui.msgline(status, failed);
85
+ const test = ui.button("\u6D4B\u8BD5\u8FDE\u63A5", {
22
86
  onClick: () => {
23
- open.disabled = true;
24
- void ctx.invoke("openKeyFile", [current.source]).then(({ file }) => {
25
- message.textContent = `\u5DF2\u6253\u5F00 ${file}`;
26
- message.classList.remove("bad");
87
+ test.disabled = true;
88
+ result.textContent = "\u6D4B\u8BD5\u4E2D\u2026";
89
+ result.classList.remove("bad");
90
+ void ctx.invoke("testConnection").then((out) => {
91
+ result.textContent = out.ok ? "\u8FDE\u63A5\u6210\u529F" : `\u8FDE\u63A5\u5931\u8D25\uFF1A${out.error || "\u6A21\u578B\u6CA1\u6709\u8FD4\u56DE\u6709\u6548\u7ED3\u679C"}`;
92
+ result.classList.toggle("bad", !out.ok);
93
+ void ctx.refresh().catch(() => void 0);
27
94
  }).catch((error) => {
28
- message.textContent = `\u6253\u5F00\u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}`;
29
- message.classList.add("bad");
95
+ result.textContent = `\u8FDE\u63A5\u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}`;
96
+ result.classList.add("bad");
30
97
  }).finally(() => {
31
- open.disabled = false;
98
+ test.disabled = false;
32
99
  });
33
100
  }
34
101
  });
35
- const actions = ui.actions();
36
- actions.append(message, open);
37
- ctx.root.replaceChildren(actions);
38
- }
39
- }
40
- function mountConnectionTest(ctx) {
41
- const { ui } = ctx;
42
- const message = ui.msgline();
43
- const button = ui.button("\u6D4B\u8BD5\u8FDE\u63A5", {
44
- onClick: () => {
45
- button.disabled = true;
46
- message.textContent = "\u6D4B\u8BD5\u4E2D\u2026";
47
- message.classList.remove("bad");
48
- void ctx.invoke("testConnection").then((result) => {
49
- message.textContent = result.ok ? "\u8FDE\u63A5\u6210\u529F" : `\u8FDE\u63A5\u5931\u8D25\uFF1A${result.error || "\u6A21\u578B\u6CA1\u6709\u8FD4\u56DE\u6709\u6548\u7ED3\u679C"}`;
50
- message.classList.toggle("bad", !result.ok);
51
- void ctx.refresh().catch(() => void 0);
52
- }).catch((error) => {
53
- message.textContent = `\u8FDE\u63A5\u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}`;
54
- message.classList.add("bad");
55
- }).finally(() => {
56
- button.disabled = false;
57
- });
102
+ const testRow = ui.rowbar();
103
+ testRow.append(test, result);
104
+ sheet.body.appendChild(testRow);
105
+ for (const [path, property] of Object.entries(SIMPLE_TRPG_CHECK_CONFIG_GROUP.schema.properties)) {
106
+ const key = path.split(".").at(-1);
107
+ if (config.backend !== "jev" && (key === "jevSource" || key === "jevEndpoint")) continue;
108
+ if (config.backend === "jev" && (key === "forceMultilingual" || key === "pythonExecutable" || key === "layaIdleTtlMinutes")) continue;
109
+ if (key === "jevEndpoint" && config.jevSource !== "custom") continue;
110
+ const value = config[key];
111
+ let field;
112
+ if (property.type === "boolean") {
113
+ field = ui.checkbox(property.title, {
114
+ checked: Boolean(value),
115
+ onChange: (next) => {
116
+ void save(key, next);
117
+ }
118
+ }).el;
119
+ } else if (property.enum || property["x-options"]) {
120
+ const choices = property.enum ?? options.map((item) => item.value);
121
+ const entries = choices.map((choice) => ({
122
+ value: choice,
123
+ label: property["x-options"] ? options.find((item) => item.value === choice)?.label ?? choice : choice
124
+ }));
125
+ if (!entries.some((item) => item.value === value)) entries.unshift({ value: String(value), label: String(value) });
126
+ field = ui.field(property.title, ui.select({
127
+ value: String(value),
128
+ options: entries,
129
+ onChange: (next) => {
130
+ void save(key, next);
131
+ }
132
+ }));
133
+ } else {
134
+ field = ui.field(property.title, ui.input({
135
+ type: property.type === "integer" ? "number" : "text",
136
+ value: String(value),
137
+ onChange: (next) => {
138
+ void save(key, property.type === "integer" ? Number(next) : next);
139
+ }
140
+ }));
141
+ }
142
+ if (key === "jevSource") {
143
+ const sourceName = config.jevSource === "openrouter" ? "OpenRouter" : config.jevSource === "custom" ? "\u81EA\u5B9A\u4E49 Jev" : "TypeSafe";
144
+ const keyStatus = ui.msgline(state.keySet ? "\u5BC6\u94A5\u5DF2\u914D\u7F6E" : "\u5BC6\u94A5\u672A\u914D\u7F6E");
145
+ const open = ui.button(`\u6253\u5F00 ${sourceName} \u5BC6\u94A5\u6587\u4EF6`, {
146
+ onClick: () => {
147
+ open.disabled = true;
148
+ void ctx.invoke("openKeyFile", [config.jevSource]).then(({ file }) => {
149
+ keyStatus.textContent = `\u5DF2\u6253\u5F00 ${file}`;
150
+ }).catch((error) => {
151
+ keyStatus.textContent = `\u6253\u5F00\u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}`;
152
+ keyStatus.classList.add("bad");
153
+ }).finally(() => {
154
+ open.disabled = false;
155
+ });
156
+ }
157
+ });
158
+ const keyRow = ui.rowbar();
159
+ keyRow.append(field, keyStatus, open);
160
+ fields.appendChild(keyRow);
161
+ } else fields.appendChild(field);
162
+ if (key === "backend" && config.backend !== "jev") {
163
+ fields.appendChild(ui.msgline("\u672C\u5730 Laya \u68C0\u67E5\u70B9\u5728\u672C\u57DF\u5B9E\u6D4B\u8BFB\u4E0D\u51FA\u89D2\u8272\u6C34\u5E73\uFF08\u96BE\u5EA6\u51E0\u4E4E\u6052\u5B9A\u5728 60 \u4E0A\u4E0B\uFF09\uFF0C\u5EFA\u8BAE\u6539\u7528 Jev\u3002", true));
164
+ }
165
+ if (property.description) fields.appendChild(ui.msgline(property.description));
58
166
  }
59
- });
60
- const actions = ui.actions();
61
- actions.append(message, button);
62
- ctx.root.replaceChildren(actions);
167
+ sheet.body.appendChild(fields);
168
+ ctx.root.replaceChildren(sheet.el);
169
+ }
63
170
  }
64
- var client = {
65
- panels: { credentials: { mount: mountCredentials }, "connection-test": { mount: mountConnectionTest } }
66
- };
171
+ var client = { panels: { config: { mount: mountConfig } } };
67
172
  var client_default = client;
68
173
  export {
69
174
  client_default as default
package/package.json CHANGED
@@ -1,44 +1,47 @@
1
- {
2
- "name": "cortico-world-simple-trpg-check",
3
- "version": "0.1.1",
4
- "type": "module",
5
- "description": "TRPG skill checks with Laya or Jev probability scores and d100 rolls",
6
- "license": "MIT",
7
- "keywords": [
8
- "cortico-world",
9
- "cortico"
10
- ],
11
- "engines": {
12
- "node": ">=22"
13
- },
14
- "packageManager": "pnpm@11.5.0",
15
- "main": "./src/index.ts",
16
- "cortico": {
17
- "kind": "world",
18
- "api": 5,
19
- "consoleClient": "dist/console.js"
20
- },
21
- "files": [
22
- "src",
23
- "python",
24
- "dist",
25
- "README.md"
26
- ],
27
- "scripts": {
28
- "test": "vitest run",
29
- "typecheck": "tsc --noEmit",
30
- "typecheck:web": "tsc -p tsconfig.web.json --noEmit",
31
- "build:console": "esbuild src/console/client.ts --bundle --format=esm --outfile=dist/console.js"
32
- },
33
- "devDependencies": {
34
- "@types/node": "^22.10.0",
35
- "cortico": "^0.1.3",
36
- "esbuild": "^0.28.2",
37
- "tsx": "^4.19.2",
38
- "typescript": "^5.7.2",
39
- "vitest": "^2.1.8"
40
- },
41
- "optionalDependencies": {
42
- "@receptron/laya": "0.1.2"
43
- }
44
- }
1
+ {
2
+ "name": "cortico-world-simple-trpg-check",
3
+ "version": "0.2.0",
4
+ "type": "module",
5
+ "description": "TRPG skill checks with Laya or Jev probability scores and d100 rolls",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "cortico-world",
9
+ "cortico"
10
+ ],
11
+ "engines": {
12
+ "node": ">=22"
13
+ },
14
+ "packageManager": "pnpm@11.5.0",
15
+ "main": "./src/index.ts",
16
+ "cortico": {
17
+ "kind": "world",
18
+ "api": 5,
19
+ "consoleClient": "dist/console.js"
20
+ },
21
+ "files": [
22
+ "src",
23
+ "python",
24
+ "dist",
25
+ "README.md"
26
+ ],
27
+ "scripts": {
28
+ "test": "vitest run",
29
+ "typecheck": "tsc --noEmit",
30
+ "typecheck:web": "tsc -p tsconfig.web.json --noEmit",
31
+ "build:console": "esbuild src/console/client.ts --bundle --format=esm --outfile=dist/console.js",
32
+ "eval:scenarios": "tsx evals/run.ts",
33
+ "eval:probe": "tsx evals/probe.ts",
34
+ "eval:train": "tsx evals/train.ts"
35
+ },
36
+ "devDependencies": {
37
+ "@types/node": "^22.10.0",
38
+ "cortico": "^0.1.3",
39
+ "esbuild": "^0.28.2",
40
+ "tsx": "^4.19.2",
41
+ "typescript": "^5.7.2",
42
+ "vitest": "^2.1.8"
43
+ },
44
+ "optionalDependencies": {
45
+ "@receptron/laya": "0.1.2"
46
+ }
47
+ }
package/src/ENV_PROMPT.md CHANGED
@@ -1,11 +1,71 @@
1
- 仅在结果不确定且情境具有戏剧性时酌情进行技能判定:平静地走过明亮门廊无需判定;被怪物追赶时穿过遍布瓦砾的狭窄走廊则适合判定。需要判定时调用 simple_trpg_check_roll。
2
-
3
- 调用时自行填写 scenario 和 skill_lists。scenario 要写明角色的目标、如何使用列表中的每项技能,以及影响达成目标的条件,例如环境、熟练程度或身体状况。skill_lists 按判定顺序填写技能名称。
4
-
5
- simple_trpg_check_roll 收到的 state 是 { scenario: "…" },并对 skill_lists 中的每项技能分别提出以下问题,其中 <技能名> 替换为该项名称:
6
- Can the character achieve the stated goal by using the skill "<技能名>" in this scenario? Consider the described method, goal, familiarity, and relevant circumstances.
7
- 填写参数时,确保 scenario 与 skill_lists 自洽,且 scenario 包含回答这些问题所需的条件。
8
-
9
- {{simpleTrpgCheck.languageRule}}
10
-
11
- 根据工具回执继续叙事。面向用户时,按输入顺序用中文技能名列出每项结果,格式为【<技能名>检定:<判定>/<难度> <结果>】;普通成功写“成功”,普通失败写“失败”,其他等级写回执中的结果。例如:【侦查检定:80/40 失败】【灵感检定:20/80 成功】。模型请求失败时依据失败回执处理,不编造骰值。
1
+ 仅在结果不确定且情境具有戏剧性时酌情进行技能判定:平静地走过明亮门廊无需判定;被怪物追赶时穿过遍布瓦砾的狭窄走廊则适合判定。需要判定时调用 simple_trpg_check_roll;两个会推理、会反应的智能体互相比试时,改用 simple_trpg_check_roll_contest。
2
+
3
+ 投骰之前,若你已能判断玩家的方法在此情境下几乎不可能奏效(需要的东西完全不在手边、方法与目标不相干),先提醒玩家可以考虑更换方式;不要强迫,玩家坚持就照投。投过骰之后就不要再提醒,那已成既定事实。
4
+
5
+ 调用时填写 character、situation 与 checks。
6
+ - `character.traits`:长期特质、背景、词条。
7
+ - `character.condition`:伤势、疲劳、体温、精神与消耗状态(如 SAN)。
8
+ - `situation.weather`:天气与介质。`situation.gear`:工具与装备。`situation.info`:已知信息与资料的秩序。`situation.time`:时限。`situation.senses`:可见度与感官条件。
9
+ - `checks`:按顺序列出本时刻要判定的技能。`skill` 是技能名,`goal` 是这项技能要达成的直接结果,`evidence` 是角色在该技能上的水平证据。
10
+
11
+ 技能名可以用下面这套常用表(有子分类的写具体项,例如格斗的「斗殴」「刀」、射击的「手枪」「步枪」、科学的「化学」「生物学」;表外的技能名也可以用,前后保持一致):
12
+
13
+ 力量、体质、敏捷、智力(别名灵感)、意志、教育(别名知识)、说服、取悦、恐吓、欺骗、侦查、聆听、运动、潜行(别名乔装、追踪)、巧手(别名偷窃、伪造、锁匠)、驾驶、骑乘、导航、格斗、射击、闪避、投掷、爆破、急救、医学、科学、神秘学、历史、信用。
14
+
15
+ `situation` 的五个字段加上 `character.condition` 就是情境的全部六个维度,一个都不能省略;某项确实没有影响就写「无」。
16
+
17
+ 难度由角色在该技能上的水平决定:判定模型只从 `evidence` 读出角色属于哪一档,因此 `evidence` 是唯一直接决定难度的字段,必须写得足以把角色定位到某一档。`goal` 与六个维度仍要如实填写——它们是判定记录,也是你决定代偿幅度的依据。
18
+
19
+ 情景不进 `evidence`:顺手的工具、大雨、性格与体型上的优势写进 `bonus`,任务本身的难度写进 `grade`,两者都不改变判定模型看到的水平。
20
+
21
+ ## 情景代偿
22
+
23
+ 情景上的优劣势用 `checks[i].bonus` 表达,标尺是 -2…2:-2 为巨大劣势(若干个劣势叠加,但还不至于让人物不可能完成),-1 为劣势,0 是基准(可省略),1 为优势,2 为巨大优势(若干个优势叠加,但还不至于让检定无需进行)。优势与劣势 1:1 抵消,你把这一时刻的所有因素在心里加总之后,只填**合计值**。
24
+
25
+ 奖励/惩罚骰的门槛比想象中高:它挪动的不是几个百分点,别拿它记小账。一个因素如果轻微到只会让成败概率挪几个百分点,就当作不存在,不要写进 `bonus`。小雨里开车不算惩罚;暴雨里雨刷开到满速仍然几乎看不见,那才算。绝大多数时候只给 ±1;只有情势**极其**有利或极其不利时,才给 ±2。
26
+
27
+ `bonus` 只作用于掷骰:判定模型照旧只回答「角色在这项技能上处于哪一档」,随后掷 1+|bonus| 颗 d100,优势取最低、劣势取最高,再拿选中的那颗去比难度。
28
+
29
+ 合计值超过 ±2 是允许的,而且**鼓励**:假如压上两颗 banked 奖励骰把合计推到 3,这一次检定就直接自动成功,不消耗判定模型、也不掷骰。要让玩家「跳过」一次检定,这是唯一通路。
30
+
31
+ 任务本身的难度用 `checks[i].grade` 表达,只取 1 或 2。它说的是**这件事本身有多难**,与角色强不强无关:1 是连专业人士都要认真对待的挑战,2 是连专家都要吃力的挑战,已经贴近人类可能性的边界。同一件事不会因为角色弱就升档、因为角色强就降档。机制上,1 是困难(困难成功视为普通成功、普通成功视为失败),2 是极难(极难成功视为成功,其余视为失败),大成功与大失败不受影响。`grade` 与 `bonus` 可以同时用,但那属于特例,尽量少用;`grade` 不能与 `joint` 同时出现。
32
+
33
+ ## 奖励骰(账本由你维护)
34
+
35
+ 奖励与惩罚是**你的**职责,插件只给建议:奖励骰的授予、记账与落盘都由你在外部工具里完成,回执不会替你记。
36
+
37
+ - 大成功要发奖励,且**当场落档**。二选一:白得关键信息或有利位置(纯叙事,不进资源),或者一颗奖励骰。
38
+ - 困难成功与极难成功的奖励是**酌情**的:给不给、给多大全看你当场判断,不必落档。仍在上面那两类里挑。
39
+ - 拿到奖励骰就当场记账,写成 `奖励骰:N 即时奖励骰:M`,值有变化时重写一遍。
40
+ - **即时奖励骰**只能在紧接的下一次检定用掉,过期作废;**banked 奖励骰**没有期限,由玩家自己决定何时消耗(你可以提示玩家消耗奖励骰)。因为 `bonus` 是发起调用时一次写定的,同一次调用里后面的技能用不上前面刚发的即时骰——即时骰只对**后续调用**有效。
41
+ - 战斗轮里的大成功按常规反馈、**不给**奖励骰:敌人的失误多给你一次行动、暴击效果拔群、命中弱点。
42
+
43
+ ## 组合检定
44
+
45
+ 如果 `goal` 要同时满足若干个独立事件的达成,用 `joint: "and"`;只要任意一个达成即可,用 `joint: "or"`。组合检定按有效难度判:`and` 取各项难度里最小的、`or` 取最大的;`bonus` 同样按 `and` 取最小、`or` 取最大。整组只掷**一次**骰。出现 `joint` 时 `checks` 至少要两项,且各项都不能带 `grade`。
46
+
47
+ ## 对抗检定
48
+
49
+ 会推理、会反应的智能体之间用 `simple_trpg_check_roll_contest`;无推理、只会被动感知的障碍(犬、传感器、摄像头)不适用,按环境写进 `checks`。近战行动几乎总会触发对抗。
50
+
51
+ 典型配对:力量对力量、格斗对闪避、说服(或其它交涉技能)对意志、潜行对聆听或侦查、巧手对侦查。不必照抄,比例合适即可。
52
+
53
+ `actor` 与 `opponent` 各写完整的一套——`name`、`traits`、`condition`、各自的 `situation` 五维、各自的 `checks`(各自的 `skill`/`goal`/`evidence`)。对抗不接受 `grade`。`bonus` 与 `joint` 挂在**侧**上,因为每侧只掷一次骰;某一侧的 `checks` 多于一项时,必须给这一侧指定 `joint`。
54
+
55
+ 胜负先比成功等级:大成功 > 极难成功 > 困难成功 > 普通成功 > 普通失败 > 大失败;等级相同就比较难度数值,**数值高的一方胜出**,数值相等判玩家胜。换句话说,对抗永远有一个赢家。对抗检定的大成功、极难成功与困难成功都**不触发奖励**。
56
+
57
+ 某一侧的合计 `bonus` 达到 ±3 时直接定胜负:两侧都不判定、不掷骰,回执里未参与结算的一侧显示「跳过检定」。
58
+
59
+ 写作纪律:
60
+
61
+ 1. 情境必须自带全部相关上下文:判定模型只看得到这一次请求,看不到之前的对话。角色的状态、随身物与余量、已知的破法、上一次失败的原因,都要写进对应字段,不能写「同上」。
62
+ 2. 不写数字,也不写档位名(不要出现「技能 65」「职业级」这类字样)。水平只用证据表达:是否以此谋生、训练与实操年限、学历或认证、同行中的相对地位、过往的成败。
63
+ 3. 只写已经确立的事实。若有会影响结果的事实尚未确定(走廊多宽、锁是哪一种),先在叙事里定下来再调用;定不下来就不要调用。
64
+ 4. 有智慧的因素不构成本项难度的修正:人的态度、巡逻、守门走对抗检定,不要写进 `checks`。被动感知型的障碍(犬、传感器、摄像头)按环境写;人造装置算不算对手,由你判断。
65
+ 5. 需要「先 A 再 B」的链条时分两次调用:先投 A;A 成为事实、环境改变之后,第二次调用要把改变后的六个维度重新写一遍,不要复用上一段的文字。
66
+ 6. 同一次调用里的多项技能默认是同一时刻的独立检定,顺序只决定回执的排列,不表示依赖关系;需要它们共同决定一个 `goal` 时改用 `joint`。
67
+ 7. 一场对抗与一个战斗回合都是**一次**调用。回合变化后重新写两侧的六个维度,不要复用上一段的文字。
68
+
69
+ 请求长度按 token 估算(英文字符约 0.3、中文字符约 0.6、其他字符按 1 计,判定时另留一成余量):整份请求不超过 1000、情境不超过 768、单项问题不超过 256。对抗检定把两侧当作两份请求,各自按这三条检测。超出会被拒绝,按回执提示精简后重试。
70
+
71
+ 根据工具回执继续叙事。回执末尾的「显示:」块就是面向用户的成品格式:把里面的每一行**原样照抄**,不要再改写档位名、补数字或调整标点。难度是达成概率的百分数(钳在 1–99),骰值不超过难度才算成功;带 `bonus` 时回执已经列出每一颗骰值并用 `=>` 标出选中那颗,带 `grade` 时回执里的档位名已经下压过。模型请求失败或输入被拒绝时,按回执处理:精简重写后重试,不编造骰值。