Initial commit

This commit is contained in:
LZH-YS1998
2026-07-01 17:56:31 +08:00
commit d78931979d
731 changed files with 311088 additions and 0 deletions
+107
View File
@@ -0,0 +1,107 @@
"""Kanban task service shared by Office UI and CLI."""
from __future__ import annotations
import uuid
from typing import Any
from opc.core.models import TaskStatus
from opc.layer2_organization.work_item_transition import apply_task_status_transition
from opc.presentation.kanban import column_to_task_status
from .context import OfficeServiceContext
from .models import ServiceError, ServiceEvent, ServiceResult
from .session import SessionService
class KanbanService:
TERMINAL_STATUSES: set[str] = {"done", "failed", "cancelled"}
def __init__(self, context: OfficeServiceContext, session_service: SessionService) -> None:
self.context = context
self.session_service = session_service
def _reject_system_driven(self) -> None:
if self.context.mode_state.exec_mode in {"company", "org", "custom"}:
raise ServiceError("company_mode_kanban_is_system_driven", "company_mode_kanban_is_system_driven")
async def create_task(self, *, project_id: str, title: str, description: str = "", task_id: str | None = None, board_id: str | None = None, assignee_ids: list[str] | None = None) -> ServiceResult:
self._reject_system_driven()
return await self.session_service.create(
project_id=project_id,
title=title or "Untitled",
description=description,
task_id=task_id or str(uuid.uuid4()),
exec_mode=self.context.mode_state.exec_mode,
company_profile=self.context.mode_state.company_profile,
preferred_agent=self.context.mode_state.task_preferred_agent,
interface="office_ui",
board_id=board_id,
assignee_ids=assignee_ids or [],
)
async def update_task(self, *, project_id: str, task_id: str, updates: dict[str, Any]) -> ServiceResult:
self._reject_system_driven()
engine = await self.context.engine_for_project(project_id)
if task_id and getattr(engine, "store", None):
task = await engine.store.get_task(task_id)
if task:
if "title" in updates:
task.title = updates["title"]
if "description" in updates:
task.description = updates["description"]
if "tags" in updates:
task.tags = updates["tags"]
await engine.store.save_task(task)
payload = {"project_id": self.context.normalize_project_id(project_id), "task_id": task_id}
return ServiceResult(payload, [ServiceEvent("kanban_updated", payload)])
async def move_task(self, *, project_id: str, task_id: str, column_id: str) -> ServiceResult:
self._reject_system_driven()
engine = await self.context.engine_for_project(project_id)
if task_id and column_id and getattr(engine, "store", None):
task = await engine.store.get_task(task_id)
if task:
current = task.status.value if hasattr(task.status, "value") else str(task.status)
new_status = column_to_task_status(column_id)
if not new_status:
raise ServiceError("invalid_column", f"Invalid column/status: {column_id}")
target = new_status.value if hasattr(new_status, "value") else str(new_status)
if current in self.TERMINAL_STATUSES and target not in self.TERMINAL_STATUSES:
raise ServiceError("terminal_task", f"Cannot move {current} task back to {column_id}")
if current != target:
task.status = new_status
await engine.store.save_task(task)
payload = {"project_id": self.context.normalize_project_id(project_id), "task_id": task_id, "display_id": "", "column_name": column_id}
return ServiceResult(payload, [ServiceEvent("board_task_moved", payload)])
async def delete_task(self, *, project_id: str, task_id: str) -> ServiceResult:
self._reject_system_driven()
engine = await self.context.engine_for_project(project_id)
if task_id and getattr(engine, "store", None):
task = await engine.store.get_task(task_id)
if task:
await apply_task_status_transition(
engine.store,
task,
target_status_or_phase=TaskStatus.CANCELLED,
reason="kanban_delete_task",
release_claim=True,
)
payload = {"project_id": self.context.normalize_project_id(project_id), "task_id": task_id}
return ServiceResult(payload, [ServiceEvent("kanban_updated", payload)])
async def assign(self, *, project_id: str, task_id: str, agent_id: str) -> ServiceResult:
self._reject_system_driven()
engine = await self.context.engine_for_project(project_id)
if task_id and agent_id and getattr(engine, "store", None):
task = await engine.store.get_task(task_id)
if task:
agent = await self.context.agent_store._get_one(agent_id)
role_id = agent.get("opc_role_id", agent_id) if agent else agent_id
task.assigned_to = role_id
await engine.store.save_task(task)
return ServiceResult({"project_id": self.context.normalize_project_id(project_id), "task_id": task_id, "agent_id": agent_id})
async def status(self, *, project_id: str, task_id: str, status: str) -> ServiceResult:
return await self.move_task(project_id=project_id, task_id=task_id, column_id=status)