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/core/loop.ts
CHANGED
|
@@ -245,12 +245,13 @@ export class MainLoop {
|
|
|
245
245
|
/** 截断与前缀重载共用的维护串行链,避免两个 session.reset 互相覆盖。 */
|
|
246
246
|
private maintenanceChain: Promise<void> = Promise.resolve();
|
|
247
247
|
private prefixReloadPromise: Promise<void> | null = null;
|
|
248
|
-
|
|
249
|
-
|
|
248
|
+
private clearSessionPromise: Promise<void> | null = null;
|
|
249
|
+
/** 在一轮处理中请求的前缀重载与清空,等自然回合边界再释放。 */
|
|
250
|
+
private releaseAtBoundary: Array<() => void> = [];
|
|
250
251
|
/** 当前正在处理一个事件批;手动交接必须等到该批自然结束,不能重置半轮session。 */
|
|
251
252
|
private processingBatch = false;
|
|
252
253
|
private handoffRequested = false;
|
|
253
|
-
/** onDelivery
|
|
254
|
+
/** onDelivery 执行期间(含其 Promise 完成前),injectInternal 的即时项加入当前批,不经过总线。 */
|
|
254
255
|
private deliveryCollector: EventEnvelope[] | null = null;
|
|
255
256
|
/** 已投递但前面仍有外部缺口的游标;水位只越过连续前缀。 */
|
|
256
257
|
private readonly deliveredCursors = new Set<number>();
|
|
@@ -483,7 +484,7 @@ export class MainLoop {
|
|
|
483
484
|
/**
|
|
484
485
|
* 将一批事件写入主 session。即时事件在前,候选生成内容与延迟渲染内容在后。
|
|
485
486
|
* 候选按 source、origin 和处理函数分组,再按来源项在批次中的顺序生成正文。
|
|
486
|
-
*
|
|
487
|
+
* 正文归档后调用并等待 onDelivery;钩子完成前注入的内部项追加到内部行末尾、外部正文之前。
|
|
487
488
|
* 内部行合成一条 user 消息;外部正文按 eventDelivery 进入合成工具回执或同一条 user 消息。
|
|
488
489
|
*/
|
|
489
490
|
private async deliverBatch(batch: WakeItem[], generation: number): Promise<boolean> {
|
|
@@ -536,11 +537,13 @@ export class MainLoop {
|
|
|
536
537
|
}
|
|
537
538
|
if (!this.active(generation)) return false;
|
|
538
539
|
if (delivered.length > 0) {
|
|
539
|
-
//
|
|
540
|
+
// 捕获钩子完成前的即时注入;钩子完成后恢复总线投递。
|
|
540
541
|
const injected: EventEnvelope[] = [];
|
|
541
542
|
this.deliveryCollector = injected;
|
|
542
543
|
try {
|
|
543
|
-
|
|
544
|
+
// 同步钩子不经过 await,收集范围仍只是钩子本身的执行期。
|
|
545
|
+
const pending = persona.onDelivery?.({ events: [...delivered] });
|
|
546
|
+
if (pending) await pending;
|
|
544
547
|
} catch (e) {
|
|
545
548
|
log.warn('onDelivery钩子异常', { err: e });
|
|
546
549
|
} finally {
|
|
@@ -722,13 +725,15 @@ export class MainLoop {
|
|
|
722
725
|
const latest = store.latestCursor();
|
|
723
726
|
for (let c = top + 1; c <= latest; c++) {
|
|
724
727
|
const next = store.get(c);
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
728
|
+
// 清空分片或跳过损坏行留下的空位没有可投递内容,不阻止水位推进。
|
|
729
|
+
if (next) {
|
|
730
|
+
// 未处理的 archive-only 项必须保留在水位之后,重启才会补投。
|
|
731
|
+
// 已处理项由 settledArchives 标识。
|
|
732
|
+
const skippable = next.origin === 'internal'
|
|
733
|
+
|| (next.contextDelivery === 'archive-only' && this.settledArchives.has(c));
|
|
734
|
+
// 集合记录存储位置;装载期重排保证 next.cursor === c。
|
|
735
|
+
if (!skippable && !this.deliveredCursors.has(c)) break;
|
|
736
|
+
}
|
|
732
737
|
this.deliveredCursors.delete(c);
|
|
733
738
|
this.settledArchives.delete(c);
|
|
734
739
|
top = c;
|
|
@@ -904,8 +909,8 @@ export class MainLoop {
|
|
|
904
909
|
} finally {
|
|
905
910
|
this.processingBatch = false;
|
|
906
911
|
}
|
|
907
|
-
// 异常退出由 stop
|
|
908
|
-
await this.
|
|
912
|
+
// 异常退出由 stop 释放排队请求;只有正常批次边界执行重载与清空。
|
|
913
|
+
await this.flushBoundaryMaintenance();
|
|
909
914
|
}
|
|
910
915
|
} finally {
|
|
911
916
|
this.stop();
|
|
@@ -933,7 +938,11 @@ export class MainLoop {
|
|
|
933
938
|
for (let round = 1; ; round++) {
|
|
934
939
|
if (!this.active(generation)) return;
|
|
935
940
|
this.roundsLastBatch = round;
|
|
936
|
-
|
|
941
|
+
let spec: ModelSpec;
|
|
942
|
+
try { spec = this.d.spec(); } catch (error) {
|
|
943
|
+
log.warn('模型配置不可用', { error: String(error) });
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
937
946
|
// 后续轮输入超限时结束本批,由批末检查执行交接。
|
|
938
947
|
// 首轮仍处理本批新投递的事件;上一批的容量检查已在批末执行。
|
|
939
948
|
if (round > 1) {
|
|
@@ -1425,9 +1434,25 @@ export class MainLoop {
|
|
|
1425
1434
|
return rebuildTail(paired, budget, estimate);
|
|
1426
1435
|
}
|
|
1427
1436
|
|
|
1428
|
-
/**
|
|
1429
|
-
|
|
1437
|
+
/**
|
|
1438
|
+
* 清空主 session,重建 system 前缀并调用 Persona 开场钩子;事件库保留。
|
|
1439
|
+
* 正在处理批次时等到该批自然结束,不重置半轮 session;并发请求复用同一 Promise。
|
|
1440
|
+
*/
|
|
1441
|
+
clearSession(): Promise<void> {
|
|
1430
1442
|
const generation = this.generation;
|
|
1443
|
+
if (!this.active(generation)) return Promise.resolve();
|
|
1444
|
+
if (this.clearSessionPromise) return this.clearSessionPromise;
|
|
1445
|
+
let tracked: Promise<void>;
|
|
1446
|
+
tracked = this.safeBoundary()
|
|
1447
|
+
.then(() => this.enqueueMaintenance(() => this.performClearSession(generation), generation))
|
|
1448
|
+
.finally(() => {
|
|
1449
|
+
if (this.clearSessionPromise === tracked) this.clearSessionPromise = null;
|
|
1450
|
+
});
|
|
1451
|
+
this.clearSessionPromise = tracked;
|
|
1452
|
+
return tracked;
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
private async performClearSession(generation: number): Promise<void> {
|
|
1431
1456
|
if (!this.active(generation)) return;
|
|
1432
1457
|
const { session, log } = this.d;
|
|
1433
1458
|
const sysMsg = await this.buildSystem();
|
|
@@ -1447,11 +1472,8 @@ export class MainLoop {
|
|
|
1447
1472
|
const generation = this.generation;
|
|
1448
1473
|
if (!this.active(generation)) return Promise.resolve();
|
|
1449
1474
|
if (this.prefixReloadPromise) return this.prefixReloadPromise;
|
|
1450
|
-
const safeBoundary = this.processingBatch
|
|
1451
|
-
? new Promise<void>((resolve) => { this.releasePrefixReload = resolve; })
|
|
1452
|
-
: Promise.resolve();
|
|
1453
1475
|
let tracked: Promise<void>;
|
|
1454
|
-
tracked = safeBoundary
|
|
1476
|
+
tracked = this.safeBoundary()
|
|
1455
1477
|
.then(() => this.enqueueMaintenance(() => this.performSystemPrefixReload(generation), generation))
|
|
1456
1478
|
.finally(() => {
|
|
1457
1479
|
if (this.prefixReloadPromise === tracked) this.prefixReloadPromise = null;
|
|
@@ -1460,12 +1482,22 @@ export class MainLoop {
|
|
|
1460
1482
|
return tracked;
|
|
1461
1483
|
}
|
|
1462
1484
|
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
if (!
|
|
1466
|
-
this.
|
|
1467
|
-
|
|
1468
|
-
|
|
1485
|
+
/** 空闲时立即;正在处理批次时等 run() 在批末释放,或 stop() 释放。 */
|
|
1486
|
+
private safeBoundary(): Promise<void> {
|
|
1487
|
+
if (!this.processingBatch) return Promise.resolve();
|
|
1488
|
+
return new Promise<void>((resolve) => { this.releaseAtBoundary.push(resolve); });
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
private releaseBoundary(): void {
|
|
1492
|
+
const waiting = this.releaseAtBoundary;
|
|
1493
|
+
this.releaseAtBoundary = [];
|
|
1494
|
+
for (const release of waiting) release();
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
private async flushBoundaryMaintenance(): Promise<boolean> {
|
|
1498
|
+
if (this.releaseAtBoundary.length === 0) return false;
|
|
1499
|
+
this.releaseBoundary();
|
|
1500
|
+
await Promise.all([this.prefixReloadPromise, this.clearSessionPromise]);
|
|
1469
1501
|
return true;
|
|
1470
1502
|
}
|
|
1471
1503
|
|
|
@@ -1633,7 +1665,7 @@ export class MainLoop {
|
|
|
1633
1665
|
return this.stallAt.filter((t) => t >= from).length;
|
|
1634
1666
|
}
|
|
1635
1667
|
|
|
1636
|
-
/** 注入 Persona 提供的内部文本。onDelivery
|
|
1668
|
+
/** 注入 Persona 提供的内部文本。onDelivery 完成前加入当前批,其余时刻进入总线。 */
|
|
1637
1669
|
injectInternal(text: string, kind = 'notice'): void {
|
|
1638
1670
|
if (!this.activeNow()) return;
|
|
1639
1671
|
const item = this.internalItem('persona', kind, text);
|
|
@@ -1721,9 +1753,7 @@ export class MainLoop {
|
|
|
1721
1753
|
this.backoffWake?.();
|
|
1722
1754
|
if (!this.shutdown.signal.aborted) this.shutdown.abort(new Error('core 正在关机'));
|
|
1723
1755
|
this.handoffRequested = false;
|
|
1724
|
-
|
|
1725
|
-
this.releasePrefixReload = null;
|
|
1726
|
-
releasePrefixReload?.();
|
|
1756
|
+
this.releaseBoundary();
|
|
1727
1757
|
this.stopFn?.();
|
|
1728
1758
|
const round = this.currentRound;
|
|
1729
1759
|
if (round && !round.controller.signal.aborted) {
|
package/src/core/secrets.ts
CHANGED
|
@@ -3,16 +3,14 @@ import { existsSync } from 'node:fs';
|
|
|
3
3
|
import { readTextFile } from './util.ts';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
6
|
+
* 每次优先读取非空进程环境变量;否则现读文件,缺失返回空串。
|
|
7
7
|
* 文件值读取到首个空白字符,不解析引号。
|
|
8
8
|
*/
|
|
9
9
|
export function secretReader(file: string): (name: string) => string {
|
|
10
|
-
let text: string | null = null;
|
|
11
10
|
return (name: string): string => {
|
|
12
11
|
const fromEnv = process.env[name];
|
|
13
12
|
if (fromEnv) return fromEnv;
|
|
14
|
-
|
|
15
|
-
const m = new RegExp(`^\\s*${name}\\s*=\\s*(\\S+)`, 'm').exec(text);
|
|
13
|
+
const m = new RegExp(`^\\s*${name}\\s*=\\s*(\\S+)`, 'm').exec(existsSync(file) ? readTextFile(file) : '');
|
|
16
14
|
return m ? m[1] : '';
|
|
17
15
|
};
|
|
18
16
|
}
|
package/src/core/session.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Session context is append-only between lifecycle resets. */
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import type { ContextRecord } from '../protocol/open-responses/context.ts';
|
|
4
|
-
import { ContextLog } from '../protocol/open-responses/context-log.ts';
|
|
4
|
+
import { ContextLog, type ContextLoadRepair } from '../protocol/open-responses/context-log.ts';
|
|
5
5
|
import { estimateMessagesTokens } from './util.ts';
|
|
6
6
|
|
|
7
7
|
|
|
@@ -38,7 +38,8 @@ export class SessionLog {
|
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
/** 返回对未写完末行的修复;其他损坏行抛错。 */
|
|
42
|
+
load(): ContextLoadRepair | null { return this.context.load(); }
|
|
42
43
|
|
|
43
44
|
/** 整体替换:先写临时文件,再 rename 覆盖目标文件。 */
|
|
44
45
|
reset(messages: readonly ContextRecord[]): void {
|
package/src/core/timers.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* 通用持久定时器:到点回调持有方、跨重启恢复。载荷不透明——
|
|
3
3
|
* 闹钟的备注、阻断、关键词这些语义归持有它的Persona。
|
|
4
4
|
*/
|
|
5
|
-
import { existsSync, mkdirSync, readFileSync, renameSync,
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
6
6
|
import { dirname, join } from 'node:path';
|
|
7
7
|
import type { Logger, TimerEntry, TimersApi } from './types.ts';
|
|
8
8
|
import { nullLogger, shortId } from './util.ts';
|
|
@@ -47,7 +47,8 @@ export class TimerStore implements TimersApi {
|
|
|
47
47
|
const entry: TimerEntry = { id: shortId('wake_'), atIso, payload };
|
|
48
48
|
this.entries.push(entry);
|
|
49
49
|
this.save();
|
|
50
|
-
|
|
50
|
+
// 已到期的也等 set 返回后再回调,持有方先拿到 id。
|
|
51
|
+
if (this.started) this.arm(entry, 'next-tick');
|
|
51
52
|
return { ok: true, id: entry.id };
|
|
52
53
|
}
|
|
53
54
|
|
|
@@ -76,9 +77,10 @@ export class TimerStore implements TimersApi {
|
|
|
76
77
|
return n;
|
|
77
78
|
}
|
|
78
79
|
|
|
79
|
-
|
|
80
|
+
/** 已到期的条目按 whenDue 立即回调或下一个宏任务回调;start() 用前者。 */
|
|
81
|
+
private arm(entry: TimerEntry, whenDue: 'now' | 'next-tick' = 'now'): void {
|
|
80
82
|
const delay = Date.parse(entry.atIso) - Date.now();
|
|
81
|
-
if (delay <= 0) {
|
|
83
|
+
if (delay <= 0 && whenDue === 'now') {
|
|
82
84
|
this.fire(entry.id);
|
|
83
85
|
return;
|
|
84
86
|
}
|
|
@@ -88,7 +90,7 @@ export class TimerStore implements TimersApi {
|
|
|
88
90
|
// 超长延迟按 MAX_TIMEOUT 分段重新 arm。
|
|
89
91
|
this.timers.set(entry.id, setTimeout(() => this.arm(entry), MAX_TIMEOUT));
|
|
90
92
|
} else {
|
|
91
|
-
this.timers.set(entry.id, setTimeout(() => this.fire(entry.id), delay));
|
|
93
|
+
this.timers.set(entry.id, setTimeout(() => this.fire(entry.id), Math.max(0, delay)));
|
|
92
94
|
}
|
|
93
95
|
}
|
|
94
96
|
|
|
@@ -123,7 +125,7 @@ export class TimerStore implements TimersApi {
|
|
|
123
125
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
124
126
|
const tmp = this.file + '.tmp';
|
|
125
127
|
writeFileSync(tmp, JSON.stringify(this.entries, null, 2), 'utf8');
|
|
126
|
-
|
|
128
|
+
// 通过 rename 原子覆盖,不先删除,避免崩溃后定时器全部丢失。
|
|
127
129
|
renameSync(tmp, this.file);
|
|
128
130
|
}
|
|
129
131
|
}
|
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>;
|
|
@@ -550,6 +551,7 @@ export interface DeliveryGateApi {
|
|
|
550
551
|
/** 运行时查询当前模型的名称、MIME 支持和上下文窗口;配置热改后读取新值。 */
|
|
551
552
|
export interface ModelFacts {
|
|
552
553
|
model(): string;
|
|
554
|
+
/** 未选端点或端点未选模型时返回 false;activeProvider 指向不存在的端点时抛错。 */
|
|
553
555
|
accepts(mime: string): boolean;
|
|
554
556
|
/** provider 探测值与手动配置取较小者,单位 token;两者均未知时返回 undefined。 */
|
|
555
557
|
contextWindow(): number | undefined;
|
|
@@ -833,8 +835,11 @@ export interface World {
|
|
|
833
835
|
* 语言不缓存,不影响发给模型的文本;未实现时仅显示通用信息。
|
|
834
836
|
*/
|
|
835
837
|
console?(language?: Language): WorldConsoleDecl;
|
|
836
|
-
/**
|
|
837
|
-
|
|
838
|
+
/**
|
|
839
|
+
* 主 session 的输出流接收器;装配层合并已挂载 World 的接收器,随挂载变化更新。
|
|
840
|
+
* 这一刻没有接收器(未连接、模式关着)时返回 undefined,合并侧跳过。
|
|
841
|
+
*/
|
|
842
|
+
outputTap?(): OutputTap | undefined;
|
|
838
843
|
/** 挂载时接收宿主并开始连接平台、推送事件。 */
|
|
839
844
|
start(host: WorldHost): Promise<void>;
|
|
840
845
|
stop(): Promise<void>;
|
|
@@ -906,9 +911,11 @@ export interface Persona {
|
|
|
906
911
|
onOpening?(ctx: { reason: SessionOpeningReason }): void;
|
|
907
912
|
/**
|
|
908
913
|
* 一批事件已渲染并分配游标、尚未进入上下文时调用;events 按投递序包含内部和外部事件。
|
|
909
|
-
*
|
|
914
|
+
* 返回 Promise 时 Core 等它完成再投递,不设期限:钩子不完成,主循环不前进,钩子自己发起的
|
|
915
|
+
* 外部调用由 Persona 负责超时。完成前 Persona 的所有 injectInternal 都加入当前批,排在已有
|
|
916
|
+
* 内部行之后、外部正文之前。钩子抛错或拒绝时 Core 记录 warn,带着已注入的内容照常投递。
|
|
910
917
|
*/
|
|
911
|
-
onDelivery?(ctx: { events: EventEnvelope[] }): void
|
|
918
|
+
onDelivery?(ctx: { events: EventEnvelope[] }): void | Promise<void>;
|
|
912
919
|
/**
|
|
913
920
|
* 一批事件处理结束时调用;Persona 可查询 sessionInfo 并决定是否请求交接。
|
|
914
921
|
* Core 在超过 hardTokens 时强制交接。
|
|
@@ -1120,6 +1127,18 @@ export interface CoreConfig {
|
|
|
1120
1127
|
};
|
|
1121
1128
|
web: {
|
|
1122
1129
|
port: number;
|
|
1130
|
+
/** 控制台监听地址;缺省 127.0.0.1。`0.0.0.0` 或 `::` 监听所有网卡。 */
|
|
1131
|
+
host?: string;
|
|
1132
|
+
/**
|
|
1133
|
+
* 回环名与监听地址之外还接受的 Host 名(域名、反向代理对外的名字),同时放行以它为
|
|
1134
|
+
* Origin 的写请求与 WebSocket;缺省为空。
|
|
1135
|
+
*/
|
|
1136
|
+
allowedHosts?: string[];
|
|
1137
|
+
/**
|
|
1138
|
+
* 控制台访问密码;缺省或空串时不要求登录。密钥 `CORTICO_WEB_PASSWORD`(进程环境或部署
|
|
1139
|
+
* `.env`)非空时优先于此键。
|
|
1140
|
+
*/
|
|
1141
|
+
password?: string;
|
|
1123
1142
|
/** 控制台配色方案 id;浏览器没有保存过选择时用它,认不出的 id 落到框架默认方案。 */
|
|
1124
1143
|
theme: string;
|
|
1125
1144
|
};
|
package/src/core/util.ts
CHANGED
|
@@ -385,3 +385,18 @@ export function readTextFile(file: string): string {
|
|
|
385
385
|
}
|
|
386
386
|
return bytes.toString('utf8');
|
|
387
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
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
* 部署文件中的 providers 字段不参与合并。Core、Persona 与 World 的默认值由各自所有者提供。
|
|
4
4
|
* 运行时共享合并后的配置对象,控制台和调参工具原位更新。
|
|
5
5
|
*/
|
|
6
|
-
import { existsSync, mkdirSync, readdirSync,
|
|
6
|
+
import { existsSync, mkdirSync, readdirSync, statSync, writeFileSync } from 'node:fs';
|
|
7
7
|
import { isAbsolute, resolve } from 'node:path';
|
|
8
8
|
import type { CoreConfig, LLMProviderEntry } from './core/types.ts';
|
|
9
9
|
import { deepMerge, type LoadedConfig } from './core/config.ts';
|
|
10
10
|
import { secretReader } from './core/secrets.ts';
|
|
11
|
+
import { readTextFile } from './core/util.ts';
|
|
11
12
|
import { deploymentRoot, repoRoot as codeRepoRoot } from './paths.ts';
|
|
12
13
|
|
|
13
14
|
export type { LoadedConfig } from './core/config.ts';
|
|
@@ -90,7 +91,7 @@ function globalProviders(providersDir: string): Record<string, LLMProviderEntry>
|
|
|
90
91
|
const file = resolve(providersDir, name, 'config.json');
|
|
91
92
|
if (!existsSync(file)) continue;
|
|
92
93
|
try {
|
|
93
|
-
table[name] = JSON.parse(
|
|
94
|
+
table[name] = JSON.parse(readTextFile(file)) as LLMProviderEntry;
|
|
94
95
|
} catch (err) {
|
|
95
96
|
throw new Error(`${file} 解析失败:${err instanceof Error ? err.message : String(err)}`);
|
|
96
97
|
}
|
|
@@ -110,7 +111,7 @@ function packageWorldOverrides(pkgDir: string): Record<string, unknown> {
|
|
|
110
111
|
const file = resolve(ioDir, id, 'config.json');
|
|
111
112
|
if (!existsSync(file)) continue;
|
|
112
113
|
try {
|
|
113
|
-
worlds[id] = JSON.parse(
|
|
114
|
+
worlds[id] = JSON.parse(readTextFile(file));
|
|
114
115
|
} catch (err) {
|
|
115
116
|
throw new Error(`${file} 解析失败:${err instanceof Error ? err.message : String(err)}`);
|
|
116
117
|
}
|
|
@@ -137,7 +138,7 @@ export function loadDeployment<C extends CoreConfig>(
|
|
|
137
138
|
const providers = resolve(providersDir);
|
|
138
139
|
const cfgPath = resolve(dir, 'config.json');
|
|
139
140
|
const raw: Partial<C> & Record<string, unknown> = existsSync(cfgPath)
|
|
140
|
-
? (JSON.parse(
|
|
141
|
+
? (JSON.parse(readTextFile(cfgPath)) as Partial<C> & Record<string, unknown>)
|
|
141
142
|
: {};
|
|
142
143
|
// 部署文件不能覆盖共享端点表。
|
|
143
144
|
delete raw.providers;
|
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
|
|
4
|
-
* provider 使用测试端点 create
|
|
3
|
+
* World 使用默认配置、无密钥,检查 create、tools、envPromptVars、console、工具名冲突与配置路径;
|
|
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,25 @@ 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
|
+
* 声明过的每个配置路径都要落在 prefix 指的段内;给了 `defaults` 时还要在里面有对应项。
|
|
44
|
+
* 缺默认值时控制台照样渲染旋钮、也照样写回 config.json,而读到的是代码里另一处的兜底值。
|
|
45
|
+
* provider 的旋钮写进端点条目而不是 `defaults()`,所以只核对段。
|
|
46
|
+
*/
|
|
47
|
+
function configPathProblems(group: ConfigGroup, prefix: string, defaults?: Record<string, unknown>): string[] {
|
|
48
|
+
const problems: string[] = [];
|
|
49
|
+
for (const path of Object.keys(group.schema.properties ?? {})) {
|
|
50
|
+
if (!path.startsWith(prefix)) {
|
|
51
|
+
problems.push(`配置组「${group.id}」声明的「${path}」不在 ${prefix} 段里:写回按路径走,值会落到别人的段上。`);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (defaults && getByPath(defaults, path.slice(prefix.length)) === undefined) {
|
|
55
|
+
problems.push(`配置组「${group.id}」声明的「${path}」在 defaults() 里没有对应项:旋钮能改、能写回 config.json,读到的仍是代码里的兜底值。`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return problems;
|
|
59
|
+
}
|
|
60
|
+
|
|
41
61
|
/** 面板 id 的形状:控制台一页内的局部 id。 */
|
|
42
62
|
const PANEL_ID = /^[a-z0-9-]+$/;
|
|
43
63
|
/** 工具名带前缀:`<短名>_` 起头。 */
|
|
@@ -190,6 +210,7 @@ export async function dryMountWorld(def: WorldDefinition<WorldSection>, opts: Wo
|
|
|
190
210
|
}
|
|
191
211
|
for (const group of decl.config ?? []) {
|
|
192
212
|
if (group.owner !== `world:${def.id}`) warnings.push(`配置组「${group.id}」的 owner 是「${group.owner}」,World 的配置组 owner 应为 world:${def.id}。`);
|
|
213
|
+
failures.push(...configPathProblems(group, `worlds.${def.id}.`, defaults));
|
|
193
214
|
}
|
|
194
215
|
const keys = new Set<string>();
|
|
195
216
|
for (const doc of decl.promptDocs ?? []) {
|
|
@@ -213,6 +234,9 @@ export interface ProviderDryMountOptions extends DryMountOptions {
|
|
|
213
234
|
hasConsoleClient?: boolean;
|
|
214
235
|
}
|
|
215
236
|
|
|
237
|
+
/** 假端点的名字;模块声明的配置路径按它组成 `providers.<端点名>.options.`。 */
|
|
238
|
+
const PROBE_ENDPOINT = 'check';
|
|
239
|
+
|
|
216
240
|
export function dryMountProvider(mod: ProviderModule, opts: ProviderDryMountOptions): DryMountReport {
|
|
217
241
|
const report: DryMountReport = { ok: [], warnings: [], failures: [] };
|
|
218
242
|
const { ok, warnings, failures } = report;
|
|
@@ -237,7 +261,7 @@ export function dryMountProvider(mod: ProviderModule, opts: ProviderDryMountOpti
|
|
|
237
261
|
return report;
|
|
238
262
|
}
|
|
239
263
|
|
|
240
|
-
const stateDir = join(opts.scratchDir, 'providers',
|
|
264
|
+
const stateDir = join(opts.scratchDir, 'providers', PROBE_ENDPOINT);
|
|
241
265
|
mkdirSync(stateDir, { recursive: true });
|
|
242
266
|
const resources = new Map<string, unknown>();
|
|
243
267
|
const host = {
|
|
@@ -254,7 +278,7 @@ export function dryMountProvider(mod: ProviderModule, opts: ProviderDryMountOpti
|
|
|
254
278
|
log: nullLogger(),
|
|
255
279
|
};
|
|
256
280
|
try {
|
|
257
|
-
const instance = mod.create(
|
|
281
|
+
const instance = mod.create(PROBE_ENDPOINT, entry, host);
|
|
258
282
|
if (typeof instance?.client?.respond !== 'function') {
|
|
259
283
|
failures.push('create() 返回的实例没有 client.respond():Core 只经它调模型。');
|
|
260
284
|
} else {
|
|
@@ -266,11 +290,14 @@ export function dryMountProvider(mod: ProviderModule, opts: ProviderDryMountOpti
|
|
|
266
290
|
|
|
267
291
|
if (mod.config) {
|
|
268
292
|
try {
|
|
269
|
-
const groups = mod.config(
|
|
293
|
+
const groups = mod.config(PROBE_ENDPOINT, entry, language);
|
|
270
294
|
if (!Array.isArray(groups)) failures.push('config() 没有返回数组。');
|
|
271
295
|
else {
|
|
272
296
|
for (const group of groups) {
|
|
273
297
|
if (group.owner !== `provider:${mod.id}`) warnings.push(`配置组「${group.id}」的 owner 是「${group.owner}」,provider 的配置组 owner 应为 provider:${mod.id}。`);
|
|
298
|
+
// 写回的三道闸都只按 `providers.<端点名>.` 过滤:段外的路径改不动,段内 options 之外的
|
|
299
|
+
// 路径覆盖框架自己编辑的字段(baseUrl、secret、multimodal、spec)。两类都拦在这里。
|
|
300
|
+
failures.push(...configPathProblems(group, `providers.${PROBE_ENDPOINT}.options.`));
|
|
274
301
|
}
|
|
275
302
|
ok.push(`config(): ${groups.length} 个配置组。`);
|
|
276
303
|
}
|
|
@@ -385,6 +412,7 @@ export function dryMountBot(def: BotDefinition<CoreConfig>, opts: BotDryMountOpt
|
|
|
385
412
|
|
|
386
413
|
for (const group of parts.console?.configGroups ?? []) {
|
|
387
414
|
if (group.owner !== 'persona') warnings.push(`配置组「${group.id}」的 owner 是「${group.owner}」,Persona 的配置组 owner 应为 persona。`);
|
|
415
|
+
failures.push(...configPathProblems(group, '', config));
|
|
388
416
|
}
|
|
389
417
|
return report;
|
|
390
418
|
}
|
|
@@ -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: 6,
|
|
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;
|