2026-08-12 00:22:36 +08:00
|
|
|
|
# -*- 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=不限
|
2026-08-13 16:21:35 +08:00
|
|
|
|
deliver_user_id INTEGER, -- 送达者用户 id(users.id),实时取该用户邮箱
|
|
|
|
|
|
deliver_email TEXT DEFAULT '', -- 送达者邮箱快照(兼容/展示,发送时以用户最新邮箱为准)
|
2026-08-13 12:38:38 +08:00
|
|
|
|
deliver_type TEXT DEFAULT 'web', -- 交付物类型 web=网页 / file=文件包
|
|
|
|
|
|
deliver_note TEXT DEFAULT '', -- 交付说明
|
|
|
|
|
|
workspace_dir TEXT DEFAULT '', -- 项目工作目录(相对 data/ 的目录名)
|
|
|
|
|
|
demo_url TEXT DEFAULT '', -- 网页交付物 Demo 访问地址
|
|
|
|
|
|
delivered_at INTEGER, -- 最近一次交付/送达时间
|
2026-08-14 12:35:46 +08:00
|
|
|
|
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 最近一次自动收尾时间
|
2026-08-14 13:33:59 +08:00
|
|
|
|
review_token TEXT DEFAULT '', -- V3.4 项目验收令牌(发给负责人,公开链接免登录验收)
|
|
|
|
|
|
review_required INTEGER DEFAULT 1, -- V3.4 是否需负责人验收后才算完成(默认需要)
|
2026-08-12 00:22:36 +08:00
|
|
|
|
created_at INTEGER,
|
|
|
|
|
|
updated_at INTEGER
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-08-14 12:35:46 +08:00
|
|
|
|
-- 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)
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-08-12 00:22:36 +08:00
|
|
|
|
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
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-08-14 12:35:46 +08:00
|
|
|
|
-- 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
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-08-12 00:22:36 +08:00
|
|
|
|
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
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-08-12 00:48:52 +08:00
|
|
|
|
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 ''
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-08-12 00:22:36 +08:00
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_tasks_project ON tasks(project_id);
|
2026-08-12 12:21:17 +08:00
|
|
|
|
|
|
|
|
|
|
-- ===================================================================
|
|
|
|
|
|
-- 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 '',
|
2026-08-13 16:21:35 +08:00
|
|
|
|
email TEXT DEFAULT '', -- 用户邮箱(必填,送达/通知用)
|
2026-08-12 12:21:17 +08:00
|
|
|
|
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 ''
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-08-13 12:38:38 +08:00
|
|
|
|
-- ===================================================================
|
|
|
|
|
|
-- 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)
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-08-13 13:03:57 +08:00
|
|
|
|
-- ===================================================================
|
|
|
|
|
|
-- 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)
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-08-13 12:38:38 +08:00
|
|
|
|
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);
|
2026-08-13 13:03:57 +08:00
|
|
|
|
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);
|
2026-08-12 12:21:17 +08:00
|
|
|
|
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);
|
2026-08-12 00:22:36 +08:00
|
|
|
|
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);
|
2026-08-12 00:48:52 +08:00
|
|
|
|
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);
|
2026-08-12 00:22:36 +08:00
|
|
|
|
"""
|
|
|
|
|
|
|
2026-08-12 00:48:52 +08:00
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# 迁移:给旧表补列(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)')
|
2026-08-12 23:44:20 +08:00
|
|
|
|
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)')
|
2026-08-13 12:38:38 +08:00
|
|
|
|
# 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)
|
2026-08-13 16:21:35 +08:00
|
|
|
|
# V3.2:projects 送达者改为用户 id(实时取邮箱)
|
|
|
|
|
|
if 'deliver_user_id' not in pcols:
|
|
|
|
|
|
conn.execute('ALTER TABLE projects ADD COLUMN deliver_user_id INTEGER')
|
2026-08-14 12:35:46 +08:00
|
|
|
|
# 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)
|
2026-08-14 13:33:59 +08:00
|
|
|
|
# V3.4:负责人验收
|
|
|
|
|
|
pcols = {r['name'] for r in conn.execute('PRAGMA table_info(projects)')}
|
|
|
|
|
|
for col, ddl in (
|
|
|
|
|
|
('review_token', "ALTER TABLE projects ADD COLUMN review_token TEXT DEFAULT ''"),
|
|
|
|
|
|
('review_required', 'ALTER TABLE projects ADD COLUMN review_required INTEGER DEFAULT 1'),
|
|
|
|
|
|
):
|
|
|
|
|
|
if col not in pcols:
|
|
|
|
|
|
conn.execute(ddl)
|
2026-08-14 12:35:46 +08:00
|
|
|
|
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
|
|
|
|
|
|
)''')
|
2026-08-13 16:21:35 +08:00
|
|
|
|
# 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']))
|
2026-08-13 12:38:38 +08:00
|
|
|
|
# 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']))
|
2026-08-12 00:48:52 +08:00
|
|
|
|
conn.commit()
|
|
|
|
|
|
conn.close()
|
|
|
|
|
|
|
2026-08-12 00:22:36 +08:00
|
|
|
|
|
2026-08-12 12:21:17 +08:00
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# 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)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-12 00:22:36 +08:00
|
|
|
|
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()
|
2026-08-12 00:48:52 +08:00
|
|
|
|
_migrate()
|
2026-08-12 12:21:17 +08:00
|
|
|
|
migrate_v2()
|
2026-08-13 13:03:57 +08:00
|
|
|
|
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()
|
2026-08-12 12:21:17 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
2026-08-12 00:22:36 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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'])
|
2026-08-12 00:48:52 +08:00
|
|
|
|
try:
|
|
|
|
|
|
t['depends_on'] = json.loads(t.get('depends_on') or '[]')
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
t['depends_on'] = []
|
2026-08-12 00:22:36 +08:00
|
|
|
|
return t
|
2026-08-12 00:48:52 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-08-16 19:26:13 +08:00
|
|
|
|
def serialize_task_light(t):
|
|
|
|
|
|
"""轻量序列化(列表接口用):去掉大字段 output_text / error,
|
|
|
|
|
|
看板、DAG、回收站等列表视图不需要它们,可显著减小传输体积。
|
|
|
|
|
|
详情接口 /api/tasks/<id> 仍返回完整数据。"""
|
|
|
|
|
|
t = serialize_task(t)
|
|
|
|
|
|
t.pop('output_text', None)
|
|
|
|
|
|
t.pop('error', None)
|
|
|
|
|
|
return t
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-12 00:48:52 +08:00
|
|
|
|
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()
|