1. 项目缘起与核心价值作为一个长期在效率工具和自动化领域折腾的开发者我今年被诊断出患有ADHD注意力缺陷多动障碍。这个诊断像一把钥匙瞬间解释了我过去几十年里许多“拧巴”的工作习惯为什么我总是难以开始一个项目为什么复杂的任务清单会让我感到窒息为什么“去健身房”这样简单的事情在我脑子里会变成一团无法解开的乱麻。在寻求解决方案的过程中“任务原子化”这个概念反复出现。它的理念很简单把一个宏大的、令人望而生畏的任务比如“完成季度报告”拆解成一系列微小到几乎不可能失败的具体动作比如“打开文档软件”、“列出三个核心数据点”、“写第一段引言”。对于ADHD大脑来说这种拆解能绕过执行功能障碍通过完成一个个小步骤带来的即时成就感来维持动力。但问题来了拆解任务本身对我而言就是一个需要巨大“启动能量”的复杂任务。我的大脑在“创造力”和“结构化思维”上似乎总在打架让我对着“整理书房”这样的待办事项发呆半小时也列不出“先把书桌上的笔放进笔筒”这样的第一步。就在我几乎要放弃这个方法时我像许多人一样转向了ChatGPT。我试着把“准备一周的健康餐”丢给它它几乎瞬间就吐出了“1. 检查冰箱库存并列出缺少的食材 2. 根据现有食材选择三个食谱 3. 上网订购缺失的食材……”这样清晰的清单。它不完美有时会遗漏关键步骤或过于理想化但作为一个起点它比我空白的脑子强了不止一个数量级。于是一个典型的开发者思维出现了既然手动操作有效但麻烦为什么不把它自动化直接集成到我每天都在用的任务管理工具里呢我选择的是TickTick一个我非常喜欢的跨平台待办应用。我的目标很明确在TickTick内部通过简单的标签或描述一键调用AI将模糊的大任务自动拆解为可执行的小任务或者让AI帮我润色、补充任务描述让任务本身更具可操作性。这个项目我称之为“TickGPTick”——一个让TickTick插上GPT翅膀的本地化小工具。它运行在树莓派上安静地在后台工作将AI的“思考”能力无缝注入到我的日常任务流中。下面我就来详细拆解这个项目的设计思路、实现细节以及我踩过的那些坑。2. 整体架构设计与技术选型2.1 为什么选择本地化部署与轮询机制首先明确一个核心原则数据隐私与可控性。任务清单包含了我的工作计划、个人目标甚至生活琐事这些数据非常敏感。直接将它们持续发送到第三方云服务即使像OpenAI这样相对可信的长期来看存在隐私风险和政策变化的不确定性。因此我决定采用本地化部署方案。所有的任务数据获取、处理逻辑都运行在我自己的硬件树莓派上只有当我明确需要AI能力时才会将单个任务的标题或描述内容发送给OpenAI API并且我会在发送前进行审视。这种“按需、最小化”的数据外发策略让我心里踏实很多。硬件选择上我用了树莓派4B。它功耗极低常年开机电费可忽略不计性能足以应对轻量的HTTP请求和JSON解析而且体积小巧可以扔在路由器旁边默默工作。这构成了整个系统的“家庭服务器”。其次关于数据同步方式。TickTick官方提供了API但功能上更侧重于数据读取和简单的任务管理缺乏真正的“实时推送”机制如Webhook。因此要实现“检测到特定标签就触发AI”这种效果最直接可靠的方式就是主动轮询Polling。我让运行在树莓派上的脚本每隔一个很短的时间间隔比如1秒去查询一次我的TickTick账户检查任务列表是否有变化。虽然这看起来有点“笨”不够优雅但对于个人使用场景来说其优点非常突出实现简单无需搭建复杂的消息队列或Webhook接收服务。可靠性高不依赖TickTick的推送稳定性自己控制查询节奏。资源消耗低对于个人账户任务数量有限每秒一次的API调用频率完全在免费限额和树莓派处理能力之内。注意频繁轮询第三方API时务必遵守其速率限制Rate Limit。TickTick的API限制相对宽松但为了做一名友好的开发者我将轮询间隔设为1秒并确保在代码中加入了简单的错误重试和退避逻辑避免因网络波动导致请求失败而陷入疯狂重试的循环。2.2 核心功能模块分解整个TickGPTick应用可以看作由三个核心模块串联而成TickTick客户端模块负责与TickTick API进行认证和通信。核心工作是使用我的账号密码通过环境变量注入获取OAuth Token然后定期拉取任务列表并能在需要时创建、更新任务。AI处理引擎模块这是大脑。它接收从TickTick任务中提取的文本标题、描述、特定标签构造合适的Prompt指令发送给OpenAI APIGPT-3.5-turbo或GPT-4并解析返回的JSON或文本结果。业务逻辑与调度模块这是中枢神经系统。它以一个循环或使用setInterval运行每秒执行一次“检查-处理”流程检查调用TickTick客户端获取最新的任务列表。过滤从列表中找出那些“需要AI处理”的任务。我定义了两个触发条件任务被打上了类似expand-5的标签或者任务描述中包含ai{{...}}的魔法字符串。处理根据触发条件调用AI处理引擎并将AI生成的结果新的子任务列表或新的描述文本通过TickTick客户端写回。状态管理防止对同一个任务进行重复处理。例如给一个任务添加expand-5标签后AI会为其创建5个子任务然后脚本应该移除该标签或标记该任务为“已处理”避免下一轮循环再次触发。这个架构清晰地将外部服务、AI能力和业务规则解耦方便后续维护和扩展比如更换AI服务提供商或支持其他任务管理工具。3. 核心功能实现细节与踩坑实录3.1 “任务扩展Expand”功能从标签到子任务这是项目的核心功能。我想实现的是在TickTick中给任何一个任务加上一个像expand-5这样的标签脚本就能自动将其拆解成5个具体的子任务。第一步标签识别与解析我的脚本在轮询中会检查每个任务的tags数组。我设计了一个简单的规则查找以“expand-”开头的标签。后面的数字如5就是我希望拆解出的子任务数量。这里第一个坑就出现了标签的匹配需要精确且容错。用户可能手误打成expnd-5或者expand5。为了体验更好我最初想用模糊匹配但后来决定保持严格只识别expand-前缀。清晰的规则反而减少了误触发。解析出数字n后这个原始任务就进入了待处理队列。第二步构造AI Prompt指令这是决定AI输出质量的关键。你不能简单地对GPT说“把这个任务拆一下”。经过多次试验我总结出一个高效的Prompt模板你是一个专业的任务拆解专家擅长将模糊的宏观目标分解为具体、可立即执行的小步骤。请将以下任务拆解为大约{n}个清晰、连续、独立的子任务。每个子任务都应该是一个简单的动作理想情况下可以在15-30分钟内完成。请直接以JSON数组格式输出不要有任何额外解释。 原始任务[这里是任务的标题] 任务背景或细节[这里是任务的描述如果没有则为空] 输出格式示例[子任务1, 子任务2, 子任务3]这个Prompt做了几件事设定角色让AI进入“任务拆解专家”的心智模式。明确要求指定了子任务的数量、特性具体、可执行、短耗时。提供上下文同时使用了任务标题和描述字段描述字段能提供关键细节比如“健身”任务下描述“主要想练背部和核心”AI就能给出更相关的分解。强制结构化输出要求返回纯JSON数组。这是与AI协作中最重要的一课永远尽可能让AI返回结构化的数据JSON、XML而不是自由文本。自由文本解析起来太痛苦正则表达式写到头秃。而GPT模型对生成JSON格式非常擅长这极大简化了后续处理逻辑。第三步调用API与结果处理使用OpenAI的Node.js SDK调用chat.completions.create方法指定模型我常用gpt-3.5-turbo性价比高、上述Prompt和温度temperature设为0.3让输出更确定、更少“创意”。收到响应后直接解析response.choices[0].message.content并用JSON.parse()将其转化为JavaScript数组。第四步回写至TickTick拿到子任务数组后通过TickTick API为原始任务创建n个新的子任务TickTick支持父子任务结构。这里有个重要的细节创建完子任务后务必移除原任务上的expand-*标签或者给原任务添加一个processed-by-ai之类的标签。否则下一轮轮询会发现这个任务依然带着expand-5标签又会调用AI生成一批新的子任务导致任务爆炸这是我早期测试时犯过的错误生成了几十个重复的子任务。3.2 “AI描述AI Descriptions”功能描述字段的魔法语法这个功能是为了解决“任务描述写不出来”或者“写得太烂”的问题。我希望在编辑任务描述时能有一个快捷方式让AI帮我完善。实现机制我定义了一个简单的“魔法语法”在任务描述字段中写入ai{{请帮我将这份报告写得更加专业和简洁}}。脚本在轮询时不仅检查标签还会扫描所有任务的content描述字段。正则表达式是关键我使用正则表达式/ai{{(.?)}}/s来匹配。这个正则表达式的意思是查找以ai{{开头以}}结尾的文本并捕获中间(.?)的所有内容非贪婪模式s标志使.也能匹配换行符。捕获组里的内容就是给AI的指令比如“请帮我将这份报告写得更加专业和简洁”。处理流程匹配到魔法字符串后将原始描述比如“季度销售数据总结 ai{{请帮我将这份报告写得更加专业和简洁}}”和捕获的指令提取出来。构造一个新的Prompt例如“请根据以下指令优化下面的文本。指令[捕获的指令]。原始文本[原始描述中移除魔法字符串后的部分]”。调用OpenAI API获取优化后的文本。使用TickTick API的更新任务接口将整个描述字段替换为AI生成的新内容。这里必须用替换Update而不是追加。并且更新后魔法字符串ai{{...}}自然就消失了避免了循环处理。实操心得魔法字符串的设计为什么用ai{{}}而不是#ai或/ai因为{{}}在众多模板语言中常见视觉上很醒目且不容易在正常描述中误触发。双花括号也便于用正则表达式可靠地匹配。确保你的匹配逻辑足够健壮能处理用户可能在{{}}内部输入各种字符包括换行的情况。4. 环境配置与安全实践4.1 敏感信息管理绝对不要硬编码将API密钥和密码写在代码里是开发大忌。我使用.env文件配合dotenv库来管理所有敏感配置。.env文件示例OPENAI_API_KEYsk-your-openai-api-key-here TICKTICK_USERNAMEyour.emailexample.com TICKTICK_PASSWORDyourSuperSecurePassword123在代码中config.js读取require(dotenv).config(); module.exports { openaiApiKey: process.env.OPENAI_API_KEY, ticktickUsername: process.env.TICKTICK_USERNAME, ticktickPassword: process.env.TICKTICK_PASSWORD, // 可以添加其他配置如轮询间隔、默认AI模型等 pollIntervalMs: 1000, defaultOpenAIModel: gpt-3.5-turbo };安全要点将.env文件加入.gitignore这是最重要的步骤确保不会意外将密钥提交到公开的代码仓库。树莓派上的权限确保.env文件只有当前用户可读chmod 600 .env。密码的考虑直接使用TickTick密码虽然简单但并非最佳实践。更安全的方式是使用OAuth授权码流程获取长期有效的access_token。但TickTick API的OAuth流程对个人项目略显复杂。作为折中我强烈建议为这个脚本单独创建一个TickTick账户或者使用TickTick的“第三方应用密码”如果支持的话而不是主账户密码。目前我使用的是专门用于自动化的子账户密码。4.2 树莓派上的持久化运行为了让脚本在树莓派上7x24小时稳定运行我使用了PM2这个进程管理工具。安装与配置PM2# 全局安装PM2 npm install -g pm2 # 用PM2启动你的脚本并命名为 tickgptick pm2 start index.js --name tickgptick # 设置PM2开机自启 pm2 startup # 执行上面命令输出的指令通常与sudo相关 pm2 savePM2的好处进程守护如果脚本因为异常崩溃PM2会自动重启它。日志管理PM2会自动捕获并管理console.log输出你可以用pm2 logs tickgptick查看实时日志用pm2 flush清理旧日志。这对于调试轮询中的问题至关重要。监控pm2 monit可以提供一个简单的仪表板查看资源占用。5. 遇到的典型问题与排查心法在开发和运行过程中我遇到了不少问题这里记录下最典型的几个及其解决方案。5.1 问题一API限流与网络抖动现象脚本运行一段时间后控制台开始出现“Request failed with status code 429”或“ETIMEDOUT”错误随后可能停止工作。排查与解决429错误Too Many Requests这是触发了TickTick或OpenAI的速率限制。首先降低轮询频率。我从最初的每秒一次调整为每2秒甚至5秒一次。对于个人任务同步这个频率完全足够。其次在代码中实现指数退避重试机制。当请求失败时不要立即重试而是等待一段时间如1秒、2秒、4秒…再试。async function callWithRetry(apiCallFn, maxRetries 3) { let lastError; for (let i 0; i maxRetries; i) { try { return await apiCallFn(); } catch (error) { lastError error; if (error.response?.status 429) { // 指数退避 const delay Math.pow(2, i) * 1000 Math.random() * 1000; console.log(Rate limited. Retrying in ${delay}ms...); await sleep(delay); } else { // 对于其他错误可能直接跳出或简单重试 await sleep(1000 * i); } } } throw lastError; }网络超时错误树莓派的网络连接可能不如台式机稳定。除了重试机制还要确保你的HTTP客户端如axios设置了合理的timeout例如10秒。并为所有外部API调用包裹在try-catch中记录错误但不轻易让整个进程崩溃。5.2 问题二AI输出格式不稳定现象虽然Prompt要求返回JSON数组但AI偶尔会在JSON前后加上解释性文字如“好的这是拆解后的任务”导致JSON.parse()失败。解决强化Prompt在Prompt中非常强硬地指定格式。我后来把Prompt改成了“你必须只输出一个有效的JSON字符串数组不要有任何其他文本、标记或解释。任务...”。多次强调“只输出JSON”。输出后处理在解析前加一层简单的清洗逻辑。例如用正则表达式提取第一个[和最后一个]之间的内容。function extractJsonArray(text) { try { // 尝试直接解析 return JSON.parse(text); } catch (e) { // 如果失败尝试提取 const match text.match(/\[[\s\S]*\]/); if (match) { try { return JSON.parse(match[0]); } catch (e2) { console.error(Failed to extract valid JSON:, e2); return []; // 返回空数组或抛出错误 } } throw new Error(No JSON array found in AI response); } }降级方案如果清洗后仍无法解析可以记录错误并将原始AI回复作为单个子任务的描述保存下来至少保留了AI的思考而不是完全丢失。5.3 问题三任务重复处理与状态同步现象如前所述脚本可能反复处理同一个任务产生重复的子任务或描述。解决我实现了一个简单的内存中的已处理任务ID记录表。当成功处理一个任务无论是扩展还是描述替换后立即将该任务的ID记录到一个Set或数组里。在下一轮轮询开始过滤任务时先检查任务ID是否已在“已处理”列表中。同时这个记录需要有一个过期机制比如只保留过去一小时内处理过的任务ID防止内存无限增长也允许用户未来可以再次修改任务并重新触发AI比如改了描述后再次使用ai{{}}。const processedTasks new Set(); const PROCESSED_TTL_MS 60 * 60 * 1000; // 1小时 function markTaskAsProcessed(taskId) { processedTasks.add(taskId); // 设置定时器一小时后自动删除 setTimeout(() processedTasks.delete(taskId), PROCESSED_TTL_MS); } function isTaskProcessedRecently(taskId) { return processedTasks.has(taskId); }对于更持久的状态管理可以考虑使用轻量级数据库如SQLite但对于这个单用户、本地的脚本内存集合加TTL的方式简单有效。6. 未来可能的优化方向虽然当前版本已经稳定运行并极大地提升了我的效率但总有一些想法在脑子里盘旋更智能的触发条件目前依赖标签和魔法字符串。或许可以结合自然语言处理NLP让AI自动判断哪些任务描述过于模糊例如包含“搞定”、“处理”、“弄一下”等词然后主动建议或询问是否需要拆解。支持更多AI模型除了OpenAI可以集成开源的本地大模型如通过Ollama部署的Llama 3实现完全离线的任务拆解彻底消除数据出域的风险。这需要树莓派有更强的算力如树莓派5或者连接到家里另一台更强大的服务器。历史记录与学习记录每次AI拆解的结果和我最终完成任务的实际情况。理论上这些数据可以用来微调一个小模型让它越来越了解我个人的拆解偏好和做事风格。错误处理与用户反馈目前错误处理还很简陋。应该建立一个更健壮的错误通知机制比如当AI调用连续失败或TickTick认证失效时通过Telegram Bot或邮件通知我。同时可以增加一个“不满意这次拆解”的按钮点击后能让AI重新生成或手动调整并将这次反馈记录下来。这个项目本质上是一个高度个性化的“数字胶水”它将两个强大的工具TickTick和GPT粘合在一起创造了一个适应我个人思维缺陷的工作流。它不华丽但极其实用。开发它的过程本身就是一个“将大任务构建一个AI助手拆解成小步骤配置环境、写API调用、处理错误”的完美实践。如果你也有类似的效率痛点不妨也尝试用自动化的思路为自己打造一把顺手的“数字瑞士军刀”。