cortico 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/package.json +1 -1
- package/src/boot.ts +29 -0
- package/src/bot.ts +40 -5
- package/src/core/README.md +12 -2
- 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 +18 -6
- package/src/core/generation.ts +66 -9
- package/src/core/instance-lock.ts +6 -0
- package/src/core/loop.ts +62 -32
- package/src/core/secrets.ts +2 -4
- package/src/core/session.ts +3 -2
- package/src/core/timers.ts +8 -6
- package/src/core/types.ts +23 -4
- package/src/core/util.ts +15 -0
- package/src/deploy.ts +5 -4
- package/src/extensions/README.md +6 -4
- package/src/extensions/dry-mount.ts +34 -6
- package/src/extensions/manifest.ts +21 -11
- package/src/extensions.ts +147 -23
- package/src/launcher.ts +30 -22
- package/src/protocol/open-responses/context-helpers.ts +3 -1
- package/src/protocol/open-responses/context-log.ts +37 -9
- package/src/providers/README.md +44 -12
- package/src/providers/base.ts +2 -0
- package/src/providers/console/config.ts +36 -0
- package/src/providers/console/hub.ts +307 -0
- package/src/providers/console/settings.ts +30 -26
- package/src/providers/console/strings.ts +14 -0
- package/src/providers/console/types.ts +5 -0
- package/src/providers/hub-api.ts +3 -0
- package/src/providers/llamacpp/config.ts +38 -0
- package/src/providers/llamacpp/console/models-panel.ts +87 -4
- package/src/providers/llamacpp/console/runtime-panel.ts +36 -73
- package/src/providers/llamacpp/console/server.ts +47 -4
- package/src/providers/llamacpp/huggingface.ts +93 -0
- package/src/providers/llamacpp/index.ts +5 -1
- package/src/providers/llamacpp/native.ts +10 -5
- package/src/providers/llamacpp/options.ts +2 -0
- package/src/providers/llamacpp/strings.ts +42 -0
- package/src/providers/name.ts +13 -0
- package/src/providers/openai-responses-compat/config.ts +33 -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 +135 -0
- package/src/providers/openai-responses-compat/index.ts +40 -12
- package/src/providers/openai-responses-compat/native.ts +15 -5
- package/src/providers/openai-responses-compat/strings.ts +62 -1
- package/src/providers/pricebook.ts +5 -5
- package/src/providers/registry.ts +24 -10
- package/src/providers/strings.ts +2 -0
- package/src/providers/transport/errors.ts +9 -0
- package/src/providers/transport/response-http.ts +7 -3
- package/src/providers/transport/responses-input.ts +59 -10
- package/src/web/README.md +15 -3
- package/src/web/auth.ts +80 -0
- package/src/web/client/console-pages/builtins/llm-settings/panel.ts +42 -35
- 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 +2 -4
- package/src/web/client/console-pages/host.ts +22 -5
- package/src/web/client/core/api.ts +5 -1
- package/src/web/client/core/router.ts +15 -0
- 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 +1 -1
- package/src/web/client/features/live/diagnostics.ts +32 -0
- package/src/web/client/features/live/index.ts +27 -11
- package/src/web/client/features/live/protocol.ts +1 -0
- package/src/web/client/features/live/strings.ts +9 -3
- package/src/web/client/features/providers/detail.ts +309 -0
- package/src/web/client/features/providers/drafts.ts +23 -0
- package/src/web/client/features/providers/index.ts +179 -87
- package/src/web/client/features/providers/strings.ts +48 -17
- package/src/web/client/features/providers/types.ts +15 -0
- package/src/web/client/features/settings/general.ts +12 -0
- package/src/web/client/features/settings/strings.ts +6 -0
- package/src/web/client/main.ts +4 -0
- package/src/web/client/shell/index.ts +1 -1
- package/src/web/client/ui/fields.ts +3 -1
- package/src/web/client/ui/icons.ts +9 -1
- package/src/web/client/ui/prompt-input.tsx +17 -5
- package/src/web/client/ui/strings.ts +0 -2
- package/src/web/diagnostics.ts +133 -0
- package/src/web/public/login.html +67 -0
- package/src/web/public/styles.css +130 -18
- package/src/web/server.ts +229 -21
- package/src/web/shared/client-panel.ts +3 -0
- package/src/web/shared/console-protocol.ts +16 -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 +8 -0
- package/src/worlds/minecraft/client-launch.ts +11 -2
- package/src/worlds/minecraft/client.ts +4 -0
- package/src/worlds/minecraft/mineflayer-fixes.ts +55 -1
- package/src/worlds/qq/normalize.ts +14 -0
- package/src/worlds/qq/world.ts +56 -10
- package/src/worlds/terminal/world.ts +3 -4
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, providerAtBoot } 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,39 +160,47 @@ 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
|
|
|
166
|
-
const activeProvider = cfg
|
|
167
|
-
|
|
168
|
-
console.error(
|
|
169
|
-
`activeProvider="${cfg.activeProvider}" 在 providers 段里不存在(现有: ${Object.keys(cfg.providers ?? {}).join(' / ') || '无'})`,
|
|
170
|
-
);
|
|
171
|
-
process.exit(1);
|
|
172
|
-
}
|
|
173
|
-
const endpointDir = providerDir(cfg.activeProvider);
|
|
182
|
+
const activeProvider = providerAtBoot(cfg);
|
|
183
|
+
const endpointDir = activeProvider ? providerDir(cfg.activeProvider) : null;
|
|
174
184
|
const missingSecret =
|
|
175
|
-
activeProvider
|
|
185
|
+
activeProvider?.secret && endpointDir && !secretReader(resolve(endpointDir, '.env'))(activeProvider.secret)
|
|
176
186
|
? activeProvider.secret
|
|
177
187
|
: null;
|
|
178
188
|
|
|
179
189
|
const bot = createBot(loaded, definition, { extensions });
|
|
180
190
|
|
|
181
191
|
// 必须在启动主循环前暂停,避免首批事件提前投递。
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
process.env.CORTICO_START_PAUSED === 'true' ||
|
|
185
|
-
process.argv.includes('--paused');
|
|
186
|
-
if (startPaused) bot.core.bus.setPaused(true);
|
|
192
|
+
const paused = startsPaused();
|
|
193
|
+
if (paused) bot.core.bus.setPaused(true);
|
|
187
194
|
|
|
188
195
|
const { port } = await bot.start();
|
|
189
196
|
|
|
190
197
|
console.log(`\n Bot: ${botName}${cfg.displayName && cfg.displayName !== botName ? ` (${cfg.displayName})` : ''}`);
|
|
198
|
+
const consoleUrl = port === null ? null : consoleUrlOf(bot.webApp?.boundAddress ?? null, port);
|
|
191
199
|
if (port !== null) {
|
|
192
|
-
console.log(` 控制台:
|
|
193
|
-
const assetsProblem = webAssetsProblem(fileURLToPath(new URL('
|
|
200
|
+
console.log(` 控制台: ${consoleUrl}${listensOnEveryInterface(bot.webApp?.boundAddress ?? null) ? `(监听 ${bot.webApp?.boundAddress},所有网卡)` : ''}`);
|
|
201
|
+
const assetsProblem = webAssetsProblem(fileURLToPath(new URL('..', import.meta.url)));
|
|
194
202
|
if (assetsProblem) {
|
|
195
|
-
console.log(` ⚠
|
|
203
|
+
console.log(` ⚠ 控制台产物需要重建(${assetsProblem});停止 bot 后运行 pnpm build:web`);
|
|
196
204
|
}
|
|
197
205
|
}
|
|
198
206
|
for (const slot of bot.assembly.slots) {
|
|
@@ -208,13 +216,13 @@ async function main(): Promise<void> {
|
|
|
208
216
|
? ` 扩展: ${ext.name}@${ext.version} · bot 包,本部署未引用`
|
|
209
217
|
: ` 扩展: ${ext.name} 未加载: ${ext.reason}`);
|
|
210
218
|
}
|
|
211
|
-
console.log(` 主模型: ${
|
|
212
|
-
if (missingSecret) {
|
|
219
|
+
console.log(` 主模型: ${activeProvider?.spec?.model || '未配置;请在控制台「模型供应商」页选择连接与模型'}`);
|
|
220
|
+
if (missingSecret && endpointDir) {
|
|
213
221
|
const envFile = resolve(endpointDir, '.env');
|
|
214
222
|
const where = existsSync(envFile) ? `${envFile} 里也没有` : `${envFile} 不存在`;
|
|
215
223
|
console.log(` ⚠ 缺少 ${missingSecret}:进程环境里没有,${where};可在控制台「模型提供商」页修改密钥变量名或补填密钥`);
|
|
216
224
|
}
|
|
217
|
-
if (
|
|
225
|
+
if (paused) {
|
|
218
226
|
console.log(' ⏸ 已暂停');
|
|
219
227
|
}
|
|
220
228
|
console.log('');
|
|
@@ -223,7 +231,7 @@ async function main(): Promise<void> {
|
|
|
223
231
|
process.env.CORTICO_OPEN_BROWSER === '1' ||
|
|
224
232
|
process.env.CORTICO_OPEN_BROWSER === 'true' ||
|
|
225
233
|
process.argv.includes('--open');
|
|
226
|
-
if (openBrowserFlag &&
|
|
234
|
+
if (openBrowserFlag && consoleUrl !== null) openBrowser(consoleUrl);
|
|
227
235
|
|
|
228
236
|
// 此时限覆盖整个关机流程;各步骤的时限由 bot.shutdown() 管理。
|
|
229
237
|
const SHUTDOWN_GRACE_MS = 35_000;
|
|
@@ -12,7 +12,9 @@ export function withoutPastReasoning(entries: readonly ContextRecord[]): Context
|
|
|
12
12
|
}
|
|
13
13
|
export function responseRequest(spec: ModelSpec, context: readonly ContextRecord[], tools: readonly ToolSchema[] = []): Request {
|
|
14
14
|
return {
|
|
15
|
-
model: spec.model, input: context.map(inputItem),
|
|
15
|
+
model: spec.model, input: context.map(inputItem),
|
|
16
|
+
// 只有协议声明的成员进线;运行时传进来的对象还带着分类标签,它归控制台与工具装配。
|
|
17
|
+
tools: tools.map(({ name, description, parameters }) => ({ type: 'function' as const, name, description, parameters })),
|
|
16
18
|
// effort 词表归端点(见 ModelSpec.reasoningEffort);协议枚举只覆盖 OpenAI 自己的取值。
|
|
17
19
|
reasoning: spec.thinking ? { ...(spec.reasoningEffort ? { effort: spec.reasoningEffort as Schemas['ReasoningEffortEnum'] } : {}) } : { effort: 'none' },
|
|
18
20
|
...(spec.temperature !== undefined ? { temperature: spec.temperature } : {}),
|
|
@@ -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 -->
|
|
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,20 +22,23 @@
|
|
|
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
|
-
`options.*`
|
|
32
|
-
|
|
30
|
+
地址、密钥变量名与图像开关不归模块:框架在 `console/config.ts` 里为每个端点声明这一组,
|
|
31
|
+
扩展来的模块照样有。模块自己的 `options.*` 走 `config()` 的配置组,模块的面板作为端点编辑页的段落
|
|
32
|
+
用控制台的 schema 渲染器画同一份声明、经 `ctx.setConfig` 暂存到连接草稿——内建 llamacpp 的运行时与启动
|
|
33
|
+
两段走的是这条。段落顺序由 `console()` 的 `panels` 定:编辑页自带的四块(`console/config.ts` 的
|
|
34
|
+
`connectionBlocks`)与模块自己的面板排成一列。`console()` 显式给空 `config` 表示这一页不另开配置页签,
|
|
35
|
+
声明仍参与服务端校验。启停与模型操作这类动作走面板 invoke。
|
|
33
36
|
|
|
34
37
|
`create()` 返回 `ProviderInstance`:`client`(实现 `respond`)、`listModels?`、`control?`、
|
|
35
38
|
`compatibilityKey?`、`start?` / `stop?`、`contextWindow?(model)`。
|
|
36
39
|
|
|
37
40
|
`ProviderHost` 给实例:`stateDir`(`<部署根>/providers/<端点名>/`,归实例独占)、`repoRoot`、
|
|
38
|
-
`resource()`、`currentEntry()`、`secret(name)`(
|
|
41
|
+
`resource()`、`currentEntry()`、`secret(name)`(进程环境优先,否则现读 `stateDir/.env`)、
|
|
39
42
|
`readBlob()`、`keepThinking()`、`log`。
|
|
40
43
|
|
|
41
44
|
## 注册与解析
|
|
@@ -44,10 +47,11 @@ effort 收任意非空串)、`serviceTiers`、`create(name, entry, host)`。可
|
|
|
44
47
|
之前通过 `registerProviderModules()` 注册,id 重复时抛错。
|
|
45
48
|
|
|
46
49
|
`ProviderRegistry.resolve(name)`:按 `entry.kind` 找模块,`normalize`,以去掉 `pricing` 与
|
|
47
|
-
`spec` 的条目 JSON
|
|
50
|
+
`spec` 的条目 JSON 加端点 `.env` 的内容指纹为缓存键(改模型或价格不重建实例,密钥文件变更后
|
|
51
|
+
下一次解析重建),填 `stateDir` 与 `secret`。
|
|
48
52
|
`bind(name)` 在实例外包一层:注入 `quote`(报价快照)与 `origin`(实例、模块、模型、
|
|
49
53
|
`compatibilityDomain` = sha256(kind + baseUrl + `compatibilityKey()`))。Responses 历史推理仅在实例、
|
|
50
|
-
模块、兼容域与模型均匹配时回传。`invalidate()`
|
|
54
|
+
模块、兼容域与模型均匹配时回传。`invalidate()` 清除缓存,控制台保存端点后调用;`.env` 变更经缓存键在下一次解析生效。
|
|
51
55
|
|
|
52
56
|
## 配置形状
|
|
53
57
|
|
|
@@ -62,14 +66,18 @@ Core 侧:`activeProviderEntry()` / `activeSpec()` 每次现读;`contextWindowOf(
|
|
|
62
66
|
## transport
|
|
63
67
|
|
|
64
68
|
`response-http.ts` 处理 HTTP/SSE:一次生成可包含多次请求尝试,重试间隔为 `[1s, 4s, 10s]`;超时
|
|
65
|
-
四档(流式首包 300s、非流式 120s、帧空闲 120s、内容空闲 300s);只对状态 0 / 429 / 5xx
|
|
66
|
-
401 / 403 先 `transport.refresh()`
|
|
69
|
+
四档(流式首包 300s、非流式 120s、帧空闲 120s、内容空闲 300s);只对状态 0 / 408 / 429 / 5xx
|
|
70
|
+
重试,响应带 `Retry-After` 时按它退避,401 / 403 先 `transport.refresh()`
|
|
71
|
+
一次;已提交不可逆增量后不再重试;输出字符超过
|
|
67
72
|
`max_output_tokens × 12` 时终止请求并报告超限;终态只接受 `completed` / `incomplete`,`failed` 抛
|
|
68
73
|
`LLMError`;每次请求尝试记录 `meters` 与 `charges`。
|
|
69
74
|
|
|
70
75
|
`responses-input.ts` 是原生 Responses 的无状态重放(system / developer 上提为
|
|
71
|
-
`instructions
|
|
72
|
-
|
|
76
|
+
`instructions`;思维链按 `reasoningReplay` 以签名块或 `reasoning_text` 回传,明文形态下没有记录来源
|
|
77
|
+
的调用前补一项合成推理);`chat.ts` + `native-input.ts` / `history.ts` 转换 Chat Completions 请求(历史
|
|
78
|
+
思维链回不回传由模块定:`renderMessagesWithMedia` 的 `keepReasoning` 决定出不出线,`dropPastThinking`
|
|
79
|
+
把合成开头之外的每条抹空);
|
|
80
|
+
`response-assembly.ts` 把两种流归一成 Open Responses 的 Item 流,
|
|
73
81
|
`finish_reason` 的 `length` / `content_filter` 落成 `incomplete_details.reason`;
|
|
74
82
|
`response-meters.ts` 把两种 usage 归一成 `TokenMeters`,缺项保持 null。
|
|
75
83
|
|
|
@@ -83,3 +91,27 @@ Core 侧:`activeProviderEntry()` / `activeSpec()` 每次现读;`contextWindowOf(
|
|
|
83
91
|
模块默认,快照带 sha256 id 与 `capturedAt`。计量键:`input` / `output` / `total` /
|
|
84
92
|
`cachedInput` / `uncachedInput` / `reasoning` 与 `detail:*`,单价按每百万 token。实际扣费在
|
|
85
93
|
`src/core/generation.ts` 的 `priceUsage()`,缺计量记 `amount: null`。
|
|
94
|
+
|
|
95
|
+
## 端点配置接口
|
|
96
|
+
|
|
97
|
+
`console/hub.ts` 的 `ProviderHub` 是控制台改端点的唯一入口,`hub-api.ts` 把它接到
|
|
98
|
+
`/api/providers`(聚合、按名读取、保存、删除、设为当前、测试、模型列表)与
|
|
99
|
+
`/api/provider-modules`(已注册模块)。列表保留 `kind` 没有对应模块的端点,active 与可用性
|
|
100
|
+
分别报告;状态帧的 `modelConnection` 带当前端点的名称、模型、模块与地址。
|
|
101
|
+
|
|
102
|
+
新名称由 `name.ts` 校验:英文字母、数字、`-` 或 `_`,首字符是字母或数字,不收空格与 Windows
|
|
103
|
+
设备名——名字就是 `<部署根>/providers/` 下的目录名。已在磁盘上的名字不改就继续有效。保存带
|
|
104
|
+
读取时的 revision(端点 `config.json` 与 `.env` 的 sha256),对不上返回 409。共享目录上的写锁
|
|
105
|
+
`.write-lock` 串行化各进程的写入。改名同时改目录和部署根内各 `deployment.json` 的
|
|
106
|
+
`activeProvider`,中途失败回滚已动过的文件和目录;还被引用的端点不能删。
|
|
107
|
+
|
|
108
|
+
保存一次校验模型、连接与模块配置,再写 `config.json` 和可选密钥;密钥进该端点的 `.env`,读取
|
|
109
|
+
接口不返回密钥值。写完清掉本进程的实例缓存,已绑定的请求与 fork 保持原客户端;其他进程在下次
|
|
110
|
+
重读共享配置时跟上。
|
|
111
|
+
|
|
112
|
+
`ProviderConsoleHost.editing` 表示这个实例来自还没保存的表单,模块据此拒绝运行时副作用,
|
|
113
|
+
`host.save` 把配置交回浏览器暂存。`ProviderRegistry.previewRegistry` 按一份条目造注册表,实例不进
|
|
114
|
+
运行实例缓存,浏览器刚输入的密钥作为覆盖值排在进程环境与端点 `.env` 之前;测试与模型列表带
|
|
115
|
+
`{ entry, secretValue }` 时走它。
|
|
116
|
+
|
|
117
|
+
连接列表的 usage 包含其他部署的选用记录及运行锁状态,排除当前部署。运行状态按部署配置的 paths.data(缺省使用 Core 默认值)检查实例锁,不代表供应商正在处理请求,也不建立供应商独占锁。
|
package/src/providers/base.ts
CHANGED
|
@@ -70,6 +70,7 @@ export interface ProviderAvailability {
|
|
|
70
70
|
export interface ProviderModule {
|
|
71
71
|
id: string;
|
|
72
72
|
title: string;
|
|
73
|
+
description?: string;
|
|
73
74
|
defaultBaseUrl?: string;
|
|
74
75
|
/** Candidate endpoint URLs offered on the URL field. Candidates only: any URL is accepted. */
|
|
75
76
|
baseUrlSuggestions?: readonly string[];
|
|
@@ -83,6 +84,7 @@ export interface ProviderModule {
|
|
|
83
84
|
* Absent (or a field left out) = the tables above are shown as written.
|
|
84
85
|
*/
|
|
85
86
|
localize?(language: Language): {
|
|
87
|
+
description?: string;
|
|
86
88
|
reasoningTiers?: readonly ReasoningTier[];
|
|
87
89
|
serviceTiers?: readonly ServiceTier[];
|
|
88
90
|
temperatureNote?: string;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ConfigGroup } from '../../core/config-schema.ts';
|
|
2
|
+
import type { LLMProviderEntry } from '../../core/types.ts';
|
|
3
|
+
import type { Language } from '../../core/language.ts';
|
|
4
|
+
import type { ConsolePanelDecl } from '../../web/shared/console-protocol.ts';
|
|
5
|
+
import { text } from './strings.ts';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The editor's own sections with their default wording. A module lists them among its panels,
|
|
9
|
+
* in the order its workflow wants, and may override title and description.
|
|
10
|
+
*/
|
|
11
|
+
export function connectionBlocks(language: Language): Record<'endpoint' | 'model' | 'pricing' | 'protocol', ConsolePanelDecl> {
|
|
12
|
+
const S = text(language);
|
|
13
|
+
return {
|
|
14
|
+
endpoint: { id: 'endpoint', title: S.endpointBlock, description: S.endpointBlockDescription, builtin: 'connection-endpoint' },
|
|
15
|
+
model: { id: 'model', title: S.modelBlock, description: S.modelBlockDescription, builtin: 'connection-model' },
|
|
16
|
+
pricing: { id: 'pricing', title: S.pricingBlock, builtin: 'connection-pricing' },
|
|
17
|
+
protocol: { id: 'protocol', title: S.protocolBlock, description: S.protocolBlockDescription, builtin: 'connection-protocol' },
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function connectionGroup(name: string, entry: LLMProviderEntry, language: Language): ConfigGroup {
|
|
22
|
+
const S = text(language);
|
|
23
|
+
const prefix = `providers.${name}.`;
|
|
24
|
+
return {
|
|
25
|
+
id: `llm.${entry.kind}.${name}.connection`,
|
|
26
|
+
owner: `provider:${entry.kind}`,
|
|
27
|
+
schema: {
|
|
28
|
+
type: 'object', title: name, description: S.connectionDescription,
|
|
29
|
+
properties: {
|
|
30
|
+
[`${prefix}baseUrl`]: { type: 'string', title: S.baseUrl, 'x-hot': true },
|
|
31
|
+
[`${prefix}secret`]: { type: 'string', title: S.secret, description: S.secretDescription, 'x-hot': true },
|
|
32
|
+
[`${prefix}multimodal`]: { type: 'boolean', title: S.multimodal, 'x-hot': true },
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|