github-actions[bot] commited on
Commit
7fc5208
·
1 Parent(s): 37f52e0

Update from GitHub Actions

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
.cnb.yml ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ master:
2
+ push:
3
+ - docker:
4
+ image: node:18
5
+ imports: https://cnb.cool/godgodgame/oci-private-key/-/blob/main/envs.yml
6
+ stages:
7
+ - name: 环境检查
8
+ script: echo $GITHUB_TOKEN_GK && echo $GITHUB_TOKEN && node -v && npm -v
9
+ - name: 将master分支同步更新到github的master分支
10
+ script: git push https://$GITHUB_TOKEN_GK:[email protected]/zhezzma/msmail.git HEAD:master
.editorconfig ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # http://editorconfig.org
2
+ root = true
3
+
4
+ [*]
5
+ indent_style = tab
6
+ end_of_line = lf
7
+ charset = utf-8
8
+ trim_trailing_whitespace = true
9
+ insert_final_newline = true
10
+
11
+ [*.yml]
12
+ indent_style = space
.prettierrc ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ {
2
+ "printWidth": 140,
3
+ "singleQuote": true,
4
+ "semi": true,
5
+ "useTabs": true
6
+ }
.vscode/settings.json ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ {
2
+ "files.associations": {
3
+ "wrangler.json": "jsonc"
4
+ }
5
+ }
Dockerfile ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 基础镜像:使用 Node.js 20 的 Alpine Linux 版本
2
+ FROM node:20-alpine
3
+
4
+ # 设置工作目录
5
+ WORKDIR /app
6
+
7
+ # 安装系统依赖
8
+ RUN apk add --no-cache \
9
+ # 基本构建工具
10
+ python3 \
11
+ make \
12
+ g++ \
13
+ # Playwright 依赖
14
+ chromium \
15
+ nss \
16
+ freetype \
17
+ freetype-dev \
18
+ harfbuzz \
19
+ ca-certificates \
20
+ ttf-freefont \
21
+ # 其他依赖
22
+ gcompat
23
+
24
+ # 设置 Playwright 的环境变量
25
+ ENV PLAYWRIGHT_BROWSERS_PATH=/usr/bin
26
+ ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
27
+ ENV PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium-browser
28
+ ENV PLAYWRIGHT_SKIP_BROWSER_VALIDATION=1
29
+
30
+ # 复制依赖文件并安装
31
+ COPY package*.json ./
32
+ COPY tsconfig*.json ./
33
+ COPY vite.config.ts ./
34
+ COPY index.html ./
35
+ COPY index.ts ./
36
+ COPY src/ ./src/
37
+ COPY public/ ./public/
38
+ COPY functions/ ./functions/
39
+
40
+
41
+ RUN npm install
42
+ RUN npm run build:server
43
+
44
+ # 创建非 root 用户和用户组
45
+ RUN addgroup -S -g 1001 nodejs && \
46
+ adduser -S -D -H -u 1001 -G nodejs hono
47
+
48
+ # 设置应用文件的所有权
49
+ RUN chown -R hono:nodejs /app
50
+
51
+ # 切换到非 root 用户
52
+ USER hono
53
+
54
+ # 声明容器要暴露的端口
55
+ EXPOSE 7860
56
+ ENV PORT=7860
57
+
58
+ # 启动应用
59
+ CMD ["node", "dist-server/index.js"]
auto-imports.d.ts ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ /* eslint-disable */
2
+ /* prettier-ignore */
3
+ // @ts-nocheck
4
+ // noinspection JSUnusedGlobalSymbols
5
+ // Generated by unplugin-auto-import
6
+ // biome-ignore lint: disable
7
+ export {}
8
+ declare global {
9
+
10
+ }
components.d.ts ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* eslint-disable */
2
+ // @ts-nocheck
3
+ // Generated by unplugin-vue-components
4
+ // Read more: https://github.com/vuejs/core/pull/3399
5
+ // biome-ignore lint: disable
6
+ export {}
7
+
8
+ /* prettier-ignore */
9
+ declare module 'vue' {
10
+ export interface GlobalComponents {
11
+ MonacoEditor: typeof import('./src/components/MonacoEditor.vue')['default']
12
+ RouterLink: typeof import('vue-router')['RouterLink']
13
+ RouterView: typeof import('vue-router')['RouterView']
14
+ TAside: typeof import('tdesign-vue-next')['Aside']
15
+ TButton: typeof import('tdesign-vue-next')['Button']
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']
22
+ TDropdown: typeof import('tdesign-vue-next')['Dropdown']
23
+ TFooter: typeof import('tdesign-vue-next')['Footer']
24
+ TForm: typeof import('tdesign-vue-next')['Form']
25
+ TFormItem: typeof import('tdesign-vue-next')['FormItem']
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']
32
+ TLoading: typeof import('tdesign-vue-next')['Loading']
33
+ TMenu: typeof import('tdesign-vue-next')['Menu']
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
+ }
functions/api/account.ts ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { authMiddleware } from "../utils/auth.js";
2
+ import { addCorsHeaders } from "../utils/cors.js";
3
+
4
+ export const onRequest = async (context: RouteContext): Promise<Response> => {
5
+ const request = context.request;
6
+ const env = context.env as Env;
7
+
8
+ const authResponse = await authMiddleware(request, env);
9
+ if (authResponse) {
10
+ return addCorsHeaders(authResponse);
11
+ }
12
+
13
+ const KV_KEY = "accounts"
14
+
15
+ try {
16
+ // GET 请求处理
17
+ if (request.method === 'GET') {
18
+ const accounts = await env.KV.get(KV_KEY);
19
+ return addCorsHeaders(new Response(accounts || '[]', {
20
+ status: 200,
21
+ headers: { 'Content-Type': 'application/json' }
22
+ }));
23
+ }
24
+
25
+ // POST 请求处理
26
+ if (request.method === 'POST') {
27
+ const data = await request.json();
28
+
29
+ // 验证数据格式
30
+ if (!Array.isArray(data)) {
31
+ return addCorsHeaders(new Response(JSON.stringify({ error: '无效的数据格式' }), {
32
+ status: 400,
33
+ headers: { 'Content-Type': 'application/json' }
34
+ }));
35
+ }
36
+
37
+ // 存储账号数据
38
+ await env.KV.put(KV_KEY, JSON.stringify(data));
39
+
40
+ return addCorsHeaders(new Response(JSON.stringify({ message: '保存成功' }), {
41
+ status: 200,
42
+ headers: { 'Content-Type': 'application/json' }
43
+ }));
44
+ }
45
+
46
+ // 不支持的请求方法
47
+ return addCorsHeaders(new Response(JSON.stringify({ error: '不支持的请求方法' }), {
48
+ status: 405,
49
+ headers: { 'Content-Type': 'application/json' }
50
+ }));
51
+
52
+ } catch (error) {
53
+ return addCorsHeaders(new Response(JSON.stringify({ error: '服务器内部错误' }), {
54
+ status: 500,
55
+ headers: { 'Content-Type': 'application/json' }
56
+ }));
57
+ }
58
+ };
functions/api/login.ts ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { generateToken } from '../utils/jwt.js';
2
+
3
+ export const onRequest = async (context: RouteContext): Promise<Response> => {
4
+ const request = context.request;
5
+ const env = context.env as Env;
6
+ try {
7
+ // 解析登录凭证
8
+ const credentials = await request.json() as LoginCredentials;
9
+ // 验证用户名和密码
10
+ if (credentials.username === env.USER_NAME && credentials.password === env.PASSWORD) {
11
+ // 生成JWT令牌
12
+ const token = await generateToken(credentials.username, env.JWT_SECRET);
13
+ return new Response(
14
+ JSON.stringify({
15
+ success: true,
16
+ token,
17
+ user: { username: credentials.username }
18
+ }),
19
+ {
20
+ status: 200,
21
+ headers: { 'Content-Type': 'application/json' }
22
+ }
23
+ );
24
+ }
25
+
26
+ // 登录失败: 无效的凭证
27
+ return new Response(
28
+ JSON.stringify({
29
+ success: false,
30
+ error: 'Invalid credentials'
31
+ }),
32
+ {
33
+ status: 401,
34
+ headers: { 'Content-Type': 'application/json' }
35
+ }
36
+ );
37
+ } catch (error) {
38
+ // 登录处理失败
39
+ console.error(`登录处理失败:`, error);
40
+ throw error;
41
+ }
42
+ }
functions/api/mail/all.ts ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { authApiToken, authMiddleware } from "../../utils/auth.js";
2
+ import { addCorsHeaders } from "../../utils/cors.js";
3
+ import { get_access_token, getEmails } from "../../utils/mail.js";
4
+
5
+ export const onRequest = async (context: RouteContext): Promise<Response> => {
6
+ const request = context.request;
7
+ const env: Env = context.env;
8
+
9
+ // 验证权限
10
+ const authResponse = await authMiddleware(request, env);
11
+ const apiResponse = await authApiToken(request, env);
12
+ if (authResponse && apiResponse) {
13
+ return addCorsHeaders(authResponse);
14
+ }
15
+
16
+ // 获取请求参数
17
+ const url = new URL(request.url);
18
+ const method = request.method;
19
+ const params: any = method === 'GET'
20
+ ? Object.fromEntries(url.searchParams)
21
+ : await request.json();
22
+
23
+ const { email, limit = 10 } = params;
24
+
25
+ // 检查必要参数
26
+ if (!email) {
27
+ return new Response(
28
+ JSON.stringify({
29
+ error: 'Missing required parameters: email'
30
+ }),
31
+ { status: 400 }
32
+ );
33
+ }
34
+
35
+ try {
36
+ // 从KV获取刷新令牌
37
+ const tokenInfoStr = await env.KV.get(`refresh_token_${email}`);
38
+ if (!tokenInfoStr) {
39
+ throw new Error("No refresh token found for this email");
40
+ }
41
+ const tokenInfo = JSON.parse(tokenInfoStr);
42
+ const access_token = await get_access_token(tokenInfo, env.ENTRA_CLIENT_ID, env.ENTRA_CLIENT_SECRET);
43
+ const emails = await getEmails(access_token, Number(limit));
44
+
45
+ return new Response(
46
+ JSON.stringify(emails),
47
+ {
48
+ status: 200,
49
+ headers: {
50
+ 'Content-Type': 'application/json',
51
+ 'Access-Control-Allow-Origin': '*'
52
+ }
53
+ }
54
+ );
55
+ } catch (error: any) {
56
+ return new Response(
57
+ JSON.stringify({ error: error.message }),
58
+ {
59
+ status: 500, headers: {
60
+ 'Content-Type': 'application/json',
61
+ 'Access-Control-Allow-Origin': '*'
62
+ }
63
+ }
64
+ );
65
+ }
66
+ };
functions/api/mail/auth.ts ADDED
@@ -0,0 +1,203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { authApiToken, authMiddleware } from "../../utils/auth.js";
2
+ import { addCorsHeaders } from "../../utils/cors.js";
3
+ import { chromium } from 'playwright';
4
+ import { getVerificationCode } from '../../utils/emailVerification.js';
5
+
6
+
7
+ export const onRequest = async (context: RouteContext): Promise<Response> => {
8
+ const request = context.request;
9
+ const env: Env = context.env;
10
+
11
+ // 验证权限
12
+ const authResponse = await authMiddleware(request, env);
13
+ const apiResponse = await authApiToken(request, env);
14
+ if (authResponse && apiResponse) {
15
+ return addCorsHeaders(authResponse);
16
+ }
17
+
18
+ try {
19
+ const { email } = await request.json() as any;
20
+ if (!email) {
21
+ throw new Error("Email is required");
22
+ }
23
+
24
+ // 获取账户信息
25
+ const accountsStr = await env.KV.get("accounts");
26
+ const accounts: any[] = accountsStr ? JSON.parse(accountsStr) : [];
27
+ const account = accounts.find(a => a.email === email);
28
+ if (!account) {
29
+ throw new Error("Account not found");
30
+ }
31
+ const clientId = env.ENTRA_CLIENT_ID;
32
+ const clientSecret = env.ENTRA_CLIENT_SECRET;
33
+ const redirectUri = env.AUTH_REDIRECT_URI;
34
+ let browser;
35
+ let context;
36
+ try {
37
+ browser = await chromium.launch({
38
+ headless: true,
39
+ args: [
40
+ '--no-sandbox',
41
+ '--disable-setuid-sandbox',
42
+ '--disable-dev-shm-usage',
43
+ '--disable-blink-features=AutomationControlled', // 禁用自动化特征
44
+ '--disable-infobars',
45
+ '--window-size=1920,1080'
46
+ ],
47
+ executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH, // 使用系统 Chromium
48
+ });
49
+ context = await browser.newContext();
50
+ const page = await context.newPage();
51
+
52
+ const authUrl = `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?` +
53
+ `client_id=${clientId}` +
54
+ `&response_type=code` +
55
+ `&redirect_uri=${encodeURIComponent(redirectUri)}` +
56
+ `&response_mode=query` +
57
+ `&scope=offline_access%20IMAP.AccessAsUser.All%20User.Read%20Mail.ReadWrite.Shared%20Mail.Send%20Mail.Read` +
58
+ `&prompt=consent` + //强制显示权限确认窗口
59
+ `&state=${email}`;
60
+
61
+
62
+ console.log(authUrl)
63
+
64
+ //开始认证
65
+ await page.goto(authUrl);
66
+ await page.fill('input[type="email"]', account.email);
67
+ await page.click('input[type="submit"]');
68
+
69
+ try {
70
+ await page.waitForSelector('#idA_PWD_SwitchToPassword', { timeout: 3000 });
71
+ await page.click('#idA_PWD_SwitchToPassword');
72
+ } catch (error) {
73
+ console.log(`没有切换到密码登录,继续执行: ${error}`);
74
+ }
75
+
76
+ try {
77
+ const timestamp = Math.floor(Date.now() / 1000);
78
+ await page.waitForSelector('#idA_PWD_SwitchToCredPicker', { timeout: 3000 });
79
+
80
+ try {
81
+ //处理email验证
82
+ const proof = [
83
+ {
84
+ "suffix": "godgodgame.com",
85
+ "apiUrl": "http://159.138.99.139:89/api/latest-email",
86
+ "token": env.PROOF_GODGODGAME_TOKEN
87
+ },
88
+ {
89
+ "suffix": "igiven.com",
90
+ "apiUrl": "http://159.138.99.139:90/api/latest-email",
91
+ "token": env.PROOF_IGIVEN_TOKEN
92
+ }
93
+ ]
94
+
95
+ const suffix = email.substring(email.indexOf('@') + 1);
96
+ const proofConfig = proof.find(p => p.suffix === suffix)!;
97
+ const verificationCode = await getVerificationCode(proofConfig.apiUrl, proofConfig.token!, account.proofEmail, timestamp);
98
+
99
+ await page.fill('input[type="tel"]', verificationCode);
100
+ await page.click('button[type="submit"]');
101
+ }
102
+ catch (error) {
103
+ throw new Error(`邮箱验证失败:${error}`)
104
+ }
105
+
106
+ await page.click('#idA_PWD_SwitchToCredPicker');
107
+ await page.click('#tileList > div:nth-child(2) Button');
108
+
109
+ } catch (error) {
110
+ console.log(`没有直接发邮件验证,继续执行: ${error}`);
111
+ }
112
+
113
+ //输入密码
114
+ await page.waitForURL("https://login.live.com/**")
115
+ await page.fill('input[type="password"]', account.password);
116
+ await page.click('button[type="submit"]');
117
+
118
+ //确认登录
119
+ await page.waitForURL('https://login.live.com/ppsecure/**')
120
+ await page.click('button[type="submit"]#acceptButton'); // 同意按钮
121
+
122
+ //同意授权
123
+ try {
124
+ // 等待同意授权页面,如果超时5秒则跳过
125
+ await page.waitForURL("https://account.live.com/Consent/**", { timeout: 5000 })
126
+ await page.click('button[type="submit"][data-testid="appConsentPrimaryButton"]');
127
+ } catch (error) {
128
+ // 如果超时或页面未出现,直接继续执行
129
+ console.log("Consent page not found or timeout, skipping...");
130
+ }
131
+ await page.waitForURL((url)=>{
132
+ console.log(url)
133
+ return url.href.startsWith(redirectUri);
134
+ })
135
+ } finally {
136
+ if (context) await context.close();
137
+ if (browser) await browser.close();
138
+ }
139
+
140
+
141
+ // 等待回调处理完成,检查 KV 中是否有对应的 code
142
+ let code = null;
143
+ const maxRetries = 30;
144
+ let retries = 0;
145
+
146
+ while (!code && retries < maxRetries) {
147
+ const codeKey = `code_${email}`;
148
+ code = await env.KV.get(codeKey);
149
+
150
+ if (!code) {
151
+ await new Promise(resolve => setTimeout(resolve, 1000));
152
+ retries++;
153
+ }
154
+ }
155
+
156
+ if (!code) {
157
+ throw new Error("Authorization timeout");
158
+ }
159
+
160
+ // 使用授权码获取刷新令牌
161
+ const tokenResponse = await fetch('https://login.microsoftonline.com/common/oauth2/v2.0/token', {
162
+ method: 'POST',
163
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
164
+ body: new URLSearchParams({
165
+ client_id: clientId,
166
+ client_secret: clientSecret,
167
+ code: code,
168
+ redirect_uri: redirectUri,
169
+ grant_type: 'authorization_code'
170
+ })
171
+ });
172
+
173
+ const tokenData: any = await tokenResponse.json();
174
+ if (!tokenData.refresh_token) {
175
+ throw new Error("Failed to get refresh token");
176
+ }
177
+ if (!tokenData.expires_in) {
178
+ throw new Error("Missing expires_in in token response");
179
+ }
180
+
181
+ // 存储刷新令牌和时间戳
182
+ const tokenInfo = {
183
+ ...tokenData,
184
+ timestamp: Date.now()
185
+ };
186
+ console.log(tokenInfo);
187
+ await env.KV.put(`refresh_token_${email}`, JSON.stringify(tokenInfo));
188
+
189
+ // 删除临时授权码
190
+ await env.KV.delete(`code_${email}`);
191
+
192
+ return new Response(JSON.stringify({ success: true }), {
193
+ status: 200,
194
+ headers: { 'Content-Type': 'application/json' }
195
+ });
196
+
197
+ } catch (error: any) {
198
+ return new Response(
199
+ JSON.stringify({ error: error.message }),
200
+ { status: 500 }
201
+ );
202
+ }
203
+ };
functions/api/mail/callback.ts ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ export const onRequest = async (context: RouteContext): Promise<Response> => {
3
+ const request = context.request;
4
+ const env: Env = context.env;
5
+
6
+ try {
7
+ const url = new URL(request.url);
8
+ const code = url.searchParams.get('code');
9
+ const email = url.searchParams.get('state'); // 假设我们在state参数中传递了email
10
+ const error_Msg = url.searchParams.get('error_description');
11
+ if (!code || !email) {
12
+ throw new Error(`Missing code or email:${error_Msg}`);
13
+ }
14
+ console.log(code,email)
15
+ // 将授权码存储到KV中
16
+ await env.KV.put(`code_${email}`, code, { expirationTtl: 300 }); // 5分钟过期
17
+
18
+ return new Response("Authorization successful. You can close this window.", {
19
+ status: 200,
20
+ headers: { 'Content-Type': 'text/plain' }
21
+ });
22
+
23
+ } catch (error: any) {
24
+ return new Response(
25
+ JSON.stringify({ error: error.message }),
26
+ { status: 500 }
27
+ );
28
+ }
29
+ };
functions/api/mail/new.ts ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { authApiToken, authMiddleware } from "../../utils/auth.js";
2
+ import { addCorsHeaders } from "../../utils/cors.js";
3
+ import { get_access_token, getEmails } from "../../utils/mail.js";
4
+
5
+
6
+ export const onRequest = async (context: RouteContext): Promise<Response> => {
7
+ const request = context.request;
8
+ const env: Env = context.env;
9
+
10
+ // 验证权限
11
+ const authResponse = await authMiddleware(request, env);
12
+ const apiResponse = await authApiToken(request, env);
13
+ if (authResponse && apiResponse) {
14
+
15
+ return addCorsHeaders(authResponse);
16
+ }
17
+
18
+ // 获取请求参数
19
+ const url = new URL(request.url);
20
+ const method = request.method;
21
+ const params: any = method === 'GET'
22
+ ? Object.fromEntries(url.searchParams)
23
+ : await request.json();
24
+
25
+ const { email, mailbox = "INBOX ", response_type = 'json' } = params;
26
+
27
+ // 检查必要参数
28
+ if (!email) {
29
+ return new Response(
30
+ JSON.stringify({
31
+ error: 'Missing required parameters: email'
32
+ }),
33
+ { status: 400 }
34
+ );
35
+ }
36
+ try {
37
+ // 从KV获取刷新令牌
38
+ const tokenInfoStr = await env.KV.get(`refresh_token_${email}`);
39
+ if (!tokenInfoStr) {
40
+ throw new Error("No refresh token found for this email");
41
+ }
42
+ const tokenInfo = JSON.parse(tokenInfoStr);
43
+ const access_token = await get_access_token(tokenInfo, env.ENTRA_CLIENT_ID, env.ENTRA_CLIENT_SECRET);
44
+ const emails = await getEmails(access_token, 1); // 只获取最新的一封邮件
45
+ return new Response(
46
+ JSON.stringify(emails[0] || null),
47
+ {
48
+ status: 200,
49
+ headers: {
50
+ 'Content-Type': 'application/json',
51
+ 'Access-Control-Allow-Origin': '*'
52
+ }
53
+ }
54
+ );
55
+ } catch (error: any) {
56
+ return new Response(
57
+ JSON.stringify({ error: error.message }),
58
+ {
59
+ status: 500, headers: {
60
+ 'Content-Type': 'application/json',
61
+ 'Access-Control-Allow-Origin': '*'
62
+ }
63
+ }
64
+ );
65
+ }
66
+ };
functions/api/mail/send.ts ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { authApiToken, authMiddleware } from "../../utils/auth.js";
2
+ import { addCorsHeaders } from "../../utils/cors.js";
3
+ import { get_access_token, sendEmail } from "../../utils/mail.js";
4
+
5
+ export const onRequest = async (context: RouteContext): Promise<Response> => {
6
+ const request = context.request;
7
+ const env: Env = context.env;
8
+
9
+ // 验证权限
10
+ const authResponse = await authMiddleware(request, env);
11
+ const apiResponse = await authApiToken(request, env);
12
+ if (authResponse && apiResponse) {
13
+ return addCorsHeaders(authResponse);
14
+ }
15
+
16
+ const method = request.method;
17
+ if (method !== 'POST') {
18
+ return new Response(
19
+ JSON.stringify({ error: 'Method not allowed' }),
20
+ { status: 405 }
21
+ );
22
+ }
23
+
24
+ try {
25
+ const { email, to, subject, body, isHtml = false } = await request.json() as any;
26
+
27
+ // 检查必要参数
28
+ if (!email || !to || !subject || !body) {
29
+ return new Response(
30
+ JSON.stringify({
31
+ error: 'Missing required parameters: email, to, subject, body'
32
+ }),
33
+ { status: 400 }
34
+ );
35
+ }
36
+
37
+ // 从KV获取刷新令牌
38
+ const tokenInfoStr = await env.KV.get(`refresh_token_${email}`);
39
+ if (!tokenInfoStr) {
40
+ throw new Error("No refresh token found for this email");
41
+ }
42
+ const tokenInfo = JSON.parse(tokenInfoStr);
43
+ const access_token = await get_access_token(tokenInfo, env.ENTRA_CLIENT_ID, env.ENTRA_CLIENT_SECRET);
44
+
45
+ // 发送邮件
46
+ await sendEmail(access_token, Array.isArray(to) ? to : [to], subject, body, isHtml);
47
+
48
+ return new Response(
49
+ JSON.stringify({ message: 'Email sent successfully' }),
50
+ {
51
+ status: 200,
52
+ headers: {
53
+ 'Content-Type': 'application/json',
54
+ 'Access-Control-Allow-Origin': '*'
55
+ }
56
+ }
57
+ );
58
+ } catch (error: any) {
59
+ return new Response(
60
+ JSON.stringify({ error: error.message }),
61
+ {
62
+ status: 500, headers: {
63
+ 'Content-Type': 'application/json',
64
+ 'Access-Control-Allow-Origin': '*'
65
+ }
66
+ }
67
+ );
68
+ }
69
+ };
70
+
functions/api/setting.ts ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import { authMiddleware } from "../utils/auth.js";
3
+ import { addCorsHeaders } from "../utils/cors.js";
4
+
5
+
6
+ export const onRequest = async (context: RouteContext): Promise<Response> => {
7
+ const request = context.request;
8
+ const env = context.env as Env;
9
+
10
+ const authResponse = await authMiddleware(request, env);
11
+ if (authResponse) {
12
+ return addCorsHeaders(authResponse);
13
+ }
14
+
15
+ const KV_KEY = "settings"
16
+
17
+ try {
18
+ // GET 请求处理
19
+ if (request.method === 'GET') {
20
+ const settings = await env.KV.get(KV_KEY);
21
+ return addCorsHeaders(new Response(settings || '{}', {
22
+ status: 200,
23
+ headers: { 'Content-Type': 'application/json' }
24
+ }));
25
+ }
26
+
27
+ // POST 请求处理
28
+ if (request.method === 'POST') {
29
+ const data = await request.json();
30
+ // 存储账号数据
31
+ await env.KV.put(KV_KEY, JSON.stringify(data));
32
+
33
+ return addCorsHeaders(new Response(JSON.stringify({ message: '保存成功' }), {
34
+ status: 200,
35
+ headers: { 'Content-Type': 'application/json' }
36
+ }));
37
+ }
38
+
39
+ // 不支持的请求方法
40
+ return addCorsHeaders(new Response(JSON.stringify({ error: '不支持的请求方法' }), {
41
+ status: 405,
42
+ headers: { 'Content-Type': 'application/json' }
43
+ }));
44
+
45
+ } catch (error) {
46
+ return addCorsHeaders(new Response(JSON.stringify({ error: '服务器内部错误' }), {
47
+ status: 500,
48
+ headers: { 'Content-Type': 'application/json' }
49
+ }));
50
+ }
51
+ };
functions/types.d.ts ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ interface KVNamespace {
3
+ put: (key: string, value: string, options?: { expiration?: number, expirationTtl?: number, metadata?: object }) => Promise<void>;
4
+ get: (key: string) => Promise<string | null>;
5
+ delete: (key: string) => Promise<void>;
6
+ }
7
+
8
+ interface Env {
9
+ SEND_PASSWORD:string;
10
+ API_TOKEN: string; // API 访问令牌
11
+ JWT_SECRET: string; // JWT 密钥
12
+ USER_NAME: string; // 用户名
13
+ PASSWORD: string; // 密码
14
+ ENTRA_CLIENT_ID:string;
15
+ ENTRA_CLIENT_SECRET:string;
16
+ AUTH_REDIRECT_URI:string;
17
+ PROOF_GODGODGAME_TOKEN:string;
18
+ PROOF_IGIVEN_TOKEN:string;
19
+ KV: KVNamespace;
20
+ ASSETS:any;
21
+ }
22
+
23
+ /**
24
+ * 登录凭证接口
25
+ */
26
+ interface LoginCredentials {
27
+ /** 用户名 */
28
+ username: string;
29
+ /** 密码 */
30
+ password: string;
31
+ }
32
+
33
+
34
+ interface RouteContext {
35
+ request: Request;
36
+ functionPath: string;
37
+ waitUntil: (promise: Promise<any>) => void;
38
+ passThroughOnException: () => void;
39
+ next: (input?: Request | string, init?: RequestInit) => Promise<Response>;
40
+ env: Env;
41
+ params: any;
42
+ data: any;
43
+ }
functions/utils/auth.ts ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { verifyToken } from './jwt.js';
2
+
3
+ /**
4
+ * 认证中间件
5
+ * @param request 请求对象
6
+ * @param env 环境变量
7
+ * @param requestId 请求ID
8
+ * @returns 如果认证失败返回错误响应,否则返回 null
9
+ */
10
+ export async function authMiddleware(request: Request, env: Env): Promise<Response | null> {
11
+ const isValid = await verifyToken(request, env.JWT_SECRET);
12
+ if (!isValid) {
13
+ return new Response(
14
+ JSON.stringify({ error: 'Unauthorized' }),
15
+ {
16
+ status: 401,
17
+ headers: { 'Content-Type': 'application/json' }
18
+ }
19
+ );
20
+ }
21
+
22
+ return null;
23
+ }
24
+
25
+ export async function authApiToken(request: Request, env: Env): Promise<Response | null> {
26
+ // 验证API令牌
27
+ const authHeader = request.headers.get('Authorization');
28
+ if (authHeader !== `Bearer ${env.API_TOKEN}`) {
29
+ return new Response(
30
+ JSON.stringify({ error: 'Unauthorized' }),
31
+ {
32
+ status: 401,
33
+ headers: { 'Content-Type': 'application/json' }
34
+ }
35
+ );
36
+ }
37
+
38
+ return null;
39
+ }
40
+
41
+
functions/utils/cors.ts ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * CORS 相关的响应头配置
3
+ */
4
+ export const CORS_HEADERS = {
5
+ 'Access-Control-Allow-Origin': '*', // 允许所有来源
6
+ 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', // 允许的HTTP方法
7
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Requested-With', // 允许的请求头
8
+ 'Access-Control-Max-Age': '86400', // 预检请求的有效期
9
+ };
10
+
11
+ /**
12
+ * 处理 OPTIONS 预检请求
13
+ */
14
+ export function handleOptions(): Response {
15
+ return new Response(null, {
16
+ status: 204,
17
+ headers: CORS_HEADERS
18
+ });
19
+ }
20
+
21
+ /**
22
+ * 为响应添加 CORS 头
23
+ * @param response 原始响应对象
24
+ * @returns 添加了 CORS 头的新响应对象
25
+ */
26
+ export function addCorsHeaders(response: Response): Response {
27
+ const newHeaders = new Headers(response.headers);
28
+ Object.entries(CORS_HEADERS).forEach(([key, value]) => {
29
+ newHeaders.set(key, value);
30
+ });
31
+
32
+ return new Response(response.body, {
33
+ status: response.status,
34
+ statusText: response.statusText,
35
+ headers: newHeaders
36
+ });
37
+ }
functions/utils/emailVerification.ts ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ export async function getVerificationCode(proofApi: string, apiKey: String, proofEmail: string, timestamp: number): Promise<string> {
4
+ const maxRetries = 30;
5
+ console.log(`开始获取验证码${proofApi},${apiKey},${timestamp},${new Date(timestamp * 1000)}`);
6
+ for (let i = 0; i < maxRetries; i++) {
7
+ try {
8
+ const params = new URLSearchParams({
9
+ to: proofEmail,
10
+ from: '[email protected]',
11
+ timestamp: timestamp.toString()
12
+ });
13
+
14
+ const url = `${proofApi}?${params.toString()}`;
15
+ const response = await fetch(url, {
16
+ headers: {
17
+ 'Authorization': `Bearer ${apiKey}`
18
+ },
19
+ method: 'GET'
20
+ });
21
+
22
+ if (response.status === 200) {
23
+ const data: any = await response.json();
24
+ const match = data.text.match(/:\s*(\d+)\n\n/);
25
+ if (match) {
26
+ console.log(proofEmail, `获取验证码成功: ${match[1]}`);
27
+ return match[1];
28
+ }
29
+ }
30
+ else {
31
+ console.log(proofEmail, `获取验证码失败: ${await response.text()}`);
32
+ }
33
+ await new Promise(resolve => setTimeout(resolve, 2000));
34
+ } catch (error) {
35
+ if (i === maxRetries - 1) {
36
+ throw new Error("Failed to get verification code after maximum retries");
37
+ }
38
+ }
39
+ }
40
+ throw new Error("Failed to get verification code");
41
+ }
functions/utils/jwt.ts ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * 生成 JWT token
3
+ * @param username 用户名
4
+ * @param secret 密钥
5
+ * @returns 生成的 token 字符串
6
+ */
7
+ export async function generateToken(username: string, secret: string): Promise<string> {
8
+ // JWT 头部信息
9
+ const header = { alg: 'HS256', typ: 'JWT' };
10
+ // JWT 载荷信息
11
+ const payload = {
12
+ sub: username,
13
+ exp: Math.floor(Date.now() / 1000) + (30 * 24 * 60 * 60), //30天后过期
14
+ iat: Math.floor(Date.now() / 1000) // 签发时间
15
+ };
16
+
17
+ const encodedHeader = btoa(JSON.stringify(header));
18
+ const encodedPayload = btoa(JSON.stringify(payload));
19
+ const signature = await createHmacSignature(
20
+ `${encodedHeader}.${encodedPayload}`,
21
+ secret
22
+ );
23
+
24
+ return `${encodedHeader}.${encodedPayload}.${signature}`;
25
+ }
26
+
27
+ /**
28
+ * 验证 JWT token
29
+ * @param request 请求对象
30
+ * @param secret 密钥
31
+ * @returns 验证是否通过
32
+ */
33
+ export async function verifyToken(request: Request, secret: string): Promise<boolean> {
34
+ const authHeader = request.headers.get('Authorization');
35
+ if (!authHeader?.startsWith('Bearer ')) {
36
+ return false;
37
+ }
38
+
39
+ const token = authHeader.split(' ')[1];
40
+ try {
41
+ const [headerB64, payloadB64, signatureB64] = token.split('.');
42
+ const expectedSignature = await createHmacSignature(
43
+ `${headerB64}.${payloadB64}`,
44
+ secret
45
+ );
46
+
47
+ if (signatureB64 !== expectedSignature) {
48
+ return false;
49
+ }
50
+
51
+ const payload = JSON.parse(atob(payloadB64));
52
+ const now = Math.floor(Date.now() / 1000);
53
+
54
+ return payload.exp > now;
55
+ } catch (error) {
56
+ console.error('Token verification failed:', error);
57
+ return false;
58
+ }
59
+ }
60
+
61
+ /**
62
+ * 创建 HMAC 签名
63
+ * @param message 需要签名的消息
64
+ * @param secret 密钥
65
+ * @returns 签名字符串
66
+ */
67
+ async function createHmacSignature(message: string, secret: string): Promise<string> {
68
+ const encoder = new TextEncoder();
69
+ const keyData = encoder.encode(secret);
70
+ const messageData = encoder.encode(message);
71
+
72
+ const cryptoKey = await crypto.subtle.importKey(
73
+ 'raw',
74
+ keyData,
75
+ { name: 'HMAC', hash: 'SHA-256' },
76
+ false,
77
+ ['sign']
78
+ );
79
+
80
+ const signature = await crypto.subtle.sign(
81
+ 'HMAC',
82
+ cryptoKey,
83
+ messageData
84
+ );
85
+
86
+ return btoa(String.fromCharCode(...new Uint8Array(signature)));
87
+ }
functions/utils/mail.ts ADDED
@@ -0,0 +1,256 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import PostalMime from 'postal-mime';
2
+
3
+ export async function get_access_token(tokenInfo: any, client_id: string, clientSecret: string) {
4
+ // 检查token是否过期(提前5分钟刷新)
5
+ const now = Date.now();
6
+ const expiryTime = tokenInfo.timestamp + (tokenInfo.expires_in * 1000);
7
+ const shouldRefresh = now >= (expiryTime - 300000); // 5分钟 = 300000毫秒
8
+ if (!shouldRefresh) {
9
+ return tokenInfo.access_token;
10
+ }
11
+ const response = await fetch('https://login.microsoftonline.com/common/oauth2/v2.0/token', {
12
+ method: 'POST',
13
+ headers: {
14
+ 'Content-Type': 'application/x-www-form-urlencoded'
15
+ },
16
+ body: new URLSearchParams({
17
+ 'client_id': client_id,
18
+ 'client_secret': clientSecret,
19
+ 'grant_type': 'refresh_token',
20
+ 'refresh_token': tokenInfo.refresh_token
21
+ })
22
+ });
23
+
24
+ if (!response.ok) {
25
+ const errorText = await response.text();
26
+ throw new Error(`HTTP error! status: ${response.status}, response: ${errorText}`);
27
+ }
28
+ const data = await response.json() as any;
29
+ return data.access_token;
30
+ }
31
+
32
+ //https://learn.microsoft.com/zh-cn/graph/api/resources/mail-api-overview?view=graph-rest-1.0
33
+
34
+ /**
35
+ * 获取邮件列表
36
+ */
37
+ interface ParsedEmail {
38
+ id: string;
39
+ subject: string;
40
+ from: string;
41
+ to: string[];
42
+ date: {
43
+ created: string;
44
+ received: string;
45
+ sent: string;
46
+ modified: string;
47
+ };
48
+ text: string;
49
+ html: string;
50
+ importance: string;
51
+ isRead: boolean;
52
+ isDraft: boolean;
53
+ hasAttachments: boolean;
54
+ webLink: string;
55
+ preview: string;
56
+ categories: string[];
57
+ internetMessageId: string;
58
+ metadata: {
59
+ platform?: string;
60
+ browser?: string;
61
+ ip?: string;
62
+ location?: string;
63
+ };
64
+ }
65
+
66
+ async function parseEmail(email: any): Promise<ParsedEmail> {
67
+ const parser = new PostalMime();
68
+ let content = '';
69
+
70
+ try {
71
+ if (email.body.content) {
72
+ content = email.body.content;
73
+ } else {
74
+ const response = await fetch(email.body.contentUrl);
75
+ content = await response.text();
76
+ }
77
+
78
+ const parsed = await parser.parse(content);
79
+
80
+ // 从HTML内容中提取元数据
81
+ const htmlContent = email.body.content || '';
82
+ const platformMatch = htmlContent.match(/Platform:\s*([^<\r\n]+)/);
83
+ const browserMatch = htmlContent.match(/Browser:\s*([^<\r\n]+)/);
84
+ const ipMatch = htmlContent.match(/IP address:\s*([^<\r\n]+)/);
85
+ const locationMatch = htmlContent.match(/Country\/region:\s*([^<\r\n]+)/);
86
+
87
+ return {
88
+ id: email.id,
89
+ subject: email.subject,
90
+ from: email.from.emailAddress.address,
91
+ to: email.toRecipients.map((r: any) => r.emailAddress.address),
92
+ date: {
93
+ created: email.createdDateTime,
94
+ received: email.receivedDateTime,
95
+ sent: email.sentDateTime,
96
+ modified: email.lastModifiedDateTime
97
+ },
98
+ text: parsed.text || email.bodyPreview || '',
99
+ html: parsed.html || email.body.content || '',
100
+ importance: email.importance,
101
+ isRead: email.isRead,
102
+ isDraft: email.isDraft,
103
+ hasAttachments: email.hasAttachments,
104
+ webLink: email.webLink,
105
+ preview: email.bodyPreview,
106
+ categories: email.categories,
107
+ internetMessageId: email.internetMessageId,
108
+ metadata: {
109
+ platform: platformMatch?.[1]?.trim(),
110
+ browser: browserMatch?.[1]?.trim(),
111
+ ip: ipMatch?.[1]?.trim(),
112
+ location: locationMatch?.[1]?.trim(),
113
+ }
114
+ };
115
+ } catch (error) {
116
+ console.error('解析邮件失败:', error);
117
+ return {
118
+ id: email.id,
119
+ subject: email.subject,
120
+ from: email.from.emailAddress.address,
121
+ to: email.toRecipients.map((r: any) => r.emailAddress.address),
122
+ date: {
123
+ created: email.createdDateTime,
124
+ received: email.receivedDateTime,
125
+ sent: email.sentDateTime,
126
+ modified: email.lastModifiedDateTime
127
+ },
128
+ text: email.bodyPreview || '',
129
+ html: email.body.content || '',
130
+ importance: email.importance,
131
+ isRead: email.isRead,
132
+ isDraft: email.isDraft,
133
+ hasAttachments: email.hasAttachments,
134
+ webLink: email.webLink,
135
+ preview: email.bodyPreview,
136
+ categories: email.categories,
137
+ internetMessageId: email.internetMessageId,
138
+ metadata: {}
139
+ };
140
+ }
141
+ }
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: {
151
+ 'Authorization': `Bearer ${accessToken}`,
152
+ 'Content-Type': 'application/json'
153
+ }
154
+ });
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
+ }
169
+
170
+ /**
171
+ * 删除单个邮件
172
+ */
173
+ export async function deleteEmail(accessToken: string, emailId: string): Promise<void> {
174
+ const endpoint = `https://graph.microsoft.com/v1.0/me/messages/${emailId}`;
175
+
176
+ const response = await fetch(endpoint, {
177
+ method: 'DELETE',
178
+ headers: {
179
+ 'Authorization': `Bearer ${accessToken}`
180
+ }
181
+ });
182
+
183
+ if (!response.ok) {
184
+ const errorData = await response.json() as any;
185
+ throw new Error(`删除邮件失败: ${errorData.error?.message}`);
186
+ }
187
+ }
188
+
189
+ /**
190
+ * 清空邮箱 (批量删除邮件)
191
+ */
192
+ export async function emptyMailbox(accessToken: string, batchSize = 50): Promise<void> {
193
+ let hasMoreEmails = true;
194
+ let totalDeleted = 0;
195
+
196
+ while (hasMoreEmails) {
197
+ // 获取一批邮件
198
+ const emails = await getEmails(accessToken, batchSize);
199
+
200
+ if (emails.length === 0) {
201
+ hasMoreEmails = false;
202
+ break;
203
+ }
204
+
205
+ // 删除该批次的每封邮件
206
+ const deletePromises = emails.map(email => deleteEmail(accessToken, email.id));
207
+ await Promise.all(deletePromises);
208
+
209
+ totalDeleted += emails.length;
210
+ console.log(`已删除 ${emails.length} 封邮件,累计: ${totalDeleted}`);
211
+ }
212
+
213
+ console.log('邮箱已清空');
214
+ }
215
+
216
+ /**
217
+ * 发送邮件
218
+ */
219
+ export async function sendEmail(
220
+ accessToken: string,
221
+ to: string[],
222
+ subject: string,
223
+ body: string,
224
+ isHtml = false
225
+ ): Promise<void> {
226
+ const endpoint = 'https://graph.microsoft.com/v1.0/me/sendMail';
227
+
228
+ const emailData = {
229
+ message: {
230
+ subject,
231
+ body: {
232
+ contentType: isHtml ? 'HTML' : 'Text',
233
+ content: body
234
+ },
235
+ toRecipients: to.map(recipient => ({
236
+ emailAddress: {
237
+ address: recipient
238
+ }
239
+ }))
240
+ }
241
+ };
242
+
243
+ const response = await fetch(endpoint, {
244
+ method: 'POST',
245
+ headers: {
246
+ 'Authorization': `Bearer ${accessToken}`,
247
+ 'Content-Type': 'application/json'
248
+ },
249
+ body: JSON.stringify(emailData)
250
+ });
251
+
252
+ if (!response.ok) {
253
+ const errorData = await response.json() as any;
254
+ throw new Error(`发送邮件失败: ${errorData.error?.message}`);
255
+ }
256
+ }
index.html ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>微软邮箱管理平台</title>
7
+ </head>
8
+ <body>
9
+ <div id="app"></div>
10
+ <script type="module" src="/src/main.ts"></script>
11
+ </body>
12
+ </html>
index.ts ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Context, Hono } from 'hono'
2
+ import * as dotenv from 'dotenv'
3
+ import { cors } from "hono/cors";
4
+ import { compress } from "hono/compress";
5
+ import { prettyJSON } from "hono/pretty-json";
6
+ import { trimTrailingSlash } from "hono/trailing-slash";
7
+ import { serve } from '@hono/node-server'
8
+ import { createStorage } from "unstorage";
9
+ import cloudflareKVHTTPDriver from "unstorage/drivers/cloudflare-kv-http";
10
+ import { serveStatic } from '@hono/node-server/serve-static'
11
+
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'
18
+ import { onRequest as handleLogin } from './functions/api/login.js'
19
+ import { onRequest as handleSetting } from './functions/api/setting.js'
20
+ import { onRequest as handleMailAuth } from './functions/api/mail/auth.js'
21
+ import { onRequest as handleMailCallback } from './functions/api/mail/callback.js'
22
+ import { onRequest as handleMailAll } from './functions/api/mail/all.js'
23
+ import { onRequest as handleMailNew } from './functions/api/mail/new.js'
24
+ import { onRequest as handleMailSend } from './functions/api/mail/send.js'
25
+
26
+ dotenv.config({ path: ['.env', '.env.local'], override: true });
27
+ const isDev = process.env.NODE_ENV === 'development'
28
+
29
+ const app = new Hono<{ Bindings: Env }>()
30
+ app.use(compress());
31
+ app.use(prettyJSON());
32
+ app.use(trimTrailingSlash());
33
+ app.use('*', cors({
34
+ origin: '*',
35
+ allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
36
+ allowHeaders: ['Content-Type', 'Authorization'],
37
+ exposeHeaders: ['Content-Length'],
38
+ credentials: true,
39
+ }));
40
+
41
+
42
+ const storage = createStorage({
43
+ driver: cloudflareKVHTTPDriver({
44
+ accountId: process.env.CLOUDFLARE_ACCOUNT_ID || "",
45
+ namespaceId: process.env.CLOUDFLARE_NAMESPACE_ID || "",
46
+ apiToken: process.env.CLOUDFLARE_API_TOKEN || "",
47
+ }),
48
+ });
49
+
50
+ var kv: KVNamespace = {
51
+ get: async (key: string) => {
52
+ const value = await storage.getItemRaw(key);
53
+ return value as string;
54
+ },
55
+ put: async (key: string, value: string) => {
56
+ await storage.setItem(key, value);
57
+ },
58
+ delete:async(key:string)=>{
59
+ await storage.removeItem(key);
60
+ }
61
+ };
62
+
63
+ app.use('*', async (c, next) => {
64
+ c.env.KV = kv;
65
+ await next()
66
+ })
67
+
68
+
69
+ const scriptPath = fileURLToPath(import.meta.url)
70
+ const scriptDir = dirname(scriptPath)
71
+ const rootDir = isDev ? dirname(scriptPath) : dirname(scriptDir)
72
+ const currentDir = process.cwd();
73
+ let staticPath = path.relative(currentDir, rootDir);
74
+ if (!isDev) {
75
+ staticPath = path.relative(currentDir, path.join(rootDir, "dist"))
76
+ }
77
+ console.log('Script dir:', scriptDir)
78
+ console.log('Root dir:', rootDir)
79
+ console.log('Current dir:', currentDir);
80
+ console.log('Relative path for static files:', staticPath || '.');
81
+
82
+
83
+
84
+ const createContext = (c: Context) => {
85
+ const eventContext: RouteContext = {
86
+ request: c.req.raw,
87
+ functionPath: c.req.path,
88
+ waitUntil: (promise: Promise<any>) => {
89
+ if (c.executionCtx?.waitUntil) {
90
+ c.executionCtx.waitUntil(promise);
91
+ }
92
+ },
93
+ passThroughOnException: () => {
94
+ if (c.executionCtx?.passThroughOnException) {
95
+ c.executionCtx.passThroughOnException();
96
+ }
97
+ },
98
+ next: async (input?: Request | string, init?: RequestInit) => {
99
+ if (typeof input === 'string') {
100
+ return fetch(input, init);
101
+ } else if (input instanceof Request) {
102
+ return fetch(input);
103
+ }
104
+ return new Response('Not Found', { status: 404 });
105
+ },
106
+ env: {
107
+ ...c.env,
108
+ ASSETS: {
109
+ fetch: fetch.bind(globalThis)
110
+ }
111
+ },
112
+ params: c.req.param(),
113
+ // 可以从 c.get() 获取数据,或者传入空对象
114
+ data: c.get('data') || {}
115
+ };
116
+ return eventContext;
117
+ }
118
+
119
+ app.all('/api/*', async (c) => {
120
+ try {
121
+ const context = createContext(c);
122
+ const path = c.req.path;
123
+ // 根据路径匹配对应的处理函数
124
+ let response: Response;
125
+ switch (path) {
126
+ case '/api/account':
127
+ response = await handleAccount(context);
128
+ break;
129
+ case '/api/login':
130
+ response = await handleLogin(context);
131
+ break;
132
+ case '/api/setting':
133
+ response = await handleSetting(context);
134
+ break;
135
+ case '/api/mail/auth':
136
+ response = await handleMailAuth(context);
137
+ break;
138
+ case '/api/mail/callback':
139
+ response = await handleMailCallback(context);
140
+ break;
141
+ case '/api/mail/all':
142
+ response = await handleMailAll(context);
143
+ break;
144
+ case '/api/mail/new':
145
+ response = await handleMailNew(context);
146
+ break;
147
+ case '/api/mail/send':
148
+ response = await handleMailSend(context);
149
+ break;
150
+ default:
151
+ return c.json({ error: 'Route not found' }, 404);
152
+ }
153
+ return response;
154
+ } catch (error) {
155
+ return c.json({ error: (error as Error).message }, 500);
156
+ }
157
+ })
158
+
159
+
160
+ // 中间件配置
161
+ app.get('/*', serveStatic({
162
+ root: staticPath,
163
+ rewriteRequestPath: (path) => {
164
+ return path === '/' ? '/index.html' : path;
165
+ },
166
+ onFound: async (path, c) => {
167
+ console.log('Found:', path)
168
+ },
169
+ onNotFound: async (path, c) => {
170
+ console.log('Not Found:', path)
171
+ }
172
+ }))
173
+
174
+
175
+ // 启动服务器
176
+ const port = parseInt(process.env.PORT || '8788')
177
+ serve({
178
+ fetch: (request: Request, env) => app.fetch(request, { ...env, ...process.env }),
179
+ port
180
+ }, () => {
181
+ console.log(`Server running at http://localhost:${port}`)
182
+ })
183
+
184
+ export default app
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "msmail",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite --host --port 5009",
8
+ "dev:server": "cross-env NODE_ENV=development tsx watch --no-cache index.ts",
9
+ "dev:pages": "wrangler pages dev dist",
10
+ "build": "vue-tsc -b && vite build",
11
+ "build:server": "npm run build && tsc -p ./tsconfig.functions.json",
12
+ "preview": "npm run build && wrangler pages dev ./dist",
13
+ "deploy": "npm run build && wrangler pages deploy ./dist",
14
+ "test": "vitest",
15
+ "cf-typegen": "wrangler types"
16
+ },
17
+ "dependencies": {
18
+ "@hono/node-server": "^1.13.8",
19
+ "@monaco-editor/loader": "^1.5.0",
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",
26
+ "postal-mime": "^2.4.3",
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
+ },
33
+ "devDependencies": {
34
+ "@cloudflare/vitest-pool-workers": "^0.7.5",
35
+ "@cloudflare/workers-types": "^4.20250313.0",
36
+ "@types/node": "^22.10.2",
37
+ "@vitejs/plugin-vue": "^5.2.1",
38
+ "@vue/tsconfig": "^0.7.0",
39
+ "concurrently": "^8.2.2",
40
+ "cross-env": "^7.0.3",
41
+ "tsx": "^4.7.1",
42
+ "typescript": "^5.5.2",
43
+ "unplugin-auto-import": "^19.0.0",
44
+ "unplugin-vue-components": "^28.0.0",
45
+ "vite": "^6.2.0",
46
+ "vitest": "~3.0.7",
47
+ "vue-tsc": "^2.2.4",
48
+ "wrangler": "^4.0.0"
49
+ }
50
+ }
public/vite.svg ADDED
src/App.vue ADDED
@@ -0,0 +1,222 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script setup lang="ts">
2
+ import { useRoute, useRouter } from 'vue-router';
3
+ import { ref } from 'vue';
4
+ import logo from './assets/logo.png'
5
+ const router = useRouter();
6
+ const route = useRoute();
7
+ const menuVisible = ref(false);
8
+
9
+ const menu = [
10
+ {
11
+ name: '收件',
12
+ path: '/mail',
13
+ icon: 'system-log'
14
+ },
15
+ {
16
+ name: '账号',
17
+ path: '/account',
18
+ icon: 'user-list'
19
+ },
20
+ {
21
+ name: '设置',
22
+ path: '/setting',
23
+ icon: 'setting-1'
24
+ }
25
+ ];
26
+
27
+ const logout = () => {
28
+ window.localStorage.removeItem('isAuthenticated');
29
+ router.push('/login');
30
+ };
31
+
32
+ const toggleMenu = () => {
33
+ menuVisible.value = !menuVisible.value;
34
+ };
35
+
36
+
37
+ //解析token
38
+ const parseToken = () => {
39
+ try {
40
+ const token = localStorage.getItem('token') as string;
41
+ const [, payload] = token.split('.');
42
+ const data = JSON.parse(atob(payload));
43
+ return data;
44
+ } catch (error) {
45
+ return null;
46
+ }
47
+ };
48
+ const jwtToken = parseToken();
49
+
50
+ </script>
51
+
52
+ <template>
53
+ <template v-if="route.path === '/login'">
54
+ <router-view />
55
+ </template>
56
+ <t-layout v-else class="h-screen">
57
+ <!-- 移动端抽屉菜单 -->
58
+ <t-drawer v-model:visible="menuVisible" placement="left" size="232" :footer="false" :header="false"
59
+ :close-on-overlay-click="true" class="lg:hidden">
60
+ <t-menu :value="route.path" theme="dark" class="h-full bg-transparent ">
61
+ <template #logo>
62
+ <router-link to="/" class="flex items-center gap-2 p-4">
63
+ <img :src="logo" alt="logo" class="w-8 h-8" />
64
+ <h1 class="text-xl font-bold text-white">SEED LOG</h1>
65
+ </router-link>
66
+ </template>
67
+ <t-menu-item v-for="item in menu" :key="item.path" :value="item.path" :to="item.path"
68
+ @click="menuVisible = false" class="menu-item mx-4 my-2 rounded-xl">
69
+ <template #icon>
70
+ <t-icon :name="item.icon" />
71
+ </template>
72
+ {{ item.name }}
73
+ </t-menu-item>
74
+ </t-menu>
75
+ </t-drawer>
76
+
77
+ <!-- 桌面端侧边栏 -->
78
+ <t-aside class="sidebar backdrop-blur-lg hidden lg:block">
79
+ <t-menu :value="route.path" theme="dark" class="bg-transparent">
80
+ <template #logo>
81
+ <router-link to="/" class="flex items-center gap-2">
82
+ <img :src="logo" alt="logo" class="w-10 h-10 transition-transform hover:scale-110" />
83
+ <h1 class="text-2xl font-bold text-white tracking-wider">SEED LOG</h1>
84
+ </router-link>
85
+ </template>
86
+ <t-menu-item v-for="item in menu" :key="item.path" :value="item.path" :to="item.path"
87
+ class="menu-item mx-4 my-2 rounded-xl">
88
+ <template #icon>
89
+ <t-icon :name="item.icon" class="text-xl" />
90
+ </template>
91
+ <span class="font-medium">{{ item.name }}</span>
92
+ </t-menu-item>
93
+ </t-menu>
94
+ </t-aside>
95
+
96
+
97
+ <t-layout class="w-full">
98
+ <t-header class="header backdrop-blur-xl border-b border-gray-100">
99
+ <div class="flex items-center justify-between h-full px-2 sm:px-4 lg:px-8">
100
+ <!-- 移动端菜单按钮和标题 -->
101
+ <div class="flex items-center gap-2 sm:gap-4 ">
102
+ <div class="lg:hidden">
103
+ <t-button theme="default" variant="text" class=" min-w-[40px]" @click="toggleMenu">
104
+ <t-icon name="menu" size="20px" sm:size="24px" />
105
+ </t-button>
106
+ </div>
107
+ <h1
108
+ class="text-base sm:text-xl lg:text-2xl font-bold bg-gradient-to-r from-blue-600 to-indigo-600 bg-clip-text text-transparent truncate max-w-[150px] sm:max-w-full">
109
+ {{ jwtToken?.sub }}
110
+ </h1>
111
+ </div>
112
+ <div class="flex items-center gap-2 sm:gap-4">
113
+ <t-button theme="danger" @click="logout" class="logout-btn text-sm sm:text-base py-1 px-2 sm:px-4">
114
+ <template #icon>
115
+ <t-icon name="logout" class="text-sm sm:text-base" />
116
+ </template>
117
+ <span>退出</span>
118
+ </t-button>
119
+ </div>
120
+ </div>
121
+ </t-header>
122
+
123
+ <!-- 内容区域 -->
124
+ <t-content class="content bg-gray-50/30 flex-1 overflow-y-auto w-full">
125
+ <router-view v-slot="{ Component }">
126
+ <transition name="fade-slide" mode="out-in">
127
+ <component :is="Component" />
128
+ </transition>
129
+ </router-view>
130
+ </t-content>
131
+
132
+ <!-- 页脚 -->
133
+ <t-footer class="footer backdrop-blur-sm py-4 text-center text-sm text-gray-600">
134
+ <span class="opacity-75">© 微软邮箱管理系统</span>
135
+ </t-footer>
136
+ </t-layout>
137
+
138
+ </t-layout>
139
+ </template>
140
+
141
+ <style scoped>
142
+ @reference "./assets/base.css";
143
+
144
+ .sidebar {
145
+ @apply bg-gradient-to-br from-blue-600 via-blue-700 to-indigo-800;
146
+ box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
147
+ }
148
+
149
+ .header {
150
+ @apply bg-white/80 sticky top-0 z-10;
151
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
152
+ }
153
+
154
+ .menu-item {
155
+ @apply transition-all duration-300 hover:bg-white/15 active:scale-95;
156
+ @apply flex items-center gap-3 px-4 py-3;
157
+ }
158
+
159
+ .logout-btn {
160
+ @apply transition-transform hover:scale-105 active:scale-95;
161
+ }
162
+
163
+ .fade-slide-enter-active,
164
+ .fade-slide-leave-active {
165
+ transition: all 0.3s ease;
166
+ }
167
+
168
+ .fade-slide-enter-from {
169
+ opacity: 0;
170
+ transform: translateY(20px);
171
+ }
172
+
173
+ .fade-slide-leave-to {
174
+ opacity: 0;
175
+ transform: translateY(-20px);
176
+ }
177
+
178
+ /* :deep(.t-default-menu.t-menu--dark){
179
+ @apply bg-transparent;
180
+ } */
181
+
182
+ :deep(.t-drawer__body) {
183
+ @apply p-0;
184
+ }
185
+
186
+ :deep(.t-drawer__body) {
187
+ @apply p-0;
188
+ }
189
+
190
+ :deep(::-webkit-scrollbar) {
191
+ @apply w-2;
192
+ }
193
+
194
+ :deep(::-webkit-scrollbar-thumb) {
195
+ @apply bg-gray-400/30 rounded-full transition-colors hover:bg-gray-400/50;
196
+ }
197
+
198
+ :deep(::-webkit-scrollbar-track) {
199
+ @apply bg-transparent;
200
+ }
201
+
202
+ .content {
203
+ background-image: radial-gradient(circle at 50% 50%,
204
+ rgba(255, 255, 255, 0.8) 0%,
205
+ rgba(240, 240, 250, 0.6) 100%);
206
+ }
207
+
208
+ .footer {
209
+ @apply bg-white/60;
210
+ }
211
+
212
+ /* 添加移动端响应式样式 */
213
+ @media (max-width: 1024px) {
214
+ .sidebar {
215
+ display: none;
216
+ }
217
+ }
218
+
219
+ .drawer-menu {
220
+ @apply bg-gradient-to-br from-blue-600 via-blue-700 to-indigo-800;
221
+ }
222
+ </style>
src/assets/base.css ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ @import "tailwindcss";
2
+ @import "tailwindcss/preflight" layer(base);
3
+ @import "tailwindcss/utilities" layer(utilities);
src/assets/logo.png ADDED
src/assets/main.css ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ @import './base.css';
2
+
src/assets/vue.svg ADDED
src/components/MonacoEditor.vue ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <template>
2
+ <div ref="editorContainer" class="h-full"></div>
3
+ </template>
4
+
5
+ <script setup lang="ts">
6
+ import { ref, onMounted, watch, onBeforeUnmount } from 'vue';
7
+ import monaco from '@monaco-editor/loader';
8
+
9
+ const props = defineProps<{
10
+ value: string,
11
+ originalValue?: string,
12
+ language?: string,
13
+ options?: Record<string, any>,
14
+ }>();
15
+ const emit = defineEmits(['update:value']);
16
+
17
+ const editorContainer = ref<HTMLElement>();
18
+ let editorInstance: any = null;
19
+ let monacoInstance: any = null;
20
+ let editorModel: any = null;
21
+
22
+ // 创建编辑器的函数
23
+ const createEditor = () => {
24
+ if (!editorContainer.value || !monacoInstance) return;
25
+
26
+ // 如果已存在编辑器实例,先销毁
27
+ if (editorInstance) {
28
+ editorInstance.dispose();
29
+ editorInstance = null;
30
+ }
31
+
32
+ const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
33
+ const commonOptions = {
34
+ language: props.language || 'json',
35
+ minimap: { enabled: !isMobile },
36
+ fontSize: isMobile ? 16 : 14,
37
+ lineNumbers: isMobile ? 'off' as const : 'on' as const,
38
+ scrollBeyondLastLine: false,
39
+ automaticLayout: true,
40
+ ...props.options
41
+ };
42
+
43
+ // 每次都创建新模型,确保内容刷新
44
+ if (editorModel) {
45
+ editorModel.dispose();
46
+ }
47
+ editorModel = monacoInstance.editor.createModel(props.value, props.language);
48
+
49
+ if (props.originalValue !== undefined) {
50
+ // 创建差异编辑器
51
+ const originalModel = monacoInstance.editor.createModel(props.originalValue, props.language);
52
+ editorInstance = monacoInstance.editor.createDiffEditor(editorContainer.value, {
53
+ ...commonOptions,
54
+ readOnly: false,
55
+ renderSideBySide: true,
56
+ ignoreTrimWhitespace: false,
57
+ renderOverviewRuler: true,
58
+ diffWordWrap: 'on',
59
+ });
60
+
61
+ editorInstance.setModel({
62
+ original: originalModel,
63
+ modified: editorModel
64
+ });
65
+ const modifiedEditor = editorInstance.getModifiedEditor();
66
+ modifiedEditor.onDidChangeModelContent(() => {
67
+ emit('update:value', modifiedEditor.getValue());
68
+ });
69
+ } else {
70
+ // 创建普通编辑器
71
+ editorInstance = monacoInstance.editor.create(editorContainer.value, {
72
+ ...commonOptions,
73
+ model: editorModel
74
+ });
75
+ editorInstance.onDidChangeModelContent(() => {
76
+ emit('update:value', editorInstance.getValue());
77
+ });
78
+ }
79
+ };
80
+
81
+ onMounted(() => {
82
+ monaco.init().then(instance => {
83
+ monacoInstance = instance;
84
+ createEditor();
85
+ });
86
+ });
87
+
88
+ // 监听 value 和 originalValue 的变化
89
+ watch([() => props.value, () => props.originalValue], ([newVal, newOriginalVal], [oldVal, oldOriginalVal]) => {
90
+ // 判断是否需要重建编辑器
91
+ const modeChanged =
92
+ (oldOriginalVal === undefined && newOriginalVal !== undefined) ||
93
+ (oldOriginalVal !== undefined && newOriginalVal === undefined);
94
+
95
+ if (modeChanged) {
96
+ // 编辑器模式变化,重新创建
97
+ createEditor();
98
+ } else if (editorInstance) {
99
+ if (props.originalValue !== undefined) {
100
+ // 差异模式下更新内容
101
+ const model = editorInstance.getModel();
102
+ if (model && model.original && model.original.getValue() !== newOriginalVal) {
103
+ model.original.setValue(newOriginalVal || '');
104
+ }
105
+ if (model && model.modified && model.modified.getValue() !== newVal) {
106
+ model.modified.setValue(newVal || '');
107
+ }
108
+ } else {
109
+ // 普通模式下更新内容
110
+ if (editorInstance.getValue() !== newVal) {
111
+ editorInstance.setValue(newVal || '');
112
+ }
113
+ }
114
+ }
115
+ }, { deep: true });
116
+
117
+ onBeforeUnmount(() => {
118
+ if (editorInstance) {
119
+ editorInstance.dispose();
120
+ }
121
+ if (editorModel) {
122
+ editorModel.dispose();
123
+ }
124
+ editorInstance = null;
125
+ editorModel = null;
126
+ });
127
+ </script>
128
+
129
+ <style scoped>
130
+ :deep(.monaco-diff-editor),
131
+ :deep(.monaco-editor) {
132
+ height: 100%;
133
+ }
134
+ </style>
src/main.ts ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // 引入组件库的少量全局样式变量
2
+ import 'tdesign-vue-next/es/style/index.css';
3
+ import './assets/main.css'
4
+
5
+ import { createApp } from 'vue'
6
+ import { createPinia } from 'pinia'
7
+
8
+ import App from './App.vue'
9
+ import router from './router'
10
+
11
+ const app = createApp(App)
12
+
13
+ app.use(createPinia())
14
+ app.use(router)
15
+
16
+ app.mount('#app')
src/router/index.ts ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createRouter, createWebHistory } from 'vue-router'
2
+
3
+
4
+ const router = createRouter({
5
+ history: createWebHistory(import.meta.env.BASE_URL),
6
+ routes: [
7
+ {
8
+ path: '/',
9
+ redirect: '/mail',
10
+ meta: { requiresAuth: true }
11
+ },
12
+ {
13
+ path: '/login',
14
+ name: 'Login',
15
+ component: () => import('../views/LoginView.vue'),
16
+ meta: { requiresAuth: false }
17
+ },
18
+ {
19
+ path: '/mail',
20
+ name: 'Mail',
21
+ component: () => import('../views/MailView.vue'),
22
+ meta: { requiresAuth: true }
23
+ },
24
+ {
25
+ path: '/setting',
26
+ name: 'Setting',
27
+ component: () => import('../views/SettingView.vue'),
28
+ meta: { requiresAuth: true }
29
+ },
30
+ {
31
+ path: '/account',
32
+ name: 'Account',
33
+ component: () => import('../views/AccountView.vue'),
34
+ meta: { requiresAuth: true }
35
+ },
36
+ ],
37
+ })
38
+ // 添加路由守卫
39
+ router.beforeEach((to, from, next) => {
40
+ const isAuthenticated = localStorage.getItem('isAuthenticated') === 'true'
41
+ if (to.meta.requiresAuth && !isAuthenticated) {
42
+ next('/login')
43
+ } else if (to.path === '/login' && isAuthenticated) {
44
+ next('/')
45
+ } else {
46
+ next()
47
+ }
48
+ })
49
+
50
+ export default router
src/services/accountApi.ts ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { API_BASE_URL, getHeaders, handleResponse } from './util';
2
+
3
+ export interface Account {
4
+ email: string;
5
+ password: string;
6
+ proofEmail: string;
7
+ }
8
+
9
+ export const accountApi = {
10
+ async post(accounts: Account[]) {
11
+ const response = await fetch(
12
+ `${API_BASE_URL}/api/account`,
13
+ {
14
+ headers: getHeaders(),
15
+ method: 'POST',
16
+ body: JSON.stringify(accounts)
17
+ }
18
+ );
19
+ return handleResponse(response);
20
+ },
21
+
22
+ async get(): Promise<Account[]> {
23
+ const response = await fetch(
24
+ `${API_BASE_URL}/api/account`,
25
+ {
26
+ headers: getHeaders()
27
+ }
28
+ );
29
+ return handleResponse(response);
30
+ },
31
+
32
+ }
src/services/mailApi.ts ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { API_BASE_URL, getHeaders, handleResponse } from './util';
2
+
3
+ export const mailApi = {
4
+ async refreshAuth(email: string) {
5
+ const response = await fetch(
6
+ `${API_BASE_URL}/api/mail/auth`,
7
+ {
8
+ headers: getHeaders(),
9
+ method: 'POST',
10
+ body: JSON.stringify({ email })
11
+ }
12
+ );
13
+ return handleResponse(response);
14
+ },
15
+
16
+ async getLatestMails(email: string) {
17
+ const response = await fetch(
18
+ `${API_BASE_URL}/api/mail/new`,
19
+ {
20
+ headers: getHeaders(),
21
+ method: 'POST',
22
+ body: JSON.stringify({ email })
23
+ }
24
+ );
25
+ return handleResponse(response);
26
+ },
27
+
28
+ async getAllMails(email: string) {
29
+ const response = await fetch(
30
+ `${API_BASE_URL}/api/mail/all`,
31
+ {
32
+ headers: getHeaders(),
33
+ method: 'POST',
34
+ body: JSON.stringify({ email })
35
+ }
36
+ );
37
+ return handleResponse(response);
38
+ },
39
+
40
+ async sendMail(params: {
41
+ email: string;
42
+ to: string[];
43
+ subject: string;
44
+ body: string;
45
+ isHtml?: boolean;
46
+ }) {
47
+ const response = await fetch(
48
+ `${API_BASE_URL}/api/mail/send`,
49
+ {
50
+ headers: getHeaders(),
51
+ method: 'POST',
52
+ body: JSON.stringify(params)
53
+ }
54
+ );
55
+ return handleResponse(response);
56
+ }
57
+ };
src/services/settingApi.ts ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { API_BASE_URL, getHeaders, handleResponse } from './util';
2
+
3
+ export interface Settings {
4
+ /** 飞书配置 */
5
+ feishu: {
6
+ /** 飞书应用ID */
7
+ app_id: string;
8
+ /** 飞书应用密钥 */
9
+ app_secret: string;
10
+ /** 飞书应用验证Token */
11
+ verification_token: string;
12
+ /** 飞书应用加密Key */
13
+ encrypt_key: string;
14
+ /** 飞书机器人接收ID */
15
+ receive_id: string;
16
+ }
17
+ }
18
+
19
+
20
+ export const settingApi = {
21
+ async update(settings: Settings) {
22
+ const response = await fetch(
23
+ `${API_BASE_URL}/api/setting`,
24
+ {
25
+ headers: getHeaders(),
26
+ method: 'POST',
27
+ body: JSON.stringify(settings)
28
+ }
29
+ );
30
+ return handleResponse(response);
31
+ },
32
+
33
+ async get(): Promise<Settings> {
34
+ const response = await fetch(
35
+ `${API_BASE_URL}/api/setting`,
36
+ {
37
+ headers: getHeaders()
38
+ }
39
+ );
40
+ return handleResponse(response);
41
+ },
42
+ }
src/services/userApi.ts ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { API_BASE_URL, handleResponse } from './util';
2
+
3
+ export interface LoginResponse {
4
+ success: boolean;
5
+ token: string;
6
+ message?: string;
7
+ }
8
+
9
+ export const userApi = {
10
+ async login(username: string, password: string): Promise<LoginResponse> {
11
+ const response = await fetch(`${API_BASE_URL}/api/login`, {
12
+ method: 'POST',
13
+ headers: {
14
+ 'Content-Type': 'application/json',
15
+ },
16
+ body: JSON.stringify({ username, password }),
17
+ });
18
+ return handleResponse(response);
19
+ }
20
+ };
21
+
22
+
src/services/util.ts ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import router from "../router";
2
+
3
+ export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '';
4
+
5
+ export function getHeaders() {
6
+ return {
7
+ 'Content-Type': 'application/json',
8
+ Authorization: `Bearer ${localStorage.getItem('token')}`
9
+ };
10
+ }
11
+ export async function handleResponse(response: Response) {
12
+ if (response.status === 401) {
13
+ localStorage.removeItem('isAuthenticated');
14
+ localStorage.removeItem('token');
15
+ router.push('/login');
16
+ throw new Error('认证失败,请重新登录');
17
+ }
18
+ if (!response.ok) {
19
+ const contentType = response.headers.get('content-type');
20
+ if (contentType && contentType.includes('application/json')) {
21
+ const errorData = await response.json();
22
+ throw new Error(errorData.error || '请求失败');
23
+ } else {
24
+ const errorText = await response.text();
25
+ throw new Error(errorText || '请求失败');
26
+ }
27
+ }
28
+ return response.json();
29
+ }
src/stores/counter.ts ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ref, computed } from 'vue'
2
+ import { defineStore } from 'pinia'
3
+
4
+ export const useCounterStore = defineStore('counter', () => {
5
+ const count = ref(0)
6
+ const doubleCount = computed(() => count.value * 2)
7
+ function increment() {
8
+ count.value++
9
+ }
10
+
11
+ return { count, doubleCount, increment }
12
+ })
src/style.css ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
3
+ line-height: 1.5;
4
+ font-weight: 400;
5
+
6
+ color-scheme: light dark;
7
+ color: rgba(255, 255, 255, 0.87);
8
+ background-color: #242424;
9
+
10
+ font-synthesis: none;
11
+ text-rendering: optimizeLegibility;
12
+ -webkit-font-smoothing: antialiased;
13
+ -moz-osx-font-smoothing: grayscale;
14
+ }
15
+
16
+ a {
17
+ font-weight: 500;
18
+ color: #646cff;
19
+ text-decoration: inherit;
20
+ }
21
+ a:hover {
22
+ color: #535bf2;
23
+ }
24
+
25
+ body {
26
+ margin: 0;
27
+ display: flex;
28
+ place-items: center;
29
+ min-width: 320px;
30
+ min-height: 100vh;
31
+ }
32
+
33
+ h1 {
34
+ font-size: 3.2em;
35
+ line-height: 1.1;
36
+ }
37
+
38
+ button {
39
+ border-radius: 8px;
40
+ border: 1px solid transparent;
41
+ padding: 0.6em 1.2em;
42
+ font-size: 1em;
43
+ font-weight: 500;
44
+ font-family: inherit;
45
+ background-color: #1a1a1a;
46
+ cursor: pointer;
47
+ transition: border-color 0.25s;
48
+ }
49
+ button:hover {
50
+ border-color: #646cff;
51
+ }
52
+ button:focus,
53
+ button:focus-visible {
54
+ outline: 4px auto -webkit-focus-ring-color;
55
+ }
56
+
57
+ .card {
58
+ padding: 2em;
59
+ }
60
+
61
+ #app {
62
+ max-width: 1280px;
63
+ margin: 0 auto;
64
+ padding: 2rem;
65
+ text-align: center;
66
+ }
67
+
68
+ @media (prefers-color-scheme: light) {
69
+ :root {
70
+ color: #213547;
71
+ background-color: #ffffff;
72
+ }
73
+ a:hover {
74
+ color: #747bff;
75
+ }
76
+ button {
77
+ background-color: #f9f9f9;
78
+ }
79
+ }
src/views/AccountView.vue ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script setup lang="ts">
2
+ import { ref, onMounted, onUnmounted, watch } from 'vue';
3
+ import { MessagePlugin } from 'tdesign-vue-next';
4
+ import { accountApi } from '../services/accountApi';
5
+
6
+ import MonacoEditor from '../components/MonacoEditor.vue';
7
+
8
+ const accountsText = ref<string>('');
9
+ const loading = ref(false);
10
+ const showDiff = ref(false);
11
+ const originalText = ref('');
12
+
13
+ const fetchAccounts = async () => {
14
+ try {
15
+ loading.value = true;
16
+ accountsText.value = "";
17
+ let data = await accountApi.get();
18
+ const formattedData = JSON.stringify(data, null, 2);
19
+ accountsText.value = formattedData;
20
+ originalText.value = formattedData;
21
+ } catch (error) {
22
+ MessagePlugin.error('获取账号数据失败');
23
+ } finally {
24
+ loading.value = false;
25
+ }
26
+ };
27
+
28
+
29
+ const handleSave = async () => {
30
+ loading.value = true;
31
+ try {
32
+ if (accountsText.value === '') {
33
+ MessagePlugin.error('账号数据不能为空');
34
+ return;
35
+ }
36
+ if (originalText.value === accountsText.value) {
37
+ MessagePlugin.success('数据未修改');
38
+ return;
39
+ }
40
+ const accounts = JSON.parse(accountsText.value);
41
+ const result = await accountApi.post(accounts);
42
+ if (result.error) {
43
+ MessagePlugin.error(`${result.error}`);
44
+ return;
45
+ }
46
+ await fetchAccounts();
47
+ MessagePlugin.success('保存成功');
48
+ } catch (error) {
49
+ if (error instanceof SyntaxError) {
50
+ MessagePlugin.error('JSON格式错误');
51
+ } else {
52
+ MessagePlugin.error('保存失败');
53
+ }
54
+ } finally {
55
+ loading.value = false;
56
+ }
57
+ };
58
+
59
+ // 定义按键事件处理函数
60
+ const handleKeyDown = (e: KeyboardEvent) => {
61
+ if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 's') {
62
+ e.preventDefault();
63
+ handleSave();
64
+ }
65
+ };
66
+
67
+
68
+ // 在显示差异按钮点击处理中添加
69
+ const toggleDiff = () => {
70
+ showDiff.value = !showDiff.value;
71
+
72
+ if (showDiff.value) {
73
+ // 进入差异模式时,确保有原始文本作为比较
74
+ if (originalText.value === '') {
75
+ originalText.value = accountsText.value;
76
+ }
77
+ }
78
+ };
79
+ // 在 AccountView.vue 中添加
80
+ watch(showDiff, (newVal) => {
81
+ if (newVal && originalText.value === accountsText.value) {
82
+ // 如果开启差异模式但两个文本相同,可以考虑提示用户
83
+ MessagePlugin.info('当前没有差异可以显示');
84
+ }
85
+ }, { immediate: true });
86
+
87
+ onMounted(() => {
88
+ // 注册全局按键监听
89
+ window.addEventListener('keydown', handleKeyDown);
90
+ fetchAccounts();
91
+ });
92
+
93
+ onUnmounted(() => {
94
+ // 注销全局按键监听
95
+ window.removeEventListener('keydown', handleKeyDown);
96
+ });
97
+ </script>
98
+
99
+ <template>
100
+ <div class="account-container h-full p-2 md:p-5">
101
+ <t-card bordered class="h-full">
102
+ <template #content>
103
+ <div class=" flex flex-col h-full">
104
+ <div class="flex justify-between items-center mb-4 gap-4">
105
+ <div class="flex gap-2">
106
+ <t-button variant="outline" @click="toggleDiff">
107
+ {{ showDiff ? '隐藏对比' : '显示对比' }}
108
+ </t-button>
109
+ <t-button theme="primary" @click="handleSave" :loading="loading">
110
+ 保存账号
111
+ </t-button>
112
+ </div>
113
+ </div>
114
+
115
+ <div class="editor-container flex-1">
116
+ <MonacoEditor v-model:value="accountsText" :original-value="showDiff ? originalText : undefined"
117
+ language="json" :options="{ tabSize: 2 }" />
118
+ </div>
119
+ </div>
120
+ </template>
121
+ </t-card>
122
+ </div>
123
+ </template>
124
+
125
+ <style scoped>
126
+ .account-container {
127
+ width: 100%;
128
+ }
129
+
130
+ :deep(.t-card__body) {
131
+ height: 100%;
132
+ }
133
+
134
+ .editor-container {
135
+ border: 1px solid var(--td-component-border);
136
+ }
137
+ </style>
src/views/LoginView.vue ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script setup lang="ts">
2
+ import { ref } from 'vue'
3
+ import { useRouter } from 'vue-router'
4
+ import { MessagePlugin } from 'tdesign-vue-next'
5
+ import logo from '../assets/logo.png'
6
+ import { userApi } from '../services/userApi'
7
+
8
+ const router = useRouter()
9
+ const username = ref('')
10
+ const password = ref('')
11
+ const loading = ref(false)
12
+ const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '';
13
+
14
+
15
+
16
+ const handleLogin = async () => {
17
+ if (!username.value || !password.value) {
18
+ MessagePlugin.warning('请输入用户名和密码')
19
+ return
20
+ }
21
+
22
+ loading.value = true
23
+ try {
24
+ const data = await userApi.login(username.value, password.value)
25
+
26
+ if (data.success) {
27
+ localStorage.setItem('token', data.token)
28
+ localStorage.setItem('isAuthenticated', 'true')
29
+
30
+ MessagePlugin.success('登录成功')
31
+ router.push('/')
32
+ } else {
33
+ MessagePlugin.error(data.message || '登录失败')
34
+ }
35
+ } catch (error) {
36
+ MessagePlugin.error('网络错误,请稍后重试')
37
+ console.error('Login error:', error)
38
+ } finally {
39
+ loading.value = false
40
+ }
41
+ }
42
+ </script>
43
+
44
+
45
+ <template>
46
+ <div class="min-h-screen w-full flex items-center justify-center bg-gradient-to-br from-blue-50 via-indigo-50 to-purple-50">
47
+ <div class="relative w-full max-w-md">
48
+ <!-- 装饰背景 -->
49
+ <div class="absolute -top-4 -left-4 w-72 h-72 bg-purple-300 rounded-full mix-blend-multiply filter blur-xl opacity-70 animate-blob"></div>
50
+ <div class="absolute -bottom-8 -right-4 w-72 h-72 bg-blue-300 rounded-full mix-blend-multiply filter blur-xl opacity-70 animate-blob animation-delay-2000"></div>
51
+ <div class="absolute -bottom-8 left-20 w-72 h-72 bg-indigo-300 rounded-full mix-blend-multiply filter blur-xl opacity-70 animate-blob animation-delay-4000"></div>
52
+
53
+ <!-- 登录卡片 -->
54
+ <div class="relative bg-white/80 backdrop-blur-lg rounded-2xl shadow-xl p-8 m-4">
55
+ <!-- Logo和标题 -->
56
+ <div class="flex flex-col items-center mb-8">
57
+ <img :src="logo" alt="logo" class="w-16 h-16 mb-4" />
58
+ <h2 class="text-2xl font-bold text-gray-800">微软账号管理</h2>
59
+ <!-- <p class="text-gray-500 mt-2">登录以继续使用</p> -->
60
+ </div>
61
+
62
+ <!-- 登录表单 -->
63
+ <form @submit.prevent="handleLogin" class="space-y-6">
64
+ <div class="space-y-2">
65
+ <t-input
66
+ v-model="username"
67
+ size="large"
68
+ placeholder="请输入用户名"
69
+ :autofocus="true"
70
+ class="w-full"
71
+ >
72
+ <template #prefix-icon>
73
+ <t-icon name="user" />
74
+ </template>
75
+ </t-input>
76
+ </div>
77
+
78
+ <div class="space-y-2">
79
+ <t-input
80
+ v-model="password"
81
+ type="password"
82
+ size="large"
83
+ placeholder="请输入密码"
84
+ class="w-full"
85
+ >
86
+ <template #prefix-icon>
87
+ <t-icon name="lock-on" />
88
+ </template>
89
+ </t-input>
90
+ </div>
91
+
92
+ <!-- <div class="flex items-center justify-between text-sm">
93
+ <t-checkbox>记住我</t-checkbox>
94
+ <a href="#" class="text-blue-600 hover:text-blue-700 transition-colors">
95
+ 忘记密码?
96
+ </a>
97
+ </div> -->
98
+
99
+ <t-button
100
+ type="submit"
101
+ theme="primary"
102
+ :loading="loading"
103
+ size="large"
104
+ class="w-full"
105
+ :disabled="loading"
106
+ >
107
+ {{ loading ? '登录中...' : '登录' }}
108
+ </t-button>
109
+ </form>
110
+
111
+ <!-- 额外信息 -->
112
+ <div class="mt-6 text-center text-sm text-gray-500">
113
+ 测试账号: admin / password
114
+ </div>
115
+ </div>
116
+ </div>
117
+ </div>
118
+ </template>
119
+
120
+ <style scoped>
121
+ .animate-blob {
122
+ animation: blob 7s infinite;
123
+ }
124
+
125
+ .animation-delay-2000 {
126
+ animation-delay: 2s;
127
+ }
128
+
129
+ .animation-delay-4000 {
130
+ animation-delay: 4s;
131
+ }
132
+
133
+ @keyframes blob {
134
+ 0% {
135
+ transform: translate(0px, 0px) scale(1);
136
+ }
137
+ 33% {
138
+ transform: translate(30px, -50px) scale(1.1);
139
+ }
140
+ 66% {
141
+ transform: translate(-20px, 20px) scale(0.9);
142
+ }
143
+ 100% {
144
+ transform: translate(0px, 0px) scale(1);
145
+ }
146
+ }
147
+ </style>
src/views/MailView.vue ADDED
@@ -0,0 +1,411 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script setup lang="ts">
2
+ import { computed, ref, watch, onMounted } from 'vue';
3
+ import { MessagePlugin, DialogPlugin } from 'tdesign-vue-next';
4
+ import { accountApi, type Account } from '../services/accountApi';
5
+ import { mailApi } from '../services/mailApi';
6
+ const loading = ref(false);
7
+ const pagination = ref({
8
+ current: 1,
9
+ total: 0,
10
+ pageSize: 18
11
+ });
12
+
13
+ const allData = ref<Account[]>([]);
14
+ const userSearch = ref(''); // 添加用户搜索字段
15
+ const sort = ref<{ sortBy: string; descending: boolean }>({
16
+ sortBy: '',
17
+ descending: true
18
+ });
19
+
20
+
21
+
22
+ const columns = [
23
+ { colKey: 'email', title: '用户', width: 200, sorter: true },
24
+ {
25
+ colKey: 'action',
26
+ title: '操作',
27
+ width: 400, // 增加宽度以容纳多个按钮
28
+ }
29
+ ];
30
+
31
+
32
+ // 添加下拉菜单选项函数
33
+ const actionOptions = (row: Account) => [
34
+ { content: '刷新认证', value: 'refresh', onClick: () => handleRefreshAuth(row) },
35
+ { content: '最新邮件', value: 'latest', onClick: () => handleLatestMails(row) },
36
+ { content: '所有邮件', value: 'all', onClick: () => handleAllMails(row) },
37
+ { content: '发送邮件', value: 'send', onClick: () => handleSendMail(row) },
38
+ ];
39
+
40
+ // 实现处理函数
41
+ const handleRefreshAuth = async (row: Account) => {
42
+ try {
43
+ loading.value = true;
44
+ await mailApi.refreshAuth(row.email); // 假设此API存在
45
+ MessagePlugin.success('认证已刷新');
46
+
47
+ } catch (error) {
48
+ MessagePlugin.error('刷新认证失败');
49
+ } finally {
50
+ loading.value = false;
51
+ }
52
+ };
53
+
54
+ interface ParsedEmail {
55
+ subject: string;
56
+ from: string;
57
+ to: string[];
58
+ date: string;
59
+ text: string;
60
+ html: string;
61
+ }
62
+
63
+ const showEmailDialog = ref(false);
64
+ const currentEmail = ref<any>(null);
65
+
66
+ const handleLatestMails = async (row: Account) => {
67
+ try {
68
+ loading.value = true;
69
+ const result = await mailApi.getLatestMails(row.email);
70
+ if (result) {
71
+ currentEmail.value = result;
72
+ showEmailDialog.value = true;
73
+ } else {
74
+ MessagePlugin.info('没有找到邮件');
75
+ }
76
+ } catch (error:any) {
77
+ MessagePlugin.error(`${error.message}`);
78
+ } finally {
79
+ loading.value = false;
80
+ }
81
+ };
82
+
83
+ // 添加所有邮件列表相关的状态
84
+ const showAllMailsDialog = ref(false);
85
+ const allMailsList = ref<any[]>([]);
86
+ const mailsLoading = ref(false);
87
+
88
+ const handleAllMails = async (row: Account) => {
89
+ try {
90
+ mailsLoading.value = true;
91
+ showAllMailsDialog.value = true;
92
+ allMailsList.value= []
93
+ const emails = await mailApi.getAllMails(row.email);
94
+ allMailsList.value = emails;
95
+ } catch (error: any) {
96
+ MessagePlugin.error(`获取邮件失败: ${error.message}`);
97
+ } finally {
98
+ mailsLoading.value = false;
99
+ }
100
+ };
101
+
102
+ // 用于打开指定邮件详情
103
+ const viewMailDetail = (email: any) => {
104
+ currentEmail.value = email;
105
+ showEmailDialog.value = true;
106
+ };
107
+
108
+
109
+
110
+ // 添加发送邮件相关的状态
111
+ const showSendDialog = ref(false);
112
+ const sendMailForm = ref({
113
+ to: '',
114
+ subject: '',
115
+ body: '',
116
+ isHtml: false
117
+ });
118
+ const currentMailAccount = ref<Account | null>(null);
119
+
120
+ // 修改发送邮件处理函数
121
+ const handleSendMail = async (row: Account) => {
122
+ currentMailAccount.value = row;
123
+ sendMailForm.value = {
124
+ to: '',
125
+ subject: '',
126
+ body: '',
127
+ isHtml: false
128
+ };
129
+ showSendDialog.value = true;
130
+ };
131
+
132
+ // 添加发送邮件提交函数
133
+ const submitSendMail = async () => {
134
+ if (!currentMailAccount.value) return;
135
+
136
+ try {
137
+ loading.value = true;
138
+ await mailApi.sendMail({
139
+ email: currentMailAccount.value.email,
140
+ to: sendMailForm.value.to.split(',').map(e => e.trim()),
141
+ subject: sendMailForm.value.subject,
142
+ body: sendMailForm.value.body,
143
+ isHtml: sendMailForm.value.isHtml
144
+ });
145
+ MessagePlugin.success('邮件发送成功');
146
+ showSendDialog.value = false;
147
+ } catch (error: any) {
148
+ MessagePlugin.error(`发送失败: ${error.message}`);
149
+ } finally {
150
+ loading.value = false;
151
+ }
152
+ };
153
+
154
+
155
+ const handleSort = (sortInfo: { sortBy: string; descending: boolean }) => {
156
+ sort.value = sortInfo;
157
+ pagination.value.current = 1;
158
+ };
159
+
160
+
161
+ // 使用计算属性处理排序和分页
162
+ const processedData = computed(() => {
163
+ let result = [...allData.value];
164
+
165
+ // 用户搜索过滤
166
+ if (userSearch.value) {
167
+ result = result.filter(log =>
168
+ log.email.toLowerCase().includes(userSearch.value.toLowerCase())
169
+ );
170
+ }
171
+
172
+ pagination.value.total = result.length; // 更新总数以反映过滤后的结果
173
+
174
+ // 分页处理
175
+ const start = (pagination.value.current - 1) * pagination.value.pageSize;
176
+ const end = start + pagination.value.pageSize;
177
+ return result.slice(start, end);
178
+ });
179
+
180
+
181
+ const fetchData = async () => {
182
+ loading.value = true;
183
+ try {
184
+ const data = await accountApi.get();
185
+ allData.value = data;
186
+ pagination.value.total = data.length;
187
+ } catch (error) {
188
+ MessagePlugin.error('获取数据失败');
189
+ } finally {
190
+ loading.value = false;
191
+ }
192
+ };
193
+
194
+ // 添加 onMounted 钩子
195
+ onMounted(() => {
196
+ fetchData();
197
+ });
198
+
199
+
200
+ </script>
201
+
202
+ <template>
203
+ <div class="w-full h-full flex flex-col p-2 md:p-5 gap-2 md:gap-5">
204
+ <div class="flex flex-col md:flex-row md:items-center md:justify-between">
205
+ <div class="flex flex-col md:flex-row md:items-center gap-4">
206
+ <t-input v-model="userSearch" class="w-full md:w-40" placeholder="搜索用户" clearable />
207
+ </div>
208
+ </div>
209
+
210
+ <div class="overflow-x-auto flex-1 overflow-y-auto">
211
+ <t-table :data="processedData" :loading="loading" :columns="columns" row-key="datetime" hover
212
+ :sort="sort" @sort-change="handleSort" size="small" class="min-w-full">
213
+ <template #action="{ row }">
214
+ <!-- 桌面端显示 -->
215
+ <div class="hidden md:flex flex-wrap gap-2">
216
+ <t-button size="small" variant="outline" @click="handleRefreshAuth(row)">刷新认证</t-button>
217
+ <t-button size="small" variant="outline" @click="handleLatestMails(row)">最新邮件</t-button>
218
+ <t-button size="small" variant="outline" @click="handleAllMails(row)">所有邮件</t-button>
219
+ <t-button size="small" variant="outline" @click="handleSendMail(row)">发送邮件</t-button>
220
+ </div>
221
+
222
+ <!-- 移动端显示 -->
223
+ <div class="md:hidden">
224
+ <t-dropdown :options="actionOptions(row)">
225
+ <t-button variant="outline" size="small">操作 <t-icon name="chevron-down" /></t-button>
226
+ </t-dropdown>
227
+ </div>
228
+ </template>
229
+ </t-table>
230
+ </div>
231
+
232
+ <div class="flex justify-end">
233
+ <t-pagination v-model="pagination.current" :total="pagination.total" :page-size="pagination.pageSize"
234
+ size="small" />
235
+ </div>
236
+
237
+ <!-- 邮件详情对话框 -->
238
+ <t-dialog
239
+ :visible="showEmailDialog"
240
+ :header="currentEmail?.subject"
241
+ @close="showEmailDialog = false"
242
+ :width="800"
243
+ :footer="false"
244
+ class="email-detail-dialog"
245
+ >
246
+ <template v-if="currentEmail">
247
+ <div class="email-details">
248
+ <div class="email-meta">
249
+ <p><strong>发件人:</strong> {{ currentEmail.from }}</p>
250
+ <p><strong>收件人:</strong> {{ currentEmail.to?.join(', ') }}</p>
251
+ <p><strong>时间:</strong> {{ new Date(currentEmail.date.received).toLocaleString() }}</p>
252
+ <p v-if="currentEmail.metadata?.location"><strong>位置:</strong> {{ currentEmail.metadata.location }}</p>
253
+ <p v-if="currentEmail.metadata?.ip"><strong>IP:</strong> {{ currentEmail.metadata.ip }}</p>
254
+ <p v-if="currentEmail.metadata?.platform"><strong>平台:</strong> {{ currentEmail.metadata.platform }}</p>
255
+ <p v-if="currentEmail.metadata?.browser"><strong>浏览器:</strong> {{ currentEmail.metadata.browser }}</p>
256
+ <p><strong>重要性:</strong> {{ currentEmail.importance }}</p>
257
+ <p><strong>状态:</strong> {{ currentEmail.isRead ? '已读' : '未读' }}</p>
258
+ </div>
259
+ <div class="email-content" v-html="currentEmail.html || currentEmail.text"></div>
260
+ <div v-if="currentEmail.hasAttachments" class="email-attachments">包含附件</div>
261
+ <div class="email-actions">
262
+ <a :href="currentEmail.webLink" target="_blank">在Outlook中查看</a>
263
+ </div>
264
+ </div>
265
+ </template>
266
+ </t-dialog>
267
+
268
+ <!-- 所有邮件列表对话框 -->
269
+ <t-dialog
270
+ :visible="showAllMailsDialog"
271
+ header="所有邮件"
272
+ @close="showAllMailsDialog = false"
273
+ :width="900"
274
+ :footer="false"
275
+ >
276
+ <t-loading :loading="mailsLoading">
277
+ <div class="emails-list">
278
+ <t-list>
279
+ <t-list-item
280
+ v-for="email in allMailsList"
281
+ :key="email.id"
282
+ @click="viewMailDetail(email)"
283
+ class="email-list-item"
284
+ >
285
+ <div class="flex flex-col gap-1 w-full cursor-pointer hover:bg-gray-50 p-2">
286
+ <div class="flex justify-between">
287
+ <span class="font-medium">{{ email.subject || '(无主题)' }}</span>
288
+ <span class="text-gray-500 text-sm">
289
+ {{ new Date(email.date.received).toLocaleString() }}
290
+ </span>
291
+ </div>
292
+ <div class="flex justify-between text-sm">
293
+ <span class="text-gray-600">发件人: {{ email.from }}</span>
294
+ <span :class="{'text-green-600': email.isRead, 'text-red-600': !email.isRead}">
295
+ {{ email.isRead ? '已读' : '未读' }}
296
+ </span>
297
+ </div>
298
+ </div>
299
+ </t-list-item>
300
+ </t-list>
301
+ </div>
302
+ </t-loading>
303
+ </t-dialog>
304
+
305
+ <!-- 发送邮件对话框 -->
306
+ <t-dialog
307
+ :visible="showSendDialog"
308
+ header="发送邮件"
309
+ @close="showSendDialog = false"
310
+ :width="600"
311
+ :footer="false"
312
+ >
313
+ <template v-if="currentMailAccount">
314
+ <div class="send-mail-form">
315
+ <t-form>
316
+ <t-form-item label="发件人">
317
+ <t-input disabled :value="currentMailAccount.email" />
318
+ </t-form-item>
319
+ <t-form-item label="收件人">
320
+ <t-input v-model="sendMailForm.to" placeholder="多个收件人请用逗号分隔" />
321
+ </t-form-item>
322
+ <t-form-item label="主题">
323
+ <t-input v-model="sendMailForm.subject" />
324
+ </t-form-item>
325
+ <t-form-item label="内容">
326
+ <t-textarea v-model="sendMailForm.body" :rows="6" />
327
+ </t-form-item>
328
+ <t-form-item>
329
+ <t-checkbox v-model="sendMailForm.isHtml">HTML 格式</t-checkbox>
330
+ </t-form-item>
331
+ <t-form-item>
332
+ <div class="flex justify-end gap-2">
333
+ <t-button theme="default" @click="showSendDialog = false">取消</t-button>
334
+ <t-button theme="primary" @click="submitSendMail" :loading="loading">发送</t-button>
335
+ </div>
336
+ </t-form-item>
337
+ </t-form>
338
+ </div>
339
+ </template>
340
+ </t-dialog>
341
+ </div>
342
+ </template>
343
+
344
+ <style scoped>
345
+ @media (max-width: 768px) {
346
+ :deep(.t-table__header) {
347
+ font-size: 14px;
348
+ }
349
+
350
+ :deep(.t-table td) {
351
+ font-size: 13px;
352
+ }
353
+ }
354
+
355
+ .email-details {
356
+ max-height: 60vh;
357
+ overflow-y: auto;
358
+ padding: 16px;
359
+ }
360
+
361
+ .email-content {
362
+ margin-top: 16px;
363
+ border-top: 1px solid #eee;
364
+ padding-top: 16px;
365
+ }
366
+
367
+ .email-meta {
368
+ background: #f5f5f5;
369
+ padding: 12px;
370
+ border-radius: 4px;
371
+ margin-bottom: 16px;
372
+ }
373
+
374
+ .email-actions {
375
+ margin-top: 16px;
376
+ text-align: right;
377
+ }
378
+
379
+ .email-attachments {
380
+ margin-top: 16px;
381
+ color: #666;
382
+ font-style: italic;
383
+ }
384
+
385
+ .send-mail-form {
386
+ padding: 16px;
387
+ }
388
+
389
+ .emails-list {
390
+ max-height: 60vh;
391
+ overflow-y: auto;
392
+ }
393
+
394
+ .email-list-item {
395
+ border-bottom: 1px solid #eee;
396
+ }
397
+
398
+ .email-list-item:last-child {
399
+ border-bottom: none;
400
+ }
401
+
402
+ /* 添加以下CSS确保邮件详情弹窗总是显示在最上层 */
403
+ :deep(.email-detail-dialog) {
404
+ z-index: 3000 !important; /* 确保高于其他弹窗 */
405
+ }
406
+
407
+ /* 可能需要调整其他弹窗的z-index */
408
+ :deep(.t-dialog) {
409
+ z-index: 2000;
410
+ }
411
+ </style>
src/views/SettingView.vue ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script setup lang="ts">
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: {
8
+ app_id: '',
9
+ app_secret: '',
10
+ verification_token: '',
11
+ encrypt_key: '',
12
+ receive_id: ''
13
+ }
14
+ });
15
+
16
+ const loading = ref(false);
17
+
18
+ onMounted(async () => {
19
+ try {
20
+ const data = await settingApi.get();
21
+ // 合并默认值和获取的数据
22
+ settings.value = {
23
+ feishu: {
24
+ ...settings.value.feishu,
25
+ ...data.feishu
26
+ }
27
+ };
28
+ } catch (error) {
29
+ MessagePlugin.error('获取设置失败');
30
+ }
31
+ });
32
+
33
+ const handleSave = async () => {
34
+ loading.value = true;
35
+ try {
36
+ if(!settings.value)
37
+ {
38
+ return
39
+ }
40
+ await settingApi.update(settings.value);
41
+ MessagePlugin.success('保存成功');
42
+ } catch (error) {
43
+ MessagePlugin.error('保存失败');
44
+ } finally {
45
+ loading.value = false;
46
+ }
47
+ };
48
+ </script>
49
+
50
+ <template>
51
+ <div class="setting-container p-2 md:p-5">
52
+
53
+ <t-form :data="settings" @submit="handleSave">
54
+ <t-card bordered>
55
+ <t-divider>飞书配置</t-divider>
56
+ <t-form-item label="应用ID" name="feishu.app_id">
57
+ <t-input v-model="settings.feishu.app_id" placeholder="请输入飞书应用ID" />
58
+ </t-form-item>
59
+ <t-form-item label="应用密钥" name="feishu.app_secret">
60
+ <t-input v-model="settings.feishu.app_secret" type="password" placeholder="请输入飞书应用密钥" />
61
+ </t-form-item>
62
+ <t-form-item label="验证Token" name="feishu.verification_token">
63
+ <t-input v-model="settings.feishu.verification_token" placeholder="请输入飞书应用验证Token" />
64
+ </t-form-item>
65
+ <t-form-item label="加密Key" name="feishu.encrypt_key">
66
+ <t-input v-model="settings.feishu.encrypt_key" placeholder="请输入飞书应用加密Key" />
67
+ </t-form-item>
68
+ <t-form-item label="接收ID" name="feishu.receive_id">
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>
82
+
83
+ <style scoped>
84
+
85
+ </style>
src/vite-env.d.ts ADDED
@@ -0,0 +1 @@
 
 
1
+ /// <reference types="vite/client" />
test/index.spec.ts ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // test/index.spec.ts
2
+ import { env, createExecutionContext, waitOnExecutionContext, SELF } from 'cloudflare:test';
3
+ import { describe, it, expect } from 'vitest';
4
+ import worker from '../src/index';
5
+
6
+ // For now, you'll need to do something like this to get a correctly-typed
7
+ // `Request` to pass to `worker.fetch()`.
8
+ const IncomingRequest = Request<unknown, IncomingRequestCfProperties>;
9
+
10
+ describe('Hello World worker', () => {
11
+ it('responds with Hello World! (unit style)', async () => {
12
+ const request = new IncomingRequest('http://example.com');
13
+ // Create an empty context to pass to `worker.fetch()`.
14
+ const ctx = createExecutionContext();
15
+ const response = await worker.fetch(request, env, ctx);
16
+ // Wait for all `Promise`s passed to `ctx.waitUntil()` to settle before running test assertions
17
+ await waitOnExecutionContext(ctx);
18
+ expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`);
19
+ });
20
+
21
+ it('responds with Hello World! (integration style)', async () => {
22
+ const response = await SELF.fetch('https://example.com');
23
+ expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`);
24
+ });
25
+ });
test/tsconfig.json ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "extends": "../tsconfig.json",
3
+ "compilerOptions": {
4
+ "types": ["@cloudflare/workers-types/experimental", "@cloudflare/vitest-pool-workers"]
5
+ },
6
+ "include": ["./**/*.ts", "../worker-configuration.d.ts"],
7
+ "exclude": []
8
+ }
tsconfig.app.json ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "extends": "@vue/tsconfig/tsconfig.dom.json",
3
+ "compilerOptions": {
4
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
5
+
6
+ /* Linting */
7
+ "strict": true,
8
+ "noUnusedLocals": false,
9
+ "noUnusedParameters": false,
10
+ "noFallthroughCasesInSwitch": true,
11
+ "noUncheckedSideEffectImports": true
12
+ },
13
+ "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
14
+ }
tsconfig.functions.json ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ /* Visit https://aka.ms/tsconfig.json to read more about this file */
4
+ /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
5
+ "target": "ESNext",
6
+ /* Specify a set of bundled library declaration files that describe the target runtime environment. */
7
+ "lib": [
8
+ "esnext"
9
+ ],
10
+ /* Specify what JSX code is generated. */
11
+ "jsx": "react-jsx",
12
+ /* Specify what module code is generated. */
13
+ "module": "ESNext",
14
+ /* Specify how TypeScript looks up a file from a given module specifier. */
15
+ "moduleResolution": "node",
16
+ /* Specify type package names to be included without being referenced in a source file. */
17
+ "types": [
18
+ //"@cloudflare/workers-types/2023-07-01",
19
+ "node"
20
+ ],
21
+ /* Enable importing .json files */
22
+ "resolveJsonModule": true,
23
+ /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
24
+ "allowJs": true,
25
+ /* Enable error reporting in type-checked JavaScript files. */
26
+ "checkJs": false,
27
+
28
+ /* Ensure that each file can be safely transpiled without relying on other imports. */
29
+ "isolatedModules": true,
30
+ /* Allow 'import x from y' when a module doesn't have a default export. */
31
+ "allowSyntheticDefaultImports": true,
32
+ /* Ensure that casing is correct in imports. */
33
+ "forceConsistentCasingInFileNames": true,
34
+ /* Enable all strict type-checking options. */
35
+ "strict": true,
36
+ /* Skip type checking all .d.ts files. */
37
+ "skipLibCheck": true,
38
+ "outDir": "./dist-server", // 输出到 dist
39
+ "noEmit": false, //允许编译器生成 JavaScript 输出文件
40
+ },
41
+ "exclude": [
42
+ "test"
43
+ ],
44
+ "include": [
45
+ //"worker-configuration.d.ts",
46
+ "functions/types.d.ts",
47
+ "index.ts",
48
+ "functions/**/*.ts"
49
+ ]
50
+ }