c949204662
借鉴 odysseus 的能力设计,全程净室实现、零 AGPL 代码、不引入 AGPL 依赖。
T1 提示注入防护: pkg/promptguard 包裹外部/知识库内容为不可信数据,buildMessages 移出 system 指令区。 T2 安全 CI: .github/workflows(ci+security: govulncheck/gitleaks/actionlint/hadolint/trivy)+dependabot+.hadolint.yaml;go.mod 加 toolchain go1.25.11 修复 20 个 stdlib CVE。 T3 管理员 2FA: 迁移 000016 + RFC6238 TOTP/备份码(pkg/auth, 零依赖) + 登录流程集成(后端)。 T4 本地模型: LLM/embedding 支持本地 vLLM/Ollama(OpenAI 兼容, 鉴权头条件发送, NoAuth) + docs/local-deploy.md。 T6 深度研究: 迁移 000017 + Python research-worker(净室多步流水线, 检索避开 SearXNG) + Go research 服务/handler/路由。 T7 service 层: 新增 internal/service/{research,twofa}, 2FA 业务逻辑从胖 handler 下沉, 接口注入可单测。 T10 缓存/可观测性: internal/cache(Redis+内存, 优雅降级) 接入 store 热点列表; Prometheus 指标+/metrics; docs/openapi.yaml。 验证: go build/vet/test ./... 全绿(8 包); research-worker 12 单测过; 真实 PG 应用迁移并烟测。
77 lines
2.4 KiB
Python
77 lines
2.4 KiB
Python
"""research_tasks 数据库操作(psycopg3)。"""
|
|
|
|
import json
|
|
|
|
import psycopg
|
|
|
|
from config import config
|
|
|
|
|
|
def get_connection():
|
|
return psycopg.connect(config.DATABASE_URL)
|
|
|
|
|
|
def get_task(task_id: str) -> dict | None:
|
|
with get_connection() as conn:
|
|
with conn.cursor() as cur:
|
|
cur.execute(
|
|
"SELECT id, user_id, app_id, topic, config, status, progress, "
|
|
"status_message, error_message, report, sources, tokens_used, created_at "
|
|
"FROM research_tasks WHERE id = %s",
|
|
(task_id,),
|
|
)
|
|
row = cur.fetchone()
|
|
if not row:
|
|
return None
|
|
cols = [d[0] for d in cur.description]
|
|
return dict(zip(cols, row))
|
|
|
|
|
|
def update_task_status(
|
|
task_id: str,
|
|
status: str,
|
|
progress: int = None,
|
|
status_message: str = None,
|
|
error_message: str = None,
|
|
report: str = None,
|
|
sources: list = None,
|
|
tokens_used: int = None,
|
|
):
|
|
fields = ["status = %(status)s", "updated_at = NOW()"]
|
|
params = {"task_id": task_id, "status": status}
|
|
|
|
if progress is not None:
|
|
fields.append("progress = %(progress)s")
|
|
params["progress"] = progress
|
|
if status_message is not None:
|
|
fields.append("status_message = %(status_message)s")
|
|
params["status_message"] = status_message
|
|
if error_message is not None:
|
|
fields.append("error_message = %(error_message)s")
|
|
params["error_message"] = error_message
|
|
if report is not None:
|
|
fields.append("report = %(report)s")
|
|
params["report"] = report
|
|
if sources is not None:
|
|
fields.append("sources = %(sources)s")
|
|
params["sources"] = json.dumps(sources, ensure_ascii=False)
|
|
if tokens_used is not None:
|
|
fields.append("tokens_used = %(tokens_used)s")
|
|
params["tokens_used"] = tokens_used
|
|
|
|
if status == "planning":
|
|
fields.append("started_at = COALESCE(started_at, NOW())")
|
|
elif status in ("completed", "failed", "canceled"):
|
|
fields.append("completed_at = NOW()")
|
|
|
|
sql = f"UPDATE research_tasks SET {', '.join(fields)} WHERE id = %(task_id)s"
|
|
with get_connection() as conn:
|
|
with conn.cursor() as cur:
|
|
cur.execute(sql, params)
|
|
conn.commit()
|
|
|
|
|
|
def is_canceled(task_id: str) -> bool:
|
|
task = get_task(task_id)
|
|
return bool(task and task.get("status") == "canceled")
|