docs: add AI Skill integration guide (#225)

## Summary
- Add AI Skill documentation for LLM integration with FLVX panel
- Include complete API reference documentation for the skill
- Add publish workflow for skill distribution

## Changes
- New `doc/ai-skill.md` guide
- New `skills/flvx-api/` directory with API references
- New `.github/workflows/publish-skill.yml` workflow
- Update `doc/index.md` navigation
This commit is contained in:
sagit
2026-02-28 14:13:34 +08:00
committed by GitHub
19 changed files with 4131 additions and 0 deletions
+48
View File
@@ -0,0 +1,48 @@
name: Publish Skill to npm
on:
push:
tags:
- 'v*'
workflow_dispatch:
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'
- name: Get version from tag
id: version
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
VERSION=$(node -p "require('./skills/flvx-api/package.json').version")
else
VERSION="${GITHUB_REF#refs/tags/v}"
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Publishing skill version: $VERSION"
- name: Publish to npm
working-directory: skills/flvx-api
run: npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Create GitHub Release
if: github.event_name == 'push'
uses: softprops/action-gh-release@v1
with:
name: Skill v${{ steps.version.outputs.version }}
generate_release_notes: true
files: skills/flvx-api/package.json
+220
View File
@@ -0,0 +1,220 @@
# AI Skill 使用指南
让大模型直接操作 FLVX 面板的技能包。支持 OpenCode、OpenClaw、Claude Code 等工具。
## 安装
### 方式 1: npm (推荐)
```bash
npm install -g @flvx/skill-api
```
postinstall 脚本会自动链接到 `~/.agents/skills/flvx-api/`。
### 方式 2: 手动链接
```bash
# 从 FLVX 源码
cd /path/to/flvx
mkdir -p ~/.agents/skills
ln -sf $(pwd)/skills/flvx-api ~/.agents/skills/
# 或从 GitHub
git clone https://github.com/Sagit-chu/flvx.git
cd flvx
ln -sf $(pwd)/skills/flvx-api ~/.agents/skills/
```
## 配置
设置环境变量:
```bash
export FLVX_BASE_URL="https://your-panel.example.com"
export FLVX_USERNAME="admin"
export FLVX_PASSWORD="your-password"
```
或使用凭证文件:
```bash
mkdir -p ~/.flvx
cat > ~/.flvx/.env << 'EOF'
export FLVX_BASE_URL="https://panel.example.com"
export FLVX_USERNAME="admin"
export FLVX_PASSWORD="your-password"
EOF
chmod 600 ~/.flvx/.env
source ~/.flvx/.env
```
---
## 工具接入方法
### OpenCode
OpenCode 是命令行 AI 编程助手,支持通过 skills 扩展能力。
**安装 skill:**
```bash
npm install -g @flvx/skill-api
```
**使用:**
```bash
export FLVX_BASE_URL="https://panel.example.com"
export FLVX_USERNAME="admin"
export FLVX_PASSWORD="your-password"
opencode
```
**示例对话:**
```
你: 查看我的转发列表
你: 创建一个转发到 192.168.1.100:80 使用隧道 1
你: 检查节点状态
你: 查看流量使用情况
```
---
### OpenClaw
OpenClaw 同样支持 skills 机制。
**安装 skill:**
```bash
npm install -g @flvx/skill-api
# 或手动链接
mkdir -p ~/.openclaw/skills
ln -sf /path/to/flvx/skills/flvx-api ~/.openclaw/skills/flvx-api
```
**使用:**
```bash
openclaw
>>> 查看所有节点状态
>>> 给用户 alice 分配 50GB 流量
>>> 导出系统备份
```
---
### Claude Code
Claude Code 是 Anthropic 官方的命令行工具,支持通过 CLAUDE.md 扩展。
#### 方式 1: 项目级 CLAUDE.md
在项目根目录创建 `CLAUDE.md`:
```markdown
# FLVX API Skill
你可以通过 REST API 操作 FLVX 面板。
## 环境变量
- FLVX_BASE_URL: 面板地址
- FLVX_USERNAME: 用户名
- FLVX_PASSWORD: 密码
## 认证规则
- Authorization 头使用原始 JWT token,不加 "Bearer " 前缀
- 所有 API 使用 POST 方法
## 常用 API
### 登录获取 token
POST /api/v1/user/login
{"username": "...", "password": "..."}
### 查看转发列表
POST /api/v1/forward/list
Authorization: <token>
{}
### 创建转发
POST /api/v1/forward/create
{"name": "xxx", "tunnelId": 1, "remoteAddr": "1.2.3.4:80"}
### 查看节点
POST /api/v1/node/list
{}
```
**使用:**
```bash
cd /path/to/your/project
claude
```
#### 方式 2: 全局 CLAUDE.md
```bash
mkdir -p ~/.claude
cat > ~/.claude/CLAUDE.md << 'EOF'
# FLVX Panel Operations
使用 FLVX REST API 操作流量转发面板。
环境变量: FLVX_BASE_URL, FLVX_USERNAME, FLVX_PASSWORD
调用方式: curl -X POST "$FLVX_BASE_URL/api/v1/..." -H "Authorization: $TOKEN"
注意: Authorization 不要加 Bearer 前缀
EOF
```
#### 方式 3: 复制 SKILL.md
```bash
cat ~/.agents/skills/flvx-api/SKILL.md >> ~/.claude/CLAUDE.md
```
**示例对话:**
```
>>> 帮我查看 FLVX 面板上有哪些节点
>>> 创建一个名为 test 的转发,目标地址 10.0.0.1:80
>>> 查看我的流量使用情况
```
---
## API 覆盖
| 模块 | 操作 |
|------|------|
| 认证 | 登录、Token 管理 |
| 用户 | 增删改查、流量重置、密码 |
| 节点 | 增删改查、安装、升级、状态 |
| 隧道 | 增删改查、用户分配 |
| 转发 | 增删改查、暂停/恢复、诊断 |
| 分组 | 用户/隧道分组、权限 |
| 限速 | 增删改查 |
| 联邦 | 节点共享、远程节点 |
| 备份 | 导出/导入 |
## 安全提示
- ⚠️ 环境变量在进程列表中可见
- 使用 `~/.flvx/.env` 文件并设置 `chmod 600`
- 添加 `export HISTIGNORE="*FLVX_PASSWORD*"` 防止密码进入历史记录
- Token 仅在会话内存中缓存,不写入磁盘
## 发布
维护者可通过以下方式发布新版本:
```bash
# 方式 1: 推送 tag
git tag skill-v2.1.6
git push --tags
# 方式 2: GitHub Actions 手动触发
# 在 Actions 页面运行 publish-skill workflow
```
需要在 GitHub 仓库设置 `NPM_TOKEN` secret。
+1
View File
@@ -18,6 +18,7 @@
- [安装部署](./install.md) - [安装部署](./install.md)
- [使用指南](./usage.md) - [使用指南](./usage.md)
- [PostgreSQL 数据库指南](./postgresql.md) - [PostgreSQL 数据库指南](./postgresql.md)
- [AI Skill 接入](./ai-skill.md) - 让大模型直接操作面板
- [常见问题](./faq.md) - [常见问题](./faq.md)
## 免责声明 ## 免责声明
+302
View File
@@ -0,0 +1,302 @@
---
name: flvx-api
description: Operate FLVX traffic forwarding management system via REST API. Supports user/node/tunnel/forward management, federation clustering, and traffic monitoring. Use when user wants to manage FLVX panel programmatically or via natural language.
metadata:
author: FLVX Team
version: "2.1.5"
requires_env:
- FLVX_BASE_URL
- FLVX_USERNAME
- FLVX_PASSWORD
---
# FLVX API Operations
Operate FLVX panel through REST API. All endpoints use POST method and return JSON with `{code, msg, data, ts}` envelope.
## Supported AI Tools
| Tool | Installation | Notes |
|------|--------------|-------|
| **OpenCode** | `npm i -g @flvx/skill-api` or `ln -s . ~/.agents/skills/flvx-api` | Auto-loads from `~/.agents/skills/` |
| **OpenClaw** | Same as OpenCode | Compatible skill format |
| **Claude Code** | Copy SKILL.md to CLAUDE.md or `~/.claude/CLAUDE.md` | Uses context file instead of skills |
## Prerequisites
Set environment variables before starting:
```bash
export FLVX_BASE_URL="https://your-panel.example.com"
export FLVX_USERNAME="admin"
export FLVX_PASSWORD="your-password"
```
**Security tip:** Add to `~/.flvx/.env` and source on demand:
```bash
mkdir -p ~/.flvx && cat > ~/.flvx/.env << 'EOF'
export FLVX_BASE_URL="https://panel.example.com"
export FLVX_USERNAME="admin"
export FLVX_PASSWORD="your-password"
EOF
chmod 600 ~/.flvx/.env
source ~/.flvx/.env
```
## Authentication Flow
### Session Token Cache
- Token is cached **only for the current conversation**
- New conversation = fresh login required
- Token is NOT written to disk (security)
### Auto-Login Pattern
```
Before ANY API call:
1. Check if TOKEN is cached in current session
├─ Yes → Use cached token, proceed
└─ No →
1. Read FLVX_USERNAME and FLVX_PASSWORD from environment
2. POST /api/v1/user/login with credentials
3. Cache response.data.token in session memory
4. Proceed with original request
```
### Login Request
```bash
curl -X POST "${FLVX_BASE_URL}/api/v1/user/login" \
-H "Content-Type: application/json" \
-d "{\"username\":\"${FLVX_USERNAME}\",\"password\":\"${FLVX_PASSWORD}\"}"
```
**Response:**
```json
{
"code": 0,
"msg": "success",
"data": {
"token": "eyJhbGciOiJIUzI1NiIs...",
"name": "Administrator",
"role_id": 0,
"requirePasswordChange": false
},
"ts": 1706659200000
}
```
## Authentication Rules
| Header | Value | Critical |
|--------|-------|----------|
| `Authorization` | `<jwt_token>` | ⚠️ NO "Bearer" prefix! |
| `Content-Type` | `application/json` | All requests use JSON |
## Quick Start Workflow
```
User request → Check env vars → Auto-login if needed → Call API → Return result
```
## Intent → API Mapping
| User Intent | API Endpoint | Reference |
|-------------|--------------|-----------|
| "登录" / "查看我的信息" | `/api/v1/user/package` | [auth](references/auth.md) |
| "创建用户" / "添加用户" | `/api/v1/user/create` | [users](references/users.md) |
| "查看用户列表" / "所有用户" | `/api/v1/user/list` | [users](references/users.md) |
| "重置流量" | `/api/v1/user/reset` | [users](references/users.md) |
| "添加节点" / "新建节点" | `/api/v1/node/create` | [nodes](references/nodes.md) |
| "查看节点" / "节点状态" | `/api/v1/node/list` | [nodes](references/nodes.md) |
| "安装命令" / "部署节点" | `/api/v1/node/install` | [nodes](references/nodes.md) |
| "升级节点" | `/api/v1/node/upgrade` | [nodes](references/nodes.md) |
| "创建隧道" / "新建隧道" | `/api/v1/tunnel/create` | [tunnels](references/tunnels.md) |
| "分配隧道给用户" | `/api/v1/tunnel/user/assign` | [tunnels](references/tunnels.md) |
| "创建转发" / "新建转发" / "添加转发" | `/api/v1/forward/create` | [forwards](references/forwards.md) |
| "暂停转发" | `/api/v1/forward/pause` | [forwards](references/forwards.md) |
| "恢复转发" | `/api/v1/forward/resume` | [forwards](references/forwards.md) |
| "删除转发" | `/api/v1/forward/delete` | [forwards](references/forwards.md) |
| "查看我的转发" / "转发列表" | `/api/v1/forward/list` | [forwards](references/forwards.md) |
| "查看流量" / "流量统计" | `/api/v1/forward/list` or `/api/v1/user/package` | [forwards](references/forwards.md) |
| "诊断转发" / "测试连通性" | `/api/v1/forward/diagnose` | [forwards](references/forwards.md) |
| "创建限速规则" | `/api/v1/speed-limit/create` | [speed-limits](references/speed-limits.md) |
| "联邦共享" / "节点共享" | `/api/v1/federation/share/create` | [federation](references/federation.md) |
| "导出备份" | `/api/v1/backup/export` | [backup](references/backup.md) |
| "导入备份" | `/api/v1/backup/import` | [backup](references/backup.md) |
## HTTP Request Template
### Bash/curl (with auto-login)
```bash
#!/bin/bash
BASE_URL="${FLVX_BASE_URL}"
USERNAME="${FLVX_USERNAME}"
PASSWORD="${FLVX_PASSWORD}"
# Login and get token
TOKEN=$(curl -s -X POST "${BASE_URL}/api/v1/user/login" \
-H "Content-Type: application/json" \
-d "{\"username\":\"${USERNAME}\",\"password\":\"${PASSWORD}\"}" | jq -r '.data.token')
if [ "$TOKEN" == "null" ] || [ -z "$TOKEN" ]; then
echo "Login failed"
exit 1
fi
# Use token for API calls - NOTE: NO "Bearer" prefix!
curl -s -X POST "${BASE_URL}/api/v1/node/list" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{}' | jq '.'
```
### Python (requests)
```python
import os
import requests
BASE_URL = os.environ.get("FLVX_BASE_URL")
USERNAME = os.environ.get("FLVX_USERNAME")
PASSWORD = os.environ.get("FLVX_PASSWORD")
# Login
resp = requests.post(f"{BASE_URL}/api/v1/user/login",
headers={"Content-Type": "application/json"},
json={"username": USERNAME, "password": PASSWORD})
result = resp.json()
if result["code"] != 0:
raise Exception(f"Login failed: {result['msg']}")
TOKEN = result["data"]["token"]
# Authenticated request - NO "Bearer" prefix!
headers = {
"Content-Type": "application/json",
"Authorization": TOKEN
}
resp = requests.post(f"{BASE_URL}/api/v1/node/list", headers=headers, json={})
print(resp.json())
```
### Node.js (fetch)
```javascript
const BASE_URL = process.env.FLVX_BASE_URL;
const USERNAME = process.env.FLVX_USERNAME;
const PASSWORD = process.env.FLVX_PASSWORD;
// Login
const loginRes = await fetch(`${BASE_URL}/api/v1/user/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: USERNAME, password: PASSWORD })
});
const loginData = await loginRes.json();
if (loginData.code !== 0) throw new Error(loginData.msg);
const TOKEN = loginData.data.token;
// Authenticated request - NO "Bearer" prefix!
const res = await fetch(`${BASE_URL}/api/v1/node/list`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': TOKEN
},
body: JSON.stringify({})
});
console.log(await res.json());
```
## Response Handling
**Success:**
```json
{"code": 0, "msg": "success", "data": {...}, "ts": 1706659200000}
```
**Error:**
```json
{"code": -1, "msg": "用户名或密码错误", "ts": 1706659200000}
```
**Pattern:**
```
1. Parse JSON response
2. If code === 0 → return data
3. If code === 401 → token expired, re-login and retry
4. If code === 403 → permission denied, need admin
5. Else → show msg to user as error message
```
## Permission Model
| role_id | Type | Access |
|---------|------|--------|
| 0 | Admin | All endpoints |
| 1 | Regular | Forward CRUD, own profile, assigned tunnels only |
Non-admin users can only see/modify their own resources.
## Module Reference
| Module | Endpoints | Reference |
|--------|-----------|-----------|
| Auth | login, captcha | [auth.md](references/auth.md) |
| Users | CRUD, reset, password | [users.md](references/users.md) |
| Nodes | CRUD, install, upgrade, status | [nodes.md](references/nodes.md) |
| Tunnels | CRUD, user assignment | [tunnels.md](references/tunnels.md) |
| Forwards | CRUD, pause/resume, diagnose | [forwards.md](references/forwards.md) |
| Groups | User/tunnel groups, permissions | [groups.md](references/groups.md) |
| Speed Limits | CRUD | [speed-limits.md](references/speed-limits.md) |
| Federation | Share, remote nodes | [federation.md](references/federation.md) |
| Backup | Export/import | [backup.md](references/backup.md) |
| Config | System settings | [config.md](references/config.md) |
| Types | TypeScript interfaces | [types.md](references/types.md) |
| Errors | Error codes | [errors.md](references/errors.md) |
| Examples | Code samples | [examples/](references/examples/) |
## Critical Rules
1. ⚠️ **NO "Bearer" prefix** - `Authorization: <token>`, NOT `Authorization: Bearer <token>`
2. **All endpoints use POST** - Including list/get operations
3. **code === 0 means success** - Any other value is an error
4. **Traffic units**: User.flow is GB, in_flow/out_flow are bytes
5. **Timestamps**: All timestamps are milliseconds since epoch
6. **Token is session-scoped**: Cache in memory only, not on disk
## Common Workflows
### Workflow 1: New User Onboarding (Admin)
```
1. POST /api/v1/user/create → Create user with traffic quota
2. POST /api/v1/tunnel/user/assign → Assign tunnels to user
3. Tell user their username/password
4. User logs in and creates forwards
```
### Workflow 2: Add New Node (Admin)
```
1. POST /api/v1/node/create → Register node in panel
2. POST /api/v1/node/install → Get install command
3. Run install command on target server
4. POST /api/v1/node/check-status → Verify node is online
```
### Workflow 3: Create Forward (Any User)
```
1. POST /api/v1/tunnel/user/tunnel → List available tunnels
2. POST /api/v1/forward/create → Create forward on chosen tunnel
3. POST /api/v1/forward/diagnose → Verify connectivity
```
### Workflow 4: Node Maintenance (Admin)
```
1. POST /api/v1/node/list → Check node statuses
2. POST /api/v1/node/releases → Check available versions
3. POST /api/v1/node/upgrade or /batch-upgrade → Upgrade nodes
4. POST /api/v1/node/rollback → Rollback if needed
```
+44
View File
@@ -0,0 +1,44 @@
{
"name": "@flvx/skill-api",
"version": "2.1.5",
"description": "Skill for AI assistants to operate FLVX panel via REST API. Supports OpenCode, OpenClaw, Claude Code.",
"keywords": [
"opencode",
"openclaw",
"claude-code",
"skill",
"flvx",
"api",
"traffic-forwarding",
"gost"
],
"license": "MIT",
"author": "FLVX Team",
"files": [
"SKILL.md",
"references/**/*"
],
"repository": {
"type": "git",
"url": "git+https://github.com/Sagit-chu/flvx.git",
"directory": "skills/flvx-api"
},
"bugs": {
"url": "https://github.com/Sagit-chu/flvx/issues"
},
"homepage": "https://github.com/Sagit-chu/flvx/tree/main/skills/flvx-api#readme",
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org"
},
"opencode": {
"skill": true,
"installTo": "~/.agents/skills/flvx-api"
},
"scripts": {
"postinstall": "node -e \"const fs=require('fs');const path=require('path');const target=path.join(process.env.HOME,'.agents','skills','flvx-api');const src=process.cwd();try{fs.mkdirSync(path.dirname(target),{recursive:true});if(fs.existsSync(target)||fs.lstatSync(target).isSymbolicLink()){fs.unlinkSync(target)}fs.symlinkSync(src,target);console.log('✓ Installed to',target)}catch(e){console.error('Manual install: ln -s',src,target)}\"",
"preuninstall": "node -e \"const target=require('path').join(process.env.HOME,'.agents','skills','flvx-api');try{require('fs').unlinkSync(target);console.log('✓ Removed',target)}catch(e){}\"",
"link": "node -e \"const fs=require('fs');const path=require('path');const target=path.join(process.env.HOME,'.agents','skills','flvx-api');const src=process.cwd();try{fs.mkdirSync(path.dirname(target),{recursive:true});if(fs.existsSync(target)||fs.lstatSync(target).isSymbolicLink()){fs.unlinkSync(target)}fs.symlinkSync(src,target);console.log('✓ Linked to',target)}catch(e){console.error(e)}\"",
"unlink": "node -e \"const target=require('path').join(process.env.HOME,'.agents','skills','flvx-api');try{require('fs').unlinkSync(target);console.log('✓ Unlinked',target)}catch(e){}\""
}
}
+151
View File
@@ -0,0 +1,151 @@
# Authentication API
## POST /api/v1/user/login
Authenticate and obtain JWT token.
**Request:**
```json
{
"username": "admin",
"password": "secret",
"captchaId": "optional-captcha-id"
}
```
**Response:**
```json
{
"code": 0,
"msg": "success",
"data": {
"token": "eyJhbGciOiJIUzI1NiIs...",
"name": "Administrator",
"role_id": 0,
"requirePasswordChange": false
},
"ts": 1706659200000
}
```
**Response Fields:**
| Field | Type | Description |
|-------|------|-------------|
| token | string | JWT token for subsequent requests |
| name | string | User's display name |
| role_id | number | 0 = admin, 1 = regular user |
| requirePasswordChange | boolean | Whether password change is required |
## JWT Token Details
**Algorithm:** HMAC-SHA256
**Lifetime:** 90 days
**Token Claims:**
```json
{
"sub": "1",
"user": "admin",
"name": "Administrator",
"role_id": 0,
"iat": 1706659200,
"exp": 1738195200
}
```
## POST /api/v1/captcha/check
Check if captcha verification is required.
**Request:** `{}`
**Response:**
```json
{
"code": 0,
"data": {
"enabled": true,
"type": "turnstile"
}
}
```
## POST /api/v1/captcha/verify
Verify captcha response (Cloudflare Turnstile or local captcha).
**Request:**
```json
{
"captchaId": "captcha-session-id",
"captchaValue": "user-captcha-response"
}
```
## Token Usage
Include the token in all authenticated requests:
```bash
curl -X POST "${FLVX_BASE_URL}/api/v1/node/list" \
-H "Authorization: eyJhbGciOiJIUzI1NiIs..." \
-H "Content-Type: application/json" \
-d '{}'
```
⚠️ **CRITICAL: Do NOT add "Bearer " prefix!**
```
✅ Correct: Authorization: eyJhbGciOiJIUzI1NiIs...
❌ Incorrect: Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
```
## POST /api/v1/user/updatePassword
Change current user's password.
**Request:**
```json
{
"oldPassword": "current-password",
"newPassword": "new-password"
}
```
**Response:**
```json
{"code": 0, "msg": "success"}
```
## POST /api/v1/user/package
Get current user's package info (tunnels, forwards, traffic stats).
**Request:** `{}`
**Response:**
```json
{
"code": 0,
"data": {
"flow": 100,
"inFlow": 1073741824,
"outFlow": 2147483648,
"tunnels": 5,
"forwards": 10,
"expTime": 1735689600000
}
}
```
**Fields:**
| Field | Type | Description |
|-------|------|-------------|
| flow | number | Total traffic quota in GB |
| inFlow | number | Used upload in bytes |
| outFlow | number | Used download in bytes |
| tunnels | number | Number of assigned tunnels |
| forwards | number | Number of forwards created |
| expTime | number | Account expiry timestamp (ms) |
+143
View File
@@ -0,0 +1,143 @@
# Backup & Restore API
Export and import system data for backup, migration, or disaster recovery.
## POST /api/v1/backup/export
Export system data.
**Request:**
```json
{
"types": ["users", "nodes", "tunnels", "forwards", "speed_limits", "groups"]
}
```
If `types` is empty or omitted, exports all data.
**Available types:**
- `users` - User accounts
- `nodes` - Node configurations
- `tunnels` - Tunnel configurations
- `forwards` - Forward rules
- `speed_limits` - Speed limit rules
- `groups` - User/tunnel groups and permissions
- `configs` - System configurations
**Response:**
```json
{
"code": 0,
"data": {
"version": "2.1.5",
"exportedAt": 1706659200000,
"types": ["users", "nodes", "tunnels"],
"users": [...],
"nodes": [...],
"tunnels": [...],
"forwards": [...],
"speedLimits": [...],
"tunnelGroups": [...],
"userGroups": [...],
"groupPermissions": [...],
"configs": {...}
}
}
```
## POST /api/v1/backup/import
Import system data from a backup.
**Request:**
```json
{
"version": "2.1.5",
"exportedAt": 1706659200000,
"types": ["users", "nodes"],
"users": [...],
"nodes": [...]
}
```
**Import Behavior:**
- Existing records are updated if IDs match
- New records are created for non-existent IDs
- Related entities must be included (e.g., forwards require tunnels)
**Response:**
```json
{
"code": 0,
"msg": "success",
"data": {
"imported": {
"users": 5,
"nodes": 3,
"tunnels": 10
},
"skipped": {
"forwards": 2
}
}
}
```
## POST /api/v1/backup/restore
Alias for `/api/v1/backup/import`.
---
## Workflow: Full System Backup
```bash
# Export all data
curl -s -X POST "${FLVX_BASE_URL}/api/v1/backup/export" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{}' > backup-$(date +%Y%m%d).json
echo "Backup saved to backup-$(date +%Y%m%d).json"
```
## Workflow: Partial Export
```bash
# Export only users and tunnels
curl -s -X POST "${FLVX_BASE_URL}/api/v1/backup/export" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{"types":["users","tunnels"]}' > partial-backup.json
```
## Workflow: Restore from Backup
```bash
# Import from backup file
curl -s -X POST "${FLVX_BASE_URL}/api/v1/backup/import" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
-d @backup-20260226.json | jq '.'
```
## Workflow: Migrate to New Panel
```bash
# On source panel
curl -s -X POST "${SOURCE_URL}/api/v1/backup/export" \
-H "Authorization: ${SOURCE_TOKEN}" \
-H "Content-Type: application/json" \
-d '{}' > migration.json
# On target panel
curl -s -X POST "${TARGET_URL}/api/v1/backup/import" \
-H "Authorization: ${TARGET_TOKEN}" \
-H "Content-Type: application/json" \
-d @migration.json
```
**Note:** After migration, you may need to:
1. Reinstall node agents with new panel URL
2. Update node secrets if they differ
3. Reassign federation tokens
+149
View File
@@ -0,0 +1,149 @@
# System Configuration API
Manage system-wide settings and configurations.
## POST /api/v1/config/get
Get a single configuration by name. This endpoint is public (no auth required).
**Request:**
```json
{"name": "site_name"}
```
**Response:**
```json
{
"code": 0,
"data": {
"name": "site_name",
"value": "My FLVX Panel",
"time": 1706659200000
}
}
```
## POST /api/v1/config/list
List all configurations (requires authentication).
**Request:** `{}`
**Response:**
```json
{
"code": 0,
"data": {
"site_name": "My FLVX Panel",
"site_logo": "https://example.com/logo.png",
"site_announcement": "System maintenance scheduled",
"captcha_enabled": "true",
"captcha_type": "turnstile",
"turnstile_site_key": "...",
"default_user_flow": "100",
"default_user_exp_days": "30"
}
}
```
## POST /api/v1/config/update
Batch update multiple configurations (admin only).
**Request:**
```json
{
"site_name": "New Panel Name",
"site_announcement": "Welcome to the new panel!",
"default_user_flow": "50"
}
```
Only include the keys you want to update.
**Response:**
```json
{"code": 0, "msg": "success"}
```
## POST /api/v1/config/update-single
Update a single configuration (admin only).
**Request:**
```json
{
"name": "site_name",
"value": "My Awesome Panel"
}
```
## POST /api/v1/announcement/get
Get the site announcement (public endpoint).
**Method:** GET
**Response:**
```json
{
"code": 0,
"data": {
"content": "System maintenance scheduled for tonight"
}
}
```
## POST /api/v1/announcement/update
Update the site announcement (admin only).
**Request:**
```json
{"content": "New announcement message"}
```
---
## Common Configuration Keys
| Key | Description | Example |
|-----|-------------|---------|
| `site_name` | Panel display name | `"My FLVX Panel"` |
| `site_logo` | Logo URL | `"https://example.com/logo.png"` |
| `site_announcement` | Announcement HTML | `"<p>Notice...</p>"` |
| `captcha_enabled` | Enable captcha | `"true"` or `"false"` |
| `captcha_type` | Captcha provider | `"turnstile"` or `"local"` |
| `turnstile_site_key` | Cloudflare Turnstile site key | `"0x4..."` |
| `turnstile_secret_key` | Cloudflare Turnstile secret | `"0x4..."` |
| `default_user_flow` | Default user traffic (GB) | `"100"` |
| `default_user_exp_days` | Default user expiry days | `"30"` |
| `default_user_num` | Default max forwards | `"10"` |
---
## Example: Update Panel Name and Announcement
```bash
curl -s -X POST "${FLVX_BASE_URL}/api/v1/config/update" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"site_name": "Awesome Traffic Panel",
"site_announcement": "<strong>Welcome!</strong> New nodes added."
}'
```
## Example: Enable Cloudflare Turnstile Captcha
```bash
curl -s -X POST "${FLVX_BASE_URL}/api/v1/config/update" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"captcha_enabled": "true",
"captcha_type": "turnstile",
"turnstile_site_key": "0x4AAAAAAAAjq0JN9YQg",
"turnstile_secret_key": "0x4AAAAAAAAjq0JN9YQg_secret"
}'
```
+168
View File
@@ -0,0 +1,168 @@
# Error Codes & Handling
## Response Code Field
| code | Meaning | Action |
|------|---------|--------|
| `0` | Success | Use `data` field |
| `-1` | Business error | Show `msg` to user |
| `-2` | Server/DB error | Retry or report bug |
| `401` | Unauthorized | Token expired/invalid, re-login |
| `403` | Forbidden | Need admin privileges |
## Common Error Messages (Chinese)
| msg | Cause | Solution |
|-----|-------|----------|
| 用户名或密码错误 | Wrong credentials | Check username/password |
| Token已过期 | Token expired | Re-login |
| 权限不足 | Need admin | Use admin account (role_id: 0) |
| 端口已被占用 | Port in use | Choose different port or delete conflicting forward |
| 流量不足 | Out of traffic | Contact admin or upgrade plan |
| 节点离线 | Node offline | Check node status, run install command |
| 隧道不可用 | Tunnel disabled | Enable tunnel first |
| 用户已存在 | Username taken | Choose different username |
| 参数错误 | Invalid request | Check request body format |
| 转发数量已达上限 | Forward limit reached | Delete unused forwards or contact admin |
| 该隧道未分配给当前用户 | No tunnel access | Contact admin to get tunnel assigned |
## Error Handling Pattern
### JavaScript/TypeScript
```typescript
async function callApi<T>(endpoint: string, data: object): Promise<T> {
const res = await fetch(`${BASE_URL}${endpoint}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": TOKEN,
},
body: JSON.stringify(data),
});
const result = await res.json();
if (result.code === 0) {
return result.data;
}
switch (result.code) {
case 401:
// Token expired - clear and retry
TOKEN = null;
throw new Error("登录已过期,请重新登录");
case 403:
throw new Error("权限不足,需要管理员权限");
case -2:
throw new Error("服务器错误,请稍后重试");
default:
throw new Error(result.msg || "操作失败");
}
}
```
### Python
```python
def call_api(endpoint: str, data: dict = None) -> dict:
global TOKEN
headers = {"Content-Type": "application/json"}
if TOKEN:
headers["Authorization"] = TOKEN
resp = requests.post(f"{BASE_URL}{endpoint}", headers=headers, json=data or {})
result = resp.json()
if result["code"] == 0:
return result.get("data")
if result["code"] == 401:
TOKEN = None
raise Exception("登录已过期,请重新登录")
elif result["code"] == 403:
raise Exception("权限不足,需要管理员权限")
elif result["code"] == -2:
raise Exception("服务器错误,请稍后重试")
else:
raise Exception(result["msg"] or "操作失败")
```
### Bash
```bash
call_api() {
local endpoint="$1"
local data="$2"
local response
response=$(curl -s -X POST "${FLVX_BASE_URL}${endpoint}" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
-d "$data")
local code
code=$(echo "$response" | jq -r '.code')
if [ "$code" == "0" ]; then
echo "$response" | jq '.data'
return 0
fi
local msg
msg=$(echo "$response" | jq -r '.msg')
case "$code" in
401) echo "Error: 登录已过期" >&2 ;;
403) echo "Error: 权限不足" >&2 ;;
-2) echo "Error: 服务器错误" >&2 ;;
*) echo "Error: $msg" >&2 ;;
esac
return 1
}
```
## Retry Logic with Auto Re-login
```typescript
async function callApiWithRetry<T>(
endpoint: string,
data: object,
maxRetries = 1
): Promise<T> {
let lastError: Error;
for (let i = 0; i <= maxRetries; i++) {
try {
if (!TOKEN) {
await login();
}
return await callApi<T>(endpoint, data);
} catch (error) {
lastError = error;
if (error.message.includes("过期") || error.message.includes("expired")) {
TOKEN = null; // Force re-login on next attempt
continue;
}
throw error;
}
}
throw lastError!;
}
```
## Validation Errors
When request validation fails, the API returns code -1 with specific messages:
| Scenario | Error Message |
|----------|--------------|
| Missing required field | `参数错误` or field-specific message |
| Invalid port range | `端口范围无效` |
| Invalid IP format | `IP地址格式错误` |
| Invalid date | `时间格式错误` |
| Username too short | `用户名长度不能少于3个字符` |
| Password too weak | `密码长度不能少于6个字符` |
@@ -0,0 +1,256 @@
# curl Examples
Quick reference for common operations using curl.
## Setup
```bash
# Set environment variables
export FLVX_BASE_URL="https://your-panel.example.com"
export FLVX_USERNAME="admin"
export FLVX_PASSWORD="your-password"
# Login and save token
TOKEN=$(curl -s -X POST "${FLVX_BASE_URL}/api/v1/user/login" \
-H "Content-Type: application/json" \
-d "{\"username\":\"${FLVX_USERNAME}\",\"password\":\"${FLVX_PASSWORD}\"}" \
| jq -r '.data.token')
echo "Token: ${TOKEN:0:20}..."
```
## User Operations
```bash
# Get my package info
curl -s -X POST "${FLVX_BASE_URL}/api/v1/user/package" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{}' | jq '.'
# List all users (admin)
curl -s -X POST "${FLVX_BASE_URL}/api/v1/user/list" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{"page":1,"pageSize":20}' | jq '.'
# Create user (admin)
curl -s -X POST "${FLVX_BASE_URL}/api/v1/user/create" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"user": "alice",
"pwd": "SecurePass123!",
"name": "Alice",
"flow": 50,
"num": 10,
"expTime": 1767225600000
}' | jq '.'
# Reset user traffic
curl -s -X POST "${FLVX_BASE_URL}/api/v1/user/reset" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{"id":2,"type":"user"}' | jq '.'
# Delete user
curl -s -X POST "${FLVX_BASE_URL}/api/v1/user/delete" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{"id":2}' | jq '.'
```
## Node Operations
```bash
# List nodes with status
curl -s -X POST "${FLVX_BASE_URL}/api/v1/node/list" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{}' | jq '.data.list[] | {name, status: (.status == 1), ip: .server_ip}'
# Create node
curl -s -X POST "${FLVX_BASE_URL}/api/v1/node/create" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{"name":"US-Node-1","serverIp":"203.0.113.10"}' | jq '.'
# Get install command
curl -s -X POST "${FLVX_BASE_URL}/api/v1/node/install" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{"id":2}' | jq -r '.data.command'
# Check node status
curl -s -X POST "${FLVX_BASE_URL}/api/v1/node/check-status" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{}' | jq '.'
# Upgrade node
curl -s -X POST "${FLVX_BASE_URL}/api/v1/node/upgrade" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{"id":2,"version":"2.1.5"}' | jq '.'
# Delete node
curl -s -X POST "${FLVX_BASE_URL}/api/v1/node/delete" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{"id":2}' | jq '.'
```
## Tunnel Operations
```bash
# List tunnels
curl -s -X POST "${FLVX_BASE_URL}/api/v1/tunnel/list" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{}' | jq '.data.list[] | {id, name, status}'
# Create tunnel
curl -s -X POST "${FLVX_BASE_URL}/api/v1/tunnel/create" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"name": "HK-US-Tunnel",
"type": 1,
"inNodeId": [1],
"outNodeId": [2]
}' | jq '.'
# Assign tunnel to user
curl -s -X POST "${FLVX_BASE_URL}/api/v1/tunnel/user/assign" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{"userId":2,"tunnelId":1,"flow":30}' | jq '.'
# Get available tunnels (for current user)
curl -s -X POST "${FLVX_BASE_URL}/api/v1/tunnel/user/tunnel" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{}' | jq '.'
```
## Forward Operations
```bash
# List forwards with traffic
curl -s -X POST "${FLVX_BASE_URL}/api/v1/forward/list" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{}' | jq '.data.list[] | {
name,
tunnel: .tunnel_name,
port: .in_port,
target: .remote_addr,
status: (if .status == 1 then "running" else "paused" end),
upload_gb: ((.in_flow / 1073741824) | floor),
download_gb: ((.out_flow / 1073741824) | floor)
}'
# Create forward
curl -s -X POST "${FLVX_BASE_URL}/api/v1/forward/create" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"name": "my-web-server",
"tunnelId": 1,
"remoteAddr": "192.168.1.100:80",
"strategy": "fifo"
}' | jq '.'
# Create forward with load balancing
curl -s -X POST "${FLVX_BASE_URL}/api/v1/forward/create" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"name": "web-cluster",
"tunnelId": 1,
"remoteAddr": "10.0.0.1:80,10.0.0.2:80,10.0.0.3:80",
"strategy": "round"
}' | jq '.'
# Pause forward
curl -s -X POST "${FLVX_BASE_URL}/api/v1/forward/pause" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{"id":1}' | jq '.'
# Resume forward
curl -s -X POST "${FLVX_BASE_URL}/api/v1/forward/resume" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{"id":1}' | jq '.'
# Diagnose forward
curl -s -X POST "${FLVX_BASE_URL}/api/v1/forward/diagnose" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{"id":1}' | jq '.'
# Delete forward
curl -s -X POST "${FLVX_BASE_URL}/api/v1/forward/delete" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{"id":1}' | jq '.'
# Batch pause forwards
curl -s -X POST "${FLVX_BASE_URL}/api/v1/forward/batch-pause" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{"ids":[1,2,3]}' | jq '.'
```
## Backup Operations
```bash
# Export all data
curl -s -X POST "${FLVX_BASE_URL}/api/v1/backup/export" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{}' > backup-$(date +%Y%m%d).json
# Export specific types
curl -s -X POST "${FLVX_BASE_URL}/api/v1/backup/export" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{"types":["users","tunnels"]}' > partial-backup.json
# Import backup
curl -s -X POST "${FLVX_BASE_URL}/api/v1/backup/import" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
-d @backup-20260226.json | jq '.'
```
## Helper Functions
```bash
# Add to ~/.bashrc or ~/.zshrc
flvx-login() {
export FLVX_BASE_URL="${1:-$FLVX_BASE_URL}"
TOKEN=$(curl -s -X POST "${FLVX_BASE_URL}/api/v1/user/login" \
-H "Content-Type: application/json" \
-d "{\"username\":\"${FLVX_USERNAME}\",\"password\":\"${FLVX_PASSWORD}\"}" \
| jq -r '.data.token')
export FLVX_TOKEN="$TOKEN"
echo "Logged in. Token: ${TOKEN:0:20}..."
}
flvx-api() {
local endpoint="$1"
local data="${2:-{}}"
curl -s -X POST "${FLVX_BASE_URL}${endpoint}" \
-H "Authorization: ${FLVX_TOKEN}" \
-H "Content-Type: application/json" \
-d "$data" | jq '.'
}
# Usage:
# flvx-login
# flvx-api /api/v1/node/list
# flvx-api /api/v1/forward/list '{"keyword":"web"}'
```
@@ -0,0 +1,603 @@
# HTTP Client Examples
Complete, runnable examples for various languages.
## Bash / curl
### Complete Script with Auto-Login
```bash
#!/bin/bash
set -e
# Configuration
BASE_URL="${FLVX_BASE_URL:?FLVX_BASE_URL not set}"
USERNAME="${FLVX_USERNAME:?FLVX_USERNAME not set}"
PASSWORD="${FLVX_PASSWORD:?FLVX_PASSWORD not set}"
# Login and get token
echo "Logging in..."
LOGIN_RESPONSE=$(curl -s -X POST "${BASE_URL}/api/v1/user/login" \
-H "Content-Type: application/json" \
-d "{\"username\":\"${USERNAME}\",\"password\":\"${PASSWORD}\"}")
TOKEN=$(echo "$LOGIN_RESPONSE" | jq -r '.data.token // empty')
if [ -z "$TOKEN" ]; then
echo "Login failed: $(echo "$LOGIN_RESPONSE" | jq -r '.msg')"
exit 1
fi
echo "Logged in successfully"
# API call helper
api_call() {
local endpoint="$1"
local data="${2:-{}}"
curl -s -X POST "${BASE_URL}${endpoint}" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
-d "$data"
}
# Examples
echo "=== My Package Info ==="
api_call "/api/v1/user/package" | jq '.'
echo -e "\n=== Node List ==="
api_call "/api/v1/node/list" '{}' | jq '.data.list[] | {name, status: (.status == 1)}'
echo -e "\n=== Forward List ==="
api_call "/api/v1/forward/list" '{}' | jq '.data.list[] | {name, tunnel: .tunnel_name, port: .in_port, target: .remote_addr}'
```
### Create Forward Script
```bash
#!/bin/bash
BASE_URL="${FLVX_BASE_URL}"
TOKEN="${FLVX_TOKEN}" # Pre-obtained token
create_forward() {
local name="$1"
local tunnel_id="$2"
local remote_addr="$3"
curl -s -X POST "${BASE_URL}/api/v1/forward/create" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
-d "{
\"name\": \"${name}\",
\"tunnelId\": ${tunnel_id},
\"remoteAddr\": \"${remote_addr}\",
\"strategy\": \"fifo\"
}" | jq '.'
}
# Usage: ./create-forward.sh "my-web" 1 "192.168.1.100:80"
create_forward "$@"
```
---
## Python
### Complete Client Class
```python
#!/usr/bin/env python3
"""FLVX API Client"""
import os
import requests
from typing import Optional, Any, Dict, List
class FlvxError(Exception):
"""FLVX API Error"""
def __init__(self, code: int, message: str):
self.code = code
self.message = message
super().__init__(message)
class FlvxClient:
"""FLVX API Client with auto-login"""
def __init__(
self,
base_url: Optional[str] = None,
username: Optional[str] = None,
password: Optional[str] = None
):
self.base_url = base_url or os.environ.get("FLVX_BASE_URL")
self.username = username or os.environ.get("FLVX_USERNAME")
self.password = password or os.environ.get("FLVX_PASSWORD")
if not all([self.base_url, self.username, self.password]):
raise ValueError("Missing credentials. Set FLVX_BASE_URL, FLVX_USERNAME, FLVX_PASSWORD")
self.token: Optional[str] = None
def _login(self) -> None:
"""Authenticate and store token"""
resp = requests.post(
f"{self.base_url}/api/v1/user/login",
headers={"Content-Type": "application/json"},
json={"username": self.username, "password": self.password}
)
result = resp.json()
if result["code"] != 0:
raise FlvxError(result["code"], result["msg"])
self.token = result["data"]["token"]
def _headers(self) -> Dict[str, str]:
"""Get request headers with auth"""
headers = {"Content-Type": "application/json"}
if self.token:
headers["Authorization"] = self.token # NO "Bearer " prefix!
return headers
def request(self, endpoint: str, data: Any = None) -> Any:
"""Make authenticated API request"""
if not self.token:
self._login()
resp = requests.post(
f"{self.base_url}{endpoint}",
headers=self._headers(),
json=data or {}
)
result = resp.json()
if result["code"] == 0:
return result.get("data")
if result["code"] == 401:
# Token expired, retry once
self.token = None
return self.request(endpoint, data)
raise FlvxError(result["code"], result["msg"])
# Convenience methods
def get_package(self) -> Dict:
"""Get current user's package info"""
return self.request("/api/v1/user/package", {})
def list_nodes(self) -> List[Dict]:
"""List all nodes"""
data = self.request("/api/v1/node/list", {})
return data.get("list", [])
def list_forwards(self, keyword: str = "") -> List[Dict]:
"""List forwards"""
data = self.request("/api/v1/forward/list", {"keyword": keyword})
return data.get("list", [])
def create_forward(
self,
name: str,
tunnel_id: int,
remote_addr: str,
strategy: str = "fifo",
speed_id: int = 0
) -> Dict:
"""Create a forward"""
return self.request("/api/v1/forward/create", {
"name": name,
"tunnelId": tunnel_id,
"remoteAddr": remote_addr,
"strategy": strategy,
"speedId": speed_id
})
def pause_forward(self, forward_id: int) -> None:
"""Pause a forward"""
self.request("/api/v1/forward/pause", {"id": forward_id})
def resume_forward(self, forward_id: int) -> None:
"""Resume a forward"""
self.request("/api/v1/forward/resume", {"id": forward_id})
def delete_forward(self, forward_id: int) -> None:
"""Delete a forward"""
self.request("/api/v1/forward/delete", {"id": forward_id})
# Usage example
if __name__ == "__main__":
client = FlvxClient()
# Get package info
pkg = client.get_package()
print(f"Traffic: {pkg['inFlow'] / 1e9:.2f}GB ↑ / {pkg['outFlow'] / 1e9:.2f}GB ↓")
print(f"Quota: {pkg['flow']}GB")
# List forwards with traffic
print("\nForwards:")
for fwd in client.list_forwards():
print(f" {fwd['name']}: {fwd['in_port']} → {fwd['remote_addr']}")
print(f" Traffic: {fwd['in_flow'] / 1e9:.2f}GB ↑ / {fwd['out_flow'] / 1e9:.2f}GB ↓")
```
---
## Node.js / TypeScript
### Complete Client Class
```typescript
// flvx-client.ts
interface APIResponse<T = unknown> {
code: number;
msg: string;
data?: T;
ts: number;
}
class FlvxError extends Error {
constructor(public code: number, message: string) {
super(message);
this.name = "FlvxError";
}
}
interface UserPackage {
flow: number;
inFlow: number;
outFlow: number;
tunnels: number;
forwards: number;
expTime: number;
}
interface Node {
id: number;
name: string;
status: number;
server_ip: string;
}
interface Forward {
id: number;
name: string;
tunnel_id: number;
tunnel_name: string;
in_port: number;
remote_addr: string;
status: number;
in_flow: number;
out_flow: number;
}
class FlvxClient {
private baseUrl: string;
private username: string;
private password: string;
private token?: string;
constructor(options?: {
baseUrl?: string;
username?: string;
password?: string;
}) {
this.baseUrl = options?.baseUrl ?? process.env.FLVX_BASE_URL ?? "";
this.username = options?.username ?? process.env.FLVX_USERNAME ?? "";
this.password = options?.password ?? process.env.FLVX_PASSWORD ?? "";
if (!this.baseUrl || !this.username || !this.password) {
throw new Error("Missing credentials. Set FLVX_BASE_URL, FLVX_USERNAME, FLVX_PASSWORD");
}
}
private async login(): Promise<void> {
const res = await fetch(`${this.baseUrl}/api/v1/user/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
username: this.username,
password: this.password,
}),
});
const result: APIResponse<{ token: string }> = await res.json();
if (result.code !== 0) {
throw new FlvxError(result.code, result.msg);
}
this.token = result.data!.token;
}
private async request<T>(endpoint: string, data?: object): Promise<T> {
if (!this.token) {
await this.login();
}
const res = await fetch(`${this.baseUrl}${endpoint}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: this.token!, // NO "Bearer " prefix!
},
body: JSON.stringify(data ?? {}),
});
const result: APIResponse<T> = await res.json();
if (result.code === 0) {
return result.data!;
}
if (result.code === 401) {
// Token expired, retry once
this.token = undefined;
return this.request<T>(endpoint, data);
}
throw new FlvxError(result.code, result.msg);
}
// Convenience methods
async getPackage(): Promise<UserPackage> {
return this.request("/api/v1/user/package", {});
}
async listNodes(): Promise<Node[]> {
const data = await this.request<{ list: Node[] }>("/api/v1/node/list", {});
return data.list ?? [];
}
async listForwards(keyword = ""): Promise<Forward[]> {
const data = await this.request<{ list: Forward[] }>("/api/v1/forward/list", {
keyword,
});
return data.list ?? [];
}
async createForward(options: {
name: string;
tunnelId: number;
remoteAddr: string;
strategy?: "fifo" | "round";
speedId?: number;
}): Promise<Forward> {
return this.request("/api/v1/forward/create", {
name: options.name,
tunnelId: options.tunnelId,
remoteAddr: options.remoteAddr,
strategy: options.strategy ?? "fifo",
speedId: options.speedId ?? 0,
});
}
async pauseForward(id: number): Promise<void> {
await this.request("/api/v1/forward/pause", { id });
}
async resumeForward(id: number): Promise<void> {
await this.request("/api/v1/forward/resume", { id });
}
async deleteForward(id: number): Promise<void> {
await this.request("/api/v1/forward/delete", { id });
}
}
export { FlvxClient, FlvxError };
// Usage
async function main() {
const client = new FlvxClient();
// Get package info
const pkg = await client.getPackage();
console.log(`Traffic: ${(pkg.inFlow / 1e9).toFixed(2)}GB ↑ / ${(pkg.outFlow / 1e9).toFixed(2)}GB ↓`);
console.log(`Quota: ${pkg.flow}GB`);
// List nodes
console.log("\nNodes:");
const nodes = await client.listNodes();
for (const node of nodes) {
console.log(` ${node.name}: ${node.status ? "Online" : "Offline"}`);
}
// List forwards
console.log("\nForwards:");
const forwards = await client.listForwards();
for (const fwd of forwards) {
console.log(` ${fwd.name}: ${fwd.in_port} → ${fwd.remote_addr}`);
}
}
main().catch(console.error);
```
---
## Go
### Complete Client Package
```go
// flvx/client.go
package flvx
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
type Client struct {
BaseURL string
Username string
Password string
Token string
}
type Response struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data json.RawMessage `json:"data"`
TS int64 `json:"ts"`
}
type FlvxError struct {
Code int
Message string
}
func (e *FlvxError) Error() string {
return fmt.Sprintf("FLVX error %d: %s", e.Code, e.Message)
}
func NewClient() *Client {
return &Client{
BaseURL: os.Getenv("FLVX_BASE_URL"),
Username: os.Getenv("FLVX_USERNAME"),
Password: os.Getenv("FLVX_PASSWORD"),
}
}
func (c *Client) Login() error {
payload := map[string]string{
"username": c.Username,
"password": c.Password,
}
var result struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data struct {
Token string `json:"token"`
} `json:"data"`
}
if err := c.request("/api/v1/user/login", payload, &result); err != nil {
return err
}
if result.Code != 0 {
return &FlvxError{Code: result.Code, Message: result.Msg}
}
c.Token = result.Data.Token
return nil
}
func (c *Client) Request(endpoint string, data interface{}, result interface{}) error {
// Auto-login if no token
if c.Token == "" {
if err := c.Login(); err != nil {
return err
}
}
return c.request(endpoint, data, result)
}
func (c *Client) request(endpoint string, data interface{}, result interface{}) error {
body, err := json.Marshal(data)
if err != nil {
return err
}
req, err := http.NewRequest("POST", c.BaseURL+endpoint, bytes.NewReader(body))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
if c.Token != "" {
req.Header.Set("Authorization", c.Token) // NO "Bearer " prefix!
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
return json.Unmarshal(respBody, result)
}
// Convenience methods
func (c *Client) ListNodes() ([]map[string]interface{}, error) {
var result struct {
Code int `json:"code"`
Data struct {
List []map[string]interface{} `json:"list"`
} `json:"data"`
}
if err := c.Request("/api/v1/node/list", map[string]interface{}{}, &result); err != nil {
return nil, err
}
if result.Code != 0 {
return nil, &FlvxError{Code: result.Code, Message: "failed to list nodes"}
}
return result.Data.List, nil
}
func (c *Client) CreateForward(name string, tunnelID int, remoteAddr string) (map[string]interface{}, error) {
payload := map[string]interface{}{
"name": name,
"tunnelId": tunnelID,
"remoteAddr": remoteAddr,
"strategy": "fifo",
}
var result struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data map[string]interface{} `json:"data"`
}
if err := c.Request("/api/v1/forward/create", payload, &result); err != nil {
return nil, err
}
if result.Code != 0 {
return nil, &FlvxError{Code: result.Code, Message: result.Msg}
}
return result.Data, nil
}
// Usage example
func Example() {
client := NewClient()
nodes, err := client.ListNodes()
if err != nil {
fmt.Println("Error:", err)
return
}
for _, node := range nodes {
fmt.Printf("Node: %v (status: %v)\n", node["name"], node["status"])
}
fwd, err := client.CreateForward("my-forward", 1, "192.168.1.100:80")
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Printf("Created forward: %v\n", fwd)
}
```
+281
View File
@@ -0,0 +1,281 @@
# Federation / Clustering API
Federation allows sharing nodes between FLVX panels. One panel can share nodes, and another panel can use them as remote nodes.
## Share Management (Admin)
### POST /api/v1/federation/share/list
List all peer shares.
**Request:** `{}`
**Response:**
```json
{
"code": 0,
"data": [
{
"id": 1,
"name": "Share-to-Partner",
"node_id": 1,
"node_name": "HK-Node-1",
"token": "share-token-abc123",
"max_bandwidth": 107374182400,
"expiry_time": 1767225600000,
"port_range_start": 10000,
"port_range_end": 20000,
"allowed_domains": "example.com,api.example.com",
"allowed_ips": "10.0.0.0/8,192.168.0.0/16",
"status": 1,
"created_at": 1706659200000
}
]
}
```
### POST /api/v1/federation/share/create
Create a peer share (share a node with another panel).
**Request:**
```json
{
"name": "Share-to-Partner",
"nodeId": 1,
"maxBandwidth": 107374182400,
"expiryTime": 1767225600000,
"portRangeStart": 10000,
"portRangeEnd": 20000,
"allowedDomains": "example.com,api.example.com",
"allowedIps": "10.0.0.0/8,192.168.0.0/16"
}
```
**Fields:**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| name | string | Yes | Share name |
| nodeId | number | Yes | Node to share |
| maxBandwidth | number | No | Max traffic in bytes (0 = unlimited) |
| expiryTime | number | No | Expiry timestamp in ms (0 = never) |
| portRangeStart | number | No | Allowed port range start |
| portRangeEnd | number | No | Allowed port range end |
| allowedDomains | string | No | Comma-separated domains |
| allowedIps | string | No | Comma-separated IPs/CIDRs |
**Response:**
```json
{
"code": 0,
"data": {
"id": 1,
"token": "share-token-abc123"
}
}
```
The `token` is what the remote panel uses to connect.
### POST /api/v1/federation/share/update
Update a peer share.
**Request:** Same as create, with `id` field required.
### POST /api/v1/federation/share/delete
Delete a peer share.
**Request:**
```json
{"id": 1}
```
### POST /api/v1/federation/share/reset-flow
Reset traffic counter for a share.
**Request:**
```json
{"id": 1}
```
### POST /api/v1/federation/share/remote-usage/list
List remote node usage statistics.
**Request:** `{}`
---
## Federation Runtime (Peer-to-Peer)
These endpoints use **Bearer token authentication** (different from JWT).
### POST /api/v1/federation/connect
Connect to a remote panel and get share info.
**Headers:**
```
Authorization: Bearer <share-token>
```
**Request:** `{}`
**Response:**
```json
{
"code": 0,
"data": {
"nodeName": "HK-Node-1",
"allowedPorts": [10000, 20000],
"allowedDomains": ["example.com"],
"allowedIps": ["10.0.0.0/8"]
}
}
```
### POST /api/v1/federation/tunnel/create
Create a federation tunnel on the remote node.
**Headers:**
```
Authorization: Bearer <share-token>
```
**Request:**
```json
{
"tunnelId": 1,
"role": "entry"
}
```
### POST /api/v1/federation/runtime/reserve-port
Reserve a port on the remote node.
**Request:**
```json
{
"port": 15000,
"tunnelId": 1
}
```
### POST /api/v1/federation/runtime/apply-role
Apply for a role (entry/chain/exit) on the remote node.
**Request:**
```json
{
"tunnelId": 1,
"role": "exit"
}
```
### POST /api/v1/federation/runtime/release-role
Release a role on the remote node.
**Request:**
```json
{
"tunnelId": 1
}
```
### POST /api/v1/federation/runtime/diagnose
TCP ping diagnostics from remote node to target.
**Request:**
```json
{
"target": "10.0.0.1:80"
}
```
### POST /api/v1/federation/runtime/command
Execute a command on the remote node.
**Request:**
```json
{
"command": "status"
}
```
---
## Node Import (Admin)
### POST /api/v1/federation/node/import
Import a remote node from another panel.
**Request:**
```json
{
"name": "Remote-HK-Node",
"remoteUrl": "https://other-panel.example.com",
"remoteToken": "share-token-abc123"
}
```
This creates a node with `is_remote: 1`.
---
## Workflow: Share Node with Another Panel
**On the sharing panel (Panel A):**
```bash
# 1. Create a share
SHARE_RESP=$(curl -s -X POST "${FLVX_BASE_URL}/api/v1/federation/share/create" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"name": "Share-HK-Node",
"nodeId": 1,
"portRangeStart": 10000,
"portRangeEnd": 20000,
"allowedIps": "0.0.0.0/0"
}')
SHARE_TOKEN=$(echo "$SHARE_RESP" | jq -r '.data.token')
echo "Share Token: $SHARE_TOKEN"
echo "Panel URL: ${FLVX_BASE_URL}"
```
**On the receiving panel (Panel B):**
```bash
# 2. Import the remote node
curl -s -X POST "${FLVX_BASE_URL}/api/v1/federation/node/import" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"name": "Remote-HK-Node",
"remoteUrl": "https://panel-a.example.com",
"remoteToken": "share-token-abc123"
}'
# 3. Use the remote node in tunnels like a local node
curl -s -X POST "${FLVX_BASE_URL}/api/v1/tunnel/create" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"name": "Federated-Tunnel",
"type": 1,
"inNodeId": [1],
"outNodeId": [2]
}'
```
+270
View File
@@ -0,0 +1,270 @@
# Forward Management API
Forwards are port forwarding rules created by users on their assigned tunnels.
## POST /api/v1/forward/list
List forwards. Non-admin users see only their own forwards.
**Request:**
```json
{
"page": 1,
"pageSize": 20,
"keyword": "",
"status": -1
}
```
**status filter:**
- `-1` = All
- `0` = Paused
- `1` = Running
**Response:**
```json
{
"code": 0,
"data": {
"list": [
{
"id": 1,
"user_id": 2,
"tunnel_id": 1,
"tunnel_name": "HK-US-Tunnel",
"name": "my-web-server",
"in_port": 10001,
"remote_addr": "192.168.1.100:80",
"strategy": "fifo",
"status": 1,
"speed_id": 0,
"speed_name": "",
"in_flow": 1073741824,
"out_flow": 2147483648,
"created_at": 1706659200000,
"updated_at": 1706659200000
}
],
"total": 1
}
}
```
## POST /api/v1/forward/create
Create a new forward.
**Request:**
```json
{
"name": "my-web-server",
"tunnelId": 1,
"remoteAddr": "192.168.1.100:80",
"strategy": "fifo",
"inPort": 0,
"speedId": 0
}
```
**Fields:**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| name | string | Yes | Forward name |
| tunnelId | number | Yes | Tunnel to use |
| remoteAddr | string | Yes | Target address(es), comma-separated for load balancing |
| strategy | string | No | "fifo" or "round" (default: "fifo") |
| inPort | number | No | Entry port (0 = auto-assign) |
| speedId | number | No | Speed limit rule ID (0 = no limit) |
**Strategy:**
- `fifo` = First target only
- `round` = Round-robin load balancing across targets
**Remote Address Format:**
- Single: `192.168.1.100:80`
- Multiple: `192.168.1.100:80,192.168.1.101:80,192.168.1.102:80`
**Response:**
```json
{
"code": 0,
"msg": "success",
"data": {
"id": 1,
"in_port": 10001
}
}
```
## POST /api/v1/forward/update
Update forward settings.
**Request:** Same as create, with `id` field required.
```json
{
"id": 1,
"name": "my-web-server-updated",
"remoteAddr": "192.168.1.100:8080",
"strategy": "round",
"speedId": 2
}
```
## POST /api/v1/forward/delete
Delete a forward.
**Request:**
```json
{"id": 1}
```
## POST /api/v1/forward/force-delete
Force delete a forward (even if in use).
**Request:**
```json
{"id": 1}
```
## POST /api/v1/forward/pause
Pause a forward (stops traffic but keeps configuration).
**Request:**
```json
{"id": 1}
```
**Response:**
```json
{"code": 0, "msg": "success"}
```
## POST /api/v1/forward/resume
Resume a paused forward.
**Request:**
```json
{"id": 1}
```
## POST /api/v1/forward/diagnose
Diagnose forward connectivity (TCP ping to target).
**Request:**
```json
{"id": 1}
```
**Response:**
```json
{
"code": 0,
"data": {
"reachable": true,
"latency_ms": 15,
"error": ""
}
}
```
## POST /api/v1/forward/update-order
Reorder forwards.
**Request:**
```json
{
"orders": [
{"id": 1, "order": 0},
{"id": 2, "order": 1}
]
}
```
## Batch Operations
### POST /api/v1/forward/batch-delete
```json
{"ids": [1, 2, 3]}
```
### POST /api/v1/forward/batch-pause
```json
{"ids": [1, 2, 3]}
```
### POST /api/v1/forward/batch-resume
```json
{"ids": [1, 2, 3]}
```
### POST /api/v1/forward/batch-redeploy
Recreate forwarding services on nodes.
```json
{"ids": [1, 2, 3]}
```
### POST /api/v1/forward/batch-change-tunnel
Move forwards to a different tunnel.
```json
{
"ids": [1, 2, 3],
"tunnelId": 5
}
```
## Traffic Units
| Field | Unit | Notes |
|-------|------|-------|
| in_flow | Bytes | Upload traffic |
| out_flow | Bytes | Download traffic |
Convert to GB: `in_flow / 1073741824`
## Example: Create Forward with Load Balancing
```bash
# Create forward with 3 backend servers
curl -s -X POST "${FLVX_BASE_URL}/api/v1/forward/create" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"name": "web-cluster",
"tunnelId": 1,
"remoteAddr": "10.0.0.1:80,10.0.0.2:80,10.0.0.3:80",
"strategy": "round"
}'
```
## Example: Check Forward Status and Traffic
```bash
curl -s -X POST "${FLVX_BASE_URL}/api/v1/forward/list" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{}' | jq '.data.list[] | {
name,
tunnel: .tunnel_name,
entry_port: .in_port,
target: .remote_addr,
status: (if .status == 1 then "running" else "paused" end),
upload_gb: (.in_flow / 1073741824 | floor),
download_gb: (.out_flow / 1073741824 | floor)
}'
```
+240
View File
@@ -0,0 +1,240 @@
# Group & Permission Management API
Groups organize users and tunnels, with permissions controlling access.
## Tunnel Groups
### POST /api/v1/group/tunnel/list
List all tunnel groups.
**Request:** `{}`
**Response:**
```json
{
"code": 0,
"data": [
{
"id": 1,
"name": "Premium-Tunnels",
"status": 1,
"tunnel_ids": [1, 2, 3],
"created_at": 1706659200000
}
]
}
```
### POST /api/v1/group/tunnel/create
Create a tunnel group.
**Request:**
```json
{
"name": "Premium-Tunnels",
"status": 1
}
```
### POST /api/v1/group/tunnel/update
Update tunnel group.
**Request:**
```json
{
"id": 1,
"name": "VIP-Tunnels",
"status": 1
}
```
### POST /api/v1/group/tunnel/delete
Delete tunnel group.
**Request:**
```json
{"id": 1}
```
### POST /api/v1/group/tunnel/assign
Assign tunnels to a group.
**Request:**
```json
{
"groupId": 1,
"tunnelIds": [1, 2, 3]
}
```
---
## User Groups
### POST /api/v1/group/user/list
List all user groups.
**Request:** `{}`
**Response:**
```json
{
"code": 0,
"data": [
{
"id": 1,
"name": "VIP-Users",
"status": 1,
"user_ids": [2, 3, 4],
"created_at": 1706659200000
}
]
}
```
### POST /api/v1/group/user/create
Create a user group.
**Request:**
```json
{
"name": "VIP-Users",
"status": 1
}
```
### POST /api/v1/group/user/update
Update user group.
**Request:**
```json
{
"id": 1,
"name": "Premium-Users",
"status": 1
}
```
### POST /api/v1/group/user/delete
Delete user group.
**Request:**
```json
{"id": 1}
```
### POST /api/v1/group/user/assign
Assign users to a group.
**Request:**
```json
{
"groupId": 1,
"userIds": [2, 3, 4]
}
```
---
## Permissions
Permissions link user groups to tunnel groups, allowing users in a user group to access tunnels in a tunnel group.
### POST /api/v1/group/permission/list
List all permissions.
**Request:** `{}`
**Response:**
```json
{
"code": 0,
"data": [
{
"id": 1,
"user_group_id": 1,
"user_group_name": "VIP-Users",
"tunnel_group_id": 1,
"tunnel_group_name": "Premium-Tunnels",
"created_at": 1706659200000
}
]
}
```
### POST /api/v1/group/permission/assign
Create a permission (grant user group access to tunnel group).
**Request:**
```json
{
"userGroupId": 1,
"tunnelGroupId": 1
}
```
**Response:**
```json
{"code": 0, "msg": "success", "data": {"id": 1}}
```
### POST /api/v1/group/permission/remove
Remove a permission.
**Request:**
```json
{"id": 1}
```
---
## Workflow: Set Up Group-Based Access
```bash
# 1. Create user group
curl -s -X POST "${FLVX_BASE_URL}/api/v1/group/user/create" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{"name":"Standard-Users"}'
# Response: {"data":{"id":1}}
# 2. Create tunnel group
curl -s -X POST "${FLVX_BASE_URL}/api/v1/group/tunnel/create" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{"name":"Standard-Tunnels"}'
# Response: {"data":{"id":1}}
# 3. Add tunnels to tunnel group
curl -s -X POST "${FLVX_BASE_URL}/api/v1/group/tunnel/assign" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{"groupId":1,"tunnelIds":[1,2,3]}'
# 4. Add users to user group
curl -s -X POST "${FLVX_BASE_URL}/api/v1/group/user/assign" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{"groupId":1,"userIds":[2,3,4]}'
# 5. Grant permission (user group -> tunnel group)
curl -s -X POST "${FLVX_BASE_URL}/api/v1/group/permission/assign" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{"userGroupId":1,"tunnelGroupId":1}'
```
Now users 2, 3, 4 can access tunnels 1, 2, 3.
+266
View File
@@ -0,0 +1,266 @@
# Node Management API
All node endpoints require admin privileges (role_id: 0).
## POST /api/v1/node/list
List all nodes with status information.
**Request:**
```json
{
"page": 1,
"pageSize": 20,
"keyword": ""
}
```
**Response:**
```json
{
"code": 0,
"data": {
"list": [
{
"id": 1,
"name": "HK-Node-1",
"secret": "abc123...",
"server_ip": "1.2.3.4",
"server_ip_v4": "1.2.3.4",
"server_ip_v6": "2001:db8::1",
"port": "1000-65535",
"interface_name": "eth0",
"http": 1,
"tls": 1,
"socks": 1,
"tcp_listen_addr": "[::]",
"udp_listen_addr": "[::]",
"status": 1,
"is_remote": 0,
"version": "2.1.5",
"created_at": 1706659200000,
"updated_at": 1706659200000
}
],
"total": 1
}
}
```
**Status Values:**
- `0` = Offline
- `1` = Online
**is_remote Values:**
- `0` = Local node (managed by this panel)
- `1` = Remote node (federation from another panel)
## POST /api/v1/node/create
Create a new node.
**Request:**
```json
{
"name": "US-Node-1",
"serverIp": "5.6.7.8",
"serverIpV4": "5.6.7.8",
"serverIpV6": "2001:db8::2",
"port": "1000-65535",
"interfaceName": "eth0",
"http": 1,
"tls": 1,
"socks": 1,
"tcpListenAddr": "[::]",
"udpListenAddr": "[::]",
"isRemote": 0,
"remoteUrl": "",
"remoteToken": ""
}
```
**Fields:**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| name | string | Yes | Node name |
| serverIp | string | Yes | Primary server IP (display) |
| serverIpV4 | string | No | IPv4 address |
| serverIpV6 | string | No | IPv6 address |
| port | string | No | Allowed port range (default: "1000-65535") |
| interfaceName | string | No | Network interface for traffic |
| http | number | No | Enable HTTP protocol (1/0) |
| tls | number | No | Enable TLS protocol (1/0) |
| socks | number | No | Enable SOCKS protocol (1/0) |
| tcpListenAddr | string | No | TCP listen address (default: "[::]") |
| udpListenAddr | string | No | UDP listen address (default: "[::]") |
| isRemote | number | No | Federation node (1/0) |
| remoteUrl | string | If isRemote=1 | Remote panel URL |
| remoteToken | string | If isRemote=1 | Federation token |
**Response:**
```json
{"code": 0, "msg": "success", "data": {"id": 2, "secret": "xyz789..."}}
```
## POST /api/v1/node/install
Generate installation command for a node.
**Request:**
```json
{"id": 2}
```
**Response:**
```json
{
"code": 0,
"data": {
"command": "curl -fsSL https://panel.example.com/install.sh | bash -s -- --secret xyz789... --server https://panel.example.com"
}
}
```
## POST /api/v1/node/update
Update node configuration.
**Request:** Same fields as create, with `id` field required.
```json
{
"id": 2,
"name": "US-Node-1-Updated",
"serverIp": "5.6.7.8",
"http": 1,
"tls": 1,
"socks": 0
}
```
## POST /api/v1/node/delete
Delete a node.
**Request:**
```json
{"id": 2}
```
## POST /api/v1/node/batch-delete
Delete multiple nodes.
**Request:**
```json
{"ids": [2, 3, 4]}
```
## POST /api/v1/node/check-status
Refresh and check status of all nodes.
**Request:** `{}`
**Response:**
```json
{
"code": 0,
"data": {
"updated": 5,
"online": 4,
"offline": 1
}
}
```
## POST /api/v1/node/update-order
Reorder nodes (for display purposes).
**Request:**
```json
{
"orders": [
{"id": 1, "order": 0},
{"id": 2, "order": 1}
]
}
```
## POST /api/v1/node/releases
List available FLVX agent releases.
**Request:** `{}`
**Response:**
```json
{
"code": 0,
"data": [
{"version": "2.1.5", "published_at": 1706659200000},
{"version": "2.1.4", "published_at": 1706572800000}
]
}
```
## POST /api/v1/node/upgrade
Upgrade a single node agent.
**Request:**
```json
{
"id": 2,
"version": "2.1.5"
}
```
## POST /api/v1/node/batch-upgrade
Upgrade multiple node agents.
**Request:**
```json
{
"ids": [1, 2, 3],
"version": "2.1.5"
}
```
## POST /api/v1/node/rollback
Rollback node agent to previous version.
**Request:**
```json
{"id": 2}
```
## Example: Full Node Setup Workflow
```bash
# 1. Create node
RESPONSE=$(curl -s -X POST "${FLVX_BASE_URL}/api/v1/node/create" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{"name":"SG-Node-1","serverIp":"203.0.113.10"}')
NODE_ID=$(echo "$RESPONSE" | jq -r '.data.id')
# 2. Get install command
curl -s -X POST "${FLVX_BASE_URL}/api/v1/node/install" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"id\":${NODE_ID}}"
# 3. Run install command on target server (manual step)
# 4. Verify node is online
curl -s -X POST "${FLVX_BASE_URL}/api/v1/node/list" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{}' | jq ".data.list[] | select(.id == $NODE_ID) | {name, status}"
```
+143
View File
@@ -0,0 +1,143 @@
# Speed Limit Management API
Speed limits define bandwidth restrictions that can be applied to forwards or user-tunnel assignments.
## POST /api/v1/speed-limit/list
List all speed limit rules.
**Request:** `{}`
**Response:**
```json
{
"code": 0,
"data": [
{
"id": 1,
"name": "10Mbps",
"speed": 10,
"status": 1,
"created_at": 1706659200000
},
{
"id": 2,
"name": "100Mbps",
"speed": 100,
"status": 1,
"created_at": 1706659200000
}
]
}
```
## POST /api/v1/speed-limit/create
Create a speed limit rule.
**Request:**
```json
{
"name": "50Mbps",
"speed": 50,
"status": 1
}
```
**Fields:**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| name | string | Yes | Rule name |
| speed | number | Yes | Speed limit in Mbps |
| status | number | No | 1=active, 0=disabled (default: 1) |
**Response:**
```json
{"code": 0, "msg": "success", "data": {"id": 3}}
```
## POST /api/v1/speed-limit/update
Update a speed limit rule.
**Request:**
```json
{
"id": 3,
"name": "50Mbps-Premium",
"speed": 50,
"status": 1
}
```
## POST /api/v1/speed-limit/delete
Delete a speed limit rule.
**Request:**
```json
{"id": 3}
```
## Applying Speed Limits
Speed limits can be applied at two levels:
### 1. Forward Level
Set `speedId` when creating or updating a forward:
```bash
curl -s -X POST "${FLVX_BASE_URL}/api/v1/forward/create" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"name": "limited-forward",
"tunnelId": 1,
"remoteAddr": "10.0.0.1:80",
"speedId": 1
}'
```
### 2. User-Tunnel Assignment Level
Set `speedId` when assigning a tunnel to a user:
```bash
curl -s -X POST "${FLVX_BASE_URL}/api/v1/tunnel/user/assign" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"userId": 2,
"tunnelId": 1,
"flow": 50,
"speedId": 2
}'
```
## Example: Create Tiered Speed Limits
```bash
# Create speed limit tiers
curl -s -X POST "${FLVX_BASE_URL}/api/v1/speed-limit/create" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{"name":"Basic-10Mbps","speed":10}'
curl -s -X POST "${FLVX_BASE_URL}/api/v1/speed-limit/create" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{"name":"Standard-50Mbps","speed":50}'
curl -s -X POST "${FLVX_BASE_URL}/api/v1/speed-limit/create" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{"name":"Premium-Unlimited","speed":1000}'
# List all rules
curl -s -X POST "${FLVX_BASE_URL}/api/v1/speed-limit/list" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{}' | jq '.data'
```
+313
View File
@@ -0,0 +1,313 @@
# Tunnel Management API
Tunnels define the forwarding path: entry node(s) → (chain nodes) → exit node(s).
## POST /api/v1/tunnel/list
List all tunnels.
**Request:**
```json
{
"page": 1,
"pageSize": 20,
"keyword": ""
}
```
**Response:**
```json
{
"code": 0,
"data": {
"list": [
{
"id": 1,
"name": "HK-US-Tunnel",
"type": 1,
"protocol": "tcp",
"flow": 1,
"traffic_ratio": 1,
"status": 1,
"ip_preference": "ipv4",
"in_ip": "",
"in_node_id": [1],
"chain_node_id": [],
"out_node_id": [2],
"created_at": 1706659200000
}
],
"total": 1
}
}
```
## POST /api/v1/tunnel/get
Get a single tunnel by ID.
**Request:**
```json
{"id": 1}
```
## POST /api/v1/tunnel/create
Create a new tunnel.
**Request:**
```json
{
"name": "JP-SG-Tunnel",
"type": 1,
"flow": 1,
"trafficRatio": 1,
"status": 1,
"ipPreference": "ipv4",
"inIp": "",
"inNodeId": [3],
"chainNodeId": [],
"outNodeId": [4]
}
```
**Fields:**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| name | string | Yes | Tunnel name |
| type | number | Yes | 1=port forward, 2=tunnel forward |
| flow | number | No | Traffic multiplier (default: 1) |
| trafficRatio | number | No | Traffic ratio (default: 1) |
| status | number | No | 1=active, 0=disabled (default: 1) |
| ipPreference | string | No | "ipv4", "ipv6", or "" (both) |
| inIp | string | No | Custom entry IP |
| inNodeId | number[] | Yes | Entry node IDs |
| chainNodeId | number[] | No | Chain/relay node IDs |
| outNodeId | number[] | Yes | Exit node IDs |
**Tunnel Types:**
- `1` = Port Forward: Simple port-to-port forwarding
- `2` = Tunnel Forward: Multi-hop tunnel forwarding
**Response:**
```json
{"code": 0, "msg": "success", "data": {"id": 2}}
```
## POST /api/v1/tunnel/update
Update tunnel configuration.
**Request:** Same as create, with `id` field required.
## POST /api/v1/tunnel/delete
Delete a tunnel.
**Request:**
```json
{"id": 2}
```
## POST /api/v1/tunnel/batch-delete
Delete multiple tunnels.
**Request:**
```json
{"ids": [2, 3]}
```
## POST /api/v1/tunnel/diagnose
Diagnose tunnel connectivity.
**Request:**
```json
{"id": 1}
```
**Response:**
```json
{
"code": 0,
"data": {
"reachable": true,
"latency_ms": 25,
"path": ["entry-node", "exit-node"],
"error": ""
}
}
```
## POST /api/v1/tunnel/update-order
Reorder tunnels.
**Request:**
```json
{
"orders": [
{"id": 1, "order": 0},
{"id": 2, "order": 1}
]
}
```
## POST /api/v1/tunnel/batch-redeploy
Redeploy multiple tunnels (recreate forwarding services).
**Request:**
```json
{"ids": [1, 2, 3]}
```
---
## User-Tunnel Assignment
These endpoints manage which users can use which tunnels.
### POST /api/v1/tunnel/user/tunnel
List tunnels visible to the current user (or all tunnels for admin).
**Request:** `{}`
**Response:**
```json
{
"code": 0,
"data": [
{
"id": 1,
"name": "HK-US-Tunnel",
"type": 1,
"status": 1,
"in_node_name": "HK-Node-1",
"out_node_name": "US-Node-1"
}
]
}
```
### POST /api/v1/tunnel/user/list
List user-tunnel assignments (admin only).
**Request:**
```json
{
"page": 1,
"pageSize": 20,
"userId": 2
}
```
**Response:**
```json
{
"code": 0,
"data": {
"list": [
{
"id": 1,
"user_id": 2,
"tunnel_id": 1,
"tunnel_name": "HK-US-Tunnel",
"flow": 50,
"in_flow": 1073741824,
"out_flow": 2147483648,
"exp_time": 0,
"speed_id": 0
}
],
"total": 1
}
}
```
### POST /api/v1/tunnel/user/assign
Assign a tunnel to a user.
**Request:**
```json
{
"userId": 2,
"tunnelId": 1,
"flow": 50,
"expTime": 0,
"speedId": 0
}
```
**Fields:**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| userId | number | Yes | User ID |
| tunnelId | number | Yes | Tunnel ID |
| flow | number | No | Traffic quota for this tunnel in GB |
| expTime | number | No | Expiry for this assignment (ms, 0=never) |
| speedId | number | No | Speed limit rule ID |
### POST /api/v1/tunnel/user/batch-assign
Batch assign tunnels to a user.
**Request:**
```json
{
"userId": 2,
"tunnelIds": [1, 2, 3],
"flow": 50,
"expTime": 0
}
```
### POST /api/v1/tunnel/user/remove
Remove a tunnel from a user.
**Request:**
```json
{
"userId": 2,
"tunnelId": 1
}
```
### POST /api/v1/tunnel/user/update
Update user-tunnel assignment settings.
**Request:**
```json
{
"id": 1,
"flow": 100,
"expTime": 1767225600000,
"speedId": 2
}
```
## Example: Assign Tunnel to User
```bash
# 1. Create tunnel
TUNNEL_RESP=$(curl -s -X POST "${FLVX_BASE_URL}/api/v1/tunnel/create" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{"name":"Test-Tunnel","type":1,"inNodeId":[1],"outNodeId":[2]}')
TUNNEL_ID=$(echo "$TUNNEL_RESP" | jq -r '.data.id')
# 2. Assign to user with 30GB quota
curl -s -X POST "${FLVX_BASE_URL}/api/v1/tunnel/user/assign" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"userId\":2,\"tunnelId\":${TUNNEL_ID},\"flow\":30}"
```
+346
View File
@@ -0,0 +1,346 @@
# TypeScript Type Definitions
## API Response Envelope
```typescript
interface APIResponse<T = unknown> {
code: number; // 0 = success
msg: string; // Message (usually Chinese)
ts: number; // Unix timestamp in milliseconds
data?: T; // Response payload
}
```
## Pagination
```typescript
interface PaginatedRequest {
page?: number;
pageSize?: number;
keyword?: string;
}
interface PaginatedResponse<T> {
list: T[];
total: number;
}
```
## User
```typescript
interface User {
id: number;
user: string; // Username
pwd?: string; // Password (only on create/update)
name?: string; // Display name
role_id: number; // 0 = admin, 1 = regular
status: number; // 1 = active, 0 = disabled
flow: number; // Traffic quota in GB
in_flow: number; // Used upload in bytes
out_flow: number; // Used download in bytes
exp_time: number; // Expiry timestamp (ms), 0 = never
flow_reset_time: number;// Monthly reset day (1-28), 0 = no reset
created_at?: number;
updated_at?: number;
}
interface UserCreateRequest {
user: string;
pwd: string;
name?: string;
status?: number;
flow?: number;
num?: number;
expTime?: number;
flowResetTime?: number;
groupIds?: number[];
}
interface UserPackage {
flow: number; // Total quota in GB
inFlow: number; // Used upload in bytes
outFlow: number; // Used download in bytes
tunnels: number; // Assigned tunnel count
forwards: number; // Created forward count
expTime: number; // Expiry timestamp (ms)
}
```
## Node
```typescript
interface Node {
id: number;
name: string;
secret: string;
server_ip: string;
server_ip_v4?: string;
server_ip_v6?: string;
port: string; // "1000-65535"
interface_name?: string;
http: number; // 1 = enabled
tls: number;
socks: number;
tcp_listen_addr: string;// "[::]"
udp_listen_addr: string;
status: number; // 1 = online, 0 = offline
is_remote: number; // 0 = local, 1 = federation
remote_url?: string;
remote_token?: string;
version?: string;
created_at?: number;
updated_at?: number;
}
interface NodeCreateRequest {
name: string;
serverIp: string;
serverIpV4?: string;
serverIpV6?: string;
port?: string;
interfaceName?: string;
http?: number;
tls?: number;
socks?: number;
tcpListenAddr?: string;
udpListenAddr?: string;
isRemote?: number;
remoteUrl?: string;
remoteToken?: string;
}
```
## Tunnel
```typescript
interface Tunnel {
id: number;
name: string;
type: number; // 1 = port forward, 2 = tunnel forward
protocol?: string;
flow: number; // Traffic multiplier
traffic_ratio: number;
status: number; // 1 = active, 0 = disabled
ip_preference?: string; // "ipv4", "ipv6", ""
in_ip?: string;
in_node_id?: number[];
chain_node_id?: number[];
out_node_id?: number[];
created_at?: number;
}
interface TunnelCreateRequest {
name: string;
type: number;
flow?: number;
trafficRatio?: number;
status?: number;
ipPreference?: string;
inIp?: string;
inNodeId: number[];
chainNodeId?: number[];
outNodeId: number[];
}
```
## Forward
```typescript
interface Forward {
id: number;
user_id: number;
tunnel_id: number;
tunnel_name?: string;
name: string;
in_port: number;
remote_addr: string;
strategy: string; // "fifo" | "round"
status: number; // 1 = running, 0 = paused
speed_id: number;
speed_name?: string;
in_flow: number; // Upload bytes
out_flow: number; // Download bytes
created_at?: number;
updated_at?: number;
}
interface ForwardCreateRequest {
name: string;
tunnelId: number;
remoteAddr: string;
strategy?: string;
inPort?: number;
speedId?: number;
}
```
## Speed Limit
```typescript
interface SpeedLimit {
id: number;
name: string;
speed: number; // Mbps
status: number;
created_at?: number;
}
```
## Groups
```typescript
interface TunnelGroup {
id: number;
name: string;
status: number;
tunnel_ids?: number[];
created_at?: number;
}
interface UserGroup {
id: number;
name: string;
status: number;
user_ids?: number[];
created_at?: number;
}
interface GroupPermission {
id: number;
user_group_id: number;
user_group_name?: string;
tunnel_group_id: number;
tunnel_group_name?: string;
created_at?: number;
}
```
## User-Tunnel Assignment
```typescript
interface UserTunnel {
id: number;
user_id: number;
tunnel_id: number;
tunnel_name?: string;
flow: number; // Quota for this tunnel in GB
in_flow: number;
out_flow: number;
exp_time: number;
speed_id: number;
}
```
## Federation
```typescript
interface PeerShare {
id: number;
name: string;
node_id: number;
node_name?: string;
token: string;
max_bandwidth: number;
expiry_time: number;
port_range_start: number;
port_range_end: number;
allowed_domains: string;
allowed_ips: string;
status: number;
created_at?: number;
}
```
## Backup
```typescript
interface BackupExport {
version: string;
exportedAt: number;
types: string[];
users?: User[];
nodes?: Node[];
tunnels?: Tunnel[];
forwards?: Forward[];
speedLimits?: SpeedLimit[];
tunnelGroups?: TunnelGroup[];
userGroups?: UserGroup[];
groupPermissions?: GroupPermission[];
configs?: Record<string, string>;
}
```
## Client Helper Class
```typescript
class FlvxClient {
private baseUrl: string;
private username: string;
private password: string;
private token?: string;
constructor(baseUrl?: string, username?: string, password?: string) {
this.baseUrl = baseUrl ?? process.env.FLVX_BASE_URL ?? "";
this.username = username ?? process.env.FLVX_USERNAME ?? "";
this.password = password ?? process.env.FLVX_PASSWORD ?? "";
}
private async ensureToken(): Promise<void> {
if (this.token) return;
const res = await fetch(`${this.baseUrl}/api/v1/user/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
username: this.username,
password: this.password
}),
});
const result: APIResponse<{ token: string }> = await res.json();
if (result.code !== 0) throw new Error(result.msg);
this.token = result.data!.token;
}
async request<T>(endpoint: string, data?: object): Promise<T> {
await this.ensureToken();
const res = await fetch(`${this.baseUrl}${endpoint}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": this.token!, // NO "Bearer " prefix!
},
body: JSON.stringify(data ?? {}),
});
const result: APIResponse<T> = await res.json();
if (result.code === 401) {
this.token = undefined;
return this.request(endpoint, data);
}
if (result.code !== 0) throw new Error(result.msg);
return result.data!;
}
// Convenience methods
async listNodes(): Promise<Node[]> {
const data = await this.request<{ list: Node[] }>("/api/v1/node/list", {});
return data.list ?? [];
}
async listForwards(): Promise<Forward[]> {
const data = await this.request<{ list: Forward[] }>("/api/v1/forward/list", {});
return data.list ?? [];
}
async createForward(req: ForwardCreateRequest): Promise<Forward> {
return this.request("/api/v1/forward/create", req);
}
async getUserPackage(): Promise<UserPackage> {
return this.request("/api/v1/user/package", {});
}
}
```
+187
View File
@@ -0,0 +1,187 @@
# User Management API
All user management endpoints require admin privileges (role_id: 0).
## POST /api/v1/user/list
List all users with pagination and filtering.
**Request:**
```json
{
"page": 1,
"pageSize": 20,
"keyword": "search-term"
}
```
**Response:**
```json
{
"code": 0,
"data": {
"list": [
{
"id": 1,
"user": "admin",
"name": "Administrator",
"role_id": 0,
"status": 1,
"flow": 1000,
"in_flow": 10737418240,
"out_flow": 21474836480,
"exp_time": 1767225600000,
"flow_reset_time": 1,
"created_at": 1706659200000,
"updated_at": 1706659200000
}
],
"total": 1
}
}
```
## POST /api/v1/user/create
Create a new user.
**Request:**
```json
{
"user": "username",
"pwd": "password",
"name": "Display Name",
"status": 1,
"flow": 100,
"num": 10,
"expTime": 1767225600000,
"flowResetTime": 1,
"groupIds": [1, 2]
}
```
**Fields:**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| user | string | Yes | Username (unique) |
| pwd | string | Yes | Password |
| name | string | No | Display name |
| status | number | No | 1=active, 0=disabled (default: 1) |
| flow | number | No | Traffic quota in GB (default: 0) |
| num | number | No | Max forwards allowed (default: 0 = unlimited) |
| expTime | number | No | Expiry timestamp in ms (0 = never) |
| flowResetTime | number | No | Monthly reset day 1-28 (0 = no reset) |
| groupIds | number[] | No | User group IDs to assign |
**Response:**
```json
{"code": 0, "msg": "success", "data": {"id": 2}}
```
## POST /api/v1/user/update
Update user details.
**Request:**
```json
{
"id": 2,
"user": "new-username",
"pwd": "new-password",
"name": "New Name",
"status": 1,
"flow": 200,
"num": 20,
"expTime": 1767225600000,
"flowResetTime": 15,
"groupIds": [1]
}
```
Note: `pwd` is optional for updates. If omitted, password remains unchanged.
## POST /api/v1/user/delete
Delete a user (cascades to forwards and tunnel assignments).
**Request:**
```json
{"id": 2}
```
**Response:**
```json
{"code": 0, "msg": "success"}
```
## POST /api/v1/user/reset
Reset user traffic at user or tunnel level.
**Request (User level):**
```json
{
"id": 2,
"type": "user"
}
```
**Request (Tunnel level):**
```json
{
"id": 2,
"type": "tunnel",
"tunnelId": 1
}
```
**Response:**
```json
{"code": 0, "msg": "success"}
```
## POST /api/v1/user/groups
Get groups a user belongs to.
**Request:**
```json
{"id": 2}
```
**Response:**
```json
{
"code": 0,
"data": [
{"id": 1, "name": "VIP Users"}
]
}
```
## Traffic Units
| Field | Unit | Conversion |
|-------|------|------------|
| flow | GB | Gigabytes |
| in_flow | Bytes | Divide by 1,073,741,824 for GB |
| out_flow | Bytes | Divide by 1,073,741,824 for GB |
## Example: Create User with 50GB Quota
```bash
curl -X POST "${FLVX_BASE_URL}/api/v1/user/create" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"user": "alice",
"pwd": "SecurePass123!",
"name": "Alice",
"status": 1,
"flow": 50,
"num": 5,
"expTime": 1735689600000,
"flowResetTime": 1
}'
```