@pandablessing/dsh-user-sync 1.0.12 → 1.0.15

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/lib/client.js CHANGED
@@ -147,6 +147,7 @@ if (typeof window !== 'undefined' && window.__ModuleLoader__) {
147
147
  'remote-missing': '远端无包',
148
148
  };
149
149
  /** 最近一次同步的摘要:上传 / 应用 / 检查。 */
150
+ /** 最近一次同步的摘要:上传 / 应用 / 检查。 */
150
151
  function lastSyncSummary(status) {
151
152
  if (!status)
152
153
  return '尚未同步';
@@ -158,7 +159,12 @@ if (typeof window !== 'undefined' && window.__ModuleLoader__) {
158
159
  if (status.scheduler?.lastCheckAt) {
159
160
  const decision = status.scheduler.lastDecision;
160
161
  const label = decision?.action && DECISION_LABELS[decision.action] ? DECISION_LABELS[decision.action] : '';
161
- parts.push(`上次检查 ${fmtTime(status.scheduler.lastCheckAt)}${label ? `(${label})` : ''}`);
162
+ // 检查结论只在「没有比它更新的上传/应用」时才算数:否则显示「发现更新」会与上传时间矛盾
163
+ const checkAt = Date.parse(status.scheduler.lastCheckAt);
164
+ const newerAt = Math.max(status.lastUploaded?.createdAt ? Date.parse(status.lastUploaded.createdAt) : Number.NEGATIVE_INFINITY, status.lastApplied?.createdAt ? Date.parse(status.lastApplied.createdAt) : Number.NEGATIVE_INFINITY);
165
+ const stale = Number.isFinite(checkAt) && newerAt > checkAt;
166
+ const suffix = label && !stale ? `(${label})` : '';
167
+ parts.push(`上次检查 ${fmtTime(status.scheduler.lastCheckAt)}${suffix}`);
162
168
  }
163
169
  return parts.length > 0 ? parts.join(' · ') : '尚未同步';
164
170
  }
@@ -421,7 +427,19 @@ if (typeof window !== 'undefined' && window.__ModuleLoader__) {
421
427
  });
422
428
  }
423
429
  else {
424
- setNotice({ kind: 'error', data: result });
430
+ // 常见失败给出可操作的提示(服务端错误码透传到 result.code)
431
+ const friendly = {
432
+ 'quota-exceeded': '远端配额已满:请联系维护者清理旧包,或稍后再试。',
433
+ 'package-too-large': '包超过 10 MiB 上限:请减少同步范围(取消项目记忆/大文件)后重试。',
434
+ 'E_LOCKED': '另一台设备正在同步,请稍后再试。',
435
+ };
436
+ const hint = typeof result.code === 'string' ? friendly[result.code] : undefined;
437
+ setNotice({
438
+ kind: 'error',
439
+ data: hint
440
+ ? { ...result, detail: `${hint}(${result.detail ?? result.code})` }
441
+ : { ...result, detail: result.detail ?? '上传失败:远端存储可能空间不足或不可用,请联系维护者。' },
442
+ });
425
443
  }
426
444
  refresh();
427
445
  }
package/lib/index.js CHANGED
@@ -410,14 +410,11 @@ export function apply(ctx) {
410
410
  },
411
411
  },
412
412
  {
413
- // 一键更新:走 DSH 官方安装器(与插件页安装同一路径),只允许本插件自身
413
+ // 一键更新:在 profile 里用 pnpm add 真实更新安装包(installBundle 只登记 bundle、不改安装包)
414
414
  path: '/api/dsh-user-sync/update',
415
415
  methods: ['POST'],
416
416
  requestBody: 'buffered',
417
417
  fetch: async (request) => {
418
- const pm = pluginManager;
419
- if (!pm)
420
- return json({ ok: false, error: 'E_NO_PLUGIN_MANAGER' }, 503);
421
418
  let version = '';
422
419
  try {
423
420
  const body = (await request.json());
@@ -430,7 +427,13 @@ export function apply(ctx) {
430
427
  return json({ ok: false, error: 'E_BAD_VERSION' }, 400);
431
428
  const spec = `@pandablessing/dsh-user-sync@${version}`;
432
429
  try {
433
- await pm.installBundle(spec);
430
+ const rt = pnpmRuntime();
431
+ if (!rt)
432
+ return json({ ok: false, error: 'E_NO_PNPM' }, 503);
433
+ await runCommand(rt.node, [rt.pnpmCjs, 'add', spec], {
434
+ cwd: paths.profileDir,
435
+ timeout: 240_000,
436
+ });
434
437
  }
435
438
  catch (error) {
436
439
  recordHandlerError('update', error);
package/lib/sync.js CHANGED
@@ -216,7 +216,8 @@ export function createSync({ dshHome, userHome, profileName = 'desktop', transpo
216
216
  }
217
217
  catch (error) {
218
218
  appendHistory(paths.history, { at: new Date().toISOString(), action: 'backup', ok: false, ms: Date.now() - started });
219
- return { ok: false, code: 'E_UPLOAD_FAILED', detail: error.message };
219
+ const code = error?.code ?? 'E_UPLOAD_FAILED';
220
+ return { ok: false, code, detail: error.message };
220
221
  }
221
222
  finally {
222
223
  releaseLock({ lockPath: paths.lock });
@@ -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,7 +1,7 @@
1
1
  {
2
2
  "name": "@pandablessing/dsh-user-sync",
3
- "version": "1.0.12",
4
- "description": "DeepSeek Harness 用户级配置同步插件",
3
+ "version": "1.0.15",
4
+ "description": "DSH用户级配置同步插件,更新需重启DSH",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {