@telosmaylx/dsh-session-notify 0.1.7 → 0.1.9

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/lib/client.js CHANGED
@@ -45,16 +45,16 @@ window.__ModuleLoader__.load({
45
45
  title: '会话完成提醒', subtitle: '完成推送的语言与文案模板(保存后刷新生效)',
46
46
  preset: '预设', choosePreset: '选择预设…', language: '语言',
47
47
  fields: { completed: '完成', error: '出错', aborted: '中止', blocked: '阻塞', 'max-tokens': '输出上限' },
48
- titleLabel: '推送标题', titleHint: '留空则使用会话标题',
48
+ titleLabel: '推送标题', titleHint: '所有原因共用;留空时按原因用默认标题', titleTplLabel: '按原因定制标题(可选)', titleTplOpen: '展开', titleTplClose: '收起', ttPlaceholders: { completed: '任务已完成', error: '任务出错', aborted: '任务已中止', blocked: '任务被阻塞', 'max-tokens': '任务达到输出上限' },
49
49
  kindLabels: { completed: '会话已完成', error: '会话出错', aborted: '会话已中止', blocked: '会话被阻塞', 'max-tokens': '会话达到输出上限' },
50
50
  tokens: { title: '会话标题', label: '完成标签', duration: '用时', usage: '消耗', error: '错误', cache: '缓存命中', tps: '速度 TPS' },
51
51
  samples: { title: '示例会话', label: '会话已完成', duration: '12 秒', usage: '1,240 输入 / 3,560 输出', error: '连接超时', cache: '96.5%', tps: '92 tok/s' },
52
52
  presets: { custom: '自定义预设', unnamed: '未命名预设', default: '默认', playful: '轻松风格 🎉', formal: '正式简洁', detail: '详细报告' },
53
53
  placeholder: '自定义文案:点「+ 插入信息」,或直接输入文字',
54
- insertInfo: '+ 插入信息', insertTitle: '在光标处插入信息', preview: '预览:',
54
+ insertInfo: '+ 插入信息', insertTitle: '在光标处插入信息', sendTest: '发送', sendTestTitle: '发送测试通知(当前模板渲染)', preview: '预览:',
55
55
  skipSubagents: '跳过子代理会话',
56
56
  pushMode: '推送方式', pushDual: '系统通知 + 页内提示', pushSystem: '仅系统通知', pushToast: '仅页内提示',
57
- discard: '放弃', save: '保存', saving: '保存中…', saved: '已保存 ✓ · 刷新页面后生效', saveFailed: '保存失败:', refresh: '点击刷新',
57
+ discard: '放弃', save: '保存', saving: '保存中…', saved: '已保存 ✓ · 刷新页面后生效', saveFailed: '保存失败:', refresh: '点击刷新', reset: '重置', resetDone: '已重置为默认值 · 刷新页面后生效',
58
58
  permTitle: '通知权限', permGranted: '已授权:系统通知可用', permDefault: '尚未授权', permDenied: '已被浏览器屏蔽', permUnsupported: '当前环境不支持系统通知',
59
59
  permDefaultHint: '点击「请求授权」;若浏览器未弹出询问,请用地址栏权限图标手动允许', permDeniedHint: '浏览器阻止了本网站的通知:点击地址栏左侧权限图标 → 网站设置 → 通知 → 允许,然后刷新页面', permUnsupportedHint: '系统通知需要 HTTPS 或 localhost 访问;可改用「仅页内提示」', permRequest: '请求授权',
60
60
  saveAs: '新增', update: '修改', remove: '删除', presetNamePlaceholder: '预设名称', confirm: '确定', cancel: '取消', customSaved: '自定义预设已保存', customUpdated: '自定义预设已更新', customDeleted: '自定义预设已删除', saveAsTitle: '将当前配置另存为自定义预设', updateTitle: '更新当前自定义预设', removeTitle: '删除当前自定义预设', fromPreset: '来自:', modifiedSuffix: ' · 已修改',
@@ -65,16 +65,16 @@ window.__ModuleLoader__.load({
65
65
  title: '會話完成提醒', subtitle: '完成推送的語言與文案模板(儲存後重新整理生效)',
66
66
  preset: '預設', choosePreset: '選擇預設…', language: '語言',
67
67
  fields: { completed: '完成', error: '出錯', aborted: '中止', blocked: '阻塞', 'max-tokens': '輸出上限' },
68
- titleLabel: '推送標題', titleHint: '留空則使用會話標題',
68
+ titleLabel: '推送標題', titleHint: '所有原因共用;留空時按原因用預設標題', titleTplLabel: '按原因自訂標題(選填)', titleTplOpen: '展開', titleTplClose: '收起', ttPlaceholders: { completed: '任務已完成', error: '任務出錯', aborted: '任務已中止', blocked: '任務被阻塞', 'max-tokens': '任務達到輸出上限' },
69
69
  kindLabels: { completed: '會話已完成', error: '會話出錯', aborted: '會話已中止', blocked: '會話被阻塞', 'max-tokens': '會話達到輸出上限' },
70
70
  tokens: { title: '會話標題', label: '完成標籤', duration: '用時', usage: '消耗', error: '錯誤', cache: '快取命中', tps: '速度 TPS' },
71
71
  samples: { title: '範例會話', label: '會話已完成', duration: '12 秒', usage: '1,240 輸入 / 3,560 輸出', error: '連線逾時', cache: '96.5%', tps: '92 tok/s' },
72
72
  presets: { custom: '自訂預設', unnamed: '未命名預設', default: '預設', playful: '輕鬆風格 🎉', formal: '正式簡潔', detail: '詳細報告' },
73
73
  placeholder: '自訂文案:點「+ 插入資訊」,或直接輸入文字',
74
- insertInfo: '+ 插入資訊', insertTitle: '在游標處插入資訊', preview: '預覽:',
74
+ insertInfo: '+ 插入資訊', insertTitle: '在游標處插入資訊', sendTest: '傳送', sendTestTitle: '傳送測試通知(目前範本渲染)', preview: '預覽:',
75
75
  skipSubagents: '跳過子代理會話',
76
76
  pushMode: '推送方式', pushDual: '系統通知 + 頁內提示', pushSystem: '僅系統通知', pushToast: '僅頁內提示',
77
- discard: '放棄', save: '儲存', saving: '儲存中…', saved: '已儲存 ✓ · 重新整理頁面後生效', saveFailed: '儲存失敗:', refresh: '點擊重新整理',
77
+ discard: '放棄', save: '儲存', saving: '儲存中…', saved: '已儲存 ✓ · 重新整理頁面後生效', saveFailed: '儲存失敗:', refresh: '點擊重新整理', reset: '重置', resetDone: '已重置為預設值 · 重新整理頁面後生效',
78
78
  permTitle: '通知權限', permGranted: '已授權:系統通知可用', permDefault: '尚未授權', permDenied: '已被瀏覽器封鎖', permUnsupported: '目前環境不支援系統通知',
79
79
  permDefaultHint: '點擊「請求授權」;若瀏覽器未彈出詢問,請用網址列權限圖示手動允許', permDeniedHint: '瀏覽器封鎖了本網站的通知:點擊網址列左側權限圖示 → 網站設定 → 通知 → 允許,然後重新整理頁面', permUnsupportedHint: '系統通知需要 HTTPS 或 localhost 存取;可改用「僅頁內提示」', permRequest: '請求授權',
80
80
  saveAs: '新增', update: '修改', remove: '刪除', presetNamePlaceholder: '預設名稱', confirm: '確定', cancel: '取消', customSaved: '自訂預設已儲存', customUpdated: '自訂預設已更新', customDeleted: '自訂預設已刪除', saveAsTitle: '將當前設定另存為自訂預設', updateTitle: '更新目前自訂預設', removeTitle: '刪除目前自訂預設', fromPreset: '來自:', modifiedSuffix: ' · 已修改',
@@ -85,16 +85,16 @@ window.__ModuleLoader__.load({
85
85
  title: 'Session completion alert', subtitle: 'Language & message templates for completion pushes (refresh after save)',
86
86
  preset: 'Preset', choosePreset: 'Choose preset…', language: 'Language',
87
87
  fields: { completed: 'Completed', error: 'Error', aborted: 'Aborted', blocked: 'Blocked', 'max-tokens': 'Token cap' },
88
- titleLabel: 'Push title', titleHint: 'Leave empty to use the session title',
88
+ titleLabel: 'Push title', titleHint: 'Shared by all reasons; leave empty for per-reason defaults', titleTplLabel: 'Per-reason titles (optional)', titleTplOpen: 'Expand', titleTplClose: 'Collapse', ttPlaceholders: { completed: 'Task completed', error: 'Task failed', aborted: 'Task aborted', blocked: 'Task blocked', 'max-tokens': 'Task hit the output cap' },
89
89
  kindLabels: { completed: 'Session completed', error: 'Session failed', aborted: 'Session aborted', blocked: 'Session blocked', 'max-tokens': 'Session hit the output-token cap' },
90
90
  tokens: { title: 'Session title', label: 'Label', duration: 'Duration', usage: 'Usage', error: 'Error', cache: 'Cache hit', tps: 'Speed TPS' },
91
91
  samples: { title: 'Example session', label: 'Session completed', duration: '12s', usage: '1,240 in / 3,560 out', error: 'connection timeout', cache: '96.5%', tps: '92 tok/s' },
92
92
  presets: { custom: 'Custom preset', unnamed: 'Untitled preset', default: 'Default', playful: 'Playful 🎉', formal: 'Formal', detail: 'Detailed report' },
93
93
  placeholder: 'Custom text: use “+ Insert info”, or type directly',
94
- insertInfo: '+ Insert info', insertTitle: 'Insert info at cursor', preview: 'Preview: ',
94
+ insertInfo: '+ Insert info', insertTitle: 'Insert info at cursor', sendTest: 'Send', sendTestTitle: 'Send test notification (rendered from current template)', preview: 'Preview: ',
95
95
  skipSubagents: 'Skip subagent sessions',
96
96
  pushMode: 'Push channel', pushDual: 'System + in-page', pushSystem: 'System only', pushToast: 'In-page only',
97
- discard: 'Discard', save: 'Save', saving: 'Saving…', saved: 'Saved ✓ · refresh page to apply', saveFailed: 'Save failed: ', refresh: 'Refresh',
97
+ discard: 'Discard', save: 'Save', saving: 'Saving…', saved: 'Saved ✓ · refresh page to apply', saveFailed: 'Save failed: ', refresh: 'Refresh', reset: 'Reset', resetDone: 'Reset to defaults ✓ · refresh page to apply',
98
98
  permTitle: 'Notification permission', permGranted: 'Granted: system notifications available', permDefault: 'Not yet granted', permDenied: 'Blocked by browser', permUnsupported: 'System notifications not supported here',
99
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',
100
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',
@@ -105,16 +105,16 @@ window.__ModuleLoader__.load({
105
105
  title: 'セッション完了通知', subtitle: '完了プッシュの言語とメッセージテンプレート(保存後に再読み込みで反映)',
106
106
  preset: 'プリセット', choosePreset: 'プリセットを選択…', language: '言語',
107
107
  fields: { completed: '完了', error: 'エラー', aborted: '中止', blocked: 'ブロック', 'max-tokens': 'トークン上限' },
108
- titleLabel: 'プッシュタイトル', titleHint: '空欄の場合はセッションタイトルを使用',
108
+ titleLabel: 'プッシュタイトル', titleHint: '全理由共通;空欄なら理由ごとのデフォルトを使用', titleTplLabel: '理由ごとのタイトル(任意)', titleTplOpen: '展開', titleTplClose: '折りたたむ', ttPlaceholders: { completed: 'タスク完了', error: 'タスクエラー', aborted: 'タスク中止', blocked: 'タスクがブロックされました', 'max-tokens': '出力上限に到達' },
109
109
  kindLabels: { completed: 'セッション完了', error: 'セッションエラー', aborted: 'セッション中止', blocked: 'セッションがブロックされました', 'max-tokens': '出力トークン上限に到達' },
110
110
  tokens: { title: 'セッションタイトル', label: '完了ラベル', duration: '所要時間', usage: '消費', error: 'エラー', cache: 'キャッシュヒット', tps: '速度 TPS' },
111
111
  samples: { title: 'サンプルセッション', label: 'セッション完了', duration: '12 秒', usage: '1,240 入力 / 3,560 出力', error: '接続タイムアウト', cache: '96.5%', tps: '92 tok/s' },
112
112
  presets: { custom: 'カスタムプリセット', unnamed: '未命名プリセット', default: 'デフォルト', playful: 'カジュアル 🎉', formal: 'フォーマル', detail: '詳細レポート' },
113
113
  placeholder: 'カスタム文面:「+ 情報を挿入」または直接入力',
114
- insertInfo: '+ 情報を挿入', insertTitle: 'カーソル位置に情報を挿入', preview: 'プレビュー:',
114
+ insertInfo: '+ 情報を挿入', insertTitle: 'カーソル位置に情報を挿入', sendTest: '送信', sendTestTitle: 'テスト通知を送信(現在のテンプレートで描画)', preview: 'プレビュー:',
115
115
  skipSubagents: 'サブエージェントセッションをスキップ',
116
116
  pushMode: '通知チャネル', pushDual: 'システム+ページ内', pushSystem: 'システムのみ', pushToast: 'ページ内のみ',
117
- discard: '破棄', save: '保存', saving: '保存中…', saved: '保存しました ✓ · ページを再読み込み', saveFailed: '保存失敗:', refresh: '再読み込み',
117
+ discard: '破棄', save: '保存', saving: '保存中…', saved: '保存しました ✓ · ページを再読み込み', saveFailed: '保存失敗:', refresh: '再読み込み', reset: 'リセット', resetDone: 'デフォルトに戻しました ✓ · ページを再読み込み',
118
118
  permTitle: '通知権限', permGranted: '許可済み:システム通知が利用可能', permDefault: '未許可', permDenied: 'ブラウザがブロック', permUnsupported: 'この環境ではシステム通知を利用できません',
119
119
  permDefaultHint: '「許可をリクエスト」をクリック。ブラウザに確認が出ない場合は、アドレスバーの権限アイコンから手動で許可', permDeniedHint: 'このサイトの通知がブロックされています:アドレスバー左の権限アイコン → サイト設定 → 通知 → 許可 → ページを再読み込み', permUnsupportedHint: 'システム通知には HTTPS または localhost が必要です。「ページ内のみ」を推奨', permRequest: '許可をリクエスト',
120
120
  saveAs: '新規', update: '編集', remove: '削除', presetNamePlaceholder: 'プリセット名', confirm: '決定', cancel: 'キャンセル', customSaved: 'カスタムプリセットを保存しました', customUpdated: 'カスタムプリセットを更新しました', customDeleted: 'カスタムプリセットを削除しました', saveAsTitle: '現在の設定をカスタムプリセットとして保存', updateTitle: '現在のカスタムプリセットを更新', removeTitle: '現在のカスタムプリセットを削除', fromPreset: '由来:', modifiedSuffix: ' · 変更あり',
@@ -125,16 +125,16 @@ window.__ModuleLoader__.load({
125
125
  title: '세션 완료 알림', subtitle: '완료 푸시의 언어 및 메시지 템플릿(저장 후 새로고침 시 적용)',
126
126
  preset: '프리셋', choosePreset: '프리셋 선택…', language: '언어',
127
127
  fields: { completed: '완료', error: '오류', aborted: '중단', blocked: '차단', 'max-tokens': '토큰 한도' },
128
- titleLabel: '푸시 제목', titleHint: '비우면 세션 제목 사용',
128
+ titleLabel: '푸시 제목', titleHint: '모든 이유 공통;비우면 이유별 기본값 사용', titleTplLabel: '이유별 제목 (선택)', titleTplOpen: '펼치기', titleTplClose: '접기', ttPlaceholders: { completed: '작업 완료', error: '작업 오류', aborted: '작업 중단됨', blocked: '작업 차단됨', 'max-tokens': '출력 한도 도달' },
129
129
  kindLabels: { completed: '세션 완료', error: '세션 오류', aborted: '세션 중단됨', blocked: '세션 차단됨', 'max-tokens': '출력 토큰 한도 도달' },
130
130
  tokens: { title: '세션 제목', label: '완료 라벨', duration: '소요 시간', usage: '소모', error: '오류', cache: '캐시 히트', tps: '속도 TPS' },
131
131
  samples: { title: '예시 세션', label: '세션 완료', duration: '12초', usage: '1,240 입력 / 3,560 출력', error: '연결 시간 초과', cache: '96.5%', tps: '92 tok/s' },
132
132
  presets: { custom: '사용자 지정 프리셋', unnamed: '이름 없는 프리셋', default: '기본', playful: '캐주얼 🎉', formal: '정식', detail: '상세 보고' },
133
133
  placeholder: '사용자 지정 문구: "+ 정보 삽입" 또는 직접 입력',
134
- insertInfo: '+ 정보 삽입', insertTitle: '커서 위치에 정보 삽입', preview: '미리보기: ',
134
+ insertInfo: '+ 정보 삽입', insertTitle: '커서 위치에 정보 삽입', sendTest: '보내기', sendTestTitle: '테스트 알림 보내기 (현재 템플릿으로 렌더링)', preview: '미리보기: ',
135
135
  skipSubagents: '하위 에이전트 세션 건너뛰기',
136
136
  pushMode: '알림 채널', pushDual: '시스템+페이지 내', pushSystem: '시스템만', pushToast: '페이지 내만',
137
- discard: '취소', save: '저장', saving: '저장 중…', saved: '저장됨 ✓ · 새로고침 후 적용', saveFailed: '저장 실패: ', refresh: '새로고침',
137
+ discard: '취소', save: '저장', saving: '저장 중…', saved: '저장됨 ✓ · 새로고침 후 적용', saveFailed: '저장 실패: ', refresh: '새로고침', reset: '초기화', resetDone: '기본값으로 초기화됨 ✓ · 새로고침 후 적용',
138
138
  permTitle: '알림 권한', permGranted: '허용됨: 시스템 알림 사용 가능', permDefault: '아직 허용되지 않음', permDenied: '브라우저에서 차단됨', permUnsupported: '이 환경에서는 시스템 알림을 사용할 수 없음',
139
139
  permDefaultHint: '「권한 요청」클릭. 브라우저에 확인이 없으면 주소 표시줄 권한 아이콘에서 수동 허용', permDeniedHint: '이 사이트의 알림이 차단되었습니다: 주소 표시줄 왼쪽 권한 아이콘 → 사이트 설정 → 알림 → 허용 후 새로고침', permUnsupportedHint: '시스템 알림은 HTTPS 또는 localhost가 필요합니다. 「페이지 내만」사용 권장', permRequest: '권한 요청',
140
140
  saveAs: '추가', update: '수정', remove: '삭제', presetNamePlaceholder: '프리셋 이름', confirm: '확인', cancel: '취소', customSaved: '사용자 지정 프리셋 저장됨', customUpdated: '사용자 지정 프리셋 업데이트됨', customDeleted: '사용자 지정 프리셋 삭제됨', saveAsTitle: '현재 구성을 사용자 지정 프리셋으로 저장', updateTitle: '현재 사용자 지정 프리셋 업데이트', removeTitle: '현재 사용자 지정 프리셋 삭제', fromPreset: '출처: ', modifiedSuffix: ' · 수정됨',
@@ -195,10 +195,10 @@ window.__ModuleLoader__.load({
195
195
  var cwdLine = workspaceLine(s)
196
196
  var start = Date.now()
197
197
  function attempt() {
198
- var notice = readNoticeAny(id)
199
- if (notice || Date.now() - start > 6000) {
198
+ var proj = readNoticeAny(id)
199
+ if (proj || Date.now() - start > 6000) {
200
200
  retryTimer = null
201
- notifyUser(pushTitle(name, notice ? '' : ' 已完成'), notice || (cwdLine ? cwdLine + ' · ' : '') + '详情见会话内系统消息', pushModeOf())
201
+ notifyUser(pushTitle(name, proj ? proj.title : '', proj ? '' : ' 已完成'), (proj && proj.text) || (cwdLine ? cwdLine + ' · ' : '') + '详情见会话内系统消息', pushModeOf())
202
202
  return
203
203
  }
204
204
  retryTimer = setTimeout(attempt, 400)
@@ -217,9 +217,16 @@ window.__ModuleLoader__.load({
217
217
  return 'dual'
218
218
  }
219
219
 
220
- /** 推送标题:用户模板({title} = 会话标题);无通知时附带完成标记。 */
221
- function pushTitle(name, suffix) {
222
- var tpl = '{title}'
220
+ /**
221
+ * 推送标题:优先级——
222
+ * 1. 宿主投影渲染好的该原因标题(projTitle,含 {title} 替换);
223
+ * 2. 用户全局 titleTemplate({title} = 会话标题);
224
+ * 3. 默认「任务已完成」。
225
+ * 无通知正文时(降级)附带完成标记 suffix。
226
+ */
227
+ function pushTitle(name, projTitle, suffix) {
228
+ if (typeof projTitle === 'string' && projTitle.trim() !== '') return projTitle.trim() + (suffix || '')
229
+ var tpl = '任务已完成'
223
230
  try {
224
231
  if (notifyScope) {
225
232
  var st = notifyScope.getSnapshot()
@@ -227,7 +234,7 @@ window.__ModuleLoader__.load({
227
234
  }
228
235
  } catch (e) { /* 默认 */ }
229
236
  var out = String(tpl).replaceAll('{title}', name).trim()
230
- if (out === '') out = name
237
+ if (out === '') out = '任务已完成'
231
238
  return out + (suffix || '')
232
239
  }
233
240
 
@@ -237,14 +244,26 @@ window.__ModuleLoader__.load({
237
244
  * 2. 会话事件窗口里的 notice 节点(正在看的会话,落盘后立刻可用);
238
245
  * 3. 无 → 降级(工作区信息)。
239
246
  */
247
+ /**
248
+ * 读取完成通知:返回 { text, title } | null。
249
+ * 优先宿主投影(新格式对象 {kind,text,title};兼容旧版字符串值);
250
+ * 投影缺失时从会话事件窗口读 notice 节点(此时无该原因标题,title 为空串 → 走模板/默认)。
251
+ */
240
252
  function readNoticeAny(id) {
241
253
  try {
242
254
  var snap = list.getSnapshot()
243
255
  var cur = snap && snap.byId && snap.byId[id]
244
256
  var pv = cur && cur.projectionValues && cur.projectionValues[NOTICE_PROJECTION_KEY]
245
- if (typeof pv === 'string' && pv.trim() !== '') return pv
257
+ if (pv != null) {
258
+ if (typeof pv === 'string' && pv.trim() !== '') return { text: pv, title: '' }
259
+ if (typeof pv === 'object' && pv !== null) {
260
+ var txt = typeof pv.text === 'string' ? pv.text : ''
261
+ if (txt.trim() !== '') return { text: txt, title: typeof pv.title === 'string' ? pv.title : '' }
262
+ }
263
+ }
246
264
  } catch (e) { /* 投影缺失 → 走窗口路径 */ }
247
- return readNotice(id)
265
+ var fallback = readNotice(id)
266
+ return fallback ? { text: fallback, title: '' } : null
248
267
  }
249
268
 
250
269
  /** 从会话事件窗口找宿主写入的最新 notice 全文(kind=context + form=notice)。 */
@@ -505,9 +524,18 @@ window.__ModuleLoader__.load({
505
524
 
506
525
  /** 系统预设:仅保留「默认」(作为基线,其余以自定义预设承载)。 */
507
526
  var PRESETS = [
508
- { id: 'default', labelKey: 'default', language: 'zh', titleTemplate: '', templates: { completed: '', error: '', aborted: '', blocked: '', 'max-tokens': '' } },
527
+ { id: 'default', labelKey: 'default', language: 'zh', titleTemplate: '', titleTemplates: { completed: '', error: '', aborted: '', blocked: '', 'max-tokens': '' }, templates: { completed: '', error: '', aborted: '', blocked: '', 'max-tokens': '' } },
509
528
  ]
510
529
 
530
+ /** 重置目标:与宿主 DEFAULT_SETTINGS 一致(设置面板可编辑字段子集;语言保留当前值)。 */
531
+ var DEFAULT_FLAT = {
532
+ language: 'zh',
533
+ titleTemplate: '',
534
+ titleTemplates: { completed: '', error: '', aborted: '', blocked: '', 'max-tokens': '' },
535
+ pushMode: 'dual',
536
+ templates: { completed: '', error: '', aborted: '', blocked: '', 'max-tokens': '' },
537
+ }
538
+
511
539
  /** 本地存储的用户自定义预设库。 */
512
540
  var CUSTOM_PRESETS_KEY = 'dsh-scn-custom-presets'
513
541
  function loadCustomPresets() {
@@ -561,6 +589,7 @@ window.__ModuleLoader__.load({
561
589
  if (String(cur.titleTemplate ?? '') !== String(p.titleTemplate ?? '')) return false
562
590
  for (var j = 0; j < REASON_FIELDS.length; j++) {
563
591
  if (templateOf(cur, REASON_FIELDS[j]) !== String(p.templates[REASON_FIELDS[j]] ?? '').trim()) return false
592
+ if (titleTplOf(cur, REASON_FIELDS[j]) !== String((p.titleTemplates && p.titleTemplates[REASON_FIELDS[j]]) ?? '').trim()) return false
564
593
  }
565
594
  return true
566
595
  }
@@ -591,6 +620,11 @@ window.__ModuleLoader__.load({
591
620
  return String(cur && cur['tpl-' + field] !== undefined ? cur['tpl-' + field] : (tpl[field] ?? ''))
592
621
  .replace(/\{label\}/g, '').trim()
593
622
  }
623
+ /** 按原因定制标题:草稿键 tt-<field> 优先,其次已保存的 titleTemplates[<field>]。 */
624
+ function titleTplOf(cur, field) {
625
+ var tt = (cur && cur.titleTemplates) || {}
626
+ return String(cur && cur['tt-' + field] !== undefined ? cur['tt-' + field] : (tt[field] ?? '')).trim()
627
+ }
594
628
  /** 扁平快照(用于 dirty 对比与模板打包)。 */
595
629
  function flatOf(cur) {
596
630
  var out = {
@@ -600,6 +634,7 @@ window.__ModuleLoader__.load({
600
634
  pushMode: ['dual', 'system', 'toast'].indexOf(cur.pushMode) >= 0 ? cur.pushMode : 'dual',
601
635
  }
602
636
  for (var i = 0; i < REASON_FIELDS.length; i++) out['tpl-' + REASON_FIELDS[i]] = templateOf(cur, REASON_FIELDS[i])
637
+ for (var j = 0; j < REASON_FIELDS.length; j++) out['tt-' + REASON_FIELDS[j]] = titleTplOf(cur, REASON_FIELDS[j])
603
638
  return out
604
639
  }
605
640
  function sameFlat(a, b) {
@@ -750,6 +785,9 @@ window.__ModuleLoader__.load({
750
785
  function syncFromDom() {
751
786
  var tpl = domToTemplate()
752
787
  lastTpl.current = tpl
788
+ // 占位提示只随内容变化:有输入 → 隐藏;清空 → 恢复(hint 可编辑内容或 placeholder)
789
+ if (tpl !== '') clearPlaceholder()
790
+ else rebuild('')
753
791
  propsRef.current.onChange(tpl)
754
792
  }
755
793
 
@@ -833,9 +871,8 @@ window.__ModuleLoader__.load({
833
871
  contentEditable: true,
834
872
  suppressContentEditableWarning: true,
835
873
  onInput: syncFromDom,
836
- onMouseDown: function () { clearPlaceholder() },
874
+ onMouseDown: function () { /* 不清理占位:点击聚焦不消失,输入内容才隐藏 */ },
837
875
  onKeyDown: function (e) {
838
- clearPlaceholder()
839
876
  if (e.key === 'Enter') { e.preventDefault(); return }
840
877
  // Ctrl/Cmd+A:手动扩选“全部子节点(含胶囊)”——浏览器全选天然跳过
841
878
  // contenteditable=false 的胶囊,导致全选删除后胶囊残留
@@ -873,18 +910,25 @@ window.__ModuleLoader__.load({
873
910
  })
874
911
  }
875
912
 
876
- /** 内置默认内容的提示串(空模板时直接展示真实默认效果:文字 + 信息标签)。 */
913
+ /** 各语言「会话」前缀(用于默认提示中在标签内插入会话标题标签)。 */
914
+ var SESSION_PREFIX = { zh: '会话', 'zh-tw': '會話', en: 'Session ', ja: 'セッション', ko: '세션 ' }
915
+
916
+ /** 内置默认内容的提示串(空模板时直接展示真实默认效果:会话「会话标题」+ 信息标签)。 */
877
917
  function defaultHintOf(field, langCode) {
878
918
  var lang = LANG_IDS.indexOf(langCode) >= 0 ? langCode : 'zh'
879
919
  var tt = tOf(lang)
880
920
  var label = (tt.kindLabels && tt.kindLabels[field]) || tt.samples.label
881
921
  var d = '{duration}'
882
922
  var u = '{usage}'
883
- if (lang === 'en') return label + ' (took ' + d + ', used ' + u + ').'
884
- if (lang === 'ja') return label + '(所要 ' + d + '、消費 ' + u + ')。'
885
- if (lang === 'ko') return label + '(소요 ' + d + ', 소모 ' + u + ')。'
886
- if (lang === 'zh-tw') return label + '(用時 ' + d + ',消耗 ' + u + ')。'
887
- return label + '(用时 ' + d + ',消耗 ' + u + ')。'
923
+ var prefix = SESSION_PREFIX[lang] || '会话'
924
+ var rest = String(label).slice(prefix.length) // 剥掉「会话」前缀,保留原因后缀(已完成/中止/…)
925
+ var titleTag = lang === 'en' ? '"{title}"' : '「{title}」'
926
+ var head = prefix + titleTag + ((lang === 'en' || lang === 'ko') ? ' ' : '') + rest
927
+ if (lang === 'en') return head + ' (took ' + d + ', used ' + u + ').'
928
+ if (lang === 'ja') return head + '(所要 ' + d + '、消費 ' + u + ')。'
929
+ if (lang === 'ko') return head + '(소요 ' + d + ', 소모 ' + u + ')。'
930
+ if (lang === 'zh-tw') return head + '(用時 ' + d + ',消耗 ' + u + ')。'
931
+ return head + '(用时 ' + d + ',消耗 ' + u + ')。'
888
932
  }
889
933
 
890
934
  return function SettingsCard() {
@@ -928,6 +972,9 @@ window.__ModuleLoader__.load({
928
972
  var permTickState = useState(0) // 通知权限状态刷新计数(请求授权后 setPermTick(+1) 重渲染)
929
973
  var permTick = permTickState[0]
930
974
  var setPermTick = permTickState[1]
975
+ var titleTplOpenState = useState(false) // 「按原因定制标题」折叠区展开状态
976
+ var titleTplOpen = titleTplOpenState[0]
977
+ var setTitleTplOpen = titleTplOpenState[1]
931
978
 
932
979
  /** 用户手势内请求通知权限(Chromium 忽略非手势的自动请求,必须由点击触发);授权结果经重渲染刷新状态区。 */
933
980
  function requestPermissionNow() {
@@ -1017,6 +1064,10 @@ window.__ModuleLoader__.load({
1017
1064
  setCustomPresets(next)
1018
1065
  saveCustomPresets(next)
1019
1066
  setActivePreset(null)
1067
+ // 删除的是当前使用的预设 → 表单恢复默认预设内容(presetIdOf 自动匹配「默认」)
1068
+ if (shownEntry.id === (activePreset && activePreset.id)) {
1069
+ setDraft(Object.assign({}, DEFAULT_FLAT))
1070
+ }
1020
1071
  setNote(t.customDeleted)
1021
1072
  log('custom preset deleted')
1022
1073
  }
@@ -1024,15 +1075,18 @@ window.__ModuleLoader__.load({
1024
1075
  function save() {
1025
1076
  var flat = flatOf(cur)
1026
1077
  var templates = {}
1078
+ var titleTemplates = {}
1027
1079
  for (var i = 0; i < REASON_FIELDS.length; i++) {
1028
1080
  var k = REASON_FIELDS[i]
1029
1081
  templates[k] = flat['tpl-' + k]
1082
+ titleTemplates[k] = flat['tt-' + k]
1030
1083
  }
1031
1084
  setNote(t.saving)
1032
1085
  var tasks = [
1033
1086
  scope.set('language', flat.language),
1034
1087
  scope.set('templates', templates),
1035
1088
  scope.set('titleTemplate', flat.titleTemplate),
1089
+ scope.set('titleTemplates', titleTemplates),
1036
1090
  scope.set('pushMode', flat.pushMode),
1037
1091
  ]
1038
1092
  Promise.all(tasks).then(function () {
@@ -1046,7 +1100,7 @@ window.__ModuleLoader__.load({
1046
1100
  var found = null
1047
1101
  if (shownCustom && shownEntry) {
1048
1102
  targetId = shownEntry.id
1049
- entry = Object.assign({}, shownEntry, { language: lang, titleTemplate: flat.titleTemplate, templates: templates })
1103
+ entry = Object.assign({}, shownEntry, { language: lang, titleTemplate: flat.titleTemplate, titleTemplates: titleTemplates, templates: templates })
1050
1104
  list = customPresets.map(function (cp) {
1051
1105
  if (cp.id === targetId) { found = Object.assign({}, cp, entry); return found }
1052
1106
  return cp
@@ -1055,7 +1109,7 @@ window.__ModuleLoader__.load({
1055
1109
  } else {
1056
1110
  var n = nextUnnamedNumber(customPresets)
1057
1111
  targetId = n === 1 ? 'unnamed' : 'unnamed-' + n
1058
- entry = { id: targetId, name: null, auto: true, language: lang, titleTemplate: flat.titleTemplate, templates: templates }
1112
+ entry = { id: targetId, name: null, auto: true, language: lang, titleTemplate: flat.titleTemplate, titleTemplates: titleTemplates, templates: templates }
1059
1113
  list = customPresets.concat([entry])
1060
1114
  found = entry
1061
1115
  }
@@ -1072,6 +1126,52 @@ window.__ModuleLoader__.load({
1072
1126
  })
1073
1127
  }
1074
1128
 
1129
+ /** 发送测试通知:用当前模板(空模板用默认文案)渲染该原因的通知;标题按原因优先级。 */
1130
+ function sendTestNotice(field) {
1131
+ try {
1132
+ var samples = t.samples
1133
+ var titleTpl = titleTplOf(cur, field) || String(cur.titleTemplate || '').trim() || t.ttPlaceholders[field]
1134
+ var title = String(titleTpl).replaceAll('{title}', samples.title).trim() || samples.title
1135
+ var tpl = templateOf(cur, field)
1136
+ var bodyTpl = tpl !== '' ? tpl : defaultHintOf(field, cur.language)
1137
+ var body = String(bodyTpl)
1138
+ .replace(/\{label\}/g, '')
1139
+ .replaceAll('{title}', samples.title)
1140
+ .replaceAll('{duration}', samples.duration)
1141
+ .replaceAll('{usage}', samples.usage)
1142
+ .replaceAll('{error}', samples.error)
1143
+ .replaceAll('{cache}', samples.cache)
1144
+ .replaceAll('{tps}', samples.tps)
1145
+ notifyUser(title, body, ['dual', 'system', 'toast'].indexOf(cur.pushMode) >= 0 ? cur.pushMode : 'dual')
1146
+ log('test notice sent: ' + field)
1147
+ } catch (e) {
1148
+ log('test notice failed: ' + (e && e.message ? e.message : e))
1149
+ }
1150
+ }
1151
+
1152
+ /** 一键重置:除语言外全部还原为默认值(语言保留当前选择)并立即保存。 */
1153
+ function resetAll() {
1154
+ var def = DEFAULT_FLAT
1155
+ var lang = LANG_IDS.indexOf(cur.language) >= 0 ? cur.language : 'zh' // 重置不改变语言
1156
+ setNote(t.saving)
1157
+ var tasks = [
1158
+ scope.set('language', lang),
1159
+ scope.set('templates', def.templates),
1160
+ scope.set('titleTemplate', def.titleTemplate),
1161
+ scope.set('titleTemplates', def.titleTemplates),
1162
+ scope.set('pushMode', def.pushMode),
1163
+ ]
1164
+ Promise.all(tasks).then(function () {
1165
+ setDraft(null)
1166
+ setNote(t.resetDone)
1167
+ setJustSaved(true)
1168
+ log('settings reset to defaults (language kept: ' + lang + ')')
1169
+ }).catch(function (e) {
1170
+ setNote(t.saveFailed + (e && e.message ? e.message : e))
1171
+ log('settings reset failed: ' + (e && e.message ? e.message : e))
1172
+ })
1173
+ }
1174
+
1075
1175
  function discard() {
1076
1176
  setDraft(null)
1077
1177
  setNote('')
@@ -1176,13 +1276,37 @@ window.__ModuleLoader__.load({
1176
1276
  var handle = editorRefs.current.title
1177
1277
  if (handle) handle.insertAtCaret('title')
1178
1278
  else setVal('titleTemplate', String(cur.titleTemplate ?? '') + '{title}')
1179
- setMenuFor(null)
1279
+ // 插入后保持菜单展开:方便连续插入多个标签,由用户自行折叠
1180
1280
  },
1181
1281
  style: { appearance: 'none', font: 'inherit', fontSize: '12px', cursor: 'pointer', padding: '4px 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)' },
1182
1282
  }, '+ ' + t.tokens.title)
1183
1283
  )
1184
1284
  : null,
1185
1285
  ),
1286
+ // 按原因定制标题(折叠区,避免界面臃肿):留空 = 用上方全局模板或语言默认标题
1287
+ h('div', { key: 'title-tpl', style: { display: 'flex', flexDirection: 'column', gap: '6px', padding: '12px 0', borderTop: DIVIDER } },
1288
+ h('button', {
1289
+ type: 'button',
1290
+ onClick: function () { setTitleTplOpen(!titleTplOpen) },
1291
+ style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', appearance: 'none', background: 'none', border: 'none', cursor: 'pointer', padding: '0', font: 'inherit', textAlign: 'left' },
1292
+ },
1293
+ h('span', { style: { fontSize: '13px', color: 'var(--dsw-alias-label-secondary, inherit)' } }, t.titleTplLabel),
1294
+ h('span', { style: { fontSize: '12px', color: SUBTLE } }, titleTplOpen ? t.titleTplClose : t.titleTplOpen),
1295
+ ),
1296
+ titleTplOpen
1297
+ ? REASON_FIELDS.map(function (field) {
1298
+ return h('div', { key: 'tt-' + field, style: { display: 'flex', alignItems: 'center', gap: '8px' } },
1299
+ h('span', { style: { width: '76px', flex: 'none', fontSize: '12px', color: 'var(--dsw-alias-label-tertiary, rgba(127,127,127,0.8))' } }, t.fields[field]),
1300
+ h('input', {
1301
+ value: titleTplOf(cur, field),
1302
+ placeholder: t.ttPlaceholders[field],
1303
+ onChange: function (e) { setVal('tt-' + field, e.target.value) },
1304
+ style: Object.assign({}, SELECT_STYLE, { flex: 1, fontSize: '13px' }),
1305
+ }),
1306
+ )
1307
+ })
1308
+ : null,
1309
+ ),
1186
1310
  ]
1187
1311
  // 模板(forEach:每条回调独立绑定字段,避免 var 闭包串写)
1188
1312
  // 每栏 = 标签行(左文案 + 右「+插入信息」)+ ChipEditor(文字+内联胶囊,光标处插入)
@@ -1196,18 +1320,30 @@ window.__ModuleLoader__.load({
1196
1320
  key: 'tpl-' + field,
1197
1321
  style: { display: 'flex', flexDirection: 'column', gap: '6px', padding: '12px 0', borderTop: DIVIDER },
1198
1322
  },
1199
- h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
1323
+ h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: '8px' } },
1200
1324
  h('div', { style: { fontSize: '13px', color: 'var(--dsw-alias-label-secondary, inherit)' } }, t.fields[field]),
1201
- h('button', {
1202
- type: 'button',
1203
- title: t.insertTitle,
1204
- onClick: function () { setMenuFor(menuFor === field ? null : field) },
1205
- style: {
1206
- appearance: 'none', font: 'inherit', fontSize: '12px', cursor: 'pointer', padding: '3px 10px',
1207
- borderRadius: '999px', border: '1px solid var(--dsw-alias-border-l2, rgba(127,127,127,0.35))',
1208
- background: 'none', color: 'var(--dsw-alias-label-secondary, inherit)',
1209
- },
1210
- }, t.insertInfo),
1325
+ h('div', { style: { display: 'flex', gap: '6px' } },
1326
+ h('button', {
1327
+ type: 'button',
1328
+ title: t.sendTestTitle,
1329
+ onClick: function () { sendTestNotice(field) },
1330
+ style: {
1331
+ appearance: 'none', font: 'inherit', fontSize: '12px', cursor: 'pointer', padding: '3px 10px',
1332
+ borderRadius: '999px', border: '1px solid var(--dsw-alias-border-l2, rgba(127,127,127,0.35))',
1333
+ background: 'none', color: 'var(--dsw-alias-label-secondary, inherit)',
1334
+ },
1335
+ }, t.sendTest),
1336
+ h('button', {
1337
+ type: 'button',
1338
+ title: t.insertTitle,
1339
+ onClick: function () { setMenuFor(menuFor === field ? null : field) },
1340
+ style: {
1341
+ appearance: 'none', font: 'inherit', fontSize: '12px', cursor: 'pointer', padding: '3px 10px',
1342
+ borderRadius: '999px', border: '1px solid var(--dsw-alias-border-l2, rgba(127,127,127,0.35))',
1343
+ background: 'none', color: 'var(--dsw-alias-label-secondary, inherit)',
1344
+ },
1345
+ }, t.insertInfo),
1346
+ ),
1211
1347
  ),
1212
1348
  h(ChipEditor, {
1213
1349
  template: tplValue,
@@ -1234,7 +1370,7 @@ window.__ModuleLoader__.load({
1234
1370
  var handle = editorRefs.current[field]
1235
1371
  if (handle) handle.insertAtCaret(k)
1236
1372
  else setVal('tpl-' + field, templateOf(cur, field) + '{' + k + '}')
1237
- setMenuFor(null)
1373
+ // 插入后保持菜单展开:方便连续插入多个标签,由用户自行折叠
1238
1374
  },
1239
1375
  style: {
1240
1376
  appearance: 'none', font: 'inherit', fontSize: '12px', cursor: 'pointer', padding: '4px 10px',
@@ -1277,6 +1413,10 @@ window.__ModuleLoader__.load({
1277
1413
  }, t.refresh)
1278
1414
  : null,
1279
1415
  ),
1416
+ h('button', {
1417
+ type: 'button', disabled: note === t.saving, onClick: resetAll,
1418
+ style: Object.assign({}, GHOST, { opacity: note === t.saving ? 0.4 : 1 }),
1419
+ }, t.reset),
1280
1420
  h('button', {
1281
1421
  type: 'button', disabled: !dirty || note === t.saving, onClick: discard,
1282
1422
  style: Object.assign({}, GHOST, { opacity: dirty ? 1 : 0.4 }),