@telosmaylx/dsh-session-notify 0.1.5 → 0.1.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 (3) hide show
  1. package/README.md +24 -4
  2. package/lib/client.js +78 -0
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -274,9 +274,10 @@ Session completed (took 3m25s, used 1,240 in / 3,560 out).
274
274
 
275
275
  | 权限状态 | 行为 |
276
276
  | --- | --- |
277
- | `default`(未决定) | 每页加载只发起一次授权请求,后续完成事件不再触发询问 |
278
- | `granted` | 每次完成都发系统通知(独立 tag,互不覆盖) |
279
- | `denied` 或环境不支持 | 仅 toast(永远展示,保底可见) |
277
+ | `default`(未决定) | 完成事件只发 toast;设置面板「通知权限」区提供「请求授权」按钮(**用户手势内请求**——Chromium 会忽略非手势的自动请求,因此插件不再自动请求) |
278
+ | `granted` | 按「推送方式」发系统通知(独立 tag,互不覆盖) |
279
+ | `denied`(被浏览器屏蔽) | 仅 toast;设置面板显示地址栏操作指引(权限图标 → 网站设置 → 通知 → 允许) |
280
+ | `undefined`(非安全上下文 / 不支持) | 仅 toast;建议改用「仅页内提示」 |
280
281
 
281
282
  ---
282
283
 
@@ -296,10 +297,14 @@ Session completed (took 3m25s, used 1,240 in / 3,560 out).
296
297
  | 推送标题 | `{title}` 引用会话标题,留空则直接用会话标题 |
297
298
  | 模板 × 5 | 每条结束原因(完成、出错、中止、阻塞、输出上限)独立一个 Chip 编辑器:文字 + 内联信息胶囊,光标处插入、点击移除、实时预览 |
298
299
  | 跳过子代理会话 | 复选框(保存时一并写入设置文档) |
300
+ | 通知权限 | 状态实时显示:已授权(绿)/ 尚未授权(附「请求授权」按钮)/ 已被浏览器屏蔽(附地址栏操作指引)/ 环境不支持 |
299
301
  | 保存 | 写入宿主设置文档(`language` / `templates` / `titleTemplate` / `pushMode`);保存后显示「点击刷新」链接 |
300
302
 
301
303
  > [!NOTE]
302
- > 「推送方式」的取舍:`dual`(默认)同时弹 Windows 系统通知与页内 toast,toast 是保底通道,防止系统通知被平台静默(专注助手、通知横幅关闭)。但 QQ 浏览器等 Chromium 壳浏览器会把 `Notification` 渲染成页内横幅而非 Windows 通知中心——此时 `dual` 会造成页内两个提示。这类浏览器请选「仅页内提示」(不再调用 `Notification`,页内只有一个小 toast);Firefox 另需在地址栏手动授权通知,且窗口聚焦时通知显示为页内横幅、失焦才进系统通知中心。
304
+ > 「推送方式」的取舍:`dual`(默认)同时弹 Windows 系统通知与页内 toast,toast 是保底通道,防止系统通知被平台静默(专注助手、通知横幅关闭)。但 QQ 浏览器等 Chromium 壳浏览器会把 `Notification` 渲染成页内横幅而非 Windows 通知中心——此时 `dual` 会造成页内两个提示。这类浏览器请选「仅页内提示」(不再调用 `Notification`,页内只有一个小 toast)。
305
+
306
+ > [!NOTE]
307
+ > 系统通知(`Notification` API)能否弹出由**浏览器与站点访问方式**共同决定:Edge/Chrome 对"不熟悉"的站点会**自动屏蔽通知**(地址栏出现「通知已屏蔽」)——点击地址栏左侧权限图标 → 网站设置 → 通知 → 允许即可恢复;`http://IP` 这类非安全上下文访问时 `Notification` 根本不存在,请改用「仅页内提示」。设置面板「通知权限」区域会实时显示当前状态并给出对应操作指引(可一键请求授权)。Firefox 窗口聚焦时通知显示为页内横幅、失焦才进系统通知中心。
303
308
 
304
309
  > [!NOTE]
305
310
  > 面板中「跳过子代理会话」保存的是设置文档里的布尔值;宿主 `cordis.patch.yml` 的 `config.skipSubagents` 是其启动默认值,两者任一为真即跳过。
@@ -505,12 +510,27 @@ node scripts/verify-notice.mjs <session.jsonl.zstd>
505
510
 
506
511
  </details>
507
512
 
513
+ <details>
514
+ <summary><b>为什么 Edge 推不了系统通知?QQ 浏览器为什么只有页内横幅?</b></summary>
515
+
516
+ 两者都是浏览器行为,插件无法强制:
517
+
518
+ - **Edge / Chrome**:对"不熟悉"的站点会**自动屏蔽通知**(地址栏出现「通知已屏蔽」)。点击地址栏左侧权限图标 → 网站设置 → 通知 → 允许即可恢复,之后正常弹 Windows 通知中心。也可在浏览器通知设置中关闭「自动屏蔽」。
519
+ - **QQ 浏览器等国产 Chromium 壳**:把 `Notification` 固定渲染为页内横幅,不经过 Windows 通知中心,且无系统通知选项——请用「推送方式 = 仅页内提示」,页内只有一个插件自带的小 toast。
520
+ - **Firefox**:窗口聚焦时通知显示为页内横幅,失焦/最小化才进系统通知中心;权限需在地址栏手动允许。
521
+ - 另注意:`http://IP` 访问(非安全上下文)时 `Notification` 不存在,任何浏览器都弹不了系统通知。
522
+
523
+ 设置面板「通知权限」区域会实时显示当前状态与对应操作指引。
524
+
525
+ </details>
526
+
508
527
  ---
509
528
 
510
529
  ## 更新日志
511
530
 
512
531
  | 版本 | 日期 | 变更 |
513
532
  | --- | --- | --- |
533
+ | **0.1.6** | 2026-08-29 | 设置面板新增「通知权限」状态区(授权状态实时显示 + 一键请求授权按钮 + 被屏蔽时的地址栏操作指引);授权改为**用户手势内请求**(Chromium 忽略非手势自动请求,Edge 对不熟悉站点自动屏蔽通知的典型场景得以解决);FAQ 新增浏览器差异说明 |
514
534
  | **0.1.5** | 2026-08-29 | 新增「推送方式」设置(双通道 / 仅系统通知 / 仅页内提示):解决 QQ 浏览器等 Chromium 壳把 `Notification` 渲染成页内横幅导致的双提示;`pushMode` 加入设置 schema 与设置面板 |
515
535
  | **0.1.4** | 2026-08-28 | 补充完整卸载支持:`dispose` 生命周期收尾(host 置卸载标志抑制待追加微任务;client 清理正文轮询定时器、`__dsch_notify_debug` 钩子、toast 容器);卸载文档与 FAQ 同步 |
516
536
  | **0.1.3** | 2026-08-28 | 声明官方 `dsh.bundle` manifest(`dsh plugin add` 一条命令自动挂载);settings 重试定时器改为 `ctx.effect()` 包装(Cordis effect 纪律);安装文档重排 |
package/lib/client.js CHANGED
@@ -55,6 +55,8 @@ window.__ModuleLoader__.load({
55
55
  skipSubagents: '跳过子代理会话',
56
56
  pushMode: '推送方式', pushDual: '系统通知 + 页内提示', pushSystem: '仅系统通知', pushToast: '仅页内提示',
57
57
  discard: '放弃', save: '保存', saving: '保存中…', saved: '已保存 ✓ · 刷新页面后生效', saveFailed: '保存失败:', refresh: '点击刷新',
58
+ permTitle: '通知权限', permGranted: '已授权:系统通知可用', permDefault: '尚未授权', permDenied: '已被浏览器屏蔽', permUnsupported: '当前环境不支持系统通知',
59
+ permDefaultHint: '点击「请求授权」;若浏览器未弹出询问,请用地址栏权限图标手动允许', permDeniedHint: '浏览器阻止了本网站的通知:点击地址栏左侧权限图标 → 网站设置 → 通知 → 允许,然后刷新页面', permUnsupportedHint: '系统通知需要 HTTPS 或 localhost 访问;可改用「仅页内提示」', permRequest: '请求授权',
58
60
  saveAs: '新增', update: '修改', remove: '删除', presetNamePlaceholder: '预设名称', confirm: '确定', cancel: '取消', customSaved: '自定义预设已保存', customUpdated: '自定义预设已更新', customDeleted: '自定义预设已删除', saveAsTitle: '将当前配置另存为自定义预设', updateTitle: '更新当前自定义预设', removeTitle: '删除当前自定义预设', fromPreset: '来自:', modifiedSuffix: ' · 已修改',
59
61
  presetNameRequired: '请输入预设名称',
60
62
  loading: '加载中…', unavailable: '设置不可用(宿主未注册命名空间)',
@@ -73,6 +75,8 @@ window.__ModuleLoader__.load({
73
75
  skipSubagents: '跳過子代理會話',
74
76
  pushMode: '推送方式', pushDual: '系統通知 + 頁內提示', pushSystem: '僅系統通知', pushToast: '僅頁內提示',
75
77
  discard: '放棄', save: '儲存', saving: '儲存中…', saved: '已儲存 ✓ · 重新整理頁面後生效', saveFailed: '儲存失敗:', refresh: '點擊重新整理',
78
+ permTitle: '通知權限', permGranted: '已授權:系統通知可用', permDefault: '尚未授權', permDenied: '已被瀏覽器封鎖', permUnsupported: '目前環境不支援系統通知',
79
+ permDefaultHint: '點擊「請求授權」;若瀏覽器未彈出詢問,請用網址列權限圖示手動允許', permDeniedHint: '瀏覽器封鎖了本網站的通知:點擊網址列左側權限圖示 → 網站設定 → 通知 → 允許,然後重新整理頁面', permUnsupportedHint: '系統通知需要 HTTPS 或 localhost 存取;可改用「僅頁內提示」', permRequest: '請求授權',
76
80
  saveAs: '新增', update: '修改', remove: '刪除', presetNamePlaceholder: '預設名稱', confirm: '確定', cancel: '取消', customSaved: '自訂預設已儲存', customUpdated: '自訂預設已更新', customDeleted: '自訂預設已刪除', saveAsTitle: '將當前設定另存為自訂預設', updateTitle: '更新目前自訂預設', removeTitle: '刪除目前自訂預設', fromPreset: '來自:', modifiedSuffix: ' · 已修改',
77
81
  presetNameRequired: '請輸入預設名稱',
78
82
  loading: '載入中…', unavailable: '設定不可用(宿主未註冊命名空間)',
@@ -91,6 +95,8 @@ window.__ModuleLoader__.load({
91
95
  skipSubagents: 'Skip subagent sessions',
92
96
  pushMode: 'Push channel', pushDual: 'System + in-page', pushSystem: 'System only', pushToast: 'In-page only',
93
97
  discard: 'Discard', save: 'Save', saving: 'Saving…', saved: 'Saved ✓ · refresh page to apply', saveFailed: 'Save failed: ', refresh: 'Refresh',
98
+ permTitle: 'Notification permission', permGranted: 'Granted: system notifications available', permDefault: 'Not yet granted', permDenied: 'Blocked by browser', permUnsupported: 'System notifications not supported here',
99
+ permDefaultHint: 'Click "Request"; if the browser shows no prompt, allow via the address-bar permissions icon', permDeniedHint: 'This browser blocked notifications for this site: click the address-bar permissions icon → Site settings → Notifications → Allow, then reload', permUnsupportedHint: 'System notifications require HTTPS or localhost; use "In-page only" instead', permRequest: 'Request',
94
100
  saveAs: 'New', update: 'Edit', remove: 'Delete', presetNamePlaceholder: 'Preset name', confirm: 'OK', cancel: 'Cancel', customSaved: 'Custom preset saved', customUpdated: 'Custom preset updated', customDeleted: 'Custom preset deleted', saveAsTitle: 'Save current config as custom preset', updateTitle: 'Update current custom preset', removeTitle: 'Delete current custom preset', fromPreset: 'From: ', modifiedSuffix: ' · modified',
95
101
  presetNameRequired: 'Please enter a preset name',
96
102
  loading: 'Loading…', unavailable: 'Unavailable (namespace not registered on host)',
@@ -109,6 +115,8 @@ window.__ModuleLoader__.load({
109
115
  skipSubagents: 'サブエージェントセッションをスキップ',
110
116
  pushMode: '通知チャネル', pushDual: 'システム+ページ内', pushSystem: 'システムのみ', pushToast: 'ページ内のみ',
111
117
  discard: '破棄', save: '保存', saving: '保存中…', saved: '保存しました ✓ · ページを再読み込み', saveFailed: '保存失敗:', refresh: '再読み込み',
118
+ permTitle: '通知権限', permGranted: '許可済み:システム通知が利用可能', permDefault: '未許可', permDenied: 'ブラウザがブロック', permUnsupported: 'この環境ではシステム通知を利用できません',
119
+ permDefaultHint: '「許可をリクエスト」をクリック。ブラウザに確認が出ない場合は、アドレスバーの権限アイコンから手動で許可', permDeniedHint: 'このサイトの通知がブロックされています:アドレスバー左の権限アイコン → サイト設定 → 通知 → 許可 → ページを再読み込み', permUnsupportedHint: 'システム通知には HTTPS または localhost が必要です。「ページ内のみ」を推奨', permRequest: '許可をリクエスト',
112
120
  saveAs: '新規', update: '編集', remove: '削除', presetNamePlaceholder: 'プリセット名', confirm: '決定', cancel: 'キャンセル', customSaved: 'カスタムプリセットを保存しました', customUpdated: 'カスタムプリセットを更新しました', customDeleted: 'カスタムプリセットを削除しました', saveAsTitle: '現在の設定をカスタムプリセットとして保存', updateTitle: '現在のカスタムプリセットを更新', removeTitle: '現在のカスタムプリセットを削除', fromPreset: '由来:', modifiedSuffix: ' · 変更あり',
113
121
  presetNameRequired: 'プリセット名を入力してください',
114
122
  loading: '読み込み中…', unavailable: '利用不可(ホストに名前空間未登録)',
@@ -127,6 +135,8 @@ window.__ModuleLoader__.load({
127
135
  skipSubagents: '하위 에이전트 세션 건너뛰기',
128
136
  pushMode: '알림 채널', pushDual: '시스템+페이지 내', pushSystem: '시스템만', pushToast: '페이지 내만',
129
137
  discard: '취소', save: '저장', saving: '저장 중…', saved: '저장됨 ✓ · 새로고침 후 적용', saveFailed: '저장 실패: ', refresh: '새로고침',
138
+ permTitle: '알림 권한', permGranted: '허용됨: 시스템 알림 사용 가능', permDefault: '아직 허용되지 않음', permDenied: '브라우저에서 차단됨', permUnsupported: '이 환경에서는 시스템 알림을 사용할 수 없음',
139
+ permDefaultHint: '「권한 요청」클릭. 브라우저에 확인이 없으면 주소 표시줄 권한 아이콘에서 수동 허용', permDeniedHint: '이 사이트의 알림이 차단되었습니다: 주소 표시줄 왼쪽 권한 아이콘 → 사이트 설정 → 알림 → 허용 후 새로고침', permUnsupportedHint: '시스템 알림은 HTTPS 또는 localhost가 필요합니다. 「페이지 내만」사용 권장', permRequest: '권한 요청',
130
140
  saveAs: '추가', update: '수정', remove: '삭제', presetNamePlaceholder: '프리셋 이름', confirm: '확인', cancel: '취소', customSaved: '사용자 지정 프리셋 저장됨', customUpdated: '사용자 지정 프리셋 업데이트됨', customDeleted: '사용자 지정 프리셋 삭제됨', saveAsTitle: '현재 구성을 사용자 지정 프리셋으로 저장', updateTitle: '현재 사용자 지정 프리셋 업데이트', removeTitle: '현재 사용자 지정 프리셋 삭제', fromPreset: '출처: ', modifiedSuffix: ' · 수정됨',
131
141
  presetNameRequired: '프리셋 이름을 입력하세요',
132
142
  loading: '불러오는 중…', unavailable: '사용 불가(호스트에 네임스페이스 미등록)',
@@ -629,6 +639,69 @@ window.__ModuleLoader__.load({
629
639
  h('span', null, labelText))
630
640
  }
631
641
 
642
+ /**
643
+ * 通知权限状态区块:实时显示浏览器对 Notification API 的授权状态与下一步引导。
644
+ * - granted:系统通知可用(按 pushMode 生效);
645
+ * - default:未授权,提供「请求授权」按钮(用户手势内请求——Chromium 忽略非手势的自动请求);
646
+ * - denied:浏览器已屏蔽(如 Edge 对"不熟悉"站点的自动阻止),引导到地址栏手动允许;
647
+ * - undefined:非安全上下文(http://IP)或浏览器不支持,引导改用页内提示。
648
+ */
649
+ function notificationPermissionRow(t) {
650
+ var statusText, hintText, statusColor = SUBTLE
651
+ var hasApi = typeof Notification !== 'undefined'
652
+ if (!hasApi) {
653
+ statusText = t.permUnsupported
654
+ hintText = t.permUnsupportedHint
655
+ } else if (Notification.permission === 'granted') {
656
+ statusText = t.permGranted
657
+ hintText = ''
658
+ statusColor = 'var(--dsw-alias-success, #2ecc71)'
659
+ } else if (Notification.permission === 'denied') {
660
+ statusText = t.permDenied
661
+ hintText = t.permDeniedHint
662
+ statusColor = 'var(--dsw-alias-warning, #e67e22)'
663
+ } else {
664
+ statusText = t.permDefault
665
+ hintText = t.permDefaultHint
666
+ }
667
+ void permTick // 依赖刷新计数:授权后重渲染读取最新状态
668
+ return h('div', {
669
+ key: 'perm',
670
+ style: { display: 'flex', flexDirection: 'column', gap: '6px', padding: '12px 0', borderTop: DIVIDER },
671
+ },
672
+ h('div', { style: { fontSize: '13px', color: 'var(--dsw-alias-label-secondary, inherit)' } }, t.permTitle),
673
+ h('div', { style: { display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'wrap' } },
674
+ h('span', { style: { fontSize: '12px', color: statusColor } }, statusText),
675
+ hasApi && Notification.permission === 'default'
676
+ ? h('button', {
677
+ type: 'button', onClick: requestPermissionNow,
678
+ style: { appearance: 'none', font: 'inherit', fontSize: '12px', cursor: 'pointer', padding: '2px 10px', borderRadius: '999px', border: '1px solid var(--dsw-alias-border-l2, rgba(127,127,127,0.35))', background: 'none', color: 'var(--dsw-alias-label-secondary, inherit)' },
679
+ }, t.permRequest)
680
+ : null,
681
+ ),
682
+ hintText
683
+ ? h('div', { style: { fontSize: '12px', color: SUBTLE, lineHeight: 1.6, wordBreak: 'break-word' } }, hintText)
684
+ : null,
685
+ )
686
+ }
687
+
688
+ /** 用户手势内请求通知权限(Chromium 忽略非手势的自动请求,必须由点击触发)。 */
689
+ function requestPermissionNow() {
690
+ try {
691
+ if (typeof Notification !== 'undefined' && Notification.permission === 'default') {
692
+ var req = Notification.requestPermission()
693
+ if (req && typeof req.then === 'function') {
694
+ req.then(function (p) {
695
+ log('user-gesture notification permission → ' + p)
696
+ setPermTick(permTick + 1)
697
+ }).catch(function () { setPermTick(permTick + 1) })
698
+ } else {
699
+ setPermTick(permTick + 1)
700
+ }
701
+ }
702
+ } catch (e) { /* noop */ }
703
+ }
704
+
632
705
  /**
633
706
  * Chip 模板编辑器(contentEditable):
634
707
  * - 普通文字就是文字,动态信息渲染成内联胶囊({duration} 等代码永不露出);
@@ -870,6 +943,9 @@ window.__ModuleLoader__.load({
870
943
  var nameState = useState('')
871
944
  var newName = nameState[0]
872
945
  var setName = nameState[1]
946
+ var permTickState = useState(0) // 通知权限状态刷新计数(请求授权后 setPermTick(+1) 重渲染)
947
+ var permTick = permTickState[0]
948
+ var setPermTick = permTickState[1]
873
949
 
874
950
  var ready = snap.status === 'ready'
875
951
  var value = (snap && snap.value) || {}
@@ -1181,6 +1257,8 @@ window.__ModuleLoader__.load({
1181
1257
  return h('span', { key: 'tk-' + field + '-' + idx }, t.samples[seg.key] || '')
1182
1258
  })))
1183
1259
  })
1260
+ // 通知权限状态区(footer 之上):授权状态 + 引导(Edge 自动屏蔽等场景一目了然)
1261
+ rows.push(notificationPermissionRow(t))
1184
1262
  rows.push(h('div', {
1185
1263
  key: 'footer',
1186
1264
  style: { borderTop: DIVIDER, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: '8px', padding: '12px 0 4px' },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telosmaylx/dsh-session-notify",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "DSH session completion notifier: appends a plugin system message to the session log on turn/end and pushes browser notifications (Web Notification + toast), with a fully customizable official settings panel (presets, 5-language templates, cache-hit rate & tok/s from official projections).",
5
5
  "private": false,
6
6
  "type": "module",