cortico-world-simple-trpg-check 0.1.1 → 0.1.3
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 +21 -0
- package/README.md +3 -3
- package/dist/console.js +151 -49
- package/package.json +1 -1
- package/src/config.ts +10 -4
- package/src/console/client.ts +96 -54
- package/src/model.ts +14 -10
- package/src/world.ts +31 -21
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,4 +1,4 @@
|
|
|
1
|
-
<!-- Owner: src/definition.ts, src/world.ts, src/config.ts -->
|
|
1
|
+
<!-- Owner: src/definition.ts, src/world.ts, src/config.ts, src/console/client.ts -->
|
|
2
2
|
|
|
3
3
|
# cortico-world-simple-trpg-check
|
|
4
4
|
|
|
@@ -12,10 +12,10 @@
|
|
|
12
12
|
|
|
13
13
|
- `laya-multilingual`:在“多语言 Laya Python 环境”下拉选单中选择已安装 `laya` 的 Conda 环境,使用 `convaiinnovations/laya` 的 `multilingual` 检查点。选项来自本机 `~/.conda/environments.txt`,缺失或不可读时只显示当前配置值。首次运行可能由 Laya 下载权重;后续使用本地缓存。
|
|
14
14
|
- `laya`:通过可选依赖 `@receptron/laya` 在本机运行英文检查点。
|
|
15
|
-
- `jev`:按 `worlds.simple-trpg-check.jevSource` 选择 TypeSafe
|
|
15
|
+
- `jev`:按 `worlds.simple-trpg-check.jevSource` 选择 TypeSafe、OpenRouter 或自定义 System One API。自定义来源使用 `jevEndpoint` 地址和 `CORTICO_JEV_API_KEY`。工具调用会将 `scenario` 和技能名称发送给所选服务。配置页在“Jev 来源”旁显示密钥状态和“打开密钥文件”按钮;点击后打开部署目录的 `.env`,缺少当前来源密钥时补入空的密钥行。填写并保存文件后,下一次模型请求读取密钥。旧的 World 专用变量名仍可读取。
|
|
16
16
|
|
|
17
17
|
World 左栏的状态灯在首次成功完成模型评估后变绿;配置改变或评估失败后更新状态。
|
|
18
|
-
|
|
18
|
+
配置页顶部的“测试连接”会发送一项英文技能评分请求,不掷骰;结果显示在按钮旁,并更新左栏状态灯。
|
|
19
19
|
|
|
20
20
|
同一 Cortico 进程中,若 continuity 与此 World 使用相同的 Laya 类型(multilingual 还需同一 Python 路径),两者共用已加载的模型或 worker。请求依次执行;模型在所有使用者空闲后按其中最长的 TTL 回收。`layaIdleTtlMinutes` 默认 10;设为 `0` 且无其他使用者需要保留模型时,每次评分后释放。不同进程不共用运行实例。
|
|
21
21
|
|
package/dist/console.js
CHANGED
|
@@ -1,69 +1,171 @@
|
|
|
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
|
|
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
|
-
|
|
57
|
+
let status = "";
|
|
58
|
+
let failed = false;
|
|
59
|
+
render();
|
|
5
60
|
ctx.interval(() => {
|
|
6
61
|
void ctx.invoke("state").then((next) => {
|
|
7
|
-
if (
|
|
62
|
+
if (JSON.stringify(next) !== JSON.stringify(state)) {
|
|
8
63
|
state = next;
|
|
9
|
-
render(
|
|
64
|
+
render();
|
|
10
65
|
}
|
|
11
66
|
}).catch(() => void 0);
|
|
12
67
|
}, 2e3);
|
|
13
|
-
function
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
29
|
-
|
|
95
|
+
result.textContent = `\u8FDE\u63A5\u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}`;
|
|
96
|
+
result.classList.add("bad");
|
|
30
97
|
}).finally(() => {
|
|
31
|
-
|
|
98
|
+
test.disabled = false;
|
|
32
99
|
});
|
|
33
100
|
}
|
|
34
101
|
});
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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 (property.description) fields.appendChild(ui.msgline(property.description));
|
|
58
163
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
ctx.root.replaceChildren(actions);
|
|
164
|
+
sheet.body.appendChild(fields);
|
|
165
|
+
ctx.root.replaceChildren(sheet.el);
|
|
166
|
+
}
|
|
63
167
|
}
|
|
64
|
-
var client = {
|
|
65
|
-
panels: { credentials: { mount: mountCredentials }, "connection-test": { mount: mountConnectionTest } }
|
|
66
|
-
};
|
|
168
|
+
var client = { panels: { config: { mount: mountConfig } } };
|
|
67
169
|
var client_default = client;
|
|
68
170
|
export {
|
|
69
171
|
client_default as default
|
package/package.json
CHANGED
package/src/config.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import type { ConfigGroup } from 'cortico/core/types.ts';
|
|
2
2
|
|
|
3
3
|
export type CheckBackend = 'laya' | 'laya-multilingual' | 'jev';
|
|
4
|
-
export type JevSource = 'typesafe' | 'openrouter';
|
|
4
|
+
export type JevSource = 'typesafe' | 'openrouter' | 'custom';
|
|
5
5
|
|
|
6
6
|
export interface SimpleTrpgCheckConfigSection {
|
|
7
7
|
enabled: boolean;
|
|
8
8
|
backend: CheckBackend;
|
|
9
9
|
jevSource: JevSource;
|
|
10
|
+
jevEndpoint: string;
|
|
10
11
|
forceMultilingual: boolean;
|
|
11
12
|
pythonExecutable: string;
|
|
12
13
|
layaIdleTtlMinutes: number;
|
|
@@ -16,6 +17,7 @@ export const SIMPLE_TRPG_CHECK_DEFAULTS: SimpleTrpgCheckConfigSection = {
|
|
|
16
17
|
enabled: false,
|
|
17
18
|
backend: 'laya-multilingual',
|
|
18
19
|
jevSource: 'typesafe',
|
|
20
|
+
jevEndpoint: '',
|
|
19
21
|
forceMultilingual: false,
|
|
20
22
|
pythonExecutable: 'python',
|
|
21
23
|
layaIdleTtlMinutes: 10,
|
|
@@ -23,6 +25,7 @@ export const SIMPLE_TRPG_CHECK_DEFAULTS: SimpleTrpgCheckConfigSection = {
|
|
|
23
25
|
|
|
24
26
|
export const SIMPLE_TRPG_CHECK_TYPESAFE_SECRET = 'CORTICO_JEV_TYPESAFE_API_KEY';
|
|
25
27
|
export const SIMPLE_TRPG_CHECK_OPENROUTER_SECRET = 'CORTICO_JEV_OPENROUTER_API_KEY';
|
|
28
|
+
export const SIMPLE_TRPG_CHECK_CUSTOM_SECRET = 'CORTICO_JEV_API_KEY';
|
|
26
29
|
export const SIMPLE_TRPG_CHECK_LEGACY_TYPESAFE_SECRET = 'CORTICO_SIMPLE_TRPG_CHECK_TYPESAFE_API_KEY';
|
|
27
30
|
export const SIMPLE_TRPG_CHECK_LEGACY_OPENROUTER_SECRET = 'CORTICO_SIMPLE_TRPG_CHECK_OPENROUTER_API_KEY';
|
|
28
31
|
|
|
@@ -38,14 +41,17 @@ export const SIMPLE_TRPG_CHECK_CONFIG_GROUP: ConfigGroup = {
|
|
|
38
41
|
title: '判定模型',
|
|
39
42
|
enum: ['laya-multilingual', 'laya', 'jev'],
|
|
40
43
|
'x-hot': false,
|
|
41
|
-
'x-panel-slot': 'model-test',
|
|
42
44
|
},
|
|
43
45
|
'worlds.simple-trpg-check.jevSource': {
|
|
44
46
|
type: 'string',
|
|
45
47
|
title: 'Jev 来源',
|
|
46
|
-
enum: ['typesafe', 'openrouter'],
|
|
48
|
+
enum: ['typesafe', 'openrouter', 'custom'],
|
|
49
|
+
'x-hot': true,
|
|
50
|
+
},
|
|
51
|
+
'worlds.simple-trpg-check.jevEndpoint': {
|
|
52
|
+
type: 'string',
|
|
53
|
+
title: '自定义 Jev 服务地址',
|
|
47
54
|
'x-hot': true,
|
|
48
|
-
'x-panel-slot': 'jev-key',
|
|
49
55
|
},
|
|
50
56
|
'worlds.simple-trpg-check.forceMultilingual': {
|
|
51
57
|
type: 'boolean',
|
package/src/console/client.ts
CHANGED
|
@@ -1,74 +1,116 @@
|
|
|
1
1
|
import type { ConsoleClientBundle, ConsolePanelContext } from 'cortico/web/shared/client-panel.ts';
|
|
2
|
+
import { SIMPLE_TRPG_CHECK_CONFIG_GROUP, type SimpleTrpgCheckConfigSection } from '../config.ts';
|
|
2
3
|
|
|
3
|
-
interface
|
|
4
|
-
|
|
5
|
-
source: 'typesafe' | 'openrouter';
|
|
4
|
+
interface ConfigState {
|
|
5
|
+
config: SimpleTrpgCheckConfigSection;
|
|
6
6
|
keySet: boolean;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
async function
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
async function mountConfig(ctx: ConsolePanelContext): Promise<void> {
|
|
10
|
+
const { ui } = ctx;
|
|
11
|
+
const options = await ctx.invoke<Array<{ value: string; label: string }>>('options');
|
|
12
|
+
let state = await ctx.invoke<ConfigState>('state');
|
|
13
|
+
let status = '';
|
|
14
|
+
let failed = false;
|
|
15
|
+
render();
|
|
12
16
|
ctx.interval(() => {
|
|
13
|
-
void ctx.invoke<
|
|
14
|
-
if (
|
|
17
|
+
void ctx.invoke<ConfigState>('state').then((next) => {
|
|
18
|
+
if (JSON.stringify(next) !== JSON.stringify(state)) {
|
|
15
19
|
state = next;
|
|
16
|
-
render(
|
|
20
|
+
render();
|
|
17
21
|
}
|
|
18
22
|
}).catch(() => undefined);
|
|
19
23
|
}, 2_000);
|
|
20
24
|
|
|
21
|
-
function
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
async function save(key: keyof SimpleTrpgCheckConfigSection, value: string | number | boolean): Promise<void> {
|
|
26
|
+
try {
|
|
27
|
+
state = await ctx.invoke<ConfigState>('save', [key, value]);
|
|
28
|
+
status = '已保存';
|
|
29
|
+
failed = false;
|
|
30
|
+
} catch (error) {
|
|
31
|
+
status = `保存失败:${error instanceof Error ? error.message : String(error)}`;
|
|
32
|
+
failed = true;
|
|
26
33
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
34
|
+
render();
|
|
35
|
+
void ctx.refresh().catch(() => undefined);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function render(): void {
|
|
39
|
+
const sheet = ui.sheet({ title: '配置' });
|
|
40
|
+
const fields = ui.h('div');
|
|
41
|
+
const config = state.config;
|
|
42
|
+
const result = ui.msgline(status, failed);
|
|
43
|
+
const test = ui.button('测试连接', {
|
|
30
44
|
onClick: () => {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
45
|
+
test.disabled = true;
|
|
46
|
+
result.textContent = '测试中…';
|
|
47
|
+
result.classList.remove('bad');
|
|
48
|
+
void ctx.invoke<{ ok: boolean; error?: string }>('testConnection').then((out) => {
|
|
49
|
+
result.textContent = out.ok ? '连接成功' : `连接失败:${out.error || '模型没有返回有效结果'}`;
|
|
50
|
+
result.classList.toggle('bad', !out.ok);
|
|
51
|
+
void ctx.refresh().catch(() => undefined);
|
|
35
52
|
}).catch((error) => {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}).finally(() => {
|
|
53
|
+
result.textContent = `连接失败:${error instanceof Error ? error.message : String(error)}`;
|
|
54
|
+
result.classList.add('bad');
|
|
55
|
+
}).finally(() => { test.disabled = false; });
|
|
39
56
|
},
|
|
40
57
|
});
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
58
|
+
const testRow = ui.rowbar();
|
|
59
|
+
testRow.append(test, result);
|
|
60
|
+
sheet.body.appendChild(testRow);
|
|
61
|
+
for (const [path, property] of Object.entries(SIMPLE_TRPG_CHECK_CONFIG_GROUP.schema.properties)) {
|
|
62
|
+
const key = path.split('.').at(-1) as keyof SimpleTrpgCheckConfigSection;
|
|
63
|
+
if (config.backend !== 'jev' && (key === 'jevSource' || key === 'jevEndpoint')) continue;
|
|
64
|
+
if (config.backend === 'jev' && (key === 'forceMultilingual' || key === 'pythonExecutable' || key === 'layaIdleTtlMinutes')) continue;
|
|
65
|
+
if (key === 'jevEndpoint' && config.jevSource !== 'custom') continue;
|
|
66
|
+
const value = config[key];
|
|
67
|
+
let field: HTMLElement;
|
|
68
|
+
if (property.type === 'boolean') {
|
|
69
|
+
field = ui.checkbox(property.title, {
|
|
70
|
+
checked: Boolean(value), onChange: (next) => { void save(key, next); },
|
|
71
|
+
}).el;
|
|
72
|
+
} else if (property.enum || property['x-options']) {
|
|
73
|
+
const choices = property.enum ?? options.map((item) => item.value);
|
|
74
|
+
const entries = choices.map((choice) => ({
|
|
75
|
+
value: choice,
|
|
76
|
+
label: property['x-options'] ? options.find((item) => item.value === choice)?.label ?? choice : choice,
|
|
77
|
+
}));
|
|
78
|
+
if (!entries.some((item) => item.value === value)) entries.unshift({ value: String(value), label: String(value) });
|
|
79
|
+
field = ui.field(property.title, ui.select({
|
|
80
|
+
value: String(value), options: entries, onChange: (next) => { void save(key, next); },
|
|
81
|
+
}));
|
|
82
|
+
} else {
|
|
83
|
+
field = ui.field(property.title, ui.input({
|
|
84
|
+
type: property.type === 'integer' ? 'number' : 'text',
|
|
85
|
+
value: String(value),
|
|
86
|
+
onChange: (next) => { void save(key, property.type === 'integer' ? Number(next) : next); },
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
if (key === 'jevSource') {
|
|
90
|
+
const sourceName = config.jevSource === 'openrouter' ? 'OpenRouter' : config.jevSource === 'custom' ? '自定义 Jev' : 'TypeSafe';
|
|
91
|
+
const keyStatus = ui.msgline(state.keySet ? '密钥已配置' : '密钥未配置');
|
|
92
|
+
const open = ui.button(`打开 ${sourceName} 密钥文件`, {
|
|
93
|
+
onClick: () => {
|
|
94
|
+
open.disabled = true;
|
|
95
|
+
void ctx.invoke<{ file: string }>('openKeyFile', [config.jevSource]).then(({ file }) => {
|
|
96
|
+
keyStatus.textContent = `已打开 ${file}`;
|
|
97
|
+
}).catch((error) => {
|
|
98
|
+
keyStatus.textContent = `打开失败:${error instanceof Error ? error.message : String(error)}`;
|
|
99
|
+
keyStatus.classList.add('bad');
|
|
100
|
+
}).finally(() => { open.disabled = false; });
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
const keyRow = ui.rowbar();
|
|
104
|
+
keyRow.append(field, keyStatus, open);
|
|
105
|
+
fields.appendChild(keyRow);
|
|
106
|
+
} else fields.appendChild(field);
|
|
107
|
+
if (property.description) fields.appendChild(ui.msgline(property.description));
|
|
108
|
+
}
|
|
109
|
+
sheet.body.appendChild(fields);
|
|
46
110
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const message = ui.msgline();
|
|
50
|
-
const button = ui.button('测试连接', {
|
|
51
|
-
onClick: () => {
|
|
52
|
-
button.disabled = true;
|
|
53
|
-
message.textContent = '测试中…';
|
|
54
|
-
message.classList.remove('bad');
|
|
55
|
-
void ctx.invoke<{ ok: boolean; error?: string }>('testConnection').then((result) => {
|
|
56
|
-
message.textContent = result.ok ? '连接成功' : `连接失败:${result.error || '模型没有返回有效结果'}`;
|
|
57
|
-
message.classList.toggle('bad', !result.ok);
|
|
58
|
-
void ctx.refresh().catch(() => undefined);
|
|
59
|
-
}).catch((error) => {
|
|
60
|
-
message.textContent = `连接失败:${error instanceof Error ? error.message : String(error)}`;
|
|
61
|
-
message.classList.add('bad');
|
|
62
|
-
}).finally(() => { button.disabled = false; });
|
|
63
|
-
},
|
|
64
|
-
});
|
|
65
|
-
const actions = ui.actions();
|
|
66
|
-
actions.append(message, button);
|
|
67
|
-
ctx.root.replaceChildren(actions);
|
|
111
|
+
ctx.root.replaceChildren(sheet.el);
|
|
112
|
+
}
|
|
68
113
|
}
|
|
69
114
|
|
|
70
|
-
const client: ConsoleClientBundle = {
|
|
71
|
-
panels: { credentials: { mount: mountCredentials }, 'connection-test': { mount: mountConnectionTest } },
|
|
72
|
-
};
|
|
73
|
-
|
|
115
|
+
const client: ConsoleClientBundle = { panels: { config: { mount: mountConfig } } };
|
|
74
116
|
export default client;
|
package/src/model.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { WorldContext } from 'cortico/world.ts';
|
|
2
2
|
import {
|
|
3
3
|
SIMPLE_TRPG_CHECK_OPENROUTER_SECRET,
|
|
4
|
+
SIMPLE_TRPG_CHECK_CUSTOM_SECRET,
|
|
4
5
|
SIMPLE_TRPG_CHECK_TYPESAFE_SECRET,
|
|
5
6
|
SIMPLE_TRPG_CHECK_LEGACY_OPENROUTER_SECRET,
|
|
6
7
|
SIMPLE_TRPG_CHECK_LEGACY_TYPESAFE_SECRET,
|
|
@@ -84,16 +85,19 @@ export class SystemOneSkillScorer implements SkillScorer {
|
|
|
84
85
|
|
|
85
86
|
private async requestJev(state: unknown, questions: Questions): Promise<unknown> {
|
|
86
87
|
const source = this.ctx.cfg.jevSource;
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
:
|
|
88
|
+
const apiKey = source === 'custom' ? this.ctx.secret(SIMPLE_TRPG_CHECK_CUSTOM_SECRET)
|
|
89
|
+
: source === 'openrouter'
|
|
90
|
+
? this.ctx.secret(SIMPLE_TRPG_CHECK_OPENROUTER_SECRET) || this.ctx.secret(SIMPLE_TRPG_CHECK_LEGACY_OPENROUTER_SECRET)
|
|
91
|
+
: this.ctx.secret(SIMPLE_TRPG_CHECK_TYPESAFE_SECRET) || this.ctx.secret(SIMPLE_TRPG_CHECK_LEGACY_TYPESAFE_SECRET)
|
|
92
|
+
|| this.ctx.secret(SIMPLE_TRPG_CHECK_CUSTOM_SECRET);
|
|
93
|
+
if (!apiKey) throw new Error(`${source === 'openrouter' ? 'OpenRouter' : source === 'custom' ? '自定义 Jev' : 'TypeSafe'} API key 未配置`);
|
|
94
|
+
const endpoint = source === 'openrouter' ? 'https://openrouter.ai/api/alpha/decisions'
|
|
95
|
+
: source === 'custom' ? this.ctx.cfg.jevEndpoint.trim() : 'https://api.typesafe.ai/v1/systemone';
|
|
96
|
+
if (source === 'custom') {
|
|
97
|
+
let url: URL;
|
|
98
|
+
try { url = new URL(endpoint); } catch { throw new Error('自定义 Jev 服务地址无效'); }
|
|
99
|
+
if (!['http:', 'https:'].includes(url.protocol)) throw new Error('自定义 Jev 服务地址无效');
|
|
100
|
+
}
|
|
97
101
|
const response = await fetch(endpoint, {
|
|
98
102
|
method: 'POST',
|
|
99
103
|
headers: { authorization: `Bearer ${apiKey}`, 'content-type': 'application/json' },
|
package/src/world.ts
CHANGED
|
@@ -6,12 +6,14 @@ import { classify, difficultyFromProbability, formatChecks } from './check.ts';
|
|
|
6
6
|
import {
|
|
7
7
|
SIMPLE_TRPG_CHECK_CONFIG_GROUP,
|
|
8
8
|
SIMPLE_TRPG_CHECK_OPENROUTER_SECRET,
|
|
9
|
+
SIMPLE_TRPG_CHECK_CUSTOM_SECRET,
|
|
9
10
|
SIMPLE_TRPG_CHECK_TYPESAFE_SECRET,
|
|
10
11
|
SIMPLE_TRPG_CHECK_LEGACY_OPENROUTER_SECRET,
|
|
11
12
|
SIMPLE_TRPG_CHECK_LEGACY_TYPESAFE_SECRET,
|
|
12
13
|
type SimpleTrpgCheckConfigSection,
|
|
13
14
|
} from './config.ts';
|
|
14
15
|
import { SystemOneSkillScorer, type SkillScorer } from './model.ts';
|
|
16
|
+
import { discoverCondaPythonOptions } from './conda-environments.ts';
|
|
15
17
|
import { ensureSecretPlaceholder, openSecretFile } from './secret-file.ts';
|
|
16
18
|
|
|
17
19
|
const ENV_PROMPT_FILE = fileURLToPath(new URL('./ENV_PROMPT.md', import.meta.url));
|
|
@@ -25,11 +27,11 @@ export class SimpleTrpgCheckWorld implements World {
|
|
|
25
27
|
|
|
26
28
|
private hasJevKey(): boolean {
|
|
27
29
|
const source = this.ctx.cfg.jevSource;
|
|
28
|
-
return !!(this.ctx.secret(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
return !!(source === 'custom' ? this.ctx.secret(SIMPLE_TRPG_CHECK_CUSTOM_SECRET)
|
|
31
|
+
: source === 'openrouter'
|
|
32
|
+
? this.ctx.secret(SIMPLE_TRPG_CHECK_OPENROUTER_SECRET) || this.ctx.secret(SIMPLE_TRPG_CHECK_LEGACY_OPENROUTER_SECRET)
|
|
33
|
+
: this.ctx.secret(SIMPLE_TRPG_CHECK_TYPESAFE_SECRET) || this.ctx.secret(SIMPLE_TRPG_CHECK_LEGACY_TYPESAFE_SECRET)
|
|
34
|
+
|| this.ctx.secret(SIMPLE_TRPG_CHECK_CUSTOM_SECRET));
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
constructor(
|
|
@@ -82,7 +84,6 @@ export class SimpleTrpgCheckWorld implements World {
|
|
|
82
84
|
const signature = this.currentModelSignature();
|
|
83
85
|
const modelState = signature === this.modelSignature ? this.modelState : 'offline';
|
|
84
86
|
return {
|
|
85
|
-
config: [SIMPLE_TRPG_CHECK_CONFIG_GROUP],
|
|
86
87
|
lamps: [{
|
|
87
88
|
label: '判定模型',
|
|
88
89
|
state: modelState,
|
|
@@ -90,14 +91,28 @@ export class SimpleTrpgCheckWorld implements World {
|
|
|
90
91
|
? '最近一次模型请求失败' : modelState === 'loading' ? '正在请求模型' : '尚未成功请求模型',
|
|
91
92
|
}],
|
|
92
93
|
badges: this.ctx.cfg.backend === 'jev'
|
|
93
|
-
? [{ label: source === 'openrouter' ? 'OpenRouter' : 'TypeSafe', value: keySet ? '密钥已配置' : '密钥未配置', tone: keySet ? 'on' : 'off' }]
|
|
94
|
+
? [{ label: source === 'openrouter' ? 'OpenRouter' : source === 'custom' ? '自定义 Jev' : 'TypeSafe', value: keySet ? '密钥已配置' : '密钥未配置', tone: keySet ? 'on' : 'off' }]
|
|
94
95
|
: [{ label: '判定模型', value: this.ctx.cfg.backend }],
|
|
95
|
-
panels: [
|
|
96
|
-
{ id: 'connection-test', title: '测试连接', slot: 'model-test' },
|
|
97
|
-
{ id: 'credentials', title: 'Jev 密钥', slot: 'jev-key' },
|
|
98
|
-
] as unknown as WorldConsoleDecl['panels'],
|
|
96
|
+
panels: [{ id: 'config', title: '配置' }],
|
|
99
97
|
invoke: async (panel, method, args) => {
|
|
100
|
-
if (panel
|
|
98
|
+
if (panel !== 'config') throw new Error('未知面板');
|
|
99
|
+
if (method === 'state') {
|
|
100
|
+
return { config: { ...this.ctx.cfg }, keySet: this.hasJevKey() };
|
|
101
|
+
}
|
|
102
|
+
if (method === 'options') return discoverCondaPythonOptions();
|
|
103
|
+
if (method === 'save') {
|
|
104
|
+
const [key, value] = args;
|
|
105
|
+
const property = SIMPLE_TRPG_CHECK_CONFIG_GROUP.schema.properties?.[`worlds.simple-trpg-check.${key}`];
|
|
106
|
+
if (!property || typeof key !== 'string' || key === 'enabled') throw new Error('未知配置项');
|
|
107
|
+
if (property.type === 'boolean' ? typeof value !== 'boolean'
|
|
108
|
+
: property.type === 'integer' ? typeof value !== 'number' || !Number.isInteger(value) || value < (property.minimum ?? 0)
|
|
109
|
+
: typeof value !== 'string' || (property.enum && !property.enum.includes(value))) {
|
|
110
|
+
throw new Error('配置值无效');
|
|
111
|
+
}
|
|
112
|
+
this.ctx.persist({ [key]: value });
|
|
113
|
+
return { config: { ...this.ctx.cfg }, keySet: this.hasJevKey() };
|
|
114
|
+
}
|
|
115
|
+
if (method === 'testConnection') {
|
|
101
116
|
try {
|
|
102
117
|
await this.assess('A character climbs a waist-high wall to reach the other side.', ['climbing']);
|
|
103
118
|
return { ok: true };
|
|
@@ -105,18 +120,13 @@ export class SimpleTrpgCheckWorld implements World {
|
|
|
105
120
|
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
106
121
|
}
|
|
107
122
|
}
|
|
108
|
-
if (panel !== 'credentials') throw new Error('未知面板');
|
|
109
|
-
if (method === 'state') {
|
|
110
|
-
return { backend: this.ctx.cfg.backend, source: this.ctx.cfg.jevSource, keySet: this.hasJevKey() };
|
|
111
|
-
}
|
|
112
123
|
if (method === 'openKeyFile') {
|
|
113
124
|
const source = args[0];
|
|
114
125
|
if (source !== this.ctx.cfg.jevSource || this.ctx.cfg.backend !== 'jev') {
|
|
115
126
|
throw new Error('Jev 来源已改变,请重试');
|
|
116
127
|
}
|
|
117
|
-
const secretName = this.ctx.cfg.jevSource === 'openrouter'
|
|
118
|
-
?
|
|
119
|
-
: SIMPLE_TRPG_CHECK_TYPESAFE_SECRET;
|
|
128
|
+
const secretName = this.ctx.cfg.jevSource === 'openrouter' ? SIMPLE_TRPG_CHECK_OPENROUTER_SECRET
|
|
129
|
+
: this.ctx.cfg.jevSource === 'custom' ? SIMPLE_TRPG_CHECK_CUSTOM_SECRET : SIMPLE_TRPG_CHECK_TYPESAFE_SECRET;
|
|
120
130
|
const file = ensureSecretPlaceholder(this.ctx.botDir, secretName);
|
|
121
131
|
await this.openFile(file);
|
|
122
132
|
return { file };
|
|
@@ -141,8 +151,8 @@ export class SimpleTrpgCheckWorld implements World {
|
|
|
141
151
|
}
|
|
142
152
|
|
|
143
153
|
private currentModelSignature(): string {
|
|
144
|
-
const { backend, jevSource, pythonExecutable } = this.ctx.cfg;
|
|
145
|
-
return `${backend}|${backend === 'jev' ? jevSource : pythonExecutable}`;
|
|
154
|
+
const { backend, jevSource, jevEndpoint, pythonExecutable } = this.ctx.cfg;
|
|
155
|
+
return `${backend}|${backend === 'jev' ? `${jevSource}|${jevSource === 'custom' ? jevEndpoint : ''}` : pythonExecutable}`;
|
|
146
156
|
}
|
|
147
157
|
|
|
148
158
|
private async assess(scenario: string, skills: string[]): Promise<number[]> {
|