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