commit 4f9571b21ce255ea165ad6aab04c9360d425bc20 Author: ssdfasd <2156608475@qq.com> Date: Tue Mar 10 18:27:44 2026 +0800 feat: initial XCClaw基础架构 - 基于 FastAPI 的 Web API 服务 - OpenCode API 客户端封装 - 会话管理器(同步/异步任务执行) - APScheduler 定时任务调度 - 完整的 REST API 端点 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3848e6d --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +__pycache__/ +*.pyc +*.pyo +.env +.venv +venv/ diff --git a/app/api/__init__.py b/app/api/__init__.py new file mode 100644 index 0000000..ef7518b --- /dev/null +++ b/app/api/__init__.py @@ -0,0 +1 @@ +from app.api.routes import router diff --git a/app/api/routes.py b/app/api/routes.py new file mode 100644 index 0000000..a5ddb4e --- /dev/null +++ b/app/api/routes.py @@ -0,0 +1,72 @@ +from fastapi import APIRouter, HTTPException +from app.models.session import CreateSessionRequest, SessionType +from app.services.session_manager import session_manager +from app.services.scheduler import scheduler_service, ScheduleTask +from app.services.opencode_client import opencode_client + + +router = APIRouter(prefix="/api/xcclaw", tags=["xcclaw"]) + + +@router.get("/health") +async def health_check(): + try: + opencode_status = await opencode_client.health_check() + return {"status": "ok", "opencode": opencode_status} + except Exception as e: + return {"status": "error", "message": str(e)} + + +@router.post("/task") +async def create_task(request: CreateSessionRequest): + task = await session_manager.create_task(request) + return task + + +@router.post("/task/{task_id}/execute") +async def execute_task(task_id: str): + result = await session_manager.execute_task(task_id) + return result + + +@router.post("/task/{task_id}/execute_async") +async def execute_task_async(task_id: str): + await session_manager.execute_task_async(task_id) + return {"status": "started", "task_id": task_id} + + +@router.post("/task/{task_id}/abort") +async def abort_task(task_id: str): + result = await session_manager.abort_task(task_id) + return {"aborted": result} + + +@router.get("/task/{task_id}") +async def get_task(task_id: str): + task = await session_manager.get_task(task_id) + if not task: + raise HTTPException(status_code=404, detail="Task not found") + return task + + +@router.get("/task") +async def list_tasks(): + return await session_manager.list_tasks() + + +@router.post("/schedule") +async def create_schedule(task: ScheduleTask): + return await scheduler_service.add_schedule(task) + + +@router.get("/schedule") +async def list_schedules(): + return await scheduler_service.list_schedules() + + +@router.delete("/schedule/{schedule_id}") +async def delete_schedule(schedule_id: str): + result = await scheduler_service.remove_schedule(schedule_id) + if not result: + raise HTTPException(status_code=404, detail="Schedule not found") + return {"deleted": True} diff --git a/app/core/__init__.py b/app/core/__init__.py new file mode 100644 index 0000000..d25d462 --- /dev/null +++ b/app/core/__init__.py @@ -0,0 +1,4 @@ +from app.core.config import settings +from app.core.logging import logger + +__all__ = ["settings", "logger"] diff --git a/app/core/config.py b/app/core/config.py new file mode 100644 index 0000000..cddaeb5 --- /dev/null +++ b/app/core/config.py @@ -0,0 +1,19 @@ +from pydantic_settings import BaseSettings +from pathlib import Path + + +class Settings(BaseSettings): + opencode_host: str = "127.0.0.1" + opencode_port: int = 4096 + opencode_password: str = "" + + app_host: str = "0.0.0.0" + app_port: int = 3005 + + data_dir: Path = Path.home() / "Documents" / "XCDesktop" / "xcclaw" + + class Config: + env_prefix = "XCCLAW_" + + +settings = Settings() diff --git a/app/core/logging.py b/app/core/logging.py new file mode 100644 index 0000000..87a48d5 --- /dev/null +++ b/app/core/logging.py @@ -0,0 +1,14 @@ +import logging +import sys + + +def setup_logging(): + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + handlers=[logging.StreamHandler(sys.stdout)], + ) + return logging.getLogger("xcclaw") + + +logger = setup_logging() diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..3be8ec3 --- /dev/null +++ b/app/main.py @@ -0,0 +1,37 @@ +from contextlib import asynccontextmanager +from fastapi import FastAPI +from app.api.routes import router +from app.services.scheduler import scheduler_service +from app.services.opencode_client import opencode_client +from app.core.config import settings +from app.core.logging import logger + + +@asynccontextmanager +async def lifespan(app: FastAPI): + logger.info("Starting XCClaw server...") + scheduler_service.start() + yield + logger.info("Shutting down XCClaw server...") + scheduler_service.shutdown() + await opencode_client.close() + + +app = FastAPI( + title="XCClaw", + description="基于 OpenCode Agent 的任务调度系统", + version="0.1.0", + lifespan=lifespan, +) + +app.include_router(router) + + +if __name__ == "__main__": + import uvicorn + uvicorn.run( + "app.main:app", + host=settings.app_host, + port=settings.app_port, + reload=True, + ) diff --git a/app/models/__init__.py b/app/models/__init__.py new file mode 100644 index 0000000..7ef0e90 --- /dev/null +++ b/app/models/__init__.py @@ -0,0 +1 @@ +from app.models.session import SessionType, TaskStatus, CreateSessionRequest, Task diff --git a/app/models/session.py b/app/models/session.py new file mode 100644 index 0000000..883f552 --- /dev/null +++ b/app/models/session.py @@ -0,0 +1,30 @@ +from enum import Enum +from pydantic import BaseModel + + +class SessionType(str, Enum): + EPHEMERAL = "ephemeral" + PERSISTENT = "persistent" + SCHEDULED = "scheduled" + + +class TaskStatus(str, Enum): + PENDING = "pending" + RUNNING = "running" + COMPLETED = "completed" + FAILED = "failed" + + +class CreateSessionRequest(BaseModel): + type: SessionType = SessionType.EPHEMERAL + title: str | None = None + prompt: str | None = None + + +class Task(BaseModel): + id: str + type: SessionType + prompt: str + status: TaskStatus = TaskStatus.PENDING + session_id: str | None = None + schedule: str | None = None diff --git a/app/services/__init__.py b/app/services/__init__.py new file mode 100644 index 0000000..e6b1569 --- /dev/null +++ b/app/services/__init__.py @@ -0,0 +1,3 @@ +from app.services.opencode_client import opencode_client +from app.services.session_manager import session_manager +from app.services.scheduler import scheduler_service diff --git a/app/services/opencode_client.py b/app/services/opencode_client.py new file mode 100644 index 0000000..d9e7c3b --- /dev/null +++ b/app/services/opencode_client.py @@ -0,0 +1,88 @@ +from typing import Any +import httpx +from app.core.config import settings +from app.core.logging import logger + + +class OpenCodeClient: + def __init__(self): + self.base_url = f"http://{settings.opencode_host}:{settings.opencode_port}" + self._client: httpx.AsyncClient | None = None + + async def _get_client(self) -> httpx.AsyncClient: + if self._client is None: + headers = {} + if settings.opencode_password: + headers["Authorization"] = f"Bearer {settings.opencode_password}" + self._client = httpx.AsyncClient( + base_url=self.base_url, + headers=headers, + timeout=300.0, + ) + return self._client + + async def close(self): + if self._client: + await self._client.aclose() + self._client = None + + async def health_check(self) -> dict[str, Any]: + client = await self._get_client() + resp = await client.get("/global/health") + resp.raise_for_status() + return resp.json() + + async def create_session(self, title: str | None = None, parent_id: str | None = None) -> dict[str, Any]: + client = await self._get_client() + payload = {} + if title: + payload["title"] = title + if parent_id: + payload["parentID"] = parent_id + resp = await client.post("/session", json=payload) + resp.raise_for_status() + return resp.json() + + async def get_session(self, session_id: str) -> dict[str, Any]: + client = await self._get_client() + resp = await client.get(f"/session/{session_id}") + resp.raise_for_status() + return resp.json() + + async def delete_session(self, session_id: str) -> bool: + client = await self._get_client() + resp = await client.delete(f"/session/{session_id}") + resp.raise_for_status() + return resp.json() + + async def send_message(self, session_id: str, text: str) -> dict[str, Any]: + client = await self._get_client() + payload = { + "parts": [{"type": "text", "text": text}] + } + resp = await client.post(f"/session/{session_id}/message", json=payload) + resp.raise_for_status() + return resp.json() + + async def send_message_async(self, session_id: str, text: str) -> None: + client = await self._get_client() + payload = { + "parts": [{"type": "text", "text": text}] + } + resp = await client.post(f"/session/{session_id}/prompt_async", json=payload) + resp.raise_for_status() + + async def abort_session(self, session_id: str) -> bool: + client = await self._get_client() + resp = await client.post(f"/session/{session_id}/abort") + resp.raise_for_status() + return resp.json() + + async def get_session_status(self) -> dict[str, Any]: + client = await self._get_client() + resp = await client.get("/session/status") + resp.raise_for_status() + return resp.json() + + +opencode_client = OpenCodeClient() diff --git a/app/services/scheduler.py b/app/services/scheduler.py new file mode 100644 index 0000000..6897609 --- /dev/null +++ b/app/services/scheduler.py @@ -0,0 +1,79 @@ +import uuid +from pydantic import BaseModel +from apscheduler.schedulers.asyncio import AsyncIOScheduler +from apscheduler.triggers.cron import CronTrigger +from app.services.session_manager import session_manager +from app.core.logging import logger + + +class ScheduleTask(BaseModel): + id: str + name: str + cron: str + prompt: str + enabled: bool = True + + +class SchedulerService: + def __init__(self): + self.scheduler = AsyncIOScheduler() + self.scheduled_tasks: dict[str, ScheduleTask] = {} + + def start(self): + self.scheduler.start() + logger.info("Scheduler started") + + def shutdown(self): + self.scheduler.shutdown() + logger.info("Scheduler shutdown") + + async def add_schedule(self, task: ScheduleTask) -> ScheduleTask: + self.scheduled_tasks[task.id] = task + + if task.enabled: + trigger = CronTrigger.from_crontab(task.cron) + self.scheduler.add_job( + self._run_scheduled_task, + trigger, + args=[task.id], + id=task.id, + ) + logger.info(f"Added schedule: {task.name} ({task.cron})") + + return task + + async def remove_schedule(self, task_id: str) -> bool: + task = self.scheduled_tasks.pop(task_id, None) + if not task: + return False + + self.scheduler.remove_job(task_id) + logger.info(f"Removed schedule: {task.name}") + return True + + async def list_schedules(self) -> list[ScheduleTask]: + return list(self.scheduled_tasks.values()) + + async def _run_scheduled_task(self, task_id: str): + task = self.scheduled_tasks.get(task_id) + if not task: + return + + logger.info(f"Running scheduled task: {task.name}") + + from app.models.session import CreateSessionRequest, SessionType + + request = CreateSessionRequest( + type=SessionType.SCHEDULED, + prompt=task.prompt, + ) + task_obj = await session_manager.create_task(request) + + try: + await session_manager.execute_task_async(task_obj.id) + logger.info(f"Scheduled task {task.name} started") + except Exception as e: + logger.error(f"Scheduled task {task.name} failed to start: {e}") + + +scheduler_service = SchedulerService() diff --git a/app/services/session_manager.py b/app/services/session_manager.py new file mode 100644 index 0000000..8d1b144 --- /dev/null +++ b/app/services/session_manager.py @@ -0,0 +1,77 @@ +import uuid +from app.models.session import SessionType, Task, TaskStatus, CreateSessionRequest +from app.services.opencode_client import opencode_client +from app.core.logging import logger + + +class SessionManager: + def __init__(self): + self.tasks: dict[str, Task] = {} + + async def create_task(self, request: CreateSessionRequest) -> Task: + task_id = str(uuid.uuid4()) + task = Task( + id=task_id, + type=request.type, + prompt=request.prompt or "", + ) + self.tasks[task_id] = task + logger.info(f"Created task: {task_id}") + return task + + async def execute_task(self, task_id: str) -> dict: + task = self.tasks.get(task_id) + if not task: + raise ValueError(f"Task {task_id} not found") + + task.status = TaskStatus.RUNNING + session = await opencode_client.create_session( + title=task.prompt[:50] if task.prompt else None + ) + session_id = session["id"] + task.session_id = session_id + + logger.info(f"Executing task {task_id} with session {session_id}") + + try: + result = await opencode_client.send_message(session_id, task.prompt) + task.status = TaskStatus.COMPLETED + return result + except Exception as e: + task.status = TaskStatus.FAILED + logger.error(f"Task {task_id} failed: {e}") + raise + + async def execute_task_async(self, task_id: str) -> None: + task = self.tasks.get(task_id) + if not task: + raise ValueError(f"Task {task_id} not found") + + task.status = TaskStatus.RUNNING + session = await opencode_client.create_session( + title=task.prompt[:50] if task.prompt else None + ) + session_id = session["id"] + task.session_id = session_id + + logger.info(f"Executing async task {task_id} with session {session_id}") + + await opencode_client.send_message_async(session_id, task.prompt) + + async def abort_task(self, task_id: str) -> bool: + task = self.tasks.get(task_id) + if not task or not task.session_id: + return False + + result = await opencode_client.abort_session(task.session_id) + task.status = TaskStatus.FAILED + return result + + async def get_task(self, task_id: str) -> Task | None: + return self.tasks.get(task_id) + + async def list_tasks(self) -> list[Task]: + return list(self.tasks.values()) + + +session_manager = SessionManager() diff --git a/opencode-api.md b/opencode-api.md new file mode 100644 index 0000000..e9bc32d --- /dev/null +++ b/opencode-api.md @@ -0,0 +1,200 @@ +# OpenCode HTTP API 文档 + +本文档整理了 OpenCode HTTP Server 的所有 API 端点。 + +## 启动服务 + +```bash +opencode serve --port 4096 --hostname 127.0.0.1 +``` + +### 选项 + +| 参数 | 描述 | 默认值 | +|------|------|--------| +| `--port` | 监听端口 | 4096 | +| `--hostname` | 监听主机名 | 127.0.0.1 | +| `--cors` | 允许的浏览器来源 | [] | + +### 认证 + +设置环境变量启用 HTTP 基本认证: + +```bash +OPENCODE_SERVER_PASSWORD=your-password opencode serve +``` + +## Global + +| 方法 | 路径 | 描述 | 响应 | +|------|------|------|------| +| GET | `/global/health` | 获取服务器健康状态和版本 | `{ healthy: true, version: string }` | +| GET | `/global/event` | 获取全局事件 (SSE 流) | Event stream | + +## Project + +| 方法 | 路径 | 描述 | 响应 | +|------|------|------|------| +| GET | `/project` | 列出所有项目 | `Project[]` | +| GET | `/project/current` | 获取当前项目 | `Project` | + +## Path & VCS + +| 方法 | 路径 | 描述 | 响应 | +|------|------|------|------| +| GET | `/path` | 获取当前路径 | `Path` | +| GET | `/vcs` | 获取当前项目的 VCS 信息 | `VcsInfo` | + +## Instance + +| 方法 | 路径 | 描述 | 响应 | +|------|------|------|------| +| POST | `/instance/dispose` | 释放当前实例 | `boolean` | + +## Config + +| 方法 | 路径 | 描述 | 响应 | +|------|------|------|------| +| GET | `/config` | 获取配置信息 | `Config` | +| PATCH | `/config` | 更新配置 | `Config` | +| GET | `/config/providers` | 列出 providers 和默认模型 | `{ providers: Provider[], default: { [key: string]: string } }` | + +## Provider + +| 方法 | 路径 | 描述 | 响应 | +|------|------|------|------| +| GET | `/provider` | 列出所有 providers | `{ all: Provider[], default: {...}, connected: string[] }` | +| GET | `/provider/auth` | 获取 provider 认证方法 | `{ [providerID: string]: ProviderAuthMethod[] }` | +| POST | `/provider/{id}/oauth/authorize` | 使用 OAuth 授权 provider | `ProviderAuthAuthorization` | +| POST | `/provider/{id}/oauth/callback` | 处理 OAuth 回调 | `boolean` | + +## Sessions + +| 方法 | 路径 | 描述 | 备注 | +|------|------|------|------| +| GET | `/session` | 列出所有会话 | 返回 `Session[]` | +| POST | `/session` | 创建新会话 | body: `{ parentID?, title? }`, 返回 `Session` | +| GET | `/session/status` | 获取所有会话状态 | 返回 `{ [sessionID: string]: SessionStatus }` | +| GET | `/session/:id` | 获取会话详情 | 返回 `Session` | +| DELETE | `/session/:id` | 删除会话及所有数据 | 返回 `boolean` | +| PATCH | `/session/:id` | 更新会话属性 | body: `{ title? }`, 返回 `Session` | +| GET | `/session/:id/children` | 获取会话的子会话 | 返回 `Session[]` | +| GET | `/session/:id/todo` | 获取会话的待办列表 | 返回 `Todo[]` | +| POST | `/session/:id/init` | 分析应用并创建 AGENTS.md | body: `{ messageID, providerID, modelID }`, 返回 `boolean` | +| POST | `/session/:id/fork` | 在某条消息处 fork 会话 | body: `{ messageID? }`, 返回 `Session` | +| POST | `/session/:id/abort` | 中止运行中的会话 | 返回 `boolean` | +| POST | `/session/:id/share` | 分享会话 | 返回 `Session` | +| DELETE | `/session/:id/share` | 取消分享会话 | 返回 `Session` | +| GET | `/session/:id/diff` | 获取会话的 diff | query: `messageID?`, 返回 `FileDiff[]` | +| POST | `/session/:id/summarize` | 总结会话 | body: `{ providerID, modelID }`, 返回 `boolean` | +| POST | `/session/:id/revert` | 还原消息 | body: `{ messageID, partID? }`, 返回 `boolean` | +| POST | `/session/:id/unrevert` | 恢复所有还原的消息 | 返回 `boolean` | +| POST | `/session/:id/permissions/:permissionID` | 响应权限请求 | body: `{ response, remember? }`, 返回 `boolean` | + +## Messages + +| 方法 | 路径 | 描述 | 备注 | +|------|------|------|------| +| GET | `/session/:id/message` | 列出会话中的消息 | query: `limit?`, 返回 `{ info: Message, parts: Part[] }[]` | +| POST | `/session/:id/message` | 发送消息并等待响应 | body: `{ messageID?, model?, agent?, noReply?, system?, tools?, parts }`, 返回 `{ info: Message, parts: Part[] }` | +| GET | `/session/:id/message/:messageID` | 获取消息详情 | 返回 `{ info: Message, parts: Part[] }` | +| POST | `/session/:id/prompt_async` | 异步发送消息(不等待) | body: 同 `/session/:id/message`, 返回 `204 No Content` | +| POST | `/session/:id/command` | 执行斜杠命令 | body: `{ messageID?, agent?, model?, command, arguments }`, 返回 `{ info: Message, parts: Part[] }` | +| POST | `/session/:id/shell` | 运行 shell 命令 | body: `{ agent, model?, command }`, 返回 `{ info: Message, parts: Part[] }` | + +## Commands + +| 方法 | 路径 | 描述 | 响应 | +|------|------|------|------| +| GET | `/command` | 列出所有命令 | `Command[]` | + +## Files + +| 方法 | 路径 | 描述 | 响应 | +|------|------|------|------| +| GET | `/find?pattern=` | 在文件中搜索文本 | 包含 `path`, `lines`, `line_number`, `absolute_offset`, `submatches` 的匹配对象数组 | +| GET | `/find/file?query=` | 按名称查找文件和目录 | `string[]` (paths) | +| GET | `/find/symbol?query=` | 查找工作区符号 | `Symbol[]` | +| GET | `/file?path=` | 列出文件和目录 | `FileNode[]` | +| GET | `/file/content?path=

` | 读取文件 | `FileContent` | +| GET | `/file/status` | 获取跟踪文件的状态 | `File[]` | + +### `/find/file` 查询参数 + +- `query` (必需) - 搜索字符串(模糊匹配) +- `type` (可选) - 限制结果为 `"file"` 或 `"directory"` +- `directory` (可选) - 覆盖搜索的项目根目录 +- `limit` (可选) - 最大结果数 (1-200) +- `dirs` (可选) - 传统标志 (`"false"` 仅返回文件) + +## Tools (Experimental) + +| 方法 | 路径 | 描述 | 响应 | +|------|------|------|------| +| GET | `/experimental/tool/ids` | 列出所有工具 ID | `ToolIDs` | +| GET | `/experimental/tool?provider=

&model=` | 列出模型的 JSON schema 工具 | `ToolList` | + +## LSP, Formatters & MCP + +| 方法 | 路径 | 描述 | 响应 | +|------|------|------|------| +| GET | `/lsp` | 获取 LSP 服务器状态 | `LSPStatus[]` | +| GET | `/formatter` | 获取格式化程序状态 | `FormatterStatus[]` | +| GET | `/mcp` | 获取 MCP 服务器状态 | `{ [name: string]: MCPStatus }` | +| POST | `/mcp` | 动态添加 MCP 服务器 | body: `{ name, config }`, 返回 MCP status object | + +## Agents + +| 方法 | 路径 | 描述 | 响应 | +|------|------|------|------| +| GET | `/agent` | 列出所有可用代理 | `Agent[]` | + +## Logging + +| 方法 | 路径 | 描述 | 响应 | +|------|------|------|------| +| POST | `/log` | 写入日志条目 | body: `{ service, level, message, extra? }`, 返回 `boolean` | + +## TUI + +| 方法 | 路径 | 描述 | 响应 | +|------|------|------|------| +| POST | `/tui/append-prompt` | 将文本追加到提示符 | `boolean` | +| POST | `/tui/open-help` | 打开帮助对话框 | `boolean` | +| POST | `/tui/open-sessions` | 打开会话选择器 | `boolean` | +| POST | `/tui/open-themes` | 打开主题选择器 | `boolean` | +| POST | `/tui/open-models` | 打开模型选择器 | `boolean` | +| POST | `/tui/submit-prompt` | 提交当前提示符 | `boolean` | +| POST | `/tui/clear-prompt` | 清除提示符 | `boolean` | +| POST | `/tui/execute-command` | 执行命令 (`{ command }`) | `boolean` | +| POST | `/tui/show-toast` | 显示通知 (`{ title?, message, variant }`) | `boolean` | +| GET | `/tui/control/next` | 等待下一个控制请求 | Control request object | +| POST | `/tui/control/response` | 响应控制请求 (`{ body }`) | `boolean` | + +## Auth + +| 方法 | 路径 | 描述 | 响应 | +|------|------|------|------| +| PUT | `/auth/:id` | 设置认证凭据 | body 必须匹配 provider schema, 返回 `boolean` | + +## Events + +| 方法 | 路径 | 描述 | 响应 | +|------|------|------|------| +| GET | `/event` | Server-Sent Events 流 | 第一个事件是 `server.connected`, 然后是 bus events | + +## Docs + +| 方法 | 路径 | 描述 | 响应 | +|------|------|------|------| +| GET | `/doc` | OpenAPI 3.1 规范 | 带有 OpenAPI 规范的 HTML 页面 | + +## 查看 OpenAPI 规范 + +服务器发布 OpenAPI 3.1 规范,可通过以下地址查看: + +``` +http://:/doc +``` + +例如:`http://localhost:4096/doc` diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d67c0ff --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +fastapi>=0.109.0 +uvicorn>=0.27.0 +httpx>=0.26.0 +apscheduler>=3.10.4 +pydantic>=2.5.0 +pydantic-settings>=2.1.0 +python-dotenv>=1.0.0 diff --git a/xcclaw-system.md b/xcclaw-system.md new file mode 100644 index 0000000..9d5c789 --- /dev/null +++ b/xcclaw-system.md @@ -0,0 +1,332 @@ +# XCClaw 系统设计 + +## 一、系统定位 + +XCClaw 基于 OpenCode Agent,是桌面本地的个人 AI 助手 + +**核心理念**:以 OpenCode 会话为最小执行单元,通过多种触发方式调度任务,实现本地化的 AI 任务执行系统。 + +--- + +## 二、核心概念 + +### 最小执行单元:OpenCode 会话 + +1 个会话 = 1 个独立的工作空间 + 1 个对话上下文 + 执行能力 + +``` +会话生命周期: +创建 → 加载上下文 → 执行 Prompt → 流式输出 → 返回结果 → 销毁/保留 +``` + +--- + +## 三、调度模式 + +| 模式 | 描述 | 触发方式 | 适用场景 | +|------|------|----------|----------| +| **即时模式** | 立即创建会话,执行完销毁 | 快捷键/按钮 | 简单询问、快速任务 | +| **常驻模式** | 启动持久会话,可多次交互 | 托盘/面板 | 持续对话、复杂任务 | +| **定时模式** | 定时创建会话执行 | Cron 配置 | 定期数据抓取、报告生成 | +| **队列模式** | 任务进入队列,依次执行 | API/面板 | 批量处理、长链任务 | + +--- + +## 四、会话类型 + +### 1. 即时会话(Ephemeral Session) + +- 每次任务创建新会话 +- 执行完成后自动销毁 +- 适合:简单任务、一次性操作 +- 资源占用:低 + +### 2. 常驻会话(Persistent Session) + +- 启动后保持运行状态 +- 可多次交互,累积上下文 +- 适合:复杂项目、多轮对话 +- 资源占用:高 + +### 3. 定时会话(Scheduled Session) + +- 由定时器触发创建 +- 可配置是否保留会话 +- 适合:周期性任务 +- 资源占用:按需 + +--- + +## 五、任务调度 + +### 5.1 即时任务调度 + +``` +用户触发 → 创建会话 → 执行 → 返回结果 → 销毁会话 +``` + +**示例**: +- 用户按下 Ctrl+Shift+C 快捷键 +- 弹出输入框,输入 "帮我整理桌面文件" +- 系统创建即时会话执行 +- 完成后显示结果,自动销毁会话 + +### 5.2 定时任务调度 + +``` +Cron 触发 → 创建会话 → 执行 → 桌面通知 → 销毁/保留会话 +``` + +**配置示例**: + +```json +{ + "name": "晨间新闻摘要", + "trigger": "0 9 * * *", + "prompt": "帮我抓取今天的技术新闻摘要", + "sessionType": "ephemeral", + "notifyOnComplete": true +} +``` + +### 5.3 任务链调度 + +``` +任务A → 结果传递给任务B → 结果传递给任务C → 汇总结果 +``` + +**示例场景**:整理项目代码结构 + +``` +Step 1: 会话A执行 "列出 src 目录下所有文件" +Step 2: 会话B继承上下文 "分析 main.ts 的核心逻辑" +Step 3: 会话C继承上下文 "生成代码结构文档" +Step 4: 汇总结果 → 桌面通知用户 +``` + +**关键设计点**: + +- 链式调用:会话 N 的输出作为会话 N+1 的输入 +- 上下文传递:可选择继承上一个会话的上下文,或开启全新会话 +- 错误处理:某步骤失败可选择重试或跳过 + +### 5.4 队列调度 + +``` +用户提交多个任务 + │ + ▼ +┌─────────────────┐ +│ 任务队列 │ +│ [1] [2] [3]... │ ← 按优先级/时间排序 +└────────┬────────┘ + │ + ▼ 串行/并行执行 +┌─────────────────┐ +│ 执行单元 │ ← OpenCode 会话 +│ 会话A → 完成 │ +└────────┬────────┘ + │ + ▼ +┌─────────────────┐ +│ 任务2 开始 │ +│ 会话B → ... │ +└─────────────────┘ +``` + +--- + +## 六、触发入口 + +| 入口 | 操作 | 行为 | +|------|------|------| +| **快捷键** | 按下 Ctrl+Shift+C | 弹出输入框 → 创建即时会话 | +| **托盘图标** | 点击"新对话" | 创建常驻会话 | +| **Web 面板** | 点击"执行"按钮 | 根据配置创建会话 | +| **定时任务** | Cron 触发 | 创建临时会话,执行完销毁 | +| **外部 API** | POST /api/task | 根据 payload 创建会话 | + +--- + +## 七、OpenCode 服务集成 + +### 7.1 服务启动 + +```bash +# 启动 OpenCode HTTP 服务 +opencode serve --port 4096 --hostname 127.0.0.1 +``` + +服务默认端口 4096,支持参数: +- `--port`: 端口号 +- `--hostname`: 监听地址 +- `--cors`: 允许跨域访问(可多次指定) + +### 7.2 会话管理 API + +OpenCode 服务器提供 REST API 进行会话管理: + +| 方法 | 路径 | 说明 | +|------|------|------| +| `GET` | `/api/xcclaw/session` | 列出所有会话 | +| `POST` | `/api/xcclaw/session` | 创建新会话 (`{ parentID?, title? }`) | +| `GET` | `/api/xcclaw/session/:id` | 获取会话详情 | +| `DELETE` | `/api/xcclaw/session/:id` | 删除会话 | +| `PATCH` | `/api/xcclaw/session/:id` | 更新会话属性 | +| `GET` | `/api/xcclaw/session/:id/children` | 获取子会话 | +| `POST` | `/api/xcclaw/session/:id/abort` | 中止运行中的会话 | +| `POST` | `/api/xcclaw/session/:id/fork` | 叉一个会话 | + +### 7.3 消息交互 API + +| 方法 | 路径 | 说明 | +|------|------|------| +| `POST` | `/api/xcclaw/session/:id/message` | 发送消息并等待响应 | +| `POST` | `/api/xcclaw/session/:id/prompt_async` | 异步发送消息(不等待响应) | +| `GET` | `/api/xcclaw/session/:id/message` | 列出消息 (`?limit=50`) | +| `GET` | `/api/xcclaw/session/:id/message/:messageID` | 获取消息详情 | +| `POST` | `/api/xcclaw/session/:id/command` | 执行 slash 命令 | +| `POST` | `/api/xcclaw/session/:id/shell` | 运行 shell 命令 | + +### 7.4 会话调用示例 + +```bash +# 1. 创建会话 +curl -X POST http://localhost:4096/api/xcclaw/session \ + -H "Content-Type: application/json" \ + -d '{"title": "整理桌面文件"}' + +# 2. 发送消息执行任务 +curl -X POST http://localhost:4096/api/xcclaw/session/{session-id}/message \ + -H "Content-Type: application/json" \ + -d '{"parts": [{"type": "text", "text": "帮我整理桌面文件"}]}' + +# 3. 异步任务(不等待响应) +curl -X POST http://localhost:4096/api/xcclaw/session/{session-id}/prompt_async \ + -H "Content-Type: application/json" \ + -d '{"parts": [{"type": "text", "text": "执行复杂任务"}]}' + +# 4. 查询会话状态 +curl http://localhost:4096/api/xcclaw/session/status + +# 5. 中止会话 +curl -X POST http://localhost:4096/api/xcclaw/session/{session-id}/abort +``` + +### 7.5 任务调度与 OpenCode 会话映射 + +``` +即时任务 → POST /api/xcclaw/session → POST /api/xcclaw/session/:id/message → 销毁 +常驻任务 → POST /api/xcclaw/session → 保留 sessionID → 多次 POST /api/xcclaw/session/:id/message +定时任务 → Cron → POST /api/xcclaw/session → POST /api/xcclaw/session/:id/prompt_async → 通知 +``` + +--- + +## 八、结果反馈 + +| 任务类型 | 反馈方式 | +|----------|----------| +| 简单任务 | 面板直接显示 | +| 耗时任务 | 进度条 + 实时输出 | +| 定时任务 | 桌面通知 | +| 长链任务 | 每个步骤结果 + 最终汇总 | + +--- + +## 九、持久化设计 + +``` +~/Documents/XCDesktop/xcclaw/ +├── tasks/ +│ ├── pending.json # 待执行任务 +│ ├── running.json # 正在执行 +│ └── history.json # 执行历史 +├── sessions/ +│ ├── {session-id}.json # 会话上下文 +│ └── config.json # 会话配置 +├── schedules/ +│ └── crontasks.json # 定时任务配置 +├── workspace/ # 工作目录 +│ ├── AGENTS.md +│ ├── TOOLS.md +│ └── SOUL.md +└── config.json # 系统配置 +``` + +--- + +## 十、核心 API 设计 + +### 10.1 会话管理 + +```typescript +// 创建会话 +POST /api/xcclaw/session +{ + type: "ephemeral" | "persistent" | "scheduled", + prompt?: string, + context?: object, + options?: { + model?: string, + tools?: string[] + } +} + +// 获取会话状态 +GET /api/xcclaw/session/:id + +// 发送消息 +POST /api/xcclaw/session/:id/message +{ + content: string +} + +// 终止会话 +POST /api/xcclaw/session/:id/abort +``` + +### 10.2 任务调度 + +```typescript +// 提交任务 +POST /api/xcclaw/task +{ + type: "immediate" | "persistent" | "scheduled", + prompt: string, + schedule?: string, // cron 表达式 + chain?: ChainConfig, // 任务链配置 + options?: TaskOptions +} + +// 获取任务状态 +GET /api/xcclaw/task/:id + +// 取消任务 +DELETE /api/xcclaw/task/:id +``` + +### 10.3 定时任务 + +```typescript +// 创建定时任务 +POST /api/xcclaw/schedule +{ + name: string, + cron: string, + prompt: string, + sessionType: "ephemeral" | "persistent", + notifyOnComplete: boolean +} + +// 列出定时任务 +GET /api/xcclaw/schedules + +// 删除定时任务 +DELETE /api/xcclaw/schedule/:id +``` + +--- + +--- +