@trydying/opencode-feishu-notifier 0.3.1 → 0.3.2
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.md +13 -0
- package/package.json +5 -20
- package/src/config.ts +172 -0
- package/src/context/progress.ts +174 -0
- package/src/context/project.ts +202 -0
- package/src/feishu/client.ts +202 -0
- package/src/feishu/messages.ts +322 -0
- package/src/feishu/templates.ts +539 -0
- package/src/hooks.ts +40 -0
- package/src/index.ts +144 -0
- package/src/types.ts +102 -0
- package/dist/chunk-DGUM43GV.js +0 -11
- package/dist/chunk-DGUM43GV.js.map +0 -1
- package/dist/index.d.ts +0 -5
- package/dist/index.js +0 -550
- package/dist/index.js.map +0 -1
- package/dist/templates-PQN4V7CV.js +0 -564
- package/dist/templates-PQN4V7CV.js.map +0 -1
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import type { FeishuConfig } from "../config"
|
|
2
|
+
|
|
3
|
+
type TenantTokenResponse = {
|
|
4
|
+
code: number
|
|
5
|
+
msg: string
|
|
6
|
+
tenant_access_token?: string
|
|
7
|
+
expire?: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface TokenCacheEntry {
|
|
11
|
+
token: string
|
|
12
|
+
expiresAt: number // timestamp in milliseconds
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const tokenCache = new Map<string, TokenCacheEntry>()
|
|
16
|
+
|
|
17
|
+
function clearTokenCache(config: FeishuConfig) {
|
|
18
|
+
const cacheKey = `${config.appId}:${config.appSecret}`
|
|
19
|
+
tokenCache.delete(cacheKey)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isTokenExpiredError(code: number): boolean {
|
|
23
|
+
// Common Feishu token expiration error codes
|
|
24
|
+
return code === 99991663 || code === 99991664 || code === 99991668
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type MessageResponse = {
|
|
28
|
+
code: number
|
|
29
|
+
msg: string
|
|
30
|
+
data?: {
|
|
31
|
+
message_id?: string
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type FeishuPostContent = {
|
|
36
|
+
post: {
|
|
37
|
+
zh_cn: {
|
|
38
|
+
title: string
|
|
39
|
+
content: Array<Array<{
|
|
40
|
+
tag: string
|
|
41
|
+
text?: string
|
|
42
|
+
href?: string
|
|
43
|
+
un_escape?: boolean
|
|
44
|
+
}>>
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function ensureFetch(): typeof fetch {
|
|
50
|
+
if (typeof fetch === "undefined") {
|
|
51
|
+
throw new Error("Global fetch is not available. Use Node.js 18+.")
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return fetch
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function readJson<T>(response: Response): Promise<T> {
|
|
58
|
+
const text = await response.text()
|
|
59
|
+
if (!text) {
|
|
60
|
+
throw new Error(`Empty response from Feishu API (${response.status}).`)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return JSON.parse(text) as T
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function getTenantAccessToken(config: FeishuConfig): Promise<string> {
|
|
67
|
+
const cacheKey = `${config.appId}:${config.appSecret}`
|
|
68
|
+
const now = Date.now()
|
|
69
|
+
|
|
70
|
+
// Check cache
|
|
71
|
+
const cached = tokenCache.get(cacheKey)
|
|
72
|
+
if (cached && cached.expiresAt > now + 60000) { // 60 second buffer
|
|
73
|
+
return cached.token
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const fetchImpl = ensureFetch()
|
|
77
|
+
const response = await fetchImpl(
|
|
78
|
+
"https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal",
|
|
79
|
+
{
|
|
80
|
+
method: "POST",
|
|
81
|
+
headers: {
|
|
82
|
+
"Content-Type": "application/json"
|
|
83
|
+
},
|
|
84
|
+
body: JSON.stringify({
|
|
85
|
+
app_id: config.appId,
|
|
86
|
+
app_secret: config.appSecret
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
if (!response.ok) {
|
|
92
|
+
const errorText = await response.text().catch(() => `Failed to read error response`)
|
|
93
|
+
throw new Error(`Feishu auth request failed: ${response.status} - ${errorText}`)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const payload = await readJson<TenantTokenResponse>(response)
|
|
97
|
+
if (payload.code !== 0 || !payload.tenant_access_token) {
|
|
98
|
+
throw new Error(`Feishu auth failed: ${payload.msg} (${payload.code})`)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Cache token with expiration (default 2 hours if not provided)
|
|
102
|
+
const expiresIn = payload.expire ? payload.expire * 1000 : 2 * 60 * 60 * 1000
|
|
103
|
+
const expiresAt = now + expiresIn - 60000 // 60 second buffer
|
|
104
|
+
|
|
105
|
+
tokenCache.set(cacheKey, {
|
|
106
|
+
token: payload.tenant_access_token,
|
|
107
|
+
expiresAt
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
return payload.tenant_access_token
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function sendMessage(
|
|
114
|
+
config: FeishuConfig,
|
|
115
|
+
msgType: "text" | "post",
|
|
116
|
+
content: unknown
|
|
117
|
+
): Promise<MessageResponse> {
|
|
118
|
+
const fetchImpl = ensureFetch()
|
|
119
|
+
|
|
120
|
+
const sendWithToken = async (retryOnTokenExpired = true): Promise<MessageResponse> => {
|
|
121
|
+
const token = await getTenantAccessToken(config)
|
|
122
|
+
const response = await fetchImpl(
|
|
123
|
+
`https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=${config.receiverType}`,
|
|
124
|
+
{
|
|
125
|
+
method: "POST",
|
|
126
|
+
headers: {
|
|
127
|
+
Authorization: `Bearer ${token}`,
|
|
128
|
+
"Content-Type": "application/json"
|
|
129
|
+
},
|
|
130
|
+
body: JSON.stringify({
|
|
131
|
+
receive_id: config.receiverId,
|
|
132
|
+
msg_type: msgType,
|
|
133
|
+
content: JSON.stringify(content)
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
if (!response.ok) {
|
|
139
|
+
const errorText = await response.text().catch(() => `Failed to read error response`)
|
|
140
|
+
throw new Error(`Feishu message request failed: ${response.status} - ${errorText}`)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const payload = await readJson<MessageResponse>(response)
|
|
144
|
+
if (payload.code !== 0) {
|
|
145
|
+
// Check if token expired
|
|
146
|
+
if (retryOnTokenExpired && isTokenExpiredError(payload.code)) {
|
|
147
|
+
clearTokenCache(config)
|
|
148
|
+
return sendWithToken(false) // Retry once
|
|
149
|
+
}
|
|
150
|
+
throw new Error(`Feishu message failed: ${payload.msg} (${payload.code}) - Response: ${JSON.stringify(payload)}`)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return payload
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return sendWithToken()
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export async function sendTextMessage(
|
|
160
|
+
config: FeishuConfig,
|
|
161
|
+
text: string
|
|
162
|
+
): Promise<MessageResponse> {
|
|
163
|
+
return sendMessage(config, "text", { text })
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* 将纯文本转换为飞书富文本(post)格式
|
|
168
|
+
* 简化实现:所有文本作为一个段落
|
|
169
|
+
*/
|
|
170
|
+
function textToPostContent(text: string, title: string = "OpenCode 通知"): FeishuPostContent {
|
|
171
|
+
// 移除空行,但保留换行符
|
|
172
|
+
const cleanedText = text.split('\n').filter(line => line.trim().length > 0).join('\n')
|
|
173
|
+
|
|
174
|
+
const content = [
|
|
175
|
+
[
|
|
176
|
+
{
|
|
177
|
+
tag: 'text',
|
|
178
|
+
text: cleanedText,
|
|
179
|
+
un_escape: true
|
|
180
|
+
}
|
|
181
|
+
]
|
|
182
|
+
]
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
post: {
|
|
186
|
+
zh_cn: {
|
|
187
|
+
title,
|
|
188
|
+
content
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export async function sendRichTextMessage(
|
|
195
|
+
config: FeishuConfig,
|
|
196
|
+
text: string,
|
|
197
|
+
title?: string,
|
|
198
|
+
richContent?: FeishuPostContent
|
|
199
|
+
): Promise<MessageResponse> {
|
|
200
|
+
const postContent = richContent || textToPostContent(text, title)
|
|
201
|
+
return sendMessage(config, "post", postContent)
|
|
202
|
+
}
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import os from "os";
|
|
2
|
+
|
|
3
|
+
export type FeishuPostContent = {
|
|
4
|
+
post: {
|
|
5
|
+
zh_cn: {
|
|
6
|
+
title: string
|
|
7
|
+
content: Array<Array<{
|
|
8
|
+
tag: string
|
|
9
|
+
text?: string
|
|
10
|
+
href?: string
|
|
11
|
+
un_escape?: boolean
|
|
12
|
+
}>>
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type NotificationResult = {
|
|
18
|
+
title: string
|
|
19
|
+
text: string
|
|
20
|
+
richContent?: FeishuPostContent
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type NotificationType =
|
|
24
|
+
| "interaction_required"
|
|
25
|
+
| "permission_required"
|
|
26
|
+
| "command_args_required"
|
|
27
|
+
| "confirmation_required"
|
|
28
|
+
| "session_idle"
|
|
29
|
+
| "question_asked"
|
|
30
|
+
| "setup_test"
|
|
31
|
+
|
|
32
|
+
type EventPayload = {
|
|
33
|
+
type?: string;
|
|
34
|
+
payload?: unknown;
|
|
35
|
+
properties?: Record<string, unknown>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type SessionContext = {
|
|
39
|
+
sessionID?: string;
|
|
40
|
+
sessionTitle?: string;
|
|
41
|
+
agentName?: string;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
type SessionClient = {
|
|
45
|
+
session?: {
|
|
46
|
+
get?: (options: { path: { id: string } }) => Promise<{
|
|
47
|
+
data?: {
|
|
48
|
+
title?: string;
|
|
49
|
+
};
|
|
50
|
+
}>;
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const sessionTitleCache = new Map<string, string>();
|
|
55
|
+
const sessionAgentCache = new Map<string, string>();
|
|
56
|
+
|
|
57
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
58
|
+
if (value && typeof value === "object") {
|
|
59
|
+
return value as Record<string, unknown>;
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function readString(value: unknown): string | undefined {
|
|
65
|
+
return typeof value === "string" ? value : undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function extractEventPayload(event?: EventPayload): unknown {
|
|
69
|
+
if (!event) {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
if (event.payload !== undefined) {
|
|
73
|
+
return event.payload;
|
|
74
|
+
}
|
|
75
|
+
if (event.properties !== undefined) {
|
|
76
|
+
return event.properties;
|
|
77
|
+
}
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function extractEventProperties(event?: EventPayload): Record<string, unknown> | undefined {
|
|
82
|
+
if (!event) {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
if (event.properties) {
|
|
86
|
+
return asRecord(event.properties);
|
|
87
|
+
}
|
|
88
|
+
if (event.payload) {
|
|
89
|
+
return asRecord(event.payload);
|
|
90
|
+
}
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function extractSessionContext(event?: EventPayload): SessionContext {
|
|
95
|
+
const properties = extractEventProperties(event);
|
|
96
|
+
const info = asRecord(properties?.info);
|
|
97
|
+
const part = asRecord(properties?.part);
|
|
98
|
+
|
|
99
|
+
const sessionID =
|
|
100
|
+
readString(properties?.sessionID) ??
|
|
101
|
+
readString(info?.sessionID) ??
|
|
102
|
+
readString(info?.id) ??
|
|
103
|
+
readString(part?.sessionID);
|
|
104
|
+
|
|
105
|
+
const sessionTitle = readString(info?.title);
|
|
106
|
+
|
|
107
|
+
const agentName =
|
|
108
|
+
readString(properties?.agent) ??
|
|
109
|
+
readString(info?.agent) ??
|
|
110
|
+
readString(part?.agent) ??
|
|
111
|
+
readString(part?.name);
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
sessionID,
|
|
115
|
+
sessionTitle,
|
|
116
|
+
agentName,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function resolveSessionContext(
|
|
121
|
+
event?: EventPayload,
|
|
122
|
+
client?: SessionClient
|
|
123
|
+
): Promise<SessionContext> {
|
|
124
|
+
const baseContext = extractSessionContext(event);
|
|
125
|
+
if (!baseContext.sessionID) {
|
|
126
|
+
return baseContext;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const cachedTitle = sessionTitleCache.get(baseContext.sessionID);
|
|
130
|
+
const cachedAgent = sessionAgentCache.get(baseContext.sessionID);
|
|
131
|
+
const mergedContext = {
|
|
132
|
+
...baseContext,
|
|
133
|
+
sessionTitle: baseContext.sessionTitle ?? cachedTitle,
|
|
134
|
+
agentName: baseContext.agentName ?? cachedAgent,
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
if (mergedContext.sessionTitle) {
|
|
138
|
+
return mergedContext;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (client?.session?.get) {
|
|
142
|
+
try {
|
|
143
|
+
const response = await client.session.get({
|
|
144
|
+
path: { id: baseContext.sessionID },
|
|
145
|
+
});
|
|
146
|
+
const title = response?.data?.title;
|
|
147
|
+
if (title) {
|
|
148
|
+
sessionTitleCache.set(baseContext.sessionID, title);
|
|
149
|
+
return {
|
|
150
|
+
...mergedContext,
|
|
151
|
+
sessionTitle: title,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
} catch {
|
|
155
|
+
// 忽略会话信息获取失败
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return mergedContext;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function recordEventContext(event?: EventPayload): void {
|
|
163
|
+
const context = extractSessionContext(event);
|
|
164
|
+
if (!context.sessionID) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (context.sessionTitle) {
|
|
169
|
+
sessionTitleCache.set(context.sessionID, context.sessionTitle);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (context.agentName) {
|
|
173
|
+
sessionAgentCache.set(context.sessionID, context.agentName);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// 保持向后兼容的标题映射
|
|
178
|
+
const titles: Record<NotificationType, string> = {
|
|
179
|
+
interaction_required: "需要交互",
|
|
180
|
+
permission_required: "需要权限确认",
|
|
181
|
+
command_args_required: "需要补充参数",
|
|
182
|
+
confirmation_required: "需要确认",
|
|
183
|
+
session_idle: "OpenCode 闲暇",
|
|
184
|
+
question_asked: "需要选择方案",
|
|
185
|
+
setup_test: "Feishu 通知测试"
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* 构建结构化通知消息(新版本)
|
|
190
|
+
* @param type 通知类型
|
|
191
|
+
* @param event 事件数据
|
|
192
|
+
* @param directory 工作目录(可选,默认当前目录)
|
|
193
|
+
* @returns 包含标题和文本的消息对象
|
|
194
|
+
*/
|
|
195
|
+
export async function buildStructuredNotification(
|
|
196
|
+
type: NotificationType,
|
|
197
|
+
event?: EventPayload,
|
|
198
|
+
directory?: string,
|
|
199
|
+
client?: SessionClient
|
|
200
|
+
): Promise<NotificationResult> {
|
|
201
|
+
// 导入模板系统
|
|
202
|
+
const { buildStructuredMessage } = await import("./templates")
|
|
203
|
+
|
|
204
|
+
const eventPayload = extractEventPayload(event);
|
|
205
|
+
const sessionContext = await resolveSessionContext(event, client);
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
const text = await buildStructuredMessage(
|
|
209
|
+
type,
|
|
210
|
+
eventPayload,
|
|
211
|
+
event?.type,
|
|
212
|
+
directory,
|
|
213
|
+
sessionContext
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
// 返回标题(保持向后兼容)
|
|
217
|
+
return {
|
|
218
|
+
title: titles[type],
|
|
219
|
+
text,
|
|
220
|
+
richContent: textToPostContent(text, titles[type])
|
|
221
|
+
}
|
|
222
|
+
} catch (error) {
|
|
223
|
+
// 如果模板系统失败,回退到原始实现
|
|
224
|
+
return buildLegacyNotification(type, event)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* 构建传统格式的通知消息(向后兼容)
|
|
230
|
+
*/
|
|
231
|
+
export function buildLegacyNotification(
|
|
232
|
+
type: NotificationType,
|
|
233
|
+
event?: EventPayload
|
|
234
|
+
): NotificationResult {
|
|
235
|
+
const title = titles[type]
|
|
236
|
+
if (type === "setup_test") {
|
|
237
|
+
const text = `${title}\nFeishu 通知已启用。`
|
|
238
|
+
return {
|
|
239
|
+
title,
|
|
240
|
+
text,
|
|
241
|
+
richContent: textToPostContent(text, title)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const payloadText = formatPayload(extractEventPayload(event))
|
|
246
|
+
const sessionContext = extractSessionContext(event)
|
|
247
|
+
const lines = [
|
|
248
|
+
`[OpenCode] ${title}`,
|
|
249
|
+
event?.type ? `事件类型: ${event.type}` : "",
|
|
250
|
+
sessionContext.sessionTitle || sessionContext.sessionID
|
|
251
|
+
? `会话: ${sessionContext.sessionTitle ?? sessionContext.sessionID}`
|
|
252
|
+
: "",
|
|
253
|
+
sessionContext.agentName ? `Agent: ${sessionContext.agentName}` : "",
|
|
254
|
+
`主机: ${os.hostname()}`,
|
|
255
|
+
payloadText ? `详情: ${payloadText}` : ""
|
|
256
|
+
].filter(Boolean)
|
|
257
|
+
|
|
258
|
+
const text = lines.join("\n")
|
|
259
|
+
return {
|
|
260
|
+
title,
|
|
261
|
+
text,
|
|
262
|
+
richContent: textToPostContent(text, title)
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* 格式化负载文本
|
|
268
|
+
*/
|
|
269
|
+
function formatPayload(payload: unknown): string {
|
|
270
|
+
if (!payload) {
|
|
271
|
+
return ""
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const text = JSON.stringify(payload, null, 2)
|
|
275
|
+
if (text.length > 1200) {
|
|
276
|
+
return `${text.slice(0, 1200)}…`
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return text
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* 将纯文本转换为飞书富文本(post)格式
|
|
284
|
+
* 简化实现:所有文本作为一个段落
|
|
285
|
+
*/
|
|
286
|
+
function textToPostContent(text: string, title: string = "OpenCode 通知"): FeishuPostContent {
|
|
287
|
+
// 移除空行,但保留换行符
|
|
288
|
+
const cleanedText = text.split('\n').filter(line => line.trim().length > 0).join('\n')
|
|
289
|
+
|
|
290
|
+
const content = [
|
|
291
|
+
[
|
|
292
|
+
{
|
|
293
|
+
tag: 'text',
|
|
294
|
+
text: cleanedText,
|
|
295
|
+
un_escape: true
|
|
296
|
+
}
|
|
297
|
+
]
|
|
298
|
+
]
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
post: {
|
|
302
|
+
zh_cn: {
|
|
303
|
+
title,
|
|
304
|
+
content
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* 构建通知消息(主入口,保持向后兼容)
|
|
312
|
+
* 默认使用结构化消息,失败时回退
|
|
313
|
+
*/
|
|
314
|
+
export async function buildNotification(
|
|
315
|
+
type: NotificationType,
|
|
316
|
+
event?: EventPayload,
|
|
317
|
+
directory?: string,
|
|
318
|
+
client?: SessionClient
|
|
319
|
+
): Promise<NotificationResult> {
|
|
320
|
+
// 默认使用结构化消息
|
|
321
|
+
return buildStructuredNotification(type, event, directory, client)
|
|
322
|
+
}
|