main.py 40 KB

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