Parse, validate, and convert proxy links & subscriptions between formats. Three interfaces — Telegram bot, web (FastAPI), CLI — sharing one core.
vless://vmess://trojan://ss://(SIP002 + legacy)ssr://hysteria2://socks://happ://crypt*(auto-decrypted)incy://crypt*(auto-decrypted)
| Format | Description |
|---|---|
singbox |
sing-box JSON config (outbounds array) |
mihomo |
mihomo/clash-meta YAML proxy list |
flclash |
Full FlClash YAML config (mixed-port, proxy-groups & rules) |
txt |
Plain text — one share link per line |
git clone <repo-url> && cd vtk
uv sync --all-extras # core + bot + web
uv sync --extra web # or: just the web interface
uv sync --extra bot # or: just the Telegram botCore (installed always): httpx, pycryptodome, cryptography, pyyaml.
Optional: [bot] — aiogram; [web] — fastapi, uvicorn, python-multipart, jinja2.
orjson is used if present (optional speedup, not required).
| Variable | Description |
|---|---|
VTK_BOT_TOKEN |
Telegram bot token (for bot interface) |
VTK_PROXY_BASE |
Base host for passthrough /p/<params>/<url> links (default https://vtk.aneeko.qzz.io) |
uv run vtk check vless://... # validate link
uv run vtk parse-cmd vless://... # parse & display fields
uv run vtk convert-cmd -l vless://... -f singbox # convert single link
uv run vtk sub https://example.com/sub -f flclash # fetch subscription
uv run vtk batch links.txt -f mihomo # batch convert file
uv run vtk extract config.json # config → share links
uv run vtk settings show # show settings
uv run vtk settings set sub_format flclash # change settingexport VTK_BOT_TOKEN=xxx
uv run python -m bot.mainCommands: /start, /help, /settings, /proxy
Features:
- Auto-detects input type (link / subscription URL / config / TXT)
- Inline keyboard for per-type output format settings
- 📱 Proxy device menu: toggles for "send device headers" + HWID, OS switch, Randomize, Clear
- Import device settings from a pasted web app-link (
https://host/p/android,ver=…,ua=…,hwid=…/https://sub…) - File upload support (documents processed same as text)
- Rate limiting (3 msg/sec per user)
happ://crypt*auto-decryptionincy://crypt*auto-decryption
uv run python -m web.main
# http://localhost:9000HTML form + JSON API endpoints:
GET /api/convert?input=...&format=singbox— convertGET /api/extract?input=...— config → share linksGET /api/check?link=...— validate single linkGET /api/device/random— random device fingerprint params (shared with bot)- Convert tab has optional device-header fields (UA/HWID/OS/Ver/Model/Locale) + "Send device headers" toggle + Randomize
- PROXY tab builds
/p/<params>/<url>app-links (host =VTK_PROXY_BASE)
core/
__init__.py — exports
logic.py — parse links, fix_link(), fetch subscriptions, extract_country()
converters.py — singbox / mihomo / flclash / txt output
reverse.py — config → share links (sing-box / mihomo YAML)
settings.py — per-input-type format defaults
happ.py — happ://crypt* offline decryptor (all RSA keys bundled)
incy.py — incy://crypt* offline decryptor (AES-256-GCM, keymat bundled)
bot/ — Telegram bot (aiogram 3)
web/ — FastAPI + HTML form + JSON API
cli/ — CLI (typer)
Normalizes proxy links for cross-client compatibility (e.g. podkop):
- Converts
&→?at query start - Adds
type=rawif missing (vless) - Normalizes
packet-encoding=→packetEncoding=
Node.validate() checks required fields for each protocol (lightweight, no external deps):
node = parse_link(link)
node.validate() # raises ParseError on critical issuesCalled automatically in parse_text_input() and iter_parse_text().
Each protocol has a to_*_link() method for serialization:
node = parse_vless(link)
link2 = node.to_vless_link() # round-tripFor large inputs, use iter_parse_text() (generator) instead of parse_text_input().
extract_country(name) detects country from flag emoji or text patterns in node names. Used by FlClash group_by_country to create per-country proxy groups.
converters.py uses a ProtocolAdapter pattern (ABC) for format-specific generation:
VLESSAdapter,VMessAdapter,TrojanAdapter,SSAdapter,Hysteria2Adapter,SSRAAdapter- Each adapter implements
to_clash_dict(),to_mihomo_dict(),to_singbox_dict(),to_link() - YAML output via PyYAML (no manual indentation)
Stored in ~/.config/vtk/settings.json. Each input type has its own default
output format: sub_format, link_format, config_format, txt_format.
{
"sub_format": "mihomo",
"link_format": "singbox",
"config_format": "txt",
"txt_format": "mihomo",
"tag_prefix": "",
"timeout": 15,
"group_by_country": false
}pytest tests/Covers: fix_link normalization, all protocol parsers, all converters, round-trip, country extraction, error handling.
uv sync --all-extras # runtime + dev group
uv run pytest -q # tests
uv run ruff check . # lint
uvx pre-commit run --all-files # git hooks (ruff)CI (GitHub Actions): ruff + pytest on Python 3.10 & 3.13 + pip-audit. Dependabot watches pip and github-actions weekly.
crypt5_keys.json— RSA private keys for thehapp://crypt5offline decryptorincy_keymat.json— AES-256-GCM key material for theincy://crypt1decryptorincy_vectors.json— test vectors generated by the official@incy/link-encoderpackage (seescripts/gen-vectors.mjs)
These files ship inside the wheel (setuptools package-data) — the decryptors work fully offline.