@telosmaylx/dsh-session-notify 0.1.13 → 0.1.17
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 +78 -41
- package/README.ja.md +78 -41
- package/README.ko.md +78 -41
- package/README.md +78 -41
- package/README.zh-TW.md +78 -41
- package/lib/client.js +2636 -2451
- package/lib/core.js +44 -5
- package/lib/index.js +104 -10
- package/package.json +1 -1
package/lib/core.js
CHANGED
|
@@ -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 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': '任务达到上限', question: 'AI 正在向你提问' },
|
|
34
|
+
'zh-tw': { completed: '任務已完成', aborted: '任務已中止', blocked: '任務被阻塞', error: '任務出錯', 'max-tokens': '任務達到上限', question: 'AI 正在向你提問' },
|
|
35
|
+
en: { completed: 'Task completed', aborted: 'Task aborted', blocked: 'Task blocked', error: 'Task failed', 'max-tokens': 'Task hit the cap', question: 'AI is asking you a question' },
|
|
36
|
+
ja: { completed: 'タスク完了', aborted: 'タスク中止', blocked: 'タスクがブロックされました', error: 'タスクエラー', 'max-tokens': '上限に到達', question: 'AI からの質問が届きました' },
|
|
37
|
+
ko: { completed: '작업 완료', aborted: '작업 중단됨', blocked: '작업 차단됨', error: '작업 오류', 'max-tokens': '한도 도달', question: 'AI가 질문했습니다' },
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
/**
|
|
@@ -54,6 +54,7 @@ export function renderTitle(kind, opts = {}, titleValue = '') {
|
|
|
54
54
|
const tpl = per || global || fallback
|
|
55
55
|
// {image}/{icon} 是正文媒体的开关标签,标题里若误写则剥除;其余信息位({duration}{usage}{error}{cache}{tps})
|
|
56
56
|
// 用会话数据替换(无数据则替换为空串),避免字面量泄漏到推送标题
|
|
57
|
+
// {question} 仅在提问原因(kind=question)下替换为实际提问文本(opts.questionText),其他原因剥除
|
|
57
58
|
const out = String(tpl)
|
|
58
59
|
.replace(/\{image\}/g, '')
|
|
59
60
|
.replace(/\{icon\}/g, '')
|
|
@@ -64,6 +65,7 @@ export function renderTitle(kind, opts = {}, titleValue = '') {
|
|
|
64
65
|
.replaceAll('{error}', opts.error ?? '')
|
|
65
66
|
.replaceAll('{cache}', opts.cache ?? '')
|
|
66
67
|
.replaceAll('{tps}', opts.tps ?? '')
|
|
68
|
+
.replaceAll('{question}', kind === 'question' ? (opts.questionText ?? '') : '')
|
|
67
69
|
.trim()
|
|
68
70
|
return out || fallback
|
|
69
71
|
}
|
|
@@ -84,6 +86,7 @@ const DEFAULT_PHRASES = {
|
|
|
84
86
|
blocked: '会话{titleBlock}被阻塞,请点击查看。',
|
|
85
87
|
error: '会话{titleBlock}出错,请点击查看。',
|
|
86
88
|
'max-tokens': '会话{titleBlock}达到上限,请点击查看。',
|
|
89
|
+
question: 'AI 向你提问:{question}',
|
|
87
90
|
generic: '会话{titleBlock}已结束,请点击查看。',
|
|
88
91
|
},
|
|
89
92
|
'zh-tw': {
|
|
@@ -92,6 +95,7 @@ const DEFAULT_PHRASES = {
|
|
|
92
95
|
blocked: '會話{titleBlock}被阻塞,請點擊查看。',
|
|
93
96
|
error: '會話{titleBlock}出錯,請點擊查看。',
|
|
94
97
|
'max-tokens': '會話{titleBlock}達到上限,請點擊查看。',
|
|
98
|
+
question: 'AI 向你提問:{question}',
|
|
95
99
|
generic: '會話{titleBlock}已結束,請點擊查看。',
|
|
96
100
|
},
|
|
97
101
|
en: {
|
|
@@ -100,6 +104,7 @@ const DEFAULT_PHRASES = {
|
|
|
100
104
|
blocked: 'Session {titleBlock} blocked. Click to view.',
|
|
101
105
|
error: 'Session {titleBlock} failed. Click to view.',
|
|
102
106
|
'max-tokens': 'Session {titleBlock} hit the cap. Click to view.',
|
|
107
|
+
question: 'AI is asking: {question}',
|
|
103
108
|
generic: 'Session {titleBlock} ended. Click to view.',
|
|
104
109
|
},
|
|
105
110
|
ja: {
|
|
@@ -108,6 +113,7 @@ const DEFAULT_PHRASES = {
|
|
|
108
113
|
blocked: 'セッション{titleBlock}がブロックされました。クリックして表示。',
|
|
109
114
|
error: 'セッション{titleBlock}エラー。クリックして表示。',
|
|
110
115
|
'max-tokens': 'セッション{titleBlock}上限に到達。クリックして表示。',
|
|
116
|
+
question: 'AI からの質問:{question}',
|
|
111
117
|
generic: 'セッション{titleBlock}終了。クリックして表示。',
|
|
112
118
|
},
|
|
113
119
|
ko: {
|
|
@@ -116,6 +122,7 @@ const DEFAULT_PHRASES = {
|
|
|
116
122
|
blocked: '세션{titleBlock} 차단됨. 클릭하여 확인.',
|
|
117
123
|
error: '세션{titleBlock} 오류. 클릭하여 확인.',
|
|
118
124
|
'max-tokens': '세션{titleBlock} 한도 도달. 클릭하여 확인.',
|
|
125
|
+
question: 'AI가 질문합니다: {question}',
|
|
119
126
|
generic: '세션{titleBlock} 종료. 클릭하여 확인.',
|
|
120
127
|
},
|
|
121
128
|
}
|
|
@@ -302,6 +309,36 @@ export function tpsOf(usage, ms) {
|
|
|
302
309
|
return `${Math.round(output / (ms / 1000))} tok/s`
|
|
303
310
|
}
|
|
304
311
|
|
|
312
|
+
/**
|
|
313
|
+
* 渲染提问提醒的正文:templates.question(自定义模板)> 语言默认短语。
|
|
314
|
+
* 模板占位符:{question}(问题文本,必需)、{title}(会话标题,投影无上下文时为空)。
|
|
315
|
+
* 渲染结果为空时回退问题文本本身。
|
|
316
|
+
* @param {string} questionText
|
|
317
|
+
* @param {{ language?: string; templates?: Record<string, string> }} [opts]
|
|
318
|
+
* @param {string} [titleValue]
|
|
319
|
+
* @returns {string}
|
|
320
|
+
*/
|
|
321
|
+
export function buildQuestionBody(questionText, opts = {}, titleValue = '') {
|
|
322
|
+
const language = LABELS[opts.language] ? opts.language : 'zh'
|
|
323
|
+
const q = typeof questionText === 'string' ? questionText : ''
|
|
324
|
+
const tpl = (opts.templates && typeof opts.templates.question === 'string' && opts.templates.question.trim() !== '') ? opts.templates.question : ''
|
|
325
|
+
const defaultPhrase = (DEFAULT_PHRASES[language] && DEFAULT_PHRASES[language].question) || (DEFAULT_PHRASES[language] && DEFAULT_PHRASES[language].generic) || ''
|
|
326
|
+
const out = String(tpl || defaultPhrase)
|
|
327
|
+
.replaceAll('{question}', q)
|
|
328
|
+
.replaceAll('{title}', typeof titleValue === 'string' ? titleValue : '')
|
|
329
|
+
.replace(/\{label\}/g, '')
|
|
330
|
+
.replace(/\{image\}/g, '')
|
|
331
|
+
.replace(/\{icon\}/g, '')
|
|
332
|
+
// 提问通道只有提问文本与会话标题两个数据源:误插其他信息位一律替换为空串,避免字面量泄漏
|
|
333
|
+
.replaceAll('{duration}', '')
|
|
334
|
+
.replaceAll('{usage}', '')
|
|
335
|
+
.replaceAll('{error}', '')
|
|
336
|
+
.replaceAll('{cache}', '')
|
|
337
|
+
.replaceAll('{tps}', '')
|
|
338
|
+
.trim()
|
|
339
|
+
return out || q
|
|
340
|
+
}
|
|
341
|
+
|
|
305
342
|
/**
|
|
306
343
|
* 构建系统消息:可折叠行的 summary(= 正文渲染结果的截断,≤120 字符)
|
|
307
344
|
* + 展开/模型可见的全文。
|
|
@@ -393,6 +430,8 @@ export function renderTemplate(template, vars) {
|
|
|
393
430
|
.replace(/\{label\}/g, '')
|
|
394
431
|
.replace(/\{image\}/g, '')
|
|
395
432
|
.replace(/\{icon\}/g, '')
|
|
433
|
+
// {question} 仅提问通道(buildQuestionBody)有效:其他原因模板中误插一律剥除,防字面量泄漏
|
|
434
|
+
.replace(/\{question\}/g, '')
|
|
396
435
|
.replaceAll('{title}', vars.title ?? '')
|
|
397
436
|
.replaceAll('{duration}', vars.duration)
|
|
398
437
|
.replaceAll('{usage}', vars.usage)
|
package/lib/index.js
CHANGED
|
@@ -32,11 +32,11 @@
|
|
|
32
32
|
* includeUsage: true
|
|
33
33
|
*/
|
|
34
34
|
|
|
35
|
-
import { appendFileSync } from 'node:fs'
|
|
35
|
+
import { appendFileSync, statSync, truncateSync } 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, renderTitle } from './core.js?v=1' // v=1: 缓存破坏——HMR 重载按 URL 键控
|
|
39
|
+
import { createTurnTracker, buildNotice, buildQuestionBody, 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'
|
|
@@ -47,6 +47,9 @@ const PLUGIN_ID = 'dsh-session-notify'
|
|
|
47
47
|
/** 设置命名空间(官方「设置 → 插件」面板的键)。 */
|
|
48
48
|
const SETTINGS_NS = 'session-complete-notify'
|
|
49
49
|
|
|
50
|
+
/** 会话投影 key:正在等待用户回答的 ask_user_question 问题(客户端据此推送提问提醒)。 */
|
|
51
|
+
const QUESTION_PROJECTION_KEY = 'session-complete-notify-question'
|
|
52
|
+
|
|
50
53
|
/**
|
|
51
54
|
* 本插件从仓库目录以 realpath 加载,裸导入(@deepseek-ai/*)无法解析;
|
|
52
55
|
* 用 createRequire 锚定 profile 共享依赖枢纽(.dsh/profiles/node_modules),
|
|
@@ -72,10 +75,10 @@ const DEFAULT_OPTIONS = {
|
|
|
72
75
|
/** 设置面板可见字段的默认值(与 Config 同构;仓库里的 schema 默认值与此一致)。 */
|
|
73
76
|
const DEFAULT_SETTINGS = {
|
|
74
77
|
language: 'zh',
|
|
75
|
-
templates: { completed: '', error: '', aborted: '', blocked: '', 'max-tokens': '' },
|
|
78
|
+
templates: { completed: '', error: '', aborted: '', blocked: '', 'max-tokens': '', question: '' },
|
|
76
79
|
titleTemplate: '', // 空 = 未自定义全局标题,各原因使用语言默认标题(如「任务已完成」「任务出错」)
|
|
77
80
|
/** 按原因定制推送标题(空 = 用 titleTemplate 或语言默认标题)。 */
|
|
78
|
-
titleTemplates: { completed: '', error: '', aborted: '', blocked: '', 'max-tokens': '' },
|
|
81
|
+
titleTemplates: { completed: '', error: '', aborted: '', blocked: '', 'max-tokens': '', question: '' },
|
|
79
82
|
includeDuration: true,
|
|
80
83
|
includeUsage: true,
|
|
81
84
|
skipSubagents: true,
|
|
@@ -89,12 +92,12 @@ const DEFAULT_SETTINGS = {
|
|
|
89
92
|
imagePreviewUrl: '',
|
|
90
93
|
iconPreviewUrl: '',
|
|
91
94
|
/** 按原因的自定义通知大图({image} 标签开关对应的数据,本地文件压缩后的 data URI)。 */
|
|
92
|
-
images: { completed: '', error: '', aborted: '', blocked: '', 'max-tokens': '' },
|
|
95
|
+
images: { completed: '', error: '', aborted: '', blocked: '', 'max-tokens': '', question: '' },
|
|
93
96
|
/** 按原因的自定义通知图标({icon} 标签开关对应的数据,本地文件压缩后的 data URI,128×128 方形)。 */
|
|
94
|
-
icons: { completed: '', error: '', aborted: '', blocked: '', 'max-tokens': '' },
|
|
97
|
+
icons: { completed: '', error: '', aborted: '', blocked: '', 'max-tokens': '', question: '' },
|
|
95
98
|
/** 按原因的完整预览版(等比 1024 未裁切),与 images/icons 一一对应。 */
|
|
96
|
-
imagePreviews: { completed: '', error: '', aborted: '', blocked: '', 'max-tokens': '' },
|
|
97
|
-
iconPreviews: { completed: '', error: '', aborted: '', blocked: '', 'max-tokens': '' },
|
|
99
|
+
imagePreviews: { completed: '', error: '', aborted: '', blocked: '', 'max-tokens': '', question: '' },
|
|
100
|
+
iconPreviews: { completed: '', error: '', aborted: '', blocked: '', 'max-tokens': '', question: '' },
|
|
98
101
|
}
|
|
99
102
|
|
|
100
103
|
/** 从任意输入规整为设置形状(容忍缺失/多余字段)。 */
|
|
@@ -178,6 +181,7 @@ export function apply(ctx, config = {}) {
|
|
|
178
181
|
aborted: Schema.string().default(''),
|
|
179
182
|
blocked: Schema.string().default(''),
|
|
180
183
|
'max-tokens': Schema.string().default(''),
|
|
184
|
+
question: Schema.string().default(''),
|
|
181
185
|
}),
|
|
182
186
|
titleTemplate: Schema.string().default(''),
|
|
183
187
|
titleTemplates: Schema.object({
|
|
@@ -186,6 +190,7 @@ export function apply(ctx, config = {}) {
|
|
|
186
190
|
aborted: Schema.string().default(''),
|
|
187
191
|
blocked: Schema.string().default(''),
|
|
188
192
|
'max-tokens': Schema.string().default(''),
|
|
193
|
+
question: Schema.string().default(''),
|
|
189
194
|
}),
|
|
190
195
|
includeDuration: Schema.boolean().default(true),
|
|
191
196
|
includeUsage: Schema.boolean().default(true),
|
|
@@ -201,6 +206,7 @@ export function apply(ctx, config = {}) {
|
|
|
201
206
|
aborted: Schema.string().default(''),
|
|
202
207
|
blocked: Schema.string().default(''),
|
|
203
208
|
'max-tokens': Schema.string().default(''),
|
|
209
|
+
question: Schema.string().default(''),
|
|
204
210
|
}),
|
|
205
211
|
icons: Schema.object({
|
|
206
212
|
completed: Schema.string().default(''),
|
|
@@ -208,6 +214,7 @@ export function apply(ctx, config = {}) {
|
|
|
208
214
|
aborted: Schema.string().default(''),
|
|
209
215
|
blocked: Schema.string().default(''),
|
|
210
216
|
'max-tokens': Schema.string().default(''),
|
|
217
|
+
question: Schema.string().default(''),
|
|
211
218
|
}),
|
|
212
219
|
imagePreviews: Schema.object({
|
|
213
220
|
completed: Schema.string().default(''),
|
|
@@ -215,6 +222,7 @@ export function apply(ctx, config = {}) {
|
|
|
215
222
|
aborted: Schema.string().default(''),
|
|
216
223
|
blocked: Schema.string().default(''),
|
|
217
224
|
'max-tokens': Schema.string().default(''),
|
|
225
|
+
question: Schema.string().default(''),
|
|
218
226
|
}),
|
|
219
227
|
iconPreviews: Schema.object({
|
|
220
228
|
completed: Schema.string().default(''),
|
|
@@ -222,6 +230,7 @@ export function apply(ctx, config = {}) {
|
|
|
222
230
|
aborted: Schema.string().default(''),
|
|
223
231
|
blocked: Schema.string().default(''),
|
|
224
232
|
'max-tokens': Schema.string().default(''),
|
|
233
|
+
question: Schema.string().default(''),
|
|
225
234
|
}),
|
|
226
235
|
})
|
|
227
236
|
let attempts = 0
|
|
@@ -307,6 +316,43 @@ export function apply(ctx, config = {}) {
|
|
|
307
316
|
} catch (err) {
|
|
308
317
|
warn(ctx, `投影单元注册失败: ${err?.message ?? err}`)
|
|
309
318
|
}
|
|
319
|
+
|
|
320
|
+
// 提问投影:ask_user_question 发起时让客户端立即推送,避免用户在其他页面错过提问。
|
|
321
|
+
// 状态是瞬时的:tool/result 或 turn/end 后清空,不写入会话内系统消息。
|
|
322
|
+
// title/body 由宿主按 5 语言默认文案/用户模板渲染后写入投影,客户端零文案逻辑。
|
|
323
|
+
try {
|
|
324
|
+
rootRegistry.register({
|
|
325
|
+
key: QUESTION_PROJECTION_KEY,
|
|
326
|
+
schema: z.object({ id: z.string(), text: z.string(), title: z.string(), body: z.string() }),
|
|
327
|
+
init: () => ({ id: '', text: '', title: '', body: '' }),
|
|
328
|
+
apply: (state, event) => {
|
|
329
|
+
if (event.type === 'tool/call' && event.data?.name === 'ask_user_question') {
|
|
330
|
+
const id = typeof event.data.callId === 'string' ? event.data.callId : ''
|
|
331
|
+
const text = extractQuestionText(event.data.arguments)
|
|
332
|
+
if (!id || !text) return state
|
|
333
|
+
return {
|
|
334
|
+
id,
|
|
335
|
+
text,
|
|
336
|
+
title: renderTitle('question', { language: settings.language, titleTemplate: settings.titleTemplate, titleTemplates: settings.titleTemplates, questionText: text }),
|
|
337
|
+
body: buildQuestionBody(text, { language: settings.language, templates: settings.templates }),
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (event.type === 'tool/result') {
|
|
341
|
+
if (!state.id) return state
|
|
342
|
+
return matchesQuestionResult(state.id, event.data) ? { id: '', text: '', title: '', body: '' } : state
|
|
343
|
+
}
|
|
344
|
+
if (event.type === 'turn/end' && state.id) {
|
|
345
|
+
return { id: '', text: '', title: '', body: '' }
|
|
346
|
+
}
|
|
347
|
+
return state
|
|
348
|
+
},
|
|
349
|
+
view: (state) => state,
|
|
350
|
+
stateVersion: 2, // 投影值升级:新增 body(宿主渲染好的提问正文)
|
|
351
|
+
})
|
|
352
|
+
fileLog('session-projections unit registered (key=session-complete-notify-question)')
|
|
353
|
+
} catch (err) {
|
|
354
|
+
warn(ctx, `提问投影单元注册失败: ${err?.message ?? err}`)
|
|
355
|
+
}
|
|
310
356
|
})
|
|
311
357
|
}
|
|
312
358
|
} catch (err) {
|
|
@@ -326,7 +372,7 @@ export function apply(ctx, config = {}) {
|
|
|
326
372
|
const state = tracker.end(key, event.time)
|
|
327
373
|
const kind = event.data.reason?.kind
|
|
328
374
|
if (!reasons.has(kind)) return
|
|
329
|
-
if (isSubagentSession(session)) return //
|
|
375
|
+
if (settings.skipSubagents !== false && isSubagentSession(session)) return // 跳过子代理会话(面板复选框可关闭)
|
|
330
376
|
// 官方投影口径:tokenUsage(缓存命中率)+ sessionStats(tok/s 生成速度)
|
|
331
377
|
// 与 dsh-web-ui 状态栏同源;投影由宿主维护,无插件内存状态,重载也不丢。
|
|
332
378
|
// 用户以「标签是否插入」控制显示(用量/用时无独立开关)。
|
|
@@ -402,6 +448,48 @@ export function apply(ctx, config = {}) {
|
|
|
402
448
|
}
|
|
403
449
|
}
|
|
404
450
|
|
|
451
|
+
/**
|
|
452
|
+
* 从 ask_user_question 的 arguments 中提取人类可读的问题文本。
|
|
453
|
+
* @param {unknown} raw - tool/call 的 arguments(通常是 JSON 字符串)。
|
|
454
|
+
* @returns {string}
|
|
455
|
+
*/
|
|
456
|
+
function extractQuestionText(raw) {
|
|
457
|
+
try {
|
|
458
|
+
const args = typeof raw === 'string' ? JSON.parse(raw) : raw
|
|
459
|
+
const questions = Array.isArray(args?.questions) ? args.questions : []
|
|
460
|
+
return questions
|
|
461
|
+
.map((q) => (q && typeof q.question === 'string' ? q.question : ''))
|
|
462
|
+
.filter(Boolean)
|
|
463
|
+
.join('\n')
|
|
464
|
+
.trim()
|
|
465
|
+
} catch {
|
|
466
|
+
return ''
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* 判断 tool/result 是否对应某个 ask_user_question 调用。
|
|
472
|
+
* tool/result 的 data.message.source.callId 与 content[].toolCallId 都携带调用 id。
|
|
473
|
+
* @param {string} id - 待匹配的 ask_user_question callId。
|
|
474
|
+
* @param {unknown} data - tool/result 的 event.data。
|
|
475
|
+
* @returns {boolean}
|
|
476
|
+
*/
|
|
477
|
+
function matchesQuestionResult(id, data) {
|
|
478
|
+
if (!id || !data || typeof data !== 'object') return false
|
|
479
|
+
const candidates = []
|
|
480
|
+
const source = data.message?.source
|
|
481
|
+
if (source && typeof source.callId === 'string') candidates.push(source.callId)
|
|
482
|
+
if (typeof data.callId === 'string') candidates.push(data.callId)
|
|
483
|
+
const blocks = data.message?.content
|
|
484
|
+
if (Array.isArray(blocks)) {
|
|
485
|
+
for (const block of blocks) {
|
|
486
|
+
if (block && typeof block.toolCallId === 'string') candidates.push(block.toolCallId)
|
|
487
|
+
if (block && typeof block.tool_call_id === 'string') candidates.push(block.tool_call_id)
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
return candidates.includes(id)
|
|
491
|
+
}
|
|
492
|
+
|
|
405
493
|
/** 把系统消息追加进会话日志(失败只记日志,绝不抛出破坏 event 火线)。 */
|
|
406
494
|
function appendNotice(ctx, session, notice, kind, title) {
|
|
407
495
|
try {
|
|
@@ -429,9 +517,15 @@ function appendNotice(ctx, session, notice, kind, title) {
|
|
|
429
517
|
}
|
|
430
518
|
|
|
431
519
|
/** 追加失败时落一个调试文件(~/.dsh/session-complete-notify.log),便于排查。 */
|
|
520
|
+
const LOG_MAX_BYTES = 512 * 1024 // P2-8:超过 512KB 截断,避免长期运行无限增长
|
|
432
521
|
function fileLog(line) {
|
|
433
522
|
try {
|
|
434
|
-
|
|
523
|
+
const file = join(homedir(), '.dsh', 'session-complete-notify.log')
|
|
524
|
+
try {
|
|
525
|
+
const st = statSync(file)
|
|
526
|
+
if (st.size > LOG_MAX_BYTES) truncateSync(file, 0)
|
|
527
|
+
} catch { /* 文件不存在等,忽略 */ }
|
|
528
|
+
appendFileSync(file, `${new Date().toISOString()} ${line}\n`, 'utf8')
|
|
435
529
|
} catch {
|
|
436
530
|
/* 尽力而为 */
|
|
437
531
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telosmaylx/dsh-session-notify",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.17",
|
|
4
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
5
|
"private": false,
|
|
6
6
|
"type": "module",
|