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.
- package/README.md +7 -14
- package/package.json +1 -3
- package/src/boot.ts +18 -0
- package/src/bot.ts +41 -6
- package/src/core/README.md +4 -6
- package/src/core/blobs.ts +23 -2
- package/src/core/config-schema.ts +1 -1
- package/src/core/config.ts +2 -16
- package/src/core/core.ts +15 -5
- package/src/core/loop.ts +55 -27
- package/src/core/session.ts +3 -2
- package/src/core/timers.ts +8 -6
- package/src/core/types.ts +19 -3
- package/src/core/util.ts +17 -1
- package/src/deploy.ts +4 -1
- package/src/extensions/README.md +6 -4
- package/src/extensions/dry-mount.ts +23 -2
- package/src/extensions/manifest.ts +21 -11
- package/src/extensions.ts +147 -23
- package/src/launcher.ts +23 -9
- package/src/protocol/open-responses/context-log.ts +37 -9
- package/src/providers/README.md +32 -8
- package/src/providers/base.ts +3 -1
- package/src/providers/configuration.ts +2 -1
- package/src/providers/console/hub.ts +270 -0
- package/src/providers/console/settings.ts +8 -18
- package/src/providers/console/types.ts +5 -0
- package/src/providers/hub-api.ts +3 -0
- package/src/providers/llamacpp/config.ts +8 -6
- package/src/providers/llamacpp/console/runtime-panel.ts +5 -1
- package/src/providers/llamacpp/console/server.ts +3 -1
- package/src/providers/llamacpp/index.ts +3 -1
- package/src/providers/llamacpp/native.ts +10 -5
- package/src/providers/llamacpp/strings.ts +6 -6
- package/src/providers/name.ts +8 -0
- package/src/providers/openai-responses-compat/config.ts +13 -0
- package/src/providers/openai-responses-compat/console/client.ts +5 -0
- package/src/providers/openai-responses-compat/console/reasoning-panel.ts +84 -0
- package/src/providers/openai-responses-compat/console/server.ts +127 -0
- package/src/providers/openai-responses-compat/index.ts +38 -13
- package/src/providers/openai-responses-compat/native.ts +9 -4
- package/src/providers/openai-responses-compat/strings.ts +62 -1
- package/src/providers/registry.ts +5 -0
- package/src/providers/transport/responses-input.ts +59 -10
- package/src/web/README.md +11 -6
- package/src/web/auth.ts +80 -0
- package/src/web/client/console-pages/builtins/llm-settings/panel.ts +5 -4
- package/src/web/client/console-pages/builtins/llm-settings/pricing-panel.ts +22 -8
- package/src/web/client/console-pages/builtins/llm-settings/strings.ts +6 -8
- package/src/web/client/console-pages/host.ts +24 -7
- package/src/web/client/core/api.ts +5 -1
- package/src/web/client/core/router.ts +15 -0
- package/src/web/client/features/core/strings.ts +2 -2
- package/src/web/client/features/extensions/index.ts +276 -41
- package/src/web/client/features/extensions/strings.ts +84 -10
- package/src/web/client/features/feature.ts +2 -2
- package/src/web/client/features/live/diagnostics.ts +32 -0
- package/src/web/client/features/live/index.ts +34 -11
- package/src/web/client/features/live/onboarding.ts +4 -1
- package/src/web/client/features/live/protocol.ts +1 -0
- package/src/web/client/features/live/strings.ts +20 -14
- package/src/web/client/features/live/timeline.ts +2 -1
- package/src/web/client/features/providers/detail.ts +262 -0
- package/src/web/client/features/providers/drafts.ts +23 -0
- package/src/web/client/features/providers/index.ts +173 -87
- package/src/web/client/features/providers/strings.ts +44 -17
- package/src/web/client/features/providers/types.ts +15 -0
- package/src/web/client/features/settings/general.ts +13 -0
- package/src/web/client/features/settings/index.ts +1 -0
- package/src/web/client/features/settings/strings.ts +6 -0
- package/src/web/client/features/worlds/index.ts +1 -0
- package/src/web/client/main.ts +4 -0
- package/src/web/client/shell/index.ts +28 -48
- package/src/web/client/shell/strings.ts +0 -22
- package/src/web/client/ui/icons.ts +14 -1
- package/src/web/client/ui/prompt-input.tsx +17 -5
- package/src/web/client/ui/strings.ts +0 -2
- package/src/web/console-pages.ts +1 -1
- package/src/web/diagnostics.ts +133 -0
- package/src/web/public/login.html +67 -0
- package/src/web/public/styles.css +143 -26
- package/src/web/server.ts +233 -19
- package/src/web/shared/client-panel.ts +4 -1
- package/src/web/shared/console-protocol.ts +4 -1
- package/src/worlds/bilibili/README.md +1 -1
- package/src/worlds/bilibili/overlay/server.ts +4 -2
- package/src/worlds/minecraft/ADAPT.md +66 -0
- package/src/worlds/minecraft/README.md +69 -11
- package/src/worlds/minecraft/cell-facts.ts +226 -0
- package/src/worlds/minecraft/chests.ts +5 -0
- package/src/worlds/minecraft/containers.ts +325 -0
- package/src/worlds/minecraft/entity-facts.ts +31 -5
- package/src/worlds/minecraft/executor.ts +283 -10348
- package/src/worlds/minecraft/inventory.ts +268 -0
- package/src/worlds/minecraft/melee.ts +419 -0
- package/src/worlds/minecraft/mineflayer-fixes.ts +55 -1
- package/src/worlds/minecraft/placed-ledger.ts +152 -0
- package/src/worlds/minecraft/placement.ts +1038 -0
- package/src/worlds/minecraft/receipt.ts +340 -0
- package/src/worlds/minecraft/skill-context.ts +358 -0
- package/src/worlds/minecraft/skills-build.ts +1169 -0
- package/src/worlds/minecraft/skills-container.ts +1343 -0
- package/src/worlds/minecraft/skills-craft.ts +333 -0
- package/src/worlds/minecraft/skills-dig.ts +624 -0
- package/src/worlds/minecraft/skills-gather.ts +1230 -0
- package/src/worlds/minecraft/skills-interact.ts +1559 -0
- package/src/worlds/minecraft/tools.ts +331 -0
- package/src/worlds/minecraft/travel.ts +763 -0
- package/src/worlds/minecraft/until.ts +75 -0
- package/src/worlds/qq/normalize.ts +14 -0
- package/src/worlds/qq/world.ts +53 -7
- 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
|
|
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
|
-
|
|
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
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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: '
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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);
|
|
@@ -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) {
|
package/src/web/client/main.ts
CHANGED
|
@@ -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'
|
|
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,
|
|
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 (
|
|
178
|
+
const powerAction = async (): Promise<void> => {
|
|
185
179
|
if (shuttingDown) return;
|
|
186
|
-
if (
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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(
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
}>(
|
|
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 :
|
|
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(
|
|
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
|
-
|
|
438
|
+
botNameText.textContent = shown;
|
|
439
|
+
// 悬停说的是这一下能做什么;全名在改名框里看得到。
|
|
460
440
|
botName.title = S.renameTitle;
|
|
461
441
|
avatar.setLabel(shown);
|
|
462
442
|
doc.title = S.docTitle(shown);
|