database.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  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. raw_json TEXT,
  163. created_at TEXT NOT NULL,
  164. FOREIGN KEY(screen_id) REFERENCES automation_screens(id) ON DELETE CASCADE
  165. );
  166. CREATE INDEX IF NOT EXISTS idx_automation_screen_elements_screen
  167. ON automation_screen_elements(screen_id, element_index);
  168. CREATE TABLE IF NOT EXISTS automation_workflows (
  169. id INTEGER PRIMARY KEY AUTOINCREMENT,
  170. name TEXT NOT NULL,
  171. description TEXT,
  172. raw_json TEXT,
  173. created_at TEXT NOT NULL,
  174. updated_at TEXT NOT NULL
  175. );
  176. CREATE TABLE IF NOT EXISTS automation_workflow_nodes (
  177. id INTEGER PRIMARY KEY AUTOINCREMENT,
  178. workflow_id INTEGER NOT NULL,
  179. node_index INTEGER NOT NULL,
  180. node_key TEXT,
  181. node_type TEXT NOT NULL,
  182. screen_id INTEGER,
  183. title TEXT,
  184. position_x REAL NOT NULL DEFAULT 80,
  185. position_y REAL NOT NULL DEFAULT 80,
  186. next_node_keys TEXT,
  187. config_json TEXT NOT NULL,
  188. created_at TEXT NOT NULL,
  189. updated_at TEXT NOT NULL,
  190. FOREIGN KEY(workflow_id) REFERENCES automation_workflows(id) ON DELETE CASCADE,
  191. FOREIGN KEY(screen_id) REFERENCES automation_screens(id) ON DELETE SET NULL
  192. );
  193. CREATE INDEX IF NOT EXISTS idx_automation_workflow_nodes_workflow
  194. ON automation_workflow_nodes(workflow_id, node_index);
  195. CREATE TABLE IF NOT EXISTS automation_errors (
  196. id INTEGER PRIMARY KEY AUTOINCREMENT,
  197. workflow_id INTEGER,
  198. node_id INTEGER,
  199. screen_id INTEGER,
  200. action_type TEXT,
  201. message TEXT NOT NULL,
  202. similarity REAL,
  203. expected_image_path TEXT,
  204. actual_image_path TEXT,
  205. compare_result_json TEXT,
  206. created_at TEXT NOT NULL,
  207. FOREIGN KEY(workflow_id) REFERENCES automation_workflows(id) ON DELETE SET NULL,
  208. FOREIGN KEY(node_id) REFERENCES automation_workflow_nodes(id) ON DELETE SET NULL,
  209. FOREIGN KEY(screen_id) REFERENCES automation_screens(id) ON DELETE SET NULL
  210. );
  211. CREATE INDEX IF NOT EXISTS idx_automation_errors_created
  212. ON automation_errors(created_at DESC);
  213. """
  214. )
  215. ensure_column(conn, "automation_workflow_nodes", "node_key", "TEXT")
  216. ensure_column(conn, "automation_workflow_nodes", "position_x", "REAL NOT NULL DEFAULT 80")
  217. ensure_column(conn, "automation_workflow_nodes", "position_y", "REAL NOT NULL DEFAULT 80")
  218. ensure_column(conn, "automation_workflow_nodes", "next_node_keys", "TEXT")
  219. seed_default_tags(conn)
  220. seed_default_settings(conn)
  221. def ensure_column(conn: sqlite3.Connection, table: str, column: str, definition: str) -> None:
  222. existing = {row["name"] for row in conn.execute(f"PRAGMA table_info({table})").fetchall()}
  223. if column not in existing:
  224. conn.execute(f"ALTER TABLE {table} ADD COLUMN {column} {definition}")
  225. def seed_default_tags(conn: sqlite3.Connection) -> None:
  226. rows = [
  227. (
  228. "windows系统",
  229. "Windows 系统原生服务或进程。默认不可控制,避免误停止系统关键组件。",
  230. 0,
  231. 1,
  232. ),
  233. (
  234. "本系统相关",
  235. "本项目程序自身相关服务或进程。默认不可控制,避免通过监控系统停止自身。",
  236. 0,
  237. 1,
  238. ),
  239. ]
  240. now = __import__("datetime").datetime.now().astimezone().isoformat(timespec="seconds")
  241. for name, description, is_controllable, is_builtin in rows:
  242. conn.execute(
  243. """
  244. INSERT INTO tags (name, description, is_controllable, is_builtin, created_at, updated_at)
  245. VALUES (?, ?, ?, ?, ?, ?)
  246. ON CONFLICT(name) DO UPDATE SET
  247. description = COALESCE(tags.description, excluded.description),
  248. is_builtin = 1,
  249. updated_at = excluded.updated_at
  250. """,
  251. (name, description, is_controllable, is_builtin, now, now),
  252. )
  253. def seed_default_settings(conn: sqlite3.Connection) -> None:
  254. settings = [
  255. ("default_ai_provider_id", "", "全局默认 AI 服务商 ID"),
  256. ("default_ai_model_id", "", "全局默认 AI 模型 ID"),
  257. ("default_ai_temperature", "0.1", "全局默认 AI 温度参数"),
  258. ("automation_file_root", "automation", "自动化文件保存根路径"),
  259. ("automation_screen_path", "automation/screens", "已识别界面截图保存路径"),
  260. ("automation_error_path", "automation/errors", "自动化错误截图保存路径"),
  261. ("automation_runtime_path", "automation/runtime", "自动化临时截图保存路径"),
  262. ("automation_auto_screenshot_enabled", "0", "自动化操作页面是否默认自动截屏"),
  263. ("automation_auto_screenshot_interval", "30", "自动化操作页面默认自动截屏间隔秒数"),
  264. ]
  265. now = __import__("datetime").datetime.now().astimezone().isoformat(timespec="seconds")
  266. for key, value, description in settings:
  267. conn.execute(
  268. """
  269. INSERT INTO app_settings (key, value, description, updated_at)
  270. VALUES (?, ?, ?, ?)
  271. ON CONFLICT(key) DO UPDATE SET
  272. description = excluded.description
  273. """,
  274. (key, value, description, now),
  275. )