- 角色规范: 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 式自由对话
671 lines
24 KiB
Python
671 lines
24 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": "任务详细描述",
|
||
},
|
||
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()
|