免费!用iFlow结合Obsidian记录和复盘

之前我分享过两个内容:一是怎么用 AI 来辅助写日记,另一类是怎么在 Obsidian 里借助 Templater、CustomJS 这些能力,把日报、周报、月报串起来,做成一套自己的复盘流程。它们各自都很好用,但过去在我的实际使用里,这两部分始终是分开的:前者更像一个即时反馈工具,后者更像一个结构化整理框架。你可以记录,也可以复盘,但这两件事之间还是缺了一条真正顺滑的连接通道。

我把这两部分重新整理了一下,发现现在其实已经有一种更简单的组合方式,可以把”日常记录”和”阶段复盘”自然串成一个闭环。核心思路并不复杂,就是把 iflow CLI、Obsidian 的 MCP、以及我自己创建的 skill 结合起来使用。每天想到什么就先随手写进日报,就像之前在网页中通过chat的方式写日记一样,不需要一开始就写得特别完整,也不需要刻意整理结构;当记录积累到一定程度之后,再通过 skill 去做总结,思考与感悟,并且让AI提出一些改进的建议。这样一来,日记不再只是”写过就算了”的东西,而是会持续积累,进入周复盘、月复盘、季度复盘,甚至年复盘。下面就详细介绍一下我整体的实现思路。

核心组件

组件 作用
iFlow CLI 调用 AI skill 进行内容生成和分析
Templater Obsidian 插件,支持 User Scripts 执行 Node.js 代码
QuickAdd 创建命令按钮,触发复盘脚本
Dataview 在笔记中渲染交互按钮
note-reviewer skill 自定义 skill,负责分析笔记并生成复盘

创建Skill

首先是利用skill-creator根据自己的需求来创建一个复盘的流程。我的就是叫note-reviewer,要求AI每天给我生成三个部分,一个是今日要点,就是今天主要做的事情的梳理和总结,一个是思考与感悟,就是帮助我复盘今天的问题,最后是导师建议,就是给我提出一些新的建议。

其他的复盘内容都是在生成笔记的时候自动触发的,譬如周复盘的会生成本周成就,本周思考,这周的习惯追踪分析,导师的寄语,以及下周的计划(这个部分一般还是我自己写)。同样的格式也应用到月复盘、季度复盘和年复盘上,只是月复盘是从周复盘中再总结。这个颗粒度可以自己把控,以前我自己是不会复盘这么勤快的,但是现在用AI来复盘了,就弄得比较细了。这个部分大家可以根据自己的需要来创建不同的复盘维度。

将iflow cli嵌入到Template

日报的template设置

我的日报里面主要分为三个部分,一个是Todo, 一个是Memo,还有就是一个Trackers用来记录我一些习惯,以及我用dataview构建的代办task的列表。我每天会把一堆碎碎念的日记内容写到Memo下面。然后在AI review下面用创建了两个按钮,一个是复盘今天,一个是复盘昨天(因为有时候晚上看书或者健身了,就没回填tracker的信息,所以可能会在第二天复盘,这个就完全是个人习惯了),日报的格式如下图所示:

日报界面

这两个按钮的调用方式是使用本地的js文件打开powershell的窗口运行iflow以及对应的prompt。这个按钮以及运行脚本可以让AI帮忙写一下,也可以改成自己喜欢的方式。

定义按钮以及触发脚本

我是在笔记templates的文件夹里面定义了一个js文件,用于调用iflow cli并且指定特定的model使用定义的skill来进行复盘。在QuickAdd的配置里面增加两个choice,一个是总结今日日报,一个是总结昨日日报(昨日日报只需要更改date的算法即可,可以根据自己的要求写不同的JS脚本)。

const { exec } = require('child_process');
const fs = require('fs');
const path = require('path');

function runToday() {
    // 获取当前日期
    const date = moment().format('YYYY-MM-DD');
    const year = moment().format('YYYY');
    const vaultPath = app.vault.adapter.basePath;
    const filePath = `Plans/${year}/${date}.md`;
    const fullPath = path.join(vaultPath, filePath);
    const model = 'kimi-k2.5';
    
    // 构建iflow命令
    const cmd = `npx iflow --model ${model} "使用note-reviewer skill总结${date}的日报"`;
    
    new Notice(`正在生成 AI 总结: ${date},请稍候...`, 5000);
    
    try {
        exec(cmd, {
            cwd: vaultPath,
            windowsHide: true,
            maxBuffer: 1024 * 1024 // 1MB buffer for large output
        }, (error, stdout, stderr) => {
            if (error) {
                console.error('执行错误:', error);
                new Notice(`生成失败: ${error.message}`, 5000);
                exec(`start powershell -NoExit -Command "${cmd}"`);
                return;
            }
            
            // 提取 AI 总结内容(去掉 Execution Info 部分)
            let summary = stdout;
            const executionInfoIndex = summary.indexOf('<Execution Info>');
            if (executionInfoIndex !== -1) {
                summary = summary.substring(0, executionInfoIndex).trim();
            }
            
            // 读取原文件
            let content = '';
            try {
                content = fs.readFileSync(fullPath, 'utf8');
            } catch (e) {
                new Notice(`无法读取文件: ${filePath}`, 5000);
                return;
            }
            
            // 在 AI Summary 部分后插入总结
            const aiSummaryMarker = '## AI Summary';
            const aiSummaryIndex = content.indexOf(aiSummaryMarker);
            
            if (aiSummaryIndex !== -1) {
                // 找到下一个 ## 标题的位置
                const nextSectionIndex = content.indexOf('\n## ', aiSummaryIndex + aiSummaryMarker.length);
                
                // 构建新的内容
                const summarySection = `\n\n${summary}\n`;
                
                if (nextSectionIndex !== -1) {
                    // 在 AI Summary 和下一个部分之间插入
                    content = content.substring(0, nextSectionIndex) +
                              summarySection +
                              content.substring(nextSectionIndex);
                } else {
                    // 在文件末尾插入
                    content = content + summarySection;
                }
                
                // 写回文件
                fs.writeFileSync(fullPath, content);
                new Notice(`AI 总结已写入: ${filePath}`, 5000);
            } else {
                // 如果没有 AI Summary 部分,添加到文件末尾
                content = content + '\n\n## AI Summary\n\n' + summary;
                fs.writeFileSync(fullPath, content);
                new Notice(`AI 总结已写入: ${filePath}`, 5000);
            }
        });
    } catch (error) {
        console.error('捕获错误:', error);
        new Notice(`启动失败: ${error.message}`, 5000);
    }
}

module.exports = runToday;

定义button的时候就可以直接调用QuickAdd,或者可以ctrl+p直接选择这两个命令。

const container = dv.el('div', '', { attr: { style: 'margin: 10px 0;' } });

const executeQuickAdd = (choiceId) => {
  const commandId = `quickadd:choice:${choiceId}`;
  if (app.commands.commands[commandId]) {
    app.commands.executeCommandById(commandId);
  }
};

const todayBtn = dv.el('button', '总结今天', {
  attr: {
    style: 'padding:10px 20px;background:#4CAF50;color:white;border:none;border-radius:6px;cursor:pointer;margin-right:10px;font-size:14px;'
  }
});
todayBtn.onclick = () => executeQuickAdd('summarize-today');

const yesterdayBtn = dv.el('button', '总结昨天', {
  attr: {
    style: 'padding:10px 20px;background:#2196F3;color:white;border:none;border-radius:6px;cursor:pointer;font-size:14px;'
  }
});
yesterdayBtn.onclick = () => executeQuickAdd('summarize-yesterday');

container.appendChild(todayBtn);
container.appendChild(yesterdayBtn);
dv.container.appendChild(container);

周期性复盘触发

我自己手动填写的部分只有每天的日报,以及周报中下周的任务。所以周期性复盘的内容都是在创建复盘文件的时候通过templator自动触发的。

在创建的Weekly Note Template里面增加如下代码,并通过定义的weekly_review.js脚本来触发(就是tp.user.weekly_review这行代码)。

<%*
const currentWeek = tp.date.now('YYYY-[W]WW');
const year = tp.date.now('YYYY');
const weekFile = `Plans/${year}/${currentWeek}.md`;

// 自动触发周复盘
const result = tp.user.weekly_review(weekFile);
tR += result;
%>

weekly_review.js

const { exec } = require('child_process');
const fs = require('fs');
const path = require('path');

function generateWeeklyReview(weekFile) {
    const vaultPath = app.vault.adapter.basePath;
    const fullPath = path.join(vaultPath, weekFile);
    
    // 创建基础框架...
    
    // 调用 AI skill
    const iflowPath = 'C:\\Users\\<user_name>\\AppData\\Roaming\\npm\\iflow.ps1';
    const command = `start powershell -ExecutionPolicy Bypass -Command "& '${iflowPath}' '用note-reviewer 生成周复盘,文件路径是${weekFile}'"`;
    
    exec(command, { cwd: vaultPath, windowsHide: false });
    
    return `周复盘文件已创建,AI 生成中...`;
}

module.exports = generateWeeklyReview;

其它的周期性复盘同理。

除了AI的复盘内容,我还用dataview来统计每周的tracker的完成情况,以及时长。

tracker dataview

TABLE WITHOUT ID
	file.link AS "Day",
	hours AS "工作",
	workout AS "运动",
	reading AS "阅读",
	meditation AS "冥想"
FROM "Plans/<% tp.date.now('YYYY') %>"
WHERE file.day >= date("<% tp.date.now('YYYY-MM-DD', - (tp.date.now('d') - 1)) %>") 
  AND file.day <= date("<% tp.date.now('YYYY-MM-DD', 6 - (tp.date.now('d') - 1)) %>")
FLATTEN row["Hours Worked"] AS hours
FLATTEN row["Workout"] AS workout
FLATTEN row["Reading"] AS reading  
FLATTEN row["Meditation"] AS meditation
SORT file.day ASC
const year = "<% tp.date.now('YYYY') %>";

// 计算本周一和周日
const today = moment();
const dayOfWeek = today.day(); // 0=周日, 1=周一...
const daysToMonday = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
const startOfWeek = today.clone().subtract(daysToMonday, 'days').startOf('day');
const endOfWeek = startOfWeek.clone().add(6, 'days').endOf('day');

// 生成本周7天的日期
const weekDates = [];
for (let i = 0; i < 7; i++) {
  weekDates.push(startOfWeek.clone().add(i, 'days').format('YYYY-MM-DD'));
}

let totalWork = 0;
let totalWorkout = 0;
let totalReading = 0;
let meditationCount = 0;
let dayCount = 0;

weekDates.forEach(dateStr => {
  const page = dv.page(`Plans/${year}/${dateStr}`);
  if (page) {
    dayCount++;
    
    const hw = page["Hours Worked"];
    const wo = page["Workout"];
    const rd = page["Reading"];
    const md = page["Meditation"];
    
    if (hw) {
      const workStr = String(hw);
      const match = workStr.match(/([0-9.]+)/);
      if (match) totalWork += parseFloat(match[1]);
    }
    
    if (wo) {
      const str = String(wo);
      const match = str.match(/([0-9.]+)/);
      if (match) {
        let val = parseFloat(match[1]);
        if (str.includes('h')) val *= 60;
        totalWorkout += val;
      }
    }
    
    if (rd) {
      const str = String(rd);
      const match = str.match(/([0-9.]+)/);
      if (match) {
        let val = parseFloat(match[1]);
        if (str.includes('h')) val *= 60;
        totalReading += val;
      }
    }
    
    if (md && String(md).trim() !== '' && String(md) !== '0') {
      meditationCount++;
    }
  }
});

dv.paragraph(`**工作时长**: ${totalWork.toFixed(1)} 小时 (共 ${dayCount} 天)`);
dv.paragraph(`**运动**: ${Math.floor(totalWorkout/60)}小时 ${totalWorkout%60}分钟`);
dv.paragraph(`**阅读**: ${Math.floor(totalReading/60)}小时 ${totalReading%60}分钟`);
dv.paragraph(`**冥想**: ${meditationCount} 天`);

AI不是珍妮纺织机

我很喜欢这套方案,一个很重要的原因是,它不是单独解决”写”或者”总结”的问题,而是在管理个人的行为过程。以前会把复盘对我来说是一种额外任务,还需要占用我自己的休息时间总结。之前在chat中结合AI写日记,真的让我养成了良好的记录习惯,留下了很多自然、真实的行为和思考痕迹,但是chat的记忆又是有限的。用笔记来做持久化的存储,对我来说就是最好的选择了。

而且我觉得这套组合最有价值的地方,在于它的可自定义贯穿了整个链路。记录入口可以自定义,你可以决定日报长什么样、按钮放在哪里、触发方式是什么;AI 的反馈逻辑可以自定义,你可以把自己的日记反馈、周报复盘、月度总结分别做成不同的 skill;数据组织方式也可以自定义,你可以决定日报如何汇总到周报,周报如何进一步进入月报,以及不同层级的复盘重点分别是什么。它不是让你去适应一个现成工具的固定结构,而是允许你按自己的行为管理逻辑反过来设计系统。

我看总有人把AI类比成工业革命时期的珍妮纺织机,但是真的不一样。机械是有一套标准流程的,所有人按照那个流程操作,得出来的结果都是一样的。但是AI不是的,AI是一个更灵活的工具,就像我之前说过的,在使用之前,至少要把自己日常的工作流程捋清楚,然后才能找到更适合的使用方式。不管是单一的agent还是养个龙虾,最重要的是知道要做什么,而不是用哪个工具。