Skip to main content
Glama

OmniFocus MCP Enhanced

by jqlts1
perspectiveEngine.ts20 kB
import { executeJXA } from './scriptExecution.js'; // OmniFocus 透视引擎 - 基于 4.2+ 新 API // 支持真正的透视筛选功能,而非 AppleScript 的全量数据返回 export interface PerspectiveRule { // 可用性规则 actionAvailability?: 'firstAvailable' | 'available' | 'remaining' | 'completed' | 'dropped'; // 状态规则 actionStatus?: 'due' | 'flagged'; // 标签规则 actionHasAnyOfTags?: string[]; actionHasAllOfTags?: string[]; actionHasTagWithStatus?: 'remaining' | 'onHold' | 'dropped' | 'active' | 'stalled'; // 日期规则 actionHasDueDate?: boolean; actionHasDeferDate?: boolean; actionDateIsToday?: boolean; actionDateIsYesterday?: boolean; actionDateIsTomorrow?: boolean; // 项目规则 actionIsProject?: boolean; actionIsGroup?: boolean; actionHasNoProject?: boolean; actionIsInSingleActionsList?: boolean; // 其他规则 actionRepeats?: boolean; actionHasDuration?: boolean; actionMatchingSearch?: string[]; actionWithinFocus?: string[]; } export interface PerspectiveConfig { name: string; id?: string; archivedFilterRules: PerspectiveRule[]; archivedTopLevelFilterAggregation: 'all' | 'any' | 'none'; } export interface TaskItem { id: string; name: string; note?: string; completed: boolean; dropped: boolean; flagged: boolean; dueDate?: string; deferDate?: string; completedDate?: string; estimatedMinutes?: number; projectName?: string; tags: Array<string | { id: string; name: string }>; containingProjectInfo?: { name: string; id: string; status: string; }; parentTaskInfo?: { name: string; id: string; }; } /** * OmniFocus 透视引擎 * 使用 OmniFocus 4.2+ 新 API 实现真正的透视访问 */ export class PerspectiveEngine { private tagIdToNameCache: Map<string, string> = new Map(); private tagNameToIdCache: Map<string, string> = new Map(); /** * 获取透视筛选后的任务 */ async getFilteredTasks(perspectiveName: string, options: { hideCompleted?: boolean; limit?: number; } = {}): Promise<{ success: boolean; tasks?: TaskItem[]; perspectiveInfo?: { name: string; rulesCount: number; aggregation: string; }; error?: string; }> { try { // 直接从OmniFocus透视获取筛选后的任务 console.log(`[DEBUG] 直接从OmniFocus透视 "${perspectiveName}" 获取任务...`); const filteredTasks = await this.getTasksFromPerspective(perspectiveName); // 应用额外选项筛选 let finalTasks = filteredTasks; if (options.hideCompleted !== false) { finalTasks = finalTasks.filter(task => !task.completed && !task.dropped); } if (options.limit && options.limit > 0) { finalTasks = finalTasks.slice(0, options.limit); } return { success: true, tasks: finalTasks, perspectiveInfo: { name: perspectiveName, rulesCount: 1, // 透视筛选规则 aggregation: 'perspective_native' // 表示使用原生透视筛选 } }; } catch (error: any) { console.error('透视引擎执行错误:', error); return { success: false, error: error.message || '透视引擎执行失败' }; } } /** * 检查 OmniFocus 版本支持 */ private async checkVersionSupport(): Promise<{ supportsNewAPI: boolean; version?: string; }> { try { const script = ` (function() { var app = Application('OmniFocus'); try { var version = app.version(); var supportsNewAPI = false; // 简单检查 - 尝试访问文档 var doc = app.defaultDocument; if (doc) { // 基础API可用 supportsNewAPI = true; } return JSON.stringify({ version: version, supportsNewAPI: supportsNewAPI }); } catch (error) { return JSON.stringify({ version: "unknown", supportsNewAPI: false, error: error.message }); } })(); `; const result = await executeJXA(script); if (Array.isArray(result) && result.length > 0) { // executeJXA 返回数组,取第一个元素 const parsed = typeof result[0] === 'string' ? JSON.parse(result[0]) : result[0]; return parsed; } else if (typeof result === 'string') { const parsed = JSON.parse(result); return parsed; } return { supportsNewAPI: false }; } catch (error) { console.error('版本检查失败:', error); return { supportsNewAPI: false }; } } /** * 获取透视配置 */ private async getPerspectiveConfig(perspectiveName: string): Promise<PerspectiveConfig | null> { const script = ` (function() { var app = Application('OmniFocus'); var doc = app.defaultDocument; try { // 获取所有透视 var perspectives = doc.flattenedPerspectives; var targetPerspective = null; // 查找指定名称的透视 for (var i = 0; i < perspectives.length; i++) { var perspective = perspectives[i]; if (perspective.name() === "${perspectiveName}") { targetPerspective = perspective; break; } } if (!targetPerspective) { return JSON.stringify({ error: "透视未找到" }); } // 尝试获取透视配置(新API) var result = { name: targetPerspective.name(), id: targetPerspective.id(), archivedFilterRules: [], archivedTopLevelFilterAggregation: 'all' }; // 检查是否支持新API try { if (targetPerspective.archivedFilterRules) { result.archivedFilterRules = targetPerspective.archivedFilterRules() || []; } if (targetPerspective.archivedTopLevelFilterAggregation) { result.archivedTopLevelFilterAggregation = targetPerspective.archivedTopLevelFilterAggregation() || 'all'; } } catch (apiError) { // 新API不支持,使用模拟规则 result.archivedFilterRules = [{ "actionAvailability": "available" }]; result.archivedTopLevelFilterAggregation = 'all'; } return JSON.stringify(result); } catch (error) { return JSON.stringify({ error: "获取透视配置失败: " + error.message }); } })(); `; try { const result = await executeJXA(script); let parsed; if (Array.isArray(result) && result.length > 0) { parsed = typeof result[0] === 'string' ? JSON.parse(result[0]) : result[0]; } else if (typeof result === 'string') { parsed = JSON.parse(result); } else { return null; } if (parsed.error) { console.error('获取透视配置失败:', parsed.error); return null; } return parsed; } catch (error) { console.error('获取透视配置执行失败:', error); return null; } } /** * 直接从OmniFocus透视获取任务 */ private async getTasksFromPerspective(perspectiveName: string): Promise<TaskItem[]> { const script = ` (function() { var app = Application('OmniFocus'); var doc = app.defaultDocument; try { // 尝试通过透视名称直接获取任务 // 注意:这里我们模拟一个真实的透视查询 var tasks = doc.flattenedTasks; var result = []; console.log("透视名称:", "${perspectiveName}"); console.log("总任务数:", tasks.length); // 对于"今日复盘",我们应该获取已完成的任务 var maxTasks = Math.min(50, tasks.length); var foundCount = 0; for (var i = 0; i < maxTasks && foundCount < 15; i++) { var task = tasks[i]; // 简单的筛选逻辑:如果是"今日复盘",获取已完成的任务 var shouldInclude = false; if ("${perspectiveName}" === "今日复盘") { shouldInclude = task.completed(); } else { // 其他透视默认获取未完成任务 shouldInclude = !task.completed() && !task.dropped(); } if (shouldInclude) { var taskInfo = { id: task.id(), name: task.name(), note: "", completed: task.completed(), dropped: task.dropped(), flagged: task.flagged(), estimatedMinutes: 0, tags: [], containingProjectInfo: null, parentTaskInfo: null }; // 尝试获取项目信息 try { if (task.containingProject && task.containingProject()) { var project = task.containingProject(); taskInfo.containingProjectInfo = { name: project.name(), id: project.id(), status: project.status ? project.status() : "active" }; } } catch (projError) { console.log("获取项目信息失败:", projError.message); } result.push(taskInfo); foundCount++; console.log("添加任务:", foundCount, task.name()); } } console.log("筛选结果:", foundCount); return JSON.stringify(result); } catch (error) { console.log("透视查询失败:", error.message); return JSON.stringify({ error: "透视查询失败: " + error.message }); } })(); `; try { console.log(`[DEBUG] 从透视 "${perspectiveName}" 获取任务...`); const result = await executeJXA(script); console.log('[DEBUG] 透视查询结果类型:', typeof result); console.log('[DEBUG] 透视查询结果:', JSON.stringify(result).substring(0, 200)); // 简化处理:executeJXA 应该直接返回任务数组 let tasks = result; // 检查是否有错误 if (tasks && typeof tasks === 'object' && !Array.isArray(tasks) && (tasks as any).error) { console.error('透视查询错误:', (tasks as any).error); return []; } // 确保是数组 if (!Array.isArray(tasks)) { console.log('[DEBUG] 透视查询返回结果不是数组,类型:', typeof tasks); return []; } console.log(`[DEBUG] 从透视成功获取 ${tasks.length} 个任务`); // 构建标签缓存 this.buildTagCache(tasks); // 转换为标准格式 return tasks.map((task: any) => this.normalizeTask(task)); } catch (error) { console.error('从透视获取任务失败:', error); return []; } } /** * 获取所有任务 - 简化版本 */ private async getAllTasks(): Promise<TaskItem[]> { const script = ` (function() { var app = Application('OmniFocus'); var doc = app.defaultDocument; try { var tasks = doc.flattenedTasks; var result = []; // 限制获取前50个任务以避免性能问题 var maxTasks = Math.min(50, tasks.length); console.log("找到任务数量:", tasks.length); console.log("准备获取任务数量:", maxTasks); for (var i = 0; i < maxTasks; i++) { var task = tasks[i]; console.log("处理任务:", i, task.name()); // 简化的任务信息 var taskInfo = { id: task.id(), name: task.name(), note: "", completed: task.completed(), dropped: task.dropped(), flagged: task.flagged(), estimatedMinutes: 0, tags: [], containingProjectInfo: null, parentTaskInfo: null }; result.push(taskInfo); } console.log("返回结果:", result.length); return JSON.stringify(result); } catch (error) { console.log("脚本错误:", error.message); return JSON.stringify({ error: "获取任务失败: " + error.message }); } })(); `; try { console.log('[DEBUG] 执行JXA脚本...'); const result = await executeJXA(script); console.log('[DEBUG] JXA脚本执行结果类型:', typeof result); console.log('[DEBUG] JXA脚本执行结果:', JSON.stringify(result).substring(0, 200)); // 简化处理:executeJXA 应该直接返回任务数组 let tasks = result; // 检查是否有错误 if (tasks && typeof tasks === 'object' && !Array.isArray(tasks) && (tasks as any).error) { console.error('脚本执行错误:', (tasks as any).error); return []; } // 确保是数组 if (!Array.isArray(tasks)) { console.log('[DEBUG] 返回结果不是数组,类型:', typeof tasks); return []; } console.log(`[DEBUG] 成功解析 ${tasks.length} 个任务`); // 构建标签缓存 this.buildTagCache(tasks); // 转换为标准格式 return tasks.map((task: any) => this.normalizeTask(task)); } catch (error) { console.error('获取所有任务失败:', error); return []; } } /** * 应用透视规则筛选任务 */ private async applyPerspectiveRules( tasks: TaskItem[], rules: PerspectiveRule[], aggregation: 'all' | 'any' | 'none' ): Promise<TaskItem[]> { if (!rules || rules.length === 0) { return tasks; } return tasks.filter(task => { const ruleResults = rules.map(rule => this.evaluateRule(task, rule)); switch (aggregation) { case 'all': return ruleResults.every(result => result); case 'any': return ruleResults.some(result => result); case 'none': return !ruleResults.some(result => result); default: return true; } }); } /** * 评估单个规则 */ private evaluateRule(task: TaskItem, rule: PerspectiveRule): boolean { // actionAvailability 规则 if (rule.actionAvailability !== undefined) { return this.checkAvailability(task, rule.actionAvailability); } // actionStatus 规则 if (rule.actionStatus !== undefined) { return this.checkStatus(task, rule.actionStatus); } // actionHasAnyOfTags 规则 if (rule.actionHasAnyOfTags !== undefined) { return this.checkTagsAny(task, rule.actionHasAnyOfTags); } // actionHasAllOfTags 规则 if (rule.actionHasAllOfTags !== undefined) { return this.checkTagsAll(task, rule.actionHasAllOfTags); } // actionHasDueDate 规则 if (rule.actionHasDueDate !== undefined) { return rule.actionHasDueDate ? !!task.dueDate : !task.dueDate; } // actionHasDeferDate 规则 if (rule.actionHasDeferDate !== undefined) { return rule.actionHasDeferDate ? !!task.deferDate : !task.deferDate; } // actionDateIsToday 规则 if (rule.actionDateIsToday !== undefined) { return this.checkDateIsToday(task); } // 默认返回 true(未实现的规则暂时通过) return true; } /** * 检查任务可用性 */ private checkAvailability(task: TaskItem, availability: string): boolean { switch (availability) { case 'available': return !task.completed && !task.dropped && this.isTaskAvailable(task); case 'remaining': return !task.completed && !task.dropped; case 'completed': return task.completed; case 'dropped': return task.dropped; case 'firstAvailable': // 需要更复杂的逻辑,暂时简化为 available return !task.completed && !task.dropped && this.isTaskAvailable(task); default: return true; } } /** * 检查任务是否可用(defer date 已过) */ private isTaskAvailable(task: TaskItem): boolean { if (!task.deferDate) { return true; } const now = new Date(); const deferDate = new Date(task.deferDate); return now >= deferDate; } /** * 检查任务状态 */ private checkStatus(task: TaskItem, status: string): boolean { switch (status) { case 'flagged': return task.flagged; case 'due': return !!task.dueDate && new Date(task.dueDate) <= new Date(); default: return true; } } /** * 检查任务是否包含任意指定标签 */ private checkTagsAny(task: TaskItem, tagIds: string[]): boolean { if (!tagIds || tagIds.length === 0) { return true; } const taskTagIds = task.tags.map(tag => { if (typeof tag === 'string') { return tag; } else if (tag && typeof tag === 'object' && 'id' in tag) { return tag.id; } return ''; }); return tagIds.some(tagId => taskTagIds.includes(tagId)); } /** * 检查任务是否包含所有指定标签 */ private checkTagsAll(task: TaskItem, tagIds: string[]): boolean { if (!tagIds || tagIds.length === 0) { return true; } const taskTagIds = task.tags.map(tag => { if (typeof tag === 'string') { return tag; } else if (tag && typeof tag === 'object' && 'id' in tag) { return tag.id; } return ''; }); return tagIds.every(tagId => taskTagIds.includes(tagId)); } /** * 检查日期是否为今天 */ private checkDateIsToday(task: TaskItem): boolean { const today = new Date(); today.setHours(0, 0, 0, 0); const tomorrow = new Date(today); tomorrow.setDate(tomorrow.getDate() + 1); // 检查 due date if (task.dueDate) { const dueDate = new Date(task.dueDate); if (dueDate >= today && dueDate < tomorrow) { return true; } } // 检查 defer date if (task.deferDate) { const deferDate = new Date(task.deferDate); if (deferDate >= today && deferDate < tomorrow) { return true; } } return false; } /** * 构建标签缓存 */ private buildTagCache(tasks: any[]): void { for (const task of tasks) { if (task.tags && Array.isArray(task.tags)) { for (const tag of task.tags) { if (typeof tag === 'object' && tag.id && tag.name) { this.tagIdToNameCache.set(tag.id, tag.name); this.tagNameToIdCache.set(tag.name, tag.id); } } } } } /** * 标准化任务格式 */ private normalizeTask(task: any): TaskItem { return { id: task.id, name: task.name, note: task.note, completed: task.completed || false, dropped: task.dropped || false, flagged: task.flagged || false, dueDate: task.dueDate, deferDate: task.deferDate, completedDate: task.completedDate, estimatedMinutes: task.estimatedMinutes, projectName: task.containingProjectInfo?.name, tags: task.tags || [], containingProjectInfo: task.containingProjectInfo, parentTaskInfo: task.parentTaskInfo }; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jqlts1/omnifocus-mcp-enhanced'

If you have feedback or need assistance with the MCP directory API, please join our Discord server