feat: initial XCClaw基础架构
- 基于 FastAPI 的 Web API 服务 - OpenCode API 客户端封装 - 会话管理器(同步/异步任务执行) - APScheduler 定时任务调度 - 完整的 REST API 端点
This commit is contained in:
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
.env
|
||||
.venv
|
||||
venv/
|
||||
1
app/api/__init__.py
Normal file
1
app/api/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from app.api.routes import router
|
||||
72
app/api/routes.py
Normal file
72
app/api/routes.py
Normal file
@@ -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}
|
||||
4
app/core/__init__.py
Normal file
4
app/core/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from app.core.config import settings
|
||||
from app.core.logging import logger
|
||||
|
||||
__all__ = ["settings", "logger"]
|
||||
19
app/core/config.py
Normal file
19
app/core/config.py
Normal file
@@ -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()
|
||||
14
app/core/logging.py
Normal file
14
app/core/logging.py
Normal file
@@ -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()
|
||||
37
app/main.py
Normal file
37
app/main.py
Normal file
@@ -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,
|
||||
)
|
||||
1
app/models/__init__.py
Normal file
1
app/models/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from app.models.session import SessionType, TaskStatus, CreateSessionRequest, Task
|
||||
30
app/models/session.py
Normal file
30
app/models/session.py
Normal file
@@ -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
|
||||
3
app/services/__init__.py
Normal file
3
app/services/__init__.py
Normal file
@@ -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
|
||||
88
app/services/opencode_client.py
Normal file
88
app/services/opencode_client.py
Normal file
@@ -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()
|
||||
79
app/services/scheduler.py
Normal file
79
app/services/scheduler.py
Normal file
@@ -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()
|
||||
77
app/services/session_manager.py
Normal file
77
app/services/session_manager.py
Normal file
@@ -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()
|
||||
200
opencode-api.md
Normal file
200
opencode-api.md
Normal file
@@ -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=<pat>` | 在文件中搜索文本 | 包含 `path`, `lines`, `line_number`, `absolute_offset`, `submatches` 的匹配对象数组 |
|
||||
| GET | `/find/file?query=<q>` | 按名称查找文件和目录 | `string[]` (paths) |
|
||||
| GET | `/find/symbol?query=<q>` | 查找工作区符号 | `Symbol[]` |
|
||||
| GET | `/file?path=<path>` | 列出文件和目录 | `FileNode[]` |
|
||||
| GET | `/file/content?path=<p>` | 读取文件 | `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=<p>&model=<m>` | 列出模型的 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://<hostname>:<port>/doc
|
||||
```
|
||||
|
||||
例如:`http://localhost:4096/doc`
|
||||
7
requirements.txt
Normal file
7
requirements.txt
Normal file
@@ -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
|
||||
332
xcclaw-system.md
Normal file
332
xcclaw-system.md
Normal file
@@ -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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user