@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/README.md +583 -570
- package/lib/client.js +188 -48
- package/lib/core.js +103 -1
- package/lib/index.js +45 -9
- package/package.json +70 -70
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: '
|
|
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
|
|
199
|
-
if (
|
|
198
|
+
var proj = readNoticeAny(id)
|
|
199
|
+
if (proj || Date.now() - start > 6000) {
|
|
200
200
|
retryTimer = null
|
|
201
|
-
notifyUser(pushTitle(name,
|
|
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
|
-
/**
|
|
221
|
-
|
|
222
|
-
|
|
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 =
|
|
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 (
|
|
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
|
-
|
|
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 () {
|
|
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
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
return
|
|
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
|
-
|
|
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('
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
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
|
-
|
|
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 }),
|