database.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. from __future__ import annotations
  2. import sqlite3
  3. from contextlib import contextmanager
  4. from pathlib import Path
  5. from typing import Iterator
  6. BASE_DIR = Path(__file__).resolve().parent.parent
  7. DATA_DIR = BASE_DIR / "data"
  8. DB_PATH = DATA_DIR / "win_monitor.db"
  9. def dict_factory(cursor: sqlite3.Cursor, row: sqlite3.Row) -> dict:
  10. fields = [column[0] for column in cursor.description]
  11. return {key: row[index] for index, key in enumerate(fields)}
  12. @contextmanager
  13. def get_db() -> Iterator[sqlite3.Connection]:
  14. DATA_DIR.mkdir(parents=True, exist_ok=True)
  15. conn = sqlite3.connect(DB_PATH)
  16. conn.row_factory = dict_factory
  17. conn.execute("PRAGMA foreign_keys = ON")
  18. try:
  19. yield conn
  20. conn.commit()
  21. except Exception:
  22. conn.rollback()
  23. raise
  24. finally:
  25. conn.close()
  26. def init_db() -> None:
  27. with get_db() as conn:
  28. conn.executescript(
  29. """
  30. CREATE TABLE IF NOT EXISTS scan_records (
  31. id INTEGER PRIMARY KEY AUTOINCREMENT,
  32. started_at TEXT NOT NULL,
  33. finished_at TEXT,
  34. status TEXT NOT NULL,
  35. services_found INTEGER NOT NULL DEFAULT 0,
  36. processes_found INTEGER NOT NULL DEFAULT 0,
  37. new_services INTEGER NOT NULL DEFAULT 0,
  38. new_processes INTEGER NOT NULL DEFAULT 0,
  39. error_message TEXT
  40. );
  41. CREATE TABLE IF NOT EXISTS windows_services (
  42. id INTEGER PRIMARY KEY AUTOINCREMENT,
  43. identity_key TEXT NOT NULL UNIQUE,
  44. name TEXT NOT NULL,
  45. display_name TEXT,
  46. status TEXT,
  47. start_type TEXT,
  48. username TEXT,
  49. binary_path TEXT,
  50. description TEXT,
  51. is_present_now INTEGER NOT NULL DEFAULT 1,
  52. first_seen_at TEXT NOT NULL,
  53. last_seen_at TEXT NOT NULL,
  54. confirm_status TEXT NOT NULL DEFAULT 'PENDING',
  55. user_note TEXT,
  56. ai_description TEXT,
  57. ai_reason TEXT,
  58. ai_suggestion TEXT,
  59. risk_level TEXT,
  60. updated_at TEXT NOT NULL
  61. );
  62. CREATE TABLE IF NOT EXISTS windows_processes (
  63. id INTEGER PRIMARY KEY AUTOINCREMENT,
  64. identity_key TEXT NOT NULL UNIQUE,
  65. name TEXT NOT NULL,
  66. exe_path TEXT,
  67. cmdline TEXT,
  68. username TEXT,
  69. status TEXT,
  70. cwd TEXT,
  71. last_pid INTEGER,
  72. parent_pid INTEGER,
  73. create_time TEXT,
  74. is_present_now INTEGER NOT NULL DEFAULT 1,
  75. first_seen_at TEXT NOT NULL,
  76. last_seen_at TEXT NOT NULL,
  77. confirm_status TEXT NOT NULL DEFAULT 'PENDING',
  78. user_note TEXT,
  79. ai_description TEXT,
  80. ai_reason TEXT,
  81. ai_suggestion TEXT,
  82. risk_level TEXT,
  83. updated_at TEXT NOT NULL
  84. );
  85. CREATE INDEX IF NOT EXISTS idx_services_confirm_status
  86. ON windows_services(confirm_status);
  87. CREATE INDEX IF NOT EXISTS idx_processes_confirm_status
  88. ON windows_processes(confirm_status);
  89. CREATE INDEX IF NOT EXISTS idx_scan_records_started_at
  90. ON scan_records(started_at DESC);
  91. CREATE TABLE IF NOT EXISTS tags (
  92. id INTEGER PRIMARY KEY AUTOINCREMENT,
  93. name TEXT NOT NULL UNIQUE,
  94. description TEXT,
  95. is_controllable INTEGER NOT NULL DEFAULT 1,
  96. is_builtin INTEGER NOT NULL DEFAULT 0,
  97. created_at TEXT NOT NULL,
  98. updated_at TEXT NOT NULL
  99. );
  100. CREATE TABLE IF NOT EXISTS item_tags (
  101. id INTEGER PRIMARY KEY AUTOINCREMENT,
  102. item_type TEXT NOT NULL CHECK(item_type IN ('service', 'process')),
  103. item_id INTEGER NOT NULL,
  104. tag_id INTEGER NOT NULL,
  105. created_at TEXT NOT NULL,
  106. UNIQUE(item_type, item_id, tag_id),
  107. FOREIGN KEY(tag_id) REFERENCES tags(id) ON DELETE CASCADE
  108. );
  109. CREATE INDEX IF NOT EXISTS idx_item_tags_item
  110. ON item_tags(item_type, item_id);
  111. CREATE TABLE IF NOT EXISTS ai_providers (
  112. id INTEGER PRIMARY KEY AUTOINCREMENT,
  113. name TEXT NOT NULL UNIQUE,
  114. provider_type TEXT NOT NULL CHECK(provider_type IN ('OPENAI', 'OPENAI_COMPATIBLE', 'GOOGLE_GEMINI')),
  115. base_url TEXT,
  116. api_key TEXT,
  117. enabled INTEGER NOT NULL DEFAULT 1,
  118. created_at TEXT NOT NULL,
  119. updated_at TEXT NOT NULL
  120. );
  121. CREATE TABLE IF NOT EXISTS ai_models (
  122. id INTEGER PRIMARY KEY AUTOINCREMENT,
  123. provider_id INTEGER NOT NULL,
  124. name TEXT NOT NULL,
  125. display_name TEXT,
  126. is_default INTEGER NOT NULL DEFAULT 0,
  127. created_at TEXT NOT NULL,
  128. updated_at TEXT NOT NULL,
  129. UNIQUE(provider_id, name),
  130. FOREIGN KEY(provider_id) REFERENCES ai_providers(id) ON DELETE CASCADE
  131. );
  132. CREATE INDEX IF NOT EXISTS idx_ai_models_provider
  133. ON ai_models(provider_id);
  134. CREATE TABLE IF NOT EXISTS app_settings (
  135. key TEXT PRIMARY KEY,
  136. value TEXT,
  137. description TEXT,
  138. updated_at TEXT NOT NULL
  139. );
  140. CREATE TABLE IF NOT EXISTS automation_screens (
  141. id INTEGER PRIMARY KEY AUTOINCREMENT,
  142. interface_name TEXT NOT NULL,
  143. description TEXT,
  144. image_path TEXT NOT NULL,
  145. width INTEGER NOT NULL,
  146. height INTEGER NOT NULL,
  147. is_windows_desktop INTEGER NOT NULL DEFAULT 0,
  148. is_browser_webpage INTEGER NOT NULL DEFAULT 0,
  149. raw_ai_json TEXT,
  150. created_at TEXT NOT NULL,
  151. updated_at TEXT NOT NULL
  152. );
  153. CREATE TABLE IF NOT EXISTS automation_screen_elements (
  154. id INTEGER PRIMARY KEY AUTOINCREMENT,
  155. screen_id INTEGER NOT NULL,
  156. element_index INTEGER NOT NULL,
  157. name TEXT NOT NULL,
  158. x_percent REAL NOT NULL,
  159. y_percent REAL NOT NULL,
  160. x INTEGER NOT NULL,
  161. y INTEGER NOT NULL,
  162. approximate_location TEXT,
  163. is_located INTEGER NOT NULL DEFAULT 0,
  164. raw_json TEXT,
  165. created_at TEXT NOT NULL,
  166. FOREIGN KEY(screen_id) REFERENCES automation_screens(id) ON DELETE CASCADE
  167. );
  168. CREATE INDEX IF NOT EXISTS idx_automation_screen_elements_screen
  169. ON automation_screen_elements(screen_id, element_index);
  170. CREATE TABLE IF NOT EXISTS automation_workflows (
  171. id INTEGER PRIMARY KEY AUTOINCREMENT,
  172. name TEXT NOT NULL,
  173. description TEXT,
  174. raw_json TEXT,
  175. created_at TEXT NOT NULL,
  176. updated_at TEXT NOT NULL
  177. );
  178. CREATE TABLE IF NOT EXISTS automation_workflow_nodes (
  179. id INTEGER PRIMARY KEY AUTOINCREMENT,
  180. workflow_id INTEGER NOT NULL,
  181. node_index INTEGER NOT NULL,
  182. node_key TEXT,
  183. node_type TEXT NOT NULL,
  184. screen_id INTEGER,
  185. title TEXT,
  186. position_x REAL NOT NULL DEFAULT 80,
  187. position_y REAL NOT NULL DEFAULT 80,
  188. next_node_keys TEXT,
  189. config_json TEXT NOT NULL,
  190. created_at TEXT NOT NULL,
  191. updated_at TEXT NOT NULL,
  192. FOREIGN KEY(workflow_id) REFERENCES automation_workflows(id) ON DELETE CASCADE,
  193. FOREIGN KEY(screen_id) REFERENCES automation_screens(id) ON DELETE SET NULL
  194. );
  195. CREATE INDEX IF NOT EXISTS idx_automation_workflow_nodes_workflow
  196. ON automation_workflow_nodes(workflow_id, node_index);
  197. CREATE TABLE IF NOT EXISTS automation_errors (
  198. id INTEGER PRIMARY KEY AUTOINCREMENT,
  199. workflow_id INTEGER,
  200. node_id INTEGER,
  201. screen_id INTEGER,
  202. action_type TEXT,
  203. message TEXT NOT NULL,
  204. similarity REAL,
  205. expected_image_path TEXT,
  206. actual_image_path TEXT,
  207. compare_result_json TEXT,
  208. created_at TEXT NOT NULL,
  209. FOREIGN KEY(workflow_id) REFERENCES automation_workflows(id) ON DELETE SET NULL,
  210. FOREIGN KEY(node_id) REFERENCES automation_workflow_nodes(id) ON DELETE SET NULL,
  211. FOREIGN KEY(screen_id) REFERENCES automation_screens(id) ON DELETE SET NULL
  212. );
  213. CREATE INDEX IF NOT EXISTS idx_automation_errors_created
  214. ON automation_errors(created_at DESC);
  215. """
  216. )
  217. ensure_column(conn, "automation_workflow_nodes", "node_key", "TEXT")
  218. ensure_column(conn, "automation_workflow_nodes", "position_x", "REAL NOT NULL DEFAULT 80")
  219. ensure_column(conn, "automation_workflow_nodes", "position_y", "REAL NOT NULL DEFAULT 80")
  220. ensure_column(conn, "automation_workflow_nodes", "next_node_keys", "TEXT")
  221. ensure_column(conn, "automation_screen_elements", "approximate_location", "TEXT")
  222. ensure_column(conn, "automation_screen_elements", "is_located", "INTEGER NOT NULL DEFAULT 0")
  223. seed_default_tags(conn)
  224. seed_default_settings(conn)
  225. def ensure_column(conn: sqlite3.Connection, table: str, column: str, definition: str) -> None:
  226. existing = {row["name"] for row in conn.execute(f"PRAGMA table_info({table})").fetchall()}
  227. if column not in existing:
  228. conn.execute(f"ALTER TABLE {table} ADD COLUMN {column} {definition}")
  229. def seed_default_tags(conn: sqlite3.Connection) -> None:
  230. rows = [
  231. (
  232. "windows系统",
  233. "Windows 系统原生服务或进程。默认不可控制,避免误停止系统关键组件。",
  234. 0,
  235. 1,
  236. ),
  237. (
  238. "本系统相关",
  239. "本项目程序自身相关服务或进程。默认不可控制,避免通过监控系统停止自身。",
  240. 0,
  241. 1,
  242. ),
  243. ]
  244. now = __import__("datetime").datetime.now().astimezone().isoformat(timespec="seconds")
  245. for name, description, is_controllable, is_builtin in rows:
  246. conn.execute(
  247. """
  248. INSERT INTO tags (name, description, is_controllable, is_builtin, created_at, updated_at)
  249. VALUES (?, ?, ?, ?, ?, ?)
  250. ON CONFLICT(name) DO UPDATE SET
  251. description = COALESCE(tags.description, excluded.description),
  252. is_builtin = 1,
  253. updated_at = excluded.updated_at
  254. """,
  255. (name, description, is_controllable, is_builtin, now, now),
  256. )
  257. def seed_default_settings(conn: sqlite3.Connection) -> None:
  258. settings = [
  259. ("default_ai_provider_id", "", "全局默认 AI 服务商 ID"),
  260. ("default_ai_model_id", "", "全局默认 AI 模型 ID"),
  261. ("default_ai_temperature", "0.1", "全局默认 AI 温度参数"),
  262. ("automation_file_root", "automation", "自动化文件保存根路径"),
  263. ("automation_screen_path", "automation/screens", "已识别界面截图保存路径"),
  264. ("automation_error_path", "automation/errors", "自动化错误截图保存路径"),
  265. ("automation_runtime_path", "automation/runtime", "自动化临时截图保存路径"),
  266. ("automation_auto_screenshot_enabled", "0", "自动化操作页面是否默认自动截屏"),
  267. ("automation_auto_screenshot_interval", "30", "自动化操作页面默认自动截屏间隔秒数"),
  268. ]
  269. now = __import__("datetime").datetime.now().astimezone().isoformat(timespec="seconds")
  270. for key, value, description in settings:
  271. conn.execute(
  272. """
  273. INSERT INTO app_settings (key, value, description, updated_at)
  274. VALUES (?, ?, ?, ?)
  275. ON CONFLICT(key) DO UPDATE SET
  276. description = excluded.description
  277. """,
  278. (key, value, description, now),
  279. )