github-actions[bot] commited on
Commit
025b1cc
·
1 Parent(s): a9c5abb

Update from GitHub Actions

Browse files
components.d.ts CHANGED
@@ -16,6 +16,7 @@ declare module 'vue' {
16
  TCard: typeof import('tdesign-vue-next')['Card']
17
  TCheckbox: typeof import('tdesign-vue-next')['Checkbox']
18
  TContent: typeof import('tdesign-vue-next')['Content']
 
19
  TDialog: typeof import('tdesign-vue-next')['Dialog']
20
  TDivider: typeof import('tdesign-vue-next')['Divider']
21
  TDrawer: typeof import('tdesign-vue-next')['Drawer']
@@ -26,6 +27,7 @@ declare module 'vue' {
26
  THeader: typeof import('tdesign-vue-next')['Header']
27
  TIcon: typeof import('tdesign-vue-next')['Icon']
28
  TInput: typeof import('tdesign-vue-next')['Input']
 
29
  TLayout: typeof import('tdesign-vue-next')['Layout']
30
  TList: typeof import('tdesign-vue-next')['List']
31
  TListItem: typeof import('tdesign-vue-next')['ListItem']
@@ -34,6 +36,12 @@ declare module 'vue' {
34
  TMenuItem: typeof import('tdesign-vue-next')['MenuItem']
35
  TPagination: typeof import('tdesign-vue-next')['Pagination']
36
  TTable: typeof import('tdesign-vue-next')['Table']
 
 
37
  TTextarea: typeof import('tdesign-vue-next')['Textarea']
 
 
 
 
38
  }
39
  }
 
16
  TCard: typeof import('tdesign-vue-next')['Card']
17
  TCheckbox: typeof import('tdesign-vue-next')['Checkbox']
18
  TContent: typeof import('tdesign-vue-next')['Content']
19
+ TDatePicker: typeof import('tdesign-vue-next')['DatePicker']
20
  TDialog: typeof import('tdesign-vue-next')['Dialog']
21
  TDivider: typeof import('tdesign-vue-next')['Divider']
22
  TDrawer: typeof import('tdesign-vue-next')['Drawer']
 
27
  THeader: typeof import('tdesign-vue-next')['Header']
28
  TIcon: typeof import('tdesign-vue-next')['Icon']
29
  TInput: typeof import('tdesign-vue-next')['Input']
30
+ TInputNumber: typeof import('tdesign-vue-next')['InputNumber']
31
  TLayout: typeof import('tdesign-vue-next')['Layout']
32
  TList: typeof import('tdesign-vue-next')['List']
33
  TListItem: typeof import('tdesign-vue-next')['ListItem']
 
36
  TMenuItem: typeof import('tdesign-vue-next')['MenuItem']
37
  TPagination: typeof import('tdesign-vue-next')['Pagination']
38
  TTable: typeof import('tdesign-vue-next')['Table']
39
+ TTbody: typeof import('tdesign-vue-next')['Tbody']
40
+ TTd: typeof import('tdesign-vue-next')['Td']
41
  TTextarea: typeof import('tdesign-vue-next')['Textarea']
42
+ TTh: typeof import('tdesign-vue-next')['Th']
43
+ TThead: typeof import('tdesign-vue-next')['Thead']
44
+ TTimePicker: typeof import('tdesign-vue-next')['TimePicker']
45
+ TTr: typeof import('tdesign-vue-next')['Tr']
46
  }
47
  }
functions/types.d.ts CHANGED
@@ -6,7 +6,6 @@ interface KVNamespace {
6
  }
7
 
8
  interface Env {
9
- SEND_PASSWORD:string;
10
  API_TOKEN: string; // API 访问令牌
11
  JWT_SECRET: string; // JWT 密钥
12
  USER_NAME: string; // 用户名
 
6
  }
7
 
8
  interface Env {
 
9
  API_TOKEN: string; // API 访问令牌
10
  JWT_SECRET: string; // JWT 密钥
11
  USER_NAME: string; // 用户名
functions/utils/feishu.ts ADDED
@@ -0,0 +1,407 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import crypto from 'node:crypto';
2
+ import { v4 as uuidv4 } from 'uuid';
3
+
4
+ // 响应数据的接口定义
5
+ interface TenantAccessTokenResponse {
6
+ code: number;
7
+ expire: number;
8
+ msg: string;
9
+ tenant_access_token: string;
10
+ }
11
+
12
+ interface EventHeader {
13
+ event_id: string;
14
+ token: string;
15
+ create_time: string;
16
+ event_type: string;
17
+ tenant_key: string;
18
+ app_id: string;
19
+ }
20
+
21
+ interface SenderId {
22
+ open_id: string;
23
+ union_id: string;
24
+ user_id: string;
25
+ }
26
+
27
+ interface Sender {
28
+ sender_id: SenderId;
29
+ sender_type: string;
30
+ tenant_key: string;
31
+ }
32
+
33
+ interface Message {
34
+ chat_id: string;
35
+ chat_type: string;
36
+ content: string;
37
+ create_time: string;
38
+ message_id: string;
39
+ message_type: string;
40
+ update_time: string;
41
+ user_agent: string;
42
+ }
43
+
44
+ interface EventBody {
45
+ message: Message;
46
+ sender: Sender;
47
+ }
48
+
49
+ export interface EventMessage {
50
+ schema: string;
51
+ header: EventHeader;
52
+ event: EventBody;
53
+ }
54
+
55
+ /**
56
+ * 获取飞书租户访问令牌
57
+ * @param appId 应用 ID
58
+ * @param appSecret 应用密钥
59
+ * @returns Promise<TenantAccessTokenResponse>
60
+ */
61
+ export async function getTenantAccessToken(appId: string, appSecret: string): Promise<TenantAccessTokenResponse> {
62
+ const url = 'https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal';
63
+
64
+ const response = await fetch(url, {
65
+ method: 'POST',
66
+ headers: {
67
+ 'Content-Type': 'application/json',
68
+ },
69
+ body: JSON.stringify({
70
+ app_id: appId,
71
+ app_secret: appSecret,
72
+ }),
73
+ });
74
+
75
+ if (!response.ok) {
76
+ throw new Error(`HTTP error! status: ${response.status}`);
77
+ }
78
+
79
+ return await response.json() as TenantAccessTokenResponse
80
+ }
81
+
82
+
83
+
84
+
85
+
86
+
87
+ //https://open.feishu.cn/document/server-docs/event-subscription-guide/event-subscription-configure-/encrypt-key-encryption-configuration-case
88
+ export class AESCipher {
89
+ private decryptKey: Buffer;
90
+ private encryptKey: string;
91
+ constructor(key: string) {
92
+ this.encryptKey = key;
93
+ const hash = crypto.createHash('sha256');
94
+ hash.update(key);
95
+ this.decryptKey = hash.digest();
96
+ }
97
+
98
+ async decrypt(encrypt: string): Promise<string> {
99
+ // 将 base64 字符串转换为 Uint8Array
100
+ const encryptBuffer = Uint8Array.from(atob(encrypt), c => c.charCodeAt(0));
101
+
102
+ // 提取 IV (前16字节)
103
+ const iv = encryptBuffer.slice(0, 16);
104
+ // 提取加密数据
105
+ const data = encryptBuffer.slice(16);
106
+
107
+ // 从密钥字符串创建 CryptoKey
108
+ const cryptoKey = await crypto.subtle.importKey(
109
+ 'raw',
110
+ this.decryptKey,
111
+ { name: 'AES-CBC' },
112
+ false,
113
+ ['decrypt']
114
+ );
115
+
116
+ // 解密
117
+ const decryptedBuffer = await crypto.subtle.decrypt(
118
+ {
119
+ name: 'AES-CBC',
120
+ iv: iv
121
+ },
122
+ cryptoKey,
123
+ data
124
+ );
125
+
126
+ // 转换为字符串
127
+ return new TextDecoder().decode(decryptedBuffer);
128
+ }
129
+
130
+ calculateSignature(
131
+ timestamp: string,
132
+ nonce: string,
133
+ body: string
134
+ ): string {
135
+ const content = timestamp + nonce + this.encryptKey + body;
136
+ const sign = crypto.createHash('sha256').update(content).digest('hex');
137
+ return sign;
138
+ }
139
+ }
140
+
141
+
142
+
143
+
144
+ export async function handleAuth(request: Request,verificationToken:string,encryptKey:string ): Promise<Response | EventMessage> {
145
+
146
+ const { headers } = request
147
+ const contentType = headers.get('content-type') || ''
148
+ if (request.method !== 'POST' || !contentType.includes('application/json')) {
149
+ return new Response('Invalid request', { status: 400 })
150
+ }
151
+ const body: string = await request.text()
152
+ const cipher = new AESCipher(encryptKey)
153
+ let data = JSON.parse(body)
154
+ // 如果是加密事件,进行解密
155
+ if (data.encrypt) {
156
+ data = JSON.parse(await cipher.decrypt(data.encrypt))
157
+ }
158
+ const signature = headers.get('X-Lark-Signature');
159
+ //绑定的时候没有signature,所以得判断下
160
+ if (signature) {
161
+ const timestamp = headers.get('X-Lark-Request-Timestamp')!;
162
+ const nonce = headers.get('X-Lark-Request-Nonce')!;
163
+ const sign = cipher.calculateSignature(timestamp, nonce, body)
164
+
165
+ if (sign !== signature) {
166
+ return new Response('Invalid request', { status: 400 })
167
+ }
168
+ }
169
+
170
+ // 如果校验通过,返回 challenge 值
171
+ if (data.type && data.type === 'url_verification') {
172
+ return new Response(JSON.stringify({ challenge: data.challenge }), {
173
+ headers: { 'Content-Type': 'application/json' },
174
+ })
175
+ }
176
+
177
+ if (data.header.token != verificationToken) {
178
+ return new Response('Invalid request', { status: 400 })
179
+ }
180
+
181
+ return data;
182
+ }
183
+
184
+
185
+ // 定义响应接口
186
+ interface MessageResponse {
187
+ code: number;
188
+ msg: string;
189
+ data: {
190
+ body: {
191
+ content: string;
192
+ };
193
+ chat_id: string;
194
+ create_time: string;
195
+ deleted: boolean;
196
+ message_id: string;
197
+ msg_type: string;
198
+ sender: {
199
+ id: string;
200
+ id_type: string;
201
+ sender_type: string;
202
+ tenant_key: string;
203
+ };
204
+ update_time: string;
205
+ updated: boolean;
206
+ };
207
+ }
208
+
209
+ // 定义请求参数接口
210
+ interface MessageRequest {
211
+ content: string;
212
+ msg_type: string;
213
+ receive_id: string;
214
+ uuid: string;
215
+ }
216
+
217
+ // 发送消息函数
218
+ //https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/im-v1/message/create
219
+ export async function sendFeishuMessageText(token: string, receive_id: string, content: string): Promise<MessageResponse> {
220
+ const url = 'https://open.feishu.cn/open-apis/im/v1/messages';
221
+ try {
222
+ const response = await fetch(`${url}?receive_id_type=open_id`, {
223
+ method: 'POST',
224
+ headers: {
225
+ 'Content-Type': 'application/json',
226
+ 'Authorization': `Bearer ${token}`
227
+ },
228
+ body: JSON.stringify({
229
+ msg_type: "text",
230
+ content: JSON.stringify({ text: content }),
231
+ receive_id: receive_id, // 需要配置接收者ID
232
+ uuid: uuidv4()
233
+ } as MessageRequest)
234
+ });
235
+
236
+ if (!response.ok) {
237
+ throw new Error(`HTTP error! status: ${response.status}, ${await response.text()}`);
238
+ }
239
+ const data: MessageResponse = await response.json() as MessageResponse;
240
+
241
+ return data;
242
+ } catch (error) {
243
+ console.error('发送消息失败:', error);
244
+ throw error;
245
+ }
246
+ }
247
+
248
+
249
+
250
+
251
+ interface LarkCardRequest {
252
+ type: string;
253
+ data: string;
254
+ }
255
+ interface LarkCardResponse {
256
+ code: number;
257
+ data: {
258
+ card_id: string;
259
+ };
260
+ msg: string;
261
+ }
262
+
263
+ //第一步创建卡片
264
+ //https://open.feishu.cn/document/uAjLw4CM/ukzMukzMukzM/feishu-cards/streaming-updates-openapi-overview#5ac65a50
265
+ export async function sendFeishuCreateCard(token: string, title: string, content: string, element_id: string = "markdown_content"): Promise<LarkCardResponse> {
266
+
267
+ const url = 'https://open.feishu.cn/open-apis/cardkit/v1/cards';
268
+ const requestBody: LarkCardRequest = {
269
+ type: "card_json",
270
+ data: JSON.stringify({
271
+ schema: "2.0",
272
+ header: {
273
+ title: {
274
+ content: title,
275
+ tag: "plain_text"
276
+ }
277
+ },
278
+ config: {
279
+ streaming_mode: true,
280
+ summary: {
281
+ content: ""
282
+ }
283
+ },
284
+ body: {
285
+ elements: [
286
+ {
287
+ tag: "markdown",
288
+ content: content,
289
+ element_id: element_id
290
+ }
291
+ ]
292
+ }
293
+ })
294
+ };
295
+
296
+ try {
297
+ const response = await fetch(url, {
298
+ method: 'POST',
299
+ headers: {
300
+ 'Authorization': `Bearer ${token}`,
301
+ 'Content-Type': 'application/json; charset=utf-8'
302
+ },
303
+ body: JSON.stringify(requestBody)
304
+ });
305
+
306
+ if (!response.ok) {
307
+ throw new Error(`HTTP error! status: ${response.status}`);
308
+ }
309
+
310
+ const data = await response.json();
311
+ return data as LarkCardResponse;
312
+ } catch (error) {
313
+ console.error('Error:', error instanceof Error ? error.message : 'Unknown error');
314
+ throw error;
315
+ }
316
+ }
317
+
318
+ //第二部发送消息
319
+ export async function sendFeishuMessageCard(token: string, card_id: string, receive_id: string): Promise<MessageResponse> {
320
+ const url = 'https://open.feishu.cn/open-apis/im/v1/messages';
321
+ try {
322
+ const response = await fetch(`${url}?receive_id_type=open_id`, {
323
+ method: 'POST',
324
+ headers: {
325
+ 'Content-Type': 'application/json',
326
+ 'Authorization': `Bearer ${token}`
327
+ },
328
+ body: JSON.stringify({
329
+ msg_type: "interactive",
330
+ content: JSON.stringify({
331
+ type: "card", data: {
332
+ "card_id": card_id
333
+ }
334
+ }),
335
+ receive_id: receive_id, // 需要配置接收者ID
336
+ uuid: uuidv4()
337
+ } as MessageRequest)
338
+ });
339
+
340
+ if (!response.ok) {
341
+ throw new Error(`HTTP error! status: ${response.status}, ${await response.text()}`);
342
+ }
343
+ const data: MessageResponse = await response.json() as MessageResponse;
344
+ return data;
345
+ } catch (error) {
346
+ console.error('发送消息失败:', error);
347
+ throw error;
348
+ }
349
+ }
350
+
351
+ //第三部更新卡片
352
+ interface UpdateCardParams {
353
+ token: string;
354
+ card_id: string;
355
+ element_id: string;
356
+ sequence: number;
357
+ content: string;
358
+ }
359
+
360
+ interface UpdateCardResponse {
361
+ code: number;
362
+ data: Record<string, unknown>;
363
+ msg: string;
364
+ }
365
+
366
+ export async function updateMarkdownCard({
367
+ token,
368
+ card_id,
369
+ element_id,
370
+ sequence,
371
+ content
372
+ }: UpdateCardParams): Promise<UpdateCardResponse> {
373
+ const url = `https://open.feishu.cn/open-apis/cardkit/v1/cards/${card_id}/elements/${element_id}/content`;
374
+ const headers = {
375
+ 'Authorization': `Bearer ${token}`,
376
+ 'Content-Type': 'application/json'
377
+ };
378
+ const body = JSON.stringify({
379
+ content,
380
+ sequence,
381
+ uuid: uuidv4()
382
+ });
383
+
384
+ try {
385
+ const response = await fetch(url, {
386
+ method: 'PUT',
387
+ headers,
388
+ body
389
+ });
390
+
391
+ if (!response.ok) {
392
+ throw new Error(`HTTP error! status: ${response.status}`);
393
+ }
394
+
395
+ const result: UpdateCardResponse = await response.json() as UpdateCardResponse;
396
+ if (result.code !== 0) {
397
+ throw new Error(`API error: ${result.msg}`);
398
+ }
399
+
400
+ return result;
401
+ } catch (error) {
402
+ console.error('Update failed:', error);
403
+ throw error;
404
+ }
405
+ }
406
+
407
+
functions/utils/mail.ts CHANGED
@@ -142,9 +142,6 @@ async function parseEmail(email: any): Promise<ParsedEmail> {
142
 
143
  export async function getEmails(accessToken: string, limit = 50): Promise<ParsedEmail[]> {
144
  const endpoint = 'https://graph.microsoft.com/v1.0/me/messages';
145
-
146
-
147
- console.log(accessToken)
148
  const response = await fetch(`${endpoint}?$top=${limit}`, {
149
  method: 'GET',
150
  headers: {
@@ -155,14 +152,10 @@ export async function getEmails(accessToken: string, limit = 50): Promise<Parsed
155
 
156
  const data = await response.json() as any;
157
 
158
-
159
-
160
  if (!response.ok) {
161
  throw new Error(`获取邮件失败: ${data.error?.message}`);
162
  }
163
-
164
  const emails = data.value;
165
- console.log(emails)
166
  const parsedEmails = await Promise.all(emails.map(parseEmail));
167
  return parsedEmails;
168
  }
 
142
 
143
  export async function getEmails(accessToken: string, limit = 50): Promise<ParsedEmail[]> {
144
  const endpoint = 'https://graph.microsoft.com/v1.0/me/messages';
 
 
 
145
  const response = await fetch(`${endpoint}?$top=${limit}`, {
146
  method: 'GET',
147
  headers: {
 
152
 
153
  const data = await response.json() as any;
154
 
 
 
155
  if (!response.ok) {
156
  throw new Error(`获取邮件失败: ${data.error?.message}`);
157
  }
 
158
  const emails = data.value;
 
159
  const parsedEmails = await Promise.all(emails.map(parseEmail));
160
  return parsedEmails;
161
  }
index.ts CHANGED
@@ -12,6 +12,8 @@ import { serveStatic } from '@hono/node-server/serve-static'
12
  import path from 'path'
13
  import { fileURLToPath } from 'url'
14
  import { dirname } from 'path'
 
 
15
 
16
  // 导入所有路由处理函数
17
  import { onRequest as handleAccount } from './functions/api/account.js'
@@ -48,7 +50,7 @@ const storage = createStorage({
48
  }),
49
  });
50
 
51
- var kv: KVNamespace = {
52
  get: async (key: string) => {
53
  const value = await storage.getItemRaw(key);
54
  return value as string;
@@ -179,6 +181,110 @@ app.get('/*', serveStatic({
179
  }))
180
 
181
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
  // 启动服务器
183
  const port = parseInt(process.env.PORT || '8788')
184
  serve({
@@ -186,6 +292,12 @@ serve({
186
  port
187
  }, () => {
188
  console.log(`Server running at http://localhost:${port}`)
 
 
 
 
 
 
189
  })
190
 
191
  export default app
 
12
  import path from 'path'
13
  import { fileURLToPath } from 'url'
14
  import { dirname } from 'path'
15
+ import { getTenantAccessToken, sendFeishuMessageText } from './functions/utils/feishu.js'
16
+ import { get_access_token, getEmails } from './functions/utils/mail.js'
17
 
18
  // 导入所有路由处理函数
19
  import { onRequest as handleAccount } from './functions/api/account.js'
 
50
  }),
51
  });
52
 
53
+ const kv: KVNamespace = {
54
  get: async (key: string) => {
55
  const value = await storage.getItemRaw(key);
56
  return value as string;
 
181
  }))
182
 
183
 
184
+ // 邮箱监控功能
185
+ async function checkEmailsForNewMessages(env: Env) {
186
+ try {
187
+ // 获取设置
188
+ const KV_KEY = "settings";
189
+ const settingsStr = await env.KV.get(KV_KEY);
190
+ if (!settingsStr) {
191
+ console.log('未找到设置信息');
192
+ return;
193
+ }
194
+
195
+ const settings = JSON.parse(settingsStr);
196
+ if (!settings.emailMonitor || !Array.isArray(settings.emailMonitor) || settings.emailMonitor.length === 0) {
197
+ console.log('未配置监控邮箱');
198
+ return;
199
+ }
200
+
201
+ // 获取飞书访问令牌
202
+ const feishuConfig = settings.feishu;
203
+ if (!feishuConfig || !feishuConfig.app_id || !feishuConfig.app_secret || !feishuConfig.receive_id) {
204
+ console.log('飞书配置不完整');
205
+ return;
206
+ }
207
+
208
+ const tokenResponse = await getTenantAccessToken(feishuConfig.app_id, feishuConfig.app_secret);
209
+ if (tokenResponse.code !== 0) {
210
+ console.log('获取飞书访问令牌失败:', tokenResponse.msg);
211
+ return;
212
+ }
213
+
214
+ const feishuToken = tokenResponse.tenant_access_token;
215
+ const now = new Date();
216
+ const currentTime = now.toISOString();
217
+
218
+ // 遍历所有监控邮箱
219
+ for (const monitor of settings.emailMonitor) {
220
+ if (!monitor.email) continue;
221
+
222
+ // 检查是否到了检查间隔
223
+ let shouldCheck = false;
224
+ if (monitor.lastCheckTime) {
225
+ const lastCheck = new Date(monitor.lastCheckTime);
226
+ const diffMinutes = Math.floor((now.getTime() - lastCheck.getTime()) / (1000 * 60));
227
+ shouldCheck = diffMinutes >= (monitor.checkInterval || 60);
228
+ } else {
229
+ shouldCheck = true; // 如果从未检查过,则立即检查
230
+ }
231
+
232
+ if (shouldCheck) {
233
+ try {
234
+ // 获取邮箱的访问令牌
235
+ const tokenInfoStr = await env.KV.get(`refresh_token_${monitor.email}`);
236
+ if (!tokenInfoStr) {
237
+ console.log(`邮箱 ${monitor.email} 未授权`);
238
+ continue;
239
+ }
240
+
241
+ const tokenInfo = JSON.parse(tokenInfoStr);
242
+ const access_token = await get_access_token(tokenInfo, env.ENTRA_CLIENT_ID, env.ENTRA_CLIENT_SECRET);
243
+
244
+ // 获取最新邮件
245
+ const emails = await getEmails(access_token, 1);
246
+ if (emails.length === 0) {
247
+ console.log(`邮箱 ${monitor.email} 没有邮件`);
248
+ continue;
249
+ }
250
+
251
+ const latestEmail = emails[0];
252
+
253
+ // 更新检查时间
254
+ monitor.lastCheckTime = currentTime;
255
+
256
+
257
+ // 获取最新邮件的时间戳
258
+ const latestEmailTime = new Date(latestEmail.date.received || latestEmail.date.created || latestEmail.date.sent || latestEmail.date.modified).getTime();
259
+ const lastEmailTime = monitor.lastEmailTimestamp ? new Date(monitor.lastEmailTimestamp).getTime() : 0;
260
+ console.log(JSON.stringify(latestEmail),latestEmailTime,lastEmailTime)
261
+ // 如果最新邮件的时间戳大于用户设置的时间戳,且ID不同,则认为有新邮件
262
+ if (latestEmailTime > lastEmailTime && (!monitor.lastEmailId || monitor.lastEmailId !== latestEmail.id)) {
263
+ // 只更新最新邮件ID,不更新lastEmailTimestamp(该值由用户手动设置)
264
+ monitor.lastEmailId = latestEmail.id;
265
+
266
+ // 发送飞书通知
267
+ const notificationContent = `邮箱 ${monitor.email} 收到新邮件\n主题: ${latestEmail.subject}\n发件人: ${latestEmail.from}\n时间: ${latestEmail.date.received}\n预览: ${latestEmail.preview || '无预览内容'}`;
268
+
269
+ await sendFeishuMessageText(feishuToken, feishuConfig.receive_id, notificationContent);
270
+ console.log(`已发送邮箱 ${monitor.email} 的新邮件通知`);
271
+ } else {
272
+ console.log(`邮箱 ${monitor.email} 没有新邮件`);
273
+ }
274
+ } catch (error) {
275
+ console.error(`检查邮箱 ${monitor.email} 失败:`, error);
276
+ }
277
+ }
278
+ }
279
+
280
+ // 保存更新后的设置
281
+ await env.KV.put(KV_KEY, JSON.stringify(settings));
282
+ console.log('邮箱监控完成');
283
+ } catch (error) {
284
+ console.error('邮箱监控出错:', error);
285
+ }
286
+ }
287
+
288
  // 启动服务器
289
  const port = parseInt(process.env.PORT || '8788')
290
  serve({
 
292
  port
293
  }, () => {
294
  console.log(`Server running at http://localhost:${port}`)
295
+
296
+ // 启动定时监控邮箱
297
+ setInterval(() => {
298
+ const envWithKV = { ...process.env, KV: kv };
299
+ checkEmailsForNewMessages(envWithKV as unknown as Env);
300
+ }, 60000); // 每分钟检查一次
301
  })
302
 
303
  export default app
package-lock.json CHANGED
@@ -13,6 +13,7 @@
13
  "@tailwindcss/vite": "^4.0.14",
14
  "dotenv": "^16.4.7",
15
  "hono": "^4.7.4",
 
16
  "monaco-editor": "^0.52.2",
17
  "pinia": "^3.0.1",
18
  "playwright": "^1.51.0",
@@ -20,6 +21,7 @@
20
  "tailwindcss": "^4.0.14",
21
  "tdesign-vue-next": "^1.11.4",
22
  "unstorage": "^1.15.0",
 
23
  "vue": "^3.5.13",
24
  "vue-router": "^4.5.0"
25
  },
@@ -3424,6 +3426,27 @@
3424
  "pathe": "^2.0.1"
3425
  }
3426
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3427
  "node_modules/monaco-editor": {
3428
  "version": "0.52.2",
3429
  "resolved": "https://registry.npmmirror.com/monaco-editor/-/monaco-editor-0.52.2.tgz",
@@ -5056,6 +5079,19 @@
5056
  "url": "https://paulmillr.com/funding/"
5057
  }
5058
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
5059
  "node_modules/validator": {
5060
  "version": "13.12.0",
5061
  "resolved": "https://registry.npmmirror.com/validator/-/validator-13.12.0.tgz",
 
13
  "@tailwindcss/vite": "^4.0.14",
14
  "dotenv": "^16.4.7",
15
  "hono": "^4.7.4",
16
+ "moment-timezone": "^0.5.48",
17
  "monaco-editor": "^0.52.2",
18
  "pinia": "^3.0.1",
19
  "playwright": "^1.51.0",
 
21
  "tailwindcss": "^4.0.14",
22
  "tdesign-vue-next": "^1.11.4",
23
  "unstorage": "^1.15.0",
24
+ "uuid": "^11.1.0",
25
  "vue": "^3.5.13",
26
  "vue-router": "^4.5.0"
27
  },
 
3426
  "pathe": "^2.0.1"
3427
  }
3428
  },
3429
+ "node_modules/moment": {
3430
+ "version": "2.30.1",
3431
+ "resolved": "https://registry.npmmirror.com/moment/-/moment-2.30.1.tgz",
3432
+ "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
3433
+ "license": "MIT",
3434
+ "engines": {
3435
+ "node": "*"
3436
+ }
3437
+ },
3438
+ "node_modules/moment-timezone": {
3439
+ "version": "0.5.48",
3440
+ "resolved": "https://registry.npmmirror.com/moment-timezone/-/moment-timezone-0.5.48.tgz",
3441
+ "integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==",
3442
+ "license": "MIT",
3443
+ "dependencies": {
3444
+ "moment": "^2.29.4"
3445
+ },
3446
+ "engines": {
3447
+ "node": "*"
3448
+ }
3449
+ },
3450
  "node_modules/monaco-editor": {
3451
  "version": "0.52.2",
3452
  "resolved": "https://registry.npmmirror.com/monaco-editor/-/monaco-editor-0.52.2.tgz",
 
5079
  "url": "https://paulmillr.com/funding/"
5080
  }
5081
  },
5082
+ "node_modules/uuid": {
5083
+ "version": "11.1.0",
5084
+ "resolved": "https://registry.npmmirror.com/uuid/-/uuid-11.1.0.tgz",
5085
+ "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
5086
+ "funding": [
5087
+ "https://github.com/sponsors/broofa",
5088
+ "https://github.com/sponsors/ctavan"
5089
+ ],
5090
+ "license": "MIT",
5091
+ "bin": {
5092
+ "uuid": "dist/esm/bin/uuid"
5093
+ }
5094
+ },
5095
  "node_modules/validator": {
5096
  "version": "13.12.0",
5097
  "resolved": "https://registry.npmmirror.com/validator/-/validator-13.12.0.tgz",
package.json CHANGED
@@ -20,6 +20,7 @@
20
  "@tailwindcss/vite": "^4.0.14",
21
  "dotenv": "^16.4.7",
22
  "hono": "^4.7.4",
 
23
  "monaco-editor": "^0.52.2",
24
  "pinia": "^3.0.1",
25
  "playwright": "^1.51.0",
@@ -27,6 +28,7 @@
27
  "tailwindcss": "^4.0.14",
28
  "tdesign-vue-next": "^1.11.4",
29
  "unstorage": "^1.15.0",
 
30
  "vue": "^3.5.13",
31
  "vue-router": "^4.5.0"
32
  },
 
20
  "@tailwindcss/vite": "^4.0.14",
21
  "dotenv": "^16.4.7",
22
  "hono": "^4.7.4",
23
+ "moment-timezone": "^0.5.48",
24
  "monaco-editor": "^0.52.2",
25
  "pinia": "^3.0.1",
26
  "playwright": "^1.51.0",
 
28
  "tailwindcss": "^4.0.14",
29
  "tdesign-vue-next": "^1.11.4",
30
  "unstorage": "^1.15.0",
31
+ "uuid": "^11.1.0",
32
  "vue": "^3.5.13",
33
  "vue-router": "^4.5.0"
34
  },
src/services/settingApi.ts CHANGED
@@ -1,5 +1,18 @@
1
  import { API_BASE_URL, getHeaders, handleResponse } from './util';
2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  export interface Settings {
4
  /** 飞书配置 */
5
  feishu: {
@@ -13,7 +26,9 @@ export interface Settings {
13
  encrypt_key: string;
14
  /** 飞书机器人接收ID */
15
  receive_id: string;
16
- }
 
 
17
  }
18
 
19
 
 
1
  import { API_BASE_URL, getHeaders, handleResponse } from './util';
2
 
3
+ export interface EmailMonitorConfig {
4
+ /** 监控的邮箱地址 */
5
+ email: string;
6
+ /** 上次邮件时间戳 (用于判断新邮件) */
7
+ lastEmailTimestamp: string;
8
+ /** 检查间隔 (分钟) */
9
+ checkInterval: number;
10
+ /** 上次检查时间 */
11
+ lastCheckTime?: string;
12
+ /** 上次邮件ID */
13
+ lastEmailId?: string;
14
+ }
15
+
16
  export interface Settings {
17
  /** 飞书配置 */
18
  feishu: {
 
26
  encrypt_key: string;
27
  /** 飞书机器人接收ID */
28
  receive_id: string;
29
+ };
30
+ /** 邮箱监控配置 */
31
+ emailMonitor?: EmailMonitorConfig[];
32
  }
33
 
34
 
src/views/SettingView.vue CHANGED
@@ -2,6 +2,7 @@
2
  import { ref, onMounted } from 'vue';
3
  import { MessagePlugin } from 'tdesign-vue-next';
4
  import { settingApi, type Settings } from '../services/settingApi';
 
5
 
6
  const settings = ref<Settings>({
7
  feishu: {
@@ -10,7 +11,8 @@ const settings = ref<Settings>({
10
  verification_token: '',
11
  encrypt_key: '',
12
  receive_id: ''
13
- }
 
14
  });
15
 
16
  const loading = ref(false);
@@ -23,7 +25,8 @@ onMounted(async () => {
23
  feishu: {
24
  ...settings.value.feishu,
25
  ...data.feishu
26
- }
 
27
  };
28
  } catch (error) {
29
  MessagePlugin.error('获取设置失败');
@@ -45,6 +48,31 @@ const handleSave = async () => {
45
  loading.value = false;
46
  }
47
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  </script>
49
 
50
  <template>
@@ -69,13 +97,54 @@ const handleSave = async () => {
69
  <t-input v-model="settings.feishu.receive_id" placeholder="请输入飞书机器人接收ID" />
70
  </t-form-item>
71
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
 
73
  <t-form-item class="flex justify-center">
74
  <t-button theme="primary" type="submit" :loading="loading">保存设置</t-button>
75
  </t-form-item>
76
  </t-card>
77
-
78
-
79
  </t-form>
80
  </div>
81
  </template>
 
2
  import { ref, onMounted } from 'vue';
3
  import { MessagePlugin } from 'tdesign-vue-next';
4
  import { settingApi, type Settings } from '../services/settingApi';
5
+ import moment from 'moment-timezone';
6
 
7
  const settings = ref<Settings>({
8
  feishu: {
 
11
  verification_token: '',
12
  encrypt_key: '',
13
  receive_id: ''
14
+ },
15
+ emailMonitor: []
16
  });
17
 
18
  const loading = ref(false);
 
25
  feishu: {
26
  ...settings.value.feishu,
27
  ...data.feishu
28
+ },
29
+ emailMonitor: data.emailMonitor || []
30
  };
31
  } catch (error) {
32
  MessagePlugin.error('获取设置失败');
 
48
  loading.value = false;
49
  }
50
  };
51
+
52
+ // 添加新的监控邮箱
53
+ const addEmailMonitor = () => {
54
+ if (!settings.value.emailMonitor) {
55
+ settings.value.emailMonitor = [];
56
+ }
57
+ // 初始化为上海时区的当前时间
58
+ // 使用moment-timezone获取上海时区的当前时间
59
+ const shanghaiTime = moment().tz('Asia/Shanghai');
60
+
61
+ settings.value.emailMonitor.push({
62
+ email: '',
63
+ lastEmailTimestamp: shanghaiTime.format('YYYY-MM-DD HH:mm:ss'),
64
+ checkInterval: 60
65
+ });
66
+ };
67
+
68
+ // 删除监控邮箱
69
+ const removeEmailMonitor = (index: number) => {
70
+ if (settings.value.emailMonitor) {
71
+ settings.value.emailMonitor.splice(index, 1);
72
+ }
73
+ };
74
+
75
+
76
  </script>
77
 
78
  <template>
 
97
  <t-input v-model="settings.feishu.receive_id" placeholder="请输入飞书机器人接收ID" />
98
  </t-form-item>
99
 
100
+ <t-divider>监控邮箱配置</t-divider>
101
+ <div class="mb-4 flex justify-end">
102
+ <t-button theme="default" @click="addEmailMonitor">
103
+ <template #icon><t-icon name="add" /></template>
104
+ 添加监控邮箱
105
+ </t-button>
106
+ </div>
107
+
108
+ <div v-if="settings.emailMonitor && settings.emailMonitor.length > 0" class="mb-4">
109
+ <!-- 表头 -->
110
+ <div class="grid grid-cols-4 gap-4 mb-2 font-bold">
111
+ <div>邮箱地址</div>
112
+ <div>检查间隔(分钟)</div>
113
+ <div>邮件时间</div>
114
+ <div>操作</div>
115
+ </div>
116
+
117
+ <!-- 每一行邮箱配置 -->
118
+ <div v-for="(item, index) in settings.emailMonitor" :key="index" class="grid grid-cols-4 gap-4 mb-3 items-center">
119
+ <div>
120
+ <t-input v-model="item.email" placeholder="请输入邮箱地址" />
121
+ </div>
122
+ <div>
123
+ <t-input-number v-model="item.checkInterval" :min="1" :max="1440" placeholder="请输入检查间隔" />
124
+ </div>
125
+ <div>
126
+ <t-date-picker
127
+ v-model="item.lastEmailTimestamp"
128
+ enable-time-picker
129
+ clearable
130
+ placeholder="请选择上次邮件时间"
131
+ format="YYYY-MM-DD HH:mm:ss"
132
+ :timezone="{ name: 'Asia/Shanghai', offset: 8 }"
133
+ />
134
+ </div>
135
+ <div>
136
+ <t-button theme="danger" variant="text" @click="removeEmailMonitor(index)">
137
+ <template #icon><t-icon name="delete" /></template>
138
+ 删除
139
+ </t-button>
140
+ </div>
141
+ </div>
142
+ </div>
143
 
144
  <t-form-item class="flex justify-center">
145
  <t-button theme="primary" type="submit" :loading="loading">保存设置</t-button>
146
  </t-form-item>
147
  </t-card>
 
 
148
  </t-form>
149
  </div>
150
  </template>