dsh-plugin-t-expert 0.4.53 → 0.4.58

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/CHANGELOG.md CHANGED
@@ -16,7 +16,140 @@
16
16
  > 发布时**不要再跑 `npm version`**(那会把已提交好的版本又升一级,发出去的号与 CHANGELOG、tag 就对不上),
17
17
  > 只补一个 annotated tag(`git tag -a`,因为 `git push --follow-tags` 只推带注释的 tag)。
18
18
 
19
- ## [0.4.53] — 未发布
19
+ ## [0.4.58] — 未发布
20
+
21
+ **面板的 ☁️ 标识改成可点的下载入口** —— 之前它只是个 `<span>`,注释却写着「点选与召唤都会自动下载」。
22
+
23
+ ### 现象
24
+
25
+ 用户反馈:「部分专家有头像、部分怎么点都没有」。
26
+
27
+ ### 根因
28
+
29
+ `warmupAssets`(下载预热)只挂在两处:
30
+
31
+ - `@` 弹窗的 `pick()`(`summon.jsx`)
32
+ - 输入框 `@` 菜单的 `onPick()`(`index.jsx`)
33
+
34
+ **面板列表(`settings.jsx`)完全没有触发点**。于是规律很清楚:
35
+
36
+ - **召唤过 / 在 @ 弹窗选过**的专家 → 附件已下载 → 有头像 ✓
37
+ - **只在面板里看过**的专家 → 永远不触发下载 → 永远 emoji ✗
38
+
39
+ 而面板那个 ☁️ 标识本身是个 `<span>`(注释写着「只是个标识、不是按钮」),点它同样没用。
40
+
41
+ ### 修复
42
+
43
+ 把 ☁️ 改成 `<button>`,点击即 `warmupAssets(remote, expert)`;点完立刻收起标识
44
+ (下载在后台跑,不给即时反馈会让人以为没生效)。CSS 加 `.t-team-cloud` 抹掉按钮默认外观。
45
+
46
+ ### 教训
47
+
48
+ **同一个动作(下载预热)在三个入口里只挂了两处** —— 注释写的是「点选与召唤都会自动下载」,
49
+ 但面板那条路径压根没实现。写「A 和 B 都会做 X」这类注释时,得真的去核对 B。
50
+
51
+
52
+
53
+ **清理残留的诊断 alert** —— 0.4.55 那次「移除诊断」的字符串替换没匹配上,`alert` 留到了源码里。
54
+
55
+ - `settings.jsx` 删除 `alert('[头像诊断]…')` 残留(否则每次取头像都会弹窗)。
56
+ - 更新过时注释:头像 effect 的依赖早已改成 `[avatarRev]`,注释却还写着「依赖用 [](只跑一次)」。
57
+
58
+ ### 教训
59
+
60
+ 用字符串替换「删除调试代码」不可靠 —— 匹配串与实际代码对不上时会**静默失败**,
61
+ 留下一段每次执行都弹窗的代码。删调试代码后要 `grep` 全库确认,而不是只看替换的返回。
62
+
63
+
64
+
65
+ **点选专家下载完头像后,面板自动显示头像**(之前必须重开面板)。
66
+
67
+ ### 现象
68
+
69
+ 用户实测:点选专家 → 头像确实下载到了本地 → **但面板还是 emoji,要重开面板/重启才看得到**。
70
+
71
+ ### 原因
72
+
73
+ `warmupAssets()`(点选预热)下载完成时只做了 `refresh(remote, true)` —— **刷新名册**;
74
+ 而**头像是在面板/弹窗挂载时单独取一次的**(`getAvatars`)—— 两者互不知情。
75
+ 结果名册刷新了、`assets` 状态更新了,但头像那份 state 还是旧的。
76
+
77
+ ### 修复
78
+
79
+ `warmupAssets` 完成后额外广播 `t-team:avatars-changed`;面板与 `@` 弹窗各加一个监听,
80
+ 收到就把 `avatarRev` +1,带动取头像的 effect 重跑。
81
+
82
+ ### 教训
83
+
84
+ **同一份数据被两处独立取用时,刷新必须一起刷** —— 只刷了名册、漏了头像,
85
+ 表现就是「东西明明下好了,界面纹丝不动」,看起来像功能根本没生效。
86
+
87
+
88
+
89
+ **头像显示的真凶:RPC 返回值外面套着 `{ok, value}` 信封,我没解。**
90
+
91
+ ### 根因
92
+
93
+ DSH 的 remote 调用返回值形如 `{ ok: true, value: 真实数据 }`,**必须用 `unwrap()` 解掉**
94
+ (`src/client/catalog.js:21`,`getCatalog` 等所有既有调用都这么写)。新加的 `getAvatars` 漏了这一步,
95
+ 客户端拿到的是**信封本身** —— 诊断时打印 `Object.keys(map)` 得到 `["ok", "value"]`,
96
+ 而 `map["novel-generator"]` 永远是 `undefined` → 卡片全部回退 emoji。
97
+
98
+ **把 `Object.keys(map)` 弹出来这一步是一击定位的关键**:那两个字段名直接暴露了信封结构。
99
+
100
+ ### 修复
101
+
102
+ `settings.jsx` / `summon.jsx` 改用 `unwrap(await remote?.getAvatars?.(), "getAvatars")`,
103
+ 失败兜底成 `{}`;诊断代码移除。
104
+
105
+ ### 这一轮叠在一起的三个坑
106
+
107
+ 1. **`useEffect` 依赖 `[remote]` 不稳定** → effect 反复重挂 → `alive` 恒为 false →
108
+ `setAvatars` 一次都没执行(真 bug,0.4.54 已修)。
109
+ 2. **pnpm 的 `file:` 依赖是「拷贝」不是符号链接** → 改源码 + build 后 `desktop/node_modules`
110
+ 里那份纹丝不动 → **前两次修复根本没到 App**,一直在看旧 client.js。必须
111
+ `rm -rf node_modules/dsh-plugin-t-expert && pnpm add file:...` 强制重挂
112
+ (`realpath` 看着像链接,很容易误判成软链 —— 这次就被它骗了一轮)。
113
+ 3. **RPC 返回值信封没解**(本条)。
114
+
115
+ ### 附带发现
116
+
117
+ `~/.t-team/experts` 是 325(目录形态)+ 323(单文件)= **648**,而 `data/experts` 是 300 + 323 = **623** ——
118
+ 运行时目录**多 25 位历史残留**(删过但没同步回运行时),面板上的「专家 648」就是这么来的。
119
+ 不影响功能,要清需单独处理。
120
+
121
+
122
+
123
+ **修掉「头像数据取到了却显示不出来」的 bug** —— 用户实测:诊断弹窗显示 RPC 返回 2 条,界面却全是 emoji。
124
+
125
+ ### 根因
126
+
127
+ `settings.jsx` / `summon.jsx` 里取头像的 `useEffect` 依赖写成了 `[remote]`:
128
+
129
+ ```js
130
+ React.useEffect(() => {
131
+ let alive = true;
132
+ (async () => { ... if (alive && ...) setAvatars(map); })();
133
+ return () => { alive = false; };
134
+ }, [remote]); // ← desktop 上 remote 走 /api 网关,引用不稳定
135
+ ```
136
+
137
+ desktop 的 remote 是经 `/api` API Gateway 构造的,**引用不稳定** → 每次重渲染 effect 都重挂 →
138
+ cleanup 把 `alive` 置回 `false` → **数据取到了也被 `if (alive …)` 丢掉**,state 永远停在空对象。
139
+
140
+ `alert` 写在 `.then` 里、不看 `alive`,所以照样弹「OK 返回 2 条」—— 这个反差正是定位根因的关键。
141
+
142
+ ### 修复
143
+
144
+ 依赖改 `[]`(只跑一次)。`remote` 在组件生命周期内是稳定的,本来就不需要跟。
145
+
146
+ ### 教训
147
+
148
+ **用 `alive` 防 setState-on-unmounted 时,依赖数组必须是稳定的** —— 否则它防的不是「卸载后 setState」,
149
+ 而是「一切 setState」,而且**完全静默**。另外这次能一步定位,靠的是把被 `catch(() => void 0)`
150
+ 吞掉的结果/错误暴露出来:**给静默失败装一个能看见的出口**,比反复猜快得多。
151
+
152
+
20
153
 
21
154
  **面板与 @ 弹窗显示专家头像** —— 0.4.52 只做到数据链路,这一版接上最后一环。
22
155
 
package/lib/client.js CHANGED
@@ -67,7 +67,7 @@ var SectionBoundary = class extends import_react.default.Component {
67
67
 
68
68
  // src/client/state.js
69
69
  var NS = "t-team";
70
- var CLIENT_BUILD_VERSION = "0.4.53";
70
+ var CLIENT_BUILD_VERSION = "0.4.58";
71
71
  var PLUGIN_ID = "dsh-plugin-t-expert";
72
72
  var DIVISION_FALLBACK = "specialized";
73
73
  var NEW_DIVISION = "__new";
@@ -118,7 +118,13 @@ var warming = /* @__PURE__ */ new Set();
118
118
  function warmupAssets(remote, expert) {
119
119
  if (expert?.assets !== "missing" || warming.has(expert.slug)) return;
120
120
  warming.add(expert.slug);
121
- void remote.fetchAssets(expert.slug).then(() => refresh(remote, true)).catch(() => void 0).finally(() => warming.delete(expert.slug));
121
+ void remote.fetchAssets(expert.slug).then(() => {
122
+ refresh(remote, true);
123
+ try {
124
+ window.dispatchEvent(new CustomEvent("t-team:avatars-changed"));
125
+ } catch {
126
+ }
127
+ }).catch(() => void 0).finally(() => warming.delete(expert.slug));
122
128
  }
123
129
  function displayDescription(expert, active) {
124
130
  return active === "en" ? expert.descriptionEn || expert.description : expert.description;
@@ -333,6 +339,7 @@ var CSS = `
333
339
  .t-team-prompt-body{margin:0;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;line-height:19px;white-space:pre-wrap;word-break:break-word;color:var(--dsw-alias-label-secondary)}
334
340
  .t-team-avatar{display:flex;align-items:center;justify-content:center;width:44px;height:44px;border-radius:50%;background:var(--dsw-alias-bg-layer-3);font-size:22px;line-height:1}
335
341
  .t-team-avatar img{width:100%;height:100%;border-radius:50%;object-fit:cover;display:block}
342
+ .t-team-cloud{border:0;background:none;padding:0;margin-inline-start:4px;cursor:pointer;font-size:inherit;line-height:1}.t-team-cloud:hover{opacity:.7}
336
343
  .t-team-identity{display:flex;min-width:0;flex-direction:column;padding-right:46px}
337
344
  .t-team-card-name{font-size:14px;font-weight:600;line-height:20px;color:var(--dsw-alias-label-primary);display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden}
338
345
  .t-team-card-division{font-size:13px;line-height:18px;margin-top:2px;color:var(--dsw-alias-label-secondary)}
@@ -21579,6 +21586,8 @@ function SettingsPanel({ t, ctx, remote, skillRemote, getActive, initialTab, vie
21579
21586
  const [snapshot, setSnapshot] = import_react5.default.useState(() => snapshotOf(remote) ?? null);
21580
21587
  const [query, setQuery] = import_react5.default.useState("");
21581
21588
  const [avatars, setAvatars] = import_react5.default.useState({});
21589
+ const [avatarRev, setAvatarRev] = import_react5.default.useState(0);
21590
+ const [cloudClicked, setCloudClicked] = import_react5.default.useState({});
21582
21591
  const [division, setDivision] = import_react5.default.useState("");
21583
21592
  const [status, setStatus] = import_react5.default.useState("");
21584
21593
  const [error62, setError] = import_react5.default.useState("");
@@ -21610,16 +21619,28 @@ function SettingsPanel({ t, ctx, remote, skillRemote, getActive, initialTab, vie
21610
21619
  alive = false;
21611
21620
  };
21612
21621
  }, [reload]);
21622
+ import_react5.default.useEffect(() => {
21623
+ const onAvatarsChanged = () => setAvatarRev((n) => n + 1);
21624
+ window.addEventListener("t-team:avatars-changed", onAvatarsChanged);
21625
+ return () => window.removeEventListener("t-team:avatars-changed", onAvatarsChanged);
21626
+ }, []);
21613
21627
  import_react5.default.useEffect(() => {
21614
21628
  let alive = true;
21615
21629
  (async () => {
21616
- const map2 = await Promise.resolve(remote?.getAvatars?.()).catch(() => void 0);
21617
- if (alive && map2 !== void 0 && map2 !== null) setAvatars(map2);
21630
+ let map2 = {};
21631
+ try {
21632
+ map2 = unwrap(await remote?.getAvatars?.(), "getAvatars") ?? {};
21633
+ } catch {
21634
+ map2 = {};
21635
+ }
21636
+ if (alive && map2 !== void 0 && map2 !== null) {
21637
+ setAvatars(map2);
21638
+ }
21618
21639
  })();
21619
21640
  return () => {
21620
21641
  alive = false;
21621
21642
  };
21622
- }, [remote]);
21643
+ }, [avatarRev]);
21623
21644
  const active = getActive();
21624
21645
  async function openPrompt(expert) {
21625
21646
  setCopyState("");
@@ -21901,7 +21922,19 @@ function SettingsPanel({ t, ctx, remote, skillRemote, getActive, initialTab, vie
21901
21922
  /* @__PURE__ */ import_react5.default.createElement("span", { className: "t-team-group-chevron", "aria-hidden": "true" }, open2 ? "\u25BE" : "\u25B8")
21902
21923
  ), open2 && /* @__PURE__ */ import_react5.default.createElement("div", { className: "t-team-grid" }, group.items.map((expert) => {
21903
21924
  const on = enabledSet.has(expert.slug);
21904
- return /* @__PURE__ */ import_react5.default.createElement("div", { className: "t-team-card", "data-enabled": on, key: expert.slug }, /* @__PURE__ */ import_react5.default.createElement("div", { className: "t-team-card-body" }, /* @__PURE__ */ import_react5.default.createElement("div", { className: "t-team-avatar" }, avatars[expert.slug] === void 0 ? expert.emoji || "\u{1F9E9}" : /* @__PURE__ */ import_react5.default.createElement("img", { src: avatars[expert.slug], alt: "" })), /* @__PURE__ */ import_react5.default.createElement("div", { className: "t-team-identity" }, /* @__PURE__ */ import_react5.default.createElement("div", { className: "t-team-card-name" }, displayName(expert, active), expert.custom === true && /* @__PURE__ */ import_react5.default.createElement(Tag2, { tone: "info" }, t("custom.badge")), expert.assets === "missing" && /* @__PURE__ */ import_react5.default.createElement("span", { title: t("assets.cloud"), style: { marginInlineStart: 4 } }, "\u2601\uFE0F")), /* @__PURE__ */ import_react5.default.createElement("div", { className: "t-team-card-division" }, divisionLabel(expert, active)), /* @__PURE__ */ import_react5.default.createElement("div", { className: "t-team-card-slug" }, expert.slug)), /* @__PURE__ */ import_react5.default.createElement("p", { className: "t-team-card-desc" }, displayDescription(expert, active)), expert.conflict === true && /* @__PURE__ */ import_react5.default.createElement("div", { className: "t-team-conflict", style: { gridColumn: "1/-1" } }, t("settings.conflict"))), /* @__PURE__ */ import_react5.default.createElement("div", { className: "t-team-card-tools" }, /* @__PURE__ */ import_react5.default.createElement(
21925
+ return /* @__PURE__ */ import_react5.default.createElement("div", { className: "t-team-card", "data-enabled": on, key: expert.slug }, /* @__PURE__ */ import_react5.default.createElement("div", { className: "t-team-card-body" }, /* @__PURE__ */ import_react5.default.createElement("div", { className: "t-team-avatar" }, avatars[expert.slug] === void 0 ? expert.emoji || "\u{1F9E9}" : /* @__PURE__ */ import_react5.default.createElement("img", { src: avatars[expert.slug], alt: "" })), /* @__PURE__ */ import_react5.default.createElement("div", { className: "t-team-identity" }, /* @__PURE__ */ import_react5.default.createElement("div", { className: "t-team-card-name" }, displayName(expert, active), expert.custom === true && /* @__PURE__ */ import_react5.default.createElement(Tag2, { tone: "info" }, t("custom.badge")), expert.assets === "missing" && cloudClicked[expert.slug] !== true && /* @__PURE__ */ import_react5.default.createElement(
21926
+ "button",
21927
+ {
21928
+ type: "button",
21929
+ className: "t-team-cloud",
21930
+ title: t("assets.cloud"),
21931
+ onClick: () => {
21932
+ warmupAssets(remote, expert);
21933
+ setCloudClicked((prev) => ({ ...prev, [expert.slug]: true }));
21934
+ }
21935
+ },
21936
+ "\u2601\uFE0F"
21937
+ )), /* @__PURE__ */ import_react5.default.createElement("div", { className: "t-team-card-division" }, divisionLabel(expert, active)), /* @__PURE__ */ import_react5.default.createElement("div", { className: "t-team-card-slug" }, expert.slug)), /* @__PURE__ */ import_react5.default.createElement("p", { className: "t-team-card-desc" }, displayDescription(expert, active)), expert.conflict === true && /* @__PURE__ */ import_react5.default.createElement("div", { className: "t-team-conflict", style: { gridColumn: "1/-1" } }, t("settings.conflict"))), /* @__PURE__ */ import_react5.default.createElement("div", { className: "t-team-card-tools" }, /* @__PURE__ */ import_react5.default.createElement(
21905
21938
  Button2,
21906
21939
  {
21907
21940
  variant: "ghost",
@@ -22036,16 +22069,27 @@ function SummonButton({ t, ctx, remote, getActive, insertReference, insertSkillH
22036
22069
  const [popTab, setPopTab] = import_react6.default.useState(preview?.tab ?? "experts");
22037
22070
  const [skills, setSkills] = import_react6.default.useState(null);
22038
22071
  const [avatars, setAvatars] = import_react6.default.useState({});
22072
+ const [avatarRev, setAvatarRev] = import_react6.default.useState(0);
22073
+ import_react6.default.useEffect(() => {
22074
+ const onAvatarsChanged = () => setAvatarRev((n) => n + 1);
22075
+ window.addEventListener("t-team:avatars-changed", onAvatarsChanged);
22076
+ return () => window.removeEventListener("t-team:avatars-changed", onAvatarsChanged);
22077
+ }, []);
22039
22078
  import_react6.default.useEffect(() => {
22040
22079
  let alive = true;
22041
22080
  (async () => {
22042
- const map2 = await Promise.resolve(remote?.getAvatars?.()).catch(() => void 0);
22081
+ let map2 = {};
22082
+ try {
22083
+ map2 = unwrap(await remote?.getAvatars?.(), "getAvatars") ?? {};
22084
+ } catch {
22085
+ map2 = {};
22086
+ }
22043
22087
  if (alive && map2 !== void 0 && map2 !== null) setAvatars(map2);
22044
22088
  })();
22045
22089
  return () => {
22046
22090
  alive = false;
22047
22091
  };
22048
- }, [remote]);
22092
+ }, [avatarRev]);
22049
22093
  const [skillQuery, setSkillQuery] = import_react6.default.useState("");
22050
22094
  const [skillsDiag, setSkillsDiag] = import_react6.default.useState("");
22051
22095
  import_react6.default.useEffect(() => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-t-expert",
3
- "version": "0.4.53",
3
+ "version": "0.4.58",
4
4
  "type": "module",
5
5
  "description": "T Expert — a 623-expert, 22-division roster with full Chinese translations, as a standalone DeepSeek Harness plugin.",
6
6
  "icon": "icon.svg",