@freely01/opencode-notify 0.4.0 → 0.4.1

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/cli.ts CHANGED
@@ -105,7 +105,7 @@ function cmdCheck() {
105
105
  let hasError = false
106
106
 
107
107
  for (const [name, ch] of Object.entries(channels)) {
108
- if (!ch || !ch.enabled) continue
108
+ if (!ch || !ch.mode || ch.mode === "none") continue
109
109
 
110
110
  switch (name) {
111
111
  case "system":
@@ -177,23 +177,25 @@ async function cmdTest(channel?: string) {
177
177
  allChannels.push([name, enabled, sender])
178
178
  }
179
179
 
180
- add("system_message", !!(channels.system_message?.enabled), async () => {
180
+ const modeOk = (m: string | undefined): boolean => m !== undefined && m !== "none"
181
+
182
+ add("system_message", modeOk(channels.system_message?.mode), async () => {
181
183
  await new SystemSender().send(SAMPLE_MSG)
182
184
  })
183
185
 
184
- if (channels.custom_webhook?.enabled && channels.custom_webhook.url) {
186
+ if (modeOk(channels.custom_webhook?.mode) && channels.custom_webhook?.url) {
185
187
  add("custom_webhook", true, async () => {
186
188
  await new CustomWebhookSender(channels.custom_webhook!).send(SAMPLE_MSG)
187
189
  })
188
190
  }
189
191
 
190
- if (channels.wechat_work?.enabled && channels.wechat_work.webhook_url) {
192
+ if (modeOk(channels.wechat_work?.mode) && channels.wechat_work?.webhook_url) {
191
193
  add("wechat_work", true, async () => {
192
194
  await new WechatWorkSender(channels.wechat_work!).send(SAMPLE_MSG)
193
195
  })
194
196
  }
195
197
 
196
- if (channels.feishu?.enabled && channels.feishu.webhook_url) {
198
+ if (modeOk(channels.feishu?.mode) && channels.feishu?.webhook_url) {
197
199
  add("feishu", true, async () => {
198
200
  await new FeishuSender(channels.feishu!).send(SAMPLE_MSG)
199
201
  })
@@ -303,13 +305,13 @@ function cmdInfo() {
303
305
  console.log(`\n 🔔 通知渠道:`)
304
306
  const channelNames: [string, any, string][] = [
305
307
  ["系统消息", ch.system_message, ""],
306
- ["屏幕跑马灯", ch.screen_flash, ch.screen_flash?.enabled ? `强度${ch.screen_flash.intensity ?? 0.9}` : ""],
308
+ ["屏幕跑马灯", ch.screen_flash, ch.screen_flash?.mode !== "none" ? `强度${ch.screen_flash?.intensity ?? 0.9}` : ""],
307
309
  ["自定义 Webhook", ch.custom_webhook, ch.custom_webhook?.url ?? ""],
308
310
  ["企业微信", ch.wechat_work, ch.wechat_work?.webhook_url ? `${ch.wechat_work.webhook_url.slice(0, 40)}...` : ""],
309
311
  ["飞书", ch.feishu, ch.feishu?.webhook_url ? `${ch.feishu.webhook_url.slice(0, 40)}...` : ""],
310
312
  ]
311
313
  for (const [label, config, url] of channelNames) {
312
- if (config?.enabled) {
314
+ if (config?.mode !== "none") {
313
315
  const urlInfo = url ? ` ${url}` : ""
314
316
  console.log(` ✅ ${label}${urlInfo}`)
315
317
  } else {
package/config.ts CHANGED
@@ -1,6 +1,14 @@
1
+ /**
2
+ * 渠道模式
3
+ * - "all" → 即时通知 + 延迟推送都启用
4
+ * - "delay_only" → 仅用于延迟推送(不弹即时通知)
5
+ * - "none" → 禁用
6
+ */
7
+ export type ChannelMode = "all" | "delay_only" | "none"
8
+
1
9
  /** 通知渠道配置 */
2
10
  export interface ChannelConfig {
3
- enabled: boolean
11
+ mode: ChannelMode
4
12
  /**
5
13
  * 渠道级事件过滤 — 仅这些事件触发本渠道通知
6
14
  * 不填或留空则继承全局 events
@@ -171,9 +179,17 @@ channels:
171
179
  # Linux - 使用 notify-send (需 libnotify 包,桌面版通常预装)
172
180
  # Windows - 使用 PowerShell New-BurntToastNotification
173
181
  # (需额外安装 BurntToast 模块)
182
+ #
183
+ # mode 可选值:
184
+ # all → 启用即时通知(是否延迟推送由下方 remote_delay_channels 独立控制)
185
+ # delay_only → 仅用于远程延迟推送,不弹即时通知
186
+ # none → 禁用
187
+ #
188
+ # 纯即时通知(不延迟推送):mode: all 且不要加入 remote_delay_channels 即可。
189
+ # 纯延迟推送(不即时通知):mode: delay_only 并加入 remote_delay_channels。
174
190
  # ---------------------------------------------------------------------------
175
191
  system_message:
176
- enabled: true # true=启用, false=禁用
192
+ mode: all # all | delay_only | none
177
193
 
178
194
  # ---------------------------------------------------------------------------
179
195
  # 屏幕跑马灯 (Linux X11 专用)
@@ -184,7 +200,7 @@ channels:
184
200
  # 取消下方注释启用:
185
201
  # ---------------------------------------------------------------------------
186
202
  # screen_flash:
187
- # enabled: true
203
+ # mode: all # all | delay_only | none
188
204
  # duration: 3.5 # 持续秒数
189
205
  # speed: 5.0 # 移动速度因子
190
206
  # intensity: 0.85 # 不透明度 0.0~1.0
@@ -198,7 +214,7 @@ channels:
198
214
  # 取消下方注释并填入 webhook_url 启用:
199
215
  # ---------------------------------------------------------------------------
200
216
  # wechat_work:
201
- # enabled: true
217
+ # mode: all # all | delay_only | none
202
218
  # webhook_url: "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx"
203
219
 
204
220
  # ---------------------------------------------------------------------------
@@ -210,7 +226,7 @@ channels:
210
226
  # 取消下方注释并填入 webhook_url 启用:
211
227
  # ---------------------------------------------------------------------------
212
228
  # feishu:
213
- # enabled: true
229
+ # mode: all # all | delay_only | none
214
230
  # webhook_url: "https://open.feishu.cn/open-apis/bot/v2/hook/xxx"
215
231
 
216
232
  # ---------------------------------------------------------------------------
@@ -231,7 +247,7 @@ channels:
231
247
  # 取消下方注释并配置 url 启用:
232
248
  # ---------------------------------------------------------------------------
233
249
  # custom_webhook:
234
- # enabled: true
250
+ # mode: all # all | delay_only | none
235
251
  # url: ""
236
252
  # method: "POST" # 请求方法: "POST" | "GET"
237
253
  # headers: {} # 自定义请求头
@@ -305,6 +321,12 @@ session_stale_timeout_ms: 600000 # 超时会话自动淘汰(毫秒)
305
321
  # 正常通知发出后,如果用户长时间未操作,针对指定渠道额外再推送一次。
306
322
  # 用户回到 opencode TUI 操作 → 自动取消所有待发延迟通知。
307
323
  #
324
+ # 只影响此列表中的渠道,不在列表中的渠道为"纯即时通知"。
325
+ # 结合 mode 使用:
326
+ # mode: all + 在此列表中 → 即时 + 延迟
327
+ # mode: all + 不在列表中 → 纯即时(不延迟推送)
328
+ # mode: delay_only + 在此列表中 → 纯延迟(不弹即时通知)
329
+ #
308
330
  # 适用场景:用户离开电脑后,系统通知可能一闪而过没看到,
309
331
  # 延迟推送在用户仍未回来时再次尝试发出。
310
332
  #
@@ -383,11 +405,11 @@ export function mergeConfig(base: PluginConfig, overrides: PluginConfig): Plugin
383
405
  /** 默认配置 */
384
406
  const DEFAULT_CONFIG: Required<Pick<PluginConfig, "suppress_when_active" | "activity_timeout_ms" | "suppress_events_when_active" | "session_stale_timeout_ms" | "remote_delay_seconds" | "remote_delay_max_count">> & PluginConfig = {
385
407
  channels: {
386
- system_message: { enabled: true },
387
- screen_flash: { enabled: false },
388
- wechat_work: { enabled: false },
389
- feishu: { enabled: false },
390
- custom_webhook: { enabled: false },
408
+ system_message: { mode: "all" },
409
+ screen_flash: { mode: "none" },
410
+ wechat_work: { mode: "none" },
411
+ feishu: { mode: "none" },
412
+ custom_webhook: { mode: "none" },
391
413
  },
392
414
  events: [
393
415
  "permission_required",
@@ -421,39 +443,39 @@ export function resolveConfig(options: PluginConfig): PluginConfig {
421
443
  return {
422
444
  channels: {
423
445
  system_message: {
424
- enabled:
425
- options.channels?.system_message?.enabled ??
426
- DEFAULT_CONFIG.channels!.system_message!.enabled,
446
+ mode:
447
+ options.channels?.system_message?.mode ??
448
+ DEFAULT_CONFIG.channels!.system_message!.mode,
427
449
  events: chEvents(options.channels?.system_message),
428
450
  },
429
451
  screen_flash: {
430
- enabled:
431
- options.channels?.screen_flash?.enabled ??
432
- DEFAULT_CONFIG.channels!.screen_flash!.enabled,
452
+ mode:
453
+ options.channels?.screen_flash?.mode ??
454
+ DEFAULT_CONFIG.channels!.screen_flash!.mode,
433
455
  duration: options.channels?.screen_flash?.duration,
434
456
  speed: options.channels?.screen_flash?.speed,
435
457
  intensity: options.channels?.screen_flash?.intensity,
436
458
  events: chEvents(options.channels?.screen_flash),
437
459
  },
438
460
  wechat_work: {
439
- enabled:
440
- options.channels?.wechat_work?.enabled ??
441
- DEFAULT_CONFIG.channels!.wechat_work!.enabled,
461
+ mode:
462
+ options.channels?.wechat_work?.mode ??
463
+ DEFAULT_CONFIG.channels!.wechat_work!.mode,
442
464
  webhook_url: options.channels?.wechat_work?.webhook_url || undefined,
443
465
  events: chEvents(options.channels?.wechat_work),
444
466
  },
445
467
  feishu: {
446
- enabled:
447
- options.channels?.feishu?.enabled ??
448
- DEFAULT_CONFIG.channels!.feishu!.enabled,
468
+ mode:
469
+ options.channels?.feishu?.mode ??
470
+ DEFAULT_CONFIG.channels!.feishu!.mode,
449
471
  webhook_url: options.channels?.feishu?.webhook_url || undefined,
450
472
  events: chEvents(options.channels?.feishu),
451
473
  },
452
474
  custom_webhook: {
453
- enabled:
454
- options.channels?.custom_webhook?.enabled ??
455
- DEFAULT_CONFIG.channels?.custom_webhook?.enabled ??
456
- false,
475
+ mode:
476
+ options.channels?.custom_webhook?.mode ??
477
+ DEFAULT_CONFIG.channels?.custom_webhook?.mode ??
478
+ "none",
457
479
  url: options.channels?.custom_webhook?.url || undefined,
458
480
  method: options.channels?.custom_webhook?.method ?? "POST",
459
481
  headers: options.channels?.custom_webhook?.headers,
@@ -96,6 +96,26 @@ export class DelayedDispatcher {
96
96
  }
97
97
  }
98
98
 
99
+ /**
100
+ * 在通知正文追加或替换延迟推送标记
101
+ *
102
+ * 清除正文末尾已有的旧标记行,追加最新标记。
103
+ * 标记格式:
104
+ * ─────────────────
105
+ * ⚠️ 延迟推送 第2/3次(下次约 15:31:00)
106
+ */
107
+ private markDelayBody(body: string, current: number, total: number, delayMs: number): string {
108
+ // 移除旧标记(从末尾 ─── 分隔线到最后)
109
+ const clean = body.replace(/\n─{3,}[\s\S]*$/, "")
110
+ if (current < total) {
111
+ const next = new Date(Date.now() + delayMs)
112
+ const t = `${String(next.getHours()).padStart(2, "0")}:${String(next.getMinutes()).padStart(2, "0")}:${String(next.getSeconds()).padStart(2, "0")}`
113
+ return `${clean}\n─────────────────\n⚠️ 延迟推送 第${current}/${total}次(下次约 ${t})`
114
+ }
115
+ // 最后一次推送,不显示下次时间
116
+ return `${clean}\n─────────────────\n⚠️ 延迟推送 第${current}/${total}次(最终)`
117
+ }
118
+
99
119
  /**
100
120
  * 调度单次延迟发送
101
121
  */
@@ -108,6 +128,10 @@ export class DelayedDispatcher {
108
128
  entry.timeoutId = setTimeout(() => {
109
129
  entry.timeoutId = null
110
130
 
131
+ // 在正文追加延迟标记(第几次 / 共几次 / 间隔秒数)
132
+ const sendCount = entry.count + 1 // 1-based
133
+ msg.body = this.markDelayBody(msg.body, sendCount, this.maxCount, this.delayMs)
134
+
111
135
  // 发送延迟通知
112
136
  const sender = this.senders.get(ch)
113
137
  if (sender) {
package/index.ts CHANGED
@@ -2,6 +2,7 @@ import type { Plugin } from "@opencode-ai/plugin"
2
2
  import type { PluginConfig } from "./config.js"
3
3
  import { resolveConfig, loadYamlConfig, mergeConfig, ensureConfigFile } from "./config.js"
4
4
  import { route } from "./events.js"
5
+ import { enrich } from "./message.js"
5
6
  import { Dispatcher } from "./dispatcher.js"
6
7
  import { FileStore } from "./store.js"
7
8
  import { SystemSender } from "./senders/system/index.js"
@@ -85,8 +86,9 @@ const plugin: Plugin = async (_input, options) => {
85
86
 
86
87
  // 会话生命周期事件
87
88
  if (type === "session.created") {
88
- tracker.register(sessionID)
89
- debug(`→ 会话已创建, 会话=${sessionID}`)
89
+ const title = properties.info?.title
90
+ tracker.register(sessionID, title)
91
+ debug(`→ 会话已创建, 会话=${sessionID}${title ? ` title="${title}"` : ""}`)
90
92
  }
91
93
  if (type === "session.deleted") {
92
94
  tracker.remove(sessionID)
@@ -102,7 +104,11 @@ const plugin: Plugin = async (_input, options) => {
102
104
  const msg = route(event, cfg.events)
103
105
  if (!msg) return // 不关心的事件
104
106
 
105
- debug(`→ 匹配通知: ${msg.event}`)
107
+ // 注入会话标题(任务描述),增强通知内容
108
+ const sessionTitle = tracker.getSessionTitle(sessionID)
109
+ enrich(msg, sessionTitle)
110
+
111
+ debug(`→ 匹配通知: ${msg.event}${sessionTitle ? ` sessionTitle="${sessionTitle}"` : ""}`)
106
112
 
107
113
  // 会话感知抑制判定
108
114
  let shouldSuppress = cfg.suppress_when_active && suppressEvents.includes(msg.event)
@@ -121,7 +127,10 @@ const plugin: Plugin = async (_input, options) => {
121
127
  }
122
128
 
123
129
  if (shouldSuppress) {
124
- info(`→ 会话 ${sessionID} 活跃中,跳过通知 (${msg.event})`)
130
+ info(`→ 会话 ${sessionID} 活跃中,跳过即时通知 (${msg.event})`)
131
+ // 仍调度延迟推送:用户可能在电脑前屏上可见所以抑制,
132
+ // 但万一用户已离开电脑,延迟推送能在用户未回来时再次提醒
133
+ delayedDispatcher?.schedule(msg)
125
134
  return
126
135
  }
127
136
 
@@ -140,50 +149,57 @@ interface BuildSendersResult {
140
149
  senderMap: Map<string, import("./senders/types.js").Sender>
141
150
  }
142
151
 
143
- function addSender(
144
- senders: import("./senders/types.js").Sender[],
145
- sender: import("./senders/types.js").Sender,
146
- events: string[],
147
- label: string,
148
- extra?: string,
149
- ): import("./senders/types.js").Sender {
150
- const filtered = new FilteredSender(sender, events)
151
- senders.push(filtered)
152
- const evt = events.length < 6 ? `events=${JSON.stringify(events)}` : `events=${events.length}个`
153
- info(`${label}已启用 (${evt})${extra ? `, ${extra}` : ""}`)
154
- return filtered
155
- }
156
-
157
152
  function buildSenders(cfg: PluginConfig): BuildSendersResult {
158
153
  const senders: import("./senders/types.js").Sender[] = []
159
154
  const senderMap = new Map<string, import("./senders/types.js").Sender>()
160
155
  const globalEvents = cfg.events ?? []
161
156
 
162
- if (cfg.channels?.system_message?.enabled) {
163
- const events = cfg.channels.system_message.events ?? globalEvents
164
- const s = addSender(senders, new SystemSender(), events, "系统通知")
165
- senderMap.set("system_message", s)
166
- }
167
- if (cfg.channels?.screen_flash?.enabled) {
168
- const events = cfg.channels.screen_flash.events ?? globalEvents
169
- const s = addSender(senders, new ScreenFlashSender(cfg.channels.screen_flash), events, "屏幕跑马灯")
170
- senderMap.set("screen_flash", s)
171
- }
172
- if (cfg.channels?.custom_webhook?.enabled && cfg.channels.custom_webhook.url) {
173
- const events = cfg.channels.custom_webhook.events ?? globalEvents
174
- const s = addSender(senders, new CustomWebhookSender(cfg.channels.custom_webhook), events, "自定义 Webhook")
175
- senderMap.set("custom_webhook", s)
176
- }
177
- if (cfg.channels?.wechat_work?.enabled && cfg.channels.wechat_work.webhook_url) {
178
- const events = cfg.channels.wechat_work.events ?? globalEvents
179
- const s = addSender(senders, new WechatWorkSender(cfg.channels.wechat_work), events, "企业微信")
180
- senderMap.set("wechat_work", s)
181
- }
182
- if (cfg.channels?.feishu?.enabled && cfg.channels.feishu.webhook_url) {
183
- const events = cfg.channels.feishu.events ?? globalEvents
184
- const s = addSender(senders, new FeishuSender(cfg.channels.feishu), events, "飞书")
185
- senderMap.set("feishu", s)
157
+ /**
158
+ * 注册渠道发送器
159
+ * @param key 渠道键名
160
+ * @param mode 渠道模式
161
+ * @param chEvents 渠道级事件过滤
162
+ * @param create 创建原始 Sender 的回调
163
+ * @param label 日志标签
164
+ */
165
+ function register(
166
+ key: string,
167
+ mode: string | undefined,
168
+ chEvents: string[] | undefined,
169
+ create: () => import("./senders/types.js").Sender,
170
+ label: string,
171
+ ): void {
172
+ if (mode === "none" || !mode) return // 禁用
173
+
174
+ const evts = chEvents ?? globalEvents
175
+ const raw = create()
176
+ const filtered = new FilteredSender(raw, evts)
177
+
178
+ if (mode === "delay_only") {
179
+ // 仅延迟推送:不进 senders[],只入 senderMap
180
+ senderMap.set(key, filtered)
181
+ info(`${label}已启用 (delay_only, 仅延迟推送)`)
182
+ } else {
183
+ // all:即时通知 + 延迟推送
184
+ senders.push(filtered)
185
+ senderMap.set(key, filtered)
186
+ const evtStr = evts.length < 6 ? `events=${JSON.stringify(evts)}` : `events=${evts.length}个`
187
+ info(`${label}已启用 (${evtStr})`)
188
+ }
186
189
  }
190
+
191
+ const ch = cfg.channels
192
+ register("system_message", ch?.system_message?.mode, ch?.system_message?.events,
193
+ () => new SystemSender(), "系统通知")
194
+ register("screen_flash", ch?.screen_flash?.mode, ch?.screen_flash?.events,
195
+ () => new ScreenFlashSender(ch?.screen_flash!), "屏幕跑马灯")
196
+ register("custom_webhook", ch?.custom_webhook?.mode, ch?.custom_webhook?.events,
197
+ () => new CustomWebhookSender(ch?.custom_webhook!), "自定义 Webhook")
198
+ register("wechat_work", ch?.wechat_work?.mode, ch?.wechat_work?.events,
199
+ () => new WechatWorkSender(ch?.wechat_work!), "企业微信")
200
+ register("feishu", ch?.feishu?.mode, ch?.feishu?.events,
201
+ () => new FeishuSender(ch?.feishu!), "飞书")
202
+
187
203
  return { senders, senderMap }
188
204
  }
189
205
 
package/message.ts CHANGED
@@ -28,9 +28,7 @@ const EVENT_LABELS: Record<string, string> = {
28
28
  */
29
29
  function shortSession(sessionID: string): string {
30
30
  if (!sessionID || sessionID === "unknown") return "未知"
31
- return sessionID.length > 11
32
- ? sessionID.slice(0, 11) + "…"
33
- : sessionID
31
+ return sessionID
34
32
  }
35
33
 
36
34
  /**
@@ -38,12 +36,9 @@ function shortSession(sessionID: string): string {
38
36
  * @param event 事件类型
39
37
  * @param sessionID 会话 ID(可选,传入后在标题前加会话标签)
40
38
  */
41
- export function formatTitle(event: string, sessionID?: string): string {
39
+ export function formatTitle(event: string, _sessionID?: string): string {
42
40
  const label = EVENT_LABELS[event] ?? event
43
- const prefix = sessionID && sessionID !== "unknown"
44
- ? `[${shortSession(sessionID)}] `
45
- : ""
46
- return `${prefix}opencode - ${label}`
41
+ return `opencode - ${label}`
47
42
  }
48
43
 
49
44
  /**
@@ -85,3 +80,29 @@ export function formatBody(msg: Message): string {
85
80
  `时间:${time}`,
86
81
  ].join("\n")
87
82
  }
83
+
84
+ /** 截断标题到指定长度 */
85
+ function shortTitle(title: string, maxLen = 20): string {
86
+ if (title.length <= maxLen) return title
87
+ return title.slice(0, maxLen - 1) + "…"
88
+ }
89
+
90
+ /**
91
+ * 增强通知消息:注入会话标题
92
+ *
93
+ * - 标题:`[短标题] 事件标签`(替换 `opencode - 事件标签`)
94
+ * - 正文:追加一行 `任务:完整标题`
95
+ *
96
+ * @param msg 原始通知消息
97
+ * @param sessionTitle 会话标题(用户输入的问题/任务描述),为空则不增强
98
+ * @returns 增强后的消息(原地修改并返回)
99
+ */
100
+ export function enrich(msg: Message, sessionTitle?: string): Message {
101
+ if (!sessionTitle) return msg
102
+
103
+ const label = EVENT_LABELS[msg.event] ?? msg.event
104
+ msg.title = `[${shortTitle(sessionTitle)}] ${label}`
105
+ // 用"任务"行替换"详情"行(详情的事件细节在会话标题下显得冗余)
106
+ msg.body = msg.body.replace(/^详情:.*$/m, `任务:${sessionTitle}`)
107
+ return msg
108
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@freely01/opencode-notify",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "opencode 通知插件 - 监听会话事件并通过多渠道推送通知",
5
5
  "type": "module",
6
6
  "main": "index.ts",
package/senders/feishu.ts CHANGED
@@ -47,18 +47,6 @@ export class FeishuSender implements Sender {
47
47
  tag: "markdown",
48
48
  content: msg.body,
49
49
  },
50
- {
51
- tag: "hr",
52
- },
53
- {
54
- tag: "note",
55
- elements: [
56
- {
57
- tag: "plain_text",
58
- content: `会话: ${msg.sessionID}`,
59
- },
60
- ],
61
- },
62
50
  ],
63
51
  },
64
52
  })
@@ -33,12 +33,11 @@ export class WechatWorkSender implements Sender {
33
33
  throw new Error("wechat_work: webhook_url not configured")
34
34
  }
35
35
 
36
- // 构造 markdown 内容
36
+ // 构造 markdown 内容(msg.body 已包含事件/会话/详情/时间/延迟标记)
37
37
  const content = [
38
38
  `**${msg.title}**`,
39
39
  "",
40
40
  msg.body,
41
- `> 会话: ${msg.sessionID}`,
42
41
  ].join("\n")
43
42
 
44
43
  const body = JSON.stringify({
@@ -20,6 +20,8 @@ export interface SessionInfo {
20
20
  lastActivity: number
21
21
  /** 会话创建时间 */
22
22
  createdAt: number
23
+ /** 会话标题(用户输入的问题/任务描述) */
24
+ title?: string
23
25
  }
24
26
 
25
27
  const CLEANUP_INTERVAL_MS = 5 * 60 * 1000 // 5 分钟
@@ -69,17 +71,26 @@ export class SessionTracker {
69
71
  }
70
72
 
71
73
  /** 注册新会话 */
72
- register(sessionID: string): void {
74
+ register(sessionID: string, title?: string): void {
73
75
  if (sessionID === "unknown") return
74
- if (!this.sessions.has(sessionID)) {
76
+ const existing = this.sessions.get(sessionID)
77
+ if (existing) {
78
+ if (title) existing.title = title
79
+ } else {
75
80
  this.sessions.set(sessionID, {
76
81
  sessionID,
77
82
  lastActivity: Date.now(),
78
83
  createdAt: Date.now(),
84
+ title,
79
85
  })
80
86
  }
81
87
  }
82
88
 
89
+ /** 获取会话标题 */
90
+ getSessionTitle(sessionID: string): string | undefined {
91
+ return this.sessions.get(sessionID)?.title
92
+ }
93
+
83
94
  /** 移除会话 */
84
95
  remove(sessionID: string): void {
85
96
  this.sessions.delete(sessionID)