minecodex 2.0.52 → 2.0.54
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 +1 -1
- package/apps/dashboard/assets/preview-content.js +2 -2
- package/features/mermaid-diagrams/assets/qv-dependencies.js +850 -815
- package/features/mermaid-diagrams/assets/qv-template.json +1 -1
- package/features/mermaid-diagrams/codex-feature.json +4 -4
- package/features/mermaid-diagrams/src/adapters/codex/native.mjs +25 -1
- package/features/mermaid-diagrams/src/injection-source.mjs +2 -2
- package/features/mermaid-diagrams/src/mermaid/detail-position.mjs +7 -0
- package/features/mermaid-diagrams/src/mermaid/frame.mjs +1 -1
- package/features/mermaid-diagrams/src/mermaid/qv-git.js +95 -0
- package/features/mermaid-diagrams/src/mermaid/qv-motion.js +25 -0
- package/features/mermaid-diagrams/src/mermaid/qv-polish.css +20 -0
- package/features/mermaid-diagrams/src/mermaid/qv-title.mjs +11 -0
- package/features/mermaid-diagrams/vendor/qv/THIRD_PARTY_NOTICES.txt +27 -0
- package/features/quick-chat/src/adapters/codex/rspack.mjs +50 -0
- package/features/quick-chat/src/adapters/codex/shortcut.mjs +8 -1
- package/features/quick-chat/src/adapters/codex/sidebar.mjs +5 -1
- package/features/quick-chat/src/injection-source.mjs +3 -2
- package/package.json +2 -1
- package/packages/cli/src/config.mjs +1 -1
- package/packages/codex-adapter/src/markdown/rspack.mjs +3 -4
- package/packages/codex-adapter/src/renderer/modules.mjs +75 -57
- package/packages/plugin/assets/app.js +2 -2
- package/packages/plugin/assets/prompts.js +1 -1
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
"id": "mermaid-diagrams",
|
|
4
4
|
"version": "0.1.0",
|
|
5
5
|
"label": {
|
|
6
|
-
"en": "Mermaid Diagrams
|
|
7
|
-
"zh-CN": "Mermaid
|
|
6
|
+
"en": "Mermaid Diagrams",
|
|
7
|
+
"zh-CN": "Mermaid 图表增强"
|
|
8
8
|
},
|
|
9
9
|
"description": {
|
|
10
|
-
"en": "
|
|
11
|
-
"zh-CN": "在 Markdown
|
|
10
|
+
"en": "Enhance Markdown diagrams with readable layouts, zoom and PNG download. Enabled by default.",
|
|
11
|
+
"zh-CN": "在 Markdown 预览中增强图表排版,支持缩放与 PNG 下载。默认启用。"
|
|
12
12
|
},
|
|
13
13
|
"entry": {
|
|
14
14
|
"kind": "page-script",
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { findCodexModule } from '../../../../../packages/codex-adapter/src/renderer/modules.mjs';
|
|
2
2
|
import { saveQvDiagram } from '../../mermaid/export.mjs';
|
|
3
3
|
|
|
4
|
-
//
|
|
4
|
+
// 图表下载协议只由图表功能加载,阅读增强不依赖此能力。
|
|
5
5
|
export async function loadNativeMermaidActions(native, {read = async url => { const response = await fetch(url); if (!response.ok) throw new Error('native-diagram-source-unavailable'); return response.text(); }, load = url => import(url)} = {}) {
|
|
6
|
+
if (native.rspack) return loadRspackMermaidActions(native);
|
|
6
7
|
const url = await findCodexModule('app-initial',{read});
|
|
7
8
|
if (!url) throw new Error('native-diagram-module-unavailable');
|
|
8
9
|
const source = await read(url);
|
|
@@ -41,3 +42,26 @@ export async function loadNativeMermaidActions(native, {read = async url => { co
|
|
|
41
42
|
return saveQvDiagram(downloadSrc,saveFile,markdownDownloadsDirectory,suggestedFilename);
|
|
42
43
|
} };
|
|
43
44
|
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
export function loadRspackMermaidActions(native) {
|
|
48
|
+
const registry=native.rspack;
|
|
49
|
+
const preview=registry.module(source=>source.includes('imagePreviewDialog.label')&&source.includes('suggestedFilename:')&&source.includes('downloadSrc:'),'diagram preview download');
|
|
50
|
+
const call=preview.source.match(/\(0,([\w$]+\.[\w$]+)\)\(\{downloadSrc:[^}]*suggestedFilename:/);
|
|
51
|
+
const icon=call&&preview.source.slice(call.index).match(/children:\(0,[\w$]+\.jsx\)\(([\w$]+\.[\w$]+),\{className:"icon-xs"\}/)?.[1];
|
|
52
|
+
// 新版原生图标通过组件 + asset 绘制,沿真实下载按钮读取两者。
|
|
53
|
+
const assetIcon=call&&preview.source.slice(call.index).match(/children:\(0,[\w$]+\.jsx\)\(([\w$]+\.[\w$]+),\{"aria-hidden":!1,asset:([\w$]+\.[\w$]+)\}/);
|
|
54
|
+
if (!call || (!icon && !assetIcon)) throw new Error('native-diagram-download-unavailable');
|
|
55
|
+
const downloadImage=registry.binding(preview,call[1]);
|
|
56
|
+
let DownloadIcon;
|
|
57
|
+
if(icon) DownloadIcon=registry.binding(preview,icon);
|
|
58
|
+
else {
|
|
59
|
+
const Icon=registry.binding(preview,assetIcon[1]),asset=registry.binding(preview,assetIcon[2]);
|
|
60
|
+
if(typeof Icon!=='function'||!asset||!native.React?.createElement) throw new Error('native-diagram-download-unavailable');
|
|
61
|
+
DownloadIcon=props=>native.React.createElement(Icon,{...props,asset});
|
|
62
|
+
}
|
|
63
|
+
if (typeof DownloadIcon!=='function' || typeof downloadImage!=='function'
|
|
64
|
+
|| !String(downloadImage).includes('workspaceFiles.saveStream')
|
|
65
|
+
|| !String(downloadImage).includes('Failed to download image')) throw new Error('native-diagram-download-unavailable');
|
|
66
|
+
return {...native,DownloadIcon,downloadImage};
|
|
67
|
+
}
|
|
@@ -14,14 +14,14 @@ import { exportQvDiagram, saveQvDiagram } from './mermaid/export.mjs';
|
|
|
14
14
|
import { createQvLoading } from './mermaid/loading.mjs';
|
|
15
15
|
import { createMarkdownMermaidControls } from './adapters/codex/mermaid-viewer.mjs';
|
|
16
16
|
import { installMarkdownMermaid } from './adapters/codex/mermaid.mjs';
|
|
17
|
-
import { loadNativeMermaidActions } from './adapters/codex/native.mjs';
|
|
17
|
+
import { loadNativeMermaidActions, loadRspackMermaidActions } from './adapters/codex/native.mjs';
|
|
18
18
|
import { createMermaidRuntime } from './adapters/codex/runtime.mjs';
|
|
19
19
|
|
|
20
20
|
export async function createPageScriptSource() {
|
|
21
21
|
const bindings = [findCodexModule, codexModuleStyles,codexModuleRegistry,loadRspackMarkdownListAPI,loadRspackMarkdownToc,loadRspackMarkdownTableActions,loadRspackMarkdownRenderer, createMarkdownPreviewAdapter, loadMarkdownListAPI, captureMarkdownProviders,
|
|
22
22
|
loadNativeMarkdownToc, loadNativeMarkdownRenderer, markdownExportAlias, loadNativeMarkdownTableActions,
|
|
23
23
|
createQvResources, createQvFrame, readQvTheme, exportQvDiagram, saveQvDiagram, createQvLoading,
|
|
24
|
-
createMarkdownMermaidControls, installMarkdownMermaid, loadNativeMermaidActions, createMermaidRuntime];
|
|
24
|
+
createMarkdownMermaidControls, installMarkdownMermaid, loadNativeMermaidActions, loadRspackMermaidActions, createMermaidRuntime];
|
|
25
25
|
const declarations = bindings.map(binding => `const ${binding.name} = ${binding.toString()};`).join('\n');
|
|
26
26
|
const entry = await readFile(new URL('./mermaid-diagrams.mjs',import.meta.url),'utf8');
|
|
27
27
|
return `const markdownDownloadsDirectory=${JSON.stringify(join(homedir(),'Downloads'))};\n${declarations}\n${entry}`;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// 先限定与触发对象的距离,再在局部候选中避让;避免宽画布的空角吸走气泡。
|
|
2
|
+
export function nearbyRadarDetails(candidates) {
|
|
3
|
+
const unobscured = candidates.filter(candidate => !(candidate.pointer ?? candidate.coversPointer));
|
|
4
|
+
const available = unobscured.length ? unobscured : candidates;
|
|
5
|
+
const nearest = Math.min(...available.map(candidate => candidate.distance));
|
|
6
|
+
return available.filter(candidate => candidate.distance <= Math.max(32, nearest + 16));
|
|
7
|
+
}
|
|
@@ -51,7 +51,7 @@ export function createQvFrame(source, assets, theme, onHeight, onReady, onEscape
|
|
|
51
51
|
frame.style.height = '240px';
|
|
52
52
|
let disposed = false, height = 240, input = null;
|
|
53
53
|
const timeout = setTimeout(() => { if (!disposed) onReady('error'); }, 30000);
|
|
54
|
-
const config = { source, title: '', descriptions: {}, icons: 'auto',
|
|
54
|
+
const config = { source, title: '', descriptions: {}, icons: 'auto', readingWidth: theme.readingWidth };
|
|
55
55
|
const encode = (value) => JSON.stringify(value).replace(/</g, '\\u003c');
|
|
56
56
|
const bootstrap = `const token=${encode(token)};window.__mcxDiagramConfig=${encode(config)};window.openai={displayMode:'inline',maxHeight:innerHeight};
|
|
57
57
|
const send=()=>{const root=document.querySelector('.qv-mermaid');parent.postMessage({type:'mcx-qv',token,state:root.dataset.state,height:Math.ceil(root.offsetHeight)},'*');};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// QV 风格的 Git 分支图:模型读取与绘制。由构建器组装进 QV 模板,不修改 vendor 快照。
|
|
2
|
+
// 视觉沿用 QV:分支色取 --viz-series-*(与背景混合降饱和)、12px 文字、按钮承载交互、共享视口与入场动效。
|
|
3
|
+
function readMermaidGitModel(db,descriptions={}){
|
|
4
|
+
if(!db.getCommits||!db.getBranchesAsObjArray)return {reason:'gitGraph 数据接口不兼容'};
|
|
5
|
+
const branches=db.getBranchesAsObjArray().map(b=>b.name);
|
|
6
|
+
// 未写 id 的提交由 Mermaid 生成「序号-7 位哈希」,只展示哈希。
|
|
7
|
+
const commits=[...db.getCommits().values()].sort((a,b)=>a.seq-b.seq).map(c=>({id:String(c.id),message:String(c.message||''),type:c.type,tags:[...(c.tags||[])].map(String),parents:[...(c.parents||[])].map(String),branch:c.branch,auto:/^\d+-[0-9a-f]{7}$/.test(String(c.id)),description:descriptions[c.id]||''}));
|
|
8
|
+
if(!commits.length)return {reason:'gitGraph 没有提交'};
|
|
9
|
+
if(commits.some(c=>[c.id,c.message,...c.tags].some(hasRichMermaidLabel)))return {reason:'gitGraph 包含富文本,保留原生语义'};
|
|
10
|
+
const unknown=Object.keys(descriptions).filter(id=>!commits.some(c=>c.id===id));if(unknown.length)throw new Error('说明引用了未知提交:'+unknown.join('、'));
|
|
11
|
+
return {data:{kind:'git',branches,commits,direction:db.getDirection?.()||'LR',title:db.getDiagramTitle?.()||''}};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function renderMermaidGit(root,data){
|
|
15
|
+
const scene=root.querySelector('.scene'),viewport=root.querySelector('.diagram-scroll'),svg=scene.querySelector('svg'),NS='http://www.w3.org/2000/svg';
|
|
16
|
+
root.classList.add('qv-git');root.dataset.state='enhanced';root.dataset.renderer='qv-git';root.querySelector('.edge-key').hidden=true;
|
|
17
|
+
if(data.title&&root.querySelector('h3').hidden){root.querySelector('h3').textContent=data.title;root.querySelector('h3').hidden=false;}
|
|
18
|
+
const style=document.createElement('style');style.textContent=`
|
|
19
|
+
#${root.id} .git-branch{position:absolute;display:block;box-sizing:border-box;height:24px;padding:0 10px;border:0;border-radius:999px;font:inherit;font-size:12px;line-height:24px;color:color-mix(in srgb,var(--git-tone) 60%,var(--foreground));background:color-mix(in srgb,var(--git-tone) 14%,var(--background));white-space:nowrap;overflow:hidden;text-overflow:ellipsis;cursor:var(--cursor-interaction,pointer)}
|
|
20
|
+
#${root.id} .git-commit{position:absolute;display:flex;align-items:center;gap:8px;box-sizing:border-box;height:24px;padding:0;border:0;background:transparent;font:inherit;font-size:12px;line-height:18px;color:var(--foreground);white-space:nowrap;overflow:hidden;cursor:var(--cursor-interaction,pointer)}
|
|
21
|
+
#${root.id} .git-commit.lr{justify-content:center}
|
|
22
|
+
#${root.id} .git-commit>span{overflow:hidden;text-overflow:ellipsis;min-width:0}
|
|
23
|
+
#${root.id} .git-commit .git-text{flex:0 1 auto}
|
|
24
|
+
#${root.id} .git-commit .git-hash{color:var(--muted-foreground);font-variant-numeric:tabular-nums}
|
|
25
|
+
#${root.id} .git-commit .git-ref{flex:none;max-width:160px;height:20px;padding:0 8px;border-radius:999px;line-height:20px;color:color-mix(in srgb,var(--git-tone) 60%,var(--foreground));background:color-mix(in srgb,var(--git-tone) 14%,var(--background))}
|
|
26
|
+
#${root.id} .git-tag{flex:none;max-width:200px;height:20px;padding:0 8px;box-sizing:border-box;border:1px solid var(--border);border-radius:6px;line-height:18px;color:var(--muted-foreground);background:var(--background);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
27
|
+
#${root.id} .git-tag.lr{position:absolute}
|
|
28
|
+
#${root.id} .git-edge{fill:none;stroke:color-mix(in srgb,var(--git-tone) 70%,var(--background));stroke-width:2;stroke-linecap:round}
|
|
29
|
+
#${root.id} .git-dot{fill:var(--git-tone);stroke:var(--background);stroke-width:2}
|
|
30
|
+
#${root.id} .git-dot.merge{fill:var(--background);stroke:var(--git-tone);stroke-width:2.5}
|
|
31
|
+
#${root.id} .git-dot.cherry{fill:var(--background);stroke:var(--git-tone);stroke-width:2;stroke-dasharray:3 2}
|
|
32
|
+
#${root.id} .git-mark{fill:none;stroke:var(--git-tone);stroke-width:2;stroke-linecap:round}
|
|
33
|
+
#${root.id} [data-branch]{transition:opacity 140ms}
|
|
34
|
+
#${root.id} [data-dim]{opacity:.22}
|
|
35
|
+
#${root.id} .git-commit:focus-visible,#${root.id} .git-branch:focus-visible{outline:1px solid var(--viz-series-1);outline-offset:2px}
|
|
36
|
+
@media(prefers-reduced-motion:reduce){#${root.id} [data-branch]{transition:none}}
|
|
37
|
+
`;root.append(style);
|
|
38
|
+
const el=(tag,attrs)=>{const n=document.createElementNS(NS,tag);for(const [k,v] of Object.entries(attrs))n.setAttribute(k,v);svg.append(n);return n;};
|
|
39
|
+
const html=(tag,cls,text)=>{const n=document.createElement(tag);n.className=cls;if(text!==undefined)n.textContent=text;return n;};
|
|
40
|
+
const lane=new Map(data.branches.map((b,i)=>[b,i])),tone=b=>`var(--viz-series-${(lane.get(b)??0)%6+1})`;
|
|
41
|
+
const index=new Map(data.commits.map((c,i)=>[c.id,i])),firstOf=new Map();data.commits.forEach(c=>{if(!firstOf.has(c.branch))firstOf.set(c.branch,c.id);});
|
|
42
|
+
const context=document.createElement('canvas').getContext('2d');context.font='12px '+getComputedStyle(root).fontFamily;const measure=t=>Math.ceil(context.measureText(t).width);
|
|
43
|
+
const label=c=>c.auto?c.id.split('-')[1]:c.id;
|
|
44
|
+
// 横排放得下正文宽度就沿用 Mermaid 默认方向;放不下改为 git log 式纵排(与流程图主轴规则一致)。
|
|
45
|
+
const limit=cfg.readingWidth||root.clientWidth,n=data.commits.length,lanes=data.branches.length;
|
|
46
|
+
const branchCol=Math.min(160,Math.max(...data.branches.map(measure))+20),step=Math.min(140,Math.max(56,...data.commits.map(c=>measure(label(c))+16)));
|
|
47
|
+
const lrStart=24+branchCol+24,lrWidth=lrStart+(n-1)*step+step/2+24;
|
|
48
|
+
const vertical=data.direction!=='LR'||lrWidth>limit;root.dataset.gitLayout=vertical?'rows':'lanes';root.dataset.mcxLayout=vertical?'git-rows':'git-lanes';
|
|
49
|
+
const commits=[],edges=[],refs=[];let width,height,point;
|
|
50
|
+
if(!vertical){
|
|
51
|
+
const tagged=new Set(data.commits.filter(c=>c.tags.length).map(c=>lane.get(c.branch)));let y=36;const laneY=data.branches.map((b,i)=>{if(tagged.has(i))y+=26;const v=y;y+=56;return v;});
|
|
52
|
+
width=lrWidth;height=laneY.at(-1)+48;point=c=>({x:lrStart+index.get(c.id)*step,y:laneY[lane.get(c.branch)]});
|
|
53
|
+
data.branches.forEach((b,i)=>{const r=html('button','git-branch',b);r.type='button';r.title=b;r.dataset.branch=b;r.style.setProperty('--git-tone',tone(b));r.style.left='24px';r.style.top=(laneY[i]-12)+'px';r.style.maxWidth=branchCol+'px';scene.append(r);refs.push(r);});
|
|
54
|
+
}else{
|
|
55
|
+
const rowH=32,top=12,textX=24+lanes*24+12,order=data.direction==='BT'?[...data.commits].reverse():data.commits,row=new Map(order.map((c,i)=>[c.id,i]));
|
|
56
|
+
width=Math.max(textX+240,limit);height=top+n*rowH+12;point=c=>({x:24+12+lane.get(c.branch)*24,y:top+row.get(c.id)*rowH+rowH/2});
|
|
57
|
+
}
|
|
58
|
+
scene.style.width=scene.style.minWidth=width+'px';scene.style.height=height+'px';svg.setAttribute('width',width);svg.setAttribute('height',height);svg.setAttribute('viewBox',`0 0 ${width} ${height}`);svg.style.width=width+'px';svg.style.height=height+'px';
|
|
59
|
+
// 分支分出时在父提交附近转入新泳道;合并时沿来源泳道前进,在合并提交前转入。
|
|
60
|
+
const toXY=(m,c)=>vertical?[c,m]:[m,c],unit=vertical?32:step;
|
|
61
|
+
for(const c of data.commits){const b=point(c),bm=vertical?b.y:b.x,bc=vertical?b.x:b.y;
|
|
62
|
+
for(const [k,pid] of c.parents.entries()){const parent=data.commits[index.get(pid)];if(!parent)continue;const a=point(parent),am=vertical?a.y:a.x,ac=vertical?a.x:a.y,s=Math.sign(bm-am)||1,r=Math.min(Math.abs(bm-am),unit);
|
|
63
|
+
let d,owner;const curve=(m0,c0,m1,c1)=>{const mid=(m0+m1)/2;return `C${toXY(mid,c0)} ${toXY(mid,c1)} ${toXY(m1,c1)}`;};
|
|
64
|
+
if(ac===bc){d=`M${toXY(am,ac)} L${toXY(bm,bc)}`;owner=c.branch;}
|
|
65
|
+
else if(firstOf.get(c.branch)===c.id&&k===0){d=`M${toXY(am,ac)} ${curve(am,ac,am+s*r,bc)} L${toXY(bm,bc)}`;owner=c.branch;}
|
|
66
|
+
else{d=`M${toXY(am,ac)} L${toXY(bm-s*r,ac)} ${curve(bm-s*r,ac,bm,bc)}`;owner=parent.branch;}
|
|
67
|
+
const path=el('path',{d,class:'git-edge','data-branch':owner});path.style.setProperty('--git-tone',tone(owner));edges.push({path,index:index.get(c.id)});}}
|
|
68
|
+
for(const c of data.commits){const p=point(c),i=index.get(c.id),t=tone(c.branch),kind=['normal','reverse','highlight','merge','cherry'][c.type]||'normal';let mark;
|
|
69
|
+
if(kind==='highlight')mark=el('rect',{x:p.x-6,y:p.y-6,width:12,height:12,rx:2,class:'git-dot','data-branch':c.branch});
|
|
70
|
+
else{mark=el('circle',{cx:p.x,cy:p.y,r:kind==='merge'?6:kind==='cherry'?6:5.5,class:'git-dot '+kind,'data-branch':c.branch});}
|
|
71
|
+
mark.style.setProperty('--git-tone',t);const marks=[mark];
|
|
72
|
+
if(kind==='reverse'){const x=el('path',{d:`M${p.x-3} ${p.y-3}l6 6m0 -6l-6 6`,class:'git-mark','data-branch':c.branch});x.style.setProperty('--git-tone','var(--background)');marks.push(x);}
|
|
73
|
+
const b=html('button','git-commit'+(vertical?'':' lr'));b.type='button';b.dataset.branch=c.branch;b.style.setProperty('--git-tone',t);
|
|
74
|
+
b.title=[c.branch,c.auto?c.id.split('-')[1]:c.id,c.message,...c.tags,c.description].filter(Boolean).join(' · ');b.setAttribute('aria-label',b.title);
|
|
75
|
+
const text=html('span','git-text'+(c.auto?' git-hash':''),label(c));const tags=[];
|
|
76
|
+
if(vertical){const textX=24+lanes*24+12;if(firstOf.get(c.branch)===c.id){const ref=html('span','git-ref',c.branch);b.append(ref);}b.append(text);if(c.message&&!c.auto)b.append(html('span','git-hash',c.message));c.tags.forEach(v=>{const g=html('span','git-tag',v);b.append(g);});b.style.left=textX+'px';b.style.top=(p.y-12)+'px';b.style.maxWidth=(width-textX-16)+'px';}
|
|
77
|
+
else{b.append(text);b.style.left=(p.x-(step-8)/2)+'px';b.style.width=(step-8)+'px';b.style.top=(p.y+10)+'px';c.tags.forEach((v,j)=>{const g=html('span','git-tag lr',v);g.title=v;g.dataset.branch=c.branch;g.style.maxWidth=Math.max(step-8,96)+'px';scene.append(g);g.style.left=(p.x-g.offsetWidth/2)+'px';g.style.top=(p.y-34-j*24)+'px';tags.push(g);});}
|
|
78
|
+
scene.append(b);commits.push({c,i,b,marks,tags});}
|
|
79
|
+
const view=createMermaidViewport(root);
|
|
80
|
+
// 悬停或聚焦某提交时只保留其所在分支,其余分支降为背景。
|
|
81
|
+
const all=()=>root.querySelectorAll('[data-branch]');function clear(){all().forEach(n=>n.removeAttribute('data-dim'));}
|
|
82
|
+
function focus(branch){all().forEach(n=>n.toggleAttribute('data-dim',n.dataset.branch!==branch));}
|
|
83
|
+
for(const o of commits){o.b.addEventListener('pointerenter',()=>focus(o.c.branch));o.b.addEventListener('pointerleave',clear);o.b.addEventListener('focus',()=>{focus(o.c.branch);const r=o.b.getBoundingClientRect(),v=viewport.getBoundingClientRect();if(r.left<v.left+12)viewport.scrollLeft-=v.left+12-r.left;else if(r.right>v.right-12)viewport.scrollLeft+=r.right-v.right+12;});o.b.addEventListener('blur',clear);}
|
|
84
|
+
for(const r of refs){r.addEventListener('pointerenter',()=>focus(r.dataset.branch));r.addEventListener('pointerleave',clear);r.addEventListener('focus',()=>focus(r.dataset.branch));r.addEventListener('blur',clear);}
|
|
85
|
+
viewport.addEventListener('scroll',clear,{passive:true});root.addEventListener('keydown',e=>{if(e.key==='Escape')clear();});
|
|
86
|
+
// 入场:按提交顺序画出连线并点亮提交,总时长约 1.2 秒;减少动态效果时不播放。
|
|
87
|
+
const reduced=matchMedia('(prefers-reduced-motion:reduce)');let running=[],armed=true;const gap=Math.min(120,1200/Math.max(1,n-1));
|
|
88
|
+
function replay(){running.forEach(a=>a.cancel());running=[];if(reduced.matches||view.expanded)return;
|
|
89
|
+
for(const {path,index:i} of edges){const length=path.getTotalLength();path.style.strokeDasharray=`${length} ${length}`;const a=path.animate([{strokeDashoffset:length},{strokeDashoffset:0}],{duration:360,delay:Math.max(0,i-1)*gap,easing:'cubic-bezier(.2,.7,.3,1)',fill:'backwards'});a.onfinish=()=>{path.style.strokeDasharray='';};running.push(a);}
|
|
90
|
+
for(const o of commits){const delay=o.i*gap;o.marks.forEach(m=>running.push(m.animate([{opacity:0},{opacity:1}],{duration:200,delay,fill:'backwards'})));[o.b,...o.tags].forEach(n=>running.push(n.animate([{opacity:0},{opacity:1}],{duration:200,delay:delay+60,fill:'backwards'})));}
|
|
91
|
+
root.dataset.replays=String(+(root.dataset.replays||0)+1);}
|
|
92
|
+
new IntersectionObserver(es=>{if(!es[0].isIntersecting)armed=true;else if(es[0].intersectionRatio>=.2&&armed){armed=false;replay();}},{threshold:[0,.2]}).observe(root);
|
|
93
|
+
document.addEventListener('visibilitychange',()=>{if(document.hidden)armed=true;else if(armed){armed=false;replay();}});
|
|
94
|
+
root.addEventListener('qv:display-mode',()=>{running.forEach(a=>a.cancel());clear();if(!view.expanded)viewport.scrollLeft=view.savedInlinePosition?.left||0;});reduced.addEventListener('change',()=>running.forEach(a=>a.cancel()));view.refresh();
|
|
95
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// 规则 32–33:渲染器显式接入,不修改宿主或 iframe 的全局原型。
|
|
2
|
+
const mermaidEntryEase='cubic-bezier(.23,1,.32,1)',mermaidEntryBudget=1200;
|
|
3
|
+
let mermaidEntryBatch=null;
|
|
4
|
+
function animateMermaidEntry(element,frames,options){
|
|
5
|
+
const timing=typeof options==='number'?{duration:options}:{...options};
|
|
6
|
+
timing.easing=mermaidEntryEase;
|
|
7
|
+
if(Array.isArray(frames))frames=frames.map(frame=>{
|
|
8
|
+
const match=typeof frame.transform==='string'&&frame.transform.match(/^scale\((0|0?\.\d+)\)$/);
|
|
9
|
+
if(!match||+match[1]>=.9)return frame;
|
|
10
|
+
return {...frame,transform:`scale(${+match[1]<.5?.92:.97})`,opacity:frame.opacity??(+match[1]<.5?0:1)};
|
|
11
|
+
});
|
|
12
|
+
const animation=element.animate(frames,timing);
|
|
13
|
+
if(!mermaidEntryBatch){mermaidEntryBatch=[];queueMicrotask(()=>{
|
|
14
|
+
const batch=mermaidEntryBatch;mermaidEntryBatch=null;
|
|
15
|
+
const end=Math.max(0,...batch.map(a=>{const t=a.effect.getTiming();return Number(t.delay)+Number(t.duration);}));
|
|
16
|
+
if(end>mermaidEntryBudget){const ratio=mermaidEntryBudget/end;for(const a of batch){const t=a.effect.getTiming();a.effect.updateTiming({delay:Number(t.delay)*ratio,duration:Number(t.duration)*ratio});}}
|
|
17
|
+
});}
|
|
18
|
+
mermaidEntryBatch.push(animation);return animation;
|
|
19
|
+
}
|
|
20
|
+
// rAF 与 D3 几何动画采用同一条曲线,二分求解 Bézier 的时间参数。
|
|
21
|
+
function mermaidEntryProgress(time){
|
|
22
|
+
const t=Math.max(0,Math.min(1,time));let low=0,high=1;
|
|
23
|
+
for(let i=0;i<16;i++){const u=(low+high)/2,x=3*(1-u)*(1-u)*u*.23+3*(1-u)*u*u*.32+u*u*u;if(x<t)low=u;else high=u;}
|
|
24
|
+
const u=(low+high)/2;return 1-(1-u)**3;
|
|
25
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#mcx-qv-root.qv-mermaid{--mcx-ease-out:cubic-bezier(.23,1,.32,1);--mcx-tip-shadow:0 4px 16px light-dark(color-mix(in srgb,var(--foreground) 9%,transparent),rgb(0 0 0/.5));--mcx-tip-surface:light-dark(var(--popover),color-mix(in srgb,var(--popover-foreground) 6%,var(--popover)))}
|
|
2
|
+
#mcx-qv-root.qv-mermaid :is(.node-tip,.seq-tip,.er-tip,.class-tip,.state-tip,.state-peers,.xy-tip,.sankey-detail,.radar-detail,.quadrant-detail,.architecture-detail,.bubble,.quick-radar-tooltip){background:var(--mcx-tip-surface);color:var(--popover-foreground);border:1px solid var(--border);border-radius:12px;padding:12px 14px;font-size:12px;line-height:18px;box-shadow:var(--mcx-tip-shadow);transition:opacity 140ms var(--mcx-ease-out),scale 140ms var(--mcx-ease-out)}
|
|
3
|
+
@starting-style{#mcx-qv-root.qv-mermaid :is(.node-tip,.seq-tip,.er-tip,.class-tip,.state-tip,.state-peers,.xy-tip,.sankey-detail,.radar-detail,.quadrant-detail,.architecture-detail,.bubble,.quick-radar-tooltip){opacity:0;scale:.97}}
|
|
4
|
+
#mcx-qv-root.qv-mermaid :is(.xy-tip-title,.sankey-detail-title,.radar-detail-title,.c4-tip-title,.state-tip-name,.bubble-title,.quick-radar-tooltip-dimension){font-size:14px;line-height:20px;font-weight:500}
|
|
5
|
+
#mcx-qv-root.qv-mermaid :is(.xy-tip-name,.radar-detail-name,.quick-radar-tooltip-name){color:var(--muted-foreground);font-weight:400}
|
|
6
|
+
#mcx-qv-root.qv-mermaid :is(.xy-tip-value,.radar-detail-value,.quick-radar-tooltip-value,.bubble-value){color:var(--popover-foreground);font-weight:500;font-variant-numeric:tabular-nums}
|
|
7
|
+
#mcx-qv-root.qv-mermaid :is(.state-tip-caption,.architecture-detail-label){font-size:12px;line-height:18px}
|
|
8
|
+
@media (prefers-reduced-motion:reduce){#mcx-qv-root.qv-mermaid :is(.node-tip,.seq-tip,.er-tip,.class-tip,.state-tip,.state-peers,.xy-tip,.sankey-detail,.radar-detail,.quadrant-detail,.architecture-detail,.bubble,.quick-radar-tooltip){transition:none}}
|
|
9
|
+
|
|
10
|
+
/* 规则 37:标题、正文与辅助信息保持稳定层级。 */
|
|
11
|
+
#mcx-qv-root.qv-mermaid h3{font-size:16px;line-height:24px}
|
|
12
|
+
#mcx-qv-root.qv-mermaid .state-node{font-size:14px;line-height:20px}
|
|
13
|
+
#mcx-qv-root.qv-mermaid .timeline-period{font-size:14px}
|
|
14
|
+
#mcx-qv-root.qv-mermaid .er-type{color:var(--muted-foreground)}
|
|
15
|
+
|
|
16
|
+
/* 内容画布在宽容器内居中,超宽时保留左侧可达的滚动起点。 */
|
|
17
|
+
#mcx-qv-root.qv-mermaid:is([data-renderer="qv-quadrant"],[data-renderer="qv-er"],[data-renderer="qv-class"]) .scene{margin-inline:auto}
|
|
18
|
+
/* Journey 的固定参考轴与步骤画布一起居中,溢出时只滚动步骤。 */
|
|
19
|
+
#mcx-qv-root.qv-mermaid[data-renderer="qv-journey"] .journey-frame{max-width:100%;margin-inline:auto}
|
|
20
|
+
#mcx-qv-root.qv-mermaid:is([data-renderer="qv-radar"],[data-renderer="qv-sankey"]) h3{margin-inline:0;text-align:center}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { load, JSON_SCHEMA } from 'js-yaml';
|
|
2
|
+
|
|
3
|
+
// Mermaid 11.12 的 getDiagramFromText 只转交显式 metadata,未转交预处理得到的标题。
|
|
4
|
+
// 使用相同的 YAML schema 与边界,避免另写只能识别单行标题的解析器。
|
|
5
|
+
export function readMermaidTitle(source) {
|
|
6
|
+
const match = source.replace(/\r\n?/g, '\n').match(/^-{3}\s*[\n\r](.*?)[\n\r]-{3}\s*[\n\r]+/s);
|
|
7
|
+
if (!match) return '';
|
|
8
|
+
const metadata = load(match[1], { schema: JSON_SCHEMA });
|
|
9
|
+
return metadata && typeof metadata === 'object' && !Array.isArray(metadata) && metadata.title
|
|
10
|
+
? String(metadata.title) : '';
|
|
11
|
+
}
|
|
@@ -2563,6 +2563,33 @@ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
|
|
|
2563
2563
|
THIS SOFTWARE.
|
|
2564
2564
|
|
|
2565
2565
|
|
|
2566
|
+
========================================================================
|
|
2567
|
+
js-yaml@4.1.1 — MIT
|
|
2568
|
+
|
|
2569
|
+
LICENSE
|
|
2570
|
+
(The MIT License)
|
|
2571
|
+
|
|
2572
|
+
Copyright (C) 2011-2015 by Vitaly Puzrin
|
|
2573
|
+
|
|
2574
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
2575
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
2576
|
+
in the Software without restriction, including without limitation the rights
|
|
2577
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
2578
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
2579
|
+
furnished to do so, subject to the following conditions:
|
|
2580
|
+
|
|
2581
|
+
The above copyright notice and this permission notice shall be included in
|
|
2582
|
+
all copies or substantial portions of the Software.
|
|
2583
|
+
|
|
2584
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
2585
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
2586
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
2587
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
2588
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
2589
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
2590
|
+
THE SOFTWARE.
|
|
2591
|
+
|
|
2592
|
+
|
|
2566
2593
|
========================================================================
|
|
2567
2594
|
katex@0.16.47 — MIT
|
|
2568
2595
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// 从已注册的原生模块与导出表解析角色,不执行模块工厂或依赖压缩名称。
|
|
2
|
+
export function loadRspackQuickChatBindings(registry, commandCatalog) {
|
|
3
|
+
const catalog=registry.module(source=>/\{id:[`"']temporaryChat[`"'],titleIntlId:/.test(source),'Quick Chat command catalog');
|
|
4
|
+
const native=commandCatalog(catalog.source);
|
|
5
|
+
if (native) return {native};
|
|
6
|
+
const entry=registry.module(source=>/has-opened-quick-chat-v1/.test(source)&&/chatgpt\.quick-chat/.test(source),'Quick Chat lifecycle');
|
|
7
|
+
const exported=local=>{
|
|
8
|
+
if (!local) throw new Error('quick-chat-lifecycle-contract-unavailable');
|
|
9
|
+
const escaped=local.replace(/[.*+?^${}()|[\]\\]/g,'\\$&');
|
|
10
|
+
const key=entry.source.match(new RegExp(`([\\w$]+):\\(\\)=>${escaped}(?=[,}])`))?.[1];
|
|
11
|
+
const value=key && registry.exports(entry)[key];
|
|
12
|
+
if (value==null) throw new Error('quick-chat-lifecycle-export-unavailable');
|
|
13
|
+
return value;
|
|
14
|
+
};
|
|
15
|
+
const open=registry.value(entry,(value,source)=>typeof value==='function'&&/chatgpt\.quick-chat/.test(source)&&/\.get\(([\w$]+)\)\|\|/.test(source),'Quick Chat open');
|
|
16
|
+
const source=String(open);
|
|
17
|
+
const create=exported(source.match(/null==[\w$]+\?([\w$]+)\([\w$]+,[\w$]+\):/)?.[1]);
|
|
18
|
+
const state=exported(source.match(/\.get\(([\w$]+)\)\|\|/)?.[1]);
|
|
19
|
+
const bindings={native,open,create,isOpen:scope=>scope.get(state)};
|
|
20
|
+
if (typeof create!=='function') throw new Error('quick-chat-lifecycle-contract-unavailable');
|
|
21
|
+
try {
|
|
22
|
+
const discard=registry.value(entry,(value,source)=>typeof value==='function'&&/draftKey:/.test(source)&&/content:null/.test(source)&&/conversationId,""/.test(source),'Quick Chat discard');
|
|
23
|
+
const actions=String(discard).match(/let ([\w$]+)=([\w$]+)\.get\(([\w$]+)\);([\w$]+)\(\2\),\2\.set\(\3,null\),null!=\1&&\(\(0,([\w$]+\.[\w$]+)\)\(\2,\{draftKey:\1\.conversationId,content:null\}\),\(0,([\w$]+\.[\w$]+)\)\(\2,\1\.conversationId,""\),\2\.set\(([\w$]+\.[\w$]+),\1\.conversationId,\[\]\)/);
|
|
24
|
+
if (!actions) throw new Error('quick-chat-close-contract-unavailable');
|
|
25
|
+
const session=exported(actions[3]),close=exported(actions[4]);
|
|
26
|
+
const draft=registry.binding(entry,actions[5]),text=registry.binding(entry,actions[6]),attachments=registry.binding(entry,actions[7]);
|
|
27
|
+
if ([close,draft,text].some(value=>typeof value!=='function')) throw new Error('quick-chat-close-contract-unavailable');
|
|
28
|
+
bindings.close=(scope,id)=>{
|
|
29
|
+
if (scope.get(session)?.conversationId!==id) return false;
|
|
30
|
+
draft(scope,{draftKey:id,content:null});text(scope,id,'');scope.set(attachments,id,[]);close(scope);return true;
|
|
31
|
+
};
|
|
32
|
+
} catch { /* 关闭协议独立失效时仍保留打开能力。 */ }
|
|
33
|
+
try {
|
|
34
|
+
const modeEntry=registry.module(source=>/conversationDetailMode/.test(source)&&/"work":"codex"/.test(source),'Quick Chat product mode');
|
|
35
|
+
const modes=Object.values(registry.exports(modeEntry));
|
|
36
|
+
if (modes.length===1) bindings.mode=scope=>scope.get(modes[0]);
|
|
37
|
+
} catch { /* 无模式 selector 时只使用可见模式选择器。 */ }
|
|
38
|
+
return bindings;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function loadRspackQuickChatIcon(registry) {
|
|
42
|
+
const entry=registry.module(source=>/"data-floating-chat-surface":/.test(source)&&/minimizedIcon:/.test(source),'Quick Chat floating surface');
|
|
43
|
+
const local=entry.source.match(/minimizedIcon:([\w$]+)/)?.[1];
|
|
44
|
+
const escaped=local?.replace(/[.*+?^${}()|[\]\\]/g,'\\$&');
|
|
45
|
+
const reference=escaped&&entry.source.match(new RegExp(`children:${escaped}\\?\\?\\(0,[\\w$]+\\.jsx\\)\\(([\\w$]+\\.[\\w$]+),\\{\\}\\)`))?.[1];
|
|
46
|
+
if (!reference) throw new Error('quick-chat-icon-contract-unavailable');
|
|
47
|
+
const icon=registry.binding(entry,reference);
|
|
48
|
+
if (typeof icon?.createElement!=='function') throw new Error('quick-chat-icon-contract-unavailable');
|
|
49
|
+
return icon;
|
|
50
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {loadRspackQuickChatBindings} from "./rspack.mjs";
|
|
2
|
+
import { findCodexModule, codexModuleRegistry } from '../../../../../packages/codex-adapter/src/renderer/modules.mjs';
|
|
2
3
|
import {quickChatLifecycleActions, quickChatScope} from "./lifecycle.mjs";
|
|
3
4
|
// 通过原生能力检查与已有会话分支定位打开动作,不依赖构建哈希或压缩名称。
|
|
4
5
|
export function quickChatOpenExport(source) {
|
|
@@ -99,6 +100,12 @@ export function installQuickChatShortcut(signal, codex, discover = null) {
|
|
|
99
100
|
}
|
|
100
101
|
async function load() {
|
|
101
102
|
codex.report('quick-chat-shortcut', false, 'quick-chat-module-pending');
|
|
103
|
+
const registry=await codexModuleRegistry({signal:loading.signal,waitForEntry:true,read:async url=>(await fetch(url,{signal:loading.signal})).text()});
|
|
104
|
+
if (registry) {
|
|
105
|
+
const bindings=loadRspackQuickChatBindings(registry,quickChatNativeCommand);
|
|
106
|
+
if (!bindings.native && !bindings.close) codex.report('quick-chat-close',false,'quick-chat-close-contract-unavailable');
|
|
107
|
+
return bindings;
|
|
108
|
+
}
|
|
102
109
|
const url=await findCodexModule('app-initial',{signal:loading.signal,waitForEntry:true});
|
|
103
110
|
if (!url) throw new Error('quick-chat-module-unavailable');
|
|
104
111
|
const source = await (await fetch(url, {signal:loading.signal})).text();
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {loadRspackQuickChatIcon} from "./rspack.mjs";
|
|
2
|
+
import { findCodexModule, codexModuleRegistry } from '../../../../../packages/codex-adapter/src/renderer/modules.mjs';
|
|
2
3
|
export async function loadQuickChatSidebarIcon(signal) {
|
|
4
|
+
signal.throwIfAborted();
|
|
5
|
+
const registry=await codexModuleRegistry({signal,waitForEntry:true,read:async url=>(await fetch(url,{signal})).text()});
|
|
6
|
+
if (registry) return loadRspackQuickChatIcon(registry);
|
|
3
7
|
const initialUrl = await findCodexModule('app-initial',{signal});
|
|
4
8
|
if (!initialUrl) throw new Error('quick-chat-icon-module-unavailable');
|
|
5
9
|
const initial = await (await fetch(initialUrl,{signal})).text();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {loadRspackQuickChatBindings,loadRspackQuickChatIcon} from "./adapters/codex/rspack.mjs";
|
|
2
|
+
import { findCodexModule, codexModuleRegistry } from '../../../packages/codex-adapter/src/renderer/modules.mjs';
|
|
2
3
|
import {quickChatLifecycleActions, quickChatScope} from "./adapters/codex/lifecycle.mjs";
|
|
3
4
|
import {quickChatHistoryVisible} from "./adapters/codex/history.mjs";
|
|
4
5
|
import { readFile } from "node:fs/promises";
|
|
@@ -10,7 +11,7 @@ import { installQuickChatSidebar } from "./frontend/sidebar.mjs";
|
|
|
10
11
|
import { createHistoryTracker } from "./domain/history.mjs";
|
|
11
12
|
export async function createPageScriptSource() {
|
|
12
13
|
const source = await readFile(new URL("./frontend/entry.mjs", import.meta.url), "utf8");
|
|
13
|
-
const functions = [findCodexModule, quickChatHistoryVisible, loadQuickChatSidebarIcon, createQuickChatSidebarAdapter, installQuickChatSidebar,
|
|
14
|
+
const functions = [findCodexModule, codexModuleRegistry, loadRspackQuickChatBindings, loadRspackQuickChatIcon, quickChatHistoryVisible, loadQuickChatSidebarIcon, createQuickChatSidebarAdapter, installQuickChatSidebar,
|
|
14
15
|
quickChatLifecycleActions, quickChatScope, quickChatOpenExport, quickChatLifecycleExports, quickChatNativeCommand, installQuickChatShortcut, createQuickChatAdapter,
|
|
15
16
|
createSubmissionTracker, createHistoryTracker];
|
|
16
17
|
return functions.map(fn => `const ${fn.name}=${fn.toString()};`).join('\n') + '\n' + source;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "minecodex",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.54",
|
|
4
4
|
"description": "Lightweight, local-first plugins for the Codex desktop app.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -142,6 +142,7 @@
|
|
|
142
142
|
"elkjs": "0.10.0",
|
|
143
143
|
"esbuild": "^0.28.2",
|
|
144
144
|
"html-to-image": "1.11.13",
|
|
145
|
+
"js-yaml": "4.1.1",
|
|
145
146
|
"lucide": "0.468.0",
|
|
146
147
|
"mermaid": "11.12.0"
|
|
147
148
|
}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
// Markdown 共用原生角色;只沿已注册模块与原生 lazy 声明绑定依赖。
|
|
2
2
|
export async function loadRspackMarkdownToc(registry) {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
}
|
|
3
|
+
// 工厂已注册不等于依赖已初始化;并行加载的阅读与图表增强共用原生 import 完成点。
|
|
4
|
+
const page=registry.module(source=>source.includes(').PageEditorContent'),'Pages content loader');
|
|
5
|
+
await registry.lazy(page,'PageEditorContent');
|
|
7
6
|
const owner=registry.module(source=>source.includes('data-heading-preview')&&source.includes('page-table-of-contents'),'Pages table of contents');
|
|
8
7
|
const body=marker=>{
|
|
9
8
|
const at=owner.source.indexOf(marker),start=owner.source.lastIndexOf('function ',at);
|
|
@@ -66,62 +66,80 @@ export function codexModuleStyles(source, ownerUrl, moduleUrl) {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
// rspack 的入口公开原生 require;只读取已注册模块,不重新执行模块工厂。
|
|
69
|
-
export async function codexModuleRegistry({read = async url => (await fetch(url)).text(), load = url => import(url)} = {}) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
69
|
+
export async function codexModuleRegistry({read = async url => (await fetch(url)).text(), load = url => import(url), signal, waitForEntry = false} = {}) {
|
|
70
|
+
const scripts=()=>[...document.querySelectorAll('script[src]')];
|
|
71
|
+
const visited=new Set();
|
|
72
|
+
while (true) {
|
|
73
|
+
signal?.throwIfAborted();
|
|
74
|
+
for (const script of scripts()) {
|
|
75
|
+
signal?.throwIfAborted();
|
|
76
|
+
if (visited.has(script.src)) continue;
|
|
77
|
+
visited.add(script.src);
|
|
78
|
+
const url = script.src;
|
|
79
|
+
if (!url) continue;
|
|
80
|
+
const source = await read(url);
|
|
81
|
+
if (!/export\s*\{[^}]*\bas\s+__webpack_require__\b/.test(source)) continue;
|
|
82
|
+
const runtime = (await load(url)).__webpack_require__;
|
|
83
|
+
if (typeof runtime !== 'function' || !runtime.m || !runtime.c) throw new Error('native-module-registry-unavailable');
|
|
84
|
+
const sourceOf = value => {
|
|
85
|
+
try { return Function.prototype.toString.call(value); } catch { return ''; }
|
|
86
|
+
};
|
|
87
|
+
const entries = Object.entries(runtime.m).map(([id, factory]) => ({id, source:sourceOf(factory)}));
|
|
88
|
+
const unique = (values, role) => {
|
|
89
|
+
if (values.length !== 1) throw new Error(`Native module contract changed: ${role}`);
|
|
90
|
+
return values[0];
|
|
91
|
+
};
|
|
92
|
+
const resolvedExports = new Map();
|
|
93
|
+
const exportsOf = entry => {
|
|
94
|
+
if (resolvedExports.has(entry.id)) return resolvedExports.get(entry.id);
|
|
95
|
+
const cached = runtime.c[entry.id];
|
|
96
|
+
if (!cached || cached.error) throw new Error('native-module-not-loaded');
|
|
97
|
+
return cached.exports;
|
|
98
|
+
};
|
|
99
|
+
const module = (predicate, role) => unique(entries.filter(entry => predicate(entry.source, entry)), role);
|
|
100
|
+
const value = (entry, predicate, role) => unique(Object.values(exportsOf(entry)).filter(item => predicate(item, sourceOf(item))), role);
|
|
101
|
+
const binding = (entry, reference) => {
|
|
102
|
+
const [owner, ...properties] = reference.split('.');
|
|
103
|
+
const escaped = owner.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
104
|
+
const id = entry.source.match(new RegExp(`(?:^|[^\\w$])${escaped}=[\\w$]+\\("([^"\\n]+)"\\)`))?.[1];
|
|
105
|
+
if (!id) throw new Error('native-module-binding-unavailable');
|
|
106
|
+
let result = exportsOf({id});
|
|
107
|
+
for (const property of properties) result = result?.[property];
|
|
108
|
+
if (result == null) throw new Error('native-module-export-unavailable');
|
|
109
|
+
return result;
|
|
110
|
+
};
|
|
111
|
+
const lazy = async (entry, name) => {
|
|
112
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
113
|
+
const match = entry.source.match(new RegExp(`[\\w$]+\\.e\\((\\d+)\\)\\.then\\([\\w$]+\\.bind\\([\\w$]+,"([^"\\n]+)"\\)\\)\\.then\\([\\w$]+=>\\(\\{default:[\\w$]+\\.${escaped}\\}\\)\\)`));
|
|
114
|
+
let id;
|
|
115
|
+
if (match) {
|
|
116
|
+
await runtime.e(Number(match[1]));
|
|
117
|
+
id=match[2];
|
|
118
|
+
} else {
|
|
119
|
+
// Pages 与 KaTeX 使用具名异步导入;依赖列表仍完全来自原生声明。
|
|
120
|
+
const group=entry.source.match(new RegExp(`Promise\\.all\\(\\[([^\\]]+)\\]\\)\\.then\\([\\w$]+\\.bind\\([\\w$]+,"([^"\\n]+)"\\)\\)(?:\\)\\.${escaped}\\b|\\.then\\(\\(\\{${escaped}(?=[:,}]))`));
|
|
121
|
+
if(!group || !/^(?:[\w$]+\.e\(\d+\),?)+$/.test(group[1])) throw new Error('native-module-lazy-contract-unavailable');
|
|
122
|
+
await Promise.all([...group[1].matchAll(/\.e\((\d+)\)/g)].map(item=>runtime.e(Number(item[1]))));
|
|
123
|
+
id=group[2];
|
|
124
|
+
}
|
|
125
|
+
const exports = await runtime(id);
|
|
126
|
+
if (exports[name] == null) throw new Error('native-module-lazy-export-unavailable');
|
|
127
|
+
resolvedExports.set(id,exports);
|
|
128
|
+
entries.splice(0,entries.length,...Object.entries(runtime.m).map(([id,factory])=>({id,source:sourceOf(factory)})));
|
|
129
|
+
return {id, source:sourceOf(runtime.m[id])};
|
|
130
|
+
};
|
|
131
|
+
return {entries, module, value, binding, lazy, exports:exportsOf, sourceOf};
|
|
132
|
+
}
|
|
133
|
+
if (!waitForEntry || scripts().some(script=>/\/(?:index|app-(?:main|initial))-[^/]+\.js(?:$|\?)/.test(script.src))) return null;
|
|
134
|
+
// HTML 仍在逐个挂载入口脚本时,首个 chunk 不一定拥有注册表。
|
|
135
|
+
await new Promise((resolve,reject)=>{
|
|
136
|
+
const cleanup=()=>{observer.disconnect();signal?.removeEventListener('abort',abort);};
|
|
137
|
+
const abort=()=>{cleanup();reject(signal.reason);};
|
|
138
|
+
const check=()=>{if(scripts().some(script=>!visited.has(script.src))){cleanup();resolve();}};
|
|
139
|
+
const observer=new MutationObserver(check);
|
|
140
|
+
observer.observe(document.documentElement,{childList:true,subtree:true,attributes:true,attributeFilter:['src']});
|
|
141
|
+
signal?.addEventListener('abort',abort,{once:true});
|
|
142
|
+
if(signal?.aborted)abort();else check();
|
|
143
|
+
});
|
|
125
144
|
}
|
|
126
|
-
return null;
|
|
127
145
|
}
|