database.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  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. workflow_key TEXT UNIQUE,
  173. name TEXT NOT NULL,
  174. description TEXT,
  175. raw_json TEXT,
  176. created_at TEXT NOT NULL,
  177. updated_at TEXT NOT NULL
  178. );
  179. CREATE TABLE IF NOT EXISTS automation_workflow_nodes (
  180. id INTEGER PRIMARY KEY AUTOINCREMENT,
  181. workflow_id INTEGER NOT NULL,
  182. node_index INTEGER NOT NULL,
  183. node_key TEXT,
  184. node_type TEXT NOT NULL,
  185. screen_id INTEGER,
  186. title TEXT,
  187. position_x REAL NOT NULL DEFAULT 80,
  188. position_y REAL NOT NULL DEFAULT 80,
  189. next_node_keys TEXT,
  190. config_json TEXT NOT NULL,
  191. created_at TEXT NOT NULL,
  192. updated_at TEXT NOT NULL,
  193. FOREIGN KEY(workflow_id) REFERENCES automation_workflows(id) ON DELETE CASCADE,
  194. FOREIGN KEY(screen_id) REFERENCES automation_screens(id) ON DELETE SET NULL
  195. );
  196. CREATE INDEX IF NOT EXISTS idx_automation_workflow_nodes_workflow
  197. ON automation_workflow_nodes(workflow_id, node_index);
  198. CREATE TABLE IF NOT EXISTS automation_workflow_tasks (
  199. id TEXT PRIMARY KEY,
  200. workflow_id INTEGER,
  201. workflow_key TEXT NOT NULL,
  202. workflow_name TEXT NOT NULL,
  203. status TEXT NOT NULL,
  204. request_json TEXT NOT NULL,
  205. workflow_snapshot_json TEXT NOT NULL,
  206. result_json TEXT,
  207. return_data_json TEXT,
  208. error_message TEXT,
  209. created_at TEXT NOT NULL,
  210. started_at TEXT,
  211. finished_at TEXT,
  212. FOREIGN KEY(workflow_id) REFERENCES automation_workflows(id) ON DELETE SET NULL
  213. );
  214. CREATE INDEX IF NOT EXISTS idx_automation_workflow_tasks_status_created
  215. ON automation_workflow_tasks(status, created_at);
  216. CREATE INDEX IF NOT EXISTS idx_automation_workflow_tasks_created
  217. ON automation_workflow_tasks(created_at DESC);
  218. CREATE TABLE IF NOT EXISTS automation_workflow_runtime (
  219. id INTEGER PRIMARY KEY CHECK(id = 1),
  220. active_task_id TEXT,
  221. updated_at TEXT,
  222. FOREIGN KEY(active_task_id) REFERENCES automation_workflow_tasks(id) ON DELETE SET NULL
  223. );
  224. INSERT OR IGNORE INTO automation_workflow_runtime (id, active_task_id, updated_at)
  225. VALUES (1, NULL, NULL);
  226. CREATE TABLE IF NOT EXISTS automation_errors (
  227. id INTEGER PRIMARY KEY AUTOINCREMENT,
  228. workflow_id INTEGER,
  229. node_id INTEGER,
  230. screen_id INTEGER,
  231. action_type TEXT,
  232. message TEXT NOT NULL,
  233. similarity REAL,
  234. expected_image_path TEXT,
  235. actual_image_path TEXT,
  236. compare_result_json TEXT,
  237. created_at TEXT NOT NULL,
  238. FOREIGN KEY(workflow_id) REFERENCES automation_workflows(id) ON DELETE SET NULL,
  239. FOREIGN KEY(node_id) REFERENCES automation_workflow_nodes(id) ON DELETE SET NULL,
  240. FOREIGN KEY(screen_id) REFERENCES automation_screens(id) ON DELETE SET NULL
  241. );
  242. CREATE INDEX IF NOT EXISTS idx_automation_errors_created
  243. ON automation_errors(created_at DESC);
  244. """
  245. )
  246. ensure_column(conn, "automation_workflows", "workflow_key", "TEXT")
  247. conn.execute(
  248. """
  249. CREATE UNIQUE INDEX IF NOT EXISTS idx_automation_workflows_key
  250. ON automation_workflows(workflow_key)
  251. WHERE workflow_key IS NOT NULL AND workflow_key != ''
  252. """
  253. )
  254. ensure_column(conn, "automation_workflow_nodes", "node_key", "TEXT")
  255. ensure_column(conn, "automation_workflow_nodes", "position_x", "REAL NOT NULL DEFAULT 80")
  256. ensure_column(conn, "automation_workflow_nodes", "position_y", "REAL NOT NULL DEFAULT 80")
  257. ensure_column(conn, "automation_workflow_nodes", "next_node_keys", "TEXT")
  258. ensure_column(conn, "automation_screen_elements", "approximate_location", "TEXT")
  259. ensure_column(conn, "automation_screen_elements", "is_located", "INTEGER NOT NULL DEFAULT 0")
  260. seed_default_tags(conn)
  261. seed_default_settings(conn)
  262. def ensure_column(conn: sqlite3.Connection, table: str, column: str, definition: str) -> None:
  263. existing = {row["name"] for row in conn.execute(f"PRAGMA table_info({table})").fetchall()}
  264. if column not in existing:
  265. conn.execute(f"ALTER TABLE {table} ADD COLUMN {column} {definition}")
  266. def seed_default_tags(conn: sqlite3.Connection) -> None:
  267. rows = [
  268. (
  269. "windows系统",
  270. "Windows 系统原生服务或进程。默认不可控制,避免误停止系统关键组件。",
  271. 0,
  272. 1,
  273. ),
  274. (
  275. "本系统相关",
  276. "本项目程序自身相关服务或进程。默认不可控制,避免通过监控系统停止自身。",
  277. 0,
  278. 1,
  279. ),
  280. ]
  281. now = __import__("datetime").datetime.now().astimezone().isoformat(timespec="seconds")
  282. for name, description, is_controllable, is_builtin in rows:
  283. conn.execute(
  284. """
  285. INSERT INTO tags (name, description, is_controllable, is_builtin, created_at, updated_at)
  286. VALUES (?, ?, ?, ?, ?, ?)
  287. ON CONFLICT(name) DO UPDATE SET
  288. description = COALESCE(tags.description, excluded.description),
  289. is_builtin = 1,
  290. updated_at = excluded.updated_at
  291. """,
  292. (name, description, is_controllable, is_builtin, now, now),
  293. )
  294. def seed_default_settings(conn: sqlite3.Connection) -> None:
  295. settings = [
  296. ("default_ai_provider_id", "", "全局默认 AI 服务商 ID"),
  297. ("default_ai_model_id", "", "全局默认 AI 模型 ID"),
  298. ("default_ai_temperature", "0.1", "全局默认 AI 温度参数"),
  299. ("automation_file_root", "automation", "自动化文件保存根路径"),
  300. ("automation_screen_path", "automation/screens", "已识别界面截图保存路径"),
  301. ("automation_error_path", "automation/errors", "自动化错误截图保存路径"),
  302. ("automation_runtime_path", "automation/runtime", "自动化临时截图保存路径"),
  303. ("automation_auto_screenshot_enabled", "0", "自动化操作页面是否默认自动截屏"),
  304. ("automation_auto_screenshot_interval", "30", "自动化操作页面默认自动截屏间隔秒数"),
  305. ("automation_remote_token", "", "远程执行工作流和查询任务状态的 Token,设置后需通过 X-Automation-Token 或 Bearer Token 传入"),
  306. ]
  307. now = __import__("datetime").datetime.now().astimezone().isoformat(timespec="seconds")
  308. for key, value, description in settings:
  309. conn.execute(
  310. """
  311. INSERT INTO app_settings (key, value, description, updated_at)
  312. VALUES (?, ?, ?, ?)
  313. ON CONFLICT(key) DO UPDATE SET
  314. description = excluded.description
  315. """,
  316. (key, value, description, now),
  317. )