minecodex 0.2.2 → 1.0.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 +2 -0
- package/features/hide-upsell-banner/src/hide-upsell-banner.mjs +2 -1
- package/features/images/codex-feature.json +2 -2
- package/features/images/src/http-server.mjs +67 -1
- package/features/images/src/prompt-index.mjs +45 -10
- package/features/images/web/app.js +555 -0
- package/features/images/web/index.html +16 -1
- package/features/images/web/styles.css +80 -0
- package/package.json +1 -1
- package/packages/cli/src/commands.mjs +117 -73
- package/packages/cli/src/{install-progress.mjs → progress.mjs} +2 -2
- package/packages/runtime-host/src/codex-runtime.mjs +318 -21
- package/packages/runtime-host/src/feature-registry.mjs +7 -0
- package/packages/runtime-host/src/main.mjs +1 -1
package/README.md
CHANGED
|
@@ -26,6 +26,8 @@ mcx install
|
|
|
26
26
|
|
|
27
27
|
运行 `mcx install` 不会强制关闭或启动 Codex,也不会影响当前打开的窗口。安装完成后,终端会说明更改将在下一次重启 Codex 时生效,并询问是否立即重启;默认选项为 **No**。只有在您明确确认,或后续手动运行 `mcx restart` 时,才会重新打开 Codex。安装完成后,macOS 的“登录项与扩展”中会添加名为 **MineCodex** 的后台项目。
|
|
28
28
|
|
|
29
|
+
`mcx install`、`mcx update`、`mcx restart` 和 `mcx uninstall` 会在终端持续显示当前阶段;`mcx open` 仅在需要启动后台服务时显示进度。查询类命令保持简洁,便于直接阅读或用于脚本。
|
|
30
|
+
|
|
29
31
|
MineCodex 只以后台辅助服务运行,前台应用始终是 OpenAI 签名的官方 `ChatGPT.app`,并继续使用官方默认用户数据。通过 Dock、Finder、Spotlight、登录项或会话恢复打开 ChatGPT 时,后台服务会验证官方应用与对应进程;若该进程尚未开放兼容的本机调试端口,则确认其完整退出后再通过 LaunchServices 重新打开官方应用并注入功能。MineCodex 不修改 `ChatGPT.app`、`app.asar`、Dock 项目或默认打开方式。
|
|
30
32
|
|
|
31
33
|
打开本地控制台:
|
|
@@ -15,7 +15,8 @@ function isUpsellBanner(element) {
|
|
|
15
15
|
if (!text) return false;
|
|
16
16
|
const isCard = element.classList.contains("rounded-2xl")
|
|
17
17
|
&& element.classList.contains("border")
|
|
18
|
-
&& element.classList.contains("bg-token-main-surface-primary")
|
|
18
|
+
&& (element.classList.contains("bg-token-main-surface-primary")
|
|
19
|
+
|| element.classList.contains("bg-surface"));
|
|
19
20
|
if (!isCard) return false;
|
|
20
21
|
const titleMatch = /out of Codex and Work usage/i.test(text)
|
|
21
22
|
|| /out of Codex messages/i.test(text)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"id": "images",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.12",
|
|
5
5
|
"label": {
|
|
6
6
|
"en": "Images",
|
|
7
7
|
"zh-CN": "图片"
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"pageScript": {
|
|
17
17
|
"resource": "src/chatgpt-image-source-page.mjs"
|
|
18
18
|
},
|
|
19
|
-
"hostActions": ["import-generated-image"],
|
|
19
|
+
"hostActions": ["import-generated-image", "create-image-edit-thread"],
|
|
20
20
|
"placement": {
|
|
21
21
|
"after": "sites",
|
|
22
22
|
"order": 10
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createReadStream } from "node:fs";
|
|
2
|
-
import { readFile, stat } from "node:fs/promises";
|
|
2
|
+
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import { createServer as createNodeServer } from "node:http";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import { MAX_IMPORTED_IMAGE_BYTES } from "./image-library.mjs";
|
|
6
7
|
import { saveImageAs } from "./save-as.mjs";
|
|
@@ -11,6 +12,7 @@ const DEFAULT_PAGE_LIMIT = 36;
|
|
|
11
12
|
const MAX_PAGE_LIMIT = 72;
|
|
12
13
|
const MAX_PAGE_OFFSET = Number.MAX_SAFE_INTEGER;
|
|
13
14
|
const MAX_IMPORT_BODY_BYTES = Math.ceil(MAX_IMPORTED_IMAGE_BYTES * 4 / 3) + 64 * 1024;
|
|
15
|
+
const MAX_EDIT_AUXILIARY_BYTES = 8 * 1024 * 1024;
|
|
14
16
|
|
|
15
17
|
const STATIC_FILES = new Map([
|
|
16
18
|
["/", ["index.html", "text/html; charset=utf-8"]],
|
|
@@ -134,6 +136,34 @@ async function readImportPayload(request) {
|
|
|
134
136
|
return { ...payload, bytes };
|
|
135
137
|
}
|
|
136
138
|
|
|
139
|
+
async function readEditDraftPayload(request) {
|
|
140
|
+
if (!String(request.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) {
|
|
141
|
+
throw Object.assign(new Error("Image edit draft requires application/json"), { status: 415, code: "UNSUPPORTED_CONTENT_TYPE" });
|
|
142
|
+
}
|
|
143
|
+
let payload;
|
|
144
|
+
try {
|
|
145
|
+
payload = JSON.parse((await readRequestBody(request, Math.ceil(MAX_EDIT_AUXILIARY_BYTES * 4 / 3) + 8 * 1024)).toString("utf8"));
|
|
146
|
+
} catch (error) {
|
|
147
|
+
if (error.code === "BODY_TOO_LARGE") throw error;
|
|
148
|
+
throw Object.assign(new Error("Image edit draft must be valid JSON"), { status: 400, code: "INVALID_JSON" });
|
|
149
|
+
}
|
|
150
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)
|
|
151
|
+
|| !/^[a-f0-9]{24}$/.test(payload.imageId ?? "")) {
|
|
152
|
+
throw Object.assign(new Error("Image edit draft requires an image id"), { status: 400, code: "INVALID_EDIT_DRAFT" });
|
|
153
|
+
}
|
|
154
|
+
if (payload.auxiliaryPngBase64 === undefined) return { imageId: payload.imageId, auxiliary: null };
|
|
155
|
+
if (typeof payload.auxiliaryPngBase64 !== "string") {
|
|
156
|
+
throw Object.assign(new Error("Image edit auxiliary image must be base64"), { status: 400, code: "INVALID_EDIT_AUXILIARY" });
|
|
157
|
+
}
|
|
158
|
+
const auxiliary = Buffer.from(payload.auxiliaryPngBase64, "base64");
|
|
159
|
+
if (!auxiliary.length || auxiliary.length > MAX_EDIT_AUXILIARY_BYTES
|
|
160
|
+
|| auxiliary.toString("base64") !== payload.auxiliaryPngBase64.replace(/\s/g, "")
|
|
161
|
+
|| auxiliary.readUInt32BE(0) !== 0x89504e47) {
|
|
162
|
+
throw Object.assign(new Error("Image edit auxiliary image must be a bounded PNG"), { status: 400, code: "INVALID_EDIT_AUXILIARY" });
|
|
163
|
+
}
|
|
164
|
+
return { imageId: payload.imageId, auxiliary };
|
|
165
|
+
}
|
|
166
|
+
|
|
137
167
|
function contentTypeForPath(sourcePath) {
|
|
138
168
|
const extension = path.extname(sourcePath).toLowerCase();
|
|
139
169
|
return extension === ".jpg" || extension === ".jpeg"
|
|
@@ -225,6 +255,7 @@ export async function createHttpServer({
|
|
|
225
255
|
}) {
|
|
226
256
|
if (!library) throw new Error("library is required");
|
|
227
257
|
if (!LOOPBACK_HOSTS.has(host)) throw new Error("Images only supports loopback hosts");
|
|
258
|
+
const editDrafts = new Map();
|
|
228
259
|
const server = createNodeServer(async (request, response) => {
|
|
229
260
|
try {
|
|
230
261
|
applyCodexEmbedCors(request, response);
|
|
@@ -324,6 +355,41 @@ export async function createHttpServer({
|
|
|
324
355
|
return;
|
|
325
356
|
}
|
|
326
357
|
|
|
358
|
+
if (request.method === "POST" && url.pathname === "/api/image-edit-draft") {
|
|
359
|
+
const payload = await readEditDraftPayload(request);
|
|
360
|
+
const image = library.get(payload.imageId);
|
|
361
|
+
if (!image) {
|
|
362
|
+
json(response, 404, { error: "Image not found" });
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
let auxiliaryPath = null;
|
|
366
|
+
if (payload.auxiliary) {
|
|
367
|
+
const editDir = path.join(library.dataDir, "edit-drafts");
|
|
368
|
+
await mkdir(editDir, { recursive: true });
|
|
369
|
+
auxiliaryPath = path.join(editDir, `${randomUUID()}.png`);
|
|
370
|
+
await writeFile(auxiliaryPath, payload.auxiliary, { mode: 0o600 });
|
|
371
|
+
}
|
|
372
|
+
const draftId = randomUUID();
|
|
373
|
+
editDrafts.set(draftId, { sourcePath: image.sourcePath, auxiliaryPath });
|
|
374
|
+
json(response, 201, { draftId });
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const editDraftMatch = url.pathname.match(/^\/api\/image-edit-draft\/([a-f0-9-]{36})$/);
|
|
379
|
+
if (request.method === "GET" && editDraftMatch) {
|
|
380
|
+
if (request.headers.origin !== CODEX_EMBED_ORIGIN) {
|
|
381
|
+
throw mutationError("Image edit attachments are available only to RuntimeHost", "ORIGIN_NOT_ALLOWED");
|
|
382
|
+
}
|
|
383
|
+
const draft = editDrafts.get(editDraftMatch[1]);
|
|
384
|
+
if (!draft) {
|
|
385
|
+
json(response, 404, { error: "Image edit draft not found" });
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
editDrafts.delete(editDraftMatch[1]);
|
|
389
|
+
json(response, 200, draft);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
327
393
|
const saveAsMatch = url.pathname.match(/^\/api\/images\/([a-f0-9]{24})\/save-as$/);
|
|
328
394
|
if (request.method === "POST" && saveAsMatch) {
|
|
329
395
|
const image = library.get(saveAsMatch[1]);
|
|
@@ -22,26 +22,61 @@ async function findThreadLogs(directory, threadId, output = []) {
|
|
|
22
22
|
return output;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
function
|
|
26
|
-
if (
|
|
25
|
+
function legacyPromptFromPayload(payload) {
|
|
26
|
+
if (payload?.type !== "image_generation_end") return null;
|
|
27
|
+
if (typeof payload.call_id !== "string" || typeof payload.revised_prompt !== "string") return null;
|
|
28
|
+
return { imageGenerationItemId: payload.call_id, prompt: payload.revised_prompt };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function wrappedImagegenPrompt(payload) {
|
|
32
|
+
if (payload?.type !== "custom_tool_call" || payload.name !== "exec") return null;
|
|
33
|
+
if (typeof payload.call_id !== "string" || typeof payload.input !== "string") return null;
|
|
34
|
+
const marker = "tools.image_gen__imagegen";
|
|
35
|
+
const markerIndex = payload.input.indexOf(marker);
|
|
36
|
+
if (markerIndex < 0) return null;
|
|
37
|
+
const generatedImageIndex = payload.input.indexOf("generatedImage(", markerIndex + marker.length);
|
|
38
|
+
const callSource = payload.input.slice(markerIndex, generatedImageIndex < 0 ? undefined : generatedImageIndex);
|
|
39
|
+
const match = callSource.match(/\bprompt\s*:\s*("(?:\\.|[^"\\])*")/s);
|
|
40
|
+
if (!match) return null;
|
|
27
41
|
try {
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
if (payload?.type !== "image_generation_end") return null;
|
|
31
|
-
if (typeof payload.call_id !== "string" || typeof payload.revised_prompt !== "string") return null;
|
|
32
|
-
return { imageGenerationItemId: payload.call_id, prompt: payload.revised_prompt };
|
|
42
|
+
const prompt = JSON.parse(match[1]);
|
|
43
|
+
return typeof prompt === "string" ? { callId: payload.call_id, prompt } : null;
|
|
33
44
|
} catch {
|
|
34
45
|
return null;
|
|
35
46
|
}
|
|
36
47
|
}
|
|
37
48
|
|
|
49
|
+
function generatedItemIdsFromOutputLine(line) {
|
|
50
|
+
if (!line.includes('"type":"custom_tool_call_output"') || !line.includes("generated_images")) return null;
|
|
51
|
+
const callId = line.match(/"call_id":"([a-zA-Z0-9_-]+)"/)?.[1];
|
|
52
|
+
if (!callId) return null;
|
|
53
|
+
const itemIds = new Set();
|
|
54
|
+
const pattern = /generated_images[\\/][^\\/\s]+[\\/]([a-zA-Z0-9_-]+)\.png\b/g;
|
|
55
|
+
for (const match of line.matchAll(pattern)) itemIds.add(match[1]);
|
|
56
|
+
return { callId, itemIds };
|
|
57
|
+
}
|
|
58
|
+
|
|
38
59
|
async function readPrompts(logPath, prompts) {
|
|
39
60
|
const input = createReadStream(logPath, { encoding: "utf8" });
|
|
40
61
|
const lines = createInterface({ input, crlfDelay: Infinity });
|
|
62
|
+
const wrappedPrompts = new Map();
|
|
41
63
|
for await (const line of lines) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
64
|
+
if (line.includes('"image_generation_end"') || line.includes("image_gen__imagegen")) {
|
|
65
|
+
let event;
|
|
66
|
+
try { event = JSON.parse(line); } catch { continue; }
|
|
67
|
+
const payload = event?.payload ?? event;
|
|
68
|
+
const legacy = legacyPromptFromPayload(payload);
|
|
69
|
+
if (legacy && !prompts.has(legacy.imageGenerationItemId)) {
|
|
70
|
+
prompts.set(legacy.imageGenerationItemId, legacy.prompt);
|
|
71
|
+
}
|
|
72
|
+
const wrapped = wrappedImagegenPrompt(payload);
|
|
73
|
+
if (wrapped) wrappedPrompts.set(wrapped.callId, wrapped.prompt);
|
|
74
|
+
}
|
|
75
|
+
const generated = generatedItemIdsFromOutputLine(line);
|
|
76
|
+
const prompt = generated ? wrappedPrompts.get(generated.callId) : null;
|
|
77
|
+
if (!prompt) continue;
|
|
78
|
+
for (const itemId of generated.itemIds) {
|
|
79
|
+
if (!prompts.has(itemId)) prompts.set(itemId, prompt);
|
|
45
80
|
}
|
|
46
81
|
}
|
|
47
82
|
}
|