# llm-web **Repository Path**: lbrave/llm-web ## Basic Information - **Project Name**: llm-web - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-08-25 - **Last Updated**: 2026-09-25 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # llm-web 桥接代理(本地大模型 API 人工中继) 本地 agent 无法访问外网大模型服务时的桥接工具:**代理伪装成大模型 API 端点**,agent 的请求挂起后,在 Web 界面把入参复制到外网网页查询,再把结果粘贴回来,代理按原协议格式(非流式 JSON / SSE 流式)返回给 agent。 ``` agent ──请求──▶ 本地代理(127.0.0.1:8765) ──挂起等待──┐ ▲ │ │◀──协议格式返回──── 你粘贴结果到 Web 界面 ◀── 你复制入参到外网网页查询 ``` ## 快速开始 ```bash # 方式一(推荐):双击 run.bat,启动后自动打开管理界面 # 方式二:命令行启动(同样自动打开浏览器) python app.py # 方式三:仅启动服务不打开浏览器 python -m uvicorn app:app --host 127.0.0.1 --port 8765 ``` 管理界面: 依赖:Python 3.10+、`uvicorn`、`starlette`(`pip install -r requirements.txt`;本机已安装则无需联网安装)。 可选环境变量: | 变量 | 默认 | 说明 | |---|---|---| | `PORT` | 8765 | 监听端口 | | `HOST` | 127.0.0.1 | 监听地址(不建议改公网) | | `LLM_BRIDGE_NO_BROWSER` | 未设置 | 设为 `1` 时启动不自动打开浏览器 | | `LLM_BRIDGE_TIMEOUT` | 600 | 挂起请求等待人工回复的超时秒数 | | `LLM_BRIDGE_CHUNK_DELAY` | 0 | 流式输出每块之间的延迟秒数(模拟打字效果,建议 0) | ## agent 配置 ### Claude Code(Anthropic 协议) ```bash export ANTHROPIC_BASE_URL=http://127.0.0.1:8765 export ANTHROPIC_API_KEY=anything # 任意值,代理不校验 ``` 或在 `~/.claude/settings.json` 中: ```json { "env": { "ANTHROPIC_BASE_URL": "http://127.0.0.1:8765", "ANTHROPIC_API_KEY": "anything" } } ``` ### OpenAI 兼容 agent(Dify / LangChain / 各类本地 agent) ```bash export OPENAI_BASE_URL=http://127.0.0.1:8765/v1 export OPENAI_API_KEY=anything # 模型名随意填写,代理会原样回显;可用 /v1/models 查看 ``` ### DeepSeek Harness(deepseek-harness-dsh) 它的默认请求路径是 `{baseURL}/chat/completions`(**不带 /v1 前缀**),本代理已兼容,两种写法都可用: ```yaml # $DSH_HOME/settings.yaml llm-deepseek: baseURL: http://127.0.0.1:8765 # 或 http://127.0.0.1:8765/v1 ``` 或环境变量: ```bash export DEEPSEEK_BASE_URL=http://127.0.0.1:8765 export DEEPSEEK_API_KEY=anything ``` > 代理对 `/chat/completions`、`/v1/chat/completions`、`/messages`、`/v1/messages` 等带任意前缀的路径都识别;`GET /models`、`GET /v1/models` 均返回模型列表。 ## 使用流程 1. 启动代理,保持 Web 界面打开(自动刷新)。 2. 本地 agent 照常发起请求(请求会挂起,最多等待 10 分钟)。 3. 在 Web 界面找到对应请求卡片: - 聊天式网页:点「复制完整对话」或单条消息的「复制此条」; - API Playground:点「复制原始 JSON」。 4. 把查询结果粘贴回卡片的文本框,点「提交结果 → 返回给 agent」。 5. agent 立即收到按协议格式包装(或原样透传)的回复。 > 支持流式(SSE):agent 请求带 `"stream": true` 时,代理把结果拆成块模拟流式返回,非流式请求则一次性返回。 > 多请求可同时挂起、乱序回复。**同一个会话只占一张卡片**:后续轮次会自动复用那张卡, > 卡片位置保持不变(详见下文「会话归并」);完全相同的请求体(客户端超时重试)同样复用,不会重复。 ## curl 测试 ```bash # Anthropic 非流式 curl -s http://127.0.0.1:8765/v1/messages \ -H "x-api-key: anything" -H "content-type: application/json" \ -d '{"model":"claude-x","max_tokens":200,"messages":[{"role":"user","content":"你好"}]}' # Anthropic 流式 curl -N http://127.0.0.1:8765/v1/messages \ -H "x-api-key: anything" -H "content-type: application/json" \ -d '{"model":"claude-x","max_tokens":200,"stream":true,"messages":[{"role":"user","content":"你好"}]}' # OpenAI 兼容非流式 curl -s http://127.0.0.1:8765/v1/chat/completions \ -H "content-type: application/json" \ -d '{"model":"gpt-x","messages":[{"role":"user","content":"你好"}]}' # OpenAI 兼容流式 curl -N http://127.0.0.1:8765/v1/chat/completions \ -H "content-type: application/json" \ -d '{"model":"gpt-x","stream":true,"messages":[{"role":"user","content":"你好"}]}' # 查看挂起请求,然后粘贴结果(把 {id} 换成界面上的请求 ID) curl -s http://127.0.0.1:8765/api/requests curl -s http://127.0.0.1:8765/api/requests/{id}/resolve \ -H "content-type: application/json" -d '{"text":"你好,我是回复内容"}' ``` ## 让 harness 执行工具(工具调用桥接) 网页端输出工具调用 JSON 粘贴回来后,会被转成 OpenAI `tool_calls` / Anthropic `tool_use`,harness 会**真正执行**该工具(读文件、写文件、跑命令等)。 **最简单的用法**:请求卡片上有「复制工具调用提示词」按钮(仅带 tools 的请求显示),一键复制后粘贴到网页端,网页模型就会按要求输出工具调用 JSON;把它粘贴回响应区提交即可。 手动格式(也可自己写): 单个工具调用: ```json {"type":"tool_use","name":"read","input":{"file_path":"D:\\project\\llm-web\\README.md"}} ``` 多个工具调用(并行,按执行顺序排列,元素个数不限): ```json {"content":[ {"type":"tool_use","name":"read","input":{"file_path":"D:\\a.txt"}}, {"type":"tool_use","name":"glob","input":{"pattern":"*","path":"D:/project/llm-web"}} ]} ``` 网页端提示词模板(手动场景,把 `<工具清单>` 换成请求卡片上的工具列表): ``` 你现在被一个本地桥接代理拦截在大模型 API 层,代替 agent 做下一步决策。 请只输出工具调用(一个或多个),必须用 JSON 表示,并用 ```json 代码块包裹,不要输出任何解释文字。 单个工具调用: {"type":"tool_use","name":"<工具名>","input":{...参数...}} 多个工具调用(互不依赖、可并行的调用放在同一次输出里,按执行顺序排列): {"content":[{"type":"tool_use","name":"<工具名>","input":{...参数...}},{"type":"tool_use","name":"<工具名>","input":{...参数...}}]} 禁止使用 <工具名>... 之类的标签格式,工具调用必须且只能用上述 JSON 表示。 如果没有合适的工具,改为输出: {"type":"text","text":"<你的回复>"} 可用工具:<工具清单> 用户最后一条消息:<消息内容> ``` 识别注意事项: - **直接输出 tool_use JSON 对象**(可带 ```json 围栏);`{"content":[{"type":"tool_use",...}]}` 形式支持**多个并行工具调用**(每个元素一个独立调用,harness 会按顺序调度、可并行的并发执行); - 不要包 `{"code":0,"data":...}` 信封,不要出现 `choices` / `type:"message"` 字段(会被当作完整 API 响应透传); - Windows 路径用正斜杠或双反斜杠最稳妥(`D:/project/llm-web` 或 `D:\\project\\llm-web`),单个反斜杠代理也会自动修复; - 工具执行后 harness 会带着工具结果发起下一轮请求,继续在界面上粘贴下一步即可完成整个循环。 **常见疑问:粘贴了网页返回的完整响应 JSON,为什么没有触发工具调用?** 因为那段 JSON 的 `message` 里只有 `content` 文本、没有 `tool_calls`(`finish_reason` 为 `stop`)——桥接原样透传后,harness 把它当作最终文本回复,本轮结束。要让 harness 执行工具,粘贴的内容必须携带工具调用(tool_use JSON,或含 `tool_calls` 字段的完整响应)。 ## agent 类型适配 界面顶部有「agent 类型」下拉,选择会记住(localStorage)。它影响两件事: 1. **提示词** —— 「复制工具调用提示词」按钮改由后端按 agent 类型生成(`/api/requests/{id}/prompt`),不同 agent 的约束不同; 2. **输出封装** —— 粘贴回来的结果先按 agent 规则整理,再交付给 agent。 | 类型 | 适用 | 适配内容 | |---|---|---| | `auto`(默认) | — | 按协议 / 工具名 / 模型名 / system 提示自动判断,卡片徽标显示判定依据 | | `claude-code` | Claude Code | 提示词强调工具名逐字符匹配(PascalCase);封装时把 `read` / `READ` 纠正回 `Read` | | `deepseek-harness` | dsh | 提示词要求小写工具名、并行用 `content` 数组;封装时 `bash`/`shell`/`sh` → `pwsh`、`cat` → `read` | | `codex` | Codex | 提示词强调 `apply_patch` 优先;封装时同样做工具名规范化 | | `generic` | Dify / LangChain / 自研 | 不做特化,保持原行为 | 优先级:**界面选择 > 请求头 `X-LLM-Bridge-Agent` > 自动检测**。请求头适合同时挂多个 agent 时逐个指定: ```bash curl -s http://127.0.0.1:8765/v1/messages \ -H "X-LLM-Bridge-Agent: claude-code" -H "content-type: application/json" \ -d '{"model":"claude-x","max_tokens":200,"messages":[{"role":"user","content":"你好"}]}' ``` > 工具名大小写是个真实陷阱:Claude Code 的工具名区分大小写,网页模型输出 `read` 而请求里声明的是 `Read` 时, > 旧逻辑会判定"工具不存在"并退化成一段普通文本,工具永远不会执行。适配层会把这类变体纠正回请求里声明的形式; > 确实不存在的工具名仍按文本降级,不会凭空捏造。 **扩展新的 agent 类型**:只改 `agents.py`,加一个 `AgentProfile` 子类(按需覆盖 `score` / `build_prompt` / `adapt_result` / `tool_aliases`)并注册进 `REGISTRY`,HTTP 层与界面下拉自动生效。 ## 粘贴内容的识别规则 代理会自动识别粘贴结果的形态并**封装成 agent 期望的响应格式**: - **纯文本** → 原样封装为 assistant 回复(按请求协议); - **agent 式输出**(网页端返回的含 ``/``/`` 等伪工具调用标签的文本)→ 自动提取为**真正的工具调用**:**请求中下发过的所有工具名**(`ask_user_question`、`web_search`、`todo_write`、`pwsh`、`subagent` 等)都能识别,与请求中的 tools 匹配时生成 `tool_calls` / `tool_use`,harness 会实际执行;参数按请求里该工具的 JSON Schema 自动校正类型(数字/布尔/数组/对象),保证 harness 校验通过;不匹配时清洗为可读文本(`【工具调用 glob】 pattern=*, path=...`)再封装。``/``/`` 会自动映射到 harness 的 `pwsh`/`read` 工具名; - 工具调用的 wire 格式(`delta.tool_calls` 增量、`finish_reason: "tool_calls"`、`[DONE]` 哨兵)与 `llm-response-format.md` 记录的一致,流式/非流式均按该规范输出; - **JSON 信封**(`{"code":0,"data":{"answer":"..."}}`、`{"result":"..."}` 等)→ 自动抽取回复文本再封装; - **标准 SSE 流**(`data:` 行开头的 OpenAI/Anthropic 流式出参,如真实 API 转发的原始输出)→ 与请求协议一致时**原样中继**给 agent(只补齐缺失的 `[DONE]` / `message_stop` 终止哨兵,不改任何内容);协议不一致时从流中提取文本重新封装;非流式请求会把它装配成完整响应 JSON; - **完整 API 响应 JSON**(含 `choices` 或 `type:"message"`)→ 原样透传;流式请求下忠实重放(保留 `tool_calls` / `reasoning_content` / `usage`);协议不匹配时自动跨协议转换(保留工具调用,`stop_reason`/`finish_reason` 同步映射); - **tool_use JSON**(`{"type":"tool_use","name":...,"input":...}`)→ 构造工具调用块返回(best-effort); - 内容含 `[DONE]` / `[TRUNCATED]` 标记 → `finish_reason`/`stop_reason` 按截断处理。 ## 模型会话页(将请求转发到真实大模型 API) 除人工中继外,代理还提供 `/models` 页(也可用 `/llm`):把 agent 请求**直接转发到真实大模型 API**,流式输出整合成完整响应 JSON,复制回待处理卡片即可返回给 agent。 - 入口:地址栏访问 `http://127.0.0.1:8765/models`; - 复制「完整 JSON 回复」成功后,页面会**自动清空「发送到模型」输入框**,避免把同一请求重复发送;结果区仍保留,方便再次复制; - **长回复健壮性**:流式增量合并到同一个文本节点、滚动按帧合并,避免长输出把页面卡死;复制优先用 Clipboard API,失败时回退 `execCommand`;内容特别大时建议点「⬇ 下载 JSON」,不经过剪贴板、不受大小限制; - **截断可见**:若上游流未正常结束(缺少 `[DONE]`/`finish_reason` 或 `message_stop`),代理会**明确告警**并给出已收到的部分结果,不再静默返回看似正常实则残缺的 JSON; - 模型清单保存在服务器 `llm_models.json`,**可在网页上直接增删改**:点页面右上角「⚙ 模型配置」; - 每个模型字段:`key`(唯一标识,**支持中文**、字母、数字、下划线、点、连字符;纯本地标识,不会发给上游)、`label`(显示名)、`protocol`(`openai` / `anthropic`)、`base_url`、`model`(转发上游时使用的模型名)、`api_key`; - 编辑时 `api_key` 留空表示**保持原密钥不变**,勾选「清除已保存的密钥」才会清空; - **默认模型**:`llm_models.json` 顶层的 `default_model` 决定自动创建的会话用哪个模型(极简模式补位的格子、新建会话的初始值、会话在模型清单到达前就建好时的回填)。配置页里点某个模型的「设为默认」即可切换,该模型会挂上「默认」标记;没配、或它指向的模型已被删除时,回落到清单第一个;改名的正好是默认模型时默认跟着改名走; - 「测试连接」用当前表单值发一个最小请求,验证 `base_url` / `api_key` / `model` 是否可用; - 保存后立即生效、无需重启;接口**不返回 `api_key` 的任何内容**(含打码片段),只用 `has_key` 说明是否已配置;因此编辑已有模型时无需重填密钥,可直接修改显示名 / 模型名 / `base_url` / 协议。 对应接口(非本机访问需先登录): | 方法 | 路径 | 说明 | |---|---|---| | GET | `/api/llm/models` | 模型清单(隐藏明文密钥),附带 `default_model`(已回落到清单内某个 key) | | POST | `/api/llm/models` | 新增/更新模型;传 `original_key` 表示编辑(支持改名),`clear_key: true` 表示清空密钥 | | POST | `/api/llm/models/delete` | 删除模型(body: `{"key": "..."}`);删的是默认模型时自动回落到清单第一个 | | POST | `/api/llm/models/default` | 设置默认模型(body: `{"key": "..."}`) | | POST | `/api/llm/models/test` | 测试连通性(传表单值,或已保存模型的 `key`) | | POST | `/api/llm/chat` | 转发请求到选定模型,SSE 返回 `delta` + 整合后的完整响应 | 回归测试: | 命令 | 覆盖 | |---|---| | `python test_models_config.py` | 模型配置增删改(临时起实例,跑完自动还原 `llm_models.json`) | | `python test_long_stream.py` | 超长流完整性 + 上游静默截断告警 | | `python test_sender.py` | 发送方指纹:同一发送方多轮一致、换工作目录/显式指定后区分 | | `python test_slots.py` | 槽位注册表:单调分配、重启延续、重号防护、归零重排 | | `node test_ui_dom.js` | 用 jsdom 真实加载两个页面并驱动交互:会话顺序固定、关闭/恢复、极简模式固定槽位、多会话各自独立发送 | | `node test_slot_geom.js` | 两页槽位几何契约一致性(CSS 变量与几何函数逐字比对)+ 按屏幕/分屏/缩放核算排布与槽位坐标 | `test_ui_dom.js` 需要 jsdom,用 `NODE_PATH` 指向装有 jsdom 的 node_modules,例如: ```bash NODE_PATH="<工作区>/node_modules" node test_ui_dom.js ``` ## 极简模式:固定槽位网格 两个页面右上角都有「⚡ 极简模式」开关,状态存在浏览器 localStorage(key `llm-web.minimal`),**两页共用同一个开关**,切一次即可。 极简模式存在的理由:用模拟鼠标键盘的程序在内网桥接页与安全浏览器模型页之间搬数据时,**只能靠固定坐标点击**。所以极简模式下会话不再是会随内容伸缩的卡片,而是**尺寸固定、位置固定的槽位**。 | 项 | 约定 | |---|---| | 槽位号 | 即会话身份,一次分配、永不变动、删除也不复用;桥接页 `S5` 与模型页 `S5` 必须是同一个会话 | | 槽位尺寸 | 三行固定高(编号/按钮/状态 · 输入+发送 · 摘要+模型),格高由 CSS 变量算出,两页必须一致 | | 空位 | 已关闭/已清空的槽位**保留编号并渲染成空位**,不塌陷,后面的槽位不会前移 | | 分页 | 一页放不下时翻页,分页条位置固定,脚本可按固定坐标翻页 | | 定位锚点 | 槽位内所有可点元素都带 `data-hit`(`no`/`copy`/`close`/`input`/`submit`/`read`/`model`),坐标表按它导出 | | 参数同步 | 两页网络不通,用布局抽屉里的「复制参数 / 粘贴参数」离线同步;几何指纹不一致会直接告警 | **默认按 1920×1080 的右侧 1/4 上下分屏(= 两个 1/8 窗)标定**:真实视口 478×432,槽位 1 列 × 4 行、格 460×84,网格占 476×424(宽余 2px、高余 8px)。左侧 3/4 屏留给正常办公,右侧 1/8 窗专供脚本操作。 两页共用的开关入口:管理页在页头,模型页在页头与槽位工具条各一个。工具条与分页条里的文字宽度是固定的 —— 否则文字一变长就会把按钮挤出 478px 宽的窗口,而按钮位置正是脚本的坐标契约。 改槽位相关 CSS 时:**两个页面必须同时改**,改完跑 `node test_slot_geom.js` 确认契约仍然一致,并重新导出坐标表。 > 极简模式是**做减法而不是砍内容**:会话本身的标识、内容入口和提交/复制通道都不隐藏,只去掉辅助按钮和完整对话折叠。非极简模式仍是人工手动操作,用常规卡片列表,不受槽位约束。 ## 粘贴即发送 / 复制即清空(两种模式都生效) | 位置 | 行为 | |---|---| | 管理页 · 结果输入框 | 检测到粘贴动作 → **自动提交**;提交成功后**清空输入框** | | 模型会话页 · 请求输入框 | 检测到粘贴内容 → **自动发送到模型** | | 模型会话页 · 复制回复按钮 | 复制成功后**清空该会话的输入框**,便于直接粘贴下一个请求 | 实现说明: - `paste` 事件触发时输入框的 value 还没更新,因此统一延后一个事件循环再读取,不会读到空值; - 自动提交成功后会用强制刷新绕过「正在输入不打断」的保护,请求随即变成已回复状态; - 该行为不区分极简模式,普通模式下同样生效。 ## 会话顺序固定(管理页) 多个并发请求同时中转时,卡片位置原本会随「谁先被回复」而跳动,很难记住哪张卡对应哪条对话。现在: - 客户端按**首次出现的顺序**记住每个请求(localStorage `llm-web.session.order`),卡片位置从此固定; - 请求被回复后**仍留在原位**(渲染成已回复卡片),不会移出列表导致后面的卡片整体上移; - 点「**关闭会话**」把已结束的会话归档到「已回复历史」,随时可点「**恢复**」放回主列表末尾; - 只有**已回复**的卡片才有「关闭会话」按钮——未回复的不能关,避免误关掉还在等结果、会让 agent 一直等的请求; - 「已回复历史」现在的含义是「**已关闭会话的归档**」(不再自动镜像服务端全部历史); - 列表标题显示「会话列表(N)· 待回复 X · 已回复 Y」。 ## 多会话平铺(模型会话页) 模型会话页不再是「侧栏切换 + 单面板」,而是把所有会话**同时平铺**展示: - 每个会话一张面板(lane),各自拥有**独立的**模型下拉、请求输入框、发送按钮、流式输出区、复制回复、下载、删除; - 多个会话可以**同时各自流式输出**,互不干扰(每个会话有独立的运行态); - 面板用 `grid-template-columns: repeat(auto-fit, minmax(460px, 1fr))` 自适应:宽屏并排、窄屏自动竖排; - 侧栏会话列表保留,点一下即可滚动定位到对应面板;新建会话**追加到末尾**(不插队); - 会话编号(会话 1、会话 2…)持久化,删除会话后不重排,避免顺序错乱。 ## 复制按钮配色:绿 = 可点,灰 = 已处理 / 还不能点 | 按钮 | 绿色(可点) | 灰色 | |---|---|---| | 管理页「复制原始 JSON」 | 还没复制过 | **复制完成后变灰**显示「✓ 已复制」;另有「已回复历史」里不可点的灰 | | 模型会话页「📋 复制回复」 | 模型返回结束 | ① 未发送 / 流式输出中(置灰**禁用**);② **复制完成后变灰**显示「✓ 已复制」(仍可再点) | 两个页面的「已复制」灰色都表示"这条已经取过了",都**保持可点击**(点一下可重新复制),只是作为进度标记: - 管理页按**请求 id** 记忆,跨列表重绘保持,不会刷新一下又变回绿色; - 模型会话页按**会话**记忆,发起新一轮发送(或点清空)时自动重置为灰色未就绪; - 复制失败 → 短暂显示「复制失败」,约 1.6 秒后回到该处应有的状态; - 其它复制按钮(复制完整对话 / 复制此条 / 复制工具调用提示词等)点击成功后短暂变绿提示,随后恢复原样。 ## 发送方识别(判断是不是同一个 agent 在发) 请求 id(`req_xxx`)每次都是新的,光看 id 判断不出是不是同一个 agent 在连续发。所以后端按**跨轮次稳定的线索**算一个 8 位"发送方指纹",同一发送方的所有请求指纹相同: | 线索 | 说明 | |---|---| | **会话开头**(主线索) | 取请求 JSON 开头的 `system` + 第一条用户消息各前 400 字符做哈希。对话每轮只在**尾部**追加,开头不会变 → 同一会话的多轮请求指纹完全一致 | | `X-LLM-Bridge-Sender` 头(或 `?sender=`) | 显式指定;用它可以直接给每个 agent 起名字 | | `metadata.user_id` | Claude Code 会带,内含 session id → 界面显示成 `session xxxxxxxx` | | API key 哈希 | `authorization` / `x-api-key`,只存 12 位哈希、不落明文 | | `user-agent` | 例如 `claude-cli/1.0.3` | | 工作目录 | 从 system 提示词的 `` / `Working directory:` 里提取 | **为什么取"开头一段"而不是整段对话**:对话每轮都在增长,拿整段做哈希会让指纹每轮都漂移; 只取开头则天然稳定。反过来说,只靠 UA + 目录又分不开"同一个 agent 在同一个目录下的两个会话", **开头那一段才是区分不同会话的关键**。 悬停徽标可以看到「会话开头指纹」与「会话开头」摘要——摘要特意取**第一条用户消息**而不是 system 提示词,因为同一个 agent 的 system 往往千篇一律,用户第一句话才便于人眼核对。 界面上: - 每张卡片带一个「🔗 标签 · 短码」徽标,**同一发送方颜色和短码相同**,可直接比对; - 悬停徽标可看到指纹与全部原始线索,便于核对; - 列表标题汇总「发送方 N」——显示 **1** 就说明发送方一直没变。 想给某个 agent 固定名字,在它的环境变量里加一个自定义头即可,例如 Claude Code: ``` ANTHROPIC_CUSTOM_HEADERS="X-LLM-Bridge-Sender: 我的Agent" ``` ## 限制说明 - 仅监听 `127.0.0.1`,接受任意 API key —— 不要绑定公网地址; - `llm_models.json` 以**明文**保存各模型的 api_key,注意文件权限,勿提交到版本库; - agent 的 tool 调用场景只能靠粘贴 tool_use JSON 手动构造,属于 best-effort; - 超时(默认 10 分钟)后请求仍保留在界面,agent 重试会自动复用。