minecodex 2.0.5 → 2.0.7

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.
Files changed (85) hide show
  1. package/DESIGN.md +19 -0
  2. package/README.md +5 -1
  3. package/apps/dashboard/DESIGN.md +53 -31
  4. package/apps/dashboard/app.js +55 -39
  5. package/apps/dashboard/assets/OFL.txt +93 -0
  6. package/apps/dashboard/assets/info.svg +15 -0
  7. package/apps/dashboard/assets/moon.svg +13 -0
  8. package/apps/dashboard/assets/refresh-cw.svg +16 -0
  9. package/apps/dashboard/assets/silkscreen.ttf +0 -0
  10. package/apps/dashboard/assets/sun.svg +21 -0
  11. package/apps/dashboard/assets/terrain.js +89 -0
  12. package/apps/dashboard/assets/triangle-alert.svg +15 -0
  13. package/apps/dashboard/favicon.svg +12 -17
  14. package/apps/dashboard/index.html +24 -18
  15. package/apps/dashboard/styles.css +147 -181
  16. package/assets/brand/README.md +19 -0
  17. package/assets/brand/minecodex-icon-dark.svg +9 -0
  18. package/assets/brand/minecodex-icon-light.svg +9 -0
  19. package/assets/brand/minecodex-icon.svg +8 -0
  20. package/assets/brand/references/lucide-LICENSE.txt +43 -0
  21. package/assets/readme/hero.svg +93 -0
  22. package/features/hide-upsell-banner/README.md +7 -5
  23. package/features/hide-upsell-banner/codex-feature.json +2 -2
  24. package/features/hide-upsell-banner/src/hide-upsell-banner.mjs +30 -12
  25. package/features/images/DESIGN.md +18 -3
  26. package/features/images/README.md +4 -3
  27. package/features/images/codex-feature.json +2 -2
  28. package/features/images/src/adapters/codex/native-image-modules.mjs +220 -0
  29. package/features/images/src/adapters/codex/native-image-source.mjs +113 -198
  30. package/features/images/src/assets/prompts-menu.svg +15 -0
  31. package/features/images/src/frontend/cloud-image-actions.mjs +8 -6
  32. package/features/images/src/frontend/local-image-source.mjs +2 -1
  33. package/features/images/src/library-prompt-page.mjs +10 -4
  34. package/features/images/src/library-prompt-source.mjs +4 -5
  35. package/features/model-slider/codex-feature.json +2 -2
  36. package/features/notes/DESIGN.md +1 -1
  37. package/features/notes/codex-feature.json +2 -2
  38. package/features/notes/src/frontend/entry.mjs +1 -1
  39. package/features/session-status/codex-feature.json +2 -2
  40. package/features/sidebar-focus/DESIGN.md +4 -4
  41. package/features/sidebar-focus/codex-feature.json +3 -2
  42. package/features/sidebar-focus/src/adapters/codex/renderer.mjs +1 -1
  43. package/features/sidebar-focus/src/sidebar-focus.mjs +9 -7
  44. package/features/sites/DESIGN.md +25 -3
  45. package/features/sites/README.md +6 -2
  46. package/features/sites/codex-feature.json +3 -2
  47. package/features/sites/src/adapters/codex/renderer.mjs +25 -4
  48. package/features/sites/src/backend/project-sites.mjs +18 -6
  49. package/features/sites/src/backend/sites-service.mjs +22 -10
  50. package/features/sites/src/domain/discovery-candidates.mjs +53 -0
  51. package/features/sites/src/frontend/entry.mjs +76 -38
  52. package/features/sites/src/frontend/styles.css +3 -3
  53. package/features/unread-inbox/DESIGN.md +24 -25
  54. package/features/unread-inbox/codex-feature.json +3 -2
  55. package/features/unread-inbox/src/adapters/codex/modules.mjs +52 -0
  56. package/features/unread-inbox/src/adapters/codex/renderer.mjs +90 -0
  57. package/features/unread-inbox/src/domain/notifications.mjs +21 -0
  58. package/features/unread-inbox/src/injection-source.mjs +5 -2
  59. package/features/unread-inbox/src/unread-inbox.mjs +212 -297
  60. package/features/usage-meter/codex-feature.json +2 -2
  61. package/package.json +11 -3
  62. package/packages/cli/native/MineCodex.app/Contents/Info.plist +4 -2
  63. package/packages/cli/native/MineCodex.app/Contents/MacOS/MineCodexServiceManager +0 -0
  64. package/packages/cli/native/MineCodex.app/Contents/Resources/MineCodex.icns +0 -0
  65. package/packages/cli/native/MineCodex.app/Contents/_CodeSignature/CodeResources +11 -0
  66. package/packages/cli/native/README.md +31 -0
  67. package/packages/cli/src/browser-control-panel.mjs +48 -0
  68. package/packages/cli/src/commands.mjs +3 -1
  69. package/packages/cli/src/control-server.mjs +6 -3
  70. package/packages/cli/src/platform.mjs +5 -0
  71. package/packages/runtime-host/README.md +10 -5
  72. package/packages/runtime-host/src/codex-injection.mjs +12 -3
  73. package/packages/runtime-host/src/codex-runtime.mjs +10 -0
  74. package/packages/ui/DESIGN.md +5 -1
  75. package/packages/ui/assets/arrow-up-right.svg +13 -0
  76. package/packages/ui/assets/lucide-LICENSE.txt +43 -0
  77. package/packages/ui/assets/minecodex-menu-16.svg +7 -0
  78. package/packages/ui/assets/minecodex-menu-20.svg +7 -0
  79. package/packages/ui/src/components/account-menu.mjs +88 -0
  80. package/packages/ui/src/components/page.mjs +2 -2
  81. package/packages/ui/src/foundation/layers.mjs +1 -0
  82. package/packages/ui/src/install.mjs +3 -0
  83. package/packages/ui/src/native/account-menu.mjs +21 -0
  84. package/packages/ui/src/native/page.mjs +17 -2
  85. package/packages/ui/src/native/tokens.mjs +1 -0
@@ -1,152 +1,15 @@
1
- // 原生组件、构建产物与 React 上下文只在此适配;不复制图库或编辑器。
2
- export function createNativeImageSource(codex, { read = async url => (await fetch(url)).text(), load = url => import(url), requestHostAction, libraryApi, createCloudImageActions } = {}) {
3
- let connecting;
1
+ import {createNativeImageModules} from "./native-image-modules.mjs";
2
+
3
+ // 原生呈现与上下文只在此适配;构建产物发现由专用模块提供。
4
+ export function createNativeImageSource(codex, { requestHostAction, libraryApi, createCloudImageActions, promptGlyphSource, signal, modules = createNativeImageModules() } = {}) {
5
+ const connect = modules.gallery;
4
6
  const opened = new Map();
5
7
 
6
- function imported(source, local) {
7
- for (const match of source.matchAll(/import\{([^}]+)\}from["']([^"']+)["']/g)) {
8
- for (const binding of match[1].split(",")) {
9
- const parts = binding.trim().split(/\s+as\s+/);
10
- if (parts.at(-1) === local) return { name: parts[0], file: match[2] };
11
- }
12
- }
13
- throw new Error(`Native Images import contract changed: ${local}`);
14
- }
15
-
16
- async function connect() {
17
- if (!connecting) connecting = (async () => {
18
- const urls = [...document.querySelectorAll('link[rel="modulepreload"],script[src]')].map(node => node.href || node.src);
19
- const initialUrl = urls.find(url => /\/app-initial-[^/]+\.js/.test(url));
20
- const mainUrl = urls.find(url => /\/app-main-[^/]+\.js/.test(url));
21
- if (!initialUrl || !mainUrl) throw new Error("Native Images modules are unavailable");
22
- const [initialSource, mainSource] = await Promise.all([read(initialUrl), read(mainUrl)]);
23
- const asset = (name, references = initialSource) => {
24
- const file = references.match(new RegExp(`(?:["'./])(${name}-[\\w-]+\\.js)`))?.[1];
25
- if (!file) throw new Error(`Native Images capability is unavailable: ${name}`);
26
- return new URL(file, initialUrl).href;
27
- };
28
- const module = async (name,references) => {
29
- const url = asset(name,references);
30
- const [source, exports] = await Promise.all([read(url), load(url)]);
31
- // 调用产物公开的惰性初始化器,不扫描或执行任意导出函数。
32
- for (const value of Object.values(exports)) {
33
- if (typeof value === "function" && /^function ([\w$]+)\(\)\{return\(\1=/.test(String(value))) value();
34
- }
35
- return {url, source, exports};
36
- };
37
- const [masonry, layout, sourceImage, menu, selection, initial] = await Promise.all([
38
- module("library-masonry"), module("library-item-layout"), module("generated-image-source"),
39
- module("generated-image-context-menu"), module("library-selection-toolbar"), load(initialUrl),
40
- ]);
41
- const [moveDialog,dialogs] = await Promise.all([module("library-move-dialog",selection.source),module("library-item-dialogs",selection.source)]);
42
- const [canvas,canvasLayout,filePreview] = await Promise.all([
43
- module("image-side-panel"),module("preview-layout"),module("library-cloud-file-preview"),
44
- ]);
45
- const component = (exports, predicate, name) => {
46
- const matches = Object.values(exports).filter(value => typeof value === "function" && predicate(String(value)));
47
- if (matches.length !== 1) throw new Error(`Native Images contract changed: ${name}`);
48
- return matches[0];
49
- };
50
- const binding = async (source, url, local) => {
51
- if (!local) throw new Error("Native Images component contract changed");
52
- const importedBinding = imported(source, local);
53
- return (await load(new URL(importedBinding.file, url).href))[importedBinding.name];
54
- };
55
- const routerLocal = selection.source.match(/get\(([\w$]+)\)\.prepareNavigation\(/)?.[1];
56
- const routerAtom = await binding(selection.source, selection.url, routerLocal);
57
- const switchLocal = selection.source.match(/([\w$]+)\(e,i\);let _=t.length===1/)?.[1];
58
- const switchMode = await binding(selection.source, selection.url, switchLocal);
59
- const draftLocal = selection.source.match(/let u=([\w$]+)\(e,\[\]\);e.set/)?.[1];
60
- const createChatDraft = await binding(selection.source, selection.url, draftLocal);
61
- const rootOwner = mainSource.match(/\(0,([\w$]+)\.createRoot\)/)?.[1];
62
- const rootInit = rootOwner && mainSource.match(new RegExp(`${rootOwner}=([\\w$]+)\\(\\)`))?.[1];
63
- const reactOwner = masonry.source.match(/([\w$]+)\.useRef\b/)?.[1];
64
- const reactInit = reactOwner && masonry.source.match(new RegExp(`${reactOwner}=([\\w$]+)\\(\\)`))?.[1];
65
- const [reactFactory, rootFactory] = await Promise.all([
66
- binding(masonry.source, masonry.url, reactInit), binding(mainSource, mainUrl, rootInit),
67
- ]);
68
- const React = reactFactory(), ReactDOM = rootFactory();
69
- const portalOwner=canvas.source.match(/\(0,([\w$]+)\.createPortal\)/)?.[1];
70
- const portalInit=portalOwner&&canvas.source.match(new RegExp(`${portalOwner}=([\\w$]+)\\(\\)`))?.[1];
71
- const createPortal=(await binding(canvas.source,canvas.url,portalInit))().createPortal;
72
- const dropdownLocal = selection.source.match(/\(0,[\w$]+\.jsxs?\)\(([\w$]+),\{contentWidth:`menu`/)?.[1];
73
- const itemsLocal = selection.source.match(/\(0,[\w$]+\.jsxs?\)\(([\w$]+)\.Item,\{/)?.[1];
74
- const buttonLocal = selection.source.match(/\(0,[\w$]+\.jsx\)\(([\w$]+),\{ref:[^,]+,color:`mediaOverlay`/)?.[1];
75
- const iconLocal = selection.source.match(/color:`mediaOverlay`[^;]+?children:\(0,[\w$]+\.jsx\)\(([\w$]+),\{\}\)/)?.[1];
76
- const [Dropdown, Menu, Button, MoreIcon] = await Promise.all([dropdownLocal, itemsLocal, buttonLocal, iconLocal].map(local => binding(selection.source, selection.url, local)));
77
- const checkboxFunction = component(selection.exports,source=>/onCheckedChange:/.test(source)&&/viewMode:/.test(source),"selection checkbox");
78
- const checkboxLocal = String(checkboxFunction).match(/\(0,[\w$]+\.jsx\)\(([\w$]+),\{className:`cursor-interaction`,variant:/)?.[1];
79
- const Checkbox = await binding(selection.source,selection.url,checkboxLocal);
80
- const rangeSelection = component(selection.exports,source=>/^function [\w$]+\(\{orderedIds:/.test(source)&&/extendRange:/.test(source),"range selection");
81
- const selectionLocal = selection.source.match(/\.set\(([\w$]+),new Set\)/)?.[1];
82
- const selectionExport = selection.source.match(/export\{([^}]+)\}/)?.[1].split(",").map(value=>value.trim().split(/\s+as\s+/)).find(parts=>parts[0]===selectionLocal)?.at(-1);
83
- const nativeSelectedIds = selection.exports[selectionExport];
84
- if (!nativeSelectedIds) throw new Error("Native selection state contract changed");
85
- const clearPrefix = selection.source.slice(selection.source.indexOf("id:`libraryNext.selection.clear`"));
86
- const clearLocal = clearPrefix.match(/\(0,[\w$]+\.jsx\)\(([\w$]+),\{\}\)/)?.[1];
87
- const ClearIcon = await binding(selection.source,selection.url,clearLocal);
88
- const dialogLocal = dialogs.source.match(/\(0,[\w$]+\.jsx\)\(([\w$]+),\{open:[^,]+,showDialogClose:!1/)?.[1];
89
- const contentLocal = dialogs.source.match(/\(0,[\w$]+\.jsx\)\(([\w$]+),\{as:`form`,onSubmit:/)?.[1];
90
- const [Dialog,DialogContent] = await Promise.all([dialogLocal,contentLocal].map(local=>binding(dialogs.source,dialogs.url,local)));
91
- const menuCoreFile = menu.source.match(/from["']([^"']*generated-image-context-menu-[^"']+)["']/)?.[1];
92
- const menuCoreUrl = menuCoreFile ? new URL(menuCoreFile,menu.url).href : menu.url;
93
- const menuCoreSource = menuCoreFile ? await read(menuCoreUrl) : menu.source;
94
- const imageActions = component(menu.exports,source=>/onOpenedInNewTab:/.test(source),"image actions");
95
- const imageActionSource = String(imageActions);
96
- const imageFileLocal = imageActionSource.match(/let\{file:[\w$]+\}=([\w$]+)\(/)?.[1];
97
- const attachLocal = imageActionSource.match(/await ([\w$]+)\(e,i,\[n\],/)?.[1];
98
- const downloadLocal = imageActionSource.match(/await ([\w$]+)\(\{downloadSrc:/)?.[1];
99
- const [imageFile,attachChatImages,downloadImage] = await Promise.all([imageFileLocal,attachLocal,downloadLocal].map(local=>binding(menuCoreSource,menuCoreUrl,local)));
100
- const toolbarClass = selection.source.match(/className:`(pointer-events-auto flex h-toolbar[^`]+)`/)?.[1];
101
- const toolbarPositionClass = selection.source.match(/className:`(pointer-events-none fixed inset-x-4 bottom-6[^`]+)`/)?.[1];
102
- if (!toolbarClass || !toolbarPositionClass) throw new Error("Native selection toolbar layout changed");
103
- const libraryIcon = async label => {
104
- const marker = selection.source.indexOf(`defaultMessage:\`${label}\``);
105
- const prefix = selection.source.slice(Math.max(0, marker - 500), marker);
106
- const match = [...prefix.matchAll(/leftIcon:\(0,[\w$]+\.jsx\)\(([\w$]+),\{icon:\{(assets:\{)?16:([\w$]+),20:([\w$]+)/g)].at(-1);
107
- if (!match) throw new Error(`Native Library icon contract changed: ${label}`);
108
- const [Icon, small, medium] = await Promise.all([match[1], match[3], match[4]].map(local => binding(selection.source, selection.url, local)));
109
- const sizes = {16:small, 20:medium};
110
- return React.createElement(Icon, {icon:match[2] ? {assets:sizes} : sizes});
111
- };
112
- const upload = component(initial, source => /async function/.test(source) && /libraryPersistenceMode:/.test(source) && /uploadPurpose:/.test(source), "Library upload");
113
- const setFavorite = component(initial, source => /target_ref:/.test(source) && /showToast:/.test(source), "Library favorite");
114
- const auth = String(setFavorite).match(/accountId:e.get\(([\w$]+)\),userId:e.get\(([\w$]+)\)/);
115
- const [accountAtom,userAtom] = await Promise.all([auth?.[1],auth?.[2]].map(local => binding(initialSource,initialUrl,local)));
116
- if (!accountAtom || !userAtom) throw new Error("Native Library account contract changed");
117
- const toastLocal = selection.source.match(/get\(([\w$]+)\)\.danger\(/)?.[1];
118
- const toastAtom = await binding(selection.source, selection.url, toastLocal);
119
- const favoritePrefix = selection.source.slice(Math.max(0,selection.source.indexOf("id:`library.favorites.add`")-2000),selection.source.indexOf("id:`library.favorites.add`")+500);
120
- const favoriteAsset = favoritePrefix.match(/\{16:([\w$]+),20:([\w$]+)\}:\{16:([\w$]+),20:([\w$]+)\}/);
121
- if (!favoriteAsset) throw new Error("Native Library favorite icon contract changed");
122
- const favoriteIconLocal = favoritePrefix.match(/icon:\{assets:/)?.index;
123
- const favoriteIconOwner = favoritePrefix.slice(0,favoriteIconLocal).match(/\(0,[\w$]+\.jsx\)\(([\w$]+),\{$/)?.[1];
124
- const [FavoriteIcon, savedSmall, savedMedium, emptySmall, emptyMedium] = await Promise.all([favoriteIconOwner,...favoriteAsset.slice(1)].map(local => binding(selection.source,selection.url,local)));
125
- const favoriteIcon = saved => React.createElement(FavoriteIcon,{icon:{assets:saved?{16:savedSmall,20:savedMedium}:{16:emptySmall,20:emptyMedium}}});
126
- const [chatIcon, downloadIcon, folderIcon, deleteIcon] = await Promise.all([
127
- libraryIcon("Chat about this"), libraryIcon("Download"), libraryIcon("Add to folder"), libraryIcon("{kind, select, directory {Delete folder} other {Delete}}"),
128
- ]);
129
- // 文件画布使用原生 Home Composer 的控制器、附件 scope 和输入组件。
130
- const composerLocals=["kr","Qn","$n","rr","ke","hn","tn","P"];
131
- const [useComposerController,useHomeComposer,Composer,ComposerSurface,ScopeProvider,composerScope,createComposerValue,composerRouteScope] = await Promise.all(composerLocals.map(local=>binding(filePreview.source,filePreview.url,local)));
132
- if (!React.createElement || !ReactDOM.createRoot || !Menu.Item) throw new Error("Native Images rendering contract changed");
133
- return {
134
- React, ReactDOM, createPortal, Dropdown, Menu, Button, MoreIcon, chatIcon, downloadIcon, folderIcon, favoriteIcon, routerAtom, switchMode, createChatDraft, upload, setFavorite, toastAtom, accountAtom, userAtom,
135
- MoveDialog:moveDialog.exports.MoveLibraryItemsDialog,
136
- Checkbox,rangeSelection,nativeSelectedIds,ClearIcon,Dialog,DialogContent,ConfirmationContent:dialogs.exports.LibraryItemConfirmationContent,
137
- Canvas:canvas.exports.t,CanvasLayout:canvasLayout.exports.r,
138
- useComposerController,useHomeComposer,Composer,ComposerSurface,ScopeProvider,composerScope,createComposerValue,composerRouteScope,
139
- editImage:component(initial,source=>/async function/.test(source)&&/requireDispatchAcceptance:/.test(source)&&/sourceAttachment:/.test(source)&&/imageVariant/.test(source),"image edit submission"),
140
- imageFile,attachChatImages,downloadImage,toolbarClass,toolbarPositionClass,deleteIcon,
141
- Masonry: component(masonry.exports, source => /\{items:[^}]+animate:/.test(source), "masonry"),
142
- Skeleton: component(masonry.exports, source => /square:/.test(source) && /Loading library/.test(source), "skeleton"),
143
- Card: component(layout.exports, source => /actionIndicator:/.test(source) && /documentHeader:/.test(source), "card"),
144
- useImage: component(sourceImage.exports, source => /shouldLoadFileDataUrl:/.test(source), "image resolver"),
145
- imageMenu: component(menu.exports, source => /onOpenedInNewTab:/.test(source), "image actions"),
146
- openImage: component(initial, source => /availableImageCount:/.test(source) && /openInEditor:/.test(source) && /entrypoint:/.test(source), "image preview"),
147
- };
148
- })().catch(error => { connecting = null; throw error; });
149
- return connecting;
8
+ function uploadFile({base64,name}) {
9
+ const decoded=atob(base64),bytes=new Uint8Array(decoded.length);
10
+ // 直接写入字节,避免大图经字符串迭代器创建中间数组。
11
+ for(let index=0;index<decoded.length;index++)bytes[index]=decoded.charCodeAt(index);
12
+ return new File([bytes],name,{type:"image/png"});
150
13
  }
151
14
 
152
15
  function environment(anchor) {
@@ -189,7 +52,7 @@ export function createNativeImageSource(codex, { read = async url => (await fetc
189
52
  }
190
53
 
191
54
  async function mountNavigation(container, anchor, onSelect) {
192
- const native = await connect();
55
+ const native = await modules.navigation();
193
56
  const context = environment(anchor);
194
57
  const root = native.ReactDOM.createRoot(container);
195
58
  return {
@@ -200,7 +63,6 @@ export function createNativeImageSource(codex, { read = async url => (await fetc
200
63
  selectedKey,
201
64
  tabs:[...contract.props.tabs, {key:"imagegen", name:"Image Gen"}],
202
65
  onSelect:key => {
203
- if (key === "imagegen") context.libraryScope?.set(native.nativeSelectedIds,new Set());
204
66
  onSelect(key);
205
67
  if (key !== "imagegen") navigationContract(anchor).props.onSelect(key);
206
68
  },
@@ -216,20 +78,21 @@ export function createNativeImageSource(codex, { read = async url => (await fetc
216
78
  }
217
79
 
218
80
  async function mount(container, anchor, {onOpen, onPrompt, onRetry, onHidden}) {
219
- const native = await connect();
81
+ let native = await connect();
220
82
  const context = environment(anchor);
221
83
  const {React, ReactDOM} = native;
222
84
  const h = React.createElement;
85
+ const promptIcon=h("span",{className:"inline-flex size-4 items-center justify-center","aria-hidden":true,dangerouslySetInnerHTML:{__html:promptGlyphSource}});
223
86
  let failure, currentContent, folderDialog, removeDialog, canvasImage, disposed=false, batchBusy=false;
224
- let currentImages=[],currentLoading=true,selectedIds=new Set(),selectionAnchor=null;
87
+ let currentImages=[],currentLoading=true,selectedIds=new Set(),selectionAnchor=null,openRevision=0;
225
88
  const scope = context.libraryScope;
226
89
  if (!scope) throw new Error("Native Library context is unavailable");
227
90
  const {accountId,userId} = scope.value;
228
91
  const message = (id,defaultMessage) => context.intl.formatMessage({id,defaultMessage});
229
92
  const toast = () => scope.get(native.toastAtom);
230
- const assertAccount = () => {
231
- if (disposed || scope.get(native.accountAtom) !== accountId || scope.get(native.userAtom) !== userId) throw new Error("Library account changed");
232
- };
93
+ const sameAccount = () => scope.get(native.accountAtom) === accountId && scope.get(native.userAtom) === userId;
94
+ const isCurrent = () => !disposed && sameAccount();
95
+ const assertAccount = () => {if (!isCurrent()) throw new Error("Library action is no longer current");};
233
96
  const reference = (image,operation,extra={}) => requestHostAction("local-image-source",{operation,id:image.id,accountId,userId,...extra});
234
97
  async function favoriteStatus(id) {
235
98
  const result = await libraryApi.favoriteStatus(id);
@@ -239,13 +102,12 @@ export function createNativeImageSource(codex, { read = async url => (await fetc
239
102
  }
240
103
  const cloud = createCloudImageActions({
241
104
  reference, assertAccount,
242
- fileNode:id => libraryApi.fileNode(id), favoriteStatus,
105
+ favoriteStatus,
243
106
  setFavorite:(libraryFileId,favoriteId,isFavorited) => native.setFavorite(scope,{libraryFileId,favoriteId,isFavorited,surface:"library_grid",showToast:true}),
244
107
  upload:async file => {
245
- const bytes = Uint8Array.from(atob(file.base64),value => value.charCodeAt(0));
246
108
  const pendingToast = toast().info(message("minecodex.images.saving","Saving image to Library…"),{duration:0,hasCloseButton:false});
247
109
  try {
248
- const uploaded = await native.upload(scope,new File([bytes],file.name,{type:"image/png"}),{
110
+ const uploaded = await native.upload(scope,uploadFile(file),{
249
111
  libraryPersistenceMode:"required",storeInLibrary:true,uploadPurpose:"library",beforeRequest:assertAccount,
250
112
  });
251
113
  return uploaded.libraryFileId;
@@ -255,13 +117,21 @@ export function createNativeImageSource(codex, { read = async url => (await fetc
255
117
  async function folder(image) {
256
118
  const images=Array.isArray(image)?image:[image];
257
119
  const registered=[];
258
- for(const item of images)registered.push(await cloud.registered(item));
259
- const entries=registered.map(item=>item.entry),created=registered.some(item=>item.created);
120
+ const entries=[];
121
+ for (const item of images) {
122
+ const result=await cloud.registered(item);
123
+ const node=await libraryApi.fileNode(result.libraryFileId);
124
+ assertAccount();
125
+ registered.push(result);
126
+ entries.push({node});
127
+ }
128
+ const created=registered.some(item=>item.created);
260
129
  let moved = false;
261
130
  folderDialog = h(native.MoveDialog,{
262
131
  accountId,userId,entries,initialPath:[],
263
132
  onMoved:() => {moved=true;},
264
133
  onClose:() => {
134
+ if (!isCurrent()) return;
265
135
  folderDialog=null;render(currentContent);
266
136
  if (!moved) toast().info(created
267
137
  ? message("minecodex.images.folderCanceledSaved","Image saved to Library, but not added to a folder.")
@@ -271,7 +141,7 @@ export function createNativeImageSource(codex, { read = async url => (await fetc
271
141
  render(currentContent);
272
142
  }
273
143
  function operationError(error) {
274
- if (disposed) return;
144
+ if (!isCurrent()) return;
275
145
  toast().danger(error.code === "UPLOAD_UNCONFIRMED"
276
146
  ? message("minecodex.images.uploadUnconfirmed","Image upload is pending or could not be confirmed. Check Library before uploading again.")
277
147
  : message("minecodex.images.libraryActionFailed","Couldn’t complete the Library action. Please try again."));
@@ -279,6 +149,7 @@ export function createNativeImageSource(codex, { read = async url => (await fetc
279
149
  }
280
150
  const root = ReactDOM.createRoot(container, {onUncaughtError:error => {failure=error;onRetry(error);}});
281
151
  function render(content) {
152
+ if (!isCurrent()) return;
282
153
  currentContent = content;
283
154
  let tree = h(React.Fragment,null,content,folderDialog,removeDialog,selectionToolbar(),canvasImage?h(ImageCanvas,{key:canvasImage.id,image:canvasImage}):null);
284
155
  for (const provider of context.providers) tree = h(provider.type, {value:provider.value}, tree);
@@ -293,7 +164,7 @@ export function createNativeImageSource(codex, { read = async url => (await fetc
293
164
  selectedIds=next.selectedIds;selectionAnchor=next.anchor;refresh();
294
165
  }
295
166
  const selectedImages = () => currentImages.filter(image=>selectedIds.has(image.id));
296
- function clearSelection() {selectedIds=new Set();selectionAnchor=null;removeDialog=null;refresh();}
167
+ function clearSelection() {openRevision++;selectedIds=new Set();selectionAnchor=null;removeDialog=null;refresh();}
297
168
  async function performBatch(action) {
298
169
  if (batchBusy) return;
299
170
  batchBusy=true;refresh();
@@ -307,13 +178,13 @@ export function createNativeImageSource(codex, { read = async url => (await fetc
307
178
  h(native.DialogContent,{as:"form",onSubmit:event=>{
308
179
  event.preventDefault();if(pending)return;pending=true;update();refresh();
309
180
  void requestHostAction("local-image-source",{operation:"hide",ids:images.map(image=>image.id)}).then(result=>{
310
- if(disposed)return;
181
+ if(!isCurrent())return;
311
182
  const hidden=new Set(result.hiddenIds);
312
183
  currentImages=currentImages.filter(image=>!hidden.has(image.id));
313
184
  selectedIds=new Set([...selectedIds].filter(id=>!hidden.has(id)));
314
185
  pending=false;removeDialog=null;onHidden?.(result.hiddenIds);refresh();
315
186
  toast().info(message("minecodex.images.hidden","Hidden from Image Gen"));
316
- }).catch(error=>{pending=false;update();refresh();operationError(error);});
187
+ }).catch(error=>{if(!isCurrent())return;pending=false;update();refresh();operationError(error);});
317
188
  }},h(native.ConfirmationContent,{
318
189
  title:context.intl.formatMessage({id:"minecodex.images.hideTitle",defaultMessage:"Delete {count, plural, one {image} other {# images}} from Image Gen?"},{count:images.length}),
319
190
  description:message("minecodex.images.hideDescription","These images will only be hidden from Image Gen. Local originals and cloud copies will be kept."),
@@ -347,11 +218,13 @@ export function createNativeImageSource(codex, { read = async url => (await fetc
347
218
  }
348
219
  document.addEventListener("keydown",onSelectionKey);
349
220
  async function startChat(images) {
221
+ assertAccount();
350
222
  const conversationId=native.createChatDraft(context.scope,[]);
351
223
  native.switchMode(context.scope,"chat");
352
224
  const files=[];
353
- for(const image of images){const file=await reference(image,"upload-file");files.push(native.imageFile(`data:image/png;base64,${file.base64}`,file.name).file);}
225
+ for(const image of images){const file=await reference(image,"upload-file");assertAccount();files.push(native.imageFile(`data:image/png;base64,${file.base64}`,file.name).file);}
354
226
  if (!await native.attachChatImages(context.scope,conversationId,files,{isTemporaryChat:false})) throw new Error("Could not attach selected images");
227
+ assertAccount();
355
228
  const navigate=context.scope.get(native.routerAtom).prepareNavigation("/",{state:{codexAppMode:"chat",initialChatGptConversationId:conversationId,focusComposerNonce:Date.now()}});
356
229
  if(!navigate)throw new Error("Native Chat navigation is unavailable");
357
230
  clearSelection();await navigate();
@@ -359,9 +232,19 @@ export function createNativeImageSource(codex, { read = async url => (await fetc
359
232
  function localImage(image) {
360
233
  return {id:image.imageGenerationItemId, src:image.sourcePath, previewSrc:image.sourcePath, alt:image.name, tabTitle:image.name, width:image.width, height:image.height};
361
234
  }
362
- function open(image, options) {
235
+ async function open(image, options) {
236
+ if (!options) {
237
+ assertAccount();
238
+ const revision=++openRevision;
239
+ const canvas=await modules.canvas();
240
+ // 切换来源或重置列表后,不让晚到的画布覆盖当前页面。
241
+ if(revision!==openRevision || !isCurrent())return false;
242
+ native={...native,...canvas};
243
+ opened.set(image.sourcePath,image);
244
+ canvasImage=image;refresh();return true;
245
+ }
246
+ if (signal?.aborted || !sameAccount()) return;
363
247
  opened.set(image.sourcePath, image);
364
- if (!options) {canvasImage=image;refresh();return;}
365
248
  const {readOnly=false,scope=context.scope,chatGptConversationId}=options;
366
249
  const result = native.openImage(scope, {
367
250
  chatGptConversationId, alt:image.name, title:image.name, attachmentId:`image-playground:${image.imageGenerationItemId}`,
@@ -369,7 +252,7 @@ export function createNativeImageSource(codex, { read = async url => (await fetc
369
252
  imageId:image.imageGenerationItemId, sourceConversationId:image.threadId,
370
253
  imageSourceContext:{conversationId:image.threadId, hostId:"local"}, imageSource:"generated",
371
254
  readOnly, openInEditor:true,
372
- renderHeaderMenuActions:({renderMenu}) => renderMenu(h(native.Menu.Item, {onSelect:() => onPrompt(image), children:context.intl.formatMessage({id:"minecodex.images.viewPrompts",defaultMessage:"View prompts"})})),
255
+ renderHeaderMenuActions:({renderMenu}) => renderMenu(h(native.Menu.Item, {leftIcon:promptIcon,onSelect:() => onPrompt(image), children:context.intl.formatMessage({id:"minecodex.images.viewPrompts",defaultMessage:"View prompts"})})),
373
256
  }, {addToHistory:false});
374
257
  if (!result) throw new Error("Native image preview could not open");
375
258
  }
@@ -389,15 +272,28 @@ export function createNativeImageSource(codex, { read = async url => (await fetc
389
272
  h(native.ScopeProvider,{scope:native.composerRouteScope,value:{pathname:"/",routeKind:"home",routeTemplate:"/",search:"",projectContext:null}},
390
273
  h(native.ScopeProvider,{scope:native.composerScope,value:composerValue},h(CanvasSurface,{image,resolved,close}))));
391
274
  // 原生文件预览占据主内容 viewport,保留导航、二级侧栏和窗口栏。
392
- return native.createPortal(h("div",{className:"absolute inset-0 z-40 overflow-hidden", "data-minecodex-image-canvas":image.id,children:content}),viewport);
275
+ return native.createPortal(h("div",{className:"absolute inset-0 z-40 overflow-hidden", "data-minecodex-image-canvas":image.id,children:[
276
+ // 画布在主内容内,原生窗口级 overlay 的交通灯避让不适用于此处。
277
+ h("style",{children:'[data-minecodex-image-canvas] .shrink-0:has(>[data-testid="viewer-header"]){padding-left:0!important;padding-right:0!important}'}),content,
278
+ ]}),viewport);
393
279
  }
394
280
  function CanvasSurface({image,resolved,close}) {
395
281
  const controller=native.useComposerController();
282
+ const navigate=native.useFileNavigate();
396
283
  const home=native.useHomeComposer({homeComposerController:controller,homeComposerMode:"chat"});
397
284
  const [abort]=React.useState(()=>new AbortController());
398
285
  const [markup,setMarkup]=React.useState(false);
399
286
  const [manualZoomPercent,setManualZoomPercent]=React.useState(null);
400
287
  const [submitting,setSubmitting]=React.useState(false);
288
+ const [composerHeight,setComposerHeight]=React.useState(0);
289
+ const measureComposer=React.useCallback(node=>{
290
+ const element=node?.querySelector("[data-codex-composer-root]");
291
+ if(!element)return;
292
+ const measure=()=>setComposerHeight(element.getBoundingClientRect().height);
293
+ const observer=new ResizeObserver(measure);
294
+ observer.observe(element);measure();
295
+ return ()=>{observer.disconnect();setComposerHeight(0);};
296
+ },[]);
401
297
  const editorRef=React.useRef(null);
402
298
  const attachment=React.useRef(null);
403
299
  const submissionAccepted=React.useRef(false);
@@ -408,8 +304,7 @@ export function createNativeImageSource(codex, { read = async url => (await fetc
408
304
  assertCurrent();
409
305
  if(!attachment.current)attachment.current=(async()=>{
410
306
  const file=await reference(image,"upload-file");assertCurrent();
411
- const bytes=Uint8Array.from(atob(file.base64),value=>value.charCodeAt(0));
412
- const uploaded=await native.upload(context.scope,new File([bytes],file.name,{type:"image/png"}),{storeInLibrary:false,beforeRequest:assertCurrent,signal:abort.signal});
307
+ const uploaded=await native.upload(context.scope,uploadFile(file),{storeInLibrary:false,beforeRequest:assertCurrent,signal:abort.signal});
413
308
  return {...uploaded,width:image.width,height:image.height,imageReference:true};
414
309
  })().catch(error=>{attachment.current=null;throw error;});
415
310
  return attachment.current;
@@ -438,31 +333,47 @@ export function createNativeImageSource(codex, { read = async url => (await fetc
438
333
  }catch(error){if(!abort.signal.aborted)operationError(error);return false;}
439
334
  finally {if(!abort.signal.aborted)setSubmitting(false);}
440
335
  };
441
- const composer=h(native.ComposerSurface,{placement:{kind:"home"}},h(native.Composer,{
442
- ...props,composerController:controller,composerLayoutMode:"auto-single-line",lockedLayout:"single-line",
443
- className:"mx-auto w-full max-w-2xl px-toolbar",errorClassName:"p-3",showFilePicker:false,signal:abort.signal,
444
- placeholder:message("libraryFilePreview.composerPlaceholder","Ask about this file"),isPrimaryComposer:false,
445
- disableSubmit:props.disableSubmit||submitting,
446
- attachmentPreparation:{mimeTypes:["image/png"],prepare:async signal=>{signal?.throwIfAborted();return [await prepare()];}},
447
- onSubmitAccepted:accepted,
448
- }));
336
+ const composer=h("div",{ref:measureComposer,className:"contents"},h(native.ComposerSurface,{placement:{kind:"home"}},h(native.FileComposer,{
337
+ composerController:controller,composerLayoutMode:"auto-single-line",
338
+ className:"mx-auto w-[min(48rem,calc(100vw-2rem))] max-w-full",
339
+ composerModeAvailability:native.fileComposerAvailability,
340
+ hideRunLocationDropdownOverride:true,showUtilityBar:false,surfaceVariant:"inline",
341
+ placeholderText:message("libraryFilePreview.composerPlaceholder","Ask about this file"),
342
+ submitDisabled:submitting,selectedProject:null,
343
+ // 原图仅在发送时成为本地附件,不伪造 Library 身份,也不上传云端。
344
+ prepareLocalConversationAttachments:async ({fileAttachments,hostId})=>{
345
+ assertCurrent();
346
+ const file=await reference(image,"upload-file");assertCurrent();
347
+ const attached=await native.localAttachmentService(context.scope,hostId).createFileAttachment({contents:uploadFile(file),label:file.name});
348
+ if(abort.signal.aborted||!isCurrent()){
349
+ await native.deleteLocalAttachment(context.scope,hostId,attached.path);
350
+ assertCurrent();
351
+ }
352
+ return [...fileAttachments,attached];
353
+ },
354
+ onLocalFirstTurnAccepted:()=>{submissionAccepted.current=true;},
355
+ onLocalConversationCreated:(conversationId,state)=>{submissionAccepted.current=true;close();navigate(native.localThreadRoute(conversationId),{state});},
356
+ })));
449
357
  const dataUrl=resolved.dataUrl??resolved.previewSrc;
450
358
  const canvas=h(native.Canvas,{
451
359
  alt:image.name,title:image.name,src:dataUrl??image.sourcePath,downloadSrc:dataUrl??image.sourcePath,downloadFilename:image.name,
452
360
  attachmentId:`image-playground:${image.imageGenerationItemId}`,attachmentSrc:image.sourcePath,
453
361
  chatGptConversationId:props.conversationId,
454
362
  imageSource:"generated",imageSourceContext:{conversationId:image.threadId,hostId:"local"},
455
- readOnly:false,presentation:"dialog",standalone:true,showViewerHeader:true,onClose:requestClose,
363
+ readOnly:false,presentation:"overlay",standalone:true,showViewerHeader:true,onClose:requestClose,
456
364
  markupMode:{active:markup,onActiveChange:setMarkup,editorRef,isSubmitting:submitting,standaloneComposer:true},
457
365
  onImageEditSubmit:submitEdit,
458
366
  zoom:{manualZoomPercent,onZoomPercentChange:setManualZoomPercent,onZoomToFit:()=>setManualZoomPercent(null)},
459
367
  renderComposer:visible=>h(React.Activity,{mode:visible?"visible":"hidden",children:composer}),
460
- renderHeaderMenuActions:({renderMenu})=>renderMenu(h(native.Menu.Item,{onSelect:()=>onPrompt(image),children:message("minecodex.images.viewPrompts","View prompts")})),
368
+ renderHeaderMenuActions:({renderMenu})=>renderMenu(h(native.Menu.Item,{leftIcon:promptIcon,onSelect:()=>onPrompt(image),children:message("minecodex.images.viewPrompts","View prompts")})),
461
369
  });
462
- // 原生 overlay 负责底部留白、宽度、避让与 Comment 时的输入框显隐。
463
- return h(native.CanvasLayout,{assetName:image.name,contentOwnsHeader:true,onClose:requestClose,children:canvas});
370
+ // 原生 overlay 的 118px 下限属于旧输入框;画布和拖拽边界跟随实际高度。
371
+ const reserve=composerHeight>0?`calc(${composerHeight}px + var(--spacing)*4)`:"0px";
372
+ return h("div",{className:"contents",style:{"--right-panel-composer-overlay-reserve":reserve}},
373
+ h(native.CanvasLayout,{assetName:image.name,contentOwnsHeader:true,onClose:requestClose,children:canvas}));
464
374
  }
465
375
  async function chatAbout(image) {
376
+ assertAccount();
466
377
  const conversationId = native.createChatDraft(context.scope, []);
467
378
  native.switchMode(context.scope, "chat");
468
379
  const navigate = context.scope.get(native.routerAtom).prepareNavigation("/", {
@@ -471,24 +382,31 @@ export function createNativeImageSource(codex, { read = async url => (await fetc
471
382
  if (!navigate) throw new Error("Native Chat navigation is unavailable");
472
383
  await navigate();
473
384
  await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
474
- const composer = document.querySelector('[data-codex-composer="true"]') ?? document.querySelector('[contenteditable="true"]');
385
+ // 导航会正常卸载图库;只把面板移交给目标 Chat 的可见输入框。
386
+ if (signal?.aborted || !sameAccount()) return;
387
+ const composer = codex.composer(null,{purpose:"insertion"})?.editable;
475
388
  if (!composer) throw new Error("Native Chat composer is unavailable");
476
389
  const chat = environment(composer);
477
- open(image, {readOnly:true, scope:chat.scope, chatGptConversationId:conversationId});
390
+ await open(image, {readOnly:true, scope:chat.scope, chatGptConversationId:conversationId});
478
391
  }
479
392
  function ImageCard({image}) {
480
393
  const [saved,setSaved] = React.useState(false);
481
394
  const [busy,setBusy] = React.useState(false);
395
+ const active=React.useRef(true);
396
+ const favoriteRevision=React.useRef(0);
397
+ React.useEffect(()=>{active.current=true;return()=>{active.current=false;favoriteRevision.current++;};},[]);
482
398
  async function perform(action) {
483
399
  if (busy) return;
484
400
  setBusy(true);
485
- try {await action();} catch(error) {operationError(error);} finally {setBusy(false);}
401
+ try {assertAccount();await action();} catch(error) {operationError(error);} finally {if(active.current&&isCurrent())setBusy(false);}
486
402
  }
487
403
  async function refreshFavorite(open) {
404
+ const revision=++favoriteRevision.current;
488
405
  if (!open) return;
489
406
  try {
490
407
  const cached = await reference(image,"reference");
491
- setSaved(cached.libraryFileId ? !!(await favoriteStatus(cached.libraryFileId)) : false);
408
+ const value=cached.libraryFileId ? !!(await favoriteStatus(cached.libraryFileId)) : false;
409
+ if (active.current && isCurrent() && revision===favoriteRevision.current) setSaved(value);
492
410
  } catch(error) {console.error("MineCodex favorite status",error);}
493
411
  }
494
412
  const resolved = native.useImage({src:image.sourcePath, conversationId:image.threadId, hostId:"local", shouldLoadFileDataUrl:true});
@@ -500,22 +418,19 @@ export function createNativeImageSource(codex, { read = async url => (await fetc
500
418
  const item = byId.get(id);
501
419
  return item && {...item, icon, text:context.intl.formatMessage(message ?? item.message)};
502
420
  };
503
- const promptGlyph = document.querySelector('[aria-label="Prompts"] svg');
504
- const menuGlyph = promptGlyph?.cloneNode(true);
505
- if (menuGlyph) {
506
- menuGlyph.setAttribute("width","16");menuGlyph.setAttribute("height","16");
507
- menuGlyph.style.width="16px";menuGlyph.style.height="16px";
508
- }
509
- const promptIcon = menuGlyph ? h("span", {className:"inline-flex size-4 items-center justify-center", "aria-hidden":true, dangerouslySetInnerHTML:{__html:menuGlyph.outerHTML}}) : null;
510
421
  const menuItems = [
511
422
  {id:"chat-about", icon:native.chatIcon, text:context.intl.formatMessage({id:"libraryNext.actions.chatAboutItem",defaultMessage:"Chat about this"}),onSelect:() => {void chatAbout(image).catch(onRetry);}},
423
+ {id:"view-prompts", icon:promptIcon, text:context.intl.formatMessage({id:"minecodex.images.viewPrompts",defaultMessage:"View prompts"}),onSelect:() => {void open(image).then(opened=>{if(opened&&isCurrent())onPrompt(image);}).catch(operationError);}},
512
424
  {id:"favorite",icon:native.favoriteIcon(saved),text:saved
513
425
  ? message("library.favorites.remove","Remove from Favorites")
514
- : message("library.favorites.add","Add to Favorites"),onSelect:() => {void perform(async () => setSaved(await cloud.favorite(image)));}},
426
+ : message("library.favorites.add","Add to Favorites"),onSelect:() => {void perform(async () => {
427
+ favoriteRevision.current++;
428
+ const value=await cloud.favorite(image);
429
+ if(active.current&&isCurrent()){favoriteRevision.current++;setSaved(value);}
430
+ });}},
515
431
  action("download-a-copy", native.downloadIcon, {id:"libraryNext.actions.download", defaultMessage:"Download"}),
516
432
  {id:"folder",icon:native.folderIcon,text:message("libraryNext.actions.addToFolder","Add to folder"),onSelect:() => {void perform(() => folder(image));}},
517
433
  {id:"delete",icon:native.deleteIcon,tone:"danger",text:message("libraryNext.actions.delete","Delete"),onSelect:()=>confirmRemove([image])},
518
- {id:"view-prompts", icon:promptIcon, text:context.intl.formatMessage({id:"minecodex.images.viewPrompts",defaultMessage:"View prompts"}),onSelect:() => {open(image);onPrompt(image);}},
519
434
  ].filter(Boolean);
520
435
  const dropdown = h(native.Dropdown, {
521
436
  contentWidth:"menu", align:"end", onOpenChange:refreshFavorite,
@@ -534,17 +449,17 @@ export function createNativeImageSource(codex, { read = async url => (await fetc
534
449
  },children:h(native.Checkbox,{className:"cursor-interaction",variant:"overlay",checked,disabled:batchBusy,"aria-label":context.intl.formatMessage({id:"libraryNext.selection.item",defaultMessage:"Select {name}"},{name:image.name}),onCheckedChange:value=>select(image,value===true)})});
535
450
  return h(native.Card,{name:image.name,selection,selected:checked,selectionActive:selectedIds.size>0,onSelect:event=>{
536
451
  if(selectedIds.size || event.shiftKey){select(image,!checked,event.shiftKey);return true;}return false;
537
- },onOpen:() => onOpen(image,open),actions:dropdown,children:h("span",{className:"relative flex min-h-0 w-full flex-1 items-center justify-center",children:h("span",{className:"relative flex w-full items-center justify-center",style:{aspectRatio:cardRatio},children:picture})})});
452
+ },onOpen:() => {void Promise.resolve(onOpen(image,open)).catch(operationError);},actions:dropdown,children:h("span",{className:"relative flex min-h-0 w-full flex-1 items-center justify-center",children:h("span",{className:"relative flex w-full items-center justify-center",style:{aspectRatio:cardRatio},children:picture})})});
538
453
  }
539
454
  return {
540
455
  render({images,loading}) {
541
456
  if (failure) throw failure;
542
457
  currentImages=images;currentLoading=loading;
543
- if(loading){selectedIds=new Set();selectionAnchor=null;removeDialog=null;}
458
+ if(loading){openRevision++;selectedIds=new Set();selectionAnchor=null;removeDialog=null;scope.set(native.nativeSelectedIds,new Set());}
544
459
  refresh();
545
460
  },
546
461
  clearSelection,
547
- dispose() {disposed=true;document.removeEventListener("keydown",onSelectionKey);root.unmount();},
462
+ dispose() {disposed=true;openRevision++;document.removeEventListener("keydown",onSelectionKey);root.unmount();},
548
463
  };
549
464
  }
550
465
 
@@ -0,0 +1,15 @@
1
+ <!-- Derived from Lucide book-open (ISC): merged stroke subpaths for uniform alpha. -->
2
+ <svg
3
+ class="icon-xs shrink-0"
4
+ xmlns="http://www.w3.org/2000/svg"
5
+ width="16"
6
+ height="16"
7
+ viewBox="-1.5 -1.5 27 27"
8
+ fill="none"
9
+ stroke="currentColor"
10
+ stroke-width="1.8"
11
+ stroke-linecap="round"
12
+ stroke-linejoin="round"
13
+ >
14
+ <path d="M12 7v14 M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z" />
15
+ </svg>
@@ -1,12 +1,13 @@
1
1
  // 工作流只管理云端身份与操作顺序;上传协议和原生组件由 Adapter 提供。
2
- export function createCloudImageActions({reference,upload,fileNode,favoriteStatus,setFavorite,assertAccount}) {
2
+ export function createCloudImageActions({reference,upload,favoriteStatus,setFavorite,assertAccount}) {
3
3
  const pending = new Map();
4
4
  function registered(image) {
5
5
  if (!pending.has(image.id)) {
6
6
  const task = (async () => {
7
7
  assertAccount();
8
8
  const cached = await reference(image,"reference");
9
- if (cached.libraryFileId) return {entry:{node:await fileNode(cached.libraryFileId)},created:false};
9
+ assertAccount();
10
+ if (cached.libraryFileId) return {libraryFileId:cached.libraryFileId,created:false};
10
11
  const unconfirmed = () => Object.assign(new Error("Library upload is pending or could not be confirmed"),{code:"UPLOAD_UNCONFIRMED"});
11
12
  if (cached.status === "uploading") throw unconfirmed();
12
13
  const file = await reference(image,"upload-file");
@@ -18,7 +19,7 @@ export function createCloudImageActions({reference,upload,fileNode,favoriteStatu
18
19
  // 先保存身份;收藏失败或取消文件夹选择后仍可复用上传结果。
19
20
  await reference(image,"register",{libraryFileId,fingerprint:file.fingerprint});
20
21
  assertAccount();
21
- return {entry:{node:await fileNode(libraryFileId)},created:true};
22
+ return {libraryFileId,created:true};
22
23
  })();
23
24
  pending.set(image.id,task);
24
25
  const release = () => {if (pending.get(image.id) === task) pending.delete(image.id);};
@@ -29,10 +30,11 @@ export function createCloudImageActions({reference,upload,fileNode,favoriteStatu
29
30
  return {
30
31
  registered,
31
32
  async favorite(image) {
32
- const {entry} = await registered(image);
33
- const favoriteId = await favoriteStatus(entry.node.id);
33
+ const {libraryFileId} = await registered(image);
34
+ const favoriteId = await favoriteStatus(libraryFileId);
35
+ assertAccount();
36
+ if (!await setFavorite(libraryFileId,favoriteId,!favoriteId)) throw new Error("Library favorite could not be changed");
34
37
  assertAccount();
35
- if (!await setFavorite(entry.node.id,favoriteId,!favoriteId)) throw new Error("Library favorite could not be changed");
36
38
  return !favoriteId;
37
39
  },
38
40
  };
@@ -183,7 +183,8 @@ export function installLocalImageSource(config, signal, sources, native) {
183
183
  });
184
184
  observer.observe(document.body, {childList:true, subtree:true, attributes:true, attributeFilter:["aria-selected"]});
185
185
  void scan();
186
- const cleanup = () => {observer.disconnect(); unmount(); pendingPrompt = null;};
186
+ let cleaned = false;
187
+ const cleanup = () => {if(cleaned)return;cleaned=true;observer.disconnect();unmount();pendingPrompt=null;};
187
188
  signal.addEventListener("abort", cleanup, {once:true});
188
189
  return {
189
190
  cleanup,