Files
projects/digital-asset-manager/digital_assets_manager.py
T
2026-08-05 17:25:11 +08:00

1976 lines
71 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
个人项目与数字资产管理器
=========================
使用SQLite本地数据库管理项目进度、技术决策、踩坑记录、知识卡和训练语料索引。
纯标准库实现,命令行交互,单文件可运行。
数据库文件: digital_assets.db(与脚本同目录)
"""
import argparse
import json
import os
import sqlite3
import textwrap
from datetime import datetime
# ── 全局配置 ──────────────────────────────────────────────
DB_FILENAME = "digital_assets.db"
DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), DB_FILENAME)
# ═══════════════════════════════════════════════════════════
# 数据库核心
# ═══════════════════════════════════════════════════════════
def get_conn():
"""获取数据库连接。
启用外键约束(FOREIGN KEYS)确保级联删除正确工作,
启用WAL模式提升并发读写性能。row_factory=sqlite3.Row
让查询结果支持 dict-style 列名访问(row["col"])。
"""
conn = sqlite3.connect(DB_PATH)
conn.execute("PRAGMA foreign_keys = ON")
conn.execute("PRAGMA journal_mode = WAL")
conn.row_factory = sqlite3.Row # 让查询结果支持按列名访问
return conn
def init_db():
"""初始化数据库建表(幂等操作,已存在的表不重建)。
创建6张核心表:
- projects: 项目信息
- decisions: 技术决策
- pitfalls: 踩坑记录
- knowledge_cards: 知识卡(项目文件/工作日志)
- training_corpus: 训练语料索引
- import_history: 导入历史记录
同时创建7个索引加速常用查询(project_id/date/status/category/type)。
"""
conn = get_conn()
cursor = conn.cursor()
# ── 项目表 ──
cursor.execute("""
CREATE TABLE IF NOT EXISTS projects (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
status TEXT NOT NULL DEFAULT 'active'
CHECK(status IN ('active','paused','archived')),
description TEXT DEFAULT '',
key_milestone TEXT DEFAULT '',
related_files TEXT DEFAULT '[]', -- JSON数组
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
""")
# ── 决策表 ──
cursor.execute("""
CREATE TABLE IF NOT EXISTS decisions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
project_id INTEGER NOT NULL,
date TEXT NOT NULL,
title TEXT NOT NULL,
content TEXT DEFAULT '',
basis TEXT DEFAULT '',
conclusion TEXT DEFAULT '',
tags TEXT DEFAULT '',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
);
""")
# ── 踩坑记录表 ──
cursor.execute("""
CREATE TABLE IF NOT EXISTS pitfalls (
id INTEGER PRIMARY KEY AUTOINCREMENT,
project_id INTEGER NOT NULL,
date TEXT NOT NULL,
problem TEXT NOT NULL,
root_cause TEXT DEFAULT '',
solution TEXT DEFAULT '',
tags TEXT DEFAULT '',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
);
""")
# ── 知识卡表 ──
cursor.execute("""
CREATE TABLE IF NOT EXISTS knowledge_cards (
id INTEGER PRIMARY KEY AUTOINCREMENT,
topic TEXT NOT NULL,
category TEXT DEFAULT '',
file_path TEXT DEFAULT '',
summary TEXT DEFAULT '',
version TEXT DEFAULT '1.0',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
""")
# ── 训练语料索引表 ──
cursor.execute("""
CREATE TABLE IF NOT EXISTS training_corpus (
id INTEGER PRIMARY KEY AUTOINCREMENT,
date TEXT NOT NULL,
type TEXT NOT NULL
CHECK(type IN ('qa','decision','pitfall')),
tags TEXT DEFAULT '',
file_path TEXT DEFAULT '',
record_count INTEGER DEFAULT 0,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
""")
# ── 导入历史表 ──
cursor.execute("""
CREATE TABLE IF NOT EXISTS import_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
import_date TEXT NOT NULL,
import_type TEXT NOT NULL,
folder_path TEXT DEFAULT '',
project_name TEXT DEFAULT '',
file_count INTEGER DEFAULT 0,
summary TEXT DEFAULT '',
created_at TEXT NOT NULL
);
""")
# ── 索引:加速按外键、标签、日期、状态查询 ──
cursor.execute(
"CREATE INDEX IF NOT EXISTS idx_decisions_project ON decisions(project_id);"
)
cursor.execute(
"CREATE INDEX IF NOT EXISTS idx_pitfalls_project ON pitfalls(project_id);"
)
cursor.execute(
"CREATE INDEX IF NOT EXISTS idx_decisions_date ON decisions(date);"
)
cursor.execute(
"CREATE INDEX IF NOT EXISTS idx_pitfalls_date ON pitfalls(date);"
)
cursor.execute(
"CREATE INDEX IF NOT EXISTS idx_projects_status ON projects(status);"
)
cursor.execute(
"CREATE INDEX IF NOT EXISTS idx_knowledge_category ON knowledge_cards(category);"
)
cursor.execute(
"CREATE INDEX IF NOT EXISTS idx_corpus_type ON training_corpus(type);"
)
conn.commit()
conn.close()
print(f"[OK] 数据库初始化完成: {DB_PATH}")
# ═══════════════════════════════════════════════════════════
# 工具函数
# ═══════════════════════════════════════════════════════════
def now():
"""返回当前时间字符串,ISO格式精确到秒。
格式: "YYYY-MM-DD HH:MM:SS"
所有数据库记录的 created_at/updated_at 统一使用此函数。
"""
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
def print_rows(rows, columns=None):
"""以表格形式打印查询结果。
Args:
rows: sqlite3.Row 列表,每个 row 支持 dict-style 访问
columns: 要显示的列名列表,默认取第一行的 keys()
输出格式: 表头 | 分隔线 | N行数据 | 总条数
无结果时打印"(无结果)"。
"""
if not rows:
print("(无结果)")
return
if columns is None:
columns = rows[0].keys()
# 表头
header = " | ".join(columns)
print(header)
print("-" * len(header))
for row in rows:
vals = []
for c in columns:
v = row.get(c, "")
vals.append(str(v) if v is not None else "")
print(" | ".join(vals))
print(f"(共 {len(rows)} 条)")
def resolve_project_id(conn, project_ref):
"""将名称或ID解析为 project_id。
Args:
conn: 数据库连接
project_ref: 纯数字字符串→按ID查,否则→按项目名查
Returns:
int: project_id, 找不到返回 None
"""
cursor = conn.cursor()
if project_ref.isdigit():
cursor.execute("SELECT id FROM projects WHERE id = ?", (int(project_ref),))
else:
cursor.execute("SELECT id FROM projects WHERE name = ?", (project_ref,))
row = cursor.fetchone()
return row["id"] if row else None
def timestamp_fields():
"""返回 (created_at, updated_at) 的当前时间元组。
用于 INSERT 语句一次性获取两个时间戳字段的值。
"""
t = now()
return (t, t)
def validate_date(date_str):
"""校验日期是否严格符合 YYYY-MM-DD 格式。
Args:
date_str: 待校验字符串
Returns:
bool: 合法且可解析则 True
用于所有 --date 参数的输入验证,防止非法日期写入数据库。
"""
if date_str is None:
return False
try:
datetime.strptime(date_str, "%Y-%m-%d")
return True
except ValueError:
return False
def check_db_initialized():
"""检测数据库是否已初始化。
通过查询 sqlite_master 确认 projects 表存在。
未初始化时打印提示信息指导用户运行 init 命令。
Returns:
bool: 已初始化返回 True
"""
try:
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name='projects'"
)
exists = cursor.fetchone() is not None
conn.close()
if not exists:
print(
"[ERR] 数据库未初始化,请先运行: python digital_assets_manager.py init"
)
return exists
except Exception:
print("[ERR] 数据库未初始化,请先运行: python digital_assets_manager.py init")
return False
# ═══════════════════════════════════════════════════════════
# 命令处理
# ═══════════════════════════════════════════════════════════
# ────────── 项目 (projects) ──────────
def cmd_project_add(args):
"""添加新项目"""
conn = get_conn()
t = now()
try:
conn.execute(
"""INSERT INTO projects (name, status, description, key_milestone, related_files, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?)""",
(
args.name,
args.status or "active",
args.desc or "",
args.milestone or "",
json.dumps(args.files or [], ensure_ascii=False),
t,
t,
),
)
conn.commit()
print(f"[OK] 项目 '{args.name}' 已添加")
except sqlite3.IntegrityError as e:
print(f"[ERR] 添加失败: {e}")
finally:
conn.close()
def cmd_project_list(args):
"""列出项目,支持按状态过滤"""
conn = get_conn()
if args.status:
rows = conn.execute(
"SELECT id, name, status, key_milestone, updated_at FROM projects WHERE status = ? ORDER BY updated_at DESC",
(args.status,),
).fetchall()
else:
rows = conn.execute(
"SELECT id, name, status, key_milestone, updated_at FROM projects ORDER BY updated_at DESC"
).fetchall()
print_rows(rows)
conn.close()
def cmd_project_update(args):
"""更新项目字段"""
conn = get_conn()
pid = resolve_project_id(conn, args.project)
if pid is None:
print(f"[ERR] 未找到项目: {args.project}")
conn.close()
return
# 构建动态UPDATE
updates = {}
if args.status:
if args.status not in ("active", "paused", "archived"):
print("[ERR] status 必须是 active / paused / archived")
conn.close()
return
updates["status"] = args.status
if args.desc is not None:
updates["description"] = args.desc
if args.milestone is not None:
updates["key_milestone"] = args.milestone
if args.files is not None:
updates["related_files"] = json.dumps(args.files, ensure_ascii=False)
if not updates:
print("[WARN] 未指定要更新的字段")
conn.close()
return
updates["updated_at"] = now()
set_clause = ", ".join(f"{k} = ?" for k in updates)
values = list(updates.values()) + [pid]
conn.execute(f"UPDATE projects SET {set_clause} WHERE id = ?", values)
conn.commit()
print(f"[OK] 项目 '{args.project}' 已更新")
conn.close()
def cmd_project_delete(args):
"""删除项目(级联删除关联的决策和踩坑记录)"""
conn = get_conn()
pid = resolve_project_id(conn, args.project)
if pid is None:
print(f"[ERR] 未找到项目: {args.project}")
conn.close()
return
if not args.force:
confirm = (
input(
f"[WARN] 确定删除项目 '{args.project}' 及其关联的所有决策/踩坑记录?(y/N): "
)
.strip()
.lower()
)
if confirm != "y":
print("已取消")
conn.close()
return
conn.execute("DELETE FROM projects WHERE id = ?", (pid,))
conn.commit()
print(f"[OK] 项目 '{args.project}' 已删除")
conn.close()
# ────────── 决策 (decisions) ──────────
def cmd_decision_add(args):
"""添加决策记录"""
if not validate_date(args.date):
print("[ERR] 日期格式错误,需要 YYYY-MM-DD")
return
conn = get_conn()
pid = resolve_project_id(conn, args.project)
if pid is None:
print(f"[ERR] 未找到项目: {args.project}")
conn.close()
return
t = now()
conn.execute(
"""INSERT INTO decisions (project_id, date, title, content, basis, conclusion, tags, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""",
(
pid,
args.date,
args.title,
args.content or "",
args.basis or "",
args.conclusion or "",
args.tags or "",
t,
t,
),
)
conn.commit()
print(f"[OK] 决策 '{args.title}' 已添加到项目 '{args.project}'")
conn.close()
def cmd_decision_list(args):
"""列出决策,支持按项目过滤"""
conn = get_conn()
if args.project:
pid = resolve_project_id(conn, args.project)
if pid is None:
print(f"[ERR] 未找到项目: {args.project}")
conn.close()
return
rows = conn.execute(
"""SELECT d.id, p.name AS project, d.date, d.title, d.tags, d.updated_at
FROM decisions d JOIN projects p ON d.project_id = p.id
WHERE d.project_id = ? ORDER BY d.date DESC""",
(pid,),
).fetchall()
else:
rows = conn.execute(
"""SELECT d.id, p.name AS project, d.date, d.title, d.tags, d.updated_at
FROM decisions d JOIN projects p ON d.project_id = p.id
ORDER BY d.date DESC"""
).fetchall()
print_rows(rows)
conn.close()
def cmd_decision_update(args):
"""更新决策 — 通过ID"""
conn = get_conn()
row = conn.execute("SELECT id FROM decisions WHERE id = ?", (args.id,)).fetchone()
if not row:
print(f"[ERR] 未找到决策 ID={args.id}")
conn.close()
return
updates = {}
if args.title is not None:
updates["title"] = args.title
if args.content is not None:
updates["content"] = args.content
if args.basis is not None:
updates["basis"] = args.basis
if args.conclusion is not None:
updates["conclusion"] = args.conclusion
if args.tags is not None:
updates["tags"] = args.tags
if args.date is not None:
if not validate_date(args.date):
print("[ERR] 日期格式错误,需要 YYYY-MM-DD")
conn.close()
return
updates["date"] = args.date
if not updates:
print("[WARN] 未指定要更新的字段")
conn.close()
return
updates["updated_at"] = now()
set_clause = ", ".join(f"{k} = ?" for k in updates)
values = list(updates.values()) + [args.id]
conn.execute(f"UPDATE decisions SET {set_clause} WHERE id = ?", values)
conn.commit()
print(f"[OK] 决策 ID={args.id} 已更新")
conn.close()
def cmd_decision_delete(args):
"""删除决策"""
conn = get_conn()
conn.execute("DELETE FROM decisions WHERE id = ?", (args.id,))
conn.commit()
print(f"[OK] 决策 ID={args.id} 已删除(如存在)")
conn.close()
def cmd_decision_show(args):
"""查看单条决策完整内容"""
conn = get_conn()
row = conn.execute(
"""SELECT d.*, p.name AS project_name
FROM decisions d JOIN projects p ON d.project_id = p.id
WHERE d.id = ?""",
(args.id,),
).fetchone()
if not row:
print(f"[ERR] 未找到决策 ID={args.id}")
else:
print(f"ID: {row['id']}")
print(f"项目: {row['project_name']}")
print(f"日期: {row['date']}")
print(f"标题: {row['title']}")
print(f"内容: {row['content']}")
print(f"依据: {row['basis']}")
print(f"结论: {row['conclusion']}")
print(f"标签: {row['tags']}")
print(f"更新时间: {row['updated_at']}")
conn.close()
# ────────── 踩坑记录 (pitfalls) ──────────
def cmd_pitfall_add(args):
"""添加踩坑记录"""
if not validate_date(args.date):
print("[ERR] 日期格式错误,需要 YYYY-MM-DD")
return
conn = get_conn()
pid = resolve_project_id(conn, args.project)
if pid is None:
print(f"[ERR] 未找到项目: {args.project}")
conn.close()
return
t = now()
conn.execute(
"""INSERT INTO pitfalls (project_id, date, problem, root_cause, solution, tags, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
(
pid,
args.date,
args.problem,
args.root_cause or "",
args.solution or "",
args.tags or "",
t,
t,
),
)
conn.commit()
print(f"[OK] 踩坑记录已添加到项目 '{args.project}'")
conn.close()
def cmd_pitfall_list(args):
"""列出踩坑记录"""
conn = get_conn()
if args.project:
pid = resolve_project_id(conn, args.project)
if pid is None:
print(f"[ERR] 未找到项目: {args.project}")
conn.close()
return
rows = conn.execute(
"""SELECT pf.id, p.name AS project, pf.date, pf.problem, pf.tags, pf.updated_at
FROM pitfalls pf JOIN projects p ON pf.project_id = p.id
WHERE pf.project_id = ? ORDER BY pf.date DESC""",
(pid,),
).fetchall()
else:
rows = conn.execute(
"""SELECT pf.id, p.name AS project, pf.date, pf.problem, pf.tags, pf.updated_at
FROM pitfalls pf JOIN projects p ON pf.project_id = p.id
ORDER BY pf.date DESC"""
).fetchall()
print_rows(rows)
conn.close()
def cmd_pitfall_update(args):
"""更新踩坑记录"""
conn = get_conn()
row = conn.execute("SELECT id FROM pitfalls WHERE id = ?", (args.id,)).fetchone()
if not row:
print(f"[ERR] 未找到踩坑记录 ID={args.id}")
conn.close()
return
updates = {}
if args.problem is not None:
updates["problem"] = args.problem
if args.root_cause is not None:
updates["root_cause"] = args.root_cause
if args.solution is not None:
updates["solution"] = args.solution
if args.tags is not None:
updates["tags"] = args.tags
if args.date is not None:
if not validate_date(args.date):
print("[ERR] 日期格式错误,需要 YYYY-MM-DD")
conn.close()
return
updates["date"] = args.date
if not updates:
print("[WARN] 未指定要更新的字段")
conn.close()
return
updates["updated_at"] = now()
set_clause = ", ".join(f"{k} = ?" for k in updates)
values = list(updates.values()) + [args.id]
conn.execute(f"UPDATE pitfalls SET {set_clause} WHERE id = ?", values)
conn.commit()
print(f"[OK] 踩坑记录 ID={args.id} 已更新")
conn.close()
def cmd_pitfall_delete(args):
"""删除踩坑记录"""
conn = get_conn()
conn.execute("DELETE FROM pitfalls WHERE id = ?", (args.id,))
conn.commit()
print(f"[OK] 踩坑记录 ID={args.id} 已删除(如存在)")
conn.close()
def cmd_pitfall_show(args):
"""查看单条踩坑记录完整内容"""
conn = get_conn()
row = conn.execute(
"""SELECT pf.*, p.name AS project_name
FROM pitfalls pf JOIN projects p ON pf.project_id = p.id
WHERE pf.id = ?""",
(args.id,),
).fetchone()
if not row:
print(f"[ERR] 未找到踩坑记录 ID={args.id}")
else:
print(f"ID: {row['id']}")
print(f"项目: {row['project_name']}")
print(f"日期: {row['date']}")
print(f"问题: {row['problem']}")
print(f"根因: {row['root_cause']}")
print(f"解决方案: {row['solution']}")
print(f"标签: {row['tags']}")
print(f"更新时间: {row['updated_at']}")
conn.close()
# ────────── 知识卡 (knowledge_cards) ──────────
def cmd_knowledge_add(args):
"""添加知识卡"""
conn = get_conn()
t = now()
conn.execute(
"""INSERT INTO knowledge_cards (topic, category, file_path, summary, version, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?)""",
(
args.topic,
args.category or "",
args.file or "",
args.summary or "",
args.version or "1.0",
t,
t,
),
)
conn.commit()
print(f"[OK] 知识卡 '{args.topic}' 已添加")
conn.close()
def cmd_knowledge_list(args):
"""列出知识卡,支持按分类过滤"""
conn = get_conn()
if args.category:
rows = conn.execute(
"""SELECT id, topic, category, version, updated_at
FROM knowledge_cards WHERE category = ? ORDER BY updated_at DESC""",
(args.category,),
).fetchall()
else:
rows = conn.execute(
"SELECT id, topic, category, version, updated_at FROM knowledge_cards ORDER BY updated_at DESC"
).fetchall()
print_rows(rows)
conn.close()
def cmd_knowledge_update(args):
"""更新知识卡"""
conn = get_conn()
row = conn.execute(
"SELECT id FROM knowledge_cards WHERE id = ?", (args.id,)
).fetchone()
if not row:
print(f"[ERR] 未找到知识卡 ID={args.id}")
conn.close()
return
updates = {}
if args.topic is not None:
updates["topic"] = args.topic
if args.category is not None:
updates["category"] = args.category
if args.file is not None:
updates["file_path"] = args.file
if args.summary is not None:
updates["summary"] = args.summary
if args.version is not None:
updates["version"] = args.version
if not updates:
print("[WARN] 未指定要更新的字段")
conn.close()
return
updates["updated_at"] = now()
set_clause = ", ".join(f"{k} = ?" for k in updates)
values = list(updates.values()) + [args.id]
conn.execute(f"UPDATE knowledge_cards SET {set_clause} WHERE id = ?", values)
conn.commit()
print(f"[OK] 知识卡 ID={args.id} 已更新")
conn.close()
def cmd_knowledge_delete(args):
"""删除知识卡"""
conn = get_conn()
conn.execute("DELETE FROM knowledge_cards WHERE id = ?", (args.id,))
conn.commit()
print(f"[OK] 知识卡 ID={args.id} 已删除(如存在)")
conn.close()
def cmd_knowledge_show(args):
"""查看单条知识卡完整内容"""
conn = get_conn()
row = conn.execute(
"SELECT * FROM knowledge_cards WHERE id = ?", (args.id,)
).fetchone()
if not row:
print(f"[ERR] 未找到知识卡 ID={args.id}")
else:
print(f"ID: {row['id']}")
print(f"主题: {row['topic']}")
print(f"分类: {row['category']}")
print(f"文件路径: {row['file_path']}")
print(f"摘要: {row['summary']}")
print(f"版本: {row['version']}")
print(f"更新时间: {row['updated_at']}")
conn.close()
# ────────── 训练语料 (training_corpus) ──────────
def cmd_corpus_add(args):
"""添加训练语料索引记录"""
if not validate_date(args.date):
print("[ERR] 日期格式错误,需要 YYYY-MM-DD")
return
conn = get_conn()
if args.type not in ("qa", "decision", "pitfall"):
print("[ERR] type 必须是 qa / decision / pitfall")
conn.close()
return
t = now()
conn.execute(
"""INSERT INTO training_corpus (date, type, tags, file_path, record_count, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?)""",
(args.date, args.type, args.tags or "", args.file or "", args.count or 0, t, t),
)
conn.commit()
print("[OK] 训练语料记录已添加")
conn.close()
def cmd_corpus_list(args):
"""列出训练语料"""
conn = get_conn()
query = """SELECT id, date, type, tags, file_path, record_count, updated_at
FROM training_corpus WHERE 1=1"""
params = []
if args.type:
query += " AND type = ?"
params.append(args.type)
query += " ORDER BY date DESC"
rows = conn.execute(query, params).fetchall()
print_rows(rows)
conn.close()
def cmd_corpus_update(args):
"""更新训练语料记录"""
conn = get_conn()
row = conn.execute(
"SELECT id FROM training_corpus WHERE id = ?", (args.id,)
).fetchone()
if not row:
print(f"[ERR] 未找到语料记录 ID={args.id}")
conn.close()
return
updates = {}
if args.type is not None:
if args.type not in ("qa", "decision", "pitfall"):
print("[ERR] type 必须是 qa / decision / pitfall")
conn.close()
return
updates["type"] = args.type
if args.tags is not None:
updates["tags"] = args.tags
if args.file is not None:
updates["file_path"] = args.file
if args.count is not None:
updates["record_count"] = args.count
if args.date is not None:
if not validate_date(args.date):
print("[ERR] 日期格式错误,需要 YYYY-MM-DD")
conn.close()
return
updates["date"] = args.date
if not updates:
print("[WARN] 未指定要更新的字段")
conn.close()
return
updates["updated_at"] = now()
set_clause = ", ".join(f"{k} = ?" for k in updates)
values = list(updates.values()) + [args.id]
conn.execute(f"UPDATE training_corpus SET {set_clause} WHERE id = ?", values)
conn.commit()
print(f"[OK] 语料记录 ID={args.id} 已更新")
conn.close()
def cmd_corpus_delete(args):
"""删除训练语料记录"""
conn = get_conn()
conn.execute("DELETE FROM training_corpus WHERE id = ?", (args.id,))
conn.commit()
print(f"[OK] 语料记录 ID={args.id} 已删除(如存在)")
conn.close()
def cmd_corpus_show(args):
"""查看单条训练语料索引完整内容"""
conn = get_conn()
row = conn.execute(
"SELECT * FROM training_corpus WHERE id = ?", (args.id,)
).fetchone()
if not row:
print(f"[ERR] 未找到语料记录 ID={args.id}")
else:
print(f"ID: {row['id']}")
print(f"日期: {row['date']}")
print(f"类型: {row['type']}")
print(f"标签: {row['tags']}")
print(f"文件路径: {row['file_path']}")
print(f"记录数: {row['record_count']}")
print(f"创建时间: {row['created_at']}")
print(f"更新时间: {row['updated_at']}")
conn.close()
def cmd_import(args):
"""批量导入文件夹,自动按子目录创建项目并分类文件。
核心流程:
1. 扫描目录(可选递归) → 子文件夹名→项目名
2. 文件类型判断: .jsonl(含Q&A)→语料库, 含日志关键词→工作日志, 其余→项目资产
3. 二进制文件只存元数据(路径+类型标签),文本文件读全文(超10000字截断)
4. 自动创建不存在的项目,状态设为 active
5. 写入 import_history 记录
Args:
args.dir: 要导入的目录路径
args.recursive: 是否递归子目录(每个子文件夹=一个项目)
args.project: 手动指定项目名(覆盖自动检测)
"""
import_dir = os.path.abspath(args.dir)
if not os.path.isdir(import_dir):
print(f"[ERR] 目录不存在: {import_dir}")
return
recursive = getattr(args, "recursive", False)
project_name = args.project if args.project else None
conn = get_conn()
cursor = conn.cursor()
# 收集目录(os.walk 真递归,单次遍历整棵树,按所属顶级目录分配项目名)
top_dirs = {}
if recursive:
for entry in os.listdir(import_dir):
epath = os.path.join(import_dir, entry)
if os.path.isdir(epath):
top_dirs[os.path.abspath(epath)] = project_name or entry
scan_groups = [("未归类", import_dir)]
else:
scan_groups = [(project_name or "未归类", import_dir)]
# 文件类型
text_ext = (
".md",
".txt",
".jsonl",
".py",
".c",
".h",
".cpp",
".hpp",
".json",
".csv",
".log",
".yaml",
".yml",
".ini",
".cfg",
".toml",
)
binary_ext = (
".pdf",
".docx",
".xlsx",
".pptx",
".png",
".jpg",
".jpeg",
".gif",
".bmp",
".svg",
".bin",
".hex",
".elf",
".uf2",
".sch",
".brd",
".kicad_pcb",
".kicad_sch",
".zip",
".rar",
".7z",
".gz",
)
supported_ext = text_ext + binary_ext
all_files = []
for pname, dpath in scan_groups:
if not os.path.isdir(dpath):
continue
for root, dirs, files in os.walk(dpath):
dirs.sort()
if not recursive:
dirs.clear()
abs_root = os.path.abspath(root)
owner = None
if recursive and top_dirs:
for td, td_name in top_dirs.items():
if abs_root == td or abs_root.startswith(td + os.sep):
owner = td_name
break
fn_pname = owner or pname
for fn in sorted(files):
fp = os.path.join(root, fn)
fn_low = fn.lower()
if fn_low.endswith(supported_ext) and os.path.isfile(fp):
is_bin = fn_low.endswith(binary_ext)
is_jsonl = fn_low.endswith(".jsonl")
all_files.append((fn_pname, root, fn, fp, is_bin, is_jsonl))
if not all_files:
print("[WARN] 目录下未找到可导入的文件")
conn.close()
return
# 确保项目存在
project_cache = {}
for pname, _, _, _, _, _ in all_files:
if pname in project_cache:
continue
cursor.execute("SELECT id FROM projects WHERE name=?", (pname,))
row = cursor.fetchone()
if row:
project_cache[pname] = row["id"]
else:
t = now()
cursor.execute(
"INSERT INTO projects (name,status,description,created_at,updated_at) VALUES(?,'active','封存导入',?,?)",
(pname, t, t),
)
conn.commit()
project_cache[pname] = cursor.lastrowid
print(f"[OK] 创建项目: {pname}")
# 逐文件导入
stats = {"imported": 0, "skipped": 0, "projects": 0, "logs": 0, "corpus": 0}
total = len(all_files)
processed = set()
for idx, (pname, dpath, filename, filepath, is_binary, is_jsonl) in enumerate(
all_files, 1
):
if dpath not in processed:
print(f"\n 扫描 [{pname}] {dpath}")
processed.add(dpath)
project_cache[pname]
title = os.path.splitext(filename)[0]
# JSONL -> 训练语料(一个文件一条记录,带去重)
if is_jsonl:
existing = conn.execute("SELECT id FROM training_corpus WHERE file_path=?", (filepath,)).fetchone()
if existing:
stats["skipped"] += 1
print(f" [{idx}/{total}] [SKIP] {filename}")
continue
with open(filepath, "r", encoding="utf-8", errors="replace") as fh:
raw = fh.read()
qa_n = dec_n = pf_n = other_n = 0
tags_set = set()
dates = []
for line in raw.strip().split("\n"):
line = line.strip()
if not line:
continue
try:
obj = json.loads(line)
except json.JSONDecodeError:
continue
if "question" in obj and "answer" in obj:
qa_n += 1
elif "decision" in obj or "conclusion" in obj:
dec_n += 1
elif "problem" in obj or "solution" in obj:
pf_n += 1
else:
other_n += 1
_tags = obj.get("tags")
if _tags:
if isinstance(_tags, list):
tags_set.update(str(t) for t in _tags)
elif isinstance(_tags, str):
tags_set.update(
t.strip() for t in _tags.split(",") if t.strip()
)
else:
tags_set.add(str(_tags))
if obj.get("date"):
dates.append(obj["date"])
total_valid = qa_n + dec_n + pf_n + other_n
if total_valid == 0:
stats["skipped"] += 1
print(f" [{idx}/{total}] [SKIP] {filename} (无有效行)")
continue
tags = ",".join(sorted(tags_set)[:5]) if tags_set else ""
date = dates[0] if dates else now()[:10]
# 按多数类型
if qa_n >= dec_n and qa_n >= pf_n and qa_n > 0:
best_type = "qa"
elif dec_n >= pf_n and dec_n > 0:
best_type = "decision"
elif pf_n > 0:
best_type = "pitfall"
else:
best_type = "qa"
t = now()
conn.execute(
"INSERT INTO training_corpus (date,type,tags,file_path,record_count,created_at,updated_at) VALUES(?,?,?,?,?,?,?)",
(date, best_type, tags, filepath, total_valid, t, t),
)
stats["imported"] += 1
stats["corpus"] += 1
parts = [f"QA {qa_n}"] if qa_n else []
if dec_n:
parts.append(f"决策 {dec_n}")
if pf_n:
parts.append(f"踩坑 {pf_n}")
if other_n:
parts.append(f"其他 {other_n}")
print(
f" [{idx}/{total}] [OK] {filename} -> 语料库 ({total_valid}条, {', '.join(parts)})"
)
continue
# 工作日志判定(按分词匹配,避免 logo/catalog 含 log 子串被误判)
stem = os.path.splitext(filename)[0].lower()
_toks = stem.replace("-", " ").replace("_", " ").replace(".", " ").split()
is_worklog = any(kw in stem for kw in ["日志", "工作记录", "周报", "日报"]) or any(
t in ("log", "diary", "journal") for t in _toks
)
category = "工作日志" if is_worklog else pname
# 二进制文件
if is_binary:
existing = conn.execute(
"SELECT id FROM knowledge_cards WHERE topic=? AND file_path=?",
(title, filepath),
).fetchone()
if existing:
stats["skipped"] += 1
print(f" [{idx}/{total}] [SKIP] {filename}")
continue
t = now()
ext = os.path.splitext(filename)[1].lower()
cat_map = {
".pdf": "PDF",
".docx": "Word",
".xlsx": "Excel",
".png": "图片",
".jpg": "图片",
".jpeg": "图片",
".gif": "图片",
".bmp": "图片",
".svg": "矢量图",
".bin": "固件",
".hex": "固件",
".elf": "固件",
".uf2": "固件",
".sch": "原理图",
".brd": "PCB",
".kicad_pcb": "KiCad",
".kicad_sch": "KiCad",
".zip": "压缩包",
".rar": "压缩包",
".7z": "压缩包",
".gz": "压缩包",
}
cn = cat_map.get(ext, "文档")
file_mtime = datetime.fromtimestamp(os.path.getmtime(filepath)).strftime("%Y-%m-%d")
conn.execute(
"INSERT INTO knowledge_cards (topic,category,file_path,summary,version,created_at,updated_at) VALUES(?,?,?,?,?,?,?)",
(title, category, filepath, f"[{cn} 文件] {filepath}", file_mtime, t, t),
)
stats["imported"] += 1
if is_worklog:
stats["logs"] += 1
else:
stats["projects"] += 1
print(
f" [{idx}/{total}] [OK] {filename} -> {'工作日志' if is_worklog else '项目资产'} ({cn})"
)
continue
# 文本文件
with open(filepath, "r", encoding="utf-8", errors="replace") as fh:
raw = fh.read()
existing = conn.execute(
"SELECT id FROM knowledge_cards WHERE topic=? AND file_path=?",
(title, filepath),
).fetchone()
if existing:
stats["skipped"] += 1
print(f" [{idx}/{total}] [SKIP] {filename}")
continue
t = now()
file_mtime = datetime.fromtimestamp(os.path.getmtime(filepath)).strftime(
"%Y-%m-%d"
)
conn.execute(
"INSERT INTO knowledge_cards (topic,category,file_path,summary,version,created_at,updated_at) VALUES(?,?,?,?,?,?,?)",
(title, category, filepath, raw[:10000], file_mtime, t, t),
)
conn.commit()
stats["imported"] += 1
if is_worklog:
stats["logs"] += 1
else:
stats["projects"] += 1
print(
f" [{idx}/{total}] [OK] {filename} -> {'工作日志' if is_worklog else '项目资产'}"
)
conn.commit()
conn.close()
print()
print("=" * 50)
print(f" 导入完成: 成功 {stats['imported']} 条, 跳过 {stats['skipped']} 条")
print(
f" 项目资产: {stats['projects']} | 工作日志: {stats['logs']} | 训练语料: {stats['corpus']}"
)
print(f" 涉及项目: {len(project_cache)} 个")
print("=" * 50)
# 写入导入历史(使 UI 导入历史面板可见)
try:
hconn = get_conn()
hconn.execute(
"INSERT INTO import_history (import_date, import_type, folder_path, project_name, file_count, summary, created_at) VALUES (?,?,?,?,?,?,?)",
(
now()[:10],
"CLI导入",
import_dir,
project_name or "",
stats["imported"],
f"成功{stats['imported']}条, 跳过{stats['skipped']}条",
now(),
),
)
hconn.commit()
hconn.close()
except Exception as e:
print(f"[WARN] 导入历史写入失败: {e}")
# ═══════════════════════════════════════════════════════════
# 搜索功能
# ═══════════════════════════════════════════════════════════
def highlight_keyword(text, keywords, context=30):
"""在文本中高亮关键词。
找到关键词在文本中最早出现的位置,截取前后各 context 个字符,
用【】包裹每个关键词。跨行不区分大小写。
Args:
text: 原始文本
keywords: 关键词列表(AND逻辑)
context: 前后截取字符数,默认30
Returns:
str: 高亮后的文本片段,前后加 ... 标记截断
"""
if not text or not keywords:
return text[: context * 2] if text else ""
import re
best_pos = len(text)
best_kw = keywords[0]
# 找到最早出现的关键词位置
for kw in keywords:
pos = text.lower().find(kw.lower())
if pos != -1 and pos < best_pos:
best_pos = pos
best_kw = kw
if best_pos >= len(text):
return text[: context * 2] if text else ""
start = max(0, best_pos - context)
end = min(len(text), best_pos + len(best_kw) + context)
snippet = text[start:end]
# 用【】包裹关键词(不区分大小写)
for kw in keywords:
snippet = re.sub(f"({re.escape(kw)})", r"【\1】", snippet, flags=re.IGNORECASE)
return ("..." if start > 0 else "") + snippet + ("..." if end < len(text) else "")
def cmd_search(args):
"""全局搜索,支持关键词AND逻辑和结果片段高亮。
搜索范围: decisions(title/content/basis/conclusion/tags) +
pitfalls(problem/root_cause/solution/tags) +
knowledge_cards(topic/summary/category)
关键词用空格分隔 → AND逻辑(每条记录必须同时包含所有关键词)
结果显示: 关键词前后30字上下文,用【】包裹关键词
支持叠加筛选: --project / --tag / --date-from / --date-to
Args:
args.keyword: 空格分隔的关键词(可选)
args.project: 项目名模糊匹配(可选)
args.tag: 标签模糊匹配(可选)
args.date_from/date_to: 日期范围(可选)
"""
conn = get_conn()
# 解析关键词(空格分隔,AND逻辑)
keywords = []
if hasattr(args, "keyword") and args.keyword:
keywords = [kw.strip() for kw in args.keyword.split() if kw.strip()]
# 构建过滤条件
project_clause = ""
project_params = []
if args.project:
project_clause = "AND p.name LIKE ?"
project_params = [f"%{args.project}%"]
tag_clause = ""
tag_params = []
if args.tag:
tag_clause = "AND d.tags LIKE ?"
tag_params = [f"%{args.tag}%"]
date_clause = ""
date_params = []
if args.date_from:
date_clause += " AND d.date >= ?"
date_params.append(args.date_from)
if args.date_to:
date_clause += " AND d.date <= ?"
date_params.append(args.date_to)
# ── 关键词搜索子句:在多个字段中模糊匹配,多个关键词AND逻辑 ──
def build_kw_clause(fields, table_alias=""):
"""为指定字段列表构建关键词LIKE子句,返回(clause_sql, params_list)"""
if not keywords:
return "", []
prefix = f"{table_alias}." if table_alias else ""
clauses = []
params = []
for kw in keywords:
or_parts = " OR ".join([f"{prefix}{f} LIKE ?" for f in fields])
clauses.append(f"({or_parts})")
params.extend([f"%{kw}%"] * len(fields))
return " AND " + " AND ".join(clauses), params
kw_d_clause, kw_d_params = build_kw_clause(
["title", "content", "basis", "conclusion", "tags"], "d"
)
kw_pf_clause, kw_pf_params = build_kw_clause(
["problem", "root_cause", "solution", "tags"], "pf"
)
kw_kc_clause, kw_kc_params = build_kw_clause(["topic", "summary", "category"])
results = []
# 搜索决策
d_query = f"""SELECT 'decision' AS source, d.id, p.name AS project, d.date,
d.title AS summary, d.tags,
d.title AS t_title, d.content AS t_content, d.basis AS t_basis, d.conclusion AS t_conclusion
FROM decisions d JOIN projects p ON d.project_id = p.id
WHERE 1=1 {project_clause} {tag_clause} {date_clause} {kw_d_clause}
ORDER BY d.date DESC LIMIT 100"""
d_params = project_params + tag_params + date_params + kw_d_params
results.extend([dict(r) for r in conn.execute(d_query, d_params).fetchall()])
# 搜索踩坑记录
d_tag_clause = tag_clause.replace("d.", "pf.")
pf_query = f"""SELECT 'pitfall' AS source, pf.id, p.name AS project, pf.date,
pf.problem AS summary, pf.tags,
pf.problem AS t_problem, pf.root_cause AS t_root_cause, pf.solution AS t_solution
FROM pitfalls pf JOIN projects p ON pf.project_id = p.id
WHERE 1=1 {project_clause} {d_tag_clause} {date_clause} {kw_pf_clause}
ORDER BY pf.date DESC LIMIT 100"""
pf_params = project_params + tag_params + date_params + kw_pf_params
results.extend([dict(r) for r in conn.execute(pf_query, pf_params).fetchall()])
# 搜索知识卡
kc_tag_clause = ""
kc_tag_params = []
if args.tag:
kc_tag_clause = "AND (category LIKE ? OR topic LIKE ?)"
kc_tag_params = [f"%{args.tag}%", f"%{args.tag}%"]
kc_query = f"""SELECT 'knowledge' AS source, id, topic AS label, updated_at AS date,
summary, category,
topic AS t_topic, summary AS t_summary
FROM knowledge_cards
WHERE 1=1 {kc_tag_clause} {kw_kc_clause}
ORDER BY updated_at DESC LIMIT 100"""
kc_params = kc_tag_params + kw_kc_params
results.extend([dict(r) for r in conn.execute(kc_query, kc_params).fetchall()])
if results:
print(f"\n[Search] 搜索结果(共 {len(results)} 条):\n")
for r in results:
source = r["source"]
if source == "knowledge":
line1 = f"[{source}] ID={r['id']} | {r['label']} | {r['date']}"
line2 = f" 摘要: {r['summary'][:80]}"
print(line1)
print(line2)
else:
line1 = f"[{source}] ID={r['id']} | {r['project']} | {r['date']} | {r['summary'][:60]}"
print(line1)
# 关键词高亮:在内容字段中查找并显示片段
if keywords:
text_fields = {
"decision": ["t_title", "t_content", "t_basis", "t_conclusion"],
"pitfall": ["t_problem", "t_root_cause", "t_solution"],
"knowledge": ["t_topic", "t_summary"],
}.get(source, [])
for tf in text_fields:
val = r.get(tf, "")
if val:
snippet = highlight_keyword(val, keywords)
print(f" [{tf.replace('t_', '')}]: {snippet}")
print()
else:
print("[Search] 无匹配结果")
conn.close()
# ═══════════════════════════════════════════════════════════
# 导出功能
# ═══════════════════════════════════════════════════════════
def cmd_export(args):
"""将指定项目的全部数据导出为 Markdown 文件。
导出内容: 项目信息 + 技术决策 + 踩坑记录 + 知识卡 + 训练语料索引
输出路径: 脚本同目录下 {项目名}_export.md
Args:
args.project: 项目名称或ID
"""
conn = get_conn()
pid = resolve_project_id(conn, args.project)
if pid is None:
print(f"[ERR] 未找到项目: {args.project}")
conn.close()
return
# 获取项目基本信息
proj = conn.execute("SELECT * FROM projects WHERE id = ?", (pid,)).fetchone()
# 获取该项目的决策
decisions = conn.execute(
"SELECT * FROM decisions WHERE project_id = ? ORDER BY date DESC", (pid,)
).fetchall()
# 获取该项目的踩坑记录
pitfalls = conn.execute(
"SELECT * FROM pitfalls WHERE project_id = ? ORDER BY date DESC", (pid,)
).fetchall()
# 组装Markdown内容
md = []
md.append(f"# 项目: {proj['name']}\n")
md.append(f"- **状态**: {proj['status']}")
md.append(f"- **描述**: {proj['description']}")
md.append(f"- **关键里程碑**: {proj['key_milestone']}")
md.append(f"- **关联文件**: {proj['related_files']}")
md.append(f"- **创建时间**: {proj['created_at']}")
md.append(f"- **最后更新**: {proj['updated_at']}")
md.append("")
md.append("---\n")
md.append(f"## 技术决策({len(decisions)} 条)\n")
for i, d in enumerate(decisions, 1):
md.append(f"### 决策 {i}: {d['title']}")
md.append(f"- **日期**: {d['date']}")
md.append(f"- **标签**: {d['tags']}")
md.append(f"\n**内容**:\n\n{d['content']}\n")
md.append(f"**依据**:\n\n{d['basis']}\n")
md.append(f"**结论**:\n\n{d['conclusion']}\n")
md.append("")
md.append("---\n")
md.append(f"## 踩坑记录({len(pitfalls)} 条)\n")
for i, pf in enumerate(pitfalls, 1):
md.append(f"### 踩坑 {i}: {pf['problem'][:50]}")
md.append(f"- **日期**: {pf['date']}")
md.append(f"- **标签**: {pf['tags']}")
md.append(f"\n**问题**:\n\n{pf['problem']}\n")
md.append(f"**根因分析**:\n\n{pf['root_cause']}\n")
md.append(f"**解决方案**:\n\n{pf['solution']}\n")
md.append("")
# ── 关联知识卡:按项目名模糊匹配 category,或者遍历全部知识卡 ──
# 获取全部知识卡,显示与项目名可能相关的(category包含项目关键词,或输出全部由用户自行筛选)
all_knowledge = conn.execute(
"SELECT * FROM knowledge_cards ORDER BY updated_at DESC"
).fetchall()
md.append("---\n")
md.append(f"## 知识卡({len(all_knowledge)} 条)\n")
if all_knowledge:
for i, kc in enumerate(all_knowledge, 1):
md.append(f"### 知识卡 {i}: {kc['topic']}")
md.append(f"- **分类**: {kc['category']}")
md.append(f"- **版本**: {kc['version']}")
md.append(f"- **关联文件**: {kc['file_path']}")
md.append(f"- **更新时间**: {kc['updated_at']}")
md.append(f"\n**摘要**:\n\n{kc['summary']}\n")
md.append("")
else:
md.append("(无)\n")
# ── 训练语料索引:按项目名模糊匹配 tags ──
training_corpus_rows = conn.execute(
"SELECT * FROM training_corpus ORDER BY date DESC"
).fetchall()
md.append("---\n")
md.append(f"## 训练语料索引({len(training_corpus_rows)} 条)\n")
if training_corpus_rows:
for i, tc in enumerate(training_corpus_rows, 1):
md.append(f"### 语料 {i}")
md.append(f"- **日期**: {tc['date']}")
md.append(f"- **类型**: {tc['type']}")
md.append(f"- **标签**: {tc['tags']}")
md.append(f"- **文件路径**: {tc['file_path']}")
md.append(f"- **记录数**: {tc['record_count']}")
md.append(f"- **更新时间**: {tc['updated_at']}")
md.append("")
else:
md.append("(无)\n")
md.append(f"> 导出时间: {now()}")
# 写入文件
filename = f"{proj['name'].replace(' ', '_')}_export.md"
output_path = os.path.join(os.path.dirname(DB_PATH), filename)
with open(output_path, "w", encoding="utf-8") as f:
f.write("\n".join(md))
print(f"[OK] 已导出到: {output_path}")
conn.close()
# ═══════════════════════════════════════════════════════════
# 统计功能
# ═══════════════════════════════════════════════════════════
def cmd_stats(args):
"""显示数据库统计信息概览。
统计内容: 项目数量+状态分布, 各项目决策/踩坑数,
全局总决策/总踩坑/总知识卡/总语料,
最近更新的10条记录。
"""
conn = get_conn()
print("=" * 60)
print(" [STATS] 数据库统计")
print("=" * 60)
# 项目概览
projects = conn.execute(
"SELECT name, status, updated_at FROM projects ORDER BY status, updated_at DESC"
).fetchall()
print(f"\n[Projects] 项目概览(共 {len(projects)} 个):\n")
for p in projects:
# 统计该项目下的决策和踩坑数
d_count = conn.execute(
"SELECT COUNT(*) AS cnt FROM decisions WHERE project_id = (SELECT id FROM projects WHERE name = ?)",
(p["name"],),
).fetchone()["cnt"]
pf_count = conn.execute(
"SELECT COUNT(*) AS cnt FROM pitfalls WHERE project_id = (SELECT id FROM projects WHERE name = ?)",
(p["name"],),
).fetchone()["cnt"]
status_icon = {"active": "[A]", "paused": "[P]", "archived": "[X]"}.get(
p["status"], "[?]"
)
print(f" {status_icon} {p['name']}")
print(
f" 状态: {p['status']} | 决策: {d_count} | 踩坑: {pf_count} | 更新: {p['updated_at']}"
)
# 全局统计
total_decisions = conn.execute("SELECT COUNT(*) AS cnt FROM decisions").fetchone()[
"cnt"
]
total_pitfalls = conn.execute("SELECT COUNT(*) AS cnt FROM pitfalls").fetchone()[
"cnt"
]
total_knowledge = conn.execute(
"SELECT COUNT(*) AS cnt FROM knowledge_cards"
).fetchone()["cnt"]
total_corpus = conn.execute(
"SELECT COUNT(*) AS cnt FROM training_corpus"
).fetchone()["cnt"]
print("\n[Summary] 全局汇总:")
print(f" 总决策数: {total_decisions}")
print(f" 总踩坑记录: {total_pitfalls}")
print(f" 总知识卡: {total_knowledge}")
print(f" 总语料索引: {total_corpus}")
# 最近更新
print("\n[Recent] 最近更新的10条记录:")
recent = conn.execute("""
SELECT 'decision' AS kind, title AS label, updated_at FROM decisions
UNION ALL
SELECT 'pitfall', problem, updated_at FROM pitfalls
UNION ALL
SELECT 'knowledge', topic, updated_at FROM knowledge_cards
ORDER BY updated_at DESC LIMIT 10
""").fetchall()
for r in recent:
print(f" [{r['kind']}] {r['label'][:50]}{r['updated_at']}")
conn.close()
# ═══════════════════════════════════════════════════════════
# 命令行界面
# ═══════════════════════════════════════════════════════════
def build_parser():
"""构建 argparse 命令行解析器,注册所有子命令和参数。
命令树:
init | project {add,list,update,delete} |
decision {add,list,update,delete,show} |
pitfall {add,list,update,delete,show} |
knowledge {add,list,update,delete,show} |
corpus {add,list,update,delete,show} |
search [--keyword] [--project] [--tag] [--date-*] |
import --dir [--recursive] [--project] |
export --project | stats
Returns:
argparse.ArgumentParser: 配置好的解析器
"""
parser = argparse.ArgumentParser(
prog="python digital_assets_manager.py",
description="个人项目与数字资产管理器 — SQLite本地数据库 + CLI",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=textwrap.dedent("""\
使用示例:
# 初始化数据库
python digital_assets_manager.py init
# 项目管理
python digital_assets_manager.py project add --name "电子宠物V1.0" --desc "ESP32驱动项目"
python digital_assets_manager.py project list
python digital_assets_manager.py project list --status active
python digital_assets_manager.py project update "电子宠物V1.0" --status paused
python digital_assets_manager.py project delete "电子宠物V1.0"
# 决策管理
python digital_assets_manager.py decision add --project "电子宠物V1.0" --date 2026-07-01 --title "选择主控芯片" --content "对比了ESP32-S3和ESP32-C3" --basis "需要USB OTG和WiFi+BLE共存" --conclusion "选用ESP32-S3-PICO-1" --tags "芯片选型,ESP32"
python digital_assets_manager.py decision list
python digital_assets_manager.py decision list --project "电子宠物V1.0"
python digital_assets_manager.py decision show 1
python digital_assets_manager.py decision update 1 --conclusion "改用ESP32-S3-WROOM-1"
python digital_assets_manager.py decision delete 1
# 踩坑记录
python digital_assets_manager.py pitfall add --project "电子宠物V1.0" --date 2026-07-15 --problem "TFT屏幕花屏" --root_cause "SPI时钟过高且杜邦线过长" --solution "降频到40MHz,缩短接线并加磁环" --tags "硬件,SPI,TFT"
python digital_assets_manager.py pitfall list
python digital_assets_manager.py pitfall show 1
# 知识卡
python digital_assets_manager.py knowledge add --topic "ESP32-S3 GPIO12上拉问题" --category "ESP32" --summary "GPIO12为内部flash电压选择引脚,启动时必须为高电平" --file "docs/esp32_gpio12.md"
python digital_assets_manager.py knowledge list --category ESP32
# 训练语料
python digital_assets_manager.py corpus add --date 2026-07-20 --type qa --tags "硬件调试" --file "corpus/qa_20260720.jsonl" --count 150
# 搜索
python digital_assets_manager.py search --tag "ESP32"
python digital_assets_manager.py search --project "电子宠物" --date-from 2026-06-01
# 导出
python digital_assets_manager.py export --project "电子宠物V1.0"
# 统计
python digital_assets_manager.py stats
"""),
)
subparsers = parser.add_subparsers(dest="command", help="可用命令")
# ── init ──
subparsers.add_parser("init", help="初始化数据库(建表+索引)")
# ── project ──
p_project = subparsers.add_parser("project", help="项目管理")
p_project_subs = p_project.add_subparsers(dest="action")
p_add = p_project_subs.add_parser("add", help="添加项目")
p_add.add_argument("--name", required=True, help="项目名称")
p_add.add_argument(
"--status",
default="active",
choices=["active", "paused", "archived"],
help="状态",
)
p_add.add_argument("--desc", default="", help="描述")
p_add.add_argument("--milestone", default="", help="关键里程碑")
p_add.add_argument("--files", nargs="*", default=[], help="关联文件路径列表")
p_list = p_project_subs.add_parser("list", help="列出项目")
p_list.add_argument(
"--status", choices=["active", "paused", "archived"], help="按状态过滤"
)
p_update = p_project_subs.add_parser("update", help="更新项目")
p_update.add_argument("project", help="项目名称或ID")
p_update.add_argument(
"--status", choices=["active", "paused", "archived"], help="新状态"
)
p_update.add_argument("--desc", help="新描述")
p_update.add_argument("--milestone", help="新里程碑")
p_update.add_argument("--files", nargs="*", help="新关联文件列表")
p_delete = p_project_subs.add_parser("delete", help="删除项目")
p_delete.add_argument("project", help="项目名称或ID")
p_delete.add_argument("--force", action="store_true", help="跳过确认直接删除")
# ── decision ──
p_dec = subparsers.add_parser("decision", help="决策管理")
p_dec_subs = p_dec.add_subparsers(dest="action")
p_da = p_dec_subs.add_parser("add", help="添加决策")
p_da.add_argument("--project", required=True, help="项目名称或ID")
p_da.add_argument("--date", required=True, help="决策日期 (YYYY-MM-DD)")
p_da.add_argument("--title", required=True, help="决策标题")
p_da.add_argument("--content", default="", help="决策内容")
p_da.add_argument("--basis", default="", help="决策依据")
p_da.add_argument("--conclusion", default="", help="结论")
p_da.add_argument("--tags", default="", help="标签(逗号分隔)")
p_dl = p_dec_subs.add_parser("list", help="列出决策")
p_dl.add_argument("--project", help="按项目过滤")
p_du = p_dec_subs.add_parser("update", help="更新决策")
p_du.add_argument("id", type=int, help="决策ID")
p_du.add_argument("--date", help="新日期")
p_du.add_argument("--title", help="新标题")
p_du.add_argument("--content", help="新内容")
p_du.add_argument("--basis", help="新依据")
p_du.add_argument("--conclusion", help="新结论")
p_du.add_argument("--tags", help="新标签")
p_dd = p_dec_subs.add_parser("delete", help="删除决策")
p_dd.add_argument("id", type=int, help="决策ID")
p_ds = p_dec_subs.add_parser("show", help="查看决策详情")
p_ds.add_argument("id", type=int, help="决策ID")
# ── pitfall ──
p_pf = subparsers.add_parser("pitfall", help="踩坑记录管理")
p_pf_subs = p_pf.add_subparsers(dest="action")
p_pa = p_pf_subs.add_parser("add", help="添加踩坑记录")
p_pa.add_argument("--project", required=True, help="项目名称或ID")
p_pa.add_argument("--date", required=True, help="日期 (YYYY-MM-DD)")
p_pa.add_argument("--problem", required=True, help="问题描述")
p_pa.add_argument("--root_cause", default="", help="根因分析")
p_pa.add_argument("--solution", default="", help="解决方案")
p_pa.add_argument("--tags", default="", help="标签")
p_pl = p_pf_subs.add_parser("list", help="列出踩坑记录")
p_pl.add_argument("--project", help="按项目过滤")
p_pu = p_pf_subs.add_parser("update", help="更新踩坑记录")
p_pu.add_argument("id", type=int, help="记录ID")
p_pu.add_argument("--date", help="新日期")
p_pu.add_argument("--problem", help="新问题描述")
p_pu.add_argument("--root_cause", help="新根因分析")
p_pu.add_argument("--solution", help="新解决方案")
p_pu.add_argument("--tags", help="新标签")
p_pd = p_pf_subs.add_parser("delete", help="删除踩坑记录")
p_pd.add_argument("id", type=int, help="记录ID")
p_ps = p_pf_subs.add_parser("show", help="查看踩坑记录详情")
p_ps.add_argument("id", type=int, help="记录ID")
# ── knowledge ──
p_kc = subparsers.add_parser("knowledge", help="知识卡管理")
p_kc_subs = p_kc.add_subparsers(dest="action")
p_ka = p_kc_subs.add_parser("add", help="添加知识卡")
p_ka.add_argument("--topic", required=True, help="主题")
p_ka.add_argument("--category", default="", help="分类")
p_ka.add_argument("--file", default="", help="关联文件路径")
p_ka.add_argument("--summary", default="", help="摘要")
p_ka.add_argument("--version", default="1.0", help="版本号")
p_kl = p_kc_subs.add_parser("list", help="列出知识卡")
p_kl.add_argument("--category", help="按分类过滤")
p_ku = p_kc_subs.add_parser("update", help="更新知识卡")
p_ku.add_argument("id", type=int, help="知识卡ID")
p_ku.add_argument("--topic", help="新主题")
p_ku.add_argument("--category", help="新分类")
p_ku.add_argument("--file", help="新文件路径")
p_ku.add_argument("--summary", help="新摘要")
p_ku.add_argument("--version", help="新版本号")
p_kd = p_kc_subs.add_parser("delete", help="删除知识卡")
p_kd.add_argument("id", type=int, help="知识卡ID")
p_ks = p_kc_subs.add_parser("show", help="查看知识卡详情")
p_ks.add_argument("id", type=int, help="知识卡ID")
# ── corpus ──
p_cp = subparsers.add_parser("corpus", help="训练语料管理")
p_cp_subs = p_cp.add_subparsers(dest="action")
p_ca = p_cp_subs.add_parser("add", help="添加语料索引")
p_ca.add_argument("--date", required=True, help="日期 (YYYY-MM-DD)")
p_ca.add_argument(
"--type", required=True, choices=["qa", "decision", "pitfall"], help="语料类型"
)
p_ca.add_argument("--tags", default="", help="标签")
p_ca.add_argument("--file", default="", help="JSONL文件路径")
p_ca.add_argument("--count", type=int, default=0, help="记录数")
p_cl = p_cp_subs.add_parser("list", help="列出语料索引")
p_cl.add_argument(
"--type", choices=["qa", "decision", "pitfall"], help="按类型过滤"
)
p_cu = p_cp_subs.add_parser("update", help="更新语料索引")
p_cu.add_argument("id", type=int, help="记录ID")
p_cu.add_argument("--date", help="新日期")
p_cu.add_argument("--type", choices=["qa", "decision", "pitfall"], help="新类型")
p_cu.add_argument("--tags", help="新标签")
p_cu.add_argument("--file", help="新文件路径")
p_cu.add_argument("--count", type=int, help="新记录数")
p_cd = p_cp_subs.add_parser("delete", help="删除语料索引")
p_cd.add_argument("id", type=int, help="记录ID")
p_cs = p_cp_subs.add_parser("show", help="查看语料索引详情")
p_cs.add_argument("id", type=int, help="记录ID")
# ── search ──
p_search = subparsers.add_parser("search", help="全局搜索")
p_search.add_argument("--keyword", help="全文搜索关键词(空格分隔,AND逻辑)")
p_search.add_argument("--project", help="项目名(模糊匹配)")
p_search.add_argument("--tag", help="标签(模糊匹配)")
p_search.add_argument("--date-from", help="起始日期 (YYYY-MM-DD)")
p_search.add_argument("--date-to", help="截止日期 (YYYY-MM-DD)")
# ── import ──
p_import = subparsers.add_parser("import", help="批量导入文件")
p_import.add_argument("--dir", required=True, help="扫描目录路径")
p_import.add_argument(
"--project", help="指定项目名(覆盖自动检测,全部归入该项目)"
)
p_import.add_argument(
"--recursive",
action="store_true",
help="递归扫描子文件夹,每个子文件夹作为一个项目",
)
# ── export ──
p_export = subparsers.add_parser("export", help="导出项目为Markdown")
p_export.add_argument("--project", required=True, help="项目名称或ID")
# ── stats ──
subparsers.add_parser("stats", help="显示统计信息")
return parser
def main():
parser = build_parser()
args = parser.parse_args()
if args.command is None:
parser.print_help()
return
# 除了 init 之外,所有数据库操作前检查是否已初始化
if args.command != "init" and not check_db_initialized():
return
# 路由到对应处理函数
if args.command == "init":
init_db()
elif args.command == "project":
if args.action == "add":
cmd_project_add(args)
elif args.action == "list":
cmd_project_list(args)
elif args.action == "update":
cmd_project_update(args)
elif args.action == "delete":
cmd_project_delete(args)
else:
print("[ERR] 未知 project 子命令,可用: add / list / update / delete")
elif args.command == "decision":
if args.action == "add":
cmd_decision_add(args)
elif args.action == "list":
cmd_decision_list(args)
elif args.action == "update":
cmd_decision_update(args)
elif args.action == "delete":
cmd_decision_delete(args)
elif args.action == "show":
cmd_decision_show(args)
else:
print(
"[ERR] 未知 decision 子命令,可用: add / list / update / delete / show"
)
elif args.command == "pitfall":
if args.action == "add":
cmd_pitfall_add(args)
elif args.action == "list":
cmd_pitfall_list(args)
elif args.action == "update":
cmd_pitfall_update(args)
elif args.action == "delete":
cmd_pitfall_delete(args)
elif args.action == "show":
cmd_pitfall_show(args)
else:
print(
"[ERR] 未知 pitfall 子命令,可用: add / list / update / delete / show"
)
elif args.command == "knowledge":
if args.action == "add":
cmd_knowledge_add(args)
elif args.action == "list":
cmd_knowledge_list(args)
elif args.action == "update":
cmd_knowledge_update(args)
elif args.action == "delete":
cmd_knowledge_delete(args)
elif args.action == "show":
cmd_knowledge_show(args)
else:
print(
"[ERR] 未知 knowledge 子命令,可用: add / list / update / delete / show"
)
elif args.command == "corpus":
if args.action == "add":
cmd_corpus_add(args)
elif args.action == "list":
cmd_corpus_list(args)
elif args.action == "update":
cmd_corpus_update(args)
elif args.action == "delete":
cmd_corpus_delete(args)
elif args.action == "show":
cmd_corpus_show(args)
else:
print("[ERR] 未知 corpus 子命令,可用: add / list / update / delete / show")
elif args.command == "search":
cmd_search(args)
elif args.command == "import":
cmd_import(args)
elif args.command == "export":
cmd_export(args)
elif args.command == "stats":
cmd_stats(args)
else:
print(f"[ERR] 未知命令: {args.command}")
parser.print_help()
if __name__ == "__main__":
main()