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/core/types.ts
CHANGED
|
@@ -516,6 +516,7 @@ export interface TimerEntry {
|
|
|
516
516
|
|
|
517
517
|
/** 跨重启恢复的定时器。 */
|
|
518
518
|
export interface TimersApi {
|
|
519
|
+
/** 到期回调在 set 返回之后才发生,包括 atIso 已经过去的条目。 */
|
|
519
520
|
set(atIso: string, payload?: Record<string, unknown>): { ok: true; id: string } | { ok: false; error: string };
|
|
520
521
|
cancel(id: string): boolean;
|
|
521
522
|
list(): ReadonlyArray<TimerEntry>;
|
|
@@ -833,8 +834,11 @@ export interface World {
|
|
|
833
834
|
* 语言不缓存,不影响发给模型的文本;未实现时仅显示通用信息。
|
|
834
835
|
*/
|
|
835
836
|
console?(language?: Language): WorldConsoleDecl;
|
|
836
|
-
/**
|
|
837
|
-
|
|
837
|
+
/**
|
|
838
|
+
* 主 session 的输出流接收器;装配层合并已挂载 World 的接收器,随挂载变化更新。
|
|
839
|
+
* 这一刻没有接收器(未连接、模式关着)时返回 undefined,合并侧跳过。
|
|
840
|
+
*/
|
|
841
|
+
outputTap?(): OutputTap | undefined;
|
|
838
842
|
/** 挂载时接收宿主并开始连接平台、推送事件。 */
|
|
839
843
|
start(host: WorldHost): Promise<void>;
|
|
840
844
|
stop(): Promise<void>;
|
|
@@ -930,7 +934,7 @@ export interface Persona {
|
|
|
930
934
|
/**
|
|
931
935
|
* session 的合成开头:每次请求置于 system 前缀之后、持久历史之前,不写入 session,交接时
|
|
932
936
|
* 不进保留内容。Core 每次出请求前调用一次,丢弃 system 与 developer 项,补齐工具配对,
|
|
933
|
-
* 计入 hardTokens
|
|
937
|
+
* 计入 hardTokens。内容只应随 Persona 自己的输入变化;每次不同就每次打穿前缀缓存。
|
|
934
938
|
*/
|
|
935
939
|
sessionHead?(): Item[];
|
|
936
940
|
/** Memory 实例。Core 不读它的内容;bot 没给 memoryName 时控制台以它的类名作 Memory 页标题。 */
|
|
@@ -1120,6 +1124,18 @@ export interface CoreConfig {
|
|
|
1120
1124
|
};
|
|
1121
1125
|
web: {
|
|
1122
1126
|
port: number;
|
|
1127
|
+
/** 控制台监听地址;缺省 127.0.0.1。`0.0.0.0` 或 `::` 监听所有网卡。 */
|
|
1128
|
+
host?: string;
|
|
1129
|
+
/**
|
|
1130
|
+
* 回环名与监听地址之外还接受的 Host 名(域名、反向代理对外的名字),同时放行以它为
|
|
1131
|
+
* Origin 的写请求与 WebSocket;缺省为空。
|
|
1132
|
+
*/
|
|
1133
|
+
allowedHosts?: string[];
|
|
1134
|
+
/**
|
|
1135
|
+
* 控制台访问密码;缺省或空串时不要求登录。密钥 `CORTICO_WEB_PASSWORD`(进程环境或部署
|
|
1136
|
+
* `.env`)非空时优先于此键。
|
|
1137
|
+
*/
|
|
1138
|
+
password?: string;
|
|
1123
1139
|
/** 控制台配色方案 id;浏览器没有保存过选择时用它,认不出的 id 落到框架默认方案。 */
|
|
1124
1140
|
theme: string;
|
|
1125
1141
|
};
|
package/src/core/util.ts
CHANGED
|
@@ -372,7 +372,8 @@ export function shortId(prefix = ''): string {
|
|
|
372
372
|
}
|
|
373
373
|
|
|
374
374
|
/**
|
|
375
|
-
*
|
|
375
|
+
* 读一个手写的文本文件:按 BOM 选编码,UTF-8 与 UTF-16 LE 都读得出来,BOM 不进返回值。
|
|
376
|
+
* 没有 BOM 就是 UTF-8。Windows 的 shell 重定向写出的是带 BOM 的 UTF-8 或 UTF-16 LE。
|
|
376
377
|
*/
|
|
377
378
|
export function readTextFile(file: string): string {
|
|
378
379
|
const bytes = readFileSync(file);
|
|
@@ -384,3 +385,18 @@ export function readTextFile(file: string): string {
|
|
|
384
385
|
}
|
|
385
386
|
return bytes.toString('utf8');
|
|
386
387
|
}
|
|
388
|
+
|
|
389
|
+
/** 键名像凭据的值抹成 `***`;数组与嵌套对象逐层走。导出记录与配置副本前过一遍。 */
|
|
390
|
+
const SECRET_KEY = /secret|token|key|password/i;
|
|
391
|
+
|
|
392
|
+
export function redactSecrets(value: unknown): unknown {
|
|
393
|
+
if (Array.isArray(value)) return value.map(redactSecrets);
|
|
394
|
+
if (value && typeof value === 'object') {
|
|
395
|
+
const out: Record<string, unknown> = {};
|
|
396
|
+
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
|
397
|
+
out[k] = SECRET_KEY.test(k) ? '***' : redactSecrets(v);
|
|
398
|
+
}
|
|
399
|
+
return out;
|
|
400
|
+
}
|
|
401
|
+
return value;
|
|
402
|
+
}
|
package/src/deploy.ts
CHANGED
|
@@ -27,7 +27,10 @@ export function listBots(root = deploymentRoot()): string[] {
|
|
|
27
27
|
/** 部署根下一份部署都没有时自建的那份,以及它引用的 bot 代码包。 */
|
|
28
28
|
export const DEFAULT_DEPLOYMENT = { name: 'mybot', bot: 'cormini' } as const;
|
|
29
29
|
|
|
30
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* 开场引导的一次性标记,放在部署目录下。自建部署时写入,控制台见到它才给引导,操作员
|
|
32
|
+
* 开口或按下那颗按钮后删除。手动建的部署没有这个文件,也就不会看到引导。
|
|
33
|
+
*/
|
|
31
34
|
export const ONBOARDING_FLAG_FILE = '.onboarding';
|
|
32
35
|
|
|
33
36
|
/** 部署根下另有用途的目录名,不能拿来当部署名。 */
|
package/src/extensions/README.md
CHANGED
|
@@ -19,7 +19,7 @@ World 调用 `create()` / `tools()` / `console()`,provider 调用 `create()`,bot
|
|
|
19
19
|
"keywords": ["cortico-world"], // npm 搜索按类关键字:cortico-world / cortico-provider / cortico-bot
|
|
20
20
|
"cortico": {
|
|
21
21
|
"kind": "world", // world | provider | bot
|
|
22
|
-
"api":
|
|
22
|
+
"api": 5, // 这一类的契约版本,与 EXTENSION_API_VERSIONS[kind] 相等才加载
|
|
23
23
|
"consoleClient": "dist/console.js", // 可选:预构建的面板 bundle,包内相对路径
|
|
24
24
|
"consoleStyle": "dist/console.css" // 可选:随 bundle 注入的样式
|
|
25
25
|
}
|
|
@@ -27,9 +27,11 @@ World 调用 `create()` / `tools()` / `console()`,provider 调用 `create()`,bot
|
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
`parseExtensionManifest(pkg)` 只做解析与校验,不碰文件系统;装载器与
|
|
30
|
-
`pnpm check:extension <dir>` 共用它。`api`
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
`pnpm check:extension <dir>` 共用它。`api` 与这一类的契约版本不等时不加载,扩展页说明哪一边旧。
|
|
31
|
+
|
|
32
|
+
`EXTENSION_API_VERSIONS` 按 kind 各记一个版本:`WorldDefinition` 不兼容变更只加 world,
|
|
33
|
+
`ProviderModule` 只加 provider,`BotDefinition`(连同 `BotParts`、`Persona`、`LoadedConfig`)只加 bot;
|
|
34
|
+
`ConsolePanelContext` 与其余共用接口变更时三类一起加一。同一次发布里的多处变更合计加一。
|
|
33
35
|
|
|
34
36
|
## 装载
|
|
35
37
|
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 在临时部署中检查扩展构造与声明接口,不调用 start()。
|
|
3
|
-
* World 使用默认配置、无密钥,检查 create、tools、envPromptVars、console
|
|
3
|
+
* World 使用默认配置、无密钥,检查 create、tools、envPromptVars、console、工具名冲突与配置路径;
|
|
4
4
|
* provider 使用测试端点 create,bot 使用测试部署 build。
|
|
5
5
|
* World 构造失败为错误;provider 仅在绑定端点时构造,测试条目不完整导致的失败记为警告。
|
|
6
6
|
* 临时文件位于 scratchDir,调用方负责创建与清理。
|
|
7
7
|
*/
|
|
8
8
|
import { existsSync, mkdirSync } from 'node:fs';
|
|
9
9
|
import { join } from 'node:path';
|
|
10
|
-
import type { CoreConfig, World } from '../core/types.ts';
|
|
10
|
+
import type { ConfigGroup, CoreConfig, World } from '../core/types.ts';
|
|
11
11
|
import { MODULE_LAMP_MAX } from '../core/types.ts';
|
|
12
12
|
import type { LoadedConfig } from '../core/config.ts';
|
|
13
13
|
import { CORE_DEFAULTS } from '../core/config.ts';
|
|
14
|
+
import { getByPath } from '../core/config-schema.ts';
|
|
14
15
|
import { RESERVED_FRAME_NAMES } from '../core/loop.ts';
|
|
15
16
|
import { nullLogger } from '../core/util.ts';
|
|
16
17
|
import type { Language } from '../core/language.ts';
|
|
@@ -38,6 +39,24 @@ const isPlainObject = (v: unknown): v is Record<string, unknown> =>
|
|
|
38
39
|
|
|
39
40
|
const message = (error: unknown): string => (error instanceof Error ? error.message : String(error));
|
|
40
41
|
|
|
42
|
+
/**
|
|
43
|
+
* 声明过的每个配置路径都要在 `defaults()` 里有对应项,且落在 prefix 指的段内。
|
|
44
|
+
* 缺默认值时控制台照样渲染旋钮、也照样写回 config.json,而读到的是代码里另一处的兜底值。
|
|
45
|
+
*/
|
|
46
|
+
function configPathProblems(group: ConfigGroup, defaults: Record<string, unknown>, prefix: string): string[] {
|
|
47
|
+
const problems: string[] = [];
|
|
48
|
+
for (const path of Object.keys(group.schema.properties ?? {})) {
|
|
49
|
+
if (!path.startsWith(prefix)) {
|
|
50
|
+
problems.push(`配置组「${group.id}」声明的「${path}」不在 ${prefix} 段里:写回按路径走,值会落到别人的段上。`);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (getByPath(defaults, path.slice(prefix.length)) === undefined) {
|
|
54
|
+
problems.push(`配置组「${group.id}」声明的「${path}」在 defaults() 里没有对应项:旋钮能改、能写回 config.json,读到的仍是代码里的兜底值。`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return problems;
|
|
58
|
+
}
|
|
59
|
+
|
|
41
60
|
/** 面板 id 的形状:控制台一页内的局部 id。 */
|
|
42
61
|
const PANEL_ID = /^[a-z0-9-]+$/;
|
|
43
62
|
/** 工具名带前缀:`<短名>_` 起头。 */
|
|
@@ -190,6 +209,7 @@ export async function dryMountWorld(def: WorldDefinition<WorldSection>, opts: Wo
|
|
|
190
209
|
}
|
|
191
210
|
for (const group of decl.config ?? []) {
|
|
192
211
|
if (group.owner !== `world:${def.id}`) warnings.push(`配置组「${group.id}」的 owner 是「${group.owner}」,World 的配置组 owner 应为 world:${def.id}。`);
|
|
212
|
+
failures.push(...configPathProblems(group, defaults, `worlds.${def.id}.`));
|
|
193
213
|
}
|
|
194
214
|
const keys = new Set<string>();
|
|
195
215
|
for (const doc of decl.promptDocs ?? []) {
|
|
@@ -385,6 +405,7 @@ export function dryMountBot(def: BotDefinition<CoreConfig>, opts: BotDryMountOpt
|
|
|
385
405
|
|
|
386
406
|
for (const group of parts.console?.configGroups ?? []) {
|
|
387
407
|
if (group.owner !== 'persona') warnings.push(`配置组「${group.id}」的 owner 是「${group.owner}」,Persona 的配置组 owner 应为 persona。`);
|
|
408
|
+
failures.push(...configPathProblems(group, config, ''));
|
|
388
409
|
}
|
|
389
410
|
return report;
|
|
390
411
|
}
|
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 解析 package.json 的 cortico 扩展声明,不访问文件系统;装载器与 check:extension 共用。
|
|
3
|
-
* kind 必须为 world、provider 或 bot;api
|
|
4
|
-
* WorldDefinition、ProviderModule、BotDefinition 及其可达接口或 ConsolePanelContext
|
|
5
|
-
* 发生不兼容变更时递增版本。
|
|
3
|
+
* kind 必须为 world、provider 或 bot;api 必须等于这一类的契约版本。
|
|
6
4
|
* consoleClient 和 consoleStyle 是预构建产物的包内相对路径;服务端分配 URL。
|
|
7
5
|
* 包必须使用 type=module,使扩展与框架通过同一 ESM 解析方式共享模块实例。
|
|
8
6
|
*/
|
|
9
7
|
|
|
10
|
-
/** 扩展接口发生不兼容变更时递增。 */
|
|
11
|
-
export const EXTENSION_API_VERSION = 4;
|
|
12
|
-
|
|
13
8
|
export type ExtensionKind = 'world' | 'provider' | 'bot';
|
|
14
9
|
export const EXTENSION_KINDS: readonly ExtensionKind[] = ['world', 'provider', 'bot'];
|
|
15
10
|
|
|
11
|
+
/**
|
|
12
|
+
* 三类扩展各自的契约版本。一类的接口不兼容变更只递增这一类:
|
|
13
|
+
* world 看 `WorldDefinition`,provider 看 `ProviderModule`,bot 看 `BotDefinition`
|
|
14
|
+
* (连同 `BotParts`、`Persona`、`LoadedConfig`);`ConsolePanelContext` 与其余共用接口
|
|
15
|
+
* 变更时三类一起加一。同一次发布里的多处变更合计加一。
|
|
16
|
+
*/
|
|
17
|
+
export const EXTENSION_API_VERSIONS: Readonly<Record<ExtensionKind, number>> = {
|
|
18
|
+
world: 5,
|
|
19
|
+
provider: 5,
|
|
20
|
+
bot: 5,
|
|
21
|
+
};
|
|
22
|
+
|
|
16
23
|
/** npm 上按类发现用的关键字。 */
|
|
17
24
|
export const EXTENSION_KEYWORDS: Readonly<Record<ExtensionKind, string>> = {
|
|
18
25
|
world: 'cortico-world',
|
|
@@ -79,12 +86,15 @@ export function parseExtensionManifest(pkg: ExtensionPackageJson): ExtensionMani
|
|
|
79
86
|
}
|
|
80
87
|
|
|
81
88
|
const api = m.api;
|
|
89
|
+
// kind 认不出时不比版本:每一类的契约版本各走各的,不知道是哪一类就没有可比的数。
|
|
90
|
+
const expected = kindOk ? EXTENSION_API_VERSIONS[kind as ExtensionKind] : undefined;
|
|
82
91
|
if (typeof api !== 'number' || !Number.isInteger(api) || api < 1) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
92
|
+
const note = expected === undefined ? '' : `(本框架的 ${kind} 契约是 v${expected})`;
|
|
93
|
+
reasons.push(`cortico.api 必须是正整数${note},现在是 ${JSON.stringify(api)}。`);
|
|
94
|
+
} else if (expected !== undefined && api < expected) {
|
|
95
|
+
reasons.push(`扩展按 ${kind} 契约 v${api} 编写,本框架的 ${kind} 契约是 v${expected}:扩展需要升级。`);
|
|
96
|
+
} else if (expected !== undefined && api > expected) {
|
|
97
|
+
reasons.push(`扩展要求 ${kind} 契约 v${api},本框架的 ${kind} 契约只到 v${expected}:框架需要升级。`);
|
|
88
98
|
}
|
|
89
99
|
|
|
90
100
|
const client = m.consoleClient;
|
package/src/extensions.ts
CHANGED
|
@@ -14,8 +14,14 @@ import type { CoreConfig, Logger } from './core/types.ts';
|
|
|
14
14
|
import type { BotDefinition } from './bot.ts';
|
|
15
15
|
import type { WorldDefinition, WorldSection } from './world.ts';
|
|
16
16
|
import type { ProviderModule } from './providers/base.ts';
|
|
17
|
-
import type {
|
|
17
|
+
import type {
|
|
18
|
+
ExtensionInfo,
|
|
19
|
+
ExtensionInstallTarget,
|
|
20
|
+
ExtensionPackageDetail,
|
|
21
|
+
ExtensionSearchHit,
|
|
22
|
+
} from './web/server.ts';
|
|
18
23
|
import {
|
|
24
|
+
EXTENSION_API_VERSIONS,
|
|
19
25
|
EXTENSION_KEYWORDS,
|
|
20
26
|
parseExtensionManifest,
|
|
21
27
|
type ExtensionConsoleAsset,
|
|
@@ -368,11 +374,59 @@ interface RegistrySearchResponse {
|
|
|
368
374
|
description?: string;
|
|
369
375
|
keywords?: string[];
|
|
370
376
|
date?: string;
|
|
377
|
+
license?: string;
|
|
371
378
|
links?: { npm?: string; homepage?: string; repository?: string };
|
|
372
379
|
publisher?: { username?: string };
|
|
373
380
|
};
|
|
381
|
+
/** 被依赖的包数。热门包回字符串,新包回数字。 */
|
|
382
|
+
dependents?: number | string;
|
|
374
383
|
downloads?: { monthly?: number };
|
|
375
384
|
}>;
|
|
385
|
+
total?: number;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** registry 的包文档(`GET /<name>`)。只列我们要读的字段。 */
|
|
389
|
+
interface RegistryPackument {
|
|
390
|
+
'dist-tags'?: Record<string, string>;
|
|
391
|
+
versions?: Record<string, ExtensionPackageJson & {
|
|
392
|
+
license?: string;
|
|
393
|
+
deprecated?: string;
|
|
394
|
+
engines?: Record<string, string>;
|
|
395
|
+
maintainers?: Array<{ username?: string; name?: string }>;
|
|
396
|
+
_npmUser?: { name?: string };
|
|
397
|
+
dist?: { unpackedSize?: number; fileCount?: number };
|
|
398
|
+
homepage?: string;
|
|
399
|
+
bugs?: { url?: string } | string;
|
|
400
|
+
repository?: { url?: string } | string;
|
|
401
|
+
}>;
|
|
402
|
+
/** `created`、`modified` 和每个版本号各一条发布时间 */
|
|
403
|
+
time?: Record<string, string>;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** 一页取满 250:registry 的上限(传更大也只回 250)。 */
|
|
407
|
+
const SEARCH_PAGE_SIZE = 250;
|
|
408
|
+
/**
|
|
409
|
+
* 最多翻四页。`keywords:` 过滤后的全集现在是个位数;真涨到一千条,说明关键字被滥用,
|
|
410
|
+
* 再往后翻的也不是操作员要找的包。
|
|
411
|
+
*/
|
|
412
|
+
const SEARCH_MAX_HITS = 1000;
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* registry 给的仓库地址是 npm 规范化过的 `git+https://….git`:浏览器不认这个 scheme。
|
|
416
|
+
* 收成可点的 https;认不出形状就原样返回,让操作员自己看。
|
|
417
|
+
*/
|
|
418
|
+
export function repositoryWebUrl(raw: string): string {
|
|
419
|
+
let url = raw.trim().replace(/^git\+/, '').replace(/^git:\/\//, 'https://');
|
|
420
|
+
const ssh = /^(?:ssh:\/\/)?git@([^:/]+)[:/](.+)$/.exec(url);
|
|
421
|
+
if (ssh) url = `https://${ssh[1]}/${ssh[2]}`;
|
|
422
|
+
url = url.replace(/\.git$/, '');
|
|
423
|
+
return /^https?:\/\//.test(url) ? url : raw;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** `{ url }` 或裸串两种写法都收。 */
|
|
427
|
+
function urlOf(v: { url?: string } | string | undefined): string | undefined {
|
|
428
|
+
const raw = typeof v === 'string' ? v : v?.url;
|
|
429
|
+
return raw && raw.trim() ? raw.trim() : undefined;
|
|
376
430
|
}
|
|
377
431
|
|
|
378
432
|
export interface ExtensionManagerOptions {
|
|
@@ -441,35 +495,105 @@ export class ExtensionManager {
|
|
|
441
495
|
return { dir: this.dir, extensions: out };
|
|
442
496
|
}
|
|
443
497
|
|
|
444
|
-
|
|
498
|
+
/**
|
|
499
|
+
* 列出 npm 上带这一类关键字的全部包。**不接文本查询**:registry 的 `text=` 在
|
|
500
|
+
* `keywords:` 过滤之下只影响排序不缩小结果(实测 `keywords:cortico-world 任意词`
|
|
501
|
+
* 仍返回同样 8 条),筛选交给控制台在整份结果上做。
|
|
502
|
+
*/
|
|
503
|
+
async search(kind: ExtensionKind = 'world'): Promise<ExtensionSearchHit[]> {
|
|
445
504
|
const keyword = EXTENSION_KEYWORDS[kind];
|
|
446
|
-
const text = `keywords:${keyword} ${query.trim()}`.trim();
|
|
447
|
-
const url = `${this.registry}/-/v1/search?text=${encodeURIComponent(text)}&size=50`;
|
|
448
|
-
const data = (await this.fetchJson(url)) as RegistrySearchResponse;
|
|
449
505
|
const installed = new Set(readInstalled(this.dir).map((p) => p.name));
|
|
450
506
|
const hits: ExtensionSearchHit[] = [];
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
links
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
507
|
+
const seen = new Set<string>();
|
|
508
|
+
for (let from = 0; from < SEARCH_MAX_HITS; from += SEARCH_PAGE_SIZE) {
|
|
509
|
+
const text = encodeURIComponent(`keywords:${keyword}`);
|
|
510
|
+
const url = `${this.registry}/-/v1/search?text=${text}&size=${SEARCH_PAGE_SIZE}&from=${from}`;
|
|
511
|
+
const data = (await this.fetchJson(url)) as RegistrySearchResponse;
|
|
512
|
+
const objects = data.objects ?? [];
|
|
513
|
+
for (const obj of objects) {
|
|
514
|
+
const p = obj.package;
|
|
515
|
+
if (!p?.name || !p.version || !(p.keywords ?? []).includes(keyword)) continue;
|
|
516
|
+
if (seen.has(p.name)) continue;
|
|
517
|
+
seen.add(p.name);
|
|
518
|
+
const repository = p.links?.repository;
|
|
519
|
+
hits.push({
|
|
520
|
+
name: p.name,
|
|
521
|
+
version: p.version,
|
|
522
|
+
description: p.description ?? '',
|
|
523
|
+
kind,
|
|
524
|
+
...(p.date ? { date: p.date } : {}),
|
|
525
|
+
...(p.publisher?.username ? { publisher: p.publisher.username } : {}),
|
|
526
|
+
...(p.license ? { license: p.license } : {}),
|
|
527
|
+
...(p.keywords?.length ? { keywords: p.keywords } : {}),
|
|
528
|
+
downloads: obj.downloads?.monthly ?? 0,
|
|
529
|
+
dependents: Number(obj.dependents) || 0,
|
|
530
|
+
links: {
|
|
531
|
+
...(p.links?.npm ? { npm: p.links.npm } : {}),
|
|
532
|
+
...(repository ? { repository: repositoryWebUrl(repository) } : {}),
|
|
533
|
+
...(p.links?.homepage ? { homepage: p.links.homepage } : {}),
|
|
534
|
+
},
|
|
535
|
+
installed: installed.has(p.name),
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
if (objects.length < SEARCH_PAGE_SIZE) break;
|
|
469
539
|
}
|
|
470
540
|
return hits;
|
|
471
541
|
}
|
|
472
542
|
|
|
543
|
+
/**
|
|
544
|
+
* 一个包的详情。控制台在操作员点开某张卡片时才调,因此这里取整份包文档
|
|
545
|
+
* (`GET /<name>`,比搜索结果多出 cortico 声明、许可证、体积与版本史)。
|
|
546
|
+
* readme 不回传:一份 30KB 以上的 markdown,控制台也不渲染它。
|
|
547
|
+
*/
|
|
548
|
+
async packageInfo(name: string): Promise<ExtensionPackageDetail> {
|
|
549
|
+
if (!PACKAGE_NAME.test(name)) throw new Error(`不是合法的 npm 包名: ${name}`);
|
|
550
|
+
const doc = (await this.fetchJson(`${this.registry}/${name.replace('/', '%2F')}`)) as RegistryPackument;
|
|
551
|
+
const latest = doc['dist-tags']?.latest;
|
|
552
|
+
const v = latest ? doc.versions?.[latest] : undefined;
|
|
553
|
+
if (!latest || !v) throw new Error(`registry 没有给出 ${name} 的 latest 版本`);
|
|
554
|
+
|
|
555
|
+
const parsed = parseExtensionManifest(v);
|
|
556
|
+
const times = Object.entries(doc.time ?? {}).filter(([k]) => k !== 'created' && k !== 'modified');
|
|
557
|
+
const history = times
|
|
558
|
+
.sort((a, b) => (a[1] < b[1] ? 1 : -1))
|
|
559
|
+
.slice(0, 6)
|
|
560
|
+
.map(([version, date]) => ({ version, date }));
|
|
561
|
+
const spec = readInstalled(this.dir).find((p) => p.name === name)?.spec;
|
|
562
|
+
const repository = urlOf(v.repository);
|
|
563
|
+
const bugs = urlOf(v.bugs);
|
|
564
|
+
|
|
565
|
+
return {
|
|
566
|
+
name,
|
|
567
|
+
version: latest,
|
|
568
|
+
...(v.description ? { description: v.description } : {}),
|
|
569
|
+
...(v.license ? { license: v.license } : {}),
|
|
570
|
+
...(v.keywords?.length ? { keywords: v.keywords } : {}),
|
|
571
|
+
...(doc.time?.[latest] ? { published: doc.time[latest] } : {}),
|
|
572
|
+
...(doc.time?.created ? { created: doc.time.created } : {}),
|
|
573
|
+
versionCount: times.length,
|
|
574
|
+
history,
|
|
575
|
+
...(v.deprecated ? { deprecated: v.deprecated } : {}),
|
|
576
|
+
...(parsed.ok
|
|
577
|
+
? { manifest: parsed.manifest, frameworkApi: EXTENSION_API_VERSIONS[parsed.manifest.kind] }
|
|
578
|
+
: { problems: parsed.reasons }),
|
|
579
|
+
warnings: parsed.warnings,
|
|
580
|
+
...(v.engines?.node ? { engines: v.engines.node } : {}),
|
|
581
|
+
...(v.dist?.unpackedSize ? { unpackedSize: v.dist.unpackedSize } : {}),
|
|
582
|
+
...(v.dist?.fileCount ? { fileCount: v.dist.fileCount } : {}),
|
|
583
|
+
dependencies: Object.keys(v.dependencies ?? {}),
|
|
584
|
+
maintainers: (v.maintainers ?? []).map((m) => m.username ?? m.name ?? '').filter(Boolean),
|
|
585
|
+
...(v._npmUser?.name ? { publisher: v._npmUser.name } : {}),
|
|
586
|
+
links: {
|
|
587
|
+
npm: `https://www.npmjs.com/package/${name}`,
|
|
588
|
+
...(repository ? { repository: repositoryWebUrl(repository) } : {}),
|
|
589
|
+
...(v.homepage ? { homepage: v.homepage } : {}),
|
|
590
|
+
...(bugs ? { bugs } : {}),
|
|
591
|
+
},
|
|
592
|
+
installed: spec !== undefined,
|
|
593
|
+
...(spec !== undefined ? { installedSpec: spec } : {}),
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
|
|
473
597
|
async install(target: ExtensionInstallTarget): Promise<string> {
|
|
474
598
|
const spec = this.installSpec(target);
|
|
475
599
|
if (!existsSync(this.dir)) mkdirSync(this.dir, { recursive: true });
|
package/src/launcher.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { createBot } from './bot.ts';
|
|
|
13
13
|
import { createDeployment, ensureDeployment, listBots, loadDeployment } from './deploy.ts';
|
|
14
14
|
import { buildListing, type BotDefaults } from './deploy-listing.ts';
|
|
15
15
|
import { secretReader } from './core/secrets.ts';
|
|
16
|
-
import { announceDataDir, consumeBootFlags } from './boot.ts';
|
|
16
|
+
import { announceDataDir, consoleUrlOf, consumeBootFlags, listensOnEveryInterface, startsPaused } from './boot.ts';
|
|
17
17
|
import { extensionsDir, importBotDefinition, loadExtensions, locateBotPackage, readInstalled, type ActiveBotPackage } from './extensions.ts';
|
|
18
18
|
import { withWorlds } from './world.ts';
|
|
19
19
|
import { BUILTIN_WORLDS } from './worlds/index.ts';
|
|
@@ -160,6 +160,22 @@ async function main(): Promise<void> {
|
|
|
160
160
|
cfg.logging.file = levelArg as LogLevel;
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
// 监听地址与端口:--host= / --port= 只覆盖本次运行,不写回 config.json。
|
|
164
|
+
const hostArg = flagValue('host');
|
|
165
|
+
if (hostArg !== null) {
|
|
166
|
+
if (!hostArg.trim()) { console.error('--host 需要一个地址,如 0.0.0.0'); process.exit(1); }
|
|
167
|
+
cfg.web.host = hostArg.trim();
|
|
168
|
+
}
|
|
169
|
+
const portArg = flagValue('port');
|
|
170
|
+
if (portArg !== null) {
|
|
171
|
+
const port = Number(portArg);
|
|
172
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
|
173
|
+
console.error(`无效端口: ${portArg}(0–65535 的整数,0 由系统分配)`);
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
cfg.web.port = port;
|
|
177
|
+
}
|
|
178
|
+
|
|
163
179
|
// 重启标志必须在装配与 session 加载之前处理
|
|
164
180
|
consumeBootFlags(loaded.dataDir);
|
|
165
181
|
|
|
@@ -179,17 +195,15 @@ async function main(): Promise<void> {
|
|
|
179
195
|
const bot = createBot(loaded, definition, { extensions });
|
|
180
196
|
|
|
181
197
|
// 必须在启动主循环前暂停,避免首批事件提前投递。
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
process.env.CORTICO_START_PAUSED === 'true' ||
|
|
185
|
-
process.argv.includes('--paused');
|
|
186
|
-
if (startPaused) bot.core.bus.setPaused(true);
|
|
198
|
+
const paused = startsPaused();
|
|
199
|
+
if (paused) bot.core.bus.setPaused(true);
|
|
187
200
|
|
|
188
201
|
const { port } = await bot.start();
|
|
189
202
|
|
|
190
203
|
console.log(`\n Bot: ${botName}${cfg.displayName && cfg.displayName !== botName ? ` (${cfg.displayName})` : ''}`);
|
|
204
|
+
const consoleUrl = port === null ? null : consoleUrlOf(bot.webApp?.boundAddress ?? null, port);
|
|
191
205
|
if (port !== null) {
|
|
192
|
-
console.log(` 控制台:
|
|
206
|
+
console.log(` 控制台: ${consoleUrl}${listensOnEveryInterface(bot.webApp?.boundAddress ?? null) ? `(监听 ${bot.webApp?.boundAddress},所有网卡)` : ''}`);
|
|
193
207
|
const assetsProblem = webAssetsProblem(fileURLToPath(new URL('../dist/web', import.meta.url)));
|
|
194
208
|
if (assetsProblem) {
|
|
195
209
|
console.log(` ⚠ 控制台产物不完整(${assetsProblem});停止 bot 后运行 pnpm build:web`);
|
|
@@ -214,7 +228,7 @@ async function main(): Promise<void> {
|
|
|
214
228
|
const where = existsSync(envFile) ? `${envFile} 里也没有` : `${envFile} 不存在`;
|
|
215
229
|
console.log(` ⚠ 缺少 ${missingSecret}:进程环境里没有,${where};可在控制台「模型提供商」页修改密钥变量名或补填密钥`);
|
|
216
230
|
}
|
|
217
|
-
if (
|
|
231
|
+
if (paused) {
|
|
218
232
|
console.log(' ⏸ 已暂停');
|
|
219
233
|
}
|
|
220
234
|
console.log('');
|
|
@@ -223,7 +237,7 @@ async function main(): Promise<void> {
|
|
|
223
237
|
process.env.CORTICO_OPEN_BROWSER === '1' ||
|
|
224
238
|
process.env.CORTICO_OPEN_BROWSER === 'true' ||
|
|
225
239
|
process.argv.includes('--open');
|
|
226
|
-
if (openBrowserFlag &&
|
|
240
|
+
if (openBrowserFlag && consoleUrl !== null) openBrowser(consoleUrl);
|
|
227
241
|
|
|
228
242
|
// 此时限覆盖整个关机流程;各步骤的时限由 bot.shutdown() 管理。
|
|
229
243
|
const SHUTDOWN_GRACE_MS = 35_000;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { appendFileSync, closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
1
|
+
import { appendFileSync, closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, truncateSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { dirname } from 'node:path';
|
|
3
3
|
import type { ContextRecord } from './context.ts';
|
|
4
4
|
|
|
@@ -10,6 +10,20 @@ function freeze<T>(value: T): T {
|
|
|
10
10
|
return value;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
function isRecord(row: unknown): row is ContextRecord {
|
|
14
|
+
const r = row as ContextRecord | null;
|
|
15
|
+
return !!r && r.version === 2 && !!r.item && typeof r.item === 'object' && !Array.isArray(r.item)
|
|
16
|
+
&& !!r.context && typeof r.context === 'object' && !Array.isArray(r.context);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The repair `load()` applied to a final line that has no trailing newline. A line that does not
|
|
21
|
+
* parse is cut off; a complete record only gets its newline.
|
|
22
|
+
*/
|
|
23
|
+
export type ContextLoadRepair =
|
|
24
|
+
| { kind: 'torn-tail'; bytes: number }
|
|
25
|
+
| { kind: 'unterminated-tail' };
|
|
26
|
+
|
|
13
27
|
/** Immutable standard Items with runtime metadata stored in a separate field. */
|
|
14
28
|
export class ContextLog {
|
|
15
29
|
private entries: readonly ContextRecord[] = Object.freeze([]);
|
|
@@ -19,18 +33,32 @@ export class ContextLog {
|
|
|
19
33
|
|
|
20
34
|
get records(): readonly ContextRecord[] { return this.entries; }
|
|
21
35
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
36
|
+
/** Any other damaged line throws and leaves the file untouched. */
|
|
37
|
+
load(): ContextLoadRepair | null {
|
|
38
|
+
if (!existsSync(this.file)) { this.entries = Object.freeze([]); return null; }
|
|
39
|
+
const raw = readFileSync(this.file, 'utf8');
|
|
40
|
+
let body = raw;
|
|
41
|
+
let repair: ContextLoadRepair | null = null;
|
|
42
|
+
if (raw.length > 0 && !raw.endsWith('\n')) {
|
|
43
|
+
const cut = raw.lastIndexOf('\n') + 1;
|
|
44
|
+
const tail = raw.slice(cut);
|
|
45
|
+
let parsed: unknown = null;
|
|
46
|
+
try { parsed = JSON.parse(tail); } catch { /* torn */ }
|
|
47
|
+
if (isRecord(parsed)) repair = { kind: 'unterminated-tail' };
|
|
48
|
+
else { body = raw.slice(0, cut); repair = { kind: 'torn-tail', bytes: Buffer.byteLength(tail, 'utf8') }; }
|
|
49
|
+
}
|
|
50
|
+
const rows = body.split(/\r?\n/).filter(line => line.trim()).map((line, index) => {
|
|
51
|
+
let row: unknown;
|
|
52
|
+
try { row = JSON.parse(line); }
|
|
27
53
|
catch { throw new Error(`Invalid session JSON at ${this.file}:${index + 1}`); }
|
|
28
|
-
if (!row
|
|
29
|
-
|| !row.context || typeof row.context !== 'object' || Array.isArray(row.context))
|
|
30
|
-
throw new Error(`Invalid context record at ${this.file}:${index + 1}`);
|
|
54
|
+
if (!isRecord(row)) throw new Error(`Invalid context record at ${this.file}:${index + 1}`);
|
|
31
55
|
return freeze(row);
|
|
32
56
|
});
|
|
57
|
+
// Written only after every kept line validated.
|
|
58
|
+
if (repair?.kind === 'torn-tail') truncateSync(this.file, Buffer.byteLength(body, 'utf8'));
|
|
59
|
+
else if (repair) appendFileSync(this.file, '\n');
|
|
33
60
|
this.entries = Object.freeze(rows);
|
|
61
|
+
return repair;
|
|
34
62
|
}
|
|
35
63
|
|
|
36
64
|
append(entry: ContextRecord): ContextRecord {
|
package/src/providers/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- Owner: src/providers/base.ts, src/providers/registry.ts, src/providers/console/settings.ts, src/providers/console/config.ts, src/providers/openai-responses-compat/config.ts -->
|
|
1
|
+
<!-- Owner: src/providers/base.ts, src/providers/console/hub.ts, src/providers/hub-api.ts, src/providers/name.ts, src/providers/registry.ts, src/providers/console/settings.ts, src/providers/console/config.ts, src/providers/openai-responses-compat/config.ts, src/providers/transport/responses-input.ts, src/providers/transport/history.ts, src/providers/llamacpp/native.ts -->
|
|
2
2
|
|
|
3
3
|
# src/providers
|
|
4
4
|
|
|
@@ -22,15 +22,16 @@
|
|
|
22
22
|
## ProviderModule
|
|
23
23
|
|
|
24
24
|
`id` 必须等于目录名(扩展包里则是包声明的 id)。必填:`title`、`reasoningTiers`(空表 = 开放,
|
|
25
|
-
effort 收任意非空串)、`serviceTiers`、`create(name, entry, host)`。可选:`defaultBaseUrl` 与
|
|
25
|
+
effort 收任意非空串)、`serviceTiers`、`create(name, entry, host)`。可选:`description`、`defaultBaseUrl` 与
|
|
26
26
|
`baseUrlSuggestions`、`effortSuggestions`、`temperatureNote`、`localize()`、`normalize()`、
|
|
27
27
|
`validateEntry()`、`validateModel()`、`accepts()`(多模态判定)、`config()` 与 `console()`
|
|
28
28
|
(附加配置组与面板)、`prices()`、`estimateTokens()`、`contextOverflow()`。
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
地址、密钥变量名与图像开关不归模块:框架在 `console/config.ts` 里为每个端点声明这一组,
|
|
31
|
+
扩展来的模块照样有。模块自己的 `options.*` 走 `config()` 的配置组,面板在 `instance` 插槽里
|
|
32
|
+
用控制台的 schema 渲染器画同一份声明、经 `ctx.setConfig` 暂存到连接草稿——内建 llamacpp 的运行时与启动
|
|
33
|
+
两段走的是这条。`console()` 显式给空 `config` 表示这一页不另开配置页签,声明仍参与服务端
|
|
34
|
+
校验。启停与模型操作这类动作走面板 invoke。
|
|
34
35
|
|
|
35
36
|
`create()` 返回 `ProviderInstance`:`client`(实现 `respond`)、`listModels?`、`control?`、
|
|
36
37
|
`compatibilityKey?`、`start?` / `stop?`、`contextWindow?(model)`。
|
|
@@ -69,8 +70,11 @@ Core 侧:`activeProviderEntry()` / `activeSpec()` 每次现读;`contextWindowOf(
|
|
|
69
70
|
`LLMError`;每次请求尝试记录 `meters` 与 `charges`。
|
|
70
71
|
|
|
71
72
|
`responses-input.ts` 是原生 Responses 的无状态重放(system / developer 上提为
|
|
72
|
-
`instructions
|
|
73
|
-
|
|
73
|
+
`instructions`;思维链按 `reasoningReplay` 以签名块或 `reasoning_text` 回传,明文形态下没有记录来源
|
|
74
|
+
的调用前补一项合成推理);`chat.ts` + `native-input.ts` / `history.ts` 转换 Chat Completions 请求(历史
|
|
75
|
+
思维链回不回传由模块定:`renderMessagesWithMedia` 的 `keepReasoning` 决定出不出线,`dropPastThinking`
|
|
76
|
+
把合成开头之外的每条抹空);
|
|
77
|
+
`response-assembly.ts` 把两种流归一成 Open Responses 的 Item 流,
|
|
74
78
|
`finish_reason` 的 `length` / `content_filter` 落成 `incomplete_details.reason`;
|
|
75
79
|
`response-meters.ts` 把两种 usage 归一成 `TokenMeters`,缺项保持 null。
|
|
76
80
|
|
|
@@ -84,3 +88,23 @@ Core 侧:`activeProviderEntry()` / `activeSpec()` 每次现读;`contextWindowOf(
|
|
|
84
88
|
模块默认,快照带 sha256 id 与 `capturedAt`。计量键:`input` / `output` / `total` /
|
|
85
89
|
`cachedInput` / `uncachedInput` / `reasoning` 与 `detail:*`,单价按每百万 token。实际扣费在
|
|
86
90
|
`src/core/generation.ts` 的 `priceUsage()`,缺计量记 `amount: null`。
|
|
91
|
+
|
|
92
|
+
## 端点配置接口
|
|
93
|
+
|
|
94
|
+
`console/hub.ts` 的 `ProviderHub` 是控制台改端点的唯一入口,`hub-api.ts` 把它接到
|
|
95
|
+
`/api/providers`(聚合、按名读取、保存、删除、设为当前、测试、模型列表)与
|
|
96
|
+
`/api/provider-modules`(已注册模块)。列表保留 `kind` 没有对应模块的端点,active 与可用性
|
|
97
|
+
分别报告;状态帧的 `modelConnection` 带当前端点的名称、模型、模块与地址。
|
|
98
|
+
|
|
99
|
+
新名称由 `name.ts` 校验:英文字母、数字、`-` 或 `_`,首字符是字母或数字,不收空格与 Windows
|
|
100
|
+
设备名——名字就是 `<部署根>/providers/` 下的目录名。已在磁盘上的名字不改就继续有效。保存带
|
|
101
|
+
读取时的 revision(端点 `config.json` 与 `.env` 的 sha256),对不上返回 409。共享目录上的写锁
|
|
102
|
+
`.write-lock` 串行化各进程的写入。改名同时改目录和部署根内各 `deployment.json` 的
|
|
103
|
+
`activeProvider`,中途失败回滚已动过的文件和目录;还被引用的端点不能删。
|
|
104
|
+
|
|
105
|
+
保存一次校验模型、连接与模块配置,再写 `config.json` 和可选密钥;密钥进该端点的 `.env`,读取
|
|
106
|
+
接口不返回密钥值。写完清掉本进程的实例缓存,已绑定的请求与 fork 保持原客户端;其他进程在下次
|
|
107
|
+
重读共享配置时跟上。
|
|
108
|
+
|
|
109
|
+
`ProviderConsoleHost.editing` 表示这个实例来自还没保存的表单,模块据此拒绝运行时副作用,
|
|
110
|
+
`host.save` 把配置交回浏览器暂存。`ProviderRegistry.preview` 拿到的实例不进运行实例缓存。
|