Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
1634cfd
feat(wework): connect tasks with default work items
qdaxb Aug 15, 2026
ee7451b
Merge remote-tracking branch 'origin/main' into feature/work-item-lif…
qdaxb Aug 16, 2026
49cb1db
feat(wework): organize related tasks in workspaces
qdaxb Aug 16, 2026
eb8306d
Merge remote-tracking branch 'origin/main' into feature/work-item-lif…
qdaxb Aug 16, 2026
5d70837
feat(wework): complete workspace issue workflow
qdaxb Aug 16, 2026
3d2b4fd
docs(wework): document workspace issue creation
qdaxb Aug 16, 2026
528741f
fix(wework): simplify issue composer actions
qdaxb Aug 16, 2026
d4a15ae
feat(workspace): complete issue execution workflow
qdaxb Aug 16, 2026
36560bf
Merge remote-tracking branch 'origin/main' into feature/work-item-lif…
qdaxb Aug 16, 2026
4756fb2
docs(wework): document external issue hooks
qdaxb Aug 16, 2026
e6d38a9
Merge remote-tracking branch 'origin/main' into feature/work-item-lif…
qdaxb Aug 17, 2026
2757b9a
test(wework): align workspace flow with nested task chat
qdaxb Aug 17, 2026
74776da
Merge remote-tracking branch 'origin/main' into feature/work-item-lif…
qdaxb Aug 17, 2026
2118c4a
Merge branch 'main' into feature/work-item-lifecycle
qdaxb Aug 17, 2026
b6c5d0c
Merge remote-tracking branch 'origin/main' into feature/work-item-lif…
qdaxb Aug 17, 2026
4c22818
test(wework): wait for refreshed task board
qdaxb Aug 17, 2026
1d7dcbd
Merge remote-tracking branch 'origin/feature/work-item-lifecycle' int…
qdaxb Aug 17, 2026
d4f6025
Merge remote-tracking branch 'origin/main' into feature/work-item-lif…
qdaxb Aug 17, 2026
4310a75
fix(wework): stabilize project task and model launch
qdaxb Aug 17, 2026
8dc1c0b
docs(wework): clarify linked board navigation
qdaxb Aug 17, 2026
82ae947
Merge remote-tracking branch 'origin/main' into feature/work-item-lif…
qdaxb Aug 17, 2026
df74bd6
test(wework): update project automation issue flow
qdaxb Aug 17, 2026
cda52b2
test(wework): open linked task from issue detail
qdaxb Aug 17, 2026
6cc4f3c
fix: fix ci
qdaxb Aug 17, 2026
1924803
Merge remote-tracking branch 'origin/main' into feature/work-item-lif…
qdaxb Aug 17, 2026
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
11 changes: 11 additions & 0 deletions backend/app/api/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
openapi_responses,
pet,
project_automations,
project_incoming_hooks,
projects,
prompt_optimization,
quota,
Expand Down Expand Up @@ -154,6 +155,16 @@
prefix="/v1/cloud-projects",
tags=["project-automations"],
)
api_router.include_router(
project_incoming_hooks.router,
prefix="/v1/cloud-projects",
tags=["project-incoming-hooks"],
)
api_router.include_router(
project_incoming_hooks.public_router,
prefix="/v1/incoming-hooks",
tags=["project-incoming-hooks"],
)
api_router.include_router(
loop_item_executions.router,
prefix="/v1/cloud-projects",
Expand Down
125 changes: 125 additions & 0 deletions backend/app/api/endpoints/project_incoming_hooks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# SPDX-FileCopyrightText: 2026 Weibo, Inc.
# SPDX-License-Identifier: Apache-2.0

"""Project incoming hook management and public ingestion endpoints."""

from fastapi import APIRouter, Depends, Request, status
from sqlalchemy.orm import Session

from app.api.dependencies import get_db
from app.core.security import get_current_user
from app.models.delivery import ProjectIncomingHook
from app.models.user import User
from app.schemas.project_incoming_hook import (
ProjectIncomingHookCreate,
ProjectIncomingHookUpdate,
ProjectIncomingHookView,
ProjectIncomingReceipt,
)
from app.services.project_incoming_hooks import project_incoming_hook_service

router = APIRouter()
public_router = APIRouter()


def _view(request: Request, hook: ProjectIncomingHook) -> ProjectIncomingHookView:
return ProjectIncomingHookView(
id=str(hook.id),
project_id=str(hook.cloud_project_id),
name=hook.name or "",
status=hook.status or "disabled",
webhook_url=str(
request.url_for("receive_project_incoming_hook", token=hook.public_id)
),
version=hook.version,
created_at=hook.created_at,
updated_at=hook.updated_at,
)


@router.get(
"/{project_id}/incoming-hooks",
response_model=list[ProjectIncomingHookView],
)
def list_incoming_hooks(
project_id: str,
request: Request,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
) -> list[ProjectIncomingHookView]:
return [
_view(request, hook)
for hook in project_incoming_hook_service.list(db, project_id, current_user.id)
]


@router.post(
"/{project_id}/incoming-hooks",
response_model=ProjectIncomingHookView,
status_code=status.HTTP_201_CREATED,
)
def create_incoming_hook(
project_id: str,
values: ProjectIncomingHookCreate,
request: Request,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
) -> ProjectIncomingHookView:
hook = project_incoming_hook_service.create(db, project_id, current_user.id, values)
return _view(request, hook)


@router.patch(
"/{project_id}/incoming-hooks/{hook_id}",
response_model=ProjectIncomingHookView,
)
def update_incoming_hook(
project_id: str,
hook_id: str,
values: ProjectIncomingHookUpdate,
request: Request,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
) -> ProjectIncomingHookView:
hook = project_incoming_hook_service.update(
db, project_id, hook_id, current_user.id, values
)
return _view(request, hook)


@router.post(
"/{project_id}/incoming-hooks/{hook_id}/rotate",
response_model=ProjectIncomingHookView,
)
def rotate_incoming_hook(
project_id: str,
hook_id: str,
request: Request,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
) -> ProjectIncomingHookView:
hook = project_incoming_hook_service.rotate(
db, project_id, hook_id, current_user.id
)
return _view(request, hook)


@public_router.post(
"/{token}",
response_model=ProjectIncomingReceipt,
status_code=status.HTTP_202_ACCEPTED,
name="receive_project_incoming_hook",
)
async def receive_project_incoming_hook(
token: str,
request: Request,
db: Session = Depends(get_db),
) -> ProjectIncomingReceipt:
result = await project_incoming_hook_service.receive(
db,
token,
await request.body(),
request.headers.get("content-type", ""),
request.headers,
)
return ProjectIncomingReceipt.model_validate(result)
4 changes: 4 additions & 0 deletions backend/app/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
LoopItemCollaborator,
ProjectAutomationRule,
ProjectAutomationRun,
ProjectIncomingEvent,
ProjectIncomingHook,
)
from app.models.dingtalk_doc import DingtalkSyncedNode
from app.models.im_session import IMPrivateSession, IMSessionMode, IMSessionState
Expand Down Expand Up @@ -80,6 +82,8 @@
"LoopItem",
"LoopItemAttachment",
"LoopItemCollaborator",
"ProjectIncomingEvent",
"ProjectIncomingHook",
"ProjectAutomationRule",
"ProjectAutomationRun",
"Delivery",
Expand Down
12 changes: 12 additions & 0 deletions backend/app/models/delivery.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,18 @@ class ProjectAutomationRun(LoopNode):
__mapper_args__ = {"polymorphic_identity": "automation_run"}


class ProjectIncomingHook(LoopNode):
"""An opaque project endpoint that turns external events into loop items."""

__mapper_args__ = {"polymorphic_identity": "incoming_hook"}


class ProjectIncomingEvent(LoopNode):
"""One deduplicated delivery received by a project incoming hook."""

__mapper_args__ = {"polymorphic_identity": "incoming_event"}


class CloudProjectLocalBinding(LoopNode):
__mapper_args__ = {"polymorphic_identity": "local_binding"}

Expand Down
52 changes: 52 additions & 0 deletions backend/app/schemas/project_incoming_hook.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# SPDX-FileCopyrightText: 2026 Weibo, Inc.
# SPDX-License-Identifier: Apache-2.0

"""Schemas for deterministic project incoming hooks."""

from datetime import datetime
from typing import Literal

from pydantic import BaseModel, ConfigDict, Field, field_validator

IncomingHookStatus = Literal["active", "disabled"]


class ProjectIncomingHookCreate(BaseModel):
name: str = Field(default="外部系统", min_length=1, max_length=100)

@field_validator("name")
@classmethod
def normalize_name(cls, value: str) -> str:
return value.strip()


class ProjectIncomingHookUpdate(BaseModel):
name: str | None = Field(default=None, min_length=1, max_length=100)
status: IncomingHookStatus | None = None
version: int = Field(ge=1)

@field_validator("name")
@classmethod
def normalize_name(cls, value: str | None) -> str | None:
return value.strip() if value is not None else None


class ProjectIncomingHookView(BaseModel):
model_config = ConfigDict(from_attributes=True)

id: str
project_id: str
name: str
status: IncomingHookStatus
webhook_url: str
version: int
created_at: datetime
updated_at: datetime


class ProjectIncomingReceipt(BaseModel):
status: Literal["created", "duplicate", "ignored", "failed"]
provider: str
event_id: str
loop_item_id: str | None = None
reason: str | None = None
Loading
Loading