cortico 0.1.2 → 0.1.3
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/package.json +1 -1
- package/src/boot.ts +11 -0
- package/src/core/README.md +8 -0
- package/src/core/core.ts +3 -1
- package/src/core/generation.ts +66 -9
- package/src/core/instance-lock.ts +6 -0
- package/src/core/loop.ts +7 -5
- package/src/core/secrets.ts +2 -4
- package/src/core/types.ts +5 -2
- package/src/extensions/dry-mount.ts +16 -9
- package/src/extensions/manifest.ts +1 -1
- package/src/launcher.ts +8 -14
- package/src/protocol/open-responses/context-helpers.ts +3 -1
- package/src/providers/README.md +16 -9
- package/src/providers/console/config.ts +15 -0
- package/src/providers/console/hub.ts +49 -12
- package/src/providers/console/settings.ts +20 -8
- package/src/providers/console/strings.ts +14 -0
- package/src/providers/llamacpp/console/models-panel.ts +87 -4
- package/src/providers/llamacpp/console/server.ts +24 -2
- package/src/providers/llamacpp/huggingface.ts +93 -0
- package/src/providers/llamacpp/strings.ts +42 -0
- package/src/providers/name.ts +5 -0
- package/src/providers/openai-responses-compat/console/server.ts +9 -1
- package/src/providers/openai-responses-compat/native.ts +8 -3
- package/src/providers/pricebook.ts +5 -5
- package/src/providers/registry.ts +21 -12
- package/src/providers/strings.ts +2 -0
- package/src/providers/transport/errors.ts +9 -0
- package/src/providers/transport/response-http.ts +7 -3
- package/src/web/README.md +3 -0
- package/src/web/client/console-pages/host.ts +10 -10
- package/src/web/client/features/providers/detail.ts +184 -137
- package/src/web/client/features/providers/index.ts +48 -42
- package/src/web/client/features/providers/strings.ts +22 -18
- package/src/web/client/ui/fields.ts +3 -1
- package/src/web/public/styles.css +28 -20
- package/src/web/server.ts +4 -2
- package/src/web/shared/console-protocol.ts +13 -0
- package/src/worlds/minecraft/client-launch.ts +11 -2
- package/src/worlds/minecraft/client.ts +4 -0
- package/src/worlds/qq/world.ts +3 -3
|
@@ -3,13 +3,12 @@ import { icon } from '../../ui/icons.ts';
|
|
|
3
3
|
import { NEW_DRAFT_ID, providerDrafts } from './drafts.ts';
|
|
4
4
|
import type { FeatureContext, FrameworkFeature } from '../feature.ts';
|
|
5
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';
|
|
9
6
|
import { S } from './strings.ts';
|
|
10
7
|
import { connectionPath, type HubState, type Connection, type Module, type Detail, type Editing } from './types.ts';
|
|
11
8
|
import { mountDetail, type DetailController } from './detail.ts';
|
|
12
9
|
|
|
10
|
+
/** How often the card list re-reads other deployments' usage. */
|
|
11
|
+
const USAGE_REFRESH_MS = 5000;
|
|
13
12
|
export async function mountProviders(ctx: FeatureContext): Promise<void> {
|
|
14
13
|
const { ui, root } = ctx;
|
|
15
14
|
root.append(pageIntro(ui, S.pageTitle));
|
|
@@ -32,7 +31,7 @@ export async function mountProviders(ctx: FeatureContext): Promise<void> {
|
|
|
32
31
|
let renderId = 0;
|
|
33
32
|
interface CardNode {
|
|
34
33
|
el: HTMLElement; title: HTMLElement; model: HTMLElement; url: HTMLElement; status: HTMLElement; secondary: HTMLElement;
|
|
35
|
-
kind: HTMLElement; activate: HTMLButtonElement; erase: HTMLButtonElement;
|
|
34
|
+
kind: HTMLElement; activate: HTMLButtonElement; erase: HTMLButtonElement;
|
|
36
35
|
}
|
|
37
36
|
const nodes = new Map<string, CardNode>();
|
|
38
37
|
const rows = new Map<string, Connection>();
|
|
@@ -52,40 +51,51 @@ export async function mountProviders(ctx: FeatureContext): Promise<void> {
|
|
|
52
51
|
let node = nodes.get(identity);
|
|
53
52
|
if (!node) {
|
|
54
53
|
const el = ui.h('article', 'connection-card'); el.dataset.provider = identity;
|
|
55
|
-
const title = ui.h('div', 'connection-name');
|
|
56
|
-
const
|
|
54
|
+
const title = ui.h('div', 'connection-name');
|
|
55
|
+
const facts = ui.h('div', 'connection-facts');
|
|
56
|
+
const fact = (key: string) => { const box = ui.h('span', `kv kv-${key}`); const value = ui.h('span', 'kv-v'); box.append(ui.h('span', 'kv-k', key), value); facts.append(box); return value; };
|
|
57
|
+
const kind = fact('kind'); const model = fact('model'); const url = fact('baseUrl');
|
|
57
58
|
const status = ui.h('div', 'connection-status'); const secondary = ui.h('div', 'connection-secondary');
|
|
58
|
-
const
|
|
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 () => {
|
|
59
|
+
const activate = ui.button('⇄', { size: 'sm', onClick: () => run(async () => {
|
|
61
60
|
activate.disabled = true;
|
|
62
61
|
try { await post(connectionPath(identity) + '/activate', {}, opts); state.active = identity; paint(); }
|
|
63
62
|
finally { activate.disabled = false; }
|
|
64
63
|
}) });
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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'));
|
|
64
|
+
activate.classList.add('connection-activate');
|
|
65
|
+
activate.setAttribute('aria-label', S.activate); activate.title = S.activate;
|
|
66
|
+
const connect = ui.h('div', 'connection-connect'); connect.append(activate, ui.h('span', 'connection-connect-label', S.connect));
|
|
70
67
|
const erase = eraseButton(identity);
|
|
71
|
-
|
|
68
|
+
el.append(erase, title, facts, status, secondary, connect);
|
|
69
|
+
el.tabIndex = 0;
|
|
70
|
+
el.addEventListener('keydown', event => { if (event.target === el && (event.key === 'Enter' || event.key === ' ')) { event.preventDefault(); run(() => select(identity)); } }, opts);
|
|
72
71
|
el.addEventListener('click', event => { if (!(event.target as Element).closest('button')) run(() => select(identity)); }, opts);
|
|
73
72
|
if (identity === NEW_DRAFT_ID) cards.prepend(el); else cards.append(el);
|
|
74
|
-
node = { el, title, kind, model, url, status, secondary, activate, erase
|
|
73
|
+
node = { el, title, kind, model, url, status, secondary, activate, erase }; nodes.set(identity, node);
|
|
75
74
|
}
|
|
76
75
|
const active = identity === state.active;
|
|
77
76
|
node.el.classList.toggle('is-active', active); node.el.classList.toggle('is-selected', identity === selected);
|
|
78
77
|
node.title.textContent = identity === NEW_DRAFT_ID ? newDraft?.name || S.newName : identity;
|
|
79
78
|
node.title.title = node.title.textContent;
|
|
79
|
+
const draft = identity === NEW_DRAFT_ID ? null : drafts.get(identity);
|
|
80
|
+
const model = draft?.entry.spec?.model || item.model || '—'; const url = draft?.entry.baseUrl || item.baseUrl || '—';
|
|
80
81
|
node.kind.textContent = item.moduleTitle; node.kind.title = item.moduleTitle;
|
|
81
|
-
node.model.textContent =
|
|
82
|
-
node.model.title =
|
|
82
|
+
node.model.textContent = model; node.url.textContent = url;
|
|
83
|
+
node.model.title = model; node.url.title = url;
|
|
83
84
|
const readiness = invalid.has(identity) && identity !== NEW_DRAFT_ID ? 'invalid' : item.readiness.state;
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
node.
|
|
85
|
+
const users = 'usage' in item ? item.usage : [];
|
|
86
|
+
const running = users.filter(user => user.running).map(user => user.name);
|
|
87
|
+
const stopped = users.filter(user => !user.running).map(user => user.name);
|
|
88
|
+
const tone = active || running.length ? 'active' : readiness === 'draft' ? 'draft' : readiness === 'ready' ? 'ready' : 'error';
|
|
89
|
+
node.status.dataset.tone = tone;
|
|
90
|
+
node.status.textContent = `${{ active: '●', ready: '✓', error: '!', draft: '✎' }[tone]} ${active ? S.active : running.length ? S.inUse(running.join('、')) : S.readiness[readiness]}`;
|
|
91
|
+
const notes: string[] = [];
|
|
92
|
+
if ((active || running.length) && readiness !== 'ready') notes.push('! ' + S.readiness[readiness]);
|
|
93
|
+
if (identity !== NEW_DRAFT_ID && drafts.has(identity)) notes.push('✎ ' + S.readiness.draft);
|
|
94
|
+
if (active && running.length) notes.push(S.inUse(running.join('、')));
|
|
95
|
+
if (stopped.length) notes.push(S.selectedBy(stopped.join('、')));
|
|
96
|
+
node.secondary.textContent = notes.join(' · ');
|
|
97
|
+
node.secondary.dataset.tone = (active || running.length) && readiness !== 'ready' ? 'error' : 'draft';
|
|
98
|
+
node.activate.parentElement!.hidden = active || identity === NEW_DRAFT_ID || readiness !== 'ready' || running.length > 0;
|
|
89
99
|
if (identity === NEW_DRAFT_ID) rows.delete(identity); else rows.set(identity, item as Connection);
|
|
90
100
|
}
|
|
91
101
|
}
|
|
@@ -118,23 +128,9 @@ export async function mountProviders(ctx: FeatureContext): Promise<void> {
|
|
|
118
128
|
await refresh();
|
|
119
129
|
if (selected === identity) await select(fallback(), true);
|
|
120
130
|
}
|
|
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
131
|
async function refresh() { state = await get<HubState>('/api/providers', opts); paint(); }
|
|
135
132
|
async function select(identity: string, force = false): Promise<void> {
|
|
136
133
|
if (!force && identity === selected) return;
|
|
137
|
-
if (!force && controller && !(await controller.leave())) return;
|
|
138
134
|
const gen = ++renderId;
|
|
139
135
|
controller?.dispose(); controller = null;
|
|
140
136
|
selected = identity; paint(); detailRoot.replaceChildren();
|
|
@@ -146,9 +142,13 @@ export async function mountProviders(ctx: FeatureContext): Promise<void> {
|
|
|
146
142
|
if (gen !== renderId || ctx.signal.aborted) return;
|
|
147
143
|
const detailView = ui.h('div'); detailRoot.replaceChildren(detailView);
|
|
148
144
|
const mounted = await mountDetail({ ctx, root: detailView, modules, saved, draft: identity === NEW_DRAFT_ID ? newDraft : drafts.get(identity),
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
145
|
+
// Edits persist as a browser draft as they happen; a form back at its saved state drops the draft.
|
|
146
|
+
changed: (editing, hasErrors, dirty) => {
|
|
147
|
+
if (hasErrors) invalid.add(identity); else invalid.delete(identity);
|
|
148
|
+
if (identity === NEW_DRAFT_ID) { newDraft = editing; drafts.set(editing); }
|
|
149
|
+
else if (dirty) drafts.set(editing); else drafts.remove(identity);
|
|
150
|
+
paint();
|
|
151
|
+
},
|
|
152
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
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
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); },
|
|
@@ -164,8 +164,8 @@ export async function mountProviders(ctx: FeatureContext): Promise<void> {
|
|
|
164
164
|
}
|
|
165
165
|
async function create() {
|
|
166
166
|
if (newDraft) return select(NEW_DRAFT_ID);
|
|
167
|
-
if (controller && !(await controller.leave())) return;
|
|
168
167
|
newDraft = { original: null, name: '', entry: { kind: '', baseUrl: '' }, secretValue: '', raw: {} };
|
|
168
|
+
drafts.set(newDraft);
|
|
169
169
|
await select(NEW_DRAFT_ID, true);
|
|
170
170
|
}
|
|
171
171
|
const moduleName = (kind: string) => modules.find(module => module.id === kind)?.title ?? kind;
|
|
@@ -175,10 +175,16 @@ export async function mountProviders(ctx: FeatureContext): Promise<void> {
|
|
|
175
175
|
creator.append(ui.button(S.create, { variant: 'primary', onClick: () => run(create) }), ui.h('p', 'connection-create-hint', S.createHint));
|
|
176
176
|
index.append(creator, cards);
|
|
177
177
|
ctx.lifecycle.own({ dispose: () => { renderId++; controller?.dispose(); } });
|
|
178
|
-
ctx.lifecycle.own(ctx.router.addLeaveDecision(async () => controller ? controller.leave() : true));
|
|
179
178
|
ctx.lifecycle.own(ctx.router.onChange(route => {
|
|
180
179
|
if (route.segments[0] === 'providers' && route.segments[1]) run(() => select(route.segments[1]));
|
|
181
180
|
}));
|
|
181
|
+
// Other deployments' selections and liveness change without this page; the detail form is left alone.
|
|
182
|
+
let refreshing = false;
|
|
183
|
+
ctx.lifecycle.interval(() => {
|
|
184
|
+
if (refreshing) return;
|
|
185
|
+
refreshing = true;
|
|
186
|
+
run(async () => { try { await refresh(); } finally { refreshing = false; } });
|
|
187
|
+
}, USAGE_REFRESH_MS);
|
|
182
188
|
paint();
|
|
183
189
|
const wanted = ctx.route.segments[1] ?? (newDraft ? NEW_DRAFT_ID : undefined);
|
|
184
190
|
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);
|
|
@@ -1,50 +1,54 @@
|
|
|
1
1
|
import { pick } from '../../core/language.ts';
|
|
2
2
|
const zh = {
|
|
3
|
-
pageTitle: '模型供应商', navLabel: '模型供应商', navGroup: 'Core', create: '+ 新建供应商实例', newName: '
|
|
3
|
+
pageTitle: '模型供应商', navLabel: '模型供应商', navGroup: 'Core', create: '+ 新建供应商实例', newName: '未命名实例',
|
|
4
4
|
createHint: '实例 = 填一组供应商配置保存成卡片',
|
|
5
5
|
empty: '还没有模型供应商', emptyHint: '添加一条模型连接后,Cortico 才能进行模型推理。',
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
eraseConfirm: '确认删除',
|
|
7
|
+
connect: '连接', activate: '设为当前供应商', active: '当前模型', missing: '当前模型供应商不存在,配置中引用:',
|
|
8
|
+
inUse: (names: string) => `其他实例使用中:${names}`, selectedBy: (names: string) => `已被 ${names} 选用(未运行)`,
|
|
9
|
+
readiness: { ready: '可连接', 'needs-setup': '需要完善配置', 'runtime-unavailable': '运行环境未就绪', invalid: '配置错误', 'module-missing': '模块不可用', draft: '草稿' } as Record<string, string>,
|
|
9
10
|
basic: '基本信息', connection: '连接', modelSection: '模型与生成', moduleSection: '模块设置', pricing: '成本与计价', advanced: '高级协议',
|
|
10
11
|
name: '供应商名称', module: '供应商类型', url: 'API 地址', key: 'API Key', model: '模型',
|
|
11
|
-
moduleNote: (description: string, id: string) => `${description} 模块标识:${id}`,
|
|
12
12
|
nameHint: '英文字母、数字、- 或 _,以字母或数字开头,不允许空格或系统保留名。',
|
|
13
13
|
fixedModule: '供应商类型保存后不可更改。使用其他类型需新建供应商。',
|
|
14
|
-
test: '测试连接', testOk: '测试成功', testFailed: '测试失败',
|
|
15
|
-
|
|
14
|
+
test: '测试连接', testOk: '测试成功', testFailed: '测试失败',
|
|
15
|
+
keyEmpty: '输入 API Key', fetchModels: '获取模型列表',
|
|
16
16
|
reasoning: '推理强度', thinking: '开启推理', temperature: '温度', maxTokens: '最大输出 token', context: '上下文上限', tier: '服务档位', images: '接受图片',
|
|
17
|
+
catalogNoWindow: '目录没有给出这个模型的上下文上限;请按服务商文档手填。',
|
|
17
18
|
advancedHint: '仅在使用自定义 API 网关或兼容服务时调整。', secret: '密钥环境变量名',
|
|
18
19
|
extraHeaders: '附加请求头(JSON object)', extraBody: '附加请求体(JSON object)', priceRules: '完整计价规则(JSON array)',
|
|
19
20
|
shared: '共享配置:此供应商可被多个 Bot 使用。修改会改变这份共享配置;若需要不同参数,建议新建一个供应商。',
|
|
20
|
-
remove: '删除供应商', duplicate: '复制供应商', cancel: '
|
|
21
|
+
remove: '删除供应商', duplicate: '复制供应商', cancel: '放弃更改', save: '保存', saved: '已保存',
|
|
22
|
+
draftNote: '改动暂存在此浏览器,保存后才写入配置。API Key 不进暂存。',
|
|
21
23
|
chooseModule: '请选择供应商类型。', invalidNumber: '请输入有效范围内的数值。',
|
|
22
24
|
required: '此项必填。', jsonObject: '请输入合法 JSON 对象。', jsonArray: '请输入合法 JSON 数组。',
|
|
23
|
-
unsaved: '
|
|
25
|
+
unsaved: '已暂存;保存后写入配置。',
|
|
24
26
|
deleteConfirm: '删除供应商及其目录中的全部文件?', referenced: '此供应商正在被以下 Bot 使用,不能删除:', reload: '重新加载',
|
|
25
27
|
};
|
|
26
28
|
const en: typeof zh = {
|
|
27
|
-
pageTitle: 'Model connections', navLabel: 'Model connections', navGroup: 'Core', create: '+ New connection instance', newName: '
|
|
29
|
+
pageTitle: 'Model connections', navLabel: 'Model connections', navGroup: 'Core', create: '+ New connection instance', newName: 'Unnamed instance',
|
|
28
30
|
createHint: 'An instance is one set of provider settings saved as a card.',
|
|
29
31
|
empty: 'No model connections yet', emptyHint: 'Add a model connection to enable inference.',
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
eraseConfirm: 'Confirm delete',
|
|
33
|
+
connect: 'Connect', activate: 'Set current connection', active: 'Current model', missing: 'Current model connection is missing: ',
|
|
34
|
+
inUse: (names: string) => `In use by: ${names}`, selectedBy: (names: string) => `Selected by ${names} (not running)`,
|
|
35
|
+
readiness: { ready: 'Ready to connect', 'needs-setup': 'Needs setup', 'runtime-unavailable': 'Runtime unavailable', invalid: 'Invalid configuration', 'module-missing': 'Module unavailable', draft: 'Draft' },
|
|
33
36
|
basic: 'Basic information', connection: 'Connection', modelSection: 'Model and generation', moduleSection: 'Module settings', pricing: 'Pricing', advanced: 'Advanced protocol',
|
|
34
37
|
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
38
|
nameHint: 'English letters, digits, - or _; start with a letter or digit. No spaces or reserved system names.',
|
|
37
39
|
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',
|
|
39
|
-
|
|
40
|
+
test: 'Test connection', testOk: 'Test successful', testFailed: 'Test failed',
|
|
41
|
+
keyEmpty: 'Enter API Key', fetchModels: 'Fetch model list',
|
|
40
42
|
reasoning: 'Reasoning effort', thinking: 'Enable reasoning', temperature: 'Temperature', maxTokens: 'Max output tokens', context: 'Context limit', tier: 'Service tier', images: 'Accept images',
|
|
43
|
+
catalogNoWindow: 'The model list states no context window for this model; fill it in from the provider\'s documentation.',
|
|
41
44
|
advancedHint: 'Adjust only for custom API gateways or compatible services.', secret: 'Credential environment variable',
|
|
42
45
|
extraHeaders: 'Extra headers (JSON object)', extraBody: 'Extra request body (JSON object)', priceRules: 'Full pricing rules (JSON array)',
|
|
43
46
|
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: '
|
|
47
|
+
remove: 'Delete connection', duplicate: 'Duplicate connection', cancel: 'Discard changes', save: 'Save', saved: 'Saved',
|
|
48
|
+
draftNote: 'Edits are kept in this browser and written to the configuration on save. API Keys are never kept.',
|
|
45
49
|
chooseModule: 'Choose a connection type.', invalidNumber: 'Enter a number within the allowed range.',
|
|
46
50
|
required: 'Required.', jsonObject: 'Enter a valid JSON object.', jsonArray: 'Enter a valid JSON array.',
|
|
47
|
-
unsaved: '
|
|
51
|
+
unsaved: 'Kept as a draft; written on save.',
|
|
48
52
|
deleteConfirm: 'Delete this connection and all files in its directory?', referenced: 'This connection is used by these bots and cannot be deleted: ', reload: 'Reload',
|
|
49
53
|
};
|
|
50
54
|
export const S = pick({ zh, en });
|
|
@@ -138,7 +138,9 @@ export function checkbox(
|
|
|
138
138
|
*/
|
|
139
139
|
export function field(doc: Document, label: string, control: HTMLElement): HTMLLabelElement {
|
|
140
140
|
const el = h(doc, 'label', 'fieldrow');
|
|
141
|
-
|
|
141
|
+
const caption = h(doc, 'span', 'fieldlabel', label.replace(/\s*\*$/, ''));
|
|
142
|
+
if (/\*$/.test(label)) caption.append(h(doc, 'span', 'required-mark', ' *'));
|
|
143
|
+
el.append(caption, control);
|
|
142
144
|
return el;
|
|
143
145
|
}
|
|
144
146
|
|
|
@@ -1749,17 +1749,23 @@
|
|
|
1749
1749
|
.connection-card{min-width:0;border:1px solid var(--line);border-radius:12px;padding:14px;background:var(--paper);cursor:pointer;transition:background .15s,border-color .15s}
|
|
1750
1750
|
.connection-card.is-active{border-color:var(--accent);box-shadow:0 0 8px color-mix(in srgb,var(--accent) 12%,transparent)}
|
|
1751
1751
|
.connection-card.is-selected{background:color-mix(in srgb,var(--ink) 5%,var(--paper));outline:2px solid var(--ink-dim);outline-offset:1px}
|
|
1752
|
-
.connection-name,.connection-
|
|
1753
|
-
.connection-name{font:700 17px/1.25 var(--sans)}
|
|
1752
|
+
.connection-name,.connection-status,.connection-secondary{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
1753
|
+
.connection-name{font:700 17px/1.25 var(--sans)}
|
|
1754
|
+
.connection-facts{display:flex;flex-wrap:wrap;gap:6px;margin-top:10px;min-width:0}
|
|
1755
|
+
.connection-facts .kv{max-width:100%}.connection-facts .kv-v{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
1756
|
+
.kv-kind{--kv-line:color-mix(in srgb,var(--ink-blue) 60%,transparent)}.kv-model{--kv-line:color-mix(in srgb,var(--accent-2) 75%,transparent)}.kv-baseUrl{--kv-line:var(--line-strong)}
|
|
1754
1757
|
.connection-status{font-size:12px;margin-top:14px}.connection-secondary{font-size:11px;color:var(--ink-dim)}
|
|
1755
|
-
.connection-card .rowbar{margin-top:12px;justify-content:space-between;gap:4px}
|
|
1756
1758
|
.connection-detail{min-width:0}.connection-detail .sheet{margin-bottom:16px}
|
|
1759
|
+
.connection-identity{background:var(--paper-2);border-color:var(--line-2)}
|
|
1760
|
+
.connection-identity+.connection-flow{margin-top:22px;padding-top:18px;border-top:1px dashed var(--line-2)}
|
|
1761
|
+
.connection-flow{counter-reset:step}.connection-step{counter-increment:step;min-width:0}
|
|
1762
|
+
.connection-step td.mono{white-space:normal;overflow-wrap:anywhere}
|
|
1763
|
+
.connection-step table.data .btn{white-space:nowrap}
|
|
1764
|
+
.connection-step>.sheet>h3::before,.connection-step>details.sheet>summary>h3::before{content:counter(step);display:inline-grid;place-items:center;width:20px;height:20px;margin-right:9px;border-radius:50%;background:color-mix(in srgb,var(--accent) 16%,transparent);color:var(--accent);font:600 11px/1 var(--mono);vertical-align:2px}
|
|
1757
1765
|
.connection-detail .connection-field{margin-bottom:15px}.connection-detail input:not([type=checkbox]),.connection-detail select,.connection-detail textarea{max-width:100%;width:100%}
|
|
1758
1766
|
.connection-detail .field-error{color:var(--danger,#b43c38);font-size:12px;min-height:0}.connection-shared{color:var(--ink-dim);font-size:12px;line-height:1.7;margin:20px 0 12px}
|
|
1759
1767
|
.connection-actions{display:flex;gap:8px;flex-wrap:wrap;align-items:center}.connection-actions .grow{flex:1}
|
|
1760
1768
|
@media(max-width:760px){.connection-hub{grid-template-columns:1fr}.connection-index{position:static;max-height:320px}.connection-cards{grid-template-columns:repeat(auto-fit,minmax(210px,1fr))}}
|
|
1761
|
-
.connection-guard{border:1px solid var(--line);border-radius:16px;padding:24px;background:var(--paper);color:var(--ink);max-width:560px;box-shadow:0 12px 60px #0003}
|
|
1762
|
-
.connection-guard::backdrop{background:#0005}.connection-guard h3{margin-bottom:20px}
|
|
1763
1769
|
|
|
1764
1770
|
.live-summary { flex-wrap:wrap; }
|
|
1765
1771
|
.live-connection { display:inline-flex; align-items:center; gap:7px; min-width:0; max-width:48%; background:none; border:0; color:var(--ink-soft); cursor:pointer; padding:0; font:11.5px/1 var(--sans); }
|
|
@@ -1807,26 +1813,28 @@
|
|
|
1807
1813
|
.coreview > .scroll { scrollbar-gutter: auto; }
|
|
1808
1814
|
.panelslot > :first-child > .sheet:first-child { padding-top: 0; }
|
|
1809
1815
|
|
|
1810
|
-
/*
|
|
1816
|
+
/* 卡片:连接在右上,删除在右下,图标底边对齐状态行。 */
|
|
1811
1817
|
.connection-create { margin-bottom:16px; }
|
|
1812
1818
|
.connection-create-hint { margin:7px 2px 0; font:var(--text-detail)/1.5 var(--sans); color:var(--muted-foreground); }
|
|
1813
1819
|
.connection-create > .btn { width:100%; }
|
|
1814
|
-
.connection-card { position:relative; }
|
|
1815
|
-
.connection-erase { position:absolute;
|
|
1816
|
-
.connection-erase .icon { width:
|
|
1820
|
+
.connection-card { position:relative; padding-right:64px; }
|
|
1821
|
+
.connection-erase { position:absolute; bottom:13px; right:13px; display:flex; align-items:center; gap:0; height:28px; padding:0 4px; border:1px solid transparent; border-radius:8px; background:none; color:var(--ink-dim); cursor:pointer; transition:color .14s ease, background .14s ease, border-color .14s ease, gap .18s ease; }
|
|
1822
|
+
.connection-erase .icon { width:20px; height:20px; flex:none; }
|
|
1817
1823
|
.connection-erase:hover { color:var(--danger,#b43c38); background:var(--hover); }
|
|
1818
1824
|
.connection-erase-text { display:grid; grid-template-columns:0fr; overflow:hidden; font:600 12px/1 var(--sans); white-space:nowrap; transition:grid-template-columns .18s ease; }
|
|
1819
1825
|
.connection-erase-text > span { overflow:hidden; }
|
|
1820
1826
|
.connection-erase.is-armed { gap:6px; color:var(--danger,#b43c38); border-color:currentColor; background:var(--paper); }
|
|
1821
1827
|
.connection-erase.is-armed .connection-erase-text { grid-template-columns:1fr; }
|
|
1822
|
-
.connection-
|
|
1823
|
-
.connection-
|
|
1824
|
-
.connection-
|
|
1825
|
-
|
|
1826
|
-
.connection-
|
|
1827
|
-
.connection-
|
|
1828
|
-
.
|
|
1829
|
-
.connection-
|
|
1830
|
-
.connection-
|
|
1831
|
-
.connection-
|
|
1832
|
-
.connection-
|
|
1828
|
+
.connection-connect { position:absolute; top:12px; right:12px; width:32px; text-align:center; }
|
|
1829
|
+
.connection-connect[hidden] { display:none; }
|
|
1830
|
+
.connection-hub .btn.connection-activate { display:block; width:32px; height:28px; min-height:28px; padding:0; border:0; font-size:17px; line-height:28px; }
|
|
1831
|
+
.connection-connect-label { display:block; font-size:10px; line-height:14px; color:var(--muted-foreground); }
|
|
1832
|
+
.connection-index { scrollbar-width:none; }
|
|
1833
|
+
.connection-index::-webkit-scrollbar { display:none; }
|
|
1834
|
+
.required-mark { color:var(--danger); }
|
|
1835
|
+
.connection-test { gap:12px; align-items:center; }
|
|
1836
|
+
.connection-test .msgline { margin:0; }
|
|
1837
|
+
.connection-status[data-tone="active"] { color:var(--accent); }
|
|
1838
|
+
.connection-status[data-tone="ready"] { color:var(--ok); }
|
|
1839
|
+
.connection-status[data-tone="error"], .connection-secondary[data-tone="error"] { color:var(--danger); }
|
|
1840
|
+
.connection-status[data-tone="draft"], .connection-secondary[data-tone="draft"] { color:var(--foreground); }
|
package/src/web/server.ts
CHANGED
|
@@ -1409,8 +1409,10 @@ export class WebApp {
|
|
|
1409
1409
|
app.post('/api/providers/:name/save', express.json(), providerRoute((hub, req) => hub.save(String(req.params.name), req.body, this.languageOf(req))));
|
|
1410
1410
|
app.post('/api/providers/:name/delete', express.json(), providerRoute((hub, req) => hub.delete(String(req.params.name), req.body.expectedRevision)));
|
|
1411
1411
|
app.post('/api/providers/:name/activate', providerRoute((hub, req) => hub.activate(String(req.params.name), this.languageOf(req))));
|
|
1412
|
-
|
|
1413
|
-
|
|
1412
|
+
// A body with `entry` probes the browser's draft instead of the saved connection.
|
|
1413
|
+
const draftOf = (req: Request) => (req.body && typeof req.body === 'object' && req.body.entry ? { entry: req.body.entry, secretValue: req.body.secretValue } : undefined);
|
|
1414
|
+
app.post('/api/providers/:name/test', express.json(), providerRoute((hub, req) => hub.action(String(req.params.name), 'test', this.languageOf(req), draftOf(req))));
|
|
1415
|
+
app.post('/api/providers/:name/models', express.json(), providerRoute((hub, req) => hub.action(String(req.params.name), 'models', this.languageOf(req), draftOf(req))));
|
|
1414
1416
|
|
|
1415
1417
|
app.get('/api/status', wrap((_req, res) => {
|
|
1416
1418
|
res.json({ ...this.safeStatus(), uptimeSec: Math.round(process.uptime()) });
|
|
@@ -58,6 +58,17 @@ export function isBuiltinPanel(name: unknown): name is string {
|
|
|
58
58
|
return typeof name === 'string' && BUILTIN_PANEL_RE.test(name);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
/**
|
|
62
|
+
* 端点编辑页自带的四块段落。`llm:*` 页除 `settings` 外的面板按声明顺序就是编辑页的段落:
|
|
63
|
+
* `builtin` 取这里的名字的由编辑页自己画,标题与说明用声明的;其余是模块自己的浏览器实现。
|
|
64
|
+
*/
|
|
65
|
+
export const CONNECTION_EDITOR_BLOCKS = ['connection-endpoint', 'connection-model', 'connection-pricing', 'connection-protocol'] as const;
|
|
66
|
+
export type ConnectionEditorBlock = (typeof CONNECTION_EDITOR_BLOCKS)[number];
|
|
67
|
+
|
|
68
|
+
export function isConnectionEditorBlock(name: unknown): name is ConnectionEditorBlock {
|
|
69
|
+
return (CONNECTION_EDITOR_BLOCKS as readonly unknown[]).includes(name);
|
|
70
|
+
}
|
|
71
|
+
|
|
61
72
|
/** 构建脚本与服务端注册表共用的目录到 page id 映射。 */
|
|
62
73
|
export function pageIdFor(kind: ContributingKind, name: string): string {
|
|
63
74
|
return `${kind}:${name}`;
|
|
@@ -85,6 +96,8 @@ export interface ConsoleAssetManifest {
|
|
|
85
96
|
core: string | null;
|
|
86
97
|
/** key 为 page id。 */
|
|
87
98
|
providers: Record<string, ConsoleAssetEntry>;
|
|
99
|
+
/** esbuild 在仓库内读到的每个源文件的 sha256,键为仓库相对路径;启动器据此判断产物是否落后于源码。 */
|
|
100
|
+
sources?: Record<string, string>;
|
|
88
101
|
}
|
|
89
102
|
|
|
90
103
|
/** 静态资源 URL 的唯一合法前缀。服务端把它映射到 `dist/web/`。 */
|
|
@@ -65,6 +65,8 @@ interface ClientLaunch {
|
|
|
65
65
|
nativeJars: string[];
|
|
66
66
|
/** 实际用到的版本 id 链,child → root */
|
|
67
67
|
versionChain: string[];
|
|
68
|
+
/** 版本 JSON 点名、但 libraries 目录里没有的条目,相对 libraries 目录。类路径里少了它们。 */
|
|
69
|
+
missingLibraries: string[];
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
const OS_NAMES: Partial<Record<NodeJS.Platform, string>> = {
|
|
@@ -202,6 +204,7 @@ export function buildClientLaunch(input: ClientLaunchInput): ClientLaunch | { er
|
|
|
202
204
|
const librariesDir = join(gameDir, 'libraries');
|
|
203
205
|
const classpath: string[] = [];
|
|
204
206
|
const nativeJars: string[] = [];
|
|
207
|
+
const missingLibraries: string[] = [];
|
|
205
208
|
const seenPaths = new Set<string>();
|
|
206
209
|
for (const version of chain) {
|
|
207
210
|
for (const lib of version.libraries ?? []) {
|
|
@@ -211,7 +214,10 @@ export function buildClientLaunch(input: ClientLaunchInput): ClientLaunch | { er
|
|
|
211
214
|
const abs = join(librariesDir, rel);
|
|
212
215
|
if (seenPaths.has(abs)) continue;
|
|
213
216
|
seenPaths.add(abs);
|
|
214
|
-
if (!existsSync(abs))
|
|
217
|
+
if (!existsSync(abs)) {
|
|
218
|
+
missingLibraries.push(rel);
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
215
221
|
if (/natives/i.test(rel)) nativeJars.push(abs);
|
|
216
222
|
classpath.push(abs);
|
|
217
223
|
}
|
|
@@ -222,7 +228,9 @@ export function buildClientLaunch(input: ClientLaunchInput): ClientLaunch | { er
|
|
|
222
228
|
const separator = os === 'windows' ? ';' : ':';
|
|
223
229
|
const vars: Record<string, string> = {
|
|
224
230
|
auth_player_name: input.username,
|
|
225
|
-
version_name
|
|
231
|
+
// 加载器版本靠 `-DignoreList=...,${version_name}.jar` 把主 jar 挡在模块路径外,所以这个名字
|
|
232
|
+
// 必须是实际加载的那个 jar;继承链上的版本没有自己的 jar,用的是 jarId 那个。
|
|
233
|
+
version_name: jarId,
|
|
226
234
|
game_directory: gameDir,
|
|
227
235
|
assets_root: join(gameDir, 'assets'),
|
|
228
236
|
game_assets: join(gameDir, 'assets'),
|
|
@@ -268,5 +276,6 @@ export function buildClientLaunch(input: ClientLaunchInput): ClientLaunch | { er
|
|
|
268
276
|
mainClass,
|
|
269
277
|
nativeJars,
|
|
270
278
|
versionChain: chain.map((v, i) => v.id ?? (i === 0 ? versionId : '?')),
|
|
279
|
+
missingLibraries,
|
|
271
280
|
};
|
|
272
281
|
}
|
|
@@ -215,6 +215,10 @@ export class GameClient {
|
|
|
215
215
|
return this.state();
|
|
216
216
|
}
|
|
217
217
|
this.activeGameDir = gameDir;
|
|
218
|
+
if ('missingLibraries' in launch && launch.missingLibraries.length > 0) {
|
|
219
|
+
this.opts.log.warn(`${this.opts.label}版本 JSON 点名的 ${launch.missingLibraries.length} 个库`
|
|
220
|
+
+ `不在 libraries 目录,类路径里没有它们:${launch.missingLibraries.join(' ')}`);
|
|
221
|
+
}
|
|
218
222
|
if ('nativeJars' in launch) prepareNatives(launch.nativeJars, launch.nativesDir, this.opts.log);
|
|
219
223
|
if (!this.opts.commandOverride) {
|
|
220
224
|
if (this.opts.noPauseOnLostFocus()) {
|
package/src/worlds/qq/world.ts
CHANGED
|
@@ -132,7 +132,9 @@ export class QQWorld implements World {
|
|
|
132
132
|
private host?: WorldHost;
|
|
133
133
|
private driver?: OneBotDriver;
|
|
134
134
|
private log: Logger = nullLogger();
|
|
135
|
-
|
|
135
|
+
/** 主模型支持 image/png 或 World 配置了视觉模型时启用取图;每次渲染按当前端点判断。 */
|
|
136
|
+
private readonly imagePolicy: ImageRenderPolicy = (data) =>
|
|
137
|
+
makeImagePolicy(!!this.vision || (this.host?.modelFacts.accepts('image/png') ?? false))(data);
|
|
136
138
|
|
|
137
139
|
/** 外挂视觉(注入=auxVLM生效);null=无,一切保持[图片]占位现状 */
|
|
138
140
|
private readonly vision?: VisionService;
|
|
@@ -548,8 +550,6 @@ export class QQWorld implements World {
|
|
|
548
550
|
async start(host: WorldHost): Promise<void> {
|
|
549
551
|
this.host = host;
|
|
550
552
|
this.log = host.log;
|
|
551
|
-
// 主模型支持 image/png 或 World 配置了视觉模型时启用取图。
|
|
552
|
-
this.imagePolicy = makeImagePolicy(!!this.vision || host.modelFacts.accepts('image/png'));
|
|
553
553
|
// 外挂视觉的用量自愿上报进 core 的成本账(不报就在成本页看不到)
|
|
554
554
|
this.vision?.setUsageSink((usage, model) =>
|
|
555
555
|
host.reportUsage(usage, { model, label: 'QQWorld·辅助视觉' }),
|