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,625 @@
1
+ /**
2
+ * 内置 llm-settings 面板,通过 ctx.invoke 使用声明方的数据面。
3
+ * 每一格在 change 时写回端点条目,模块自有的段落挂在 `instance` 插槽里。
4
+ * reasoningTiers 非空时限制为所列档位;为空时接受开放字符串。空值使用端点默认,none 关闭推理,其余写入 reasoningEffort。
5
+ */
6
+ import type {
7
+ ConsoleDisablable,
8
+ ConsoleFormat,
9
+ ConsolePanelContext,
10
+ ConsolePanel,
11
+ ConsoleUi,
12
+ Disposable,
13
+ } from '../../../../shared/client-panel.ts';
14
+ import { pricingEditor, type ModelQuote } from './pricing-panel.ts';
15
+ import { panel } from './strings.ts';
16
+ import { configField, type ConfigGroupEntry } from '../../../features/config/view.ts';
17
+
18
+ /** 每个端点实例的模型与推理配置。 */
19
+ interface Spec {
20
+ model: string;
21
+ thinking: boolean;
22
+ reasoningEffort?: string;
23
+ temperature?: number;
24
+ maxTokens?: number;
25
+ contextWindow?: number;
26
+ }
27
+ interface Tier {
28
+ id: string;
29
+ label: string;
30
+ thinking: boolean;
31
+ effort?: string;
32
+ note?: string;
33
+ }
34
+ interface Entry {
35
+ kind: string;
36
+ baseUrl: string;
37
+ secret?: string;
38
+ multimodal?: boolean;
39
+ spec?: Spec;
40
+ pricing?: unknown[];
41
+ serviceTier?: string;
42
+ options?: Record<string, unknown>;
43
+ }
44
+ interface Instance {
45
+ name: string;
46
+ entry: Entry;
47
+ quotes: ModelQuote[];
48
+ secretConfigured: 'env' | 'file' | 'none';
49
+ config: ConfigGroupEntry[];
50
+ }
51
+ interface SettingsState {
52
+ active: string;
53
+ reasoningTiers: Tier[];
54
+ serviceTiers: Array<{ id: string; label: string; note?: string }>;
55
+ temperatureNote?: string;
56
+ baseUrlSuggestions?: string[];
57
+ effortSuggestions?: string[];
58
+ instances: Instance[];
59
+ }
60
+ interface ModelEntry {
61
+ id: string;
62
+ contextWindow?: number;
63
+ }
64
+ interface ProbeResult {
65
+ ok: boolean;
66
+ status: number | null;
67
+ elapsedMs: number;
68
+ model?: string;
69
+ usage?: {
70
+ input: number | null;
71
+ cachedInput: number | null;
72
+ output: number | null;
73
+ reasoning: number | null;
74
+ };
75
+ encryptedReasoning?: boolean;
76
+ charges?: Array<{ currency: string; amount: number | null }>;
77
+ error?: string;
78
+ hint?: string;
79
+ }
80
+
81
+ const DEFAULT_ENDPOINT_PATH = '/responses';
82
+
83
+ /** 当前端点的模块面板插槽。 */
84
+ const MODULE_SLOT = 'instance';
85
+
86
+ /** 开放模式的 effort 格 ↔ spec 的 thinking/reasoningEffort。 */
87
+ function effortOf(spec: Spec): string {
88
+ if (!spec.thinking) return 'none';
89
+ return spec.reasoningEffort ?? '';
90
+ }
91
+ function applyEffort(spec: Spec, value: string): void {
92
+ const effort = value.trim();
93
+ spec.thinking = effort !== 'none';
94
+ if (effort === '' || effort === 'none') delete spec.reasoningEffort;
95
+ else spec.reasoningEffort = effort;
96
+ }
97
+
98
+ function datalist(ui: ConsoleUi, id: string, values: readonly string[]): HTMLDataListElement {
99
+ const list = ui.h('datalist');
100
+ list.id = id;
101
+ for (const value of values) {
102
+ const option = ui.h('option');
103
+ option.value = value;
104
+ list.append(option);
105
+ }
106
+ return list;
107
+ }
108
+
109
+ function probeCard(ui: ConsoleUi, S: typeof panel.zh, result: ProbeResult): HTMLElement {
110
+ const fmt: ConsoleFormat = ui.fmt;
111
+ const usage = result.usage;
112
+ const rows = [
113
+ {
114
+ k: S.probeStatus,
115
+ v: ui.pill(
116
+ `${result.ok ? S.probeOk : S.probeFailed}${result.status === null ? '' : ` · ${result.status}`}`,
117
+ result.ok ? 'on' : 'off',
118
+ ),
119
+ },
120
+ { k: S.probeLatency, v: fmt.duration(result.elapsedMs) },
121
+ { k: S.probeModel, v: result.model ?? '' },
122
+ {
123
+ k: S.probeUsage,
124
+ v: usage
125
+ ? S.probeUsageLine(
126
+ fmt.count(usage.input),
127
+ fmt.count(usage.cachedInput),
128
+ fmt.count(usage.output),
129
+ fmt.count(usage.reasoning),
130
+ )
131
+ : '',
132
+ },
133
+ {
134
+ k: S.probeEncrypted,
135
+ v: result.encryptedReasoning === undefined ? '' : result.encryptedReasoning ? S.yes : S.no,
136
+ },
137
+ {
138
+ k: S.probeCost,
139
+ v: (result.charges ?? [])
140
+ .map((charge) => fmt.money(charge.amount, charge.currency))
141
+ .join(' · '),
142
+ },
143
+ ];
144
+ const sheet = ui.sheet({ title: S.probeTitle });
145
+ sheet.body.append(ui.kv(rows));
146
+ if (!result.ok) {
147
+ if (result.error) sheet.body.append(ui.msgline(result.error, true));
148
+ if (result.hint) sheet.body.append(ui.msgline(result.hint, true));
149
+ }
150
+ return sheet.el;
151
+ }
152
+
153
+ export const llmSettingsPanel: ConsolePanel = {
154
+ mount: async (ctx: ConsolePanelContext) => {
155
+ const { ui, root } = ctx;
156
+ const S = ctx.language === 'en' ? panel.en : panel.zh;
157
+ // `<datalist>` 靠全局 id 绑定;每次挂载一个前缀,同页两份面板互不串。
158
+ const uid = `llm-${Math.random().toString(36).slice(2, 8)}`;
159
+ let selected = '';
160
+ /** 模块段落的句柄,重画前先结束上一批。 */
161
+ let sections: Disposable | null = null;
162
+ /** 仅最新一次 load 可以更新 DOM。 */
163
+ let generation = 0;
164
+ const report = ui.msgline();
165
+ const body = ui.h('div');
166
+ root.append(body, report);
167
+ /** 写一格:落盘后只更新状态行与页头徽标,表单留在原处。 */
168
+ const patch = async (fields: Record<string, unknown>): Promise<boolean> => {
169
+ try {
170
+ await ctx.invoke('save', [{ name: selected, ...fields }]);
171
+ } catch (error) {
172
+ report.textContent = String(error);
173
+ return false;
174
+ }
175
+ report.textContent = S.saved;
176
+ await ctx.refresh();
177
+ return true;
178
+ };
179
+ /** 改了实例集合或当前实例的动作:落盘后整页重画。 */
180
+ const action = async (method: string, args: unknown[], done = S.saved): Promise<boolean> => {
181
+ try {
182
+ await ctx.invoke(method, args);
183
+ } catch (error) {
184
+ report.textContent = String(error);
185
+ return false;
186
+ }
187
+ report.textContent = done;
188
+ await ctx.refresh();
189
+ await load();
190
+ return true;
191
+ };
192
+ /** 读类方法(取模型、探测):不重画;失败把服务端措辞放进 report。 */
193
+ const query = async <T>(method: string, args: unknown[], ...locked: ConsoleDisablable[]) => {
194
+ const release = ui.disable(...locked);
195
+ try {
196
+ return await ctx.invoke<T>(method, args);
197
+ } catch (error) {
198
+ report.textContent = String(error);
199
+ return null;
200
+ } finally {
201
+ release.dispose();
202
+ }
203
+ };
204
+ async function load() {
205
+ const gen = ++generation;
206
+ const state = await ctx.invoke<SettingsState>('state');
207
+ if (ctx.signal.aborted || gen !== generation) return;
208
+ sections?.dispose();
209
+ sections = null;
210
+ body.replaceChildren();
211
+ if (!state.instances.some((instance) => instance.name === selected))
212
+ selected =
213
+ state.instances.find((instance) => instance.name === state.active)?.name ??
214
+ state.instances[0]?.name ??
215
+ '';
216
+ const open = state.reasoningTiers.length === 0;
217
+ const top = ui.sheet({
218
+ title: S.instancesTitle,
219
+ desc: S.instancesDescription,
220
+ });
221
+ const selector = ui.select({
222
+ value: selected,
223
+ options: state.instances.map((instance) => ({
224
+ value: instance.name,
225
+ label: instance.name + (instance.name === state.active ? S.activeSuffix : ''),
226
+ })),
227
+ onChange: (name) => {
228
+ selected = name;
229
+ void load();
230
+ },
231
+ });
232
+ top.body.append(ui.field(S.instanceField, selector));
233
+ const createName = ui.input({ placeholder: S.newInstanceName });
234
+ // 候选就是几个已知能用的地址,选不选随意:这一格收任何 URL。
235
+ const createUrl = ui.input({ placeholder: S.newInstanceUrl, cls: 'mono' });
236
+ createUrl.setAttribute('list', `${uid}-baseurls`);
237
+ const createRow = ui.rowbar();
238
+ createRow.append(
239
+ createName,
240
+ createUrl,
241
+ datalist(ui, `${uid}-baseurls`, state.baseUrlSuggestions ?? []),
242
+ ui.button(S.addInstance, {
243
+ onClick: async () => {
244
+ const name = createName.value.trim();
245
+ const before = selected;
246
+ selected = name;
247
+ if (!(await action('create', [{ name, baseUrl: createUrl.value.trim() }])))
248
+ selected = before;
249
+ },
250
+ }),
251
+ );
252
+ top.body.append(createRow);
253
+ body.append(top.el);
254
+ const current = state.instances.find((instance) => instance.name === selected);
255
+ if (!current) return;
256
+ const entry = current.entry;
257
+ const spec: Spec = structuredClone(entry.spec) ?? { model: '', thinking: open };
258
+ const saveSpec = () => void patch({ spec });
259
+
260
+ function configInput(suffix: string, label: string, changed?: (value: unknown) => void | Promise<void>): HTMLElement {
261
+ const path = `providers.${current!.name}.${suffix}`;
262
+ const { group, values } = current!.config.find(({ group }) => path in group.schema.properties!)!;
263
+ const field = configField(ui, group.schema.properties![path], values?.[path], () => {
264
+ void (async () => {
265
+ const raw = field.read!();
266
+ const value = typeof raw === 'string' ? raw.trim() : raw;
267
+ try {
268
+ await ctx.setConfig(group.id, { [path]: value });
269
+ report.textContent = S.saved;
270
+ await ctx.refresh();
271
+ await changed?.(value);
272
+ } catch (error) {
273
+ report.textContent = String(error);
274
+ }
275
+ })();
276
+ }, ctx.signal);
277
+ (field.node.querySelector('input, select, textarea') ?? field.node).setAttribute('aria-label', label);
278
+ return field.node;
279
+ }
280
+
281
+ // ---- 连接 ----
282
+ const connection = ui.sheet({ title: S.connectionTitle, desc: S.connectionDescription });
283
+ const options = entry.options ?? {};
284
+ const endpointPath =
285
+ typeof options.endpointPath === 'string' ? options.endpointPath : DEFAULT_ENDPOINT_PATH;
286
+ const jsonText = (value: unknown) =>
287
+ value === undefined ? '' : JSON.stringify(value, null, 2);
288
+ const baseUrlInput = configInput('baseUrl', S.baseUrl, connectionChanged);
289
+ const secretNameInput = configInput('secret', S.secretName, connectionChanged);
290
+ (secretNameInput as HTMLInputElement).placeholder = S.secretNamePlaceholder;
291
+ connection.body.append(
292
+ ui.field(S.baseUrl, baseUrlInput),
293
+ ui.field(S.secretName, secretNameInput),
294
+ );
295
+ {
296
+ const row = ui.rowbar();
297
+ const status = ui.pill(
298
+ S.secretSource[current.secretConfigured],
299
+ current.secretConfigured === 'none' ? 'off' : 'on',
300
+ );
301
+ const value = ui.input({ type: 'password', placeholder: S.secretValuePlaceholder });
302
+ value.setAttribute('aria-label', S.secretValue);
303
+ row.append(status, value, ui.button(S.saveSecret, {
304
+ onClick: () =>
305
+ void action('setSecret', [{ name: selected, value: value.value }], S.secretSaved),
306
+ }));
307
+ connection.body.append(ui.field(S.secretStatus, row));
308
+ if (!entry.secret) connection.body.append(ui.msgline(S.secretNameFirst));
309
+ }
310
+ connection.body.append(ui.field(S.multimodal, configInput('multimodal', S.multimodal)));
311
+ let extraHeaders: HTMLTextAreaElement | null = null;
312
+ let extraBody: HTMLTextAreaElement | null = null;
313
+ let path: HTMLInputElement | null = null;
314
+ if (open) {
315
+ const pathKey = `providers.${current.name}.options.endpointPath`;
316
+ const hasPath = current.config.some(({ group }) => pathKey in group.schema.properties!);
317
+ path = hasPath ? configInput('options.endpointPath', S.endpointPath) as HTMLInputElement : null;
318
+ if (path) {
319
+ path.value = endpointPath;
320
+ path.placeholder = DEFAULT_ENDPOINT_PATH;
321
+ }
322
+ extraHeaders = ui.textarea({
323
+ rows: 3,
324
+ cls: 'mono',
325
+ value: jsonText(options.extraHeaders),
326
+ onChange: saveOptions,
327
+ });
328
+ extraHeaders.setAttribute('aria-label', S.extraHeaders);
329
+ extraBody = ui.textarea({
330
+ rows: 3,
331
+ cls: 'mono',
332
+ value: jsonText(options.extraBody),
333
+ onChange: saveOptions,
334
+ });
335
+ extraBody.setAttribute('aria-label', S.extraBody);
336
+ connection.body.append(
337
+ ui.section(S.advancedProtocolTitle, S.advancedProtocolDescription),
338
+ ...(path ? [ui.field(S.endpointPath, path)] : []),
339
+ ui.field(S.extraHeaders, extraHeaders),
340
+ ui.field(S.extraBody, extraBody),
341
+ );
342
+ }
343
+ body.append(connection.el);
344
+ /** 面板可编辑的 options 三键并回原 options;空的删掉。非法 JSON 抛错,由保存时报出。 */
345
+ const optionsPayload = (): Record<string, unknown> => {
346
+ const next = { ...options };
347
+ const object = (textarea: HTMLTextAreaElement, label: string): unknown => {
348
+ const text = textarea.value.trim();
349
+ if (!text) return undefined;
350
+ const parsed: unknown = JSON.parse(text);
351
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
352
+ throw new Error(S.jsonObjectRequired(label));
353
+ return parsed;
354
+ };
355
+ const edited: Record<string, unknown> = {
356
+ endpointPath: path?.value.trim() ?? options.endpointPath,
357
+ extraHeaders: object(extraHeaders!, S.extraHeaders),
358
+ extraBody: object(extraBody!, S.extraBody),
359
+ };
360
+ for (const [key, value] of Object.entries(edited)) {
361
+ if (value === undefined || value === '') delete next[key];
362
+ else next[key] = value;
363
+ }
364
+ return next;
365
+ };
366
+ function saveOptions(): void {
367
+ try {
368
+ void patch({ options: optionsPayload() });
369
+ } catch (error) {
370
+ report.textContent = String(error);
371
+ }
372
+ }
373
+
374
+ // ---- 模块自有段落(llama.cpp 的运行时与模型) ----
375
+ const slot = ui.h('div');
376
+ body.append(slot);
377
+
378
+ // ---- 模型档 ----
379
+ const card = ui.sheet({ title: S.specTitle, desc: S.specDescription });
380
+ let catalog: ModelEntry[] = [];
381
+ const modelHolder = ui.h('div');
382
+ const modelNote = ui.msgline();
383
+ /** 目录取到了就给选单,取不到就自由输入:两种都写同一格 spec.model。 */
384
+ function renderModelField(): void {
385
+ const control = catalog.length
386
+ ? ui.select({
387
+ value: spec.model,
388
+ options: [
389
+ ...(catalog.some((item) => item.id === spec.model)
390
+ ? []
391
+ : [{ value: spec.model, label: spec.model || S.selectModel }]),
392
+ ...catalog.map((item) => ({ value: item.id, label: item.id })),
393
+ ],
394
+ onChange: (value) => {
395
+ spec.model = value;
396
+ const known = catalog.find((item) => item.id === value)?.contextWindow;
397
+ if (known !== undefined) spec.contextWindow = known;
398
+ saveSpec();
399
+ renderModelField();
400
+ },
401
+ })
402
+ : ui.input({
403
+ value: spec.model,
404
+ placeholder: S.modelName,
405
+ onChange: (value) => {
406
+ spec.model = value;
407
+ saveSpec();
408
+ },
409
+ });
410
+ control.setAttribute('aria-label', S.modelAria);
411
+ const row = ui.rowbar();
412
+ const fetchModels = ui.button(S.fetchModels, {
413
+ size: 'sm',
414
+ onClick: () => void fetchCatalog(fetchModels),
415
+ });
416
+ row.append(control, fetchModels);
417
+ modelHolder.replaceChildren(ui.field(S.modelFieldLabel, row));
418
+ }
419
+ /** 取目录:成功换成选单并带出上下文窗口,失败把原因留在模型格下面。 */
420
+ async function fetchCatalog(...locked: ConsoleDisablable[]): Promise<void> {
421
+ const release = ui.disable(...locked);
422
+ try {
423
+ const result = await ctx.invoke<{ models: ModelEntry[] }>('models', [{ name: selected }]);
424
+ catalog = result.models;
425
+ modelNote.textContent = S.modelsFetched(catalog.length);
426
+ const known = catalog.find((item) => item.id === spec.model)?.contextWindow;
427
+ if (known !== undefined && known !== spec.contextWindow) {
428
+ spec.contextWindow = known;
429
+ contextWindowInput.value = String(known);
430
+ await patch({ spec });
431
+ }
432
+ } catch (error) {
433
+ catalog = [];
434
+ modelNote.textContent = S.modelListFailed(String(error));
435
+ } finally {
436
+ release.dispose();
437
+ }
438
+ if (!ctx.signal.aborted) renderModelField();
439
+ }
440
+ /** 地址或密钥改过就重取目录:换了端点,上一份模型表不作数。 */
441
+ async function connectionChanged(): Promise<void> {
442
+ catalog = [];
443
+ await fetchCatalog();
444
+ }
445
+
446
+ // 推理强度与温度
447
+ const effortSuggestions = datalist(ui, `${uid}-effort`, state.effortSuggestions ?? []);
448
+ let effortControl: HTMLInputElement | HTMLSelectElement;
449
+ if (open) {
450
+ const effort = ui.input({
451
+ value: effortOf(spec),
452
+ placeholder: S.effortPlaceholder,
453
+ onChange: (value) => {
454
+ applyEffort(spec, value);
455
+ saveSpec();
456
+ },
457
+ });
458
+ effort.setAttribute('list', `${uid}-effort`);
459
+ effort.setAttribute('aria-label', S.effortAria);
460
+ effortControl = effort;
461
+ } else {
462
+ const hit = state.reasoningTiers.find(
463
+ (tier) => tier.thinking === spec.thinking && tier.effort === spec.reasoningEffort,
464
+ );
465
+ const tiers = state.reasoningTiers.map((tier) => ({ value: tier.id, label: tier.label }));
466
+ if (!hit)
467
+ tiers.unshift({
468
+ value: '__unsupported',
469
+ label: S.unsupportedTier(
470
+ spec.reasoningEffort ?? (spec.thinking ? S.thinkingOn : S.thinkingOff),
471
+ ),
472
+ });
473
+ const tier = ui.select({
474
+ value: hit?.id ?? '__unsupported',
475
+ options: tiers,
476
+ onChange: (value) => {
477
+ const chosen = state.reasoningTiers.find((tier) => tier.id === value);
478
+ if (!chosen) return;
479
+ spec.thinking = chosen.thinking;
480
+ if (chosen.effort) spec.reasoningEffort = chosen.effort;
481
+ else delete spec.reasoningEffort;
482
+ saveSpec();
483
+ },
484
+ });
485
+ tier.setAttribute('aria-label', S.tierAria);
486
+ effortControl = tier;
487
+ }
488
+ const temperature = ui.input({
489
+ type: 'number',
490
+ placeholder: S.temperaturePlaceholder,
491
+ value: spec.temperature === undefined ? '' : String(spec.temperature),
492
+ onChange: (value) => {
493
+ if (value.trim() === '') delete spec.temperature;
494
+ else spec.temperature = Number(value);
495
+ saveSpec();
496
+ },
497
+ });
498
+ temperature.min = '0';
499
+ temperature.max = '2';
500
+ temperature.step = '0.1';
501
+ temperature.setAttribute('aria-label', S.temperatureAria);
502
+
503
+ const controlRow = ui.rowbar();
504
+ controlRow.append(
505
+ ui.field(open ? S.effortFieldLabel : S.tierAria, effortControl),
506
+ effortSuggestions,
507
+ ui.field(S.temperatureFieldLabel, temperature),
508
+ );
509
+
510
+ // Token 与上下文限制
511
+ const tokenRow = ui.rowbar();
512
+ const numbers: Partial<Record<'maxTokens' | 'contextWindow', HTMLInputElement>> = {};
513
+ for (const [key, label] of [
514
+ ['maxTokens', S.maxTokens],
515
+ ['contextWindow', S.contextWindow],
516
+ ] as const) {
517
+ const input = ui.input({
518
+ type: 'number',
519
+ placeholder: label,
520
+ value: spec[key] === undefined ? '' : String(spec[key]),
521
+ onChange: (value) => {
522
+ if (value.trim() === '') delete spec[key];
523
+ else spec[key] = Number(value);
524
+ saveSpec();
525
+ },
526
+ });
527
+ input.min = '1';
528
+ input.step = '1';
529
+ input.setAttribute('aria-label', label);
530
+ numbers[key] = input;
531
+ tokenRow.append(ui.field(label, input));
532
+ }
533
+ const contextWindowInput = numbers.contextWindow!;
534
+ renderModelField();
535
+ card.body.append(modelHolder, modelNote, controlRow, tokenRow);
536
+ for (const note of new Set(state.reasoningTiers.map((tier) => tier.note).filter(Boolean)))
537
+ card.body.append(ui.msgline(note!));
538
+ if (state.temperatureNote) card.body.append(ui.msgline(state.temperatureNote));
539
+ if (state.serviceTiers.length)
540
+ card.body.append(
541
+ ui.field(
542
+ S.serviceTier,
543
+ ui.select({
544
+ value: entry.serviceTier ?? '',
545
+ options: [
546
+ { value: '', label: S.serverDefault },
547
+ ...state.serviceTiers.map((tier) => ({ value: tier.id, label: tier.label })),
548
+ ],
549
+ onChange: (value) => void patch({ serviceTier: value }),
550
+ }),
551
+ ),
552
+ );
553
+ for (const tier of state.serviceTiers) if (tier.note) card.body.append(ui.msgline(tier.note));
554
+ body.append(card.el);
555
+
556
+ // ---- 报价 ----
557
+ const prices = pricingEditor(
558
+ ui,
559
+ entry.pricing ?? [],
560
+ current.quotes,
561
+ (pricing) => void patch({ pricing }),
562
+ ctx.language,
563
+ );
564
+ body.append(prices.el);
565
+
566
+ // ---- 动作 ----
567
+ const buttons = ui.rowbar();
568
+ const probeBox = ui.h('div');
569
+ const duplicateHolder = ui.h('div');
570
+ duplicateHolder.hidden = true;
571
+ const duplicateRow = ui.rowbar();
572
+ duplicateHolder.append(duplicateRow);
573
+ const duplicateName = ui.input({ placeholder: S.duplicateName });
574
+ duplicateName.setAttribute('aria-label', S.duplicateName);
575
+ duplicateRow.append(
576
+ duplicateName,
577
+ ui.button(S.duplicateConfirm, {
578
+ size: 'sm',
579
+ onClick: async () => {
580
+ const as = duplicateName.value.trim();
581
+ const before = selected;
582
+ selected = as;
583
+ if (!(await action('duplicate', [{ name: before, as }], S.duplicated))) selected = before;
584
+ },
585
+ }),
586
+ );
587
+ const probe = ui.button(S.probe, {
588
+ onClick: async () => {
589
+ const result = await query<ProbeResult>('probe', [{ name: selected }], probe);
590
+ if (!result || ctx.signal.aborted) return;
591
+ probeBox.replaceChildren(probeCard(ui, S, result));
592
+ },
593
+ });
594
+ buttons.append(
595
+ ui.button(S.activate, {
596
+ variant: 'primary',
597
+ onClick: () => void action('activate', [{ name: selected }]),
598
+ }),
599
+ probe,
600
+ ui.button(S.duplicate, {
601
+ onClick: () => {
602
+ duplicateHolder.hidden = !duplicateHolder.hidden;
603
+ if (!duplicateHolder.hidden) duplicateName.focus();
604
+ },
605
+ }),
606
+ ui.button(S.delete, {
607
+ variant: 'danger',
608
+ onClick: async () => {
609
+ const ok = await ui.confirm({
610
+ title: S.deleteConfirmTitle(selected),
611
+ body: S.deleteConfirmBody,
612
+ danger: true,
613
+ });
614
+ if (ok) void action('delete', [{ name: selected }], S.deleted);
615
+ },
616
+ }),
617
+ );
618
+ body.append(buttons, duplicateHolder, probeBox);
619
+ const mounted = await ctx.mountSlot(MODULE_SLOT, slot, { instance: selected });
620
+ if (gen === generation) sections = mounted;
621
+ else mounted.dispose();
622
+ }
623
+ await load();
624
+ },
625
+ };