feat: 补齐任务执行指标与结构化结果摘要

- 补齐 tool_call 和 edge 验证链路的 duration_ms 计算与返回
- 任务详情和任务报告新增 result_summary_detail 结构化摘要
- 摘要中补充最终状态、失败原因、software-a 摘要、审批摘要、验证摘要
- 软件A层术语统一为“最小能力实现”
- 同步更新 README、当前进度总结和相关设计文档
- 补充并通过对应自动化测试
This commit is contained in:
redbotu 2026-04-08 22:35:25 +08:00
parent 62186e7994
commit 5021c8c2ea
14 changed files with 214 additions and 64 deletions

View File

@ -63,7 +63,7 @@ Current backend includes:
`GET /api/demo/approval/requests/{approval_id}` `GET /api/demo/approval/requests/{approval_id}`
`POST /api/demo/approval/requests/{approval_id}/decision` `POST /api/demo/approval/requests/{approval_id}/decision`
`GET /api/demo/approval/requests` `GET /api/demo/approval/requests`
4. demo software-a 4. software-a minimal implementation
`POST /api/demo/software-a/deploy-tasks` `POST /api/demo/software-a/deploy-tasks`
`GET /api/demo/software-a/deploy-tasks/{software_a_task_id}` `GET /api/demo/software-a/deploy-tasks/{software_a_task_id}`
`POST /api/demo/software-a/permissions/check` `POST /api/demo/software-a/permissions/check`
@ -78,16 +78,26 @@ Current execution flow:
1. create task 1. create task
2. confirm task 2. confirm task
3. high-risk task enters approval flow 3. high-risk task enters approval flow
4. check `software-a demo` permission 4. check `software-a` minimal implementation permission
5. create `software-a demo` deploy task 5. create `software-a` minimal implementation deploy task
6. create default edge verification step 6. create default edge verification step
7. edge pulls and reports verification result 7. edge pulls and reports verification result
8. task reaches `SUCCEEDED` / `FAILED` / `CANCELLED` 8. task reaches `SUCCEEDED` / `FAILED` / `CANCELLED`
9. task detail/report returns software-a status, approval trace, tool trace, verification trace and audit trace 9. task detail/report returns software-a status, approval trace, tool trace, verification trace and audit trace
Current execution metrics:
1. `tool_call.duration_ms` is persisted from `started_at` / `finished_at`
2. `verification_trace.duration_ms` is persisted for edge task reports
Current result summary capabilities:
1. task detail/report returns `result_summary_detail`
2. summary includes final status, final reason, software-a result, approval result and verification result
Demo failure semantics currently include: Demo failure semantics currently include:
1. if `app_code` or `version` contains `fail`, `software-a demo` returns a failed deploy task 1. if `app_code` or `version` contains `fail`, the `software-a` minimal implementation returns a failed deploy task
2. approval rejection moves task to `CANCELLED` 2. approval rejection moves task to `CANCELLED`
3. failed edge report moves task to `FAILED` 3. failed edge report moves task to `FAILED`
@ -97,7 +107,7 @@ Automated tests currently cover:
1. create / confirm / get task 1. create / confirm / get task
2. high-risk approval path 2. high-risk approval path
3. identity and software-a demo APIs 3. identity and software-a minimal implementation APIs
4. edge heartbeat / pull / report 4. edge heartbeat / pull / report
5. edge event report 5. edge event report
6. task report trace aggregation 6. task report trace aggregation
@ -109,7 +119,7 @@ Current baseline: `14 passed`
Recommended next implementation steps: Recommended next implementation steps:
1. compute and persist `duration_ms` 1. add more idempotency and rollback tests
2. refine richer result summaries and audit details 2. continue enriching audit details and task-level aggregate metrics
3. add more idempotency and rollback tests 3. continue toward local edge-agent bootstrap
4. then continue with local edge-agent bootstrap and second-batch OpenAPI 4. then continue with second-batch OpenAPI

View File

@ -5,7 +5,7 @@ from app.schemas.software_a import CreateDeployTaskRequest
from app.services.software_a_service import SoftwareAService from app.services.software_a_service import SoftwareAService
class DemoSoftwareAAdapter(SoftwareAAdapter): class MinimalSoftwareAAdapter(SoftwareAAdapter):
def __init__(self, timezone_name: str) -> None: def __init__(self, timezone_name: str) -> None:
self.service = SoftwareAService(timezone_name) self.service = SoftwareAService(timezone_name)

View File

@ -15,8 +15,10 @@ from app.repositories.approval_repository import ApprovalRepository
from app.repositories.audit_repository import AuditRepository from app.repositories.audit_repository import AuditRepository
from app.repositories.edge_repository import EdgeTaskRepository from app.repositories.edge_repository import EdgeTaskRepository
from app.repositories.tool_call_repository import ToolCallRepository from app.repositories.tool_call_repository import ToolCallRepository
from app.adapters.software_a.minimal_adapter import MinimalSoftwareAAdapter
from app.schemas.common import ApiResponse from app.schemas.common import ApiResponse
from app.schemas.task import ( from app.schemas.task import (
ApprovalSummary,
ApprovalTraceItem, ApprovalTraceItem,
AuditTraceItem, AuditTraceItem,
CancelTaskRequest, CancelTaskRequest,
@ -25,11 +27,14 @@ from app.schemas.task import (
CreateTaskData, CreateTaskData,
CreateTaskRequest, CreateTaskRequest,
ParsedIntent, ParsedIntent,
ResultSummaryDetail,
SoftwareAResultSummary,
TaskBasic, TaskBasic,
TaskDetailData, TaskDetailData,
TaskReportData, TaskReportData,
ToolTraceItem, ToolTraceItem,
ToolCallItem, ToolCallItem,
VerificationResultSummary,
VerificationTraceItem, VerificationTraceItem,
) )
from app.services.task_service import TaskConflictError, TaskNotFoundError, TaskService from app.services.task_service import TaskConflictError, TaskNotFoundError, TaskService
@ -42,6 +47,54 @@ def build_request_id(header_value: str | None) -> str:
return header_value or f"req-{uuid4().hex[:12]}" return header_value or f"req-{uuid4().hex[:12]}"
def build_result_summary_detail(task, approval, software_a_detail: dict | None, edge_tasks) -> ResultSummaryDetail:
latest_edge_task = edge_tasks[0] if edge_tasks else None
final_reason = task.summary
if software_a_detail and software_a_detail.get("error_detail"):
final_reason = software_a_detail["error_detail"]
elif latest_edge_task and latest_edge_task.message:
final_reason = latest_edge_task.message
elif approval and approval.approval_status == "REJECTED" and approval.reason:
final_reason = approval.reason
approval_summary = None
if approval:
approval_summary = ApprovalSummary(
approval_id=approval.approval_id,
approval_status=approval.approval_status,
reason=approval.reason,
)
software_a_summary = None
if software_a_detail or task.software_a_task_id or task.software_a_task_status:
software_a_summary = SoftwareAResultSummary(
software_a_task_id=task.software_a_task_id,
task_status=(software_a_detail or {}).get("task_status", task.software_a_task_status),
progress_percent=(software_a_detail or {}).get("progress_percent"),
error_detail=(software_a_detail or {}).get("error_detail"),
started_at=(software_a_detail or {}).get("started_at"),
finished_at=(software_a_detail or {}).get("finished_at"),
)
verification_summary = None
if latest_edge_task:
verification_summary = VerificationResultSummary(
step_id=latest_edge_task.step_id,
step_status=latest_edge_task.step_status,
success=None if latest_edge_task.success is None else bool(latest_edge_task.success),
duration_ms=latest_edge_task.duration_ms,
message=latest_edge_task.message,
)
return ResultSummaryDetail(
final_status=task.task_status,
final_reason=final_reason,
approval=approval_summary,
software_a=software_a_summary,
verification=verification_summary,
)
@router.post("", response_model=ApiResponse[CreateTaskData]) @router.post("", response_model=ApiResponse[CreateTaskData])
def create_task( def create_task(
payload: CreateTaskRequest, payload: CreateTaskRequest,
@ -183,6 +236,10 @@ def get_task(
edge_tasks = EdgeTaskRepository(db).list_by_task_id(task_id) edge_tasks = EdgeTaskRepository(db).list_by_task_id(task_id)
tool_calls = ToolCallRepository(db).list_by_task_id(task_id) tool_calls = ToolCallRepository(db).list_by_task_id(task_id)
approval = ApprovalRepository(db).get_by_task_id(task_id)
software_a_detail = None
if task.software_a_task_id:
software_a_detail = MinimalSoftwareAAdapter(settings.default_timezone).get_deploy_task(task.software_a_task_id)
verification_result = None verification_result = None
if edge_tasks: if edge_tasks:
latest_edge_task = edge_tasks[0] latest_edge_task = edge_tasks[0]
@ -216,6 +273,7 @@ def get_task(
], ],
verification_result=verification_result, verification_result=verification_result,
summary=task.summary, summary=task.summary,
result_summary_detail=build_result_summary_detail(task, approval, software_a_detail, edge_tasks),
), ),
timestamp=format_now(settings.default_timezone), timestamp=format_now(settings.default_timezone),
) )
@ -243,6 +301,9 @@ def get_task_report(
tool_calls = ToolCallRepository(db).list_by_task_id(task_id) tool_calls = ToolCallRepository(db).list_by_task_id(task_id)
edge_tasks = EdgeTaskRepository(db).list_by_task_id(task_id) edge_tasks = EdgeTaskRepository(db).list_by_task_id(task_id)
audit_logs = AuditRepository(db).list_by_task_id(task_id) audit_logs = AuditRepository(db).list_by_task_id(task_id)
software_a_detail = None
if task.software_a_task_id:
software_a_detail = MinimalSoftwareAAdapter(settings.default_timezone).get_deploy_task(task.software_a_task_id)
approval_trace = [] approval_trace = []
if approval: if approval:
@ -282,6 +343,7 @@ def get_task_report(
tool_name=item.tool_name, tool_name=item.tool_name,
step_status=item.step_status, step_status=item.step_status,
success=None if item.success is None else bool(item.success), success=None if item.success is None else bool(item.success),
duration_ms=item.duration_ms,
message=item.message, message=item.message,
params=json.loads(item.params_json), params=json.loads(item.params_json),
result_data=json.loads(item.result_data_json), result_data=json.loads(item.result_data_json),
@ -327,6 +389,7 @@ def get_task_report(
tool_trace=tool_trace, tool_trace=tool_trace,
verification_trace=verification_trace, verification_trace=verification_trace,
result_summary=task.summary, result_summary=task.summary,
result_summary_detail=build_result_summary_detail(task, approval, software_a_detail, edge_tasks),
audit_trace=audit_trace, audit_trace=audit_trace,
), ),
timestamp=format_now(settings.default_timezone), timestamp=format_now(settings.default_timezone),

View File

@ -4,6 +4,9 @@ from datetime import UTC, datetime, timedelta, timezone
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
TIME_FORMAT = "%Y-%m-%d %H:%M:%S.%f"
def resolve_timezone(timezone_name: str): def resolve_timezone(timezone_name: str):
try: try:
return ZoneInfo(timezone_name) return ZoneInfo(timezone_name)
@ -18,4 +21,22 @@ def resolve_timezone(timezone_name: str):
def format_now(timezone_name: str) -> str: def format_now(timezone_name: str) -> str:
current = datetime.now(resolve_timezone(timezone_name)) current = datetime.now(resolve_timezone(timezone_name))
return current.strftime("%Y-%m-%d %H:%M:%S.%f")[:-3] return current.strftime(TIME_FORMAT)[:-3]
def parse_timestamp(value: str | None) -> datetime | None:
if not value:
return None
try:
return datetime.strptime(value, TIME_FORMAT)
except ValueError:
return None
def compute_duration_ms(started_at: str | None, finished_at: str | None) -> int | None:
started = parse_timestamp(started_at)
finished = parse_timestamp(finished_at)
if not started or not finished:
return None
duration_ms = int((finished - started).total_seconds() * 1000)
return max(duration_ms, 0)

View File

@ -20,6 +20,7 @@ class EdgeTask(Base):
message: Mapped[str | None] = mapped_column(Text, nullable=True) message: Mapped[str | None] = mapped_column(Text, nullable=True)
result_data_json: Mapped[str] = mapped_column(Text, nullable=False) result_data_json: Mapped[str] = mapped_column(Text, nullable=False)
evidence_json: Mapped[str] = mapped_column(Text, nullable=False) evidence_json: Mapped[str] = mapped_column(Text, nullable=False)
duration_ms: Mapped[int | None] = mapped_column(Integer, nullable=True)
expire_at: Mapped[str] = mapped_column(Text, nullable=False) expire_at: Mapped[str] = mapped_column(Text, nullable=False)
started_at: Mapped[str | None] = mapped_column(Text, nullable=True) started_at: Mapped[str | None] = mapped_column(Text, nullable=True)
finished_at: Mapped[str | None] = mapped_column(Text, nullable=True) finished_at: Mapped[str | None] = mapped_column(Text, nullable=True)

View File

@ -59,6 +59,37 @@ class VerificationResult(BaseModel):
log_error_count: int | None = None log_error_count: int | None = None
class ApprovalSummary(BaseModel):
approval_id: str | None = None
approval_status: str | None = None
reason: str | None = None
class SoftwareAResultSummary(BaseModel):
software_a_task_id: str | None = None
task_status: str | None = None
progress_percent: int | None = None
error_detail: str | None = None
started_at: str | None = None
finished_at: str | None = None
class VerificationResultSummary(BaseModel):
step_id: str | None = None
step_status: str | None = None
success: bool | None = None
duration_ms: int | None = None
message: str | None = None
class ResultSummaryDetail(BaseModel):
final_status: str
final_reason: str | None = None
approval: ApprovalSummary | None = None
software_a: SoftwareAResultSummary | None = None
verification: VerificationResultSummary | None = None
class TaskDetailData(BaseModel): class TaskDetailData(BaseModel):
task_id: str task_id: str
task_status: str task_status: str
@ -70,6 +101,7 @@ class TaskDetailData(BaseModel):
tool_calls: list[ToolCallItem] tool_calls: list[ToolCallItem]
verification_result: VerificationResult | None = None verification_result: VerificationResult | None = None
summary: str | None = None summary: str | None = None
result_summary_detail: ResultSummaryDetail | None = None
class TaskBasic(BaseModel): class TaskBasic(BaseModel):
@ -112,6 +144,7 @@ class VerificationTraceItem(BaseModel):
tool_name: str tool_name: str
step_status: str step_status: str
success: bool | None = None success: bool | None = None
duration_ms: int | None = None
message: str | None = None message: str | None = None
params: dict[str, Any] params: dict[str, Any]
result_data: dict[str, Any] result_data: dict[str, Any]
@ -139,4 +172,5 @@ class TaskReportData(BaseModel):
tool_trace: list[ToolTraceItem] tool_trace: list[ToolTraceItem]
verification_trace: list[VerificationTraceItem] verification_trace: list[VerificationTraceItem]
result_summary: str | None = None result_summary: str | None = None
result_summary_detail: ResultSummaryDetail | None = None
audit_trace: list[AuditTraceItem] audit_trace: list[AuditTraceItem]

View File

@ -129,7 +129,7 @@ class ApprovalService:
task.approval_status = approval_status task.approval_status = approval_status
task.updated_at = format_now(self.timezone_name) task.updated_at = format_now(self.timezone_name)
if approval_status == APPROVAL_STATUS_APPROVED: if approval_status == APPROVAL_STATUS_APPROVED:
task.summary = "审批已通过,准备调用 software-a demo 执行。" task.summary = "审批已通过,准备调用 software-a 最小能力实现执行。"
self.task_repository.update(task) self.task_repository.update(task)
from app.services.task_service import TaskService from app.services.task_service import TaskService

View File

@ -19,7 +19,7 @@ from app.core.constants import (
TASK_STATUS_SUCCEEDED, TASK_STATUS_SUCCEEDED,
TASK_STATUS_VERIFYING, TASK_STATUS_VERIFYING,
) )
from app.core.time import format_now from app.core.time import compute_duration_ms, format_now
from app.models.edge_node import EdgeNode from app.models.edge_node import EdgeNode
from app.models.edge_task import EdgeTask from app.models.edge_task import EdgeTask
from app.models.audit_log import AuditLog from app.models.audit_log import AuditLog
@ -103,6 +103,7 @@ class EdgeService:
message=None, message=None,
result_data_json="{}", result_data_json="{}",
evidence_json="{}", evidence_json="{}",
duration_ms=None,
expire_at=current_time, expire_at=current_time,
started_at=None, started_at=None,
finished_at=None, finished_at=None,
@ -169,6 +170,7 @@ class EdgeService:
edge_task.evidence_json = json.dumps(evidence, ensure_ascii=False) edge_task.evidence_json = json.dumps(evidence, ensure_ascii=False)
edge_task.started_at = started_at edge_task.started_at = started_at
edge_task.finished_at = finished_at edge_task.finished_at = finished_at
edge_task.duration_ms = compute_duration_ms(started_at, finished_at)
edge_task.updated_at = format_now(self.timezone_name) edge_task.updated_at = format_now(self.timezone_name)
updated_edge_task = self.edge_task_repository.update(edge_task) updated_edge_task = self.edge_task_repository.update(edge_task)
self._write_tool_call( self._write_tool_call(
@ -247,7 +249,7 @@ class EdgeService:
request_payload_json=json.dumps(request_payload, ensure_ascii=False), request_payload_json=json.dumps(request_payload, ensure_ascii=False),
response_payload_json=json.dumps(response_payload, ensure_ascii=False), response_payload_json=json.dumps(response_payload, ensure_ascii=False),
success=1 if success else 0, success=1 if success else 0,
duration_ms=None, duration_ms=compute_duration_ms(started_at, finished_at),
started_at=started_at, started_at=started_at,
finished_at=finished_at, finished_at=finished_at,
) )

View File

@ -27,9 +27,9 @@ from app.core.constants import (
) )
from app.schemas.approval import ApprovalOperator, ApprovalTarget, CreateApprovalRequest from app.schemas.approval import ApprovalOperator, ApprovalTarget, CreateApprovalRequest
from app.schemas.software_a import CreateDeployTaskRequest, DeployOptions, SoftwareAOperator from app.schemas.software_a import CreateDeployTaskRequest, DeployOptions, SoftwareAOperator
from app.core.time import format_now from app.core.time import compute_duration_ms, format_now
from app.adapters.approval.demo_adapter import DemoApprovalAdapter from app.adapters.approval.demo_adapter import DemoApprovalAdapter
from app.adapters.software_a.demo_adapter import DemoSoftwareAAdapter from app.adapters.software_a.minimal_adapter import MinimalSoftwareAAdapter
from app.models.audit_log import AuditLog from app.models.audit_log import AuditLog
from app.models.tool_call import ToolCall from app.models.tool_call import ToolCall
from app.models.task import Task from app.models.task import Task
@ -161,7 +161,7 @@ class TaskService:
else: else:
task.task_status = TASK_STATUS_RUNNING task.task_status = TASK_STATUS_RUNNING
task.approval_status = APPROVAL_STATUS_NOT_REQUIRED task.approval_status = APPROVAL_STATUS_NOT_REQUIRED
task.summary = "任务已确认,准备调用 software-a demo 执行。" task.summary = "任务已确认,准备调用 software-a 最小能力实现执行。"
updated_task = self.repository.update(task) updated_task = self.repository.update(task)
self._write_audit_log( self._write_audit_log(
task_id=updated_task.task_id, task_id=updated_task.task_id,
@ -191,12 +191,12 @@ class TaskService:
def refresh_software_a_status(self, task: Task) -> Task: def refresh_software_a_status(self, task: Task) -> Task:
if not task.software_a_task_id: if not task.software_a_task_id:
return task return task
software_a_task = DemoSoftwareAAdapter(self.timezone_name).get_deploy_task(task.software_a_task_id) software_a_task = MinimalSoftwareAAdapter(self.timezone_name).get_deploy_task(task.software_a_task_id)
if software_a_task: if software_a_task:
task.software_a_task_status = software_a_task["task_status"] task.software_a_task_status = software_a_task["task_status"]
if software_a_task["task_status"] == SOFTWARE_A_TASK_STATUS_FAILED and task.task_status not in {TASK_STATUS_FAILED, TASK_STATUS_CANCELLED}: if software_a_task["task_status"] == SOFTWARE_A_TASK_STATUS_FAILED and task.task_status not in {TASK_STATUS_FAILED, TASK_STATUS_CANCELLED}:
task.task_status = TASK_STATUS_FAILED task.task_status = TASK_STATUS_FAILED
task.summary = f"software-a demo 执行失败: {software_a_task.get('error_detail') or 'unknown error'}" task.summary = f"software-a 最小能力实现执行失败: {software_a_task.get('error_detail') or 'unknown error'}"
task.updated_at = format_now(self.timezone_name) task.updated_at = format_now(self.timezone_name)
task = self.repository.update(task) task = self.repository.update(task)
return task return task
@ -216,7 +216,7 @@ class TaskService:
if self.edge_task_repository.list_active_by_task_id(task.task_id): if self.edge_task_repository.list_active_by_task_id(task.task_id):
raise TaskConflictError("当前任务已存在待处理的 edge 验证步骤,不允许重复调度。") raise TaskConflictError("当前任务已存在待处理的 edge 验证步骤,不允许重复调度。")
allowed, reason = DemoSoftwareAAdapter(self.timezone_name).check_permission( allowed, reason = MinimalSoftwareAAdapter(self.timezone_name).check_permission(
task.action_type or "DEPLOY", task.action_type or "DEPLOY",
task.env, task.env,
task.approval_status, task.approval_status,
@ -226,7 +226,7 @@ class TaskService:
if task.action_type == "DEPLOY": if task.action_type == "DEPLOY":
tool_started_at = format_now(self.timezone_name) tool_started_at = format_now(self.timezone_name)
deploy_result = DemoSoftwareAAdapter(self.timezone_name).create_deploy_task( deploy_result = MinimalSoftwareAAdapter(self.timezone_name).create_deploy_task(
CreateDeployTaskRequest( CreateDeployTaskRequest(
operator=SoftwareAOperator(user_id="u1001", user_name="alice"), operator=SoftwareAOperator(user_id="u1001", user_name="alice"),
tenant_id=task.tenant_id, tenant_id=task.tenant_id,
@ -243,9 +243,9 @@ class TaskService:
deploy_success = deploy_result["task_status"] != SOFTWARE_A_TASK_STATUS_FAILED deploy_success = deploy_result["task_status"] != SOFTWARE_A_TASK_STATUS_FAILED
task.task_status = TASK_STATUS_RUNNING if deploy_success else TASK_STATUS_FAILED task.task_status = TASK_STATUS_RUNNING if deploy_success else TASK_STATUS_FAILED
task.summary = ( task.summary = (
"software-a demo 部署任务已创建,等待边缘验证。" "software-a 最小能力部署任务已创建,等待边缘验证。"
if deploy_success if deploy_success
else f"software-a demo 执行失败: {deploy_result.get('error_detail') or 'unknown error'}" else f"software-a 最小能力实现执行失败: {deploy_result.get('error_detail') or 'unknown error'}"
) )
self._write_tool_call( self._write_tool_call(
task_id=task.task_id, task_id=task.task_id,
@ -363,7 +363,7 @@ class TaskService:
request_payload_json=json.dumps(request_payload, ensure_ascii=False), request_payload_json=json.dumps(request_payload, ensure_ascii=False),
response_payload_json=json.dumps(response_payload, ensure_ascii=False), response_payload_json=json.dumps(response_payload, ensure_ascii=False),
success=1 if success else 0, success=1 if success else 0,
duration_ms=duration_ms, duration_ms=duration_ms if duration_ms is not None else compute_duration_ms(started_at, finished_at),
started_at=started_at, started_at=started_at,
finished_at=finished_at, finished_at=finished_at,
) )

View File

@ -38,6 +38,8 @@ def test_task_create_confirm_get() -> None:
assert get_response.json()["data"]["software_a_task_id"] is not None assert get_response.json()["data"]["software_a_task_id"] is not None
assert get_response.json()["data"]["software_a_task_status"] == "SUCCEEDED" assert get_response.json()["data"]["software_a_task_status"] == "SUCCEEDED"
assert get_response.json()["data"]["tool_calls"][0]["tool_name"] == "software_a_deploy" assert get_response.json()["data"]["tool_calls"][0]["tool_name"] == "software_a_deploy"
assert get_response.json()["data"]["result_summary_detail"]["final_status"] == "RUNNING"
assert get_response.json()["data"]["result_summary_detail"]["software_a"]["task_status"] == "SUCCEEDED"
def test_high_risk_task_creates_approval_and_can_be_approved() -> None: def test_high_risk_task_creates_approval_and_can_be_approved() -> None:
@ -274,6 +276,13 @@ def test_task_report_contains_traces() -> None:
assert any(item["request_id"] == "req-report-confirm-001" for item in payload["tool_trace"]) assert any(item["request_id"] == "req-report-confirm-001" for item in payload["tool_trace"])
assert any(item["operator_user_name"] == "alice" for item in payload["tool_trace"]) assert any(item["operator_user_name"] == "alice" for item in payload["tool_trace"])
assert any(item["request_id"] == "req-report-create-001" for item in payload["audit_trace"]) assert any(item["request_id"] == "req-report-create-001" for item in payload["audit_trace"])
assert payload["result_summary_detail"]["final_status"] == "SUCCEEDED"
assert payload["result_summary_detail"]["software_a"]["task_status"] == "SUCCEEDED"
assert payload["result_summary_detail"]["verification"]["success"] is True
deploy_trace = next(item for item in payload["tool_trace"] if item["tool_name"] == "software_a_deploy")
assert deploy_trace["duration_ms"] is not None
verification_trace = payload["verification_trace"][0]
assert verification_trace["duration_ms"] == 100
def test_cancel_running_task() -> None: def test_cancel_running_task() -> None:
@ -473,6 +482,8 @@ def test_task_fails_when_software_a_deploy_fails() -> None:
assert get_response.status_code == 200 assert get_response.status_code == 200
assert get_response.json()["data"]["task_status"] == "FAILED" assert get_response.json()["data"]["task_status"] == "FAILED"
assert get_response.json()["data"]["software_a_task_status"] == "FAILED" assert get_response.json()["data"]["software_a_task_status"] == "FAILED"
assert get_response.json()["data"]["result_summary_detail"]["final_status"] == "FAILED"
assert get_response.json()["data"]["result_summary_detail"]["software_a"]["error_detail"] is not None
pull_response = client.post( pull_response = client.post(
"/api/agent/edge/tasks/pull", "/api/agent/edge/tasks/pull",
@ -599,3 +610,5 @@ def test_edge_failure_marks_task_failed() -> None:
assert get_response.status_code == 200 assert get_response.status_code == 200
assert get_response.json()["data"]["task_status"] == "FAILED" assert get_response.json()["data"]["task_status"] == "FAILED"
assert get_response.json()["data"]["verification_result"]["http_ok"] is False assert get_response.json()["data"]["verification_result"]["http_ok"] is False
assert get_response.json()["data"]["result_summary_detail"]["verification"]["success"] is False
assert get_response.json()["data"]["result_summary_detail"]["final_reason"] == "health check failed"

View File

@ -33,7 +33,7 @@
1. `task` 是主链路核心表。 1. `task` 是主链路核心表。
2. `approval_request` 为高风险任务确认后进入审批预留。 2. `approval_request` 为高风险任务确认后进入审批预留。
3. `tool_call` 为后续软件 A demo / edge 验证接入预留。 3. `tool_call` 为后续软件 A 最小能力实现 / edge 验证接入预留。
4. `audit_log` 为关键动作审计预留。 4. `audit_log` 为关键动作审计预留。
--- ---
@ -107,7 +107,7 @@
用途: 用途:
1. 记录软件 A demo、edge 验证和后续工具调用轨迹。 1. 记录软件 A 最小能力实现、edge 验证和后续工具调用轨迹。
字段: 字段:

View File

@ -12,7 +12,7 @@
2. 项目如何分层和分模块。 2. 项目如何分层和分模块。
3. 哪些模块先实现,哪些模块后实现。 3. 哪些模块先实现,哪些模块后实现。
4. 数据如何落库。 4. 数据如何落库。
5. 如何与软件 A demo、身份 demo、审批 demo、本地 Agent 对接。 5. 如何与软件 A 最小能力实现、身份 demo、审批 demo、本地 Agent 对接。
### 1.2 适用范围 ### 1.2 适用范围
@ -290,7 +290,7 @@ backend/
职责: 职责:
1. 封装软件 A demo 调用。 1. 封装软件 A 最小能力实现调用。
2. 封装身份 demo 调用。 2. 封装身份 demo 调用。
3. 封装审批 demo 调用。 3. 封装审批 demo 调用。
4. 封装模型网关调用。 4. 封装模型网关调用。
@ -321,7 +321,7 @@ backend/
职责: 职责:
1. 执行异步任务。 1. 执行异步任务。
2. 轮询软件 A demo 状态。 2. 轮询软件 A 最小能力实现状态。
3. 拉起验证流程。 3. 拉起验证流程。
4. 处理长任务和超时任务。 4. 处理长任务和超时任务。
@ -537,13 +537,13 @@ demo 阶段建议至少建立以下表:
1. `workflows/deploy_workflow.py` 1. `workflows/deploy_workflow.py`
2. `services/agent_service.py` 2. `services/agent_service.py`
3. `adapters/software_a/demo_adapter.py` 3. `adapters/software_a/minimal_adapter.py`
4. `workers/task_polling_worker.py` 4. `workers/task_polling_worker.py`
5. `services/verification_service.py` 5. `services/verification_service.py`
执行步骤: 执行步骤:
1. 调用软件 A demo 创建部署任务。 1. 调用软件 A 最小能力实现创建部署任务。
2. 轮询部署状态。 2. 轮询部署状态。
3. 部署成功后生成验证步骤。 3. 部署成功后生成验证步骤。
4. 下发到本地 Agent。 4. 下发到本地 Agent。
@ -663,7 +663,7 @@ demo 阶段正式确认:
## 11.2 第二批完成 ## 11.2 第二批完成
1. 软件 A demo adapter。 1. 软件 A 最小能力实现 adapter。
2. 部署工作流。 2. 部署工作流。
3. 审批 demo adapter。 3. 审批 demo adapter。
4. 身份 demo adapter。 4. 身份 demo adapter。
@ -710,7 +710,7 @@ demo 阶段正式确认:
至少覆盖: 至少覆盖:
1. 软件 A demo 联调。 1. 软件 A 最小能力实现联调。
2. 身份 demo 联调。 2. 身份 demo 联调。
3. 审批 demo 联调。 3. 审批 demo 联调。
4. 本地 Agent 联调。 4. 本地 Agent 联调。

View File

@ -10,7 +10,7 @@
1. Agent 对外任务接口。 1. Agent 对外任务接口。
2. 云端与本地 Agent 的交互接口。 2. 云端与本地 Agent 的交互接口。
3. 软件 A demo 接口。 3. 软件 A 最小能力实现接口。
4. 身份 demo 接口。 4. 身份 demo 接口。
5. 审批 demo 接口。 5. 审批 demo 接口。
@ -40,7 +40,7 @@
说明: 说明:
1. 软件 A demo、身份 demo、审批 demo、edge 接口继续保留在本文档中作为后续实现输入。 1. 软件 A 最小能力实现、身份 demo、审批 demo、edge 接口继续保留在本文档中作为后续实现输入。
2. 首批代码骨架只要求先打通以上三条主接口。 2. 首批代码骨架只要求先打通以上三条主接口。
3. 后续扩展 OpenAPI 时,优先保持当前对象模型和错误码不变。 3. 后续扩展 OpenAPI 时,优先保持当前对象模型和错误码不变。
@ -442,11 +442,11 @@ X-Signature: <signature>
--- ---
## 5. 软件 A Demo 接口 ## 5. 软件 A 最小能力实现接口
## 5.1 设计说明 ## 5.1 设计说明
软件 A demo 版本用于支撑 MVP 闭环,其接口语义需尽量贴近未来真实软件 A 的标准能力。 软件 A 最小能力实现用于支撑 MVP 闭环,其接口语义需尽量贴近未来真实软件 A 的标准能力。
建议 base path: 建议 base path:
@ -891,9 +891,9 @@ X-Signature: <signature>
2. Agent 完成解析并返回 `task_id` 和结构化结果。 2. Agent 完成解析并返回 `task_id` 和结构化结果。
3. 用户调用确认接口。 3. 用户调用确认接口。
4. Agent 调用身份 demo 获取操作者信息和权限。 4. Agent 调用身份 demo 获取操作者信息和权限。
5. Agent 调用软件 A demo 权限校验。 5. Agent 调用软件 A 最小能力实现权限校验。
6. Agent 调用软件 A demo 创建部署任务。 6. Agent 调用软件 A 最小能力实现创建部署任务。
7. Agent 轮询软件 A demo 查询状态。 7. Agent 轮询软件 A 最小能力实现查询状态。
8. Agent 调用 Edge 接口执行本地验证。 8. Agent 调用 Edge 接口执行本地验证。
9. Agent 汇总结果,更新任务状态并生成报告。 9. Agent 汇总结果,更新任务状态并生成报告。
@ -911,7 +911,7 @@ X-Signature: <signature>
## 10. demo 与正式系统的替换原则 ## 10. demo 与正式系统的替换原则
1. Agent 上层只依赖统一语义接口,不直接依赖 demo 接口字段差异。 1. Agent 上层只依赖统一语义接口,不直接依赖 demo 接口字段差异。
2. 软件 A demo、身份 demo、审批 demo 均应封装在适配层后面。 2. 软件 A 最小能力实现、身份 demo、审批 demo 均应封装在适配层后面。
3. 后续替换真实系统时,优先保持上层对象模型不变。 3. 后续替换真实系统时,优先保持上层对象模型不变。
4. 如真实系统能力不足,应在适配层内做降级,而不是修改编排主链路。 4. 如真实系统能力不足,应在适配层内做降级,而不是修改编排主链路。
@ -921,7 +921,7 @@ X-Signature: <signature>
1. 先完成通用响应格式、错误码和枚举定义。 1. 先完成通用响应格式、错误码和枚举定义。
2. 再完成 Agent 对外任务接口。 2. 再完成 Agent 对外任务接口。
3. 再完成软件 A demo 接口。 3. 再完成软件 A 最小能力实现接口。
4. 再完成身份 demo 和审批 demo。 4. 再完成身份 demo 和审批 demo。
5. 最后完成本地 Agent 拉取任务与结果回传接口。 5. 最后完成本地 Agent 拉取任务与结果回传接口。

View File

@ -25,7 +25,7 @@
用于描述系统架构、模块分层、数据模型、接口建议、安全设计和实施约束。 用于描述系统架构、模块分层、数据模型、接口建议、安全设计和实施约束。
3. `智能化部署agent-demo接口定义说明.md` 3. `智能化部署agent-demo接口定义说明.md`
用于描述 demo 阶段的接口协议、统一响应格式、状态枚举、Agent 接口、软件 A demo 接口、身份 demo 接口、审批 demo 接口。 用于描述 demo 阶段的接口协议、统一响应格式、状态枚举、Agent 接口、软件 A 最小能力实现接口、身份 demo 接口、审批 demo 接口。
4. `智能化部署agent-demo后端项目骨架设计.md` 4. `智能化部署agent-demo后端项目骨架设计.md`
用于描述 demo 后端的推荐技术栈、项目结构、模块职责、数据库表建议、代码落点和开发顺序。 用于描述 demo 后端的推荐技术栈、项目结构、模块职责、数据库表建议、代码落点和开发顺序。
@ -86,7 +86,7 @@ demo 接口定义文档已覆盖:
1. Agent 对外任务接口。 1. Agent 对外任务接口。
2. 云端与本地 Agent 交互接口。 2. 云端与本地 Agent 交互接口。
3. 软件 A demo 接口。 3. 软件 A 最小能力实现接口。
4. 身份 demo 接口。 4. 身份 demo 接口。
5. 审批 demo 接口。 5. 审批 demo 接口。
6. 内部对象结构。 6. 内部对象结构。
@ -123,17 +123,19 @@ demo 接口定义文档已覆盖:
3. 已实现 `task``approval_request``tool_call``audit_log` 对应的最小模型和数据库初始化逻辑。 3. 已实现 `task``approval_request``tool_call``audit_log` 对应的最小模型和数据库初始化逻辑。
4. 已打通三条主接口: 4. 已打通三条主接口:
`POST /api/agent/tasks``POST /api/agent/tasks/{task_id}/confirm``GET /api/agent/tasks/{task_id}` `POST /api/agent/tasks``POST /api/agent/tasks/{task_id}/confirm``GET /api/agent/tasks/{task_id}`
5. 已实现最小 `identity demo``approval demo``software-a demo` 接口 5. 已实现最小 `identity demo``approval demo``software-a 最小能力实现接口`
6. 已将高风险任务确认后的审批创建流程接入后端主链路。 6. 已将高风险任务确认后的审批创建流程接入后端主链路。
7. 已实现最小 `edge` 心跳、拉取任务、回传结果接口。 7. 已实现最小 `edge` 心跳、拉取任务、回传结果接口。
8. 已将默认验证任务接入 edge 调度主链路。 8. 已将默认验证任务接入 edge 调度主链路。
9. 已将 `software-a demo` 部署任务创建接入主执行链。 9. 已将 `software-a` 最小能力部署任务创建接入主执行链。
10. 已将 `tool_call``audit_log` 接入主链路关键动作。 10. 已将 `tool_call``audit_log` 接入主链路关键动作。
11. 已实现任务报告接口,可返回审批、工具、验证、审计轨迹。 11. 已实现任务报告接口,可返回审批、工具、验证、审计轨迹。
12. 已实现任务取消接口,并将 `request_id``operator` 维度写入关键审计和工具调用记录。 12. 已实现任务取消接口,并将 `request_id``operator` 维度写入关键审计和工具调用记录。
13. 已补充自动化测试,并基于内存 SQLite 完成首轮通过验证。 13. 已补充自动化测试,并基于内存 SQLite 完成首轮通过验证。
14. 已完成任务状态机第一轮收紧,补上重复确认、审批后任务状态漂移、edge 重复回传等冲突校验。 14. 已完成任务状态机第一轮收紧,补上重复确认、审批后任务状态漂移、edge 重复回传等冲突校验。
15. 已补上首轮失败分支细化,包括 software-a demo 执行失败、审批驳回、edge 验证失败三条主失败路径。 15. 已补上首轮失败分支细化,包括 software-a 最小能力实现执行失败、审批驳回、edge 验证失败三条主失败路径。
16. 已完成 `duration_ms` 第一轮落地,`tool_call` 和 edge 验证轨迹可基于 `started_at` / `finished_at` 自动计算并返回时长。
17. 已完成结果摘要第一轮结构化改造,任务详情和任务报告可返回 `result_summary_detail`,包含最终状态、失败原因、software-a 摘要、审批摘要和验证摘要。
### 3.8 当前代码可运行范围 ### 3.8 当前代码可运行范围
@ -143,13 +145,17 @@ demo 接口定义文档已覆盖:
2. 高风险任务确认后自动创建审批单。 2. 高风险任务确认后自动创建审批单。
3. 审批通过后进入执行链,审批驳回后进入取消态。 3. 审批通过后进入执行链,审批驳回后进入取消态。
4. 执行链包含: 4. 执行链包含:
software-a 权限校验 -> software-a demo 部署任务创建 -> edge 默认验证任务创建 -> edge 拉取 -> edge 回传。 software-a 权限校验 -> software-a 最小能力部署任务创建 -> edge 默认验证任务创建 -> edge 拉取 -> edge 回传。
5. 任务详情接口可返回: 5. 任务详情接口可返回:
当前状态、software-a 状态、工具调用摘要、验证结果摘要。 当前状态、software-a 状态、工具调用摘要、验证结果摘要。
6. 任务报告接口可返回: 6. 任务报告接口可返回:
`task_basic``intent_snapshot``approval_trace``tool_trace``verification_trace``result_summary``audit_trace` `task_basic``intent_snapshot``approval_trace``tool_trace``verification_trace``result_summary``audit_trace`
7. edge 侧已支持: 7. edge 侧已支持:
心跳、拉取任务、回传结果、上报异常事件。 心跳、拉取任务、回传结果、上报异常事件。
8. 执行指标当前已支持:
`tool_trace.duration_ms``verification_trace.duration_ms`
9. 结果摘要当前已支持:
`result_summary_detail.final_status``final_reason``software_a``approval``verification`
当前测试基线: 当前测试基线:
@ -174,7 +180,7 @@ demo 接口定义文档已覆盖:
1. 自然语言发起任务。 1. 自然语言发起任务。
2. Agent 解析意图并做结构化任务生成。 2. Agent 解析意图并做结构化任务生成。
3. 策略层做风险判断。 3. 策略层做风险判断。
4. 调用软件 A demo 执行部署或控制动作。 4. 调用软件 A 最小能力实现执行部署或控制动作。
5. 调用本地 Agent 做验证。 5. 调用本地 Agent 做验证。
6. 汇总结果,生成报告和审计。 6. 汇总结果,生成报告和审计。
@ -239,7 +245,7 @@ demo 接口定义文档已覆盖:
`CREATED` -> `PENDING_CONFIRM` -> `RUNNING` -> `VERIFYING` -> `SUCCEEDED` / `FAILED` / `CANCELLED` `CREATED` -> `PENDING_CONFIRM` -> `RUNNING` -> `VERIFYING` -> `SUCCEEDED` / `FAILED` / `CANCELLED`
2. 高风险任务路径为: 2. 高风险任务路径为:
`PENDING_CONFIRM` -> `PENDING_APPROVAL` -> `RUNNING` `PENDING_CONFIRM` -> `PENDING_APPROVAL` -> `RUNNING`
3. `software-a demo` 当前在任务详情查询时会同步刷新状态,因此: 3. `software-a` 最小能力实现当前在任务详情查询时会同步刷新状态,因此:
确认接口返回的 `software_a_task_status` 可能是 `RUNNING`,而后续查询任务详情时可能已变为 `SUCCEEDED` 确认接口返回的 `software_a_task_status` 可能是 `RUNNING`,而后续查询任务详情时可能已变为 `SUCCEEDED`
4. 当前 demo 中的 operator 默认使用: 4. 当前 demo 中的 operator 默认使用:
`alice(u1001)` 作为任务发起和执行方,`bob(u2001)` 作为审批人 `alice(u1001)` 作为任务发起和执行方,`bob(u2001)` 作为审批人
@ -251,7 +257,7 @@ demo 接口定义文档已覆盖:
8. 当前已补上的状态约束包括: 8. 当前已补上的状态约束包括:
重复确认拦截、重复执行拦截、审批决策前必须仍处于 `PENDING_APPROVAL`、edge 重复回传拦截、非 `RUNNING` 任务不再下发 edge 执行。 重复确认拦截、重复执行拦截、审批决策前必须仍处于 `PENDING_APPROVAL`、edge 重复回传拦截、非 `RUNNING` 任务不再下发 edge 执行。
9. 当前 demo 已支持可控失败模拟: 9. 当前 demo 已支持可控失败模拟:
`app_code``version` 包含 `fail`,则 `software-a demo` 会返回失败任务,用于联调失败分支。 `app_code``version` 包含 `fail`,则 `software-a` 最小能力实现会返回失败任务,用于联调失败分支。
--- ---
@ -262,7 +268,7 @@ demo 接口定义文档已覆盖:
1. 本地 `edge-agent` 初始化代码与打包脚本。 1. 本地 `edge-agent` 初始化代码与打包脚本。
2. 文件型 SQLite / PostgreSQL 实库运行验证。 2. 文件型 SQLite / PostgreSQL 实库运行验证。
3. 身份 demo / 审批 demo 与任务主链路的权限、审批决策联动细化。 3. 身份 demo / 审批 demo 与任务主链路的权限、审批决策联动细化。
4. `duration_ms` 等执行指标的真实计算与回填 4. 任务级聚合指标仍未完成,如总耗时、审批耗时、等待耗时
5. 更真实的验证插件实现。 5. 更真实的验证插件实现。
6. 部署脚本和运行脚本完善。 6. 部署脚本和运行脚本完善。
7. OpenAPI 扩展到第二批接口。 7. OpenAPI 扩展到第二批接口。
@ -284,12 +290,12 @@ demo 接口定义文档已覆盖:
当前不是继续补基础文档,而是继续补强现有可运行链路。优先级建议收敛为: 当前不是继续补基础文档,而是继续补强现有可运行链路。优先级建议收敛为:
1. 回填执行指标: 1. 增补失败路径与幂等性测试:
重点补 `duration_ms`、更完整的执行结果摘要与审计信息。
2. 增补失败路径与幂等性测试:
重点补重复请求、重复回传、异常回滚等场景。 重点补重复请求、重复回传、异常回滚等场景。
3. 继续丰富结果摘要与审计细节: 2. 继续丰富审计细节与任务级聚合指标:
让失败原因在详情和报告里更直观可见。 让任务级总耗时、审批耗时、等待耗时可直观看到。
3. 再补更多执行指标:
如任务级聚合耗时、审批耗时、等待耗时。
4. 然后再继续: 4. 然后再继续:
本地 `edge-agent` 骨架、第二批 OpenAPI、更多联调能力。 本地 `edge-agent` 骨架、第二批 OpenAPI、更多联调能力。
@ -303,9 +309,9 @@ demo 接口定义文档已覆盖:
按当前进度,建议后续直接按以下顺序推进: 按当前进度,建议后续直接按以下顺序推进:
1. 计算并持久化 `duration_ms` 1. 增补状态冲突、失败回滚、重复上报等测试
2. 增补状态冲突、失败回滚、重复上报等测试 2. 再补更多任务级执行指标
3. 丰富结果摘要与失败原因呈现 3. 继续增强审计细节
4. 再进入本地 `edge-agent` 初始化代码和第二批 OpenAPI。 4. 再进入本地 `edge-agent` 初始化代码和第二批 OpenAPI。
当前更推荐: 当前更推荐:
@ -329,9 +335,9 @@ demo 接口定义文档已覆盖:
下一步推荐顺序: 下一步推荐顺序:
1. 计算并回填 `duration_ms` 1. 再补失败路径和幂等性测试
2. 再补失败路径和幂等性测试 2. 再补任务级执行指标
3. 再补结果摘要和失败原因展示 3. 再补审计细节和聚合摘要
4. 再补本地 Agent 初始化代码或第二批 OpenAPI。 4. 再补本地 Agent 初始化代码或第二批 OpenAPI。
### 7.2 如果上下文快满,有什么影响 ### 7.2 如果上下文快满,有什么影响
@ -364,4 +370,4 @@ set DATABASE_URL=sqlite:///:memory:
当前已经完成从"写文档"切换到"写 demo 代码"的第一步,下一步进入: 当前已经完成从"写文档"切换到"写 demo 代码"的第一步,下一步进入:
**duration_ms 回填 -> 失败结果呈现增强 -> 本地 Agent 与联调能力继续补齐** **更多执行指标 -> 审计细节增强 -> 本地 Agent 与联调能力继续补齐**