diff --git a/worker/index.js b/worker/index.js index 97496c6..bbe919f 100644 --- a/worker/index.js +++ b/worker/index.js @@ -9,7 +9,9 @@ // - /api/ev-stats その記録を週ごと・ページごとに数えて見せる(数だけ。誰かは分からない) // - ページ・llms.txt・全文の Markdown など 静的ファイルを返す。検索エンジン・AI のクローラーが読みに来たら、R2(p2m-messages の cr/)に1件ずつ残す // - /api/cr-stats その記録を、どのクローラーが・いつ・どのページを読んだかで数えて見せる(2026-10-25) -// - 5分ごとの scheduled SNS の予約投稿の時刻が来ていたら GitHub Actions を起こす(worker/sns_cron.js) +// - /api/kpi-stats 主に見る3つの数字(AI の依頼読み・AI のリンクからの来訪・サイト名の検索)を日ごと・週ごとに見せる(worker/kpi.js。2026-11-06) +// - /mcp/{鍵} サイトの計測を Claude につなぐ部品(MCP サーバー。worker/mcp.js。2026-11-06) +// - 5分ごとの scheduled SNS の予約投稿の時刻が来ていたら GitHub Actions を起こす(worker/sns_cron.js)。前の日の3つの数字がまだなければ集計して残す(worker/kpi.js) // // 守り(2026-10-23) // - 数の表(/api/hk-stats・/api/ev-stats・/api/cr-stats)は、鍵(Cloudflare の秘密の値 STATS_KEY)を付けたときだけ見られる。鍵がなければ 404 @@ -19,10 +21,14 @@ // - このファイルが返す応答には、内容の種類を推測させない指定(nosniff)を付ける。ページ側の指定は public/_headers import { EmailMessage } from 'cloudflare:email'; import { snsCron } from './sns_cron.js'; +import { kpiCron, kpiRange, todayJst, addDays } from './kpi.js'; +import { handleMcp } from './mcp.js'; +import { gscRefresh } from './gsc.js'; export default { async fetch(request, env, ctx) { const url = new URL(request.url); + if (url.pathname.startsWith('/mcp/')) return secured(await handleMcp(request, env, url, { evStats, crStats, hkStats })); if (url.pathname.startsWith('/api/') || url.pathname.startsWith('/media/') || url.pathname === '/pixelstomotors' || url.pathname.startsWith('/pixelstomotors/')) { return secured(await route(request, env, url)); } @@ -59,6 +65,11 @@ export default { // 5分ごと(wrangler.jsonc の triggers):SNS の予約投稿の時刻が来ていたら、GitHub Actions を起こす(worker/sns_cron.js) async scheduled(event, env, ctx) { ctx.waitUntil(snsCron(env, event.scheduledTime).then((m) => console.log('sns-cron:', m), (e) => console.error('sns-cron:', e.message))); + // 前の日の分を作ったとき(1日1回)だけ、Search Console からサイト名の検索の数も取り直す(worker/gsc.js) + ctx.waitUntil(kpiCron(env).then(async (m) => { + console.log(m); + if (m.startsWith('kpi: made')) console.log('gsc:', JSON.stringify(await gscRefresh(env))); + }).catch((e) => console.error('kpi-cron:', e.message))); }, }; @@ -101,6 +112,7 @@ async function route(request, env, url) { if (url.pathname === '/api/hk-stats') return (await statsAllowed(request, env, url)) ? hkStats(request, env, url) : notFound(); if (url.pathname === '/api/ev-stats') return (await statsAllowed(request, env, url)) ? evStats(request, env, url) : notFound(); if (url.pathname === '/api/cr-stats') return (await statsAllowed(request, env, url)) ? crStats(request, env, url) : notFound(); + if (url.pathname === '/api/kpi-stats') return (await statsAllowed(request, env, url)) ? kpiStats(request, env, url) : notFound(); return env.ASSETS.fetch(request); } @@ -308,15 +320,6 @@ async function hkStats(request, env, url) { const byDay = new Map(), byWeek = new Map(), byItem = new Map(), byW = new Map(); for (const r of rows) { add(byDay, r.day, r.e); add(byWeek, weekOf(r.day), r.e); add(byItem, r.id, r.e); add(byW, r.w, r.e); } const KIND_JA = { n: '機械の印なし', b: '機械', o: '印なし(10/28より前)' }; - // 「AIで読む」経由(?via=)。押した AI ごとに、実際に読みに来たのはだれか(2026-11-01) - const viaMap = new Map(); - for (const r of rows) { - if (!r.v) continue; - const e = viaMap.get(r.v) || { via: r.v, visits: 0, records: new Set(), bots: new Map(), last: '' }; - e.visits++; e.records.add(r.p); count(e.bots, r.b); if (r.day > e.last) e.last = r.day; viaMap.set(r.v, e); - } - const viaWeeks = new Map(); - for (const r of rows) { if (!r.v) continue; const w = viaWeeks.get(r.week) || new Map(); count(w, r.v); viaWeeks.set(r.week, w); } const data = { total: rows.length, who: who || null, @@ -346,7 +349,6 @@ h1{font-size:20px}h2{font-size:16px;margin-top:28px}table{border-collapse:collap
記録の件数:${data.total}