#!/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()