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