codex-overleaf-link 2.3.0 → 2.3.2
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 +23 -23
- package/extension/bootstrap/manifest.template.json +1 -1
- package/extension/src/background.js +20 -0
- package/extension/src/content/assetTransferBroker.js +169 -0
- package/extension/src/content/composerPanel.js +0 -2
- package/extension/src/content/contentRuntime.js +135 -111
- package/extension/src/content/generated/content.bundle.js +1066 -345
- package/extension/src/content/generated/content.bundle.meta.json +7 -3
- package/extension/src/content/legacyGlobalRegistry.js +3 -0
- package/extension/src/content/markdownText.js +12 -10
- package/extension/src/content/mathText.js +155 -9
- package/extension/src/content/modelPicker.js +2 -2
- package/extension/src/content/modelPickerSupport.js +3 -3
- package/extension/src/content/nativeCompatibilityController.js +2 -2
- package/extension/src/content/otWarmMirror.js +1 -1
- package/extension/src/content/pageBridgeClient.js +1 -0
- package/extension/src/content/runResultActions.js +114 -0
- package/extension/src/content/runTimelineView.js +23 -0
- package/extension/src/content/sessionForkController.js +120 -0
- package/extension/src/content/writebackController.js +3 -0
- package/extension/src/content/writebackOrchestrator.js +5 -50
- package/extension/src/page/binaryAssetUploader.js +343 -0
- package/extension/src/page/trackedChangesLifecycle.js +12 -4
- package/extension/src/page/writebackRouter.js +6 -2
- package/extension/src/pageBridge.js +30 -1
- package/extension/src/shared/agentTranscript.js +10 -3
- package/extension/src/shared/compatibility.js +4 -2
- package/extension/src/shared/i18n.js +18 -4
- package/extension/src/shared/models.js +13 -2
- package/extension/src/shared/pageRpcContract.js +6 -2
- package/extension/src/shared/runExecutionSnapshot.js +14 -4
- package/extension/src/shared/runExecutionSnapshotCodec.js +5 -2
- package/extension/src/shared/scopedPersistenceQueuePolicy.js +20 -1
- package/extension/src/shared/scopedPersistenceReceipt.js +60 -0
- package/extension/src/shared/scopedPersistenceTransaction.js +50 -47
- package/extension/src/shared/sessionState.js +58 -15
- package/extension/src/shared/storageDb.js +6 -5
- package/extension/src/shared/storageRunActions.js +11 -1
- package/extension/src/shared/writebackOperationEvidence.js +16 -6
- package/extension/src/shared/writebackSettlement.js +28 -6
- package/extension/styles/panel.css +79 -8
- package/native-host/src/codexModels.js +6 -2
- package/native-host/src/codexPromptAssembly.js +1 -1
- package/native-host/src/codexSessionRunner.js +5 -1
- package/native-host/src/codexThreadFork.js +103 -0
- package/native-host/src/mirrorWorkspace.js +5 -19
- package/native-host/src/nativeAssetTransfer.js +183 -0
- package/native-host/src/taskRunnerRuntime.js +33 -81
- package/native-host/src/updateManager.js +36 -1
- package/package.json +1 -1
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
(function initCodexOverleafSessionForkController(root, factory) {
|
|
2
|
+
const api = factory();
|
|
3
|
+
if (typeof module === 'object' && module.exports) module.exports = api;
|
|
4
|
+
if (root) root.CodexOverleafSessionForkController = api;
|
|
5
|
+
})(typeof window !== 'undefined' ? window : globalThis, function sessionForkControllerFactory() {
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
function create(deps = {}) {
|
|
9
|
+
const {
|
|
10
|
+
SessionState, getState, setState, sendBackgroundNative, saveState,
|
|
11
|
+
applyStateToPanel, showPluginToast, tr
|
|
12
|
+
} = deps;
|
|
13
|
+
|
|
14
|
+
async function forkRunFromNode(runId) {
|
|
15
|
+
const previousState = getState();
|
|
16
|
+
const source = findSource(previousState, runId);
|
|
17
|
+
if (!source) return failFork('session_fork_source_missing', tr('forkRunUnavailable'));
|
|
18
|
+
const { session, run, runIndex } = source;
|
|
19
|
+
if (run.status === 'running' || !session.codexThreadId || !run.codexTurnId) {
|
|
20
|
+
return failFork('session_fork_unavailable', tr('forkRunUnavailable'));
|
|
21
|
+
}
|
|
22
|
+
const response = await sendBackgroundNative({
|
|
23
|
+
method: 'codex.thread.fork',
|
|
24
|
+
params: {
|
|
25
|
+
threadId: session.codexThreadId,
|
|
26
|
+
lastTurnId: run.codexTurnId,
|
|
27
|
+
speedTier: session.speedTier,
|
|
28
|
+
loadCodexLocalSkills: previousState.loadCodexLocalSkills !== false,
|
|
29
|
+
loadCodexOverleafSkills: previousState.loadCodexOverleafSkills !== false
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
if (!response?.ok || !response.result?.threadId) {
|
|
33
|
+
return failFork(response?.error?.code || 'session_fork_failed', response?.error?.message || tr('forkRunFailed'));
|
|
34
|
+
}
|
|
35
|
+
const copiedRuns = session.runs.slice(0, runIndex + 1).map(copyRunSnapshot);
|
|
36
|
+
const titleBase = String(session.title || copiedRuns[0]?.task || tr('newSessionFallback')).trim();
|
|
37
|
+
const forkTitle = buildUniqueForkTitle(previousState.sessions, titleBase);
|
|
38
|
+
const forked = SessionState.createSession({
|
|
39
|
+
title: forkTitle,
|
|
40
|
+
titleSource: 'manual',
|
|
41
|
+
runs: copiedRuns,
|
|
42
|
+
task: '',
|
|
43
|
+
mode: session.mode,
|
|
44
|
+
providerId: session.providerId,
|
|
45
|
+
model: session.model,
|
|
46
|
+
reasoningEffort: session.reasoningEffort,
|
|
47
|
+
speedTier: session.speedTier,
|
|
48
|
+
requireReviewing: session.requireReviewing,
|
|
49
|
+
focusFiles: session.focusFiles,
|
|
50
|
+
projectReferenceFiles: session.projectReferenceFiles,
|
|
51
|
+
codexThreadId: response.result.threadId,
|
|
52
|
+
pendingInputs: [],
|
|
53
|
+
forkedFromSessionId: session.id,
|
|
54
|
+
forkedFromRunId: run.id
|
|
55
|
+
});
|
|
56
|
+
setState(SessionState.normalizePanelState({
|
|
57
|
+
...previousState,
|
|
58
|
+
sessions: [...(previousState.sessions || []), forked],
|
|
59
|
+
activeSessionId: forked.id
|
|
60
|
+
}));
|
|
61
|
+
try {
|
|
62
|
+
await saveState();
|
|
63
|
+
} catch (error) {
|
|
64
|
+
setState(previousState);
|
|
65
|
+
await sendBackgroundNative({
|
|
66
|
+
method: 'codex.history.clearPlugin',
|
|
67
|
+
params: { sessionId: forked.id, threadId: response.result.threadId }
|
|
68
|
+
}).catch(() => null);
|
|
69
|
+
showPluginToast(error.message || tr('forkRunFailed'), { status: 'failed', sticky: true });
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
applyStateToPanel();
|
|
73
|
+
showPluginToast(tr('forkRunDone'), { status: 'completed' });
|
|
74
|
+
return forked;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function failFork(code, message) {
|
|
78
|
+
showPluginToast(message, { status: 'failed', sticky: true });
|
|
79
|
+
throw codedError(code, message);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return { forkRunFromNode };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function buildUniqueForkTitle(sessions = [], sourceTitle = '') {
|
|
86
|
+
const fallback = String(sourceTitle || 'New Session').trim() || 'New Session';
|
|
87
|
+
const baseTitle = fallback
|
|
88
|
+
.replace(/\s+\(fork\)$/i, '')
|
|
89
|
+
.replace(/\s+\(\d+\)$/, '')
|
|
90
|
+
.trim() || fallback;
|
|
91
|
+
const usedTitles = new Set((sessions || []).map(session => String(session?.title || '').trim()));
|
|
92
|
+
let index = 1;
|
|
93
|
+
while (usedTitles.has(`${baseTitle} (${index})`)) index += 1;
|
|
94
|
+
return `${baseTitle} (${index})`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function findSource(state, runId) {
|
|
98
|
+
for (const session of state?.sessions || []) {
|
|
99
|
+
const runIndex = (session.runs || []).findIndex(run => run.id === runId);
|
|
100
|
+
if (runIndex >= 0) return { session, run: session.runs[runIndex], runIndex };
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function copyRunSnapshot(run) {
|
|
106
|
+
const copy = JSON.parse(JSON.stringify(run || {}));
|
|
107
|
+
return {
|
|
108
|
+
...copy,
|
|
109
|
+
id: `fork_${String(copy.id || 'run')}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`,
|
|
110
|
+
forkSnapshot: true,
|
|
111
|
+
forkSourceRunId: String(copy.id || ''),
|
|
112
|
+
attachments: [], appliedOperations: [], undoOperations: [], undoBaseFiles: [],
|
|
113
|
+
undoTrackedChanges: [], undoExpectedFiles: [], undoStatus: '',
|
|
114
|
+
trackedChangeStatus: '', nativeRequestId: '', queueItemId: '', interruptedDraft: undefined
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function codedError(code, message) { const error = new Error(message); error.code = code; return error; }
|
|
119
|
+
return { copyRunSnapshot, create };
|
|
120
|
+
});
|
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
type: change.type,
|
|
16
16
|
path: change.path,
|
|
17
17
|
contentBase64: change.contentBase64 || '',
|
|
18
|
+
assetRef: change.assetRef && typeof change.assetRef === 'object' ? { ...change.assetRef } : undefined,
|
|
19
|
+
sha256: change.sha256 || change.assetRef?.sha256 || '',
|
|
20
|
+
mimeType: change.mimeType || change.assetRef?.mimeType || '',
|
|
18
21
|
size: Number.isFinite(Number(change.size)) ? Number(change.size) : undefined,
|
|
19
22
|
previousExists: change.previousExists === true,
|
|
20
23
|
previousKind: change.previousKind || '',
|
|
@@ -25,7 +25,6 @@
|
|
|
25
25
|
let appendOperationsPreview;
|
|
26
26
|
let appendPartialWritebackWarning;
|
|
27
27
|
let appendApplyResult;
|
|
28
|
-
let renderDiffReview;
|
|
29
28
|
let renderReadOnlyDiffReview;
|
|
30
29
|
let showPluginConfirm;
|
|
31
30
|
let callPageBridge;
|
|
@@ -62,6 +61,7 @@
|
|
|
62
61
|
let onMirrorRefreshSettled;
|
|
63
62
|
let writebackSettlement;
|
|
64
63
|
let injectedWritebackSettlement;
|
|
64
|
+
let assetTransferBroker;
|
|
65
65
|
|
|
66
66
|
function buildSettlement(input = {}) {
|
|
67
67
|
return writebackSettlement?.settle instanceof Function
|
|
@@ -96,8 +96,8 @@
|
|
|
96
96
|
applyResults: [],
|
|
97
97
|
mode: runMode,
|
|
98
98
|
nextStep: tx(
|
|
99
|
-
'Switch to
|
|
100
|
-
'只有希望 Codex
|
|
99
|
+
'Switch to Auto only when you want Codex to edit files.',
|
|
100
|
+
'只有希望 Codex 修改文件时,才需要切换到自动写入。'
|
|
101
101
|
)
|
|
102
102
|
});
|
|
103
103
|
return {
|
|
@@ -178,51 +178,6 @@
|
|
|
178
178
|
};
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
-
if (runMode === 'confirm') {
|
|
182
|
-
appendRunEvent({
|
|
183
|
-
title: tx(
|
|
184
|
-
`Local Codex produced ${operations.length} change(s). Review the diff before applying.`,
|
|
185
|
-
`本地 Codex 产生了 ${operations.length} 项改动,请查看差异后确认。`
|
|
186
|
-
),
|
|
187
|
-
status: 'running'
|
|
188
|
-
});
|
|
189
|
-
const accepted = await renderDiffReview(visibleSyncChanges);
|
|
190
|
-
if (!accepted.length) {
|
|
191
|
-
appendRunEvent({
|
|
192
|
-
title: tx('Sync cancelled: Overleaf was not modified.', '已取消同步:Overleaf 没有被修改。'),
|
|
193
|
-
status: 'completed'
|
|
194
|
-
});
|
|
195
|
-
appendCompletionReport({
|
|
196
|
-
conclusion: tx('You cancelled syncing. Local Codex changes were not written back to Overleaf.', '你取消了同步,本地 Codex 改动没有写回 Overleaf。'),
|
|
197
|
-
status: 'rejected',
|
|
198
|
-
operations: [],
|
|
199
|
-
applyResults: [],
|
|
200
|
-
mode: runMode,
|
|
201
|
-
nextStep: tx('Run the task again, or switch to Auto if you want changes synced directly.', '可以重新运行任务,或切换到自动写入后再同步。')
|
|
202
|
-
});
|
|
203
|
-
return {
|
|
204
|
-
summaryLine: tx('Sync cancelled', '已取消同步'),
|
|
205
|
-
hasSkippedOperations: additionalSkippedEntries.length > 0,
|
|
206
|
-
settlement: buildSettlement({
|
|
207
|
-
operations,
|
|
208
|
-
applyResult: { ok: false, applied: [], skipped: additionalSkippedEntries }
|
|
209
|
-
}),
|
|
210
|
-
audit: buildAuditSummaryFromApply({
|
|
211
|
-
operations,
|
|
212
|
-
applyResults: additionalSkippedEntries.length ? [{ ok: false, applied: [], skipped: additionalSkippedEntries }] : [],
|
|
213
|
-
blockedFiles: additionalSkippedEntries.map(item => summarizeOperationForAudit(item.operation, item.result, 'blocked')),
|
|
214
|
-
resultStatus: 'rejected'
|
|
215
|
-
})
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
operations = buildSyncApplyOperations(accepted, project);
|
|
219
|
-
const acceptedPathSafety = partitionUnsafeProjectPathOperations(operations);
|
|
220
|
-
if (acceptedPathSafety.skipped.length) {
|
|
221
|
-
additionalSkippedEntries.push(...acceptedPathSafety.skipped);
|
|
222
|
-
operations = acceptedPathSafety.safe;
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
181
|
const deleteOperations = operations.filter(operation => operation.type === 'delete');
|
|
227
182
|
if (deleteOperations.length) {
|
|
228
183
|
const approved = await showPluginConfirm({
|
|
@@ -329,7 +284,7 @@
|
|
|
329
284
|
};
|
|
330
285
|
}
|
|
331
286
|
const applied = operations.length
|
|
332
|
-
? mergeApplyResultSkipped(await
|
|
287
|
+
? mergeApplyResultSkipped(await assetTransferBroker.applyOperations({
|
|
333
288
|
operations,
|
|
334
289
|
baseFiles: project?.files || [],
|
|
335
290
|
requireReviewing: runRequireReviewing,
|
|
@@ -872,7 +827,6 @@
|
|
|
872
827
|
appendOperationsPreview,
|
|
873
828
|
appendPartialWritebackWarning,
|
|
874
829
|
appendApplyResult,
|
|
875
|
-
renderDiffReview,
|
|
876
830
|
renderReadOnlyDiffReview,
|
|
877
831
|
showPluginConfirm,
|
|
878
832
|
callPageBridge,
|
|
@@ -906,6 +860,7 @@
|
|
|
906
860
|
getState,
|
|
907
861
|
getCurrentRunView,
|
|
908
862
|
} = deps);
|
|
863
|
+
assetTransferBroker = deps.assetTransferBroker;
|
|
909
864
|
onMirrorRefreshSettled = deps.onMirrorRefreshSettled;
|
|
910
865
|
writebackSettlement = deps.writebackSettlement;
|
|
911
866
|
compileAdapter = deps.compileAdapter;
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
(function initCodexOverleafBinaryAssetUploader(root, factory) {
|
|
2
|
+
const api = factory();
|
|
3
|
+
if (typeof module === 'object' && module.exports) module.exports = api;
|
|
4
|
+
if (root) root.CodexOverleafBinaryAssetUploader = api;
|
|
5
|
+
})(typeof window !== 'undefined' ? window : globalThis, function binaryAssetUploaderFactory() {
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
const MAX_ASSET_BYTES = 10 * 1024 * 1024;
|
|
9
|
+
|
|
10
|
+
function create(deps = {}) {
|
|
11
|
+
const windowRef = deps.window || globalThis.window;
|
|
12
|
+
const documentRef = deps.document || windowRef?.document;
|
|
13
|
+
const treeOperations = deps.treeOperations;
|
|
14
|
+
const snapshotRouter = deps.snapshotRouter;
|
|
15
|
+
const transfers = new Map();
|
|
16
|
+
|
|
17
|
+
function begin(params = {}) {
|
|
18
|
+
let transferId;
|
|
19
|
+
let projectPath;
|
|
20
|
+
try {
|
|
21
|
+
transferId = cleanTransferId(params.transferId);
|
|
22
|
+
projectPath = normalizePath(params.path);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
return failure('binary_asset_path_invalid', error.message);
|
|
25
|
+
}
|
|
26
|
+
const size = Number(params.size);
|
|
27
|
+
if (!Number.isSafeInteger(size) || size < 0 || size > MAX_ASSET_BYTES) {
|
|
28
|
+
return failure('binary_asset_size_invalid', 'Asset size is invalid or exceeds 10 MiB.');
|
|
29
|
+
}
|
|
30
|
+
if (!/^[a-f0-9]{64}$/i.test(String(params.sha256 || ''))) {
|
|
31
|
+
return failure('binary_asset_hash_invalid', 'Asset SHA-256 is missing or invalid.');
|
|
32
|
+
}
|
|
33
|
+
transfers.set(transferId, {
|
|
34
|
+
id: transferId,
|
|
35
|
+
path: projectPath,
|
|
36
|
+
size,
|
|
37
|
+
sha256: String(params.sha256).toLowerCase(),
|
|
38
|
+
mimeType: String(params.mimeType || 'application/octet-stream'),
|
|
39
|
+
overwrite: params.overwrite === true,
|
|
40
|
+
chunks: [],
|
|
41
|
+
received: 0
|
|
42
|
+
});
|
|
43
|
+
return { ok: true, transferId };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function append(params = {}) {
|
|
47
|
+
const transfer = transfers.get(String(params.transferId || ''));
|
|
48
|
+
if (!transfer) return failure('binary_upload_transfer_missing', 'Asset upload transfer was not found.');
|
|
49
|
+
if (Number(params.offset) !== transfer.received) {
|
|
50
|
+
return failure('binary_upload_chunk_order_invalid', 'Asset chunks arrived out of order.');
|
|
51
|
+
}
|
|
52
|
+
let bytes;
|
|
53
|
+
try { bytes = decodeBase64(params.contentBase64, windowRef); } catch (_error) {
|
|
54
|
+
return failure('binary_upload_chunk_invalid', 'Asset chunk is not valid Base64.');
|
|
55
|
+
}
|
|
56
|
+
if (transfer.received + bytes.length > transfer.size) {
|
|
57
|
+
return failure('binary_upload_size_mismatch', 'Asset chunks exceed the declared size.');
|
|
58
|
+
}
|
|
59
|
+
transfer.chunks.push(bytes);
|
|
60
|
+
transfer.received += bytes.length;
|
|
61
|
+
return { ok: true, transferId: transfer.id, received: transfer.received };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function commit(params = {}) {
|
|
65
|
+
const transferId = String(params.transferId || '');
|
|
66
|
+
const transfer = transfers.get(transferId);
|
|
67
|
+
if (!transfer) return failure('binary_upload_transfer_missing', 'Asset upload transfer was not found.');
|
|
68
|
+
try {
|
|
69
|
+
if (transfer.received !== transfer.size) return failure('binary_upload_incomplete', 'Asset upload is incomplete.');
|
|
70
|
+
const bytes = joinChunks(transfer.chunks, transfer.size);
|
|
71
|
+
const digest = await sha256Hex(bytes, windowRef.crypto);
|
|
72
|
+
if (digest !== transfer.sha256) {
|
|
73
|
+
return failure('binary_upload_checksum_mismatch', 'Asset checksum did not match the Native Host descriptor.');
|
|
74
|
+
}
|
|
75
|
+
const fileName = transfer.path.split('/').pop();
|
|
76
|
+
const file = new windowRef.File([bytes], fileName, { type: transfer.mimeType });
|
|
77
|
+
const attempts = [
|
|
78
|
+
// Prefer Overleaf's visible uploader. Its UI contract is more
|
|
79
|
+
// stable than the private multipart endpoint across deployments.
|
|
80
|
+
() => uploadWithDom(file, transfer),
|
|
81
|
+
() => uploadWithMultipart(file, transfer),
|
|
82
|
+
() => uploadWithLegacyManager(file, transfer)
|
|
83
|
+
];
|
|
84
|
+
const errors = [];
|
|
85
|
+
for (const attempt of attempts) {
|
|
86
|
+
try {
|
|
87
|
+
const result = await attempt();
|
|
88
|
+
const observed = result?.ok ? await waitForPath(transfer.path, 12000) : null;
|
|
89
|
+
const hashVerified = observed ? await verifyRemoteHash(observed, transfer).catch(() => false) : false;
|
|
90
|
+
const overwriteVerified = !transfer.overwrite || hashVerified || result?.confirmedByTransport === true;
|
|
91
|
+
if (result?.ok && observed && overwriteVerified) {
|
|
92
|
+
return { ok: true, written: true, path: transfer.path, method: result.method, changedDocument: true };
|
|
93
|
+
}
|
|
94
|
+
if (result?.ok && observed && !overwriteVerified) errors.push('Overleaf did not expose enough evidence to verify the binary replacement.');
|
|
95
|
+
if (result?.reason) errors.push(result.reason);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
errors.push(error.message);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return failure('binary_upload_unavailable', errors.filter(Boolean).join('; ') || 'No supported Overleaf asset upload path succeeded.');
|
|
101
|
+
} finally {
|
|
102
|
+
transfers.delete(transferId);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function abort(params = {}) {
|
|
107
|
+
return { ok: true, aborted: transfers.delete(String(params.transferId || '')) };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function uploadWithMultipart(file, transfer) {
|
|
111
|
+
const projectId = treeOperations.getProjectId();
|
|
112
|
+
if (!projectId || typeof windowRef.fetch !== 'function') return { ok: false, reason: 'Project upload endpoint is unavailable.' };
|
|
113
|
+
const parentPath = transfer.path.split('/').slice(0, -1).join('/');
|
|
114
|
+
const folderId = await findFolderId(parentPath);
|
|
115
|
+
if (parentPath && !folderId) return { ok: false, reason: 'Target folder id is unavailable for direct upload.' };
|
|
116
|
+
const form = new windowRef.FormData();
|
|
117
|
+
form.append('qqfile', file, file.name);
|
|
118
|
+
form.append('name', file.name);
|
|
119
|
+
if (folderId) form.append('folder_id', folderId);
|
|
120
|
+
const csrf = findCsrfToken();
|
|
121
|
+
const headers = csrf ? { 'X-CSRF-Token': csrf } : {};
|
|
122
|
+
const query = folderId ? `?folder_id=${encodeURIComponent(folderId)}` : '';
|
|
123
|
+
const response = await windowRef.fetch(`/project/${encodeURIComponent(projectId)}/upload${query}`, {
|
|
124
|
+
method: 'POST', body: form, credentials: 'same-origin', headers
|
|
125
|
+
});
|
|
126
|
+
const responseText = await response.text();
|
|
127
|
+
if (!response.ok) throw new Error(formatUploadHttpError(response.status, responseText));
|
|
128
|
+
try {
|
|
129
|
+
const payload = responseText ? JSON.parse(responseText) : null;
|
|
130
|
+
if (payload && (payload.success === false || payload.ok === false || payload.error)) {
|
|
131
|
+
throw new Error(payload.error?.message || payload.error || payload.message || 'Overleaf rejected the asset upload.');
|
|
132
|
+
}
|
|
133
|
+
} catch (error) {
|
|
134
|
+
if (error instanceof SyntaxError) {
|
|
135
|
+
// Some Overleaf deployments return an empty or HTML success body.
|
|
136
|
+
} else {
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return { ok: true, method: 'overleaf.multipart', confirmedByTransport: true };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function uploadWithDom(file, transfer) {
|
|
144
|
+
const parentPath = transfer.path.split('/').slice(0, -1).join('/');
|
|
145
|
+
if (parentPath) {
|
|
146
|
+
const folderNode = treeOperations.findFileTreeNode(parentPath);
|
|
147
|
+
if (!folderNode) return { ok: false, reason: `Target folder ${parentPath} is unavailable in the Overleaf file tree.` };
|
|
148
|
+
folderNode?.dispatchEvent(new windowRef.MouseEvent('click', { bubbles: true }));
|
|
149
|
+
await delay(120);
|
|
150
|
+
}
|
|
151
|
+
let input = findFileInput();
|
|
152
|
+
if (!input) {
|
|
153
|
+
const button = findUploadButton();
|
|
154
|
+
if (!button) return { ok: false, reason: 'Overleaf upload button was not found.' };
|
|
155
|
+
button.click();
|
|
156
|
+
input = await waitForFileInput(4000);
|
|
157
|
+
}
|
|
158
|
+
if (!input) return { ok: false, reason: 'Overleaf upload file input was not found.' };
|
|
159
|
+
const data = new windowRef.DataTransfer();
|
|
160
|
+
data.items.add(file);
|
|
161
|
+
assignFilesToInput(input, data.files, windowRef);
|
|
162
|
+
input.dispatchEvent(new windowRef.Event('input', { bubbles: true }));
|
|
163
|
+
input.dispatchEvent(new windowRef.Event('change', { bubbles: true }));
|
|
164
|
+
await clickReplaceIfShown();
|
|
165
|
+
return { ok: true, method: 'overleaf.file-input' };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function uploadWithLegacyManager(file, transfer) {
|
|
169
|
+
const manager = treeOperations.findFileTreeManager();
|
|
170
|
+
if (!manager) return { ok: false, reason: 'Legacy file-tree manager is unavailable.' };
|
|
171
|
+
for (const name of ['uploadFile', 'uploadAsset', 'createBinaryFile', 'createFile', 'addFile']) {
|
|
172
|
+
if (typeof manager[name] !== 'function') continue;
|
|
173
|
+
try {
|
|
174
|
+
await manager[name](transfer.path, file);
|
|
175
|
+
return { ok: true, method: `fileTreeManager.${name}`, confirmedByTransport: true };
|
|
176
|
+
} catch (_error) { /* try the next adapter */ }
|
|
177
|
+
}
|
|
178
|
+
return { ok: false, reason: 'Legacy file-tree manager has no compatible asset method.' };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function findFolderId(parentPath) {
|
|
182
|
+
const node = parentPath ? treeOperations.findFileTreeNode(parentPath) : null;
|
|
183
|
+
const element = node?.closest?.('[data-entity-id], [data-folder-id], [data-id]') || node;
|
|
184
|
+
const domId = element?.dataset?.entityId || element?.dataset?.folderId || element?.dataset?.id;
|
|
185
|
+
if (domId) return domId;
|
|
186
|
+
for (const root of [windowRef._ide, windowRef.Overleaf, windowRef.overleaf, windowRef.OL]) {
|
|
187
|
+
const folder = root?.project?.rootFolder?.[0] || root?.project?.rootFolder || root?.rootFolder?.[0] || root?.rootFolder;
|
|
188
|
+
if (!parentPath && (folder?._id || folder?.id)) return folder._id || folder.id;
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
const list = await snapshotRouter?.buildProjectFileList({
|
|
192
|
+
force: true, maxAgeMs: 0, preferLightweight: true, allowZipFallback: false
|
|
193
|
+
});
|
|
194
|
+
const folder = (list?.files || []).find(item => item.path === parentPath && /folder/i.test(item.kind || item.type || ''));
|
|
195
|
+
if (folder) return folder.id || folder._id || folder.entityId || '';
|
|
196
|
+
} catch (_error) { /* DOM and legacy adapters remain available */ }
|
|
197
|
+
return '';
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function findCsrfToken() {
|
|
201
|
+
return documentRef.querySelector('meta[name="ol-csrfToken"], meta[name="csrf-token"], meta[name="_csrf"]')?.content
|
|
202
|
+
|| windowRef.csrfToken || windowRef._csrf || '';
|
|
203
|
+
}
|
|
204
|
+
function findFileInput() {
|
|
205
|
+
return chooseOverleafFileInput(Array.from(documentRef.querySelectorAll('input[type="file"]')));
|
|
206
|
+
}
|
|
207
|
+
function findUploadButton() {
|
|
208
|
+
return chooseOverleafUploadButton(Array.from(documentRef.querySelectorAll('button, [role="button"]')));
|
|
209
|
+
}
|
|
210
|
+
function chooseOverleafUploadButton(nodes) {
|
|
211
|
+
return nodes.filter(node => {
|
|
212
|
+
if (!node || node.disabled || isCodexOwnedNode(node)) return false;
|
|
213
|
+
const label = `${node.getAttribute('aria-label') || ''} ${node.getAttribute('title') || ''} ${node.textContent || ''}`;
|
|
214
|
+
return /upload|上传/i.test(label);
|
|
215
|
+
}).sort((left, right) => uploadButtonScore(right) - uploadButtonScore(left))[0] || null;
|
|
216
|
+
}
|
|
217
|
+
async function waitForFileInput(timeoutMs) {
|
|
218
|
+
const deadline = Date.now() + timeoutMs;
|
|
219
|
+
while (Date.now() < deadline) {
|
|
220
|
+
const input = findFileInput();
|
|
221
|
+
if (input) return input;
|
|
222
|
+
await delay(50);
|
|
223
|
+
}
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
async function clickReplaceIfShown() {
|
|
227
|
+
await delay(180);
|
|
228
|
+
const button = Array.from(documentRef.querySelectorAll('button')).find(node => /replace|overwrite|替换|覆盖/i.test(node.textContent || ''));
|
|
229
|
+
if (button) button.click();
|
|
230
|
+
}
|
|
231
|
+
async function waitForPath(projectPath, timeoutMs) {
|
|
232
|
+
const deadline = Date.now() + timeoutMs;
|
|
233
|
+
while (Date.now() < deadline) {
|
|
234
|
+
if (treeOperations.projectPathExists(projectPath)) {
|
|
235
|
+
const node = treeOperations.findFileTreeNode(projectPath);
|
|
236
|
+
return {
|
|
237
|
+
path: projectPath,
|
|
238
|
+
id: node?.dataset?.entityId || node?.dataset?.fileId || node?.dataset?.id || ''
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
await delay(250);
|
|
242
|
+
try {
|
|
243
|
+
const list = await snapshotRouter?.buildProjectFileList({
|
|
244
|
+
force: true, maxAgeMs: 0, preferLightweight: true, allowZipFallback: false
|
|
245
|
+
});
|
|
246
|
+
const file = (list?.files || []).find(item => item.path === projectPath);
|
|
247
|
+
if (file) return file;
|
|
248
|
+
} catch (_error) { /* continue until the deadline */ }
|
|
249
|
+
}
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
async function verifyRemoteHash(observed, transfer) {
|
|
253
|
+
const fileId = observed?.id || observed?._id || observed?.entityId || observed?.fileId || '';
|
|
254
|
+
const projectId = treeOperations.getProjectId();
|
|
255
|
+
if (!fileId || !projectId || typeof windowRef.fetch !== 'function') return false;
|
|
256
|
+
const response = await windowRef.fetch(`/project/${encodeURIComponent(projectId)}/file/${encodeURIComponent(fileId)}`, {
|
|
257
|
+
credentials: 'same-origin', cache: 'no-store'
|
|
258
|
+
});
|
|
259
|
+
if (!response.ok) return false;
|
|
260
|
+
const digest = await sha256Hex(new Uint8Array(await response.arrayBuffer()), windowRef.crypto);
|
|
261
|
+
return digest === transfer.sha256;
|
|
262
|
+
}
|
|
263
|
+
function delay(ms) { return new Promise(resolve => windowRef.setTimeout(resolve, ms)); }
|
|
264
|
+
return { abort, append, begin, commit };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function normalizePath(value) {
|
|
268
|
+
const path = String(value || '').replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
|
|
269
|
+
if (!path || path.split('/').some(part => !part || part === '.' || part === '..')) throw new Error('Asset path is invalid.');
|
|
270
|
+
return path;
|
|
271
|
+
}
|
|
272
|
+
function cleanTransferId(value) {
|
|
273
|
+
const id = String(value || '');
|
|
274
|
+
if (!/^[a-zA-Z0-9_-]{8,120}$/.test(id)) throw new Error('Asset transfer id is invalid.');
|
|
275
|
+
return id;
|
|
276
|
+
}
|
|
277
|
+
function chooseOverleafFileInput(inputs) {
|
|
278
|
+
return Array.from(inputs || [])
|
|
279
|
+
.filter(input => input && !input.disabled && !isCodexOwnedNode(input))
|
|
280
|
+
.sort((left, right) => uploadInputScore(right) - uploadInputScore(left))[0] || null;
|
|
281
|
+
}
|
|
282
|
+
function isCodexOwnedNode(node) {
|
|
283
|
+
return Boolean(node?.closest?.('#codex-overleaf-panel, [data-codex-overleaf-root], .codex-overleaf-panel'));
|
|
284
|
+
}
|
|
285
|
+
function uploadInputScore(input) {
|
|
286
|
+
let score = 0;
|
|
287
|
+
if (input?.closest?.('.uppy-Dashboard, [data-uppy-drag-drop-supported]')) score += 100;
|
|
288
|
+
if (input?.closest?.('[role="dialog"], .modal')) score += 40;
|
|
289
|
+
if (/uppy|upload/i.test(`${input?.className || ''} ${input?.getAttribute?.('data-testid') || ''}`)) score += 30;
|
|
290
|
+
if (input?.multiple) score += 10;
|
|
291
|
+
if (input?.isConnected !== false) score += 1;
|
|
292
|
+
return score;
|
|
293
|
+
}
|
|
294
|
+
function uploadButtonScore(node) {
|
|
295
|
+
let score = 0;
|
|
296
|
+
const label = `${node?.getAttribute?.('aria-label') || ''} ${node?.getAttribute?.('title') || ''} ${node?.textContent || ''}`.trim();
|
|
297
|
+
if (/^(upload|上传)$/i.test(label)) score += 50;
|
|
298
|
+
if (node?.closest?.('[data-testid*="file-tree"], .file-tree, .ide-react-file-tree')) score += 30;
|
|
299
|
+
if (node?.isConnected !== false) score += 1;
|
|
300
|
+
return score;
|
|
301
|
+
}
|
|
302
|
+
function assignFilesToInput(input, files, windowRef) {
|
|
303
|
+
const prototype = windowRef?.HTMLInputElement?.prototype;
|
|
304
|
+
const setter = prototype && Object.getOwnPropertyDescriptor(prototype, 'files')?.set;
|
|
305
|
+
if (setter) {
|
|
306
|
+
setter.call(input, files);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
Object.defineProperty(input, 'files', { configurable: true, value: files });
|
|
310
|
+
}
|
|
311
|
+
function formatUploadHttpError(status, responseText) {
|
|
312
|
+
const detail = summarizeUploadResponse(responseText);
|
|
313
|
+
return `Overleaf upload endpoint returned HTTP ${status}${detail ? ` (${detail})` : ''}.`;
|
|
314
|
+
}
|
|
315
|
+
function summarizeUploadResponse(value) {
|
|
316
|
+
const text = String(value || '').trim();
|
|
317
|
+
if (!text || /^\s*</.test(text)) return '';
|
|
318
|
+
let message = text;
|
|
319
|
+
try {
|
|
320
|
+
const payload = JSON.parse(text);
|
|
321
|
+
message = payload?.error?.message || payload?.error || payload?.message || text;
|
|
322
|
+
} catch (_error) { /* use the bounded plain-text response */ }
|
|
323
|
+
return String(message || '').replace(/\s+/g, ' ').slice(0, 240);
|
|
324
|
+
}
|
|
325
|
+
function decodeBase64(value, windowRef) {
|
|
326
|
+
const binary = windowRef.atob(String(value || ''));
|
|
327
|
+
const bytes = new Uint8Array(binary.length);
|
|
328
|
+
for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
|
|
329
|
+
return bytes;
|
|
330
|
+
}
|
|
331
|
+
function joinChunks(chunks, size) {
|
|
332
|
+
const output = new Uint8Array(size);
|
|
333
|
+
let offset = 0;
|
|
334
|
+
for (const chunk of chunks) { output.set(chunk, offset); offset += chunk.length; }
|
|
335
|
+
return output;
|
|
336
|
+
}
|
|
337
|
+
async function sha256Hex(bytes, cryptoImpl) {
|
|
338
|
+
const digest = await cryptoImpl.subtle.digest('SHA-256', bytes);
|
|
339
|
+
return Array.from(new Uint8Array(digest), byte => byte.toString(16).padStart(2, '0')).join('');
|
|
340
|
+
}
|
|
341
|
+
function failure(code, reason) { return { ok: false, code, reason, changedDocument: false }; }
|
|
342
|
+
return { chooseOverleafFileInput, create, formatUploadHttpError, joinChunks, normalizePath };
|
|
343
|
+
});
|
|
@@ -971,8 +971,15 @@
|
|
|
971
971
|
}
|
|
972
972
|
|
|
973
973
|
for (const path of paths) {
|
|
974
|
-
|
|
975
|
-
|
|
974
|
+
const postContent = postByPath.get(path);
|
|
975
|
+
const activePathMatches = getActiveFilePath() === path;
|
|
976
|
+
const activeContentMatches = readActiveEditorText() === postContent;
|
|
977
|
+
// Overleaf can update the selected tree path before CodeMirror swaps its
|
|
978
|
+
// document. In that state getActiveFilePath() already names `path`, while
|
|
979
|
+
// readActiveEditorText() still belongs to the previously viewed file.
|
|
980
|
+
// Force-reopen the target whenever either identity signal disagrees.
|
|
981
|
+
if (path && (!activePathMatches || !activeContentMatches)) {
|
|
982
|
+
const opened = await openFileByPath(path, { force: true });
|
|
976
983
|
if (!opened.ok) {
|
|
977
984
|
return {
|
|
978
985
|
ok: false,
|
|
@@ -983,8 +990,7 @@
|
|
|
983
990
|
}
|
|
984
991
|
}
|
|
985
992
|
|
|
986
|
-
const
|
|
987
|
-
const postReady = await waitForActiveEditorExpectedText(path, postContent, 2500);
|
|
993
|
+
const postReady = await waitForActiveEditorExpectedText(path, postContent, 9000);
|
|
988
994
|
if (!postReady.ok) {
|
|
989
995
|
return {
|
|
990
996
|
ok: false,
|
|
@@ -1492,6 +1498,8 @@
|
|
|
1492
1498
|
'[data-track-change-id]',
|
|
1493
1499
|
'[data-ol-change-id]',
|
|
1494
1500
|
'[data-path][class*="change" i]',
|
|
1501
|
+
'.ol-cm-change',
|
|
1502
|
+
'.review-panel-entry-change',
|
|
1495
1503
|
'[class*="track-change" i]',
|
|
1496
1504
|
'[class*="review-change" i]',
|
|
1497
1505
|
'[class*="suggest" i]',
|
|
@@ -1667,13 +1667,17 @@
|
|
|
1667
1667
|
continue;
|
|
1668
1668
|
}
|
|
1669
1669
|
const key = typeof ref.key === 'string' ? ref.key : '';
|
|
1670
|
-
if (!key
|
|
1670
|
+
if (!key) {
|
|
1671
1671
|
continue;
|
|
1672
1672
|
}
|
|
1673
|
-
seen.add(key);
|
|
1674
1673
|
const hasPath = typeof ref.path === 'string';
|
|
1675
1674
|
const hasExplicitPath = hasPath && ref.path.trim() !== '';
|
|
1676
1675
|
const path = hasPath ? normalizeSafeProjectPath(ref.path) : '';
|
|
1676
|
+
const identity = `${path}\u0000${key}`;
|
|
1677
|
+
if (seen.has(identity)) {
|
|
1678
|
+
continue;
|
|
1679
|
+
}
|
|
1680
|
+
seen.add(identity);
|
|
1677
1681
|
normalized.push({
|
|
1678
1682
|
key,
|
|
1679
1683
|
id: typeof ref.id === 'string' ? ref.id : '',
|