main.py 39 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073
  1. from __future__ import annotations
  2. import json
  3. import sqlite3
  4. from typing import Any
  5. from fastapi import FastAPI, HTTPException, Query
  6. from fastapi.middleware.cors import CORSMiddleware
  7. from . import ai_service, automation_service, settings_service, windows_automation
  8. from .control import (
  9. CONFIRMED_CONTROL_STATUSES,
  10. restart_service,
  11. start_process,
  12. start_service,
  13. stop_process,
  14. stop_service,
  15. )
  16. from .database import get_db, init_db
  17. from .scanner import now_iso, run_full_scan
  18. from .sensors import collect_sensors
  19. from .schemas import (
  20. AiAnalyzeRequest,
  21. AiChatRequest,
  22. AiImportRequest,
  23. AiModelCreate,
  24. AiModelUpdate,
  25. AiProviderCreate,
  26. AiProviderUpdate,
  27. AutomationKeyboardRequest,
  28. AutomationKeyboardActionRequest,
  29. AutomationElementLocateRequest,
  30. AutomationMouseRequest,
  31. AutomationMouseActionRequest,
  32. AutomationPowerRequest,
  33. AutomationCloseProgramsRequest,
  34. AutomationScreenshotCaptureRequest,
  35. AutomationStartProgramRequest,
  36. AutomationProgramStartRequest,
  37. AutomationProgramStopRequest,
  38. AutomationScreenshotRequest,
  39. AutomationTextInputRequest,
  40. AutomationVisionAnalyzeRequest,
  41. AutomationWorkflowRunRequest,
  42. AutomationWorkflowSaveRequest,
  43. BatchStatusUpdate,
  44. PromptRequest,
  45. StatusUpdate,
  46. SystemSettingsUpdate,
  47. TagAssignRequest,
  48. TagCreate,
  49. TagUpdate,
  50. )
  51. from .smart import collect_all_smart, get_device_smart, scan_devices
  52. AI_PROMPT_TEMPLATE = """请作为资深的 Windows 系统安全专家,帮我分析下面这些 Windows 服务和进程是否可信,并严格按照 JSON 数组格式输出结果。
  53. 输出要求:
  54. 1. 必须且只能输出纯 JSON 数组,不要输出任何额外的解释、问候语,也不要使用 Markdown 代码块(如 ```json)包裹。
  55. 2. 每个对象必须包含以下 8 个字段:type、name、description、judgement、risk_level、reason、suggestion、tags。
  56. 3. type 只能是 "service" 或 "process"。
  57. 4. description 请简要说明该服务或进程的官方用途或常规功能(如果是未知/恶意程序,请描述其伪装意图或表现)。
  58. 5. judgement 只能是 "TRUSTED"、"SUSPICIOUS"、"NEED_MORE_INFO"。
  59. 6. risk_level 只能是 "LOW"、"MEDIUM"、"HIGH"。
  60. 7. 如果提供的信息不足以做出判断,请将 judgement 设为 "NEED_MORE_INFO"。
  61. 8. 待分析数据里的 tags 字段是当前已有标签上下文,不代表最终结论,但如果标签显示为“windows系统”或“本系统相关”,请在 reason 或 suggestion 中体现这一点。
  62. 9. 输出对象里的 tags 字段必须是字符串数组,填写你建议系统最终绑定到该对象上的标签名称。可以使用系统已有标签,也可以在确有必要时给出新的短标签名称;标签名称应简洁稳定,不要把长句放入标签。
  63. JSON 格式示例:
  64. [
  65. {
  66. "type": "service",
  67. "name": "WinDefend",
  68. "description": "Microsoft Defender 防病毒核心服务,负责保护系统免受恶意软件和间谍软件的威胁。",
  69. "judgement": "TRUSTED",
  70. "risk_level": "LOW",
  71. "reason": "这是 Microsoft 官方的安全组件,路径和名称符合系统原生服务的标准特征。",
  72. "suggestion": "可标记为可信,建议保持运行。",
  73. "tags": ["windows系统"]
  74. },
  75. {
  76. "type": "process",
  77. "name": "unknown.exe",
  78. "description": "未知用途的执行文件,无明确的官方功能说明。",
  79. "judgement": "SUSPICIOUS",
  80. "risk_level": "HIGH",
  81. "reason": "进程位于用户 AppData 临时目录,启动命令行异常,且缺少有效的官方数字签名。",
  82. "suggestion": "建议立即隔离,检查文件的 SHA256 散列值及外部网络连接记录,不要直接运行或信任。",
  83. "tags": ["可疑程序"]
  84. }
  85. ]
  86. 下面是待分析数据:
  87. {pending_items_json}
  88. 系统中已有标签信息:
  89. {tags_json}
  90. """
  91. app = FastAPI(title="Windows Monitor API", version="1.0.0")
  92. app.add_middleware(
  93. CORSMiddleware,
  94. allow_origins=["*"],
  95. allow_credentials=True,
  96. allow_methods=["*"],
  97. allow_headers=["*"],
  98. )
  99. @app.on_event("startup")
  100. def startup() -> None:
  101. init_db()
  102. def build_where(
  103. keyword: str | None,
  104. confirm_status: str | None,
  105. present: bool | None,
  106. fields: list[str],
  107. ) -> tuple[str, list[Any]]:
  108. clauses: list[str] = []
  109. params: list[Any] = []
  110. if keyword:
  111. like = f"%{keyword}%"
  112. clauses.append("(" + " OR ".join(f"{field} LIKE ?" for field in fields) + ")")
  113. params.extend([like] * len(fields))
  114. if confirm_status:
  115. clauses.append("confirm_status = ?")
  116. params.append(confirm_status)
  117. if present is not None:
  118. clauses.append("is_present_now = ?")
  119. params.append(1 if present else 0)
  120. return ("WHERE " + " AND ".join(clauses)) if clauses else "", params
  121. def list_items(
  122. table: str,
  123. keyword: str | None,
  124. confirm_status: str | None,
  125. present: bool | None,
  126. page: int,
  127. page_size: int,
  128. fields: list[str],
  129. sort_by: str | None = None,
  130. sort_order: str | None = None,
  131. ) -> dict[str, Any]:
  132. where_sql, params = build_where(keyword, confirm_status, present, fields)
  133. order_sql = build_order_by(table, sort_by, sort_order)
  134. offset = (page - 1) * page_size
  135. with get_db() as conn:
  136. total = conn.execute(f"SELECT COUNT(*) AS total FROM {table} {where_sql}", params).fetchone()["total"]
  137. rows = conn.execute(
  138. f"SELECT * FROM {table} {where_sql} {order_sql} LIMIT ? OFFSET ?",
  139. [*params, page_size, offset],
  140. ).fetchall()
  141. rows = attach_item_metadata(conn, table_to_item_type(table), rows)
  142. return {"items": rows, "total": total, "page": page, "page_size": page_size}
  143. def build_order_by(table: str, sort_by: str | None, sort_order: str | None) -> str:
  144. allowed = {
  145. "windows_services": {
  146. "name",
  147. "display_name",
  148. "status",
  149. "start_type",
  150. "username",
  151. "is_present_now",
  152. "confirm_status",
  153. "first_seen_at",
  154. "last_seen_at",
  155. "updated_at",
  156. },
  157. "windows_processes": {
  158. "name",
  159. "exe_path",
  160. "username",
  161. "status",
  162. "last_pid",
  163. "parent_pid",
  164. "is_present_now",
  165. "confirm_status",
  166. "create_time",
  167. "first_seen_at",
  168. "last_seen_at",
  169. "updated_at",
  170. },
  171. }
  172. default_sql = "ORDER BY is_present_now DESC, last_seen_at DESC"
  173. if not sort_by or sort_by not in allowed.get(table, set()):
  174. return default_sql
  175. direction = "ASC" if sort_order == "asc" else "DESC"
  176. if sort_by == "is_present_now":
  177. return f"ORDER BY {sort_by} {direction}, last_seen_at DESC"
  178. return f"ORDER BY {sort_by} {direction}, is_present_now DESC, last_seen_at DESC"
  179. def get_item(table: str, item_id: int) -> dict[str, Any]:
  180. with get_db() as conn:
  181. item = conn.execute(f"SELECT * FROM {table} WHERE id = ?", (item_id,)).fetchone()
  182. if item:
  183. item = attach_item_metadata(conn, table_to_item_type(table), [item])[0]
  184. if not item:
  185. raise HTTPException(status_code=404, detail="Item not found")
  186. return item
  187. def table_to_item_type(table: str) -> str:
  188. if table == "windows_services":
  189. return "service"
  190. if table == "windows_processes":
  191. return "process"
  192. raise ValueError(f"Unsupported table: {table}")
  193. def bool_tag(row: dict[str, Any]) -> dict[str, Any]:
  194. item = dict(row)
  195. item["is_controllable"] = bool(item["is_controllable"])
  196. item["is_builtin"] = bool(item["is_builtin"])
  197. return item
  198. def all_tags(conn) -> list[dict[str, Any]]:
  199. return [
  200. bool_tag(row)
  201. for row in conn.execute("SELECT * FROM tags ORDER BY is_builtin DESC, name ASC").fetchall()
  202. ]
  203. def tags_for_items(conn, item_type: str, item_ids: list[int]) -> dict[int, list[dict[str, Any]]]:
  204. if not item_ids:
  205. return {}
  206. placeholders = ",".join("?" for _ in item_ids)
  207. rows = conn.execute(
  208. f"""
  209. SELECT it.item_id, t.*
  210. FROM item_tags it
  211. JOIN tags t ON t.id = it.tag_id
  212. WHERE it.item_type = ? AND it.item_id IN ({placeholders})
  213. ORDER BY t.name ASC
  214. """,
  215. [item_type, *item_ids],
  216. ).fetchall()
  217. result = {item_id: [] for item_id in item_ids}
  218. for row in rows:
  219. item_id = row["item_id"]
  220. tag = {key: value for key, value in row.items() if key != "item_id"}
  221. result.setdefault(item_id, []).append(bool_tag(tag))
  222. return result
  223. def can_control_item(item_type: str, row: dict[str, Any], tags: list[dict[str, Any]]) -> bool:
  224. if row.get("confirm_status") not in CONFIRMED_CONTROL_STATUSES:
  225. return False
  226. if item_type == "process":
  227. protected_names = {"system idle process", "system", "registry"}
  228. if row.get("last_pid") in (0, 4) or (row.get("name") or "").lower() in protected_names:
  229. return False
  230. return all(tag.get("is_controllable", True) for tag in tags)
  231. def attach_item_metadata(conn, item_type: str, rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
  232. tag_map = tags_for_items(conn, item_type, [row["id"] for row in rows])
  233. enriched = []
  234. for row in rows:
  235. item = dict(row)
  236. item["is_present_now"] = bool(item.get("is_present_now"))
  237. item["tags"] = tag_map.get(row["id"], [])
  238. item["can_control"] = can_control_item(item_type, item, item["tags"])
  239. enriched.append(item)
  240. return enriched
  241. def update_one(table: str, item_id: int, payload: StatusUpdate) -> dict[str, Any]:
  242. get_item(table, item_id)
  243. with get_db() as conn:
  244. conn.execute(
  245. f"UPDATE {table} SET confirm_status = ?, user_note = ?, updated_at = ? WHERE id = ?",
  246. (payload.confirm_status, payload.user_note, now_iso(), item_id),
  247. )
  248. return get_item(table, item_id)
  249. def update_batch(table: str, payload: BatchStatusUpdate) -> dict[str, Any]:
  250. if not payload.ids:
  251. return {"updated": 0}
  252. placeholders = ",".join("?" for _ in payload.ids)
  253. with get_db() as conn:
  254. cursor = conn.execute(
  255. f"""
  256. UPDATE {table}
  257. SET confirm_status = ?, user_note = COALESCE(?, user_note), updated_at = ?
  258. WHERE id IN ({placeholders})
  259. """,
  260. [payload.confirm_status, payload.user_note, now_iso(), *payload.ids],
  261. )
  262. return {"updated": cursor.rowcount}
  263. def rows_for_prompt(table: str, item_type: str, payload: PromptRequest) -> list[dict[str, Any]]:
  264. with get_db() as conn:
  265. if payload.scope == "selected" and payload.ids:
  266. placeholders = ",".join("?" for _ in payload.ids)
  267. rows = conn.execute(f"SELECT * FROM {table} WHERE id IN ({placeholders})", payload.ids).fetchall()
  268. else:
  269. rows = conn.execute(f"SELECT * FROM {table} WHERE confirm_status = 'PENDING'").fetchall()
  270. rows = attach_item_metadata(conn, item_type, rows)
  271. return [normalize_prompt_row(item_type, row) for row in rows]
  272. def normalize_prompt_row(item_type: str, row: dict[str, Any]) -> dict[str, Any]:
  273. if item_type == "service":
  274. return {
  275. "type": "service",
  276. "id": row["id"],
  277. "name": row["name"],
  278. "display_name": row["display_name"],
  279. "status": row["status"],
  280. "start_type": row["start_type"],
  281. "username": row["username"],
  282. "binary_path": row["binary_path"],
  283. "description": row["description"],
  284. "is_present_now": bool(row["is_present_now"]),
  285. "tags": [
  286. {
  287. "name": tag["name"],
  288. "description": tag["description"],
  289. "is_controllable": tag["is_controllable"],
  290. }
  291. for tag in row.get("tags", [])
  292. ],
  293. }
  294. return {
  295. "type": "process",
  296. "id": row["id"],
  297. "name": row["name"],
  298. "exe_path": row["exe_path"],
  299. "cmdline": row["cmdline"],
  300. "username": row["username"],
  301. "status": row["status"],
  302. "last_pid": row["last_pid"],
  303. "parent_pid": row["parent_pid"],
  304. "is_present_now": bool(row["is_present_now"]),
  305. "tags": [
  306. {
  307. "name": tag["name"],
  308. "description": tag["description"],
  309. "is_controllable": tag["is_controllable"],
  310. }
  311. for tag in row.get("tags", [])
  312. ],
  313. }
  314. def markdown_table(rows: list[dict[str, Any]]) -> str:
  315. headers = ["type", "id", "name", "status", "tags", "path_or_command", "user", "present"]
  316. lines = ["| " + " | ".join(headers) + " |", "| " + " | ".join(["---"] * len(headers)) + " |"]
  317. for row in rows:
  318. path_or_command = row.get("binary_path") or row.get("exe_path") or row.get("cmdline") or ""
  319. values = [
  320. str(row.get("type", "")),
  321. str(row.get("id", "")),
  322. str(row.get("name", "")),
  323. str(row.get("status", "")),
  324. ", ".join(tag.get("name", "") for tag in row.get("tags", [])).replace("|", "\\|"),
  325. str(path_or_command).replace("|", "\\|"),
  326. str(row.get("username", "")).replace("|", "\\|"),
  327. "yes" if row.get("is_present_now") else "no",
  328. ]
  329. lines.append("| " + " | ".join(values) + " |")
  330. return "\n".join(lines)
  331. def prompt_response(rows: list[dict[str, Any]]) -> dict[str, Any]:
  332. pending_json = json.dumps(rows, ensure_ascii=False, indent=2)
  333. table = markdown_table(rows)
  334. with get_db() as conn:
  335. tags_json = json.dumps(all_tags(conn), ensure_ascii=False, indent=2)
  336. prompt_text = AI_PROMPT_TEMPLATE.replace("{pending_items_json}", pending_json).replace("{tags_json}", tags_json)
  337. return {"prompt_text": prompt_text, "markdown_table": table, "items": rows}
  338. def set_item_tags(item_type: str, table: str, item_id: int, payload: TagAssignRequest) -> dict[str, Any]:
  339. get_item(table, item_id)
  340. unique_ids = sorted(set(payload.tag_ids))
  341. now = now_iso()
  342. with get_db() as conn:
  343. if unique_ids:
  344. placeholders = ",".join("?" for _ in unique_ids)
  345. found = conn.execute(f"SELECT id FROM tags WHERE id IN ({placeholders})", unique_ids).fetchall()
  346. if len(found) != len(unique_ids):
  347. raise HTTPException(status_code=400, detail="One or more tag ids do not exist")
  348. conn.execute("DELETE FROM item_tags WHERE item_type = ? AND item_id = ?", (item_type, item_id))
  349. for tag_id in unique_ids:
  350. conn.execute(
  351. "INSERT INTO item_tags (item_type, item_id, tag_id, created_at) VALUES (?, ?, ?, ?)",
  352. (item_type, item_id, tag_id, now),
  353. )
  354. return get_item(table, item_id)
  355. def ensure_control_allowed(table: str, item_id: int) -> dict[str, Any]:
  356. item = get_item(table, item_id)
  357. if not item.get("can_control"):
  358. raise HTTPException(status_code=403, detail="This item is not controllable because it is unconfirmed or has a non-controllable tag")
  359. return item
  360. def normalize_import_tag_names(tag_names: list[str] | None) -> list[str]:
  361. if tag_names is None:
  362. return []
  363. normalized = []
  364. seen = set()
  365. for tag_name in tag_names:
  366. name = str(tag_name).strip()[:80]
  367. if not name or name in seen:
  368. continue
  369. seen.add(name)
  370. normalized.append(name)
  371. return normalized
  372. def ensure_tag_ids(conn, tag_names: list[str]) -> list[int]:
  373. tag_ids = []
  374. now = now_iso()
  375. for tag_name in tag_names:
  376. row = conn.execute("SELECT id FROM tags WHERE name = ?", (tag_name,)).fetchone()
  377. if row:
  378. tag_ids.append(row["id"])
  379. continue
  380. cursor = conn.execute(
  381. """
  382. INSERT INTO tags (name, description, is_controllable, is_builtin, created_at, updated_at)
  383. VALUES (?, ?, 1, 0, ?, ?)
  384. """,
  385. (tag_name, "AI 自动新增标签", now, now),
  386. )
  387. tag_ids.append(cursor.lastrowid)
  388. return tag_ids
  389. def replace_item_tags(conn, item_type: str, item_id: int, tag_ids: list[int]) -> None:
  390. now = now_iso()
  391. conn.execute("DELETE FROM item_tags WHERE item_type = ? AND item_id = ?", (item_type, item_id))
  392. for tag_id in tag_ids:
  393. conn.execute(
  394. "INSERT INTO item_tags (item_type, item_id, tag_id, created_at) VALUES (?, ?, ?, ?)",
  395. (item_type, item_id, tag_id, now),
  396. )
  397. def import_ai_results(table: str, item_type: str, payload: AiImportRequest) -> dict[str, Any]:
  398. updated = 0
  399. with get_db() as conn:
  400. for item in payload.items:
  401. if item.type != item_type:
  402. continue
  403. matched_rows = conn.execute(f"SELECT id FROM {table} WHERE name = ?", (item.name,)).fetchall()
  404. tag_ids = ensure_tag_ids(conn, normalize_import_tag_names(item.tags)) if item.tags is not None else None
  405. for row in matched_rows:
  406. cursor = conn.execute(
  407. f"""
  408. UPDATE {table}
  409. SET confirm_status = ?, ai_description = ?, ai_reason = ?,
  410. ai_suggestion = ?, risk_level = ?, updated_at = ?
  411. WHERE id = ?
  412. """,
  413. (
  414. item.judgement,
  415. item.description,
  416. item.reason,
  417. item.suggestion,
  418. item.risk_level,
  419. now_iso(),
  420. row["id"],
  421. ),
  422. )
  423. if tag_ids is not None:
  424. replace_item_tags(conn, item_type, row["id"], tag_ids)
  425. updated += cursor.rowcount
  426. return {"updated": updated}
  427. def ai_update_preview(table: str, item_type: str, proposed_items: list[dict[str, Any]]) -> list[dict[str, Any]]:
  428. names = [item["name"] for item in proposed_items if item.get("type") == item_type and item.get("name")]
  429. if not names:
  430. return []
  431. placeholders = ",".join("?" for _ in names)
  432. with get_db() as conn:
  433. rows = conn.execute(
  434. f"""
  435. SELECT id, name, confirm_status, ai_description, ai_reason, ai_suggestion, risk_level
  436. FROM {table}
  437. WHERE name IN ({placeholders})
  438. """,
  439. names,
  440. ).fetchall()
  441. tag_map = tags_for_items(conn, item_type, [row["id"] for row in rows])
  442. row_map = {}
  443. for row in rows:
  444. current = dict(row)
  445. current["tags"] = [tag["name"] for tag in tag_map.get(row["id"], [])]
  446. row_map[row["name"]] = current
  447. preview = []
  448. for item in proposed_items:
  449. if item.get("type") != item_type:
  450. continue
  451. current = row_map.get(item.get("name"))
  452. preview.append(
  453. {
  454. "matched": current is not None,
  455. "current": current,
  456. "proposed": item,
  457. }
  458. )
  459. return preview
  460. def analyze_items_with_ai(table: str, item_type: str, payload: AiAnalyzeRequest) -> dict[str, Any]:
  461. rows = rows_for_prompt(table, item_type, PromptRequest(scope=payload.scope, ids=payload.ids))
  462. if not rows:
  463. raise HTTPException(status_code=400, detail="No items available for AI analysis")
  464. prompt_data = prompt_response(rows)
  465. result = ai_service.chat(payload.provider_id, payload.model_id, prompt_data["prompt_text"], payload.temperature)
  466. try:
  467. parsed_items = ai_service.parse_ai_items(result["content"])
  468. except (json.JSONDecodeError, ValueError) as exc:
  469. raise HTTPException(
  470. status_code=502,
  471. detail=f"AI output is not valid import JSON: {exc}",
  472. ) from exc
  473. return {
  474. "items": parsed_items,
  475. "preview": ai_update_preview(table, item_type, parsed_items),
  476. "raw_output": result["content"],
  477. "provider": result["provider"],
  478. "model": result["model"],
  479. "prompt_text": prompt_data["prompt_text"],
  480. "markdown_table": prompt_data["markdown_table"],
  481. }
  482. @app.get("/api/dashboard")
  483. def dashboard() -> dict[str, Any]:
  484. with get_db() as conn:
  485. latest_scan = conn.execute("SELECT * FROM scan_records ORDER BY started_at DESC LIMIT 1").fetchone()
  486. service_total = conn.execute("SELECT COUNT(*) AS total FROM windows_services").fetchone()["total"]
  487. process_total = conn.execute("SELECT COUNT(*) AS total FROM windows_processes").fetchone()["total"]
  488. pending_services = conn.execute(
  489. "SELECT COUNT(*) AS total FROM windows_services WHERE confirm_status = 'PENDING'"
  490. ).fetchone()["total"]
  491. pending_processes = conn.execute(
  492. "SELECT COUNT(*) AS total FROM windows_processes WHERE confirm_status = 'PENDING'"
  493. ).fetchone()["total"]
  494. missing_services = conn.execute(
  495. "SELECT COUNT(*) AS total FROM windows_services WHERE is_present_now = 0"
  496. ).fetchone()["total"]
  497. missing_processes = conn.execute(
  498. "SELECT COUNT(*) AS total FROM windows_processes WHERE is_present_now = 0"
  499. ).fetchone()["total"]
  500. return {
  501. "latest_scan": latest_scan,
  502. "service_total": service_total,
  503. "process_total": process_total,
  504. "pending_services": pending_services,
  505. "pending_processes": pending_processes,
  506. "missing_services": missing_services,
  507. "missing_processes": missing_processes,
  508. }
  509. @app.get("/api/tags")
  510. def tags() -> dict[str, Any]:
  511. with get_db() as conn:
  512. rows = all_tags(conn)
  513. return {"items": rows}
  514. @app.post("/api/tags")
  515. def tag_create(payload: TagCreate) -> dict[str, Any]:
  516. now = now_iso()
  517. try:
  518. with get_db() as conn:
  519. cursor = conn.execute(
  520. """
  521. INSERT INTO tags (name, description, is_controllable, is_builtin, created_at, updated_at)
  522. VALUES (?, ?, ?, 0, ?, ?)
  523. """,
  524. (payload.name.strip(), payload.description, 1 if payload.is_controllable else 0, now, now),
  525. )
  526. tag_id = cursor.lastrowid
  527. row = conn.execute("SELECT * FROM tags WHERE id = ?", (tag_id,)).fetchone()
  528. except sqlite3.IntegrityError as exc:
  529. raise HTTPException(status_code=409, detail="Tag name already exists") from exc
  530. return bool_tag(row)
  531. @app.patch("/api/tags/{tag_id}")
  532. def tag_update(tag_id: int, payload: TagUpdate) -> dict[str, Any]:
  533. now = now_iso()
  534. try:
  535. with get_db() as conn:
  536. existing = conn.execute("SELECT * FROM tags WHERE id = ?", (tag_id,)).fetchone()
  537. if not existing:
  538. raise HTTPException(status_code=404, detail="Tag not found")
  539. conn.execute(
  540. """
  541. UPDATE tags
  542. SET name = ?, description = ?, is_controllable = ?, updated_at = ?
  543. WHERE id = ?
  544. """,
  545. (payload.name.strip(), payload.description, 1 if payload.is_controllable else 0, now, tag_id),
  546. )
  547. row = conn.execute("SELECT * FROM tags WHERE id = ?", (tag_id,)).fetchone()
  548. except sqlite3.IntegrityError as exc:
  549. raise HTTPException(status_code=409, detail="Tag name already exists") from exc
  550. return bool_tag(row)
  551. @app.delete("/api/tags/{tag_id}")
  552. def tag_delete(tag_id: int) -> dict[str, Any]:
  553. with get_db() as conn:
  554. row = conn.execute("SELECT * FROM tags WHERE id = ?", (tag_id,)).fetchone()
  555. if not row:
  556. raise HTTPException(status_code=404, detail="Tag not found")
  557. if row["is_builtin"]:
  558. raise HTTPException(status_code=400, detail="Built-in tags cannot be deleted")
  559. cursor = conn.execute("DELETE FROM tags WHERE id = ?", (tag_id,))
  560. return {"deleted": cursor.rowcount}
  561. @app.get("/api/ai/providers")
  562. def ai_providers() -> dict[str, Any]:
  563. return {"items": ai_service.list_providers()}
  564. @app.post("/api/ai/providers")
  565. def ai_provider_create(payload: AiProviderCreate) -> dict[str, Any]:
  566. return ai_service.create_provider(payload)
  567. @app.patch("/api/ai/providers/{provider_id}")
  568. def ai_provider_update(provider_id: int, payload: AiProviderUpdate) -> dict[str, Any]:
  569. return ai_service.update_provider(provider_id, payload)
  570. @app.delete("/api/ai/providers/{provider_id}")
  571. def ai_provider_delete(provider_id: int) -> dict[str, Any]:
  572. return ai_service.delete_provider(provider_id)
  573. @app.get("/api/ai/models")
  574. def ai_models(provider_id: int | None = None) -> dict[str, Any]:
  575. return {"items": ai_service.list_models(provider_id)}
  576. @app.post("/api/ai/models")
  577. def ai_model_create(payload: AiModelCreate) -> dict[str, Any]:
  578. return ai_service.create_model(payload)
  579. @app.patch("/api/ai/models/{model_id}")
  580. def ai_model_update(model_id: int, payload: AiModelUpdate) -> dict[str, Any]:
  581. return ai_service.update_model(model_id, payload)
  582. @app.delete("/api/ai/models/{model_id}")
  583. def ai_model_delete(model_id: int) -> dict[str, Any]:
  584. return ai_service.delete_model(model_id)
  585. @app.post("/api/ai/test")
  586. def ai_test(payload: AiChatRequest) -> dict[str, Any]:
  587. return ai_service.chat(payload.provider_id, payload.model_id, payload.prompt, payload.temperature)
  588. @app.get("/api/settings")
  589. def system_settings() -> dict[str, Any]:
  590. return settings_service.list_settings()
  591. @app.put("/api/settings")
  592. def system_settings_update(payload: SystemSettingsUpdate) -> dict[str, Any]:
  593. return settings_service.update_settings(payload)
  594. @app.post("/api/automation/power/shutdown")
  595. def automation_shutdown(payload: AutomationPowerRequest) -> dict[str, Any]:
  596. return windows_automation.shutdown_windows(payload.delay_seconds, payload.force, payload.reason)
  597. @app.post("/api/automation/power/restart")
  598. def automation_restart(payload: AutomationPowerRequest) -> dict[str, Any]:
  599. return windows_automation.restart_windows(payload.delay_seconds, payload.force, payload.reason)
  600. @app.post("/api/automation/power/cancel")
  601. def automation_power_cancel() -> dict[str, Any]:
  602. return windows_automation.cancel_power_action()
  603. @app.post("/api/automation/programs/start")
  604. def automation_program_start(payload: AutomationProgramStartRequest) -> dict[str, Any]:
  605. return windows_automation.start_program(payload.command, payload.cwd, payload.shell)
  606. @app.post("/api/automation/programs/stop")
  607. def automation_program_stop(payload: AutomationProgramStopRequest) -> dict[str, Any]:
  608. return windows_automation.stop_program(
  609. pid=payload.pid,
  610. name=payload.name,
  611. timeout_seconds=payload.timeout_seconds,
  612. kill_after_timeout=payload.kill_after_timeout,
  613. )
  614. @app.post("/api/automation/screenshot")
  615. def automation_screenshot(payload: AutomationScreenshotRequest) -> dict[str, Any]:
  616. return windows_automation.take_screenshot(payload.save_path, payload.include_base64)
  617. @app.post("/api/automation/mouse")
  618. def automation_mouse(payload: AutomationMouseRequest) -> dict[str, Any]:
  619. return windows_automation.mouse_action(
  620. action=payload.action,
  621. x=payload.x,
  622. y=payload.y,
  623. duration=payload.duration,
  624. button=payload.button,
  625. clicks=payload.clicks,
  626. amount=payload.amount,
  627. )
  628. @app.post("/api/automation/keyboard")
  629. def automation_keyboard(payload: AutomationKeyboardRequest) -> dict[str, Any]:
  630. return windows_automation.keyboard_action(
  631. action=payload.action,
  632. key=payload.key,
  633. keys=payload.keys,
  634. text=payload.text,
  635. interval=payload.interval,
  636. )
  637. @app.post("/api/automation/vision/analyze")
  638. def automation_vision_analyze(payload: AutomationVisionAnalyzeRequest) -> dict[str, Any]:
  639. return automation_service.analyze_screen(payload)
  640. @app.post("/api/automation/vision/screenshot")
  641. def automation_vision_screenshot(payload: AutomationScreenshotCaptureRequest) -> dict[str, Any]:
  642. return automation_service.capture_screenshot(payload)
  643. @app.post("/api/automation/screens/{screen_id}/elements/{element_id}/locate")
  644. def automation_element_locate(screen_id: int, element_id: int, payload: AutomationElementLocateRequest) -> dict[str, Any]:
  645. return automation_service.locate_element(screen_id, element_id, payload)
  646. @app.post("/api/automation/actions/mouse")
  647. def automation_action_mouse(payload: AutomationMouseActionRequest) -> dict[str, Any]:
  648. return automation_service.execute_mouse_action(payload)
  649. @app.post("/api/automation/actions/keyboard")
  650. def automation_action_keyboard(payload: AutomationKeyboardActionRequest) -> dict[str, Any]:
  651. return automation_service.execute_keyboard_action(payload)
  652. @app.post("/api/automation/actions/text-input")
  653. def automation_action_text_input(payload: AutomationTextInputRequest) -> dict[str, Any]:
  654. return automation_service.execute_text_input(payload)
  655. @app.post("/api/automation/actions/start-program")
  656. def automation_action_start_program(payload: AutomationStartProgramRequest) -> dict[str, Any]:
  657. return automation_service.execute_start_program(payload)
  658. @app.post("/api/automation/actions/close-opened-programs")
  659. def automation_action_close_opened_programs(payload: AutomationCloseProgramsRequest) -> dict[str, Any]:
  660. return automation_service.close_opened_programs(payload.pids)
  661. @app.get("/api/automation/workflows")
  662. def automation_workflows(page: int = Query(default=1, ge=1), page_size: int = Query(default=20, ge=1, le=200)) -> dict[str, Any]:
  663. return automation_service.list_workflows(page, page_size)
  664. @app.post("/api/automation/workflows")
  665. def automation_workflow_create(payload: AutomationWorkflowSaveRequest) -> dict[str, Any]:
  666. return automation_service.save_workflow(payload)
  667. @app.get("/api/automation/workflows/{workflow_id}")
  668. def automation_workflow_detail(workflow_id: int) -> dict[str, Any]:
  669. return automation_service.get_workflow(workflow_id)
  670. @app.post("/api/automation/workflows/{workflow_id}/run")
  671. def automation_workflow_run(workflow_id: int, payload: AutomationWorkflowRunRequest) -> dict[str, Any]:
  672. return automation_service.run_workflow(workflow_id, payload)
  673. @app.put("/api/automation/workflows/{workflow_id}")
  674. def automation_workflow_update(workflow_id: int, payload: AutomationWorkflowSaveRequest) -> dict[str, Any]:
  675. return automation_service.update_workflow(workflow_id, payload)
  676. @app.delete("/api/automation/workflows/{workflow_id}")
  677. def automation_workflow_delete(workflow_id: int) -> dict[str, Any]:
  678. return automation_service.delete_workflow(workflow_id)
  679. @app.get("/api/automation/screens")
  680. def automation_screens(page: int = Query(default=1, ge=1), page_size: int = Query(default=20, ge=1, le=200)) -> dict[str, Any]:
  681. return automation_service.list_screens(page, page_size)
  682. @app.get("/api/automation/screens/{screen_id}")
  683. def automation_screen_detail(screen_id: int, include_image: bool = False) -> dict[str, Any]:
  684. return automation_service.get_screen(screen_id, include_image)
  685. @app.delete("/api/automation/screens/{screen_id}")
  686. def automation_screen_delete(screen_id: int) -> dict[str, Any]:
  687. return automation_service.delete_screen(screen_id)
  688. @app.get("/api/automation/errors")
  689. def automation_errors(page: int = Query(default=1, ge=1), page_size: int = Query(default=20, ge=1, le=200)) -> dict[str, Any]:
  690. return automation_service.list_errors(page, page_size)
  691. @app.get("/api/automation/errors/{error_id}")
  692. def automation_error_detail(error_id: int, include_images: bool = False) -> dict[str, Any]:
  693. return automation_service.get_error(error_id, include_images)
  694. @app.get("/api/sensors")
  695. def sensors() -> dict[str, Any]:
  696. return collect_sensors()
  697. @app.get("/api/smart/scan")
  698. def smart_scan() -> dict[str, Any]:
  699. return scan_devices()
  700. @app.get("/api/smart/devices")
  701. def smart_devices(include_jmb39x: bool = True, jmb39x_slots: int = Query(default=8, ge=0, le=16)) -> dict[str, Any]:
  702. return collect_all_smart(include_jmb39x=include_jmb39x, jmb39x_slots=jmb39x_slots)
  703. @app.get("/api/smart/device")
  704. def smart_device(device: str, device_type: str | None = None) -> dict[str, Any]:
  705. return get_device_smart(device, device_type)
  706. @app.post("/api/scans/run")
  707. def run_scan() -> dict[str, Any]:
  708. return run_full_scan()
  709. @app.get("/api/scans")
  710. def scan_history(page: int = 1, page_size: int = 20) -> dict[str, Any]:
  711. offset = (page - 1) * page_size
  712. with get_db() as conn:
  713. total = conn.execute("SELECT COUNT(*) AS total FROM scan_records").fetchone()["total"]
  714. rows = conn.execute(
  715. "SELECT * FROM scan_records ORDER BY started_at DESC LIMIT ? OFFSET ?",
  716. (page_size, offset),
  717. ).fetchall()
  718. return {"items": rows, "total": total, "page": page, "page_size": page_size}
  719. @app.get("/api/scans/{scan_id}")
  720. def scan_detail(scan_id: int) -> dict[str, Any]:
  721. with get_db() as conn:
  722. scan = conn.execute("SELECT * FROM scan_records WHERE id = ?", (scan_id,)).fetchone()
  723. if not scan:
  724. raise HTTPException(status_code=404, detail="Scan not found")
  725. return scan
  726. @app.get("/api/services")
  727. def services(
  728. keyword: str | None = None,
  729. confirm_status: str | None = None,
  730. present: bool | None = None,
  731. sort_by: str | None = None,
  732. sort_order: str | None = Query(default=None, pattern="^(asc|desc)$"),
  733. page: int = Query(default=1, ge=1),
  734. page_size: int = Query(default=20, ge=1, le=200),
  735. ) -> dict[str, Any]:
  736. return list_items(
  737. "windows_services",
  738. keyword,
  739. confirm_status,
  740. present,
  741. page,
  742. page_size,
  743. ["name", "display_name", "binary_path", "description"],
  744. sort_by,
  745. sort_order,
  746. )
  747. @app.patch("/api/services/batch")
  748. def service_batch_update(payload: BatchStatusUpdate) -> dict[str, Any]:
  749. return update_batch("windows_services", payload)
  750. @app.post("/api/services/import-ai")
  751. def service_import_ai(payload: AiImportRequest) -> dict[str, Any]:
  752. return import_ai_results("windows_services", "service", payload)
  753. @app.post("/api/services/analyze-ai")
  754. def service_analyze_ai(payload: AiAnalyzeRequest) -> dict[str, Any]:
  755. return analyze_items_with_ai("windows_services", "service", payload)
  756. @app.post("/api/services/ai-prompt")
  757. def service_ai_prompt(payload: PromptRequest) -> dict[str, Any]:
  758. return prompt_response(rows_for_prompt("windows_services", "service", payload))
  759. @app.put("/api/services/{service_id}/tags")
  760. def service_tags_update(service_id: int, payload: TagAssignRequest) -> dict[str, Any]:
  761. return set_item_tags("service", "windows_services", service_id, payload)
  762. @app.post("/api/services/{service_id}/start")
  763. def service_start(service_id: int) -> dict[str, Any]:
  764. item = ensure_control_allowed("windows_services", service_id)
  765. if not item.get("is_present_now"):
  766. raise HTTPException(status_code=400, detail="This service was not present in the latest scan")
  767. result = start_service(item["name"])
  768. with get_db() as conn:
  769. conn.execute(
  770. "UPDATE windows_services SET status = ?, updated_at = ? WHERE id = ?",
  771. (result.get("status") or "running", now_iso(), service_id),
  772. )
  773. return result
  774. @app.post("/api/services/{service_id}/stop")
  775. def service_stop(service_id: int) -> dict[str, Any]:
  776. item = ensure_control_allowed("windows_services", service_id)
  777. if not item.get("is_present_now"):
  778. raise HTTPException(status_code=400, detail="This service was not present in the latest scan")
  779. result = stop_service(item["name"])
  780. with get_db() as conn:
  781. conn.execute(
  782. "UPDATE windows_services SET status = ?, updated_at = ? WHERE id = ?",
  783. (result.get("status") or "stopped", now_iso(), service_id),
  784. )
  785. return result
  786. @app.post("/api/services/{service_id}/restart")
  787. def service_restart(service_id: int) -> dict[str, Any]:
  788. item = ensure_control_allowed("windows_services", service_id)
  789. if not item.get("is_present_now"):
  790. raise HTTPException(status_code=400, detail="This service was not present in the latest scan")
  791. result = restart_service(item["name"])
  792. with get_db() as conn:
  793. conn.execute(
  794. "UPDATE windows_services SET status = ?, updated_at = ? WHERE id = ?",
  795. (result.get("status") or "running", now_iso(), service_id),
  796. )
  797. return result
  798. @app.get("/api/services/{service_id}")
  799. def service_detail(service_id: int) -> dict[str, Any]:
  800. return get_item("windows_services", service_id)
  801. @app.patch("/api/services/{service_id}")
  802. def service_update(service_id: int, payload: StatusUpdate) -> dict[str, Any]:
  803. return update_one("windows_services", service_id, payload)
  804. @app.get("/api/processes")
  805. def processes(
  806. keyword: str | None = None,
  807. confirm_status: str | None = None,
  808. present: bool | None = None,
  809. sort_by: str | None = None,
  810. sort_order: str | None = Query(default=None, pattern="^(asc|desc)$"),
  811. page: int = Query(default=1, ge=1),
  812. page_size: int = Query(default=20, ge=1, le=200),
  813. ) -> dict[str, Any]:
  814. return list_items(
  815. "windows_processes",
  816. keyword,
  817. confirm_status,
  818. present,
  819. page,
  820. page_size,
  821. ["name", "exe_path", "cmdline", "username"],
  822. sort_by,
  823. sort_order,
  824. )
  825. @app.patch("/api/processes/batch")
  826. def process_batch_update(payload: BatchStatusUpdate) -> dict[str, Any]:
  827. return update_batch("windows_processes", payload)
  828. @app.post("/api/processes/import-ai")
  829. def process_import_ai(payload: AiImportRequest) -> dict[str, Any]:
  830. return import_ai_results("windows_processes", "process", payload)
  831. @app.post("/api/processes/analyze-ai")
  832. def process_analyze_ai(payload: AiAnalyzeRequest) -> dict[str, Any]:
  833. return analyze_items_with_ai("windows_processes", "process", payload)
  834. @app.post("/api/processes/ai-prompt")
  835. def process_ai_prompt(payload: PromptRequest) -> dict[str, Any]:
  836. return prompt_response(rows_for_prompt("windows_processes", "process", payload))
  837. @app.put("/api/processes/{process_id}/tags")
  838. def process_tags_update(process_id: int, payload: TagAssignRequest) -> dict[str, Any]:
  839. return set_item_tags("process", "windows_processes", process_id, payload)
  840. @app.post("/api/processes/{process_id}/start")
  841. def process_start(process_id: int) -> dict[str, Any]:
  842. item = ensure_control_allowed("windows_processes", process_id)
  843. result = start_process(item)
  844. with get_db() as conn:
  845. conn.execute(
  846. "UPDATE windows_processes SET last_pid = ?, is_present_now = 1, status = ?, updated_at = ? WHERE id = ?",
  847. (result.get("pid"), "running", now_iso(), process_id),
  848. )
  849. return result
  850. @app.post("/api/processes/{process_id}/stop")
  851. def process_stop(process_id: int) -> dict[str, Any]:
  852. item = ensure_control_allowed("windows_processes", process_id)
  853. if not item.get("is_present_now"):
  854. raise HTTPException(status_code=400, detail="This process was not present in the latest scan")
  855. result = stop_process(item)
  856. with get_db() as conn:
  857. conn.execute(
  858. "UPDATE windows_processes SET is_present_now = 0, status = ?, updated_at = ? WHERE id = ?",
  859. ("stopped", now_iso(), process_id),
  860. )
  861. return result
  862. @app.get("/api/processes/{process_id}")
  863. def process_detail(process_id: int) -> dict[str, Any]:
  864. return get_item("windows_processes", process_id)
  865. @app.patch("/api/processes/{process_id}")
  866. def process_update(process_id: int, payload: StatusUpdate) -> dict[str, Any]:
  867. return update_one("windows_processes", process_id, payload)