minecodex 2.0.12 → 2.0.13
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 +12 -0
- package/apps/dashboard/app.js +37 -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 +52 -1
- package/features/file-navigation/assets/book-open.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/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 +1 -0
- 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 +2 -1
- 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/src/domain/session-stats.mjs +22 -8
- package/features/session-status/src/session-status.mjs +2 -2
- package/features/sites/src/backend/sites-service.mjs +20 -5
- package/features/sites/src/backend/transfer.mjs +43 -0
- package/package.json +11 -3
- package/packages/cli/src/config.mjs +4 -2
- package/packages/cli/src/control-server.mjs +23 -4
- 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 +1 -0
- package/packages/runtime-host/src/codex-runtime.mjs +11 -0
- package/packages/runtime-host/src/feature-registry.mjs +1 -0
- package/packages/ui/src/native/theme.mjs +22 -1
- package/packages/ui/src/native/tokens.mjs +4 -0
|
@@ -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";
|
|
@@ -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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "minecodex",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.13",
|
|
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": false,
|
|
18
|
+
"file-navigation": false,
|
|
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,6 +9,8 @@ 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;
|
|
@@ -16,6 +18,7 @@ const CONTENT_TYPES = new Map([
|
|
|
16
18
|
[".css", "text/css; charset=utf-8"],
|
|
17
19
|
[".html", "text/html; charset=utf-8"],
|
|
18
20
|
[".js", "text/javascript; charset=utf-8"],
|
|
21
|
+
[".mjs", "text/javascript; charset=utf-8"],
|
|
19
22
|
[".svg", "image/svg+xml"],
|
|
20
23
|
[".ttf", "font/ttf"],
|
|
21
24
|
]);
|
|
@@ -51,12 +54,12 @@ function json(response, statusCode, value) {
|
|
|
51
54
|
response.end(body);
|
|
52
55
|
}
|
|
53
56
|
|
|
54
|
-
async function readJson(request) {
|
|
57
|
+
async function readJson(request, maxBytes = MAX_BODY_BYTES) {
|
|
55
58
|
let size = 0;
|
|
56
59
|
const chunks = [];
|
|
57
60
|
for await (const chunk of request) {
|
|
58
61
|
size += chunk.length;
|
|
59
|
-
if (size >
|
|
62
|
+
if (size > maxBytes) throw Object.assign(new Error("Request body is too large"), { statusCode: 413 });
|
|
60
63
|
chunks.push(chunk);
|
|
61
64
|
}
|
|
62
65
|
let value;
|
|
@@ -220,8 +223,9 @@ async function iconResponse(id, paths, response) {
|
|
|
220
223
|
|
|
221
224
|
async function staticResponse(url, paths, response) {
|
|
222
225
|
const requested = url.pathname === "/" ? "index.html" : url.pathname.slice(1);
|
|
223
|
-
const
|
|
224
|
-
|
|
226
|
+
const sharedTooltip = requested === "assets/ui-tooltip.js";
|
|
227
|
+
const absolute = sharedTooltip ? path.join(packageRoot, "packages/ui/src/components/tooltip.mjs") : path.resolve(paths.dashboardDir, requested);
|
|
228
|
+
if (!sharedTooltip && absolute !== paths.dashboardDir && !absolute.startsWith(`${paths.dashboardDir}${path.sep}`)) {
|
|
225
229
|
json(response, 404, { error: "NOT_FOUND" });
|
|
226
230
|
return;
|
|
227
231
|
}
|
|
@@ -251,6 +255,8 @@ export async function createControlServer({
|
|
|
251
255
|
logger = console,
|
|
252
256
|
exchangeStore = new BrowserExchangeStore(),
|
|
253
257
|
}) {
|
|
258
|
+
let transfer;
|
|
259
|
+
const dataTransfer = () => transfer ??= createDataTransfer({ paths, version: packageMetadata.version });
|
|
254
260
|
let boundPort = port;
|
|
255
261
|
const server = createServer(async (request, response) => {
|
|
256
262
|
try {
|
|
@@ -328,6 +334,19 @@ export async function createControlServer({
|
|
|
328
334
|
json(response, 200, { exchange: exchangeStore.issue(token), expiresInMs: exchangeStore.ttlMs });
|
|
329
335
|
return;
|
|
330
336
|
}
|
|
337
|
+
if (request.method === "GET" && url.pathname === "/api/data") {
|
|
338
|
+
json(response, 200, await dataTransfer().inventory()); return;
|
|
339
|
+
}
|
|
340
|
+
if (request.method === "GET" && url.pathname === "/api/data/export") {
|
|
341
|
+
const bundle = await dataTransfer().export();
|
|
342
|
+
response.setHeader("Content-Disposition", 'attachment; filename="minecodex-data.mcx.json"');
|
|
343
|
+
json(response, 200, bundle); return;
|
|
344
|
+
}
|
|
345
|
+
if (request.method === "POST" && ["/api/data/preview", "/api/data/import"].includes(url.pathname)) {
|
|
346
|
+
requireMutationOrigin(request, host, boundPort);
|
|
347
|
+
const bundle = await readJson(request, MAX_TRANSFER_BYTES);
|
|
348
|
+
json(response, 200, await dataTransfer()[url.pathname.endsWith("/import") ? "import" : "preview"](bundle)); return;
|
|
349
|
+
}
|
|
331
350
|
const featureMatch = url.pathname.match(/^\/api\/features\/([a-z-]+)$/);
|
|
332
351
|
if (request.method === "PATCH" && featureMatch) {
|
|
333
352
|
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());
|
|
@@ -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";
|
|
@@ -576,6 +577,16 @@ export class CodexRuntime {
|
|
|
576
577
|
}
|
|
577
578
|
return;
|
|
578
579
|
}
|
|
580
|
+
if (feature.id === "model-slider" && request.action === "model-favorites") {
|
|
581
|
+
try {
|
|
582
|
+
this.modelFavorites ??= new ModelFavorites();
|
|
583
|
+
const result = this.modelFavorites.action(request.payload);
|
|
584
|
+
await this.resolveHostAction(client, request.requestId, { ok: true, result });
|
|
585
|
+
} catch (error) {
|
|
586
|
+
await this.resolveHostAction(client, request.requestId, { ok: false, error: { code: "MODEL_FAVORITES_FAILED", message: error.message } });
|
|
587
|
+
}
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
579
590
|
if (feature.id === "sites" && request.action === "sites") {
|
|
580
591
|
try {
|
|
581
592
|
const result = await this.sitesService.action(request.payload);
|
|
@@ -46,5 +46,26 @@ 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
|
+
return {read,dispose,checkTokens,bindScope,diagnostics:() => checkTokens(document.documentElement,themeTokenNames)};
|
|
50
71
|
}
|
|
@@ -77,7 +77,11 @@ export const CODEX_THEME_TOKENS = Object.freeze([
|
|
|
77
77
|
"--color-token-input-foreground",
|
|
78
78
|
"--color-token-input-placeholder-foreground",
|
|
79
79
|
"--font-sans-default",
|
|
80
|
+
"--font-content",
|
|
81
|
+
"--text-document",
|
|
82
|
+
"--leading-relaxed",
|
|
80
83
|
"--font-weight-normal",
|
|
84
|
+
"--font-weight-medium",
|
|
81
85
|
"--vscode-font-weight",
|
|
82
86
|
"--default-mono-font-family",
|
|
83
87
|
"--text-heading-lg",
|