content_db.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507
  1. """content_db.py — SQLite 内容数据库操作类"""
  2. import json
  3. import sqlite3
  4. from pathlib import Path
  5. from typing import Optional
  6. class ContentDB:
  7. """SQLite 内容数据库操作类
  8. 每个文档对应一个独立的 SQLite 数据库文件,存储文档的所有 Blocks。
  9. """
  10. def __init__(self, db_path: str):
  11. self.db_path = Path(db_path)
  12. self.conn: Optional[sqlite3.Connection] = None
  13. def connect(self):
  14. """连接数据库"""
  15. self.conn = sqlite3.connect(str(self.db_path))
  16. self.conn.row_factory = sqlite3.Row
  17. return self
  18. def close(self):
  19. """关闭连接"""
  20. if self.conn:
  21. self.conn.close()
  22. self.conn = None
  23. def __enter__(self):
  24. """上下文管理器入口"""
  25. return self.connect()
  26. def __exit__(self, exc_type, exc_val, exc_tb):
  27. """上下文管理器退出"""
  28. self.close()
  29. def create_tables(self):
  30. """创建 document_blocks 表"""
  31. self.conn.execute("""
  32. CREATE TABLE IF NOT EXISTS document_blocks (
  33. id TEXT PRIMARY KEY,
  34. block_order INTEGER NOT NULL,
  35. type TEXT NOT NULL,
  36. level INTEGER DEFAULT 0,
  37. "index" INTEGER DEFAULT 0,
  38. content TEXT NOT NULL,
  39. word_style TEXT DEFAULT '',
  40. style TEXT DEFAULT '{}',
  41. metadata TEXT DEFAULT '{}'
  42. )
  43. """)
  44. # 创建索引
  45. self.conn.execute(
  46. 'CREATE INDEX IF NOT EXISTS idx_block_order ON document_blocks(block_order)'
  47. )
  48. self.conn.execute(
  49. 'CREATE INDEX IF NOT EXISTS idx_type ON document_blocks(type)'
  50. )
  51. self.conn.execute(
  52. 'CREATE INDEX IF NOT EXISTS idx_level ON document_blocks(level)'
  53. )
  54. self.conn.commit()
  55. def insert_blocks(self, blocks: list[dict]):
  56. """批量插入 blocks"""
  57. for block in blocks:
  58. content = block['content']
  59. if isinstance(content, (dict, list)):
  60. content = json.dumps(content, ensure_ascii=False)
  61. self.conn.execute("""
  62. INSERT INTO document_blocks
  63. (id, block_order, type, level, "index", content, word_style, style, metadata)
  64. VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
  65. """, (
  66. block['id'],
  67. block['block_order'],
  68. block['type'],
  69. block.get('level', 0),
  70. block.get('index', 0),
  71. content,
  72. block.get('word_style', ''),
  73. json.dumps(block.get('style', {}), ensure_ascii=False),
  74. json.dumps(block.get('metadata', {}), ensure_ascii=False)
  75. ))
  76. self.conn.commit()
  77. def get_blocks(self, order_by: str = 'block_order') -> list[dict]:
  78. """查询所有 blocks"""
  79. cursor = self.conn.execute(f"""
  80. SELECT * FROM document_blocks
  81. ORDER BY {order_by}
  82. """)
  83. rows = cursor.fetchall()
  84. return [self._row_to_dict(row) for row in rows]
  85. def get_block_by_id(self, block_id: str) -> Optional[dict]:
  86. """按 ID 查询单个 block"""
  87. cursor = self.conn.execute("""
  88. SELECT * FROM document_blocks WHERE id = ?
  89. """, (block_id,))
  90. row = cursor.fetchone()
  91. return self._row_to_dict(row) if row else None
  92. def update_block(self, block_id: str, updates: dict):
  93. """更新单个 block"""
  94. set_clauses = []
  95. params = []
  96. if 'content' in updates:
  97. content = updates['content']
  98. if isinstance(content, (dict, list)):
  99. content = json.dumps(content, ensure_ascii=False)
  100. set_clauses.append('content = ?')
  101. params.append(content)
  102. if 'style' in updates:
  103. set_clauses.append('style = ?')
  104. params.append(json.dumps(updates['style'], ensure_ascii=False))
  105. if 'word_style' in updates:
  106. set_clauses.append('word_style = ?')
  107. params.append(updates['word_style'])
  108. if 'metadata' in updates:
  109. set_clauses.append('metadata = ?')
  110. params.append(json.dumps(updates['metadata'], ensure_ascii=False))
  111. if not set_clauses:
  112. return
  113. params.append(block_id)
  114. sql = f"UPDATE document_blocks SET {', '.join(set_clauses)} WHERE id = ?"
  115. self.conn.execute(sql, params)
  116. self.conn.commit()
  117. def delete_block(self, block_id: str):
  118. """删除单个 block"""
  119. self.conn.execute('DELETE FROM document_blocks WHERE id = ?', (block_id,))
  120. self.conn.commit()
  121. def search_blocks(self, query: str, block_type: Optional[str] = None) -> list[dict]:
  122. """搜索 blocks"""
  123. sql = "SELECT * FROM document_blocks WHERE content LIKE ?"
  124. params = [f'%{query}%']
  125. if block_type:
  126. sql += " AND type = ?"
  127. params.append(block_type)
  128. sql += " ORDER BY block_order"
  129. cursor = self.conn.execute(sql, params)
  130. rows = cursor.fetchall()
  131. return [self._row_to_dict(row) for row in rows]
  132. def get_headings(self) -> list[dict]:
  133. """获取所有标题块"""
  134. cursor = self.conn.execute("""
  135. SELECT * FROM document_blocks
  136. WHERE type = 'heading'
  137. ORDER BY block_order
  138. """)
  139. rows = cursor.fetchall()
  140. return [self._row_to_dict(row) for row in rows]
  141. def get_stats(self) -> dict:
  142. """获取统计信息"""
  143. cursor = self.conn.execute("""
  144. SELECT
  145. type,
  146. COUNT(*) as count
  147. FROM document_blocks
  148. GROUP BY type
  149. """)
  150. stats = {row['type']: row['count'] for row in cursor.fetchall()}
  151. cursor = self.conn.execute("SELECT COUNT(*) as total FROM document_blocks")
  152. total = cursor.fetchone()['total']
  153. return {
  154. 'total': total,
  155. 'by_type': stats
  156. }
  157. def calculate_index_for_insert(
  158. self,
  159. after_block_id: str,
  160. new_type: str,
  161. new_level: int = 0
  162. ) -> int:
  163. """计算插入 block 的 index(稀疏排序)
  164. Args:
  165. after_block_id: 在哪个 block 后插入(如果为 None 则插入到末尾)
  166. new_type: 新 block 的类型
  167. new_level: 新 block 的级别(标题有效)
  168. Returns:
  169. 计算得到的 index
  170. """
  171. if after_block_id:
  172. # 1. 获取 after_block 的 block_order
  173. after_block = self.get_block_by_id(after_block_id)
  174. if not after_block:
  175. raise ValueError(f"Block not found: {after_block_id}")
  176. after_order = after_block['block_order']
  177. # 2. 查找下一个同类型同级别的 block
  178. if new_type == 'heading':
  179. # 标题:按 level 查询
  180. cursor = self.conn.execute("""
  181. SELECT "index" FROM document_blocks
  182. WHERE type = ? AND level = ? AND block_order > ?
  183. ORDER BY block_order LIMIT 1
  184. """, (new_type, new_level, after_order))
  185. else:
  186. # 其他类型:只按 type 查询
  187. cursor = self.conn.execute("""
  188. SELECT "index" FROM document_blocks
  189. WHERE type = ? AND block_order > ?
  190. ORDER BY block_order LIMIT 1
  191. """, (new_type, after_order))
  192. next_row = cursor.fetchone()
  193. if next_row:
  194. next_index = next_row['index']
  195. # 3. 查找前一个同类型同级别的 block
  196. if new_type == 'heading':
  197. cursor = self.conn.execute("""
  198. SELECT "index" FROM document_blocks
  199. WHERE type = ? AND level = ? AND block_order <= ?
  200. ORDER BY block_order DESC LIMIT 1
  201. """, (new_type, new_level, after_order))
  202. else:
  203. cursor = self.conn.execute("""
  204. SELECT "index" FROM document_blocks
  205. WHERE type = ? AND block_order <= ?
  206. ORDER BY block_order DESC LIMIT 1
  207. """, (new_type, after_order))
  208. prev_row = cursor.fetchone()
  209. prev_index = prev_row['index'] if prev_row else -100
  210. # 4. 计算中间值
  211. gap = next_index - prev_index
  212. if gap <= 1:
  213. # 间隙不足,触发局部重排
  214. self._rebalance_indexes_between(
  215. new_type, new_level, prev_index, next_index
  216. )
  217. # 重新查询
  218. if new_type == 'heading':
  219. cursor = self.conn.execute("""
  220. SELECT "index" FROM document_blocks
  221. WHERE type = ? AND level = ? AND block_order > ?
  222. ORDER BY block_order LIMIT 1
  223. """, (new_type, new_level, after_order))
  224. else:
  225. cursor = self.conn.execute("""
  226. SELECT "index" FROM document_blocks
  227. WHERE type = ? AND block_order > ?
  228. ORDER BY block_order LIMIT 1
  229. """, (new_type, after_order))
  230. next_row = cursor.fetchone()
  231. next_index = next_row['index'] if next_row else prev_index + 200
  232. new_index = (prev_index + next_index) // 2
  233. else:
  234. # 没有下一个同类 block,追加到最后
  235. if new_type == 'heading':
  236. cursor = self.conn.execute("""
  237. SELECT MAX("index") as max_index FROM document_blocks
  238. WHERE type = ? AND level = ?
  239. """, (new_type, new_level))
  240. else:
  241. cursor = self.conn.execute("""
  242. SELECT MAX("index") as max_index FROM document_blocks
  243. WHERE type = ?
  244. """, (new_type,))
  245. row = cursor.fetchone()
  246. max_index = row['max_index'] if row['max_index'] is not None else -100
  247. new_index = max_index + 100
  248. else:
  249. # 插入到文档末尾
  250. if new_type == 'heading':
  251. cursor = self.conn.execute("""
  252. SELECT MAX("index") as max_index FROM document_blocks
  253. WHERE type = ? AND level = ?
  254. """, (new_type, new_level))
  255. else:
  256. cursor = self.conn.execute("""
  257. SELECT MAX("index") as max_index FROM document_blocks
  258. WHERE type = ?
  259. """, (new_type,))
  260. row = cursor.fetchone()
  261. max_index = row['max_index'] if row['max_index'] is not None else -100
  262. new_index = max_index + 100
  263. return new_index
  264. def calculate_block_order_for_insert(self, after_block_id: str = None) -> int:
  265. """计算插入 block 的 block_order(稀疏排序)
  266. Args:
  267. after_block_id: 在哪个 block 后插入(如果为 None 则插入到末尾)
  268. Returns:
  269. 计算得到的 block_order
  270. """
  271. if after_block_id:
  272. # 1. 获取 after_block 的 block_order
  273. after_block = self.get_block_by_id(after_block_id)
  274. if not after_block:
  275. raise ValueError(f"Block not found: {after_block_id}")
  276. after_order = after_block['block_order']
  277. # 2. 查询下一个 block 的 block_order
  278. cursor = self.conn.execute("""
  279. SELECT block_order FROM document_blocks
  280. WHERE block_order > ?
  281. ORDER BY block_order LIMIT 1
  282. """, (after_order,))
  283. next_row = cursor.fetchone()
  284. if next_row:
  285. next_order = next_row['block_order']
  286. gap = next_order - after_order
  287. # 3. 检查间隙是否足够
  288. if gap <= 1:
  289. # 触发局部重排
  290. self._rebalance_block_orders_between(after_order, next_order)
  291. # 重新查询
  292. cursor = self.conn.execute("""
  293. SELECT block_order FROM document_blocks
  294. WHERE block_order > ?
  295. ORDER BY block_order LIMIT 1
  296. """, (after_order,))
  297. next_row = cursor.fetchone()
  298. next_order = next_row['block_order'] if next_row else after_order + 200
  299. # 4. 计算中间值
  300. return (after_order + next_order) // 2
  301. else:
  302. # 没有下一个 block,插入到最后
  303. return after_order + 100
  304. else:
  305. # 插入到文档末尾
  306. cursor = self.conn.execute("""
  307. SELECT MAX(block_order) as max_order FROM document_blocks
  308. """)
  309. row = cursor.fetchone()
  310. max_order = row['max_order'] if row['max_order'] is not None else 0
  311. return max_order + 100
  312. def _rebalance_indexes_between(
  313. self,
  314. block_type: str,
  315. level: int,
  316. start_index: int,
  317. end_index: int
  318. ):
  319. """局部重排:重新分配区间内同类型同级别 blocks 的 index
  320. Args:
  321. block_type: Block 类型
  322. level: 级别(标题有效)
  323. start_index: 起始 index
  324. end_index: 结束 index
  325. """
  326. # 查询区间内的所有同类型同级别 blocks
  327. if block_type == 'heading':
  328. cursor = self.conn.execute("""
  329. SELECT id, "index"
  330. FROM document_blocks
  331. WHERE type = ? AND level = ? AND "index" > ? AND "index" < ?
  332. ORDER BY "index"
  333. """, (block_type, level, start_index, end_index))
  334. else:
  335. cursor = self.conn.execute("""
  336. SELECT id, "index"
  337. FROM document_blocks
  338. WHERE type = ? AND "index" > ? AND "index" < ?
  339. ORDER BY "index"
  340. """, (block_type, start_index, end_index))
  341. blocks = cursor.fetchall()
  342. if not blocks:
  343. return
  344. # 计算新的间隔
  345. count = len(blocks)
  346. gap = end_index - start_index
  347. step = gap // (count + 1)
  348. # 重新分配 index
  349. new_index = start_index
  350. for block in blocks:
  351. new_index += step
  352. self.conn.execute(
  353. 'UPDATE document_blocks SET "index" = ? WHERE id = ?',
  354. (new_index, block['id'])
  355. )
  356. self.conn.commit()
  357. def _rebalance_block_orders_between(self, start_order: int, end_order: int):
  358. """局部重排:重新分配区间内所有 blocks 的 block_order
  359. Args:
  360. start_order: 起始 block_order
  361. end_order: 结束 block_order
  362. """
  363. # 查询区间内的所有 blocks
  364. cursor = self.conn.execute("""
  365. SELECT id, block_order
  366. FROM document_blocks
  367. WHERE block_order > ? AND block_order < ?
  368. ORDER BY block_order
  369. """, (start_order, end_order))
  370. blocks = cursor.fetchall()
  371. if not blocks:
  372. return
  373. # 计算新的间隔
  374. count = len(blocks)
  375. gap = end_order - start_order
  376. step = gap // (count + 1)
  377. # 重新分配 block_order
  378. new_order = start_order
  379. for block in blocks:
  380. new_order += step
  381. self.conn.execute(
  382. 'UPDATE document_blocks SET block_order = ? WHERE id = ?',
  383. (new_order, block['id'])
  384. )
  385. self.conn.commit()
  386. def _row_to_dict(self, row) -> dict:
  387. """将 sqlite3.Row 转换为字典"""
  388. d = dict(row)
  389. # 解析 JSON 字段
  390. if d.get('style'):
  391. try:
  392. d['style'] = json.loads(d['style'])
  393. except (json.JSONDecodeError, TypeError):
  394. d['style'] = {}
  395. else:
  396. d['style'] = {}
  397. if d.get('metadata'):
  398. try:
  399. d['metadata'] = json.loads(d['metadata'])
  400. except (json.JSONDecodeError, TypeError):
  401. d['metadata'] = {}
  402. else:
  403. d['metadata'] = {}
  404. # content 可能是 JSON(富文本或表格)
  405. if d.get('content'):
  406. try:
  407. d['content'] = json.loads(d['content'])
  408. except (json.JSONDecodeError, TypeError):
  409. pass # 保持原字符串
  410. return d
  411. @staticmethod
  412. def generate_block_id(block_type: str, level: int, index: int) -> str:
  413. """生成 Block ID
  414. Args:
  415. block_type: Block 类型
  416. level: 级别(标题有效)
  417. index: 序号
  418. Returns:
  419. 生成的 Block ID
  420. """
  421. if block_type == 'heading':
  422. return f'block-h{level}-{index}'
  423. elif block_type == 'paragraph':
  424. return f'block-p-{index}'
  425. elif block_type == 'table':
  426. return f'block-table-{index}'
  427. elif block_type == 'image':
  428. return f'block-img-{index}'
  429. elif block_type == 'toc':
  430. return f'block-toc-{index}'
  431. else:
  432. return f'block-{block_type}-{index}'