cortico-world-surfing 0.4.0 → 0.4.1

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.1 - 2026-11-13
4
+
5
+ - Save the Microlink API key directly in the Surfing panel. The compact password field clears after saving, and leaving it empty preserves the existing key.
6
+
3
7
  ## 0.4.0 - 2026-11-06
4
8
 
5
9
  - Add `surfing_search_all`, which searches Bilibili, Tieba, Moegirlpedia and Baidu Baike in parallel and returns short results. Open numbered results and further Tieba results with `surfing_read_page`. The WebUI has a switch for each source and a per-source result limit.
package/README.md CHANGED
@@ -31,19 +31,14 @@
31
31
 
32
32
  无需 API Key 即可使用 Microlink 后备。配置 Key 后使用认证接口及该 Key 的额度;Key 仅发送给 Microlink,不进入模型回执。
33
33
 
34
- 在网上冲浪的「配置」页找到「Microlink 远程渲染」,点击「打开密钥文件」,编辑部署目录的 `.env`:
35
-
36
- ```dotenv
37
- CORTICO_SURFING_MICROLINK_API_KEY=
38
- ```
34
+ 在网上冲浪的「配置」页找到「Microlink 远程渲染」。
39
35
 
40
36
  取得免费 Key 的步骤:
41
37
 
42
38
  1. 打开 [Microlink 注册页](https://dashboard.microlink.io/signup),注册账号并登录,无需购买付费套餐。
43
39
  2. 在控制台左侧选择「Overview」,点击「Usage」标签。
44
40
  3. 点击「Development」标签右上角的「…」,复制 API Key。
45
- 4. 将 Key 填在上面的等号后,不加引号,保存文件。
46
- 5. 点击「刷新密钥状态」,状态变为「API Key 已配置」。下次远程读取使用新 Key;清空值并保存可恢复免 Key 调用。
41
+ 4. 将 Key 粘贴到 Microlink API Key 输入框并保存,下次远程读取即使用新 Key。
47
42
 
48
43
  免 Key 和免费 Key 的额度不同,当前限制见 [Microlink 额度说明](https://microlink.io/docs/api/basics/rate-limit)。额度耗尽或访问失败时,回执说明原因;已有短静态正文仍返回,并标明未取得完整渲染内容。
49
44
 
package/dist/webui.js CHANGED
@@ -843,7 +843,6 @@ var require_ipaddr = __commonJS({
843
843
  var import_ipaddr = __toESM(require_ipaddr(), 1);
844
844
 
845
845
  // src/config.ts
846
- var SURFING_MICROLINK_SECRET = "CORTICO_SURFING_MICROLINK_API_KEY";
847
846
  var PROXY_MODE_PATH = "worlds.surfing.network.proxyCompatibility";
848
847
  var PROXY_RANGES_PATH = "worlds.surfing.network.customProxyRanges";
849
848
  var SURFING_DEFAULTS = {
@@ -1066,7 +1065,7 @@ async function mount(ctx) {
1066
1065
  const toggles = ui.h("div", "segwrap umulti");
1067
1066
  toggles.style.flexWrap = "wrap";
1068
1067
  for (const [path, property] of Object.entries(group.schema.properties)) {
1069
- const save = (value, apply) => saveValue(path, property.title, saved, value, apply);
1068
+ const save2 = (value, apply) => saveValue(path, property.title, saved, value, apply);
1070
1069
  if (property.type === "boolean") {
1071
1070
  const toggle = ui.h("button", "seg", property.title);
1072
1071
  toggle.type = "button";
@@ -1075,7 +1074,7 @@ async function mount(ctx) {
1075
1074
  toggle.addEventListener("click", () => {
1076
1075
  const value = !toggle.classList.contains("active");
1077
1076
  showPressed(toggle, value);
1078
- save(value);
1077
+ save2(value);
1079
1078
  }, { signal: ctx.signal });
1080
1079
  toggles.append(toggle);
1081
1080
  continue;
@@ -1084,7 +1083,7 @@ async function mount(ctx) {
1084
1083
  note.id = path;
1085
1084
  const scale = property["x-scale"] ?? 1;
1086
1085
  const input = ui.input({ type: "number", value: String(Number(state.values[path]) / scale), onChange: (value) => {
1087
- save(Number(value) * scale, (next) => {
1086
+ save2(Number(value) * scale, (next) => {
1088
1087
  if (input.value === value) input.value = String(Number(next) / scale);
1089
1088
  });
1090
1089
  } });
@@ -1135,34 +1134,25 @@ async function mount(ctx) {
1135
1134
  keyStatus.classList.add("bad");
1136
1135
  }
1137
1136
  }
1138
- const keyBar = ui.rowbar();
1139
- keyBar.append(
1140
- keyPill,
1141
- ui.button("\u6253\u5F00\u5BC6\u94A5\u6587\u4EF6", { onClick: () => {
1142
- void keyAction(async () => {
1143
- await ctx.invoke("openKeyFile");
1144
- return "\u5DF2\u6253\u5F00\u5BC6\u94A5\u6587\u4EF6";
1145
- });
1146
- } }),
1147
- ui.button("\u5237\u65B0\u5BC6\u94A5\u72B6\u6001", { onClick: () => {
1148
- void keyAction(async () => {
1149
- showKey((await ctx.invoke("state")).keySet);
1150
- void ctx.refresh().catch(() => void 0);
1151
- return "\u5DF2\u5237\u65B0\u5BC6\u94A5\u72B6\u6001";
1152
- });
1153
- } }),
1154
- keyStatus
1155
- );
1156
- showKey(state.keySet);
1137
+ const keyInput = ui.input({ type: "password", cls: "mono", onCommit: () => save.click() });
1138
+ const save = ui.button("\u4FDD\u5B58\u5BC6\u94A5", { size: "sm", variant: "primary", onClick: () => {
1139
+ if (!keyInput.value.trim()) return;
1140
+ void keyAction(async () => {
1141
+ const next = await ctx.invoke("saveKey", [keyInput.value]);
1142
+ keyInput.value = "";
1143
+ showKey(next.keySet);
1144
+ return "\u5DF2\u4FDD\u5B58";
1145
+ });
1146
+ } });
1147
+ const keyRow = ui.rowbar();
1148
+ keyRow.append(keyInput, save);
1157
1149
  const steps = ui.h("ol");
1158
1150
  steps.style.cssText = "list-style:decimal;padding-left:24px;margin:6px 0;line-height:1.8";
1159
- const secret = ui.h("code", "mono", `${SURFING_MICROLINK_SECRET}=`);
1160
1151
  for (const parts of [
1161
1152
  ["\u6253\u5F00", anchor("Microlink \u6CE8\u518C\u9875", "https://dashboard.microlink.io/signup"), "\uFF0C\u6CE8\u518C\u8D26\u53F7\u5E76\u767B\u5F55\uFF0C\u65E0\u9700\u8D2D\u4E70\u4ED8\u8D39\u5957\u9910\u3002"],
1162
1153
  ["\u5728\u63A7\u5236\u53F0\u5DE6\u4FA7\u9009\u62E9\u300COverview\u300D\uFF0C\u70B9\u51FB\u300CUsage\u300D\u6807\u7B7E\u3002"],
1163
1154
  ["\u70B9\u51FB\u300CDevelopment\u300D\u6807\u7B7E\u53F3\u4E0A\u89D2\u7684\u300C\u2026\u300D\uFF0C\u590D\u5236 API Key\u3002"],
1164
- ["\u70B9\u51FB\u300C\u6253\u5F00\u5BC6\u94A5\u6587\u4EF6\u300D\uFF0C\u5C06 Key \u586B\u5728 ", secret, " \u540E\u9762\uFF0C\u4E0D\u52A0\u5F15\u53F7\uFF0C\u4FDD\u5B58\u6587\u4EF6\u3002"],
1165
- ["\u4FDD\u5B58\u540E\uFF0C\u4E0B\u6B21\u8FDC\u7A0B\u8BFB\u53D6\u4F7F\u7528\u8BE5 Key \u7684\u989D\u5EA6\u3002\u70B9\u51FB\u300C\u5237\u65B0\u5BC6\u94A5\u72B6\u6001\u300D\u53EF\u67E5\u770B\u914D\u7F6E\u72B6\u6001\u3002\u6E05\u7A7A Key \u7684\u503C\u5E76\u4FDD\u5B58\u53EF\u6062\u590D\u514D Key \u8C03\u7528\u3002"]
1155
+ ["\u5728 Microlink API Key \u8F93\u5165\u6846\u7C98\u8D34\u5E76\u4FDD\u5B58\u3002"]
1166
1156
  ]) {
1167
1157
  const step = ui.h("li");
1168
1158
  step.append(...parts);
@@ -1172,11 +1162,12 @@ async function mount(ctx) {
1172
1162
  limits.append("\u514D Key \u548C\u514D\u8D39 Key \u7684\u989D\u5EA6\u4E0D\u540C\uFF0C\u5F53\u524D\u9650\u5236\u89C1", anchor("Microlink \u989D\u5EA6\u8BF4\u660E", "https://microlink.io/docs/api/basics/rate-limit"), "\u3002");
1173
1163
  const microlink = ui.sheet({
1174
1164
  title: "Microlink \u8FDC\u7A0B\u6E32\u67D3",
1175
- desc: "\u672C\u673A Chromium \u4E0D\u53EF\u7528\u65F6\uFF0C\u5C06\u7F51\u9875 URL \u4EA4\u7ED9 Microlink \u6E32\u67D3\u3002API Key \u53EF\u9009\uFF0C\u7559\u7A7A\u4F7F\u7528\u514D Key \u989D\u5EA6\u3002"
1165
+ desc: "\u672C\u673A Chromium \u4E0D\u53EF\u7528\u65F6\uFF0C\u5C06\u7F51\u9875 URL \u4EA4\u7ED9 Microlink \u6E32\u67D3\u3002API Key \u53EF\u9009\uFF0C\u672A\u914D\u7F6E\u65F6\u4F7F\u7528\u514D Key \u989D\u5EA6\u3002"
1176
1166
  });
1177
1167
  const help = ui.foldSheet("surfing-microlink-key", { title: "\u5982\u4F55\u53D6\u5F97 API Key", defaultOpen: false });
1178
1168
  help.body.append(steps, limits);
1179
- microlink.body.append(keyBar, help.el);
1169
+ microlink.body.append(keyPill, ui.field("Microlink API Key", keyRow), keyStatus, help.el);
1170
+ showKey(state.keySet);
1180
1171
  layout.append(microlink.el);
1181
1172
  ctx.root.replaceChildren(layout);
1182
1173
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cortico-world-surfing",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "type": "module",
5
5
  "description": "搜索、上网冲浪、学习最新的知识。读取公开网页正文和B站视频简介、字幕、评论,支持B站、贴吧与百科站点搜索、缩写释义、分段续读。",
6
6
  "author": "DTMosken",
@@ -1,5 +1,5 @@
1
1
  import type { WebUiClientBundle, WebUiPanelContext } from 'cortico/web/shared/client-panel.ts';
2
- import { PROXY_MODE_PATH, PROXY_RANGES_PATH, SURFING_CONFIG_GROUPS, SURFING_LIMITS_CONFIG_GROUP, SURFING_MICROLINK_SECRET, SURFING_PROXY_CONFIG_GROUP } from '../config.ts';
2
+ import { PROXY_MODE_PATH, PROXY_RANGES_PATH, SURFING_CONFIG_GROUPS, SURFING_LIMITS_CONFIG_GROUP, SURFING_PROXY_CONFIG_GROUP } from '../config.ts';
3
3
 
4
4
  type ConfigValue = number | boolean | string;
5
5
  interface ConfigState { values: Record<string, ConfigValue>; keySet: boolean }
@@ -131,25 +131,25 @@ async function mount(ctx: WebUiPanelContext): Promise<void> {
131
131
  try { keyStatus.textContent = await run(); keyStatus.classList.remove('bad'); }
132
132
  catch (error) { keyStatus.textContent = errorText(error); keyStatus.classList.add('bad'); }
133
133
  }
134
- const keyBar = ui.rowbar();
135
- keyBar.append(keyPill,
136
- ui.button('打开密钥文件', { onClick: () => { void keyAction(async () => { await ctx.invoke('openKeyFile'); return '已打开密钥文件'; }); } }),
137
- ui.button('刷新密钥状态', { onClick: () => { void keyAction(async () => {
138
- showKey((await ctx.invoke<ConfigState>('state')).keySet);
139
- void ctx.refresh().catch(() => undefined);
140
- return '已刷新密钥状态';
141
- }); } }), keyStatus);
142
- showKey(state.keySet);
143
-
134
+ const keyInput = ui.input({ type: 'password', cls: 'mono', onCommit: () => save.click() });
135
+ const save = ui.button('保存密钥', { size: 'sm', variant: 'primary', onClick: () => {
136
+ if (!keyInput.value.trim()) return;
137
+ void keyAction(async () => {
138
+ const next = await ctx.invoke<ConfigState>('saveKey', [keyInput.value]);
139
+ keyInput.value = '';
140
+ showKey(next.keySet);
141
+ return '已保存';
142
+ });
143
+ } });
144
+ const keyRow = ui.rowbar();
145
+ keyRow.append(keyInput, save);
144
146
  const steps = ui.h('ol');
145
147
  steps.style.cssText = 'list-style:decimal;padding-left:24px;margin:6px 0;line-height:1.8';
146
- const secret = ui.h('code', 'mono', `${SURFING_MICROLINK_SECRET}=`);
147
148
  for (const parts of [
148
149
  ['打开', anchor('Microlink 注册页', 'https://dashboard.microlink.io/signup'), ',注册账号并登录,无需购买付费套餐。'],
149
150
  ['在控制台左侧选择「Overview」,点击「Usage」标签。'],
150
151
  ['点击「Development」标签右上角的「…」,复制 API Key。'],
151
- ['点击「打开密钥文件」,将 Key 填在 ', secret, ' 后面,不加引号,保存文件。'],
152
- ['保存后,下次远程读取使用该 Key 的额度。点击「刷新密钥状态」可查看配置状态。清空 Key 的值并保存可恢复免 Key 调用。'],
152
+ ['在 Microlink API Key 输入框粘贴并保存。'],
153
153
  ]) {
154
154
  const step = ui.h('li');
155
155
  step.append(...parts);
@@ -159,11 +159,12 @@ async function mount(ctx: WebUiPanelContext): Promise<void> {
159
159
  limits.append('免 Key 和免费 Key 的额度不同,当前限制见', anchor('Microlink 额度说明', 'https://microlink.io/docs/api/basics/rate-limit'), '。');
160
160
  const microlink = ui.sheet({
161
161
  title: 'Microlink 远程渲染',
162
- desc: '本机 Chromium 不可用时,将网页 URL 交给 Microlink 渲染。API Key 可选,留空使用免 Key 额度。',
162
+ desc: '本机 Chromium 不可用时,将网页 URL 交给 Microlink 渲染。API Key 可选,未配置时使用免 Key 额度。',
163
163
  });
164
164
  const help = ui.foldSheet('surfing-microlink-key', { title: '如何取得 API Key', defaultOpen: false });
165
165
  help.body.append(steps, limits);
166
- microlink.body.append(keyBar, help.el);
166
+ microlink.body.append(keyPill, ui.field('Microlink API Key', keyRow), keyStatus, help.el);
167
+ showKey(state.keySet);
167
168
  layout.append(microlink.el);
168
169
  ctx.root.replaceChildren(layout);
169
170
  }
package/src/world.ts CHANGED
@@ -12,7 +12,6 @@ import { PageReader, pageKey } from './page-reader.ts';
12
12
  import { SnapshotCache, estimateTokens, type Material } from './snapshots.ts';
13
13
  import { MicrolinkRenderer } from './microlink.ts';
14
14
  import { SITE_SOURCES, continueTieba, searchSites } from './sites.ts';
15
- import { prepareKeyFile, openSecretFile } from './secret-file.ts';
16
15
 
17
16
  const ENV_PROMPT_FILE = fileURLToPath(new URL('./ENV_PROMPT.md', import.meta.url));
18
17
 
@@ -80,8 +79,7 @@ export class SurfingWorld implements World {
80
79
  private stopped = false;
81
80
  private last?: { status: string; failed: boolean; estimatedTokens: number; sourceTruncated: boolean };
82
81
 
83
- constructor(private readonly ctx: WorldContext<SurfingConfigSection>, private readonly client = new PublicClient(),
84
- private readonly openFile = openSecretFile) {
82
+ constructor(private readonly ctx: WorldContext<SurfingConfigSection>, private readonly client = new PublicClient()) {
85
83
  applyConfigDefaults(ctx.cfg);
86
84
  this.pages = new PageReader(undefined, new MicrolinkRenderer(() => ctx.secret(SURFING_MICROLINK_SECRET)));
87
85
  }
@@ -244,9 +242,13 @@ export class SurfingWorld implements World {
244
242
  if (panel !== 'config') throw new Error('未知面板。');
245
243
  if (method === 'state') return this.configState();
246
244
  if (method === 'save') { this.saveConfig(args[0], args[1]); return this.configState(); }
247
- if (method === 'openKeyFile') {
248
- const file = prepareKeyFile(this.ctx.botDir);
249
- await this.openFile(file); return { file };
245
+ if (method === 'saveKey') {
246
+ const [value] = args;
247
+ if (typeof value !== 'string') throw new Error('密钥须为文本');
248
+ const key = value.trim();
249
+ if (/\s/.test(key)) throw new Error('密钥不能包含空白字符');
250
+ if (key) this.ctx.storeSecret(SURFING_MICROLINK_SECRET, key);
251
+ return this.configState();
250
252
  }
251
253
  throw new Error('未知操作。');
252
254
  },
@@ -1,24 +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
- import { SURFING_MICROLINK_SECRET } from './config.ts';
5
-
6
- export function prepareKeyFile(botDir: string): string {
7
- const file = join(botDir, '.env');
8
- const contents = existsSync(file) ? readFileSync(file, 'utf8') : '';
9
- if (!new RegExp(`^${SURFING_MICROLINK_SECRET}=`, 'm').test(contents)) {
10
- const line = `${contents && !contents.endsWith('\n') ? '\n' : ''}${SURFING_MICROLINK_SECRET}=\n`;
11
- if (existsSync(file)) appendFileSync(file, line);
12
- else writeFileSync(file, line, { mode: 0o600 });
13
- }
14
- return file;
15
- }
16
-
17
- export function openSecretFile(file: string): Promise<void> {
18
- const command = process.platform === 'win32' ? 'notepad.exe' : process.platform === 'darwin' ? 'open' : 'xdg-open';
19
- return new Promise((resolve, reject) => {
20
- const child = spawn(command, [file], { detached: true, stdio: 'ignore' });
21
- child.once('error', () => reject(new Error('无法打开密钥文件。')));
22
- child.once('spawn', () => { child.unref(); resolve(); });
23
- });
24
- }