import { execFileSync } from 'node:child_process'; import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from 'node:fs'; import { homedir } from 'node:os'; import { basename, isAbsolute, join, relative } from 'node:path'; import { DatabaseSync } from 'node:sqlite'; import { fileURLToPath } from 'node:url'; const home = homedir(); const project = fileURLToPath(new URL('../..', import.meta.url)); export const aiwork = process.env.AIWORK_REPO || join(home, 'Desktop/EMdev/dongdong-cowork'); const discoveryRoots = [project, join(home, 'Desktop/EMdev'), join(home, '.codex/worktrees'), aiwork]; const day = 86_400_000; const shanghai = 8 * 3_600_000; export function weekWindow(now = Date.now()) { const today = new Date(now + shanghai); const midnight = Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate()); const start = midnight - ((today.getUTCDay() + 6) % 7) * day - shanghai; const end = Math.min(now, start + 5 * day - 1); const local = (t: number) => new Date(t + shanghai).toISOString().replace('Z', '+08:00'); return { start, end, from: local(start), through: local(end), timezone: 'Asia/Shanghai' }; } export function within(root: string, path: string) { const rel = relative(root, path); return rel !== '..' && !rel.startsWith('../') && !isAbsolute(rel); } export function childEnvironment() { // Git, rg and the evidence command only need local paths and locale. return { HOME: home, PATH: process.env.PATH, LANG: process.env.LANG, CODEX_HOME: process.env.CODEX_HOME, XDG_DATA_HOME: process.env.XDG_DATA_HOME, AIWORK_REPO: process.env.AIWORK_REPO, WEEKLY_AUTHOR_EMAILS: process.env.WEEKLY_AUTHOR_EMAILS, GIT_OPTIONAL_LOCKS: '0', GIT_TERMINAL_PROMPT: '0', }; } export function git(cwd: string, ...args: string[]) { return execFileSync('git', ['--no-optional-locks', '-c', 'core.fsmonitor=false', '-C', cwd, ...args], { encoding: 'utf8', env: childEnvironment(), timeout: 8000, maxBuffer: 24 * 1024 * 1024, stdio: ['ignore', 'pipe', 'pipe'], }).trim(); } type Window = ReturnType; type Session = { cwd: string; time: number }; type Source = { source: string; status: string; sessions: Session[] }; function fromDatabase(source: string, path: string, sql: string, window: Window): Source { if (!existsSync(path)) return { source, status: '本机未找到会话库', sessions: [] }; try { const db = new DatabaseSync(path, { readOnly: true, timeout: 300 }); try { const sessions = db.prepare(sql).all(window.start, window.end) as unknown as Session[]; return { source, status: '已查询', sessions }; } finally { db.close(); } } catch { return { source, status: '会话库无法读取', sessions: [] }; } } function fromDSH(window: Window): Source { const source = 'DSH'; const dir = join(home, '.dsh/storages/session_projcache/sessions'); if (!existsSync(dir)) return { source, status: '本机未找到会话缓存', sessions: [] }; const sessions: Session[] = []; for (const entry of readdirSync(dir, { withFileTypes: true })) { if (!entry.isFile() || !entry.name.endsWith('.json')) continue; try { const path = join(dir, entry.name); if (statSync(path).size > 2_000_000) continue; const record = JSON.parse(readFileSync(path, 'utf8')).record; const cwd = record?.identity?.cwd; const time = Number(record?.rows?.sessionListMetadata?.val?.lastPromptAt); if (typeof cwd === 'string' && time >= window.start && time <= window.end) sessions.push({ cwd, time }); } catch { // A cache entry may be replaced while DSH is running. } } return { source, status: '已查询', sessions }; } function sessionSources(window: Window): Source[] { const codexHome = process.env.CODEX_HOME || join(home, '.codex'); const codexDB = existsSync(codexHome) ? readdirSync(codexHome).filter((file) => /^state_\d+\.sqlite$/.test(file)).sort((a, b) => Number(b.match(/\d+/)?.[0]) - Number(a.match(/\d+/)?.[0]))[0] : undefined; return [ fromDatabase('Codex', join(codexHome, codexDB || 'state_5.sqlite'), 'SELECT cwd, COALESCE(updated_at_ms, updated_at * 1000) AS time FROM threads WHERE COALESCE(updated_at_ms, updated_at * 1000) BETWEEN ? AND ?', window), fromDSH(window), fromDatabase('OpenCode', join(process.env.XDG_DATA_HOME || join(home, '.local/share'), 'opencode/opencode.db'), 'SELECT directory AS cwd, time_updated AS time FROM session WHERE parent_id IS NULL AND time_updated BETWEEN ? AND ?', window), ]; } export function verifiedEmails() { const configured = process.env.WEEKLY_AUTHOR_EMAILS; if (configured) return configured.split(',').map((part) => part.trim().toLowerCase()).filter(Boolean); try { return git(aiwork, 'config', '--get', 'user.email').toLowerCase().split(/\r?\n/).filter(Boolean); } catch { return []; } } function commits(cwd: string, window: Window, emails: string[]) { if (!emails.length) return []; const detachedHeads = git(cwd, 'worktree', 'list', '--porcelain').split('\n\n') .filter((entry) => entry.split('\n').includes('detached')) .flatMap((entry) => entry.match(/^HEAD ([0-9a-f]{40,64})$/m)?.[1] || []); let raw: string; try { raw = git(cwd, 'log', '--all', ...detachedHeads, '--no-merges', '--fixed-strings', ...emails.map((email) => `--author=<${email}>`), '--format=%H%x1f%aI%x1f%ae%x1f%s%x00', '--'); } catch { if (!git(cwd, 'for-each-ref', '--count=1', '--format=%(refname)') && !detachedHeads.length) return []; throw new Error(`无法读取 ${basename(cwd)} 的 Git 历史`); } const seen = new Set(); return raw.split('\0').flatMap((line) => { const [sha, authored, email, subject] = line.trim().split('\x1f'); const timestamp = Date.parse(authored); if (!emails.includes(email?.toLowerCase()) || !(timestamp >= window.start && timestamp <= window.end)) return []; // Same author, author time and subject typically identify a replayed commit. const identity = `${email}\0${authored}\0${subject}`; if (seen.has(identity)) return []; seen.add(identity); return [{ sha: sha.slice(0, 12), authored, subject: subject.slice(0, 250) }]; }).sort((a, b) => Date.parse(b.authored) - Date.parse(a.authored)).slice(0, 20); } export function repositoryLog(cwd: string) { const window = weekWindow(); const emails = verifiedEmails(); return { repository: cwd, window, verifiedAuthors: emails.length, commits: commits(cwd, window, emails) }; } function artifacts(cwd: string, window: Window) { const paths: { path: string; modified: string }[] = []; for (const folder of ['research', 'explainers', 'docs']) { try { for (const entry of readdirSync(join(cwd, folder), { withFileTypes: true })) { if (!entry.isFile() || !/^[\w\u4e00-\u9fa5-]+\.(md|html)$/i.test(entry.name)) continue; const path = join(cwd, folder, entry.name); const stat = statSync(path); if (stat.mtimeMs >= window.start && stat.mtimeMs <= window.end && stat.size < 300_000) { paths.push({ path: `${folder}/${entry.name}`, modified: stat.mtime.toISOString() }); } } } catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; } } return paths.sort((a, b) => b.modified.localeCompare(a.modified)).slice(0, 8); } export function collectEvidence() { const window = weekWindow(); const sources = sessionSources(window); const groups = new Map; clues: Set }>(); const candidates = new Map>(); for (const source of sources) { for (const { cwd } of source.sessions) { if (!candidates.has(cwd)) candidates.set(cwd, new Set()); candidates.get(cwd)?.add(source.source); } } for (const [cwd, clues] of candidates) { try { if (!isAbsolute(cwd)) continue; const real = realpathSync(cwd); if (!discoveryRoots.some((root) => within(root, real))) continue; const root = git(real, 'rev-parse', '--show-toplevel'); if (!discoveryRoots.some((allowed) => within(allowed, root))) continue; const common = git(root, 'rev-parse', '--path-format=absolute', '--git-common-dir'); const group = groups.get(common) ?? { path: root, worktrees: new Set(), clues: new Set() }; group.worktrees.add(root); for (const clue of clues) group.clues.add(clue); groups.set(common, group); } catch { // Some recent sessions have no Git repo or refer to a removed worktree. } } const emails = verifiedEmails(); return { window, sources: sources.map(({ source, status, sessions }) => ({ source, status, sessions: sessions.length })), verifiedAuthors: emails.length, repositories: [...groups.values()].map(({ path, worktrees, clues }) => ({ path, sources: [...clues], worktrees: [...worktrees], commits: commits(path, window, emails), artifacts: [...worktrees].flatMap((cwd) => artifacts(cwd, window).map((entry) => ({ cwd, ...entry }))), })), }; } if (process.argv[1] === fileURLToPath(import.meta.url)) { const repo = process.argv[2]; const evidence = repo ? repositoryLog(repo) : collectEvidence(); process.stdout.write(JSON.stringify(evidence) + '\n'); }