Obsidianにログを集約するための仕組みを作った
やりたかったこと
iPhoneで日誌的に記録する
最終的にはObsidianのデイリーノートに集約する
前提と方針
私はObsidianのファイルはローカルに保存して、Obsidian Syncで同期している
そのため、デイリーノートに直接書き込む方式だと同期ミスが起こる可能性がある
そこでChatGPTに相談して、
Inbox.mdを作成しておき、そこにログを集約する。
後から、Templaterのスクリプトでデイリーノートの末尾の#LOGの下に転記する
ことにした
つまり、スマホだけでしか書き込まないログ専用のページを持つ
やったこと
iPhoneで日誌的な記録ができるようショートカットを設定
テキストを入力して送ったらObsidianのInbox.mdに入力される
- 時刻 入力したテキスト %%隠した正確な日時%%が入力されるようにした
例えば、- 16:54 テスト %%2025-09-25T16:54%%が入力される
Obsidian側の設定(Inbox→デイリー)
スクリプト(Templater)の設定
merge_inbox_to_daily.mdファイルに以下のコードを入力
code:merge_inbox_to_daily
<%*
/* 本文に何も出さない */
tR = "";
/* ===== 設定 ===== */
const DAILY_DIR = "Daily"; // デイリーノートのフォルダ
const DATE_FMT = "YYYY-MM-DD"; // デイリーファイル名のフォーマット
const SECTION_HDR = "# LOG"; // 追記・整列対象の見出し
const CUTOFF_MIN = 4 * 60; // 04:00を境に前日扱い
/* ===== ユーティリティ ===== */
const vault = app.vault;
const adapter = vault.adapter;
function pad(n){ return (n<10?"0":"")+n; }
function ymd(d){ return ${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}; }
function minutesFromHHmm(hhmm){ const h,m=hhmm.split(":").map(Number); return h*60+m; } function makeLocalDate(Y,M,D,hh,mm){ return new Date(+Y, +M-1, +D, +hh, +mm, 0, 0); }
/* フォルダ作成(パスの最終要素はファイル名想定なので除外) */
async function ensureFolder(path){
const parts = path.split("/");
let cur = "";
for (const p of parts.slice(0, -1)) {
cur += (cur ? "/" : "") + p;
if (!(await adapter.exists(cur))) await vault.createFolder(cur);
}
}
async function ensureDailyFile(dateStr){
const dailyPath = ${DAILY_DIR}/${dateStr}.md;
let f = vault.getAbstractFileByPath(dailyPath);
if (f) return f;
await ensureFolder(dailyPath);
const header = # ${dateStr}\n\n${SECTION_HDR}\n;
return await vault.create(dailyPath, header);
}
function findLogSectionBounds(lines){
const hRegex = /^#{1,6}\s+LOG\s*$/i;
const headIdx = lines.findIndex(l => hRegex.test(l.trim()));
if (headIdx === -1) return { headIdx: -1, nextIdx: -1, level: -1 };
const level = (linesheadIdx.match(/^#+/)0 || "#").length; let nextIdx = lines.length;
for (let i=headIdx+1;i<lines.length;i++){
const m = linesi.match(/^#{1,6}\s+/); if (m && m0.length <= level){ nextIdx = i; break; } }
return { headIdx, nextIdx, level };
}
/* LOGセクションに text を追記(見出しが無ければ作成) */
async function appendUnderHeaderSorted(file, textToAdd){
const raw = await vault.read(file);
const lines = raw.split("\n");
// LOGセクション境界
let { headIdx, nextIdx, level } = findLogSectionBounds(lines);
// 無ければ末尾に作成
if (headIdx === -1){
const appended = (raw.endsWith("\n") ? raw : raw+"\n") + ${SECTION_HDR}\n + textToAdd.trim() + "\n";
await vault.modify(file, appended);
return;
}
// 既存LOGの本文(headの次行〜nextIdx-1)
const before = lines.slice(0, headIdx+1).join("\n");
const body = lines.slice(headIdx+1, nextIdx).join("\n");
const after = lines.slice(nextIdx).join("\n");
// 既存行+新規行をまとめて整形
const existingLines = body.split("\n").map(l=>l.trim()).filter(l=>l.length>0);
const newLines = textToAdd.split("\n").map(l=>l.trim()).filter(l=>l.length>0);
// 重複除去と整形(- HH:mm テキスト に統一、隠しタグ削除)
const normalize = (s)=>{
let t = s.replace(/%%\s*0-9:\-T+\s*%%/g, "").trim(); // 隠しタグ除去 t = t.replace(/^-\s*(0-9{1,2}:0-9{2})\s*/, (_m, tm)=>- ${tm} ); // 時刻の後にスペース return t;
};
// 既存+新規の重複を除去(上書きにならないよう先勝ちで)
const seen = new Set();
const deduped = [];
for (const l of all){
const key = l;
if (seen.has(key)) continue;
seen.add(key);
deduped.push(l);
}
// ソート:- HH:mm ... を昇順、時刻なしは後ろ(元の順を維持)
const timedRe = /^-\s*(0-9{1,2}):(0-9{2})\b/; const indexed = deduped.map((l, idx)=>{
const m = l.match(timedRe);
return m ? { line:l, idx, timed:true, mins: (+m1)*60+(+m2) } : { line:l, idx, timed:false, mins: 999999 }; });
indexed.sort((a,b)=>{
if (a.timed && b.timed) return a.mins - b.mins || a.idx - b.idx;
if (a.timed && !b.timed) return -1;
if (!a.timed && b.timed) return 1;
return a.idx - b.idx;
});
const newBody = indexed.map(x=>x.line).join("\n");
// 再構成して書き戻し
const glue1 = before.endsWith("\n") ? "" : "\n";
const glue2 = (after ? (after.startsWith("\n")?"":"\n") : "");
const newContent = before + glue1 + newBody + "\n" + (after ? glue2 + after : "");
await vault.modify(file, newContent);
}
/* ===== メイン ===== */
try {
// Inbox = 開いているノート
const inboxPath = tp.file.path(true);
const inboxFile = vault.getAbstractFileByPath(inboxPath);
if (!inboxFile){ new Notice("⚠️ Inboxが見つかりません"); return; }
const inboxRaw = (await vault.read(inboxFile)).trim();
if (!inboxRaw){ new Notice("ℹ️ Inboxは空です"); return; }
// 行を整形:ゴミ除去&フォーマット補正
const rows = inboxRaw.split(/\r?\n/)
.map(l => l.trim())
.filter(Boolean)
.filter(l => !/^#\s*LOG\s*$/i.test(l)) // # LOG の見出しを捨てる
.filter(l => !/^✅\s*マージ完了/.test(l)) // 完了メッセージを捨てる
.filter(l => l !== "`"); // コードフェンス残骸を捨てる
// 日付ごとにバケツ分け(隠しタグ or HH:mm + 4:00締め)
const bucket = {}; // { 'YYYY-MM-DD': lines... } for (const row of rows){
const tag = row.match(/%%\s*(0-9{4})-(0-9{2})-(0-9{2})T(0-9{2}):(0-9{2})(?::0-9{2})?\s*%%/); let dateStr;
if (tag){
const Y,M,D,hh,mm = tag;
const dt = makeLocalDate(Y,M,D,hh,mm);
const mins = (+hh)*60 + (+mm);
dateStr = (mins < CUTOFF_MIN)
? ymd(new Date(dt.getFullYear(), dt.getMonth(), dt.getDate()-1))
: ymd(dt);
} else {
// タグが無い:先頭の HH:mm から推定、無ければ今日
const m = row.match(/^\-\s*(0-9{1,2}:0-9{2})\b/); if (m){
const mins = minutesFromHHmm(m1); const base = new Date();
const baseDate = (mins < CUTOFF_MIN)
? new Date(base.getFullYear(), base.getMonth(), base.getDate()-1)
: new Date(base.getFullYear(), base.getMonth(), base.getDate());
dateStr = ymd(baseDate);
} else {
dateStr = tp.date.now(DATE_FMT);
}
}
}
// 各日付の LOG に結合&整列で反映
const dates = Object.keys(bucket).sort();
for (const ds of dates){
const f = await ensureDailyFile(ds);
await appendUnderHeaderSorted(f, "\n" + bucketds.join("\n") + "\n"); }
// Inbox を空に
await vault.modify(inboxFile, "");
new Notice("✅ マージ&整列完了: " + dates.join(", "));
} catch (e){
console.error(e);
new Notice("❌ エラー: " + (e?.message ?? e));
}
%>
これを実行するとInbox.mdの内容が当日のデイリーノートの#LOGに追記される
https://gyazo.com/404f35156627e2706a8902abcc4ba038
すでに記入済みのものがあっても時系列に整理してくれる
https://gyazo.com/cddfdbde61f670da2b0622adb15028b6
例えば、「翌日の朝にまとめてデイリーに送る」が可能になる
この作業ができなかったとしても、正確な日付が入力されるようになっているから、過去のデイリーノートにも送ってくれる
ただし、そのためには
HH:mmのフォーマットで揃えておく
LOGは1行で書く(階層にしない)
必要がある
実行後、Inbox.mdは空になる
実行方法
コマンドパレット
Templater:Insert template→でmerge_inbox_to_dailyを実行する
https://gyazo.com/2c66d2b56ff88e032b34e751ba8d36ed
ショートカットキー:Cmd + Ctrl + Mに設定
これでコマンドパレット、もしくはショートカットキーでできる
Inbox.mdにのみ「デイリーに送る」ボタンを表示
ボタンを押すとmerge_inbox_to_dailyが実行される
https://gyazo.com/f3b00674b5931f807ad78fb0a633710e
PCでもInboxにメモしちゃうと同期ミスは起こるから、PCでは直接デイリーに書いて、最終的に時系列で整理されるのが良さそう
PCはRaycast経由で末尾に記入されるようにした