minecodex 1.0.82 → 1.1.0
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 +2 -2
- package/features/images/src/http-server.mjs +2 -1
- package/features/images/src/save-as.mjs +37 -8
- package/features/images/web/app.js +91 -80
- package/features/images/web/i18n.mjs +127 -0
- package/features/images/web/index.html +33 -33
- package/features/model-slider/README.md +7 -4
- package/features/notes/README.md +1 -1
- package/features/notes/src/http-server.mjs +31 -0
- package/features/notes/web/app.js +13 -306
- package/features/notes/web/file-icons.mjs +68 -0
- package/features/notes/web/i18n.mjs +237 -0
- package/features/notes/web/todo-drag.mjs +18 -0
- package/package.json +4 -2
- package/packages/cli/src/commands.mjs +3 -3
- package/packages/cli/src/npm-adapter.mjs +12 -4
- package/packages/cli/src/paths.mjs +36 -1
- package/packages/cli/src/platform.mjs +517 -0
- package/packages/cli/src/runtime-manager.mjs +9 -2
- package/packages/runtime-host/README.md +5 -3
- package/packages/runtime-host/src/codex-cdp.mjs +153 -0
- package/packages/runtime-host/src/codex-design-contract.mjs +116 -0
- package/packages/runtime-host/src/codex-injection.mjs +4586 -0
- package/packages/runtime-host/src/codex-runtime.mjs +63 -4930
- package/packages/runtime-host/src/host-actions.mjs +221 -0
- package/packages/runtime-host/src/main.mjs +17 -9
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// Host action 命令:Codex Composer 的文件附件、图片编辑线程与降级路径。
|
|
2
|
+
// 每个命令是参数注入的纯函数(client 与依赖由 CodexRuntime 传入),
|
|
3
|
+
// CodexRuntime 只做 handleBindingCalled 路由,不再持有命令实现。
|
|
4
|
+
|
|
5
|
+
import { stat } from "node:fs/promises";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { CODEX_APP_ORIGIN } from "./codex-injection.mjs";
|
|
8
|
+
import {
|
|
9
|
+
evaluationValue,
|
|
10
|
+
waitForExpression,
|
|
11
|
+
} from "./codex-cdp.mjs";
|
|
12
|
+
export async function attachFileToComposer(client, filePath, requestId) {
|
|
13
|
+
if (typeof filePath !== "string" || !path.isAbsolute(filePath)) {
|
|
14
|
+
throw new Error("An absolute file path is required");
|
|
15
|
+
}
|
|
16
|
+
const info = await stat(filePath);
|
|
17
|
+
if (!info.isFile()) throw new Error("Only regular files can be attached");
|
|
18
|
+
const fileName = path.basename(filePath);
|
|
19
|
+
const marker = `codex-personal-${requestId}`;
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
evaluationValue(await client.send("Runtime.evaluate", {
|
|
23
|
+
expression: `(() => {
|
|
24
|
+
document.querySelectorAll('[data-codex-personal-file-input]').forEach((input) => input.remove());
|
|
25
|
+
const input = document.createElement("input");
|
|
26
|
+
input.type = "file";
|
|
27
|
+
input.multiple = true;
|
|
28
|
+
input.dataset.codexPersonalFileInput = ${JSON.stringify(marker)};
|
|
29
|
+
input.style.display = "none";
|
|
30
|
+
document.body.append(input);
|
|
31
|
+
return true;
|
|
32
|
+
})()`,
|
|
33
|
+
returnByValue: true,
|
|
34
|
+
}));
|
|
35
|
+
const { root } = await client.send("DOM.getDocument", { depth: 0 });
|
|
36
|
+
const { nodeId } = await client.send("DOM.querySelector", {
|
|
37
|
+
nodeId: root.nodeId,
|
|
38
|
+
selector: `[data-codex-personal-file-input="${marker}"]`,
|
|
39
|
+
});
|
|
40
|
+
if (!nodeId) throw new Error("The temporary file bridge could not be resolved");
|
|
41
|
+
await client.send("DOM.setFileInputFiles", { files: [filePath], nodeId });
|
|
42
|
+
|
|
43
|
+
const attachment = evaluationValue(await client.send("Runtime.evaluate", {
|
|
44
|
+
expression: `(async () => {
|
|
45
|
+
const marker = ${JSON.stringify(marker)};
|
|
46
|
+
const fileName = ${JSON.stringify(fileName)};
|
|
47
|
+
const input = Array.from(document.querySelectorAll('[data-codex-personal-file-input]'))
|
|
48
|
+
.find((candidate) => candidate.dataset.codexPersonalFileInput === marker);
|
|
49
|
+
const composer = document.querySelector('[data-codex-composer="true"][contenteditable="true"]');
|
|
50
|
+
if (!input?.files?.length || !composer) return { attached: false, reason: "Composer unavailable" };
|
|
51
|
+
const transfer = new DataTransfer();
|
|
52
|
+
for (const file of input.files) transfer.items.add(file);
|
|
53
|
+
for (const type of ["dragenter", "dragover", "drop"]) {
|
|
54
|
+
composer.dispatchEvent(new DragEvent(type, {
|
|
55
|
+
bubbles: true,
|
|
56
|
+
cancelable: true,
|
|
57
|
+
composed: true,
|
|
58
|
+
dataTransfer: transfer,
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
await new Promise((resolve) => setTimeout(resolve, 850));
|
|
62
|
+
const attached = Boolean(document.querySelector(
|
|
63
|
+
'button[aria-label=' + JSON.stringify('Remove ' + fileName) + ']'
|
|
64
|
+
));
|
|
65
|
+
return { attached, reason: attached ? null : "Native attachment chip was not observed" };
|
|
66
|
+
})()`,
|
|
67
|
+
awaitPromise: true,
|
|
68
|
+
returnByValue: true,
|
|
69
|
+
}));
|
|
70
|
+
|
|
71
|
+
if (attachment?.attached) return { mode: "attach", name: fileName };
|
|
72
|
+
const fallback = evaluationValue(await client.send("Runtime.evaluate", {
|
|
73
|
+
expression: `window.__codexPersonalRuntime?.insertText(${JSON.stringify(filePath)})`,
|
|
74
|
+
returnByValue: true,
|
|
75
|
+
}));
|
|
76
|
+
if (!fallback) throw new Error(attachment?.reason ?? "File attachment failed");
|
|
77
|
+
return { mode: "insert-path", path: filePath, fallback: true };
|
|
78
|
+
} finally {
|
|
79
|
+
await client.send("Runtime.evaluate", {
|
|
80
|
+
expression: `document.querySelectorAll('[data-codex-personal-file-input]').forEach((input) => input.remove())`,
|
|
81
|
+
}).catch(() => {});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function attachFilesToComposer(client, filePaths, requestId) {
|
|
86
|
+
if (!Array.isArray(filePaths) || !filePaths.length || filePaths.length > 2) {
|
|
87
|
+
throw new Error("One or two image edit attachments are required");
|
|
88
|
+
}
|
|
89
|
+
for (const filePath of filePaths) {
|
|
90
|
+
if (typeof filePath !== "string" || !path.isAbsolute(filePath) || !(await stat(filePath)).isFile()) {
|
|
91
|
+
throw new Error("Only absolute regular files can be attached");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const fileNames = filePaths.map((filePath) => path.basename(filePath));
|
|
95
|
+
const requiredLabels = fileNames.reduce((counts, fileName) => {
|
|
96
|
+
const label = `Remove ${fileName}`;
|
|
97
|
+
counts[label] = (counts[label] ?? 0) + 1;
|
|
98
|
+
return counts;
|
|
99
|
+
}, {});
|
|
100
|
+
const marker = `codex-personal-${requestId}-batch`;
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
evaluationValue(await client.send("Runtime.evaluate", {
|
|
104
|
+
expression: `(() => {
|
|
105
|
+
document.querySelectorAll('[data-codex-personal-file-input]').forEach((input) => input.remove());
|
|
106
|
+
const input = document.createElement("input");
|
|
107
|
+
input.type = "file";
|
|
108
|
+
input.multiple = true;
|
|
109
|
+
input.dataset.codexPersonalFileInput = ${JSON.stringify(marker)};
|
|
110
|
+
input.style.display = "none";
|
|
111
|
+
document.body.append(input);
|
|
112
|
+
return true;
|
|
113
|
+
})()`,
|
|
114
|
+
returnByValue: true,
|
|
115
|
+
}));
|
|
116
|
+
const { root } = await client.send("DOM.getDocument", { depth: 0 });
|
|
117
|
+
const { nodeId } = await client.send("DOM.querySelector", {
|
|
118
|
+
nodeId: root.nodeId,
|
|
119
|
+
selector: `[data-codex-personal-file-input="${marker}"]`,
|
|
120
|
+
});
|
|
121
|
+
if (!nodeId) throw new Error("The temporary multi-file bridge could not be resolved");
|
|
122
|
+
await client.send("DOM.setFileInputFiles", { files: filePaths, nodeId });
|
|
123
|
+
const dropped = evaluationValue(await client.send("Runtime.evaluate", {
|
|
124
|
+
expression: `(() => {
|
|
125
|
+
const input = document.querySelector(${JSON.stringify(`[data-codex-personal-file-input="${marker}"]`)});
|
|
126
|
+
const composer = document.querySelector('[data-codex-composer="true"][contenteditable="true"]');
|
|
127
|
+
if (!input?.files?.length || !composer) return false;
|
|
128
|
+
const transfer = new DataTransfer();
|
|
129
|
+
for (const file of input.files) transfer.items.add(file);
|
|
130
|
+
for (const type of ["dragenter", "dragover", "drop"]) {
|
|
131
|
+
composer.dispatchEvent(new DragEvent(type, {
|
|
132
|
+
bubbles: true,
|
|
133
|
+
cancelable: true,
|
|
134
|
+
composed: true,
|
|
135
|
+
dataTransfer: transfer,
|
|
136
|
+
}));
|
|
137
|
+
}
|
|
138
|
+
return transfer.files.length === ${filePaths.length};
|
|
139
|
+
})()`,
|
|
140
|
+
returnByValue: true,
|
|
141
|
+
}));
|
|
142
|
+
if (!dropped) throw new Error("The native Composer rejected the image edit drop");
|
|
143
|
+
await waitForExpression(client, `(() => {
|
|
144
|
+
const required = ${JSON.stringify(requiredLabels)};
|
|
145
|
+
const observed = {};
|
|
146
|
+
for (const button of document.querySelectorAll('button[aria-label^="Remove "]')) {
|
|
147
|
+
const label = button.getAttribute("aria-label");
|
|
148
|
+
observed[label] = (observed[label] ?? 0) + 1;
|
|
149
|
+
}
|
|
150
|
+
return Object.entries(required).every(([label, count]) => (observed[label] ?? 0) >= count);
|
|
151
|
+
})()`, 10_000);
|
|
152
|
+
return fileNames.map((name) => ({ mode: "attach", name }));
|
|
153
|
+
} finally {
|
|
154
|
+
await client.send("Runtime.evaluate", {
|
|
155
|
+
expression: `document.querySelectorAll('[data-codex-personal-file-input]').forEach((input) => input.remove())`,
|
|
156
|
+
}).catch(() => {});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function createImageEditThread(client, feature, payload, requestId, { fetchImpl, attachFiles = attachFilesToComposer } = {}) {
|
|
161
|
+
const draftId = typeof payload?.draftId === "string" ? payload.draftId : "";
|
|
162
|
+
const prompt = typeof payload?.prompt === "string" ? payload.prompt.trim() : "";
|
|
163
|
+
if (!/^[a-f0-9-]{36}$/.test(draftId) || !prompt || prompt.length > 2_000 || !feature?.surfaceUrl) {
|
|
164
|
+
throw Object.assign(new Error("The image edit request is incomplete"), { code: "INVALID_EDIT_REQUEST" });
|
|
165
|
+
}
|
|
166
|
+
const draftResponse = await fetchImpl(new URL(`/api/image-edit-draft/${draftId}`, feature.surfaceUrl), {
|
|
167
|
+
headers: { Origin: CODEX_APP_ORIGIN },
|
|
168
|
+
});
|
|
169
|
+
if (!draftResponse?.ok) {
|
|
170
|
+
throw Object.assign(new Error("The image edit draft is unavailable"), { code: "EDIT_DRAFT_UNAVAILABLE" });
|
|
171
|
+
}
|
|
172
|
+
const draft = await draftResponse.json();
|
|
173
|
+
const paths = [draft?.sourcePath, draft?.auxiliaryPath].filter(Boolean);
|
|
174
|
+
if (!paths.length || paths.length > 2) {
|
|
175
|
+
throw Object.assign(new Error("The image edit attachments are unavailable"), { code: "INVALID_EDIT_ATTACHMENT" });
|
|
176
|
+
}
|
|
177
|
+
for (const filePath of paths) {
|
|
178
|
+
if (typeof filePath !== "string" || !path.isAbsolute(filePath) || !(await stat(filePath)).isFile()) {
|
|
179
|
+
throw Object.assign(new Error("The image edit attachment is unavailable"), { code: "INVALID_EDIT_ATTACHMENT" });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const opened = evaluationValue(await client.send("Runtime.evaluate", {
|
|
184
|
+
expression: `(() => {
|
|
185
|
+
const candidates = Array.from(document.querySelectorAll('button, [role="button"]'));
|
|
186
|
+
const trigger = candidates.find((element) => /^new chat$/i.test(
|
|
187
|
+
(element.getAttribute('aria-label') || element.textContent || '').trim(),
|
|
188
|
+
));
|
|
189
|
+
if (!trigger) return false;
|
|
190
|
+
trigger.click();
|
|
191
|
+
return true;
|
|
192
|
+
})()`,
|
|
193
|
+
returnByValue: true,
|
|
194
|
+
}));
|
|
195
|
+
if (!opened) throw Object.assign(new Error("The native New chat control is unavailable"), {
|
|
196
|
+
code: "NEW_CHAT_UNAVAILABLE",
|
|
197
|
+
});
|
|
198
|
+
await waitForExpression(client, `Boolean(document.querySelector('[data-testid="home-icon"]')) && Boolean(document.querySelector('[data-codex-composer="true"][contenteditable="true"]'))`);
|
|
199
|
+
const attached = await attachFiles(client, paths, requestId);
|
|
200
|
+
evaluationValue(await client.send("Runtime.evaluate", {
|
|
201
|
+
expression: `window.__codexPersonalRuntime?.insertText(${JSON.stringify(prompt)})`,
|
|
202
|
+
returnByValue: true,
|
|
203
|
+
}));
|
|
204
|
+
const sent = evaluationValue(await client.send("Runtime.evaluate", {
|
|
205
|
+
expression: `(() => {
|
|
206
|
+
const send = document.querySelector('button[aria-label="Send message"]')
|
|
207
|
+
|| document.querySelector('button[aria-label="Send"]')
|
|
208
|
+
|| document.querySelector('button[data-testid="send-button"]');
|
|
209
|
+
if (!send || send.disabled) return false;
|
|
210
|
+
send.click();
|
|
211
|
+
return true;
|
|
212
|
+
})()`,
|
|
213
|
+
returnByValue: true,
|
|
214
|
+
}));
|
|
215
|
+
if (!sent) throw Object.assign(new Error("The new Chat is ready but its Send button is unavailable"), {
|
|
216
|
+
code: "SEND_UNAVAILABLE",
|
|
217
|
+
});
|
|
218
|
+
await waitForExpression(client, `!document.querySelector('[data-testid="home-icon"]') && Boolean(document.querySelector('[data-codex-composer="true"][contenteditable="true"]'))`);
|
|
219
|
+
return { attached, threadStarted: true };
|
|
220
|
+
}
|
|
221
|
+
|
|
@@ -93,14 +93,22 @@ async function shutdown() {
|
|
|
93
93
|
await featureProcesses.stop();
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
let shuttingDown = false;
|
|
97
|
+
async function requestShutdown() {
|
|
98
|
+
if (shuttingDown) return;
|
|
99
|
+
shuttingDown = true;
|
|
100
|
+
try {
|
|
101
|
+
await shutdown();
|
|
102
|
+
process.exit(0);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
console.error(`Runtime shutdown failed: ${error.message}`);
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
96
108
|
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
97
|
-
process.once(signal,
|
|
98
|
-
try {
|
|
99
|
-
await shutdown();
|
|
100
|
-
process.exit(0);
|
|
101
|
-
} catch (error) {
|
|
102
|
-
console.error(`Runtime shutdown failed: ${error.message}`);
|
|
103
|
-
process.exit(1);
|
|
104
|
-
}
|
|
105
|
-
});
|
|
109
|
+
process.once(signal, requestShutdown);
|
|
106
110
|
}
|
|
111
|
+
process.on("message", (message) => {
|
|
112
|
+
if (message?.type === "minecodex:shutdown") void requestShutdown();
|
|
113
|
+
});
|
|
114
|
+
if (process.connected) process.once("disconnect", requestShutdown);
|