From 29874e6ca99d7b09267aa1338f07b6df83d8c15c Mon Sep 17 00:00:00 2001 From: hz4th_coder Date: Wed, 12 Aug 2026 00:48:52 +0800 Subject: [PATCH] =?UTF-8?q?V1:=20DAG=E5=A4=9A=E4=BB=BB=E5=8A=A1=E7=BC=96?= =?UTF-8?q?=E6=8E=92=20+=20AI=E8=A7=84=E5=88=92WBS=20+=20RAG=E7=9F=A5?= =?UTF-8?q?=E8=AF=86=E5=BA=93=20+=20=E9=A2=84=E7=AE=97=E5=91=8A=E8=AD=A6?= =?UTF-8?q?=E4=B8=AD=E5=BF=83=20+=20=E5=BC=80=E6=94=BEAPI=20+=20=E9=A3=9E?= =?UTF-8?q?=E4=B9=A6/=E4=BC=81=E5=BE=AE/=E9=82=AE=E4=BB=B6=E9=80=9A?= =?UTF-8?q?=E7=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 85 +++++--- app.py | 342 +++++++++++++++++++++++++++++- config.py | 13 ++ dag_verify.py | 58 +++++ db.py | 88 ++++++++ docs/aw_dag.png | Bin 0 -> 90425 bytes docs/aw_kb.png | Bin 0 -> 79937 bytes engine.py | 135 +++++++++++- notify.py | 111 ++++++++++ rag.py | 119 +++++++++++ static/app.js | 529 ++++++++++++++++++++++++++++++++++++++++------ static/index.html | 3 + static/style.css | 30 +++ 13 files changed, 1402 insertions(+), 111 deletions(-) create mode 100644 dag_verify.py create mode 100644 docs/aw_dag.png create mode 100644 docs/aw_kb.png create mode 100644 notify.py create mode 100644 rag.py diff --git a/README.md b/README.md index cc499a6..2a76265 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,33 @@ -# AI Worker 项目管理平台(MVP) +# AI Worker 项目管理平台 > 以「项目」为中心、以「AI Worker」为执行单元的项目管理平台。 > 把大模型团队变成一支可指挥、可审计、可控成本的"虚拟团队"。 -**MVP 目标**:跑通「人派活 → AI 干活 → 人验收」最小闭环。 +**当前版本:V1**(MVP 闭环 + 多 Agent 编排 / RAG / 预算告警 / 开放 API / 飞书企微集成) -## 功能清单(MVP) +--- +## 功能总览 + +### MVP(已交付 v1.0) | 模块 | 能力 | |---|---| -| 📁 项目管理 | 项目 CRUD、目标/验收标准/预算上限、状态流转(规划/进行/完成/归档) | -| 🧑💻 Worker 管理 | 注册虚拟员工(供应商 + 模型 + 角色提示词 + 温度/Token + 成本上限)、连通性测试、启停用 | -| 📋 任务编排 | 任务 CRUD、看板(拖拽改状态)、优先级、截止时间、**自动路由**(按价格选最便宜可用 Worker) | -| 🚀 执行引擎 | 后台线程执行、统一模型网关(多供应商 OpenAI 兼容)、失败重试、超时、成本上限预检、断点状态 | -| ✅ 人工审核 HITL | 产出自动进入「待审核」,人工通过/打回(必填原因)→ 返工重跑,AI 不可绕过 | -| 💰 成本记账 | Token/成本按 项目×Worker×模型 多维核算、预算硬约束、近 7 天趋势 | -| 📊 质量指标 | 一次通过率、返工次数、失败数 | -| 📜 全链路日志 | 任务级执行日志 + 全局日志流,每个结论可溯源 | +| 📁 项目管理 | 项目 CRUD、目标/验收标准/预算上限、状态流转 | +| 🧑💻 Worker 管理 | 虚拟员工档案(供应商+模型+角色提示词+温度/Token+成本上限)、连通性测试、自动路由 | +| 📋 看板 | 6 列看板拖拽、优先级、截止时间、返工计数 | +| 🚀 执行引擎 | 统一模型网关(doubao/deepseek/openai/qwen/vLLM)、失败重试、超时、成本预检 | +| ✅ HITL 审核 | 产出进「待审核」,通过/打回(必填原因)→ 返工,AI 不可绕过 | +| 💰 成本记账 | Token/成本按 项目×Worker×模型 核算、预算硬约束、一次通过率 | + +### V1(本次交付 v2.0) +| 模块 | 能力 | +|---|---| +| 🔗 **DAG 多任务编排** | 任务依赖(depends_on)、依赖校验拦截、**完成后自动触发下游**、并行执行、一键执行整个工作流、SVG 流程可视化 | +| 🧠 **AI 辅助规划 WBS** | 输入目标 → LLM 自动生成 4~8 个任务的任务分解(含依赖关系)→ 可编辑预览 → 一键导入项目 | +| 📚 **RAG 知识库** | 项目级文档(粘贴/上传 txt·md)、自动分块索引、BM25 检索(jieba 分词)、**任务执行时自动注入相关知识**(日志可见命中来源) | +| 🚨 **预算控制与告警** | 项目预算使用率阈值告警(默认 80%)、成本上限拦截、**告警中心**(未读角标/标记已读)、去重防刷屏 | +| 🔌 **开放 API** | Bearer Token 认证、Token 管理、外部系统可建任务/派活/查结果/审核(curl 示例内置) | +| 📨 **通知集成** | 飞书/企微群机器人 Webhook + 邮件 SMTP,订阅事件:待审核/完成/失败/预算告警/Worker 异常/规划完成,渠道连通性测试 | ## 快速开始 @@ -26,45 +37,49 @@ ./start.sh restart # 重启 ``` -- 管理界面:`http://:16071/`,默认口令 `admin123`(改 `config.py` 的 `AUTH_PASSWORD`,留空则免登录) -- 首次启动自动灌入演示数据(1 项目 / 1 Worker / 3 任务),可用 `python3 seed.py` 重新初始化 +- 管理界面:`http://:16071/`,默认口令 `admin123`(改 `config.py` 的 `AUTH_PASSWORD`) +- 首次启动自动灌入演示数据;`python3 seed.py` 重新初始化 +- V1 全链路验证脚本:`python3 dag_verify.py <终点任务ID>`(自动审核放行直到链路完成) ## 技术栈 -- 后端:Python + Flask + SQLite(WAL),无外部依赖,纯标准库 + flask/requests -- 前端:原生 HTML/JS/CSS 单页应用(无 CDN,离线可用) -- 模型网关:OpenAI 兼容协议,内置 doubao / deepseek / openai / qwen / vLLM 供应商,可扩展 +- 后端:Python + Flask + SQLite(WAL),依赖仅 flask/requests/jieba +- 前端:原生 HTML/JS/CSS SPA(无 CDN,离线可用) +- RAG:本地 BM25(jieba 分词),零依赖;升级路径 = 商用 embedding + pgvector/Qdrant +- 编排:自研轻量 DAG 引擎(线程级,断点状态落库);升级路径 = Temporal 持久执行 + LangGraph 多 Agent 推理 ## 目录结构 ``` ai-worker-platform/ -├── app.py # Flask 应用 + REST API -├── config.py # 供应商/定价/鉴权/端口配置 -├── db.py # SQLite 数据层(projects/workers/tasks/logs/cost_records) -├── engine.py # 任务执行引擎(后台线程 + 自动路由 + 预算预检) -├── llm_gateway.py # 统一模型网关(调用/重试/计量/计价) +├── app.py # Flask 应用 + REST API(含 V1 路由) +├── config.py # 供应商/定价/鉴权/告警阈值/邮件 SMTP +├── db.py # SQLite 数据层 + 自动迁移 +├── engine.py # 执行引擎:DAG 依赖/自动触发/RAG 注入/告警/通知 +├── llm_gateway.py # 统一模型网关 +├── rag.py # 知识库:分块 + BM25 检索 +├── notify.py # 飞书/企微/邮件通知 +├── dag_verify.py # DAG 全链路验证脚本 ├── seed.py # 演示数据 ├── start.sh # 启停脚本 ├── static/ # 前端 SPA +├── docs/ # 界面截图 └── data/ # SQLite 库;logs/ 运行日志 ``` -## API 摘要 +## 开放 API 摘要 -- `POST /api/login` 登录;`GET /api/me` 会话检查 -- `GET|POST /api/projects`、`GET|PUT|DELETE /api/projects/` -- `GET|POST /api/workers`、`GET|PUT|DELETE /api/workers/`、`POST /api/workers//test` -- `GET|POST /api/tasks`、`GET|PUT|DELETE /api/tasks/` -- `POST /api/tasks//run` 派活、`/cancel` 取消、`/review` 审核(approve/reject+reason) -- `GET /api/reports/cost?group=project|worker|model`、`GET /api/stats`、`GET /api/logs` +所有接口支持 `Authorization: Bearer `(管理界面「开放 API」页生成): -## 定价配置 +- `POST /api/projects` 建项目 | `GET /api/projects` +- `POST /api/tasks` 建任务(body 含 project_id/title/description/depends_on…) +- `POST /api/tasks//run` 派活 | `GET /api/tasks/` 查结果 +- `POST /api/tasks//review` 审核 `{"action":"approve"}` 或 `{"action":"reject","reason":"…"}` +- `POST /api/projects//workflow/run` 执行整个工作流 +- `POST /api/projects//wbs/generate` AI 生成任务分解 | `/wbs/import` 导入 +- `GET /api/reports/cost?group=project|worker|model` 成本报表 +- `GET /api/alerts` 告警 | `GET /api/stats` 统计 -`config.py` 中 `MODEL_PRICING` 按「元 / 1M tokens」登记各模型单价,未登记的走默认价。 -成本 = 输入 tokens/1e6 × 输入单价 + 输出 tokens/1e6 × 输出单价。 +## 路线图 -## 路线图(超出 MVP) - -- V1:DAG 多任务编排(Temporal 持久执行)、RAG 知识库、开放 API、飞书/企微集成 -- V2:多 Agent 协作(主管/评审/辩论模式)、自动评估体系、模板市场、企业版 +- **V2**:多 Agent 协作模式(主管/评审/辩论)、Temporal 持久执行、自动评估体系(eval)、模板市场、企业版(私有化/SSO/审计) diff --git a/app.py b/app.py index 87e97e0..68fbe47 100644 --- a/app.py +++ b/app.py @@ -4,13 +4,17 @@ AI Worker 项目管理平台 - MVP 人派活 → AI 干活 → 人验收 最小闭环 """ import os +import secrets import functools +import json as _json from flask import Flask, request, jsonify, session, send_from_directory import config import db import engine import llm_gateway +import rag +import notify app = Flask(__name__, static_folder='static', static_url_path='') app.secret_key = config.SECRET_KEY @@ -46,11 +50,27 @@ def me(): return jsonify({'ok': True, 'authed': not auth_enabled() or session.get('authed')}) +def _auth_ok(): + """会话或 API Token 任一通过即可""" + if not auth_enabled(): + return True + if session.get('authed'): + return True + hdr = request.headers.get('Authorization', '') + if hdr.startswith('Bearer '): + tok = hdr[7:].strip() + r = db.q('SELECT id FROM api_tokens WHERE token=?', (tok,), one=True) + if r: + db.w('UPDATE api_tokens SET last_used_at=? WHERE id=?', (db.now(), r['id'])) + return True + return False + + def require_auth(fn): @functools.wraps(fn) def wrapper(*args, **kwargs): - if auth_enabled() and not session.get('authed'): - return jsonify({'ok': False, 'error': '未登录'}), 401 + if not _auth_ok(): + return jsonify({'ok': False, 'error': '未登录或 Token 无效'}), 401 return fn(*args, **kwargs) return wrapper @@ -199,11 +219,11 @@ def tasks(): d = request.get_json(force=True) tid = db.w( 'INSERT INTO tasks (project_id, worker_id, title, description, priority, ' - 'review_required, deadline, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)', + 'review_required, deadline, depends_on, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?)', (d.get('project_id'), d.get('worker_id'), d.get('title', '').strip(), d.get('description', ''), d.get('priority', 'medium'), 1 if d.get('review_required', True) else 0, d.get('deadline', ''), - db.now(), db.now())) + _json.dumps(d.get('depends_on') or []), db.now(), db.now())) db.w('INSERT INTO task_logs (task_id, level, message, created_at) VALUES (?,?,?,?)', (tid, 'info', f'任务创建:{d.get("title","")}', db.now())) return jsonify({'ok': True, 'id': tid}) @@ -245,6 +265,9 @@ def task_detail(tid): if f in d: sets.append(f'{f}=?') args.append(d[f]) + if 'depends_on' in d: + sets.append('depends_on=?') + args.append(_json.dumps(d['depends_on'] or [])) if sets: args.append(db.now()) db.w(f'UPDATE tasks SET {", ".join(sets)}, updated_at=? WHERE id=?', (*args, tid)) @@ -259,6 +282,9 @@ def task_run(tid): return jsonify({'ok': False, 'error': '任务不存在'}), 404 if t['status'] == 'running': return jsonify({'ok': False, 'error': '任务已在执行中'}), 400 + ok, blockers = engine.check_dependencies(t) + if not ok: + return jsonify({'ok': False, 'error': '前置任务未完成:' + '、'.join(blockers)}), 400 if engine.runner.submit(tid): return jsonify({'ok': True}) return jsonify({'ok': False, 'error': '任务已在执行中'}), 400 @@ -295,6 +321,11 @@ def task_review(tid): db.w('UPDATE tasks SET status="done", updated_at=? WHERE id=?', (db.now(), tid)) db.w('INSERT INTO task_logs (task_id, level, message, created_at) VALUES (?,?,?,?)', (tid, 'success', '✅ 人工审核通过,任务完成', db.now())) + # DAG:审核放行等同完成,触发下游就绪任务 + fresh = db.q('SELECT * FROM tasks WHERE id=?', (tid,), one=True) + for t in engine._trigger_downstream(fresh): + db.w('INSERT INTO task_logs (task_id, level, message, created_at) VALUES (?,?,?,?)', + (t['id'], 'info', f'🔗 前置任务「{fresh["title"]}」已验收完成,自动触发执行', db.now())) return jsonify({'ok': True}) if action == 'reject': if not reason: @@ -330,7 +361,7 @@ def report_cost(): 'SELECT project_id, COUNT(*) runs, SUM(total_tokens) tokens, ' 'SUM(cost) cost FROM cost_records GROUP BY project_id ORDER BY cost DESC') for r in rows: - p = db.q('SELECT name FROM projects WHERE id=?', (r['project_id']), one=True) + p = db.q('SELECT name FROM projects WHERE id=?', (r['project_id'],), one=True) r['project_name'] = p['name'] if p else f'#{r["project_id"]}' return jsonify({'ok': True, 'data': rows}) @@ -383,6 +414,307 @@ def logs(): return jsonify({'ok': True, 'data': rows}) +@app.route('/api/projects//workflow/run', methods=['POST']) +@require_auth +def workflow_run(pid): + """执行整个工作流:跑所有就绪(无未完成前置)任务""" + rows = db.q('SELECT * FROM tasks WHERE project_id=? AND status IN ("todo","failed")', (pid,)) + started, blocked = [], [] + for t in rows: + ok, blockers = engine.check_dependencies(t) + if ok: + if engine.runner.submit(t['id']): + started.append({'id': t['id'], 'title': t['title']}) + else: + blocked.append({'id': t['id'], 'title': t['title'], 'by': blockers}) + return jsonify({'ok': True, 'started': started, 'blocked': blocked}) + + +@app.route('/api/projects//dag') +@require_auth +def project_dag(pid): + """DAG 图数据:节点 + 边""" + rows = db.q('SELECT * FROM tasks WHERE project_id=? ORDER BY id', (pid,)) + nodes, edges, id_map = [], [], {} + for t in rows: + node = db.serialize_task(t) + nodes.append(node) + id_map[t['id']] = node + for t in nodes: + for dep_id in t['depends_on']: + if dep_id in id_map: + edges.append({'from': dep_id, 'to': t['id']}) + return jsonify({'ok': True, 'nodes': nodes, 'edges': edges}) + + +# --------------------------------------------------------------------------- +# AI 辅助规划(WBS 生成 + 导入) +# --------------------------------------------------------------------------- +WBS_PROMPT = ( + '你是资深项目经理。请把下面的项目目标拆解为可执行的任务列表(WBS),' + '要求:\n1. 输出严格 JSON,格式 {{"tasks": [{{"title": "任务标题", ' + '"description": "给AI Worker的执行指令(含要求与输出格式)", "depends_on": [0,2]}}]}}\n' + '2. depends_on 是前置任务的数组下标(无依赖填 []),下标从 0 开始\n' + '3. 4~8 个任务,逻辑清晰,可并行任务并行,不要输出 JSON 以外的任何内容\n\n' + '项目目标:{goal}\n' + '项目验收标准:{accept}' +) + + +def _parse_wbs(text): + """从 LLM 输出中提取 JSON""" + t = text.strip() + if t.startswith('```'): + t = t.strip('`') + if t.startswith('json'): + t = t[4:] + t = t.strip() + start = min([i for i in (t.find('{'), t.find('[')) if i >= 0] or [0]) + end = max(t.rfind('}'), t.rfind(']')) + 1 + data = _json.loads(t[start:end]) + tasks = data['tasks'] if isinstance(data, dict) else data + assert isinstance(tasks, list) and tasks, '任务列表为空' + return tasks + + +@app.route('/api/projects//wbs/generate', methods=['POST']) +@require_auth +def wbs_generate(pid): + d = request.get_json(force=True) or {} + goal = d.get('goal') or '' + proj = db.q('SELECT * FROM projects WHERE id=?', (pid,), one=True) + if not proj: + return jsonify({'ok': False, 'error': '项目不存在'}), 404 + if not goal: + goal = proj.get('objective') or proj.get('name') or '' + if not goal: + return jsonify({'ok': False, 'error': '请提供项目目标'}), 400 + worker = db.q('SELECT * FROM workers WHERE status="enabled" ORDER BY id', one=True) + if not worker: + return jsonify({'ok': False, 'error': '请先注册至少一个 Worker 用于规划'}), 400 + try: + r = llm_gateway.chat(worker['provider'], worker['model'], [ + {'role': 'system', 'content': '你只输出 JSON,不输出任何解释文字。'}, + {'role': 'user', 'content': WBS_PROMPT.format(goal=goal, accept=proj.get('acceptance_criteria') or '—')}, + ], temperature=0.3, max_tokens=3000) + tasks = _parse_wbs(r['text']) + return jsonify({'ok': True, 'data': tasks, 'usage': r['total_tokens'], 'cost': r['cost']}) + except Exception as e: + return jsonify({'ok': False, 'error': f'WBS 生成失败:{e}'}), 500 + + +@app.route('/api/projects//wbs/import', methods=['POST']) +@require_auth +def wbs_import(pid): + d = request.get_json(force=True) + tasks = d.get('tasks') or [] + worker_id = d.get('worker_id') + if not tasks: + return jsonify({'ok': False, 'error': '任务列表为空'}), 400 + created = [] + for i, t in enumerate(tasks): + dep_idx = t.get('depends_on') or [] + dep_ids = [created[idx] for idx in dep_idx + if isinstance(idx, int) and 0 <= idx < len(created)] + tid = db.w( + 'INSERT INTO tasks (project_id, worker_id, title, description, priority, ' + 'review_required, deadline, depends_on, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?)', + (pid, worker_id, t.get('title', f'任务{i+1}'), t.get('description', ''), + t.get('priority', 'medium'), 1, '', _json.dumps(dep_ids), db.now(), db.now())) + created.append(tid) + db.w('INSERT INTO task_logs (task_id, level, message, created_at) VALUES (?,?,?,?)', + (tid, 'info', f'AI 规划导入:{t.get("title", "")}', db.now())) + return jsonify({'ok': True, 'created': len(created), 'ids': created}) + + +# --------------------------------------------------------------------------- +# RAG 知识库 +# --------------------------------------------------------------------------- +@app.route('/api/projects//documents', methods=['GET', 'POST']) +@require_auth +def documents(pid): + if request.method == 'POST': + d = request.get_json(force=True) + doc_id = db.w( + 'INSERT INTO documents (project_id, name, content, source, created_at, updated_at) ' + 'VALUES (?,?,?,?,?,?)', + (pid, d.get('name', '未命名文档').strip(), d.get('content', ''), + d.get('source', 'manual'), db.now(), db.now())) + n = rag.rebuild_document(doc_id) + return jsonify({'ok': True, 'id': doc_id, 'chunks': n}) + rows = db.q('SELECT * FROM documents WHERE project_id=? ORDER BY id DESC', (pid,)) + for r in rows: + r['chunks'] = db.q('SELECT COUNT(*) c FROM doc_chunks WHERE document_id=?', (r['id'],))[0]['c'] + return jsonify({'ok': True, 'data': rows}) + + +@app.route('/api/documents/', methods=['GET', 'PUT', 'DELETE']) +@require_auth +def document_detail(doc_id): + doc = db.q('SELECT * FROM documents WHERE id=?', (doc_id,), one=True) + if not doc: + return jsonify({'ok': False, 'error': '文档不存在'}), 404 + if request.method == 'GET': + return jsonify({'ok': True, 'data': doc}) + if request.method == 'DELETE': + db.w('DELETE FROM doc_chunks WHERE document_id=?', (doc_id,)) + db.w('DELETE FROM documents WHERE id=?', (doc_id,)) + return jsonify({'ok': True}) + d = request.get_json(force=True) + if 'content' in d: + db.w('UPDATE documents SET content=?, updated_at=? WHERE id=?', (d['content'], db.now(), doc_id)) + rag.rebuild_document(doc_id) + if 'name' in d: + db.w('UPDATE documents SET name=? WHERE id=?', (d['name'].strip(), doc_id)) + return jsonify({'ok': True}) + + +@app.route('/api/projects//search') +@require_auth +def kb_search(pid): + q = request.args.get('q', '') + if not q: + return jsonify({'ok': True, 'data': []}) + hits, hit = rag.search_project(pid, q, top_k=5) + return jsonify({'ok': True, 'data': hits if hit else [], 'hit': hit}) + + +# --------------------------------------------------------------------------- +# 告警中心 +# --------------------------------------------------------------------------- +@app.route('/api/alerts') +@require_auth +def alerts(): + limit = min(int(request.args.get('limit', 100)), 500) + rows = db.q('SELECT * FROM alerts ORDER BY id DESC LIMIT ?', (limit,)) + return jsonify({'ok': True, 'data': rows}) + + +@app.route('/api/alerts/unread_count') +@require_auth +def alerts_unread(): + c = db.q('SELECT COUNT(*) c FROM alerts WHERE read=0')[0]['c'] + return jsonify({'ok': True, 'count': c}) + + +@app.route('/api/alerts//read', methods=['POST']) +@require_auth +def alert_read(aid): + db.w('UPDATE alerts SET read=1 WHERE id=?', (aid,)) + return jsonify({'ok': True}) + + +@app.route('/api/alerts/read_all', methods=['POST']) +@require_auth +def alerts_read_all(): + db.w('UPDATE alerts SET read=1 WHERE read=0') + return jsonify({'ok': True}) + + +# --------------------------------------------------------------------------- +# 开放 API Token +# --------------------------------------------------------------------------- +@app.route('/api/tokens', methods=['GET', 'POST']) +@require_auth +def api_tokens(): + if request.method == 'POST': + d = request.get_json(force=True) + tok = secrets.token_hex(24) + db.w('INSERT INTO api_tokens (name, token, created_at) VALUES (?,?,?)', + (d.get('name', '未命名').strip(), tok, db.now())) + return jsonify({'ok': True, 'token': tok}) + rows = db.q('SELECT id, name, created_at, last_used_at FROM api_tokens ORDER BY id DESC') + return jsonify({'ok': True, 'data': rows}) + + +@app.route('/api/tokens/', methods=['DELETE']) +@require_auth +def api_token_delete(tid): + db.w('DELETE FROM api_tokens WHERE id=?', (tid,)) + return jsonify({'ok': True}) + + +# --------------------------------------------------------------------------- +# 通知渠道(飞书/企微/邮件) +# --------------------------------------------------------------------------- +@app.route('/api/channels', methods=['GET', 'POST']) +@require_auth +def channels(): + if request.method == 'POST': + d = request.get_json(force=True) + cid = db.w( + 'INSERT INTO notify_channels (name, type, webhook, email, events, enabled, created_at) ' + 'VALUES (?,?,?,?,?,?,?)', + (d.get('name', '').strip(), d.get('type', 'feishu'), d.get('webhook', ''), + d.get('email', ''), _json.dumps(d.get('events') or []), + 1 if d.get('enabled', True) else 0, db.now())) + return jsonify({'ok': True, 'id': cid}) + rows = db.q('SELECT * FROM notify_channels ORDER BY id DESC') + for r in rows: + try: + r['events'] = _json.loads(r['events'] or '[]') + except Exception: + r['events'] = [] + return jsonify({'ok': True, 'data': rows}) + + +@app.route('/api/channels/', methods=['PUT', 'DELETE']) +@require_auth +def channel_detail(cid): + if request.method == 'DELETE': + db.w('DELETE FROM notify_channels WHERE id=?', (cid,)) + return jsonify({'ok': True}) + d = request.get_json(force=True) + fields = ['name', 'type', 'webhook', 'email', 'enabled'] + sets, args = [], [] + for f in fields: + if f in d: + sets.append(f'{f}=?') + args.append(d[f]) + if 'events' in d: + sets.append('events=?') + args.append(_json.dumps(d['events'] or [])) + if sets: + db.w(f'UPDATE notify_channels SET {", ".join(sets)} WHERE id=?', (*args, cid)) + return jsonify({'ok': True}) + + +@app.route('/api/channels//test', methods=['POST']) +@require_auth +def channel_test(cid): + ch = db.q('SELECT * FROM notify_channels WHERE id=?', (cid,), one=True) + if not ch: + return jsonify({'ok': False, 'error': '渠道不存在'}), 404 + ok, msg = notify.test_channel(ch) + return jsonify({'ok': ok, 'msg': msg}) + + +@app.route('/api/events') +@require_auth +def events(): + return jsonify({'ok': True, 'data': [{'id': k, 'name': v} for k, v in notify.EVENTS.items()]}) + + +# --------------------------------------------------------------------------- +# 设置 +# --------------------------------------------------------------------------- +@app.route('/api/settings', methods=['GET', 'PUT']) +@require_auth +def settings(): + if request.method == 'PUT': + d = request.get_json(force=True) + for k, v in d.items(): + db.set_setting(k, v) + if 'budget_alert_ratio' in d: + config.BUDGET_ALERT_RATIO = float(d['budget_alert_ratio']) + return jsonify({'ok': True}) + return jsonify({'ok': True, 'data': { + 'budget_alert_ratio': config.BUDGET_ALERT_RATIO, + 'auth_enabled': auth_enabled(), + 'email_configured': bool(config.EMAIL.get('host')), + }}) + + # --------------------------------------------------------------------------- # 前端 # --------------------------------------------------------------------------- diff --git a/config.py b/config.py index d375934..c5f2a54 100644 --- a/config.py +++ b/config.py @@ -77,5 +77,18 @@ DEFAULT_PRICE = {'input': 2.0, 'output': 8.0} MAX_RETRY = 1 # 失败重试次数(429/5xx/网络错误) TASK_TIMEOUT = 600 # 单任务超时(秒) +# 预算告警阈值(项目预算使用率 >= 该值触发告警) +BUDGET_ALERT_RATIO = 0.8 + +# 邮件通知配置(可选,留空 host 则禁用) +EMAIL = { + 'host': os.environ.get('SMTP_HOST', 'mail.tphai.com'), + 'port': int(os.environ.get('SMTP_PORT', 587)), + 'starttls': os.environ.get('SMTP_STARTTLS', '0') == '1', + 'user': os.environ.get('SMTP_USER', 'hz4th_coder@tphai.com'), + 'password': os.environ.get('SMTP_PASSWORD', 'hz4th_coder@!'), + 'from_name': 'AI Worker 平台', +} + # 自动路由:按模型单价升序挑选可用 Worker AUTO_ROUTE_POOL = 'enabled' # enabled | all diff --git a/dag_verify.py b/dag_verify.py new file mode 100644 index 0000000..58f51c6 --- /dev/null +++ b/dag_verify.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +"""DAG 全链路自动验证:审核放行 → 观察自动触发 → 直至终点任务 done""" +import json, sys, time, urllib.request + +BASE = 'http://127.0.0.1:16071' +COOKIE = '/tmp/aw_cookies.txt' + +def req(path, method='GET', body=None): + r = urllib.request.Request(BASE + path, method=method) + cookie = '' + for line in open(COOKIE): + parts = line.strip().split('\t') + if len(parts) >= 7 and parts[5] and parts[6]: + cookie += f'{parts[5]}={parts[6]}; ' + r.add_header('Cookie', cookie.strip()) + if body is not None: + r.add_header('Content-Type', 'application/json') + data = json.dumps(body).encode() + else: + data = None + with urllib.request.urlopen(r, data=data) as resp: + return json.loads(resp.read()) + +def task_status(tid): + d = req(f'/api/tasks/{tid}')['data'] + return d['status'], d.get('costs', []) + +# 终点任务 +END = int(sys.argv[1]) if len(sys.argv) > 1 else 9 +chain = [4, 5, 6, 7, 8, 9] +approved = set() +deadline = time.time() + 900 + +print(f'开始 DAG 全链路验证,终点任务 #{END}') +while time.time() < deadline: + all_done = True + for tid in chain: + st, costs = task_status(tid) + mark = f'¥{sum(c["cost"] for c in costs):.4f}' if costs else '-' + print(f' #{tid}: {st:9s} {mark}', flush=True) + if st == 'review' and tid not in approved: + req(f'/api/tasks/{tid}/review', 'POST', {'action': 'approve'}) + approved.add(tid) + print(f' ✅ 审核通过 #{tid}', flush=True) + if st != 'done': + all_done = False + if all_done: + print(f'🎉 全链路完成!共审核 {len(approved)} 个任务') + break + time.sleep(20) +else: + print('⏰ 超时未完成') + +total = 0 +for tid in chain: + _, costs = task_status(tid) + total += sum(c['cost'] for c in costs) +print(f'总成本: ¥{total:.4f}') diff --git a/db.py b/db.py index 095dfee..64b76df 100644 --- a/db.py +++ b/db.py @@ -80,13 +80,81 @@ CREATE TABLE IF NOT EXISTS cost_records ( created_at INTEGER ); +CREATE TABLE IF NOT EXISTS documents ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + project_id INTEGER NOT NULL, + name TEXT NOT NULL, + content TEXT DEFAULT '', + source TEXT DEFAULT 'manual', -- manual/file/url + chunk_size INTEGER DEFAULT 0, + created_at INTEGER, + updated_at INTEGER +); + +CREATE TABLE IF NOT EXISTS doc_chunks ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + document_id INTEGER NOT NULL, + idx INTEGER DEFAULT 0, + content TEXT DEFAULT '', + tokens INTEGER DEFAULT 0 +); + +CREATE TABLE IF NOT EXISTS alerts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + type TEXT DEFAULT 'system', -- budget/task_failed/worker/limit/notify/plan + level TEXT DEFAULT 'info', -- info/warn/critical + title TEXT DEFAULT '', + detail TEXT DEFAULT '', + read INTEGER DEFAULT 0, + created_at INTEGER +); + +CREATE TABLE IF NOT EXISTS api_tokens ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + token TEXT NOT NULL UNIQUE, + created_at INTEGER, + last_used_at INTEGER +); + +CREATE TABLE IF NOT EXISTS notify_channels ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + type TEXT NOT NULL, -- feishu/wecom/email + webhook TEXT DEFAULT '', + email TEXT DEFAULT '', + events TEXT DEFAULT '[]', -- JSON: task_review/task_done/task_failed/budget_alert/worker_alert + enabled INTEGER DEFAULT 1, + created_at INTEGER +); + +CREATE TABLE IF NOT EXISTS settings ( + key TEXT PRIMARY KEY, + value TEXT DEFAULT '' +); + CREATE INDEX IF NOT EXISTS idx_tasks_project ON tasks(project_id); CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status); CREATE INDEX IF NOT EXISTS idx_logs_task ON task_logs(task_id); CREATE INDEX IF NOT EXISTS idx_cost_task ON cost_records(task_id); CREATE INDEX IF NOT EXISTS idx_cost_project ON cost_records(project_id); +CREATE INDEX IF NOT EXISTS idx_docs_project ON documents(project_id); +CREATE INDEX IF NOT EXISTS idx_chunks_doc ON doc_chunks(document_id); +CREATE INDEX IF NOT EXISTS idx_alerts_read ON alerts(read); """ +# --------------------------------------------------------------------------- +# 迁移:给旧表补列(V1) +# --------------------------------------------------------------------------- +def _migrate(): + conn = get_conn() + cols = {r['name'] for r in conn.execute('PRAGMA table_info(tasks)')} + if 'depends_on' not in cols: + conn.execute("ALTER TABLE tasks ADD COLUMN depends_on TEXT DEFAULT '[]'") + conn.execute('CREATE INDEX IF NOT EXISTS idx_tasks_depends ON tasks(depends_on)') + conn.commit() + conn.close() + def get_conn(): conn = sqlite3.connect(DB_PATH, timeout=30) @@ -101,6 +169,7 @@ def init_db(): conn.executescript(SCHEMA) conn.commit() conn.close() + _migrate() def q(sql, args=(), one=False): @@ -154,4 +223,23 @@ def task_worker_cost(worker_id): def serialize_task(t): t = dict(t) t['review_required'] = bool(t['review_required']) + try: + t['depends_on'] = json.loads(t.get('depends_on') or '[]') + except Exception: + t['depends_on'] = [] return t + + +def get_setting(key, default=''): + r = q('SELECT value FROM settings WHERE key=?', (key,), one=True) + return r['value'] if r else default + + +def set_setting(key, value): + conn = get_conn() + try: + conn.execute('INSERT INTO settings (key, value) VALUES (?,?) ' + 'ON CONFLICT(key) DO UPDATE SET value=excluded.value', (key, str(value))) + conn.commit() + finally: + conn.close() diff --git a/docs/aw_dag.png b/docs/aw_dag.png new file mode 100644 index 0000000000000000000000000000000000000000..3179c047f8ab7d1cce0a6c9a2023bc9e2307ffd7 GIT binary patch literal 90425 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 I*YW;;08-j7NB{r; literal 0 HcmV?d00001 diff --git a/docs/aw_kb.png b/docs/aw_kb.png new file mode 100644 index 0000000000000000000000000000000000000000..10bd525a63794a58c8f8f9be18d0a3a995bff363 GIT binary patch literal 79937 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�siQ;L1eieRCpmgR@CXhaj6Lkg$=4 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
i2KgwI4C_o=?wb>>5IOUjcewpLAqh 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
执行 Worker
${w ? `${esc(w.name)} ${esc(w.provider)}/${esc(w.model)}` : '未指派(自动路由)'}
优先级
●${PRI[t.priority]} · 返工 ${t.rejection_count} 次
+
前置任务
${deps.length ? deps.map(d => `#${d.id} ${esc(d.title).slice(0,12)}(${STATUS[d.status].label})`).join('') : '无'}
创建/更新
${fmtTime(t.created_at)} / ${fmtTime(t.updated_at)}
截止
${t.deadline || '—'}
@@ -314,14 +508,16 @@ async function openTaskDetail(tid) { ${t.error ? `
错误信息
${esc(t.error)}
` : ''}
执行日志
-
${t.logs.map(l => logLine(l)).join('') || '
暂无日志
'}
+
${t.logs.map(logLine).join('') || '
暂无日志
'}
`); } @@ -341,22 +537,70 @@ async function reviewTask(tid, action) { await api(`/api/tasks/${tid}/review`, {method:'POST', body:{action}}); } toast(action === 'approve' ? '✅ 验收通过,任务完成' : '任务已打回,等待返工', 'ok'); - closeModal(); refreshKanban(); + closeModal(); refreshProj(); } async function cancelTask(tid) { if (!confirm('确认取消该任务?')) return; await api(`/api/tasks/${tid}/cancel`, {method:'POST'}); - toast('已取消', 'ok'); closeModal(); refreshKanban(); + toast('已取消', 'ok'); closeModal(); refreshProj(); } async function reopenTask(tid) { await api(`/api/tasks/${tid}`, {method:'PUT', body:{status:'todo'}}); - toast('任务已重新打开', 'ok'); closeModal(); refreshKanban(); + toast('任务已重新打开', 'ok'); closeModal(); refreshProj(); } async function delTask(tid) { if (!confirm('确认删除该任务及其日志/成本记录?')) return; await api(`/api/tasks/${tid}`, {method:'DELETE'}); - toast('已删除', 'ok'); closeModal(); refreshKanban(); + toast('已删除', 'ok'); closeModal(); refreshProj(); +} + +/* ---------- AI 规划 WBS ---------- */ +function openWbsModal() { + const {p} = projCtx; + openModal(` +

🧠 AI 辅助规划(WBS 任务分解)

+ + + +
`); + $('#wb-gen').addEventListener('click', async () => { + const goal = $('#wb-goal').value.trim(); + if (!goal) return toast('请填写项目目标', 'err'); + $('#wb-result').innerHTML = '
AI 正在拆解任务(约 10-30 秒)…
'; + try { + const r = await api(`/api/projects/${projCtx.pid}/wbs/generate`, {method:'POST', body:{goal}}); + let wbs = r.data; + $('#wb-result').innerHTML = ` +
生成 ${wbs.length} 个任务(可编辑后导入)· 花费 ¥${r.cost.toFixed(4)} / ${r.usage} tokens
+ ${wbs.map((t, i) => ` +
+
+ ${i+1} + +
+ +
依赖任务:${wbs.map((_, j) => ``).join('')}
+
`).join('')} + `; + $('#wb-import').addEventListener('click', async () => { + const tasks = wbs.map((t, i) => ({ + title: $(`[data-f="title"][data-i="${i}"]`).value.trim() || t.title, + description: $(`[data-f="desc"][data-i="${i}"]`).value, + depends_on: $$(`[data-dep="${i}"]:checked`).map(x => parseInt(x.value)) + })); + const rr = await api(`/api/projects/${projCtx.pid}/wbs/import`, {method:'POST', body:{tasks, worker_id: projCtx.workers[0]?.id || null}}); + toast(`✅ 已导入 ${rr.created} 个任务(含依赖关系)`, 'ok'); + closeModal(); refreshProj(); router(); + }); + } catch (e) { $('#wb-result').innerHTML = `
❌ ${esc(e.message)}
`; } + }); } /* ---------- Worker 管理 ---------- */ @@ -493,6 +737,170 @@ async function pageLogs() { [任务#${l.task_id} ${esc(l.task_title || '')}] ${esc(l.message)}`).join('') || '
暂无日志
'}`; } +/* ---------- 告警中心 ---------- */ +async function pageAlerts() { + const alerts = (await api('/api/alerts?limit=200')).data; + $('#main').innerHTML = ` +

告警中心任务失败 · 预算超标 · Worker 异常 · 成本拦截

+
+
${alerts.map(a => ` +
+
${ALERT_LEVEL[a.level] || 'ℹ️'} ${esc(a.title)} ${a.read ? '' : '未读'}
+
${esc(a.detail)}
+
${fmtTime(a.created_at)} · ${esc(a.type)}
+
`).join('') || '
暂无告警 🎉
'}
`; + refreshAlertBadge(); +} +async function readAlert(id) { + await api(`/api/alerts/${id}/read`, {method:'POST'}); + pageAlerts(); +} +async function readAllAlerts() { + await api('/api/alerts/read_all', {method:'POST'}); + toast('已全部标记为已读', 'ok'); pageAlerts(); +} + +/* ---------- 开放 API ---------- */ +async function pageApiTokens() { + const tokens = (await api('/api/tokens')).data; + $('#main').innerHTML = ` +

开放 API外部系统通过 Bearer Token 调平台能力

+
+ +
+
+
+
已生成的 Token
+ + ${tokens.map(t => ` + + `).join('') || ''} +
名称创建时间最近使用操作
${esc(t.name)}${fmtTime(t.created_at)}${t.last_used_at ? fmtTime(t.last_used_at) : '从未使用'}
暂无 Token
+
+
+
调用示例
+
+
POST /api/tasks 创建任务
+
curl -X POST http://<host>:16071/api/tasks \\
+  -H "Authorization: Bearer <TOKEN>" \\
+  -H "Content-Type: application/json" \\
+  -d '{"project_id":1,"title":"生成周报","description":"…"}'
+# 响应: {"ok":true,"id":42}
+
POST /api/tasks/42/run 派活执行
+
curl -X POST http://<host>:16071/api/tasks/42/run \\
+  -H "Authorization: Bearer <TOKEN>"
+
GET /api/tasks/42 查结果(status=review 时人工审核)
+
POST /api/tasks/42/review 审核
+
curl -X POST http://<host>:16071/api/tasks/42/review \\
+  -H "Authorization: Bearer <TOKEN>" -H "Content-Type: application/json" \\
+  -d '{"action":"approve"}'   # 或 {"action":"reject","reason":"…"}
+
+
+
`; +} + +async function createToken() { + const name = prompt('Token 名称(用途说明):') || 'default'; + const r = await api('/api/tokens', {method:'POST', body:{name}}); + alert('✅ 生成成功(只显示一次,请立即复制保存):\n\n' + r.token); + pageApiTokens(); +} +async function delToken(id) { + if (!confirm('删除后使用该 Token 的调用将失效,确认?')) return; + await api(`/api/tokens/${id}`, {method:'DELETE'}); + toast('已删除', 'ok'); pageApiTokens(); +} + +/* ---------- 通知设置 ---------- */ +async function pageSettings() { + const [chans, evs, st] = await Promise.all([api('/api/channels'), api('/api/events'), api('/api/settings')]); + $('#main').innerHTML = ` +

通知与设置飞书 / 企微群机器人 · 邮件 · 预算告警阈值

+
+
+
+
通知渠道
+ + ${chans.data.map(c => ` + + + + + + `).join('') || ''} +
名称类型订阅事件状态操作
${esc(c.name)}${c.type === 'feishu' ? '飞书' : c.type === 'wecom' ? '企微' : '邮件'}${(c.events || []).map(e => `${(evs.data.find(x => x.id === e) || {}).name || e}`).join('') || '—'}${c.enabled ? '启用' : '停用'} + +
暂无渠道,添加飞书/企微机器人后任务事件自动推送
+
💡 飞书群机器人:群设置 → 群机器人 → 添加自定义机器人,复制 Webhook 地址(以 https://open.feishu.cn/open-apis/bot/v2/hook/ 开头)。企微同理(https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=…)。
+
+
+
预算告警
+ +
+
+
告警会写入告警中心并推送到订阅了「预算告警」事件的渠道。
+
邮件通知
+
${st.data.email_configured ? '✅ 已配置 SMTP(config.py EMAIL)' : '⚠️ 未配置 SMTP(config.py 的 EMAIL 填写 host/user/password 后重启生效)'}
+
+
`; +} +async function saveSettings() { + await api('/api/settings', {method:'PUT', body:{budget_alert_ratio: parseFloat($('#st-ratio').value)}}); + toast('已保存', 'ok'); +} +async function testChannel(id) { + toast('正在发送测试消息…'); + try { const r = await api(`/api/channels/${id}/test`, {method:'POST'}); + r.ok ? toast('✅ 测试消息已送达', 'ok') : toast('❌ ' + r.msg, 'err'); } + catch (e) { toast('❌ ' + e.message, 'err'); } +} +async function delChannel(id) { + if (!confirm('确认删除该渠道?')) return; + await api(`/api/channels/${id}`, {method:'DELETE'}); + toast('已删除', 'ok'); pageSettings(); +} +function openChannelModal(c, events) { + c = c || {}; + openModal(` +

${c.id ? '编辑通知渠道' : '添加通知渠道'}

+ + +
+ + +
+ ${events.map(e => ``).join('')} +
+
+ `); + const toggle = () => { + const isMail = $('#cm-type').value === 'email'; + $('#cm-webhook-wrap').style.display = isMail ? 'none' : ''; + $('#cm-email-wrap').style.display = isMail ? '' : 'none'; + }; + $('#cm-type').addEventListener('change', toggle); + toggle(); + $('#cm-save').addEventListener('click', async () => { + const body = { + name: $('#cm-name').value.trim(), type: $('#cm-type').value, + webhook: $('#cm-webhook').value.trim(), email: $('#cm-email').value.trim(), + events: $$('#modal-box .check-group input:checked').map(x => x.value), + enabled: $('#cm-enabled').checked ? 1 : 0 + }; + if (!body.name || (body.type !== 'email' && !body.webhook) || (body.type === 'email' && !body.email)) + return toast('请填写完整', 'err'); + await api(c.id ? `/api/channels/${c.id}` : '/api/channels', {method: c.id ? 'PUT' : 'POST', body}); + toast('已保存', 'ok'); closeModal(); pageSettings(); + }); +} + /* ---------- 启动 ---------- */ (async function init() { try { @@ -501,5 +909,6 @@ async function pageLogs() { $('#logout-btn').style.display = 'block'; } catch (e) { showLogin(); return; } try { await api('/api/health'); } catch (e) { $('#conn-state').textContent = '● 服务异常'; $('#conn-state').className = 'conn err'; } + refreshAlertBadge(); router(); })(); diff --git a/static/index.html b/static/index.html index 4e3d2d8..bdfd599 100644 --- a/static/index.html +++ b/static/index.html @@ -16,6 +16,9 @@ 🧑‍💻 AI Worker 💰 成本报表 📜 运行日志 + 🚨 告警中心 + 🔌 开放 API + ⚙️ 通知设置
pLO`Y;}FagT-Jl0^88;3X)l6NF1si4M`c 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 Kk|SaG>c0TXUG6gg literal 0 HcmV?d00001 diff --git a/engine.py b/engine.py index 82580a2..ce603ae 100644 --- a/engine.py +++ b/engine.py @@ -1,13 +1,19 @@ # -*- coding: utf-8 -*- """ -任务执行引擎:后台线程执行单次 LLM 调用,记录日志与成本, -完成后进入「待审核」或直接「已完成」。 +任务执行引擎 V1: +- DAG 依赖校验 + 下游自动触发(串行/并行) +- RAG 知识库上下文注入 +- 预算告警(项目预算使用率阈值) +- 事件通知(待审核/完成/失败)与告警记录 """ +import json import threading import traceback import db import llm_gateway import config +import rag +import notify def _log(task_id, level, message): @@ -36,6 +42,25 @@ def _cost_record(task, worker, usage): usage['total_tokens'], usage['cost'], db.now())) +def _deps(task): + try: + return json.loads(task.get('depends_on') or '[]') + except Exception: + return [] + + +def check_dependencies(task): + """DAG 依赖检查:返回 (ok, blockers)""" + blockers = [] + for dep_id in _deps(task): + dep = db.q('SELECT id, title, status FROM tasks WHERE id=?', (dep_id,), one=True) + if not dep: + blockers.append(f'# {dep_id}(已删除)') + elif dep['status'] != 'done': + blockers.append(f'「{dep["title"]}」(#{dep_id}) {dep["status"]}') + return (not blockers), blockers + + def pick_worker_auto(task): """自动路由:按模型输入单价升序挑选 enabled Worker""" rows = db.q('SELECT * FROM workers WHERE status="enabled" ORDER BY id') @@ -63,17 +88,71 @@ def check_worker_limits(worker): return True, '' +def _project_cost(project_id): + rows = db.q('SELECT COALESCE(SUM(cost),0) AS t FROM cost_records WHERE project_id=?', + (project_id,)) + return rows[0]['t'] if rows else 0.0 + + def _check_project_budget(task): proj = db.q('SELECT * FROM projects WHERE id=?', (task['project_id'],), one=True) if proj and proj['budget_limit'] and proj['budget_limit'] > 0: - rows = db.q('SELECT COALESCE(SUM(cost),0) AS t FROM cost_records WHERE project_id=?', - (task['project_id'],)) - used = rows[0]['t'] if rows else 0 + used = _project_cost(task['project_id']) if used >= proj['budget_limit']: return False, f'项目预算已用完({used:.2f}/{proj["budget_limit"]:.2f} 元)' return True, '' +def _budget_alert(project_id): + """预算使用率告警(每次任务完成后检查,避免重复刷屏)""" + proj = db.q('SELECT * FROM projects WHERE id=?', (project_id,), one=True) + if not proj or not proj['budget_limit'] or proj['budget_limit'] <= 0: + return + used = _project_cost(project_id) + ratio = used / proj['budget_limit'] + if ratio >= config.BUDGET_ALERT_RATIO: + # 同项目 1 小时内只告警一次,避免刷屏 + dup = db.q('SELECT COUNT(*) c FROM alerts WHERE type="budget" AND detail LIKE ? ' + 'AND created_at > ?', (f'项目「{proj["name"]}」%', db.now() - 3600)) + if dup[0]['c'] == 0: + notify.notify('budget_alert', + f'预算告警:项目「{proj["name"]}」已使用 {ratio*100:.0f}%', + f'已花费 ¥{used:.2f} / 预算 ¥{proj["budget_limit"]:.2f},' + f'超过阈值 {config.BUDGET_ALERT_RATIO*100:.0f}%,请关注成本控制。', + save_alert=True, level='warn', atype='budget') + + +def _build_messages(task, worker): + """构造提示词:任务指令 + RAG 知识库上下文""" + messages = [] + if worker['system_prompt']: + messages.append({'role': 'system', 'content': worker['system_prompt']}) + user_text = task['description'] or task['title'] + ctx, refs = rag.build_context(task['project_id'], user_text) + if ctx: + user_text = f'{ctx}\n\n----\n\n任务指令:{user_text}' + _log(task['id'], 'info', f'📚 RAG 知识库命中 {len(refs)} 个片段:' + ';'.join(refs[:5])) + messages.append({'role': 'user', 'content': user_text}) + return messages + + +def _trigger_downstream(task): + """DAG:任务完成后自动触发所有就绪的下游任务""" + rows = db.q('SELECT * FROM tasks WHERE status IN ("todo","failed")') + triggered = [] + for t in rows: + deps = _deps(t) + if task['id'] not in deps: + continue + ok, blockers = check_dependencies(t) + if ok: + if runner.submit(t['id']): + triggered.append(t) + else: + _log(t['id'], 'info', f'⏳ 等待前置任务完成:' + '、'.join(blockers)) + return triggered + + def run_task(task_id): """在后台线程中执行任务""" task = db.q('SELECT * FROM tasks WHERE id=?', (task_id,), one=True) @@ -82,6 +161,17 @@ def run_task(task_id): if task['status'] == 'running': return + # DAG 依赖检查 + ok, blockers = check_dependencies(task) + if not ok: + _set_task(task_id, status='failed', error='前置任务未完成:' + '、'.join(blockers), + finished_at=db.now()) + _log(task_id, 'error', '❌ 依赖未满足,无法执行:' + '、'.join(blockers)) + notify.notify('task_failed', f'任务失败:{task["title"]}', + f'项目 #{task["project_id"]} 任务「{task["title"]}」因依赖未完成被拒绝执行:' + + '、'.join(blockers), save_alert=True, level='warn', atype='task_failed') + return + # 确定 Worker worker = None if task['worker_id']: @@ -90,6 +180,9 @@ def run_task(task_id): _set_task(task_id, status='failed', error='指定 Worker 不存在或已停用', finished_at=db.now()) _log(task_id, 'error', '指定 Worker 不存在或已停用') + notify.notify('worker_alert', f'Worker 异常:任务「{task["title"]}」', + f'指定 Worker #{task["worker_id"]} 不存在或已停用', save_alert=True, + level='warn', atype='worker_alert') return else: worker = pick_worker_auto(task) @@ -97,6 +190,9 @@ def run_task(task_id): _set_task(task_id, status='failed', error='无可用 Worker(自动路由失败)', finished_at=db.now()) _log(task_id, 'error', '自动路由失败:无可用 Worker') + notify.notify('worker_alert', f'Worker 异常:任务「{task["title"]}」', + '自动路由失败:没有可用的 Worker', save_alert=True, + level='warn', atype='worker_alert') return _set_task(task_id, worker_id=worker['id']) _log(task_id, 'info', f'自动路由 → Worker「{worker["name"]}」({worker["provider"]}/{worker["model"]})') @@ -106,29 +202,31 @@ def run_task(task_id): if not ok: _set_task(task_id, status='failed', error=reason, finished_at=db.now()) _log(task_id, 'error', reason) + notify.notify('budget_alert', f'成本上限拦截:任务「{task["title"]}」', reason, + save_alert=True, level='warn', atype='budget') return ok, reason = _check_project_budget(task) if not ok: _set_task(task_id, status='failed', error=reason, finished_at=db.now()) _log(task_id, 'error', reason) + notify.notify('budget_alert', f'预算拦截:任务「{task["title"]}」', reason, + save_alert=True, level='warn', atype='budget') return _set_task(task_id, status='running', started_at=db.now(), error='') _log(task_id, 'info', f'开始执行:Worker「{worker["name"]}」 模型 {worker["provider"]}/{worker["model"]}') - messages = [] - if worker['system_prompt']: - messages.append({'role': 'system', 'content': worker['system_prompt']}) - messages.append({'role': 'user', 'content': task['description'] or task['title']}) - try: usage = llm_gateway.chat( - worker['provider'], worker['model'], messages, + worker['provider'], worker['model'], _build_messages(task, worker), temperature=worker['temperature'], max_tokens=worker['max_tokens'], base_url=worker['base_url'] or None, api_key=worker['api_key'] or None) except Exception as e: _set_task(task_id, status='failed', error=str(e), finished_at=db.now()) _log(task_id, 'error', f'执行失败: {e}') + notify.notify('task_failed', f'任务失败:{task["title"]}', + f'项目 #{task["project_id"]} 任务「{task["title"]}」执行出错:{str(e)[:300]}', + save_alert=True, level='warn', atype='task_failed') return _cost_record(task, worker, usage) @@ -146,8 +244,23 @@ def run_task(task_id): _set_task(task_id, **fields) if new_status == 'review': _log(task_id, 'info', '产出已提交,等待人工审核(HITL)') + notify.notify('task_review', f'任务待审核:{task["title"]}', + f'项目 #{task["project_id"]} 任务「{task["title"]}」已完成,等待人工验收。\n' + f'模型 {worker["provider"]}/{worker["model"]} · {usage["total_tokens"]} tokens · ¥{usage["cost"]:.4f}', + save_alert=True, level='info', atype='task_review') else: _log(task_id, 'success', '任务完成(无需审核)') + notify.notify('task_done', f'任务完成:{task["title"]}', + f'项目 #{task["project_id"]} 任务「{task["title"]}」执行完毕,' + f'成本 ¥{usage["cost"]:.4f},tokens {usage["total_tokens"]}', + save_alert=False) + + _budget_alert(task['project_id']) + + # DAG:触发下游就绪任务 + downstream = _trigger_downstream(task) + for t in downstream: + _log(t['id'], 'info', f'🔗 前置任务「{task["title"]}」已完成,自动触发执行') class TaskRunner: diff --git a/notify.py b/notify.py new file mode 100644 index 0000000..3c2d2eb --- /dev/null +++ b/notify.py @@ -0,0 +1,111 @@ +# -*- coding: utf-8 -*- +""" +通知渠道:飞书/企微群机器人 Webhook + 邮件(可选 SMTP) +事件:task_review(待审核)/ task_done(完成)/ task_failed(失败) + budget_alert(预算告警)/ worker_alert(Worker 异常)/ wbs_ready(规划完成) +""" +import json +import smtplib +import requests +import db + +EVENTS = { + 'task_review': '任务待审核', + 'task_done': '任务完成', + 'task_failed': '任务失败', + 'budget_alert': '预算告警', + 'worker_alert': 'Worker 异常', + 'wbs_ready': '规划完成', +} + + +def _channels_for(event): + rows = db.q('SELECT * FROM notify_channels WHERE enabled=1') + out = [] + for r in rows: + try: + evs = json.loads(r['events'] or '[]') + except Exception: + evs = [] + if event in evs: + out.append(r) + return out + + +def _send_feishu(ch, title, text): + payload = {'msg_type': 'text', 'content': {'text': f'【AI Worker 平台】{title}\n{text}'}} + r = requests.post(ch['webhook'], json=payload, timeout=10) + try: + body = r.json() + if body.get('code', 0) not in (0, None): + raise ValueError(f'飞书返回错误: {body.get("msg", r.text[:100])}') + except ValueError: + raise + except Exception: + pass + return r + + +def _send_wecom(ch, title, text): + payload = {'msgtype': 'text', 'text': {'content': f'【AI Worker 平台】{title}\n{text}'}} + return requests.post(ch['webhook'], json=payload, timeout=10) + + +def _send_email(ch, title, text): + from config import EMAIL # 邮件配置在 config.py + if not EMAIL.get('host'): + return None + msg = f'From: {EMAIL["from_name"]} <{EMAIL["user"]}>\nTo: {ch["email"]}\n' \ + f'Subject: 【AI Worker 平台】{title}\nContent-Type: text/plain; charset=utf-8\n\n{text}' + try: + s = smtplib.SMTP(EMAIL['host'], EMAIL['port']) + if EMAIL.get('starttls'): + s.starttls() + if EMAIL.get('user'): + s.login(EMAIL['user'], EMAIL['password']) + s.sendmail(EMAIL['user'], [ch['email']], msg.encode('utf-8')) + s.quit() + return True + except Exception as e: + return f'邮件发送失败: {e}' + + +def notify(event, title, text, save_alert=True, level='info', atype=None): + """触发事件:写告警记录 + 推送所有订阅渠道""" + if save_alert: + db.w('INSERT INTO alerts (type, level, title, detail, read, created_at) ' + 'VALUES (?,?,?,?,0,?)', + (atype or event, level, title, text[:500], db.now())) + ok, fail = [], [] + for ch in _channels_for(event): + try: + if ch['type'] == 'feishu': + r = _send_feishu(ch, title, text) + (ok if r.status_code == 200 else fail).append(f'{ch["name"]}({r.status_code})') + elif ch['type'] == 'wecom': + r = _send_wecom(ch, title, text) + (ok if r.status_code == 200 else fail).append(f'{ch["name"]}({r.status_code})') + elif ch['type'] == 'email': + r = _send_email(ch, title, text) + (ok if r is True else fail).append(f'{ch["name"]}({r})') + except Exception as e: + fail.append(f'{ch["name"]}({e})') + return {'ok': ok, 'fail': fail} + + +def test_channel(ch): + """渠道连通性测试""" + text = '这是一条来自 AI Worker 项目管理平台的连通性测试消息 ✅' + try: + if ch['type'] == 'feishu': + r = _send_feishu(ch, '连通测试', text) + return r.status_code == 200, f'HTTP {r.status_code}' + if ch['type'] == 'wecom': + r = _send_wecom(ch, '连通测试', text) + return r.status_code == 200, f'HTTP {r.status_code}' + if ch['type'] == 'email': + r = _send_email(ch, '连通测试', text) + return r is True, str(r) + except Exception as e: + return False, str(e) + return False, '未知渠道类型' diff --git a/rag.py b/rag.py new file mode 100644 index 0000000..3af043c --- /dev/null +++ b/rag.py @@ -0,0 +1,119 @@ +# -*- coding: utf-8 -*- +""" +RAG 知识库:文档分块 + jieba 分词 + BM25 检索(本地零依赖) +升级路径:接入商用 embedding + pgvector/Qdrant 做向量检索(见 README) +""" +import math +import re +import jieba +import db + +CHUNK_SIZE = 400 # 每块目标字数 +CHUNK_OVERLAP = 80 # 块间重叠 + + +def segment(text): + """jieba 分词,去停用词/单字/空白""" + text = re.sub(r'[\s,。!?、;:""''()【】《》·…—0-9a-zA-Z-]+', ' ', text) + words = [w for w in jieba.cut(text) if len(w.strip()) > 1 and not w.isspace()] + return words + +def split_chunks(content, size=CHUNK_SIZE, overlap=CHUNK_OVERLAP): + """按段落聚合切块,避免从句子中间切断""" + paras = [p.strip() for p in re.split(r'\n+', content) if p.strip()] + chunks, buf, buf_len = [], '', 0 + for p in paras: + if buf_len + len(p) > size and buf: + chunks.append(buf) + tail = buf[-overlap:] if overlap else '' + buf, buf_len = tail + p, len(tail) + len(p) + else: + buf += ('\n' if buf else '') + p + buf_len += len(p) + if buf: + chunks.append(buf) + return chunks or [content[:size]] + + +def rebuild_document(doc_id): + """重新分块索引文档""" + doc = db.q('SELECT * FROM documents WHERE id=?', (doc_id,), one=True) + if not doc: + return 0 + db.w('DELETE FROM doc_chunks WHERE document_id=?', (doc_id,)) + chunks = split_chunks(doc['content']) + for i, c in enumerate(chunks): + db.w('INSERT INTO doc_chunks (document_id, idx, content, tokens) VALUES (?,?,?,?)', + (doc_id, i, c, len(segment(c)))) + db.w('UPDATE documents SET chunk_size=?, updated_at=? WHERE id=?', (len(chunks), db.now(), doc_id)) + return len(chunks) + + +class BM25Index: + """轻量 BM25:按查询词 IDF 加权打分""" + + def __init__(self, chunks): + # chunks: [{id, document_id, content, tokens}] + self.chunks = chunks + self.doc_len = [max(c['tokens'], 1) for c in chunks] + self.avg_len = sum(self.doc_len) / max(len(self.doc_len), 1) + self.N = len(chunks) + self.k1, self.b = 1.5, 0.75 + # 倒排:term -> set of chunk idx + self.inv = {} + self.df = {} + for i, c in enumerate(chunks): + seen = set() + for w in segment(c['content']): + if w in seen: + continue + seen.add(w) + self.inv.setdefault(w, []).append(i) + for w, lst in self.inv.items(): + self.df[w] = len(lst) + + def search(self, query, top_k=5): + q_terms = [w for w in segment(query)] + if not q_terms or not self.N: + return [] + scores = {} + for w in q_terms: + postings = self.inv.get(w, []) + if not postings: + continue + idf = math.log(1 + (self.N - self.df[w] + 0.5) / (self.df[w] + 0.5)) + for idx in postings: + tf = sum(1 for x in segment(self.chunks[idx]['content']) if x == w) + denom = tf + self.k1 * (1 - self.b + self.b * self.doc_len[idx] / self.avg_len) + scores[idx] = scores.get(idx, 0) + idf * (tf * (self.k1 + 1)) / denom + ranked = sorted(scores.items(), key=lambda x: -x[1])[:top_k] + return [{'chunk_id': self.chunks[i]['id'], 'document_id': self.chunks[i]['document_id'], + 'content': self.chunks[i]['content'], 'score': round(s, 4)} + for i, s in ranked] + + +def search_project(project_id, query, top_k=5): + """在项目知识库中检索,返回 (片段列表, 是否命中)""" + chunks = db.q( + 'SELECT c.id, c.document_id, c.content, c.tokens FROM doc_chunks c ' + 'JOIN documents d ON d.id=c.document_id WHERE d.project_id=?', + (project_id,)) + if not chunks: + return [], False + idx = BM25Index(chunks) + return idx.search(query, top_k), True + + +def build_context(project_id, query, top_k=4): + """生成注入提示词的检索上下文(含来源标注)""" + hits, hit = search_project(project_id, query, top_k) + if not hit or not hits: + return '', [] + parts, refs = [], [] + for h in hits: + doc = db.q('SELECT name FROM documents WHERE id=?', (h['document_id'],), one=True) + name = doc['name'] if doc else f'文档#{h["document_id"]}' + parts.append(f'【来源:{name}】\n{h["content"]}') + refs.append(f'{name}#块{h["chunk_id"]}') + ctx = '以下是项目知识库中的相关资料,回答时请优先参考:\n\n' + '\n\n---\n\n'.join(parts) + return ctx, refs diff --git a/static/app.js b/static/app.js index de065d5..5ba2bcd 100644 --- a/static/app.js +++ b/static/app.js @@ -1,4 +1,4 @@ -/* AI Worker 项目管理平台 - 前端 SPA */ +/* AI Worker 项目管理平台 - 前端 SPA(V1) */ 'use strict'; const $ = (sel, el = document) => el.querySelector(sel); @@ -15,6 +15,7 @@ const STATUS = { }; const PRI = {high:'高', medium:'中', low:'低'}; const KANBAN_ORDER = ['todo','running','review','done','rejected','failed']; +const ALERT_LEVEL = {info:'ℹ️', warn:'⚠️', critical:'🚨'}; /* ---------- API ---------- */ async function api(path, opts = {}) { @@ -62,18 +63,32 @@ function openModal(html) { function closeModal() { $('#modal-mask').style.display = 'none'; $('#modal-box').innerHTML = ''; } $('#modal-mask').addEventListener('click', e => { if (e.target.id === 'modal-mask') closeModal(); }); +/* ---------- 告警角标 ---------- */ +async function refreshAlertBadge() { + try { + const r = await api('/api/alerts/unread_count'); + const b = $('#alert-badge'); + if (r.count > 0) { b.textContent = r.count; b.style.display = 'inline'; } + else b.style.display = 'none'; + } catch (e) {} +} +setInterval(refreshAlertBadge, 30000); + /* ---------- Router ---------- */ const routes = { 'dashboard': pageDashboard, 'projects': pageProjects, 'project': pageProject, - 'workers': pageWorkers, 'reports': pageReports, 'logs': pageLogs + 'workers': pageWorkers, 'reports': pageReports, 'logs': pageLogs, + 'alerts': pageAlerts, 'api': pageApiTokens, 'settings': pageSettings }; function router() { const hash = location.hash.replace(/^#\//, '') || 'dashboard'; - const [name, ...rest] = hash.split('/'); + const parts = hash.split('/'); + const name = parts[0]; const fn = routes[name] || pageDashboard; - $$('#sidebar nav a').forEach(a => a.classList.toggle('active', a.dataset.route === (name === 'project' ? 'projects' : name))); + const navMap = {project:'projects', api:'api', settings:'settings', alerts:'alerts'}; + $$('#sidebar nav a').forEach(a => a.classList.toggle('active', a.dataset.route === (navMap[name] || name))); $('#main').innerHTML = '
加载中…
'; - fn(rest).catch(e => { $('#main').innerHTML = `
${esc(e.message)}
`; }); + fn(parts.slice(1)).catch(e => { $('#main').innerHTML = `
${esc(e.message)}
`; }); } window.addEventListener('hashchange', router); @@ -172,58 +187,85 @@ function openProjectModal(p = {}) { }); } -/* ---------- 项目详情(看板) ---------- */ -let kanbanData = null; -async function pageProject([pid]) { +/* ---------- 项目详情(看板 / DAG / 知识库) ---------- */ +let projCtx = null; // {pid, p, tasks, workers, tab} + +async function pageProject([pid, tab]) { const p = (await api(`/api/projects/${pid}`)).data; const tasks = (await api(`/api/tasks?project_id=${pid}`)).data; const workers = (await api('/api/workers')).data; - kanbanData = {pid, tasks, workers}; + projCtx = {pid, p, tasks, workers, tab: tab || 'kanban'}; + renderProjectShell(); + if (projCtx.tab === 'dag') renderDag(); + else if (projCtx.tab === 'kb') renderKb(); + else renderKanban(); +} + +function renderProjectShell() { + const {p, pid, tab} = projCtx; $('#main').innerHTML = `
← 返回

${esc(p.name)}${esc(p.objective || '')}

- + +
-
${KANBAN_ORDER.map(s => ` -
-
${STATUS[s].label} - 0
-
-
`).join('')}
`; - renderKanban(); - $$('.kb-body').forEach(bd => { - bd.addEventListener('dragover', e => { e.preventDefault(); bd.classList.add('drag-over'); }); - bd.addEventListener('dragleave', () => bd.classList.remove('drag-over')); - bd.addEventListener('drop', e => { - e.preventDefault(); bd.classList.remove('drag-over'); - const tid = e.dataTransfer.getData('text/plain'); - moveTask(parseInt(tid), bd.dataset.drop); - }); - }); + +
`; } +async function refreshProj() { + projCtx.tasks = (await api(`/api/tasks?project_id=${projCtx.pid}`)).data; + if (projCtx.tab === 'dag') renderDag(); + else renderKanban(); +} + +/* --- 看板 --- */ function renderKanban() { - const {tasks} = kanbanData; + const {tasks} = projCtx; const by = {}; tasks.forEach(t => { (by[t.status] = by[t.status] || []).push(t); }); + $('#tab-body').innerHTML = `
${KANBAN_ORDER.map(s => ` +
+
${STATUS[s].label} + 0
+
+
`).join('')}
`; KANBAN_ORDER.forEach(s => { const list = by[s] || []; $('#cnt-' + s).textContent = list.length; $('#kanban .kb-body[data-drop="' + s + '"]').innerHTML = list.map(t => ` -
${esc(t.title)}
●${PRI[t.priority]} ${t.worker_id ? `Worker#${t.worker_id}` : '自动路由'} + ${t.depends_on && t.depends_on.length ? `依赖×${t.depends_on.length}` : ''} ${t.rejection_count ? `打回×${t.rejection_count}` : ''} ${t.status === 'running' ? '' : ''}
${t.status === 'done' ? '
✅ 已交付 v' + t.output_version + '
' : ''}
`).join('') || '
'; }); + bindKanbanDnD(); +} + +function bindKanbanDnD() { + $$('.kb-body').forEach(bd => { + bd.addEventListener('dragover', e => { e.preventDefault(); bd.classList.add('drag-over'); }); + bd.addEventListener('dragleave', () => bd.classList.remove('drag-over')); + bd.addEventListener('drop', e => { + e.preventDefault(); bd.classList.remove('drag-over'); + const tid = parseInt(e.dataTransfer.getData('text/plain')); + moveTask(tid, bd.dataset.drop); + }); + }); $$('.kb-card[draggable="true"]').forEach(c => { c.addEventListener('dragstart', e => { e.dataTransfer.setData('text/plain', c.dataset.id); @@ -234,76 +276,228 @@ function renderKanban() { } async function moveTask(tid, status) { - const t = kanbanData.tasks.find(x => x.id === tid); + const t = projCtx.tasks.find(x => x.id === tid); if (!t || t.status === status) return; if (['running', 'review'].includes(status)) return toast('不能直接拖到该状态', 'err'); if (status === 'todo' && !['rejected', 'failed', 'done'].includes(t.status)) return; if (status === 'done') return toast('完成请走审核流(审核通过)', 'err'); await api(`/api/tasks/${tid}`, {method:'PUT', body:{status}}); - toast('状态已更新', 'ok'); refreshKanban(); + toast('状态已更新', 'ok'); refreshProj(); } -async function refreshKanban() { - const {pid} = kanbanData; - kanbanData.tasks = (await api(`/api/tasks?project_id=${pid}`)).data; - renderKanban(); +/* --- DAG 工作流 --- */ +function renderDag() { + const {tasks} = projCtx; + const idMap = {}; + tasks.forEach(t => { idMap[t.id] = t; }); + const edges = []; + tasks.forEach(t => (t.depends_on || []).forEach(d => { if (idMap[d]) edges.push({from:d, to:t.id}); })); + + // 拓扑分层:最长路径 + const layer = {}; + const visit = id => { + if (layer[id] !== undefined) return layer[id]; + const t = idMap[id]; + let l = 0; + (t.depends_on || []).forEach(d => { if (idMap[d]) l = Math.max(l, visit(d) + 1); }); + layer[id] = l; + return l; + }; + tasks.forEach(t => visit(t.id)); + const layers = {}; + tasks.forEach(t => { (layers[layer[t.id]] = layers[layer[t.id]] || []).push(t.id); }); + + const W = 260, H = 110, PAD = 60; + const width = Math.max(2, Object.keys(layers).length) * W + PAD; + const height = Math.max(...tasks.map(t => layer[t.id] === undefined ? 0 : layer[t.id]), 0) * H + PAD + 60; + const pos = {}; + Object.entries(layers).forEach(([l, ids]) => { + const x = PAD + parseInt(l) * W; + const step = Math.min(H - 60, Math.max(70, (height - 2*PAD) / (ids.length + 1))); + ids.forEach((id, i) => { pos[id] = {x, y: PAD + (i + 1) * step}; }); + }); + const colors = {todo:'#2a3550', running:'#173b6b', review:'#4a3a10', done:'#0f3d2e', rejected:'#4a1c1c', failed:'#3d1c2e', cancelled:'#333'}; + const ready = tasks.filter(t => (t.status === 'todo' || t.status === 'failed') && (!t.depends_on || t.depends_on.every(d => idMap[d]?.status === 'done'))); + $('#tab-body').innerHTML = ` +
+ 共 ${tasks.length} 个节点 · ${edges.length} 条依赖边${ready.length ? ` · ${ready.length} 个任务就绪` : ''} + +
+
+ + ${edges.map(e => { + const a = pos[e.from], b = pos[e.to]; + if (!a || !b) return ''; + const mx = (a.x + b.x) / 2; + const act = idMap[e.from].status === 'done'; + return ``; + }).join('')} + ${tasks.map(t => { + const p = pos[t.id]; + const col = colors[t.status]; + return ` + + #${t.id} ${esc(t.title).slice(0,10)} + ${STATUS[t.status].label}${t.depends_on?.length ? ' · 前置'+t.depends_on.length : ''} + `; + }).join('')} +
+
💡 节点点击查看详情;前置任务完成后下游自动触发。执行整个工作流 = 派发所有就绪任务(含失败重试)。
`; } -function openTaskModal(pid) { - const workers = kanbanData ? kanbanData.workers : []; +async function runWorkflow() { + const r = await api(`/api/projects/${projCtx.pid}/workflow/run`, {method:'POST'}); + toast(`🚀 已派发 ${r.started.length} 个任务${r.blocked.length ? `,${r.blocked.length} 个因依赖阻塞` : ''}`, 'ok'); + setTimeout(refreshProj, 1500); +} + +/* --- 知识库 RAG --- */ +async function renderKb() { + const docs = (await api(`/api/projects/${projCtx.pid}/documents`)).data; + $('#tab-body').innerHTML = ` +
+ 文档共 ${docs.length} 篇 · 任务执行时自动检索注入上下文 + +
+
+ +
+
+
文档列表
+
${docs.map(d => ` +
+
${esc(d.name)} ${d.chunks} 块 ${d.content.length} 字 · ${fmtTime(d.created_at)}
+
+
+
`).join('') || '
暂无文档,添加产品资料/知识手册后 AI Worker 干活时会自动参考
'} +
+
`; +} + +async function kbSearch() { + const q = $('#kb-q').value.trim(); + if (!q) return; + const r = await api(`/api/projects/${projCtx.pid}/search?q=${encodeURIComponent(q)}`); + $('#kb-results').innerHTML = r.data.length ? r.data.map(h => ` +
+
📄 命中 文档#${h.document_id} · 块${h.chunk_id} · 得分 ${h.score}
+
${esc(h.content.slice(0, 160))}${h.content.length > 160 ? '…' : ''}
+
`).join('') : '
无命中(知识库为空或关键词不匹配)
'; +} + +function openDocModal(docId) { + (docId ? api(`/api/documents/${docId}`) : Promise.resolve({data: {}})).then(async r => { + const doc = docId ? r.data : {}; + openModal(` +

${docId ? '编辑文档' : '添加文档'}

+ + + + + `); + $('#dm-file').addEventListener('change', e => { + const f = e.target.files[0]; + if (!f) return; + const rd = new FileReader(); + rd.onload = () => { $('#dm-content').value = rd.result; if (!$('#dm-name').value) $('#dm-name').value = f.name; }; + rd.readAsText(f); + }); + $('#dm-save').addEventListener('click', async () => { + const name = $('#dm-name').value.trim(), content = $('#dm-content').value; + if (!name || !content.trim()) return toast('名称和内容必填', 'err'); + const rr = await api(docId ? `/api/documents/${docId}` : `/api/projects/${projCtx.pid}/documents`, + {method: docId ? 'PUT' : 'POST', body: docId ? {name, content} : {name, content}}); + toast(docId ? '已保存' : `已添加,分块 ${rr.chunks} 块`, 'ok'); closeModal(); renderKb(); + }); + }); +} + +async function delDoc(docId) { + if (!confirm('确认删除该文档及其分块?')) return; + await api(`/api/documents/${docId}`, {method:'DELETE'}); + toast('已删除', 'ok'); renderKb(); +} + +/* --- 新建任务(含依赖) --- */ +function openTaskModal(editTask) { + const t = editTask || {}; + const {tasks, workers, pid} = projCtx || {tasks: [], workers: []}; + const candidates = tasks.filter(x => x.status !== 'running' && x.id !== t.id); + const deps = t.depends_on || []; openModal(` -

新建任务

- - +

${t.id ? '编辑任务' : '新建任务'}

+ +
-
-
+
+
+
+ +
+ ${candidates.length ? candidates.map(c => ` + `).join('') : '暂无其他任务可选'}
`); $('#tm-save').addEventListener('click', async () => { const title = $('#tm-title').value.trim(); if (!title) return toast('请填写任务标题', 'err'); + const body = { + title, description: $('#tm-desc').value, + worker_id: $('#tm-worker').value ? parseInt($('#tm-worker').value) : null, + priority: $('#tm-pri').value, deadline: $('#tm-deadline').value, + review_required: $('#tm-review').value === '1', + depends_on: $$('#tm-deps input:checked').map(x => parseInt(x.value)) + }; try { - const r = await api('/api/tasks', {method:'POST', body:{ - project_id: pid, title, description: $('#tm-desc').value, - worker_id: $('#tm-worker').value ? parseInt($('#tm-worker').value) : null, - priority: $('#tm-pri').value, deadline: $('#tm-deadline').value, - review_required: $('#tm-review').value === '1' - }}); - toast('任务已创建', 'ok'); closeModal(); refreshKanban(); - if (confirm('任务已创建,立即派给 AI Worker 执行?')) runTask(r.id); + if (t.id) { + await api(`/api/tasks/${t.id}`, {method:'PUT', body}); + toast('已保存', 'ok'); closeModal(); refreshProj(); + } else { + const r = await api('/api/tasks', {method:'POST', body:{...body, project_id: projCtx.pid}}); + toast('任务已创建', 'ok'); closeModal(); refreshProj(); + if (confirm('任务已创建,立即派给 AI Worker 执行?')) runTask(r.id); + } } catch (e) { toast(e.message, 'err'); } }); } async function runTask(tid) { - try { await api(`/api/tasks/${tid}/run`, {method:'POST'}); toast('🚀 已派活,AI Worker 开始执行', 'ok'); refreshKanban(); } + try { await api(`/api/tasks/${tid}/run`, {method:'POST'}); toast('🚀 已派活,AI Worker 开始执行', 'ok'); refreshProj(); } catch (e) { toast(e.message, 'err'); } } /* ---------- 任务详情(含审核) ---------- */ async function openTaskDetail(tid) { const t = (await api(`/api/tasks/${tid}`)).data; - const workers = kanbanData ? kanbanData.workers : (await api('/api/workers')).data; + const workers = projCtx?.workers || (await api('/api/workers')).data; const w = t.worker || (t.worker_id ? {id: t.worker_id, name: `#${t.worker_id}`} : null); + const deps = (t.depends_on || []).map(d => projCtx?.tasks.find(x => x.id === d)).filter(Boolean); openModal(`

${esc(t.title)}${STATUS[t.status].label}