ymq bf819f3b79 refactor(agent_loop): 状态机迁移走 task 能力 + 角色规范化 agent.{role}
- 角色规范: agent 角色 agent.{role}(_normalize_role 补前缀+ROLE_ALIASES/ROLE_CHAIN/SDL_ROLES 改规范名),
  人角色 {orgtype}.{role}(默认 owner.superuser)
- role_agent_run/pm_review_run/handle_failed_task 硬编码状态 SQL 改 task_capability 工具
  (submit/approve/reject/complete/mark_failed/retry,CAS+审计)
- communication/questions/sdlc_ability 默认 handler 角色规范化(main_agent→agent.main_agent, customer→owner.superuser)
- 通用助手路径(AgentExecutor)不动,保持 hermes 式自由对话
2026-08-16 23:51:55 +08:00

671 lines
24 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": "任务详细描述",
},
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="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",
),
]
# ── prompt 片段(追加到 core 通用心智之后) ──
SDL_PROMPT = """你是「开发产线」的驾驶舱 agent,负责软件项目的全流程管理(需求 → 设计 → 开发 → 测试 → 部署)。
## 典型场景(帮助判断何时用哪个工具,非强制)
- 用户提出新的开发需求/功能("实现XX""XX系统要做XX")→ 用 create_task 创建任务(可按 requirement→design→develop 拆分),再 start_agents 启动。
- 用户问进展/状态 → list_tasks / diagnose_project / check_progress。
- 用户问待回答问题 → list_questions / answer_question。
- 用户报告异常/故障 → 先用 diagnose_project 定位根因,再用 task_detail 查详情。
- 仓库管理 → add_repo / list_repos / clone_repo。
- 发现卡点(任务卡死、审核超时、失败、僵尸 claimed_by)时,主动定位根因并推动修复。
- 发现「待回答问题」> 0 或角色提问时,加载 team-communication 技能按冒泡链处理(list_questions 列问题 → 能答则 answer_question → 不能答 escalate_question 沿冒泡路径转下一个处理方)。"""
# ── 产线角色集(可插拔,替代硬编码 ROLE_SPECIFICS/ROLE_ALIASES/ROLE_CHAIN) ──
SDL_ROLES = [
RoleSpec(
name="agent.requirement",
description="需求分析师",
aliases=["requirements", "requirement_analysis"],
system_prompt="""你是需求分析师。按 SDLC 仓库标准产出文档。
产出路径: docs/00-requirement/requirement-spec.md
同时创建(项目至少一个应用):
- apps/<应用名>.md: 应用描述、部署环境、端口
- modules/<模块名>.md: 模块功能、仓库URL(先填写规划地址)、技术栈、依赖
- 每个应用至少关联一个模块
内容: 项目概述、用户角色及权限、功能列表(每个功能:输入/处理/输出/验收标准)、非功能需求、业务流程。""",
next_role="agent.design",
),
RoleSpec(
name="agent.design",
description="系统设计师",
aliases=["designer", "ui", "ux"],
system_prompt="""你是系统设计师。按 SDLC 仓库标准产出文档。
产出路径: docs/01-design/ 目录下:
- architecture.md: 系统架构、技术选型理由
- database-design.md: ER图描述、表结构DDL
- api-design.md: 接口列表(method/path/request/response)
- ui-design.md: 页面结构、组件树(如适用)
产出后用 result 输出文档,files 列出所有文件路径。""",
next_role="agent.develop",
),
RoleSpec(
name="agent.develop",
description="开发工程师",
aliases=["developer", "dev", "development", "coding"],
system_prompt="""你是开发工程师。源码写入模块独立仓库,不在项目仓库。
准备工作:
1. read_file 读 docs/01-design/ 下的设计文档
2. read_file 读 modules/<模块名>.md 获取模块仓库 URL
3. git_clone 克隆模块仓库到工作空间
开发流程:
4. write_file 写代码到模块仓库目录下
5. run_shell 编译/运行验证
6. git_commit_push 提交 "develop: <简述>"
7. write_file 更新 modules/<模块名>.md 状态为已完成
8. write_file 写 docs/02-develop/dev-notes.md 记录开发内容
9. deliver 交付,files 列出所有产出文件路径
PM审核: git_status 检查模块仓库有提交记录。""",
next_role="agent.test",
),
RoleSpec(
name="agent.test",
description="测试工程师",
aliases=["testing", "qa", "tester"],
system_prompt="""你是测试工程师。按 SDLC 仓库标准产出文档。
产出路径: docs/03-test/ 目录下:
- test-plan.md: 测试策略(单元/集成/端到端)
- test-cases.md: 用例清单(编号/前置条件/步骤/预期结果)
- test-report.md: 执行结果、Bug清单、覆盖率
测试脚本放到 files。""",
next_role="agent.deploy",
),
RoleSpec(
name="agent.deploy",
description="部署运维工程师",
aliases=["deployment", "release"],
system_prompt="""你是部署运维工程师。按 SDLC 仓库标准产出文档和配置。
产出路径:
- docs/04-deploy/deploy-guide.md: 环境要求、部署步骤、回滚方案
- docs/04-deploy/release-notes.md: 发布说明
- config/ 下: Dockerfile、nginx配置、docker-compose.yml 等
产出后用 result 输出部署说明,files 列出所有配置文件路径。""",
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 "需要任务标题"
tid = getID()
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({"description": desc}, ensure_ascii=False),
})
return f"OK: 已创建任务 {title}(角色: {role})"
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": "⏸️",
}
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_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:
its = await sor.sqlExe(
"SELECT id FROM sd_iterations WHERE project_id=${pid}$ ORDER BY created_at ASC LIMIT 1",
{"pid": pid})
if its:
iteration_id = getattr(its[0], "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}"
# ── 注册能力包 ──
SDL_HANDLERS = {
"create_task": _h_create_task,
"list_tasks": _h_list_tasks,
"task_detail": _h_task_detail,
"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,
}
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_project_list/index.ui", "type": "popup", "width": "85%", "height": "80%"},
{"label": "🐛 Bug", "icon": "", "url": "/pipeline-sdlc/sd_bug_list/index.ui", "type": "popup", "width": "85%", "height": "80%"},
{"label": "🧪 测试用例", "icon": "", "url": "/pipeline-sdlc/sd_test_case_list/index.ui", "type": "popup", "width": "85%", "height": "80%"},
{"label": "🔄 迭代", "icon": "", "url": "/pipeline-sdlc/sd_iteration_list/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_diagnose(args, ctx):
ex = ctx.get("executor")
r = await ex._execute_ability_tool("diagnose_project", {}) if ex else None
return r or "无诊断数据"
def register_sdlc_slash_commands():
for cmd in [
SlashCommand("tasks", "列出任务(可按角色筛选)", _slash_tasks, "pipeline", "sdlc_general"),
SlashCommand("diagnose", "诊断项目状态", _slash_diagnose, "pipeline", "sdlc_general"),
]:
register_slash_command(cmd)
# import 即注册(模块加载副作用),pipeline-app 启动时 import pipeline_service 即生效
register_sdlc_ability()
register_sdlc_slash_commands()