dsh-model 0.25.0 → 0.26.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 +7 -1
- package/README.md +7 -1
- package/dist/checkin/transfer.js +65 -0
- package/dist/daemon/core.js +15 -1
- package/dist/daemon/usage.js +1 -1
- package/dist/engine/compat.js +3 -1
- package/dist/integrations/apikeys.js +55 -6
- package/dist/sources.js +7 -0
- package/package.json +1 -1
- package/plugin/client.js +58 -2
- package/plugin/index.js +1 -1
package/README.en.md
CHANGED
|
@@ -32,6 +32,8 @@ English · [中文](README.md)
|
|
|
32
32
|
|
|
33
33
|
- **Automatic by default**: done at a random 5–55 minutes after midnight Beijing time; failures retry an hour later; if the computer was off, it catches up within a minute of starting.
|
|
34
34
|
- **You can see it**: one line at the top of the settings page, "Checked in today: …", with a one-click "Check in all" for anything missed, plus a one-time notice in the corner of dsh.
|
|
35
|
+
- **What else this machine can do**: the card lists the supported products not set up here yet, each with "+ Add".
|
|
36
|
+
- **Move it to an always-on VPS**: "Copy check-in accounts to another machine" gives an encrypted text and a code to import on the VPS page (Qoder and Doubao; sign in to WorkBuddy on the VPS itself, and Trae stays on the computer that has Trae).
|
|
35
37
|
- **Several accounts**: each product can have more than one. In the check-in card's Details: "+ Add a WorkBuddy account" (approve through a link) and "+ Add another Qoder account"; several Trae installs are checked in once per account automatically.
|
|
36
38
|
- **Check-in only, credits untouched**: they stay on your account for use in each product's own app. Command line: `dsh-model checkin`.
|
|
37
39
|
|
|
@@ -51,7 +53,7 @@ Your model sources each have their own API and sign-in. dsh-model puts them behi
|
|
|
51
53
|
| Kind | Sources |
|
|
52
54
|
|---|---|
|
|
53
55
|
| **Free (suggested)** | **SiliconFlow** (reachable from China; some models free after identity verification), **Gemini** (if you can reach Google; the best free models) |
|
|
54
|
-
| Free · limited | Groq, OpenRouter (`:free` models) |
|
|
56
|
+
| Free · limited | **Zhipu GLM** (the Flash models), **iFlytek Spark** (Spark Lite), Baidu Qianfan (ERNIE Speed / Lite), Groq, OpenRouter (`:free` models) |
|
|
55
57
|
| Local | Ollama, LM Studio: appear once started, no sign-in |
|
|
56
58
|
| Subscription | Codex (ChatGPT Plus / Pro), Kimi Code, Grok, Muse, Devin: sign in from the management page |
|
|
57
59
|
| Pay as you go | DeepSeek official, OpenCode Zen, any OpenAI-compatible service |
|
|
@@ -112,6 +114,10 @@ If a source is signed in but has no usable plan (say, a Kimi account without Kim
|
|
|
112
114
|
| Gemini (Google AI Studio), Groq | Free · limited | Paste a free API key; daily / per-minute limits apply |
|
|
113
115
|
| OpenRouter | Free · limited | `:free` models are free (about 50 requests a day) |
|
|
114
116
|
| SiliconFlow | Free · limited | Some small models are free after identity verification |
|
|
117
|
+
| Zhipu GLM | Free · limited | The Flash models (e.g. GLM-4.7-Flash) are free; reachable from China |
|
|
118
|
+
| iFlytek Spark | Free · limited | Spark Lite is free with no token cap, 2 requests per second; use the console's APIPassword as the key |
|
|
119
|
+
| Baidu Qianfan | Free · limited | ERNIE Speed / Lite are free for now (rate limited) |
|
|
120
|
+
| Volcengine Ark (Doubao models), MiniMax, Huawei Cloud ModelArts | Paid | Official APIs, just add a key; Ark and Huawei Cloud have free quotas for new users |
|
|
115
121
|
| DeepSeek official, any OpenAI-compatible service | Paid | Paste an API key (and a URL for other services) |
|
|
116
122
|
| Codex | Paid | ChatGPT Plus / Pro |
|
|
117
123
|
| Kimi | Paid | Kimi Code plan |
|
package/README.md
CHANGED
|
@@ -32,6 +32,8 @@
|
|
|
32
32
|
|
|
33
33
|
- **自动签到默认打开**:北京时间零点后随机延后 5–55 分钟签好;失败的一小时后重试;电脑关机错过了,开机一分钟内补签。
|
|
34
34
|
- **结果看得见**:设置页顶部一行「今日已完成签到:…」,没签上的可以「一键签到」;签完在 dsh 右下角提示一次。
|
|
35
|
+
- **这台还能签哪些**:卡片里列出支持、但这台还没设置的,旁边就是「+ 添加」。
|
|
36
|
+
- **放到 VPS 上签,不会漏**:「复制签到账号到另一台机器」导出一段加密文本和提取码,到 VPS 的页面里「导入」即可(Qoder、豆包;WorkBuddy 在 VPS 上各自登录,Trae 留在装了 Trae 的电脑上签)。
|
|
35
37
|
- **多账号**:一家可以签几个账号。在签到卡片的「详情」里「+ 添加 WorkBuddy 账号」(打开链接授权)、「+ 再加一个 Qoder 账号」;本机装了几个 Trae 会自动按账号各签一次。
|
|
36
38
|
- **只签到,不碰积分**:积分留在你的账号里,照常在各家官方软件里用。命令行:`dsh-model checkin`。
|
|
37
39
|
|
|
@@ -51,7 +53,7 @@
|
|
|
51
53
|
| 类型 | 来源 |
|
|
52
54
|
|---|---|
|
|
53
55
|
| **免费(推荐)** | **硅基流动**(国内直连,实名后部分模型免费)、**Gemini**(能上外网,免费模型里质量最好) |
|
|
54
|
-
| 免费 · 额度少 | Groq、OpenRouter(`:free` 模型) |
|
|
56
|
+
| 免费 · 额度少 | **智谱 GLM**(Flash 系列免费)、**讯飞星火**(Spark Lite 免费)、百度千帆(ERNIE Speed / Lite)、Groq、OpenRouter(`:free` 模型) |
|
|
55
57
|
| 本机 | Ollama、LM Studio:启动后自动出现,不用登录 |
|
|
56
58
|
| 订阅 | Codex(ChatGPT Plus / Pro)、Kimi Code、Grok、Muse、Devin:在管理页里登录即可 |
|
|
57
59
|
| 按量付费 | DeepSeek 官方、OpenCode Zen、任意 OpenAI 兼容服务 |
|
|
@@ -112,6 +114,10 @@ dsh-model setup
|
|
|
112
114
|
| Gemini(Google AI Studio)、Groq | 免费·额度少 | 填免费 API key 即可,每天 / 每分钟有上限 |
|
|
113
115
|
| OpenRouter | 免费·额度少 | 名字带 `:free` 的模型免费(每天约 50 次) |
|
|
114
116
|
| 硅基流动 | 免费·额度少 | 实名认证后部分小模型免费 |
|
|
117
|
+
| 智谱 GLM | 免费·额度少 | Flash 系列(如 GLM-4.7-Flash)免费,国内直连 |
|
|
118
|
+
| 讯飞星火 | 免费·额度少 | Spark Lite 免费不限量,每秒 2 次;key 填控制台的 APIPassword |
|
|
119
|
+
| 百度千帆 | 免费·额度少 | ERNIE Speed / Lite 长期免费(限速) |
|
|
120
|
+
| 火山方舟(豆包大模型)、MiniMax 开放平台、华为云 ModelArts | 需付费 | 官方 API,填 key 即可;火山方舟、华为云有新用户免费额度 |
|
|
115
121
|
| DeepSeek 官方、其他 OpenAI 兼容服务 | 需付费 | 填 API key(其他服务还要填地址) |
|
|
116
122
|
| Codex | 需付费 | ChatGPT Plus / Pro |
|
|
117
123
|
| Kimi | 需付费 | Kimi Code 套餐 |
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// checkin/transfer.ts — 把签到账号复制到另一台机器(设计 §14.20):比如 Mac 上加好的,搬到不关机的 VPS 上签
|
|
2
|
+
//
|
|
3
|
+
// 只搬能在任何机器上用的:Qoder 令牌、豆包 Cookie。
|
|
4
|
+
// WorkBuddy 不搬:登录令牌会续期轮换,两台用同一份会互相顶掉,所以每台各自登录。
|
|
5
|
+
// Trae 不搬:读的是本机 Trae 软件的登录,要在装了 Trae 的电脑上签。
|
|
6
|
+
// 导出 = 一段加密文本 + 一个 8 位提取码(分开显示,光有文本解不开)。AES-256-GCM,密钥由提取码经 scrypt 派生。
|
|
7
|
+
import { createCipheriv, createDecipheriv, randomBytes, randomInt, scryptSync } from 'node:crypto';
|
|
8
|
+
import { DshModelError } from '../errors.js';
|
|
9
|
+
import { L } from '../i18n.js';
|
|
10
|
+
import { addDoubao, addQoder, loadDoubaos, loadQoders } from './accounts.js';
|
|
11
|
+
const PREFIX = 'dshm-checkin:v1:';
|
|
12
|
+
/** 去掉容易看错的 0/O、1/I/L */
|
|
13
|
+
const ALPHABET = 'ABCDEFGHJKMNPQRSTUVWXYZ23456789';
|
|
14
|
+
const keyFrom = (code, salt) => scryptSync(code.replace(/[\s-]/g, '').toUpperCase(), salt, 32, { N: 1 << 15, r: 8, p: 1, maxmem: 64 * 1024 * 1024 });
|
|
15
|
+
export function sealBundle(bundle, code) {
|
|
16
|
+
const c = code ?? Array.from({ length: 8 }, () => ALPHABET[randomInt(ALPHABET.length)]).join('');
|
|
17
|
+
const salt = randomBytes(16);
|
|
18
|
+
const iv = randomBytes(12);
|
|
19
|
+
const cipher = createCipheriv('aes-256-gcm', keyFrom(c, salt), iv);
|
|
20
|
+
const body = Buffer.concat([cipher.update(JSON.stringify(bundle), 'utf8'), cipher.final()]);
|
|
21
|
+
const text = PREFIX + Buffer.concat([salt, iv, cipher.getAuthTag(), body]).toString('base64url');
|
|
22
|
+
return { text, code: `${c.slice(0, 4)}-${c.slice(4)}` };
|
|
23
|
+
}
|
|
24
|
+
export function openBundle(text, code) {
|
|
25
|
+
const t = text.trim();
|
|
26
|
+
if (!t.startsWith(PREFIX))
|
|
27
|
+
throw new DshModelError('bad_transfer', L('这不是 dsh-model 导出的签到账号(应以 dshm-checkin: 开头)', 'This is not a dsh-model check-in export (it should start with dshm-checkin:)'));
|
|
28
|
+
const raw = Buffer.from(t.slice(PREFIX.length), 'base64url');
|
|
29
|
+
if (raw.length < 16 + 12 + 16 + 2)
|
|
30
|
+
throw new DshModelError('bad_transfer', L('导出的文本不完整:复制时可能少了一截', 'The export text is incomplete: part of it may be missing'));
|
|
31
|
+
const salt = raw.subarray(0, 16);
|
|
32
|
+
const iv = raw.subarray(16, 28);
|
|
33
|
+
const tag = raw.subarray(28, 44);
|
|
34
|
+
try {
|
|
35
|
+
const d = createDecipheriv('aes-256-gcm', keyFrom(code, salt), iv);
|
|
36
|
+
d.setAuthTag(tag);
|
|
37
|
+
const json = Buffer.concat([d.update(raw.subarray(44)), d.final()]).toString('utf8');
|
|
38
|
+
const b = JSON.parse(json);
|
|
39
|
+
return { qoders: Array.isArray(b.qoders) ? b.qoders : [], doubaos: Array.isArray(b.doubaos) ? b.doubaos : [] };
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
throw new DshModelError('bad_transfer_code', L('提取码不对,或者文本被改动过', 'Wrong code, or the text was changed'));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** 导出这台机器上能搬的签到账号 */
|
|
46
|
+
export async function exportCheckin(ctx) {
|
|
47
|
+
const qoders = await loadQoders(ctx);
|
|
48
|
+
const doubaos = await loadDoubaos(ctx);
|
|
49
|
+
if (!qoders.length && !doubaos.length)
|
|
50
|
+
throw new DshModelError('nothing_to_export', L('这台机器上没有能复制的签到账号(只能复制 Qoder 和豆包;WorkBuddy 请在那台机器上各自登录,Trae 要在装了 Trae 的电脑上签)', 'Nothing to copy from this machine (only Qoder and Doubao can be copied; sign in to WorkBuddy on each machine, and Trae needs a computer with Trae installed)'));
|
|
51
|
+
const sealed = sealBundle({
|
|
52
|
+
qoders: qoders.map((q) => ({ pat: q.pat, region: q.region, ...(q.name ? { name: q.name } : {}) })),
|
|
53
|
+
doubaos: doubaos.map((d) => ({ cookie: d.cookie, seed: d.seed, ...(d.name ? { name: d.name } : {}) })),
|
|
54
|
+
});
|
|
55
|
+
return { ...sealed, counts: { qoder: qoders.length, doubao: doubaos.length } };
|
|
56
|
+
}
|
|
57
|
+
/** 导入:已有的同一个令牌 / Cookie 不会重复加 */
|
|
58
|
+
export async function importCheckin(ctx, text, code) {
|
|
59
|
+
const b = openBundle(text, code);
|
|
60
|
+
for (const q of b.qoders)
|
|
61
|
+
await addQoder(ctx, q);
|
|
62
|
+
for (const d of b.doubaos)
|
|
63
|
+
await addDoubao(ctx, d);
|
|
64
|
+
return { qoder: b.qoders.length, doubao: b.doubaos.length };
|
|
65
|
+
}
|
package/dist/daemon/core.js
CHANGED
|
@@ -20,7 +20,8 @@ import { autoDue, beijingNow, checkinAdvice, ensureJitter, loadCheckin, runCheck
|
|
|
20
20
|
import { QODER_REGIONS, exchangePat, qoderCheckin } from '../checkin/qoder.js';
|
|
21
21
|
import { findTraeCredentials, traeCheckin } from '../checkin/trae.js';
|
|
22
22
|
import { DoubaoAuthError, doubaoCheckin, doubaoStatus, normalizeCookie } from '../checkin/doubao.js';
|
|
23
|
-
import {
|
|
23
|
+
import { exportCheckin, importCheckin } from '../checkin/transfer.js';
|
|
24
|
+
import { accountTag, addDoubao, checkinKind, addQoder, listWorkbuddyAccounts, loadDoubaos, loadQoders, removeDoubao, removeQoder, removeWorkbuddyAccount, workbuddyAccountStore, workbuddyAccountsDir } from '../checkin/accounts.js';
|
|
24
25
|
import { CN_VARIANT } from '../bridge/workbuddy/variants.js';
|
|
25
26
|
import { PKG_ROOT, pkgVersion } from '../util/pkg.js';
|
|
26
27
|
import { randomBytes } from 'node:crypto';
|
|
@@ -792,6 +793,13 @@ export class Daemon {
|
|
|
792
793
|
qoder: qoders.map((q, i) => ({ id: q.id, label: checkers.find((c) => c.id === (i === 0 ? 'qoder' : `qoder:${q.id}`))?.label ?? q.id, region: q.region })),
|
|
793
794
|
doubao: (await loadDoubaos(this.ctx)).map((d, i) => ({ id: d.id, label: checkers.find((c) => c.id === (i === 0 ? 'doubao' : `doubao:${d.id}`))?.label ?? d.id })),
|
|
794
795
|
},
|
|
796
|
+
// 支持、但这台还没设置的(签到卡片里列出来,旁边就是「添加」)
|
|
797
|
+
missing: [
|
|
798
|
+
...(checkers.some((c) => checkinKind(c.id) === 'workbuddy') ? [] : [{ id: 'workbuddy', label: L('WorkBuddy 国内版', 'WorkBuddy (China)'), action: 'workbuddy' }]),
|
|
799
|
+
...(checkers.some((c) => checkinKind(c.id) === 'qoder') ? [] : [{ id: 'qoder', label: 'Qoder', action: 'qoder' }]),
|
|
800
|
+
...(checkers.some((c) => checkinKind(c.id) === 'doubao') ? [] : [{ id: 'doubao', label: L('豆包', 'Doubao'), action: 'doubao' }]),
|
|
801
|
+
...(checkers.some((c) => checkinKind(c.id) === 'trae') ? [] : [{ id: 'trae', label: 'Trae', reason: L('这台没装 Trae:要在装了 Trae 的电脑上签', 'Trae is not installed here: it is checked in on a computer with Trae') }]),
|
|
802
|
+
],
|
|
795
803
|
};
|
|
796
804
|
}
|
|
797
805
|
checkinInflight;
|
|
@@ -1219,6 +1227,12 @@ export class Daemon {
|
|
|
1219
1227
|
}
|
|
1220
1228
|
if (method === 'POST' && path === '/checkin/workbuddy')
|
|
1221
1229
|
return ok({ login: await this.startWorkbuddyCheckinLogin() });
|
|
1230
|
+
if (method === 'POST' && path === '/checkin/export')
|
|
1231
|
+
return ok(await exportCheckin(this.ctx));
|
|
1232
|
+
if (method === 'POST' && path === '/checkin/import') {
|
|
1233
|
+
const n = await importCheckin(this.ctx, String(b.text ?? ''), String(b.code ?? ''));
|
|
1234
|
+
return ok({ imported: n, ...(await this.checkinNow(true)) });
|
|
1235
|
+
}
|
|
1222
1236
|
if (method === 'POST' && path === '/checkin/doubao') {
|
|
1223
1237
|
await this.addDoubaoCookie(String(b.cookie ?? ''), typeof b.name === 'string' ? b.name : undefined);
|
|
1224
1238
|
return ok(await this.checkinNow(true));
|
package/dist/daemon/usage.js
CHANGED
|
@@ -13,7 +13,7 @@ import { join } from 'node:path';
|
|
|
13
13
|
import { beijingTimeToIso } from '../bridge/workbuddy/upstream.js';
|
|
14
14
|
const TIMEOUT_MS = 15_000;
|
|
15
15
|
/** 这几家暂不支持(见设计 §14.6) */
|
|
16
|
-
export const USAGE_UNSUPPORTED = new Set(['antigravity', 'devin', 'meta', 'opencode', 'ollama', 'lmstudio', 'gemini', 'groq', 'openrouter', 'siliconflow', 'deepseek', 'custom']);
|
|
16
|
+
export const USAGE_UNSUPPORTED = new Set(['antigravity', 'devin', 'meta', 'opencode', 'ollama', 'lmstudio', 'gemini', 'groq', 'openrouter', 'siliconflow', 'zhipu', 'spark', 'qianfan', 'ark', 'minimax', 'modelarts', 'deepseek', 'custom']);
|
|
17
17
|
async function getJson(url, headers) {
|
|
18
18
|
const res = await fetch(url, { headers: { Accept: 'application/json', ...headers }, signal: AbortSignal.timeout(TIMEOUT_MS) });
|
|
19
19
|
const text = await res.text();
|
package/dist/engine/compat.js
CHANGED
|
@@ -71,7 +71,7 @@ export async function loadCompatUpstreams(ctx) {
|
|
|
71
71
|
});
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
|
-
// 自带 API key 的来源:有 key
|
|
74
|
+
// 自带 API key 的来源:有 key、有模型、没被关掉就接进来(海外的走出站代理,国内平台直连)
|
|
75
75
|
const keyCat = await loadKeyCatalog(ctx.paths.home);
|
|
76
76
|
for (const [id, s] of Object.entries(secrets.apikeys ?? {})) {
|
|
77
77
|
const p = keyProvider(id);
|
|
@@ -83,6 +83,8 @@ export async function loadCompatUpstreams(ctx) {
|
|
|
83
83
|
label: p.label,
|
|
84
84
|
baseUrl: s.base ?? p.base,
|
|
85
85
|
apiKey: s.key,
|
|
86
|
+
// 国内平台直连,不绕出站代理
|
|
87
|
+
...(p.direct ? { direct: true } : {}),
|
|
86
88
|
models: models.map((m) => ({ name: m.id, alias: `${id}/${m.id}`, displayName: `${p.label} · ${m.id}`, dshName: m.free ? `${m.id} · Free` : m.id, ...(m.free ? { rate: 0 } : {}), group: id, groupLabel: p.label })),
|
|
87
89
|
});
|
|
88
90
|
}
|
|
@@ -13,6 +13,13 @@ export const KEY_PROVIDERS = [
|
|
|
13
13
|
{ id: 'groq', label: 'Groq', base: 'https://api.groq.com/openai/v1', keyUrl: 'https://console.groq.com/keys' },
|
|
14
14
|
{ id: 'openrouter', label: 'OpenRouter', base: 'https://openrouter.ai/api/v1', keyUrl: 'https://openrouter.ai/keys' },
|
|
15
15
|
{ id: 'siliconflow', label: '硅基流动 SiliconFlow', base: 'https://api.siliconflow.cn/v1', keyUrl: 'https://cloud.siliconflow.cn/account/ak', listQuery: 'type=text&sub_type=chat' },
|
|
16
|
+
// 国内大厂的官方 API(2026-10-29 加):智谱、讯飞星火有长期免费的模型
|
|
17
|
+
{ id: 'zhipu', label: '智谱 GLM', base: 'https://open.bigmodel.cn/api/paas/v4', keyUrl: 'https://open.bigmodel.cn/usercenter/apikeys', fallbackModels: ['glm-4.7-flash', 'glm-4.6', 'glm-4.5-air'], freeRe: /flash/i, direct: true },
|
|
18
|
+
{ id: 'spark', label: '讯飞星火', base: 'https://spark-api-open.xf-yun.com/v1', keyUrl: 'https://console.xfyun.cn/services/cbm', fallbackModels: ['lite', 'generalv3', 'pro-128k', 'generalv3.5', 'max-32k', '4.0Ultra'], freeRe: /^lite$/i, direct: true },
|
|
19
|
+
{ id: 'qianfan', label: '百度千帆', base: 'https://qianfan.baidubce.com/v2', keyUrl: 'https://console.bce.baidu.com/iam/#/iam/apikey/list', fallbackModels: ['ernie-speed-128k', 'ernie-lite-8k', 'ernie-4.5-turbo-128k', 'deepseek-v3'], freeRe: /^ernie-(speed|lite)/i, direct: true },
|
|
20
|
+
{ id: 'ark', label: '火山方舟(豆包大模型)', base: 'https://ark.cn-beijing.volces.com/api/v3', keyUrl: 'https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey', fallbackModels: ['doubao-seed-1-6-flash-250615', 'doubao-seed-1-6-250615', 'deepseek-v3-1-250821'], direct: true },
|
|
21
|
+
{ id: 'minimax', label: 'MiniMax 开放平台', base: 'https://api.minimaxi.com/v1', keyUrl: 'https://platform.minimaxi.com/user-center/basic-information/interface-key', fallbackModels: ['MiniMax-M2', 'MiniMax-M1', 'MiniMax-Text-01'], direct: true },
|
|
22
|
+
{ id: 'modelarts', label: '华为云 ModelArts', base: 'https://api.modelarts-maas.com/v1', keyUrl: 'https://console.huaweicloud.com/modelarts/?region=cn-southwest-2#/model-studio/authmanage', fallbackModels: ['DeepSeek-V3', 'DeepSeek-R1', 'qwen3-235b-a22b'], direct: true },
|
|
16
23
|
{ id: 'deepseek', label: 'DeepSeek 官方', base: 'https://api.deepseek.com/v1', keyUrl: 'https://platform.deepseek.com/api_keys' },
|
|
17
24
|
{ id: 'custom', label: '其他 OpenAI 兼容服务', base: '', keyUrl: '' },
|
|
18
25
|
];
|
|
@@ -37,22 +44,60 @@ export function parseModelList(providerId, body) {
|
|
|
37
44
|
if (!id)
|
|
38
45
|
continue;
|
|
39
46
|
const pricing = m.pricing;
|
|
40
|
-
const free = id.endsWith(':free') || (pricing ? Number(pricing.prompt) === 0 && Number(pricing.completion) === 0 : false);
|
|
47
|
+
const free = id.endsWith(':free') || (pricing ? Number(pricing.prompt) === 0 && Number(pricing.completion) === 0 : false) || Boolean(keyProvider(providerId)?.freeRe?.test(id));
|
|
41
48
|
out.push({ id, ...(free ? { free: true } : {}) });
|
|
42
49
|
}
|
|
43
50
|
// OpenRouter 有几百个模型:付费的照样能用,但先把免费的排前面,挑默认时就先挑它们
|
|
44
51
|
return out.sort((a, b) => Number(Boolean(b.free)) - Number(Boolean(a.free)) || a.id.localeCompare(b.id));
|
|
45
52
|
}
|
|
46
53
|
/** 用这把 key 拉模型列表:拉得到就是可用的 key(不花 token) */
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const
|
|
54
|
+
/** 带 key 的一次请求(curl:认代理,key 经 stdin 传、不进进程参数) */
|
|
55
|
+
async function keyRequest(provider, key, url, proxy) {
|
|
56
|
+
const viaProxy = proxy && !provider.direct;
|
|
57
|
+
const r = await run('curl', ['-s', '-m', '25', '-w', '\n%{http_code}', '-K', '-', ...(viaProxy ? ['-x', proxy] : provider.direct ? ['--noproxy', '*'] : []), url], {
|
|
50
58
|
input: `header = "Authorization: Bearer ${key.replace(/"/g, '')}"\n`,
|
|
51
59
|
timeoutMs: 40_000,
|
|
52
60
|
});
|
|
53
61
|
const lines = r.stdout.trimEnd().split('\n');
|
|
54
|
-
|
|
55
|
-
|
|
62
|
+
return { status: Number(lines.pop()) || null, text: lines.join('\n') };
|
|
63
|
+
}
|
|
64
|
+
/** 不支持列模型的平台:发一次 1 个 token 的对话验证 key,返回内置的常用模型 */
|
|
65
|
+
async function verifyByChat(provider, key, base, proxy) {
|
|
66
|
+
const models = provider.fallbackModels ?? [];
|
|
67
|
+
const probe = models[0];
|
|
68
|
+
// curl 的 --data-binary @- 和 -K - 都读 stdin,这里把请求体写进临时文件
|
|
69
|
+
const { writeFile, rm, mkdtemp } = await import('node:fs/promises');
|
|
70
|
+
const { tmpdir } = await import('node:os');
|
|
71
|
+
const dir = await mkdtemp(join(tmpdir(), 'dsh-model-key-'));
|
|
72
|
+
const file = join(dir, 'body.json');
|
|
73
|
+
await writeFile(file, JSON.stringify({ model: probe, messages: [{ role: 'user', content: 'ok' }], max_tokens: 1, stream: false }), { mode: 0o600 });
|
|
74
|
+
try {
|
|
75
|
+
const viaProxy = proxy && !provider.direct;
|
|
76
|
+
const r = await run('curl', ['-s', '-m', '40', '-w', '\n%{http_code}', '-K', '-', ...(viaProxy ? ['-x', proxy] : provider.direct ? ['--noproxy', '*'] : []), '-X', 'POST', '--data-binary', `@${file}`, `${base}/chat/completions`], {
|
|
77
|
+
input: `header = "Authorization: Bearer ${key.replace(/"/g, '')}"\nheader = "Content-Type: application/json"\n`,
|
|
78
|
+
timeoutMs: 60_000,
|
|
79
|
+
});
|
|
80
|
+
const lines = r.stdout.trimEnd().split('\n');
|
|
81
|
+
const status = Number(lines.pop()) || null;
|
|
82
|
+
const text = lines.join('\n');
|
|
83
|
+
if (status === 401 || status === 403 || /invalid.*(api.?key|token)|apikey not found|authenticat|鉴权|令牌/i.test(status === 200 ? '' : text)) {
|
|
84
|
+
throw new DshModelError('key_rejected', L(`${provider.label} 不认这把 key(HTTP ${status ?? '-'})`, `${provider.label} rejected the key (HTTP ${status ?? '-'})`), L(`到 ${provider.keyUrl} 重新创建一把`, `Create a new one at ${provider.keyUrl}`));
|
|
85
|
+
}
|
|
86
|
+
// 200 = 能用;429 = key 对、只是限流;其余(模型没开通等)key 也是对的,照常保存
|
|
87
|
+
if (!status)
|
|
88
|
+
throw new DshModelError('key_check_failed', L(`连不上 ${base}`, `Cannot reach ${base}`));
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
await rm(dir, { recursive: true, force: true });
|
|
92
|
+
}
|
|
93
|
+
return parseModelList(provider.id, { data: models.map((id) => ({ id })) });
|
|
94
|
+
}
|
|
95
|
+
export async function fetchKeyModels(provider, key, base, proxy) {
|
|
96
|
+
const url = `${base}/models${provider.listQuery ? `?${provider.listQuery}` : ''}`;
|
|
97
|
+
const { status, text } = await keyRequest(provider, key, url, proxy);
|
|
98
|
+
// 有内置列表的平台:列模型不成功(不支持,或这个接口要额外权限)就改用一次短对话验证 key,由它来判断 key 对不对
|
|
99
|
+
if (provider.fallbackModels?.length && status !== 200)
|
|
100
|
+
return verifyByChat(provider, key, base, proxy);
|
|
56
101
|
if (status === 401 || status === 403)
|
|
57
102
|
throw new DshModelError('key_rejected', L(`${provider.label} 不认这把 key(HTTP ${status})`, `${provider.label} rejected the key (HTTP ${status})`), provider.keyUrl ? L(`到 ${provider.keyUrl} 重新创建一把`, `Create a new one at ${provider.keyUrl}`) : undefined);
|
|
58
103
|
if (status !== 200) {
|
|
@@ -63,8 +108,12 @@ export async function fetchKeyModels(provider, key, base, proxy) {
|
|
|
63
108
|
models = parseModelList(provider.id, JSON.parse(text));
|
|
64
109
|
}
|
|
65
110
|
catch {
|
|
111
|
+
if (provider.fallbackModels?.length)
|
|
112
|
+
return verifyByChat(provider, key, base, proxy);
|
|
66
113
|
throw new DshModelError('key_check_failed', L(`${provider.label} 的模型列表看不懂`, `Could not read the ${provider.label} model list`));
|
|
67
114
|
}
|
|
115
|
+
if (!models.length && provider.fallbackModels?.length)
|
|
116
|
+
return verifyByChat(provider, key, base, proxy);
|
|
68
117
|
if (!models.length)
|
|
69
118
|
throw new DshModelError('key_no_models', L(`${provider.label} 没有返回任何模型`, `${provider.label} returned no models`));
|
|
70
119
|
return models;
|
package/dist/sources.js
CHANGED
|
@@ -20,6 +20,13 @@ export const SOURCES = [
|
|
|
20
20
|
{ id: 'groq', label: 'Groq', kind: 'apikey', login: 'key', subscribeUrl: 'https://console.groq.com/keys', pricing: { tier: 'limited', zh: '免费 key,每个模型每分钟 / 每天有上限', en: 'Free key with per-model per-minute / per-day limits' } },
|
|
21
21
|
{ id: 'openrouter', label: 'OpenRouter', kind: 'apikey', login: 'key', subscribeUrl: 'https://openrouter.ai/keys', pricing: { tier: 'limited', zh: '名字带 :free 的模型免费(每天约 50 次,充值 10 美元后约 1000 次);其余按量付费', en: ':free models are free (about 50/day, about 1,000/day after a $10 top-up); others pay as you go' } },
|
|
22
22
|
{ id: 'siliconflow', label: '硅基流动 SiliconFlow', kind: 'apikey', login: 'key', subscribeUrl: 'https://cloud.siliconflow.cn/account/ak', pricing: { tier: 'limited', zh: '实名认证后部分小模型免费(限速);其余按量付费', en: 'Some small models are free after identity verification (rate limited); others pay as you go' } },
|
|
23
|
+
// 国内大厂官方 API(2026-10-29 加):免费档按各家公开说明,以官网为准
|
|
24
|
+
{ id: 'zhipu', label: '智谱 GLM', kind: 'apikey', login: 'key', subscribeUrl: 'https://open.bigmodel.cn/usercenter/apikeys', pricing: { tier: 'limited', zh: 'Flash 系列(如 GLM-4.7-Flash)免费;其余按量付费。国内直连', en: 'The Flash models (e.g. GLM-4.7-Flash) are free; others pay as you go. Reachable from China' } },
|
|
25
|
+
{ id: 'spark', label: '讯飞星火', kind: 'apikey', login: 'key', subscribeUrl: 'https://console.xfyun.cn/services/cbm', pricing: { tier: 'limited', zh: 'Spark Lite 免费不限量,但每秒只能 2 次;key 填控制台里的 APIPassword。国内直连', en: 'Spark Lite is free with no token cap but only 2 requests per second; use the APIPassword from the console. Reachable from China' } },
|
|
26
|
+
{ id: 'qianfan', label: '百度千帆', kind: 'apikey', login: 'key', subscribeUrl: 'https://console.bce.baidu.com/iam/#/iam/apikey/list', pricing: { tier: 'limited', zh: 'ERNIE Speed / Lite 长期免费(限速);其余按量付费。国内直连', en: 'ERNIE Speed / Lite are free for now (rate limited); others pay as you go. Reachable from China' } },
|
|
27
|
+
{ id: 'ark', label: '火山方舟(豆包大模型)', kind: 'apikey', login: 'key', subscribeUrl: 'https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey', pricing: { tier: 'paid', zh: '按量付费,新用户有限时免费额度;模型要先在控制台开通。国内直连', en: 'Pay as you go with a time-limited free quota for new users; enable models in the console first. Reachable from China' } },
|
|
28
|
+
{ id: 'minimax', label: 'MiniMax 开放平台', kind: 'apikey', login: 'key', subscribeUrl: 'https://platform.minimaxi.com/user-center/basic-information/interface-key', pricing: { tier: 'paid', zh: '按量付费或 Coding Plan 套餐。国内直连', en: 'Pay as you go or a Coding Plan. Reachable from China' } },
|
|
29
|
+
{ id: 'modelarts', label: '华为云 ModelArts', kind: 'apikey', login: 'key', subscribeUrl: 'https://console.huaweicloud.com/modelarts/?region=cn-southwest-2#/model-studio/authmanage', pricing: { tier: 'paid', zh: '按量付费,新用户有免费额度;模型要先在 ModelArts Studio 里开通。国内直连', en: 'Pay as you go with a free quota for new users; enable models in ModelArts Studio first. Reachable from China' } },
|
|
23
30
|
{ id: 'deepseek', label: 'DeepSeek 官方', kind: 'apikey', login: 'key', subscribeUrl: 'https://platform.deepseek.com/api_keys', pricing: { tier: 'paid', zh: '按量付费(价格低)', en: 'Pay as you go (low prices)' } },
|
|
24
31
|
{ id: 'custom', label: '其他 OpenAI 兼容服务', kind: 'apikey', login: 'key', pricing: { tier: 'paid', zh: '填地址和 key,接入任意 OpenAI 兼容服务', en: 'Enter a URL and a key for any OpenAI-compatible service' } },
|
|
25
32
|
{ id: 'opencode', label: 'OpenCode Zen', kind: 'opencode', login: 'key', subscribeUrl: 'https://opencode.ai/zen', pricing: { tier: 'paid', zh: '要先充值拿 key 按量计费;它的免费模型只能在 OpenCode 软件里用', en: 'Top up for a pay-as-you-go key; its free models only work inside the OpenCode app' } },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-model",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
4
4
|
"description": "Daily check-in credits claimed for you (Trae, Qoder, WorkBuddy China), one OpenAI-compatible address that relays every model, and free or subscription model sources (SiliconFlow, Gemini, Codex, Kimi, Ollama…). For DeepSeek Harness (dsh) and any OpenAI-compatible tool. 每天自动签到领积分 · 一个地址中转所有模型 · 免费和订阅模型随你接。",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"siliconflow",
|
package/plugin/client.js
CHANGED
|
@@ -1037,6 +1037,49 @@ window.__ModuleLoader__.load({
|
|
|
1037
1037
|
msg ? h('div', { style: { ...S.meta, marginTop: 8 } }, msg) : null)
|
|
1038
1038
|
}
|
|
1039
1039
|
|
|
1040
|
+
/** 把签到账号复制到另一台机器:导出一段加密文本 + 提取码,到那台的页面里导入 */
|
|
1041
|
+
function TransferDialog({ mode, onDone, onClose }) {
|
|
1042
|
+
const [out, setOut] = useState(null)
|
|
1043
|
+
const [text, setText] = useState('')
|
|
1044
|
+
const [code, setCode] = useState('')
|
|
1045
|
+
const [busy, setBusy] = useState(false)
|
|
1046
|
+
const [msg, setMsg] = useState('')
|
|
1047
|
+
useEffect(() => {
|
|
1048
|
+
if (mode !== 'export') return
|
|
1049
|
+
api('POST', '/checkin/export').then(setOut, (e) => setMsg(e.message))
|
|
1050
|
+
}, [mode])
|
|
1051
|
+
const doImport = async () => {
|
|
1052
|
+
setBusy(true)
|
|
1053
|
+
setMsg(L('正在导入并签到…', 'Importing and checking in…'))
|
|
1054
|
+
try {
|
|
1055
|
+
const r = await api('POST', '/checkin/import', { text: text.trim(), code: code.trim() })
|
|
1056
|
+
onDone(r)
|
|
1057
|
+
} catch (e) { setMsg(e.message); setBusy(false) }
|
|
1058
|
+
}
|
|
1059
|
+
if (mode === 'export') {
|
|
1060
|
+
return h(Modal, { title: L('复制签到账号到另一台机器', 'Copy check-in accounts to another machine'), onClose },
|
|
1061
|
+
h('p', { style: S.note }, L('比如搬到不关机的 VPS 上签,就不会漏。会复制 Qoder 令牌和豆包 Cookie;WorkBuddy 请在那台机器上各自登录(复制过去两边会互相顶掉),Trae 要在装了 Trae 的电脑上签。', 'For example move them to an always-on VPS so no day is missed. Qoder tokens and Doubao cookies are copied; sign in to WorkBuddy on that machine itself (a copied sign-in would knock the other out), and Trae needs a computer with Trae.')),
|
|
1062
|
+
out ? h('div', null,
|
|
1063
|
+
h('div', { style: { ...S.meta, marginBottom: 4 } }, L(`1. 复制这段文本(${out.counts.qoder} 个 Qoder、${out.counts.doubao} 个豆包):`, `1. Copy this text (${out.counts.qoder} Qoder, ${out.counts.doubao} Doubao):`)),
|
|
1064
|
+
h('div', { style: S.line },
|
|
1065
|
+
h('textarea', { readOnly: true, value: out.text, rows: 3, style: { ...S.input, ...S.mono, ...S.grow, resize: 'none' }, 'aria-label': L('导出的文本', 'Exported text') }),
|
|
1066
|
+
h(CopyButton, { get: out.text })),
|
|
1067
|
+
h('div', { style: { ...S.meta, margin: '10px 0 4px' } }, L('2. 记下提取码(光有文本解不开):', '2. Note the code (the text alone cannot be opened):')),
|
|
1068
|
+
h('div', { style: S.line }, h('span', { style: { ...S.mono, fontSize: 18, letterSpacing: 2 } }, out.code), h(CopyButton, { get: out.code })),
|
|
1069
|
+
h('div', { style: { ...S.meta, marginTop: 10 } }, L('3. 打开那台机器的「设置 → dsh-model」,签到卡片「详情」→「导入签到账号」,把两样都粘进去。', '3. On the other machine open Settings → dsh-model, check-in card Details → "Import check-in accounts", and paste both.')),
|
|
1070
|
+
h('div', { style: { fontSize: 12, color: C.warn, marginTop: 8 } }, L('这段文本加提取码就等于你的登录,只在自己的两台机器之间用。', 'Text plus code amount to your sign-ins: use them only between your own machines.'))) : null,
|
|
1071
|
+
msg ? h('div', { style: { fontSize: 12, color: C.err, marginTop: 8 } }, msg) : null)
|
|
1072
|
+
}
|
|
1073
|
+
return h(Modal, { title: L('导入签到账号', 'Import check-in accounts'), onClose },
|
|
1074
|
+
h('p', { style: S.note }, L('粘贴另一台机器「复制签到账号」给出的文本和提取码。已有的同一个账号不会重复加。', 'Paste the text and code from "Copy check-in accounts" on the other machine. Accounts already here are not added twice.')),
|
|
1075
|
+
h('textarea', { rows: 3, placeholder: 'dshm-checkin:v1:…', value: text, onChange: (e) => setText(e.target.value), style: { ...S.input, ...S.mono, width: '100%', boxSizing: 'border-box', resize: 'none', marginBottom: 8 }, 'aria-label': L('导出的文本', 'Exported text') }),
|
|
1076
|
+
h('div', { style: S.line },
|
|
1077
|
+
h('input', { style: { ...S.input, ...S.mono, width: 160 }, placeholder: L('提取码', 'Code'), value: code, onChange: (e) => setCode(e.target.value), 'aria-label': L('提取码', 'Code') }),
|
|
1078
|
+
h('span', { style: S.grow }),
|
|
1079
|
+
h('button', { type: 'button', style: S.btnPrimary, disabled: busy || !text.trim() || !code.trim(), onClick: () => void doImport() }, L('导入', 'Import'))),
|
|
1080
|
+
msg ? h('div', { style: { ...S.meta, marginTop: 8 } }, msg) : null)
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1040
1083
|
/** 签到卡片:平时一行"今日已完成签到:…";没签完的列出来,一键签到;自动签到开关 */
|
|
1041
1084
|
function CheckinCard() {
|
|
1042
1085
|
const [st, setSt] = useState(null)
|
|
@@ -1045,6 +1088,7 @@ window.__ModuleLoader__.load({
|
|
|
1045
1088
|
const [qoder, setQoder] = useState(false)
|
|
1046
1089
|
const [wbLogin, setWbLogin] = useState(null)
|
|
1047
1090
|
const [doubao, setDoubao] = useState(false)
|
|
1091
|
+
const [transfer, setTransfer] = useState('')
|
|
1048
1092
|
const [err, setErr] = useState('')
|
|
1049
1093
|
const refresh = useCallback(() => api('GET', '/checkin').then(setSt, () => {}), [])
|
|
1050
1094
|
const addWorkbuddy = async () => {
|
|
@@ -1073,7 +1117,9 @@ window.__ModuleLoader__.load({
|
|
|
1073
1117
|
return h('div', { style: { ...S.section, marginBottom: 16 } },
|
|
1074
1118
|
h('div', { style: { ...S.meta } }, L('每日签到:登录 WorkBuddy 国内版,或', 'Daily check-in: sign in to WorkBuddy (China), or '),
|
|
1075
1119
|
h('button', { type: 'button', style: { ...S.btn, marginLeft: 6 }, onClick: () => setQoder(true) }, L('添加 Qoder', 'Add Qoder')),
|
|
1076
|
-
h('button', { type: 'button', style: { ...S.btn, marginLeft: 6 }, onClick: () => setDoubao(true) }, L('添加豆包', 'Add Doubao'))
|
|
1120
|
+
h('button', { type: 'button', style: { ...S.btn, marginLeft: 6 }, onClick: () => setDoubao(true) }, L('添加豆包', 'Add Doubao')),
|
|
1121
|
+
h('button', { type: 'button', style: { ...S.btn, marginLeft: 6 }, onClick: () => setTransfer('import') }, L('从另一台机器导入', 'Import from another machine'))),
|
|
1122
|
+
transfer ? h(TransferDialog, { mode: transfer, onDone: (r) => { setTransfer(''); setSt(r); setOpen(true) }, onClose: () => setTransfer('') }) : null,
|
|
1077
1123
|
qoder ? h(QoderDialog, { onDone: () => { setQoder(false); void refresh() }, onClose: () => setQoder(false) }) : null,
|
|
1078
1124
|
doubao ? h(DoubaoDialog, { onDone: () => { setDoubao(false); void refresh() }, onClose: () => setDoubao(false) }) : null)
|
|
1079
1125
|
}
|
|
@@ -1089,6 +1135,12 @@ window.__ModuleLoader__.load({
|
|
|
1089
1135
|
h('button', { type: 'button', style: S.btn, onClick: () => setOpen(!open) }, open ? L('收起', 'Hide') : L('详情', 'Details')),
|
|
1090
1136
|
h('label', { style: { ...S.meta, display: 'flex', alignItems: 'center', gap: 4, cursor: 'pointer', whiteSpace: 'nowrap' } },
|
|
1091
1137
|
h('input', { type: 'checkbox', checked: st.auto, onChange: (e) => void toggleAuto(e.target.checked) }), L('自动签到', 'Auto'))),
|
|
1138
|
+
// 支持、但这台还没设置的:一眼看出还能签哪些,旁边就能加
|
|
1139
|
+
(st.missing || []).length ? h('div', { style: { ...S.line, flexWrap: 'wrap', gap: 6, marginTop: 4, fontSize: 12, color: C.sub } },
|
|
1140
|
+
h('span', null, L('这台还能签:', 'Also available here:')),
|
|
1141
|
+
(st.missing || []).map((m) => h('span', { key: m.id, style: { display: 'inline-flex', alignItems: 'center', gap: 4 } },
|
|
1142
|
+
m.label,
|
|
1143
|
+
m.action ? h('button', { type: 'button', style: { ...S.btn, padding: '1px 6px', fontSize: 11 }, onClick: () => { if (m.action === 'workbuddy') void addWorkbuddy(); else if (m.action === 'qoder') setQoder(true); else setDoubao(true) } }, L('+ 添加', '+ Add')) : h('span', { style: { color: C.faint } }, `(${m.reason})`)))) : null,
|
|
1092
1144
|
open ? h('div', { style: { marginTop: 8 } },
|
|
1093
1145
|
st.results.map((r) => h('div', { key: r.id, style: { fontSize: 12, lineHeight: 1.8, color: r.state === 'failed' ? C.err : C.text } },
|
|
1094
1146
|
`${r.label}:${CHECKIN_TEXT(r)}`, r.advice ? h('span', { style: { color: C.sub, marginLeft: 6 } }, r.advice) : null)),
|
|
@@ -1102,12 +1154,16 @@ window.__ModuleLoader__.load({
|
|
|
1102
1154
|
h('div', { style: { ...S.line, marginTop: 4, flexWrap: 'wrap' } },
|
|
1103
1155
|
h('button', { type: 'button', style: S.btn, onClick: () => void addWorkbuddy() }, L('+ 添加 WorkBuddy 账号', '+ Add a WorkBuddy account')),
|
|
1104
1156
|
h('button', { type: 'button', style: S.btn, onClick: () => setQoder(true) }, st.qoder.configured ? L('+ 再加一个 Qoder 账号', '+ Add another Qoder account') : L('+ 添加 Qoder', '+ Add Qoder')),
|
|
1105
|
-
h('button', { type: 'button', style: S.btn, onClick: () => setDoubao(true) }, (st.accounts && st.accounts.doubao && st.accounts.doubao.length) ? L('+ 再加一个豆包账号', '+ Add another Doubao account') : L('+ 添加豆包', '+ Add Doubao')))
|
|
1157
|
+
h('button', { type: 'button', style: S.btn, onClick: () => setDoubao(true) }, (st.accounts && st.accounts.doubao && st.accounts.doubao.length) ? L('+ 再加一个豆包账号', '+ Add another Doubao account') : L('+ 添加豆包', '+ Add Doubao'))),
|
|
1158
|
+
h('div', { style: { ...S.line, marginTop: 4, flexWrap: 'wrap' } },
|
|
1159
|
+
h('button', { type: 'button', style: S.btn, onClick: () => setTransfer('export') }, L('复制签到账号到另一台机器', 'Copy check-in accounts to another machine')),
|
|
1160
|
+
h('button', { type: 'button', style: S.btn, onClick: () => setTransfer('import') }, L('导入签到账号', 'Import check-in accounts')))),
|
|
1106
1161
|
// Trae 读本机 Trae 的登录:这台没装(比如 VPS)就不会出现,在装了 Trae 的电脑上签就行(每个账号一天一次)
|
|
1107
1162
|
!st.available.some((a) => a.id === 'trae') ? h('div', { style: { ...S.meta, marginTop: 6 } }, L('Trae:这台电脑上没装 Trae,所以不签。在装了 Trae 的电脑上会自动签(一个账号一天签一次就够)。', 'Trae: not installed on this machine, so it is skipped. A computer with Trae installed checks it in automatically (once a day per account is enough).')) : null) : null,
|
|
1108
1163
|
!open && !st.qoder.configured ? h('div', { style: { ...S.meta, marginTop: 4 } }, h('button', { type: 'button', style: { ...S.btn, padding: '2px 8px' }, onClick: () => setQoder(true) }, L('+ 添加 Qoder', '+ Add Qoder'))) : null,
|
|
1109
1164
|
err ? h('div', { style: { fontSize: 12, color: C.err, marginTop: 6 } }, err) : null),
|
|
1110
1165
|
qoder ? h(QoderDialog, { another: st.qoder.configured, onDone: () => { setQoder(false); void refresh() }, onClose: () => setQoder(false) }) : null,
|
|
1166
|
+
transfer ? h(TransferDialog, { mode: transfer, onDone: (r) => { setTransfer(''); setSt(r); setOpen(true) }, onClose: () => setTransfer('') }) : null,
|
|
1111
1167
|
doubao ? h(DoubaoDialog, { another: Boolean(st.accounts && st.accounts.doubao && st.accounts.doubao.length), onDone: () => { setDoubao(false); setOpen(true); void refresh() }, onClose: () => setDoubao(false) }) : null,
|
|
1112
1168
|
wbLogin ? h(LoginDialog, { source: { id: 'workbuddy-checkin', label: L('WorkBuddy 签到账号', 'WorkBuddy check-in account'), login: 'link' }, session: wbLogin, onDone: () => { setWbLogin(null); setOpen(true); setTimeout(() => void refresh(), 3000) }, onClose: () => { setWbLogin(null); void refresh() } }) : null)
|
|
1113
1169
|
}
|
package/plugin/index.js
CHANGED
|
@@ -63,7 +63,7 @@ const ALLOWED = [
|
|
|
63
63
|
['GET', /^\/self$/],
|
|
64
64
|
['GET', /^\/failures$/],
|
|
65
65
|
['GET', /^\/checkin$/],
|
|
66
|
-
['POST', /^\/checkin(\/auto|\/qoder|\/workbuddy|\/doubao)?$/],
|
|
66
|
+
['POST', /^\/checkin(\/auto|\/qoder|\/workbuddy|\/doubao|\/export|\/import)?$/],
|
|
67
67
|
['DELETE', /^\/checkin\/qoder(\/[A-Za-z0-9_-]{1,40})?$/],
|
|
68
68
|
['DELETE', /^\/checkin\/workbuddy\/[A-Za-z0-9_-]{1,80}$/],
|
|
69
69
|
['DELETE', /^\/checkin\/doubao\/[A-Za-z0-9_-]{1,40}$/],
|