@pandablessing/dsh-user-sync 1.0.7 → 1.0.8
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 +8 -0
- package/lib/index.js +60 -19
- package/lib/transport/api.js +175 -175
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -87,6 +87,8 @@ powershell -File scripts/smoke.ps1 # 真实 DSH 路由冒烟
|
|
|
87
87
|
|
|
88
88
|
## 发布
|
|
89
89
|
|
|
90
|
+
> **发布时机由维护者决定**:`scripts/publish.mjs` 只在手动执行时发布,agent 不自动发布(改版本、构建、dry-run 可以提前做,发布等确认)。
|
|
91
|
+
|
|
90
92
|
token 只放本地文件,绝不进仓库(`.env.local` 已在 `.gitignore`):
|
|
91
93
|
|
|
92
94
|
```bash
|
|
@@ -99,6 +101,12 @@ node scripts/publish.mjs --otp=123456 # 账号开 2FA 时
|
|
|
99
101
|
- CI 用 secret 注入 `NPM_TOKEN` 环境变量即可,优先级高于 `.env.local`。
|
|
100
102
|
- 发布前记得先 `pnpm run build && pnpm run build:server`,产物与源码不一致时 tarball 里会是旧代码。
|
|
101
103
|
|
|
104
|
+
## 开发与真实环境
|
|
105
|
+
|
|
106
|
+
- **日常开发(junction)**:`scripts/install-dev.ps1` 把仓库 link 进 profile,改源码 → `pnpm run build` → 热重载即生效;调试最快。
|
|
107
|
+
- **真实安装(npm 包)**:`node scripts/verify-npm-install.mjs` 在临时 profile 里 `pnpm add` 当前发布版本,校验 patch name、宿主/client 两个入口、locale 与 patch 资源,用完自动删目录,不影响开发 profile。
|
|
108
|
+
- 为什么分开:npm 安装的是 `lib/*.js` 编译产物副本,改仓库源码不会生效;所以开发用 link,验证真实安装路径用临时 profile。
|
|
109
|
+
|
|
102
110
|
## 仓库结构
|
|
103
111
|
|
|
104
112
|
```text
|
package/lib/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { appendFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
@@ -28,7 +28,13 @@ const NON_USER_LEVEL = ['projectMemory', 'sessions'];
|
|
|
28
28
|
/** 异步执行外部命令:绝不阻塞宿主事件循环(Electron 主进程卡死会拖垮整个界面)。 */
|
|
29
29
|
function runCommand(file, args, options) {
|
|
30
30
|
return new Promise((resolve, reject) => {
|
|
31
|
-
|
|
31
|
+
// 保留 stderr:安装失败时把它写进 plugin-error.log,便于定位(此前只有退出码)
|
|
32
|
+
const child = spawn(file, args, { cwd: options.cwd, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
33
|
+
let stderr = '';
|
|
34
|
+
child.stderr?.on('data', (chunk) => {
|
|
35
|
+
if (stderr.length < 4000)
|
|
36
|
+
stderr += chunk.toString('utf8');
|
|
37
|
+
});
|
|
32
38
|
const timer = setTimeout(() => {
|
|
33
39
|
child.kill();
|
|
34
40
|
reject(new Error(`命令超时(${options.timeout} ms): ${file}`));
|
|
@@ -42,7 +48,7 @@ function runCommand(file, args, options) {
|
|
|
42
48
|
if (code === 0)
|
|
43
49
|
resolve();
|
|
44
50
|
else
|
|
45
|
-
reject(new Error(`命令退出码 ${code}: ${file}`));
|
|
51
|
+
reject(new Error(`命令退出码 ${code}: ${file} ${args.join(' ')}\n${stderr.slice(-800)}`));
|
|
46
52
|
});
|
|
47
53
|
});
|
|
48
54
|
}
|
|
@@ -211,8 +217,10 @@ export function apply(ctx) {
|
|
|
211
217
|
};
|
|
212
218
|
/**
|
|
213
219
|
* 按 profile 的 package.json 安装依赖。
|
|
214
|
-
*
|
|
215
|
-
*
|
|
220
|
+
* 两类依赖会让 pnpm install 整体失败(连带普通包都装不上):
|
|
221
|
+
* - `link:` / `file:` 指向不存在的本地路径
|
|
222
|
+
* - `node_modules` 里是 junction/符号链接(开发期 link 装的插件,pnpm 重装会 EPERM)
|
|
223
|
+
* 因此安装期间临时把这些依赖从 package.json 摘掉,装完原样恢复,并重建被 pnpm 移走的链接。
|
|
216
224
|
*/
|
|
217
225
|
const installProfileDependencies = async () => {
|
|
218
226
|
const rt = pnpmRuntime();
|
|
@@ -222,6 +230,7 @@ export function apply(ctx) {
|
|
|
222
230
|
const workspacePath = join(paths.profileDir, 'pnpm-workspace.yaml');
|
|
223
231
|
const raw = readFileSync(pkgPath, 'utf8');
|
|
224
232
|
const removedLinks = [];
|
|
233
|
+
const linkTargets = new Map();
|
|
225
234
|
let workspaceRaw = null;
|
|
226
235
|
const missingPatchLines = [];
|
|
227
236
|
try {
|
|
@@ -236,25 +245,44 @@ export function apply(ctx) {
|
|
|
236
245
|
missingPatchLines.push(line);
|
|
237
246
|
}
|
|
238
247
|
if (missingPatchLines.length > 0) {
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
248
|
+
const kept = workspaceRaw.split('\n').filter((line) => !missingPatchLines.includes(line));
|
|
249
|
+
// 段内条目全删光时,连 `patchedDependencies:` 键一起删:pnpm 不接受 null
|
|
250
|
+
const keyIndex = kept.findIndex((line) => line.trim() === 'patchedDependencies:');
|
|
251
|
+
if (keyIndex >= 0) {
|
|
252
|
+
const next = kept[keyIndex + 1];
|
|
253
|
+
if (next === undefined || !/^\s{2}\S/.test(next))
|
|
254
|
+
kept.splice(keyIndex, 1);
|
|
255
|
+
}
|
|
256
|
+
writeFileSync(workspacePath, kept.join('\n'), 'utf8');
|
|
244
257
|
}
|
|
245
258
|
}
|
|
246
259
|
catch {
|
|
247
260
|
workspaceRaw = null;
|
|
248
261
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
262
|
+
// 需要临时摘除的依赖:坏 link / 文件依赖 + 已经是 junction 的已装包
|
|
263
|
+
const detached = Object.entries(dependencies).filter(([name, spec]) => {
|
|
264
|
+
if (spec.startsWith('link:') || spec.startsWith('file:')) {
|
|
265
|
+
const target = join(paths.profileDir, spec.slice(5));
|
|
266
|
+
if (!existsSync(target))
|
|
267
|
+
return true;
|
|
268
|
+
linkTargets.set(name, target);
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
const abs = join(paths.profileDir, 'node_modules', name);
|
|
272
|
+
try {
|
|
273
|
+
if (lstatSync(abs).isSymbolicLink()) {
|
|
274
|
+
linkTargets.set(name, realpathSync(abs));
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
// 没装或读不到:交给 pnpm 正常安装
|
|
280
|
+
}
|
|
281
|
+
return false;
|
|
254
282
|
});
|
|
255
|
-
if (
|
|
283
|
+
if (detached.length > 0) {
|
|
256
284
|
const patched = { ...pkg, dependencies: { ...dependencies } };
|
|
257
|
-
for (const [name] of
|
|
285
|
+
for (const [name] of detached) {
|
|
258
286
|
delete patched.dependencies[name];
|
|
259
287
|
removedLinks.push(name);
|
|
260
288
|
}
|
|
@@ -265,8 +293,21 @@ export function apply(ctx) {
|
|
|
265
293
|
finally {
|
|
266
294
|
if (removedLinks.length > 0)
|
|
267
295
|
writeFileSync(pkgPath, raw, 'utf8');
|
|
268
|
-
if (workspaceRaw !== null && missingPatchLines
|
|
269
|
-
writeFileSync(
|
|
296
|
+
if (workspaceRaw !== null && missingPatchLines.length > 0)
|
|
297
|
+
writeFileSync(workspacePath, workspaceRaw, 'utf8');
|
|
298
|
+
// pnpm 可能把不在依赖树里的链接移进 .ignored;装完重建,保证开发期 link 不断
|
|
299
|
+
for (const [name, target] of linkTargets) {
|
|
300
|
+
const abs = join(paths.profileDir, 'node_modules', name);
|
|
301
|
+
if (existsSync(abs))
|
|
302
|
+
continue;
|
|
303
|
+
try {
|
|
304
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
305
|
+
symlinkSync(target, abs, process.platform === 'win32' ? 'junction' : 'dir');
|
|
306
|
+
}
|
|
307
|
+
catch (error) {
|
|
308
|
+
log('relink', error);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
270
311
|
}
|
|
271
312
|
return { removedLinks };
|
|
272
313
|
};
|
package/lib/transport/api.js
CHANGED
|
@@ -1,175 +1,175 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import { dirname } from 'node:path';
|
|
3
|
-
import { isPackageId } from '../archive.js';
|
|
4
|
-
const OFFICIAL_HOST = /(^|\.)deepseek\.com$/i;
|
|
5
|
-
export class ApiError extends Error {
|
|
6
|
-
code;
|
|
7
|
-
status;
|
|
8
|
-
constructor(code, message, status) {
|
|
9
|
-
super(message);
|
|
10
|
-
this.code = code;
|
|
11
|
-
this.status = status;
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
/**
|
|
15
|
-
* 运营方 API 传输(B 模型)。插件侧只持有用户自己的 token,不接触存储凭据。
|
|
16
|
-
* 认证:register 首次建账号,login 取回同一 token;token 存 auth.json(0600)。
|
|
17
|
-
*/
|
|
18
|
-
export function createApiTransport({ baseUrl, authPath, token: initialToken, fetchImpl = globalThis.fetch, timeoutMs = 30_000, retries = 3, }) {
|
|
19
|
-
if (!baseUrl)
|
|
20
|
-
throw new Error('缺少 baseUrl');
|
|
21
|
-
const host = new URL(baseUrl).hostname;
|
|
22
|
-
if (OFFICIAL_HOST.test(host)) {
|
|
23
|
-
throw new ApiError('E_OFFICIAL_HOST', `拒绝官方域名: ${host}`);
|
|
24
|
-
}
|
|
25
|
-
let token = initialToken ?? null;
|
|
26
|
-
if (!token && authPath && existsSync(authPath)) {
|
|
27
|
-
try {
|
|
28
|
-
token = JSON.parse(readFileSync(authPath, 'utf8')).token ?? null;
|
|
29
|
-
}
|
|
30
|
-
catch {
|
|
31
|
-
token = null;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
function persistAuth(next) {
|
|
35
|
-
token = next.token;
|
|
36
|
-
if (!authPath)
|
|
37
|
-
return;
|
|
38
|
-
mkdirSync(dirname(authPath), { recursive: true });
|
|
39
|
-
writeFileSync(authPath, `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600 });
|
|
40
|
-
}
|
|
41
|
-
function clearAuth() {
|
|
42
|
-
token = null;
|
|
43
|
-
if (authPath)
|
|
44
|
-
rmSync(authPath, { force: true });
|
|
45
|
-
}
|
|
46
|
-
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
47
|
-
async function request(method, path, { body, headers = {}, expectBytes = false } = {}) {
|
|
48
|
-
const url = `${baseUrl.replace(/\/$/, '')}${path}`;
|
|
49
|
-
let lastError;
|
|
50
|
-
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
51
|
-
try {
|
|
52
|
-
const response = await fetchImpl(url, {
|
|
53
|
-
method,
|
|
54
|
-
headers: {
|
|
55
|
-
...(token ? { authorization: `Bearer ${token}` } : {}),
|
|
56
|
-
...(body && !expectBytes ? { 'content-type': 'application/json' } : {}),
|
|
57
|
-
...headers,
|
|
58
|
-
},
|
|
59
|
-
body: body,
|
|
60
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
61
|
-
});
|
|
62
|
-
if (response.status === 401 || response.status === 403) {
|
|
63
|
-
throw new ApiError('E_REMOTE_AUTH', `认证失败(${response.status})`, response.status);
|
|
64
|
-
}
|
|
65
|
-
if (response.status >= 400 && response.status < 500) {
|
|
66
|
-
const detail = await response.text().catch(() => '');
|
|
67
|
-
throw new ApiError(`E_HTTP_${response.status}`, detail.slice(0, 200) || `HTTP ${response.status}`, response.status);
|
|
68
|
-
}
|
|
69
|
-
if (!response.ok) {
|
|
70
|
-
throw new ApiError('E_REMOTE_5XX', `服务端错误(${response.status})`, response.status);
|
|
71
|
-
}
|
|
72
|
-
return expectBytes ? Buffer.from(await response.arrayBuffer()) : response;
|
|
73
|
-
}
|
|
74
|
-
catch (error) {
|
|
75
|
-
lastError = error;
|
|
76
|
-
const retryable = !(error instanceof ApiError) || error.code === 'E_REMOTE_5XX';
|
|
77
|
-
if (!retryable || attempt === retries)
|
|
78
|
-
throw error;
|
|
79
|
-
await sleep(200 * 2 ** attempt);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
throw lastError;
|
|
83
|
-
}
|
|
84
|
-
const parseJson = async (response) => {
|
|
85
|
-
const text = await response.text();
|
|
86
|
-
return text ? JSON.parse(text) : null;
|
|
87
|
-
};
|
|
88
|
-
/**
|
|
89
|
-
* 用同一 accountKey 取回同一个 token(不签发新 token)。
|
|
90
|
-
* 抽成独立函数:register 里改走 login 时不依赖 `this`,解构调用也不会炸。
|
|
91
|
-
*/
|
|
92
|
-
async function loginWith(accountKey) {
|
|
93
|
-
const response = await request('POST', '/v1/auth/login', { body: JSON.stringify({ accountKey }) });
|
|
94
|
-
const data = await parseJson(response);
|
|
95
|
-
const token = String(data?.token ?? '');
|
|
96
|
-
persistAuth({ token, accountKeyHint: accountKey.slice(0, 6), registeredAt: new Date().toISOString() });
|
|
97
|
-
return { ok: true, token };
|
|
98
|
-
}
|
|
99
|
-
return {
|
|
100
|
-
id: 'api',
|
|
101
|
-
baseUrl,
|
|
102
|
-
get token() {
|
|
103
|
-
return token;
|
|
104
|
-
},
|
|
105
|
-
/** 首次注册;已存在(409)时改走 login,取回同一个 token。 */
|
|
106
|
-
async register(accountKey) {
|
|
107
|
-
try {
|
|
108
|
-
const response = await request('POST', '/v1/auth/register', { body: JSON.stringify({ accountKey }) });
|
|
109
|
-
const data = await parseJson(response);
|
|
110
|
-
const token = String(data?.token ?? '');
|
|
111
|
-
persistAuth({ token, accountKeyHint: accountKey.slice(0, 6), registeredAt: new Date().toISOString() });
|
|
112
|
-
return { ok: true, token };
|
|
113
|
-
}
|
|
114
|
-
catch (error) {
|
|
115
|
-
if (error.code === 'E_HTTP_409')
|
|
116
|
-
return loginWith(accountKey);
|
|
117
|
-
throw error;
|
|
118
|
-
}
|
|
119
|
-
},
|
|
120
|
-
login: loginWith,
|
|
121
|
-
async reset() {
|
|
122
|
-
await request('POST', '/v1/auth/reset');
|
|
123
|
-
clearAuth();
|
|
124
|
-
return { ok: true };
|
|
125
|
-
},
|
|
126
|
-
async test() {
|
|
127
|
-
try {
|
|
128
|
-
const response = await request('GET', '/v1/quota');
|
|
129
|
-
const data = await parseJson(response);
|
|
130
|
-
return { ok: true, detail: `配额 ${data?.used ?? 0}/${data?.limit ?? 0} 字节` };
|
|
131
|
-
}
|
|
132
|
-
catch (error) {
|
|
133
|
-
return { ok: false, detail: error.message };
|
|
134
|
-
}
|
|
135
|
-
},
|
|
136
|
-
async list() {
|
|
137
|
-
const response = await request('GET', '/v1/packages');
|
|
138
|
-
const data = await parseJson(response);
|
|
139
|
-
return (data?.packages ?? []).map((item) => ({
|
|
140
|
-
name: item.id,
|
|
141
|
-
size: item.size,
|
|
142
|
-
modifiedAt: item.createdAt,
|
|
143
|
-
}));
|
|
144
|
-
},
|
|
145
|
-
async meta(id) {
|
|
146
|
-
if (!isPackageId(id))
|
|
147
|
-
throw new ApiError('E_BAD_NAME', `非法包名: ${id}`);
|
|
148
|
-
const response = await request('GET', `/v1/packages/${encodeURIComponent(id)}/meta`);
|
|
149
|
-
return parseJson(response);
|
|
150
|
-
},
|
|
151
|
-
async get(id) {
|
|
152
|
-
if (!isPackageId(id))
|
|
153
|
-
throw new ApiError('E_BAD_NAME', `非法包名: ${id}`);
|
|
154
|
-
return request('GET', `/v1/packages/${encodeURIComponent(id)}`, { expectBytes: true });
|
|
155
|
-
},
|
|
156
|
-
async put(id, data) {
|
|
157
|
-
if (!isPackageId(id))
|
|
158
|
-
throw new ApiError('E_BAD_NAME', `非法包名: ${id}`);
|
|
159
|
-
await request('PUT', `/v1/packages/${encodeURIComponent(id)}`, {
|
|
160
|
-
body: data,
|
|
161
|
-
expectBytes: true,
|
|
162
|
-
headers: { 'content-type': 'application/octet-stream' },
|
|
163
|
-
});
|
|
164
|
-
},
|
|
165
|
-
async remove(id) {
|
|
166
|
-
if (!isPackageId(id))
|
|
167
|
-
throw new ApiError('E_BAD_NAME', `非法包名: ${id}`);
|
|
168
|
-
await request('DELETE', `/v1/packages/${encodeURIComponent(id)}`);
|
|
169
|
-
},
|
|
170
|
-
/** 导出我的全部数据(服务端打包当前包 + 元数据)。 */
|
|
171
|
-
async exportPackage() {
|
|
172
|
-
return request('GET', '/v1/export', { expectBytes: true });
|
|
173
|
-
},
|
|
174
|
-
};
|
|
175
|
-
}
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { isPackageId } from '../archive.js';
|
|
4
|
+
const OFFICIAL_HOST = /(^|\.)deepseek\.com$/i;
|
|
5
|
+
export class ApiError extends Error {
|
|
6
|
+
code;
|
|
7
|
+
status;
|
|
8
|
+
constructor(code, message, status) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.code = code;
|
|
11
|
+
this.status = status;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* 运营方 API 传输(B 模型)。插件侧只持有用户自己的 token,不接触存储凭据。
|
|
16
|
+
* 认证:register 首次建账号,login 取回同一 token;token 存 auth.json(0600)。
|
|
17
|
+
*/
|
|
18
|
+
export function createApiTransport({ baseUrl, authPath, token: initialToken, fetchImpl = globalThis.fetch, timeoutMs = 30_000, retries = 3, }) {
|
|
19
|
+
if (!baseUrl)
|
|
20
|
+
throw new Error('缺少 baseUrl');
|
|
21
|
+
const host = new URL(baseUrl).hostname;
|
|
22
|
+
if (OFFICIAL_HOST.test(host)) {
|
|
23
|
+
throw new ApiError('E_OFFICIAL_HOST', `拒绝官方域名: ${host}`);
|
|
24
|
+
}
|
|
25
|
+
let token = initialToken ?? null;
|
|
26
|
+
if (!token && authPath && existsSync(authPath)) {
|
|
27
|
+
try {
|
|
28
|
+
token = JSON.parse(readFileSync(authPath, 'utf8')).token ?? null;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
token = null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function persistAuth(next) {
|
|
35
|
+
token = next.token;
|
|
36
|
+
if (!authPath)
|
|
37
|
+
return;
|
|
38
|
+
mkdirSync(dirname(authPath), { recursive: true });
|
|
39
|
+
writeFileSync(authPath, `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600 });
|
|
40
|
+
}
|
|
41
|
+
function clearAuth() {
|
|
42
|
+
token = null;
|
|
43
|
+
if (authPath)
|
|
44
|
+
rmSync(authPath, { force: true });
|
|
45
|
+
}
|
|
46
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
47
|
+
async function request(method, path, { body, headers = {}, expectBytes = false } = {}) {
|
|
48
|
+
const url = `${baseUrl.replace(/\/$/, '')}${path}`;
|
|
49
|
+
let lastError;
|
|
50
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
51
|
+
try {
|
|
52
|
+
const response = await fetchImpl(url, {
|
|
53
|
+
method,
|
|
54
|
+
headers: {
|
|
55
|
+
...(token ? { authorization: `Bearer ${token}` } : {}),
|
|
56
|
+
...(body && !expectBytes ? { 'content-type': 'application/json' } : {}),
|
|
57
|
+
...headers,
|
|
58
|
+
},
|
|
59
|
+
body: body,
|
|
60
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
61
|
+
});
|
|
62
|
+
if (response.status === 401 || response.status === 403) {
|
|
63
|
+
throw new ApiError('E_REMOTE_AUTH', `认证失败(${response.status})`, response.status);
|
|
64
|
+
}
|
|
65
|
+
if (response.status >= 400 && response.status < 500) {
|
|
66
|
+
const detail = await response.text().catch(() => '');
|
|
67
|
+
throw new ApiError(`E_HTTP_${response.status}`, detail.slice(0, 200) || `HTTP ${response.status}`, response.status);
|
|
68
|
+
}
|
|
69
|
+
if (!response.ok) {
|
|
70
|
+
throw new ApiError('E_REMOTE_5XX', `服务端错误(${response.status})`, response.status);
|
|
71
|
+
}
|
|
72
|
+
return expectBytes ? Buffer.from(await response.arrayBuffer()) : response;
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
lastError = error;
|
|
76
|
+
const retryable = !(error instanceof ApiError) || error.code === 'E_REMOTE_5XX';
|
|
77
|
+
if (!retryable || attempt === retries)
|
|
78
|
+
throw error;
|
|
79
|
+
await sleep(200 * 2 ** attempt);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
throw lastError;
|
|
83
|
+
}
|
|
84
|
+
const parseJson = async (response) => {
|
|
85
|
+
const text = await response.text();
|
|
86
|
+
return text ? JSON.parse(text) : null;
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* 用同一 accountKey 取回同一个 token(不签发新 token)。
|
|
90
|
+
* 抽成独立函数:register 里改走 login 时不依赖 `this`,解构调用也不会炸。
|
|
91
|
+
*/
|
|
92
|
+
async function loginWith(accountKey) {
|
|
93
|
+
const response = await request('POST', '/v1/auth/login', { body: JSON.stringify({ accountKey }) });
|
|
94
|
+
const data = await parseJson(response);
|
|
95
|
+
const token = String(data?.token ?? '');
|
|
96
|
+
persistAuth({ token, accountKeyHint: accountKey.slice(0, 6), registeredAt: new Date().toISOString() });
|
|
97
|
+
return { ok: true, token };
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
id: 'api',
|
|
101
|
+
baseUrl,
|
|
102
|
+
get token() {
|
|
103
|
+
return token;
|
|
104
|
+
},
|
|
105
|
+
/** 首次注册;已存在(409)时改走 login,取回同一个 token。 */
|
|
106
|
+
async register(accountKey) {
|
|
107
|
+
try {
|
|
108
|
+
const response = await request('POST', '/v1/auth/register', { body: JSON.stringify({ accountKey }) });
|
|
109
|
+
const data = await parseJson(response);
|
|
110
|
+
const token = String(data?.token ?? '');
|
|
111
|
+
persistAuth({ token, accountKeyHint: accountKey.slice(0, 6), registeredAt: new Date().toISOString() });
|
|
112
|
+
return { ok: true, token };
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
if (error.code === 'E_HTTP_409')
|
|
116
|
+
return loginWith(accountKey);
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
login: loginWith,
|
|
121
|
+
async reset() {
|
|
122
|
+
await request('POST', '/v1/auth/reset');
|
|
123
|
+
clearAuth();
|
|
124
|
+
return { ok: true };
|
|
125
|
+
},
|
|
126
|
+
async test() {
|
|
127
|
+
try {
|
|
128
|
+
const response = await request('GET', '/v1/quota');
|
|
129
|
+
const data = await parseJson(response);
|
|
130
|
+
return { ok: true, detail: `配额 ${data?.used ?? 0}/${data?.limit ?? 0} 字节` };
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
return { ok: false, detail: error.message };
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
async list() {
|
|
137
|
+
const response = await request('GET', '/v1/packages');
|
|
138
|
+
const data = await parseJson(response);
|
|
139
|
+
return (data?.packages ?? []).map((item) => ({
|
|
140
|
+
name: item.id,
|
|
141
|
+
size: item.size,
|
|
142
|
+
modifiedAt: item.createdAt,
|
|
143
|
+
}));
|
|
144
|
+
},
|
|
145
|
+
async meta(id) {
|
|
146
|
+
if (!isPackageId(id))
|
|
147
|
+
throw new ApiError('E_BAD_NAME', `非法包名: ${id}`);
|
|
148
|
+
const response = await request('GET', `/v1/packages/${encodeURIComponent(id)}/meta`);
|
|
149
|
+
return parseJson(response);
|
|
150
|
+
},
|
|
151
|
+
async get(id) {
|
|
152
|
+
if (!isPackageId(id))
|
|
153
|
+
throw new ApiError('E_BAD_NAME', `非法包名: ${id}`);
|
|
154
|
+
return request('GET', `/v1/packages/${encodeURIComponent(id)}`, { expectBytes: true });
|
|
155
|
+
},
|
|
156
|
+
async put(id, data) {
|
|
157
|
+
if (!isPackageId(id))
|
|
158
|
+
throw new ApiError('E_BAD_NAME', `非法包名: ${id}`);
|
|
159
|
+
await request('PUT', `/v1/packages/${encodeURIComponent(id)}`, {
|
|
160
|
+
body: data,
|
|
161
|
+
expectBytes: true,
|
|
162
|
+
headers: { 'content-type': 'application/octet-stream' },
|
|
163
|
+
});
|
|
164
|
+
},
|
|
165
|
+
async remove(id) {
|
|
166
|
+
if (!isPackageId(id))
|
|
167
|
+
throw new ApiError('E_BAD_NAME', `非法包名: ${id}`);
|
|
168
|
+
await request('DELETE', `/v1/packages/${encodeURIComponent(id)}`);
|
|
169
|
+
},
|
|
170
|
+
/** 导出我的全部数据(服务端打包当前包 + 元数据)。 */
|
|
171
|
+
async exportPackage() {
|
|
172
|
+
return request('GET', '/v1/export', { expectBytes: true });
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pandablessing/dsh-user-sync",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
4
4
|
"description": "DeepSeek Harness 用户级配置同步插件",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
"build:server": "tsc -p server/tsconfig.json",
|
|
61
61
|
"test": "node --test test/*.test.js",
|
|
62
62
|
"publish:npm": "node scripts/publish.mjs",
|
|
63
|
-
"publish:dry": "node scripts/publish.mjs --dry-run"
|
|
63
|
+
"publish:dry": "node scripts/publish.mjs --dry-run",
|
|
64
|
+
"smoke:npm": "node scripts/verify-npm-install.mjs"
|
|
64
65
|
}
|
|
65
66
|
}
|