@dsh-plus/notify-email 0.1.0
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/LICENSE +21 -0
- package/lib/client.js +624 -0
- package/lib/index.d.ts +149 -0
- package/lib/index.js +579 -0
- package/package.json +62 -0
- package/src/audit.ts +48 -0
- package/src/client/api.ts +83 -0
- package/src/client/card.tsx +239 -0
- package/src/client/client.ts +63 -0
- package/src/client/fields.tsx +75 -0
- package/src/client/i18n.ts +104 -0
- package/src/client/styles.ts +64 -0
- package/src/config-api.ts +116 -0
- package/src/config.ts +131 -0
- package/src/index.ts +24 -0
- package/src/mailer.ts +103 -0
- package/src/service.ts +122 -0
- package/src/triggers/builtin.ts +47 -0
- package/src/triggers/render.ts +93 -0
- package/src/triggers/types.ts +40 -0
- package/src/watchers/decision.ts +33 -0
- package/src/watchers/turn-end.ts +135 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 决策观察:tools/pre-execute 是零侵入观察点(waterfall,原样放行),
|
|
3
|
+
* 在工具体阻塞等待用户之前触发——时机即「agent 停下等用户决策」。
|
|
4
|
+
* 发送异步进行,绝不拖慢工具闸门;按 callId 去重。
|
|
5
|
+
* @module notify-email/watchers/decision
|
|
6
|
+
*/
|
|
7
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
8
|
+
|
|
9
|
+
import type { NotifyEmailService } from '../service.ts'
|
|
10
|
+
|
|
11
|
+
export function installDecisionWatcher(ctx: Context, service: NotifyEmailService): void {
|
|
12
|
+
const seen = new Set<string>()
|
|
13
|
+
const logger = ctx.logger('notify-email')
|
|
14
|
+
ctx.on('tools/pre-execute', (exec, next) => {
|
|
15
|
+
if (seen.has(exec.callId)) return next()
|
|
16
|
+
seen.add(exec.callId)
|
|
17
|
+
const sessionId = exec.agent?.id ?? 'unknown'
|
|
18
|
+
service
|
|
19
|
+
.dispatchDecision({
|
|
20
|
+
callId: exec.callId,
|
|
21
|
+
sessionId,
|
|
22
|
+
name: exec.name,
|
|
23
|
+
args: (exec.arguments ?? {}) as Record<string, unknown>,
|
|
24
|
+
})
|
|
25
|
+
.catch((error: unknown) => {
|
|
26
|
+
logger.warn(
|
|
27
|
+
`decision dispatch failed (${exec.name}, call ${exec.callId}): ` +
|
|
28
|
+
(error instanceof Error ? error.message : String(error)),
|
|
29
|
+
)
|
|
30
|
+
})
|
|
31
|
+
return next()
|
|
32
|
+
})
|
|
33
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 任务停止观察:订阅 session/event 持久事件流。
|
|
3
|
+
* - assistant/message 记录该会话最新的交付文本;
|
|
4
|
+
* - turn/end(completed/error/aborted)且属 runtime root agent → 空闲防抖后下发;
|
|
5
|
+
* - 防抖窗口内新 turn/start 到达则取消(goal 续轮、followup 排队等场景不误报);
|
|
6
|
+
* - 按 session:turn:kind 去重,同一会话边界不重复通知。
|
|
7
|
+
* @module notify-email/watchers/turn-end
|
|
8
|
+
*/
|
|
9
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
10
|
+
import type { Session, TurnEndReason } from '@deepseek-ai/dsh-session'
|
|
11
|
+
|
|
12
|
+
import type { NotifyEmailConfig } from '../config.ts'
|
|
13
|
+
import type { NotifyEmailService } from '../service.ts'
|
|
14
|
+
|
|
15
|
+
/** 只有这三类 turn 结束对用户有可决策价值;blocked/max-tokens/interrupted 不通知。 */
|
|
16
|
+
const NOTIFY_KINDS = new Set(['completed', 'error', 'aborted'])
|
|
17
|
+
|
|
18
|
+
interface SessionWatch {
|
|
19
|
+
lastDelivery?: string
|
|
20
|
+
timer?: ReturnType<typeof setTimeout>
|
|
21
|
+
readonly notified: Set<string>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface ContentBlockLike {
|
|
25
|
+
type?: string
|
|
26
|
+
text?: unknown
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** 从 assistant 消息 content 提取纯文本交付内容(仅 text 块,忽略工具调用等)。 */
|
|
30
|
+
export function extractDeliveryText(content: readonly ContentBlockLike[]): string | undefined {
|
|
31
|
+
const text = content
|
|
32
|
+
.filter((b) => b.type === 'text' && typeof b.text === 'string')
|
|
33
|
+
.map((b) => b.text as string)
|
|
34
|
+
.join('\n')
|
|
35
|
+
.trim()
|
|
36
|
+
return text.length > 0 ? text : undefined
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function extractErrorMessage(reason: unknown): string | undefined {
|
|
40
|
+
if (typeof reason !== 'object' || reason === null) return undefined
|
|
41
|
+
const error = (reason as { error?: unknown }).error
|
|
42
|
+
if (typeof error !== 'object' || error === null) return undefined
|
|
43
|
+
const message = (error as { message?: unknown }).message
|
|
44
|
+
return typeof message === 'string' ? message : undefined
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function installTurnEndWatcher(
|
|
48
|
+
ctx: Context,
|
|
49
|
+
service: NotifyEmailService,
|
|
50
|
+
resolveConfig: () => NotifyEmailConfig,
|
|
51
|
+
): void {
|
|
52
|
+
const watches = new Map<string, SessionWatch>()
|
|
53
|
+
const logger = ctx.logger('notify-email')
|
|
54
|
+
|
|
55
|
+
const watchOf = (sessionId: string): SessionWatch => {
|
|
56
|
+
let watch = watches.get(sessionId)
|
|
57
|
+
if (watch === undefined) {
|
|
58
|
+
watch = { notified: new Set() }
|
|
59
|
+
watches.set(sessionId, watch)
|
|
60
|
+
}
|
|
61
|
+
return watch
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const fire = (session: Session, turn: number, kind: string, errorMessage?: string): void => {
|
|
65
|
+
const agent = ctx.agents.get(session.id)
|
|
66
|
+
if (agent === undefined) return
|
|
67
|
+
if (agent.status !== 'idle' || agent.inbox.hasPending) return
|
|
68
|
+
if (!ctx.agents.roots().includes(agent)) return
|
|
69
|
+
const watch = watchOf(session.id)
|
|
70
|
+
const key = `${turn}:${kind}`
|
|
71
|
+
if (watch.notified.has(key)) return
|
|
72
|
+
watch.notified.add(key)
|
|
73
|
+
service
|
|
74
|
+
.dispatchTurnEnd({
|
|
75
|
+
sessionId: session.id,
|
|
76
|
+
turn,
|
|
77
|
+
kind,
|
|
78
|
+
...(errorMessage !== undefined ? { errorMessage } : {}),
|
|
79
|
+
...(watch.lastDelivery !== undefined ? { lastDelivery: watch.lastDelivery } : {}),
|
|
80
|
+
})
|
|
81
|
+
.catch((error: unknown) => {
|
|
82
|
+
logger.warn(
|
|
83
|
+
`turn-end dispatch failed (${session.id}, turn ${turn}, ${kind}): ` +
|
|
84
|
+
(error instanceof Error ? error.message : String(error)),
|
|
85
|
+
)
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const onTurnEnd = (session: Session, turn: number, reason: TurnEndReason): void => {
|
|
90
|
+
if (!NOTIFY_KINDS.has(reason.kind)) return
|
|
91
|
+
const watch = watchOf(session.id)
|
|
92
|
+
if (watch.timer !== undefined) clearTimeout(watch.timer)
|
|
93
|
+
const errorMessage = extractErrorMessage(reason)
|
|
94
|
+
watch.timer = setTimeout(
|
|
95
|
+
() => {
|
|
96
|
+
watch.timer = undefined
|
|
97
|
+
fire(session, turn, reason.kind, errorMessage)
|
|
98
|
+
},
|
|
99
|
+
resolveConfig().idleDebounceMs,
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
ctx.on('session/event', (session, event) => {
|
|
104
|
+
if (event.type === 'assistant/message') {
|
|
105
|
+
const text = extractDeliveryText(event.data.message.content)
|
|
106
|
+
if (text !== undefined) watchOf(session.id).lastDelivery = text
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
if (event.type === 'turn/start') {
|
|
110
|
+
const watch = watches.get(session.id)
|
|
111
|
+
if (watch?.timer !== undefined) {
|
|
112
|
+
clearTimeout(watch.timer)
|
|
113
|
+
watch.timer = undefined
|
|
114
|
+
}
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
if (event.type === 'turn/end') onTurnEnd(session, event.data.turn, event.data.reason)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
ctx.on('session/disposed', (session) => {
|
|
121
|
+
const watch = watches.get(session.id)
|
|
122
|
+
if (watch?.timer !== undefined) clearTimeout(watch.timer)
|
|
123
|
+
watches.delete(session.id)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
ctx.effect(
|
|
127
|
+
() => () => {
|
|
128
|
+
for (const watch of watches.values()) {
|
|
129
|
+
if (watch.timer !== undefined) clearTimeout(watch.timer)
|
|
130
|
+
}
|
|
131
|
+
watches.clear()
|
|
132
|
+
},
|
|
133
|
+
'notify-email: turn-end timers',
|
|
134
|
+
)
|
|
135
|
+
}
|