business_store.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. """业务身份、任务和推送状态;复用应用 SQLite 事务连接。"""
  2. import hashlib
  3. import json
  4. from db import db_conn
  5. def ensure_state(name):
  6. with db_conn.transaction():
  7. if not db_conn.query_one("SELECT name FROM business_agent_state WHERE name = ?", (name,)):
  8. db_conn.execute("INSERT INTO business_agent_state (name, local_enabled, gateway_online, updated_at) VALUES (?, 1, 1, ?)",
  9. (name, db_conn.fmt_dt()))
  10. def state(name):
  11. ensure_state(name)
  12. return db_conn.query_one("SELECT * FROM business_agent_state WHERE name = ?", (name,))
  13. def set_state(name, *, local_enabled=None, gateway_online=None):
  14. ensure_state(name)
  15. for key, value in (("local_enabled", local_enabled), ("gateway_online", gateway_online)):
  16. if value is not None:
  17. db_conn.execute(f"UPDATE business_agent_state SET {key} = ?, updated_at = ? WHERE name = ?",
  18. (int(bool(value)), db_conn.fmt_dt(), name))
  19. def session_agent(session_id):
  20. row = db_conn.query_one("SELECT agent_name FROM business_session_agents WHERE session_id = ?", (session_id,))
  21. return row["agent_name"] if row else None
  22. def bind_session(session_id, name):
  23. with db_conn.transaction():
  24. existing = session_agent(session_id)
  25. # 旧会话没有绑定表记录时,已有对话仍归属主 Agent。
  26. if existing is None and db_conn.scalar("SELECT COUNT(*) FROM messages WHERE session_id = ?", (session_id,)):
  27. existing = "main"
  28. if existing is not None and existing != name:
  29. raise ValueError("会话已绑定其他智能体,请新建会话")
  30. if session_agent(session_id) is None:
  31. db_conn.execute("INSERT INTO business_session_agents (session_id, agent_name) VALUES (?, ?)", (session_id, name))
  32. def _json(value):
  33. return json.dumps(value, ensure_ascii=False, sort_keys=True, allow_nan=False)
  34. def enqueue(name, user_name, data, files, *, request_id=None, source="manual", publish=False):
  35. payload = _json({"input": data, "files": files, "publish": publish})
  36. job_id = (hashlib.sha256(f"{name}\0{user_name}\0{source}\0{request_id}".encode()).hexdigest()[:32]
  37. if request_id else db_conn.new_id())
  38. with db_conn.transaction():
  39. previous = get(job_id)
  40. if previous:
  41. if previous["payload"] != payload:
  42. raise ValueError("同一 request_id 已用于不同任务输入")
  43. return previous, False
  44. now = db_conn.fmt_dt()
  45. db_conn.execute(
  46. "INSERT INTO business_jobs (id, agent_name, user_name, source, payload, status, push_status, push_attempts, created_at, updated_at)"
  47. " VALUES (?, ?, ?, ?, ?, 'queued', 'none', 0, ?, ?)",
  48. (job_id, name, user_name, source, payload, now, now))
  49. return get(job_id), True
  50. def get(job_id):
  51. return db_conn.query_one("SELECT * FROM business_jobs WHERE id = ?", (job_id,))
  52. def list_jobs(name=None, user_name=None, limit=50):
  53. clauses, values = [], []
  54. for key, value in (("agent_name", name), ("user_name", user_name)):
  55. if value is not None:
  56. clauses.append(f"{key} = ?")
  57. values.append(value)
  58. where = " WHERE " + " AND ".join(clauses) if clauses else ""
  59. return db_conn.query("SELECT * FROM business_jobs" + where + " ORDER BY created_at DESC LIMIT ?", (*values, limit))
  60. def claim(name):
  61. with db_conn.transaction():
  62. row = db_conn.query_one("SELECT * FROM business_jobs WHERE agent_name = ? AND status = 'queued' ORDER BY created_at, id LIMIT 1", (name,))
  63. if not row:
  64. return None
  65. updated = db_conn.execute("UPDATE business_jobs SET status = 'running', updated_at = ? WHERE id = ? AND status = 'queued'",
  66. (db_conn.fmt_dt(), row["id"]))
  67. return get(row["id"]) if updated else None
  68. _FIELDS = {"status", "session_id", "run_id", "error", "result_json", "push_status", "push_attempts",
  69. "next_push_at", "receipt_json", "push_payload", "finished_at", "agent_code"}
  70. def update(job_id, **fields):
  71. if not fields or set(fields) - _FIELDS:
  72. raise ValueError("业务任务更新字段非法")
  73. assignments = ", ".join(f"{key} = ?" for key in fields)
  74. db_conn.execute(f"UPDATE business_jobs SET {assignments}, updated_at = ? WHERE id = ?",
  75. (*fields.values(), db_conn.fmt_dt(), job_id))
  76. def pending_pushes(now):
  77. return db_conn.query("SELECT * FROM business_jobs WHERE push_status = 'pending' AND (next_push_at IS NULL OR next_push_at <= ?) ORDER BY updated_at LIMIT 50", (now,))
  78. def push_target(job_id):
  79. return db_conn.scalar("SELECT endpoint FROM business_push_targets WHERE job_id = ?", (job_id,))
  80. def freeze_push(job_id, payload, endpoint):
  81. """首次排队即冻结报文和目标;重试不会跟随配置切换到另一个平台。"""
  82. if not endpoint:
  83. raise ValueError("结果接收平台地址尚未配置")
  84. content = _json(payload)
  85. with db_conn.transaction():
  86. job = get(job_id)
  87. if not job:
  88. raise ValueError("结果所属任务不存在")
  89. if job.get("push_payload") and _json(json.loads(job["push_payload"])) != content:
  90. raise ValueError("已冻结的推送内容不能改写")
  91. previous = push_target(job_id)
  92. if previous and previous != endpoint:
  93. raise ValueError("结果接收平台地址已改变,不能改投历史结果")
  94. if previous is None:
  95. db_conn.execute("INSERT INTO business_push_targets (job_id, endpoint, created_at) VALUES (?, ?, ?)",
  96. (job_id, endpoint, db_conn.fmt_dt()))
  97. update(job_id, push_payload=content, agent_code=payload["agentCode"])
  98. return payload
  99. def record_completed_result(name, user_name, session_id, data, result, endpoint):
  100. """网页/工具已完成的研判直接进入结果队列,不再次执行智能体。"""
  101. payload = result["platform_payload"]
  102. job_id = payload["taskId"]
  103. with db_conn.transaction():
  104. existing = get(job_id)
  105. if existing:
  106. if (existing["agent_name"], existing["user_name"], existing["session_id"]) != (name, user_name, session_id):
  107. raise ValueError("结果任务身份冲突")
  108. freeze_push(job_id, payload, endpoint)
  109. return existing
  110. now = db_conn.fmt_dt()
  111. db_conn.execute(
  112. "INSERT INTO business_jobs (id, agent_name, user_name, source, payload, status, session_id, result_json,"
  113. " push_status, push_attempts, created_at, updated_at, finished_at)"
  114. " VALUES (?, ?, ?, 'chat', ?, ?, ?, ?, 'pending', 0, ?, ?, ?)",
  115. (job_id, name, user_name, _json({"input": data, "files": [], "publish": True}), result["status"],
  116. session_id, _json(result), now, now, payload["checkedAt"]))
  117. freeze_push(job_id, payload, endpoint)
  118. return get(job_id)
  119. def recover():
  120. # 不自动重跑已经调用模型但尚未保存结果的任务,避免重复计费与副作用。
  121. db_conn.execute("UPDATE business_jobs SET status = 'interrupted', error = 'process_restarted', updated_at = ?, finished_at = ? WHERE status = 'running'",
  122. (db_conn.fmt_dt(), db_conn.fmt_dt()))
  123. def retry(job_id):
  124. return db_conn.execute("UPDATE business_jobs SET status = 'queued', error = NULL, finished_at = NULL, updated_at = ? WHERE id = ? AND status IN ('failed', 'cancelled', 'interrupted')",
  125. (db_conn.fmt_dt(), job_id))
  126. def metrics(name):
  127. rows = db_conn.query("SELECT status, COUNT(*) AS n FROM business_jobs WHERE agent_name = ? GROUP BY status", (name,))
  128. counts = {row["status"]: row["n"] for row in rows}
  129. executed = db_conn.query("SELECT e.status, COUNT(*) AS n FROM execution_runs e JOIN business_session_agents b ON b.session_id = e.session_id WHERE b.agent_name = ? GROUP BY e.status", (name,))
  130. return {"tasks": counts, "completed": counts.get("completed", 0),
  131. "executions": {row["status"]: row["n"] for row in executed},
  132. "pushed": db_conn.scalar("SELECT COUNT(*) FROM business_jobs WHERE agent_name = ? AND push_status = 'pushed'", (name,))}
  133. def record_file(job_id, metadata):
  134. db_conn.execute("INSERT INTO business_files (id, job_id, file_id, version, filename, local_path, sha256, size, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
  135. (db_conn.new_id(), job_id, metadata["file_id"], metadata["version"], metadata["filename"],
  136. metadata["local_path"], metadata["sha256"], metadata["size"], db_conn.fmt_dt()))