cortico 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +47 -0
- package/package.json +43 -0
- package/src/boot.ts +46 -0
- package/src/bot.ts +1342 -0
- package/src/config-file.ts +41 -0
- package/src/core/README.md +92 -0
- package/src/core/billing.ts +26 -0
- package/src/core/blobs.ts +106 -0
- package/src/core/bus.ts +311 -0
- package/src/core/config-schema.ts +201 -0
- package/src/core/config.ts +262 -0
- package/src/core/core.ts +699 -0
- package/src/core/cost.ts +244 -0
- package/src/core/event-store.ts +304 -0
- package/src/core/fork.ts +113 -0
- package/src/core/generation.ts +118 -0
- package/src/core/instance-lock.ts +215 -0
- package/src/core/ipc-logger.ts +105 -0
- package/src/core/language.ts +62 -0
- package/src/core/log-context.ts +38 -0
- package/src/core/loop.ts +1858 -0
- package/src/core/markers.ts +48 -0
- package/src/core/prefix.ts +121 -0
- package/src/core/run.ts +95 -0
- package/src/core/secrets.ts +18 -0
- package/src/core/session.ts +56 -0
- package/src/core/sessions.ts +261 -0
- package/src/core/state.ts +104 -0
- package/src/core/template.ts +72 -0
- package/src/core/timers.ts +129 -0
- package/src/core/tool-log.ts +135 -0
- package/src/core/transcript.ts +92 -0
- package/src/core/truncate.ts +121 -0
- package/src/core/types.ts +1140 -0
- package/src/core/usage-log.ts +79 -0
- package/src/core/util.ts +386 -0
- package/src/deploy-listing.ts +160 -0
- package/src/deploy.ts +162 -0
- package/src/extensions/README.md +72 -0
- package/src/extensions/dry-mount.ts +390 -0
- package/src/extensions/manifest.ts +153 -0
- package/src/extensions/runtime.ts +42 -0
- package/src/extensions.ts +519 -0
- package/src/launcher.ts +285 -0
- package/src/paths.ts +155 -0
- package/src/protocol/open-responses/LICENSE +201 -0
- package/src/protocol/open-responses/README.md +14 -0
- package/src/protocol/open-responses/context-helpers.ts +27 -0
- package/src/protocol/open-responses/context-log.ts +59 -0
- package/src/protocol/open-responses/context.ts +89 -0
- package/src/protocol/open-responses/generated.ts +115 -0
- package/src/protocol/open-responses/index.ts +33 -0
- package/src/protocol/open-responses/openapi.json +4230 -0
- package/src/protocol/open-responses/stream.ts +146 -0
- package/src/protocol/open-responses/tokens.ts +31 -0
- package/src/providers/README.md +86 -0
- package/src/providers/base.ts +111 -0
- package/src/providers/configuration.ts +97 -0
- package/src/providers/console/config.ts +21 -0
- package/src/providers/console/settings.ts +443 -0
- package/src/providers/console/strings.ts +74 -0
- package/src/providers/console/types.ts +11 -0
- package/src/providers/llamacpp/archive.ts +148 -0
- package/src/providers/llamacpp/catalog.ts +247 -0
- package/src/providers/llamacpp/config.ts +36 -0
- package/src/providers/llamacpp/console/client.ts +6 -0
- package/src/providers/llamacpp/console/models-panel.ts +120 -0
- package/src/providers/llamacpp/console/runtime-panel.ts +173 -0
- package/src/providers/llamacpp/console/server.ts +153 -0
- package/src/providers/llamacpp/index.ts +95 -0
- package/src/providers/llamacpp/native.ts +51 -0
- package/src/providers/llamacpp/options.ts +117 -0
- package/src/providers/llamacpp/runtime-store.ts +139 -0
- package/src/providers/llamacpp/runtime.ts +214 -0
- package/src/providers/llamacpp/server.ts +286 -0
- package/src/providers/llamacpp/strings.ts +230 -0
- package/src/providers/openai-responses-compat/config.ts +20 -0
- package/src/providers/openai-responses-compat/index.ts +89 -0
- package/src/providers/openai-responses-compat/native.ts +127 -0
- package/src/providers/openai-responses-compat/strings.ts +19 -0
- package/src/providers/pricebook.ts +75 -0
- package/src/providers/registry.ts +137 -0
- package/src/providers/strings.ts +71 -0
- package/src/providers/transport/chat.ts +156 -0
- package/src/providers/transport/errors.ts +48 -0
- package/src/providers/transport/history.ts +68 -0
- package/src/providers/transport/native-input.ts +83 -0
- package/src/providers/transport/native-types.ts +13 -0
- package/src/providers/transport/response-assembly.ts +191 -0
- package/src/providers/transport/response-http.ts +196 -0
- package/src/providers/transport/response-meters.ts +27 -0
- package/src/providers/transport/responses-input.ts +56 -0
- package/src/web/README.md +84 -0
- package/src/web/client/console-pages/builtins/llm-settings/panel.ts +625 -0
- package/src/web/client/console-pages/builtins/llm-settings/pricing-panel.ts +168 -0
- package/src/web/client/console-pages/builtins/llm-settings/strings.ts +213 -0
- package/src/web/client/console-pages/builtins.ts +10 -0
- package/src/web/client/console-pages/context.ts +135 -0
- package/src/web/client/console-pages/host.ts +633 -0
- package/src/web/client/console-pages/loader.ts +132 -0
- package/src/web/client/console-pages/strings.ts +90 -0
- package/src/web/client/console-pages/tools/strings.ts +49 -0
- package/src/web/client/console-pages/tools/view.ts +215 -0
- package/src/web/client/core/api.ts +200 -0
- package/src/web/client/core/language.ts +56 -0
- package/src/web/client/core/lifecycle.ts +132 -0
- package/src/web/client/core/router.ts +199 -0
- package/src/web/client/core/stream.ts +157 -0
- package/src/web/client/core/websocket.ts +82 -0
- package/src/web/client/features/appearance/index.ts +333 -0
- package/src/web/client/features/appearance/strings.ts +87 -0
- package/src/web/client/features/config/index.ts +11 -0
- package/src/web/client/features/config/strings.ts +41 -0
- package/src/web/client/features/config/view.ts +428 -0
- package/src/web/client/features/core/events.ts +197 -0
- package/src/web/client/features/core/index.ts +254 -0
- package/src/web/client/features/core/run.ts +56 -0
- package/src/web/client/features/core/runlog.ts +214 -0
- package/src/web/client/features/core/sessions.ts +53 -0
- package/src/web/client/features/core/strings.ts +157 -0
- package/src/web/client/features/extensions/index.ts +422 -0
- package/src/web/client/features/extensions/strings.ts +164 -0
- package/src/web/client/features/feature.ts +93 -0
- package/src/web/client/features/live/context.ts +280 -0
- package/src/web/client/features/live/fork.ts +156 -0
- package/src/web/client/features/live/index.ts +392 -0
- package/src/web/client/features/live/onboarding.ts +113 -0
- package/src/web/client/features/live/protocol.ts +122 -0
- package/src/web/client/features/live/sessions.ts +71 -0
- package/src/web/client/features/live/status.ts +68 -0
- package/src/web/client/features/live/strings.ts +252 -0
- package/src/web/client/features/live/timeline.ts +590 -0
- package/src/web/client/features/prompts/editor.ts +292 -0
- package/src/web/client/features/prompts/index.ts +346 -0
- package/src/web/client/features/prompts/strings.ts +136 -0
- package/src/web/client/features/prompts/view.ts +256 -0
- package/src/web/client/features/providers/index.ts +100 -0
- package/src/web/client/features/providers/strings.ts +23 -0
- package/src/web/client/features/settings/general.ts +28 -0
- package/src/web/client/features/settings/index.ts +82 -0
- package/src/web/client/features/settings/strings.ts +33 -0
- package/src/web/client/features/storage/index.ts +1 -0
- package/src/web/client/features/storage/strings.ts +45 -0
- package/src/web/client/features/storage/view.ts +154 -0
- package/src/web/client/features/usage/chart.ts +486 -0
- package/src/web/client/features/usage/index.ts +407 -0
- package/src/web/client/features/usage/labels.ts +46 -0
- package/src/web/client/features/usage/range.ts +36 -0
- package/src/web/client/features/usage/state.ts +37 -0
- package/src/web/client/features/usage/strings.ts +191 -0
- package/src/web/client/features/usage/tooltip.ts +86 -0
- package/src/web/client/features/usage/types.ts +96 -0
- package/src/web/client/features/worlds/index.ts +424 -0
- package/src/web/client/features/worlds/strings.ts +121 -0
- package/src/web/client/main.ts +273 -0
- package/src/web/client/shell/avatar.ts +233 -0
- package/src/web/client/shell/index.ts +549 -0
- package/src/web/client/shell/strings.ts +89 -0
- package/src/web/client/theme/handoff.ts +43 -0
- package/src/web/client/theme/palette.ts +109 -0
- package/src/web/client/theme/registry.ts +130 -0
- package/src/web/client/theme/storage.ts +72 -0
- package/src/web/client/theme/strings.ts +123 -0
- package/src/web/client/theme/studio.ts +366 -0
- package/src/web/client/ui/actions.ts +109 -0
- package/src/web/client/ui/data.ts +185 -0
- package/src/web/client/ui/dom.ts +23 -0
- package/src/web/client/ui/fields.ts +191 -0
- package/src/web/client/ui/format.ts +75 -0
- package/src/web/client/ui/icons.ts +183 -0
- package/src/web/client/ui/images.ts +91 -0
- package/src/web/client/ui/index.ts +89 -0
- package/src/web/client/ui/lamp.ts +125 -0
- package/src/web/client/ui/log.ts +98 -0
- package/src/web/client/ui/overlay.ts +184 -0
- package/src/web/client/ui/page.ts +8 -0
- package/src/web/client/ui/prompt-input.tsx +311 -0
- package/src/web/client/ui/sheet.ts +131 -0
- package/src/web/client/ui/strings.ts +71 -0
- package/src/web/console-pages.ts +367 -0
- package/src/web/files.ts +107 -0
- package/src/web/path-picker.ts +296 -0
- package/src/web/public/index.html +17 -0
- package/src/web/public/styles.css +1715 -0
- package/src/web/server.ts +1883 -0
- package/src/web/shared/client-panel.ts +603 -0
- package/src/web/shared/console-protocol.ts +560 -0
- package/src/web/shared/css.d.ts +1 -0
- package/src/web/shared/path-picker.ts +23 -0
- package/src/web/shared/theme.ts +256 -0
- package/src/web/theme-store.ts +37 -0
- package/src/world.ts +449 -0
- package/src/worlds/bilibili/ENV_PROMPT.md +5 -0
- package/src/worlds/bilibili/README.md +176 -0
- package/src/worlds/bilibili/audience-admission.ts +995 -0
- package/src/worlds/bilibili/client.ts +528 -0
- package/src/worlds/bilibili/coalescing-buffer.ts +125 -0
- package/src/worlds/bilibili/config.ts +185 -0
- package/src/worlds/bilibili/console/client.ts +202 -0
- package/src/worlds/bilibili/definition.ts +35 -0
- package/src/worlds/bilibili/gift-frame.ts +131 -0
- package/src/worlds/bilibili/normalize.ts +452 -0
- package/src/worlds/bilibili/overlay/announcement.ts +76 -0
- package/src/worlds/bilibili/overlay/assets.ts +82 -0
- package/src/worlds/bilibili/overlay/model.ts +494 -0
- package/src/worlds/bilibili/overlay/project.ts +202 -0
- package/src/worlds/bilibili/overlay/server.ts +286 -0
- package/src/worlds/bilibili/overlay/types.ts +217 -0
- package/src/worlds/bilibili/overlay/web/app.js +690 -0
- package/src/worlds/bilibili/overlay/web/editor.css +297 -0
- package/src/worlds/bilibili/overlay/web/editor.html +102 -0
- package/src/worlds/bilibili/overlay/web/editor.js +2116 -0
- package/src/worlds/bilibili/overlay/web/overlay.html +13 -0
- package/src/worlds/bilibili/overlay/web/styles.css +258 -0
- package/src/worlds/bilibili/protobuf.ts +123 -0
- package/src/worlds/bilibili/wire.ts +94 -0
- package/src/worlds/bilibili/world.ts +1389 -0
- package/src/worlds/console-fixture/ENV_PROMPT.md +1 -0
- package/src/worlds/console-fixture/console/client.ts +72 -0
- package/src/worlds/console-fixture/world.ts +73 -0
- package/src/worlds/index.ts +15 -0
- package/src/worlds/minecraft/ENV_PROMPT.md +32 -0
- package/src/worlds/minecraft/ENV_PROMPT_CAMERA.md +2 -0
- package/src/worlds/minecraft/LICENSE-mineflayer-pathfinder.txt +21 -0
- package/src/worlds/minecraft/README.md +917 -0
- package/src/worlds/minecraft/blueprint-plan.ts +915 -0
- package/src/worlds/minecraft/blueprint-registry.ts +566 -0
- package/src/worlds/minecraft/blueprint-repair.ts +236 -0
- package/src/worlds/minecraft/blueprint-resource.ts +272 -0
- package/src/worlds/minecraft/blueprint.ts +492 -0
- package/src/worlds/minecraft/body-lease.ts +340 -0
- package/src/worlds/minecraft/bridge.ts +749 -0
- package/src/worlds/minecraft/check.ts +651 -0
- package/src/worlds/minecraft/chests.ts +292 -0
- package/src/worlds/minecraft/client-launch.ts +272 -0
- package/src/worlds/minecraft/client-options.ts +85 -0
- package/src/worlds/minecraft/client-skins.ts +164 -0
- package/src/worlds/minecraft/client-window.ps1 +91 -0
- package/src/worlds/minecraft/client.ts +423 -0
- package/src/worlds/minecraft/combat-context.ts +109 -0
- package/src/worlds/minecraft/combat.ts +1552 -0
- package/src/worlds/minecraft/config.ts +499 -0
- package/src/worlds/minecraft/console/access.ts +218 -0
- package/src/worlds/minecraft/console/client.ts +244 -0
- package/src/worlds/minecraft/console/log.ts +110 -0
- package/src/worlds/minecraft/console/mount.ts +420 -0
- package/src/worlds/minecraft/console/skin.ts +257 -0
- package/src/worlds/minecraft/console/style.css +154 -0
- package/src/worlds/minecraft/console/world.ts +449 -0
- package/src/worlds/minecraft/deaths.ts +135 -0
- package/src/worlds/minecraft/definition.ts +18 -0
- package/src/worlds/minecraft/engine-child.ts +274 -0
- package/src/worlds/minecraft/engine-ipc.ts +98 -0
- package/src/worlds/minecraft/entity-facts.ts +132 -0
- package/src/worlds/minecraft/escape.ts +407 -0
- package/src/worlds/minecraft/executor.ts +14494 -0
- package/src/worlds/minecraft/explored.ts +156 -0
- package/src/worlds/minecraft/geometry.ts +205 -0
- package/src/worlds/minecraft/goal-plan.ts +647 -0
- package/src/worlds/minecraft/item-break.ts +126 -0
- package/src/worlds/minecraft/item-facts.ts +146 -0
- package/src/worlds/minecraft/item-pick.ts +67 -0
- package/src/worlds/minecraft/level-dat.ts +176 -0
- package/src/worlds/minecraft/log.ts +132 -0
- package/src/worlds/minecraft/mineflayer-fixes.ts +1180 -0
- package/src/worlds/minecraft/names.ts +377 -0
- package/src/worlds/minecraft/pathfinder-lib.d.ts +13 -0
- package/src/worlds/minecraft/pathfinder-perf.ts +796 -0
- package/src/worlds/minecraft/piglin.ts +49 -0
- package/src/worlds/minecraft/policy.ts +547 -0
- package/src/worlds/minecraft/precheck.ts +655 -0
- package/src/worlds/minecraft/proxy.ts +445 -0
- package/src/worlds/minecraft/ranged.ts +635 -0
- package/src/worlds/minecraft/readouts.ts +102 -0
- package/src/worlds/minecraft/round.ts +26 -0
- package/src/worlds/minecraft/search-observation.ts +92 -0
- package/src/worlds/minecraft/server-config.ts +467 -0
- package/src/worlds/minecraft/server.ts +658 -0
- package/src/worlds/minecraft/show.ts +84 -0
- package/src/worlds/minecraft/skills.ts +1763 -0
- package/src/worlds/minecraft/terrain.ts +1735 -0
- package/src/worlds/minecraft/window.ts +96 -0
- package/src/worlds/minecraft/works.ts +179 -0
- package/src/worlds/minecraft/world.ts +6438 -0
- package/src/worlds/qq/ENV_PROMPT.md +5 -0
- package/src/worlds/qq/config.ts +108 -0
- package/src/worlds/qq/console/client.ts +126 -0
- package/src/worlds/qq/console/events.ts +155 -0
- package/src/worlds/qq/console/gate.ts +232 -0
- package/src/worlds/qq/console/roster.ts +203 -0
- package/src/worlds/qq/console/style.css +65 -0
- package/src/worlds/qq/conversation.ts +28 -0
- package/src/worlds/qq/definition.ts +62 -0
- package/src/worlds/qq/driver.ts +375 -0
- package/src/worlds/qq/history-tools.ts +209 -0
- package/src/worlds/qq/normalize.ts +323 -0
- package/src/worlds/qq/qface-map.ts +282 -0
- package/src/worlds/qq/vision-prompt.ts +27 -0
- package/src/worlds/qq/vision.ts +642 -0
- package/src/worlds/qq/vlm.ts +142 -0
- package/src/worlds/qq/world.ts +1503 -0
- package/src/worlds/terminal/ENV_PROMPT.md +11 -0
- package/src/worlds/terminal/config.ts +9 -0
- package/src/worlds/terminal/definition.ts +14 -0
- package/src/worlds/terminal/world.ts +700 -0
- package/src/worlds/websearch/ENV_PROMPT.md +1 -0
- package/src/worlds/websearch/brave-client.ts +261 -0
- package/src/worlds/websearch/config.ts +75 -0
- package/src/worlds/websearch/definition.ts +20 -0
- package/src/worlds/websearch/world.ts +171 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/** 表单控件与标签;事件监听绑定 UI 实例的 signal。 */
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
ConsoleCheckbox,
|
|
5
|
+
ConsoleCheckboxOptions,
|
|
6
|
+
ConsoleFieldOptions,
|
|
7
|
+
ConsoleInputOptions,
|
|
8
|
+
ConsoleSegmented,
|
|
9
|
+
ConsoleSegmentedOptions,
|
|
10
|
+
ConsoleSelectOptions,
|
|
11
|
+
ConsoleTextareaOptions,
|
|
12
|
+
} from '../../shared/client-panel.ts';
|
|
13
|
+
import { h } from './dom.ts';
|
|
14
|
+
|
|
15
|
+
/** `field` + 调用方追加的 class(如 `mono` / `grow`) */
|
|
16
|
+
function fieldClass(extra?: string): string {
|
|
17
|
+
return extra ? 'field ' + extra : 'field';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** 初始化值、禁用状态与回调。onInput 监听输入(select 使用 change),onChange 监听 change,onCommit 监听提交键。 */
|
|
21
|
+
function wire(
|
|
22
|
+
el: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement,
|
|
23
|
+
signal: AbortSignal,
|
|
24
|
+
opts: ConsoleFieldOptions,
|
|
25
|
+
event: 'input' | 'change',
|
|
26
|
+
): void {
|
|
27
|
+
if (opts.value != null) el.value = opts.value;
|
|
28
|
+
if (opts.disabled) el.disabled = true;
|
|
29
|
+
const onInput = opts.onInput;
|
|
30
|
+
if (onInput) el.addEventListener(event, () => onInput(el.value), { signal });
|
|
31
|
+
const onChange = opts.onChange;
|
|
32
|
+
if (onChange) el.addEventListener('change', () => onChange(el.value), { signal });
|
|
33
|
+
const onCommit = opts.onCommit;
|
|
34
|
+
if (onCommit) {
|
|
35
|
+
// 形参写成 `Event` 再收窄:`el` 是三种控件的联合类型,它的 `addEventListener`
|
|
36
|
+
// 只剩下不带事件名类型推导的那条通用重载(TS 对联合方法取的是签名的交集),
|
|
37
|
+
// 直接写 `(ev: KeyboardEvent)` 会被判成不兼容。
|
|
38
|
+
el.addEventListener(
|
|
39
|
+
'keydown',
|
|
40
|
+
(ev: Event) => {
|
|
41
|
+
if (isCommitKey(el, ev as KeyboardEvent)) onCommit(el.value);
|
|
42
|
+
},
|
|
43
|
+
{ signal },
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 判断 Enter 是否提交:IME composition 期间只选词;textarea 裸 Enter 换行,Ctrl/⌘+Enter 提交。
|
|
50
|
+
*/
|
|
51
|
+
function isCommitKey(el: { tagName: string }, ev: KeyboardEvent): boolean {
|
|
52
|
+
if (ev.key !== 'Enter' || ev.isComposing) return false;
|
|
53
|
+
if (el.tagName.toLowerCase() !== 'textarea') return true;
|
|
54
|
+
return ev.ctrlKey || ev.metaKey;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** 单行输入。缺省 `type=text`。 */
|
|
58
|
+
export function input(
|
|
59
|
+
doc: Document,
|
|
60
|
+
signal: AbortSignal,
|
|
61
|
+
opts?: ConsoleInputOptions,
|
|
62
|
+
): HTMLInputElement {
|
|
63
|
+
const o = opts ?? {};
|
|
64
|
+
const el = h(doc, 'input', fieldClass(o.cls));
|
|
65
|
+
el.type = o.type ?? 'text';
|
|
66
|
+
if (o.placeholder != null) el.placeholder = o.placeholder;
|
|
67
|
+
wire(el, signal, o, 'input');
|
|
68
|
+
return el;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** 下拉选项先创建,再设置选中值;字符串选项同时作为 value 与 label。 */
|
|
72
|
+
export function select(
|
|
73
|
+
doc: Document,
|
|
74
|
+
signal: AbortSignal,
|
|
75
|
+
opts?: ConsoleSelectOptions,
|
|
76
|
+
): HTMLSelectElement {
|
|
77
|
+
const o = opts ?? {};
|
|
78
|
+
const el = h(doc, 'select', fieldClass(o.cls));
|
|
79
|
+
for (const raw of o.options ?? []) {
|
|
80
|
+
const item = typeof raw === 'string' ? { value: raw, label: raw } : raw;
|
|
81
|
+
const opt = h(doc, 'option', null, item.label ?? item.value);
|
|
82
|
+
opt.value = item.value;
|
|
83
|
+
el.appendChild(opt);
|
|
84
|
+
}
|
|
85
|
+
wire(el, signal, o, 'change');
|
|
86
|
+
return el;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** 多行输入。`rows` 缺省交给 CSS 的 `min-height`。 */
|
|
90
|
+
export function textarea(
|
|
91
|
+
doc: Document,
|
|
92
|
+
signal: AbortSignal,
|
|
93
|
+
opts?: ConsoleTextareaOptions,
|
|
94
|
+
): HTMLTextAreaElement {
|
|
95
|
+
const o = opts ?? {};
|
|
96
|
+
const el = h(doc, 'textarea', fieldClass(o.cls));
|
|
97
|
+
if (o.rows != null) el.rows = o.rows;
|
|
98
|
+
if (o.placeholder != null) el.placeholder = o.placeholder;
|
|
99
|
+
wire(el, signal, o, 'input');
|
|
100
|
+
return el;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 勾选框采用 label.check > input[type=checkbox] + span。文本单独放入 span,使 inline-flex 的 gap 在控件与整段文本之间一致生效。
|
|
105
|
+
*/
|
|
106
|
+
export function checkbox(
|
|
107
|
+
doc: Document,
|
|
108
|
+
signal: AbortSignal,
|
|
109
|
+
label: string,
|
|
110
|
+
opts?: ConsoleCheckboxOptions,
|
|
111
|
+
): ConsoleCheckbox {
|
|
112
|
+
const o = opts ?? {};
|
|
113
|
+
const el = h(doc, 'label', 'check');
|
|
114
|
+
const box = h(doc, 'input');
|
|
115
|
+
box.type = 'checkbox';
|
|
116
|
+
if (o.checked) box.checked = true;
|
|
117
|
+
if (o.disabled) box.disabled = true;
|
|
118
|
+
if (o.title) el.title = o.title;
|
|
119
|
+
el.append(box, h(doc, 'span', null, label));
|
|
120
|
+
const cb = o.onChange;
|
|
121
|
+
if (cb) box.addEventListener('change', () => cb(box.checked), { signal });
|
|
122
|
+
return {
|
|
123
|
+
el,
|
|
124
|
+
input: box,
|
|
125
|
+
get checked(): boolean {
|
|
126
|
+
return box.checked;
|
|
127
|
+
},
|
|
128
|
+
// 程序设置值不触发用户 change 回调。
|
|
129
|
+
setChecked(v: boolean): void {
|
|
130
|
+
box.checked = v;
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* 字段标签 + 控件。控件嵌在 `<label>` 里,点标签即聚焦控件(不必配 `for`/`id`,
|
|
137
|
+
* 扩展也就不必发明一套全局唯一的 id)。
|
|
138
|
+
*/
|
|
139
|
+
export function field(doc: Document, label: string, control: HTMLElement): HTMLLabelElement {
|
|
140
|
+
const el = h(doc, 'label', 'fieldrow');
|
|
141
|
+
el.append(h(doc, 'span', 'fieldlabel', label), control);
|
|
142
|
+
return el;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** 分段选择器保留全部重复 value 选项,并统一更新它们的选中态。 */
|
|
146
|
+
export function segmented(
|
|
147
|
+
doc: Document,
|
|
148
|
+
signal: AbortSignal,
|
|
149
|
+
items: readonly (string | { value: string; label?: string })[],
|
|
150
|
+
opts?: ConsoleSegmentedOptions,
|
|
151
|
+
): ConsoleSegmented {
|
|
152
|
+
const o = opts ?? {};
|
|
153
|
+
const el = h(doc, 'div', o.size === 'sm' ? 'segwrap sm' : 'segwrap');
|
|
154
|
+
const keys: { value: string; btn: HTMLButtonElement }[] = [];
|
|
155
|
+
let current = o.value ?? '';
|
|
156
|
+
const paint = (): void => {
|
|
157
|
+
for (const k of keys) k.btn.className = k.value === current ? 'seg active' : 'seg';
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
for (const raw of items) {
|
|
161
|
+
const item = typeof raw === 'string' ? { value: raw, label: raw } : raw;
|
|
162
|
+
const btn = h(doc, 'button', 'seg', item.label ?? item.value);
|
|
163
|
+
btn.type = 'button';
|
|
164
|
+
keys.push({ value: item.value, btn });
|
|
165
|
+
btn.addEventListener(
|
|
166
|
+
'click',
|
|
167
|
+
() => {
|
|
168
|
+
// 点已经选中的那颗:不回调。onSelect 后面挂的常是一次重新取数,
|
|
169
|
+
// 白刷一遍不只是浪费,还会把用户刚滚到的位置弹回去。
|
|
170
|
+
if (current === item.value) return;
|
|
171
|
+
current = item.value;
|
|
172
|
+
paint();
|
|
173
|
+
o.onSelect?.(item.value);
|
|
174
|
+
},
|
|
175
|
+
{ signal },
|
|
176
|
+
);
|
|
177
|
+
el.appendChild(btn);
|
|
178
|
+
}
|
|
179
|
+
paint();
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
el,
|
|
183
|
+
get value(): string {
|
|
184
|
+
return current;
|
|
185
|
+
},
|
|
186
|
+
setValue(v: string): void {
|
|
187
|
+
current = v;
|
|
188
|
+
paint();
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ConsoleFormat 提供不依赖 DOM 的纯格式化函数,供各控制台面板使用统一显示口径。
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ConsoleFormat } from '../../shared/client-panel.ts';
|
|
6
|
+
|
|
7
|
+
/** 补零到两位。`4` → `04` */
|
|
8
|
+
function pad2(n: number): string {
|
|
9
|
+
return n < 10 ? '0' + n : String(n);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const consoleFormat: ConsoleFormat = {
|
|
13
|
+
/** **满 10000 才转 k**(9999 仍是 9999,不是 10.0k) */
|
|
14
|
+
count(n) {
|
|
15
|
+
return n == null ? '—' : n >= 10000 ? (n / 1000).toFixed(1) + 'k' : String(n);
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
/** null 给空串(不是 `—`),单位是 1024 进制的 `B/K/M` */
|
|
19
|
+
bytes(n) {
|
|
20
|
+
return n == null
|
|
21
|
+
? ''
|
|
22
|
+
: n < 1024
|
|
23
|
+
? n + 'B'
|
|
24
|
+
: n < 1048576
|
|
25
|
+
? (n / 1024).toFixed(1) + 'K'
|
|
26
|
+
: (n / 1048576).toFixed(1) + 'M';
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
/** `0.42` → `42%`,四舍五入到整数 */
|
|
30
|
+
percent(r) {
|
|
31
|
+
return r == null ? '—' : Math.round(r * 100) + '%';
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* ISO 串直接切 `11..19` 位拿 `HH:MM:SS`。
|
|
36
|
+
* 不 `new Date()` 是有意的——服务端给的就是本地时间串,转一道反而会带上时区偏移。
|
|
37
|
+
* 非字符串或长度不足 19 → 空串。
|
|
38
|
+
*/
|
|
39
|
+
clock(ts) {
|
|
40
|
+
return typeof ts === 'string' && ts.length >= 19 ? ts.slice(11, 19) : '';
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* `<1` 保 4 位小数(单次调用成本常在 0.0x),`>=1` 保 2 位。
|
|
45
|
+
* 币种是 ISO 代码:`USD`(缺省)渲染成 `$`,其余代码原样前置并空一格(`EUR 1.00`)。
|
|
46
|
+
*/
|
|
47
|
+
money(n, currency) {
|
|
48
|
+
const code = currency || 'USD';
|
|
49
|
+
const prefix = code === 'USD' ? '$' : code + ' ';
|
|
50
|
+
return prefix + (n == null ? '0' : n < 1 ? n.toFixed(4) : n.toFixed(2));
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 毫秒时长,口径在这里定死:
|
|
55
|
+
*
|
|
56
|
+
* - `null` / 非有限数 → `—`(与 `count` / `percent` 一致,空值都是破折号)
|
|
57
|
+
* - 负数按 0 处理
|
|
58
|
+
* - `< 1s`:整毫秒,`999` → `999ms`
|
|
59
|
+
* - `< 1min`:一位小数,`1200` → `1.2s`
|
|
60
|
+
* - `< 1h`:`3m 04s`(秒补零,好竖排对齐)
|
|
61
|
+
* - 更长:`1h 00m`
|
|
62
|
+
*
|
|
63
|
+
* 先把 1s 以上的值**量化到 0.1s** 再分档,免得 `59999` 落进秒档印出 `60.0s`
|
|
64
|
+
* 这种自相矛盾的读数(量化后它是 `1m 00s`)。
|
|
65
|
+
*/
|
|
66
|
+
duration(ms) {
|
|
67
|
+
if (ms == null || !Number.isFinite(ms)) return '—';
|
|
68
|
+
const raw = ms < 0 ? 0 : ms;
|
|
69
|
+
const t = raw < 1000 ? Math.round(raw) : Math.round(raw / 100) * 100;
|
|
70
|
+
if (t < 1000) return t + 'ms';
|
|
71
|
+
if (t < 60_000) return (t / 1000).toFixed(1) + 's';
|
|
72
|
+
if (t < 3_600_000) return Math.floor(t / 60_000) + 'm ' + pad2(Math.floor((t % 60_000) / 1000)) + 's';
|
|
73
|
+
return Math.floor(t / 3_600_000) + 'h ' + pad2(Math.floor((t % 3_600_000) / 60_000)) + 'm';
|
|
74
|
+
},
|
|
75
|
+
};
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
2
|
+
|
|
3
|
+
export type ConsoleIconName =
|
|
4
|
+
| 'terminal'
|
|
5
|
+
| 'activity'
|
|
6
|
+
| 'chart'
|
|
7
|
+
| 'boxes'
|
|
8
|
+
| 'bot'
|
|
9
|
+
| 'settings'
|
|
10
|
+
| 'play'
|
|
11
|
+
| 'pause'
|
|
12
|
+
| 'power'
|
|
13
|
+
| 'image'
|
|
14
|
+
| 'folder-open'
|
|
15
|
+
| 'download'
|
|
16
|
+
| 'cpu'
|
|
17
|
+
| 'text'
|
|
18
|
+
| 'eye'
|
|
19
|
+
| 'eye-off'
|
|
20
|
+
| 'refresh';
|
|
21
|
+
|
|
22
|
+
type Shape = readonly [tag: 'path' | 'circle' | 'rect' | 'line', attrs: Readonly<Record<string, string>>];
|
|
23
|
+
|
|
24
|
+
const SHAPES: Readonly<Record<ConsoleIconName, readonly Shape[]>> = {
|
|
25
|
+
terminal: [
|
|
26
|
+
['path', { d: 'm4 17 6-6-6-6' }],
|
|
27
|
+
['path', { d: 'M12 19h8' }],
|
|
28
|
+
],
|
|
29
|
+
activity: [
|
|
30
|
+
['path', { d: 'M3 12h4l2-7 4 14 2-7h6' }],
|
|
31
|
+
],
|
|
32
|
+
chart: [
|
|
33
|
+
['path', { d: 'M4 19V9' }],
|
|
34
|
+
['path', { d: 'M10 19V5' }],
|
|
35
|
+
['path', { d: 'M16 19v-7' }],
|
|
36
|
+
['path', { d: 'M22 19H2' }],
|
|
37
|
+
],
|
|
38
|
+
boxes: [
|
|
39
|
+
['path', { d: 'm12 2 7 4-7 4-7-4 7-4Z' }],
|
|
40
|
+
['path', { d: 'm5 10 7 4 7-4' }],
|
|
41
|
+
['path', { d: 'm5 14 7 4 7-4' }],
|
|
42
|
+
],
|
|
43
|
+
bot: [
|
|
44
|
+
['rect', { x: '5', y: '7', width: '14', height: '12', rx: '3' }],
|
|
45
|
+
['path', { d: 'M12 3v4' }],
|
|
46
|
+
['circle', { cx: '9', cy: '13', r: '1' }],
|
|
47
|
+
['circle', { cx: '15', cy: '13', r: '1' }],
|
|
48
|
+
],
|
|
49
|
+
settings: [
|
|
50
|
+
['circle', { cx: '12', cy: '12', r: '3' }],
|
|
51
|
+
['path', { d: 'M19.4 15a1.7 1.7 0 0 0 .3 1.9l.1.1-2.8 2.8-.1-.1a1.7 1.7 0 0 0-1.9-.3 1.7 1.7 0 0 0-1 1.5V21h-4v-.1a1.7 1.7 0 0 0-1-1.5 1.7 1.7 0 0 0-1.9.3l-.1.1L4.2 17l.1-.1a1.7 1.7 0 0 0 .3-1.9 1.7 1.7 0 0 0-1.5-1H3v-4h.1a1.7 1.7 0 0 0 1.5-1 1.7 1.7 0 0 0-.3-1.9L4.2 7 7 4.2l.1.1A1.7 1.7 0 0 0 9 4.6a1.7 1.7 0 0 0 1-1.5V3h4v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.9-.3l.1-.1L19.8 7l-.1.1a1.7 1.7 0 0 0-.3 1.9 1.7 1.7 0 0 0 1.5 1h.1v4h-.1a1.7 1.7 0 0 0-1.5 1Z' }],
|
|
52
|
+
],
|
|
53
|
+
play: [['path', { d: 'm8 5 11 7-11 7V5Z' }]],
|
|
54
|
+
power: [
|
|
55
|
+
['path', { d: 'M12 3v8' }],
|
|
56
|
+
['path', { d: 'M17.7 6.3a8 8 0 1 1-11.4 0' }],
|
|
57
|
+
],
|
|
58
|
+
pause: [
|
|
59
|
+
['rect', { x: '7', y: '5', width: '3', height: '14', rx: '1' }],
|
|
60
|
+
['rect', { x: '14', y: '5', width: '3', height: '14', rx: '1' }],
|
|
61
|
+
],
|
|
62
|
+
image: [
|
|
63
|
+
['rect', { x: '3', y: '4', width: '18', height: '16', rx: '3' }],
|
|
64
|
+
['circle', { cx: '9', cy: '10', r: '2' }],
|
|
65
|
+
['path', { d: 'm21 15-4-4L5 20' }],
|
|
66
|
+
],
|
|
67
|
+
'folder-open': [
|
|
68
|
+
['path', { d: 'M3 6h6l2 2h10' }],
|
|
69
|
+
['path', { d: 'M3 6v13h15l3-8H6l-3 8' }],
|
|
70
|
+
],
|
|
71
|
+
download: [
|
|
72
|
+
['path', { d: 'M12 3v12' }],
|
|
73
|
+
['path', { d: 'm7 10 5 5 5-5' }],
|
|
74
|
+
['path', { d: 'M5 21h14' }],
|
|
75
|
+
],
|
|
76
|
+
cpu: [
|
|
77
|
+
['rect', { x: '5', y: '5', width: '14', height: '14', rx: '2' }],
|
|
78
|
+
['rect', { x: '9', y: '9', width: '6', height: '6', rx: '1' }],
|
|
79
|
+
['path', { d: 'M9 2v3M15 2v3M9 19v3M15 19v3M2 9h3M2 15h3M19 9h3M19 15h3' }],
|
|
80
|
+
],
|
|
81
|
+
text: [
|
|
82
|
+
['path', { d: 'M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z' }],
|
|
83
|
+
['path', { d: 'M14 2v4a2 2 0 0 0 2 2h4' }],
|
|
84
|
+
['path', { d: 'M10 9H8M16 13H8M16 17H8' }],
|
|
85
|
+
],
|
|
86
|
+
eye: [
|
|
87
|
+
['path', { d: 'M2 12s3.5-6 10-6 10 6 10 6-3.5 6-10 6S2 12 2 12Z' }],
|
|
88
|
+
['circle', { cx: '12', cy: '12', r: '3' }],
|
|
89
|
+
],
|
|
90
|
+
'eye-off': [
|
|
91
|
+
['path', { d: 'M3 3l18 18' }],
|
|
92
|
+
['path', { d: 'M10.6 5.2A10.9 10.9 0 0 1 12 5c6.5 0 10 7 10 7a17.3 17.3 0 0 1-3.2 4' }],
|
|
93
|
+
['path', { d: 'M6.6 6.6C3.8 8.5 2 12 2 12s3.5 7 10 7c1.6 0 3-.4 4.3-1' }],
|
|
94
|
+
['path', { d: 'M9.9 9.9a3 3 0 0 0 4.2 4.2' }],
|
|
95
|
+
],
|
|
96
|
+
refresh: [
|
|
97
|
+
['path', { d: 'M21 12a9 9 0 1 1-2.6-6.4' }],
|
|
98
|
+
['path', { d: 'M21 3v6h-6' }],
|
|
99
|
+
],
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Cortico 字标。笔画与 README 横幅(assets/cortico-banner.svg)是同一份。
|
|
104
|
+
* 横幅上那四个圈(两只眼睛与两个字母 o)是品牌绿,这里改成跟当前主题的强调色走,
|
|
105
|
+
* 其余笔画取 currentColor。颜色写在样式表里:presentation 属性吃不下 var()。
|
|
106
|
+
*/
|
|
107
|
+
const WORDMARK_MARK: readonly Shape[] = [
|
|
108
|
+
['path', { d: 'M182 63.66A84 84 0 1 0 182 192.34', 'stroke-width': '30' }],
|
|
109
|
+
['circle', { cx: '113', cy: '117', r: '16', 'stroke-width': '11', class: 'accent' }],
|
|
110
|
+
['circle', { cx: '163', cy: '117', r: '16', 'stroke-width': '11', class: 'accent' }],
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
const WORDMARK_TEXT: readonly Shape[] = [
|
|
114
|
+
['path', { d: 'M230.97 93.03A24 24 0 1 0 230.97 126.97' }],
|
|
115
|
+
['circle', { cx: '286', cy: '110', r: '24', class: 'accent' }],
|
|
116
|
+
['path', { d: 'M341 86V134' }],
|
|
117
|
+
['path', { d: 'M341 112A24 24 0 0 1 365 88' }],
|
|
118
|
+
['path', { d: 'M412 70V134' }],
|
|
119
|
+
['path', { d: 'M396 78H428' }],
|
|
120
|
+
['path', { d: 'M459 86V134' }],
|
|
121
|
+
['path', { d: 'M530.97 93.03A24 24 0 1 0 530.97 126.97' }],
|
|
122
|
+
['circle', { cx: '586', cy: '110', r: '24', class: 'accent' }],
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
function shapesInto(doc: Document, parent: SVGElement, shapes: readonly Shape[]): void {
|
|
126
|
+
for (const [tag, attrs] of shapes) {
|
|
127
|
+
const child = doc.createElementNS(SVG_NS, tag);
|
|
128
|
+
for (const [key, value] of Object.entries(attrs)) child.setAttribute(key, value);
|
|
129
|
+
parent.appendChild(child);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function wordmark(doc: Document, cls = 'wordmark'): SVGSVGElement {
|
|
134
|
+
const svg = doc.createElementNS(SVG_NS, 'svg');
|
|
135
|
+
svg.setAttribute('viewBox', '28 32 594 156');
|
|
136
|
+
svg.setAttribute('fill', 'none');
|
|
137
|
+
svg.setAttribute('stroke', 'currentColor');
|
|
138
|
+
svg.setAttribute('stroke-linecap', 'round');
|
|
139
|
+
svg.setAttribute('aria-hidden', 'true');
|
|
140
|
+
svg.classList.add(cls);
|
|
141
|
+
// 字母前那个符号自带一套尺度,照横幅原样缩放,不重算坐标。
|
|
142
|
+
const mark = doc.createElementNS(SVG_NS, 'g');
|
|
143
|
+
mark.setAttribute('transform', 'translate(10 14) scale(.75)');
|
|
144
|
+
shapesInto(doc, mark, WORDMARK_MARK);
|
|
145
|
+
const text = doc.createElementNS(SVG_NS, 'g');
|
|
146
|
+
text.setAttribute('stroke-width', '16');
|
|
147
|
+
shapesInto(doc, text, WORDMARK_TEXT);
|
|
148
|
+
const dot = doc.createElementNS(SVG_NS, 'circle');
|
|
149
|
+
for (const [key, value] of Object.entries({ cx: '459', cy: '58', r: '9', fill: 'currentColor', stroke: 'none' })) {
|
|
150
|
+
dot.setAttribute(key, value);
|
|
151
|
+
}
|
|
152
|
+
svg.appendChild(mark);
|
|
153
|
+
svg.appendChild(text);
|
|
154
|
+
svg.appendChild(dot);
|
|
155
|
+
return svg;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** 字标前那个符号单独用。视框按它自己的坐标取,描边算在内。 */
|
|
159
|
+
export function brandMark(doc: Document, cls = 'brandmark'): SVGSVGElement {
|
|
160
|
+
const svg = doc.createElementNS(SVG_NS, 'svg');
|
|
161
|
+
svg.setAttribute('viewBox', '29 29 198 198');
|
|
162
|
+
svg.setAttribute('fill', 'none');
|
|
163
|
+
svg.setAttribute('stroke', 'currentColor');
|
|
164
|
+
svg.setAttribute('stroke-linecap', 'round');
|
|
165
|
+
svg.setAttribute('aria-hidden', 'true');
|
|
166
|
+
svg.setAttribute('class', cls);
|
|
167
|
+
shapesInto(doc, svg, WORDMARK_MARK);
|
|
168
|
+
return svg;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function icon(doc: Document, name: ConsoleIconName, cls = 'icon'): SVGSVGElement {
|
|
172
|
+
const svg = doc.createElementNS(SVG_NS, 'svg');
|
|
173
|
+
svg.setAttribute('viewBox', '0 0 24 24');
|
|
174
|
+
svg.setAttribute('fill', 'none');
|
|
175
|
+
svg.setAttribute('stroke', 'currentColor');
|
|
176
|
+
svg.setAttribute('stroke-width', '1.8');
|
|
177
|
+
svg.setAttribute('stroke-linecap', 'round');
|
|
178
|
+
svg.setAttribute('stroke-linejoin', 'round');
|
|
179
|
+
svg.setAttribute('aria-hidden', 'true');
|
|
180
|
+
svg.setAttribute('class', cls);
|
|
181
|
+
shapesInto(doc, svg, SHAPES[name]);
|
|
182
|
+
return svg;
|
|
183
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/** 归一化输入图片:只缩不放,合规原文件保持原字节。缩放后超限时改用 JPEG,仍超限或解码失败则拒收。 */
|
|
2
|
+
|
|
3
|
+
import type { ConsoleImageAttachment, ConsolePromptImagesOptions } from '../../shared/client-panel.ts';
|
|
4
|
+
import { S } from './strings.ts';
|
|
5
|
+
|
|
6
|
+
export const IMAGE_MIMES: readonly string[] = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
|
|
7
|
+
|
|
8
|
+
export const IMAGE_DEFAULTS: Required<ConsolePromptImagesOptions> = {
|
|
9
|
+
max: 8,
|
|
10
|
+
maxEdge: 2048,
|
|
11
|
+
maxBytes: 6 * 1024 * 1024,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function isImageFile(file: { type: string }): boolean {
|
|
15
|
+
return IMAGE_MIMES.includes(file.type);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** 只缩不放:长边不超过 `maxEdge` 的原样返回;缩放保持比例,四舍五入且不小于 1。 */
|
|
19
|
+
export function fitWithin(width: number, height: number, maxEdge: number): { width: number; height: number } {
|
|
20
|
+
const edge = Math.max(width, height);
|
|
21
|
+
if (!(edge > maxEdge) || maxEdge <= 0) return { width, height };
|
|
22
|
+
const scale = maxEdge / edge;
|
|
23
|
+
return {
|
|
24
|
+
width: Math.max(1, Math.round(width * scale)),
|
|
25
|
+
height: Math.max(1, Math.round(height * scale)),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function toBase64(blob: Blob): Promise<string> {
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
const reader = new FileReader();
|
|
32
|
+
reader.onerror = () => reject(reader.error ?? new Error(S.imageReadFailed));
|
|
33
|
+
reader.onload = () => {
|
|
34
|
+
const url = String(reader.result ?? '');
|
|
35
|
+
resolve(url.slice(url.indexOf(',') + 1));
|
|
36
|
+
};
|
|
37
|
+
reader.readAsDataURL(blob);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function encode(bitmap: ImageBitmap, width: number, height: number, mime: string, quality?: number): Promise<Blob> {
|
|
42
|
+
const canvas = document.createElement('canvas');
|
|
43
|
+
canvas.width = width;
|
|
44
|
+
canvas.height = height;
|
|
45
|
+
const ctx = canvas.getContext('2d');
|
|
46
|
+
if (!ctx) throw new Error(S.canvasUnsupported);
|
|
47
|
+
ctx.drawImage(bitmap, 0, 0, width, height);
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
canvas.toBlob((blob) => (blob ? resolve(blob) : reject(new Error(S.imageEncodeFailed))), mime, quality);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 归一化一张图。拒收(不是图片、解码失败、缩到上限后仍超字节上限)以异常表达,
|
|
55
|
+
* 消息可直接给用户看。
|
|
56
|
+
*/
|
|
57
|
+
export async function normalizeImage(
|
|
58
|
+
file: File,
|
|
59
|
+
opts: ConsolePromptImagesOptions = {},
|
|
60
|
+
): Promise<ConsoleImageAttachment> {
|
|
61
|
+
const { maxEdge, maxBytes } = { ...IMAGE_DEFAULTS, ...opts };
|
|
62
|
+
if (!isImageFile(file)) throw new Error(S.notImage(file.name || S.fileFallback));
|
|
63
|
+
let bitmap: ImageBitmap;
|
|
64
|
+
try {
|
|
65
|
+
bitmap = await createImageBitmap(file);
|
|
66
|
+
} catch {
|
|
67
|
+
throw new Error(S.undecodable(file.name || S.imageFallback));
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
const name = file.name || 'image';
|
|
71
|
+
const fitted = fitWithin(bitmap.width, bitmap.height, maxEdge);
|
|
72
|
+
const untouched = fitted.width === bitmap.width && fitted.height === bitmap.height && file.size <= maxBytes;
|
|
73
|
+
if (untouched) {
|
|
74
|
+
return { name, mime: file.type, base64: await toBase64(file), bytes: file.size, width: bitmap.width, height: bitmap.height };
|
|
75
|
+
}
|
|
76
|
+
// 缩放后先试原格式(GIF 没有 canvas 编码器,退 PNG);超字节上限再退 JPEG。
|
|
77
|
+
const preferred = file.type === 'image/gif' ? 'image/png' : file.type;
|
|
78
|
+
let blob = await encode(bitmap, fitted.width, fitted.height, preferred);
|
|
79
|
+
let mime = preferred;
|
|
80
|
+
if (blob.size > maxBytes && preferred !== 'image/jpeg') {
|
|
81
|
+
blob = await encode(bitmap, fitted.width, fitted.height, 'image/jpeg', 0.85);
|
|
82
|
+
mime = 'image/jpeg';
|
|
83
|
+
}
|
|
84
|
+
if (blob.size > maxBytes) {
|
|
85
|
+
throw new Error(S.stillTooLarge(name, Math.round(maxBytes / 1024 / 1024)));
|
|
86
|
+
}
|
|
87
|
+
return { name, mime, base64: await toBase64(blob), bytes: blob.size, width: fitted.width, height: fitted.height };
|
|
88
|
+
} finally {
|
|
89
|
+
bitmap.close();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/** 每次面板挂载创建 ConsoleUi,绑定 memo 命名空间、signal 与浮层宿主。 */
|
|
2
|
+
|
|
3
|
+
import type { ConsoleMemo, ConsoleUi, Disposable } from '../../shared/client-panel.ts';
|
|
4
|
+
import { esc, h } from './dom.ts';
|
|
5
|
+
import { consoleFormat } from './format.ts';
|
|
6
|
+
import { kv, progress, statgrid, stat, table } from './data.ts';
|
|
7
|
+
import { checkbox, field, input, segmented, select, textarea } from './fields.ts';
|
|
8
|
+
import { copyButton, disable } from './actions.ts';
|
|
9
|
+
import { log } from './log.ts';
|
|
10
|
+
import { promptInput } from './prompt-input.tsx';
|
|
11
|
+
import {
|
|
12
|
+
actions,
|
|
13
|
+
button,
|
|
14
|
+
chip,
|
|
15
|
+
foldSheet,
|
|
16
|
+
msgline,
|
|
17
|
+
pill,
|
|
18
|
+
placeholder,
|
|
19
|
+
rowbar,
|
|
20
|
+
section,
|
|
21
|
+
sheet,
|
|
22
|
+
} from './sheet.ts';
|
|
23
|
+
import type { Overlay, OverlayEnv } from './overlay.ts';
|
|
24
|
+
import { busy, confirm, drawer, noopDisposable, toast } from './overlay.ts';
|
|
25
|
+
|
|
26
|
+
export interface ConsoleUiDeps {
|
|
27
|
+
/** 面板局部持久化。`foldSheet` 的展开状态存这儿。 */
|
|
28
|
+
memo: ConsoleMemo;
|
|
29
|
+
/**
|
|
30
|
+
* toast / confirm / drawer 的挂载点。host 传 `document.body` 或一个专用容器。
|
|
31
|
+
* **不能是面板的 `root`**——那个在 unmount 时会被清空。
|
|
32
|
+
*/
|
|
33
|
+
overlayHost: HTMLElement;
|
|
34
|
+
/** 面板的 ctx.signal,用于取消弹窗与释放资源。 */
|
|
35
|
+
signal: AbortSignal;
|
|
36
|
+
/** 缺省为 overlayHost.ownerDocument。 */
|
|
37
|
+
doc?: Document;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function createConsoleUi(deps: ConsoleUiDeps): ConsoleUi {
|
|
41
|
+
const doc = deps.doc ?? deps.overlayHost.ownerDocument;
|
|
42
|
+
const signal = deps.signal;
|
|
43
|
+
const env: OverlayEnv = { doc, host: deps.overlayHost, signal };
|
|
44
|
+
// 同一实例只留一条 toast:后来的顶掉前面的,免得叠成一摞
|
|
45
|
+
let liveToast: Overlay | null = null;
|
|
46
|
+
// 抽出来是因为 copyButton 也要弹 toast,而"只留一条"这条规矩记在上面那个闭包里
|
|
47
|
+
const showToast = (text: string, tone?: 'ok' | 'bad'): Disposable => {
|
|
48
|
+
liveToast = toast(env, text, tone, liveToast);
|
|
49
|
+
// abort 之后 toast 不显示,但仍要给回一个句柄:调用方 `.dispose()` 不该炸
|
|
50
|
+
return liveToast ?? noopDisposable;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
h: (tag, cls, text) => h(doc, tag, cls, text),
|
|
55
|
+
esc,
|
|
56
|
+
sheet: (opts) => sheet(doc, opts),
|
|
57
|
+
foldSheet: (id, opts) => foldSheet(doc, deps.memo, signal, id, opts),
|
|
58
|
+
rowbar: () => rowbar(doc),
|
|
59
|
+
section: (title, description) => section(doc, title, description),
|
|
60
|
+
actions: () => actions(doc),
|
|
61
|
+
button: (label, opts) => button(doc, signal, label, opts),
|
|
62
|
+
copyButton: (text, opts) => copyButton(env, showToast, text, opts),
|
|
63
|
+
pill: (text, tone) => pill(doc, text, tone),
|
|
64
|
+
chip: (text, tone) => chip(doc, text, tone),
|
|
65
|
+
msgline: (text, bad) => msgline(doc, text, bad),
|
|
66
|
+
placeholder: (text) => placeholder(doc, text),
|
|
67
|
+
input: (opts) => input(doc, signal, opts),
|
|
68
|
+
select: (opts) => select(doc, signal, opts),
|
|
69
|
+
textarea: (opts) => textarea(doc, signal, opts),
|
|
70
|
+
promptInput: (opts) => promptInput(doc, signal, opts),
|
|
71
|
+
checkbox: (label, opts) => checkbox(doc, signal, label, opts),
|
|
72
|
+
field: (label, control) => field(doc, label, control),
|
|
73
|
+
segmented: (items, opts) => segmented(doc, signal, items, opts),
|
|
74
|
+
table: (opts) => table(doc, opts),
|
|
75
|
+
kv: (rows) => kv(doc, rows),
|
|
76
|
+
log: (opts) => log(doc, signal, opts),
|
|
77
|
+
stat: (item) => stat(doc, item),
|
|
78
|
+
statgrid: (items) => statgrid(doc, items),
|
|
79
|
+
progress: (opts) => progress(doc, opts),
|
|
80
|
+
toast: showToast,
|
|
81
|
+
confirm: (opts) => confirm(env, opts),
|
|
82
|
+
drawer: (title, body) => drawer(env, title, body),
|
|
83
|
+
busy: (title, text) => busy(env, title, text),
|
|
84
|
+
disable: (...els) => disable(els),
|
|
85
|
+
fmt: consoleFormat,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export { LOG_STICK_PX, shouldStick } from './log.ts';
|