@pandablessing/dsh-user-sync 1.0.8 → 1.0.10

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
@@ -162,24 +162,12 @@ if (typeof window !== 'undefined' && window.__ModuleLoader__) {
162
162
  }
163
163
  return parts.length > 0 ? parts.join(' · ') : '尚未同步';
164
164
  }
165
- function ResultCard({ notice, onApply, onConfirmModules, onDismiss, onInstallPlugins, onForceUpload }) {
165
+ function ResultCard({ notice, onConfirmModules, onDismiss, onForceUpload }) {
166
166
  if (!notice)
167
167
  return null;
168
168
  const { kind, data } = notice;
169
169
  if (kind === 'check') {
170
170
  const decision = data.decision ?? {};
171
- if (decision.action === 'pull' || decision.action === 'confirm') {
172
- const plan = data.plan ?? { added: [], overwritten: [], skipped: [] };
173
- const missing = data.plugins?.missing ?? [];
174
- const linkDeps = data.plugins?.linkDeps ?? [];
175
- return R.createElement('div', { style: styles.card }, R.createElement('strong', null, decision.action === 'confirm' ? '两侧都有更新,需要你决定' : '远端有更新'), R.createElement('p', { style: styles.muted }, `远端包 ${data.latest?.name ?? ''}(${data.meta?.createdAt ?? '时间未知'}),新增 ${(plan.added ?? []).length} / 覆盖 ${(plan.overwritten ?? []).length} / 跳过 ${(plan.skipped ?? []).length} 个文件。`), missing.length > 0
176
- ? R.createElement('p', { style: styles.muted }, `缺失插件:${missing.map((item) => `${item.name}@${item.spec}`).join('、')}`)
177
- : null, linkDeps.length > 0
178
- ? R.createElement('p', { style: styles.muted }, `本地包需手动放回:${linkDeps.map((item) => item.name).join('、')}`)
179
- : null, R.createElement('div', { style: styles.actions }, missing.length > 0
180
- ? R.createElement('button', { style: styles.primary, onClick: () => onInstallPlugins(missing) }, '先安装缺失插件')
181
- : null, R.createElement('button', { style: styles.secondary, onClick: () => onDismiss() }, '取消')), R.createElement('p', { style: styles.muted }, '覆盖动作会在弹窗中确认。'));
182
- }
183
171
  return R.createElement('div', { style: styles.card }, R.createElement('strong', null, decision.action === 'skip' ? '本机已是最新' : '本机有改动'), R.createElement('p', { style: styles.muted }, `判定:${decision.reason ?? decision.action}`));
184
172
  }
185
173
  if (kind === 'error') {
@@ -225,6 +213,8 @@ if (typeof window !== 'undefined' && window.__ModuleLoader__) {
225
213
  const [confirmDialog, setConfirmDialog] = R.useState(null);
226
214
  // 覆盖确认弹窗:上传发现远端更新 / 从远端同步发现远端更新时弹出(不再用结果卡按钮)
227
215
  const [overwriteDialog, setOverwriteDialog] = R.useState(null);
216
+ // 版本检查结果:有新版本时在面板顶部提示(不自动安装)
217
+ const [updateInfo, setUpdateInfo] = R.useState(null);
228
218
  // 当前展开文件清单的模块(null 表示都收起)
229
219
  const [expandedModule, setExpandedModule] = R.useState(null);
230
220
  // 设置改动后自动保存(600ms 防抖),不再提供「保存设置」按钮
@@ -270,6 +260,10 @@ if (typeof window !== 'undefined' && window.__ModuleLoader__) {
270
260
  };
271
261
  R.useEffect(() => {
272
262
  refresh();
263
+ // 版本检查失败不影响面板(网络不可用时静默)
264
+ fetchJson(`${API}/update-check`)
265
+ .then((data) => setUpdateInfo(data))
266
+ .catch(() => setUpdateInfo(null));
273
267
  }, []);
274
268
  // 设置改动后 600ms 自动保存(输入间隔时不会每敲一下都发请求)
275
269
  R.useEffect(() => {
@@ -411,29 +405,6 @@ if (typeof window !== 'undefined' && window.__ModuleLoader__) {
411
405
  setBusy(false);
412
406
  }
413
407
  };
414
- const installPlugins = async (packages) => {
415
- setBusy(true);
416
- try {
417
- const result = await fetchJson(`${API}/install`, {
418
- method: 'POST',
419
- headers: { 'content-type': 'application/json' },
420
- body: JSON.stringify({ packages }),
421
- });
422
- if (result.ok) {
423
- setNotice({ kind: 'result', data: { detail: `已安装 ${result.results?.length ?? 0} 个插件,重启 DSH 后生效` } });
424
- }
425
- else {
426
- setNotice({ kind: 'error', data: { detail: `安装失败:${(result.results ?? []).filter((item) => !item.ok).map((item) => `${item.name}(${item.error})`).join(';') || result.error}` } });
427
- }
428
- refresh();
429
- }
430
- catch (error) {
431
- setNotice({ kind: 'error', data: { detail: String(error) } });
432
- }
433
- finally {
434
- setBusy(false);
435
- }
436
- };
437
408
  const checkRemote = async () => {
438
409
  setBusy(true);
439
410
  setNotice(null);
@@ -488,8 +459,8 @@ if (typeof window !== 'undefined' && window.__ModuleLoader__) {
488
459
  // 计数优先用 status 的宿主探针(与 /scan 同一条扫描路径),
489
460
  // 这样 /scan 请求失败时面板仍能显示真实数量。
490
461
  const counts = status?.probe?.counts ?? scan?.counts ?? {};
491
- return R.createElement('div', { style: styles.section }, R.createElement('h2', { style: styles.heading }, '设置同步'), status?.scheduler?.lastDecision?.action === 'pull' && consentGiven
492
- ? R.createElement('div', { style: styles.card }, R.createElement('strong', null, '远端有更新'), R.createElement('p', { style: styles.muted }, `检测到 ${fmtTime(status.scheduler.lastCheckAt)} 的远端更新(来自 ${status.scheduler.lastDecision.packageId ?? '远端'})。`), R.createElement('div', { style: styles.actions }, R.createElement('button', { style: styles.secondary, disabled: busy, onClick: checkRemote }, '立即同步')))
462
+ return R.createElement('div', { style: styles.section }, R.createElement('h2', { style: styles.heading }, '设置同步'), updateInfo?.available
463
+ ? R.createElement('p', { style: styles.warn }, `插件有新版本 v${updateInfo.latest}(当前 v${updateInfo.current}):pnpm add @pandablessing/dsh-user-sync@${updateInfo.latest}`)
493
464
  : null, !consentGiven
494
465
  ? R.createElement('div', { style: styles.card }, R.createElement('strong', null, '启用前请确认'), R.createElement('p', { style: styles.muted }, '上传内容:技能、插件清单、配置、用户记忆。数据经运营方服务中转并存放在运营方账号(非 DSH 官方服务器);远端只保留 1 份,上传即覆盖。'), R.createElement('div', { style: styles.actions },
495
466
  // 用普通按钮:主色按钮看起来像「已选中」,同意是主动作但不应暗示状态
@@ -552,10 +523,8 @@ if (typeof window !== 'undefined' && window.__ModuleLoader__) {
552
523
  // 服务地址只读展示:默认内置,用户覆盖后标注来源(面板不再提供输入框)
553
524
  R.createElement('p', { style: styles.muted }, `服务地址:${status?.baseUrl ?? '(未解析)'}${status?.baseUrlSource === 'user' ? '(自定义)' : '(内置)'}`), R.createElement(ResultCard, {
554
525
  notice,
555
- onApply: applyRemote,
556
526
  onConfirmModules: confirmModules,
557
527
  onDismiss: () => setNotice(null),
558
- onInstallPlugins: installPlugins,
559
528
  onForceUpload: () => upload(true),
560
529
  }), confirmDialog
561
530
  ? R.createElement('div', { style: styles.overlay, onClick: () => setConfirmDialog(null) }, R.createElement('div', { style: styles.dialog, onClick: (event) => event.stopPropagation?.() }, R.createElement('strong', null, '同步项目/会话内容?'), R.createElement('p', { style: styles.muted }, `${confirmDialog.warning.map((id) => MODULE_LABELS[id] ?? id).join('、')} 属于项目/会话级,可能包含具体工作内容。确认要跨机器同步吗?`), R.createElement('div', { style: styles.actions }, R.createElement('button', { style: styles.primary, onClick: () => confirmModules(confirmDialog.modules) }, '仍要同步'), R.createElement('button', { style: styles.secondary, onClick: () => setConfirmDialog(null) }, '取消'))))
package/lib/index.js CHANGED
@@ -353,7 +353,62 @@ export function apply(ctx) {
353
353
  }));
354
354
  return { installed, linkDeps: plan.linkDeps.map(({ name, spec }) => ({ name, spec })) };
355
355
  };
356
+ /** 判断 latest 是否比 current 新(只比较 x.y.z 数字段)。 */
357
+ const isNewerVersion = (latest, current) => {
358
+ const toParts = (value) => value.split('-')[0].split('.').map((part) => Number.parseInt(part, 10) || 0);
359
+ const a = toParts(latest);
360
+ const b = toParts(current);
361
+ for (let i = 0; i < Math.max(a.length, b.length); i++) {
362
+ const left = a[i] ?? 0;
363
+ const right = b[i] ?? 0;
364
+ if (left !== right)
365
+ return left > right;
366
+ }
367
+ return false;
368
+ };
369
+ /** 版本检查:查 npm registry 的 dist-tags 与当前版本比对(30 分钟缓存,失败静默)。 */
370
+ let versionCache = null;
371
+ const checkLatestVersion = async () => {
372
+ const now = Date.now();
373
+ if (versionCache && now - versionCache.at < 30 * 60_000) {
374
+ const latest = versionCache.latest;
375
+ return { current: PLUGIN_VERSION, latest, available: Boolean(latest && isNewerVersion(latest, PLUGIN_VERSION)) };
376
+ }
377
+ const registry = process.env.NPM_REGISTRY || 'https://registry.npmjs.org';
378
+ const encoded = encodeURIComponent('@pandablessing/dsh-user-sync').replace('%2F', '%2f');
379
+ const controller = new AbortController();
380
+ const timer = setTimeout(() => controller.abort(), 5000);
381
+ try {
382
+ const response = await fetch(`${registry}/-/package/${encoded}/dist-tags`, { signal: controller.signal });
383
+ const tags = (await response.json());
384
+ const latest = typeof tags.latest === 'string' ? tags.latest : null;
385
+ versionCache = { at: now, latest };
386
+ return { current: PLUGIN_VERSION, latest, available: Boolean(latest && isNewerVersion(latest, PLUGIN_VERSION)) };
387
+ }
388
+ catch {
389
+ versionCache = { at: now, latest: null };
390
+ return { current: PLUGIN_VERSION, latest: null, available: false };
391
+ }
392
+ finally {
393
+ clearTimeout(timer);
394
+ }
395
+ };
356
396
  const routes = [
397
+ {
398
+ // 版本检查:面板据此提示「有新版本可升级」,不做自动安装
399
+ path: '/api/dsh-user-sync/update-check',
400
+ methods: ['GET'],
401
+ requestBody: 'buffered',
402
+ fetch: async () => {
403
+ try {
404
+ return json(await checkLatestVersion());
405
+ }
406
+ catch (error) {
407
+ recordHandlerError('update-check', error);
408
+ return json({ current: PLUGIN_VERSION, latest: null, available: false });
409
+ }
410
+ },
411
+ },
357
412
  {
358
413
  // 最小同步路由:用于区分「同步 handler 本身不可用」与「status/scan 具体逻辑抛错」
359
414
  path: '/api/dsh-user-sync/ping',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pandablessing/dsh-user-sync",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
4
4
  "description": "DeepSeek Harness 用户级配置同步插件",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",