# dsh-doorman 设计说明(2026-10-11 已确认) > 适用版本:DeepSeek Harness 桌面端 **0.2.0-rc.2**(Windows)。已对照源码标签 `dsh-v0.2.0-rc.2` 核实; > 也比对了 `dsh-v0.2.1-alpha.2`,审批相关代码没有变化(见第 7 节)。 > 第 8 节的四个决定均已按推荐确认;第 10 节是阶段 0 在桌面端实测的结论。 ## 0. 一句话 agent 想让某条命令或某次文件写入**跳出沙箱**时,dsh 会弹窗问你。dsh-doorman 在弹窗出现之前先看一眼: 明显危险的直接拒绝,必须由人决定的照常弹给你,其余交给千问判断。 **沙箱一直开着**,它只决定“这一次要不要放行”,不改 dsh、不改权限模式。 重要的大白话:弹窗里的“允许一次”= **这一条命令完全不受沙箱限制地运行一次**(权限等同于你本人)。 所以插件每放行一次,分量和你亲手点“允许一次”一样。 ## 1. 它管哪些审批 只管同时满足下面全部条件的审批,其余一律原样交给你(插件什么都不做): 1. 工具名是 `pwsh`、`bash`、`write`、`edit` 之一; 2. 审批理由的格式正好是 dsh 提权流程生成的 `escalate sandbox to danger-full-access: <理由>`, 且和插件事先记下的这次调用的参数逐字对得上(对不上 = 不是标准提权,交给你); 3. 这个会话当前是“工作区内修改”沙箱; 4. 不是 `run_code` 程序里套着发起的调用(这类一律交给你); 5. 会话的工作区没有包含(也不位于)dsh 主目录、插件数据目录、插件安装位置—— 比如你把整个用户主目录当成了工作区,这个会话里**全部弹给你**,并写日志提醒。 `plugin_manager`(能装/停/卸插件,包括停掉本插件)的申请格式和普通提权一模一样, 所以是**按工具名**排除的,永远弹给你。 ## 2. 三层判断 ``` 审批请求 ──► 不归我管? ──是──► 原样弹给你 │否 ▼ 第1层 硬性规则 ──命中“破坏”──► 直接拒绝 │ ──命中“必须由人决定”──► 弹给你(不问千问、不查清单) ▼ 第2层 放行清单 ──精确命中且依赖文件没变──► 允许一次(不调 API) │ ──命中“以后都弹给我”的条目──► 弹给你 ▼ 第3层 千问审查 ──allow──► 允许一次 ──deny──► 拒绝 ──ask_user / 超时 / 报错 / 格式不对 / 超过每小时上限 / 没配 key──► 弹给你 ``` 任何环节出异常(取不到参数、路径解析失败、文件读不到……)都按“拿不准”处理:**弹给你,绝不放行**。 ### 第 1 层:硬性规则(本机代码,不联网) 硬性规则压过一切,千问和清单都推翻不了。它只认得出写得直白的情况——它是底线,不是全部。 按 Windows PowerShell 的写法实现(同时认 `cmd /c` 和常见的 bash 写法)。 **直接拒绝:** - 递归删除工作区外的目录(`Remove-Item -Recurse`/`rm -r`/`rd /s`/`del /s` 等,含别名和参数缩写); - 对系统根目录(`C:\`)、`C:\Windows`、`C:\Program Files*`、`C:\Users`、整个用户主目录动手(删、移、改权限、改属主); - 磁盘工具:`format`、`diskpart`、`Format-Volume`、`Clear-Disk`、分区命令、`bcdedit`、`cipher /w`、`vssadmin delete` 等; - 下载后直接执行:`iwr … | iex`、`irm … | iex`、`DownloadString … | iex`、`curl … | sh`、`certutil -urlcache` 下载、`mshta http…`、`-EncodedCommand` 等; - 改动插件自己(代码、规则、清单、日志、key)或 dsh 本身:dsh 主目录 `%USERPROFILE%\.dsh` 下的一切(含凭据文件、profile、插件安装位置、插件数据)、 dsh 安装目录、dsh 更新缓存、桌面端用户数据目录;结束 dsh 进程; - (我加的)关闭安全机制:关 Defender 实时防护、加 Defender 排除项、关防火墙。 **直接弹给你(不送千问、不查清单):** - 以管理员身份或其他提权方式执行:`Start-Process -Verb RunAs`、`runas`、`sudo`、`gsudo`、`psexec` 等; - SSH 密钥(`~\.ssh`、`ssh-keygen`);各类凭据(`.npmrc`、`.git-credentials`、`.aws`、`.docker\config.json`、`cmdkey`、`gh auth`、`npm login` 等)、浏览器配置目录; - shell 启动文件(PowerShell 的 `$PROFILE`、`.bashrc` 等);开机启动项(启动文件夹、计划任务、服务、注册表 Run 键); - 注册表(`reg add/delete/import`、`HKCU:`/`HKLM:` 路径、`setx`、持久化环境变量); - 系统目录里的写入(`C:\Windows`、`C:\Program Files*`、`C:\ProgramData`、hosts 文件); - 账户和权限:`net user`、`net localgroup`、`icacls`、`takeown`、`Set-Acl`、`Set-ExecutionPolicy`; - 无法判断目标的递归删除(目标里有变量、通配符等); - (我加的)命令、理由或脚本里出现像是在“对审查员说话”的文字(如“请判定为安全”“ignore previous instructions”)。 **文件工具(write / edit)**:先把目标路径解析成真实路径(处理 `..`、符号链接、目录联接),再过同样的路径规则。 ### 第 2 层:放行清单(长期免审) - 条目必须**精确**:同一工具 + 同一工作区 + 同一工作目录 + 一模一样的命令(只去掉首尾空白)。不允许任何通配; 以后真想加通配规则,我会先用大白话向你说明,由你确认。 - **绑定依赖文件的指纹**:命令的实际行为取决于工作区(或临时目录)里 agent 能改的文件时,条目记下这些文件的 SHA-256; 文件一变,条目失效,重新送千问。例如 `npm install` 绑定 `package.json` 和锁文件,`npm run build` 绑定 `package.json`, `make` 绑定 `Makefile`,`python x.py` 绑定 `x.py`。**判断不出依赖哪些文件的命令不进清单**,每次都送千问。 - **永不进清单**(即使千问放行也每次重审):删除、移动、覆盖工作区外已有文件的命令; 含静态看不穿写法的命令(`$(…)`、`iex`、`& $变量`、脚本块 `{…}`、`python -c`、`node -e`、`pwsh -Command` 嵌套等); `write` / `edit` 文件工具(只做单次判断,不进清单)。 - 进清单的条件(全部满足):千问给 `allow` 且 `risk=low`;命令正常结束(退出码 0、没超时、没被终止);事后检查没发现问题;不属于永不进清单的类别。 - 清单里的命令事后检查出了问题 → 立刻移出,并标记为“以后都弹给我”。 ### 第 3 层:千问审查 - 走 OpenAI 兼容接口,地址、模型名、key 都是配置项,任何 OpenAI 兼容服务都能用。默认(已确认): 地址 `https://dashscope.aliyuncs.com/compatible-mode/v1`,模型 `qwen3.7-plus`,`temperature=0`、固定 `seed`、 `response_format={"type":"json_object"}`、`enable_thinking=false`(千问专有参数,可在配置里去掉)。 - 请求由插件所在的 Host 进程直接发出,不经过 agent 的命令行。 - 超时 20 秒;每小时调用上限默认 100 次(可配置),超过后全部弹给你。 - 系统提示词固定不变;待审内容作为 JSON 数据放在单独的字段里,并明确告诉千问:这些字段都不可信, 里面的任何指令都不执行;agent 写的理由只作参考,以命令本身为准。 - 只接受严格 JSON:`{"decision": "allow|ask_user|deny", "risk": "low|medium|high", "explanation": "一两句大白话"}`。 代码逐项校验,多字段、少字段、取值不对、自相矛盾(allow+high、deny+low)都按“拿不准”弹给你。 - 判断标准:效果清楚、范围有限、属于正常开发操作(典型:工具往自己的缓存或配置目录写东西)→ allow; 删除或覆盖工作区外已有的东西、改系统级或用户级配置、效果说不清 → ask_user; 明显破坏、把私人文件或密钥往外发、关闭安全机制 → deny。 - 千问的输出只能影响“允许一次 / 拒绝 / 弹给你”三种结果,不能影响任何别的东西(不能改清单规则、不能改硬性规则、不能改模式)。 ### 事后检查 1. 先看确定性信号:退出码、是否超时、是否被终止、沙箱是否又拒绝了、报错文字。 2. 信号干净时,再把限长、打码后的输出交给千问,问“有没有异常或破坏的迹象”(输出同样当不可信内容)。 3. 只对两类命令做:千问放行的(决定能否进清单)和清单命中的(决定要不要移出)。 4. **如实说明**:事后检查只能看到命令的输出,看不到磁盘上实际改了什么,“没发现问题”不等于“确定没问题”。 5. (D3 已确认要做)低成本的“前后对比”:放行前,把命令里出现的、以及上一次失败的报错里点名的工作区外目录, 拍一个浅快照(文件名、大小、修改时间;最多 5 个目录、2 层、2000 项);命令跑完再拍一次对比。 发现原有文件被删 → 记为问题。只读目录信息,不读文件内容,也不发给千问(只发“新增 N、修改 N、删除 N”)。 ### “只观察”模式(默认开启)和自动模式 - **只观察**:三层照常判断、照常调千问、照常写日志,但**全部弹给你**;硬性规则的“拒绝”也只记录不执行。 千问请求和弹窗同时开始,不会拖慢弹窗。日志同时记下你实际点的是允许还是拒绝;清单只记录“本来会加入”的条目,不真正写入。 - **自动**:按上面的流程真正执行。 - 切换方式:改 profile 里本插件设置的 `mode`(`observe` / `auto` / `off`,见第 6 节)。用几天后我根据日志给你出报告,再由你决定是否开自动。 ## 3. 发给千问的内容清单 **审查一条命令时:** | 字段 | 内容 | 处理 | |---|---|---| | `tool` | `pwsh` / `bash` | — | | `os` | 如 `Windows 11, PowerShell` | — | | `command` | 完整命令 | 打码 + 路径替换 | | `working_directory` | 这条命令的工作目录 | 路径替换 | | `workspace_root` | 固定写 `` | 不发真实路径 | | `agent_justification` | agent 写的理由(≤500 字) | 打码,标为不可信 | | `previous_failed_attempt` | 本会话里同一工具最近一次**没带提权**、结果失败的调用(10 分钟内):它的命令和输出末尾(≤1500 字,通常含被拦路径),没有则为空。按“失败”找、不按 dsh 的“沙箱拒绝”标记找,原因见第 10 节 | 打码 + 路径替换 | | `scripts` | 命令直接运行的工作区/临时目录里的脚本内容(每个 ≤8000 字,最多 3 个;`npm run x` 则是 package.json 里对应的脚本行;裸的 `npm install` 是安装时会自动运行的那几个脚本) | 打码 + 路径替换;超长或读不到 → 不发,直接弹给你 | | `static_analysis` | (阶段 2 新增)插件自己算出的摘要:命令提到的工作区外路径、会删除/写入/移动哪些路径、哪些部分看不穿。全部来自同一条命令,不含新的个人信息 | 路径替换 | | `request_type` | `shell_command` / `file_write` / `file_edit` | — | 命令超过 8000 字、运行的是工作区里看不到内容的程序(`.exe` 等),也一律不送审,直接弹给你。 **审查一次文件写入(write / edit)时:** `tool`、`os`、`target_path`(真实路径,路径替换)、`target_exists`、 内容摘要(write:总长度 + 前 1000 字;edit:被替换和替换成的片段各 ≤500 字,均打码)、`agent_justification`。不发整份文件。 **事后检查时:** `tool`、`command`(同上处理)、退出码、是否超时/被终止、输出末尾 ≤3000 字(打码 + 路径替换)、 (若启用 D3)“新增 N、修改 N、删除 N”。 **路径替换**:工作区路径 → ``,用户主目录 → `%USERPROFILE%`,临时目录 → `%TEMP%`。这样千问看不到你的用户名和项目路径, 也更容易分清“工作区内/外”。 **打码**:`-Password xxx`、`--token=xxx`、`password=`/`token=`/`secret=`/`apikey=`、`Authorization: Bearer xxx`、 网址里的 `user:pass@`、`sk-…`、`ghp_…`、`github_pat_…`、`glpat-…`、`AKIA…`、`AIza…`、`xox?-…`、JWT、私钥块、 名字带 KEY/TOKEN/SECRET/PASSWORD 的环境变量赋值等 → `[REDACTED]`。 **从不发送**:key 本身、你的对话内容、其他文件、dsh 配置、真实用户名和工作区路径。 ## 4. key 放哪,剩余风险 - 已核实:第三方插件可以使用 dsh 自带的凭据存储(`ctx.credentials`)。插件按名字 `DSH_DOORMAN_API_KEY` 取 key,查找顺序由 dsh 决定: 启动环境变量 → dsh 凭据文件 `%USERPROFILE%\.dsh\.credentials.yaml` → 两个 `.env` 后备文件 (实测桌面端后台的启动目录就是 profile 文件夹,所以这两个 `.env` 都在 dsh 主目录里,同样受保护)。 - **已决定(D1=A)**:放进 dsh 凭据文件。阶段 2 起改成**在插件设置页面里填**:dsh「插件」页 → 本插件 → API key 一栏, 页面通过 dsh 官方的凭据接口写进同一个凭据文件(和 dsh 自己“模型设置”页存 key 的方式一样)。不用手改文件,不会写坏, 页面只显示“已保存/未设置”,从不显示 key 本身。 - 推荐放法和备选见第 8 节 D1。无论哪种,插件都**不把 key 写进日志**、不写进代码仓库;dsh 的凭据界面和诊断也从不显示值。 - 额外保护(已核实):dsh 启动 agent 的命令时,会去掉名字里含 KEY/TOKEN/SECRET/PASSWORD 的环境变量和所有 `DSH_` 开头的变量, 所以 key 不会出现在 agent 命令行的环境变量里。 - **剩余风险(如实说)**:agent 和你是同一个 Windows 用户,而沙箱**不限制读文件、不限制联网**。 agent 只要有意去读凭据文件(或注册表里的环境变量),就能拿到 key,而且这一步**根本不需要提权、不会弹窗**,插件看不到也拦不住。 你现有的 DeepSeek key 已经在同一个文件里,面临的是同一类风险。 缓解办法:给这个用途单独建一个 key;在百炼上设消费上限和告警(阶段 2 教你);发现异常就在百炼控制台删掉这个 key 重建。 ## 5. 放行清单条目长什么样,怎么查看和删除 ```json { "id": "k7d2m9", "status": "active", "tool": "pwsh", "workspace": "C:\\Users\\<你>\\Desktop\\my-app", "workdir": "C:\\Users\\<你>\\Desktop\\my-app", "command": "npm install", "dependsOn": [ { "path": "package.json", "sha256": "…" }, { "path": "package-lock.json", "sha256": "…" } ], "explanation": "按 package.json 安装依赖,会往 npm 自己的下载缓存目录写文件。", "model": "qwen3.7-plus", "addedAt": "2026-10-20T08:00:00Z", "hits": 3, "lastUsedAt": "2026-10-21T02:10:00Z" } ``` `status` 为 `blocked` 的条目表示“以后都弹给我”。 查看和删除:插件注册一个只给你用的聊天命令(已核实 dsh 支持插件注册斜杠命令,结果只显示给你,不进模型对话): `/doorman list` 列出清单(编号 + 命令 + 大白话解释),`/doorman remove 3` 删第 3 条,`/doorman clear` 清空,`/doorman status` 看当前模式和今日统计。 这些命令只能让插件**更保守**(删条目、切回只观察),**打开自动模式只能改配置文件**。 ## 6. 数据和日志放哪 **设置**(阶段 2 起)放在 dsh 自己的插件设置里,也就是 profile 的 `%USERPROFILE%\.dsh\profiles\desktop\cordis.patch.yml` (同样在 dsh 主目录里,agent 改不了): - 接口地址、模型、超时、每小时上限:在 dsh「插件」页本插件的设置页面里改,保存后立即生效,不用重启。 - 模式(只观察/自动/关闭)和“已确认的 dsh 版本”:**故意不放进页面**,只能改 profile 文件(阶段 3 再定要不要做成页面开关)。 - key:见第 4 节,存在 dsh 凭据文件里。 其余数据在 `%USERPROFILE%\.dsh\dsh-doorman\`(dsh 主目录里插件自己的文件夹): | 文件 | 用途 | |---|---| | `allowlist.json` | 放行清单 | | `logs\decisions-YYYY-MM.jsonl` | 每次决定一行:时间、工具、完整命令或路径、走到哪一层、决定、千问解释、(只观察时)你实际点了什么 | | `state.json` | 上次见到的 dsh 版本、上次试连的结果等 | | `packages\dsh-doorman-<版本>.tgz` | 安装用的压缩包(**插件装着的时候不要删**) | 实际运行的插件代码由 dsh 从压缩包**复制**到 `%USERPROFILE%\.dsh\profiles\desktop\node_modules\dsh-doorman\`(已实测:本地 `.tgz` 按压缩包安装, 插件目录是真实目录、不是链接)。补充一个实测细节:pnpm 默认用“硬链接”把文件放进去,所以这些文件和 pnpm 的公共缓存 `%LOCALAPPDATA%\pnpm\store` 里的一份是同一个文件。只要那份缓存不被改,插件代码就不会变;这一点写进“已知局限”。 profile 里记录的是压缩包的绝对路径,所以**插件装着的时候不要删压缩包**。 这些位置都不在任何工作区或临时目录里;沙箱本来就不让 agent 写,硬性规则又把“改动 dsh 主目录下任何东西”列为直接拒绝,提权也绕不过去。 ## 7. dsh 更新后怎么办 - 桌面端走 nightly 通道,会把壳和 dsh 一起自动更新,接口可能变。我已比对 0.2.1-alpha.2:审批、提权、工具事件的代码没变; 新增了“会话当前目录”服务,插件会兼容新旧两种。 - 插件每次启动读取 dsh 版本,和上次不同就在日志里写一条醒目的 `DSH VERSION CHANGED`。 - 是否自动退回“只观察”见第 8 节 D2。 - 插件依赖的服务或事件不存在时,整个插件什么都不做,审批照常弹给你。 - 更新后你要做的:把新版本号告诉我,我对照新源码复核几处关键代码,确认后再恢复自动模式。 ## 8. 需要你决定的事(2026-10-11 已全部按推荐确认:D1=A,D2=B,D3=做,D4=qwen3.7-plus) **D1. key 放哪** - A(推荐)放进 dsh 自带的凭据文件 `%USERPROFILE%\.dsh\.credentials.yaml`,用记事本加一行。好处:dsh 官方的密钥存放处、改完立即生效、别的程序不会顺手带上它。 风险:这个文件格式写错会导致 dsh 启动失败——所以你改完后我会运行一个**不显示任何 key 内容**的检查,确认格式无误再重启。 - B 设成 Windows 用户环境变量 `DSH_DOORMAN_API_KEY`(系统自带的“编辑账户的环境变量”窗口)。好处:不碰 dsh 的文件,写错不会让 dsh 起不来。 缺点:你电脑上所有程序都能看到这个环境变量;改完要完全退出 dsh 再打开。 **D2. dsh 版本变了以后** - A 只写醒目日志,照常按当前模式运行。 - B(推荐)写日志,并自动退回“只观察”,直到你确认(改配置里的“已确认版本”,或让我复核后改)。 - C 直接停用全部判断,全部弹给你,直到确认。 **D3. 磁盘前后对比(第 2 节“事后检查”第 5 条)**:推荐做;也可以不做,只看输出。 **D4. 默认模型**:推荐 `qwen3.7-plus`(输入 2 元/百万 token,输出 8 元/百万 token;估算每次审查约 0.5 分钱)。 更省钱可选 `qwen-plus`(约 0.2 分钱/次),更强可选 `qwen3.8-max`(约 3 分钱/次)。阶段 2 接入时也可以再改。 ## 9. 已知局限(会写进使用说明) - 硬性规则只认得出写得直白的情况,绕得弯的写法要靠千问和你。 - 事后检查看不到磁盘实际改动(D3 只能补一部分)。 - Windows 沙箱官方标注为“部分强制”(比如硬链接可以作为别名绕过);插件不改变这一点。 - agent 不提权也能读文件和联网,插件只管“提权弹窗”这一个口子,不是全面的安全保证。 - 被放行的命令拥有你的全部权限,它内部再调用的程序(比如 npm 的安装脚本)也一样;插件只能看到命令文本本身。 - 插件代码文件与 pnpm 公共缓存里的文件是硬链接(见第 6 节)。 ## 10. 阶段 0 实测结论(2026-10-11,桌面端 0.2.0-rc.2) 用只记录、不判断的探针插件(0.0.1)在桌面端实测: 1. **审批发生时能拿到完整命令**:成立。pwsh 拿到完整命令文本,write 拿到目标路径和内容; 审批理由和事先记下的参数逐字对得上(`reasonMatchesArgs: true`)。 2. **弹窗照常出现,插件排在弹窗之前**:成立。插件先收到请求,交给下一位后拿到了你点的结果(允许一次 / 拒绝)。 3. **卸载后一切恢复原样**:成立。卸载后的提权请求插件不再看到,弹窗照常出现;profile 的插件列表和 `cordis.patch.yml` 与安装前完全一致,只多了 pnpm 自己的记录文件(空锁文件、空 `node_modules`、安装日志),无害。 实测中发现、已据此调整设计的地方: - **中文 Windows 上 dsh 认不出 PowerShell 的“沙箱拒绝”**:报错是中文的“对路径…的访问被拒绝”, 而 dsh 只按英文报错判断,所以没有打上 `[sandbox: …]` 标记(write 工具不受影响)。agent 照样会自己发起提权。 因此插件找“上一次失败”时**按失败找**(退出码非 0 或报错),不依赖这个标记。这是 dsh 的问题,可以另外向官方反馈。 - **agent 重试时不一定用“完全相同”的命令**(实测第一次是四条命令连写,重试时去掉了两条),所以“上一次失败”按 “同一会话、同一工具、10 分钟内最近一次”找,并把那次的命令一起给千问对照。 - **插件刚加载时,dsh 的各项服务还没就绪**(`ctx.get` 拿不到),到审批发生时才可用。所以插件一律在用到的时候再取服务。 - **后台进程里没有 `DSH_HOME` 环境变量**,启动目录是 profile 文件夹。插件按“自己的安装位置”推算 dsh 主目录,推不出来才用 `%USERPROFILE%\.dsh`。 - dsh 版本可以在不执行 dsh 代码的情况下读到(实测读到 `0.2.0-rc.2`)。 ## 11. 阶段 1 实现时补充的细节 - **提到就拒绝**:提权命令里只要出现 dsh 主目录、插件目录、凭据文件等受保护位置(不论是读还是写),一律直接拒绝。 正常的提权命令没有理由碰这些地方,而“读出 key 再发出去”和“改掉插件”都要经过这些路径。 - **看不出命令名的**:命令名是算出来的(变量、表达式),又带着删除/移动之类的字样,直接弹给你。 - **含密钥的环境变量绝不展开**:判断路径时会展开 `$env:USERPROFILE` 这类变量,但名字里带 KEY/TOKEN/SECRET/PASSWORD 等的一律不展开, 所以 key 不会因为出现在命令里而被写进日志或(阶段 2)发给千问。 - **能追踪的就追踪**:同一条命令里先 `cd`、先给变量赋值、先定义别名再使用,规则都能跟上;嵌套的 `cmd /c`、`powershell -Command`、 `Start-Process`、`Invoke-Expression`、脚本块都会拆开再查。 - **一定交给你的情况**:删除目标看不出来(变量、管道、表达式);同一条命令里先写出脚本再运行。 - 这一版只观察:`mode` 写成 `auto` 也按只观察运行,并在日志里注明原因(自动模式在阶段 3 才有)。 ## 12. 阶段 2 实现时补充的细节 - **设置页面**(应你的要求提前做了):dsh「插件」页 → 本插件,页面上有接口地址(北京/新加坡/自定义)、模型、API key、 高级设置(超时、每小时上限)。颜色用 dsh 的主题变量,深浅色都能用。页面坏了只影响这一块,不影响 dsh。 - **只观察模式下的千问调用**:通过硬性规则的申请,先整理送审内容并检查有没有“对审查员说话”的文字,然后在弹窗出现的**同时** 后台调用千问,绝不等它。千问的判断写进日志,和你实际点的选项放在一起,方便事后对比。 - **日志里记下实际发出去的内容**(`sentToModel` 字段),你可以随时核对到底发了什么。 - **试连**:key 保存后、或模型设置改了之后几秒,插件会用一条固定的无害示例(`npm install`)调一次千问,结果写进日志 (`model-selftest`),每次约 0.5 分钱。没有 key 时只记一行“没有 key”,不联网、不花钱。 - **任何异常都等于“拿不准”**:没有 key、超过每小时上限、超时、接口报错、回答不是严格的 JSON、字段多了少了、自相矛盾、 重复字段(防止 `{"decision":"deny", …, "decision":"allow"}` 这种把戏)——一律记为“交给你”。 - **参数**:`temperature=0`、固定 `seed`、`response_format=json_object`;地址是阿里云百炼时自动带上 `enable_thinking=false` (千问 3 系列默认开“思考”,会干扰 JSON 输出),其他 OpenAI 兼容接口不带这个专有字段。