codex-overleaf-link 2.3.5 → 2.4.0
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 +63 -31
- package/README.zh-CN.md +543 -0
- package/extension/bootstrap/manifest.template.json +1 -1
- package/extension/src/background.js +7 -0
- package/extension/src/backgroundUpdateCoordinator.js +88 -4
- package/extension/src/content/activeTurnControl.js +1 -0
- package/extension/src/content/assetTransferBroker.js +2 -0
- package/extension/src/content/contentRuntime.js +77 -57
- package/extension/src/content/contextTray.js +4 -0
- package/extension/src/content/diagnosticsController.js +17 -3
- package/extension/src/content/diagnosticsPanel.js +129 -5
- package/extension/src/content/generated/content.bundle.js +1422 -348
- package/extension/src/content/generated/content.bundle.meta.json +7 -3
- package/extension/src/content/legacyGlobalRegistry.js +3 -0
- package/extension/src/content/modelPicker.js +21 -9
- package/extension/src/content/modelPickerSupport.js +38 -1
- package/extension/src/content/nativeCompatibilityController.js +4 -1
- package/extension/src/content/otWarmMirror.js +126 -92
- package/extension/src/content/otWarmMirrorController.js +122 -1
- package/extension/src/content/pageBridgeClient.js +2 -0
- package/extension/src/content/panelMaintenance.js +26 -15
- package/extension/src/content/providerSettingsCoordinator.js +1 -0
- package/extension/src/content/runFailureNotice.js +104 -0
- package/extension/src/content/runResultActions.js +16 -1
- package/extension/src/content/runScrollLayout.js +124 -0
- package/extension/src/content/runTimelineView.js +37 -11
- package/extension/src/content/sessionForkController.js +20 -11
- package/extension/src/content/settingsPanel.js +28 -11
- package/extension/src/content/trackedChangeCaptureController.js +153 -0
- package/extension/src/content/updateNotice.js +24 -4
- package/extension/src/content/writebackOrchestrator.js +31 -0
- package/extension/src/page/binaryAssetUploader.js +101 -29
- package/extension/src/page/overleafRealtimeObserver.js +120 -25
- package/extension/src/page/textFileCreator.js +493 -0
- package/extension/src/page/trackedChangeCapture.js +342 -0
- package/extension/src/page/trackedChangesLifecycle.js +145 -149
- package/extension/src/page/treeOperations.js +30 -2
- package/extension/src/page/writebackRouter.js +94 -48
- package/extension/src/pageBridge.js +62 -25
- package/extension/src/shared/compatibility.js +1 -1
- package/extension/src/shared/i18n.js +77 -37
- package/extension/src/shared/managedUpdateProjection.js +1 -1
- package/extension/src/shared/otText.js +0 -1
- package/extension/src/shared/pageRpcContract.js +2 -1
- package/extension/src/shared/sessionState.js +23 -23
- package/extension/src/shared/sessionTitle.js +120 -0
- package/extension/src/shared/storageDb.js +10 -12
- package/extension/src/shared/storageRunActions.js +29 -1
- package/extension/src/shared/updateRuntimeIdentity.js +32 -0
- package/extension/src/shared/writebackSettlement.js +19 -5
- package/extension/styles/panel.css +128 -24
- package/native-host/src/mirrorWorkspace.js +20 -1
- package/native-host/src/releaseProxy.js +177 -0
- package/native-host/src/releaseTransport.js +154 -0
- package/native-host/src/taskRunnerRuntime.js +25 -0
- package/native-host/src/updateManager.js +40 -39
- package/package.json +5 -1
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
(function initCodexOverleafOtWarmMirror() {
|
|
2
2
|
'use strict';
|
|
3
|
-
|
|
4
3
|
// Experimental OT warm-mirror glue carved out of contentRuntime.js (v1.4.9
|
|
5
4
|
// structural-debt phase 5): the per-project enable toggle flow, the
|
|
6
5
|
// poll/flush timers and patch queue, mirror prefetch, warm-start
|
|
@@ -50,7 +49,9 @@
|
|
|
50
49
|
} = deps;
|
|
51
50
|
|
|
52
51
|
let currentOtStatus = 'off';
|
|
52
|
+
let otTogglePending = false;
|
|
53
53
|
let otSyncRequestId = 0;
|
|
54
|
+
let otStartRetryCount = 0;
|
|
54
55
|
let otWarmMirrorProjectId = '';
|
|
55
56
|
let lastExperimentalOtProjectId = '';
|
|
56
57
|
let otWarmMirrorState = {
|
|
@@ -61,7 +62,10 @@
|
|
|
61
62
|
patchQueue: [],
|
|
62
63
|
lastStatus: 'off',
|
|
63
64
|
lastPatchAt: 0,
|
|
64
|
-
lastErrorCode: ''
|
|
65
|
+
lastErrorCode: '',
|
|
66
|
+
failureActive: false,
|
|
67
|
+
lastFailure: null,
|
|
68
|
+
suspended: false
|
|
65
69
|
};
|
|
66
70
|
let mirrorPrefetchState = {
|
|
67
71
|
inFlight: null,
|
|
@@ -159,14 +163,14 @@
|
|
|
159
163
|
}
|
|
160
164
|
|
|
161
165
|
async function handleExperimentalOtToggleClick(event) {
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
// is the user's intended target. preventDefault then rolls the visual
|
|
165
|
-
// state back (synchronously, when dispatch completes), and the confirm
|
|
166
|
-
// flow applies the target only after approval.
|
|
166
|
+
// Capture the pre-activated target, then let cancelled click dispatch
|
|
167
|
+
// restore the old checkbox value before projecting the approved state.
|
|
167
168
|
const clicked = event?.currentTarget || event?.target;
|
|
168
169
|
const targetEnabled = clicked && typeof clicked.checked === 'boolean' ? clicked.checked : undefined;
|
|
170
|
+
const projectId = getCurrentProjectId();
|
|
169
171
|
event.preventDefault();
|
|
172
|
+
await new Promise(resolve => window.setTimeout(resolve, 0));
|
|
173
|
+
if (projectId !== getCurrentProjectId()) return;
|
|
170
174
|
await toggleExperimentalOtCheckbox(targetEnabled);
|
|
171
175
|
}
|
|
172
176
|
|
|
@@ -180,9 +184,12 @@
|
|
|
180
184
|
|
|
181
185
|
async function toggleExperimentalOtCheckbox(targetEnabled) {
|
|
182
186
|
const checkbox = getPanel()?.querySelector('[data-experimental-ot]');
|
|
183
|
-
if (!checkbox) {
|
|
187
|
+
if (!checkbox || otTogglePending) {
|
|
184
188
|
return;
|
|
185
189
|
}
|
|
190
|
+
const projectId = getCurrentProjectId();
|
|
191
|
+
otTogglePending = true;
|
|
192
|
+
try {
|
|
186
193
|
// Explicit target from the click interceptor wins; the keyboard/legacy
|
|
187
194
|
// path (no argument, un-flipped checkbox) falls back to inversion.
|
|
188
195
|
const nextEnabled = typeof targetEnabled === 'boolean' ? targetEnabled : !checkbox.checked;
|
|
@@ -200,11 +207,15 @@
|
|
|
200
207
|
} else {
|
|
201
208
|
closeDiagnosticsMenu();
|
|
202
209
|
}
|
|
210
|
+
if (projectId !== getCurrentProjectId() || checkbox !== getPanel()?.querySelector('[data-experimental-ot]')) return;
|
|
203
211
|
checkbox.checked = nextEnabled;
|
|
204
212
|
handleExperimentalOtToggleChange({ currentTarget: checkbox });
|
|
205
213
|
if (nextEnabled) {
|
|
206
214
|
showPluginToast(tr('experimentalOtEnabledToast'), { status: 'info' });
|
|
207
215
|
}
|
|
216
|
+
} finally {
|
|
217
|
+
otTogglePending = false;
|
|
218
|
+
}
|
|
208
219
|
}
|
|
209
220
|
|
|
210
221
|
function handleExperimentalOtToggleChange(event) {
|
|
@@ -213,7 +224,9 @@
|
|
|
213
224
|
return;
|
|
214
225
|
}
|
|
215
226
|
const projectId = getCurrentProjectId();
|
|
227
|
+
otWarmMirrorState.failureActive = false;
|
|
216
228
|
lastExperimentalOtProjectId = projectId;
|
|
229
|
+
otStartRetryCount = 0;
|
|
217
230
|
setExperimentalOtEnabledForProject(projectId, checkbox.checked);
|
|
218
231
|
updateExperimentalOtToggleControl(checkbox.checked);
|
|
219
232
|
// Per-card saved feedback, same contract as every other settings card.
|
|
@@ -230,7 +243,7 @@
|
|
|
230
243
|
updateOtStatusDisplay(checkbox.checked ? (currentOtStatus === 'off' ? 'starting' : currentOtStatus) : 'off');
|
|
231
244
|
syncOtWarmMirrorController().catch(error => {
|
|
232
245
|
updateOtStatusDisplay('unavailable');
|
|
233
|
-
appendPlainLog(tx(`
|
|
246
|
+
appendPlainLog(tx(`OT warm mirror unavailable: ${error.message}`, `OT 预热镜像不可用:${error.message}`));
|
|
234
247
|
});
|
|
235
248
|
saveStateSoon();
|
|
236
249
|
}
|
|
@@ -252,6 +265,7 @@
|
|
|
252
265
|
otWarmMirrorProjectId = projectId;
|
|
253
266
|
ensureOtWarmMirrorStateProject(projectId);
|
|
254
267
|
if (enabled) {
|
|
268
|
+
otWarmMirrorState.suspended = false;
|
|
255
269
|
const pause = otWarmMirrorController?.shouldPauseOtWarmMirror?.({ running: Boolean(getCurrentRunView()) }) || { pause: false };
|
|
256
270
|
if (pause.pause) {
|
|
257
271
|
clearOtEventPolling({ clearPatchQueue: true });
|
|
@@ -263,9 +277,12 @@
|
|
|
263
277
|
} else {
|
|
264
278
|
clearOtEventPolling({ clearPatchQueue: true });
|
|
265
279
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
: await callPageBridge('stopOtObserver', {});
|
|
280
|
+
let response;
|
|
281
|
+
try {
|
|
282
|
+
response = enabled ? await callPageBridge('startOtObserver', { projectId }) : await callPageBridge('stopOtObserver', {});
|
|
283
|
+
} catch (error) {
|
|
284
|
+
response = { ok: false, error: otWarmMirrorController.normalizeFailureCode(error?.code || error?.message, 'ot_bridge_unavailable') };
|
|
285
|
+
}
|
|
269
286
|
if (!isCurrentOtSync(requestId, projectId) || getCurrentProjectId() !== projectId) {
|
|
270
287
|
if (enabled) {
|
|
271
288
|
await handleStaleOtStartResponse(projectId, requestId);
|
|
@@ -282,7 +299,7 @@
|
|
|
282
299
|
}
|
|
283
300
|
const status = readOtBridgeStatus(response);
|
|
284
301
|
if (!isSuccessfulOtBridgeResponse(response) || status === 'unavailable') {
|
|
285
|
-
handleFailedOtStart(projectId, requestId);
|
|
302
|
+
await handleFailedOtStart(projectId, requestId, response);
|
|
286
303
|
return response;
|
|
287
304
|
}
|
|
288
305
|
const pauseAfterStart = otWarmMirrorController?.shouldPauseOtWarmMirror?.({ running: Boolean(getCurrentRunView()) }) || { pause: false };
|
|
@@ -290,7 +307,10 @@
|
|
|
290
307
|
await pauseOtWarmMirror(pauseAfterStart.reason || 'paused');
|
|
291
308
|
return response;
|
|
292
309
|
}
|
|
310
|
+
if (status === 'starting') recordOtFailure('missing_active_path', 'start');
|
|
311
|
+
else otWarmMirrorState.failureActive = false;
|
|
293
312
|
updateOtStatusDisplay(status);
|
|
313
|
+
otStartRetryCount = 0;
|
|
294
314
|
scheduleOtEventPolling(projectId, { immediate: true });
|
|
295
315
|
return response;
|
|
296
316
|
}
|
|
@@ -314,25 +334,43 @@
|
|
|
314
334
|
}
|
|
315
335
|
}
|
|
316
336
|
|
|
317
|
-
function handleFailedOtStart(projectId, requestId) {
|
|
337
|
+
async function handleFailedOtStart(projectId, requestId, response) {
|
|
318
338
|
if (!isCurrentOtSync(requestId, projectId) || getCurrentProjectId() !== projectId) {
|
|
319
339
|
return;
|
|
320
340
|
}
|
|
321
341
|
clearOtEventPolling({ clearPatchQueue: true });
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
342
|
+
recordOtFailure(readOtBridgeErrorCode(response, 'ot_start_failed'), 'start');
|
|
343
|
+
otWarmMirrorState.suspended = true;
|
|
344
|
+
// Observer availability is transient; only an explicit user action may
|
|
345
|
+
// change the persisted opt-in. A cold editor/bridge must not turn OT off
|
|
346
|
+
// permanently. failureActive/suspended already prohibit cache reuse.
|
|
347
|
+
updateExperimentalOtToggleControl(isExperimentalOtEnabledForProject(projectId));
|
|
327
348
|
updateOtStatusDisplay('unavailable');
|
|
328
|
-
|
|
349
|
+
try { await callPageBridge('stopOtObserver', { projectId }); } catch (_error) { /* preserve the original failure */ }
|
|
350
|
+
scheduleOtStartRetry(projectId, requestId);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function scheduleOtStartRetry(projectId, requestId) {
|
|
354
|
+
const retryDelays = [1000, 3000, 6000];
|
|
355
|
+
const canRetry = () => isCurrentOtSync(requestId, projectId) && getCurrentProjectId() === projectId
|
|
356
|
+
&& isExperimentalOtEnabledForProject(projectId) && !getCurrentRunView();
|
|
357
|
+
if (!canRetry() || otStartRetryCount >= retryDelays.length) return;
|
|
358
|
+
// Use the existing owned timer so disabling OT, changing projects and
|
|
359
|
+
// starting a new observation all cancel pending retries normally.
|
|
360
|
+
const delayMs = retryDelays[otStartRetryCount++];
|
|
361
|
+
otWarmMirrorState.pollTimer = window.setTimeout(() => {
|
|
362
|
+
otWarmMirrorState.pollTimer = null;
|
|
363
|
+
if (!canRetry()) return;
|
|
364
|
+
syncOtWarmMirrorController().catch(() => {
|
|
365
|
+
if (getCurrentProjectId() === projectId && isExperimentalOtEnabledForProject(projectId)) {
|
|
366
|
+
updateOtStatusDisplay('unavailable');
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
}, delayMs);
|
|
329
370
|
}
|
|
330
371
|
|
|
331
372
|
function readOtBridgeStatus(response) {
|
|
332
|
-
|
|
333
|
-
return 'unavailable';
|
|
334
|
-
}
|
|
335
|
-
return response.getState() || response.status || response.result?.getState() || response.result?.status || 'unavailable';
|
|
373
|
+
return otWarmMirrorController.readOtBridgeStatus(response);
|
|
336
374
|
}
|
|
337
375
|
|
|
338
376
|
function ensureOtWarmMirrorStateProject(projectId) {
|
|
@@ -341,8 +379,12 @@
|
|
|
341
379
|
}
|
|
342
380
|
clearOtEventPolling({ clearPatchQueue: true });
|
|
343
381
|
otWarmMirrorState.projectId = projectId;
|
|
382
|
+
otStartRetryCount = 0;
|
|
344
383
|
otWarmMirrorState.lastPatchAt = 0;
|
|
345
384
|
otWarmMirrorState.lastErrorCode = '';
|
|
385
|
+
otWarmMirrorState.failureActive = false;
|
|
386
|
+
otWarmMirrorState.lastFailure = null;
|
|
387
|
+
otWarmMirrorState.suspended = false;
|
|
346
388
|
otWarmMirrorState.lastStatus = currentOtStatus;
|
|
347
389
|
}
|
|
348
390
|
|
|
@@ -386,7 +428,8 @@
|
|
|
386
428
|
&& otWarmMirrorController?.buildPatchFilesRequest
|
|
387
429
|
&& isExperimentalOtEnabledForProject(projectId)
|
|
388
430
|
&& otWarmMirrorProjectId === projectId
|
|
389
|
-
&& getCurrentProjectId() === projectId
|
|
431
|
+
&& getCurrentProjectId() === projectId
|
|
432
|
+
&& !otWarmMirrorState.suspended);
|
|
390
433
|
}
|
|
391
434
|
|
|
392
435
|
async function pollOtEvents(projectId = otWarmMirrorState.projectId || getCurrentProjectId()) {
|
|
@@ -409,11 +452,13 @@
|
|
|
409
452
|
return { ok: false, skipped: true, reason: 'project_changed' };
|
|
410
453
|
}
|
|
411
454
|
if (!isSuccessfulOtBridgeResponse(statusResponse)) {
|
|
412
|
-
|
|
455
|
+
recordOtFailure(readOtBridgeErrorCode(statusResponse, 'get_ot_status_failed'), 'observe');
|
|
413
456
|
updateOtStatusDisplay('unavailable');
|
|
414
457
|
return statusResponse;
|
|
415
458
|
}
|
|
416
459
|
const status = readOtBridgeStatus(statusResponse);
|
|
460
|
+
if (status === 'observing' && otWarmMirrorState.lastFailure?.phase === 'start') Object.assign(otWarmMirrorState, { failureActive: false, lastErrorCode: '', lastFailure: null });
|
|
461
|
+
if (['unavailable', 'inconsistent'].includes(status)) recordOtFailure(readOtBridgeErrorCode(statusResponse, 'ot_observer_unavailable'), 'observe');
|
|
417
462
|
if (status) {
|
|
418
463
|
updateOtStatusDisplay(status);
|
|
419
464
|
}
|
|
@@ -423,7 +468,7 @@
|
|
|
423
468
|
return { ok: false, skipped: true, reason: 'project_changed' };
|
|
424
469
|
}
|
|
425
470
|
if (!isSuccessfulOtBridgeResponse(drainResponse)) {
|
|
426
|
-
|
|
471
|
+
recordOtFailure(readOtBridgeErrorCode(drainResponse, 'drain_ot_events_failed'), 'observe');
|
|
427
472
|
updateOtStatusDisplay('unavailable');
|
|
428
473
|
return drainResponse;
|
|
429
474
|
}
|
|
@@ -433,7 +478,8 @@
|
|
|
433
478
|
}
|
|
434
479
|
return drainResponse;
|
|
435
480
|
} catch (error) {
|
|
436
|
-
|
|
481
|
+
if (!canPollOtWarmMirror(projectId)) return { ok: false, skipped: true, reason: 'project_changed' };
|
|
482
|
+
recordOtFailure(error?.code || error?.message || 'ot_poll_failed', 'observe');
|
|
437
483
|
updateOtStatusDisplay('unavailable');
|
|
438
484
|
return { ok: false, error };
|
|
439
485
|
} finally {
|
|
@@ -444,25 +490,16 @@
|
|
|
444
490
|
}
|
|
445
491
|
|
|
446
492
|
function readOtBridgeEvents(response) {
|
|
447
|
-
|
|
448
|
-
return response;
|
|
449
|
-
}
|
|
450
|
-
if (Array.isArray(response?.events)) {
|
|
451
|
-
return response.events;
|
|
452
|
-
}
|
|
453
|
-
if (Array.isArray(response?.result?.events)) {
|
|
454
|
-
return response.result.events;
|
|
455
|
-
}
|
|
456
|
-
return [];
|
|
493
|
+
return otWarmMirrorController.readOtBridgeEvents(response);
|
|
457
494
|
}
|
|
458
|
-
|
|
459
495
|
function readOtBridgeErrorCode(response, fallback) {
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
496
|
+
return otWarmMirrorController.readOtBridgeErrorCode(response, fallback);
|
|
497
|
+
}
|
|
498
|
+
function recordOtFailure(code, phase) {
|
|
499
|
+
code = otWarmMirrorController.normalizeFailureCode(code);
|
|
500
|
+
otWarmMirrorState.lastErrorCode = code;
|
|
501
|
+
otWarmMirrorState.failureActive = true;
|
|
502
|
+
otWarmMirrorState.lastFailure = { code, phase, at: Date.now() };
|
|
466
503
|
}
|
|
467
504
|
|
|
468
505
|
function queueOtPatchEvents(events, projectId = getCurrentProjectId()) {
|
|
@@ -470,7 +507,16 @@
|
|
|
470
507
|
return;
|
|
471
508
|
}
|
|
472
509
|
ensureOtWarmMirrorStateProject(projectId);
|
|
473
|
-
otWarmMirrorState.patchQueue
|
|
510
|
+
const queued = otWarmMirrorController.queuePatchEvents(otWarmMirrorState.patchQueue, events);
|
|
511
|
+
if (!queued.ok) {
|
|
512
|
+
recordOtFailure(queued.reason, 'queue');
|
|
513
|
+
clearOtEventPolling({ clearPatchQueue: true });
|
|
514
|
+
otWarmMirrorState.suspended = true;
|
|
515
|
+
updateOtStatusDisplay('inconsistent');
|
|
516
|
+
void callPageBridge('stopOtObserver', { projectId }).catch(() => {});
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
otWarmMirrorState.patchQueue = queued.queue;
|
|
474
520
|
scheduleOtPatchFlush(projectId);
|
|
475
521
|
}
|
|
476
522
|
|
|
@@ -510,11 +556,18 @@
|
|
|
510
556
|
return { ok: false, skipped: true, reason: pause.reason || 'paused' };
|
|
511
557
|
}
|
|
512
558
|
|
|
513
|
-
|
|
514
|
-
const batch = otWarmMirrorState.patchQueue.splice(0, maxBatch);
|
|
559
|
+
let batch = otWarmMirrorController.takePatchBatch(otWarmMirrorState.patchQueue);
|
|
515
560
|
if (!batch.length) {
|
|
516
561
|
return { ok: true, skipped: true, reason: 'empty_batch' };
|
|
517
562
|
}
|
|
563
|
+
const fresh = otWarmMirrorController.queuePatchEvents([], batch);
|
|
564
|
+
if (!fresh.ok) {
|
|
565
|
+
recordOtFailure(fresh.reason, 'queue');
|
|
566
|
+
clearOtEventPolling({ clearPatchQueue: true });
|
|
567
|
+
updateOtStatusDisplay('inconsistent');
|
|
568
|
+
return { ok: false, skipped: true, reason: fresh.reason };
|
|
569
|
+
}
|
|
570
|
+
batch = fresh.queue;
|
|
518
571
|
|
|
519
572
|
otWarmMirrorState.flushing = true;
|
|
520
573
|
try {
|
|
@@ -530,46 +583,37 @@
|
|
|
530
583
|
}
|
|
531
584
|
|
|
532
585
|
const response = await sendBackgroundNative(request);
|
|
586
|
+
if (!canPollOtWarmMirror(projectId)) return { ok: false, skipped: true, reason: 'project_changed' };
|
|
533
587
|
if (!response?.ok) {
|
|
534
|
-
|
|
588
|
+
recordOtFailure(response?.error?.code || 'mirror_patch_failed', 'flush');
|
|
535
589
|
updateOtStatusDisplay('inconsistent');
|
|
536
590
|
return response;
|
|
537
591
|
}
|
|
538
592
|
|
|
539
|
-
const
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
? Number(result.skippedCount)
|
|
543
|
-
: skippedFiles.length;
|
|
544
|
-
if (skippedFiles.length || skippedCount > 0) {
|
|
545
|
-
otWarmMirrorState.lastErrorCode = 'mirror_patch_skipped';
|
|
546
|
-
updateOtStatusDisplay('inconsistent');
|
|
547
|
-
return response;
|
|
548
|
-
}
|
|
549
|
-
const appliedFiles = Array.isArray(result?.appliedFiles) ? result.appliedFiles : null;
|
|
550
|
-
const appliedCount = Number.isFinite(Number(result?.appliedCount))
|
|
551
|
-
? Number(result.appliedCount)
|
|
552
|
-
: NaN;
|
|
553
|
-
if (!appliedFiles || !Number.isFinite(appliedCount) || appliedCount <= 0 || appliedFiles.length !== appliedCount) {
|
|
554
|
-
otWarmMirrorState.lastErrorCode = 'mirror_patch_invalid_result';
|
|
593
|
+
const receipt = otWarmMirrorController.validatePatchReceipt(batch, response.result);
|
|
594
|
+
if (!receipt.ok) {
|
|
595
|
+
recordOtFailure(receipt.reason, 'flush');
|
|
555
596
|
updateOtStatusDisplay('inconsistent');
|
|
556
597
|
return response;
|
|
557
598
|
}
|
|
558
599
|
|
|
559
600
|
otWarmMirrorState.lastPatchAt = Date.now();
|
|
560
601
|
otWarmMirrorState.lastErrorCode = '';
|
|
561
|
-
|
|
602
|
+
otWarmMirrorState.failureActive = false;
|
|
603
|
+
if (canPollOtWarmMirror(projectId) && !getCurrentRunView()) {
|
|
562
604
|
updateOtStatusDisplay('observing');
|
|
563
605
|
}
|
|
564
606
|
return response;
|
|
565
607
|
} catch (error) {
|
|
566
|
-
|
|
608
|
+
if (!canPollOtWarmMirror(projectId)) return { ok: false, skipped: true, reason: 'project_changed' };
|
|
609
|
+
recordOtFailure(error?.code || 'mirror_patch_failed', 'flush');
|
|
567
610
|
updateOtStatusDisplay('inconsistent');
|
|
568
611
|
return { ok: false, error };
|
|
569
612
|
} finally {
|
|
570
613
|
otWarmMirrorState.flushing = false;
|
|
571
|
-
|
|
572
|
-
|
|
614
|
+
const nextProjectId = otWarmMirrorState.projectId;
|
|
615
|
+
if (otWarmMirrorState.patchQueue.length && canPollOtWarmMirror(nextProjectId) && !getCurrentRunView()) {
|
|
616
|
+
scheduleOtPatchFlush(nextProjectId, { immediate: true });
|
|
573
617
|
}
|
|
574
618
|
}
|
|
575
619
|
}
|
|
@@ -612,12 +656,13 @@
|
|
|
612
656
|
}
|
|
613
657
|
|
|
614
658
|
function updateOtStatusDisplay(status = currentOtStatus) {
|
|
615
|
-
const normalized = normalizeOtStatus(status);
|
|
659
|
+
const normalized = otWarmMirrorState.failureActive && status === 'observing' ? 'inconsistent' : normalizeOtStatus(status);
|
|
616
660
|
currentOtStatus = normalized;
|
|
617
661
|
otWarmMirrorState.lastStatus = normalized;
|
|
618
662
|
const statusElement = getPanel()?.querySelector('[data-ot-status]');
|
|
619
663
|
if (statusElement) {
|
|
620
|
-
const label = formatOtStatusLabel(normalized)
|
|
664
|
+
const label = formatOtStatusLabel(normalized) + (otWarmMirrorState.failureActive
|
|
665
|
+
? ': ' + tr(otWarmMirrorController.getFailureMessageKey(otWarmMirrorState.lastErrorCode)) : '');
|
|
621
666
|
statusElement.textContent = formatOtToggleStatusText(label);
|
|
622
667
|
statusElement.dataset.otStatus = normalized;
|
|
623
668
|
statusElement.title = formatOtToggleStatusText(label);
|
|
@@ -781,38 +826,24 @@
|
|
|
781
826
|
}
|
|
782
827
|
|
|
783
828
|
async function resolveWarmRunStart(taskContext = {}) {
|
|
829
|
+
const projectId = getCurrentProjectId();
|
|
784
830
|
const focusFiles = taskContext.focusFiles || [];
|
|
785
831
|
const mode = taskContext.mode || getState()?.mode;
|
|
786
832
|
const mirrorStatus = await getMirrorFreshness();
|
|
833
|
+
if (getCurrentProjectId() !== projectId) return { useExistingMirror: false, reason: 'project_changed' };
|
|
787
834
|
const otWarmStart = otWarmMirrorController.canUseOtWarmStart({
|
|
788
|
-
enabled: isExperimentalOtEnabled(),
|
|
835
|
+
enabled: isExperimentalOtEnabled() && !otWarmMirrorState.failureActive,
|
|
789
836
|
focusFiles,
|
|
790
837
|
mirrorStatus
|
|
791
838
|
});
|
|
792
|
-
if (otWarmStart.ok) {
|
|
793
|
-
return {
|
|
794
|
-
useExistingMirror: true,
|
|
795
|
-
warmStart: true,
|
|
796
|
-
otWarmStart: true,
|
|
797
|
-
reason: 'ot_focus_fresh',
|
|
798
|
-
mirrorStatus,
|
|
799
|
-
focusFiles: otWarmStart.focusFiles,
|
|
800
|
-
project: {
|
|
801
|
-
capabilities: {
|
|
802
|
-
fullProjectSnapshot: false,
|
|
803
|
-
method: 'ot-warm-mirror'
|
|
804
|
-
},
|
|
805
|
-
files: []
|
|
806
|
-
}
|
|
807
|
-
};
|
|
808
|
-
}
|
|
809
|
-
if (!mirrorStatus?.exists || !isMirrorReusable(mirrorStatus)) {
|
|
839
|
+
if (!mirrorStatus?.exists || (!otWarmStart.ok && !isMirrorReusable(mirrorStatus))) {
|
|
810
840
|
return { useExistingMirror: false, reason: 'mirror_not_fresh', mirrorStatus };
|
|
811
841
|
}
|
|
812
842
|
|
|
813
843
|
let overlayProject = null;
|
|
814
844
|
try {
|
|
815
845
|
overlayProject = await callPageBridge('getProjectSnapshot', {
|
|
846
|
+
projectId,
|
|
816
847
|
force: true,
|
|
817
848
|
maxAgeMs: 0,
|
|
818
849
|
preferLightweight: true,
|
|
@@ -826,6 +857,7 @@
|
|
|
826
857
|
} catch (error) {
|
|
827
858
|
return { useExistingMirror: false, reason: 'overlay_probe_failed', mirrorStatus };
|
|
828
859
|
}
|
|
860
|
+
if (getCurrentProjectId() !== projectId) return { useExistingMirror: false, reason: 'project_changed' };
|
|
829
861
|
const fileOverlays = buildSnapshotFileOverlays(overlayProject, focusFiles);
|
|
830
862
|
if (!fileOverlays.length) {
|
|
831
863
|
return { useExistingMirror: false, reason: 'missing_current_overlay', mirrorStatus };
|
|
@@ -840,7 +872,9 @@
|
|
|
840
872
|
return {
|
|
841
873
|
useExistingMirror: true,
|
|
842
874
|
warmStart: true,
|
|
843
|
-
|
|
875
|
+
otWarmStart: otWarmStart.ok,
|
|
876
|
+
focusFiles,
|
|
877
|
+
reason: otWarmStart.ok ? 'ot_focus_fresh' : mode === 'ask' ? 'warm_whole_project_ask' : 'warm_whole_project',
|
|
844
878
|
mirrorStatus,
|
|
845
879
|
fileOverlays,
|
|
846
880
|
project: overlayProject
|
|
@@ -928,7 +962,7 @@
|
|
|
928
962
|
return syncOtWarmMirrorController();
|
|
929
963
|
}).catch(error => {
|
|
930
964
|
updateOtStatusDisplay('unavailable');
|
|
931
|
-
appendPlainLog(tx(`
|
|
965
|
+
appendPlainLog(tx(`OT warm mirror unavailable: ${error.message}`, `OT 预热镜像不可用:${error.message}`));
|
|
932
966
|
});
|
|
933
967
|
}
|
|
934
968
|
|
|
@@ -10,6 +10,12 @@
|
|
|
10
10
|
const OT_POLL_INTERVAL_MS = 1000;
|
|
11
11
|
const OT_PATCH_DEBOUNCE_MS = 500;
|
|
12
12
|
const OT_MAX_PATCH_BATCH = 25;
|
|
13
|
+
const OT_FRESHNESS_MAX_AGE_MS = 30000;
|
|
14
|
+
const OT_MAX_FILE_BYTES = 1024 * 1024;
|
|
15
|
+
const OT_MAX_PENDING_EVENTS = 64;
|
|
16
|
+
const OT_MAX_PENDING_BYTES = 4 * 1024 * 1024;
|
|
17
|
+
const OT_MAX_BATCH_BYTES = 2 * 1024 * 1024;
|
|
18
|
+
const encoder = typeof TextEncoder === 'function' ? new TextEncoder() : null;
|
|
13
19
|
const PAUSE_STATES = new Set(['running', 'writing', 'undoing', 'compiling']);
|
|
14
20
|
|
|
15
21
|
function buildPatchFilesRequest({ projectId, events, nativeCompatibility } = {}) {
|
|
@@ -67,13 +73,14 @@
|
|
|
67
73
|
return { pause: false };
|
|
68
74
|
}
|
|
69
75
|
|
|
70
|
-
function canUseOtWarmStart({ enabled, focusFiles, mirrorStatus } = {}) {
|
|
76
|
+
function canUseOtWarmStart({ enabled, focusFiles, mirrorStatus, now = Date.now() } = {}) {
|
|
71
77
|
if (!enabled) {
|
|
72
78
|
return { ok: false, reason: 'disabled' };
|
|
73
79
|
}
|
|
74
80
|
if (!mirrorStatus || mirrorStatus.exists !== true) {
|
|
75
81
|
return { ok: false, reason: 'mirror_missing' };
|
|
76
82
|
}
|
|
83
|
+
if (mirrorStatus.dirty === true) return { ok: false, reason: 'mirror_dirty' };
|
|
77
84
|
|
|
78
85
|
const normalizedFocusFiles = normalizePaths(focusFiles);
|
|
79
86
|
if (!normalizedFocusFiles.length) {
|
|
@@ -85,6 +92,8 @@
|
|
|
85
92
|
if (file?.state !== 'fresh') {
|
|
86
93
|
continue;
|
|
87
94
|
}
|
|
95
|
+
const age = now - Date.parse(file.observedAt ?? file.lastPatchAt ?? '');
|
|
96
|
+
if (!Number.isFinite(age) || age < 0 || age > OT_FRESHNESS_MAX_AGE_MS) continue;
|
|
88
97
|
const filePath = normalizePath(file.path);
|
|
89
98
|
if (filePath) {
|
|
90
99
|
freshFiles.add(filePath);
|
|
@@ -102,6 +111,105 @@
|
|
|
102
111
|
return { ok: false, reason: 'missing_ot_fresh_focus' };
|
|
103
112
|
}
|
|
104
113
|
|
|
114
|
+
function textBytes(value) {
|
|
115
|
+
const text = String(value || '');
|
|
116
|
+
return encoder ? encoder.encode(text).byteLength : text.length * 3;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function patchBytes(event) { return textBytes(event.nextContent) + textBytes(event.path) + 512; }
|
|
120
|
+
|
|
121
|
+
function queuePatchEvents(queue, events, now = Date.now()) {
|
|
122
|
+
if (!Array.isArray(events) || events.length > OT_MAX_PENDING_EVENTS) return { ok: false, reason: 'ot_queue_limit' };
|
|
123
|
+
const next = Array.isArray(queue) ? queue.slice() : [];
|
|
124
|
+
let bytes = next.reduce((sum, event) => sum + patchBytes(event), 0);
|
|
125
|
+
if (next.length > OT_MAX_PENDING_EVENTS || bytes > OT_MAX_PENDING_BYTES) return { ok: false, reason: 'ot_queue_limit' };
|
|
126
|
+
for (const input of events) {
|
|
127
|
+
const path = normalizePath(input?.path);
|
|
128
|
+
const time = typeof input?.observedAt === 'number' ? input.observedAt : Date.parse(input?.observedAt || '');
|
|
129
|
+
if (!isSafeRelativePath(path) || path.length > 1024 || typeof input?.nextContent !== 'string' ||
|
|
130
|
+
typeof input.baseHash !== 'string' || !input.baseHash || input.baseHash.length > 128) {
|
|
131
|
+
return { ok: false, reason: 'ot_event_invalid' };
|
|
132
|
+
}
|
|
133
|
+
if (!Number.isFinite(time) || now - time < 0 || now - time > OT_FRESHNESS_MAX_AGE_MS) return { ok: false, reason: 'ot_event_expired' };
|
|
134
|
+
if (textBytes(input.nextContent) > OT_MAX_FILE_BYTES) return { ok: false, reason: 'ot_file_limit' };
|
|
135
|
+
const event = { path, baseHash: input.baseHash, nextContent: input.nextContent,
|
|
136
|
+
nextHash: typeof input.nextHash === 'string' && input.nextHash.length <= 128 ? input.nextHash : '',
|
|
137
|
+
observedAt: new Date(time).toISOString() };
|
|
138
|
+
if (isPrimitiveMetadata(input.observedVersion) && String(input.observedVersion).length <= 128) event.observedVersion = input.observedVersion;
|
|
139
|
+
const tail = next[next.length - 1];
|
|
140
|
+
if (tail?.path === path && tail.nextHash && tail.nextHash === event.baseHash) {
|
|
141
|
+
event.baseHash = tail.baseHash;
|
|
142
|
+
bytes -= patchBytes(next.pop());
|
|
143
|
+
}
|
|
144
|
+
if (event.nextHash !== event.baseHash) { next.push(event); bytes += patchBytes(event); }
|
|
145
|
+
if (next.length > OT_MAX_PENDING_EVENTS || bytes > OT_MAX_PENDING_BYTES) return { ok: false, reason: 'ot_queue_limit' };
|
|
146
|
+
}
|
|
147
|
+
return { ok: true, queue: next, bytes };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function takePatchBatch(queue) {
|
|
151
|
+
let bytes = 0;
|
|
152
|
+
let count = 0;
|
|
153
|
+
while (count < Math.min(queue.length, OT_MAX_PATCH_BATCH)) {
|
|
154
|
+
const size = patchBytes(queue[count]);
|
|
155
|
+
if (bytes + size > OT_MAX_BATCH_BYTES) break;
|
|
156
|
+
bytes += size;
|
|
157
|
+
count += 1;
|
|
158
|
+
}
|
|
159
|
+
return queue.splice(0, count);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function validatePatchReceipt(events, result = {}, now = Date.now()) {
|
|
163
|
+
if ((Array.isArray(result.skippedFiles) && result.skippedFiles.length) || Number(result.skippedCount || 0) > 0) {
|
|
164
|
+
return { ok: false, reason: 'mirror_patch_skipped' };
|
|
165
|
+
}
|
|
166
|
+
if (!events.length || Number(result.appliedCount) !== events.length || !Array.isArray(result.appliedFiles) ||
|
|
167
|
+
result.appliedFiles.length !== events.length || !result.appliedFiles.every((file, index) =>
|
|
168
|
+
normalizePath(file?.path) === events[index].path && (!events[index].nextHash || file.hash === events[index].nextHash))) {
|
|
169
|
+
return { ok: false, reason: 'mirror_patch_invalid_result' };
|
|
170
|
+
}
|
|
171
|
+
if (events.some(event => { const time = Date.parse(event.observedAt);
|
|
172
|
+
return !Number.isFinite(time) || time > now || now - time > OT_FRESHNESS_MAX_AGE_MS;
|
|
173
|
+
})) return { ok: false, reason: 'ot_event_expired' };
|
|
174
|
+
return { ok: true };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function readOtBridgeStatus(response) {
|
|
178
|
+
if (!response || response.ok === false) return 'unavailable';
|
|
179
|
+
return response.state || response.status || response.result?.state || response.result?.status || 'unavailable';
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function readOtBridgeEvents(response) {
|
|
183
|
+
if (Array.isArray(response)) return response;
|
|
184
|
+
if (Array.isArray(response?.events)) return response.events;
|
|
185
|
+
if (Array.isArray(response?.result?.events)) return response.result.events;
|
|
186
|
+
return [];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function readOtBridgeErrorCode(response, fallback) {
|
|
190
|
+
const value = response?.lastErrorCode || response?.reason || response?.error?.code || response?.error || fallback;
|
|
191
|
+
return typeof value === 'string' ? value : fallback;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function normalizeFailureCode(value, fallback = 'ot_error') {
|
|
195
|
+
const code = typeof value === 'string' ? value : value?.code;
|
|
196
|
+
if (/^[a-z][a-z0-9_]{0,79}$/.test(code || '')) return code;
|
|
197
|
+
if (/page bridge|capability|bridge timed out/i.test(String(code || ''))) return 'ot_bridge_unavailable';
|
|
198
|
+
return fallback;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function getFailureMessageKey(value) {
|
|
202
|
+
const code = normalizeFailureCode(value);
|
|
203
|
+
if (/bridge|ot_status|ot_events/.test(code)) return 'otFailureBridge';
|
|
204
|
+
if (/missing_ot_text|observer_create|observer_unavailable/.test(code)) return 'otFailureObserver';
|
|
205
|
+
if (code === 'ot_file_limit') return 'otFailureFileLimit';
|
|
206
|
+
if (code === 'ot_queue_limit') return 'otFailureQueueLimit';
|
|
207
|
+
if (code === 'ot_event_expired') return 'otFailureExpired';
|
|
208
|
+
if (/editor|active_path|document/.test(code)) return 'otFailureEditor';
|
|
209
|
+
if (/mirror|hash|baseline/.test(code)) return 'otFailureMirror';
|
|
210
|
+
return 'otFailureGeneric';
|
|
211
|
+
}
|
|
212
|
+
|
|
105
213
|
function normalizePath(value) {
|
|
106
214
|
return String(value ?? '')
|
|
107
215
|
.trim()
|
|
@@ -165,6 +273,19 @@
|
|
|
165
273
|
OT_POLL_INTERVAL_MS,
|
|
166
274
|
OT_PATCH_DEBOUNCE_MS,
|
|
167
275
|
OT_MAX_PATCH_BATCH,
|
|
276
|
+
OT_FRESHNESS_MAX_AGE_MS,
|
|
277
|
+
OT_MAX_FILE_BYTES,
|
|
278
|
+
OT_MAX_PENDING_EVENTS,
|
|
279
|
+
OT_MAX_PENDING_BYTES,
|
|
280
|
+
OT_MAX_BATCH_BYTES,
|
|
281
|
+
queuePatchEvents,
|
|
282
|
+
takePatchBatch,
|
|
283
|
+
validatePatchReceipt,
|
|
284
|
+
normalizeFailureCode,
|
|
285
|
+
readOtBridgeStatus,
|
|
286
|
+
readOtBridgeEvents,
|
|
287
|
+
readOtBridgeErrorCode,
|
|
288
|
+
getFailureMessageKey,
|
|
168
289
|
buildPatchFilesRequest,
|
|
169
290
|
canUseOtWarmStart,
|
|
170
291
|
normalizePath,
|
|
@@ -21,9 +21,11 @@
|
|
|
21
21
|
['src/page/overleafProjectSnapshot.js', 'codex-overleaf-project-snapshot-script'],
|
|
22
22
|
['src/page/pageBridgeCapability.js', 'codex-overleaf-page-bridge-capability-script'],
|
|
23
23
|
['src/page/treeOperations.js', 'codex-overleaf-tree-operations-script', true],
|
|
24
|
+
['src/page/textFileCreator.js', 'codex-overleaf-text-file-creator-script', true],
|
|
24
25
|
['src/page/binaryAssetUploader.js', 'codex-overleaf-binary-asset-uploader-script', true],
|
|
25
26
|
['src/page/snapshotRouter.js', 'codex-overleaf-snapshot-router-script'],
|
|
26
27
|
['src/page/writeGuard.js', 'codex-overleaf-write-guard-script', true],
|
|
28
|
+
['src/page/trackedChangeCapture.js', 'codex-overleaf-tracked-change-capture-script', true],
|
|
27
29
|
['src/page/trackedChangesLifecycle.js', 'codex-overleaf-tracked-changes-lifecycle-script', true],
|
|
28
30
|
['src/page/writebackRouter.js', 'codex-overleaf-writeback-router-script', true],
|
|
29
31
|
['src/pageBridge.js', 'codex-overleaf-page-bridge-script', true]
|