minecodex 2.0.43 → 2.0.44
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/features/composer-expand/src/native.mjs +43 -7
- package/features/quick-chat/src/adapters/codex/shortcut.mjs +55 -21
- package/features/quick-chat/src/adapters/codex/sidebar.mjs +1 -1
- package/features/quick-chat/src/injection-source.mjs +2 -2
- package/package.json +1 -1
- package/packages/cli/src/platform.mjs +8 -1
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
export async function loadComposerExpandControls(signal) {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
signal.throwIfAborted();
|
|
3
|
+
function modules() {
|
|
4
|
+
const urls = [...document.querySelectorAll('link[rel="modulepreload"],script[src]')].map(node => node.href || node.src);
|
|
5
|
+
const initialUrl = urls.find(url => /\/app-initial-[^/]+\.js/.test(url));
|
|
6
|
+
const mainUrl = urls.find(url => /\/app-main-[^/]+\.js/.test(url));
|
|
7
|
+
return initialUrl && mainUrl ? {initialUrl,mainUrl} : null;
|
|
8
|
+
}
|
|
9
|
+
// 冷启动时入口标签可能晚于 DOMContentLoaded 挂载。
|
|
10
|
+
const {initialUrl,mainUrl} = modules() ?? await new Promise((resolve,reject) => {
|
|
11
|
+
const observer = new MutationObserver(check);
|
|
12
|
+
function cleanup() { observer.disconnect();signal.removeEventListener('abort',abort); }
|
|
13
|
+
function abort() { cleanup();reject(signal.reason); }
|
|
14
|
+
function check() { const found=modules();if(found){cleanup();resolve(found);} }
|
|
15
|
+
observer.observe(document.documentElement,{subtree:true,childList:true,attributes:true,attributeFilter:['src','href','rel']});
|
|
16
|
+
signal.addEventListener('abort',abort,{once:true});
|
|
17
|
+
check();
|
|
18
|
+
});
|
|
6
19
|
const read = async url => { const response=await fetch(url,{signal}); if(!response.ok) throw new Error('composer-expand-source-unavailable'); return response.text(); };
|
|
7
20
|
const [initialSource,mainSource] = await Promise.all([read(initialUrl),read(mainUrl)]);
|
|
8
21
|
const file = initialSource.match(/["'`]\.\/(app-primary-[\w-]+\.js)["'`]/)?.[1];
|
|
@@ -13,17 +26,40 @@ export async function loadComposerExpandControls(signal) {
|
|
|
13
26
|
if (!local) throw new Error('composer-expand-binding-unavailable');
|
|
14
27
|
const escaped = local.replace(/[.*+?^${}()|[\]\\]/g,'\\$&');
|
|
15
28
|
const alias = source.slice(source.lastIndexOf('export{')).match(new RegExp(`(?:\\{|,)${escaped} as ([\\w$]+)(?:,|\\})`))?.[1];
|
|
16
|
-
if (alias)
|
|
29
|
+
if (alias) {
|
|
30
|
+
const module=await import(url);
|
|
31
|
+
if (module[alias]===undefined) {
|
|
32
|
+
// 图标导出可能仍在原生 once 初始化器中,先初始化再读取实时导出。
|
|
33
|
+
const assignment=source.search(new RegExp(`(?:[,;{])${escaped}=`));
|
|
34
|
+
const initializer=assignment<0?null:source.slice(source.lastIndexOf('function ',assignment),assignment+1)
|
|
35
|
+
.match(/^function ([\w$]+)\(\)\{return\(\1=[\w$]+\(\(\(\)=>\{/);
|
|
36
|
+
if (!initializer) throw new Error('composer-expand-binding-unavailable');
|
|
37
|
+
const initialize=await binding(source,url,initializer[1]);
|
|
38
|
+
if (typeof initialize!=='function') throw new Error('composer-expand-binding-unavailable');
|
|
39
|
+
initialize();
|
|
40
|
+
}
|
|
41
|
+
if (module[alias]===undefined) throw new Error('composer-expand-binding-unavailable');
|
|
42
|
+
return module[alias];
|
|
43
|
+
}
|
|
17
44
|
for (const match of source.matchAll(/import\{([^}]+)\}from["'`]([^"'`]+)["'`]/g)) {
|
|
18
45
|
for (const entry of match[1].split(',')) {
|
|
19
46
|
const parts=entry.trim().split(/\s+as\s+/);
|
|
20
|
-
if (parts.at(-1)===local)
|
|
47
|
+
if (parts.at(-1)===local) {
|
|
48
|
+
const dependencyUrl=new URL(match[2],url).href,module=await import(dependencyUrl);
|
|
49
|
+
if (module[parts[0]]!==undefined) return module[parts[0]];
|
|
50
|
+
const dependency=await read(dependencyUrl);
|
|
51
|
+
const exported=parts[0].replace(/[.*+?^${}()|[\]\\]/g,'\\$&');
|
|
52
|
+
const name=dependency.slice(dependency.lastIndexOf('export{')).match(new RegExp(`(?:\\{|,)([\\w$]+) as ${exported}(?:,|\\})`))?.[1];
|
|
53
|
+
return binding(dependency,dependencyUrl,name);
|
|
54
|
+
}
|
|
21
55
|
}
|
|
22
56
|
}
|
|
23
57
|
throw new Error('composer-expand-binding-unavailable');
|
|
24
58
|
}
|
|
25
59
|
// 语义定位原生展开按钮,导入它实际使用的控件和成对图标。
|
|
26
|
-
|
|
60
|
+
// 只匹配控件属性,避免先命中布局 className 中的同名 CSS 选择器。
|
|
61
|
+
const marker=source.search(/["'`]data-composer-expand-toggle["'`]:/);
|
|
62
|
+
const start=marker<0?-1:source.lastIndexOf('function ',marker);
|
|
27
63
|
const control=source.slice(start,source.indexOf('var ',start));
|
|
28
64
|
const button=control.match(/jsx\)\(([\w$]+),\{"aria-label"/ )?.[1];
|
|
29
65
|
const icons=control.match(/children:[\w$]+\?\(0,[\w$]+\.jsx\)\(([\w$]+),\{\}\):\(0,[\w$]+\.jsx\)\(([\w$]+),\{\}\)/);
|
|
@@ -21,19 +21,29 @@ export function quickChatLifecycleExports(source) {
|
|
|
21
21
|
const exportOf = name => name && exports.match(new RegExp(`(?:\\{|,)${escaped(name)} as ([\\w$]+)(?:,|\\})`))?.[1];
|
|
22
22
|
const createExport = exportOf(create), openState = exportOf(state);
|
|
23
23
|
if (!createExport || !openState) throw new Error('quick-chat-lifecycle-contract-unavailable');
|
|
24
|
-
|
|
24
|
+
// 冷启动时状态 atom 也可能尚未初始化,定位其所属的原生 once 初始化器。
|
|
25
|
+
const assignment = source.search(new RegExp(`(?:[,;{])${escaped(state)}=[\\w$]+\\([\\w$]+,!1\\)`));
|
|
26
|
+
const initializer = assignment < 0 ? null : source.slice(source.lastIndexOf('function ', assignment), assignment + 1)
|
|
27
|
+
.match(/^function ([\w$]+)\(\)\{return\(\1=[\w$]+\(\(\(\)=>\{/);
|
|
28
|
+
const initialize = exportOf(initializer?.[1]);
|
|
29
|
+
return {open, create:createExport, openState, ...(initialize ? {initialize} : {})};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 命令目录是懒初始化导出;按命令定义识别,不能要求数组已经初始化。
|
|
33
|
+
export function quickChatNativeCommand(source) {
|
|
34
|
+
const ids = new Set([...source.matchAll(/\{id:[`"']([\w$.-]+)[`"'],titleIntlId:[`"']codex\.command\.[^`"']+[`"']/g)].map(match => match[1]));
|
|
35
|
+
if (!ids.has('temporaryChat') || !ids.has('newTask')) throw new Error('quick-chat-command-catalog-unavailable');
|
|
36
|
+
return ids.has('quickChat');
|
|
25
37
|
}
|
|
26
38
|
|
|
27
39
|
export function installQuickChatShortcut(signal, codex, discover = null) {
|
|
28
40
|
const windows = navigator.platform.startsWith('Win');
|
|
29
|
-
let bindings = null, disposed = false, pendingFocus = null;
|
|
30
|
-
|
|
31
|
-
&& (p.unifiedDocking === true || typeof p.dockThreadKey === 'string' && typeof p.renderMenu === 'function'));
|
|
32
|
-
function activateExisting() {
|
|
41
|
+
let bindings = null, disposed = false, pendingFocus = null, stopWaiting = null;
|
|
42
|
+
function activateExisting(minimize = false) {
|
|
33
43
|
// 原生恢复按钮同时恢复 presentation、取消最小化并聚焦输入框。
|
|
34
44
|
for (const button of document.querySelectorAll('button')) {
|
|
35
45
|
if (!button.checkVisibility({visibilityProperty:true})) continue;
|
|
36
|
-
const floating =
|
|
46
|
+
const floating = codex.props(button, p => typeof p.isMinimized === 'boolean' && typeof p.onMinimizedChange === 'function' && typeof p.onPresentationChange === 'function');
|
|
37
47
|
if (!floating?.isMinimized || floating.isHidden) continue;
|
|
38
48
|
const restore = codex.props(button, p => p.uniform === true && p.size === 'composer' && typeof p.onClick === 'function'
|
|
39
49
|
&& typeof p.onPointerEnter === 'function' && typeof p.onPointerDown === 'function' && typeof p.onKeyDown === 'function');
|
|
@@ -43,11 +53,12 @@ export function installQuickChatShortcut(signal, codex, discover = null) {
|
|
|
43
53
|
}
|
|
44
54
|
for (const surface of document.querySelectorAll('[data-floating-chat-surface]')) {
|
|
45
55
|
if (surface.closest('[hidden], [inert], [aria-hidden="true"]')) continue;
|
|
46
|
-
const floating =
|
|
56
|
+
const floating = codex.props(surface, p => typeof p.isMinimized === 'boolean' && typeof p.onMinimizedChange === 'function' && typeof p.onPresentationChange === 'function');
|
|
47
57
|
if (!floating || floating.isMinimized || floating.isHidden) continue;
|
|
48
58
|
const editable = surface.querySelector('[data-chatgpt-composer] [contenteditable="true"]');
|
|
49
59
|
if (!editable?.checkVisibility({visibilityProperty:true})) continue;
|
|
50
|
-
|
|
60
|
+
if (minimize) { pendingFocus=null; floating.onMinimizedChange(true); }
|
|
61
|
+
else editable.focus({preventScroll:true});
|
|
51
62
|
return true;
|
|
52
63
|
}
|
|
53
64
|
return false;
|
|
@@ -69,8 +80,9 @@ export function installQuickChatShortcut(signal, codex, discover = null) {
|
|
|
69
80
|
function context() {
|
|
70
81
|
for (const button of document.querySelectorAll('button[aria-haspopup="menu"]')) {
|
|
71
82
|
if (!button.checkVisibility({visibilityProperty:true})) continue;
|
|
72
|
-
const
|
|
73
|
-
|
|
83
|
+
const picker = codex.props(button, p => typeof p.onModeSelect === 'function' && typeof p.mode === 'string', 80);
|
|
84
|
+
// 原生 ChatGPT 也使用 work 路由,具体产品由 workModeAccess 区分。
|
|
85
|
+
if (picker?.mode !== 'codex' && !(picker?.mode === 'work' && picker.workModeAccess === 'chatgpt')) continue;
|
|
74
86
|
for (let fiber = codex.currentFiber(button), depth = 0; fiber && depth++ < 200; fiber = fiber.return) {
|
|
75
87
|
const values = [];
|
|
76
88
|
for (let hook = fiber.memoizedState, n = 0; hook && n++ < 150; hook = hook.next) values.push(hook.memoizedState?.current);
|
|
@@ -82,14 +94,35 @@ export function installQuickChatShortcut(signal, codex, discover = null) {
|
|
|
82
94
|
return null;
|
|
83
95
|
}
|
|
84
96
|
async function load() {
|
|
85
|
-
const
|
|
86
|
-
|
|
97
|
+
const findUrl = () => [...document.querySelectorAll('link[rel="modulepreload"],script[src]')].map(e => e.href || e.src).find(url => /\/app-initial-[^/]+\.js(?:$|\?)/.test(url));
|
|
98
|
+
// 宿主可早于原生模块入口注入;等待入口挂载,不能把正常启动顺序当作协议失效。
|
|
99
|
+
const url = findUrl() ?? await new Promise((resolve, reject) => {
|
|
100
|
+
codex.report('quick-chat-shortcut', false, 'quick-chat-module-pending');
|
|
101
|
+
const cleanup = () => { observer.disconnect(); signal.removeEventListener('abort', cancel); stopWaiting=null; };
|
|
102
|
+
const cancel = () => { cleanup(); reject(new Error('quick-chat-load-aborted')); };
|
|
103
|
+
const check = () => { const found=findUrl(); if (found) { cleanup(); resolve(found); } };
|
|
104
|
+
const observer = new MutationObserver(check);
|
|
105
|
+
stopWaiting=cancel;
|
|
106
|
+
observer.observe(document.documentElement, {childList:true,subtree:true,attributes:true,attributeFilter:['href','src']});
|
|
107
|
+
signal.addEventListener('abort', cancel, {once:true});
|
|
108
|
+
if (disposed || signal.aborted) cancel(); else check();
|
|
109
|
+
});
|
|
87
110
|
const source = await (await fetch(url, {signal})).text();
|
|
88
111
|
const module = await import(url);
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
112
|
+
let native;
|
|
113
|
+
try { native=quickChatNativeCommand(source); }
|
|
114
|
+
catch (error) {
|
|
115
|
+
if (error.message !== 'quick-chat-command-catalog-unavailable') throw error;
|
|
116
|
+
// 新版命令目录移入共享模块;打开动作仍由入口模块导出。
|
|
117
|
+
const file=source.match(/["'`]\.\/(app-shared-[\w-]+\.js)["'`]/)?.[1];
|
|
118
|
+
if (!file) throw error;
|
|
119
|
+
const response=await fetch(new URL(file,url).href,{signal});
|
|
120
|
+
if (!response.ok) throw error;
|
|
121
|
+
native=quickChatNativeCommand(await response.text());
|
|
122
|
+
}
|
|
123
|
+
if (native) return {native:true};
|
|
92
124
|
const roles = quickChatLifecycleExports(source);
|
|
125
|
+
if (!module[roles.openState] && typeof module[roles.initialize] === 'function') module[roles.initialize]();
|
|
93
126
|
const open = module[roles.open], create = module[roles.create], openState = module[roles.openState];
|
|
94
127
|
if (typeof open !== 'function' || typeof create !== 'function' || !openState) throw new Error('quick-chat-open-action-unavailable');
|
|
95
128
|
return {native:false, open, create, isOpen:scope => scope.get(openState)};
|
|
@@ -98,15 +131,16 @@ export function installQuickChatShortcut(signal, codex, discover = null) {
|
|
|
98
131
|
if (disposed || signal.aborted) return;
|
|
99
132
|
bindings = value;
|
|
100
133
|
codex.report('quick-chat-shortcut', true);
|
|
101
|
-
}).catch(
|
|
102
|
-
|
|
134
|
+
}).catch(error => { if (!disposed && !signal.aborted) codex.report('quick-chat-shortcut', false,
|
|
135
|
+
/^quick-chat-[\w-]+$/.test(error?.message) ? error.message : 'native-shortcut-contract-unavailable'); });
|
|
136
|
+
function activate({minimize=false} = {}) {
|
|
103
137
|
if (disposed || signal.aborted || !bindings || bindings.native) return false;
|
|
104
|
-
const scope =
|
|
138
|
+
const scope = context();
|
|
105
139
|
if (scope && bindings.isOpen(scope) === false) {
|
|
106
140
|
pendingFocus={scope,previous:new Set([...document.querySelectorAll('[data-floating-chat-surface]')].map(sessionId))};
|
|
107
141
|
bindings.create(scope);
|
|
108
142
|
focusNew();
|
|
109
|
-
} else if (!activateExisting()) {
|
|
143
|
+
} else if (!activateExisting(minimize)) {
|
|
110
144
|
if (!scope) return false;
|
|
111
145
|
bindings.open(scope);
|
|
112
146
|
}
|
|
@@ -115,12 +149,12 @@ export function installQuickChatShortcut(signal, codex, discover = null) {
|
|
|
115
149
|
function keydown(event) {
|
|
116
150
|
const primaryModifier = windows ? event.ctrlKey && !event.metaKey : event.metaKey && !event.ctrlKey;
|
|
117
151
|
if (event.defaultPrevented || event.repeat || event.isComposing
|
|
118
|
-
|| !primaryModifier || !event.altKey || event.shiftKey || event.code !== 'KeyN' || !activate()) return;
|
|
152
|
+
|| !primaryModifier || !event.altKey || event.shiftKey || event.code !== 'KeyN' || !activate({minimize:true})) return;
|
|
119
153
|
event.preventDefault();
|
|
120
154
|
event.stopPropagation();
|
|
121
155
|
}
|
|
122
156
|
document.addEventListener('keydown', keydown, {signal});
|
|
123
|
-
function dispose() { disposed = true; document.removeEventListener('keydown', keydown); bindings = null; pendingFocus=null; }
|
|
157
|
+
function dispose() { disposed = true; stopWaiting?.(); document.removeEventListener('keydown', keydown); bindings = null; pendingFocus=null; }
|
|
124
158
|
signal.addEventListener('abort', dispose, {once:true});
|
|
125
159
|
return {ready, dispose, sync:focusNew, activate, available:() => Boolean(bindings && !bindings.native && !disposed && !signal.aborted)};
|
|
126
160
|
}
|
|
@@ -3,7 +3,7 @@ export async function loadQuickChatSidebarIcon(signal) {
|
|
|
3
3
|
const initialUrl = urls.find(url => /\/app-initial-[^/]+\.js/.test(url));
|
|
4
4
|
if (!initialUrl) throw new Error('quick-chat-icon-module-unavailable');
|
|
5
5
|
const initial = await (await fetch(initialUrl,{signal})).text();
|
|
6
|
-
const file = initial.match(/["']\.\/(quick-chat-floating-surface-[\w-]+\.js)["']/)?.[1];
|
|
6
|
+
const file = initial.match(/["']\.\/((?:quick-chat-floating-surface|floating-chat-surface)-[\w-]+\.js)["']/)?.[1];
|
|
7
7
|
if (!file) throw new Error('quick-chat-icon-module-unavailable');
|
|
8
8
|
const url = new URL(file,initialUrl).href;
|
|
9
9
|
const source = await (await fetch(url,{signal})).text();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {quickChatHistoryVisible} from "./adapters/codex/history.mjs";
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import { createQuickChatAdapter } from "./adapters/codex/renderer.mjs";
|
|
4
|
-
import { quickChatOpenExport, quickChatLifecycleExports, installQuickChatShortcut } from "./adapters/codex/shortcut.mjs";
|
|
4
|
+
import { quickChatOpenExport, quickChatLifecycleExports, quickChatNativeCommand, installQuickChatShortcut } from "./adapters/codex/shortcut.mjs";
|
|
5
5
|
import { createSubmissionTracker } from "./domain/submission.mjs";
|
|
6
6
|
import { createQuickChatSidebarAdapter, loadQuickChatSidebarIcon } from "./adapters/codex/sidebar.mjs";
|
|
7
7
|
import { installQuickChatSidebar } from "./frontend/sidebar.mjs";
|
|
@@ -9,7 +9,7 @@ import { createHistoryTracker } from "./domain/history.mjs";
|
|
|
9
9
|
export async function createPageScriptSource() {
|
|
10
10
|
const source = await readFile(new URL("./frontend/entry.mjs", import.meta.url), "utf8");
|
|
11
11
|
const functions = [quickChatHistoryVisible, loadQuickChatSidebarIcon, createQuickChatSidebarAdapter, installQuickChatSidebar,
|
|
12
|
-
quickChatOpenExport, quickChatLifecycleExports, installQuickChatShortcut, createQuickChatAdapter,
|
|
12
|
+
quickChatOpenExport, quickChatLifecycleExports, quickChatNativeCommand, installQuickChatShortcut, createQuickChatAdapter,
|
|
13
13
|
createSubmissionTracker, createHistoryTracker];
|
|
14
14
|
return functions.map(fn => `const ${fn.name}=${fn.toString()};`).join('\n') + '\n' + source;
|
|
15
15
|
}
|
package/package.json
CHANGED
|
@@ -851,7 +851,14 @@ export class WindowsPlatformAdapter {
|
|
|
851
851
|
async codex() {
|
|
852
852
|
if (!this.codexInstallation) return this.assertCodexInstalled();
|
|
853
853
|
if (this.codexInstallationInjected) return this.codexInstallation;
|
|
854
|
-
|
|
854
|
+
let currentFingerprint;
|
|
855
|
+
try {
|
|
856
|
+
currentFingerprint = await this.readCodexInstallationFingerprint(this.codexInstallation);
|
|
857
|
+
} catch (error) {
|
|
858
|
+
// MSIX 更新会删除旧版本目录,重新发现并校验当前官方包。
|
|
859
|
+
if (error.code !== "ENOENT") throw error;
|
|
860
|
+
return this.assertCodexInstalled();
|
|
861
|
+
}
|
|
855
862
|
if (this.codexInstallationFingerprint == null) {
|
|
856
863
|
this.codexInstallationFingerprint = currentFingerprint;
|
|
857
864
|
} else if (currentFingerprint !== this.codexInstallationFingerprint) {
|