@max-null/dsh-achievements 0.1.0 → 0.1.1

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/client.js CHANGED
@@ -52,16 +52,84 @@ var STRINGS = {
52
52
  desc: "SSiD \u5168\u5BB6\u6876\u8FDB\u5EA6\uFF1A\u5DE5\u5177/\u8BB0\u5FC6/\u5BA1\u8BA1/GenUI \u4F7F\u7528\u6210\u5C31\uFF0C\u89E3\u9501\u53EF\u9886 toast\u3002",
53
53
  refresh: "\u5237\u65B0",
54
54
  locked: "\u672A\u89E3\u9501",
55
- unlocked: "\u5DF2\u89E3\u9501"
55
+ unlocked: "\u5DF2\u89E3\u9501",
56
+ pluginMissing: "\u63D2\u4EF6\u672A\u88C5",
57
+ growing: "\u66F4\u591A\u7EC4\u4EF6\u7684\u6210\u5C31\u6301\u7EED\u589E\u52A0\u4E2D \u2014\u2014 Guardian / Habit / \u6DA6\u8272 / \u5BA1\u67E5\u7B49\u63D2\u4EF6\u5C06\u9646\u7EED\u52A0\u5165\u3002",
58
+ builtin: "\u5185\u7F6E"
56
59
  },
57
60
  en: {
58
61
  title: "Achievements",
59
62
  desc: "SSiD family progress: tool/memory/audit/GenUI usage trophies with unlock toasts.",
60
63
  refresh: "Refresh",
61
64
  locked: "Locked",
62
- unlocked: "Unlocked"
65
+ unlocked: "Unlocked",
66
+ pluginMissing: "plugin not installed",
67
+ growing: "More component achievements are on the way \u2014 Guardian / Habit / Polish / Review and more will join soon.",
68
+ builtin: "built-in"
63
69
  }
64
70
  };
71
+ var PLUGIN_SHORT = {
72
+ "@max-null/dsh-memory": "\u8BB0\u5FC6",
73
+ "dsh-context-doctor": "\u5BA1\u8BA1",
74
+ "@changfenhuang/dsh-genui": "GenUI"
75
+ };
76
+ function pluginLabel(plugin) {
77
+ if (plugin === null) return null;
78
+ return PLUGIN_SHORT[plugin] ?? plugin.split("/").pop() ?? plugin;
79
+ }
80
+ var TROPHY_PATHS = [
81
+ "M6 9H4.5a2.5 2.5 0 0 1 0-5H6",
82
+ "M18 9h1.5a2.5 2.5 0 0 0 0-5H18",
83
+ "M4 22h16",
84
+ "M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20.24 7 22",
85
+ "M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20.24 17 22",
86
+ "M18 2H6v7a6 6 0 0 0 12 0V2Z"
87
+ ];
88
+ var SETTINGS_NAV_MARKER = "data-dsh-achievements-settings-nav";
89
+ var TROPHY_MASK_SVG = encodeURIComponent(
90
+ `<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'>${TROPHY_PATHS.map((d) => `<path d='${d}'/>`).join("")}</svg>`
91
+ ).replace(/'/g, "%27").replace(/\//g, "%2F").replace(/\(/g, "%28").replace(/\)/g, "%29").replace(/#/g, "%23");
92
+ var NAV_ICON_CSS = `
93
+ [data-dsh-achievements-settings-nav] > svg:first-child { display: none; }
94
+ [data-dsh-achievements-settings-nav]::before {
95
+ content: '';
96
+ flex: none;
97
+ width: 16px;
98
+ height: 16px;
99
+ background: currentColor;
100
+ -webkit-mask: url("data:image/svg+xml,${TROPHY_MASK_SVG}") center / contain no-repeat;
101
+ mask: url("data:image/svg+xml,${TROPHY_MASK_SVG}") center / contain no-repeat;
102
+ }
103
+ `;
104
+ var navCssInjected = false;
105
+ function injectNavCss() {
106
+ if (navCssInjected || typeof document === "undefined") return;
107
+ navCssInjected = true;
108
+ const style = document.createElement("style");
109
+ style.setAttribute("data-plugin", "@max-null/dsh-achievements");
110
+ style.textContent = NAV_ICON_CSS;
111
+ document.head.append(style);
112
+ }
113
+ function registerSettingsNavIcon(label) {
114
+ let disposed = false;
115
+ const sync = () => {
116
+ if (disposed) return;
117
+ const currentLabel = label().trim();
118
+ const buttons = document.querySelectorAll('[role="dialog"] nav button');
119
+ for (const button of buttons) {
120
+ const matches = currentLabel.length > 0 && button.textContent?.trim() === currentLabel;
121
+ if (matches) button.setAttribute(SETTINGS_NAV_MARKER, "");
122
+ else button.removeAttribute(SETTINGS_NAV_MARKER);
123
+ }
124
+ };
125
+ sync();
126
+ const observer = new MutationObserver(sync);
127
+ observer.observe(document.body, { subtree: true, childList: true, characterData: true });
128
+ return () => {
129
+ disposed = true;
130
+ observer.disconnect();
131
+ };
132
+ }
65
133
  var GENUI_LS_KEY = "dsh.genui.achievements";
66
134
  function readGenUI() {
67
135
  try {
@@ -103,8 +171,18 @@ function AchievementsView(_props) {
103
171
  (0, import_react.createElement)(
104
172
  "div",
105
173
  { className: "achHead" },
106
- (0, import_react.createElement)("span", { className: "achTitle" }, t.title),
107
- (0, import_react.createElement)("span", { className: "achMeta" }, snapshot !== null ? `${snapshot.unlocked} / ${snapshot.total}` : "\u2026"),
174
+ (0, import_react.createElement)(
175
+ "div",
176
+ { className: "achHeadLeft" },
177
+ (0, import_react.createElement)("span", { className: "achTitle" }, t.title),
178
+ (0, import_react.createElement)("span", { className: "achDesc" }, t.desc)
179
+ ),
180
+ (0, import_react.createElement)(
181
+ "div",
182
+ { className: "achScore", "data-locked": snapshot === null ? void 0 : String(snapshot.unlocked) },
183
+ (0, import_react.createElement)("span", { className: "achScoreNum" }, snapshot !== null ? `${snapshot.unlocked}` : "\u2026"),
184
+ (0, import_react.createElement)("span", { className: "achScoreTotal" }, snapshot !== null ? `/ ${snapshot.total}` : "")
185
+ ),
108
186
  (0, import_react.createElement)("button", {
109
187
  type: "button",
110
188
  className: "achBtn",
@@ -115,7 +193,6 @@ function AchievementsView(_props) {
115
193
  }
116
194
  }, t.refresh)
117
195
  ),
118
- (0, import_react.createElement)("div", { className: "achDesc" }, t.desc),
119
196
  (0, import_react.createElement)(
120
197
  "div",
121
198
  { className: "achCats" },
@@ -126,13 +203,23 @@ function AchievementsView(_props) {
126
203
  onClick: () => setCategory(c)
127
204
  }, c))
128
205
  ),
206
+ (0, import_react.createElement)(
207
+ "div",
208
+ { className: "achGrowing" },
209
+ (0, import_react.createElement)("span", { className: "achGrowingIcon", "aria-hidden": "" }, "\u{1F9E9}"),
210
+ (0, import_react.createElement)("span", null, t.growing)
211
+ ),
129
212
  (0, import_react.createElement)(
130
213
  "div",
131
214
  { className: "achList" },
132
215
  items.map((a) => (0, import_react.createElement)(
133
216
  "div",
134
- { key: a.id, className: `achRow${a.locked ? "" : " achRowOn"}` },
135
- (0, import_react.createElement)("span", { className: "achIcon", "aria-hidden": "" }, a.icon),
217
+ { key: a.id, className: `achRow${a.unlocked ? " achRowOn" : ""}${a.pluginInstalled ? "" : " achRowMuted"}` },
218
+ (0, import_react.createElement)(
219
+ "span",
220
+ { className: `achIconWrap achIconWrap-${a.rarity}`, "aria-hidden": "" },
221
+ (0, import_react.createElement)("span", { className: "achIcon" }, a.icon)
222
+ ),
136
223
  (0, import_react.createElement)(
137
224
  "div",
138
225
  { className: "achBody" },
@@ -144,7 +231,8 @@ function AchievementsView(_props) {
144
231
  "span",
145
232
  { className: `achRarity achRarity-${a.rarity}` },
146
233
  a.rarity === "legendary" ? "\u4F20\u8BF4" : a.rarity === "epic" ? "\u53F2\u8BD7" : a.rarity === "rare" ? "\u7A00\u6709" : "\u666E\u901A"
147
- )
234
+ ),
235
+ a.plugin !== null ? (0, import_react.createElement)("span", { className: "achPlugin" }, pluginLabel(a.plugin)) : null
148
236
  ),
149
237
  (0, import_react.createElement)("div", { className: "achDesc2" }, a.desc),
150
238
  (0, import_react.createElement)(
@@ -161,7 +249,9 @@ function AchievementsView(_props) {
161
249
  (0, import_react.createElement)("span", { className: "achProgNum" }, `${a.progress.current} / ${a.progress.target}`)
162
250
  )
163
251
  ),
164
- (0, import_react.createElement)("span", { className: `achState${a.locked ? "" : " achStateOn"}` }, a.locked ? t.locked : t.unlocked)
252
+ (0, import_react.createElement)("span", {
253
+ className: `achState${a.pluginInstalled ? a.unlocked ? " achStateOn" : "" : " achStateOff"}`
254
+ }, a.pluginInstalled ? a.unlocked ? t.unlocked : t.locked : t.pluginMissing)
165
255
  ))
166
256
  ),
167
257
  note !== "" ? (0, import_react.createElement)("div", { className: "achNote" }, note) : null
@@ -206,46 +296,73 @@ function apply(ctx) {
206
296
  label: () => STRINGS[lang()].title
207
297
  }, () => (0, import_react.createElement)(AchievementsView, { visible: true })));
208
298
  disposers.push(mountToastLayer());
299
+ injectNavCss();
300
+ disposers.push(registerSettingsNavIcon(() => STRINGS[lang()].title));
209
301
  return () => {
210
302
  for (const dispose of disposers) dispose();
211
303
  };
212
304
  }
213
- if (typeof document !== "undefined" && document.querySelector("style[data-dsh-achievements-css]") === null) {
214
- const tag = document.createElement("style");
215
- tag.dataset.dshAchievementsCss = "1";
216
- tag.textContent = CSS;
217
- document.head.appendChild(tag);
218
- }
219
305
  var CSS = [
220
- ".ach{display:flex;flex-direction:column;gap:10px;padding:4px 0}",
221
- ".achHead{display:flex;align-items:center;gap:10px}",
222
- ".achTitle{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary,inherit)}",
223
- ".achMeta{font-size:13px;color:var(--dsw-alias-label-secondary,inherit)}",
224
- ".achBtn{margin-left:auto;padding:4px 12px;border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.4));border-radius:8px;background:transparent;color:var(--dsw-alias-label-primary,inherit);font-size:12px;cursor:pointer}",
225
- ".achDesc{font-size:12px;line-height:1.5;color:var(--dsw-alias-label-tertiary,inherit)}",
306
+ /* ---- 视图容器 ---- */
307
+ ".ach{display:flex;flex-direction:column;gap:14px;padding:8px 0 4px}",
308
+ /* ---- 头部:标题+进度徽章+刷新 ---- */
309
+ ".achHead{display:flex;align-items:center;gap:14px}",
310
+ ".achHeadLeft{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}",
311
+ ".achTitle{font-size:16px;font-weight:600;letter-spacing:.01em;color:var(--dsw-alias-label-primary,inherit)}",
312
+ ".achDesc{font-size:12px;line-height:1.6;color:var(--dsw-alias-label-tertiary,inherit)}",
313
+ ".achScore{flex:none;display:flex;align-items:baseline;gap:2px;padding:8px 14px;border-radius:14px;background:color-mix(in srgb,var(--dsw-alias-state-business-primary,#4f8ef7) 10%,transparent);border:1px solid color-mix(in srgb,var(--dsw-alias-state-business-primary,#4f8ef7) 25%,transparent)}",
314
+ ".achScoreNum{font-size:20px;font-weight:700;color:var(--dsw-alias-state-business-primary,#4f8ef7);font-variant-numeric:tabular-nums}",
315
+ ".achScoreTotal{font-size:13px;color:var(--dsw-alias-label-secondary,inherit);font-variant-numeric:tabular-nums}",
316
+ ".achBtn{flex:none;padding:5px 14px;border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.4));border-radius:999px;background:transparent;color:var(--dsw-alias-label-secondary,inherit);font-size:12px;cursor:pointer;transition:background .15s}",
317
+ ".achBtn:hover{background:var(--dsw-alias-fill-hover,rgba(127,127,127,.12))}",
318
+ /* ---- 类别 tabs ---- */
226
319
  ".achCats{display:flex;flex-wrap:wrap;gap:6px}",
227
- ".achCat{padding:3px 10px;border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.4));border-radius:999px;background:transparent;color:var(--dsw-alias-label-secondary,inherit);font-size:12px;cursor:pointer}",
228
- ".achCatOn{border-color:var(--dsw-alias-state-business-primary,#4f8ef7);color:var(--dsw-alias-state-business-primary,#4f8ef7)}",
229
- ".achList{display:flex;flex-direction:column;gap:8px;max-height:420px;overflow:auto}",
230
- ".achRow{display:flex;align-items:center;gap:10px;padding:10px 12px;border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.25));border-radius:12px}",
231
- ".achRowOn{border-color:color-mix(in srgb,var(--dsw-alias-state-business-primary,#4f8ef7) 35%,transparent)}",
232
- ".achIcon{font-size:20px;line-height:1}",
233
- ".achBody{flex:1;min-width:0}",
320
+ ".achCat{padding:3px 11px;border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.35));border-radius:999px;background:transparent;color:var(--dsw-alias-label-secondary,inherit);font-size:12px;line-height:1.6;cursor:pointer;transition:all .15s}",
321
+ ".achCat:hover{color:var(--dsw-alias-label-primary,inherit)}",
322
+ ".achCatOn{border-color:var(--dsw-alias-state-business-primary,#4f8ef7);color:var(--dsw-alias-state-business-primary,#4f8ef7);background:color-mix(in srgb,var(--dsw-alias-state-business-primary,#4f8ef7) 10%,transparent)}",
323
+ /* ---- 成就列表 ---- */
324
+ ".achList{display:flex;flex-direction:column;gap:8px;max-height:480px;overflow:auto;padding-right:2px}",
325
+ ".achRow{display:flex;align-items:center;gap:12px;padding:10px 12px;border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.22));border-radius:14px;background:var(--dsw-alias-bg-layer-2,transparent);transition:border-color .15s}",
326
+ ".achRowOn{border-color:color-mix(in srgb,var(--dsw-alias-state-business-primary,#4f8ef7) 40%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-business-primary,#4f8ef7) 6%,transparent)}",
327
+ /* icon 圆底(稀有度 tint) */
328
+ ".achIconWrap{flex:none;width:40px;height:40px;display:flex;align-items:center;justify-content:center;border-radius:12px;font-size:20px;line-height:1;background:color-mix(in srgb,var(--dsw-alias-state-business-primary,#4f8ef7) 12%,transparent);border:1px solid color-mix(in srgb,var(--dsw-alias-state-business-primary,#4f8ef7) 18%,transparent)}",
329
+ ".achIconWrap-rare{background:color-mix(in srgb,#f59e0b 12%,transparent);border-color:color-mix(in srgb,#f59e0b 20%,transparent)}",
330
+ ".achIconWrap-epic{background:color-mix(in srgb,#a78bfa 12%,transparent);border-color:color-mix(in srgb,#a78bfa 20%,transparent)}",
331
+ ".achIconWrap-legendary{background:color-mix(in srgb,#f43f5e 12%,transparent);border-color:color-mix(in srgb,#f43f5e 20%,transparent)}",
332
+ ".achIcon{filter:saturate(1.05)}",
333
+ /* 行正文 */
334
+ ".achBody{flex:1;min-width:0;display:flex;flex-direction:column;gap:3px}",
234
335
  ".achName{display:flex;align-items:center;gap:8px;font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary,inherit)}",
235
- ".achRarity{font-size:11px;padding:0 6px;border-radius:6px;background:color-mix(in srgb,var(--dsw-alias-state-business-primary,#4f8ef7) 14%,transparent);color:var(--dsw-alias-state-business-primary,#4f8ef7)}",
236
- ".achRarity-rare{background:color-mix(in srgb,#f59e0b 14%,transparent);color:#f59e0b}",
237
- ".achRarity-epic{background:color-mix(in srgb,#a78bfa 14%,transparent);color:#a78bfa}",
238
- ".achRarity-legendary{background:color-mix(in srgb,#f43f5e 14%,transparent);color:#f43f5e}",
336
+ ".achRarity{font-size:10px;line-height:1.6;padding:0 7px;border-radius:999px;background:color-mix(in srgb,var(--dsw-alias-state-business-primary,#4f8ef7) 12%,transparent);color:var(--dsw-alias-state-business-primary,#4f8ef7);font-weight:500}",
337
+ ".achRarity-rare{background:color-mix(in srgb,#f59e0b 12%,transparent);color:#d97706}",
338
+ ".achRarity-epic{background:color-mix(in srgb,#a78bfa 12%,transparent);color:#7c3aed}",
339
+ ".achRarity-legendary{background:color-mix(in srgb,#f43f5e 12%,transparent);color:#e11d48}",
340
+ /* 所属插件标签 */
341
+ ".achPlugin{font-size:10px;line-height:1.6;padding:0 7px;border-radius:999px;background:var(--dsw-alias-fill-hover,rgba(127,127,127,.12));color:var(--dsw-alias-label-secondary,inherit);font-weight:500;border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.25))}",
342
+ /* 成就持续增加中提示卡 */
343
+ ".achGrowing{display:flex;align-items:flex-start;gap:10px;padding:10px 12px;border:1px dashed var(--dsw-alias-border-l2,rgba(127,127,127,.35));border-radius:12px;background:var(--dsw-alias-bg-layer-1,transparent);font-size:12px;line-height:1.6;color:var(--dsw-alias-label-secondary,inherit)}",
344
+ ".achGrowingIcon{flex:none;font-size:15px;line-height:1.4}",
345
+ /* 插件未装置灰 */
346
+ ".achRowMuted{opacity:.55;filter:grayscale(.6)}",
347
+ ".achStateOff{color:var(--dsw-alias-label-tertiary,inherit);background:var(--dsw-alias-fill-hover,rgba(127,127,127,.08))}",
239
348
  ".achDesc2{font-size:12px;line-height:1.5;color:var(--dsw-alias-label-secondary,inherit)}",
240
- ".achProgRow{display:flex;align-items:center;gap:8px;margin-top:4px}",
241
- ".achProg{flex:1;height:4px;border-radius:999px;background:var(--dsw-alias-border-l1,rgba(127,127,127,.3));overflow:hidden}",
242
- ".achProgFill{height:100%;background:var(--dsw-alias-state-business-primary,#4f8ef7)}",
243
- ".achProgNum{font-size:11px;color:var(--dsw-alias-label-tertiary,inherit)}",
244
- ".achState{flex:none;font-size:11px;color:var(--dsw-alias-label-tertiary,inherit)}",
245
- ".achStateOn{color:var(--dsw-alias-state-success-primary,#6ccb5f)}",
246
- ".achNote{font-size:12px;color:var(--dsw-alias-state-success-primary,#6ccb5f)}",
349
+ ".achProgRow{display:flex;align-items:center;gap:8px;margin-top:2px}",
350
+ ".achProg{flex:1;height:5px;border-radius:999px;background:var(--dsw-alias-border-l1,rgba(127,127,127,.28));overflow:hidden}",
351
+ ".achProgFill{height:100%;border-radius:999px;background:var(--dsw-alias-state-business-primary,#4f8ef7);transition:width .3s}",
352
+ ".achProgNum{flex:none;font-size:11px;color:var(--dsw-alias-label-tertiary,inherit);font-variant-numeric:tabular-nums}",
353
+ /* 状态徽章 */
354
+ ".achState{flex:none;font-size:11px;line-height:1.6;padding:0 8px;border-radius:999px;color:var(--dsw-alias-label-tertiary,inherit);background:var(--dsw-alias-fill-hover,rgba(127,127,127,.1))}",
355
+ ".achStateOn{color:var(--dsw-alias-state-success-primary,#16a34a);background:color-mix(in srgb,var(--dsw-alias-state-success-primary,#16a34a) 12%,transparent)}",
356
+ ".achNote{font-size:12px;color:var(--dsw-alias-state-success-primary,#16a34a)}",
357
+ /* toast */
247
358
  ".at-row{position:fixed;right:16px;bottom:16px;z-index:9999;width:300px;padding:10px 12px;border:1px solid var(--dsw-alias-state-business-primary,#4f8ef7);border-radius:12px;background:var(--dsw-alias-bg-layer-2,rgba(0,0,0,.85));color:var(--dsw-alias-state-business-primary,#4f8ef7);font-size:13px;box-shadow:0 8px 24px rgba(0,0,0,.25)}"
248
359
  ].join("");
360
+ if (typeof document !== "undefined" && document.querySelector("style[data-dsh-achievements-css]") === null) {
361
+ const tag = document.createElement("style");
362
+ tag.dataset.dshAchievementsCss = "1";
363
+ tag.textContent = CSS;
364
+ document.head.appendChild(tag);
365
+ }
249
366
  return module.exports;
250
367
  },
251
368
  });
package/dist/engine.d.ts CHANGED
@@ -1,5 +1,9 @@
1
1
  import type { Context } from '@deepseek-ai/cordis';
2
2
  import { type AchieveState } from './rules.ts';
3
+ /** 有成就关联的插件(探测存在性以置灰未装插件的成就)。 */
4
+ export declare const ASSOCIATED_PLUGINS: readonly string[];
5
+ /** 探测已安装的关联插件:SSID_PROFILE_DIR(壳注入)与常见 profile 目录。 */
6
+ export declare function installedPlugins(): Set<string>;
3
7
  export interface EngineSnapshot {
4
8
  state: AchieveState;
5
9
  distinct: Record<string, number>;
package/dist/engine.js CHANGED
@@ -5,7 +5,7 @@
5
5
  * 隐私纪律:只读叶级标量(工具名/成功标志/session 事件类型/token 数/
6
6
  * 当天时刻),绝不读消息正文/文件内容/错误详情。
7
7
  */
8
- import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
8
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
9
9
  import { homedir } from 'node:os';
10
10
  import { join } from 'node:path';
11
11
  import { defineTool } from '@deepseek-ai/dsh-tools';
@@ -101,6 +101,28 @@ const MEMORY_TOOLS = new Set([
101
101
  const CONFIRM_TOOLS = new Set(['memory_confirm']);
102
102
  const TEMPLATE_TOOLS = new Set(['prompt_search', 'prompt_get', 'prompt_list', 'prompt_add']);
103
103
  const AUDIT_TOOLS = new Set(['context_audit']);
104
+ /** 有成就关联的插件(探测存在性以置灰未装插件的成就)。 */
105
+ export const ASSOCIATED_PLUGINS = [
106
+ '@max-null/dsh-memory',
107
+ 'dsh-context-doctor',
108
+ '@changfenhuang/dsh-genui',
109
+ ];
110
+ /** 探测已安装的关联插件:SSID_PROFILE_DIR(壳注入)与常见 profile 目录。 */
111
+ export function installedPlugins() {
112
+ const candidates = [
113
+ process.env.SSID_PROFILE_DIR,
114
+ process.env.DSH_HOME ? join(process.env.DSH_HOME, 'profiles', 'ssid') : undefined,
115
+ join(homedir(), '.dsh', 'profiles', 'ssid'),
116
+ ].filter((dir) => typeof dir === 'string' && dir !== '');
117
+ const found = new Set();
118
+ for (const dir of candidates) {
119
+ for (const name of ASSOCIATED_PLUGINS) {
120
+ if (existsSync(join(dir, 'node_modules', ...name.split('/'))))
121
+ found.add(name);
122
+ }
123
+ }
124
+ return found;
125
+ }
104
126
  /** 引擎(带事件源的宿主注入)。 */
105
127
  export class AchievementsEngine {
106
128
  store = new StateStore();
@@ -169,8 +191,9 @@ export class AchievementsEngine {
169
191
  return queue;
170
192
  }
171
193
  fullSnapshot() {
194
+ const installed = installedPlugins();
172
195
  return {
173
- ...snapshot(this.count, this.store.unlockMap),
196
+ ...snapshot(this.count, this.store.unlockMap, installed),
174
197
  recent: this.snapshot().recent,
175
198
  distinct: this.store.distinctCounts,
176
199
  };
package/dist/rules.d.ts CHANGED
@@ -21,6 +21,8 @@ export interface AchievementDef {
21
21
  counter: string;
22
22
  target: number;
23
23
  };
24
+ /** 所属插件(未装对应插件时成就置灰);null = 引擎内置(工具/行为/隐藏)。 */
25
+ plugin: string | null;
24
26
  }
25
27
  /** 计数键(leaf scalars only)。 */
26
28
  export declare const KEYS: {
@@ -49,7 +51,7 @@ export interface AchieveState {
49
51
  export declare function emptyState(): AchieveState;
50
52
  /** 检查新解锁(不看 hidden——hidden 仅影响展示)。 */
51
53
  export declare function checkUnlocks(state: AchieveState, unlocked: Record<string, number>): AchievementDef[];
52
- /** 视图行(client 渲染用)。 */
53
- export declare function viewOf(achievement: AchievementDef, state: AchieveState, unlocked: Record<string, number>): Record<string, unknown>;
54
+ /** 视图行(client 渲染用;pluginInstalled 由 host 探测注入)。 */
55
+ export declare function viewOf(achievement: AchievementDef, state: AchieveState, unlocked: Record<string, number>, pluginInstalled?: boolean): Record<string, unknown>;
54
56
  /** 快照(HTTP / 工具共用)。 */
55
- export declare function snapshot(state: AchieveState, unlocked: Record<string, number>): Record<string, unknown>;
57
+ export declare function snapshot(state: AchieveState, unlocked: Record<string, number>, installed?: Set<string>): Record<string, unknown>;
package/dist/rules.js CHANGED
@@ -27,31 +27,50 @@ export const KEYS = {
27
27
  /** SSiD 全家桶成就表。 */
28
28
  export const ACHIEVEMENTS = [
29
29
  // ── 启程 ──
30
- { id: 'first-session', name: '启程', desc: '第一个会话开跑。', icon: '🚀', category: '启程', rarity: 'common', threshold: { counter: KEYS.sessions, target: 1 } },
31
- { id: 'first-turn', name: '初试身手', desc: '完成第一轮对话。', icon: '👋', category: '启程', rarity: 'common', threshold: { counter: KEYS.turns, target: 1 } },
32
- { id: 'first-tool', name: '工具初体验', desc: '第一次调用工具。', icon: '🛠️', category: '启程', rarity: 'common', threshold: { counter: KEYS.tools, target: 1 } },
30
+ { id: 'first-session', name: '启程', desc: '第一个会话开跑。', icon: '🚀', category: '启程', rarity: 'common', threshold: { counter: KEYS.sessions, target: 1 },
31
+ plugin: null },
32
+ { id: 'first-turn', name: '初试身手', desc: '完成第一轮对话。', icon: '👋', category: '启程', rarity: 'common', threshold: { counter: KEYS.turns, target: 1 },
33
+ plugin: null },
34
+ { id: 'first-tool', name: '工具初体验', desc: '第一次调用工具。', icon: '🛠️', category: '启程', rarity: 'common', threshold: { counter: KEYS.tools, target: 1 },
35
+ plugin: null },
33
36
  // ── 记忆 ──
34
- { id: 'memory-first-save', name: '落笔成忆', desc: '第一次使用记忆工具(memory_*)。', icon: '📝', category: '记忆', rarity: 'common', threshold: { counter: KEYS.memoryTools, target: 1 } },
35
- { id: 'memory-10', name: '记忆织工', desc: '累计 10 次记忆工具调用。', icon: '🧵', category: '记忆', rarity: 'rare', threshold: { counter: KEYS.memoryTools, target: 10 } },
36
- { id: 'memory-first-confirm', name: '审核人', desc: '第一次人工确认(memory_confirm)。', icon: '✅', category: '记忆', rarity: 'rare', threshold: { counter: KEYS.memoryConfirmed, target: 1 } },
37
- { id: 'template-user', name: '模板常客', desc: '第一次使用模板库工具(prompt_*)。', icon: '🗂️', category: '记忆', rarity: 'rare', threshold: { counter: KEYS.templateTools, target: 1 } },
37
+ { id: 'memory-first-save', name: '落笔成忆', desc: '第一次使用记忆工具(memory_*)。', icon: '📝', category: '记忆', rarity: 'common', threshold: { counter: KEYS.memoryTools, target: 1 },
38
+ plugin: '@max-null/dsh-memory' },
39
+ { id: 'memory-10', name: '记忆织工', desc: '累计 10 次记忆工具调用。', icon: '🧵', category: '记忆', rarity: 'rare', threshold: { counter: KEYS.memoryTools, target: 10 },
40
+ plugin: '@max-null/dsh-memory' },
41
+ { id: 'memory-first-confirm', name: '审核人', desc: '第一次人工确认(memory_confirm)。', icon: '✅', category: '记忆', rarity: 'rare', threshold: { counter: KEYS.memoryConfirmed, target: 1 },
42
+ plugin: '@max-null/dsh-memory' },
43
+ { id: 'template-user', name: '模板常客', desc: '第一次使用模板库工具(prompt_*)。', icon: '🗂️', category: '记忆', rarity: 'rare', threshold: { counter: KEYS.templateTools, target: 1 },
44
+ plugin: '@max-null/dsh-memory' },
38
45
  // ── 审计 ──
39
- { id: 'audit-first', name: '上下文审计官', desc: '第一次跑 context_audit。', icon: '🔍', category: '审计', rarity: 'rare', threshold: { counter: KEYS.audits, target: 1 } },
40
- { id: 'audit-10', name: '注审老手', desc: '累计 10 次上下文审计。', icon: '🕵️', category: '审计', rarity: 'epic', threshold: { counter: KEYS.audits, target: 10 } },
46
+ { id: 'audit-first', name: '上下文审计官', desc: '第一次跑 context_audit。', icon: '🔍', category: '审计', rarity: 'rare', threshold: { counter: KEYS.audits, target: 1 },
47
+ plugin: 'dsh-context-doctor' },
48
+ { id: 'audit-10', name: '注审老手', desc: '累计 10 次上下文审计。', icon: '🕵️', category: '审计', rarity: 'epic', threshold: { counter: KEYS.audits, target: 10 },
49
+ plugin: 'dsh-context-doctor' },
41
50
  // ── GenUI(client 侧合并 genui 计数)──
42
- { id: 'genui-first', name: '第一块面板', desc: 'GenUI 渲染了第一块界面。', icon: '🎨', category: 'GenUI', rarity: 'common', threshold: { counter: KEYS.genuiUnlocked, target: 1 } },
43
- { id: 'genui-5', name: '界面收藏家', desc: 'GenUI 解锁 5 个成就。', icon: '🖼️', category: 'GenUI', rarity: 'rare', threshold: { counter: KEYS.genuiUnlocked, target: 5 } },
44
- { id: 'genui-25', name: '界面编织者', desc: 'GenUI 渲染过 25 个界面。', icon: '🧶', category: 'GenUI', rarity: 'epic', threshold: { counter: KEYS.genuiFences, target: 25 } },
51
+ { id: 'genui-first', name: '第一块面板', desc: 'GenUI 渲染了第一块界面。', icon: '🎨', category: 'GenUI', rarity: 'common', threshold: { counter: KEYS.genuiUnlocked, target: 1 },
52
+ plugin: '@changfenhuang/dsh-genui' },
53
+ { id: 'genui-5', name: '界面收藏家', desc: 'GenUI 解锁 5 个成就。', icon: '🖼️', category: 'GenUI', rarity: 'rare', threshold: { counter: KEYS.genuiUnlocked, target: 5 },
54
+ plugin: '@changfenhuang/dsh-genui' },
55
+ { id: 'genui-25', name: '界面编织者', desc: 'GenUI 渲染过 25 个界面。', icon: '🧶', category: 'GenUI', rarity: 'epic', threshold: { counter: KEYS.genuiFences, target: 25 },
56
+ plugin: '@changfenhuang/dsh-genui' },
45
57
  // ── 工具 ──
46
- { id: 'tool-10', name: '工具新手', desc: '累计调用 10 次工具。', icon: '🔧', category: '工具', rarity: 'common', threshold: { counter: KEYS.tools, target: 10 } },
47
- { id: 'tool-50', name: '工具达人', desc: '累计调用 50 次工具。', icon: '⚙️', category: '工具', rarity: 'rare', threshold: { counter: KEYS.tools, target: 50 } },
48
- { id: 'five-tools', name: '多面手', desc: '使用过 5 种不同工具。', icon: '🧰', category: '工具', rarity: 'rare', threshold: { counter: KEYS.toolsUsed, target: 5 } },
58
+ { id: 'tool-10', name: '工具新手', desc: '累计调用 10 次工具。', icon: '🔧', category: '工具', rarity: 'common', threshold: { counter: KEYS.tools, target: 10 },
59
+ plugin: null },
60
+ { id: 'tool-50', name: '工具达人', desc: '累计调用 50 次工具。', icon: '⚙️', category: '工具', rarity: 'rare', threshold: { counter: KEYS.tools, target: 50 },
61
+ plugin: null },
62
+ { id: 'five-tools', name: '多面手', desc: '使用过 5 种不同工具。', icon: '🧰', category: '工具', rarity: 'rare', threshold: { counter: KEYS.toolsUsed, target: 5 },
63
+ plugin: null },
49
64
  // ── 行为 ──
50
- { id: 'marathon', name: '马拉松', desc: '单回合内调用 10 次工具。', icon: '🏃', category: '行为', rarity: 'rare', hidden: true, threshold: { counter: KEYS.marathonTurns, target: 1 } },
51
- { id: 'night-owl', name: '夜猫子', desc: '在凌晨 0-5 点发过消息。', icon: '🦉', category: '行为', rarity: 'rare', hidden: true, threshold: { counter: KEYS.midnightTurns, target: 1 } },
65
+ { id: 'marathon', name: '马拉松', desc: '单回合内调用 10 次工具。', icon: '🏃', category: '行为', rarity: 'rare', hidden: true, threshold: { counter: KEYS.marathonTurns, target: 1 },
66
+ plugin: null },
67
+ { id: 'night-owl', name: '夜猫子', desc: '在凌晨 0-5 点发过消息。', icon: '🦉', category: '行为', rarity: 'rare', hidden: true, threshold: { counter: KEYS.midnightTurns, target: 1 },
68
+ plugin: null },
52
69
  // ── 隐藏 ──
53
- { id: 'self-ref', name: '自我指涉', desc: '用成就工具查询自己。', icon: '🪞', category: '隐藏', rarity: 'rare', hidden: true, threshold: { counter: KEYS.selfQueries, target: 1 } },
54
- { id: 'billionaire', name: '亿万富翁', desc: '累计消耗一亿 token。', icon: '💰', category: '隐藏', rarity: 'legendary', hidden: true, threshold: { counter: KEYS.tokens, target: 100000000 } },
70
+ { id: 'self-ref', name: '自我指涉', desc: '用成就工具查询自己。', icon: '🪞', category: '隐藏', rarity: 'rare', hidden: true, threshold: { counter: KEYS.selfQueries, target: 1 },
71
+ plugin: null },
72
+ { id: 'billionaire', name: '亿万富翁', desc: '累计消耗一亿 token。', icon: '💰', category: '隐藏', rarity: 'legendary', hidden: true, threshold: { counter: KEYS.tokens, target: 100000000 },
73
+ plugin: null },
55
74
  ];
56
75
  export function emptyState() {
57
76
  const state = {};
@@ -64,8 +83,8 @@ export function checkUnlocks(state, unlocked) {
64
83
  return ACHIEVEMENTS.filter(achievement => unlocked[achievement.id] === undefined
65
84
  && (state[achievement.threshold.counter] ?? 0) >= achievement.threshold.target);
66
85
  }
67
- /** 视图行(client 渲染用)。 */
68
- export function viewOf(achievement, state, unlocked) {
86
+ /** 视图行(client 渲染用;pluginInstalled 由 host 探测注入)。 */
87
+ export function viewOf(achievement, state, unlocked, pluginInstalled = true) {
69
88
  const current = Math.min(state[achievement.threshold.counter] ?? 0, achievement.threshold.target);
70
89
  return {
71
90
  id: achievement.id,
@@ -77,13 +96,15 @@ export function viewOf(achievement, state, unlocked) {
77
96
  hidden: achievement.hidden === true,
78
97
  unlocked: unlocked[achievement.id] !== undefined,
79
98
  progress: { current, target: achievement.threshold.target },
99
+ plugin: achievement.plugin,
100
+ pluginInstalled: achievement.plugin === null || pluginInstalled,
80
101
  };
81
102
  }
82
103
  /** 快照(HTTP / 工具共用)。 */
83
- export function snapshot(state, unlocked) {
104
+ export function snapshot(state, unlocked, installed = new Set()) {
84
105
  return {
85
106
  total: ACHIEVEMENTS.length,
86
107
  unlocked: Object.keys(unlocked).length,
87
- achievements: ACHIEVEMENTS.map(achievement => viewOf(achievement, state, unlocked)),
108
+ achievements: ACHIEVEMENTS.map(achievement => viewOf(achievement, state, unlocked, achievement.plugin === null || installed.has(achievement.plugin))),
88
109
  };
89
110
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@max-null/dsh-achievements",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "SSiD family progress achievements — listens to session/tool events, folds activity into durable counters, unlocks trophies with toasts and an achievements page; the GenUI category merges the genui plugin's own counter data.",
5
5
  "license": "MIT",
6
6
  "type": "module",