wangzhibo
2026-08-31 17ac2ce0ed52874616c79c910667e41e34604826
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
#!/usr/bin/env python3
"""Standalone translation MCP. Only exposes translate. Does not call ConnectionHandler.chat."""
 
from __future__ import annotations
 
import os
from pathlib import Path
 
import httpx
import yaml
from mcp.server.fastmcp import FastMCP
 
ROOT = Path(__file__).resolve().parents[2]
TRANSLATOR_PROMPT = (
    "你是一台翻译机。只输出译文,不要解释、不要拼音课、不要追问、不要闲聊。"
    "未指定语言时:中文译成英文,英文译成中文;其它语言默认译成中文。"
)
 
mcp = FastMCP("english-translate")
 
 
def _read_yaml(path: Path) -> dict:
    if not path.is_file():
        return {}
    with path.open("r", encoding="utf-8") as f:
        data = yaml.safe_load(f) or {}
    return data if isinstance(data, dict) else {}
 
 
def _load_llm_config() -> dict:
    default_cfg = _read_yaml(ROOT / "config.yaml")
    custom_cfg = _read_yaml(ROOT / "data" / ".config.yaml")
 
    def merge(base, overlay):
        out = dict(base)
        for k, v in overlay.items():
            if isinstance(v, dict) and isinstance(out.get(k), dict):
                out[k] = merge(out[k], v)
            else:
                out[k] = v
        return out
 
    cfg = merge(default_cfg, custom_cfg)
    selected = (cfg.get("selected_module") or {}).get("LLM")
    llm_map = cfg.get("LLM") or {}
    if selected and selected in llm_map:
        item = dict(llm_map[selected])
        item["_name"] = selected
        return item
    if llm_map:
        name, item = next(iter(llm_map.items()))
        data = dict(item)
        data["_name"] = name
        return data
    return {}
 
 
def _chat_complete(text: str, source_lang: str, target_lang: str) -> str:
    llm = _load_llm_config()
    api_key = llm.get("api_key") or os.environ.get("OPENAI_API_KEY") or ""
    if api_key and ("你的" in str(api_key) or "key" == str(api_key).lower()):
        api_key = os.environ.get("OPENAI_API_KEY") or api_key
    base_url = llm.get("base_url") or llm.get("url") or "https://api.openai.com/v1"
    model = llm.get("model_name") or llm.get("model") or "gpt-4o-mini"
    if not api_key or "你的" in str(api_key):
        raise RuntimeError("翻译 MCP 未配置可用的 LLM api_key")
 
    extra = ""
    if source_lang or target_lang:
        extra = f"\n源语言:{source_lang or 'auto'} 目标语言:{target_lang or 'auto'}"
    user = f"翻译下面内容,只输出译文:{extra}\n{text}"
 
    url = str(base_url).rstrip("/")
    if not url.endswith("/chat/completions"):
        if url.endswith("/v1"):
            url = url + "/chat/completions"
        else:
            url = url + "/v1/chat/completions"
 
    headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
    payload = {
        "model": model,
        "temperature": 0.1,
        "messages": [
            {"role": "system", "content": TRANSLATOR_PROMPT},
            {"role": "user", "content": user},
        ],
    }
    with httpx.Client(timeout=60.0) as client:
        resp = client.post(url, headers=headers, json=payload)
        resp.raise_for_status()
        data = resp.json()
    choices = data.get("choices") or []
    if not choices:
        raise RuntimeError("翻译接口没有返回内容")
    msg = (choices[0].get("message") or {}).get("content") or ""
    return str(msg).strip()
 
 
@mcp.tool()
def translate(text: str, source_lang: str = "", target_lang: str = "") -> str:
    """Translate text. Chinese->English, English->Chinese unless languages are set. Return translation only."""
    if not text or not str(text).strip():
        return ""
    return _chat_complete(str(text).strip(), source_lang or "", target_lang or "")
 
 
if __name__ == "__main__":
    os.chdir(ROOT)
    mcp.run(transport="stdio")