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.
- package/.dockerignore +16 -0
- package/.github/workflows/docker-build.yml +52 -0
- package/CLAUDE.md +85 -0
- package/DEPLOY.md +367 -0
- package/Dockerfile +27 -0
- package/archive/dataCrypto.js +124 -0
- package/archive/delte.js +29 -0
- package/archive/edit.js +29 -0
- package/axios-interceptor-example.js +57 -0
- package/axios-interceptor.js +191 -0
- package/cli.js +313 -0
- package/design-output.txt +5 -0
- package/design-system/virtual-host-management/MASTER.md +202 -0
- package/design-system-output.md +5 -0
- package/design-system-result.md +50 -0
- package/design-system.md +0 -0
- package/docker-compose.yml +18 -0
- package/index.js +404 -0
- package/jsencrypt.min.js +2 -0
- package/modem-api.js +419 -0
- package/package.json +42 -0
- package/parser.js +146 -0
- package/public/index.html +656 -0
- package/readme.md +1 -0
- package/server.js +268 -0
- package/test.js +7 -0
|
@@ -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
|
package/design-system.md
ADDED
|
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
|
+
};
|