dsh-messager 0.3.5 → 0.3.6

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.
Files changed (63) hide show
  1. package/README.en.md +5 -4
  2. package/README.md +5 -4
  3. package/lib/channels/dingtalk.d.ts +3 -5
  4. package/lib/channels/dingtalk.d.ts.map +1 -1
  5. package/lib/channels/dingtalk.js +3 -6
  6. package/lib/channels/dingtalk.js.map +1 -1
  7. package/lib/channels/discord.d.ts +2 -3
  8. package/lib/channels/discord.d.ts.map +1 -1
  9. package/lib/channels/discord.js +2 -3
  10. package/lib/channels/discord.js.map +1 -1
  11. package/lib/channels/feishu.d.ts +3 -14
  12. package/lib/channels/feishu.d.ts.map +1 -1
  13. package/lib/channels/feishu.js +6 -15
  14. package/lib/channels/feishu.js.map +1 -1
  15. package/lib/channels/telegram.d.ts.map +1 -1
  16. package/lib/channels/telegram.js +0 -1
  17. package/lib/channels/telegram.js.map +1 -1
  18. package/lib/channels/wecom.d.ts +1 -1
  19. package/lib/channels/wecom.d.ts.map +1 -1
  20. package/lib/channels/wecom.js +1 -2
  21. package/lib/channels/wecom.js.map +1 -1
  22. package/lib/client.js +188 -35
  23. package/lib/client.js.map +1 -1
  24. package/lib/config-route.d.ts +18 -7
  25. package/lib/config-route.d.ts.map +1 -1
  26. package/lib/config-route.js +59 -16
  27. package/lib/config-route.js.map +1 -1
  28. package/lib/config-shared.d.ts +1 -1
  29. package/lib/config-shared.d.ts.map +1 -1
  30. package/lib/config-shared.js +1 -1
  31. package/lib/config-shared.js.map +1 -1
  32. package/lib/config.d.ts +0 -6
  33. package/lib/config.d.ts.map +1 -1
  34. package/lib/config.js +0 -1
  35. package/lib/config.js.map +1 -1
  36. package/lib/environment.d.ts +68 -0
  37. package/lib/environment.d.ts.map +1 -0
  38. package/lib/environment.js +87 -0
  39. package/lib/environment.js.map +1 -0
  40. package/lib/index.d.ts.map +1 -1
  41. package/lib/index.js +69 -14
  42. package/lib/index.js.map +1 -1
  43. package/lib/templates.d.ts +1 -3
  44. package/lib/templates.d.ts.map +1 -1
  45. package/lib/templates.js +1 -3
  46. package/lib/templates.js.map +1 -1
  47. package/lib/types/client/card-controller.d.ts +51 -1
  48. package/lib/types/client/card-controller.d.ts.map +1 -1
  49. package/lib/types/client/desktop.d.ts +30 -0
  50. package/lib/types/client/desktop.d.ts.map +1 -0
  51. package/lib/types/client/diff.d.ts +20 -2
  52. package/lib/types/client/diff.d.ts.map +1 -1
  53. package/lib/types/client/index.d.ts.map +1 -1
  54. package/lib/types/client/locales.d.ts +10 -2
  55. package/lib/types/client/locales.d.ts.map +1 -1
  56. package/lib/types/client/section.d.ts.map +1 -1
  57. package/lib/types/client/settings-form.d.ts +8 -1
  58. package/lib/types/client/settings-form.d.ts.map +1 -1
  59. package/lib/types/config-shared.d.ts +1 -1
  60. package/lib/types/config-shared.d.ts.map +1 -1
  61. package/lib/types/config.d.ts +0 -6
  62. package/lib/types/config.d.ts.map +1 -1
  63. package/package.json +1 -1
package/lib/client.js CHANGED
@@ -972,8 +972,7 @@ window.__ModuleLoader__.load({
972
972
  }),
973
973
  message: Schema.object({
974
974
  titlePrefix: Schema.string().volatile(),
975
- includeSessionTitle: Schema.boolean().default(true).volatile(),
976
- guiUrl: Schema.string().default("http://127.0.0.1:3080").volatile()
975
+ includeSessionTitle: Schema.boolean().default(true).volatile()
977
976
  })
978
977
  });
979
978
  /**
@@ -1004,7 +1003,7 @@ window.__ModuleLoader__.load({
1004
1003
  * 配置路由的跨端共享类型(host 与浏览器 client 共用,不得 import 任何
1005
1004
  * Node/浏览器专属模块)。
1006
1005
  */
1007
- /** 配置路由路径(挂在 DSH webServer 上,同源访问)。 */
1006
+ /** 配置路由路径(挂在 DSH webServer 上;访问经宿主鉴权,见 src/config-route.ts)。 */
1008
1007
  const CONFIG_PATH = "/dsh-messager/config";
1009
1008
  //#endregion
1010
1009
  //#region src/client/config.ts
@@ -1055,15 +1054,21 @@ window.__ModuleLoader__.load({
1055
1054
  //#region src/client/diff.ts
1056
1055
  /**
1057
1056
  * 对比两次列表快照。首次出现的会话只建立基线、不通知;被移除的会话不通知。
1057
+ *
1058
+ * @param options.ignoreMainView - 忽略「会话是否正被主视图持有」这条过滤(v0.3.6 起):
1059
+ * 窗口/minimized 到后台(用户没在看)时,**当前打开的那个会话**完成了也应该提醒 ——
1060
+ * 否则「跑一个任务、切走」这个最常见用法永远收不到完成通知。
1061
+ * 页面可见但仍要通知(`onlyWhenHidden=false`)时保持过滤:那时用户正看着它。
1058
1062
  */
1059
- function diffSessionSummaries(previous, next) {
1063
+ function diffSessionSummaries(previous, next, options = {}) {
1064
+ const ignoreMainView = options.ignoreMainView === true;
1060
1065
  const notices = [];
1061
1066
  for (const id of Object.keys(next)) {
1062
1067
  const summary = next[id];
1063
1068
  if (summary === void 0) continue;
1064
1069
  const before = previous[id];
1065
1070
  if (before === void 0) continue;
1066
- if (before.running && !summary.running && (summary.retainedBy.mainView ?? 0) === 0) notices.push({
1071
+ if (before.running && !summary.running && (ignoreMainView || (summary.retainedBy.mainView ?? 0) === 0)) notices.push({
1067
1072
  kind: "completed",
1068
1073
  sessionId: id,
1069
1074
  title: summary.displayTitle
@@ -1087,7 +1092,11 @@ window.__ModuleLoader__.load({
1087
1092
  * 用户关掉后仍会收到(Windows 把浏览器通知渲染成系统 toast,难以分辨来源)。
1088
1093
  */
1089
1094
  function triggerAllows(notice, triggers) {
1090
- return notice.kind === "interaction" ? triggers.interaction : triggers.completed;
1095
+ switch (notice.kind) {
1096
+ case "interaction": return triggers.interaction;
1097
+ case "completed": return triggers.completed;
1098
+ case "error": return triggers.error;
1099
+ }
1091
1100
  }
1092
1101
  /** 对比两次 uiSession 统一状态快照。 */
1093
1102
  function diffPendingInteractions(previous, next, summaries) {
@@ -1107,6 +1116,21 @@ window.__ModuleLoader__.load({
1107
1116
  }
1108
1117
  return notices;
1109
1118
  }
1119
+ /**
1120
+ * `api-session/error`(宿主 `agent/error` 的客户端转发事件)→ 任务出错通知。
1121
+ *
1122
+ * 会话标题从当前列表摘要里取;取不到就只发错误摘要。
1123
+ * 桌面版默认只用渲染端通道,这条是「关掉 system 通道后仍能收到错误提醒」的前提。
1124
+ */
1125
+ function errorNotice(sessionId, message, summaries) {
1126
+ const title = summaries[sessionId]?.displayTitle;
1127
+ return {
1128
+ kind: "error",
1129
+ sessionId,
1130
+ message,
1131
+ ...title === void 0 ? {} : { title }
1132
+ };
1133
+ }
1110
1134
  //#endregion
1111
1135
  //#region src/client/card-controller.ts
1112
1136
  /** 字段键:`group.field`。 */
@@ -1122,6 +1146,36 @@ window.__ModuleLoader__.load({
1122
1146
  if (gate === void 0) return false;
1123
1147
  return fields[fieldKey(gate.group, gate.field)]?.text !== "true";
1124
1148
  }
1149
+ /** 用户层是否写过该组(哪怕写的是空对象)。 */
1150
+ function userHasGroup(user, group) {
1151
+ return typeof user === "object" && user !== null && Object.hasOwn(user, group);
1152
+ }
1153
+ /**
1154
+ * 计算当前环境下**可见**的字段清单(纯函数,可单测)。
1155
+ *
1156
+ * 标记了 `desktopHidden` 的字段在桌面版会隐藏(用户层写过该组时除外)。
1157
+ * 📌 当前 CARD_FIELDS 里**没有**这样的字段 —— v0.3.6 实测发现渲染端 toast 可能被系统丢弃
1158
+ * (Windows 未登记应用),隐藏 host 通道开关会让用户无兜底可用,故改为「显示 + 说明」。
1159
+ * 机制保留:将来若有「开了只会重复」的字段,标记即可。
1160
+ */
1161
+ function visibleFields(fields, options) {
1162
+ if (!options.desktop) return [...fields];
1163
+ return fields.filter((spec) => spec.desktopHidden !== true || userHasGroup(options.user, spec.group));
1164
+ }
1165
+ /**
1166
+ * 字段的渲染文案键:桌面版有专用文案时优先。
1167
+ *
1168
+ * 用途:桌面版里 `browser` 通道的产物是**应用原生通知**(Windows toast / macOS
1169
+ * 通知中心,署名是应用本体),不是「浏览器通知」,沿用浏览器文案会误导用户。
1170
+ */
1171
+ function labelKeyFor(spec, desktop) {
1172
+ return desktop && spec.desktopLabel !== void 0 ? spec.desktopLabel : spec.label;
1173
+ }
1174
+ /** 字段的 hint 键(桌面版优先 desktopHint);无 hint 返回 undefined。 */
1175
+ function hintKeyFor(spec, desktop) {
1176
+ if (desktop && spec.desktopHint !== void 0) return spec.desktopHint;
1177
+ return spec.hint;
1178
+ }
1125
1179
  /** 把草稿解析为组内值;undefined 表示非法(阻止保存)。 */
1126
1180
  function parseDraft(spec, text) {
1127
1181
  const trimmed = text.trim();
@@ -1150,9 +1204,12 @@ window.__ModuleLoader__.load({
1150
1204
  saving = false;
1151
1205
  failed = false;
1152
1206
  cache = null;
1153
- constructor(scope, fields) {
1207
+ /** 是否运行在桌面版窗口(决定 desktopHidden 字段是否隐藏)。 */
1208
+ desktop;
1209
+ constructor(scope, fields, options = {}) {
1154
1210
  this.scope = scope;
1155
1211
  this.fields = fields;
1212
+ this.desktop = options.desktop === true;
1156
1213
  scope.subscribe(() => this.invalidate());
1157
1214
  }
1158
1215
  /** 槽位注入面。 */
@@ -1187,7 +1244,8 @@ window.__ModuleLoader__.load({
1187
1244
  this.failed = false;
1188
1245
  this.invalidate();
1189
1246
  },
1190
- t: (key) => key
1247
+ t: (key) => key,
1248
+ desktop: this.desktop
1191
1249
  };
1192
1250
  }
1193
1251
  /**
@@ -1203,7 +1261,7 @@ window.__ModuleLoader__.load({
1203
1261
  const snapshot = this.scope.getSnapshot();
1204
1262
  const available = snapshot.status === "ready";
1205
1263
  const fields = {};
1206
- for (const spec of this.fields) {
1264
+ for (const spec of this.visibleSpecs()) {
1207
1265
  const key = fieldKey(spec.group, spec.field);
1208
1266
  const staged = this.staged.get(key);
1209
1267
  const effective = this.effectiveValue(spec);
@@ -1240,6 +1298,18 @@ window.__ModuleLoader__.load({
1240
1298
  if (group === void 0) return void 0;
1241
1299
  return group[spec.field];
1242
1300
  }
1301
+ /** 当前环境下可见的字段(桌面版隐藏 system 组等,见 visibleFields)。 */
1302
+ visibleSpecs() {
1303
+ return visibleFields(this.fields, {
1304
+ desktop: this.desktop,
1305
+ user: this.scope.getSnapshot().user
1306
+ });
1307
+ }
1308
+ /** 单个字段在当前环境下是否可见(同一批 spec 对象,恒等比较即可)。 */
1309
+ isVisible(spec) {
1310
+ if (spec.desktopHidden !== true) return true;
1311
+ return this.visibleSpecs().some((candidate) => candidate === spec);
1312
+ }
1243
1313
  groupValue(group) {
1244
1314
  const value = this.scope.getSnapshot().value;
1245
1315
  if (typeof value !== "object" || value === null) return void 0;
@@ -1279,6 +1349,7 @@ window.__ModuleLoader__.load({
1279
1349
  for (const [key, staged] of this.staged) {
1280
1350
  const [group, field] = key.split(".");
1281
1351
  const spec = this.spec(group, field);
1352
+ if (!this.isVisible(spec)) continue;
1282
1353
  if (this.gatedOff(spec)) continue;
1283
1354
  if (spec.secret === true && staged.text.trim() === "" && !staged.clear) continue;
1284
1355
  if (staged.clear) {
@@ -1423,7 +1494,8 @@ window.__ModuleLoader__.load({
1423
1494
  group: "browser",
1424
1495
  field: "enabled",
1425
1496
  kind: "toggle",
1426
- label: "field.browser.enabled"
1497
+ label: "field.browser.enabled",
1498
+ desktopLabel: "field.browser.enabled.desktop"
1427
1499
  },
1428
1500
  {
1429
1501
  group: "browser",
@@ -1433,7 +1505,8 @@ window.__ModuleLoader__.load({
1433
1505
  hiddenUnless: {
1434
1506
  group: "browser",
1435
1507
  field: "enabled"
1436
- }
1508
+ },
1509
+ desktopLabel: "field.browser.onlyWhenHidden.desktop"
1437
1510
  },
1438
1511
  {
1439
1512
  group: "browser",
@@ -1725,15 +1798,41 @@ window.__ModuleLoader__.load({
1725
1798
  field: "includeSessionTitle",
1726
1799
  kind: "toggle",
1727
1800
  label: "field.message.includeSessionTitle"
1728
- },
1729
- {
1730
- group: "message",
1731
- field: "guiUrl",
1732
- kind: "text",
1733
- label: "field.message.guiUrl"
1734
1801
  }
1735
1802
  ];
1736
1803
  //#endregion
1804
+ //#region src/client/desktop.ts
1805
+ /**
1806
+ * 桌面版(DSH Desktop)窗口判定。
1807
+ *
1808
+ * 官方标记:桌面壳的 preload 在窗口里 `contextBridge.exposeInMainWorld('dshDesktopBoot', …)`
1809
+ * (asar 内 `lib/preload-app.cjs`);DSH 自己的前端也用
1810
+ * `globalThis.dshDesktopBoot !== undefined` 决定 boot 通道(桌面走 IPC 取启动清单,
1811
+ * 浏览器读 HTML 里的 `__DSH_BOOT__`)。所以这是「我在桌面窗口里」的权威信号。
1812
+ *
1813
+ * ⚠️ 仅 client 端可用(host 端没有 window;host 侧的等价判定见 src/environment.ts)。
1814
+ */
1815
+ /** 判断给定全局对象是否处于桌面版窗口(可传替身便于单测)。 */
1816
+ function isDesktopWindow(target = globalThis) {
1817
+ return typeof target === "object" && target !== null && "dshDesktopBoot" in target;
1818
+ }
1819
+ /**
1820
+ * 「用户现在没在看这个窗口」判定 —— 决定 `onlyWhenHidden` 是否放行通知。
1821
+ *
1822
+ * - 浏览器(`dsh web`):页面 `hidden`(切走标签页 / 最小化)即为没在看;
1823
+ * - **桌面版:`visibilityState` 只在窗口最小化或被隐藏时才变 `hidden`**,
1824
+ * 窗口被别的窗口盖住、失去焦点时它仍是 `visible`。若只看 `visibilityState`,
1825
+ * 用户切到别的应用去干活时永远不会收到通知 —— 所以桌面版把**失去焦点**也算没在看。
1826
+ *
1827
+ * @param options.desktop - 是否桌面版窗口(见 isDesktopWindow)。
1828
+ * @param options.visibilityState - `document.visibilityState`。
1829
+ * @param options.hasFocus - `document.hasFocus()`。
1830
+ */
1831
+ function isWindowBackgrounded(options) {
1832
+ if (options.visibilityState === "hidden") return true;
1833
+ return options.desktop && !options.hasFocus;
1834
+ }
1835
+ //#endregion
1737
1836
  //#region src/client/fetch-scope.ts
1738
1837
  /** 创建 fetch 版 scope。 */
1739
1838
  function createFetchScope(fetcher) {
@@ -1818,6 +1917,11 @@ window.__ModuleLoader__.load({
1818
1917
  telegram: "group.telegram",
1819
1918
  message: "group.message"
1820
1919
  };
1920
+ /**
1921
+ * 桌面版专用分组标题(当前只有 browser):桌面版里那条通道产出的是**应用原生通知**,
1922
+ * 不是"浏览器通知"。键集必须与 locales.ts 一致(字典一致性测试覆盖)。
1923
+ */
1924
+ const GROUP_TITLE_KEYS_DESKTOP = { browser: "group.browser.desktop" };
1821
1925
  const FIELD_STYLE = {
1822
1926
  display: "flex",
1823
1927
  flexDirection: "column",
@@ -1994,9 +2098,10 @@ window.__ModuleLoader__.load({
1994
2098
  });
1995
2099
  }
1996
2100
  /** 渲染一个字段行(DSH ValueField 同款;toggle 标签与开关同一行)。 */
1997
- function FieldRow({ spec, state, actions, t, disabled }) {
2101
+ function FieldRow({ spec, state, actions, t, disabled, desktop }) {
1998
2102
  const id = `dsh-messager-${spec.group}-${spec.field}`;
1999
- const label = t(spec.label);
2103
+ const label = t(labelKeyFor(spec, desktop));
2104
+ const hintKey = hintKeyFor(spec, desktop);
2000
2105
  const overriddenBadge = state.overridden && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2001
2106
  style: {
2002
2107
  display: "inline-flex",
@@ -2096,7 +2201,7 @@ window.__ModuleLoader__.load({
2096
2201
  control,
2097
2202
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2098
2203
  style: state.invalid ? INVALID_STYLE : HINT_STYLE,
2099
- children: state.invalid ? t("status.invalidField") : spec.hint === void 0 ? "" : t(spec.hint)
2204
+ children: state.invalid ? t("status.invalidField") : hintKey === void 0 ? "" : t(hintKey)
2100
2205
  })
2101
2206
  ]
2102
2207
  });
@@ -2108,8 +2213,9 @@ window.__ModuleLoader__.load({
2108
2213
  * @param state - 控制器快照(useMessagerCard 的返回)。
2109
2214
  * @param actions - 控制器动作(edit/reset/save/discard)。
2110
2215
  * @param t - 翻译函数(键 → 当前语言文案)。
2216
+ * @param desktop - 桌面版窗口:分组标题与字段文案走桌面变体(见 GROUP_TITLE_KEYS_DESKTOP)。
2111
2217
  */
2112
- function MessagerSettingsForm({ state, actions, t }) {
2218
+ function MessagerSettingsForm({ state, actions, t, desktop = false }) {
2113
2219
  const groups = [...new Set(Object.keys(state.fields).map((key) => key.split(".")[0]))];
2114
2220
  const actionsDisabled = !state.writable || state.saving;
2115
2221
  const blocked = !state.dirty || state.invalid || state.saving;
@@ -2141,7 +2247,7 @@ window.__ModuleLoader__.load({
2141
2247
  }),
2142
2248
  groups.map((group) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2143
2249
  style: GROUP_HEADING_STYLE,
2144
- children: t(GROUP_TITLE_KEYS[group] ?? group)
2250
+ children: t((desktop ? GROUP_TITLE_KEYS_DESKTOP[group] : void 0) ?? GROUP_TITLE_KEYS[group] ?? group)
2145
2251
  }), Object.entries(state.fields).filter(([key]) => key.startsWith(`${group}.`)).map(([key, field]) => {
2146
2252
  key.slice(key.indexOf(".") + 1);
2147
2253
  const spec = FIELD_LOOKUP[key];
@@ -2152,7 +2258,8 @@ window.__ModuleLoader__.load({
2152
2258
  state: field,
2153
2259
  actions,
2154
2260
  t,
2155
- disabled: actionsDisabled
2261
+ disabled: actionsDisabled,
2262
+ desktop
2156
2263
  }, key);
2157
2264
  })] }, group)),
2158
2265
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -2233,13 +2340,23 @@ window.__ModuleLoader__.load({
2233
2340
  lineHeight: 1.6,
2234
2341
  color: "var(--dsw-alias-label-tertiary)"
2235
2342
  };
2343
+ /** 环境说明(如桌面版隐藏了「系统通知」开关)。 */
2344
+ const NOTE_STYLE = {
2345
+ margin: 0,
2346
+ padding: "8px 10px",
2347
+ borderRadius: 6,
2348
+ fontSize: 12,
2349
+ lineHeight: 1.6,
2350
+ color: "var(--dsw-alias-label-secondary)",
2351
+ background: "var(--dsw-alias-bg-layer-3)"
2352
+ };
2236
2353
  /**
2237
2354
  * dsh-messager 设置分区(设置页「通知&信使」)。
2238
2355
  * @param props - 槽位 owner props + 注入面(useMessagerCard + 动作 + t)。
2239
2356
  */
2240
2357
  function MessagerSection(props) {
2241
2358
  const state = props.useMessagerCard((snapshot) => snapshot);
2242
- const { edit, reset, save, discard, t } = props;
2359
+ const { edit, reset, save, discard, t, desktop } = props;
2243
2360
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2244
2361
  style: SECTION_STYLE,
2245
2362
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
@@ -2247,8 +2364,11 @@ window.__ModuleLoader__.load({
2247
2364
  children: t("nav")
2248
2365
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2249
2366
  style: DESCRIPTION_STYLE,
2250
- children: t("section.description")
2251
- })] }), state.available ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MessagerSettingsForm, {
2367
+ children: t(desktop ? "section.description.desktop" : "section.description")
2368
+ })] }), state.available ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [desktop ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2369
+ style: NOTE_STYLE,
2370
+ children: t("hint.desktopFallback")
2371
+ }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MessagerSettingsForm, {
2252
2372
  state,
2253
2373
  actions: {
2254
2374
  edit,
@@ -2256,8 +2376,9 @@ window.__ModuleLoader__.load({
2256
2376
  save,
2257
2377
  discard
2258
2378
  },
2259
- t
2260
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2379
+ t,
2380
+ desktop
2381
+ })] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2261
2382
  style: UNAVAILABLE_STYLE,
2262
2383
  role: "status",
2263
2384
  children: t("status.unavailable")
@@ -2269,9 +2390,11 @@ window.__ModuleLoader__.load({
2269
2390
  const zh = {
2270
2391
  nav: "通知&信使",
2271
2392
  "section.description": "会话交互 / 任务完成 / 出错时的通知推送:系统通知、浏览器通知、飞书/企业微信/Discord/钉钉/Telegram。",
2393
+ "section.description.desktop": "会话交互 / 任务完成 / 出错时的通知推送:应用通知(由本应用投递,点通知可把窗口提到前台)、飞书/企业微信/Discord/钉钉/Telegram。",
2272
2394
  "group.triggers": "触发时机",
2273
2395
  "group.system": "系统通知",
2274
2396
  "group.browser": "浏览器通知",
2397
+ "group.browser.desktop": "应用通知",
2275
2398
  "group.feishu": "飞书机器人",
2276
2399
  "group.wecom": "企业微信",
2277
2400
  "group.discord": "Discord",
@@ -2285,8 +2408,10 @@ window.__ModuleLoader__.load({
2285
2408
  "field.system.icon": "图标路径",
2286
2409
  "field.system.verbosity": "内容繁复度",
2287
2410
  "field.browser.enabled": "启用浏览器通知",
2411
+ "field.browser.enabled.desktop": "启用应用通知",
2288
2412
  "field.browser.icon": "图标 URL",
2289
2413
  "field.browser.onlyWhenHidden": "仅页面隐藏时通知",
2414
+ "field.browser.onlyWhenHidden.desktop": "仅窗口不在前台时通知",
2290
2415
  "field.browser.verbosity": "内容繁复度",
2291
2416
  "field.feishu.enabled": "飞书机器人",
2292
2417
  "field.feishu.webhookUrl": "Webhook 地址",
@@ -2314,10 +2439,10 @@ window.__ModuleLoader__.load({
2314
2439
  "field.telegram.verbosity": "内容繁复度",
2315
2440
  "field.message.titlePrefix": "标题前缀",
2316
2441
  "field.message.includeSessionTitle": "正文包含会话标题",
2317
- "field.message.guiUrl": "打开链接地址",
2318
2442
  "hint.system.icon": "node-notifier 需要文件绝对路径",
2319
2443
  "hint.secret": "留空不修改;重置可清除已存密钥",
2320
2444
  "hint.message.titlePrefix": "如 [DSH]",
2445
+ "hint.desktopFallback": "桌面版的通知由应用本体投递。若一直收不到(例如 Windows 未登记本应用),可打开上面的「系统通知」用 host 通道兜底 —— 代价是通知的署名不是本应用。",
2321
2446
  "action.save": "保存",
2322
2447
  "action.saving": "保存中…",
2323
2448
  "action.discard": "放弃修改",
@@ -2333,9 +2458,11 @@ window.__ModuleLoader__.load({
2333
2458
  const en = {
2334
2459
  nav: "Messenger",
2335
2460
  "section.description": "Notifications for interaction, task completion and errors: system toast, browser notification, Feishu/WeCom/Discord/DingTalk/Telegram.",
2461
+ "section.description.desktop": "Notifications for interaction, task completion and errors: app notifications (delivered by this app; clicking one brings the window forward), Feishu/WeCom/Discord/DingTalk/Telegram.",
2336
2462
  "group.triggers": "Triggers",
2337
2463
  "group.system": "System",
2338
2464
  "group.browser": "Browser",
2465
+ "group.browser.desktop": "App notification",
2339
2466
  "group.feishu": "Feishu bot",
2340
2467
  "group.wecom": "WeCom",
2341
2468
  "group.discord": "Discord",
@@ -2349,8 +2476,10 @@ window.__ModuleLoader__.load({
2349
2476
  "field.system.icon": "Icon path",
2350
2477
  "field.system.verbosity": "Verbosity",
2351
2478
  "field.browser.enabled": "Enable browser notifications",
2479
+ "field.browser.enabled.desktop": "Enable app notifications",
2352
2480
  "field.browser.icon": "Icon URL",
2353
2481
  "field.browser.onlyWhenHidden": "Notify only when the page is hidden",
2482
+ "field.browser.onlyWhenHidden.desktop": "Notify only when the app is in the background",
2354
2483
  "field.browser.verbosity": "Verbosity",
2355
2484
  "field.feishu.enabled": "Feishu bot",
2356
2485
  "field.feishu.webhookUrl": "Webhook URL",
@@ -2378,10 +2507,10 @@ window.__ModuleLoader__.load({
2378
2507
  "field.telegram.verbosity": "Verbosity",
2379
2508
  "field.message.titlePrefix": "Title prefix",
2380
2509
  "field.message.includeSessionTitle": "Include session title in the body",
2381
- "field.message.guiUrl": "Open link URL",
2382
2510
  "hint.system.icon": "node-notifier requires an absolute file path",
2383
2511
  "hint.secret": "Leave blank to keep the stored secret; Reset clears it",
2384
2512
  "hint.message.titlePrefix": "e.g. [DSH]",
2513
+ "hint.desktopFallback": "On the desktop app, notifications are delivered by the app itself. If none ever appear (for example Windows has not registered the app), turn on \"System\" above as a fallback — those are delivered by the host, so they are not attributed to the app.",
2385
2514
  "action.save": "Save",
2386
2515
  "action.saving": "Saving…",
2387
2516
  "action.discard": "Discard",
@@ -2410,7 +2539,7 @@ window.__ModuleLoader__.load({
2410
2539
  /** localStorage 跨标签页去重键前缀。 */
2411
2540
  const STORAGE_PREFIX = "dsh-messager:notified:";
2412
2541
  /** 浏览器通知投递器。 */
2413
- var BrowserNotifier = class {
2542
+ var BrowserNotifier = class BrowserNotifier {
2414
2543
  config;
2415
2544
  /** `${kind}:${sessionId}` → 上次投递时间戳(内存冷却)。 */
2416
2545
  cooldowns = /* @__PURE__ */ new Map();
@@ -2422,18 +2551,37 @@ window.__ModuleLoader__.load({
2422
2551
  }
2423
2552
  /** 列表快照变化 → 通知。 */
2424
2553
  onListChange(previous, next) {
2425
- this.onNotices(diffSessionSummaries(previous.byId, next.byId));
2554
+ this.onNotices(diffSessionSummaries(previous.byId, next.byId, { ignoreMainView: BrowserNotifier.backgrounded() }));
2426
2555
  }
2427
2556
  /** 待交互状态变化 → 通知。 */
2428
2557
  onPendingChange(previous, next, sessions) {
2429
2558
  this.onNotices(diffPendingInteractions(previous, next, sessions.byId));
2430
2559
  }
2560
+ /** 任务出错(宿主 `agent/error` 经 `api-session/error` 转发)→ 通知。 */
2561
+ onError(sessionId, message, sessions) {
2562
+ this.onNotices([errorNotice(sessionId, message, sessions.byId)]);
2563
+ }
2564
+ /**
2565
+ * 用户现在是否「没在看这个窗口」。
2566
+ *
2567
+ * 浏览器:页面 hidden(切走标签页 / 最小化);桌面版:再补上「失去焦点」——
2568
+ * 桌面窗口被别的窗口盖住时 `visibilityState` 仍是 `visible`,
2569
+ * 只看它会导致切到别的应用就永远不提醒(见 src/client/desktop.ts)。
2570
+ */
2571
+ static backgrounded() {
2572
+ if (typeof document === "undefined") return false;
2573
+ return isWindowBackgrounded({
2574
+ desktop: isDesktopWindow(),
2575
+ visibilityState: document.visibilityState,
2576
+ hasFocus: document.hasFocus()
2577
+ });
2578
+ }
2431
2579
  onNotices(notices) {
2432
2580
  const config = this.config.get();
2433
2581
  if (!config.browser.enabled) return;
2434
2582
  if (typeof Notification === "undefined") return;
2435
2583
  if (Notification.permission !== "granted") return;
2436
- if (config.browser.onlyWhenHidden && document.visibilityState !== "hidden") return;
2584
+ if (config.browser.onlyWhenHidden && !BrowserNotifier.backgrounded()) return;
2437
2585
  for (const notice of notices) {
2438
2586
  if (!triggerAllows(notice, config.triggers)) continue;
2439
2587
  if (!this.allow(notice, config)) continue;
@@ -2471,6 +2619,7 @@ window.__ModuleLoader__.load({
2471
2619
  titleOf(notice, config) {
2472
2620
  let base;
2473
2621
  if (notice.kind === "interaction") base = notice.interaction === "approval" ? "需要交互:等待审批" : notice.interaction === "plan-review" ? "需要交互:计划待审" : "需要交互:等待回答";
2622
+ else if (notice.kind === "error") base = "任务出错";
2474
2623
  else base = "任务完成";
2475
2624
  const prefix = config.message.titlePrefix;
2476
2625
  return prefix === void 0 || prefix === "" ? base : `${prefix} ${base}`;
@@ -2478,8 +2627,8 @@ window.__ModuleLoader__.load({
2478
2627
  bodyOf(notice, config, verbosity) {
2479
2628
  const lines = [];
2480
2629
  if (verbosity === "minimal") return "";
2630
+ if (notice.kind === "error" && notice.message !== void 0 && notice.message !== "") lines.push(`错误:${notice.message}`);
2481
2631
  if (config.message.includeSessionTitle && notice.title !== void 0) lines.push(`会话:${notice.title}`);
2482
- if (verbosity === "detailed") lines.push(`打开:${config.message.guiUrl}`);
2483
2632
  return lines.join("\n");
2484
2633
  }
2485
2634
  };
@@ -2541,7 +2690,11 @@ window.__ModuleLoader__.load({
2541
2690
  previousStatus = next;
2542
2691
  });
2543
2692
  ctx.effect(() => () => offStatus(), "dsh-messager: session status subscription");
2544
- const controller = new MessagerCardController(fetchScope.scope, CARD_FIELDS);
2693
+ const offError = ctx.remote.$on("api-session/error", (sessionId, message) => {
2694
+ notifier.onError(sessionId, message, ctx.sessions.list.getSnapshot());
2695
+ });
2696
+ ctx.effect(() => () => offError(), "dsh-messager: error subscription");
2697
+ const controller = new MessagerCardController(fetchScope.scope, CARD_FIELDS, { desktop: isDesktopWindow() });
2545
2698
  ctx.effect(() => ctx.locale.register(LOCALE_NS, {
2546
2699
  zh,
2547
2700
  en