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/README.md
CHANGED
|
@@ -23,9 +23,9 @@ checker and the test runner. It has no entry point and starts nothing.
|
|
|
23
23
|
pnpm add -D cortico
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
your extension was written against.
|
|
26
|
+
Each extension kind has its own contract version — world 5, provider 5, bot 5 — and a package
|
|
27
|
+
declares its own as `"cortico": { "kind": "world", "api": 5 }` in its `package.json`. This
|
|
28
|
+
package's minor version tracks the framework, so pin the range your extension was written against.
|
|
29
29
|
|
|
30
30
|
`templates/extension/` in the repository holds a ready package for each kind, and
|
|
31
31
|
[docs/extensions.md](https://github.com/Pal-AI-Lab/Cortico/blob/main/docs/extensions.md) states
|
package/package.json
CHANGED
package/src/boot.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { existsSync, rmSync, writeFileSync } from 'node:fs';
|
|
6
6
|
import { join } from 'node:path';
|
|
7
|
+
import type { CoreConfig, LLMProviderEntry } from './core/types.ts';
|
|
7
8
|
|
|
8
9
|
export const RESTART_FLAG_FILE = '.restart-request';
|
|
9
10
|
|
|
@@ -19,6 +20,14 @@ export function isSupervised(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
|
19
20
|
return env[SUPERVISED_ENV] === '1' || env[SUPERVISED_ENV] === 'true';
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
/**
|
|
24
|
+
* 启动时是否暂停事件投递。每一轮受监管的启动拿到同一份环境和同一串参数,
|
|
25
|
+
* 所以这个值也是重启后的状态。
|
|
26
|
+
*/
|
|
27
|
+
export function startsPaused(env: NodeJS.ProcessEnv = process.env, argv: readonly string[] = process.argv): boolean {
|
|
28
|
+
return env.CORTICO_START_PAUSED === '1' || env.CORTICO_START_PAUSED === 'true' || argv.includes('--paused');
|
|
29
|
+
}
|
|
30
|
+
|
|
22
31
|
function notifyLauncher(message: { type: string; dataDir?: string }, env = process.env): void {
|
|
23
32
|
if (!isSupervised(env)) return;
|
|
24
33
|
process.send?.(message);
|
|
@@ -44,3 +53,23 @@ export function consumeBootFlags(dataDir: string): void {
|
|
|
44
53
|
}
|
|
45
54
|
}
|
|
46
55
|
}
|
|
56
|
+
|
|
57
|
+
export function listensOnEveryInterface(bound: string | null): boolean {
|
|
58
|
+
return bound === '0.0.0.0' || bound === '::';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** 本机打得开的控制台地址:监听所有网卡时给回环地址,IPv6 加方括号。 */
|
|
62
|
+
export function consoleUrlOf(bound: string | null, port: number): string {
|
|
63
|
+
const host = bound === null || listensOnEveryInterface(bound) ? '127.0.0.1' : bound.includes(':') ? `[${bound}]` : bound;
|
|
64
|
+
return `http://${host}:${port}/`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 空选择允许进入控制台完成配置;非空名称必须引用已有连接。 */
|
|
68
|
+
export function providerAtBoot(config: Pick<CoreConfig, 'activeProvider' | 'providers'>): LLMProviderEntry | undefined {
|
|
69
|
+
if (!config.activeProvider) return undefined;
|
|
70
|
+
const entry = config.providers[config.activeProvider];
|
|
71
|
+
if (!entry) {
|
|
72
|
+
throw new Error(`activeProvider="${config.activeProvider}" 在 providers 段里不存在(现有: ${Object.keys(config.providers).join(' / ') || '无'})`);
|
|
73
|
+
}
|
|
74
|
+
return entry;
|
|
75
|
+
}
|
package/src/bot.ts
CHANGED
|
@@ -19,7 +19,7 @@ import { coreConfigGroup } from './core/config.ts';
|
|
|
19
19
|
import { pick, resolveLanguage, type Language } from './core/language.ts';
|
|
20
20
|
import { updateJsonObject } from './config-file.ts';
|
|
21
21
|
import { ONBOARDING_FLAG_FILE } from './deploy.ts';
|
|
22
|
-
import { isSupervised, requestRestart } from './boot.ts';
|
|
22
|
+
import { isSupervised, requestRestart, startsPaused } from './boot.ts';
|
|
23
23
|
import { ExtensionManager, type ExtensionSet } from './extensions.ts';
|
|
24
24
|
import { WorldAssembly, type WorldDefinition, type WorldDeclaration, type WorldSection } from './world.ts';
|
|
25
25
|
import { Core, type WorldStopFailure } from './core/core.ts';
|
|
@@ -30,6 +30,7 @@ import { assembleSystemSegments, envPromptOverridePath, envPromptTemplateSource,
|
|
|
30
30
|
import { aggregateUsage } from './core/cost.ts';
|
|
31
31
|
import { nowIso, withDeadline } from './core/util.ts';
|
|
32
32
|
import { closeRun } from './core/run.ts';
|
|
33
|
+
import { ProviderHub } from './providers/console/hub.ts';
|
|
33
34
|
import { ProviderSettings } from './providers/console/settings.ts';
|
|
34
35
|
import { providerModules } from './providers/registry.ts';
|
|
35
36
|
import { readGroupValues, setByPath as setConfigPath } from './core/config-schema.ts';
|
|
@@ -51,6 +52,9 @@ import {
|
|
|
51
52
|
type ConsolePageContribution,
|
|
52
53
|
} from './web/shared/console-protocol.ts';
|
|
53
54
|
|
|
55
|
+
/** 控制台访问密码的密钥名;进程环境或部署 .env 里非空时优先于 web.password。 */
|
|
56
|
+
export const WEB_PASSWORD_SECRET = 'CORTICO_WEB_PASSWORD';
|
|
57
|
+
|
|
54
58
|
/** 启动器使用的 bot 包装配契约。 */
|
|
55
59
|
export interface BotDefinition<C extends CoreConfig = CoreConfig> {
|
|
56
60
|
/** 控制台页 id `persona:<id>` 与产物键取它;仓内包的 id 与目录同名。 */
|
|
@@ -166,7 +170,7 @@ const BOT_TEXT = {
|
|
|
166
170
|
},
|
|
167
171
|
session: {
|
|
168
172
|
label: '主session(当前对话上下文)',
|
|
169
|
-
note: '清除对话上下文并重新开场,保留 Memory
|
|
173
|
+
note: '清除对话上下文并重新开场,保留 Memory 和事件库。正在处理批次时等该批结束后执行',
|
|
170
174
|
stat: (records: number, ktok: number, size: string) => `${records}条 / ~${ktok}k tok / ${size}`,
|
|
171
175
|
cleared: 'session已清空重开(system前缀+开场消息)',
|
|
172
176
|
},
|
|
@@ -212,6 +216,12 @@ const BOT_TEXT = {
|
|
|
212
216
|
stat: (n: number) => `${n}条待投递`,
|
|
213
217
|
cleared: (n: number) => `已丢弃${n}条待投递事件`,
|
|
214
218
|
},
|
|
219
|
+
media: {
|
|
220
|
+
label: '附件库(事件与工具回执里的图片、音频)',
|
|
221
|
+
note: '删除全部附件文件,保留引用它们的事件与 session 记录;删掉的附件在上下文里只剩文字说明',
|
|
222
|
+
stat: (n: number, size: string) => `${n}份 / ${size}`,
|
|
223
|
+
cleared: (n: number) => `已删除${n}份附件`,
|
|
224
|
+
},
|
|
215
225
|
},
|
|
216
226
|
config: {
|
|
217
227
|
unknownGroup: (id: string) => `没有这一组配置: ${id}`,
|
|
@@ -260,7 +270,7 @@ const BOT_TEXT = {
|
|
|
260
270
|
},
|
|
261
271
|
session: {
|
|
262
272
|
label: 'Main session (current conversation context)',
|
|
263
|
-
note: 'Clears the conversation and reopens the session, keeping Memory and the event store.
|
|
273
|
+
note: 'Clears the conversation and reopens the session, keeping Memory and the event store. While a batch is in progress, runs after it finishes',
|
|
264
274
|
stat: (records: number, ktok: number, size: string) => `${records} records / ~${ktok}k tok / ${size}`,
|
|
265
275
|
cleared: 'Session cleared and reopened (system prefix + opening message)',
|
|
266
276
|
},
|
|
@@ -306,6 +316,12 @@ const BOT_TEXT = {
|
|
|
306
316
|
stat: (n: number) => `${n} pending`,
|
|
307
317
|
cleared: (n: number) => `Discarded ${n} pending events`,
|
|
308
318
|
},
|
|
319
|
+
media: {
|
|
320
|
+
label: 'Attachment store (images and audio from events and tool results)',
|
|
321
|
+
note: 'Deletes every attachment file and keeps the events and session records that reference them; a deleted attachment leaves only its text description in context',
|
|
322
|
+
stat: (n: number, size: string) => `${n} files / ${size}`,
|
|
323
|
+
cleared: (n: number) => `Deleted ${n} attachments`,
|
|
324
|
+
},
|
|
309
325
|
},
|
|
310
326
|
config: {
|
|
311
327
|
unknownGroup: (id: string) => `No such config group: ${id}`,
|
|
@@ -466,6 +482,19 @@ function deriveStorage<C extends CoreConfig>(core: Core<C>, dataDir: string, lan
|
|
|
466
482
|
// 保留延迟渲染项,其回调还负责复位 World 的排队状态。
|
|
467
483
|
clear: () => s.pending.cleared(core.discardPendingEvents()),
|
|
468
484
|
},
|
|
485
|
+
{
|
|
486
|
+
key: 'media',
|
|
487
|
+
label: s.media.label,
|
|
488
|
+
kind: 'disk',
|
|
489
|
+
location: 'data/media/',
|
|
490
|
+
danger: true,
|
|
491
|
+
note: s.media.note,
|
|
492
|
+
stat: () => {
|
|
493
|
+
const { count, bytes } = core.logBlobs.stat();
|
|
494
|
+
return s.media.stat(count, `${(bytes / 1024).toFixed(1)}KB`);
|
|
495
|
+
},
|
|
496
|
+
clear: () => s.media.cleared(core.logBlobs.clear()),
|
|
497
|
+
},
|
|
469
498
|
];
|
|
470
499
|
}
|
|
471
500
|
|
|
@@ -990,6 +1019,7 @@ export function createBot<C extends CoreConfig>(
|
|
|
990
1019
|
|
|
991
1020
|
// 共享端点配置位于部署根的 providers/;activeProvider 属于当前部署。
|
|
992
1021
|
const providerSettings = new ProviderSettings(cfg,core.providers,join(loaded.rootDir,'config.json'),loaded.providersDir ?? join(loaded.rootDir,'providers'));
|
|
1022
|
+
const providerHub = new ProviderHub(cfg, core.providers, providerSettings, join(loaded.rootDir, 'config.json'), loaded.providersDir ?? join(loaded.rootDir, 'providers'));
|
|
993
1023
|
const allConfigGroups = (language: Language) => [...configGroups(language),...providerSettings.groups(language)];
|
|
994
1024
|
const llmManagers = new Map<string,{stop():Promise<unknown>}>([['providers',{stop:()=>core.providers.stopAll()}]]);
|
|
995
1025
|
|
|
@@ -1058,6 +1088,9 @@ export function createBot<C extends CoreConfig>(
|
|
|
1058
1088
|
dataDir: loaded.dataDir,
|
|
1059
1089
|
botDir: loaded.rootDir,
|
|
1060
1090
|
language,
|
|
1091
|
+
...(cfg.web.host ? { host: cfg.web.host } : {}),
|
|
1092
|
+
allowedHosts: cfg.web.allowedHosts ?? [],
|
|
1093
|
+
password: loaded.secret(WEB_PASSWORD_SECRET) || (cfg.web.password ?? ''),
|
|
1061
1094
|
defaultScheme: cfg.web.theme,
|
|
1062
1095
|
sessions: core.sessions,
|
|
1063
1096
|
storage: consoleStorage,
|
|
@@ -1099,7 +1132,7 @@ export function createBot<C extends CoreConfig>(
|
|
|
1099
1132
|
? (language) => contribution.consolePages!({ storage: consoleStorage(language), language })
|
|
1100
1133
|
: undefined,
|
|
1101
1134
|
)(),...providerSettings.sources()],
|
|
1102
|
-
|
|
1135
|
+
providers: providerHub,
|
|
1103
1136
|
worldVisibility: {
|
|
1104
1137
|
state: () => core.worldVisibility(),
|
|
1105
1138
|
set: (id, visible, language) => {
|
|
@@ -1127,6 +1160,7 @@ export function createBot<C extends CoreConfig>(
|
|
|
1127
1160
|
return beginShutdown('控制台重启键', { closeWeb: false, exit: true, language });
|
|
1128
1161
|
},
|
|
1129
1162
|
supervised: isSupervised(),
|
|
1163
|
+
startsPaused: startsPaused(),
|
|
1130
1164
|
},
|
|
1131
1165
|
onboarding: {
|
|
1132
1166
|
dismiss: () => { try { unlinkSync(join(loaded.rootDir, ONBOARDING_FLAG_FILE)); } catch { /* 已经删过 */ } },
|
|
@@ -1166,6 +1200,7 @@ export function createBot<C extends CoreConfig>(
|
|
|
1166
1200
|
},
|
|
1167
1201
|
getStatus: () => ({
|
|
1168
1202
|
displayName: cfg.displayName,
|
|
1203
|
+
modelConnection: providerHub.current(language),
|
|
1169
1204
|
startedAt,
|
|
1170
1205
|
loop: core.loop.getStatus(),
|
|
1171
1206
|
eventCount: core.store.latestCursor(),
|
|
@@ -1199,7 +1234,7 @@ export function createBot<C extends CoreConfig>(
|
|
|
1199
1234
|
|
|
1200
1235
|
const orphans = Object.entries(cfg.providers).filter(([, entry]) => !providerModules.some((m) => m.id === entry.kind)).map(([name, entry]) => `${name}(kind=${entry.kind})`);
|
|
1201
1236
|
if (orphans.length) core.runlog.logger('provider').warn('端点条目没有对应的 Provider 模块,不可用', { orphans });
|
|
1202
|
-
void core.providers.start(cfg.activeProvider).catch(error=>core.runlog.logger('provider').error('Provider 启动失败',{error:String(error)}));
|
|
1237
|
+
if (cfg.activeProvider && cfg.providers[cfg.activeProvider]) void core.providers.start(cfg.activeProvider).catch(error=>core.runlog.logger('provider').error('Provider 启动失败',{error:String(error)}));
|
|
1203
1238
|
await parts.onStart?.({ core, loaded, port });
|
|
1204
1239
|
await core.start();
|
|
1205
1240
|
return { port };
|
package/src/core/README.md
CHANGED
|
@@ -34,6 +34,9 @@ Core 管理 session、事件流与模型调用的生命周期,包括事件投
|
|
|
34
34
|
`sessionDecls`;`spawnFork`、`resolveBlob` / `internBlobs`、`setWorldVisible`、`activeSpec` /
|
|
35
35
|
`activeProviderEntry`、`mountWorld` / `unmountWorld`、`start` / `stop`。
|
|
36
36
|
|
|
37
|
+
`WorldHost.modelFacts` 每次调用按当前端点读取。`activeProvider` 为空或端点没有模型时 `accepts`
|
|
38
|
+
返回 false,`activeProvider` 指向不存在的端点时抛错。
|
|
39
|
+
|
|
37
40
|
Persona 通过 `CoreApi` 访问:`injectInternal` / `injectDeferred` / `injectExternal`、
|
|
38
41
|
`requestContextHandoff`、`spawnFork`、`sessionInfo`、`llm`、`timers`、`deliveryGate`、
|
|
39
42
|
`personaState` / `savePersonaState`、`toolsTagged`、`blob`、`log`。
|
|
@@ -51,14 +54,19 @@ piggyback 只入队,随后续唤醒一起投递。
|
|
|
51
54
|
更新后的值在下一次入队时参与计算。
|
|
52
55
|
|
|
53
56
|
投递水位 `lastDeliveredCursor` 持久化,队列不持久化。重启时补投水位之后的外部事件;
|
|
54
|
-
|
|
57
|
+
已被候选处理结果引用的原始归档不重复投递。内部事件仅在当次运行投递。清空分片或跳过损坏行
|
|
58
|
+
留下的游标空位没有可投递内容,不阻止水位推进。
|
|
55
59
|
|
|
56
60
|
## 主循环
|
|
57
61
|
|
|
58
62
|
每次唤醒处理一批事件,可进行多轮模型调用。`SessionDecl.rounds()` 提供
|
|
59
63
|
`{ soft, hard, softHint? }`:到 soft 轮时将 `softHint` 追加到最后一条工具回执;到 hard 轮时
|
|
60
64
|
记录 warn 并结束本批。`endsTurn` 工具和自然结束共用结束处理;本批结束时尚未处理的事件
|
|
61
|
-
|
|
65
|
+
退回总线,进入下一批。控制台的前缀重载与清空 session 在批次边界执行:正在处理批次时等该批
|
|
66
|
+
结束,空闲时立即;并发请求复用同一事务。
|
|
67
|
+
|
|
68
|
+
一批正文归档后,Core 调用 `Persona.onDelivery` 并等待它返回的 Promise,不设期限。完成前
|
|
69
|
+
`injectInternal` 的内容排在这批的内部行末尾、外部正文之前;完成后的注入进入总线。
|
|
62
70
|
|
|
63
71
|
状态 0、429 或 5xx 的模型调用失败,可保留已记录的输出和工具回执,按 `ResubmitPolicy` 重试。
|
|
64
72
|
默认允许连续重试 2 次、每批最多 4 次,退避为 2 秒、10 秒;上下文超限、抢占、关机或轮数
|
|
@@ -86,3 +94,5 @@ handler 异常转为失败回执。流式生成时 `EagerDispatch` 可提前执
|
|
|
86
94
|
warn;`onHandoff` 异常记录 error 并使用默认交接策略。其他钩子的异常由调用方处理。
|
|
87
95
|
读取 `console()` 失败时省略该 World 的环境段,继续构建前缀。主循环异常退出时记录 error
|
|
88
96
|
并停止定时器。启动器处理未捕获的进程异常并执行关机流程。
|
|
97
|
+
|
|
98
|
+
`instanceIsRunning(dataDir)` 只读检查实例锁对应进程是否存活,沿用锁接管时的陈旧记录判定,不获取或更改锁。
|
package/src/core/blobs.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* `mem:<后端标识>` 由 Persona 的 BlobStore 解析,后端决定标识格式。
|
|
5
5
|
*/
|
|
6
6
|
import { createHash } from 'node:crypto';
|
|
7
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
8
8
|
import { join } from 'node:path';
|
|
9
9
|
import type { BlobRef } from './types.ts';
|
|
10
10
|
|
|
@@ -58,7 +58,7 @@ export function withBlobLines(text: string, refs: readonly BlobRef[] | undefined
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
/**
|
|
61
|
-
* 字节按内容哈希存入 data/media
|
|
61
|
+
* 字节按内容哈希存入 data/media/,重复写入不增加副本。
|
|
62
62
|
* 读取接受完整句柄或唯一的十六进制前缀(至少 8 位)。
|
|
63
63
|
*/
|
|
64
64
|
export class LogBlobStore {
|
|
@@ -81,6 +81,27 @@ export class LogBlobStore {
|
|
|
81
81
|
return `${LOG_SCHEME}${name}`;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
/** 现存份数与占用字节;目录还没建时都是 0。 */
|
|
85
|
+
stat(): { count: number; bytes: number } {
|
|
86
|
+
if (!existsSync(this.dir)) return { count: 0, bytes: 0 };
|
|
87
|
+
const names = readdirSync(this.dir);
|
|
88
|
+
return { count: names.length, bytes: names.reduce((sum, name) => sum + statSync(join(this.dir, name)).size, 0) };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 删掉目录里的全部附件,返回删掉的份数。附件那一行正文在落库时已写定
|
|
93
|
+
* (句柄、mime 与 fallbackText),此后 read() 返回 null,投递时不再附字节。
|
|
94
|
+
*/
|
|
95
|
+
clear(): number {
|
|
96
|
+
if (!existsSync(this.dir)) return 0;
|
|
97
|
+
let removed = 0;
|
|
98
|
+
for (const name of readdirSync(this.dir)) {
|
|
99
|
+
rmSync(join(this.dir, name), { force: true });
|
|
100
|
+
removed += 1;
|
|
101
|
+
}
|
|
102
|
+
return removed;
|
|
103
|
+
}
|
|
104
|
+
|
|
84
105
|
/** 按句柄取回;句柄不合形状、前缀不唯一或文件不在了返回 null。 */
|
|
85
106
|
read(handle: string): { bytes: Buffer; mime: string } | null {
|
|
86
107
|
const name = this.resolveName(handle);
|
|
@@ -12,7 +12,7 @@ import type { CoreConfig } from './types.ts';
|
|
|
12
12
|
import { pick, type Language } from './language.ts';
|
|
13
13
|
|
|
14
14
|
export interface ConfigProperty {
|
|
15
|
-
type: 'integer' | 'number' | 'boolean' | 'string' | 'array';
|
|
15
|
+
type: 'integer' | 'number' | 'boolean' | 'string' | 'array' | 'object';
|
|
16
16
|
title: string;
|
|
17
17
|
description?: string;
|
|
18
18
|
minimum?: number;
|
package/src/core/config.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/** Core 默认配置与深合并工具。Persona 和 World 参数由各自声明,部署合并顺序见 src/deploy.ts。 */
|
|
2
2
|
import type { CoreConfig } from './types.ts';
|
|
3
|
-
import type { PriceDefinition } from '../providers/pricebook.ts';
|
|
4
3
|
import type { ConfigGroup } from './config-schema.ts';
|
|
5
4
|
import { pick, type Language } from './language.ts';
|
|
6
5
|
|
|
@@ -186,21 +185,8 @@ export const CORE_DEFAULTS = {
|
|
|
186
185
|
/** 用于控制台标题和终端消息的发送方名称。 */
|
|
187
186
|
displayName: 'Cortico Bot',
|
|
188
187
|
timezone: 'Asia/Shanghai',
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
deepseek: {
|
|
192
|
-
kind: 'openai-responses-compat' as const,
|
|
193
|
-
baseUrl: 'https://api.deepseek.com',
|
|
194
|
-
secret: 'DEEPSEEK_API_KEY',
|
|
195
|
-
spec: { model: 'deepseek-flash', thinking: false },
|
|
196
|
-
// 默认价目为零;操作者可在控制台填写实际价格。
|
|
197
|
-
pricing: [{
|
|
198
|
-
models: ['*'], currency: 'USD', basis: 'marginal', source: 'console',
|
|
199
|
-
rules: [{ meter: 'cachedInput', perMillion: 0 }, { meter: 'uncachedInput', perMillion: 0 }, { meter: 'output', perMillion: 0 }],
|
|
200
|
-
}] as PriceDefinition[],
|
|
201
|
-
},
|
|
202
|
-
},
|
|
203
|
-
activeProvider: 'deepseek',
|
|
188
|
+
providers: {} as Record<string, import('./types.ts').LLMProviderEntry>,
|
|
189
|
+
activeProvider: '',
|
|
204
190
|
web: { port: 7777, theme: 'mint' },
|
|
205
191
|
paths: { memory: 'memory', data: 'data' },
|
|
206
192
|
batching: { quietGapMs: 2500, minBatchAgeMs: 0, maxBatchAgeMs: 15000, maxBatchSize: 100 },
|
package/src/core/core.ts
CHANGED
|
@@ -32,7 +32,7 @@ import { SessionLog } from './session.ts';
|
|
|
32
32
|
import { CoreState } from './state.ts';
|
|
33
33
|
import { ProviderRegistry } from '../providers/registry.ts';
|
|
34
34
|
import { runForkLoop } from './fork.ts';
|
|
35
|
-
import { providerModule } from '../providers/registry.ts';
|
|
35
|
+
import { providerModule, providerModules } from '../providers/registry.ts';
|
|
36
36
|
import { LogBlobStore, blobScheme, mimeOfHandle, withBlobLines } from './blobs.ts';
|
|
37
37
|
import { TimerStore } from './timers.ts';
|
|
38
38
|
import { SessionTracker, type SessionHandle } from './sessions.ts';
|
|
@@ -289,15 +289,19 @@ export class Core<C extends CoreConfig = CoreConfig> {
|
|
|
289
289
|
|
|
290
290
|
/** 按当前活跃端点读取模型上下文事实。 */
|
|
291
291
|
private contextFacts(): ContextFacts {
|
|
292
|
-
const module = () =>
|
|
292
|
+
const module = () => {
|
|
293
|
+
const entry = this.config.providers[this.config.activeProvider];
|
|
294
|
+
return entry ? providerModules.find(module => module.id === entry.kind) : undefined;
|
|
295
|
+
};
|
|
293
296
|
return {
|
|
294
297
|
hardTokens: () => {
|
|
298
|
+
if (!module() || !this.config.providers[this.config.activeProvider]?.spec) return null;
|
|
295
299
|
const spec = this.activeSpec();
|
|
296
300
|
const window = this.contextWindowOf(spec);
|
|
297
301
|
return window === undefined ? null : Math.max(0, window - (spec.maxTokens ?? 0));
|
|
298
302
|
},
|
|
299
|
-
estimateTokens: (records) => module()
|
|
300
|
-
contextOverflow: (error) => module()
|
|
303
|
+
estimateTokens: (records) => module()?.estimateTokens?.(records, this.activeSpec()) ?? estimateMessagesTokens(records),
|
|
304
|
+
contextOverflow: (error) => module()?.contextOverflow?.(error) ?? false,
|
|
301
305
|
};
|
|
302
306
|
}
|
|
303
307
|
|
|
@@ -464,8 +468,10 @@ export class Core<C extends CoreConfig = CoreConfig> {
|
|
|
464
468
|
return {
|
|
465
469
|
model: () => spec().model,
|
|
466
470
|
accepts: (mime) => {
|
|
471
|
+
if (!this.loaded.config.activeProvider) return false;
|
|
467
472
|
const {entry} = this.activeProviderEntry();
|
|
468
|
-
|
|
473
|
+
if (!entry.spec) return false;
|
|
474
|
+
return providerModule(entry.kind).accepts?.(entry,entry.spec,mime) ?? (entry.multimodal === true && mime.startsWith('image/'));
|
|
469
475
|
},
|
|
470
476
|
contextWindow: () => this.contextWindowOf(spec()),
|
|
471
477
|
};
|
|
@@ -587,7 +593,13 @@ export class Core<C extends CoreConfig = CoreConfig> {
|
|
|
587
593
|
const dataDir = this.loaded.dataDir;
|
|
588
594
|
if (!existsSync(dataDir)) mkdirSync(dataDir, { recursive: true });
|
|
589
595
|
// 构造期已加载状态;此处再次 load 会覆盖装配后、启动前的修改。
|
|
590
|
-
this.session.load();
|
|
596
|
+
const repair = this.session.load();
|
|
597
|
+
if (repair) {
|
|
598
|
+
this.log.warn(
|
|
599
|
+
repair.kind === 'torn-tail' ? 'session 末行未写完,已截掉' : 'session 末行缺换行,已补上',
|
|
600
|
+
{ ...repair, file: 'session-main.jsonl' },
|
|
601
|
+
);
|
|
602
|
+
}
|
|
591
603
|
for (const mod of this.worlds) {
|
|
592
604
|
try {
|
|
593
605
|
await mod.start(this.makeHost(mod));
|
package/src/core/generation.ts
CHANGED
|
@@ -16,13 +16,26 @@ export interface TokenMeters {
|
|
|
16
16
|
}
|
|
17
17
|
export type Meter = Exclude<keyof TokenMeters, 'native' | 'details'> | `detail:${string}`;
|
|
18
18
|
export interface PriceRule { meter: Meter; perMillion: number; unit?: string; }
|
|
19
|
-
export interface
|
|
19
|
+
export interface PriceSchedule { rules: PriceRule[]; inputBands?: Array<{ from: number; rules: PriceRule[] }>; }
|
|
20
|
+
export interface PriceTable extends PriceSchedule { serviceTiers?: Record<string, PriceSchedule>; }
|
|
21
|
+
/**
|
|
22
|
+
* `from` and `to` are `HH:MM` on the clock of `timezone`, half-open; `to` at or before `from` spans midnight.
|
|
23
|
+
* A span belongs to the local date it starts on. `weekdays` (ISO, 1 = Monday to 7 = Sunday) limits the dates
|
|
24
|
+
* that open it; `exceptDates` (`YYYY-MM-DD`) removes single dates.
|
|
25
|
+
*/
|
|
26
|
+
export interface PriceWindow extends PriceTable {
|
|
27
|
+
from: string;
|
|
28
|
+
to: string;
|
|
29
|
+
timezone: string;
|
|
30
|
+
weekdays?: number[];
|
|
31
|
+
exceptDates?: string[];
|
|
32
|
+
}
|
|
33
|
+
export interface PriceSnapshot extends PriceTable {
|
|
20
34
|
id: string;
|
|
21
35
|
currency: string;
|
|
22
36
|
basis: 'marginal' | 'equivalent';
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
serviceTiers?: Record<string, { rules: PriceRule[]; inputBands?: Array<{ from: number; rules: PriceRule[] }> }>;
|
|
37
|
+
/** The first window holding `capturedAt` replaces the table above it; outside every window that table applies. */
|
|
38
|
+
timeWindows?: PriceWindow[];
|
|
26
39
|
source: string;
|
|
27
40
|
capturedAt: string;
|
|
28
41
|
}
|
|
@@ -30,7 +43,7 @@ export interface Charge {
|
|
|
30
43
|
quote: PriceSnapshot;
|
|
31
44
|
amount: number | null;
|
|
32
45
|
knownAmount: number;
|
|
33
|
-
missing: Array<Meter | 'serviceTier'>;
|
|
46
|
+
missing: Array<Meter | 'serviceTier' | 'timeWindow'>;
|
|
34
47
|
lines: Array<{ meter: Meter; unit: string; quantity: number | null; perMillion: number; amount: number | null }>;
|
|
35
48
|
}
|
|
36
49
|
export interface ProviderAttempt {
|
|
@@ -89,13 +102,57 @@ export function standardUsage(meters: TokenMeters): Usage | null {
|
|
|
89
102
|
return { input_tokens: input, output_tokens: output, total_tokens: total,
|
|
90
103
|
input_tokens_details: { cached_tokens: cachedInput }, output_tokens_details: { reasoning_tokens: reasoning } };
|
|
91
104
|
}
|
|
105
|
+
/** Minutes since midnight for an `HH:MM` bound, 24:00 included; null when the text is not one. */
|
|
106
|
+
function clockMinutes(value: string): number | null {
|
|
107
|
+
const match = /^(\d{1,2}):(\d{2})$/.exec(value);
|
|
108
|
+
if (!match) return null;
|
|
109
|
+
const minutes = Number(match[1]) * 60 + Number(match[2]);
|
|
110
|
+
return Number(match[2]) > 59 || minutes > 24 * 60 ? null : minutes;
|
|
111
|
+
}
|
|
112
|
+
/** Whether the text is a real calendar date written `YYYY-MM-DD`. */
|
|
113
|
+
function isCalendarDate(value: string): boolean {
|
|
114
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
|
|
115
|
+
if (!match) return false;
|
|
116
|
+
const [year, month, day] = [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
117
|
+
const date = new Date(Date.UTC(year, month - 1, day));
|
|
118
|
+
return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day;
|
|
119
|
+
}
|
|
120
|
+
/** Whether the instant falls in the window on the window's own clock and date; null when a field is unreadable. */
|
|
121
|
+
function holdsTime(window: PriceWindow, at: string): boolean | null {
|
|
122
|
+
const instant = new Date(at);
|
|
123
|
+
const [from, to] = [clockMinutes(window.from), clockMinutes(window.to)];
|
|
124
|
+
if (from === null || to === null || Number.isNaN(instant.getTime())) return null;
|
|
125
|
+
if (window.weekdays?.some(day => !Number.isInteger(day) || day < 1 || day > 7)) return null;
|
|
126
|
+
if (window.exceptDates?.some(date => !isCalendarDate(date))) return null;
|
|
127
|
+
let year: number, month: number, day: number, local: number;
|
|
128
|
+
try {
|
|
129
|
+
// Intl throws on a zone name it does not know.
|
|
130
|
+
const parts = new Intl.DateTimeFormat('en-GB', { timeZone: window.timezone, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hourCycle: 'h23' }).formatToParts(instant);
|
|
131
|
+
const read = (type: string): number => Number(parts.find(part => part.type === type)!.value);
|
|
132
|
+
[year, month, day] = [read('year'), read('month'), read('day')];
|
|
133
|
+
local = read('hour') * 60 + read('minute');
|
|
134
|
+
} catch { return null; }
|
|
135
|
+
const spansMidnight = to <= from;
|
|
136
|
+
if (spansMidnight ? local < from && local >= to : local < from || local >= to) return false;
|
|
137
|
+
// After midnight the span still belongs to the previous date, the one it started on.
|
|
138
|
+
const start = new Date(Date.UTC(year, month - 1, spansMidnight && local < to ? day - 1 : day));
|
|
139
|
+
if (window.weekdays && !window.weekdays.includes(start.getUTCDay() || 7)) return false;
|
|
140
|
+
return !window.exceptDates?.includes(start.toISOString().slice(0, 10));
|
|
141
|
+
}
|
|
92
142
|
export function priceUsage(meters: TokenMeters, quotes: readonly PriceSnapshot[], serviceTier: string | null = null): Charge[] {
|
|
93
143
|
return quotes.map(quote => {
|
|
94
144
|
const missing: Charge['missing'] = [];
|
|
95
|
-
let
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
145
|
+
let table: PriceTable = quote;
|
|
146
|
+
for (const window of quote.timeWindows ?? []) {
|
|
147
|
+
const holds = holdsTime(window, quote.capturedAt);
|
|
148
|
+
// An unreadable window may be the one holding the request; a later table cannot stand in for it.
|
|
149
|
+
if (holds === null) { missing.push('timeWindow'); break; }
|
|
150
|
+
if (holds) { table = window; break; }
|
|
151
|
+
}
|
|
152
|
+
let schedule: PriceSchedule = table;
|
|
153
|
+
if (table.serviceTiers) {
|
|
154
|
+
if (serviceTier === null || !table.serviceTiers[serviceTier]) missing.push('serviceTier');
|
|
155
|
+
else schedule = table.serviceTiers[serviceTier];
|
|
99
156
|
}
|
|
100
157
|
let rules = schedule.rules;
|
|
101
158
|
if (schedule.inputBands?.length) {
|
|
@@ -72,6 +72,12 @@ function readPayload(file: string): LockPayload | null {
|
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
/** 只读检查数据目录的锁是否仍对应存活进程。 */
|
|
76
|
+
export function instanceIsRunning(dataDir: string): boolean {
|
|
77
|
+
const payload = readPayload(join(dataDir, INSTANCE_LOCK_FILE));
|
|
78
|
+
return payload !== null && ownerStillRunning(payload);
|
|
79
|
+
}
|
|
80
|
+
|
|
75
81
|
/** 排他创建;仅 EEXIST 返回 false,其他错误抛出。 */
|
|
76
82
|
function createExclusive(file: string, payload: LockPayload): boolean {
|
|
77
83
|
let fd: number;
|