@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 +9 -7
- package/config.ts +49 -27
- package/delayed-dispatcher.ts +24 -0
- package/index.ts +58 -42
- package/message.ts +29 -8
- package/package.json +1 -1
- package/senders/feishu.ts +0 -12
- package/senders/wechat-work.ts +1 -2
- package/session-tracker.ts +13 -2
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.
|
|
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
|
-
|
|
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?.
|
|
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?.
|
|
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?.
|
|
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?.
|
|
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?.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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: {
|
|
387
|
-
screen_flash: {
|
|
388
|
-
wechat_work: {
|
|
389
|
-
feishu: {
|
|
390
|
-
custom_webhook: {
|
|
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
|
-
|
|
425
|
-
options.channels?.system_message?.
|
|
426
|
-
DEFAULT_CONFIG.channels!.system_message!.
|
|
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
|
-
|
|
431
|
-
options.channels?.screen_flash?.
|
|
432
|
-
DEFAULT_CONFIG.channels!.screen_flash!.
|
|
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
|
-
|
|
440
|
-
options.channels?.wechat_work?.
|
|
441
|
-
DEFAULT_CONFIG.channels!.wechat_work!.
|
|
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
|
-
|
|
447
|
-
options.channels?.feishu?.
|
|
448
|
-
DEFAULT_CONFIG.channels!.feishu!.
|
|
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
|
-
|
|
454
|
-
options.channels?.custom_webhook?.
|
|
455
|
-
DEFAULT_CONFIG.channels?.custom_webhook?.
|
|
456
|
-
|
|
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,
|
package/delayed-dispatcher.ts
CHANGED
|
@@ -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
|
-
|
|
89
|
-
|
|
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
|
-
|
|
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}
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
|
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,
|
|
39
|
+
export function formatTitle(event: string, _sessionID?: string): string {
|
|
42
40
|
const label = EVENT_LABELS[event] ?? event
|
|
43
|
-
|
|
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
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
|
})
|
package/senders/wechat-work.ts
CHANGED
|
@@ -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({
|
package/session-tracker.ts
CHANGED
|
@@ -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
|
-
|
|
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)
|