github-actions[bot]
commited on
Commit
·
11a8eb2
1
Parent(s):
3ed7406
Update from GitHub Actions
Browse files- components.d.ts +2 -0
- functions/api/debug/cleanup.ts +43 -0
- functions/api/debug/list.ts +47 -0
- functions/api/debug/screenshot.ts +61 -0
- functions/types.d.ts +1 -0
- functions/utils/authService.ts +109 -11
- functions/utils/debugStorage.ts +135 -0
- index.ts +21 -0
- src/App.vue +5 -0
- src/router/index.ts +6 -0
- src/views/DebugView.vue +161 -0
components.d.ts
CHANGED
@@ -11,6 +11,7 @@ declare module 'vue' {
|
|
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']
|
@@ -36,6 +37,7 @@ declare module 'vue' {
|
|
36 |
TMenuItem: typeof import('tdesign-vue-next')['MenuItem']
|
37 |
TPagination: typeof import('tdesign-vue-next')['Pagination']
|
38 |
TTable: typeof import('tdesign-vue-next')['Table']
|
|
|
39 |
TTbody: typeof import('tdesign-vue-next')['Tbody']
|
40 |
TTd: typeof import('tdesign-vue-next')['Td']
|
41 |
TTextarea: typeof import('tdesign-vue-next')['Textarea']
|
|
|
11 |
MonacoEditor: typeof import('./src/components/MonacoEditor.vue')['default']
|
12 |
RouterLink: typeof import('vue-router')['RouterLink']
|
13 |
RouterView: typeof import('vue-router')['RouterView']
|
14 |
+
TAlert: typeof import('tdesign-vue-next')['Alert']
|
15 |
TAside: typeof import('tdesign-vue-next')['Aside']
|
16 |
TButton: typeof import('tdesign-vue-next')['Button']
|
17 |
TCard: typeof import('tdesign-vue-next')['Card']
|
|
|
37 |
TMenuItem: typeof import('tdesign-vue-next')['MenuItem']
|
38 |
TPagination: typeof import('tdesign-vue-next')['Pagination']
|
39 |
TTable: typeof import('tdesign-vue-next')['Table']
|
40 |
+
TTag: typeof import('tdesign-vue-next')['Tag']
|
41 |
TTbody: typeof import('tdesign-vue-next')['Tbody']
|
42 |
TTd: typeof import('tdesign-vue-next')['Td']
|
43 |
TTextarea: typeof import('tdesign-vue-next')['Textarea']
|
functions/api/debug/cleanup.ts
ADDED
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { CORS_HEADERS as corsHeaders } from '../../utils/cors.js';
|
2 |
+
import { cleanupOldDebugFiles } from '../../utils/debugStorage.js';
|
3 |
+
|
4 |
+
export async function onRequestPOST(context: any) {
|
5 |
+
const { request } = context;
|
6 |
+
const url = new URL(request.url);
|
7 |
+
const daysToKeep = parseInt(url.searchParams.get('days') || '7');
|
8 |
+
|
9 |
+
try {
|
10 |
+
const deletedCount = await cleanupOldDebugFiles(daysToKeep);
|
11 |
+
|
12 |
+
return new Response(JSON.stringify({
|
13 |
+
success: true,
|
14 |
+
message: `已清理 ${deletedCount} 条超过 ${daysToKeep} 天的调试数据`,
|
15 |
+
deletedCount
|
16 |
+
}), {
|
17 |
+
headers: {
|
18 |
+
'Content-Type': 'application/json',
|
19 |
+
...corsHeaders
|
20 |
+
}
|
21 |
+
});
|
22 |
+
|
23 |
+
} catch (error) {
|
24 |
+
console.error('Error cleaning up debug data:', error);
|
25 |
+
return new Response(JSON.stringify({
|
26 |
+
error: 'Failed to cleanup debug data',
|
27 |
+
details: error instanceof Error ? error.message : String(error)
|
28 |
+
}), {
|
29 |
+
status: 500,
|
30 |
+
headers: {
|
31 |
+
'Content-Type': 'application/json',
|
32 |
+
...corsHeaders
|
33 |
+
}
|
34 |
+
});
|
35 |
+
}
|
36 |
+
}
|
37 |
+
|
38 |
+
export async function onRequestOPTIONS() {
|
39 |
+
return new Response(null, {
|
40 |
+
status: 200,
|
41 |
+
headers: corsHeaders
|
42 |
+
});
|
43 |
+
}
|
functions/api/debug/list.ts
ADDED
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { CORS_HEADERS as corsHeaders } from '../../utils/cors.js';
|
2 |
+
import { getAllDebugInfo } from '../../utils/debugStorage.js';
|
3 |
+
|
4 |
+
export async function onRequestGET(context: any) {
|
5 |
+
const { request } = context;
|
6 |
+
const url = new URL(request.url);
|
7 |
+
const limit = parseInt(url.searchParams.get('limit') || '50');
|
8 |
+
|
9 |
+
try {
|
10 |
+
// 获取所有调试信息
|
11 |
+
const debugItems = await getAllDebugInfo();
|
12 |
+
|
13 |
+
// 限制返回数量
|
14 |
+
const limitedItems = debugItems.slice(0, limit);
|
15 |
+
|
16 |
+
return new Response(JSON.stringify({
|
17 |
+
success: true,
|
18 |
+
data: limitedItems,
|
19 |
+
total: debugItems.length
|
20 |
+
}), {
|
21 |
+
headers: {
|
22 |
+
'Content-Type': 'application/json',
|
23 |
+
...corsHeaders
|
24 |
+
}
|
25 |
+
});
|
26 |
+
|
27 |
+
} catch (error) {
|
28 |
+
console.error('Error retrieving debug list:', error);
|
29 |
+
return new Response(JSON.stringify({
|
30 |
+
error: 'Failed to retrieve debug list',
|
31 |
+
details: error instanceof Error ? error.message : String(error)
|
32 |
+
}), {
|
33 |
+
status: 500,
|
34 |
+
headers: {
|
35 |
+
'Content-Type': 'application/json',
|
36 |
+
...corsHeaders
|
37 |
+
}
|
38 |
+
});
|
39 |
+
}
|
40 |
+
}
|
41 |
+
|
42 |
+
export async function onRequestOPTIONS() {
|
43 |
+
return new Response(null, {
|
44 |
+
status: 200,
|
45 |
+
headers: corsHeaders
|
46 |
+
});
|
47 |
+
}
|
functions/api/debug/screenshot.ts
ADDED
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { CORS_HEADERS as corsHeaders } from '../../utils/cors.js';
|
2 |
+
import { getScreenshot } from '../../utils/debugStorage.js';
|
3 |
+
|
4 |
+
export async function onRequestGET(context: any) {
|
5 |
+
const { request } = context;
|
6 |
+
const url = new URL(request.url);
|
7 |
+
const debugId = url.searchParams.get('id');
|
8 |
+
|
9 |
+
if (!debugId) {
|
10 |
+
return new Response(JSON.stringify({ error: 'Missing debug ID' }), {
|
11 |
+
status: 400,
|
12 |
+
headers: {
|
13 |
+
'Content-Type': 'application/json',
|
14 |
+
...corsHeaders
|
15 |
+
}
|
16 |
+
});
|
17 |
+
}
|
18 |
+
|
19 |
+
try {
|
20 |
+
// 获取截图数据
|
21 |
+
const screenshotBuffer = await getScreenshot(debugId);
|
22 |
+
|
23 |
+
if (!screenshotBuffer) {
|
24 |
+
return new Response(JSON.stringify({ error: 'Screenshot not found' }), {
|
25 |
+
status: 404,
|
26 |
+
headers: {
|
27 |
+
'Content-Type': 'application/json',
|
28 |
+
...corsHeaders
|
29 |
+
}
|
30 |
+
});
|
31 |
+
}
|
32 |
+
|
33 |
+
return new Response(screenshotBuffer, {
|
34 |
+
headers: {
|
35 |
+
'Content-Type': 'image/png',
|
36 |
+
'Cache-Control': 'public, max-age=3600',
|
37 |
+
...corsHeaders
|
38 |
+
}
|
39 |
+
});
|
40 |
+
|
41 |
+
} catch (error) {
|
42 |
+
console.error('Error retrieving screenshot:', error);
|
43 |
+
return new Response(JSON.stringify({
|
44 |
+
error: 'Failed to retrieve screenshot',
|
45 |
+
details: error instanceof Error ? error.message : String(error)
|
46 |
+
}), {
|
47 |
+
status: 500,
|
48 |
+
headers: {
|
49 |
+
'Content-Type': 'application/json',
|
50 |
+
...corsHeaders
|
51 |
+
}
|
52 |
+
});
|
53 |
+
}
|
54 |
+
}
|
55 |
+
|
56 |
+
export async function onRequestOPTIONS() {
|
57 |
+
return new Response(null, {
|
58 |
+
status: 200,
|
59 |
+
headers: corsHeaders
|
60 |
+
});
|
61 |
+
}
|
functions/types.d.ts
CHANGED
@@ -3,6 +3,7 @@ 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 {
|
|
|
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 |
+
list: (options?: { prefix?: string }) => Promise<{ keys: { name: string }[] }>
|
7 |
}
|
8 |
|
9 |
interface Env {
|
functions/utils/authService.ts
CHANGED
@@ -1,6 +1,7 @@
|
|
1 |
import { Page } from 'playwright';
|
2 |
import BrowserManager from './browser.js';
|
3 |
import { getVerificationCode } from './emailVerification.js';
|
|
|
4 |
|
5 |
interface Account {
|
6 |
email: string;
|
@@ -39,18 +40,61 @@ export class AuthService {
|
|
39 |
const redirectUri = this.env.AUTH_REDIRECT_URI;
|
40 |
let browser;
|
41 |
let context;
|
|
|
|
|
42 |
|
43 |
try {
|
44 |
browser = await BrowserManager.getInstance();
|
45 |
context = await browser.newContext();
|
46 |
-
|
47 |
|
48 |
const authUrl = this.buildAuthUrl(clientId, redirectUri, account.email);
|
49 |
await this.handleLoginProcess(page, account, authUrl);
|
50 |
await this.handleMultiFactorAuth(page, account);
|
51 |
await this.confirmLogin(page, account);
|
52 |
await this.handleConsent(page, redirectUri);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
53 |
} finally {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
54 |
if (context) await context.close();
|
55 |
}
|
56 |
}
|
@@ -67,6 +111,11 @@ export class AuthService {
|
|
67 |
}
|
68 |
|
69 |
public async loginMail(email: string): Promise<{ success: boolean; error?: string }> {
|
|
|
|
|
|
|
|
|
|
|
70 |
try {
|
71 |
const accountsStr = await this.env.KV.get("accounts");
|
72 |
const accounts: Account[] = accountsStr ? JSON.parse(accountsStr) : [];
|
@@ -75,19 +124,62 @@ export class AuthService {
|
|
75 |
if (!account) {
|
76 |
throw new Error("Account not found");
|
77 |
}
|
78 |
-
|
79 |
-
|
80 |
-
|
|
|
81 |
await this.handleLoginProcess(page, account, "https://outlook.live.com/mail/0/?prompt=select_account");
|
82 |
await this.handleMultiFactorAuth(page, account);
|
83 |
await this.confirmLogin(page, account);
|
84 |
await page.waitForTimeout(5000);
|
85 |
return { success: true };
|
86 |
} catch (error: any) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
87 |
return {
|
88 |
success: false,
|
89 |
error: error.message
|
90 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
91 |
}
|
92 |
}
|
93 |
|
@@ -122,11 +214,17 @@ export class AuthService {
|
|
122 |
console.log(account.email, `没有新版切换到密码登录,继续执行: ${error}`);
|
123 |
}
|
124 |
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
130 |
|
131 |
|
132 |
const proofEmail = account.proofEmail;
|
@@ -288,13 +386,13 @@ export class AuthService {
|
|
288 |
}, { timeout: 3000 });
|
289 |
await page.click('button[type="submit"]#acceptButton', { timeout: 3000 });
|
290 |
} catch (error) {
|
291 |
-
console.log(account.email,
|
292 |
}
|
293 |
}
|
294 |
|
295 |
private async handleConsent(page: Page, redirectUri: string) {
|
296 |
try {
|
297 |
-
await page.waitForURL("https://account.live.com/Consent/**", { timeout:
|
298 |
await page.click('button[type="submit"][data-testid="appConsentPrimaryButton"]');
|
299 |
} catch (error) {
|
300 |
console.log("Consent page not found or timeout, skipping...");
|
|
|
1 |
import { Page } from 'playwright';
|
2 |
import BrowserManager from './browser.js';
|
3 |
import { getVerificationCode } from './emailVerification.js';
|
4 |
+
import { saveScreenshot, saveDebugInfo, saveErrorInfo } from './debugStorage.js';
|
5 |
|
6 |
interface Account {
|
7 |
email: string;
|
|
|
40 |
const redirectUri = this.env.AUTH_REDIRECT_URI;
|
41 |
let browser;
|
42 |
let context;
|
43 |
+
let page;
|
44 |
+
const debugId = `auth_${account.email}_${Date.now()}`;
|
45 |
|
46 |
try {
|
47 |
browser = await BrowserManager.getInstance();
|
48 |
context = await browser.newContext();
|
49 |
+
page = await context.newPage();
|
50 |
|
51 |
const authUrl = this.buildAuthUrl(clientId, redirectUri, account.email);
|
52 |
await this.handleLoginProcess(page, account, authUrl);
|
53 |
await this.handleMultiFactorAuth(page, account);
|
54 |
await this.confirmLogin(page, account);
|
55 |
await this.handleConsent(page, redirectUri);
|
56 |
+
} catch (error) {
|
57 |
+
// 记录错误信息
|
58 |
+
const errorInfo = {
|
59 |
+
email: account.email,
|
60 |
+
error: error instanceof Error ? error.message : String(error),
|
61 |
+
timestamp: new Date().toISOString(),
|
62 |
+
debugId: debugId
|
63 |
+
};
|
64 |
+
|
65 |
+
try {
|
66 |
+
await saveErrorInfo(debugId, errorInfo);
|
67 |
+
} catch (saveError) {
|
68 |
+
console.error('Failed to save error info:', saveError);
|
69 |
+
}
|
70 |
+
throw error;
|
71 |
} finally {
|
72 |
+
// 截取最后的页面截图
|
73 |
+
if (page) {
|
74 |
+
try {
|
75 |
+
const screenshot = await page.screenshot({
|
76 |
+
fullPage: true,
|
77 |
+
type: 'png'
|
78 |
+
});
|
79 |
+
|
80 |
+
const screenshotInfo = {
|
81 |
+
email: account.email,
|
82 |
+
timestamp: new Date().toISOString(),
|
83 |
+
debugId: debugId,
|
84 |
+
url: page.url(),
|
85 |
+
title: await page.title().catch(() => 'Unknown')
|
86 |
+
};
|
87 |
+
|
88 |
+
// 保存截图和调试信息到本地文件
|
89 |
+
await saveScreenshot(debugId, screenshot);
|
90 |
+
await saveDebugInfo(debugId, screenshotInfo);
|
91 |
+
|
92 |
+
console.log(`Screenshot saved for debug: ${debugId}`);
|
93 |
+
} catch (screenshotError) {
|
94 |
+
console.error('Failed to take screenshot:', screenshotError);
|
95 |
+
}
|
96 |
+
}
|
97 |
+
|
98 |
if (context) await context.close();
|
99 |
}
|
100 |
}
|
|
|
111 |
}
|
112 |
|
113 |
public async loginMail(email: string): Promise<{ success: boolean; error?: string }> {
|
114 |
+
let browser;
|
115 |
+
let context;
|
116 |
+
let page;
|
117 |
+
const debugId = `login_${email}_${Date.now()}`;
|
118 |
+
|
119 |
try {
|
120 |
const accountsStr = await this.env.KV.get("accounts");
|
121 |
const accounts: Account[] = accountsStr ? JSON.parse(accountsStr) : [];
|
|
|
124 |
if (!account) {
|
125 |
throw new Error("Account not found");
|
126 |
}
|
127 |
+
|
128 |
+
browser = await BrowserManager.getInstance();
|
129 |
+
context = await browser.newContext();
|
130 |
+
page = await context.newPage();
|
131 |
await this.handleLoginProcess(page, account, "https://outlook.live.com/mail/0/?prompt=select_account");
|
132 |
await this.handleMultiFactorAuth(page, account);
|
133 |
await this.confirmLogin(page, account);
|
134 |
await page.waitForTimeout(5000);
|
135 |
return { success: true };
|
136 |
} catch (error: any) {
|
137 |
+
// 记录错误信息
|
138 |
+
const errorInfo = {
|
139 |
+
email: email,
|
140 |
+
error: error instanceof Error ? error.message : String(error),
|
141 |
+
timestamp: new Date().toISOString(),
|
142 |
+
debugId: debugId
|
143 |
+
};
|
144 |
+
|
145 |
+
try {
|
146 |
+
await saveErrorInfo(debugId, errorInfo);
|
147 |
+
} catch (saveError) {
|
148 |
+
console.error('Failed to save error info:', saveError);
|
149 |
+
}
|
150 |
+
|
151 |
return {
|
152 |
success: false,
|
153 |
error: error.message
|
154 |
};
|
155 |
+
} finally {
|
156 |
+
// 截取最后的页面截图
|
157 |
+
if (page) {
|
158 |
+
try {
|
159 |
+
const screenshot = await page.screenshot({
|
160 |
+
fullPage: true,
|
161 |
+
type: 'png'
|
162 |
+
});
|
163 |
+
|
164 |
+
const screenshotInfo = {
|
165 |
+
email: email,
|
166 |
+
timestamp: new Date().toISOString(),
|
167 |
+
debugId: debugId,
|
168 |
+
url: page.url(),
|
169 |
+
title: await page.title().catch(() => 'Unknown')
|
170 |
+
};
|
171 |
+
|
172 |
+
// 保存截图和调试信息到本地文件
|
173 |
+
await saveScreenshot(debugId, screenshot);
|
174 |
+
await saveDebugInfo(debugId, screenshotInfo);
|
175 |
+
|
176 |
+
console.log(`Screenshot saved for debug: ${debugId}`);
|
177 |
+
} catch (screenshotError) {
|
178 |
+
console.error('Failed to take screenshot:', screenshotError);
|
179 |
+
}
|
180 |
+
}
|
181 |
+
|
182 |
+
if (context) await context.close();
|
183 |
}
|
184 |
}
|
185 |
|
|
|
214 |
console.log(account.email, `没有新版切换到密码登录,继续执行: ${error}`);
|
215 |
}
|
216 |
|
217 |
+
try {
|
218 |
+
await page.waitForURL("https://login.live.com/**", { timeout: 30000 });
|
219 |
+
// 填写密码 - 双重填写确保成功
|
220 |
+
await page.fill('input[type="password"]', account.password);
|
221 |
+
await page.waitForTimeout(500); // 等待页面稳定
|
222 |
+
await page.fill('input[type="password"]', account.password);
|
223 |
+
await page.click('button[type="submit"]');
|
224 |
+
await page.waitForTimeout(2000); // 等待提交处理
|
225 |
+
} catch (error) {
|
226 |
+
console.log(account.email, `填写密码失败: ${error}`);
|
227 |
+
}
|
228 |
|
229 |
|
230 |
const proofEmail = account.proofEmail;
|
|
|
386 |
}, { timeout: 3000 });
|
387 |
await page.click('button[type="submit"]#acceptButton', { timeout: 3000 });
|
388 |
} catch (error) {
|
389 |
+
console.log(account.email, `无旧版的登录确认,继续执行: ${error}`);
|
390 |
}
|
391 |
}
|
392 |
|
393 |
private async handleConsent(page: Page, redirectUri: string) {
|
394 |
try {
|
395 |
+
await page.waitForURL("https://account.live.com/Consent/**", { timeout: 20000 });
|
396 |
await page.click('button[type="submit"][data-testid="appConsentPrimaryButton"]');
|
397 |
} catch (error) {
|
398 |
console.log("Consent page not found or timeout, skipping...");
|
functions/utils/debugStorage.ts
ADDED
@@ -0,0 +1,135 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import fs from 'fs/promises';
|
2 |
+
import path from 'path';
|
3 |
+
|
4 |
+
const DEBUG_DIR = 'debug';
|
5 |
+
|
6 |
+
// 确保调试目录存在
|
7 |
+
async function ensureDebugDir() {
|
8 |
+
try {
|
9 |
+
await fs.access(DEBUG_DIR);
|
10 |
+
} catch {
|
11 |
+
await fs.mkdir(DEBUG_DIR, { recursive: true });
|
12 |
+
}
|
13 |
+
}
|
14 |
+
|
15 |
+
// 保存截图
|
16 |
+
export async function saveScreenshot(debugId: string, screenshot: Buffer): Promise<void> {
|
17 |
+
await ensureDebugDir();
|
18 |
+
const screenshotPath = path.join(DEBUG_DIR, `${debugId}.png`);
|
19 |
+
await fs.writeFile(screenshotPath, screenshot);
|
20 |
+
}
|
21 |
+
|
22 |
+
// 保存调试信息
|
23 |
+
export async function saveDebugInfo(debugId: string, info: any): Promise<void> {
|
24 |
+
await ensureDebugDir();
|
25 |
+
const infoPath = path.join(DEBUG_DIR, `${debugId}.json`);
|
26 |
+
await fs.writeFile(infoPath, JSON.stringify(info, null, 2));
|
27 |
+
}
|
28 |
+
|
29 |
+
// 保存错误信息
|
30 |
+
export async function saveErrorInfo(debugId: string, error: any): Promise<void> {
|
31 |
+
await ensureDebugDir();
|
32 |
+
const errorPath = path.join(DEBUG_DIR, `${debugId}_error.json`);
|
33 |
+
await fs.writeFile(errorPath, JSON.stringify(error, null, 2));
|
34 |
+
}
|
35 |
+
|
36 |
+
// 获取所有调试信息
|
37 |
+
export async function getAllDebugInfo(): Promise<any[]> {
|
38 |
+
try {
|
39 |
+
await ensureDebugDir();
|
40 |
+
const files = await fs.readdir(DEBUG_DIR);
|
41 |
+
const debugItems = [];
|
42 |
+
|
43 |
+
// 获取所有 .json 文件(排除 _error.json)
|
44 |
+
const infoFiles = files.filter(file => file.endsWith('.json') && !file.endsWith('_error.json'));
|
45 |
+
|
46 |
+
for (const file of infoFiles) {
|
47 |
+
try {
|
48 |
+
const debugId = file.replace('.json', '');
|
49 |
+
const infoPath = path.join(DEBUG_DIR, file);
|
50 |
+
const errorPath = path.join(DEBUG_DIR, `${debugId}_error.json`);
|
51 |
+
const screenshotPath = path.join(DEBUG_DIR, `${debugId}.png`);
|
52 |
+
|
53 |
+
// 读取基本信息
|
54 |
+
const infoData = await fs.readFile(infoPath, 'utf-8');
|
55 |
+
const info = JSON.parse(infoData);
|
56 |
+
|
57 |
+
// 检查是否有错误信息
|
58 |
+
let errorInfo = null;
|
59 |
+
try {
|
60 |
+
const errorData = await fs.readFile(errorPath, 'utf-8');
|
61 |
+
errorInfo = JSON.parse(errorData);
|
62 |
+
} catch {
|
63 |
+
// 没有错误文件,忽略
|
64 |
+
}
|
65 |
+
|
66 |
+
// 检查是否有截图
|
67 |
+
let hasScreenshot = false;
|
68 |
+
try {
|
69 |
+
await fs.access(screenshotPath);
|
70 |
+
hasScreenshot = true;
|
71 |
+
} catch {
|
72 |
+
// 没有截图文件,忽略
|
73 |
+
}
|
74 |
+
|
75 |
+
debugItems.push({
|
76 |
+
debugId,
|
77 |
+
...info,
|
78 |
+
hasError: !!errorInfo,
|
79 |
+
errorMessage: errorInfo?.error || null,
|
80 |
+
hasScreenshot,
|
81 |
+
screenshotUrl: hasScreenshot ? `/api/debug/screenshot?id=${debugId}` : null
|
82 |
+
});
|
83 |
+
} catch (error) {
|
84 |
+
console.error(`Error reading debug file ${file}:`, error);
|
85 |
+
}
|
86 |
+
}
|
87 |
+
|
88 |
+
// 按时间戳排序(最新的在前)
|
89 |
+
debugItems.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
90 |
+
|
91 |
+
return debugItems;
|
92 |
+
} catch (error) {
|
93 |
+
console.error('Error reading debug directory:', error);
|
94 |
+
return [];
|
95 |
+
}
|
96 |
+
}
|
97 |
+
|
98 |
+
// 获取截图
|
99 |
+
export async function getScreenshot(debugId: string): Promise<Buffer | null> {
|
100 |
+
try {
|
101 |
+
const screenshotPath = path.join(DEBUG_DIR, `${debugId}.png`);
|
102 |
+
return await fs.readFile(screenshotPath);
|
103 |
+
} catch {
|
104 |
+
return null;
|
105 |
+
}
|
106 |
+
}
|
107 |
+
|
108 |
+
// 清理旧的调试文件
|
109 |
+
export async function cleanupOldDebugFiles(daysToKeep: number = 7): Promise<number> {
|
110 |
+
try {
|
111 |
+
await ensureDebugDir();
|
112 |
+
const files = await fs.readdir(DEBUG_DIR);
|
113 |
+
const cutoffTime = Date.now() - (daysToKeep * 24 * 60 * 60 * 1000);
|
114 |
+
let deletedCount = 0;
|
115 |
+
|
116 |
+
for (const file of files) {
|
117 |
+
try {
|
118 |
+
const filePath = path.join(DEBUG_DIR, file);
|
119 |
+
const stats = await fs.stat(filePath);
|
120 |
+
|
121 |
+
if (stats.mtime.getTime() < cutoffTime) {
|
122 |
+
await fs.unlink(filePath);
|
123 |
+
deletedCount++;
|
124 |
+
}
|
125 |
+
} catch (error) {
|
126 |
+
console.error(`Error processing file ${file}:`, error);
|
127 |
+
}
|
128 |
+
}
|
129 |
+
|
130 |
+
return deletedCount;
|
131 |
+
} catch (error) {
|
132 |
+
console.error('Error cleaning up debug files:', error);
|
133 |
+
return 0;
|
134 |
+
}
|
135 |
+
}
|
index.ts
CHANGED
@@ -30,6 +30,9 @@ import { onRequest as handleMailSend } from './functions/api/mail/send.js'
|
|
30 |
import { onRequest as handleBatch } from './functions/api/mail/batch.js'
|
31 |
import { onRequest as handleMailStatus } from './functions/api/mail/status.js'
|
32 |
import { onRequest as handleMailActivate } from './functions/api/mail/activate.js'
|
|
|
|
|
|
|
33 |
dotenv.config({ path: ['.env', '.env.local'], override: true });
|
34 |
const isDev = process.env.NODE_ENV === 'development'
|
35 |
|
@@ -64,6 +67,13 @@ const kv: KVNamespace = {
|
|
64 |
},
|
65 |
delete: async (key: string) => {
|
66 |
await storage.removeItem(key);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
67 |
}
|
68 |
};
|
69 |
|
@@ -169,7 +179,18 @@ app.all('/api/*', async (c) => {
|
|
169 |
case '/api/mail/activate':
|
170 |
response = await handleMailActivate(context);
|
171 |
break;
|
|
|
|
|
|
|
|
|
|
|
|
|
172 |
default:
|
|
|
|
|
|
|
|
|
|
|
173 |
return c.json({ error: 'Route not found' }, 404);
|
174 |
}
|
175 |
return response;
|
|
|
30 |
import { onRequest as handleBatch } from './functions/api/mail/batch.js'
|
31 |
import { onRequest as handleMailStatus } from './functions/api/mail/status.js'
|
32 |
import { onRequest as handleMailActivate } from './functions/api/mail/activate.js'
|
33 |
+
import { onRequestGET as handleDebugList } from './functions/api/debug/list.js'
|
34 |
+
import { onRequestGET as handleDebugScreenshot } from './functions/api/debug/screenshot.js'
|
35 |
+
import { onRequestPOST as handleDebugCleanup } from './functions/api/debug/cleanup.js'
|
36 |
dotenv.config({ path: ['.env', '.env.local'], override: true });
|
37 |
const isDev = process.env.NODE_ENV === 'development'
|
38 |
|
|
|
67 |
},
|
68 |
delete: async (key: string) => {
|
69 |
await storage.removeItem(key);
|
70 |
+
},
|
71 |
+
list: async (options?: { prefix?: string }) => {
|
72 |
+
const keys = await storage.getKeys(options?.prefix);
|
73 |
+
console.log(options?.prefix,keys)
|
74 |
+
return {
|
75 |
+
keys: keys.map(key => ({ name: key }))
|
76 |
+
};
|
77 |
}
|
78 |
};
|
79 |
|
|
|
179 |
case '/api/mail/activate':
|
180 |
response = await handleMailActivate(context);
|
181 |
break;
|
182 |
+
case '/api/debug/list':
|
183 |
+
response = await handleDebugList(context);
|
184 |
+
break;
|
185 |
+
case '/api/debug/cleanup':
|
186 |
+
response = await handleDebugCleanup(context);
|
187 |
+
break;
|
188 |
default:
|
189 |
+
// 处理带参数的路由
|
190 |
+
if (path.startsWith('/api/debug/screenshot')) {
|
191 |
+
response = await handleDebugScreenshot(context);
|
192 |
+
break;
|
193 |
+
}
|
194 |
return c.json({ error: 'Route not found' }, 404);
|
195 |
}
|
196 |
return response;
|
src/App.vue
CHANGED
@@ -24,6 +24,11 @@ const menu = [
|
|
24 |
name: '设置',
|
25 |
path: '/setting',
|
26 |
icon: 'setting-1'
|
|
|
|
|
|
|
|
|
|
|
27 |
}
|
28 |
];
|
29 |
|
|
|
24 |
name: '设置',
|
25 |
path: '/setting',
|
26 |
icon: 'setting-1'
|
27 |
+
},
|
28 |
+
{
|
29 |
+
name: '调试',
|
30 |
+
path: '/debug',
|
31 |
+
icon: 'bug'
|
32 |
}
|
33 |
];
|
34 |
|
src/router/index.ts
CHANGED
@@ -33,6 +33,12 @@ const router = createRouter({
|
|
33 |
component: () => import('../views/AccountView.vue'),
|
34 |
meta: { requiresAuth: true }
|
35 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
36 |
],
|
37 |
})
|
38 |
// 添加路由守卫
|
|
|
33 |
component: () => import('../views/AccountView.vue'),
|
34 |
meta: { requiresAuth: true }
|
35 |
},
|
36 |
+
{
|
37 |
+
path: '/debug',
|
38 |
+
name: 'Debug',
|
39 |
+
component: () => import('../views/DebugView.vue'),
|
40 |
+
meta: { requiresAuth: true }
|
41 |
+
},
|
42 |
],
|
43 |
})
|
44 |
// 添加路由守卫
|
src/views/DebugView.vue
ADDED
@@ -0,0 +1,161 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<template>
|
2 |
+
<div class="debug-view p-6">
|
3 |
+
<div class="mb-4">
|
4 |
+
<t-button theme="primary" @click="loadDebugData" :loading="loading">
|
5 |
+
<template #icon>
|
6 |
+
<t-icon name="refresh" />
|
7 |
+
</template>
|
8 |
+
刷新数据
|
9 |
+
</t-button>
|
10 |
+
</div>
|
11 |
+
|
12 |
+
<t-loading :loading="loading" text="正在加载调试数据...">
|
13 |
+
<div v-if="error" class="mb-4">
|
14 |
+
<t-alert theme="error" :message="error" />
|
15 |
+
</div>
|
16 |
+
|
17 |
+
<div v-if="!loading && debugList.length === 0" class="text-center py-12">
|
18 |
+
<t-icon name="inbox" size="48px" class="text-gray-400 mb-4" />
|
19 |
+
<p class="text-gray-500">暂无调试数据</p>
|
20 |
+
</div>
|
21 |
+
|
22 |
+
<div v-else class="space-y-4">
|
23 |
+
<t-card
|
24 |
+
v-for="item in debugList"
|
25 |
+
:key="item.debugId"
|
26 |
+
:class="{ 'border-red-200': item.hasError }"
|
27 |
+
class="cursor-pointer hover:shadow-md transition-shadow"
|
28 |
+
@click="toggleDebugContent(item.debugId)"
|
29 |
+
>
|
30 |
+
<template #header>
|
31 |
+
<div class="flex justify-between items-center">
|
32 |
+
<div class="flex items-center space-x-3">
|
33 |
+
<strong class="text-lg">{{ item.email }}</strong>
|
34 |
+
<t-tag
|
35 |
+
:theme="item.hasError ? 'danger' : 'success'"
|
36 |
+
variant="light"
|
37 |
+
>
|
38 |
+
{{ item.hasError ? '失败' : '成功' }}
|
39 |
+
</t-tag>
|
40 |
+
<span class="text-gray-500 text-sm">
|
41 |
+
{{ formatTime(item.timestamp) }}
|
42 |
+
</span>
|
43 |
+
</div>
|
44 |
+
<t-icon
|
45 |
+
:name="expandedItems.has(item.debugId) ? 'chevron-up' : 'chevron-down'"
|
46 |
+
class="text-gray-400"
|
47 |
+
/>
|
48 |
+
</div>
|
49 |
+
</template>
|
50 |
+
|
51 |
+
<div v-if="expandedItems.has(item.debugId)" class="space-y-4">
|
52 |
+
<div v-if="item.hasError" class="mb-4">
|
53 |
+
<t-alert theme="error" :message="`错误信息: ${item.errorMessage}`" />
|
54 |
+
</div>
|
55 |
+
|
56 |
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
|
57 |
+
<div>
|
58 |
+
<span class="font-medium text-gray-700">调试ID:</span>
|
59 |
+
<span class="ml-2 text-gray-600 font-mono">{{ item.debugId }}</span>
|
60 |
+
</div>
|
61 |
+
<div>
|
62 |
+
<span class="font-medium text-gray-700">邮箱:</span>
|
63 |
+
<span class="ml-2 text-gray-600">{{ item.email }}</span>
|
64 |
+
</div>
|
65 |
+
<div>
|
66 |
+
<span class="font-medium text-gray-700">时间:</span>
|
67 |
+
<span class="ml-2 text-gray-600">{{ formatTime(item.timestamp) }}</span>
|
68 |
+
</div>
|
69 |
+
<div>
|
70 |
+
<span class="font-medium text-gray-700">页面标题:</span>
|
71 |
+
<span class="ml-2 text-gray-600">{{ item.title }}</span>
|
72 |
+
</div>
|
73 |
+
<div class="md:col-span-2">
|
74 |
+
<span class="font-medium text-gray-700">页面URL:</span>
|
75 |
+
<span class="ml-2 text-gray-600 break-all">{{ item.url }}</span>
|
76 |
+
</div>
|
77 |
+
</div>
|
78 |
+
|
79 |
+
<div>
|
80 |
+
<h4 class="font-medium text-gray-700 mb-2">页面截图:</h4>
|
81 |
+
<div class="border rounded-lg overflow-hidden bg-gray-50">
|
82 |
+
<img
|
83 |
+
:src="item.screenshotUrl"
|
84 |
+
:alt="`${item.email} 的页面截图`"
|
85 |
+
class="w-full h-auto max-h-96 object-contain"
|
86 |
+
loading="lazy"
|
87 |
+
@error="handleImageError"
|
88 |
+
/>
|
89 |
+
</div>
|
90 |
+
</div>
|
91 |
+
</div>
|
92 |
+
</t-card>
|
93 |
+
</div>
|
94 |
+
</t-loading>
|
95 |
+
</div>
|
96 |
+
</template>
|
97 |
+
|
98 |
+
<script setup lang="ts">
|
99 |
+
import { ref, onMounted } from 'vue'
|
100 |
+
|
101 |
+
interface DebugItem {
|
102 |
+
debugId: string
|
103 |
+
email: string
|
104 |
+
timestamp: string
|
105 |
+
url: string
|
106 |
+
title: string
|
107 |
+
hasError: boolean
|
108 |
+
errorMessage?: string
|
109 |
+
screenshotUrl: string
|
110 |
+
}
|
111 |
+
|
112 |
+
const loading = ref(false)
|
113 |
+
const error = ref('')
|
114 |
+
const debugList = ref<DebugItem[]>([])
|
115 |
+
const expandedItems = ref(new Set<string>())
|
116 |
+
|
117 |
+
const loadDebugData = async () => {
|
118 |
+
loading.value = true
|
119 |
+
error.value = ''
|
120 |
+
|
121 |
+
try {
|
122 |
+
const response = await fetch('/api/debug/list')
|
123 |
+
const result = await response.json()
|
124 |
+
|
125 |
+
if (result.success) {
|
126 |
+
debugList.value = result.data
|
127 |
+
} else {
|
128 |
+
error.value = `加载失败: ${result.error}`
|
129 |
+
}
|
130 |
+
} catch (err) {
|
131 |
+
error.value = `网络错误: ${err instanceof Error ? err.message : String(err)}`
|
132 |
+
} finally {
|
133 |
+
loading.value = false
|
134 |
+
}
|
135 |
+
}
|
136 |
+
|
137 |
+
const toggleDebugContent = (debugId: string) => {
|
138 |
+
if (expandedItems.value.has(debugId)) {
|
139 |
+
expandedItems.value.delete(debugId)
|
140 |
+
} else {
|
141 |
+
expandedItems.value.add(debugId)
|
142 |
+
}
|
143 |
+
}
|
144 |
+
|
145 |
+
const formatTime = (timestamp: string) => {
|
146 |
+
return new Date(timestamp).toLocaleString('zh-CN')
|
147 |
+
}
|
148 |
+
|
149 |
+
const handleImageError = (event: Event) => {
|
150 |
+
const img = event.target as HTMLImageElement
|
151 |
+
img.style.display = 'none'
|
152 |
+
const parent = img.parentElement
|
153 |
+
if (parent) {
|
154 |
+
parent.innerHTML = '<div class="text-center py-8 text-gray-500">截图加载失败</div>'
|
155 |
+
}
|
156 |
+
}
|
157 |
+
|
158 |
+
onMounted(() => {
|
159 |
+
loadDebugData()
|
160 |
+
})
|
161 |
+
</script>
|