feat: add pool editing UI and agent guidance
This commit is contained in:
271
AGENTS.md
Normal file
271
AGENTS.md
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This repo is a small Python Flask service that converts subscription links (`vmess`, `vless`, `trojan`, `ss`) into local SOCKS5 proxies via `xray-core`.
|
||||||
|
Almost all backend logic, API routes, and the inline web UI live in `main.py`.
|
||||||
|
Agents should prefer minimal, in-place changes over architectural rewrites.
|
||||||
|
|
||||||
|
## Repository Layout
|
||||||
|
|
||||||
|
- `main.py`: app entrypoint, dataclasses, parsers, proxy manager, schedulers, Flask routes, and embedded HTML/CSS/JS.
|
||||||
|
- `requirements.txt`: runtime dependencies.
|
||||||
|
- `start.sh`: local bootstrap script.
|
||||||
|
- `Dockerfile`: image build.
|
||||||
|
- `docker-compose.yml`: recommended container run path.
|
||||||
|
- `config.yaml.example`: optional config sample.
|
||||||
|
- `config/`: persisted JSON state.
|
||||||
|
- `data/`: runtime files such as generated xray configs.
|
||||||
|
- `bin/`: xray binary/assets.
|
||||||
|
|
||||||
|
## Extra Agent Rules
|
||||||
|
|
||||||
|
I checked for repo-specific agent instruction files:
|
||||||
|
|
||||||
|
- `.cursor/rules/`: not present.
|
||||||
|
- `.cursorrules`: not present.
|
||||||
|
- `.github/copilot-instructions.md`: not present.
|
||||||
|
|
||||||
|
There are no Cursor or Copilot rules to merge into this file right now.
|
||||||
|
|
||||||
|
## Environment
|
||||||
|
|
||||||
|
- Python: README says `3.10+`.
|
||||||
|
- Docker image: `python:3.13-slim`.
|
||||||
|
- Default web port: `8080`.
|
||||||
|
- App downloads `xray-core` on startup if `bin/xray` is missing.
|
||||||
|
- Runtime state is persisted under `config/` and `data/`.
|
||||||
|
|
||||||
|
## Setup Commands
|
||||||
|
|
||||||
|
### Local environment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run locally
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run via helper script
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x start.sh
|
||||||
|
./start.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build Docker image
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t airtosocks .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run with Docker Compose
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
docker-compose logs -f
|
||||||
|
docker-compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build, Lint, And Test Reality
|
||||||
|
|
||||||
|
This repository currently has no formal lint or test setup.
|
||||||
|
|
||||||
|
- No `tests/` directory was found.
|
||||||
|
- No `pytest` or `unittest` test files were found.
|
||||||
|
- No `ruff`, `black`, `isort`, `flake8`, `pylint`, `mypy`, `tox`, or `nox` config was found.
|
||||||
|
|
||||||
|
Agents should say this explicitly instead of pretending a missing command exists.
|
||||||
|
|
||||||
|
## Verification Commands That Work Today
|
||||||
|
|
||||||
|
### Syntax check
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m py_compile main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compile Python files
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m compileall .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual app run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### HTTP smoke checks
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://127.0.0.1:8080/api/links
|
||||||
|
curl http://127.0.0.1:8080/api/subscriptions
|
||||||
|
curl http://127.0.0.1:8080/api/random
|
||||||
|
```
|
||||||
|
|
||||||
|
### Container smoke checks
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
curl http://127.0.0.1:8080/api/links
|
||||||
|
docker-compose logs --tail=200
|
||||||
|
docker-compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
## Single-Test Guidance
|
||||||
|
|
||||||
|
There is no real single-test command yet because there is no test suite.
|
||||||
|
|
||||||
|
If tests are added later, prefer `pytest` and document commands like:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pytest
|
||||||
|
pytest tests/test_parser.py
|
||||||
|
pytest tests/test_parser.py::test_parse_vmess
|
||||||
|
```
|
||||||
|
|
||||||
|
Until then, a “single test” means a targeted manual verification of the exact parser, route, or runtime behavior touched by the change.
|
||||||
|
|
||||||
|
## Config Notes
|
||||||
|
|
||||||
|
Optional runtime config lives in `config.yaml` and currently supports:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
port: 8080
|
||||||
|
check_interval: 600
|
||||||
|
```
|
||||||
|
|
||||||
|
When changing config behavior:
|
||||||
|
|
||||||
|
- Preserve compatibility for missing keys.
|
||||||
|
- Keep defaults aligned across code, `README.md`, and `config.yaml.example`.
|
||||||
|
- Do not silently rename config fields.
|
||||||
|
|
||||||
|
## Code Style
|
||||||
|
|
||||||
|
The repo does not use an autoformatter today. Match the existing style unless the user asks for cleanup.
|
||||||
|
|
||||||
|
### Imports
|
||||||
|
|
||||||
|
- Keep standard library imports before third-party imports.
|
||||||
|
- Do not reorder the whole import block for cosmetic reasons.
|
||||||
|
- Add new imports near related imports.
|
||||||
|
- Prefer explicit imports over wildcard imports.
|
||||||
|
|
||||||
|
### Formatting
|
||||||
|
|
||||||
|
- Follow normal Python/PEP 8 conventions without broad reformatting.
|
||||||
|
- Use 4-space indentation.
|
||||||
|
- Preserve surrounding quote style; Python code here mostly uses single quotes.
|
||||||
|
- Avoid reflowing large inline HTML, CSS, or JS unless required.
|
||||||
|
- Keep diffs small and local.
|
||||||
|
|
||||||
|
### Types
|
||||||
|
|
||||||
|
- Continue the existing `typing` style: `Optional`, `List`, `Dict`, `Tuple`, `Any`.
|
||||||
|
- Add type hints for new helpers when practical.
|
||||||
|
- Use `@dataclass` for record-like state models.
|
||||||
|
- Do not introduce a new validation or typing framework without a concrete need.
|
||||||
|
|
||||||
|
### Naming
|
||||||
|
|
||||||
|
- `snake_case` for variables, functions, and methods.
|
||||||
|
- `PascalCase` for classes.
|
||||||
|
- `UPPER_SNAKE_CASE` for module constants.
|
||||||
|
- Use descriptive domain names like `subscription_id`, `socks_port`, `last_check`, `available_count`.
|
||||||
|
|
||||||
|
### Data Modeling And Persistence
|
||||||
|
|
||||||
|
- Persist structured state through dataclasses and `asdict(...)`.
|
||||||
|
- New persisted fields need safe defaults so old JSON continues to load.
|
||||||
|
- Consider compatibility with existing files in `config/`.
|
||||||
|
- Do not casually rename persisted fields.
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
|
||||||
|
- Follow the existing pattern: catch failures near parsing, file I/O, network calls, and subprocess operations.
|
||||||
|
- Use `logger.error(...)` or `logger.warning(...)`, then return a safe fallback or JSON error.
|
||||||
|
- Avoid crashing daemon threads for recoverable problems.
|
||||||
|
- Prefer specific exceptions when obvious, but broad `except Exception as e` is already common here.
|
||||||
|
- API handlers should return meaningful JSON error payloads with sensible HTTP status codes.
|
||||||
|
|
||||||
|
### Logging
|
||||||
|
|
||||||
|
- Use the module-level `logger`, not `print`.
|
||||||
|
- Keep log messages short and operational.
|
||||||
|
- Include useful identifiers such as node name, subscription name, or port.
|
||||||
|
- Do not log secrets or full credentials from proxy links unless absolutely necessary.
|
||||||
|
|
||||||
|
### Concurrency And Shared State
|
||||||
|
|
||||||
|
- `ProxyManager` protects mutable state with `threading.RLock`; preserve that discipline.
|
||||||
|
- Be careful when changing `nodes`, `subscriptions`, `pools`, `used_ports`, or `xray_processes`.
|
||||||
|
- Keep background threads daemonized when matching the current pattern.
|
||||||
|
- Do not introduce async or heavier concurrency models for small tasks.
|
||||||
|
|
||||||
|
### Files, Processes, And Runtime Data
|
||||||
|
|
||||||
|
- Runtime files belong under `config/` or `data/`.
|
||||||
|
- Keep xray config generation deterministic and JSON-serializable.
|
||||||
|
- Follow the existing `subprocess.Popen(...)` pattern for xray processes.
|
||||||
|
- Release ports and clean up subprocess state on failure paths.
|
||||||
|
|
||||||
|
### Flask And API Conventions
|
||||||
|
|
||||||
|
- Keep Flask route handlers thin.
|
||||||
|
- Put behavior in `ProxyManager` or small helpers rather than in the route body.
|
||||||
|
- Return `jsonify(...)` for API responses.
|
||||||
|
- Validate request JSON defensively.
|
||||||
|
- Keep API response shapes stable unless a breaking change is explicitly requested.
|
||||||
|
|
||||||
|
### Frontend Conventions
|
||||||
|
|
||||||
|
- The frontend is intentionally inline inside the `/` route.
|
||||||
|
- For small UI changes, edit the embedded HTML/JS directly instead of adding a frontend build system.
|
||||||
|
- Preserve the current fetch-based interaction pattern.
|
||||||
|
- Keep the existing visual language unless the task asks for redesign work.
|
||||||
|
|
||||||
|
### Comments And Docs
|
||||||
|
|
||||||
|
- Existing comments/docstrings are short and often Chinese; match the surrounding language in touched code.
|
||||||
|
- Add comments only when behavior is not obvious from the code.
|
||||||
|
- Update `README.md`, `config.yaml.example`, and this file when commands or behavior change.
|
||||||
|
|
||||||
|
## Change Strategy For Agents
|
||||||
|
|
||||||
|
- Prefer surgical edits.
|
||||||
|
- Avoid splitting `main.py` into modules unless explicitly requested.
|
||||||
|
- Respect persisted data compatibility.
|
||||||
|
- Ignore unrelated runtime files unless the task specifically involves them.
|
||||||
|
- If you add tests or lint tooling, immediately document the new commands here.
|
||||||
|
|
||||||
|
## Recommended Post-Change Checks
|
||||||
|
|
||||||
|
For most Python-only changes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m py_compile main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
For API behavior changes:
|
||||||
|
|
||||||
|
1. Start the app with `python3 main.py`.
|
||||||
|
2. Hit the affected endpoint with `curl`.
|
||||||
|
3. Confirm there is no traceback and the response shape is correct.
|
||||||
|
|
||||||
|
For Docker-related changes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t airtosocks .
|
||||||
|
docker-compose up -d
|
||||||
|
curl http://127.0.0.1:8080/api/links
|
||||||
|
docker-compose down
|
||||||
|
```
|
||||||
54
main.py
54
main.py
@@ -1067,6 +1067,7 @@ def index():
|
|||||||
<div id="poolSelectedNodes"></div>
|
<div id="poolSelectedNodes"></div>
|
||||||
<div style="margin-bottom: 15px;">
|
<div style="margin-bottom: 15px;">
|
||||||
<button class="btn btn-success" onclick="createPool()" id="btnCreatePool" disabled>创建节点池</button>
|
<button class="btn btn-success" onclick="createPool()" id="btnCreatePool" disabled>创建节点池</button>
|
||||||
|
<button class="btn" onclick="resetPoolEditor()" id="btnCancelPoolEdit" style="display:none">取消修改</button>
|
||||||
<span id="poolSelectedCount" style="color: #888; margin-left: 10px;">已选 0 个节点</span>
|
<span id="poolSelectedCount" style="color: #888; margin-left: 10px;">已选 0 个节点</span>
|
||||||
</div>
|
</div>
|
||||||
<h3 style="color: #00d9ff; margin-bottom: 10px;">已有节点池</h3>
|
<h3 style="color: #00d9ff; margin-bottom: 10px;">已有节点池</h3>
|
||||||
@@ -1324,6 +1325,7 @@ def index():
|
|||||||
// ===== 节点池管理 =====
|
// ===== 节点池管理 =====
|
||||||
let poolSearchResults = [];
|
let poolSearchResults = [];
|
||||||
let poolSelectedIds = new Set();
|
let poolSelectedIds = new Set();
|
||||||
|
let editingPoolId = null;
|
||||||
|
|
||||||
async function searchPoolNodes() {
|
async function searchPoolNodes() {
|
||||||
const q = document.getElementById('poolSearch').value.trim();
|
const q = document.getElementById('poolSearch').value.trim();
|
||||||
@@ -1416,6 +1418,40 @@ def index():
|
|||||||
function updatePoolSelectedCount() {
|
function updatePoolSelectedCount() {
|
||||||
document.getElementById('poolSelectedCount').textContent = `已选 ${poolSelectedIds.size} 个节点`;
|
document.getElementById('poolSelectedCount').textContent = `已选 ${poolSelectedIds.size} 个节点`;
|
||||||
document.getElementById('btnCreatePool').disabled = poolSelectedIds.size === 0;
|
document.getElementById('btnCreatePool').disabled = poolSelectedIds.size === 0;
|
||||||
|
document.getElementById('btnCreatePool').textContent = editingPoolId ? '保存修改' : '创建节点池';
|
||||||
|
document.getElementById('btnCancelPoolEdit').style.display = editingPoolId ? 'inline-block' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetPoolEditor() {
|
||||||
|
editingPoolId = null;
|
||||||
|
poolSelectedIds.clear();
|
||||||
|
poolSearchResults = [];
|
||||||
|
document.getElementById('poolName').value = '';
|
||||||
|
document.getElementById('poolSearch').value = '';
|
||||||
|
document.getElementById('poolSearchResults').innerHTML = '';
|
||||||
|
document.getElementById('poolSelectedNodes').innerHTML = '';
|
||||||
|
updatePoolSelectedCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function editPool(id) {
|
||||||
|
try {
|
||||||
|
const resp = await fetch('/api/pools');
|
||||||
|
const pools = await resp.json();
|
||||||
|
const pool = pools.find(p => p.id === id);
|
||||||
|
if (!pool) {
|
||||||
|
return showResult('节点池不存在', 'info');
|
||||||
|
}
|
||||||
|
editingPoolId = id;
|
||||||
|
poolSelectedIds = new Set(pool.node_ids || []);
|
||||||
|
document.getElementById('poolName').value = pool.name || '';
|
||||||
|
document.getElementById('poolSearch').value = '';
|
||||||
|
document.getElementById('poolSearchResults').innerHTML = '<p style="color:#888">已加载节点池,请继续搜索并调整节点选择</p>';
|
||||||
|
renderSelectedNodes();
|
||||||
|
updatePoolSelectedCount();
|
||||||
|
window.scrollTo({top: document.getElementById('poolName').offsetTop - 80, behavior: 'smooth'});
|
||||||
|
} catch (e) {
|
||||||
|
showResult('加载节点池失败: ' + e.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createPool() {
|
async function createPool() {
|
||||||
@@ -1423,23 +1459,20 @@ def index():
|
|||||||
if (!name) return showResult('请输入池名称', 'info');
|
if (!name) return showResult('请输入池名称', 'info');
|
||||||
if (poolSelectedIds.size === 0) return showResult('请先搜索并选择节点', 'info');
|
if (poolSelectedIds.size === 0) return showResult('请先搜索并选择节点', 'info');
|
||||||
try {
|
try {
|
||||||
const resp = await fetch('/api/pools', {
|
const isEditing = !!editingPoolId;
|
||||||
method: 'POST',
|
const url = isEditing ? `/api/pools/${encodeURIComponent(editingPoolId)}` : '/api/pools';
|
||||||
|
const resp = await fetch(url, {
|
||||||
|
method: isEditing ? 'PUT' : 'POST',
|
||||||
headers: {'Content-Type': 'application/json'},
|
headers: {'Content-Type': 'application/json'},
|
||||||
body: JSON.stringify({name, node_ids: [...poolSelectedIds]})
|
body: JSON.stringify({name, node_ids: [...poolSelectedIds]})
|
||||||
});
|
});
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
if (!resp.ok) throw new Error(data.error);
|
if (!resp.ok) throw new Error(data.error);
|
||||||
showResult(`节点池"${name}"创建成功, ${data.node_ids.length} 个节点`, 'success');
|
showResult(`节点池"${name}"${isEditing ? '修改' : '创建'}成功, ${data.node_ids.length} 个节点`, 'success');
|
||||||
document.getElementById('poolName').value = '';
|
resetPoolEditor();
|
||||||
document.getElementById('poolSearch').value = '';
|
|
||||||
document.getElementById('poolSearchResults').innerHTML = '';
|
|
||||||
document.getElementById('poolSelectedNodes').innerHTML = '';
|
|
||||||
poolSelectedIds.clear();
|
|
||||||
updatePoolSelectedCount();
|
|
||||||
loadPools();
|
loadPools();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showResult('创建失败: ' + e.message);
|
showResult((editingPoolId ? '修改失败: ' : '创建失败: ') + e.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1488,6 +1521,7 @@ def index():
|
|||||||
<div class="api-url" style="margin-top:4px;font-size:12px">${apiUrl}</div>
|
<div class="api-url" style="margin-top:4px;font-size:12px">${apiUrl}</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn" onclick="getRandomFromPool('${p.id}')" style="margin-left:10px;padding:8px 12px">随机获取</button>
|
<button class="btn" onclick="getRandomFromPool('${p.id}')" style="margin-left:10px;padding:8px 12px">随机获取</button>
|
||||||
|
<button class="btn btn-success" onclick="editPool('${p.id}')" style="margin-left:5px;padding:8px 12px">修改</button>
|
||||||
<button class="btn btn-danger" onclick="deletePool('${p.id}')" style="margin-left:5px;padding:8px 12px">删除</button>
|
<button class="btn btn-danger" onclick="deletePool('${p.id}')" style="margin-left:5px;padding:8px 12px">删除</button>
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|||||||
Reference in New Issue
Block a user