# -*- coding: utf-8 -*- """ AI Worker 项目管理平台 - MVP 人派活 → AI 干活 → 人验收 最小闭环 """ import os import secrets import functools import json as _json from flask import Flask, request, jsonify, session, send_from_directory import config import db import engine import llm_gateway import rag import notify app = Flask(__name__, static_folder='static', static_url_path='') app.secret_key = config.SECRET_KEY db.init_db() # --------------------------------------------------------------------------- # 鉴权(简单口令登录) # --------------------------------------------------------------------------- def auth_enabled(): return bool(config.AUTH_PASSWORD) @app.route('/api/login', methods=['POST']) def login(): data = request.get_json(force=True) if not auth_enabled(): return jsonify({'ok': True}) if data.get('password') == config.AUTH_PASSWORD: session['authed'] = True return jsonify({'ok': True}) return jsonify({'ok': False, 'error': '口令错误'}), 401 @app.route('/api/logout', methods=['POST']) def logout(): session.clear() return jsonify({'ok': True}) @app.route('/api/me') def me(): return jsonify({'ok': True, 'authed': not auth_enabled() or session.get('authed')}) def _auth_ok(): """会话或 API Token 任一通过即可""" if not auth_enabled(): return True if session.get('authed'): return True hdr = request.headers.get('Authorization', '') if hdr.startswith('Bearer '): tok = hdr[7:].strip() r = db.q('SELECT id FROM api_tokens WHERE token=?', (tok,), one=True) if r: db.w('UPDATE api_tokens SET last_used_at=? WHERE id=?', (db.now(), r['id'])) return True return False def require_auth(fn): @functools.wraps(fn) def wrapper(*args, **kwargs): if not _auth_ok(): return jsonify({'ok': False, 'error': '未登录或 Token 无效'}), 401 return fn(*args, **kwargs) return wrapper @app.route('/api/health') def health(): return jsonify({'ok': True, 'service': 'ai-worker-platform', 'version': 'v1.0.0'}) # --------------------------------------------------------------------------- # 项目 # --------------------------------------------------------------------------- @app.route('/api/projects', methods=['GET', 'POST']) @require_auth def projects(): if request.method == 'POST': d = request.get_json(force=True) pid = db.w( 'INSERT INTO projects (name, description, objective, acceptance_criteria, ' 'status, budget_limit, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)', (d.get('name', '').strip(), d.get('description', ''), d.get('objective', ''), d.get('acceptance_criteria', ''), d.get('status', 'active'), float(d.get('budget_limit') or 0), db.now(), db.now())) return jsonify({'ok': True, 'id': pid}) rows = db.q('SELECT * FROM projects ORDER BY id DESC') for r in rows: r['task_count'] = db.q('SELECT COUNT(*) c FROM tasks WHERE project_id=?', (r['id'],))[0]['c'] r['done_count'] = db.q('SELECT COUNT(*) c FROM tasks WHERE project_id=? AND status="done"', (r['id'],))[0]['c'] return jsonify({'ok': True, 'data': rows}) @app.route('/api/projects/', methods=['GET', 'PUT', 'DELETE']) @require_auth def project_detail(pid): if request.method == 'GET': p = db.q('SELECT * FROM projects WHERE id=?', (pid,), one=True) if not p: return jsonify({'ok': False, 'error': '项目不存在'}), 404 return jsonify({'ok': True, 'data': p}) if request.method == 'DELETE': db.w('DELETE FROM tasks WHERE project_id=?', (pid,)) db.w('DELETE FROM cost_records WHERE project_id=?', (pid,)) db.w('DELETE FROM projects WHERE id=?', (pid,)) return jsonify({'ok': True}) d = request.get_json(force=True) fields = ['name', 'description', 'objective', 'acceptance_criteria', 'status', 'budget_limit'] sets, args = [], [] for f in fields: if f in d: sets.append(f'{f}=?') args.append(d[f]) if sets: args.append(db.now()) db.w(f'UPDATE projects SET {", ".join(sets)}, updated_at=? WHERE id=?', (*args, pid)) return jsonify({'ok': True}) # --------------------------------------------------------------------------- # Worker # --------------------------------------------------------------------------- @app.route('/api/workers', methods=['GET', 'POST']) @require_auth def workers(): if request.method == 'POST': d = request.get_json(force=True) wid = db.w( 'INSERT INTO workers (name, description, provider, model, base_url, api_key, ' 'system_prompt, temperature, max_tokens, task_cost_limit, monthly_cost_limit, ' 'status, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)', (d.get('name', '').strip(), d.get('description', ''), d.get('provider', ''), d.get('model', ''), d.get('base_url', ''), d.get('api_key', ''), d.get('system_prompt', ''), float(d.get('temperature', 0.7)), int(d.get('max_tokens', 2000)), float(d.get('task_cost_limit') or 0), float(d.get('monthly_cost_limit') or 0), d.get('status', 'enabled'), db.now(), db.now())) return jsonify({'ok': True, 'id': wid}) rows = db.q('SELECT * FROM workers ORDER BY id DESC') for r in rows: r['month_cost'] = round(db.monthly_worker_cost(r['id']), 6) return jsonify({'ok': True, 'data': rows}) @app.route('/api/workers/', methods=['GET', 'PUT', 'DELETE']) @require_auth def worker_detail(wid): if request.method == 'GET': w = db.q('SELECT * FROM workers WHERE id=?', (wid,), one=True) return jsonify({'ok': True, 'data': w}) if w else (jsonify({'ok': False, 'error': '不存在'}), 404) if request.method == 'DELETE': db.w('UPDATE tasks SET worker_id=NULL WHERE worker_id=?', (wid,)) db.w('DELETE FROM workers WHERE id=?', (wid,)) return jsonify({'ok': True}) d = request.get_json(force=True) fields = ['name', 'description', 'provider', 'model', 'base_url', 'api_key', 'system_prompt', 'temperature', 'max_tokens', 'task_cost_limit', 'monthly_cost_limit', 'status'] sets, args = [], [] for f in fields: if f in d: sets.append(f'{f}=?') args.append(d[f]) if sets: args.append(db.now()) db.w(f'UPDATE workers SET {", ".join(sets)}, updated_at=? WHERE id=?', (*args, wid)) return jsonify({'ok': True}) @app.route('/api/workers//test', methods=['POST']) @require_auth def worker_test(wid): w = db.q('SELECT * FROM workers WHERE id=?', (wid,), one=True) if not w: return jsonify({'ok': False, 'error': '不存在'}), 404 try: r = llm_gateway.test_connection(w['provider'], w['model'], base_url=w['base_url'] or None, api_key=w['api_key'] or None) return jsonify({'ok': True, 'data': r}) except Exception as e: return jsonify({'ok': False, 'error': str(e)}) @app.route('/api/providers') @require_auth def providers(): data = [] for k, v in config.PROVIDERS.items(): data.append({ 'id': k, 'name': v['name'], 'base_url': v['base_url'], 'has_key': bool(v['api_key']), 'models': [m for m in config.MODEL_PRICING.keys() if m.startswith( {'doubao': 'doubao', 'deepseek': 'deepseek', 'openai': 'gpt', 'qwen': 'qwen', 'vllm': ''}.get(k, '__none__'))], }) return jsonify({'ok': True, 'data': data}) # --------------------------------------------------------------------------- # 任务 # --------------------------------------------------------------------------- @app.route('/api/tasks', methods=['GET', 'POST']) @require_auth def tasks(): if request.method == 'POST': d = request.get_json(force=True) tid = db.w( 'INSERT INTO tasks (project_id, worker_id, title, description, priority, ' 'review_required, deadline, depends_on, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?)', (d.get('project_id'), d.get('worker_id'), d.get('title', '').strip(), d.get('description', ''), d.get('priority', 'medium'), 1 if d.get('review_required', True) else 0, d.get('deadline', ''), _json.dumps(d.get('depends_on') or []), db.now(), db.now())) db.w('INSERT INTO task_logs (task_id, level, message, created_at) VALUES (?,?,?,?)', (tid, 'info', f'任务创建:{d.get("title","")}', db.now())) return jsonify({'ok': True, 'id': tid}) pid = request.args.get('project_id') if pid: rows = db.q('SELECT * FROM tasks WHERE project_id=? ORDER BY id DESC', (int(pid),)) else: rows = db.q('SELECT * FROM tasks ORDER BY id DESC LIMIT 200') return jsonify({'ok': True, 'data': [db.serialize_task(t) for t in rows]}) @app.route('/api/tasks/', methods=['GET', 'PUT', 'DELETE']) @require_auth def task_detail(tid): t = db.q('SELECT * FROM tasks WHERE id=?', (tid,), one=True) if not t: return jsonify({'ok': False, 'error': '任务不存在'}), 404 if request.method == 'GET': t = db.serialize_task(t) t['logs'] = db.q('SELECT * FROM task_logs WHERE task_id=? ORDER BY id', (tid,)) t['costs'] = db.q('SELECT * FROM cost_records WHERE task_id=? ORDER BY id', (tid,)) if t['worker_id']: t['worker'] = db.q('SELECT id,name,provider,model FROM workers WHERE id=?', (t['worker_id'],), one=True) return jsonify({'ok': True, 'data': t}) if request.method == 'DELETE': db.w('DELETE FROM task_logs WHERE task_id=?', (tid,)) db.w('DELETE FROM cost_records WHERE task_id=?', (tid,)) db.w('DELETE FROM tasks WHERE id=?', (tid,)) return jsonify({'ok': True}) d = request.get_json(force=True) # 只允许在非运行中修改基础字段 if t['status'] == 'running': return jsonify({'ok': False, 'error': '任务执行中,禁止修改'}), 400 fields = ['title', 'description', 'worker_id', 'priority', 'review_required', 'deadline', 'status'] sets, args = [], [] for f in fields: if f in d: sets.append(f'{f}=?') args.append(d[f]) if 'depends_on' in d: sets.append('depends_on=?') args.append(_json.dumps(d['depends_on'] or [])) if sets: args.append(db.now()) db.w(f'UPDATE tasks SET {", ".join(sets)}, updated_at=? WHERE id=?', (*args, tid)) return jsonify({'ok': True}) @app.route('/api/tasks//run', methods=['POST']) @require_auth def task_run(tid): t = db.q('SELECT * FROM tasks WHERE id=?', (tid,), one=True) if not t: return jsonify({'ok': False, 'error': '任务不存在'}), 404 if t['status'] == 'running': return jsonify({'ok': False, 'error': '任务已在执行中'}), 400 ok, blockers = engine.check_dependencies(t) if not ok: return jsonify({'ok': False, 'error': '前置任务未完成:' + '、'.join(blockers)}), 400 if engine.runner.submit(tid): return jsonify({'ok': True}) return jsonify({'ok': False, 'error': '任务已在执行中'}), 400 @app.route('/api/tasks//cancel', methods=['POST']) @require_auth def task_cancel(tid): t = db.q('SELECT * FROM tasks WHERE id=?', (tid,), one=True) if not t: return jsonify({'ok': False, 'error': '任务不存在'}), 404 if t['status'] != 'running': return jsonify({'ok': False, 'error': '仅执行中的任务可取消'}), 400 # MVP:标记取消(线程无法强杀,完成后会回到 review;这里直接置 cancelled 并忽略结果) db.w('UPDATE tasks SET status="cancelled", updated_at=?, finished_at=? WHERE id=?', (db.now(), db.now(), tid)) db.w('INSERT INTO task_logs (task_id, level, message, created_at) VALUES (?,?,?,?)', (tid, 'warn', '任务已由人工取消(引擎线程将在后台自然结束)', db.now())) return jsonify({'ok': True}) @app.route('/api/tasks//review', methods=['POST']) @require_auth def task_review(tid): t = db.q('SELECT * FROM tasks WHERE id=?', (tid,), one=True) if not t: return jsonify({'ok': False, 'error': '任务不存在'}), 404 if t['status'] != 'review': return jsonify({'ok': False, 'error': '仅待审核状态可审核'}), 400 d = request.get_json(force=True) action = d.get('action') reason = (d.get('reason') or '').strip() if action == 'approve': db.w('UPDATE tasks SET status="done", updated_at=? WHERE id=?', (db.now(), tid)) db.w('INSERT INTO task_logs (task_id, level, message, created_at) VALUES (?,?,?,?)', (tid, 'success', '✅ 人工审核通过,任务完成', db.now())) # DAG:审核放行等同完成,触发下游就绪任务 fresh = db.q('SELECT * FROM tasks WHERE id=?', (tid,), one=True) for t in engine._trigger_downstream(fresh): db.w('INSERT INTO task_logs (task_id, level, message, created_at) VALUES (?,?,?,?)', (t['id'], 'info', f'🔗 前置任务「{fresh["title"]}」已验收完成,自动触发执行', db.now())) return jsonify({'ok': True}) if action == 'reject': if not reason: return jsonify({'ok': False, 'error': '打回必须填写原因'}), 400 db.w('UPDATE tasks SET status="todo", rejection_count=rejection_count+1, ' 'output_text="", updated_at=? WHERE id=?', (db.now(), tid)) db.w('INSERT INTO task_logs (task_id, level, message, created_at) VALUES (?,?,?,?)', (tid, 'warn', f'⛔ 人工打回:{reason}(等待返工重跑)', db.now())) return jsonify({'ok': True}) return jsonify({'ok': False, 'error': '未知操作'}), 400 # --------------------------------------------------------------------------- # 报表 / 统计 # --------------------------------------------------------------------------- @app.route('/api/reports/cost') @require_auth def report_cost(): group = request.args.get('group', 'project') if group == 'worker': rows = db.q( 'SELECT worker_id, provider, model, COUNT(*) runs, SUM(total_tokens) tokens, ' 'SUM(cost) cost FROM cost_records GROUP BY worker_id ORDER BY cost DESC') for r in rows: w = db.q('SELECT name FROM workers WHERE id=?', (r['worker_id'],), one=True) r['worker_name'] = w['name'] if w else f'#{r["worker_id"]}' elif group == 'model': rows = db.q( 'SELECT provider, model, COUNT(*) runs, SUM(total_tokens) tokens, ' 'SUM(cost) cost FROM cost_records GROUP BY model ORDER BY cost DESC') else: rows = db.q( 'SELECT project_id, COUNT(*) runs, SUM(total_tokens) tokens, ' 'SUM(cost) cost FROM cost_records GROUP BY project_id ORDER BY cost DESC') for r in rows: p = db.q('SELECT name FROM projects WHERE id=?', (r['project_id'],), one=True) r['project_name'] = p['name'] if p else f'#{r["project_id"]}' return jsonify({'ok': True, 'data': rows}) @app.route('/api/stats') @require_auth def stats(): out = {'projects': 0, 'tasks': 0, 'workers': 0, 'total_cost': 0, 'total_tokens': 0, 'by_status': {}, 'recent': [], 'daily_cost': []} out['projects'] = db.q('SELECT COUNT(*) c FROM projects')[0]['c'] out['workers'] = db.q('SELECT COUNT(*) c FROM workers')[0]['c'] out['tasks'] = db.q('SELECT COUNT(*) c FROM tasks')[0]['c'] for r in db.q('SELECT status, COUNT(*) c FROM tasks GROUP BY status'): out['by_status'][r['status']] = r['c'] c = db.q('SELECT COALESCE(SUM(cost),0) cost, COALESCE(SUM(total_tokens),0) tokens ' 'FROM cost_records')[0] out['total_cost'], out['total_tokens'] = round(c['cost'], 4), c['tokens'] # 一次通过率:done 且 rejection_count=0 done = out['by_status'].get('done', 0) clean = db.q('SELECT COUNT(*) c FROM tasks WHERE status="done" AND rejection_count=0')[0]['c'] out['one_pass_rate'] = round(clean / done * 100, 1) if done else None out['rework_count'] = db.q('SELECT COALESCE(SUM(rejection_count),0) c FROM tasks')[0]['c'] out['recent'] = db.q( 'SELECT t.id, t.title, t.status, t.project_id, p.name AS project_name, t.updated_at ' 'FROM tasks t LEFT JOIN projects p ON p.id=t.project_id ' 'ORDER BY t.updated_at DESC LIMIT 10') # 近 7 天成本 import datetime rows = db.q('SELECT created_at, cost FROM cost_records ORDER BY created_at') day_map = {} for r in rows: d = datetime.datetime.fromtimestamp(r['created_at']).strftime('%m-%d') day_map[d] = round(day_map.get(d, 0) + r['cost'], 4) for i in range(6, -1, -1): d = (datetime.datetime.now() - datetime.timedelta(days=i)).strftime('%m-%d') out['daily_cost'].append({'day': d, 'cost': day_map.get(d, 0)}) return jsonify({'ok': True, 'data': out}) @app.route('/api/logs') @require_auth def logs(): limit = min(int(request.args.get('limit', 100)), 500) rows = db.q( 'SELECT l.*, t.title AS task_title FROM task_logs l ' 'LEFT JOIN tasks t ON t.id=l.task_id ORDER BY l.id DESC LIMIT ?', (limit,)) return jsonify({'ok': True, 'data': rows}) @app.route('/api/projects//workflow/run', methods=['POST']) @require_auth def workflow_run(pid): """执行整个工作流:跑所有就绪(无未完成前置)任务""" rows = db.q('SELECT * FROM tasks WHERE project_id=? AND status IN ("todo","failed")', (pid,)) started, blocked = [], [] for t in rows: ok, blockers = engine.check_dependencies(t) if ok: if engine.runner.submit(t['id']): started.append({'id': t['id'], 'title': t['title']}) else: blocked.append({'id': t['id'], 'title': t['title'], 'by': blockers}) return jsonify({'ok': True, 'started': started, 'blocked': blocked}) @app.route('/api/projects//dag') @require_auth def project_dag(pid): """DAG 图数据:节点 + 边""" rows = db.q('SELECT * FROM tasks WHERE project_id=? ORDER BY id', (pid,)) nodes, edges, id_map = [], [], {} for t in rows: node = db.serialize_task(t) nodes.append(node) id_map[t['id']] = node for t in nodes: for dep_id in t['depends_on']: if dep_id in id_map: edges.append({'from': dep_id, 'to': t['id']}) return jsonify({'ok': True, 'nodes': nodes, 'edges': edges}) # --------------------------------------------------------------------------- # AI 辅助规划(WBS 生成 + 导入) # --------------------------------------------------------------------------- WBS_PROMPT = ( '你是资深项目经理。请把下面的项目目标拆解为可执行的任务列表(WBS),' '要求:\n1. 输出严格 JSON,格式 {{"tasks": [{{"title": "任务标题", ' '"description": "给AI Worker的执行指令(含要求与输出格式)", "depends_on": [0,2]}}]}}\n' '2. depends_on 是前置任务的数组下标(无依赖填 []),下标从 0 开始\n' '3. 4~8 个任务,逻辑清晰,可并行任务并行,不要输出 JSON 以外的任何内容\n\n' '项目目标:{goal}\n' '项目验收标准:{accept}' ) def _parse_wbs(text): """从 LLM 输出中提取 JSON""" t = text.strip() if t.startswith('```'): t = t.strip('`') if t.startswith('json'): t = t[4:] t = t.strip() start = min([i for i in (t.find('{'), t.find('[')) if i >= 0] or [0]) end = max(t.rfind('}'), t.rfind(']')) + 1 data = _json.loads(t[start:end]) tasks = data['tasks'] if isinstance(data, dict) else data assert isinstance(tasks, list) and tasks, '任务列表为空' return tasks @app.route('/api/projects//wbs/generate', methods=['POST']) @require_auth def wbs_generate(pid): d = request.get_json(force=True) or {} goal = d.get('goal') or '' proj = db.q('SELECT * FROM projects WHERE id=?', (pid,), one=True) if not proj: return jsonify({'ok': False, 'error': '项目不存在'}), 404 if not goal: goal = proj.get('objective') or proj.get('name') or '' if not goal: return jsonify({'ok': False, 'error': '请提供项目目标'}), 400 worker = db.q('SELECT * FROM workers WHERE status="enabled" ORDER BY id', one=True) if not worker: return jsonify({'ok': False, 'error': '请先注册至少一个 Worker 用于规划'}), 400 try: r = llm_gateway.chat(worker['provider'], worker['model'], [ {'role': 'system', 'content': '你只输出 JSON,不输出任何解释文字。'}, {'role': 'user', 'content': WBS_PROMPT.format(goal=goal, accept=proj.get('acceptance_criteria') or '—')}, ], temperature=0.3, max_tokens=3000) tasks = _parse_wbs(r['text']) return jsonify({'ok': True, 'data': tasks, 'usage': r['total_tokens'], 'cost': r['cost']}) except Exception as e: return jsonify({'ok': False, 'error': f'WBS 生成失败:{e}'}), 500 @app.route('/api/projects//wbs/import', methods=['POST']) @require_auth def wbs_import(pid): d = request.get_json(force=True) tasks = d.get('tasks') or [] worker_id = d.get('worker_id') if not tasks: return jsonify({'ok': False, 'error': '任务列表为空'}), 400 created = [] for i, t in enumerate(tasks): dep_idx = t.get('depends_on') or [] dep_ids = [created[idx] for idx in dep_idx if isinstance(idx, int) and 0 <= idx < len(created)] tid = db.w( 'INSERT INTO tasks (project_id, worker_id, title, description, priority, ' 'review_required, deadline, depends_on, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?)', (pid, worker_id, t.get('title', f'任务{i+1}'), t.get('description', ''), t.get('priority', 'medium'), 1, '', _json.dumps(dep_ids), db.now(), db.now())) created.append(tid) db.w('INSERT INTO task_logs (task_id, level, message, created_at) VALUES (?,?,?,?)', (tid, 'info', f'AI 规划导入:{t.get("title", "")}', db.now())) return jsonify({'ok': True, 'created': len(created), 'ids': created}) # --------------------------------------------------------------------------- # RAG 知识库 # --------------------------------------------------------------------------- @app.route('/api/projects//documents', methods=['GET', 'POST']) @require_auth def documents(pid): if request.method == 'POST': d = request.get_json(force=True) doc_id = db.w( 'INSERT INTO documents (project_id, name, content, source, created_at, updated_at) ' 'VALUES (?,?,?,?,?,?)', (pid, d.get('name', '未命名文档').strip(), d.get('content', ''), d.get('source', 'manual'), db.now(), db.now())) n = rag.rebuild_document(doc_id) return jsonify({'ok': True, 'id': doc_id, 'chunks': n}) rows = db.q('SELECT * FROM documents WHERE project_id=? ORDER BY id DESC', (pid,)) for r in rows: r['chunks'] = db.q('SELECT COUNT(*) c FROM doc_chunks WHERE document_id=?', (r['id'],))[0]['c'] return jsonify({'ok': True, 'data': rows}) @app.route('/api/documents/', methods=['GET', 'PUT', 'DELETE']) @require_auth def document_detail(doc_id): doc = db.q('SELECT * FROM documents WHERE id=?', (doc_id,), one=True) if not doc: return jsonify({'ok': False, 'error': '文档不存在'}), 404 if request.method == 'GET': return jsonify({'ok': True, 'data': doc}) if request.method == 'DELETE': db.w('DELETE FROM doc_chunks WHERE document_id=?', (doc_id,)) db.w('DELETE FROM documents WHERE id=?', (doc_id,)) return jsonify({'ok': True}) d = request.get_json(force=True) if 'content' in d: db.w('UPDATE documents SET content=?, updated_at=? WHERE id=?', (d['content'], db.now(), doc_id)) rag.rebuild_document(doc_id) if 'name' in d: db.w('UPDATE documents SET name=? WHERE id=?', (d['name'].strip(), doc_id)) return jsonify({'ok': True}) @app.route('/api/projects//search') @require_auth def kb_search(pid): q = request.args.get('q', '') if not q: return jsonify({'ok': True, 'data': []}) hits, hit = rag.search_project(pid, q, top_k=5) return jsonify({'ok': True, 'data': hits if hit else [], 'hit': hit}) # --------------------------------------------------------------------------- # 告警中心 # --------------------------------------------------------------------------- @app.route('/api/alerts') @require_auth def alerts(): limit = min(int(request.args.get('limit', 100)), 500) rows = db.q('SELECT * FROM alerts ORDER BY id DESC LIMIT ?', (limit,)) return jsonify({'ok': True, 'data': rows}) @app.route('/api/alerts/unread_count') @require_auth def alerts_unread(): c = db.q('SELECT COUNT(*) c FROM alerts WHERE read=0')[0]['c'] return jsonify({'ok': True, 'count': c}) @app.route('/api/alerts//read', methods=['POST']) @require_auth def alert_read(aid): db.w('UPDATE alerts SET read=1 WHERE id=?', (aid,)) return jsonify({'ok': True}) @app.route('/api/alerts/read_all', methods=['POST']) @require_auth def alerts_read_all(): db.w('UPDATE alerts SET read=1 WHERE read=0') return jsonify({'ok': True}) # --------------------------------------------------------------------------- # 开放 API Token # --------------------------------------------------------------------------- @app.route('/api/tokens', methods=['GET', 'POST']) @require_auth def api_tokens(): if request.method == 'POST': d = request.get_json(force=True) tok = secrets.token_hex(24) db.w('INSERT INTO api_tokens (name, token, created_at) VALUES (?,?,?)', (d.get('name', '未命名').strip(), tok, db.now())) return jsonify({'ok': True, 'token': tok}) rows = db.q('SELECT id, name, created_at, last_used_at FROM api_tokens ORDER BY id DESC') return jsonify({'ok': True, 'data': rows}) @app.route('/api/tokens/', methods=['DELETE']) @require_auth def api_token_delete(tid): db.w('DELETE FROM api_tokens WHERE id=?', (tid,)) return jsonify({'ok': True}) # --------------------------------------------------------------------------- # 通知渠道(飞书/企微/邮件) # --------------------------------------------------------------------------- @app.route('/api/channels', methods=['GET', 'POST']) @require_auth def channels(): if request.method == 'POST': d = request.get_json(force=True) cid = db.w( 'INSERT INTO notify_channels (name, type, webhook, email, events, enabled, created_at) ' 'VALUES (?,?,?,?,?,?,?)', (d.get('name', '').strip(), d.get('type', 'feishu'), d.get('webhook', ''), d.get('email', ''), _json.dumps(d.get('events') or []), 1 if d.get('enabled', True) else 0, db.now())) return jsonify({'ok': True, 'id': cid}) rows = db.q('SELECT * FROM notify_channels ORDER BY id DESC') for r in rows: try: r['events'] = _json.loads(r['events'] or '[]') except Exception: r['events'] = [] return jsonify({'ok': True, 'data': rows}) @app.route('/api/channels/', methods=['PUT', 'DELETE']) @require_auth def channel_detail(cid): if request.method == 'DELETE': db.w('DELETE FROM notify_channels WHERE id=?', (cid,)) return jsonify({'ok': True}) d = request.get_json(force=True) fields = ['name', 'type', 'webhook', 'email', 'enabled'] sets, args = [], [] for f in fields: if f in d: sets.append(f'{f}=?') args.append(d[f]) if 'events' in d: sets.append('events=?') args.append(_json.dumps(d['events'] or [])) if sets: db.w(f'UPDATE notify_channels SET {", ".join(sets)} WHERE id=?', (*args, cid)) return jsonify({'ok': True}) @app.route('/api/channels//test', methods=['POST']) @require_auth def channel_test(cid): ch = db.q('SELECT * FROM notify_channels WHERE id=?', (cid,), one=True) if not ch: return jsonify({'ok': False, 'error': '渠道不存在'}), 404 ok, msg = notify.test_channel(ch) return jsonify({'ok': ok, 'msg': msg}) @app.route('/api/events') @require_auth def events(): return jsonify({'ok': True, 'data': [{'id': k, 'name': v} for k, v in notify.EVENTS.items()]}) # --------------------------------------------------------------------------- # 设置 # --------------------------------------------------------------------------- @app.route('/api/settings', methods=['GET', 'PUT']) @require_auth def settings(): if request.method == 'PUT': d = request.get_json(force=True) for k, v in d.items(): db.set_setting(k, v) if 'budget_alert_ratio' in d: config.BUDGET_ALERT_RATIO = float(d['budget_alert_ratio']) return jsonify({'ok': True}) return jsonify({'ok': True, 'data': { 'budget_alert_ratio': config.BUDGET_ALERT_RATIO, 'auth_enabled': auth_enabled(), 'email_configured': bool(config.EMAIL.get('host')), }}) # --------------------------------------------------------------------------- # 前端 # --------------------------------------------------------------------------- @app.route('/') def index(): return send_from_directory(app.static_folder, 'index.html') if __name__ == '__main__': print(f'AI Worker 项目管理平台启动: http://0.0.0.0:{config.PORT}') app.run(host=config.HOST, port=config.PORT, debug=False, threaded=True)