cortico-world-surfing 0.3.1 → 0.3.2
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 +5 -0
- package/README.md +24 -2
- package/dist/webui.js +216 -0
- package/package.json +5 -2
- package/src/config.ts +11 -3
- package/src/microlink.ts +35 -0
- package/src/network.ts +1 -1
- package/src/page-reader.ts +12 -2
- package/src/secret-file.ts +24 -0
- package/src/webui/client.ts +97 -0
- package/src/world.ts +35 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.2 - 2026-11-03
|
|
4
|
+
|
|
5
|
+
- Use Microlink to render pages when local Chromium is unavailable, with an optional API key and keyless requests.
|
|
6
|
+
- Merge reading limits and Microlink settings into one WebUI config panel, with key file actions, key status and step-by-step API key instructions.
|
|
7
|
+
|
|
3
8
|
## 0.3.1 - 2026-10-30
|
|
4
9
|
|
|
5
10
|
- Align development and CI validation with Cortico 0.2.10.
|
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- Owner: package.json, src/definition.ts, src/world.ts, src/ENV_PROMPT.md, src/network.ts, src/bili.ts -->
|
|
1
|
+
<!-- Owner: package.json, src/definition.ts, src/world.ts, src/ENV_PROMPT.md, src/network.ts, src/bili.ts, src/microlink.ts, src/webui/client.ts -->
|
|
2
2
|
# 网上冲浪
|
|
3
3
|
|
|
4
4
|
让 Cortico 阅读公开网页、B站视频简介、字幕和评论,搜索视频,并根据读到的内容回答问题。发送链接或BV号即可使用,B站读取均使用匿名请求。
|
|
@@ -18,12 +18,34 @@
|
|
|
18
18
|
|
|
19
19
|
1. 在 Cortico 扩展管理页输入 `cortico-world-surfing` 安装 npm 包;使用本地源码时选择本项目目录。
|
|
20
20
|
2. 重启 Cortico 进程,启用“网上冲浪”。
|
|
21
|
-
3.
|
|
21
|
+
3. 读取依赖脚本加载正文的网页时,优先使用本机 Chromium。可在扩展目录安装:
|
|
22
22
|
|
|
23
23
|
```powershell
|
|
24
24
|
corepack pnpm install:browser
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
+
本机 Chromium 不可用时,扩展将目标网页 URL 发送给 Microlink,由其渲染后返回 HTML;正文、编号链接和章节锚点仍由本扩展提取。静态读取、Chromium 和 Microlink 共用单次读取超时及下载上限。Chromium 已启动后的网页错误、超时或取消不触发 Microlink。
|
|
28
|
+
|
|
29
|
+
## Microlink(可选)
|
|
30
|
+
|
|
31
|
+
无需 API Key 即可使用 Microlink 后备。配置 Key 后使用认证接口及该 Key 的额度;Key 仅发送给 Microlink,不进入模型回执。
|
|
32
|
+
|
|
33
|
+
在网上冲浪的「配置」页找到「Microlink 远程渲染」,点击「打开密钥文件」,编辑部署目录的 `.env`:
|
|
34
|
+
|
|
35
|
+
```dotenv
|
|
36
|
+
CORTICO_SURFING_MICROLINK_API_KEY=
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
取得免费 Key 的步骤:
|
|
40
|
+
|
|
41
|
+
1. 打开 [Microlink 注册页](https://dashboard.microlink.io/signup),注册账号并登录,无需购买付费套餐。
|
|
42
|
+
2. 在控制台左侧选择「Overview」,点击「Usage」标签。
|
|
43
|
+
3. 点击「Development」标签右上角的「…」,复制 API Key。
|
|
44
|
+
4. 将 Key 填在上面的等号后,不加引号,保存文件。
|
|
45
|
+
5. 点击「刷新密钥状态」,状态变为「API Key 已配置」。下次远程读取使用新 Key;清空值并保存可恢复免 Key 调用。
|
|
46
|
+
|
|
47
|
+
免 Key 和免费 Key 的额度不同,当前限制见 [Microlink 额度说明](https://microlink.io/docs/api/basics/rate-limit)。额度耗尽或访问失败时,回执说明原因;已有短静态正文仍返回,并标明未取得完整渲染内容。
|
|
48
|
+
|
|
27
49
|
## World 兼容性
|
|
28
50
|
|
|
29
51
|
按 [Cortico World 兼容等级](https://github.com/Pal-AI-Lab/Cortico/blob/main/docs/world-compatibility.md),本 World 的最低等级为 **L1**,完整等级为 **L3**。
|
package/dist/webui.js
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// src/config.ts
|
|
2
|
+
var SURFING_MICROLINK_SECRET = "CORTICO_SURFING_MICROLINK_API_KEY";
|
|
3
|
+
var SURFING_DEFAULTS = {
|
|
4
|
+
enabled: false,
|
|
5
|
+
reading: { maxResponseEstimatedTokens: 3e3, maxSourceChars: 3e5 },
|
|
6
|
+
bili: { subtitleGroupSec: 30, maxSubtitleRetries: 2, subtitleRetryDelayMs: 1e3 },
|
|
7
|
+
network: { maxDownloadBytes: 8 * 1024 * 1024, requestTimeoutMs: 2e4 },
|
|
8
|
+
cache: { ttlMs: 9e5, maxBytes: 32 * 1024 * 1024 }
|
|
9
|
+
};
|
|
10
|
+
var SURFING_CONFIG_GROUP = {
|
|
11
|
+
id: "world:surfing",
|
|
12
|
+
owner: "world:surfing",
|
|
13
|
+
schema: { type: "object", title: "\u9605\u8BFB\u4E0E\u5B57\u5E55", properties: {
|
|
14
|
+
"worlds.surfing.reading.maxResponseEstimatedTokens": {
|
|
15
|
+
type: "integer",
|
|
16
|
+
title: "\u5355\u6B21\u8FD4\u56DE\u4E0A\u9650",
|
|
17
|
+
minimum: 1024,
|
|
18
|
+
maximum: 32768,
|
|
19
|
+
"x-suffix": "\u4F30\u7B97 token",
|
|
20
|
+
"x-hot": true,
|
|
21
|
+
description: "\u5305\u542B\u6B63\u6587\u548C\u7EED\u8BFB\u4FE1\u606F\uFF1B\u4E0B\u6B21\u8C03\u7528\u751F\u6548\u3002\u8FDE\u7EED\u7EED\u8BFB\u4F1A\u7D2F\u79EF\u4E0A\u4E0B\u6587\u3002"
|
|
22
|
+
},
|
|
23
|
+
"worlds.surfing.bili.subtitleGroupSec": {
|
|
24
|
+
type: "integer",
|
|
25
|
+
title: "\u5B57\u5E55\u65F6\u95F4\u5206\u7EC4",
|
|
26
|
+
minimum: 1,
|
|
27
|
+
maximum: 120,
|
|
28
|
+
"x-suffix": "\u79D2",
|
|
29
|
+
"x-hot": true,
|
|
30
|
+
description: "\u5408\u5E76\u663E\u793A\u65F6\u95F4\u6233\uFF0C\u4FDD\u7559\u539F\u53E5\uFF1B\u65B0\u8BFB\u53D6\u751F\u6548\u3002"
|
|
31
|
+
},
|
|
32
|
+
"worlds.surfing.bili.maxSubtitleRetries": {
|
|
33
|
+
type: "integer",
|
|
34
|
+
title: "\u7A7A\u5B57\u5E55\u91CD\u8BD5\u6B21\u6570",
|
|
35
|
+
minimum: 0,
|
|
36
|
+
maximum: 5,
|
|
37
|
+
"x-suffix": "\u6B21",
|
|
38
|
+
"x-hot": true,
|
|
39
|
+
description: "\u9996\u6B21\u4E3A\u7A7A\u540E\u989D\u5916\u91CD\u8BD5\uFF1B0 \u8868\u793A\u5173\u95ED\u3002\u65B0\u8BFB\u53D6\u751F\u6548\u3002"
|
|
40
|
+
},
|
|
41
|
+
"worlds.surfing.bili.subtitleRetryDelayMs": {
|
|
42
|
+
type: "integer",
|
|
43
|
+
title: "\u9996\u6B21\u91CD\u8BD5\u7B49\u5F85",
|
|
44
|
+
minimum: 100,
|
|
45
|
+
maximum: 1e4,
|
|
46
|
+
"x-scale": 1e3,
|
|
47
|
+
"x-suffix": "\u79D2",
|
|
48
|
+
"x-hot": true,
|
|
49
|
+
description: "\u540E\u7EED\u6BCF\u6B21\u7B49\u5F85\u7FFB\u500D\uFF0C\u91CD\u8BD5\u5171\u7528\u5355\u6B21\u8BFB\u53D6\u8D85\u65F6\u3002\u65B0\u8BFB\u53D6\u751F\u6548\u3002"
|
|
50
|
+
}
|
|
51
|
+
} }
|
|
52
|
+
};
|
|
53
|
+
var SURFING_LIMITS_CONFIG_GROUP = {
|
|
54
|
+
id: "world:surfing:limits",
|
|
55
|
+
owner: "world:surfing",
|
|
56
|
+
schema: { type: "object", title: "\u83B7\u53D6\u4E0E\u7F13\u5B58\u9650\u5236", properties: {
|
|
57
|
+
"worlds.surfing.reading.maxSourceChars": {
|
|
58
|
+
type: "integer",
|
|
59
|
+
title: "\u5355\u4EFD\u6750\u6599\u4FDD\u7559\u4E0A\u9650",
|
|
60
|
+
minimum: 1e4,
|
|
61
|
+
maximum: 2e6,
|
|
62
|
+
"x-suffix": "\u5B57\u7B26",
|
|
63
|
+
description: "\u6B63\u6587\u3001\u94FE\u63A5\u5730\u5740\u548C\u641C\u7D22\u7ED3\u679C\u5408\u8BA1\uFF1B\u89E6\u9650\u65F6\u6807\u660E\u6750\u6599\u4E0D\u5B8C\u6574\u3002\u65B0\u8BFB\u53D6\u751F\u6548\u3002"
|
|
64
|
+
},
|
|
65
|
+
"worlds.surfing.network.maxDownloadBytes": {
|
|
66
|
+
type: "integer",
|
|
67
|
+
title: "\u5355\u6B21\u8BFB\u53D6\u4E0B\u8F7D\u4E0A\u9650",
|
|
68
|
+
minimum: 1048576,
|
|
69
|
+
maximum: 67108864,
|
|
70
|
+
"x-scale": 1048576,
|
|
71
|
+
"x-suffix": "MiB",
|
|
72
|
+
description: "\u6309\u89E3\u538B\u540E\u6570\u636E\u7D2F\u8BA1\uFF0C\u5305\u542B\u91CD\u5B9A\u5411\u53CA\u6E32\u67D3\u8BF7\u6C42\u3002"
|
|
73
|
+
},
|
|
74
|
+
"worlds.surfing.network.requestTimeoutMs": {
|
|
75
|
+
type: "integer",
|
|
76
|
+
title: "\u5355\u6B21\u8BFB\u53D6\u8D85\u65F6",
|
|
77
|
+
minimum: 3e3,
|
|
78
|
+
maximum: 12e4,
|
|
79
|
+
"x-scale": 1e3,
|
|
80
|
+
"x-suffix": "\u79D2",
|
|
81
|
+
description: "\u6574\u6B21\u8BFB\u53D6\u5171\u7528\u65F6\u9650\uFF1B\u4E0B\u6B21\u8C03\u7528\u751F\u6548\u3002"
|
|
82
|
+
},
|
|
83
|
+
"worlds.surfing.cache.ttlMs": {
|
|
84
|
+
type: "integer",
|
|
85
|
+
title: "\u7EED\u8BFB\u4FDD\u7559\u65F6\u95F4",
|
|
86
|
+
minimum: 6e4,
|
|
87
|
+
maximum: 72e5,
|
|
88
|
+
"x-scale": 6e4,
|
|
89
|
+
"x-suffix": "\u5206\u949F",
|
|
90
|
+
description: "\u5230\u671F\u9700\u91CD\u65B0\u8BFB\u53D6\uFF1B\u65B0\u5FEB\u7167\u751F\u6548\u3002"
|
|
91
|
+
},
|
|
92
|
+
"worlds.surfing.cache.maxBytes": {
|
|
93
|
+
type: "integer",
|
|
94
|
+
title: "\u6587\u672C\u7F13\u5B58\u4E0A\u9650",
|
|
95
|
+
minimum: 8388608,
|
|
96
|
+
maximum: 268435456,
|
|
97
|
+
"x-scale": 1048576,
|
|
98
|
+
"x-suffix": "MiB",
|
|
99
|
+
description: "\u6309\u4FDD\u5B58\u7684\u6570\u636E\u5927\u5C0F\u6838\u7B97\uFF0C\u8D85\u9650\u6DD8\u6C70\u6700\u4E45\u672A\u8BFB\u7684\u6750\u6599\u3002"
|
|
100
|
+
}
|
|
101
|
+
} }
|
|
102
|
+
};
|
|
103
|
+
var SURFING_CONFIG_GROUPS = [SURFING_CONFIG_GROUP, SURFING_LIMITS_CONFIG_GROUP];
|
|
104
|
+
|
|
105
|
+
// src/webui/client.ts
|
|
106
|
+
var errorText = (error) => error instanceof Error ? error.message : "\u64CD\u4F5C\u5931\u8D25";
|
|
107
|
+
async function mount(ctx) {
|
|
108
|
+
const { ui } = ctx;
|
|
109
|
+
const state = await ctx.invoke("state");
|
|
110
|
+
const layout = ui.h("div");
|
|
111
|
+
layout.style.cssText = "display:grid;gap:20px;max-width:960px;margin:auto";
|
|
112
|
+
let saves = Promise.resolve();
|
|
113
|
+
for (const group of SURFING_CONFIG_GROUPS) {
|
|
114
|
+
const sheet = group === SURFING_LIMITS_CONFIG_GROUP ? ui.foldSheet("surfing-limits", { title: group.schema.title, defaultOpen: false }) : ui.sheet({ title: group.schema.title });
|
|
115
|
+
const grid = ui.h("div");
|
|
116
|
+
grid.style.cssText = "display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,320px),1fr));gap:16px 24px";
|
|
117
|
+
const saved = ui.msgline("");
|
|
118
|
+
for (const [path, property] of Object.entries(group.schema.properties)) {
|
|
119
|
+
const scale = property["x-scale"] ?? 1;
|
|
120
|
+
const input = ui.input({ type: "number", value: String(state.values[path] / scale), onChange: (value) => {
|
|
121
|
+
saves = saves.then(async () => {
|
|
122
|
+
const next = await ctx.invoke("save", [path, Number(value) * scale]);
|
|
123
|
+
if (input.value === value) input.value = String(next.values[path] / scale);
|
|
124
|
+
saved.textContent = `\u5DF2\u4FDD\u5B58\uFF1A${property.title}`;
|
|
125
|
+
saved.classList.remove("bad");
|
|
126
|
+
void ctx.refresh().catch(() => void 0);
|
|
127
|
+
}).catch((error) => {
|
|
128
|
+
saved.textContent = errorText(error);
|
|
129
|
+
saved.classList.add("bad");
|
|
130
|
+
});
|
|
131
|
+
} });
|
|
132
|
+
input.min = String(property.minimum / scale);
|
|
133
|
+
input.max = String(property.maximum / scale);
|
|
134
|
+
const field = ui.field(`${property.title}\uFF08${property["x-suffix"]}\uFF09`, input);
|
|
135
|
+
const note = ui.h("small", "muted", property.description);
|
|
136
|
+
note.id = path;
|
|
137
|
+
input.setAttribute("aria-describedby", note.id);
|
|
138
|
+
const row = ui.h("div");
|
|
139
|
+
row.append(field, note);
|
|
140
|
+
grid.append(row);
|
|
141
|
+
}
|
|
142
|
+
sheet.body.append(grid, saved);
|
|
143
|
+
layout.append(sheet.el);
|
|
144
|
+
}
|
|
145
|
+
const anchor = (text, href) => {
|
|
146
|
+
const link = ui.h("a", null, text);
|
|
147
|
+
link.href = href;
|
|
148
|
+
link.target = "_blank";
|
|
149
|
+
link.rel = "noopener noreferrer";
|
|
150
|
+
return link;
|
|
151
|
+
};
|
|
152
|
+
const keyStatus = ui.msgline("");
|
|
153
|
+
let keyPill = ui.pill("", "off");
|
|
154
|
+
const showKey = (keySet) => {
|
|
155
|
+
const next = ui.pill(keySet ? "API Key \u5DF2\u914D\u7F6E" : "\u514D Key", keySet ? "on" : "off");
|
|
156
|
+
keyPill.replaceWith(next);
|
|
157
|
+
keyPill = next;
|
|
158
|
+
};
|
|
159
|
+
async function keyAction(run) {
|
|
160
|
+
try {
|
|
161
|
+
keyStatus.textContent = await run();
|
|
162
|
+
keyStatus.classList.remove("bad");
|
|
163
|
+
} catch (error) {
|
|
164
|
+
keyStatus.textContent = errorText(error);
|
|
165
|
+
keyStatus.classList.add("bad");
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const keyBar = ui.rowbar();
|
|
169
|
+
keyBar.append(
|
|
170
|
+
keyPill,
|
|
171
|
+
ui.button("\u6253\u5F00\u5BC6\u94A5\u6587\u4EF6", { onClick: () => {
|
|
172
|
+
void keyAction(async () => {
|
|
173
|
+
await ctx.invoke("openKeyFile");
|
|
174
|
+
return "\u5DF2\u6253\u5F00\u5BC6\u94A5\u6587\u4EF6";
|
|
175
|
+
});
|
|
176
|
+
} }),
|
|
177
|
+
ui.button("\u5237\u65B0\u5BC6\u94A5\u72B6\u6001", { onClick: () => {
|
|
178
|
+
void keyAction(async () => {
|
|
179
|
+
showKey((await ctx.invoke("state")).keySet);
|
|
180
|
+
void ctx.refresh().catch(() => void 0);
|
|
181
|
+
return "\u5DF2\u5237\u65B0\u5BC6\u94A5\u72B6\u6001";
|
|
182
|
+
});
|
|
183
|
+
} }),
|
|
184
|
+
keyStatus
|
|
185
|
+
);
|
|
186
|
+
showKey(state.keySet);
|
|
187
|
+
const steps = ui.h("ol");
|
|
188
|
+
steps.style.cssText = "list-style:decimal;padding-left:24px;margin:6px 0;line-height:1.8";
|
|
189
|
+
const secret = ui.h("code", "mono", `${SURFING_MICROLINK_SECRET}=`);
|
|
190
|
+
for (const parts of [
|
|
191
|
+
["\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"],
|
|
192
|
+
["\u5728\u63A7\u5236\u53F0\u5DE6\u4FA7\u9009\u62E9\u300COverview\u300D\uFF0C\u70B9\u51FB\u300CUsage\u300D\u6807\u7B7E\u3002"],
|
|
193
|
+
["\u70B9\u51FB\u300CDevelopment\u300D\u6807\u7B7E\u53F3\u4E0A\u89D2\u7684\u300C\u2026\u300D\uFF0C\u590D\u5236 API Key\u3002"],
|
|
194
|
+
["\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"],
|
|
195
|
+
["\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"]
|
|
196
|
+
]) {
|
|
197
|
+
const step = ui.h("li");
|
|
198
|
+
step.append(...parts);
|
|
199
|
+
steps.append(step);
|
|
200
|
+
}
|
|
201
|
+
const limits = ui.h("p", "muted");
|
|
202
|
+
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");
|
|
203
|
+
const microlink = ui.sheet({
|
|
204
|
+
title: "Microlink \u8FDC\u7A0B\u6E32\u67D3",
|
|
205
|
+
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"
|
|
206
|
+
});
|
|
207
|
+
const help = ui.foldSheet("surfing-microlink-key", { title: "\u5982\u4F55\u53D6\u5F97 API Key", defaultOpen: false });
|
|
208
|
+
help.body.append(steps, limits);
|
|
209
|
+
microlink.body.append(keyBar, help.el);
|
|
210
|
+
layout.append(microlink.el);
|
|
211
|
+
ctx.root.replaceChildren(layout);
|
|
212
|
+
}
|
|
213
|
+
var client_default = { panels: { config: { mount } } };
|
|
214
|
+
export {
|
|
215
|
+
client_default as default
|
|
216
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cortico-world-surfing",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "读取公开网页正文和 B站视频简介、字幕、评论,支持视频搜索、分段续读和可调返回量。",
|
|
6
6
|
"author": "DTMosken",
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
"browser",
|
|
28
28
|
"video"
|
|
29
29
|
],
|
|
30
|
-
"describe": "describe.json"
|
|
30
|
+
"describe": "describe.json",
|
|
31
|
+
"webUiClient": "dist/webui.js"
|
|
31
32
|
},
|
|
32
33
|
"files": [
|
|
33
34
|
"src",
|
|
@@ -43,6 +44,8 @@
|
|
|
43
44
|
"scripts": {
|
|
44
45
|
"test": "node scripts/build-engine.mjs && vitest run",
|
|
45
46
|
"build:engine": "node scripts/build-engine.mjs",
|
|
47
|
+
"build:web": "esbuild src/webui/client.ts --bundle --format=esm --outfile=dist/webui.js",
|
|
48
|
+
"typecheck:web": "tsc -p tsconfig.web.json --noEmit",
|
|
46
49
|
"typecheck": "tsc --noEmit",
|
|
47
50
|
"install:browser": "node scripts/install-browser.mjs"
|
|
48
51
|
},
|
package/src/config.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { ConfigGroup } from 'cortico/core/types.ts';
|
|
2
2
|
|
|
3
|
+
export const SURFING_MICROLINK_SECRET = 'CORTICO_SURFING_MICROLINK_API_KEY';
|
|
4
|
+
|
|
3
5
|
export interface SurfingConfigSection {
|
|
4
6
|
enabled: boolean;
|
|
5
7
|
reading: { maxResponseEstimatedTokens: number; maxSourceChars: number };
|
|
@@ -18,7 +20,7 @@ export const SURFING_DEFAULTS: SurfingConfigSection = {
|
|
|
18
20
|
|
|
19
21
|
export const SURFING_CONFIG_GROUP: ConfigGroup = {
|
|
20
22
|
id: 'world:surfing', owner: 'world:surfing',
|
|
21
|
-
schema: { type: 'object', title: '
|
|
23
|
+
schema: { type: 'object', title: '阅读与字幕', properties: {
|
|
22
24
|
'worlds.surfing.reading.maxResponseEstimatedTokens': {
|
|
23
25
|
type: 'integer', title: '单次返回上限', minimum: 1024, maximum: 32768,
|
|
24
26
|
'x-suffix': '估算 token', 'x-hot': true,
|
|
@@ -66,6 +68,12 @@ export const SURFING_LIMITS_CONFIG_GROUP: ConfigGroup = {
|
|
|
66
68
|
} },
|
|
67
69
|
};
|
|
68
70
|
|
|
71
|
+
export const SURFING_CONFIG_GROUPS = [SURFING_CONFIG_GROUP, SURFING_LIMITS_CONFIG_GROUP];
|
|
72
|
+
|
|
73
|
+
export function configValue(config: SurfingConfigSection, path: string): unknown {
|
|
74
|
+
return path.split('.').slice(2).reduce<unknown>((part, key) => (part as Record<string, unknown>)?.[key], config);
|
|
75
|
+
}
|
|
76
|
+
|
|
69
77
|
export function applyBiliDefaults(config: SurfingConfigSection): void {
|
|
70
78
|
if (config.bili.maxSubtitleRetries === undefined) config.bili.maxSubtitleRetries = SURFING_DEFAULTS.bili.maxSubtitleRetries;
|
|
71
79
|
if (config.bili.subtitleRetryDelayMs === undefined) config.bili.subtitleRetryDelayMs = SURFING_DEFAULTS.bili.subtitleRetryDelayMs;
|
|
@@ -73,9 +81,9 @@ export function applyBiliDefaults(config: SurfingConfigSection): void {
|
|
|
73
81
|
|
|
74
82
|
export function validateConfig(config: SurfingConfigSection): void {
|
|
75
83
|
applyBiliDefaults(config);
|
|
76
|
-
for (const group of
|
|
84
|
+
for (const group of SURFING_CONFIG_GROUPS) {
|
|
77
85
|
for (const [path, schema] of Object.entries(group.schema.properties)) {
|
|
78
|
-
const value =
|
|
86
|
+
const value = configValue(config, path);
|
|
79
87
|
if (typeof value !== 'number' || !Number.isInteger(value) || value < schema.minimum! || value > schema.maximum!)
|
|
80
88
|
throw new Error(`${schema.title}超出配置范围。`);
|
|
81
89
|
}
|
package/src/microlink.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { ReadError, validatePublicUrl, type ReadOperation } from './network.ts';
|
|
2
|
+
|
|
3
|
+
export class MicrolinkRenderer {
|
|
4
|
+
constructor(private readonly apiKey: () => string = () => '') {}
|
|
5
|
+
|
|
6
|
+
async render(input: string, operation: ReadOperation, maxBytes: number): Promise<{ html: string; url: string }> {
|
|
7
|
+
const target = validatePublicUrl(input);
|
|
8
|
+
const key = this.apiKey().trim();
|
|
9
|
+
const endpoint = new URL(key ? 'https://pro.microlink.io/' : 'https://api.microlink.io/');
|
|
10
|
+
endpoint.search = new URLSearchParams({ url: target.href, 'data.html.attr': 'html', meta: 'false', prerender: 'true' }).toString();
|
|
11
|
+
const response = await operation.get(endpoint.href, {
|
|
12
|
+
headers: key ? { 'x-api-key': key } : undefined,
|
|
13
|
+
allowHost: hostname => hostname === endpoint.hostname,
|
|
14
|
+
});
|
|
15
|
+
if (response.status === 429) throw new ReadError('access_denied', 'Microlink 请求额度已用完或请求过于频繁,请等待额度重置或检查套餐。');
|
|
16
|
+
if (response.status === 401 || response.status === 403)
|
|
17
|
+
throw new ReadError('access_denied', 'Microlink 未允许本次读取,请检查可选 API Key 和访问权限。');
|
|
18
|
+
if (response.status !== 200) throw new ReadError('network_error', `Microlink 读取失败(HTTP ${response.status})。`);
|
|
19
|
+
let result: { status?: string; code?: string; statusCode?: number; data?: { html?: string; url?: string } };
|
|
20
|
+
try { result = JSON.parse(response.body.toString('utf8')); }
|
|
21
|
+
catch { throw new ReadError('protocol_error', 'Microlink 未返回有效 JSON。'); }
|
|
22
|
+
if (result?.status !== 'success') {
|
|
23
|
+
if (result?.code === 'EPROXYNEEDED') throw new ReadError('access_denied', '该网页需要 Microlink 代理,请配置 API Key 后重试。');
|
|
24
|
+
throw new ReadError('content_unavailable', 'Microlink 未取得渲染后的网页。');
|
|
25
|
+
}
|
|
26
|
+
if (result.statusCode !== 200)
|
|
27
|
+
throw new ReadError(result.statusCode === 404 ? 'not_found' : 'access_denied', '网页未允许本次 Microlink 公开读取。');
|
|
28
|
+
if (typeof result.data?.html !== 'string' || typeof result.data.url !== 'string')
|
|
29
|
+
throw new ReadError('protocol_error', 'Microlink 响应缺少 HTML 或最终网页地址。');
|
|
30
|
+
if (Buffer.byteLength(result.data.html) > maxBytes) throw new ReadError('source_limit', '渲染后的 HTML 超出单次读取上限。');
|
|
31
|
+
const url = validatePublicUrl(result.data.url);
|
|
32
|
+
if (!url.hash) url.hash = target.hash;
|
|
33
|
+
return { html: result.data.html, url: url.href };
|
|
34
|
+
}
|
|
35
|
+
}
|
package/src/network.ts
CHANGED
|
@@ -76,7 +76,7 @@ export function requestHeaders(options: GetOptions): Record<string, string> {
|
|
|
76
76
|
'accept-encoding': 'gzip, deflate, br', accept: '*/*', 'accept-language': '*',
|
|
77
77
|
};
|
|
78
78
|
for (const [key, value] of Object.entries(options.headers ?? {})) {
|
|
79
|
-
if (['accept', 'accept-language', 'referer', 'user-agent'].includes(key.toLowerCase())) headers[key.toLowerCase()] = value;
|
|
79
|
+
if (['accept', 'accept-language', 'referer', 'user-agent', 'x-api-key'].includes(key.toLowerCase())) headers[key.toLowerCase()] = value;
|
|
80
80
|
}
|
|
81
81
|
return headers;
|
|
82
82
|
}
|
package/src/page-reader.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { ReadError, asReadError, validatePublicUrl, waitWithSignal, type Failure
|
|
|
3
3
|
import { createServer, type Server } from 'node:net';
|
|
4
4
|
import { chromium, type Browser } from 'playwright';
|
|
5
5
|
import type { Material } from './snapshots.ts';
|
|
6
|
+
import { MicrolinkRenderer } from './microlink.ts';
|
|
6
7
|
|
|
7
8
|
const ENGINE = new URL('../dist/page-engine.mjs', import.meta.url);
|
|
8
9
|
export function pageKey(input: string): string { return 'page:' + validatePublicUrl(input).href; }
|
|
@@ -27,7 +28,7 @@ function extract(html: string, url: string, signal: AbortSignal): Promise<Materi
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
export class PageReader {
|
|
30
|
-
constructor(private readonly renderer = new Renderer()) {}
|
|
31
|
+
constructor(private readonly renderer = new Renderer(), private readonly microlink = new MicrolinkRenderer()) {}
|
|
31
32
|
|
|
32
33
|
async read(input: string, operation: ReadOperation, maxBytes: number): Promise<Material> {
|
|
33
34
|
const original = validatePublicUrl(input);
|
|
@@ -65,10 +66,19 @@ export class PageReader {
|
|
|
65
66
|
catch (error) {
|
|
66
67
|
if (!(error instanceof ReadError) || error.kind !== 'content_unavailable') throw error;
|
|
67
68
|
try {
|
|
68
|
-
|
|
69
|
+
let rendered: { html: string; url: string };
|
|
70
|
+
let provider = 'chromium';
|
|
71
|
+
try { rendered = await this.renderer.render(target.href, operation, maxBytes, response); }
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (operation.signal.aborted || !(error instanceof ReadError) || error.kind !== 'browser_unavailable') throw error;
|
|
74
|
+
rendered = await this.microlink.render(target.href, operation, maxBytes);
|
|
75
|
+
provider = 'microlink';
|
|
76
|
+
}
|
|
77
|
+
this.rejectVideo(validatePublicUrl(rendered.url));
|
|
69
78
|
material = await extract(rendered.html, rendered.url, operation.signal);
|
|
70
79
|
delete material.scope.mayNeedRendering;
|
|
71
80
|
material.scope.extraction = 'rendered-html';
|
|
81
|
+
material.scope.renderingProvider = provider;
|
|
72
82
|
} catch (renderError) {
|
|
73
83
|
if (!staticMaterial || operation.signal.aborted) throw renderError;
|
|
74
84
|
const failure = asReadError(renderError);
|
|
@@ -0,0 +1,24 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { WebUiClientBundle, WebUiPanelContext } from 'cortico/web/shared/client-panel.ts';
|
|
2
|
+
import { SURFING_CONFIG_GROUPS, SURFING_LIMITS_CONFIG_GROUP, SURFING_MICROLINK_SECRET } from '../config.ts';
|
|
3
|
+
|
|
4
|
+
interface ConfigState { values: Record<string, number>; keySet: boolean }
|
|
5
|
+
|
|
6
|
+
const errorText = (error: unknown): string => error instanceof Error ? error.message : '操作失败';
|
|
7
|
+
|
|
8
|
+
async function mount(ctx: WebUiPanelContext): Promise<void> {
|
|
9
|
+
const { ui } = ctx;
|
|
10
|
+
const state = await ctx.invoke<ConfigState>('state');
|
|
11
|
+
const layout = ui.h('div');
|
|
12
|
+
layout.style.cssText = 'display:grid;gap:20px;max-width:960px;margin:auto';
|
|
13
|
+
let saves: Promise<void> = Promise.resolve();
|
|
14
|
+
for (const group of SURFING_CONFIG_GROUPS) {
|
|
15
|
+
const sheet = group === SURFING_LIMITS_CONFIG_GROUP
|
|
16
|
+
? ui.foldSheet('surfing-limits', { title: group.schema.title, defaultOpen: false })
|
|
17
|
+
: ui.sheet({ title: group.schema.title });
|
|
18
|
+
const grid = ui.h('div');
|
|
19
|
+
grid.style.cssText = 'display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,320px),1fr));gap:16px 24px';
|
|
20
|
+
const saved = ui.msgline('');
|
|
21
|
+
for (const [path, property] of Object.entries(group.schema.properties)) {
|
|
22
|
+
const scale = property['x-scale'] ?? 1;
|
|
23
|
+
const input = ui.input({ type: 'number', value: String(state.values[path]! / scale), onChange: value => {
|
|
24
|
+
saves = saves.then(async () => {
|
|
25
|
+
const next = await ctx.invoke<ConfigState>('save', [path, Number(value) * scale]);
|
|
26
|
+
if (input.value === value) input.value = String(next.values[path]! / scale);
|
|
27
|
+
saved.textContent = `已保存:${property.title}`; saved.classList.remove('bad');
|
|
28
|
+
void ctx.refresh().catch(() => undefined);
|
|
29
|
+
}).catch(error => { saved.textContent = errorText(error); saved.classList.add('bad'); });
|
|
30
|
+
} });
|
|
31
|
+
input.min = String(property.minimum! / scale);
|
|
32
|
+
input.max = String(property.maximum! / scale);
|
|
33
|
+
const field = ui.field(`${property.title}(${property['x-suffix']})`, input);
|
|
34
|
+
const note = ui.h('small', 'muted', property.description);
|
|
35
|
+
note.id = path;
|
|
36
|
+
input.setAttribute('aria-describedby', note.id);
|
|
37
|
+
const row = ui.h('div');
|
|
38
|
+
row.append(field, note);
|
|
39
|
+
grid.append(row);
|
|
40
|
+
}
|
|
41
|
+
sheet.body.append(grid, saved);
|
|
42
|
+
layout.append(sheet.el);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const anchor = (text: string, href: string): HTMLAnchorElement => {
|
|
46
|
+
const link = ui.h('a', null, text);
|
|
47
|
+
link.href = href; link.target = '_blank'; link.rel = 'noopener noreferrer';
|
|
48
|
+
return link;
|
|
49
|
+
};
|
|
50
|
+
const keyStatus = ui.msgline('');
|
|
51
|
+
let keyPill = ui.pill('', 'off');
|
|
52
|
+
const showKey = (keySet: boolean): void => {
|
|
53
|
+
const next = ui.pill(keySet ? 'API Key 已配置' : '免 Key', keySet ? 'on' : 'off');
|
|
54
|
+
keyPill.replaceWith(next); keyPill = next;
|
|
55
|
+
};
|
|
56
|
+
async function keyAction(run: () => Promise<string>): Promise<void> {
|
|
57
|
+
try { keyStatus.textContent = await run(); keyStatus.classList.remove('bad'); }
|
|
58
|
+
catch (error) { keyStatus.textContent = errorText(error); keyStatus.classList.add('bad'); }
|
|
59
|
+
}
|
|
60
|
+
const keyBar = ui.rowbar();
|
|
61
|
+
keyBar.append(keyPill,
|
|
62
|
+
ui.button('打开密钥文件', { onClick: () => { void keyAction(async () => { await ctx.invoke('openKeyFile'); return '已打开密钥文件'; }); } }),
|
|
63
|
+
ui.button('刷新密钥状态', { onClick: () => { void keyAction(async () => {
|
|
64
|
+
showKey((await ctx.invoke<ConfigState>('state')).keySet);
|
|
65
|
+
void ctx.refresh().catch(() => undefined);
|
|
66
|
+
return '已刷新密钥状态';
|
|
67
|
+
}); } }), keyStatus);
|
|
68
|
+
showKey(state.keySet);
|
|
69
|
+
|
|
70
|
+
const steps = ui.h('ol');
|
|
71
|
+
steps.style.cssText = 'list-style:decimal;padding-left:24px;margin:6px 0;line-height:1.8';
|
|
72
|
+
const secret = ui.h('code', 'mono', `${SURFING_MICROLINK_SECRET}=`);
|
|
73
|
+
for (const parts of [
|
|
74
|
+
['打开', anchor('Microlink 注册页', 'https://dashboard.microlink.io/signup'), ',注册账号并登录,无需购买付费套餐。'],
|
|
75
|
+
['在控制台左侧选择「Overview」,点击「Usage」标签。'],
|
|
76
|
+
['点击「Development」标签右上角的「…」,复制 API Key。'],
|
|
77
|
+
['点击「打开密钥文件」,将 Key 填在 ', secret, ' 后面,不加引号,保存文件。'],
|
|
78
|
+
['保存后,下次远程读取使用该 Key 的额度。点击「刷新密钥状态」可查看配置状态。清空 Key 的值并保存可恢复免 Key 调用。'],
|
|
79
|
+
]) {
|
|
80
|
+
const step = ui.h('li');
|
|
81
|
+
step.append(...parts);
|
|
82
|
+
steps.append(step);
|
|
83
|
+
}
|
|
84
|
+
const limits = ui.h('p', 'muted');
|
|
85
|
+
limits.append('免 Key 和免费 Key 的额度不同,当前限制见', anchor('Microlink 额度说明', 'https://microlink.io/docs/api/basics/rate-limit'), '。');
|
|
86
|
+
const microlink = ui.sheet({
|
|
87
|
+
title: 'Microlink 远程渲染',
|
|
88
|
+
desc: '本机 Chromium 不可用时,将网页 URL 交给 Microlink 渲染。API Key 可选,留空使用免 Key 额度。',
|
|
89
|
+
});
|
|
90
|
+
const help = ui.foldSheet('surfing-microlink-key', { title: '如何取得 API Key', defaultOpen: false });
|
|
91
|
+
help.body.append(steps, limits);
|
|
92
|
+
microlink.body.append(keyBar, help.el);
|
|
93
|
+
layout.append(microlink.el);
|
|
94
|
+
ctx.root.replaceChildren(layout);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export default { panels: { config: { mount } } } satisfies WebUiClientBundle;
|
package/src/world.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { coerceGroupValues } from 'cortico/core/config-schema.ts';
|
|
4
5
|
import type { ToolDef, ToolSpec, ToolOutcome, ToolCallContext, World, WorldWebUiDecl, WorldHost } from 'cortico/core/types.ts';
|
|
5
6
|
import type { WorldContext } from 'cortico/world.ts';
|
|
6
|
-
import {
|
|
7
|
+
import { SURFING_CONFIG_GROUPS, SURFING_MICROLINK_SECRET, applyBiliDefaults, configValue, validateConfig, type SurfingConfigSection } from './config.ts';
|
|
7
8
|
import { BiliClient, biliKey, biliCommentsKey, type BiliInput, type BiliCommentsInput } from './bili.ts';
|
|
8
9
|
import { ReadError, asReadError, PublicClient, waitWithSignal, type ReadOperation } from './network.ts';
|
|
9
10
|
import { PageReader, pageKey } from './page-reader.ts';
|
|
10
11
|
import { SnapshotCache, estimateTokens, type Material } from './snapshots.ts';
|
|
12
|
+
import { MicrolinkRenderer } from './microlink.ts';
|
|
13
|
+
import { prepareKeyFile, openSecretFile } from './secret-file.ts';
|
|
11
14
|
|
|
12
15
|
const ENV_PROMPT_FILE = fileURLToPath(new URL('./ENV_PROMPT.md', import.meta.url));
|
|
13
16
|
|
|
@@ -68,14 +71,16 @@ export class SurfingWorld implements World {
|
|
|
68
71
|
readonly id = 'surfing';
|
|
69
72
|
private readonly cache = new SnapshotCache();
|
|
70
73
|
private readonly bili = new BiliClient();
|
|
71
|
-
private readonly pages
|
|
74
|
+
private readonly pages: PageReader;
|
|
72
75
|
private readonly pendingBatches = new Map<string,{promise:Promise<string>;signal:AbortSignal}>();
|
|
73
76
|
private lifecycle = new AbortController();
|
|
74
77
|
private stopped = false;
|
|
75
78
|
private last?: { status: string; failed: boolean; estimatedTokens: number; sourceTruncated: boolean };
|
|
76
79
|
|
|
77
|
-
constructor(private readonly ctx: WorldContext<SurfingConfigSection>, private readonly client = new PublicClient()
|
|
80
|
+
constructor(private readonly ctx: WorldContext<SurfingConfigSection>, private readonly client = new PublicClient(),
|
|
81
|
+
private readonly openFile = openSecretFile) {
|
|
78
82
|
applyBiliDefaults(ctx.cfg);
|
|
83
|
+
this.pages = new PageReader(undefined, new MicrolinkRenderer(() => ctx.secret(SURFING_MICROLINK_SECRET)));
|
|
79
84
|
}
|
|
80
85
|
|
|
81
86
|
envPromptVars(): Record<string, string> {
|
|
@@ -184,6 +189,22 @@ export class SurfingWorld implements World {
|
|
|
184
189
|
}
|
|
185
190
|
}
|
|
186
191
|
|
|
192
|
+
private configState(): { values: Record<string, unknown>; keySet: boolean } {
|
|
193
|
+
const paths = SURFING_CONFIG_GROUPS.flatMap(group => Object.keys(group.schema.properties));
|
|
194
|
+
return { values: Object.fromEntries(paths.map(path => [path, configValue(this.ctx.cfg, path)])),
|
|
195
|
+
keySet: !!this.ctx.secret(SURFING_MICROLINK_SECRET).trim() };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private saveConfig(path: unknown, value: unknown): void {
|
|
199
|
+
const group = SURFING_CONFIG_GROUPS.find(candidate => typeof path === 'string' && Object.hasOwn(candidate.schema.properties, path));
|
|
200
|
+
if (!group) throw new Error('未知配置项。');
|
|
201
|
+
const key = path as string;
|
|
202
|
+
const result = coerceGroupValues(group, { [key]: value });
|
|
203
|
+
if ('error' in result) throw new Error(result.error);
|
|
204
|
+
const [section, field] = key.split('.').slice(2);
|
|
205
|
+
this.ctx.persist({ [section]: { [field]: result.values[key] } });
|
|
206
|
+
}
|
|
207
|
+
|
|
187
208
|
webUi(): WorldWebUiDecl {
|
|
188
209
|
const override = join(this.ctx.botDir, 'prompts', 'worlds', 'surfing', 'ENV_PROMPT.md');
|
|
189
210
|
return {
|
|
@@ -196,7 +217,17 @@ export class SurfingWorld implements World {
|
|
|
196
217
|
{ label: '源材料', value: this.last ? (this.last.sourceTruncated ? '触及保留上限' : '未触及保留上限') : '尚未读取' },
|
|
197
218
|
{ label: '文本缓存', value: Math.ceil(this.cache.sizeBytes / 1024) + ' KiB' },
|
|
198
219
|
],
|
|
199
|
-
|
|
220
|
+
panels: [{ id: 'config', title: '配置' }],
|
|
221
|
+
invoke: async (panel, method, args) => {
|
|
222
|
+
if (panel !== 'config') throw new Error('未知面板。');
|
|
223
|
+
if (method === 'state') return this.configState();
|
|
224
|
+
if (method === 'save') { this.saveConfig(args[0], args[1]); return this.configState(); }
|
|
225
|
+
if (method === 'openKeyFile') {
|
|
226
|
+
const file = prepareKeyFile(this.ctx.botDir);
|
|
227
|
+
await this.openFile(file); return { file };
|
|
228
|
+
}
|
|
229
|
+
throw new Error('未知操作。');
|
|
230
|
+
},
|
|
200
231
|
promptDocs: [{ key: 'worlds.surfing.envPrompt', title: '网上冲浪环境提示词', description: '网页、视频与学习工具的使用指导。',
|
|
201
232
|
path: existsSync(override) ? override : ENV_PROMPT_FILE, deploymentPath: override, role: 'envPrompt',
|
|
202
233
|
vars: [
|