docs: 添加 cacheOperation.ts 详细注释

This commit is contained in:
HeroBlackInk 2026-02-18 11:23:36 +08:00
parent fac76b38d4
commit 63d80d27fc

View file

@ -1,7 +1,76 @@
/**
* ==========================================================================================
* CacheOperation -
* ==========================================================================================
*
*
* Obsidian Todoist
* 1. fileMetadata - Todoist
* 2. taskFileMapping - ID到文件路径的映射taskId -> {filePath, lineNumber}
* 3. rebuildCache - Vault Todoist
*
*
* - fileMetadata: { [filepath]: { todoistTasks: string[], todoistCount: number, defaultProjectId?: string } }
* markdown Todoist
*
* - taskFileMapping: { [taskId]: { filePath: string, lineNumber: number } }
* ID对应的文件位置
*
*
* Step 1: 清空 taskFileMapping fileMetadata
* Step 2: 确保 syncData Todoist API
* Step 3: 扫描 Vault .md #todoist
* Step 3.5: 检测并转换 legacy IDID -> ID
* Step 4: 遍历每个任务 Todoist
* Step 5: 弹出冲突解决对话框
* Step 6: 保存设置
*
*
* - contentConflict: Obsidian Todoist
* - statusConflict: 完成状态不一致
*
* ==========================================================================================
*/
import { App} from 'obsidian';
import UltimateTodoistSyncForObsidian from "../main";
import { TaskConflict, ConflictResolutionModal } from './conflictModal';
/**
* ==========================================================================================
* -
* ==========================================================================================
*
* DatabaseCheckIssue
*
* - task_deleted_in_todoist: 任务在 Todoist
* - missing_in_cache: 缓存中缺少任务
* - new_task_not_synced: 新任务未同步
* - file_reference_missing: 文件引用丢失
* - orphaned_in_cache: 缓存中存在孤立任务
* - task_not_in_vault: 任务在 Vault
* - content_mismatch: 内容不匹配
* - cache_content_outdated: 缓存内容过时
* - status_mismatch: 状态不匹配
* - cache_status_outdated: 缓存状态过时
* - duedate_mismatch: 截止日期不匹配
* - duplicate_task: 重复任务
* - priority_mismatch: 优先级不匹配
* - label_mismatch: 标签不匹配
* - project_mismatch: 项目不匹配
* - empty_metadata: 空元数据
*
* DatabaseCheckResult
*
* - success: 检查是否成功
* - totalIssues: 问题总数
* - issues: 问题列表
* - summary: 各类问题的统计
* - reportPath: 报告文件路径
*
* ==========================================================================================
*/
interface Due {
date?: string;
[key: string]: any;
@ -72,47 +141,122 @@ export interface DatabaseCheckResult {
reportPath?: string;
}
/**
* ==========================================================================================
* CacheOperation
* ==========================================================================================
*
*
* - fileMetadata taskFileMapping CRUD
* - rebuildCache
* -
*
*
* - this.app: Obsidian Vault
* - this.plugin: 主插件实例 (UltimateTodoistSyncForObsidian)
* - plugin.settings: 插件设置 fileMetadata, taskFileMapping
* - plugin.todoistSyncAPI: Todoist API
* - plugin.taskParser: 任务解析器
* - plugin.fileOperation: 文件操作
* - plugin.logOperation: 日志操作
*
* ==========================================================================================
*/
export class CacheOperation {
app:App;
plugin: UltimateTodoistSyncForObsidian;
/**
*
* @param app - Obsidian Vault
* @param plugin -
*/
constructor(app:App, plugin: UltimateTodoistSyncForObsidian) {
//super(app,settings);
this.app = app;
this.plugin = plugin;
}
/**
* ==========================================================================================
* FileMetadata
* ==========================================================================================
*
* fileMetadata
* {
* "filepath.md": {
* todoistTasks: ["taskId1", "taskId2"], // 该文件包含的 Todoist 任务 ID 列表
* todoistCount: 2, // 任务数量
* defaultProjectId: "project123" // 可选,默认项目 ID
* }
* }
*
*
* - Markdown Todoist
* -
*
* ==========================================================================================
*/
/**
*
* @param filepath -
* @returns null
*/
async getFileMetadata(filepath:string) {
// 从插件设置中获取 fileMetadata然后查找对应 filepath 的元数据
return this.plugin.settings.fileMetadata[filepath] ?? null
}
/**
*
* @returns fileMetadata
*/
async getFileMetadatas(){
return this.plugin.settings.fileMetadata ?? null
}
/**
*
* @param filepath -
*
* 使
* - Todoist
* - todoistTasks todoistCount 0
*/
async newEmptyFileMetadata(filepath:string){
const metadatas = this.plugin.settings.fileMetadata
// 如果元数据已存在,则不创建
if(metadatas[filepath]) {
return
}
else{
metadatas[filepath] = {}
}
// 初始化空任务列表
metadatas[filepath].todoistTasks = [];
metadatas[filepath].todoistCount = 0;
// 将更新后的metadatas对象保存回设置对象中
// 将更新后的 metadatas 对象保存回设置对象中
this.plugin.settings.fileMetadata = metadatas
}
/**
*
* @param filepath -
* @param newMetadata - todoistTasks todoistCount
*
* 使
* - /
* -
*/
async updateFileMetadata(filepath:string,newMetadata) {
const metadatas = this.plugin.settings.fileMetadata
// 如果元数据对象不存在则创建一个新的对象并添加到metadatas中
// 如果元数据对象不存在,则创建一个新的对象并添加到 metadatas
if (!metadatas[filepath]) {
metadatas[filepath] = {}
}
@ -121,16 +265,26 @@ export class CacheOperation {
metadatas[filepath].todoistTasks = newMetadata.todoistTasks;
metadatas[filepath].todoistCount = newMetadata.todoistCount;
// 将更新后的metadatas对象保存回设置对象中
// 将更新后的 metadatas 对象保存回设置对象中
this.plugin.settings.fileMetadata = metadatas
this.plugin.logOperation?.log('CACHE_FILE_METADATA_UPDATED', `Updated file metadata for: ${filepath}`, filepath);
}
async deleteTaskIdFromMetadata(filepath:string,taskId:string){
/**
* ID
* @param filepath -
* @param taskId - ID
*
*
* todoistTasks ID
* saveSettings()
*/
async deleteTaskIdFromMetadata(filepath:string, taskId:string){
console.log(filepath)
const metadata = await this.getFileMetadata(filepath)
console.log(metadata)
// 过滤掉指定的任务 ID
const newTodoistTasks = metadata.todoistTasks.filter(function(element){
return element !== taskId
})
@ -143,6 +297,14 @@ export class CacheOperation {
}
/**
*
* @param filepath -
*
* 使
* -
* -
*/
//delete filepath from filemetadata
async deleteFilepathFromMetadata(filepath:string){
Reflect.deleteProperty(this.plugin.settings.fileMetadata, filepath);
@ -151,36 +313,113 @@ export class CacheOperation {
console.log(`${filepath} is deleted from file metadatas.`)
}
// TaskFileMapping methods - replaces todoistTasksData.tasks
// ==========================================================================================
// getFileMetadatas - 获取所有文件元数据
// ==========================================================================================
// ==========================================================================================
// [已移动到上方]
// ==========================================================================================
// TaskFileMapping 方法组
// ==========================================================================================
// let newMetadata = {}
// newMetadata.todoistTasks = newTodoistTasks
// newMetadata.todoistCount = newTodoistCount
// console.log(`new metadata ${newMetadata}`)
// }
// ==========================================================================================
// TaskFileMapping 方法组
// ==========================================================================================
//
// 【taskFileMapping 数据结构】
// {
// "taskId123": { filePath: "folder/file.md", lineNumber: 5 },
// "taskId456": { filePath: "folder/file2.md", lineNumber: 10 }
// }
//
// 【作用】
// - 存储每个 Todoist 任务 ID 对应的 Obsidian 文件位置
// - 用于双向同步时定位任务在 Vault 中的位置
// - 当任务在 Todoist 端发生变化时,需要通过此映射更新 Obsidian 文件
//
// ==========================================================================================
/**
* ID
* @param taskId - Todoist ID
* @returns null
*/
getTaskFileMapping(taskId: string): { filePath: string; lineNumber: number } | null {
return this.plugin.settings.taskFileMapping[taskId] ?? null;
}
/**
* ID
* @param taskId - Todoist ID
* @param filePath - Obsidian
* @param lineNumber -
*/
setTaskFileMapping(taskId: string, filePath: string, lineNumber: number): void {
this.plugin.settings.taskFileMapping[taskId] = { filePath, lineNumber };
}
/**
* ID
* @param taskId - Todoist ID
*/
deleteTaskFileMapping(taskId: string): void {
delete this.plugin.settings.taskFileMapping[taskId];
}
/**
* ID
* @returns taskFileMapping
*/
getAllTaskFileMappings(): { [taskId: string]: { filePath: string; lineNumber: number } } {
return this.plugin.settings.taskFileMapping ?? {};
}
//Check errors in filemata where the filepath is incorrect.
// ==========================================================================================
// checkFileMetadata - 检查文件元数据错误
// ==========================================================================================
//
// 【功能】
// 遍历所有 fileMetadata检查
// 1. 文件是否仍然存在(不存在且元数据为空则删除)
// 2. 文件是否被重命名(不存在但有任务则尝试查找新路径)
//
// 【流程】
// for each metadata:
// if file not exist AND todoistTasks is empty -> delete metadata
// if file not exist BUT has todoistTasks -> search new path by taskId
//
// ==========================================================================================
/**
*
* @returns Promise<void>
*
* 使
* - Vault
* -
*
*/
async checkFileMetadata(){
const metadatas = await this.getFileMetadatas()
for (const key in metadatas) {
let filepath = key
const value = metadatas[key];
let file = this.app.vault.getAbstractFileByPath(key)
// 情况1: 文件不存在且元数据为空 -> 直接删除
if(!file && (value.todoistTasks?.length === 0 || !value.todoistTasks)){
console.log(`${key} is not existed and metadata is empty.`)
await this.deleteFilepathFromMetadata(key)
continue
}
// 情况2: 元数据为空 -> 跳过(可能是待处理的文件)
if(value.todoistTasks?.length === 0 || !value.todoistTasks){
//todo
//delelte empty metadata
@ -219,7 +458,21 @@ export class CacheOperation {
}
// Helper function to get all file metadatas
// ==========================================================================================
// getDefaultProjectNameForFilepath - 获取文件的默认项目名称
// ==========================================================================================
//
// 【功能】
// 获取指定文件对应的默认项目名称
// 如果文件没有设置默认项目,则使用插件全局默认项目
//
// ==========================================================================================
/**
*
* @param filepath -
* @returns
*/
getDefaultProjectNameForFilepath(filepath: string): string {
const metadatas = this.plugin.settings.fileMetadata;
if (!metadatas[filepath] || metadatas[filepath].defaultProjectId === undefined) {
@ -231,7 +484,21 @@ export class CacheOperation {
}
}
// ==========================================================================================
// getDefaultProjectIdForFilepath - 获取文件的默认项目 ID
// ==========================================================================================
//
// 【功能】
// 获取指定文件对应的默认项目 ID
// 如果文件没有设置默认项目,则使用插件全局默认项目 ID
//
// ==========================================================================================
/**
* ID
* @param filepath -
* @returns ID
*/
getDefaultProjectIdForFilepath(filepath:string){
const metadatas = this.plugin.settings.fileMetadata
if (!metadatas[filepath] || metadatas[filepath].defaultProjectId === undefined) {
@ -243,6 +510,21 @@ export class CacheOperation {
}
}
// ==========================================================================================
// setDefaultProjectIdForFilepath - 设置文件的默认项目 ID
// ==========================================================================================
//
// 【功能】
// 为指定文件设置默认项目 ID
// 该设置影响在此文件中创建的新任务的默认所属项目
//
// ==========================================================================================
/**
* ID
* @param filepath -
* @param defaultProjectId - ID
*/
setDefaultProjectIdForFilepath(filepath:string,defaultProjectId:string){
const metadatas = this.plugin.settings.fileMetadata
if (!metadatas[filepath]) {
@ -256,6 +538,16 @@ export class CacheOperation {
}
// ==========================================================================================
// DEPRECATED methods - 已废弃的方法
// ==========================================================================================
//
// 【说明】
// 以下方法已不再使用,因为现在直接使用 Todoist API 的 syncData
// 保留这些空方法是为了保持 API 兼容性,避免其他调用方报错
//
// ==========================================================================================
// DEPRECATED: Using syncData from Todoist API instead - no longer needed
loadTasksFromCache() {
return [];
@ -279,12 +571,32 @@ export class CacheOperation {
deleteTaskFromCache(_taskId: string): void {}
deleteTaskFromCacheByIDs(_deletedTaskIds: string[]): void {}
// ==========================================================================================
// getProjectIdByNameFromCache - 根据项目名称获取项目 ID (已废弃)
// ==========================================================================================
// DEPRECATED: Using todoistSyncAPI.getProjectByName instead
/**
* ID
* @deprecated 使 todoistSyncAPI.getProjectByName
* @param projectName -
* @returns ID null
*/
getProjectIdByNameFromCache(projectName: string): any {
return this.plugin.todoistSyncAPI.getSyncData()?.projects?.find((p: any) => p.name === projectName)?.id || null;
}
// ==========================================================================================
// getProjectNameByIdFromCache - 根据项目 ID 获取项目名称 (已废弃)
// ==========================================================================================
// DEPRECATED: Using todoistSyncAPI.getProjectById instead
/**
* ID
* @deprecated 使 todoistSyncAPI.getProjectById
* @param projectId - ID
* @returns null
*/
getProjectNameByIdFromCache(projectId: string): any {
return this.plugin.todoistSyncAPI.getSyncData()?.projects?.find((p: any) => p.id === projectId)?.name || null;
}
@ -292,7 +604,30 @@ export class CacheOperation {
// DEPRECATED: Using syncData from Todoist API instead
async saveProjectsToCache(): Promise<boolean> { return true; }
// DEPRECATED: Using taskFileMapping instead
// ==========================================================================================
// updateRenamedFilePath - 更新重命名文件的路径映射
// ==========================================================================================
//
// 【功能】
// 当用户在 Obsidian 中重命名文件时,更新 taskFileMapping 和 fileMetadata 中的路径
//
// 【参数】
// - oldpath: 旧的文件路径
// - newpath: 新的文件路径
//
// 【流程】
// 1. 遍历 taskFileMapping将所有指向旧路径的任务更新为新路径
// 2. 在 fileMetadata 中,将旧路径的元数据迁移到新路径
// 3. 记录操作日志
//
// ==========================================================================================
/**
*
* @deprecated 使 taskFileMapping
* @param oldpath -
* @param newpath -
*/
async updateRenamedFilePath(oldpath: string, newpath: string): Promise<void> {
const taskFileMapping = this.plugin.settings.taskFileMapping || {};
const fileMetadata = this.plugin.settings.fileMetadata || {};
@ -310,6 +645,47 @@ export class CacheOperation {
this.plugin.logOperation?.log('CACHE_RENAMED', `Renamed file path from ${oldpath} to ${newpath}`, newpath);
}
// ==========================================================================================
// rebuildCache - 重建缓存(核心方法)
// ==========================================================================================
//
// 【功能】
// 完整扫描 Vault 并与 Todoist 同步数据,是数据一致性的最终保障
//
// 【执行流程】
// Step 1: 清空 taskFileMapping保留 fileMetadata因为文件可能还在
// Step 2: 确保 syncData 已加载(从 Todoist API 获取最新数据)
// Step 3: 扫描 Vault 中所有 .md 文件,查找带有 #todoist 标签的任务
// - 提取 todoist_id: %%[todoist_id:: xxx]%%
// - 提取任务内容(去除 checkbox、metadata、link 等)
// - 检查完成状态 ([x] = completed, [ ] = not completed)
// Step 3.5: 检测并转换 legacy ID旧版数字 ID -> 新版字符串 ID
// - 遍历 Vault 中找到的任务 ID
// - 检查 ID 是否在 syncData 中
// - 不存在的 ID 可能是 legacy ID通过内容匹配查找新 ID
// Step 4: 遍历每个任务,与 Todoist 对比
// - 检查任务是否在 Todoist 存在
// - 对比内容是否一致
// - 对比完成状态是否一致
// - 检测到的冲突收集到数组中
// Step 5: 弹出冲突解决对话框,让用户选择解决方式
// Step 6: 保存设置taskFileMapping 更新)
//
// 【输出】
// - { success: boolean, tasksProcessed: number }
//
// 【使用场景】
// - 插件首次加载时
// - 用户点击"重建缓存"按钮
// - 数据出现不一致时手动修复
//
// ==========================================================================================
/**
* - Vault Todoist
* @param noticeCallback -
* @returns Promise<{ success: boolean; tasksProcessed: number }>
*/
async rebuildCache(noticeCallback?: (message: string) => void): Promise<{ success: boolean; tasksProcessed: number }> {
try {
if (noticeCallback) {
@ -319,9 +695,31 @@ export class CacheOperation {
}
this.plugin.logOperation?.log('CACHE_REBUILT', 'Starting cache rebuild...');
// ==========================================================================================
// Step 1: 清空 taskFileMapping
// ==========================================================================================
//
// 【说明】
// taskFileMapping 是从任务 ID 到文件位置的映射
// 重建缓存时需要重新扫描,所以先清空
// fileMetadata 保留,因为文件本身可能还在
//
// ==========================================================================================
// Step 1: Clear taskFileMapping (keep fileMetadata for other uses)
this.plugin.settings.taskFileMapping = {};
// ==========================================================================================
// Step 2: 确保 syncData 已加载
// ==========================================================================================
//
// 【说明】
// syncData 是从 Todoist API 获取的同步数据
// 包含所有任务、项目、标签等信息
// 如果未加载,则调用 initializeSync() 获取
//
// ==========================================================================================
// Step 2: Ensure syncData is loaded
if (noticeCallback) {
noticeCallback('Ensuring sync data is loaded...');
@ -334,6 +732,27 @@ export class CacheOperation {
await this.plugin.todoistSyncAPI.initializeSync();
}
// ==========================================================================================
// Step 3: 扫描 Vault 中所有 .md 文件
// ==========================================================================================
//
// 【说明】
// 遍历 Vault 中的所有 markdown 文件
// 查找带有 #todoist 标签的任务行
// 提取任务信息:
// - todoist_id: %%[todoist_id:: xxx]%%
// - 任务内容(使用 taskParser 去除各种标记)
// - 完成状态 ([x] = 已完成, [ ] = 未完成)
//
// 【数据结构 fileTaskMap】
// Map<filePath, Array<{taskId, lineNumber, content, isCompleted}>>
//
// 【注意】
// 这里只提取有 %%[todoist_id:: xxx]%% 的任务
// 没有 ID 的任务会在后续被当作新任务处理
//
// ==========================================================================================
// Step 3: Scan Vault for all .md files
if (noticeCallback) {
noticeCallback('Scanning vault for tasks...');
@ -344,26 +763,30 @@ export class CacheOperation {
const files = this.app.vault.getFiles()
.filter(f => f.extension === 'md');
// fileTaskMap: Map<filePath, tasks[]>
const fileTaskMap: Map<string, { taskId: string; lineNumber: number; content: string; isCompleted: boolean }[]> = new Map();
for (const file of files) {
try {
// 读取文件内容
const content = await this.app.vault.cachedRead(file);
const lines = content.split('\n');
// 遍历每一行
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Check for #todoist tag
// 检查是否包含 #todoist 标签
if (line.includes('#todoist')) {
// Extract todoist_id: %%[todoist_id:: xxx]%%
// 提取 todoist_id: %%[todoist_id:: xxx]%%
const match = line.match(/%%\[todoist_id::\s*(\w+)\]%%/);
if (match && match[1]) {
const taskId = match[1];
// Extract task content (remove checkbox and metadata)
// 使用 taskParser 提取任务内容(去除 checkbox、metadata、link 等)
const taskContent = this.plugin.taskParser.getTaskContentFromLineText(line);
// Check completion status - [x] = completed, [ ] = not completed
// 检查完成状态 - [x] = 已完成, [ ] = 未完成
const isCompleted = /\[x\]/i.test(line);
// 添加到 fileTaskMap
if (!fileTaskMap.has(file.path)) {
fileTaskMap.set(file.path, []);
}
@ -381,6 +804,31 @@ export class CacheOperation {
}
}
// ==========================================================================================
// Step 3.5: 检测并转换 legacy ID
// ==========================================================================================
//
// 【背景】
// Todoist API 在 2024 年将任务 ID 从数字格式(如 123456改为字符串格式如 "abc123"
// 旧版插件存储的是数字 ID新版 API 返回的是字符串 ID
// 需要通过任务内容匹配来找到对应的新 ID
//
// 【逻辑】
// 1. 获取 syncData 中所有活动的任务 ID
// 2. 遍历 Vault 中的任务 ID
// 3. 如果 ID 不在 syncData 中,可能是 legacy ID
// 4. 将这些任务收集起来,调用 convertLegacyIds 进行内容匹配
//
// 【convertLegacyIds 逻辑】
// 1. 遍历所有 syncData 中的任务
// 2. 标准化任务内容(小写、去除多余空格)
// 3. 查找内容完全匹配的任务
// 4. 如果找到唯一匹配,则记录映射关系
// 5. 如果找到多个匹配,抛出错误(需要手动处理)
// 6. 如果没有找到,记录日志并跳过
//
// ==========================================================================================
// Step 3.5: Detect and convert legacy IDs
if (noticeCallback) {
noticeCallback('Checking for legacy IDs...');
@ -388,16 +836,16 @@ export class CacheOperation {
console.log('Checking for legacy IDs...');
}
// Get active task IDs from syncData (from Step 2)
// 获取 syncData 中所有活动的任务 ID用于判断是否是 legacy ID
const activeTaskIds = new Set(syncData?.items?.map(t => t.id) || []);
// Find potential legacy IDs (not in syncData)
// 找出需要转换的潜在 legacy ID不在 syncData 中的 ID
const tasksNeedConversion: { taskId: string; content: string; filePath: string; lineNumber: number }[] = [];
for (const [filePath, fileTasks] of fileTaskMap.entries()) {
for (const taskInfo of fileTasks) {
if (!activeTaskIds.has(taskInfo.taskId)) {
// ID not in syncData - might be legacy
// ID 不在 syncData 中 - 可能是 legacy ID
tasksNeedConversion.push({
taskId: taskInfo.taskId,
content: taskInfo.content,
@ -408,18 +856,42 @@ export class CacheOperation {
}
}
// Convert legacy IDs if any
// 如果有需要转换的 ID调用 convertLegacyIds 进行转换
let idMapping: { [oldId: string]: string } = {};
let convertedCount = 0;
if (tasksNeedConversion.length > 0) {
if (noticeCallback) {
noticeCallback(`Converting ${tasksNeedConversion.length} legacy IDs...`);
}
// 调用 todoistSyncAPI 的 convertLegacyIds 方法
// 该方法通过任务内容匹配来找到对应的新 ID
idMapping = await this.plugin.todoistSyncAPI.convertLegacyIds(tasksNeedConversion);
convertedCount = Object.keys(idMapping).length;
console.log(`[rebuildCache] Converted ${convertedCount} legacy IDs`);
}
// ==========================================================================================
// Step 4: 处理任务并检测冲突
// ==========================================================================================
//
// 【说明】
// 遍历 Vault 中扫描到的所有任务,与 Todoist 进行对比
//
// 【检测内容】
// 1. 任务是否存在(使用转换后的 ID 查询 Todoist
// 2. 任务内容是否一致
// 3. 完成状态是否一致
//
// 【冲突类型】
// - contentConflict: Obsidian 任务内容与 Todoist 不一致
// - statusConflict: 完成状态不一致(一边完成/未完成,另一边相反)
//
// 【无效任务处理】
// - 任务在 Todoist 中不存在(可能被删除)
// - 记录到 invalidTaskIds后续会从缓存中移除
//
// ==========================================================================================
// Step 4: Process tasks and detect conflicts
if (noticeCallback) {
noticeCallback('Processing tasks and detecting conflicts...');
@ -427,33 +899,39 @@ export class CacheOperation {
console.log('Processing tasks and detecting conflicts...');
}
// 收集冲突任务
const conflicts: TaskConflict[] = [];
let processedCount = 0;
const totalTasks = Array.from(fileTaskMap.values())
.reduce((sum, tasks) => sum + tasks.length, 0);
// Track invalid taskIds (not in Todoist)
// 记录无效的任务 ID不在 Todoist 中)
const invalidTaskIds: string[] = [];
// 遍历每个文件的每个任务
for (const [filePath, fileTasks] of fileTaskMap.entries()) {
for (const taskInfo of fileTasks) {
try {
// Use converted ID if available
// 使用转换后的 ID如果有的话否则使用原始 ID
const taskId = idMapping[taskInfo.taskId] || taskInfo.taskId;
// Get task from syncData using potentially converted ID
// 从 syncData 中获取任务(使用可能转换后的 ID
const task = await this.plugin.todoistSyncAPI.GetTaskById(taskId);
// 情况1: 任务在 Todoist 中不存在(即使转换后也找不到)
if (!task) {
// Task doesn't exist in Todoist (even after ID conversion)
console.log(`Task ${taskId} (original: ${taskInfo.taskId}) not found in Todoist, will be removed...`);
// 记录无效 ID
invalidTaskIds.push(taskInfo.taskId);
// 记录日志
this.plugin.logOperation?.log('CACHE_TASK_DELETED', `Task ${taskInfo.taskId} not found in Todoist during rebuild`, filePath, taskInfo.taskId);
// 跳过此任务
continue;
}
// If ID was converted, update the vault file and taskFileMapping
// 情况2: 如果 ID 被转换了,更新 Vault 文件和 taskFileMapping
if (idMapping[taskInfo.taskId]) {
// 调用 fileOperation 更新 Vault 文件中的 ID
await this.plugin.fileOperation.updateTaskIdInVault(
taskInfo.filePath,
taskInfo.lineNumber,
@ -461,33 +939,34 @@ export class CacheOperation {
taskId
);
// Update taskFileMapping with new ID
// 使用新 ID 更新 taskFileMapping
this.plugin.settings.taskFileMapping[taskId] = {
filePath: taskInfo.filePath,
lineNumber: taskInfo.lineNumber
};
// Delete old mapping
// 删除旧的映射
delete this.plugin.settings.taskFileMapping[taskInfo.taskId];
console.log(`[rebuildCache] Updated mapping: ${taskInfo.taskId} -> ${taskId}`);
}
// Compare content
// 对比任务内容
const todoistContent = task.content || '';
const obsidianContent = taskInfo.content;
// Use converted ID if available
// 使用转换后的 ID 用于映射
const mappingTaskId = idMapping[taskInfo.taskId] || taskInfo.taskId;
// Check completion status
// 检查完成状态
const todoistIsCompleted = task.isCompleted || false;
const obsidianIsCompleted = taskInfo.isCompleted;
// Detect content conflict
// 检测内容冲突
const contentConflict = obsidianContent.trim() !== todoistContent.trim();
// Detect status conflict
// 检测状态冲突
const statusConflict = obsidianIsCompleted !== todoistIsCompleted;
// 如果有冲突,添加到冲突列表
if (contentConflict || statusConflict) {
conflicts.push({
taskId: mappingTaskId,
@ -498,7 +977,7 @@ export class CacheOperation {
});
}
// Save to taskFileMapping (use converted ID if available)
// 保存到 taskFileMapping使用转换后的 ID
this.plugin.settings.taskFileMapping[mappingTaskId] = {
filePath: filePath,
lineNumber: taskInfo.lineNumber
@ -506,7 +985,7 @@ export class CacheOperation {
processedCount++;
// Update UI every 10 tasks
// 每处理 10 个任务更新一次 UI
if (noticeCallback && processedCount % 10 === 0) {
noticeCallback(`Processing ${processedCount}/${totalTasks}...`);
}
@ -517,18 +996,38 @@ export class CacheOperation {
}
}
// ==========================================================================================
// Step 5: 解决冲突
// ==========================================================================================
//
// 【说明】
// 如果检测到冲突,弹出对话框让用户选择解决方式
//
// 【解决选项】
// - obsidian: 用 Obsidian 内容覆盖 Todoist
// - todoist: 用 Todoist 内容覆盖 Obsidian
// - skip: 跳过,不做任何修改
//
// 【ConflictResolutionModal】
// 显示冲突列表,让用户逐一选择解决方式
// 用户选择后,调用 resolveConflicts 执行实际的更新操作
//
// ==========================================================================================
// Step 5: Resolve conflicts
if (conflicts.length > 0) {
if (noticeCallback) {
noticeCallback(`Found ${conflicts.length} conflicts. Please resolve in dialog...`);
}
// 弹出冲突解决对话框
await new Promise<void>((resolve) => {
new ConflictResolutionModal(
this.app,
this.plugin,
conflicts,
async (resolutions) => {
// 执行冲突解决
await this.resolveConflicts(resolutions, conflicts);
resolve();
}
@ -536,9 +1035,20 @@ export class CacheOperation {
});
}
// ==========================================================================================
// Step 6: 保存设置
// ==========================================================================================
//
// 【说明】
// 所有处理完成后,保存更新后的设置
// 主要保存 taskFileMapping 的更新
//
// ==========================================================================================
// Step 6: Save settings
await this.plugin.saveSettings();
// 构建结果消息
const invalidMsg = invalidTaskIds.length > 0 ? ` (${invalidTaskIds.length} tasks not found in Todoist removed)` : '';
const conflictMsg = conflicts.length > 0 ? ` (${conflicts.length} conflicts resolved)` : '';
const convertedMsg = convertedCount > 0 ? ` (${convertedCount} legacy IDs converted)` : '';
@ -563,6 +1073,31 @@ export class CacheOperation {
}
}
// ==========================================================================================
// extractTaskContent - 提取任务内容(辅助方法)
// ==========================================================================================
//
// 【功能】
// 从任务行中提取纯文本内容,去除各种标记
//
// 【处理步骤】
// 1. 去掉 checkbox 标记 [- ] 或 [x]
// 2. 去掉 #todoist 标签
// 3. 去掉 todoist_id 元数据 %%[todoist_id:: xxx]%%
// 4. 去掉 link [link](url)
// 5. 去掉日期 🗓️📅📆🗓 2024-01-01
// 6. 去掉优先级 !!1 !!2 !!3 !!4
//
// 【注意】
// 此方法是 private只在类内部使用
//
// ==========================================================================================
/**
*
* @param line -
* @returns
*/
private extractTaskContent(line: string): string {
// 去掉 checkbox 标记 [- ] 或 [x]
let content = line.replace(/^(\s*)([-*])\s+\[(x|X| )\]\s*/, '');
@ -581,6 +1116,29 @@ export class CacheOperation {
return content;
}
// ==========================================================================================
// resolveConflicts - 解决冲突(辅助方法)
// ==========================================================================================
//
// 【功能】
// 根据用户选择的解决方式,执行实际的更新操作
//
// 【解决方式】
// - obsidian: 用 Obsidian 内容更新 Todoist调用 UpdateTask API
// - todoist: 用 Todoist 内容更新 Obsidian 文件(修改文件内容)
// - skip: 跳过,不做任何操作
//
// 【参数】
// - resolutions: Map<taskId, ConflictResolution> 用户选择的解决方式
// - conflicts: TaskConflict[] 冲突列表
//
// ==========================================================================================
/**
*
* @param resolutions -
* @param conflicts -
*/
private async resolveConflicts(resolutions: Map<string, ConflictResolution>, conflicts: TaskConflict[]): Promise<void> {
for (const conflict of conflicts) {
const resolution = resolutions.get(conflict.taskId);