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.
- package/README.md +90 -26
- package/apps/dashboard/app.js +20 -14
- package/apps/dashboard/assets/chevron-left.svg +13 -0
- package/apps/dashboard/assets/chevron-right.svg +13 -0
- package/apps/dashboard/assets/feature-showcase.js +185 -0
- package/apps/dashboard/assets/pause.svg +14 -0
- package/apps/dashboard/assets/play.svg +13 -0
- package/apps/dashboard/assets/previews/composer-expand.jpg +0 -0
- package/apps/dashboard/assets/previews/composer-expand.mp4 +0 -0
- package/apps/dashboard/assets/previews/file-navigation.jpg +0 -0
- package/apps/dashboard/assets/previews/file-navigation.mp4 +0 -0
- package/apps/dashboard/assets/previews/files-auto-refresh.jpg +0 -0
- package/apps/dashboard/assets/previews/files-auto-refresh.mp4 +0 -0
- package/apps/dashboard/assets/previews/images.jpg +0 -0
- package/apps/dashboard/assets/previews/images.mp4 +0 -0
- package/apps/dashboard/assets/previews/manifest.json +67 -1
- package/apps/dashboard/assets/previews/markdown-reading.jpg +0 -0
- package/apps/dashboard/assets/previews/markdown-reading.mp4 +0 -0
- package/apps/dashboard/assets/previews/mermaid-diagrams.jpg +0 -0
- package/apps/dashboard/assets/previews/mermaid-diagrams.mp4 +0 -0
- package/apps/dashboard/assets/previews/model-slider.jpg +0 -0
- package/apps/dashboard/assets/previews/model-slider.mp4 +0 -0
- package/apps/dashboard/assets/previews/notes.jpg +0 -0
- package/apps/dashboard/assets/previews/notes.mp4 +0 -0
- package/apps/dashboard/assets/previews/quick-chat.jpg +0 -0
- package/apps/dashboard/assets/previews/quick-chat.mp4 +0 -0
- package/apps/dashboard/assets/previews/session-status.jpg +0 -0
- package/apps/dashboard/assets/previews/session-status.mp4 +0 -0
- package/apps/dashboard/assets/previews/sidebar-focus.jpg +0 -0
- package/apps/dashboard/assets/previews/sidebar-focus.mp4 +0 -0
- package/apps/dashboard/assets/previews/sites.jpg +0 -0
- package/apps/dashboard/assets/previews/sites.mp4 +0 -0
- package/apps/dashboard/assets/previews/usage-meter.jpg +0 -0
- package/apps/dashboard/assets/previews/usage-meter.mp4 +0 -0
- package/apps/dashboard/index.html +26 -2
- package/apps/dashboard/styles.css +58 -39
- package/assets/readme/hero-dark.svg +50 -0
- package/assets/readme/hero.svg +36 -79
- package/features/images/src/adapters/codex/rspack-modules.mjs +9 -3
- package/features/images/src/library-prompt-source.mjs +2 -2
- package/features/notes/codex-feature.json +1 -0
- package/features/quick-chat/src/adapters/codex/renderer.mjs +3 -1
- package/package.json +3 -3
- package/packages/cli/src/config.mjs +1 -2
- package/packages/cli/src/control-server.mjs +1 -1
- package/packages/plugin/assets/app.js +35 -35
- package/packages/runtime-host/src/codex-cdp.mjs +0 -7
- package/packages/runtime-host/src/codex-injection.mjs +50 -2114
- package/packages/runtime-host/src/codex-runtime.mjs +4 -203
- package/packages/runtime-host/src/feature-registry.mjs +10 -91
- package/packages/runtime-host/src/main.mjs +0 -1
- package/packages/ui/src/native/tokens.mjs +0 -28
- package/apps/dashboard/assets/feature-previews.js +0 -206
- package/features/images/src/adapters/codex/cloud-import.mjs +0 -52
- package/features/images/src/backend/cloud-library.mjs +0 -12
- package/features/images/src/chatgpt-image-source-page.mjs +0 -207
- package/features/images/src/chatgpt-image-source.mjs +0 -42
- package/features/images/src/http-server.mjs +0 -487
- package/features/images/src/main.mjs +0 -52
- package/features/images/src/save-as.mjs +0 -69
- package/features/images/web/app.js +0 -1780
- package/features/images/web/i18n.mjs +0 -132
- package/features/images/web/index.html +0 -187
- package/features/images/web/styles.css +0 -1114
- 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
|
|
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
|
-
|
|
612
|
-
|
|
613
|
-
|
|
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
|
|
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
|
|
166
|
+
const resource = manifest.entry?.script;
|
|
176
167
|
if (!resource) {
|
|
177
|
-
throw new Error("page-script entries require entry.script
|
|
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
|
|
222
|
-
if (!
|
|
223
|
-
throw new Error("entry.kind must be
|
|
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
|
-
|
|
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:
|
|
230
|
+
modelSelector: entryKind === "composer-model-selector"
|
|
310
231
|
? await normalizeModelSelector(manifest.modelSelector, projectDir)
|
|
311
232
|
: null,
|
|
312
|
-
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}`);
|
|
@@ -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
|
-
}
|