@telosmaylx/dsh-session-notify 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/core.js CHANGED
@@ -25,6 +25,96 @@ function tableFor(language) {
25
25
  return LABELS[language] || LABELS.zh
26
26
  }
27
27
 
28
+ /**
29
+ * 各语言默认推送标题(按结束原因差异化)。
30
+ * 优先级:titleTemplates[kind] > titleTemplate(全局模板)> 本表。
31
+ */
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': '출력 한도 도달' },
38
+ }
39
+
40
+ /**
41
+ * 渲染某原因的实际推送标题:
42
+ * titleTemplates[kind](按原因定制)> titleTemplate(全局模板)> DEFAULT_TITLES[lang][kind]。
43
+ * 模板支持 {title} 占位符(会话标题,缺失为空);渲染后为空时回退默认标题。
44
+ * @param {string} kind
45
+ * @param {{ language?: string; titleTemplate?: string; titleTemplates?: Record<string, string> }} opts
46
+ * @param {string} [titleValue]
47
+ * @returns {string}
48
+ */
49
+ export function renderTitle(kind, opts = {}, titleValue = '') {
50
+ const language = LABELS[opts.language] ? opts.language : 'zh'
51
+ const per = (opts.titleTemplates && typeof opts.titleTemplates[kind] === 'string' && opts.titleTemplates[kind].trim() !== '') ? opts.titleTemplates[kind] : ''
52
+ const global = typeof opts.titleTemplate === 'string' && opts.titleTemplate.trim() !== '' ? opts.titleTemplate : ''
53
+ const fallback = (DEFAULT_TITLES[language] && DEFAULT_TITLES[language][kind]) || DEFAULT_TITLES.zh[kind] || '任务已完成'
54
+ const tpl = per || global || fallback
55
+ const out = String(tpl).replaceAll('{title}', titleValue).trim()
56
+ return out || fallback
57
+ }
58
+
59
+ /**
60
+ * 各语言默认正文句式(templates 留空时的回退文案)。
61
+ * 按 reason 差异化表达,避免清一色「(用时 X,消耗 Y)」:
62
+ * - completed:括号紧凑式;aborted/blocked:句号拆句;error:错误详情前置;
63
+ * - max-tokens:句号拆句 + 建议;generic:兜底。
64
+ * 标签内嵌会话标题:{titleBlock}(带语言引号;无标题时为空块,自动退回「会话已完成」)。
65
+ * 其他占位符:{errBlock} 错误详情块(含分隔符,无错误为空)、{dur} 用时、{use} 消耗。
66
+ * 仅当用时与消耗都有数据时启用;数据不全回退到 buildNotice 的 parts 拼接逻辑(自动省略缺失项)。
67
+ */
68
+ const DEFAULT_PHRASES = {
69
+ 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}。',
76
+ },
77
+ '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}。',
84
+ },
85
+ 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}.',
92
+ },
93
+ 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}。',
100
+ },
101
+ 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}.',
108
+ },
109
+ }
110
+
111
+ /** 会话标题块:带语言引号(zh/zh-tw/ja/ko「」,en ");无标题返回空块。 */
112
+ function titleValueBlock(language, titleValue) {
113
+ const v = typeof titleValue === 'string' ? titleValue.trim() : ''
114
+ if (!v) return ''
115
+ return language === 'en' ? `"${v}"` : `「${v}」`
116
+ }
117
+
28
118
  /**
29
119
  * 按语言取 reason 标签。
30
120
  * @param {string} kind
@@ -218,6 +308,7 @@ export function buildNotice(kind, reasonData, { startedAt, endedAt, usage = null
218
308
  const templates = custom.templates ?? {}
219
309
  const label = labelFor(kind, language)
220
310
  const detail = detailFor(reasonData, language)
311
+ const errText = plainError(reasonData)
221
312
  let summary = `${label}${detail}`
222
313
  const durationLine = includeDuration ? formatDuration(endedAt - startedAt, language) : null
223
314
  const usageLine = includeUsage ? summarizeUsage(usage, language) : null
@@ -229,6 +320,7 @@ export function buildNotice(kind, reasonData, { startedAt, endedAt, usage = null
229
320
  if (usageLine) parts.push(`${L.use} ${usageLine}`)
230
321
  const suffix = parts.length ? `(${parts.join(',')})` : ''
231
322
  const template = templates[kind]
323
+ const phraseTpl = (DEFAULT_PHRASES[language] && DEFAULT_PHRASES[language][kind]) || (DEFAULT_PHRASES[language] && DEFAULT_PHRASES[language].generic) || ''
232
324
  let text
233
325
  if (typeof template === 'string' && template.trim() !== '') {
234
326
  // 自定义模板:占位符替换,保持 summary 与正文一致的语义
@@ -236,11 +328,21 @@ export function buildNotice(kind, reasonData, { startedAt, endedAt, usage = null
236
328
  label,
237
329
  duration: durationLine ?? '',
238
330
  usage: usageLine ?? '',
239
- error: plainError(reasonData) || 'none', // 无报错时显示 none,而非空串
331
+ error: errText || 'none', // 无报错时显示 none,而非空串
240
332
  cache: cacheLine,
241
333
  tps: tpsLine,
242
334
  title: typeof titleValue === 'string' ? titleValue : '',
243
335
  })
336
+ } else if (durationLine && usageLine && phraseTpl) {
337
+ // 默认句式(用时+消耗都有时启用,按 reason 差异化表达)
338
+ const errBlock = errText ? L.sep + errText : ''
339
+ const titleBlock = titleValueBlock(language, titleValue)
340
+ text = phraseTpl
341
+ .replaceAll('{label}', label)
342
+ .replaceAll('{titleBlock}', titleBlock)
343
+ .replaceAll('{errBlock}', errBlock)
344
+ .replaceAll('{dur}', durationLine)
345
+ .replaceAll('{use}', usageLine)
244
346
  } else if (language === 'en') {
245
347
  text = `${summary}${suffix ? ` (${parts.join(', ')}).` : '.'}`
246
348
  } else if (language === 'ja') {
package/lib/index.js CHANGED
@@ -36,7 +36,7 @@ import { appendFileSync } from 'node:fs'
36
36
  import { join } from 'node:path'
37
37
  import { homedir } from 'node:os'
38
38
  import { createRequire } from 'node:module'
39
- import { createTurnTracker, buildNotice, isSubagentSession, officialCacheRate, officialTps } from './core.js?v=1' // v=1: 缓存破坏——HMR 重载按 URL 键控
39
+ import { createTurnTracker, buildNotice, isSubagentSession, officialCacheRate, officialTps, renderTitle } from './core.js?v=1' // v=1: 缓存破坏——HMR 重载按 URL 键控
40
40
 
41
41
  /** Cordis loader 诊断用插件名。 */
42
42
  export const name = 'session-complete-notify'
@@ -73,7 +73,9 @@ const DEFAULT_OPTIONS = {
73
73
  const DEFAULT_SETTINGS = {
74
74
  language: 'zh',
75
75
  templates: { completed: '', error: '', aborted: '', blocked: '', 'max-tokens': '' },
76
- titleTemplate: '',
76
+ titleTemplate: '', // 空 = 未自定义全局标题,各原因使用语言默认标题(如「任务已完成」「任务出错」)
77
+ /** 按原因定制推送标题(空 = 用 titleTemplate 或语言默认标题)。 */
78
+ titleTemplates: { completed: '', error: '', aborted: '', blocked: '', 'max-tokens': '' },
77
79
  includeDuration: true,
78
80
  includeUsage: true,
79
81
  skipSubagents: true,
@@ -89,10 +91,16 @@ function sanitizeSettings(raw) {
89
91
  for (const key of Object.keys(templates)) {
90
92
  if (typeof templatesRaw[key] === 'string') templates[key] = templatesRaw[key]
91
93
  }
94
+ const titlesRaw = src.titleTemplates && typeof src.titleTemplates === 'object' ? src.titleTemplates : {}
95
+ const titleTemplates = { ...DEFAULT_SETTINGS.titleTemplates }
96
+ for (const key of Object.keys(titleTemplates)) {
97
+ if (typeof titlesRaw[key] === 'string') titleTemplates[key] = titlesRaw[key]
98
+ }
92
99
  return {
93
100
  language: ['zh', 'zh-tw', 'en', 'ja', 'ko'].includes(src.language) ? src.language : 'zh',
94
101
  templates,
95
102
  titleTemplate: typeof src.titleTemplate === 'string' ? src.titleTemplate : '',
103
+ titleTemplates,
96
104
  includeDuration: typeof src.includeDuration === 'boolean' ? src.includeDuration : true,
97
105
  includeUsage: typeof src.includeUsage === 'boolean' ? src.includeUsage : true,
98
106
  skipSubagents: typeof src.skipSubagents === 'boolean' ? src.skipSubagents : true,
@@ -130,6 +138,13 @@ export function apply(ctx, config = {}) {
130
138
  'max-tokens': Schema.string().default(''),
131
139
  }),
132
140
  titleTemplate: Schema.string().default(''),
141
+ titleTemplates: Schema.object({
142
+ completed: Schema.string().default(''),
143
+ error: Schema.string().default(''),
144
+ aborted: Schema.string().default(''),
145
+ blocked: Schema.string().default(''),
146
+ 'max-tokens': Schema.string().default(''),
147
+ }),
133
148
  includeDuration: Schema.boolean().default(true),
134
149
  includeUsage: Schema.boolean().default(true),
135
150
  skipSubagents: Schema.boolean().default(true),
@@ -196,17 +211,23 @@ export function apply(ctx, config = {}) {
196
211
  try {
197
212
  rootRegistry.register({
198
213
  key: SETTINGS_NS,
199
- schema: z.string(),
200
- init: () => '',
214
+ // 投影值:{ kind: 结束原因, text: 系统消息全文, title: 该原因的推送标题(host 已渲染,含 {title} 替换)}
215
+ schema: z.object({ kind: z.string(), text: z.string(), title: z.string() }),
216
+ init: () => ({ kind: '', text: '', title: '' }),
201
217
  apply: (state, event) => {
202
218
  if (event.type !== 'user/message') return state
203
219
  const src = event.data?.source ?? {}
204
220
  if (src.kind !== 'plugin' || src.plugin !== PLUGIN_ID) return state
205
221
  const text = ((event.data?.content ?? []).map((b) => b?.text ?? '')).join('').trim()
206
- return text || state
222
+ if (!text) return state
223
+ return {
224
+ kind: typeof src.reasonKind === 'string' ? src.reasonKind : state.kind,
225
+ text,
226
+ title: typeof src.titleText === 'string' && src.titleText ? src.titleText : state.title,
227
+ }
207
228
  },
208
229
  view: (state) => state,
209
- stateVersion: 1,
230
+ stateVersion: 2, // 投影值从 string 升级为对象(kind/text/title)
210
231
  })
211
232
  fileLog('session-projections unit registered (key=session-complete-notify)')
212
233
  } catch (err) {
@@ -250,6 +271,12 @@ export function apply(ctx, config = {}) {
250
271
  } catch (projErr) {
251
272
  warn(ctx, `投影快照读取失败: ${projErr?.message ?? projErr}`)
252
273
  }
274
+ // 标题投影缺失时从会话自身兜底(header.title / title),确保默认文案能内嵌会话标题
275
+ if (!titleValue) {
276
+ const h = session?.header ?? {}
277
+ titleValue = (typeof h.title === 'string' && h.title.trim()) ? h.title
278
+ : (typeof session?.title === 'string' && session.title.trim()) ? session.title : ''
279
+ }
253
280
  const notice = buildNotice(kind, event.data.reason, {
254
281
  ...state,
255
282
  includeDuration: true, // 标签即开关:{duration} 插了才显示
@@ -258,6 +285,8 @@ export function apply(ctx, config = {}) {
258
285
  tpsValue,
259
286
  titleValue,
260
287
  }, { 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)
261
290
  // 边界约束:session/event 观察者回调运行在 turn/end 那次 append 的
262
291
  // 发布边界之内(dsh-session 在 dispatch 前 set entry.appending,
263
292
  // finally 中复位),此时同步 append 会被拒绝:
@@ -265,7 +294,7 @@ export function apply(ctx, config = {}) {
265
294
  // 推迟到微任务——微任务队列在本次同步栈(含 finally 复位)之后才跑。
266
295
  queueMicrotask(() => {
267
296
  if (disposed) return // 卸载窗口内已调度的微任务:fiber 已拆除,跳过追加
268
- appendNotice(ctx, session, notice)
297
+ appendNotice(ctx, session, notice, kind, title)
269
298
  })
270
299
  return
271
300
  }
@@ -290,7 +319,7 @@ export function apply(ctx, config = {}) {
290
319
  }
291
320
 
292
321
  /** 把系统消息追加进会话日志(失败只记日志,绝不抛出破坏 event 火线)。 */
293
- function appendNotice(ctx, session, notice) {
322
+ function appendNotice(ctx, session, notice, kind, title) {
294
323
  try {
295
324
  session.append(
296
325
  'user/message',
@@ -298,7 +327,14 @@ function appendNotice(ctx, session, notice) {
298
327
  id: newId(),
299
328
  role: 'user',
300
329
  content: [{ type: 'text', text: notice.text }],
301
- source: { kind: 'plugin', plugin: PLUGIN_ID, form: 'notice', summary: notice.summary },
330
+ source: {
331
+ kind: 'plugin',
332
+ plugin: PLUGIN_ID,
333
+ form: 'notice',
334
+ summary: notice.summary,
335
+ reasonKind: typeof kind === 'string' ? kind : '', // 投影 apply 取用
336
+ titleText: typeof title === 'string' ? title : '', // 投影 apply 取用(host 渲染好的推送标题)
337
+ },
302
338
  },
303
339
  { surfaceOp: 'append' },
304
340
  )
package/package.json CHANGED
@@ -1,70 +1,70 @@
1
- {
2
- "name": "@telosmaylx/dsh-session-notify",
3
- "version": "0.1.7",
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.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
+ }