#!/usr/bin/env python3
"""任务管理系统 - 动态HTTP服务 (FastAPI + Jinja2) v3 PER+P2"""
import os, sys, json, subprocess, re, time
from datetime import datetime, timedelta, date
from pathlib import Path

DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, DIR)
from typing import List, Optional
from fastapi import FastAPI, Request, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import HTMLResponse, JSONResponse, FileResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
import uvicorn
import jinja2

from database import (init_db, get_db, add_task, get_all_tasks, get_task,
                       add_execution, get_config, set_config, get_balance_history,
                       add_requirement, get_requirements, get_requirement,
                       update_requirement_status, get_pending_requirements,
                       has_executing_requirement, get_next_waiting_requirement,
                       has_pending_requirements, get_queue_position)

init_db()
app = FastAPI(title="任务管理系统", docs_url="/api/docs")

def next_task_number():
    """生成下一个任务编号 NO.XXX"""
    db = get_db()
    row = db.execute("SELECT number FROM tasks WHERE number LIKE 'NO.%' ORDER BY id DESC LIMIT 1").fetchone()
    db.close()
    if row and row["number"]:
        try:
            n = int(row["number"].replace("NO.",""))
            return f"NO.{n+1:03d}"
        except: pass
    return "NO.001"

def auto_category(name, brief=""):
    """自动检测分类"""
    from category_skills import detect_category
    cat = detect_category(name, brief)
    if isinstance(cat, (list, tuple)):
        cat = cat[0]
    if cat == "其他" or not cat:
        return "编程"  # 默认
    return cat

def clean_slug(name, task_id=0):
    """生成干净URL: {id}-{英文名}, 无中文"""
    s = name.lower().strip()
    # 去除非ASCII（中文等）
    s = re.sub(r'[^\x00-\x7f]', '', s)
    # 去除非字母数字连字符
    s = re.sub(r'[^a-z0-9-]', '-', s)
    s = re.sub(r'-+', '-', s).strip('-')
    if not s: s = 'task'
    s = s[:40]
    return f"{task_id}-{s}" if task_id else s

def migrate_slugs():
    """迁移已有任务中带中文的slug到新格式"""
    db = get_db()
    rows = db.execute("SELECT id, name, slug FROM tasks").fetchall()
    count = 0
    for r in rows:
        slug = r['slug']
        # 检查是否含非ASCII
        if any(ord(c) > 127 for c in slug):
            new_slug = clean_slug(r['name'], r['id'])
            # 存储旧slug
            try:
                db.execute("ALTER TABLE tasks ADD COLUMN old_slug TEXT DEFAULT ''")
            except:
                pass
            db.execute("UPDATE tasks SET old_slug=?, slug=? WHERE id=?",
                       (slug, new_slug, r['id']))
            count += 1
    if count:
        db.commit()
        print(f"[migrate_slugs] 已迁移 {count} 个任务slug")
    db.close()

migrate_slugs()
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
app.mount("/static", StaticFiles(directory=os.path.join(DIR, "static")), name="static")
os.makedirs(os.path.join(DIR, "logs"), exist_ok=True)

def render(template_name, context):
    templates = jinja2.Environment(
        loader=jinja2.FileSystemLoader(os.path.join(DIR, "templates")),
        autoescape=False, enable_async=False)
    templates.filters["fromjson"] = json.loads
    def make_title(s):
        s = s.strip()
        if len(s) <= 100:
            return s[:30] + "..." if len(s) > 30 else s
        # >100 chars: 取前20字+首句+...
        first_sentence = ""
        for sep in "。！？.!?；;":
            idx = s.find(sep)
            if idx >= 0 and idx < 60:
                first_sentence = s[:idx+1]
                break
        if not first_sentence:
            first_sentence = s[:25]
        return first_sentence[:30]
    templates.filters["req_title"] = make_title
    return HTMLResponse(templates.get_template(template_name).render(context))

def calc_duration(start, end):
    if not start or not end: return ""
    try:
        s = datetime.strptime(start, "%Y-%m-%d %H:%M:%S")
        e = datetime.strptime(end, "%Y-%m-%d %H:%M:%S")
        d = (e - s).total_seconds()
        if d < 60: return f"{int(d)}s"
        if d < 3600: return f"{int(d//60)}m"
        return f"{int(d//3600)}h{int((d%3600)//60)}m"
    except: return ""

def get_stats(tasks):
    db = get_db()
    tc = db.execute("SELECT COALESCE(SUM(cost),0) FROM executions").fetchone()[0]
    td = db.execute("SELECT COALESCE(SUM(cost),0) FROM executions WHERE date(started_at)=date('now')").fetchone()[0]
    db.close()
    return {"total": len(tasks), "done": sum(1 for t in tasks if t.get("status")=="已完成"),
            "fail": sum(1 for t in tasks if "失败" in (t.get("status") or "")),
            "in_progress": sum(1 for t in tasks if t.get("status")=="进行中"),
            "total_cost": tc, "daily_cost": td}

# ===== Pages =====
@app.get("/", response_class=HTMLResponse)
def index(request: Request):
    tasks = get_all_tasks()
    # 执行者统计
    executor_stats = {}
    for t in tasks:
        ex = t.get("executor") or "华硕"
        if ex not in executor_stats:
            executor_stats[ex] = {"total": 0, "fail": 0, "completed": 0}
        executor_stats[ex]["total"] += 1
        if t.get("status") == "已完成":
            executor_stats[ex]["completed"] += 1
        elif "失败" in (t.get("status") or ""):
            executor_stats[ex]["fail"] += 1
    stats = get_stats(tasks)
    db = get_db()
    latest = db.execute("SELECT balance FROM balance_history ORDER BY recorded_at DESC LIMIT 1").fetchone()
    real_balance = latest["balance"] if latest else 0
    db.close()
    return render("index.html", {"request": request, "tasks": tasks, "stats": stats,
                   "now": datetime.now().strftime("%Y-%m-%d %H:%M"),
                   "balance": real_balance,
                   "executor_stats": executor_stats,
                   "daily_cost": round(stats["daily_cost"], 4)})

@app.get("/task/new", response_class=HTMLResponse)
def task_new_page(request: Request):
    return render("task_edit.html", {"request": request, "task": {}, "is_new": True,
                   "template_json": "{}", "api_url": "/api/task/create"})

@app.get("/task/{slug}", response_class=HTMLResponse)
def task_detail(request: Request, slug: str):
    if slug == "new": return HTMLResponse("", status_code=404)
    # 检查旧slug → 301重定向
    db = get_db()
    old_row = db.execute("SELECT slug FROM tasks WHERE old_slug=?", (slug,)).fetchone()
    if old_row:
        db.close()
        return RedirectResponse(url=f"/task/{old_row['slug']}", status_code=301)
    db.close()
    task = get_task(slug)
    if not task: return HTMLResponse("not found", status_code=404)
    db = get_db()
    rows = db.execute("SELECT * FROM executions WHERE task_id=? ORDER BY completed_at DESC LIMIT 50",
                      (task["id"],)).fetchall()
    execs = [dict(r) for r in rows]
    for e in execs: e["duration"] = calc_duration(e.get("started_at"), e.get("completed_at"))
    tc = sum(e.get("cost",0) or 0 for e in execs)
    # Real token stats from token_stats table (all time)
    ts = db.execute("SELECT COALESCE(SUM(total_input),0) as ti, COALESCE(SUM(total_output),0) as to_ FROM token_stats").fetchone()
    real_input = int(ts["ti"]) if ts else 0
    real_output = int(ts["to_"]) if ts else 0
    total_tokens = real_input + real_output
    # cache_hit: token_stats 中的 cache_read 数据不可靠(累计计数错误), 暂不显示
    cache_hit = "N/A"
    reqs = get_requirements(task["id"])
    all_t = get_all_tasks()
    prev=next_s=""
    for i,t in enumerate(all_t):
        if t["slug"]==slug:
            if i>0: prev=all_t[i-1]["slug"]
            if i<len(all_t)-1: next_s=all_t[i+1]["slug"]
            break
    return render("task_detail.html", {"request":request,"task":task,"executions":execs,
                   "task_cost":tc,"total_tokens":total_tokens,"cache_hit_rate":cache_hit,"requirements":reqs,"prev_slug":prev,"next_slug":next_s})

@app.get("/task/{slug}/edit", response_class=HTMLResponse)
def task_edit_page(request: Request, slug: str):
    task = get_task(slug)
    if not task: return HTMLResponse("not found", status_code=404)
    td = json.loads(task.get("template_data","{}")) if task.get("template_data") else {}
    return render("task_edit.html", {"request":request,"task":task,"is_new":False,
                   "template_json":json.dumps(td,ensure_ascii=False),
                   "api_url":"/api/task/"+slug+"/update"})

@app.get("/exec/{slug}/{exec_id}", response_class=HTMLResponse)
def exec_detail(request: Request, slug: str, exec_id: int):
    db = get_db()
    # 检查旧slug → 301重定向
    old_row = db.execute("SELECT slug FROM tasks WHERE old_slug=?", (slug,)).fetchone()
    if old_row:
        db.close()
        return RedirectResponse(url=f"/exec/{old_row['slug']}/{exec_id}", status_code=301)
    task = db.execute("SELECT * FROM tasks WHERE slug=?",(slug,)).fetchone()
    if not task:
        db.close()
        return HTMLResponse("not found",404)
    row = db.execute("SELECT * FROM executions WHERE id=?",(exec_id,)).fetchone()
    if not row: db.close(); return HTMLResponse("not found",404)
    ed = dict(row); ed["duration"]=calc_duration(ed.get("started_at"),ed.get("completed_at"))
    # 解析output_files JSON字符串为列表，同名文件合并多路径
    try:
        raw = json.loads(ed.get("output_files","[]") or "[]")
        groups = {}
        for f in raw:
            fname = f.get("path","").split("\\\\")[-1].split("/")[-1]
            if not fname: continue
            if fname in groups:
                p = f["path"]
                if p not in groups[fname]["paths"]:
                    groups[fname]["paths"].append(p)
            else:
                groups[fname] = dict(f)
                groups[fname]["paths"] = [f["path"]]
        ed["output_files_parsed"] = list(groups.values())
    except:
        ed["output_files_parsed"] = []
    db.close()
    return render("exec_detail.html",{"request":request,"task":dict(task),"execution":ed})

@app.get("/api/exec/{exec_id}/children")
def api_exec_children(exec_id: int):
    db = get_db()
    row = db.execute("SELECT requirement_id,task_id FROM executions WHERE id=?",(exec_id,)).fetchone()
    if not row: db.close(); return {"children":[]}
    rid = row['requirement_id']
    tid = row['task_id']
    # 查找所有 parent_req_id=该requirement的需求
    children = db.execute("SELECT * FROM requirements WHERE task_id=? AND parent_req_id=? ORDER BY created_at ASC",
                          (tid, str(rid))).fetchall()
    result = []
    for c in children:
        c_dict = dict(c)
        # 找子需求的最近执行
        ex = db.execute("SELECT id,status,result_brief,completed_at,output_files FROM executions WHERE task_id=? AND requirement_id=? ORDER BY id DESC LIMIT 1",
                        (tid, c['id'])).fetchone()
        if ex:
            c_dict['execution'] = dict(ex)
        result.append(c_dict)
    db.close()
    return {"children": result}

@app.get("/api/task/{slug}/latest-exec")
def api_latest_exec(slug: str):
    db = get_db()
    t = get_task(slug)
    if not t: raise HTTPException(404,"no task")
    row = db.execute("SELECT * FROM executions WHERE task_id=? ORDER BY id DESC LIMIT 1", (t["id"],)).fetchone()
    db.close()
    if not row: return {"id":0}
    e = dict(row)
    e["duration"] = calc_duration(e.get("started_at"),e.get("completed_at"))
    return e

@app.get("/executions", response_class=HTMLResponse)
def executions_page(request: Request):
    db = get_db()
    rows = db.execute("SELECT e.*,t.name task_name,t.slug task_slug FROM executions e "
                      "JOIN tasks t ON e.task_id=t.id ORDER BY e.completed_at DESC LIMIT 100").fetchall()
    db.close()
    return render("executions.html",{"request":request,"executions":[dict(r) for r in rows]})

@app.get("/kanban", response_class=HTMLResponse)
def kanban(request: Request):
    tasks = get_all_tasks()
    # 按状态分组
    groups = {"待处理":[],"进行中":[],"已完成":[],"其他":[]}
    for t in tasks:
        s = t.get("status","")
        if s in groups: groups[s].append(t)
        else: groups["其他"].append(t)
    return render("kanban.html",{"request":request,"tasks":tasks,"stats":get_stats(tasks),
                   "cols":groups})

@app.get("/quality", response_class=HTMLResponse)
def quality_page(request: Request, days: int = 30):
    cutoff = (datetime.now()-timedelta(days=days)).strftime("%Y-%m-%d")
    db = get_db()
    stats = db.execute("SELECT COUNT(*) total, SUM(validation_passed) passed, "
                       "SUM(CASE WHEN validation_passed=0 THEN 1 ELSE 0 END) failed "
                       "FROM quality_metrics WHERE measured_at>=?", (cutoff,)).fetchone()
    total_execs = stats["total"] or 0 if stats else 0
    passed = stats["passed"] or 0 if stats else 0
    failed = stats["failed"] or 0 if stats else 0
    pass_rate = round(passed/total_execs*100,1) if total_execs>0 else 100
    total_issues = db.execute("SELECT COUNT(*) ct FROM quality_metrics WHERE "
                              "validation_passed=0 AND measured_at>=?", (cutoff,)).fetchone()
    issues = db.execute("SELECT issues FROM quality_metrics WHERE validation_passed=0 "
                        "AND measured_at>=? ORDER BY measured_at DESC LIMIT 20", (cutoff,)).fetchall()
    top_issues = []
    for r in issues:
        try: top_issues.extend(json.loads(r["issues"] or "[]"))
        except: pass
    top_issues = top_issues[:10]
    db.close()
    return render("quality.html",{"request":request,"days":days,
                   "now":datetime.now().strftime("%Y-%m-%d %H:%M"),
                   "total_execs":total_execs,"passed":passed,"failed":failed,
                   "pass_rate":pass_rate,
                   "total_issues":total_issues["ct"] if total_issues else 0,
                   "top_issues":top_issues})

@app.get("/api/stats")
def api_stats():
    tasks = get_all_tasks()
    s = get_stats(tasks)
    db = get_db()
    latest = db.execute("SELECT balance FROM balance_history ORDER BY recorded_at DESC LIMIT 1").fetchone()
    db.close()
    s["balance"] = latest["balance"] if latest else 0
    s["total_cost"] = round(s["total_cost"], 4)
    s["daily_cost"] = round(s["daily_cost"], 4)
    return s

@app.get("/api/balance/range")
def api_balance_range(range: str = "7"):
    days = {"7":7,"30":30,"all":365}
    d = days.get(range, 7)
    # 从executions按天聚合真实消耗
    db = get_db()
    cutoff = (date.today() - timedelta(days=d)).isoformat()
    rows = db.execute("""SELECT date(started_at) as dt, COALESCE(SUM(cost),0) as daily_cost,
        COALESCE(SUM(tokens_used),0) as daily_tokens, COUNT(*) as execs
        FROM executions WHERE started_at >= ? AND cost > 0
        GROUP BY date(started_at) ORDER BY dt""", (cutoff,)).fetchall()
    # 拼合balance balance
    bal_rows = db.execute("SELECT recorded_at, balance FROM balance_history ORDER BY recorded_at ASC").fetchall()
    db.close()
    # 按天合并
    daily_map = {}
    for r in rows:
        daily_map[r["dt"]] = {"daily_cost": round(r["daily_cost"],4), "daily_tokens": r["daily_tokens"], "execs": r["execs"]}
    # 取每天最后一个balance
    bal_by_day = {}
    for r in bal_rows:
        dt = r["recorded_at"][:10]
        bal_by_day[dt] = r["balance"]
    history = []
    for dt in sorted(daily_map.keys()):
        entry = {"recorded_at": dt, "balance": bal_by_day.get(dt, 0),
                 "daily_cost": daily_map[dt]["daily_cost"],
                 "daily_tokens": daily_map[dt]["daily_tokens"]}
        history.append(entry)
    if not history:
        # 没executions数据时，用balance_history
        for r in bal_rows:
            dt = r["recorded_at"][:10]
            if dt not in [h["recorded_at"] for h in history]:
                history.append({"recorded_at": dt, "balance": r["balance"],
                                "daily_cost": 0, "daily_tokens": 0})
    history = list(reversed(history))
    max_cost = max((h["daily_cost"] for h in history), default=0) or 1
    return {"history": history, "max_cost": max_cost, "count": len(history)}

@app.get("/api/balance/hourly")
def api_balance_hourly():
    db = get_db()
    today = datetime.now().strftime("%Y-%m-%d")
    rows = db.execute("""SELECT substr(started_at,12,2) as hour,
        COALESCE(SUM(cost),0) as cost, COUNT(*) as execs
        FROM executions WHERE started_at >= ? AND cost > 0
        GROUP BY substr(started_at,12,2) ORDER BY hour""", (today,)).fetchall()
    db.close()
    labels = [f"{r['hour']}:00" for r in rows]
    costs = [round(r['cost'],4) for r in rows]
    return {"labels": labels, "costs": costs, "execs": [r['execs'] for r in rows]}

@app.get("/api/token-stats")
def api_token_stats(days: int = 1):
    from database import get_hermes_token_stats, get_db
    s = get_hermes_token_stats(days)
    # 还取token_stats表的历史记录
    db = get_db()
    rows = db.execute("SELECT * FROM token_stats WHERE recorded_at >= datetime('now','-1 day','localtime') ORDER BY id DESC LIMIT 288").fetchall()
    db.close()
    history = [dict(r) for r in rows]
    return {"stats": s, "history": history}

@app.get("/api/token-stats/record")
def api_token_stats_record():
    from database import add_token_stats
    s = add_token_stats()
    return s


@app.get("/balance", response_class=HTMLResponse)
def balance_page(request: Request, days: int = 7):
    db = get_db()
    total_cost = db.execute("SELECT COALESCE(SUM(cost),0) FROM executions").fetchone()[0]
    total_tokens = db.execute("SELECT COALESCE(SUM(tokens_used),0) FROM executions").fetchone()[0]
    # 从balance_history取最新余额
    latest = db.execute("SELECT balance FROM balance_history ORDER BY recorded_at DESC LIMIT 1").fetchone()
    balance = latest["balance"] if latest else 0
    # 今日消耗实时
    today = datetime.now().strftime("%Y-%m-%d")
    daily = db.execute("SELECT SUM(cost) as dc FROM executions WHERE started_at >= ?", (today,)).fetchone()
    daily_cost = round(daily["dc"], 4) if daily and daily["dc"] else 0.0
    c = db.execute("SELECT SUM(CASE WHEN status='已完成' THEN 1 ELSE 0 END) completed, COUNT(*) total FROM tasks")
    stats_row = c.fetchone()
    db.close()
    stats = {"total": stats_row["total"] or 0, "completed": stats_row["completed"] or 0} if stats_row else {"total": 0, "completed": 0}
    # 从executions按天聚合
    db2 = get_db()
    cutoff = (date.today() - timedelta(days=days)).isoformat()
    rows = db2.execute("""SELECT date(started_at) as dt, COALESCE(SUM(cost),0) as dc,
        COALESCE(SUM(tokens_used),0) as dtok FROM executions WHERE started_at >= ? AND cost > 0
        GROUP BY date(started_at) ORDER BY dt""", (cutoff,)).fetchall()
    bal_rows = db2.execute("SELECT recorded_at, balance FROM balance_history ORDER BY recorded_at ASC").fetchall()
    db2.close()
    daily_map = {}
    for r in rows: daily_map[r["dt"]] = {"daily_cost": round(r["dc"],4), "daily_tokens": r["dtok"]}
    bal_by_day = {}
    for r in bal_rows: bal_by_day[r["recorded_at"][:10]] = r["balance"]
    history = []
    for dt in sorted(daily_map.keys()):
        history.append({"recorded_at": dt, "balance": bal_by_day.get(dt, 0),
                        "daily_cost": daily_map[dt]["daily_cost"],
                        "daily_tokens": daily_map[dt]["daily_tokens"]})
    if not history:
        for r in bal_rows:
            dt = r["recorded_at"][:10]
            if dt not in [h["recorded_at"] for h in history]:
                history.append({"recorded_at": dt, "balance": r["balance"], "daily_cost": 0, "daily_tokens": 0})
    history = list(reversed(history))
    max_cost = max((h["daily_cost"] for h in history), default=0) or 1
    return render("balance.html",{"request":request,"history":history,
                   "balance":balance,"days":days,"now":datetime.now().strftime("%Y-%m-%d %H:%M"),
                   "daily_cost":daily_cost,"total_cost":total_cost,"total_tokens":total_tokens,
                   "stats":stats,"max_cost":max_cost})

# ===== Health =====
@app.get("/api/health")
def api_health():
    return {"status":"ok","time":datetime.now().isoformat()}

@app.get("/api/requirements/pending")
def api_pending_requirements():
    """获取所有待处理需求"""
    return get_pending_requirements()

@app.get("/api/requirements/due-scheduled")
def api_due_scheduled_requirements():
    """获取并激活已到期的定时需求"""
    from database import get_due_scheduled_requirements
    due = get_due_scheduled_requirements()
    if due:
        db = get_db()
        for r in due:
            db.execute("UPDATE requirements SET status='waiting', scheduled_at='' WHERE id=?", (r["id"],))
        db.commit()
        db.close()
    return due

# ===== Task CRUD =====
class TC(BaseModel):
    name: str; slug: str; priority: str = "normal"; category: str = ""; brief: str = ""

@app.post("/api/task")
def api_create_task(tc: TC):
    from database import add_task as db_add
    slug = tc.slug.strip()
    if not slug or not tc.name.strip(): raise HTTPException(400, "bad")
    db = get_db()
    ex = db.execute("SELECT id FROM tasks WHERE slug=?",(slug,)).fetchone()
    db.close()
    if ex: raise HTTPException(409, "exists")
    num = next_task_number()
    db_add(tc.name,slug,number=num,priority=tc.priority,category=tc.category,brief=tc.brief)
    # 自动绑定分类对应技能
    from category_skills import get_recommended_skills
    skills = get_recommended_skills(tc.name, brief=tc.brief or "", manual_category=tc.category or "")
    if skills:
        from database import update_task
        update_task(slug, skills=json.dumps(skills, ensure_ascii=False))
    return {"success":True,"slug":slug}

@app.put("/api/task/{slug}")
def api_update_task(slug: str, data: dict):
    from database import update_task as db_up
    task = get_task(slug)
    if not task: raise HTTPException(404, "no")
    kw = {}
    for k in ["name","number","status","priority","category","brief","note","work_folder",
              "executor","task_type","is_script","script_path"]:
        if k in data and data[k]: kw[k] = data[k]
    if kw: db_up(slug, **kw)
    return {"success":True,"slug":slug,"message":"ok"}

@app.post("/api/task/{slug}/pin")
def api_pin_task(slug: str):
    db = get_db()
    task = db.execute("SELECT id FROM tasks WHERE slug=?",(slug,)).fetchone()
    if not task: db.close(); raise HTTPException(404,"no")
    now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    db.execute("UPDATE tasks SET is_pinned=1,pinned_at=? WHERE id=?",(now,task["id"]))
    db.commit(); db.close()
    return {"success":True,"slug":slug,"pinned_at":now}

@app.post("/api/task/{slug}/unpin")
def api_unpin_task(slug: str):
    db = get_db()
    task = db.execute("SELECT id FROM tasks WHERE slug=?",(slug,)).fetchone()
    if not task: db.close(); raise HTTPException(404,"no")
    db.execute("UPDATE tasks SET is_pinned=0,pinned_at='' WHERE id=?",(task["id"],))
    db.commit(); db.close()
    return {"success":True,"slug":slug}

@app.delete("/api/task/{slug}")
def api_delete_task(slug: str):
    db = get_db()
    task = db.execute("SELECT id FROM tasks WHERE slug=?",(slug,)).fetchone()
    if not task: db.close(); raise HTTPException(404,"no")
    tid = task["id"]
    # 先删子表，最后删tasks（按FK约束顺序）
    for t in ["quality_metrics","execution_plans","executions","requirements","lessons_learned"]:
        db.execute("DELETE FROM "+t+" WHERE task_id=?", (tid,))
    db.execute("DELETE FROM tasks WHERE id=?",(tid,))
    db.commit(); db.close()
    return {"success":True,"slug":slug}

# ===== P2 Script + Template =====
@app.post("/api/task/create")
def api_task_create_v2(data: dict):
    from database import add_task_v2
    name = (data.get("name") or "").strip()
    if not name: raise HTTPException(400,"no name")
    # 先以name为基础创建临时slug
    temp_slug = re.sub(r'[^\x00-\x7f\w-]', '', name.lower().replace(' ','-'))[:30] or "task"
    temp_slug += "-" + str(int(time.time()))
    tpl = {}
    rt = data.get("template")
    if rt:
        try: tpl = json.loads(rt) if isinstance(rt,str) else rt
        except: pass
    brief = data.get("brief","")
    cat = data.get("category","") or auto_category(name, brief)
    num = next_task_number()
    add_task_v2(name, temp_slug, number=num, task_type=data.get("task_type",""),
                is_script=data.get("is_script",""), script_path=data.get("script_path",""),
                template_data=tpl, category=cat,
                brief=brief, priority=data.get("priority","normal"),
                status=data.get("status","pending"))
    # 获取自增ID → 生成干净slug
    db = get_db()
    row = db.execute("SELECT id FROM tasks WHERE slug=?", (temp_slug,)).fetchone()
    if row:
        new_slug = clean_slug(name, row["id"])
        db.execute("UPDATE tasks SET slug=? WHERE id=?", (new_slug, row["id"]))
        db.commit()
        slug = new_slug
    db.close()
    return {"success":True,"slug":slug}

@app.post("/api/task/{slug}/update")
def api_task_update_v2(slug: str, data: dict):
    from database import update_task
    kw = {}
    for k in ["name","status","priority","category","brief","task_type","is_script","script_path"]:
        if k in data: kw[k] = data[k]
    rt = data.get("template")
    if rt:
        try: kw["template_data"] = json.dumps(
            json.loads(rt) if isinstance(rt,str) else rt, ensure_ascii=False)
        except: pass
    if kw: update_task(slug, **kw)
    return {"success":True,"slug":slug}

@app.post("/api/task/{slug}/script/run")
def api_script_run(slug: str):
    from script_runner import run_script
    return run_script(slug)

@app.post("/api/execution/{exec_id}/retry")
def api_script_retry(exec_id: int):
    from script_runner import retry_exec
    return retry_exec(exec_id)

@app.get("/api/scripts/list")
def api_scripts_list():
    from script_runner import list_scripts
    return {"scripts": list_scripts()}

@app.post("/api/task/{slug}/template")
def api_save_template(slug: str, data: dict):
    db = get_db()
    db.execute("UPDATE tasks SET template_data=?,updated_at=datetime('now','localtime') WHERE slug=?",
               (json.dumps(data,ensure_ascii=False),slug))
    db.commit(); db.close()
    return {"success":True,"slug":slug,"template":data}

@app.get("/api/task/{slug}/template")
def api_get_template(slug: str):
    db = get_db()
    row = db.execute("SELECT template_data FROM tasks WHERE slug=?",(slug,)).fetchone()
    db.close()
    if not row: raise HTTPException(404,"no")
    return {"slug":slug,"template":json.loads(row["template_data"] or "{}")}

# ===== Skills =====
@app.put("/api/task/{slug}/skills")
async def api_upd_skills(slug: str, request: Request):
    from category_skills import CATEGORY_SKILLS
    body = await request.json()
    skills = body.get("skills",[])
    auto = body.get("auto_detect",False)
    db = get_db()
    task = db.execute("SELECT id,category FROM tasks WHERE slug=?",(slug,)).fetchone()
    if not task: db.close(); raise HTTPException(404,"no")
    if auto: skills = CATEGORY_SKILLS.get(task["category"] or "",[])
    sjson = json.dumps(skills,ensure_ascii=False)
    db.execute("UPDATE tasks SET skills=?,updated_at=datetime('now','localtime') WHERE slug=?",(sjson,slug))
    db.commit(); db.close()
    return {"success":True,"skills":skills,"slug":slug}

@app.get("/api/task/{slug}/skills")
def api_get_skills(slug: str):
    from category_skills import CATEGORY_SKILLS, detect_category
    task = get_task(slug)
    if not task: raise HTTPException(404,"no")
    cur = json.loads(task.get("skills") or "[]")
    rec = CATEGORY_SKILLS.get(task.get("category") or "",[])
    ac = detect_category(task["name"]+" "+(task.get("brief","")))
    return {"current":cur,"recommended":rec,"auto_category":ac,"task_category":task.get("category","")}

# ====== 分类知识库 API ======

@app.get("/api/category-knowledge/{category}")
def api_cat_knowledge(category: str, item_type: str = None):
    from database import get_category_knowledge
    return get_category_knowledge(category, item_type)

@app.post("/api/category-knowledge")
def api_upsert_knowledge(data: dict):
    from database import upsert_category_knowledge
    upsert_category_knowledge(
        data.get("category",""), data.get("item_type","skill"),
        data.get("title",""), data.get("content",""),
        data.get("ref_link",""), int(data.get("sort_order",0)),
        data.get("id")
    )
    return {"success":True}

# ====== 分类管理 API ======
@app.get("/api/categories")
def api_list_categories():
    """返回所有已知分类（从category_knowledge表+任务已有分类去重）"""
    db = get_db()
    from_db = db.execute("SELECT DISTINCT category FROM category_knowledge WHERE category!='' AND is_active=1").fetchall()
    from_tasks = db.execute("SELECT DISTINCT category FROM tasks WHERE category!='' AND category IS NOT NULL").fetchall()
    db.close()
    cats = set()
    for r in from_db: cats.add(r[0])
    for r in from_tasks: cats.add(r[0])
    if "其他" in cats: cats.remove("其他")
    cats = sorted(cats) + ["其他"]
    return {"categories": cats}

@app.post("/api/categories")
def api_add_category(data: dict):
    """添加新分类"""
    name = (data.get("name") or "").strip()
    if not name: raise HTTPException(400, "分类名不能为空")
    db = get_db()
    exists = db.execute("SELECT id FROM category_knowledge WHERE category=? LIMIT 1", (name,)).fetchone()
    db.close()
    if not exists:
        from database import upsert_category_knowledge, ensure_category_slug
        upsert_category_knowledge(name, "note", f"{name}分类", "新建分类，请补充知识条目", "", 0)
    from database import ensure_category_slug
    slug = ensure_category_slug(name)
    return {"success": True, "category": name, "slug": slug}

@app.delete("/api/categories")
def api_delete_category(data: dict):
    """删除分类（从category_knowledge清理）+ 重命名已有任务的分类为'其他'"""
    name = (data.get("name") or "").strip()
    if not name: raise HTTPException(400, "分类名不能为空")
    from database import get_db as gdb2
    db = gdb2()
    db.execute("DELETE FROM category_knowledge WHERE category=?", (name,))
    db.execute("UPDATE tasks SET category='其他' WHERE category=?", (name,))
    db.commit(); db.close()
    return {"success": True}

@app.post("/api/categories/rename")
def api_rename_category(data: dict):
    """重命名分类（更新category_knowledge+任务）"""
    old = (data.get("old_name") or "").strip()
    new = (data.get("new_name") or "").strip()
    if not old or not new: raise HTTPException(400, "参数不完整")
    from database import get_db as gdb3
    db = gdb3()
    db.execute("UPDATE category_knowledge SET category=? WHERE category=?", (new, old))
    db.execute("UPDATE tasks SET category=? WHERE category=?", (new, old))
    db.commit(); db.close()
    return {"success": True, "old": old, "new": new}

@app.delete("/api/category-knowledge/{kid}")
def api_del_knowledge(kid: int):
    from database import delete_category_knowledge
    delete_category_knowledge(kid)
    return {"success":True}

@app.get("/api/category/{category}/skills-binding")
def api_cat_skills_binding(category: str):
    from database import get_category_skills_binding
    return get_category_skills_binding(category) or {"category":category,"skills":[],"force_load":1}

@app.put("/api/category/{category}/skills-binding")
def api_update_skills_binding(category: str, data: dict):
    from database import update_category_skills_binding
    update_category_skills_binding(category, data.get("skills",[]))
    return {"success":True,"category":category}

@app.get("/category-knowledge/{slug}", response_class=HTMLResponse)
def cat_knowledge_page(slug: str, request: Request):
    from database import get_category_knowledge, get_category_skills_binding, get_category_by_slug
    # slug→中文分类名反查
    slug_info = get_category_by_slug(slug)
    if slug_info:
        category = slug_info["category"]
    else:
        # 兼容旧路径：直接传中文分类名
        category = slug
        from database import ensure_category_slug
        slug = ensure_category_slug(category)
    items = get_category_knowledge(category)
    binding = get_category_skills_binding(category)
    return render("category_knowledge.html",{
        "request":request, "category":category, "slug":slug,
        "items":items, "binding":binding or {"skills":[]},
        "item_types":["skill","experience","note","template","script","rule","knowledge_graph"]
    })

@app.get("/api/categories/list")
def api_categories():
    from database import get_db, get_category_slug_map
    slug_map = get_category_slug_map()
    default_icons = {"软著":"📝","专利":"💡","编程":"💻","部署":"🚀","运营":"📈","法律":"⚖️","合同":"📄","GEO":"🌐","电商":"🛒","其他":"📋"}
    # 从DB读取所有有slug的分类 + 任务中出现的分类
    db = get_db()
    from_csb = db.execute("SELECT category, slug FROM category_skills_binding WHERE slug!=''").fetchall()
    from_tasks = db.execute("SELECT DISTINCT category FROM tasks WHERE category!='' AND category IS NOT NULL").fetchall()
    from_ck = db.execute("SELECT DISTINCT category FROM category_knowledge WHERE category!='' AND is_active=1").fetchall()
    db.close()
    seen = set()
    result = []
    for row in from_csb:
        cat = row["category"]
        if cat not in seen:
            seen.add(cat)
            result.append({"category": cat, "slug": row["slug"], "icon": default_icons.get(cat, "📋")})
    for source in [from_tasks, from_ck]:
        for row in source:
            cat = row["category"] if isinstance(row, dict) else row[0]
            if cat and cat not in seen:
                seen.add(cat)
                slug = slug_map.get(cat, "")
                if not slug:
                    from database import ensure_category_slug
                    slug = ensure_category_slug(cat)
                result.append({"category": cat, "slug": slug, "icon": default_icons.get(cat, "📋")})
    return result

@app.get("/api/tasks")
def api_tasks():
    return [dict(t) for t in get_all_tasks()]

@app.get("/api/requirements/{slug}/queue")
def api_queue(slug: str):
    db = get_db()
    task = db.execute("SELECT id FROM tasks WHERE slug=?",(slug,)).fetchone()
    if not task: db.close(); raise HTTPException(404,"no")
    ex = db.execute("SELECT r.*,t.slug ts,t.name tn FROM requirements r JOIN tasks t "
                    "ON r.task_id=t.id WHERE r.task_id=? AND r.status='executing' LIMIT 1",
                    (task["id"],)).fetchone()
    w = db.execute("SELECT r.*,t.slug ts,t.name tn FROM requirements r JOIN tasks t "
                   "ON r.task_id=t.id WHERE r.task_id=? AND r.status IN ('waiting','queued') ORDER BY r.id",
                   (task["id"],)).fetchall()
    c = db.execute("SELECT COUNT(*) ct FROM requirements WHERE task_id=? AND status='completed'",
                   (task["id"],)).fetchone()
    f = db.execute("SELECT COUNT(*) ct FROM requirements WHERE task_id=? AND status='failed'",
                   (task["id"],)).fetchone()
    db.close()
    return {"task_slug":slug,"executing":dict(ex) if ex else None,
            "queue_length":len(w),"queue":[dict(r) for r in w],
            "total_completed":c["ct"]if c else 0,"total_failed":f["ct"]if f else 0}

@app.get("/api/task/{slug}/subtasks")
def api_subtasks(slug: str):
    db = get_db()
    task = db.execute("SELECT id FROM tasks WHERE slug=?",(slug,)).fetchone()
    if not task: db.close(); return {"subtasks":[]}
    s = db.execute("SELECT id,slug,name,status,priority,category,created_at,executor "
                   "FROM tasks WHERE parent_task_id=? ORDER BY created_at",(task["id"],)).fetchall()
    db.close()
    return {"subtasks":[dict(r) for r in s]}

@app.post("/api/task/{slug}/subtask")
def api_create_subtask(slug: str, data: dict):
    name = (data.get("name") or "").strip()
    brief = (data.get("brief") or "").strip()
    if not name: raise HTTPException(400,"no name")
    db = get_db()
    p = db.execute("SELECT id FROM tasks WHERE slug=?",(slug,)).fetchone()
    if not p: db.close(); raise HTTPException(404,"no parent")
    ss = slug + "-sub-" + str(int(time.time()*1000)%10000)
    db.execute("INSERT INTO tasks (name,slug,status,priority,category,brief,parent_task_id,created_at) "
               "VALUES (?,?,'pending','normal','',?,?,datetime('now','localtime'))",
               (name,ss,brief,p["id"]))
    db.commit(); db.close()
    return {"success":True,"slug":ss,"name":name}

# ===== Webhooks =====
@app.post("/api/requirements/batch")
def api_batch_requirements(data: dict):
    slug = (data.get("slug") or "").strip()
    items = data.get("items") or []
    if not slug or not items: raise HTTPException(400,"need slug+items")
    task = get_task(slug)
    if not task: raise HTTPException(404,"no task")
    tid = task["id"]
    from database import add_requirement as ar
    ids = []
    immediate_ids = []
    from datetime import datetime, timedelta
    now = datetime.now()
    for item in items:
        c = (item.get("content") or "").strip()
        d = item.get("delay_hours")
        s = item.get("schedule_at")
        pr = item.get("parent_req_id")
        if not c: continue
        # 计算实际执行时间
        scheduled_at = ""
        status = "waiting"
        if d:
            dt = now + timedelta(hours=d)
            scheduled_at = dt.strftime("%Y-%m-%d %H:%M:%S")
            status = "scheduled"
        elif s:
            try:
                dt = datetime.strptime(s.replace("T"," ")[:16], "%Y-%m-%d %H:%M")
                scheduled_at = dt.strftime("%Y-%m-%d %H:%M:%S")
                status = "scheduled"
            except:
                pass
        rich = json.dumps({"delay_hours":d,"schedule_at":s,"scheduled_at":scheduled_at,"batch_order":True,"parent_req_id":pr}) if d or s or pr else ""
        ar(tid, c, rich, status=status, scheduled_at=scheduled_at, parent_req_id=pr)
        r = get_db().execute("SELECT id FROM requirements WHERE task_id=? ORDER BY id DESC LIMIT 1",(tid,)).fetchone()
        if r:
            ids.append(r["id"])
            # queued_only=true → 不入 immediate, 仅排队
            if item.get("queued_only"):
                get_db().execute("UPDATE requirements SET status='queued' WHERE id=?", (r["id"],))
            elif status == "waiting":
                immediate_ids.append(r["id"])
    get_db().commit()
    # 立即触发执行
    trigger_script = os.path.join(os.path.dirname(os.path.abspath(__file__)), "trigger_execute.py")
    for rid in immediate_ids:
        from datetime import datetime
        log_ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        print(f"[{log_ts}] BATCH_TRIGGER: req #{rid} task={slug}", flush=True)
        subprocess.Popen(
            [sys.executable, trigger_script, str(rid), slug],
            stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
        )
    return {"success":True,"count":len(ids),"ids":ids}

@app.post("/api/webhook/requirement")
def api_create_req(data: dict):
    slug = (data.get("task_slug") or "").strip()
    content = (data.get("content") or "").strip()
    rich = data.get("rich_content","")
    serial = data.get("is_serial",True)
    if not slug or not content: raise HTTPException(400,"bad")
    task = get_task(slug)
    if not task: raise HTTPException(404,"no")
    add_requirement(task["id"],content,rich)
    db = get_db()
    r = db.execute("SELECT id FROM requirements WHERE task_id=? ORDER BY id DESC LIMIT 1",
                   (task["id"],)).fetchone()
    rid = r["id"] if r else None
    ex = db.execute("SELECT id FROM requirements WHERE task_id=? AND status='executing' LIMIT 1",
                    (task["id"],)).fetchone()
    st = "queued" if ex and serial else "waiting"
    if st == "queued": db.execute("UPDATE requirements SET status='queued' WHERE id=?",(rid,))
    db.commit(); db.close()
    return {"success":True,"requirement_id":rid,"status":st}

@app.get("/api/task/{slug}/metadata")
def api_get_metadata(slug: str):
    """Get all metadata sections for a task"""
    task = get_task(slug)
    if not task: raise HTTPException(404,"no task")
    db = get_db()
    rows = db.execute("SELECT section, content, updated_at FROM task_metadata WHERE task_id=? ORDER BY section",
                      (task["id"],)).fetchall()
    db.close()
    return {"sections": [dict(r) for r in rows]}

@app.get("/api/task/{slug}/metadata/{section}")
def api_get_metadata_section(slug: str, section: str):
    """Get one metadata section"""
    task = get_task(slug)
    if not task: raise HTTPException(404,"no task")
    db = get_db()
    r = db.execute("SELECT section, content, updated_at FROM task_metadata WHERE task_id=? AND section=?",
                   (task["id"], section)).fetchone()
    db.close()
    if not r: raise HTTPException(404,"section not found")
    return dict(r)

@app.post("/api/task/{slug}/metadata/{section}")
def api_upsert_metadata(slug: str, section: str, data: dict):
    """Upsert a metadata section (JSON content)"""
    content = data.get("content",{})
    if not isinstance(content, dict):
        content = {"text": str(content)}
    task = get_task(slug)
    if not task: raise HTTPException(404,"no task")
    db = get_db()
    now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    db.execute(
        "INSERT INTO task_metadata (task_id, section, content, updated_at) VALUES (?,?,?,?) "
        "ON CONFLICT(task_id,section) DO UPDATE SET content=excluded.content, updated_at=excluded.updated_at",
        (task["id"], section, json.dumps(content, ensure_ascii=False), now))
    db.commit()
    db.close()
    return {"success":True, "section": section}

@app.delete("/api/task/{slug}/metadata/{section}")
def api_delete_metadata(slug: str, section: str):
    """Delete a metadata section"""
    task = get_task(slug)
    if not task: raise HTTPException(404,"no task")
    db = get_db()
    db.execute("DELETE FROM task_metadata WHERE task_id=? AND section=?", (task["id"], section))
    db.commit()
    db.close()
    return {"success":True}

@app.post("/api/task/{slug}/execute-next")
def api_execute_next(slug: str):
    """Trigger next queued requirement sequentially"""
    task = get_task(slug)
    if not task: raise HTTPException(404,"no task")
    db = get_db()
    # Check if any requirement is already executing
    executing = db.execute(
        "SELECT id FROM requirements WHERE task_id=? AND status='executing' LIMIT 1",
        (task["id"],)).fetchone()
    if executing:
        db.close()
        return {"success":True, "triggered":0, "total":0, "detail":"已有执行中的需求#"+str(executing["id"])+"，等待其完成"}
    # Get all queued/waiting requirements in order
    pending = db.execute(
        "SELECT id, content FROM requirements WHERE task_id=? AND status IN ('queued','waiting') ORDER BY id",
        (task["id"],)).fetchall()
    if not pending:
        db.close()
        return {"success":True, "triggered":0, "total":0, "detail":"无排队需求"}
    # Trigger the first one
    first = pending[0]
    db.execute("UPDATE requirements SET status='waiting' WHERE id=?", (first["id"],))
    db.commit()
    db.close()
    # Spawn executor for first requirement (sequential: subsequent will auto-trigger via existing queue)
    trigger_script = os.path.join(os.path.dirname(os.path.abspath(__file__)), "trigger_execute.py")
    subprocess.Popen(
        [sys.executable, trigger_script, str(first["id"]), slug],
        stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
    )
    return {"success":True, "triggered":1, "total":len(pending), "requirement_id":first["id"], "content":first["content"][:60]}

@app.post("/api/webhook/task/execute")
def api_exec_submit(data: dict):
    slug = data.get("slug","")
    brief = data.get("result_brief","")
    log = data.get("log_html","")
    files = data.get("output_files",[])
    rid = data.get("requirement_id",0)
    st = data.get("status","done")
    task = get_task(slug)
    if not task: raise HTTPException(404,"no")
    db = get_db()
    db.execute("INSERT INTO executions (task_id,status,result_brief,log_html,output_files,"
               "requirement_id,started_at,completed_at) VALUES (?,?,?,?,?,?,"
               "datetime('now','localtime'),datetime('now','localtime'))",
               (task["id"],st,brief,log,json.dumps(files,ensure_ascii=False),rid))
    eid = db.execute("SELECT last_insert_rowid()").fetchone()[0]
    if rid:
        success_states = ("done", "完成", "completed", "成功", "success")
        db.execute("UPDATE requirements SET status=? WHERE id=?",
                   ("completed" if st in success_states else "failed", rid))
    # 触发下一排队需求（顺序执行：每个完成自动触发下一个）
    nxt = db.execute("SELECT id FROM requirements WHERE task_id=? AND status='queued' ORDER BY id LIMIT 1",
                     (task["id"],)).fetchone()
    if nxt:
        db.execute("UPDATE requirements SET status='waiting' WHERE id=?",(nxt["id"],))
        trigger_script = os.path.join(os.path.dirname(os.path.abspath(__file__)), "trigger_execute.py")
        subprocess.Popen(
            [sys.executable, trigger_script, str(nxt["id"]), slug],
            stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
        )
    # 全部需求完成 → 更新任务状态
    remaining = db.execute(
        "SELECT COUNT(*) as c FROM requirements WHERE task_id=? AND status IN ('waiting','queued','executing')",
        (task["id"],)).fetchone()
    if remaining and remaining["c"] == 0:
        all_done = db.execute(
            "SELECT COUNT(*) as c FROM requirements WHERE task_id=? AND status='completed'",
            (task["id"],)).fetchone()
        total_r = db.execute(
            "SELECT COUNT(*) as c FROM requirements WHERE task_id=?",
            (task["id"],)).fetchone()
        if all_done and total_r and all_done["c"] == total_r["c"]:
            db.execute("UPDATE tasks SET status='已完成', updated_at=datetime('now','localtime') WHERE id=?", (task["id"],))
        else:
            db.execute("UPDATE tasks SET status='执行失败', updated_at=datetime('now','localtime') WHERE id=?", (task["id"],))
    db.commit(); db.close()
    return {"success":True,"execution_id":eid}

@app.post("/api/task/{slug}/retry")
def api_retry(slug: str, data: dict = None):
    task = get_task(slug)
    if not task: raise HTTPException(404,"no")
    rid = data.get("requirement_id",0) if data else 0
    db = get_db()
    if rid:
        db.execute("UPDATE requirements SET status='waiting' WHERE id=? AND task_id=?",(rid,task["id"]))
    else:
        for f in db.execute("SELECT id FROM requirements WHERE task_id=? AND status='failed' ORDER BY id",
                            (task["id"],)).fetchall():
            db.execute("UPDATE requirements SET status='waiting' WHERE id=?",(f["id"],))
    db.commit(); db.close()
    return {"success":True,"message":"retry set"}

@app.post("/api/task/{slug}/retry-failed")
def api_retry_failed(slug: str):
    return api_retry(slug, {"requirement_id":0})

# ===== Quality =====
@app.get("/api/quality/trend")
def api_qt(days: int = 30):
    cutoff = (datetime.now()-timedelta(days=days)).strftime("%Y-%m-%d")
    db = get_db()
    r = db.execute("SELECT date(measured_at) day,COUNT(*) total,SUM(validation_passed) passed,"
                   "SUM(file_verified+url_verified) files_ok FROM quality_metrics "
                   "WHERE measured_at>=? GROUP BY date(measured_at) ORDER BY day",(cutoff,)).fetchall()
    db.close()
    return {"trend":[dict(x) for x in r],"days":days}

@app.get("/api/quality/per-task")
def api_qpt(days: int = 30):
    cutoff = (datetime.now()-timedelta(days=days)).strftime("%Y-%m-%d")
    db = get_db()
    r = db.execute("SELECT qm.task_id,t.name,t.slug,t.category,COUNT(*) tc,"
                   "SUM(validation_passed) passed,SUM(qm.file_verified+qm.url_verified) fok,"
                   "AVG(qm.file_verified+qm.url_verified) af "
                   "FROM quality_metrics qm JOIN tasks t ON qm.task_id=t.id "
                   "WHERE qm.measured_at>=? GROUP BY qm.task_id ORDER BY tc DESC",(cutoff,)).fetchall()
    res = []
    for x in r:
        d=dict(x)
        d["pass_rate"]=round(d["passed"]/d["tc"]*100,1) if d["tc"]>0 else 100
        d["avg_files"]=round(d["af"] or 0,1)
        res.append(d)
    db.close()
    return {"tasks":res}

@app.get("/api/quality/recent-issues")
def api_qri(days: int = 30, limit: int = 20):
    cutoff = (datetime.now()-timedelta(days=days)).strftime("%Y-%m-%d")
    db = get_db()
    r = db.execute("SELECT qm.*,t.name tn,t.slug ts FROM quality_metrics qm JOIN tasks t "
                   "ON qm.task_id=t.id WHERE qm.validation_passed=0 AND qm.measured_at>=? "
                   "ORDER BY qm.measured_at DESC LIMIT ?",(cutoff,limit)).fetchall()
    db.close()
    return {"issues":[dict(x) for x in r]}

# ===== Lessons + Plans =====
@app.post("/api/plan")
def api_save_plan(data: dict):
    from database import save_plan
    rid = data.get("requirement_id",0)
    slug = data.get("task_slug","")
    steps = data.get("steps",[])
    skills = data.get("skills_needed",[])
    est = data.get("estimated_steps",1)
    if not slug: raise HTTPException(400,"no slug")
    task = get_task(slug)
    if not task: raise HTTPException(404,"no task")
    pj = {"steps":steps,"skills_needed":skills,"estimated_steps":est}
    pid = save_plan(rid,task["id"],pj)
    score = 0; issues = []
    if steps:
        score += min(len(steps)*30,60)
        for s in steps:
            if s.get("deliverable"): score+=10
            else: issues.append("step "+str(s.get("step","?"))+" no deliverable")
            if s.get("risk"): score+=5
        if skills: score+=10
        else: issues.append("no skills")
        if est>=len(steps): score+=10
        score = min(score,100)
    pj["quality_score"]=score; pj["quality_issues"]=issues
    db = get_db()
    db.execute("UPDATE execution_plans SET plan_json=? WHERE id=?",
               (json.dumps(pj,ensure_ascii=False),pid))
    db.commit(); db.close()
    return {"success":True,"plan_id":pid,"quality_score":score,
            "quality_issues":issues,"message":"plan saved"}

@app.post("/api/webhook/execute-requirement")
def api_exec_req(data: dict):
    rid = data.get("requirement_id",0)
    slug = data.get("slug","")
    if rid:
        update_requirement_status(rid,"executing")
        db = get_db()
        db.execute("UPDATE tasks SET status='running' WHERE slug=?",(slug,))
        db.commit(); db.close()
    return {"success":True,"message":"marked"}

@app.post("/api/file/register")
def api_reg_file(data: dict):
    eid = data.get("execution_id",0)
    files = data.get("files",[])
    if not eid or not files: raise HTTPException(400,"bad")
    db = get_db()
    ex = db.execute("SELECT output_files FROM executions WHERE id=?",(eid,)).fetchone()
    cur = json.loads(ex["output_files"] or "[]") if ex else []
    cur.extend(files)
    db.execute("UPDATE executions SET output_files=? WHERE id=?",
               (json.dumps(cur,ensure_ascii=False),eid))
    db.commit(); db.close()
    return {"success":True,"registered":len(files)}

@app.get("/api/file/{slug}/{execution_id}/{file_index}")
def api_file_download(slug: str, execution_id: int, file_index: int):
    """下载/预览输出文件"""
    task = get_task(slug)
    if not task: raise HTTPException(404,"no task")
    db = get_db()
    ex = db.execute("SELECT output_files,requirement_id FROM executions WHERE id=? AND task_id=?",
                    (execution_id,task["id"])).fetchone()
    db.close()
    if not ex: raise HTTPException(404,"no exec")
    files = json.loads(ex["output_files"] or "[]")
    if file_index < 0 or file_index >= len(files): raise HTTPException(404,"bad index")
    f = files[file_index]
    path = f["path"]
    # 转换Windows路径
    if path[1:3] == ":/":
        lp = "/mnt/" + path[0].lower() + path[2:].replace("\\","/")
    else:
        lp = path.replace("\\","/")
    if not os.path.exists(lp):
        # 尝试文件名查找
        fname = os.path.basename(lp)
        lp2 = os.path.join(os.path.dirname(os.path.abspath(__file__)),"static",fname)
        if os.path.exists(lp2): lp = lp2
        else:
            for sd in ["/tmp","/var/www/tasks/static"]:
                p = os.path.join(sd, fname)
                if os.path.exists(p): lp = p; break
    if not os.path.exists(lp): raise HTTPException(404,"file not found")
    ext = lp.rsplit(".",1)[-1].lower() if "." in lp else ""
    mime = {"png":"image/png","jpg":"image/jpeg","jpeg":"image/jpeg","gif":"image/gif",
            "webp":"image/webp","svg":"image/svg+xml","pdf":"application/pdf",
            "docx":"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
            "txt":"text/plain","html":"text/html","htm":"text/html",
            "md":"text/markdown","json":"application/json"}.get(ext,"application/octet-stream")
    return FileResponse(lp, media_type=mime)

@app.post("/api/lessons")
def api_save_lesson(data: dict):
    from database import add_lesson
    slug = data.get("task_slug","")
    cat = data.get("category","general")
    text = data.get("lesson_text","")
    if not text or len(text.strip())<10: raise HTTPException(400,"too short")
    db = get_db()
    task = db.execute("SELECT id FROM tasks WHERE slug=?",(slug,)).fetchone()
    db.close()
    tid = task["id"] if task else None
    lid = add_lesson(tid,cat,text)
    return {"success":True,"lesson_id":lid}

@app.get("/api/task/{slug}/lessons")
def api_lessons(slug: str):
    db = get_db()
    task = db.execute("SELECT id,category FROM tasks WHERE slug=?",(slug,)).fetchone()
    if not task: db.close(); return {"lessons":[],"related":[],"task_category":""}
    l = db.execute("SELECT * FROM lessons_learned WHERE task_id=? ORDER BY created_at DESC LIMIT 20",
                   (task["id"],)).fetchall()
    cat = task["category"] or ""
    rel = []
    if cat:
        r = db.execute("SELECT ll.*,t.name task_name FROM lessons_learned ll JOIN tasks t "
                       "ON ll.task_id=t.id WHERE ll.category=? AND ll.task_id!=? "
                       "ORDER BY ll.created_at DESC LIMIT 10",(cat,task["id"])).fetchall()
        rel = [dict(x) for x in r]
    db.close()
    return {"lessons":[dict(x) for x in l],"related":rel,"task_category":cat}

@app.get("/api/task/{slug}/related-tasks")
def api_related(slug: str):
    db = get_db()
    task = db.execute("SELECT category FROM tasks WHERE slug=?",(slug,)).fetchone()
    if not task or not task["category"]: db.close(); return {"tasks":[]}
    r = db.execute("SELECT id,name,slug,status,category,brief FROM tasks WHERE category=? "
                   "AND slug!=? ORDER BY updated_at DESC LIMIT 10",
                   (task["category"],slug)).fetchall()
    db.close()
    return {"tasks":[dict(x) for x in r]}

@app.get("/api/task/{slug}/plans-list")
def api_plans(slug: str):
    db = get_db()
    task = db.execute("SELECT id FROM tasks WHERE slug=?",(slug,)).fetchone()
    if not task: db.close(); return {"plans":[]}
    r = db.execute("SELECT ep.id plan_id,ep.plan_json,ep.created_at,r.content req_content "
                   "FROM execution_plans ep LEFT JOIN requirements r ON ep.requirement_id=r.id "
                   "WHERE ep.task_id=? ORDER BY ep.created_at DESC LIMIT 10",(task["id"],)).fetchall()
    plans = []
    for x in r:
        p=dict(x)
        try: p["plan"]=json.loads(p["plan_json"]) if isinstance(p["plan_json"],str) else p["plan_json"]
        except: p["plan"]={}
        plans.append(p)
    db.close()
    return {"plans":plans}

@app.post("/api/task/{slug}/categorize")
def api_categorize(slug: str):
    from category_skills import detect_category
    from database import update_task
    task = get_task(slug)
    if not task: raise HTTPException(404,"no")
    text = task["name"]+" "+(task.get("brief",""))+" "+(task.get("note",""))
    cat = detect_category(text)
    if cat and cat!="other": update_task(slug,category=cat)
    return {"success":True,"slug":slug,"category":cat}

@app.post("/api/task/quick-create")
def api_quick_create(data: dict):
    from database import add_task as db_add
    name = (data.get("name") or "").strip()
    slug = (data.get("slug") or name.lower().replace(" ","-").replace("_","-")[:50]).strip()
    if not name: raise HTTPException(400,"no name")
    client = (data.get("client") or "").strip()
    folder = (data.get("work_folder") or "").strip()
    if not folder: folder = "D:/ai-tasks/"+slug
    executor = data.get("executor") or "local"
    from category_skills import detect_category
    cat = detect_category(name+" "+client)
    ef = cat if cat!="other" else (data.get("category") or "")
    brief = ("client: "+client+"\n"+name) if client else name
    brief = ("client: "+client+"\n"+name) if client else name
    if folder: brief += "\nwork_folder: "+folder
    return {"success":True,"slug":slug,"category":ef}

# ===== KG API =====
KG_BASE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data", "kg")

@app.post("/api/kg/build/{slug}")
def api_kg_build(slug: str):
    try:
        from kg_sync import build_task_kg
        result = build_task_kg(slug)
        if result.get("success"):
            return {"success": True, "message": f"构建完成: {result.get('nodes',0)} nodes, {result.get('edges',0)} edges"}
        return {"success": False, "message": result.get("error", "构建失败")}
    except Exception as e:
        return {"success": False, "message": str(e)}

@app.get("/api/kg/status/{slug}")
def api_kg_status(slug: str):
    try:
        from kg_sync import status as kg_status
        s = kg_status(slug)
        return {
            "graph_exists": s.get("graph_exists", False),
            "nodes": s.get("nodes", 0),
            "edges": s.get("edges", 0),
            "raw_files": s.get("raw_files", 0),
        }
    except Exception as e:
        return {"graph_exists": False, "nodes": 0, "edges": 0, "raw_files": 0, "error": str(e)}

@app.get("/api/kg/nodes/{slug}")
def api_kg_nodes(slug: str):
    graph_path = os.path.join(KG_BASE, slug, "graphify-out", "graph.json")
    if not os.path.exists(graph_path):
        return {"nodes": []}
    try:
        with open(graph_path) as f:
            g = json.load(f)
        return {"nodes": g.get("nodes", [])}
    except:
        return {"nodes": []}

@app.get("/api/kg/edges/{slug}")
def api_kg_edges(slug: str):
    graph_path = os.path.join(KG_BASE, slug, "graphify-out", "graph.json")
    if not os.path.exists(graph_path):
        return {"edges": []}
    try:
        with open(graph_path) as f:
            g = json.load(f)
        edges = g.get("edges", g.get("links", []))
        return {"edges": edges}
    except:
        return {"edges": []}

@app.get("/api/kg/report/{slug}")
def api_kg_report(slug: str):
    out_dir = os.path.join(KG_BASE, slug, "graphify-out")
    report_path = os.path.join(out_dir, "GRAPH_REPORT.md")
    if os.path.exists(report_path):
        with open(report_path) as f:
            return {"report": f.read()[:3000]}
    # fallback: 生成简单摘要
    graph_path = os.path.join(out_dir, "graph.json")
    if os.path.exists(graph_path):
        try:
            with open(graph_path) as f:
                g = json.load(f)
            nodes = len(g.get("nodes", []))
            edges = len(g.get("edges", g.get("links", [])))
            raw_path = os.path.join(KG_BASE, slug, "raw")
            files = len(os.listdir(raw_path)) if os.path.exists(raw_path) else 0
            return {"report": f"## 知识图谱概要\n\n- 节点数: {nodes}\n- 关系数: {edges}\n- 源文件数: {files}\n- 构建时间: {datetime.now().strftime('%Y-%m-%d %H:%M')}"}
        except:
            pass
    return {"report": "请先构建知识图谱（点击「构建知识图谱」按钮）"}

@app.post("/api/code-index/build/{slug}")
def api_code_build(slug: str):
    """构建 chromadb 代码索引"""
    try:
        from code_index import build_index
        r = build_index(slug)
        return r
    except Exception as e:
        return {"success": False, "error": str(e)}

@app.get("/api/code-index/status/{slug}")
def api_code_status(slug: str):
    """查看代码索引状态"""
    try:
        from code_index import status
        s = status(slug)
        return s
    except Exception as e:
        return {"indexed": False, "error": str(e)}

@app.get("/api/code-index/search/{slug}")
def api_code_search(slug: str, q: str = "", n: int = 5):
    """语义搜索代码"""
    if not q:
        return {"results": [], "error": "need q param"}
    try:
        from code_index import search
        r = search(slug, q, n)
        return r
    except Exception as e:
        return {"results": [], "error": str(e)}

@app.get("/api/kg/auto-build")
def api_kg_auto_build():
    """增量构建KG — 只构建最近24h有更新的任务，最多5个"""
    from database import get_db
    db = get_db()
    try:
        cutoff = (datetime.now() - timedelta(hours=24)).strftime("%Y-%m-%d %H:%M:%S")
        rows = db.execute(
            "SELECT slug, name FROM tasks WHERE updated_at>=? AND status NOT IN ('已归档') ORDER BY updated_at DESC LIMIT 5",
            (cutoff,)
        ).fetchall()
        db.close()
        if not rows:
            return {"success": True, "message": "最近24h无更新任务", "built": 0, "results": []}
        from kg_sync import build_task_kg
        results = []
        for r in rows:
            try:
                res = build_task_kg(r["slug"])
                results.append({"slug": r["slug"], "name": r["name"],
                                "success": res.get("success", False),
                                "nodes": res.get("nodes", 0),
                                "edges": res.get("edges", 0)})
            except Exception as e:
                results.append({"slug": r["slug"], "name": r["name"],
                                "success": False, "error": str(e)})
        return {"success": True, "message": f"构建了 {len(results)} 个任务KG",
                "built": len(results), "results": results}
    except Exception as e:
        return {"success": False, "message": str(e), "built": 0, "results": []}

@app.post("/api/kg/build-category/{slug}")
def api_kg_build_category(slug: str):
    """构建分类知识图谱"""
    try:
        from kg_sync import build_category_kg
        result = build_category_kg(slug)
        if result.get("success"):
            return {"success": True, "message": f"分类KG构建完成: {result.get('nodes',0)} nodes, {result.get('edges',0)} edges", "detail": result}
        return {"success": False, "message": result.get("error", "构建失败")}
    except Exception as e:
        return {"success": False, "message": str(e)}

@app.post("/api/kg/build-system")
def api_kg_build_system():
    """构建任务管理系统自身的知识图谱"""
    try:
        from kg_sync import build_system_kg
        result = build_system_kg()
        if result.get("success"):
            return {"success": True, "message": f"系统KG构建完成: {result.get('nodes',0)} nodes, {result.get('edges',0)} edges", "detail": result}
        return {"success": False, "message": result.get("error", "构建失败")}
    except Exception as e:
        return {"success": False, "message": str(e)}

@app.get("/api/kg/report-system")
def api_kg_report_system():
    """获取系统KG报告"""
    graph_path = os.path.join(KG_BASE, "task-system", "graphify-out", "graph.json")
    if not os.path.exists(graph_path):
        return {"success": False, "report": "尚未构建"}
    try:
        with open(graph_path, "r", encoding="utf-8") as f:
            g = json.load(f)
        nodes = g.get("nodes", [])
        edges = g.get("edges", g.get("links", []))
        lines = [f"# 任务管理系统 · 知识图谱报告", f"节点: {len(nodes)} | 关系: {len(edges)}", ""]
        # 按类型分组节点
        by_type = {}
        for n in nodes:
            t = n.get("type", "unknown")
            by_type.setdefault(t, []).append(n)
        for t, ns in sorted(by_type.items()):
            lines.append(f"## {t} ({len(ns)})")
            for n in ns[:20]:
                label = n.get("label", n.get("name", n.get("id", "")))
                desc = n.get("description", "")[:100]
                lines.append(f"  - {label}" + (f": {desc}" if desc else ""))
            if len(ns) > 20:
                lines.append(f"  ... 共{len(ns)}个")
            lines.append("")
        # 关系统计
        by_rel = {}
        for e in edges:
            r = e.get("type", e.get("relation", "unknown"))
            by_rel.setdefault(r, []).append(e)
        lines.append(f"## 关系类型 ({len(by_rel)})")
        for r, es in sorted(by_rel.items(), key=lambda x: -len(x[1])):
            lines.append(f"  - {r}: {len(es)}条")
        return {"success": True, "report": "\n".join(lines)}
    except Exception as e:
        return {"success": False, "report": f"读取失败: {e}"}

@app.get("/api/kg/report-category/{slug}")
def api_kg_report_category(slug: str):
    """获取分类KG报告"""
    graph_path = os.path.join(KG_BASE, f"cat-{slug}", "graphify-out", "graph.json")
    if not os.path.exists(graph_path):
        return {"success": False, "report": "尚未构建"}
    try:
        with open(graph_path, "r", encoding="utf-8") as f:
            g = json.load(f)
        nodes = g.get("nodes", [])
        edges = g.get("edges", g.get("links", []))
        lines = [f"# 分类KG · {slug}", f"节点: {len(nodes)} | 关系: {len(edges)}", ""]
        by_type = {}
        for n in nodes:
            t = n.get("type", "unknown")
            by_type.setdefault(t, []).append(n)
        for t, ns in sorted(by_type.items()):
            lines.append(f"## {t} ({len(ns)})")
            for n in ns[:15]:
                label = n.get("label", n.get("name", n.get("id", "")))
                desc = n.get("description", "")[:120]
                lines.append(f"  - {label}" + (f": {desc}" if desc else ""))
            if len(ns) > 15:
                lines.append(f"  ... 共{len(ns)}个")
            lines.append("")
        by_rel = {}
        for e in edges:
            r = e.get("type", e.get("relation", "unknown"))
            by_rel.setdefault(r, []).append(e)
        lines.append(f"## 关系类型 ({len(by_rel)})")
        for r, es in sorted(by_rel.items(), key=lambda x: -len(x[1])):
            lines.append(f"  - {r}: {len(es)}条")
        return {"success": True, "report": "\n".join(lines)}
    except Exception as e:
        return {"success": False, "report": f"读取失败: {e}"}

# ===== 执行详情API =====
@app.get("/api/exec/{exec_id}/detail")
def api_exec_detail(exec_id: int):
    db = get_db()
    row = db.execute("SELECT * FROM executions WHERE id=?", (exec_id,)).fetchone()
    if not row: db.close(); return {"plan":{}, "quality":{}}
    ed = dict(row)
    task_id = ed["task_id"]
    # 找计划
    plan_row = db.execute("SELECT plan_json FROM execution_plans WHERE requirement_id=? AND task_id=? ORDER BY created_at DESC LIMIT 1",
                          (ed.get("requirement_id"), task_id)).fetchone()
    plan = json.loads(plan_row["plan_json"]) if plan_row else {}
    # 找质量
    quality_row = db.execute("SELECT * FROM quality_metrics WHERE execution_id=? LIMIT 1", (exec_id,)).fetchone()
    quality = dict(quality_row) if quality_row else {}
    if quality and quality.get("issues") and isinstance(quality["issues"], str):
        try: quality["issues"] = json.loads(quality["issues"])
        except: quality["issues"] = []
    db.close()
    return {"plan": plan, "quality": quality}

# ===== Dell在线检测 =====
@app.get("/api/dell/health")
def api_dell_health():
    import time as _tdell
    try:
        _t0 = _tdell.time()
        r = subprocess.run(
            ["ssh", "-i", os.path.expanduser("~/.ssh/hermes-remote-key"),
             "-p", "2222", "-o", "StrictHostKeyChecking=no",
             "-o", "ConnectTimeout=5", "-o", "BatchMode=yes",
             "fangwenhe@192.168.1.21",
             "hermes --version 2>/dev/null || echo 'no-hermes'; echo '---'; python3 --version 2>/dev/null"],
            capture_output=True, text=True, timeout=10
        )
        _latency = int((_tdell.time() - _t0) * 1000)
        if r.returncode == 0:
            lines = r.stdout.strip().split('\n')
            version = lines[0] if lines else ""
            return {"online": True, "latency_ms": _latency, "hermes_version": version}
        return {"online": False, "error": f"ssh exit={r.returncode}"}
    except subprocess.TimeoutExpired:
        return {"online": False, "error": "连接超时"}
    except Exception as e:
        return {"online": False, "error": str(e)}

# ===== 快速创建(兼容旧API路径) =====
@app.post("/api/task/quick")
def api_quick_old(data: dict):
    content = (data.get("content") or "").strip()
    if not content: return {"success": False, "message": "内容为空"}
    name = (data.get("name") or content.split('\n')[0][:40]).strip()
    import time as _tslug
    # 临时slug
    temp_slug = re.sub(r'[^\x00-\x7f\w-]', '', name.lower().replace(' ','-'))[:30] or "task"
    temp_slug += "-" + str(int(_tslug.time()))
    client = (data.get("client") or "").strip()
    folder = (data.get("folder") or "D:/ai-tasks/"+temp_slug).strip()
    executor = data.get("executor","华硕")
    category = data.get("category","") or auto_category(name, content[:100])
    num = next_task_number()
    from database import add_task_v2 as db_add_v2
    db_add_v2(name=name, slug=temp_slug, status="待处理", priority="普通", number=num,
              brief=f"client: {client}\n{content[:200]}" if client else content[:200],
              work_folder=folder, executor=executor, category=category)
    # 获取ID → 更新slug
    from database import get_db as gdb
    db = gdb()
    row = db.execute("SELECT id FROM tasks WHERE slug=?", (temp_slug,)).fetchone()
    if row:
        tid = row["id"]
        new_slug = clean_slug(name, tid)
        db.execute("UPDATE tasks SET slug=?, work_folder=? WHERE id=?",
                   (new_slug, folder.replace(temp_slug, new_slug, 1), tid))
        db.commit()
        slug = new_slug
    db.close()
    from database import get_task as gt
    task_data = gt(slug)
    if not task_data: return {"success": False, "message": "创建失败(查找)"}
    req_id = add_requirement(task_data["id"], content)
    # 绑定实体
    bind_entity = data.get("bind_entity")
    if bind_entity:
        from database import bind_task_entity
        bind_task_entity(task_data["id"], int(bind_entity))
    subprocess.Popen(
        [sys.executable, os.path.join(DIR, "trigger_execute.py"), str(req_id), slug],
        stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
    )
    return {"success": True, "slug": slug, "task_id": task_data["id"], "requirement_id": req_id}

@app.post("/api/task/quick-init")
def api_quick_init(data: dict):
    """创建+初始化（不执行），用户审查后手动执行"""
    content = (data.get("content") or "").strip()
    if not content: return {"success": False, "message": "内容为空"}
    name = (data.get("name") or content.split('\n')[0][:40]).strip()
    import time as _tslug
    temp_slug = re.sub(r'[^\x00-\x7f\w-]', '', name.lower().replace(' ','-'))[:30] or "task"
    temp_slug += "-" + str(int(_tslug.time()))
    client = (data.get("client") or "").strip()
    category = data.get("category","") or auto_category(name, content[:100])
    executor = data.get("executor","华硕")
    num = next_task_number()
    # 工作文件夹: 编程→WSL, 其他→D:\projects\
    work_folder = data.get("folder","")
    if not work_folder:
        if category == "编程":
            work_folder = f"/home/fangwenhe/projects/{temp_slug}"
        else:
            work_folder = f"D:\\projects\\{temp_slug}"
    from database import add_task_v2 as db_add_v2
    db_add_v2(name=name, slug=temp_slug, status="待处理", priority="普通", number=num,
              brief=f"client: {client}\n{content[:200]}" if client else content[:200],
              work_folder=work_folder, executor=executor, category=category)
    # 获取ID → 更新slug
    from database import get_db as gdb
    db = gdb()
    row = db.execute("SELECT id FROM tasks WHERE slug=?", (temp_slug,)).fetchone()
    if row:
        tid = row["id"]
        new_slug = clean_slug(name, tid)
        db.execute("UPDATE tasks SET slug=?, work_folder=? WHERE id=?",
                   (new_slug, work_folder.replace(temp_slug, new_slug, 1), tid))
        db.commit()
        slug = new_slug
    db.close()
    # 写入需求
    from database import add_requirement as add_req, get_task as gt
    task_data = gt(slug)
    if not task_data: return {"success": False, "message": "创建失败(查找)"}
    req_id = add_req(task_data["id"], content)
    # 绑定实体
    bind_entity = data.get("bind_entity")
    if bind_entity:
        from database import bind_task_entity
        bind_task_entity(task_data["id"], int(bind_entity))
    # 自动分析技能 & 保存
    from category_skills import get_recommended_skills
    skills = get_recommended_skills(name, manual_category=category)
    if skills:
        from database import update_task
        update_task(slug, skills=json.dumps(skills, ensure_ascii=False))
    # 创建工作文件夹（用更新后的路径）
    task_data2 = get_task(slug)
    final_folder = task_data2.get("work_folder","") if task_data2 else work_folder
    try:
        if final_folder.startswith("/"):
            os.makedirs(final_folder, exist_ok=True)
        else:
            win_path = final_folder.replace("\\", "/").replace("D:", "/mnt/d")
            os.makedirs(win_path, exist_ok=True)
    except Exception as e:
        pass  # 文件夹创建非致命
    return {"success": True, "slug": slug, "task_id": task_data["id"], "requirement_id": req_id, "mode": "init"}

# ===== DAG (任务依赖关系) =====
@app.get("/dag", response_class=HTMLResponse)
def dag_page(request: Request):
    return render("dag.html", {"request": request,
                   "tasks": get_all_tasks(),
                   "now": datetime.now().strftime("%Y-%m-%d %H:%M")})

@app.get("/api/dag")
def api_dag():
    tasks = get_all_tasks()
    nodes = []
    edges = []
    task_map = {t["slug"]: t for t in tasks}
    for t in tasks:
        nodes.append({"id": t["slug"], "label": t["name"][:20],
                       "status": t.get("status", "待处理"),
                       "category": t.get("category", ""),
                       "priority": t.get("priority", "普通")})
        deps = json.loads(t.get("dependencies") or "[]")
        for d_slug in deps:
            if d_slug in task_map:
                edges.append({"from": d_slug, "to": t["slug"],
                              "label": "依赖"})
    return {"nodes": nodes, "edges": edges}

@app.get("/api/task/{slug}/dependencies")
def api_task_deps(slug: str):
    task = get_task(slug)
    if not task:
        raise HTTPException(404)
    deps = json.loads(task.get("dependencies") or "[]")
    all_tasks = get_all_tasks()
    available = [{"slug": t["slug"], "name": t["name"], "status": t.get("status","")}
                 for t in all_tasks if t["slug"] != slug and t["slug"] not in deps]
    return {"dependencies": deps, "available": available}

@app.post("/api/task/{slug}/dependencies")
def api_set_deps(slug: str, data: dict):
    task = get_task(slug)
    if not task:
        raise HTTPException(404)
    deps = data.get("dependencies", [])
    db = get_db()
    db.execute("UPDATE tasks SET dependencies=? WHERE slug=?", (json.dumps(deps), slug))
    db.commit()
    db.close()
    return {"success": True, "dependencies": deps}

# ===== SLA面板 =====
@app.get("/sla", response_class=HTMLResponse)
def sla_page(request: Request, days: int = 30):
    return render("sla.html", {"request": request,
                   "sla": get_sla_data(days),
                   "days": days,
                   "now": datetime.now().strftime("%Y-%m-%d %H:%M")})

@app.get("/api/sla")
def api_sla(days: int = 30):
    return get_sla_data(days)

def get_sla_data(days=30):
    cutoff = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")
    db = get_db()
    # 整体统计
    rows = db.execute("""
        SELECT task_id, t.name as task_name, t.slug, t.status as task_status,
               COUNT(*) as exec_count,
               SUM(CASE WHEN e.status LIKE '%完成%' THEN 1 ELSE 0 END) as success_count,
               AVG(CASE WHEN e.completed_at AND e.started_at
                   THEN (julianday(e.completed_at) - julianday(e.started_at)) * 86400
                   ELSE NULL END) as avg_duration_sec,
               MIN(CASE WHEN e.completed_at AND e.started_at
                   THEN (julianday(e.completed_at) - julianday(e.started_at)) * 86400
                   ELSE NULL END) as min_duration_sec,
               MAX(CASE WHEN e.completed_at AND e.started_at
                   THEN (julianday(e.completed_at) - julianday(e.started_at)) * 86400
                   ELSE NULL END) as max_duration_sec,
               SUM(CASE WHEN e.status LIKE '%失败%' OR e.status LIKE '%超时%' THEN 1 ELSE 0 END) as fail_count,
               SUM(CASE WHEN e.status LIKE '%超时%' THEN 1 ELSE 0 END) as timeout_count
        FROM executions e
        JOIN tasks t ON e.task_id = t.id
        WHERE e.started_at >= ? OR e.completed_at >= ?
        GROUP BY task_id
        ORDER BY exec_count DESC
    """, (cutoff, cutoff)).fetchall()
    db.close()
    result = []
    for r in rows:
        d = dict(r)
        total = d["exec_count"] or 1
        d["success_rate"] = round((d["success_count"] or 0) / total * 100, 1)
        d["fail_rate"] = round((d["fail_count"] or 0) / total * 100, 1)
        d["avg_duration_sec"] = round(d["avg_duration_sec"] or 0, 1)
        d["min_duration_sec"] = round(d["min_duration_sec"] or 0, 1)
        d["max_duration_sec"] = round(d["max_duration_sec"] or 0, 1)
        result.append(d)
    return {"tasks": result, "total_days": days}

# ===== Main =====

@app.get("/api/auto-retry/check")
def api_auto_retry_check():
    """Find tasks with failed requirements and non-empty category."""
    db = get_db()
    rows = db.execute("""
        SELECT DISTINCT t.slug, t.name, t.category, COUNT(r.id) as failed_count
        FROM tasks t JOIN requirements r ON r.task_id = t.id
        WHERE r.status = 'failed' AND t.category != ''
        GROUP BY t.id
        ORDER BY t.id
    """).fetchall()
    db.close()
    return [dict(r) for r in rows]

# ===== 实体数字化 API =====
@app.get("/entities/app", response_class=HTMLResponse)
def entities_app_page(request: Request):
    from database import get_all_entities as gae
    ents = [e for e in gae() if e.get("entity_type") == "app"]
    return render("entity_apps.html", {"request": request, "entities": ents,
                   "now": datetime.now().strftime("%Y-%m-%d %H:%M")})

@app.get("/entities/website", response_class=HTMLResponse)
def entities_website_page(request: Request):
    from database import get_all_entities as gae
    ents = [e for e in gae() if e.get("entity_type") == "website"]
    return render("entity_websites.html", {"request": request, "entities": ents,
                   "now": datetime.now().strftime("%Y-%m-%d %H:%M")})

@app.get("/entities/client", response_class=HTMLResponse)
def entities_client_page(request: Request):
    from database import get_all_entities as gae
    ents = [e for e in gae() if e.get("entity_type") == "client"]
    ents.sort(key=lambda x: x.get("created_at",""), reverse=True)
    return render("entity_clients.html", {"request": request, "entities": ents,
                   "now": datetime.now().strftime("%Y-%m-%d %H:%M")})

@app.get("/entities/system", response_class=HTMLResponse)
def entities_system_page(request: Request):
    from database import get_all_entities as gae
    ents = [e for e in gae() if e.get("entity_type") == "system"]
    return render("entity_systems.html", {"request": request, "entities": ents,
                   "now": datetime.now().strftime("%Y-%m-%d %H:%M")})

@app.get("/entities/n8n", response_class=HTMLResponse)
def entities_n8n_page(request: Request):
    from database import get_all_entities as gae
    ents = [e for e in gae() if e.get("entity_type") == "n8n"]
    return render("entity_n8n.html", {"request": request, "entities": ents,
                   "now": datetime.now().strftime("%Y-%m-%d %H:%M")})

@app.get("/entities", response_class=HTMLResponse)
def entities_page(request: Request):
    from database import get_all_entities as gae
    all_ents = gae()
    cnt_app = sum(1 for e in all_ents if e.get("entity_type")=="app")
    cnt_web = sum(1 for e in all_ents if e.get("entity_type")=="website")
    cnt_client = sum(1 for e in all_ents if e.get("entity_type")=="client")
    cnt_system = sum(1 for e in all_ents if e.get("entity_type")=="system")
    cnt_n8n = sum(1 for e in all_ents if e.get("entity_type")=="n8n")
    return render("entities.html", {"request": request, "entities": all_ents,
                   "cnt_app": cnt_app, "cnt_web": cnt_web, "cnt_client": cnt_client, "cnt_system": cnt_system, "cnt_n8n": cnt_n8n,
                   "now": datetime.now().strftime("%Y-%m-%d %H:%M")})

@app.get("/api/entities")
def api_list_entities(etype: str = ""):
    from database import get_all_entities as gae, get_entities_by_type as gebt
    if etype: return gebt(etype)
    return gae()

@app.post("/api/entities")
def api_create_entity(data: dict):
    from database import create_entity as ce, get_db as gdb
    etype = (data.get("type") or "").strip()
    eid = (data.get("entity_id") or "").strip()
    name = (data.get("name") or "").strip()
    if not etype or not name: raise HTTPException(400, "type and name required")
    if not eid: eid = ""
    extra = data.get("extra") or {}
    extra_data = data.get("extra_data")

    # 重复检查：website按domain，system按system_url
    if etype == "website":
        domain = (extra.get("domain") or "").strip()
        if domain:
            conn = gdb()
            dup = conn.execute(
                "SELECT e.id, e.name FROM entities e "
                "JOIN entity_websites ew ON e.id=ew.entity_id "
                "WHERE ew.domain=?", (domain,)
            ).fetchone()
            conn.close()
            if dup:
                raise HTTPException(400, "域名 " + domain + " 已存在实体【" + str(dup[1]) + "】(#" + str(dup[0]) + ")，不能重复添加")
    elif etype == "system":
        sys_url = (extra.get("system_url") or "").strip()
        if sys_url:
            conn = gdb()
            dup = conn.execute(
                "SELECT e.id, e.name FROM entities e "
                "JOIN entity_systems es ON e.id=es.entity_id "
                "WHERE es.system_url=?", (sys_url,)
            ).fetchone()
            conn.close()
            if dup:
                raise HTTPException(400, "系统URL " + sys_url + " 已存在实体【" + str(dup[1]) + "】(#" + str(dup[0]) + ")，不能重复添加")

    ent_id = ce(etype, eid, name, data.get("note",""), extra, extra_data)
    conn = gdb()
    seq = conn.execute("SELECT type_seq FROM entities WHERE id=?", (ent_id,)).fetchone()[0]
    conn.close()
    return {"success": True, "id": ent_id, "entity_id": eid, "display_id": f"{etype}-{seq}"}

@app.get("/api/entities/{eid}")
def api_get_entity(eid: int):
    from database import get_entity as ge
    ent = ge(eid)
    if not ent: raise HTTPException(404, "no")
    return ent

@app.put("/api/entities/{eid}")
def api_update_entity(eid: int, data: dict):
    from database import update_entity as ue
    ue(eid, data.get("name"), data.get("note"), data.get("extra"))
    return {"success": True}

# ===== 实体重新分类 =====
TYPE_SUB_MAP = {
    "website": "entity_websites",
    "system": "entity_systems",
    "app": "entity_apps",
    "client": "entity_clients",
    "n8n": "entity_n8n",
}

def _get_sub_row(conn, eid, sub_table):
    return conn.execute(f"SELECT * FROM {sub_table} WHERE entity_id=?", (eid,)).fetchone()

def _delete_sub(conn, eid, sub_table):
    conn.execute(f"DELETE FROM {sub_table} WHERE entity_id=?", (eid,))

def _insert_sub_system(conn, eid, system_url):
    conn.execute(
        "INSERT INTO entity_systems (entity_id, system_type, system_url, status) VALUES (?,?,?,?)",
        (eid, "其他", system_url or "", "运营中")
    )

def _insert_sub_website(conn, eid, domain):
    conn.execute(
        "INSERT INTO entity_websites (entity_id, domain) VALUES (?,?)",
        (eid, domain or "")
    )

@app.post("/api/entities/{eid}/reclassify")
def api_reclassify_entity(eid: int, data: dict):
    from database import get_db
    new_type = (data.get("new_type") or "").strip()
    if new_type not in TYPE_SUB_MAP:
        raise HTTPException(400, "无效的实体类型: " + str(new_type))

    conn = get_db()
    ent = conn.execute("SELECT id, name, entity_type FROM entities WHERE id=?", (eid,)).fetchone()
    if not ent:
        conn.close()
        raise HTTPException(404, "实体不存在")
    old_type = ent["entity_type"]
    if old_type == new_type:
        conn.close()
        raise HTTPException(400, "实体已经是 " + new_type + " 类型")

    old_sub = TYPE_SUB_MAP.get(old_type)
    new_sub = TYPE_SUB_MAP.get(new_type)

    # 提取当前子表数据用于创建新记录
    domain = ""
    system_url = ""
    old_data = {}

    if old_sub:
        row = _get_sub_row(conn, eid, old_sub)
        if row:
            old_data = dict(row)
            domain = str(old_data.get("domain", "") or "")
            system_url = str(old_data.get("system_url", "") or "")
        # 删除旧子表
        _delete_sub(conn, eid, old_sub)

    # 创建新子表
    if new_type == "website":
        d = domain or system_url.replace("https://", "").replace("http://", "").split("/")[0] if system_url else ""
        _insert_sub_website(conn, eid, d)
    elif new_type == "system":
        u = system_url or ("https://" + domain if domain else "")
        _insert_sub_system(conn, eid, u)
    elif new_type in ("app", "n8n"):
        pass  # 这些类型不需要额外子表数据
    elif new_type == "client":
        pass

    # 更新entity_type
    conn.execute("UPDATE entities SET entity_type=? WHERE id=?", (new_type, eid))
    # 重新分配type_seq
    max_seq = conn.execute("SELECT COALESCE(MAX(type_seq),0) FROM entities WHERE entity_type=?", (new_type,)).fetchone()[0]
    conn.execute("UPDATE entities SET type_seq=?, entity_id=? WHERE id=?", (max_seq + 1, f"{new_type}-{max_seq + 1}", eid))

    conn.commit()
    conn.close()
    return {"success": True, "message": f"实体【{ent['name']}】已从 {old_type} 重新分类为 {new_type}"}

@app.delete("/api/entities/{eid}")
def api_delete_entity(eid: int):
    from database import get_db, delete_entity as de
    conn = get_db()
    ent = conn.execute("SELECT id, name, entity_type FROM entities WHERE id=?", (eid,)).fetchone()
    if not ent:
        conn.close()
        raise HTTPException(404, "实体不存在")
    # 检查任务引用
    task_count = conn.execute(
        "SELECT COUNT(*) FROM task_entities WHERE entity_id=?", (eid,)
    ).fetchone()[0]
    conn.close()
    if task_count > 0:
        raise HTTPException(400, "该实体被 " + str(task_count) + " 个任务绑定，请先解绑后再删除")
    de(eid)
    return {"success": True, "message": "实体【" + str(ent["name"]) + "】(#" + str(eid) + ") 已删除"}

@app.get("/api/task/{slug}/entities")
def api_task_entities(slug: str):
    from database import get_task as gt, get_task_entities as gte
    task = gt(slug)
    if not task: raise HTTPException(404, "no")
    return gte(task["id"])

@app.post("/api/task/{slug}/entities/{eid}")
def api_bind_entity(slug: str, eid: int):
    from database import get_task as gt, bind_task_entity as bte
    task = gt(slug)
    if not task: raise HTTPException(404, "no task")
    bte(task["id"], eid)
    return {"success": True}

@app.delete("/api/task/{slug}/entities/{eid}")
def api_unbind_entity(slug: str, eid: int):
    from database import get_task as gt, unbind_task_entity as ute
    task = gt(slug)
    if not task: raise HTTPException(404, "no task")
    ute(task["id"], eid)
    return {"success": True}

@app.get("/api/entity/{eid}/tasks")
def api_entity_tasks(eid: int):
    from database import get_entity_tasks as get_tasks
    return get_tasks(eid)

@app.post("/api/entities/sync-eweishop")
def api_sync_eweishop():
    """从eweishop导入app到实体库"""
    from database import create_entity as ce, get_db as gdb
    import json
    # 获取已存在的eweishop app名称集合
    conn = gdb()
    existing = set(row["name"] for row in conn.execute(
        "SELECT e.name FROM entities e JOIN entity_apps a ON a.entity_id=e.id WHERE a.app_type='非自研'").fetchall())
    # 取eweishop app数据
    import pymysql
    try:
        db = pymysql.connect(host="124.222.165.229", port=3306,
                             user="eweishop", password="YaJeckec8jhGRDmf",
                             database="eweishop", connect_timeout=5)
        cur = db.cursor(pymysql.cursors.DictCursor)
        cur.execute("SELECT u.id, u.shop_id, u.app_name, u.application_name, "
                    "u.status, u.create_time, u.expire_time, "
                    "u.android_package_name, u.android_app_version, u.android_sha1, u.android_md5, u.android_download_url, "
                    "u.ios_bundleid, u.ios_app_version, u.ios_download_url, u.has_android, u.has_ios, "
                    "s.name as shop_name, s.expire_time as shop_expire "
                    "FROM es_uapp u LEFT JOIN es_shop s ON u.shop_id=s.id "
                    "ORDER BY u.id")
        rows = cur.fetchall()
        cur.close(); db.close()
    except Exception as e:
        return {"success": False, "error": f"eweishop数据库连接失败: {str(e)}", "imported": 0, "skipped": 0}

    imported = 0; skipped = 0
    for r in rows:
        name = (r.get("application_name") or r.get("app_name") or "").strip()
        if not name or name in existing:
            skipped += 1; continue
        extra = {
            "app_type": "非自研", "system_name": name,
            "source_table": f"eweishop_uapp_{r['id']}"
        }
        extra_data = {
            "shop_id": r.get("shop_id"), "shop_name": r.get("shop_name"),
            "app_version": r.get("status"), "expire_time": str(r.get("expire_time") or ""),
            "has_android": r.get("has_android"), "android_package_name": r.get("android_package_name"),
            "android_app_version": r.get("android_app_version"), "android_sha1": r.get("android_sha1"),
            "android_md5": r.get("android_md5"), "android_download_url": r.get("android_download_url"),
            "has_ios": r.get("has_ios"), "ios_bundleid": r.get("ios_bundleid"),
            "ios_app_version": r.get("ios_app_version"), "ios_download_url": r.get("ios_download_url"),
        }
        try:
            ce("app", "", name, "", extra, extra_data)
            existing.add(name)
            imported += 1
        except: skipped += 1

    return {"success": True, "imported": imported, "skipped": skipped}

@app.post("/api/entities/dedup")
def api_dedup_entities():
    """去重：按名称合并相同实体（单连接避免锁）"""
    from database import get_db as gdb
    conn = gdb()
    try:
        conn.execute("BEGIN IMMEDIATE")
        dups = conn.execute("""
            SELECT entity_type, name, GROUP_CONCAT(id ORDER BY id) as ids
            FROM entities GROUP BY entity_type, name HAVING COUNT(*) > 1
        """).fetchall()
        removed = 0
        for d in dups:
            ids = [int(x) for x in d["ids"].split(",")]
            keep = ids[0]
            for rm_id in ids[1:]:
                conn.execute("UPDATE OR IGNORE task_entities SET entity_id=? WHERE entity_id=?", (keep, rm_id))
                # 手动清理子表 + 主表（不用 delete_entity 避免另开连接）
                conn.execute("DELETE FROM entity_apps WHERE entity_id=?", (rm_id,))
                conn.execute("DELETE FROM entity_websites WHERE entity_id=?", (rm_id,))
                conn.execute("DELETE FROM entity_clients WHERE entity_id=?", (rm_id,))
                conn.execute("DELETE FROM entities WHERE id=?", (rm_id,))
                removed += 1
        conn.commit()
    except Exception as e:
        conn.rollback()
        conn.close()
        return {"success": False, "error": str(e), "dedup_count": 0}
    conn.close()
    return {"success": True, "dedup_count": removed}

@app.get("/api/eweishop/apps")
def api_eweishop_apps(search: str = ""):
    """查询eweishop的es_uapp数据"""
    import pymysql
    try:
        conn = pymysql.connect(host="124.222.165.229", port=3306,
                               user="eweishop", password="YaJeckec8jhGRDmf",
                               database="eweishop", connect_timeout=5)
        cur = conn.cursor(pymysql.cursors.DictCursor)
        if search:
            cur.execute("SELECT u.*, s.name as shop_name, s.expire_time as shop_expire "
                        "FROM es_uapp u LEFT JOIN es_shop s ON u.shop_id=s.id "
                        "WHERE u.app_name LIKE %s OR u.application_name LIKE %s "
                        "ORDER BY u.id DESC LIMIT 20",
                        (f"%{search}%", f"%{search}%"))
        else:
            cur.execute("SELECT u.id, u.shop_id, u.app_name, u.application_name, "
                        "u.status, u.create_time, u.expire_time, "
                        "s.name as shop_name, s.expire_time as shop_expire "
                        "FROM es_uapp u LEFT JOIN es_shop s ON u.shop_id=s.id "
                        "ORDER BY u.id DESC LIMIT 50")
        rows = cur.fetchall()
        cur.close()
        conn.close()
        return {"success": True, "total": len(rows), "apps": rows}
    except Exception as e:
        return {"success": False, "error": str(e), "apps": []}

# ===== n8n 集成 API =====
N8N_CLIENT = None

def get_n8n_client():
    global N8N_CLIENT
    if N8N_CLIENT:
        return N8N_CLIENT
    from database import get_db as gdb
    conn = gdb()
    key = conn.execute("SELECT value FROM config WHERE key='n8n_api_key'").fetchone()
    url = conn.execute("SELECT value FROM config WHERE key='n8n_base_url'").fetchone()
    conn.close()
    if not key:
        return None
    k = key["value"]
    u = url["value"] if url else "http://localhost:5678"
    class N8nClient:
        def __init__(self):
            self.base_url = u
            self.api_key = k
        def list_workflows(self):
            import urllib.request
            req = urllib.request.Request(f"{self.base_url}/api/v1/workflows",
                headers={"X-N8N-API-KEY": self.api_key})
            try:
                resp = urllib.request.urlopen(req, timeout=10)
                data = json.loads(resp.read())
                return {"success": True, "workflows": data.get("data", [])}
            except Exception as e:
                return {"success": False, "error": str(e)}
        def execute_workflow(self, workflow_id, data=None):
            import urllib.request
            body = {"workflowId": workflow_id}
            if data:
                body["data"] = data
            req = urllib.request.Request(
                f"{self.base_url}/api/v1/workflows/{workflow_id}/run",
                data=json.dumps(body).encode(),
                headers={"X-N8N-API-KEY": self.api_key, "Content-Type": "application/json"},
                method="POST")
            try:
                resp = urllib.request.urlopen(req, timeout=30)
                return {"success": True, "result": json.loads(resp.read())}
            except Exception as e:
                return {"success": False, "error": str(e)}
    N8N_CLIENT = N8nClient()
    return N8N_CLIENT

@app.post("/api/n8n/sync")
def api_n8n_sync():
    """从n8n导入workflows到实体库"""
    client = get_n8n_client()
    if not client:
        return {"success": False, "error": "N8N_API_KEY not configured"}
    r = client.list_workflows()
    if not r["success"]:
        return {"success": False, "error": r["error"]}
    from database import create_entity as ce, get_db as gdb
    conn = gdb()
    existing = set(row[0] for row in conn.execute(
        "SELECT DISTINCT n.workflow_id FROM entity_n8n n").fetchall())
    conn.close()
    conn.close()
    imported = 0; skipped = 0
    for wf in r.get("workflows", []):
        wf_id = wf.get("id", "")
        if wf_id in existing:
            skipped += 1; continue
        extra = {
            "workflow_id": wf_id,
            "workflow_name": wf.get("name", ""),
            "active": 1 if wf.get("active") else 0,
            "nodes_count": len(wf.get("nodes", [])),
            "tags": ",".join(wf.get("tags", [])),
            "description": wf.get("name", ""),
        }
        try:
            ce("n8n", "", wf.get("name", wf_id), "从n8n自动导入", extra)
            imported += 1
        except:
            skipped += 1
    return {"success": True, "imported": imported, "skipped": skipped, "total": len(r["workflows"])}

@app.post("/api/n8n/webhook")
def api_n8n_webhook(data: dict):
    """n8n执行回调 — 写回任务系统execution记录"""
    from database import add_execution, get_db
    task_slug = (data.get("task_slug") or "").strip()
    wf_id = (data.get("workflow_id") or "").strip()
    status = (data.get("status") or "完成").strip()
    brief = (data.get("brief") or "").strip()
    output = data.get("output", {})
    if not task_slug:
        return {"success": False, "error": "task_slug required"}
    conn = get_db()
    task = conn.execute("SELECT id FROM tasks WHERE slug=?", (task_slug,)).fetchone()
    conn.close()
    if not task:
        return {"success": False, "error": f"task {task_slug} not found"}
    output_files = json.dumps(output.get("files", []), ensure_ascii=False)
    add_execution(task["id"], status, brief, output_files=output_files)
    return {"success": True}

@app.get("/api/n8n/workflows")
def api_n8n_list_workflows(active: str = ""):
    """列出n8n中的workflow（实体库）"""
    from database import get_entities_by_type
    ents = get_entities_by_type("n8n")
    if active == "true":
        ents = [e for e in ents if e.get("active")]
    return {"success": True, "workflows": ents}

@app.post("/api/n8n/execute/{eid}")
def api_n8n_execute(eid: int, body: dict = {}):
    """通过实体ID触发n8n workflow执行"""
    from database import get_entity as ge
    ent = ge(eid)
    if not ent or ent.get("entity_type") != "n8n":
        raise HTTPException(404, "n8n entity not found")
    wf_id = ent.get("workflow_id", "")
    if not wf_id:
        raise HTTPException(400, "workflow_id not set")
    client = get_n8n_client()
    if not client:
        raise HTTPException(500, "n8n not configured")
    payload = body.get("data", {})
    payload["_task_slug"] = ent.get("name", "")
    return client.execute_workflow(wf_id, payload)

if __name__=='__main__':
    init_db()
    port = int(sys.argv[1]) if len(sys.argv)>1 else 19007
    print('Tasks API running on :'+str(port))
    uvicorn.run(app,host='0.0.0.0',port=port)
