Bundle
dsh-our-free-model
Our Free Model — 在 dsh 里装上这个插件即可,无需登录、注册或填 API Key,就能使用包括 Muse Spark 1.3、MiMo V2.6 在内的前沿模型,完全免费不限量;模型清单随上游更新、地区可用性自动探测、思考强度真实生效,另附 Token 热力图与吞吐看板和 OpenAI 格式本地转发端口。
- Source
- zouyuxuan122
- stars
- 589 stars
- License
- MIT
- Updated
- Updated 1 hour ago
Readme
<div align="center">
<img src="icon.svg" alt="Our Free Model — DeepSeek Harness 免费模型插件" width="120">
# dsh-our-free-model
**简体中文** | [English](README_EN.md)
<img alt="许可证" src="https://img.shields.io/badge/license-MIT-263146?style=flat-square">
<img alt="零依赖" src="https://img.shields.io/badge/dependencies-zero-4b6fff?style=flat-square">
<img alt="无构建步骤" src="https://img.shields.io/badge/build%20step-none-7da1de?style=flat-square">
<img alt="适配内核" src="https://img.shields.io/badge/dsh-0.1.5--0.1.7--rc.2-2f6f4f?style=flat-square">
<img alt="状态" src="https://img.shields.io/badge/status-beta-f0a441?style=flat-square">
</div>
<div align="center">
> 你只需在 dsh 里装上这个插件,无需登录、注册、填 API Key 或任何其它操作,
> 就能用上包括 Muse Spark 1.3、MiMo V2.6 在内的前沿模型——完全免费,不限量。
>
> *All you do is install this plugin in dsh: no login, no sign-up, no API key, nothing
> else. The frontier models are just there — Muse Spark 1.3, MiMo V2.6 and the rest.
> Free, with no usage cap.*
>
> 模型清单跟随上游刷新,可用性由**你自己这台机器的网络出口**实测得出,
> 思考强度下发的是真实预算而不是提示词,另附一个 OpenAI 兼容的本地转发端口。
>
> 纯插件挂载:不改内核、无构建步骤、零依赖。
</div>
---
## 亮点
- **装完即用,没有配置环节**——不需要账号、不需要 Key、不需要去哪个后台开配额。
- **上游写在明面上**——只有一个来源:OpenCode 的 Zen 网关(`https://opencode.ai`),不经任何第三方中转。谁在服务你的请求、你的数据发到哪儿,见[上游是哪些源](#上游是哪些源)。
- **清单跟随上游**——模型集合、上下文长度与能力每次刷新重新拉取,不是写死在插件里的一份快照。
- **选择器只给真能用的模型**——上游清单点名、但网关明确拒绝路由的模型(回 `Model is unavailable`、404 找不到这个 id)会从下拉框里移除,只在设置页留痕并写清拒因;网关自己的毛病(5xx)、配额(429)、超时断网这些**不是对模型的判定**,一律保持可达;地区门拦截的单独归到 `region-limited` 分组。整轮全部被拒时一律保留,绝不让选择器变空。
- **公告中心 + 实时推送**——仓库主人在仓库里编辑一份 JSON 并推送,所有安装最迟在一个轮询周期内收到;内容是白名单约束下的 HTML,支持图文排版;`urgent` 级别直接全屏弹窗;可选系统级通知。
- **应用内升级**——设置页一键升级:下载 → SHA-256 校验 → 备份 → 原子替换 → 校验回读 → 热重载,任一步失败自动回滚到上一个版本。
- **热重载**——升级与代码更新即时生效,不需要重启应用;也可在设置页手动触发,或开启文件监视自动重载。
- **流式响应认出 body 而不是认出 header**——网关在高负载下会用 `application/json` 的 content-type 回一整套 SSE 帧,插件按 body 的形状判定并把已嗅探的字节重新喂回流,既不会整轮报错,也不会因为一个 header 说谎就把能用的模型判成不可用。
- **思考强度真的生效**——`Light / Balanced / Deep` 对应输出 token 预算 2 048 / 8 192 / 模型上限,且逐次调用留痕。思考关不掉的模型(MiMo V2.6 这类)三档整体翻倍为 4 096 / 16 384 / 模型上限,因为思考与正文抢的是同一份额度;设置页每张模型卡都直接印出这一档实际会下发的上限。它不是把一个 effort 字符串丢给上游然后假装有用(原因见[为什么用预算而不是 reasoning_effort](#为什么用预算而不是-reasoning_effort))。
- **无浏览器界面也能跑**——插件只把 `llm` 当作硬依赖,没有 web server 的 composition(`dsh-tui` 这类)里照样启动、照样出模型;看板半身挂在一条自己的 fiber 上,等 `webServer` 出现再挂载,所以既不会把模型车道拖下水,也不会因为插件先于 web 服务加载就永远丢掉设置页。
- **用量看板,全部留在本机**——Token 热力图、总量曲线(可看总计或单个模型)、输出速度与首字延迟逐次采样。不上传任何东西。
- **OpenAI 兼容转发端口**——本机其它工具用一个 base URL + Key 就能调用这些模型。
- **接口有鉴权围栏**——插件的 HTTP 路由优先级高于内核 `/api`,因此自带与内核一致的信任检查(优先复用 composition 的 connection 服务,缺失时退回结构化围栏)。
## 你会看到什么
**输入框的模型选择器**
| 分组 | 内容 |
| --- | --- |
| `Our Free Model` | 当前网络出口可直接使用的模型 |
| `Our Free Model · region-limited` | 上游对该地区不放行的模型,保留可见但单独隔离 |
被判定为「点名但不路由」的模型不出现在任何分组里——它们只在设置页的**不在选择器中**分组留痕,
带上拒因与探测时间,某一次探测重新通过后会自动回到选择器。
**设置页 `设置 → Our Free Model`**,六个分区:
1. **模型清单**——每个模型的可用性、视觉还是纯文本、上下文窗口、最长输出、各思考档位实际下发的
输出上限、实测首字延迟,以及一次单调用基准测试按钮。
2. **公告中心**——仓库主人推送的公告流:未读计数、紧急徽章、单条/全部已读、检查新公告按钮、系统通知开关。公告正文按白名单渲染 HTML。
3. **用量看板**——总览计数、17 周 Token 热力图、总量曲线(Token / 请求数切换,总计与单模型切换)、速度迷你图、按模型汇总表。
4. **本地转发**——开关、监听地址与端口、复制 base URL、显示 / 复制 / 轮换 API Key,并给出一条可直接跑的 `curl` 示例。
5. **插件设置**——总开关、是否展示地区受限模型、探测间隔、默认输出上限,以及当前探测到的出口 IP 与国家。
6. **插件升级**——当前/最新版本、检查更新、一键升级(含进度与失败原因)、最近一次升级历史、热重载按钮与文件监视开关。
**首次启动公告**——分 5 页(前言 / 模型清单 / 使用步骤 / 功能介绍 / 公告与升级),确认一次后不再弹,除非文案版本号被提升。
## 安装
### 命令行(纯 `dsh web`)
```bash
dsh plugin --profile web add /绝对路径/dsh-our-free-model
```
装完重启一次应用。`--profile` 填你实际使用的那个。
### DSHEAC AIO / 桌面端:先看这段
桌面端在拉起 web 服务之前会跑一道 profile 闸门。它的扫描器**只**放行 dsh 自己在
`.dsh-module-fallback` 下生成的链接;profile 目录树里出现**任何其它符号链接或目录
junction,应用会直接拒绝启动**,报:
```text
PROFILE_UPGRADE_REQUIRED: offline dependency migration is not yet available
```
所以桌面端**不要用 `link:` 依赖安装,也不要建 junction**。请用应用内的插件管理器,
或者放一个真实目录。
手工以真实目录安装,在 `<DSH_HOME>/profiles/<profile>/` 下做三件事:
1. 把发布文件**复制**进 `node_modules/dsh-our-free-model/`
(`index.js`、`client.js`、`src/`、`locale/`、`icon.svg`、`cordis.patch.yml`、`package.json`)
2. `dependencies` 里加 `"dsh-our-free-model": "1.1.2"`——写版本号,**不要写 `link:`**
3. `dsh.profile.bundles` 末尾追加 `"dsh-our-free-model"`
> **不要**再往 `cordis.patch.yml` 里加条目。被 `dsh.profile.bundles` 引用的包,它自带的
> patch 层会自动生效;两处都注册会报 `duplicate loader entry id: our-free-model`。
#### 整合包(托管安装)
插件被整合包(EAC 整合包、Mojobox 等)安装时,由**安装方**负责更新时机与字节:
在安装时把 bundle config 设为 `distribution: 'managed'`(或向 settings.json 写入
同一字段),插件的应用内升级、公告 feed 与热重载即全部停用——两个写入者同时操作
同一个安装目录只会把它写坏;模型 lane 不受影响。相关验收见
`scripts/offline-test.mjs`;目录就绪记录(manifest 0.15 / 出处 / 许可 / 完整性)
在 `catalog/` 下。
### 启动桌面端前先自检
不要盲试,直接调用桌面端自己的闸门代码:
```bash
node -e "
const g = require('<APP_ROOT>/sidecar/dist/lib/profile-upgrade.js');
const app = '<APP_ROOT>', profile = '<DSH_HOME>/profiles/<profile>';
console.log(g.planProfileUpgrade(app, profile));
g.assertProfileStartup(app, profile);
console.log('startup gate: PASS');
"
```
期望看到 `status: 'compatible'`、`mismatches` 为空数组,然后 `PASS`。
只想确认 bundle 组合是否正确、不启动界面:
```bash
DSH_HOME=<DSH_HOME> dsh --profile <profile> --dump-config | grep our-free-model
```
应当只出现**一个** `id: our-free-model`。
### 安装失败:`ERR_PNPM_VIRTUAL_STORE_DIR_MAX_LENGTH_DIFF`
这是**目标 profile 的 pnpm 状态问题,与插件仓库无关**(报错发生在下载插件之前):profile 里已有的
`node_modules` 是旧版 pnpm 生成的,dsh 更新后自带的 pnpm 版本变了,pnpm 拒绝在旧参数上继续安装。
关掉 dsh,删掉 profile 的 `node_modules` 和 `pnpm-lock.yaml` 让它重建,然后重新安装:
```bash
rd /s /q "%DSH_HOME%\profiles\web\node_modules"
del "%DSH_HOME%\profiles\web\pnpm-lock.yaml"
```
同时出现的 `Ignoring broken lockfile` 警告会随重建一起消失。
## 使用说明
**选模型**:打开输入框的模型选择器,选 `Our Free Model` 分组下的任意模型。选择按会话持久。
**调思考强度**:同一菜单里的 `Effort`,三档 `Light / Balanced / Deep`。档位越高,思考占用的
输出预算越多;上限是**强制下发**的,所以差异可测量,不是装饰。它是思考与可见回答**共用**的一
份额度,所以思考关不掉的模型会把三档整体抬高(`设置 → Our Free Model` 的模型卡上写着每一档
的实际数字)。如果觉得回答被截断,先换 `Deep`,或调高设置里的单次输出上限。
**给其它本地工具用**:`设置 → Our Free Model → 本地转发`,启用后复制 base URL 并生成 Key。支持:
```text
GET /v1/models
POST /v1/chat/completions 流式与非流式
POST /v1/responses
```
**重新核对地区**:点"重新探测可用性",会按当前出口重跑探测。开/关 VPN 后再点一次,
地区受限模型会在两个分组之间自动迁移。
**接收公告**:一切自动。仓库主人推送新公告后,运行中的插件在一个轮询周期内(默认
30 分钟,也可在公告中心点"检查新公告"立即拉取)收到通知:普通公告弹 toast,
`urgent` 级别直接全屏弹窗,两者都会出现在公告中心并保留未读标记。想同时收到
系统级通知,在公告中心点"开启系统通知"。
**升级插件**:`设置 → Our Free Model → 插件升级` → 检查更新 → 立即升级。全过程
在应用内完成(下载 → 校验 → 备份 → 替换 → 热重载),不需要重新安装,也不需要
重启应用。升级失败会自动恢复上一个版本并给出原因。
## 实现结构
```text
index.js Host 半身:适配器注册、清单与可用性探测、设置/用量存储、
webServer 路由、转发端口生命周期、公告/升级/热重载接线
adapter/ 内核接缝:全包唯一允许 import @deepseek-ai/* 的地方
(kernel.js:attribution User-Agent,失败降级为字面量)
src/adapter.js 结构性 LlmAdapter:providerInfo、listModels、resolveModel、
prepareCall、stream、providerRetryPolicy
src/upstream.js 网关身份:凭据、session/request id 铸造、工具指纹、按线协议选端点
src/stream.js 三种线协议解码(chat / messages / responses)归一为 harness StreamChunk,
并做不相交的 token 计数
src/messages.js harness 消息 -> 各线协议形态,外加工具调用配对修复
src/effort.js 思考档位 -> 输出预算
src/forward.js 独立的 OpenAI 兼容监听器
src/trust.js 插件路由的请求信任围栏(connection 服务桥 + 结构化围栏)
src/push.js SSE 推送枢纽:公告到达、更新可用、升级完成
src/feed.js 远程公告 feed:多源拉取、校验、缓存、到达检测
src/updater.js 应用内升级:清单校验、SHA-256 分级校验、备份、原子替换、回滚
src/reload.js 自热重载:镜像内核 HMR 的缓存清除 + 重导入 + 重注册 + 回滚序列
client.js 浏览器半身:手写 ModuleLoader bundle,无构建步骤
```
几个值得知道的架构决定:
- **一个适配器,两条 provider 路由**。harness 的模型选择器严格按 provider 路由分组,
而清单的线格式里根本没有 group / tag / badge 字段。所以要出现独立的 `region-limited`
标题,唯一办法就是再注册一条路由;又因为客户端会丢弃空分组,一旦地区不再限制,
两个分组会自动合并成一个。
- **结构性实现适配器,不 import `@deepseek-ai/dsh-llm`**。内核从不检查 `instanceof`,
所以鸭子类型即可。这让插件不把依赖钉死在某个内核版本上,也是同一份代码能同时跑
0.1.5 与 0.1.7 的原因。
- **自己的 JSON 存储,而不是 settings seam**。settings 注册 API 在两版内核间不一致;
私有 JSON 存储行为一致,并且转发 Key 落在一个 `0600` 文件里,不进入任何共享设置文档。
### 为什么用预算,而不是 `reasoning_effort`
实测过:在这条车道上把 reasoning-effort 字符串发给上游是**空操作**——三个不同名义档位
反复采样,思考 token 数量在统计上无法区分。做一个不起作用的控件比不做更糟,
所以思考强度实现为**硬性输出 token 上限**,它确实会约束——留痕的思考 token 随档位单调上升。
### 三个真实浪费过调试时间的内核行为
这里摘出来是因为任何写 provider 插件的人都会踩:
1. **`providerRetryPolicy()` 被原样存下来用**。两版内核都不解析它,而退避调度器读的是
**顶层**的 `initialDelayMs / maxDelayMs / jitterRatio`。把这三项嵌在 `backoff:{}` 里,
它们读到 `undefined`,于是 `undefined * 2ⁿ = NaN`,而持久会话日志直接拒绝非有限数——
一个本可恢复的瞬时故障就变成整轮对话报废。要返回**已解析好的扁平策略**。
2. **一次被打断的工具调用会永久毒化该会话**。没有对应结果的工具调用重放时上游回
`400 invalid_request_error`,此后该会话里**每一次**请求都失败。本插件在发送前修复配对,
三种线协议共用同一处修复。
3. **没在 `inject` 里声明的服务,属性直读是抛错不是 `undefined`;而 `ctx.get()` 在服务
"还没被 provide 出来"时返回 `undefined`。** 这两条本轮各咬了一口:把 `inject` 缩到只剩
`llm` 之后 `typeof ctx.interval === 'function'` 直接抛 `cannot get property "timer"
without inject`(`ctx.interval` 是 `timer` 服务上的 mixin),插件在**所有** composition
里都不再激活;改用 `ctx.get('webServer')` 之后它返回 `undefined`——不是因为没有 web
server,而是因为插件比 web 半身先加载——于是设置页路由一条都没挂上。正解是
`ctx.inject(deps, callback)`:为需要的服务开一条自己的 fiber,让它去待命,
而不是在加载的那一瞬间猜一次。
### 为什么按 body 的形状而不是 `Content-Type` 读响应
这条车道会在高负载下用 200 + `application/json` 回一整套 SSE 帧。旧写法信 header,
于是 `await response.text()` 把整条流读成字符串、`JSON.parse` 失败、整轮报废——而那个
错误对象带着 `status: 200`,还会让可用性探测把这个**完全能用**的模型判成"不可路由",
从下拉框里消失一轮。现在读法是先嗅探首块(≤4 KB)按形状分流,再把已经读到的字节
重新喂回流里,实时性一点不损失;`src/http.js` 的 `sniffBody` 是唯一判据。
### 为什么速度那一栏会显示 `—`
早先的版本给一条实际只有 ~40 tok/s 的车道报出过 2 941 tok/s。问题不在网关——直连
读包的探针显示 64 个帧跨了 5.6 秒,是实打实的增量投递——而在分子分母量的不是同一段
时间:一次调用计费 422 个输出 token,其中 291 个是**一个帧都没流出来**的 reasoning,
它们早在第一个可见 token 之前就生成完了,可窗口的起点正是那个 token。拿整段
completion 去除答案文本落地的几秒钟,不叫解码速度。
所以现在只有"装得下分子的那段时间"才允许出速率:`windowTokens()` 把没流出来的
reasoning token 从分子里剔掉,`decodeWindow()` 拒掉短到没法计时的窗口和快得不真实的
速率,看板和模型表都改成 `Σtoken / Σ秒` 而不是把每次的速率再取平均——曾经有一个 1 ms
的窗口把 26 次调用的均值抬了三个数量级。于是答案挤在一两个大帧里落地的模型,就是
没有可测的输出速度,它显示 `—`。
### 长思考截断后的自动恢复(issue #12)
所有模型默认启用一次有界恢复,Chat Completions、Messages、Responses 三种上游协议
共用这项行为。触发条件是:第一段只有非空思考、没有正文或工具调用,且上游在没有正常
结束帧时直接关闭流;用户取消、正常结束、达到输出上限和明确的上游错误不会触发恢复。
插件把已收到的思考作为检查点文本,与原始输入一起发起**一个新请求**,要求直接整理回答。
这不是上游原生 resume,也不会重新播放已经显示的思考。恢复段关闭工具调用;已经输出
正文或开始调用工具的截断回合不会恢复,避免重复内容或重复执行。
为避免重复长思考,恢复提示要求结论优先、最多 800 词;这是提示约束,不是硬性 token
限制,模型可能不遵守。恢复属于降级回答,原要求的长篇或逐步分析可能被简化。
每个逻辑回合最多两次物理请求:原请求加一次恢复。默认总时限 480 秒,恢复段最多
180 秒,且受剩余总时间约束;恢复输出上限 8192 tokens,并继续受用户与模型的上限
约束。首段已知的输出 tokens 会从原预算扣除,恢复预算不足 512 tokens 就不发起恢复。
检查点最多 131072 字符,还必须通过文本字节的保守上下文余量估算;此估算不计算图片
token,并非精确 tokenizer 校验。超限就停止恢复,不会无限续接。恢复必须正常结束且
产生非空白正文才算成功;失败返回 `STREAM_CUT`,宿主不会把它整轮重新发送,用户取消
则保持 `aborted` / `ABORTED`。
本机 `settings.json` 的 `streamRecovery: false` 可关闭恢复;也可用对象的 `enabled`
开关及数值限制缩小边界,数值只能低于或等于默认上限。默认没有模型名单限制。
用量看板把**物理请求**和**逻辑回合**分开统计。上游请求、请求失败按每次实际发出的
请求计数;对话回合、回合失败、已恢复按一次适配器调用的最终结果计数。比如首段中断、
续写成功会显示为 2 次上游请求、1 次请求失败,同时是 1 个对话回合、0 次回合失败、
1 次已恢复。每段样本仍包含恢复关联、段序号与 `noUsage` 等标记;通过 `recoveryId`
关联各段的 `noUsage` 判断缺失。最终发给宿主的 usage 只汇总上游实际报告的已知值;
缺少某段 usage 时,它不是整轮完整 token 总量。思考检查点不写入统计文件。
升级前的旧统计只有物理请求记录,迁移后显示的回合和失败数是基于这些记录的估算,
看板会明确标注;升级后的回合按最终结果精确记录。
请求、回合和 Token 总量是累计值;速度、首帧和热力图使用本机保留的历史窗口。
已在一次真实 MiMo V2.6 Flash · Deep 请求中恢复成功;此前一次恢复也曾在 480 秒
截止时失败。这不代表全部模型均已实测,也不能保证每个长思考回合都能生成答案。
下节列出的 v1.3.1 记录描述的是恢复加入前的行为。
## 验收情况
在 Windows 环境、对真实上游实测。本节按轮次记录,并写明每一行的验证方式——只有标着
「真实上游」与「实机点击」的那些才是用户在界面上会看到的行为。
### Issue #12:验收状态
恢复实现、离线回归与真实上游结果单独记录在
[`docs/issue-12-recovery.md`](docs/issue-12-recovery.md)。合并前 review 修掉一处告警
重复累积的缺陷并补了回归,recovery 99/99、truncation
35/35、fingerprint 新增 11 项与 typecheck 已通过;其中 12 项使用真实插件和本地 HTTP
验证转发成功、失败、客户端断开与统计落盘。最终全量结果以技术文档为准。
真实 MiMo Deep 首段在 304.161 秒自然 EOF,
恢复段 32.777 秒生成 4264 字符正文,以 `stop` / usage / `[DONE]` 正常结束;共两次
请求、336.942 秒。第一段无 usage,汇总仅为已知用量。第一次 480 秒恢复失败记录
保留在技术文档中;真实 dsh UI 与其他模型尚未逐一验收。
### 历史:v1.3.1(对应 issue #8、#9、#10)
| 项目 | 方式 | 结果 |
| --- | --- | --- |
| 两种工具结果形状都真的送达模型 | 真实上游 `host-selftest.mjs` 第 2 步 | 同一模型、同一个 `get_weather` 结果,分别按 pre-V4 的 `user`+`tool-result` 与 V4 的 `role:'tool'` 各发一轮:两条都 `finish=stop`,且模型都复述出结果里的 `22°C / sunny`。改动前插件对第一种形状完全无感(`src/messages.js` 从头到尾没出现过 `tool-result`),那一轮发出去的请求里既没有工具调用也没有结果 |
| 消息形状取自记录,不是想象 | 本机全部 4 处 session 根、共 40 个 `session*.jsonl.zstd`(zstd 需按帧切开逐段解压) | `tool/result` 事件共出现两种形状:`role:'user'` 携带 `tool-result` 块 **362 条**(V1/V3 格式——AIO 6.9.3 的 v3 家目录写 336 条,`~/.dsh` 的 v1 写 26 条),V4 的 `role:'tool'` **27 条**(v4 格式的会话副本)。两代都有人在用,而插件只认后者——issue #9 的判断成立,只是它描述的形状比报告者的内核更新 |
| 投影与配对修复 | 离线 `projection-test.mjs`(新增,40 条断言) | 三条线 × 两种形状:调用与其结果都落到 wire 上、按 call id 一一对应;三个并行调用各自的结果逐个送达,Messages 线合并进同一个 user 轮且 `tool_result` 排在最前(与内核自己的 Messages 适配器同一条规则);结果里嵌的图像不再消失(Chat 走紧随其后的 user 轮,Messages 直接嵌进 `tool_result`);**同一份结果在任一线上只出现一次**(按字节数答案出现次数,不看块类型——块类型断言看不见重复);无 call id 的工具答案、悬空调用、孤儿结果照旧剔除;已配对历史逐字节不被改写 |
| `pwsh` 顶替 `bash` 槽位能被网关接受 | 真实上游 `probes/shell-slot-promotion.mjs`(新增) | 声明名 `bash, glob, grep, read`,其中 `bash` 携带 pwsh 的真实 schema、无诱饵:网关不回 `FreeTierError`,`finish=tool-calls`,模型返回的调用被还原成 **`pwsh`**(`{"command":"Get-Date"}`)——即内核真的能执行。离线另有 `fingerprint-test.mjs`(新增,20 条)钉住不重复声明、真实 `bash` 优先、无 donor 时才落诱饵 |
| 半截流不再被当成说完 | 离线 `truncation-test.mjs`(新增 22 条) | 本地 HTTP 服务真的 `res.end()` 关掉一条没有 finish token 的流 → `kind=error`、统计行 `ok=false` 且带 `truncated`;三条线各验一次;`message_stop`、`response.completed` 这类真结束不误伤;有 finish 却没有 usage 的行标 `noUsage`,`0/0` 与"真的没有产出"从此可分。**重发按已经流出什么来分**:一个 token 都没出来的截断仍是 `TRANSPORT`(可重试),已经流出内容的截断是 `STREAM_CUT`(名单之外,当场失败)——理由见下一行 |
| 一次真实内核会话量出来的重试代价 | 真实内核 `dsh` web + 浏览器里发的一轮 MiMo V2.6 Flash · Deep | durable 会话日志逐事件取回:`llm/retry retry=1 delay=702ms code=TRANSPORT` 落在 304s,`retry=2 delay=1124ms` 落在 608s,`turn/end {kind:"error",code:"TRANSPORT"}` 在 912s;`stats.json` 里三条 `ok:false truncated:true` 的 0/0 行,间隔 304s;界面上是「本轮运行失败」加原句加 `TRANSPORT`。即:这条车道的掐断由这轮长度决定、可重试代码只会把同一个五分钟付三遍,所以内容已流出时不再重发 |
| 复查本轮发布,找出上面两条自身的毛病 | 离线 `projection-test.mjs` 第 7 节、`truncation-test.mjs` 第 12 节,另加 `tui-test.mjs` 一次转发端往返 | ① Messages 线在认出 V4 的 `role:'tool'` 之后,把同一段结果既放进 `tool_result`、又留在紧随的普通块里——文本两份,图像连 base64 一起两份;现在发出结果就跳过该消息自身,把修复撤掉这条断言立刻红。② `response.incomplete` / `response.failed` / `response.done` 同样是 Responses 线的正常收尾,此前只认 `response.completed`,于是撞到输出上限的一轮被读成"被掐断"、白白烧掉两次重试;现在仍由 status 决定 finish token,且不携带原因的收尾帧不再抹掉前一帧已经给出的 token。③ 转发端的 `/v1/responses` 从来没有用例走过工具历史,现用桩上游逐字节核对发出去的 `messages`:调用与它的结果都在 |
| 这条车道的正常收尾长什么样 | 真实上游 `probes/stream-terminal-frames.mjs`(新增) | 逐帧抓取原始 SSE:`finish_reason:"stop"` → 带 usage 的帧 → `data: [DONE]` → `{"choices":[],"cost":"0"}`,两个模型一致。所以"始终没有出现 finish token"确实是异常,而不是这条车道的另一种正常写法——这也是 #10 判定条件的依据 |
| 真实车道上不误判 | 真实上游 `host-selftest.mjs` 全量 | 11 个模型探测 + 普通对话 + 两轮工具 + 三档思考 + 视觉输入,全部落到各自的正常收尾(`stop` / `tool-calls` / `max-tokens`),没有一条被读成截断;被地区门挡住的那个仍回 `REGION_BLOCKED` |
| 上游来源成文(#8) | 代码里全量出网目标逐条核对 | 新增[上游是哪些源](#上游是哪些源):`opencode.ai/zen/v1/*`(推理与 `/models`)、本仓库 `feed/*.json`(raw 优先、jsDelivr 兜底)、`api.ipify.org` / `ipinfo.io` / `ipapi.co`(只读本机出口 IP 与国家码)。当前没有号池、没有中转 |
| 离线套件 | `npm test`(18 个套件) | 全绿;新增 `projection`、`fingerprint` 两套,仍不出网、不花免费额度 |
### 上一轮:v1.2.2(对应 issue #1–#4、#6)
每一行都写明**用什么方式验的**:`离线假内核`不出网、`真实上游`是插件真打网关但跑在
手搭的 cordis context 上、`真实内核`才是把插件装进 dsh 里启动。这三者的差别本轮吃过一次
教训——见最后一行。
| 项目 | 方式 | 结果 |
| --- | --- | --- |
| 发布清单与实物一致 | 离线 `build-manifest.mjs --check` + `release-e2e.mjs` | 26 个发布文件全部一致。改动前线上 main 的 1.2.1 清单已漂移 **6 个文件**(`index.js`、`README.md`、`README_EN.md`、`src/catalog.js`、`src/store.js`、`src/upstream.js`),比 issue #1 报的 2 个更多——即提出 issue 之后又复发了一次。现已随版本重生成;`--check` 与新增的 `release-e2e.mjs`(拿真实清单+真实文件把升级完整跑一遍,含"清单谎报 1 个字节必须被拒绝"的反向对照)都进了 `npm test` 和 CI。本轮补上两个同源漏洞:清单现在按 **LF 归一化后的字节**计算(`.gitattributes` 是 `* text=auto eol=lf`,而 Windows 上的编辑器可以把工作区写成 CRLF 且 `git status` 不报错——这正是 issue #1 上一次的回来方式),目录也改成**递归**收集(旧的单层扫描会漏掉 `src/lib/x.js` 这类嵌套文件:npm 会装它、清单里没有它,而 `installStaged` 会把清单没点名的文件从用户机器上删掉)。两条各有一条常驻断言,后者还把 26 个文件全量转成 CRLF 再跑 `--check` |
| 离线套件 | `npm test`(14 个套件) | 14/14 通过;不出网、不花免费额度。套件端口取自临时端口段(写死的端口被两个进程同时跑会撞车:本轮实测——先占住 tui 写死的那个端口再跑它,请求打到别人家的监听上,套件一直挂到 runner 的超时被杀、打印出来的"失败详情"是六行 `ok`),runner 对每个套件有 60 s 硬超时并把挂死原因打出来 |
| 新测试真的能咬住回归 | 逐条把旧行为改回去再跑 | `computeMembership` 的未判定模型:改回旧写法 → picker 报 `Cannot read properties of undefined (reading 'state')`(2 条检查失败);`postStreamed` 的 Content-Type 判定:改回旧写法 → sniff 7 条检查失败。恢复后各自全绿。本轮新增的断言另跑了一遍**变异测试**:在临时副本里把 9 处修复逐个改回去再跑对应套件,9/9 变红(逐条输出见下方"本轮复审"三行) |
| 探测判定与选择器广播 | 真实上游 `host-selftest.mjs` | 清单 10 个模型 → 主分组 7 + `region-limited` 2,转发端口 `/v1/models` 同步 9 个;`deepseek-v4-flash-free`(`Model is unavailable`)移出下拉框并在设置页保留拒因;5xx/429/断网一律保持可达(issue #3 的判定不再误伤抖动) |
| 未判定模型不再致命 | 离线假内核 `picker-test.mjs` | 清单新增一个模型、它的探测被测试按住时,`listModels` 与 `/summary` 都正常返回,该模型 `availability=unknown` 且照常广播 |
| 思考档位仍然强制 | 真实上游,MiMo V2.6 Flash | `light` 现在发 4096:同一 prompt 下 2 980 tokens 正常 `stop`;改动前 `light`=2048 在同一个 prompt 上以 `length` 收尾 |
| 长回答不再被截断 | 真实上游 `probes/long-answer.mjs` | `balanced` 实发 `max_tokens=16384`,一次请求 10 164 output tokens(19 914 字符、194 秒)后 `finish=stop`;同样的请求落在旧的 8192 档上必然以 `length` 结束——就是 issue #2 报的现象。设置页模型卡印出每一档真实数字(`默认档上限 16K`,已在浏览器里读到) |
| SSE 帧不再被 header 出卖(issue #6) | 离线假网关 `sniff-test.mjs` | 200 + `application/json` + 体内是 SSE 帧:正常吐 token,不报错;跨 chunk 截断的中文字符、超过 4 K 嗅探窗口的 400 帧长流、单包 JSON、空 body、HTML 杂七杂八全部按形状分流。同一场景在旧代码上会让探测把可用模型判成 `unavailable` |
| 无 web server 的 composition | 离线假内核 `scripts/tui-test.mjs` | 只挂 `llm` 时 `apply()` 不抛错、注册两条路由、跑完一轮流式对话、转发端口起来并拒掉无 Key 请求;后台循环走普通 unref 定时器。看板半身停在待命状态,`webServer` 一出现就自己挂上两条路由。**真实 dsh-tui 尚未实机验证**:本机两套内核(dsh 0.1.7 源码构建、AIO 6.9.3)都不含 tui profile |
| 插件在真实内核里真的能装 | **真实内核** `dsh` 0.1.7-rc.1 web,端口 3099 | 启动无 `did not activate`;`/api/our-free-model/summary` 200(10 个模型:6 available、2 region-blocked、1 unknown、1 unavailable)、`/events` 起来推 hello;浏览器里进 `设置 → Our Free Model` 逐项读到 10 张模型卡、`不在选择器中` 分组、`思考不可关` 与 `默认档上限 16K` 标签,控制台零报错。本轮最初版本在这一步是**失败**的:`inject` 只留 `llm` 之后读 `ctx.interval` 抛 `cannot get property "timer" without inject`,插件在所有 composition 里都不再激活 |
| 复审自己抓到的两个新问题 | 定向复现 + 常驻断言 | ① head 嗅探阶段被 abort 时抛的是原生 `AbortError`(`code` 是数字 20),`toFailure` 认不出来会降级成 `TRANSPORT`——而 `TRANSPORT` 在可重试名单里,用户主动取消的回合有被内核重试的口子;现在三条读取路径共用 `classifyStreamFailure`,sniff 里两条 abort 断言常驻。② 提交后我又改了 `src/http.js`,清单再次漂移,被 `--check` 与新加的 `release-e2e.mjs` 当场逮住——门禁按设计起作用,但也说明**任何一次改动后都得重跑** |
| 本轮复审:请求路径上两处"丢整轮/重复计费" | 本地假网关定向复现 + 常驻断言 | ① `readHead` 要填满 4 KB 才判定形状:一条**已经说完**的短回答,只要网关不立刻关掉连接,就会在嗅探窗口里等到 deadline,然后连已经读到的帧一起被 cancel 丢掉——整轮以一个可重试的 `TIMEOUT` 收场,内核再发一次、额度再花一次(复现输出就是 `frames delivered: 0`)。现在首帧认出是流就立刻放行,顺带不再压住每轮开头那 4 KB 的 token。② 流内错误的分类写进了 `llmCode`,而 `toFailure` 只读 `code`——于是每一条流内拒绝都降级成可重试的 `TRANSPORT`(已经吐过 token 的回合被重发),中途的 `RegionError` 也永远触发不了换出口重探。现在流内错误与错误信封走同一个 `classifyFailure` |
| 本轮复审:设置入口没做类型检查 | 离线 `picker-test.mjs` + `effort-test.mjs` | `probeIntervalMinutes:'abc'` → `Math.max(1,'abc')` 是 NaN,而 `setTimeout(fn, NaN)` 在 Node 里等价于 1 ms:一秒一整轮全量探测。`defaultMaxTokens:0`(把设置页输入框清空就会发 0)→ `min(容量, 0)`,每一轮都被裁到 512 token,而选择器照旧印着 4 K/16 K/32 K 的梯子。现在数值项在 `POST /settings` 入口和取用处各设一道,非正值一律按"没设过"回落。同一批断言还钉住转发端口**只能绑回环**(`0.0.0.0` 直接 400 并写明原因),以及 `connection` 的准入决定是**逐请求**读的(改回 apply 时一次快照 → 那条 401 断言当场变红) |
| 本轮复审:三条"看起来在测"的套件 | 变异测试(临时副本里逐条改回旧行为) | `retry-safety-test.mjs` 的四个用例全都落在"模型不在清单上"的提前返回(传的是 `model:"our-free-model/test-model-free"`,而 `baseModelId` 只剥 label 不剥路由),一个请求都没发出去过——改成真实调用后 7 个用例逐个钉住 code/可否重试/是否触发区域重探。`tui-test.mjs` 的 unref 断言比的是两个不同总体的计数(把 120 s 循环的 `unref()` 去掉仍然全绿)→ 改为按周期逐个核对。写死在套件里的端口(tui 的 18931)与另一个进程抢同一个端口时,表现为 180 s 静默挂死、打印出来的"失败详情"是六行 `ok` → 端口取自临时端口段,runner 加 60 s 硬超时并如实说明"它挂住了" |
| usage 计数干净 | 离线 `retry-safety-test.mjs` | 没有 `prompt_tokens_details` 的 usage 不再把 `inputTokens` 算成 `NaN`;转发端口按 `prompt_tokens/completion_tokens` 回报,被网关拒绝的转发请求返回错误而不是空的 200。Messages 线上 `message_delta` 只带 output 一侧,旧写法把整条 usage 记录覆盖掉、每个 Claude 回合的 prompt tokens 记成 0;现在按字段合并 |
| 本轮复审:5xx 的原因短语不再被当成判定(issue #3 的同类回归) | 离线 `sniff-test.mjs` + 反向验证 | `stateOf` 的消息兜底匹配不看状态,而反向代理给 503 的标准原因短语就是 `Service Unavailable`——过载的网关于是被读成"点名拒绝了这个模型",模型一个个从选择器里消失(issue #3 修掉的代价从消息兜底那条路回来了)。现在这条兜底只在状态没能替网关回答时才出声;响应体里点名模型的(含 5xx 下的 `type: ModelError`)仍算拒绝。新增 4 条断言,还原旧实现后其中 2 条立刻变红 |
### 上一轮:v1.1.2(公告、升级、热重载与信任围栏)
全部新能力都做了**实际操作验证**,包括浏览器与 DSHEAC AIO 桌面窗口内的逐项点击:
| 项目 | 结果 |
| --- | --- |
| `dsh` 0.1.7-rc.1(源码构建) | 启动无报错;选择器两个分组正确;多轮工具调用完成 |
| `dsh` 0.1.5-rc.2(DSHEAC AIO 6.9.3 内核) | 启动无报错;与已装的其它第三方插件并存 |
| EAC 启动闸门 | 安装时与**应用内升级之后**各跑一次:`compatible` / `PASS` |
| 模型可调用性 | 10 个清单模型全部可达(该轮行为:连探测失败/暂不可用者也保留在选择器里;v1.2.2 起不再广播判定不可用的模型);real chat、多轮工具、视觉输入在两套内核通过 |
| 真实 harness 对话 | dsh web 与 AIO 桌面端各自完成一轮真实对话并收到回复 |
| 公告 feed | 本地"仓库服务器"上推送新公告 → 运行中的两端在一个轮询周期内到达 |
| 公告中心 UI | 4 条公告渲染(粗体/代码/链接/列表)、紧急红色徽章、未读点、单条/全部已读 |
| 紧急公告 | `urgent` 推送触发全屏弹窗,"知道了"即写入已确认状态 |
| Toast + 系统通知 | 新公告实时 toast;桌面端 `window.Notification` 通道存在(WebView2 权限策略拒绝授权时 UI 如实提示) |
| 应用内升级 | dsh web 与 AIO 桌面端各完成一次 1.1.0 → 1.1.2 升级:23 文件下载、SHA-256 校验、备份、替换、热重载;升级后 `/meta` 立即报新版本 |
| 升级安全性 | 哈希不匹配 → staging 丢弃、已装包不动、历史记录失败;升级产物通过 EAC 闸门并完整重启加载 |
| 热重载 | 按钮、API 两种入口;ESM 缓存清除 + 重注册 + 回滚序列;`generation` 递增、客户端经 localStorage 提示一次 |
| 客户端 bundle 热更 | 替换 `client.js` 后浏览器端由内核 client-hmr 自动重载(实测两次) |
| 信任围栏 | 非 loopback Host / 跨站 `sec-fetch-site` / 异源 `Origin` 一律 403;无 cookie 回环请求 401(与内核 `/api` 一致) |
| SSE 推送 | `hello`/`announcements`/`update`/`upgraded` 事件实测;热重载后自动重连 |
| 思考强度传递 | light/balanced/deep 三档实测:reasoning 2048(被预算截断)/ 3386 / 3522,输出单调上升 |
| 地区门 | 受限模型报 `REGION_BLOCKED` 并留在自己的分组 |
| 转发端口 | `/v1/models`、流式与非流式 `/v1/chat/completions`;无 Key 请求被拒 `401` |
| 界面文案 | 无乱码;上游厂名只出现在仓库文档与公告正文两处——#8 要求的披露得有个用户看得见的位置,公告算说明位。模型选择器、设置页与报错文案里始终不出现 |
**没验证的部分如实说明**:OS 级通知的**最终视觉呈现**没有逐像素确认——AIO
桌面端的 WebView2 权限策略拒绝了 `Notification.requestPermission()`(插件的
Tauri 通知通道存在,纯浏览器路径可用,被拒时设置页如实显示"需要在系统/浏览器
设置里手动恢复");AIO 对内置插件有报告式的静态启发扫描(`TROJAN_EXFIL_ENV`
对 `src/upstream.js` 的 env+URL 邻近模式会记一条报告日志),属报告不动文件。
## 已知边界
- **"不限量"指的是没有额度这套东西**:不充值、不按 token 计费、没有套餐和用量面板。但这条车道按 session 记速率,短时间内打满会回 `429`。插件把该模型标成"已达限额"而不是隐藏,下一轮探测自动恢复。
- **个别模型上游本身很慢**。`nemotron-3.5-lightning-free` 有一次实测首字超过 30 秒。这是上游延迟,看板会如实显示。
- **输出速度有时会显示 `—`**。答案只落在一两个大帧里、或者思考过程根本不流式返回的模型,没有值得相除的时间窗。看板宁可空着,也不会把"想的时间"算成"写的速度"。
- **自动恢复有次数、时间和上下文边界**。只恢复纯思考 EOF,最多追加一个请求;已有正文、工具调用、取消和明确错误不会恢复。它是用检查点重新请求,无法保留上游未发出的内部状态,也不能保证成功。缺失 usage 的用量仅为已知部分。
- **能力只到探测能证明的程度**。公开清单和实测都拿不到证据的,就不标注。
- **源码是明文 JavaScript**。作为本地插件必须如此。能拿到这个目录的人就能读懂网关逻辑——请把它当成这种分发形态的固有属性,而不是"混淆一下就能解决"的问题。
- **桌面端安装需要真实目录**,原因见[安装](#安装)一节。
- **升级与热重载的信任边界**:应用内升级信任的是插件仓库本身——能推送仓库的人能推送任意代码,这与"安装一个插件更新"的信任模型相同。文件层面的完整性由 SHA-256 清单保障,代码层面的安全由客户端的白名单 HTML 渲染器与宿主的插件隔离承担。
- **DSHEAC AIO 的 WebView2 权限策略可能拒绝通知授权**(本机实测 `denied`)。被拒时公告中心的开关会如实提示;纯浏览器访问 dsh web 不受影响。
- **插件接口的鉴权取决于 composition**:挂了 connection 服务的 composition(dsh web、AIO 桌面端)下与内核 `/api` 同级(需要应用自己的 cookie/token);没有 connection 服务的极简 composition 退回到结构化围栏(loopback + 同源检查),本机其它进程仍可访问——与内核在同类 composition 下的行为一致。
## 开发
```bash
npm test # 下面全部离线套件 + 清单一致性检查,一条命令
node scripts/client-lint.mjs # 浏览器半身:文案键与样式键双向覆盖、bundle 可执行
node scripts/retry-safety-test.mjs # 交给内核的失败对象、退避策略与 usage 计数必须可持久化
node scripts/speed-stat-test.mjs # 没有任何一次调用能被平均成假的 tok/s
node scripts/sanitize-test.mjs # 公告 HTML 白名单渲染器:XSS 语料必须全部被丢弃
node scripts/trust-test.mjs # 插件路由的请求信任围栏
node scripts/feed-test.mjs # 公告 feed:解析、故障转移、缓存、到达检测(本地 HTTP 服务器)
node scripts/updater-test.mjs # 应用内升级:清单校验、SHA-256、备份/回滚(本地 HTTP 服务器)
node scripts/effort-test.mjs # 思考档位 = 真正下发的 max_tokens,且与留痕的档位一致
node scripts/sniff-test.mjs # 200 响应按 body 形状分流:SSE 帧、单包 JSON、空 body、跨 chunk 多字节、中途 abort
node scripts/release-e2e.mjs # 用真实的 feed/manifest.json 装一遍升级,并验证谎报字节的清单会被拒绝
node scripts/picker-test.mjs # 选择器只广播真能用的模型,且永不广播空集合
node scripts/tui-test.mjs # 没有 web server 的 composition 里插件照样启动并出模型
node scripts/host-selftest.mjs # Host 半身端到端,会真实出网
node scripts/build-manifest.mjs # 发布:重新生成 feed/manifest.json(发布文件哈希清单)
```
`scripts/probes/` 是逆向过程中的一次性取证脚本——能力矩阵、地区门、
`reasoning_effort` 空操作采样、预算方言、悬空工具调用、工具名字符集规则、
原始读包时刻(`batch-delivery`)、逐帧到达与 usage 对照(`decode-window`)、
长回答会不会被额度截断(`long-answer`)。
其中六个跑的是本插件自己的代码,从仓库根目录就能执行
(`node scripts/probes/pairing-repair.mjs`);其余借助一个第三方 SSE 客户端直连上游,
模块路径写成了那个仓库的样子,所以只作为取证记录保留,不能当测试套件用。
它们都没有接进 `npm test`——那是取证记录,不是套件;`npm test` 跑的是上面这些不需要
出网、不花免费额度的离线检查。
需要 Node `^22.19.0 || >=24.0.0`。无安装步骤、无依赖。
```bash
npm run typecheck # tsc --noEmit,严格检查 adapter/ 接缝(可选:需要 typescript)
```
## 上游是哪些源
只有一个,而且它不是中转站:**OpenCode 的 Zen 网关**,`https://opencode.ai/zen/v1/*`。
装上插件之后,你的会话内容从这台机器直达它,中间没有任何第三方经手。
具体到代码——`src/upstream.js` 里每一条都在 2026-09-24 用直接请求核过:
| 用途 | 目标 | 带什么凭据 |
| --- | --- | --- |
| 推理请求 | `POST …/zen/v1/chat/completions`、`…/zen/v1/responses`、`…/zen/v1/messages`(按模型分流,见 `endpointFor`) | `Authorization: Bearer public`——这条车道本来就是公开免密额度,插件里没有属于你的任何密钥 |
| 模型清单 | `GET …/zen/v1/models` | 同上 |
| 公告与升级清单 | 本仓库的 `feed/*.json`:`raw.githubusercontent.com` 优先,`cdn.jsdelivr.net` 兜底 | 无 |
| 出口地区判定 | `api.ipify.org` / `ipinfo.io` / `ipapi.co`,只为读到你自己的公网 IP 与国家码 | 无 |
关于隐私与信任,把话说清楚:
- **没有号池、没有中转、没有二道贩子**。当前版本不存在第二条车道,上表四行就是这个插件会出网的全部目标;`npm test` 的离线套件一步不出网,只有 `scripts/host-selftest.mjs` 与 `scripts/probes/` 会主动去打这些地址,而它们要人手动运行。将来若加入新的来源,这一节会先于功能更新,不会默认把流量分给别人。
- **你的 prompt、工具结果与随附图像会作为正常推理请求发给这个上游**——与调用任何一家模型 API 没有区别。除此之外插件不上传任何东西:用量看板的数据、设置、转发 Key 全部只落在本机 `DSH_HOME/our-free-model/`。
- **免密不等于无人管**:这条车道靠 `x-opencode-*` 指纹识别客户端、按会话计免费额度,会因地区回 403、因超量回 429。模型集合与额度政策由上游决定,随时可能变化;插件能做的只是如实把不可用从选择器里摘掉。
- 这一节是仓库文档。应用内的选择器、设置页与报错文案仍然不出现上游厂名(那条约定见[验收情况](#验收情况))。
## 安全与隐私
- 所有状态写在 `DSH_HOME/our-free-model/`;用量与设置只落本地,不上传任何地方。
- 转发监听**只绑回环地址**,默认 `127.0.0.1`,无 Key 请求一律拒绝。把地址改成可路由接口会被直接拒绝(`POST /settings` 回 400 并写明原因;没有 web server 的 composition 里手工写进 `settings.json` 也一样不起监听)——这一格流量花的是本机这条免密车道,不该由一个字符串决定要不要给整个子网用。
- 转发 Key 由 `crypto` 运行时生成、用 `timingSafeEqual` 比对、存在 `0600` 文件里。本仓库不含任何硬编码凭据。`/` 与 `/health` 是存活探针,先于 Key 检查应答,但只回答"在不在",模型清单要 Key。
- 插件的 HTTP 路由带**请求信任围栏**(v1.1 起修复):插件的 `/api/our-free-model` 前缀在 webServer 的最长前缀分发下优先于内核 `/api`,曾绕过内核鉴权。现在每个请求先走 composition 的 `connection` 服务准入(与内核 `/api` 完全同级的 cookie/token 校验);connection 缺席的 composition 退回结构化围栏——loopback Host、拒绝跨站 `sec-fetch-site`、`Origin`/`Referer` 必须与 Host 同源同端口,**Host 缺失或为空也拒**(fail closed,不退回 socket 本地地址)。实测:异源 Host/Origin 403,无 cookie 回环请求 401。`connection` 是逐请求取的,因为浏览器半身要到插件加载之后才把它 provide 出来——快照式地在 apply 时读一次,围栏会整轮进程退化成结构化那一层(本轮把这条读取改回快照,picker-test 的 401 断言当场变红)。
- **公告 HTML 在客户端经严格白名单渲染**:`scripts/sanitize-test.mjs` 用 XSS 语料(脚本注入、事件属性、`javascript:`/`data:` URL、iframe/svg/form、样式注入、畸形标签)验证全部丢弃;不经过任何 `innerHTML` sink。公告源的 `feedUrl` 可被用户改指向任意 URL,因此渲染器按不可信输入对待。
- **应用内升级的完整性链**:清单校验(semver、路径逃逸、哈希格式)→ 下载逐文件 SHA-256 + 字节数 → staging 回读校验 → 安装后回读校验 → 任一步失败恢复备份;安装前强制重新拉取清单,杜绝陈旧清单。升级信任根是插件仓库本身(与安装插件更新相同),文件与代码边界见[已知边界](#已知边界)。
- 卸载只需移除 bundle 条目,插件不留任何补丁;它的数据目录是纯 JSON,可直接删除。
## 许可证
MIT,见 [LICENSE](LICENSE)。
本项目是独立插件,与任何模型提供方无隶属、认可或赞助关系。用它访问免费额度受各提供方
自身条款约束;在超出你自己机器的场景部署前,请先确认这些条款。
Install
dsh plugin --profile web add github:zouyuxuan122/dsh-our-free-model#98842df146aac9ae5aae8ecf08db3d976ca473a2
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install dsh-our-free-model from the hub