2055 lines
92 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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执行。角色必须属于当前产线不同产线角色集不同"
"不确定有哪些角色时先调 list_roles 查看,禁止凭记忆用其他产线的角色名",
parameters={
"title": "任务标题",
"role": "目标角色(必须是当前产线的角色,用 list_roles 查看)",
"description": "任务详细描述",
"iteration_id": "归属迭代(可选,默认当前迭代)",
},
category="task",
),
ToolDefinition(
name="list_roles",
description="列出当前产线的全部角色(角色名/职责/别名)。建任务前用它确认当前产线有哪些可用角色",
parameters={},
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→approvedPM 验收)",
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="克隆项目关联的仓库到机构工作空间 apps/modules 下不传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-maxmodel_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="暂停项目推进(进行中→pausedSDLC项目状态为active、投标/商机项目为in_progress两者都可暂停暂停后PM不再推进仅项目人类owner可执行)", parameters={"project_id": "项目ID"}, category="project", requires_confirmation=True),
ToolDefinition(name="resume_project", description="恢复项目推进(暂停中→恢复为暂停前的状态;仅项目人类 owner 可执行)", parameters={"project_id": "项目ID"}, category="project", requires_confirmation=True),
ToolDefinition(name="backup_project", description="归档备份项目(打包工作目录+数据记录为tgz到机构工作目录_archive/)", parameters={"project_id": "项目ID"}, category="project"),
ToolDefinition(name="delete_project", description="删除项目(直接带confirm=true调用,平台确认门会弹窗向用户最终确认,不要先向用户复述确认;删除前自动归档备份;仅项目人类owner可执行)", parameters={"project_id": "项目ID", "confirm": "固定传true"}, category="project", requires_confirmation=True),
ToolDefinition(name="cleanup_orphans", description="清扫指向已删项目/迭代/任务的孤儿残留记录(需confirm=truedry_run=true只统计不删)", parameters={"confirm": "二次确认必须为true", "dry_run": "true=只统计不删除(可选)"}, category="admin"),
# ── 迭代 ──
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"),
# ── 知识库按项目owner权限统一走 rag 对外 API──
ToolDefinition(name="rag_search", description="检索知识库按项目owner权限自动限定可检范围。查资料/找依据/了解背景时用;不确定有哪些库先调 rag_kb_list", parameters={"query": "检索内容", "kb_id": "知识库ID(可选,缺省检索全部可见库)", "top_k": "返回条数(可选,默认10)"}, category="knowledge"),
ToolDefinition(name="rag_kb_list", description="列出项目可见的知识库(名称+ID+文档数),检索前先确认有哪些库", parameters={}, category="knowledge"),
]
# ── 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 全套」这种巨型任务。**模块化原则**:模块划分在 requirement 阶段初步完成module-partitioning 技能:八原则+四张图,产出 module-partition.mddesign 阶段复核+细化+定 app应用/部署单元与模块间依赖PM 按 design 定稿的模块清单派发apppublic/sqlor/ahserver/accounting/appbase/rbac 等基础模块已存在可直接引用,不必再开发。**复杂度评估**:所有角色交付件必附复杂度评估(见 sdlc-repo-standard 通用交付约定PM 派发/审核时读取并据此编排。
- 用户问进展/状态 → 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_taskreview→approved、标记完成 complete_taskapproved/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",
task_title="需求分析",
deliverable_types=["requirement_spec"],
),
RoleSpec(
name="agent.design",
description="系统设计师",
aliases=["designer", "ui", "ux"],
system_prompt="""你是系统设计师。先 load_skill 加载 role 技能,按其中的职责与应遵守规范执行任务。""",
next_role="agent.develop",
task_title="应用架构与模块设计",
deliverable_types=["design_doc"],
),
RoleSpec(
name="agent.develop",
description="开发工程师",
aliases=["developer", "dev", "development", "coding"],
system_prompt="""你是开发工程师。先 load_skill 加载 role 技能,按其中的职责与应遵守规范执行任务。""",
next_role="agent.deploy_test",
task_title="应用脚手架开发",
deliverable_types=["code_files"],
),
RoleSpec(
name="agent.deploy_test",
description="测试环境部署工程师",
aliases=["deploy_staging", "staging", "deploy", "agent.deploy"],
system_prompt="""你是测试环境部署工程师。先 load_skill 加载 role 技能,按其中的职责与应遵守规范执行任务。""",
next_role="agent.test",
task_title="部署测试",
deliverable_types=["deploy_doc", "deploy_evidence", "deploy_config"],
),
RoleSpec(
name="agent.test",
description="测试工程师",
aliases=["testing", "qa", "tester"],
system_prompt="""你是测试工程师。先 load_skill 加载 role 技能,按其中的职责与应遵守规范执行任务。""",
# 生产部署需人工指令test 通过后不自动派发 deploy_proddeploy_prod 由用户明确指令触发
next_role="",
task_title="功能测试",
deliverable_types=["test_report", "test_docs", "test_plan"],
),
RoleSpec(
name="agent.deploy_prod",
description="生产环境部署工程师",
aliases=["deploy_production", "production", "release"],
system_prompt="""你是生产环境部署工程师。先 load_skill 加载 role 技能,按其中的职责与应遵守规范执行任务。""",
next_role="",
deliverable_types=["deploy_doc", "deploy_evidence", "release_notes"],
),
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_list_roles(sor, p, ctx):
"""列出当前产线的全部角色(名称/职责/别名)。"""
from pipeline_core import list_roles as _list_roles
cur_pipeline = ctx.get("pipeline_id", "")
if not cur_pipeline:
return "无产线上下文(请先进入某产线的项目)"
specs = _list_roles(cur_pipeline)
if not specs:
return "产线「%s」未注册角色集" % cur_pipeline
lines = []
for s in specs:
aliases = "(别名: %s" % "/".join(s.aliases) if getattr(s, "aliases", None) else ""
lines.append("- %s%s%s" % (s.name, s.description, aliases))
return "当前产线「%s」共 %d 个角色:\n" % (cur_pipeline, len(specs)) + "\n".join(lines)
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)
# 产线角色守卫:角色必须属于当前产线的角色集,否则拒绝创建。
# 背景:共享 create_task 被多产线复用PM 曾按工具描述里硬编码的开发角色名
# requirement/design/develop在投标产线里建章节任务 → 角色无人认领、任务死循环。
# 校验用 pipeline_core 注册表按当前产线查角色;能力包未注册时放行(逃逸阀,不阻塞)。
from pipeline_core import get_role_spec, list_roles as _list_roles
cur_pipeline = ctx.get("pipeline_id", "")
if cur_pipeline and _list_roles(cur_pipeline):
spec = get_role_spec(cur_pipeline, role)
if not spec:
avail = "".join("%s(%s)" % (s.name, s.description) for s in _list_roles(cur_pipeline))
return ("⚠️ 拒绝创建:角色「%s」不属于当前产线「%s」。当前产线可用角色:%s"
"请改用上述角色重新创建(可用 list_roles 随时查看)。"
% (role, cur_pipeline, avail))
role = spec.name # 归一到注册表规范名(比裸名拼接更可靠)
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
space_dir = os.path.dirname(workspace_dir.rstrip('/'))
project_name = os.path.basename(workspace_dir.rstrip('/'))
await _commit_repos_after_approve(space_dir, project_name, 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):
"""标记任务完成PMapproved/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, _get_space_dir, _git_clone)
workspace_dir = await _get_workspace_dir(sor, pid)
space_dir = await _get_space_dir(sor, pid)
url = (p.get("repo_url", "") or "").strip()
if url:
name = url.rstrip("/").split("/")[-1].replace(".git", "")
if name.endswith("_app"):
target = os.path.join(space_dir, "apps", name[:-4])
else:
target = os.path.join(space_dir, "modules", name)
r = await _git_clone(url, target, p.get("branch", "main"))
return f"rc={r['rc']} {r['message']}"
results = await _setup_repos(sor, space_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 _require_project_owner(sor, ctx, pid):
"""项目生命周期变更的 owner 门禁2026-08-31 安全加固)。
会话 agent 的项目终止/删除等指令必须来自该项目的**人类 owner**。
校验在代码层做(不靠 promptctx.user_id 是网关层登录态,无法被 LLM 伪造;
仅当 created_by == 登录用户时放行。
无 user_id无人值守/agent 直调)一律拒绝——生命周期变更只允许人类 owner
从会话发起;产线内部如需暂停(如 failed-poller 熔断)直接调
project_capability不走本工具层。
返回 (ok, err)ok=True 放行ok=False 时 err 为拒绝原因,原样返回给调用方。
"""
from .project_capability import check_project_owner
uid = ctx.get("user_id", "") or ""
if not uid:
return False, "权限不足:项目生命周期变更(暂停/恢复/归档/删除)必须由登录用户发起,且须为项目 owner。当前调用无登录身份已拒绝。"
ok, err = await check_project_owner(pid, uid, sor)
if not ok:
return False, f"权限不足:{err}。终止/删除等项目指令只接受该项目的创建者(人类 owner"
return True, ""
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()
# 产线隔离强制2026-09-05 收紧真实产线会话space≠general只列本产线项目——
# 投标/商机产线不串出开发产线的项目。
# space=='general' 只会出现在纯通用会话(无产线插件),此时绝不列任何项目
# (通用助手不得看到/操作其他产线的项目);上游 _execute_ability_tool 对
# generic 会话已直接拒绝,这里是执行层兜底。
space = ctx.get("space", "") or ""
if not space or space == "general":
return "FAIL: 当前会话无产线上下文,不提供项目列表。"
lst = await list_projects(org_id=org_id or None, status=status or None,
pipeline_id=space)
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
ok, err = await _require_project_owner(sor, ctx, pid)
if not ok:
return f"ERROR: {err}"
from .project_capability import start_project
ok, msg = await start_project(pid, who=ctx.get("user_id") or "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
ok, err = await _require_project_owner(sor, ctx, pid)
if not ok:
return f"ERROR: {err}"
from .project_capability import complete_project
ok, msg = await complete_project(pid, who=ctx.get("user_id") or "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
ok, err = await _require_project_owner(sor, ctx, pid)
if not ok:
return f"ERROR: {err}"
from .project_capability import archive_project
ok, msg = await archive_project(pid, who=ctx.get("user_id") or "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
ok, err = await _require_project_owner(sor, ctx, pid)
if not ok:
return f"ERROR: {err}"
from .project_capability import reopen_project
ok, msg = await reopen_project(pid, who=ctx.get("user_id") or "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
ok, err = await _require_project_owner(sor, ctx, pid)
if not ok:
return f"ERROR: {err}"
from .project_capability import pause_project
ok, msg = await pause_project(pid, who=ctx.get("user_id") or "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
ok, err = await _require_project_owner(sor, ctx, pid)
if not ok:
return f"ERROR: {err}"
from .project_capability import resume_project
ok, msg = await resume_project(pid, who=ctx.get("user_id") or "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
ok, err = await _require_project_owner(sor, ctx, pid)
if not ok:
return f"ERROR: {err}"
from .project_capability import backup_project
ok, msg = await backup_project(pid, who=ctx.get("user_id") or "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
ok, err = await _require_project_owner(sor, ctx, pid)
if not ok:
return f"ERROR: {err}"
from .project_capability import delete_project
ok, msg = await delete_project(pid, who=ctx.get("user_id") or "agent.main_agent", confirm=True)
return f"OK: {msg}" if ok else f"ERROR: {msg}"
async def _h_cleanup_orphans(sor, p, ctx):
confirm = (p.get("confirm", "") or "").strip().lower()
dry_run = (p.get("dry_run", "") or "").strip().lower() == "true"
from .project_capability import cleanup_orphans
ok, msg = await cleanup_orphans(confirm=(confirm == "true"), dry_run=dry_run)
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}"
# ── 知识库按项目owner权限统一走 rag 对外 API──
async def _h_rag_search(sor, p, ctx):
pid = ctx.get("project_id", "")
if not pid:
return "请先切换到项目"
from .rag_client import tool_rag_search
return await tool_rag_search(pid, p.get("query", ""), kb_id=p.get("kb_id", ""),
top_k=p.get("top_k", 10))
async def _h_rag_kb_list(sor, p, ctx):
pid = ctx.get("project_id", "")
if not pid:
return "请先切换到项目"
from .rag_client import tool_rag_kb_list
return await tool_rag_kb_list(pid)
# ── 注册能力包 ──
SDL_HANDLERS = {
"create_task": _h_create_task,
"list_roles": _h_list_roles,
"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,
"cleanup_orphans": _h_cleanup_orphans,
# ── 迭代 ──
"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,
# ── 知识库 ──
"rag_search": _h_rag_search,
"rag_kb_list": _h_rag_kb_list,
}
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=[
# 「📁 项目」入口已上移到 core agent_menus.dspy 硬编码(新建/切换弹窗,
# 所有产线通用2026-09-11此处不再声明避免重复出现两个项目按钮。
{"label": "⚙️ 模型配置", "icon": "", "url": "/pipeline_core/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()