codex-overleaf-link 2.5.0 → 2.5.1
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 +38 -26
- package/README.zh-CN.md +39 -27
- package/extension/bootstrap/background.js +52 -9
- package/extension/bootstrap/manifest.template.json +1 -1
- package/extension/bootstrap/popup.html +1 -0
- package/extension/bootstrap/popupFallback.js +55 -0
- package/extension/bootstrap/updateStatus.js +1 -0
- package/extension/src/background.js +206 -53
- package/extension/src/backgroundBootstrapMigration.js +181 -0
- package/extension/src/backgroundSharedSessions.js +74 -14
- package/extension/src/backgroundUpdateCoordinator.js +118 -21
- package/extension/src/backgroundUpdateSafety.js +173 -0
- package/extension/src/content/activeTurnControl.js +8 -2
- package/extension/src/content/applyResultFormatters.js +46 -73
- package/extension/src/content/assetTransferBroker.js +72 -14
- package/extension/src/content/composerAttachments.js +93 -45
- package/extension/src/content/composerPanel.js +18 -0
- package/extension/src/content/contentRuntime.js +661 -676
- package/extension/src/content/diagnosticsController.js +122 -96
- package/extension/src/content/diagnosticsExport.js +19 -5
- package/extension/src/content/diagnosticsPanel.js +30 -0
- package/extension/src/content/directoryCleanupRecovery.js +54 -0
- package/extension/src/content/generated/content.bundle.js +6879 -2786
- package/extension/src/content/generated/content.bundle.meta.json +11 -3
- package/extension/src/content/globalPreferencesController.js +1 -1
- package/extension/src/content/legacyGlobalRegistry.js +6 -0
- package/extension/src/content/localSkillsPanel.js +102 -12
- package/extension/src/content/markdownText.js +5 -6
- package/extension/src/content/modelPicker.js +7 -4
- package/extension/src/content/nativeCompatibilityController.js +45 -20
- package/extension/src/content/otWarmMirror.js +4 -5
- package/extension/src/content/otWarmMirrorController.js +17 -20
- package/extension/src/content/pageBridgeClient.js +15 -2
- package/extension/src/content/panelMaintenance.js +40 -12
- package/extension/src/content/panelRenderer.js +6 -0
- package/extension/src/content/projectProviderSelection.js +13 -4
- package/extension/src/content/projectSessionCleanup.js +109 -62
- package/extension/src/content/projectSettingsCoordinator.js +39 -2
- package/extension/src/content/providerSettingsCoordinator.js +46 -8
- package/extension/src/content/providerSettingsDialog.js +71 -13
- package/extension/src/content/recentProjects.js +11 -5
- package/extension/src/content/runActivitySummary.js +40 -6
- package/extension/src/content/runCompletionMeta.js +344 -0
- package/extension/src/content/runController.js +37 -14
- package/extension/src/content/runFailureNotice.js +21 -12
- package/extension/src/content/runGuidanceController.js +72 -27
- package/extension/src/content/runGuidanceView.js +59 -10
- package/extension/src/content/runQueueScheduler.js +148 -42
- package/extension/src/content/runRecoveryActions.js +146 -0
- package/extension/src/content/runResultActions.js +8 -125
- package/extension/src/content/runReviewController.js +574 -0
- package/extension/src/content/runScrollLayout.js +21 -2
- package/extension/src/content/runTimelineView.js +146 -153
- package/extension/src/content/scopedPersistencePanelState.js +51 -6
- package/extension/src/content/selectionContextView.js +1 -1
- package/extension/src/content/sensitivePreflight.js +140 -0
- package/extension/src/content/sessionForkController.js +80 -17
- package/extension/src/content/sessionManager.js +1 -1
- package/extension/src/content/settingsPanel.js +7 -3
- package/extension/src/content/settingsWorkbench.js +5 -0
- package/extension/src/content/sharedSessionViewSync.js +45 -8
- package/extension/src/content/updateIdle.js +4 -1
- package/extension/src/content/updateNotice.js +60 -8
- package/extension/src/content/updateSafetyReceiver.js +58 -0
- package/extension/src/content/writebackController.js +53 -4
- package/extension/src/content/writebackOrchestrator.js +82 -73
- package/extension/src/content/writingStyleSettings.js +35 -11
- package/extension/src/page/binaryAssetUploader.js +136 -14
- package/extension/src/page/fileMover.js +256 -0
- package/extension/src/page/ownedDirectoryCleanup.js +94 -0
- package/extension/src/page/referenceProjects.js +1 -0
- package/extension/src/page/reviewContext.js +113 -0
- package/extension/src/page/saveState.js +6 -4
- package/extension/src/page/snapshotRouter.js +84 -39
- package/extension/src/page/textFileCreator.js +105 -25
- package/extension/src/page/trackedChangeCapture.js +12 -2
- package/extension/src/page/trackedChangeOwnership.js +19 -1
- package/extension/src/page/trackedChangeReplay.js +41 -21
- package/extension/src/page/trackedChangesLifecycle.js +217 -199
- package/extension/src/page/treeOperations.js +7 -10
- package/extension/src/page/updateSafetyProbe.js +41 -0
- package/extension/src/page/writebackReceiptJournal.js +2 -0
- package/extension/src/page/writebackRouter.js +80 -79
- package/extension/src/pageBridge.js +19 -3
- package/extension/src/popup.js +62 -10
- package/extension/src/shared/agentTranscript.js +23 -0
- package/extension/src/shared/auditRecords.js +2 -0
- package/extension/src/shared/compatibility.js +22 -6
- package/extension/src/shared/compileAdapter.js +79 -57
- package/extension/src/shared/composerAttachmentDraft.js +151 -0
- package/extension/src/shared/directoryCleanupProof.js +43 -0
- package/extension/src/shared/failureReasons.js +309 -3
- package/extension/src/shared/globalPreferences.js +30 -13
- package/extension/src/shared/governanceRules.js +4 -37
- package/extension/src/shared/i18n.js +298 -2
- package/extension/src/shared/managedUpdateProjection.js +2 -2
- package/extension/src/shared/nativeRequestIdentity.js +1 -0
- package/extension/src/shared/pageRpcContract.js +5 -4
- package/extension/src/shared/projectFiles.js +21 -44
- package/extension/src/shared/providerProfiles.js +5 -3
- package/extension/src/shared/runInputQueue.js +48 -5
- package/extension/src/shared/selectionContext.js +12 -3
- package/extension/src/shared/sessionState.js +72 -27
- package/extension/src/shared/settlementFacts.js +10 -2
- package/extension/src/shared/sharedSessionBridge.js +37 -14
- package/extension/src/shared/sharedSessionTransfer.js +140 -0
- package/extension/src/shared/staleGuard.js +118 -13
- package/extension/src/shared/storageDb.js +58 -26
- package/extension/src/shared/storageMigration.js +71 -111
- package/extension/src/shared/storageRunActions.js +209 -26
- package/extension/src/shared/subagentActivityModel.js +1 -1
- package/extension/src/shared/undoOperations.js +136 -13
- package/extension/src/shared/updateConsent.js +6 -4
- package/extension/src/shared/updateRuntimeIdentity.js +1 -1
- package/extension/src/shared/writebackIntent.js +16 -0
- package/extension/src/shared/writebackOperationEvidence.js +4 -15
- package/extension/src/shared/writebackSettlement.js +38 -38
- package/extension/styles/panel.css +19 -5
- package/native-host/src/anthropicBridgeRequest.js +4 -2
- package/native-host/src/anthropicBridgeResponse.js +6 -2
- package/native-host/src/anthropicMessagesBridge.js +24 -16
- package/native-host/src/bootstrapMigration.js +296 -0
- package/native-host/src/bootstrapMigrationContract.js +101 -0
- package/native-host/src/bootstrapUpdate.js +233 -0
- package/native-host/src/bootstrapUpdateWatchdog.js +44 -0
- package/native-host/src/chatBridgeResponse.js +10 -0
- package/native-host/src/chatCompletionsBridge.js +21 -15
- package/native-host/src/codexConfigPaths.js +106 -0
- package/native-host/src/codexHome.js +66 -73
- package/native-host/src/codexHomeScope.js +124 -0
- package/native-host/src/codexInheritedAuth.js +90 -0
- package/native-host/src/codexLocalSkills.js +301 -0
- package/native-host/src/codexPromptAssembly.js +31 -12
- package/native-host/src/codexProviderTest.js +24 -26
- package/native-host/src/codexSessionRunner.js +110 -183
- package/native-host/src/codexSessionTiming.js +18 -8
- package/native-host/src/codexSkillIsolation.js +170 -0
- package/native-host/src/codexThreadFork.js +19 -3
- package/native-host/src/index.js +19 -7
- package/native-host/src/managedInstall.js +110 -35
- package/native-host/src/managedInstallRecovery.js +140 -0
- package/native-host/src/managedRuntimeEvidence.js +57 -0
- package/native-host/src/mirrorWorkspace.js +92 -22
- package/native-host/src/nativeDoctor.js +15 -0
- package/native-host/src/nativeHelperLifecycle.js +21 -0
- package/native-host/src/nativeHostRegistration.js +33 -0
- package/native-host/src/nativeRunLifecycle.js +71 -0
- package/native-host/src/processStopBarrier.js +42 -7
- package/native-host/src/projectPathPlatform.js +30 -0
- package/native-host/src/providerHttp.js +80 -0
- package/native-host/src/providerProfile.js +4 -2
- package/native-host/src/providerReasoning.js +2 -7
- package/native-host/src/providerRuntime.js +1 -0
- package/native-host/src/providerStore.js +72 -14
- package/native-host/src/responsesPassthroughBridge.js +266 -52
- package/native-host/src/runtimeInstaller.js +21 -1
- package/native-host/src/selectionScope.js +36 -8
- package/native-host/src/subagentAdmission.js +26 -0
- package/native-host/src/subagentBroker.js +79 -66
- package/native-host/src/subagentTelemetry.js +19 -10
- package/native-host/src/subagentWorkspacePath.js +5 -2
- package/native-host/src/taskRunnerRuntime.js +77 -54
- package/native-host/src/tomlStatements.js +140 -0
- package/native-host/src/updateManager.js +297 -39
- package/native-host/src/writingStyleRuntime.js +57 -29
- package/native-host/src/writingStyleStore.js +73 -0
- package/package.json +1 -1
- package/scripts/install-managed.mjs +1 -0
package/extension/src/popup.js
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
(function initPopup() {
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
+
// Keep only copy produced by this popup eligible for a language refresh.
|
|
5
|
+
const popupCopyPairs = new Map();
|
|
6
|
+
let newSiteCopyNodes = [];
|
|
7
|
+
|
|
4
8
|
const popupScriptUrl = document.currentScript?.src || chrome.runtime.getURL('src/popup.js');
|
|
5
9
|
ensureMinimalPopupShell();
|
|
6
10
|
const CodexOverleafCompatibility = window.CodexOverleafCompatibility;
|
|
7
|
-
const
|
|
11
|
+
const isManagedInstall = chrome.runtime.getManifest()?.background?.service_worker === 'bootstrap/background.js'
|
|
12
|
+
|| window.__CODEX_OVERLEAF_RUNTIME_PREFIX__ === 'runtime/';
|
|
13
|
+
let installIdentity = { managed: isManagedInstall };
|
|
14
|
+
const DEFAULT_INSTALL_COMMAND = isManagedInstall ? '' : CodexOverleafCompatibility?.buildInstallCommand?.(
|
|
8
15
|
undefined,
|
|
9
16
|
undefined,
|
|
10
17
|
getCurrentExtensionId()
|
|
@@ -71,6 +78,7 @@
|
|
|
71
78
|
});
|
|
72
79
|
|
|
73
80
|
copyInstallCommand.addEventListener('click', async () => {
|
|
81
|
+
if (!currentInstallCommand) return;
|
|
74
82
|
copyInstallCommand.disabled = true;
|
|
75
83
|
copyFeedback.hidden = true;
|
|
76
84
|
try {
|
|
@@ -81,7 +89,7 @@
|
|
|
81
89
|
copyFeedback.textContent = utx('Copy failed. Select the command above to copy it.', '复制失败,可选中上方命令复制。');
|
|
82
90
|
copyFeedback.hidden = false;
|
|
83
91
|
} finally {
|
|
84
|
-
copyInstallCommand.disabled =
|
|
92
|
+
copyInstallCommand.disabled = !currentInstallCommand;
|
|
85
93
|
}
|
|
86
94
|
});
|
|
87
95
|
|
|
@@ -157,6 +165,7 @@
|
|
|
157
165
|
const granted = await chrome.permissions.request({ origins: missing }).catch(() => false);
|
|
158
166
|
if (granted) section.remove();
|
|
159
167
|
});
|
|
168
|
+
newSiteCopyNodes = [title, detail, button];
|
|
160
169
|
section.append(title, detail, button);
|
|
161
170
|
document.getElementById('popup-shell')?.append(section);
|
|
162
171
|
}
|
|
@@ -172,10 +181,10 @@
|
|
|
172
181
|
connectionDetails.setAttribute('aria-label', utx('Version details', '版本信息'));
|
|
173
182
|
connectionToggle.title = utx('Show or hide version details', '展开或收起版本信息');
|
|
174
183
|
copyInstallCommand.textContent = utx('Copy install command', '复制安装命令');
|
|
184
|
+
[status, copyFeedback, ...newSiteCopyNodes].forEach(refreshPopupCopy);
|
|
175
185
|
if (lastCompatibility) {
|
|
176
186
|
setVersionStatus(lastCompatibility);
|
|
177
|
-
if (!nativeInstall.hidden)
|
|
178
|
-
lastCompatibility.status, getCompatibilityClassification(lastCompatibility));
|
|
187
|
+
if (!nativeInstall.hidden) showNativeInstallGuide(lastCompatibility);
|
|
179
188
|
}
|
|
180
189
|
}
|
|
181
190
|
|
|
@@ -217,6 +226,7 @@
|
|
|
217
226
|
}
|
|
218
227
|
|
|
219
228
|
async function checkNativeHost() {
|
|
229
|
+
await readInstallIdentity();
|
|
220
230
|
try {
|
|
221
231
|
const response = await chrome.runtime.sendMessage({
|
|
222
232
|
type: 'codex-overleaf/native-request',
|
|
@@ -244,11 +254,29 @@
|
|
|
244
254
|
}
|
|
245
255
|
}
|
|
246
256
|
|
|
257
|
+
async function readInstallIdentity() {
|
|
258
|
+
if (!isManagedInstall) return;
|
|
259
|
+
try {
|
|
260
|
+
const response = await fetch(chrome.runtime.getURL('.codex-overleaf-managed-extension.json'));
|
|
261
|
+
const marker = response.ok ? await response.json() : null;
|
|
262
|
+
if (marker?.managedBy === 'codex-overleaf-link' && marker.kind === 'extension'
|
|
263
|
+
&& marker.installIdentity?.extensionId === getCurrentExtensionId()) {
|
|
264
|
+
installIdentity = { ...marker.installIdentity, managed: true };
|
|
265
|
+
}
|
|
266
|
+
} catch (_) { /* Older markers cannot prove the original root or browser. */ }
|
|
267
|
+
}
|
|
268
|
+
|
|
247
269
|
function showNativeInstallGuide(compatibility = {}) {
|
|
248
270
|
nativeInstall.hidden = false;
|
|
249
271
|
currentInstallCommand = getCompatibilityUpdateCommand(compatibility);
|
|
250
272
|
installCommand.textContent = currentInstallCommand;
|
|
251
|
-
|
|
273
|
+
copyInstallCommand.disabled = !currentInstallCommand;
|
|
274
|
+
nativeStatusMessage.textContent = !currentInstallCommand && isManagedInstall
|
|
275
|
+
? utx('Repair this Managed installation using its original managed root, browser and extension ID. Rerun the original install-managed command with those parameters.',
|
|
276
|
+
'修复此托管安装需要原管理根目录、浏览器和扩展 ID。请使用这些原参数重新运行 install-managed 安装命令。')
|
|
277
|
+
: getNativeStatusMessage(compatibility.status, getCompatibilityClassification(compatibility))
|
|
278
|
+
+ ((compatibility.native?.platform || installIdentity.platform) === 'win32' && isManagedInstall
|
|
279
|
+
? utx(' Run the command in PowerShell.', ' 请在 PowerShell 中运行该命令。') : '');
|
|
252
280
|
}
|
|
253
281
|
|
|
254
282
|
function evaluateNativeCompatibility(response) {
|
|
@@ -303,7 +331,8 @@
|
|
|
303
331
|
version: CodexOverleafCompatibility?.BUILD_TARGET_VERSION ||
|
|
304
332
|
chrome.runtime.getManifest?.().version ||
|
|
305
333
|
'',
|
|
306
|
-
extensionId: getCurrentExtensionId()
|
|
334
|
+
extensionId: getCurrentExtensionId(),
|
|
335
|
+
...(isManagedInstall ? { installIdentity } : {})
|
|
307
336
|
};
|
|
308
337
|
}
|
|
309
338
|
|
|
@@ -331,8 +360,9 @@
|
|
|
331
360
|
return CodexOverleafCompatibility?.buildInstallCommand?.(
|
|
332
361
|
compatibility.recommendedVersion || CodexOverleafCompatibility.BUILD_TARGET_VERSION,
|
|
333
362
|
compatibility.native?.platform || compatibility.platform,
|
|
334
|
-
getCurrentExtensionId()
|
|
335
|
-
|
|
363
|
+
getCurrentExtensionId(),
|
|
364
|
+
installIdentity
|
|
365
|
+
) || (isManagedInstall ? '' : compatibility.updateCommand || compatibility.installCommand || DEFAULT_INSTALL_COMMAND);
|
|
336
366
|
}
|
|
337
367
|
|
|
338
368
|
function getStatusIconText(classification) {
|
|
@@ -558,7 +588,7 @@ async function sendConsentAction(action) {
|
|
|
558
588
|
const summary = document.createElement('summary');
|
|
559
589
|
summary.textContent = utx('Technical details', '技术详情');
|
|
560
590
|
const code = document.createElement('code');
|
|
561
|
-
code.textContent = [state.code, state
|
|
591
|
+
code.textContent = [state.code, formatUpdateFailure(state)].filter(Boolean).join(': ');
|
|
562
592
|
details.append(summary, code);
|
|
563
593
|
card.append(details);
|
|
564
594
|
}
|
|
@@ -668,12 +698,26 @@ async function sendConsentAction(action) {
|
|
|
668
698
|
awaiting_health: [utx('Checking updated components', '正在检查更新后的组件'), latest, utx('Restarting and checking both components', '正在重启并检查两个组件')],
|
|
669
699
|
committed: [utx('Update complete', '更新完成'), latest || current, utx('Extension and Native Host are healthy.', '扩展与 Native Host 均运行正常。')],
|
|
670
700
|
rolled_back: [utx('Previous version restored', '已恢复上一版本'), current, utx('Health check failed, so the previous coordinated pair was restored.', '健康检查失败,已恢复上一组协调版本。')],
|
|
671
|
-
failed: [utx('Update could not continue', '更新无法继续'), latest, state
|
|
701
|
+
failed: [utx('Update could not continue', '更新无法继续'), latest, formatUpdateFailure(state) || utx('Check the details and retry.', '请查看详情后重试。')]
|
|
672
702
|
};
|
|
673
703
|
const value = map[state.state] || map.idle;
|
|
674
704
|
return { title: value[0], version: value[1], detail: value[2], phase: value[2] };
|
|
675
705
|
}
|
|
676
706
|
|
|
707
|
+
function formatUpdateFailure(state = {}) {
|
|
708
|
+
const code = String(state.code || '');
|
|
709
|
+
if (code === 'update_recovery_pending') return utx('The update result is being checked. Recovery has not been confirmed.', '正在确认更新结果,恢复状态尚未确认。');
|
|
710
|
+
if (['update_bootstrap_health_timeout', 'update_bootstrap_reload_failed'].includes(code)) return utx('The updated startup components did not finish restarting and confirming. Check recovery before retrying.', '更新后的启动组件未能完成重启和确认,请先检查恢复状态。');
|
|
711
|
+
if (['update_bootstrap_source_mismatch', 'update_bootstrap_source_version_mismatch'].includes(code)) return utx('The installed startup files do not match a supported update source. Check recovery before retrying.', '已安装的启动文件与支持的升级来源不一致,请先检查恢复状态。');
|
|
712
|
+
if (['update_bootstrap_record_missing', 'update_bootstrap_record_invalid', 'update_bootstrap_backup_mismatch', 'update_bootstrap_already_restoring'].includes(code)) return utx('The startup recovery material could not be confirmed. The update result remains unconfirmed.', '尚未确认启动组件的恢复材料有效,更新结果仍待确认。');
|
|
713
|
+
if (['update_bootstrap_confirmation_pending', 'update_bootstrap_native_restart_required'].includes(code)) return utx('Waiting for the updated startup components and Native Host to finish restarting and confirming.', '正在等待更新后的启动组件和本机服务完成重启及确认。');
|
|
714
|
+
if (code === 'update_bootstrap_monitor_unavailable') return utx('The update recovery monitor could not start. Check recovery before retrying.', '更新恢复监护程序未能启动,请先检查恢复状态。');
|
|
715
|
+
if (['update_bootstrap_transaction_mismatch', 'update_bootstrap_transaction_invalid'].includes(code)) return utx('The startup update transaction could not be matched. Check recovery before retrying.', '未能匹配启动组件的更新事务,请先检查恢复状态。');
|
|
716
|
+
if (code.startsWith('update_bootstrap_')) return utx('The startup update package or installed files could not be verified. Check recovery before retrying.', '无法验证启动组件更新包或已安装文件,请先检查恢复状态。');
|
|
717
|
+
if (['update_health_failed', 'update_request_failed', 'update_failed'].includes(code)) return utx('The updated components could not be confirmed. Check recovery before retrying.', '尚未确认更新后的组件正常,请先检查恢复状态。');
|
|
718
|
+
return state.message || '';
|
|
719
|
+
}
|
|
720
|
+
|
|
677
721
|
function formatBlocker(blocker) {
|
|
678
722
|
const labels = {
|
|
679
723
|
unsaved: utx('Overleaf has not confirmed that this document is saved.', 'Overleaf 尚未确认当前文档已保存。'),
|
|
@@ -688,7 +732,15 @@ async function sendConsentAction(action) {
|
|
|
688
732
|
return labels[blocker] || utx('Waiting until every Overleaf tab is saved and idle.', '正在等待所有 Overleaf 标签页完成保存并进入空闲状态。');
|
|
689
733
|
}
|
|
690
734
|
|
|
735
|
+
function refreshPopupCopy(element) {
|
|
736
|
+
const pair = popupCopyPairs.get(element?.textContent);
|
|
737
|
+
if (pair) element.textContent = utx(...pair);
|
|
738
|
+
}
|
|
739
|
+
|
|
691
740
|
function utx(english, chinese) {
|
|
741
|
+
const pair = [english, chinese];
|
|
742
|
+
popupCopyPairs.set(english, pair);
|
|
743
|
+
popupCopyPairs.set(chinese, pair);
|
|
692
744
|
const locale = document.documentElement?.lang || 'en';
|
|
693
745
|
return /^zh\b/i.test(locale) ? chinese : english;
|
|
694
746
|
}
|
|
@@ -63,6 +63,14 @@
|
|
|
63
63
|
const locale = normalizeLocale(options);
|
|
64
64
|
const type = String(event.type || '');
|
|
65
65
|
|
|
66
|
+
if (type === 'codex.local_skills.missing' || type === 'codex.local_skill_invocation.ignored') {
|
|
67
|
+
return { kind: 'activity', visible: true,
|
|
68
|
+
title: type === 'codex.local_skills.missing'
|
|
69
|
+
? textFor(locale, '所选的本地技能未能加载。', 'A selected local skill could not be loaded.')
|
|
70
|
+
: textFor(locale, '所选的本地技能已关闭或不可用。', 'A selected local skill is disabled or unavailable.'),
|
|
71
|
+
status: event.status || 'failed', technicalDetail: normalizeRawEvent(event) };
|
|
72
|
+
}
|
|
73
|
+
|
|
66
74
|
if (isContextCompactionEvent(event)) {
|
|
67
75
|
return formatContextCompactionCheckpoint(event, locale);
|
|
68
76
|
}
|
|
@@ -730,6 +738,21 @@
|
|
|
730
738
|
// failure data alongside the human-readable text. The string text is kept
|
|
731
739
|
// verbatim because it has mode-aware nuance (e.g. "No files were written"
|
|
732
740
|
// vs "This run did not start") the catalog's fallback strings don't capture.
|
|
741
|
+
if (context.code === 'codex_local_skill_unavailable' || /(?:The )?selected local Codex skill is unavailable/i.test(text)) {
|
|
742
|
+
return {
|
|
743
|
+
conclusion: textFor(locale, '所选 Codex 本地技能不可用或已关闭,本轮没有启动。', 'The selected local Codex skill is unavailable or disabled. This run did not start.'),
|
|
744
|
+
nextStep: textFor(locale, '请打开技能页,选择可用且已启用的技能后重新提交。', 'Open Skills, select an available enabled skill, and submit again.'),
|
|
745
|
+
failureCode: 'codex_local_skill_unavailable'
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
if (/^codex_local_skills_/.test(String(context.code || ''))
|
|
749
|
+
|| /local Codex skill discovery|Codex could not (?:list local skills|start local skill discovery)|Codex returned an invalid local skill catalog/i.test(text)) {
|
|
750
|
+
return {
|
|
751
|
+
conclusion: textFor(locale, 'Codex 本地技能未能加载,本轮没有启动。', 'Local Codex skills could not be loaded. This run did not start.'),
|
|
752
|
+
nextStep: textFor(locale, '请打开技能页重新加载本地目录,然后重新提交。', 'Open Skills to reload the local catalog, then submit again.'),
|
|
753
|
+
failureCode: 'codex_local_skills_failed'
|
|
754
|
+
};
|
|
755
|
+
}
|
|
733
756
|
const providerError = translateProviderError(text, locale);
|
|
734
757
|
if (providerError) {
|
|
735
758
|
return providerError;
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
const now = new Date().toISOString();
|
|
26
26
|
return {
|
|
27
27
|
id: input.id || generateId('aud'),
|
|
28
|
+
accountScopeId: redactSecretLikeText(stringField(input.accountScopeId)),
|
|
28
29
|
projectId: redactSecretLikeText(stringField(input.projectId)),
|
|
29
30
|
sessionId: redactSecretLikeText(stringField(input.sessionId)),
|
|
30
31
|
turnId: redactSecretLikeText(stringField(input.turnId)),
|
|
@@ -84,6 +85,7 @@
|
|
|
84
85
|
function normalizeAuditLogRecord(input = {}) {
|
|
85
86
|
return {
|
|
86
87
|
id: redactSecretLikeText(stringField(input.id)) || generateId('aud'),
|
|
88
|
+
accountScopeId: redactSecretLikeText(stringField(input.accountScopeId)),
|
|
87
89
|
projectId: redactSecretLikeText(stringField(input.projectId)),
|
|
88
90
|
sessionId: redactSecretLikeText(stringField(input.sessionId)),
|
|
89
91
|
turnId: redactSecretLikeText(stringField(input.turnId)),
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
const MIN_NATIVE_VERSION = '1.0.0';
|
|
13
13
|
const MIN_COMPATIBLE_NATIVE_VERSION = '1.0.0';
|
|
14
14
|
const MIN_COMPATIBLE_EXTENSION_VERSION = '1.0.0';
|
|
15
|
-
const BUILD_TARGET_VERSION = '2.5.
|
|
15
|
+
const BUILD_TARGET_VERSION = '2.5.1';
|
|
16
16
|
const DEFAULT_CHROME_EXTENSION_ID = 'illdpneeeopfffmiepaejglgmhpmdhdc';
|
|
17
17
|
const REQUIRED_CAPABILITIES = Object.freeze([
|
|
18
18
|
'bridgePing',
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
'codexModels',
|
|
26
26
|
'historyClearPlugin',
|
|
27
27
|
'localSkills',
|
|
28
|
+
'localCodexSkills',
|
|
28
29
|
'mirrorSensitiveScan',
|
|
29
30
|
'providerProfiles',
|
|
30
31
|
'assetTransfer',
|
|
@@ -64,6 +65,8 @@
|
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
function evaluateNativeCompatibility(response, metadata = {}) {
|
|
68
|
+
if (response?.extensionInstallIdentity) metadata = { ...metadata,
|
|
69
|
+
installIdentity: { ...(metadata.installIdentity || {}), ...response.extensionInstallIdentity } };
|
|
67
70
|
const native = unwrapNativeResponse(response);
|
|
68
71
|
if (!native) {
|
|
69
72
|
return compatibilityResult('native_missing', response, metadata, 'incompatible');
|
|
@@ -86,13 +89,25 @@
|
|
|
86
89
|
return analyzeNativeCompatibility(native, metadata).classification;
|
|
87
90
|
}
|
|
88
91
|
|
|
89
|
-
function buildInstallCommand(version = BUILD_TARGET_VERSION, platform, extensionId) {
|
|
92
|
+
function buildInstallCommand(version = BUILD_TARGET_VERSION, platform, extensionId, options = {}) {
|
|
90
93
|
const normalized = normalizeReleaseVersion(version) || BUILD_TARGET_VERSION;
|
|
91
94
|
const allowedExtensionId = normalizeInstallExtensionId(extensionId);
|
|
92
95
|
const extensionIdArg = allowedExtensionId && allowedExtensionId !== DEFAULT_CHROME_EXTENSION_ID
|
|
93
|
-
? ` --extension-id ${allowedExtensionId}`
|
|
94
|
-
|
|
95
|
-
|
|
96
|
+
? ` --extension-id ${allowedExtensionId}` : '';
|
|
97
|
+
if (options.managed === true) {
|
|
98
|
+
if (!allowedExtensionId || options.extensionId !== allowedExtensionId) return '';
|
|
99
|
+
const managedRoot = options.managedRoot;
|
|
100
|
+
const browser = options.browser === 'auto' ? 'chrome' : options.browser;
|
|
101
|
+
if (!['chrome', 'chromium'].includes(browser) || typeof managedRoot !== 'string'
|
|
102
|
+
|| !managedRoot || managedRoot.length > 4096 || managedRoot.trim() !== managedRoot
|
|
103
|
+
|| /[\u0000-\u001f\u007f]/.test(managedRoot)
|
|
104
|
+
|| !(/^(?:\/|[a-zA-Z]:[\\/]|\\\\)/.test(managedRoot))) return '';
|
|
105
|
+
// Windows setup commands are shown for PowerShell; POSIX uses sh quoting.
|
|
106
|
+
const quotedRoot = platform === 'win32' || options.platform === 'win32'
|
|
107
|
+
? "'" + managedRoot.replace(/'/g, "''") + "'"
|
|
108
|
+
: "'" + managedRoot.replace(/'/g, "'\\''") + "'";
|
|
109
|
+
return `npm exec --yes codex-overleaf-link@${normalized} -- install-managed --browser ${browser} --managed-root ${quotedRoot}${extensionIdArg}`;
|
|
110
|
+
}
|
|
96
111
|
return `npm exec --yes codex-overleaf-link@${normalized} -- install-native${extensionIdArg}`;
|
|
97
112
|
}
|
|
98
113
|
|
|
@@ -126,7 +141,7 @@
|
|
|
126
141
|
const extensionVersion = resolveMetadataVersion(metadata);
|
|
127
142
|
const nativeVersion = native && typeof native === 'object' ? native.version : undefined;
|
|
128
143
|
const platform = native && typeof native === 'object' ? native.platform : undefined;
|
|
129
|
-
const updateCommand = buildInstallCommand(BUILD_TARGET_VERSION, platform, metadata.extensionId);
|
|
144
|
+
const updateCommand = buildInstallCommand(BUILD_TARGET_VERSION, platform, metadata.extensionId, metadata.installIdentity);
|
|
130
145
|
const updateAvailable = isOlderReleaseVersion(nativeVersion, BUILD_TARGET_VERSION);
|
|
131
146
|
return {
|
|
132
147
|
status,
|
|
@@ -139,6 +154,7 @@
|
|
|
139
154
|
requiredVersion: MIN_COMPATIBLE_NATIVE_VERSION,
|
|
140
155
|
recommendedVersion: BUILD_TARGET_VERSION,
|
|
141
156
|
updateAvailable,
|
|
157
|
+
...(metadata.installIdentity ? { installIdentity: metadata.installIdentity } : {}),
|
|
142
158
|
installCommand: updateCommand,
|
|
143
159
|
updateCommand,
|
|
144
160
|
releaseUrl: buildReleaseUrl(BUILD_TARGET_VERSION),
|
|
@@ -9,32 +9,18 @@
|
|
|
9
9
|
|
|
10
10
|
const MAX_LOG_BYTES = 32 * 1024;
|
|
11
11
|
const COMPILABLE_EXTENSIONS = new Set(['.tex', '.bib', '.sty', '.cls', '.bst', '.bbx', '.cbx']);
|
|
12
|
-
|
|
13
12
|
function parseCompileResponse(response) {
|
|
14
|
-
if (!response || typeof response !== 'object') {
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
if (!Array.isArray(response.outputFiles)) {
|
|
19
|
-
return { ok: false, reason: 'Invalid compile response: outputFiles is not an array' };
|
|
20
|
-
}
|
|
21
|
-
|
|
13
|
+
if (!response || typeof response !== 'object') return { ok: false, reason: 'Invalid compile response: not an object' };
|
|
14
|
+
if (!Array.isArray(response.outputFiles)) return { ok: false, reason: 'Invalid compile response: outputFiles is not an array' };
|
|
22
15
|
const logFile = response.outputFiles.find(function (file) {
|
|
23
16
|
if (!file) return false;
|
|
24
17
|
if (file.type === 'log') return true;
|
|
25
18
|
if (typeof file.path === 'string' && file.path.endsWith('.log')) return true;
|
|
26
19
|
return false;
|
|
27
20
|
});
|
|
28
|
-
|
|
29
|
-
if (!logFile) {
|
|
30
|
-
return { ok: false, reason: 'No log file found in compile output' };
|
|
31
|
-
}
|
|
32
|
-
|
|
21
|
+
if (!logFile) return { ok: false, reason: 'No log file found in compile output' };
|
|
33
22
|
var logUrl = logFile.url || null;
|
|
34
|
-
if (!logUrl) {
|
|
35
|
-
return { ok: false, reason: 'Log file has no URL' };
|
|
36
|
-
}
|
|
37
|
-
|
|
23
|
+
if (!logUrl) return { ok: false, reason: 'Log file has no URL' };
|
|
38
24
|
return {
|
|
39
25
|
ok: true,
|
|
40
26
|
status: response.status,
|
|
@@ -82,13 +68,9 @@
|
|
|
82
68
|
}
|
|
83
69
|
|
|
84
70
|
function truncateLogForContext(log) {
|
|
85
|
-
if (!log || typeof log !== 'string')
|
|
86
|
-
return '';
|
|
87
|
-
}
|
|
71
|
+
if (!log || typeof log !== 'string') return '';
|
|
88
72
|
|
|
89
|
-
if (new Blob([log]).size <= MAX_LOG_BYTES)
|
|
90
|
-
return log;
|
|
91
|
-
}
|
|
73
|
+
if (new Blob([log]).size <= MAX_LOG_BYTES) return log;
|
|
92
74
|
|
|
93
75
|
var lines = log.split('\n');
|
|
94
76
|
var errorBlocks = extractErrorBlocks(lines);
|
|
@@ -108,26 +90,17 @@
|
|
|
108
90
|
}
|
|
109
91
|
|
|
110
92
|
function isCompileLogFresh(log, lastKnownSourceEditTimestamp) {
|
|
111
|
-
if (!log || typeof log !== 'object')
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (log.sourceChangeTimestamp == null) {
|
|
116
|
-
return false;
|
|
117
|
-
}
|
|
93
|
+
if (!log || typeof log !== 'object') return false;
|
|
94
|
+
if (log.sourceChangeTimestamp == null) return false;
|
|
118
95
|
|
|
119
96
|
return log.sourceChangeTimestamp >= lastKnownSourceEditTimestamp;
|
|
120
97
|
}
|
|
121
98
|
|
|
122
99
|
function isCompilableFile(filePath) {
|
|
123
|
-
if (!filePath || typeof filePath !== 'string')
|
|
124
|
-
return false;
|
|
125
|
-
}
|
|
100
|
+
if (!filePath || typeof filePath !== 'string') return false;
|
|
126
101
|
|
|
127
102
|
var lastDot = filePath.lastIndexOf('.');
|
|
128
|
-
if (lastDot === -1)
|
|
129
|
-
return false;
|
|
130
|
-
}
|
|
103
|
+
if (lastDot === -1) return false;
|
|
131
104
|
|
|
132
105
|
var ext = filePath.slice(lastDot).toLowerCase();
|
|
133
106
|
return COMPILABLE_EXTENSIONS.has(ext);
|
|
@@ -171,27 +144,11 @@
|
|
|
171
144
|
const warnings = logAvailable && Array.isArray(logResult.warnings)
|
|
172
145
|
? logResult.warnings.slice(0, 5).map(formatCompileDiagnosticForSummary)
|
|
173
146
|
: [];
|
|
174
|
-
|
|
175
|
-
return {
|
|
176
|
-
status: 'failed',
|
|
177
|
-
reason: error?.message || 'Post-write compile failed.',
|
|
178
|
-
errors,
|
|
179
|
-
warnings,
|
|
180
|
-
logAvailable
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
if (!result?.ok) {
|
|
184
|
-
return {
|
|
185
|
-
status: 'failed',
|
|
186
|
-
reason: result?.reason || 'Post-write compile did not succeed.',
|
|
187
|
-
errors,
|
|
188
|
-
warnings,
|
|
189
|
-
logAvailable
|
|
190
|
-
};
|
|
191
|
-
}
|
|
147
|
+
const failed = Boolean(error) || !result?.ok;
|
|
192
148
|
return {
|
|
193
|
-
status: result.compile?.status || 'triggered',
|
|
194
|
-
reason:
|
|
149
|
+
status: failed ? 'failed' : result.compile?.status || 'triggered',
|
|
150
|
+
reason: error ? error?.message || 'Post-write compile failed.'
|
|
151
|
+
: !result?.ok ? result?.reason || 'Post-write compile did not succeed.' : result.reason || '',
|
|
195
152
|
errors,
|
|
196
153
|
warnings,
|
|
197
154
|
logAvailable
|
|
@@ -203,9 +160,74 @@
|
|
|
203
160
|
return text.length > 180 ? `${text.slice(0, 179)}...` : text;
|
|
204
161
|
}
|
|
205
162
|
|
|
163
|
+
// Page/adapter-owned explanations and activity labels are display copy. TeX logs and unknown
|
|
164
|
+
// upstream diagnostics stay literal; callers retain the original evidence.
|
|
165
|
+
function localizeCompileReason(value, locale = 'en', depth = 0) {
|
|
166
|
+
const text = String(value || '');
|
|
167
|
+
const zh = /^zh(?:\b|[-_])/i.test(String(locale));
|
|
168
|
+
const messages = [
|
|
169
|
+
['Compile succeeded.', '编译成功。'],
|
|
170
|
+
['Overleaf compile was triggered. The page will continue showing progress.', '已触发 Overleaf 编译;页面会继续显示编译进度。'],
|
|
171
|
+
['Fetching compile log (@compile-log).', '正在获取编译日志 (@compile-log)。'],
|
|
172
|
+
['Overleaf save state was not verified.', '尚未确认 Overleaf 已保存。'],
|
|
173
|
+
['No Overleaf Recompile button or compile request template is available.', '没有可用的 Overleaf 重新编译按钮或编译请求模板。'],
|
|
174
|
+
['Stored compile request template expired. Please click Overleaf Recompile once, then retry.', '已保存的编译请求模板已过期。请先点击一次 Overleaf 重新编译,再重试。'],
|
|
175
|
+
['CompileAdapter not available', '编译适配器不可用'],
|
|
176
|
+
['CompileAdapter not loaded', '编译适配器未加载'],
|
|
177
|
+
['Overleaf save-state checker is unavailable.', 'Overleaf 保存状态检查器不可用。'],
|
|
178
|
+
['No compile log URL available', '没有可用的编译日志地址'],
|
|
179
|
+
['Clicked Overleaf Recompile, but no compile result was captured.', '已点击 Overleaf 重新编译,但未获取到编译结果。'],
|
|
180
|
+
['Invalid compile response: not an object', '编译响应无效:结果不是对象'],
|
|
181
|
+
['Invalid compile response: outputFiles is not an array', '编译响应无效:outputFiles 不是数组'],
|
|
182
|
+
['No log file found in compile output', '编译输出中未找到日志文件'],
|
|
183
|
+
['Log file has no URL', '日志文件没有可用地址'],
|
|
184
|
+
['Post-write compile failed.', '写后编译失败。'],
|
|
185
|
+
['Post-write compile did not succeed.', '写后编译未成功。'],
|
|
186
|
+
['Overleaf save state has not been checked yet.', '尚未检查 Overleaf 保存状态。'],
|
|
187
|
+
['Overleaf save state is unavailable.', 'Overleaf 保存状态不可用。'],
|
|
188
|
+
['Overleaf is offline; saving cannot be confirmed.', 'Overleaf 当前离线,无法确认保存。'],
|
|
189
|
+
['Overleaf document is unavailable for save-state detection.', 'Overleaf 文档不可用,无法检测保存状态。'],
|
|
190
|
+
['Overleaf save indicator reports changes are not saved.', 'Overleaf 保存指示器显示修改尚未保存。'],
|
|
191
|
+
['Overleaf is still saving or syncing changes.', 'Overleaf 仍在保存或同步修改。'],
|
|
192
|
+
['Overleaf save indicator was present, but did not verify that all changes are saved.', '已找到 Overleaf 保存指示器,但未能确认全部修改已保存。'],
|
|
193
|
+
['Overleaf save indicator was not found.', '未找到 Overleaf 保存指示器。']
|
|
194
|
+
];
|
|
195
|
+
for (const [en, chinese] of messages) {
|
|
196
|
+
if (text === en || text === chinese) return zh ? chinese : en;
|
|
197
|
+
}
|
|
198
|
+
for (const [en, chinese] of [
|
|
199
|
+
['Compile request failed with status ', '编译请求失败,状态码为 '],
|
|
200
|
+
['Failed to fetch compile log: ', '获取编译日志失败,状态码为 ']
|
|
201
|
+
]) {
|
|
202
|
+
const prefix = text.startsWith(en) ? en : text.startsWith(chinese) ? chinese : '';
|
|
203
|
+
if (prefix && /^\d{3}$/.test(text.slice(prefix.length))) return (zh ? chinese : en) + text.slice(prefix.length);
|
|
204
|
+
}
|
|
205
|
+
if (depth < 4) for (const [en, chinese] of [
|
|
206
|
+
['Post-write compile did not succeed: ', '写后编译未成功:'],
|
|
207
|
+
['Post-write compile failed: ', '写后编译出错:'],
|
|
208
|
+
['Compile log unavailable: ', '编译日志不可用:'],
|
|
209
|
+
['Compile request error: ', '编译请求出错:'],
|
|
210
|
+
['Overleaf save-state checker failed: ', 'Overleaf 保存状态检查失败:'],
|
|
211
|
+
['Could not check Overleaf save state: ', '无法检查 Overleaf 保存状态:'],
|
|
212
|
+
['Could not get fresh compile: ', '无法获取最新编译结果:'],
|
|
213
|
+
['Log fetch error: ', '获取日志出错:'],
|
|
214
|
+
['Timed out waiting for Overleaf to finish saving changes.', '等待 Overleaf 完成保存超时。'],
|
|
215
|
+
['Timed out waiting for a verified Overleaf save state.', '等待确认 Overleaf 保存状态超时。']
|
|
216
|
+
]) {
|
|
217
|
+
const prefix = text.startsWith(en) ? en : text.startsWith(chinese) ? chinese : '';
|
|
218
|
+
if (prefix) {
|
|
219
|
+
const rest = text.slice(prefix.length);
|
|
220
|
+
if (prefix === en && en.endsWith('.') && rest && !rest.startsWith(' ')) continue;
|
|
221
|
+
const suffix = localizeCompileReason(prefix === en && en.endsWith('.') ? rest.slice(1) : rest, locale, depth + 1);
|
|
222
|
+
return (zh ? chinese : en) + (!zh && suffix && en.endsWith('.') ? ' ' : '') + suffix;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return text;
|
|
226
|
+
}
|
|
206
227
|
return {
|
|
207
228
|
buildPostWriteCompileSummary,
|
|
208
229
|
formatCompileDiagnosticForSummary,
|
|
230
|
+
localizeCompileReason,
|
|
209
231
|
COMPILABLE_EXTENSIONS,
|
|
210
232
|
MAX_LOG_BYTES,
|
|
211
233
|
extractErrorBlocks,
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
(function (root, factory) {
|
|
2
|
+
if (typeof module === 'object' && module.exports) module.exports = factory(require('./sessionState'));
|
|
3
|
+
else root.CodexOverleafModuleRegistry.define('ComposerAttachmentDraft', ['SessionState'], factory);
|
|
4
|
+
})(typeof globalThis !== 'undefined' ? globalThis : this, function (SessionState) {
|
|
5
|
+
'use strict';
|
|
6
|
+
const EPOCH_KEY = 'composer-attachment-draft:clear-epoch';
|
|
7
|
+
const normalize = value => SessionState.normalizeComposerAttachmentsForPersistence(value);
|
|
8
|
+
const scopeKey = scope => JSON.stringify([scope?.accountScopeId || '', scope?.projectId || '']);
|
|
9
|
+
function checkedScope(scope) {
|
|
10
|
+
const accountScopeId = String(scope?.accountScopeId || '').trim();
|
|
11
|
+
const projectId = String(scope?.projectId || '').trim();
|
|
12
|
+
if (!accountScopeId || !projectId) throw failure('account_scope_unavailable');
|
|
13
|
+
return { accountScopeId, projectId };
|
|
14
|
+
}
|
|
15
|
+
function failure(code) {
|
|
16
|
+
return Object.assign(new Error(code === 'composer_draft_cleared'
|
|
17
|
+
? 'Saved attachments were cleared in another tab. Reload, then reattach files before retrying.'
|
|
18
|
+
: code === 'composer_draft_conflict'
|
|
19
|
+
? 'The saved attachment draft changed in another tab. Reload before changing it.'
|
|
20
|
+
: 'Attachment draft scope is unavailable or changed.'), { code });
|
|
21
|
+
}
|
|
22
|
+
function createStore(openDb) {
|
|
23
|
+
async function access(input, value) {
|
|
24
|
+
const scope = checkedScope(input);
|
|
25
|
+
const id = 'composer-attachment-draft:' + scopeKey(scope);
|
|
26
|
+
const db = await openDb();
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
const tx = db.transaction('artifacts', value ? 'readwrite' : 'readonly');
|
|
29
|
+
const store = tx.objectStore('artifacts');
|
|
30
|
+
let result, error;
|
|
31
|
+
tx.oncomplete = () => resolve(result);
|
|
32
|
+
tx.onerror = tx.onabort = () => reject(error || tx.error || new Error('Attachment draft transaction failed.'));
|
|
33
|
+
const epochRequest = store.get(EPOCH_KEY);
|
|
34
|
+
epochRequest.onsuccess = () => {
|
|
35
|
+
const epoch = Number(epochRequest.result?.epoch) || 0;
|
|
36
|
+
const clearedAt = Number(epochRequest.result?.clearedAt) || 0;
|
|
37
|
+
const request = store.get(id);
|
|
38
|
+
request.onsuccess = () => {
|
|
39
|
+
try {
|
|
40
|
+
const previous = request.result;
|
|
41
|
+
if (previous && (previous.accountScopeId !== scope.accountScopeId || previous.projectId !== scope.projectId)) {
|
|
42
|
+
throw failure('account_scope_unavailable');
|
|
43
|
+
}
|
|
44
|
+
const revision = Number(previous?.revision) || 0;
|
|
45
|
+
result = { epoch, clearedAt, revision, attachments: normalize(previous?.attachments) };
|
|
46
|
+
if (!value) return;
|
|
47
|
+
const attachments = normalize(value.attachments);
|
|
48
|
+
if (value.epoch !== epoch) {
|
|
49
|
+
// A clear performed by this page also invalidates its old base.
|
|
50
|
+
// Its empty draft needs no write; an old nonempty draft cannot return.
|
|
51
|
+
if (!attachments.length) return;
|
|
52
|
+
throw failure('composer_draft_cleared');
|
|
53
|
+
}
|
|
54
|
+
if (value.revision !== revision) throw failure('composer_draft_conflict');
|
|
55
|
+
if (attachments.length && value.unbased && clearedAt && value.changedAt <= clearedAt) {
|
|
56
|
+
throw failure('composer_draft_cleared');
|
|
57
|
+
}
|
|
58
|
+
result = { epoch, clearedAt, revision: revision + 1, attachments };
|
|
59
|
+
store.put({ id, type: 'composer-attachment-draft', ...scope, ...result });
|
|
60
|
+
} catch (caught) { error = caught; tx.abort(); }
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
async function clear(storeNames) {
|
|
66
|
+
const db = await openDb();
|
|
67
|
+
return new Promise((resolve, reject) => {
|
|
68
|
+
const tx = db.transaction(storeNames, 'readwrite');
|
|
69
|
+
const artifacts = tx.objectStore('artifacts');
|
|
70
|
+
const request = artifacts.get(EPOCH_KEY);
|
|
71
|
+
let epoch;
|
|
72
|
+
request.onsuccess = () => {
|
|
73
|
+
epoch = (Number(request.result?.epoch) || 0) + 1;
|
|
74
|
+
for (const name of storeNames) tx.objectStore(name).clear();
|
|
75
|
+
artifacts.put({ id: EPOCH_KEY, type: 'composer-attachment-draft-epoch', epoch, clearedAt: Date.now() });
|
|
76
|
+
};
|
|
77
|
+
tx.oncomplete = () => resolve({ composerDraftEpoch: epoch });
|
|
78
|
+
tx.onerror = tx.onabort = () => reject(tx.error || new Error('History clear failed.'));
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return { read: scope => access(scope), write: (scope, value) => access(scope, value), clear };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function createController(options) {
|
|
85
|
+
let scope = options.getScope(), key = scopeKey(scope), generation = 0;
|
|
86
|
+
let base = null, hydrated = false, changedVersion = 0, savedVersion = 0;
|
|
87
|
+
let dirtySince = 0, dirtyUnbased = false, loadSequence = 0;
|
|
88
|
+
let saving = Promise.resolve();
|
|
89
|
+
const publish = () => options.setAttachments(normalize(options.getAttachments()));
|
|
90
|
+
function syncScope() {
|
|
91
|
+
const next = options.getScope(), nextKey = scopeKey(next);
|
|
92
|
+
if (nextKey === key) return;
|
|
93
|
+
scope = next; key = nextKey; generation++;
|
|
94
|
+
base = null; hydrated = false; changedVersion = savedVersion = 0; dirtySince = 0; dirtyUnbased = false;
|
|
95
|
+
options.clear(); publish();
|
|
96
|
+
}
|
|
97
|
+
async function load(requestedScope = options.getScope()) {
|
|
98
|
+
syncScope();
|
|
99
|
+
if (scopeKey(requestedScope) !== key) throw failure('stale_hydration');
|
|
100
|
+
const captured = generation, sequence = ++loadSequence;
|
|
101
|
+
const snapshot = await options.read(checkedScope(scope));
|
|
102
|
+
if (captured !== generation || sequence !== loadSequence || scopeKey(options.getScope()) !== key) throw failure('stale_hydration');
|
|
103
|
+
if (base && snapshot.epoch !== base.epoch) {
|
|
104
|
+
// Adopt an observed history clear together with its empty local view;
|
|
105
|
+
// otherwise the next edit could attach an old view to the new epoch.
|
|
106
|
+
options.clear(); changedVersion = savedVersion = 0; dirtySince = 0; dirtyUnbased = false;
|
|
107
|
+
hydrated = false; base = snapshot; publish();
|
|
108
|
+
}
|
|
109
|
+
const keepLocal = changedVersion || hydrated;
|
|
110
|
+
// A read alone does not give an unchanged visible draft a newer CAS base.
|
|
111
|
+
if (!base || !keepLocal) base = snapshot;
|
|
112
|
+
return keepLocal ? normalize(options.getAttachments()) : snapshot.attachments;
|
|
113
|
+
}
|
|
114
|
+
function hydrate(attachments) {
|
|
115
|
+
syncScope();
|
|
116
|
+
if (hydrated) return;
|
|
117
|
+
// Loading is allowed to finish after a local add/remove. Local edits win.
|
|
118
|
+
if (!changedVersion && !options.getAttachments().length) options.restore(normalize(attachments));
|
|
119
|
+
hydrated = true; publish();
|
|
120
|
+
}
|
|
121
|
+
function changed() {
|
|
122
|
+
const before = key;
|
|
123
|
+
syncScope();
|
|
124
|
+
if (before !== key) return;
|
|
125
|
+
if (changedVersion === savedVersion) { dirtySince = Date.now(); dirtyUnbased = !base; }
|
|
126
|
+
changedVersion++; hydrated = true; publish(); options.scheduleSave();
|
|
127
|
+
}
|
|
128
|
+
function save(requestedScope = options.getScope()) {
|
|
129
|
+
syncScope();
|
|
130
|
+
if (scopeKey(requestedScope) !== key) return Promise.resolve();
|
|
131
|
+
const captured = generation;
|
|
132
|
+
const work = async () => {
|
|
133
|
+
if (captured !== generation || scopeKey(options.getScope()) !== key || changedVersion === savedVersion) return;
|
|
134
|
+
const version = changedVersion;
|
|
135
|
+
const attachments = normalize(options.getAttachments());
|
|
136
|
+
const unbased = dirtyUnbased, changedAt = dirtySince;
|
|
137
|
+
if (!base) await load(scope);
|
|
138
|
+
if (captured !== generation || scopeKey(options.getScope()) !== key) return;
|
|
139
|
+
const written = await options.write(checkedScope(scope), { ...base, attachments, unbased, changedAt });
|
|
140
|
+
if (captured !== generation) return;
|
|
141
|
+
base = written; savedVersion = version;
|
|
142
|
+
if (changedVersion === version) { dirtySince = 0; dirtyUnbased = false; }
|
|
143
|
+
};
|
|
144
|
+
const next = saving.then(work, work);
|
|
145
|
+
saving = next.catch(() => {});
|
|
146
|
+
return next;
|
|
147
|
+
}
|
|
148
|
+
return { syncScope, load, hydrate, changed, save };
|
|
149
|
+
}
|
|
150
|
+
return { createStore, createController, normalize, EPOCH_KEY };
|
|
151
|
+
});
|