# -*- 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 最近一次自动收尾时间 review_token TEXT DEFAULT '', -- V3.4 项目验收令牌(发给负责人,公开链接免登录验收) review_required INTEGER DEFAULT 1, -- V3.4 是否需负责人验收后才算完成(默认需要) 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); -- =================================================================== -- V3.5 表结构:大模型接口库 / AI Worker 团队 / 对话 / 精细化计量 -- =================================================================== CREATE TABLE IF NOT EXISTS llm_endpoints ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, provider TEXT DEFAULT 'custom', -- doubao/deepseek/autodl/qwen/openai/vllm/custom base_url TEXT DEFAULT '', api_key TEXT DEFAULT '', models TEXT DEFAULT '[]', -- JSON: 可用模型名列表 pricing TEXT DEFAULT '{}', -- JSON: {model: {input: 元/1M, input_cache: 元/1M, output: 元/1M}} capabilities TEXT DEFAULT '{}', -- JSON: {model: [能力列表 chat/thinking/vision/audio_in/audio_out/image_gen/video_gen/embedding/rerank]} input_price REAL DEFAULT 0, -- 兜底输入价(元/1M tokens) output_price REAL DEFAULT 0, -- 兜底输出价(元/1M tokens) price_per_call REAL DEFAULT 0, -- 按调用次数计费单价(元/千次) billing TEXT DEFAULT 'token', -- token=按token数计费 / call=按调用次数计费 description TEXT DEFAULT '', status TEXT DEFAULT 'enabled', -- enabled/disabled created_at INTEGER, updated_at INTEGER ); CREATE TABLE IF NOT EXISTS worker_teams ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, description TEXT DEFAULT '', worker_ids TEXT DEFAULT '[]', -- JSON: worker id 列表 created_at INTEGER, updated_at INTEGER ); CREATE TABLE IF NOT EXISTS chat_sessions ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT DEFAULT '', target_type TEXT DEFAULT 'worker', -- model=大模型接口 / worker=AI Worker / team=团队 target_id INTEGER DEFAULT 0, model TEXT DEFAULT '', -- target_type=model 时选定的模型名 pinned INTEGER DEFAULT 0, -- 置顶 use_kb INTEGER DEFAULT 0, -- 是否注入知识库上下文 created_at INTEGER, updated_at INTEGER ); CREATE TABLE IF NOT EXISTS chat_messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_id INTEGER NOT NULL, role TEXT DEFAULT 'user', -- user/assistant content TEXT DEFAULT '', thinking TEXT DEFAULT '', -- 思考模型的过程内容 image TEXT DEFAULT '', -- 用户消息附带的图片(路径/数据URL) doc TEXT DEFAULT '', -- 用户消息附带的文档附件(路径) model TEXT DEFAULT '', worker_id INTEGER, prompt_tokens INTEGER DEFAULT 0, completion_tokens INTEGER DEFAULT 0, cached_tokens INTEGER DEFAULT 0, cost REAL DEFAULT 0, latency_ms INTEGER DEFAULT 0, first_token_ms INTEGER DEFAULT 0, error TEXT DEFAULT '', created_at INTEGER ); CREATE TABLE IF NOT EXISTS kb_documents ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, content TEXT DEFAULT '', tags TEXT DEFAULT '[]', source TEXT DEFAULT 'manual', created_at INTEGER, updated_at INTEGER ); CREATE TABLE IF NOT EXISTS kb_chunks ( id INTEGER PRIMARY KEY AUTOINCREMENT, doc_id INTEGER NOT NULL, idx INTEGER DEFAULT 0, content TEXT DEFAULT '', tokens TEXT DEFAULT '[]' ); CREATE INDEX IF NOT EXISTS idx_kb_chunks_doc ON kb_chunks(doc_id); CREATE INDEX IF NOT EXISTS idx_chat_msg_session ON chat_messages(session_id); CREATE INDEX IF NOT EXISTS idx_cost_worker ON cost_records(worker_id); """ # --------------------------------------------------------------------------- # 迁移:给旧表补列(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) # 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) 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'])) # ================= V3.5 迁移:精细化计量 / 接口库 / 团队 / 对话 ================= ccols = {r['name'] for r in conn.execute('PRAGMA table_info(cost_records)')} for col, ddl in ( ('calls', 'ALTER TABLE cost_records ADD COLUMN calls INTEGER DEFAULT 1'), ('cached_tokens', 'ALTER TABLE cost_records ADD COLUMN cached_tokens INTEGER DEFAULT 0'), ('latency_ms', 'ALTER TABLE cost_records ADD COLUMN latency_ms INTEGER DEFAULT 0'), ('first_token_ms', 'ALTER TABLE cost_records ADD COLUMN first_token_ms INTEGER DEFAULT 0'), ): if col not in ccols: conn.execute(ddl) wcols = {r['name'] for r in conn.execute('PRAGMA table_info(workers)')} for col, ddl in ( ('endpoint_id', 'ALTER TABLE workers ADD COLUMN endpoint_id INTEGER'), ('is_main', 'ALTER TABLE workers ADD COLUMN is_main INTEGER DEFAULT 0'), ): if col not in wcols: conn.execute(ddl) pcols3 = {r['name'] for r in conn.execute('PRAGMA table_info(projects)')} if 'is_reference' not in pcols3: conn.execute('ALTER TABLE projects ADD COLUMN is_reference INTEGER DEFAULT 0') if 'ref_builtin' not in pcols3: conn.execute('ALTER TABLE projects ADD COLUMN ref_builtin INTEGER DEFAULT 0') if 'ref_source_id' not in pcols3: conn.execute('ALTER TABLE projects ADD COLUMN ref_source_id INTEGER DEFAULT 0') acols = {r['name'] for r in conn.execute('PRAGMA table_info(agent_runs)')} if 'workspace_dir' not in acols: conn.execute("ALTER TABLE agent_runs ADD COLUMN workspace_dir TEXT DEFAULT ''") scols = {r['name'] for r in conn.execute('PRAGMA table_info(chat_sessions)')} if 'model' not in scols: conn.execute("ALTER TABLE chat_sessions ADD COLUMN model TEXT DEFAULT ''") if 'pinned' not in scols: conn.execute('ALTER TABLE chat_sessions ADD COLUMN pinned INTEGER DEFAULT 0') if 'use_kb' not in scols: conn.execute('ALTER TABLE chat_sessions ADD COLUMN use_kb INTEGER DEFAULT 0') mcols = {r['name'] for r in conn.execute('PRAGMA table_info(chat_messages)')} if 'thinking' not in mcols: conn.execute("ALTER TABLE chat_messages ADD COLUMN thinking TEXT DEFAULT ''") if 'image' not in mcols: conn.execute("ALTER TABLE chat_messages ADD COLUMN image TEXT DEFAULT ''") if 'doc' not in mcols: conn.execute("ALTER TABLE chat_messages ADD COLUMN doc TEXT DEFAULT ''") ecols = {r['name'] for r in conn.execute('PRAGMA table_info(llm_endpoints)')} if 'capabilities' not in ecols: conn.execute("ALTER TABLE llm_endpoints ADD COLUMN capabilities TEXT DEFAULT '{}'") # V3.5 新表(幂等) conn.execute('''CREATE TABLE IF NOT EXISTS llm_endpoints ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, provider TEXT DEFAULT 'custom', base_url TEXT DEFAULT '', api_key TEXT DEFAULT '', models TEXT DEFAULT '[]', pricing TEXT DEFAULT '{}', input_price REAL DEFAULT 0, output_price REAL DEFAULT 0, price_per_call REAL DEFAULT 0, billing TEXT DEFAULT 'token', description TEXT DEFAULT '', status TEXT DEFAULT 'enabled', created_at INTEGER, updated_at INTEGER )''') conn.execute('''CREATE TABLE IF NOT EXISTS worker_teams ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, description TEXT DEFAULT '', worker_ids TEXT DEFAULT '[]', created_at INTEGER, updated_at INTEGER )''') conn.execute('''CREATE TABLE IF NOT EXISTS chat_sessions ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT DEFAULT '', target_type TEXT DEFAULT 'worker', target_id INTEGER DEFAULT 0, model TEXT DEFAULT '', created_at INTEGER, updated_at INTEGER )''') conn.execute('''CREATE TABLE IF NOT EXISTS chat_messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_id INTEGER NOT NULL, role TEXT DEFAULT 'user', content TEXT DEFAULT '', model TEXT DEFAULT '', worker_id INTEGER, prompt_tokens INTEGER DEFAULT 0, completion_tokens INTEGER DEFAULT 0, cached_tokens INTEGER DEFAULT 0, cost REAL DEFAULT 0, latency_ms INTEGER DEFAULT 0, first_token_ms INTEGER DEFAULT 0, error TEXT DEFAULT '', created_at INTEGER )''') # V3.5.2:chat_messages 补 thinking/image;chat_sessions 补 pinned/use_kb;endpoints 补 capabilities mcols = {r['name'] for r in conn.execute('PRAGMA table_info(chat_messages)')} for col, ddl in ( ('thinking', "ALTER TABLE chat_messages ADD COLUMN thinking TEXT DEFAULT ''"), ('image', "ALTER TABLE chat_messages ADD COLUMN image TEXT DEFAULT ''"), ('doc', "ALTER TABLE chat_messages ADD COLUMN doc TEXT DEFAULT ''"), ): if col not in mcols: conn.execute(ddl) scols = {r['name'] for r in conn.execute('PRAGMA table_info(chat_sessions)')} for col, ddl in ( ('pinned', 'ALTER TABLE chat_sessions ADD COLUMN pinned INTEGER DEFAULT 0'), ('use_kb', 'ALTER TABLE chat_sessions ADD COLUMN use_kb INTEGER DEFAULT 0'), ): if col not in scols: conn.execute(ddl) ecols = {r['name'] for r in conn.execute('PRAGMA table_info(llm_endpoints)')} if 'capabilities' not in ecols: conn.execute("ALTER TABLE llm_endpoints ADD COLUMN capabilities TEXT DEFAULT '{}'") # V3.5.2 知识库 conn.execute('''CREATE TABLE IF NOT EXISTS kb_documents ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, content TEXT DEFAULT '', tags TEXT DEFAULT '[]', source TEXT DEFAULT 'manual', created_at INTEGER, updated_at INTEGER )''') conn.execute('''CREATE TABLE IF NOT EXISTS kb_chunks ( id INTEGER PRIMARY KEY AUTOINCREMENT, doc_id INTEGER NOT NULL, idx INTEGER DEFAULT 0, content TEXT DEFAULT '', tokens TEXT DEFAULT '[]' )''') conn.execute('CREATE INDEX IF NOT EXISTS idx_kb_chunks_doc ON kb_chunks(doc_id)') conn.execute('CREATE INDEX IF NOT EXISTS idx_kb_title ON kb_documents(title)') 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() seed_endpoints() link_workers_endpoints() # 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) # --------------------------------------------------------------------------- # V3.5 大模型接口库:从 config.PROVIDERS/MODEL_PRICING 自动导入 + 存量 Worker 关联 # --------------------------------------------------------------------------- def seed_endpoints(): """幂等:首次启动把 config.PROVIDERS 导入为大模型接口库(含逐模型定价), 便于在页面上统一管理与配置价格(按 token / 按调用次数)。""" c = q('SELECT COUNT(*) c FROM llm_endpoints')[0]['c'] if c > 0: return import config as cfg prefix = {'doubao': 'doubao', 'deepseek': 'deepseek', 'openai': 'gpt', 'qwen': 'qwen', 'vllm': '', 'autodl': 'qwen3'} ts = now() for pid, p in cfg.PROVIDERS.items(): models = [m for m in cfg.MODEL_PRICING if m.startswith(prefix.get(pid, '__none__'))] pricing = {m: cfg.MODEL_PRICING[m] for m in models} fp = cfg.MODEL_PRICING.get(models[0]) if models else cfg.DEFAULT_PRICE w('INSERT INTO llm_endpoints (name, provider, base_url, api_key, models, pricing, ' 'input_price, output_price, price_per_call, billing, description, status, created_at, updated_at) ' 'VALUES (?,?,?,?,?,?,?,?,0,?,?,?,?,?)', (p['name'], pid, p['base_url'], p['api_key'], json.dumps(models), json.dumps(pricing), fp.get('input', 2.0), fp.get('output', 8.0), 'token', f'由系统配置自动导入({pid})', 'enabled', ts, ts)) def link_workers_endpoints(): """存量 Worker:provider 匹配的接口库自动关联 endpoint_id(统一计价与鉴权)""" conn = get_conn() try: eps = {r['provider']: r['id'] for r in conn.execute('SELECT id, provider FROM llm_endpoints')} for r in conn.execute('SELECT id, provider FROM workers WHERE endpoint_id IS NULL OR endpoint_id=0'): eid = eps.get(r['provider']) if eid: conn.execute('UPDATE workers SET endpoint_id=? WHERE id=?', (eid, r['id'])) conn.commit() finally: conn.close() def main_worker_id(): """主力 AI Worker id:优先取设置 main_worker_id 指向的启用 Worker,否则首个启用 Worker""" mid = get_setting('main_worker_id', '') if mid: r = q('SELECT id FROM workers WHERE id=? AND status="enabled"', (int(mid),), one=True) if r: return r['id'] r = q('SELECT id FROM workers WHERE status="enabled" ORDER BY id', one=True) return r['id'] if r else None def set_main_worker(worker_id): """设置主力 AI Worker:先清除其它 is_main,再标记目标""" conn = get_conn() try: conn.execute('UPDATE workers SET is_main=0 WHERE is_main=1') conn.execute('UPDATE workers SET is_main=1 WHERE id=?', (int(worker_id),)) conn.commit() finally: conn.close() set_setting('main_worker_id', int(worker_id)) def get_llm_timeouts(): """读取超时配置:token_timeout(单token返回超时) / first_token_timeout(首字延迟超时) / request_timeout(整体兜底)。 单位秒,0/空 回退 config 默认。""" import config as cfg try: tk = float(get_setting('token_timeout', '') or 0) or cfg.TOKEN_TIMEOUT except Exception: tk = cfg.TOKEN_TIMEOUT try: fk = float(get_setting('first_token_timeout', '') or 0) or cfg.FIRST_TOKEN_TIMEOUT except Exception: fk = cfg.FIRST_TOKEN_TIMEOUT try: rk = float(get_setting('request_timeout', '') or 0) or cfg.TASK_TIMEOUT except Exception: rk = cfg.TASK_TIMEOUT return max(3.0, tk), max(3.0, fk), max(10.0, rk) 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 serialize_task_light(t): """轻量序列化(列表接口用):去掉大字段 output_text / error, 看板、DAG、回收站等列表视图不需要它们,可显著减小传输体积。 详情接口 /api/tasks/ 仍返回完整数据。""" t = serialize_task(t) t.pop('output_text', None) t.pop('error', None) 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()