@telosmaylx/dsh-session-notify 0.1.9 → 0.1.11

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/core.js CHANGED
@@ -14,11 +14,11 @@ export const LANGUAGES = ['zh', 'zh-tw', 'en', 'ja', 'ko']
14
14
  * 键为 LANGUAGES 中的代码;缺省回落 zh。
15
15
  */
16
16
  const LABELS = {
17
- zh: { completed: '会话已完成', aborted: '会话已中止', blocked: '会话被阻塞', error: '会话出错', 'max-tokens': '会话达到输出上限', interrupted: '会话已中断', generic: '会话已结束', sep: ':', dur: '用时', use: '消耗' },
18
- 'zh-tw': { completed: '會話已完成', aborted: '會話已中止', blocked: '會話被阻塞', error: '會話出錯', 'max-tokens': '會話達到輸出上限', interrupted: '會話已中斷', generic: '會話已結束', sep: ':', dur: '用時', use: '消耗' },
19
- en: { completed: 'Session completed', aborted: 'Session aborted', blocked: 'Session blocked', error: 'Session failed', 'max-tokens': 'Session hit the output-token cap', interrupted: 'Session interrupted', generic: 'Session ended', sep: ': ', dur: 'took', use: 'used' },
20
- ja: { completed: 'セッション完了', aborted: 'セッション中止', blocked: 'セッションがブロックされました', error: 'セッションエラー', 'max-tokens': '出力トークン上限に到達', interrupted: 'セッションが中断されました', generic: 'セッション終了', sep: ':', dur: '所要', use: '消費' },
21
- ko: { completed: '세션 완료', aborted: '세션 중단됨', blocked: '세션 차단됨', error: '세션 오류', 'max-tokens': '출력 토큰 한도 도달', interrupted: '세션 중단됨', generic: '세션 종료', sep: ': ', dur: '소요', use: '소모' },
17
+ zh: { completed: '会话已完成', aborted: '会话已中止', blocked: '会话被阻塞', error: '会话出错', 'max-tokens': '会话达到上限', interrupted: '会话已中断', generic: '会话已结束', sep: ':', dur: '用时', use: '消耗' },
18
+ 'zh-tw': { completed: '會話已完成', aborted: '會話已中止', blocked: '會話被阻塞', error: '會話出錯', 'max-tokens': '會話達到上限', interrupted: '會話已中斷', generic: '會話已結束', sep: ':', dur: '用時', use: '消耗' },
19
+ en: { completed: 'Session completed', aborted: 'Session aborted', blocked: 'Session blocked', error: 'Session failed', 'max-tokens': 'Session hit the cap', interrupted: 'Session interrupted', generic: 'Session ended', sep: ': ', dur: 'took', use: 'used' },
20
+ ja: { completed: 'セッション完了', aborted: 'セッション中止', blocked: 'セッションがブロックされました', error: 'セッションエラー', 'max-tokens': '上限に到達', interrupted: 'セッションが中断されました', generic: 'セッション終了', sep: ':', dur: '所要', use: '消費' },
21
+ ko: { completed: '세션 완료', aborted: '세션 중단됨', blocked: '세션 차단됨', error: '세션 오류', 'max-tokens': '한도 도달', interrupted: '세션 중단됨', generic: '세션 종료', sep: ': ', dur: '소요', use: '소모' },
22
22
  }
23
23
 
24
24
  function tableFor(language) {
@@ -30,11 +30,11 @@ function tableFor(language) {
30
30
  * 优先级:titleTemplates[kind] > titleTemplate(全局模板)> 本表。
31
31
  */
32
32
  const DEFAULT_TITLES = {
33
- zh: { completed: '任务已完成', aborted: '任务已中止', blocked: '任务被阻塞', error: '任务出错', 'max-tokens': '任务达到输出上限' },
34
- 'zh-tw': { completed: '任務已完成', aborted: '任務已中止', blocked: '任務被阻塞', error: '任務出錯', 'max-tokens': '任務達到輸出上限' },
35
- en: { completed: 'Task completed', aborted: 'Task aborted', blocked: 'Task blocked', error: 'Task failed', 'max-tokens': 'Task hit the output cap' },
36
- ja: { completed: 'タスク完了', aborted: 'タスク中止', blocked: 'タスクがブロックされました', error: 'タスクエラー', 'max-tokens': '出力上限に到達' },
37
- ko: { completed: '작업 완료', aborted: '작업 중단됨', blocked: '작업 차단됨', error: '작업 오류', 'max-tokens': '출력 한도 도달' },
33
+ zh: { completed: '任务已完成', aborted: '任务已中止', blocked: '任务被阻塞', error: '任务出错', 'max-tokens': '任务达到上限' },
34
+ 'zh-tw': { completed: '任務已完成', aborted: '任務已中止', blocked: '任務被阻塞', error: '任務出錯', 'max-tokens': '任務達到上限' },
35
+ en: { completed: 'Task completed', aborted: 'Task aborted', blocked: 'Task blocked', error: 'Task failed', 'max-tokens': 'Task hit the cap' },
36
+ ja: { completed: 'タスク完了', aborted: 'タスク中止', blocked: 'タスクがブロックされました', error: 'タスクエラー', 'max-tokens': '上限に到達' },
37
+ ko: { completed: '작업 완료', aborted: '작업 중단됨', blocked: '작업 차단됨', error: '작업 오류', 'max-tokens': '한도 도달' },
38
38
  }
39
39
 
40
40
  /**
@@ -52,7 +52,19 @@ export function renderTitle(kind, opts = {}, titleValue = '') {
52
52
  const global = typeof opts.titleTemplate === 'string' && opts.titleTemplate.trim() !== '' ? opts.titleTemplate : ''
53
53
  const fallback = (DEFAULT_TITLES[language] && DEFAULT_TITLES[language][kind]) || DEFAULT_TITLES.zh[kind] || '任务已完成'
54
54
  const tpl = per || global || fallback
55
- const out = String(tpl).replaceAll('{title}', titleValue).trim()
55
+ // {image}/{icon} 是正文媒体的开关标签,标题里若误写则剥除;其余信息位({duration}{usage}{error}{cache}{tps})
56
+ // 用会话数据替换(无数据则替换为空串),避免字面量泄漏到推送标题
57
+ const out = String(tpl)
58
+ .replace(/\{image\}/g, '')
59
+ .replace(/\{icon\}/g, '')
60
+ .replace(/\{label\}/g, '')
61
+ .replaceAll('{title}', titleValue)
62
+ .replaceAll('{duration}', opts.duration ?? '')
63
+ .replaceAll('{usage}', opts.usage ?? '')
64
+ .replaceAll('{error}', opts.error ?? '')
65
+ .replaceAll('{cache}', opts.cache ?? '')
66
+ .replaceAll('{tps}', opts.tps ?? '')
67
+ .trim()
56
68
  return out || fallback
57
69
  }
58
70
 
@@ -67,44 +79,44 @@ export function renderTitle(kind, opts = {}, titleValue = '') {
67
79
  */
68
80
  const DEFAULT_PHRASES = {
69
81
  zh: {
70
- completed: '会话{titleBlock}已完成(用时 {dur},消耗 {use})。',
71
- aborted: '会话{titleBlock}已中止。用时 {dur},消耗 {use}。',
72
- blocked: '会话{titleBlock}被阻塞。用时 {dur},消耗 {use}。',
73
- error: '会话{titleBlock}出错{errBlock}(用时 {dur},消耗 {use})。',
74
- 'max-tokens': '会话{titleBlock}达到输出上限。用时 {dur},消耗 {use},建议拆分任务后重试。',
75
- generic: '会话{titleBlock}已结束。用时 {dur},消耗 {use}。',
82
+ completed: '会话{titleBlock}已完成,请点击查看。',
83
+ aborted: '会话{titleBlock}已中止,请点击查看。',
84
+ blocked: '会话{titleBlock}被阻塞,请点击查看。',
85
+ error: '会话{titleBlock}出错,请点击查看。',
86
+ 'max-tokens': '会话{titleBlock}达到上限,请点击查看。',
87
+ generic: '会话{titleBlock}已结束,请点击查看。',
76
88
  },
77
89
  'zh-tw': {
78
- completed: '會話{titleBlock}已完成(用時 {dur},消耗 {use})。',
79
- aborted: '會話{titleBlock}已中止。用時 {dur},消耗 {use}。',
80
- blocked: '會話{titleBlock}被阻塞。用時 {dur},消耗 {use}。',
81
- error: '會話{titleBlock}出錯{errBlock}(用時 {dur},消耗 {use})。',
82
- 'max-tokens': '會話{titleBlock}達到輸出上限。用時 {dur},消耗 {use},建議拆分任務後重試。',
83
- generic: '會話{titleBlock}已結束。用時 {dur},消耗 {use}。',
90
+ completed: '會話{titleBlock}已完成,請點擊查看。',
91
+ aborted: '會話{titleBlock}已中止,請點擊查看。',
92
+ blocked: '會話{titleBlock}被阻塞,請點擊查看。',
93
+ error: '會話{titleBlock}出錯,請點擊查看。',
94
+ 'max-tokens': '會話{titleBlock}達到上限,請點擊查看。',
95
+ generic: '會話{titleBlock}已結束,請點擊查看。',
84
96
  },
85
97
  en: {
86
- completed: 'Session {titleBlock} completed (took {dur}, used {use}).',
87
- aborted: 'Session {titleBlock} aborted. Took {dur}, used {use}.',
88
- blocked: 'Session {titleBlock} blocked. Took {dur}, used {use}.',
89
- error: 'Session {titleBlock} failed{errBlock} (took {dur}, used {use}).',
90
- 'max-tokens': 'Session {titleBlock} hit the output-token cap. Took {dur}, used {use} — consider splitting the task.',
91
- generic: 'Session {titleBlock} ended. Took {dur}, used {use}.',
98
+ completed: 'Session {titleBlock} completed. Click to view.',
99
+ aborted: 'Session {titleBlock} aborted. Click to view.',
100
+ blocked: 'Session {titleBlock} blocked. Click to view.',
101
+ error: 'Session {titleBlock} failed. Click to view.',
102
+ 'max-tokens': 'Session {titleBlock} hit the cap. Click to view.',
103
+ generic: 'Session {titleBlock} ended. Click to view.',
92
104
  },
93
105
  ja: {
94
- completed: 'セッション{titleBlock}完了(所要 {dur}、消費 {use})。',
95
- aborted: 'セッション{titleBlock}中止。所要 {dur}、消費 {use}。',
96
- blocked: 'セッション{titleBlock}がブロックされました。所要 {dur}、消費 {use}。',
97
- error: 'セッション{titleBlock}エラー{errBlock}(所要 {dur}、消費 {use})。',
98
- 'max-tokens': 'セッション{titleBlock}が出力トークン上限に到達。所要 {dur}、消費 {use}。タスクの分割をおすすめします。',
99
- generic: 'セッション{titleBlock}終了。所要 {dur}、消費 {use}。',
106
+ completed: 'セッション{titleBlock}完了。クリックして表示。',
107
+ aborted: 'セッション{titleBlock}中止。クリックして表示。',
108
+ blocked: 'セッション{titleBlock}がブロックされました。クリックして表示。',
109
+ error: 'セッション{titleBlock}エラー。クリックして表示。',
110
+ 'max-tokens': 'セッション{titleBlock}上限に到達。クリックして表示。',
111
+ generic: 'セッション{titleBlock}終了。クリックして表示。',
100
112
  },
101
113
  ko: {
102
- completed: '세션{titleBlock} 완료(소요 {dur}, 소모 {use})。',
103
- aborted: '세션{titleBlock} 중단됨. 소요 {dur}, 소모 {use}.',
104
- blocked: '세션{titleBlock} 차단됨. 소요 {dur}, 소모 {use}.',
105
- error: '세션{titleBlock} 오류{errBlock}(소요 {dur}, 소모 {use}).',
106
- 'max-tokens': '세션{titleBlock} 출력 한도 도달. 소요 {dur}, 소모 {use} — 작업 분할을 권장합니다.',
107
- generic: '세션{titleBlock} 종료. 소요 {dur}, 소모 {use}.',
114
+ completed: '세션{titleBlock} 완료. 클릭하여 확인.',
115
+ aborted: '세션{titleBlock} 중단됨. 클릭하여 확인.',
116
+ blocked: '세션{titleBlock} 차단됨. 클릭하여 확인.',
117
+ error: '세션{titleBlock} 오류. 클릭하여 확인.',
118
+ 'max-tokens': '세션{titleBlock} 한도 도달. 클릭하여 확인.',
119
+ generic: '세션{titleBlock} 종료. 클릭하여 확인.',
108
120
  },
109
121
  }
110
122
 
@@ -299,7 +311,7 @@ export function tpsOf(usage, ms) {
299
311
  * @param {{ startedAt: number; endedAt: number; usage?: object | null; includeDuration?: boolean; includeUsage?: boolean; cacheValue?: string; tpsValue?: string; titleValue?: string }} info
300
312
  * @param {{ language?: 'zh' | 'en'; templates?: Record<string, string> }} [custom] -
301
313
  * language 切换文案语言;templates[reason] 非空时作为正文模板,
302
- * 支持占位符:{label} {title} {duration} {usage} {error} {cache} {tps}
314
+ * 支持占位符:{label} {title} {duration} {usage} {error} {cache} {tps}({image} 渲染时剥除)。
303
315
  * @returns {{ summary: string; text: string }}
304
316
  */
305
317
  export function buildNotice(kind, reasonData, { startedAt, endedAt, usage = null, includeDuration = true, includeUsage = true, cacheValue, tpsValue, titleValue }, custom = {}) {
@@ -359,11 +371,19 @@ export function buildNotice(kind, reasonData, { startedAt, endedAt, usage = null
359
371
  // 「会话已完成」,用户展开前完全看不到内容)。
360
372
  summary: bound(text, 120),
361
373
  text,
374
+ // 标题信息位数据(renderTitle 替换 {duration}{usage}{error}{cache}{tps} 用)
375
+ duration: durationLine ?? '',
376
+ usage: usageLine ?? '',
377
+ error: errText || 'none', // 无报错时显示 none,而非空串
378
+ cache: cacheLine ?? '',
379
+ tps: tpsLine ?? '',
362
380
  }
363
381
  }
364
382
 
365
383
  /**
366
384
  * 模板占位符替换:{label} 已全面移除(模板中直接剥除);支持 {duration} {usage} {error} {cache} {tps}。
385
+ * {image} {icon} 同样在正文渲染时剥除——它们只是「该原因通知带自定义大图/图标」的开关标签,
386
+ * 图片本体(data URI)存于设置文档 images[kind] / icons[kind],由客户端发通知时附加,绝不进入会话日志文本。
367
387
  * @param {string} template
368
388
  * @param {{ label: string; duration: string; usage: string; error: string; cache: string; tps: string }} vars
369
389
  * @returns {string}
@@ -371,6 +391,8 @@ export function buildNotice(kind, reasonData, { startedAt, endedAt, usage = null
371
391
  export function renderTemplate(template, vars) {
372
392
  return String(template)
373
393
  .replace(/\{label\}/g, '')
394
+ .replace(/\{image\}/g, '')
395
+ .replace(/\{icon\}/g, '')
374
396
  .replaceAll('{title}', vars.title ?? '')
375
397
  .replaceAll('{duration}', vars.duration)
376
398
  .replaceAll('{usage}', vars.usage)
package/lib/index.js CHANGED
@@ -81,6 +81,20 @@ const DEFAULT_SETTINGS = {
81
81
  skipSubagents: true,
82
82
  /** 推送通道:dual=系统通知+页内提示(默认);system=仅系统通知;toast=仅页内提示。 */
83
83
  pushMode: 'dual',
84
+ /** 系统通知大图(Notification image,https URL):显示在通知卡片正文中/下方;空 = 不显示。 */
85
+ imageUrl: '',
86
+ /** 系统通知图标(Notification icon,https URL):空 = 浏览器默认(站点图标)。 */
87
+ iconUrl: '',
88
+ /** 完整预览版(等比 1024 未裁切,lightbox 查看原图用):与 imageUrl/iconUrl 对应,空 = 回退裁切版。 */
89
+ imagePreviewUrl: '',
90
+ iconPreviewUrl: '',
91
+ /** 按原因的自定义通知大图({image} 标签开关对应的数据,本地文件压缩后的 data URI)。 */
92
+ images: { completed: '', error: '', aborted: '', blocked: '', 'max-tokens': '' },
93
+ /** 按原因的自定义通知图标({icon} 标签开关对应的数据,本地文件压缩后的 data URI,128×128 方形)。 */
94
+ icons: { completed: '', error: '', aborted: '', blocked: '', 'max-tokens': '' },
95
+ /** 按原因的完整预览版(等比 1024 未裁切),与 images/icons 一一对应。 */
96
+ imagePreviews: { completed: '', error: '', aborted: '', blocked: '', 'max-tokens': '' },
97
+ iconPreviews: { completed: '', error: '', aborted: '', blocked: '', 'max-tokens': '' },
84
98
  }
85
99
 
86
100
  /** 从任意输入规整为设置形状(容忍缺失/多余字段)。 */
@@ -96,6 +110,26 @@ function sanitizeSettings(raw) {
96
110
  for (const key of Object.keys(titleTemplates)) {
97
111
  if (typeof titlesRaw[key] === 'string') titleTemplates[key] = titlesRaw[key]
98
112
  }
113
+ const imagesRaw = src.images && typeof src.images === 'object' ? src.images : {}
114
+ const images = { ...DEFAULT_SETTINGS.images }
115
+ for (const key of Object.keys(images)) {
116
+ if (typeof imagesRaw[key] === 'string') images[key] = imagesRaw[key]
117
+ }
118
+ const iconsRaw = src.icons && typeof src.icons === 'object' ? src.icons : {}
119
+ const icons = { ...DEFAULT_SETTINGS.icons }
120
+ for (const key of Object.keys(icons)) {
121
+ if (typeof iconsRaw[key] === 'string') icons[key] = iconsRaw[key]
122
+ }
123
+ const imagePreviewsRaw = src.imagePreviews && typeof src.imagePreviews === 'object' ? src.imagePreviews : {}
124
+ const imagePreviews = { ...DEFAULT_SETTINGS.imagePreviews }
125
+ for (const key of Object.keys(imagePreviews)) {
126
+ if (typeof imagePreviewsRaw[key] === 'string') imagePreviews[key] = imagePreviewsRaw[key]
127
+ }
128
+ const iconPreviewsRaw = src.iconPreviews && typeof src.iconPreviews === 'object' ? src.iconPreviews : {}
129
+ const iconPreviews = { ...DEFAULT_SETTINGS.iconPreviews }
130
+ for (const key of Object.keys(iconPreviews)) {
131
+ if (typeof iconPreviewsRaw[key] === 'string') iconPreviews[key] = iconPreviewsRaw[key]
132
+ }
99
133
  return {
100
134
  language: ['zh', 'zh-tw', 'en', 'ja', 'ko'].includes(src.language) ? src.language : 'zh',
101
135
  templates,
@@ -105,6 +139,14 @@ function sanitizeSettings(raw) {
105
139
  includeUsage: typeof src.includeUsage === 'boolean' ? src.includeUsage : true,
106
140
  skipSubagents: typeof src.skipSubagents === 'boolean' ? src.skipSubagents : true,
107
141
  pushMode: ['dual', 'system', 'toast'].includes(src.pushMode) ? src.pushMode : 'dual',
142
+ imageUrl: typeof src.imageUrl === 'string' ? src.imageUrl : '',
143
+ iconUrl: typeof src.iconUrl === 'string' ? src.iconUrl : '',
144
+ imagePreviewUrl: typeof src.imagePreviewUrl === 'string' ? src.imagePreviewUrl : '',
145
+ iconPreviewUrl: typeof src.iconPreviewUrl === 'string' ? src.iconPreviewUrl : '',
146
+ images,
147
+ icons,
148
+ imagePreviews,
149
+ iconPreviews,
108
150
  }
109
151
  }
110
152
 
@@ -149,6 +191,38 @@ export function apply(ctx, config = {}) {
149
191
  includeUsage: Schema.boolean().default(true),
150
192
  skipSubagents: Schema.boolean().default(true),
151
193
  pushMode: Schema.union(['dual', 'system', 'toast']).default('dual'),
194
+ imageUrl: Schema.string().default(''),
195
+ iconUrl: Schema.string().default(''),
196
+ imagePreviewUrl: Schema.string().default(''),
197
+ iconPreviewUrl: Schema.string().default(''),
198
+ images: Schema.object({
199
+ completed: Schema.string().default(''),
200
+ error: Schema.string().default(''),
201
+ aborted: Schema.string().default(''),
202
+ blocked: Schema.string().default(''),
203
+ 'max-tokens': Schema.string().default(''),
204
+ }),
205
+ icons: Schema.object({
206
+ completed: Schema.string().default(''),
207
+ error: Schema.string().default(''),
208
+ aborted: Schema.string().default(''),
209
+ blocked: Schema.string().default(''),
210
+ 'max-tokens': Schema.string().default(''),
211
+ }),
212
+ imagePreviews: Schema.object({
213
+ completed: Schema.string().default(''),
214
+ error: Schema.string().default(''),
215
+ aborted: Schema.string().default(''),
216
+ blocked: Schema.string().default(''),
217
+ 'max-tokens': Schema.string().default(''),
218
+ }),
219
+ iconPreviews: Schema.object({
220
+ completed: Schema.string().default(''),
221
+ error: Schema.string().default(''),
222
+ aborted: Schema.string().default(''),
223
+ blocked: Schema.string().default(''),
224
+ 'max-tokens': Schema.string().default(''),
225
+ }),
152
226
  })
153
227
  let attempts = 0
154
228
  const tryRegister = () => {
@@ -285,8 +359,18 @@ export function apply(ctx, config = {}) {
285
359
  tpsValue,
286
360
  titleValue,
287
361
  }, { language: settings.language, templates: settings.templates })
288
- // 推送标题按原因渲染:titleTemplates[kind] > titleTemplate > 语言默认(如「任务已完成」)
289
- const title = renderTitle(kind, { language: settings.language, titleTemplate: settings.titleTemplate, titleTemplates: settings.titleTemplates }, titleValue)
362
+ // 推送标题按原因渲染:titleTemplates[kind] > titleTemplate > 语言默认(如「任务已完成」);
363
+ // 标题里插的信息位({duration}{usage}{error}{cache}{tps})用会话数据替换,无则剥除
364
+ const title = renderTitle(kind, {
365
+ language: settings.language,
366
+ titleTemplate: settings.titleTemplate,
367
+ titleTemplates: settings.titleTemplates,
368
+ duration: notice.duration,
369
+ usage: notice.usage,
370
+ error: notice.error,
371
+ cache: notice.cache,
372
+ tps: notice.tps,
373
+ }, titleValue)
290
374
  // 边界约束:session/event 观察者回调运行在 turn/end 那次 append 的
291
375
  // 发布边界之内(dsh-session 在 dispatch 前 set entry.appending,
292
376
  // finally 中复位),此时同步 append 会被拒绝:
package/package.json CHANGED
@@ -1,70 +1,74 @@
1
- {
2
- "name": "@telosmaylx/dsh-session-notify",
3
- "version": "0.1.9",
4
- "description": "DSH session completion notifier: appends a plugin system message to the session log on turn/end and pushes browser notifications (Web Notification + toast), with a fully customizable official settings panel (presets, 5-language templates, cache-hit rate & tok/s from official projections).",
5
- "private": false,
6
- "type": "module",
7
- "main": "./lib/index.js",
8
- "exports": {
9
- ".": {
10
- "default": "./lib/index.js"
11
- },
12
- "./client": {
13
- "default": "./lib/client.js"
14
- },
15
- "./core": {
16
- "default": "./lib/core.js"
17
- },
18
- "./package.json": "./package.json"
19
- },
20
- "files": [
21
- "lib",
22
- "scripts",
23
- "cordis.patch.yml",
24
- "README.md",
25
- "LICENSE"
26
- ],
27
- "publishConfig": {
28
- "access": "public"
29
- },
30
- "keywords": [
31
- "deepseek-harness",
32
- "dsh",
33
- "dsh-plugin",
34
- "session-notify",
35
- "notification",
36
- "completion-reminder",
37
- "agent"
38
- ],
39
- "author": "dsh-session-notify contributors",
40
- "license": "MIT",
41
- "repository": {
42
- "type": "git",
43
- "url": "git+https://github.com/TelosmaYLX/dsh-session-notify.git"
44
- },
45
- "homepage": "https://github.com/TelosmaYLX/dsh-session-notify#readme",
46
- "bugs": {
47
- "url": "https://github.com/TelosmaYLX/dsh-session-notify/issues"
48
- },
49
- "peerDependencies": {
50
- "cordis": ">=4.0.0-rc <5"
51
- },
52
- "engines": {
53
- "node": ">=22"
54
- },
55
- "dsh": {
56
- "bundle": {
57
- "patch": "./cordis.patch.yml"
58
- },
59
- "client": {
60
- "inject": [
61
- "@deepseek-ai/dsh-client-runtime"
62
- ],
63
- "platform": "web"
64
- }
65
- },
66
- "scripts": {
67
- "build": "bash scripts/build.sh || node --check lib/index.js && node --check lib/core.js && node --check lib/client.js",
68
- "prepublishOnly": "node --check lib/index.js && node --check lib/core.js && node --check lib/client.js"
69
- }
70
- }
1
+ {
2
+ "name": "@telosmaylx/dsh-session-notify",
3
+ "version": "0.1.11",
4
+ "description": "DSH session completion notifier: appends a plugin system message to the session log on turn/end and pushes browser notifications (Web Notification + toast), with a fully customizable official settings panel (presets, 5-language templates, cache-hit rate & tok/s from official projections).",
5
+ "private": false,
6
+ "type": "module",
7
+ "main": "./lib/index.js",
8
+ "exports": {
9
+ ".": {
10
+ "default": "./lib/index.js"
11
+ },
12
+ "./client": {
13
+ "default": "./lib/client.js"
14
+ },
15
+ "./core": {
16
+ "default": "./lib/core.js"
17
+ },
18
+ "./package.json": "./package.json"
19
+ },
20
+ "files": [
21
+ "lib",
22
+ "scripts",
23
+ "cordis.patch.yml",
24
+ "README.md",
25
+ "README.en.md",
26
+ "README.zh-TW.md",
27
+ "README.ja.md",
28
+ "README.ko.md",
29
+ "LICENSE"
30
+ ],
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
34
+ "keywords": [
35
+ "deepseek-harness",
36
+ "dsh",
37
+ "dsh-plugin",
38
+ "session-notify",
39
+ "notification",
40
+ "completion-reminder",
41
+ "agent"
42
+ ],
43
+ "author": "dsh-session-notify contributors",
44
+ "license": "MIT",
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "git+https://github.com/TelosmaYLX/dsh-session-notify.git"
48
+ },
49
+ "homepage": "https://github.com/TelosmaYLX/dsh-session-notify#readme",
50
+ "bugs": {
51
+ "url": "https://github.com/TelosmaYLX/dsh-session-notify/issues"
52
+ },
53
+ "peerDependencies": {
54
+ "cordis": ">=4.0.0-rc <5"
55
+ },
56
+ "engines": {
57
+ "node": ">=22"
58
+ },
59
+ "dsh": {
60
+ "bundle": {
61
+ "patch": "./cordis.patch.yml"
62
+ },
63
+ "client": {
64
+ "inject": [
65
+ "@deepseek-ai/dsh-client-runtime"
66
+ ],
67
+ "platform": "web"
68
+ }
69
+ },
70
+ "scripts": {
71
+ "build": "bash scripts/build.sh || node --check lib/index.js && node --check lib/core.js && node --check lib/client.js",
72
+ "prepublishOnly": "node --check lib/index.js && node --check lib/core.js && node --check lib/client.js"
73
+ }
74
+ }
@@ -0,0 +1,70 @@
1
+ // 检查 5 语言 i18n 键一致性:解析 lib/client.js 的 5 个语言块,对比键集合与空值
2
+ import { readFileSync } from 'node:fs'
3
+
4
+ const src = readFileSync('lib/client.js', 'utf8')
5
+ const lines = src.split('\n')
6
+
7
+ // 找到每个语言块的起始行
8
+ const LANG_KEYS = ['zh', "'zh-tw'", 'en', 'ja', 'ko']
9
+ const blocks = {}
10
+ for (let i = 0; i < lines.length; i++) {
11
+ const m = /^\s*(?:'([a-z-]+)'|([a-z]+))\s*:\s*\{/.exec(lines[i])
12
+ if (m) {
13
+ const key = m[1] || m[2]
14
+ // 只收集 I18N 主块(在 i18n 区域 40-180 行附近,避免误抓其他对象)
15
+ if (i < 200 && (LANG_KEYS.includes("'" + key + "'") || LANG_KEYS.includes(key))) {
16
+ // 读取到闭合的 }, 或 }, 或 }, 或
17
+ let block = ''
18
+ let j = i
19
+ let depth = 0
20
+ let started = false
21
+ for (; j < lines.length; j++) {
22
+ const l = lines[j]
23
+ for (const ch of l) {
24
+ if (ch === '{') { depth++; started = true }
25
+ else if (ch === '}') depth--
26
+ }
27
+ block += l + '\n'
28
+ if (started && depth <= 0) break
29
+ }
30
+ blocks[key] = block
31
+ }
32
+ }
33
+ }
34
+
35
+ if (Object.keys(blocks).length < 5) {
36
+ console.log('FAIL: 只找到语言块:', Object.keys(blocks))
37
+ process.exit(1)
38
+ }
39
+
40
+ // 提取键:匹配 key: 'value' 或 key: "value"(含嵌套对象如 fields: {...} —— 只取顶层键)
41
+ function extractKeys(block) {
42
+ const keys = new Set()
43
+ const re = /^\s*([A-Za-z][A-Za-z0-9]*)\s*:/gm
44
+ let m
45
+ while ((m = re.exec(block))) keys.add(m[1])
46
+ // 去掉块首行(语言名,如 "zh: {" / "'zh-tw': {")
47
+ keys.delete('zh'); keys.delete('en'); keys.delete('ja'); keys.delete('ko')
48
+ return keys
49
+ }
50
+
51
+ const zhKeys = extractKeys(blocks['zh'])
52
+ console.log('zh 键数量:', zhKeys.size)
53
+
54
+ let allOk = true
55
+ for (const lang of ['zh-tw', 'en', 'ja', 'ko']) {
56
+ const k = extractKeys(blocks[lang])
57
+ const missing = [...zhKeys].filter((x) => !k.has(x))
58
+ const extra = [...k].filter((x) => !zhKeys.has(x))
59
+ console.log(`\n[${lang}] 键数 ${k.size}`)
60
+ if (missing.length) { console.log(' 缺少(相对zh):', missing.join(', ')); allOk = false }
61
+ if (extra.length) console.log(' 多余(相对zh):', extra.join(', '))
62
+ // 检查关键值是否为空
63
+ for (const key of [...zhKeys]) {
64
+ const vm = new RegExp('\\b' + key + '\\s*:\\s*([\'"])(.*?)\\1').exec(blocks[lang])
65
+ if (vm && vm[2] === '' ) { console.log(` [${lang}] 键 ${key} 值为空!`); allOk = false }
66
+ }
67
+ }
68
+
69
+ console.log('\n===== i18n 键一致性: ' + (allOk ? 'PASS' : 'FAIL') + ' =====')
70
+ process.exit(allOk ? 0 : 1)