sidekick-chat 0.2.10 → 0.2.11
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.md +12 -2
- package/package.json +1 -1
- package/src/api.mjs +37 -11
- package/src/config.mjs +39 -5
- package/src/main.mjs +11 -7
- package/src/quota.mjs +6 -1
package/README.md
CHANGED
|
@@ -58,9 +58,18 @@ sidekick login --url http://47.96.64.66:18792 --token <设备令牌> # 直连
|
|
|
58
58
|
```
|
|
59
59
|
|
|
60
60
|
- **网页密码**:就是网页版那个 `CORTICO_WEB_PASSWORD`。客户端拿它调站点的 `/api/auth/login`,
|
|
61
|
-
拿到 `cortico_session` Cookie 存起来,400 天不用重登。服务器地址默认 `https://hkv4.cznorth.cn/chat
|
|
61
|
+
拿到 `cortico_session` Cookie 存起来,400 天不用重登。服务器地址默认 `https://hkv4.cznorth.cn/chat`(香港)。
|
|
62
62
|
- **设备令牌**:部署 `.env` 里的 `SIDEKICK_DEVICE_TOKEN`,和手机 App 用的是同一个。
|
|
63
|
-
|
|
63
|
+
备用线路没有网页登录接口,要用令牌。
|
|
64
|
+
|
|
65
|
+
默认线路顺序(主线路连不上或返回 502/503/504 才换,换过去 5 分钟后会先试回主线路):
|
|
66
|
+
|
|
67
|
+
1. `https://hkv4.cznorth.cn/chat` — 香港主线路
|
|
68
|
+
2. `https://kiro.ch66.top/sk-us/chat` — 美国直连(备用)
|
|
69
|
+
3. `http://47.96.64.66:18792/chat` — 阿里云 frps → 香港 OpenResty → 美国(备用)
|
|
70
|
+
|
|
71
|
+
用 `--url` 自己指定地址时不会自动加备用线路;要备用就再给 `--alt`(可重复,或逗号分隔),
|
|
72
|
+
也可以写进 `~/.sidekick/config.json` 的 `alts` 数组。`SIDEKICK_ALT` / `SIDEKICK_ALTS` 环境变量同样认。
|
|
64
73
|
|
|
65
74
|
`sidekick logout` 清掉本机登录态。Cookie 失效时会提示重新 `sidekick login`。
|
|
66
75
|
|
|
@@ -99,6 +108,7 @@ sidekick --device 家里的电脑 "…" # 换一个署名(默认「终端」)
|
|
|
99
108
|
| 变量 | 作用 |
|
|
100
109
|
|---|---|
|
|
101
110
|
| `SIDEKICK_URL` | 服务器地址,如 `https://hkv4.cznorth.cn/chat` |
|
|
111
|
+
| `SIDEKICK_ALT` / `SIDEKICK_ALTS` | 备用线路,逗号分隔;主线路连不上时按顺序换 |
|
|
102
112
|
| `SIDEKICK_TOKEN` | 设备令牌(Bearer) |
|
|
103
113
|
| `SIDEKICK_PASSWORD` | 网页登录密码(只在 `sidekick login` 时用,不落盘) |
|
|
104
114
|
| `SIDEKICK_COOKIE` | 直接给 `cortico_session` 的值 |
|
package/package.json
CHANGED
package/src/api.mjs
CHANGED
|
@@ -11,22 +11,36 @@ import { authOrigin } from './config.mjs';
|
|
|
11
11
|
/** 鉴权没过:登录态失效或令牌不对,调用方据此提示重新登录。 */
|
|
12
12
|
export class AuthError extends Error {}
|
|
13
13
|
|
|
14
|
-
/** 连不上(没拿到响应):只有这种错才换备用线路,4xx
|
|
14
|
+
/** 连不上(没拿到响应):只有这种错才换备用线路,4xx 不换。 */
|
|
15
15
|
export class NetworkError extends Error {}
|
|
16
16
|
|
|
17
|
-
class HttpError extends Error {
|
|
17
|
+
export class HttpError extends Error {
|
|
18
18
|
constructor(status, message) { super(message); this.status = status; }
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
/** 网关错误通常是隧道断了、上游没起来,换一条线路可能就好了。 */
|
|
22
|
+
function routeFailed(err) {
|
|
23
|
+
return err instanceof NetworkError || (err instanceof HttpError && [502, 503, 504].includes(err.status));
|
|
24
|
+
}
|
|
25
|
+
|
|
21
26
|
const REQUEST_TIMEOUT_MS = 20_000;
|
|
27
|
+
/** 换到备用线路后,过这么久再试回主线路。 */
|
|
28
|
+
const PRIMARY_RETRY_MS = 5 * 60_000;
|
|
22
29
|
|
|
23
30
|
export class SidekickApi {
|
|
24
|
-
/**
|
|
25
|
-
|
|
31
|
+
/**
|
|
32
|
+
* `alts` 是备用线路列表(美国直连、阿里→香港→美国),按顺序在连不上时换;
|
|
33
|
+
* `alt` 是旧版的单条写法,仍认。
|
|
34
|
+
*/
|
|
35
|
+
constructor({ url, alt, alts, token, cookie, now = Date.now, primaryRetryMs = PRIMARY_RETRY_MS }) {
|
|
26
36
|
this.primary = stripSlashes(String(url || ''));
|
|
27
|
-
|
|
37
|
+
const list = Array.isArray(alts) ? alts : alts !== undefined ? [alts] : alt !== undefined ? [alt] : [];
|
|
38
|
+
this.alts = [...new Set(list.map((b) => stripSlashes(String(b || ''))).filter((b) => b && b !== this.primary))];
|
|
28
39
|
/** 当前用着哪条:切到备用线路后就不再每次先撞一次主线路。 */
|
|
29
40
|
this.current = this.primary;
|
|
41
|
+
this.switchedAt = 0;
|
|
42
|
+
this.now = now;
|
|
43
|
+
this.primaryRetryMs = primaryRetryMs;
|
|
30
44
|
this.token = token || '';
|
|
31
45
|
this.cookie = cookie || '';
|
|
32
46
|
}
|
|
@@ -42,20 +56,32 @@ export class SidekickApi {
|
|
|
42
56
|
return h;
|
|
43
57
|
}
|
|
44
58
|
|
|
45
|
-
/**
|
|
59
|
+
/**
|
|
60
|
+
* 当前线路优先;换到备用线路超过 5 分钟后,下一次请求先试回主线路。
|
|
61
|
+
* 返回的列表里不再包含已经试过的那条当前线路。
|
|
62
|
+
*/
|
|
63
|
+
routeOrder() {
|
|
64
|
+
const all = [this.primary, ...this.alts].filter(Boolean);
|
|
65
|
+
if (this.current === this.primary) return all;
|
|
66
|
+
const backups = all.filter((b) => b !== this.primary);
|
|
67
|
+
if (this.now() - this.switchedAt >= this.primaryRetryMs) return [this.primary, ...backups];
|
|
68
|
+
// 还没到回头试主线路的时候:当前这条优先,其余备用在后面,主线路垫底。
|
|
69
|
+
return [this.current, ...backups.filter((b) => b !== this.current), this.primary];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** 先试当前线路,连不上再按顺序试备用线路。4xx 是请求本身的问题,不换。 */
|
|
46
73
|
async attempt(run) {
|
|
47
|
-
const bases = [this.current === this.alt ? this.primary : this.current];
|
|
48
|
-
if (this.alt && this.alt !== bases[0]) bases.push(this.alt);
|
|
49
74
|
let last;
|
|
50
|
-
for (const base of
|
|
75
|
+
for (const base of this.routeOrder()) {
|
|
51
76
|
try {
|
|
52
77
|
const out = await run(base);
|
|
53
78
|
this.current = base;
|
|
79
|
+
this.switchedAt = base === this.primary ? 0 : this.now();
|
|
54
80
|
return out;
|
|
55
81
|
} catch (err) {
|
|
56
|
-
if (!(err
|
|
82
|
+
if (!routeFailed(err)) {
|
|
57
83
|
// 备用线路上没有网页登录接口:提示改用令牌,别让人以为密码错了。
|
|
58
|
-
if (err instanceof AuthError && base
|
|
84
|
+
if (err instanceof AuthError && base !== this.primary && this.cookie) {
|
|
59
85
|
throw new AuthError('备用线路不认网页登录,改成 sidekick login --token <设备令牌>');
|
|
60
86
|
}
|
|
61
87
|
throw err;
|
package/src/config.mjs
CHANGED
|
@@ -7,8 +7,17 @@ import { homedir } from 'node:os';
|
|
|
7
7
|
import { dirname, join } from 'node:path';
|
|
8
8
|
|
|
9
9
|
export const DEFAULT_URL = 'https://hkv4.cznorth.cn/chat';
|
|
10
|
-
/**
|
|
11
|
-
|
|
10
|
+
/**
|
|
11
|
+
* 主线路(香港)连不上时按顺序换的备用线路:
|
|
12
|
+
* 1. 美国直连(kiro.ch66.top 的证书在美机上,`/sk-us` 前缀转到各 World);
|
|
13
|
+
* 2. 阿里云 frps → 香港 OpenResty → 美国(和主线路同一个后端,只是入口在杭州)。
|
|
14
|
+
*/
|
|
15
|
+
export const DEFAULT_ALTS = [
|
|
16
|
+
'https://kiro.ch66.top/sk-us/chat',
|
|
17
|
+
'http://47.96.64.66:18792/chat',
|
|
18
|
+
];
|
|
19
|
+
/** 旧配置里的单条备用线路;仍然是「阿里云 → 美国」,现在中间也经香港。 */
|
|
20
|
+
export const DEFAULT_ALT = DEFAULT_ALTS[1];
|
|
12
21
|
|
|
13
22
|
export function configPath() {
|
|
14
23
|
return process.env.SIDEKICK_CONFIG || join(homedir(), '.sidekick', 'config.json');
|
|
@@ -48,13 +57,17 @@ export function clearConfig() {
|
|
|
48
57
|
export function resolveSettings(flags = {}) {
|
|
49
58
|
const saved = readConfig();
|
|
50
59
|
const url = String(flags.url || process.env.SIDEKICK_URL || saved.url || DEFAULT_URL).trim();
|
|
51
|
-
const
|
|
60
|
+
const rawAlts = flags.alts ?? flags.alt ?? process.env.SIDEKICK_ALTS ?? process.env.SIDEKICK_ALT
|
|
61
|
+
?? upgradeSavedAlts(saved) ?? defaultAltsFor(url);
|
|
62
|
+
const alts = normalizeAlts(rawAlts).map(stripTrailingSlash);
|
|
52
63
|
const token = String(flags.token || process.env.SIDEKICK_TOKEN || saved.token || '').trim();
|
|
53
64
|
const cookie = String(process.env.SIDEKICK_COOKIE || saved.cookie || '').trim();
|
|
54
65
|
const device = String(flags.device || process.env.SIDEKICK_DEVICE || saved.device || '终端').trim();
|
|
55
66
|
return {
|
|
56
67
|
url: stripTrailingSlash(url),
|
|
57
|
-
|
|
68
|
+
/** 旧代码只认单条备用线路,保留第一个。 */
|
|
69
|
+
alt: alts[0] || '',
|
|
70
|
+
alts,
|
|
58
71
|
token,
|
|
59
72
|
cookie,
|
|
60
73
|
device,
|
|
@@ -64,13 +77,34 @@ export function resolveSettings(flags = {}) {
|
|
|
64
77
|
};
|
|
65
78
|
}
|
|
66
79
|
|
|
80
|
+
/** 旧版本登录时会自动把单条杭州备用线路写进 `alt`;读到它就升级成新的两条默认线路。 */
|
|
81
|
+
export function upgradeSavedAlts(saved) {
|
|
82
|
+
if (Array.isArray(saved.alts)) return saved.alts;
|
|
83
|
+
if (!saved.alt) return undefined;
|
|
84
|
+
const old = stripTrailingSlash(String(saved.alt));
|
|
85
|
+
if (old === 'http://47.96.64.66:18792' || old === 'http://47.96.64.66:18792/chat') return [...DEFAULT_ALTS];
|
|
86
|
+
return [saved.alt];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** `--alt` 可重复,也可写成逗号/空格/换行分隔;数组要原样保留。 */
|
|
90
|
+
export function normalizeAlts(value) {
|
|
91
|
+
if (value === undefined || value === null) return [];
|
|
92
|
+
const list = Array.isArray(value) ? value : String(value).split(/[,\s]+/);
|
|
93
|
+
return [...new Set(list.map((v) => String(v ?? '').trim()).filter(Boolean))];
|
|
94
|
+
}
|
|
95
|
+
|
|
67
96
|
export function stripTrailingSlash(url) {
|
|
68
97
|
return url.replace(/\/+$/, '');
|
|
69
98
|
}
|
|
70
99
|
|
|
71
100
|
/** 用默认的主线路时自动带上已知的备用线路;自己指定 `--url` 就只走那一条。 */
|
|
101
|
+
export function defaultAltsFor(url) {
|
|
102
|
+
return stripTrailingSlash(url) === DEFAULT_URL ? [...DEFAULT_ALTS] : [];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** 兼容旧调用:只要第一条备用线路。 */
|
|
72
106
|
export function defaultAltFor(url) {
|
|
73
|
-
return
|
|
107
|
+
return defaultAltsFor(url)[0] || '';
|
|
74
108
|
}
|
|
75
109
|
|
|
76
110
|
/** WebUI 的登录接口在站点根上,不在 `/chat/` 这类前缀下面。 */
|
package/src/main.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import { readFileSync } from 'node:fs';
|
|
|
10
10
|
import { fileURLToPath } from 'node:url';
|
|
11
11
|
import { AuthError, SidekickApi, loginWithPassword } from './api.mjs';
|
|
12
12
|
import {
|
|
13
|
-
|
|
13
|
+
DEFAULT_ALTS,
|
|
14
14
|
DEFAULT_URL,
|
|
15
15
|
clearConfig,
|
|
16
16
|
configPath,
|
|
@@ -54,7 +54,8 @@ const HELP = `Sidekick 终端客户端 ${PACKAGE_VERSION}
|
|
|
54
54
|
|
|
55
55
|
参数:
|
|
56
56
|
--url <地址> 服务器地址,默认 ${DEFAULT_URL}
|
|
57
|
-
--alt <地址>
|
|
57
|
+
--alt <地址> 备用线路,主线路连不上时按顺序换;可重复或逗号分隔
|
|
58
|
+
默认:${DEFAULT_ALTS.join(' , ')}
|
|
58
59
|
--token <令牌> 设备令牌(部署 .env 的 SIDEKICK_DEVICE_TOKEN),跳过密码登录
|
|
59
60
|
--password <密码> 网页登录密码(不给就在终端里问)
|
|
60
61
|
--conv <会话> 指定会话:id、名字或 /conv 列表里的序号,默认和秘书的私聊
|
|
@@ -68,13 +69,13 @@ const HELP = `Sidekick 终端客户端 ${PACKAGE_VERSION}
|
|
|
68
69
|
|
|
69
70
|
进交互后可用 /help 看命令,/conv 列会话,/to 切换,Ctrl-C 退出。
|
|
70
71
|
|
|
71
|
-
环境变量(优先于配置文件):SIDEKICK_URL、SIDEKICK_ALT、SIDEKICK_TOKEN、SIDEKICK_PASSWORD、SIDEKICK_DEVICE、SIDEKICK_COOKIE。
|
|
72
|
+
环境变量(优先于配置文件):SIDEKICK_URL、SIDEKICK_ALT / SIDEKICK_ALTS、SIDEKICK_TOKEN、SIDEKICK_PASSWORD、SIDEKICK_DEVICE、SIDEKICK_COOKIE。
|
|
72
73
|
配置放在 ${configPath()}(权限 600)。`;
|
|
73
74
|
|
|
74
75
|
/** 命令行参数解析:node 自带的 parseArgs,不引第三方依赖。 */
|
|
75
76
|
function parseFlags(argv) {
|
|
76
77
|
const flags = { positional: [] };
|
|
77
|
-
const takesValue = new Set(['url', 'alt', 'token', 'password', 'conv', 'device', 'wait']);
|
|
78
|
+
const takesValue = new Set(['url', 'alt', 'alts', 'token', 'password', 'conv', 'device', 'wait']);
|
|
78
79
|
for (let i = 0; i < argv.length; i += 1) {
|
|
79
80
|
const arg = argv[i];
|
|
80
81
|
if (arg === '--') {
|
|
@@ -107,7 +108,9 @@ function parseFlags(argv) {
|
|
|
107
108
|
if (!takesValue.has(name)) throw new Error(`不认识的参数:--${name}`);
|
|
108
109
|
const value = eq >= 0 ? arg.slice(eq + 1) : argv[++i];
|
|
109
110
|
if (value === undefined) throw new Error(`--${name} 缺一个值`);
|
|
110
|
-
|
|
111
|
+
// --alt 可重复:攒成列表交给 resolveSettings。
|
|
112
|
+
if (name === 'alt') (flags.alts ??= []).push(value);
|
|
113
|
+
else flags[name] = value;
|
|
111
114
|
continue;
|
|
112
115
|
}
|
|
113
116
|
flags.positional.push(arg);
|
|
@@ -143,10 +146,11 @@ async function loginCommand(flags) {
|
|
|
143
146
|
cookie = await loginWithPassword(url, password);
|
|
144
147
|
}
|
|
145
148
|
|
|
146
|
-
const api = new SidekickApi({ url,
|
|
149
|
+
const api = new SidekickApi({ url, alts: current.alts, token, cookie });
|
|
147
150
|
const state = await api.state();
|
|
148
151
|
const primary = state.bots.find((b) => b.primary);
|
|
149
|
-
const cfg = { ...readConfig(), url,
|
|
152
|
+
const cfg = { ...readConfig(), url, alts: current.alts, device: current.device || '终端', savedAt: new Date().toISOString() };
|
|
153
|
+
delete cfg.alt;
|
|
150
154
|
if (token) {
|
|
151
155
|
cfg.token = token;
|
|
152
156
|
delete cfg.cookie;
|
package/src/quota.mjs
CHANGED
|
@@ -75,7 +75,12 @@ export function createQuotaReader(settings, { api = new SidekickApi(settings), a
|
|
|
75
75
|
opencode: () => api.request('GET', 'v1/opencode/quota', undefined, { signal: ctl.signal }),
|
|
76
76
|
codex: () => api.request('GET', 'v1/codex/status', undefined, { signal: ctl.signal }),
|
|
77
77
|
claude: () => {
|
|
78
|
-
|
|
78
|
+
if (!ccApi) {
|
|
79
|
+
// 每条线路都换成对应的 ccweb 入口;推不出的(自定义地址)跳过,别让额度查询整个失败。
|
|
80
|
+
const bases = [settings.url, ...(settings.alts ?? (settings.alt ? [settings.alt] : []))]
|
|
81
|
+
.flatMap((base) => { try { return [ccwebBase(base)]; } catch { return []; } });
|
|
82
|
+
ccApi = new SidekickApi({ ...settings, url: bases[0], alts: bases.slice(1) });
|
|
83
|
+
}
|
|
79
84
|
return ccApi.request('GET', 'v1/agents', undefined, { signal: ctl.signal });
|
|
80
85
|
},
|
|
81
86
|
};
|