🎁 福利专区全网大模型免费应用 + 新用户福利 + 注册活动入口,低成本玩转 AI

OpenCode 免费路由 mirelconstantin/dsh-opencode-router

DeepSeek Harnessv1.1.1
mirelconstantin18 天前有提交已提供中文文档扫描:含敏感能力

免密钥调用 OpenCode 免费模型,自动切换代理出口

31.4/100
综合分
2
GitHub 星标
31
npm 周下载
—/5
用户评分

数据截至 2026/10/15 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。

安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add dsh-opencode-router
npm 源 · dsh-opencode-router 的发布者账号已校验与本仓库一致

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:已验证
是什么
dsh 原生插件 · model
装得上吗
本站已在 dsh 0.2.1-rc.2 下真实安装成功
非静态推断
L5 · 运行时冒烟等级说明 →
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 18 天前
档位由哪些信号合成

本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。

验证判据与等级说明 →
社区实装还没有人打卡 · 装过的第一个就是你
安装兼容性检查✓ 自动检查通过
展开查看检查明细 ↓

以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。

✓npm 包dsh-opencode-router @ 1.1.1
✓Node 引擎要求 ^22.19.0 || >=24.0.0 · 基线 Node 22.19 满足
✓dsh CLI 依赖未声明 dsh 版本约束
✓入口文件main/exports/bin 已声明

验证方式:npm registry 存在性 + package.json 静态校验 · 最后检查 2026/10/15 17:42:42

安全扫描自动 · 静态分析源码
v2.7.0
🔵 扫描:含敏感能力尚无人工评估
展开查看扫描说明与证据 ↓

静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。

可执行外部命令读写/删除本地文件发起外部网络请求
查看扫描证据(共 75 处,展示前 74 条)
  • 执行外部命令来自配置/变量
    lib/index.js:856
    const child = spawn(this.agentPath, this.args, { stdio: [
  • 执行外部命令固定命令
    lib/index.js:1000
    const killer = spawn("taskkill", force ? [
  • 读取本地文件
    lib/index.js:1225
    await stat(path);
  • 读取本地文件
    lib/index.js:1237
    const existing = (await readFile(paths.tokenPath, "utf8")).trim();
  • 写入本地文件
    lib/index.js:1241
    await mkdir(dirname(paths.tokenPath), { recursive: true });
  • 写入本地文件
    lib/index.js:1242
    await writeFile(paths.tokenPath, token + "\n", { encoding: "utf8" });
  • 写入本地文件
    lib/index.js:1243
    if (platform !== "win32") await chmod(paths.tokenPath, 384).catch(() => {});
  • 写入本地文件
    lib/index.js:1267
    await mkdir(paths.dataDir, { recursive: true });
  • 写入本地文件
    lib/index.js:1269
    await writeFile(tmpPath, JSON.stringify(config, null, 2), "utf8");
  • 删除本地文件
    lib/index.js:1270
    await rm(paths.configPath, { force: true });
  • 写入本地文件
    lib/index.js:1271
    await rename(tmpPath, paths.configPath);
  • 发起网络请求
    lib/index.js:1799
    const response = await fetch(providerBaseURL(port) + "/models", {
  • 发起网络请求
    lib/index.js:1814
    const response = await fetch(`http://127.0.0.1:${port}/healthz`, { signal: controller.signal });
  • 读取本地文件
    lib/index.js:2104
    for (const sibling of [join(here, "..", "..", "..", "legacy", "agent", exe), join(here, "..", "..", "legacy", "agent", exe)]) if (existsSync(sibling))
  • 发起网络请求
    lib/catalog-DdaxGYNb.js:428
    const response = await withTimeout(this.#fetch(this.#metadataUrl, { headers: { accept: "application/json" } }));
  • 写入本地文件
    lib/catalog-DdaxGYNb.js:574
    await mkdir(dirname(path), { recursive: true });
  • 写入本地文件
    lib/catalog-DdaxGYNb.js:575
    await writeFile(tmp, JSON.stringify(cache), "utf8");
  • 删除本地文件
    lib/catalog-DdaxGYNb.js:576
    await rm(path, { force: true });
  • 写入本地文件
    lib/catalog-DdaxGYNb.js:577
    await rename(tmp, path);
  • 读取本地文件
    lib/catalog-DdaxGYNb.js:580
    const raw = JSON.parse(await readFile(path, "utf8"));
  • 读取本地文件
    lib/messages-CLaBfusR.js:69
    data: (await readFile(path)).toString("base64"),
  • 发起网络请求
    lib/admission-BKoqnnTQ.js:623
    const echo = await withTimeout(deps.undici.request(ipEcho, { dispatcher: agent }), timeout, "probe");
  • 发起网络请求
    lib/admission-BKoqnnTQ.js:704
    const request = (url, init = {}) => withTimeout(deps.undici.request(url, {
  • 发起网络请求
    lib/ip-pool-C9FThTdV.js:1139
    const report = parseSubscription(await this.#fetch(url));
  • 发起网络请求
    lib/ip-pool-C9FThTdV.js:1208
    async #fetch(url) {
  • 读取本地文件
    lib/ip-pool-C9FThTdV.js:1484
    if (existsSync(bin)) return bin;
  • 执行外部命令来自配置/变量
    lib/ip-pool-C9FThTdV.js:1488
    const child = spawn(bin, ["version"], { stdio: "ignore" });
  • 写入本地文件
    lib/ip-pool-C9FThTdV.js:1504
    await mkdir(this.#dataDir, { recursive: true });
  • 写入本地文件
    lib/ip-pool-C9FThTdV.js:1506
    await writeFile(tmp, JSON.stringify(config, null, 2), "utf8");
  • 删除本地文件
    lib/ip-pool-C9FThTdV.js:1507
    await rm(this.#configPath, { force: true });
  • 写入本地文件
    lib/ip-pool-C9FThTdV.js:1508
    await rename(tmp, this.#configPath);
  • 执行外部命令来自配置/变量
    lib/ip-pool-C9FThTdV.js:1510
    const child$1 = spawn(binary, [
  • 执行外部命令来自配置/变量
    lib/ip-pool-C9FThTdV.js:1521
    this.#child = spawn(binary, [
  • 执行外部命令固定命令
    lib/ip-pool-C9FThTdV.js:1579
    const killer = spawn("taskkill", [
  • 执行外部命令固定命令
    lib/ip-pool-C9FThTdV.js:1592
    const killer = spawn("taskkill", [
  • 发起网络请求
    src/adapter/catalog.ts:384
    const response = await withTimeout(this.#fetch(this.#metadataUrl, { headers: { accept: 'application/json' } }))
  • 写入本地文件
    src/adapter/catalog.ts:555
    await mkdir(dirname(path), { recursive: true })
  • 写入本地文件
    src/adapter/catalog.ts:556
    await writeFile(tmp, JSON.stringify(cache), 'utf8')
  • 删除本地文件
    src/adapter/catalog.ts:557
    await rm(path, { force: true })
  • 写入本地文件
    src/adapter/catalog.ts:558
    await rename(tmp, path)
  • 读取本地文件
    src/adapter/catalog.ts:562
    const raw = JSON.parse(await readFile(path, 'utf8')) as MetadataCache
  • 读取本地文件
    src/adapter/messages.ts:144
    const bytes = await readFile(path)
  • 执行外部命令来自配置/变量
    src/agent-process.ts:89
    const child = spawn(this.agentPath, this.args, { stdio: ['ignore', 'pipe', 'pipe'] })
  • 执行外部命令固定命令
    src/agent-process.ts:265
    const killer = spawn('taskkill', args, { stdio: 'ignore', windowsHide: true, timeout: GRACEFUL_STOP_TIMEOUT_MS })
  • 读取本地文件
    src/config.ts:225
    await stat(path)
  • 读取本地文件
    src/config.ts:238
    const existing = (await readFile(paths.tokenPath, 'utf8')).trim()
  • 写入本地文件
    src/config.ts:242
    await mkdir(dirname(paths.tokenPath), { recursive: true })
  • 写入本地文件
    src/config.ts:243
    await writeFile(paths.tokenPath, token + '\n', { encoding: 'utf8' })
  • 写入本地文件
    src/config.ts:245
    await chmod(paths.tokenPath, 0o600).catch(() => {})
  • 写入本地文件
    src/config.ts:273
    await mkdir(paths.dataDir, { recursive: true })
  • 写入本地文件
    src/config.ts:275
    await writeFile(tmpPath, JSON.stringify(config, null, 2), 'utf8')
  • 删除本地文件
    src/config.ts:276
    await rm(paths.configPath, { force: true })
  • 写入本地文件
    src/config.ts:277
    await rename(tmpPath, paths.configPath)
  • 写入本地文件
    src/index.ts:126
    void writeFile(
  • 读取本地文件
    src/index.ts:401
    if (existsSync(sibling)) return sibling
  • 发起网络请求
    src/pool/admission.ts:80
    deps.undici.request(ipEcho, { dispatcher: agent }),
  • 发起网络请求
    src/pool/admission.ts:226
    deps.undici.request(url, { dispatcher: agent!, ...init }),
  • 发起网络请求
    src/pool/dispatcher.ts:160
    this.#direct = new options.undici.Agent()
  • 发起网络请求
    src/pool/dispatcher.ts:196
    const agent = new seam.ProxyAgent({
  • 发起网络请求
    src/pool/dispatcher.ts:199
    new seam.Agent({ ...(opts as object), pipelining: 0 }),
  • 读取本地文件
    src/pool/singbox.ts:317
    if (existsSync(bin)) return bin
  • 执行外部命令来自配置/变量
    src/pool/singbox.ts:322
    const child = spawn(bin, ['version'], { stdio: 'ignore' })
  • 写入本地文件
    src/pool/singbox.ts:343
    await mkdir(this.#dataDir, { recursive: true })
  • 写入本地文件
    src/pool/singbox.ts:345
    await writeFile(tmp, JSON.stringify(config, null, 2), 'utf8')
  • 删除本地文件
    src/pool/singbox.ts:346
    await rm(this.#configPath, { force: true })
  • 写入本地文件
    src/pool/singbox.ts:347
    await rename(tmp, this.#configPath)
  • 执行外部命令来自配置/变量
    src/pool/singbox.ts:351
    const child = spawn(binary, ['check', '-c', this.#configPath, '-D', this.#dataDir], { stdio: 'ignore' })
  • 执行外部命令来自配置/变量
    src/pool/singbox.ts:360
    this.#child = spawn(binary, ['run', '-c', this.#configPath, '-D', this.#dataDir], {
  • 执行外部命令固定命令
    src/pool/singbox.ts:415
    const killer = spawn('taskkill', ['/T', '/PID', String(child.pid)], { stdio: 'ignore' })
  • 执行外部命令固定命令
    src/pool/singbox.ts:427
    const killer = spawn('taskkill', ['/T', '/F', '/PID', String(child.pid)], { stdio: 'ignore' })
  • 发起网络请求
    src/pool/subscription-fetcher.ts:140
    const body = await this.#fetch(url)
  • 发起网络请求
    src/pool/subscription-fetcher.ts:229
    async #fetch(url: string): Promise<string> {
  • 发起网络请求
    src/provider.ts:121
    const response = await fetch(providerBaseURL(port) + '/models', {
  • 发起网络请求
    src/provider.ts:137
    const response = await fetch(`http://127.0.0.1:${port}/healthz`, { signal: controller.signal })
扫描于 2026/11/1(0 天前) · 范围 入口闭包 + 全包扫描 · 来源 npm 发布包

静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。

依赖的 DSH / Cordis 模块
@deepseek-ai/cordis@deepseek-ai/dsh-client-locale@deepseek-ai/dsh-client-ui-plugin-manager@deepseek-ai/dsh-client-ui-primitives@deepseek-ai/dsh-client-ui-renderer@deepseek-ai/dsh-client-ui-settings@deepseek-ai/dsh-client-ui-slots@deepseek-ai/dsh-credentials@deepseek-ai/dsh-llm@deepseek-ai/dsh-settings@deepseek-ai/schemastery
给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个

OpenCode Zen 免费模型接入 DeepSeek Harness:原生 dsh-llm 适配器,无需 API key,代理出口池可扩展到数百个出口。

README

由 DeepSeek 最新模型翻译生成
dsh-opencode-router 在 DeepSeek Harness 里免费用 OpenCode Zen 模型。不用 API key,不用账号,不用额外起进程。免费通道对你的 IP 限流时,一组代理出口会接手,模型选择器照常能用。 安装 装上就能用:出口池默认开启,会抓取公开代理列表,把你的请求轮流分发到每个真正能连上 API 的出口。不用配置,不用账号,不用 API key,也不用起隧道。在输入框下方的 composer 芯片里就能看到它工作,出口通过检测后会一个个出现——通常发第一条消息后一分钟内就有。 在 GUI 的 Add plugin 输入框里填: dsh-opencode-router 或者在终端里执行,这里必须指定配置档名: dsh plugin add dsh-opencode-router --profile web 其他方式 | 方式 | 命令 | | 锁定版本 | dsh plugin add dsh-opencode-router@1.1.1 --profile web | | 锁定 commit | dsh plugin add github:mirelconstantin/dsh-opencode-router#<sha> |
展开完整文档(README 剩余 1 节)
dsh-opencode-router

在 DeepSeek Harness 里免费用 OpenCode Zen 模型。不用 API key,不用账号,不用额外起进程。免费通道对你的 IP 限流时,一组代理出口会接手,模型选择器照常能用。

安装

装上就能用:出口池默认开启,会抓取公开代理列表,把你的请求轮流分发到每个真正能连上 API 的出口。不用配置,不用账号,不用 API key,也不用起隧道。在输入框下方的 composer 芯片里就能看到它工作,出口通过检测后会一个个出现——通常发第一条消息后一分钟内就有。

在 GUI 的 Add plugin 输入框里填:

dsh-opencode-router

或者在终端里执行,这里必须指定配置档名:

dsh plugin add dsh-opencode-router --profile web

其他方式

| 方式 | 命令 |
| 锁定版本 | dsh plugin add dsh-opencode-router@1.1.1 --profile web |
| 锁定 commit | dsh plugin add github:mirelconstantin/dsh-opencode-router#<sha> |
| Tarball | dsh plugin add ./dsh-opencode-router-1.1.0.tgz |
| 本地检出 | dsh plugin add /absolute/path/to/dsh-opencode-router |

重启配置档,打开模型选择器,从 opencode2dsh 分组里挑一个模型。所有东西都是可选的:出口池默认关闭,所以装上之后什么都不会变,直到你把它打开。

它做什么

插件注册了一个原生 dsh-llm 适配器,直接从 OpenCode Zen 的匿名免费通道流式拉取,也就是 OpenCode 自家命令行不登录时用的那批模型。请求会带上命令行的 user agent,以及它的 session、request、project 请求头,所以上游看到的就是普通命令行流量。

模型目录会自己保持最新,靠三步兜底:先取上游实时列表,再退回离线缓存,最后退回一份验证过的静态列表。哪些模型算免费,由 models.dev 的定价元数据决定。这一切都不需要 key。

你的 IP 被限流时,代理出口池会接手。 匿名通道按出口 IP 计量配额,所以一个 IP 用光了,别的 IP 还能继续服务。出口池从四个来源填充(公开免费列表、你粘贴的地址、订阅 URL,以及 GoProxy 转换器),逐个探测候选出口,再把每个请求都走最健康的那个出口:

| 来源 | 来自哪里 |
| free | 公开代理列表,抓取并筛选。默认开启 |
| manual | 你粘贴的地址,每行一个 |
| subscription | Clash 或 V2ray 订阅 URL。明文节点直接拨号,加密节点经 sing-box 转换 |
| goproxy | GoProxy 适配器贡献的节点 |

出口返回 429 就进入冷却,按指数退避,池里其余节点继续服务。
模型级拒绝通常源于地区封锁,而不是代理坏了,因此只封禁那一对「出口 + 模型」。
出口连续失败多次后会被剔除。真实流量会边走边教这个池子:路由器看到的每个响应,都会更新探测器写入的同一份健康数据,所以不会把探测配额浪费在已经在用的出口上。

查看并选择你正在用的代理

只要插件已加载,输入框下方就有一个紧凑控件——底部停靠栏里、上下文计量条旁边。
这个标签显示你上一次请求走的出口;没有走代理时显示 direct;它的颜色表示该出口是否仍可用。
池子开着但没有可用出口,或者它把全局调度器交给了另一个插件,都会如实说明,因为「池子已启用」和「你的流量走了代理」是两回事。

点它就能选。 选择器给出三种答案,它们不能互换:

| 行 | 含义 |
| No proxy (direct) | 什么都不路由。每个请求都直连。 |
| Automatic | 让池子自行选择和轮换。 |
| 某个出口行 | 固定用该出口,直到你改动。可以故意固定一个已死的出口,用来调试坏节点。 |

每个出口行都带一个状态点、延迟,以及它花掉了你多少:

- Tokens —— 该出口承载的总量,覆盖所有已完成的请求,并做了拆分,让重对话一目了然。
旁边是来自提示词缓存的那部分占比:缓存前缀的计费远低于全新前缀,所以占比高意味着该出口在便宜地承载长会话。悬停提示会解释这个数字。
- 上次使用时间 —— 3 uses · 2m ago。

行的排序方式跟你的选择逻辑一致:当前固定的,然后健康出口,然后真正承载过流量的,其余按最近使用排。经过验证的出口胜过同等健康度但没测过的出口。

No proxy 刻意不等同于 Automatic:不固定仍然会路由,而你可能想在不碰设置的情况下关掉路由。
它的优先级也高于固定项,所以早先留下的固定不会悄悄把路由复活。

标签读取的是设置页轮询的同一个 /status 桥,并通过受回环保护的 POST /pin 写入,所以不新增传输层,也不新增配置。它刻意不依赖设置页是否打开:池子出问题时,你在对话中途就需要它。

标签跟随你的路由决策,而不只是已完成的流量: No proxy 和固定某个出口,都会在你点击的那一刻改变标签,因为下一个请求会走那里。还没承载过流量的 pin 会标成 pin,而不会声称有请求走过它。什么都没 pin 时,读数回退到最近一次完成的请求,所以在一次 429 轮换和下一次之间,它显示的是会话即将离开的出口——tooltip 会说明这一点,而不是藏着。

用量统计和 token 计数都在内存里:它们统计的是本进程的路由,不是持久记录,重启就清零。

一个出口能撑多久

Zen 免费通道按 IP、按 UTC 日 计量——桶的键是 ratelimit:ip:<ip>:<YYYYMMDD>,不是按 API key,也不是全局。由此有两点,也正是轮换值得做的原因:

- 新的出口 IP 就是一个全新的空桶。 轮换真的能把当天的额度翻倍,而不只是把它摊开。
- 重置点是 UTC 午夜,不是你第一个请求之后的 24 小时。 UTC 23:00 撞上 429,大约一小时就清了;UTC 01:00 撞上,要等将近 23 小时。这是上游自己的 retry-after 值,池子现在收到它就原样使用,收不到就自己算出同样的重置时间。

新 IP 比老 IP 待遇更宽:在生命周期阈值以下,上游给两倍的每日上限,所以最近才出现的出口比长期使用的更值钱。

上限本身没有公开。 这个数字在服务端的密钥里;被广泛引用的「200 requests/day」是社区传说,没有官方来源。按你自己实测的吞吐量来规划,别按那个数字。还要注意,上限计的是请求数,不是 token——一个很长的智能体回合和一句提示词各算一次——而且免费模型即使已认证也按 IP 计量,因为那是模型的属性,不是请求的属性。

两个看起来像 bug 的轮换细节:IPv6 会被截断到 /64,所以在同一个 /64 里轮换地址没用;而且只公开 retry-after 头,没有 x-ratelimit-remaining 可读。

还要注意,上面这个限流器最后一次开源是 2026-10-11,Zen 现在只是指向一个闭源服务的薄代理,所以这些数字可能随时变。来源以及已确认/推断/未知的划分见 research/opencode-zen-rate-limits.md。

管理大量代理

设置页在 Plugins 页面上,位于插件那一行下面,每次改动保存即生效,不用重启。

粘贴一整份列表。 两个地址字段都有批量粘贴框。丢进几百行,摘要会在你提交前告诉你结果:会加进去多少、多少因重复被跳过、多少读不出来。等价写法算重复,所以 http://Host:080 和 http://host:80 是一条而不是两条。空行和 # 注释会被忽略。

一眼看出池子有多满。 容量计量条显示可用出口对比目标,状态徽章显示 healthy、low、critical 或 depleted。出口表列出每个出口的来源、区域、延迟、质量等级、实时状态和模型封禁,你可以在任意一行上探测或固定该出口。

固定主出口,前提是你有一个信任的节点,比如本地 Clash 混合端口。固定会让该出口始终排在轮换池前面。开启绝对固定后,失败会直接透传,而不是轮换——当这个节点必须使用时,这正是你要的行为。

设置

默认值开箱即用。在配置档的 cordis.patch.yml 中覆盖它们:

- id: opencode2dsh
name: 'dsh-opencode-router'
config:
providerId: opencode2dsh
refreshSeconds: 300
ipPool:
enabled: true
free:
enabled: true
targetSize: 200

| 选项 | 默认值 | 说明 |
| providerId | opencode2dsh | 在 DSH 中显示的服务商名称。 |
| refreshSeconds | 300 | 实时目录刷新间隔。 |
| ipPool.enabled | true | 开箱即开——全新安装无需任何配置即可路由和轮换。 关闭意味着每个请求都直连,就像池不存在一样。 |
| ipPool.free.enabled | true | 抓取并收录公开代理列表。 |
| ipPool.free.targetSize | 30 | 保留多少个可用免费出口。schema 接受 1 到 1000;选择和收录的耗时随这个数字线性增长。30 适合实测的收录率:抓到的候选里大约只有 11% 真能连上 API,而免费代理通常撑不过一小时,所以目标值太小会让池大部分时间处于紧急状态。 |
| ipPool.free.blockedCountries | ["CN"] | 拒绝其出口的 ISO 国家代码。 |
| ipPool.manual | [] | 代理地址,http://host:port 或 socks5://host:port。 |
| ipPool.subscription.urls | [] | 订阅 URL,按 refreshMs 刷新。 |
| ipPool.subscription.refreshMs | 1800000 | 订阅拉取间隔。 |
| ipPool.singbox.path | sing-box | 加密订阅节点的转换核心。 |
| ipPool.probeModels | [] | 用来探测出口的模型。留空表示使用默认值 big-pickle。 |
| ipPool.maxConcurrentProbes | 3 | 跨不同出口的并发探测数,1 到 32。同一个出口始终串行探测。 |
| ipPool.pinnedExitId | "" | 主出口的地址。 |
| ipPool.pinnedStrict | false | 绝对固定:失败即报错,不轮换也不直连。 |
| firstEventMs | 30000 | 首个流事件到达时间的看门狗。 |
| bodyIdleMs | 120000 | 响应中途静默的看门狗。 |
| responsesBodyIdleMs | 300000 | 仅 Responses 模型使用的同一静默下限。 |

每项设置都能在 Plugins 页面修改,通常比改补丁文件更方便。

扩展

池的设计目标是容纳数百个出口,同时路由路径不必为没在用的出口付出代价。

- 选择只扫一遍。 每个请求只遍历出口一次并保留最优者,而不是构建并排序一份包含所有出口的列表。健康状态和冷却状态从池中读取,只有池真正发生变化时才会重建候选列表。
- 封禁记录按出口建立索引。 某个出口对其模型是否已耗尽,是与该出口自己的封禁集合做计数比较,而不是扫描整张封禁表。池状态、准入配额和状态页所需的可用性信息,只需遍历一遍出口即可得出。
- 出口地址已建立索引。 添加候选时会在地图中检查其出口 IP,因此准入几百个候选是线性开销,而不是平方级开销。
- 代理智能体的淘汰是常数时间。 每个出口的智能体缓存维护自己的最近使用顺序,默认上限为 64,因此工作集超过旧上限后不再反复重拨。
- 筛选认领任务按索引进行。 粗筛把下一个候选直接交给每个工作线程,而不是移动队列,从而消除了在多达 20,000 个候选的列表上的平方级开销。

以上改动都没有改变选择顺序:固定出口可用时优先,其次是会话的上一个出口,然后是健康优于未知,然后是连续 429 更少,然后是延迟更低,最后是在同等条件下随机挑选。

健康与故障排查

插件在每次刷新后写入状态快照:

~/.opencode2dsh/adapter-status.json

{
"status": "ready",
"total": 64,
"exposed": 9,
"lastError": "",
"writtenAt": "2026-08-29T07:01:54.915Z"
}

| 症状 | 可能原因与修复 |
| 只出现 3 个模型 | 首次目录抓取与你的网络发生了竞争。重试会在大约一分钟内成功;检查状态文件中的 lastError。 |
| lastError: "fetch failed" 持续存在 | 到 opencode.ai 的出站 HTTPS 被阻断。检查你的代理或 VPN 规则。 |
| 聊天时出现限流错误 | 匿名通道按 IP 计算配额。打开池、添加出口,或切换网络节点。 |
| 某个模型报告 REGION_BLOCKED | Zen 拒绝了该模型当前所在的网络区域。出口池将其视为出口与模型的封禁,因此其他模型仍可在该出口上正常工作。 |
| 所有服务商都失败并报 routing dispatcher closed | 出口路由层的生命周期超过了插件。重启配置档。 |
| 手动条目显示健康,但始终无法填充其出口 IP | 地址中已经带了 scheme。请只写一次 http://host:port 或 socks5://host:port。 |
| 加密订阅节点始终无法准入 | sing-box 未安装或不在 PATH 中。设置 ipPool.singbox.path。 |
| 安装失败并报 ERR_PNPM_IGNORED_BUILDS | pi-ai 的传递依赖带有运行时不需要的构建脚本。在配置档的 pnpm-workspace.yaml 中,把 allowBuilds: 下的两项都设为 false。 |

安全

- 不涉及任何密钥。 免费通道的密钥就是字面字符串 public。不存储任何内容,也不上报任何遥测数据。
- 请求直接从你的机器发出,发往 opencode.ai 和 models.dev,或经由你配置的代理出口发出。
- 公开免费代理不可信。 它们能看到流量。只把它们用在免费通道上,这条通道不携带任何凭据;其他用途请优先用你自己的节点或订阅。
- 安装时不会有任何依赖偷偷装构建脚本。

开发

pnpm install
pnpm run typecheck
pnpm test
pnpm run build # node bundle
pnpm run build:client # browser bundle

pnpm run verify:all 检查对外发布的接口:清单承诺的每个入口点是否都存在,bundle 补丁安装的包是否就是清单里写的那个,行 id 是否与插件自己导出的 name 一致,以及客户端 bundle 注册用的 id 是否是 shell 加载器期望的那个。pnpm run clean 会在构建前删掉 lib/,因为 node 构建会保留自己的输出目录,模块一旦改名,上一次的 chunk 就会留在那里。

pnpm run build 构建前会先清理 lib/,而 build:client 必须跟在它后面跑——产物测试读的是构建出来的 bundle,lib/ 缺失时它们会以 ENOENT 失败,而不是因为真正的原因失败。

发布。 每次上线的改动都要有新版本号——改 package.json 里的 version,或者直接跑 npm version patch --no-git-tag-version 让它帮你改。npm 会拒绝重复的版本号,所以 1.1.1 不可能再变成 1.1.1;你发布的那个号就是真实存在的那个号。完整流程,包括客户端 bundle 的导入规则和必须在两处同时改的配置默认值,都在 AGENTS.md 里。

测试套件覆盖出口池的选择顺序、冷却与封禁语义、批量粘贴解析器,以及构建产物本身:它会导入 lib/,遍历 chunk 图证明没有孤儿文件被发布出去,并用打包后的代码搭出一个能用的池。每次 push 和 pull request,CI 都会在 Linux 和 Windows 上跑类型检查、测试套件和两项结构检查。

目录结构

src/
adapter/ the Zen adapter: streaming, message shaping, request identity
pool/ the exit pool: selection, health, probing, screening, routing
ip-pool-settings/ the settings schema and the status bridge the page reads
client/ the browser half: the IP pool page and its dictionaries

致谢

本插件源自 @FishBottle7 的 opencode2dsh,该项目采用 MIT 协议,至今仍是目录回退链、请求伪装细节和匿名通道实现的源头。完整署名见 NOTICE。

- OpenCode,用来跑免费的匿名 Zen 通道。
- models.dev,提供模型元数据,决定哪些模型免费。
- @earendil-works/pi-ai,适配器流式传输所走的线路层。

协议

MIT,见 LICENSE。

由 Pazalo 构建。
预览分享卡片长什么样 ↓mirelconstantin/dsh-opencode-router
分享 · DeepSeek Harness Hub
mirelconstantin/dsh-opencode-router

免密钥调用 OpenCode 免费模型,自动切换代理出口

回到顶部