在上游一天两版的项目上做二次开发:9 天 65 次提交的 fork 手记 (dsh-our-free-model)

在上游一天两版的项目上做二次开发:9 天 65 次提交的 fork 手记

10 月 18 号,上游一天发了两个版本:凌晨 2.5.2,上午 2.5.3。

我当时正在调模型清单页的视图标签。改完一个间距,刷新一下仓库,版本号又变了。

九天里这种事发生过五次。这篇写我在这个两万星的项目上做自己版本的过程:改了什么,被什么绊住,以及上游一路往前跑的时候,我那些改动是怎么活下来的。

我 fork 的项目叫 dsh-our-free-model (原仓库,我的 fork 在这里)。它干的事很简单:不用注册、不用填 API Key,就能用上 DeepSeek V4.1 Flash、Kimi K3 这些模型。它把十几家免费入口接进来,在本机起个网关,拧成一个 OpenAI 兼容的接口,任何软件把地址指过来就能用。

清单里现在一百三十多个模型,能接的九十九个,剩下的要么地区限制,要么已经下架,被单独隔开。

它替我挡掉的是每个月买 token 的那笔钱。所以我不能只是「玩一下」,我得让它天天能用。


一、第一天:什么情况下才值得 fork

fork 是有代价的。按下那个按钮,你就多了一个要维护的项目。

所以第一天我没写代码,先看它到底想长成什么样。

原项目的主线是插件形态,装在 dsh 里跟着宿主跑。独立本地服务它标着「开发中」。我要的恰恰是后者:一台机器上长期跑着的模型入口,WorkBuddy、脚本、别的客户端,谁都能连过来。插件形态做不到这件事。

判断就很简单了。

上游的路线图里没有你要的那条路,fork 才成立。它要只是有个 bug、缺个小功能,去提 issue 就行。自己开一条线,就得一直养着它。

我 fork 出来的 11 项增强,全是冲着「把独立服务当主力形态用」这一个目标去的。没有一项是「顺手觉得这功能不错」。

这句话后来被打了一次脸,第九天再说。


二、第一晚:把降级链从配置文件里捞出来

fork 的当天晚上我就动了第一刀,动的是这个项目最核心的东西,降级链。

上游的做法是在配置文件里手写一条链,写对了能用,写错了翻文档。可这条链恰恰是我每天都要调的:哪个渠道限流了、哪个模型这几天不行了,顺序就得挪。

所以我先给它做了个页面。每条链一张卡片,成员按调用顺序编号,能上下移、能测、能停。两条链之间可以选「顺序降级」还是「轮询分流」。

链名支持中文。看着是小事,实现上就一行正则:字母和数字按字保留,其他字符换成连字符,于是「深度求索」能原样变成 combo:深度求索,不会被削成一串拼音。

js 复制代码
// src/combos.js ------ 字母数字按字保留,其余字符换成连字符
export function comboId(name) {
  const slug = [...String(name ?? '').trim().toLowerCase()]
    .map(char => /\p{Letter}|\p{Number}/u.test(char) ? char : '-')
    .join('')
    .replace(/^-+/, '')
    .slice(0, 48)
    .replace(/-+$/g, '')
  if (slug !== '') return `${COMBO_PREFIX}${slug}`
  // 纯 emoji / 标点的名字没有可读 slug,另走确定性哈希兜底
}

链能编辑了,下一个问题更实际:怎么把它用起来。

我日常主力客户端是 WorkBuddy,它读一个模型配置文件。于是我加了个「同步到 WorkBuddy」,一键把模型组写进去。

这段我写得比功能本身慢,因为它往别人的配置文件里动手:

  • 先写临时文件,成功了再原子替换,不留半个坏文件
  • 替换前把原文备份到 models.json.backups/,留 20 份
  • 归属判断卡得很死:只动 url 是回环地址、apiKey 以 ofm- 开头的条目;其他条目"刷新但不清理",绝不碰你手工写的配置
  • 格式不对就直接拒绝写入并报错,宁可这次失败,也不覆盖
  • 备份文件名走白名单正则,回滚本身也能回滚

整个提交管线就一个函数,次序固定,每一步的失败后果都写在注释里:

js 复制代码
// workbuddy-sync.mjs ------ tmp 写成功 → 备份原文 → rename 原子替换
const tmp = `${paths.file}.tmp-ofm`
fs.writeFileSync(tmp, `${JSON.stringify(entries, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 })
try {
  if (previousRaw !== null) writeBackup(previousRaw, paths) // 首次写入没有原文,跳过
  fs.renameSync(tmp, paths.file)
} catch (error) {
  try { fs.rmSync(tmp, { force: true }) } catch { /* tmp 残留无害 */ }
  throw error
}

这个文件里除了我写的,还有别人手工加的东西。那个"别人"通常是三个月后的我,早忘了当初为什么加那一条。所以动它之前,得先证明自己有资格动哪几行。


三、第二天:禁用不是开关,是四处的统一

模型清单里一百多个模型,不是每个都能用。地区限制的、下架的、临时失败的,我得能把它们按下去。

最开始我以为在界面上点一下"禁用"就完了。改到这块才发现不是。

一个模型在系统里有四个地方会出现:对外公开的模型清单、推理时的路由候选、模型组(降级链)的成员、定时探活的对象。禁用只从清单里摘掉,它还会在降级链里被选中,还会被探活拉起来,还会在推理时被当成备选。

禁用一个模型,要在清单、路由、降级链、探活四处同时消失。漏一处,等于没禁。

改完之后页面重做了一遍,几个互斥的视图:全部、已探测可用、可接入、未公开、已禁用。禁用的模型从这四处统一剔除,但留在"已禁用"页里,随时能开回来。

禁用不是删除。得留退路。

顺带把开关文案统一了。原来写"开启 / 关闭",现在全改成"启用 / 禁用"。前者听着像调音量,后者才是一种状态。


四、第二晚到第三晚:我先把自动登录关掉了

这一段最值得记:同一件事,我做完又拆了,中间隔了大半天。

独立服务原来有个很贴心的设计,从本机打开管理页,自动用本机令牌登录,不用翻文件找 Key。舒服。

10 月 12 号深夜我发现问题:这个"本机"的判断,站在反向代理后面就不成立了。

我把服务挂在自己的域名下访问。浏览器发出来的请求,到服务这边看,"来自本机",自动登录就放行了。也就是说,任何能访问这个域名的人,都能直接进管理页。

那天 23 点 07 分,我把它整个关掉了,提交说明写得很干脆:「撤回登录页自动读取本机令牌,管理页必须凭 Key 进入」。

第二天的两次合并,说明里都留着同一句话:保留「凭 Key 登录」。

关掉之后,安全了,也难用了。每次进管理页都要翻一遍 Key。

13 号晚上七点多,我又想了一遍。错不在"自动登录",错在把"本机"当成了一道二选一的题。本机直连和经域名访问是两回事,得分开判:

  • 部署时声明了 OFM_BEHIND_PROXY=1,自动登录整体关掉,一律手动填 Key
  • 就算没声明,请求头里带了任何一条转发痕迹(X-Forwarded-For、X-Real-IP、X-Forwarded-Proto),也一律拒绝
  • 再叠两道:sec-fetch-site 必须同源,Host 必须落在回环地址上,防的是 DNS 重绑定
  • 主动退出之后不再自动登回,留一个标记挡住

晚上八点零九分推上去,说明写得很长:「恢复自动登录并升级为反代识别,本机直连自动进、域名走手动 Key。」

先关掉不丢人。想清楚"本机"到底指什么,才敢再开回来。所谓安全上的方便,基本都是判断条件写得太粗;粗的判断被利用一次,你这辈子都不敢再开了。


五、第三晚:把每天手抄三遍的东西,做成一个按钮

13 号凌晨改的是另一件小事。

我要把本机网关接到别的客户端里,需要三样东西:API 地址、Key、模型名。原来的做法是打开设置页,挨个找、挨个复制,一天下来要重复五六遍。地址还得看情况换,有时候是本机 127.0.0.1:18900,有时候是走我自己的域名。

前两天用下来,我记了一句:一天里重复三次以上的动作,就该做成按钮。

所以加了条「接入信息条」:左边整段地址,右边打码的 Key,各自独立复制。模型清单页和模型组页顶部各一条,概览页上是同一套信息的卡片版。地址按当前访问来源自动解析,你从域名打开页面,复制出来的就是域名那条;从本机打开,就是本机那条。

有个细节我较真了一下:Key 的明文不进内存状态。点复制的瞬间才用局部变量取,取完直接写剪贴板,渲染时只看到打码后的样子。一个自用工具也得守住这条,不然哪天截图发群里,顺手就把 Key 发出去了。

同一天下午做的是桌面壳:macOS 的 DMG 打包、菜单栏、Dock 图标唤回。再给 GitHub Actions 加了个双平台出包任务,Windows 出安装包、macOS 出 DMG。Tauri 不能交叉编译,两个平台各用自己的 runner 跑,这是唯一诚实的做法。

桌面上双击能用,比开终端敲命令少一步。这一步省下来,才叫"天天想用"。


六、第五天:报错不带原因,等于什么都没说

15 号一整天基本都在跟各种"说不清"较劲。

第一个坑:账号渠道的模型永远显示"清单已收录",从来不告诉你到底能不能用。查下去发现,那些渠道根本没人去真的请求过一次。补了个最小探活,真发一次请求,max_tokens 给 16,并发压到 2,按返回结果分四类:没报错是可用,404 是不可用,429 是限流,其他一律"未知",未知不动状态。

第二个坑更隐蔽。有一类模型永远探活失败,界面显示"未探测"。我一开始以为是渠道挂了,逐个试才发现:探测请求里少了一条 system 消息,人家那边直接判定请求不合法。补上就通了。

第三个坑是归因错误。渠道返回 429,界面上写的是"未登录"。这两件事差得很远,一个是等一会儿就好,一个是得重新登一次账号。按错误提示去排查,方向就是反的。

还有一次,错误从 worker 边界穿回来就丢了,只剩一句"测试失败"。我把渠道的原始错误码一路透传过来,管理端和日志里都能看到原文。

界面上的"失败"两个字,信息量等于零。省下的那句原因,明天得由我自己花两小时查回来。


七、第八到九天:上台阶,接进第 14 个渠道

前六天改的都是自己的使用体验:界面、按钮、开关、报错。这些东西都在系统边上,改坏了顶多难看。

19 号做的是另一回事,往系统里接一个新渠道。

原项目有十三个账号渠道,全是上游那个渠道包带来的。我加了第十四个,千问办公 QwenWork。

这不是加个配置项。推理请求要加密签名,积分得从官方接口查,token 到期要自动续。一个渠道十个文件,两千三百多行。

登录这块我走了两条路,第一条走废了。

一开始想省事:从本机已经装好的官方客户端里直接读凭据,绕开登录流程。凌晨做完,能跑通,我还挺得意。

但它有个要命的前提,这台电脑上得装过官方客户端。换句话说,这个渠道只有我自己能用。想给别人用,就得让对方先装客户端,再去读他机器上的加密存储。这不是能拿出去的东西,也不该那么做。

上午我把它推倒重来,改成设备码网页登录。用户在自己浏览器里打开一个授权链接、登自己的阿里账号,服务这边轮询等授权结果。不装客户端,不碰本机凭据,谁都能用。

一条只有你能用的路,走得再顺,也交付不出去。

中间踩了三个坑,都属于"看起来一切正常"的那类。

第一个:连通性测试不带工具,所以它永远发现不了工具映射的错。

渠道通了、能对话了,我以为成了。结果一接带工具的请求,全线 "All models failed"。原因是我把 tools 原样传过去,上游要的是包成 {type:'function', function:{...}} 的形状。连接测试只发一句"你好",压根不会带工具。测试用例的形状和真实用法的形状对不上,绿灯就是假的。

第二个:删一个占位账号,把真凭据删没了。

多账号是每个账号一个独立 ref。我先写了个占位账号,它和默认 ref 共用了同一份凭据;后来清理占位账号,account.delete 把凭据文件一起清成了 {}。这种错不会报错,只会在下次调用时说"未登录"。

第三个:token 寿命 7 天,而共享的刷新窗口只有 1 小时。

自动续期要算提前量。7 天的 token 我按 12 小时提前,30 分钟一轮全局续期,这样即使错过一两轮也还在安全区。实测伪造到期后重启,第一轮就续上了,到期日从 10-19 推到 10-26。

提交说明最后我写了句风险:协议是靠观察官方客户端行为还原的,上游一升级就可能失效。这不是客气。改界面改坏了,你自己一眼看得见;协议变了,是静悄悄的。


八、第九天:上游也走到了同一条路上

19 号合上游 v2.5.3,我头一回遇到比代码冲突更难受的事:需求撞车。

我 12 号做的「禁用」,上游 2.5.2 里也有一套,叫「隐藏」。都是想把不想看见的模型收起来,但语义不一样。我的禁用是"它还活着,只是从四处同时摘掉";上游的隐藏就是把模型从视图里收起来。

结果模型页上现在有两套入口:行内的电源键(我的禁用)、行内 ⋯ 菜单里的隐藏(上游的)、还有一个独立工具条用来整组隐藏免费/EAC。视图标签从五个变成六个:全部、已探测可用、可接入、未公开、已禁用、已隐藏。

同一天还有第二件撞车。上游 10 月 18 号加了个「推荐免费组」,默认预设一条链,把三个最强的免费模型串起来,新装的人一眼就能用。而我 11 号晚上做的就是这个,只不过我做成了可编辑的页面。

这次我做了个明确决定:不接收上游的默认预设,我的模型组默认还是空的。

理由是我这套模型组要跟 WorkBuddy 同步。自动塞一条预设进来,等于在别人没同意的情况下改他的客户端配置。宁可空着,让人自己建第一条。

第三件事更疼,也更值得说。

合并时跑全量回归,76 项里过了 66 项。剩下没过的不是我的锅,签名私钥不在手上、缺宿主的 peer 包、还有几项在我动手之前就是红的。但其中一项上游新加的检查,照出了一个我一直不知道的问题:我新加的几个文件,从来没登记进打包清单。

channel-probe.mjs、workbuddy-sync.mjs、src/core/usage-periods.js,还有那个 2.3 MB 的 qwenwork-auth-wasm.wasm。它们在源码里好好的,测试也过,可独立端打包时根本不会被打进去。也就是说,我一直在用一份缺零件的构建产物。

这不是懒。"新增文件要登记"这件事,没有任何机制会提醒你。上游自己的新测试顺手把漏洞照出来了。

处理冲突时我也定了条规矩,写在提交说明里:产物全部重建,不手解冲突。

pack.js、business.mjs、前端 bundle 这些构建产物,冲突了绝对不去逐行改。手改过的产物没人能复现,下次构建又打回原形。改源码,重跑构建,让产物自己长出来。手改攒下的十分钟,下次构建全还回去。


九、什么是我的,什么不是

最后说件跟代码无关的事。

fork 之后我做的第一件事,是把 README 改了。不是加功能说明,是删掉原项目的宣传文案、榜单截图、历史回顾,换成一句清楚的声明:这是某某项目的个人 fork,原项目的一切请以原仓库为准,这里只说明我在它之上多做了什么。

理由很简单。别人搜到我的仓库,得一眼看明白这不是原项目,别装成上游。

第一章我说"上游路线图里没有你要的那条路,fork 才成立"。九天过去,这句得补一句:上游的路线图是会变的。它也在往"更好用的独立服务"上走,走得还不慢。所以你 fork 的理由不能只是"它没有",得是"我要的那个东西,只有我自己能定义清楚"。

比如"禁用一个模型要在四处同时消失",这是我的语义,上游没义务跟。撞车了就并存,各留各的入口,代价我自己认。

哪些是上游的:免费车道、十三个账号渠道、聚合注册表、省 token 那一套,全是上游的。我加的是第 14 个渠道,和那 11 项围绕"我天天想用"的改动。

那什么时候该把改动提回上游?

我的判断是:对所有人都有用的,提回去;只对你这条路线有用的,自己留着。修个 bug、补个错误码,提 PR,你也不想下次合并再冲突一遍。但"独立服务当主力形态"是我的用法,不是所有人的用法,留在 fork 里更合适。


最后

九天下来最实在的一条:fork 之后,上游每发一版,你手上就多一件事。躲不掉。

现在上游 19 号下午又动了,我还差几个提交没合。不写了,去合。

下一篇写这些免费额度到底怎么拧成一条能用的链:十几家渠道、两条不用登录的车道、降级链怎么排、探活怎么做。这篇讲我改了它什么,那篇讲它到底怎么跑起来的。


说明:本文只讲工程方法与取舍,不提供任何绕过第三方服务条款的操作步骤。文中工具用于个人学习与本地开发;免费额度由上游单方面提供,可能随时限流、收费或下线,使用各渠道即须遵守对应上游的服务条款,由此产生的限流、封号等后果由使用者自负。请勿通过免费通道传输密码、密钥或任何敏感信息。

本文由 AI 辅助整理,项目数据、代码片段与结论均经作者核实。


附:我的 fork ------ github.com/XuePengJu/dsh-our-free-model;原项目 ------ github.com/Ebony-Vinyl/dsh-our-free-model。文中渠道、额度、版本信息以两个仓库当期为准。

相关推荐
米小虾1 小时前
工作流还是自主 Agent:怎么选、怎么混
人工智能·agent
FII工业富联科技服务1 小时前
AI+制造还是制造+AI?从2026工博会看工业AI落地的典型问题与解法
人工智能·制造
wangfpp1 小时前
本地大模型微调与 GGUF 部署全指南
人工智能·agent
Smoothcloud润云1 小时前
GPU算力平台跑知识库问答,模型回答总跑偏?先排查检索链路
人工智能·python·深度学习·gpu算力·gpu
mklpo1471 小时前
python3:如何按照实际执行需求导入模块,而不是启动时统一导入?(Python3.15)。2026-10-20
开发语言·人工智能·python
甲维斯1 小时前
国产Flash大乱斗!结果很意外
人工智能
Elaine10121 小时前
AI智能化项目管理(三):AI如何辅助项目文档检查?从交付物质量到流程附件审核
人工智能·ai·项目管理·项目管理工具
CServer_011 小时前
源码交付!AI+重型装备制造企业数智化平台
人工智能·物联网·制造