@wenaixi/dsh-superpower 7.6.0 → 7.7.0

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
@@ -195,5 +195,6 @@ Issues and PRs welcome. See [CONTRIBUTING.md](./CONTRIBUTING.md).
195
195
 
196
196
  ## Credits
197
197
 
198
- - Upstream [obra/superpowers](https://github.com/obra/superpowers) by [Jesse Vincent](https://blog.fsck.com) and [Prime Radiant](https://primeradiant.com), also MIT licensed
198
+ - Upstream [obra/superpowers](https://github.com/obra/superpowers)
199
199
  - [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) for the three-role plugin architecture
200
+ - [dsh-plugin-dev](https://github.com/Wenaixi/dsh-plugin-dev) for DSH plugin development standards, architecture references, and verification methodologies
package/README.zh.md CHANGED
@@ -198,5 +198,6 @@ Issue 和 PR 都欢迎。详见 [CONTRIBUTING.md](./CONTRIBUTING.md)。
198
198
 
199
199
  ## 致谢
200
200
 
201
- - 上游 [obra/superpowers](https://github.com/obra/superpowers),作者 [Jesse Vincent](https://blog.fsck.com) 与 [Prime Radiant](https://primeradiant.com),同为 MIT 协议
201
+ - 上游 [obra/superpowers](https://github.com/obra/superpowers)
202
202
  - [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 的三角色插件架构
203
+ - [dsh-plugin-dev](https://github.com/Wenaixi/dsh-plugin-dev) 提供的 DSH 插件开发规范、架构参考与真机验收工程方法
package/lib/client.js CHANGED
@@ -31,6 +31,7 @@ window.__ModuleLoader__.load({
31
31
  var PACKAGE_NAME = '@wenaixi/dsh-superpower';
32
32
  var NS = 'dsh-superpower';
33
33
  var DISABLED_FIELD = 'disabled';
34
+ var LANGUAGE_FIELD = 'language';
34
35
  /* v7.3.0 及之前的双侧禁言表。面板写入成功后会把它们 unset,
35
36
  这里仍需列出:迁移那批操作的路径必须与 schema 里的字段名逐字一致。 */
36
37
  var LEGACY_FIELDS = ['modelDisabled', 'userDisabled'];
@@ -147,17 +148,6 @@ window.__ModuleLoader__.load({
147
148
  return !!dictionary && dictionary[name] === true;
148
149
  }
149
150
 
150
- /**
151
- * 组装一次写入的全部操作:主写入 + 两条清理旧表的 unset。
152
- *
153
- * 清理动作与主写合在同一次 mutate 里,因为 ConfigForm 的 mutate 是一批
154
- * 操作共享一个 revision 栅栏、一次校验、一次持久化决策。这样不存在
155
- * 「新表写了一部分、旧表还留着」的中间态,也省掉每技能一次额外往返。
156
- * op 为 enable 时 unset 路径即恢复继承的默认值。
157
- */
158
- /* 语言偏好字段:'zh' | 'en' 显式固定;缺失时跟随宿主界面语言(locale active)。 */
159
- var LANGUAGE_FIELD = 'language';
160
-
161
151
  /**
162
152
  * 读取当前语言偏好(三态):'zh'/'en' 显式值原样;缺项为 undefined(跟随宿主)。
163
153
  * 与宿主侧 readSwitches 同规则,缺失绝不折叠成 zh —— 否则面板无法表达「跟随宿主」。
@@ -167,86 +157,230 @@ window.__ModuleLoader__.load({
167
157
  return raw === 'zh' || raw === 'en' ? raw : undefined;
168
158
  }
169
159
 
170
- function switchOps(names, op, legacyCarry) {
171
- var list = Array.isArray(names) ? names : [names];
172
- var ops = [];
173
- if (legacyCarry) {
174
- Object.keys(legacyCarry).forEach(function (key) {
175
- if (legacyCarry[key] === true && list.indexOf(key) < 0) {
176
- ops.push({ op: 'set', path: [DISABLED_FIELD, key], value: true });
160
+ /**
161
+ * 客户端配置控制器深度模块 (Deep Module):
162
+ * 将 DSH ConfigForm 订阅、批处理 ops 拍平、旧表原子继承、disposed 拦截与堆栈归集彻底封闭在门后。
163
+ */
164
+ class SwitchPanelModel {
165
+ constructor(scope, options) {
166
+ this.scope = scope;
167
+ this.t = (options && options.t) || ((k) => k);
168
+ this.allNames = (options && options.allNames) || [];
169
+ this.listeners = new Set();
170
+ this.pending = {};
171
+ this.failures = {};
172
+ this.unsubscribe = null;
173
+ this._bindScope(scope);
174
+ }
175
+
176
+ _bindScope(scope) {
177
+ if (this.scope !== scope) {
178
+ if (this.unsubscribe) {
179
+ this.unsubscribe();
180
+ this.unsubscribe = null;
177
181
  }
182
+ this.scope = scope;
183
+ }
184
+ if (this.scope && typeof this.scope.subscribe === 'function' && !this.unsubscribe) {
185
+ this.unsubscribe = this.scope.subscribe(() => this._notify());
186
+ }
187
+ }
188
+
189
+ updateScope(nextScope) {
190
+ this._bindScope(nextScope);
191
+ this._notify();
192
+ }
193
+
194
+ subscribe(listener) {
195
+ this.listeners.add(listener);
196
+ return () => this.listeners.delete(listener);
197
+ }
198
+
199
+ _notify() {
200
+ this.listeners.forEach((fn) => {
201
+ try { fn(); } catch (e) {}
178
202
  });
179
203
  }
180
- list.forEach(function (name) {
181
- if (op === 'disable') {
182
- ops.push({ op: 'set', path: [DISABLED_FIELD, name], value: true });
183
- } else {
184
- ops.push({ op: 'unset', path: [DISABLED_FIELD, name] });
204
+
205
+ getSnapshot() {
206
+ var snap = (this.scope && typeof this.scope.getSnapshot === 'function')
207
+ ? this.scope.getSnapshot()
208
+ : { status: 'loading', writable: false, value: {} };
209
+ var value = snap.value || {};
210
+ var rawDisabled = value[DISABLED_FIELD] || {};
211
+ var legacyActive = Object.keys(rawDisabled).length === 0;
212
+ var disabled = !legacyActive
213
+ ? rawDisabled
214
+ : Object.assign({}, value.modelDisabled || {}, value.userDisabled || {});
215
+ var language = readLanguage(value);
216
+
217
+ return {
218
+ status: snap.status || 'ready',
219
+ writable: snap.writable !== false && snap.mode !== 'memory',
220
+ disabled: disabled,
221
+ legacyActive: legacyActive,
222
+ language: language,
223
+ langConfigured: language !== undefined,
224
+ langValue: language !== undefined ? language : 'auto',
225
+ pending: this.pending,
226
+ failures: this.failures,
227
+ };
228
+ }
229
+
230
+ _buildOps(names, op, legacyCarry) {
231
+ var list = Array.isArray(names) ? names : [names];
232
+ var ops = [];
233
+ if (legacyCarry) {
234
+ Object.keys(legacyCarry).forEach(function (key) {
235
+ if (legacyCarry[key] === true && list.indexOf(key) < 0) {
236
+ ops.push({ op: 'set', path: [DISABLED_FIELD, key], value: true });
237
+ }
238
+ });
185
239
  }
186
- });
187
- return ops.concat(LEGACY_FIELDS.map(function (field) {
188
- return { op: 'unset', path: [field] };
189
- }));
190
- }
240
+ list.forEach(function (name) {
241
+ if (op === 'disable') {
242
+ ops.push({ op: 'set', path: [DISABLED_FIELD, name], value: true });
243
+ } else {
244
+ ops.push({ op: 'unset', path: [DISABLED_FIELD, name] });
245
+ }
246
+ });
247
+ return ops.concat(LEGACY_FIELDS.map(function (field) {
248
+ return { op: 'unset', path: [field] };
249
+ }));
250
+ }
191
251
 
192
- /**
193
- * 提交一批写入并按技能名归集失败原因。settle 后清掉这些技能的忙碌态。
194
- *
195
- * 用户只要在面板动过一次开关,profile 里就只剩 disabled 一个字段。
196
- */
197
- function submit(form, t, names, op, setPending, setFailures, opsOverride, legacyCarry) {
198
- var clear = function (prev) {
199
- var next = {};
200
- Object.keys(prev).forEach(function (key) {
201
- if (names.indexOf(key) < 0) next[key] = prev[key];
252
+ async _submit(names, ops) {
253
+ if (!this.scope || this.scope.disposed || typeof this.scope.mutate !== 'function') {
254
+ var err = new Error(this.t('hostUnavailable'));
255
+ console.error('[dsh-superpower] form.mutate failed:', err);
256
+ this._setFailures(names, this.t('writeFailed') + ': ' + err.message);
257
+ return false;
258
+ }
259
+
260
+ this._setPending(names, true);
261
+ this._clearFailures(names);
262
+
263
+ try {
264
+ var accepted = await this.scope.mutate(ops);
265
+ if (accepted === false) {
266
+ throw new Error(this.t('writeFailed'));
267
+ }
268
+ this._clearFailures(names);
269
+ return true;
270
+ } catch (error) {
271
+ console.error('[dsh-superpower] form.mutate failed:', error);
272
+ var msg = this.t('writeFailed') + ': ' + String(error && error.message ? error.message : error);
273
+ this._setFailures(names, msg);
274
+ return false;
275
+ } finally {
276
+ this._setPending(names, false);
277
+ }
278
+ }
279
+
280
+ _setPending(names, isPending) {
281
+ var next = Object.assign({}, this.pending);
282
+ names.forEach(function (name) {
283
+ if (isPending) next[name] = true;
284
+ else delete next[name];
202
285
  });
203
- return next;
204
- };
205
- setPending(function (prev) {
206
- var next = clear(prev);
207
- names.forEach(function (name) { next[name] = true; });
208
- return next;
209
- });
210
- var ops = opsOverride || switchOps(names, op, legacyCarry);
211
- var work = form.mutate(ops).then(function (accepted) {
212
- if (accepted === false) throw new Error(t('writeFailed'));
213
- });
214
- work.then(function () {
215
- setFailures(clear);
216
- }, function (error) {
217
- console.error('[dsh-superpower] form.mutate failed:', error);
218
- setFailures(function (prev) {
219
- var next = clear(prev);
220
- var message = t('writeFailed') + ': ' + String(error && error.message ? error.message : error);
221
- names.forEach(function (name) { next[name] = message; });
222
- return next;
286
+ this.pending = next;
287
+ this._notify();
288
+ }
289
+
290
+ _setFailures(names, message) {
291
+ var next = Object.assign({}, this.failures);
292
+ names.forEach(function (name) { next[name] = message; });
293
+ this.failures = next;
294
+ this._notify();
295
+ }
296
+
297
+ _clearFailures(names) {
298
+ var next = Object.assign({}, this.failures);
299
+ var changed = false;
300
+ names.forEach(function (name) {
301
+ if (next[name]) {
302
+ delete next[name];
303
+ changed = true;
304
+ }
223
305
  });
224
- }).then(function () {
225
- setPending(clear);
226
- });
306
+ if (changed) {
307
+ this.failures = next;
308
+ this._notify();
309
+ }
310
+ }
311
+
312
+ toggle(name, nextState) {
313
+ var snap = this.getSnapshot();
314
+ var op = nextState ? 'enable' : 'disable';
315
+ var legacyCarry = snap.legacyActive ? snap.disabled : null;
316
+ var ops = this._buildOps([name], op, legacyCarry);
317
+ return this._submit([name], ops);
318
+ }
319
+
320
+ setLanguage(next) {
321
+ var ops = next === 'auto'
322
+ ? [{ op: 'unset', path: [LANGUAGE_FIELD] }]
323
+ : [{ op: 'set', path: [LANGUAGE_FIELD], value: next }];
324
+ return this._submit(['language'], ops);
325
+ }
326
+
327
+ enableAll() {
328
+ var ops = this._buildOps(this.allNames, 'enable');
329
+ return this._submit(this.allNames, ops);
330
+ }
331
+
332
+ disableAll() {
333
+ var ops = this._buildOps(this.allNames, 'disable');
334
+ return this._submit(this.allNames, ops);
335
+ }
336
+
337
+ resetAll() {
338
+ // Q2 裁决:恢复默认纯粹重置技能禁言表,保持语言偏好正交独立
339
+ return this.enableAll();
340
+ }
341
+
342
+ dispose() {
343
+ if (this.unsubscribe) {
344
+ this.unsubscribe();
345
+ this.unsubscribe = null;
346
+ }
347
+ this.listeners.clear();
348
+ }
227
349
  }
228
350
 
229
351
  /**
230
- * 技能开关面板。每个技能一个开关,同时决定模型可见性与用户可调用性。
352
+ * 技能开关面板 (View)。纯渲染视图,完全由 SwitchPanelModel 控制器驱动。
231
353
  *
232
- * props.useSwitches 来自 inject 面绑定好的 ConfigForm 选择器 hook;
233
- * props.actions 是本面板独占的写操作面。
354
+ * props.actions.form 是本面板绑定的 ConfigForm 控制器。
234
355
  */
235
356
  function SkillSwitchPanel(props) {
236
357
  var t = props.t;
237
358
  var form = props.actions.form;
238
- var state = props.useSwitches(function (value) { return value; });
239
359
  var hostActive = props.useLocaleActive(function (v) { return v && v.preference === 'en' ? 'en' : 'zh'; });
360
+ var allNames = SKILL_CATALOG.map(function (skill) { return skill.name; });
361
+
362
+ var modelRef = React.useRef(null);
363
+ if (!modelRef.current) {
364
+ modelRef.current = new SwitchPanelModel(form, { t: t, allNames: allNames });
365
+ } else {
366
+ modelRef.current.updateScope(form);
367
+ }
368
+
369
+ React.useEffect(() => {
370
+ return () => {
371
+ if (modelRef.current) modelRef.current.dispose();
372
+ };
373
+ }, []);
374
+
375
+ var state = React.useSyncExternalStore(
376
+ (listener) => modelRef.current.subscribe(listener),
377
+ () => modelRef.current.getSnapshot()
378
+ );
379
+ var controller = modelRef.current;
240
380
 
241
381
  var keywordState = React.useState('');
242
382
  var keyword = keywordState[0];
243
383
  var setKeyword = keywordState[1];
244
- var pendingState = React.useState({});
245
- var pending = pendingState[0];
246
- var setPending = pendingState[1];
247
- var failuresState = React.useState({});
248
- var failures = failuresState[0];
249
- var setFailures = failuresState[1];
250
384
 
251
385
  if (state.status !== 'ready') {
252
386
  return h('div', { className: 'spSw' },
@@ -254,33 +388,18 @@ window.__ModuleLoader__.load({
254
388
  h('p', { className: 'spSwIntro' }, t('hostUnavailable')));
255
389
  }
256
390
 
257
- var value = state.value || {};
258
- var rawDisabled = value[DISABLED_FIELD] || {};
259
- var legacyActive = Object.keys(rawDisabled).length === 0;
260
- var disabled = !legacyActive
261
- ? rawDisabled
262
- : Object.assign({}, value.modelDisabled || {}, value.userDisabled || {});
263
- var language = readLanguage(value);
264
- var langConfigured = language !== undefined;
265
- var langValue = langConfigured ? language : 'auto';
391
+ var disabled = state.disabled;
392
+ var language = state.language;
393
+ var langConfigured = state.langConfigured;
394
+ var langValue = state.langValue;
266
395
  var langOptions = [
267
396
  { value: 'zh', label: t('langZh') },
268
397
  { value: 'en', label: t('langEn') },
269
398
  { value: 'auto', label: t('langAuto') },
270
399
  ];
271
- var setLang = function (next) {
272
- if (next === 'auto') {
273
- submit(form, t, ['language'], 'enable', setPending, setFailures,
274
- [{ op: 'unset', path: [LANGUAGE_FIELD] }]);
275
- } else {
276
- submit(form, t, ['language'], 'enable', setPending, setFailures,
277
- [{ op: 'set', path: [LANGUAGE_FIELD], value: next }]);
278
- }
279
- };
280
- var writable = state.writable !== false && state.mode !== 'memory';
400
+ var writable = state.writable;
281
401
 
282
402
  var lower = keyword.trim().toLowerCase();
283
- /* 描述取词:按当前语言偏好取对应语言文本,两侧都参与搜索过滤。 */
284
403
  var skillText = function (skill) {
285
404
  var effective = language === undefined ? hostActive : language;
286
405
  return effective === 'en' && skill.descriptionEn ? skill.descriptionEn : skill.description;
@@ -294,17 +413,9 @@ window.__ModuleLoader__.load({
294
413
  || (skill.descriptionEn || '').toLowerCase().indexOf(lower) >= 0;
295
414
  });
296
415
 
297
- var allNames = SKILL_CATALOG.map(function (skill) { return skill.name; });
298
- var batchEnable = function () {
299
- submit(form, t, allNames, 'enable', setPending, setFailures);
300
- };
301
- var batchDisable = function () {
302
- submit(form, t, allNames, 'disable', setPending, setFailures);
303
- };
304
-
305
416
  var rows = shown.map(function (skill) {
306
- var busy = !!pending[skill.name] || !writable;
307
- var failure = failures[skill.name];
417
+ var busy = !!state.pending[skill.name] || !writable;
418
+ var failure = state.failures[skill.name];
308
419
  var on = !isBlocked(disabled, skill.name);
309
420
  return h('li', { key: skill.name, className: 'spSwItem' },
310
421
  h('div', { className: 'spSwMain' },
@@ -319,7 +430,7 @@ window.__ModuleLoader__.load({
319
430
  label: t('invocable') + ' ' + skill.name,
320
431
  title: t('invocableHint'),
321
432
  onChange: function (next) {
322
- submit(form, t, [skill.name], next ? 'enable' : 'disable', setPending, setFailures, null, legacyActive ? disabled : null);
433
+ controller.toggle(skill.name, next);
323
434
  },
324
435
  })));
325
436
  });
@@ -334,13 +445,13 @@ window.__ModuleLoader__.load({
334
445
  label: t('langTitle'),
335
446
  value: langValue,
336
447
  options: langOptions,
337
- disabled: !writable || !!pending['language'],
338
- onChange: setLang,
448
+ disabled: !writable || !!state.pending['language'],
449
+ onChange: function (next) { controller.setLanguage(next); },
339
450
  }),
340
451
  !langConfigured
341
452
  ? h('p', { className: 'spSwLangNote' }, t('langUnsetHint', { lang: t(hostActive === 'en' ? 'langEn' : 'langZh') }))
342
453
  : h('p', { className: 'spSwLangNote' }, t('langNote')),
343
- failures['language'] ? h('span', { className: 'spSwLangFailure', role: 'alert' }, failures['language']) : null),
454
+ state.failures['language'] ? h('span', { className: 'spSwLangFailure', role: 'alert' }, state.failures['language']) : null),
344
455
 
345
456
  h('div', { className: 'spSwBar' },
346
457
  h(P.Button, {
@@ -348,21 +459,21 @@ window.__ModuleLoader__.load({
348
459
  size: 'sm',
349
460
  disabled: !writable,
350
461
  title: t('resetHint'),
351
- onClick: batchEnable,
462
+ onClick: function () { controller.enableAll(); },
352
463
  }, t('enableAll')),
353
464
  h(P.Button, {
354
465
  variant: 'outline',
355
466
  size: 'sm',
356
467
  disabled: !writable,
357
468
  title: t('disableAllHint'),
358
- onClick: batchDisable,
469
+ onClick: function () { controller.disableAll(); },
359
470
  }, t('disableAll')),
360
471
  h(P.Button, {
361
472
  variant: 'ghost',
362
473
  size: 'sm',
363
474
  disabled: !writable,
364
475
  title: t('resetHint'),
365
- onClick: batchEnable,
476
+ onClick: function () { controller.resetAll(); },
366
477
  }, t('resetAll')),
367
478
  h(P.Input, {
368
479
  className: 'spSwSearch',
@@ -421,6 +532,7 @@ window.__ModuleLoader__.load({
421
532
 
422
533
  exports.apply = apply;
423
534
  exports.inject = ['slots', 'configForms', 'locale'];
535
+ exports.SwitchPanelModel = SwitchPanelModel;
424
536
  return module.exports;
425
537
  },
426
538
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wenaixi/dsh-superpower",
3
- "version": "7.6.0",
3
+ "version": "7.7.0",
4
4
  "description": "DeepSeek Harness plugin: 15 obra/superpowers engineering skills, bilingual descriptions, per-skill toggles | DeepSeek Harness 插件:15 个 obra/superpowers 工程纪律技能,技能描述中英双语自由切换,每一个技能本身自由开关",
5
5
  "type": "module",
6
6
  "main": "lib/superpowers.js",
@@ -21,10 +21,11 @@
21
21
  "scripts": {
22
22
  "build": "tsc -p tsconfig.build.json && node scripts/build-client.mjs",
23
23
  "typecheck": "tsc --noEmit",
24
+ "test": "node --test scripts/*.test.mjs",
24
25
  "lint": "eslint src --ext .ts 2>&1 || echo \"(no eslint config — skipped)\"",
25
26
  "prepare": "npm run build",
26
27
  "prepack": "npm run build",
27
- "verify": "node scripts/verify.mjs && node scripts/check-skill-switches.mjs"
28
+ "verify": "pnpm test && node scripts/verify.mjs && node scripts/check-skill-switches.mjs"
28
29
  },
29
30
  "dsh": {
30
31
  "bundle": {