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
@@ -0,0 +1,519 @@
1
+ /**
2
+ * 扩展是 extensions/ 下的 npm 包,manifest.kind 为 world、provider 或 bot。
3
+ * 启动时读取直接依赖并分别校验 manifest、默认导出、id 命名空间与控制台页前缀。
4
+ * 单包失败不阻止其他包加载;安装或卸载后需重启进程,ESM 模块不会在运行时替换。
5
+ * 仅导入 deployment.json 选定的 bot;其他 bot 包记为 idle,仓内同名 bot 优先。
6
+ * 扩展包目录只读,避免修改 pnpm store 的硬链接文件;模板写入规则见 src/bot.ts。
7
+ * 扩展提供包内相对产物路径,服务端校验后分配 URL,只提供已声明文件。
8
+ */
9
+ import { spawn } from 'node:child_process';
10
+ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
11
+ import { isAbsolute, join, resolve, sep } from 'node:path';
12
+ import { pathToFileURL } from 'node:url';
13
+ import type { CoreConfig, Logger } from './core/types.ts';
14
+ import type { BotDefinition } from './bot.ts';
15
+ import type { WorldDefinition, WorldSection } from './world.ts';
16
+ import type { ProviderModule } from './providers/base.ts';
17
+ import type { ExtensionInfo, ExtensionInstallTarget, ExtensionSearchHit } from './web/server.ts';
18
+ import {
19
+ EXTENSION_KEYWORDS,
20
+ parseExtensionManifest,
21
+ type ExtensionConsoleAsset,
22
+ type ExtensionKind,
23
+ type ExtensionPackageJson,
24
+ } from './extensions/manifest.ts';
25
+ import { registerFrameworkResolver } from './extensions/runtime.ts';
26
+ import { pageIdFor, type ContributingKind } from './web/shared/console-protocol.ts';
27
+
28
+ export const EXTENSIONS_DIRNAME = 'extensions';
29
+
30
+ /** 每类扩展的控制台页前缀。 */
31
+ export const EXTENSION_PAGE_KIND: Readonly<Record<ExtensionKind, ContributingKind>> = {
32
+ world: 'world',
33
+ provider: 'llm',
34
+ bot: 'persona',
35
+ };
36
+ const NPM_REGISTRY = 'https://registry.npmjs.org';
37
+
38
+ /** 一个已安装包在本进程启动时的加载结果。 */
39
+ export interface ExtensionRecord {
40
+ name: string;
41
+ /** extensions/package.json 里写的版本范围或 `link:` 路径。 */
42
+ spec: string;
43
+ /** 磁盘上的版本;node_modules 里没有这个包时为 null。 */
44
+ version: string | null;
45
+ description?: string;
46
+ /** manifest 里的类别;package.json 解析不出 manifest 时缺席。 */
47
+ kind?: ExtensionKind;
48
+ /** 扩展声明的契约版本(`cortico.api`)。 */
49
+ api?: number;
50
+ /** 包声明了浏览器端产物(`cortico.consoleClient`)。 */
51
+ consoleClient: boolean;
52
+ /** 浏览器产物状态:none=未声明,served=文件可提供,missing=声明的文件不存在。 */
53
+ console?: 'none' | 'served' | 'missing';
54
+ loaded: boolean;
55
+ /** bot 包装了但这份部署没引用它:没有 import,不算失败。 */
56
+ idle?: true;
57
+ reason?: string;
58
+ /** 默认导出的 id:World id、provider id 或 bot id,按 kind 解释。 */
59
+ worldId?: string;
60
+ label?: string;
61
+ }
62
+
63
+ export interface ExtensionSet {
64
+ dir: string;
65
+ records: ExtensionRecord[];
66
+ worlds: WorldDefinition<WorldSection>[];
67
+ providers: ProviderModule[];
68
+ /** 已加载的扩展里带浏览器端产物的那些;服务端据此分配 URL 并只发这几个文件。 */
69
+ consoleAssets: ExtensionConsoleAsset[];
70
+ /** 这份部署的 bot 来自扩展包时是它;仓内 bot 则缺席。 */
71
+ bot?: ActiveBotPackage;
72
+ }
73
+
74
+ /** 启动器已经 import 过的那个 bot 包:包名与 `BotDefinition.id`。 */
75
+ export interface ActiveBotPackage {
76
+ name: string;
77
+ id: string;
78
+ }
79
+
80
+ export function extensionsDir(repoRoot: string): string {
81
+ return join(repoRoot, EXTENSIONS_DIRNAME);
82
+ }
83
+
84
+ function readPackageJson(file: string): ExtensionPackageJson | null {
85
+ if (!existsSync(file)) return null;
86
+ return JSON.parse(readFileSync(file, 'utf8')) as ExtensionPackageJson;
87
+ }
88
+
89
+ /** 读取 extensions/package.json 的直接依赖;目录不存在时返回空列表。 */
90
+ export function readInstalled(dir: string): Array<{ name: string; spec: string }> {
91
+ const pkg = readPackageJson(join(dir, 'package.json'));
92
+ return Object.entries(pkg?.dependencies ?? {}).map(([name, spec]) => ({ name, spec }));
93
+ }
94
+
95
+ /**
96
+ * 包的 ESM 入口:`exports`(字符串或 `.` 条目的 import/default)→ `module` → `main` → index.js。
97
+ * 只认这几层;更复杂的 exports 条件表由包在 `main` 里给一个平坦入口。
98
+ */
99
+ export function resolveExtensionEntry(pkgDir: string, pkg: ExtensionPackageJson): string {
100
+ const pick = (v: unknown): string | null => {
101
+ if (typeof v === 'string') return v;
102
+ if (v && typeof v === 'object') {
103
+ const o = v as Record<string, unknown>;
104
+ return pick(o['.'] ?? o.import ?? o.default ?? null);
105
+ }
106
+ return null;
107
+ };
108
+ const rel = pick(pkg.exports) ?? pkg.module ?? pkg.main ?? 'index.js';
109
+ return join(pkgDir, rel);
110
+ }
111
+
112
+ export function isWorldDefinition(v: unknown): v is WorldDefinition<WorldSection> {
113
+ if (!v || typeof v !== 'object') return false;
114
+ const d = v as Record<string, unknown>;
115
+ return typeof d.id === 'string' && d.id !== ''
116
+ && typeof d.label === 'string'
117
+ && typeof d.defaults === 'function'
118
+ && typeof d.create === 'function';
119
+ }
120
+
121
+ export function isProviderModule(v: unknown): v is ProviderModule {
122
+ if (!v || typeof v !== 'object') return false;
123
+ const d = v as Record<string, unknown>;
124
+ return typeof d.id === 'string' && d.id !== ''
125
+ && typeof d.title === 'string'
126
+ && Array.isArray(d.reasoningTiers)
127
+ && Array.isArray(d.serviceTiers)
128
+ && typeof d.create === 'function';
129
+ }
130
+
131
+ export function isBotDefinition(v: unknown): v is BotDefinition<CoreConfig> {
132
+ if (!v || typeof v !== 'object') return false;
133
+ const d = v as Record<string, unknown>;
134
+ return typeof d.id === 'string' && d.id !== ''
135
+ && typeof d.defaults === 'function'
136
+ && typeof d.build === 'function';
137
+ }
138
+
139
+ const SHAPE_NEEDS: Readonly<Record<ExtensionKind, string>> = {
140
+ world: 'WorldDefinition 的 id / label / defaults() / create()',
141
+ provider: 'ProviderModule 的 id / title / reasoningTiers / serviceTiers / create()',
142
+ bot: 'BotDefinition 的 id / defaults() / build()',
143
+ };
144
+
145
+ /** 默认导出与 `cortico.kind` 对不上时给作者的一句话。装载器与 `check:extension` 共用。 */
146
+ export function extensionShapeMismatch(kind: ExtensionKind): string {
147
+ return `声明是 ${kind} 类,但默认导出缺 ${SHAPE_NEEDS[kind]}。`;
148
+ }
149
+
150
+ /** 解析包内相对路径;超出包目录或文件不存在时返回 null。 */
151
+ export function extensionPackageFile(pkgDir: string, relative: string): string | null {
152
+ const abs = resolve(pkgDir, relative);
153
+ if (!abs.startsWith(resolve(pkgDir) + sep)) return null;
154
+ return existsSync(abs) ? abs : null;
155
+ }
156
+
157
+ // bot 包:定位与 import(启动器在装载其它扩展之前做)
158
+
159
+ export type BotPackageLocation =
160
+ | { source: 'tree'; pkgDir: string; entry: string }
161
+ | { source: 'extension'; name: string; pkgDir: string; entry: string };
162
+
163
+ /**
164
+ * `deployment.json` 的 `bot` 字段是包标识,两处找,仓内赢:`treeDir`(`bots/<bot>/`)下有
165
+ * `index.ts` 就是仓内包;否则 `extensions/package.json` 的 dependencies 里有这个名字,且它的
166
+ * manifest 是 `kind: "bot"`。都没有就报错,两处路径都列出来。
167
+ */
168
+ export function locateBotPackage(repoRoot: string, ref: string, treeDir: string): BotPackageLocation {
169
+ const treeEntry = join(treeDir, 'index.ts');
170
+ if (existsSync(treeEntry)) return { source: 'tree', pkgDir: treeDir, entry: treeEntry };
171
+ const dir = extensionsDir(repoRoot);
172
+ if (!readInstalled(dir).some((p) => p.name === ref)) {
173
+ throw new Error(`找不到 bot 代码包「${ref}」:仓内没有 ${treeEntry},${dir} 下也没装叫这个名字的扩展。`);
174
+ }
175
+ const pkgDir = join(dir, 'node_modules', ...ref.split('/'));
176
+ const pkg = readPackageJson(join(pkgDir, 'package.json'));
177
+ if (!pkg) throw new Error(`扩展「${ref}」登记在 ${dir} 的 package.json 里,但 node_modules 里没有它;在 extensions/ 下重新安装一次。`);
178
+ const parsed = parseExtensionManifest(pkg);
179
+ if (!parsed.ok) throw new Error(`扩展「${ref}」装不上: ${parsed.reasons.join(';')}`);
180
+ if (parsed.manifest.kind !== 'bot') {
181
+ throw new Error(`「${ref}」是 ${parsed.manifest.kind} 类扩展,不是 bot 包;deployment.json 的 bot 字段要指向 cortico.kind 为 "bot" 的包。`);
182
+ }
183
+ return { source: 'extension', name: ref, pkgDir, entry: resolveExtensionEntry(pkgDir, pkg) };
184
+ }
185
+
186
+ /**
187
+ * import 定位到的 bot 包并校验默认导出。扩展来源的 bot 其 id 不得与仓内 `bots/` 任一目录同名:
188
+ * 控制台按 `persona:<id>` 找面板产物,构建产物表里同 key 的仓内页优先,撞名会拿到仓内那份面板。
189
+ */
190
+ export async function importBotDefinition(
191
+ location: BotPackageLocation,
192
+ opts: { treeHas: (id: string) => boolean },
193
+ ): Promise<BotDefinition<CoreConfig>> {
194
+ registerFrameworkResolver();
195
+ const mod = (await import(pathToFileURL(location.entry).href)) as { default?: unknown };
196
+ if (!isBotDefinition(mod.default)) {
197
+ throw new Error(location.source === 'tree'
198
+ ? `${location.entry} 没有默认导出一个 BotDefinition`
199
+ : `扩展「${location.name}」${extensionShapeMismatch('bot')}`);
200
+ }
201
+ if (location.source === 'extension' && opts.treeHas(mod.default.id)) {
202
+ throw new Error(`扩展「${location.name}」的 bot id「${mod.default.id}」与仓内 bots/${mod.default.id}/ 撞名;控制台面板产物按 persona:<id> 找,会拿到仓内那份。换一个 id。`);
203
+ }
204
+ return mod.default;
205
+ }
206
+
207
+ /**
208
+ * 加载扩展 World 与 provider;id 与内建冲突时拒绝,扩展间重名时保留先加载项。
209
+ * 两个 kind 使用独立命名空间。activeBot 已由启动器导入,此处只标记 loaded,其他 bot 记 idle。
210
+ */
211
+ export async function loadExtensions(
212
+ repoRoot: string,
213
+ opts: { reserved?: Iterable<string>; reservedProviders?: Iterable<string>; activeBot?: ActiveBotPackage; log?: Logger } = {},
214
+ ): Promise<ExtensionSet> {
215
+ registerFrameworkResolver();
216
+ const dir = extensionsDir(repoRoot);
217
+ const taken: Record<Exclude<ExtensionKind, 'bot'>, Set<string>> = {
218
+ world: new Set(opts.reserved ?? []),
219
+ provider: new Set(opts.reservedProviders ?? []),
220
+ };
221
+ const records: ExtensionRecord[] = [];
222
+ const worlds: WorldDefinition<WorldSection>[] = [];
223
+ const providers: ProviderModule[] = [];
224
+ const consoleAssets: ExtensionConsoleAsset[] = [];
225
+ for (const { name, spec } of readInstalled(dir)) {
226
+ const pkgDir = join(dir, 'node_modules', ...name.split('/'));
227
+ const pkg = readPackageJson(join(pkgDir, 'package.json'));
228
+ const record: ExtensionRecord = {
229
+ name,
230
+ spec,
231
+ version: pkg?.version ?? null,
232
+ consoleClient: false,
233
+ loaded: false,
234
+ ...(pkg?.description ? { description: pkg.description } : {}),
235
+ };
236
+ records.push(record);
237
+ if (!pkg) {
238
+ record.reason = 'node_modules 里没有这个包;在 extensions/ 下重新安装一次。';
239
+ continue;
240
+ }
241
+ const parsed = parseExtensionManifest(pkg);
242
+ for (const warning of parsed.warnings) opts.log?.warn(`扩展 manifest: ${name} — ${warning}`);
243
+ if (!parsed.ok) {
244
+ record.reason = parsed.reasons.join(';');
245
+ continue;
246
+ }
247
+ const { kind, api, consoleClient, consoleStyle } = parsed.manifest;
248
+ record.kind = kind;
249
+ record.api = api;
250
+ record.consoleClient = consoleClient !== undefined;
251
+
252
+ // 缺少面板产物不阻止定义加载。
253
+ const jsFile = consoleClient === undefined ? null : extensionPackageFile(pkgDir, consoleClient);
254
+ record.console = consoleClient === undefined ? 'none' : jsFile ? 'served' : 'missing';
255
+ if (consoleClient !== undefined && !jsFile) {
256
+ opts.log?.warn(`扩展声明的控制台产物不在: ${name} — ${consoleClient}`);
257
+ }
258
+ const cssFile = consoleStyle !== undefined && jsFile ? extensionPackageFile(pkgDir, consoleStyle) : null;
259
+ if (consoleStyle !== undefined && jsFile && !cssFile) {
260
+ opts.log?.warn(`扩展声明的控制台样式不在,面板按无样式发: ${name} — ${consoleStyle}`);
261
+ }
262
+
263
+ const serveAsset = (id: string): void => {
264
+ if (!jsFile) return;
265
+ consoleAssets.push({
266
+ pageId: pageIdFor(EXTENSION_PAGE_KIND[kind], id),
267
+ packageName: name,
268
+ version: pkg.version ?? '0',
269
+ jsFile,
270
+ ...(cssFile ? { cssFile } : {}),
271
+ });
272
+ };
273
+
274
+ if (kind === 'bot') {
275
+ if (opts.activeBot?.name !== name) {
276
+ record.idle = true;
277
+ continue;
278
+ }
279
+ record.worldId = opts.activeBot.id;
280
+ record.loaded = true;
281
+ serveAsset(opts.activeBot.id);
282
+ continue;
283
+ }
284
+
285
+ const entry = resolveExtensionEntry(pkgDir, pkg);
286
+ let exported: unknown;
287
+ try {
288
+ exported = ((await import(pathToFileURL(entry).href)) as { default?: unknown }).default;
289
+ } catch (error) {
290
+ record.reason = `导入失败: ${error instanceof Error ? error.message : String(error)}`;
291
+ opts.log?.warn(`扩展导入失败: ${name}`, { entry, error: record.reason });
292
+ continue;
293
+ }
294
+
295
+ let id: string;
296
+ if (kind === 'world') {
297
+ if (!isWorldDefinition(exported)) {
298
+ record.reason = extensionShapeMismatch('world');
299
+ continue;
300
+ }
301
+ record.worldId = exported.id;
302
+ if (taken.world.has(exported.id)) {
303
+ record.reason = `World id「${exported.id}」已被内建 World 或先加载的扩展占用。`;
304
+ continue;
305
+ }
306
+ taken.world.add(exported.id);
307
+ record.label = exported.label;
308
+ worlds.push(exported);
309
+ id = exported.id;
310
+ } else {
311
+ if (!isProviderModule(exported)) {
312
+ record.reason = extensionShapeMismatch('provider');
313
+ continue;
314
+ }
315
+ record.worldId = exported.id;
316
+ if (taken.provider.has(exported.id)) {
317
+ record.reason = `provider id「${exported.id}」已被内建 provider 或先加载的扩展占用。`;
318
+ continue;
319
+ }
320
+ taken.provider.add(exported.id);
321
+ record.label = exported.title;
322
+ providers.push(exported);
323
+ id = exported.id;
324
+ }
325
+ record.loaded = true;
326
+ serveAsset(id);
327
+ }
328
+ const bot = opts.activeBot && records.some((r) => r.loaded && r.kind === 'bot') ? opts.activeBot : undefined;
329
+ return { dir, records, worlds, providers, consoleAssets, ...(bot ? { bot } : {}) };
330
+ }
331
+
332
+ // 装卸与搜索(控制台那一面)
333
+
334
+ /** npm 包名。与 npm 自己的校验同形;不含任何 shell 元字符。 */
335
+ const PACKAGE_NAME = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
336
+ /**
337
+ * 版本或 dist-tag。`^` `~` `*` 之外的范围符号(`>=` `||`)不收:它们在 cmd.exe 里是
338
+ * 重定向与管道,而 Windows 上 corepack 只能经 shell 起。
339
+ */
340
+ const VERSION_SPEC = /^[0-9a-zA-Z.^~*+-]{1,64}$/;
341
+ /** 本地目录。排除 `%` `!` `"` 与重定向符,其余交给引号。 */
342
+ const LOCAL_PATH = /^[A-Za-z0-9_.\-/:\\ ~]{1,512}$/;
343
+
344
+ export type PackageManagerRunner = (args: string[], cwd: string) => Promise<{ code: number; output: string }>;
345
+
346
+ /** Windows 通过 shell 调用 corepack 的 .cmd 文件,参数逐个加引号。 */
347
+ export const runPnpm: PackageManagerRunner = (args, cwd) => new Promise((done, fail) => {
348
+ const viaShell = process.platform === 'win32';
349
+ const argv = ['pnpm', ...args].map((a) => (viaShell ? `"${a}"` : a));
350
+ const child = spawn('corepack', argv, {
351
+ cwd,
352
+ shell: viaShell,
353
+ windowsHide: true,
354
+ env: { ...process.env, COREPACK_ENABLE_DOWNLOAD_PROMPT: '0' },
355
+ });
356
+ let output = '';
357
+ child.stdout.on('data', (chunk: Buffer) => { output += chunk.toString(); });
358
+ child.stderr.on('data', (chunk: Buffer) => { output += chunk.toString(); });
359
+ child.on('error', fail);
360
+ child.on('close', (code) => done({ code: code ?? -1, output }));
361
+ });
362
+
363
+ interface RegistrySearchResponse {
364
+ objects?: Array<{
365
+ package?: {
366
+ name?: string;
367
+ version?: string;
368
+ description?: string;
369
+ keywords?: string[];
370
+ date?: string;
371
+ links?: { npm?: string; homepage?: string; repository?: string };
372
+ publisher?: { username?: string };
373
+ };
374
+ downloads?: { monthly?: number };
375
+ }>;
376
+ }
377
+
378
+ export interface ExtensionManagerOptions {
379
+ run?: PackageManagerRunner;
380
+ registry?: string;
381
+ fetchJson?: (url: string) => Promise<unknown>;
382
+ }
383
+
384
+ /** 扩展管理接口;安装与卸载串行执行,避免并发修改同一依赖目录。 */
385
+ export class ExtensionManager {
386
+ private readonly dir: string;
387
+ private readonly run: PackageManagerRunner;
388
+ private readonly registry: string;
389
+ private readonly fetchJson: (url: string) => Promise<unknown>;
390
+ private busy = false;
391
+
392
+ constructor(
393
+ private readonly repoRoot: string,
394
+ private readonly booted: ExtensionSet,
395
+ opts: ExtensionManagerOptions = {},
396
+ ) {
397
+ this.dir = booted.dir;
398
+ this.run = opts.run ?? runPnpm;
399
+ this.registry = (opts.registry ?? NPM_REGISTRY).replace(/\/$/, '');
400
+ this.fetchJson = opts.fetchJson ?? (async (url) => {
401
+ const res = await fetch(url);
402
+ if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
403
+ return res.json();
404
+ });
405
+ }
406
+
407
+ /** 已加载扩展的浏览器端产物。服务端据此把页 id 映到 URL 并只发这几个文件。 */
408
+ consoleAssets(): readonly ExtensionConsoleAsset[] {
409
+ return this.booted.consoleAssets;
410
+ }
411
+
412
+ /** 将启动时的加载结果与当前安装状态比较;不一致时标记待重启。 */
413
+ list(): { dir: string; extensions: ExtensionInfo[] } {
414
+ const onDisk = new Map(readInstalled(this.dir).map((p) => [p.name, p.spec]));
415
+ const out: ExtensionInfo[] = [];
416
+ for (const r of this.booted.records) {
417
+ const spec = onDisk.get(r.name);
418
+ onDisk.delete(r.name);
419
+ const state: ExtensionInfo['state'] = spec === undefined ? 'removed'
420
+ : spec !== r.spec ? 'pending-restart'
421
+ : r.loaded ? 'loaded' : r.idle ? 'idle' : 'failed';
422
+ out.push({ ...r, state });
423
+ }
424
+ // 新装包仅报告 manifest 声明,浏览器产物状态在下一次加载时确定。
425
+ for (const [name, spec] of onDisk) {
426
+ const pkg = readPackageJson(join(this.dir, 'node_modules', ...name.split('/'), 'package.json'));
427
+ const parsed = pkg ? parseExtensionManifest(pkg) : null;
428
+ const manifest = parsed?.ok ? parsed.manifest : null;
429
+ out.push({
430
+ name,
431
+ spec,
432
+ version: pkg?.version ?? null,
433
+ consoleClient: manifest?.consoleClient !== undefined,
434
+ ...(manifest ? { kind: manifest.kind, api: manifest.api } : {}),
435
+ ...(manifest && manifest.consoleClient === undefined ? { console: 'none' as const } : {}),
436
+ loaded: false,
437
+ state: 'pending-restart',
438
+ ...(pkg?.description ? { description: pkg.description } : {}),
439
+ });
440
+ }
441
+ return { dir: this.dir, extensions: out };
442
+ }
443
+
444
+ async search(query: string, kind: ExtensionKind = 'world'): Promise<ExtensionSearchHit[]> {
445
+ const keyword = EXTENSION_KEYWORDS[kind];
446
+ const text = `keywords:${keyword} ${query.trim()}`.trim();
447
+ const url = `${this.registry}/-/v1/search?text=${encodeURIComponent(text)}&size=50`;
448
+ const data = (await this.fetchJson(url)) as RegistrySearchResponse;
449
+ const installed = new Set(readInstalled(this.dir).map((p) => p.name));
450
+ const hits: ExtensionSearchHit[] = [];
451
+ for (const obj of data.objects ?? []) {
452
+ const p = obj.package;
453
+ if (!p?.name || !p.version || !(p.keywords ?? []).includes(keyword)) continue;
454
+ hits.push({
455
+ name: p.name,
456
+ version: p.version,
457
+ description: p.description ?? '',
458
+ kind,
459
+ ...(p.date ? { date: p.date } : {}),
460
+ ...(p.publisher?.username ? { publisher: p.publisher.username } : {}),
461
+ downloads: obj.downloads?.monthly ?? 0,
462
+ links: {
463
+ ...(p.links?.npm ? { npm: p.links.npm } : {}),
464
+ ...(p.links?.repository ? { repository: p.links.repository } : {}),
465
+ ...(p.links?.homepage ? { homepage: p.links.homepage } : {}),
466
+ },
467
+ installed: installed.has(p.name),
468
+ });
469
+ }
470
+ return hits;
471
+ }
472
+
473
+ async install(target: ExtensionInstallTarget): Promise<string> {
474
+ const spec = this.installSpec(target);
475
+ if (!existsSync(this.dir)) mkdirSync(this.dir, { recursive: true });
476
+ const pkgFile = join(this.dir, 'package.json');
477
+ if (!existsSync(pkgFile)) {
478
+ writeFileSync(pkgFile, JSON.stringify({ name: 'cortico-extensions', private: true, dependencies: {} }, null, 2) + '\n', 'utf8');
479
+ }
480
+ const output = await this.exclusive(['add', spec, '--ignore-workspace']);
481
+ return `已安装 ${spec}。重启进程后加载。\n${output}`;
482
+ }
483
+
484
+ async uninstall(name: string): Promise<string> {
485
+ if (!PACKAGE_NAME.test(name)) throw new Error(`不是合法的包名: ${name}`);
486
+ if (!readInstalled(this.dir).some((p) => p.name === name)) throw new Error(`没有安装这个包: ${name}`);
487
+ const output = await this.exclusive(['remove', name, '--ignore-workspace']);
488
+ return `已卸载 ${name}。重启进程后生效。\n${output}`;
489
+ }
490
+
491
+ private installSpec(target: ExtensionInstallTarget): string {
492
+ if ('path' in target) {
493
+ const raw = target.path.trim();
494
+ if (!LOCAL_PATH.test(raw)) throw new Error('路径含有不接受的字符。');
495
+ const abs = isAbsolute(raw) ? raw : resolve(this.repoRoot, raw);
496
+ if (!existsSync(abs) || !statSync(abs).isDirectory()) throw new Error(`目录不存在: ${abs}`);
497
+ if (!existsSync(join(abs, 'package.json'))) throw new Error(`目录里没有 package.json: ${abs}`);
498
+ return abs;
499
+ }
500
+ const name = target.name.trim();
501
+ if (!PACKAGE_NAME.test(name)) throw new Error(`不是合法的 npm 包名: ${name}`);
502
+ const version = target.version?.trim() ?? '';
503
+ if (version && !VERSION_SPEC.test(version)) throw new Error(`不是合法的版本: ${version}`);
504
+ return version ? `${name}@${version}` : name;
505
+ }
506
+
507
+ private async exclusive(args: string[]): Promise<string> {
508
+ if (this.busy) throw new Error('已有一个安装 / 卸载在进行,等它结束。');
509
+ this.busy = true;
510
+ try {
511
+ const { code, output } = await this.run(args, this.dir);
512
+ const tail = output.trim().split('\n').slice(-20).join('\n');
513
+ if (code !== 0) throw new Error(`pnpm ${args[0]} 退出码 ${code}:\n${tail}`);
514
+ return tail;
515
+ } finally {
516
+ this.busy = false;
517
+ }
518
+ }
519
+ }