cortico 0.1.0 → 0.1.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/README.md +7 -14
- package/package.json +1 -3
- package/src/boot.ts +18 -0
- package/src/bot.ts +41 -6
- package/src/core/README.md +4 -6
- package/src/core/blobs.ts +23 -2
- package/src/core/config-schema.ts +1 -1
- package/src/core/config.ts +2 -16
- package/src/core/core.ts +15 -5
- package/src/core/loop.ts +55 -27
- package/src/core/session.ts +3 -2
- package/src/core/timers.ts +8 -6
- package/src/core/types.ts +19 -3
- package/src/core/util.ts +17 -1
- package/src/deploy.ts +4 -1
- package/src/extensions/README.md +6 -4
- package/src/extensions/dry-mount.ts +23 -2
- package/src/extensions/manifest.ts +21 -11
- package/src/extensions.ts +147 -23
- package/src/launcher.ts +23 -9
- package/src/protocol/open-responses/context-log.ts +37 -9
- package/src/providers/README.md +32 -8
- package/src/providers/base.ts +3 -1
- package/src/providers/configuration.ts +2 -1
- package/src/providers/console/hub.ts +270 -0
- package/src/providers/console/settings.ts +8 -18
- package/src/providers/console/types.ts +5 -0
- package/src/providers/hub-api.ts +3 -0
- package/src/providers/llamacpp/config.ts +8 -6
- package/src/providers/llamacpp/console/runtime-panel.ts +5 -1
- package/src/providers/llamacpp/console/server.ts +3 -1
- package/src/providers/llamacpp/index.ts +3 -1
- package/src/providers/llamacpp/native.ts +10 -5
- package/src/providers/llamacpp/strings.ts +6 -6
- package/src/providers/name.ts +8 -0
- package/src/providers/openai-responses-compat/config.ts +13 -0
- package/src/providers/openai-responses-compat/console/client.ts +5 -0
- package/src/providers/openai-responses-compat/console/reasoning-panel.ts +84 -0
- package/src/providers/openai-responses-compat/console/server.ts +127 -0
- package/src/providers/openai-responses-compat/index.ts +38 -13
- package/src/providers/openai-responses-compat/native.ts +9 -4
- package/src/providers/openai-responses-compat/strings.ts +62 -1
- package/src/providers/registry.ts +5 -0
- package/src/providers/transport/responses-input.ts +59 -10
- package/src/web/README.md +11 -6
- package/src/web/auth.ts +80 -0
- package/src/web/client/console-pages/builtins/llm-settings/panel.ts +5 -4
- package/src/web/client/console-pages/builtins/llm-settings/pricing-panel.ts +22 -8
- package/src/web/client/console-pages/builtins/llm-settings/strings.ts +6 -8
- package/src/web/client/console-pages/host.ts +24 -7
- package/src/web/client/core/api.ts +5 -1
- package/src/web/client/core/router.ts +15 -0
- package/src/web/client/features/core/strings.ts +2 -2
- package/src/web/client/features/extensions/index.ts +276 -41
- package/src/web/client/features/extensions/strings.ts +84 -10
- package/src/web/client/features/feature.ts +2 -2
- package/src/web/client/features/live/diagnostics.ts +32 -0
- package/src/web/client/features/live/index.ts +34 -11
- package/src/web/client/features/live/onboarding.ts +4 -1
- package/src/web/client/features/live/protocol.ts +1 -0
- package/src/web/client/features/live/strings.ts +20 -14
- package/src/web/client/features/live/timeline.ts +2 -1
- package/src/web/client/features/providers/detail.ts +262 -0
- package/src/web/client/features/providers/drafts.ts +23 -0
- package/src/web/client/features/providers/index.ts +173 -87
- package/src/web/client/features/providers/strings.ts +44 -17
- package/src/web/client/features/providers/types.ts +15 -0
- package/src/web/client/features/settings/general.ts +13 -0
- package/src/web/client/features/settings/index.ts +1 -0
- package/src/web/client/features/settings/strings.ts +6 -0
- package/src/web/client/features/worlds/index.ts +1 -0
- package/src/web/client/main.ts +4 -0
- package/src/web/client/shell/index.ts +28 -48
- package/src/web/client/shell/strings.ts +0 -22
- package/src/web/client/ui/icons.ts +14 -1
- package/src/web/client/ui/prompt-input.tsx +17 -5
- package/src/web/client/ui/strings.ts +0 -2
- package/src/web/console-pages.ts +1 -1
- package/src/web/diagnostics.ts +133 -0
- package/src/web/public/login.html +67 -0
- package/src/web/public/styles.css +143 -26
- package/src/web/server.ts +233 -19
- package/src/web/shared/client-panel.ts +4 -1
- package/src/web/shared/console-protocol.ts +4 -1
- package/src/worlds/bilibili/README.md +1 -1
- package/src/worlds/bilibili/overlay/server.ts +4 -2
- package/src/worlds/minecraft/ADAPT.md +66 -0
- package/src/worlds/minecraft/README.md +69 -11
- package/src/worlds/minecraft/cell-facts.ts +226 -0
- package/src/worlds/minecraft/chests.ts +5 -0
- package/src/worlds/minecraft/containers.ts +325 -0
- package/src/worlds/minecraft/entity-facts.ts +31 -5
- package/src/worlds/minecraft/executor.ts +283 -10348
- package/src/worlds/minecraft/inventory.ts +268 -0
- package/src/worlds/minecraft/melee.ts +419 -0
- package/src/worlds/minecraft/mineflayer-fixes.ts +55 -1
- package/src/worlds/minecraft/placed-ledger.ts +152 -0
- package/src/worlds/minecraft/placement.ts +1038 -0
- package/src/worlds/minecraft/receipt.ts +340 -0
- package/src/worlds/minecraft/skill-context.ts +358 -0
- package/src/worlds/minecraft/skills-build.ts +1169 -0
- package/src/worlds/minecraft/skills-container.ts +1343 -0
- package/src/worlds/minecraft/skills-craft.ts +333 -0
- package/src/worlds/minecraft/skills-dig.ts +624 -0
- package/src/worlds/minecraft/skills-gather.ts +1230 -0
- package/src/worlds/minecraft/skills-interact.ts +1559 -0
- package/src/worlds/minecraft/tools.ts +331 -0
- package/src/worlds/minecraft/travel.ts +763 -0
- package/src/worlds/minecraft/until.ts +75 -0
- package/src/worlds/qq/normalize.ts +14 -0
- package/src/worlds/qq/world.ts +53 -7
- package/src/worlds/terminal/world.ts +5 -3
package/src/web/server.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ProviderHubApi as ProviderHub } from '../providers/hub-api.ts';
|
|
1
2
|
import type { ContextRecord } from '../protocol/open-responses/context.ts';
|
|
2
3
|
import type { EnvPromptOrigin } from '../core/prefix.ts';
|
|
3
4
|
/**
|
|
@@ -33,6 +34,7 @@ const SERVER_TEXT = {
|
|
|
33
34
|
paused: '已暂停:事件照常落库排队,不投递唤醒',
|
|
34
35
|
resumed: '已继续:积压事件一次性投递',
|
|
35
36
|
exitSupervised: '进程即将退出,启动器随即重新拉起',
|
|
37
|
+
exitSupervisedPaused: '进程即将退出,启动器随即重新拉起;回来时事件投递是暂停的,要在运行状态里按继续',
|
|
36
38
|
exitUnsupervised: '进程即将退出;没有检测到启动器循环,需要手动重新启动',
|
|
37
39
|
shutdownSkipped: (n: number, labels: string[]) => `本地关机完成,但有 ${n} 步没走完:${labels.join('、')}`,
|
|
38
40
|
shutdownComplete: (n: number) => `本地关机完成(${n} 步全部走完)`,
|
|
@@ -45,6 +47,7 @@ const SERVER_TEXT = {
|
|
|
45
47
|
paused: 'Paused: events are still stored and queued, no wake is delivered',
|
|
46
48
|
resumed: 'Resumed: the backlog is delivered in one batch',
|
|
47
49
|
exitSupervised: 'The process is about to exit; the launcher will start it again',
|
|
50
|
+
exitSupervisedPaused: 'The process is about to exit; the launcher will start it again with event delivery paused, so resume it in the run status',
|
|
48
51
|
exitUnsupervised: 'The process is about to exit; no launcher loop was detected, so it must be started again by hand',
|
|
49
52
|
shutdownSkipped: (n: number, labels: string[]) => `Local shutdown finished, but ${n} step(s) did not complete: ${labels.join(', ')}`,
|
|
50
53
|
shutdownComplete: (n: number) => `Local shutdown finished (all ${n} steps completed)`,
|
|
@@ -55,12 +58,14 @@ const SERVER_TEXT = {
|
|
|
55
58
|
},
|
|
56
59
|
};
|
|
57
60
|
import { logPredicate, readRunsIndex, readTailRecordsWhere } from './files.ts';
|
|
61
|
+
import { AUTH_KEY_FILE, ConsoleAuth, SESSION_COOKIE, SESSION_COOKIE_MAX_AGE_SEC, cookieValue } from './auth.ts';
|
|
62
|
+
import { buildDiagnostics, DIAGNOSTICS_TAIL } from './diagnostics.ts';
|
|
58
63
|
import { ConsoleAssets, ConsolePageRegistry, type ConsolePageSource } from './console-pages.ts';
|
|
59
64
|
import { THEME_FILE, readDeploymentTheme, writeDeploymentTheme } from './theme-store.ts';
|
|
60
65
|
import { THEME_SCRIPT_ID, type InjectedTheme, type StoredTheme } from './shared/theme.ts';
|
|
61
66
|
import { EXTENSION_ASSET_PREFIX, extensionAssetSegment, type ExtensionConsoleAsset } from '../extensions/manifest.ts';
|
|
62
67
|
import {
|
|
63
|
-
CONSOLE_LAMPS_ROUTE, CONSOLE_LANGUAGE_HEADER, CONSOLE_LANGUAGE_QUERY, CONSOLE_PROTOCOL_VERSION,
|
|
68
|
+
CONSOLE_AUTH_HEADER, CONSOLE_LAMPS_ROUTE, CONSOLE_LANGUAGE_HEADER, CONSOLE_LANGUAGE_QUERY, CONSOLE_PROTOCOL_VERSION,
|
|
64
69
|
PROVIDERS_LAMP_ID,
|
|
65
70
|
isBinaryResult, isFileResult,
|
|
66
71
|
type ConsoleFileResult, type ConsoleLamp, type ConsoleStream,
|
|
@@ -192,28 +197,76 @@ export interface ExtensionInfo {
|
|
|
192
197
|
state: 'loaded' | 'failed' | 'pending-restart' | 'removed' | 'idle';
|
|
193
198
|
}
|
|
194
199
|
|
|
200
|
+
/**
|
|
201
|
+
* 搜索结果里的一条。字段全部来自 registry 的搜索端点,够列表排序与筛选;
|
|
202
|
+
* 契约版本、体积、版本史这些要另取包文档,见 `ExtensionPackageDetail`。
|
|
203
|
+
*/
|
|
195
204
|
export interface ExtensionSearchHit {
|
|
196
205
|
name: string;
|
|
197
206
|
version: string;
|
|
198
207
|
description: string;
|
|
208
|
+
/** 这个版本的发布时间 */
|
|
199
209
|
date?: string;
|
|
200
210
|
publisher?: string;
|
|
211
|
+
license?: string;
|
|
212
|
+
keywords?: string[];
|
|
201
213
|
/** 月下载量 */
|
|
202
214
|
downloads: number;
|
|
215
|
+
/** npm 上依赖这个包的包数 */
|
|
216
|
+
dependents: number;
|
|
203
217
|
links: { npm?: string; repository?: string; homepage?: string };
|
|
204
218
|
installed: boolean;
|
|
205
219
|
/** 按哪一类关键字搜到的(`cortico-world` → world,`cortico-provider` → provider,`cortico-bot` → bot) */
|
|
206
220
|
kind?: 'world' | 'provider' | 'bot';
|
|
207
221
|
}
|
|
208
222
|
|
|
223
|
+
/** 一个 npm 包的详情。操作员点开某条搜索结果时才取。 */
|
|
224
|
+
export interface ExtensionPackageDetail {
|
|
225
|
+
name: string;
|
|
226
|
+
/** dist-tag `latest` 指的版本 */
|
|
227
|
+
version: string;
|
|
228
|
+
description?: string;
|
|
229
|
+
license?: string;
|
|
230
|
+
keywords?: string[];
|
|
231
|
+
/** latest 版本的发布时间 */
|
|
232
|
+
published?: string;
|
|
233
|
+
/** 这个包第一次发布的时间 */
|
|
234
|
+
created?: string;
|
|
235
|
+
versionCount: number;
|
|
236
|
+
/** 最近几个版本,新的在前 */
|
|
237
|
+
history: Array<{ version: string; date: string }>;
|
|
238
|
+
/** npm 上标了 deprecated 时是那句话 */
|
|
239
|
+
deprecated?: string;
|
|
240
|
+
/** package.json 的 cortico 块解析通过时给出 */
|
|
241
|
+
manifest?: { kind: 'world' | 'provider' | 'bot'; api: number; consoleClient?: string; consoleStyle?: string };
|
|
242
|
+
/** 解析不通过的理由,与本机装载时用的是同一套判据 */
|
|
243
|
+
problems?: string[];
|
|
244
|
+
warnings: string[];
|
|
245
|
+
/** 本进程对这个包所属那一类的契约版本;manifest 解析不通过时缺席 */
|
|
246
|
+
frameworkApi?: number;
|
|
247
|
+
/** `engines.node` */
|
|
248
|
+
engines?: string;
|
|
249
|
+
unpackedSize?: number;
|
|
250
|
+
fileCount?: number;
|
|
251
|
+
dependencies: string[];
|
|
252
|
+
maintainers: string[];
|
|
253
|
+
publisher?: string;
|
|
254
|
+
links: { npm: string; repository?: string; homepage?: string; bugs?: string };
|
|
255
|
+
installed: boolean;
|
|
256
|
+
/** 已安装时 extensions/package.json 里写的那个版本范围 */
|
|
257
|
+
installedSpec?: string;
|
|
258
|
+
}
|
|
259
|
+
|
|
209
260
|
/** 装 npm 上的包(可带版本或 dist-tag),或本机一个含 package.json 的目录。 */
|
|
210
261
|
export type ExtensionInstallTarget = { name: string; version?: string } | { path: string };
|
|
211
262
|
|
|
212
263
|
/** 扩展面:清单、搜索、装卸。装卸只改磁盘,加载要重启进程。 */
|
|
213
264
|
export interface WebAppExtensionDeps {
|
|
214
265
|
list(): { dir: string; extensions: ExtensionInfo[] };
|
|
215
|
-
/**
|
|
216
|
-
search(
|
|
266
|
+
/** 该类关键字下 npm 上的全部包;不给 kind = world(`cortico-world`)。 */
|
|
267
|
+
search(kind?: 'world' | 'provider' | 'bot'): Promise<ExtensionSearchHit[]>;
|
|
268
|
+
/** 单个包的详情(另取一次包文档)。 */
|
|
269
|
+
packageInfo(name: string): Promise<ExtensionPackageDetail>;
|
|
217
270
|
/**
|
|
218
271
|
* 已加载扩展的浏览器端产物。服务端据此把页 id 映到 `/assets/extensions/<包>/<版本>/<文件>`
|
|
219
272
|
* 并只发这几个文件;缺席 = 没有扩展带面板。
|
|
@@ -415,6 +468,12 @@ export interface ConsoleSurface {
|
|
|
415
468
|
* 要放到反向代理后面或有意让别的机器访问,才显式换成 `0.0.0.0`。
|
|
416
469
|
*/
|
|
417
470
|
host?: string;
|
|
471
|
+
/** 回环名与监听地址之外还接受的 Host 名;以它为 Origin 的写请求与 WebSocket 同样放行。 */
|
|
472
|
+
allowedHosts?: readonly string[];
|
|
473
|
+
/** 访问密码;缺省或空串时不要求登录。 */
|
|
474
|
+
password?: string;
|
|
475
|
+
/** 登录失败的回应延迟(毫秒);缺省见 FAILED_LOGIN_DELAY_MS,测试里调到 0。 */
|
|
476
|
+
failedLoginDelayMs?: number;
|
|
418
477
|
/** 主循环状态快照(token/截断/梦状态…有什么给什么) */
|
|
419
478
|
getStatus(): Record<string, unknown>;
|
|
420
479
|
/** 调试通道(可选;不挂载时 /ws/debug 拒绝连接) */
|
|
@@ -447,7 +506,10 @@ export interface ConsoleSurface {
|
|
|
447
506
|
dismiss(): void;
|
|
448
507
|
};
|
|
449
508
|
|
|
450
|
-
/**
|
|
509
|
+
/**
|
|
510
|
+
* 可选的运行控制,未挂载时 /api/run/* 返回 503;暂停期间事件仍落库排队。
|
|
511
|
+
* shutdown 按装配层顺序停止投递、IO、托管 LLM server 并落盘,各步有时间预算并返回结果;未提供时不显示关机键,进程退出由装配层决定。
|
|
512
|
+
*/
|
|
451
513
|
run?: {
|
|
452
514
|
pause(): void;
|
|
453
515
|
resume(): void;
|
|
@@ -460,6 +522,8 @@ export interface ConsoleSurface {
|
|
|
460
522
|
*/
|
|
461
523
|
restart?(language: Language): Promise<WebAppShutdownReport>;
|
|
462
524
|
supervised?: boolean;
|
|
525
|
+
/** 重启后是否以暂停态回来;重启回执据此说清楚。 */
|
|
526
|
+
startsPaused?: boolean;
|
|
463
527
|
};
|
|
464
528
|
/** 扩展装卸(可选;不挂载时 /api/extensions* 503)。 */
|
|
465
529
|
extensions?: WebAppExtensionDeps;
|
|
@@ -469,7 +533,10 @@ export interface ConsoleSurface {
|
|
|
469
533
|
* 不挂载时 /api/console/manifest 返回一份只有 framework 能力的空 manifest。
|
|
470
534
|
*/
|
|
471
535
|
consolePageSources?: () => ConsolePageSource[];
|
|
472
|
-
/**
|
|
536
|
+
/**
|
|
537
|
+
* 有没有一个可用的端点。缺省不挂:「模型提供商」那一行不点灯。
|
|
538
|
+
*/
|
|
539
|
+
providers?: ProviderHub;
|
|
473
540
|
providersLamp?: (language: Language) => ConsoleLamp;
|
|
474
541
|
/**
|
|
475
542
|
* 浏览器端构建产物目录(含 asset-manifest.json)。缺省取仓库的 `dist/web`。
|
|
@@ -538,7 +605,7 @@ const hasPort = (h: string): boolean => /:\d+$/.test(h);
|
|
|
538
605
|
const barePort = (h: string): string => h.replace(/:\d+$/, '');
|
|
539
606
|
|
|
540
607
|
/** 按 Host 校验 Origin;缺失 Origin 时放行,null、非法或主机不匹配时拒绝。 */
|
|
541
|
-
function isForeignOrigin(origin: unknown, hostHeader: unknown): boolean {
|
|
608
|
+
function isForeignOrigin(origin: unknown, hostHeader: unknown, allowedHosts: readonly string[] = []): boolean {
|
|
542
609
|
if (typeof origin !== 'string' || origin === '') return false;
|
|
543
610
|
if (origin === 'null') return true; // 沙箱 iframe / file:// 一类,不是本控制台
|
|
544
611
|
let originHost: string;
|
|
@@ -547,6 +614,8 @@ function isForeignOrigin(origin: unknown, hostHeader: unknown): boolean {
|
|
|
547
614
|
} catch {
|
|
548
615
|
return true;
|
|
549
616
|
}
|
|
617
|
+
// 反向代理改写 Host 时 Origin 与 Host 不同名;操作员点名的对外名字照样算本站。
|
|
618
|
+
if (namesHost(allowedHosts, originHost)) return false;
|
|
550
619
|
const host = typeof hostHeader === 'string' ? hostHeader : '';
|
|
551
620
|
if (!originHost || !host) return true;
|
|
552
621
|
if (originHost === host) return false;
|
|
@@ -555,17 +624,35 @@ function isForeignOrigin(origin: unknown, hostHeader: unknown): boolean {
|
|
|
555
624
|
return true;
|
|
556
625
|
}
|
|
557
626
|
|
|
627
|
+
/** 表里的名字可带端口也可不带;不带端口的匹配任意端口。比较不分大小写。 */
|
|
628
|
+
function namesHost(names: readonly string[], host: string): boolean {
|
|
629
|
+
const full = host.toLowerCase();
|
|
630
|
+
const bare = barePort(full);
|
|
631
|
+
return names.some((name) => {
|
|
632
|
+
const n = name.trim().toLowerCase();
|
|
633
|
+
return n !== '' && (n === full || n === bare);
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/** 对端地址是不是本机回环(含 IPv4 映射的 IPv6 写法)。 */
|
|
638
|
+
function isLoopbackPeer(address: string | undefined): boolean {
|
|
639
|
+
return address === '127.0.0.1' || address === '::1' || address === '::ffff:127.0.0.1';
|
|
640
|
+
}
|
|
641
|
+
|
|
558
642
|
const LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '[::1]']);
|
|
559
643
|
const WILDCARD_HOSTS = new Set(['0.0.0.0', '::', '[::]']);
|
|
560
644
|
|
|
561
645
|
/**
|
|
562
|
-
* Host
|
|
646
|
+
* Host 头必须是回环名或显式绑定的地址。DNS rebinding 让外站页面的 Origin 与 Host 同名,
|
|
647
|
+
* 同源闸门因此失效;这一关把请求钉在浏览器真正访问的名字上。绑到通配地址即操作员
|
|
648
|
+
* 已选择对外露面,不校验。
|
|
563
649
|
*/
|
|
564
|
-
function isAllowedHost(hostHeader: unknown, listenHost: string): boolean {
|
|
650
|
+
function isAllowedHost(hostHeader: unknown, listenHost: string, allowedHosts: readonly string[] = []): boolean {
|
|
565
651
|
if (WILDCARD_HOSTS.has(listenHost)) return true;
|
|
566
652
|
if (typeof hostHeader !== 'string' || hostHeader === '') return false;
|
|
567
653
|
const name = barePort(hostHeader).toLowerCase();
|
|
568
|
-
return LOOPBACK_HOSTS.has(name) || name === listenHost.toLowerCase() || name === `[${listenHost.toLowerCase()}]
|
|
654
|
+
return LOOPBACK_HOSTS.has(name) || name === listenHost.toLowerCase() || name === `[${listenHost.toLowerCase()}]`
|
|
655
|
+
|| namesHost(allowedHosts, hostHeader);
|
|
569
656
|
}
|
|
570
657
|
|
|
571
658
|
/**
|
|
@@ -708,6 +795,8 @@ export class WebApp {
|
|
|
708
795
|
private readonly deps: WebAppDeps;
|
|
709
796
|
private readonly app: express.Express;
|
|
710
797
|
private readonly listenHost: string;
|
|
798
|
+
private readonly allowedHosts: readonly string[];
|
|
799
|
+
private readonly auth: ConsoleAuth;
|
|
711
800
|
/** 控制台页聚合。没挂 consolePageSources 时它也在,只是永远收到空清单。 */
|
|
712
801
|
private readonly consolePages: ConsolePageRegistry;
|
|
713
802
|
private readonly assets: ConsoleAssets;
|
|
@@ -726,6 +815,8 @@ export class WebApp {
|
|
|
726
815
|
constructor(deps: WebAppDeps) {
|
|
727
816
|
this.deps = deps;
|
|
728
817
|
this.listenHost = deps.host ?? '127.0.0.1';
|
|
818
|
+
this.allowedHosts = deps.allowedHosts ?? [];
|
|
819
|
+
this.auth = new ConsoleAuth(deps.password ?? '', join(deps.dataDir, AUTH_KEY_FILE), deps.failedLoginDelayMs);
|
|
729
820
|
this.language = deps.language ?? systemLanguage();
|
|
730
821
|
this.webDistDir = deps.webDistDir ?? fileURLToPath(new URL('../../dist/web', import.meta.url));
|
|
731
822
|
this.extensionAssets = deps.extensions?.consoleAssets?.() ?? [];
|
|
@@ -809,6 +900,7 @@ export class WebApp {
|
|
|
809
900
|
supervised: this.deps.run?.supervised === true,
|
|
810
901
|
extensions: !!this.deps.extensions,
|
|
811
902
|
avatar: !!this.deps.botDir,
|
|
903
|
+
auth: this.auth.enabled,
|
|
812
904
|
};
|
|
813
905
|
}
|
|
814
906
|
|
|
@@ -932,6 +1024,29 @@ export class WebApp {
|
|
|
932
1024
|
}
|
|
933
1025
|
}
|
|
934
1026
|
|
|
1027
|
+
private authorized(req: { headers: Record<string, unknown> }): boolean {
|
|
1028
|
+
return !this.auth.enabled || this.auth.verify(cookieValue(req.headers.cookie, SESSION_COOKIE));
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
/** 直连 HTTPS,或反向代理报告 X-Forwarded-Proto: https。 */
|
|
1032
|
+
private overHttps(req: Request): boolean {
|
|
1033
|
+
return req.secure || req.headers['x-forwarded-proto'] === 'https';
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
/** HTTPS 下加 Secure;HttpOnly 使页面脚本读不到令牌。 */
|
|
1037
|
+
private sessionCookie(value: string, maxAgeSec: number, req: Request): string {
|
|
1038
|
+
const secure = this.overHttps(req);
|
|
1039
|
+
return `${SESSION_COOKIE}=${value}; Path=/; HttpOnly; SameSite=Strict; Max-Age=${maxAgeSec}${secure ? '; Secure' : ''}`;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
private serveLogin(res: Response): void {
|
|
1043
|
+
const file = fileURLToPath(new URL('./public/login.html', import.meta.url));
|
|
1044
|
+
let html = readFileSync(file, 'utf8');
|
|
1045
|
+
if (this.language === 'en') html = html.replace('<html lang="zh-CN">', '<html lang="en">');
|
|
1046
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
1047
|
+
res.type('html').send(html);
|
|
1048
|
+
}
|
|
1049
|
+
|
|
935
1050
|
private safeSessionList(): SessionStats[] {
|
|
936
1051
|
try {
|
|
937
1052
|
return this.deps.sessions?.list() ?? [];
|
|
@@ -1086,7 +1201,7 @@ export class WebApp {
|
|
|
1086
1201
|
socket.destroy();
|
|
1087
1202
|
return;
|
|
1088
1203
|
}
|
|
1089
|
-
if (!isAllowedHost(req.headers.host, this.listenHost)) {
|
|
1204
|
+
if (!isAllowedHost(req.headers.host, this.listenHost, this.allowedHosts)) {
|
|
1090
1205
|
this.deps.log.warn('拒绝 Host 不在白名单的 WebSocket 连接', {
|
|
1091
1206
|
path: pathname, host: String(req.headers.host),
|
|
1092
1207
|
});
|
|
@@ -1094,13 +1209,18 @@ export class WebApp {
|
|
|
1094
1209
|
return;
|
|
1095
1210
|
}
|
|
1096
1211
|
// 跨站页面开的 WS 不受同源策略限制,只能在 upgrade 这一关自己拦(见 isForeignOrigin)
|
|
1097
|
-
if (isForeignOrigin(req.headers.origin, req.headers.host)) {
|
|
1212
|
+
if (isForeignOrigin(req.headers.origin, req.headers.host, this.allowedHosts)) {
|
|
1098
1213
|
this.deps.log.warn('拒绝跨站WebSocket连接', {
|
|
1099
1214
|
path: pathname, origin: String(req.headers.origin),
|
|
1100
1215
|
});
|
|
1101
1216
|
socket.destroy();
|
|
1102
1217
|
return;
|
|
1103
1218
|
}
|
|
1219
|
+
if (!this.authorized(req)) {
|
|
1220
|
+
this.deps.log.warn('拒绝未登录的 WebSocket 连接', { path: pathname });
|
|
1221
|
+
socket.destroy();
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1104
1224
|
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
1105
1225
|
if (stream) {
|
|
1106
1226
|
this.handleConsolePageStream(ws, stream.pageId, stream.panelId, this.languageOf(req));
|
|
@@ -1136,7 +1256,8 @@ export class WebApp {
|
|
|
1136
1256
|
// 固定端口被占时顺延;0 交给 OS 分配且只尝试一次。
|
|
1137
1257
|
//
|
|
1138
1258
|
// 固定端口最多尝试 5 个候选端口。
|
|
1139
|
-
|
|
1259
|
+
// 候选不越过 65535:listen(65536) 抛的是非法端口,会盖住真正的监听错误。
|
|
1260
|
+
const maxAttempts = port === 0 ? 1 : Math.min(5, 65536 - port);
|
|
1140
1261
|
let server: Server | null = null;
|
|
1141
1262
|
let lastErr: unknown;
|
|
1142
1263
|
for (let i = 0; i < maxAttempts; i++) {
|
|
@@ -1146,14 +1267,15 @@ export class WebApp {
|
|
|
1146
1267
|
if (i > 0) {
|
|
1147
1268
|
// 端口顺延记录 warn,便于发现预期端口被其他实例占用。
|
|
1148
1269
|
this.deps.log.warn(
|
|
1149
|
-
`端口 ${port}
|
|
1270
|
+
`端口 ${port} 被占用或不可监听,改用 ${candidate}。`,
|
|
1150
1271
|
);
|
|
1151
1272
|
}
|
|
1152
1273
|
break;
|
|
1153
1274
|
} catch (err) {
|
|
1154
1275
|
lastErr = err;
|
|
1155
1276
|
const code = (err as NodeJS.ErrnoException)?.code;
|
|
1156
|
-
|
|
1277
|
+
// EACCES 与 EADDRINUSE 一样只说明这个候选端口此刻不能监听,成因不论(Windows 的排除端口段、权限、安全策略);试下一个,候选用尽时抛最后一个错误。
|
|
1278
|
+
if ((code !== 'EADDRINUSE' && code !== 'EACCES') || port === 0) throw err;
|
|
1157
1279
|
}
|
|
1158
1280
|
}
|
|
1159
1281
|
if (!server) {
|
|
@@ -1166,6 +1288,9 @@ export class WebApp {
|
|
|
1166
1288
|
this.wss = wss;
|
|
1167
1289
|
const actual = (server.address() as AddressInfo).port;
|
|
1168
1290
|
this.deps.log.info(`web面板已启动 http://${this.listenHost}:${actual}/`);
|
|
1291
|
+
if (!LOOPBACK_HOSTS.has(this.listenHost) && this.listenHost !== '::1' && !this.auth.enabled) {
|
|
1292
|
+
this.deps.log.warn('控制台监听在非回环地址且没有设访问密码,能连到这个端口的人都能操作 bot', { host: this.listenHost });
|
|
1293
|
+
}
|
|
1169
1294
|
return actual;
|
|
1170
1295
|
}
|
|
1171
1296
|
|
|
@@ -1202,7 +1327,7 @@ export class WebApp {
|
|
|
1202
1327
|
app.disable('x-powered-by');
|
|
1203
1328
|
|
|
1204
1329
|
app.use((req, res, next) => {
|
|
1205
|
-
if (isAllowedHost(req.headers.host, this.listenHost)) {
|
|
1330
|
+
if (isAllowedHost(req.headers.host, this.listenHost, this.allowedHosts)) {
|
|
1206
1331
|
next();
|
|
1207
1332
|
return;
|
|
1208
1333
|
}
|
|
@@ -1218,7 +1343,7 @@ export class WebApp {
|
|
|
1218
1343
|
next();
|
|
1219
1344
|
return;
|
|
1220
1345
|
}
|
|
1221
|
-
if (!isForeignOrigin(req.headers.origin, req.headers.host)) {
|
|
1346
|
+
if (!isForeignOrigin(req.headers.origin, req.headers.host, this.allowedHosts)) {
|
|
1222
1347
|
next();
|
|
1223
1348
|
return;
|
|
1224
1349
|
}
|
|
@@ -1234,6 +1359,59 @@ export class WebApp {
|
|
|
1234
1359
|
if (!res.headersSent) res.status(500).json({ error: String(err) });
|
|
1235
1360
|
}
|
|
1236
1361
|
};
|
|
1362
|
+
// 登录、登出与状态三条路由在闸前;其余路由连同各自的 body 解析都在闸后。
|
|
1363
|
+
app.get('/api/auth/status', wrap((req, res) => {
|
|
1364
|
+
res.json({ required: this.auth.enabled, authenticated: this.authorized(req) });
|
|
1365
|
+
}));
|
|
1366
|
+
app.post('/api/auth/login', express.json(), (req: Request, res: Response) => {
|
|
1367
|
+
void (async () => {
|
|
1368
|
+
if (!this.auth.enabled) { res.json({ ok: true }); return; }
|
|
1369
|
+
if (!this.overHttps(req) && !isLoopbackPeer(req.socket.remoteAddress)) {
|
|
1370
|
+
this.deps.log.warn('登录请求从非回环对端经明文连接送来,密码与登录态 Cookie 可被途中截获;公网使用请放在终结 HTTPS 的反向代理后面', { from: req.socket.remoteAddress });
|
|
1371
|
+
}
|
|
1372
|
+
const candidate = (req.body as { password?: unknown } | undefined)?.password;
|
|
1373
|
+
if (typeof candidate !== 'string' || !(await this.auth.login(candidate))) {
|
|
1374
|
+
this.deps.log.warn('控制台登录失败', { from: req.socket.remoteAddress });
|
|
1375
|
+
res.status(401).json({ error: '密码不对' });
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
1378
|
+
res.setHeader('Set-Cookie', this.sessionCookie(this.auth.issue(), SESSION_COOKIE_MAX_AGE_SEC, req));
|
|
1379
|
+
this.deps.log.info('控制台登录', { from: req.socket.remoteAddress });
|
|
1380
|
+
res.json({ ok: true });
|
|
1381
|
+
})().catch((err: unknown) => {
|
|
1382
|
+
if (!res.headersSent) res.status(500).json({ error: String(err) });
|
|
1383
|
+
});
|
|
1384
|
+
});
|
|
1385
|
+
app.post('/api/auth/logout', wrap((req, res) => {
|
|
1386
|
+
res.setHeader('Set-Cookie', this.sessionCookie('', 0, req));
|
|
1387
|
+
res.json({ ok: true });
|
|
1388
|
+
}));
|
|
1389
|
+
app.use((req, res, next) => {
|
|
1390
|
+
if (this.authorized(req)) { next(); return; }
|
|
1391
|
+
if (req.method === 'GET' && (req.path === '/' || req.path === '/index.html')) { this.serveLogin(res); return; }
|
|
1392
|
+
res.setHeader(CONSOLE_AUTH_HEADER, 'required');
|
|
1393
|
+
res.status(401).json({ error: '需要登录' });
|
|
1394
|
+
});
|
|
1395
|
+
|
|
1396
|
+
const providerRoute = (handler: (hub: ProviderHub, req: Request) => unknown) => wrap(async (req, res) => {
|
|
1397
|
+
if (!this.deps.providers) { res.status(503).json({ error: 'Provider configuration unavailable.' }); return; }
|
|
1398
|
+
try { res.json((await handler(this.deps.providers, req)) ?? { ok: true });
|
|
1399
|
+
if (req.method === 'POST' && /^\/api\/providers(?:$|\/[^/]+\/(save|delete|activate)$)/.test(req.path)) this.debugBroadcast({ t: 'status', status: this.safeStatus() });
|
|
1400
|
+
}
|
|
1401
|
+
catch (error) { res.status(error instanceof Error && 'status' in error && typeof error.status === 'number' ? error.status : 400).json({ error: error instanceof Error ? error.message : String(error) }); }
|
|
1402
|
+
});
|
|
1403
|
+
app.get('/api/providers', providerRoute((hub, req) => hub.list(this.languageOf(req))));
|
|
1404
|
+
app.get('/api/provider-modules', providerRoute((hub, req) => hub.moduleList(this.languageOf(req))));
|
|
1405
|
+
app.post('/api/provider-modules/config', express.json(), providerRoute((hub, req) => hub.groups(req.body.name || 'draft', req.body.entry, this.languageOf(req))));
|
|
1406
|
+
app.post('/api/provider-modules/preview', express.json(), providerRoute((hub, req) => hub.preview(req.body.name, req.body.entry, req.body.panel, req.body.method, req.body.args ?? [], this.languageOf(req))));
|
|
1407
|
+
app.get('/api/providers/:name', providerRoute((hub, req) => hub.detail(String(req.params.name), this.languageOf(req))));
|
|
1408
|
+
app.post('/api/providers', express.json(), providerRoute((hub, req) => hub.save(null, req.body, this.languageOf(req))));
|
|
1409
|
+
app.post('/api/providers/:name/save', express.json(), providerRoute((hub, req) => hub.save(String(req.params.name), req.body, this.languageOf(req))));
|
|
1410
|
+
app.post('/api/providers/:name/delete', express.json(), providerRoute((hub, req) => hub.delete(String(req.params.name), req.body.expectedRevision)));
|
|
1411
|
+
app.post('/api/providers/:name/activate', providerRoute((hub, req) => hub.activate(String(req.params.name), this.languageOf(req))));
|
|
1412
|
+
app.post('/api/providers/:name/test', providerRoute((hub, req) => hub.action(String(req.params.name), 'test', this.languageOf(req))));
|
|
1413
|
+
app.post('/api/providers/:name/models', providerRoute((hub, req) => hub.action(String(req.params.name), 'models', this.languageOf(req))));
|
|
1414
|
+
|
|
1237
1415
|
app.get('/api/status', wrap((_req, res) => {
|
|
1238
1416
|
res.json({ ...this.safeStatus(), uptimeSec: Math.round(process.uptime()) });
|
|
1239
1417
|
}));
|
|
@@ -1319,6 +1497,24 @@ export class WebApp {
|
|
|
1319
1497
|
res.json(readTailRecordsWhere(join(this.deps.dataDir, 'runs', runId, 'log.jsonl'), limit, pred));
|
|
1320
1498
|
}));
|
|
1321
1499
|
|
|
1500
|
+
// 诊断包:一次导出把排查一场跑要看的记录收进一个 JSON。缺席的接缝那一段为空。
|
|
1501
|
+
app.get('/api/diagnostics', wrap(async (req, res) => {
|
|
1502
|
+
const dbg = this.deps.debug;
|
|
1503
|
+
res.json(buildDiagnostics({
|
|
1504
|
+
dataDir: this.deps.dataDir,
|
|
1505
|
+
runId: dbg?.runId?.() ?? null,
|
|
1506
|
+
exportedAt: new Date(),
|
|
1507
|
+
status: this.deps.getStatus(),
|
|
1508
|
+
session: dbg ? { messages: dbg.sessionMessages(), head: dbg.sessionHead?.() ?? [] } : null,
|
|
1509
|
+
sessions: this.deps.sessions?.list() ?? [],
|
|
1510
|
+
toolSchemas: dbg?.toolSchemas() ?? [],
|
|
1511
|
+
events: this.deps.store.range({ limit: DIAGNOSTICS_TAIL }),
|
|
1512
|
+
latestCursor: this.deps.store.latestCursor(),
|
|
1513
|
+
worlds: (await this.deps.worlds?.(this.languageOf(req))) ?? [],
|
|
1514
|
+
usage: this.deps.usage?.aggregate({ bucket: 'day' }) ?? null,
|
|
1515
|
+
}));
|
|
1516
|
+
}));
|
|
1517
|
+
|
|
1322
1518
|
app.get('/api/runs', wrap((_req, res) => {
|
|
1323
1519
|
res.json({ current: this.deps.debug?.runId?.() ?? null, runs: readRunsIndex(join(this.deps.dataDir, 'runs', 'index.jsonl')) });
|
|
1324
1520
|
}));
|
|
@@ -1387,6 +1583,7 @@ export class WebApp {
|
|
|
1387
1583
|
})();
|
|
1388
1584
|
});
|
|
1389
1585
|
|
|
1586
|
+
// 开场引导只出现一次:控制台在操作员开口或按下那颗按钮时销掉标记。
|
|
1390
1587
|
app.post('/api/onboarding/dismiss', wrap((_req, res) => {
|
|
1391
1588
|
const src = this.deps.onboarding;
|
|
1392
1589
|
if (!src) { res.status(503).json({ error: '开场引导标记不可用' }); return; }
|
|
@@ -1418,13 +1615,16 @@ export class WebApp {
|
|
|
1418
1615
|
);
|
|
1419
1616
|
});
|
|
1420
1617
|
|
|
1421
|
-
// 重启 = 落下重启标志 +
|
|
1618
|
+
// 重启 = 落下重启标志 + 规范关机。有没有启动器循环把它拉起来、回来时投递是不是暂停的,回执里说明。
|
|
1422
1619
|
app.post('/api/run/restart', (req: Request, res: Response) => {
|
|
1423
1620
|
const supervised = this.deps.run?.supervised === true;
|
|
1424
1621
|
const language = this.languageOf(req);
|
|
1622
|
+
const text = pick(language, SERVER_TEXT);
|
|
1425
1623
|
void this.respondPowerAction(
|
|
1426
1624
|
res, language, this.deps.run?.restart, '重启控制不可用', '收到重启请求(人工操作)',
|
|
1427
|
-
supervised ?
|
|
1625
|
+
!supervised ? text.exitUnsupervised
|
|
1626
|
+
: this.deps.run?.startsPaused ? text.exitSupervisedPaused
|
|
1627
|
+
: text.exitSupervised,
|
|
1428
1628
|
);
|
|
1429
1629
|
});
|
|
1430
1630
|
|
|
@@ -1508,12 +1708,26 @@ export class WebApp {
|
|
|
1508
1708
|
return;
|
|
1509
1709
|
}
|
|
1510
1710
|
try {
|
|
1511
|
-
res.json({ hits: await src.search(
|
|
1711
|
+
res.json({ hits: await src.search(kind) });
|
|
1512
1712
|
} catch (err) {
|
|
1513
1713
|
res.status(502).json({ error: `npm 搜索失败: ${String(err)}` });
|
|
1514
1714
|
}
|
|
1515
1715
|
}));
|
|
1516
1716
|
|
|
1717
|
+
app.get('/api/extensions/package', wrap(async (req, res) => {
|
|
1718
|
+
const src = this.deps.extensions;
|
|
1719
|
+
if (!src) { res.status(503).json({ error: '扩展管理不可用' }); return; }
|
|
1720
|
+
const name = strParam(req.query.name)?.trim();
|
|
1721
|
+
if (!name) { res.status(400).json({ error: '缺少包名' }); return; }
|
|
1722
|
+
try {
|
|
1723
|
+
res.json(await src.packageInfo(name));
|
|
1724
|
+
} catch (err) {
|
|
1725
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1726
|
+
// 包名不合法是请求的问题,其余当成 registry 那一头的问题
|
|
1727
|
+
res.status(msg.includes('包名') ? 400 : 502).json({ error: msg });
|
|
1728
|
+
}
|
|
1729
|
+
}));
|
|
1730
|
+
|
|
1517
1731
|
app.post('/api/extensions/install', express.json(), wrap(async (req, res) => {
|
|
1518
1732
|
const src = this.deps.extensions;
|
|
1519
1733
|
if (!src) { res.status(503).json({ error: '扩展管理不可用' }); return; }
|
|
@@ -423,10 +423,13 @@ export interface ConsolePromptImagesOptions {
|
|
|
423
423
|
export interface ConsolePromptInputOptions {
|
|
424
424
|
label?: string;
|
|
425
425
|
placeholder?: string;
|
|
426
|
+
/** 底栏左侧的灰字。不给就不占字。 */
|
|
426
427
|
hint?: string;
|
|
427
428
|
disabled?: boolean;
|
|
428
429
|
/** 发送键左侧的紧凑工具入口。节点所有权随 prompt input 一起结束。 */
|
|
429
430
|
tools?: HTMLElement;
|
|
431
|
+
/** 底栏最左的入口,排在 `hint` 之前。节点所有权随 prompt input 一起结束。 */
|
|
432
|
+
leading?: HTMLElement;
|
|
430
433
|
/** 图片通道。不给 = 不收图,`onSubmit` 的第二参恒为空数组。 */
|
|
431
434
|
images?: ConsolePromptImagesOptions;
|
|
432
435
|
/**
|
|
@@ -440,7 +443,7 @@ export interface ConsolePromptInput {
|
|
|
440
443
|
el: HTMLDivElement;
|
|
441
444
|
focus(): void;
|
|
442
445
|
setDisabled(disabled: boolean): void;
|
|
443
|
-
/**
|
|
446
|
+
/** 盖住输入框里的灰字。`null` 回到构造时给的那一句。 */
|
|
444
447
|
setPlaceholder(text: string | null): void;
|
|
445
448
|
}
|
|
446
449
|
|
|
@@ -14,6 +14,8 @@ export const CONSOLE_PROTOCOL_VERSION = 1;
|
|
|
14
14
|
* WebSocket 构造器带不了头)。值是 `zh` / `en`;缺席或不认识时服务端用部署默认语言。
|
|
15
15
|
*/
|
|
16
16
|
export const CONSOLE_LANGUAGE_HEADER = 'x-cortico-language';
|
|
17
|
+
/** 设了访问密码而请求未登录时,401 响应带这个头;浏览器端据此回到入口页。 */
|
|
18
|
+
export const CONSOLE_AUTH_HEADER = 'x-cortico-auth';
|
|
17
19
|
export const CONSOLE_LANGUAGE_QUERY = 'language';
|
|
18
20
|
|
|
19
21
|
/** world:World 控制面;llm:LLM 供应模块;persona:bot/Persona 控制面;memory:Persona 的 Memory 页。framework 为保留类别,不经贡献协议提供页面。 */
|
|
@@ -401,7 +403,8 @@ export interface ConsoleLampsResponse {
|
|
|
401
403
|
}
|
|
402
404
|
|
|
403
405
|
/**
|
|
404
|
-
*
|
|
406
|
+
* 「模型提供商」那一行的灯在灯表里的键。贡献方的 page id 是 `<kind>:<id>`,
|
|
407
|
+
* kind 取 world / llm / persona,因此这个键与谁都不撞。
|
|
405
408
|
*/
|
|
406
409
|
export const PROVIDERS_LAMP_ID = 'framework:providers';
|
|
407
410
|
|
|
@@ -113,7 +113,7 @@ B 站直播间协议只读接入。观众的弹幕、礼物、醒目留言、上
|
|
|
113
113
|
|
|
114
114
|
World 在 `127.0.0.1` 启动独立页面:`GET /overlay` 是 OBS browser source,`GET /editor`
|
|
115
115
|
是编辑器,`GET /stream` 是同源 SSE 数据面,`GET /assets/<id>` 提供上传素材。端口默认 7795,
|
|
116
|
-
|
|
116
|
+
被占用或不可监听(`listen` 回 `EADDRINUSE` 或 `EACCES`)时顺延,含配置端口最多尝试 5 个端口,并记录 warn。
|
|
117
117
|
编辑接口只接受浏览器携带的同源 `Origin` 与 JSON 请求,并限制请求体大小;
|
|
118
118
|
服务不发送 wildcard CORS。设计和公告分别使用修订号做冲突检测,素材、设计与公告写入共用串行队列,
|
|
119
119
|
避免保存过程中删除素材或后完成的旧请求覆盖新状态。
|
|
@@ -98,14 +98,16 @@ export class BilibiliOverlayServer {
|
|
|
98
98
|
this.boundPort = (server.address() as AddressInfo).port;
|
|
99
99
|
if (index > 0) {
|
|
100
100
|
log.warn(
|
|
101
|
-
`B站 Overlay 端口 ${preferred}
|
|
101
|
+
`B站 Overlay 端口 ${preferred} 被占用或不可监听,改用 ${this.boundPort}`,
|
|
102
102
|
);
|
|
103
103
|
}
|
|
104
104
|
break;
|
|
105
105
|
} catch (error) {
|
|
106
106
|
lastError = error;
|
|
107
107
|
server.close();
|
|
108
|
-
|
|
108
|
+
const code = (error as NodeJS.ErrnoException)?.code;
|
|
109
|
+
// EACCES 与 EADDRINUSE 一样只说明这个候选端口此刻不能监听,成因不论(Windows 的排除端口段、权限、安全策略);试下一个,候选用尽时抛最后一个错误。
|
|
110
|
+
if ((code !== 'EADDRINUSE' && code !== 'EACCES') || preferred === 0) throw error;
|
|
109
111
|
}
|
|
110
112
|
}
|
|
111
113
|
if (!this.http) throw lastError instanceof Error ? lastError : new Error('B站 Overlay 没有可用端口');
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<!-- Owner: src/worlds/minecraft/mineflayer-fixes.ts -->
|
|
2
|
+
|
|
3
|
+
# 版本适配
|
|
4
|
+
|
|
5
|
+
Minecraft 服务端换一个版本,客户端这边要跟着改的事实记在这里:版本边界、认得出它的
|
|
6
|
+
判据、改在哪、当时怎么量出来的。
|
|
7
|
+
|
|
8
|
+
**接到「她走不动 / 动作被打回 / 某个动作忽然失效」这类报告,先读这一页。** 这类症状里
|
|
9
|
+
客户端看得见的只有"位置包被打回",光读代码分不出是判据变了还是别的;下面每条都带着
|
|
10
|
+
当时的实测数字,对得上就直接按那条走。
|
|
11
|
+
|
|
12
|
+
## 1.21.11:包围盒贴着方块侧面的位置被拒,静默传送回
|
|
13
|
+
|
|
14
|
+
**版本边界** Paper 1.21.8、1.21.9、1.21.10 收下贴面位置,1.21.11 拒。不带任何插件的
|
|
15
|
+
原版 Paper 即可复现,与服务端分支(Leaves)、反作弊插件无关。
|
|
16
|
+
|
|
17
|
+
**服务端的判据** 把玩家包围盒摆在离方块侧面缝隙 `g` 处发过去:
|
|
18
|
+
|
|
19
|
+
| `g` | 服务端 |
|
|
20
|
+
|---|---|
|
|
21
|
+
| > 0(1e-7 也算) | 收下 |
|
|
22
|
+
| = 0 或 < 0 | 回一个 `position` 包把人传回上一个好位置,**不打 `moved wrongly`** |
|
|
23
|
+
|
|
24
|
+
竖直方向的判据不同:贴着地面顶面、贴着天花板底面都照收,只有真的插进去才被拒。
|
|
25
|
+
|
|
26
|
+
**客户端为什么总是踩中** `prismarine-physics` 的 `AABB.computeOffsetX/Z` 把移动量削到
|
|
27
|
+
"恰好贴上"。于是走到墙根、沿墙走、上一格台阶时贴着立面起跳,发出去的位置包缝隙恰好
|
|
28
|
+
为零,每个物理刻都被打回,人钉在原地。
|
|
29
|
+
|
|
30
|
+
**认得出它的样子**
|
|
31
|
+
|
|
32
|
+
- 服务端日志干净:同时段别的玩家有 `moved wrongly`,bot 一条都没有
|
|
33
|
+
- 每秒 1 到 9 个服务端 `position` 包,落点比当前位置略微靠后
|
|
34
|
+
- 单次被放行的净位移 0.02 到 0.03 格
|
|
35
|
+
- `goto-stall-probe` 里 `onGround=false`、三轴速度为零、y 停在起跳弧线中途的值
|
|
36
|
+
(贴墙起跳第一刻被收、第二刻被打回第一刻的位置),`controlKeys` 显示前进键一直按着
|
|
37
|
+
|
|
38
|
+
**改法** `mineflayer-fixes.ts` 的 `installWallGap()`:替换 `prismarine-physics` AABB 原型上的
|
|
39
|
+
`computeOffsetX` 与 `computeOffsetZ`,朝方块走的那一步停在离面 `WALL_GAP`(1e-5)处,已经
|
|
40
|
+
贴着再往里走时退开同样距离;`computeOffsetY` 不动。bot 的物理与寻路器的判据模拟共用
|
|
41
|
+
这一个类,改原型两处同时生效。原型上打 `Symbol.for` 标记,装几次都只改一次。
|
|
42
|
+
|
|
43
|
+
**试过没用的**
|
|
44
|
+
|
|
45
|
+
- 补上原版客户端 1.21.2+ 每刻发的 `tick_end`、按键变化时的 `player_input`、位置包里真实的
|
|
46
|
+
`hasHorizontalCollision`:三样一起上,打回次数不变
|
|
47
|
+
- 压掉 mineflayer 收到传送后回的那个位置包、补发 `player_loaded`:与这条无关
|
|
48
|
+
|
|
49
|
+
**验证** 不带插件的 Paper 1.21.11,八种一格台阶几何(斜角单块、正对与斜对高台、贴脸
|
|
50
|
+
起步、头顶树冠):改前每场被打回 160 到 334 次、两场钉死;改后 16 场全部到达、打回
|
|
51
|
+
零次,耗时与 1.20.6 同一量级。
|
|
52
|
+
|
|
53
|
+
## 怎么量一条新的版本差异
|
|
54
|
+
|
|
55
|
+
服务端改了移动判据时,别读反编译猜,直接问服务端:
|
|
56
|
+
|
|
57
|
+
1. 取各版本的原版服务端 jar(Paper 的 `fill.papermc.io/v3/projects/paper/versions/<版本>/builds`),
|
|
58
|
+
一个版本一个进程,端口与存档都隔离。
|
|
59
|
+
2. 关掉客户端物理(`bot.physicsEnabled = false`),手工写 `bot.entity.position` 把人摆到要
|
|
60
|
+
测的位置,数 400 毫秒内收到几个服务端 `position` 包。零个 = 服务端认,一个及以上 = 被拒。
|
|
61
|
+
3. 缝隙按 0.05、1e-3、1e-5、1e-7、0、负值扫一遍;地面与空中各扫一次;水平与竖直分开扫
|
|
62
|
+
——两个方向的判据可以不一样,1.21.11 就不一样。
|
|
63
|
+
4. 同一套摆法在相邻版本上各跑一次,边界就出来了。
|
|
64
|
+
|
|
65
|
+
台架在 `scratch/mc-bench/`(不入库):`rig.ts` 起真服务端配真执行器,`BENCH_JAR` 选服务端
|
|
66
|
+
jar、`BENCH_VERSION` 选协议版本。
|