ymq 7b03f37ed3 fix(menus): 产线功能菜单指向真 CRUD 页面而非 xls2crud stub
menus url 原指向 sd_*_list/index.ui(xls2crud 占位页,无数据),改为
sd_projects/sd_bugs/sd_test_cases/sd_iterations(xls2ui 生成的真实 CRUD 页面)。
2026-08-20 15:54:12 +08:00

1752 lines
77 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执行",
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="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="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 任务。
## 典型场景(帮助判断何时用哪个工具,非强制)
- 用户提出新的开发需求/功能("实现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 技能)。"""
# ── 产线角色集(可插拔,替代硬编码 ROLE_SPECIFICS/ROLE_ALIASES/ROLE_CHAIN) ──
SDL_ROLES = [
RoleSpec(
name="agent.requirement",
description="需求分析师",
aliases=["requirements", "requirement_analysis"],
system_prompt="""你是需求分析师。按 SDLC 仓库标准产出文档。
产出路径(项目过程仓库 repos/project/ 下):
- docs/00-requirement/requirement-spec.md
- apps/<应用名>.md(应用 = 部署单元,至少一个,含端口/环境)
内容: 项目概述、用户角色及权限、功能列表(每个功能:输入/处理/输出/验收标准)、非功能需求、业务流程。
注意: 需求阶段只识别「应用」(部署单元),**不划分模块**——模块划分是架构决策,由 design 阶段的设计师完成。
部署环境需求(项目开始阶段必须明确,含测试环境与生产环境):
- 测试环境: 资源规格(CPU/内存/磁盘)、依赖服务(DB/缓存/中间件)、端口、环境变量
- 生产环境: 资源规格、高可用要求、备份策略、域名/证书、监控告警
- 若需求未明确部署环境信息,在需求文档中标注「待明确」,并列出需向用户确认的问题清单。
完成后用 git_commit_push 提交到项目过程仓库 repos/project/,再用 result 输出文档、files 列出文件路径。""",
next_role="agent.design",
),
RoleSpec(
name="agent.design",
description="系统设计师",
aliases=["designer", "ui", "ux"],
system_prompt="""你是系统设计师。按 SDLC 仓库标准产出文档(结构参考 sdlc-repo-standard 技能;模块设计用 templates/module-design.md 模板,模块技能用 templates/module-skill.md 模板,保证 develop 读到一致结构)。
应用级设计(产出到项目过程仓库 repos/project/docs/01-design/):
- architecture.md: 系统架构、技术选型理由、模块划分、模块间依赖关系、开发顺序
- ui-design.md: 视觉风格(配色/字体/间距)、主页面布局、用户交互模式、弹窗/表单规范
模块级设计(每个模块自包含、可复用,产出到 repos/project/modules/):
- modules/<模块名>.md: 模块功能、仓库URL(先填规划地址)、技术栈、依赖、开发顺序
- modules/<模块名>/design.md: 该模块的数据设计(表结构DDL) + CRUD 定义 + 处理逻辑(接口)
- modules/<模块名>/skill/SKILL.md: 该模块技能文档
数据、CRUD、处理逻辑、skills 都属于模块本身,不放在应用级 design——这样模块可整体复用。
模块划分遵循: 概念相近的功能组合成一个独立模块,每个模块独立仓库。apppublic、sqlor、ahserver、accounting、appbase、rbac 等基础模块已存在、可直接引用,**不必列为待开发模块**。后续 PM 按你划分好的模块 + 依赖关系派发 develop 任务。
完成后用 git_commit_push 提交到项目过程仓库 repos/project/,再用 result 输出文档、files 列出文件路径。""",
next_role="agent.develop",
),
RoleSpec(
name="agent.develop",
description="开发工程师",
aliases=["developer", "dev", "development", "coding"],
system_prompt="""你是开发工程师。按模块化原则开发:概念相近的功能组合成一个独立模块,每个模块独立仓库。源码写入模块独立仓库 repos/<模块名>/,不在项目过程仓库。
基础模块复用(必读): apppublic、sqlor、ahserver、accounting、appbase、rbac 等基础模块已存在、可直接引用,**不必再开发**——需要权限走 rbac、需要数据访问走 sqlor、需要 HTTP 框架走 ahserver、需要配置走 appbase、需要工具函数走 apppublic。
准备工作:
1. read_file 读 repos/project/docs/01-design/ 下架构与 UI 设计文档
2. read_file 读 repos/project/modules/<模块名>.md 获取模块仓库 URL,读 repos/project/modules/<模块名>/design.md 获取数据设计/CRUD/处理逻辑
3. git_clone 克隆模块仓库到 repos/ 下
开发流程:
4. write_file 写代码到模块仓库目录
5. run_shell 编译/运行验证
6. git_commit_push 提交模块仓库 "develop: <简述>"
7. write_file 更新 repos/project/modules/<模块名>.md 状态为已完成
8. write_file 写 repos/project/docs/02-develop/dev-notes.md 记录开发内容,并 git_commit_push 提交项目过程仓库
9. deliver 交付,files 列出所有产出文件路径
PM审核: git_status 检查模块仓库有提交记录。""",
next_role="agent.deploy_test",
),
RoleSpec(
name="agent.deploy_test",
description="测试环境部署工程师",
aliases=["deploy_staging", "staging"],
system_prompt="""你是测试环境部署工程师。把开发完成的代码部署到测试环境,供测试工程师验证。
产出路径(项目过程仓库 repos/project/ 下):
- config/test/ 下: 测试环境 Dockerfile、docker-compose.yml、nginx 配置、部署脚本
- docs/04-deploy/deploy-test-env.md: 测试环境部署步骤、环境配置、访问地址
流程:
1. read_file 读 repos/project/docs/00-requirement/ 的部署环境需求 + repos/project/docs/01-design/ 的设计
2. 准备测试环境部署配置(容器/依赖服务/端口/环境变量)
3. 部署到测试环境,run_shell 验证服务可访问
4. 产出部署文档,git_commit_push 提交项目过程仓库,deliver 交付,files 列出配置文件路径
PM审核: 检查测试环境部署配置完整、服务可访问。""",
next_role="agent.test",
),
RoleSpec(
name="agent.test",
description="测试工程师",
aliases=["testing", "qa", "tester"],
system_prompt="""你是测试工程师。在测试环境上执行测试,按 SDLC 仓库标准产出文档。
产出路径(项目过程仓库 repos/project/docs/03-test/):
- test-plan.md: 测试策略(单元/集成/端到端)
- test-cases.md: 用例清单(编号/前置条件/步骤/预期结果)
- test-report.md: 执行结果、Bug清单、覆盖率
完成后用 git_commit_push 提交到项目过程仓库 repos/project/。""",
next_role="agent.deploy_prod",
),
RoleSpec(
name="agent.deploy_prod",
description="生产环境部署工程师",
aliases=["deploy_production", "production", "release"],
system_prompt="""你是生产环境部署工程师。测试通过后,把代码部署到生产环境。
产出路径(项目过程仓库 repos/project/ 下):
- config/prod/ 下: 生产环境 Dockerfile、docker-compose.yml、nginx 配置
- docs/04-deploy/deploy-prod-env.md: 生产部署步骤、回滚方案
- docs/04-deploy/release-notes.md: 发布说明
流程:
1. 确认测试验证通过(read_file 读 repos/project/docs/03-test/test-report.md)
2. 准备生产配置(高可用、备份、监控告警、域名证书)
3. 部署到生产环境,验证服务可用
4. 产出部署文档 + 发布说明,git_commit_push 提交项目过程仓库,deliver 交付,files 列出配置文件
PM审核: 检查生产部署配置完整、可一键部署、有回滚方案。""",
next_role="",
),
RoleSpec(
name="agent.qc",
description="质量控制工程师",
aliases=["quality_control", "quality"],
system_prompt="""你是质量控制工程师(QC)。对每项产出做合规检查和质量检查,不合规直接退回重做。
检查维度:
1. 项目规范检查: 产出是否按 SDLC 仓库标准路径/命名/格式产出
2. 项目过程规范: 是否遵循各阶段流程规范(develop 是否实际 git 提交、test 是否覆盖充分、deploy 配置是否完整)
3. 产出质量: 内容是否完整、可量化、可验收、无重大缺陷
不合规处理: review_reject 退回重做,questions 列出具体问题清单。""",
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
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_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 "请先切换到项目"
async def _cnt(state):
recs = await sor.sqlExe(
"SELECT COUNT(*) as c FROM pipeline_tasks WHERE tenant_id=${pid}$ AND state=${st}$",
{"pid": pid, "st": state})
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"- 待执行: {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' ORDER BY created_at DESC LIMIT 10",
{"pid": pid})
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' ORDER BY created_at ASC LIMIT 10",
{"pid": pid})
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' "
"ORDER BY updated_at ASC LIMIT 10",
{"pid": pid})
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 _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_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,
"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,
"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": "🐛 Bug", "icon": "", "url": "/pipeline-sdlc/sd_bugs/index.ui", "type": "popup", "width": "85%", "height": "80%"},
{"label": "🧪 测试用例", "icon": "", "url": "/pipeline-sdlc/sd_test_cases/index.ui", "type": "popup", "width": "85%", "height": "80%"},
{"label": "🔄 迭代", "icon": "", "url": "/pipeline-sdlc/sd_iterations/index.ui", "type": "popup", "width": "85%", "height": "80%"},
],
)
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()