cortico-world-simple-trpg-check 0.4.2 → 0.4.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/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.3 - 2026-11-13
4
+
5
+ - Save a Jev API key for the selected service directly in the configuration panel. Existing keys and legacy key lookup remain supported.
6
+
3
7
  ## 0.4.2 - 2026-10-21
4
8
 
5
9
  - Shorten the environment prompt while retaining narrative and reward bookkeeping rules.
package/README.md CHANGED
@@ -33,7 +33,7 @@ corepack pnpm add --ignore-workspace cortico-world-simple-trpg-check
33
33
 
34
34
  默认 JEV 模型使用现有技能评分校准。Luna 与其他模型显示“未校准”,按档位代表值取期望;财富与信用评级仍使用专用标尺。
35
35
 
36
- 决策服务密钥保存在部署目录的 `.env` 中,配置页的“打开密钥文件”可打开该文件。同一服务的模型共用密钥。填写并保存后,下一次模型请求读取密钥。
36
+ 在配置页选择决策服务,输入该服务的 API Key 并点击“保存密钥”。同一服务的模型共用密钥。填写并保存后,下一次模型请求读取密钥。
37
37
 
38
38
  | 决策服务 | 密钥变量 |
39
39
  | --- | --- |
package/dist/webui.js CHANGED
@@ -183,23 +183,27 @@ async function mountConfig(ctx) {
183
183
  }
184
184
  if (key === "jevSource") {
185
185
  const sourceName = config.jevSource === "openrouter" ? "OpenRouter" : config.jevSource === "custom" ? "\u81EA\u5B9A\u4E49\u670D\u52A1" : "TypeSafe";
186
- const keyStatus = ui.msgline(state.keySet ? "\u5BC6\u94A5\u5DF2\u914D\u7F6E" : "\u5BC6\u94A5\u672A\u914D\u7F6E");
187
- const open = ui.button(`\u6253\u5F00 ${sourceName} \u5BC6\u94A5\u6587\u4EF6`, {
188
- onClick: () => {
189
- open.disabled = true;
190
- void ctx.invoke("openKeyFile", [config.jevSource]).then(({ file }) => {
191
- keyStatus.textContent = `\u5DF2\u6253\u5F00 ${file}`;
192
- }).catch((error) => {
193
- keyStatus.textContent = `\u6253\u5F00\u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}`;
194
- keyStatus.classList.add("bad");
195
- }).finally(() => {
196
- open.disabled = false;
197
- });
198
- }
199
- });
186
+ const keyStatus = ui.pill(state.keySet ? "\u5BC6\u94A5\u5DF2\u914D\u7F6E" : "\u5BC6\u94A5\u672A\u914D\u7F6E", state.keySet ? "on" : "off");
187
+ const input = ui.input({ type: "password", cls: "mono", onCommit: () => saveKey.click() });
188
+ const saveKey = ui.button("\u4FDD\u5B58\u5BC6\u94A5", { size: "sm", variant: "primary", onClick: () => {
189
+ if (!input.value.trim()) return;
190
+ saveKey.disabled = true;
191
+ void ctx.invoke("saveKey", [config.jevSource, input.value]).then((next) => {
192
+ state = next;
193
+ status = "\u5DF2\u4FDD\u5B58";
194
+ failed = false;
195
+ render();
196
+ void ctx.refresh().catch(() => void 0);
197
+ }).catch((error) => {
198
+ result.textContent = error instanceof Error ? error.message : "\u4FDD\u5B58\u5931\u8D25";
199
+ result.classList.add("bad");
200
+ }).finally(() => {
201
+ saveKey.disabled = false;
202
+ });
203
+ } });
200
204
  const keyRow = ui.rowbar();
201
- keyRow.append(field, keyStatus, open);
202
- fields.appendChild(keyRow);
205
+ keyRow.append(input, saveKey);
206
+ fields.append(field, keyStatus, ui.field(`${sourceName} API Key`, keyRow));
203
207
  } else fields.appendChild(field);
204
208
  if (key === "jevModel" && !hasCalibratedReadout(decisionModel(config))) {
205
209
  fields.appendChild(ui.msgline("\u672A\u6821\u51C6\uFF1A\u6280\u80FD\u6C34\u5E73\u6309\u6863\u4F4D\u4EE3\u8868\u503C\u53D6\u671F\u671B\u3002"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cortico-world-simple-trpg-check",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "type": "module",
5
5
  "description": "Cortico 的 TRPG 技能检定 World 扩展,使用本地或远程模型评估技能水平,支持 d100 掷骰、组合与对抗检定。",
6
6
  "license": "MIT",
@@ -98,21 +98,25 @@ async function mountConfig(ctx: WebUiPanelContext): Promise<void> {
98
98
  }
99
99
  if (key === 'jevSource') {
100
100
  const sourceName = config.jevSource === 'openrouter' ? 'OpenRouter' : config.jevSource === 'custom' ? '自定义服务' : 'TypeSafe';
101
- const keyStatus = ui.msgline(state.keySet ? '密钥已配置' : '密钥未配置');
102
- const open = ui.button(`打开 ${sourceName} 密钥文件`, {
103
- onClick: () => {
104
- open.disabled = true;
105
- void ctx.invoke<{ file: string }>('openKeyFile', [config.jevSource]).then(({ file }) => {
106
- keyStatus.textContent = `已打开 ${file}`;
107
- }).catch((error) => {
108
- keyStatus.textContent = `打开失败:${error instanceof Error ? error.message : String(error)}`;
109
- keyStatus.classList.add('bad');
110
- }).finally(() => { open.disabled = false; });
111
- },
112
- });
101
+ const keyStatus = ui.pill(state.keySet ? '密钥已配置' : '密钥未配置', state.keySet ? 'on' : 'off');
102
+ const input = ui.input({ type: 'password', cls: 'mono', onCommit: () => saveKey.click() });
103
+ const saveKey = ui.button('保存密钥', { size: 'sm', variant: 'primary', onClick: () => {
104
+ if (!input.value.trim()) return;
105
+ saveKey.disabled = true;
106
+ void ctx.invoke<ConfigState>('saveKey', [config.jevSource, input.value]).then(next => {
107
+ state = next;
108
+ status = '已保存';
109
+ failed = false;
110
+ render();
111
+ void ctx.refresh().catch(() => undefined);
112
+ }).catch(error => {
113
+ result.textContent = error instanceof Error ? error.message : '保存失败';
114
+ result.classList.add('bad');
115
+ }).finally(() => { saveKey.disabled = false; });
116
+ } });
113
117
  const keyRow = ui.rowbar();
114
- keyRow.append(field, keyStatus, open);
115
- fields.appendChild(keyRow);
118
+ keyRow.append(input, saveKey);
119
+ fields.append(field, keyStatus, ui.field(`${sourceName} API Key`, keyRow));
116
120
  } else fields.appendChild(field);
117
121
  if (key === 'jevModel' && !hasCalibratedReadout(decisionModel(config))) {
118
122
  fields.appendChild(ui.msgline('未校准:技能水平按档位代表值取期望。'));
package/src/world.ts CHANGED
@@ -35,7 +35,6 @@ import { formatContest, formatReceipt, type ContestSide, type SkillCheck } from
35
35
  import { SystemOneSkillScorer, type SkillScorer } from './model.ts';
36
36
  import { budgetViolation, checksLabel, type CheckRequest, type CheckSpec } from './request.ts';
37
37
  import { discoverCondaPythonOptions } from './conda-environments.ts';
38
- import { ensureSecretPlaceholder, openSecretFile } from './secret-file.ts';
39
38
 
40
39
  const ENV_PROMPT_FILE = fileURLToPath(new URL('./ENV_PROMPT.md', import.meta.url));
41
40
 
@@ -175,7 +174,6 @@ export class SimpleTrpgCheckWorld implements World {
175
174
  private readonly ctx: WorldContext<SimpleTrpgCheckConfigSection>,
176
175
  scorer?: SkillScorer,
177
176
  private readonly roll: () => number = () => randomInt(1, 101),
178
- private readonly openFile: (path: string) => void | Promise<void> = openSecretFile,
179
177
  ) {
180
178
  this.scorer = scorer ?? new SystemOneSkillScorer(ctx);
181
179
  }
@@ -255,16 +253,19 @@ export class SimpleTrpgCheckWorld implements World {
255
253
  return { ok: false, error: error instanceof Error ? error.message : String(error) };
256
254
  }
257
255
  }
258
- if (method === 'openKeyFile') {
256
+ if (method === 'saveKey') {
259
257
  const source = args[0];
260
258
  if (source !== this.ctx.cfg.jevSource || this.ctx.cfg.backend !== 'jev') {
261
259
  throw new Error('决策服务已改变,请重试');
262
260
  }
263
261
  const secretName = this.ctx.cfg.jevSource === 'openrouter' ? SIMPLE_TRPG_CHECK_OPENROUTER_SECRET
264
262
  : this.ctx.cfg.jevSource === 'custom' ? SIMPLE_TRPG_CHECK_CUSTOM_SECRET : SIMPLE_TRPG_CHECK_TYPESAFE_SECRET;
265
- const file = ensureSecretPlaceholder(this.ctx.botDir, secretName);
266
- await this.openFile(file);
267
- return { file };
263
+ const value = args[1];
264
+ if (typeof value !== 'string') throw new Error('密钥须为文本');
265
+ const key = value.trim();
266
+ if (/\s/.test(key)) throw new Error('密钥不能包含空白字符');
267
+ if (key) this.ctx.storeSecret(secretName, key);
268
+ return { config: { ...this.ctx.cfg }, keySet: this.hasJevKey() };
268
269
  }
269
270
  throw new Error('未知操作');
270
271
  },
@@ -1,23 +0,0 @@
1
- import { spawn } from 'node:child_process';
2
- import { appendFileSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
3
- import { join } from 'node:path';
4
-
5
- export function ensureSecretPlaceholder(botDir: string, name: string): string {
6
- const file = join(botDir, '.env');
7
- const contents = existsSync(file) ? readFileSync(file, 'utf8') : '';
8
- if (!new RegExp(`^${name}=`, 'm').test(contents)) {
9
- const line = `${contents && !contents.endsWith('\n') ? '\n' : ''}${name}=\n`;
10
- if (existsSync(file)) appendFileSync(file, line);
11
- else writeFileSync(file, line, { mode: 0o600 });
12
- }
13
- return file;
14
- }
15
-
16
- export function openSecretFile(file: string): Promise<void> {
17
- const command = process.platform === 'win32' ? 'notepad.exe' : process.platform === 'darwin' ? 'open' : 'xdg-open';
18
- return new Promise((resolve, reject) => {
19
- const child = spawn(command, [file], { detached: true, stdio: 'ignore' });
20
- child.once('error', reject);
21
- child.once('spawn', () => { child.unref(); resolve(); });
22
- });
23
- }