- 新建项目必填 AI 主管(manager_worker_id)+ 干活团队(project_team_workers 多选,默认AI牛/AI马),均可随时更换
- 创建后立即自动开工:AI主管WBS拆解→轮询分派团队→DAG自动执行→全程监控(autostart.py)
- 失败任务由AI主管诊断原因并修订指令重试(最多2次),全程记入 project_logs 动态
- 项目页新增「AI主管动态」面板:状态/最新动态/主管与团队/动态流水,运行中每3秒自动刷新看板
- 支持手动「🚀 AI主管开工」:已有任务跳过规划直接执行存量;换主管/团队后按新配置生效
- 收工自动汇总完成数/失败数/待审核数/总成本,触发交付体系通知送达者
- 老库自动迁移:projects 新增 manager_worker_id/auto_status 等列 + project_team_workers/project_logs 表
637 lines
24 KiB
Python
637 lines
24 KiB
Python
# -*- 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_user_id INTEGER, -- 送达者用户 id(users.id),实时取该用户邮箱
|
||
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, -- 最近一次交付/送达时间
|
||
manager_worker_id INTEGER, -- V3.3 AI 主管 Worker id(负责拆解/派活/监控)
|
||
auto_status TEXT DEFAULT 'none', -- V3.3 自动开工状态 none/running/done/failed
|
||
auto_message TEXT DEFAULT '', -- V3.3 自动开工最新动态
|
||
auto_started_at INTEGER, -- V3.3 最近一次自动开工时间
|
||
auto_finished_at INTEGER, -- V3.3 最近一次自动收尾时间
|
||
created_at INTEGER,
|
||
updated_at INTEGER
|
||
);
|
||
|
||
-- V3.3 项目干活团队:AI 主管支配的多个 Worker
|
||
CREATE TABLE IF NOT EXISTS project_team_workers (
|
||
project_id INTEGER NOT NULL,
|
||
worker_id INTEGER NOT NULL,
|
||
created_at INTEGER,
|
||
PRIMARY KEY (project_id, worker_id)
|
||
);
|
||
|
||
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
|
||
);
|
||
|
||
-- V3.3 AI 主管项目级动态(拆解/派活/监控/诊断重试)
|
||
CREATE TABLE IF NOT EXISTS project_logs (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
project_id INTEGER NOT NULL,
|
||
level TEXT DEFAULT 'info',
|
||
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 '',
|
||
email 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)
|
||
);
|
||
|
||
-- ===================================================================
|
||
-- V3.1 表结构:自定义角色(权限功能点)+ Worker 权限组(批量授权)
|
||
-- ===================================================================
|
||
CREATE TABLE IF NOT EXISTS roles (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
name TEXT NOT NULL UNIQUE,
|
||
description TEXT DEFAULT '',
|
||
perms TEXT DEFAULT '[]', -- JSON: 权限点 id 列表
|
||
is_builtin INTEGER DEFAULT 0, -- 内置角色(admin/auditor/member)不可删除
|
||
created_at INTEGER,
|
||
updated_at INTEGER
|
||
);
|
||
|
||
CREATE TABLE IF NOT EXISTS user_roles (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
user_id INTEGER NOT NULL,
|
||
role_id INTEGER NOT NULL,
|
||
created_at INTEGER,
|
||
UNIQUE(user_id, role_id)
|
||
);
|
||
|
||
CREATE TABLE IF NOT EXISTS worker_perm_groups (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
name TEXT NOT NULL,
|
||
description TEXT DEFAULT '',
|
||
created_at INTEGER,
|
||
updated_at INTEGER
|
||
);
|
||
|
||
CREATE TABLE IF NOT EXISTS worker_perm_group_members (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
group_id INTEGER NOT NULL,
|
||
worker_id INTEGER NOT NULL,
|
||
perm TEXT DEFAULT 'view', -- 组内该 Worker 权限(可覆盖组默认)
|
||
created_at INTEGER,
|
||
UNIQUE(group_id, worker_id)
|
||
);
|
||
|
||
CREATE TABLE IF NOT EXISTS user_worker_groups (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
user_id INTEGER NOT NULL,
|
||
group_id INTEGER NOT NULL,
|
||
created_at INTEGER,
|
||
UNIQUE(user_id, group_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_user_roles_user ON user_roles(user_id);
|
||
CREATE INDEX IF NOT EXISTS idx_wpg_members_group ON worker_perm_group_members(group_id);
|
||
CREATE INDEX IF NOT EXISTS idx_uwg_user ON user_worker_groups(user_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)')
|
||
# V3:projects 交付字段
|
||
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.2:projects 送达者改为用户 id(实时取邮箱)
|
||
if 'deliver_user_id' not in pcols:
|
||
conn.execute('ALTER TABLE projects ADD COLUMN deliver_user_id INTEGER')
|
||
# V3.3:projects AI 主管 + 自动开工状态
|
||
for col, ddl in (
|
||
('manager_worker_id', 'ALTER TABLE projects ADD COLUMN manager_worker_id INTEGER'),
|
||
('auto_status', "ALTER TABLE projects ADD COLUMN auto_status TEXT DEFAULT 'none'"),
|
||
('auto_message', "ALTER TABLE projects ADD COLUMN auto_message TEXT DEFAULT ''"),
|
||
('auto_started_at', 'ALTER TABLE projects ADD COLUMN auto_started_at INTEGER'),
|
||
('auto_finished_at', 'ALTER TABLE projects ADD COLUMN auto_finished_at INTEGER'),
|
||
):
|
||
if col not in pcols:
|
||
conn.execute(ddl)
|
||
conn.execute('''CREATE TABLE IF NOT EXISTS project_team_workers (
|
||
project_id INTEGER NOT NULL,
|
||
worker_id INTEGER NOT NULL,
|
||
created_at INTEGER,
|
||
PRIMARY KEY (project_id, worker_id)
|
||
)''')
|
||
conn.execute('''CREATE TABLE IF NOT EXISTS project_logs (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
project_id INTEGER NOT NULL,
|
||
level TEXT DEFAULT 'info',
|
||
message TEXT DEFAULT '',
|
||
created_at INTEGER
|
||
)''')
|
||
# V3.2:users 邮箱列 + 存量用户默认邮箱 + 旧项目按邮箱回填送达者用户
|
||
ucols = {r['name'] for r in conn.execute('PRAGMA table_info(users)')}
|
||
if 'email' not in ucols:
|
||
conn.execute("ALTER TABLE users ADD COLUMN email TEXT DEFAULT ''")
|
||
for r in conn.execute('SELECT id, username, email FROM users WHERE email IS NULL OR email=\'\''):
|
||
conn.execute('UPDATE users SET email=? WHERE id=?',
|
||
(f'{r["username"]}@tphai.com', r['id']))
|
||
# 旧项目:按 deliver_email 匹配用户回填 deliver_user_id
|
||
for r in conn.execute("SELECT id, deliver_email FROM projects WHERE (deliver_user_id IS NULL OR deliver_user_id=0) "
|
||
"AND deliver_email IS NOT NULL AND deliver_email != ''"):
|
||
u = conn.execute('SELECT id FROM users WHERE email=?', (r['deliver_email'],)).fetchone()
|
||
if u:
|
||
conn.execute('UPDATE projects SET deliver_user_id=? WHERE id=?', (u['id'], r['id']))
|
||
# 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()
|
||
seed_builtin_roles()
|
||
|
||
|
||
# V3.1 内置角色权限点定义(admin 为特殊值 ALL,表示全部权限)
|
||
BUILTIN_ROLE_PERMS = {
|
||
'admin': ['*'],
|
||
# 审计员:全量只读
|
||
'auditor': ['dashboard.view', 'project.view', 'worker.view', 'agent.view', 'eval.view',
|
||
'template.view', 'report.view', 'log.view', 'alert.view'],
|
||
# 成员:基础功能(资源级仍受项目/Worker 授权约束)
|
||
'member': ['dashboard.view', 'project.view', 'project.create', 'project.deliver',
|
||
'worker.view', 'agent.view', 'agent.run', 'eval.view', 'eval.run',
|
||
'template.view', 'report.view', 'log.view', 'alert.view'],
|
||
}
|
||
|
||
|
||
def seed_builtin_roles():
|
||
"""幂等:仅首次创建 admin/auditor/member 内置角色(不覆盖管理员后续编辑)"""
|
||
import json as _json
|
||
conn = get_conn()
|
||
try:
|
||
for name, perms in BUILTIN_ROLE_PERMS.items():
|
||
r = conn.execute('SELECT id FROM roles WHERE name=?', (name,)).fetchone()
|
||
if not r:
|
||
conn.execute(
|
||
'INSERT INTO roles (name, description, perms, is_builtin, created_at, updated_at) '
|
||
'VALUES (?,?,?,1,?,?)',
|
||
(name, {'admin': '超级管理员:全部权限', 'auditor': '审计员:全量只读',
|
||
'member': '成员:基础功能,资源级按项目/Worker 授权'}[name],
|
||
_json.dumps(perms), now(), now()))
|
||
conn.commit()
|
||
finally:
|
||
conn.close()
|
||
|
||
|
||
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()
|