cortico 0.1.0 → 0.1.2

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 (112) hide show
  1. package/README.md +7 -14
  2. package/package.json +1 -3
  3. package/src/boot.ts +18 -0
  4. package/src/bot.ts +41 -6
  5. package/src/core/README.md +4 -6
  6. package/src/core/blobs.ts +23 -2
  7. package/src/core/config-schema.ts +1 -1
  8. package/src/core/config.ts +2 -16
  9. package/src/core/core.ts +15 -5
  10. package/src/core/loop.ts +55 -27
  11. package/src/core/session.ts +3 -2
  12. package/src/core/timers.ts +8 -6
  13. package/src/core/types.ts +19 -3
  14. package/src/core/util.ts +17 -1
  15. package/src/deploy.ts +4 -1
  16. package/src/extensions/README.md +6 -4
  17. package/src/extensions/dry-mount.ts +23 -2
  18. package/src/extensions/manifest.ts +21 -11
  19. package/src/extensions.ts +147 -23
  20. package/src/launcher.ts +23 -9
  21. package/src/protocol/open-responses/context-log.ts +37 -9
  22. package/src/providers/README.md +32 -8
  23. package/src/providers/base.ts +3 -1
  24. package/src/providers/configuration.ts +2 -1
  25. package/src/providers/console/hub.ts +270 -0
  26. package/src/providers/console/settings.ts +8 -18
  27. package/src/providers/console/types.ts +5 -0
  28. package/src/providers/hub-api.ts +3 -0
  29. package/src/providers/llamacpp/config.ts +8 -6
  30. package/src/providers/llamacpp/console/runtime-panel.ts +5 -1
  31. package/src/providers/llamacpp/console/server.ts +3 -1
  32. package/src/providers/llamacpp/index.ts +3 -1
  33. package/src/providers/llamacpp/native.ts +10 -5
  34. package/src/providers/llamacpp/strings.ts +6 -6
  35. package/src/providers/name.ts +8 -0
  36. package/src/providers/openai-responses-compat/config.ts +13 -0
  37. package/src/providers/openai-responses-compat/console/client.ts +5 -0
  38. package/src/providers/openai-responses-compat/console/reasoning-panel.ts +84 -0
  39. package/src/providers/openai-responses-compat/console/server.ts +127 -0
  40. package/src/providers/openai-responses-compat/index.ts +38 -13
  41. package/src/providers/openai-responses-compat/native.ts +9 -4
  42. package/src/providers/openai-responses-compat/strings.ts +62 -1
  43. package/src/providers/registry.ts +5 -0
  44. package/src/providers/transport/responses-input.ts +59 -10
  45. package/src/web/README.md +11 -6
  46. package/src/web/auth.ts +80 -0
  47. package/src/web/client/console-pages/builtins/llm-settings/panel.ts +5 -4
  48. package/src/web/client/console-pages/builtins/llm-settings/pricing-panel.ts +22 -8
  49. package/src/web/client/console-pages/builtins/llm-settings/strings.ts +6 -8
  50. package/src/web/client/console-pages/host.ts +24 -7
  51. package/src/web/client/core/api.ts +5 -1
  52. package/src/web/client/core/router.ts +15 -0
  53. package/src/web/client/features/core/strings.ts +2 -2
  54. package/src/web/client/features/extensions/index.ts +276 -41
  55. package/src/web/client/features/extensions/strings.ts +84 -10
  56. package/src/web/client/features/feature.ts +2 -2
  57. package/src/web/client/features/live/diagnostics.ts +32 -0
  58. package/src/web/client/features/live/index.ts +34 -11
  59. package/src/web/client/features/live/onboarding.ts +4 -1
  60. package/src/web/client/features/live/protocol.ts +1 -0
  61. package/src/web/client/features/live/strings.ts +20 -14
  62. package/src/web/client/features/live/timeline.ts +2 -1
  63. package/src/web/client/features/providers/detail.ts +262 -0
  64. package/src/web/client/features/providers/drafts.ts +23 -0
  65. package/src/web/client/features/providers/index.ts +173 -87
  66. package/src/web/client/features/providers/strings.ts +44 -17
  67. package/src/web/client/features/providers/types.ts +15 -0
  68. package/src/web/client/features/settings/general.ts +13 -0
  69. package/src/web/client/features/settings/index.ts +1 -0
  70. package/src/web/client/features/settings/strings.ts +6 -0
  71. package/src/web/client/features/worlds/index.ts +1 -0
  72. package/src/web/client/main.ts +4 -0
  73. package/src/web/client/shell/index.ts +28 -48
  74. package/src/web/client/shell/strings.ts +0 -22
  75. package/src/web/client/ui/icons.ts +14 -1
  76. package/src/web/client/ui/prompt-input.tsx +17 -5
  77. package/src/web/client/ui/strings.ts +0 -2
  78. package/src/web/console-pages.ts +1 -1
  79. package/src/web/diagnostics.ts +133 -0
  80. package/src/web/public/login.html +67 -0
  81. package/src/web/public/styles.css +143 -26
  82. package/src/web/server.ts +233 -19
  83. package/src/web/shared/client-panel.ts +4 -1
  84. package/src/web/shared/console-protocol.ts +4 -1
  85. package/src/worlds/bilibili/README.md +1 -1
  86. package/src/worlds/bilibili/overlay/server.ts +4 -2
  87. package/src/worlds/minecraft/ADAPT.md +66 -0
  88. package/src/worlds/minecraft/README.md +69 -11
  89. package/src/worlds/minecraft/cell-facts.ts +226 -0
  90. package/src/worlds/minecraft/chests.ts +5 -0
  91. package/src/worlds/minecraft/containers.ts +325 -0
  92. package/src/worlds/minecraft/entity-facts.ts +31 -5
  93. package/src/worlds/minecraft/executor.ts +283 -10348
  94. package/src/worlds/minecraft/inventory.ts +268 -0
  95. package/src/worlds/minecraft/melee.ts +419 -0
  96. package/src/worlds/minecraft/mineflayer-fixes.ts +55 -1
  97. package/src/worlds/minecraft/placed-ledger.ts +152 -0
  98. package/src/worlds/minecraft/placement.ts +1038 -0
  99. package/src/worlds/minecraft/receipt.ts +340 -0
  100. package/src/worlds/minecraft/skill-context.ts +358 -0
  101. package/src/worlds/minecraft/skills-build.ts +1169 -0
  102. package/src/worlds/minecraft/skills-container.ts +1343 -0
  103. package/src/worlds/minecraft/skills-craft.ts +333 -0
  104. package/src/worlds/minecraft/skills-dig.ts +624 -0
  105. package/src/worlds/minecraft/skills-gather.ts +1230 -0
  106. package/src/worlds/minecraft/skills-interact.ts +1559 -0
  107. package/src/worlds/minecraft/tools.ts +331 -0
  108. package/src/worlds/minecraft/travel.ts +763 -0
  109. package/src/worlds/minecraft/until.ts +75 -0
  110. package/src/worlds/qq/normalize.ts +14 -0
  111. package/src/worlds/qq/world.ts +53 -7
  112. package/src/worlds/terminal/world.ts +5 -3
@@ -1,100 +1,186 @@
1
- /**
2
- * 「语言模型」页 —— 模型供应模块的入口。
3
- *
4
- * 左侧次级菜单列出 manifest 里 kind 为 `llm` 的页(每个供应模块一条),右侧由
5
- * 嵌入的控制台页宿主渲染选中模块的面板:实例与模型、授权、托管、配置。这一页不认识
6
- * 任何具体模块——名字、灯、徽标与面板全部来自 manifest。
7
- *
8
- * 路由 `#/providers/<pageId>/<panelId>`:第二段选模块,第三段选面板;缺省取第一个
9
- * 模块及其第一个面板。面板页签由宿主渲染并指向同一前缀,所以切换留在本页内。
10
- */
11
-
12
- import { PROVIDERS_LAMP_ID } from '../../../shared/console-protocol.ts';
13
- import { lampRow, paintLamps, subscribeLamps } from '../../ui/lamp.ts';
14
1
  import { pageIntro } from '../../ui/page.ts';
15
- import type { Route } from '../../core/router.ts';
2
+ import { icon } from '../../ui/icons.ts';
3
+ import { NEW_DRAFT_ID, providerDrafts } from './drafts.ts';
16
4
  import type { FeatureContext, FrameworkFeature } from '../feature.ts';
5
+ import { get, post } from '../../core/api.ts';
6
+ import { LANGUAGE } from '../../core/language.ts';
7
+ import { panel } from '../../console-pages/builtins/llm-settings/strings.ts';
8
+ import { probeCard, type ProbeResult } from '../../console-pages/builtins/llm-settings/panel.ts';
17
9
  import { S } from './strings.ts';
18
-
19
- const PROVIDERS_ROUTE = 'providers';
20
- /** 这一页只列供应模块;人格与 IO 各有自己的入口。 */
21
- const PROVIDER_KIND = 'llm';
10
+ import { connectionPath, type HubState, type Connection, type Module, type Detail, type Editing } from './types.ts';
11
+ import { mountDetail, type DetailController } from './detail.ts';
22
12
 
23
13
  export async function mountProviders(ctx: FeatureContext): Promise<void> {
24
14
  const { ui, root } = ctx;
15
+ root.append(pageIntro(ui, S.pageTitle));
16
+ const report = ui.msgline();
17
+ const layout = ui.h('div', 'connection-hub');
18
+ const index = ui.h('div', 'connection-index');
19
+ const cards = ui.h('div', 'connection-cards');
20
+ const detailRoot = ui.h('div', 'connection-detail');
21
+ layout.append(index, detailRoot); root.append(report, layout);
22
+ const opts = { signal: ctx.signal };
23
+ let state = await get<HubState>('/api/providers', opts);
24
+ state.providers.sort((a, b) => Number(b.name === state.active) - Number(a.name === state.active));
25
+ const modules = await get<Module[]>('/api/provider-modules', opts);
26
+ if (ctx.signal.aborted) return;
27
+ const drafts = providerDrafts(root.ownerDocument.defaultView!.localStorage, state.scope);
28
+ let selected = '';
29
+ let newDraft: Editing | null = drafts.get(NEW_DRAFT_ID);
30
+ const invalid = new Set<string>();
31
+ let controller: DetailController | null = null;
32
+ let renderId = 0;
33
+ interface CardNode {
34
+ el: HTMLElement; title: HTMLElement; model: HTMLElement; url: HTMLElement; status: HTMLElement; secondary: HTMLElement;
35
+ kind: HTMLElement; activate: HTMLButtonElement; erase: HTMLButtonElement; probe: HTMLButtonElement; probePanel: HTMLElement; probeBody: HTMLElement;
36
+ }
37
+ const nodes = new Map<string, CardNode>();
38
+ const rows = new Map<string, Connection>();
25
39
  const doc = root.ownerDocument;
26
- const intro = pageIntro(ui, S.pageTitle);
27
- const layout = ui.h('div', 'settings-layout providerhub');
28
- const index = ui.h('nav', 'settings-index');
29
- index.setAttribute('aria-label', S.modulesAria);
30
- index.setAttribute('role', 'tablist');
31
- const content = ui.h('div', 'settings-content');
32
- layout.append(index, content);
33
- root.append(intro, layout);
34
-
35
- if (!ctx.consolePageHost) {
36
- content.appendChild(ui.placeholder(S.needHost));
37
- return;
40
+ const run = (work: () => Promise<unknown>) => { void work().catch(error => { if (!ctx.signal.aborted) report.textContent = String(error); }); };
41
+ // 同时只有一张卡处在「确认删除」态。
42
+ let armed: (() => void) | null = null;
43
+ const disarm = () => { armed?.(); armed = null; };
44
+ const fallback = () => state.providers.find(item => item.name === state.active)?.name || state.providers[0]?.name || '';
45
+ function paint() {
46
+ report.textContent = state.active && !state.providers.some(item => item.name === state.active) ? S.missing + state.active : '';
47
+ const all: Array<Connection | { name: string; moduleTitle: string; model: string | null; baseUrl: string; readiness: { state: string } }> = [...state.providers];
48
+ if (newDraft) all.unshift({ name: NEW_DRAFT_ID, moduleTitle: moduleName(newDraft.entry.kind), model: newDraft.entry.spec?.model ?? null, baseUrl: newDraft.entry.baseUrl, readiness: { state: 'draft' } });
49
+ for (const [name, node] of nodes) if (!all.some(item => item.name === name)) { node.el.remove(); nodes.delete(name); }
50
+ for (const item of all) {
51
+ const identity = item.name;
52
+ let node = nodes.get(identity);
53
+ if (!node) {
54
+ const el = ui.h('article', 'connection-card'); el.dataset.provider = identity;
55
+ const title = ui.h('div', 'connection-name'); const kind = ui.h('div', 'connection-kind');
56
+ const model = ui.h('div', 'connection-model'); const url = ui.h('div', 'connection-url');
57
+ const status = ui.h('div', 'connection-status'); const secondary = ui.h('div', 'connection-secondary');
58
+ const actions = ui.rowbar();
59
+ const configure = ui.button(S.configure, { size: 'sm', onClick: () => run(() => select(identity)) });
60
+ const activate = ui.button(S.activate, { size: 'sm', onClick: () => run(async () => {
61
+ activate.disabled = true;
62
+ try { await post(connectionPath(identity) + '/activate', {}, opts); state.active = identity; paint(); }
63
+ finally { activate.disabled = false; }
64
+ }) });
65
+ const probePanel = ui.h('div', 'connection-probe');
66
+ const probeBody = ui.h('div', 'connection-probe-body');
67
+ probePanel.append(probeBody);
68
+ const probe = ui.button(S.probe, { size: 'sm', onClick: () => run(() => runProbe(identity)) });
69
+ probe.classList.add('connection-probe-btn'); probe.append(ui.h('span', 'connection-spin'));
70
+ const erase = eraseButton(identity);
71
+ actions.append(configure, probe, activate); el.append(erase, title, kind, model, url, status, secondary, actions, probePanel);
72
+ el.addEventListener('click', event => { if (!(event.target as Element).closest('button')) run(() => select(identity)); }, opts);
73
+ if (identity === NEW_DRAFT_ID) cards.prepend(el); else cards.append(el);
74
+ node = { el, title, kind, model, url, status, secondary, activate, erase, probe, probePanel, probeBody }; nodes.set(identity, node);
75
+ }
76
+ const active = identity === state.active;
77
+ node.el.classList.toggle('is-active', active); node.el.classList.toggle('is-selected', identity === selected);
78
+ node.title.textContent = identity === NEW_DRAFT_ID ? newDraft?.name || S.newName : identity;
79
+ node.title.title = node.title.textContent;
80
+ node.kind.textContent = item.moduleTitle; node.kind.title = item.moduleTitle;
81
+ node.model.textContent = item.model || '—'; node.url.textContent = item.baseUrl || '—';
82
+ node.model.title = item.model ?? ''; node.url.title = item.baseUrl ?? '';
83
+ const readiness = invalid.has(identity) && identity !== NEW_DRAFT_ID ? 'invalid' : item.readiness.state;
84
+ node.status.textContent = active ? S.active : S.readiness[readiness];
85
+ node.secondary.textContent = active && readiness !== 'ready' ? S.readiness[readiness] : identity !== NEW_DRAFT_ID && drafts.has(identity) ? S.readiness.draft : '';
86
+ node.activate.hidden = active || identity === NEW_DRAFT_ID;
87
+ node.activate.disabled = item.readiness.state !== 'ready';
88
+ node.probe.hidden = identity === NEW_DRAFT_ID;
89
+ if (identity === NEW_DRAFT_ID) rows.delete(identity); else rows.set(identity, item as Connection);
90
+ }
38
91
  }
39
- const host = ctx.consolePageHost({
40
- root: content,
41
- route: (pageId, panelId) => [PROVIDERS_ROUTE, pageId, panelId],
42
- });
43
- ctx.lifecycle.own({ dispose: () => host.unmount() });
44
- content.appendChild(ui.placeholder(S.loading));
45
- await host.load();
46
- if (ctx.signal.aborted) return;
47
-
48
- const providers = host.pages.filter((p) => p.kind === PROVIDER_KIND);
49
- if (!providers.length) {
50
- content.replaceChildren(ui.placeholder(S.none));
51
- return;
92
+ /** 删除按钮先伸成「确认删除」,第二次点击才动手;点别处或 Esc 收回。 */
93
+ function eraseButton(identity: string): HTMLButtonElement {
94
+ const el = ui.h('button', 'connection-erase') as HTMLButtonElement;
95
+ el.type = 'button';
96
+ const text = ui.h('span', 'connection-erase-text'); text.append(ui.h('span', '', S.eraseConfirm));
97
+ el.append(icon(doc, 'trash'), text);
98
+ const label = (armedNow: boolean) => { const name = armedNow ? S.eraseConfirm : S.remove; el.title = name; el.setAttribute('aria-label', name); };
99
+ label(false);
100
+ el.addEventListener('click', event => {
101
+ event.stopPropagation();
102
+ const wasArmed = el.classList.contains('is-armed');
103
+ disarm();
104
+ if (wasArmed) { run(() => remove(identity)); return; }
105
+ el.classList.add('is-armed'); label(true);
106
+ armed = () => { el.classList.remove('is-armed'); label(false); };
107
+ }, opts);
108
+ return el;
52
109
  }
53
-
54
- const jumps = new Map<string, HTMLButtonElement>();
55
- const lampNodes = new Map<string, HTMLSpanElement>();
56
- for (const p of providers) {
57
- const jump = ui.h('button', 'settings-jump providerhub-jump');
58
- jump.type = 'button';
59
- jump.setAttribute('role', 'tab');
60
- jump.append(ui.h('span', 'lbl', p.label || p.id));
61
- const lamps = lampRow(doc, p.lamps ?? []);
62
- jump.appendChild(lamps);
63
- lampNodes.set(p.id, lamps);
64
- jump.addEventListener('click', () => {
65
- try { ctx.router.navigate([PROVIDERS_ROUTE, p.id]); } catch (err) { ctx.onError(err); }
66
- }, { signal: ctx.signal });
67
- index.appendChild(jump);
68
- jumps.set(p.id, jump);
110
+ async function remove(identity: string): Promise<void> {
111
+ if (identity === NEW_DRAFT_ID) {
112
+ drafts.remove(NEW_DRAFT_ID); newDraft = null; invalid.delete(identity);
113
+ if (selected === NEW_DRAFT_ID) await select(fallback(), true); else paint();
114
+ return;
115
+ }
116
+ await post(connectionPath(identity) + '/delete', { expectedRevision: rows.get(identity)?.revision }, opts);
117
+ drafts.remove(identity); invalid.delete(identity);
118
+ await refresh();
119
+ if (selected === identity) await select(fallback(), true);
69
120
  }
70
- // 灯的活数据只改那几个点,不重排菜单(与左栏同一节拍)。
71
- ctx.lifecycle.own(subscribeLamps(doc, (lamps) => {
72
- for (const [id, el] of lampNodes) paintLamps(el, lamps[id] ?? []);
73
- }));
74
-
75
- const show = (route: Route): void => {
76
- const wanted = route.segments[1];
77
- const provider = providers.find((p) => p.id === wanted) ?? providers[0]!;
78
- for (const [id, jump] of jumps) {
79
- const on = id === provider.id;
80
- jump.classList.toggle('active', on);
81
- jump.setAttribute('aria-selected', String(on));
121
+ /** 探活结果挂在卡片下面,与模块页上的那份同一种呈现。 */
122
+ async function runProbe(identity: string): Promise<void> {
123
+ const node = nodes.get(identity);
124
+ if (!node || node.probe.disabled) return;
125
+ node.probe.disabled = true; node.probe.classList.add('is-busy');
126
+ try {
127
+ const result = await post<ProbeResult>(connectionPath(identity) + '/test', {}, opts);
128
+ if (ctx.signal.aborted) return;
129
+ const close = ui.button(S.probeClose, { size: 'sm', onClick: () => node.probePanel.classList.remove('is-open') });
130
+ node.probeBody.replaceChildren(probeCard(ui, LANGUAGE === 'en' ? panel.en : panel.zh, result), close);
131
+ node.probePanel.classList.add('is-open');
132
+ } finally { node.probe.disabled = false; node.probe.classList.remove('is-busy'); }
133
+ }
134
+ async function refresh() { state = await get<HubState>('/api/providers', opts); paint(); }
135
+ async function select(identity: string, force = false): Promise<void> {
136
+ if (!force && identity === selected) return;
137
+ if (!force && controller && !(await controller.leave())) return;
138
+ const gen = ++renderId;
139
+ controller?.dispose(); controller = null;
140
+ selected = identity; paint(); detailRoot.replaceChildren();
141
+ if (!identity) {
142
+ detailRoot.append(ui.h('h3', '', S.empty), ui.msgline(S.emptyHint), ui.button(S.create, { onClick: () => run(create) }));
143
+ return;
82
144
  }
83
- const panel = route.segments[2];
84
- void host.show(provider.id, typeof panel === 'string' && panel !== '' ? panel : undefined);
85
- };
86
- show(ctx.route);
87
- ctx.lifecycle.own(ctx.router.onChange((route) => {
88
- if (route.segments[0] !== PROVIDERS_ROUTE) return;
89
- show(route);
145
+ const saved = identity === NEW_DRAFT_ID ? null : await get<Detail>(connectionPath(identity), opts);
146
+ if (gen !== renderId || ctx.signal.aborted) return;
147
+ const detailView = ui.h('div'); detailRoot.replaceChildren(detailView);
148
+ const mounted = await mountDetail({ ctx, root: detailView, modules, saved, draft: identity === NEW_DRAFT_ID ? newDraft : drafts.get(identity),
149
+ discarded: () => { invalid.delete(identity); if (identity === NEW_DRAFT_ID) newDraft = drafts.get(NEW_DRAFT_ID); paint(); },
150
+ saveDraft: editing => { drafts.set(editing); paint(); },
151
+ changed: (editing, hasErrors) => { if (hasErrors) invalid.add(identity); else invalid.delete(identity); if (identity === NEW_DRAFT_ID) newDraft = editing; paint(); },
152
+ onSaved: async (name, show = true) => { drafts.remove(identity); invalid.delete(identity); if (identity === NEW_DRAFT_ID) newDraft = null; await refresh(); if (show) await select(name, true); },
153
+ cancelled: async () => { drafts.remove(identity); invalid.delete(identity); if (identity === NEW_DRAFT_ID) newDraft = null; await select(identity === NEW_DRAFT_ID ? state.providers.find(item => item.name === state.active)?.name || state.providers[0]?.name || '' : identity, true); },
154
+ deleted: async () => { drafts.remove(identity); invalid.delete(identity); await refresh(); await select(state.providers.find(item => item.name === state.active)?.name || state.providers[0]?.name || '', true); },
155
+ duplicate: async editing => {
156
+ if (newDraft) { await select(NEW_DRAFT_ID); return; }
157
+ const used = new Set(state.providers.map(item => item.name.toLowerCase()));
158
+ const base = editing.name.slice(0, 56); let candidate = base; let number = 2;
159
+ while (used.has(candidate.toLowerCase())) candidate = `${base}-${number++}`;
160
+ editing.name = candidate; newDraft = editing; await select(NEW_DRAFT_ID, true);
161
+ },
162
+ });
163
+ if (gen !== renderId || ctx.signal.aborted) mounted.dispose(); else controller = mounted;
164
+ }
165
+ async function create() {
166
+ if (newDraft) return select(NEW_DRAFT_ID);
167
+ if (controller && !(await controller.leave())) return;
168
+ newDraft = { original: null, name: '', entry: { kind: '', baseUrl: '' }, secretValue: '', raw: {} };
169
+ await select(NEW_DRAFT_ID, true);
170
+ }
171
+ const moduleName = (kind: string) => modules.find(module => module.id === kind)?.title ?? kind;
172
+ doc.addEventListener('click', event => { if (!(event.target as Element).closest('.connection-erase')) disarm(); }, opts);
173
+ doc.addEventListener('keydown', event => { if (event.key === 'Escape') disarm(); }, opts);
174
+ const creator = ui.h('div', 'connection-create');
175
+ creator.append(ui.button(S.create, { variant: 'primary', onClick: () => run(create) }), ui.h('p', 'connection-create-hint', S.createHint));
176
+ index.append(creator, cards);
177
+ ctx.lifecycle.own({ dispose: () => { renderId++; controller?.dispose(); } });
178
+ ctx.lifecycle.own(ctx.router.addLeaveDecision(async () => controller ? controller.leave() : true));
179
+ ctx.lifecycle.own(ctx.router.onChange(route => {
180
+ if (route.segments[0] === 'providers' && route.segments[1]) run(() => select(route.segments[1]));
90
181
  }));
182
+ paint();
183
+ const wanted = ctx.route.segments[1] ?? (newDraft ? NEW_DRAFT_ID : undefined);
184
+ await select(wanted && (wanted === NEW_DRAFT_ID || state.providers.some(item => item.name === wanted)) ? wanted : state.providers.find(item => item.name === state.active)?.name ?? state.providers[0]?.name ?? '', true);
91
185
  }
92
-
93
- export const providersFeature: FrameworkFeature = {
94
- route: PROVIDERS_ROUTE,
95
- label: S.navLabel,
96
- icon: 'cpu',
97
- lampId: PROVIDERS_LAMP_ID,
98
- navGroup: S.navGroup,
99
- mount: mountProviders,
100
- };
186
+ export const providersFeature: FrameworkFeature = { route: 'providers', label: S.navLabel, icon: 'cpu', navGroup: S.navGroup, mount: mountProviders };
@@ -1,23 +1,50 @@
1
1
  import { pick } from '../../core/language.ts';
2
-
3
2
  const zh = {
4
- pageTitle: '模型提供商',
5
- modulesAria: '供应模块',
6
- needHost: "模型提供商设置不可用。",
7
- loading: '读取供应模块…',
8
- none: '没有已注册的供应模块。',
9
- navLabel: '模型提供商',
10
- navGroup: 'Core',
3
+ pageTitle: '模型供应商', navLabel: '模型供应商', navGroup: 'Core', create: '+ 新建供应商实例', newName: '新建实例',
4
+ createHint: '实例 = 填一组供应商配置保存成卡片',
5
+ empty: '还没有模型供应商', emptyHint: '添加一条模型连接后,Cortico 才能进行模型推理。',
6
+ configure: '⚙ 配置', activate: '⇄ 设为当前', active: '当前模型', missing: '当前模型供应商不存在,配置中引用:',
7
+ eraseConfirm: '确认删除', probe: '测试可用性', probeClose: '收起',
8
+ readiness: { ready: '配置完成', 'needs-setup': '需要完善配置', 'runtime-unavailable': '运行环境未就绪', invalid: '配置错误', 'module-missing': '模块不可用', draft: '草稿' } as Record<string, string>,
9
+ basic: '基本信息', connection: '连接', modelSection: '模型与生成', moduleSection: '模块设置', pricing: '成本与计价', advanced: '高级协议',
10
+ name: '供应商名称', module: '供应商类型', url: 'API 地址', key: 'API Key', model: '模型',
11
+ moduleNote: (description: string, id: string) => `${description} 模块标识:${id}`,
12
+ nameHint: '英文字母、数字、- 或 _,以字母或数字开头,不允许空格或系统保留名。',
13
+ fixedModule: '供应商类型保存后不可更改。使用其他类型需新建供应商。',
14
+ test: '测试连接', testOk: '测试成功', testFailed: '测试失败', savedFirst: '请先保存配置,再执行此操作。',
15
+ keySet: '已配置;留空保留现有密钥', keyEmpty: '输入 API Key', fetchModels: '获取模型列表',
16
+ reasoning: '推理强度', thinking: '开启推理', temperature: '温度', maxTokens: '最大输出 token', context: '上下文上限', tier: '服务档位', images: '接受图片',
17
+ advancedHint: '仅在使用自定义 API 网关或兼容服务时调整。', secret: '密钥环境变量名',
18
+ extraHeaders: '附加请求头(JSON object)', extraBody: '附加请求体(JSON object)', priceRules: '完整计价规则(JSON array)',
19
+ shared: '共享配置:此供应商可被多个 Bot 使用。修改会改变这份共享配置;若需要不同参数,建议新建一个供应商。',
20
+ remove: '删除供应商', duplicate: '复制供应商', cancel: '取消', draft: '保存草稿', save: '保存', saved: '已保存', drafted: '草稿已保存到此浏览器。API Key 不保存在浏览器草稿中。',
21
+ chooseModule: '请选择供应商类型。', invalidNumber: '请输入有效范围内的数值。',
22
+ required: '此项必填。', jsonObject: '请输入合法 JSON 对象。', jsonArray: '请输入合法 JSON 数组。',
23
+ unsaved: '你有未保存的更改。', stay: '留在这里', discard: '放弃更改并切换',
24
+ deleteConfirm: '删除供应商及其目录中的全部文件?', referenced: '此供应商正在被以下 Bot 使用,不能删除:', reload: '重新加载',
11
25
  };
12
-
13
26
  const en: typeof zh = {
14
- pageTitle: 'LLM Provider',
15
- modulesAria: 'Provider modules',
16
- needHost: "LLM provider settings unavailable.",
17
- loading: 'Loading provider modules…',
18
- none: 'No provider modules registered.',
19
- navLabel: 'LLM Provider',
20
- navGroup: 'Core',
27
+ pageTitle: 'Model connections', navLabel: 'Model connections', navGroup: 'Core', create: '+ New connection instance', newName: 'New instance',
28
+ createHint: 'An instance is one set of provider settings saved as a card.',
29
+ empty: 'No model connections yet', emptyHint: 'Add a model connection to enable inference.',
30
+ configure: ' Configure', activate: '⇄ Set current', active: 'Current model', missing: 'Current model connection is missing: ',
31
+ eraseConfirm: 'Confirm delete', probe: 'Test endpoint', probeClose: 'Hide',
32
+ readiness: { ready: 'Configured', 'needs-setup': 'Needs setup', 'runtime-unavailable': 'Runtime unavailable', invalid: 'Invalid configuration', 'module-missing': 'Module unavailable', draft: 'Draft' },
33
+ basic: 'Basic information', connection: 'Connection', modelSection: 'Model and generation', moduleSection: 'Module settings', pricing: 'Pricing', advanced: 'Advanced protocol',
34
+ name: 'Connection name', module: 'Connection type', url: 'API URL', key: 'API Key', model: 'Model',
35
+ moduleNote: (description: string, id: string) => `${description} Module ID: ${id}`,
36
+ nameHint: 'English letters, digits, - or _; start with a letter or digit. No spaces or reserved system names.',
37
+ fixedModule: 'The connection type cannot change after saving. Create a connection to use another type.',
38
+ test: 'Test connection', testOk: 'Test successful', testFailed: 'Test failed', savedFirst: 'Save the configuration before this operation.',
39
+ keySet: 'Configured; leave blank to keep the current key', keyEmpty: 'Enter API Key', fetchModels: 'Fetch model list',
40
+ reasoning: 'Reasoning effort', thinking: 'Enable reasoning', temperature: 'Temperature', maxTokens: 'Max output tokens', context: 'Context limit', tier: 'Service tier', images: 'Accept images',
41
+ advancedHint: 'Adjust only for custom API gateways or compatible services.', secret: 'Credential environment variable',
42
+ extraHeaders: 'Extra headers (JSON object)', extraBody: 'Extra request body (JSON object)', priceRules: 'Full pricing rules (JSON array)',
43
+ shared: 'Shared configuration: multiple bots can use this connection. Changes update this shared configuration; create another connection if you need different settings.',
44
+ remove: 'Delete connection', duplicate: 'Duplicate connection', cancel: 'Cancel', draft: 'Save draft', save: 'Save', saved: 'Saved', drafted: 'Draft saved in this browser. API Keys are not stored in browser drafts.',
45
+ chooseModule: 'Choose a connection type.', invalidNumber: 'Enter a number within the allowed range.',
46
+ required: 'Required.', jsonObject: 'Enter a valid JSON object.', jsonArray: 'Enter a valid JSON array.',
47
+ unsaved: 'You have unsaved changes.', stay: 'Stay here', discard: 'Discard and switch',
48
+ deleteConfirm: 'Delete this connection and all files in its directory?', referenced: 'This connection is used by these bots and cannot be deleted: ', reload: 'Reload',
21
49
  };
22
-
23
50
  export const S = pick({ zh, en });
@@ -0,0 +1,15 @@
1
+ import type { ProviderHubApi } from '../../../../providers/hub-api.ts';
2
+ export type HubState = ReturnType<ProviderHubApi['list']>;
3
+ export type Connection = HubState['providers'][number];
4
+ export type Detail = ReturnType<ProviderHubApi['detail']>;
5
+ export type Module = ReturnType<ProviderHubApi['moduleList']>[number];
6
+ export interface Editing {
7
+ original: string | null;
8
+ copyFrom?: { name: string; revision: string };
9
+ name: string;
10
+ entry: Detail['entry'];
11
+ revision?: string;
12
+ secretValue: string;
13
+ raw: Record<string, string>;
14
+ }
15
+ export const connectionPath = (name: string) => `/api/providers/${encodeURIComponent(name)}`;
@@ -1,10 +1,12 @@
1
1
  import { LANGUAGE, saveLanguage, type Language } from '../../core/language.ts';
2
+ import { post } from '../../core/api.ts';
2
3
  import type { FeatureContext } from '../feature.ts';
3
4
  import { S } from './strings.ts';
4
5
 
5
6
  export function mountGeneral(ctx: FeatureContext): void {
6
7
  const { ui, root, signal } = ctx;
7
8
  const win = root.ownerDocument.defaultView!;
9
+ // 同页别处都是档案卡:标题与说明的字体从卡片来,这里自己写 h3/p 会与它们对不齐。
8
10
  const sheet = ui.sheet({ title: S.language, en: 'language', desc: S.languageDesc });
9
11
  const group = ui.h('div', 'rowbar');
10
12
  group.setAttribute('role', 'group');
@@ -20,6 +22,17 @@ export function mountGeneral(ctx: FeatureContext): void {
20
22
  group.append(button);
21
23
  }
22
24
 
25
+ if (ctx.capabilities.auth) {
26
+ const access = ui.sheet({ title: S.access, en: 'access', desc: S.accessDesc });
27
+ const signOut = ui.h('button', 'btn', S.signOut);
28
+ signOut.type = 'button';
29
+ signOut.addEventListener('click', () => {
30
+ void post('/api/auth/logout', {}, { signal }).then(() => win.location.reload()).catch(ctx.onError);
31
+ }, { signal });
32
+ access.body.appendChild(signOut);
33
+ root.append(access.el);
34
+ }
35
+
23
36
  async function changeLanguage(language: Language): Promise<void> {
24
37
  if (!await ui.confirm({ title: S.reloadTitle, body: S.reloadBody }) || signal.aborted) return;
25
38
  saveLanguage(language, win.localStorage);
@@ -73,6 +73,7 @@ export function mountSettings(ctx: FeatureContext): void {
73
73
  if (visible[0]) select(visible[0].id);
74
74
  }
75
75
 
76
+ // 入口是底栏那颗齿轮,不占左栏一行;`hidden` 只保留路由。
76
77
  export const settingsFeature: FrameworkFeature = {
77
78
  route: 'settings',
78
79
  label: S.navLabel,
@@ -7,6 +7,9 @@ const zh = {
7
7
  languageDesc: '仅保存在当前浏览器,刷新页面后生效。',
8
8
  reloadTitle: '切换界面语言?',
9
9
  reloadBody: '页面将刷新,未保存的编辑会丢失。Bot 将继续运行。',
10
+ access: '访问',
11
+ accessDesc: '这个控制台设置了访问密码;登录态保存在当前浏览器的 Cookie 里。',
12
+ signOut: '退出登录',
10
13
  appearance: '外观',
11
14
  appearanceDesc: '控制台主题、明暗模式与自定义配色。',
12
15
 
@@ -22,6 +25,9 @@ const en: typeof zh = {
22
25
  languageDesc: 'Saved in this browser; takes effect after reloading.',
23
26
  reloadTitle: 'Change interface language?',
24
27
  reloadBody: 'The page will reload and unsaved edits will be lost. The bot will keep running.',
28
+ access: 'Access',
29
+ accessDesc: 'This console is protected by an access password; the sign-in is kept in a cookie in this browser.',
30
+ signOut: 'Sign out',
25
31
  appearance: 'Appearance',
26
32
  appearanceDesc: 'Console theme, light/dark mode and custom palettes.',
27
33
 
@@ -179,6 +179,7 @@ export function mountWorlds(ctx: FeatureContext): void {
179
179
  );
180
180
  if (ctx.signal.aborted) return;
181
181
  setMsg(out?.result || (wantEnabled ? S.activated : S.deactivated));
182
+ // 左栏重排失败只是导航旧了一拍,激活本身已经成功,不进上面那行的失败文案。
182
183
  void ctx.refreshNav?.().catch(ctx.onError);
183
184
  await load();
184
185
  } catch (err) {
@@ -215,6 +215,10 @@ export function boot(doc: Document = document): { dispose(): void } {
215
215
  }
216
216
  const head = route.segments[0];
217
217
 
218
+ if (head === PROVIDER_ROUTE && route.segments[1]?.startsWith('llm:')) {
219
+ router.replace(['providers']);
220
+ return;
221
+ }
218
222
  if (head === PROVIDER_ROUTE) {
219
223
  unmountFeature();
220
224
  const pageId = route.segments[1];
@@ -117,6 +117,7 @@ export function createShell(deps: ShellDeps): ConsoleShell {
117
117
  const el = ui.h('div');
118
118
  el.id = 'rail';
119
119
 
120
+ // 左上角只有框架字标。这个 bot 叫什么写在底栏头像旁边——那才是这一台的名字。
120
121
  const brand = ui.h('div', 'brand');
121
122
  brand.setAttribute('role', 'img');
122
123
  brand.setAttribute('aria-label', FRAMEWORK_NAME);
@@ -127,8 +128,14 @@ export function createShell(deps: ShellDeps): ConsoleShell {
127
128
 
128
129
  const foot = ui.h('div', 'railfoot');
129
130
  const avatar = createAvatarControl({ doc, ui, signal, onError });
130
- const botName = ui.h('button', 'rail-name', DEFAULT_BRAND);
131
+ const botName = ui.h('button', 'rail-name');
131
132
  botName.type = 'button';
133
+ // 名字自己是按钮(点了就地改名);铅笔只在悬停时露出来,不占额外宽度。
134
+ const botNameText = ui.h('span', 'rail-name-text', DEFAULT_BRAND);
135
+ const botNameEdit = ui.h('span', 'rail-name-edit');
136
+ botNameEdit.setAttribute('aria-hidden', 'true');
137
+ botNameEdit.appendChild(icon(doc, 'pencil'));
138
+ botName.append(botNameText, botNameEdit);
132
139
  const who = ui.h('div', 'rail-who');
133
140
  who.append(avatar.el, botName);
134
141
  const footActions = ui.h('div', 'rail-actions');
@@ -137,16 +144,12 @@ export function createShell(deps: ShellDeps): ConsoleShell {
137
144
  const shutdownButton = ui.h('button', 'rail-action rail-shutdown');
138
145
  shutdownButton.type = 'button';
139
146
  shutdownButton.appendChild(icon(doc, 'power'));
140
- // 重启 = 同一套收尾 + 退出前落重启标志,启动器循环把进程拉起来。
141
- const restartButton = ui.h('button', 'rail-action rail-restart');
142
- restartButton.type = 'button';
143
- restartButton.appendChild(icon(doc, 'refresh'));
144
147
  const settingsButton = ui.h('button', 'rail-action');
145
148
  settingsButton.type = 'button';
146
149
  settingsButton.setAttribute('aria-label', S.settingsAria);
147
150
  settingsButton.title = S.settingsTitle;
148
151
  settingsButton.appendChild(icon(doc, 'settings'));
149
- footActions.append(runButton, settingsButton, restartButton, shutdownButton);
152
+ footActions.append(runButton, settingsButton, shutdownButton);
150
153
  foot.append(who, footActions);
151
154
 
152
155
  let paused = false;
@@ -161,7 +164,7 @@ export function createShell(deps: ShellDeps): ConsoleShell {
161
164
  };
162
165
  renderRun();
163
166
 
164
- /** 关机和重启共用请求锁,等待请求完成后释放。 */
167
+ /** 关机请求的锁,等待请求完成后释放。 */
165
168
  let shuttingDown = false;
166
169
  const renderPower = (): void => {
167
170
  const canShutdown = capabilities.shutdown === true;
@@ -169,45 +172,23 @@ export function createShell(deps: ShellDeps): ConsoleShell {
169
172
  shutdownButton.disabled = !canShutdown || shuttingDown;
170
173
  shutdownButton.setAttribute('aria-label', S.shutdownAria);
171
174
  shutdownButton.title = shuttingDown ? S.finishing : S.shutdownTitle;
172
- const canRestart = capabilities.restart === true;
173
- restartButton.hidden = !canRestart;
174
- restartButton.disabled = !canRestart || shuttingDown;
175
- restartButton.setAttribute('aria-label', S.restartAria);
176
- restartButton.title = shuttingDown
177
- ? S.finishing
178
- : capabilities.supervised === true
179
- ? S.restartTitleSupervised
180
- : S.restartTitleUnsupervised;
181
175
  };
182
176
  renderPower();
183
177
 
184
- const powerAction = async (kind: 'shutdown' | 'restart'): Promise<void> => {
178
+ const powerAction = async (): Promise<void> => {
185
179
  if (shuttingDown) return;
186
- if (kind === 'shutdown' ? capabilities.shutdown !== true : capabilities.restart !== true) return;
187
- const supervised = capabilities.supervised === true;
188
- const first = await ui.confirm(kind === 'shutdown'
189
- ? {
190
- title: S.confirmShutdownTitle,
191
- body: S.shutdownBody,
192
- danger: true,
193
- }
194
- : {
195
- title: S.confirmRestartTitle,
196
- body: supervised ? S.restartSupervisedNote : S.restartUnsupervisedNote,
197
- danger: true,
198
- });
180
+ if (capabilities.shutdown !== true) return;
181
+ const first = await ui.confirm({
182
+ title: S.confirmShutdownTitle,
183
+ body: S.shutdownBody,
184
+ danger: true,
185
+ });
199
186
  if (!first || signal.aborted) return;
200
- const second = await ui.confirm(kind === 'shutdown'
201
- ? {
202
- title: S.confirmShutdownAgainTitle,
203
- body: S.confirmShutdownAgainBody,
204
- danger: true,
205
- }
206
- : {
207
- title: S.confirmRestartAgainTitle,
208
- body: supervised ? S.confirmRestartAgainSupervised : S.confirmRestartAgainUnsupervised,
209
- danger: true,
210
- });
187
+ const second = await ui.confirm({
188
+ title: S.confirmShutdownAgainTitle,
189
+ body: S.confirmShutdownAgainBody,
190
+ danger: true,
191
+ });
211
192
  if (!second || signal.aborted) return;
212
193
  shuttingDown = true;
213
194
  renderPower();
@@ -217,7 +198,7 @@ export function createShell(deps: ShellDeps): ConsoleShell {
217
198
  ok?: boolean; localComplete?: boolean; result?: string; error?: string;
218
199
  steps?: Array<{ label: string; ok: boolean; ms: number; detail?: string }>;
219
200
  externalChecks?: Array<{ status: 'verified-ended' | 'still-live' | 'unknown' }>;
220
- }>(kind === 'shutdown' ? '/api/run/shutdown' : '/api/run/restart', undefined, { signal });
201
+ }>('/api/run/shutdown', undefined, { signal });
221
202
  if (out?.error) throw new Error(out.error);
222
203
  const steps = out?.steps ?? [];
223
204
  const localComplete = out?.localComplete ?? steps.every((step) => step.ok);
@@ -225,13 +206,12 @@ export function createShell(deps: ShellDeps): ConsoleShell {
225
206
  .some((check) => check.status !== 'verified-ended');
226
207
  const lines = steps.map((s) =>
227
208
  `${s.ok ? '✓' : '✗'} ${s.label} · ${(s.ms / 1000).toFixed(1)}s${s.ok ? '' : ` — ${s.detail ?? S.stepIncomplete}`}`);
228
- const done = kind === 'shutdown' ? S.doneShutdown : supervised ? S.doneRestartSupervised : S.doneRestart;
229
209
  void ui.confirm({
230
210
  title: !localComplete
231
211
  ? S.resultLocalIncomplete
232
212
  : externalUnverified
233
213
  ? S.resultExternalUnverified
234
- : out?.ok === false ? S.resultUnverified : done,
214
+ : out?.ok === false ? S.resultUnverified : S.doneShutdown,
235
215
  body: [out?.result ?? S.resultDefault, '', ...lines].join('\n'),
236
216
  });
237
217
  } catch (err) {
@@ -246,8 +226,7 @@ export function createShell(deps: ShellDeps): ConsoleShell {
246
226
  renderPower();
247
227
  }
248
228
  };
249
- shutdownButton.addEventListener('click', () => { void powerAction('shutdown'); }, { signal });
250
- restartButton.addEventListener('click', () => { void powerAction('restart'); }, { signal });
229
+ shutdownButton.addEventListener('click', () => { void powerAction(); }, { signal });
251
230
 
252
231
  runButton.addEventListener('click', () => {
253
232
  if (runPending || capabilities.run !== true) return;
@@ -381,7 +360,7 @@ export function createShell(deps: ShellDeps): ConsoleShell {
381
360
 
382
361
  // 只列已装配的:左栏是"能去的地方",不是全量清单。未装配的仍由各自的
383
362
  // 总览页负责露面(那里才有"为什么没装上"的位置)。供应模块(kind `llm`)不在
384
- // 这里逐个列出:它们的入口是框架的「语言模型」页,模块清单是那一页里的次级菜单。
363
+ // 这里逐个列出:它们的入口是框架的「模型提供商」页,模块清单是那一页里的次级菜单。
385
364
  const listed = consolePages.filter((p) => p.availability === 'active');
386
365
  // Persona 页在前,它的 Memory 页跟在后面,再是归这一组的框架页(系统提示词),同一组。
387
366
  const personas = [
@@ -456,7 +435,8 @@ export function createShell(deps: ShellDeps): ConsoleShell {
456
435
  const setBrand = (name: string | null | undefined): void => {
457
436
  const shown = typeof name === 'string' && name.trim() !== '' ? name.trim() : DEFAULT_BRAND;
458
437
  shownName = shown;
459
- botName.textContent = shown;
438
+ botNameText.textContent = shown;
439
+ // 悬停说的是这一下能做什么;全名在改名框里看得到。
460
440
  botName.title = S.renameTitle;
461
441
  avatar.setLabel(shown);
462
442
  doc.title = S.docTitle(shown);