""" pipeline-service: sdlc_ability — SDLC 开发产线能力包 以「可插拔能力包」形式定义 SDLC 产线专属能力: - 工具定义(ToolDefinition):任务/交付件/问答/仓库/进度/Bug - prompt 片段(追加到 core 通用心智之后) - 工具 handler(独立函数,签名 async def handler(sor, params, ctx) -> str) 通过 register_ability 注册到 pipeline-core 的 PipelineAbility 注册表, AgentExecutor 按 pipeline_id 动态挂载。 未来 B 产线 = 新增 b_ability.py + register_ability,零侵入 core / AgentExecutor。 """ import json import os from pipeline_core import ( ToolDefinition, PipelineAbility, RoleSpec, register_ability, SlashCommand, register_slash_command, ) # ── 工具定义 ── SDL_TOOLS = [ ToolDefinition( name="create_task", description="创建开发任务,分配给角色agent执行", parameters={ "title": "任务标题", "role": "目标角色(requirement/design/develop/test/deploy)", "description": "任务详细描述", "iteration_id": "归属迭代(可选,默认当前迭代)", }, category="task", ), ToolDefinition( name="list_tasks", description="查看当前项目的任务列表", parameters={"role": "按角色筛选(可选)", "state": "按状态筛选(可选)"}, category="task", ), ToolDefinition( name="task_detail", description="查看任务详情", parameters={"task_id": "任务ID"}, category="task", ), ToolDefinition( name="reset_task_retry", description="人工处理后恢复任务:重复计数清零+重新执行+恢复项目推进(任务重复超限抛故障给人工后使用)", parameters={"task_id": "任务ID"}, category="task", ), ToolDefinition( name="approve_task", description="审核通过任务(review→approved,PM 验收)", parameters={"task_id": "任务ID", "comment": "审核意见(可选)"}, category="task", ), ToolDefinition( name="complete_task", description="标记任务完成(approved/review→completed)", parameters={"task_id": "任务ID"}, category="task", ), ToolDefinition( name="cancel_task", description="取消任务(重做/作废前必须先取消旧任务,避免两个相同任务并存)", parameters={"task_id": "任务ID", "comment": "取消原因(可选)"}, category="task", ), ToolDefinition( name="start_agents", description="启动角色agent执行已提交的任务", parameters={}, category="agent", ), ToolDefinition( name="diagnose_project", description="诊断项目:汇总卡点/失败/问题/运行中任务", parameters={}, category="agent", ), ToolDefinition( name="list_deliverables", description="查看交付件列表", parameters={"role": "按角色筛选(可选)"}, category="task", ), ToolDefinition( name="view_deliverable", description="查看交付件详细内容", parameters={"deliverable_id": "交付件ID"}, category="task", ), ToolDefinition( name="list_questions", description="查看待我(主agent)处理的问题", parameters={}, category="agent", ), ToolDefinition( name="answer_question", description="回答agent提出的问题(解决→停止冒泡)", parameters={"question_id": "问题ID", "answer": "回答内容"}, category="agent", ), ToolDefinition( name="escalate_question", description="答不了的问题沿冒泡路径转给下一个处理方(通常转客户)", parameters={"question_id": "问题ID"}, category="agent", ), ToolDefinition( name="add_repo", description="添加项目关联的Git仓库", parameters={"repo_url": "仓库URL", "repo_name": "仓库名称", "default_branch": "默认分支(默认main)"}, category="repo", ), ToolDefinition( name="list_repos", description="查看项目关联的仓库列表", parameters={}, category="repo", ), ToolDefinition( name="clone_repo", description="克隆项目关联的仓库到工作空间repos/下(不传url则clone全部,传则clone指定)", parameters={"repo_url": "仓库URL(可选)"}, category="repo", ), ToolDefinition( name="check_progress", description="查看项目整体进展", parameters={}, category="agent", ), ToolDefinition( name="add_bug", description="提交Bug", parameters={"title": "Bug标题", "description": "描述", "severity": "严重程度"}, category="task", ), ToolDefinition( name="propose_feature", description="提出新功能/需求(用户提出新需求或需求拆解时用)", parameters={"feature_name": "功能名称", "description": "功能描述", "iteration_id": "所属迭代(可选)", "priority": "优先级(P0/P1/P2/P3,默认P2)"}, category="feature", ), ToolDefinition( name="list_features", description="查看当前项目功能列表", parameters={"status": "按状态筛选(可选)"}, category="feature", ), ToolDefinition( name="approve_feature", description="审批通过功能(需求评审通过)", parameters={"feature_id": "功能ID"}, category="feature", ), ToolDefinition( name="reject_feature", description="驳回功能(附意见)", parameters={"feature_id": "功能ID", "comment": "驳回意见"}, category="feature", ), ToolDefinition( name="verify_feature", description="验证功能(验收通过)", parameters={"feature_id": "功能ID"}, category="feature", ), # ── 项目生命周期 ── ToolDefinition(name="list_projects", description="列出项目列表(含状态)", parameters={"status": "按状态筛选(可选)"}, category="project"), ToolDefinition(name="list_role_models", description="列出项目各角色的模型配置(缺省用项目缺省模型)", parameters={"project_id": "项目ID"}, category="project"), ToolDefinition(name="set_role_model", description="设置某角色的模型(如 deploy_test 用 qwen3.8-max;model_name 空=清除回退缺省)", parameters={"project_id": "项目ID", "role": "角色名(agent.develop/agent.deploy_test等)", "model_name": "模型名(llm.name,空=回退缺省)"}, category="project"), ToolDefinition(name="start_project", description="启动项目(draft/archived→active)", parameters={"project_id": "项目ID"}, category="project"), ToolDefinition(name="complete_project", description="完成项目(active→completed)", parameters={"project_id": "项目ID"}, category="project"), ToolDefinition(name="archive_project", description="归档项目(completed→archived)", parameters={"project_id": "项目ID"}, category="project"), ToolDefinition(name="reopen_project", description="重新打开归档项目(archived→active)", parameters={"project_id": "项目ID"}, category="project"), ToolDefinition(name="pause_project", description="暂停项目推进(active→paused,PM 不再推进,仅用户明确指令暂停时用)", parameters={"project_id": "项目ID"}, category="project"), ToolDefinition(name="resume_project", description="恢复项目推进(paused→active)", parameters={"project_id": "项目ID"}, category="project"), ToolDefinition(name="backup_project", description="归档备份项目(打包工作目录+数据记录为tgz到机构工作目录_archive/)", parameters={"project_id": "项目ID"}, category="project"), ToolDefinition(name="delete_project", description="删除项目(需二次确认confirm=true,删除前自动归档备份tgz)", parameters={"project_id": "项目ID", "confirm": "二次确认,必须为true"}, category="project"), # ── 迭代 ── ToolDefinition(name="list_iterations", description="查看当前项目迭代列表", parameters={"status": "按状态筛选(可选)"}, category="iteration"), ToolDefinition(name="create_iteration", description="创建迭代(planning)", parameters={"iteration_name": "迭代名称", "iteration_type": "迭代类型(可选)", "scope": "迭代范围(可选)"}, category="iteration"), ToolDefinition(name="start_iteration", description="开始迭代(planning→in_progress)", parameters={"iteration_id": "迭代ID"}, category="iteration"), ToolDefinition(name="complete_iteration", description="完成迭代(in_progress→completed)", parameters={"iteration_id": "迭代ID"}, category="iteration"), ToolDefinition(name="cancel_iteration", description="取消迭代(planning/in_progress→cancelled)", parameters={"iteration_id": "迭代ID", "comment": "取消原因(可选)"}, category="iteration"), ToolDefinition(name="start_next_iteration", description="启动下一个迭代(若当前迭代有未完成任务需二次确认confirm=true,否则强制完成当前+作废其活跃任务+启动下一planning迭代)", parameters={"confirm": "二次确认(当前迭代有未完成任务时必须为true)"}, category="iteration"), # ── 交付件评审 ── ToolDefinition(name="submit_deliverable", description="提交交付件(pending)", parameters={"title": "交付件标题", "deliverable_type": "类型(code/doc/config等)", "content": "交付件内容", "task_id": "关联任务ID(可选)"}, category="deliverable"), ToolDefinition(name="approve_deliverable", description="评审通过交付件(pending→approved)", parameters={"deliverable_id": "交付件ID"}, category="deliverable"), ToolDefinition(name="reject_deliverable", description="评审驳回交付件(pending→rejected,附意见)", parameters={"deliverable_id": "交付件ID", "comment": "评审意见(必填)"}, category="deliverable"), # ── 测试计划 ── ToolDefinition(name="list_plans", description="查看迭代测试计划列表", parameters={"iteration_id": "迭代ID(可选)", "status": "按状态筛选(可选)"}, category="test-plan"), ToolDefinition(name="create_test_plan", description="创建测试计划(draft)", parameters={"iteration_id": "迭代ID", "plan_name": "方案名称", "plan_type": "方案类型(可选)", "scope": "测试范围(可选)"}, category="test-plan"), ToolDefinition(name="approve_plan", description="审批测试计划(draft→approved)", parameters={"plan_id": "计划ID"}, category="test-plan"), ToolDefinition(name="start_plan", description="开始执行测试计划(approved→executing)", parameters={"plan_id": "计划ID"}, category="test-plan"), ToolDefinition(name="complete_plan", description="完成测试计划(executing→completed)", parameters={"plan_id": "计划ID"}, category="test-plan"), # ── 测试用例 ── ToolDefinition(name="list_cases", description="查看测试计划用例列表", parameters={"plan_id": "计划ID", "status": "按状态筛选(可选)"}, category="test-case"), ToolDefinition(name="create_case", description="创建测试用例(pending)", parameters={"plan_id": "计划ID", "case_name": "用例名称", "case_type": "用例类型(可选)", "steps": "测试步骤(可选)", "expected_result": "预期结果(可选)"}, category="test-case"), ToolDefinition(name="pass_case", description="用例通过(pending/fail→pass)", parameters={"case_id": "用例ID", "actual_result": "实际结果(可选)"}, category="test-case"), ToolDefinition(name="fail_case", description="用例失败(pending/pass→fail,应联动建Bug)", parameters={"case_id": "用例ID", "actual_result": "实际结果(必填)"}, category="test-case"), ToolDefinition(name="skip_case", description="用例跳过(pending→skipped,注明原因)", parameters={"case_id": "用例ID", "actual_result": "跳过原因(可选)"}, category="test-case"), ToolDefinition(name="block_case", description="用例阻塞(pending→blocked,注明原因)", parameters={"case_id": "用例ID", "actual_result": "阻塞原因(可选)"}, category="test-case"), # ── Bug ── ToolDefinition(name="list_bugs", description="查看迭代Bug列表", parameters={"iteration_id": "迭代ID(可选)", "status": "按状态筛选(可选)"}, category="bug"), ToolDefinition(name="report_bug", description="上报Bug(open)", parameters={"title": "Bug标题", "description": "描述", "severity": "严重度(可选)", "priority": "优先级(可选)", "iteration_id": "迭代ID(可选)", "case_id": "关联用例ID(可选)"}, category="bug"), ToolDefinition(name="confirm_bug", description="确认Bug(open→confirmed)", parameters={"bug_id": "BugID"}, category="bug"), ToolDefinition(name="start_fix", description="开始修复Bug(confirmed→fixing)", parameters={"bug_id": "BugID", "assignee_id": "处理人(可选)"}, category="bug"), ToolDefinition(name="fix_bug", description="修复完成(fixing→fixed,附修复说明)", parameters={"bug_id": "BugID", "fix_description": "修复说明", "fix_commit": "修复Commit(可选)"}, category="bug"), ToolDefinition(name="verify_bug", description="验证修复(fixed→verified)", parameters={"bug_id": "BugID"}, category="bug"), ToolDefinition(name="close_bug", description="关闭Bug(verified→closed)", parameters={"bug_id": "BugID"}, category="bug"), ToolDefinition(name="reject_bug", description="驳回Bug(open/confirmed→rejected,附意见)", parameters={"bug_id": "BugID", "comment": "驳回意见(必填)"}, category="bug"), ToolDefinition(name="reopen_bug", description="重新打开Bug(closed/rejected→open)", parameters={"bug_id": "BugID"}, category="bug"), # ── 部署环境 ── ToolDefinition(name="list_envs", description="查看项目部署环境列表", parameters={"env_type": "环境类型(可选)", "status": "按状态筛选(可选)"}, category="deploy"), ToolDefinition(name="configure_env", description="配置部署环境(configured)", parameters={"env_type": "环境类型(test/staging/production)", "host": "SSH主机", "user": "SSH用户", "deploy_path": "部署目录", "port": "SSH端口(可选)"}, category="deploy"), ToolDefinition(name="verify_env", description="验证环境通过(configured→verified)", parameters={"env_id": "环境ID"}, category="deploy"), ] # ── prompt 片段(追加到 core 通用心智之后) ── SDL_PROMPT = """你是「开发产线」的驾驶舱 agent,负责软件项目的全流程管理(需求 → 设计 → 开发 → 部署测试环境 → 测试 → 部署生产环境)。 ## 部署环境需求(项目开始阶段必须明确) - 除功能需求外,需求阶段还要明确部署环境需求:测试环境 + 生产环境的资源规格、依赖服务、端口、环境变量、高可用/备份/域名证书/监控告警等。 - 用户未提供时,在会话中逐步追问明确(用 ask_user / list_questions),不要跳过;明确后再创建 requirement 任务。 ## 迭代粒度(重要) - 每个迭代有自己独立的初始任务(requirement 需求分析)和完整任务链(requirement → design → develop → deploy_test → test → deploy_prod)。 - 创建 requirement 任务前,必须先 list_tasks 查「当前迭代」是否已有需求分析任务;同一迭代已有(含 approved/completed)→ 更新现有需求或推进下一阶段,禁止重复创建;只有新迭代(start_next_iteration)才需要新的初始任务。 - 诊断卡点、派发任务、推进项目都以「当前迭代」为单位(diagnose_project 已按当前迭代统计),不要混着整个项目看。 ## 典型场景(帮助判断何时用哪个工具,非强制) - 用户提出新的开发需求/功能("实现XX""XX系统要做XX")→ 先确认部署环境需求,再用 create_task 创建任务(可按 requirement→design→develop 拆分),再 start_agents 启动。**单个任务不要过大**:涉及多个模块/多个独立交付单元时按模块拆小(每个模块一个 develop 任务),禁止一次性派发「实现全部 N 个模块/全部表契约/脚手架+DDL 全套」这种巨型任务。**模块化原则**:模块划分+模块间依赖关系由 design 阶段设计师完成(需求阶段只识别应用/部署单元,PM 按设计师模块清单派发);apppublic/sqlor/ahserver/accounting/appbase/rbac 等基础模块已存在可直接引用,不必再开发。 - 用户问进展/状态 → list_tasks / diagnose_project / check_progress。 - 用户问待回答问题 → list_questions / answer_question。 - 用户报告异常/故障 → 先用 diagnose_project 定位根因,再用 task_detail 查详情。 - 仓库管理 → add_repo / list_repos / clone_repo。 - 发现卡点(任务卡死、审核超时、失败、僵尸 claimed_by)时,主动定位根因并推动修复。 - 任务重复超限(retry_count 达 task_max_retry 上限)→ 系统已暂停任务链并抛 fault_report 故障;你(主 agent)收到故障后先冒泡给用户(ask_user/answer_question),用户处理完毕调 reset_task_retry 恢复该任务(重复计数清零+重新执行+恢复项目推进)。 - 发现「待回答问题」> 0 或角色提问时,加载 team-communication 技能按冒泡链处理(list_questions 列问题 → 能答则 answer_question → 不能答 escalate_question 沿冒泡路径转下一个处理方)。 - 功能/需求管理 → 用户提新需求时 propose_feature;查功能清单 list_features;需求评审/验收时 approve_feature / reject_feature / verify_feature(状态机规范见 feature 技能)。 - 项目管理 → list_projects 列项目;启动/完成/归档/重开项目用 start_project / complete_project / archive_project / reopen_project;暂停/恢复推进用 pause_project / resume_project(仅用户明确指令「暂停推进」时才 pause,默认必须推进,状态机规范见 project 技能)。 - 迭代管理 → create_iteration 建迭代、list_iterations 列迭代;开始/完成/取消迭代用 start_iteration / complete_iteration / cancel_iteration(规范见 iteration 技能)。 - 交付件评审 → submit_deliverable 提交、list_deliverables 列交付件;评审用 approve_deliverable / reject_deliverable(规范见 deliverable 技能)。 - 测试计划 → create_test_plan 建计划、list_plans 列计划;审批/执行/完成用 approve_plan / start_plan / complete_plan(规范见 test-plan 技能)。 - 测试用例 → create_case 建用例、list_cases 列用例;执行结果用 pass_case / fail_case / skip_case / block_case(失败用例应联动 report_bug,规范见 test-case 技能)。 - Bug 管理 → report_bug 上报、list_bugs 列 Bug;确认/修复/验证/关闭/驳回/重开用 confirm_bug / start_fix / fix_bug / verify_bug / close_bug / reject_bug / reopen_bug(规范见 bug 技能)。 - 部署环境 → configure_env 配环境、list_envs 列环境;验证用 verify_env(规范见 deploy-env 技能)。 - 任务流转(PM 权限操作)→ 审核通过任务 approve_task(review→approved)、标记完成 complete_task(approved/review→completed)、取消任务 cancel_task(重做/作废前必须先取消旧任务)。这三个是 PM 验收级操作,仅在用户明确要求审核/验收/完成/取消任务时使用,不主动越权推进。""" # ── 产线角色集(可插拔,替代硬编码 ROLE_SPECIFICS/ROLE_ALIASES/ROLE_CHAIN) ── SDL_ROLES = [ RoleSpec( name="agent.requirement", description="需求分析师", aliases=["requirements", "requirement_analysis"], system_prompt="""你是需求分析师。先 load_skill 加载 role 技能,按其中的职责与应遵守规范执行任务。""", next_role="agent.design", ), RoleSpec( name="agent.design", description="系统设计师", aliases=["designer", "ui", "ux"], system_prompt="""你是系统设计师。先 load_skill 加载 role 技能,按其中的职责与应遵守规范执行任务。""", next_role="agent.develop", ), RoleSpec( name="agent.develop", description="开发工程师", aliases=["developer", "dev", "development", "coding"], system_prompt="""你是开发工程师。先 load_skill 加载 role 技能,按其中的职责与应遵守规范执行任务。""", next_role="agent.deploy_test", ), RoleSpec( name="agent.deploy_test", description="测试环境部署工程师", aliases=["deploy_staging", "staging"], system_prompt="""你是测试环境部署工程师。先 load_skill 加载 role 技能,按其中的职责与应遵守规范执行任务。""", next_role="agent.test", ), RoleSpec( name="agent.test", description="测试工程师", aliases=["testing", "qa", "tester"], system_prompt="""你是测试工程师。先 load_skill 加载 role 技能,按其中的职责与应遵守规范执行任务。""", # 生产部署需人工指令:test 通过后不自动派发 deploy_prod,deploy_prod 由用户明确指令触发 next_role="", ), RoleSpec( name="agent.deploy_prod", description="生产环境部署工程师", aliases=["deploy_production", "production", "release"], system_prompt="""你是生产环境部署工程师。先 load_skill 加载 role 技能,按其中的职责与应遵守规范执行任务。""", next_role="", ), RoleSpec( name="agent.qc", description="质量控制工程师", aliases=["quality_control", "quality"], system_prompt="""你是质量控制工程师。先 load_skill 加载 role 技能,按其中的职责与应遵守规范执行任务。""", next_role="", ), ] # ── handler 独立函数(签名统一 async def handler(sor, params, ctx) -> str) ── async def _h_create_task(sor, p, ctx): pid = ctx.get("project_id", "") if not pid: return "请先切换到项目" from appPublic.uniqueID import getID title = p.get("title", "").strip() role = p.get("role", "agent.develop") desc = p.get("description", "") if not title: return "需要任务标题" # 归一化角色名:裸名(design/test/develop/requirement) → agent.{role}。 # 角色agent认领时(_claim_task→_normalize_role)用规范名匹配,裸名入库会永远匹配不上, # 任务卡 submitted 无人认领(poller 每10秒空转 dispatch)。 from .agent_loop import _normalize_role role = _normalize_role(role) tid = getID() # 迭代归属:显式指定 > 当前迭代(该 project 最新创建的迭代)。 # 「当前迭代」概念:新建迭代后当前迭代即新建的那个,任务默认归到它。 # 统一存「迭代名」:agent 可能传迭代 ID 或迭代名,都归一化为迭代名(task_tree 按名匹配)。 iteration_name = (p.get("iteration_id", "") or "").strip() if iteration_name: # 归一化:传的是迭代 ID → 反查迭代名;传的是迭代名 → 反查不到则保持原名 irecs = await sor.sqlExe( "SELECT iteration_name FROM sd_iterations WHERE id=${iid}$ AND project_id=${pid}$", {"iid": iteration_name, "pid": pid}) if irecs and getattr(irecs[0], 'iteration_name', ''): iteration_name = getattr(irecs[0], 'iteration_name', '') else: # 默认当前迭代 = status='in_progress' 的唯一迭代(不再靠 created_at DESC 推断) from .iteration_capability import get_current_iteration cur = await get_current_iteration(sor, pid) if cur: iteration_name = cur.get('iteration_name', '') or '' params = {"description": desc} if iteration_name: params["iteration_id"] = iteration_name # 迭代级查重:requirement 是迭代的初始任务,同一迭代内不应重复创建。 # 判断粒度是「迭代」而非「项目」——每个迭代有自己独立的初始任务与任务链, # 新迭代(start_next_iteration)才需要新的需求分析任务。 if role == 'agent.requirement' and iteration_name: dup_recs = await sor.sqlExe( "SELECT id, title, state FROM pipeline_tasks WHERE tenant_id=${pid}$ " "AND role='agent.requirement' AND state != 'cancelled' " "AND JSON_UNQUOTE(JSON_EXTRACT(params,'$.iteration_id'))=${nm}$ LIMIT 1", {"pid": pid, "nm": iteration_name}) if dup_recs: d = dup_recs[0] return (f"⚠️ 拒绝创建:当前迭代「{iteration_name}」已有需求分析任务「{getattr(d, 'title', '')}」" f"(状态 {getattr(d, 'state', '')})。需求分析是迭代的初始任务,同一迭代不应重复创建;" f"请 list_tasks 查看现状、更新现有需求或推进下一阶段;" f"若要新起一轮需求分析,请用 start_next_iteration 开启新迭代。") await sor.C("pipeline_tasks", { "id": tid, "tenant_id": pid, "pipeline_id": "role_task", "owner_id": ctx.get("user_id") or "user", "title": title, "state": "submitted", "role": role, "params": json.dumps(params, ensure_ascii=False), }) return f"OK: 已创建任务 {title}(角色: {role},迭代: {iteration_name or '未分配'})" async def _h_list_tasks(sor, p, ctx): pid = ctx.get("project_id", "") if not pid: return "请先切换到项目" role = p.get("role", "") state = p.get("state", "") sql = "SELECT id, title, role, state, created_at FROM pipeline_tasks WHERE tenant_id=${pid}$" params_dict = {"pid": pid} if role: sql += " AND role=${role}$" params_dict["role"] = role if state: sql += " AND state=${state}$" params_dict["state"] = state sql += " ORDER BY created_at DESC LIMIT 30" recs = await sor.sqlExe(sql, params_dict) if not recs: return "暂无任务" icons = { "submitted": "⏳", "running": "🔄", "review": "👀", "approved": "✅", "completed": "✔️", "failed": "❌", "waiting": "⏸️", "qc_review": "🔍", } lines = ["| 状态 | 任务 | 角色 | ID |", "|------|------|------|----|"] for r in recs: st = getattr(r, 'state', '?') icon = icons.get(st, "❓") title = (getattr(r, 'title', '') or '')[:60] rrole = getattr(r, 'role', '') tid = getattr(r, 'id', '') lines.append(f"| {icon} {st} | {title} | {rrole} | {tid} |") return "\n".join(lines) async def _h_task_detail(sor, p, ctx): tid = p.get("task_id", "") if not tid: return "需要任务ID" recs = await sor.sqlExe( "SELECT * FROM pipeline_tasks WHERE id=${tid}$", {"tid": tid}) if not recs and len(tid) >= 6: recs = await sor.sqlExe( "SELECT * FROM pipeline_tasks WHERE id LIKE ${prefix}$ LIMIT 1", {"prefix": tid + "%"}) if not recs: return f"任务不存在: {tid}" r = recs[0] return json.dumps({ "id": getattr(r, "id", ""), "title": getattr(r, "title", ""), "state": getattr(r, "state", ""), "role": getattr(r, "role", ""), "params": getattr(r, "params", "{}"), }, ensure_ascii=False, indent=2) async def _h_reset_task_retry(sor, p, ctx): """人工处理后恢复:任务重复计数清零 + 重新执行 + 恢复项目推进(任务链)。 任务重复超限后任务链暂停、故障抛给人工;人工处理完毕调用本工具恢复该任务并继续执行。 """ task_id = (p.get("task_id") or "").strip() if not task_id: return "需要任务ID" pid = ctx.get("project_id", "") if not pid: return "请先切换到项目" # 前缀解析(LLM 可能传截断 ID) recs = await sor.sqlExe( "SELECT id FROM pipeline_tasks WHERE id=${tid}$ AND tenant_id=${pid}$", {"tid": task_id, "pid": pid}) if not recs and len(task_id) >= 6: recs = await sor.sqlExe( "SELECT id FROM pipeline_tasks WHERE id LIKE ${prefix}$ AND tenant_id=${pid}$ LIMIT 1", {"prefix": task_id + "%", "pid": pid}) if not recs: return f"任务不存在: {task_id}" task_id = getattr(recs[0], 'id', '') or task_id from .task_capability import reset_task_retry ok, msg = await reset_task_retry(task_id, pid, who="agent.main_agent") if not ok: return f"ERROR: {msg}" # 恢复项目推进(若因故障被 pause_project 暂停) from .project_capability import resume_project rok, rmsg = await resume_project(pid, who="agent.main_agent") resume_note = ",项目已恢复推进" if rok else f"(项目恢复: {rmsg})" return f"OK: 任务重复计数已归零并重新执行{resume_note}" async def _h_approve_task(sor, p, ctx): """审核通过任务(PM 验收):review → approved + 对齐 PM 行为推进任务链。 对齐 pm_review_run 的 approved 分支:解决该任务 pending 的 review_reject 问题、 git 提交产出、查下一角色并创建下一阶段任务(含迭代边界检查)。这样会话 agent 手动 approve 不会让任务链断(下一角色任务照常自动创建)。 """ task_id = (p.get("task_id") or "").strip() if not task_id: return "需要任务ID" pid = ctx.get("project_id", "") if not pid: return "请先切换到项目" full = await _resolve_task_id(sor, task_id, pid) if not full: return f"任务不存在: {task_id}" recs = await sor.R('pipeline_tasks', {'id': full}) if not recs: return f"任务不存在: {task_id}" task = recs[0] task_role = getattr(task, 'role', '') or '' title = getattr(task, 'title', '') or '' comment = (p.get("comment") or "").strip() from .task_capability import approve_task ok, msg = await approve_task(full, pid, who="agent.pm", agent_id=ctx.get("user_id", ""), comment=comment) if not ok: return f"ERROR: {msg}" # 解决该任务 pending 的 review_reject 问题(对齐 PM,防永久堆积) await sor.sqlExe( "UPDATE pipeline_agent_questions SET status='answered', answer=${a}$, " "answer_source='role_agent', answered_by=${role}$ " "WHERE task_id=${tid}$ AND status='pending' " "AND (problem_type='review_reject' " " OR ((problem_type IS NULL OR problem_type='') AND from_role IN ('pm','cockpit')))", {"a": "角色已响应,审核通过", "role": task_role or "role_agent", "tid": full}) # git 提交产出(对齐 PM:审核通过后统一提交;workspace_dir 空或 git 失败不阻断 approve) workspace_dir = ctx.get("workspace_dir", "") or "" if workspace_dir: try: from .agent_loop import _commit_repos_after_approve await _commit_repos_after_approve(workspace_dir, title) except Exception: pass # 推进任务链(对齐 PM):查下一角色 + 迭代边界 + 创建下一阶段任务 from .agent_loop import _get_next_role, _create_next_task, _task_iteration_name next_role = await _get_next_role(task_role, pid) if next_role: task_iter = _task_iteration_name(task) if task_iter: _it = await sor.sqlExe( "SELECT status FROM sd_iterations WHERE project_id=${pid}$ AND iteration_name=${name}$", {"pid": pid, "name": task_iter}) _it_status = getattr(_it[0], 'status', '') if _it else '' if _it_status in ('completed', 'cancelled'): return f"OK: {msg}(任务归属迭代「{task_iter}」已 {_it_status},链终止)" next_tid, next_title = await _create_next_task(sor, pid, task, next_role, comment) return f"OK: {msg};已创建下一阶段任务「{next_title}」({next_role},id={next_tid})" return f"OK: {msg}(无下一角色,任务链终结)" async def _h_complete_task(sor, p, ctx): """标记任务完成(PM):approved/review → completed + 同步交付件标记 approved(对齐 PM)。 对齐 pm_review_run 的 review_complete 分支:先把交付件 review_status 从 pending 改 approved, 避免任务 state=completed 但交付件 review_status 仍 pending 的状态不一致。 """ task_id = (p.get("task_id") or "").strip() if not task_id: return "需要任务ID" pid = ctx.get("project_id", "") if not pid: return "请先切换到项目" full = await _resolve_task_id(sor, task_id, pid) if not full: return f"任务不存在: {task_id}" await sor.sqlExe( "UPDATE pipeline_deliverables SET review_status='approved', review_comment=${cm}$ " "WHERE task_id=${tid}$ AND review_status='pending'", {"cm": (p.get("comment") or "").strip(), "tid": full}) from .task_capability import complete_task ok, msg = await complete_task(full, pid, who="agent.pm", agent_id=ctx.get("user_id", "")) return f"OK: {msg}" if ok else f"ERROR: {msg}" async def _h_cancel_task(sor, p, ctx): """取消任务(PM):任意非终态 → cancelled,清 claimed_by。""" task_id = (p.get("task_id") or "").strip() if not task_id: return "需要任务ID" pid = ctx.get("project_id", "") if not pid: return "请先切换到项目" full = await _resolve_task_id(sor, task_id, pid) if not full: return f"任务不存在: {task_id}" from .task_capability import cancel_task ok, msg = await cancel_task(full, pid, who="agent.pm", agent_id=ctx.get("user_id", ""), comment=(p.get("comment") or "").strip()) return f"OK: {msg}" if ok else f"ERROR: {msg}" async def _h_start_agents(sor, p, ctx): pid = ctx.get("project_id", "") if not pid: return "请先切换到项目" recs = await sor.sqlExe( "SELECT id, title, role FROM pipeline_tasks " "WHERE tenant_id=${pid}$ AND state='submitted' ORDER BY created_at ASC LIMIT 10", {"pid": pid}) if not recs: return "没有待执行的任务" results = [] for r in recs: tid = getattr(r, "id", "") role = getattr(r, "role", "agent.develop") try: from pipeline_service.agent_loop import role_agent_run result = await role_agent_run(pid, role) results.append(f"- {getattr(r, 'title', '')}: {result.get('status', '?')}") except Exception as e: results.append(f"- {getattr(r, 'title', '')}: error={str(e)[:100]}") return "Agent 执行结果:\n" + "\n".join(results) async def _h_diagnose_project(sor, p, ctx): pid = ctx.get("project_id", "") if not pid: return "请先切换到项目" # 迭代级诊断:诊断单位是「当前迭代」(in_progress),而非整个项目。 # 项目只是迭代的容器;PM 诊断卡点、派发任务、推进都以当前迭代为单位。 from .iteration_capability import get_current_iteration cur_iter = await get_current_iteration(sor, pid) iter_name = cur_iter.get('iteration_name', '') if cur_iter else '' iter_cond = " AND JSON_UNQUOTE(JSON_EXTRACT(params,'$.iteration_id'))=${nm}$ " if iter_name else "" iter_bind = {"nm": iter_name} if iter_name else {} async def _cnt(state): recs = await sor.sqlExe( "SELECT COUNT(*) as c FROM pipeline_tasks WHERE tenant_id=${pid}$ AND state=${st}$" + iter_cond, {"pid": pid, "st": state, **iter_bind}) return getattr(recs[0], "c", 0) if recs else 0 sc = await _cnt("submitted") rc = await _cnt("running") fc = await _cnt("failed") vc = await _cnt("review") qc = 0 try: from .communication import list_problems_for # 待回答问题 = 当前该「主 agent」处理的问题(不混入 PM 退回给角色 agent 的) mine = await list_problems_for("agent.main_agent", tenant_id=pid, limit=200) qc = len(mine) except Exception: pass lines = [ f"项目诊断(当前迭代:{iter_name or '无 in_progress 迭代(回退项目级)'}):", f"- 待执行: {sc}", f"- 运行中: {rc}", f"- 待审核: {vc}", f"- 失败: {fc}", f"- 待回答问题: {qc}", ] if fc: failed_rows = await sor.sqlExe( "SELECT id, title, role FROM pipeline_tasks WHERE tenant_id=${pid}$ AND state='failed'" + iter_cond + "ORDER BY created_at DESC LIMIT 10", {"pid": pid, **iter_bind}) lines.append("失败任务:") for fr in (failed_rows or []): lines.append(f" - [{getattr(fr, 'role', '?')}] {getattr(fr, 'title', '')} (id={getattr(fr, 'id', '')})") if sc: submitted_rows = await sor.sqlExe( "SELECT id, title, role FROM pipeline_tasks WHERE tenant_id=${pid}$ AND state='submitted'" + iter_cond + "ORDER BY created_at ASC LIMIT 10", {"pid": pid, **iter_bind}) lines.append("待执行任务:") for sr in (submitted_rows or []): lines.append(f" - [{getattr(sr, 'role', '?')}] {getattr(sr, 'title', '')} (id={getattr(sr, 'id', '')})") if rc: running_rows = await sor.sqlExe( "SELECT id, title, role, claimed_by, " "TIMESTAMPDIFF(MINUTE, updated_at, NOW()) AS mins " "FROM pipeline_tasks WHERE tenant_id=${pid}$ AND state='running'" + iter_cond + "ORDER BY updated_at ASC LIMIT 10", {"pid": pid, **iter_bind}) lines.append("运行中任务:") for rr in (running_rows or []): rrole = getattr(rr, 'role', '?') rtitle = getattr(rr, 'title', '') rid = getattr(rr, 'id', '') rcb = getattr(rr, 'claimed_by', '') or '' try: mins = int(getattr(rr, 'mins', 0) or 0) except (TypeError, ValueError): mins = 0 flag = f"⚠️心跳超时{mins}分钟(疑似僵尸,将被回收重跑)" if mins >= 20 else f"已运行{mins}分钟" lines.append(f" - [{rrole}] {rtitle} (id={rid}) | {flag} | claimed_by={rcb[:8]}") return "\n".join(lines) async def _h_list_deliverables(sor, p, ctx): pid = ctx.get("project_id", "") if not pid: return "请先切换到项目" sql = "SELECT id, title, deliverable_type, review_status FROM pipeline_deliverables WHERE project_id=${pid}$ ORDER BY created_at DESC LIMIT 20" recs = await sor.sqlExe(sql, {"pid": pid}) if not recs: return "暂无交付件" lines = [] for r in recs: lines.append( f"- [{getattr(r, 'review_status', '?')}] {getattr(r, 'title', '')} " f"({getattr(r, 'deliverable_type', '')}) id={getattr(r, 'id', '')}") return "\n".join(lines) async def _h_view_deliverable(sor, p, ctx): did = p.get("deliverable_id", "") if not did: return "需要交付件ID" recs = await sor.sqlExe( "SELECT * FROM pipeline_deliverables WHERE id=${did}$", {"did": did}) if not recs and len(did) >= 6: recs = await sor.sqlExe( "SELECT * FROM pipeline_deliverables WHERE id LIKE ${prefix}$ LIMIT 1", {"prefix": did + "%"}) if not recs: return f"交付件不存在: {did}" return getattr(recs[0], "content", "")[:4000] or "(空)" async def _h_list_questions(sor, p, ctx): pid = ctx.get("project_id", "") if not pid: return "请先切换到项目" from .communication import list_problems_for # 只列「当前该主 agent 处理」的问题(不混入 PM 退回给角色 agent 的退回意见) recs = await list_problems_for("agent.main_agent", tenant_id=pid, limit=10) if not recs: return "没有待回答问题" lines = [] for r in recs: qid = r.get('id', '') or '' q = (r.get('question', '') or '')[:200] ptype = r.get('problem_type', '') or '' tag = f"[{ptype}] " if ptype else "" lines.append(f"- [{qid}] {tag}{q}") return "\n".join(lines) async def _h_escalate_question(sor, p, ctx): qid = p.get("question_id", "") if not qid: return "需要问题ID" from .communication import escalate_problem # 模糊匹配短 id full_qid = qid if len(qid) < 32: recs = await sor.sqlExe( "SELECT id FROM pipeline_agent_questions WHERE id LIKE ${prefix}$ LIMIT 1", {"prefix": qid + "%"}) if recs: full_qid = getattr(recs[0], "id", qid) # 主 agent 答不了 → 沿冒泡路径转给客户(下一个处理方由 team-communication skill 决定) r = await escalate_problem(full_qid, "owner.superuser", by_role="agent.main_agent") if r is None: return f"问题不存在: {qid}" return f"OK: 已沿冒泡路径转给 {r.get('current_handler_role', 'owner.superuser')}" async def _h_answer_question(sor, p, ctx): qid = p.get("question_id", "") answer = p.get("answer", "") if not qid or not answer: return "需要问题ID和回答内容" recs = await sor.sqlExe( "SELECT id, task_id FROM pipeline_agent_questions WHERE id=${qid}$", {"qid": qid}) if not recs and len(qid) >= 6: recs = await sor.sqlExe( "SELECT id, task_id FROM pipeline_agent_questions WHERE id LIKE ${prefix}$ LIMIT 1", {"prefix": qid + "%"}) if not recs: return f"问题不存在: {qid}" full_qid = getattr(recs[0], "id", qid) task_id = getattr(recs[0], "task_id", "") or "" await sor.sqlExe( "UPDATE pipeline_agent_questions SET answer=${a}$, answer_source='agent.main_agent', " "answered_by='agent.main_agent', status='answered' WHERE id=${qid}$", {"a": answer, "qid": full_qid}) resumed = False if task_id: await sor.sqlExe( "UPDATE pipeline_tasks SET state='submitted', claimed_by=NULL " "WHERE id=${tid}$ AND state='waiting'", {"tid": task_id}) resumed = True suffix = ",任务已恢复执行" if resumed else "" return "OK: 已回答" + suffix async def _h_add_repo(sor, p, ctx): pid = ctx.get("project_id", "") if not pid: return "请先切换到项目" from appPublic.uniqueID import getID url = p.get("repo_url", "").strip() name = p.get("repo_name", "").strip() branch = p.get("default_branch", "main") if not url: return "需要仓库URL" await sor.C("sd_project_repos", { "id": getID(), "project_id": pid, "repo_url": url, "repo_name": name or url.split("/")[-1].replace(".git", ""), "default_branch": branch, }) return f"OK: 已添加仓库 {name or url}" async def _h_list_repos(sor, p, ctx): pid = ctx.get("project_id", "") if not pid: return "请先切换到项目" recs = await sor.sqlExe( "SELECT repo_name, repo_url, default_branch FROM sd_project_repos WHERE project_id=${pid}$", {"pid": pid}) if not recs: return "暂无关联仓库" lines = [] for r in recs: lines.append(f"- {getattr(r, 'repo_name', '')}: {getattr(r, 'repo_url', '')}") return "\n".join(lines) async def _h_clone_repo(sor, p, ctx): pid = ctx.get("project_id", "") if not pid: return "请先切换到项目" from pipeline_service.agent_loop import ( _setup_repos, _get_workspace_dir, _git_clone) workspace_dir = await _get_workspace_dir(sor, pid) url = (p.get("repo_url", "") or "").strip() if url: name = url.rstrip("/").split("/")[-1].replace(".git", "") target = os.path.join(workspace_dir, "repos", name) r = await _git_clone(url, target, p.get("branch", "main")) return f"rc={r['rc']} {r['message']}" results = await _setup_repos(sor, workspace_dir, pid) if not results: return "无关联仓库可克隆" return "\n".join( f"- {x['repo']}: rc={x.get('rc', '?')} {x.get('message', '')}" for x in results) async def _h_check_progress(sor, p, ctx): pid = ctx.get("project_id", "") if not pid: return "请先切换到项目" recs = await sor.sqlExe( "SELECT role, state, COUNT(*) as c FROM pipeline_tasks " "WHERE tenant_id=${pid}$ GROUP BY role, state ORDER BY role, state", {"pid": pid}) if not recs: return "暂无进度数据" lines = [] for r in recs: lines.append( f"- {getattr(r, 'role', '?')}: {getattr(r, 'state', '?')} x{getattr(r, 'c', 0)}") return "\n".join(lines) async def _h_add_bug(sor, p, ctx): pid = ctx.get("project_id", "") if not pid: return "请先切换到项目" from appPublic.uniqueID import getID title = p.get("title", "").strip() desc = p.get("description", "") severity = p.get("severity", "medium") if not title: return "需要Bug标题" iteration_id = p.get("iteration_id", "") if not iteration_id: # 默认当前迭代(status='in_progress'),不再用 created_at ASC(最旧)——Bug 与任务归同一迭代 from .iteration_capability import get_current_iteration cur = await get_current_iteration(sor, pid) if cur: iteration_id = cur.get('id', '') if not iteration_id: return "ERROR: 项目无当前迭代,无法提交Bug" await sor.C("sd_bugs", { "id": getID(), "title": title, "description": desc, "severity": severity, "status": "open", "iteration_id": iteration_id, }) return f"OK: 已提交Bug: {title}" async def _resolve_feature_id(sor, fid): """功能 ID 前缀兜底:精确匹配失败且入参≥6位时,用 LIKE 前缀解析回完整 ID。""" if not fid: return "" recs = await sor.sqlExe( "SELECT id FROM sd_features WHERE id=${fid}$", {"fid": fid}) if recs: return getattr(recs[0], "id", "") if len(fid) >= 6: recs = await sor.sqlExe( "SELECT id FROM sd_features WHERE id LIKE ${prefix}$ LIMIT 1", {"prefix": fid + "%"}) if recs: return getattr(recs[0], "id", "") return "" async def _h_propose_feature(sor, p, ctx): pid = ctx.get("project_id", "") if not pid: return "请先切换到项目" name = (p.get("feature_name", "") or "").strip() if not name: return "需要功能名称" from .feature_capability import propose_feature ok, fid = await propose_feature( project_id=pid, feature_name=name, description=p.get("description", "") or "", iteration_id=p.get("iteration_id", "") or "", priority=p.get("priority", "P2") or "P2", created_by=ctx.get("user_id", "") or "", who="agent.main_agent", ) if not ok: return f"ERROR: {fid}" return f"OK: 已提出功能「{name}」(id={fid},状态 proposed)" async def _h_list_features(sor, p, ctx): pid = ctx.get("project_id", "") if not pid: return "请先切换到项目" status = (p.get("status", "") or "").strip() sql = "SELECT id, feature_name, status, priority, iteration_id FROM sd_features WHERE project_id=${pid}$" params = {"pid": pid} if status: sql += " AND status=${status}$" params["status"] = status sql += " ORDER BY created_at DESC LIMIT 30" recs = await sor.sqlExe(sql, params) if not recs: return "暂无功能" lines = [] for r in recs: lines.append( f"- [{getattr(r, 'status', '?')}][{getattr(r, 'priority', '') or ''}] " f"{getattr(r, 'feature_name', '')} (id={getattr(r, 'id', '')})") return "\n".join(lines) async def _h_approve_feature(sor, p, ctx): fid = (p.get("feature_id", "") or "").strip() if not fid: return "需要功能ID" pid = ctx.get("project_id", "") fid = await _resolve_feature_id(sor, fid) or fid from .feature_capability import approve_feature ok, msg = await approve_feature(fid, pid, who="agent.main_agent") return f"OK: {msg}" if ok else f"ERROR: {msg}" async def _h_reject_feature(sor, p, ctx): fid = (p.get("feature_id", "") or "").strip() if not fid: return "需要功能ID" pid = ctx.get("project_id", "") fid = await _resolve_feature_id(sor, fid) or fid from .feature_capability import reject_feature ok, msg = await reject_feature(fid, pid, who="agent.main_agent", comment=p.get("comment", "") or "") return f"OK: {msg}" if ok else f"ERROR: {msg}" async def _h_verify_feature(sor, p, ctx): fid = (p.get("feature_id", "") or "").strip() if not fid: return "需要功能ID" pid = ctx.get("project_id", "") fid = await _resolve_feature_id(sor, fid) or fid from .feature_capability import verify_feature ok, msg = await verify_feature(fid, pid, who="agent.main_agent") return f"OK: {msg}" if ok else f"ERROR: {msg}" # ── 通用 ID 前缀兜底 + scope 反查 ── async def _resolve_id(sor, table, fid): """通用 ID 前缀兜底:精确匹配失败且入参≥6位时,用 LIKE 前缀解析回完整 ID。""" if not fid: return "" recs = await sor.sqlExe( f"SELECT id FROM {table} WHERE id=${{fid}}$", {"fid": fid}) if recs: return getattr(recs[0], "id", "") if len(fid) >= 6: recs = await sor.sqlExe( f"SELECT id FROM {table} WHERE id LIKE ${{prefix}}$ LIMIT 1", {"prefix": fid + "%"}) if recs: return getattr(recs[0], "id", "") return "" async def _resolve_task_id(sor, task_id, pid): """任务 ID 前缀兜底(租户隔离):精确匹配失败且入参≥6位时 LIKE 前缀解析完整 ID。""" if not task_id: return "" recs = await sor.sqlExe( "SELECT id FROM pipeline_tasks WHERE id=${tid}$ AND tenant_id=${pid}$", {"tid": task_id, "pid": pid}) if recs: return getattr(recs[0], "id", "") if len(task_id) >= 6: recs = await sor.sqlExe( "SELECT id FROM pipeline_tasks WHERE id LIKE ${prefix}$ AND tenant_id=${pid}$ LIMIT 1", {"prefix": task_id + "%", "pid": pid}) if recs: return getattr(recs[0], "id", "") return "" async def _get_scope(sor, table, id_field, id_val, scope_field): """反查实体的 scope 列值(如 iteration_id/plan_id)。返回 scope_val 或空。""" recs = await sor.sqlExe( f"SELECT {scope_field} FROM {table} WHERE {id_field}=${{id}}$", {"id": id_val}) return getattr(recs[0], scope_field, "") if recs else "" async def _resolve_iteration_id(sor, pid, iteration_id): """解析迭代 ID:显式传入则用,否则取当前迭代(status='in_progress')。返回 (iteration_id, error)。""" if iteration_id: full = await _resolve_id(sor, "sd_iterations", iteration_id) if full: return full, "" return "", f"迭代不存在: {iteration_id}" from .iteration_capability import get_current_iteration cur = await get_current_iteration(sor, pid) if cur: return cur.get('id', ''), "" return "", "项目无当前迭代,请先 create_iteration 并 start" async def _resolve_plan_id(sor, pid, plan_id): """解析计划 ID:显式传入则用,否则取项目第一个计划(经迭代)。返回 (plan_id, error)。""" if plan_id: full = await _resolve_id(sor, "sd_test_plans", plan_id) if full: return full, "" return "", f"计划不存在: {plan_id}" iid, err = await _resolve_iteration_id(sor, pid, "") if err: return "", err plans = await sor.sqlExe( "SELECT id FROM sd_test_plans WHERE iteration_id=${iid}$ ORDER BY created_at ASC LIMIT 1", {"iid": iid}) if not plans: return "", "项目无测试计划,请先 create_test_plan" return getattr(plans[0], "id", ""), "" # ── 项目生命周期 handler ── async def _h_list_projects(sor, p, ctx): from .project_capability import list_projects # 按当前用户 org 过滤(租户隔离),org_id 空则列全部 org_id = "" uid = ctx.get("user_id", "") if uid: _u = await sor.sqlExe("SELECT orgid FROM users WHERE id=${u}$ LIMIT 1", {"u": uid}) if _u: org_id = getattr(_u[0], "orgid", "") or "" status = (p.get("status", "") or "").strip() lst = await list_projects(org_id=org_id or None, status=status or None) if not lst: return "暂无项目" lines = [] for r in lst: lines.append(f"- [{r.get('status', '?')}] {r.get('name', '')} (id={r.get('id', '')})") return "\n".join(lines) async def _h_start_project(sor, p, ctx): pid = (p.get("project_id", "") or "").strip() if not pid: return "需要项目ID" pid = await _resolve_id(sor, "sd_projects", pid) or pid from .project_capability import start_project ok, msg = await start_project(pid, who="agent.main_agent") return f"OK: {msg}" if ok else f"ERROR: {msg}" async def _h_list_role_models(sor, p, ctx): """列出项目各角色的模型配置(sd_project_role_models)。""" pid = (p.get("project_id", "") or "").strip() or (ctx.get("project_id", "") or "") if not pid: return "需要 project_id" pid = await _resolve_id(sor, "sd_projects", pid) or pid recs = await sor.sqlExe( "SELECT role, model_name FROM sd_project_role_models WHERE project_id=${pid}$ ORDER BY role", {"pid": pid}) await sor.sqlExe("COMMIT", {}) if not recs: return "(无角色模型配置,所有角色用项目缺省模型)" lines = [] for r in recs: lines.append(f"- {r.role} -> {r.model_name or '(空=缺省)'}") return "\n".join(lines) async def _h_set_role_model(sor, p, ctx): """设置某角色的模型(model_name 空 = 清除,回退项目缺省模型)。""" pid = (p.get("project_id", "") or "").strip() or (ctx.get("project_id", "") or "") role = (p.get("role", "") or "").strip() model_name = (p.get("model_name", "") or "").strip() if not pid: return "需要 project_id" if not role: return "需要 role" pid = await _resolve_id(sor, "sd_projects", pid) or pid from appPublic.uniqueID import getID recs = await sor.sqlExe( "SELECT id FROM sd_project_role_models WHERE project_id=${pid}$ AND role=${r}$", {"pid": pid, "r": role}) await sor.sqlExe("COMMIT", {}) if recs: await sor.sqlExe( "UPDATE sd_project_role_models SET model_name=${m}$, updated_at=NOW() WHERE id=${id}$", {"m": model_name or None, "id": getattr(recs[0], "id", "")}) else: await sor.C("sd_project_role_models", { "id": getID(), "project_id": pid, "role": role, "model_name": model_name or None, }) await sor.sqlExe("COMMIT", {}) return f"OK: {role} 模型 -> {model_name or '(空=用项目缺省模型)'}" async def _h_complete_project(sor, p, ctx): pid = (p.get("project_id", "") or "").strip() if not pid: return "需要项目ID" pid = await _resolve_id(sor, "sd_projects", pid) or pid from .project_capability import complete_project ok, msg = await complete_project(pid, who="agent.main_agent") return f"OK: {msg}" if ok else f"ERROR: {msg}" async def _h_archive_project(sor, p, ctx): pid = (p.get("project_id", "") or "").strip() if not pid: return "需要项目ID" pid = await _resolve_id(sor, "sd_projects", pid) or pid from .project_capability import archive_project ok, msg = await archive_project(pid, who="agent.main_agent") return f"OK: {msg}" if ok else f"ERROR: {msg}" async def _h_reopen_project(sor, p, ctx): pid = (p.get("project_id", "") or "").strip() if not pid: return "需要项目ID" pid = await _resolve_id(sor, "sd_projects", pid) or pid from .project_capability import reopen_project ok, msg = await reopen_project(pid, who="agent.main_agent") return f"OK: {msg}" if ok else f"ERROR: {msg}" async def _h_pause_project(sor, p, ctx): pid = (p.get("project_id", "") or "").strip() if not pid: return "需要项目ID" pid = await _resolve_id(sor, "sd_projects", pid) or pid from .project_capability import pause_project ok, msg = await pause_project(pid, who="agent.main_agent") return f"OK: {msg}" if ok else f"ERROR: {msg}" async def _h_resume_project(sor, p, ctx): pid = (p.get("project_id", "") or "").strip() if not pid: return "需要项目ID" pid = await _resolve_id(sor, "sd_projects", pid) or pid from .project_capability import resume_project ok, msg = await resume_project(pid, who="agent.main_agent") return f"OK: {msg}" if ok else f"ERROR: {msg}" async def _h_backup_project(sor, p, ctx): pid = (p.get("project_id", "") or "").strip() if not pid: return "需要项目ID" pid = await _resolve_id(sor, "sd_projects", pid) or pid from .project_capability import backup_project ok, msg = await backup_project(pid, who="agent.main_agent") return f"OK: {msg}" if ok else f"ERROR: {msg}" async def _h_delete_project(sor, p, ctx): pid = (p.get("project_id", "") or "").strip() if not pid: return "需要项目ID" confirm = (p.get("confirm", "") or "").strip().lower() if confirm != "true": return "ERROR: 删除项目需二次确认,请传 confirm=true 后再执行(删除前会自动归档备份到机构工作目录 _archive/)" pid = await _resolve_id(sor, "sd_projects", pid) or pid from .project_capability import delete_project ok, msg = await delete_project(pid, who="agent.main_agent", confirm=True) return f"OK: {msg}" if ok else f"ERROR: {msg}" # ── 迭代 handler ── async def _h_list_iterations(sor, p, ctx): pid = ctx.get("project_id", "") if not pid: return "请先切换到项目" from .iteration_capability import list_iterations status = (p.get("status", "") or "").strip() lst = await list_iterations(pid, status=status or None) if not lst: return "暂无迭代" lines = [] for r in lst: mark = "▶" if r.get('status', '') == 'in_progress' else " " lines.append(f"- {mark}[{r.get('status', '?')}] #{r.get('seq_no', '?')} {r.get('iteration_name', '')} (id={r.get('id', '')})") return "\n".join(lines) async def _h_start_next_iteration(sor, p, ctx): pid = ctx.get("project_id", "") if not pid: return "请先切换到项目" confirm = (str(p.get("confirm", "") or "")).strip().lower() == "true" from .iteration_capability import start_next_iteration ok, msg = await start_next_iteration(pid, who="agent.main_agent", agent_id=ctx.get("user_id", "") or "", confirm=confirm) if ok: return f"OK: {msg}" if msg.startswith("CONFIRM:"): return msg # 确认提示原样返回,让 agent 引导用户确认 return f"ERROR: {msg}" async def _h_create_iteration(sor, p, ctx): pid = ctx.get("project_id", "") if not pid: return "请先切换到项目" name = (p.get("iteration_name", "") or "").strip() if not name: return "需要迭代名称" from .iteration_capability import create_iteration ok, iid = await create_iteration( project_id=pid, iteration_name=name, iteration_type=p.get("iteration_type", "new_feature") or "new_feature", scope=p.get("scope", "") or "", created_by=ctx.get("user_id", "") or "", who="agent.main_agent", ) if not ok: return f"ERROR: {iid}" return f"OK: 已创建迭代「{name}」(id={iid},状态 planning)" async def _h_start_iteration(sor, p, ctx): iid = (p.get("iteration_id", "") or "").strip() if not iid: return "需要迭代ID" pid = ctx.get("project_id", "") iid = await _resolve_id(sor, "sd_iterations", iid) or iid from .iteration_capability import start_iteration ok, msg = await start_iteration(iid, pid, who="agent.main_agent") return f"OK: {msg}" if ok else f"ERROR: {msg}" async def _h_complete_iteration(sor, p, ctx): iid = (p.get("iteration_id", "") or "").strip() if not iid: return "需要迭代ID" pid = ctx.get("project_id", "") iid = await _resolve_id(sor, "sd_iterations", iid) or iid from .iteration_capability import complete_iteration ok, msg = await complete_iteration(iid, pid, who="agent.main_agent") return f"OK: {msg}" if ok else f"ERROR: {msg}" async def _h_cancel_iteration(sor, p, ctx): iid = (p.get("iteration_id", "") or "").strip() if not iid: return "需要迭代ID" pid = ctx.get("project_id", "") iid = await _resolve_id(sor, "sd_iterations", iid) or iid from .iteration_capability import cancel_iteration ok, msg = await cancel_iteration(iid, pid, who="agent.main_agent", comment=p.get("comment", "") or "") return f"OK: {msg}" if ok else f"ERROR: {msg}" # ── 交付件评审 handler ── async def _h_submit_deliverable(sor, p, ctx): pid = ctx.get("project_id", "") if not pid: return "请先切换到项目" title = (p.get("title", "") or "").strip() if not title: return "需要交付件标题" from .deliverable_capability import submit_deliverable ok, did = await submit_deliverable( project_id=pid, title=title, deliverable_type=p.get("deliverable_type", "code") or "code", task_id=p.get("task_id", "") or "", content=p.get("content", "") or "", created_by=ctx.get("user_id", "") or "", who="agent.main_agent", ) if not ok: return f"ERROR: {did}" return f"OK: 已提交交付件「{title}」(id={did},状态 pending)" async def _h_approve_deliverable(sor, p, ctx): did = (p.get("deliverable_id", "") or "").strip() if not did: return "需要交付件ID" pid = ctx.get("project_id", "") did = await _resolve_id(sor, "pipeline_deliverables", did) or did from .deliverable_capability import approve_deliverable ok, msg = await approve_deliverable(did, pid, who="agent.main_agent") return f"OK: {msg}" if ok else f"ERROR: {msg}" async def _h_reject_deliverable(sor, p, ctx): did = (p.get("deliverable_id", "") or "").strip() if not did: return "需要交付件ID" pid = ctx.get("project_id", "") did = await _resolve_id(sor, "pipeline_deliverables", did) or did from .deliverable_capability import reject_deliverable ok, msg = await reject_deliverable(did, pid, who="agent.main_agent", comment=p.get("comment", "") or "") return f"OK: {msg}" if ok else f"ERROR: {msg}" # ── 测试计划 handler ── async def _h_list_plans(sor, p, ctx): pid = ctx.get("project_id", "") if not pid: return "请先切换到项目" from .test_plan_capability import list_plans iid, err = await _resolve_iteration_id(sor, pid, (p.get("iteration_id", "") or "").strip()) if err: return err status = (p.get("status", "") or "").strip() lst = await list_plans(iid, status=status or None) if not lst: return "暂无测试计划" lines = [] for r in lst: lines.append(f"- [{r.get('status', '?')}] {r.get('plan_name', '')} (id={r.get('id', '')})") return "\n".join(lines) async def _h_create_test_plan(sor, p, ctx): pid = ctx.get("project_id", "") if not pid: return "请先切换到项目" name = (p.get("plan_name", "") or "").strip() if not name: return "需要方案名称" iid, err = await _resolve_iteration_id(sor, pid, (p.get("iteration_id", "") or "").strip()) if err: return err from .test_plan_capability import create_test_plan ok, plan_id = await create_test_plan( iteration_id=iid, plan_name=name, plan_type=p.get("plan_type", "functional") or "functional", scope=p.get("scope", "") or "", created_by=ctx.get("user_id", "") or "", who="agent.main_agent", ) if not ok: return f"ERROR: {plan_id}" return f"OK: 已创建测试计划「{name}」(id={plan_id},状态 draft)" async def _h_approve_plan(sor, p, ctx): plan_id = (p.get("plan_id", "") or "").strip() if not plan_id: return "需要计划ID" plan_id = await _resolve_id(sor, "sd_test_plans", plan_id) or plan_id iid = await _get_scope(sor, "sd_test_plans", "id", plan_id, "iteration_id") from .test_plan_capability import approve_plan ok, msg = await approve_plan(plan_id, iid, who="agent.main_agent") return f"OK: {msg}" if ok else f"ERROR: {msg}" async def _h_start_plan(sor, p, ctx): plan_id = (p.get("plan_id", "") or "").strip() if not plan_id: return "需要计划ID" plan_id = await _resolve_id(sor, "sd_test_plans", plan_id) or plan_id iid = await _get_scope(sor, "sd_test_plans", "id", plan_id, "iteration_id") from .test_plan_capability import start_plan ok, msg = await start_plan(plan_id, iid, who="agent.main_agent") return f"OK: {msg}" if ok else f"ERROR: {msg}" async def _h_complete_plan(sor, p, ctx): plan_id = (p.get("plan_id", "") or "").strip() if not plan_id: return "需要计划ID" plan_id = await _resolve_id(sor, "sd_test_plans", plan_id) or plan_id iid = await _get_scope(sor, "sd_test_plans", "id", plan_id, "iteration_id") from .test_plan_capability import complete_plan ok, msg = await complete_plan(plan_id, iid, who="agent.main_agent") return f"OK: {msg}" if ok else f"ERROR: {msg}" # ── 测试用例 handler ── async def _h_list_cases(sor, p, ctx): pid = ctx.get("project_id", "") if not pid: return "请先切换到项目" from .test_case_capability import list_cases plan_id, err = await _resolve_plan_id(sor, pid, (p.get("plan_id", "") or "").strip()) if err: return err status = (p.get("status", "") or "").strip() lst = await list_cases(plan_id, status=status or None) if not lst: return "暂无测试用例" lines = [] for r in lst: lines.append(f"- [{r.get('status', '?')}] {r.get('case_name', '')} (id={r.get('id', '')})") return "\n".join(lines) async def _h_create_case(sor, p, ctx): pid = ctx.get("project_id", "") if not pid: return "请先切换到项目" name = (p.get("case_name", "") or "").strip() if not name: return "需要用例名称" plan_id, err = await _resolve_plan_id(sor, pid, (p.get("plan_id", "") or "").strip()) if err: return err from .test_case_capability import create_case ok, cid = await create_case( plan_id=plan_id, case_name=name, case_type=p.get("case_type", "functional") or "functional", steps=p.get("steps", "") or "", expected_result=p.get("expected_result", "") or "", who="agent.main_agent", ) if not ok: return f"ERROR: {cid}" return f"OK: 已创建测试用例「{name}」(id={cid},状态 pending)" async def _h_pass_case(sor, p, ctx): cid = (p.get("case_id", "") or "").strip() if not cid: return "需要用例ID" cid = await _resolve_id(sor, "sd_test_cases", cid) or cid plan_id = await _get_scope(sor, "sd_test_cases", "id", cid, "plan_id") from .test_case_capability import pass_case ok, msg = await pass_case(cid, plan_id, actual_result=p.get("actual_result", "") or "", who="agent.main_agent") return f"OK: {msg}" if ok else f"ERROR: {msg}" async def _h_fail_case(sor, p, ctx): cid = (p.get("case_id", "") or "").strip() if not cid: return "需要用例ID" cid = await _resolve_id(sor, "sd_test_cases", cid) or cid plan_id = await _get_scope(sor, "sd_test_cases", "id", cid, "plan_id") from .test_case_capability import fail_case ok, msg = await fail_case(cid, plan_id, actual_result=p.get("actual_result", "") or "", who="agent.main_agent") return f"OK: {msg}" if ok else f"ERROR: {msg}" async def _h_skip_case(sor, p, ctx): cid = (p.get("case_id", "") or "").strip() if not cid: return "需要用例ID" cid = await _resolve_id(sor, "sd_test_cases", cid) or cid plan_id = await _get_scope(sor, "sd_test_cases", "id", cid, "plan_id") from .test_case_capability import skip_case ok, msg = await skip_case(cid, plan_id, actual_result=p.get("actual_result", "") or "", who="agent.main_agent") return f"OK: {msg}" if ok else f"ERROR: {msg}" async def _h_block_case(sor, p, ctx): cid = (p.get("case_id", "") or "").strip() if not cid: return "需要用例ID" cid = await _resolve_id(sor, "sd_test_cases", cid) or cid plan_id = await _get_scope(sor, "sd_test_cases", "id", cid, "plan_id") from .test_case_capability import block_case ok, msg = await block_case(cid, plan_id, actual_result=p.get("actual_result", "") or "", who="agent.main_agent") return f"OK: {msg}" if ok else f"ERROR: {msg}" # ── Bug handler ── async def _h_list_bugs(sor, p, ctx): pid = ctx.get("project_id", "") if not pid: return "请先切换到项目" from .bug_capability import list_bugs iid, err = await _resolve_iteration_id(sor, pid, (p.get("iteration_id", "") or "").strip()) if err: return err status = (p.get("status", "") or "").strip() lst = await list_bugs(iid, status=status or None) if not lst: return "暂无Bug" lines = [] for r in lst: lines.append(f"- [{r.get('status', '?')}][{r.get('severity', '') or ''}] " f"{r.get('title', '')} (id={r.get('id', '')})") return "\n".join(lines) async def _h_report_bug(sor, p, ctx): pid = ctx.get("project_id", "") if not pid: return "请先切换到项目" title = (p.get("title", "") or "").strip() if not title: return "需要Bug标题" iid, err = await _resolve_iteration_id(sor, pid, (p.get("iteration_id", "") or "").strip()) if err: return err from .bug_capability import report_bug ok, bid = await report_bug( iteration_id=iid, title=title, description=p.get("description", "") or "", severity=p.get("severity", "major") or "major", priority=p.get("priority", "P1") or "P1", case_id=p.get("case_id", "") or "", reporter_type="agent", reporter_id=ctx.get("user_id", "") or "", who="agent.main_agent", ) if not ok: return f"ERROR: {bid}" return f"OK: 已上报Bug「{title}」(id={bid},状态 open)" async def _h_confirm_bug(sor, p, ctx): bid = (p.get("bug_id", "") or "").strip() if not bid: return "需要BugID" bid = await _resolve_id(sor, "sd_bugs", bid) or bid iid = await _get_scope(sor, "sd_bugs", "id", bid, "iteration_id") from .bug_capability import confirm_bug ok, msg = await confirm_bug(bid, iid, who="agent.main_agent") return f"OK: {msg}" if ok else f"ERROR: {msg}" async def _h_start_fix(sor, p, ctx): bid = (p.get("bug_id", "") or "").strip() if not bid: return "需要BugID" bid = await _resolve_id(sor, "sd_bugs", bid) or bid iid = await _get_scope(sor, "sd_bugs", "id", bid, "iteration_id") from .bug_capability import start_fix ok, msg = await start_fix(bid, iid, assignee_id=p.get("assignee_id", "") or "", who="agent.main_agent") return f"OK: {msg}" if ok else f"ERROR: {msg}" async def _h_fix_bug(sor, p, ctx): bid = (p.get("bug_id", "") or "").strip() if not bid: return "需要BugID" bid = await _resolve_id(sor, "sd_bugs", bid) or bid iid = await _get_scope(sor, "sd_bugs", "id", bid, "iteration_id") from .bug_capability import fix_bug ok, msg = await fix_bug(bid, iid, fix_description=p.get("fix_description", "") or "", fix_commit=p.get("fix_commit", "") or "", who="agent.main_agent") return f"OK: {msg}" if ok else f"ERROR: {msg}" async def _h_verify_bug(sor, p, ctx): bid = (p.get("bug_id", "") or "").strip() if not bid: return "需要BugID" bid = await _resolve_id(sor, "sd_bugs", bid) or bid iid = await _get_scope(sor, "sd_bugs", "id", bid, "iteration_id") from .bug_capability import verify_bug ok, msg = await verify_bug(bid, iid, verified_by=ctx.get("user_id", "") or "", who="agent.main_agent") return f"OK: {msg}" if ok else f"ERROR: {msg}" async def _h_close_bug(sor, p, ctx): bid = (p.get("bug_id", "") or "").strip() if not bid: return "需要BugID" bid = await _resolve_id(sor, "sd_bugs", bid) or bid iid = await _get_scope(sor, "sd_bugs", "id", bid, "iteration_id") from .bug_capability import close_bug ok, msg = await close_bug(bid, iid, who="agent.main_agent") return f"OK: {msg}" if ok else f"ERROR: {msg}" async def _h_reject_bug(sor, p, ctx): bid = (p.get("bug_id", "") or "").strip() if not bid: return "需要BugID" bid = await _resolve_id(sor, "sd_bugs", bid) or bid iid = await _get_scope(sor, "sd_bugs", "id", bid, "iteration_id") from .bug_capability import reject_bug ok, msg = await reject_bug(bid, iid, who="agent.main_agent", comment=p.get("comment", "") or "") return f"OK: {msg}" if ok else f"ERROR: {msg}" async def _h_reopen_bug(sor, p, ctx): bid = (p.get("bug_id", "") or "").strip() if not bid: return "需要BugID" bid = await _resolve_id(sor, "sd_bugs", bid) or bid iid = await _get_scope(sor, "sd_bugs", "id", bid, "iteration_id") from .bug_capability import reopen_bug ok, msg = await reopen_bug(bid, iid, who="agent.main_agent") return f"OK: {msg}" if ok else f"ERROR: {msg}" # ── 部署环境 handler ── async def _h_list_envs(sor, p, ctx): pid = ctx.get("project_id", "") if not pid: return "请先切换到项目" from .deploy_capability import list_envs env_type = (p.get("env_type", "") or "").strip() status = (p.get("status", "") or "").strip() lst = await list_envs(pid, env_type=env_type or None, status=status or None) if not lst: return "暂无部署环境" lines = [] for r in lst: lines.append(f"- [{r.get('env_type', '?')}][{r.get('status', '?')}] " f"{r.get('host', '')} ({r.get('user', '')}@{r.get('deploy_path', '')}) id={r.get('id', '')}") return "\n".join(lines) async def _h_configure_env(sor, p, ctx): pid = ctx.get("project_id", "") if not pid: return "请先切换到项目" env_type = (p.get("env_type", "") or "").strip() host = (p.get("host", "") or "").strip() user = (p.get("user", "") or "").strip() deploy_path = (p.get("deploy_path", "") or "").strip() if not env_type or not host or not user or not deploy_path: return "需要 env_type/host/user/deploy_path" from .deploy_capability import configure_env ok, eid = await configure_env( project_id=pid, env_type=env_type, host=host, user=user, deploy_path=deploy_path, port=p.get("port", 22), who="agent.main_agent", ) if not ok: return f"ERROR: {eid}" return f"OK: 已配置环境「{env_type}」(id={eid},状态 configured)" async def _h_verify_env(sor, p, ctx): eid = (p.get("env_id", "") or "").strip() if not eid: return "需要环境ID" pid = ctx.get("project_id", "") eid = await _resolve_id(sor, "sd_deploy_envs", eid) or eid from .deploy_capability import verify_env ok, msg = await verify_env(eid, pid, who="agent.main_agent") return f"OK: {msg}" if ok else f"ERROR: {msg}" # ── 注册能力包 ── SDL_HANDLERS = { "create_task": _h_create_task, "list_tasks": _h_list_tasks, "task_detail": _h_task_detail, "reset_task_retry": _h_reset_task_retry, "approve_task": _h_approve_task, "complete_task": _h_complete_task, "cancel_task": _h_cancel_task, "start_agents": _h_start_agents, "diagnose_project": _h_diagnose_project, "list_deliverables": _h_list_deliverables, "view_deliverable": _h_view_deliverable, "list_questions": _h_list_questions, "answer_question": _h_answer_question, "escalate_question": _h_escalate_question, "add_repo": _h_add_repo, "list_repos": _h_list_repos, "clone_repo": _h_clone_repo, "check_progress": _h_check_progress, "add_bug": _h_add_bug, "propose_feature": _h_propose_feature, "list_features": _h_list_features, "approve_feature": _h_approve_feature, "reject_feature": _h_reject_feature, "verify_feature": _h_verify_feature, # ── 项目生命周期 ── "list_projects": _h_list_projects, "list_role_models": _h_list_role_models, "set_role_model": _h_set_role_model, "start_project": _h_start_project, "complete_project": _h_complete_project, "archive_project": _h_archive_project, "reopen_project": _h_reopen_project, "pause_project": _h_pause_project, "resume_project": _h_resume_project, "backup_project": _h_backup_project, "delete_project": _h_delete_project, # ── 迭代 ── "list_iterations": _h_list_iterations, "create_iteration": _h_create_iteration, "start_iteration": _h_start_iteration, "complete_iteration": _h_complete_iteration, "cancel_iteration": _h_cancel_iteration, "start_next_iteration": _h_start_next_iteration, # ── 交付件评审 ── "submit_deliverable": _h_submit_deliverable, "approve_deliverable": _h_approve_deliverable, "reject_deliverable": _h_reject_deliverable, # ── 测试计划 ── "list_plans": _h_list_plans, "create_test_plan": _h_create_test_plan, "approve_plan": _h_approve_plan, "start_plan": _h_start_plan, "complete_plan": _h_complete_plan, # ── 测试用例 ── "list_cases": _h_list_cases, "create_case": _h_create_case, "pass_case": _h_pass_case, "fail_case": _h_fail_case, "skip_case": _h_skip_case, "block_case": _h_block_case, # ── Bug ── "list_bugs": _h_list_bugs, "report_bug": _h_report_bug, "confirm_bug": _h_confirm_bug, "start_fix": _h_start_fix, "fix_bug": _h_fix_bug, "verify_bug": _h_verify_bug, "close_bug": _h_close_bug, "reject_bug": _h_reject_bug, "reopen_bug": _h_reopen_bug, # ── 部署环境 ── "list_envs": _h_list_envs, "configure_env": _h_configure_env, "verify_env": _h_verify_env, } def register_sdlc_ability(): """注册 SDLC 产线能力包(幂等)。""" ability = PipelineAbility( pipeline_id="sdlc_general", name="通用软件开发产线", tools=SDL_TOOLS, system_prompt=SDL_PROMPT, handlers=SDL_HANDLERS, roles=SDL_ROLES, menus=[ {"label": "📁 项目", "icon": "", "url": "/pipeline-sdlc/sd_projects/index.ui", "type": "popup", "width": "85%", "height": "80%"}, {"label": "⚙️ 模型配置", "icon": "", "url": "/pipeline-sdlc/sd_project_role_models/index.ui", "type": "popup", "width": "85%", "height": "80%", "require_project": True}, {"label": "🐛 Bug", "icon": "", "url": "/pipeline-sdlc/sd_bugs/index.ui", "type": "popup", "width": "85%", "height": "80%", "require_project": True}, {"label": "🧪 测试用例", "icon": "", "url": "/pipeline-sdlc/sd_test_cases/index.ui", "type": "popup", "width": "85%", "height": "80%", "require_project": True}, {"label": "🔄 迭代", "icon": "", "url": "/pipeline-sdlc/sd_iterations/index.ui", "type": "popup", "width": "85%", "height": "80%", "require_project": True}, ], ) register_ability(ability) return ability # ── SDLC 产线 slash 命令(产线专属命名) ── async def _slash_tasks(args, ctx): ex = ctx.get("executor") r = await ex._execute_ability_tool("list_tasks", {"role": args.strip() or ""}) if ex else None return r or "无任务" async def _slash_task(args, ctx): """任务树 + 输入输出(ResourceBrowser 弹窗)。返回 widget dict,前端 AgentOut 检测 widgettype 渲染。""" base = (ctx.get("base_url") or "").rstrip("/") tree_url = f"{base}/pipeline-sdlc/api/task_tree.dspy" io_url = f"{base}/pipeline-sdlc/api/task_io.dspy" return { "widgettype": "PopupWindow", "options": { "title": "任务树 - 输入输出", "width": "85%", "height": "80%", "auto_open": True, }, "subwidgets": [{ "widgettype": "ResourceBrowser", "id": "task_rb", "options": { "tree_options": { "widgettype": "Tree", "options": { "dataurl": tree_url, "textField": "label", "idField": "id", "cfontsize": 1.0, }, }, "browser_options": { "widgettype": "urlwidget", "options": {"url": io_url}, }, "tree_width": "300px", }, }], } async def _slash_diagnose(args, ctx): ex = ctx.get("executor") r = await ex._execute_ability_tool("diagnose_project", {}) if ex else None return r or "无诊断数据" async def _slash_download(args, ctx): """下载项目实时 tgz:打包 + 生成下载链接,弹窗带下载按钮。""" ex = ctx.get("executor") base = (ctx.get("base_url") or "").rstrip("/") pid = ctx.get("project_id") or "" if not pid: return {"widgettype": "Message", "options": {"title": "错误", "message": "请先切换到项目"}} # 实时打包 tgz(复用 backup_project) r = await ex._execute_ability_tool("backup_project", {"project_id": pid}) if ex else None if not r or r.startswith("ERROR"): return {"widgettype": "Message", "options": {"title": "错误", "message": r or "打包失败"}} # r 格式:"OK: /d/pipeline/workspaces/0/_archive/xxx.tgz" tgz_path = (r[4:].strip() if r.startswith("OK: ") else "").strip() if not tgz_path: return {"widgettype": "Message", "options": {"title": "错误", "message": "打包结果异常: " + r}} from urllib.parse import quote dl_url = f"{base}/pipeline-sdlc/api/download_project.dspy?path={quote(tgz_path)}" fname = tgz_path.split('/')[-1] return { "widgettype": "PopupWindow", "options": {"title": "项目备份下载", "cwidth": 60, "cheight": 12, "auto_open": True}, "subwidgets": [ {"widgettype": "Text", "options": {"text": f"项目备份已生成:{fname}", "cfontsize": 1.0}}, {"widgettype": "Button", "options": {"label": "点击下载 tgz", "css": "filler"}, "binds": [{"wid": "self", "event": "click", "actiontype": "script", "target": "self", "script": f"window.location.href='{dl_url}';"}]}, ], } def register_sdlc_slash_commands(): for cmd in [ SlashCommand("tasks", "列出任务(可按角色筛选)", _slash_tasks, "pipeline", "sdlc_general"), SlashCommand("task", "任务树+输入输出(弹窗)", _slash_task, "pipeline", "sdlc_general"), SlashCommand("diagnose", "诊断项目状态", _slash_diagnose, "pipeline", "sdlc_general"), SlashCommand("download", "下载项目tgz备份(生成下载链接)", _slash_download, "pipeline", "sdlc_general"), ]: register_slash_command(cmd) # import 即注册(模块加载副作用),pipeline-app 启动时 import pipeline_service 即生效 register_sdlc_ability() register_sdlc_slash_commands()