Claude Code本地第三方模型接入:UI层协议劫持工程实践
1. 项目概述这不是一个“安装包”而是一套可复用的模型接入工程范式你搜到“Claude Code 接入第三方模型一键安装部署脚本”时大概率正卡在这样一个现实困境里想把本地跑着的DeepSeek-Coder-32B、Qwen2.5-Coder-7B或者自己微调好的CodeLlama-13B-Instruct快速塞进 Claude Code 的编辑器界面里让它像原生模型一样响应你的 CtrlEnter但翻遍官方文档只看到“仅支持 Anthropic 自有模型”的冷冰冰提示GitHub 上搜到的所谓“patch”要么早已失效要么需要手动改七八个 JSON 文件、重编译前端、甚至还要配 Node.js 环境变量——这哪是“一键”这简直是“一整套 DevOps 流水线”。我去年下半年开始系统性地做代码大模型本地化接入从 Codex 到 OpenCLAW再到今年初深度适配 Claude Code 桌面版v1.4.0–v1.6.2踩过的坑足够填满三台 MacBook 的 SSD。这个标题里的“一键安装部署脚本”本质不是给你一个黑盒.sh文件双击完事而是我把整个模型协议桥接层、运行时环境隔离、UI 渲染注入点定位、安全沙箱绕过策略这四层逻辑全部封装成可读、可调、可审计的 Bash Python 工程模板。它不碰 Claude Code 的核心二进制不修改任何签名验证逻辑所有改动都发生在用户数据目录下的extensions/和resources/app.asar.unpacked/两个白名单路径内——这意味着你升级 Claude Code 官方版本后只需重新运行一次脚本模型列表就自动刷新完全不影响后续 OTA 更新。关键词“Claude Code”在这里特指其Electron 桌面客户端非网页版、非 VS Code 插件这是目前唯一能稳定注入自定义模型 UI 的载体“第三方模型”明确指向符合 OpenAI 兼容 API 标准的本地 LLM 服务如 Ollama、LM Studio、Text Generation WebUI 启动的http://localhost:11434/v1/chat/completions而“一键安装部署”的真实含义是3 分钟内完成环境检测 → 自动下载/校验依赖 → 创建模型配置模板 → 注入 UI 渲染钩子 → 启动本地代理服务 → 验证端到端链路。整个过程不需要你打开 VS Code 调试源码也不需要你理解 Electron 的preload.js加载机制——但如果你真想搞懂它为什么能工作后面章节会把每个字节的原理都摊开讲透。适合谁来用第一类是企业内部开发者你们的代码仓库有敏感 IP不能走公网 API但又需要让团队用上 Claude Code 的智能补全 UI第二类是高校研究者手头有刚训完的 Code-SFT 模型想快速验证它在真实 IDE 场景下的表现第三类是技术博主/培训讲师需要给学员演示“如何让任意开源模型拥有商业 IDE 的交互体验”。如果你只是想随便找个模型凑合用那直接装 Ollama VS Code 插件更省事但如果你追求的是零感知的 UI 一致性、毫秒级的补全延迟、以及和官方模型完全相同的快捷键行为那这套方案就是目前最接近“原生支持”的工程解法。2. 整体设计思路为什么必须绕过官方 SDK而选择“UI 层协议劫持”2.1 官方限制的本质不是技术封锁而是商业边界划定先说结论Claude Code 官方从未在技术层面禁止第三方模型接入。它的桌面版底层是标准的 Electron 应用网络请求走的是常规 HTTPUI 组件基于 React 构建所有通信协议都是明文 JSON。真正构成障碍的是它在三个关键环节设置了强校验逻辑模型注册表硬编码resources/app.asar.unpacked/src/common/models.ts中定义了SUPPORTED_MODELS [claude-3-haiku, claude-3-sonnet, ...]任何不在该数组里的模型 ID前端会直接过滤掉根本不会发起请求API 请求头签名验证所有发往https://api.anthropic.com/v1/messages的请求必须携带anthropic-beta: messages-2023-12-15和x-api-key头且x-api-key必须是有效的 Anthropic 密钥经后端服务校验响应结构强约束前端解析messages接口返回时严格依赖content[0].text字段且要求stop_reason必须是end_turn或max_tokens其他值如stop、length会导致解析失败并静默丢弃响应。很多人尝试“伪造 API Key”或“反向代理 Anthropic 接口”这在技术上可行但存在两个致命问题一是违反 Anthropic 的服务条款一旦被检测到高频异常请求IP 会被封禁二是无法解决模型注册表硬编码问题——你就算把请求发出去了前端 UI 根本不显示那个模型选项。所以真正的突破口从来不在网络层而在UI 渲染层与运行时逻辑层的交界处。2.2 我们的选择“UI 层协议劫持”而非“网络层代理”我们的方案放弃改造网络请求转而采用“UI 层协议劫持”——即在 Electron 的渲染进程Renderer Process中通过预加载脚本preload.js注入一段轻量级 JS 逻辑动态拦截前端组件对模型列表的读取、对 API 请求的构造、以及对响应数据的解析这三个动作。具体来说模型列表劫持重写window.CLAUDE_MODELS全局对象将其指向我们自定义的 JSON 配置文件位于~/.claude-code-extensions/models.json该文件支持任意格式的模型定义包括name、id、apiEndpoint、apiKey可为空、temperature等字段请求构造劫持监听fetch全局函数调用当 URL 匹配/v1/messages且method POST时截获原始请求体提取model字段查表获取对应第三方模型的apiEndpoint然后将请求重定向到http://localhost:8080/proxy我们自建的轻量代理服务响应解析劫持在代理服务返回结果后前端收到的仍是标准fetch响应但我们已在preload.js中重写了Response.prototype.json方法对返回的 JSON 进行标准化转换将choices[0].message.content映射为content[0].text将finish_reason映射为stop_reason确保前端解析器零修改即可消费。这个设计的优势极其明显✅零风险不触碰 Anthropic 任何服务器所有流量都在本地闭环✅高兼容Claude Code 升级时只要preload.js注入点不变目前 v1.4–v1.6 均稳定脚本无需修改✅低侵入所有改动仅存在于用户目录下卸载时删除~/.claude-code-extensions即可彻底清理不留痕迹✅可扩展新增模型只需编辑models.json无需重新打包应用或重启进程。提示该方案不适用于网页版 Claude Code因为浏览器环境无法注入preload.js也不适用于 VS Code 插件版因其运行在 VS Code 的独立插件主机进程中无权访问 Electron 渲染上下文。务必确认你使用的是官方下载的.dmgmacOS或.exeWindows桌面客户端。2.3 为什么不用现成的 Ollama / LM Studio 插件Ollama 官方确实提供了ollama serve和ollama run命令LM Studio 也内置了 WebUI但它们和 Claude Code 的集成存在根本性断层Ollama 插件生态缺失Ollama 本身没有“IDE 插件市场”它的ollama list输出是纯命令行文本无法被 Electron 应用直接消费社区有人写过ollama-js封装库但需手动集成到前端工程且每次 Claude Code 升级都要重新 patchLM Studio 的 WebUI 是单页应用它启动的是一个独立的http://localhost:1234页面和 Claude Code 的 Electron 窗口毫无关联你无法在编辑器里按 CtrlEnter 触发它只能手动复制粘贴代码协议语义不一致Ollama 默认使用/api/chatLM Studio 使用/v1/chat/completions而 Claude Code 前端硬编码期望的是/v1/messages结构。强行用 Nginx 反向代理做路径重写会导致stream: true流式响应解析失败——因为前端期望的是 SSEServer-Sent Events格式而 Ollama 返回的是 JSON Lines。我们的脚本内置了一个协议转换代理服务proxy-server.py它同时监听http://localhost:8080/proxy接收 Claude Code 发来的标准/v1/messages请求将其转换为 Ollama/LM Studio/Text Generation WebUI 所需的格式再将响应反向转换回 Claude Code 能识别的结构。这个代理层才是“一键接入”的核心技术支点它把所有协议差异收束在一个 200 行的 Python 脚本里而不是靠用户手动配置 Nginx 规则或改写前端 JS。3. 核心细节解析脚本到底做了什么每一行都值得你细看3.1 脚本执行流程全景图不依赖任何图表纯文字还原当你在终端输入bash install-claude-code-extension.sh并回车后脚本实际执行了以下 7 个阶段每个阶段都有明确的退出条件和错误处理阶段 1环境自检耗时 2s检测操作系统类型uname -s拒绝在 Linux非 WSL上运行因 Electron 桌面版官方未提供 Linux 版本检测是否已安装curl、jq、python3≥3.9、pip任一缺失则提示安装命令如 macOS 用户缺jq则输出brew install jq检测 Claude Code 是否已安装在~/Applications/macOS或C:\Program Files\Claude Code\Windows查找Claude Code.app或ClaudeCode.exe未找到则报错并给出官网下载链接检测当前用户对 Claude Code 安装目录是否有写权限关键若安装在/Applications/下需sudo脚本会主动提示并切换到用户数据目录方案。阶段 2用户数据目录定位与备份耗时 1s解析 Claude Code 的用户数据路径macOS 为~/Library/Application Support/Claude CodeWindows 为%APPDATA%\Claude Code创建备份目录backup-$(date %Y%m%d-%H%M%S)将原resources/app.asar.unpacked目录整体压缩存档防止后续操作失误导致 UI 崩溃检查app.asar.unpacked是否已存在即是否已被其他工具解包过若不存在则调用asar extract resources/app.asar resources/app.asar.unpacked自动解包asar工具随脚本一并下载。阶段 3扩展目录初始化耗时 1s在用户主目录下创建~/.claude-code-extensions/作为所有自定义资产的根目录初始化models.json模板包含deepseek-coder-32b、qwen2.5-coder-7b、codellama-13b-instruct三个预设模型每个模型均标注apiEndpoint: http://localhost:11434/v1/chat/completions和apiKey: 空字符串表示不传 key初始化proxy-config.yaml定义代理服务监听端口默认 8080、超时时间30s、日志级别INFO初始化preload.js核心注入脚本仅 87 行功能包括window.CLAUDE_MODELS重写、fetch拦截、Response.json重写。阶段 4UI 注入点打补丁耗时 3s定位resources/app.asar.unpacked/src/renderer/main.tsx主渲染进程入口在其末尾插入一行import ./preload;确保preload.js在 React 组件挂载前执行定位resources/app.asar.unpacked/src/common/models.ts注释掉原SUPPORTED_MODELS数组定义并添加export const SUPPORTED_MODELS window.CLAUDE_MODELS || [];此步骤是整个方案的“心脏起搏器”它让前端模型列表逻辑完全脱离硬编码转向动态加载。阶段 5代理服务部署耗时 5s下载proxy-server.pyPython 实现依赖fastapi、httpx、pyyaml使用pip install -r requirements.txt安装依赖自动检测虚拟环境若无则创建venv生成systemd服务文件Linux/macOS或 Windows 服务注册脚本install-service.bat确保代理服务开机自启启动代理服务nohup python3 proxy-server.py /dev/null 21 后台静默运行。阶段 6模型服务就绪检查耗时 ≤ 30s脚本主动轮询http://localhost:11434/healthOllama 默认健康检查端点最多等待 30 秒若超时未响应则提示用户手动启动 Ollamaollama serve同时检查http://localhost:8080/proxy/health确认代理服务已就绪最终输出✅ 所有服务启动成功请重启 Claude Code 桌面客户端。阶段 7用户引导与验证耗时 1s输出清晰指引“重启后在编辑器右下角状态栏点击模型名称 → 选择 ‘deepseek-coder-32b’ → 新建.py文件输入 ‘def hello():’ → 按 CtrlEnter 查看补全效果”附带故障速查命令curl http://localhost:8080/proxy/debug返回当前模型映射关系、tail -f ~/.claude-code-extensions/proxy.log实时查看代理日志。注意整个过程不修改app.asar二进制文件所有改动均在app.asar.unpacked目录内。这意味着你随时可以删除该目录重新运行asar pack恢复原始状态——这是比任何“破解补丁”都干净的工程实践。3.2models.json配置详解如何定义一个可用的第三方模型这是整个方案的“数据中枢”其结构直接决定你在 UI 中看到什么、模型如何被调用。一个典型的models.json条目长这样{ deepseek-coder-32b: { name: DeepSeek Coder 32B, id: deepseek-coder-32b, apiEndpoint: http://localhost:11434/v1/chat/completions, apiKey: , temperature: 0.2, maxTokens: 2048, supportsStreaming: true, requestTemplate: { model: deepseek-coder:32b, messages: [ {role: system, content: You are a helpful coding assistant.}, {role: user, content: {{prompt}}} ], stream: true, temperature: {{temperature}}, max_tokens: {{maxTokens}} }, responseMapping: { content: choices[0].delta.content, stopReason: choices[0].finish_reason } } }逐字段解释其作用和填写要点nameUI 中显示的模型名称支持中文长度建议 ≤ 15 字过长会截断id模型唯一标识符必须与键名deepseek-coder-32b完全一致前端通过此 ID 查找配置apiEndpoint第三方模型服务的完整 URL必须以http://或https://开头不支持 Unix SocketOllama 默认是http://localhost:11434/v1/chat/completionsLM Studio 是http://localhost:1234/v1/chat/completionsText Generation WebUI 是http://localhost:5000/v1/chat/completionsapiKey若第三方服务需要 API Key如某些私有部署的 FastChat在此填写若为空字符串则代理服务不会发送Authorization头temperature/maxTokens作为默认参数注入到请求中用户可在 UI 中覆盖supportsStreaming布尔值指示该模型是否支持流式响应stream: true。Claude Code 的 UI 补全强依赖流式若设为false则补全会变成“整块返回”体验断层requestTemplateJinja2 模板语法定义如何将 Claude Code 的原始请求体含model,messages,temperature等转换为第三方模型所需的格式。{{prompt}}是占位符代表用户输入的代码上下文{{temperature}}等同理responseMappingJSONPath 表达式定义如何从第三方模型的响应中提取content和stopReason。Ollama 返回choices[0].delta.contentLM Studio 返回choices[0].message.content这里必须精确匹配。实操心得我最初把responseMapping.content写成choices[0].message.content结果发现 DeepSeek 模型返回的是delta结构导致补全内容为空。后来加了一行日志打印原始响应体才定位到问题。强烈建议你在首次配置新模型时先用curl手动发一个测试请求把返回的 JSON 完整粘贴到在线 JSONPath 测试器里验证路径是否正确。3.3proxy-server.py协议转换核心逻辑200 行代码的精华这个 Python 脚本是整个方案的“翻译官”它用最少的代码实现了最复杂的协议桥接。以下是其核心逻辑的伪代码还原真实代码已做混淆保护此处展示原理# 1. 接收 Claude Code 的 /v1/messages 请求POST app.post(/proxy) async def proxy_request(request: Request): raw_body await request.body() claude_req json.loads(raw_body) # 2. 根据 claude_req[model] 查 models.json 获取配置 model_config load_models_json().get(claude_req[model]) if not model_config: raise HTTPException(400, Model not found in config) # 3. 构造第三方模型请求体使用 Jinja2 渲染 template Template(model_config[requestTemplate]) ollama_req_body template.render( promptextract_prompt_from_claude_messages(claude_req[messages]), temperatureclaude_req.get(temperature, model_config[temperature]), maxTokensclaude_req.get(max_tokens, model_config[maxTokens]) ) # 4. 转发请求到第三方服务如 Ollama async with httpx.AsyncClient() as client: resp await client.post( model_config[apiEndpoint], contentollama_req_body, headers{Content-Type: application/json} ) # 5. 将第三方响应转换为 Claude Code 能识别的格式 ollama_resp resp.json() claude_resp { content: [ {text: extract_content_from_ollama(ollama_resp, model_config)} ], stop_reason: extract_stop_reason_from_ollama(ollama_resp, model_config), model: claude_req[model] } return JSONResponse(claude_resp)其中最关键的两个函数extract_content_from_ollama()和extract_stop_reason_from_ollama()就是根据responseMapping中的 JSONPath 表达式动态求值。我们没有用笨重的jsonpath-ng库而是用 Python 内置的eval()安全沙箱仅允许dict/list/str/int操作配合正则预检确保表达式不会执行任意代码。实操心得在调试代理服务时不要只看最终返回结果。我在proxy-server.py里埋了三级日志DEBUG 级打印原始 Claude 请求体、INFO 级打印转发后的 Ollama 请求体、ERROR 级打印第三方服务返回的原始响应体。当补全失败时直接tail -f proxy.log三行日志就能定位是请求没发出去、还是响应解析错了。这个习惯帮我节省了至少 20 小时的无效排查时间。4. 实操过程全记录从零开始手把手带你跑通第一个模型4.1 准备工作确保你的本地环境已就绪在运行脚本前请按顺序确认以下 4 项Claude Code 桌面客户端已安装访问 https://claude.ai/download 注意必须是.dmg或.exe文件网页版无效macOS 用户拖拽Claude Code.app到Applications文件夹不要放在Downloads或桌面Windows 用户以管理员身份运行安装程序安装路径保持默认C:\Program Files\Claude Code\验证启动 Claude Code右下角状态栏应显示claude-3-sonnet或类似字样。Ollama 已安装并运行访问 https://ollama.com/download 下载对应系统安装包安装后终端执行ollama --version确认输出 ≥0.3.0启动服务ollama serve保持终端常驻或配置为系统服务拉取模型ollama pull deepseek-coder:32b约 20GB需耐心等待验证curl http://localhost:11434/api/tags应返回包含deepseek-coder的 JSON。Python 3.9 环境已就绪终端执行python3 --version确认 ≥3.9若未安装macOS 推荐brew install python3Windows 推荐从 python.org 下载安装包勾选 “Add Python to PATH”验证pip3 list | grep fastapi若无输出则后续脚本会自动安装。关闭所有安全软件干扰macOS临时关闭 Gatekeepersudo spctl --master-disable脚本执行完再恢复sudo spctl --master-enableWindows将Claude Code目录和~/.claude-code-extensions/添加到 Defender 白名单原因脚本需向app.asar.unpacked写入文件部分安全软件会误判为“恶意行为”并拦截。提示以上步骤看似繁琐但每一步都是为了规避后续 90% 的常见失败。我见过太多人跳过 Ollama 验证结果脚本跑完发现模型列表为空折腾半天才发现是ollama serve没启动。4.2 执行一键脚本3 分钟见证奇迹现在打开终端macOS/Linux或 PowerShellWindows执行以下命令# 下载脚本国内用户推荐用 gitee 镜像避免 github 限速 curl -fsSL https://gitee.com/claude-code-ext/install/raw/main/install-claude-code-extension.sh -o install.sh # 赋予执行权限 chmod x install.sh # 运行macOS/Linux ./install.sh # Windows PowerShell需以管理员身份运行 Set-ExecutionPolicy RemoteSigned -Scope CurrentUser ./install.sh脚本运行时你会看到类似这样的实时输出 正在检测环境... ✅ 操作系统Darwin (macOS) ✅ 已安装 curl, jq, python3, pip ✅ Claude Code 已安装于 /Applications/Claude Code.app ✅ 用户数据目录/Users/yourname/Library/Application Support/Claude Code ✅ 正在备份原 app.asar.unpacked... ✅ 备份完成backup-20240520-142315.tar.gz ✅ 正在初始化扩展目录 ~/.claude-code-extensions... ✅ 正在注入 UI 补丁到 /Applications/Claude Code.app/Contents/Resources/app.asar.unpacked... ✅ 正在部署代理服务... ✅ 代理服务已启动监听 http://localhost:8080/proxy ✅ 正在检查 Ollama 服务... ✅ Ollama 健康检查通过 ✅ 所有服务启动成功请重启 Claude Code 桌面客户端关键动作此时请手动关闭 Claude Code 应用不是关窗口是右键 Dock 图标 → Quit然后重新双击启动。这是必须的一步因为 Electron 需要重新加载preload.js。4.3 首次验证在编辑器里亲手触发一次补全重启后Claude Code 界面看起来和之前一模一样但细微之处已有变化右下角状态栏原本固定的claude-3-sonnet变成了可点击的下拉箭头点击后出现DeepSeek Coder 32B、Qwen2.5 Coder 7B、CodeLlama 13B Instruct三个选项新建文件测试CmdNmacOS或CtrlNWindows新建一个空白文件将语言模式设为Python右下角点击Plain Text→ 选择Python输入触发代码def fibonacci(n): Calculate the nth Fibonacci number. 按下CmdEntermacOS或CtrlEnterWindows你会看到编辑器右上角出现一个旋转的加载图标约 2–3 秒后光标下方自动补全if n 1: return n else: return fibonacci(n-1) fibonacci(n-2)如果补全成功说明整个链路——从 UI 选择、请求劫持、代理转发、Ollama 推理、响应转换——全部打通。此时你可以打开~/.claude-code-extensions/proxy.log会看到类似这样的日志INFO: POST /proxy received for model deepseek-coder-32b INFO: Forwarding to http://localhost:11434/v1/chat/completions INFO: Response mapped: contentif n 1:\n return n\nelse:\n return fibonacci(n-1) fibonacci(n-2) stop_reasonstop实操心得第一次补全延迟可能略高Ollama 首次加载模型到 GPU 显存但第二次起就会快很多。如果你等了 10 秒还没反应立刻检查proxy.log90% 的问题是apiEndpoint地址写错比如少了个/v1或者requestTemplate里的{{prompt}}占位符没被正确替换。4.4 进阶操作添加自己的私有模型假设你有一个微调好的my-codellama-7b-finetuned模型已通过ollama create注册现在想把它接入 Claude Code启动你的模型服务ollama run my-codellama-7b-finetuned确保它在http://localhost:11434上提供服务编辑~/.claude-code-extensions/models.json在末尾添加新条目my-codellama-7b-finetuned: { name: My Fine-tuned CodeLlama 7B, id: my-codellama-7b-finetuned, apiEndpoint: http://localhost:11434/v1/chat/completions, apiKey: , temperature: 0.1, maxTokens: 1024, supportsStreaming: true, requestTemplate: { model: my-codellama-7b-finetuned, messages: [ {role: system, content: You are an expert Python developer at Company X. Follow our internal style guide strictly.}, {role: user, content: {{prompt}}} ], stream: true, temperature: {{temperature}}, max_tokens: {{maxTokens}} }, responseMapping: { content: choices[0].delta.content, stopReason: choices[0].finish_reason } }无需重启 Claude Code模型列表是运行时动态加载的保存文件后下拉菜单里立刻会出现新选项验证选择它输入class User:按CmdEnter观察补全是否符合你的微调预期比如自动加上__init__方法和公司特定的 docstring 格式。这个过程证明了方案的热插拔能力——你可以在不中断开发的前提下随时切换、测试、对比多个模型这才是真正提升研发效率的生产力工具。5. 常见问题与排查技巧实录那些我没写在文档里的坑5.1 模型列表为空90% 是这个原因现象重启 Claude Code 后右下角状态栏仍是灰色的claude-3-sonnet点击无下拉菜单或下拉后只有官方模型。排查路径首先检查~/.claude-code-extensions/models.json是否存在且格式正确用jq . ~/.claude-code-extensions/models.json验证 JSON 有效性检查app.asar.unpacked/src/common/models.ts是否被成功修改搜索文件内容应看到export const SUPPORTED_MODELS window.CLAUDE_MODELS || [];而不是原来的硬编码数组检查app.asar.unpacked/src/renderer/main.tsx末尾是否添加了import ./preload;终极验证打开 Claude Code 的开发者工具CmdOptionI在 Console 里输入window.CLAUDE_MODELS应返回一个包含你定义模型的对象若返回undefined说明preload.js未加载或执行出错。我踩过的坑某次 macOS 系统更新后app.asar.unpacked目录权限变为root:wheel导致普通用户无法写入main.tsx。脚本日志显示“注入成功”但实际文件没改。解决方案是脚本增加权限修复步骤sudo chown -R $USER:$GROUP ~/.claude-code-extensions sudo chown -R $USER:$GROUP /Applications/Claude Code.app/Contents/Resources/app.asar.unpacked。5.2 补全卡住不动检查代理服务和模型服务的连通性现象点击模型后状态栏显示Loading...但一直不返回结果proxy.log里也没有新日志。分步诊断Step 1确认代理服务存活curl http://localhost:8080/proxy/health应返回{status:ok}若连接被拒执行ps aux | grep proxy-server.py看进程是否存在不存在则手动启动python3 ~/.claude-code-extensions/proxy-server.pyStep 2确认 Ollama 服务可达curl http://localhost:11434/health应返回{status:ok}若失败检查ollama serve是否在运行端口是否被占用lsof -i :11434Step 3模拟一次完整请求链路# 构造一个最小化 Claude 请求体 echo {model:deepseek-coder-32b,messages:[{role:user,content:def hello(): pass}]} test.json # 直接调用代理服务 curl -X POST http://localhost:8080/proxy -H Content-Type: application/json -d test.json若此处返回500 Internal Server Error说明requestTemplate或responseMapping