dsh-model 0.22.2 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -26,7 +26,7 @@ You may have several at once: Codex from ChatGPT Plus, a Kimi coding plan, free
26
26
  dsh-model turns them into **one address and one kind of key**:
27
27
 
28
28
  - 🔌 **Connect once, use everywhere**: every model shows up in dsh, grouped by source; any other tool uses the same address and key.
29
- - 🆓 **Get the most from free tiers**: WorkBuddy credits, free Gemini / Groq / OpenRouter keys and local models; every source is tagged Free / Free · limited / Paid.
29
+ - 🆓 **Get the most from free tiers**: free Gemini / Groq / OpenRouter keys, local models and WorkBuddy credits; every source is tagged Free / Free · limited / Paid.
30
30
  - 📅 **Daily check-in, credits claimed for you**: WorkBuddy (China), Qoder and Trae are checked in automatically every day.
31
31
  - 🔁 **Rate limits are not the end**: build a combo (say Codex → Kimi → WorkBuddy) and requests move to the next model on a rate limit or used-up quota; a model offered by several sources also switches automatically.
32
32
  - 📊 **Know where you stand**: plan usage with reset countdowns, remaining credits and when they expire, success rate and speed per model, recent failures with advice, and a notice when quota runs low.
@@ -48,7 +48,7 @@ On a dsh-vps server, use `sudo dsh-model setup`. On Windows, run it in a normal
48
48
  `setup` is safe to re-run. It:
49
49
 
50
50
  1. downloads and verifies the engine and runs it on `127.0.0.1:8317/v1`, with key auth enforced;
51
- 2. connects WorkBuddy (dsh-model's own bridge). Setup asks for no keys: turn on OpenCode Zen and other key-based sources from the management page when you need them;
51
+ 2. starts dsh-model's daemon (the management page, check-in and sign-ins run on it). Setup asks for no keys: for a free source we suggest **SiliconFlow** (reachable from China) or **Gemini** (if you can reach Google), added from the management page. WorkBuddy is used for check-in only; its models are not connected to dsh by default;
52
52
  3. installs the management page into dsh and syncs the models into dsh;
53
53
  4. on a VPS, also opens a public address `https://<dsh's domain>:9443/v1` (or `<IP>.sslip.io` when there is no domain).
54
54
 
@@ -82,7 +82,7 @@ If a source is signed in but has no usable plan (say, a Kimi account without Kim
82
82
 
83
83
  | Source | Cost | Notes |
84
84
  |---|---|---|
85
- | WorkBuddy / WorkBuddy AI | Free | Credits come with the account |
85
+ | WorkBuddy / WorkBuddy AI | Free | Credits come with the account. **Since 2026-10 WorkBuddy recognises conversations sent by dsh and blocks them**, so it barely works inside dsh; check-in still works |
86
86
  | Ollama / LM Studio | Free | Models running on this machine; they appear once installed and started, no sign-in |
87
87
  | Gemini (Google AI Studio), Groq | Free · limited | Paste a free API key; daily / per-minute limits apply |
88
88
  | OpenRouter | Free · limited | `:free` models are free (about 50 requests a day) |
package/README.md CHANGED
@@ -26,7 +26,7 @@
26
26
  dsh-model 把它们合成**一个地址、一种 key**:
27
27
 
28
28
  - 🔌 **一处接入,处处可用**:dsh 里自动出现全部模型,按来源分组;其他软件填上同一个地址和 key 就行。
29
- - 🆓 **把免费的用足**:WorkBuddy 积分、Gemini / Groq / OpenRouter 免费 key、本机模型都能接;每个来源标出「免费 / 免费·额度少 / 需付费」。
29
+ - 🆓 **把免费的用足**:Gemini / Groq / OpenRouter 免费 key、本机模型、WorkBuddy 积分都能接;每个来源标出「免费 / 免费·额度少 / 需付费」。
30
30
  - 📅 **每日签到,自动领积分**:WorkBuddy 国内版、Qoder、Trae 每天自动签到,不用记。
31
31
  - 🔁 **不怕限流**:自己排一个组合(如 Codex → Kimi → WorkBuddy),一个限流或额度用完就自动换下一个;同一个模型在几个来源都有时也会自动切换。
32
32
  - 📊 **心里有数**:套餐用量和重置倒计时、剩余积分和到期时间、每个模型的成功率和速度、最近失败的原因和建议;额度快用完时提醒。
@@ -48,7 +48,7 @@ dsh-model setup
48
48
  `setup` 可以重复执行,它会依次:
49
49
 
50
50
  1. 下载并校验引擎,在本机 `127.0.0.1:8317/v1` 上运行,强制 key 鉴权;
51
- 2. 接入 WorkBuddy(dsh-model 自己的 bridge)。安装时不问任何 key,OpenCode Zen 等要 key 的来源,需要时在管理页里打开;
51
+ 2. 启动 dsh-model 的守护进程(管理页、签到、登录都靠它)。安装时不问任何 key:免费来源推荐**硅基流动**(国内直连)或 **Gemini**(能上外网),装好后在管理页里填 key。WorkBuddy 只用来签到,模型默认不接进 dsh;
52
52
  3. 把管理页装进 dsh,把模型同步进 dsh;
53
53
  4. 在 VPS 上,额外开通对外地址 `https://<dsh 的域名>:9443/v1`(没有域名时用 `<IP>.sslip.io`)。
54
54
 
@@ -82,7 +82,7 @@ dsh-model setup
82
82
 
83
83
  | 来源 | 费用 | 说明 |
84
84
  |---|---|---|
85
- | WorkBuddy / WorkBuddy AI | 免费 | 账号自带积分 |
85
+ | WorkBuddy / WorkBuddy AI | 免费 | 账号自带积分。**2026-10 起 WorkBuddy 会认出 dsh 发出的对话并拦下**,在 dsh 里基本用不了;签到照常 |
86
86
  | Ollama / LM Studio | 免费 | 本机运行的模型。装好并启动后自动出现,不用登录 |
87
87
  | Gemini(Google AI Studio)、Groq | 免费·额度少 | 填免费 API key 即可,每天 / 每分钟有上限 |
88
88
  | OpenRouter | 免费·额度少 | 名字带 `:free` 的模型免费(每天约 50 次) |
@@ -30,7 +30,7 @@ import { Mgmt } from '../engine/mgmt.js';
30
30
  import { DshModelError, isDshModelError } from '../errors.js';
31
31
  import { L } from '../i18n.js';
32
32
  import { refreshOpencodeModels, removeOpencodeKey, saveOpencodeKey, validateKeyShape, verifyOpencodeKey } from '../integrations/opencode.js';
33
- import { riskNotice as workbuddyRiskNotice } from '../integrations/workbuddy.js';
33
+ import { blockedNotice as workbuddyBlockedNotice, riskNotice as workbuddyRiskNotice } from '../integrations/workbuddy.js';
34
34
  import { DSH_KEY_NAME, addKey, findActive, loadKeys, revokeKey, rotateKey, saveKeys } from '../keys.js';
35
35
  import { applyKeyChange, dshCandidates, dshKey, loadAll, saveAll, syncAll } from '../ops.js';
36
36
  import { defaultPick, pickedFor } from '../dsh/pick.js';
@@ -301,6 +301,9 @@ export class Daemon {
301
301
  return { enabled: false, riskNotice: upstreamRiskNotice(getUpstream(def.id)) };
302
302
  return { enabled: false, login: await this.startLogin(def) };
303
303
  }
304
+ // WorkBuddy 会拦 dsh 的对话:从关着打开时先提醒一次
305
+ if (def.kind === 'workbuddy' && !state.enabled && !opts.acceptRisk)
306
+ return { enabled: false, riskNotice: workbuddyBlockedNotice() };
304
307
  if (def.kind === 'engine') {
305
308
  const m = await this.mgmt();
306
309
  for (const c of credsFor(await m.credentials(), def))
@@ -866,7 +869,9 @@ export class Daemon {
866
869
  catch {
867
870
  // 不是 JSON 也算通了
868
871
  }
869
- return { ok: true, ms, reply };
872
+ // 测试只发一句短话;WorkBuddy 会拦带 dsh 内置说明的真实对话(2026-10),通过不代表在 dsh 里能用
873
+ const note = /^workbuddy(-ai)?\//.test(model) ? L('这只是一句短测试;WorkBuddy 会拦下 dsh 里的真实对话', 'Only a short test; WorkBuddy blocks real conversations from dsh') : undefined;
874
+ return { ok: true, ms, reply, ...(note ? { note } : {}) };
870
875
  }
871
876
  let message = text.slice(0, 300);
872
877
  let code;
@@ -46,7 +46,7 @@ export function classify(f) {
46
46
  // 服务商在挡官方客户端之外的调用(2026-10 起 WorkBuddy:国际版"请求被拦截",国内版 11-128 unapproved channel)。
47
47
  // 不去绕:改用别的来源
48
48
  if (BLOCKED_RE.test(m)) {
49
- return { kind: 'blocked', advice: L('被服务商拦截:它在限制官方客户端之外的使用,换模型、重试都没用。请改用其他来源(WorkBuddy 国际版不行可以用国内版),或建一个组合自动换到下一个;继续调用有封号风险,建议在「来源」里先把它关掉', 'Blocked by the provider: it is restricting use outside its official app, so retrying or switching its models will not help. Use another source (for WorkBuddy AI, try WorkBuddy China), or build a combo that moves on automatically; continuing may get the account banned, so consider turning this source off under Sources') };
49
+ return { kind: 'blocked', advice: L('被服务商拦截:它在限制官方客户端之外的使用(WorkBuddy 会认出 dsh 发出的请求并拦下,国内版、国际版都是),换模型、重试都没用。请改用其他来源,或建一个组合自动换到下一个;继续调用有封号风险,建议在「来源」里先把它关掉', 'Blocked by the provider: it is restricting use outside its official app (WorkBuddy recognises requests sent by dsh and blocks them, China and international alike), so retrying or switching its models will not help. Use another source, or build a combo that moves on automatically; continuing may get the account banned, so consider turning this source off under Sources') };
50
50
  }
51
51
  if (f.status === 429 || /cooldown|rate|频繁|too many/.test(m)) {
52
52
  return { kind: 'rate', advice: L('被限流:请求太频繁。等一会儿再试,或换一个来源的同款模型', 'Rate limited: too many requests. Wait a bit, or use the same model from another source') };
@@ -37,7 +37,31 @@ async function waitCatalogs(ctx, since, expected, timeoutMs = 45_000) {
37
37
  return cats;
38
38
  }
39
39
  /** 检测 App → 首次打印说明 → bridge 配置与服务 → 等目录。返回目录(调用方随后 syncAll) */
40
+ export const WORKBUDDY_SOURCES = ['workbuddy', 'workbuddy-ai'];
41
+ /** WorkBuddy 会拦 dsh 发出的对话时的提醒(新装默认关、从页面打开前都显示) */
42
+ export function blockedNotice() {
43
+ return L('WorkBuddy 现在会认出 dsh 发出的对话并拦下(「请求被拦截」),在 dsh 里基本用不了;「测一下」能通也不代表能用。dsh-model 不会去绕开这个限制,继续调用有封号风险。签到不受影响。仍要把它的模型接进 dsh 吗?', 'WorkBuddy now recognises conversations sent by dsh and blocks them ("请求被拦截"), so it barely works in dsh; a passing Test does not mean it works. dsh-model will not try to get around this, and continuing risks the account. Check-in is not affected. Connect its models to dsh anyway?');
44
+ }
45
+ /**
46
+ * 只签到(设计 §14.18):第一次装时把 WorkBuddy 的模型关掉,签到照常(签到不看来源开关)。
47
+ * 以前装过的(已有 riskNoticeAt)保持原样;只做一次,之后用户自己打开的不改回去。
48
+ */
49
+ export function applyWorkbuddyDefault(all) {
50
+ if (all.config.workbuddyDefault)
51
+ return;
52
+ if (all.config.bridge?.riskNoticeAt) {
53
+ all.config.workbuddyDefault = 'kept';
54
+ return;
55
+ }
56
+ const off = new Set(all.config.disabledSources ?? []);
57
+ for (const id of WORKBUDDY_SOURCES)
58
+ off.add(id);
59
+ all.config.disabledSources = [...off];
60
+ all.config.workbuddyDefault = 'checkin-only';
61
+ info(L('WorkBuddy 只用来签到:它现在会拦 dsh 的对话,所以模型默认不接进 dsh(要用可在「来源」里打开)', 'WorkBuddy is used for check-in only: it now blocks conversations from dsh, so its models are not connected by default (turn it on under Sources if you want)'));
62
+ }
40
63
  export async function enableWorkbuddy(ctx, all) {
64
+ applyWorkbuddyDefault(all);
41
65
  const variants = await availableVariants(ctx.paths.home);
42
66
  if (!variants.length) {
43
67
  skip(ctx.platform === 'darwin'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-model",
3
- "version": "0.22.2",
3
+ "version": "0.23.0",
4
4
  "description": "One OpenAI-compatible address for all your models: subscriptions (Codex, Kimi…), free tiers (WorkBuddy, Gemini, Groq, OpenRouter), local models (Ollama, LM Studio), daily check-in credits, auto failover. Works with DeepSeek Harness (dsh) and any OpenAI-compatible tool. 一个地址用上你所有的模型。",
5
5
  "keywords": [
6
6
  "dsh-plugin",
package/plugin/client.js CHANGED
@@ -412,6 +412,7 @@ window.__ModuleLoader__.load({
412
412
  m.recommended ? h('span', { style: { fontSize: 11, color: C.accent } }, L('推荐', 'Suggested')) : null,
413
413
  tests[m.id] ? h('span', { style: { fontSize: 11, color: tests[m.id].running ? C.sub : tests[m.id].ok ? C.ok : C.err, maxWidth: 260, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: tests[m.id].message || tests[m.id].reply || '' },
414
414
  tests[m.id].running ? L('测试中…', 'Testing…') : tests[m.id].ok ? L(`可用 · ${(tests[m.id].ms / 1000).toFixed(1)} 秒${tests[m.id].reply ? ` · ${tests[m.id].reply}` : ''}`, `OK · ${(tests[m.id].ms / 1000).toFixed(1)}s${tests[m.id].reply ? ` · ${tests[m.id].reply}` : ''}`) : (tests[m.id].advice || L('不可用', 'Not working'))) : null,
415
+ tests[m.id]?.ok && tests[m.id].note ? h('span', { style: { fontSize: 11, color: C.warn } }, tests[m.id].note) : null,
415
416
  h('button', { type: 'button', style: { ...S.btn, padding: '2px 8px' }, disabled: tests[m.id]?.running, onClick: (e) => { e.preventDefault(); void test(m.id) } }, L('测一下', 'Test'))))),
416
417
  msg ? h('div', { style: { fontSize: 12, color: C.err, marginTop: 6 } }, msg) : null,
417
418
  h('div', { style: { ...S.line, marginTop: 8, flexWrap: 'wrap' } },
@@ -490,6 +491,7 @@ window.__ModuleLoader__.load({
490
491
  s.loggedIn && s.kind !== 'opencode' && s.kind !== 'local' ? h('button', { type: 'button', style: S.btn, onClick: () => logout(s) }, s.kind === 'apikey' ? L('删除 key', 'Remove key') : L('退出登录', 'Sign out')) : null,
491
492
  (s.kind === 'opencode' || s.kind === 'apikey') && s.loggedIn ? h('button', { type: 'button', style: S.btn, onClick: () => onKey(s) }, L('换 key', 'Change key')) : null),
492
493
  picking === s.id ? h(ModelPicker, { source: s, onClose: (saved) => { setPicking(''); if (saved) void reload() } }) : null,
494
+ s.kind === 'workbuddy' && s.loggedIn ? h('div', { style: { fontSize: 12, color: C.warn, paddingLeft: 46, marginTop: 4, lineHeight: 1.6 } }, L('注意:WorkBuddy 现在会认出 dsh 发出的对话并拦下(「请求被拦截」),「测一下」能通也不代表能用。积分建议在官方 WorkBuddy 软件里用;签到照常。', 'Note: WorkBuddy now recognises conversations sent by dsh and blocks them ("请求被拦截"); a passing Test does not mean it works. Use the credits in the official WorkBuddy app; check-in still works.')) : null,
493
495
  s.enabled ? h(Usage, { usage: s.usage, subscribeUrl: s.subscribeUrl }) : null,
494
496
  !s.loggedIn && s.pricing ? h('div', { style: { ...S.meta, paddingLeft: 46, marginTop: 4 } },
495
497
  L(s.pricing.zh, s.pricing.en),
@@ -1087,9 +1089,15 @@ window.__ModuleLoader__.load({
1087
1089
  /** 刚装好、接入的来源少于两个时显示:先接一个免费来源 → 模型已经挑好 → 去 dsh 里用 */
1088
1090
  function Guide({ sources, onConnect }) {
1089
1091
  const [done, setDone] = useState(readGuideDone)
1090
- const connected = sources.filter((s) => s.loggedIn && s.enabled)
1091
- if (done || connected.length >= 2) return null
1092
- const free = sources.filter((s) => !s.risky && !(s.loggedIn && s.enabled) && s.pricing && s.pricing.tier !== 'paid')
1092
+ // WorkBuddy 只签到(它拦 dsh 的对话),不算"已接入"
1093
+ const connected = sources.filter((s) => s.loggedIn && s.enabled && s.kind !== 'workbuddy')
1094
+ if (done || connected.length >= 1) return null
1095
+ // 只推两家:国内直连的硅基流动、能上外网的 Gemini
1096
+ const PICKS = [
1097
+ { id: 'siliconflow', why: () => L('国内直连,不用代理;实名认证后部分模型免费', 'Reachable from China without a proxy; some models are free after identity verification') },
1098
+ { id: 'gemini', why: () => L('能上外网就选它:免费模型里质量最好,每天有次数上限', 'If you can reach Google: the best free models, with a daily limit') },
1099
+ ]
1100
+ const picks = PICKS.map((p) => ({ ...p, s: sources.find((s) => s.id === p.id) })).filter((p) => p.s)
1093
1101
  const hide = () => {
1094
1102
  setDone(true)
1095
1103
  try { window.localStorage.setItem(GUIDE_KEY, '1') } catch { /* 无痕模式:只在本次生效 */ }
@@ -1104,9 +1112,11 @@ window.__ModuleLoader__.load({
1104
1112
  h('button', { type: 'button', style: S.btn, onClick: hide }, L('我知道了,不再显示', 'Got it, hide this'))),
1105
1113
  step(1, L('接入一个免费来源', 'Connect a free source'),
1106
1114
  h('div', null,
1107
- h('div', { style: S.meta }, L('先接一个就能用。标「免费·额度少」的要先去对方网站拿一把免费 key。', 'One is enough to start. "Free · limited" ones need a free key from their website first.')),
1108
- h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 6, marginTop: 6 } },
1109
- free.slice(0, 8).map((s) => h('button', { key: s.id, type: 'button', style: S.btn, onClick: () => onConnect(s) }, `${s.label}${s.pricing.tier === 'free' ? L(' · 免费', ' · free') : ''}`))))),
1115
+ h('div', { style: S.meta }, L('接一个就能用:去对方网站领一把免费 key,点下面的按钮粘贴进来。', 'One is enough: get a free key from their website, then click below and paste it.')),
1116
+ picks.map((p) => h('div', { key: p.id, style: { ...S.line, marginTop: 6, flexWrap: 'wrap' } },
1117
+ h('button', { type: 'button', style: S.btn, onClick: () => onConnect(p.s) }, p.s.label),
1118
+ h('span', { style: { ...S.meta, ...S.grow } }, p.why()),
1119
+ p.s.subscribeUrl ? h('a', { href: p.s.subscribeUrl, target: '_blank', rel: 'noopener noreferrer', style: { color: C.accent, fontSize: 12 } }, L('去领 key ↗', 'Get a key ↗')) : null)))),
1110
1120
  step(2, L('模型已经替你挑好', 'Models are picked for you'),
1111
1121
  h('div', { style: S.meta }, L('每个来源默认挑 5 个放进 dsh(免费的优先)。想换就在下面「来源」里点「选模型」。', 'Each source puts 5 suggested models into dsh (free ones first). Change them with "Models" under Sources below.'))),
1112
1122
  step(3, L('去用', 'Use it'),