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
|
@@ -32,12 +32,88 @@ export interface SearchHitView {
|
|
|
32
32
|
description: string;
|
|
33
33
|
date?: string;
|
|
34
34
|
publisher?: string;
|
|
35
|
+
license?: string;
|
|
36
|
+
keywords?: string[];
|
|
35
37
|
downloads: number;
|
|
38
|
+
dependents: number;
|
|
36
39
|
links: { npm?: string; repository?: string; homepage?: string };
|
|
37
40
|
installed: boolean;
|
|
38
41
|
kind?: ExtensionKindView;
|
|
39
42
|
}
|
|
40
43
|
|
|
44
|
+
/** 与 `ExtensionPackageDetail` 同形。点开一张结果卡时才取。 */
|
|
45
|
+
export interface PackageDetailView {
|
|
46
|
+
name: string;
|
|
47
|
+
version: string;
|
|
48
|
+
description?: string;
|
|
49
|
+
license?: string;
|
|
50
|
+
keywords?: string[];
|
|
51
|
+
published?: string;
|
|
52
|
+
created?: string;
|
|
53
|
+
versionCount: number;
|
|
54
|
+
history: Array<{ version: string; date: string }>;
|
|
55
|
+
deprecated?: string;
|
|
56
|
+
manifest?: { kind: ExtensionKindView; api: number; consoleClient?: string; consoleStyle?: string };
|
|
57
|
+
problems?: string[];
|
|
58
|
+
warnings: string[];
|
|
59
|
+
frameworkApi: number;
|
|
60
|
+
engines?: string;
|
|
61
|
+
unpackedSize?: number;
|
|
62
|
+
fileCount?: number;
|
|
63
|
+
dependencies: string[];
|
|
64
|
+
maintainers: string[];
|
|
65
|
+
publisher?: string;
|
|
66
|
+
links: { npm: string; repository?: string; homepage?: string; bugs?: string };
|
|
67
|
+
installed: boolean;
|
|
68
|
+
installedSpec?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** 结果的排序口径。npm 自己的相关度不在其中:同一关键字下的包它给的分全是 0。 */
|
|
72
|
+
export type HitSort = 'downloads' | 'date' | 'name' | 'dependents';
|
|
73
|
+
export const HIT_SORTS: readonly HitSort[] = ['downloads', 'date', 'name', 'dependents'];
|
|
74
|
+
|
|
75
|
+
/** 一页放几张卡。 */
|
|
76
|
+
export const HITS_PER_PAGE = 12;
|
|
77
|
+
|
|
78
|
+
export interface ArrangeOptions {
|
|
79
|
+
filter: string;
|
|
80
|
+
hideInstalled: boolean;
|
|
81
|
+
sort: HitSort;
|
|
82
|
+
page: number;
|
|
83
|
+
pageSize: number;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 在整份结果上筛选、排序、切页。registry 的搜索端点在 `keywords:` 过滤下不按文本
|
|
88
|
+
* 缩小结果,所以文本匹配在这里做:包名、描述、关键字任一命中即算。
|
|
89
|
+
* `page` 超出范围时收回最后一页,返回值里的 `page` 是实际用的那一页。
|
|
90
|
+
*/
|
|
91
|
+
export function arrangeHits(
|
|
92
|
+
hits: readonly SearchHitView[],
|
|
93
|
+
opts: ArrangeOptions,
|
|
94
|
+
): { matched: number; pages: number; page: number; shown: SearchHitView[] } {
|
|
95
|
+
const needle = opts.filter.trim().toLowerCase();
|
|
96
|
+
const matched = hits.filter((h) => {
|
|
97
|
+
if (opts.hideInstalled && h.installed) return false;
|
|
98
|
+
if (!needle) return true;
|
|
99
|
+
return h.name.toLowerCase().includes(needle)
|
|
100
|
+
|| h.description.toLowerCase().includes(needle)
|
|
101
|
+
|| (h.keywords ?? []).some((k) => k.toLowerCase().includes(needle));
|
|
102
|
+
});
|
|
103
|
+
matched.sort((a, b) => {
|
|
104
|
+
switch (opts.sort) {
|
|
105
|
+
case 'downloads': return b.downloads - a.downloads || a.name.localeCompare(b.name);
|
|
106
|
+
// 发布时间是 ISO 串,按串比就是按时间比;没有日期的排在最后
|
|
107
|
+
case 'date': return (b.date ?? '').localeCompare(a.date ?? '') || a.name.localeCompare(b.name);
|
|
108
|
+
case 'dependents': return b.dependents - a.dependents || a.name.localeCompare(b.name);
|
|
109
|
+
default: return a.name.localeCompare(b.name);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
const pages = Math.max(1, Math.ceil(matched.length / opts.pageSize));
|
|
113
|
+
const page = Math.min(Math.max(opts.page, 0), pages - 1);
|
|
114
|
+
return { matched: matched.length, pages, page, shown: matched.slice(page * opts.pageSize, (page + 1) * opts.pageSize) };
|
|
115
|
+
}
|
|
116
|
+
|
|
41
117
|
interface PowerReport {
|
|
42
118
|
ok?: boolean;
|
|
43
119
|
localComplete?: boolean;
|
|
@@ -187,6 +263,7 @@ export function mountExtensions(ctx: FeatureContext): void {
|
|
|
187
263
|
const out = await post<{ result?: string }>('/api/extensions/uninstall', { name: p.name }, { signal: ctx.signal });
|
|
188
264
|
if (ctx.signal.aborted) return;
|
|
189
265
|
setMsg(out?.result?.split('\n')[0] || S.uninstalled);
|
|
266
|
+
markHit(p.name, false);
|
|
190
267
|
await load();
|
|
191
268
|
} catch (err) {
|
|
192
269
|
if (isAbort(err) || ctx.signal.aborted) return;
|
|
@@ -198,32 +275,35 @@ export function mountExtensions(ctx: FeatureContext): void {
|
|
|
198
275
|
}
|
|
199
276
|
|
|
200
277
|
/** 装完问一句要不要顺手重启;答"否"也留在清单里标「待重启」。 */
|
|
201
|
-
|
|
278
|
+
/** 返回值是"装上了没有":搜索结果与详细页按它更新自己的已安装标记。 */
|
|
279
|
+
async function install(target: { name: string; version?: string } | { path: string }, btn?: HTMLButtonElement): Promise<boolean> {
|
|
202
280
|
const lock = btn ? ui.disable(btn) : null;
|
|
203
281
|
const hold = ui.toast(S.installing);
|
|
204
282
|
let result = '';
|
|
205
283
|
try {
|
|
206
284
|
const out = await post<{ result?: string }>('/api/extensions/install', target, { signal: ctx.signal });
|
|
207
|
-
if (ctx.signal.aborted) return;
|
|
285
|
+
if (ctx.signal.aborted) return false;
|
|
208
286
|
result = out?.result ?? S.installed;
|
|
209
287
|
setMsg(result.split('\n')[0]);
|
|
288
|
+
if ('name' in target) markHit(target.name, true);
|
|
210
289
|
await load();
|
|
211
290
|
} catch (err) {
|
|
212
|
-
if (isAbort(err) || ctx.signal.aborted) return;
|
|
291
|
+
if (isAbort(err) || ctx.signal.aborted) return false;
|
|
213
292
|
setMsg(S.installFailed(errText(err)), true);
|
|
214
|
-
return;
|
|
293
|
+
return false;
|
|
215
294
|
} finally {
|
|
216
295
|
hold.dispose();
|
|
217
296
|
lock?.dispose();
|
|
218
297
|
}
|
|
219
|
-
if (!canRestart) return;
|
|
298
|
+
if (!canRestart) return true;
|
|
220
299
|
const go = await ui.confirm({
|
|
221
300
|
title: S.installedRestartTitle,
|
|
222
301
|
body: result + '\n\n' + (supervised ? S.installedRestartNote : S.installedNoLoopNote),
|
|
223
302
|
danger: !supervised,
|
|
224
303
|
});
|
|
225
|
-
if (!go || ctx.signal.aborted) return;
|
|
304
|
+
if (!go || ctx.signal.aborted) return true;
|
|
226
305
|
await restartProcess(undefined, true);
|
|
306
|
+
return true;
|
|
227
307
|
}
|
|
228
308
|
|
|
229
309
|
function extensionCard(p: ExtensionView): HTMLElement {
|
|
@@ -299,6 +379,13 @@ export function mountExtensions(ctx: FeatureContext): void {
|
|
|
299
379
|
desc: S.searchDesc,
|
|
300
380
|
});
|
|
301
381
|
let searchKind: ExtensionKindView = 'world';
|
|
382
|
+
/** 当前这一类在 npm 上的全部包。筛选、排序、分页都在它上面做,不再往 registry 跑。 */
|
|
383
|
+
let allHits: SearchHitView[] = [];
|
|
384
|
+
let filter = '';
|
|
385
|
+
let sort: HitSort = 'downloads';
|
|
386
|
+
let hideInstalled = false;
|
|
387
|
+
let page = 0;
|
|
388
|
+
|
|
302
389
|
const searchBar = ui.rowbar();
|
|
303
390
|
const kindSeg = ui.segmented(
|
|
304
391
|
[
|
|
@@ -312,69 +399,126 @@ export function mountExtensions(ctx: FeatureContext): void {
|
|
|
312
399
|
onSelect: (v) => {
|
|
313
400
|
searchKind = v as ExtensionKindView;
|
|
314
401
|
paintKeyword();
|
|
315
|
-
|
|
316
|
-
resultGrid.replaceChildren();
|
|
317
|
-
searchMsg.textContent = '';
|
|
402
|
+
void search();
|
|
318
403
|
},
|
|
319
404
|
},
|
|
320
405
|
);
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
406
|
+
const filterInput = ui.input({
|
|
407
|
+
type: 'search',
|
|
408
|
+
placeholder: S.filterPlaceholder,
|
|
409
|
+
onInput: (v) => {
|
|
410
|
+
filter = v;
|
|
411
|
+
page = 0;
|
|
412
|
+
renderHits();
|
|
413
|
+
},
|
|
414
|
+
});
|
|
415
|
+
const sortSelect = ui.select({
|
|
416
|
+
value: sort,
|
|
417
|
+
options: HIT_SORTS.map((value) => ({ value, label: S.sortLabel[value] })),
|
|
418
|
+
onChange: (v) => {
|
|
419
|
+
sort = v as HitSort;
|
|
420
|
+
page = 0;
|
|
421
|
+
renderHits();
|
|
422
|
+
},
|
|
423
|
+
});
|
|
424
|
+
const hideBox = ui.checkbox(S.hideInstalled, {
|
|
425
|
+
checked: hideInstalled,
|
|
426
|
+
onChange: (on) => {
|
|
427
|
+
hideInstalled = on;
|
|
428
|
+
page = 0;
|
|
429
|
+
renderHits();
|
|
430
|
+
},
|
|
431
|
+
});
|
|
432
|
+
const searchBtn = ui.button(S.refresh, { size: 'sm', onClick: () => void search() });
|
|
433
|
+
searchBar.append(kindSeg.el, filterInput, sortSelect, hideBox.el);
|
|
434
|
+
// 关键字说明与重取键共一行:上面那行控件已经占满,刷新键挤下去会单独占一行
|
|
435
|
+
const keywordBar = ui.rowbar();
|
|
325
436
|
const keywordLine = ui.msgline();
|
|
326
437
|
function paintKeyword(): void {
|
|
327
438
|
keywordLine.textContent = S.searchKeywordNote(KIND_KEYWORD[searchKind]);
|
|
328
439
|
}
|
|
329
440
|
paintKeyword();
|
|
441
|
+
keywordBar.append(keywordLine, ui.h('span', 'grow'), searchBtn);
|
|
330
442
|
const searchMsg = ui.msgline();
|
|
331
443
|
const resultGrid = ui.h('div', 'iogrid');
|
|
332
|
-
|
|
444
|
+
const pager = ui.rowbar();
|
|
445
|
+
searchSheet.body.append(searchBar, keywordBar, searchMsg, resultGrid, pager);
|
|
333
446
|
|
|
334
447
|
function openLink(href: string): void {
|
|
335
448
|
view?.open(href, '_blank', 'noopener');
|
|
336
449
|
}
|
|
337
450
|
|
|
451
|
+
/** 结果卡只放搜索端点给得起的那几项;契约版本、体积、版本史等着操作员点开再取。 */
|
|
338
452
|
function hitCard(h: SearchHitView): HTMLElement {
|
|
339
|
-
const card = ui.sheet({ title: h.name, en: S.hitMeta(h.version, h.downloads, h.publisher) });
|
|
340
|
-
card.el.classList.add('iocard');
|
|
341
|
-
if (h.
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
if (h.description) card.body.appendChild(ui.
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
actions.appendChild(installBtn);
|
|
359
|
-
card.body.appendChild(actions);
|
|
453
|
+
const card = ui.sheet({ title: h.name, en: S.hitMeta(h.version, ui.fmt.count(h.downloads), h.publisher) });
|
|
454
|
+
card.el.classList.add('iocard', 'iocard-open');
|
|
455
|
+
if (h.installed) card.el.classList.add('is-installed');
|
|
456
|
+
card.el.tabIndex = 0;
|
|
457
|
+
card.el.setAttribute('role', 'button');
|
|
458
|
+
card.el.title = S.openDetail;
|
|
459
|
+
// 描述走中性灰(与 provider 卡一致),msgline 的绿留给状态
|
|
460
|
+
if (h.description) card.body.appendChild(ui.h('div', 'tdesc', h.description));
|
|
461
|
+
const bar = ui.rowbar();
|
|
462
|
+
if (h.installed) bar.appendChild(ui.pill(S.alreadyInstalled, 'on'));
|
|
463
|
+
if (h.license) bar.appendChild(ui.chip(h.license));
|
|
464
|
+
if (h.date) bar.appendChild(ui.chip(h.date.slice(0, 10)));
|
|
465
|
+
if (h.dependents > 0) bar.appendChild(ui.chip(S.dependents(h.dependents)));
|
|
466
|
+
card.body.appendChild(bar);
|
|
467
|
+
const open = (): void => openDetail(h);
|
|
468
|
+
card.el.addEventListener('click', open, { signal: ctx.signal });
|
|
469
|
+
card.el.addEventListener('keydown', (ev: KeyboardEvent) => {
|
|
470
|
+
if (ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); open(); }
|
|
471
|
+
}, { signal: ctx.signal });
|
|
360
472
|
return card.el;
|
|
361
473
|
}
|
|
362
474
|
|
|
475
|
+
/** 筛选、排序、翻页一次画完,含结果计数与页脚。 */
|
|
476
|
+
function renderHits(): void {
|
|
477
|
+
const arranged = arrangeHits(allHits, { filter, hideInstalled, sort, page, pageSize: HITS_PER_PAGE });
|
|
478
|
+
page = arranged.page;
|
|
479
|
+
resultGrid.replaceChildren();
|
|
480
|
+
for (const h of arranged.shown) resultGrid.appendChild(hitCard(h));
|
|
481
|
+
if (arranged.matched === 0) {
|
|
482
|
+
resultGrid.appendChild(ui.placeholder(allHits.length === 0 ? S.noPackages : S.noHits));
|
|
483
|
+
}
|
|
484
|
+
searchMsg.className = 'msgline';
|
|
485
|
+
searchMsg.textContent = arranged.matched === allHits.length
|
|
486
|
+
? S.hitCount(allHits.length)
|
|
487
|
+
: S.hitCountFiltered(arranged.matched, allHits.length);
|
|
488
|
+
|
|
489
|
+
pager.replaceChildren();
|
|
490
|
+
if (arranged.pages <= 1) return;
|
|
491
|
+
const prev = ui.button(S.prevPage, { size: 'sm', onClick: () => { page -= 1; renderHits(); } });
|
|
492
|
+
const next = ui.button(S.nextPage, { size: 'sm', onClick: () => { page += 1; renderHits(); } });
|
|
493
|
+
prev.disabled = arranged.page === 0;
|
|
494
|
+
next.disabled = arranged.page >= arranged.pages - 1;
|
|
495
|
+
pager.append(prev, ui.chip(S.pageOf(arranged.page + 1, arranged.pages)), next);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/** 装完、卸完之后就地更新这一份结果里的已安装标记,不必再往 registry 跑一趟。 */
|
|
499
|
+
function markHit(name: string, installed: boolean): void {
|
|
500
|
+
let touched = false;
|
|
501
|
+
for (const h of allHits) if (h.name === name && h.installed !== installed) { h.installed = installed; touched = true; }
|
|
502
|
+
if (touched) renderHits();
|
|
503
|
+
}
|
|
504
|
+
|
|
363
505
|
async function search(): Promise<void> {
|
|
364
506
|
const lock = ui.disable(searchBtn);
|
|
365
507
|
searchMsg.textContent = S.searching;
|
|
366
508
|
searchMsg.className = 'msgline';
|
|
509
|
+
// 换了类就换了关键字:上一类的命中留在屏幕上会被当成这一类的结果
|
|
510
|
+
resultGrid.replaceChildren();
|
|
511
|
+
pager.replaceChildren();
|
|
512
|
+
allHits = [];
|
|
513
|
+
page = 0;
|
|
367
514
|
try {
|
|
368
|
-
const q = encodeURIComponent(searchInput.value.trim());
|
|
369
515
|
const data = await get<{ hits?: SearchHitView[] }>(
|
|
370
|
-
`/api/extensions/search?
|
|
516
|
+
`/api/extensions/search?kind=${searchKind}`,
|
|
371
517
|
{ signal: ctx.signal },
|
|
372
518
|
);
|
|
373
519
|
if (ctx.signal.aborted) return;
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
searchMsg.textContent = hits.length === 0 ? S.noHits : S.hitCount(hits.length);
|
|
377
|
-
for (const h of hits) resultGrid.appendChild(hitCard(h));
|
|
520
|
+
allHits = Array.isArray(data?.hits) ? data.hits : [];
|
|
521
|
+
renderHits();
|
|
378
522
|
} catch (err) {
|
|
379
523
|
if (isAbort(err) || ctx.signal.aborted) return;
|
|
380
524
|
searchMsg.textContent = S.searchFailed(errText(err));
|
|
@@ -384,6 +528,95 @@ export function mountExtensions(ctx: FeatureContext): void {
|
|
|
384
528
|
}
|
|
385
529
|
}
|
|
386
530
|
|
|
531
|
+
// -------------------------------------------------------------------------
|
|
532
|
+
// 一个包的详细页
|
|
533
|
+
// -------------------------------------------------------------------------
|
|
534
|
+
|
|
535
|
+
/** 抽屉正文。契约版本、体积、依赖、版本史只有这里有,来自 /api/extensions/package。 */
|
|
536
|
+
function detailBody(d: PackageDetailView, hit: SearchHitView): HTMLElement {
|
|
537
|
+
const box = ui.h('div');
|
|
538
|
+
const bar = ui.rowbar();
|
|
539
|
+
if (d.manifest) {
|
|
540
|
+
bar.appendChild(ui.pill(KIND_LABEL[d.manifest.kind]));
|
|
541
|
+
bar.appendChild(ui.pill(S.apiPill(d.manifest.api), d.manifest.api === d.frameworkApi ? 'on' : 'off'));
|
|
542
|
+
if (d.manifest.consoleClient) bar.appendChild(ui.pill(S.hasPanel, 'on'));
|
|
543
|
+
} else {
|
|
544
|
+
bar.appendChild(ui.pill(S.notLoadable, 'off'));
|
|
545
|
+
}
|
|
546
|
+
if (d.installed) bar.appendChild(ui.pill(d.installedSpec ? S.installedSpec(d.installedSpec) : S.alreadyInstalled, 'on'));
|
|
547
|
+
box.appendChild(bar);
|
|
548
|
+
|
|
549
|
+
if (d.description) box.appendChild(ui.h('div', 'tdesc', d.description));
|
|
550
|
+
if (d.deprecated) box.appendChild(ui.msgline(S.deprecated(d.deprecated), true));
|
|
551
|
+
for (const p of d.problems ?? []) box.appendChild(ui.msgline(p, true));
|
|
552
|
+
for (const w of d.warnings ?? []) box.appendChild(ui.msgline(w));
|
|
553
|
+
|
|
554
|
+
const rows: Array<{ k: string; v: string }> = [
|
|
555
|
+
{ k: S.fieldVersion, v: `${d.version}${d.published ? ` · ${d.published.slice(0, 10)}` : ''}` },
|
|
556
|
+
{ k: S.fieldReleases, v: S.releaseCount(d.versionCount, d.created ? d.created.slice(0, 10) : '') },
|
|
557
|
+
];
|
|
558
|
+
if (d.history.length > 1) {
|
|
559
|
+
rows.push({ k: S.fieldHistory, v: d.history.map((h) => `${h.version} ${h.date.slice(0, 10)}`).join(' ') });
|
|
560
|
+
}
|
|
561
|
+
if (d.license) rows.push({ k: S.fieldLicense, v: d.license });
|
|
562
|
+
if (d.unpackedSize) rows.push({ k: S.fieldSize, v: S.sizeAndFiles(ui.fmt.bytes(d.unpackedSize), d.fileCount ?? 0) });
|
|
563
|
+
if (d.engines) rows.push({ k: S.fieldNode, v: d.engines });
|
|
564
|
+
rows.push({ k: S.fieldDeps, v: d.dependencies.length ? d.dependencies.join(', ') : S.none });
|
|
565
|
+
rows.push({ k: S.fieldDownloads, v: S.perMonth(ui.fmt.count(hit.downloads)) });
|
|
566
|
+
if (hit.dependents > 0) rows.push({ k: S.fieldDependents, v: String(hit.dependents) });
|
|
567
|
+
if (d.maintainers.length) rows.push({ k: S.fieldMaintainers, v: d.maintainers.join(', ') });
|
|
568
|
+
if (d.keywords?.length) rows.push({ k: S.fieldKeywords, v: d.keywords.join(', ') });
|
|
569
|
+
box.appendChild(ui.kv(rows));
|
|
570
|
+
|
|
571
|
+
const actions = ui.actions();
|
|
572
|
+
const links: Array<[string, string | undefined]> = [
|
|
573
|
+
['npm', d.links.npm],
|
|
574
|
+
[S.linkRepo, d.links.repository],
|
|
575
|
+
[S.linkHome, d.links.homepage],
|
|
576
|
+
[S.linkBugs, d.links.bugs],
|
|
577
|
+
];
|
|
578
|
+
for (const [label, href] of links) {
|
|
579
|
+
if (href) actions.appendChild(ui.button(label, { size: 'sm', onClick: () => openLink(href) }));
|
|
580
|
+
}
|
|
581
|
+
actions.appendChild(ui.h('span', 'grow'));
|
|
582
|
+
// 契约版本对不上还是可以装(操作员也许就是要先装上再升级框架),但按钮按危险样式给
|
|
583
|
+
const loadable = d.manifest !== undefined && d.manifest.api === d.frameworkApi;
|
|
584
|
+
const installBtn = ui.button(d.installed ? S.alreadyInstalled : S.install, {
|
|
585
|
+
size: 'sm',
|
|
586
|
+
variant: loadable ? 'primary' : 'danger',
|
|
587
|
+
onClick: async (ev) => {
|
|
588
|
+
const ok = await install({ name: d.name, version: d.version }, ev.currentTarget as HTMLButtonElement);
|
|
589
|
+
if (!ok) return;
|
|
590
|
+
installBtn.disabled = true;
|
|
591
|
+
installBtn.textContent = S.alreadyInstalled;
|
|
592
|
+
},
|
|
593
|
+
});
|
|
594
|
+
installBtn.disabled = d.installed;
|
|
595
|
+
actions.appendChild(installBtn);
|
|
596
|
+
box.appendChild(actions);
|
|
597
|
+
return box;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/** 点开一张卡:先开抽屉再取详情,registry 慢的时候抽屉里是"读取中"而不是空白。 */
|
|
601
|
+
function openDetail(hit: SearchHitView): void {
|
|
602
|
+
const box = ui.h('div');
|
|
603
|
+
box.appendChild(ui.placeholder(S.loadingDetail));
|
|
604
|
+
ui.drawer(hit.name, box);
|
|
605
|
+
void (async () => {
|
|
606
|
+
try {
|
|
607
|
+
const d = await get<PackageDetailView>(
|
|
608
|
+
`/api/extensions/package?name=${encodeURIComponent(hit.name)}`,
|
|
609
|
+
{ signal: ctx.signal },
|
|
610
|
+
);
|
|
611
|
+
if (ctx.signal.aborted) return;
|
|
612
|
+
box.replaceChildren(detailBody(d, hit));
|
|
613
|
+
} catch (err) {
|
|
614
|
+
if (isAbort(err) || ctx.signal.aborted) return;
|
|
615
|
+
box.replaceChildren(ui.msgline(S.detailFailed(errText(err)), true));
|
|
616
|
+
}
|
|
617
|
+
})();
|
|
618
|
+
}
|
|
619
|
+
|
|
387
620
|
// -------------------------------------------------------------------------
|
|
388
621
|
// 手动安装
|
|
389
622
|
// -------------------------------------------------------------------------
|
|
@@ -410,6 +643,8 @@ export function mountExtensions(ctx: FeatureContext): void {
|
|
|
410
643
|
root.append(intro, installedSheet.el, installedGroups, searchSheet.el, manualSheet.el);
|
|
411
644
|
installedGroups.appendChild(ui.placeholder(S.loading));
|
|
412
645
|
void load();
|
|
646
|
+
// 这一类在 npm 上的包一进页就列出来:筛选与排序都在整份结果上做,没有「先搜一下」这一步
|
|
647
|
+
void search();
|
|
413
648
|
}
|
|
414
649
|
|
|
415
650
|
export const extensionsFeature: FrameworkFeature = {
|
|
@@ -60,17 +60,54 @@ const zh = {
|
|
|
60
60
|
|
|
61
61
|
searchTitle: '从 npm 安装',
|
|
62
62
|
searchDesc: '扩展拥有与框架相同的文件和网络权限。',
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
filterPlaceholder: '筛选:包名、描述、关键字',
|
|
64
|
+
sortLabel: {
|
|
65
|
+
downloads: '按月下载量',
|
|
66
|
+
date: '按最近发布',
|
|
67
|
+
name: '按名字',
|
|
68
|
+
dependents: '按被依赖数',
|
|
69
|
+
},
|
|
70
|
+
hideInstalled: '隐藏已安装',
|
|
65
71
|
searchKeywordNote: (keyword: string) => `列出 npm 上带 ${keyword} 关键字的包。`,
|
|
66
|
-
searching: '
|
|
72
|
+
searching: '读取中…',
|
|
73
|
+
noPackages: 'npm 上还没有这一类的包',
|
|
67
74
|
noHits: '没有匹配的包',
|
|
68
75
|
hitCount: (n: number) => `${n} 个包`,
|
|
69
|
-
|
|
70
|
-
|
|
76
|
+
hitCountFiltered: (shown: number, total: number) => `${shown} / ${total} 个包`,
|
|
77
|
+
searchFailed: (msg: string) => `读取失败: ${msg}`,
|
|
78
|
+
hitMeta: (version: string, downloads: string, publisher?: string) =>
|
|
71
79
|
`${version} · 月下载 ${downloads}${publisher ? ` · ${publisher}` : ''}`,
|
|
80
|
+
dependents: (n: number) => `被依赖 ${n}`,
|
|
81
|
+
openDetail: '点开看详情',
|
|
82
|
+
prevPage: '‹ 上一页',
|
|
83
|
+
nextPage: '下一页 ›',
|
|
84
|
+
pageOf: (page: number, pages: number) => `${page} / ${pages}`,
|
|
85
|
+
|
|
86
|
+
loadingDetail: '读取包信息…',
|
|
87
|
+
detailFailed: (msg: string) => `包信息读取失败: ${msg}`,
|
|
88
|
+
apiPill: (api: number) => `契约 v${api}`,
|
|
89
|
+
hasPanel: '自带控制台面板',
|
|
90
|
+
notLoadable: '不是可装载的扩展',
|
|
91
|
+
deprecated: (note: string) => `npm 上标了弃用: ${note}`,
|
|
92
|
+
installedSpec: (spec: string) => `已安装 ${spec}`,
|
|
93
|
+
fieldVersion: '最新版本',
|
|
94
|
+
fieldReleases: '发布',
|
|
95
|
+
releaseCount: (n: number, created: string) => `${n} 个版本${created ? `,首发 ${created}` : ''}`,
|
|
96
|
+
fieldHistory: '近几版',
|
|
97
|
+
fieldLicense: '许可证',
|
|
98
|
+
fieldSize: '体积',
|
|
99
|
+
sizeAndFiles: (size: string, files: number) => `${size} · ${files} 个文件`,
|
|
100
|
+
fieldNode: 'Node',
|
|
101
|
+
fieldDeps: '依赖',
|
|
102
|
+
fieldDownloads: '下载',
|
|
103
|
+
perMonth: (count: string) => `${count} / 月`,
|
|
104
|
+
fieldDependents: '被依赖',
|
|
105
|
+
fieldMaintainers: '维护者',
|
|
106
|
+
fieldKeywords: '关键字',
|
|
107
|
+
none: '无',
|
|
72
108
|
linkRepo: '仓库',
|
|
73
109
|
linkHome: '主页',
|
|
110
|
+
linkBugs: 'issues',
|
|
74
111
|
install: '安装',
|
|
75
112
|
alreadyInstalled: '已安装',
|
|
76
113
|
|
|
@@ -141,17 +178,54 @@ const en: typeof zh = {
|
|
|
141
178
|
|
|
142
179
|
searchTitle: 'Install from npm',
|
|
143
180
|
searchDesc: 'Extensions have the same file and network access as the framework.',
|
|
144
|
-
|
|
145
|
-
|
|
181
|
+
filterPlaceholder: 'Filter: name, description, keywords',
|
|
182
|
+
sortLabel: {
|
|
183
|
+
downloads: 'Monthly downloads',
|
|
184
|
+
date: 'Recently published',
|
|
185
|
+
name: 'Name',
|
|
186
|
+
dependents: 'Dependents',
|
|
187
|
+
},
|
|
188
|
+
hideInstalled: 'Hide installed',
|
|
146
189
|
searchKeywordNote: (keyword: string) => `Lists npm packages carrying the ${keyword} keyword.`,
|
|
147
|
-
searching: '
|
|
190
|
+
searching: 'Loading…',
|
|
191
|
+
noPackages: 'No packages of this kind on npm yet',
|
|
148
192
|
noHits: 'No matching packages',
|
|
149
193
|
hitCount: (n: number) => `${n} packages`,
|
|
150
|
-
|
|
151
|
-
|
|
194
|
+
hitCountFiltered: (shown: number, total: number) => `${shown} / ${total} packages`,
|
|
195
|
+
searchFailed: (msg: string) => `Loading failed: ${msg}`,
|
|
196
|
+
hitMeta: (version: string, downloads: string, publisher?: string) =>
|
|
152
197
|
`${version} · ${downloads}/month${publisher ? ` · ${publisher}` : ''}`,
|
|
198
|
+
dependents: (n: number) => `${n} dependents`,
|
|
199
|
+
openDetail: 'Open for details',
|
|
200
|
+
prevPage: '‹ Previous',
|
|
201
|
+
nextPage: 'Next ›',
|
|
202
|
+
pageOf: (page: number, pages: number) => `${page} / ${pages}`,
|
|
203
|
+
|
|
204
|
+
loadingDetail: 'Loading package info…',
|
|
205
|
+
detailFailed: (msg: string) => `Package info failed to load: ${msg}`,
|
|
206
|
+
apiPill: (api: number) => `API v${api}`,
|
|
207
|
+
hasPanel: 'Ships a console panel',
|
|
208
|
+
notLoadable: 'Not a loadable extension',
|
|
209
|
+
deprecated: (note: string) => `Deprecated on npm: ${note}`,
|
|
210
|
+
installedSpec: (spec: string) => `Installed ${spec}`,
|
|
211
|
+
fieldVersion: 'Latest',
|
|
212
|
+
fieldReleases: 'Releases',
|
|
213
|
+
releaseCount: (n: number, created: string) => `${n} versions${created ? `, first ${created}` : ''}`,
|
|
214
|
+
fieldHistory: 'Recent',
|
|
215
|
+
fieldLicense: 'License',
|
|
216
|
+
fieldSize: 'Size',
|
|
217
|
+
sizeAndFiles: (size: string, files: number) => `${size} · ${files} files`,
|
|
218
|
+
fieldNode: 'Node',
|
|
219
|
+
fieldDeps: 'Dependencies',
|
|
220
|
+
fieldDownloads: 'Downloads',
|
|
221
|
+
perMonth: (count: string) => `${count}/month`,
|
|
222
|
+
fieldDependents: 'Dependents',
|
|
223
|
+
fieldMaintainers: 'Maintainers',
|
|
224
|
+
fieldKeywords: 'Keywords',
|
|
225
|
+
none: 'none',
|
|
153
226
|
linkRepo: 'Repository',
|
|
154
227
|
linkHome: 'Homepage',
|
|
228
|
+
linkBugs: 'Issues',
|
|
155
229
|
install: 'Install',
|
|
156
230
|
alreadyInstalled: 'Installed',
|
|
157
231
|
|
|
@@ -11,7 +11,7 @@ import type { ConsolePageHost } from '../console-pages/host.ts';
|
|
|
11
11
|
import type { ConsoleIconName } from '../ui/icons.ts';
|
|
12
12
|
|
|
13
13
|
/** 嵌在框架页里的控制台页宿主:manifest 与面板加载都由它管,页面只给容器与路由前缀。 */
|
|
14
|
-
export type EmbeddedConsolePageHost = Pick<ConsolePageHost, 'load' | 'show' | 'unmount' | 'pages' | 'find'>;
|
|
14
|
+
export type EmbeddedConsolePageHost = Pick<ConsolePageHost, 'load' | 'show' | 'unmount' | 'pages' | 'find' | 'mountConnection'>;
|
|
15
15
|
|
|
16
16
|
export interface FeatureContext {
|
|
17
17
|
/** 本页面的 DOM 根。离开时由 host 清空。 */
|
|
@@ -57,7 +57,7 @@ export type FrameworkFeature = {
|
|
|
57
57
|
readonly label: string;
|
|
58
58
|
/** 左栏图标。 World 那一页不走此字段。 */
|
|
59
59
|
readonly icon?: ConsoleIconName;
|
|
60
|
-
/**
|
|
60
|
+
/** 这一行那盏灯在灯表里的键。省略就不点灯。 */
|
|
61
61
|
readonly lampId?: string;
|
|
62
62
|
/** 列出的 capability 任一已挂载即显示导航项;省略或为空时始终显示。 */
|
|
63
63
|
readonly needsAny?: readonly string[];
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { get } from '../../core/api.ts';
|
|
2
|
+
|
|
3
|
+
/** `cortico-diagnostics-r-20260917-104402-c2ab-20260917-142530.json`;没有 run id 时写 norun。 */
|
|
4
|
+
export function diagnosticsFileName(runId: string | null, at: Date): string {
|
|
5
|
+
const p = (n: number): string => String(n).padStart(2, '0');
|
|
6
|
+
const stamp = `${at.getFullYear()}${p(at.getMonth() + 1)}${p(at.getDate())}`
|
|
7
|
+
+ `-${p(at.getHours())}${p(at.getMinutes())}${p(at.getSeconds())}`;
|
|
8
|
+
const run = (runId ?? 'norun').replace(/[^\w.-]/g, '_');
|
|
9
|
+
return `cortico-diagnostics-${run}-${stamp}.json`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** 交给浏览器下载。对象 URL 在同一轮事件循环之后撤掉。 */
|
|
13
|
+
export function downloadJson(doc: Document, name: string, data: unknown): void {
|
|
14
|
+
const url = URL.createObjectURL(new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }));
|
|
15
|
+
const link = doc.createElement('a');
|
|
16
|
+
link.href = url;
|
|
17
|
+
link.download = name;
|
|
18
|
+
doc.body.appendChild(link);
|
|
19
|
+
link.click();
|
|
20
|
+
link.remove();
|
|
21
|
+
setTimeout(() => URL.revokeObjectURL(url), 0);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface DiagnosticsDeps {
|
|
25
|
+
doc: Document;
|
|
26
|
+
signal: AbortSignal;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function exportDiagnostics(deps: DiagnosticsDeps): Promise<void> {
|
|
30
|
+
const bundle = await get<{ run?: { id?: string | null } }>('/api/diagnostics', { signal: deps.signal });
|
|
31
|
+
downloadJson(deps.doc, diagnosticsFileName(bundle?.run?.id ?? null, new Date()), bundle);
|
|
32
|
+
}
|