Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 0 additions & 106 deletions backend/app/api/endpoints/deliveries.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,6 @@
LoopItemUpdate,
MyWorkItemResponse,
MyWorkListResponse,
RuntimeTaskTrack,
RuntimeTaskTrackingStatusUpdate,
RuntimeTaskTrackResponse,
)
from app.services.cloud_projects import cloud_project_service
from app.services.delivery import delivery_service
Expand Down Expand Up @@ -197,109 +194,6 @@ def bind_cloud_project_task(
return LoopItemTaskBindingResponse.model_validate(binding)


def _tracked_item_response(
db: Session,
item_id: str,
current_user: User,
) -> LoopItemResponse:
if external_loop_item_provider.is_external_item(db, item_id):
return LoopItemResponse.model_validate(
external_loop_item_provider.get(db, item_id, current_user.id)
)
item = loop_item_service.get(db, item_id, current_user.id)
return _loop_item_response(db, item, current_user)


@router.post(
"/cloud-projects/{project_id}/tasks/track",
response_model=RuntimeTaskTrackResponse,
status_code=status.HTTP_201_CREATED,
)
def track_cloud_project_task(
project_id: int,
values: RuntimeTaskTrack,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
) -> RuntimeTaskTrackResponse:
existing = loop_item_service.find_active_task_binding(
db, current_user.id, values.device_id, values.task_id
)
if existing is not None and existing.loop_item_id:
if str(existing.cloud_project_id) != str(project_id):
raise HTTPException(
status.HTTP_409_CONFLICT,
"Runtime task is already tracked by another project",
)
return RuntimeTaskTrackResponse(
item=_tracked_item_response(db, existing.loop_item_id, current_user),
binding=LoopItemTaskBindingResponse.model_validate(existing),
)

project = cloud_project_service.get(db, project_id, current_user.id)
created = loop_item_provider_router.create(
db,
project,
current_user,
LoopItemCreate(
title=values.task_title,
description=values.description,
status="in_progress",
),
)
item_id = str(created.values["id"])
external_loop_item_provider.ensure_shadow(db, item_id, current_user.id)
binding = loop_item_service.bind_task(
db, item_id, values.binding(), current_user.id
)
return RuntimeTaskTrackResponse(
item=LoopItemResponse.model_validate(created.values),
binding=LoopItemTaskBindingResponse.model_validate(binding),
)


@router.patch(
"/runtime-tasks/cloud-context/tracking-status",
response_model=LoopItemResponse | None,
)
def update_runtime_task_tracking_status(
values: RuntimeTaskTrackingStatusUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
) -> LoopItemResponse | None:
binding = loop_item_service.find_active_task_binding(
db, current_user.id, values.device_id, values.task_id
)
if binding is None or not binding.loop_item_id:
return None
item = _tracked_item_response(db, binding.loop_item_id, current_user)
next_status: str | None = None
if values.execution_status == "running" and item.status in {
"inbox",
"pending",
"in_review",
}:
next_status = "in_progress"
elif values.execution_status == "succeeded" and item.status == "in_progress":
next_status = "in_review"
if next_status is None:
return item
if external_loop_item_provider.is_external_item(db, item.id):
updated = external_loop_item_provider.update(
db,
item.id,
current_user.id,
LoopItemUpdate(version=item.version, status=next_status),
)
return LoopItemResponse.model_validate(updated)
stored = loop_item_service.update(
db,
item.id,
current_user.id,
LoopItemUpdate(version=item.version, status=next_status),
)
return _loop_item_response(db, stored, current_user)


@router.delete("/runtime-tasks/cloud-context", status_code=status.HTTP_204_NO_CONTENT)
def unbind_runtime_task_cloud_context(
values: LoopItemTaskBind,
Expand Down
33 changes: 0 additions & 33 deletions backend/app/schemas/delivery.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,39 +234,6 @@ def normalize_unlinked_at(cls, value: object) -> object:
return LoopItemResponse.normalize_unset_datetime(value)


class RuntimeTaskTrack(BaseModel):
model_config = ConfigDict(populate_by_name=True)

device_id: str = Field(alias="deviceId", min_length=1, max_length=100)
task_id: str = Field(alias="taskId", min_length=1, max_length=255)
task_title: str = Field(alias="taskTitle", min_length=1, max_length=255)
description: str = ""
backend_task_id: int | None = Field(default=None, alias="backendTaskId")

def binding(self) -> LoopItemTaskBind:
return LoopItemTaskBind(
deviceId=self.device_id,
taskId=self.task_id,
taskTitle=self.task_title,
backendTaskId=self.backend_task_id,
)


class RuntimeTaskTrackingStatusUpdate(BaseModel):
model_config = ConfigDict(populate_by_name=True)

device_id: str = Field(alias="deviceId", min_length=1, max_length=100)
task_id: str = Field(alias="taskId", min_length=1, max_length=255)
execution_status: Literal["running", "succeeded", "failed", "cancelled"] = Field(
alias="executionStatus"
)


class RuntimeTaskTrackResponse(BaseModel):
item: LoopItemResponse
binding: LoopItemTaskBindingResponse


class CloudTaskContextResponse(LoopItemTaskBindingResponse):
project: CloudProjectResponse
loop_item: LoopItemResponse | None = None
Expand Down
18 changes: 0 additions & 18 deletions backend/app/services/loop_items/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -788,24 +788,6 @@ def find_cloud_context(
item = db.get(LoopItem, binding.loop_item_id) if binding.loop_item_id else None
return binding, project, item

def find_active_task_binding(
self,
db: Session,
user_id: int,
device_id: str,
task_id: str,
) -> LoopItemTaskBinding | None:
return (
db.query(LoopItemTaskBinding)
.filter(
LoopItemTaskBinding.task_user_id == user_id,
LoopItemTaskBinding.device_id == device_id,
LoopItemTaskBinding.task_id == task_id,
loop_datetime_is_unset(LoopItemTaskBinding.unlinked_at),
)
.first()
)

def unbind_cloud_context(
self, db: Session, values: LoopItemTaskBind, user_id: int
) -> None:
Expand Down
47 changes: 0 additions & 47 deletions backend/tests/api/test_cloud_projects_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -785,53 +785,6 @@ def provider_request(
assert all("server-only-secret" not in str(payload) for _, _, payload in requests)


def test_explicit_project_selection_tracks_runtime_task_idempotently(
test_client: TestClient,
test_db: Session,
test_user: User,
test_token: str,
) -> None:
project = test_client.post(
"/api/v1/cloud-projects",
headers=_auth(test_token),
json={"project_key": "track", "name": "Tracked project"},
).json()
payload = {
"deviceId": "desktop-1",
"taskId": "runtime-task-1",
"taskTitle": "Implement explicit task tracking",
"description": "Created from the Wework task composer.",
}
tracked = test_client.post(
f"/api/v1/cloud-projects/{project['id']}/tasks/track",
headers=_auth(test_token),
json=payload,
)
assert tracked.status_code == 201
assert tracked.json()["item"]["status"] == "in_progress"
item_id = tracked.json()["item"]["id"]

retried = test_client.post(
f"/api/v1/cloud-projects/{project['id']}/tasks/track",
headers=_auth(test_token),
json=payload,
)
assert retried.status_code == 201
assert retried.json()["item"]["id"] == item_id

reviewed = test_client.patch(
"/api/v1/runtime-tasks/cloud-context/tracking-status",
headers=_auth(test_token),
json={
"deviceId": "desktop-1",
"taskId": "runtime-task-1",
"executionStatus": "succeeded",
},
)
assert reviewed.status_code == 200
assert reviewed.json()["status"] == "in_review"


def test_todo_lifecycle_and_multiple_local_tasks(
test_client: TestClient,
test_db: Session,
Expand Down
24 changes: 13 additions & 11 deletions docs/en/wegent/developer-guide/cloud-project-collaboration.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,12 @@ CloudProject

## Data ownership

| Data | Source of truth |
| --- | --- |
| Cloud projects, members, TODOs, task links, delivery metadata | Backend MySQL |
| Data | Source of truth |
| --------------------------------------------------------------------------------------- | -------------------------------- |
| Cloud projects, members, TODOs, task links, delivery metadata | Backend MySQL |
| Local paths, devices, Git, execution configuration, and default project-space reference | Device-local Codex project state |
| Shared files, Markdown, conversations, and delivery snapshots | MinIO/S3 |
| AI access to cloud data | MCP authorized by the Backend |
| Shared files, Markdown, conversations, and delivery snapshots | MinIO/S3 |
| AI access to cloud data | MCP authorized by the Backend |

Objects are isolated by the cloud project's public ID:

Expand Down Expand Up @@ -95,12 +95,12 @@ Completed TODOs may be reopened into `in_progress`. Updates carry a `version` va

Reuse `resource_members` and `share_links` with a new `CloudProject` resource type.

| Role | Read | Edit TODOs/files | Manage members | Archive project |
| --- | --- | --- | --- | --- |
| Reporter | Yes | No | No | No |
| Developer | Yes | Yes | No | No |
| Maintainer | Yes | Yes | Yes | No |
| Owner | Yes | Yes | Yes | Yes |
| Role | Read | Edit TODOs/files | Manage members | Archive project |
| ---------- | ---- | ---------------- | -------------- | --------------- |
| Reporter | Yes | No | No | No |
| Developer | Yes | Yes | No | No |
| Maintainer | Yes | Yes | Yes | No |
| Owner | Yes | Yes | Yes | Yes |

Every TODO, delivery, file, and MCP request resolves the caller's cloud-project role first. Inaccessible resources return 404 to avoid disclosing their existence.

Expand Down Expand Up @@ -146,6 +146,8 @@ Delivery services do not own TODO CRUD. LoopItem services do not access MinIO di

Creation and updates use separate endpoints rather than PUT upsert. Shared files support folder creation, upload, rename/move, short-lived access, and recursive deletion. A move copies MinIO objects first, commits metadata, and only then removes the old objects; failed moves clean up newly copied objects.

When Wework adds a new runtime task to a cloud project space, it composes the existing primitives: create a `LoopItem`, then bind the runtime task; when execution status changes, read the task context and update the linked TODO. The Backend intentionally has no aggregate tracking endpoint dedicated to that orchestration. This allows the desktop app and Backend to be released independently while the stable TODO-creation, task-binding, and optimistic-locking APIs preserve the same behavior. The desktop app deduplicates concurrent association requests for the same runtime task and reuses a created TODO after a temporary binding failure to avoid duplicate cards.

The Wework Composer encodes cloud projects, directories, files, TODOs, and deliveries as atomic `cloud://` references. Tasks carrying cloud-project context receive the Delivery MCP, and `resolve_cloud_reference` authorizes and resolves every reference in Backend so neither clients nor AI receive S3 credentials. The TODO board refreshes periodically while visible, while writes continue to use `version` optimistic locking for concurrent collaborators.

## Delivery sequence
Expand Down
26 changes: 14 additions & 12 deletions docs/zh/wegent/developer-guide/cloud-project-collaboration.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,12 @@ CloudProject

## 数据归属

| 数据 | 事实来源 |
| --- | --- |
| 云项目、成员、TODO、任务关联、交付元数据 | Backend MySQL |
| 本地路径、设备、Git、执行配置和默认项目空间引用 | 本地 Codex 项目状态 |
| 共享文件、Markdown、聊天记录、交付快照 | MinIO/S3 |
| AI 对云空间的访问 | Backend 鉴权后的 MCP |
| 数据 | 事实来源 |
| ----------------------------------------------- | -------------------- |
| 云项目、成员、TODO、任务关联、交付元数据 | Backend MySQL |
| 本地路径、设备、Git、执行配置和默认项目空间引用 | 本地 Codex 项目状态 |
| 共享文件、Markdown、聊天记录、交付快照 | MinIO/S3 |
| AI 对云空间的访问 | Backend 鉴权后的 MCP |

MinIO 对象使用云项目公开 ID 隔离:

Expand Down Expand Up @@ -95,12 +95,12 @@ inbox → pending → in_progress → in_review → completed

复用 `resource_members` 和 `share_links`,新增 `CloudProject` 资源类型。

| 角色 | 读取 | 编辑 TODO/文件 | 管理成员 | 归档项目 |
| --- | --- | --- | --- | --- |
| Reporter | 是 | 否 | 否 | 否 |
| Developer | 是 | 是 | 否 | 否 |
| Maintainer | 是 | 是 | 是 | 否 |
| Owner | 是 | 是 | 是 | 是 |
| 角色 | 读取 | 编辑 TODO/文件 | 管理成员 | 归档项目 |
| ---------- | ---- | -------------- | -------- | -------- |
| Reporter | 是 | 否 | 否 | 否 |
| Developer | 是 | 是 | 否 | 否 |
| Maintainer | 是 | 是 | 是 | 否 |
| Owner | 是 | 是 | 是 | 是 |

所有 TODO、交付、文件和 MCP 请求都必须先解析云项目角色。无权限资源统一返回 404,避免泄露资源是否存在。

Expand Down Expand Up @@ -146,6 +146,8 @@ Delivery 服务不负责 TODO CRUD;LoopItem 服务不直接访问 MinIO;MCP

创建与更新使用不同端点,不提供 PUT upsert。共享文件支持创建目录、上传、重命名/移动、短期授权访问和递归删除;移动对象时先复制 MinIO 对象、提交元数据,再删除旧对象,失败时清理新对象。

Wework 把新运行任务加入云项目空间时,使用已有的基础能力组合完成:先创建 `LoopItem`,再绑定运行任务;运行状态变化时先读取任务上下文,再更新对应 TODO。Backend 不提供仅为这条编排流程设计的聚合追踪接口,因此桌面端和 Backend 可以独立发布,同时仍由 TODO 创建、任务绑定和乐观锁更新这三类稳定 API 保证行为一致。桌面端会对同一运行任务的并发关联请求去重;如果绑定临时失败,会复用已创建的 TODO 后重试,避免产生重复卡片。

Comment on lines +149 to +150

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Document the cloud-context API route.

The tracking flow reads task context before it updates a TODO. The API inventories list /v1/runtime-tasks/loop-item, but the implementation uses /v1/runtime-tasks/cloud-context. Add this route and identify it as the task-context lookup API.

  • docs/zh/wegent/developer-guide/cloud-project-collaboration.md#L149-L150: Add /v1/runtime-tasks/cloud-context and name its task-context purpose.
  • docs/en/wegent/developer-guide/cloud-project-collaboration.md#L149-L150: Add /v1/runtime-tasks/cloud-context and name its task-context purpose.
📍 Affects 2 files
  • docs/zh/wegent/developer-guide/cloud-project-collaboration.md#L149-L150 (this comment)
  • docs/en/wegent/developer-guide/cloud-project-collaboration.md#L149-L150
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/zh/wegent/developer-guide/cloud-project-collaboration.md` around lines
149 - 150, Add /v1/runtime-tasks/cloud-context to the cloud-project API
inventory in both docs/zh/wegent/developer-guide/cloud-project-collaboration.md
(lines 149-150) and
docs/en/wegent/developer-guide/cloud-project-collaboration.md (lines 149-150),
identifying it as the task-context lookup API used before TODO updates.

Wework Composer 把云项目、目录、文件、TODO 和交付编码为 `cloud://` 原子引用。任务携带云项目上下文时注入 Delivery MCP;`resolve_cloud_reference` 在 Backend 再次鉴权并解析引用,客户端和 AI 均不接触 S3 凭证。TODO 看板在窗口可见时周期刷新,写操作仍依赖 `version` 乐观锁处理多人并发。

## 实施顺序
Expand Down
Loading
Loading