DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

DeZhou123 /

DeZhou123/dsh-free-model

Verified

Free-model console for DSH: CodeBuddy + WorkBuddy account lanes, local service lanes (OpenCode free tier, FreeLLMAPI gateway, Ollama) with HTTP probes, and 21 bring-your-own-key / keyless channels 鈥?one settings page, three tabs. Channel data is a three-layer catalog so new channels arrive without a release.

★ 0 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@44594ed4

dsh-free-model

把本机所有「不花钱」的模型渠道装进 DSH,用一个设置项、三个页签管起来, 并且让渠道清单能远程更新 —— 加渠道不用等插件发版。

类 条数 路由 能力 需要什么 页内页签
账号渠道(本插件自己挂) 2 buddy/*、workbuddy/* 完整:工具调用、看图 一个账号(浏览器登录) 账号渠道
服务渠道(本机进程) 3 opencode/* 无工具:能看图、不能调工具 本机 opencode serve(插件可代起) 本地与网关
自带 key / 免 key 渠道 21 <渠道 id>/* 取决于上游声明 去上游拿免费 key(免 key 渠道零配置) 自带 key

它要回答的问题是:我到底有哪些免费模型、它们现在能不能用、不能用的话下一步做什么。 所以每条渠道都带就绪状态(就绪 / 需动作 / 不可用 / 未知)与一条能指导动作的原因 (配哪个凭据变量、去哪拿 key、去起哪个服务),而不是等用户点下去猜。

三类渠道的「不能用」是三件不同的事,所以必须分开:

类 不能用时用户要做什么
账号渠道 登录 / 续期 / 领积分 / 换账号
服务渠道 起本地进程 / 填网关 key / 换一个本地服务
自带 key 在页面上把 key 填进去(0.4.1 起)、或写进凭据条目;免 key 渠道不需要

合并成一句「渠道不可用」会让用户乱试方向,所以本插件宁可多写三个分支。

自带 key 那一页:能填、能测、能清(0.4.1)

「去拿 key」→ 粘进输入框 → 保存(写宿主凭据,回脱敏值)→ 测试 key → 分类结论
                                     ↓ 就绪后立刻挂上路由
                            改主意了?「清除 key」删掉凭据条目

三条动作对应三条路由,凭据值只进不出(POST 收原文、只回 sk-t…6789;GET 永不回显原文):

动作 路由 关键行为
保存 POST /key {lane, key} 先校验(空 / 空白 / 控制字符 / 超长都拒)→ 写宿主同名凭据 → 立刻挂上(已挂载的走 setApiKey(),不重注册)
清除 DELETE /key {lane} 删凭据条目,环境变量回退随即生效;路由保留(报错比静默消失可读)
探测 POST /key/probe {lane, force} 打一次 {baseURL}/models(或声明的 modelsURL),结果按 kind 分类

探测为什么值得单独做:没有 key 时不构造请求(no-key),有 key 时十一种结论各说各的话 ——

kind 用户该做什么
ok 没别的事了,顺手显示抄回来的模型数
invalid-key(401) key 复制错了 / 已吊销 → 换 key
forbidden(403) key 是对的,但这个接口 / 模型没权限 → 开通或换模型
rate-limited(429) key 是对的,等一会(读 Retry-After)
quota(402) 额度 / 余额不足
not-found(404) 目录里的 baseURL 可能过期
timeout / network 网络问题,key 本身未必有问题
no-key 还没填

旧版页面刻意不放「测试连通性」,理由是「没 key 点测试必然失败,会被读成渠道坏了」。 那个理由只对没 key 成立 —— 所以现在是「没有 key 就不发请求」+「失败按 kind 分类」, 旧理由不再适用。

骨架:注册表 + 可远程迭代的渠道目录

src/lanes/registry.js     唯一事实来源:三类渠道 → 同一种记录形状 → 就绪四态投影
  ├── src/lanes/service.js    3 条服务车道 + HTTP 探针(双 TTL 缓存,失败退避 30 秒)
  ├── src/lanes/keyed/**      21 条自带 key / 免 key 车道
  │     ├── catalog.js         目录(每条带 evidence 与未确证标注)
  │     ├── ready.js           就绪判定 + 三种凭据形态解码
  │     ├── adapter.js         OpenAI 兼容适配器(SSE / 工具 / 图片 / 错误分类)
  │     ├── index.js           descriptors() / mount() / mountAll()
  │     ├── keys.js            填 key / 清 key / 脱敏 / 凭据面状态(0.4.1)
  │     ├── probe.js           凭据探测 + 十一种分类(0.4.1)
  │     └── manage.js          运行时管理器:存完 key 立刻挂上、换 key 不重注册(0.4.1)
  ├── src/lanes/catalog.js    记录校验(远程数据是**不可信输入**)+ 三层叠加(shipped/remote/local)
  └── src/lanes/remote.js     远程目录拉取(https-only、格式版本不符整份拒绝、原子缓存、失败降级)

为什么渠道数据要能远程更新:免费渠道是外部策略,不是技术事实。上游换域名、加免费模型、 收回额度,都不该逼用户等一次插件发版。所以渠道数据分三层、同形状、按 id 覆盖: 随包(离线基线)← 远程 JSON(随时可更新)← 本地覆盖文件(用户自己钉住/改对)。 远程数据只被当作数据读:没有 eval、没有动态 import,每条记录都要过校验器, 不合格的整条丢弃并记账 —— 一条 baseURL 写错的渠道会表现成「插件坏了」,比少一条渠道糟得多。

⚠️ 仓库是私有的,远程目录默认拉不到(三条路)

raw.githubusercontent.com/<owner>/<repo>/… 对私有仓库一律 404(本机 2026-10-25 实测), 而这个仓库按约定默认 private —— 所以「远程目录」这一层默认是不可用的: 插件会优雅降级到随包数据(功能一切正常,只是「加渠道不用发版」这句落不了地)。

三种让它真正生效的办法,按代价排序:

路 怎么做 代价
① 目录放到公开位置 传到公开 Gist 或自己的静态站点,catalog.url 指过去 目录内容会公开(里面没有凭据,只有渠道元数据)
② 给一个只读 token 凭据或环境变量里放 GITHUB_TOKEN(或 DSH_FREE_MODEL_CATALOG_TOKEN) 要建一个 token;token 只进请求头,不进 URL、不落盘、不出现在返回值里
③ 仓库转公开 你自己的决定,插件不代做 ——

失败时页面会原样显示后端给的原因;如果是「404 且没有 token」,原因里会点明是私有仓库这件事 (不是干说一句「拉取失败」)。页面上同一行还给出本地覆盖文件的全路径 ($DSH_HOME/free-model/channels.local.json)—— 不想动仓库时,那是第四条路。

怎么确认它真的装上了

Get-Content "$env:USERPROFILE\.dsh\free-model\apply-ok.json"

这个文件是装配自证:版本、上次装配时间、已经挂上适配器的路由、目录层计数与指纹。 不含任何凭据值(只记「挂了哪几条」)。

为什么需要它:headless / CLI 模式不打 info 日志,而装配是异步分片的(native 车道在 inject 回调里挂、自带 key 车道是 void keyedManager.mountAllReady()),任何一片失败都只留一行 warn —— 看不到 warn 就没法判断。它是历史记录(appliedAt 是上次装配的时刻),不是存活标记: 「现在在不在跑」看设置页能不能打开。

正常装配时它长这样(真宿主进程内实测):

{ "plugin": "dsh-free-model", "version": "0.5.0", "phase": "full",
  "mountedRoutes": ["buddy", "opencode", "pollinations", "workbuddy"],
  "channels": { "account": 2, "keyed": 21, "service": 3, "total": 26 } }

端点会变:npm run probe:endpoints 复查一次

渠道 URL 是会变的(换域名、下线、改路径),而插件把「能不能用」的责任交给了运行时 —— 目录里的 URL 错了,用户看到的是「配了 key 还是 401/404」,最难查。 所以改过渠道数据就重跑一次这个探针(它不带凭据、默认只打印、不写仓库文件):

npm run probe:endpoints          # 逐条探 {baseURL}/models,按状态码分类
npm run probe:endpoints:dry      # 只列出将探测的 URL

判读:401/403 = 端点活着且要凭据(最健康的一档)、200 = 免凭据、 404/405 = 路径或方法不对、连不上 = 本机此刻不可达(不代表目录错)。 目录自己声明过的例外(supportsModelList:false / placeholders:['{ACCOUNT_ID}'])算预期; 只有当「声称能列模型且没有占位符」的渠道突然 404 时才以非零退出码报警。

2026-10-25 实测:19/21 可达(14 条 401/403、5 条 200),另 2 条与目录声明一致。

账号渠道:2 条车道(CodeBuddy / WorkBuddy)

本插件的「账号渠道」页只管本插件自己挂载的两条腾讯车道:

车道 id 登录形态 余额 签到
CodeBuddy (腾讯国内版) buddy 浏览器回调 ✓ ✓
WorkBuddy (腾讯国际版) workbuddy 浏览器回调 ✓ 无(上游不提供签到接口)

其余免费账号车道不在这里(0.5.0 拆分)

华为云 CodeArts、有道 LobsterAI、Qoder、TRAE、Cline、Loomy、Raccoon、MiniMax、 Gemini Code Assist、ZCode 那 11 条车道,其 auth / 适配器 / 积分 / 登录实现都在 另一个插件里,页面也归它。它们曾在 0.4.x 被本插件借页面展示过,0.5.0 起 整体迁出,改由独立插件 dsh-ofm-lanes 提供(同一个设置区里另一个入口)。

为什么拆:那 11 条的执行体从来不在本插件内 —— 本插件只做「页面 + 转发」, 而转发要跟着对方的 RPC 变、登录时序要跟着对方的产品怪癖变。两件事绑在一个 插件里,任何一边发版都得同步另一边,且本插件的测试根本覆盖不到对方的实现。

为什么本插件不能顺手把 11 条也挂一遍(历史坑,仍然成立):

  1. cordis 同名 Service 会抛错。比如 new QoderAuth(ctx) 会注册 ctx.qoderAuth;同名再注册一次直接抛 service "qoderAuth" has been registered, 把整个 apply 打死;
  2. 两份 auth 实例 = 两份续期定时器。同一个 refresh_token 被并发消费, 表现是「刚登录却一直认证失败」(本仓库记录过同型事故)。

backend 现在只剩两档

backend 谁实现 页面怎么操作
native 本插件挂载的 auth + adapter(buddy / workbuddy) /api/free-model/*
gateway 别的插件已经注册了路由(本插件只观测,不重复注册) 只读展示

GET /api/free-model/summary 里每条车道都带 backend / login / credits, 页面据此渲染卡片与动作 —— 不需要认识任何具体渠道。

回归防线

test/lanes-registry.mjs —— 断言账号车道全部是 native 两条、 注册表三类渠道齐全、未挂载时一律 unavailable 且原因指向下一步; test/routes-channels.mjs 断言 /summary 里不再有「宿主侧后端就绪」那一段。

账号型两条车道与「不需要登录的本地车道」在同一个设置项内分页:前者要管账号、 积分、签到;后者只有一个本地进程要管。同一时刻屏幕上只有一个主题,页签上还带 数字(账号数 / 模型数),切换前就知道对面有没有东西。

账号池是共享的,但不是必需的。 宿主若提供了 accountPool 服务,本插件 直接用它的池(同一份账号、同一份限流标记);没有则退化为本地实现,读写同一个 $DSH_HOME/channel-pack/state.json。两种情况下都是同一份文档,不会出现「两个池 各写各的」。

为什么需要这个插件

三件事各自都能做,但没有一个地方能同时回答「我到底有哪些免费模型、它们现在 能不能用、要做什么才能用」。

  • 腾讯那两条车道的入口在宿主侧早已下线,但账号与凭据还在(本插件的前身 dsh-ofm-workbuddy 把它们接回来了);
  • opencode 的免费层只在「OpenCode 自己的调用方」里合法可用(直连会被拒,实测), 而它跑的 serve 正好就是合法调用方;
  • 这两批模型的能力不一样:前者能调工具、能看图;后者能看图、不能调工具 (工具协议的缺口在 opencode 侧,不在本插件)。

本插件把三者放在一页里,并且把能力差异写在卡片上,而不是等用户踩坑。

三条车道的产品定位

完整能力车道(buddy / workbuddy)

可以当主模型用:DSH 的 agent loop 需要的工具调用、图片输入都支持。

  • 账号型:一个账号 = 一份凭据 + 一个有效期;
  • 多账号按「N 天内到期的积分」分档选号(积分快过期的先用掉,避免浪费);
  • 免费模型跳过积分分档(消耗 0 积分,没有「烧哪个包」的问题);
  • 限流按模型记:deepseek-v4.1-flash 被限流不影响 glm-5.3。

无工具车道(opencode)

不要拿它当主模型。 opencode 的消息接口只接受 tools: { [name]: boolean } —— 那是它内置工具的开关表,整份 API 里没有 任何入口能把调用方自己的工具 schema 传进去(/doc 的 PromptInput 只有 text / files / agents)。

DSH 的 agent loop 靠 tool call 驱动,所以这条车道能做的事只有「一句话进、 一句话出」:会话标题、压缩摘要、翻译改写、代码解释、识图。

能看图(2026-10-23 修正)。早先这里写着「仅文本」,那是错的: /config/providers 里 9 个 opencode 免费模型中有 4 个声明 capabilities.input.image = true,图片用 FilePartInput 的 data: URL 发送(实测 prompt_async 返回 204,上游确实收到了图)。真正的缺口只有 工具调用。

页面上给这条卡常驻一枚 无工具 徽章和一句说明 —— 这件事只写在 README 里 是不够的,用户一定会在 agent 会话里选它然后困惑。

它为什么仍然值得做

  • 免费层模型走 serve 是合法调用方,直连会被拒;
  • 用户一条命令都不用敲:插件自己拉起服务、自己管会话、自己清理;
  • 三条车道同页对比,用户一眼看到「现在哪条能用」。

安装

仓库:https://github.com/DeZhou123/dsh-free-model(MIT)

# 方式一:从 GitHub 装(跟版本走)
dsh plugin --profile web add github:DeZhou123/dsh-free-model

# 方式二:本地开发用 link 安装(改代码即时生效)
dsh plugin --profile web add link:E:\06AI\00DeeSeekHarness\dsh-free-model

然后在 profile 的 dsh.profile.bundles 末尾追加 "dsh-free-model",重启宿主。

页面(一个设置项,页内两个页签)

设置 → 免费模型(settings.section id free-model,order 37)。 进去之后是页内页签,当前页签记在 localStorage 的 free-model:tab 上, 刷新后停在原处。

┌─ 免费模型 ─────────────────────────────────────────────────────────────┐
│ ( 账号渠道 2/2 ) ( OpenCode 免费层 12 )                                 │
└────────────────────────────────────────────────────────────────────────┘
  • 账号渠道:CodeBuddy / WorkBuddy 两条账号车道的全部内容(下方页一);
  • OpenCode 免费层:不需要登录的本地车道(下方页二)。

页签上的数字由各页自己上报(账号页报 启用/总数,本地页报运行中的模型数, 没跑就不显示数字)—— 外壳不重复请求一遍接口。

页签一:账号渠道

┌─ 免费模型 · 账号渠道 ──────────────────────────────────────────────────┐
│  2/2 账号   38/38 模型   剩余积分 1.2K   账号池:共享池   自动领积分·已领      │
└────────────────────────────────────────────────────────────────────────┘
┌─ 自动领积分 ───────────────────────────────────────────────────────────┐
│ ( ● ) 每日自动领积分   [今日已自动领取]  上次结果:1 个账号领取成功(+10)  │
│                                                          [立即领取]      │
└────────────────────────────────────────────────────────────────────────┘
┌─ CodeBuddy (腾讯) ─────────────────┐ ┌─ WorkBuddy (国际版) ───────────┐
│ [完整能力] [本插件接管]   2/3 账号   │ │ [完整能力] [本插件接管]  1/1 账号 │
│ 账号 2/3  模型 38/38  剩余积分 1.2K  │ │ 账号 1/1  模型 23/23  剩余 ——     │
│ [新建账号][全部续期][刷新余额][领取] │ │ [新建账号][全部续期][刷新余额]     │
│  国内号 [启用] 有效期… 今日 +10      │ │  国际号 [启用] 有效期…            │
│  剩余积分 1.2K                       │ │  剩余积分 · 未返回余额数据        │
│  [停用][续期][查积分][领积分][探测]  │ │  [停用][续期][查积分][探测]       │
│  账号 │ 模型                         │ │  账号 │ 模型                     │
└──────────────────────────────────────┘ └───────────────────────────────────┘

页签二:OpenCode 免费层

┌─ OpenCode 免费层 ──────────────────────────────────────────────────────┐
│ [服务运行中] [无工具] [可看图]   12 个模型                               │
│ 本机 opencode serve 提供的免费模型,不需要账号、不需要登录。              │
└────────────────────────────────────────────────────────────────────────┘
┌─ OpenCode 免费层 ──────────────────────────────────────────────────────┐
│ [无工具] [服务运行中] [流式:SSE]   12 个模型                            │
│ 不能调用工具…适合标题、摘要、翻译。                                       │
│ [停止服务] [刷新目录]                                                   │
└────────────────────────────────────────────────────────────────────────┘

设计取舍(产品视角)

决定 为什么
页内分页,而不是侧边栏拆两项 侧边栏是「系统功能」的层级,免费模型是一个功能;两条车道的切换是高频动作,页内切换比「退出设置 → 找另一项 → 再进来」少三次点击
页签上带数字 切换前就知道对面有没有东西;数字由各页自己上报,不重复打接口
卡片汇总行带上剩余积分 「现在值不值得用它」是账号页的核心问题;藏进账号行里要展开两次才看得到
领取按钮按产品能力渲染 WorkBuddy 国际版没有签到接口:不渲染,而不是给一个点了必然 404 的灰按钮
查不到余额显示原因,不显示 0 「查不到」与「用完了」要做的动作完全不同(续期/重登 vs 省着用)
状态药丸三分:就绪 / 需动作 / 不可用 「不可用」和「能用但没配好」是两种处境,合成一个词会让用户乱试方向
空状态直接给按钮 「暂无数据」是开发者的语言;用户要的是「点这里」
失败原因原样显示 这条链路能失败的地方有十几种(二进制不在、端口被占、限流、旧版本),藏进日志等于让用户猜
OpenCode 状态单列一行 「进程在跑」与「路由已挂载」是两个独立失败点,修法完全不同

积分:余额 / 一键领取 / 每日自动领取

三条能力都建立在 src/core/credits.js 的三个腾讯计费接口上 (get-user-resource / checkin-activity-status / daily-checkin)。

能力 接口 行为
余额 GET /api/free-model/credits?provider=buddy(可加 &force=1) 逐账号顺序查询,服务端缓存 60 秒;force 绕过缓存
一键领取 POST /api/free-model/claim {provider} 逐账号顺序领取(并发易触发风控),单个失败不中断,返回 summary + results
自动领取 GET/POST /api/free-model/auto-checkin 一个总开关 + 一天一次 + 上次结果;{run:true} 立刻跑一轮

为什么余额要独立一跳:它为每个账号打一次上游请求,而 /accounts 是打开页面就 会调的轻量操作。混在一起会让账号列表被网络耗时绑架,且一次查询失败会把整份列表 打空 —— 于是「页面打不开」和「查不到余额」变成同一个现象。

自动领取的状态文件是独立的:本插件写 $DSH_HOME/channel-pack/free-model-auto-checkin.json,不碰宿主那份 auto-checkin.json。两个进程同改一份文档时,后写的会把前者的 lastDate 抹掉, 对方随即以为「今天已经跑过」而跳过它自己的渠道。代价是两边的开关各自独立 (页面上写明了这一条)。

没领到就不记「今天已跑」:池为空或全部失败时不写日期,下次启动会重试; 否则今天新加的账号会被推迟到明天才自动领取。

实现要点

目录

index.js                 三条车道的装配(账号型 ×2 + 服务型 ×1)+ 积分能力位
src/channels.js          buddy 系通道装配(auth + adapter + 选号)
src/lanes/registry.js    三类渠道的统一注册表 + 就绪四态投影(页面与路由的唯一事实来源)
src/lanes/catalog.js     渠道记录校验 + 三层目录叠加(shipped / remote / local)
src/lanes/remote.js      远程渠道目录:拉取 / 校验 / 原子缓存 / 失败降级
src/lanes/service.js     服务型车道(opencode / FreeLLMAPI 网关 / Ollama)+ HTTP 探针
src/lanes/update.js      插件自身的版本检查(semver 比较 + 6 小时缓存 + 失败静默)
src/lanes/keyed/catalog.js  21 条自带 key / 免 key 车道的目录(每条带 evidence 与未确证标注)
src/lanes/keyed/ready.js    就绪判定:三种凭据形态 / 环境变量优先 / ref: 解引用
src/lanes/keyed/adapter.js  OpenAI 兼容适配器(SSE + 工具调用 + 图片 + 错误分类)
src/lanes/keyed/index.js    descriptors() / mount() / mountAll()(只出描述符,注册留给上层)
src/lanes/keyed/keys.js     填 key / 清 key / 脱敏展示 / 凭据面状态(写入走宿主 credentials)
src/lanes/keyed/probe.js    凭据探测:一次 /models 或最小补全,结果按 11 种 kind 分类
src/lanes/keyed/manage.js   运行时管理器:保存后立刻挂载、换 key 走 setApiKey(不重注册)
scripts/build-catalog.mjs   从随包渠道生成远程目录 catalog/channels.json(生成时自校验)
src/credits-service.js   余额 / 一键领取(缓存 + 并发合并 + 顺序领取)
src/auto-checkin.js      每日自动领积分(独立状态文件 + 一天一次 + 开关)
src/routes.js            /api/free-model/*(账号面 + /channels /catalog /update /service + opencode)
src/pool.js              账号池:宿主池优先,缺席时退化实现
src/live-pool.js         账号池的晚绑定门面
src/trust.js             HTTP 面的信任围栏
src/core/                buddy 全栈(从 v2.5.7 逐行移植,注释即踩坑记录)
src/opencode/client.js   opencode serve 的 HTTP 客户端(会话复用 + 增量流式)
src/opencode/adapter.js  DSH LlmAdapter(把 parts 翻成 chunk)
src/opencode/service.js  进程生命周期(探测 / 拉起 / 端口顺延 / 清理)
client.js                浏览器半身(一个设置项 + 三个页签 + 常驻目录/版本状态条)

流式是怎么做的(2026-10-23 改为 SSE)

opencode 的 POST /session/{id}/message 是阻塞到生成完成的,拿不到增量。 早先走轮询 GET /session/{id}/message(按 part id 记已发长度),能work,但 粒度是「上游写库的节奏」—— 实测同一 3 秒窗口里只有 3 次 part 更新, 用户看到的是整段整段跳。

现在走 SSE(GET /event):

  1. POST /session/{id}/prompt_async → 204 立即返回;
  2. 订阅 GET /event,只收本 session 的事件;
  3. message.part.delta(field: 'text' | 'reasoning')是真 token 增量 —— 实测 26 帧 / 3 秒,约 40ms 一帧;
  4. message.updated 带 info.tokens 取 usage;session.idle 收尾;
  5. session.error 立刻给出真实失败原因,不必等 120 秒 idle 超时。

轮询保留为降级路径:老版本 serve 没有 /event,或 SSE 被中间层掐断时 自动回退(只在「还没发出任何内容」时降级,避免重复文本)。

实测(2026-10-23,本机配置的一条免费模型):首块 1.9s,26 个 delta 帧, usage {input:131, output:54, reasoning:18, cacheRead:1920}。

模型目录从哪来(2026-10-23 修正)

早先读 /api/model:一次 8887 条(4.9MB),其中 752 条属于本机配置的两个 provider —— 但只有 /config/providers 里出现过的才真能调用。实测 opencode/space-bunny-free 在 /api/model 里存在,prompt 却回 Model not found;而 /config/providers 里那 9 条全部可用。

现在读 /config/providers(471KB,1/10 体积):一次拿全「已配置 + 能力 + 限额」,不会再给用户列出一堆点了就报错的模型,能力(input.image / toolcall)也直接来自上游声明而不是猜。

三个必须挡住的坑

  1. limit.context 可能是 0。上游对本地/聚合 provider 常下发 { context: 0, output: 0 },而 0 是「未知」不是「零窗口」。不挡住会让 resolveModel 返回 contextWindow: 0,宿主据此把请求判成超窗口 (本机实测的那条免费模型就是 0)。
  2. 上游失败也标 completed。错误挂在 info.error 上,必须原样抛出去 —— 否则用户拿到的是空回答加一句猜的「可能限流」,而真实原因可能是 「免费层只能在 OpenCode 内使用」「模型不存在」「协议不支持」。
  3. 工具必须全关。opencode 的 build agent 默认带 bash / write / edit, 不关掉模型会真的在用户机器上执行命令,而 DSH 侧又拿不到结果(两套工具 协议不互通)。toolsDisabled() 把 14 个内置工具全部置 false。

账号池的两种形态

场景 行为
宿主提供了 accountPool 直接用它的池(同一份账号、同一份限流标记、同一份黑名单)
没有这个服务 退化成 LocalPool,读写同一个 $DSH_HOME/channel-pack/state.json

本地实现只替换 accounts 里属于本插件的那几条,其余字段与条目原样保留 —— state.json 是 dsh home 级共享文档,整份重写会静默丢掉别的通道的字段。

测试

# 主套件:52 项协议/路由断言 + 客户端离线渲染(不需要宿主、不发网络)
npm test

# 用真实宿主包验证契约(临时 link dsh-llm / dsh-credentials,跑完删掉)
npm run test:host

# 真实端到端(需要 opencode serve 在跑;模型 id 用你自己 opencode 里配好的那条)
npm run test:e2e -- '<provider>/<model>'

三套测试各挡一类错:

套件 挡什么
smoke.mjs 协议与路由:三条车道的注册/目录/元数据/stream chunk 序列、HTTP 面每条路由、信任围栏、让位语义;积分与自动领取用假的 fetch 打(不碰腾讯计费接口),状态文件写进临时目录(不碰真实 ~/.dsh)
render.mjs 白屏:用极小的 React 替身把组件树跑一遍(两个设置页都要注册、卡片的三条余额分支、自动领取条的四态)。node --check 只能抓语法错,抓不到「渲染时抛 ReferenceError」——而那正是白屏最常见的原因
host-check / host-apply 契约漂移:instanceof LlmAdapter、providerInfo.id === provider、prepareCall 返回 stream、LlmError code 被宿主认得
keyed/keys-flow.mjs(0.4.1) 凭据全流程:入参校验(空/空白/控制字符/超长)、脱敏、保存与清除、探测的十一种分类、以及保存后立刻挂上 / 清除后路由保留这两条运行时语义
host-keyed.mjs 自带 key 车道的真实装配:免 key 车道上、缺 key 的一条都不上、凭据名是裸名(不带 ref: 前缀——那是 0.4.1 修掉的一个静默 bug)

npm run check 是另一道门:对 index.js / client.js / src/** / test/** / scripts/** 下全部 .js / .mjs(当前 70 个)逐个 node --check,只抓语法错、本机约 8 秒。所以上面 以及下面这些不进 npm test 的套件(e2e-opencode、host-check/apply/keyed、keyed-live、 service-start、opencode-affinity、boot-serve)里写坏语法,CI 也会红。

test:e2e 依赖本机装没装 opencode、以及免费层此刻有没有被限流,故不进 npm test。

已知边界

  • WorkBuddy 国际版没有签到接口:daily-checkin 在它的后端不存在,故这条车道 上不会出现「一键领取」,自动领取也会把它跳过并标成「无签到接口」。 (余额接口 get-user-resource 两边都有。)

  • 自动领取的开关与宿主那份开关各自独立:本插件写自己的状态文件,见上文理由。 两边同时开着不会重复领取(服务端对「今天已领」会明确回报),但会有两次无谓请求。

  • 余额是「查得到才算」:合计数只累计读成功的账号;查不到的账号会单独显示原因, 不会被当成 0(否则卡片上的合计数看起来像「额度凭空少了」)。

  • OpenCode 免费层的失败有三种,处置完全相反,别混成一句「限流了」 (2026-10-24 修正:早先这里只写了下面第 2 条,还把当日额度误当成它的同义词):

    类别 上游原文特征 该怎么办
    ① 当日额度耗尽 Free usage limit reached. Subscribe to OpenCode Go to keep going 按天重置,北京时间 08:00(UTC 0:00)恢复。在此之前重试无效;换成同池的其它 *-free 模型通常也无效。要立刻干活就换通道(换到本插件里的其它渠道)或订阅 Go
    ② 调用方身份被拒 403 FreeTierError: OpenCode's free tier can only be used from within OpenCode 这是 build 级事实,重试永不恢复。2026-10-23 复验:连 opencode serve 自己发出的请求都被同样拒绝(试过 Authorization / x-api-key / UA / x-opencode-session 五种组合,全 403)。出路是升级 opencode 的 build,或换通道
    ③ 限流 429 / rate limit / too many requests 等一两分钟,或换个模型

    插件做的是:按类别分开报(adapter.describeUpstreamFailure() 里额度分支排在限流 分支之前,否则会被限流正则误伤),并在卡片上标出具体原因,别让人一直重试那件 永远不会成功的事。

  • OpenCode 车道不支持工具(见上)。

  • OpenCode 车道支持图片(2026-10-23 修正):/config/providers 里 4 个 免费模型声明 input.image,图片经 FilePartInput 的 data: URL 发送 (实测 204,上游确实收到)。inputModalities 按上游声明逐模型给出, 不再一律 text。

  • 不硬接宿主的 token 账本:token-ledger 是对方包内的内部模块,硬 import 会在它下次重构时把整个插件打死。记账退化为 no-op,推理不受影响。

与前身的关系

dsh-ofm-workbuddy 只做一件事:把宿主移除的两条腾讯通道装回来。本插件 在此基础上:

  1. 拆掉对宿主的隐含依赖(池可退化、账本不接);
  2. 加第三条车道(opencode 免费层);
  3. 重做信息架构(能力徽章、三分状态、空状态给动作)。

buddy 全栈(src/core/)逐行保留原注释 —— 那些注释是这份代码最值钱的部分, 每一条都对应一次真实报障(限流 6004 vs 模型饱和 14003、401 刷新自锁、 免费模型被永久积分锁定误拦、prepareCall 的记账隐式依赖……)。

—/ 5

No ratings yet

Verified DSH bundle

Commit 44594ed41752

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout