# OpenAI 代理服务设计方案

## 概述

利用 `mcp-keep-feedback` 现有的 MCP 反馈循环机制，将 Cursor 作为后端 LLM，对外提供 OpenAI 兼容的 API 服务。外部 AI 工具可以通过标准的 OpenAI SDK 直接调用，无需用户手动介入。

## 架构图

```
外部 AI 客户端                    mcp-keep-feedback                  Cursor (MCP Host)
     |                                    |                                    |
     |--- POST /v1/chat/completions ----->|                                    |
     |    (OpenAI 格式请求)               |                                    |
     |                                    |  [入队 + 阻塞等待 response_event]  |
     |                                    |                                    |
     |                                    |<-- interactive_feedback(summary) ---|
     |                                    |    summary = Cursor 对上一轮的回复  |
     |                                    |                                    |
     |                                    |  [1. 将 summary 投递给等待中的 API  |
     |                                    |       请求 (resolve response_event)]|
     |                                    |  [2. 从队列取出下一条 → 返回给     |
     |                                    |       Cursor 作为 "用户反馈"]       |
     |                                    |                                    |
     |                                    |--- return feedback_text ----------->|
     |                                    |    (外部 API 请求的内容)            |
     |                                    |                                    |
     |                                    |<-- interactive_feedback(summary) ---|
     |                                    |    summary = Cursor 处理后的输出    |
     |                                    |                                    |
     |                                    |  [投递 summary → resolve event]     |
     |                                    |                                    |
     |<-- OpenAI 格式响应 ----------------|                                    |
     |    (choices[0].message.content =   |                                    |
     |     Cursor 的 summary)             |                                    |
```

## 核心机制

### 请求-响应关联

现有的输入队列 (`enqueue_feedback` / `dequeue_feedback`) 只负责"输入"方向，没有"输出回传"机制。需要新增请求-响应关联层：

```python
import asyncio
import time
import uuid
import threading
from dataclasses import dataclass, field

@dataclass
class PendingAPIRequest:
    """一个等待 Cursor 回复的 API 请求"""
    request_id: str
    messages: list
    model: str
    response_event: asyncio.Event = field(default_factory=asyncio.Event)
    response_data: str | None = None
    created_at: float = field(default_factory=time.time)
    queue_item_id: str | None = None   # 关联的队列项 ID

class OpenAIProxyManager:
    """OpenAI 代理管理器"""

    def __init__(self):
        self._pending_response: PendingAPIRequest | None = None
        self._lock = threading.Lock()

    def submit_request(self, messages: list, model: str) -> PendingAPIRequest:
        """提交一个 API 请求，返回 PendingAPIRequest 供调用者 await"""
        req = PendingAPIRequest(
            request_id=str(uuid.uuid4()),
            messages=messages,
            model=model,
        )
        with self._lock:
            self._pending_response = req
        return req

    def deliver_response(self, summary: str) -> bool:
        """投递 Cursor 的回复给等待中的 API 请求"""
        with self._lock:
            pending = self._pending_response
            if not pending:
                return False
            self._pending_response = None

        pending.response_data = summary
        pending.response_event.set()
        return True

    def has_pending_response(self) -> bool:
        """是否有 API 请求在等待回复"""
        with self._lock:
            return self._pending_response is not None
```

### 修改 `launch_web_feedback_ui` 流程

```python
async def launch_web_feedback_ui(project_directory: str, summary: str) -> dict:
    manager = get_web_ui_manager()

    # 启动服务器
    if manager.server_thread is None or not manager.server_thread.is_alive():
        manager.start_server()

    manager.add_history("ai", summary)

    # ===== 新增：投递响应给等待中的 API 请求 =====
    proxy = manager.openai_proxy
    if proxy and proxy.has_pending_response():
        proxy.deliver_response(summary)
    # =============================================

    # 原有逻辑：检查队列
    queued = manager.dequeue_feedback()
    if queued:
        # ... 原有处理 ...
        return queued

    # 原有逻辑：创建会话等待用户输入
    manager.create_session(project_directory, summary)
    session = manager.get_current_session()
    # ...
```

## API 设计

### `POST /v1/chat/completions`

**请求格式（OpenAI 标准）：**

```json
{
    "model": "cursor",
    "messages": [
        {"role": "system", "content": "You are a helpful assistant."},
        {"role": "user", "content": "帮我写一个快速排序算法"}
    ],
    "temperature": 0.7,
    "max_tokens": 4096,
    "stream": false
}
```

**处理流程：**

1. 提取 `messages` 中最后一条 `user` 角色的消息
2. 可选：将 `system` 消息和对话历史拼接为上下文
3. 调用 `proxy.submit_request(messages, model)` 获取 `PendingAPIRequest`
4. 将消息内容通过 `manager.enqueue_feedback(text)` 入队
5. 将 `queue_item_id` 关联到 `PendingAPIRequest`
6. `await request.response_event.wait()` 阻塞等待（带超时）
7. 格式化为 OpenAI 响应返回

**响应格式（OpenAI 标准）：**

```json
{
    "id": "chatcmpl-xxx",
    "object": "chat.completion",
    "created": 1709000000,
    "model": "cursor",
    "choices": [
        {
            "index": 0,
            "message": {
                "role": "assistant",
                "content": "这是 Cursor 的回复内容..."
            },
            "finish_reason": "stop"
        }
    ],
    "usage": {
        "prompt_tokens": -1,
        "completion_tokens": -1,
        "total_tokens": -1
    }
}
```

### `GET /v1/models`

**响应：**

```json
{
    "object": "list",
    "data": [
        {
            "id": "cursor",
            "object": "model",
            "created": 1709000000,
            "owned_by": "mcp-keep-feedback"
        }
    ]
}
```

## 文件结构

```
src/mcp_keep_feedback/web/
├── openai_proxy.py              # 新建：代理管理器
├── routes/
│   ├── openai_routes.py         # 新建：OpenAI 兼容 API 路由
│   └── __init__.py              # 修改：注册 openai_routes
├── main.py                      # 修改：集成代理管理器
└── ...
```

### `web/openai_proxy.py`（新建）

- `PendingAPIRequest` 数据类
- `OpenAIProxyManager` 管理类
  - `submit_request(messages, model)` → PendingAPIRequest
  - `deliver_response(summary)` → bool
  - `has_pending_response()` → bool
  - `format_openai_response(request_id, content, model)` → dict
  - `extract_user_message(messages)` → str

### `web/routes/openai_routes.py`（新建）

- `POST /v1/chat/completions` — 主要 API 端点
- `GET /v1/models` — 模型列表
- 可选：API Key 鉴权中间件

### `web/main.py`（修改）

- `WebUIManager.__init__`：初始化 `self.openai_proxy = OpenAIProxyManager()`
- `launch_web_feedback_ui`：在 `add_history` 后增加 `deliver_response` 调用

### `web/routes/__init__.py`（修改）

- 注册 `setup_openai_routes(manager)` 到 FastAPI app

## 消息格式化

外部 API 请求的 `messages` 需要格式化为适合 Cursor 处理的文本：

```python
def extract_user_message(messages: list) -> str:
    """从 OpenAI messages 格式提取用户消息，拼接上下文"""
    parts = []

    # 提取 system 消息（如果有）
    system_msgs = [m for m in messages if m["role"] == "system"]
    if system_msgs:
        parts.append(f"[System Instruction]\n{system_msgs[-1]['content']}")

    # 提取对话历史
    for msg in messages:
        if msg["role"] == "user":
            parts.append(f"[User]\n{msg['content']}")
        elif msg["role"] == "assistant":
            parts.append(f"[Assistant]\n{msg['content']}")

    return "\n\n".join(parts)
```

## 配置项

| 环境变量 | 默认值 | 说明 |
|---------|--------|------|
| `MCP_OPENAI_ENABLED` | `false` | 是否启用 OpenAI 代理功能 |
| `MCP_OPENAI_API_KEY` | （空） | 可选的 API Key 鉴权，为空则不鉴权 |
| `MCP_OPENAI_TIMEOUT` | `300` | API 请求超时时间（秒） |
| `MCP_OPENAI_MODEL_NAME` | `cursor` | 对外暴露的模型名称 |

## 限制和注意事项

### 串行处理

Cursor 是串行调用 `interactive_feedback` 的（调用一次 → 等待返回 → 处理 → 再调用），因此 API 请求也只能串行处理。多个并发请求会排队等待。

### 延迟

从 API 请求提交到收到响应，需要等待 Cursor 完成一个完整的 MCP 调用周期。延迟取决于：
- Cursor 当前是否正在处理其他任务
- Cursor 使用的 LLM 模型的响应速度
- 任务本身的复杂度

典型延迟：5 秒 ~ 5 分钟

### 首次调用

Cursor 第一次调用 `interactive_feedback` 时，还没有 API 请求的响应需要投递，这是正常行为。只有当 API 请求入队并被 Cursor 消费后，下一次 `interactive_feedback` 调用才会带有对应的响应。

### Streaming（暂不支持）

Cursor 的输出是通过 `summary`（即 `full_report_and_output`）一次性传递的，无法实现真正的流式输出。未来可以考虑：
- 模拟 streaming：收到完整响应后逐字/逐段发送 SSE 事件
- 或者在 Cursor 端实现增量输出（需要修改 MCP 协议层）

### Token 计数

无法精确统计 token 使用量。响应中的 `usage` 字段将返回 `-1` 或估算值。

## 使用示例

### Python (OpenAI SDK)

```python
import openai

client = openai.OpenAI(
    base_url="http://localhost:8765/v1",
    api_key="optional-key"
)

response = client.chat.completions.create(
    model="cursor",
    messages=[
        {"role": "system", "content": "你是一个代码助手"},
        {"role": "user", "content": "用Python实现二分查找"}
    ]
)

print(response.choices[0].message.content)
```

### cURL

```bash
curl http://localhost:8765/v1/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer optional-key" \
  -d '{
    "model": "cursor",
    "messages": [
      {"role": "user", "content": "Hello, who are you?"}
    ]
  }'
```

### 其他 AI 工具配置

任何支持自定义 OpenAI 端点的 AI 工具都可以使用：

- **Aider**: `aider --openai-api-base http://localhost:8765/v1`
- **LangChain**: `ChatOpenAI(base_url="http://localhost:8765/v1")`
- **Continue.dev**: 配置 `apiBase` 为 `http://localhost:8765/v1`

## 开发计划

### 阶段一：核心功能

1. 实现 `OpenAIProxyManager`（请求-响应关联）
2. 实现 `/v1/chat/completions` 和 `/v1/models` 路由
3. 修改 `launch_web_feedback_ui` 集成响应投递
4. 基本错误处理和超时机制

### 阶段二：增强功能

1. API Key 鉴权
2. 请求日志和统计
3. 模拟 streaming 输出
4. Web UI 中显示 API 代理状态

### 阶段三：优化

1. 并发请求排队和优先级
2. 请求去重和缓存
3. 健康检查端点
4. 完善文档和示例
