cortico 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/README.md +7 -14
  2. package/package.json +1 -3
  3. package/src/boot.ts +18 -0
  4. package/src/bot.ts +41 -6
  5. package/src/core/README.md +4 -6
  6. package/src/core/blobs.ts +23 -2
  7. package/src/core/config-schema.ts +1 -1
  8. package/src/core/config.ts +2 -16
  9. package/src/core/core.ts +15 -5
  10. package/src/core/loop.ts +55 -27
  11. package/src/core/session.ts +3 -2
  12. package/src/core/timers.ts +8 -6
  13. package/src/core/types.ts +19 -3
  14. package/src/core/util.ts +17 -1
  15. package/src/deploy.ts +4 -1
  16. package/src/extensions/README.md +6 -4
  17. package/src/extensions/dry-mount.ts +23 -2
  18. package/src/extensions/manifest.ts +21 -11
  19. package/src/extensions.ts +147 -23
  20. package/src/launcher.ts +23 -9
  21. package/src/protocol/open-responses/context-log.ts +37 -9
  22. package/src/providers/README.md +32 -8
  23. package/src/providers/base.ts +3 -1
  24. package/src/providers/configuration.ts +2 -1
  25. package/src/providers/console/hub.ts +270 -0
  26. package/src/providers/console/settings.ts +8 -18
  27. package/src/providers/console/types.ts +5 -0
  28. package/src/providers/hub-api.ts +3 -0
  29. package/src/providers/llamacpp/config.ts +8 -6
  30. package/src/providers/llamacpp/console/runtime-panel.ts +5 -1
  31. package/src/providers/llamacpp/console/server.ts +3 -1
  32. package/src/providers/llamacpp/index.ts +3 -1
  33. package/src/providers/llamacpp/native.ts +10 -5
  34. package/src/providers/llamacpp/strings.ts +6 -6
  35. package/src/providers/name.ts +8 -0
  36. package/src/providers/openai-responses-compat/config.ts +13 -0
  37. package/src/providers/openai-responses-compat/console/client.ts +5 -0
  38. package/src/providers/openai-responses-compat/console/reasoning-panel.ts +84 -0
  39. package/src/providers/openai-responses-compat/console/server.ts +127 -0
  40. package/src/providers/openai-responses-compat/index.ts +38 -13
  41. package/src/providers/openai-responses-compat/native.ts +9 -4
  42. package/src/providers/openai-responses-compat/strings.ts +62 -1
  43. package/src/providers/registry.ts +5 -0
  44. package/src/providers/transport/responses-input.ts +59 -10
  45. package/src/web/README.md +11 -6
  46. package/src/web/auth.ts +80 -0
  47. package/src/web/client/console-pages/builtins/llm-settings/panel.ts +5 -4
  48. package/src/web/client/console-pages/builtins/llm-settings/pricing-panel.ts +22 -8
  49. package/src/web/client/console-pages/builtins/llm-settings/strings.ts +6 -8
  50. package/src/web/client/console-pages/host.ts +24 -7
  51. package/src/web/client/core/api.ts +5 -1
  52. package/src/web/client/core/router.ts +15 -0
  53. package/src/web/client/features/core/strings.ts +2 -2
  54. package/src/web/client/features/extensions/index.ts +276 -41
  55. package/src/web/client/features/extensions/strings.ts +84 -10
  56. package/src/web/client/features/feature.ts +2 -2
  57. package/src/web/client/features/live/diagnostics.ts +32 -0
  58. package/src/web/client/features/live/index.ts +34 -11
  59. package/src/web/client/features/live/onboarding.ts +4 -1
  60. package/src/web/client/features/live/protocol.ts +1 -0
  61. package/src/web/client/features/live/strings.ts +20 -14
  62. package/src/web/client/features/live/timeline.ts +2 -1
  63. package/src/web/client/features/providers/detail.ts +262 -0
  64. package/src/web/client/features/providers/drafts.ts +23 -0
  65. package/src/web/client/features/providers/index.ts +173 -87
  66. package/src/web/client/features/providers/strings.ts +44 -17
  67. package/src/web/client/features/providers/types.ts +15 -0
  68. package/src/web/client/features/settings/general.ts +13 -0
  69. package/src/web/client/features/settings/index.ts +1 -0
  70. package/src/web/client/features/settings/strings.ts +6 -0
  71. package/src/web/client/features/worlds/index.ts +1 -0
  72. package/src/web/client/main.ts +4 -0
  73. package/src/web/client/shell/index.ts +28 -48
  74. package/src/web/client/shell/strings.ts +0 -22
  75. package/src/web/client/ui/icons.ts +14 -1
  76. package/src/web/client/ui/prompt-input.tsx +17 -5
  77. package/src/web/client/ui/strings.ts +0 -2
  78. package/src/web/console-pages.ts +1 -1
  79. package/src/web/diagnostics.ts +133 -0
  80. package/src/web/public/login.html +67 -0
  81. package/src/web/public/styles.css +143 -26
  82. package/src/web/server.ts +233 -19
  83. package/src/web/shared/client-panel.ts +4 -1
  84. package/src/web/shared/console-protocol.ts +4 -1
  85. package/src/worlds/bilibili/README.md +1 -1
  86. package/src/worlds/bilibili/overlay/server.ts +4 -2
  87. package/src/worlds/minecraft/ADAPT.md +66 -0
  88. package/src/worlds/minecraft/README.md +69 -11
  89. package/src/worlds/minecraft/cell-facts.ts +226 -0
  90. package/src/worlds/minecraft/chests.ts +5 -0
  91. package/src/worlds/minecraft/containers.ts +325 -0
  92. package/src/worlds/minecraft/entity-facts.ts +31 -5
  93. package/src/worlds/minecraft/executor.ts +283 -10348
  94. package/src/worlds/minecraft/inventory.ts +268 -0
  95. package/src/worlds/minecraft/melee.ts +419 -0
  96. package/src/worlds/minecraft/mineflayer-fixes.ts +55 -1
  97. package/src/worlds/minecraft/placed-ledger.ts +152 -0
  98. package/src/worlds/minecraft/placement.ts +1038 -0
  99. package/src/worlds/minecraft/receipt.ts +340 -0
  100. package/src/worlds/minecraft/skill-context.ts +358 -0
  101. package/src/worlds/minecraft/skills-build.ts +1169 -0
  102. package/src/worlds/minecraft/skills-container.ts +1343 -0
  103. package/src/worlds/minecraft/skills-craft.ts +333 -0
  104. package/src/worlds/minecraft/skills-dig.ts +624 -0
  105. package/src/worlds/minecraft/skills-gather.ts +1230 -0
  106. package/src/worlds/minecraft/skills-interact.ts +1559 -0
  107. package/src/worlds/minecraft/tools.ts +331 -0
  108. package/src/worlds/minecraft/travel.ts +763 -0
  109. package/src/worlds/minecraft/until.ts +75 -0
  110. package/src/worlds/qq/normalize.ts +14 -0
  111. package/src/worlds/qq/world.ts +53 -7
  112. package/src/worlds/terminal/world.ts +5 -3
@@ -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
- async function install(target: { name: string; version?: string } | { path: string }, btn?: HTMLButtonElement): Promise<void> {
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 searchInput = ui.input({ type: 'search', placeholder: S.searchPlaceholder });
322
- const searchBtn = ui.button(S.search, { size: 'sm', variant: 'primary', onClick: () => void search() });
323
- searchInput.addEventListener('keydown', (ev) => { if (ev.key === 'Enter') void search(); }, { signal: ctx.signal });
324
- searchBar.append(kindSeg.el, searchInput, searchBtn);
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
- searchSheet.body.append(searchBar, keywordLine, searchMsg, resultGrid);
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.kind) {
342
- const bar = ui.rowbar();
343
- bar.appendChild(ui.pill(KIND_LABEL[h.kind]));
344
- card.body.appendChild(bar);
345
- }
346
- if (h.description) card.body.appendChild(ui.msgline(h.description));
347
- const actions = ui.actions();
348
- const links: Array<[string, string | undefined]> = [['npm', h.links.npm], [S.linkRepo, h.links.repository], [S.linkHome, h.links.homepage]];
349
- for (const [label, href] of links) {
350
- if (href) actions.appendChild(ui.button(label, { size: 'sm', onClick: () => openLink(href) }));
351
- }
352
- const installBtn = ui.button(h.installed ? S.alreadyInstalled : S.install, {
353
- size: 'sm',
354
- variant: 'primary',
355
- onClick: (ev) => void install({ name: h.name, version: h.version }, ev.currentTarget as HTMLButtonElement),
356
- });
357
- installBtn.disabled = h.installed;
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?q=${q}&kind=${searchKind}`,
516
+ `/api/extensions/search?kind=${searchKind}`,
371
517
  { signal: ctx.signal },
372
518
  );
373
519
  if (ctx.signal.aborted) return;
374
- const hits = Array.isArray(data?.hits) ? data.hits : [];
375
- resultGrid.replaceChildren();
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
- searchPlaceholder: '关键字;留空列出全部',
64
- search: '搜索',
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
- searchFailed: (msg: string) => `搜索失败: ${msg}`,
70
- hitMeta: (version: string, downloads: number, publisher?: string) =>
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
- searchPlaceholder: 'Keyword; empty lists everything',
145
- search: 'Search',
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: '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
- searchFailed: (msg: string) => `Search failed: ${msg}`,
151
- hitMeta: (version: string, downloads: number, publisher?: string) =>
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
+ }