@pandablessing/dsh-user-sync 1.0.14 → 1.0.16

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
@@ -510,7 +510,7 @@ if (typeof window !== 'undefined' && window.__ModuleLoader__) {
510
510
  ? R.createElement('div', { style: styles.card }, R.createElement('strong', null, '启用前请确认'), R.createElement('p', { style: styles.muted }, '上传内容:技能、插件清单、配置、用户记忆。数据经运营方服务中转并存放在运营方账号(非 DSH 官方服务器);远端只保留 1 份,上传即覆盖。'), R.createElement('div', { style: styles.actions },
511
511
  // 用普通按钮:主色按钮看起来像「已选中」,同意是主动作但不应暗示状态
512
512
  R.createElement('button', { style: styles.secondary, disabled: busy, onClick: acceptConsent }, '同意并启用上传')))
513
- : null, R.createElement('div', { style: styles.card }, R.createElement('div', { style: styles.row }, R.createElement('span', null, 'DSH 主目录'), R.createElement('code', null, status?.dshHome ?? '…')), R.createElement('div', { style: styles.row }, R.createElement('span', null, 'Profile'), R.createElement('span', null, status?.profile ?? '…')), R.createElement('div', { style: styles.row }, R.createElement('span', null, '账号'), R.createElement('span', null, status?.account?.hint ? `已配对 · ${status.account.hint}` : '未登录官方账号')), R.createElement('div', { style: styles.row }, R.createElement('span', null, '传输'), R.createElement('span', null, status?.transport ?? '…')), R.createElement('div', { style: styles.row }, R.createElement('span', null, '版本'), R.createElement('span', null, status?.version ? `v${status.version}` : '加载中…')), R.createElement('div', { style: styles.row }, R.createElement('span', null, '接口'), R.createElement('span', { style: status?.error ? styles.error : undefined }, status?.error ? `错误:${status.error}${status.detail ? `(${status.detail})` : ''}` : status?.installed ? '正常' : '加载中…')), R.createElement('div', { style: styles.row }, R.createElement('span', null, '扫描'), R.createElement('span', { style: scan?.error ? styles.error : undefined }, scan?.error
513
+ : null, R.createElement('div', { style: styles.card }, R.createElement('div', { style: styles.row }, R.createElement('span', null, 'DSH 主目录'), R.createElement('code', null, status?.dshHome ?? '…')), R.createElement('div', { style: styles.row }, R.createElement('span', null, 'Profile'), R.createElement('span', null, status?.profile ?? '…')), R.createElement('div', { style: styles.row }, R.createElement('span', null, '账号'), R.createElement('span', null, status?.account?.hint ? `已配对 · ${status.account.hint}` : '未登录官方账号')), R.createElement('div', { style: styles.row }, R.createElement('span', null, '版本'), R.createElement('span', null, status?.version ? `v${status.version}` : '加载中…')), R.createElement('div', { style: styles.row }, R.createElement('span', null, '接口'), R.createElement('span', { style: status?.error ? styles.error : undefined }, status?.error ? `错误:${status.error}${status.detail ? `(${status.detail})` : ''}` : status?.installed ? '正常' : '加载中…')), R.createElement('div', { style: styles.row }, R.createElement('span', null, '扫描'), R.createElement('span', { style: scan?.error ? styles.error : undefined }, scan?.error
514
514
  ? `失败:${scan.error}${scan.detail ? `:${String(scan.detail).slice(0, 160)}` : ''}`
515
515
  : `已选 ${scan?.selectedFiles ?? scan?.totalFiles ?? 0} 个文件 · ${fmtBytes(scan?.selectedBytes ?? scan?.totalBytes ?? 0)}(同步范围 ${scan?.syncableFiles ?? scan?.totalFiles ?? 0} 个 · ${fmtBytes(scan?.syncableBytes ?? scan?.totalBytes ?? 0)})`))), R.createElement('div', { style: styles.card }, R.createElement('strong', null, '同步范围'), (scan?.selectedBytes ?? 0) > 10 * 1024 * 1024
516
516
  ? R.createElement('p', { style: styles.warn }, `预计 ${fmtBytes(scan?.selectedBytes)},超过 10 MiB 单包上限;上传会被拒绝,请取消「会话」或减少同步范围。`)
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);
@@ -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.14",
3
+ "version": "1.0.16",
4
4
  "description": "DSH用户级配置同步插件,更新需重启DSH",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",