Files
ai-worker-platform/db.py
T
hz4th_coder 9c45a143b9 DAG画布:文字清晰 + 无边画布 + 删除区回收站
1. 文字看不清修复:.dag-node hover stroke 继承到文字导致白色小字被描边盖糊 → label/sub 加 stroke:none,字号加大(13/11px)加粗
2. 无边画布:bg rect 扩大至±4000且与画布同色 + svg overflow:visible,大幅平移缩放不再露出边缘/裁剪节点
3. 删除区与回收站:
- 画布左下角🗑️删除区,节点拖入即软删除(deleted=1)
- 点击删除区打开回收站:恢复/彻底删除
- 后端 tasks 加 deleted/deleted_at 列,全查询过滤,trash/restore/hard-delete API,看板删除改软删
- 修复画布重建后事件失效:box监听每次重绘重绑、window监听只绑一次且拖拽状态提升为模块级(dagDrag)
2026-08-12 23:44:20 +08:00

432 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""
数据库层:SQLite + WAL,轻量直连封装
"""
import sqlite3
import json
import time
from config import DB_PATH
SCHEMA = """
CREATE TABLE IF NOT EXISTS projects (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
description TEXT DEFAULT '',
objective TEXT DEFAULT '',
acceptance_criteria TEXT DEFAULT '',
status TEXT DEFAULT 'active', -- planning/active/done/archived
budget_limit REAL DEFAULT 0, -- 项目预算上限(元),0=不限
created_at INTEGER,
updated_at INTEGER
);
CREATE TABLE IF NOT EXISTS workers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
description TEXT DEFAULT '',
provider TEXT NOT NULL,
model TEXT NOT NULL,
base_url TEXT DEFAULT '',
api_key TEXT DEFAULT '', -- 留空则用供应商全局 key
system_prompt TEXT DEFAULT '',
temperature REAL DEFAULT 0.7,
max_tokens INTEGER DEFAULT 2000,
task_cost_limit REAL DEFAULT 0, -- 单任务成本上限(元),0=不限
monthly_cost_limit REAL DEFAULT 0, -- 月度成本上限(元),0=不限
status TEXT DEFAULT 'enabled', -- enabled/disabled
created_at INTEGER,
updated_at INTEGER
);
CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
project_id INTEGER NOT NULL,
worker_id INTEGER, -- NULL = 自动路由
title TEXT NOT NULL,
description TEXT DEFAULT '',
status TEXT DEFAULT 'todo', -- todo/running/review/done/rejected/failed/cancelled
priority TEXT DEFAULT 'medium', -- high/medium/low
review_required INTEGER DEFAULT 1, -- 完成后是否需要人工审核
output_text TEXT DEFAULT '',
output_version INTEGER DEFAULT 0,
rejection_count INTEGER DEFAULT 0,
error TEXT DEFAULT '',
deadline TEXT DEFAULT '',
created_at INTEGER,
updated_at INTEGER,
started_at INTEGER,
finished_at INTEGER
);
CREATE TABLE IF NOT EXISTS task_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task_id INTEGER NOT NULL,
level TEXT DEFAULT 'info', -- info/success/warn/error
message TEXT DEFAULT '',
created_at INTEGER
);
CREATE TABLE IF NOT EXISTS cost_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task_id INTEGER,
project_id INTEGER,
worker_id INTEGER,
provider TEXT DEFAULT '',
model TEXT DEFAULT '',
prompt_tokens INTEGER DEFAULT 0,
completion_tokens INTEGER DEFAULT 0,
total_tokens INTEGER DEFAULT 0,
cost REAL DEFAULT 0,
created_at INTEGER
);
CREATE TABLE IF NOT EXISTS documents (
id INTEGER PRIMARY KEY AUTOINCREMENT,
project_id INTEGER NOT NULL,
name TEXT NOT NULL,
content TEXT DEFAULT '',
source TEXT DEFAULT 'manual', -- manual/file/url
chunk_size INTEGER DEFAULT 0,
created_at INTEGER,
updated_at INTEGER
);
CREATE TABLE IF NOT EXISTS doc_chunks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
document_id INTEGER NOT NULL,
idx INTEGER DEFAULT 0,
content TEXT DEFAULT '',
tokens INTEGER DEFAULT 0
);
CREATE TABLE IF NOT EXISTS alerts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
type TEXT DEFAULT 'system', -- budget/task_failed/worker/limit/notify/plan
level TEXT DEFAULT 'info', -- info/warn/critical
title TEXT DEFAULT '',
detail TEXT DEFAULT '',
read INTEGER DEFAULT 0,
created_at INTEGER
);
CREATE TABLE IF NOT EXISTS api_tokens (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
token TEXT NOT NULL UNIQUE,
created_at INTEGER,
last_used_at INTEGER
);
CREATE TABLE IF NOT EXISTS notify_channels (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
type TEXT NOT NULL, -- feishu/wecom/email
webhook TEXT DEFAULT '',
email TEXT DEFAULT '',
events TEXT DEFAULT '[]', -- JSON: task_review/task_done/task_failed/budget_alert/worker_alert
enabled INTEGER DEFAULT 1,
created_at INTEGER
);
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT DEFAULT ''
);
CREATE INDEX IF NOT EXISTS idx_tasks_project ON tasks(project_id);
-- ===================================================================
-- V2 表结构:多 Agent 协作 / 自动评估 / 模板市场 / 企业版
-- ===================================================================
CREATE TABLE IF NOT EXISTS agent_runs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
mode TEXT NOT NULL, -- supervisor / review / debate
title TEXT DEFAULT '',
topic TEXT DEFAULT '', -- 输入主题 / 任务
context TEXT DEFAULT '', -- 附加上下文(知识库/约束)
worker_ids TEXT DEFAULT '[]', -- JSON: 参与协作的 worker id 列表
params TEXT DEFAULT '{}', -- JSON: rounds/阈值/立场等
status TEXT DEFAULT 'running', -- running/done/failed/cancelled
result TEXT DEFAULT '', -- 最终产出
summary TEXT DEFAULT '', -- 过程摘要(评审意见/共识等)
error TEXT DEFAULT '',
total_tokens INTEGER DEFAULT 0,
cost REAL DEFAULT 0,
created_at INTEGER,
finished_at INTEGER
);
CREATE TABLE IF NOT EXISTS agent_steps (
id INTEGER PRIMARY KEY AUTOINCREMENT,
run_id INTEGER NOT NULL,
role TEXT DEFAULT '', -- supervisor/worker/reviewer/judge/debater
worker_id INTEGER,
seq INTEGER DEFAULT 0,
stage TEXT DEFAULT '', -- plan/delegate/produce/critique/revise/synthesize/verdict
content TEXT DEFAULT '',
tokens INTEGER DEFAULT 0,
cost REAL DEFAULT 0,
created_at INTEGER
);
CREATE TABLE IF NOT EXISTS eval_datasets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
description TEXT DEFAULT '',
rubric TEXT DEFAULT '', -- 评分标准(LLM-as-judge
tags TEXT DEFAULT '[]',
is_builtin INTEGER DEFAULT 0,
created_at INTEGER,
updated_at INTEGER
);
CREATE TABLE IF NOT EXISTS eval_cases (
id INTEGER PRIMARY KEY AUTOINCREMENT,
dataset_id INTEGER NOT NULL,
input TEXT DEFAULT '',
expected TEXT DEFAULT '',
tags TEXT DEFAULT '[]',
created_at INTEGER
);
CREATE TABLE IF NOT EXISTS eval_runs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
dataset_id INTEGER NOT NULL,
worker_id INTEGER NOT NULL,
status TEXT DEFAULT 'running', -- running/done/failed/cancelled
score REAL DEFAULT 0, -- 平均分 0-100
total_tokens INTEGER DEFAULT 0,
cost REAL DEFAULT 0,
cases_total INTEGER DEFAULT 0,
cases_done INTEGER DEFAULT 0,
created_at INTEGER,
finished_at INTEGER
);
CREATE TABLE IF NOT EXISTS eval_results (
id INTEGER PRIMARY KEY AUTOINCREMENT,
run_id INTEGER NOT NULL,
case_id INTEGER NOT NULL,
worker_id INTEGER,
output TEXT DEFAULT '',
score REAL DEFAULT 0,
judgment TEXT DEFAULT '',
latency_ms INTEGER DEFAULT 0,
cost REAL DEFAULT 0,
created_at INTEGER
);
CREATE TABLE IF NOT EXISTS templates (
id INTEGER PRIMARY KEY AUTOINCREMENT,
type TEXT NOT NULL, -- task / project / team
name TEXT NOT NULL,
description TEXT DEFAULT '',
content TEXT DEFAULT '{}', -- JSON
tags TEXT DEFAULT '[]',
author TEXT DEFAULT 'system',
is_builtin INTEGER DEFAULT 0,
usage_count INTEGER DEFAULT 0,
created_at INTEGER,
updated_at INTEGER
);
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
password_hash TEXT DEFAULT '',
display_name TEXT DEFAULT '',
role TEXT DEFAULT 'member', -- admin / member / auditor
source TEXT DEFAULT 'local', -- local / oidc / ldap
status TEXT DEFAULT 'active', -- active / disabled
last_login_at INTEGER,
created_at INTEGER
);
CREATE TABLE IF NOT EXISTS audit_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
actor TEXT DEFAULT '', -- 用户名 / token 名 / system
action TEXT DEFAULT '', -- 如 task.create / worker.update / agent.run
target TEXT DEFAULT '',
detail TEXT DEFAULT '',
ip TEXT DEFAULT '',
user_agent TEXT DEFAULT '',
created_at INTEGER
);
CREATE TABLE IF NOT EXISTS enterprise_settings (
key TEXT PRIMARY KEY,
value TEXT DEFAULT ''
);
CREATE INDEX IF NOT EXISTS idx_agent_steps_run ON agent_steps(run_id);
CREATE INDEX IF NOT EXISTS idx_agent_runs_status ON agent_runs(status);
CREATE INDEX IF NOT EXISTS idx_eval_cases_ds ON eval_cases(dataset_id);
CREATE INDEX IF NOT EXISTS idx_eval_runs_ds ON eval_runs(dataset_id);
CREATE INDEX IF NOT EXISTS idx_eval_results_run ON eval_results(run_id);
CREATE INDEX IF NOT EXISTS idx_audit_time ON audit_logs(created_at);
CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
CREATE INDEX IF NOT EXISTS idx_logs_task ON task_logs(task_id);
CREATE INDEX IF NOT EXISTS idx_cost_task ON cost_records(task_id);
CREATE INDEX IF NOT EXISTS idx_cost_project ON cost_records(project_id);
CREATE INDEX IF NOT EXISTS idx_docs_project ON documents(project_id);
CREATE INDEX IF NOT EXISTS idx_chunks_doc ON doc_chunks(document_id);
CREATE INDEX IF NOT EXISTS idx_alerts_read ON alerts(read);
"""
# ---------------------------------------------------------------------------
# 迁移:给旧表补列(V1
# ---------------------------------------------------------------------------
def _migrate():
conn = get_conn()
cols = {r['name'] for r in conn.execute('PRAGMA table_info(tasks)')}
if 'depends_on' not in cols:
conn.execute("ALTER TABLE tasks ADD COLUMN depends_on TEXT DEFAULT '[]'")
conn.execute('CREATE INDEX IF NOT EXISTS idx_tasks_depends ON tasks(depends_on)')
if 'deleted' not in cols:
conn.execute('ALTER TABLE tasks ADD COLUMN deleted INTEGER DEFAULT 0')
conn.execute('ALTER TABLE tasks ADD COLUMN deleted_at INTEGER')
conn.execute('CREATE INDEX IF NOT EXISTS idx_tasks_deleted ON tasks(deleted)')
conn.commit()
conn.close()
# ---------------------------------------------------------------------------
# V2 迁移:旧库升级(幂等)
# ---------------------------------------------------------------------------
def migrate_v2():
"""老数据库升级:V2 表由 SCHEMA 中的 CREATE TABLE IF NOT EXISTS 保证存在;
此处处理老表缺列 / 默认数据(管理员账号、内置模板)。"""
conn = get_conn()
# users 表首次出现时注入默认管理员
c = conn.execute('SELECT COUNT(*) c FROM users').fetchone()['c']
if c == 0:
conn.execute(
"INSERT INTO users (username, password_hash, display_name, role, source, status, created_at) "
"VALUES ('admin', ?, '管理员', 'admin', 'local', 'active', ?)",
(_hash_password('admin123'), int(time.time())))
conn.commit()
conn.close()
def _hash_password(pwd):
import hashlib
return 'sha256$' + hashlib.sha256(pwd.encode('utf-8')).hexdigest()
def verify_password(pwd, pwd_hash):
if not pwd_hash:
return False
if pwd_hash.startswith('sha256$'):
import hashlib
return hashlib.sha256(pwd.encode('utf-8')).hexdigest() == pwd_hash.split('$', 1)[1]
return pwd == pwd_hash # 兼容明文
def hash_password(pwd):
return _hash_password(pwd)
def get_conn():
conn = sqlite3.connect(DB_PATH, timeout=30)
conn.row_factory = sqlite3.Row
conn.execute('PRAGMA journal_mode=WAL')
conn.execute('PRAGMA foreign_keys=ON')
return conn
def init_db():
conn = get_conn()
conn.executescript(SCHEMA)
conn.commit()
conn.close()
_migrate()
migrate_v2()
def recover_stale_runs():
"""启动恢复:进程重启后,把遗留的 running 状态标记为 failed(线程已随进程消亡)。
覆盖:V1 任务 / V2 协作运行 / V2 评估运行。"""
now_ts = now()
n1 = w('UPDATE tasks SET status="failed", error="服务重启,执行中断", finished_at=? '
'WHERE status="running"', (now_ts,))
n2 = w('UPDATE agent_runs SET status="failed", error="服务重启,协作中断", finished_at=? '
'WHERE status="running"', (now_ts,))
n3 = w('UPDATE eval_runs SET status="failed", finished_at=? WHERE status="running"', (now_ts,))
if n1 or n2 or n3:
import logging
logging.warning(f'recover_stale_runs: tasks={n1 or 0} agent_runs={n2 or 0} eval_runs={n3 or 0}')
return (n1 or 0, n2 or 0, n3 or 0)
def q(sql, args=(), one=False):
"""查询"""
conn = get_conn()
try:
cur = conn.execute(sql, args)
rows = [dict(r) for r in cur.fetchall()]
return (rows[0] if rows else None) if one else rows
finally:
conn.close()
def w(sql, args=()):
"""写入,返回 lastrowid"""
conn = get_conn()
try:
cur = conn.execute(sql, args)
conn.commit()
return cur.lastrowid
finally:
conn.close()
def now():
return int(time.time())
# ---------------------------------------------------------------------------
# 成本统计辅助
# ---------------------------------------------------------------------------
def monthly_worker_cost(worker_id, month_ts=None):
"""某 Worker 当月累计成本(元)"""
if month_ts is None:
import datetime
month_ts = int(datetime.datetime.now().replace(day=1, hour=0, minute=0, second=0, microsecond=0).timestamp())
rows = q(
'SELECT COALESCE(SUM(cost),0) AS total FROM cost_records '
'WHERE worker_id=? AND created_at>=?', (worker_id, month_ts))
return rows[0]['total'] if rows else 0.0
def task_worker_cost(worker_id):
"""某 Worker 最近一次任务成本(用于单任务上限判断前先看历史,不作为硬限制)"""
rows = q(
'SELECT COALESCE(SUM(cost),0) AS total FROM cost_records WHERE worker_id=?',
(worker_id,))
return rows[0]['total'] if rows else 0.0
def serialize_task(t):
t = dict(t)
t['review_required'] = bool(t['review_required'])
try:
t['depends_on'] = json.loads(t.get('depends_on') or '[]')
except Exception:
t['depends_on'] = []
return t
def get_setting(key, default=''):
r = q('SELECT value FROM settings WHERE key=?', (key,), one=True)
return r['value'] if r else default
def set_setting(key, value):
conn = get_conn()
try:
conn.execute('INSERT INTO settings (key, value) VALUES (?,?) '
'ON CONFLICT(key) DO UPDATE SET value=excluded.value', (key, str(value)))
conn.commit()
finally:
conn.close()