@qic-9821/dsh-plugin-sync 0.1.0 → 0.1.2

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 CHANGED
@@ -40,6 +40,8 @@ dsh plugin --profile <profile> add @qic-9821/dsh-plugin-sync
40
40
  | 允许对端改动本机 | `capRemoteApply` | 关掉后对端连读都读不到 |
41
41
  | 同步密钥字段 | `capSecrets` | 默认关;`appSecret` / `token` / `*key*` 一律写成 `[redacted]` |
42
42
 
43
+ 默认只开「插件清单与启停」和「允许对端改动本机」;配置、安装、复制二进制、密钥四项要自己在设置页打开,打开后下一次自动同步就会真的落地对应操作。
44
+
43
45
  安装、升级、卸载、启停全部走宿主的 `pluginManager` 服务(`installBundle` / `updateBundle` / `removeBundle` / `setBundleEnabled` / `setPluginEnabled`),不自己拼 pnpm 命令行、不手写 profile 的 `package.json`,失败由宿主回滚它自己的改动。
44
46
 
45
47
  ## 怎么连
@@ -55,9 +57,11 @@ dsh plugin --profile <profile> add @qic-9821/dsh-plugin-sync
55
57
 
56
58
  - `autoStart`:DSH 启动后约 25 秒跑一轮(等宿主把插件挂完)。
57
59
  - `autoRestart`:进程重启后再补一轮(约 2 分钟),处理对端还在启动的情况;服务器重启后的同步就是这条。
58
- - `intervalMinutes`:固定间隔(0 = 关)。
60
+ - `intervalMinutes`:固定间隔(0 = 关)。只在 DSH 运行期间生效,每 N 分钟跑一轮完整同步;和「启动时」「重启后」两轮互不影响。
59
61
  - 手动:面板/设置页按钮,或工具 `plugin_sync` `action="run"`。
60
62
 
63
+ 总开关 `enabled` 关掉后,以上任何自动同步与手动同步都不执行(仍可在面板上「只看计划」)。
64
+
61
65
  ## 冲突策略
62
66
 
63
67
  - `local`:以发起方为准(默认)
package/client.js CHANGED
@@ -94,6 +94,9 @@ window.__ModuleLoader__.load({
94
94
  }),
95
95
  h('span', { key: 'text' }, [
96
96
  h('b', { key: 'name' }, props.label),
97
+ props.dirty
98
+ ? h('span', { key: 'dirty', style: { marginLeft: 6, fontSize: 11, color: 'var(--dsh-accent, #3b82f6)' } }, '未保存')
99
+ : null,
97
100
  props.desc ? h('div', { key: 'desc', style: MUTED }, props.desc) : null,
98
101
  ]),
99
102
  ],
@@ -125,8 +128,18 @@ window.__ModuleLoader__.load({
125
128
  }, [load])
126
129
 
127
130
  const config = state?.config || {}
128
- const value = useCallback((key) => (draft[key] !== undefined ? draft[key] : config[key]), [draft, config])
131
+ const caps = config.capabilities || {}
132
+ // draft wins, then the nested capabilities map, then the flat key
133
+ const value = useCallback(
134
+ (key) => (draft[key] !== undefined ? draft[key] : caps[key] !== undefined ? caps[key] : config[key]),
135
+ [draft, caps, config],
136
+ )
137
+ const dirty = useCallback((key) => draft[key] !== undefined, [draft])
129
138
  const setValue = useCallback((key, next) => setDraft((current) => ({ ...current, [key]: next })), [])
139
+ const capsOn = useMemo(
140
+ () => (state?.capabilityCatalog || []).filter((cap) => Boolean(value(cap.key))).length,
141
+ [state, value],
142
+ )
130
143
 
131
144
  const save = useCallback(async () => {
132
145
  if (Object.keys(draft).length === 0) return
@@ -182,6 +195,7 @@ window.__ModuleLoader__.load({
182
195
  : status.localError || '读取失败',
183
196
  ],
184
197
  ['profile 目录', status.profileDir || '?'],
198
+ ['版本', status.package ? `${status.package.name} ${status.package.version || ''}`.trim() : '?'],
185
199
  ['对端', status.peer?.describe || '?'],
186
200
  [
187
201
  '上次同步',
@@ -189,9 +203,10 @@ window.__ModuleLoader__.load({
189
203
  ? `${formatTime(status.lastRun.at)} · ${status.lastRun.ok ? '成功' : `失败:${status.lastRun.error || ''}`}`
190
204
  : '还没跑过',
191
205
  ],
206
+ ['设置保存于', state?.updatedAt ? formatTime(state.updatedAt) : '还没保存过'],
192
207
  ['当前', status.running ? '同步中…' : '空闲'],
193
208
  ]
194
- }, [status])
209
+ }, [status, state?.updatedAt])
195
210
 
196
211
  if (!state) {
197
212
  return h('div', { style: { padding: 16, fontSize: 13 } }, error ? `读取失败:${error}` : '读取中…')
@@ -259,7 +274,8 @@ window.__ModuleLoader__.load({
259
274
  h(Toggle, {
260
275
  key: 'enable',
261
276
  label: '总开关',
262
- desc: '关掉后「启动时同步」和手动同步都不动作',
277
+ desc: '关掉后任何自动与手动同步都不执行(仍可「只看计划」)',
278
+ dirty: dirty('enabled'),
263
279
  checked: Boolean(value('enabled')),
264
280
  onChange: (next) => setValue('enabled', next),
265
281
  }),
@@ -267,6 +283,7 @@ window.__ModuleLoader__.load({
267
283
  key: 'startup',
268
284
  label: '软件启动时同步',
269
285
  desc: 'DSH 启动后约 25 秒跑一轮(等宿主把插件都挂上)',
286
+ dirty: dirty('autoStart'),
270
287
  checked: Boolean(value('autoStart')),
271
288
  onChange: (next) => setValue('autoStart', next),
272
289
  }),
@@ -274,22 +291,38 @@ window.__ModuleLoader__.load({
274
291
  key: 'restart',
275
292
  label: '服务器重启后同步',
276
293
  desc: '进程重启之后再补一轮,处理对端还在启动的情况',
294
+ dirty: dirty('autoRestart'),
277
295
  checked: Boolean(value('autoRestart')),
278
296
  onChange: (next) => setValue('autoRestart', next),
279
297
  }),
280
- h(Field, { key: 'interval', label: '每隔多少分钟同步(0 = 不按间隔)' },
281
- input(value('intervalMinutes'), '0', (next) => setValue('intervalMinutes', Number(next) || 0))),
282
- h(Field, { key: 'delay', label: '启动后延迟秒数' },
298
+ h(Toggle, {
299
+ key: 'interval',
300
+ label: '定时同步',
301
+ desc: 'DSH 运行期间每 N 分钟跑一轮;关掉就是「不按间隔」,和上面两轮互不影响',
302
+ dirty: dirty('intervalMinutes'),
303
+ checked: Number(value('intervalMinutes')) > 0,
304
+ onChange: (next) =>
305
+ setValue('intervalMinutes', next ? Number(value('intervalMinutes')) || 15 : 0),
306
+ }),
307
+ Number(value('intervalMinutes')) > 0
308
+ ? h(
309
+ Field,
310
+ { key: 'intervalValue', label: '间隔分钟数(1–1440)' },
311
+ input(value('intervalMinutes'), '15', (next) => setValue('intervalMinutes', Math.max(0, Number(next) || 0))),
312
+ )
313
+ : null,
314
+ h(Field, { key: 'delay', label: '启动后延迟秒数(只影响「软件启动时同步」)' },
283
315
  input(value('startupDelaySec'), '25', (next) => setValue('startupDelaySec', Number(next) || 0))),
284
316
  ]),
285
317
  ]),
286
318
 
287
- h(Card, { key: 'caps', title: '权限(每一项可以单独关)' },
319
+ h(Card, { key: 'caps', title: `权限(已开启 ${capsOn} / ${(state.capabilityCatalog || []).length},每一项可以单独关)` },
288
320
  (state.capabilityCatalog || []).map((cap) =>
289
321
  h(Toggle, {
290
322
  key: cap.key,
291
323
  label: cap.label,
292
324
  desc: cap.desc,
325
+ dirty: dirty(cap.key),
293
326
  checked: Boolean(value(cap.key)),
294
327
  onChange: (next) => setValue(cap.key, next),
295
328
  }),
@@ -298,7 +331,13 @@ window.__ModuleLoader__.load({
298
331
 
299
332
  h('div', { key: 'save', style: { display: 'flex', gap: 8, alignItems: 'center' } }, [
300
333
  h('button', { key: 'save', style: BUTTON_PRIMARY, disabled: Boolean(busy) || Object.keys(draft).length === 0, onClick: () => void save() }, '保存设置'),
301
- h('span', { key: 'hint', style: MUTED }, Object.keys(draft).length === 0 ? '没有未保存的改动' : `${Object.keys(draft).length} 项待保存`),
334
+ h(
335
+ 'span',
336
+ { key: 'hint', style: MUTED },
337
+ Object.keys(draft).length === 0
338
+ ? `已保存 · ${state?.updatedAt ? formatTime(state.updatedAt) : '(本次打开后还没改过)'}`
339
+ : `${Object.keys(draft).length} 项待保存`,
340
+ ),
302
341
  ]),
303
342
 
304
343
  out ? h('pre', { key: 'out', style: { marginTop: 12, whiteSpace: 'pre-wrap', fontSize: 11.5, maxHeight: 260, overflow: 'auto', background: 'rgba(127,127,127,.1)', padding: 10, borderRadius: 8 } }, out) : null,
package/lib/engine.js CHANGED
@@ -90,6 +90,7 @@ export class Engine {
90
90
  },
91
91
  direction: cfg.direction,
92
92
  conflict: cfg.conflict,
93
+ package: { name: PACKAGE_NAME, version: this.version || null },
93
94
  capabilities: Object.fromEntries(CAPABILITY_KEYS.map((key) => [key, Boolean(cfg[key])])),
94
95
  local,
95
96
  localError,
package/lib/http.js CHANGED
@@ -148,6 +148,8 @@ export async function statePayload(engine, cfg) {
148
148
  capabilities: Object.fromEntries(
149
149
  Object.keys(CAPABILITY_LABELS).map((key) => [key, Boolean(cfg[key])]),
150
150
  ),
151
+ // flat mirror of the capabilities, so consumers can read `capPlugins` directly
152
+ ...Object.fromEntries(Object.keys(CAPABILITY_LABELS).map((key) => [key, Boolean(cfg[key])])),
151
153
  include: cfg.include,
152
154
  exclude: cfg.exclude,
153
155
  },
@@ -319,6 +321,7 @@ function render() {
319
321
  const s = state.status
320
322
  $('status').innerHTML = [
321
323
  ['本机', s.local ? (s.local.node || '?') + ' · ' + (s.local.profile || '') + ' · ' + s.local.bundles + ' 包 / ' + s.local.plugins + ' 条目' : '读取失败'],
324
+ ['版本', s.package ? s.package.name + ' ' + (s.package.version || '') : '?'],
322
325
  ['profile', s.profileDir || '?'],
323
326
  ['对端', s.peer.describe],
324
327
  ['上次同步', s.lastRun ? (s.lastRun.at + ' · ' + (s.lastRun.ok ? '成功' : '失败:' + (s.lastRun.error || ''))) : '还没跑过'],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qic-9821/dsh-plugin-sync",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Keep the DSH plugin set (inventory, configs, enable state, package installs, local bundle binaries) in sync between two machines, with per-capability switches, schedule controls and a settings/panel UI.",
5
5
  "license": "MIT",
6
6
  "author": "QIC-9821",