From 5021c8c2eaa41e69c5cb9ad22e95d250211be75d Mon Sep 17 00:00:00 2001 From: redbotu Date: Wed, 8 Apr 2026 22:35:25 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=A1=A5=E9=BD=90=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E6=89=A7=E8=A1=8C=E6=8C=87=E6=A0=87=E4=B8=8E=E7=BB=93=E6=9E=84?= =?UTF-8?q?=E5=8C=96=E7=BB=93=E6=9E=9C=E6=91=98=E8=A6=81=20-=20=E8=A1=A5?= =?UTF-8?q?=E9=BD=90=20tool=5Fcall=20=E5=92=8C=20edge=20=E9=AA=8C=E8=AF=81?= =?UTF-8?q?=E9=93=BE=E8=B7=AF=E7=9A=84=20duration=5Fms=20=E8=AE=A1?= =?UTF-8?q?=E7=AE=97=E4=B8=8E=E8=BF=94=E5=9B=9E=20-=20=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E8=AF=A6=E6=83=85=E5=92=8C=E4=BB=BB=E5=8A=A1=E6=8A=A5=E5=91=8A?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=20result=5Fsummary=5Fdetail=20=E7=BB=93?= =?UTF-8?q?=E6=9E=84=E5=8C=96=E6=91=98=E8=A6=81=20-=20=E6=91=98=E8=A6=81?= =?UTF-8?q?=E4=B8=AD=E8=A1=A5=E5=85=85=E6=9C=80=E7=BB=88=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E3=80=81=E5=A4=B1=E8=B4=A5=E5=8E=9F=E5=9B=A0=E3=80=81software-?= =?UTF-8?q?a=20=E6=91=98=E8=A6=81=E3=80=81=E5=AE=A1=E6=89=B9=E6=91=98?= =?UTF-8?q?=E8=A6=81=E3=80=81=E9=AA=8C=E8=AF=81=E6=91=98=E8=A6=81=20-=20?= =?UTF-8?q?=E8=BD=AF=E4=BB=B6A=E5=B1=82=E6=9C=AF=E8=AF=AD=E7=BB=9F?= =?UTF-8?q?=E4=B8=80=E4=B8=BA=E2=80=9C=E6=9C=80=E5=B0=8F=E8=83=BD=E5=8A=9B?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E2=80=9D=20-=20=E5=90=8C=E6=AD=A5=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=20README=E3=80=81=E5=BD=93=E5=89=8D=E8=BF=9B=E5=BA=A6?= =?UTF-8?q?=E6=80=BB=E7=BB=93=E5=92=8C=E7=9B=B8=E5=85=B3=E8=AE=BE=E8=AE=A1?= =?UTF-8?q?=E6=96=87=E6=A1=A3=20-=20=E8=A1=A5=E5=85=85=E5=B9=B6=E9=80=9A?= =?UTF-8?q?=E8=BF=87=E5=AF=B9=E5=BA=94=E8=87=AA=E5=8A=A8=E5=8C=96=E6=B5=8B?= =?UTF-8?q?=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/README.md | 28 ++++++--- .../{demo_adapter.py => minimal_adapter.py} | 2 +- backend/app/api/agent/tasks.py | 63 +++++++++++++++++++ backend/app/core/time.py | 23 ++++++- backend/app/models/edge_task.py | 1 + backend/app/schemas/task.py | 34 ++++++++++ backend/app/services/approval_service.py | 2 +- backend/app/services/edge_service.py | 6 +- backend/app/services/task_service.py | 20 +++--- backend/tests/test_task_api.py | 13 ++++ docs/智能化部署agent-demo最小DDL设计.md | 4 +- 智能化部署agent-demo后端项目骨架设计.md | 14 ++--- 智能化部署agent-demo接口定义说明.md | 18 +++--- 智能化部署agent-当前进度总结.md | 50 ++++++++------- 14 files changed, 214 insertions(+), 64 deletions(-) rename backend/app/adapters/software_a/{demo_adapter.py => minimal_adapter.py} (94%) diff --git a/backend/README.md b/backend/README.md index 6ddad79..052bc34 100644 --- a/backend/README.md +++ b/backend/README.md @@ -63,7 +63,7 @@ Current backend includes: `GET /api/demo/approval/requests/{approval_id}` `POST /api/demo/approval/requests/{approval_id}/decision` `GET /api/demo/approval/requests` -4. demo software-a +4. software-a minimal implementation `POST /api/demo/software-a/deploy-tasks` `GET /api/demo/software-a/deploy-tasks/{software_a_task_id}` `POST /api/demo/software-a/permissions/check` @@ -78,16 +78,26 @@ Current execution flow: 1. create task 2. confirm task 3. high-risk task enters approval flow -4. check `software-a demo` permission -5. create `software-a demo` deploy task +4. check `software-a` minimal implementation permission +5. create `software-a` minimal implementation deploy task 6. create default edge verification step 7. edge pulls and reports verification result 8. task reaches `SUCCEEDED` / `FAILED` / `CANCELLED` 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: -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` 3. failed edge report moves task to `FAILED` @@ -97,7 +107,7 @@ Automated tests currently cover: 1. create / confirm / get task 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 5. edge event report 6. task report trace aggregation @@ -109,7 +119,7 @@ Current baseline: `14 passed` Recommended next implementation steps: -1. compute and persist `duration_ms` -2. refine richer result summaries and audit details -3. add more idempotency and rollback tests -4. then continue with local edge-agent bootstrap and second-batch OpenAPI +1. add more idempotency and rollback tests +2. continue enriching audit details and task-level aggregate metrics +3. continue toward local edge-agent bootstrap +4. then continue with second-batch OpenAPI diff --git a/backend/app/adapters/software_a/demo_adapter.py b/backend/app/adapters/software_a/minimal_adapter.py similarity index 94% rename from backend/app/adapters/software_a/demo_adapter.py rename to backend/app/adapters/software_a/minimal_adapter.py index 08c4ce2..47830e7 100644 --- a/backend/app/adapters/software_a/demo_adapter.py +++ b/backend/app/adapters/software_a/minimal_adapter.py @@ -5,7 +5,7 @@ from app.schemas.software_a import CreateDeployTaskRequest from app.services.software_a_service import SoftwareAService -class DemoSoftwareAAdapter(SoftwareAAdapter): +class MinimalSoftwareAAdapter(SoftwareAAdapter): def __init__(self, timezone_name: str) -> None: self.service = SoftwareAService(timezone_name) diff --git a/backend/app/api/agent/tasks.py b/backend/app/api/agent/tasks.py index 35030e5..d78241d 100644 --- a/backend/app/api/agent/tasks.py +++ b/backend/app/api/agent/tasks.py @@ -15,8 +15,10 @@ from app.repositories.approval_repository import ApprovalRepository from app.repositories.audit_repository import AuditRepository from app.repositories.edge_repository import EdgeTaskRepository 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.task import ( + ApprovalSummary, ApprovalTraceItem, AuditTraceItem, CancelTaskRequest, @@ -25,11 +27,14 @@ from app.schemas.task import ( CreateTaskData, CreateTaskRequest, ParsedIntent, + ResultSummaryDetail, + SoftwareAResultSummary, TaskBasic, TaskDetailData, TaskReportData, ToolTraceItem, ToolCallItem, + VerificationResultSummary, VerificationTraceItem, ) 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]}" +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]) def create_task( payload: CreateTaskRequest, @@ -183,6 +236,10 @@ def get_task( edge_tasks = EdgeTaskRepository(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 if edge_tasks: latest_edge_task = edge_tasks[0] @@ -216,6 +273,7 @@ def get_task( ], verification_result=verification_result, summary=task.summary, + result_summary_detail=build_result_summary_detail(task, approval, software_a_detail, edge_tasks), ), timestamp=format_now(settings.default_timezone), ) @@ -243,6 +301,9 @@ def get_task_report( tool_calls = ToolCallRepository(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) + 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 = [] if approval: @@ -282,6 +343,7 @@ def get_task_report( tool_name=item.tool_name, step_status=item.step_status, success=None if item.success is None else bool(item.success), + duration_ms=item.duration_ms, message=item.message, params=json.loads(item.params_json), result_data=json.loads(item.result_data_json), @@ -327,6 +389,7 @@ def get_task_report( tool_trace=tool_trace, verification_trace=verification_trace, result_summary=task.summary, + result_summary_detail=build_result_summary_detail(task, approval, software_a_detail, edge_tasks), audit_trace=audit_trace, ), timestamp=format_now(settings.default_timezone), diff --git a/backend/app/core/time.py b/backend/app/core/time.py index 6a87fc6..3e33e1a 100644 --- a/backend/app/core/time.py +++ b/backend/app/core/time.py @@ -4,6 +4,9 @@ from datetime import UTC, datetime, timedelta, timezone from zoneinfo import ZoneInfo, ZoneInfoNotFoundError +TIME_FORMAT = "%Y-%m-%d %H:%M:%S.%f" + + def resolve_timezone(timezone_name: str): try: return ZoneInfo(timezone_name) @@ -18,4 +21,22 @@ def resolve_timezone(timezone_name: str): def format_now(timezone_name: str) -> str: 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) diff --git a/backend/app/models/edge_task.py b/backend/app/models/edge_task.py index d86cdd1..07d88ea 100644 --- a/backend/app/models/edge_task.py +++ b/backend/app/models/edge_task.py @@ -20,6 +20,7 @@ class EdgeTask(Base): message: Mapped[str | None] = mapped_column(Text, nullable=True) result_data_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) started_at: Mapped[str | None] = mapped_column(Text, nullable=True) finished_at: Mapped[str | None] = mapped_column(Text, nullable=True) diff --git a/backend/app/schemas/task.py b/backend/app/schemas/task.py index 0b6b429..b92a59e 100644 --- a/backend/app/schemas/task.py +++ b/backend/app/schemas/task.py @@ -59,6 +59,37 @@ class VerificationResult(BaseModel): 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): task_id: str task_status: str @@ -70,6 +101,7 @@ class TaskDetailData(BaseModel): tool_calls: list[ToolCallItem] verification_result: VerificationResult | None = None summary: str | None = None + result_summary_detail: ResultSummaryDetail | None = None class TaskBasic(BaseModel): @@ -112,6 +144,7 @@ class VerificationTraceItem(BaseModel): tool_name: str step_status: str success: bool | None = None + duration_ms: int | None = None message: str | None = None params: dict[str, Any] result_data: dict[str, Any] @@ -139,4 +172,5 @@ class TaskReportData(BaseModel): tool_trace: list[ToolTraceItem] verification_trace: list[VerificationTraceItem] result_summary: str | None = None + result_summary_detail: ResultSummaryDetail | None = None audit_trace: list[AuditTraceItem] diff --git a/backend/app/services/approval_service.py b/backend/app/services/approval_service.py index 024a37c..f3e9c20 100644 --- a/backend/app/services/approval_service.py +++ b/backend/app/services/approval_service.py @@ -129,7 +129,7 @@ class ApprovalService: task.approval_status = approval_status task.updated_at = format_now(self.timezone_name) if approval_status == APPROVAL_STATUS_APPROVED: - task.summary = "审批已通过,准备调用 software-a demo 执行。" + task.summary = "审批已通过,准备调用 software-a 最小能力实现执行。" self.task_repository.update(task) from app.services.task_service import TaskService diff --git a/backend/app/services/edge_service.py b/backend/app/services/edge_service.py index f28f3e3..f4cddbd 100644 --- a/backend/app/services/edge_service.py +++ b/backend/app/services/edge_service.py @@ -19,7 +19,7 @@ from app.core.constants import ( TASK_STATUS_SUCCEEDED, 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_task import EdgeTask from app.models.audit_log import AuditLog @@ -103,6 +103,7 @@ class EdgeService: message=None, result_data_json="{}", evidence_json="{}", + duration_ms=None, expire_at=current_time, started_at=None, finished_at=None, @@ -169,6 +170,7 @@ class EdgeService: edge_task.evidence_json = json.dumps(evidence, ensure_ascii=False) edge_task.started_at = started_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) updated_edge_task = self.edge_task_repository.update(edge_task) self._write_tool_call( @@ -247,7 +249,7 @@ class EdgeService: request_payload_json=json.dumps(request_payload, ensure_ascii=False), response_payload_json=json.dumps(response_payload, ensure_ascii=False), success=1 if success else 0, - duration_ms=None, + duration_ms=compute_duration_ms(started_at, finished_at), started_at=started_at, finished_at=finished_at, ) diff --git a/backend/app/services/task_service.py b/backend/app/services/task_service.py index 5f3be99..5394b47 100644 --- a/backend/app/services/task_service.py +++ b/backend/app/services/task_service.py @@ -27,9 +27,9 @@ from app.core.constants import ( ) from app.schemas.approval import ApprovalOperator, ApprovalTarget, CreateApprovalRequest 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.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.tool_call import ToolCall from app.models.task import Task @@ -161,7 +161,7 @@ class TaskService: else: task.task_status = TASK_STATUS_RUNNING task.approval_status = APPROVAL_STATUS_NOT_REQUIRED - task.summary = "任务已确认,准备调用 software-a demo 执行。" + task.summary = "任务已确认,准备调用 software-a 最小能力实现执行。" updated_task = self.repository.update(task) self._write_audit_log( task_id=updated_task.task_id, @@ -191,12 +191,12 @@ class TaskService: def refresh_software_a_status(self, task: Task) -> Task: if not task.software_a_task_id: 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: 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}: 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 = self.repository.update(task) return task @@ -216,7 +216,7 @@ class TaskService: if self.edge_task_repository.list_active_by_task_id(task.task_id): 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.env, task.approval_status, @@ -226,7 +226,7 @@ class TaskService: if task.action_type == "DEPLOY": 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( operator=SoftwareAOperator(user_id="u1001", user_name="alice"), tenant_id=task.tenant_id, @@ -243,9 +243,9 @@ class TaskService: 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.summary = ( - "software-a demo 部署任务已创建,等待边缘验证。" + "software-a 最小能力部署任务已创建,等待边缘验证。" 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( task_id=task.task_id, @@ -363,7 +363,7 @@ class TaskService: request_payload_json=json.dumps(request_payload, ensure_ascii=False), response_payload_json=json.dumps(response_payload, ensure_ascii=False), 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, finished_at=finished_at, ) diff --git a/backend/tests/test_task_api.py b/backend/tests/test_task_api.py index 250fd6a..cde409c 100644 --- a/backend/tests/test_task_api.py +++ b/backend/tests/test_task_api.py @@ -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_status"] == "SUCCEEDED" 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: @@ -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["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 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: @@ -473,6 +482,8 @@ def test_task_fails_when_software_a_deploy_fails() -> None: assert get_response.status_code == 200 assert get_response.json()["data"]["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( "/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.json()["data"]["task_status"] == "FAILED" 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" diff --git a/docs/智能化部署agent-demo最小DDL设计.md b/docs/智能化部署agent-demo最小DDL设计.md index 05d9e0b..7dc25e5 100644 --- a/docs/智能化部署agent-demo最小DDL设计.md +++ b/docs/智能化部署agent-demo最小DDL设计.md @@ -33,7 +33,7 @@ 1. `task` 是主链路核心表。 2. `approval_request` 为高风险任务确认后进入审批预留。 -3. `tool_call` 为后续软件 A demo / edge 验证接入预留。 +3. `tool_call` 为后续软件 A 最小能力实现 / edge 验证接入预留。 4. `audit_log` 为关键动作审计预留。 --- @@ -107,7 +107,7 @@ 用途: -1. 记录软件 A demo、edge 验证和后续工具调用轨迹。 +1. 记录软件 A 最小能力实现、edge 验证和后续工具调用轨迹。 字段: diff --git a/智能化部署agent-demo后端项目骨架设计.md b/智能化部署agent-demo后端项目骨架设计.md index 64a4529..e9fa7e9 100644 --- a/智能化部署agent-demo后端项目骨架设计.md +++ b/智能化部署agent-demo后端项目骨架设计.md @@ -12,7 +12,7 @@ 2. 项目如何分层和分模块。 3. 哪些模块先实现,哪些模块后实现。 4. 数据如何落库。 -5. 如何与软件 A demo、身份 demo、审批 demo、本地 Agent 对接。 +5. 如何与软件 A 最小能力实现、身份 demo、审批 demo、本地 Agent 对接。 ### 1.2 适用范围 @@ -290,7 +290,7 @@ backend/ 职责: -1. 封装软件 A demo 调用。 +1. 封装软件 A 最小能力实现调用。 2. 封装身份 demo 调用。 3. 封装审批 demo 调用。 4. 封装模型网关调用。 @@ -321,7 +321,7 @@ backend/ 职责: 1. 执行异步任务。 -2. 轮询软件 A demo 状态。 +2. 轮询软件 A 最小能力实现状态。 3. 拉起验证流程。 4. 处理长任务和超时任务。 @@ -537,13 +537,13 @@ demo 阶段建议至少建立以下表: 1. `workflows/deploy_workflow.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` 5. `services/verification_service.py` 执行步骤: -1. 调用软件 A demo 创建部署任务。 +1. 调用软件 A 最小能力实现创建部署任务。 2. 轮询部署状态。 3. 部署成功后生成验证步骤。 4. 下发到本地 Agent。 @@ -663,7 +663,7 @@ demo 阶段正式确认: ## 11.2 第二批完成 -1. 软件 A demo adapter。 +1. 软件 A 最小能力实现 adapter。 2. 部署工作流。 3. 审批 demo adapter。 4. 身份 demo adapter。 @@ -710,7 +710,7 @@ demo 阶段正式确认: 至少覆盖: -1. 软件 A demo 联调。 +1. 软件 A 最小能力实现联调。 2. 身份 demo 联调。 3. 审批 demo 联调。 4. 本地 Agent 联调。 diff --git a/智能化部署agent-demo接口定义说明.md b/智能化部署agent-demo接口定义说明.md index 92805f0..79a684e 100644 --- a/智能化部署agent-demo接口定义说明.md +++ b/智能化部署agent-demo接口定义说明.md @@ -10,7 +10,7 @@ 1. Agent 对外任务接口。 2. 云端与本地 Agent 的交互接口。 -3. 软件 A demo 接口。 +3. 软件 A 最小能力实现接口。 4. 身份 demo 接口。 5. 审批 demo 接口。 @@ -40,7 +40,7 @@ 说明: -1. 软件 A demo、身份 demo、审批 demo、edge 接口继续保留在本文档中作为后续实现输入。 +1. 软件 A 最小能力实现、身份 demo、审批 demo、edge 接口继续保留在本文档中作为后续实现输入。 2. 首批代码骨架只要求先打通以上三条主接口。 3. 后续扩展 OpenAPI 时,优先保持当前对象模型和错误码不变。 @@ -442,11 +442,11 @@ X-Signature: --- -## 5. 软件 A Demo 接口 +## 5. 软件 A 最小能力实现接口 ## 5.1 设计说明 -软件 A demo 版本用于支撑 MVP 闭环,其接口语义需尽量贴近未来真实软件 A 的标准能力。 +软件 A 最小能力实现用于支撑 MVP 闭环,其接口语义需尽量贴近未来真实软件 A 的标准能力。 建议 base path: @@ -891,9 +891,9 @@ X-Signature: 2. Agent 完成解析并返回 `task_id` 和结构化结果。 3. 用户调用确认接口。 4. Agent 调用身份 demo 获取操作者信息和权限。 -5. Agent 调用软件 A demo 权限校验。 -6. Agent 调用软件 A demo 创建部署任务。 -7. Agent 轮询软件 A demo 查询状态。 +5. Agent 调用软件 A 最小能力实现权限校验。 +6. Agent 调用软件 A 最小能力实现创建部署任务。 +7. Agent 轮询软件 A 最小能力实现查询状态。 8. Agent 调用 Edge 接口执行本地验证。 9. Agent 汇总结果,更新任务状态并生成报告。 @@ -911,7 +911,7 @@ X-Signature: ## 10. demo 与正式系统的替换原则 1. Agent 上层只依赖统一语义接口,不直接依赖 demo 接口字段差异。 -2. 软件 A demo、身份 demo、审批 demo 均应封装在适配层后面。 +2. 软件 A 最小能力实现、身份 demo、审批 demo 均应封装在适配层后面。 3. 后续替换真实系统时,优先保持上层对象模型不变。 4. 如真实系统能力不足,应在适配层内做降级,而不是修改编排主链路。 @@ -921,7 +921,7 @@ X-Signature: 1. 先完成通用响应格式、错误码和枚举定义。 2. 再完成 Agent 对外任务接口。 -3. 再完成软件 A demo 接口。 +3. 再完成软件 A 最小能力实现接口。 4. 再完成身份 demo 和审批 demo。 5. 最后完成本地 Agent 拉取任务与结果回传接口。 diff --git a/智能化部署agent-当前进度总结.md b/智能化部署agent-当前进度总结.md index 47bb961..ab5b976 100644 --- a/智能化部署agent-当前进度总结.md +++ b/智能化部署agent-当前进度总结.md @@ -25,7 +25,7 @@ 用于描述系统架构、模块分层、数据模型、接口建议、安全设计和实施约束。 3. `智能化部署agent-demo接口定义说明.md` - 用于描述 demo 阶段的接口协议、统一响应格式、状态枚举、Agent 接口、软件 A demo 接口、身份 demo 接口、审批 demo 接口。 + 用于描述 demo 阶段的接口协议、统一响应格式、状态枚举、Agent 接口、软件 A 最小能力实现接口、身份 demo 接口、审批 demo 接口。 4. `智能化部署agent-demo后端项目骨架设计.md` 用于描述 demo 后端的推荐技术栈、项目结构、模块职责、数据库表建议、代码落点和开发顺序。 @@ -86,7 +86,7 @@ demo 接口定义文档已覆盖: 1. Agent 对外任务接口。 2. 云端与本地 Agent 交互接口。 -3. 软件 A demo 接口。 +3. 软件 A 最小能力实现接口。 4. 身份 demo 接口。 5. 审批 demo 接口。 6. 内部对象结构。 @@ -123,17 +123,19 @@ demo 接口定义文档已覆盖: 3. 已实现 `task`、`approval_request`、`tool_call`、`audit_log` 对应的最小模型和数据库初始化逻辑。 4. 已打通三条主接口: `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. 已将高风险任务确认后的审批创建流程接入后端主链路。 7. 已实现最小 `edge` 心跳、拉取任务、回传结果接口。 8. 已将默认验证任务接入 edge 调度主链路。 -9. 已将 `software-a demo` 部署任务创建接入主执行链。 +9. 已将 `software-a` 最小能力部署任务创建接入主执行链。 10. 已将 `tool_call` 和 `audit_log` 接入主链路关键动作。 11. 已实现任务报告接口,可返回审批、工具、验证、审计轨迹。 12. 已实现任务取消接口,并将 `request_id`、`operator` 维度写入关键审计和工具调用记录。 13. 已补充自动化测试,并基于内存 SQLite 完成首轮通过验证。 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 当前代码可运行范围 @@ -143,13 +145,17 @@ demo 接口定义文档已覆盖: 2. 高风险任务确认后自动创建审批单。 3. 审批通过后进入执行链,审批驳回后进入取消态。 4. 执行链包含: - software-a 权限校验 -> software-a demo 部署任务创建 -> edge 默认验证任务创建 -> edge 拉取 -> edge 回传。 + software-a 权限校验 -> software-a 最小能力部署任务创建 -> edge 默认验证任务创建 -> edge 拉取 -> edge 回传。 5. 任务详情接口可返回: 当前状态、software-a 状态、工具调用摘要、验证结果摘要。 6. 任务报告接口可返回: `task_basic`、`intent_snapshot`、`approval_trace`、`tool_trace`、`verification_trace`、`result_summary`、`audit_trace` 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. 自然语言发起任务。 2. Agent 解析意图并做结构化任务生成。 3. 策略层做风险判断。 -4. 调用软件 A demo 执行部署或控制动作。 +4. 调用软件 A 最小能力实现执行部署或控制动作。 5. 调用本地 Agent 做验证。 6. 汇总结果,生成报告和审计。 @@ -239,7 +245,7 @@ demo 接口定义文档已覆盖: `CREATED` -> `PENDING_CONFIRM` -> `RUNNING` -> `VERIFYING` -> `SUCCEEDED` / `FAILED` / `CANCELLED` 2. 高风险任务路径为: `PENDING_CONFIRM` -> `PENDING_APPROVAL` -> `RUNNING` -3. `software-a demo` 当前在任务详情查询时会同步刷新状态,因此: +3. `software-a` 最小能力实现当前在任务详情查询时会同步刷新状态,因此: 确认接口返回的 `software_a_task_status` 可能是 `RUNNING`,而后续查询任务详情时可能已变为 `SUCCEEDED` 4. 当前 demo 中的 operator 默认使用: `alice(u1001)` 作为任务发起和执行方,`bob(u2001)` 作为审批人 @@ -251,7 +257,7 @@ demo 接口定义文档已覆盖: 8. 当前已补上的状态约束包括: 重复确认拦截、重复执行拦截、审批决策前必须仍处于 `PENDING_APPROVAL`、edge 重复回传拦截、非 `RUNNING` 任务不再下发 edge 执行。 9. 当前 demo 已支持可控失败模拟: - 若 `app_code` 或 `version` 包含 `fail`,则 `software-a demo` 会返回失败任务,用于联调失败分支。 + 若 `app_code` 或 `version` 包含 `fail`,则 `software-a` 最小能力实现会返回失败任务,用于联调失败分支。 --- @@ -262,7 +268,7 @@ demo 接口定义文档已覆盖: 1. 本地 `edge-agent` 初始化代码与打包脚本。 2. 文件型 SQLite / PostgreSQL 实库运行验证。 3. 身份 demo / 审批 demo 与任务主链路的权限、审批决策联动细化。 -4. `duration_ms` 等执行指标的真实计算与回填。 +4. 任务级聚合指标仍未完成,如总耗时、审批耗时、等待耗时。 5. 更真实的验证插件实现。 6. 部署脚本和运行脚本完善。 7. OpenAPI 扩展到第二批接口。 @@ -284,12 +290,12 @@ demo 接口定义文档已覆盖: 当前不是继续补基础文档,而是继续补强现有可运行链路。优先级建议收敛为: -1. 回填执行指标: - 重点补 `duration_ms`、更完整的执行结果摘要与审计信息。 -2. 增补失败路径与幂等性测试: +1. 增补失败路径与幂等性测试: 重点补重复请求、重复回传、异常回滚等场景。 -3. 继续丰富结果摘要与审计细节: - 让失败原因在详情和报告里更直观可见。 +2. 继续丰富审计细节与任务级聚合指标: + 让任务级总耗时、审批耗时、等待耗时可直观看到。 +3. 再补更多执行指标: + 如任务级聚合耗时、审批耗时、等待耗时。 4. 然后再继续: 本地 `edge-agent` 骨架、第二批 OpenAPI、更多联调能力。 @@ -303,9 +309,9 @@ demo 接口定义文档已覆盖: 按当前进度,建议后续直接按以下顺序推进: -1. 计算并持久化 `duration_ms`。 -2. 增补状态冲突、失败回滚、重复上报等测试。 -3. 丰富结果摘要与失败原因呈现。 +1. 增补状态冲突、失败回滚、重复上报等测试。 +2. 再补更多任务级执行指标。 +3. 继续增强审计细节。 4. 再进入本地 `edge-agent` 初始化代码和第二批 OpenAPI。 当前更推荐: @@ -329,9 +335,9 @@ demo 接口定义文档已覆盖: 下一步推荐顺序: -1. 计算并回填 `duration_ms`。 -2. 再补失败路径和幂等性测试。 -3. 再补结果摘要和失败原因展示。 +1. 再补失败路径和幂等性测试。 +2. 再补任务级执行指标。 +3. 再补审计细节和聚合摘要。 4. 再补本地 Agent 初始化代码或第二批 OpenAPI。 ### 7.2 如果上下文快满,有什么影响 @@ -364,4 +370,4 @@ set DATABASE_URL=sqlite:///:memory: 当前已经完成从"写文档"切换到"写 demo 代码"的第一步,下一步进入: -**duration_ms 回填 -> 失败结果呈现增强 -> 本地 Agent 与联调能力继续补齐** +**更多执行指标 -> 审计细节增强 -> 本地 Agent 与联调能力继续补齐**