vscode-templete/.claude/skills/auto-wiki-cn/references/store.py

308 lines
12 KiB
Python
Raw Permalink Normal View History

"""Wiki 结构化数据存储层。
每个 wiki 目录下维护一个 data.dbSQLite存储所有结构化数据
Markdown 页面只负责叙事分析不在 frontmatter 中存储 data/history
用法
from store import WikiStore
store = WikiStore(".wiki/my-research/")
store.upsert_data("alpha-corp", "管理规模", 1200, "亿元", "2025-12", "2026-04-policy-doc")
store.add_relation("alpha-corp", "受托人市场格局", "part_of")
# 查询
rows = store.query_data(page_slug="alpha-corp")
timeline = store.query_timeline(field="管理规模")
CLI:
python store.py init .wiki/my-research/
python store.py dump .wiki/my-research/
"""
from __future__ import annotations
import sqlite3
import sys
from datetime import date, datetime
from pathlib import Path
from typing import Any, Optional
SCHEMA_SQL = """
CREATE TABLE IF NOT EXISTS pages (
slug TEXT PRIMARY KEY,
title TEXT NOT NULL,
type TEXT NOT NULL CHECK(type IN ('source','entity','concept','analysis','mental-model')),
confidence TEXT NOT NULL DEFAULT 'medium' CHECK(confidence IN ('high','medium','low','contested')),
created TEXT NOT NULL,
updated TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS data_points (
id INTEGER PRIMARY KEY AUTOINCREMENT,
page_slug TEXT NOT NULL REFERENCES pages(slug),
field TEXT NOT NULL,
value REAL NOT NULL,
unit TEXT NOT NULL,
period TEXT NOT NULL,
source_slug TEXT NOT NULL,
scope TEXT,
verified INTEGER, -- NULL=unknown, 0=false, 1=true
confidence TEXT DEFAULT 'high' CHECK(confidence IN ('high','medium','low','contested')),
created_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE(page_slug, field, period) -- 同一页面同一字段同一时段只留一条upsert 覆盖
);
CREATE TABLE IF NOT EXISTS history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
page_slug TEXT NOT NULL REFERENCES pages(slug),
field TEXT NOT NULL,
old_value REAL NOT NULL,
old_unit TEXT NOT NULL,
old_source TEXT NOT NULL,
new_source TEXT,
reason TEXT NOT NULL,
date TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS relations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
from_slug TEXT NOT NULL,
to_slug TEXT NOT NULL,
type TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE(from_slug, to_slug, type)
);
CREATE INDEX IF NOT EXISTS idx_dp_page ON data_points(page_slug);
CREATE INDEX IF NOT EXISTS idx_dp_field ON data_points(field);
CREATE INDEX IF NOT EXISTS idx_dp_period ON data_points(period);
CREATE INDEX IF NOT EXISTS idx_rel_from ON relations(from_slug);
CREATE INDEX IF NOT EXISTS idx_rel_to ON relations(to_slug);
CREATE INDEX IF NOT EXISTS idx_hist_page ON history(page_slug);
"""
class WikiStore:
"""单个 wiki 的 SQLite 存储接口。"""
def __init__(self, wiki_dir: str | Path):
self.wiki_dir = Path(wiki_dir)
self.db_path = self.wiki_dir / "data.db"
self._conn: Optional[sqlite3.Connection] = None
@property
def conn(self) -> sqlite3.Connection:
if self._conn is None:
self._conn = sqlite3.connect(str(self.db_path))
self._conn.row_factory = sqlite3.Row
self._conn.execute("PRAGMA journal_mode=WAL")
self._conn.execute("PRAGMA foreign_keys=ON")
return self._conn
def init_db(self) -> None:
"""创建表结构(幂等)。"""
self.conn.executescript(SCHEMA_SQL)
self.conn.commit()
def close(self) -> None:
if self._conn:
self._conn.close()
self._conn = None
# ── Pages ──
def upsert_page(self, slug: str, title: str, page_type: str,
confidence: str = "medium",
created: str = "", updated: str = "") -> None:
today = date.today().isoformat()
self.conn.execute("""
INSERT INTO pages (slug, title, type, confidence, created, updated)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(slug) DO UPDATE SET
title=excluded.title, type=excluded.type,
confidence=excluded.confidence, updated=excluded.updated
""", (slug, title, page_type, confidence, created or today, updated or today))
self.conn.commit()
# ── Data Points ──
def upsert_data(self, page_slug: str, field: str, value: float,
unit: str, period: str, source_slug: str,
scope: str = None, verified: bool = None,
confidence: str = "high") -> Optional[dict]:
"""写入数据点。如果同字段同时段已有旧值,自动写入 history 并返回旧记录。"""
# 查旧值
old = self.conn.execute(
"SELECT value, unit, source_slug FROM data_points WHERE page_slug=? AND field=? AND period=?",
(page_slug, field, period)
).fetchone()
old_record = None
if old and old["value"] != value:
old_record = dict(old)
# 写 history
self.conn.execute("""
INSERT INTO history (page_slug, field, old_value, old_unit, old_source, new_source, reason, date)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""", (page_slug, field, old["value"], old["unit"], old["source_slug"],
source_slug, f"{field}: {old['value']}{value}", date.today().isoformat()))
# upsert data point
v_int = None if verified is None else (1 if verified else 0)
self.conn.execute("""
INSERT INTO data_points (page_slug, field, value, unit, period, source_slug, scope, verified, confidence)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(page_slug, field, period) DO UPDATE SET
value=excluded.value, unit=excluded.unit,
source_slug=excluded.source_slug, scope=excluded.scope,
verified=excluded.verified, confidence=excluded.confidence
""", (page_slug, field, value, unit, period, source_slug, scope, v_int, confidence))
self.conn.commit()
return old_record
# ── Relations ──
def add_relation(self, from_slug: str, to_slug: str, rel_type: str) -> None:
self.conn.execute("""
INSERT OR IGNORE INTO relations (from_slug, to_slug, type)
VALUES (?, ?, ?)
""", (from_slug, to_slug, rel_type))
self.conn.commit()
# ── Queries ──
def query_data(self, page_slug: str = None, field: str = None) -> list[dict]:
"""查询数据点。可按页面或字段过滤。"""
sql = "SELECT * FROM data_points WHERE 1=1"
params: list = []
if page_slug:
sql += " AND page_slug=?"
params.append(page_slug)
if field:
sql += " AND field=?"
params.append(field)
sql += " ORDER BY period DESC"
return [dict(r) for r in self.conn.execute(sql, params).fetchall()]
def query_timeline(self, field: str, page_slug: str = None) -> list[dict]:
"""查询某字段的时间线(含历史值)。"""
# 当前值
sql = "SELECT page_slug, field, value, unit, period, source_slug, 'current' as status FROM data_points WHERE field=?"
params: list = [field]
if page_slug:
sql += " AND page_slug=?"
params.append(page_slug)
# 历史值
sql2 = "SELECT page_slug, field, old_value as value, old_unit as unit, date as period, old_source as source_slug, 'superseded' as status FROM history WHERE field=?"
params2: list = [field]
if page_slug:
sql2 += " AND page_slug=?"
params2.append(page_slug)
rows = [dict(r) for r in self.conn.execute(sql, params).fetchall()]
rows += [dict(r) for r in self.conn.execute(sql2, params2).fetchall()]
rows.sort(key=lambda r: r.get("period", ""), reverse=True)
return rows
def query_relations(self, slug: str = None, rel_type: str = None) -> list[dict]:
sql = "SELECT * FROM relations WHERE 1=1"
params: list = []
if slug:
sql += " AND (from_slug=? OR to_slug=?)"
params += [slug, slug]
if rel_type:
sql += " AND type=?"
params.append(rel_type)
return [dict(r) for r in self.conn.execute(sql, params).fetchall()]
def get_page(self, slug: str) -> Optional[dict]:
row = self.conn.execute("SELECT * FROM pages WHERE slug=?", (slug,)).fetchone()
return dict(row) if row else None
def list_pages(self, page_type: str = None) -> list[dict]:
sql = "SELECT * FROM pages"
params: list = []
if page_type:
sql += " WHERE type=?"
params.append(page_type)
sql += " ORDER BY updated DESC"
return [dict(r) for r in self.conn.execute(sql, params).fetchall()]
def stats(self) -> dict:
"""返回 wiki 数据库统计。"""
s: dict[str, Any] = {}
s["pages"] = self.conn.execute("SELECT COUNT(*) FROM pages").fetchone()[0]
s["data_points"] = self.conn.execute("SELECT COUNT(*) FROM data_points").fetchone()[0]
s["relations"] = self.conn.execute("SELECT COUNT(*) FROM relations").fetchone()[0]
s["contested"] = self.conn.execute("SELECT COUNT(*) FROM data_points WHERE confidence='contested'").fetchone()[0]
for row in self.conn.execute("SELECT type, COUNT(*) as cnt FROM pages GROUP BY type").fetchall():
s[f"pages_{row['type']}"] = row["cnt"]
return s
def dump(self) -> str:
"""输出人类可读的数据库摘要。"""
st = self.stats()
lines = [
f"Wiki Store: {self.wiki_dir.name}",
f"{'='*50}",
f"Pages: {st['pages']} | Data Points: {st['data_points']} | Relations: {st['relations']} | Contested: {st['contested']}",
"",
]
# pages by type
for pt in ["entity", "concept", "source", "analysis", "mental-model"]:
key = f"pages_{pt}"
if st.get(key):
lines.append(f" {pt}: {st[key]}")
# recent data points
recent = self.conn.execute(
"SELECT page_slug, field, value, unit, period FROM data_points ORDER BY created_at DESC LIMIT 10"
).fetchall()
if recent:
lines += ["", "Recent Data Points:"]
for r in recent:
lines.append(f" {r['page_slug']}.{r['field']} = {r['value']} {r['unit']} ({r['period']})")
# relations
rels = self.conn.execute("SELECT * FROM relations ORDER BY created_at DESC LIMIT 10").fetchall()
if rels:
lines += ["", "Recent Relations:"]
for r in rels:
lines.append(f" {r['from_slug']} --{r['type']}--> {r['to_slug']}")
return "\n".join(lines)
# ── CLI ──
def main():
if len(sys.argv) < 3:
print("Usage:")
print(" python store.py init <wiki_dir> — initialize data.db")
print(" python store.py dump <wiki_dir> — dump database summary")
sys.exit(1)
cmd, target = sys.argv[1], Path(sys.argv[2])
store = WikiStore(target)
if cmd == "init":
store.init_db()
print(f"Initialized: {store.db_path}")
elif cmd == "dump":
if not store.db_path.exists():
print(f"No data.db found in {target}")
sys.exit(1)
print(store.dump())
else:
print(f"Unknown command: {cmd}")
sys.exit(1)
store.close()
if __name__ == "__main__":
main()