在 Opencode 接入 Deekseek 的基础上让 Deepseek 长出眼睛

效果

原图:
在这里插入图片描述

效果:
在这里插入图片描述

安装 python 环境

python 需要的安装包如下:

pip install mcp httpx zhipuai

写入下面 mcp 服务器

这个位置随意,我放到:~/.config/opencode/mcp/unserstand_image_mcp.py

"""understand_image MCP —— 图片理解工具(兼容 Anthropic / OpenAI / 智谱 三种协议)。

功能:
  - 图片来源: 本地文件路径 或 http(s) URL, 混用亦可。
  - 协议模式: 通过环境变量 IMAGE_API_MODE 切换 anthropic / openai / zhipu。
    - anthropic / openai: 走 HTTP 接口, 图片统一转 base64。
    - zhipu: 走 zhipuai SDK (ZhipuAI), URL 直传, 本地文件转 base64 data URL。
  - 本地文件限制大小防止误传大图。

环境变量:
  IMAGE_API_MODE         协议模式, 可选 anthropic (默认) / openai / zhipu
  IMAGE_API_KEY          API Key (必填)
  IMAGE_MODEL            模型名(必填)
  IMAGE_BASE_URL         接口地址, 需为 OpenAI 风格 /v1 前缀
  IMAGE_MAX_BYTES        单张图片最大字节数, 默认 10MB
  IMAGE_ZHIPU_THINKING   智谱 thinking 开关, 默认 disabled, 设 enabled 开启

"""

import base64
import logging
import mimetypes
import os

import httpx
from mcp.server.mcpserver import MCPServer

logging.getLogger("httpx").setLevel(logging.WARNING)
logging.getLogger("httpcore").setLevel(logging.WARNING)

# ---------------------------------------------------------------------------
# 配置
# ---------------------------------------------------------------------------

_MODE = os.environ.get("IMAGE_API_MODE", "anthropic").strip().lower()
if _MODE not in ("anthropic", "openai", "zhipu"):
    raise RuntimeError(f"IMAGE_API_MODE 必须为 anthropic / openai / zhipu, 当前为: {_MODE!r}")

MODEL = os.environ.get("IMAGE_MODEL")
BASE_URL = os.environ.get("IMAGE_BASE_URL", " ").rstrip("/")
MAX_BYTES = int(os.environ.get("IMAGE_MAX_BYTES", 10 * 1024 * 1024))
HTTP_TIMEOUT = 600.0

# 常见图片扩展名兜底映射 (mimetypes 覆盖不到的 webp/avif 等)
_EXT_TYPES = {
    ".png": "image/png",
    ".jpg": "image/jpeg",
    ".jpeg": "image/jpeg",
    ".webp": "image/webp",
    ".gif": "image/gif",
    ".bmp": "image/bmp",
    ".avif": "image/avif",
}


def _media_type(path: str) -> str:
    """由文件名推断图片 MIME, 无法识别时兜底为 image/png。"""
    mt, _ = mimetypes.guess_type(path)
    if mt:
        return mt
    ext = "." + path.rsplit(".", 1)[-1].lower() if "." in path else ""
    return _EXT_TYPES.get(ext, "image/png")


# ---------------------------------------------------------------------------
# 图片加载: URL 或本地文件 -> (base64, mime)
# ---------------------------------------------------------------------------

def _load_image(source: str) -> tuple[str, str]:
    """把 url 或本地路径统一加载为 (base64_data, media_type)。失败抛明确异常。"""
    source = source.strip()
    if not source:
        raise ValueError("图片路径不能为空")

    if source.startswith(("http://", "https://")):
        try:
            resp = httpx.get(source, timeout=30)
            resp.raise_for_status()
        except httpx.HTTPError as e:
            raise ValueError(f"下载图片失败: {source} ({e})") from e
        raw, path = resp.content, source
    elif os.path.isfile(source):
        with open(source, "rb") as f:
            raw = f.read()
        path = source
    else:
        raise FileNotFoundError(f"图片既不是有效文件也不是 URL: {source}")

    if not raw:
        raise ValueError(f"图片内容为空: {source}")
    if len(raw) > MAX_BYTES:
        raise ValueError(f"图片超过大小限制 ({len(raw)} > {MAX_BYTES} 字节): {source}")

    return base64.b64encode(raw).decode(), _media_type(path)


# ---------------------------------------------------------------------------
# 三种协议的消息构造与调用
# ---------------------------------------------------------------------------

def _build_messages(image_sources: list[str], prompt: str) -> tuple:
    """按当前模式构造请求体 (dict) 与请求头 (dict), 失败抛出明确异常。"""
    if not image_sources:
        raise ValueError("至少需要一张图片")
    if not prompt.strip():
        raise ValueError("prompt 不能为空")

    payload = {"model": MODEL, "max_tokens": 1024}
    if _MODE == "anthropic":
        content = []
        for src in image_sources:
            data, mt = _load_image(src)
            content.append({"type": "image", "source": {"type": "base64", "media_type": mt, "data": data}})
        content.append({"type": "text", "text": prompt})
        payload["messages"] = [{"role": "user", "content": content}]
        headers = {
            "x-api-key": os.environ["IMAGE_API_KEY"],
            "anthropic-version": "2023-06-01",
            "content-type": "application/json",
        }
    else:  # openai
        content = [{"type": "text", "text": prompt}]
        for src in image_sources:
            data, mt = _load_image(src)
            content.append({"type": "image_url", "image_url": {"url": f"data:{mt};base64,{data}"}})
        payload["messages"] = [{"role": "user", "content": content}]
        headers = {"authorization": f"Bearer {os.environ['IMAGE_API_KEY']}", "content-type": "application/json"}
    return payload, headers


def _extract_text(data: dict) -> str:
    """从不同协议的响应中抽取首个文本内容。"""
    if _MODE == "anthropic":
        blocks = data.get("content") or []
        for block in blocks:
            if block.get("type") == "text":
                return block["text"]
        raise RuntimeError(f"anthropic 响应中没有 text 内容块: {str(data)[:500]}")
    choices = data.get("choices") or []
    if not choices:
        raise RuntimeError(f"openai 响应中没有 choices: {str(data)[:500]}")
    msg = choices[0].get("message") or {}
    return msg.get("content") or ""


def _call_zhipu(image_sources: list[str], prompt: str) -> str:
    """智谱模式: 通过 zhipuai SDK (ZhipuAI) 调用视觉模型。

    URL 图片直接传给 SDK, 本地文件转为 data URL 后再传, 与 OpenAI 格式一致。
    """
    if not image_sources:
        raise ValueError("至少需要一张图片")
    if not prompt.strip():
        raise ValueError("prompt 不能为空")

    try:
        from zhipuai import ZhipuAI
    except ImportError as e:
        raise RuntimeError("智谱模式需要安装 zhipuai 包: pip install zhipuai") from e

    client = ZhipuAI(api_key=os.environ["IMAGE_API_KEY"])

    content = [{"type": "text", "text": prompt}]
    for src in image_sources:
        src = src.strip()
        if src.startswith(("http://", "https://")):
            url = src
        else:
            data, mt = _load_image(src)
            url = f"data:{mt};base64,{data}"
        content.append({"type": "image_url", "image_url": {"url": url}})

    kwargs = {
        "model": MODEL,
        "messages": [{"role": "user", "content": content}],
    }
    if os.environ.get("IMAGE_ZHIPU_THINKING", "disabled") == "enabled":
        kwargs["thinking"] = {"type": "enabled"}

    try:
        response = client.chat.completions.create(**kwargs)
    except Exception as e:
        raise RuntimeError(f"智谱接口调用失败: {e}") from e

    if not getattr(response, "choices", None):
        raise RuntimeError(f"智谱响应中没有 choices: {str(response)[:500]}")
    message = response.choices[0].message
    text = getattr(message, "content", None) or ""
    return text if isinstance(text, str) else str(text)


def call_model(image_sources: list[str], prompt: str) -> str:
    """入口: 按模式路由 -> 调用模型 -> 返回文本结果。"""
    if _MODE == "zhipu":
        return _call_zhipu(image_sources, prompt)

    payload, headers = _build_messages(image_sources, prompt)
    try:
        resp = httpx.post(f"{BASE_URL}/messages" if _MODE == "anthropic" else f"{BASE_URL}/chat/completions",
                          headers=headers, json=payload, timeout=HTTP_TIMEOUT)
    except httpx.HTTPError as e:
        raise RuntimeError(f"请求模型接口失败: {e}") from e
    if resp.status_code != 200:
        raise RuntimeError(f"模型接口返回 {resp.status_code}: {resp.text[:500]}")
    return _extract_text(resp.json())


# ---------------------------------------------------------------------------
# MCP 服务注册
# ---------------------------------------------------------------------------

mcp = MCPServer("understand-image")


@mcp.tool()
def understand_image(image_paths: list[str], prompt: str) -> str:
    """理解一张或多张图片。

    Args:
        image_paths: 本地图片文件路径或 http(s) 图片 URL 的列表, 可混用。
        prompt: 对图片的提问指令。

    Returns:
        模型的理解结果文本。
    """
    return call_model(image_paths, prompt)


def main():
    mcp.run()


if __name__ == "__main__":
    main()


写入模型配置

这里只要接入有多模态识别能力就好了。
这个 MCP 服务进行了三个服务商的兼容,OPENAI,anthropic,zhipu
Opencode 的配置文件(~/.config/opencode/opencode.jsonc)中写入 MCP 配置

{
    "$schema": "https://opencode.ai/config.json",
    "plugin": [
        "superpowers@git+https://github.com/obra/superpowers.git",
        "@dietrichgebert/ponytail"
    ],
    // "mcp": {
    //     "understand-image": {
    //         "type": "local",
    //         "command": [
    //             "python.exe",
    //             "understand_image_mcp.py"
    //         ],
    //         "enabled": true,
    //         "timeout": 600000,
    //         "environment": {
    //             "IMAGE_API_MODE": "openai",
    //             "IMAGE_API_KEY": "sk-ws-H.ELXPMIH.qIiY.",
    //             "IMAGE_MODEL": "qwen3.8-max",
    //             "IMAGE_BASE_URL": "https://dashscope.aliyuncs.com/compatible-mode/v1"
    //         }
    //     }
    // },
    "mcp": {
        "understand-image": {
            "type": "local",
            "command": [
                "python.exe",
                "understand_image_mcp.py"
            ],
            "enabled": true,
            "timeout": 600000,
            "environment": {
                "IMAGE_API_MODE": "zhipu",
                "IMAGE_API_KEY": ".XJuvCbWEKRnRQxj3",
                "IMAGE_MODEL": "glm-4.6v-flash"
            }
        }
    },
    "provider": {
        
        }
    }
}

主要注意的地方:

  1. command : 其中 python 解释器的位置要填下载相关依赖的解释器位置,如果不知道在哪里,可以用 where python 找到在哪里,如果结果有多个选择第一个就好了,其中 Python 文件地方,你自己放在哪里,就写哪里。
  2. 写入相关环境配置,这些配置,要自己去配,去官网申请
"IMAGE_API_MODE": "zhipu",
"IMAGE_API_KEY": ".XJuvCbWEKRnRQxj3",
"IMAGE_MODEL": "glm-4.6v-flash"
"IMAGE_BASE_URL": "https://dashscope.aliyuncs.com/compatible-mode/v1" // 这个智谱不需要填写
  1. 智谱是免费的多模态模型,然后 Qwen3.8Max 也有免费额度,可以尝试
    • 智谱网址:https://bigmodel.cn/apikey/platform
    • 千问网址:https://platform.qianwenai.com/home/analytics/request-logs

检测是否成功

Opencode 中写入上述配置以后,Opencode 输入 /status 会显示如下:
在这里插入图片描述

更多推荐