@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
package/src/types.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { NotificationType } from "./feishu/messages";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 项目上下文信息
|
|
5
|
+
* 包含项目名称、分支、工作目录等基本信息
|
|
6
|
+
*/
|
|
7
|
+
export interface ProjectContext {
|
|
8
|
+
/** 项目名称,从 package.json 或目录名提取 */
|
|
9
|
+
projectName: string;
|
|
10
|
+
/** Git 当前分支名(如果项目是 git 仓库) */
|
|
11
|
+
branch?: string;
|
|
12
|
+
/** 工作目录绝对路径 */
|
|
13
|
+
workingDir: string;
|
|
14
|
+
/** 仓库 URL(如果是 git 项目) */
|
|
15
|
+
repoUrl?: string;
|
|
16
|
+
/** 是否 Git 仓库 */
|
|
17
|
+
isGitRepo: boolean;
|
|
18
|
+
/** 机器 hostname,用于区分不同的机器 */
|
|
19
|
+
hostname?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 进度信息
|
|
24
|
+
* 包含当前任务状态和进度摘要
|
|
25
|
+
*/
|
|
26
|
+
export interface ProgressInfo {
|
|
27
|
+
/** 最近的操作描述 */
|
|
28
|
+
lastAction?: string;
|
|
29
|
+
/** 当前任务描述(如果可获取) */
|
|
30
|
+
currentTask?: string;
|
|
31
|
+
/** 时间戳 */
|
|
32
|
+
timestamp: string;
|
|
33
|
+
/** 工作目录中的文件变更信息 */
|
|
34
|
+
fileChanges?: {
|
|
35
|
+
/** 新增文件数 */
|
|
36
|
+
added?: number;
|
|
37
|
+
/** 修改文件数 */
|
|
38
|
+
modified?: number;
|
|
39
|
+
/** 删除文件数 */
|
|
40
|
+
deleted?: number;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 消息上下文
|
|
46
|
+
* 构建消息所需的所有上下文信息
|
|
47
|
+
*/
|
|
48
|
+
export interface MessageContext {
|
|
49
|
+
/** 项目上下文 */
|
|
50
|
+
project: ProjectContext;
|
|
51
|
+
/** 进度信息 */
|
|
52
|
+
progress: ProgressInfo;
|
|
53
|
+
/** 事件类型 */
|
|
54
|
+
eventType: NotificationType;
|
|
55
|
+
/** 原始事件负载 */
|
|
56
|
+
eventPayload?: unknown;
|
|
57
|
+
/** 事件原始类型 */
|
|
58
|
+
originalEventType?: string;
|
|
59
|
+
/** 会话 ID */
|
|
60
|
+
sessionID?: string;
|
|
61
|
+
/** 会话标题 */
|
|
62
|
+
sessionTitle?: string;
|
|
63
|
+
/** 触发事件的 Agent 名称 */
|
|
64
|
+
agentName?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 消息模板接口
|
|
69
|
+
* 定义三段式消息的构建方法
|
|
70
|
+
*/
|
|
71
|
+
export interface MessageTemplate {
|
|
72
|
+
/** 构建标题区域:项目名 + 分支 + 事件类型 */
|
|
73
|
+
buildTitle(context: MessageContext): string;
|
|
74
|
+
|
|
75
|
+
/** 构建原因区域:为什么发送通知 + 具体说明 */
|
|
76
|
+
buildReason(context: MessageContext): string;
|
|
77
|
+
|
|
78
|
+
/** 构建进度区域:工作目录 + 最近操作 + 当前任务 */
|
|
79
|
+
buildProgress(context: MessageContext): string;
|
|
80
|
+
|
|
81
|
+
/** 构建完整消息 */
|
|
82
|
+
buildFullMessage(context: MessageContext): string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 事件原因说明配置
|
|
87
|
+
*/
|
|
88
|
+
export interface ReasonConfig {
|
|
89
|
+
/** 原因分类:闲暇等待/需要权限/需要选择等 */
|
|
90
|
+
category: string;
|
|
91
|
+
/** 原因说明文案 */
|
|
92
|
+
description: string;
|
|
93
|
+
/** 是否需要具体操作说明 */
|
|
94
|
+
requiresAction: boolean;
|
|
95
|
+
/** 事件类型对应的 emoji */
|
|
96
|
+
emoji?: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 事件类型到原因配置的映射
|
|
101
|
+
*/
|
|
102
|
+
export type ReasonConfigMap = Record<NotificationType, ReasonConfig>;
|
package/dist/chunk-DGUM43GV.js
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
-
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
-
}) : x)(function(x) {
|
|
4
|
-
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
-
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
-
});
|
|
7
|
-
|
|
8
|
-
export {
|
|
9
|
-
__require
|
|
10
|
-
};
|
|
11
|
-
//# sourceMappingURL=chunk-DGUM43GV.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/dist/index.d.ts
DELETED
package/dist/index.js
DELETED
|
@@ -1,550 +0,0 @@
|
|
|
1
|
-
import "./chunk-DGUM43GV.js";
|
|
2
|
-
|
|
3
|
-
// src/config.ts
|
|
4
|
-
import fs from "fs";
|
|
5
|
-
import os from "os";
|
|
6
|
-
import path from "path";
|
|
7
|
-
var receiverTypes = ["user_id", "open_id", "chat_id"];
|
|
8
|
-
function getConfigPaths(options) {
|
|
9
|
-
if (options.configPath) {
|
|
10
|
-
return [options.configPath];
|
|
11
|
-
}
|
|
12
|
-
const paths = [];
|
|
13
|
-
const directory = options.directory ?? process.cwd();
|
|
14
|
-
const xdgConfig = process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), ".config");
|
|
15
|
-
paths.push(path.join(xdgConfig, "opencode", "feishu-notifier.json"));
|
|
16
|
-
paths.push(path.join(directory, ".opencode", "feishu-notifier.json"));
|
|
17
|
-
return paths;
|
|
18
|
-
}
|
|
19
|
-
function readConfigFile(filePath) {
|
|
20
|
-
if (!fs.existsSync(filePath)) {
|
|
21
|
-
return null;
|
|
22
|
-
}
|
|
23
|
-
const raw = fs.readFileSync(filePath, "utf8").trim();
|
|
24
|
-
if (!raw) {
|
|
25
|
-
return null;
|
|
26
|
-
}
|
|
27
|
-
try {
|
|
28
|
-
return JSON.parse(raw);
|
|
29
|
-
} catch (error) {
|
|
30
|
-
throw new Error(`Invalid JSON in ${filePath}: ${String(error)}`);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
function readEnvConfig() {
|
|
34
|
-
return {
|
|
35
|
-
appId: process.env.FEISHU_APP_ID,
|
|
36
|
-
appSecret: process.env.FEISHU_APP_SECRET,
|
|
37
|
-
receiverType: process.env.FEISHU_RECEIVER_TYPE,
|
|
38
|
-
receiverId: process.env.FEISHU_RECEIVER_ID
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
function resolveConfig(options) {
|
|
42
|
-
const configPaths = getConfigPaths(options);
|
|
43
|
-
let mergedConfig = {};
|
|
44
|
-
const sources = [];
|
|
45
|
-
for (const configPath of configPaths) {
|
|
46
|
-
const config = readConfigFile(configPath);
|
|
47
|
-
if (config) {
|
|
48
|
-
mergedConfig = {
|
|
49
|
-
...mergedConfig,
|
|
50
|
-
...config
|
|
51
|
-
};
|
|
52
|
-
sources.push({ type: "file", detail: configPath });
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
const envConfig = readEnvConfig();
|
|
56
|
-
if (envConfig.appId || envConfig.appSecret || envConfig.receiverType || envConfig.receiverId) {
|
|
57
|
-
mergedConfig = {
|
|
58
|
-
...mergedConfig,
|
|
59
|
-
...envConfig
|
|
60
|
-
};
|
|
61
|
-
sources.push({ type: "env", detail: "FEISHU_*" });
|
|
62
|
-
}
|
|
63
|
-
return { mergedConfig, sources };
|
|
64
|
-
}
|
|
65
|
-
function finalizeConfig(mergedConfig, sources) {
|
|
66
|
-
if (sources.length === 0) {
|
|
67
|
-
throw new Error(
|
|
68
|
-
"Missing Feishu configuration. Use FEISHU_* environment variables or create feishu-notifier.json."
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
const missing = [];
|
|
72
|
-
if (!mergedConfig.appId) missing.push("appId");
|
|
73
|
-
if (!mergedConfig.appSecret) missing.push("appSecret");
|
|
74
|
-
if (!mergedConfig.receiverType) missing.push("receiverType");
|
|
75
|
-
if (!mergedConfig.receiverId) missing.push("receiverId");
|
|
76
|
-
if (missing.length > 0) {
|
|
77
|
-
throw new Error(`Missing config fields: ${missing.join(", ")}`);
|
|
78
|
-
}
|
|
79
|
-
const receiverType = mergedConfig.receiverType;
|
|
80
|
-
if (!receiverTypes.includes(receiverType)) {
|
|
81
|
-
throw new Error(
|
|
82
|
-
`Invalid receiverType: ${mergedConfig.receiverType}. Expected one of ${receiverTypes.join(", ")}`
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
return {
|
|
86
|
-
appId: mergedConfig.appId,
|
|
87
|
-
appSecret: mergedConfig.appSecret,
|
|
88
|
-
receiverType,
|
|
89
|
-
receiverId: mergedConfig.receiverId
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
function loadConfigWithSource(options = {}) {
|
|
93
|
-
const { mergedConfig, sources } = resolveConfig(options);
|
|
94
|
-
return {
|
|
95
|
-
config: finalizeConfig(mergedConfig, sources),
|
|
96
|
-
sources
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// src/feishu/client.ts
|
|
101
|
-
var tokenCache = /* @__PURE__ */ new Map();
|
|
102
|
-
function clearTokenCache(config) {
|
|
103
|
-
const cacheKey = `${config.appId}:${config.appSecret}`;
|
|
104
|
-
tokenCache.delete(cacheKey);
|
|
105
|
-
}
|
|
106
|
-
function isTokenExpiredError(code) {
|
|
107
|
-
return code === 99991663 || code === 99991664 || code === 99991668;
|
|
108
|
-
}
|
|
109
|
-
function ensureFetch() {
|
|
110
|
-
if (typeof fetch === "undefined") {
|
|
111
|
-
throw new Error("Global fetch is not available. Use Node.js 18+.");
|
|
112
|
-
}
|
|
113
|
-
return fetch;
|
|
114
|
-
}
|
|
115
|
-
async function readJson(response) {
|
|
116
|
-
const text = await response.text();
|
|
117
|
-
if (!text) {
|
|
118
|
-
throw new Error(`Empty response from Feishu API (${response.status}).`);
|
|
119
|
-
}
|
|
120
|
-
return JSON.parse(text);
|
|
121
|
-
}
|
|
122
|
-
async function getTenantAccessToken(config) {
|
|
123
|
-
const cacheKey = `${config.appId}:${config.appSecret}`;
|
|
124
|
-
const now = Date.now();
|
|
125
|
-
const cached = tokenCache.get(cacheKey);
|
|
126
|
-
if (cached && cached.expiresAt > now + 6e4) {
|
|
127
|
-
return cached.token;
|
|
128
|
-
}
|
|
129
|
-
const fetchImpl = ensureFetch();
|
|
130
|
-
const response = await fetchImpl(
|
|
131
|
-
"https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal",
|
|
132
|
-
{
|
|
133
|
-
method: "POST",
|
|
134
|
-
headers: {
|
|
135
|
-
"Content-Type": "application/json"
|
|
136
|
-
},
|
|
137
|
-
body: JSON.stringify({
|
|
138
|
-
app_id: config.appId,
|
|
139
|
-
app_secret: config.appSecret
|
|
140
|
-
})
|
|
141
|
-
}
|
|
142
|
-
);
|
|
143
|
-
if (!response.ok) {
|
|
144
|
-
const errorText = await response.text().catch(() => `Failed to read error response`);
|
|
145
|
-
throw new Error(`Feishu auth request failed: ${response.status} - ${errorText}`);
|
|
146
|
-
}
|
|
147
|
-
const payload = await readJson(response);
|
|
148
|
-
if (payload.code !== 0 || !payload.tenant_access_token) {
|
|
149
|
-
throw new Error(`Feishu auth failed: ${payload.msg} (${payload.code})`);
|
|
150
|
-
}
|
|
151
|
-
const expiresIn = payload.expire ? payload.expire * 1e3 : 2 * 60 * 60 * 1e3;
|
|
152
|
-
const expiresAt = now + expiresIn - 6e4;
|
|
153
|
-
tokenCache.set(cacheKey, {
|
|
154
|
-
token: payload.tenant_access_token,
|
|
155
|
-
expiresAt
|
|
156
|
-
});
|
|
157
|
-
return payload.tenant_access_token;
|
|
158
|
-
}
|
|
159
|
-
async function sendMessage(config, msgType, content) {
|
|
160
|
-
const fetchImpl = ensureFetch();
|
|
161
|
-
const sendWithToken = async (retryOnTokenExpired = true) => {
|
|
162
|
-
const token = await getTenantAccessToken(config);
|
|
163
|
-
const response = await fetchImpl(
|
|
164
|
-
`https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=${config.receiverType}`,
|
|
165
|
-
{
|
|
166
|
-
method: "POST",
|
|
167
|
-
headers: {
|
|
168
|
-
Authorization: `Bearer ${token}`,
|
|
169
|
-
"Content-Type": "application/json"
|
|
170
|
-
},
|
|
171
|
-
body: JSON.stringify({
|
|
172
|
-
receive_id: config.receiverId,
|
|
173
|
-
msg_type: msgType,
|
|
174
|
-
content: JSON.stringify(content)
|
|
175
|
-
})
|
|
176
|
-
}
|
|
177
|
-
);
|
|
178
|
-
if (!response.ok) {
|
|
179
|
-
const errorText = await response.text().catch(() => `Failed to read error response`);
|
|
180
|
-
throw new Error(`Feishu message request failed: ${response.status} - ${errorText}`);
|
|
181
|
-
}
|
|
182
|
-
const payload = await readJson(response);
|
|
183
|
-
if (payload.code !== 0) {
|
|
184
|
-
if (retryOnTokenExpired && isTokenExpiredError(payload.code)) {
|
|
185
|
-
clearTokenCache(config);
|
|
186
|
-
return sendWithToken(false);
|
|
187
|
-
}
|
|
188
|
-
throw new Error(`Feishu message failed: ${payload.msg} (${payload.code}) - Response: ${JSON.stringify(payload)}`);
|
|
189
|
-
}
|
|
190
|
-
return payload;
|
|
191
|
-
};
|
|
192
|
-
return sendWithToken();
|
|
193
|
-
}
|
|
194
|
-
async function sendTextMessage(config, text) {
|
|
195
|
-
return sendMessage(config, "text", { text });
|
|
196
|
-
}
|
|
197
|
-
function textToPostContent(text, title = "OpenCode \u901A\u77E5") {
|
|
198
|
-
const cleanedText = text.split("\n").filter((line) => line.trim().length > 0).join("\n");
|
|
199
|
-
const content = [
|
|
200
|
-
[
|
|
201
|
-
{
|
|
202
|
-
tag: "text",
|
|
203
|
-
text: cleanedText,
|
|
204
|
-
un_escape: true
|
|
205
|
-
}
|
|
206
|
-
]
|
|
207
|
-
];
|
|
208
|
-
return {
|
|
209
|
-
post: {
|
|
210
|
-
zh_cn: {
|
|
211
|
-
title,
|
|
212
|
-
content
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
async function sendRichTextMessage(config, text, title, richContent) {
|
|
218
|
-
const postContent = richContent || textToPostContent(text, title);
|
|
219
|
-
return sendMessage(config, "post", postContent);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// src/feishu/messages.ts
|
|
223
|
-
import os2 from "os";
|
|
224
|
-
var sessionTitleCache = /* @__PURE__ */ new Map();
|
|
225
|
-
var sessionAgentCache = /* @__PURE__ */ new Map();
|
|
226
|
-
function asRecord(value) {
|
|
227
|
-
if (value && typeof value === "object") {
|
|
228
|
-
return value;
|
|
229
|
-
}
|
|
230
|
-
return void 0;
|
|
231
|
-
}
|
|
232
|
-
function readString(value) {
|
|
233
|
-
return typeof value === "string" ? value : void 0;
|
|
234
|
-
}
|
|
235
|
-
function extractEventPayload(event) {
|
|
236
|
-
if (!event) {
|
|
237
|
-
return void 0;
|
|
238
|
-
}
|
|
239
|
-
if (event.payload !== void 0) {
|
|
240
|
-
return event.payload;
|
|
241
|
-
}
|
|
242
|
-
if (event.properties !== void 0) {
|
|
243
|
-
return event.properties;
|
|
244
|
-
}
|
|
245
|
-
return void 0;
|
|
246
|
-
}
|
|
247
|
-
function extractEventProperties(event) {
|
|
248
|
-
if (!event) {
|
|
249
|
-
return void 0;
|
|
250
|
-
}
|
|
251
|
-
if (event.properties) {
|
|
252
|
-
return asRecord(event.properties);
|
|
253
|
-
}
|
|
254
|
-
if (event.payload) {
|
|
255
|
-
return asRecord(event.payload);
|
|
256
|
-
}
|
|
257
|
-
return void 0;
|
|
258
|
-
}
|
|
259
|
-
function extractSessionContext(event) {
|
|
260
|
-
const properties = extractEventProperties(event);
|
|
261
|
-
const info = asRecord(properties?.info);
|
|
262
|
-
const part = asRecord(properties?.part);
|
|
263
|
-
const sessionID = readString(properties?.sessionID) ?? readString(info?.sessionID) ?? readString(info?.id) ?? readString(part?.sessionID);
|
|
264
|
-
const sessionTitle = readString(info?.title);
|
|
265
|
-
const agentName = readString(properties?.agent) ?? readString(info?.agent) ?? readString(part?.agent) ?? readString(part?.name);
|
|
266
|
-
return {
|
|
267
|
-
sessionID,
|
|
268
|
-
sessionTitle,
|
|
269
|
-
agentName
|
|
270
|
-
};
|
|
271
|
-
}
|
|
272
|
-
async function resolveSessionContext(event, client) {
|
|
273
|
-
const baseContext = extractSessionContext(event);
|
|
274
|
-
if (!baseContext.sessionID) {
|
|
275
|
-
return baseContext;
|
|
276
|
-
}
|
|
277
|
-
const cachedTitle = sessionTitleCache.get(baseContext.sessionID);
|
|
278
|
-
const cachedAgent = sessionAgentCache.get(baseContext.sessionID);
|
|
279
|
-
const mergedContext = {
|
|
280
|
-
...baseContext,
|
|
281
|
-
sessionTitle: baseContext.sessionTitle ?? cachedTitle,
|
|
282
|
-
agentName: baseContext.agentName ?? cachedAgent
|
|
283
|
-
};
|
|
284
|
-
if (mergedContext.sessionTitle) {
|
|
285
|
-
return mergedContext;
|
|
286
|
-
}
|
|
287
|
-
if (client?.session?.get) {
|
|
288
|
-
try {
|
|
289
|
-
const response = await client.session.get({
|
|
290
|
-
path: { id: baseContext.sessionID }
|
|
291
|
-
});
|
|
292
|
-
const title = response?.data?.title;
|
|
293
|
-
if (title) {
|
|
294
|
-
sessionTitleCache.set(baseContext.sessionID, title);
|
|
295
|
-
return {
|
|
296
|
-
...mergedContext,
|
|
297
|
-
sessionTitle: title
|
|
298
|
-
};
|
|
299
|
-
}
|
|
300
|
-
} catch {
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
return mergedContext;
|
|
304
|
-
}
|
|
305
|
-
function recordEventContext(event) {
|
|
306
|
-
const context = extractSessionContext(event);
|
|
307
|
-
if (!context.sessionID) {
|
|
308
|
-
return;
|
|
309
|
-
}
|
|
310
|
-
if (context.sessionTitle) {
|
|
311
|
-
sessionTitleCache.set(context.sessionID, context.sessionTitle);
|
|
312
|
-
}
|
|
313
|
-
if (context.agentName) {
|
|
314
|
-
sessionAgentCache.set(context.sessionID, context.agentName);
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
var titles = {
|
|
318
|
-
interaction_required: "\u9700\u8981\u4EA4\u4E92",
|
|
319
|
-
permission_required: "\u9700\u8981\u6743\u9650\u786E\u8BA4",
|
|
320
|
-
command_args_required: "\u9700\u8981\u8865\u5145\u53C2\u6570",
|
|
321
|
-
confirmation_required: "\u9700\u8981\u786E\u8BA4",
|
|
322
|
-
session_idle: "OpenCode \u95F2\u6687",
|
|
323
|
-
question_asked: "\u9700\u8981\u9009\u62E9\u65B9\u6848",
|
|
324
|
-
setup_test: "Feishu \u901A\u77E5\u6D4B\u8BD5"
|
|
325
|
-
};
|
|
326
|
-
async function buildStructuredNotification(type, event, directory, client) {
|
|
327
|
-
const { buildStructuredMessage } = await import("./templates-PQN4V7CV.js");
|
|
328
|
-
const eventPayload = extractEventPayload(event);
|
|
329
|
-
const sessionContext = await resolveSessionContext(event, client);
|
|
330
|
-
try {
|
|
331
|
-
const text = await buildStructuredMessage(
|
|
332
|
-
type,
|
|
333
|
-
eventPayload,
|
|
334
|
-
event?.type,
|
|
335
|
-
directory,
|
|
336
|
-
sessionContext
|
|
337
|
-
);
|
|
338
|
-
return {
|
|
339
|
-
title: titles[type],
|
|
340
|
-
text,
|
|
341
|
-
richContent: textToPostContent2(text, titles[type])
|
|
342
|
-
};
|
|
343
|
-
} catch (error) {
|
|
344
|
-
return buildLegacyNotification(type, event);
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
function buildLegacyNotification(type, event) {
|
|
348
|
-
const title = titles[type];
|
|
349
|
-
if (type === "setup_test") {
|
|
350
|
-
const text2 = `${title}
|
|
351
|
-
Feishu \u901A\u77E5\u5DF2\u542F\u7528\u3002`;
|
|
352
|
-
return {
|
|
353
|
-
title,
|
|
354
|
-
text: text2,
|
|
355
|
-
richContent: textToPostContent2(text2, title)
|
|
356
|
-
};
|
|
357
|
-
}
|
|
358
|
-
const payloadText = formatPayload(extractEventPayload(event));
|
|
359
|
-
const sessionContext = extractSessionContext(event);
|
|
360
|
-
const lines = [
|
|
361
|
-
`[OpenCode] ${title}`,
|
|
362
|
-
event?.type ? `\u4E8B\u4EF6\u7C7B\u578B: ${event.type}` : "",
|
|
363
|
-
sessionContext.sessionTitle || sessionContext.sessionID ? `\u4F1A\u8BDD: ${sessionContext.sessionTitle ?? sessionContext.sessionID}` : "",
|
|
364
|
-
sessionContext.agentName ? `Agent: ${sessionContext.agentName}` : "",
|
|
365
|
-
`\u4E3B\u673A: ${os2.hostname()}`,
|
|
366
|
-
payloadText ? `\u8BE6\u60C5: ${payloadText}` : ""
|
|
367
|
-
].filter(Boolean);
|
|
368
|
-
const text = lines.join("\n");
|
|
369
|
-
return {
|
|
370
|
-
title,
|
|
371
|
-
text,
|
|
372
|
-
richContent: textToPostContent2(text, title)
|
|
373
|
-
};
|
|
374
|
-
}
|
|
375
|
-
function formatPayload(payload) {
|
|
376
|
-
if (!payload) {
|
|
377
|
-
return "";
|
|
378
|
-
}
|
|
379
|
-
const text = JSON.stringify(payload, null, 2);
|
|
380
|
-
if (text.length > 1200) {
|
|
381
|
-
return `${text.slice(0, 1200)}\u2026`;
|
|
382
|
-
}
|
|
383
|
-
return text;
|
|
384
|
-
}
|
|
385
|
-
function textToPostContent2(text, title = "OpenCode \u901A\u77E5") {
|
|
386
|
-
const cleanedText = text.split("\n").filter((line) => line.trim().length > 0).join("\n");
|
|
387
|
-
const content = [
|
|
388
|
-
[
|
|
389
|
-
{
|
|
390
|
-
tag: "text",
|
|
391
|
-
text: cleanedText,
|
|
392
|
-
un_escape: true
|
|
393
|
-
}
|
|
394
|
-
]
|
|
395
|
-
];
|
|
396
|
-
return {
|
|
397
|
-
post: {
|
|
398
|
-
zh_cn: {
|
|
399
|
-
title,
|
|
400
|
-
content
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
};
|
|
404
|
-
}
|
|
405
|
-
async function buildNotification(type, event, directory, client) {
|
|
406
|
-
return buildStructuredNotification(type, event, directory, client);
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// src/hooks.ts
|
|
410
|
-
function mapEventToNotification(eventType) {
|
|
411
|
-
switch (eventType) {
|
|
412
|
-
case "permission.asked":
|
|
413
|
-
return "permission_required";
|
|
414
|
-
case "question.asked":
|
|
415
|
-
return "question_asked";
|
|
416
|
-
default:
|
|
417
|
-
return null;
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
function mapCompletionEventToNotification(event) {
|
|
421
|
-
const status = event.properties?.status;
|
|
422
|
-
if (event.type === "session.status" && status?.type === "idle") {
|
|
423
|
-
return "session_idle";
|
|
424
|
-
}
|
|
425
|
-
if (event.type === "message.completed" || event.type === "message.failed" || event.type === "message.errored") {
|
|
426
|
-
return "session_idle";
|
|
427
|
-
}
|
|
428
|
-
return null;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
// src/index.ts
|
|
432
|
-
var serviceName = "opencode-feishu-notifier";
|
|
433
|
-
var FeishuNotifierPlugin = async ({ client, directory }) => {
|
|
434
|
-
let configCache = null;
|
|
435
|
-
let configError = null;
|
|
436
|
-
const log = (level, message, extra) => {
|
|
437
|
-
const payload = {
|
|
438
|
-
body: {
|
|
439
|
-
service: serviceName,
|
|
440
|
-
level,
|
|
441
|
-
message,
|
|
442
|
-
extra
|
|
443
|
-
}
|
|
444
|
-
};
|
|
445
|
-
void client.app.log(payload).catch(() => void 0);
|
|
446
|
-
};
|
|
447
|
-
const logDebug = (message, extra) => {
|
|
448
|
-
log("debug", message, extra);
|
|
449
|
-
};
|
|
450
|
-
const logInfo = (message, extra) => {
|
|
451
|
-
log("info", message, extra);
|
|
452
|
-
};
|
|
453
|
-
const logError = (message, extra) => {
|
|
454
|
-
log("error", message, extra);
|
|
455
|
-
};
|
|
456
|
-
logInfo("Feishu notifier plugin loading", { directory });
|
|
457
|
-
const ensureConfig = () => {
|
|
458
|
-
if (configCache || configError) {
|
|
459
|
-
return;
|
|
460
|
-
}
|
|
461
|
-
try {
|
|
462
|
-
configCache = loadConfigWithSource({ directory });
|
|
463
|
-
logInfo("Feishu notifier plugin initialized", { sources: configCache.sources.map((s) => s.type) });
|
|
464
|
-
logDebug("Loaded Feishu config", { sources: configCache.sources });
|
|
465
|
-
} catch (error) {
|
|
466
|
-
configError = error instanceof Error ? error : new Error(String(error));
|
|
467
|
-
logError("Feishu config error", { error: configError.message });
|
|
468
|
-
}
|
|
469
|
-
};
|
|
470
|
-
ensureConfig();
|
|
471
|
-
return {
|
|
472
|
-
event: async ({ event }) => {
|
|
473
|
-
recordEventContext(event);
|
|
474
|
-
logDebug("Event received", { eventType: event.type });
|
|
475
|
-
let notificationType = mapEventToNotification(event.type);
|
|
476
|
-
notificationType = notificationType ?? mapCompletionEventToNotification(event);
|
|
477
|
-
if (!notificationType) {
|
|
478
|
-
logDebug("Event ignored", { eventType: event.type });
|
|
479
|
-
return;
|
|
480
|
-
}
|
|
481
|
-
logDebug("Event mapped to notification", {
|
|
482
|
-
eventType: event.type,
|
|
483
|
-
notificationType
|
|
484
|
-
});
|
|
485
|
-
ensureConfig();
|
|
486
|
-
if (configError) {
|
|
487
|
-
logError("Feishu config error (cached)", { error: configError.message });
|
|
488
|
-
return;
|
|
489
|
-
}
|
|
490
|
-
if (!configCache) {
|
|
491
|
-
logError("Feishu config not loaded");
|
|
492
|
-
return;
|
|
493
|
-
}
|
|
494
|
-
const { text, title, richContent } = await buildNotification(
|
|
495
|
-
notificationType,
|
|
496
|
-
event,
|
|
497
|
-
directory,
|
|
498
|
-
{ session: client.session }
|
|
499
|
-
);
|
|
500
|
-
logDebug("Sending Feishu notification", {
|
|
501
|
-
eventType: event.type,
|
|
502
|
-
notificationType,
|
|
503
|
-
directory,
|
|
504
|
-
hasRichContent: !!richContent
|
|
505
|
-
});
|
|
506
|
-
try {
|
|
507
|
-
let response;
|
|
508
|
-
if (richContent) {
|
|
509
|
-
try {
|
|
510
|
-
logDebug("Attempting to send rich text message", {
|
|
511
|
-
richContentType: typeof richContent,
|
|
512
|
-
hasPost: !!richContent.post,
|
|
513
|
-
hasZhCn: !!richContent.post?.zh_cn,
|
|
514
|
-
titleLength: richContent.post?.zh_cn?.title?.length ?? 0,
|
|
515
|
-
contentLength: richContent.post?.zh_cn?.content?.length ?? 0
|
|
516
|
-
});
|
|
517
|
-
response = await sendRichTextMessage(configCache.config, text, title, richContent);
|
|
518
|
-
logDebug("Feishu rich notification sent", {
|
|
519
|
-
messageId: response.data?.message_id ?? null
|
|
520
|
-
});
|
|
521
|
-
} catch (richError) {
|
|
522
|
-
logDebug("Rich text message failed, falling back to text", {
|
|
523
|
-
error: richError instanceof Error ? richError.message : String(richError),
|
|
524
|
-
stack: richError instanceof Error ? richError.stack : void 0,
|
|
525
|
-
name: richError instanceof Error ? richError.name : void 0
|
|
526
|
-
});
|
|
527
|
-
response = await sendTextMessage(configCache.config, text);
|
|
528
|
-
logDebug("Feishu text notification sent (fallback)", {
|
|
529
|
-
messageId: response.data?.message_id ?? null
|
|
530
|
-
});
|
|
531
|
-
}
|
|
532
|
-
} else {
|
|
533
|
-
response = await sendTextMessage(configCache.config, text);
|
|
534
|
-
logDebug("Feishu text notification sent", {
|
|
535
|
-
messageId: response.data?.message_id ?? null
|
|
536
|
-
});
|
|
537
|
-
}
|
|
538
|
-
} catch (error) {
|
|
539
|
-
logError("Failed to send Feishu notification", {
|
|
540
|
-
error: String(error)
|
|
541
|
-
});
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
};
|
|
545
|
-
};
|
|
546
|
-
var index_default = FeishuNotifierPlugin;
|
|
547
|
-
export {
|
|
548
|
-
index_default as default
|
|
549
|
-
};
|
|
550
|
-
//# sourceMappingURL=index.js.map
|