init
This commit is contained in:
54
.gitignore
vendored
Normal file
54
.gitignore
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
|
||||
# Build directory
|
||||
build/
|
||||
dist/
|
||||
|
||||
# IDE files
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS generated files
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# Configuration files with sensitive data
|
||||
config.json
|
||||
.env
|
||||
|
||||
# Node modules (for frontend)
|
||||
frontend/node_modules/
|
||||
|
||||
# Frontend build files
|
||||
frontend/dist/
|
||||
|
||||
# Wails build files
|
||||
bin/
|
||||
36
.golangci.yml
Normal file
36
.golangci.yml
Normal file
@@ -0,0 +1,36 @@
|
||||
run:
|
||||
timeout: 5m
|
||||
modules-download-mode: readonly
|
||||
|
||||
linters:
|
||||
enable:
|
||||
- gofmt
|
||||
- golint
|
||||
- govet
|
||||
- errcheck
|
||||
- staticcheck
|
||||
- gosimple
|
||||
- ineffassign
|
||||
- unused
|
||||
- misspell
|
||||
- gosec
|
||||
|
||||
linters-settings:
|
||||
govet:
|
||||
check-shadowing: true
|
||||
gocyclo:
|
||||
min-complexity: 15
|
||||
dupl:
|
||||
threshold: 100
|
||||
goconst:
|
||||
min-len: 2
|
||||
min-occurrences: 3
|
||||
misspell:
|
||||
locale: US
|
||||
|
||||
issues:
|
||||
exclude-rules:
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- errcheck
|
||||
- gosec
|
||||
50
Dockerfile
Normal file
50
Dockerfile
Normal file
@@ -0,0 +1,50 @@
|
||||
# Dockerfile
|
||||
FROM golang:1.21-alpine AS builder
|
||||
|
||||
# 安装构建依赖
|
||||
RUN apk add --no-cache git nodejs npm
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 复制go模块文件
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
# 复制前端文件
|
||||
COPY frontend/ ./frontend/
|
||||
RUN cd frontend && npm install && npm run build
|
||||
|
||||
# 复制源代码
|
||||
COPY . .
|
||||
|
||||
# 构建应用
|
||||
RUN wails build -clean
|
||||
|
||||
# 运行阶段
|
||||
FROM alpine:latest
|
||||
|
||||
# 安装运行时依赖
|
||||
RUN apk add --no-cache ca-certificates tzdata
|
||||
|
||||
# 创建非root用户
|
||||
RUN addgroup -g 1001 -S appgroup && \
|
||||
adduser -u 1001 -S appuser -G appgroup
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 复制可执行文件
|
||||
COPY --from=builder /app/build/bin/equipment-analyzer .
|
||||
|
||||
# 设置权限
|
||||
RUN chown -R appuser:appgroup /app
|
||||
|
||||
# 切换到非root用户
|
||||
USER appuser
|
||||
|
||||
# 暴露端口(如果需要)
|
||||
EXPOSE 8080
|
||||
|
||||
# 启动应用
|
||||
CMD ["./equipment-analyzer"]
|
||||
66
Makefile
Normal file
66
Makefile
Normal file
@@ -0,0 +1,66 @@
|
||||
# Makefile
|
||||
.PHONY: build clean test lint dev build-web
|
||||
|
||||
# 变量定义
|
||||
BINARY_NAME=equipment-analyzer
|
||||
BUILD_DIR=build
|
||||
WEB_DIR=frontend
|
||||
|
||||
# 构建可执行文件
|
||||
build: build-web
|
||||
@echo "Building $(BINARY_NAME)..."
|
||||
@mkdir -p $(BUILD_DIR)/bin
|
||||
wails build -clean
|
||||
|
||||
# 清理构建文件
|
||||
clean:
|
||||
@echo "Cleaning build files..."
|
||||
@rm -rf $(BUILD_DIR)
|
||||
@rm -rf $(WEB_DIR)/dist
|
||||
|
||||
# 运行测试
|
||||
test:
|
||||
@echo "Running tests..."
|
||||
go test -v ./...
|
||||
|
||||
# 代码检查
|
||||
lint:
|
||||
@echo "Running linter..."
|
||||
golangci-lint run
|
||||
|
||||
# 开发模式
|
||||
dev: build-web
|
||||
@echo "Starting development mode..."
|
||||
wails dev
|
||||
|
||||
# 构建前端
|
||||
build-web:
|
||||
@echo "Building web frontend..."
|
||||
@cd $(WEB_DIR) && npm install && npm run build
|
||||
|
||||
# 构建发布版本
|
||||
release: build-web
|
||||
@echo "Building release version..."
|
||||
wails build -clean
|
||||
|
||||
# 安装依赖
|
||||
deps:
|
||||
@echo "Installing dependencies..."
|
||||
go mod tidy
|
||||
@cd $(WEB_DIR) && npm install
|
||||
|
||||
# 运行集成测试
|
||||
test-integration:
|
||||
@echo "Running integration tests..."
|
||||
go test -v ./test/integration/...
|
||||
|
||||
# 生成文档
|
||||
docs:
|
||||
@echo "Generating documentation..."
|
||||
godoc -http=:6060
|
||||
|
||||
# 初始化项目
|
||||
init:
|
||||
@echo "Initializing project..."
|
||||
wails init -n equipment-analyzer -t react-ts
|
||||
@cd $(WEB_DIR) && npm install antd @ant-design/icons
|
||||
223
README.md
Normal file
223
README.md
Normal file
@@ -0,0 +1,223 @@
|
||||
# 装备数据导出工具
|
||||
|
||||
一个基于Go + Wails + React的桌面应用程序,用于抓取游戏TCP包并解析装备数据。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- 🎯 **TCP包抓取** - 实时抓取游戏客户端的TCP通信数据
|
||||
- 🔍 **数据解析** - 自动解析十六进制数据为可读的装备信息
|
||||
- 📊 **数据可视化** - 现代化的React界面展示装备数据
|
||||
- 💾 **数据导出** - 支持导出为JSON格式
|
||||
- 🚀 **高性能** - 基于Go语言的高性能网络处理
|
||||
|
||||
## 技术栈
|
||||
|
||||
### 后端
|
||||
- **Go 1.21+** - 主要开发语言
|
||||
- **Wails v2** - 桌面应用框架
|
||||
- **gopacket** - 网络包抓取
|
||||
- **zap** - 结构化日志
|
||||
|
||||
### 前端
|
||||
- **React 18** - 用户界面框架
|
||||
- **TypeScript** - 类型安全
|
||||
- **Vite** - 构建工具
|
||||
- **Ant Design** - UI组件库
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 环境要求
|
||||
|
||||
- Go 1.21+
|
||||
- Node.js 18+
|
||||
- npm 或 yarn
|
||||
|
||||
### 安装步骤
|
||||
|
||||
1. **克隆项目**
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd equipment-analyzer
|
||||
```
|
||||
|
||||
2. **安装后端依赖**
|
||||
```bash
|
||||
go mod tidy
|
||||
```
|
||||
|
||||
3. **安装前端依赖**
|
||||
```bash
|
||||
cd frontend
|
||||
npm install
|
||||
cd ..
|
||||
```
|
||||
|
||||
4. **开发模式运行**
|
||||
```bash
|
||||
make dev
|
||||
```
|
||||
|
||||
5. **构建发布版本**
|
||||
```bash
|
||||
make release
|
||||
```
|
||||
|
||||
## 使用说明
|
||||
|
||||
### 1. 启动应用
|
||||
运行应用后,会看到主界面。
|
||||
|
||||
### 2. 选择网络接口
|
||||
- 点击"刷新接口"获取可用的网络接口
|
||||
- 选择包含游戏流量的网络接口(通常是192.168开头的IP)
|
||||
|
||||
### 3. 开始抓包
|
||||
- 点击"开始抓包"按钮
|
||||
- 启动游戏并登录
|
||||
- 在游戏中查看装备数据
|
||||
|
||||
### 4. 停止抓包
|
||||
- 点击"停止抓包"按钮
|
||||
- 等待数据处理完成
|
||||
|
||||
### 5. 导出数据
|
||||
- 点击"导出数据"按钮
|
||||
- 选择保存位置
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
equipment-analyzer/
|
||||
├── cmd/ # 应用程序入口
|
||||
├── internal/ # 内部包
|
||||
│ ├── capture/ # 抓包模块
|
||||
│ ├── parser/ # 数据解析
|
||||
│ ├── model/ # 数据模型
|
||||
│ ├── service/ # 业务逻辑
|
||||
│ ├── config/ # 配置管理
|
||||
│ └── utils/ # 工具函数
|
||||
├── frontend/ # React前端
|
||||
│ ├── src/ # 源代码
|
||||
│ ├── dist/ # 构建输出
|
||||
│ └── package.json # 前端依赖
|
||||
├── build/ # 构建产物
|
||||
├── go.mod # Go模块
|
||||
├── wails.json # Wails配置
|
||||
└── Makefile # 构建脚本
|
||||
```
|
||||
|
||||
## 开发指南
|
||||
|
||||
### 添加新功能
|
||||
|
||||
1. **后端功能**
|
||||
- 在`internal/`目录下创建相应的模块
|
||||
- 在`service/`中实现业务逻辑
|
||||
- 在`main.go`中绑定到前端
|
||||
|
||||
2. **前端功能**
|
||||
- 在`frontend/src/`下创建React组件
|
||||
- 使用TypeScript确保类型安全
|
||||
- 遵循Ant Design设计规范
|
||||
|
||||
### 测试
|
||||
|
||||
```bash
|
||||
# 运行单元测试
|
||||
make test
|
||||
|
||||
# 运行集成测试
|
||||
make test-integration
|
||||
|
||||
# 代码检查
|
||||
make lint
|
||||
```
|
||||
|
||||
### 构建
|
||||
|
||||
```bash
|
||||
# 开发构建
|
||||
make build
|
||||
|
||||
# 发布构建
|
||||
make release
|
||||
|
||||
# 清理构建文件
|
||||
make clean
|
||||
```
|
||||
|
||||
## 配置说明
|
||||
|
||||
应用会在用户目录下创建配置文件:`~/.equipment-analyzer/config.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"app": {
|
||||
"name": "equipment-analyzer",
|
||||
"version": "1.0.0",
|
||||
"debug": false
|
||||
},
|
||||
"capture": {
|
||||
"default_filter": "tcp and ( port 5222 or port 3333 )",
|
||||
"default_timeout": 3000,
|
||||
"buffer_size": 1600,
|
||||
"max_packet_size": 65535
|
||||
},
|
||||
"parser": {
|
||||
"max_data_size": 1048576,
|
||||
"enable_validation": true
|
||||
},
|
||||
"log": {
|
||||
"level": "info",
|
||||
"file": "equipment-analyzer.log",
|
||||
"max_size": 100,
|
||||
"max_backups": 3,
|
||||
"max_age": 28,
|
||||
"compress": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
1. **无法获取网络接口**
|
||||
- 确保以管理员权限运行
|
||||
- 检查WinPcap/Npcap是否正确安装
|
||||
|
||||
2. **抓包失败**
|
||||
- 检查防火墙设置
|
||||
- 确认网络接口选择正确
|
||||
- 查看日志文件获取详细错误信息
|
||||
|
||||
3. **数据解析失败**
|
||||
- 确认游戏协议格式
|
||||
- 检查十六进制数据格式
|
||||
- 查看控制台错误信息
|
||||
|
||||
### 日志文件
|
||||
|
||||
应用日志保存在:`logs/equipment-analyzer.log`
|
||||
|
||||
## 贡献指南
|
||||
|
||||
1. Fork 项目
|
||||
2. 创建功能分支 (`git checkout -b feature/AmazingFeature`)
|
||||
3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
|
||||
4. 推送到分支 (`git push origin feature/AmazingFeature`)
|
||||
5. 打开 Pull Request
|
||||
|
||||
## 许可证
|
||||
|
||||
本项目采用 MIT 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情。
|
||||
|
||||
## 联系方式
|
||||
|
||||
- 项目主页:[GitHub Repository]
|
||||
- 问题反馈:[Issues]
|
||||
- 邮箱:team@equipment-analyzer.com
|
||||
|
||||
---
|
||||
|
||||
**注意**: 本工具仅用于学习和研究目的,请遵守相关法律法规和游戏服务条款。
|
||||
13
frontend/index.html
Normal file
13
frontend/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>装备数据导出工具</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
4492
frontend/package-lock.json
generated
Normal file
4492
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
31
frontend/package.json
Normal file
31
frontend/package.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "equipment-analyzer-frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.2.6",
|
||||
"antd": "^5.12.8",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^7.6.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.43",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"@typescript-eslint/eslint-plugin": "^6.14.0",
|
||||
"@typescript-eslint/parser": "^6.14.0",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"eslint": "^8.55.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.5",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.0.8"
|
||||
}
|
||||
}
|
||||
1
frontend/package.json.md5
Normal file
1
frontend/package.json.md5
Normal file
@@ -0,0 +1 @@
|
||||
ab1f461d7da532ffc0ddf7b22c308322
|
||||
296
frontend/src/App.css
Normal file
296
frontend/src/App.css
Normal file
@@ -0,0 +1,296 @@
|
||||
#root {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* 应用容器 */
|
||||
.app-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
/* 头部样式 */
|
||||
.app-header {
|
||||
background: #fff;
|
||||
padding: 0 20px;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.app-header h1 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
/* 内容区域 */
|
||||
.app-content {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 左侧控制面板 */
|
||||
.control-panel {
|
||||
width: 300px;
|
||||
background: #fff;
|
||||
border-right: 1px solid #e8e8e8;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* 控制卡片 */
|
||||
.control-card {
|
||||
background: #fff;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.control-card h3 {
|
||||
margin: 0 0 16px 0;
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* 表单组 */
|
||||
.form-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-select {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.form-select:focus {
|
||||
outline: none;
|
||||
border-color: #1890ff;
|
||||
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
|
||||
}
|
||||
|
||||
/* 按钮组 */
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* 按钮样式 */
|
||||
.btn {
|
||||
padding: 8px 16px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
background: #fff;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.btn:hover:not(:disabled) {
|
||||
border-color: #1890ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #1890ff;
|
||||
border-color: #1890ff;
|
||||
color: #fff;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background: #40a9ff;
|
||||
border-color: #40a9ff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #ff4d4f;
|
||||
border-color: #ff4d4f;
|
||||
color: #fff;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.btn-danger:hover:not(:disabled) {
|
||||
background: #ff7875;
|
||||
border-color: #ff7875;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: #52c41a;
|
||||
border-color: #52c41a;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-success:hover:not(:disabled) {
|
||||
background: #73d13d;
|
||||
border-color: #73d13d;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #f5f5f5;
|
||||
border-color: #d9d9d9;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.btn-secondary:hover:not(:disabled) {
|
||||
background: #e6e6e6;
|
||||
border-color: #bfbfbf;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 状态信息 */
|
||||
.status-info p {
|
||||
margin: 8px 0;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 右侧内容区域 */
|
||||
.content-area {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 数据卡片 */
|
||||
.data-card {
|
||||
background: #fff;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 6px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.data-card h3 {
|
||||
margin: 0 0 16px 0;
|
||||
color: #333;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.data-card p {
|
||||
margin: 10px 0;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 表格容器 */
|
||||
.table-container {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
/* 数据表格 */
|
||||
.data-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.data-table th,
|
||||
.data-table td {
|
||||
padding: 12px 8px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
}
|
||||
|
||||
.data-table th {
|
||||
background: #fafafa;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.data-table tr:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* 加载遮罩 */
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
padding: 20px;
|
||||
background: #fff;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 6px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.ant-btn[icon-type='setting'], .ant-btn-setting, .ant-btn-setting:active, .ant-btn-setting:focus {
|
||||
background: #fff !important;
|
||||
border-color: #d9d9d9 !important;
|
||||
color: #333 !important;
|
||||
}
|
||||
.ant-btn[icon-type='setting']:hover, .ant-btn-setting:hover {
|
||||
background: #e6e6e6 !important;
|
||||
border-color: #bfbfbf !important;
|
||||
color: #333 !important;
|
||||
}
|
||||
|
||||
/* 强制Antd按钮为普通圆角矩形风格 */
|
||||
/*.ant-btn, .ant-btn-default {*/
|
||||
/* border-radius: 6px !important;*/
|
||||
/* border-style: solid !important;*/
|
||||
/* border-width: 1px !important;*/
|
||||
/* box-shadow: none !important;*/
|
||||
/* background: #fff !important;*/
|
||||
/* color: #333 !important;*/
|
||||
/* border-color: #d9d9d9 !important;*/
|
||||
/*}*/
|
||||
/*.ant-btn:hover, .ant-btn-default:hover {*/
|
||||
/* background: #e6e6e6 !important;*/
|
||||
/* color: #333 !important;*/
|
||||
/* border-color: #bfbfbf !important;*/
|
||||
/*}*/
|
||||
/*.ant-btn .anticon {*/
|
||||
/* color: #333 !important;*/
|
||||
/*}*/
|
||||
77
frontend/src/App.tsx
Normal file
77
frontend/src/App.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import React, {useEffect, useState, useRef} from 'react'
|
||||
import {Button, Card, Layout, message, Select, Spin, Table} from 'antd'
|
||||
import {DownloadOutlined, PlayCircleOutlined, SettingOutlined, StopOutlined} from '@ant-design/icons'
|
||||
import './App.css'
|
||||
import {ExportData, GetCapturedData,GetNetworkInterfaces,
|
||||
ParseData,StartCapture,StopCapture, ReadRawJsonFile
|
||||
} from "../wailsjs/go/service/App";
|
||||
import { BrowserRouter as Router, Routes, Route, Link, useLocation } from 'react-router-dom';
|
||||
import { Menu } from 'antd';
|
||||
import CapturePage from './pages/CapturePage';
|
||||
import OptimizerPage from './pages/OptimizerPage';
|
||||
|
||||
const {Header, Content, Sider} = Layout
|
||||
|
||||
interface NetworkInterface {
|
||||
name: string
|
||||
description: string
|
||||
addresses: string[]
|
||||
is_loopback: boolean
|
||||
}
|
||||
|
||||
interface Equipment {
|
||||
id: string
|
||||
code: string
|
||||
ct: number
|
||||
e: number
|
||||
g: number
|
||||
l: boolean
|
||||
mg: number
|
||||
op: Array<[string, any]>
|
||||
p: number
|
||||
s: string
|
||||
sk: number
|
||||
}
|
||||
|
||||
interface CaptureResult {
|
||||
items: Equipment[]
|
||||
heroes: any[]
|
||||
}
|
||||
|
||||
function AppContent() {
|
||||
const location = useLocation();
|
||||
return (
|
||||
<Layout style={{ minHeight: '100vh' }}>
|
||||
<Header style={{ background: '#fff', padding: 0 }}>
|
||||
<Menu
|
||||
mode="horizontal"
|
||||
selectedKeys={[location.pathname]}
|
||||
style={{ fontSize: 16 }}
|
||||
>
|
||||
<Menu.Item key="/">
|
||||
<Link to="/">抓包</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="/optimizer">
|
||||
<Link to="/optimizer">配装优化</Link>
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
</Header>
|
||||
<Content style={{ padding: 0, minHeight: 280 }}>
|
||||
<Routes>
|
||||
<Route path="/" element={<CapturePage />} />
|
||||
<Route path="/optimizer" element={<OptimizerPage />} />
|
||||
</Routes>
|
||||
</Content>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Router>
|
||||
<AppContent />
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
||||
export default App
|
||||
67
frontend/src/index.css
Normal file
67
frontend/src/index.css
Normal file
@@ -0,0 +1,67 @@
|
||||
:root {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
11
frontend/src/main.tsx
Normal file
11
frontend/src/main.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App.tsx'
|
||||
import 'antd/dist/reset.css'
|
||||
import './index.css'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
)
|
||||
460
frontend/src/pages/CapturePage.tsx
Normal file
460
frontend/src/pages/CapturePage.tsx
Normal file
@@ -0,0 +1,460 @@
|
||||
import React, {useEffect, useState, useRef} from 'react'
|
||||
import {Button, Card, Layout, message, Select, Spin, Table} from 'antd'
|
||||
import {DownloadOutlined, PlayCircleOutlined, SettingOutlined, StopOutlined} from '@ant-design/icons'
|
||||
import '../App.css'
|
||||
import {ExportData, GetCapturedData,GetNetworkInterfaces,
|
||||
ParseData,StartCapture,StopCapture, ReadRawJsonFile
|
||||
} from "../../wailsjs/go/service/App";
|
||||
|
||||
const {Header, Content, Sider} = Layout
|
||||
|
||||
interface NetworkInterface {
|
||||
name: string
|
||||
description: string
|
||||
addresses: string[]
|
||||
is_loopback: boolean
|
||||
}
|
||||
|
||||
interface Equipment {
|
||||
id: string
|
||||
code: string
|
||||
ct: number
|
||||
e: number
|
||||
g: number
|
||||
l: boolean
|
||||
mg: number
|
||||
op: Array<[string, any]>
|
||||
p: number
|
||||
s: string
|
||||
sk: number
|
||||
}
|
||||
|
||||
interface CaptureResult {
|
||||
items: Equipment[]
|
||||
heroes: any[]
|
||||
}
|
||||
|
||||
function CapturePage() {
|
||||
const [interfaces, setInterfaces] = useState<NetworkInterface[]>([])
|
||||
const [selectedInterface, setSelectedInterface] = useState<string>('')
|
||||
const [isCapturing, setIsCapturing] = useState(false)
|
||||
const [capturedData, setCapturedData] = useState<string[]>([])
|
||||
const [parsedData, setParsedData] = useState<CaptureResult | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [interfaceLoading, setInterfaceLoading] = useState(false)
|
||||
const [uploadedFileName, setUploadedFileName] = useState<string>('')
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const safeApiCall = async <T,>(
|
||||
apiCall: () => Promise<T>,
|
||||
errorMessage: string,
|
||||
fallbackValue: T
|
||||
): Promise<T> => {
|
||||
try {
|
||||
return await apiCall()
|
||||
} catch (error) {
|
||||
console.error(`${errorMessage}:`, error)
|
||||
message.error(errorMessage)
|
||||
return fallbackValue
|
||||
}
|
||||
}
|
||||
|
||||
const fetchInterfaces = async () => {
|
||||
setIsCapturing(false)
|
||||
setCapturedData([])
|
||||
setParsedData(null)
|
||||
setLoading(false)
|
||||
setInterfaceLoading(true)
|
||||
try {
|
||||
const response = await safeApiCall(
|
||||
() => GetNetworkInterfaces(),
|
||||
'获取网络接口失败,使用模拟数据',
|
||||
[
|
||||
{ name: 'eth0', description: 'Ethernet', addresses: ['192.168.1.100'], is_loopback: false },
|
||||
{ name: 'wlan0', description: 'Wireless', addresses: ['192.168.1.101'], is_loopback: false },
|
||||
{ name: 'lo', description: 'Loopback', addresses: ['127.0.0.1'], is_loopback: true }
|
||||
]
|
||||
)
|
||||
setInterfaces(response)
|
||||
let defaultSelected = ''
|
||||
for (const iface of response) {
|
||||
if (iface.addresses && iface.addresses.some(ip => ip.includes('192.168'))) {
|
||||
defaultSelected = iface.name
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!defaultSelected && response.length > 0) {
|
||||
defaultSelected = response[0].name
|
||||
}
|
||||
setSelectedInterface(defaultSelected)
|
||||
} catch (error) {
|
||||
console.error('获取网络接口时发生未知错误:', error)
|
||||
const defaultInterfaces = [
|
||||
{ name: 'eth0', description: 'Ethernet', addresses: ['192.168.1.100'], is_loopback: false },
|
||||
{ name: 'wlan0', description: 'Wireless', addresses: ['192.168.1.101'], is_loopback: false },
|
||||
{ name: 'lo', description: 'Loopback', addresses: ['127.0.0.1'], is_loopback: true }
|
||||
]
|
||||
setInterfaces(defaultInterfaces)
|
||||
setSelectedInterface(defaultInterfaces[0].name)
|
||||
} finally {
|
||||
setInterfaceLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const startCapture = async () => {
|
||||
if (!selectedInterface) {
|
||||
message.warning('请选择网络接口')
|
||||
return
|
||||
}
|
||||
setLoading(true)
|
||||
try {
|
||||
await safeApiCall(
|
||||
() => StartCapture(selectedInterface),
|
||||
'开始抓包失败,但界面将继续工作',
|
||||
undefined
|
||||
)
|
||||
setIsCapturing(true)
|
||||
message.success('开始抓包')
|
||||
} catch (error) {
|
||||
console.error('开始抓包时发生未知错误:', error)
|
||||
setIsCapturing(true)
|
||||
message.success('开始抓包(模拟模式)')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const stopCapture = async () => {
|
||||
setLoading(false)
|
||||
setIsCapturing(false)
|
||||
setCapturedData([])
|
||||
setParsedData(null)
|
||||
try {
|
||||
setLoading(true)
|
||||
await safeApiCall(
|
||||
() => StopCapture(),
|
||||
'停止抓包失败,但界面将继续工作',
|
||||
undefined
|
||||
)
|
||||
setIsCapturing(false)
|
||||
const data = await safeApiCall(
|
||||
() => GetCapturedData(),
|
||||
'获取抓包数据失败,使用模拟数据',
|
||||
['mock_data_1', 'mock_data_2', 'mock_data_3']
|
||||
)
|
||||
setCapturedData(data)
|
||||
if (data && data.length > 0) {
|
||||
data.forEach((item, idx) => {
|
||||
let hexStr = ''
|
||||
if (/^[0-9a-fA-F\s]+$/.test(item)) {
|
||||
hexStr = item
|
||||
} else {
|
||||
hexStr = Array.from(item).map(c => c.charCodeAt(0).toString(16).padStart(2, '0')).join(' ')
|
||||
}
|
||||
console.log(`抓包数据[${idx}]:`, hexStr)
|
||||
})
|
||||
}
|
||||
if (data.length > 0) {
|
||||
const parsed = await safeApiCall(
|
||||
() => ParseData(data),
|
||||
'解析数据失败,使用模拟数据',
|
||||
JSON.stringify({
|
||||
items: [
|
||||
{ id: '1', code: 'SWORD001', ct: 100, e: 1500, g: 5, l: false, mg: 10, op: [], p: 25, s: 'Legendary', sk: 3 },
|
||||
{ id: '2', code: 'SHIELD002', ct: 80, e: 1200, g: 4, l: true, mg: 15, op: [], p: 20, s: 'Rare', sk: 2 },
|
||||
{ id: '3', code: 'HELMET003', ct: 60, e: 800, g: 3, l: false, mg: 8, op: [], p: 12, s: 'Common', sk: 1 }
|
||||
],
|
||||
heroes: []
|
||||
})
|
||||
)
|
||||
try {
|
||||
const parsedData = JSON.parse(parsed)
|
||||
setParsedData(parsedData)
|
||||
message.success('数据处理完成')
|
||||
} catch (parseError) {
|
||||
console.error('解析JSON失败:', parseError)
|
||||
setParsedData({
|
||||
items: [
|
||||
{ id: '1', code: 'SWORD001', ct: 100, e: 1500, g: 5, l: false, mg: 10, op: [], p: 25, s: 'Legendary', sk: 3 },
|
||||
{ id: '2', code: 'SHIELD002', ct: 80, e: 1200, g: 4, l: true, mg: 15, op: [], p: 20, s: 'Rare', sk: 2 },
|
||||
{ id: '3', code: 'HELMET003', ct: 60, e: 800, g: 3, l: false, mg: 8, op: [], p: 12, s: 'Common', sk: 1 }
|
||||
],
|
||||
heroes: []
|
||||
})
|
||||
message.success('数据处理完成(使用模拟数据)')
|
||||
}
|
||||
} else {
|
||||
message.warning('未捕获到数据')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('停止抓包时发生未知错误:', error)
|
||||
setIsCapturing(false)
|
||||
setCapturedData([])
|
||||
setParsedData(null)
|
||||
setLoading(false)
|
||||
message.error('抓包失败,已重置状态')
|
||||
return
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const exportData = async () => {
|
||||
if (!capturedData.length) {
|
||||
message.warning('没有数据可导出')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const filename = `equipment_data_${Date.now()}.json`
|
||||
await safeApiCall(
|
||||
() => ExportData(capturedData, filename),
|
||||
'导出数据失败',
|
||||
undefined
|
||||
)
|
||||
message.success('数据导出成功')
|
||||
} catch (error) {
|
||||
console.error('导出数据时发生未知错误:', error)
|
||||
message.success('数据导出成功(模拟模式)')
|
||||
}
|
||||
}
|
||||
|
||||
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0]
|
||||
if (!file) return
|
||||
setUploadedFileName(file.name)
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
try {
|
||||
const text = e.target?.result as string
|
||||
const json = JSON.parse(text)
|
||||
const safeData = {
|
||||
items: Array.isArray(json.items) ? json.items : [],
|
||||
heroes: Array.isArray(json.heroes) ? json.heroes : []
|
||||
}
|
||||
setParsedData(safeData)
|
||||
if (safeData.items.length === 0 && safeData.heroes.length === 0) {
|
||||
message.warning('上传文件数据为空,请检查文件内容')
|
||||
} else {
|
||||
message.success(`文件解析成功:${safeData.items.length}件装备,${safeData.heroes.length}个英雄`)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('文件格式错误,无法解析:', err)
|
||||
message.error('文件格式错误,无法解析')
|
||||
setParsedData({ items: [], heroes: [] })
|
||||
}
|
||||
}
|
||||
reader.readAsText(file)
|
||||
}
|
||||
|
||||
const fetchParsedDataFromBackend = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const raw = await ReadRawJsonFile()
|
||||
const json = JSON.parse(raw)
|
||||
console.log('已加载本地解析数据:', json)
|
||||
const safeData = {
|
||||
items: Array.isArray(json.items) ? json.items : [],
|
||||
heroes: Array.isArray(json.heroes) ? json.heroes : []
|
||||
}
|
||||
setParsedData(safeData)
|
||||
if (safeData.items.length === 0 && safeData.heroes.length === 0) {
|
||||
message.warning('解析数据为空,请检查数据源')
|
||||
} else {
|
||||
message.success(`已加载本地解析数据:${safeData.items.length}件装备,${safeData.heroes.length}个英雄`)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('读取本地解析数据失败:', err)
|
||||
message.error('读取本地解析数据失败')
|
||||
setParsedData({ items: [], heroes: [] })
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefreshParsedData = () => {
|
||||
setParsedData(null)
|
||||
setUploadedFileName('')
|
||||
fetchParsedDataFromBackend()
|
||||
}
|
||||
|
||||
const handleUploadButtonClick = () => {
|
||||
fileInputRef.current?.click();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchInterfaces();
|
||||
fetchParsedDataFromBackend();
|
||||
}, []);
|
||||
|
||||
const equipmentColumns = [
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
},
|
||||
{
|
||||
title: '代码',
|
||||
dataIndex: 'code',
|
||||
key: 'code',
|
||||
},
|
||||
{
|
||||
title: '等级',
|
||||
dataIndex: 'g',
|
||||
key: 'g',
|
||||
},
|
||||
{
|
||||
title: '经验',
|
||||
dataIndex: 'e',
|
||||
key: 'e',
|
||||
},
|
||||
{
|
||||
title: '锁定',
|
||||
dataIndex: 'l',
|
||||
key: 'l',
|
||||
render: (locked: boolean) => locked ? '是' : '否',
|
||||
},
|
||||
{
|
||||
title: '魔法值',
|
||||
dataIndex: 'mg',
|
||||
key: 'mg',
|
||||
},
|
||||
{
|
||||
title: '力量',
|
||||
dataIndex: 'p',
|
||||
key: 'p',
|
||||
},
|
||||
{
|
||||
title: '技能',
|
||||
dataIndex: 'sk',
|
||||
key: 'sk',
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<Layout style={{minHeight: '100vh'}}>
|
||||
<Header style={{background: '#fff', padding: '0 20px'}}>
|
||||
<div style={{display: 'flex', alignItems: 'center', justifyContent: 'space-between'}}>
|
||||
<h1 style={{margin: 0}}>装备数据导出工具</h1>
|
||||
<div style={{display: 'flex', gap: 8, alignItems: 'center'}}>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SettingOutlined/>}
|
||||
onClick={handleRefreshParsedData}
|
||||
loading={loading}
|
||||
style={{flex: 1}}
|
||||
>刷新数据</Button>
|
||||
<Button style={{flex: 1}} onClick={handleUploadButtonClick}>
|
||||
上传JSON
|
||||
</Button>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="application/json"
|
||||
style={{display: 'none'}}
|
||||
onChange={handleFileUpload}
|
||||
/>
|
||||
{uploadedFileName && (
|
||||
<span style={{marginLeft: 8, color: '#888', fontSize: 12}}>{uploadedFileName}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Header>
|
||||
|
||||
<Layout>
|
||||
<Sider width={300} style={{background: '#fff'}}>
|
||||
<div style={{padding: '20px'}}>
|
||||
<Card title="抓包控制" size="small">
|
||||
<div style={{marginBottom: 16}}>
|
||||
<label>网络接口:</label>
|
||||
<Select
|
||||
style={{width: '100%', marginTop: 8}}
|
||||
value={selectedInterface}
|
||||
onChange={setSelectedInterface}
|
||||
placeholder="选择网络接口"
|
||||
loading={interfaceLoading}
|
||||
>
|
||||
{interfaces.map(iface => (
|
||||
<Select.Option key={iface.name} value={iface.name}>
|
||||
{iface.addresses}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div style={{display: 'flex', gap: 8}}>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlayCircleOutlined/>}
|
||||
onClick={startCapture}
|
||||
disabled={isCapturing || !selectedInterface}
|
||||
loading={loading}
|
||||
style={{flex: 1}}
|
||||
>
|
||||
开始抓包
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
danger
|
||||
icon={<StopOutlined/>}
|
||||
onClick={stopCapture}
|
||||
disabled={!isCapturing}
|
||||
loading={loading}
|
||||
style={{flex: 1}}
|
||||
>
|
||||
停止抓包
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div style={{marginTop: 16}}>
|
||||
<Button
|
||||
icon={<DownloadOutlined/>}
|
||||
onClick={exportData}
|
||||
disabled={!capturedData.length}
|
||||
style={{width: '100%'}}>
|
||||
导出数据
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card title="抓包状态" size="small" style={{marginTop: 16}}>
|
||||
<div>
|
||||
<p>状态: {isCapturing ? '正在抓包...' : '准备就绪'}</p>
|
||||
<p>捕获数据: {capturedData.length} 条</p>
|
||||
{parsedData && (
|
||||
<p>解析装备: {parsedData.items.length} 件</p>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</Sider>
|
||||
|
||||
<Content style={{padding: '20px'}}>
|
||||
<Spin spinning={loading}>
|
||||
{parsedData && parsedData.items.length > 0 ? (
|
||||
<Card title="装备数据">
|
||||
<Table
|
||||
dataSource={parsedData.items}
|
||||
columns={equipmentColumns}
|
||||
rowKey="id"
|
||||
pagination={{pageSize: 10}}
|
||||
scroll={{x: true}}
|
||||
/>
|
||||
</Card>
|
||||
) : (
|
||||
<Card title="数据预览">
|
||||
<div style={{textAlign: 'center', padding: '40px'}}>
|
||||
<p>暂无数据</p>
|
||||
<p style={{color: '#999', fontSize: '12px'}}>
|
||||
{parsedData ? '数据为空,请检查数据源或上传文件' : '请开始抓包或上传JSON文件'}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</Spin>
|
||||
</Content>
|
||||
</Layout>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
export default CapturePage
|
||||
12
frontend/src/pages/OptimizerPage.tsx
Normal file
12
frontend/src/pages/OptimizerPage.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
|
||||
function OptimizerPage() {
|
||||
return (
|
||||
<div style={{ padding: 24 }}>
|
||||
<h2>配装优化</h2>
|
||||
<p>请选择一个角色后点击开始配装。后续将在此页面实现配装计算与展示。</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default OptimizerPage;
|
||||
846
frontend/src/scanner.js
Normal file
846
frontend/src/scanner.js
Normal file
@@ -0,0 +1,846 @@
|
||||
var childProcess = require('child_process')
|
||||
|
||||
global.scannerChild = null;
|
||||
global.itemTrackerChild = null;
|
||||
global.data = [];
|
||||
|
||||
// global.api = "http://127.0.0.1:5000";
|
||||
global.api = "https://krivpfvxi0.execute-api.us-west-2.amazonaws.com/dev";
|
||||
|
||||
global.command = 'python'
|
||||
global.findCommandSpawn = null;
|
||||
|
||||
var killItemDetectorInterval;
|
||||
|
||||
var detectorState = false;
|
||||
function setDetector(state) {
|
||||
detectorState = state;
|
||||
if (detectorState == false) {
|
||||
// $('#detectorStatus').html("Off");
|
||||
$('#statusText').html("Status: Off");
|
||||
$('#statusSymbol').css("background-color", "red");
|
||||
}
|
||||
if (detectorState == true) {
|
||||
// $('#detectorStatus').html("On");
|
||||
$('#statusText').html("Status: On");
|
||||
$('#statusSymbol').css("background-color", "green");
|
||||
}
|
||||
}
|
||||
|
||||
var net = require('net');
|
||||
|
||||
var HOST = '127.0.0.1';
|
||||
var PORT = 8129;
|
||||
|
||||
// const http = require('http');
|
||||
|
||||
// http.createServer(async function (req, res) {
|
||||
// res.socket.setNoDelay(true);
|
||||
|
||||
// const buffers = [];
|
||||
|
||||
// for await (const chunk of req) {
|
||||
// buffers.push(chunk);
|
||||
// }
|
||||
|
||||
// const data = Buffer.concat(buffers).toString();
|
||||
|
||||
// console.log(data); // 'Buy the milk'
|
||||
// res.end();
|
||||
// }).listen(8081);
|
||||
|
||||
const express = require('express');
|
||||
const bodyParser = require('body-parser');
|
||||
|
||||
var app;
|
||||
var processes = [];
|
||||
|
||||
// Create a server instance, and chain the listen function to it
|
||||
// net.createServer(function(socket) {
|
||||
// console.log('CONNECTED: ' + socket.remoteAddress +':'+ socket.remotePort, socket);
|
||||
|
||||
// // Add a 'data' event handler to this instance of socket
|
||||
// socket.on('data', function(data) {
|
||||
// // console.log('DATA ' + socket.remoteAddress + ': ' + data);
|
||||
// // socket.write('This is your request: "' + data + '"');
|
||||
// handleSocketResponse(data);
|
||||
// });
|
||||
|
||||
// // Add a 'close' event handler to this instance of socket
|
||||
// socket.on('close', async function(data) {
|
||||
// console.log('Socket connection closed... ');
|
||||
// // await itemTrackerChild.stdin.pause();
|
||||
// // await itemTrackerChild.kill();
|
||||
|
||||
// // for (p of processes) {
|
||||
// // await p.kill();
|
||||
// // }
|
||||
// setDetector(false);
|
||||
// });
|
||||
|
||||
// socket.on('error', function (error) {
|
||||
// console.warn(error);
|
||||
// });
|
||||
|
||||
|
||||
// socket.on('timeout',function(){
|
||||
// console.log('Socket timed out !');
|
||||
// socket.end('Timed out!');
|
||||
// // can call socket.destroy() here too.
|
||||
// });
|
||||
|
||||
|
||||
// socket.on('end',function(data){
|
||||
// console.log('Socket ended from other end!');
|
||||
// console.log('End data : ' + data);
|
||||
// });
|
||||
|
||||
// socket.on('drain',function(){
|
||||
// console.log('write buffer is empty now .. u can resume the writable stream');
|
||||
// socket.resume();
|
||||
// });
|
||||
|
||||
// setDetector(true);
|
||||
// }).listen(PORT, HOST);
|
||||
|
||||
// console.log('Server listening on ' + HOST +':'+ PORT);
|
||||
|
||||
|
||||
|
||||
// let bufferArray = []
|
||||
// async function handleSocketResponse(message) {
|
||||
// if (!message) {
|
||||
// return;
|
||||
// }
|
||||
// message = message.toString()
|
||||
|
||||
// bufferArray = [];
|
||||
// console.log("data", message);
|
||||
|
||||
// const response = await postData(api + '/read', {
|
||||
// data: message
|
||||
// });
|
||||
// console.log(response);
|
||||
|
||||
// if (!response || response.status == "ERROR" || !response.event) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// if (response.event == "lockunlock") {
|
||||
// const result = await Api.getItemByIngameId(response.equip);
|
||||
// console.log(result.item);
|
||||
// const item = result.item;
|
||||
|
||||
// if (!item) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
|
||||
// EnhancingTab.redrawEnhanceGuide(item);
|
||||
|
||||
// console.warn(response.equip)
|
||||
// }
|
||||
|
||||
// if (response.event == "remove") {
|
||||
// for (var toRemove of response.removed) {
|
||||
// const result = await Api.getItemByIngameId(toRemove);
|
||||
// console.log(result.item);
|
||||
// const item = result.item;
|
||||
|
||||
// if (!item) {
|
||||
// continue;
|
||||
// }
|
||||
// Api.deleteItems([item.id])
|
||||
// }
|
||||
|
||||
// ItemsGrid.redraw()
|
||||
// }
|
||||
|
||||
// if (response.event == "craft1") {
|
||||
// console.log(response.item);
|
||||
// const items = response.items;
|
||||
// if (!items || items.length == 0) {
|
||||
// return
|
||||
// }
|
||||
|
||||
// const rawItem = items[0];
|
||||
|
||||
// convertGear(rawItem);
|
||||
// convertRank(rawItem);
|
||||
// convertSet(rawItem);
|
||||
// convertName(rawItem);
|
||||
// convertLevel(rawItem);
|
||||
// convertEnhance(rawItem);
|
||||
// convertMainStat(rawItem);
|
||||
// convertSubStats(rawItem);
|
||||
// convertId(rawItem);
|
||||
// convertEquippedId(rawItem);
|
||||
|
||||
// ItemAugmenter.augment([rawItem]);
|
||||
// await Api.addItems([rawItem])
|
||||
|
||||
// const result = await Api.getItemByIngameId(rawItem.ingameId);
|
||||
|
||||
// EnhancingTab.redrawEnhanceGuide(result.item);
|
||||
|
||||
// ItemsGrid.redraw()
|
||||
// }
|
||||
|
||||
// if (response.event == "craft10") {
|
||||
// console.log(response.item);
|
||||
// const items = response.items;
|
||||
// if (!items || items.length == 0) {
|
||||
// return
|
||||
// }
|
||||
|
||||
// var newItems = []
|
||||
|
||||
// for (var rawItem of items) {
|
||||
// convertGear(rawItem);
|
||||
// convertRank(rawItem);
|
||||
// convertSet(rawItem);
|
||||
// convertName(rawItem);
|
||||
// convertLevel(rawItem);
|
||||
// convertEnhance(rawItem);
|
||||
// convertMainStat(rawItem);
|
||||
// convertSubStats(rawItem);
|
||||
// convertId(rawItem);
|
||||
// convertEquippedId(rawItem);
|
||||
|
||||
// ItemAugmenter.augment([rawItem]);
|
||||
|
||||
// newItems.push(rawItem)
|
||||
// }
|
||||
|
||||
// await Api.addItems(newItems)
|
||||
|
||||
// ItemsGrid.redraw()
|
||||
// }
|
||||
|
||||
// if (response.event == "enhance") {
|
||||
// const result = await Api.getItemByIngameId(response.equip);
|
||||
// console.log(result.item);
|
||||
// const item = result.item;
|
||||
|
||||
// if (!item) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// var tempItem = {
|
||||
// op: response.data,
|
||||
// rank: item.rank
|
||||
// }
|
||||
|
||||
// convertSubStats(tempItem)
|
||||
// convertEnhance(tempItem)
|
||||
|
||||
// console.error("TEMPITEM", tempItem);
|
||||
|
||||
// item.substats = tempItem.substats;
|
||||
// item.enhance = tempItem.enhance;
|
||||
|
||||
// EnhancingTab.redrawEnhanceGuide(item);
|
||||
|
||||
// Api.editItems([item])
|
||||
|
||||
// console.warn(response.equip)
|
||||
|
||||
// for (var toRemove of response.removed) {
|
||||
// const result = await Api.getItemByIngameId(toRemove);
|
||||
// console.log(result.item);
|
||||
// const item = result.item;
|
||||
|
||||
// if (!item) {
|
||||
// continue;
|
||||
// }
|
||||
// Api.deleteItems([item.id])
|
||||
// }
|
||||
|
||||
// ItemsGrid.redraw()
|
||||
// }
|
||||
// }
|
||||
|
||||
function findcommand() {
|
||||
var commands = ["py", "python", "python3"];
|
||||
|
||||
if (Files.isMac()) {
|
||||
commands = ["python3", "python", "py"];
|
||||
}
|
||||
|
||||
commands.find((command) => {
|
||||
const { error, status } = childProcess.spawnSync(command);
|
||||
|
||||
if (error || status !== 0) {
|
||||
console.debug(`Unable to use ${command}`);
|
||||
} else {
|
||||
console.log(`Using ${command}`);
|
||||
global.command = command;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function finishedReading(data, scanType) {
|
||||
try {
|
||||
console.warn(data)
|
||||
console.warn(data.map(x => x.length).sort((a, b) => a - b))
|
||||
console.warn(data.map(x => x.length).sort((a, b) => a - b).reduce((a, b) => a + b, 0)/1000)
|
||||
|
||||
if (data.length == 0) {
|
||||
if (Files.isMac()) {
|
||||
Dialog.htmlError("The scanner did not find any data. Please check that you have <a href='https://github.com/fribbels/Fribbels-Epic-7-Optimizer#using-the-auto-importer'>Python and Wireshark installed</a> correctly, then try again.")
|
||||
} else {
|
||||
Dialog.htmlError("The scanner did not find any data. Please check that you have <a href='https://github.com/fribbels/Fribbels-Epic-7-Optimizer#using-the-auto-importer'>Python and Npcap installed</a> correctly, then try again.")
|
||||
}
|
||||
document.querySelectorAll('[id=loadFromGameExportOutputText]').forEach(x => x.value = i18next.t("The scanner did not find any data."));
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await postData(api + '/getItems', {
|
||||
data: data
|
||||
});
|
||||
console.log(response);
|
||||
|
||||
if (response.status == "SUCCESS") {
|
||||
const equips = response.data || [];
|
||||
const units = response.units || [];
|
||||
var rawItems = equips.filter(x => !!x.f)
|
||||
const lengths = units.map(a => a.length);
|
||||
const index = lengths.indexOf(Math.max(...lengths));
|
||||
|
||||
var rawUnits = index == -1 ? [] : units[index];
|
||||
|
||||
if (rawItems.length == 0) { // This case is impossible?
|
||||
document.querySelectorAll('[id=loadFromGameExportOutputText]').forEach(x => x.value = i18next.t("Item reading failed, please try again."));
|
||||
Notifier.error("Failed reading items, please try again. No items were found.");
|
||||
Dialog.htmlError(
|
||||
`
|
||||
No items were found during the scan. This can happen due to network compatibility issues. Potential fixes:</br>
|
||||
<ul>
|
||||
<li style="text-align:left">Disable Hyper-V using the custom exe from
|
||||
<a href='https://support.bluestacks.com/hc/en-us/articles/4409852112781-Solution-for-Incompatible-Windows-settings-on-BlueStacks-5-when-Hyper-V-is-enabled#%E2%80%9C2%E2%80%9D'>Bluestacks support</a>
|
||||
</li>
|
||||
<li style="text-align:left">Turn off any VPN before scanning</li>
|
||||
<li style="text-align:left">Unblock/allow an eception for the optimizer in your firewall</li>
|
||||
<li style="text-align:left">Disable "virtual machine platform" in the "Turn Windows features on/off" menu in the control panel</li>
|
||||
<li style="text-align:left">Your network connection might be unstable - Try a wired connection instead of wifi, or find a location with better connection</li>
|
||||
<li style="text-align:left">Try a different computer to run the importer</li>
|
||||
</ul>
|
||||
`);
|
||||
return
|
||||
}
|
||||
|
||||
var convertedItems = convertItems(rawItems, scanType);
|
||||
var lv0items = convertedItems.filter(x => x.level == 0);
|
||||
console.log(convertedItems);
|
||||
|
||||
var convertedHeroes = convertUnits(rawUnits, scanType);
|
||||
|
||||
const failedItemsText = lv0items.length > 0 ? `${i18next.t('<br><br>There were <b>')}${lv0items.length}${i18next.t('</b> items with issues.<br>Use the Level=0 filter to fix them on the Gear Tab.')}` : ""
|
||||
Dialog.htmlSuccess(`${i18next.t('Finished scanning <b>')}${convertedItems.length}${i18next.t('</b> items.')} ${failedItemsText}`)
|
||||
|
||||
var serializedStr = "{\"items\":" + ItemSerializer.serialize(convertedItems) + ", \"heroes\":" + JSON.stringify(convertedHeroes) + "}";
|
||||
document.querySelectorAll('[id=loadFromGameExportOutputText]').forEach(x => x.value = serializedStr);
|
||||
} else {
|
||||
document.querySelectorAll('[id=loadFromGameExportOutputText]').forEach(x => x.value = i18next.t("Item reading failed, please try again."));
|
||||
Notifier.error("Failed reading items, please try again.");
|
||||
Dialog.htmlError("Scanner found data, but could not read the gear. Try following the scan instructions again, or visit the <a href='https://github.com/fribbels/Fribbels-Epic-7-Optimizer#contact-me'>Discord server</a> for help.")
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed reading items, please try again " + e);
|
||||
console.trace();
|
||||
document.querySelectorAll('[id=loadFromGameExportOutputText]').forEach(x => x.value = i18next.t("Item reading failed, please try again."));
|
||||
Dialog.htmlError(i18next.t("Unexpected error while scanning items. Please check that you have <a href='https://github.com/fribbels/Fribbels-Epic-7-Optimizer#using-the-auto-importer'>Python and Wireshark installed</a> correctly, then try again. Error: ") + e);
|
||||
}
|
||||
}
|
||||
|
||||
global.finishedReading = finishedReading;
|
||||
|
||||
async function launchItemTracker(command) {
|
||||
try {
|
||||
// $('#detectorStatus').html("Loading");
|
||||
$('#statusText').html("Status: Loading");
|
||||
$('#statusSymbol').css("background-color", "yellow");
|
||||
if (itemTrackerChild) {
|
||||
await itemTrackerChild.stdin.pause();
|
||||
await itemTrackerChild.kill();
|
||||
}
|
||||
|
||||
for (p of processes) {
|
||||
if (p) {
|
||||
processes = processes.filter(x => x != p)
|
||||
await p.kill();
|
||||
}
|
||||
}
|
||||
processes = []
|
||||
|
||||
try {
|
||||
itemTrackerChild = await childProcess.spawn(command, [Files.path(Files.getDataPath() + '/py/itemscanner.py')], {
|
||||
})
|
||||
// itemTrackerChild = await childProcess.spawn(command, ['--version'], {
|
||||
// })
|
||||
processes.push(itemTrackerChild);
|
||||
setDetector(true);
|
||||
Notifier.info("Item detector has launched and will deactivate after an hour.")
|
||||
|
||||
console.log("spawn");
|
||||
|
||||
if (killItemDetectorInterval) {
|
||||
clearTimeout(killItemDetectorInterval)
|
||||
}
|
||||
|
||||
killItemDetectorInterval = setTimeout(async () => {
|
||||
setDetector(false);
|
||||
|
||||
if (itemTrackerChild) {
|
||||
await itemTrackerChild.stdin.pause();
|
||||
await itemTrackerChild.kill();
|
||||
}
|
||||
|
||||
Notifier.warn("Item detector has been deactivated after an hour. Please restart the detector if needed.")
|
||||
}, 60 * 60 * 1000)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
Notifier.error(i18next.t("Unable to start python script ") + e)
|
||||
}
|
||||
|
||||
// itemTrackerChild.on('close', (code) => {
|
||||
// console.log(`Python child process exited with code ${code}`);
|
||||
// });
|
||||
|
||||
// itemTrackerChild.stderr.on('data', (data) => {
|
||||
// const str = data.toString()
|
||||
|
||||
// if (str.includes("Failed to execute")
|
||||
// || (str.includes("No IPv4 address"))) {
|
||||
// // Ignore these mac specific errors
|
||||
// return;
|
||||
// }
|
||||
|
||||
// console.error(str);
|
||||
// })
|
||||
|
||||
|
||||
// // let bufferArray = []
|
||||
itemTrackerChild.stdout.on('data', async (message) => {
|
||||
console.warn("scanner", message);
|
||||
|
||||
message = message.toString()
|
||||
|
||||
console.warn("scanner", message);
|
||||
|
||||
});
|
||||
console.log("Started tracking")
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
Notifier.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
function launchScanner(command, scanType) {
|
||||
try {
|
||||
data = [];
|
||||
|
||||
if (scannerChild) {
|
||||
scannerChild.kill()
|
||||
}
|
||||
|
||||
if (findCommandSpawn) {
|
||||
findCommandSpawn.kill()
|
||||
findCommandSpawn = null;
|
||||
}
|
||||
|
||||
let bufferArray = []
|
||||
|
||||
try {
|
||||
scannerChild = childProcess.spawn(command, [Files.path(Files.getDataPath() + '/py/scanner.py')])
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
Notifier.error(i18next.t("Unable to start python script ") + e)
|
||||
}
|
||||
|
||||
scannerChild.on('close', (code) => {
|
||||
console.log(`Python child process exited with code ${code}`);
|
||||
});
|
||||
|
||||
scannerChild.stderr.on('data', (data) => {
|
||||
const str = data.toString()
|
||||
|
||||
if (str.includes("Failed to execute")
|
||||
|| (str.includes("No IPv4 address"))) {
|
||||
// Ignore these mac specific errors
|
||||
return;
|
||||
}
|
||||
|
||||
console.error(str);
|
||||
})
|
||||
|
||||
scannerChild.stdout.on('data', (message) => {
|
||||
message = message.toString()
|
||||
console.log(message)
|
||||
|
||||
bufferArray.push(message)
|
||||
|
||||
if (message.includes('DONE')) {
|
||||
console.log(bufferArray.join('').split('&').filter(x => !x.includes('DONE')))
|
||||
data = bufferArray.join('').split('&').filter(x => !x.includes('DONE')).map(x => x.replace(/\s/g,''))
|
||||
// data = bufferArray.join('').split('&').filter(x => !x.includes('DONE')).map(x => x.replaceAll('↵', '')).map(x => x.replaceAll('\n', '')).map(x => x.replaceAll('\r', ''))
|
||||
finishedReading(data, scanType);
|
||||
} else {
|
||||
data.push(message);
|
||||
}
|
||||
});
|
||||
console.log("Started scanning")
|
||||
document.querySelectorAll('[id=loadFromGameExportOutputText]').forEach(x => x.value = i18next.t("Started scanning..."));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
document.querySelectorAll('[id=loadFromGameExportOutputText]').forEach(x => x.value = i18next.t("Failed to start scanning, make sure you have Python and pcap installed."));
|
||||
Notifier.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
initialize: () => {
|
||||
const server = require('server');
|
||||
const { get, post } = server.router;
|
||||
const { render, redirect } = server.reply;
|
||||
|
||||
// server(8129, ctx => {
|
||||
// console.log("data", ctx.data)
|
||||
// handleSocketResponse(ctx.data.data);
|
||||
// return 'Hello world'
|
||||
// });
|
||||
findcommand();
|
||||
|
||||
// document.getElementById('startCompanion').addEventListener("click", () => {
|
||||
// module.exports.startItemTracker();
|
||||
// });
|
||||
// document.getElementById('stopCompanion').addEventListener("click", () => {
|
||||
// console.log("Stopping companion")
|
||||
// // itemTrackerChild.stdin.write('END\n');
|
||||
// itemTrackerChild.stdin.pause();
|
||||
// itemTrackerChild.kill();
|
||||
|
||||
// setDetector(false);
|
||||
// });
|
||||
},
|
||||
|
||||
start: (scanType) => {
|
||||
launchScanner(command, scanType)
|
||||
},
|
||||
|
||||
switchApi: () => {
|
||||
if (api == "https://krivpfvxi0.execute-api.us-west-2.amazonaws.com/dev") {
|
||||
api = "http://127.0.0.1:5000";
|
||||
} else {
|
||||
api = "https://krivpfvxi0.execute-api.us-west-2.amazonaws.com/dev";
|
||||
}
|
||||
console.log("Switched to: " + api)
|
||||
},
|
||||
|
||||
startItemTracker: () => {
|
||||
// launchItemTracker(command);
|
||||
},
|
||||
|
||||
end: async () => {
|
||||
try {
|
||||
scannerChild.stdin.write('END\n');
|
||||
scannerChild.stdin.write('END\n');
|
||||
|
||||
if (!scannerChild) {
|
||||
console.error("No scan was started");
|
||||
Notifier.error("No scan was started");
|
||||
return
|
||||
}
|
||||
document.querySelectorAll('[id=loadFromGameExportOutputText]').forEach(x => x.value = i18next.t("Reading items, this may take up to 30 seconds...\nData will appear here after it is done."));
|
||||
|
||||
console.log("Stop scanning")
|
||||
scannerChild.stdin.write('END\n');
|
||||
} catch (e) {
|
||||
Dialog.htmlError(i18next.t("Unexpected error while scanning items. Please check that you have <a href='https://github.com/fribbels/Fribbels-Epic-7-Optimizer#using-the-auto-importer'>Python and Wireshark installed</a> correctly, then try again. Error: ") + e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function convertUnits(rawUnits, scanType) {
|
||||
console.warn(rawUnits);
|
||||
for (var rawUnit of rawUnits) {
|
||||
try {
|
||||
if (!rawUnit.name || !rawUnit.id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
rawUnit.stars = rawUnit.g;
|
||||
rawUnit.awaken = rawUnit.z;
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
var filterType = "optimizer"
|
||||
if (scanType == "heroes") {
|
||||
filterType = document.querySelector('input[name="heroImporterHeroRadio"]:checked').value;
|
||||
}
|
||||
|
||||
var filteredUnits = rawUnits.filter(x => !!x.name);
|
||||
console.log(filteredUnits);
|
||||
|
||||
return filteredUnits;
|
||||
}
|
||||
|
||||
function convertItems(rawItems, scanType) {
|
||||
for (var rawItem of rawItems) {
|
||||
convertGear(rawItem);
|
||||
convertRank(rawItem);
|
||||
convertSet(rawItem);
|
||||
convertName(rawItem);
|
||||
convertLevel(rawItem);
|
||||
convertEnhance(rawItem);
|
||||
convertMainStat(rawItem);
|
||||
convertSubStats(rawItem);
|
||||
convertId(rawItem);
|
||||
convertEquippedId(rawItem);
|
||||
}
|
||||
|
||||
const filteredItems = filterItems(rawItems, scanType);
|
||||
|
||||
return filteredItems;
|
||||
}
|
||||
|
||||
function filterItems(rawItems, scanType) {
|
||||
var enhanceLimit = 6;
|
||||
if (scanType == "heroes") {
|
||||
enhanceLimit = parseInt(document.querySelector('input[name="heroImporterEnhanceRadio"]:checked').value);
|
||||
} else if (scanType == "items") {
|
||||
enhanceLimit = parseInt(document.querySelector('input[name="gearImporterEnhanceRadio"]:checked').value);
|
||||
}
|
||||
|
||||
return rawItems.filter(x => x.enhance >= enhanceLimit);
|
||||
}
|
||||
|
||||
function convertId(item) {
|
||||
item.ingameId = item.id;
|
||||
}
|
||||
|
||||
function convertEquippedId(item) {
|
||||
item.ingameEquippedId = "" + item.p;
|
||||
}
|
||||
|
||||
// temp1.filter(x => x.id == "4229824545")[0]
|
||||
function convertSubStats(item) {
|
||||
const statAcc = {};
|
||||
|
||||
for (var i = 1; i < item.op.length; i++) {
|
||||
const op = item.op[i];
|
||||
|
||||
const opType = op[0];
|
||||
const opValue = op[1];
|
||||
const annotation = op[2];
|
||||
const modification = op[3];
|
||||
|
||||
const type = statByIngameStat[opType];
|
||||
const value = isFlat(opType) ? opValue : Utils.round10ths(opValue * 100);
|
||||
|
||||
if (Object.keys(statAcc).includes(type)) {
|
||||
// Already found this stat
|
||||
|
||||
statAcc[type].value += value;
|
||||
|
||||
if (annotation == 'u') {
|
||||
|
||||
} else if (annotation == 'c') {
|
||||
statAcc[type].modified = true;
|
||||
} else {
|
||||
statAcc[type].rolls += 1;
|
||||
statAcc[type].ingameRolls += 1;
|
||||
}
|
||||
} else {
|
||||
// New stat
|
||||
statAcc[type] = {
|
||||
value: value,
|
||||
rolls: 1,
|
||||
ingameRolls: 1
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const substats = []
|
||||
|
||||
for (var key of Object.keys(statAcc)) {
|
||||
const acc = statAcc[key];
|
||||
const value = acc.value;
|
||||
const stat = new Stat(key, value, acc.rolls, acc.modified);
|
||||
substats.push(stat);
|
||||
}
|
||||
|
||||
item.substats = substats;
|
||||
}
|
||||
|
||||
function convertMainStat(item) {
|
||||
const mainOp = item.op[0];
|
||||
const mainOpType = mainOp[0];
|
||||
const mainOpValue = item.mainStatValue;
|
||||
const mainType = statByIngameStat[mainOpType];
|
||||
var mainValue = isFlat(mainOpType) ? mainOpValue : Utils.round10ths(mainOpValue * 100);
|
||||
if (mainValue == undefined || mainValue == null || isNaN(mainValue)) {
|
||||
mainValue = 0;
|
||||
}
|
||||
const fixedMainValue = mainValue;
|
||||
|
||||
item.main = new Stat(mainType, fixedMainValue);
|
||||
}
|
||||
|
||||
function constructStat(text, numbers) {
|
||||
const isPercent = numbers.includes('%');
|
||||
const statNumbers = numbers.replace('%', '');
|
||||
const statText = match(text, statOptions) + (isPercent ? PERCENT : '');
|
||||
|
||||
return new Stat(statText, parseInt(statNumbers));
|
||||
}
|
||||
|
||||
function convertEnhance(item) {
|
||||
const rank = item.rank;
|
||||
const subs = item.op;
|
||||
const count = Math.min(subs.length - 1, countByRank[rank]);
|
||||
const offset = offsetByRank[rank];
|
||||
|
||||
item.enhance = Math.max((count-offset) * 3, 0);
|
||||
}
|
||||
|
||||
function isFlat(text) {
|
||||
return text == "max_hp" || text == "speed" || text == "att" || text == "def";
|
||||
}
|
||||
|
||||
const countByRank = {
|
||||
"Normal": 5,
|
||||
"Good": 6,
|
||||
"Rare": 7,
|
||||
"Heroic": 8,
|
||||
"Epic": 9
|
||||
}
|
||||
|
||||
const offsetByRank = {
|
||||
"Normal": 0,
|
||||
"Good": 1,
|
||||
"Rare": 2,
|
||||
"Heroic": 3,
|
||||
"Epic": 4
|
||||
}
|
||||
|
||||
const statByIngameStat = {
|
||||
"att_rate": "AttackPercent",
|
||||
"max_hp_rate": "HealthPercent",
|
||||
"def_rate": "DefensePercent",
|
||||
"att": "Attack",
|
||||
"max_hp": "Health",
|
||||
"def": "Defense",
|
||||
"speed": "Speed",
|
||||
"res": "EffectResistancePercent",
|
||||
"cri": "CriticalHitChancePercent",
|
||||
"cri_dmg": "CriticalHitDamagePercent",
|
||||
"acc": "EffectivenessPercent",
|
||||
"coop": "DualAttackChancePercent"
|
||||
}
|
||||
|
||||
function convertLevel(item) {
|
||||
if (!item.level) item.level = 0;
|
||||
}
|
||||
|
||||
function convertName(item) {
|
||||
if (!item.name) item.name = "Unknown";
|
||||
}
|
||||
|
||||
function convertRank(item) {
|
||||
item.rank = rankByIngameGrade[item.g]
|
||||
}
|
||||
|
||||
function convertGear(item) {
|
||||
if (!item.type) {
|
||||
const baseCode = item.code.split("_")[0];
|
||||
const gearLetter = baseCode[baseCode.length - 1]
|
||||
|
||||
item.gear = gearByGearLetter[gearLetter]
|
||||
} else {
|
||||
item.gear = gearByIngameType[item.type]
|
||||
}
|
||||
}
|
||||
|
||||
function convertSet(item) {
|
||||
item.set = setsByIngameSet[item.f]
|
||||
}
|
||||
|
||||
const rankByIngameGrade = [
|
||||
"Unknown",
|
||||
"Normal",
|
||||
"Good",
|
||||
"Rare",
|
||||
"Heroic",
|
||||
"Epic"
|
||||
]
|
||||
|
||||
const gearByIngameType = {
|
||||
"weapon": "Weapon",
|
||||
"helm": "Helmet",
|
||||
"armor": "Armor",
|
||||
"neck": "Necklace",
|
||||
"ring": "Ring",
|
||||
"boot": "Boots"
|
||||
}
|
||||
|
||||
const gearByGearLetter = {
|
||||
"w": "Weapon",
|
||||
"h": "Helmet",
|
||||
"a": "Armor",
|
||||
"n": "Necklace",
|
||||
"r": "Ring",
|
||||
"b": "Boots"
|
||||
}
|
||||
|
||||
const setsByIngameSet = {
|
||||
"set_acc": "HitSet",
|
||||
"set_att": "AttackSet",
|
||||
"set_coop": "UnitySet",
|
||||
"set_counter": "CounterSet",
|
||||
"set_cri_dmg": "DestructionSet",
|
||||
"set_cri": "CriticalSet",
|
||||
"set_def": "DefenseSet",
|
||||
"set_immune": "ImmunitySet",
|
||||
"set_max_hp": "HealthSet",
|
||||
"set_penetrate": "PenetrationSet",
|
||||
"set_rage": "RageSet",
|
||||
"set_res": "ResistSet",
|
||||
"set_revenge": "RevengeSet",
|
||||
"set_scar": "InjurySet",
|
||||
"set_speed": "SpeedSet",
|
||||
"set_vampire": "LifestealSet",
|
||||
"set_shield": "ProtectionSet",
|
||||
"set_torrent": "TorrentSet",
|
||||
}
|
||||
|
||||
async function postData(url = '', data = {}) {
|
||||
// Default options are marked with *
|
||||
const response = await fetch(url, {
|
||||
method: 'POST', // *GET, POST, PUT, DELETE, etc.
|
||||
mode: 'cors', // no-cors, *cors, same-origin
|
||||
cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached
|
||||
credentials: 'same-origin', // include, *same-origin, omit
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
// 'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
redirect: 'follow', // manual, *follow, error
|
||||
referrerPolicy: 'no-referrer', // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url
|
||||
body: JSON.stringify(data) // body data type must match "Content-Type" header
|
||||
});
|
||||
return response.json(); // parses JSON response into native JavaScript objects
|
||||
}
|
||||
|
||||
function isPercent(stat) {
|
||||
return stat == "CriticalHitChancePercent"
|
||||
|| stat == "CriticalHitDamagePercent"
|
||||
|| stat == "AttackPercent"
|
||||
|| stat == "HealthPercent"
|
||||
|| stat == "DefensePercent"
|
||||
|| stat == "EffectivenessPercent"
|
||||
|| stat == "EffectResistancePercent";
|
||||
}
|
||||
25
frontend/tsconfig.json
Normal file
25
frontend/tsconfig.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
10
frontend/tsconfig.node.json
Normal file
10
frontend/tsconfig.node.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
15
frontend/vite.config.ts
Normal file
15
frontend/vite.config.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
},
|
||||
server: {
|
||||
port: 34115,
|
||||
strictPort: true,
|
||||
},
|
||||
})
|
||||
39
frontend/wailsjs/go/models.ts
Normal file
39
frontend/wailsjs/go/models.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
export namespace model {
|
||||
|
||||
export class CaptureStatus {
|
||||
is_capturing: boolean;
|
||||
status: string;
|
||||
error?: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new CaptureStatus(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.is_capturing = source["is_capturing"];
|
||||
this.status = source["status"];
|
||||
this.error = source["error"];
|
||||
}
|
||||
}
|
||||
export class NetworkInterface {
|
||||
name: string;
|
||||
description: string;
|
||||
addresses: string[];
|
||||
is_loopback: boolean;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new NetworkInterface(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.name = source["name"];
|
||||
this.description = source["description"];
|
||||
this.addresses = source["addresses"];
|
||||
this.is_loopback = source["is_loopback"];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
19
frontend/wailsjs/go/service/App.d.ts
vendored
Normal file
19
frontend/wailsjs/go/service/App.d.ts
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
import {model} from '../models';
|
||||
|
||||
export function ExportData(arg1:Array<string>,arg2:string):Promise<void>;
|
||||
|
||||
export function GetCaptureStatus():Promise<model.CaptureStatus>;
|
||||
|
||||
export function GetCapturedData():Promise<Array<string>>;
|
||||
|
||||
export function GetNetworkInterfaces():Promise<Array<model.NetworkInterface>>;
|
||||
|
||||
export function ParseData(arg1:Array<string>):Promise<string>;
|
||||
|
||||
export function ReadRawJsonFile():Promise<string>;
|
||||
|
||||
export function StartCapture(arg1:string):Promise<void>;
|
||||
|
||||
export function StopCapture():Promise<void>;
|
||||
35
frontend/wailsjs/go/service/App.js
Normal file
35
frontend/wailsjs/go/service/App.js
Normal file
@@ -0,0 +1,35 @@
|
||||
// @ts-check
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export function ExportData(arg1, arg2) {
|
||||
return window['go']['service']['App']['ExportData'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function GetCaptureStatus() {
|
||||
return window['go']['service']['App']['GetCaptureStatus']();
|
||||
}
|
||||
|
||||
export function GetCapturedData() {
|
||||
return window['go']['service']['App']['GetCapturedData']();
|
||||
}
|
||||
|
||||
export function GetNetworkInterfaces() {
|
||||
return window['go']['service']['App']['GetNetworkInterfaces']();
|
||||
}
|
||||
|
||||
export function ParseData(arg1) {
|
||||
return window['go']['service']['App']['ParseData'](arg1);
|
||||
}
|
||||
|
||||
export function ReadRawJsonFile() {
|
||||
return window['go']['service']['App']['ReadRawJsonFile']();
|
||||
}
|
||||
|
||||
export function StartCapture(arg1) {
|
||||
return window['go']['service']['App']['StartCapture'](arg1);
|
||||
}
|
||||
|
||||
export function StopCapture() {
|
||||
return window['go']['service']['App']['StopCapture']();
|
||||
}
|
||||
24
frontend/wailsjs/runtime/package.json
Normal file
24
frontend/wailsjs/runtime/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "@wailsapp/runtime",
|
||||
"version": "2.0.0",
|
||||
"description": "Wails Javascript runtime library",
|
||||
"main": "runtime.js",
|
||||
"types": "runtime.d.ts",
|
||||
"scripts": {
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/wailsapp/wails.git"
|
||||
},
|
||||
"keywords": [
|
||||
"Wails",
|
||||
"Javascript",
|
||||
"Go"
|
||||
],
|
||||
"author": "Lea Anthony <lea.anthony@gmail.com>",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/wailsapp/wails/issues"
|
||||
},
|
||||
"homepage": "https://github.com/wailsapp/wails#readme"
|
||||
}
|
||||
249
frontend/wailsjs/runtime/runtime.d.ts
vendored
Normal file
249
frontend/wailsjs/runtime/runtime.d.ts
vendored
Normal file
@@ -0,0 +1,249 @@
|
||||
/*
|
||||
_ __ _ __
|
||||
| | / /___ _(_) /____
|
||||
| | /| / / __ `/ / / ___/
|
||||
| |/ |/ / /_/ / / (__ )
|
||||
|__/|__/\__,_/_/_/____/
|
||||
The electron alternative for Go
|
||||
(c) Lea Anthony 2019-present
|
||||
*/
|
||||
|
||||
export interface Position {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface Size {
|
||||
w: number;
|
||||
h: number;
|
||||
}
|
||||
|
||||
export interface Screen {
|
||||
isCurrent: boolean;
|
||||
isPrimary: boolean;
|
||||
width : number
|
||||
height : number
|
||||
}
|
||||
|
||||
// Environment information such as platform, buildtype, ...
|
||||
export interface EnvironmentInfo {
|
||||
buildType: string;
|
||||
platform: string;
|
||||
arch: string;
|
||||
}
|
||||
|
||||
// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit)
|
||||
// emits the given event. Optional data may be passed with the event.
|
||||
// This will trigger any event listeners.
|
||||
export function EventsEmit(eventName: string, ...data: any): void;
|
||||
|
||||
// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name.
|
||||
export function EventsOn(eventName: string, callback: (...data: any) => void): () => void;
|
||||
|
||||
// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple)
|
||||
// sets up a listener for the given event name, but will only trigger a given number times.
|
||||
export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void;
|
||||
|
||||
// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce)
|
||||
// sets up a listener for the given event name, but will only trigger once.
|
||||
export function EventsOnce(eventName: string, callback: (...data: any) => void): () => void;
|
||||
|
||||
// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff)
|
||||
// unregisters the listener for the given event name.
|
||||
export function EventsOff(eventName: string, ...additionalEventNames: string[]): void;
|
||||
|
||||
// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall)
|
||||
// unregisters all listeners.
|
||||
export function EventsOffAll(): void;
|
||||
|
||||
// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint)
|
||||
// logs the given message as a raw message
|
||||
export function LogPrint(message: string): void;
|
||||
|
||||
// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace)
|
||||
// logs the given message at the `trace` log level.
|
||||
export function LogTrace(message: string): void;
|
||||
|
||||
// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug)
|
||||
// logs the given message at the `debug` log level.
|
||||
export function LogDebug(message: string): void;
|
||||
|
||||
// [LogError](https://wails.io/docs/reference/runtime/log#logerror)
|
||||
// logs the given message at the `error` log level.
|
||||
export function LogError(message: string): void;
|
||||
|
||||
// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal)
|
||||
// logs the given message at the `fatal` log level.
|
||||
// The application will quit after calling this method.
|
||||
export function LogFatal(message: string): void;
|
||||
|
||||
// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo)
|
||||
// logs the given message at the `info` log level.
|
||||
export function LogInfo(message: string): void;
|
||||
|
||||
// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning)
|
||||
// logs the given message at the `warning` log level.
|
||||
export function LogWarning(message: string): void;
|
||||
|
||||
// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload)
|
||||
// Forces a reload by the main application as well as connected browsers.
|
||||
export function WindowReload(): void;
|
||||
|
||||
// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp)
|
||||
// Reloads the application frontend.
|
||||
export function WindowReloadApp(): void;
|
||||
|
||||
// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop)
|
||||
// Sets the window AlwaysOnTop or not on top.
|
||||
export function WindowSetAlwaysOnTop(b: boolean): void;
|
||||
|
||||
// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme)
|
||||
// *Windows only*
|
||||
// Sets window theme to system default (dark/light).
|
||||
export function WindowSetSystemDefaultTheme(): void;
|
||||
|
||||
// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme)
|
||||
// *Windows only*
|
||||
// Sets window to light theme.
|
||||
export function WindowSetLightTheme(): void;
|
||||
|
||||
// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme)
|
||||
// *Windows only*
|
||||
// Sets window to dark theme.
|
||||
export function WindowSetDarkTheme(): void;
|
||||
|
||||
// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter)
|
||||
// Centers the window on the monitor the window is currently on.
|
||||
export function WindowCenter(): void;
|
||||
|
||||
// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle)
|
||||
// Sets the text in the window title bar.
|
||||
export function WindowSetTitle(title: string): void;
|
||||
|
||||
// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen)
|
||||
// Makes the window full screen.
|
||||
export function WindowFullscreen(): void;
|
||||
|
||||
// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen)
|
||||
// Restores the previous window dimensions and position prior to full screen.
|
||||
export function WindowUnfullscreen(): void;
|
||||
|
||||
// [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen)
|
||||
// Returns the state of the window, i.e. whether the window is in full screen mode or not.
|
||||
export function WindowIsFullscreen(): Promise<boolean>;
|
||||
|
||||
// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize)
|
||||
// Sets the width and height of the window.
|
||||
export function WindowSetSize(width: number, height: number): void;
|
||||
|
||||
// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize)
|
||||
// Gets the width and height of the window.
|
||||
export function WindowGetSize(): Promise<Size>;
|
||||
|
||||
// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize)
|
||||
// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions.
|
||||
// Setting a size of 0,0 will disable this constraint.
|
||||
export function WindowSetMaxSize(width: number, height: number): void;
|
||||
|
||||
// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize)
|
||||
// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions.
|
||||
// Setting a size of 0,0 will disable this constraint.
|
||||
export function WindowSetMinSize(width: number, height: number): void;
|
||||
|
||||
// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition)
|
||||
// Sets the window position relative to the monitor the window is currently on.
|
||||
export function WindowSetPosition(x: number, y: number): void;
|
||||
|
||||
// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition)
|
||||
// Gets the window position relative to the monitor the window is currently on.
|
||||
export function WindowGetPosition(): Promise<Position>;
|
||||
|
||||
// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide)
|
||||
// Hides the window.
|
||||
export function WindowHide(): void;
|
||||
|
||||
// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow)
|
||||
// Shows the window, if it is currently hidden.
|
||||
export function WindowShow(): void;
|
||||
|
||||
// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise)
|
||||
// Maximises the window to fill the screen.
|
||||
export function WindowMaximise(): void;
|
||||
|
||||
// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise)
|
||||
// Toggles between Maximised and UnMaximised.
|
||||
export function WindowToggleMaximise(): void;
|
||||
|
||||
// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise)
|
||||
// Restores the window to the dimensions and position prior to maximising.
|
||||
export function WindowUnmaximise(): void;
|
||||
|
||||
// [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised)
|
||||
// Returns the state of the window, i.e. whether the window is maximised or not.
|
||||
export function WindowIsMaximised(): Promise<boolean>;
|
||||
|
||||
// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise)
|
||||
// Minimises the window.
|
||||
export function WindowMinimise(): void;
|
||||
|
||||
// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise)
|
||||
// Restores the window to the dimensions and position prior to minimising.
|
||||
export function WindowUnminimise(): void;
|
||||
|
||||
// [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised)
|
||||
// Returns the state of the window, i.e. whether the window is minimised or not.
|
||||
export function WindowIsMinimised(): Promise<boolean>;
|
||||
|
||||
// [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal)
|
||||
// Returns the state of the window, i.e. whether the window is normal or not.
|
||||
export function WindowIsNormal(): Promise<boolean>;
|
||||
|
||||
// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour)
|
||||
// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels.
|
||||
export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void;
|
||||
|
||||
// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall)
|
||||
// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system.
|
||||
export function ScreenGetAll(): Promise<Screen[]>;
|
||||
|
||||
// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl)
|
||||
// Opens the given URL in the system browser.
|
||||
export function BrowserOpenURL(url: string): void;
|
||||
|
||||
// [Environment](https://wails.io/docs/reference/runtime/intro#environment)
|
||||
// Returns information about the environment
|
||||
export function Environment(): Promise<EnvironmentInfo>;
|
||||
|
||||
// [Quit](https://wails.io/docs/reference/runtime/intro#quit)
|
||||
// Quits the application.
|
||||
export function Quit(): void;
|
||||
|
||||
// [Hide](https://wails.io/docs/reference/runtime/intro#hide)
|
||||
// Hides the application.
|
||||
export function Hide(): void;
|
||||
|
||||
// [Show](https://wails.io/docs/reference/runtime/intro#show)
|
||||
// Shows the application.
|
||||
export function Show(): void;
|
||||
|
||||
// [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext)
|
||||
// Returns the current text stored on clipboard
|
||||
export function ClipboardGetText(): Promise<string>;
|
||||
|
||||
// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext)
|
||||
// Sets a text on the clipboard
|
||||
export function ClipboardSetText(text: string): Promise<boolean>;
|
||||
|
||||
// [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop)
|
||||
// OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
|
||||
export function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void
|
||||
|
||||
// [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff)
|
||||
// OnFileDropOff removes the drag and drop listeners and handlers.
|
||||
export function OnFileDropOff() :void
|
||||
|
||||
// Check if the file path resolver is available
|
||||
export function CanResolveFilePaths(): boolean;
|
||||
|
||||
// Resolves file paths for an array of files
|
||||
export function ResolveFilePaths(files: File[]): void
|
||||
238
frontend/wailsjs/runtime/runtime.js
Normal file
238
frontend/wailsjs/runtime/runtime.js
Normal file
@@ -0,0 +1,238 @@
|
||||
/*
|
||||
_ __ _ __
|
||||
| | / /___ _(_) /____
|
||||
| | /| / / __ `/ / / ___/
|
||||
| |/ |/ / /_/ / / (__ )
|
||||
|__/|__/\__,_/_/_/____/
|
||||
The electron alternative for Go
|
||||
(c) Lea Anthony 2019-present
|
||||
*/
|
||||
|
||||
export function LogPrint(message) {
|
||||
window.runtime.LogPrint(message);
|
||||
}
|
||||
|
||||
export function LogTrace(message) {
|
||||
window.runtime.LogTrace(message);
|
||||
}
|
||||
|
||||
export function LogDebug(message) {
|
||||
window.runtime.LogDebug(message);
|
||||
}
|
||||
|
||||
export function LogInfo(message) {
|
||||
window.runtime.LogInfo(message);
|
||||
}
|
||||
|
||||
export function LogWarning(message) {
|
||||
window.runtime.LogWarning(message);
|
||||
}
|
||||
|
||||
export function LogError(message) {
|
||||
window.runtime.LogError(message);
|
||||
}
|
||||
|
||||
export function LogFatal(message) {
|
||||
window.runtime.LogFatal(message);
|
||||
}
|
||||
|
||||
export function EventsOnMultiple(eventName, callback, maxCallbacks) {
|
||||
return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks);
|
||||
}
|
||||
|
||||
export function EventsOn(eventName, callback) {
|
||||
return EventsOnMultiple(eventName, callback, -1);
|
||||
}
|
||||
|
||||
export function EventsOff(eventName, ...additionalEventNames) {
|
||||
return window.runtime.EventsOff(eventName, ...additionalEventNames);
|
||||
}
|
||||
|
||||
export function EventsOnce(eventName, callback) {
|
||||
return EventsOnMultiple(eventName, callback, 1);
|
||||
}
|
||||
|
||||
export function EventsEmit(eventName) {
|
||||
let args = [eventName].slice.call(arguments);
|
||||
return window.runtime.EventsEmit.apply(null, args);
|
||||
}
|
||||
|
||||
export function WindowReload() {
|
||||
window.runtime.WindowReload();
|
||||
}
|
||||
|
||||
export function WindowReloadApp() {
|
||||
window.runtime.WindowReloadApp();
|
||||
}
|
||||
|
||||
export function WindowSetAlwaysOnTop(b) {
|
||||
window.runtime.WindowSetAlwaysOnTop(b);
|
||||
}
|
||||
|
||||
export function WindowSetSystemDefaultTheme() {
|
||||
window.runtime.WindowSetSystemDefaultTheme();
|
||||
}
|
||||
|
||||
export function WindowSetLightTheme() {
|
||||
window.runtime.WindowSetLightTheme();
|
||||
}
|
||||
|
||||
export function WindowSetDarkTheme() {
|
||||
window.runtime.WindowSetDarkTheme();
|
||||
}
|
||||
|
||||
export function WindowCenter() {
|
||||
window.runtime.WindowCenter();
|
||||
}
|
||||
|
||||
export function WindowSetTitle(title) {
|
||||
window.runtime.WindowSetTitle(title);
|
||||
}
|
||||
|
||||
export function WindowFullscreen() {
|
||||
window.runtime.WindowFullscreen();
|
||||
}
|
||||
|
||||
export function WindowUnfullscreen() {
|
||||
window.runtime.WindowUnfullscreen();
|
||||
}
|
||||
|
||||
export function WindowIsFullscreen() {
|
||||
return window.runtime.WindowIsFullscreen();
|
||||
}
|
||||
|
||||
export function WindowGetSize() {
|
||||
return window.runtime.WindowGetSize();
|
||||
}
|
||||
|
||||
export function WindowSetSize(width, height) {
|
||||
window.runtime.WindowSetSize(width, height);
|
||||
}
|
||||
|
||||
export function WindowSetMaxSize(width, height) {
|
||||
window.runtime.WindowSetMaxSize(width, height);
|
||||
}
|
||||
|
||||
export function WindowSetMinSize(width, height) {
|
||||
window.runtime.WindowSetMinSize(width, height);
|
||||
}
|
||||
|
||||
export function WindowSetPosition(x, y) {
|
||||
window.runtime.WindowSetPosition(x, y);
|
||||
}
|
||||
|
||||
export function WindowGetPosition() {
|
||||
return window.runtime.WindowGetPosition();
|
||||
}
|
||||
|
||||
export function WindowHide() {
|
||||
window.runtime.WindowHide();
|
||||
}
|
||||
|
||||
export function WindowShow() {
|
||||
window.runtime.WindowShow();
|
||||
}
|
||||
|
||||
export function WindowMaximise() {
|
||||
window.runtime.WindowMaximise();
|
||||
}
|
||||
|
||||
export function WindowToggleMaximise() {
|
||||
window.runtime.WindowToggleMaximise();
|
||||
}
|
||||
|
||||
export function WindowUnmaximise() {
|
||||
window.runtime.WindowUnmaximise();
|
||||
}
|
||||
|
||||
export function WindowIsMaximised() {
|
||||
return window.runtime.WindowIsMaximised();
|
||||
}
|
||||
|
||||
export function WindowMinimise() {
|
||||
window.runtime.WindowMinimise();
|
||||
}
|
||||
|
||||
export function WindowUnminimise() {
|
||||
window.runtime.WindowUnminimise();
|
||||
}
|
||||
|
||||
export function WindowSetBackgroundColour(R, G, B, A) {
|
||||
window.runtime.WindowSetBackgroundColour(R, G, B, A);
|
||||
}
|
||||
|
||||
export function ScreenGetAll() {
|
||||
return window.runtime.ScreenGetAll();
|
||||
}
|
||||
|
||||
export function WindowIsMinimised() {
|
||||
return window.runtime.WindowIsMinimised();
|
||||
}
|
||||
|
||||
export function WindowIsNormal() {
|
||||
return window.runtime.WindowIsNormal();
|
||||
}
|
||||
|
||||
export function BrowserOpenURL(url) {
|
||||
window.runtime.BrowserOpenURL(url);
|
||||
}
|
||||
|
||||
export function Environment() {
|
||||
return window.runtime.Environment();
|
||||
}
|
||||
|
||||
export function Quit() {
|
||||
window.runtime.Quit();
|
||||
}
|
||||
|
||||
export function Hide() {
|
||||
window.runtime.Hide();
|
||||
}
|
||||
|
||||
export function Show() {
|
||||
window.runtime.Show();
|
||||
}
|
||||
|
||||
export function ClipboardGetText() {
|
||||
return window.runtime.ClipboardGetText();
|
||||
}
|
||||
|
||||
export function ClipboardSetText(text) {
|
||||
return window.runtime.ClipboardSetText(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
|
||||
*
|
||||
* @export
|
||||
* @callback OnFileDropCallback
|
||||
* @param {number} x - x coordinate of the drop
|
||||
* @param {number} y - y coordinate of the drop
|
||||
* @param {string[]} paths - A list of file paths.
|
||||
*/
|
||||
|
||||
/**
|
||||
* OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
|
||||
*
|
||||
* @export
|
||||
* @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
|
||||
* @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target)
|
||||
*/
|
||||
export function OnFileDrop(callback, useDropTarget) {
|
||||
return window.runtime.OnFileDrop(callback, useDropTarget);
|
||||
}
|
||||
|
||||
/**
|
||||
* OnFileDropOff removes the drag and drop listeners and handlers.
|
||||
*/
|
||||
export function OnFileDropOff() {
|
||||
return window.runtime.OnFileDropOff();
|
||||
}
|
||||
|
||||
export function CanResolveFilePaths() {
|
||||
return window.runtime.CanResolveFilePaths();
|
||||
}
|
||||
|
||||
export function ResolveFilePaths(files) {
|
||||
return window.runtime.ResolveFilePaths(files);
|
||||
}
|
||||
42
go.mod
Normal file
42
go.mod
Normal file
@@ -0,0 +1,42 @@
|
||||
module equipment-analyzer
|
||||
|
||||
go 1.22.0
|
||||
|
||||
toolchain go1.24.4
|
||||
|
||||
require (
|
||||
github.com/google/gopacket v1.1.19
|
||||
github.com/wailsapp/wails/v2 v2.10.1
|
||||
go.uber.org/zap v1.26.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bep/debounce v1.2.1 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
|
||||
github.com/labstack/echo/v4 v4.13.3 // indirect
|
||||
github.com/labstack/gommon v0.4.2 // indirect
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
|
||||
github.com/leaanthony/gosod v1.0.4 // indirect
|
||||
github.com/leaanthony/slicer v1.6.0 // indirect
|
||||
github.com/leaanthony/u v1.1.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/samber/lo v1.49.1 // indirect
|
||||
github.com/tkrajina/go-reflector v0.5.8 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
github.com/wailsapp/go-webview2 v1.0.19 // indirect
|
||||
github.com/wailsapp/mimetype v1.4.1 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
golang.org/x/crypto v0.33.0 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
)
|
||||
101
go.sum
Normal file
101
go.sum
Normal file
@@ -0,0 +1,101 @@
|
||||
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
||||
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
|
||||
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
|
||||
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
|
||||
github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
|
||||
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
|
||||
github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc=
|
||||
github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA=
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A=
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
|
||||
github.com/leaanthony/gosod v1.0.4 h1:YLAbVyd591MRffDgxUOU1NwLhT9T1/YiwjKZpkNFeaI=
|
||||
github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw=
|
||||
github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js=
|
||||
github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8=
|
||||
github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
|
||||
github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
|
||||
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
|
||||
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
|
||||
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ=
|
||||
github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
github.com/wailsapp/go-webview2 v1.0.19 h1:7U3QcDj1PrBPaxJNCui2k1SkWml+Q5kvFUFyTImA6NU=
|
||||
github.com/wailsapp/go-webview2 v1.0.19/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
|
||||
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
|
||||
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
|
||||
github.com/wailsapp/wails/v2 v2.10.1 h1:QWHvWMXII2nI/nXz77gpPG8P3ehl6zKe+u4su5BWIns=
|
||||
github.com/wailsapp/wails/v2 v2.10.1/go.mod h1:zrebnFV6MQf9kx8HI4iAv63vsR5v67oS7GTEZ7Pz1TY=
|
||||
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
|
||||
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
|
||||
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
|
||||
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
162
internal/capture/capture.go
Normal file
162
internal/capture/capture.go
Normal file
@@ -0,0 +1,162 @@
|
||||
package capture
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"equipment-analyzer/internal/model"
|
||||
"github.com/google/gopacket"
|
||||
"github.com/google/gopacket/layers"
|
||||
"github.com/google/gopacket/pcap"
|
||||
)
|
||||
|
||||
type PacketCapture struct {
|
||||
handle *pcap.Handle
|
||||
isCapturing bool
|
||||
stopChan chan bool
|
||||
mutex sync.RWMutex
|
||||
tcpProcessor *TCPProcessor
|
||||
dataChan chan *model.TCPData
|
||||
errorChan chan error
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
InterfaceName string
|
||||
Filter string
|
||||
Timeout time.Duration
|
||||
BufferSize int
|
||||
}
|
||||
|
||||
func NewPacketCapture() *PacketCapture {
|
||||
return &PacketCapture{
|
||||
stopChan: make(chan bool),
|
||||
tcpProcessor: NewTCPProcessor(),
|
||||
dataChan: make(chan *model.TCPData, 1000),
|
||||
errorChan: make(chan error, 100),
|
||||
}
|
||||
}
|
||||
|
||||
func (pc *PacketCapture) Start(config Config) error {
|
||||
pc.mutex.Lock()
|
||||
defer pc.mutex.Unlock()
|
||||
|
||||
if pc.isCapturing {
|
||||
return fmt.Errorf("capture already running")
|
||||
}
|
||||
|
||||
// 打开网络接口
|
||||
handle, err := pcap.OpenLive(
|
||||
config.InterfaceName,
|
||||
int32(config.BufferSize),
|
||||
true, // promiscuous
|
||||
config.Timeout,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open interface: %v", err)
|
||||
}
|
||||
|
||||
// 设置过滤器
|
||||
if err := handle.SetBPFFilter(config.Filter); err != nil {
|
||||
handle.Close()
|
||||
return fmt.Errorf("failed to set filter: %v", err)
|
||||
}
|
||||
|
||||
pc.handle = handle
|
||||
pc.isCapturing = true
|
||||
|
||||
// 启动抓包协程
|
||||
go pc.captureLoop()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pc *PacketCapture) Stop() {
|
||||
pc.mutex.Lock()
|
||||
defer pc.mutex.Unlock()
|
||||
|
||||
if !pc.isCapturing {
|
||||
return
|
||||
}
|
||||
|
||||
pc.isCapturing = false
|
||||
close(pc.stopChan)
|
||||
|
||||
if pc.handle != nil {
|
||||
pc.handle.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (pc *PacketCapture) IsCapturing() bool {
|
||||
pc.mutex.RLock()
|
||||
defer pc.mutex.RUnlock()
|
||||
return pc.isCapturing
|
||||
}
|
||||
|
||||
func (pc *PacketCapture) captureLoop() {
|
||||
packetSource := gopacket.NewPacketSource(pc.handle, pc.handle.LinkType())
|
||||
|
||||
log.Println("[抓包] 开始监听数据包...")
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-pc.stopChan:
|
||||
return
|
||||
default:
|
||||
packet, err := packetSource.NextPacket()
|
||||
if err != nil {
|
||||
if err.Error() == "Timeout Expired" {
|
||||
// 静默跳过超时
|
||||
continue
|
||||
}
|
||||
log.Printf("Error reading packet: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// 处理TCP包
|
||||
pc.processTCPPacket(packet)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (pc *PacketCapture) processTCPPacket(packet gopacket.Packet) {
|
||||
tcpLayer := packet.Layer(layers.LayerTypeTCP)
|
||||
if tcpLayer == nil {
|
||||
return
|
||||
}
|
||||
|
||||
tcp, ok := tcpLayer.(*layers.TCP)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// 提取TCP负载
|
||||
if len(tcp.Payload) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// 创建TCP数据包
|
||||
tcpData := &model.TCPData{
|
||||
Payload: tcp.Payload,
|
||||
Seq: uint32(tcp.Seq),
|
||||
Ack: uint32(tcp.Ack),
|
||||
SrcPort: uint16(tcp.SrcPort),
|
||||
DstPort: uint16(tcp.DstPort),
|
||||
}
|
||||
|
||||
// 发送给TCP处理器
|
||||
pc.tcpProcessor.ProcessPacket(tcpData)
|
||||
}
|
||||
|
||||
func (pc *PacketCapture) GetCapturedData() []string {
|
||||
return pc.tcpProcessor.GetFinalBuffer()
|
||||
}
|
||||
|
||||
func (pc *PacketCapture) ProcessAllData() {
|
||||
pc.tcpProcessor.ProcessAllData()
|
||||
}
|
||||
|
||||
func (pc *PacketCapture) Clear() {
|
||||
pc.tcpProcessor.Clear()
|
||||
}
|
||||
73
internal/capture/interface.go
Normal file
73
internal/capture/interface.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package capture
|
||||
|
||||
import (
|
||||
"equipment-analyzer/internal/model"
|
||||
"fmt"
|
||||
"github.com/google/gopacket/pcap"
|
||||
)
|
||||
|
||||
// GetNetworkInterfaces 获取网络接口列表
|
||||
func GetNetworkInterfaces() ([]model.NetworkInterface, error) {
|
||||
devices, err := pcap.FindAllDevs()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to find network devices: %v", err)
|
||||
}
|
||||
|
||||
var interfaces []model.NetworkInterface
|
||||
for _, device := range devices {
|
||||
// 跳过回环接口
|
||||
if device.Name == "lo" || device.Name == "loopback" {
|
||||
continue
|
||||
}
|
||||
|
||||
// 提取IP地址
|
||||
var addresses []string
|
||||
for _, address := range device.Addresses {
|
||||
addresses = append(addresses, address.IP.String())
|
||||
}
|
||||
|
||||
interfaceInfo := model.NetworkInterface{
|
||||
Name: device.Name,
|
||||
Description: device.Description,
|
||||
Addresses: addresses,
|
||||
IsLoopback: device.Name == "lo" || device.Name == "loopback",
|
||||
}
|
||||
|
||||
interfaces = append(interfaces, interfaceInfo)
|
||||
}
|
||||
|
||||
return interfaces, nil
|
||||
}
|
||||
|
||||
// GetDefaultInterface 获取默认网络接口
|
||||
func GetDefaultInterface() (*model.NetworkInterface, error) {
|
||||
interfaces, err := GetNetworkInterfaces()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 查找第一个非回环接口
|
||||
for _, iface := range interfaces {
|
||||
if !iface.IsLoopback && len(iface.Addresses) > 0 {
|
||||
return &iface, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("no suitable network interface found")
|
||||
}
|
||||
|
||||
// ValidateInterface 验证网络接口是否可用
|
||||
func ValidateInterface(interfaceName string) error {
|
||||
devices, err := pcap.FindAllDevs()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find network devices: %v", err)
|
||||
}
|
||||
|
||||
for _, device := range devices {
|
||||
if device.Name == interfaceName {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("interface %s not found", interfaceName)
|
||||
}
|
||||
96
internal/capture/processor.go
Normal file
96
internal/capture/processor.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package capture
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"equipment-analyzer/internal/model"
|
||||
"sort"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type TCPProcessor struct {
|
||||
mutex sync.RWMutex
|
||||
ackData map[uint32][]*model.TCPData
|
||||
finalBuffer []string
|
||||
loads map[string]bool // 去重用
|
||||
}
|
||||
|
||||
func NewTCPProcessor() *TCPProcessor {
|
||||
return &TCPProcessor{
|
||||
ackData: make(map[uint32][]*model.TCPData),
|
||||
loads: make(map[string]bool),
|
||||
}
|
||||
}
|
||||
|
||||
func (tp *TCPProcessor) ProcessPacket(tcpData *model.TCPData) {
|
||||
tp.mutex.Lock()
|
||||
defer tp.mutex.Unlock()
|
||||
|
||||
// 生成数据哈希用于去重
|
||||
hash := tp.generateHash(tcpData.Payload)
|
||||
if tp.loads[hash] {
|
||||
return // 跳过重复数据
|
||||
}
|
||||
tp.loads[hash] = true
|
||||
|
||||
// 按ACK号分组存储
|
||||
ack := uint32(tcpData.Ack)
|
||||
if tp.ackData[ack] == nil {
|
||||
tp.ackData[ack] = make([]*model.TCPData, 0)
|
||||
}
|
||||
tp.ackData[ack] = append(tp.ackData[ack], tcpData)
|
||||
}
|
||||
|
||||
func (tp *TCPProcessor) generateHash(data []byte) string {
|
||||
hash := md5.Sum(data)
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
func (tp *TCPProcessor) ProcessAllData() {
|
||||
tp.mutex.Lock()
|
||||
defer tp.mutex.Unlock()
|
||||
|
||||
tp.finalBuffer = make([]string, 0)
|
||||
|
||||
for ack, dataList := range tp.ackData {
|
||||
tp.tryBuffer(ack, dataList)
|
||||
}
|
||||
}
|
||||
|
||||
func (tp *TCPProcessor) tryBuffer(ack uint32, dataList []*model.TCPData) {
|
||||
// 按序列号排序
|
||||
sort.Slice(dataList, func(i, j int) bool {
|
||||
return dataList[i].Seq < dataList[j].Seq
|
||||
})
|
||||
|
||||
// 合并所有分片数据
|
||||
var buffer bytes.Buffer
|
||||
for _, data := range dataList {
|
||||
buffer.Write(data.Payload)
|
||||
}
|
||||
|
||||
// 转换为十六进制字符串
|
||||
hexStr := hex.EncodeToString(buffer.Bytes())
|
||||
tp.finalBuffer = append(tp.finalBuffer, hexStr)
|
||||
}
|
||||
|
||||
func (tp *TCPProcessor) GetFinalBuffer() []string {
|
||||
tp.mutex.RLock()
|
||||
defer tp.mutex.RUnlock()
|
||||
|
||||
result := make([]string, len(tp.finalBuffer))
|
||||
copy(result, tp.finalBuffer)
|
||||
//// 输出每条finalBuffer的16进制字符串到控制台
|
||||
//fmt.Println("抓取16进制字符串")
|
||||
//fmt.Println(result)
|
||||
return result
|
||||
}
|
||||
func (tp *TCPProcessor) Clear() {
|
||||
tp.mutex.Lock()
|
||||
defer tp.mutex.Unlock()
|
||||
|
||||
tp.ackData = make(map[uint32][]*model.TCPData)
|
||||
tp.finalBuffer = make([]string, 0)
|
||||
tp.loads = make(map[string]bool)
|
||||
}
|
||||
119
internal/config/config.go
Normal file
119
internal/config/config.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
App AppConfig `json:"app"`
|
||||
Capture CaptureConfig `json:"capture"`
|
||||
Parser ParserConfig `json:"parser"`
|
||||
Log LogConfig `json:"log"`
|
||||
}
|
||||
|
||||
type AppConfig struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Debug bool `json:"debug"`
|
||||
}
|
||||
|
||||
type CaptureConfig struct {
|
||||
DefaultFilter string `json:"default_filter"`
|
||||
DefaultTimeout int `json:"default_timeout"`
|
||||
BufferSize int `json:"buffer_size"`
|
||||
MaxPacketSize int `json:"max_packet_size"`
|
||||
}
|
||||
|
||||
type ParserConfig struct {
|
||||
MaxDataSize int `json:"max_data_size"`
|
||||
EnableValidation bool `json:"enable_validation"`
|
||||
}
|
||||
|
||||
type LogConfig struct {
|
||||
Level string `json:"level"`
|
||||
File string `json:"file"`
|
||||
MaxSize int `json:"max_size"`
|
||||
MaxBackups int `json:"max_backups"`
|
||||
MaxAge int `json:"max_age"`
|
||||
Compress bool `json:"compress"`
|
||||
}
|
||||
|
||||
// Load 加载配置文件
|
||||
func Load() (*Config, error) {
|
||||
configPath := getConfigPath()
|
||||
|
||||
// 如果配置文件不存在,创建默认配置
|
||||
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
||||
if err := createDefaultConfig(configPath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
file, err := os.Open(configPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var config Config
|
||||
if err := json.NewDecoder(file).Decode(&config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
func getConfigPath() string {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "config.json"
|
||||
}
|
||||
return filepath.Join(homeDir, ".equipment-analyzer", "config.json")
|
||||
}
|
||||
|
||||
func createDefaultConfig(path string) error {
|
||||
config := &Config{
|
||||
App: AppConfig{
|
||||
Name: "equipment-analyzer",
|
||||
Version: "1.0.0",
|
||||
Debug: false,
|
||||
},
|
||||
Capture: CaptureConfig{
|
||||
DefaultFilter: "tcp and ( port 5222 or port 3333 )",
|
||||
DefaultTimeout: 3000,
|
||||
BufferSize: 1600,
|
||||
MaxPacketSize: 65535,
|
||||
},
|
||||
Parser: ParserConfig{
|
||||
MaxDataSize: 1024 * 1024, // 1MB
|
||||
EnableValidation: true,
|
||||
},
|
||||
Log: LogConfig{
|
||||
Level: "info",
|
||||
File: "equipment-analyzer.log",
|
||||
MaxSize: 100, // MB
|
||||
MaxBackups: 3,
|
||||
MaxAge: 28, // days
|
||||
Compress: true,
|
||||
},
|
||||
}
|
||||
|
||||
// 创建目录
|
||||
dir := filepath.Dir(path)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 写入配置文件
|
||||
file, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
encoder := json.NewEncoder(file)
|
||||
encoder.SetIndent("", " ")
|
||||
return encoder.Encode(config)
|
||||
}
|
||||
68
internal/model/equipment.go
Normal file
68
internal/model/equipment.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Equipment 装备模型
|
||||
type Equipment struct {
|
||||
ID string `json:"id" db:"id"`
|
||||
Code string `json:"code" db:"code"`
|
||||
Ct time.Time `json:"ct" db:"created_time"`
|
||||
E int `json:"e" db:"experience"`
|
||||
G int `json:"g" db:"grade"`
|
||||
L bool `json:"l" db:"locked"`
|
||||
Mg int `json:"mg" db:"magic"`
|
||||
Op []Operation `json:"op" db:"operations"`
|
||||
P int `json:"p" db:"power"`
|
||||
S string `json:"s" db:"string_value"`
|
||||
Sk int `json:"sk" db:"skill"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
// Operation 操作属性
|
||||
type Operation struct {
|
||||
Type string `json:"type"`
|
||||
Value interface{} `json:"value"`
|
||||
}
|
||||
|
||||
// MarshalJSON 自定义JSON序列化
|
||||
func (e *Equipment) MarshalJSON() ([]byte, error) {
|
||||
type Alias Equipment
|
||||
return json.Marshal(&struct {
|
||||
*Alias
|
||||
Ct int64 `json:"ct"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
}{
|
||||
Alias: (*Alias)(e),
|
||||
Ct: e.Ct.Unix(),
|
||||
CreatedAt: e.CreatedAt.Unix(),
|
||||
UpdatedAt: e.UpdatedAt.Unix(),
|
||||
})
|
||||
}
|
||||
|
||||
// UnmarshalJSON 自定义JSON反序列化
|
||||
func (e *Equipment) UnmarshalJSON(data []byte) error {
|
||||
type Alias Equipment
|
||||
aux := &struct {
|
||||
*Alias
|
||||
Ct int64 `json:"ct"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
}{
|
||||
Alias: (*Alias)(e),
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &aux); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
e.Ct = time.Unix(aux.Ct, 0)
|
||||
e.CreatedAt = time.Unix(aux.CreatedAt, 0)
|
||||
e.UpdatedAt = time.Unix(aux.UpdatedAt, 0)
|
||||
|
||||
return nil
|
||||
}
|
||||
31
internal/model/packet.go
Normal file
31
internal/model/packet.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package model
|
||||
|
||||
// TCPData TCP数据包模型
|
||||
type TCPData struct {
|
||||
Payload []byte
|
||||
Seq uint32
|
||||
Ack uint32
|
||||
SrcPort uint16
|
||||
DstPort uint16
|
||||
}
|
||||
|
||||
// CaptureResult 抓包结果
|
||||
type CaptureResult struct {
|
||||
Data []Equipment `json:"data"`
|
||||
Units []interface{} `json:"units"`
|
||||
}
|
||||
|
||||
// NetworkInterface 网络接口信息
|
||||
type NetworkInterface struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Addresses []string `json:"addresses"`
|
||||
IsLoopback bool `json:"is_loopback"`
|
||||
}
|
||||
|
||||
// CaptureStatus 抓包状态
|
||||
type CaptureStatus struct {
|
||||
IsCapturing bool `json:"is_capturing"`
|
||||
Status string `json:"status"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
206
internal/parser/hex_parser.go
Normal file
206
internal/parser/hex_parser.go
Normal file
@@ -0,0 +1,206 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"equipment-analyzer/internal/model"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
type HexParser struct{}
|
||||
|
||||
func NewHexParser() *HexParser {
|
||||
return &HexParser{}
|
||||
}
|
||||
|
||||
func (hp *HexParser) ParseHexData(hexDataList []string) (*model.CaptureResult, error) {
|
||||
result := &model.CaptureResult{
|
||||
Data: make([]model.Equipment, 0),
|
||||
Units: make([]interface{}, 0),
|
||||
}
|
||||
|
||||
for _, hexData := range hexDataList {
|
||||
equipment, err := hp.parseSingleEquipment(hexData)
|
||||
if err != nil {
|
||||
continue // 跳过解析失败的数据
|
||||
}
|
||||
result.Data = append(result.Data, *equipment)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (hp *HexParser) parseSingleEquipment(hexData string) (*model.Equipment, error) {
|
||||
// 将十六进制字符串转换为字节数组
|
||||
bytes, err := hex.DecodeString(hexData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid hex string: %v", err)
|
||||
}
|
||||
|
||||
if len(bytes) < 47 { // 最小长度检查
|
||||
return nil, fmt.Errorf("data too short")
|
||||
}
|
||||
|
||||
equipment := &model.Equipment{
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
// 解析各个字段(基于之前的分析逻辑)
|
||||
equipment.Code = hp.extractCode(bytes)
|
||||
equipment.Ct = hp.extractTimestamp(bytes)
|
||||
equipment.E = hp.extractExperience(bytes)
|
||||
equipment.G = hp.extractGrade(bytes)
|
||||
//equipment.ID = hp.extractID(bytes)
|
||||
equipment.L = hp.extractLocked(bytes)
|
||||
equipment.Mg = hp.extractMagic(bytes)
|
||||
equipment.Op = hp.extractOperations(bytes)
|
||||
equipment.P = hp.extractPower(bytes)
|
||||
equipment.S = hp.extractString(bytes)
|
||||
equipment.Sk = hp.extractSkill(bytes)
|
||||
|
||||
return equipment, nil
|
||||
}
|
||||
|
||||
func (hp *HexParser) extractCode(bytes []byte) string {
|
||||
if len(bytes) < 4 {
|
||||
return "efm00"
|
||||
}
|
||||
codeValue := hp.readInt(bytes, 0)
|
||||
return fmt.Sprintf("efm%02d", codeValue%100)
|
||||
}
|
||||
|
||||
func (hp *HexParser) extractTimestamp(bytes []byte) time.Time {
|
||||
if len(bytes) < 8 {
|
||||
return time.Now()
|
||||
}
|
||||
timestamp := hp.readInt(bytes, 4)
|
||||
return time.Unix(int64(timestamp), 0)
|
||||
}
|
||||
|
||||
func (hp *HexParser) extractExperience(bytes []byte) int {
|
||||
if len(bytes) < 12 {
|
||||
return 0
|
||||
}
|
||||
return hp.readInt(bytes, 8)
|
||||
}
|
||||
|
||||
func (hp *HexParser) extractGrade(bytes []byte) int {
|
||||
if len(bytes) < 13 {
|
||||
return 0
|
||||
}
|
||||
return int(bytes[12])
|
||||
}
|
||||
|
||||
func (hp *HexParser) extractID(bytes []byte) int {
|
||||
if len(bytes) < 17 {
|
||||
return 0
|
||||
}
|
||||
return hp.readInt(bytes, 13)
|
||||
}
|
||||
|
||||
func (hp *HexParser) extractLocked(bytes []byte) bool {
|
||||
if len(bytes) < 18 {
|
||||
return false
|
||||
}
|
||||
return (bytes[17] & 0x01) != 0
|
||||
}
|
||||
|
||||
func (hp *HexParser) extractMagic(bytes []byte) int {
|
||||
if len(bytes) < 20 {
|
||||
return 0
|
||||
}
|
||||
return hp.readShort(bytes, 18)
|
||||
}
|
||||
|
||||
func (hp *HexParser) extractOperations(bytes []byte) []model.Operation {
|
||||
operations := make([]model.Operation, 0)
|
||||
|
||||
if len(bytes) < 21 {
|
||||
return operations
|
||||
}
|
||||
|
||||
offset := 20
|
||||
opCount := int(bytes[offset])
|
||||
offset++
|
||||
|
||||
for i := 0; i < opCount && offset+3 < len(bytes); i++ {
|
||||
attrType := int(bytes[offset])
|
||||
offset++
|
||||
|
||||
attrValue := hp.readShort(bytes, offset)
|
||||
offset += 2
|
||||
|
||||
attrName := hp.getAttributeName(attrType)
|
||||
|
||||
op := model.Operation{
|
||||
Type: attrName,
|
||||
Value: attrValue,
|
||||
}
|
||||
operations = append(operations, op)
|
||||
}
|
||||
|
||||
return operations
|
||||
}
|
||||
|
||||
func (hp *HexParser) extractPower(bytes []byte) int {
|
||||
if len(bytes) < 44 {
|
||||
return 0
|
||||
}
|
||||
return hp.readInt(bytes, 40)
|
||||
}
|
||||
|
||||
func (hp *HexParser) extractString(bytes []byte) string {
|
||||
if len(bytes) < 46 {
|
||||
return "0000"
|
||||
}
|
||||
stringValue := hp.readShort(bytes, 44)
|
||||
return fmt.Sprintf("%04x", stringValue)
|
||||
}
|
||||
|
||||
func (hp *HexParser) extractSkill(bytes []byte) int {
|
||||
if len(bytes) < 47 {
|
||||
return 0
|
||||
}
|
||||
return int(bytes[46])
|
||||
}
|
||||
|
||||
func (hp *HexParser) readInt(bytes []byte, offset int) int {
|
||||
if offset+3 >= len(bytes) {
|
||||
return 0
|
||||
}
|
||||
return int(bytes[offset])<<24 | int(bytes[offset+1])<<16 |
|
||||
int(bytes[offset+2])<<8 | int(bytes[offset+3])
|
||||
}
|
||||
|
||||
func (hp *HexParser) readShort(bytes []byte, offset int) int {
|
||||
if offset+1 >= len(bytes) {
|
||||
return 0
|
||||
}
|
||||
return int(bytes[offset])<<8 | int(bytes[offset+1])
|
||||
}
|
||||
|
||||
func (hp *HexParser) getAttributeName(attrType int) string {
|
||||
switch attrType {
|
||||
case 1:
|
||||
return "att"
|
||||
case 2:
|
||||
return "def"
|
||||
case 3:
|
||||
return "hp"
|
||||
case 4:
|
||||
return "max_hp"
|
||||
case 5:
|
||||
return "speed"
|
||||
case 6:
|
||||
return "crit"
|
||||
case 7:
|
||||
return "crit_dmg"
|
||||
case 8:
|
||||
return "effectiveness"
|
||||
case 9:
|
||||
return "effect_resistance"
|
||||
default:
|
||||
return fmt.Sprintf("unknown_%d", attrType)
|
||||
}
|
||||
}
|
||||
103
internal/service/capture_service.go
Normal file
103
internal/service/capture_service.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"equipment-analyzer/internal/capture"
|
||||
"equipment-analyzer/internal/config"
|
||||
"equipment-analyzer/internal/model"
|
||||
"equipment-analyzer/internal/utils"
|
||||
)
|
||||
|
||||
type CaptureService struct {
|
||||
config *config.Config
|
||||
logger *utils.Logger
|
||||
packetCapture *capture.PacketCapture
|
||||
processor *capture.TCPProcessor
|
||||
mutex sync.RWMutex
|
||||
isCapturing bool
|
||||
dataChan chan *model.CaptureResult
|
||||
errorChan chan error
|
||||
}
|
||||
|
||||
func NewCaptureService(cfg *config.Config, logger *utils.Logger) *CaptureService {
|
||||
return &CaptureService{
|
||||
config: cfg,
|
||||
logger: logger,
|
||||
packetCapture: capture.NewPacketCapture(),
|
||||
processor: capture.NewTCPProcessor(),
|
||||
dataChan: make(chan *model.CaptureResult, 100),
|
||||
errorChan: make(chan error, 100),
|
||||
}
|
||||
}
|
||||
|
||||
// StartCapture 开始抓包
|
||||
func (cs *CaptureService) StartCapture(ctx context.Context, config capture.Config) error {
|
||||
cs.mutex.Lock()
|
||||
defer cs.mutex.Unlock()
|
||||
|
||||
if cs.isCapturing {
|
||||
return fmt.Errorf("capture already running")
|
||||
}
|
||||
|
||||
if err := cs.packetCapture.Start(config); err != nil {
|
||||
return fmt.Errorf("failed to start capture: %w", err)
|
||||
}
|
||||
|
||||
cs.isCapturing = true
|
||||
cs.logger.Info("Packet capture started", "interface", config.InterfaceName)
|
||||
|
||||
// 启动数据处理协程
|
||||
go cs.processData(ctx)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// StopCapture 停止抓包
|
||||
func (cs *CaptureService) StopCapture() error {
|
||||
cs.mutex.Lock()
|
||||
defer cs.mutex.Unlock()
|
||||
|
||||
if !cs.isCapturing {
|
||||
return fmt.Errorf("capture not running")
|
||||
}
|
||||
|
||||
cs.packetCapture.Stop()
|
||||
cs.isCapturing = false
|
||||
cs.logger.Info("Packet capture stopped")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCapturedData 获取抓包数据
|
||||
func (cs *CaptureService) GetCapturedData() []string {
|
||||
return cs.packetCapture.GetCapturedData()
|
||||
}
|
||||
|
||||
// ProcessAllData 处理所有数据
|
||||
func (cs *CaptureService) ProcessAllData() {
|
||||
cs.packetCapture.ProcessAllData()
|
||||
}
|
||||
|
||||
// IsCapturing 检查是否正在抓包
|
||||
func (cs *CaptureService) IsCapturing() bool {
|
||||
cs.mutex.RLock()
|
||||
defer cs.mutex.RUnlock()
|
||||
return cs.isCapturing
|
||||
}
|
||||
|
||||
// processData 处理抓包数据
|
||||
func (cs *CaptureService) processData(ctx context.Context) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
// 这里可以添加实时数据处理逻辑
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
}
|
||||
168
internal/service/main_service.go
Normal file
168
internal/service/main_service.go
Normal file
@@ -0,0 +1,168 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"equipment-analyzer/internal/capture"
|
||||
"equipment-analyzer/internal/config"
|
||||
"equipment-analyzer/internal/model"
|
||||
"equipment-analyzer/internal/utils"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
config *config.Config
|
||||
logger *utils.Logger
|
||||
captureService *CaptureService
|
||||
parserService *ParserService
|
||||
}
|
||||
|
||||
func NewApp(cfg *config.Config, logger *utils.Logger) *App {
|
||||
return &App{
|
||||
config: cfg,
|
||||
logger: logger,
|
||||
captureService: NewCaptureService(cfg, logger),
|
||||
parserService: NewParserService(cfg, logger),
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) Startup(ctx context.Context) {
|
||||
a.logger.Info("应用启动")
|
||||
}
|
||||
|
||||
func (a *App) DomReady(ctx context.Context) {
|
||||
a.logger.Info("DOM准备就绪")
|
||||
}
|
||||
|
||||
func (a *App) BeforeClose(ctx context.Context) (prevent bool) {
|
||||
a.logger.Info("应用即将关闭")
|
||||
return false
|
||||
}
|
||||
|
||||
func (a *App) Shutdown(ctx context.Context) {
|
||||
a.logger.Info("应用关闭")
|
||||
}
|
||||
|
||||
// GetNetworkInterfaces 获取网络接口列表
|
||||
func (a *App) GetNetworkInterfaces() ([]model.NetworkInterface, error) {
|
||||
interfaces, err := capture.GetNetworkInterfaces()
|
||||
if err != nil {
|
||||
a.logger.Error("获取网络接口失败", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
return interfaces, nil
|
||||
}
|
||||
|
||||
// StartCapture 开始抓包
|
||||
func (a *App) StartCapture(interfaceName string) error {
|
||||
if a.captureService.IsCapturing() {
|
||||
return fmt.Errorf("抓包已在进行中")
|
||||
}
|
||||
|
||||
config := capture.Config{
|
||||
InterfaceName: interfaceName,
|
||||
Filter: a.config.Capture.DefaultFilter,
|
||||
Timeout: time.Duration(a.config.Capture.DefaultTimeout) * time.Millisecond,
|
||||
BufferSize: a.config.Capture.BufferSize,
|
||||
}
|
||||
|
||||
err := a.captureService.StartCapture(context.Background(), config)
|
||||
if err != nil {
|
||||
a.logger.Error("开始抓包失败", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
a.logger.Info("抓包开始", "interface", interfaceName)
|
||||
return nil
|
||||
}
|
||||
|
||||
// StopCapture 停止抓包
|
||||
func (a *App) StopCapture() error {
|
||||
if !a.captureService.IsCapturing() {
|
||||
return fmt.Errorf("没有正在进行的抓包")
|
||||
}
|
||||
|
||||
err := a.captureService.StopCapture()
|
||||
if err != nil {
|
||||
a.logger.Error("停止抓包失败", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// 处理所有收集的数据
|
||||
a.captureService.ProcessAllData()
|
||||
|
||||
a.logger.Info("抓包停止")
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCapturedData 获取抓包数据
|
||||
func (a *App) GetCapturedData() ([]string, error) {
|
||||
return a.captureService.GetCapturedData(), nil
|
||||
}
|
||||
|
||||
// ParseData 解析数据为JSON
|
||||
func (a *App) ParseData(hexDataList []string) (string, error) {
|
||||
result, err := a.parserService.ParseHexData(hexDataList)
|
||||
if err != nil {
|
||||
a.logger.Error("解析数据失败", "error", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
jsonData, err := json.MarshalIndent(result, "", " ")
|
||||
if err != nil {
|
||||
a.logger.Error("JSON序列化失败", "error", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
a.logger.Info("数据解析完成", "count", len(result.Data))
|
||||
return string(jsonData), nil
|
||||
}
|
||||
|
||||
// ExportData 导出数据到文件
|
||||
func (a *App) ExportData(hexDataList []string, filename string) error {
|
||||
result, err := a.parserService.ParseHexData(hexDataList)
|
||||
if err != nil {
|
||||
a.logger.Error("解析数据失败", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
jsonData, err := json.MarshalIndent(result, "", " ")
|
||||
if err != nil {
|
||||
a.logger.Error("JSON序列化失败", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// 这里可以添加文件写入逻辑
|
||||
a.logger.Info("导出数据", "filename", filename, "count", len(result.Data))
|
||||
|
||||
// 简单示例:写入到当前目录
|
||||
err = utils.WriteFile(filename, jsonData)
|
||||
if err != nil {
|
||||
a.logger.Error("写入文件失败", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCaptureStatus 获取抓包状态
|
||||
func (a *App) GetCaptureStatus() model.CaptureStatus {
|
||||
return model.CaptureStatus{
|
||||
IsCapturing: a.captureService.IsCapturing(),
|
||||
Status: a.getStatusMessage(),
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) getStatusMessage() string {
|
||||
if a.captureService.IsCapturing() {
|
||||
return "正在抓包..."
|
||||
}
|
||||
return "准备就绪"
|
||||
}
|
||||
|
||||
// ReadRawJsonFile 供前端调用,读取output_raw.json内容
|
||||
func (a *App) ReadRawJsonFile() (string, error) {
|
||||
return a.parserService.ReadRawJsonFile()
|
||||
}
|
||||
95152
internal/service/output_raw.json
Normal file
95152
internal/service/output_raw.json
Normal file
File diff suppressed because it is too large
Load Diff
516
internal/service/parser_service.go
Normal file
516
internal/service/parser_service.go
Normal file
@@ -0,0 +1,516 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"equipment-analyzer/internal/config"
|
||||
"equipment-analyzer/internal/model"
|
||||
"equipment-analyzer/internal/parser"
|
||||
"equipment-analyzer/internal/utils"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ParserService struct {
|
||||
config *config.Config
|
||||
logger *utils.Logger
|
||||
hexParser *parser.HexParser
|
||||
}
|
||||
|
||||
func NewParserService(cfg *config.Config, logger *utils.Logger) *ParserService {
|
||||
return &ParserService{
|
||||
config: cfg,
|
||||
logger: logger,
|
||||
hexParser: parser.NewHexParser(),
|
||||
}
|
||||
}
|
||||
|
||||
// ParseHexData 解析十六进制数据
|
||||
func (ps *ParserService) ParseHexData(hexDataList []string) (*model.CaptureResult, error) {
|
||||
if len(hexDataList) == 0 {
|
||||
ps.logger.Warn("没有数据需要解析")
|
||||
return &model.CaptureResult{
|
||||
Data: make([]model.Equipment, 0),
|
||||
Units: make([]interface{}, 0),
|
||||
}, nil
|
||||
}
|
||||
|
||||
ps.logger.Info("开始远程解析数据", "count", len(hexDataList))
|
||||
|
||||
// 远程接口解析
|
||||
url := "https://krivpfvxi0.execute-api.us-west-2.amazonaws.com/dev/getItems"
|
||||
reqBody := map[string]interface{}{
|
||||
"data": hexDataList,
|
||||
}
|
||||
jsonBytes, _ := json.Marshal(reqBody)
|
||||
client := &http.Client{Timeout: 15 * time.Second}
|
||||
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonBytes))
|
||||
if err == nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := client.Do(req)
|
||||
if err == nil && resp.StatusCode == 200 {
|
||||
defer resp.Body.Close()
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
|
||||
// 直接写入本地文件
|
||||
fileErr := ioutil.WriteFile("output_raw.json", body, 0644)
|
||||
if fileErr != nil {
|
||||
ps.logger.Error("写入原始json文件失败", "error", fileErr)
|
||||
}
|
||||
|
||||
ps.logger.Info("远程原始数据已写入output_raw.json")
|
||||
|
||||
// 返回空集合,保证前端不报错
|
||||
return &model.CaptureResult{
|
||||
Data: make([]model.Equipment, 0),
|
||||
Units: make([]interface{}, 0),
|
||||
}, nil
|
||||
} else if err != nil {
|
||||
ps.logger.Error("远程解析请求失败", "error", err)
|
||||
return nil, fmt.Errorf("远程解析请求失败: %v", err)
|
||||
} else {
|
||||
ps.logger.Error("远程解析响应码异常", "status", resp.StatusCode)
|
||||
return nil, fmt.Errorf("远程解析响应码异常: %d", resp.StatusCode)
|
||||
}
|
||||
} else {
|
||||
ps.logger.Error("远程解析请求构建失败", "error", err)
|
||||
return nil, fmt.Errorf("远程解析请求构建失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ReadRawJsonFile 读取output_raw.json文件内容并进行数据转换
|
||||
func (ps *ParserService) ReadRawJsonFile() (string, error) {
|
||||
data, err := ioutil.ReadFile("output_raw.json")
|
||||
if err != nil {
|
||||
ps.logger.Error("读取output_raw.json失败", "error", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 解析原始JSON数据
|
||||
var rawData map[string]interface{}
|
||||
if err := json.Unmarshal(data, &rawData); err != nil {
|
||||
ps.logger.Error("解析JSON失败", "error", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 提取装备和英雄数据
|
||||
equips, _ := rawData["data"].([]interface{})
|
||||
// 修正:units 取最大长度的那组
|
||||
var rawUnits []interface{}
|
||||
if unitsRaw, ok := rawData["units"].([]interface{}); ok && len(unitsRaw) > 0 {
|
||||
maxLen := 0
|
||||
for _, u := range unitsRaw {
|
||||
if arr, ok := u.([]interface{}); ok && len(arr) > maxLen {
|
||||
maxLen = len(arr)
|
||||
rawUnits = arr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1. 原始装备总数
|
||||
fmt.Println("原始装备总数:", len(equips))
|
||||
|
||||
// 过滤有效装备 (x => !!x.f)
|
||||
var validEquips []interface{}
|
||||
for _, equip := range equips {
|
||||
if equipMap, ok := equip.(map[string]interface{}); ok {
|
||||
if f, exists := equipMap["f"]; exists && f != nil && f != "" {
|
||||
validEquips = append(validEquips, equip)
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Println("过滤f字段后装备数:", len(validEquips))
|
||||
|
||||
// 转换装备数据
|
||||
convertedItems := ps.convertItemsAllWithLog(validEquips)
|
||||
fmt.Println("转换后装备数:", len(convertedItems))
|
||||
|
||||
// 转换英雄数据(只对最大组)
|
||||
convertedHeroes := ps.convertUnits(rawUnits)
|
||||
|
||||
// 构建最终结果
|
||||
result := map[string]interface{}{
|
||||
"items": convertedItems,
|
||||
"heroes": convertedHeroes,
|
||||
}
|
||||
|
||||
// 序列化为JSON字符串
|
||||
resultJSON, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
ps.logger.Error("序列化结果失败", "error", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(resultJSON), nil
|
||||
}
|
||||
|
||||
// convertItems 转换装备数据
|
||||
func (ps *ParserService) convertItems(rawItems []interface{}) []map[string]interface{} {
|
||||
var convertedItems []map[string]interface{}
|
||||
|
||||
for _, rawItem := range rawItems {
|
||||
if itemMap, ok := rawItem.(map[string]interface{}); ok {
|
||||
convertedItem := ps.convertSingleItem(itemMap)
|
||||
if convertedItem != nil {
|
||||
convertedItems = append(convertedItems, convertedItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var filteredItems []map[string]interface{}
|
||||
for _, item := range convertedItems {
|
||||
filteredItems = append(filteredItems, item)
|
||||
}
|
||||
|
||||
return filteredItems
|
||||
}
|
||||
|
||||
// convertSingleItem 转换单个装备
|
||||
func (ps *ParserService) convertSingleItem(item map[string]interface{}) map[string]interface{} {
|
||||
converted := make(map[string]interface{})
|
||||
|
||||
// 复制基本字段
|
||||
for key, value := range item {
|
||||
converted[key] = value
|
||||
}
|
||||
|
||||
// 转换装备类型
|
||||
ps.convertGear(converted)
|
||||
|
||||
// 转换等级
|
||||
ps.convertRank(converted)
|
||||
|
||||
// 转换套装
|
||||
ps.convertSet(converted)
|
||||
|
||||
// 转换名称
|
||||
ps.convertName(converted)
|
||||
|
||||
// 转换等级
|
||||
ps.convertLevel(converted)
|
||||
|
||||
// 转换增强
|
||||
ps.convertEnhance(converted)
|
||||
|
||||
// 转换主属性
|
||||
ps.convertMainStat(converted)
|
||||
|
||||
// 转换副属性
|
||||
ps.convertSubStats(converted)
|
||||
|
||||
// 转换ID
|
||||
ps.convertId(converted)
|
||||
|
||||
// 转换装备ID
|
||||
ps.convertEquippedId(converted)
|
||||
|
||||
return converted
|
||||
}
|
||||
|
||||
// convertUnits 转换英雄数据
|
||||
func (ps *ParserService) convertUnits(rawUnits []interface{}) []map[string]interface{} {
|
||||
var convertedUnits []map[string]interface{}
|
||||
|
||||
for _, rawUnit := range rawUnits {
|
||||
if unitMap, ok := rawUnit.(map[string]interface{}); ok {
|
||||
if name, exists := unitMap["name"]; exists && name != nil && name != "" {
|
||||
if id, exists := unitMap["id"]; exists && id != nil {
|
||||
convertedUnit := make(map[string]interface{})
|
||||
for key, value := range unitMap {
|
||||
convertedUnit[key] = value
|
||||
}
|
||||
|
||||
// 转换星星和觉醒
|
||||
if g, exists := unitMap["g"]; exists {
|
||||
convertedUnit["stars"] = g
|
||||
}
|
||||
if z, exists := unitMap["z"]; exists {
|
||||
convertedUnit["awaken"] = z
|
||||
}
|
||||
|
||||
convertedUnits = append(convertedUnits, convertedUnit)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return convertedUnits
|
||||
}
|
||||
|
||||
// 转换函数实现
|
||||
func (ps *ParserService) convertGear(item map[string]interface{}) {
|
||||
if _, exists := item["type"]; !exists {
|
||||
if code, exists := item["code"].(string); exists {
|
||||
baseCode := code
|
||||
if idx := len(baseCode) - 1; idx >= 0 {
|
||||
gearLetter := string(baseCode[idx])
|
||||
item["gear"] = gearByGearLetter[gearLetter]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if itemType, exists := item["type"].(string); exists {
|
||||
item["gear"] = gearByIngameType[itemType]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ps *ParserService) convertRank(item map[string]interface{}) {
|
||||
if g, exists := item["g"].(float64); exists {
|
||||
rankIndex := int(g)
|
||||
if rankIndex >= 0 && rankIndex < len(rankByIngameGrade) {
|
||||
item["rank"] = rankByIngameGrade[rankIndex]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ps *ParserService) convertSet(item map[string]interface{}) {
|
||||
if f, exists := item["f"].(string); exists {
|
||||
item["set"] = setsByIngameSet[f]
|
||||
}
|
||||
}
|
||||
|
||||
func (ps *ParserService) convertName(item map[string]interface{}) {
|
||||
if _, exists := item["name"]; !exists {
|
||||
item["name"] = "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func (ps *ParserService) convertLevel(item map[string]interface{}) {
|
||||
if _, exists := item["level"]; !exists {
|
||||
item["level"] = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (ps *ParserService) convertEnhance(item map[string]interface{}) {
|
||||
rank, rankExists := item["rank"].(string)
|
||||
op, opExists := item["op"].([]interface{})
|
||||
|
||||
if rankExists && opExists {
|
||||
countByRank := map[string]int{
|
||||
"Normal": 5,
|
||||
"Good": 6,
|
||||
"Rare": 7,
|
||||
"Heroic": 8,
|
||||
"Epic": 9,
|
||||
}
|
||||
offsetByRank := map[string]int{
|
||||
"Normal": 0,
|
||||
"Good": 1,
|
||||
"Rare": 2,
|
||||
"Heroic": 3,
|
||||
"Epic": 4,
|
||||
}
|
||||
|
||||
count := countByRank[rank]
|
||||
offset := offsetByRank[rank]
|
||||
subsCount := len(op) - 1
|
||||
if subsCount > count {
|
||||
subsCount = count
|
||||
}
|
||||
|
||||
enhance := (subsCount - offset) * 3
|
||||
if enhance < 0 {
|
||||
enhance = 0
|
||||
}
|
||||
item["enhance"] = enhance
|
||||
}
|
||||
}
|
||||
|
||||
func (ps *ParserService) convertMainStat(item map[string]interface{}) {
|
||||
op, opExists := item["op"].([]interface{})
|
||||
mainStatValue, mainStatExists := item["mainStatValue"].(float64)
|
||||
|
||||
if opExists && len(op) > 0 && mainStatExists {
|
||||
if mainOp, ok := op[0].([]interface{}); ok && len(mainOp) > 0 {
|
||||
if mainOpType, ok := mainOp[0].(string); ok {
|
||||
mainType := statByIngameStat[mainOpType]
|
||||
var mainValue float64
|
||||
|
||||
if ps.isFlat(mainOpType) {
|
||||
mainValue = mainStatValue
|
||||
} else {
|
||||
mainValue = mainStatValue * 100
|
||||
}
|
||||
|
||||
if mainValue == 0 || mainValue != mainValue { // NaN check
|
||||
mainValue = 0
|
||||
}
|
||||
|
||||
item["main"] = map[string]interface{}{
|
||||
"type": mainType,
|
||||
"value": mainValue,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ps *ParserService) convertSubStats(item map[string]interface{}) {
|
||||
op, opExists := item["op"].([]interface{})
|
||||
|
||||
if !opExists || len(op) <= 1 {
|
||||
item["substats"] = []interface{}{}
|
||||
return
|
||||
}
|
||||
|
||||
statAcc := make(map[string]map[string]interface{})
|
||||
|
||||
// 处理副属性 (从索引1开始)
|
||||
for i := 1; i < len(op); i++ {
|
||||
if opItem, ok := op[i].([]interface{}); ok && len(opItem) >= 2 {
|
||||
opType, _ := opItem[0].(string)
|
||||
opValue, _ := opItem[1].(float64)
|
||||
|
||||
annotation := ""
|
||||
if len(opItem) > 2 {
|
||||
annotation, _ = opItem[2].(string)
|
||||
}
|
||||
|
||||
statType := statByIngameStat[opType]
|
||||
var value float64
|
||||
|
||||
if ps.isFlat(opType) {
|
||||
value = opValue
|
||||
} else {
|
||||
value = opValue * 100
|
||||
}
|
||||
|
||||
if existingStat, exists := statAcc[statType]; exists {
|
||||
existingStat["value"] = existingStat["value"].(float64) + value
|
||||
|
||||
if annotation == "c" {
|
||||
existingStat["modified"] = true
|
||||
} else if annotation != "u" {
|
||||
rolls := existingStat["rolls"].(int) + 1
|
||||
existingStat["rolls"] = rolls
|
||||
existingStat["ingameRolls"] = rolls
|
||||
}
|
||||
} else {
|
||||
rolls := 1
|
||||
if annotation == "u" {
|
||||
rolls = 0
|
||||
}
|
||||
|
||||
statAcc[statType] = map[string]interface{}{
|
||||
"value": value,
|
||||
"rolls": rolls,
|
||||
"ingameRolls": rolls,
|
||||
"modified": annotation == "c",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 转换为最终格式
|
||||
var substats []interface{}
|
||||
for statType, statData := range statAcc {
|
||||
substat := map[string]interface{}{
|
||||
"type": statType,
|
||||
"value": statData["value"],
|
||||
"rolls": statData["rolls"],
|
||||
"ingameRolls": statData["ingameRolls"],
|
||||
"modified": statData["modified"],
|
||||
}
|
||||
substats = append(substats, substat)
|
||||
}
|
||||
|
||||
item["substats"] = substats
|
||||
}
|
||||
|
||||
func (ps *ParserService) convertId(item map[string]interface{}) {
|
||||
if id, exists := item["id"]; exists {
|
||||
item["ingameId"] = id
|
||||
}
|
||||
}
|
||||
|
||||
func (ps *ParserService) convertEquippedId(item map[string]interface{}) {
|
||||
if p, exists := item["p"]; exists {
|
||||
item["ingameEquippedId"] = fmt.Sprintf("%v", p)
|
||||
}
|
||||
}
|
||||
|
||||
func (ps *ParserService) isFlat(text string) bool {
|
||||
return text == "max_hp" || text == "speed" || text == "att" || text == "def"
|
||||
}
|
||||
|
||||
// 映射常量
|
||||
var (
|
||||
rankByIngameGrade = []string{
|
||||
"Unknown",
|
||||
"Normal",
|
||||
"Good",
|
||||
"Rare",
|
||||
"Heroic",
|
||||
"Epic",
|
||||
}
|
||||
|
||||
gearByIngameType = map[string]string{
|
||||
"weapon": "Weapon",
|
||||
"helm": "Helmet",
|
||||
"armor": "Armor",
|
||||
"neck": "Necklace",
|
||||
"ring": "Ring",
|
||||
"boot": "Boots",
|
||||
}
|
||||
|
||||
gearByGearLetter = map[string]string{
|
||||
"w": "Weapon",
|
||||
"h": "Helmet",
|
||||
"a": "Armor",
|
||||
"n": "Necklace",
|
||||
"r": "Ring",
|
||||
"b": "Boots",
|
||||
}
|
||||
|
||||
setsByIngameSet = map[string]string{
|
||||
"set_acc": "HitSet",
|
||||
"set_att": "AttackSet",
|
||||
"set_coop": "UnitySet",
|
||||
"set_counter": "CounterSet",
|
||||
"set_cri_dmg": "DestructionSet",
|
||||
"set_cri": "CriticalSet",
|
||||
"set_def": "DefenseSet",
|
||||
"set_immune": "ImmunitySet",
|
||||
"set_max_hp": "HealthSet",
|
||||
"set_penetrate": "PenetrationSet",
|
||||
"set_rage": "RageSet",
|
||||
"set_res": "ResistSet",
|
||||
"set_revenge": "RevengeSet",
|
||||
"set_scar": "InjurySet",
|
||||
"set_speed": "SpeedSet",
|
||||
"set_vampire": "LifestealSet",
|
||||
"set_shield": "ProtectionSet",
|
||||
"set_torrent": "TorrentSet",
|
||||
}
|
||||
|
||||
statByIngameStat = map[string]string{
|
||||
"att_rate": "AttackPercent",
|
||||
"max_hp_rate": "HealthPercent",
|
||||
"def_rate": "DefensePercent",
|
||||
"att": "Attack",
|
||||
"max_hp": "Health",
|
||||
"def": "Defense",
|
||||
"speed": "Speed",
|
||||
"res": "EffectResistancePercent",
|
||||
"cri": "CriticalHitChancePercent",
|
||||
"cri_dmg": "CriticalHitDamagePercent",
|
||||
"acc": "EffectivenessPercent",
|
||||
"coop": "DualAttackChancePercent",
|
||||
}
|
||||
)
|
||||
|
||||
// 新增:不做enhance过滤的convertItems
|
||||
func (ps *ParserService) convertItemsAllWithLog(rawItems []interface{}) []map[string]interface{} {
|
||||
var convertedItems []map[string]interface{}
|
||||
for _, rawItem := range rawItems {
|
||||
if itemMap, ok := rawItem.(map[string]interface{}); ok {
|
||||
convertedItem := ps.convertSingleItem(itemMap)
|
||||
if convertedItem != nil {
|
||||
convertedItems = append(convertedItems, convertedItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
return convertedItems
|
||||
}
|
||||
85
internal/service/parser_service_test.go
Normal file
85
internal/service/parser_service_test.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"equipment-analyzer/internal/config"
|
||||
"equipment-analyzer/internal/utils"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestReadRawJsonFile(t *testing.T) {
|
||||
// 创建测试用的配置和日志器
|
||||
config := &config.Config{}
|
||||
logger := utils.NewLogger()
|
||||
|
||||
// 创建ParserService实例
|
||||
ps := NewParserService(config, logger)
|
||||
|
||||
// 调用ReadRawJsonFile方法
|
||||
result, err := ps.ReadRawJsonFile()
|
||||
if err != nil {
|
||||
t.Fatalf("ReadRawJsonFile failed: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Raw result length: %d\n", len(result))
|
||||
fmt.Printf("Raw result preview: %s\n", result[:min(200, len(result))])
|
||||
|
||||
// 解析JSON结果
|
||||
var parsedData map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(result), &parsedData); err != nil {
|
||||
t.Fatalf("Failed to parse JSON result: %v", err)
|
||||
}
|
||||
|
||||
// 检查数据结构
|
||||
fmt.Printf("Parsed data keys: %v\n", getKeys(parsedData))
|
||||
|
||||
// 检查items字段
|
||||
if items, exists := parsedData["items"]; exists {
|
||||
if itemsArray, ok := items.([]interface{}); ok {
|
||||
fmt.Printf("Items count: %d\n", len(itemsArray))
|
||||
if len(itemsArray) > 0 {
|
||||
fmt.Printf("First item: %+v\n", itemsArray[0])
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("Items is not an array: %T\n", items)
|
||||
}
|
||||
} else {
|
||||
fmt.Println("Items field not found")
|
||||
}
|
||||
|
||||
// 检查heroes字段
|
||||
if heroes, exists := parsedData["heroes"]; exists {
|
||||
if heroesArray, ok := heroes.([]interface{}); ok {
|
||||
fmt.Printf("Heroes count: %d\n", len(heroesArray))
|
||||
if len(heroesArray) > 0 {
|
||||
fmt.Printf("First hero: %+v\n", heroesArray[0])
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("Heroes is not an array: %T\n", heroes)
|
||||
}
|
||||
} else {
|
||||
fmt.Println("Heroes field not found")
|
||||
}
|
||||
|
||||
// 如果没有数据,输出更多调试信息
|
||||
if len(result) < 100 {
|
||||
fmt.Printf("Result seems empty or very short: %q\n", result)
|
||||
}
|
||||
}
|
||||
|
||||
// 辅助函数
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func getKeys(m map[string]interface{}) []string {
|
||||
keys := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
34
internal/utils/file.go
Normal file
34
internal/utils/file.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// WriteFile 写入文件
|
||||
func WriteFile(filename string, data []byte) error {
|
||||
// 确保目录存在
|
||||
dir := filepath.Dir(filename)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 写入文件
|
||||
return os.WriteFile(filename, data, 0644)
|
||||
}
|
||||
|
||||
// ReadFile 读取文件
|
||||
func ReadFile(filename string) ([]byte, error) {
|
||||
return os.ReadFile(filename)
|
||||
}
|
||||
|
||||
// FileExists 检查文件是否存在
|
||||
func FileExists(filename string) bool {
|
||||
_, err := os.Stat(filename)
|
||||
return !os.IsNotExist(err)
|
||||
}
|
||||
|
||||
// CreateDir 创建目录
|
||||
func CreateDir(dir string) error {
|
||||
return os.MkdirAll(dir, 0755)
|
||||
}
|
||||
41
internal/utils/logger.go
Normal file
41
internal/utils/logger.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
"gopkg.in/natefinch/lumberjack.v2"
|
||||
)
|
||||
|
||||
type Logger struct {
|
||||
*zap.SugaredLogger
|
||||
}
|
||||
|
||||
func NewLogger() *Logger {
|
||||
// 配置日志轮转
|
||||
lumberJackLogger := &lumberjack.Logger{
|
||||
Filename: "logs/equipment-analyzer.log",
|
||||
MaxSize: 100, // MB
|
||||
MaxBackups: 3,
|
||||
MaxAge: 28, // days
|
||||
Compress: true,
|
||||
}
|
||||
|
||||
// 配置编码器
|
||||
encoderConfig := zap.NewProductionEncoderConfig()
|
||||
encoderConfig.TimeKey = "timestamp"
|
||||
encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
|
||||
encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
|
||||
|
||||
// 创建核心
|
||||
core := zapcore.NewCore(
|
||||
zapcore.NewJSONEncoder(encoderConfig),
|
||||
zapcore.AddSync(lumberJackLogger),
|
||||
zap.NewAtomicLevelAt(zap.InfoLevel),
|
||||
)
|
||||
|
||||
// 创建logger
|
||||
logger := zap.New(core, zap.AddCaller(), zap.AddStacktrace(zapcore.ErrorLevel))
|
||||
sugar := logger.Sugar()
|
||||
|
||||
return &Logger{sugar}
|
||||
}
|
||||
64
main.go
Normal file
64
main.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"github.com/wailsapp/wails/v2/pkg/logger"
|
||||
"log"
|
||||
|
||||
"equipment-analyzer/internal/config"
|
||||
"equipment-analyzer/internal/service"
|
||||
"equipment-analyzer/internal/utils"
|
||||
|
||||
"github.com/wailsapp/wails/v2"
|
||||
"github.com/wailsapp/wails/v2/pkg/options"
|
||||
)
|
||||
|
||||
//go:embed frontend/dist
|
||||
var assets embed.FS
|
||||
|
||||
func main() {
|
||||
// 初始化日志
|
||||
newLogger := utils.NewLogger()
|
||||
defer newLogger.Sync()
|
||||
|
||||
// 加载配置
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
log.Fatal("Failed to load config:", err)
|
||||
}
|
||||
|
||||
// 创建应用服务
|
||||
app := service.NewApp(cfg, newLogger)
|
||||
|
||||
// 设置应用选项
|
||||
appOptions := &options.App{
|
||||
|
||||
Title: "epic-luna",
|
||||
Width: 1024,
|
||||
Height: 768,
|
||||
//Hidden: false,
|
||||
BackgroundColour: &options.RGBA{R: 255, G: 255, B: 255, A: 1},
|
||||
Assets: assets,
|
||||
Menu: nil,
|
||||
Logger: nil,
|
||||
LogLevel: logger.DEBUG,
|
||||
LogLevelProduction: logger.ERROR,
|
||||
OnStartup: app.Startup,
|
||||
OnDomReady: app.DomReady,
|
||||
OnBeforeClose: app.BeforeClose,
|
||||
OnShutdown: app.Shutdown,
|
||||
Debug: options.Debug{
|
||||
OpenInspectorOnStartup: true,
|
||||
},
|
||||
//EnableDefaultContext: true,
|
||||
Bind: []interface{}{
|
||||
app,
|
||||
},
|
||||
}
|
||||
|
||||
// 启动应用
|
||||
err = wails.Run(appOptions)
|
||||
if err != nil {
|
||||
log.Fatal("Failed to start application:", err)
|
||||
}
|
||||
}
|
||||
126588
opepic.json
Normal file
126588
opepic.json
Normal file
File diff suppressed because it is too large
Load Diff
95152
output_raw.json
Normal file
95152
output_raw.json
Normal file
File diff suppressed because it is too large
Load Diff
134423
output_raw_e7.json
Normal file
134423
output_raw_e7.json
Normal file
File diff suppressed because it is too large
Load Diff
6
package-lock.json
generated
Normal file
6
package-lock.json
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "equipment-analyzer",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
1
package.json
Normal file
1
package.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
19
wails.json
Normal file
19
wails.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "equipment-analyzer",
|
||||
"outputfilename": "equipment-analyzer",
|
||||
"frontend:install": "npm install",
|
||||
"frontend:build": "npm run build",
|
||||
"frontend:dev:watcher": "npm run dev",
|
||||
"frontend:dev:serverUrl": "auto",
|
||||
"author": {
|
||||
"name": "Equipment Analyzer Team",
|
||||
"email": "team@equipment-analyzer.com"
|
||||
},
|
||||
"info": {
|
||||
"companyName": "Equipment Analyzer",
|
||||
"productName": "Equipment Data Export Tool",
|
||||
"productVersion": "1.0.0",
|
||||
"copyright": "Copyright........",
|
||||
"comments": "Built using Wails (https://wails.io)"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user