dsh-model 0.21.0 → 0.21.2
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/dist/daemon/core.js +5 -2
- package/dist/daemon/stats.js +8 -0
- package/dist/service/launchd.js +18 -3
- package/package.json +1 -1
- package/plugin/client.js +43 -14
package/dist/daemon/core.js
CHANGED
|
@@ -1041,8 +1041,11 @@ export class Daemon {
|
|
|
1041
1041
|
await this.removeQoderPat();
|
|
1042
1042
|
return ok(await this.checkinStatus());
|
|
1043
1043
|
}
|
|
1044
|
-
if (method === 'GET' && path === '/failures')
|
|
1045
|
-
|
|
1044
|
+
if (method === 'GET' && path === '/failures') {
|
|
1045
|
+
// 失败之后同一个模型又成功调用过(例如限流过一会儿就好了):已经自己恢复,不再显示
|
|
1046
|
+
const list = await recentFailures(this.ctx.paths.logs);
|
|
1047
|
+
return ok(list.filter((f) => !f.model || !((this.stats.lastSuccessAt(f.model) ?? 0) > Date.parse(f.at))));
|
|
1048
|
+
}
|
|
1046
1049
|
if (method === 'POST' && path === '/models/test')
|
|
1047
1050
|
return ok(await this.testModel(String(b.model ?? '')));
|
|
1048
1051
|
if (method === 'POST' && path === '/self/update')
|
package/dist/daemon/stats.js
CHANGED
|
@@ -66,6 +66,14 @@ export class Stats {
|
|
|
66
66
|
this.recs = (saved?.recs ?? []).filter((r) => Date.now() - r.t < DAY_MS);
|
|
67
67
|
}
|
|
68
68
|
/** 收进一批用量记录;keyName 把客户端 key 映射成名字(未知的记为 unknown) */
|
|
69
|
+
/** 这个模型最近一次成功调用的时间(毫秒);24 小时内没成功过返回 undefined */
|
|
70
|
+
lastSuccessAt(model) {
|
|
71
|
+
let last;
|
|
72
|
+
for (const r of this.recs)
|
|
73
|
+
if (r.m === model && r.f === 0 && (last === undefined || r.t > last))
|
|
74
|
+
last = r.t;
|
|
75
|
+
return last;
|
|
76
|
+
}
|
|
69
77
|
add(batch, keyName) {
|
|
70
78
|
for (const r of batch) {
|
|
71
79
|
const t = Date.parse(r.timestamp) || Date.now();
|
package/dist/service/launchd.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// service/launchd.ts — macOS 用户级 LaunchAgent(RunAtLoad + KeepAlive)
|
|
2
2
|
import { rm } from 'node:fs/promises';
|
|
3
|
+
import { info } from '../util/output.js';
|
|
3
4
|
import { DshModelError } from '../errors.js';
|
|
4
5
|
import { L } from '../i18n.js';
|
|
5
6
|
import { run } from '../util/exec.js';
|
|
@@ -41,11 +42,25 @@ ${envBlock} <key>WorkingDirectory</key>
|
|
|
41
42
|
export function launchd(spec) {
|
|
42
43
|
const domain = `gui/${process.getuid?.() ?? 501}`;
|
|
43
44
|
const target = `${domain}/${spec.label}`;
|
|
45
|
+
/**
|
|
46
|
+
* 被关掉的后台服务(系统设置「登录项 → 允许在后台」被关,或清理软件关的)重新允许:
|
|
47
|
+
* 只动 dsh-model 自己的两个服务,用户点「修复」/ 执行 setup 就是要它们跑起来(2026-10-14 实测:被关后 bootstrap 报 5: Input/output error)。
|
|
48
|
+
*/
|
|
49
|
+
const reenable = async () => {
|
|
50
|
+
const r = await run('launchctl', ['print-disabled', domain]);
|
|
51
|
+
if (!new RegExp(`"${spec.label.replace(/\./g, '\\.')}" => (disabled|true)`).test(r.stdout))
|
|
52
|
+
return false;
|
|
53
|
+
await run('launchctl', ['enable', target]);
|
|
54
|
+
info(L(`${spec.label} 之前被 macOS 关掉了后台运行,已重新允许`, `${spec.label} had background running turned off in macOS; allowed it again`));
|
|
55
|
+
return true;
|
|
56
|
+
};
|
|
57
|
+
const loaded = async () => (await run('launchctl', ['print', target])).code === 0;
|
|
44
58
|
const bootstrap = async () => {
|
|
59
|
+
await reenable();
|
|
45
60
|
const r = await run('launchctl', ['bootstrap', domain, spec.file]);
|
|
46
|
-
//
|
|
47
|
-
if (r.code !== 0 &&
|
|
48
|
-
throw new DshModelError('service_failed', L(
|
|
61
|
+
// 已经加载过会报错(5 / 37 等):不看错误码,直接看它到底加载上没有
|
|
62
|
+
if (r.code !== 0 && !(await loaded())) {
|
|
63
|
+
throw new DshModelError('service_failed', L(`后台服务 ${spec.label} 没能加载:${r.stderr.trim()}`, `Could not load background service ${spec.label}: ${r.stderr.trim()}`), L('看一下「系统设置 → 通用 → 登录项与扩展 → 允许在后台」里它是否被关掉了', 'Check System Settings → General → Login Items & Extensions → Allow in the Background'));
|
|
49
64
|
}
|
|
50
65
|
};
|
|
51
66
|
const bootout = async () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-model",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.2",
|
|
4
4
|
"description": "Many model sources behind one authenticated OpenAI-compatible endpoint for DeepSeek Harness (dsh) and your tools: WorkBuddy, OpenCode Zen and your CLI subscriptions (Codex, Kimi…), with a management page inside dsh. 多家模型汇成一个 OpenAI 兼容地址,dsh 和其他软件都能直接用。",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh-plugin",
|
package/plugin/client.js
CHANGED
|
@@ -540,23 +540,52 @@ window.__ModuleLoader__.load({
|
|
|
540
540
|
h('button', { type: 'button', style: S.btnPrimary, disabled: !name.trim(), onClick: add }, L('新增 key', 'Add key'))))
|
|
541
541
|
}
|
|
542
542
|
|
|
543
|
-
/**
|
|
543
|
+
/**
|
|
544
|
+
* 最近失败的请求:平时一行"最近 24 小时失败 N 次(多是…)▸",点开看明细;「知道了」之后这一批不再显示,有新的失败才重新出现。
|
|
545
|
+
* 已经自己恢复的(同一个模型之后又成功过)守护进程那边就去掉了。
|
|
546
|
+
*/
|
|
547
|
+
const FAIL_ACK = 'dsh-model.failuresSeenAt'
|
|
548
|
+
const readAck = () => {
|
|
549
|
+
try { return Number(window.localStorage.getItem(FAIL_ACK)) || 0 } catch { return 0 }
|
|
550
|
+
}
|
|
551
|
+
const KIND_LABEL = {
|
|
552
|
+
rate: () => L('限流', 'rate limits'),
|
|
553
|
+
quota: () => L('额度或套餐不足', 'quota or plan'),
|
|
554
|
+
auth: () => L('登录或 key 失效', 'sign-in or key'),
|
|
555
|
+
model: () => L('模型不存在', 'missing models'),
|
|
556
|
+
upstream: () => L('上游或网络问题', 'upstream or network'),
|
|
557
|
+
other: () => L('其他原因', 'other causes'),
|
|
558
|
+
}
|
|
544
559
|
function FailuresCard({ failures }) {
|
|
545
|
-
const [
|
|
546
|
-
|
|
560
|
+
const [open, setOpen] = useState(false)
|
|
561
|
+
const [ack, setAck] = useState(readAck)
|
|
562
|
+
const fresh = (failures || []).filter((f) => Date.parse(f.at) > ack)
|
|
563
|
+
if (!fresh.length) return null
|
|
564
|
+
const counts = {}
|
|
565
|
+
for (const f of fresh) counts[f.kind] = (counts[f.kind] || 0) + 1
|
|
566
|
+
const top = Object.keys(counts).sort((a, b) => counts[b] - counts[a])[0]
|
|
547
567
|
const color = (k) => (k === 'rate' || k === 'upstream' ? C.warn : C.err)
|
|
548
|
-
const
|
|
568
|
+
const dismiss = () => {
|
|
569
|
+
const latest = Math.max(...fresh.map((f) => Date.parse(f.at)))
|
|
570
|
+
setAck(latest)
|
|
571
|
+
setOpen(false)
|
|
572
|
+
try { window.localStorage.setItem(FAIL_ACK, String(latest)) } catch { /* 无痕模式:只在本次生效 */ }
|
|
573
|
+
}
|
|
549
574
|
return h('div', { style: S.section },
|
|
550
|
-
h('div', { style: S.
|
|
551
|
-
h('p', { style: S.note }, L('经统一端点失败的请求。只记录模型和报错,不记录对话内容。', 'Requests that failed through the endpoint. Only the model and the error are kept, never the conversation.')),
|
|
552
|
-
h('div', { style: S.card }, shown.map((f, i) => h('div', { key: `${f.at}-${i}`, style: i ? S.row : S.rowFirst },
|
|
575
|
+
h('div', { style: { ...S.card, padding: '10px 14px' } },
|
|
553
576
|
h('div', { style: S.line },
|
|
554
|
-
h('
|
|
555
|
-
|
|
556
|
-
h('
|
|
557
|
-
h('div', { style: {
|
|
558
|
-
|
|
559
|
-
|
|
577
|
+
h('button', { type: 'button', 'aria-expanded': open, onClick: () => setOpen(!open), style: { ...S.grow, textAlign: 'left', background: 'none', border: 'none', padding: 0, cursor: 'pointer', fontSize: 13, color: color(top) } },
|
|
578
|
+
`${open ? '▾' : '▸'} ${L(`最近 24 小时失败 ${fresh.length} 次(多是${(KIND_LABEL[top] || KIND_LABEL.other)()})`, `${fresh.length} failed request${fresh.length > 1 ? 's' : ''} in 24 hours (mostly ${(KIND_LABEL[top] || KIND_LABEL.other)()})`)}`),
|
|
579
|
+
h('button', { type: 'button', style: S.btn, onClick: dismiss }, L('知道了', 'Got it'))),
|
|
580
|
+
open ? h('div', { style: { marginTop: 8 } },
|
|
581
|
+
h('div', { style: { ...S.meta, marginBottom: 4 } }, L('经统一端点失败、之后也没再成功的请求。只记录模型和报错,不记录对话内容。', 'Requests that failed through the endpoint and have not succeeded since. Only the model and the error are kept, never the conversation.')),
|
|
582
|
+
fresh.slice(0, 20).map((f, i) => h('div', { key: `${f.at}-${i}`, style: { borderTop: i ? `1px solid ${C.border}` : 'none', padding: '6px 0' } },
|
|
583
|
+
h('div', { style: S.line },
|
|
584
|
+
h('span', { style: { ...S.mono, ...S.grow, fontSize: 12, color: C.text } }, f.model || L('(未知模型)', '(unknown model)')),
|
|
585
|
+
h('span', { style: { fontSize: 12, color: color(f.kind) } }, `HTTP ${f.status}`),
|
|
586
|
+
h('span', { style: S.meta }, ago(f.at))),
|
|
587
|
+
h('div', { style: { fontSize: 12, color: color(f.kind) } }, f.advice),
|
|
588
|
+
f.message ? h('div', { style: { ...S.meta, ...S.mono, wordBreak: 'break-all' } }, f.message) : null))) : null))
|
|
560
589
|
}
|
|
561
590
|
|
|
562
591
|
function ModelsCard({ stats }) {
|
|
@@ -1031,9 +1060,9 @@ window.__ModuleLoader__.load({
|
|
|
1031
1060
|
err ? h('div', { style: { fontSize: 12, color: C.err, marginBottom: 12 } }, err) : null,
|
|
1032
1061
|
h(EndpointCard, { endpoints: data.endpoints }),
|
|
1033
1062
|
h(SourcesCard, { sources: data.sources, reload: () => load(false), onLogin: (s, session) => setLogin({ source: s, session }), onKey: (s) => setKeyDialog(s || true) }),
|
|
1034
|
-
h(FailuresCard, { failures: data.failures }),
|
|
1035
1063
|
h(KeysCard, { keys: data.keys, reload: () => load(false) }),
|
|
1036
1064
|
h(ModelsCard, { stats: data.stats }),
|
|
1065
|
+
h(FailuresCard, { failures: data.failures }),
|
|
1037
1066
|
h(AboutCard, { onJob: setSelfJob }),
|
|
1038
1067
|
login ? h(LoginDialog, { source: login.source, session: login.session, onDone: () => { setLogin(null); void load(true) }, onClose: () => { setLogin(null); void load(false) } }) : null,
|
|
1039
1068
|
keyDialog ? h(KeyDialog, { source: keyDialog === true ? null : keyDialog, onDone: () => { setKeyDialog(false); void load(true) }, onClose: () => setKeyDialog(false) }) : null)
|