This commit is contained in:
hu xiaotong
2025-07-02 16:12:52 +08:00
commit 0246bc7060
48 changed files with 460639 additions and 0 deletions

54
.gitignore vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

31
frontend/package.json Normal file
View 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"
}
}

View File

@@ -0,0 +1 @@
ab1f461d7da532ffc0ddf7b22c308322

296
frontend/src/App.css Normal file
View 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
View 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
View 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
View 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>,
)

View 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

View 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
View 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
View 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" }]
}

View 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
View 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,
},
})

View 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
View 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>;

View 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']();
}

View 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
View 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

View 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
View 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
View 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
View 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()
}

View 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)
}

View 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
View 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)
}

View 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
View 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"`
}

View 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)
}
}

View 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)
}
}
}

View 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()
}

File diff suppressed because it is too large Load Diff

View 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
}

View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

95152
output_raw.json Normal file

File diff suppressed because it is too large Load Diff

134423
output_raw_e7.json Normal file

File diff suppressed because it is too large Load Diff

6
package-lock.json generated Normal file
View File

@@ -0,0 +1,6 @@
{
"name": "equipment-analyzer",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

1
package.json Normal file
View File

@@ -0,0 +1 @@
{}

19
wails.json Normal file
View 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)"
}
}