minecodex 2.0.12 → 2.0.14
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 +20 -8
- package/apps/dashboard/app.js +4 -4
- package/apps/dashboard/assets/data-transfer.js +129 -0
- package/apps/dashboard/assets/download.svg +15 -0
- package/apps/dashboard/assets/upload.svg +15 -0
- package/apps/dashboard/index.html +10 -0
- package/apps/dashboard/styles.css +50 -3
- package/features/file-navigation/assets/book-open.svg +16 -0
- package/features/file-navigation/assets/folder-tree.svg +16 -0
- package/features/file-navigation/codex-feature.json +27 -0
- package/features/file-navigation/src/adapters/codex/modules.mjs +67 -0
- package/features/file-navigation/src/adapters/codex/navigation.mjs +84 -0
- package/features/file-navigation/src/domain/navigation.mjs +12 -0
- package/features/file-navigation/src/entry.mjs +34 -0
- package/features/file-navigation/src/frontend/navigation.mjs +140 -0
- package/features/file-navigation/src/injection-source.mjs +9 -0
- package/features/file-navigation/src/navigation.css +16 -0
- package/features/file-navigation/vendor/lucide-LICENSE +43 -0
- package/features/hide-upsell-banner/codex-feature.json +4 -4
- package/features/images/codex-feature.json +4 -4
- package/features/images/src/adapters/codex/native-image-modules.mjs +13 -2
- package/features/images/src/library-prompt-source.mjs +2 -2
- package/features/markdown-reading/assets/book-open.svg +16 -0
- package/features/markdown-reading/codex-feature.json +27 -0
- package/features/markdown-reading/src/adapters/codex/links.mjs +280 -0
- package/features/markdown-reading/src/adapters/codex/lists.mjs +168 -0
- package/features/markdown-reading/src/adapters/codex/markdown-native.mjs +28 -0
- package/features/markdown-reading/src/adapters/codex/preview.mjs +38 -0
- package/features/markdown-reading/src/adapters/codex/providers.mjs +18 -0
- package/features/markdown-reading/src/adapters/codex/reading-runtime.mjs +152 -0
- package/features/markdown-reading/src/adapters/codex/rich-text.mjs +561 -0
- package/features/markdown-reading/src/adapters/codex/table-actions.mjs +270 -0
- package/features/markdown-reading/src/adapters/codex/tables.mjs +493 -0
- package/features/markdown-reading/src/adapters/codex/toc-native.mjs +131 -0
- package/features/markdown-reading/src/adapters/codex/toc.mjs +334 -0
- package/features/markdown-reading/src/domain/footnotes.mjs +56 -0
- package/features/markdown-reading/src/injection-source.mjs +43 -0
- package/features/markdown-reading/src/markdown-reading.mjs +82 -0
- package/features/markdown-reading/src/reading.css +346 -0
- package/features/markdown-reading/vendor/lucide-LICENSE +43 -0
- package/features/model-slider/codex-feature.json +5 -4
- package/features/model-slider/src/backend/favorites.mjs +54 -0
- package/features/model-slider/src/domain/model-rules.mjs +6 -1
- package/features/model-slider/src/frontend/install.mjs +24 -3
- package/features/notes/assets/prompt-previews.json +26 -0
- package/features/notes/codex-feature.json +6 -5
- package/features/notes/src/backend/transfer.mjs +97 -0
- package/features/notes/src/frontend/entry.mjs +380 -10
- package/features/notes/src/frontend/icons.json +4 -1
- package/features/notes/src/frontend/styles.css +63 -0
- package/features/session-status/codex-feature.json +6 -3
- package/features/session-status/src/domain/session-stats.mjs +22 -8
- package/features/session-status/src/session-status.mjs +2 -2
- package/features/sidebar-focus/codex-feature.json +2 -2
- package/features/sidebar-focus/src/sidebar-focus.mjs +13 -5
- package/features/sites/codex-feature.json +4 -4
- package/features/sites/src/backend/sites-service.mjs +20 -5
- package/features/sites/src/backend/transfer.mjs +43 -0
- package/features/usage-meter/codex-feature.json +6 -3
- package/package.json +11 -3
- package/packages/cli/src/config.mjs +4 -2
- package/packages/cli/src/control-server.mjs +34 -10
- package/packages/cli/src/data-transfer/service.mjs +42 -0
- package/packages/cli/src/runtime-manager.mjs +3 -0
- package/packages/runtime-host/src/codex-injection.mjs +2 -1
- package/packages/runtime-host/src/codex-runtime.mjs +24 -1
- package/packages/runtime-host/src/feature-registry.mjs +1 -0
- package/packages/ui/src/native/theme.mjs +23 -1
- package/packages/ui/src/native/tokens.mjs +4 -4
|
@@ -7,6 +7,7 @@ function addSections(target, sections) {
|
|
|
7
7
|
export function createSessionState() {
|
|
8
8
|
return {
|
|
9
9
|
turns: new Set(),
|
|
10
|
+
currentTurnId: null,
|
|
10
11
|
runningTurns: new Set(),
|
|
11
12
|
responses: new Set(),
|
|
12
13
|
hasResponseUsage: false,
|
|
@@ -58,13 +59,15 @@ export function applySessionStatusEvent(state, event) {
|
|
|
58
59
|
state.hasVisibleHistory=event.sections !== null;
|
|
59
60
|
break;
|
|
60
61
|
case "history-added": addSections(state.visibleSections,event.sections); state.hasVisibleHistory=true; break;
|
|
61
|
-
case "turn-observed":
|
|
62
|
+
case "turn-observed":
|
|
63
|
+
if (state.currentTurnId !== event.id) reset();
|
|
64
|
+
state.currentTurnId=event.id; state.turns.add(event.id); break;
|
|
62
65
|
case "session-configured":
|
|
63
66
|
if (Number.isFinite(event.contextWindow)) state.contextWindow=event.contextWindow;
|
|
64
67
|
state.baseInstructionTokens=event.instructionTokens; break;
|
|
65
68
|
case "context-reset": state.compactionEvents++; state.last=null; reset(); break;
|
|
66
|
-
case "turn-started": reset(); state.turns.add(event.id); state.runningTurns.add(event.id); break;
|
|
67
|
-
case "turn-ended": reset(); if (event.id) state.runningTurns.delete(event.id); else state.runningTurns.clear(); break;
|
|
69
|
+
case "turn-started": reset(); state.currentTurnId=event.id; state.turns.add(event.id); state.runningTurns.add(event.id); break;
|
|
70
|
+
case "turn-ended": reset(); state.currentTurnId=null; if (event.id) state.runningTurns.delete(event.id); else state.runningTurns.clear(); break;
|
|
68
71
|
case "first-token-measured": state.ttftTotalMs+=event.elapsed; state.ttftSamples++; break;
|
|
69
72
|
case "compaction-finished": state.compactionCompleted++; break;
|
|
70
73
|
case "output-observed":
|
|
@@ -88,7 +91,7 @@ export function applySessionStatusEvent(state, event) {
|
|
|
88
91
|
state.responses.add(event.id);
|
|
89
92
|
const output=event.last?.output;
|
|
90
93
|
if (Number.isFinite(output) && output > 0 && state.firstOutputAt != null && Number.isFinite(event.end) && event.end > state.firstOutputAt)
|
|
91
|
-
state.speedMeasurements.push({ start:state.firstOutputAt,end:event.end,output,
|
|
94
|
+
state.speedMeasurements.push({ turnId:state.currentTurnId,start:state.firstOutputAt,end:event.end,output,
|
|
92
95
|
outputs:state.pendingOutputIntervals.slice(),tools:state.pendingToolIntervals.slice() });
|
|
93
96
|
}
|
|
94
97
|
reset();
|
|
@@ -103,12 +106,23 @@ export function sessionStatusSnapshot(state, threadId) {
|
|
|
103
106
|
let decodeTokens = 0;
|
|
104
107
|
let decodeMs = 0;
|
|
105
108
|
let speedSamples = 0;
|
|
109
|
+
const turnSpeeds = new Map();
|
|
106
110
|
for (const sample of state.speedMeasurements) {
|
|
107
111
|
const elapsed = sample.end - sample.start - boundedToolWaitMs(sample.tools, sample.start, sample.end, sample.outputs);
|
|
108
|
-
if (elapsed <= 0 || elapsed >= 600_000) continue;
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
+
if (!sample.turnId || elapsed <= 0 || elapsed >= 600_000) continue;
|
|
113
|
+
const turn = turnSpeeds.get(sample.turnId) ?? { tokens:0, ms:0, samples:0 };
|
|
114
|
+
turn.tokens += sample.output;
|
|
115
|
+
turn.ms += elapsed;
|
|
116
|
+
turn.samples += 1;
|
|
117
|
+
turnSpeeds.set(sample.turnId, turn);
|
|
118
|
+
}
|
|
119
|
+
// 按回合顺序取最近三个有效回合;同轮多步合并,无数据的回合不占名额。
|
|
120
|
+
const recentTurns = [...state.turns].filter(id => turnSpeeds.has(id)).slice(-3);
|
|
121
|
+
for (const id of recentTurns) {
|
|
122
|
+
const turn = turnSpeeds.get(id);
|
|
123
|
+
decodeTokens += turn.tokens;
|
|
124
|
+
decodeMs += turn.ms;
|
|
125
|
+
speedSamples += turn.samples;
|
|
112
126
|
}
|
|
113
127
|
const total = state.total;
|
|
114
128
|
const input = total?.input ?? 0;
|
|
@@ -7,14 +7,14 @@ const copyByLocale = { "zh-CN": {
|
|
|
7
7
|
firstToken: "平均首字延迟", input: "输入", cached: "缓存输入", output: "输出",
|
|
8
8
|
sum: "合计", capacity: "窗口容量", contextShare: "上下文占用", compactions: "压缩次数",
|
|
9
9
|
instructions: "指令", messages: "消息", toolHistory: "工具交互", other: "未分类",
|
|
10
|
-
speedHelp: "关于 TPS", speedInfo: "GPT 等模型不公开完整思考过程及逐 Token
|
|
10
|
+
speedHelp: "关于 TPS", speedInfo: "GPT 等模型不公开完整思考过程及逐 Token 生成时间。TPS 取最近 3 个有有效数据的回合,不足 3 轮时按已有数据计算。它是基于可观测事件估算的体感响应速度,仅供参考,不代表模型真实的 Token 生成速度。",
|
|
11
11
|
}, en: {
|
|
12
12
|
turns: "turns", steps: "steps", speed: "TPS", cache: "Cache", cacheDetail: "Cache hit",
|
|
13
13
|
statistics: "Session stats", usage: "Token usage", context: "Context", total: "Total",
|
|
14
14
|
firstToken: "Avg first token", input: "Input", cached: "Cached input", output: "Output",
|
|
15
15
|
sum: "Sum", capacity: "Window size", contextShare: "Context used", compactions: "Compactions",
|
|
16
16
|
instructions: "Instructions", messages: "Messages", toolHistory: "Tool activity", other: "Unclassified",
|
|
17
|
-
speedHelp: "About TPS", speedInfo: "Models such as GPT do not expose their full reasoning process or per-token generation timing.
|
|
17
|
+
speedHelp: "About TPS", speedInfo: "Models such as GPT do not expose their full reasoning process or per-token generation timing. TPS uses the latest 3 turns with valid timing data, or fewer if unavailable. It estimates perceived response speed from observable events, rather than the model’s actual token generation speed.",
|
|
18
18
|
} };
|
|
19
19
|
function selectedCopy() {
|
|
20
20
|
const language = document.documentElement.lang || navigator.languages?.[0] || navigator.language || "en";
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
"zh-CN": "侧栏聚焦"
|
|
8
8
|
},
|
|
9
9
|
"description": {
|
|
10
|
-
"en": "
|
|
11
|
-
"zh-CN": "
|
|
10
|
+
"en": "Collapse other sidebar groups with one click, then click again to restore them.",
|
|
11
|
+
"zh-CN": "一键收起侧栏中的其他分组,再次点击恢复原来的展开状态。"
|
|
12
12
|
},
|
|
13
13
|
"entry": {
|
|
14
14
|
"kind": "page-script",
|
|
@@ -4,7 +4,7 @@ const EXPAND_GLYPH = "<svg aria-hidden=\"true\"\n xmlns=\"http://www.w3.org/200
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
const states = new Map();
|
|
7
|
-
let active = false, restoring = false, button, timer, disposed = false;
|
|
7
|
+
let active = false, restoring = false, button, timer = null, disposed = false;
|
|
8
8
|
const text = (en, cn) => (document.documentElement.lang || navigator.language).startsWith("zh") ? cn : en;
|
|
9
9
|
|
|
10
10
|
|
|
@@ -74,8 +74,8 @@ function reconcile() {
|
|
|
74
74
|
if (change) { change.toggle(); schedule(); }
|
|
75
75
|
}
|
|
76
76
|
function schedule() {
|
|
77
|
-
|
|
78
|
-
timer =
|
|
77
|
+
if (disposed || timer !== null) return;
|
|
78
|
+
timer = requestAnimationFrame(() => { timer = null; reconcile(); });
|
|
79
79
|
}
|
|
80
80
|
document.addEventListener("click", event => {
|
|
81
81
|
if (!active || !event.isTrusted) return;
|
|
@@ -83,7 +83,15 @@ document.addEventListener("click", event => {
|
|
|
83
83
|
active = false; states.clear(); schedule();
|
|
84
84
|
}
|
|
85
85
|
}, { capture:true, signal });
|
|
86
|
-
const observer = new MutationObserver(
|
|
86
|
+
const observer = new MutationObserver(records => {
|
|
87
|
+
// 流式回复等侧栏外更新不应推迟或重复触发布局整理。
|
|
88
|
+
if (records.some(record =>
|
|
89
|
+
record.target.closest?.("#app-shell-sidebar")
|
|
90
|
+
|| record.type === "attributes" && ["data-above-composer-conversation-id", "lang"].includes(record.attributeName)
|
|
91
|
+
|| [...record.addedNodes ?? [], ...record.removedNodes ?? []].some(node =>
|
|
92
|
+
node.matches?.("#app-shell-sidebar") || node.querySelector?.("#app-shell-sidebar"))
|
|
93
|
+
)) schedule();
|
|
94
|
+
});
|
|
87
95
|
observer.observe(document.documentElement, { subtree:true, childList:true, attributes:true, attributeFilter:[
|
|
88
96
|
"aria-expanded", "aria-pressed", "data-app-action-sidebar-thread-selected",
|
|
89
97
|
"data-app-action-sidebar-section-collapsed", "data-above-composer-conversation-id", "lang"
|
|
@@ -97,4 +105,4 @@ config.registerSelfCheck?.(() => {
|
|
|
97
105
|
return {state:available ? "available" : "unavailable",reason:available ? null : "sidebar-focus-anchor-unavailable",
|
|
98
106
|
checks:[{name:"sidebar-focus-anchor",available}],scopes:[{name:"sidebar",element:sidebar}]};
|
|
99
107
|
});
|
|
100
|
-
return () => { disposed = true;
|
|
108
|
+
return () => { disposed = true; cancelAnimationFrame(timer); observer.disconnect(); button?.remove(); };
|
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
"id": "sites",
|
|
4
4
|
"version": "0.1.0",
|
|
5
5
|
"label": {
|
|
6
|
-
"en": "
|
|
7
|
-
"zh-CN": "
|
|
6
|
+
"en": "Saved Sites",
|
|
7
|
+
"zh-CN": "站点收藏"
|
|
8
8
|
},
|
|
9
9
|
"description": {
|
|
10
|
-
"en": "Find project
|
|
11
|
-
"zh-CN": "
|
|
10
|
+
"en": "Find running project sites and save useful links for quick access.",
|
|
11
|
+
"zh-CN": "发现项目正在运行的网页,收藏常用网址,随时打开。"
|
|
12
12
|
},
|
|
13
13
|
"entry": {
|
|
14
14
|
"kind": "page-script",
|
|
@@ -3,6 +3,8 @@ import { localListeners, probePage } from "./discovery.mjs";
|
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import { mkdir, readFile, rename, writeFile, realpath } from "node:fs/promises";
|
|
5
5
|
import os from "node:os";
|
|
6
|
+
import { readFileSync, writeFileSync, renameSync, rmSync } from "node:fs";
|
|
7
|
+
import { DatabaseSync } from "node:sqlite";
|
|
6
8
|
import path from "node:path";
|
|
7
9
|
import { CodexCatalog } from "../adapters/codex/catalog.mjs";
|
|
8
10
|
|
|
@@ -21,17 +23,30 @@ export class SitesService {
|
|
|
21
23
|
this.catalogReader = catalog; this.listeners = listeners; this.probe = probe; this.queue = Promise.resolve();
|
|
22
24
|
}
|
|
23
25
|
catalog() { return this.catalogReader.snapshot(); }
|
|
24
|
-
|
|
25
|
-
try { const data = JSON.parse(
|
|
26
|
+
readStoredSites() {
|
|
27
|
+
try { const data = JSON.parse(readFileSync(this.file, "utf8")); if (data.schemaVersion !== 1 || !Array.isArray(data.sites)) throw new Error("Unsupported sites data."); return data.sites.map(site => site.url ? { ...site, hosting: isLocalAddress(new URL(site.url).hostname) ? "local" : "self-hosted" } : site); }
|
|
26
28
|
catch (error) { if (error.code === "ENOENT") return []; throw error; }
|
|
27
29
|
}
|
|
30
|
+
async read() { return this.readStoredSites(); }
|
|
28
31
|
async mutate(operation) {
|
|
29
32
|
const task = this.queue.catch(() => {}).then(async () => {
|
|
30
|
-
const sites = await this.read(); const result = operation(sites);
|
|
31
33
|
await mkdir(path.dirname(this.file), { recursive: true, mode: 0o700 });
|
|
34
|
+
// SQLite 仅协调跨进程写锁,进程退出自动释放;业务数据仍在 sites.json。
|
|
35
|
+
const lock = new DatabaseSync(path.join(path.dirname(this.file), "sites-write-lock.sqlite"));
|
|
32
36
|
const temporary = `${this.file}.${randomUUID()}.tmp`;
|
|
33
|
-
|
|
34
|
-
|
|
37
|
+
try {
|
|
38
|
+
lock.exec("PRAGMA busy_timeout=3000; BEGIN IMMEDIATE");
|
|
39
|
+
// 锁内不让出事件循环,避免同进程另一个写入等待自己。
|
|
40
|
+
const sites = this.readStoredSites(); const result = operation(sites);
|
|
41
|
+
writeFileSync(temporary, JSON.stringify({ schemaVersion: 1, sites }, null, 2) + "\n", { mode: 0o600 });
|
|
42
|
+
renameSync(temporary, this.file);
|
|
43
|
+
lock.exec("COMMIT");
|
|
44
|
+
return result;
|
|
45
|
+
} finally {
|
|
46
|
+
if (lock.isTransaction) lock.exec("ROLLBACK");
|
|
47
|
+
lock.close();
|
|
48
|
+
rmSync(temporary, { force: true });
|
|
49
|
+
}
|
|
35
50
|
});
|
|
36
51
|
this.queue = task; return task;
|
|
37
52
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { SitesService } from './sites-service.mjs';
|
|
2
|
+
import { normalizeSite, isLocalAddress } from '../domain/site.mjs';
|
|
3
|
+
|
|
4
|
+
function portable(row) {
|
|
5
|
+
if (!row.url) return null;
|
|
6
|
+
const site = normalizeSite({ title: row.title, url: row.url, origin: { kind: 'external' } });
|
|
7
|
+
if (isLocalAddress(new URL(site.url).hostname) || new URL(site.url).hostname === '0.0.0.0' || new URL(site.url).hostname === '[::]') return null;
|
|
8
|
+
return { id: row.id, title: site.title, url: site.url };
|
|
9
|
+
}
|
|
10
|
+
export function validateSites(data) {
|
|
11
|
+
if (!data || data.version !== 1 || !Array.isArray(data.sites) || data.sites.length > 50000) throw new Error('Unsupported Sites data');
|
|
12
|
+
const ids = new Set();
|
|
13
|
+
return { version: 1, sites: data.sites.map(row => {
|
|
14
|
+
if (!row || typeof row.id !== 'string' || !row.id || row.id.length > 256 || ids.has(row.id) || typeof row.title !== 'string' || row.title.length > 160 || typeof row.url !== 'string' || row.url.length > 8192) throw new Error('Invalid Site record');
|
|
15
|
+
const clean = portable(row);
|
|
16
|
+
if (!clean) throw new Error('Local URLs cannot be transferred');
|
|
17
|
+
ids.add(row.id); return clean;
|
|
18
|
+
}) };
|
|
19
|
+
}
|
|
20
|
+
function merge(sites, incoming) {
|
|
21
|
+
const result = { added: 0, duplicate: 0, conflict: 0 };
|
|
22
|
+
const ids = new Map(sites.map(row => [row.id,row])), urls = new Set(sites.map(row => row.url));
|
|
23
|
+
for (const row of incoming.sites) {
|
|
24
|
+
const match = ids.get(row.id);
|
|
25
|
+
if (match) result[match.url === row.url && match.title === row.title ? 'duplicate' : 'conflict']++;
|
|
26
|
+
else if (urls.has(row.url)) result.duplicate++;
|
|
27
|
+
else {
|
|
28
|
+
result.added++;
|
|
29
|
+
ids.set(row.id,row); urls.add(row.url);
|
|
30
|
+
sites.push({ ...row, origin: { kind: 'external' }, hosting: 'self-hosted', createdAt: Date.now(), updatedAt: Date.now() });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
export function sitesTransfer(dataDir) {
|
|
36
|
+
const service = new SitesService({ dataDir });
|
|
37
|
+
return {
|
|
38
|
+
id: 'sites', validate: validateSites,
|
|
39
|
+
async snapshot() { const all = await service.read(), sites = all.map(portable).filter(Boolean); return { data: { version: 1, sites }, count: sites.length, excluded: all.length - sites.length }; },
|
|
40
|
+
async preview(data) { return merge(await service.read(), data); },
|
|
41
|
+
async apply(data) { return service.mutate(sites => merge(sites, data)); },
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -2,10 +2,13 @@
|
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"id": "usage-meter",
|
|
4
4
|
"version": "0.1.0",
|
|
5
|
-
"label": {
|
|
5
|
+
"label": {
|
|
6
|
+
"en": "Usage Ring",
|
|
7
|
+
"zh-CN": "额度环"
|
|
8
|
+
},
|
|
6
9
|
"description": {
|
|
7
|
-
"en": "Check your
|
|
8
|
-
"zh-CN": "
|
|
10
|
+
"en": "Check your five-hour and weekly allowance in the sidebar; click for details.",
|
|
11
|
+
"zh-CN": "在侧栏查看五小时和每周剩余额度,点击打开用量详情。"
|
|
9
12
|
},
|
|
10
13
|
"entry": { "kind": "page-script", "script": "src/usage-meter.mjs", "sourceFactory": "src/injection-source.mjs" },
|
|
11
14
|
"icon": { "viewBox": "0 0 24 24", "markup": "<circle cx=\"12\" cy=\"12\" r=\"9\"></circle><circle cx=\"12\" cy=\"12\" r=\"5\"></circle>" },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "minecodex",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.14",
|
|
4
4
|
"description": "Lightweight, local-first plugins for the Codex desktop app.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -59,14 +59,22 @@
|
|
|
59
59
|
"assets/brand/minecodex-icon-dark.svg",
|
|
60
60
|
"assets/brand/references/lucide-LICENSE.txt",
|
|
61
61
|
"assets/readme/hero.svg",
|
|
62
|
-
"packages/ui/assets"
|
|
62
|
+
"packages/ui/assets",
|
|
63
|
+
"features/markdown-reading/src",
|
|
64
|
+
"features/markdown-reading/codex-feature.json",
|
|
65
|
+
"features/markdown-reading/vendor/lucide-LICENSE",
|
|
66
|
+
"features/markdown-reading/assets",
|
|
67
|
+
"features/file-navigation/src",
|
|
68
|
+
"features/file-navigation/codex-feature.json",
|
|
69
|
+
"features/file-navigation/assets",
|
|
70
|
+
"features/file-navigation/vendor/lucide-LICENSE"
|
|
63
71
|
],
|
|
64
72
|
"engines": {
|
|
65
73
|
"node": ">=22.5.0"
|
|
66
74
|
},
|
|
67
75
|
"scripts": {
|
|
68
76
|
"start": "node packages/cli/bin/mcx.mjs serve",
|
|
69
|
-
"test": "node --test packages/cli/test/*.test.mjs packages/runtime-host/test/*.test.mjs features/model-slider/test/*.test.mjs features/notes/test/*.test.mjs features/images/test/*.test.mjs features/hide-upsell-banner/test/*.test.mjs features/session-status/test/*.test.mjs features/usage-meter/test/*.test.mjs apps/dashboard/test/*.test.mjs features/unread-inbox/test/*.test.mjs features/sidebar-focus/test/*.test.mjs features/sites/test/*.test.mjs packages/ui/test/*.test.mjs packages/codex-adapter/test/*.test.mjs",
|
|
77
|
+
"test": "node --test packages/cli/test/*.test.mjs packages/runtime-host/test/*.test.mjs features/model-slider/test/*.test.mjs features/notes/test/*.test.mjs features/images/test/*.test.mjs features/hide-upsell-banner/test/*.test.mjs features/session-status/test/*.test.mjs features/usage-meter/test/*.test.mjs apps/dashboard/test/*.test.mjs features/unread-inbox/test/*.test.mjs features/sidebar-focus/test/*.test.mjs features/sites/test/*.test.mjs packages/ui/test/*.test.mjs packages/codex-adapter/test/*.test.mjs features/markdown-reading/test/*.test.mjs features/file-navigation/test/*.test.mjs",
|
|
70
78
|
"test:cli": "node --test packages/cli/test/*.test.mjs",
|
|
71
79
|
"test:runtime": "node --test packages/runtime-host/test/*.test.mjs",
|
|
72
80
|
"check": "node scripts/check-syntax.mjs",
|
|
@@ -2,7 +2,7 @@ import { randomBytes } from "node:crypto";
|
|
|
2
2
|
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
|
-
export const FEATURE_IDS = Object.freeze(["model-slider", "notes", "images", "hide-upsell-banner", "session-status", "usage-meter", "sidebar-focus", "sites"]);
|
|
5
|
+
export const FEATURE_IDS = Object.freeze(["model-slider", "notes", "images", "hide-upsell-banner", "session-status", "usage-meter", "sidebar-focus", "sites", "markdown-reading", "file-navigation"]);
|
|
6
6
|
export const DEFAULT_CONFIG = Object.freeze({
|
|
7
7
|
schemaVersion: 1,
|
|
8
8
|
features: Object.freeze({
|
|
@@ -14,6 +14,8 @@ export const DEFAULT_CONFIG = Object.freeze({
|
|
|
14
14
|
"usage-meter": true,
|
|
15
15
|
"sidebar-focus": true,
|
|
16
16
|
sites: true,
|
|
17
|
+
"markdown-reading": true,
|
|
18
|
+
"file-navigation": true,
|
|
17
19
|
}),
|
|
18
20
|
});
|
|
19
21
|
|
|
@@ -22,7 +24,7 @@ const configQueues = new Map();
|
|
|
22
24
|
function normalizeFeatures(features) {
|
|
23
25
|
return Object.fromEntries(FEATURE_IDS.map((id) => [
|
|
24
26
|
id,
|
|
25
|
-
typeof features?.[id] === "boolean" ? features[id] :
|
|
27
|
+
typeof features?.[id] === "boolean" ? features[id] : DEFAULT_CONFIG.features[id],
|
|
26
28
|
]));
|
|
27
29
|
}
|
|
28
30
|
|
|
@@ -9,13 +9,17 @@ import { BrowserExchangeStore } from "./browser-exchange.mjs";
|
|
|
9
9
|
import { FEATURE_IDS, readConfig, updateFeature } from "./config.mjs";
|
|
10
10
|
import { normalizeRuntimeReady } from "./runtime-manager.mjs";
|
|
11
11
|
|
|
12
|
+
import { createDataTransfer, MAX_TRANSFER_BYTES } from "./data-transfer/service.mjs";
|
|
13
|
+
|
|
12
14
|
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../..");
|
|
13
15
|
const packageMetadata = JSON.parse(await readFile(path.join(packageRoot, "package.json"), "utf8"));
|
|
14
16
|
const MAX_BODY_BYTES = 64 * 1024;
|
|
17
|
+
const DASHBOARD_FEATURE_ORDER = ["model-slider", "notes", "usage-meter", "session-status", "markdown-reading", "file-navigation", "images", "sites", "sidebar-focus", "hide-upsell-banner"];
|
|
15
18
|
const CONTENT_TYPES = new Map([
|
|
16
19
|
[".css", "text/css; charset=utf-8"],
|
|
17
20
|
[".html", "text/html; charset=utf-8"],
|
|
18
21
|
[".js", "text/javascript; charset=utf-8"],
|
|
22
|
+
[".mjs", "text/javascript; charset=utf-8"],
|
|
19
23
|
[".svg", "image/svg+xml"],
|
|
20
24
|
[".ttf", "font/ttf"],
|
|
21
25
|
]);
|
|
@@ -51,12 +55,12 @@ function json(response, statusCode, value) {
|
|
|
51
55
|
response.end(body);
|
|
52
56
|
}
|
|
53
57
|
|
|
54
|
-
async function readJson(request) {
|
|
58
|
+
async function readJson(request, maxBytes = MAX_BODY_BYTES) {
|
|
55
59
|
let size = 0;
|
|
56
60
|
const chunks = [];
|
|
57
61
|
for await (const chunk of request) {
|
|
58
62
|
size += chunk.length;
|
|
59
|
-
if (size >
|
|
63
|
+
if (size > maxBytes) throw Object.assign(new Error("Request body is too large"), { statusCode: 413 });
|
|
60
64
|
chunks.push(chunk);
|
|
61
65
|
}
|
|
62
66
|
let value;
|
|
@@ -90,7 +94,7 @@ function acceptedHealth(featureId, health) {
|
|
|
90
94
|
}
|
|
91
95
|
|
|
92
96
|
async function loadFeatureDefinitions(paths, locale = "en") {
|
|
93
|
-
return Promise.all(FEATURE_IDS.map(async (id) => {
|
|
97
|
+
return Promise.all([...FEATURE_IDS].sort((a, b) => DASHBOARD_FEATURE_ORDER.indexOf(a) - DASHBOARD_FEATURE_ORDER.indexOf(b)).map(async (id) => {
|
|
94
98
|
const manifestPath = path.join(paths.featuresRoot, id, "codex-feature.json");
|
|
95
99
|
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
96
100
|
return {
|
|
@@ -203,11 +207,15 @@ async function iconResponse(id, paths, response) {
|
|
|
203
207
|
iconPath = path.join(paths.featuresRoot, id, "assets", "icons", "book-open.svg");
|
|
204
208
|
} else if (FEATURE_IDS.includes(id)) {
|
|
205
209
|
const manifest = JSON.parse(await readFile(path.join(paths.featuresRoot, id, "codex-feature.json"), "utf8"));
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
210
|
+
if (manifest.icon?.resource) {
|
|
211
|
+
iconPath = path.resolve(paths.featuresRoot, id, manifest.icon.resource);
|
|
212
|
+
} else {
|
|
213
|
+
const viewBox = manifest.icon?.viewBox ?? "0 0 24 24";
|
|
214
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${viewBox}" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round">${manifest.icon.markup}</svg>`;
|
|
215
|
+
response.writeHead(200, { "Content-Type": "image/svg+xml", "Cache-Control": "public, max-age=3600" });
|
|
216
|
+
response.end(svg);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
211
219
|
} else {
|
|
212
220
|
json(response, 404, { error: "NOT_FOUND" });
|
|
213
221
|
return;
|
|
@@ -220,8 +228,9 @@ async function iconResponse(id, paths, response) {
|
|
|
220
228
|
|
|
221
229
|
async function staticResponse(url, paths, response) {
|
|
222
230
|
const requested = url.pathname === "/" ? "index.html" : url.pathname.slice(1);
|
|
223
|
-
const
|
|
224
|
-
|
|
231
|
+
const sharedTooltip = requested === "assets/ui-tooltip.js";
|
|
232
|
+
const absolute = sharedTooltip ? path.join(packageRoot, "packages/ui/src/components/tooltip.mjs") : path.resolve(paths.dashboardDir, requested);
|
|
233
|
+
if (!sharedTooltip && absolute !== paths.dashboardDir && !absolute.startsWith(`${paths.dashboardDir}${path.sep}`)) {
|
|
225
234
|
json(response, 404, { error: "NOT_FOUND" });
|
|
226
235
|
return;
|
|
227
236
|
}
|
|
@@ -251,6 +260,8 @@ export async function createControlServer({
|
|
|
251
260
|
logger = console,
|
|
252
261
|
exchangeStore = new BrowserExchangeStore(),
|
|
253
262
|
}) {
|
|
263
|
+
let transfer;
|
|
264
|
+
const dataTransfer = () => transfer ??= createDataTransfer({ paths, version: packageMetadata.version });
|
|
254
265
|
let boundPort = port;
|
|
255
266
|
const server = createServer(async (request, response) => {
|
|
256
267
|
try {
|
|
@@ -328,6 +339,19 @@ export async function createControlServer({
|
|
|
328
339
|
json(response, 200, { exchange: exchangeStore.issue(token), expiresInMs: exchangeStore.ttlMs });
|
|
329
340
|
return;
|
|
330
341
|
}
|
|
342
|
+
if (request.method === "GET" && url.pathname === "/api/data") {
|
|
343
|
+
json(response, 200, await dataTransfer().inventory()); return;
|
|
344
|
+
}
|
|
345
|
+
if (request.method === "GET" && url.pathname === "/api/data/export") {
|
|
346
|
+
const bundle = await dataTransfer().export();
|
|
347
|
+
response.setHeader("Content-Disposition", 'attachment; filename="minecodex-data.mcx.json"');
|
|
348
|
+
json(response, 200, bundle); return;
|
|
349
|
+
}
|
|
350
|
+
if (request.method === "POST" && ["/api/data/preview", "/api/data/import"].includes(url.pathname)) {
|
|
351
|
+
requireMutationOrigin(request, host, boundPort);
|
|
352
|
+
const bundle = await readJson(request, MAX_TRANSFER_BYTES);
|
|
353
|
+
json(response, 200, await dataTransfer()[url.pathname.endsWith("/import") ? "import" : "preview"](bundle)); return;
|
|
354
|
+
}
|
|
331
355
|
const featureMatch = url.pathname.match(/^\/api\/features\/([a-z-]+)$/);
|
|
332
356
|
if (request.method === "PATCH" && featureMatch) {
|
|
333
357
|
requireMutationOrigin(request, host, boundPort);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { promptsTransfer } from '../../../../features/notes/src/backend/transfer.mjs';
|
|
3
|
+
import { sitesTransfer } from '../../../../features/sites/src/backend/transfer.mjs';
|
|
4
|
+
|
|
5
|
+
import { favoritesTransfer } from '../../../../features/model-slider/src/backend/favorites.mjs';
|
|
6
|
+
import { FEATURE_IDS } from '../config.mjs';
|
|
7
|
+
|
|
8
|
+
export const MAX_TRANSFER_BYTES = 16 * 1024 * 1024;
|
|
9
|
+
export function createDataTransfer({ paths, version, adapters = [
|
|
10
|
+
promptsTransfer(process.env.CODEX_NOTES_DATABASE || path.join(paths.notesDataDir, 'codex-notes.sqlite')),
|
|
11
|
+
sitesTransfer(process.env.MINECODEX_SITES_DATA_DIR || path.join(paths.supportDir, 'sites')),
|
|
12
|
+
favoritesTransfer(process.env.MINECODEX_MODEL_DATA_FILE || path.join(paths.supportDir, 'model-slider', 'favorites.sqlite')),
|
|
13
|
+
] }) {
|
|
14
|
+
const registry = new Map(adapters.map(adapter => [adapter.id, adapter]));
|
|
15
|
+
function validate(bundle) {
|
|
16
|
+
if (!bundle || bundle.format !== 'minecodex-data' || bundle.version !== 1 || !bundle.features || typeof bundle.features !== 'object' || Array.isArray(bundle.features)) throw Object.assign(new Error('Unsupported MineCodex data file'), { statusCode: 400 });
|
|
17
|
+
try {
|
|
18
|
+
return Object.entries(bundle.features).map(([id, data]) => {
|
|
19
|
+
if (!registry.has(id)) throw new Error(`Unsupported feature: ${id}`);
|
|
20
|
+
return [id, registry.get(id).validate(data)];
|
|
21
|
+
});
|
|
22
|
+
} catch (error) { throw Object.assign(error, { statusCode: 400 }); }
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
async inventory() { return { features: await Promise.all(FEATURE_IDS.map(async id => { const adapter = registry.get(id); if (!adapter) return { id, portable: false }; const { data, ...counts } = await adapter.snapshot(); return { id, portable: true, ...counts }; })) }; },
|
|
26
|
+
async export() {
|
|
27
|
+
const bundle = { format: 'minecodex-data', version: 1, appVersion: version, exportedAt: new Date().toISOString(), features: Object.fromEntries(await Promise.all(adapters.map(async adapter => [adapter.id, (await adapter.snapshot()).data]))) };
|
|
28
|
+
if (Buffer.byteLength(JSON.stringify(bundle, null, 2)) > MAX_TRANSFER_BYTES) throw Object.assign(new Error('Data file exceeds 16 MB'), { statusCode: 413 });
|
|
29
|
+
return bundle;
|
|
30
|
+
},
|
|
31
|
+
async preview(bundle) { return { features: await Promise.all(validate(bundle).map(async ([id, data]) => ({ id, ...await registry.get(id).preview(data) }))) }; },
|
|
32
|
+
async import(bundle) {
|
|
33
|
+
const entries = validate(bundle), results = [];
|
|
34
|
+
// 各领域独立提交;失败时保留已完成结果,重试依靠幂等合并。
|
|
35
|
+
for (const [id, data] of entries) {
|
|
36
|
+
try { results.push({ id, status: 'imported', ...await registry.get(id).apply(data) }); }
|
|
37
|
+
catch { results.push({ id, status: 'failed' }); }
|
|
38
|
+
}
|
|
39
|
+
return { complete: results.every(result => result.status === 'imported'), features: results };
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import path from "node:path";
|
|
1
2
|
import { spawn } from "node:child_process";
|
|
2
3
|
import { mkdir, open, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
3
4
|
import { ChatGPTLaunchCoordinator } from "./chatgpt-launch-coordinator.mjs";
|
|
@@ -189,6 +190,8 @@ export class RuntimeManager {
|
|
|
189
190
|
MINECODEX_CODEX_BUILD: codexBuild ?? "",
|
|
190
191
|
CODEX_IMAGE_HOST_DATA_DIR: this.paths.imagesDataDir,
|
|
191
192
|
CODEX_NOTES_DATA_DIR: this.paths.notesDataDir,
|
|
193
|
+
MINECODEX_MODEL_DATA_FILE: process.env.MINECODEX_MODEL_DATA_FILE || path.join(this.paths.supportDir, "model-slider", "favorites.sqlite"),
|
|
194
|
+
MINECODEX_SITES_DATA_DIR: process.env.MINECODEX_SITES_DATA_DIR || path.join(this.paths.supportDir, "sites"),
|
|
192
195
|
MINECODEX_ENABLED_FEATURES: features.join(","),
|
|
193
196
|
MINECODEX_CODEX_PID_FILE: this.paths.codexPidPath,
|
|
194
197
|
MINECODEX_RUNTIME_READY_FILE: this.paths.runtimeReadyPath,
|
|
@@ -254,6 +254,7 @@ export function createInjectionSource(features, {
|
|
|
254
254
|
try {
|
|
255
255
|
modelSlider = installModelSliderFeature(lifetime.signal, {
|
|
256
256
|
feature: modelSelectorFeature, codex, ui: featureUI, createIcon: createMarkupIcon, requestRefresh: queueEnsure, locale,
|
|
257
|
+
requestAction: payload => requestPageHostAction(modelSelectorFeature.id, "model-favorites", payload),
|
|
257
258
|
});
|
|
258
259
|
if (modelSelectorFeature) {
|
|
259
260
|
selfCheck.register(modelSelectorFeature.id, () => modelSlider.selfCheck());
|
|
@@ -736,7 +737,7 @@ export function createInjectionSource(features, {
|
|
|
736
737
|
"--codex-summary-row-line-height": "24px",
|
|
737
738
|
"--codex-summary-label-font-size": tokens["--text-base"] ?? "14px",
|
|
738
739
|
"--codex-summary-label-line-height": tokens["--text-base--line-height"] ?? "1.5",
|
|
739
|
-
"--codex-summary-font-weight":
|
|
740
|
+
"--codex-summary-font-weight": style.fontWeight || "445",
|
|
740
741
|
"--codex-summary-icon-size": "18px",
|
|
741
742
|
"--codex-summary-icon-stroke-width": "1.5px",
|
|
742
743
|
"--codex-summary-panel-radius": panelRadius,
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
attachFilesToComposer,
|
|
37
37
|
createImageEditThread,
|
|
38
38
|
} from "./host-actions.mjs";
|
|
39
|
+
import { ModelFavorites } from "../../../features/model-slider/src/backend/favorites.mjs";
|
|
39
40
|
import { SitesService } from "../../../features/sites/src/backend/sites-service.mjs";
|
|
40
41
|
import { SessionStatusReader, threadIdFromLocation } from "../../../features/session-status/src/adapters/codex/session-reader.mjs";
|
|
41
42
|
import { createCodexEnvironment } from "../../codex-adapter/src/renderer/environment.mjs";
|
|
@@ -108,6 +109,7 @@ export class CodexRuntime {
|
|
|
108
109
|
this.rendererTargetIds = new Set();
|
|
109
110
|
this.targetFailures = new Map();
|
|
110
111
|
this.targetChecks = new Map();
|
|
112
|
+
this.targetCheckDeadlines = new Map();
|
|
111
113
|
this.clients = new Map();
|
|
112
114
|
this.scriptIds = new Map();
|
|
113
115
|
this.bindingTokens = new Map();
|
|
@@ -244,6 +246,7 @@ export class CodexRuntime {
|
|
|
244
246
|
this.rendererTargetIds.clear();
|
|
245
247
|
this.targetFailures.clear();
|
|
246
248
|
this.targetChecks.clear();
|
|
249
|
+
this.targetCheckDeadlines.clear();
|
|
247
250
|
this.rendererDiscovered = false;
|
|
248
251
|
this.browserClient?.socket.close();
|
|
249
252
|
this.browserClient = null;
|
|
@@ -576,6 +579,16 @@ export class CodexRuntime {
|
|
|
576
579
|
}
|
|
577
580
|
return;
|
|
578
581
|
}
|
|
582
|
+
if (feature.id === "model-slider" && request.action === "model-favorites") {
|
|
583
|
+
try {
|
|
584
|
+
this.modelFavorites ??= new ModelFavorites();
|
|
585
|
+
const result = this.modelFavorites.action(request.payload);
|
|
586
|
+
await this.resolveHostAction(client, request.requestId, { ok: true, result });
|
|
587
|
+
} catch (error) {
|
|
588
|
+
await this.resolveHostAction(client, request.requestId, { ok: false, error: { code: "MODEL_FAVORITES_FAILED", message: error.message } });
|
|
589
|
+
}
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
579
592
|
if (feature.id === "sites" && request.action === "sites") {
|
|
580
593
|
try {
|
|
581
594
|
const result = await this.sitesService.action(request.payload);
|
|
@@ -703,6 +716,7 @@ export class CodexRuntime {
|
|
|
703
716
|
this.scriptIds.delete(id);
|
|
704
717
|
this.bindingTokens.delete(id);
|
|
705
718
|
this.targetChecks.delete(id);
|
|
719
|
+
this.targetCheckDeadlines.delete(id);
|
|
706
720
|
this.hostActionChains.delete(id);
|
|
707
721
|
this.injectedTargets.delete(id);
|
|
708
722
|
this.targetFailures.delete(id);
|
|
@@ -724,6 +738,7 @@ export class CodexRuntime {
|
|
|
724
738
|
this.bindingTokens.delete(target.id);
|
|
725
739
|
this.injectedTargets.delete(target.id);
|
|
726
740
|
this.targetChecks.delete(target.id);
|
|
741
|
+
this.targetCheckDeadlines.delete(target.id);
|
|
727
742
|
}
|
|
728
743
|
if (target.type !== "page" || target.url !== "app://-/index.html" || this.injectedTargets.has(target.id)) {
|
|
729
744
|
continue;
|
|
@@ -748,6 +763,7 @@ export class CodexRuntime {
|
|
|
748
763
|
if (this.clients.get(target.id) !== client) return;
|
|
749
764
|
// 新文档撤销启动检查,下一次接入周期重新检查一次。
|
|
750
765
|
this.targetChecks.delete(target.id);
|
|
766
|
+
this.targetCheckDeadlines.delete(target.id);
|
|
751
767
|
this.notifyStatusChange();
|
|
752
768
|
});
|
|
753
769
|
client.on?.("Runtime.bindingCalled", (params) => {
|
|
@@ -814,7 +830,12 @@ export class CodexRuntime {
|
|
|
814
830
|
async checkTarget(targetId) {
|
|
815
831
|
const client=this.clients.get(targetId);
|
|
816
832
|
if (!client || this.stopping) return false;
|
|
817
|
-
|
|
833
|
+
const previous = this.targetChecks.get(targetId);
|
|
834
|
+
const needsRetry = !previous?.checkedAt || previous.plugins.some(plugin =>
|
|
835
|
+
["unknown", "unavailable", "degraded"].includes(plugin.state));
|
|
836
|
+
if (previous && (!needsRetry || Date.now() >= this.targetCheckDeadlines.get(targetId))) return true;
|
|
837
|
+
// 页面启动时锚点尚未就绪:借用发现周期有限重试,稳定后停止检查。
|
|
838
|
+
if (!this.targetCheckDeadlines.has(targetId)) this.targetCheckDeadlines.set(targetId, Date.now() + 30_000);
|
|
818
839
|
try {
|
|
819
840
|
const response=await client.send("Runtime.evaluate", {
|
|
820
841
|
expression:`(() => { const runtime=window.__codexPersonalRuntime; return runtime?.version === ${RUNTIME_VERSION} && runtime.sessionId === ${JSON.stringify(this.runtimeSessionId)} ? runtime.selfCheck() : null; })()`,
|
|
@@ -865,6 +886,7 @@ export class CodexRuntime {
|
|
|
865
886
|
client.socket.close();
|
|
866
887
|
this.injectedTargets.delete(targetId);
|
|
867
888
|
this.targetChecks.delete(targetId);
|
|
889
|
+
this.targetCheckDeadlines.delete(targetId);
|
|
868
890
|
this.notifyStatusChange();
|
|
869
891
|
}
|
|
870
892
|
}
|
|
@@ -909,6 +931,7 @@ export class CodexRuntime {
|
|
|
909
931
|
this.rendererTargetIds.clear();
|
|
910
932
|
this.targetFailures.clear();
|
|
911
933
|
this.targetChecks.clear();
|
|
934
|
+
this.targetCheckDeadlines.clear();
|
|
912
935
|
this.rendererDiscovered = false;
|
|
913
936
|
this.notifyStatusChange();
|
|
914
937
|
this.monitorPromise = null;
|
|
@@ -46,5 +46,27 @@ export function createNativeThemeAdapter(themeTokenNames,themeTokenAliases,theme
|
|
|
46
46
|
: projectName && projectValue !== value ? "scope-mapping-mismatch" : null};
|
|
47
47
|
});
|
|
48
48
|
}
|
|
49
|
-
|
|
49
|
+
// 局部组件的映射保留原生变量引用,随作用域和主题变化,不复制根节点的快照值。
|
|
50
|
+
function bindScope(element, names) {
|
|
51
|
+
const bindings = names.map(name => {
|
|
52
|
+
const projectName = themeBindings[name];
|
|
53
|
+
if (!projectName) throw new Error(`Unknown theme binding: ${name}`);
|
|
54
|
+
const candidates = [name, ...(themeTokenAliases[name] ?? [])].flatMap(candidate => candidate.startsWith("--color-")
|
|
55
|
+
? [candidate, candidate.replace("--color-", "--app-color-")] : [candidate]);
|
|
56
|
+
const reference = candidates.reduceRight((value, candidate) => value ? `var(${candidate}, ${value})` : `var(${candidate})`, "");
|
|
57
|
+
const previous = element.style.getPropertyValue(projectName);
|
|
58
|
+
const priority = element.style.getPropertyPriority(projectName);
|
|
59
|
+
element.style.setProperty(projectName, reference);
|
|
60
|
+
return {projectName, reference, previous, priority};
|
|
61
|
+
});
|
|
62
|
+
return () => {
|
|
63
|
+
for (const {projectName, reference, previous, priority} of bindings) {
|
|
64
|
+
if (element.style.getPropertyValue(projectName) !== reference) continue;
|
|
65
|
+
if (previous) element.style.setProperty(projectName, previous, priority);
|
|
66
|
+
else element.style.removeProperty(projectName);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
// 根清单仅展示已定义变量;必要值缺失仍由控件作用域检查报告。
|
|
71
|
+
return {read,dispose,checkTokens,bindScope,diagnostics:() => checkTokens(document.documentElement,themeTokenNames).filter(token => token.available)};
|
|
50
72
|
}
|