mirror of
https://github.com/heroblackink/ultimate-todoist-sync-for-obsidian.git
synced 2026-07-22 07:40:27 +00:00
fix(repair): require successful vault update before remapping legacy IDs
This commit is contained in:
parent
b0c5049dca
commit
0eeb9c8c49
2 changed files with 142 additions and 27 deletions
|
|
@ -504,6 +504,7 @@ export class CacheOperation {
|
|||
async applyMatchFirstAutoRepairs(resultIssues: DatabaseCheckIssueLike[], shouldSave = true): Promise<MatchFirstAutoRepairResult> {
|
||||
const taskFileMapping = { ...this.plugin.settings.taskFileMapping };
|
||||
const touchedTaskIds = new Set<string>();
|
||||
const legacyConversionCandidates = new Map<string, { taskId: string; newTaskId: string; filePath: string }>();
|
||||
let changed = false;
|
||||
let mappingRepaired = 0;
|
||||
let nonActiveMarked = 0;
|
||||
|
|
@ -546,6 +547,23 @@ export class CacheOperation {
|
|||
continue;
|
||||
}
|
||||
|
||||
if (normalizedIssueType === 'mapping_legacy_id') {
|
||||
const mappedTaskId = (issue.details.match(/migrated to (\S+)/)?.[1] || '').trim();
|
||||
if (!mappedTaskId || mappedTaskId === taskId) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!legacyConversionCandidates.has(taskId)) {
|
||||
legacyConversionCandidates.set(taskId, {
|
||||
taskId,
|
||||
newTaskId: mappedTaskId,
|
||||
filePath,
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (normalizedIssueType === 'mapping_missing_for_task' || normalizedIssueType === 'mapping_pointer_stale') {
|
||||
const entry = ensureEntry(taskId, filePath);
|
||||
|
||||
|
|
@ -615,8 +633,64 @@ export class CacheOperation {
|
|||
skipped++;
|
||||
}
|
||||
|
||||
if (legacyConversionCandidates.size > 0) {
|
||||
const fileOperation = this.plugin.fileOperation;
|
||||
if (!fileOperation) {
|
||||
skipped += legacyConversionCandidates.size;
|
||||
} else {
|
||||
for (const [oldTaskId, candidate] of legacyConversionCandidates.entries()) {
|
||||
const newTaskId = candidate.newTaskId;
|
||||
|
||||
const oldEntry = taskFileMapping[oldTaskId];
|
||||
if (!oldEntry) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const vaultUpdated = await fileOperation.updateTaskIdInVault(candidate.filePath, oldTaskId, newTaskId);
|
||||
if (!vaultUpdated) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const mergedEntry = {
|
||||
...(taskFileMapping[newTaskId] || {}),
|
||||
...oldEntry,
|
||||
filePath: candidate.filePath,
|
||||
};
|
||||
|
||||
const resolvedLegacyIssue = resolveTaskIssuesByType(
|
||||
mergedEntry.issues as TaskIssueRecord | undefined,
|
||||
(issueType) => issueType === 'mapping_legacy_id'
|
||||
);
|
||||
|
||||
if (resolvedLegacyIssue.issues && Object.keys(resolvedLegacyIssue.issues).length > 0) {
|
||||
mergedEntry.issues = resolvedLegacyIssue.issues as TaskIssueRecord;
|
||||
} else if (mergedEntry.issues !== undefined) {
|
||||
delete mergedEntry.issues;
|
||||
}
|
||||
|
||||
const reconciled = reconcileTaskEntryDerivedState(
|
||||
mergedEntry as { status?: 'active' | 'nonActive' | 'conflicted' | 'issue'; syncEnabled?: boolean; issues?: TaskIssueRecord },
|
||||
(mergedEntry.status || 'active') as 'active' | 'nonActive' | 'conflicted' | 'issue'
|
||||
);
|
||||
if (reconciled.changed || resolvedLegacyIssue.changed) changed = true;
|
||||
|
||||
taskFileMapping[newTaskId] = mergedEntry;
|
||||
if (oldTaskId !== newTaskId) {
|
||||
delete taskFileMapping[oldTaskId];
|
||||
}
|
||||
|
||||
touchedTaskIds.add(newTaskId);
|
||||
mappingRepaired++;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const taskId of touchedTaskIds) {
|
||||
const entry = taskFileMapping[taskId];
|
||||
if (!entry) continue;
|
||||
const reconciled = reconcileTaskEntryDerivedState(
|
||||
entry as { status?: 'active' | 'nonActive' | 'conflicted' | 'issue'; syncEnabled?: boolean; issues?: TaskIssueRecord },
|
||||
(entry.status || 'active') as 'active' | 'nonActive' | 'conflicted' | 'issue'
|
||||
|
|
@ -640,17 +714,21 @@ export class CacheOperation {
|
|||
if (issueType === 'todoist_link_stale' || issueType === 'sync_content_mismatch' || issueType === 'sync_completion_mismatch') {
|
||||
return 'high';
|
||||
}
|
||||
if (issueType === 'sync_due_mismatch' || issueType === 'sync_labels_mismatch' || issueType === 'sync_priority_mismatch' || issueType === 'sync_project_mismatch') {
|
||||
return 'medium';
|
||||
}
|
||||
return 'low';
|
||||
}
|
||||
if (issueType === 'sync_due_mismatch' || issueType === 'sync_labels_mismatch' || issueType === 'sync_priority_mismatch' || issueType === 'sync_project_mismatch') {
|
||||
return 'medium';
|
||||
}
|
||||
if (issueType === 'mapping_legacy_id') {
|
||||
return 'medium';
|
||||
}
|
||||
return 'low';
|
||||
}
|
||||
|
||||
private getIssueManualAction(issueType: string): string | undefined {
|
||||
if (issueType === 'todoist_link_stale') return 'Repair in Manage Problem Tasks';
|
||||
if (issueType === 'todoist_task_missing') return 'Resolve in Manage Problem Tasks';
|
||||
return undefined;
|
||||
}
|
||||
private getIssueManualAction(issueType: string): string | undefined {
|
||||
if (issueType === 'todoist_link_stale') return 'Repair in Manage Problem Tasks';
|
||||
if (issueType === 'todoist_task_missing') return 'Resolve in Manage Problem Tasks';
|
||||
if (issueType === 'mapping_legacy_id') return 'Run Safe Repair to migrate legacy ID';
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private buildIssueExpectedActual(issue: DatabaseCheckIssueLike): { expected?: string; actual?: string } {
|
||||
const normalizedType = normalizeTaskIssueTypeKey(issue.type);
|
||||
|
|
@ -1209,8 +1287,7 @@ export class CacheOperation {
|
|||
// 该方法通过任务内容匹配来找到对应的新 ID
|
||||
try {
|
||||
idMapping = await todoistSyncAPI.convertLegacyIds(tasksNeedConversion);
|
||||
convertedCount = Object.keys(idMapping).length;
|
||||
this.plugin.debugLog(`[rebuildCache] Converted ${convertedCount} legacy IDs`);
|
||||
this.plugin.debugLog(`[rebuildCache] Found ${Object.keys(idMapping).length} legacy ID candidates to migrate`);
|
||||
} catch (error) {
|
||||
console.warn(`[rebuildCache] Legacy ID conversion failed: ${(error as Error).message}. These tasks will remain with their old IDs and may trigger conversion again on next rebuild.`);
|
||||
this.plugin.debugLog('[rebuildCache] Will continue without converting legacy IDs');
|
||||
|
|
@ -1266,8 +1343,9 @@ export class CacheOperation {
|
|||
for (const [filePath, fileTasks] of fileTaskMap.entries()) {
|
||||
for (const taskInfo of fileTasks) {
|
||||
try {
|
||||
const taskId = idMapping[taskInfo.taskId] || taskInfo.taskId;
|
||||
const task = syncItemsMap.get(taskId);
|
||||
const convertedTaskId = idMapping[taskInfo.taskId];
|
||||
const resolvedTaskId = convertedTaskId || taskInfo.taskId;
|
||||
const task = syncItemsMap.get(resolvedTaskId);
|
||||
|
||||
if (!task) {
|
||||
const status = taskInfo.isCompleted ? 'nonActive' : 'issue';
|
||||
|
|
@ -1300,16 +1378,41 @@ export class CacheOperation {
|
|||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (idMapping[taskInfo.taskId]) {
|
||||
await this.plugin.fileOperation!.updateTaskIdInVault(
|
||||
|
||||
let mappingTaskId = taskInfo.taskId;
|
||||
if (convertedTaskId && convertedTaskId !== taskInfo.taskId) {
|
||||
const vaultUpdated = await this.plugin.fileOperation!.updateTaskIdInVault(
|
||||
filePath,
|
||||
taskInfo.taskId, taskId
|
||||
taskInfo.taskId,
|
||||
convertedTaskId
|
||||
);
|
||||
this.plugin.debugLog(`[rebuildCache] Updated mapping: ${taskInfo.taskId} -> ${taskId}`);
|
||||
|
||||
if (!vaultUpdated) {
|
||||
issueCount++;
|
||||
nextMapping[taskInfo.taskId] = {
|
||||
filePath,
|
||||
status: 'issue',
|
||||
syncEnabled: false,
|
||||
issues: {
|
||||
mapping_legacy_id: {
|
||||
state: 'open',
|
||||
severity: 'medium',
|
||||
source: 'runtime',
|
||||
detectedAt: now,
|
||||
lastSeenAt: now,
|
||||
details: `Legacy ID conversion failed for ${taskInfo.taskId} -> ${convertedTaskId}. Vault update did not succeed.`,
|
||||
manualAction: 'Run Safe Repair to retry legacy ID migration',
|
||||
}
|
||||
}
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
mappingTaskId = convertedTaskId;
|
||||
convertedCount++;
|
||||
this.plugin.debugLog(`[rebuildCache] Updated mapping: ${taskInfo.taskId} -> ${convertedTaskId}`);
|
||||
}
|
||||
|
||||
const mappingTaskId = idMapping[taskInfo.taskId] || taskInfo.taskId;
|
||||
|
||||
const todoistContent = task.content || '';
|
||||
const obsidianContent = taskInfo.content;
|
||||
const todoistIsCompleted = !!(task as any).checked;
|
||||
|
|
|
|||
|
|
@ -725,13 +725,13 @@ export class FileOperation {
|
|||
filePath: string,
|
||||
oldId: string,
|
||||
newId: string
|
||||
): Promise<void> {
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const file = this.app.vault.getAbstractFileByPath(filePath);
|
||||
if (!file) {
|
||||
console.error(`[updateTaskIdInVault] File not found: ${filePath}`);
|
||||
this.plugin.debugLog(filePath)
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
const content = await this.app.vault.read(file);
|
||||
|
|
@ -748,7 +748,7 @@ export class FileOperation {
|
|||
|
||||
if (targetLineIndex === -1) {
|
||||
console.warn(`[updateTaskIdInVault] Old ID ${oldId} not found in ${filePath}`);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
let line = lines[targetLineIndex];
|
||||
|
|
@ -782,12 +782,22 @@ export class FileOperation {
|
|||
|
||||
if (!hasChanges) {
|
||||
console.warn(`[updateTaskIdInVault] No ID patterns found for ${oldId} in ${filePath}`);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
lines[targetLineIndex] = line;
|
||||
|
||||
await this.plugin.backupOperation?.backupFile(filePath);
|
||||
|
||||
if (!this.plugin.backupOperation) {
|
||||
console.error(`[updateTaskIdInVault] Backup module not initialized, skipping ID update ${oldId} -> ${newId}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const backupPath = await this.plugin.backupOperation.backupFile(filePath);
|
||||
if (!backupPath) {
|
||||
console.error(`[updateTaskIdInVault] Backup failed, skipping ID update ${oldId} -> ${newId}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
await this.app.vault.modify(file, lines.join('\n'));
|
||||
|
||||
this.plugin.logOperation?.log(
|
||||
|
|
@ -797,8 +807,10 @@ export class FileOperation {
|
|||
newId
|
||||
);
|
||||
this.plugin.debugLog(`[updateTaskIdInVault] Updated task ID ${oldId} -> ${newId} in ${filePath}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`[updateTaskIdInVault] Failed to update task ID in vault:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue