cortico 0.1.0

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.
Files changed (311) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +47 -0
  3. package/package.json +43 -0
  4. package/src/boot.ts +46 -0
  5. package/src/bot.ts +1342 -0
  6. package/src/config-file.ts +41 -0
  7. package/src/core/README.md +92 -0
  8. package/src/core/billing.ts +26 -0
  9. package/src/core/blobs.ts +106 -0
  10. package/src/core/bus.ts +311 -0
  11. package/src/core/config-schema.ts +201 -0
  12. package/src/core/config.ts +262 -0
  13. package/src/core/core.ts +699 -0
  14. package/src/core/cost.ts +244 -0
  15. package/src/core/event-store.ts +304 -0
  16. package/src/core/fork.ts +113 -0
  17. package/src/core/generation.ts +118 -0
  18. package/src/core/instance-lock.ts +215 -0
  19. package/src/core/ipc-logger.ts +105 -0
  20. package/src/core/language.ts +62 -0
  21. package/src/core/log-context.ts +38 -0
  22. package/src/core/loop.ts +1858 -0
  23. package/src/core/markers.ts +48 -0
  24. package/src/core/prefix.ts +121 -0
  25. package/src/core/run.ts +95 -0
  26. package/src/core/secrets.ts +18 -0
  27. package/src/core/session.ts +56 -0
  28. package/src/core/sessions.ts +261 -0
  29. package/src/core/state.ts +104 -0
  30. package/src/core/template.ts +72 -0
  31. package/src/core/timers.ts +129 -0
  32. package/src/core/tool-log.ts +135 -0
  33. package/src/core/transcript.ts +92 -0
  34. package/src/core/truncate.ts +121 -0
  35. package/src/core/types.ts +1140 -0
  36. package/src/core/usage-log.ts +79 -0
  37. package/src/core/util.ts +386 -0
  38. package/src/deploy-listing.ts +160 -0
  39. package/src/deploy.ts +162 -0
  40. package/src/extensions/README.md +72 -0
  41. package/src/extensions/dry-mount.ts +390 -0
  42. package/src/extensions/manifest.ts +153 -0
  43. package/src/extensions/runtime.ts +42 -0
  44. package/src/extensions.ts +519 -0
  45. package/src/launcher.ts +285 -0
  46. package/src/paths.ts +155 -0
  47. package/src/protocol/open-responses/LICENSE +201 -0
  48. package/src/protocol/open-responses/README.md +14 -0
  49. package/src/protocol/open-responses/context-helpers.ts +27 -0
  50. package/src/protocol/open-responses/context-log.ts +59 -0
  51. package/src/protocol/open-responses/context.ts +89 -0
  52. package/src/protocol/open-responses/generated.ts +115 -0
  53. package/src/protocol/open-responses/index.ts +33 -0
  54. package/src/protocol/open-responses/openapi.json +4230 -0
  55. package/src/protocol/open-responses/stream.ts +146 -0
  56. package/src/protocol/open-responses/tokens.ts +31 -0
  57. package/src/providers/README.md +86 -0
  58. package/src/providers/base.ts +111 -0
  59. package/src/providers/configuration.ts +97 -0
  60. package/src/providers/console/config.ts +21 -0
  61. package/src/providers/console/settings.ts +443 -0
  62. package/src/providers/console/strings.ts +74 -0
  63. package/src/providers/console/types.ts +11 -0
  64. package/src/providers/llamacpp/archive.ts +148 -0
  65. package/src/providers/llamacpp/catalog.ts +247 -0
  66. package/src/providers/llamacpp/config.ts +36 -0
  67. package/src/providers/llamacpp/console/client.ts +6 -0
  68. package/src/providers/llamacpp/console/models-panel.ts +120 -0
  69. package/src/providers/llamacpp/console/runtime-panel.ts +173 -0
  70. package/src/providers/llamacpp/console/server.ts +153 -0
  71. package/src/providers/llamacpp/index.ts +95 -0
  72. package/src/providers/llamacpp/native.ts +51 -0
  73. package/src/providers/llamacpp/options.ts +117 -0
  74. package/src/providers/llamacpp/runtime-store.ts +139 -0
  75. package/src/providers/llamacpp/runtime.ts +214 -0
  76. package/src/providers/llamacpp/server.ts +286 -0
  77. package/src/providers/llamacpp/strings.ts +230 -0
  78. package/src/providers/openai-responses-compat/config.ts +20 -0
  79. package/src/providers/openai-responses-compat/index.ts +89 -0
  80. package/src/providers/openai-responses-compat/native.ts +127 -0
  81. package/src/providers/openai-responses-compat/strings.ts +19 -0
  82. package/src/providers/pricebook.ts +75 -0
  83. package/src/providers/registry.ts +137 -0
  84. package/src/providers/strings.ts +71 -0
  85. package/src/providers/transport/chat.ts +156 -0
  86. package/src/providers/transport/errors.ts +48 -0
  87. package/src/providers/transport/history.ts +68 -0
  88. package/src/providers/transport/native-input.ts +83 -0
  89. package/src/providers/transport/native-types.ts +13 -0
  90. package/src/providers/transport/response-assembly.ts +191 -0
  91. package/src/providers/transport/response-http.ts +196 -0
  92. package/src/providers/transport/response-meters.ts +27 -0
  93. package/src/providers/transport/responses-input.ts +56 -0
  94. package/src/web/README.md +84 -0
  95. package/src/web/client/console-pages/builtins/llm-settings/panel.ts +625 -0
  96. package/src/web/client/console-pages/builtins/llm-settings/pricing-panel.ts +168 -0
  97. package/src/web/client/console-pages/builtins/llm-settings/strings.ts +213 -0
  98. package/src/web/client/console-pages/builtins.ts +10 -0
  99. package/src/web/client/console-pages/context.ts +135 -0
  100. package/src/web/client/console-pages/host.ts +633 -0
  101. package/src/web/client/console-pages/loader.ts +132 -0
  102. package/src/web/client/console-pages/strings.ts +90 -0
  103. package/src/web/client/console-pages/tools/strings.ts +49 -0
  104. package/src/web/client/console-pages/tools/view.ts +215 -0
  105. package/src/web/client/core/api.ts +200 -0
  106. package/src/web/client/core/language.ts +56 -0
  107. package/src/web/client/core/lifecycle.ts +132 -0
  108. package/src/web/client/core/router.ts +199 -0
  109. package/src/web/client/core/stream.ts +157 -0
  110. package/src/web/client/core/websocket.ts +82 -0
  111. package/src/web/client/features/appearance/index.ts +333 -0
  112. package/src/web/client/features/appearance/strings.ts +87 -0
  113. package/src/web/client/features/config/index.ts +11 -0
  114. package/src/web/client/features/config/strings.ts +41 -0
  115. package/src/web/client/features/config/view.ts +428 -0
  116. package/src/web/client/features/core/events.ts +197 -0
  117. package/src/web/client/features/core/index.ts +254 -0
  118. package/src/web/client/features/core/run.ts +56 -0
  119. package/src/web/client/features/core/runlog.ts +214 -0
  120. package/src/web/client/features/core/sessions.ts +53 -0
  121. package/src/web/client/features/core/strings.ts +157 -0
  122. package/src/web/client/features/extensions/index.ts +422 -0
  123. package/src/web/client/features/extensions/strings.ts +164 -0
  124. package/src/web/client/features/feature.ts +93 -0
  125. package/src/web/client/features/live/context.ts +280 -0
  126. package/src/web/client/features/live/fork.ts +156 -0
  127. package/src/web/client/features/live/index.ts +392 -0
  128. package/src/web/client/features/live/onboarding.ts +113 -0
  129. package/src/web/client/features/live/protocol.ts +122 -0
  130. package/src/web/client/features/live/sessions.ts +71 -0
  131. package/src/web/client/features/live/status.ts +68 -0
  132. package/src/web/client/features/live/strings.ts +252 -0
  133. package/src/web/client/features/live/timeline.ts +590 -0
  134. package/src/web/client/features/prompts/editor.ts +292 -0
  135. package/src/web/client/features/prompts/index.ts +346 -0
  136. package/src/web/client/features/prompts/strings.ts +136 -0
  137. package/src/web/client/features/prompts/view.ts +256 -0
  138. package/src/web/client/features/providers/index.ts +100 -0
  139. package/src/web/client/features/providers/strings.ts +23 -0
  140. package/src/web/client/features/settings/general.ts +28 -0
  141. package/src/web/client/features/settings/index.ts +82 -0
  142. package/src/web/client/features/settings/strings.ts +33 -0
  143. package/src/web/client/features/storage/index.ts +1 -0
  144. package/src/web/client/features/storage/strings.ts +45 -0
  145. package/src/web/client/features/storage/view.ts +154 -0
  146. package/src/web/client/features/usage/chart.ts +486 -0
  147. package/src/web/client/features/usage/index.ts +407 -0
  148. package/src/web/client/features/usage/labels.ts +46 -0
  149. package/src/web/client/features/usage/range.ts +36 -0
  150. package/src/web/client/features/usage/state.ts +37 -0
  151. package/src/web/client/features/usage/strings.ts +191 -0
  152. package/src/web/client/features/usage/tooltip.ts +86 -0
  153. package/src/web/client/features/usage/types.ts +96 -0
  154. package/src/web/client/features/worlds/index.ts +424 -0
  155. package/src/web/client/features/worlds/strings.ts +121 -0
  156. package/src/web/client/main.ts +273 -0
  157. package/src/web/client/shell/avatar.ts +233 -0
  158. package/src/web/client/shell/index.ts +549 -0
  159. package/src/web/client/shell/strings.ts +89 -0
  160. package/src/web/client/theme/handoff.ts +43 -0
  161. package/src/web/client/theme/palette.ts +109 -0
  162. package/src/web/client/theme/registry.ts +130 -0
  163. package/src/web/client/theme/storage.ts +72 -0
  164. package/src/web/client/theme/strings.ts +123 -0
  165. package/src/web/client/theme/studio.ts +366 -0
  166. package/src/web/client/ui/actions.ts +109 -0
  167. package/src/web/client/ui/data.ts +185 -0
  168. package/src/web/client/ui/dom.ts +23 -0
  169. package/src/web/client/ui/fields.ts +191 -0
  170. package/src/web/client/ui/format.ts +75 -0
  171. package/src/web/client/ui/icons.ts +183 -0
  172. package/src/web/client/ui/images.ts +91 -0
  173. package/src/web/client/ui/index.ts +89 -0
  174. package/src/web/client/ui/lamp.ts +125 -0
  175. package/src/web/client/ui/log.ts +98 -0
  176. package/src/web/client/ui/overlay.ts +184 -0
  177. package/src/web/client/ui/page.ts +8 -0
  178. package/src/web/client/ui/prompt-input.tsx +311 -0
  179. package/src/web/client/ui/sheet.ts +131 -0
  180. package/src/web/client/ui/strings.ts +71 -0
  181. package/src/web/console-pages.ts +367 -0
  182. package/src/web/files.ts +107 -0
  183. package/src/web/path-picker.ts +296 -0
  184. package/src/web/public/index.html +17 -0
  185. package/src/web/public/styles.css +1715 -0
  186. package/src/web/server.ts +1883 -0
  187. package/src/web/shared/client-panel.ts +603 -0
  188. package/src/web/shared/console-protocol.ts +560 -0
  189. package/src/web/shared/css.d.ts +1 -0
  190. package/src/web/shared/path-picker.ts +23 -0
  191. package/src/web/shared/theme.ts +256 -0
  192. package/src/web/theme-store.ts +37 -0
  193. package/src/world.ts +449 -0
  194. package/src/worlds/bilibili/ENV_PROMPT.md +5 -0
  195. package/src/worlds/bilibili/README.md +176 -0
  196. package/src/worlds/bilibili/audience-admission.ts +995 -0
  197. package/src/worlds/bilibili/client.ts +528 -0
  198. package/src/worlds/bilibili/coalescing-buffer.ts +125 -0
  199. package/src/worlds/bilibili/config.ts +185 -0
  200. package/src/worlds/bilibili/console/client.ts +202 -0
  201. package/src/worlds/bilibili/definition.ts +35 -0
  202. package/src/worlds/bilibili/gift-frame.ts +131 -0
  203. package/src/worlds/bilibili/normalize.ts +452 -0
  204. package/src/worlds/bilibili/overlay/announcement.ts +76 -0
  205. package/src/worlds/bilibili/overlay/assets.ts +82 -0
  206. package/src/worlds/bilibili/overlay/model.ts +494 -0
  207. package/src/worlds/bilibili/overlay/project.ts +202 -0
  208. package/src/worlds/bilibili/overlay/server.ts +286 -0
  209. package/src/worlds/bilibili/overlay/types.ts +217 -0
  210. package/src/worlds/bilibili/overlay/web/app.js +690 -0
  211. package/src/worlds/bilibili/overlay/web/editor.css +297 -0
  212. package/src/worlds/bilibili/overlay/web/editor.html +102 -0
  213. package/src/worlds/bilibili/overlay/web/editor.js +2116 -0
  214. package/src/worlds/bilibili/overlay/web/overlay.html +13 -0
  215. package/src/worlds/bilibili/overlay/web/styles.css +258 -0
  216. package/src/worlds/bilibili/protobuf.ts +123 -0
  217. package/src/worlds/bilibili/wire.ts +94 -0
  218. package/src/worlds/bilibili/world.ts +1389 -0
  219. package/src/worlds/console-fixture/ENV_PROMPT.md +1 -0
  220. package/src/worlds/console-fixture/console/client.ts +72 -0
  221. package/src/worlds/console-fixture/world.ts +73 -0
  222. package/src/worlds/index.ts +15 -0
  223. package/src/worlds/minecraft/ENV_PROMPT.md +32 -0
  224. package/src/worlds/minecraft/ENV_PROMPT_CAMERA.md +2 -0
  225. package/src/worlds/minecraft/LICENSE-mineflayer-pathfinder.txt +21 -0
  226. package/src/worlds/minecraft/README.md +917 -0
  227. package/src/worlds/minecraft/blueprint-plan.ts +915 -0
  228. package/src/worlds/minecraft/blueprint-registry.ts +566 -0
  229. package/src/worlds/minecraft/blueprint-repair.ts +236 -0
  230. package/src/worlds/minecraft/blueprint-resource.ts +272 -0
  231. package/src/worlds/minecraft/blueprint.ts +492 -0
  232. package/src/worlds/minecraft/body-lease.ts +340 -0
  233. package/src/worlds/minecraft/bridge.ts +749 -0
  234. package/src/worlds/minecraft/check.ts +651 -0
  235. package/src/worlds/minecraft/chests.ts +292 -0
  236. package/src/worlds/minecraft/client-launch.ts +272 -0
  237. package/src/worlds/minecraft/client-options.ts +85 -0
  238. package/src/worlds/minecraft/client-skins.ts +164 -0
  239. package/src/worlds/minecraft/client-window.ps1 +91 -0
  240. package/src/worlds/minecraft/client.ts +423 -0
  241. package/src/worlds/minecraft/combat-context.ts +109 -0
  242. package/src/worlds/minecraft/combat.ts +1552 -0
  243. package/src/worlds/minecraft/config.ts +499 -0
  244. package/src/worlds/minecraft/console/access.ts +218 -0
  245. package/src/worlds/minecraft/console/client.ts +244 -0
  246. package/src/worlds/minecraft/console/log.ts +110 -0
  247. package/src/worlds/minecraft/console/mount.ts +420 -0
  248. package/src/worlds/minecraft/console/skin.ts +257 -0
  249. package/src/worlds/minecraft/console/style.css +154 -0
  250. package/src/worlds/minecraft/console/world.ts +449 -0
  251. package/src/worlds/minecraft/deaths.ts +135 -0
  252. package/src/worlds/minecraft/definition.ts +18 -0
  253. package/src/worlds/minecraft/engine-child.ts +274 -0
  254. package/src/worlds/minecraft/engine-ipc.ts +98 -0
  255. package/src/worlds/minecraft/entity-facts.ts +132 -0
  256. package/src/worlds/minecraft/escape.ts +407 -0
  257. package/src/worlds/minecraft/executor.ts +14494 -0
  258. package/src/worlds/minecraft/explored.ts +156 -0
  259. package/src/worlds/minecraft/geometry.ts +205 -0
  260. package/src/worlds/minecraft/goal-plan.ts +647 -0
  261. package/src/worlds/minecraft/item-break.ts +126 -0
  262. package/src/worlds/minecraft/item-facts.ts +146 -0
  263. package/src/worlds/minecraft/item-pick.ts +67 -0
  264. package/src/worlds/minecraft/level-dat.ts +176 -0
  265. package/src/worlds/minecraft/log.ts +132 -0
  266. package/src/worlds/minecraft/mineflayer-fixes.ts +1180 -0
  267. package/src/worlds/minecraft/names.ts +377 -0
  268. package/src/worlds/minecraft/pathfinder-lib.d.ts +13 -0
  269. package/src/worlds/minecraft/pathfinder-perf.ts +796 -0
  270. package/src/worlds/minecraft/piglin.ts +49 -0
  271. package/src/worlds/minecraft/policy.ts +547 -0
  272. package/src/worlds/minecraft/precheck.ts +655 -0
  273. package/src/worlds/minecraft/proxy.ts +445 -0
  274. package/src/worlds/minecraft/ranged.ts +635 -0
  275. package/src/worlds/minecraft/readouts.ts +102 -0
  276. package/src/worlds/minecraft/round.ts +26 -0
  277. package/src/worlds/minecraft/search-observation.ts +92 -0
  278. package/src/worlds/minecraft/server-config.ts +467 -0
  279. package/src/worlds/minecraft/server.ts +658 -0
  280. package/src/worlds/minecraft/show.ts +84 -0
  281. package/src/worlds/minecraft/skills.ts +1763 -0
  282. package/src/worlds/minecraft/terrain.ts +1735 -0
  283. package/src/worlds/minecraft/window.ts +96 -0
  284. package/src/worlds/minecraft/works.ts +179 -0
  285. package/src/worlds/minecraft/world.ts +6438 -0
  286. package/src/worlds/qq/ENV_PROMPT.md +5 -0
  287. package/src/worlds/qq/config.ts +108 -0
  288. package/src/worlds/qq/console/client.ts +126 -0
  289. package/src/worlds/qq/console/events.ts +155 -0
  290. package/src/worlds/qq/console/gate.ts +232 -0
  291. package/src/worlds/qq/console/roster.ts +203 -0
  292. package/src/worlds/qq/console/style.css +65 -0
  293. package/src/worlds/qq/conversation.ts +28 -0
  294. package/src/worlds/qq/definition.ts +62 -0
  295. package/src/worlds/qq/driver.ts +375 -0
  296. package/src/worlds/qq/history-tools.ts +209 -0
  297. package/src/worlds/qq/normalize.ts +323 -0
  298. package/src/worlds/qq/qface-map.ts +282 -0
  299. package/src/worlds/qq/vision-prompt.ts +27 -0
  300. package/src/worlds/qq/vision.ts +642 -0
  301. package/src/worlds/qq/vlm.ts +142 -0
  302. package/src/worlds/qq/world.ts +1503 -0
  303. package/src/worlds/terminal/ENV_PROMPT.md +11 -0
  304. package/src/worlds/terminal/config.ts +9 -0
  305. package/src/worlds/terminal/definition.ts +14 -0
  306. package/src/worlds/terminal/world.ts +700 -0
  307. package/src/worlds/websearch/ENV_PROMPT.md +1 -0
  308. package/src/worlds/websearch/brave-client.ts +261 -0
  309. package/src/worlds/websearch/config.ts +75 -0
  310. package/src/worlds/websearch/definition.ts +20 -0
  311. package/src/worlds/websearch/world.ts +171 -0
package/src/bot.ts ADDED
@@ -0,0 +1,1342 @@
1
+ /** 装配 Core、Persona、World 与控制台;具体 bot 的配置和行为由 BotDefinition 提供。 */
2
+ import { existsSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
3
+ import { createHash } from 'node:crypto';
4
+ import { dirname, join } from 'node:path';
5
+ import type {
6
+ ConfigGroup,
7
+ CoreConfig,
8
+ World,
9
+ Logger,
10
+ WorldLifecycleEvent,
11
+ WorldPanelDecl,
12
+ Persona,
13
+ PromptDocDecl,
14
+ ShutdownExternalCheck,
15
+ ToolSchema,
16
+ } from './core/types.ts';
17
+ import type { LoadedConfig } from './core/config.ts';
18
+ import { coreConfigGroup } from './core/config.ts';
19
+ import { pick, resolveLanguage, type Language } from './core/language.ts';
20
+ import { updateJsonObject } from './config-file.ts';
21
+ import { ONBOARDING_FLAG_FILE } from './deploy.ts';
22
+ import { isSupervised, requestRestart } from './boot.ts';
23
+ import { ExtensionManager, type ExtensionSet } from './extensions.ts';
24
+ import { WorldAssembly, type WorldDefinition, type WorldDeclaration, type WorldSection } from './world.ts';
25
+ import { Core, type WorldStopFailure } from './core/core.ts';
26
+ import { RESERVED_FRAME_NAMES } from './core/loop.ts';
27
+ import type { ResponseClient } from './core/generation.ts';
28
+ import { acquireInstanceLock, type InstanceLock } from './core/instance-lock.ts';
29
+ import { assembleSystemSegments, envPromptOverridePath, envPromptTemplateSource, renderWorldEnvPrompt, type EnvPromptDirs, type EnvPromptOrigin } from './core/prefix.ts';
30
+ import { aggregateUsage } from './core/cost.ts';
31
+ import { nowIso, withDeadline } from './core/util.ts';
32
+ import { closeRun } from './core/run.ts';
33
+ import { ProviderSettings } from './providers/console/settings.ts';
34
+ import { providerModules } from './providers/registry.ts';
35
+ import { readGroupValues, setByPath as setConfigPath } from './core/config-schema.ts';
36
+ import type { ConfigValues } from './core/config-schema.ts';
37
+ import {
38
+ PromptRevisionConflict,
39
+ WebApp,
40
+ type ConsoleWorldInfo,
41
+ type WorldInfo,
42
+ type PromptDocument,
43
+ type OwnedStoragePart,
44
+ type StoragePart,
45
+ type ToolOwner,
46
+ type WebAppPromptDeps,
47
+ } from './web/server.ts';
48
+ import type { ConsolePageSource } from './web/console-pages.ts';
49
+ import {
50
+ pageIdFor,
51
+ type ConsolePageContribution,
52
+ } from './web/shared/console-protocol.ts';
53
+
54
+ /** 启动器使用的 bot 包装配契约。 */
55
+ export interface BotDefinition<C extends CoreConfig = CoreConfig> {
56
+ /** 控制台页 id `persona:<id>` 与产物键取它;仓内包的 id 与目录同名。 */
57
+ id: string;
58
+ /** Memory 系统的名字,作 Memory 页的标题;缺省回落到 persona.memory 的类名,再缺省是 Memory。 */
59
+ memoryName?: string;
60
+ /** 框架默认值与 Persona 默认值;World 默认段由 withWorlds() 补充。 */
61
+ defaults(): C;
62
+ /** 由 withWorlds() 注入的内建及扩展 World 定义;按 worlds.<id>.enabled 挂载。 */
63
+ worlds?: readonly WorldDefinition<WorldSection>[];
64
+ /** 默认启用的 World id;缺失实现时显示不可用。对象声明可附缺失原因,未声明的 World 默认关闭。 */
65
+ declares?: readonly WorldDeclaration[];
66
+ /** 配置已完成合并;worlds 与 Core 共用数组,激活和停用会就地更新,Persona 应在使用时读取。 */
67
+ build(loaded: LoadedConfig<C>, worlds: World[]): BotParts<C>;
68
+ }
69
+
70
+ export interface BotStartContext<C extends CoreConfig> {
71
+ core: Core<C>;
72
+ loaded: LoadedConfig<C>;
73
+ /** 控制台实际监听的端口;不起控制台时为 null */
74
+ port: number | null;
75
+ }
76
+
77
+ export interface BotParts<C extends CoreConfig = CoreConfig> {
78
+ persona: Persona;
79
+ /** 预建实例替换同 id 的定义实例,初始挂载;重启复用该实例,停用后不能通过 activate 重新挂载。 */
80
+ worlds?: World[];
81
+ llm?: ResponseClient;
82
+ console?: ConsoleContribution;
83
+ onStart?(ctx: BotStartContext<C>): void | Promise<void>;
84
+ onStop?(): void | Promise<void>;
85
+ }
86
+
87
+ /** bot 提供的控制台声明,补充 Core 与 World 的通用页面。 */
88
+ export interface ConsoleContribution {
89
+ /** false = 完全不起控制台(无头运行) */
90
+ enabled?: boolean;
91
+ /** 追加的可调配置组(Persona那组;core 与各 World 的由框架收拢) */
92
+ configGroups?: ConfigGroup[];
93
+ /** 动态下拉选项;固定选项的文案使用请求语言。 */
94
+ configOptions?(kind: string, language: Language): Array<{ value: string; label: string }>;
95
+ /** 合并进状态快照的实现特有字段(框架给的基础字段在前,这里覆盖) */
96
+ status?(): Record<string, unknown>;
97
+ /** 以 persona 作用域展示的提示词文件,框架负责读写。 */
98
+ promptDocs?: PromptDocDecl[];
99
+ /**
100
+ * 非主循环 session 的额外工具 schema。
101
+ * 每个 session 的声明方负责在此补充其工具。
102
+ */
103
+ extraToolSchemas?(): ToolSchema[];
104
+ /** 需要部署配置或跨模块操作的控制台页;Persona.console() 声明 Persona 自身的操作。 */
105
+ consolePages?(ctx: ConsolePageBuildContext): ConsolePageContribution[];
106
+ }
107
+
108
+ /** 装配层向 bot 控制台声明函数提供的共享数据。 */
109
+ export interface ConsolePageBuildContext {
110
+ /** 与 /api/storage 共用同一批对象的存储清单,文案使用请求语言。 */
111
+ storage: readonly StoragePart[];
112
+ /** 发起这次请求的浏览器的界面语言。 */
113
+ language: Language;
114
+ }
115
+
116
+ /** 关机步骤;ok=false 时 detail 说明异常或超时。 */
117
+ export interface ShutdownStep {
118
+ key: string;
119
+ label: string;
120
+ ok: boolean;
121
+ elapsedMs: number;
122
+ detail?: string;
123
+ }
124
+
125
+ /** complete 要求本地关机步骤完成且外部状态核验通过。 */
126
+ export interface ShutdownReport {
127
+ reason: string;
128
+ localComplete: boolean;
129
+ complete: boolean;
130
+ steps: ShutdownStep[];
131
+ externalChecks: ShutdownExternalCheck[];
132
+ }
133
+
134
+ export interface Bot<C extends CoreConfig = CoreConfig> {
135
+ core: Core<C>;
136
+ parts: BotParts<C>;
137
+ /** World 槽位表:挂载表、未激活槽位与缺失声明。 */
138
+ assembly: WorldAssembly;
139
+ webApp: WebApp | null;
140
+ /** 启动全部;返回控制台端口(未起控制台=null) */
141
+ start(): Promise<{ port: number | null }>;
142
+ stop(): Promise<void>;
143
+ /** 分步关机并返回结果。步骤失败或超时后继续,不调用 process.exit。 */
144
+ shutdown(reason?: string): Promise<ShutdownReport>;
145
+ }
146
+
147
+ /** 分步上限合计 33 秒;每一步超时只推进编排,不会取消外部 promise。 */
148
+ const SHUTDOWN_BUDGET_MS = {
149
+ pause: 2_000,
150
+ worlds: 22_000,
151
+ core: 1_000,
152
+ llm: 3_000,
153
+ flush: 2_000,
154
+ web: 3_000,
155
+ } as const;
156
+
157
+ const BOT_TEXT = {
158
+ zh: {
159
+ noFile: '(无文件)',
160
+ storage: {
161
+ events: {
162
+ label: '事件库(本次运行的分片)',
163
+ note: '清除本次运行的事件记录,保留此前运行的记录;游标不回退',
164
+ stat: (count: number, cursor: number, size: string) => `${count}条(游标至 ${cursor}) / ${size}`,
165
+ cleared: (n: number) => `已清除本次运行的${n}条事件`,
166
+ },
167
+ session: {
168
+ label: '主session(当前对话上下文)',
169
+ note: '清除对话上下文并重新开场,保留 Memory 和事件库。建议在空闲时操作',
170
+ stat: (records: number, ktok: number, size: string) => `${records}条 / ~${ktok}k tok / ${size}`,
171
+ cleared: 'session已清空重开(system前缀+开场消息)',
172
+ },
173
+ runlog: {
174
+ label: '运行日志(本次运行)',
175
+ note: '清除本次运行的日志,保留此前运行的日志。运行日志不进入模型上下文',
176
+ cleared: '运行日志已清空',
177
+ },
178
+ usage: {
179
+ label: 'token用量流水(成本页数据源)',
180
+ note: '清除全部模型用量与成本记录,成本页从后续写入的记录重新累计。这些记录不进入模型上下文',
181
+ stat: (n: number, size: string) => `${n}条 / ${size}`,
182
+ cleared: (n: number) => `已清除${n}条用量记录`,
183
+ },
184
+ toolcalls: {
185
+ label: '工具调用流水(工具名/原始参数/回执)',
186
+ note: '清除本次运行的工具调用日志,不改变模型上下文中的工具回执',
187
+ cleared: '工具调用流水已清空',
188
+ },
189
+ state: {
190
+ label: 'Core 状态',
191
+ note: '清除 Persona 状态、交接时间和模型连续失败记录,保留投递游标与 World 可见性',
192
+ stat: (n: number, lastHandoff: string) => `人格状态${n}项 / 上次交接${lastHandoff}`,
193
+ never: '无',
194
+ cleared: 'core状态已重置为默认',
195
+ },
196
+ wakes: {
197
+ label: '持久定时器',
198
+ note: '全部定时器取消(不产生通知)',
199
+ stat: (n: number) => `${n}个待触发`,
200
+ cleared: (n: number) => `已取消${n}个定时器`,
201
+ },
202
+ tracker: {
203
+ label: 'session统计(usage/缓存命中)',
204
+ note: '清零统计,保留正在运行的 session 条目',
205
+ stat: (n: number) => `${n}个session`,
206
+ cleared: 'session统计已清零',
207
+ },
208
+ pending: {
209
+ label: '待投递事件',
210
+ note:
211
+ '丢弃待投递的事件,保留事件库记录。延迟生成正文的队列项保留;已丢弃项不会在重启后补投',
212
+ stat: (n: number) => `${n}条待投递`,
213
+ cleared: (n: number) => `已丢弃${n}条待投递事件`,
214
+ },
215
+ },
216
+ config: {
217
+ unknownGroup: (id: string) => `没有这一组配置: ${id}`,
218
+ updated: (title: string, file: string) => `${title}已更新,已写回 ${file}`,
219
+ },
220
+ prompts: {
221
+ unknown: (key: string) => `未知提示词模板: ${key}`,
222
+ packageReadOnly: (title: string) => `${title} 是只读的扩展包模板`,
223
+ conflict: (title: string) => `${title} 已在别处被修改,请重新载入后再保存`,
224
+ saved: (title: string) => `已保存 ${title}`,
225
+ savedOverride: (title: string) => `已保存 ${title} 的部署覆盖文件`,
226
+ notEnvPrompt: (title: string) => `${title} 没有可恢复的默认模板`,
227
+ alreadyDefault: (title: string) => `${title} 本来就在用 World 默认`,
228
+ reset: (title: string) => `已删除 ${title} 的部署覆盖文件`,
229
+ },
230
+ visibility: {
231
+ shown: (id: string) => `${id} 对 agent 重新可见。事件投递已恢复;前缀段与工具要等前缀重载才回来。`,
232
+ hidden: (id: string) => `${id} 已对 agent 隐藏。新事件不再唤醒 agent(仍照常落库);前缀段与工具要等前缀重载才撤下。`,
233
+ prefixReloaded: (kept: number) => `系统前缀与工具表已重载,保留当前session的${kept}条既有消息`,
234
+ },
235
+ shutdown: {
236
+ pause: '暂停事件投递',
237
+ worlds: '停止 World',
238
+ core: '停止 Persona',
239
+ modulesTimedOut: 'World 停止超时',
240
+ externalState: (worldId: string) => `${worldId} 外部状态`,
241
+ stopIncomplete: (detail: string) => `World 停止未完成,不能采用外部核验缓存:${detail}`,
242
+ cacheReadFailed: (detail: string) => `读取已缓存的关机验证结果失败:${detail}`,
243
+ manualCheck: '请检查对应外部服务是否已停止。',
244
+ llm: '停止 Provider 实例',
245
+ flush: '保存 Core 状态',
246
+ web: '关闭控制台',
247
+ summarySkipped: '本地关机步骤未全部完成',
248
+ summaryComplete: '本地关机步骤全部完成',
249
+ summaryUnverified: (items: string[]) => `本地关机完成,但外部状态未确认结束:${items.join('、')}(需人工确认)`,
250
+ },
251
+ },
252
+ en: {
253
+ noFile: '(no file)',
254
+ storage: {
255
+ events: {
256
+ label: "Event store (this run's shard)",
257
+ note: 'Clears events from this run and keeps earlier runs; the cursor does not rewind',
258
+ stat: (count: number, cursor: number, size: string) => `${count} records (cursor at ${cursor}) / ${size}`,
259
+ cleared: (n: number) => `Cleared ${n} events from this run`,
260
+ },
261
+ session: {
262
+ label: 'Main session (current conversation context)',
263
+ note: 'Clears the conversation and reopens the session, keeping Memory and the event store. Prefer clearing while idle',
264
+ stat: (records: number, ktok: number, size: string) => `${records} records / ~${ktok}k tok / ${size}`,
265
+ cleared: 'Session cleared and reopened (system prefix + opening message)',
266
+ },
267
+ runlog: {
268
+ label: 'Run log (this run)',
269
+ note: 'Clears logs from this run and keeps earlier runs. Run logs are not included in model context',
270
+ cleared: 'Run log cleared',
271
+ },
272
+ usage: {
273
+ label: 'Token usage ledger (source of the cost page)',
274
+ note: 'Clears all model usage and cost records; totals restart with subsequently written records. These records are not included in model context',
275
+ stat: (n: number, size: string) => `${n} records / ${size}`,
276
+ cleared: (n: number) => `Cleared ${n} usage records`,
277
+ },
278
+ toolcalls: {
279
+ label: 'Tool call ledger (tool name / raw arguments / receipt)',
280
+ note: 'Clears tool call logs from this run without changing tool results in model context',
281
+ cleared: 'Tool call ledger cleared',
282
+ },
283
+ state: {
284
+ label: 'Core state',
285
+ note: 'Clears Persona state, handoff time and consecutive model failure records; keeps the delivery cursor and World visibility',
286
+ stat: (n: number, lastHandoff: string) => `${n} persona state entries / last handoff ${lastHandoff}`,
287
+ never: 'none',
288
+ cleared: 'Core state reset to defaults',
289
+ },
290
+ wakes: {
291
+ label: 'Persistent timers',
292
+ note: 'Cancels every timer (no notifications are produced)',
293
+ stat: (n: number) => `${n} pending`,
294
+ cleared: (n: number) => `Cancelled ${n} timers`,
295
+ },
296
+ tracker: {
297
+ label: 'Session statistics (usage / cache hits)',
298
+ note: 'Resets statistics and keeps entries for active sessions',
299
+ stat: (n: number) => `${n} sessions`,
300
+ cleared: 'Session statistics zeroed',
301
+ },
302
+ pending: {
303
+ label: 'Pending events',
304
+ note:
305
+ 'Discards pending events and keeps archived records. Deferred rendering items remain queued; discarded items will not be replayed after restart',
306
+ stat: (n: number) => `${n} pending`,
307
+ cleared: (n: number) => `Discarded ${n} pending events`,
308
+ },
309
+ },
310
+ config: {
311
+ unknownGroup: (id: string) => `No such config group: ${id}`,
312
+ updated: (title: string, file: string) => `${title} updated and written back to ${file}`,
313
+ },
314
+ prompts: {
315
+ unknown: (key: string) => `Unknown prompt template: ${key}`,
316
+ packageReadOnly: (title: string) => `${title} is a read-only extension package template`,
317
+ conflict: (title: string) => `${title} was modified elsewhere; reload before saving`,
318
+ saved: (title: string) => `Saved ${title}`,
319
+ savedOverride: (title: string) => `Saved the deployment override for ${title}`,
320
+ notEnvPrompt: (title: string) => `${title} has no default template to restore`,
321
+ alreadyDefault: (title: string) => `${title} is already using the World default`,
322
+ reset: (title: string) => `Removed the deployment override for ${title}`,
323
+ },
324
+ visibility: {
325
+ shown: (id: string) => `${id} is visible to the agent again. Event delivery has resumed; its prefix segment and tools return once the prefix is reloaded.`,
326
+ hidden: (id: string) => `${id} is now hidden from the agent. New events no longer wake the agent (they are still stored); its prefix segment and tools are removed once the prefix is reloaded.`,
327
+ prefixReloaded: (kept: number) => `System prefix and tool table reloaded; ${kept} existing messages of the current session kept`,
328
+ },
329
+ shutdown: {
330
+ pause: 'Pause event delivery',
331
+ worlds: 'Stop Worlds',
332
+ core: 'Stop Persona',
333
+ modulesTimedOut: 'World shutdown timed out',
334
+ externalState: (worldId: string) => `${worldId} external state`,
335
+ stopIncomplete: (detail: string) => `World stop incomplete, so the cached external verification cannot be used: ${detail}`,
336
+ cacheReadFailed: (detail: string) => `Failed to read the cached shutdown verification: ${detail}`,
337
+ manualCheck: 'Check whether the corresponding external service has stopped.',
338
+ llm: 'Stop provider instances',
339
+ flush: 'Persist core state',
340
+ web: 'Close the console',
341
+ summarySkipped: 'Local shutdown steps incomplete',
342
+ summaryComplete: 'Local shutdown finished: every step completed',
343
+ summaryUnverified: (items: string[]) => `Local shutdown finished, but external state is not confirmed ended: ${items.join(', ')} (manual confirmation needed)`,
344
+ },
345
+ },
346
+ };
347
+ type BotText = (typeof BOT_TEXT)['zh'];
348
+ const botText = (language: Language): BotText => pick(language, BOT_TEXT);
349
+
350
+ const fileSize = (dir: string, rel: string, noFile: string): string => {
351
+ const p = join(dir, rel);
352
+ if (!existsSync(p)) return noFile;
353
+ try {
354
+ return `${(statSync(p).size / 1024).toFixed(1)}KB`;
355
+ } catch {
356
+ return '?';
357
+ }
358
+ };
359
+
360
+ function deriveStorage<C extends CoreConfig>(core: Core<C>, dataDir: string, language: Language): StoragePart[] {
361
+ const t = botText(language);
362
+ const size = (dir: string, rel: string): string => fileSize(dir, rel, t.noFile);
363
+ const s = t.storage;
364
+ return [
365
+ {
366
+ key: 'events',
367
+ label: s.events.label,
368
+ kind: 'disk',
369
+ location: `data/runs/${core.run.id}/events.jsonl`,
370
+ danger: true,
371
+ note: s.events.note,
372
+ stat: () => s.events.stat(core.store.currentCount(), core.store.latestCursor(), size(core.run.dir, 'events.jsonl')),
373
+ clear: () => s.events.cleared(core.store.clear()),
374
+ },
375
+ {
376
+ key: 'session',
377
+ label: s.session.label,
378
+ kind: 'disk',
379
+ location: 'data/session-main.jsonl',
380
+ danger: true,
381
+ // 最后重建 session 前缀和开场,使其读取清理后的状态。
382
+ order: 10,
383
+ note: s.session.note,
384
+ stat: () =>
385
+ s.session.stat(core.session.records.length, Math.round(core.session.estTokens() / 1000), size(dataDir, 'session-main.jsonl')),
386
+ clear: async () => {
387
+ await core.loop.clearSession();
388
+ return s.session.cleared;
389
+ },
390
+ },
391
+ {
392
+ key: 'runlog',
393
+ label: s.runlog.label,
394
+ kind: 'disk',
395
+ location: `data/runs/${core.run.id}/log.jsonl`,
396
+ note: s.runlog.note,
397
+ stat: () => size(core.run.dir, 'log.jsonl'),
398
+ clear: () => {
399
+ core.runlog.clear();
400
+ return s.runlog.cleared;
401
+ },
402
+ },
403
+ {
404
+ key: 'usage',
405
+ label: s.usage.label,
406
+ kind: 'disk',
407
+ location: 'data/usage.jsonl',
408
+ note: s.usage.note,
409
+ stat: () => s.usage.stat(core.usageLog.count(), size(dataDir, 'usage.jsonl')),
410
+ clear: () => s.usage.cleared(core.usageLog.clear()),
411
+ },
412
+ {
413
+ key: 'toolcalls',
414
+ label: s.toolcalls.label,
415
+ kind: 'disk',
416
+ location: `data/runs/${core.run.id}/toolcalls.jsonl`,
417
+ note: s.toolcalls.note,
418
+ stat: () => core.toolLog.stat(),
419
+ clear: () => {
420
+ core.toolLog.clear();
421
+ return s.toolcalls.cleared;
422
+ },
423
+ },
424
+ {
425
+ key: 'state',
426
+ label: s.state.label,
427
+ kind: 'disk',
428
+ location: 'data/core-state.json',
429
+ note: s.state.note,
430
+ stat: () =>
431
+ s.state.stat(Object.keys(core.state.data.persona).length, core.state.data.lastTruncateAt ?? s.state.never),
432
+ clear: () => {
433
+ core.state.clear();
434
+ return s.state.cleared;
435
+ },
436
+ },
437
+ {
438
+ key: 'wakes',
439
+ label: s.wakes.label,
440
+ kind: 'disk',
441
+ location: 'data/timers.json',
442
+ note: s.wakes.note,
443
+ stat: () => s.wakes.stat(core.timers.list().length),
444
+ clear: () => s.wakes.cleared(core.timers.clearAll()),
445
+ },
446
+ {
447
+ key: 'tracker',
448
+ label: s.tracker.label,
449
+ kind: 'memory',
450
+ note: s.tracker.note,
451
+ stat: () => s.tracker.stat(core.sessions.list().length),
452
+ clear: () => {
453
+ core.sessions.reset();
454
+ return s.tracker.cleared;
455
+ },
456
+ },
457
+ {
458
+ // 在 session 重建前清除积压事件。
459
+
460
+ key: 'pending',
461
+ label: s.pending.label,
462
+ kind: 'memory',
463
+ order: 9,
464
+ note: s.pending.note,
465
+ stat: () => s.pending.stat(core.bus.pending()),
466
+ // 保留延迟渲染项,其回调还负责复位 World 的排队状态。
467
+ clear: () => s.pending.cleared(core.discardPendingEvents()),
468
+ },
469
+ ];
470
+ }
471
+
472
+ /**
473
+ * 环境提示词按 World、bot 包、部署的顺序覆盖,控制台只写部署覆盖文件。
474
+ * Persona 声明 deploymentPath 时写该路径,否则读写 path;扩展包中的模板只读,避免修改 pnpm store 的硬链接。
475
+ */
476
+ function derivePrompts<C extends CoreConfig>(
477
+ parts: BotParts<C>,
478
+ assembly: WorldAssembly,
479
+ contribution: ConsoleContribution,
480
+ timezone: string,
481
+ dirs: EnvPromptDirs,
482
+ packageReadOnly: boolean,
483
+ ): WebAppPromptDeps | undefined {
484
+ // 同 key 采用第一个声明;装配层优先于 Persona。
485
+ // 标题与说明按请求语言读取;key 与路径必须保持一致。
486
+ const docsOf = (language: Language) => {
487
+ const seen = new Set<string>();
488
+ return [
489
+ ...(contribution.promptDocs ?? []).map((d) => ({ ...d, scope: 'persona' as const })),
490
+ ...(parts.persona.console?.(language)?.promptDocs ?? []).map((d) => ({ ...d, scope: 'persona' as const })),
491
+
492
+ ...assembly.instances()
493
+ .flatMap((m) => (m.console?.(language)?.promptDocs ?? []).map((d) => ({ ...d, scope: 'world' as const, worldId: m.id }))),
494
+ ].filter((d) => (seen.has(d.key) ? false : (seen.add(d.key), true)));
495
+ };
496
+ type Doc = ReturnType<typeof docsOf>[number];
497
+ if (docsOf('zh').length === 0) return undefined;
498
+ const docOf = (key: string, language: Language): Doc => {
499
+ const doc = docsOf(language).find((d) => d.key === key);
500
+ if (!doc) throw new Error(botText(language).prompts.unknown(key));
501
+ return doc;
502
+ };
503
+ const revisionOf = (content: string): string => createHash('sha256').update(content).digest('hex');
504
+
505
+ const sourceOf = (d: Doc): { readPath: string; writePath: string; origin?: EnvPromptOrigin } => {
506
+ if (d.scope === 'world' && d.role === 'envPrompt') {
507
+ const { path, origin } = envPromptTemplateSource(d, d.worldId, dirs);
508
+
509
+ const writeDir = dirs.deploymentDir;
510
+ return { readPath: path, writePath: writeDir ? envPromptOverridePath(writeDir, d.worldId) : d.path, origin };
511
+ }
512
+ // path 由声明方解析为当前读取源。
513
+ if (d.deploymentPath) {
514
+ return {
515
+ readPath: d.path,
516
+ writePath: d.deploymentPath,
517
+ origin: d.path === d.deploymentPath ? 'deployment' : 'package',
518
+ };
519
+ }
520
+ return { readPath: d.path, writePath: d.path };
521
+ };
522
+
523
+ const varValues = async (): Promise<Record<string, string>> => {
524
+ const out: Record<string, string> = {};
525
+ try {
526
+ Object.assign(out, await parts.persona.promptVarValues?.({
527
+ now: new Date(),
528
+ timezone,
529
+ }) ?? {});
530
+ } catch { /* 忽略单个来源的变量读取错误。 */ }
531
+ for (const m of assembly.instances()) {
532
+ try {
533
+ Object.assign(out, (await m.envPromptVars()) ?? {});
534
+ } catch { /* 忽略单个来源的变量读取错误。 */ }
535
+ }
536
+ return out;
537
+ };
538
+
539
+ const readSource = (path: string): string => (existsSync(path) ? readFileSync(path, 'utf8') : '');
540
+
541
+ const readDoc = (d: Doc, values: Record<string, string>): PromptDocument => {
542
+ const { readPath, origin } = sourceOf(d);
543
+ const content = readSource(readPath);
544
+ return {
545
+ ...(d.role ? { role: d.role } : {}),
546
+ ...(origin ? { origin } : {}),
547
+ ...(d.vars?.length
548
+ ? {
549
+ vars: d.vars.map((v) => ({
550
+ ...v,
551
+ ...(Object.prototype.hasOwnProperty.call(values, v.name) ? { value: values[v.name] } : {}),
552
+ })),
553
+ }
554
+ : {}),
555
+ key: d.key,
556
+ title: d.title,
557
+ scope: d.scope,
558
+ description: d.description,
559
+ content,
560
+ revision: revisionOf(content),
561
+ };
562
+ };
563
+ return {
564
+ list: async (language) => {
565
+ const values = await varValues();
566
+ return docsOf(language).map((d) => readDoc(d, values));
567
+ },
568
+ prefix: () => assembleSystemSegments({
569
+ persona: parts.persona,
570
+ worlds: assembly.mounted,
571
+ now: new Date(),
572
+ timezone,
573
+ dirs,
574
+ }),
575
+ write: (key, content, baseRevision, language) => {
576
+ const t = botText(language).prompts;
577
+ const doc = docOf(key, language);
578
+ if (packageReadOnly && doc.scope === 'persona' && !doc.deploymentPath) {
579
+ throw new Error(t.packageReadOnly(doc.title));
580
+ }
581
+ const { readPath, writePath, origin } = sourceOf(doc);
582
+ if (baseRevision) {
583
+ const cur = revisionOf(readSource(readPath));
584
+ if (cur !== baseRevision) throw new PromptRevisionConflict(t.conflict(doc.title));
585
+ }
586
+ mkdirSync(dirname(writePath), { recursive: true });
587
+ const tmp = `${writePath}.tmp-${Math.random().toString(36).slice(2, 10)}`;
588
+ try {
589
+ writeFileSync(tmp, content, 'utf8');
590
+ renameSync(tmp, writePath);
591
+ } catch (error) {
592
+ try { if (existsSync(tmp)) unlinkSync(tmp); } catch { /* 保留原始写入错误 */ }
593
+ throw error;
594
+ }
595
+ return origin ? t.savedOverride(doc.title) : t.saved(doc.title);
596
+ },
597
+ reset: (key, language) => {
598
+ const t = botText(language).prompts;
599
+ const doc = docOf(key, language);
600
+ const { writePath, origin } = sourceOf(doc);
601
+ if (!origin) throw new Error(t.notEnvPrompt(doc.title));
602
+ if (origin === 'module') return t.alreadyDefault(doc.title);
603
+ unlinkSync(writePath);
604
+ return t.reset(doc.title);
605
+ },
606
+ };
607
+ }
608
+
609
+ type WorldVisibilityFacts = Pick<Core<CoreConfig>, 'worldVisibility'>;
610
+
611
+ /** 控制台状态清单不渲染环境模板;需要环境正文的调用方使用 deriveWorldInfo。 */
612
+ export type WorldFacts =
613
+ | Omit<WorldInfo, 'envPrompt'>
614
+ | Extract<ConsoleWorldInfo, { status: 'inactive' | 'missing' }>;
615
+
616
+ /** 挂载为 active,有实例但未挂载为 inactive,无法建立可用实例为 missing。 */
617
+ export function deriveWorldFacts(
618
+ core: WorldVisibilityFacts,
619
+ assembly: WorldAssembly,
620
+ language: Language,
621
+ ): WorldFacts[] {
622
+ const { visibility, driftedWorlds } = core.worldVisibility();
623
+ const slots: WorldFacts[] = assembly.slots.map((slot) => {
624
+ const m = slot.instance;
625
+
626
+ let decl;
627
+ try {
628
+ decl = m.console?.(language);
629
+ } catch {
630
+ decl = undefined;
631
+ }
632
+ const label = decl?.label ?? slot.label;
633
+ if (!slot.mounted) {
634
+ return { id: slot.id, status: 'inactive' as const, label, declared: slot.declared };
635
+ }
636
+ return {
637
+ id: m.id,
638
+ status: 'active' as const,
639
+ label,
640
+ declared: slot.declared,
641
+ workspace: `worlds/${m.id}`,
642
+ tools: m.tools().map((t) => t.name),
643
+ visible: visibility[m.id] !== false,
644
+ prefixDrifted: driftedWorlds.includes(m.id),
645
+ ...(decl?.lamps?.length ? { lamps: decl.lamps } : {}),
646
+ ...(decl?.badges ? { badges: decl.badges } : {}),
647
+ ...(decl?.links ? { links: decl.links } : {}),
648
+ };
649
+ });
650
+ const missing: WorldFacts[] = assembly.missing.map((m) => ({
651
+ id: m.id,
652
+ status: 'missing' as const,
653
+ label: m.label,
654
+ declared: m.declared ?? true,
655
+ reason: m.reason(language),
656
+ }));
657
+ return [...slots, ...missing];
658
+ }
659
+
660
+ async function deriveWorldInfo(
661
+ core: WorldVisibilityFacts,
662
+ assembly: WorldAssembly,
663
+ dirs: EnvPromptDirs,
664
+ language: Language,
665
+ ): Promise<ConsoleWorldInfo[]> {
666
+ return Promise.all(deriveWorldFacts(core, assembly, language).map(async (facts) => {
667
+ if (facts.status !== 'active') return facts;
668
+ return { ...facts, envPrompt: (await renderWorldEnvPrompt(assembly.slot(facts.id).instance, dirs)).text };
669
+ }));
670
+ }
671
+
672
+ /**
673
+ * 存储清单在装配期固定;stat/clear 按 key 访问当前实例,以支持定义实例重建。
674
+ */
675
+ function deriveSlotStorage(assembly: WorldAssembly, language: Language): OwnedStoragePart[] {
676
+ return assembly.slots.flatMap((slot) =>
677
+ (slot.instance.console?.(language)?.storage ?? []).map((part): OwnedStoragePart => {
678
+ const current = (): StoragePart => {
679
+ const found = slot.instance.console?.(language)?.storage?.find((p) => p.key === part.key);
680
+ if (!found) throw new Error(`${slot.id} 的存储项 ${part.key} 在当前实例上不存在`);
681
+ return found;
682
+ };
683
+ return { ...part, owner: `world:${slot.id}`, stat: () => current().stat(), clear: () => current().clear() };
684
+ }),
685
+ );
686
+ }
687
+
688
+ // 面板 id 在各页内唯一,转发时保持声明方提供的局部 id。
689
+ // 控制台声明异常由 ConsolePageRegistry 按来源隔离。
690
+ function normalizePanelDecls(
691
+ panels: readonly WorldPanelDecl[],
692
+ ): Array<{ id: string; title: string; description?: string; getMethods?: readonly string[] }> {
693
+ return panels.map((p) => ({
694
+ id: p.id,
695
+ title: p.title,
696
+ ...(p.description ? { description: p.description } : {}),
697
+ ...(p.getMethods ? { getMethods: [...p.getMethods] } : {}),
698
+ }));
699
+ }
700
+
701
+ export function ioPageContribution(
702
+ worldId: string,
703
+ label: string,
704
+ info: WorldFacts | undefined,
705
+ mod: World | undefined,
706
+ language: Language = 'zh',
707
+ ): ConsolePageContribution {
708
+ const decl = mod?.console?.(language);
709
+ const declaredPanels = decl?.panels ?? [];
710
+ const out: ConsolePageContribution = {
711
+ id: pageIdFor('world', worldId),
712
+ kind: 'world',
713
+ label: decl?.label ?? label,
714
+ availability: info?.status ?? 'inactive',
715
+ };
716
+ if (decl?.lamps?.length) out.lamps = decl.lamps;
717
+ if (decl?.badges?.length) out.badges = decl.badges;
718
+ if (declaredPanels.length) out.panels = normalizePanelDecls(declaredPanels);
719
+ if (decl?.links?.length) out.links = decl.links;
720
+ if (decl?.config?.length) out.config = decl.config;
721
+ if (decl?.promptDocs?.length) out.promptDocs = decl.promptDocs;
722
+ if (decl?.storage?.length) out.storage = decl.storage;
723
+ if (info?.declared !== undefined) out.declared = info.declared;
724
+ if (info?.status === 'missing') out.reason = info.reason;
725
+ if (info?.status === 'active') {
726
+ if (info.visible !== undefined) out.agentVisible = info.visible;
727
+ if (info.prefixDrifted !== undefined) out.prefixDrifted = info.prefixDrifted;
728
+ }
729
+ const invoke = decl?.invoke;
730
+ if (invoke) {
731
+ out.invoke = (panel, method, args) =>
732
+ invoke(panel, method, args);
733
+ }
734
+
735
+ const stream = decl?.stream;
736
+ if (stream) {
737
+ out.stream = (panel, socket) =>
738
+ stream(panel, socket);
739
+ }
740
+ return out;
741
+ }
742
+
743
+ export function personaPageContribution(
744
+ botId: string,
745
+ label: string,
746
+ core: Persona,
747
+ language: Language = 'zh',
748
+ ): ConsolePageContribution | null {
749
+ const decl = core.console?.(language);
750
+ if (!decl) return null;
751
+ const declaredPanels = decl.panels ?? [];
752
+ const out: ConsolePageContribution = {
753
+ id: pageIdFor('persona', botId),
754
+ kind: 'persona',
755
+ label,
756
+ availability: 'active',
757
+ };
758
+ if (decl.badges?.length) out.badges = decl.badges;
759
+ if (declaredPanels.length) out.panels = normalizePanelDecls(declaredPanels);
760
+ if (decl.config?.length) out.config = decl.config;
761
+ if (decl.promptDocs?.length) out.promptDocs = decl.promptDocs;
762
+ if (decl.storage?.length) out.storage = decl.storage;
763
+ const invoke = decl.invoke;
764
+ if (invoke) {
765
+ out.invoke = (panel, method, args) =>
766
+ invoke(panel, method, args);
767
+ }
768
+ return out;
769
+ }
770
+
771
+ /** Persona 的 memory 子声明成为 memory:<bot> 页;面板、模板与存储项三项皆空时没有这一页。invoke 与 Persona 页共用。 */
772
+ export function memoryPageContribution(
773
+ botId: string,
774
+ label: string,
775
+ persona: Persona,
776
+ language: Language = 'zh',
777
+ ): ConsolePageContribution | null {
778
+ const decl = persona.console?.(language)?.memory;
779
+ if (!decl) return null;
780
+ const panels = decl.panels ?? [];
781
+ const promptDocs = decl.promptDocs ?? [];
782
+ const storage = decl.storage ?? [];
783
+ if (!panels.length && !promptDocs.length && !storage.length) return null;
784
+ const out: ConsolePageContribution = {
785
+ id: pageIdFor('memory', botId),
786
+ kind: 'memory',
787
+ label,
788
+ availability: 'active',
789
+ };
790
+ if (panels.length) out.panels = normalizePanelDecls(panels);
791
+ if (promptDocs.length) out.promptDocs = promptDocs;
792
+ if (storage.length) out.storage = storage;
793
+ const invoke = persona.console?.(language)?.invoke;
794
+ if (invoke) out.invoke = (panel, method, args) => invoke(panel, method, args);
795
+ return out;
796
+ }
797
+
798
+ /** Memory 实例的类名;没有实例或只是个普通对象时为 null。 */
799
+ function memoryClassName(persona: Persona): string | null {
800
+ const name = persona.memory?.constructor?.name;
801
+ return name && name !== 'Object' ? name : null;
802
+ }
803
+
804
+ /** Persona 实例的类名;只是个普通对象时为 null。 */
805
+ function personaClassName(persona: Persona): string | null {
806
+ const name = persona.constructor?.name;
807
+ return name && name !== 'Object' ? name : null;
808
+ }
809
+
810
+ /** 合并同 id 的 Persona 页面,按面板归属分派 invoke 和 stream;重复面板由 validateContributions 拒绝。 */
811
+ export function mergePersonaContributions(
812
+ id: string,
813
+ label: string,
814
+ core: ConsolePageContribution | null,
815
+ extras: readonly ConsolePageContribution[],
816
+ ): ConsolePageContribution | null {
817
+ const parts = [core, ...extras].filter((c): c is ConsolePageContribution => !!c);
818
+ if (parts.length === 0) return null;
819
+ if (parts.length === 1 && parts[0]) return parts[0];
820
+
821
+ const out: ConsolePageContribution = {
822
+ id,
823
+ kind: 'persona',
824
+ label: parts.find((p) => p.label)?.label ?? label,
825
+ availability: 'active',
826
+ };
827
+ const badges = parts.flatMap((p) => p.badges ?? []);
828
+ if (badges.length) out.badges = badges;
829
+ const links = parts.flatMap((p) => p.links ?? []);
830
+ if (links.length) out.links = links;
831
+ const config = parts.flatMap((p) => p.config ?? []);
832
+ if (config.length) out.config = config;
833
+ const promptDocs = parts.flatMap((p) => p.promptDocs ?? []);
834
+ if (promptDocs.length) out.promptDocs = promptDocs;
835
+ const storage = parts.flatMap((p) => p.storage ?? []);
836
+ if (storage.length) out.storage = storage;
837
+
838
+ const panels = parts.flatMap((p) => p.panels ?? []);
839
+ if (panels.length) out.panels = panels;
840
+
841
+ const owner = new Map<string, ConsolePageContribution>();
842
+ for (const p of parts) {
843
+ for (const panel of p.panels ?? []) {
844
+ if (!owner.has(panel.id)) owner.set(panel.id, p);
845
+ }
846
+ }
847
+ if (parts.some((p) => p.invoke)) {
848
+ out.invoke = async (panel, method, args) => {
849
+ const target = owner.get(panel);
850
+ if (!target?.invoke) throw new Error(`没有面板数据面: ${panel}`);
851
+ return target.invoke(panel, method, args);
852
+ };
853
+ }
854
+ if (parts.some((p) => p.stream)) {
855
+ out.stream = (panel, socket) => {
856
+ const target = owner.get(panel);
857
+ if (!target?.stream) throw new Error(`没有流式面: ${panel}`);
858
+ target.stream(panel, socket);
859
+ };
860
+ }
861
+ return out;
862
+ }
863
+
864
+ export function deriveConsolePageSources(
865
+ core: WorldVisibilityFacts,
866
+
867
+ parts: { assembly: WorldAssembly; persona?: Persona },
868
+ /**
869
+ * bot 标识、展示名、Memory 名与配置组;配置组归入 Persona 页面。
870
+ * 展示名由部署配置提供;Persona 页标题取 Persona 的类名。
871
+ */
872
+ bot?: { id: string; label: string; memoryName?: string; configGroups?: readonly ConfigGroup[] },
873
+
874
+ extra?: (language: Language) => ConsolePageContribution[],
875
+ ): () => ConsolePageSource[] {
876
+ const { assembly } = parts;
877
+ const labelOf = (id: string): string => assembly.labelOf(id) ?? id;
878
+ return () => {
879
+ // 每次枚举按语言缓存状态,供同一批 contribute() 调用共享。
880
+ const once = new Map<Language, Map<string, WorldFacts>>();
881
+ const infos = (language: Language): Map<string, WorldFacts> => {
882
+ let facts = once.get(language);
883
+ if (!facts) {
884
+ facts = new Map(deriveWorldFacts(core, assembly, language).map((i) => [i.id, i]));
885
+ once.set(language, facts);
886
+ }
887
+ return facts;
888
+ };
889
+ const instances = new Map<string, World>(assembly.slots.map((s) => [s.id, s.instance]));
890
+ const ids = [...assembly.slots.map((s) => s.id), ...assembly.missing.map((m) => m.id)];
891
+ const sources: ConsolePageSource[] = ids.map((id) => ({
892
+ id: pageIdFor('world', id),
893
+ contribute: (language) =>
894
+ ioPageContribution(id, labelOf(id), infos(language).get(id), instances.get(id), language),
895
+ }));
896
+
897
+ const persona = parts.persona;
898
+ const selfId = bot ? pageIdFor('persona', bot.id) : null;
899
+
900
+ const extrasByLanguage = new Map<Language, ConsolePageContribution[]>();
901
+ const extrasOf = (language: Language): ConsolePageContribution[] => {
902
+ let list = extrasByLanguage.get(language);
903
+ if (!list) {
904
+ list = extra?.(language) ?? [];
905
+ extrasByLanguage.set(language, list);
906
+ }
907
+ return list;
908
+ };
909
+ const extras = extrasOf('zh');
910
+
911
+ // 同 id 的贡献共用页面与构建产物。
912
+ if (bot && selfId) {
913
+ // Persona 页写 Persona 的名字:一台 bot 的展示名是部署给的,类名才是这一层的身份。
914
+ const personaLabel = (persona && personaClassName(persona)) || bot.label;
915
+ const claimedGroups = [...(bot.configGroups ?? [])];
916
+ const botConfig: ConsolePageContribution[] = claimedGroups.length
917
+ ? [{ id: selfId, kind: 'persona', label: personaLabel, config: claimedGroups }]
918
+ : [];
919
+ sources.push({
920
+ id: selfId,
921
+ contribute: (language) => mergePersonaContributions(
922
+ selfId,
923
+ personaLabel,
924
+ persona ? personaPageContribution(bot.id, personaLabel, persona, language) : null,
925
+ [...extrasOf(language).filter((c) => c.id === selfId), ...botConfig],
926
+ ),
927
+ });
928
+ if (persona) {
929
+ const memoryLabel = bot.memoryName ?? memoryClassName(persona) ?? 'Memory';
930
+ sources.push({
931
+ id: pageIdFor('memory', bot.id),
932
+ contribute: (language) => memoryPageContribution(bot.id, memoryLabel, persona, language),
933
+ });
934
+ }
935
+ }
936
+ // 页面 id 不随语言变化;各来源异常由 registry 分别处理。
937
+
938
+ for (const c of extras) {
939
+ if (selfId && c.id === selfId) continue;
940
+ sources.push({ id: c.id, contribute: (language) => extrasOf(language).find((x) => x.id === c.id) ?? c });
941
+ }
942
+ return sources;
943
+ };
944
+ }
945
+
946
+ export function createBot<C extends CoreConfig>(
947
+ loaded: LoadedConfig<C>,
948
+ definition: BotDefinition<C>,
949
+ /** 启动时加载的扩展集;未提供时不显示扩展页。扩展 bot 的包内模板只读。 */
950
+ opts: { extensions?: ExtensionSet } = {},
951
+ ): Bot<C> {
952
+ const cfg = loaded.config;
953
+ // 环境模板允许包和部署覆盖,控制台仅写部署层。
954
+ const promptDirs: EnvPromptDirs = {
955
+ packageDir: loaded.packageDir ?? loaded.rootDir,
956
+ deploymentDir: loaded.rootDir,
957
+ };
958
+ // 默认语言用于 HTML 和未指定语言的请求。
959
+
960
+ const language = resolveLanguage(cfg.language);
961
+ const assembly = new WorldAssembly(loaded, definition.worlds ?? [], definition.declares ?? []);
962
+ const parts = definition.build(loaded, assembly.mounted);
963
+ if (parts.worlds?.length) assembly.addPrebuilt(parts.worlds);
964
+ const contribution = parts.console ?? {};
965
+
966
+ const core = new Core<C>(loaded, {
967
+ persona: parts.persona,
968
+ worlds: assembly.mounted,
969
+ llm: parts.llm,
970
+ });
971
+
972
+ const notifyLifecycle = (event: WorldLifecycleEvent): void => {
973
+ parts.persona.onWorldLifecycle?.(event);
974
+ };
975
+ assembly.bind({
976
+ mount: (mod) => core.mountWorld(mod),
977
+ unmount: async (id) => { await core.unmountWorld(id); },
978
+ lifecycle: notifyLifecycle,
979
+
980
+ reservedToolNames: () => [...RESERVED_FRAME_NAMES, ...(parts.persona.ownToolNames?.() ?? [])],
981
+ });
982
+
983
+ // 未激活实例也提供配置;组 id、owner 和配置键不随语言变化。
984
+
985
+ const configGroups = (language: Language): ConfigGroup[] => [
986
+ coreConfigGroup(language),
987
+ ...(contribution.configGroups ?? []),
988
+ ...assembly.instances().flatMap((m) => m.console?.(language)?.config ?? []),
989
+ ];
990
+
991
+ // 共享端点配置位于部署根的 providers/;activeProvider 属于当前部署。
992
+ const providerSettings = new ProviderSettings(cfg,core.providers,join(loaded.rootDir,'config.json'),loaded.providersDir ?? join(loaded.rootDir,'providers'));
993
+ const allConfigGroups = (language: Language) => [...configGroups(language),...providerSettings.groups(language)];
994
+ const llmManagers = new Map<string,{stop():Promise<unknown>}>([['providers',{stop:()=>core.providers.stopAll()}]]);
995
+
996
+ let webApp: WebApp | null = null;
997
+
998
+ /** 控制台与启动器共用一次关机操作,重复调用返回同一 Promise。 */
999
+ let shutdownOnce: Promise<ShutdownReport> | null = null;
1000
+ let coreStopOnce: Promise<void> | null = null;
1001
+ const stopCore = (): Promise<void> => {
1002
+ coreStopOnce ??= (async () => { await parts.onStop?.(); })();
1003
+ return coreStopOnce;
1004
+ };
1005
+ // start() 在启动控制台、Provider 和 World 之前获取单实例锁。
1006
+ let instanceLock: InstanceLock | null = null;
1007
+
1008
+ const beginShutdown = (
1009
+ reason: string,
1010
+ opts: { closeWeb: boolean; exit: boolean; language?: Language },
1011
+ ): Promise<ShutdownReport> => {
1012
+ shutdownOnce ??= runShutdown({
1013
+ reason,
1014
+ core,
1015
+ worlds: [...assembly.mounted],
1016
+ stopCore,
1017
+ llmManagers,
1018
+
1019
+ webApp: opts.closeWeb ? webApp : null,
1020
+ log: core.runlog.logger('shutdown'),
1021
+ language: opts.language ?? language,
1022
+ }).then((report) => {
1023
+ instanceLock?.release();
1024
+ instanceLock = null;
1025
+ if (opts.exit) {
1026
+ // 先发送关机结果,再关闭控制台并退出进程。
1027
+
1028
+ setTimeout(() => {
1029
+ void Promise.resolve(webApp?.stop()).finally(() => {
1030
+
1031
+ process.exit(report.complete ? 0 : 1);
1032
+ });
1033
+ }, 300);
1034
+ }
1035
+ return report;
1036
+ });
1037
+ return shutdownOnce;
1038
+ };
1039
+
1040
+ if (contribution.enabled !== false) {
1041
+ const startedAt = new Date().toISOString();
1042
+ /**
1043
+ * 可清除存储清单在装配期生成一次,/api/storage 与 bot 的 consolePages 共用这些对象;归属按来源盖章。
1044
+ * 贡献方须在 console().storage 中声明全部项目;stat 和 clear 可延迟执行,子进程代理也须在装配期提供完整声明。
1045
+ */
1046
+ const consoleStorage = (language: Language): OwnedStoragePart[] => {
1047
+ const decl = parts.persona.console?.(language);
1048
+ return [
1049
+ ...deriveStorage(core, loaded.dataDir, language).map((p): OwnedStoragePart => ({ ...p, owner: 'core' })),
1050
+ ...(decl?.storage ?? []).map((p): OwnedStoragePart => ({ ...p, owner: 'persona' })),
1051
+ ...(decl?.memory?.storage ?? []).map((p): OwnedStoragePart => ({ ...p, owner: 'memory' })),
1052
+ ...deriveSlotStorage(assembly, language),
1053
+ ];
1054
+ };
1055
+ webApp = new WebApp({
1056
+ store: core.store,
1057
+ memoryDir: loaded.memoryDir,
1058
+ dataDir: loaded.dataDir,
1059
+ botDir: loaded.rootDir,
1060
+ language,
1061
+ defaultScheme: cfg.web.theme,
1062
+ sessions: core.sessions,
1063
+ storage: consoleStorage,
1064
+ usage: { aggregate: (opts) => aggregateUsage(core.usageLog.readAll(), opts), status: () => core.usageLog.status() },
1065
+ config: {
1066
+ groups: (language) => allConfigGroups(language).map((group) => ({ group, values: group.owner.startsWith('provider:') ? providerSettings.values(group.id, language) : readGroupValues(cfg, group) })),
1067
+ set: (groupId: string, values: ConfigValues, language) => {
1068
+ const group = allConfigGroups(language).find((g) => g.id === groupId);
1069
+ if (group?.owner.startsWith('provider:')) return providerSettings.setConfig(groupId,values,language);
1070
+ if (!group) return botText(language).config.unknownGroup(groupId);
1071
+ const root = cfg as unknown as Record<string, unknown>;
1072
+ for (const [path, v] of Object.entries(values)) setConfigPath(root, path, v);
1073
+ return botText(language).config.updated(group.schema.title, persistConfig(loaded, values));
1074
+ },
1075
+
1076
+ options: (kind, language) => {
1077
+ const own = contribution.configOptions?.(kind, language);
1078
+ if (own?.length) return own;
1079
+ for (const def of definition.worlds ?? []) {
1080
+ const opts = def.configOptions?.(kind, language);
1081
+ if (opts?.length) return opts;
1082
+ }
1083
+ return [];
1084
+ },
1085
+ },
1086
+ worlds: async (language) => deriveWorldInfo(core, assembly, promptDirs, language),
1087
+
1088
+ consolePageSources: () => [...deriveConsolePageSources(
1089
+ core,
1090
+ { assembly, persona: parts.persona },
1091
+ {
1092
+ id: definition.id,
1093
+ label: cfg.displayName || definition.id,
1094
+ ...(definition.memoryName ? { memoryName: definition.memoryName } : {}),
1095
+
1096
+ ...(contribution.configGroups?.length ? { configGroups: contribution.configGroups } : {}),
1097
+ },
1098
+ contribution.consolePages
1099
+ ? (language) => contribution.consolePages!({ storage: consoleStorage(language), language })
1100
+ : undefined,
1101
+ )(),...providerSettings.sources()],
1102
+ providersLamp: (language) => providerSettings.providersLamp(language),
1103
+ worldVisibility: {
1104
+ state: () => core.worldVisibility(),
1105
+ set: (id, visible, language) => {
1106
+ core.setWorldVisible(id, visible);
1107
+ notifyLifecycle({ kind: 'visibility', id, label: assembly.labelOf(id) ?? id, visible });
1108
+ const t = botText(language).visibility;
1109
+ return visible ? t.shown(id) : t.hidden(id);
1110
+ },
1111
+ },
1112
+ sessionControl: {
1113
+ reloadPrefix: async (language) => {
1114
+ await core.loop.reloadSystemPrefix();
1115
+ return botText(language).visibility.prefixReloaded(Math.max(0, core.session.records.length - 1));
1116
+ },
1117
+ },
1118
+ run: {
1119
+ pause: () => core.bus.setPaused(true),
1120
+ resume: () => core.bus.setPaused(false),
1121
+ isPaused: () => core.bus.isPaused(),
1122
+
1123
+ shutdown: (language) => beginShutdown('控制台关机键', { closeWeb: false, exit: true, language }),
1124
+ // 重启请求先于关机写入,供监督进程在子进程退出后读取。
1125
+ restart: (language) => {
1126
+ requestRestart(loaded.dataDir);
1127
+ return beginShutdown('控制台重启键', { closeWeb: false, exit: true, language });
1128
+ },
1129
+ supervised: isSupervised(),
1130
+ },
1131
+ onboarding: {
1132
+ dismiss: () => { try { unlinkSync(join(loaded.rootDir, ONBOARDING_FLAG_FILE)); } catch { /* 已经删过 */ } },
1133
+ },
1134
+ ...(opts.extensions ? { extensions: new ExtensionManager(loaded.repoRoot ?? loaded.rootDir, opts.extensions) } : {}),
1135
+ debug: {
1136
+ sessionMessages: () => core.session.records,
1137
+ sessionHead: () => core.loop.sessionHead(),
1138
+ onSessionAppend: (cb) => core.session.onAppend(cb),
1139
+ onSessionReset: (cb) => core.session.onReset(cb),
1140
+ onEvent: (cb) => core.store.onAppend(cb),
1141
+ onRunlog: (cb) => core.runlog.onWrite(cb),
1142
+ recentLog: (limit) => core.runlog.recent(limit),
1143
+ runId: () => core.run.id,
1144
+ toolSchemas: () => core.loop.getToolSchemas(),
1145
+ },
1146
+ toolSchemas: {
1147
+ list: () => {
1148
+
1149
+ const byWorld = new Map<string, ToolOwner>();
1150
+ for (const m of assembly.mounted) {
1151
+ const label = assembly.labelOf(m.id);
1152
+ const owner: ToolOwner = { kind: 'world', id: m.id, ...(label ? { label } : {}) };
1153
+ for (const t of m.tools()) byWorld.set(t.name, owner);
1154
+ }
1155
+ const extras = (contribution.extraToolSchemas?.() ?? []).map((s) => ({ ...s, tags: [] }));
1156
+ const byName = new Map<string, ToolSchema & { owner: ToolOwner }>();
1157
+ for (const s of [...core.loop.getToolSchemas(), ...extras]) {
1158
+ if (byName.has(s.name)) continue;
1159
+ const owner = byWorld.get(s.name) ?? { kind: 'persona' as const };
1160
+ byName.set(s.name, {
1161
+ name: s.name, description: s.description, parameters: s.parameters, owner,
1162
+ });
1163
+ }
1164
+ return [...byName.values()];
1165
+ },
1166
+ },
1167
+ getStatus: () => ({
1168
+ displayName: cfg.displayName,
1169
+ startedAt,
1170
+ loop: core.loop.getStatus(),
1171
+ eventCount: core.store.latestCursor(),
1172
+ onboardingPending: existsSync(join(loaded.rootDir, ONBOARDING_FLAG_FILE)),
1173
+ ...(contribution.status?.() ?? {}),
1174
+ }),
1175
+ log: core.runlog.logger('console'),
1176
+ prompts: derivePrompts(parts, assembly, contribution, cfg.timezone, promptDirs, opts.extensions?.bot !== undefined),
1177
+
1178
+ worldActivation: {
1179
+ set: (id, enabled, language) => (enabled ? assembly.activate(id, language) : assembly.deactivate(id, language)),
1180
+ restart: (id, language) => assembly.restart(id, language),
1181
+ },
1182
+ });
1183
+ }
1184
+
1185
+ const app = webApp;
1186
+ return {
1187
+ core,
1188
+ parts,
1189
+ assembly,
1190
+ webApp,
1191
+ async start() {
1192
+ instanceLock ??= acquireInstanceLock(loaded.dataDir, {
1193
+ force: process.argv.includes('--force-second-instance'),
1194
+ log: core.runlog.logger('boot'),
1195
+ });
1196
+
1197
+ const port = app ? await app.start(cfg.web.port) : null;
1198
+ // 启动 Provider 不等待健康检查完成;健康轮询由托管器执行。
1199
+
1200
+ const orphans = Object.entries(cfg.providers).filter(([, entry]) => !providerModules.some((m) => m.id === entry.kind)).map(([name, entry]) => `${name}(kind=${entry.kind})`);
1201
+ 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)}));
1203
+ await parts.onStart?.({ core, loaded, port });
1204
+ await core.start();
1205
+ return { port };
1206
+ },
1207
+ async stop() {
1208
+ await core.stop();
1209
+ await stopCore();
1210
+ await Promise.all([...llmManagers.values()].map((m) => m.stop()));
1211
+ if (app) await app.stop();
1212
+ instanceLock?.release();
1213
+ instanceLock = null;
1214
+ },
1215
+ shutdown: (reason?: string) =>
1216
+ beginShutdown(reason ?? '外部请求', { closeWeb: true, exit: false }),
1217
+ };
1218
+ }
1219
+
1220
+ /** 按序执行关机步骤并记录结果;失败或超时后继续下一步。 */
1221
+ async function runShutdown<C extends CoreConfig>(ctx: {
1222
+ reason: string;
1223
+ core: Core<C>;
1224
+ /** 开始关机那一刻的挂载表快照 */
1225
+ worlds: readonly World[];
1226
+ stopCore: () => Promise<void>;
1227
+ llmManagers: Map<string, {stop():Promise<unknown>}>;
1228
+ webApp: WebApp | null;
1229
+ log: Logger;
1230
+ /** 步骤名和总结使用控制台请求语言。 */
1231
+ language: Language;
1232
+ }): Promise<ShutdownReport> {
1233
+ const t = pick(ctx.language, BOT_TEXT).shutdown;
1234
+ const steps: ShutdownStep[] = [];
1235
+ let externalChecks: ShutdownExternalCheck[] = [];
1236
+ let modulesSettled = false;
1237
+ let moduleFailures: WorldStopFailure[] = [];
1238
+ const run = async (
1239
+ key: string,
1240
+ label: string,
1241
+ budgetMs: number,
1242
+ work: () => Promise<unknown> | unknown,
1243
+ ): Promise<void> => {
1244
+ const t0 = Date.now();
1245
+ try {
1246
+ await withDeadline(Promise.resolve().then(work), budgetMs, label);
1247
+ steps.push({ key, label, ok: true, elapsedMs: Date.now() - t0 });
1248
+ ctx.log.info(`关机 ✓ ${label}`, { elapsedMs: Date.now() - t0 });
1249
+ } catch (err) {
1250
+ const detail = err instanceof Error ? err.message : String(err);
1251
+ steps.push({ key, label, ok: false, elapsedMs: Date.now() - t0, detail });
1252
+ ctx.log.warn(`关机步骤失败: ${label}`, { detail });
1253
+ }
1254
+ };
1255
+
1256
+ ctx.log.warn(`开始关机(${ctx.reason})`);
1257
+ await run('pause', t.pause, SHUTDOWN_BUDGET_MS.pause, () => {
1258
+ ctx.core.bus.setPaused(true);
1259
+ });
1260
+ await run('worlds', t.worlds, SHUTDOWN_BUDGET_MS.worlds, async () => {
1261
+ moduleFailures = await ctx.core.stop();
1262
+ modulesSettled = true;
1263
+ if (moduleFailures.length > 0) {
1264
+ throw new Error(moduleFailures.map((failure) => `${failure.worldId}:${failure.detail}`).join(';'));
1265
+ }
1266
+ });
1267
+ await run('core', t.core, SHUTDOWN_BUDGET_MS.core, ctx.stopCore);
1268
+ externalChecks = ctx.worlds.flatMap((module) => {
1269
+ if (!module.shutdownVerification) return [];
1270
+ const stopFailure = !modulesSettled
1271
+ ? t.modulesTimedOut
1272
+ : moduleFailures.find((failure) => failure.worldId === module.id)?.detail;
1273
+ if (stopFailure) {
1274
+ return [{
1275
+ key: `${module.id}.shutdown-verification`,
1276
+ label: t.externalState(module.id),
1277
+ status: 'unknown' as const,
1278
+ detail: t.stopIncomplete(stopFailure),
1279
+ manualAction: t.manualCheck,
1280
+ }];
1281
+ }
1282
+ try {
1283
+ return module.shutdownVerification().map((check) => ({ ...check }));
1284
+ } catch (error) {
1285
+ return [{
1286
+ key: `${module.id}.shutdown-verification`,
1287
+ label: t.externalState(module.id),
1288
+ status: 'unknown' as const,
1289
+ detail: t.cacheReadFailed(error instanceof Error ? error.message : String(error)),
1290
+ manualAction: t.manualCheck,
1291
+ }];
1292
+ }
1293
+ });
1294
+ await run('llm', t.llm, SHUTDOWN_BUDGET_MS.llm, () =>
1295
+ Promise.all([...ctx.llmManagers.values()].map((m) => m.stop())));
1296
+ await run('flush', t.flush, SHUTDOWN_BUDGET_MS.flush, () => {
1297
+ ctx.core.state.save();
1298
+ });
1299
+ const app = ctx.webApp;
1300
+ if (app) await run('web', t.web, SHUTDOWN_BUDGET_MS.web, () => app.stop());
1301
+
1302
+ const localComplete = steps.every((s) => s.ok);
1303
+ const externalComplete = externalChecks.every((check) => check.status === 'verified-ended');
1304
+ const complete = localComplete && externalComplete;
1305
+ for (const check of externalChecks) {
1306
+ if (check.status === 'verified-ended') continue;
1307
+ ctx.log.error(`[P0] 外部状态未确认结束:${check.label}`, {
1308
+ status: check.status,
1309
+ detail: check.detail,
1310
+ manualAction: check.manualAction,
1311
+ });
1312
+ }
1313
+
1314
+ const unverified = externalChecks.filter((check) => check.status !== 'verified-ended');
1315
+ const summary = !localComplete
1316
+ ? t.summarySkipped
1317
+ : complete
1318
+ ? t.summaryComplete
1319
+ : t.summaryUnverified(unverified.map((check) => `${check.label}=${check.status}`));
1320
+ ctx.log.emit('warn', summary, {
1321
+ event: 'shutdown-summary',
1322
+ data: {
1323
+ steps: steps.map((s) => `${s.label}=${s.ok ? 'ok' : s.detail}`),
1324
+ externalChecks: externalChecks.map((check) => `${check.label}=${check.status}`),
1325
+ },
1326
+ });
1327
+ closeRun(ctx.core.run, {
1328
+ endedAt: nowIso(ctx.core.config.timezone),
1329
+ lastCursor: ctx.core.store.latestCursor(),
1330
+ complete,
1331
+ reason: ctx.reason,
1332
+ });
1333
+ return { reason: ctx.reason, localComplete, complete, steps, externalChecks };
1334
+ }
1335
+
1336
+ function persistConfig<C extends CoreConfig>(loaded: LoadedConfig<C>, values: ConfigValues): string {
1337
+ const cfgPath = join(loaded.rootDir, 'config.json');
1338
+ updateJsonObject(cfgPath, (raw) => {
1339
+ for (const [path, v] of Object.entries(values)) setConfigPath(raw, path, v);
1340
+ });
1341
+ return 'config.json';
1342
+ }