From 336277627bdfc5452cf0d070299a383722e9324b Mon Sep 17 00:00:00 2001 From: axb Date: Thu, 30 Jul 2026 14:50:12 +0800 Subject: [PATCH] fix(wework): roll back cloud automations --- ...add_runtime_automation_execution_fields.py | 84 ---- backend/app/api/api.py | 6 - .../app/api/endpoints/runtime_automations.py | 411 ------------------ backend/app/api/ws/device_namespace.py | 88 ---- backend/app/models/subscription.py | 10 - backend/app/schemas/runtime_automation.py | 115 ----- backend/app/schemas/subscription.py | 6 - backend/app/services/subscription/service.py | 21 - .../services/subscription/state_machine.py | 6 - backend/app/tasks/subscription_tasks.py | 175 -------- .../api/endpoints/test_runtime_automations.py | 67 --- docs/en/user-guide/wework-automations.md | 15 +- docs/zh/user-guide/wework-automations.md | 12 +- wework/e2e/desktop/task-flow.e2e.mjs | 12 + wework/src/api/automations.test.ts | 53 --- wework/src/api/automations.ts | 41 -- wework/src/api/backend/backendServices.ts | 3 - wework/src/api/hybrid/hybridServices.ts | 112 +---- .../automations/AutomationDetailWorkspace.tsx | 4 +- .../features/workbench/workbenchServices.ts | 24 +- wework/src/pages/AutomationsPage.tsx | 21 +- 21 files changed, 48 insertions(+), 1238 deletions(-) delete mode 100644 backend/alembic/versions/20260729_d9e4f5a6b7c8_add_runtime_automation_execution_fields.py delete mode 100644 backend/app/api/endpoints/runtime_automations.py delete mode 100644 backend/app/schemas/runtime_automation.py delete mode 100644 backend/tests/api/endpoints/test_runtime_automations.py delete mode 100644 wework/src/api/automations.test.ts delete mode 100644 wework/src/api/automations.ts diff --git a/backend/alembic/versions/20260729_d9e4f5a6b7c8_add_runtime_automation_execution_fields.py b/backend/alembic/versions/20260729_d9e4f5a6b7c8_add_runtime_automation_execution_fields.py deleted file mode 100644 index d79ff15d634..00000000000 --- a/backend/alembic/versions/20260729_d9e4f5a6b7c8_add_runtime_automation_execution_fields.py +++ /dev/null @@ -1,84 +0,0 @@ -"""Add runtime automation execution fields. - -Revision ID: d9e4f5a6b7c8 -Revises: c8d2e3f4a5b6 -""" - -from typing import Sequence, Union - -import sqlalchemy as sa - -from alembic import op - -revision: str = "d9e4f5a6b7c8" -down_revision: Union[str, None] = "c8d2e3f4a5b6" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - op.add_column( - "background_executions", - sa.Column("scheduled_for", sa.DateTime(), nullable=True), - ) - op.add_column( - "background_executions", - sa.Column( - "source_surface", - sa.String(length=50), - server_default="wegent", - nullable=False, - ), - ) - op.add_column( - "background_executions", - sa.Column("runtime_device_id", sa.String(length=255), nullable=True), - ) - op.add_column( - "background_executions", - sa.Column("runtime_task_id", sa.String(length=255), nullable=True), - ) - op.create_index( - "ix_background_executions_scheduled_for", - "background_executions", - ["scheduled_for"], - ) - op.create_index( - "ix_background_executions_runtime_device_id", - "background_executions", - ["runtime_device_id"], - ) - op.create_index( - "ix_background_executions_runtime_task_id", - "background_executions", - ["runtime_task_id"], - ) - op.create_unique_constraint( - "uq_background_execution_subscription_scheduled_for", - "background_executions", - ["subscription_id", "scheduled_for"], - ) - - -def downgrade() -> None: - op.drop_constraint( - "uq_background_execution_subscription_scheduled_for", - "background_executions", - type_="unique", - ) - op.drop_index( - "ix_background_executions_runtime_task_id", - table_name="background_executions", - ) - op.drop_index( - "ix_background_executions_runtime_device_id", - table_name="background_executions", - ) - op.drop_index( - "ix_background_executions_scheduled_for", - table_name="background_executions", - ) - op.drop_column("background_executions", "runtime_task_id") - op.drop_column("background_executions", "runtime_device_id") - op.drop_column("background_executions", "source_surface") - op.drop_column("background_executions", "scheduled_for") diff --git a/backend/app/api/api.py b/backend/app/api/api.py index d5684178208..cc02e99d020 100644 --- a/backend/app/api/api.py +++ b/backend/app/api/api.py @@ -37,7 +37,6 @@ quota, remote_devices, repository, - runtime_automations, runtime_work, share, sites, @@ -289,11 +288,6 @@ api_router.include_router( runtime_work.router, prefix="/runtime-work", tags=["runtime-work"] ) -api_router.include_router( - runtime_automations.router, - prefix="/runtime-automations", - tags=["runtime-automations"], -) api_router.include_router(utils.router, prefix="/utils", tags=["utils"]) api_router.include_router( diff --git a/backend/app/api/endpoints/runtime_automations.py b/backend/app/api/endpoints/runtime_automations.py deleted file mode 100644 index 6fd64346235..00000000000 --- a/backend/app/api/endpoints/runtime_automations.py +++ /dev/null @@ -1,411 +0,0 @@ -"""Wework cloud runtime automation API.""" - -from datetime import datetime, timezone -from zoneinfo import ZoneInfo, ZoneInfoNotFoundError - -from fastapi import APIRouter, Depends, HTTPException, Query, status -from sqlalchemy.orm import Session -from sqlalchemy.orm.attributes import flag_modified - -from app.api.dependencies import get_db -from app.core import security -from app.models.kind import Kind -from app.models.subscription import BackgroundExecution -from app.models.user import User -from app.schemas.runtime_automation import ( - RuntimeAutomationListResponse, - RuntimeAutomationMutation, - RuntimeAutomationResponse, - RuntimeAutomationRunListResponse, - RuntimeAutomationRunResponse, -) -from app.schemas.subscription import ( - SubscriptionCreate, - SubscriptionExecutionTarget, - SubscriptionExecutionTargetType, - SubscriptionTaskType, - SubscriptionTriggerType, - SubscriptionUpdate, - SubscriptionVisibility, -) -from app.services.subscription import subscription_service - -router = APIRouter() -RUNTIME_AUTOMATION_MARKER = "runtime_automation" - - -@router.get("", response_model=RuntimeAutomationListResponse) -def list_runtime_automations( - db: Session = Depends(get_db), - current_user: User = Depends(security.get_current_user), -): - rows = ( - db.query(Kind) - .filter( - Kind.user_id == current_user.id, - Kind.kind == "Subscription", - Kind.is_active == True, - ) - .order_by(Kind.updated_at.desc()) - .all() - ) - return RuntimeAutomationListResponse( - items=[ - _automation_response(row) - for row in rows - if _runtime_config(row) is not None - ] - ) - - -@router.get("/runs", response_model=RuntimeAutomationRunListResponse) -def list_runtime_automation_runs( - automation_id: str | None = Query(None), - db: Session = Depends(get_db), - current_user: User = Depends(security.get_current_user), -): - query = db.query(BackgroundExecution).filter( - BackgroundExecution.user_id == current_user.id, - BackgroundExecution.source_surface == "wework", - ) - if automation_id: - subscription_id = _parse_automation_id(automation_id) - _owned_runtime_automation(db, current_user.id, subscription_id) - query = query.filter(BackgroundExecution.subscription_id == subscription_id) - executions = query.order_by(BackgroundExecution.created_at.desc()).limit(200).all() - return RuntimeAutomationRunListResponse( - items=[_run_response(execution) for execution in executions] - ) - - -@router.post( - "", - response_model=dict, - status_code=status.HTTP_201_CREATED, -) -def create_runtime_automation( - mutation: RuntimeAutomationMutation, - db: Session = Depends(get_db), - current_user: User = Depends(security.get_current_user), -): - _validate_timezone(mutation.timezone) - trigger_type, trigger_config = _subscription_schedule(mutation) - subscription = subscription_service.create_subscription( - db=db, - user_id=current_user.id, - subscription_in=SubscriptionCreate( - display_name=mutation.name, - description=mutation.description, - task_type=SubscriptionTaskType.EXECUTION, - visibility=SubscriptionVisibility.PRIVATE, - trigger_type=trigger_type, - trigger_config=trigger_config, - team_id=mutation.task_request.team_id, - prompt_template=mutation.prompt, - enabled=mutation.enabled, - execution_target=SubscriptionExecutionTarget( - type=SubscriptionExecutionTargetType.CLOUD, - device_id=mutation.task_request.device_id, - ), - ), - ) - row = _owned_runtime_automation( - db, - current_user.id, - subscription.id, - require_marker=False, - ) - _write_runtime_config(row, mutation) - db.commit() - db.refresh(row) - return {"automation": _automation_response(row)} - - -@router.get("/{automation_id}", response_model=dict) -def get_runtime_automation( - automation_id: str, - db: Session = Depends(get_db), - current_user: User = Depends(security.get_current_user), -): - row = _owned_runtime_automation( - db, current_user.id, _parse_automation_id(automation_id) - ) - return {"automation": _automation_response(row)} - - -@router.put("/{automation_id}", response_model=dict) -def update_runtime_automation( - automation_id: str, - mutation: RuntimeAutomationMutation, - db: Session = Depends(get_db), - current_user: User = Depends(security.get_current_user), -): - _validate_timezone(mutation.timezone) - row = _owned_runtime_automation( - db, current_user.id, _parse_automation_id(automation_id) - ) - config = _runtime_config(row) - if mutation.version is not None and mutation.version != int( - config.get("version", 1) - ): - raise HTTPException(status_code=409, detail="Automation version conflict") - trigger_type, trigger_config = _subscription_schedule(mutation) - subscription_service.update_subscription( - db=db, - subscription_id=row.id, - user_id=current_user.id, - subscription_in=SubscriptionUpdate( - display_name=mutation.name, - description=mutation.description, - trigger_type=trigger_type, - trigger_config=trigger_config, - team_id=mutation.task_request.team_id, - prompt_template=mutation.prompt, - enabled=mutation.enabled, - execution_target=SubscriptionExecutionTarget( - type=SubscriptionExecutionTargetType.CLOUD, - device_id=mutation.task_request.device_id, - ), - ), - ) - db.refresh(row) - _write_runtime_config(row, mutation, version=int(config.get("version", 1)) + 1) - db.commit() - db.refresh(row) - return {"automation": _automation_response(row)} - - -@router.delete("/{automation_id}", response_model=dict) -def delete_runtime_automation( - automation_id: str, - db: Session = Depends(get_db), - current_user: User = Depends(security.get_current_user), -): - row = _owned_runtime_automation( - db, current_user.id, _parse_automation_id(automation_id) - ) - row.is_active = False - internal = row.json.setdefault("_internal", {}) - internal["enabled"] = False - row.json["_internal"] = internal - flag_modified(row, "json") - db.commit() - return {"deleted": True} - - -@router.post("/{automation_id}/toggle", response_model=dict) -def toggle_runtime_automation( - automation_id: str, - payload: dict, - db: Session = Depends(get_db), - current_user: User = Depends(security.get_current_user), -): - enabled = payload.get("enabled") - if not isinstance(enabled, bool): - raise HTTPException(status_code=400, detail="enabled is required") - row = _owned_runtime_automation( - db, current_user.id, _parse_automation_id(automation_id) - ) - subscription_service.toggle_subscription( - db=db, - subscription_id=row.id, - user_id=current_user.id, - enabled=enabled, - ) - db.refresh(row) - return {"automation": _automation_response(row)} - - -@router.post("/{automation_id}/run", response_model=dict) -def run_runtime_automation_now( - automation_id: str, - db: Session = Depends(get_db), - current_user: User = Depends(security.get_current_user), -): - row = _owned_runtime_automation( - db, current_user.id, _parse_automation_id(automation_id) - ) - has_active_run = ( - db.query(BackgroundExecution.id) - .filter( - BackgroundExecution.subscription_id == row.id, - BackgroundExecution.status.in_(["PENDING", "RUNNING"]), - ) - .first() - is not None - ) - execution = subscription_service.create_execution( - db, - subscription=row, - user_id=current_user.id, - trigger_type="manual", - trigger_reason="Manual Wework automation run", - ) - model = ( - db.query(BackgroundExecution) - .filter(BackgroundExecution.id == execution.id) - .first() - ) - model.source_surface = "wework" - model.runtime_device_id = _runtime_config(row)["task_request"].get("deviceId") - if has_active_run: - model.status = "SKIPPED" - model.error_message = "Skipped because another automation run is active" - model.completed_at = datetime.now(timezone.utc).replace(tzinfo=None) - db.commit() - if not has_active_run: - subscription_service.dispatch_background_execution(row, execution) - db.refresh(model) - return {"run": _run_response(model)} - - -def _owned_runtime_automation( - db: Session, - user_id: int, - subscription_id: int, - *, - require_marker: bool = True, -) -> Kind: - row = ( - db.query(Kind) - .filter( - Kind.id == subscription_id, - Kind.user_id == user_id, - Kind.kind == "Subscription", - Kind.is_active == True, - ) - .first() - ) - if not row or (require_marker and _runtime_config(row) is None): - raise HTTPException(status_code=404, detail="Automation not found") - return row - - -def _write_runtime_config( - row: Kind, - mutation: RuntimeAutomationMutation, - *, - version: int = 1, -) -> None: - internal = row.json.setdefault("_internal", {}) - internal[RUNTIME_AUTOMATION_MARKER] = { - "version": version, - "timezone": mutation.timezone, - "conversation_mode": mutation.conversation_mode, - "notification_policy": mutation.notification_policy, - "task_request": mutation.task_request.model_dump( - mode="json", - by_alias=True, - exclude_none=True, - ), - "continuation_payload": mutation.continuation_payload, - } - internal["source_surface"] = "wework" - row.json["_internal"] = internal - flag_modified(row, "json") - - -def _runtime_config(row: Kind) -> dict | None: - value = row.json.get("_internal", {}).get(RUNTIME_AUTOMATION_MARKER) - return value if isinstance(value, dict) else None - - -def _automation_response(row: Kind) -> RuntimeAutomationResponse: - internal = row.json["_internal"] - config = _runtime_config(row) or {} - spec = row.json.get("spec", {}) - return RuntimeAutomationResponse( - id=f"cloud:{row.id}", - version=int(config.get("version", 1)), - name=spec.get("displayName") or row.name, - description=spec.get("description") or "", - prompt=spec.get("promptTemplate") or "", - schedule=_automation_schedule(internal, spec), - timezone=config.get("timezone", "UTC"), - enabled=bool(internal.get("enabled", True)), - conversationMode=config.get("conversation_mode", "independent"), - notificationPolicy=config.get("notification_policy", "all_runs"), - taskRequest=config.get("task_request", {}), - continuationPayload=config.get("continuation_payload"), - nextRunAt=_parse_datetime(internal.get("next_execution_time")), - lastRunAt=_parse_datetime(internal.get("last_execution_time")), - createdAt=row.created_at, - updatedAt=row.updated_at, - ) - - -def _run_response(execution: BackgroundExecution) -> RuntimeAutomationRunResponse: - status_map = { - "completed": "succeeded", - "completed_silent": "succeeded", - } - normalized_status = execution.status.lower() - return RuntimeAutomationRunResponse( - id=f"cloud-run:{execution.id}", - automationId=f"cloud:{execution.subscription_id}", - scheduledFor=execution.scheduled_for or execution.created_at, - trigger=execution.trigger_type, - status=status_map.get(normalized_status, normalized_status), - taskId=execution.runtime_task_id, - deviceId=execution.runtime_device_id, - error=execution.error_message or None, - createdAt=execution.created_at, - updatedAt=execution.updated_at, - ) - - -def _subscription_schedule( - mutation: RuntimeAutomationMutation, -) -> tuple[SubscriptionTriggerType, dict]: - schedule = mutation.schedule - if schedule.type == "cron": - return SubscriptionTriggerType.CRON, { - "expression": schedule.expression, - "timezone": mutation.timezone, - } - if schedule.type == "interval": - return SubscriptionTriggerType.INTERVAL, { - "value": schedule.value, - "unit": schedule.unit, - } - return SubscriptionTriggerType.ONE_TIME, { - "execute_at": schedule.execute_at.isoformat(), - } - - -def _automation_schedule(internal: dict, spec: dict) -> dict: - trigger_type = internal.get("trigger_type") - trigger = spec.get("trigger", {}) - config = trigger.get(trigger_type, {}) - if trigger_type == "one_time": - return { - "type": "one_time", - "executeAt": config.get("execute_at") or config.get("executeAt"), - } - return {"type": trigger_type, **config} - - -def _parse_automation_id(value: str) -> int: - normalized = value.removeprefix("cloud:") - try: - return int(normalized) - except ValueError as exc: - raise HTTPException(status_code=404, detail="Automation not found") from exc - - -def _parse_datetime(value: str | datetime | None) -> datetime | None: - if isinstance(value, datetime): - return value - if not value: - return None - try: - return datetime.fromisoformat(value) - except (TypeError, ValueError): - return None - - -def _validate_timezone(value: str) -> None: - try: - ZoneInfo(value) - except ZoneInfoNotFoundError as exc: - raise HTTPException(status_code=400, detail="Invalid IANA timezone") from exc diff --git a/backend/app/api/ws/device_namespace.py b/backend/app/api/ws/device_namespace.py index 4a70d5a910e..9cbc4cecad5 100644 --- a/backend/app/api/ws/device_namespace.py +++ b/backend/app/api/ws/device_namespace.py @@ -55,7 +55,6 @@ from app.core.events import TaskCompletedEvent, get_event_bus from app.core.socketio import get_sio from app.db.session import SessionLocal -from app.models.subscription import BackgroundExecution from app.models.subtask import SubtaskStatus from app.models.user import User from app.schemas.device import ( @@ -577,70 +576,6 @@ def response_api_payload(*, data: dict[str, Any], device_id: str) -> dict[str, A return payload -def _complete_runtime_automation_execution_sync( - user_id: int, - device_id: str, - local_task_id: str, - event_name: str, - event_payload: dict[str, Any], -) -> dict[str, Any] | None: - if event_name not in { - "response.completed", - "response.failed", - "response.incomplete", - }: - return None - with get_db_session() as db: - execution = ( - db.query(BackgroundExecution) - .filter( - BackgroundExecution.user_id == user_id, - BackgroundExecution.source_surface == "wework", - BackgroundExecution.runtime_device_id == device_id, - BackgroundExecution.runtime_task_id == local_task_id, - BackgroundExecution.status.in_(["PENDING", "RUNNING"]), - ) - .order_by(BackgroundExecution.created_at.desc()) - .first() - ) - if execution is None: - return None - now = datetime.utcnow() - if event_name == "response.completed": - data = event_payload.get("data") - stop_reason = data.get("stop_reason") if isinstance(data, dict) else None - if stop_reason == "waiting_for_user_input": - execution.status = "NEEDS_ATTENTION" - execution.error_message = "Automation requires user input" - else: - execution.status = "COMPLETED" - if isinstance(data, dict): - value = data.get("value") - if isinstance(value, str): - execution.result_summary = value[:4000] - elif event_name == "response.incomplete": - execution.status = "CANCELLED" - else: - execution.status = "FAILED" - data = event_payload.get("data") - error = data.get("error") if isinstance(data, dict) else None - if isinstance(error, dict): - execution.error_message = str( - error.get("message") or "Automation failed" - ) - else: - execution.error_message = "Automation failed" - execution.completed_at = now - execution.updated_at = now - db.commit() - return { - "id": execution.id, - "subscription_id": execution.subscription_id, - "status": execution.status, - "runtime_task_id": execution.runtime_task_id, - } - - async def emit_chat_user_event( *, event_name: str, payload: dict[str, Any], user_id: int ) -> None: @@ -1882,29 +1817,6 @@ async def on_runtime_event(self, sid: str, data: dict) -> dict: else: payload["payload"] = {"deviceId": device_id, "device_id": device_id} - event_name = str(payload.get("event") or "") - runtime_payload = payload.get("payload") - local_task_id = ( - str(runtime_payload.get("taskId") or runtime_payload.get("task_id") or "") - if isinstance(runtime_payload, dict) - else "" - ) - if event_name and local_task_id: - automation_update = await run_sync_in_executor( - _complete_runtime_automation_execution_sync, - int(user_id), - device_id, - local_task_id, - event_name, - runtime_payload, - ) - if automation_update: - await emit_chat_user_event( - event_name=ServerEvents.BACKGROUND_EXECUTION_UPDATE, - payload=automation_update, - user_id=int(user_id), - ) - await get_sio().emit( WEWORK_RUNTIME_EVENT, payload, diff --git a/backend/app/models/subscription.py b/backend/app/models/subscription.py index 007c571e88b..946d63d9d13 100644 --- a/backend/app/models/subscription.py +++ b/backend/app/models/subscription.py @@ -18,7 +18,6 @@ Integer, String, Text, - UniqueConstraint, ) from app.db.base import Base @@ -68,10 +67,6 @@ class BackgroundExecution(Base): trigger_reason = Column( String(500), nullable=False, default="" ) # Human-readable reason - scheduled_for = Column(DateTime, nullable=True, index=True) - source_surface = Column(String(50), nullable=False, default="wegent") - runtime_device_id = Column(String(255), nullable=True, index=True) - runtime_task_id = Column(String(255), nullable=True, index=True) # Resolved prompt (with variables substituted) prompt = Column(Text, nullable=False) @@ -104,11 +99,6 @@ class BackgroundExecution(Base): Index("ix_bg_exec_subscription_created", "subscription_id", "created_at"), # Index for status filtering Index("ix_bg_exec_user_status", "user_id", "status"), - UniqueConstraint( - "subscription_id", - "scheduled_for", - name="uq_background_execution_subscription_scheduled_for", - ), { "sqlite_autoincrement": True, "mysql_engine": "InnoDB", diff --git a/backend/app/schemas/runtime_automation.py b/backend/app/schemas/runtime_automation.py deleted file mode 100644 index 9aa4dfc0678..00000000000 --- a/backend/app/schemas/runtime_automation.py +++ /dev/null @@ -1,115 +0,0 @@ -"""Schemas for Wework local/cloud runtime automations.""" - -from datetime import datetime -from typing import Any, Dict, Literal, Optional, Union - -from pydantic import BaseModel, ConfigDict, Field, model_validator - -from app.schemas.runtime_work import RuntimeTaskCreateRequest - - -class CronAutomationSchedule(BaseModel): - type: Literal["cron"] - expression: str = Field(..., min_length=5) - - -class IntervalAutomationSchedule(BaseModel): - type: Literal["interval"] - value: int = Field(..., ge=1) - unit: Literal["minutes", "hours", "days"] - - -class OneTimeAutomationSchedule(BaseModel): - model_config = ConfigDict(populate_by_name=True) - - type: Literal["one_time"] - execute_at: datetime = Field(..., alias="executeAt") - - -AutomationSchedule = Union[ - CronAutomationSchedule, - IntervalAutomationSchedule, - OneTimeAutomationSchedule, -] - - -class RuntimeAutomationMutation(BaseModel): - model_config = ConfigDict(populate_by_name=True) - - id: Optional[str] = None - version: Optional[int] = None - source: Literal["cloud"] = "cloud" - name: str = Field(..., min_length=1, max_length=100) - description: str = "" - prompt: str = Field(..., min_length=1) - schedule: AutomationSchedule = Field(..., discriminator="type") - timezone: str = "UTC" - enabled: bool = True - conversation_mode: Literal["independent", "continue_thread"] = Field( - "independent", - alias="conversationMode", - ) - notification_policy: Literal["all_runs", "attention_only", "never"] = Field( - "all_runs", - alias="notificationPolicy", - ) - task_request: RuntimeTaskCreateRequest = Field(..., alias="taskRequest") - continuation_payload: Optional[Dict[str, Any]] = Field( - None, - alias="continuationPayload", - ) - continuation_payload: Optional[Dict[str, Any]] = Field( - None, - alias="continuationPayload", - ) - - @model_validator(mode="after") - def require_cloud_device(self) -> "RuntimeAutomationMutation": - if not self.task_request.device_id: - raise ValueError("taskRequest.deviceId is required for cloud automations") - return self - - -class RuntimeAutomationResponse(BaseModel): - model_config = ConfigDict(populate_by_name=True) - - id: str - version: int - source: Literal["cloud"] = "cloud" - name: str - description: str - prompt: str - schedule: Dict[str, Any] - timezone: str - enabled: bool - conversation_mode: str = Field(..., alias="conversationMode") - notification_policy: str = Field(..., alias="notificationPolicy") - task_request: RuntimeTaskCreateRequest = Field(..., alias="taskRequest") - next_run_at: Optional[datetime] = Field(None, alias="nextRunAt") - last_run_at: Optional[datetime] = Field(None, alias="lastRunAt") - created_at: datetime = Field(..., alias="createdAt") - updated_at: datetime = Field(..., alias="updatedAt") - - -class RuntimeAutomationListResponse(BaseModel): - items: list[RuntimeAutomationResponse] - - -class RuntimeAutomationRunResponse(BaseModel): - model_config = ConfigDict(populate_by_name=True) - - id: str - automation_id: str = Field(..., alias="automationId") - source: Literal["cloud"] = "cloud" - scheduled_for: datetime = Field(..., alias="scheduledFor") - trigger: str - status: str - task_id: Optional[str] = Field(None, alias="taskId") - device_id: Optional[str] = Field(None, alias="deviceId") - error: Optional[str] = None - created_at: datetime = Field(..., alias="createdAt") - updated_at: datetime = Field(..., alias="updatedAt") - - -class RuntimeAutomationRunListResponse(BaseModel): - items: list[RuntimeAutomationRunResponse] diff --git a/backend/app/schemas/subscription.py b/backend/app/schemas/subscription.py index 61894343316..6e2a10776b2 100644 --- a/backend/app/schemas/subscription.py +++ b/backend/app/schemas/subscription.py @@ -86,8 +86,6 @@ class BackgroundExecutionStatus(str, Enum): FAILED = "FAILED" RETRYING = "RETRYING" CANCELLED = "CANCELLED" - NEEDS_ATTENTION = "NEEDS_ATTENTION" - SKIPPED = "SKIPPED" # Trigger configuration schemas @@ -597,10 +595,6 @@ class BackgroundExecutionInDB(BackgroundExecutionBase): status: BackgroundExecutionStatus = BackgroundExecutionStatus.PENDING result_summary: Optional[str] = None error_message: Optional[str] = None - scheduled_for: Optional[datetime] = None - source_surface: str = "wegent" - runtime_device_id: Optional[str] = None - runtime_task_id: Optional[str] = None retry_attempt: int = 0 started_at: Optional[datetime] = None completed_at: Optional[datetime] = None diff --git a/backend/app/services/subscription/service.py b/backend/app/services/subscription/service.py index b7bac0efd7c..9f91f430392 100644 --- a/backend/app/services/subscription/service.py +++ b/backend/app/services/subscription/service.py @@ -973,27 +973,6 @@ def dispatch_background_execution( """ from app.core.config import settings - runtime_automation = subscription.json.get("_internal", {}).get( - "runtime_automation" - ) - if isinstance(runtime_automation, dict): - from app.tasks.subscription_tasks import execute_runtime_automation_task - - if use_sync: - import threading - - thread = threading.Thread( - target=execute_runtime_automation_task.run, - args=(subscription.id, execution.id), - daemon=True, - ) - thread.start() - else: - execute_runtime_automation_task.apply_async( - args=[subscription.id, execution.id] - ) - return - subscription_crd = validate_subscription_for_read(subscription.json) timeout_seconds = getattr( subscription_crd.spec, diff --git a/backend/app/services/subscription/state_machine.py b/backend/app/services/subscription/state_machine.py index 6dae1d20520..43081fe38af 100644 --- a/backend/app/services/subscription/state_machine.py +++ b/backend/app/services/subscription/state_machine.py @@ -22,7 +22,6 @@ BackgroundExecutionStatus.RUNNING, BackgroundExecutionStatus.CANCELLED, BackgroundExecutionStatus.FAILED, # Can fail before starting (e.g., validation error) - BackgroundExecutionStatus.SKIPPED, }, BackgroundExecutionStatus.RUNNING: { BackgroundExecutionStatus.COMPLETED, @@ -30,7 +29,6 @@ BackgroundExecutionStatus.FAILED, BackgroundExecutionStatus.RETRYING, BackgroundExecutionStatus.CANCELLED, # Allow cancellation of running executions - BackgroundExecutionStatus.NEEDS_ATTENTION, }, BackgroundExecutionStatus.RETRYING: { BackgroundExecutionStatus.RUNNING, @@ -42,8 +40,6 @@ BackgroundExecutionStatus.COMPLETED_SILENT: set(), # Terminal state (silent completion) BackgroundExecutionStatus.FAILED: set(), # Terminal state BackgroundExecutionStatus.CANCELLED: set(), # Terminal state - BackgroundExecutionStatus.NEEDS_ATTENTION: set(), # Terminal until user resumes it - BackgroundExecutionStatus.SKIPPED: set(), # Terminal state } @@ -110,8 +106,6 @@ def is_terminal_state(status: BackgroundExecutionStatus) -> bool: BackgroundExecutionStatus.COMPLETED_SILENT, BackgroundExecutionStatus.FAILED, BackgroundExecutionStatus.CANCELLED, - BackgroundExecutionStatus.NEEDS_ATTENTION, - BackgroundExecutionStatus.SKIPPED, } diff --git a/backend/app/tasks/subscription_tasks.py b/backend/app/tasks/subscription_tasks.py index 7c6241ab59a..3a007183e96 100644 --- a/backend/app/tasks/subscription_tasks.py +++ b/backend/app/tasks/subscription_tasks.py @@ -842,36 +842,6 @@ def _dispatch_due_subscription( try: subscription_crd = validate_subscription_for_read(subscription.json) - internal = subscription.json.get("_internal", {}) - runtime_automation = internal.get("runtime_automation") - scheduled_for_value = internal.get("next_execution_time") - scheduled_for = None - if scheduled_for_value: - try: - scheduled_for = datetime.fromisoformat(scheduled_for_value) - if scheduled_for.tzinfo is not None: - scheduled_for = scheduled_for.astimezone(timezone.utc).replace( - tzinfo=None - ) - except (TypeError, ValueError): - scheduled_for = None - now = datetime.now(timezone.utc).replace(tzinfo=None) - missed_recurring_run = ( - isinstance(runtime_automation, dict) - and internal.get("trigger_type") != "one_time" - and scheduled_for is not None - and scheduled_for + timedelta(minutes=1) < now - ) - overlapping_run = ( - isinstance(runtime_automation, dict) - and db.query(BackgroundExecution.id) - .filter( - BackgroundExecution.subscription_id == subscription.id, - BackgroundExecution.status.in_(["PENDING", "RUNNING"]), - ) - .first() - is not None - ) trigger_reason = _get_trigger_reason(subscription_crd, trigger_type) execution = subscription_service.create_execution( @@ -881,22 +851,6 @@ def _dispatch_due_subscription( trigger_type=trigger_type, trigger_reason=trigger_reason, ) - if isinstance(runtime_automation, dict): - execution_model = ( - db.query(BackgroundExecution) - .filter(BackgroundExecution.id == execution.id) - .first() - ) - if execution_model is None: - raise RuntimeError( - f"Background execution {execution.id} was not persisted" - ) - execution_model.source_surface = "wework" - execution_model.runtime_device_id = runtime_automation.get( - "task_request", {} - ).get("deviceId") - execution_model.scheduled_for = scheduled_for or now - db.commit() try: _update_next_execution_time( @@ -927,21 +881,6 @@ def _dispatch_due_subscription( db.rollback() return False - if missed_recurring_run or overlapping_run: - reason = ( - "Skipped because the scheduled time was missed" - if missed_recurring_run - else "Skipped because another automation run is active" - ) - subscription_service.update_execution_status( - db, - execution_id=execution.id, - status=BackgroundExecutionStatus.SKIPPED, - error_message=reason, - skip_notifications=True, - ) - return True - try: subscription_service.dispatch_background_execution( subscription, execution, use_sync=use_sync @@ -985,120 +924,6 @@ def _dispatch_due_subscription( CHECK_DUE_SUBSCRIPTIONS_LOCK_TIMEOUT = 120 # seconds -@celery_app.task( - bind=True, - name="app.tasks.subscription_tasks.execute_runtime_automation_task", - max_retries=0, -) -def execute_runtime_automation_task( - self, - subscription_id: int, - execution_id: int, -): - """Create a Wework Runtime Task for a cloud automation execution.""" - import asyncio - - from app.db.session import get_db_session - from app.models.kind import Kind - from app.models.subscription import BackgroundExecution - from app.schemas.runtime_work import RuntimeSendRequest, RuntimeTaskCreateRequest - from app.schemas.subscription import BackgroundExecutionStatus - from app.services import runtime_work_service - - with get_db_session() as db: - subscription = ( - db.query(Kind) - .filter( - Kind.id == subscription_id, - Kind.kind == "Subscription", - Kind.is_active == True, - ) - .first() - ) - execution = ( - db.query(BackgroundExecution) - .filter(BackgroundExecution.id == execution_id) - .first() - ) - if not subscription or not execution: - return {"status": "failed", "error": "automation execution not found"} - - runtime_config = subscription.json.get("_internal", {}).get( - "runtime_automation" - ) - if not isinstance(runtime_config, dict): - execution.status = BackgroundExecutionStatus.FAILED.value - execution.error_message = "Runtime automation configuration is missing" - db.commit() - return {"status": "failed", "error": execution.error_message} - - try: - conversation_mode = runtime_config.get("conversation_mode", "independent") - payload_key = ( - "continuation_payload" - if conversation_mode == "continue_thread" - else "task_request" - ) - request_payload = dict(runtime_config.get(payload_key) or {}) - request_payload["message"] = execution.prompt - context = dict(request_payload.get("additionalContext") or {}) - context["automation_info"] = { - "type": "automation_info", - "value": { - "automationId": f"cloud:{subscription_id}", - "runId": f"cloud-run:{execution_id}", - "scheduledFor": ( - execution.scheduled_for or execution.created_at - ).isoformat(), - }, - } - request_payload["additionalContext"] = context - if conversation_mode == "continue_thread": - request = RuntimeSendRequest.model_validate(request_payload) - response = asyncio.run( - runtime_work_service.send_runtime_message( - db=db, - user_id=subscription.user_id, - request=request, - ) - ) - execution.runtime_device_id = request.address.device_id - else: - request = RuntimeTaskCreateRequest.model_validate(request_payload) - response = asyncio.run( - runtime_work_service.create_runtime_task( - db=db, - user_id=subscription.user_id, - request=request, - ) - ) - execution.runtime_device_id = response.device_id - execution.runtime_task_id = response.local_task_id - execution.status = BackgroundExecutionStatus.RUNNING.value - execution.started_at = datetime.now(timezone.utc).replace(tzinfo=None) - execution.error_message = "" - db.commit() - return { - "status": "running", - "device_id": execution.runtime_device_id, - "task_id": response.local_task_id, - } - except Exception as exc: - error = str(exc) - execution.status = BackgroundExecutionStatus.FAILED.value - execution.error_message = error - execution.completed_at = datetime.now(timezone.utc).replace(tzinfo=None) - db.commit() - logger.error( - "[runtime_automation] Failed execution %s for subscription %s: %s", - execution_id, - subscription_id, - error, - exc_info=True, - ) - return {"status": "failed", "error": error} - - @celery_app.task(bind=True, name="app.tasks.subscription_tasks.check_due_subscriptions") def check_due_subscriptions(self): """ diff --git a/backend/tests/api/endpoints/test_runtime_automations.py b/backend/tests/api/endpoints/test_runtime_automations.py deleted file mode 100644 index de554ef21db..00000000000 --- a/backend/tests/api/endpoints/test_runtime_automations.py +++ /dev/null @@ -1,67 +0,0 @@ -"""Tests for the Wework runtime automation API helpers.""" - -from datetime import datetime, timezone -from types import SimpleNamespace - -from app.api.endpoints.runtime_automations import ( - _run_response, - _subscription_schedule, -) -from app.schemas.runtime_automation import RuntimeAutomationMutation -from app.schemas.subscription import SubscriptionTriggerType - - -def _mutation(schedule: dict) -> RuntimeAutomationMutation: - return RuntimeAutomationMutation.model_validate( - { - "source": "cloud", - "name": "Daily brief", - "prompt": "Summarize the project", - "schedule": schedule, - "timezone": "Asia/Shanghai", - "enabled": True, - "conversationMode": "independent", - "taskRequest": { - "deviceId": "cloud-device", - "workspacePath": "/workspace", - "teamId": 1, - "runtime": "codex", - "message": "Summarize the project", - }, - } - ) - - -def test_one_time_schedule_is_serialized_for_subscription_helper(): - mutation = _mutation( - { - "type": "one_time", - "executeAt": "2026-07-29T01:00:00Z", - } - ) - - trigger_type, config = _subscription_schedule(mutation) - - assert trigger_type == SubscriptionTriggerType.ONE_TIME - assert config == {"execute_at": "2026-07-29T01:00:00+00:00"} - - -def test_completed_execution_is_normalized_to_succeeded(): - now = datetime.now(timezone.utc).replace(tzinfo=None) - execution = SimpleNamespace( - id=9, - subscription_id=3, - scheduled_for=now, - created_at=now, - updated_at=now, - trigger_type="manual", - status="COMPLETED", - runtime_task_id="task-1", - runtime_device_id="cloud-device", - error_message="", - ) - - response = _run_response(execution) - - assert response.status == "succeeded" - assert response.automation_id == "cloud:3" diff --git a/docs/en/user-guide/wework-automations.md b/docs/en/user-guide/wework-automations.md index 8d6addea8c7..4e859736ce3 100644 --- a/docs/en/user-guide/wework-automations.md +++ b/docs/en/user-guide/wework-automations.md @@ -23,22 +23,15 @@ are scheduled by the local Executor. ## Cloud mode -After connecting Wegent, you can create an automation for a cloud device. The -Wegent Backend stores and schedules the automation, then dispatches it to the -selected Codex Runtime device. - -- Cloud automations do not require the current Wework window to remain open. -- Only connected cloud devices owned by the current user can be selected. -- Local and cloud configurations are independent. Recreate an automation to change its source. -- A run is marked as **Needs attention** when the task requests user input. -- Local automations remain available when the cloud connection is unavailable. +Cloud automations are not currently available. The **Cloud** option remains +visible in Wework but cannot be selected. Automations are stored, scheduled, +and executed by the current device's local Executor. ## Schedule types - **Fixed time** uses a Cron expression and IANA time zone. For example, `0 9 * * 1-5` runs at 9:00 AM on weekdays. -- **Interval** repeats in minutes, hours, or days. Cloud minute intervals have a - minimum of 15 minutes. +- **Interval** repeats in minutes, hours, or days. - **One time** runs at a specific time and disables itself after scheduling. ## Conversation modes diff --git a/docs/zh/user-guide/wework-automations.md b/docs/zh/user-guide/wework-automations.md index 3512edac8ca..716ffa70099 100644 --- a/docs/zh/user-guide/wework-automations.md +++ b/docs/zh/user-guide/wework-automations.md @@ -22,20 +22,14 @@ Wework 自动化可以按固定时间、固定间隔或指定时间启动 Codex ## 云端模式 -连接 Wegent 后,可以选择云端设备创建云端自动化。配置和运行记录保存在 -Wegent Backend,由云端调度器触发,并发送到所选的 Codex Runtime 设备。 - -- 云端自动化不依赖当前 Wework 窗口保持打开。 -- 只有已连接且属于当前用户的云端设备可以作为执行目标。 -- 本地和云端配置彼此独立;切换来源需要重新创建自动化。 -- 云端任务需要用户输入时,运行记录会显示“需要关注”。 -- 云端连接暂时不可用时,本地自动化仍可正常使用。 +云端自动化暂未开放,Wework 中的“云端”选项当前不可选择。自动化配置和运行记录 +只保存在当前设备,由本机 Executor 调度和执行。 ## 计划类型 - **固定时间**:使用 Cron 表达式和 IANA 时区,例如工作日早上 9 点为 `0 9 * * 1-5`。 -- **按间隔**:按分钟、小时或天重复运行。云端分钟间隔最短为 15 分钟。 +- **按间隔**:按分钟、小时或天重复运行。 - **仅一次**:在指定时间运行一次,完成调度后自动停用。 ## 对话方式 diff --git a/wework/e2e/desktop/task-flow.e2e.mjs b/wework/e2e/desktop/task-flow.e2e.mjs index afd91b6f6e8..4ce0903c774 100644 --- a/wework/e2e/desktop/task-flow.e2e.mjs +++ b/wework/e2e/desktop/task-flow.e2e.mjs @@ -3024,6 +3024,18 @@ async function verifyAutomationLifecycle(control, workspacePath) { !draftSnapshot.testIds.includes('automation-workspace-input'), 'Automation creation should derive its working directory instead of exposing a path input' ) + await control.command('click', '[data-testid="automation-source-select"]') + await assert.rejects( + control.command('click', '[data-testid="automation-source-select-option-cloud"]'), + /disabled/, + 'The cloud automation source remained selectable' + ) + const sourceSnapshot = JSON.parse(await control.command('snapshot', 'body')) + assert.ok( + sourceSnapshot.testIds.includes('automation-source-select-menu'), + 'The automation source menu closed after clicking the disabled cloud option' + ) + await control.command('click', '[data-testid="automation-source-select"]') await control.command('click', '[data-testid="automation-conversation-mode"]') await control.command( 'click', diff --git a/wework/src/api/automations.test.ts b/wework/src/api/automations.test.ts deleted file mode 100644 index 736ca1bedf0..00000000000 --- a/wework/src/api/automations.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { describe, expect, test, vi } from 'vitest' -import type { HttpClient } from './http' -import { createAutomationApi } from './automations' -import type { AutomationMutation } from '@/types/automation' - -const mutation: AutomationMutation = { - source: 'cloud', - name: 'Daily brief', - description: '', - prompt: 'Summarize the project', - schedule: { type: 'cron', expression: '0 9 * * 1-5' }, - timezone: 'Asia/Shanghai', - enabled: true, - conversationMode: 'independent', - notificationPolicy: 'all_runs', - taskRequest: { - deviceId: 'cloud-device', - workspacePath: '/workspace', - teamId: 1, - runtime: 'codex', - message: 'Summarize the project', - }, -} - -describe('createAutomationApi', () => { - test('routes CRUD operations through runtime automation endpoints', async () => { - const get = vi.fn().mockResolvedValue({ items: [] }) - const post = vi.fn().mockResolvedValue({}) - const put = vi.fn().mockResolvedValue({}) - const del = vi.fn().mockResolvedValue({ deleted: true }) - const api = createAutomationApi({ get, post, put, delete: del } as unknown as HttpClient) - - await api.listAutomations() - await api.getAutomation('cloud:7') - await api.createAutomation(mutation) - await api.updateAutomation('cloud:7', mutation) - await api.toggleAutomation('cloud:7', false) - await api.runAutomationNow('cloud:7') - await api.listAutomationRuns('cloud:7') - await api.deleteAutomation('cloud:7') - - expect(get).toHaveBeenNthCalledWith(1, '/runtime-automations') - expect(get).toHaveBeenNthCalledWith(2, '/runtime-automations/cloud:7') - expect(post).toHaveBeenCalledWith('/runtime-automations', mutation) - expect(put).toHaveBeenCalledWith('/runtime-automations/cloud:7', mutation) - expect(post).toHaveBeenCalledWith('/runtime-automations/cloud:7/toggle', { - enabled: false, - }) - expect(post).toHaveBeenCalledWith('/runtime-automations/cloud:7/run', {}) - expect(get).toHaveBeenNthCalledWith(3, '/runtime-automations/runs?automation_id=cloud%3A7') - expect(del).toHaveBeenCalledWith('/runtime-automations/cloud:7') - }) -}) diff --git a/wework/src/api/automations.ts b/wework/src/api/automations.ts deleted file mode 100644 index a8eec921bda..00000000000 --- a/wework/src/api/automations.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { - Automation, - AutomationListResponse, - AutomationMutation, - AutomationRun, - AutomationRunListResponse, -} from '@/types/automation' -import type { HttpClient } from './http' - -export function createAutomationApi(client: HttpClient) { - return { - listAutomations(): Promise { - return client.get('/runtime-automations') - }, - getAutomation(automationId: string): Promise<{ automation: Automation }> { - return client.get(`/runtime-automations/${automationId}`) - }, - createAutomation(data: AutomationMutation): Promise<{ automation: Automation }> { - return client.post('/runtime-automations', data) - }, - updateAutomation( - automationId: string, - data: AutomationMutation - ): Promise<{ automation: Automation }> { - return client.put(`/runtime-automations/${automationId}`, data) - }, - deleteAutomation(automationId: string): Promise<{ deleted: boolean }> { - return client.delete(`/runtime-automations/${automationId}`) - }, - toggleAutomation(automationId: string, enabled: boolean): Promise<{ automation: Automation }> { - return client.post(`/runtime-automations/${automationId}/toggle`, { enabled }) - }, - runAutomationNow(automationId: string): Promise<{ run: AutomationRun | null }> { - return client.post(`/runtime-automations/${automationId}/run`, {}) - }, - listAutomationRuns(automationId?: string): Promise { - const query = automationId ? `?automation_id=${encodeURIComponent(automationId)}` : '' - return client.get(`/runtime-automations/runs${query}`) - }, - } -} diff --git a/wework/src/api/backend/backendServices.ts b/wework/src/api/backend/backendServices.ts index e745e1ae31e..a9432800aeb 100644 --- a/wework/src/api/backend/backendServices.ts +++ b/wework/src/api/backend/backendServices.ts @@ -9,7 +9,6 @@ import { createImSessionApi } from '@/api/imSessions' import { createModelApi } from '@/api/models' import { createProjectApi } from '@/api/projects' import { createRuntimeWorkApi } from '@/api/runtimeWork' -import { createAutomationApi } from '@/api/automations' import { createSkillApi } from '@/api/skills' import { createTaskApi } from '@/api/tasks' import { createTeamApi } from '@/api/teams' @@ -48,7 +47,6 @@ export function createBackendWorkbenchServices( const deviceApi = createDeviceApi(client) const projectApi = createProjectApi(client) const runtimeWorkApi = createRuntimeWorkApi(client) - const automationApi = createAutomationApi(client) const taskApi = createTaskApi(client) const deliveryApi = createDeliveryApi(client) const socketClient = createSocketClient({ @@ -76,7 +74,6 @@ export function createBackendWorkbenchServices( }, imSessionApi: createImSessionApi(client), runtimeWorkApi, - automationApi, executorClient: createExecutorClientFromApis({ transportKind, deviceApi, diff --git a/wework/src/api/hybrid/hybridServices.ts b/wework/src/api/hybrid/hybridServices.ts index 79830dd6a55..2b6a044fc29 100644 --- a/wework/src/api/hybrid/hybridServices.ts +++ b/wework/src/api/hybrid/hybridServices.ts @@ -50,7 +50,6 @@ import type { UnifiedModelListResponse, User, } from '@/types/api' -import type { Automation, AutomationMutation } from '@/types/automation' const LOCAL_DEVICE_ID = 'local-device' const CLOUD_BACKGROUND_CACHE_TTL_MS = 30_000 @@ -304,8 +303,6 @@ export function createHybridWorkbenchServices( let rememberedCloudModels: UnifiedModel[] = [] let cloudModelsLoaded = false let cloudModelsRequest: Promise | null = null - let rememberedCloudAutomations: Automation[] = [] - let cloudAutomationsRequest: Promise | null = null const rememberedCloudSearch = new Map() const cloudSearchRequests = new Map>() const rememberedCloudArchives = new Map() @@ -347,23 +344,6 @@ export function createHybridWorkbenchServices( cloudModelsRequest = null }) } - const loadCloudAutomationsInBackground = () => { - if (cloudAutomationsRequest) return - cloudAutomationsRequest = cloudServices - .automationApi!.listAutomations() - .then(response => { - rememberedCloudAutomations = response.items - for (const automation of response.items) { - automationSourceById.set(automation.id, 'cloud') - } - }) - .catch(error => { - console.warn('[Wework] Failed to load cloud automations:', error) - }) - .finally(() => { - cloudAutomationsRequest = null - }) - } const rememberLocalRuntimeWorkDevices = (work: RuntimeWorkListResponse) => { work.projects.forEach(project => { localRuntimeProjectKeys.add(project.project.key) @@ -879,97 +859,7 @@ export function createHybridWorkbenchServices( return runtimeApi(data.target.deviceId).forkRuntimeTask(data) }, } - const automationSourceById = new Map() - const automationApi: NonNullable = { - async listAutomations() { - const local = await localServices.automationApi!.listAutomations() - loadCloudAutomationsInBackground() - const items = [...local.items, ...rememberedCloudAutomations].sort((left, right) => - right.updatedAt.localeCompare(left.updatedAt) - ) - for (const automation of items) { - automationSourceById.set(automation.id, automation.source) - } - return { items } - }, - getAutomation(automationId: string) { - return automationApiForId(automationId).getAutomation(automationId) - }, - createAutomation(data: AutomationMutation) { - const api = - data.source === 'cloud' ? cloudServices.automationApi! : localServices.automationApi! - return api.createAutomation(data).then(response => { - automationSourceById.set(response.automation.id, response.automation.source) - if (response.automation.source === 'cloud') { - rememberedCloudAutomations = [ - response.automation, - ...rememberedCloudAutomations.filter(item => item.id !== response.automation.id), - ] - } - return response - }) - }, - updateAutomation(automationId: string, data: AutomationMutation) { - const api = - data.source === 'cloud' ? cloudServices.automationApi! : localServices.automationApi! - return api.updateAutomation(automationId, data).then(response => { - if (response.automation.source === 'cloud') { - rememberedCloudAutomations = [ - response.automation, - ...rememberedCloudAutomations.filter(item => item.id !== automationId), - ] - } - return response - }) - }, - deleteAutomation(automationId: string) { - return automationApiForId(automationId) - .deleteAutomation(automationId) - .then(response => { - automationSourceById.delete(automationId) - rememberedCloudAutomations = rememberedCloudAutomations.filter( - item => item.id !== automationId - ) - return response - }) - }, - toggleAutomation(automationId: string, enabled: boolean) { - return automationApiForId(automationId) - .toggleAutomation(automationId, enabled) - .then(response => { - if (response.automation.source === 'cloud') { - rememberedCloudAutomations = rememberedCloudAutomations.map(item => - item.id === automationId ? response.automation : item - ) - } - return response - }) - }, - runAutomationNow(automationId: string) { - return automationApiForId(automationId).runAutomationNow(automationId) - }, - listAutomationRuns(automationId?: string) { - if (automationId) { - return automationApiForId(automationId).listAutomationRuns(automationId) - } - return Promise.all([ - localServices.automationApi!.listAutomationRuns(), - cloudServices.automationApi!.listAutomationRuns().catch(() => ({ items: [] })), - ]).then(([local, cloud]) => ({ - items: [...local.items, ...cloud.items].sort((left, right) => - right.createdAt.localeCompare(left.createdAt) - ), - })) - }, - } - - function automationApiForId( - automationId: string - ): NonNullable { - return automationId.startsWith('cloud:') || automationSourceById.get(automationId) === 'cloud' - ? cloudServices.automationApi! - : localServices.automationApi! - } + const automationApi = localServices.automationApi const cloudRuntimeChatStream = createRuntimeChatStream({ request: (method, params) => { diff --git a/wework/src/features/automations/AutomationDetailWorkspace.tsx b/wework/src/features/automations/AutomationDetailWorkspace.tsx index af5b80f158d..d1a5d68c5a4 100644 --- a/wework/src/features/automations/AutomationDetailWorkspace.tsx +++ b/wework/src/features/automations/AutomationDetailWorkspace.tsx @@ -42,7 +42,6 @@ interface AutomationDetailWorkspaceProps { automation: Automation | null runs: AutomationRun[] locale: string - cloudAvailable: boolean devices: DeviceInfo[] projects: RuntimeProjectWork[] models: UnifiedModel[] @@ -66,7 +65,6 @@ export function AutomationDetailWorkspace({ automation, runs, locale, - cloudAvailable, devices, projects, models, @@ -267,7 +265,7 @@ export function AutomationDetailWorkspace({ { value: 'cloud', label: t('workbench.automation_cloud', '云端'), - disabled: !cloudAvailable, + disabled: true, }, ]} /> diff --git a/wework/src/features/workbench/workbenchServices.ts b/wework/src/features/workbench/workbenchServices.ts index 174376666d3..deade3b1abe 100644 --- a/wework/src/features/workbench/workbenchServices.ts +++ b/wework/src/features/workbench/workbenchServices.ts @@ -17,7 +17,6 @@ import { createLocalAppServices } from '@/api/local/localServices' import { createModelApi } from '@/api/models' import { createProjectApi } from '@/api/projects' import { createRuntimeWorkApi } from '@/api/runtimeWork' -import { createAutomationApi } from '@/api/automations' import { createSkillApi } from '@/api/skills' import { createTaskApi } from '@/api/tasks' import { createTeamApi } from '@/api/teams' @@ -34,6 +33,13 @@ import type { User, } from '@/types/api' import type { DeviceSessionResponse } from '@/types/devices' +import type { + Automation, + AutomationListResponse, + AutomationMutation, + AutomationRun, + AutomationRunListResponse, +} from '@/types/automation' import type { WorkspaceFileApi } from '@/types/workspace-files' import type { AuthenticatedSocketClient } from '@wegent/chat-core' import type { createExternalIssueApi } from '@/api/local/localDelivery' @@ -56,6 +62,20 @@ export interface ProjectSpaceApis { defaultLocation: ProjectSpaceLocation } +export interface AutomationApi { + listAutomations: () => Promise + getAutomation: (automationId: string) => Promise<{ automation: Automation }> + createAutomation: (data: AutomationMutation) => Promise<{ automation: Automation }> + updateAutomation: ( + automationId: string, + data: AutomationMutation + ) => Promise<{ automation: Automation }> + deleteAutomation: (automationId: string) => Promise<{ deleted: boolean }> + toggleAutomation: (automationId: string, enabled: boolean) => Promise<{ automation: Automation }> + runAutomationNow: (automationId: string) => Promise<{ run: AutomationRun | null }> + listAutomationRuns: (automationId?: string) => Promise +} + export interface WorkbenchServices { teamApi: ReturnType modelApi: ReturnType @@ -95,7 +115,7 @@ export interface WorkbenchServices { projectSpaceApis?: ProjectSpaceApis imSessionApi?: ReturnType runtimeWorkApi?: ReturnType - automationApi?: ReturnType + automationApi?: AutomationApi attachmentApi?: { uploadAttachment: (file: File, onProgress?: (progress: number) => void) => Promise deleteAttachment?: (attachmentId: number) => Promise diff --git a/wework/src/pages/AutomationsPage.tsx b/wework/src/pages/AutomationsPage.tsx index 030603f140e..462d7277f57 100644 --- a/wework/src/pages/AutomationsPage.tsx +++ b/wework/src/pages/AutomationsPage.tsx @@ -25,7 +25,6 @@ import { type AutomationDraft, } from '@/features/automations/automationDraft' import { useAuth } from '@/features/auth/useAuth' -import { useCloudConnection } from '@/features/cloud-connection/useCloudConnection' import { useWorkbench } from '@/features/workbench/useWorkbench' import { useDesktopSidebarCollapsed } from '@/components/layout/useDesktopSidebarCollapsed' import { useIsMobile } from '@/hooks/useIsMobile' @@ -48,7 +47,6 @@ type StatusFilter = 'all' | 'active' | 'paused' export function AutomationsPage() { const { t, i18n } = useTranslation('common') const { logout } = useAuth() - const cloudConnection = useCloudConnection() const isMobile = useIsMobile() const { sidebarCollapsed } = useDesktopSidebarCollapsed() const { @@ -113,10 +111,6 @@ export function AutomationsPage() { () => state.devices.filter(device => !isCloudDevice(device)), [state.devices] ) - const cloudDevices = useMemo( - () => state.devices.filter(device => isCloudDevice(device)), - [state.devices] - ) const loadAutomations = useCallback(async () => { if (!automationApi) { setError(t('workbench.automations_unavailable', '当前运行环境不支持自动化')) @@ -264,9 +258,8 @@ export function AutomationsPage() { } const changeSource = (source: AutomationSource) => { - if (!draft || editing) return - const devices = source === 'cloud' ? cloudDevices : localDevices - const deviceId = devices[0]?.device_id ?? '' + if (!draft || editing || source !== 'local') return + const deviceId = localDevices[0]?.device_id ?? '' setDraft(current => current ? { @@ -294,10 +287,7 @@ export function AutomationsPage() { } const workspacePath = draft.workspacePath.trim() || (await getDeviceHomeDirectory(draft.deviceId)) - const team = - draft.source === 'cloud' - ? await services.cloudBackgroundApi?.getDefaultWorkbenchTeam?.() - : state.defaultTeam + const team = state.defaultTeam if (!team) { throw new Error(t('workbench.automation_team_unavailable', '无法获取运行所需的默认智能体')) } @@ -308,7 +298,7 @@ export function AutomationsPage() { runtime: 'codex', message: draft.prompt.trim(), title: draft.name.trim(), - ...(draft.source === 'local' && draft.modelId + ...(draft.modelId ? { modelId: draft.modelId, modelType: draft.modelType as RuntimeTaskCreateRequest['modelType'], @@ -554,8 +544,7 @@ export function AutomationsPage() { automation={editing} runs={runs} locale={locale} - cloudAvailable={cloudConnection.isConnected} - devices={draft.source === 'cloud' ? cloudDevices : localDevices} + devices={localDevices} projects={state.runtimeWork?.projects ?? []} models={projectChat.models} currentRuntimeTask={state.currentRuntimeTask}