Files
ai-worker-platform/db.py
T
hz4th_coder eb92a4e591 V3.0 交付体系 + 精准权限系统
交付环节优化:
- 新建项目必填送达者(人)邮箱,项目完成/遇到无法绕开的难关时自动邮件及时通知
- 每个项目独立工作目录 data/workspace/project_<id>/,交付物分类存放互不污染(上传/下载/删除)
- 网页交付物一键部署到 /demo/<id>/ 免登录公开 Demo,送达者直接打开链接查看
- 非网页交付物 zip 打包 data/packages/,随邮件附件发送送达者(含手动交付/完成交付/自动交付)
- 任务全部完成自动收尾交付;失败任务触发难关通知(30分钟限频去重)

用户(人)管理 + 精准权限:
- 管理员增删改用户,管理用户项目所属与 Worker 权限(授权弹窗 + 授权总览矩阵)
- 角色体系:admin 全部 / auditor 全量只读 / member 按授权
- 项目授权 view/manage/admin;Worker 授权 view/use/manage;创建者自动成为项目管理员
- 仪表盘/成本报表/日志/协作/评估全部按权限过滤,越权访问 403

新表:project_deliverables / user_projects / user_workers;新模块 delivery.py
2026-08-13 12:38:38 +08:00

492 lines
18 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=不限
deliver_email TEXT DEFAULT '', -- 送达者(人)邮箱,新建项目必填
deliver_type TEXT DEFAULT 'web', -- 交付物类型 web=网页 / file=文件包
deliver_note TEXT DEFAULT '', -- 交付说明
workspace_dir TEXT DEFAULT '', -- 项目工作目录(相对 data/ 的目录名)
demo_url TEXT DEFAULT '', -- 网页交付物 Demo 访问地址
delivered_at INTEGER, -- 最近一次交付/送达时间
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 ''
);
-- ===================================================================
-- V3 表结构:交付体系(工作目录/交付物/Demo/邮件送达) + 用户授权(项目/Worker 权限)
-- ===================================================================
CREATE TABLE IF NOT EXISTS project_deliverables (
id INTEGER PRIMARY KEY AUTOINCREMENT,
project_id INTEGER NOT NULL,
name TEXT NOT NULL,
kind TEXT DEFAULT 'file', -- file/dir/webpage/package
path TEXT DEFAULT '', -- 相对项目工作目录路径 / 打包文件名
demo_url TEXT DEFAULT '', -- 网页交付物的 Demo 访问地址
size INTEGER DEFAULT 0,
note TEXT DEFAULT '',
created_at INTEGER
);
CREATE TABLE IF NOT EXISTS user_projects (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
project_id INTEGER NOT NULL,
perm TEXT DEFAULT 'view', -- view 查看 / manage 管理 / admin 管理员
created_at INTEGER,
UNIQUE(user_id, project_id)
);
CREATE TABLE IF NOT EXISTS user_workers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
worker_id INTEGER NOT NULL,
perm TEXT DEFAULT 'view', -- view 查看 / use 使用(可指派任务)/ manage 管理(可改配置)
created_at INTEGER,
UNIQUE(user_id, worker_id)
);
CREATE INDEX IF NOT EXISTS idx_deliverables_project ON project_deliverables(project_id);
CREATE INDEX IF NOT EXISTS idx_user_projects_user ON user_projects(user_id);
CREATE INDEX IF NOT EXISTS idx_user_projects_project ON user_projects(project_id);
CREATE INDEX IF NOT EXISTS idx_user_workers_user ON user_workers(user_id);
CREATE INDEX IF NOT EXISTS idx_user_workers_worker ON user_workers(worker_id);
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)')
# V3projects 交付字段
pcols = {r['name'] for r in conn.execute('PRAGMA table_info(projects)')}
for col, ddl in (
('deliver_email', "ALTER TABLE projects ADD COLUMN deliver_email TEXT DEFAULT ''"),
('deliver_type', "ALTER TABLE projects ADD COLUMN deliver_type TEXT DEFAULT 'web'"),
('deliver_note', "ALTER TABLE projects ADD COLUMN deliver_note TEXT DEFAULT ''"),
('workspace_dir', "ALTER TABLE projects ADD COLUMN workspace_dir TEXT DEFAULT ''"),
('demo_url', "ALTER TABLE projects ADD COLUMN demo_url TEXT DEFAULT ''"),
('delivered_at', 'ALTER TABLE projects ADD COLUMN delivered_at INTEGER'),
):
if col not in pcols:
conn.execute(ddl)
# V3:老项目补齐工作目录名
for r in conn.execute("SELECT id, workspace_dir FROM projects WHERE workspace_dir IS NULL OR workspace_dir=''"):
conn.execute('UPDATE projects SET workspace_dir=? WHERE id=?',
('project_%d' % r['id'], r['id']))
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()