From 58abba7fc05dbca6f89f82d3f0ff6274c5ecedaf Mon Sep 17 00:00:00 2001 From: sagitchu Date: Sat, 28 Feb 2026 14:07:54 +0800 Subject: [PATCH] docs: add AI Skill integration guide --- .github/workflows/publish-skill.yml | 48 ++ doc/ai-skill.md | 220 +++++++ doc/index.md | 1 + skills/flvx-api/SKILL.md | 302 +++++++++ skills/flvx-api/package.json | 44 ++ skills/flvx-api/references/auth.md | 151 +++++ skills/flvx-api/references/backup.md | 143 +++++ skills/flvx-api/references/config.md | 149 +++++ skills/flvx-api/references/errors.md | 168 +++++ .../references/examples/curl-examples.md | 256 ++++++++ .../references/examples/http-client.md | 603 ++++++++++++++++++ skills/flvx-api/references/federation.md | 281 ++++++++ skills/flvx-api/references/forwards.md | 270 ++++++++ skills/flvx-api/references/groups.md | 240 +++++++ skills/flvx-api/references/nodes.md | 266 ++++++++ skills/flvx-api/references/speed-limits.md | 143 +++++ skills/flvx-api/references/tunnels.md | 313 +++++++++ skills/flvx-api/references/types.md | 346 ++++++++++ skills/flvx-api/references/users.md | 187 ++++++ 19 files changed, 4131 insertions(+) create mode 100644 .github/workflows/publish-skill.yml create mode 100644 doc/ai-skill.md create mode 100644 skills/flvx-api/SKILL.md create mode 100644 skills/flvx-api/package.json create mode 100644 skills/flvx-api/references/auth.md create mode 100644 skills/flvx-api/references/backup.md create mode 100644 skills/flvx-api/references/config.md create mode 100644 skills/flvx-api/references/errors.md create mode 100644 skills/flvx-api/references/examples/curl-examples.md create mode 100644 skills/flvx-api/references/examples/http-client.md create mode 100644 skills/flvx-api/references/federation.md create mode 100644 skills/flvx-api/references/forwards.md create mode 100644 skills/flvx-api/references/groups.md create mode 100644 skills/flvx-api/references/nodes.md create mode 100644 skills/flvx-api/references/speed-limits.md create mode 100644 skills/flvx-api/references/tunnels.md create mode 100644 skills/flvx-api/references/types.md create mode 100644 skills/flvx-api/references/users.md diff --git a/.github/workflows/publish-skill.yml b/.github/workflows/publish-skill.yml new file mode 100644 index 0000000..b7650ca --- /dev/null +++ b/.github/workflows/publish-skill.yml @@ -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 diff --git a/doc/ai-skill.md b/doc/ai-skill.md new file mode 100644 index 0000000..9e3e878 --- /dev/null +++ b/doc/ai-skill.md @@ -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: +{} + +### 创建转发 +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。 diff --git a/doc/index.md b/doc/index.md index e7a69f4..b7c5069 100644 --- a/doc/index.md +++ b/doc/index.md @@ -18,6 +18,7 @@ - [安装部署](./install.md) - [使用指南](./usage.md) - [PostgreSQL 数据库指南](./postgresql.md) +- [AI Skill 接入](./ai-skill.md) - 让大模型直接操作面板 - [常见问题](./faq.md) ## 免责声明 diff --git a/skills/flvx-api/SKILL.md b/skills/flvx-api/SKILL.md new file mode 100644 index 0000000..d31c76a --- /dev/null +++ b/skills/flvx-api/SKILL.md @@ -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` | `` | ⚠️ 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: `, NOT `Authorization: Bearer ` +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 +``` diff --git a/skills/flvx-api/package.json b/skills/flvx-api/package.json new file mode 100644 index 0000000..7445d2e --- /dev/null +++ b/skills/flvx-api/package.json @@ -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){}\"" + } +} diff --git a/skills/flvx-api/references/auth.md b/skills/flvx-api/references/auth.md new file mode 100644 index 0000000..0f89678 --- /dev/null +++ b/skills/flvx-api/references/auth.md @@ -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) | diff --git a/skills/flvx-api/references/backup.md b/skills/flvx-api/references/backup.md new file mode 100644 index 0000000..94e82dc --- /dev/null +++ b/skills/flvx-api/references/backup.md @@ -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 diff --git a/skills/flvx-api/references/config.md b/skills/flvx-api/references/config.md new file mode 100644 index 0000000..f846450 --- /dev/null +++ b/skills/flvx-api/references/config.md @@ -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 | `"

Notice...

"` | +| `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": "Welcome! 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" + }' +``` diff --git a/skills/flvx-api/references/errors.md b/skills/flvx-api/references/errors.md new file mode 100644 index 0000000..6d0bf15 --- /dev/null +++ b/skills/flvx-api/references/errors.md @@ -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(endpoint: string, data: object): Promise { + 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( + endpoint: string, + data: object, + maxRetries = 1 +): Promise { + let lastError: Error; + + for (let i = 0; i <= maxRetries; i++) { + try { + if (!TOKEN) { + await login(); + } + return await callApi(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个字符` | diff --git a/skills/flvx-api/references/examples/curl-examples.md b/skills/flvx-api/references/examples/curl-examples.md new file mode 100644 index 0000000..6b58870 --- /dev/null +++ b/skills/flvx-api/references/examples/curl-examples.md @@ -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"}' +``` diff --git a/skills/flvx-api/references/examples/http-client.md b/skills/flvx-api/references/examples/http-client.md new file mode 100644 index 0000000..4038cef --- /dev/null +++ b/skills/flvx-api/references/examples/http-client.md @@ -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 { + 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 { + 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(endpoint: string, data?: object): Promise { + 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 = await res.json(); + + if (result.code === 0) { + return result.data!; + } + + if (result.code === 401) { + // Token expired, retry once + this.token = undefined; + return this.request(endpoint, data); + } + + throw new FlvxError(result.code, result.msg); + } + + // Convenience methods + + async getPackage(): Promise { + return this.request("/api/v1/user/package", {}); + } + + async listNodes(): Promise { + const data = await this.request<{ list: Node[] }>("/api/v1/node/list", {}); + return data.list ?? []; + } + + async listForwards(keyword = ""): Promise { + 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 { + 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 { + await this.request("/api/v1/forward/pause", { id }); + } + + async resumeForward(id: number): Promise { + await this.request("/api/v1/forward/resume", { id }); + } + + async deleteForward(id: number): Promise { + 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) +} +``` diff --git a/skills/flvx-api/references/federation.md b/skills/flvx-api/references/federation.md new file mode 100644 index 0000000..f839992 --- /dev/null +++ b/skills/flvx-api/references/federation.md @@ -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 +``` + +**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 +``` + +**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] + }' +``` diff --git a/skills/flvx-api/references/forwards.md b/skills/flvx-api/references/forwards.md new file mode 100644 index 0000000..32396c3 --- /dev/null +++ b/skills/flvx-api/references/forwards.md @@ -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) + }' +``` diff --git a/skills/flvx-api/references/groups.md b/skills/flvx-api/references/groups.md new file mode 100644 index 0000000..e6d47cc --- /dev/null +++ b/skills/flvx-api/references/groups.md @@ -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. diff --git a/skills/flvx-api/references/nodes.md b/skills/flvx-api/references/nodes.md new file mode 100644 index 0000000..c5872b4 --- /dev/null +++ b/skills/flvx-api/references/nodes.md @@ -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}" +``` diff --git a/skills/flvx-api/references/speed-limits.md b/skills/flvx-api/references/speed-limits.md new file mode 100644 index 0000000..666c92a --- /dev/null +++ b/skills/flvx-api/references/speed-limits.md @@ -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' +``` diff --git a/skills/flvx-api/references/tunnels.md b/skills/flvx-api/references/tunnels.md new file mode 100644 index 0000000..4e8a6d6 --- /dev/null +++ b/skills/flvx-api/references/tunnels.md @@ -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}" +``` diff --git a/skills/flvx-api/references/types.md b/skills/flvx-api/references/types.md new file mode 100644 index 0000000..9e03921 --- /dev/null +++ b/skills/flvx-api/references/types.md @@ -0,0 +1,346 @@ +# TypeScript Type Definitions + +## API Response Envelope + +```typescript +interface APIResponse { + 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 { + 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; +} +``` + +## 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 { + 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(endpoint: string, data?: object): Promise { + 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 = 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 { + const data = await this.request<{ list: Node[] }>("/api/v1/node/list", {}); + return data.list ?? []; + } + + async listForwards(): Promise { + const data = await this.request<{ list: Forward[] }>("/api/v1/forward/list", {}); + return data.list ?? []; + } + + async createForward(req: ForwardCreateRequest): Promise { + return this.request("/api/v1/forward/create", req); + } + + async getUserPackage(): Promise { + return this.request("/api/v1/user/package", {}); + } +} +``` diff --git a/skills/flvx-api/references/users.md b/skills/flvx-api/references/users.md new file mode 100644 index 0000000..7240203 --- /dev/null +++ b/skills/flvx-api/references/users.md @@ -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 + }' +```