minecodex 2.0.55 → 2.1.1

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 (65) hide show
  1. package/README.md +90 -26
  2. package/apps/dashboard/app.js +20 -14
  3. package/apps/dashboard/assets/chevron-left.svg +13 -0
  4. package/apps/dashboard/assets/chevron-right.svg +13 -0
  5. package/apps/dashboard/assets/feature-showcase.js +185 -0
  6. package/apps/dashboard/assets/pause.svg +14 -0
  7. package/apps/dashboard/assets/play.svg +13 -0
  8. package/apps/dashboard/assets/previews/composer-expand.jpg +0 -0
  9. package/apps/dashboard/assets/previews/composer-expand.mp4 +0 -0
  10. package/apps/dashboard/assets/previews/file-navigation.jpg +0 -0
  11. package/apps/dashboard/assets/previews/file-navigation.mp4 +0 -0
  12. package/apps/dashboard/assets/previews/files-auto-refresh.jpg +0 -0
  13. package/apps/dashboard/assets/previews/files-auto-refresh.mp4 +0 -0
  14. package/apps/dashboard/assets/previews/images.jpg +0 -0
  15. package/apps/dashboard/assets/previews/images.mp4 +0 -0
  16. package/apps/dashboard/assets/previews/manifest.json +67 -1
  17. package/apps/dashboard/assets/previews/markdown-reading.jpg +0 -0
  18. package/apps/dashboard/assets/previews/markdown-reading.mp4 +0 -0
  19. package/apps/dashboard/assets/previews/mermaid-diagrams.jpg +0 -0
  20. package/apps/dashboard/assets/previews/mermaid-diagrams.mp4 +0 -0
  21. package/apps/dashboard/assets/previews/model-slider.jpg +0 -0
  22. package/apps/dashboard/assets/previews/model-slider.mp4 +0 -0
  23. package/apps/dashboard/assets/previews/notes.jpg +0 -0
  24. package/apps/dashboard/assets/previews/notes.mp4 +0 -0
  25. package/apps/dashboard/assets/previews/quick-chat.jpg +0 -0
  26. package/apps/dashboard/assets/previews/quick-chat.mp4 +0 -0
  27. package/apps/dashboard/assets/previews/session-status.jpg +0 -0
  28. package/apps/dashboard/assets/previews/session-status.mp4 +0 -0
  29. package/apps/dashboard/assets/previews/sidebar-focus.jpg +0 -0
  30. package/apps/dashboard/assets/previews/sidebar-focus.mp4 +0 -0
  31. package/apps/dashboard/assets/previews/sites.jpg +0 -0
  32. package/apps/dashboard/assets/previews/sites.mp4 +0 -0
  33. package/apps/dashboard/assets/previews/usage-meter.jpg +0 -0
  34. package/apps/dashboard/assets/previews/usage-meter.mp4 +0 -0
  35. package/apps/dashboard/index.html +26 -2
  36. package/apps/dashboard/styles.css +58 -39
  37. package/assets/readme/hero-dark.svg +50 -0
  38. package/assets/readme/hero.svg +36 -79
  39. package/features/images/src/adapters/codex/rspack-modules.mjs +9 -3
  40. package/features/images/src/library-prompt-source.mjs +2 -2
  41. package/features/notes/codex-feature.json +1 -0
  42. package/features/quick-chat/src/adapters/codex/renderer.mjs +3 -1
  43. package/package.json +3 -3
  44. package/packages/cli/src/config.mjs +1 -2
  45. package/packages/cli/src/control-server.mjs +1 -1
  46. package/packages/plugin/assets/app.js +35 -35
  47. package/packages/runtime-host/src/codex-cdp.mjs +0 -7
  48. package/packages/runtime-host/src/codex-injection.mjs +50 -2114
  49. package/packages/runtime-host/src/codex-runtime.mjs +4 -203
  50. package/packages/runtime-host/src/feature-registry.mjs +10 -91
  51. package/packages/runtime-host/src/main.mjs +0 -1
  52. package/packages/ui/src/native/tokens.mjs +0 -28
  53. package/apps/dashboard/assets/feature-previews.js +0 -206
  54. package/features/images/src/adapters/codex/cloud-import.mjs +0 -52
  55. package/features/images/src/backend/cloud-library.mjs +0 -12
  56. package/features/images/src/chatgpt-image-source-page.mjs +0 -207
  57. package/features/images/src/chatgpt-image-source.mjs +0 -42
  58. package/features/images/src/http-server.mjs +0 -487
  59. package/features/images/src/main.mjs +0 -52
  60. package/features/images/src/save-as.mjs +0 -69
  61. package/features/images/web/app.js +0 -1780
  62. package/features/images/web/i18n.mjs +0 -132
  63. package/features/images/web/index.html +0 -187
  64. package/features/images/web/styles.css +0 -1114
  65. package/packages/runtime-host/src/host-actions.mjs +0 -221
@@ -1,54 +1,36 @@
1
1
  import { assertPromptsRequest } from "../../../features/notes/src/adapters/codex/requests.mjs";
2
2
  import { LocalImageSource } from "../../../features/images/src/backend/local-image-source.mjs";
3
- import { cloudImagesAction } from "../../../features/images/src/backend/cloud-library.mjs";
4
3
  // CodexRuntime:Codex Renderer 生命周期与 host action 执行。
5
4
  // 注入源(createInjectionSource)与 CDP 客户端分别在 codex-injection.mjs / codex-cdp.mjs。
6
5
 
7
6
  import { randomBytes } from "node:crypto";
8
- import { mkdir, stat } from "node:fs/promises";
7
+ import { mkdir } from "node:fs/promises";
9
8
  import path from "node:path";
10
9
  import {
11
10
  CODEX_APP_ORIGIN,
12
11
  HOST_BINDING_NAME,
13
- LOOPBACK_SURFACE_HOSTS,
14
12
  RUNTIME_VERSION,
15
- SURFACE_FRAME_PREFIX,
16
- SURFACE_LOAD_ACTION,
17
13
  createDocumentBootstrapSource,
18
14
  createInjectionSource,
19
- documentWithBase,
20
- findFrameByName,
21
15
  pageScriptServiceOrigin,
22
- requireDeclaredLoopbackSurface,
23
16
  } from "./codex-injection.mjs";
24
17
  import {
25
18
  connect,
26
- evaluationValue,
27
19
  processChildHasExited,
28
20
  requireCdpWebSocketUrl,
29
21
  waitForExpression,
30
22
  waitForProcessChildExit,
31
23
  } from "./codex-cdp.mjs";
32
- import {
33
- attachFileToComposer,
34
- attachFilesToComposer,
35
- createImageEditThread,
36
- } from "./host-actions.mjs";
37
24
  import { ModelFavorites } from "../../../features/model-slider/src/backend/favorites.mjs";
38
25
  import { SitesService } from "../../../features/sites/src/backend/sites-service.mjs";
39
26
  import { SessionStatusReader, threadIdFromLocation } from "../../../features/session-status/src/adapters/codex/session-reader.mjs";
40
27
  import { createCodexEnvironment } from "../../codex-adapter/src/renderer/environment.mjs";
41
28
 
42
29
 
43
- // 保持公共入口兼容:历史 import 从本文件继续可用。
44
30
  export {
45
- RESPONSIVE_SUMMARY_LAYOUT,
46
31
  createInjectionSource,
47
32
  createRuntimeReadyContract,
48
33
  pageScriptServiceOrigin,
49
- responsiveSummaryContentShift,
50
- responsiveSummaryDisplayMode,
51
- responsiveSummaryVisibleSurface,
52
34
  } from "./codex-injection.mjs";
53
35
 
54
36
  export const SESSION_STATUS_THREAD_ID_EXPRESSION = `(() => {
@@ -71,9 +53,6 @@ export class CodexRuntime {
71
53
  availabilityTimeoutMs = 20_000,
72
54
  availabilityPollMs = 500,
73
55
  monitorIntervalMs = 2_000,
74
- surfaceLoadTimeoutMs = 5_000,
75
- surfaceFramePollMs = 25,
76
- featureInstanceId = null,
77
56
  codexBuild = null,
78
57
  runtimeSessionId = randomBytes(16).toString("hex"),
79
58
  onStatusChange = null,
@@ -94,9 +73,6 @@ export class CodexRuntime {
94
73
  this.availabilityTimeoutMs = availabilityTimeoutMs;
95
74
  this.availabilityPollMs = availabilityPollMs;
96
75
  this.monitorIntervalMs = monitorIntervalMs;
97
- this.surfaceLoadTimeoutMs = surfaceLoadTimeoutMs;
98
- this.surfaceFramePollMs = surfaceFramePollMs;
99
- this.featureInstanceId = featureInstanceId;
100
76
  this.codexBuild = codexBuild;
101
77
  this.runtimeSessionId = runtimeSessionId;
102
78
  this.onStatusChange = onStatusChange;
@@ -408,124 +384,6 @@ export class CodexRuntime {
408
384
  await client.send("Runtime.evaluate", { expression, awaitPromise: true, returnByValue: true });
409
385
  }
410
386
 
411
- async fetchSurfaceResource(url, label) {
412
- const controller = new AbortController();
413
- const timeout = setTimeout(() => controller.abort(), this.surfaceLoadTimeoutMs);
414
- try {
415
- return await this.fetchImpl(url, {
416
- cache: "no-store",
417
- headers: { origin: CODEX_APP_ORIGIN },
418
- signal: controller.signal,
419
- });
420
- } catch (error) {
421
- if (controller.signal.aborted) {
422
- throw Object.assign(new Error(`${label} timed out`), { code: "SURFACE_LOAD_TIMEOUT" });
423
- }
424
- throw error;
425
- } finally {
426
- clearTimeout(timeout);
427
- }
428
- }
429
-
430
- async verifiedSurfaceDocument(feature, surfaceUrl) {
431
- requireDeclaredLoopbackSurface(feature, surfaceUrl);
432
- if (!this.featureInstanceId || !feature.healthUrl) {
433
- throw Object.assign(new Error("Surface service identity is unavailable"), {
434
- code: "SURFACE_IDENTITY_UNAVAILABLE",
435
- });
436
- }
437
- const healthResponse = await this.fetchSurfaceResource(feature.healthUrl, "Surface health check");
438
- if (!healthResponse?.ok) {
439
- throw Object.assign(new Error(`Surface health returned HTTP ${healthResponse?.status ?? "error"}`), {
440
- code: "SURFACE_HEALTH_FAILED",
441
- });
442
- }
443
- let health;
444
- try {
445
- health = await healthResponse.json();
446
- } catch {
447
- throw Object.assign(new Error("Surface health returned invalid JSON"), {
448
- code: "SURFACE_HEALTH_FAILED",
449
- });
450
- }
451
- if (
452
- health?.ok !== true
453
- || health.service !== feature.id
454
- || health.protocolVersion !== 1
455
- || health.instanceId !== this.featureInstanceId
456
- ) {
457
- throw Object.assign(new Error("Surface service identity does not match this RuntimeHost"), {
458
- code: "SURFACE_IDENTITY_MISMATCH",
459
- });
460
- }
461
-
462
- const response = await this.fetchSurfaceResource(surfaceUrl, "Surface document request");
463
- if (!response?.ok) {
464
- throw Object.assign(new Error(`Surface document returned HTTP ${response?.status ?? "error"}`), {
465
- code: "SURFACE_DOCUMENT_FAILED",
466
- });
467
- }
468
- const contentType = String(response.headers?.get?.("content-type") ?? "").toLowerCase();
469
- if (!contentType.startsWith("text/html")) {
470
- throw Object.assign(new Error("Surface document must be HTML"), {
471
- code: "SURFACE_DOCUMENT_INVALID",
472
- });
473
- }
474
- return documentWithBase(await response.text(), surfaceUrl);
475
- }
476
-
477
- async loadSurfaceIntoFrame(client, feature, payload = {}) {
478
- const frameName = payload.frameName;
479
- if (
480
- typeof frameName !== "string"
481
- || !frameName.startsWith(SURFACE_FRAME_PREFIX)
482
- || frameName.length > 256
483
- ) {
484
- throw Object.assign(new Error("Surface frame identity is invalid"), {
485
- code: "SURFACE_FRAME_INVALID",
486
- });
487
- }
488
- const surfaceUrl = payload.surfaceUrl;
489
- const html = await this.verifiedSurfaceDocument(feature, surfaceUrl);
490
- const deadline = Date.now() + this.surfaceLoadTimeoutMs;
491
- let frame = null;
492
- while (!frame && Date.now() < deadline) {
493
- const { frameTree } = await client.send("Page.getFrameTree");
494
- frame = findFrameByName(frameTree, frameName);
495
- if (!frame) await this.sleep(this.surfaceFramePollMs);
496
- }
497
- if (!frame) {
498
- throw Object.assign(new Error("Surface frame was not discovered"), {
499
- code: "SURFACE_FRAME_NOT_FOUND",
500
- });
501
- }
502
- if (frame.url !== "about:blank") {
503
- throw Object.assign(new Error("Surface frame must remain at about:blank before loading"), {
504
- code: "SURFACE_FRAME_INVALID",
505
- });
506
- }
507
- await client.send("Page.setDocumentContent", { frameId: frame.id, html });
508
- return { loaded: true };
509
- }
510
-
511
- // 薄转发:host action 实现已迁至 host-actions.mjs,
512
- // 保留方法入口以稳定测试与 handleBindingCalled 路由。
513
- attachFileToComposer(client, filePath, requestId) {
514
- return attachFileToComposer(client, filePath, requestId);
515
- }
516
-
517
- attachFilesToComposer(client, filePaths, requestId) {
518
- return attachFilesToComposer(client, filePaths, requestId);
519
- }
520
-
521
- createImageEditThread(client, feature, payload, requestId) {
522
- return createImageEditThread(client, feature, payload, requestId, {
523
- fetchImpl: this.fetchImpl,
524
- attachFiles: (_client, filePaths, _requestId) => this.attachFilesToComposer(client, filePaths, requestId),
525
- });
526
- }
527
-
528
-
529
387
  async handleBindingCalled(targetId, client, params) {
530
388
  if (params.name !== HOST_BINDING_NAME) return;
531
389
  let request;
@@ -542,18 +400,6 @@ export class CodexRuntime {
542
400
  }
543
401
  return;
544
402
  }
545
- if (feature && request.action === SURFACE_LOAD_ACTION) {
546
- try {
547
- const result = await this.loadSurfaceIntoFrame(client, feature, request.payload);
548
- await this.resolveHostAction(client, request.requestId, { ok: true, result });
549
- } catch (error) {
550
- await this.resolveHostAction(client, request.requestId, {
551
- ok: false,
552
- error: { code: error.code ?? "SURFACE_LOAD_FAILED", message: error.message },
553
- });
554
- }
555
- return;
556
- }
557
403
  if (!feature?.hostActions?.includes(request.action)) {
558
404
  await this.resolveHostAction(client, request.requestId, {
559
405
  ok: false,
@@ -608,55 +454,10 @@ export class CodexRuntime {
608
454
  }
609
455
  return;
610
456
  }
611
- if (request.action === "import-generated-image") {
612
- await this.importGeneratedImage(feature, request.payload);
613
- return;
614
- }
615
- if (["list-cloud-images", "import-cloud-image"].includes(request.action)) {
616
- try {
617
- let result = await cloudImagesAction(
618
- async (expression, options) => evaluationValue(await client.send("Runtime.evaluate", {
619
- expression, awaitPromise:true, returnByValue:true,
620
- }, options)),
621
- request.action === "list-cloud-images" ? "list" : "import", request.payload,
622
- );
623
- if (request.action === "import-cloud-image" && !result.skipped) {
624
- result = await this.importGeneratedImage(feature, result);
625
- }
626
- await this.resolveHostAction(client, request.requestId, { ok: true, result });
627
- } catch (error) {
628
- await this.resolveHostAction(client, request.requestId, { ok: false, error: { code: "CLOUD_IMAGES_FAILED", message: error.message } });
629
- }
630
- return;
631
- }
632
- try {
633
- const result = request.action === "create-image-edit-thread"
634
- ? await this.createImageEditThread(client, feature, request.payload, request.requestId)
635
- : await this.attachFileToComposer(client, request.payload?.path, request.requestId);
636
- await this.resolveHostAction(client, request.requestId, { ok: true, result });
637
- } catch (error) {
638
- await this.resolveHostAction(client, request.requestId, {
639
- ok: false,
640
- error: { code: error.code ?? "ATTACH_FAILED", message: error.message },
641
- });
642
- }
643
- }
644
-
645
- async importGeneratedImage(feature, payload) {
646
- const origin = pageScriptServiceOrigin(feature);
647
- if (!origin) throw new Error("Generated image import service is unavailable");
648
- const response = await this.fetchImpl(new URL("/api/import-generated-image", origin), {
649
- method: "POST",
650
- headers: {
651
- "Content-Type": "application/json",
652
- Origin: CODEX_APP_ORIGIN,
653
- },
654
- body: JSON.stringify(payload),
457
+ await this.resolveHostAction(client, request.requestId, {
458
+ ok: false,
459
+ error: { code: "ACTION_NOT_IMPLEMENTED", message: "Host action has no RuntimeHost handler" },
655
460
  });
656
- if (!response?.ok) {
657
- throw new Error(`Generated image import returned HTTP ${response?.status ?? "error"}`);
658
- }
659
- return response.json();
660
461
  }
661
462
 
662
463
  async refreshTargets() {
@@ -6,24 +6,15 @@ import path from "node:path";
6
6
  import { pathToFileURL } from "node:url";
7
7
 
8
8
  const MANIFEST_NAME = "codex-feature.json";
9
+ // 每项都必须有功能在 codex-feature.json 中声明,并由 CodexRuntime 实现。
9
10
  const HOST_ACTIONS = new Set([
10
11
  "sites",
11
12
  "model-favorites",
12
13
  "quick-prompts",
13
- "insert-text",
14
- "attach-file",
15
- "open-detail-tab",
16
- "open-editor-modal",
17
- "open-sidebar",
18
- "resolve-file-paths",
19
- "import-generated-image",
20
- "list-cloud-images",
21
14
  "local-image-source",
22
- "import-cloud-image",
23
- "create-image-edit-thread",
24
15
  ]);
25
16
 
26
- const PAGE_SCRIPT_ENTRY_KINDS = new Set(["page-script"]);
17
+ const ENTRY_KINDS = new Set(["page-script", "composer-model-selector"]);
27
18
  const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "[::1]"]);
28
19
 
29
20
  export const FEATURE_PROTOCOL_VERSION = 1;
@@ -172,9 +163,9 @@ async function normalizeModelSelector(modelSelector, projectDir) {
172
163
  }
173
164
 
174
165
  async function normalizePageScript(manifest, projectDir) {
175
- const resource = manifest.entry?.script ?? manifest.pageScript?.resource ?? manifest.script;
166
+ const resource = manifest.entry?.script;
176
167
  if (!resource) {
177
- throw new Error("page-script entries require entry.script or script.resource");
168
+ throw new Error("page-script entries require entry.script");
178
169
  }
179
170
  const scriptPath = path.resolve(projectDir, resource);
180
171
  const relative = path.relative(projectDir, scriptPath);
@@ -218,69 +209,15 @@ async function normalizeFeature(manifest, projectDir, env) {
218
209
  }
219
210
 
220
211
  const label = normalizeLocalizedText(manifest.label, "label");
221
- const entryKind = manifest.entry?.kind ?? "sidebar";
222
- if (!new Set(["sidebar", "conversation-toolbar", "composer-model-selector", ...PAGE_SCRIPT_ENTRY_KINDS]).has(entryKind)) {
223
- throw new Error("entry.kind must be sidebar, conversation-toolbar, composer-model-selector, or page-script");
224
- }
225
- const isModelSelector = entryKind === "composer-model-selector";
226
- const isPageScript = PAGE_SCRIPT_ENTRY_KINDS.has(entryKind);
227
- if (!isModelSelector && !isPageScript && !manifest.surfaceUrl) {
228
- throw new Error("surfaceUrl is required for rendered features");
229
- }
230
- if (entryKind === "conversation-toolbar" && !manifest.entry?.ariaLabel) {
231
- throw new Error("conversation-toolbar entries require entry.ariaLabel");
232
- }
233
- if (entryKind === "conversation-toolbar" && !manifest.pinnedSummary?.surfaceUrl) {
234
- throw new Error("conversation-toolbar entries require pinnedSummary.surfaceUrl");
235
- }
236
- const summaryPresentation = manifest.pinnedSummary?.presentation ?? "responsive-summary";
237
- if (summaryPresentation !== "responsive-summary") {
238
- throw new Error("pinnedSummary.presentation must be responsive-summary");
239
- }
240
-
241
- const detailTabs = await Promise.all((manifest.detailTabs ?? []).map(async (detail) => {
242
- if (!detail.id || !detail.label || !detail.surfaceUrl) {
243
- throw new Error("each detailTabs entry requires id, label, and surfaceUrl");
244
- }
245
- const detailLabel = normalizeLocalizedText(detail.label, `detailTabs.${detail.id}.label`);
246
- return {
247
- id: String(detail.id),
248
- label: detailLabel.value,
249
- labels: detailLabel.locales,
250
- surfaceUrl: normalizeUrl(detail.surfaceUrl, `detailTabs.${detail.id}.surfaceUrl`),
251
- icon: await normalizeIcon(detail.icon, projectDir),
252
- };
253
- }));
254
- if (new Set(detailTabs.map((detail) => detail.id)).size !== detailTabs.length) {
255
- throw new Error("detailTabs ids must be unique");
212
+ const entryKind = manifest.entry?.kind;
213
+ if (!ENTRY_KINDS.has(entryKind)) {
214
+ throw new Error("entry.kind must be page-script or composer-model-selector");
256
215
  }
257
216
  const hostActions = (manifest.hostActions ?? []).map(String);
258
217
  if (hostActions.some((action) => !HOST_ACTIONS.has(action))) {
259
218
  throw new Error("hostActions contains an unsupported action");
260
219
  }
261
-
262
- const pageScript = manifest.pageScript
263
- ? await normalizePageScript({ ...manifest, entry: { ...(manifest.entry ?? {}), script: manifest.pageScript.resource } }, projectDir)
264
- : null;
265
-
266
220
  const { surfaceUrl, healthUrl } = normalizeRuntimeUrls(manifest, env);
267
- const entryLabel = normalizeLocalizedText(manifest.entry?.ariaLabel ?? manifest.label, "entry.ariaLabel");
268
- const summaryLabel = manifest.pinnedSummary
269
- ? normalizeLocalizedText(manifest.pinnedSummary.label ?? manifest.label, "pinnedSummary.label")
270
- : null;
271
- const overlayLabel = manifest.pinnedSummary
272
- ? normalizeLocalizedText(manifest.pinnedSummary.overlayLabel ?? manifest.label, "pinnedSummary.overlayLabel")
273
- : null;
274
- const pinnedLabel = manifest.pinnedSummary
275
- ? normalizeLocalizedText(
276
- manifest.pinnedSummary.pinnedLabel
277
- ?? manifest.entry?.ariaLabel
278
- ?? (typeof manifest.label === "string"
279
- ? `Toggle pinned ${manifest.label.toLowerCase()}`
280
- : manifest.label),
281
- "pinnedSummary.pinnedLabel",
282
- )
283
- : null;
284
221
 
285
222
  return {
286
223
  id: String(manifest.id),
@@ -288,30 +225,13 @@ async function normalizeFeature(manifest, projectDir, env) {
288
225
  labels: label.locales,
289
226
  surfaceUrl,
290
227
  healthUrl,
291
- icon: await normalizeIcon(manifest.icon, projectDir),
292
- entry: {
293
- kind: entryKind,
294
- ariaLabel: entryLabel.value,
295
- ariaLabels: entryLabel.locales,
296
- },
297
- pinnedSummary: manifest.pinnedSummary ? {
298
- label: summaryLabel.value,
299
- labels: summaryLabel.locales,
300
- surfaceUrl: normalizeUrl(manifest.pinnedSummary.surfaceUrl, "pinnedSummary.surfaceUrl"),
301
- presentation: summaryPresentation,
302
- overlayLabel: overlayLabel.value,
303
- overlayLabels: overlayLabel.locales,
304
- pinnedLabel: pinnedLabel.value,
305
- pinnedLabels: pinnedLabel.locales,
306
- } : null,
307
- detailTabs,
228
+ entry: { kind: entryKind },
308
229
  hostActions,
309
- modelSelector: isModelSelector
230
+ modelSelector: entryKind === "composer-model-selector"
310
231
  ? await normalizeModelSelector(manifest.modelSelector, projectDir)
311
232
  : null,
312
- pageScript: isPageScript ? await normalizePageScript(manifest, projectDir) : pageScript,
233
+ pageScript: entryKind === "page-script" ? await normalizePageScript(manifest, projectDir) : null,
313
234
  placement: {
314
- after: manifest.placement?.after ?? "sites",
315
235
  order: Number(manifest.placement?.order ?? 100),
316
236
  },
317
237
  command: Array.isArray(manifest.command) ? manifest.command.map(String) : null,
@@ -335,7 +255,6 @@ export async function discoverFeatures(featuresRoot, { env = process.env } = {})
335
255
  }
336
256
  try {
337
257
  const manifest = JSON.parse(source);
338
- if (manifest.disabled === true) continue;
339
258
  features.push(await normalizeFeature(manifest, projectDir, env));
340
259
  } catch (error) {
341
260
  throw new Error(`Invalid ${entry.name}/${MANIFEST_NAME}: ${error.message}`);
@@ -73,7 +73,6 @@ featureProcesses = await startFeatureProcesses(features, {
73
73
 
74
74
  runtime = new CodexRuntime({
75
75
  features,
76
- featureInstanceId,
77
76
  codexBuild:process.env.MINECODEX_CODEX_BUILD || null,
78
77
  profileDir,
79
78
  cdpPort,
@@ -144,33 +144,5 @@ export const CODEX_THEME_TOKENS = Object.freeze([
144
144
  "--color-text-danger-solid",
145
145
  ]);
146
146
 
147
- // images 图片编辑器可用性依赖的项目 token 子集。
148
- // 与 features/images/web/app.js 的 editorTokens 保持同一来源(一致性由测试锁定)。
149
- export const EDITOR_TOKEN_NAMES = Object.freeze([
150
- "--mcx-color-surface-tertiary",
151
- "--mcx-color-surface-elevated-secondary",
152
- "--mcx-color-border-subtle",
153
- "--mcx-color-background-primary-ghost-hover",
154
- "--mcx-color-chart-blue",
155
- "--mcx-color-text-inverse",
156
- "--mcx-color-token-main-surface-primary",
157
- "--mcx-color-token-input-foreground",
158
- "--mcx-color-text-foreground",
159
- "--mcx-color-text-foreground-tertiary",
160
- "--mcx-color-text-on-accent",
161
- "--mcx-color-border",
162
- "--mcx-color-border-focus",
163
- "--mcx-color-token-list-hover-background",
164
- "--mcx-color-text-accent",
165
- "--mcx-font-sans-default",
166
- "--mcx-font-weight-normal",
167
- "--mcx-text-sm",
168
- "--mcx-text-sm--line-height",
169
- "--mcx-radius-md",
170
- "--mcx-radius-xl",
171
- "--mcx-shadow-md",
172
- "--mcx-shadow-xl",
173
- ]);
174
-
175
147
  // 插件只消费 MineCodex 命名空间,原生名称变化集中在此映射。
176
148
  export const MINECODEX_THEME_BINDINGS = Object.freeze(Object.fromEntries(CODEX_THEME_TOKENS.map(name => [name, "--mcx-" + name.slice(2)])));
@@ -1,206 +0,0 @@
1
- import {previewContent} from './preview-content.js';
2
-
3
- export function installFeaturePreviews(grid, locale) {
4
- let zh = locale === 'zh-CN';
5
- const t = (cn, en) => zh ? cn : en;
6
- const lifetime = new AbortController();
7
- const on = (node, type, fn, options = {}) => node.addEventListener(type, fn, {...options, signal: lifetime.signal});
8
- const fine = matchMedia('(hover: hover) and (pointer: fine)');
9
- const reduced = matchMedia('(prefers-reduced-motion: reduce)');
10
- let current = null, panel = null, video = null, enterTimer, leaveTimer, replayTimer, pinned = false, generation = 0, restoring = false, mediaAbort = null;
11
- let manifest, hoveredCard = null, hoveredPanel = false, pendingCard = null;
12
- const media = () => manifest ??= window.minecodexPreviewMedia !== undefined
13
- ? Promise.resolve(window.minecodexPreviewMedia)
14
- : fetch('/assets/previews/manifest.json').then(r => r.ok ? r.json() : {}).then(value => value && typeof value === 'object' ? value : {}).catch(() => ({}));
15
- const node = (tag, cls, text) => {
16
- const el = document.createElement(tag); el.className = cls; if (text) el.textContent = text; return el;
17
- };
18
- async function resourceUrl(id, kind, value, signal) {
19
- if (!window.minecodexMcpApp) return `/assets/previews/${value}`;
20
- const response = await window.minecodexMcpApp.readServerResource({uri:`resource://minecodex/previews/${id}/${kind}`}, {signal});
21
- const resource = response.contents?.[0];
22
- if (!resource?.blob) throw new Error('Preview resource unavailable');
23
- return `data:${resource.mimeType};base64,${resource.blob}`;
24
- }
25
- function close({restore = false} = {}) {
26
- clearTimeout(enterTimer); clearTimeout(leaveTimer); clearTimeout(replayTimer); generation++;
27
- mediaAbort?.abort(); mediaAbort = null;
28
- // 先释放状态再移除节点,避免焦点退出事件重入清理。
29
- const closingPanel = panel, card = current, player = video;
30
- panel = null; current = null; video = null; pinned = false;
31
- if (player) { player.pause(); player.removeAttribute('src'); player.load(); }
32
- const trigger = card?.querySelector('.preview-entry');
33
- trigger?.setAttribute('aria-expanded', 'false');
34
- trigger?.removeAttribute('aria-controls');
35
- card?.classList.remove('is-previewing');
36
- grid.classList.remove('has-preview');
37
- closingPanel?.remove();
38
- if (restore && trigger?.isConnected) { restoring = true; trigger.focus({preventScroll: true}); restoring = false; }
39
- }
40
- function position() {
41
- if (!current || !panel) return;
42
- const rect = current.getBoundingClientRect();
43
- const width = Math.min(360, innerWidth - 32);
44
- panel.style.width = `${width}px`;
45
- panel.style.maxHeight = `${Math.max(120, innerHeight - 32)}px`;
46
- const height = panel.getBoundingClientRect().height;
47
- let left, top;
48
- if (innerWidth - rect.right >= width + 24) left = rect.right + 8;
49
- else if (rect.left >= width + 24) left = rect.left - width - 8;
50
- else {
51
- left = Math.max(16, Math.min(rect.left, innerWidth - width - 16));
52
- // 窄屏把面板放到开关下方,避免挡住当前操作。
53
- const below = innerHeight - rect.bottom - 24, above = rect.top - 24;
54
- if (below >= Math.min(height, 180)) {
55
- panel.style.maxHeight = `${below}px`; top = rect.bottom + 8;
56
- } else if (above >= Math.min(height, 180)) {
57
- panel.style.maxHeight = `${above}px`; top = Math.max(16, rect.top - height - 8);
58
- } else {
59
- top = Math.max(16, rect.top + current.querySelector('.plugin-topline').getBoundingClientRect().height + 32);
60
- panel.style.maxHeight = `${Math.max(100, innerHeight - top - 16)}px`;
61
- }
62
- }
63
- top ??= Math.max(16, Math.min(rect.top, innerHeight - height - 16));
64
- Object.assign(panel.style, {left: `${left}px`, top: `${top}px`, visibility: 'visible'});
65
- }
66
- function scheduleClose() {
67
- if (!pinned && !hoveredCard && !hoveredPanel) leaveTimer = setTimeout(() => close(), 100);
68
- }
69
- async function addMedia(id, host, state, ticket) {
70
- const resources = (await media())?.[id];
71
- if (ticket !== generation || !panel || !resources) return;
72
- mediaAbort = new AbortController();
73
- const mediaSignal = mediaAbort.signal;
74
- let posterUrl, videoUrl, resourceFailed = false;
75
- state.textContent = t('正在加载预览…', 'Loading preview…');
76
- const urls = await Promise.all(['poster', 'video'].map(async kind => {
77
- if (!resources[kind]) return null;
78
- try { return await resourceUrl(id, kind, resources[kind], mediaSignal); }
79
- catch { resourceFailed = true; return null; }
80
- }));
81
- if (ticket !== generation) return;
82
- [posterUrl, videoUrl] = urls;
83
- state.textContent = resourceFailed ? t('资源暂不可用 · 请参考说明', 'Preview unavailable · See description') : t('演示视频准备中', 'Demo video coming soon');
84
- let poster = null;
85
- if (posterUrl) {
86
- poster = node('img', 'preview-poster'); poster.alt = t('功能静态预览', 'Static feature preview');
87
- poster.addEventListener('load', position, {once: true});
88
- poster.addEventListener('error', () => { poster.remove(); resourceFailed = true; if (!video) state.textContent = t('资源暂不可用 · 请参考说明', 'Preview unavailable · See description'); position(); }, {once: true});
89
- poster.src = posterUrl; host.append(poster);
90
- state.textContent = t('静态预览', 'Static preview');
91
- }
92
- if (!videoUrl) { position(); return; }
93
- if (reduced.matches) { position(); return; }
94
- const player = node('video', 'preview-video'); video = player;
95
- player.muted = true; player.playsInline = true; player.preload = 'metadata'; player.hidden = true;
96
- const fallback = () => {
97
- panel.classList.remove('is-media-only');
98
- if (poster) poster.hidden = false;
99
- };
100
- const failed = () => {
101
- if (ticket !== generation) return;
102
- clearTimeout(replayTimer); player.pause(); player.removeAttribute('src'); player.load(); player.remove(); video = null; fallback();
103
- state.textContent = poster?.isConnected ? t('视频暂不可用 · 静态预览', 'Video unavailable · Static preview') : t('视频暂不可用 · 请参考说明', 'Video unavailable · See description'); position();
104
- };
105
- player.addEventListener('error', failed, {once: true});
106
- player.addEventListener('loadeddata', () => {
107
- if (ticket !== generation) return;
108
- position();
109
- });
110
- player.addEventListener('play', () => {
111
- if (ticket !== generation) return;
112
- player.hidden = false; if (poster) poster.hidden = true;
113
- panel.classList.add('is-media-only'); position();
114
- });
115
- const playVideo = async () => {
116
- if (ticket !== generation) return;
117
- try { await player.play(); }
118
- catch {
119
- if (ticket === generation && video === player && player.isConnected) {
120
- player.hidden = true; fallback();
121
- state.textContent = poster?.isConnected ? t('静态预览', 'Static preview') : t('演示视频准备中', 'Demo video coming soon'); position();
122
- }
123
- }
124
- };
125
- player.addEventListener('ended', () => {
126
- clearTimeout(replayTimer);
127
- replayTimer = setTimeout(() => {
128
- if (ticket !== generation || video !== player) return;
129
- player.currentTime = 0; void playVideo();
130
- }, 2000);
131
- });
132
- host.append(player); state.textContent = t('正在加载演示…', 'Loading demo…');
133
- player.src = videoUrl;
134
- void playVideo();
135
- position();
136
- }
137
- function open(card, fixed = false) {
138
- clearTimeout(enterTimer); clearTimeout(leaveTimer);
139
- if (card === current) { pinned ||= fixed; return; }
140
- close(); hoveredPanel = false; current = card; pinned = fixed;
141
- panel = node('section', 'feature-preview'); panel.id = 'feature-preview'; panel.setAttribute('role', 'region');
142
- const title = node('h3', 'preview-title', card.querySelector('.plugin-name').textContent);
143
- title.id = 'feature-preview-title'; panel.setAttribute('aria-labelledby', title.id);
144
- const dismiss = node('button', 'button quiet preview-close', t('关闭', 'Close')); dismiss.type = 'button';
145
- dismiss.addEventListener('click', () => close({restore: pinned || panel.contains(document.activeElement)}));
146
- const heading = node('div', 'preview-heading'); heading.append(title, dismiss); panel.append(heading);
147
- const host = node('div', 'preview-media'); panel.append(host);
148
- const details = node('dl', 'preview-details');
149
- const labels = [t('出现位置', 'Location'), t('解决问题', 'Problem'), t('操作与效果', 'Action and result')];
150
- previewContent(card.dataset.featureId, locale).forEach((value, index) => {
151
- details.append(node('dt', '', labels[index]), node('dd', '', value));
152
- });
153
- const state = node('p', 'preview-resource-state', t('演示视频准备中', 'Demo video coming soon'));
154
- panel.append(details, state);
155
- panel.addEventListener('pointerenter', () => { hoveredPanel = true; clearTimeout(leaveTimer); });
156
- panel.addEventListener('pointerleave', () => { hoveredPanel = false; scheduleClose(); });
157
- panel.addEventListener('focusin', () => { clearTimeout(leaveTimer); });
158
- panel.addEventListener('focusout', event => { if (!panel?.contains(event.relatedTarget) && !current?.contains(event.relatedTarget)) close(); });
159
- document.querySelector('.dashboard-surface').append(panel);
160
- card.classList.add('is-previewing'); grid.classList.add('has-preview');
161
- const trigger = card.querySelector('.preview-entry'); trigger.setAttribute('aria-expanded', 'true'); trigger.setAttribute('aria-controls', panel.id);
162
- position(); void addMedia(card.dataset.featureId, host, state, generation);
163
- }
164
- function attach(card) {
165
- const entry = node('button', 'preview-entry'); entry.type = 'button'; entry.setAttribute('aria-expanded', 'false');
166
- entry.setAttribute('aria-label', `${t('查看效果', 'See how it works')} · ${card.querySelector('.plugin-name').textContent || card.dataset.featureId}`);
167
- card.append(entry);
168
- on(card, 'pointerenter', () => {
169
- if (!fine.matches) return;
170
- hoveredCard = card; pendingCard = card;
171
-
172
- clearTimeout(enterTimer); clearTimeout(leaveTimer);
173
- enterTimer = setTimeout(() => { pendingCard = null; open(card); }, 200);
174
- });
175
- on(card, 'pointerleave', () => {
176
- if (hoveredCard === card) hoveredCard = null;
177
- if (pendingCard === card) { clearTimeout(enterTimer); pendingCard = null; }
178
- scheduleClose();
179
- });
180
- on(entry, 'focus', () => { if (!restoring) open(card); });
181
- on(entry, 'click', () => open(card, true));
182
- on(card, 'focusout', event => { if (!card.contains(event.relatedTarget) && !panel?.contains(event.relatedTarget) && document.documentElement.dataset.input === 'keyboard') close(); });
183
- }
184
- on(document, 'keydown', event => {
185
- if (event.key === 'Escape' && current) { event.preventDefault(); const restore = pinned || panel.contains(document.activeElement); close({restore}); }
186
- });
187
- on(document, 'pointerdown', event => {
188
- if (!current) return;
189
- if (!current.contains(event.target) && !panel.contains(event.target)) close();
190
- });
191
- on(window, 'scroll', event => {
192
- if (panel?.contains(event.target)) return;
193
- close();
194
- }, {capture: true});
195
- on(window, 'resize', () => close());
196
- on(document, 'visibilitychange', () => { if (document.hidden) close(); });
197
- on(window, 'blur', () => close());
198
- on(document.querySelector('#data-dialog'), 'toggle', () => close());
199
- on(document.querySelector('#language-menu'), 'toggle', () => close());
200
- on(window, 'pagehide', () => { close(); lifetime.abort(); });
201
- function setLocale(next) {
202
- close(); locale = next; zh = locale === 'zh-CN';
203
- for (const card of grid.querySelectorAll('.plugin-card')) card.querySelector('.preview-entry')?.setAttribute('aria-label', `${t('查看效果', 'See how it works')} · ${card.querySelector('.plugin-name').textContent}`);
204
- }
205
- return {attach, close, setLocale};
206
- }