tanyu_admin 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,202 @@
1
+ # Design System Master File
2
+
3
+ > **LOGIC:** When building a specific page, first check `design-system/pages/[page-name].md`.
4
+ > If that file exists, its rules **override** this Master file.
5
+ > If not, strictly follow the rules below.
6
+
7
+ ---
8
+
9
+ **Project:** Virtual Host Management
10
+ **Generated:** 2026-01-24 20:36:45
11
+ **Category:** Analytics Dashboard
12
+
13
+ ---
14
+
15
+ ## Global Rules
16
+
17
+ ### Color Palette
18
+
19
+ | Role | Hex | CSS Variable |
20
+ |------|-----|--------------|
21
+ | Primary | `#7C3AED` | `--color-primary` |
22
+ | Secondary | `#A78BFA` | `--color-secondary` |
23
+ | CTA/Accent | `#F97316` | `--color-cta` |
24
+ | Background | `#FAF5FF` | `--color-background` |
25
+ | Text | `#4C1D95` | `--color-text` |
26
+
27
+ **Color Notes:** Excitement purple + action orange
28
+
29
+ ### Typography
30
+
31
+ - **Heading Font:** Fira Code
32
+ - **Body Font:** Fira Sans
33
+ - **Mood:** dashboard, data, analytics, code, technical, precise
34
+ - **Google Fonts:** [Fira Code + Fira Sans](https://fonts.google.com/share?selection.family=Fira+Code:wght@400;500;600;700|Fira+Sans:wght@300;400;500;600;700)
35
+
36
+ **CSS Import:**
37
+ ```css
38
+ @import url('https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500;600;700&family=Fira+Sans:wght@300;400;500;600;700&display=swap');
39
+ ```
40
+
41
+ ### Spacing Variables
42
+
43
+ | Token | Value | Usage |
44
+ |-------|-------|-------|
45
+ | `--space-xs` | `4px` / `0.25rem` | Tight gaps |
46
+ | `--space-sm` | `8px` / `0.5rem` | Icon gaps, inline spacing |
47
+ | `--space-md` | `16px` / `1rem` | Standard padding |
48
+ | `--space-lg` | `24px` / `1.5rem` | Section padding |
49
+ | `--space-xl` | `32px` / `2rem` | Large gaps |
50
+ | `--space-2xl` | `48px` / `3rem` | Section margins |
51
+ | `--space-3xl` | `64px` / `4rem` | Hero padding |
52
+
53
+ ### Shadow Depths
54
+
55
+ | Level | Value | Usage |
56
+ |-------|-------|-------|
57
+ | `--shadow-sm` | `0 1px 2px rgba(0,0,0,0.05)` | Subtle lift |
58
+ | `--shadow-md` | `0 4px 6px rgba(0,0,0,0.1)` | Cards, buttons |
59
+ | `--shadow-lg` | `0 10px 15px rgba(0,0,0,0.1)` | Modals, dropdowns |
60
+ | `--shadow-xl` | `0 20px 25px rgba(0,0,0,0.15)` | Hero images, featured cards |
61
+
62
+ ---
63
+
64
+ ## Component Specs
65
+
66
+ ### Buttons
67
+
68
+ ```css
69
+ /* Primary Button */
70
+ .btn-primary {
71
+ background: #F97316;
72
+ color: white;
73
+ padding: 12px 24px;
74
+ border-radius: 8px;
75
+ font-weight: 600;
76
+ transition: all 200ms ease;
77
+ cursor: pointer;
78
+ }
79
+
80
+ .btn-primary:hover {
81
+ opacity: 0.9;
82
+ transform: translateY(-1px);
83
+ }
84
+
85
+ /* Secondary Button */
86
+ .btn-secondary {
87
+ background: transparent;
88
+ color: #7C3AED;
89
+ border: 2px solid #7C3AED;
90
+ padding: 12px 24px;
91
+ border-radius: 8px;
92
+ font-weight: 600;
93
+ transition: all 200ms ease;
94
+ cursor: pointer;
95
+ }
96
+ ```
97
+
98
+ ### Cards
99
+
100
+ ```css
101
+ .card {
102
+ background: #FAF5FF;
103
+ border-radius: 12px;
104
+ padding: 24px;
105
+ box-shadow: var(--shadow-md);
106
+ transition: all 200ms ease;
107
+ cursor: pointer;
108
+ }
109
+
110
+ .card:hover {
111
+ box-shadow: var(--shadow-lg);
112
+ transform: translateY(-2px);
113
+ }
114
+ ```
115
+
116
+ ### Inputs
117
+
118
+ ```css
119
+ .input {
120
+ padding: 12px 16px;
121
+ border: 1px solid #E2E8F0;
122
+ border-radius: 8px;
123
+ font-size: 16px;
124
+ transition: border-color 200ms ease;
125
+ }
126
+
127
+ .input:focus {
128
+ border-color: #7C3AED;
129
+ outline: none;
130
+ box-shadow: 0 0 0 3px #7C3AED20;
131
+ }
132
+ ```
133
+
134
+ ### Modals
135
+
136
+ ```css
137
+ .modal-overlay {
138
+ background: rgba(0, 0, 0, 0.5);
139
+ backdrop-filter: blur(4px);
140
+ }
141
+
142
+ .modal {
143
+ background: white;
144
+ border-radius: 16px;
145
+ padding: 32px;
146
+ box-shadow: var(--shadow-xl);
147
+ max-width: 500px;
148
+ width: 90%;
149
+ }
150
+ ```
151
+
152
+ ---
153
+
154
+ ## Style Guidelines
155
+
156
+ **Style:** Data-Dense Dashboard
157
+
158
+ **Keywords:** Multiple charts/widgets, data tables, KPI cards, minimal padding, grid layout, space-efficient, maximum data visibility
159
+
160
+ **Best For:** Business intelligence dashboards, financial analytics, enterprise reporting, operational dashboards, data warehousing
161
+
162
+ **Key Effects:** Hover tooltips, chart zoom on click, row highlighting on hover, smooth filter animations, data loading spinners
163
+
164
+ ### Page Pattern
165
+
166
+ **Pattern Name:** Data-Dense + Drill-Down
167
+
168
+ - **CTA Placement:** Above fold
169
+ - **Section Order:** Hero > Features > CTA
170
+
171
+ ---
172
+
173
+ ## Anti-Patterns (Do NOT Use)
174
+
175
+ - ❌ Ornate design
176
+ - ❌ No filtering
177
+
178
+ ### Additional Forbidden Patterns
179
+
180
+ - ❌ **Emojis as icons** — Use SVG icons (Heroicons, Lucide, Simple Icons)
181
+ - ❌ **Missing cursor:pointer** — All clickable elements must have cursor:pointer
182
+ - ❌ **Layout-shifting hovers** — Avoid scale transforms that shift layout
183
+ - ❌ **Low contrast text** — Maintain 4.5:1 minimum contrast ratio
184
+ - ❌ **Instant state changes** — Always use transitions (150-300ms)
185
+ - ❌ **Invisible focus states** — Focus states must be visible for a11y
186
+
187
+ ---
188
+
189
+ ## Pre-Delivery Checklist
190
+
191
+ Before delivering any UI code, verify:
192
+
193
+ - [ ] No emojis used as icons (use SVG instead)
194
+ - [ ] All icons from consistent icon set (Heroicons/Lucide)
195
+ - [ ] `cursor-pointer` on all clickable elements
196
+ - [ ] Hover states with smooth transitions (150-300ms)
197
+ - [ ] Light mode: text contrast 4.5:1 minimum
198
+ - [ ] Focus states visible for keyboard navigation
199
+ - [ ] `prefers-reduced-motion` respected
200
+ - [ ] Responsive: 375px, 768px, 1024px, 1440px
201
+ - [ ] No content hidden behind fixed navbars
202
+ - [ ] No horizontal scroll on mobile
@@ -0,0 +1,5 @@
1
+ Traceback (most recent call last):
2
+ File "D:\workspace\demo\admin\.claude\skills\ui-ux-pro-max\scripts\search.py", line 76, in <module>
3
+ print(result)
4
+ ~~~~~^^^^^^^^
5
+ UnicodeEncodeError: 'gbk' codec can't encode character '\u26a1' in position 505: illegal multibyte sequence
@@ -0,0 +1,50 @@
1
+ ## Design System: Virtual Host Management
2
+
3
+ ### Pattern
4
+ - **Name:** Data-Dense + Drill-Down
5
+ - **CTA Placement:** Above fold
6
+ - **Sections:** Hero > Features > CTA
7
+
8
+ ### Style
9
+ - **Name:** Data-Dense Dashboard
10
+ - **Keywords:** Multiple charts/widgets, data tables, KPI cards, minimal padding, grid layout, space-efficient, maximum data visibility
11
+ - **Best For:** Business intelligence dashboards, financial analytics, enterprise reporting, operational dashboards, data warehousing
12
+ - **Performance:** ⚡ Excellent | **Accessibility:** ✓ WCAG AA
13
+
14
+ ### Colors
15
+ | Role | Hex |
16
+ |------|-----|
17
+ | Primary | #7C3AED |
18
+ | Secondary | #A78BFA |
19
+ | CTA | #F97316 |
20
+ | Background | #FAF5FF |
21
+ | Text | #4C1D95 |
22
+
23
+ *Notes: Excitement purple + action orange*
24
+
25
+ ### Typography
26
+ - **Heading:** Fira Code
27
+ - **Body:** Fira Sans
28
+ - **Mood:** dashboard, data, analytics, code, technical, precise
29
+ - **Best For:** Dashboards, analytics, data visualization, admin panels
30
+ - **Google Fonts:** https://fonts.google.com/share?selection.family=Fira+Code:wght@400;500;600;700|Fira+Sans:wght@300;400;500;600;700
31
+ - **CSS Import:**
32
+ ```css
33
+ @import url('https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500;600;700&family=Fira+Sans:wght@300;400;500;600;700&display=swap');
34
+ ```
35
+
36
+ ### Key Effects
37
+ Hover tooltips, chart zoom on click, row highlighting on hover, smooth filter animations, data loading spinners
38
+
39
+ ### Avoid (Anti-patterns)
40
+ - Ornate design
41
+ - No filtering
42
+
43
+ ### Pre-Delivery Checklist
44
+ - [ ] No emojis as icons (use SVG: Heroicons/Lucide)
45
+ - [ ] cursor-pointer on all clickable elements
46
+ - [ ] Hover states with smooth transitions (150-300ms)
47
+ - [ ] Light mode: text contrast 4.5:1 minimum
48
+ - [ ] Focus states visible for keyboard nav
49
+ - [ ] prefers-reduced-motion respected
50
+ - [ ] Responsive: 375px, 768px, 1024px, 1440px
File without changes
@@ -0,0 +1,18 @@
1
+ version: '3.8'
2
+
3
+ services:
4
+ admin:
5
+ image: ghcr.io/krystalqaq/unicom-login:latest
6
+ container_name: modem-admin
7
+ restart: unless-stopped
8
+ ports:
9
+ - "3000:3000"
10
+ volumes:
11
+ - ./virtual_hosts.json:/app/virtual_hosts.json
12
+ - ./.cookie_cache.json:/app/.cookie_cache.json
13
+ - ./.cache:/app/.cache
14
+ network_mode: host
15
+ environment:
16
+ - NODE_ENV=production
17
+ mem_limit: 128m
18
+ cpus: 0.5
package/index.js ADDED
@@ -0,0 +1,404 @@
1
+ const { axios } = require('./axios-interceptor');
2
+ const crypto = require('crypto');
3
+ const qs = require('qs');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const iconv = require('iconv-lite');
7
+
8
+ // 配置信息
9
+ const config = {
10
+ baseUrl: 'http://192.168.1.1',
11
+ // accountType: 'admin' | 'user'
12
+ accountType: 'admin', // 管理员账户
13
+ password: 'Krystal1024', // 你的密码
14
+ // Cookie缓存配置
15
+ cookieCacheFile: path.join(__dirname, '.cookie_cache.json'),
16
+ cookieExpireTime: 30 * 60 * 1000 // 30分钟过期
17
+ };
18
+
19
+ /**
20
+ * 加载缓存的cookie和session_token
21
+ */
22
+ function loadCookieCache() {
23
+ try {
24
+ if (fs.existsSync(config.cookieCacheFile)) {
25
+ const data = fs.readFileSync(config.cookieCacheFile, 'utf8');
26
+ const cache = JSON.parse(data);
27
+ const now = Date.now();
28
+ // 检查是否过期
29
+ if (now - cache.timestamp < config.cookieExpireTime) {
30
+ const expireTime = Math.ceil((config.cookieExpireTime - (now - cache.timestamp)) / 1000 / 60);
31
+ // console.log(`使用缓存的Cookie (剩余 ${expireTime} 分钟有效期)`);
32
+ return {
33
+ cookie: cache.cookie,
34
+ sessionToken: cache.sessionToken || null
35
+ };
36
+ } else {
37
+ console.log('缓存的Cookie已过期');
38
+ }
39
+ }
40
+ } catch (error) {
41
+ console.log('读取Cookie缓存失败:', error.message);
42
+ }
43
+ return null;
44
+ }
45
+
46
+ /**
47
+ * 保存cookie和session_token到缓存
48
+ */
49
+ function saveCookieCache(cookie, sessionToken = null) {
50
+ try {
51
+ const cache = {
52
+ cookie: cookie,
53
+ sessionToken: sessionToken,
54
+ timestamp: Date.now()
55
+ };
56
+ fs.writeFileSync(config.cookieCacheFile, JSON.stringify(cache, null, 2));
57
+ // console.log('Cookie和SessionToken已缓存');
58
+ } catch (error) {
59
+ console.log('保存Cookie缓存失败:', error.message);
60
+ }
61
+ }
62
+
63
+ /**
64
+ * 从HTML中提取_SESSION_TOKEN
65
+ * 页面格式: var session_token = "JpRX4qbNyNj3eYzdzS5VNwbR";
66
+ */
67
+ function extractSessionToken(html) {
68
+ const match = html.match(/var\s+session_token\s*=\s*"([^"]+)"/);
69
+ return match ? match[1] : null;
70
+ }
71
+
72
+ /**
73
+ * 清除cookie缓存
74
+ */
75
+ function clearCookieCache() {
76
+ try {
77
+ if (fs.existsSync(config.cookieCacheFile)) {
78
+ fs.unlinkSync(config.cookieCacheFile);
79
+ console.log('Cookie缓存已清除');
80
+ }
81
+ } catch (error) {
82
+ console.log('清除Cookie缓存失败:', error.message);
83
+ }
84
+ }
85
+
86
+ /**
87
+ * 从登录页面获取动态token
88
+ */
89
+ async function getLoginTokens() {
90
+ try {
91
+ const response = await axios.get(`${config.baseUrl}/cu.html`, {
92
+ headers: {
93
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36'
94
+ }
95
+ });
96
+
97
+ const html = response.data;
98
+
99
+ // 解析 Frm_Logintoken: document.getElementById("Frm_Logintoken").value = "20";
100
+ const loginTokenMatch = html.match(/getElementById\("Frm_Logintoken"\)\.value\s*=\s*"(\d+)"/);
101
+ const frmLogintoken = loginTokenMatch ? loginTokenMatch[1] : '20';
102
+
103
+ // 解析 Frm_Loginchecktoken: document.getElementById("Frm_Loginchecktoken").value = "JrGiWD8ZjsY0q1M7Oy9pystC";
104
+ const checkTokenMatch = html.match(/getElementById\("Frm_Loginchecktoken"\)\.value\s*=\s*"([^"]+)"/);
105
+ const frmLoginchecktoken = checkTokenMatch ? checkTokenMatch[1] : '';
106
+
107
+ // console.log('获取到的Token:');
108
+ console.log(` Frm_Logintoken: ${frmLogintoken}`);
109
+ console.log(` Frm_Loginchecktoken: ${frmLoginchecktoken}`);
110
+
111
+ return { frmLogintoken, frmLoginchecktoken };
112
+ } catch (error) {
113
+ console.error('获取Token失败:', error);
114
+ // 返回默认值
115
+ return { frmLogintoken: '20', frmLoginchecktoken: '' };
116
+ }
117
+ }
118
+
119
+ /**
120
+ * 生成随机数 (10000000-99999999)
121
+ */
122
+ function generateRandomNum() {
123
+ return Math.round(Math.random() * 89999999) + 10000000;
124
+ }
125
+
126
+ /**
127
+ * SHA256加密: sha256(password + randomNum)
128
+ */
129
+ function sha256Encrypt(password, randomNum) {
130
+ const text = password + randomNum;
131
+ return crypto.createHash('sha256').update(text).digest('hex');
132
+ }
133
+
134
+ /**
135
+ * 构建登录请求数据
136
+ */
137
+ function buildLoginData(password, accountType = 'admin', tokens = {}) {
138
+ // 生成随机数
139
+ const userRandomNum = generateRandomNum();
140
+ // SHA256加密密码
141
+ const encryptedPassword = sha256Encrypt(password, userRandomNum);
142
+
143
+ // 管理员: Right=1, _cu_url=1
144
+ // 其他用户: Right=2, _cu_url=0
145
+ const isAdmin = accountType === 'admin';
146
+ // console.log({
147
+ // 'Frm_Logintoken': tokens.frmLogintoken || '20',
148
+ // 'Frm_Loginchecktoken': tokens.frmLoginchecktoken || '',
149
+ // '_cu_url': isAdmin ? '1' : '0',
150
+ // 'Right': isAdmin ? '1' : '2',
151
+ // 'Username': '',
152
+ // 'UserRandomNum': userRandomNum.toString(),
153
+ // 'Password': encryptedPassword,
154
+ // 'action': 'login'
155
+ // });
156
+ return qs.stringify({
157
+ 'Frm_Logintoken': tokens.frmLogintoken || '20',
158
+ 'Frm_Loginchecktoken': tokens.frmLoginchecktoken || '',
159
+ '_cu_url': isAdmin ? '1' : '0',
160
+ 'Right': isAdmin ? '1' : '2',
161
+ 'Username': '',
162
+ 'UserRandomNum': userRandomNum.toString(),
163
+ 'Password': encryptedPassword,
164
+ 'action': 'login'
165
+ });
166
+ }
167
+
168
+ /**
169
+ * 执行登录
170
+ */
171
+ async function login(password, accountType = 'admin') {
172
+ // 先获取动态token
173
+ // console.log('正在获取登录token...');
174
+ const tokens = await getLoginTokens();
175
+
176
+ const data = buildLoginData(password, accountType, tokens);
177
+
178
+ const requestConfig = {
179
+ method: 'POST',
180
+ url: `${config.baseUrl}/cu.html`,
181
+ headers: {
182
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36',
183
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
184
+ 'Content-Type': 'application/x-www-form-urlencoded',
185
+ 'Accept-Language': 'zh-CN,zh;q=0.9',
186
+ 'Cache-Control': 'max-age=0',
187
+ 'Origin': config.baseUrl,
188
+ 'Proxy-Connection': 'keep-alive',
189
+ 'Referer': `${config.baseUrl}/cu.html`,
190
+ 'Upgrade-Insecure-Requests': '1',
191
+ 'Cookie': '_TESTCOOKIESUPPORT=1'
192
+ },
193
+ data: data,
194
+ maxRedirects: 0, // 不自动跟随重定向
195
+ validateStatus: function (status) {
196
+ return status >= 200 && status < 400; // 接受所有2xx和3xx状态码
197
+ }
198
+ };
199
+
200
+ try {
201
+ const response = await axios.request(requestConfig);
202
+ // console.log('Status:', response.status);
203
+ // console.log('Headers:', response.headers);
204
+
205
+ // 检查是否有Set-Cookie (登录成功的标志)
206
+ if (response.headers['set-cookie']) {
207
+ console.log('Login successful! Cookies:', response.headers['set-cookie']);
208
+ return {
209
+ success: true,
210
+ cookies: response.headers['set-cookie'],
211
+ headers: response.headers
212
+ };
213
+ }
214
+
215
+ // 检查重定向
216
+ if (response.status === 302 || response.status === 301) {
217
+ const redirectUrl = response.headers['location'];
218
+ console.log('Redirected to:', redirectUrl);
219
+ return {
220
+ success: true,
221
+ redirectUrl: redirectUrl
222
+ };
223
+ }
224
+
225
+ return {
226
+ success: false,
227
+ message: 'Login response unclear',
228
+ data: response.data
229
+ };
230
+ } catch (error) {
231
+ console.error('Login error:', error.message);
232
+ if (error.response) {
233
+ console.error('Response status:', error.response.status);
234
+ // console.error('Response data:', error.response.data);
235
+ }
236
+ return {
237
+ success: false,
238
+ error: error.message
239
+ };
240
+ }
241
+ }
242
+
243
+ /**
244
+ * 从登录响应中提取会话Cookie
245
+ */
246
+ function getSessionCookie(setCookieHeaders) {
247
+ if (!setCookieHeaders) return '';
248
+ // 提取所有cookie
249
+ const cookies = Array.isArray(setCookieHeaders)
250
+ ? setCookieHeaders.map(c => c.split(';')[0]).join('; ')
251
+ : setCookieHeaders.split(';')[0];
252
+ return cookies;
253
+ }
254
+
255
+ /**
256
+ * 获取页面(需要登录后)
257
+ * 自动处理中文编码问题
258
+ */
259
+ async function fetchPage(path, cookies) {
260
+ const url = path.startsWith('http') ? path : `${config.baseUrl}/${path}`;
261
+ const cookieHeader = cookies || '_TESTCOOKIESUPPORT=1';
262
+
263
+ try {
264
+ const response = await axios.get(url, {
265
+ responseType: 'arraybuffer', // 获取原始二进制数据
266
+ headers: {
267
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36',
268
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
269
+ 'Accept-Language': 'zh-CN,zh;q=0.9',
270
+ 'Cookie': cookieHeader,
271
+ 'Referer': "http://192.168.1.1/getpage.gch?pid=1002&nextpage=sec_fw_alg_t.gch"
272
+ }
273
+ });
274
+
275
+ // 从响应头中获取编码
276
+ let encoding = 'utf8';
277
+ const contentType = response.headers['content-type'] || '';
278
+
279
+ // 检查 charset
280
+ const charsetMatch = contentType.match(/charset=([^\s;]+)/i);
281
+ if (charsetMatch) {
282
+ encoding = charsetMatch[1].toLowerCase().replace(/[^a-z0-9]/g, '');
283
+ }
284
+
285
+ // 常见编码别名映射
286
+ const encodingMap = {
287
+ 'gb2312': 'gbk',
288
+ 'gbk': 'gbk',
289
+ 'gb18030': 'gbk',
290
+ 'utf8': 'utf8',
291
+ 'utf-8': 'utf8',
292
+ 'iso88591': 'latin1'
293
+ };
294
+
295
+ const finalEncoding = encodingMap[encoding] || encoding;
296
+
297
+ // 使用 iconv 解码
298
+ const data = iconv.decode(Buffer.from(response.data), finalEncoding);
299
+ // console.log('请求'+path,data);
300
+ return {
301
+ success: true,
302
+ data: data,
303
+ status: response.status,
304
+ encoding: finalEncoding
305
+ };
306
+ } catch (error) {
307
+ return {
308
+ success: false,
309
+ error: error.message,
310
+ status: error.response?.status
311
+ };
312
+ }
313
+ }
314
+
315
+ // 主函数
316
+ async function main() {
317
+ console.log(`=== ${config.baseUrl} ===`);
318
+ console.log('');
319
+
320
+ let authInfo = loadCookieCache();
321
+ let cookies = authInfo ? authInfo.cookie : null;
322
+ let sessionToken = authInfo ? authInfo.sessionToken : null;
323
+ let needLogin = !cookies;
324
+
325
+ if (!needLogin) {
326
+ // 测试缓存的cookie是否有效
327
+ const testResult = await fetchPage('getpage.gch?pid=1002&nextpage=sec_fw_alg_t.gch', cookies);
328
+ if (!testResult.success || testResult.status === 302) {
329
+ console.log('缓存的Cookie无效,需要重新登录');
330
+ needLogin = true;
331
+ }
332
+ }
333
+
334
+ if (needLogin) {
335
+ console.log('开始登录...');
336
+ const result = await login(config.password, config.accountType);
337
+
338
+ if (result.success) {
339
+ console.log('\nLogin successful!');
340
+ cookies = getSessionCookie(result.cookies);
341
+ console.log('Session Cookie:', cookies);
342
+
343
+ // 登录后获取页面以提取session_token
344
+ const pageUrl = 'getpage.gch?pid=1002&nextpage=app_virtual_conf_t.gch';
345
+ const pageResult = await fetchPage(pageUrl, cookies);
346
+ if (pageResult.success) {
347
+ sessionToken = extractSessionToken(pageResult.data);
348
+ console.log('Session Token:', sessionToken);
349
+ }
350
+
351
+ saveCookieCache(cookies, sessionToken);
352
+ } else {
353
+ console.log('\nLogin failed!');
354
+ console.log(result.message || result.error);
355
+ return;
356
+ }
357
+ }
358
+
359
+ // 获取目标页面
360
+ console.log('\n=== 获取页面 ===');
361
+ const pageUrl = 'getpage.gch?pid=1002&nextpage=app_virtual_conf_t.gch';
362
+ const pageResult = await fetchPage(pageUrl, cookies);
363
+
364
+ if (pageResult.success) {
365
+ console.log(`使用编码: ${pageResult.encoding}`);
366
+ // 如果没有sessionToken,从当前页面提取
367
+ if (!sessionToken) {
368
+ sessionToken = extractSessionToken(pageResult.data);
369
+ if (sessionToken) {
370
+ console.log('从页面提取到Session Token:', sessionToken);
371
+ // 更新缓存
372
+ const auth = loadCookieCache();
373
+ if (auth) {
374
+ saveCookieCache(auth.cookie, sessionToken);
375
+ }
376
+ }
377
+ }
378
+ // console.log('\n页面内容:');
379
+ // console.log(pageResult.data);
380
+ //写入到本地
381
+ fs.writeFileSync('page.html', pageResult.data);
382
+ console.log('\n页面内容已写入到本地 page.html');
383
+ } else {
384
+ console.log('获取页面失败:', pageResult.error);
385
+ }
386
+ }
387
+
388
+ // 如果直接运行此脚本
389
+ // if (require.main === module) {
390
+ // main().catch(console.error);
391
+ // }
392
+
393
+ module.exports = {
394
+ login,
395
+ fetchPage,
396
+ getSessionCookie,
397
+ loadCookieCache,
398
+ saveCookieCache,
399
+ clearCookieCache,
400
+ extractSessionToken,
401
+ buildLoginData,
402
+ sha256Encrypt,
403
+ generateRandomNum
404
+ };