@telosmaylx/dsh-session-notify 0.1.12 → 0.1.16
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.en.md +18 -26
- package/README.ja.md +18 -26
- package/README.ko.md +18 -26
- package/README.md +17 -25
- package/README.zh-TW.md +18 -26
- package/lib/client.js +366 -45
- package/lib/index.js +8 -2
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -52,13 +52,13 @@ window.__ModuleLoader__.load({
|
|
|
52
52
|
imagePickTitle: '选择本地图片(自动压缩为通知大图)', iconPickTitle: '选择本地图片(自动压缩为通知图标)', imageFailed: '图片读取失败',
|
|
53
53
|
clearMedia: '清除',
|
|
54
54
|
mediaNote: '本地上传会自动处理:大图按通知显示比例 16:9 居中裁切(512×288)、图标 1:1 方形裁切(128×128),裁切后即通知中显示的效果',
|
|
55
|
-
presets: { custom: '自定义预设', unnamed: '未命名预设', default: '默认',
|
|
55
|
+
presets: { custom: '自定义预设', unnamed: '未命名预设', default: '默认', kaomoji: '颜文字', felyne: '艾露猫', neko: '猫娘', deepseekChan: 'DeepSeek 娘' },
|
|
56
56
|
placeholder: '留空则使用默认文案',
|
|
57
57
|
insertInfo: '+ 插入信息', insertTitle: '在光标处插入信息', sendTestTitle: '发送测试通知(当前模板渲染)',
|
|
58
58
|
skipSubagents: '跳过子代理会话',
|
|
59
59
|
pushMode: '推送方式', pushDual: '系统通知 + 页内提示', pushSystem: '仅系统通知', pushToast: '仅页内提示',
|
|
60
60
|
imageUrl: '通知大图', iconUrl: '通知图标',
|
|
61
|
-
discard: '放弃', save: '保存', saving: '保存中…', saved: '已保存 ✓ · 刷新页面后生效', saveFailed: '保存失败:', refresh: '点击刷新', reset: '重置', resetDone: '已重置为默认值 · 刷新页面后生效',
|
|
61
|
+
discard: '放弃', save: '保存', saving: '保存中…', saved: '已保存 ✓ · 刷新页面后生效', customDeleteConfirm: '确定删除该自定义预设?此操作不可恢复。', presetDeletedKeepApplied: '已应用的配置不受影响;如需恢复默认请点「重置」', presetNameExists: '已存在同名预设,请换一个名称', saveDuringEdit: '已保存,但保存期间有新修改未保存。', noChange: '当前配置与默认一致,无需保存', saveFailed: '保存失败:', refresh: '点击刷新', reset: '重置', resetDone: '已重置为默认值 · 刷新页面后生效',
|
|
62
62
|
permTitle: '通知权限', permGranted: '已授权:系统通知可用', permDefault: '尚未授权', permDenied: '已被浏览器屏蔽', permUnsupported: '当前环境不支持系统通知',
|
|
63
63
|
permDefaultHint: '点击「请求授权」;若浏览器未弹出询问,请用地址栏权限图标手动允许', permDeniedHint: '浏览器阻止了本网站的通知:点击地址栏左侧权限图标 → 网站设置 → 通知 → 允许,然后刷新页面', permUnsupportedHint: '系统通知需要 HTTPS 或 localhost 访问;可改用「仅页内提示」', permRequest: '请求授权',
|
|
64
64
|
saveAs: '新增', rename: '重命名', update: '修改', remove: '删除', presetNamePlaceholder: '预设名称', confirm: '确定', cancel: '取消', customSaved: '自定义预设已保存', customUpdated: '自定义预设已更新', customDeleted: '自定义预设已删除', saveAsTitle: '将当前配置另存为自定义预设', renameTitle: '重命名当前自定义预设', updateTitle: '更新当前自定义预设', removeTitle: '删除当前自定义预设', fromPreset: '来自:', modifiedSuffix: ' · 已修改',
|
|
@@ -76,13 +76,13 @@ window.__ModuleLoader__.load({
|
|
|
76
76
|
imagePickTitle: '選擇本機圖片(自動壓縮為通知大圖)', iconPickTitle: '選擇本機圖片(自動壓縮為通知圖示)', imageFailed: '圖片讀取失敗',
|
|
77
77
|
clearMedia: '清除',
|
|
78
78
|
mediaNote: '本機上傳會自動處理:大圖按通知顯示比例 16:9 居中裁切(512×288)、圖示 1:1 方形裁切(128×128),裁切後即通知中顯示的效果',
|
|
79
|
-
presets: { custom: '自訂預設', unnamed: '未命名預設', default: '預設',
|
|
79
|
+
presets: { custom: '自訂預設', unnamed: '未命名預設', default: '預設', kaomoji: '顏文字', felyne: '艾露貓', neko: '貓娘', deepseekChan: 'DeepSeek 娘' },
|
|
80
80
|
placeholder: '留空則使用預設文案',
|
|
81
81
|
insertInfo: '+ 插入資訊', insertTitle: '在游標處插入資訊', sendTestTitle: '傳送測試通知(目前範本渲染)',
|
|
82
82
|
skipSubagents: '跳過子代理會話',
|
|
83
83
|
pushMode: '推送方式', pushDual: '系統通知 + 頁內提示', pushSystem: '僅系統通知', pushToast: '僅頁內提示',
|
|
84
84
|
imageUrl: '通知大圖', iconUrl: '通知圖示',
|
|
85
|
-
discard: '放棄', save: '儲存', saving: '儲存中…', saved: '已儲存 ✓ · 重新整理頁面後生效', saveFailed: '儲存失敗:', refresh: '點擊重新整理', reset: '重置', resetDone: '已重置為預設值 · 重新整理頁面後生效',
|
|
85
|
+
discard: '放棄', save: '儲存', saving: '儲存中…', saved: '已儲存 ✓ · 重新整理頁面後生效', customDeleteConfirm: '確定刪除該自訂預設?此操作不可復原。', presetDeletedKeepApplied: '已套用的設定不受影響;如需恢復預設請點「重設」', presetNameExists: '已存在同名預設,請換一個名稱', saveDuringEdit: '已儲存,但儲存期間有新修改未儲存。', noChange: '目前設定與預設一致,無需儲存', saveFailed: '儲存失敗:', refresh: '點擊重新整理', reset: '重置', resetDone: '已重置為預設值 · 重新整理頁面後生效',
|
|
86
86
|
permTitle: '通知權限', permGranted: '已授權:系統通知可用', permDefault: '尚未授權', permDenied: '已被瀏覽器封鎖', permUnsupported: '目前環境不支援系統通知',
|
|
87
87
|
permDefaultHint: '點擊「請求授權」;若瀏覽器未彈出詢問,請用網址列權限圖示手動允許', permDeniedHint: '瀏覽器封鎖了本網站的通知:點擊網址列左側權限圖示 → 網站設定 → 通知 → 允許,然後重新整理頁面', permUnsupportedHint: '系統通知需要 HTTPS 或 localhost 存取;可改用「僅頁內提示」', permRequest: '請求授權',
|
|
88
88
|
saveAs: '新增', rename: '重新命名', update: '修改', remove: '刪除', presetNamePlaceholder: '預設名稱', confirm: '確定', cancel: '取消', customSaved: '自訂預設已儲存', customUpdated: '自訂預設已更新', customDeleted: '自訂預設已刪除', saveAsTitle: '將當前設定另存為自訂預設', renameTitle: '重新命名目前自訂預設', updateTitle: '更新目前自訂預設', removeTitle: '刪除目前自訂預設', fromPreset: '來自:', modifiedSuffix: ' · 已修改',
|
|
@@ -100,13 +100,13 @@ window.__ModuleLoader__.load({
|
|
|
100
100
|
imagePickTitle: 'Pick a local image (auto-compressed for the notification)', iconPickTitle: 'Pick a local image (auto-compressed for the notification icon)', imageFailed: 'Failed to read the image',
|
|
101
101
|
clearMedia: 'Clear',
|
|
102
102
|
mediaNote: 'Local uploads are processed automatically: large images are center-cropped to the notification ratio 16:9 (512×288) and icons to 1:1 square (128×128) — the cropped result is exactly what shows in the notification',
|
|
103
|
-
presets: { custom: 'Custom preset', unnamed: 'Untitled preset', default: 'Default',
|
|
103
|
+
presets: { custom: 'Custom preset', unnamed: 'Untitled preset', default: 'Default', kaomoji: 'Kaomoji', felyne: 'Felyne (MH cat)', neko: 'Neko girl', deepseekChan: 'DeepSeek-chan' },
|
|
104
104
|
placeholder: 'Leave empty to use the default text',
|
|
105
105
|
insertInfo: '+ Insert info', insertTitle: 'Insert info at cursor', sendTestTitle: 'Send test notification (rendered from current template)',
|
|
106
106
|
skipSubagents: 'Skip subagent sessions',
|
|
107
107
|
pushMode: 'Push channel', pushDual: 'System + in-page', pushSystem: 'System only', pushToast: 'In-page only',
|
|
108
108
|
imageUrl: 'Notification image', iconUrl: 'Notification icon',
|
|
109
|
-
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',
|
|
109
|
+
discard: 'Discard', save: 'Save', saving: 'Saving…', saved: 'Saved ✓ · refresh page to apply', customDeleteConfirm: 'Delete this custom preset? This cannot be undone.', presetDeletedKeepApplied: 'The applied configuration is unaffected; to restore defaults, click Reset', presetNameExists: 'A preset with this name already exists; please use another name', saveDuringEdit: 'Saved, but there are unsaved changes made during saving.', noChange: 'Current config matches the default; nothing to save.', saveFailed: 'Save failed: ', refresh: 'Refresh', reset: 'Reset', resetDone: 'Reset to defaults ✓ · refresh page to apply',
|
|
110
110
|
permTitle: 'Notification permission', permGranted: 'Granted: system notifications available', permDefault: 'Not yet granted', permDenied: 'Blocked by browser', permUnsupported: 'System notifications not supported here',
|
|
111
111
|
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',
|
|
112
112
|
saveAs: 'New', rename: 'Rename', 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', renameTitle: 'Rename current custom preset', updateTitle: 'Update current custom preset', removeTitle: 'Delete current custom preset', fromPreset: 'From: ', modifiedSuffix: ' · modified',
|
|
@@ -124,13 +124,13 @@ window.__ModuleLoader__.load({
|
|
|
124
124
|
imagePickTitle: 'ローカル画像を選択(通知用に自動圧縮)', iconPickTitle: 'ローカル画像を選択(通知アイコン用に自動圧縮)', imageFailed: '画像の読み込みに失敗しました',
|
|
125
125
|
clearMedia: 'クリア',
|
|
126
126
|
mediaNote: 'ローカルアップロードは自動処理されます:大きな画像は通知表示比率 16:9(512×288)に中央クロップ、アイコンは 1:1 正方形(128×128)。クロップ後の見た目がそのまま通知に表示されます',
|
|
127
|
-
presets: { custom: 'カスタムプリセット', unnamed: '未命名プリセット', default: 'デフォルト',
|
|
127
|
+
presets: { custom: 'カスタムプリセット', unnamed: '未命名プリセット', default: 'デフォルト', kaomoji: '顔文字', felyne: 'アイルー', neko: 'ネコ娘', deepseekChan: 'DeepSeekちゃん' },
|
|
128
128
|
placeholder: '空欄の場合はデフォルトの文面を使用',
|
|
129
129
|
insertInfo: '+ 情報を挿入', insertTitle: 'カーソル位置に情報を挿入', sendTestTitle: 'テスト通知を送信(現在のテンプレートで描画)',
|
|
130
130
|
skipSubagents: 'サブエージェントセッションをスキップ',
|
|
131
131
|
pushMode: '通知チャネル', pushDual: 'システム+ページ内', pushSystem: 'システムのみ', pushToast: 'ページ内のみ',
|
|
132
132
|
imageUrl: '通知イメージ', iconUrl: '通知アイコン',
|
|
133
|
-
discard: '破棄', save: '保存', saving: '保存中…', saved: '保存しました ✓ · ページを再読み込み', saveFailed: '保存失敗:', refresh: '再読み込み', reset: 'リセット', resetDone: 'デフォルトに戻しました ✓ · ページを再読み込み',
|
|
133
|
+
discard: '破棄', save: '保存', saving: '保存中…', saved: '保存しました ✓ · ページを再読み込み', customDeleteConfirm: 'このカスタムプリセットを削除しますか?この操作は取り消せません。', presetDeletedKeepApplied: '適用済みの設定には影響しません。デフォルトに戻すには「リセット」をクリック', presetNameExists: '同名のプリセットが既に存在します。別の名前を付けてください', saveDuringEdit: '保存しましたが、保存中に新しい変更があります(未保存)。', noChange: '現在の設定はデフォルトと同じです。保存の必要はありません', saveFailed: '保存失敗:', refresh: '再読み込み', reset: 'リセット', resetDone: 'デフォルトに戻しました ✓ · ページを再読み込み',
|
|
134
134
|
permTitle: '通知権限', permGranted: '許可済み:システム通知が利用可能', permDefault: '未許可', permDenied: 'ブラウザがブロック', permUnsupported: 'この環境ではシステム通知を利用できません',
|
|
135
135
|
permDefaultHint: '「許可をリクエスト」をクリック。ブラウザに確認が出ない場合は、アドレスバーの権限アイコンから手動で許可', permDeniedHint: 'このサイトの通知がブロックされています:アドレスバー左の権限アイコン → サイト設定 → 通知 → 許可 → ページを再読み込み', permUnsupportedHint: 'システム通知には HTTPS または localhost が必要です。「ページ内のみ」を推奨', permRequest: '許可をリクエスト',
|
|
136
136
|
saveAs: '新規', rename: '名前を変更', update: '編集', remove: '削除', presetNamePlaceholder: 'プリセット名', confirm: '決定', cancel: 'キャンセル', customSaved: 'カスタムプリセットを保存しました', customUpdated: 'カスタムプリセットを更新しました', customDeleted: 'カスタムプリセットを削除しました', saveAsTitle: '現在の設定をカスタムプリセットとして保存', renameTitle: '現在のカスタムプリセットの名前を変更', updateTitle: '現在のカスタムプリセットを更新', removeTitle: '現在のカスタムプリセットを削除', fromPreset: '由来:', modifiedSuffix: ' · 変更あり',
|
|
@@ -148,13 +148,13 @@ window.__ModuleLoader__.load({
|
|
|
148
148
|
imagePickTitle: '로컬 이미지 선택(알림용으로 자동 압축)', iconPickTitle: '로컬 이미지 선택(알림 아이콘용으로 자동 압축)', imageFailed: '이미지를 읽지 못했습니다',
|
|
149
149
|
clearMedia: '지우기',
|
|
150
150
|
mediaNote: '로컬 업로드는 자동 처리됩니다: 큰 이미지는 알림 표시 비율 16:9(512×288) 중앙 크롭, 아이콘은 1:1 정사각(128×128). 크롭 후 모습이 그대로 알림에 표시됩니다',
|
|
151
|
-
presets: { custom: '사용자 지정 프리셋', unnamed: '이름 없는 프리셋', default: '기본',
|
|
151
|
+
presets: { custom: '사용자 지정 프리셋', unnamed: '이름 없는 프리셋', default: '기본', kaomoji: '카오모지', felyne: '아이루', neko: '네코무스메', deepseekChan: 'DeepSeek짱' },
|
|
152
152
|
placeholder: '비우면 기본 문구 사용',
|
|
153
153
|
insertInfo: '+ 정보 삽입', insertTitle: '커서 위치에 정보 삽입', sendTestTitle: '테스트 알림 보내기 (현재 템플릿으로 렌더링)',
|
|
154
154
|
skipSubagents: '하위 에이전트 세션 건너뛰기',
|
|
155
155
|
pushMode: '알림 채널', pushDual: '시스템+페이지 내', pushSystem: '시스템만', pushToast: '페이지 내만',
|
|
156
156
|
imageUrl: '알림 이미지', iconUrl: '알림 아이콘',
|
|
157
|
-
discard: '포기', save: '저장', saving: '저장 중…', saved: '저장됨 ✓ · 새로고침 후 적용', saveFailed: '저장 실패: ', refresh: '새로고침', reset: '초기화', resetDone: '기본값으로 초기화됨 ✓ · 새로고침 후 적용',
|
|
157
|
+
discard: '포기', save: '저장', saving: '저장 중…', saved: '저장됨 ✓ · 새로고침 후 적용', customDeleteConfirm: '이 사용자 지정 프리셋을 삭제할까요? 이 작업은 되돌릴 수 없습니다.', presetDeletedKeepApplied: '적용된 구성에는 영향이 없습니다. 기본값으로 되돌리려면 「초기화」를 클릭하세요', presetNameExists: '같은 이름의 프리셋이 이미 있습니다. 다른 이름을 사용하세요', saveDuringEdit: '저장되었지만, 저장 중에 새로 수정된 내용이 있어 아직 저장되지 않았습니다.', noChange: '현재 설정이 기본값과 동일합니다. 저장할 필요가 없습니다', saveFailed: '저장 실패: ', refresh: '새로고침', reset: '초기화', resetDone: '기본값으로 초기화됨 ✓ · 새로고침 후 적용',
|
|
158
158
|
permTitle: '알림 권한', permGranted: '허용됨: 시스템 알림 사용 가능', permDefault: '아직 허용되지 않음', permDenied: '브라우저에서 차단됨', permUnsupported: '이 환경에서는 시스템 알림을 사용할 수 없음',
|
|
159
159
|
permDefaultHint: '「권한 요청」클릭. 브라우저에 확인이 없으면 주소 표시줄 권한 아이콘에서 수동 허용', permDeniedHint: '이 사이트의 알림이 차단되었습니다: 주소 표시줄 왼쪽 권한 아이콘 → 사이트 설정 → 알림 → 허용 후 새로고침', permUnsupportedHint: '시스템 알림은 HTTPS 또는 localhost가 필요합니다. 「페이지 내만」사용 권장', permRequest: '권한 요청',
|
|
160
160
|
saveAs: '추가', rename: '이름 변경', update: '수정', remove: '삭제', presetNamePlaceholder: '프리셋 이름', confirm: '확인', cancel: '취소', customSaved: '사용자 지정 프리셋 저장됨', customUpdated: '사용자 지정 프리셋 업데이트됨', customDeleted: '사용자 지정 프리셋 삭제됨', saveAsTitle: '현재 구성을 사용자 지정 프리셋으로 저장', renameTitle: '현재 사용자 지정 프리셋 이름 변경', updateTitle: '현재 사용자 지정 프리셋 업데이트', removeTitle: '현재 사용자 지정 프리셋 삭제', fromPreset: '출처: ', modifiedSuffix: ' · 수정됨',
|
|
@@ -274,7 +274,12 @@ window.__ModuleLoader__.load({
|
|
|
274
274
|
if (st && st.value && typeof st.value.titleTemplate === 'string' && st.value.titleTemplate.trim() !== '') tpl = st.value.titleTemplate
|
|
275
275
|
}
|
|
276
276
|
} catch (e) { /* 默认 */ }
|
|
277
|
-
|
|
277
|
+
// P1-5:与宿主 renderTitle 对齐——替换全部信息位,缺数据时替换为空串(剥除),避免 {duration} 等字面量泄漏到通知标题
|
|
278
|
+
var out = String(tpl)
|
|
279
|
+
.replace(/\{image\}/g, '').replace(/\{icon\}/g, '').replace(/\{label\}/g, '')
|
|
280
|
+
.replaceAll('{title}', name)
|
|
281
|
+
.replace(/\{duration\}/g, '').replace(/\{usage\}/g, '').replace(/\{error\}/g, '').replace(/\{cache\}/g, '').replace(/\{tps\}/g, '')
|
|
282
|
+
.trim()
|
|
278
283
|
if (out === '') out = '任务已完成'
|
|
279
284
|
return out + (suffix || '')
|
|
280
285
|
}
|
|
@@ -571,9 +576,231 @@ window.__ModuleLoader__.load({
|
|
|
571
576
|
* 注意:{label} 已从插入菜单移除(旧模板中的 {label} 仍兼容渲染/预览)。 */
|
|
572
577
|
var TOKEN_KEYS = ['title', 'duration', 'usage', 'error', 'cache', 'tps']
|
|
573
578
|
|
|
574
|
-
/**
|
|
579
|
+
/** 系统预设:「默认」为基线 + 4 套风格预设(简体中文文案,颜文字/口癖跨语言不自然,故固定 zh;切换语言仅界面翻译,模板保留)。 */
|
|
575
580
|
var PRESETS = [
|
|
576
581
|
{ id: 'default', labelKey: 'default', language: 'zh', titleTemplate: '', titleTemplates: { completed: '', error: '', aborted: '', blocked: '', 'max-tokens': '' }, templates: { completed: '', error: '', aborted: '', blocked: '', 'max-tokens': '' } },
|
|
582
|
+
// 风格内置预设:i18n[语言] 存放各语言文案(zh 为用户编辑版);language 为 null → 载入不强制切换界面语言,
|
|
583
|
+
// 载入/切换语言时自动取当前语言的文案(缺语言回退 zh)
|
|
584
|
+
{
|
|
585
|
+
id: 'kaomoji', labelKey: 'kaomoji', language: null,
|
|
586
|
+
i18n: {
|
|
587
|
+
zh: {
|
|
588
|
+
titleTemplates: { completed: '任务完成 (^▽^)', error: '任务出错 (;д;)', aborted: '任务中止 (;ω;)', blocked: '任务被阻塞 (・_・;)', 'max-tokens': '任务到上限 ( ̄▽ ̄)' },
|
|
589
|
+
templates: {
|
|
590
|
+
completed: '会话「{title}」已完成 (๑•̀ㅂ•́)و✧,请点击查看~',
|
|
591
|
+
error: '会话「{title}」出错啦 (。•́︿•̀。),请点击查看。',
|
|
592
|
+
aborted: '会话「{title}」被中止了 (;ω;),请点击查看。',
|
|
593
|
+
blocked: '会话「{title}」被阻塞了 (・_・;),请点击查看。',
|
|
594
|
+
'max-tokens': '会话「{title}」达到上限了 ( ̄▽ ̄)ゞ,建议拆分任务,请点击查看。',
|
|
595
|
+
},
|
|
596
|
+
},
|
|
597
|
+
'zh-tw': {
|
|
598
|
+
titleTemplates: { completed: '任務完成 (^▽^)', error: '任務出錯 (;д;)', aborted: '任務中止 (;ω;)', blocked: '任務被阻塞 (・_・;)', 'max-tokens': '任務到上限 ( ̄▽ ̄)' },
|
|
599
|
+
templates: {
|
|
600
|
+
completed: '會話「{title}」已完成 (๑•̀ㅂ•́)و✧,請點擊查看~',
|
|
601
|
+
error: '會話「{title}」出錯啦 (。•́︿•̀。),請點擊查看。',
|
|
602
|
+
aborted: '會話「{title}」被中止了 (;ω;),請點擊查看。',
|
|
603
|
+
blocked: '會話「{title}」被阻塞了 (・_・;),請點擊查看。',
|
|
604
|
+
'max-tokens': '會話「{title}」達到上限了 ( ̄▽ ̄)ゞ,建議拆分任務,請點擊查看。',
|
|
605
|
+
},
|
|
606
|
+
},
|
|
607
|
+
en: {
|
|
608
|
+
titleTemplates: { completed: 'Task done (^▽^)', error: 'Task failed (;д;)', aborted: 'Task aborted (;ω;)', blocked: 'Task blocked (・_・;)', 'max-tokens': 'Task hit the cap ( ̄▽ ̄)' },
|
|
609
|
+
templates: {
|
|
610
|
+
completed: 'Session "{title}" completed (๑•̀ㅂ•́)و✧ — click to view~',
|
|
611
|
+
error: 'Session "{title}" failed (。•́︿•̀。) — click to view.',
|
|
612
|
+
aborted: 'Session "{title}" was aborted (;ω;) — click to view.',
|
|
613
|
+
blocked: 'Session "{title}" was blocked (・_・;) — click to view.',
|
|
614
|
+
'max-tokens': 'Session "{title}" hit the cap ( ̄▽ ̄)ゞ — consider splitting the task. Click to view.',
|
|
615
|
+
},
|
|
616
|
+
},
|
|
617
|
+
ja: {
|
|
618
|
+
titleTemplates: { completed: 'タスク完了 (^▽^)', error: 'タスクエラー (;д;)', aborted: 'タスク中止 (;ω;)', blocked: 'タスクブロック (・_・;)', 'max-tokens': '上限到達 ( ̄▽ ̄)' },
|
|
619
|
+
templates: {
|
|
620
|
+
completed: 'セッション「{title}」完了 (๑•̀ㅂ•́)و✧ — クリックして表示~',
|
|
621
|
+
error: 'セッション「{title}」エラー (。•́︿•̀。) — クリックして表示。',
|
|
622
|
+
aborted: 'セッション「{title}」が中止されました (;ω;) — クリックして表示。',
|
|
623
|
+
blocked: 'セッション「{title}」がブロックされました (・_・;) — クリックして表示。',
|
|
624
|
+
'max-tokens': 'セッション「{title}」が上限に到達 ( ̄▽ ̄)ゞ — タスク分割をおすすめ。クリックして表示。',
|
|
625
|
+
},
|
|
626
|
+
},
|
|
627
|
+
ko: {
|
|
628
|
+
titleTemplates: { completed: '작업 완료 (^▽^)', error: '작업 오류 (;д;)', aborted: '작업 중단 (;ω;)', blocked: '작업 차단 (・_・;)', 'max-tokens': '한도 도달 ( ̄▽ ̄)' },
|
|
629
|
+
templates: {
|
|
630
|
+
completed: '세션 "{title}" 완료 (๑•̀ㅂ•́)و✧ — 클릭하여 확인~',
|
|
631
|
+
error: '세션 "{title}" 오류 (。•́︿•̀。) — 클릭하여 확인.',
|
|
632
|
+
aborted: '세션 "{title}" 중단됨 (;ω;) — 클릭하여 확인.',
|
|
633
|
+
blocked: '세션 "{title}" 차단됨 (・_・;) — 클릭하여 확인.',
|
|
634
|
+
'max-tokens': '세션 "{title}" 한도 도달 ( ̄▽ ̄)ゞ — 작업 분할 권장. 클릭하여 확인.',
|
|
635
|
+
},
|
|
636
|
+
},
|
|
637
|
+
},
|
|
638
|
+
},
|
|
639
|
+
{
|
|
640
|
+
id: 'felyne', labelKey: 'felyne', language: null,
|
|
641
|
+
i18n: {
|
|
642
|
+
zh: {
|
|
643
|
+
titleTemplates: { completed: '喵~任务完成', error: '喵…任务出错', aborted: '喵?任务中止', blocked: '喵…任务被挡住', 'max-tokens': '喵喵 任务到上限' },
|
|
644
|
+
templates: {
|
|
645
|
+
completed: '喵~任务「{title}」完成啦!老大快去看看吧喵~',
|
|
646
|
+
error: '喵……任务「{title}」出错了喵,老大快来看看喵。',
|
|
647
|
+
aborted: '喵?!任务「{title}」中止了喵。',
|
|
648
|
+
blocked: '喵……任务「{title}」遇到困难了喵。',
|
|
649
|
+
'max-tokens': '喵喵,会话「{title}」到上限啦喵!拆开任务再试喵~',
|
|
650
|
+
},
|
|
651
|
+
},
|
|
652
|
+
'zh-tw': {
|
|
653
|
+
titleTemplates: { completed: '喵~任務完成', error: '喵…任務出錯', aborted: '喵?任務中止', blocked: '喵…任務被擋住', 'max-tokens': '喵喵 任務到上限' },
|
|
654
|
+
templates: {
|
|
655
|
+
completed: '喵~任務「{title}」完成啦!老大快去看看吧喵~',
|
|
656
|
+
error: '喵……任務「{title}」出錯了喵,老大快來看看喵。',
|
|
657
|
+
aborted: '喵?!任務「{title}」中止了喵。',
|
|
658
|
+
blocked: '喵……任務「{title}」遇到困難了喵。',
|
|
659
|
+
'max-tokens': '喵喵,會話「{title}」到上限啦喵!拆開任務再試喵~',
|
|
660
|
+
},
|
|
661
|
+
},
|
|
662
|
+
en: {
|
|
663
|
+
titleTemplates: { completed: 'Meow~ Task done', error: 'Meow… Task failed', aborted: 'Meow? Task aborted', blocked: 'Meow… Task stuck', 'max-tokens': 'Meow meow, cap reached' },
|
|
664
|
+
templates: {
|
|
665
|
+
completed: 'Meow~ Task "{title}" done! Boss, go check it out meow~',
|
|
666
|
+
error: 'Meow…… Task "{title}" failed meow. Boss, come take a look meow.',
|
|
667
|
+
aborted: 'Meow?! Task "{title}" was aborted meow.',
|
|
668
|
+
blocked: 'Meow…… Task "{title}" hit a snag meow.',
|
|
669
|
+
'max-tokens': 'Meow meow, session "{title}" hit the cap meow! Split the task and try again meow~',
|
|
670
|
+
},
|
|
671
|
+
},
|
|
672
|
+
ja: {
|
|
673
|
+
titleTemplates: { completed: 'にゃ~タスク完了', error: 'にゃ…タスクエラー', aborted: 'にゃ?タスク中止', blocked: 'にゃ…タスク詰まり', 'max-tokens': 'にゃにゃ 上限到達' },
|
|
674
|
+
templates: {
|
|
675
|
+
completed: 'にゃ~タスク「{title}」完了だよ!ボス、見に行ってにゃ~',
|
|
676
|
+
error: 'にゃ……タスク「{title}」エラーにゃ。ボス、見に来てにゃ。',
|
|
677
|
+
aborted: 'にゃ?!タスク「{title}」中止にゃ。',
|
|
678
|
+
blocked: 'にゃ……タスク「{title}」詰まっちゃったにゃ。',
|
|
679
|
+
'max-tokens': 'にゃにゃ、セッション「{title}」上限に到達にゃ!タスクを分けて再挑戦にゃ~',
|
|
680
|
+
},
|
|
681
|
+
},
|
|
682
|
+
ko: {
|
|
683
|
+
titleTemplates: { completed: '냐~ 작업 완료', error: '냐… 작업 오류', aborted: '냐? 작업 중단', blocked: '냐… 작업 막힘', 'max-tokens': '냐냐 한도 도달' },
|
|
684
|
+
templates: {
|
|
685
|
+
completed: '냐~ 작업 "{title}" 완료다옹! 사장님 얼른 보러 가주세요 냐~',
|
|
686
|
+
error: '냐…… 작업 "{title}" 오류냐옹. 사장님 이리 와서 보세요 냐.',
|
|
687
|
+
aborted: '냐?! 작업 "{title}" 중단됐다냐옹.',
|
|
688
|
+
blocked: '냐…… 작업 "{title}" 막혔다냐옹.',
|
|
689
|
+
'max-tokens': '냐냐, 세션 "{title}" 한도 도달했다냐옹! 작업을 나눠 다시 시도하세요 냐~',
|
|
690
|
+
},
|
|
691
|
+
},
|
|
692
|
+
},
|
|
693
|
+
},
|
|
694
|
+
{
|
|
695
|
+
id: 'neko', labelKey: 'neko', language: null,
|
|
696
|
+
i18n: {
|
|
697
|
+
zh: {
|
|
698
|
+
titleTemplates: { completed: '主人,任务完成喵~', error: '主人…任务出错喵', aborted: '喵!任务中止', blocked: '主人,任务被挡住喵', 'max-tokens': '主人,任务到上限喵' },
|
|
699
|
+
templates: {
|
|
700
|
+
completed: '主人~会话「{title}」已经完成啦喵~,快来看看喵!',
|
|
701
|
+
error: '主人……会话「{title}」出错了喵呜,人家好担心呢……',
|
|
702
|
+
aborted: '喵!会话「{title}」被中止了喵。',
|
|
703
|
+
blocked: '主人,会话「{title}」被挡住了喵呜。',
|
|
704
|
+
'max-tokens': '主人~会话「{title}」到上限了喵……,要不要拆开任务喵?',
|
|
705
|
+
},
|
|
706
|
+
},
|
|
707
|
+
'zh-tw': {
|
|
708
|
+
titleTemplates: { completed: '主人,任務完成喵~', error: '主人…任務出錯喵', aborted: '喵!任務中止', blocked: '主人,任務被擋住喵', 'max-tokens': '主人,任務到上限喵' },
|
|
709
|
+
templates: {
|
|
710
|
+
completed: '主人~會話「{title}」已經完成啦喵~,快來看看喵!',
|
|
711
|
+
error: '主人……會話「{title}」出錯了喵嗚,人家好擔心呢……',
|
|
712
|
+
aborted: '喵!會話「{title}」被中止了喵。',
|
|
713
|
+
blocked: '主人,會話「{title}」被擋住了喵嗚。',
|
|
714
|
+
'max-tokens': '主人~會話「{title}」到上限了喵……,要不要拆開任務喵?',
|
|
715
|
+
},
|
|
716
|
+
},
|
|
717
|
+
en: {
|
|
718
|
+
titleTemplates: { completed: 'Master, task done nya~', error: 'Master… task failed nya', aborted: 'Nya! Task aborted', blocked: 'Master, task stuck nya', 'max-tokens': 'Master, cap reached nya' },
|
|
719
|
+
templates: {
|
|
720
|
+
completed: 'Master~ session "{title}" is done nya~, come check it out nya!',
|
|
721
|
+
error: 'Master…… session "{title}" failed nya…, I\'m so worried……',
|
|
722
|
+
aborted: 'Nya! Session "{title}" was aborted nya.',
|
|
723
|
+
blocked: 'Master, session "{title}" got stuck nya…….',
|
|
724
|
+
'max-tokens': 'Master~ session "{title}" hit the cap nya……, want to split the task nya?',
|
|
725
|
+
},
|
|
726
|
+
},
|
|
727
|
+
ja: {
|
|
728
|
+
titleTemplates: { completed: 'ご主人様、タスク完了にゃ~', error: 'ご主人様…タスクエラーにゃ', aborted: 'にゃ!タスク中止', blocked: 'ご主人様、タスク詰まりにゃ', 'max-tokens': 'ご主人様、上限にゃ' },
|
|
729
|
+
templates: {
|
|
730
|
+
completed: 'ご主人様~セッション「{title}」完了にゃ~、見てみてにゃ!',
|
|
731
|
+
error: 'ご主人様……セッション「{title}」エラーにゃう、心配です……',
|
|
732
|
+
aborted: 'にゃ!セッション「{title}」中止にゃ。',
|
|
733
|
+
blocked: 'ご主人様、セッション「{title}」詰まりにゃう……。',
|
|
734
|
+
'max-tokens': 'ご主人様~セッション「{title}」上限にゃ……、タスク分けるにゃ?',
|
|
735
|
+
},
|
|
736
|
+
},
|
|
737
|
+
ko: {
|
|
738
|
+
titleTemplates: { completed: '주인님, 작업 완료냐~', error: '주인님… 작업 오류냐', aborted: '냐! 작업 중단', blocked: '주인님, 작업 막힘냐', 'max-tokens': '주인님, 한도 도달냐' },
|
|
739
|
+
templates: {
|
|
740
|
+
completed: '주인님~ 세션 "{title}" 완료됐냐~, 얼른 보러 오세요 냐!',
|
|
741
|
+
error: '주인님…… 세션 "{title}" 오류났냐옹, 걱정돼요……',
|
|
742
|
+
aborted: '냐! 세션 "{title}" 중단됐냐옹.',
|
|
743
|
+
blocked: '주인님, 세션 "{title}" 막혔냐옹…….',
|
|
744
|
+
'max-tokens': '주인님~ 세션 "{title}" 한도 도달냐옹……, 작업 나눠볼까 냐?',
|
|
745
|
+
},
|
|
746
|
+
},
|
|
747
|
+
},
|
|
748
|
+
},
|
|
749
|
+
{
|
|
750
|
+
id: 'deepseek-chan', labelKey: 'deepseekChan', language: null,
|
|
751
|
+
i18n: {
|
|
752
|
+
zh: {
|
|
753
|
+
titleTemplates: { completed: '任务完成~', error: '任务出错…', aborted: '任务中止', blocked: '任务阻塞', 'max-tokens': '任务到上限' },
|
|
754
|
+
templates: {
|
|
755
|
+
completed: '深酱报告!会话「{title}」已完成啦~,请点击查看~',
|
|
756
|
+
error: '深酱道歉……会话「{title}」出错了呢,非常抱歉!',
|
|
757
|
+
aborted: '深酱遗憾……会话「{title}」被中止了。',
|
|
758
|
+
blocked: '深酱呜咽……会话「{title}」被阻塞了。',
|
|
759
|
+
'max-tokens': '深酱提示!会话「{title}」达到输出上限了~,建议拆分任务哦!',
|
|
760
|
+
},
|
|
761
|
+
},
|
|
762
|
+
'zh-tw': {
|
|
763
|
+
titleTemplates: { completed: '任務完成~', error: '任務出錯…', aborted: '任務中止', blocked: '任務阻塞', 'max-tokens': '任務到上限' },
|
|
764
|
+
templates: {
|
|
765
|
+
completed: '深醬報告!會話「{title}」已完成啦~,請點擊查看~',
|
|
766
|
+
error: '深醬道歉……會話「{title}」出錯了呢,非常抱歉!',
|
|
767
|
+
aborted: '深醬遺憾……會話「{title}」被中止了。',
|
|
768
|
+
blocked: '深醬嗚咽……會話「{title}」被阻塞了。',
|
|
769
|
+
'max-tokens': '深醬提示!會話「{title}」達到輸出上限了~,建議拆分任務哦!',
|
|
770
|
+
},
|
|
771
|
+
},
|
|
772
|
+
en: {
|
|
773
|
+
titleTemplates: { completed: 'Task done~', error: 'Task failed…', aborted: 'Task aborted', blocked: 'Task blocked', 'max-tokens': 'Cap reached' },
|
|
774
|
+
templates: {
|
|
775
|
+
completed: 'Shen-chan reporting! Session "{title}" is done~, click to view~',
|
|
776
|
+
error: 'Shen-chan apologizes…… session "{title}" failed, so sorry!',
|
|
777
|
+
aborted: 'Shen-chan regrets…… session "{title}" was aborted.',
|
|
778
|
+
blocked: 'Shen-chan whimpers…… session "{title}" was blocked.',
|
|
779
|
+
'max-tokens': 'Shen-chan tip! Session "{title}" hit the output cap~, consider splitting the task!',
|
|
780
|
+
},
|
|
781
|
+
},
|
|
782
|
+
ja: {
|
|
783
|
+
titleTemplates: { completed: 'タスク完了~', error: 'タスクエラー…', aborted: 'タスク中止', blocked: 'タスクブロック', 'max-tokens': '上限到達' },
|
|
784
|
+
templates: {
|
|
785
|
+
completed: '深ちゃん報告!セッション「{title}」完了~、クリックして表示~',
|
|
786
|
+
error: '深ちゃん謝罪……セッション「{title}」エラー、ごめんなさい!',
|
|
787
|
+
aborted: '深ちゃん残念……セッション「{title}」中止。',
|
|
788
|
+
blocked: '深ちゃんしくしく……セッション「{title}」ブロック。',
|
|
789
|
+
'max-tokens': '深ちゃんお知らせ!セッション「{title}」上限到達~、タスク分割おすすめ!',
|
|
790
|
+
},
|
|
791
|
+
},
|
|
792
|
+
ko: {
|
|
793
|
+
titleTemplates: { completed: '작업 완료~', error: '작업 오류…', aborted: '작업 중단', blocked: '작업 차단', 'max-tokens': '한도 도달' },
|
|
794
|
+
templates: {
|
|
795
|
+
completed: '심짱 보고! 세션 "{title}" 완료~, 클릭하여 확인~',
|
|
796
|
+
error: '심짱 사과…… 세션 "{title}" 오류났어요, 정말 죄송해요!',
|
|
797
|
+
aborted: '심짱 아쉬움…… 세션 "{title}" 중단됐어요.',
|
|
798
|
+
blocked: '심짱 훌쩍…… 세션 "{title}" 차단됐어요.',
|
|
799
|
+
'max-tokens': '심짱 알림! 세션 "{title}" 출력 한도 도달~, 작업 분할 추천!',
|
|
800
|
+
},
|
|
801
|
+
},
|
|
802
|
+
},
|
|
803
|
+
},
|
|
577
804
|
]
|
|
578
805
|
|
|
579
806
|
/** 重置目标:与宿主 DEFAULT_SETTINGS 一致(设置面板可编辑字段子集;语言保留当前值)。 */
|
|
@@ -643,11 +870,26 @@ window.__ModuleLoader__.load({
|
|
|
643
870
|
/** 表单(语言+模板)与某预设内容是否一致(用于「已修改」指示)。 */
|
|
644
871
|
function matchesPreset(cur, p) {
|
|
645
872
|
if (!p) return false
|
|
646
|
-
|
|
873
|
+
var lang = LANG_IDS.indexOf(cur.language) >= 0 ? cur.language : 'zh'
|
|
874
|
+
// 内置预设 language 为 null → 任意语言均可匹配(多语言风格预设);default 等固定语言预设按原规则
|
|
875
|
+
if (p.language !== null && p.language !== undefined && p.language !== lang) return false
|
|
647
876
|
if (String(cur.titleTemplate ?? '') !== String(p.titleTemplate ?? '')) return false
|
|
877
|
+
// 通知媒体参与匹配:上传了全局大图/图标的配置 ≠ 无图的默认/旧预设(否则保存后重开面板会误匹配默认)
|
|
878
|
+
if (String(cur.imageUrl ?? '') !== String(p.imageUrl ?? '')) return false
|
|
879
|
+
if (String(cur.iconUrl ?? '') !== String(p.iconUrl ?? '')) return false
|
|
880
|
+
var loc = p.i18n ? (p.i18n[lang] || p.i18n.zh || p) : p
|
|
881
|
+
var pt = (loc.templates && typeof loc.templates === 'object') ? loc.templates : {}
|
|
882
|
+
var ptt = (loc.titleTemplates && typeof loc.titleTemplates === 'object') ? loc.titleTemplates : {}
|
|
883
|
+
var pimg = (p.images && typeof p.images === 'object') ? p.images : {}
|
|
884
|
+
var picon = (p.icons && typeof p.icons === 'object') ? p.icons : {}
|
|
648
885
|
for (var j = 0; j < REASON_FIELDS.length; j++) {
|
|
649
|
-
|
|
650
|
-
if (
|
|
886
|
+
var f = REASON_FIELDS[j]
|
|
887
|
+
if (templateOf(cur, f) !== String(pt[f] ?? '').trim()) return false
|
|
888
|
+
if (titleTplOf(cur, f) !== String(ptt[f] ?? '').trim()) return false
|
|
889
|
+
// 按原因图片/图标数据也参与匹配(P1-4:用 imageOf/iconOf 兼容扁平草稿键与宿主嵌套结构,
|
|
890
|
+
// 否则刷新后(宿主 value 为嵌套 images/icons)扁平键恒空 → 匹配失败 → 下拉显示「自定义」)
|
|
891
|
+
if (imageOf(cur, f) !== String(pimg[f] ?? '')) return false
|
|
892
|
+
if (iconOf(cur, f) !== String(picon[f] ?? '')) return false
|
|
651
893
|
}
|
|
652
894
|
return true
|
|
653
895
|
}
|
|
@@ -881,6 +1123,11 @@ window.__ModuleLoader__.load({
|
|
|
881
1123
|
var editor = editorRef.current
|
|
882
1124
|
var next = el.nextSibling
|
|
883
1125
|
var prev = el.previousSibling
|
|
1126
|
+
// contentEditable 中标签/文本之间常有空文本节点或 <br>(换行占位)。
|
|
1127
|
+
// 跳过它们定位光标,让光标落在真实内容旁(可视位置正确)。
|
|
1128
|
+
// 注意:连环删标签的真正根因在 onKeyDown 找胶囊时跳过了非空文本节点,见那里。
|
|
1129
|
+
while (next && ((next.nodeType === 3 && next.textContent === '') || (next.nodeType === 1 && next.tagName === 'BR'))) next = next.nextSibling
|
|
1130
|
+
while (prev && ((prev.nodeType === 3 && prev.textContent === '') || (prev.nodeType === 1 && prev.tagName === 'BR'))) prev = prev.previousSibling
|
|
884
1131
|
el.remove()
|
|
885
1132
|
if (editor) {
|
|
886
1133
|
var range = document.createRange()
|
|
@@ -1177,7 +1424,12 @@ window.__ModuleLoader__.load({
|
|
|
1177
1424
|
// 光标停在元素容器内(如胶囊前):按子节点序号取当前一个
|
|
1178
1425
|
else if (cc.nodeType === 1 && range.startOffset < cc.childNodes.length) target = cc.childNodes[range.startOffset]
|
|
1179
1426
|
}
|
|
1180
|
-
|
|
1427
|
+
// 只跳过「空文本节点」找胶囊;一遇到非空文本必须停——
|
|
1428
|
+
// 此时光标紧邻的是真实文字,应交给浏览器删字符。
|
|
1429
|
+
// 若跳过文字继续找胶囊,连按 Back 会把标签全删光、删完标签才轮到文字
|
|
1430
|
+
//(例:【A】123【B】456【C】末尾按 3 下:修复前删掉 C、B、A 得 "123456";
|
|
1431
|
+
// 修复后删掉 C、"6"、"5" 得 【A】123【B】4)。
|
|
1432
|
+
while (target && target.nodeType === 3 && target.textContent === '') target = e.key === 'Backspace' ? target.previousSibling : target.nextSibling
|
|
1181
1433
|
if (target && target.hasAttribute && target.hasAttribute('data-tok')) {
|
|
1182
1434
|
e.preventDefault()
|
|
1183
1435
|
removeChipAt(target)
|
|
@@ -1239,6 +1491,9 @@ window.__ModuleLoader__.load({
|
|
|
1239
1491
|
var draftState = useState(null)
|
|
1240
1492
|
var draft = draftState[0]
|
|
1241
1493
|
var setDraft = draftState[1]
|
|
1494
|
+
// 最新 draft 引用:保存回调等异步路径读取「此刻」的草稿(闭包里的 draft 是触发时快照,P0-3)
|
|
1495
|
+
var draftRef = useRef(null)
|
|
1496
|
+
draftRef.current = draft
|
|
1242
1497
|
var noteState = useState('')
|
|
1243
1498
|
var note = noteState[0]
|
|
1244
1499
|
var setNote = noteState[1]
|
|
@@ -1277,7 +1532,7 @@ window.__ModuleLoader__.load({
|
|
|
1277
1532
|
var ttMenuFor = ttMenuState[0]
|
|
1278
1533
|
var setTtMenuFor = ttMenuState[1]
|
|
1279
1534
|
var ttHandles = useRef({}) // 按原因定制标题各行的 ChipEditor 句柄(插入信息到光标处)
|
|
1280
|
-
var tplOpenState = useState(
|
|
1535
|
+
var tplOpenState = useState(true) // 「内容」折叠区展开状态(默认展开:打开面板即可编辑各原因正文)
|
|
1281
1536
|
var tplOpen = tplOpenState[0]
|
|
1282
1537
|
var setTplOpen = tplOpenState[1]
|
|
1283
1538
|
var previewState = useState(null) // 大图预览(lightbox):非空 = 正在预览该 data URI/URL
|
|
@@ -1334,11 +1589,15 @@ window.__ModuleLoader__.load({
|
|
|
1334
1589
|
function loadPreset(entry, custom) {
|
|
1335
1590
|
var flat = flatOf(cur)
|
|
1336
1591
|
var next = Object.assign({}, cur)
|
|
1337
|
-
|
|
1592
|
+
// 多语言风格预设:按当前语言取文案;language 为 null/空 → 保留当前界面语言(不强制切换)
|
|
1593
|
+
var lang = LANG_IDS.indexOf(cur.language) >= 0 ? cur.language : 'zh'
|
|
1594
|
+
var loc = entry.i18n ? (entry.i18n[lang] || entry.i18n.zh || entry) : entry
|
|
1595
|
+
next.language = entry.language || cur.language
|
|
1338
1596
|
next.titleTemplate = String(entry.titleTemplate ?? '')
|
|
1339
|
-
|
|
1597
|
+
var lt = (loc.templates && typeof loc.templates === 'object') ? loc.templates : {}
|
|
1598
|
+
for (var j = 0; j < REASON_FIELDS.length; j++) next['tpl-' + REASON_FIELDS[j]] = typeof lt[REASON_FIELDS[j]] === 'string' ? lt[REASON_FIELDS[j]] : ''
|
|
1340
1599
|
// 按原因定制标题(旧预设可能没有 → 空)
|
|
1341
|
-
var et = (
|
|
1600
|
+
var et = (loc.titleTemplates && typeof loc.titleTemplates === 'object') ? loc.titleTemplates : {}
|
|
1342
1601
|
for (var t2 = 0; t2 < REASON_FIELDS.length; t2++) next['tt-' + REASON_FIELDS[t2]] = typeof et[REASON_FIELDS[t2]] === 'string' ? et[REASON_FIELDS[t2]] : ''
|
|
1343
1602
|
// 通知媒体(大图/图标 + 完整预览版;旧预设缺失 → 清空)
|
|
1344
1603
|
next.imageUrl = String(entry.imageUrl ?? '')
|
|
@@ -1375,6 +1634,10 @@ window.__ModuleLoader__.load({
|
|
|
1375
1634
|
function saveAsCustom() {
|
|
1376
1635
|
var name = newName.trim()
|
|
1377
1636
|
if (!name) { setNote(t.presetNameRequired); return }
|
|
1637
|
+
// P2-2:同名预设提示(下拉同名条目无法区分)
|
|
1638
|
+
for (var ni = 0; ni < customPresets.length; ni++) {
|
|
1639
|
+
if (String(customPresets[ni].name ?? '').trim() === name) { setNote(t.presetNameExists); return }
|
|
1640
|
+
}
|
|
1378
1641
|
var flat = flatOf(cur)
|
|
1379
1642
|
var images = {}
|
|
1380
1643
|
var icons = {}
|
|
@@ -1388,7 +1651,8 @@ window.__ModuleLoader__.load({
|
|
|
1388
1651
|
iconPreviews[sk] = flat['pv-icon-' + sk]
|
|
1389
1652
|
}
|
|
1390
1653
|
var cp = {
|
|
1391
|
-
|
|
1654
|
+
// P2-1:加随机后缀,避免同毫秒两次另存为 id 冲突(与 renameCustom 一致)
|
|
1655
|
+
id: 'cu-' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
|
|
1392
1656
|
name: name,
|
|
1393
1657
|
language: LANG_IDS.indexOf(cur.language) >= 0 ? cur.language : 'zh',
|
|
1394
1658
|
titleTemplate: flat.titleTemplate,
|
|
@@ -1416,15 +1680,21 @@ window.__ModuleLoader__.load({
|
|
|
1416
1680
|
|
|
1417
1681
|
function deleteCustom() {
|
|
1418
1682
|
if (!shownCustom || !shownEntry) return
|
|
1683
|
+
// P0-1:误点即永久丢失(预设含 5 原因模板 + 图片 data URI)→ 删除前确认
|
|
1684
|
+
if (!window.confirm(t.customDeleteConfirm)) return
|
|
1419
1685
|
var next = customPresets.filter(function (cp) { return cp.id !== shownEntry.id })
|
|
1420
1686
|
setCustomPresets(next)
|
|
1421
1687
|
saveCustomPresets(next)
|
|
1422
1688
|
setActivePreset(null)
|
|
1423
|
-
//
|
|
1689
|
+
// 删除的是当前使用的预设:表单恢复为**宿主已保存值**(而非默认值)——
|
|
1690
|
+
// 避免「表单变默认 → dirty → 用户误点保存 → 宿主配置被覆盖为默认」的不可恢复覆盖(P0-2);
|
|
1691
|
+
// 宿主值含当前语言,天然满足「删除保留语言」(P1-1)
|
|
1424
1692
|
if (shownEntry.id === (activePreset && activePreset.id)) {
|
|
1425
|
-
setDraft(Object.assign({},
|
|
1693
|
+
setDraft(Object.assign({}, value))
|
|
1694
|
+
setNote(t.presetDeletedKeepApplied) // 「已应用配置不受影响」提示
|
|
1695
|
+
} else {
|
|
1696
|
+
setNote(t.customDeleted)
|
|
1426
1697
|
}
|
|
1427
|
-
setNote(t.customDeleted)
|
|
1428
1698
|
log('custom preset deleted')
|
|
1429
1699
|
}
|
|
1430
1700
|
|
|
@@ -1494,40 +1764,81 @@ window.__ModuleLoader__.load({
|
|
|
1494
1764
|
scope.set('imagePreviews', imagePreviews),
|
|
1495
1765
|
scope.set('iconPreviews', iconPreviews),
|
|
1496
1766
|
]
|
|
1497
|
-
Promise.all
|
|
1767
|
+
// P1-3:串行写 scope 字段(失败即停,明确已写数量;不再 Promise.all 静默部分成功)
|
|
1768
|
+
var serialDone = 0
|
|
1769
|
+
var chain = Promise.resolve()
|
|
1770
|
+
for (var si2 = 0; si2 < tasks.length; si2++) {
|
|
1771
|
+
;(function (task) {
|
|
1772
|
+
chain = chain.then(function () { return task }).then(function () { serialDone++ })
|
|
1773
|
+
})(tasks[si2])
|
|
1774
|
+
}
|
|
1775
|
+
chain.then(function () {
|
|
1776
|
+
// P0-3:保存快照——回调使用触发保存时的值,避免保存期间用户操作(删预设/编辑)导致闭包陈旧竞态
|
|
1777
|
+
var snapCustom = customPresets.slice()
|
|
1778
|
+
var snapShownCustom = shownCustom
|
|
1779
|
+
var snapShownEntry = shownEntry
|
|
1780
|
+
var snapFlat = flatOf(cur)
|
|
1498
1781
|
// 预设自动同步:
|
|
1499
1782
|
// - 来源是自定义预设(含未命名预设)→ 更新它;
|
|
1500
1783
|
// - 来源是默认/空白 → 【若已有未命名预设则继续编号新建】未命名预设 2/3…
|
|
1501
|
-
var lang = LANG_IDS.indexOf(
|
|
1784
|
+
var lang = LANG_IDS.indexOf(snapFlat.language) >= 0 ? snapFlat.language : 'zh'
|
|
1502
1785
|
var targetId
|
|
1503
1786
|
var entry
|
|
1504
1787
|
var list
|
|
1505
1788
|
var found = null
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1789
|
+
// 通知媒体随预设一并保存(全局 + 按原因):否则保存后的新/更新预设丢图,
|
|
1790
|
+
// 且重开面板时 presetIdOf 会误匹配无图的默认预设
|
|
1791
|
+
var media = {
|
|
1792
|
+
imageUrl: String(snapFlat.imageUrl ?? ''),
|
|
1793
|
+
iconUrl: String(snapFlat.iconUrl ?? ''),
|
|
1794
|
+
imagePreviewUrl: String(snapFlat['preview-imageUrl'] ?? ''),
|
|
1795
|
+
iconPreviewUrl: String(snapFlat['preview-iconUrl'] ?? ''),
|
|
1796
|
+
images: images,
|
|
1797
|
+
icons: icons,
|
|
1798
|
+
imagePreviews: imagePreviews,
|
|
1799
|
+
iconPreviews: iconPreviews,
|
|
1800
|
+
}
|
|
1801
|
+
if (snapShownCustom && snapShownEntry) {
|
|
1802
|
+
targetId = snapShownEntry.id
|
|
1803
|
+
entry = Object.assign({}, snapShownEntry, { language: lang, titleTemplate: snapFlat.titleTemplate, titleTemplates: titleTemplates, templates: templates }, media)
|
|
1804
|
+
list = snapCustom.map(function (cp) {
|
|
1510
1805
|
if (cp.id === targetId) { found = Object.assign({}, cp, entry); return found }
|
|
1511
1806
|
return cp
|
|
1512
1807
|
})
|
|
1513
1808
|
if (!found) { list = list.concat([entry]); found = entry }
|
|
1514
1809
|
} else {
|
|
1515
|
-
|
|
1810
|
+
// P2-6:当前配置与默认完全一致(未做任何修改)→ 保存无意义,跳过重复新建
|
|
1811
|
+
if (presetIdOf(cur, customPresets) === 'default') {
|
|
1812
|
+
setNote(t.noChange)
|
|
1813
|
+
setDraft(null)
|
|
1814
|
+
setJustSaved(true)
|
|
1815
|
+
log('settings saved (matches default); no new preset created')
|
|
1816
|
+
return
|
|
1817
|
+
}
|
|
1818
|
+
var n = nextUnnamedNumber(snapCustom)
|
|
1516
1819
|
targetId = n === 1 ? 'unnamed' : 'unnamed-' + n
|
|
1517
|
-
entry = { id: targetId, name: null, auto: true, language: lang, titleTemplate:
|
|
1518
|
-
list =
|
|
1820
|
+
entry = Object.assign({ id: targetId, name: null, auto: true, language: lang, titleTemplate: snapFlat.titleTemplate, titleTemplates: titleTemplates, templates: templates }, media)
|
|
1821
|
+
list = snapCustom.concat([entry])
|
|
1519
1822
|
found = entry
|
|
1520
1823
|
}
|
|
1521
1824
|
setCustomPresets(list)
|
|
1522
1825
|
saveCustomPresets(list)
|
|
1523
1826
|
setActivePreset({ id: targetId, custom: true, src: found })
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1827
|
+
// P0-3:保存期间用户是否又改了草稿?改了则保留草稿并提示(不清空用户输入)
|
|
1828
|
+
var nowDraft = draftRef.current
|
|
1829
|
+
if (nowDraft !== null && !sameFlat(flatOf(nowDraft), snapFlat)) {
|
|
1830
|
+
setNote(t.saveDuringEdit)
|
|
1831
|
+
setJustSaved(true)
|
|
1832
|
+
log('settings saved; preset synced: ' + targetId + ' (edits during save kept)')
|
|
1833
|
+
} else {
|
|
1834
|
+
setDraft(null)
|
|
1835
|
+
setNote(t.saved)
|
|
1836
|
+
setJustSaved(true)
|
|
1837
|
+
log('settings saved; preset synced: ' + targetId + ' (wrote ' + serialDone + '/' + tasks.length + ')')
|
|
1838
|
+
}
|
|
1528
1839
|
}).catch(function (e) {
|
|
1529
|
-
setNote(t.saveFailed + (e && e.message ? e.message : e))
|
|
1530
|
-
log('settings save failed: ' + (e && e.message ? e.message : e))
|
|
1840
|
+
setNote(t.saveFailed + (e && e.message ? e.message : e) + '(已写入 ' + serialDone + '/' + tasks.length + ' 项)')
|
|
1841
|
+
log('settings save failed at ' + serialDone + '/' + tasks.length + ': ' + (e && e.message ? e.message : e))
|
|
1531
1842
|
})
|
|
1532
1843
|
}
|
|
1533
1844
|
|
|
@@ -1658,7 +1969,8 @@ window.__ModuleLoader__.load({
|
|
|
1658
1969
|
hasMedia
|
|
1659
1970
|
? h('button', {
|
|
1660
1971
|
type: 'button', title: t.clearMedia,
|
|
1661
|
-
|
|
1972
|
+
// P1-2:裁切版与预览版一并清除,避免 preview-* 残留脏 data URI(与 save() 按原因路径一致)
|
|
1973
|
+
onClick: function () { setVal(key, ''); setVal('preview-' + key, '') },
|
|
1662
1974
|
style: {
|
|
1663
1975
|
position: 'absolute', top: '-7px', right: '-7px', width: '20px', height: '20px', padding: '0',
|
|
1664
1976
|
borderRadius: '50%', border: 'none', cursor: 'pointer',
|
|
@@ -1754,14 +2066,22 @@ window.__ModuleLoader__.load({
|
|
|
1754
2066
|
scope.set('imagePreviews', def.imagePreviews),
|
|
1755
2067
|
scope.set('iconPreviews', def.iconPreviews),
|
|
1756
2068
|
]
|
|
1757
|
-
|
|
2069
|
+
// P1-3:串行写(与 save() 一致,失败即停并报告已写数量)
|
|
2070
|
+
var serialDone = 0
|
|
2071
|
+
var chain = Promise.resolve()
|
|
2072
|
+
for (var si3 = 0; si3 < tasks.length; si3++) {
|
|
2073
|
+
;(function (task) {
|
|
2074
|
+
chain = chain.then(function () { return task }).then(function () { serialDone++ })
|
|
2075
|
+
})(tasks[si3])
|
|
2076
|
+
}
|
|
2077
|
+
chain.then(function () {
|
|
1758
2078
|
setDraft(null)
|
|
1759
2079
|
setNote(t.resetDone)
|
|
1760
2080
|
setJustSaved(true)
|
|
1761
2081
|
log('settings reset to defaults (language kept: ' + lang + ')')
|
|
1762
2082
|
}).catch(function (e) {
|
|
1763
|
-
setNote(t.saveFailed + (e && e.message ? e.message : e))
|
|
1764
|
-
log('settings reset failed: ' + (e && e.message ? e.message : e))
|
|
2083
|
+
setNote(t.saveFailed + (e && e.message ? e.message : e) + '(已写入 ' + serialDone + '/' + tasks.length + ' 项)')
|
|
2084
|
+
log('settings reset failed at ' + serialDone + '/' + tasks.length + ': ' + (e && e.message ? e.message : e))
|
|
1765
2085
|
})
|
|
1766
2086
|
}
|
|
1767
2087
|
|
|
@@ -2160,8 +2480,9 @@ window.__ModuleLoader__.load({
|
|
|
2160
2480
|
: null,
|
|
2161
2481
|
),
|
|
2162
2482
|
h('button', {
|
|
2163
|
-
|
|
2164
|
-
|
|
2483
|
+
// P2-5:无修改(!dirty)时禁用重置,避免无意义写库
|
|
2484
|
+
type: 'button', disabled: !dirty || note === t.saving, onClick: resetAll,
|
|
2485
|
+
style: Object.assign({}, GHOST, { opacity: (!dirty || note === t.saving) ? 0.4 : 1 }),
|
|
2165
2486
|
}, t.reset),
|
|
2166
2487
|
h('button', {
|
|
2167
2488
|
type: 'button', disabled: !dirty || note === t.saving, onClick: discard,
|