mirror of
https://github.com/honmashironeko/ProxyCat.git
synced 2026-10-24 08:44:26 +00:00
更新请求逻辑变更
This commit is contained in:
1 parent
b45228188f
commit
6cf318735d
20 files changed
+582
-85
No files matched your search
@@ -139,6 +139,7 @@ The pool's adapter layer parses parameters from the **handler function's signatu
|
||||
| Access record export | At most 50000 records per call | The truncation flag exists only in the JSON metadata (`truncated`, `true` once the count reaches the limit); the CSV carries no such flag |
|
||||
| Pool `page_size` / `limit` / export | All bound by the 50000 cap | When `limit` is present, `page` is forced to 1, equivalent to "take the first `limit` rows" |
|
||||
| `GET /api/version` | Reads a cache, **makes no request** | The check is done at process start and by a background task every 24 hours |
|
||||
| `POST /api/version/check` | **Blocks the current web thread** until the check finishes (up to ~15 s) | Concurrent calls merge into the one in flight; resets the 24-hour timer |
|
||||
|
||||
## Blocking and Concurrency
|
||||
|
||||
@@ -897,8 +898,8 @@ Stores multiple credential sets for the upstream proxy-fetch endpoint and design
|
||||
|
||||
## Version Check and Ads
|
||||
|
||||
> **The endpoints in this section require no authentication**; even when a token is configured,
|
||||
> you do not need to send it.
|
||||
> **Except for `POST /api/version/check`, the endpoints in this section require no authentication**;
|
||||
> even when a token is configured, you do not need to send it.
|
||||
|
||||
### Check Version (GET /api/version)
|
||||
|
||||
@@ -931,6 +932,60 @@ never makes an external request** — calling it never incurs a network wait.
|
||||
situation no longer happens.
|
||||
- The check runs on a background thread: it does not block startup, and it does not occupy a web
|
||||
worker thread.
|
||||
- When you need the answer right now, use `POST /api/version/check` below.
|
||||
|
||||
**The version comes from the GitHub repository's release history.** By default three addresses are
|
||||
requested concurrently and **the first one to return a usable version number wins**:
|
||||
|
||||
| Address | Notes |
|
||||
|---|---|
|
||||
| `github.com/…/releases.atom` | The official release feed; no authentication, no rate limit |
|
||||
| `gh-proxy.com/https://api.github.com/…` | A third-party mirror with better reachability from mainland China; it sees your request IP and may return a cached, older version |
|
||||
| `api.github.com/repos/…/releases` | The official API; 60 requests per hour per IP when unauthenticated |
|
||||
|
||||
Concurrent rather than sequential retries: a blocked or timing-out address does not slow the
|
||||
whole thing down — a usable address usually answers within a second, and when all three fail the
|
||||
call returns after roughly 8 seconds. Requests keep httpx's default behaviour of **honouring the
|
||||
`HTTP(S)_PROXY` environment variables**.
|
||||
|
||||
When the [`version_check_url`](ProxyCat-Manual/Configuration-EN.md#advanced-options) config option
|
||||
is non-empty, **that address is the only source** and the three built-ins are not requested. The
|
||||
version is taken as the `ProxyCat[-_]v?digits.digits[.digits]` prefix of a tag, so suffixes such as
|
||||
`-测试版V5` are dropped.
|
||||
|
||||
### Check Version Now (POST /api/version/check)
|
||||
|
||||
**Requires authentication** — unlike the read-only cached GET above, this endpoint really does
|
||||
make an external request.
|
||||
|
||||
No parameters, no request body. **The current web thread waits until the check finishes** (up to
|
||||
about 15 seconds). Only one check runs at a time — concurrent calls wait for the one in flight
|
||||
instead of each firing their own round of requests.
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"current_version": "ProxyCat-V3.0.0",
|
||||
"latest_version": "ProxyCat-V3.0.0",
|
||||
"is_latest": true,
|
||||
"source": "github.com",
|
||||
"checked_at": "2026-10-06 18:39:42"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Description |
|
||||
|---|---|
|
||||
| `status` | The outcome of **this check** — the key difference from the GET |
|
||||
| `source` | Hostname of the winning address; present on success only |
|
||||
| `checked_at` | The time of this check |
|
||||
| `latest_version` / `is_latest` | Present whenever a successful version is held in memory |
|
||||
| `message` | Why **this check** failed; present on failure only |
|
||||
|
||||
**When this check fails but an earlier one succeeded**, `status` is `error` and carries a
|
||||
`message`, while still including the previous successful `latest_version`. That is exactly how it
|
||||
differs from the GET: the GET reports "last known state", this endpoint reports "did *this* one
|
||||
succeed". Both write the time of the check into `logs/version_check.json`, so **a manual check
|
||||
also resets the 24-hour timer**.
|
||||
|
||||
**When there is neither a successful result nor a failure record** (the first check after startup
|
||||
has not finished yet), it returns:
|
||||
@@ -1899,7 +1954,7 @@ curl -X POST "http://localhost:5001/api/pool/import?token=YOUR_TOKEN" \
|
||||
| GET | `/web` | Yes | Panel single page |
|
||||
| GET | `/static/<path>` | No | Frontend static assets |
|
||||
|
||||
## Panel API (33)
|
||||
## Panel API (34)
|
||||
|
||||
| Method | Path | Description |
|
||||
|---|---|---|
|
||||
@@ -1933,6 +1988,7 @@ curl -X POST "http://localhost:5001/api/pool/import?token=YOUR_TOKEN" \
|
||||
| GET | `/api/api_credentials` | Read upstream API credentials |
|
||||
| POST | `/api/api_credentials` | Manage upstream API credentials |
|
||||
| GET | `/api/version` | Version check result (read-only cache, **no authentication**) |
|
||||
| POST | `/api/version/check` | Trigger a version check immediately (**requires authentication**) |
|
||||
| GET | `/api/ads` | Get ads (**no authentication**) |
|
||||
| POST | `/api/ads/dismiss` | Dismiss ads (**no authentication**) |
|
||||
| POST | `/api/ads/reopen` | Reopen ads (**no authentication**) |
|
||||
|
||||
@@ -133,6 +133,7 @@ token 在 `config/config.ini` 的 `[Server] token`。**校验只读查询串参
|
||||
| 访问记录导出 | 单次最多 50000 条 | 被截断的标记只有 JSON 格式的元信息里有(`truncated`,条数达到上限即为 `true`),CSV 没有该标记 |
|
||||
| 代理池 `page_size` / `limit` / 导出 | 统一受 50000 约束 | `limit` 存在时 `page` 被强制为 1,等效「取前 limit 条」 |
|
||||
| `GET /api/version` | 只读缓存,**不发起请求** | 检查由进程启动时与每 24 小时的后台任务完成 |
|
||||
| `POST /api/version/check` | **会阻塞当前 Web 线程**至本次检查结束(最长约 15 秒) | 并发调用合并到正在跑的那一次;会重置 24 小时计时 |
|
||||
|
||||
## 阻塞与并发
|
||||
|
||||
@@ -888,7 +889,7 @@ IP 的黑白名单校验。
|
||||
|
||||
## 版本检查与广告
|
||||
|
||||
> **本节端点无鉴权**,配置了 token 也无需携带。
|
||||
> **除 `POST /api/version/check` 外,本节端点无鉴权**,配置了 token 也无需携带。
|
||||
|
||||
### 检查版本(GET /api/version)
|
||||
|
||||
@@ -918,6 +919,53 @@ IP 的黑白名单校验。
|
||||
- **无论成败都记录时间**,因此对外最多 24 小时发起一次请求;断网时不会再出现
|
||||
「每次打开面板都要等一次超时」的情况。
|
||||
- 检查在后台线程里做,不阻塞启动,也不占用 Web 工作线程。
|
||||
- 需要立刻知道结果时用下面的 `POST /api/version/check`。
|
||||
|
||||
**版本取自 GitHub 仓库的发布记录**,默认并发请求三个地址,**谁先返回可用的版本号就用谁**:
|
||||
|
||||
| 地址 | 说明 |
|
||||
|---|---|
|
||||
| `github.com/…/releases.atom` | 官方发布源,无需认证、无频率限制 |
|
||||
| `gh-proxy.com/https://api.github.com/…` | 第三方镜像,国内可达性更好;会看到请求 IP,且可能返回缓存的旧版本 |
|
||||
| `api.github.com/repos/…/releases` | 官方 API,未认证时每 IP 每小时限 60 次 |
|
||||
|
||||
并发而非顺序重试:被墙或超时的地址不会拖慢整体,可用时通常 1 秒内返回;三者全部失败时
|
||||
约 8 秒返回。请求沿用 httpx 默认行为,**读取 `HTTP(S)_PROXY` 环境变量**。
|
||||
|
||||
配置项 [`version_check_url`](ProxyCat-Manual/Configuration.md#高级选项) 非空时**只用该地址**,
|
||||
内置的三个源不再请求。版本号从标签里取 `ProxyCat[-_]v?数字.数字[.数字]` 这一段前缀,
|
||||
`-测试版V5` 之类的后缀被丢弃。
|
||||
|
||||
### 立即检查版本(POST /api/version/check)
|
||||
|
||||
**需鉴权**(与上面只读缓存的 GET 相反:本端点会真的发起外部请求)。
|
||||
|
||||
无参数、无请求体。**当前 Web 线程会等到本次检查结束才返回**(最长约 15 秒),
|
||||
同一时刻只有一次检查在跑 —— 并发调用会等正在跑的那一次,而不是各自再发一轮请求。
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"current_version": "ProxyCat-V3.0.0",
|
||||
"latest_version": "ProxyCat-V3.0.0",
|
||||
"is_latest": true,
|
||||
"source": "github.com",
|
||||
"checked_at": "2026-10-06 18:39:42"
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 说明 |
|
||||
|---|---|
|
||||
| `status` | **本次检查**的成败,这是与 GET 的关键差别 |
|
||||
| `source` | 本次胜出的地址主机名;仅成功时出现 |
|
||||
| `checked_at` | 本次检查的时间 |
|
||||
| `latest_version` / `is_latest` | 只要内存里有成功的版本号就会出现 |
|
||||
| `message` | **本次失败**的原因,仅失败时出现 |
|
||||
|
||||
**本次检查失败、但此前成功过**时,`status` 为 `error`、带 `message`,同时仍带上一次成功
|
||||
的 `latest_version`。这正是它与 GET 的区别:GET 只报「最后已知状态」,本端点报「这一次
|
||||
成不成」。两者都会把本次检查的时间写进 `logs/version_check.json`,因此**手动检查同样会
|
||||
重置 24 小时的计时**。
|
||||
|
||||
**既没有成功结果、也没有失败记录时**(启动后第一次检查还没跑完)返回:
|
||||
|
||||
@@ -1851,7 +1899,7 @@ curl -X POST "http://localhost:5001/api/pool/import?token=YOUR_TOKEN" \
|
||||
| GET | `/web` | 是 | 面板单页 |
|
||||
| GET | `/static/<path>` | 否 | 前端静态资源 |
|
||||
|
||||
## 面板接口(33)
|
||||
## 面板接口(34)
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|---|---|---|
|
||||
@@ -1885,6 +1933,7 @@ curl -X POST "http://localhost:5001/api/pool/import?token=YOUR_TOKEN" \
|
||||
| GET | `/api/api_credentials` | 读上游 API 凭据 |
|
||||
| POST | `/api/api_credentials` | 管理上游 API 凭据 |
|
||||
| GET | `/api/version` | 版本检查结果(只读缓存,**免鉴权**) |
|
||||
| POST | `/api/version/check` | 立即触发一次版本检查(**需鉴权**) |
|
||||
| GET | `/api/ads` | 获取广告(**免鉴权**) |
|
||||
| POST | `/api/ads/dismiss` | 关闭广告(**免鉴权**) |
|
||||
| POST | `/api/ads/reopen` | 重新开启广告(**免鉴权**) |
|
||||
|
||||
@@ -21,7 +21,7 @@ The panel's settings forms use the same grouping: the common `[Server]` options
|
||||
2. **The inline comments in `config.ini`** — the most complete, one Chinese sentence per key.
|
||||
3. **`GET /api/pool/schema`** — the machine-readable form of the `[Pool]` section (the panel's Pool Settings form is rendered from it).
|
||||
|
||||
Some of these options directly determine which external addresses the program sends requests to (e.g. `test_url`, `api_proxy_url`, `pool_remote_url`, `validator.*_apis`, `validator.host_public_ip`); for the full list see [Outbound Network Requests](Features-EN.md#outbound-network-requests).
|
||||
Some of these options directly determine which external addresses the program sends requests to (e.g. `test_url`, `api_proxy_url`, `pool_remote_url`, `version_check_url`, `validator.*_apis`, `validator.host_public_ip`); for the full list see [Outbound Network Requests](Features-EN.md#outbound-network-requests).
|
||||
|
||||
> Note: the comments inside `config.ini` are in **Chinese**; only the panel's field descriptions are rendered per language.
|
||||
|
||||
@@ -190,6 +190,7 @@ The panel's Proxy Config → Advanced group: the cooldowns, caching and internal
|
||||
| `log_backup_count` | `3` | Number of historical files kept after rotation; at least 1 (`0` would make logs never rotate) | Needs a full application restart |
|
||||
| `proxy_failure_cooldown` | `3` | Folding window for failure warnings (seconds): warnings of the same kind are written to the log once per window, with a record of how many were folded. Under high concurrency failures arrive in batches — without folding, one sentence would flood the log. Folding **happens at the log layer only**; an exit's accounting and scheduling are unaffected (they look at the recent failure share, not a timer) | Immediately |
|
||||
| `display_level` | `1` | Console verbosity: `0` shows only exit roster changes and error messages, `1` shows the exit roster with per-exit state and the countdown, `2` shows all detailed information (the behaviour of `2` and `3` is identical item by item, and a `3` is normalised to `2`); `2` appends an explanation of each level after the startup banner. It only affects output when running the `ProxyCat.py` command-line entry | Immediately (affects only the command-line entry's output) |
|
||||
| `version_check_url` | empty | The source address for the version check. When **empty**, three built-in addresses are requested concurrently (the official `releases.atom`, the `gh-proxy.com` mirror and the official API) and the first usable version number wins; when **set**, that address is the only source and the three built-ins are not requested. It may be a GitHub API, a `releases.atom` or any page a version number can be parsed out of, and must be a full http/https URL (otherwise the save is rejected). Note that the built-in third-party mirror sees your request IP and may return a cached, older version, and that the unauthenticated official API allows 60 requests per hour per IP | Immediately (used by the next check) |
|
||||
|
||||
### Access Control and UI Language
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
2. **`config.ini` 的行内注释** —— 最全,每个键都有一句中文说明。
|
||||
3. **`GET /api/pool/schema`** —— `[Pool]` 段的机器可读版(面板的「池设置」表单就由它渲染)。
|
||||
|
||||
这些配置项里有一批直接决定程序会往哪些外部地址发请求(例如 `test_url`、`api_proxy_url`、`pool_remote_url`、`validator.*_apis`、`validator.host_public_ip`),完整清单见 [功能特性里的「对外网络请求」](Features.md#对外网络请求)。
|
||||
这些配置项里有一批直接决定程序会往哪些外部地址发请求(例如 `test_url`、`api_proxy_url`、`pool_remote_url`、`version_check_url`、`validator.*_apis`、`validator.host_public_ip`),完整清单见 [功能特性里的「对外网络请求」](Features.md#对外网络请求)。
|
||||
|
||||
> 注:`config.ini` 里的注释是**中文**;面板里的字段说明才会按中英文分别渲染。
|
||||
|
||||
@@ -191,6 +191,7 @@
|
||||
| `log_backup_count` | `3` | 轮转后保留的历史文件份数;至少 1(`0` 会让日志永不轮转) | 需重启整个应用 |
|
||||
| `proxy_failure_cooldown` | `3` | 故障告警的折叠窗口(秒):同一类告警在窗口内只写一条日志,并记下被折叠了多少条。高并发下失败是成批来的,不折叠日志会被同一句话刷满。折叠**只发生在日志层**,出口的记账与调度不受影响(那套看的是近期失败占比,不靠计时器) | 即时 |
|
||||
| `display_level` | `1` | 控制台详细程度:`0` 仅显示出口名册变化与错误信息;`1` 显示出口名册、各出口状态、倒计时与错误信息;`2` 显示所有详细信息(`2` 与 `3` 的行为逐条相同,填 `3` 会被归一成 `2`);`2` 会在启动横幅后附上各级别说明。只影响命令行运行 `ProxyCat.py` 时的输出 | 即时(仅作用于命令行入口的输出) |
|
||||
| `version_check_url` | 空 | 版本检查的源地址。**留空**时并发请求三个内置地址(官方 `releases.atom`、`gh-proxy.com` 镜像、官方 API),谁先返回可用的版本号就用谁;**填写后只用该地址**,内置的三个不再请求。可填 GitHub API、`releases.atom` 或任何能解析出版本号的页面,必须是 http/https 开头的完整地址(否则保存被拒)。注意内置源里的第三方镜像会看到你的请求 IP、且可能返回缓存的旧版本;官方 API 未认证时每 IP 每小时限 60 次 | 即时(下次检查时使用) |
|
||||
|
||||
### 访问控制与界面语言
|
||||
|
||||
|
||||
@@ -77,9 +77,13 @@ below; the complete plugin authoring contract is in
|
||||
multiple sets and switched with one click.
|
||||
- **Containerized Deployment**: one-command Docker deployment, with a built-in health check that
|
||||
probes the actual panel port.
|
||||
- **Version Check**: compares against the latest release once at startup and then every 24 hours;
|
||||
- **Version Check**: reads the latest release from the GitHub release history, requesting the
|
||||
official release feed, a mirror reachable from mainland China and the official API
|
||||
concurrently — **the first answer wins**. It compares once at startup and then every 24 hours;
|
||||
the result is persisted, so restarting within 24 hours neither re-checks nor makes opening the
|
||||
panel wait on the network.
|
||||
panel wait on the network. The button next to the version chip in the sidebar **checks
|
||||
immediately**, showing the result in place (including the reason when it fails). The address can
|
||||
be replaced with a mirror of your own via `version_check_url`.
|
||||
|
||||
A tab-by-tab walkthrough of the panel's four tabs (Proxy Config / Proxy Pool / Access Control /
|
||||
Logs), with screenshots, is in the [Operation Manual](Operation%20Manual-EN.md).
|
||||
@@ -429,7 +433,7 @@ how to stop it. Direct connections are listed first.
|
||||
|
||||
| Trigger | Destination | Direct or through the upstream proxy | Related configuration | How to disable |
|
||||
|---|---|---|---|---|
|
||||
| A background thread starts with either entry point (`app.py`, `ProxyCat.py`) and fetches immediately when 24 hours have passed since the last attempt, when it has never checked, or when the version persisted in its state file differs from the running one; after that it ticks every 600 s and sends at most one request per 24 hours. The 24 hours counts from the last attempt, not the last success — a failed attempt still counts. | `y.shironekosan.cn` — the version announcement page on the author's site, hardcoded; the result is persisted to `logs/version_check.json`. | **Direct** | None — the URL is hardcoded in `modules/version_check.py`. | No switch at all; block the domain at the network layer (hosts file, firewall or DNS). The request uses httpx's default client (`trust_env=True`), so an `HTTP(S)_PROXY` environment variable routes it through that proxy — environment behaviour, not a ProxyCat setting. |
|
||||
| Version check: a background thread reads the version from the GitHub repository's release history and persists it to `logs/version_check.json`. It **requests three built-in addresses concurrently and takes the first usable version number** (the official release feed needs no authentication and has no rate limit; the mirror is more reachable from mainland China but sees your request IP and may return a cached, older version; the official API allows 60 requests per hour per IP unauthenticated); when all three fail it returns after roughly 8 s. A background thread starts with either entry point (`app.py`, `ProxyCat.py`) and fetches immediately when 24 hours have passed since the last attempt, when it has never checked, or when the version persisted in its state file differs from the running one; after that it ticks every 600 s and sends at most one request per 24 hours. The 24 hours counts from the last attempt, not the last success — a failed attempt still counts. The refresh button in the panel sidebar, or `POST /api/version/check`, forces an immediate check and likewise resets the 24-hour timer. | `github.com` (releases.atom), `gh-proxy.com`, `api.github.com` — the last two being a mirror and the official API. | **Direct** | `version_check_url` (`[Server]`, panel "Proxy Configuration → Advanced"): empty uses the three built-ins, a value makes **that address the only source** and can point at a mirror of your own. | Point it at an address of your own to replace the built-in third-party mirror; to stop the request entirely, point it at an unreachable address and block it at the network layer, or stop the process. The request uses httpx's default client (`trust_env=True`), so an `HTTP(S)_PROXY` environment variable routes it through that proxy — environment behaviour, not a ProxyCat setting. |
|
||||
| The pool starts with the plugin enabled and no next run recorded, so it scrapes once immediately; after that the scheduler scans every 60 s and runs the plugin on its own interval (60 min for a new plugin), retrying failures with backoff (2 extra attempts by default, 10 s base delay). The **Fetch** button in the plugin table (`POST /api/pool/plugins/geonode_plugin/run`) triggers a fetch at once. | `proxylist.geonode.com` — the public REST API, paged 500 at a time up to 20 pages (hardcoded). | **Direct** | Enabled state and per-plugin interval live in the pool database (Proxy Pool → Proxy Management); the global defaults under `[Pool]` are `plugins.scan_interval_seconds=60`, `plugins.default_interval_minutes=60`, `plugins.execution_timeout_seconds=300`, `plugins.max_retries_on_failure=2` and `plugins.retry_base_delay_seconds=10.0`. | Disable `geonode_plugin` in Plugin Management (written to the pool database, so it survives restarts), or stop the pool service; the address is hardcoded and redirecting it needs a code change. |
|
||||
| Same scheduling as the GeoNode plugin: one fetch as soon as the pool loads it, then every 60 min by default; the **Fetch** button (`POST /api/pool/plugins/github_proxy_plugin/run`) triggers it at once. | `raw.githubusercontent.com` — 12 hardcoded public proxy lists (6 http, 5 socks5, 1 https source). | **Direct** | The same `plugins.*` keys; enabled state and interval live in the pool database. | Disable `github_proxy_plugin` in Plugin Management, or stop the pool service; the addresses are hardcoded. |
|
||||
| Before every validation: each proxy's validation runs the reachability pre-flight (one probe per 300 s cache), the automatic re-validator runs it once at the head of each round, and a `test_url` not covered by the cached results is probed singly on demand. | `test_url` (default `https://www.baidu.com`), `validator.identity_echo_apis`, the plaintext variants derived from them, `validator.plain_identity_echo_apis` (default `http://postman-echo.com/get`) and three hardcoded connectivity checks — `connectivitycheck.platform.hicloud.com`, `cp.cloudflare.com`, `detectportal.firefox.com`; the plaintext variants and the connectivity checks join only with `check_anonymity=true`. | **Direct** | `test_url` (drives `validator.target_url`), `validator.identity_echo_apis`, `validator.plain_identity_echo_apis`, `validator.check_anonymity`. | Not switchable as a whole — it is part of the validation flow. `check_anonymity=false` removes the plaintext group and the three hardcoded probes, but `test_url` and the identity list are still probed directly; point those at addresses you own; emptying `identity_echo_apis` falls back to the built-in defaults (`httpbin.org` / `httpbingo.org`), so it is not an off switch. |
|
||||
|
||||
@@ -53,8 +53,10 @@
|
||||
- **多语言**:面板支持中文 / 英文切换,语言是服务端设置,整站文案一起切换。
|
||||
- **凭据管理**:上游取代理接口的凭据可存多套并一键切换。
|
||||
- **容器化部署**:Docker 一键部署,内置健康检查会按实际面板端口探活。
|
||||
- **版本检查**:启动时比对一次最新版本,之后每 24 小时一次;结果落盘,24 小时内重启不重查,
|
||||
也不会因为打开面板而等待网络。
|
||||
- **版本检查**:从 GitHub 的发布记录取最新版本,默认并发请求官方发布源、国内可达的镜像与官方
|
||||
API,**谁先返回用谁**;启动时比对一次,之后每 24 小时一次;结果落盘,24 小时内重启不重查,
|
||||
也不会因为打开面板而等待网络。侧边栏版本号旁的按钮可**立即检查一次**,结果显示在原处
|
||||
(失败时连原因一起显示)。地址可用 `version_check_url` 换成自有镜像。
|
||||
|
||||
面板四个页签(代理配置 / 代理池 / 访问控制 / 运行日志)的逐页说明与截图见
|
||||
[使用手册](Operation%20Manual.md)。
|
||||
@@ -322,7 +324,7 @@ ProxyCat 会在下列场景主动向本机之外发起连接,供你核对「
|
||||
|
||||
| 触发时机 | 目标地址 | 直连还是经代理 | 相关配置项 | 如何关闭 |
|
||||
|---|---|---|---|---|
|
||||
| 版本检查:后台线程抓作者站点的版本公告、解析版本号做更新提示,结果落盘 `logs/version_check.json`;启动时(`app.py` 与 `ProxyCat.py` 两个入口都会拉起该线程)距上次检查尝试满 24 小时、从未查过、或记录版本与当前版本不符即立即查一次,此后每 600 秒判断一次、至多每 24 小时发一次;24 小时按「上次检查尝试」记账,失败也算时间 | `y.shironekosan.cn`(硬编码) | **直连** | 无(地址硬编码在 `modules/version_check.py`) | 没有开关;只能在网络层屏蔽该域名(hosts / 防火墙 / DNS)。该请求走 httpx 默认客户端,系统设了 `HTTP(S)_PROXY` 环境变量时会改走环境代理 |
|
||||
| 版本检查:后台线程从 GitHub 仓库的发布记录取版本号做更新提示,结果落盘 `logs/version_check.json`;**并发请求三个内置地址、谁先返回可用的版本号就用谁**(官方发布源无认证无限流;镜像国内可达性更好,但会看到请求 IP 且可能返回缓存的旧版本;官方 API 未认证时每 IP 每小时限 60 次),全失败约 8 秒返回。启动时(`app.py` 与 `ProxyCat.py` 两个入口都会拉起该线程)距上次检查尝试满 24 小时、从未查过、或记录版本与当前版本不符即立即查一次,此后每 600 秒判断一次、至多每 24 小时发一次;24 小时按「上次检查尝试」记账,失败也算时间。面板侧边栏的刷新按钮或 `POST /api/version/check` 可强制立即检查一次,同样会重置 24 小时计时 | `github.com`(releases.atom)、`gh-proxy.com`、`api.github.com`(后两个是镜像与官方 API) | **直连** | `version_check_url`(`[Server]`,面板「代理配置 → 高级选项」):留空用内置三个地址,填入后**只用该地址**,可指向自有镜像 | 填一个自有地址把内置的第三方镜像换掉;要彻底不发就指向一个不可达地址并在网络层屏蔽,或停止进程。该请求走 httpx 默认客户端,系统设了 `HTTP(S)_PROXY` 环境变量时会改走环境代理 |
|
||||
| GeoNode 抓取插件分页抓公开代理列表(每页 500 条、最多 20 页):池启动后插件默认启用、尚无下次执行时间即抓一轮;此后每 60 秒扫描一次、按插件周期执行(新插件默认 60 分钟),失败按默认重试 2 次、基础等待 10 秒退避;面板插件列表里点「抓取」可立即触发 | `proxylist.geonode.com`(硬编码) | **直连** | 启停与周期存在池数据库(面板「代理池 → 代理管理」的插件列表);全局默认 `plugins.scan_interval_seconds=60`、`plugins.default_interval_minutes=60`、`plugins.execution_timeout_seconds=300`、`plugins.max_retries_on_failure=2`、`plugins.retry_base_delay_seconds=10.0`(`[Pool]`) | 面板插件列表里停用 `geonode_plugin`(写进池数据库、重启仍生效),或停止池服务;抓取地址硬编码,改地址需改代码 |
|
||||
| GitHub 抓取插件并发抓 12 个公开代理清单(6 个 http、5 个 socks5、1 个 https):与 GeoNode 插件共用同一套调度,池启动首次加载即抓一轮,之后默认每 60 分钟;面板插件列表里点「抓取」也可立即触发 | `raw.githubusercontent.com`(12 个硬编码清单地址) | **直连** | 同 `plugins.*`(`[Pool]`);启停与周期存在池数据库 | 面板插件列表里停用 `github_proxy_plugin`,或停止池服务;地址硬编码,改地址需改代码 |
|
||||
| 代理验证前的端点直连预筛:每次验证前不带任何代理直连探测各检测端点是否可达(本机 IP 直接对这些站点可见),结果缓存 300 秒;对不在缓存里的单个地址(如插件专属 `test_url`)随时补探一次 | `test_url`(默认 `https://www.baidu.com`)、`validator.identity_echo_apis`(默认 `httpbin.org`、`httpbingo.org`、`eu.httpbin.org`)及其明文派生地址、`validator.plain_identity_echo_apis`(默认 `postman-echo.com`)、3 个硬编码连通性地址(华为云 / Cloudflare / Firefox 的固定连通性探测地址);明文一组仅 `check_anonymity=true` 时探测 | **直连** | `test_url`(`[Server]`,派生 `validator.target_url`)、`validator.identity_echo_apis`、`validator.plain_identity_echo_apis`、`validator.check_anonymity`(`[Pool]`) | 无法整体关闭(是验证流程的一部分);可把 `test_url` 与两个端点列表换成自有地址;`check_anonymity=false` 可去掉明文与 3 个硬编码地址,但 `test_url` 与身份回显列表仍会被直连探测;清空 `identity_echo_apis` 会回落到内置默认(`httpbin.org`、`httpbingo.org`),不是关闭手段 |
|
||||
|
||||
@@ -19,11 +19,14 @@
|
||||
- Progress and cancellation for batch tasks: long jobs such as imports, batch validation and plugin fetches appear under Background tasks showing progress, rate, estimated time left and per-category counts (saved / unusable / unreachable / failed) for the current stage (Checking port reachability / Validating proxies), and a running task can be cancelled in one click (its status then reads Cancelled)
|
||||
|
||||
- Added a version check module: it checks once at startup and every 24 hours after that, persisting the check time so a restart within 24 hours does not check again (at most one outbound request per 24 hours); the panel's version API only reads the cached result, so opening the panel never blocks on the network, and a failed check keeps the last successfully fetched version
|
||||
- The version check now reads the version from the GitHub repository's release history: it requests the official `releases.atom`, the gh-proxy mirror and the official API concurrently and takes the first usable version number, so a blocked or timing-out address does not slow the rest down; when all three fail it returns after roughly 8 seconds. The new `version_check_url` option uses the three built-ins when empty and makes that address the only source when set (point it at a mirror of your own); on save it is validated as a full http/https URL
|
||||
|
||||
- Added a promo / announcement slot to the panel: drop a JSON file (title, body, image, link, dwell time) into `config/ads/` and it is shown; users can dismiss it in one click and reopen it from the sidebar (the dismissal is not persisted, so it is back after a restart)
|
||||
|
||||
**UI and Interaction**
|
||||
|
||||
- Added a Check for updates now button next to the version chip in the sidebar: clicking it forces an immediate check (ignoring the 24-hour limit), the button spins and is disabled while it runs, and the result appears in place — Up to date, or New version: xxx with the version linking to the GitHub Releases page, or the specific reason on failure (hover for the full text). Previously there was no feedback at all unless the check succeeded
|
||||
|
||||
- The panel moved from a horizontal tab strip to a fixed left sidebar with four sections: Proxy Config, Proxy Pool, Access Control and Logs; the Proxy Pool section is split into the three sub-views Proxy Management / Pool Settings / Database Maintenance; the service and pool run states sit permanently in the sidebar, and the Chinese and English wording was filled in throughout
|
||||
|
||||
- The Proxy Config page was rearranged into grouped cards (Source, Listening ports, Exit rotation, Availability checks, Performance, Logging and records, Advanced), each option showing a short title plus a one-line hint and an ⓘ with the full text; numeric inputs take their lower and upper bounds from the server-side schema; proxy authentication was merged into a single "username:password" field, and entering a username without a colon now warns explicitly that this will not enable authentication
|
||||
|
||||
@@ -12,10 +12,12 @@
|
||||
- 新增命令面板与全局快捷键:`Ctrl/⌘+K` 打开可搜索的命令面板(视图跳转、启停服务、刷新出口、批量校验、导入导出、切主题切语言等);`1–4` 切视图、`r` 刷新、`/` 聚焦搜索、`?` 快捷键说明;表格里 `↑/↓` 漫游、空格选中、`Shift` 扩选,输入态自动让路
|
||||
- 批量任务进度与取消:导入、批量验证、插件抓取等长任务进入「后台任务」,按当前阶段(初筛端口连通性 / 验证代理)显示进度、速率、预计剩余时间与分类计数(入库/不可用/初筛淘汰/失败),运行中的任务可一键取消(状态显示「已取消」)
|
||||
- 新增版本检查模块:启动时查一次、之后每 24 小时一次,检查时间落盘,24 小时内重启不会重复检查(对外最多 24 小时发一次请求);面板的版本接口只读缓存结果,打开面板不会因等网络而卡住;检查失败保留上一次成功拿到的版本
|
||||
- 版本检查改从 GitHub 仓库的发布记录取版本号:并发请求官方 `releases.atom`、gh-proxy 镜像与官方 API 三个地址,谁先返回可用的版本号就用谁,被墙或超时的地址不拖慢整体,全失败约 8 秒返回;新增 `version_check_url` 配置项,留空用内置三源,填入后只用该地址(可指向自有镜像),保存时校验必须是 http/https 完整地址
|
||||
- 面板新增推广/公告位:把 JSON(标题、正文、图片、链接、停留时长)放进 `config/ads/` 即可展示,用户可一键关闭,也能从侧栏重新打开(关闭状态不落盘,重启后恢复显示)
|
||||
|
||||
**界面与交互**
|
||||
|
||||
- 侧边栏版本号旁新增「立即检查更新」按钮:点击强制立即检查一次(不受 24 小时限制),按钮转圈期间禁用,结果显示在原处——成功显示「已是最新版本」或「新版本: xxx」(版本号可点击跳转 GitHub Releases 页面),失败显示具体原因(悬停可看全文),不再像以前那样只在成功时才有反馈
|
||||
- 面板从横排页签改为左侧固定侧边栏的四个区:代理配置、代理池、访问控制、运行日志;代理池下再分「代理管理 / 池设置 / 数据库维护」三个子视图;服务与池的启停状态常驻侧栏,中英文文案一并补齐
|
||||
- 代理配置页重排为分组卡片(来源、监听端口、出口轮换、可用性检测、性能与并发、日志与记录、高级),每项「短标题 + 一行提示 + ⓘ 全文」;数值输入的上下限随服务端 schema 下发;代理认证并为一行「用户名:密码」,填了用户名却没有冒号时会明确提示这样不会启用认证
|
||||
- 参数呈现与保存栏:计量单位移到输入框右侧、标题只留短名;保存栏未改动时置灰、新增「放弃修改」、干净态显示「已保存」回执、吸底时带阴影;切语言即时刷新表单
|
||||
|
||||
@@ -254,6 +254,7 @@ if __name__ == '__main__':
|
||||
os.path.join(BASE_DIR, 'logs', 'version_check.json'),
|
||||
language_provider=lambda: version_language,
|
||||
on_result=lambda payload: print_version_result(payload, version_language),
|
||||
url_provider=lambda: server.config.get('version_check_url', ''),
|
||||
)
|
||||
if not version_checker.start():
|
||||
print_version_result(version_checker.result_payload(), version_language)
|
||||
|
||||
+1
-1
@@ -117,7 +117,7 @@ These four are all classic "symptom first, cause later" cases; the full explanat
|
||||
|
||||
### Network Behaviour
|
||||
|
||||
Besides forwarding client traffic, ProxyCat itself also makes outbound requests, many of them **direct** (sent from this machine's real IP) — for example the version check, the pool's host public-IP lookup, the GeoNode and GitHub scraping plugins and the offline geolocation-database update. These requests can all be turned off or pointed at addresses of your own (the version check has no dedicated switch and must be blocked at the network layer); for each one's trigger, destination and how to stop it, see [Features](ProxyCat-Manual/Features-EN.md#outbound-network-requests). Traffic forwarded through a proxy is not on this list — that is the client's own traffic.
|
||||
Besides forwarding client traffic, ProxyCat itself also makes outbound requests, many of them **direct** (sent from this machine's real IP) — for example the version check, the pool's host public-IP lookup, the GeoNode and GitHub scraping plugins and the offline geolocation-database update. These requests can all be turned off or pointed at addresses of your own (pointing `version_check_url` at an address of your own keeps the version check away from the built-in third-party mirror); for each one's trigger, destination and how to stop it, see [Features](ProxyCat-Manual/Features-EN.md#outbound-network-requests). Traffic forwarded through a proxy is not on this list — that is the client's own traffic.
|
||||
|
||||
## Documentation
|
||||
|
||||
|
||||
@@ -124,7 +124,8 @@ curl -x http://neko:[email protected]:1080 https://www.baidu.com -I
|
||||
### 网络行为提示
|
||||
|
||||
ProxyCat 除转发客户端流量外,自身还会主动发起一些对外请求,其中多条是**直连**的(以本机真实 IP 发出),例如版本检查、
|
||||
代理池的本机公网 IP 查询、GeoNode 与 GitHub 抓取插件和离线归属地库更新。这些请求都可以关闭或改用自有地址(版本检查没有专属开关,需从网络层屏蔽);
|
||||
代理池的本机公网 IP 查询、GeoNode 与 GitHub 抓取插件和离线归属地库更新。这些请求都可以关闭或改用自有地址(版本检查改用
|
||||
`version_check_url` 指向自有地址即可避开内置的第三方镜像);
|
||||
逐条的触发时机、目的地与关闭方式见[功能特性](ProxyCat-Manual/Features.md#对外网络请求)。经代理转发的流量不在此列——那是客户端自己的流量。
|
||||
|
||||
## 文档导航
|
||||
|
||||
@@ -1188,6 +1188,7 @@ def change_language():
|
||||
version_checker = VersionChecker(
|
||||
os.path.join(BASE_DIR, 'logs', 'version_check.json'),
|
||||
language_provider=lambda: server.language,
|
||||
url_provider=lambda: server.config.get('version_check_url', ''),
|
||||
)
|
||||
|
||||
|
||||
@@ -1195,6 +1196,11 @@ version_checker = VersionChecker(
|
||||
def check_version():
|
||||
return jsonify(version_checker.result_payload())
|
||||
|
||||
@app.route('/api/version/check', methods=['POST'])
|
||||
@require_token
|
||||
def check_version_now():
|
||||
return jsonify(version_checker.check_now())
|
||||
|
||||
@app.route('/api/users', methods=['GET', 'POST'])
|
||||
@require_token
|
||||
def handle_users():
|
||||
|
||||
+7
-1
@@ -58,6 +58,12 @@ check_proxies_on_startup = true
|
||||
check_proxies_on_use = true
|
||||
# 远程代理池的管理接口地址;留空表示使用进程内的代理池
|
||||
pool_remote_url =
|
||||
# 版本检查的源地址;留空表示并发请求内置的 GitHub 地址(官方 releases.atom、gh-proxy 镜像、
|
||||
# GitHub 官方 API),谁先返回可用的版本号就用谁。填写后只请求填写的这一个地址,可填 GitHub API、
|
||||
# releases.atom 或任何能解析出版本号的页面,必须是 http/https 开头的完整地址。
|
||||
# 注意:内置源里的第三方镜像会看到你的请求 IP,且可能返回缓存的旧版本;官方 API 未认证时
|
||||
# 每 IP 每小时限 60 次。改后下次检查生效
|
||||
version_check_url =
|
||||
# 批量检测代理的并发数上限(1-1000)
|
||||
check_concurrency = 100
|
||||
# 日志级别:DEBUG / INFO / WARNING / ERROR / CRITICAL
|
||||
@@ -192,7 +198,7 @@ database.backup_retention_days = 7
|
||||
# ── 代理验证 ────────────────────────────────────────────────
|
||||
# 是否检查代理的匿名度。匿名度靠「目标站点实际收到了哪些请求头」判定,只在经代理发一次明文请求时才测得出来;关掉后不再发这类请求、也不再判匿名度,已经测出来的等级原样保留
|
||||
validator.check_anonymity = true
|
||||
# 本机公网出口 IP,判定「透明代理」时的对比基准。双栈主机两族各填一个、用逗号隔开(IPv4 与 IPv6 各至多一个),两族都会参与比对;只填一个也能用。留空时 ProxyCat 会**不经过代理**直接访问 IP 查询站点获取(每 30 分钟最多一次,成功与失败都重新计时);在这里填上你自己的出口 IP 可以完全免掉这次对外请求。留空或拿不到时无法判定匿名度,那一项会保持原样
|
||||
# 本机公网出口 IP,判定「透明代理」时的对比基准。双栈主机两族各填一个、用逗号隔开(IPv4 与 IPv6 各至多一个),两族都会参与比对;只填一个也能用。留空时 ProxyCat 会**不经过代理**直接访问 IP 查询站点获取(每小时最多一次);在这里填上你自己的出口 IP 可以完全免掉这次对外请求。留空或拿不到时无法判定匿名度,那一项会保持原样
|
||||
validator.host_public_ip =
|
||||
# 验证一个代理最多等多少秒,超过就判为不可用。所有探测请求都按这个上限
|
||||
validator.timeout_seconds = 5
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
import logging
|
||||
from functools import partial
|
||||
from typing import Any, Callable, Mapping
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
from modules.modules import (
|
||||
DEFAULT_CONFIG, FALSE_WORDS, TRUE_WORDS, render_config_error,
|
||||
@@ -87,6 +88,16 @@ def _as_text(value: Any, key: str) -> str:
|
||||
return str(value).strip()
|
||||
|
||||
|
||||
def _as_url(value: Any, key: str) -> str:
|
||||
text = str(value).strip()
|
||||
if not text:
|
||||
return ''
|
||||
parts = urlsplit(text)
|
||||
if parts.scheme not in ('http', 'https') or not parts.netloc:
|
||||
raise ConfigValidationError(key, value, "config_reason_expected_url")
|
||||
return text
|
||||
|
||||
|
||||
def _as_upper_choice(value: Any, key: str, allowed: tuple[str, ...]) -> str:
|
||||
return _as_choice(value, key, allowed, lower=False)
|
||||
|
||||
@@ -118,6 +129,8 @@ _SERVER_RULES: dict[str, Callable[[Any, str], str]] = {
|
||||
'check_proxies_on_use': _as_bool,
|
||||
'auto_expand_enabled': _as_bool,
|
||||
|
||||
'version_check_url': _as_url,
|
||||
|
||||
'mode': partial(_as_choice, allowed=(
|
||||
'cycle', 'loadbalance', 'continuous', 'request')),
|
||||
'proxy_source_mode': partial(_as_choice, allowed=('local', 'api', 'pool')),
|
||||
|
||||
@@ -252,6 +252,7 @@ MESSAGES = {
|
||||
'config_reason_max': '不能大于 {}',
|
||||
'config_reason_expected_choice': '只能是 {} 之一',
|
||||
'config_reason_expected_ip': '必须是一到两个合法的 IP 地址(IPv4 与 IPv6 各至多一个,逗号分隔)',
|
||||
'config_reason_expected_url': '必须是 http/https 开头的完整地址,留空表示使用内置地址',
|
||||
'config_reason_derived_key': '该配置项由 [Server] 段派生,不能单独设置',
|
||||
'config_reason_unknown_key': '不是可修改的配置项',
|
||||
'config_reason_users_via_api': '用户账号请通过用户管理接口修改',
|
||||
@@ -609,6 +610,7 @@ MESSAGES = {
|
||||
'config_reason_max': 'must not be greater than {}',
|
||||
'config_reason_expected_choice': 'must be one of {}',
|
||||
'config_reason_expected_ip': 'must be one or two valid IP addresses (at most one IPv4 and one IPv6, comma-separated)',
|
||||
'config_reason_expected_url': 'must be a full http/https URL, or empty to use the built-in addresses',
|
||||
'config_reason_derived_key': 'this option is derived from the [Server] section and cannot be set separately',
|
||||
'config_reason_unknown_key': 'not an editable option',
|
||||
'config_reason_users_via_api': 'manage user accounts through the user management API',
|
||||
@@ -838,6 +840,7 @@ DEFAULT_CONFIG = {
|
||||
'proxy_source_mode': 'local',
|
||||
'api_proxy_url': 'http://example.com/getip',
|
||||
'pool_remote_url': '',
|
||||
'version_check_url': '',
|
||||
|
||||
'proxy_file': 'ip.txt',
|
||||
'check_proxies_on_startup': 'True',
|
||||
|
||||
+340
-64
@@ -1,31 +1,48 @@
|
||||
"""
|
||||
模块名称:modules.version_check
|
||||
功能描述:版本检查的调度与状态持久化:决定何时比对版本、结果放在哪、给谁读。启动后由后台
|
||||
线程立即做一次到期判定,之后每 24 小时至多请求一次;时间与结果落盘,内存中缓存结果。
|
||||
职责边界:负责:到期判定、发起远端请求并解析版本号、持久化最近一次检查的时间与结果、
|
||||
在内存中缓存结果供只读查询;不负责:HTTP 路由(见 app)、控制台输出与用户
|
||||
可见文案渲染(入口文件与 modules.modules.get_message 负责),本模块只回调。
|
||||
关键依赖:modules.modules(CURRENT_VERSION、get_message、read_config_text、
|
||||
write_text_atomic)、httpx、packaging。
|
||||
功能描述:版本检查的调度、取数与状态持久化:决定何时比对版本、从哪些地址取、结果放在哪、给谁读。
|
||||
启动后由后台线程立即做一次到期判定,之后每 24 小时至多检查一次;时间与结果落盘,内存中缓存结果。
|
||||
取数时并发请求多个 GitHub 地址(官方发布源、镜像、官方 API),谁先返回可用的版本号就用谁。
|
||||
职责边界:负责:到期判定、并发取数并解析版本号、持久化最近一次检查的时间与结果、在内存中缓存结果
|
||||
供只读查询;不负责:HTTP 路由(见 app)、控制台输出与用户可见文案渲染(入口文件与
|
||||
modules.modules.get_message 负责),本模块只回调。
|
||||
关键依赖:modules.modules(CURRENT_VERSION、get_message、read_config_text、write_text_atomic)、
|
||||
asyncio、json 与 xml.etree.ElementTree(标准库)、httpx(异步客户端)、packaging。
|
||||
已知限制:
|
||||
1. 内存时间戳是准绳,状态文件只是尽力而为的写穿:写盘失败只记日志,不影响 24 小时内不再检查的判定。
|
||||
2. 检查失败时保留上一次成功的版本号,result_payload() 仍返回成功形态,失败只体现在日志。
|
||||
2. 检查失败时保留上一次成功的版本号,result_payload() 仍返回成功形态,失败只体现在日志与状态文件;
|
||||
本次是否成功要看 check_now() 的返回,它用 status 直接表达本次结果。
|
||||
3. 时钟回拨时时间戳落到未来,按到期处理,检查一次即以新时间自愈;tick 只用 Event.wait,不按剩余时间计算睡眠。
|
||||
4. 远端请求在锁外发起,_in_flight 只防同进程重入,多进程共用状态文件时仍可能各查一次。
|
||||
4. _in_flight 提供互斥,_in_flight_done 提供「等正在跑的那一次」;两者总在 _lock 下成对读写,
|
||||
等待事件必须在锁外进行(_lock 是叶子锁)。多进程共用状态文件时仍可能各查一次。
|
||||
5. write_text_atomic 的临时文件名为 .config-*.tmp,进程被强杀时可能在状态文件目录留下残留。
|
||||
6. 竞速以「最先返回的成功源」为准,不跨源比对最大值;第三方镜像可能返回缓存的旧响应,
|
||||
因而理论上存在「镜像先返回、报出的版本偏旧」的窗口。
|
||||
7. api.github.com 未认证限额为每 IP 每小时 60 次,共享出口 IP 下更少;它在默认源里,
|
||||
但失败不影响另外两个源。
|
||||
8. asyncio.run() 要求调用线程当前没有事件循环;本模块的全部调用点(后台检查线程、
|
||||
阻塞启动路径、Web 请求线程)都满足,新增调用点时需保持这一前提。
|
||||
9. 自定义地址按响应体形状择一解析(JSON → Atom → 整文正则);能解析成 JSON/Atom 时不再退回整文正则,
|
||||
以免把发布说明正文里的版本号当成发布版本。
|
||||
10. 版本号只取 ProxyCat[-_]v?数字.数字[.数字] 这一段前缀,测试版等后缀被丢弃
|
||||
(否则同一版本的每个测试版都会被当成一个新版本)。
|
||||
11. 请求全部经由 httpx,httpx 默认读取 HTTP(S)_PROXY 环境变量,这是既有的对外行为。
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import threading
|
||||
import time
|
||||
import xml.etree.ElementTree as ElementTree
|
||||
from datetime import datetime
|
||||
|
||||
import httpx
|
||||
from packaging import version
|
||||
from packaging.version import InvalidVersion, Version
|
||||
|
||||
from modules.loop_noise_filter import install_loop_noise_filter
|
||||
from modules.modules import (
|
||||
CURRENT_VERSION,
|
||||
get_message,
|
||||
@@ -35,7 +52,11 @@ from modules.modules import (
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
VERSION_URL = "https://y.shironekosan.cn/1.html"
|
||||
BUILTIN_SOURCES = (
|
||||
"https://github.com/honmashironeko/ProxyCat/releases.atom",
|
||||
"https://gh-proxy.com/https://api.github.com/repos/honmashironeko/ProxyCat/releases?per_page=10",
|
||||
"https://api.github.com/repos/honmashironeko/ProxyCat/releases?per_page=10",
|
||||
)
|
||||
|
||||
CHECK_INTERVAL_SECONDS = 24 * 60 * 60
|
||||
|
||||
@@ -43,16 +64,136 @@ CHECK_TICK_SECONDS = 600.0
|
||||
|
||||
REQUEST_TIMEOUT_SECONDS = 10.0
|
||||
|
||||
RACE_TIMEOUT_SECONDS = 15.0
|
||||
|
||||
_CONNECT_TIMEOUT_SECONDS = 4.0
|
||||
|
||||
_WRITE_TIMEOUT_SECONDS = 4.0
|
||||
|
||||
_STATE_TIME_FORMAT = "%Y-%m-%d %H:%M:%S"
|
||||
|
||||
_VERSION_PATTERN = re.compile(r'<p>(ProxyCat-V\d+\.\d+\.\d+)</p>')
|
||||
_ATOM_NAMESPACE = "{http://www.w3.org/2005/Atom}"
|
||||
|
||||
_TAG_PATTERN = re.compile(r'ProxyCat[-_]v?(\d+(?:\.\d+){1,2})', re.IGNORECASE)
|
||||
|
||||
_JSON_TAG_KEYS = ('tag_name',)
|
||||
|
||||
_JSON_LABEL_KEYS = ('name',)
|
||||
|
||||
|
||||
def _parse_version(label):
|
||||
match = _TAG_PATTERN.search(str(label))
|
||||
if not match:
|
||||
return None
|
||||
try:
|
||||
return Version(match.group(1))
|
||||
except InvalidVersion:
|
||||
return None
|
||||
|
||||
|
||||
def _match_versions(labels):
|
||||
matched = []
|
||||
for label in labels:
|
||||
for match in _TAG_PATTERN.finditer(str(label)):
|
||||
if match.group(0) not in matched:
|
||||
matched.append(match.group(0))
|
||||
return matched
|
||||
|
||||
|
||||
def _json_labels(text):
|
||||
try:
|
||||
payload = json.loads(text)
|
||||
except (ValueError, UnicodeDecodeError):
|
||||
return None
|
||||
|
||||
labels = []
|
||||
|
||||
def collect(node, nested):
|
||||
if isinstance(node, dict):
|
||||
for key, value in node.items():
|
||||
if key in _JSON_TAG_KEYS and isinstance(value, str):
|
||||
labels.append(value)
|
||||
elif nested and key in _JSON_LABEL_KEYS and isinstance(value, str):
|
||||
labels.append(value)
|
||||
else:
|
||||
collect(value, True)
|
||||
elif isinstance(node, list):
|
||||
for item in node:
|
||||
collect(item, nested)
|
||||
|
||||
collect(payload, False)
|
||||
return labels
|
||||
|
||||
|
||||
def _atom_labels(text):
|
||||
try:
|
||||
root = ElementTree.fromstring(text)
|
||||
except ElementTree.ParseError:
|
||||
return None
|
||||
|
||||
if root.tag != _ATOM_NAMESPACE + 'feed':
|
||||
return None
|
||||
|
||||
labels = []
|
||||
for entry in root.findall(_ATOM_NAMESPACE + 'entry'):
|
||||
for name in ('id', 'title'):
|
||||
element = entry.find(_ATOM_NAMESPACE + name)
|
||||
if element is not None and element.text:
|
||||
labels.append(element.text.rsplit('/', 1)[-1])
|
||||
return labels
|
||||
|
||||
|
||||
def _extract_versions(text):
|
||||
stripped = text.lstrip()
|
||||
if stripped[:1] in ('{', '['):
|
||||
labels = _json_labels(text)
|
||||
if labels is not None:
|
||||
return _match_versions(labels)
|
||||
elif stripped[:1] == '<':
|
||||
labels = _atom_labels(text)
|
||||
if labels is not None:
|
||||
return _match_versions(labels)
|
||||
return _match_versions([text])
|
||||
|
||||
|
||||
def _latest_of(versions):
|
||||
best = None
|
||||
best_parsed = None
|
||||
for label in versions:
|
||||
parsed = _parse_version(label)
|
||||
if parsed is None:
|
||||
continue
|
||||
if best_parsed is None or parsed > best_parsed:
|
||||
best = label
|
||||
best_parsed = parsed
|
||||
return best
|
||||
|
||||
|
||||
def _not_newer(latest_version, current_version):
|
||||
latest = _parse_version(latest_version)
|
||||
current = _parse_version(current_version)
|
||||
if latest is None or current is None:
|
||||
return True
|
||||
return latest <= current
|
||||
|
||||
|
||||
def _host_of(url):
|
||||
without_scheme = str(url).split('://', 1)[-1]
|
||||
return without_scheme.split('/', 1)[0] or str(url)
|
||||
|
||||
|
||||
def _error_detail(error):
|
||||
if isinstance(error, httpx.HTTPStatusError):
|
||||
return f"HTTP {error.response.status_code}"
|
||||
return type(error).__name__
|
||||
|
||||
|
||||
class VersionChecker:
|
||||
def __init__(self, state_path, language_provider=None, on_result=None):
|
||||
def __init__(self, state_path, language_provider=None, on_result=None, url_provider=None):
|
||||
self.state_path = str(state_path)
|
||||
self._language_provider = language_provider
|
||||
self._on_result = on_result
|
||||
self._url_provider = url_provider
|
||||
|
||||
self._lock = threading.Lock()
|
||||
self._stop_event = threading.Event()
|
||||
@@ -62,9 +203,11 @@ class VersionChecker:
|
||||
|
||||
self._checked_at: float | None = None
|
||||
self._latest_version: str | None = None
|
||||
self._source: str | None = None
|
||||
self._error_key: str | None = None
|
||||
self._error_args: list = []
|
||||
self._in_flight = False
|
||||
self._in_flight_done: threading.Event | None = None
|
||||
|
||||
def start(self, blocking: bool = False) -> bool:
|
||||
with self._lock:
|
||||
@@ -96,64 +239,164 @@ class VersionChecker:
|
||||
def check_if_due(self, notify: bool = True) -> bool:
|
||||
with self._lock:
|
||||
self._ensure_loaded_locked()
|
||||
if not self._due_locked() or self._in_flight:
|
||||
if not self._due_locked():
|
||||
return False
|
||||
self._in_flight = True
|
||||
|
||||
done = self._acquire_check()
|
||||
if done is None:
|
||||
return False
|
||||
|
||||
self._execute_check(notify=notify, done=done)
|
||||
return True
|
||||
|
||||
def check_now(self) -> dict:
|
||||
with self._lock:
|
||||
self._ensure_loaded_locked()
|
||||
running = self._in_flight_done
|
||||
|
||||
if running is None:
|
||||
done = self._acquire_check()
|
||||
if done is not None:
|
||||
self._execute_check(notify=False, done=done)
|
||||
else:
|
||||
with self._lock:
|
||||
running = self._in_flight_done
|
||||
|
||||
if running is not None:
|
||||
running.wait(RACE_TIMEOUT_SECONDS + _CONNECT_TIMEOUT_SECONDS)
|
||||
|
||||
with self._lock:
|
||||
return self._check_result_locked()
|
||||
|
||||
def result_payload(self) -> dict:
|
||||
with self._lock:
|
||||
self._ensure_loaded_locked()
|
||||
return self._payload_locked()
|
||||
|
||||
def _acquire_check(self):
|
||||
with self._lock:
|
||||
if self._in_flight:
|
||||
return None
|
||||
done = threading.Event()
|
||||
self._in_flight = True
|
||||
self._in_flight_done = done
|
||||
return done
|
||||
|
||||
def _release_check(self, done) -> None:
|
||||
with self._lock:
|
||||
self._in_flight = False
|
||||
self._in_flight_done = None
|
||||
done.set()
|
||||
|
||||
def _execute_check(self, notify: bool, done) -> None:
|
||||
notify_payload = None
|
||||
try:
|
||||
started_at = time.time()
|
||||
latest_version, error_key, error_args = self._fetch_latest()
|
||||
finally:
|
||||
latest_version, source, error_key, error_args = self._fetch_latest()
|
||||
|
||||
with self._lock:
|
||||
self._in_flight = False
|
||||
self._checked_at = started_at
|
||||
if latest_version:
|
||||
self._latest_version = latest_version
|
||||
self._source = source
|
||||
self._error_key = None
|
||||
self._error_args = []
|
||||
elif error_key:
|
||||
self._error_key = error_key
|
||||
self._error_args = error_args
|
||||
|
||||
notify_payload = None
|
||||
with self._lock:
|
||||
self._checked_at = started_at
|
||||
if latest_version:
|
||||
self._latest_version = latest_version
|
||||
self._error_key = None
|
||||
self._error_args = []
|
||||
elif error_key:
|
||||
self._error_key = error_key
|
||||
self._error_args = error_args
|
||||
self._persist_locked()
|
||||
|
||||
self._persist_locked()
|
||||
|
||||
if notify and self._on_result is not None:
|
||||
notify_payload = self._payload_locked()
|
||||
if notify and self._on_result is not None:
|
||||
notify_payload = self._payload_locked()
|
||||
finally:
|
||||
self._release_check(done)
|
||||
|
||||
if notify_payload is not None:
|
||||
try:
|
||||
self._on_result(notify_payload)
|
||||
except Exception as e:
|
||||
logger.warning(f"版本检查的结果回调执行失败(已忽略): {e}")
|
||||
return True
|
||||
|
||||
def _sources(self) -> tuple:
|
||||
if self._url_provider is None:
|
||||
return BUILTIN_SOURCES
|
||||
try:
|
||||
custom = str(self._url_provider() or '').strip()
|
||||
except Exception:
|
||||
return BUILTIN_SOURCES
|
||||
return (custom,) if custom else BUILTIN_SOURCES
|
||||
|
||||
def _fetch_latest(self) -> tuple:
|
||||
latest_version = None
|
||||
error_key = None
|
||||
error_args: list = []
|
||||
try:
|
||||
with httpx.Client(transport=httpx.HTTPTransport(retries=3)) as client:
|
||||
response = client.get(VERSION_URL, timeout=REQUEST_TIMEOUT_SECONDS)
|
||||
response.raise_for_status()
|
||||
match = _VERSION_PATTERN.search(response.text)
|
||||
|
||||
if match:
|
||||
latest_version = match.group(1)
|
||||
else:
|
||||
error_key = 'version_info_not_found'
|
||||
return asyncio.run(self._race_sources(self._sources()))
|
||||
except Exception as e:
|
||||
error_key = 'update_check_error'
|
||||
error_args = [str(e)]
|
||||
logger.warning(f"版本检查失败(本次已记账,24 小时内不再重试): {e}")
|
||||
return latest_version, error_key, error_args
|
||||
return None, None, 'update_check_error', [str(e)]
|
||||
|
||||
def result_payload(self) -> dict:
|
||||
with self._lock:
|
||||
self._ensure_loaded_locked()
|
||||
return self._payload_locked()
|
||||
async def _race_sources(self, sources) -> tuple:
|
||||
install_loop_noise_filter(asyncio.get_running_loop())
|
||||
|
||||
deadline = time.monotonic() + RACE_TIMEOUT_SECONDS
|
||||
timeouts = httpx.Timeout(
|
||||
REQUEST_TIMEOUT_SECONDS,
|
||||
connect=_CONNECT_TIMEOUT_SECONDS,
|
||||
read=REQUEST_TIMEOUT_SECONDS,
|
||||
write=_WRITE_TIMEOUT_SECONDS,
|
||||
)
|
||||
failures: list = []
|
||||
|
||||
async with httpx.AsyncClient(
|
||||
transport=httpx.AsyncHTTPTransport(retries=1),
|
||||
timeout=timeouts,
|
||||
follow_redirects=True,
|
||||
) as client:
|
||||
order = {}
|
||||
pending = set()
|
||||
for index, url in enumerate(sources):
|
||||
task = asyncio.create_task(self._fetch_source(client, url))
|
||||
order[task] = index
|
||||
pending.add(task)
|
||||
|
||||
try:
|
||||
while pending:
|
||||
remaining = deadline - time.monotonic()
|
||||
if remaining <= 0:
|
||||
break
|
||||
completed, pending = await asyncio.wait(
|
||||
pending, timeout=remaining, return_when=asyncio.FIRST_COMPLETED
|
||||
)
|
||||
if not completed:
|
||||
break
|
||||
for task in sorted(completed, key=lambda item: order[item]):
|
||||
version, source, error_key, detail = task.result()
|
||||
if version:
|
||||
return version, source, None, []
|
||||
failures.append((source, detail, error_key))
|
||||
finally:
|
||||
for task in pending:
|
||||
task.cancel()
|
||||
if pending:
|
||||
await asyncio.gather(*pending, return_exceptions=True)
|
||||
|
||||
if not failures:
|
||||
return None, None, 'update_check_error', ['timeout']
|
||||
|
||||
logger.warning(f"版本检查失败(本次已记账,24 小时内不再重试): {_failure_text(failures)}")
|
||||
return None, None, _failure_key(failures), [_failure_text(failures)]
|
||||
|
||||
async def _fetch_source(self, client, url) -> tuple:
|
||||
source = _host_of(url)
|
||||
try:
|
||||
response = await client.get(url)
|
||||
response.raise_for_status()
|
||||
except Exception as e:
|
||||
return None, source, 'update_check_error', _error_detail(e)
|
||||
|
||||
latest_version = _latest_of(_extract_versions(response.text))
|
||||
if not latest_version:
|
||||
return None, source, 'version_info_not_found', 'no version found'
|
||||
return latest_version, source, None, None
|
||||
|
||||
def _language(self) -> str:
|
||||
if self._language_provider is None:
|
||||
@@ -182,6 +425,32 @@ class VersionChecker:
|
||||
'message': get_message('version_not_checked', language),
|
||||
}
|
||||
|
||||
def _check_result_locked(self) -> dict:
|
||||
language = self._language()
|
||||
payload = {
|
||||
'current_version': CURRENT_VERSION,
|
||||
'checked_at': self._checked_at_text(),
|
||||
}
|
||||
if self._latest_version:
|
||||
payload['latest_version'] = self._latest_version
|
||||
payload['is_latest'] = _not_newer(self._latest_version, CURRENT_VERSION)
|
||||
|
||||
if self._error_key:
|
||||
payload['status'] = 'error'
|
||||
payload['message'] = get_message(self._error_key, language, *self._error_args)
|
||||
elif self._latest_version:
|
||||
payload['status'] = 'success'
|
||||
payload['source'] = self._source
|
||||
else:
|
||||
payload['status'] = 'error'
|
||||
payload['message'] = get_message('version_not_checked', language)
|
||||
return payload
|
||||
|
||||
def _checked_at_text(self):
|
||||
if self._checked_at is None:
|
||||
return None
|
||||
return datetime.fromtimestamp(self._checked_at).strftime(_STATE_TIME_FORMAT)
|
||||
|
||||
def _tick_loop(self) -> None:
|
||||
while True:
|
||||
try:
|
||||
@@ -202,11 +471,10 @@ class VersionChecker:
|
||||
def _persist_locked(self) -> None:
|
||||
payload = {
|
||||
'checked_at': self._checked_at,
|
||||
'checked_at_text': datetime.fromtimestamp(self._checked_at).strftime(
|
||||
_STATE_TIME_FORMAT
|
||||
),
|
||||
'checked_at_text': self._checked_at_text(),
|
||||
'current_version': CURRENT_VERSION,
|
||||
'latest_version': self._latest_version,
|
||||
'source': self._source,
|
||||
'error_key': self._error_key,
|
||||
'error_args': self._error_args,
|
||||
}
|
||||
@@ -254,28 +522,36 @@ class VersionChecker:
|
||||
return
|
||||
|
||||
latest_version = raw.get('latest_version')
|
||||
source = raw.get('source')
|
||||
error_key = raw.get('error_key')
|
||||
error_args = raw.get('error_args')
|
||||
|
||||
self._checked_at = float(checked_at)
|
||||
self._latest_version = latest_version if isinstance(latest_version, str) else None
|
||||
self._source = source if isinstance(source, str) else None
|
||||
self._error_key = error_key if isinstance(error_key, str) else None
|
||||
self._error_args = list(error_args) if isinstance(error_args, list) else []
|
||||
|
||||
|
||||
def _not_newer(latest_version: str, current_version: str) -> bool:
|
||||
try:
|
||||
return (
|
||||
version.parse(latest_version.split('-V')[1])
|
||||
<= version.parse(current_version.split('-V')[1])
|
||||
)
|
||||
except (IndexError, ValueError):
|
||||
return True
|
||||
def _failure_key(failures) -> str:
|
||||
for _, _, error_key in failures:
|
||||
if error_key == 'update_check_error':
|
||||
return 'update_check_error'
|
||||
return 'version_info_not_found'
|
||||
|
||||
|
||||
def _failure_text(failures) -> str:
|
||||
parts = []
|
||||
for source, detail, _ in failures:
|
||||
text = f"{source}: {detail}"
|
||||
if text not in parts:
|
||||
parts.append(text)
|
||||
return '; '.join(parts)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"VersionChecker",
|
||||
"VERSION_URL",
|
||||
"BUILTIN_SOURCES",
|
||||
"CHECK_INTERVAL_SECONDS",
|
||||
"CHECK_TICK_SECONDS",
|
||||
]
|
||||
+14
-1
@@ -200,9 +200,22 @@ body::before {
|
||||
background: var(--surface-alt); border: 1px solid var(--border);
|
||||
padding: 2px 9px; border-radius: 100px;
|
||||
}
|
||||
.hdr-ver-stat { font-size: 10px; font-weight: 650; color: var(--text-faint); }
|
||||
.hdr-ver-refresh {
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
font-size: 10px; line-height: 1; color: var(--text-faint);
|
||||
background: var(--surface-alt); border: 1px solid var(--border);
|
||||
padding: 3px 7px; border-radius: 100px; cursor: pointer;
|
||||
}
|
||||
.hdr-ver-refresh:hover { color: var(--primary-text); border-color: var(--primary-text); }
|
||||
.hdr-ver-refresh[disabled] { opacity: .55; cursor: default; }
|
||||
.hdr-ver-refresh.busy .fa { animation: spin .7s linear infinite; }
|
||||
.hdr-ver-stat {
|
||||
font-size: 10px; font-weight: 650; color: var(--text-faint);
|
||||
max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
}
|
||||
.ver-ok { color: var(--success-text); }
|
||||
.ver-new { color: var(--warn-text); }
|
||||
.ver-err { color: var(--danger-text); }
|
||||
|
||||
.sb-nav { display: flex; flex-direction: column; gap: 2px; }
|
||||
.sb-item {
|
||||
|
||||
+40
-4
@@ -782,6 +782,7 @@ function discardServerChanges() {
|
||||
var SERVER_FORM_DEFAULTS = {
|
||||
proxy_source_mode: 'local', proxy_file: 'ip.txt', mode: 'cycle',
|
||||
api_proxy_url: '', proxy_username: '', proxy_password: '', pool_remote_url: '',
|
||||
version_check_url: '',
|
||||
client_idle_timeout: '300',
|
||||
max_pool_size: '500',
|
||||
client_max_connections: '1000',
|
||||
@@ -2265,17 +2266,52 @@ function clearDomainStats() {
|
||||
}
|
||||
|
||||
|
||||
var VER_RELEASES_URL = 'https://github.com/honmashironeko/ProxyCat/releases';
|
||||
|
||||
function _renderVer(d) {
|
||||
var stat = d.is_latest
|
||||
? '<span class="ver-ok"> ' + T().latest_version + '</span>'
|
||||
: '<span class="ver-new">' + T().new_version_available + ': <a href="' + VER_RELEASES_URL + '" target="_blank" rel="noopener">' + escapeHtml(d.latest_version || '') + '</a></span>';
|
||||
$('#hdr-ver').text(d.current_version || 'ProxyCat');
|
||||
$('#ver-stat').removeAttr('title').html(stat);
|
||||
}
|
||||
|
||||
function checkVer() {
|
||||
onLangRender(checkVer);
|
||||
$.get(appendToken('/api/version'), function (d) {
|
||||
if (d.status === 'success') {
|
||||
$('#hdr-ver').text(d.current_version || 'ProxyCat');
|
||||
if (d.is_latest) $('#ver-stat').html('<span class="ver-ok"> ' + T().latest_version + '</span>');
|
||||
else $('#ver-stat').html('<span class="ver-new"> ' + T().new_version_available + ': ' + (d.latest_version || '') + '</span>');
|
||||
if (d.status === 'success') _renderVer(d);
|
||||
});
|
||||
}
|
||||
|
||||
var _verChecking = false;
|
||||
|
||||
function refreshVer() {
|
||||
if (_verChecking) return;
|
||||
_verChecking = true;
|
||||
$('#ver-refresh').prop('disabled', true).addClass('busy');
|
||||
$('#ver-stat').removeAttr('title').html('<span class="ver-checking">' + T().version_checking + '</span>');
|
||||
$.ajax({
|
||||
url: appendToken('/api/version/check'), method: 'POST',
|
||||
success: function (d) {
|
||||
if (d && d.status === 'success') { _renderVer(d); return; }
|
||||
showVerError((d && d.message) || T().version_check_failed);
|
||||
},
|
||||
error: function (x) {
|
||||
showVerError(jqErrorText(x, T().version_check_failed));
|
||||
},
|
||||
complete: function () {
|
||||
_verChecking = false;
|
||||
$('#ver-refresh').prop('disabled', false).removeClass('busy');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showVerError(message) {
|
||||
$('#ver-stat').attr('title', message).empty().append($('<span class="ver-err">').text(message));
|
||||
}
|
||||
|
||||
$(document).on('click', '#ver-refresh', refreshVer);
|
||||
|
||||
|
||||
var _ads = [];
|
||||
var _adIndex = 0;
|
||||
|
||||
@@ -287,6 +287,10 @@ var translations = {
|
||||
'web_port_label': 'Web 面板端口', 'web_port_hint': '保存后需重启整个应用,不是只重启代理服务',
|
||||
'web_port_doc': '取值范围 1-65535。保存后需要重启整个应用才会生效',
|
||||
'display_level_label': '命令行显示级别', 'display_level_hint': '只影响命令行运行 ProxyCat.py 时的输出',
|
||||
'advanced_group_version': '版本检查',
|
||||
'version_check_url_label': '版本检查地址',
|
||||
'version_check_url_hint': '留空用内置的 GitHub 地址;填了只用这个,保存后下次检查生效',
|
||||
'version_check_url_doc': '留空时并发请求内置的 GitHub 地址(官方 releases.atom、gh-proxy 镜像、GitHub API),谁先返回用谁;填写后只请求该地址。必须是 http/https 开头的完整地址。第三方镜像会看到你的请求 IP,且可能返回缓存的旧版本',
|
||||
'display_level_doc': '仅命令行(ProxyCat.py)模式生效:0 只显示状态行、1 显示经过与出口清单、2 逐条显示请求',
|
||||
'display_level_0': '0 只显示状态行', 'display_level_1': '1 显示经过与出口清单', 'display_level_2': '2 逐条显示请求',
|
||||
'db_maintain_title': '数据库维护', 'db_optimize_btn': '优化数据库',
|
||||
@@ -370,6 +374,7 @@ var translations = {
|
||||
'switching': '刷新中...', 'switch_failed_network': '网络请求失败,请检查服务是否运行',
|
||||
'loading': '加载中...', 'seconds': '秒',
|
||||
'latest_version': '已是最新版本', 'new_version_available': '新版本',
|
||||
'version_check_now': '立即检查更新', 'version_checking': '检查中...', 'version_check_failed': '检查更新失败',
|
||||
'proxy_mode': { cycle: '按顺序使用', loadbalance: '负载均衡',
|
||||
continuous: '持续更换', request: '触发更换' },
|
||||
'operation_failed': '操作失败: {}',
|
||||
@@ -726,6 +731,10 @@ var translations = {
|
||||
'web_port_label': 'Web panel port', 'web_port_hint': 'Needs a full app restart, not just a proxy restart',
|
||||
'web_port_doc': 'Range 1-65535. Takes effect only after restarting the whole app',
|
||||
'display_level_label': 'CLI display level', 'display_level_hint': 'Affects the ProxyCat.py console output only',
|
||||
'advanced_group_version': 'Version check',
|
||||
'version_check_url_label': 'Version check URL',
|
||||
'version_check_url_hint': 'Empty uses the built-in GitHub addresses; a value replaces them and applies to the next check',
|
||||
'version_check_url_doc': 'When empty, the built-in GitHub addresses (official releases.atom, the gh-proxy mirror and the GitHub API) are raced and the first answer wins; a value makes this the only source. Must be a full http/https URL. Third-party mirrors see your request IP and may return a cached, older version',
|
||||
'display_level_doc': 'CLI (ProxyCat.py) mode only: 0 shows the status line, 1 adds progress and the exit list, 2 logs every request',
|
||||
'display_level_0': '0 Status line only', 'display_level_1': '1 Progress and exit list', 'display_level_2': '2 Every request',
|
||||
'db_maintain_title': 'Database Maintenance', 'db_optimize_btn': 'Optimize Database',
|
||||
@@ -809,6 +818,7 @@ var translations = {
|
||||
'switching': 'Refreshing...', 'switch_failed_network': 'Network request failed, check service status',
|
||||
'loading': 'Loading...', 'seconds': 's',
|
||||
'latest_version': 'Up to date', 'new_version_available': 'New version',
|
||||
'version_check_now': 'Check for updates now', 'version_checking': 'Checking...', 'version_check_failed': 'Update check failed',
|
||||
'proxy_mode': { cycle: 'In order', loadbalance: 'Least loaded',
|
||||
continuous: 'Continuous', request: 'On request' },
|
||||
'operation_failed': 'Failed: {}',
|
||||
|
||||
@@ -43,6 +43,9 @@
|
||||
<span class="sb-title" data-i18n="title">ProxyCat 控制面板</span>
|
||||
<span class="sb-meta">
|
||||
<span class="hdr-ver" id="hdr-ver">{{ app_version }}</span>
|
||||
<button class="hdr-ver-refresh" id="ver-refresh" type="button" data-i18n-title="version_check_now" title="立即检查更新">
|
||||
<i class="fa fa-refresh"></i>
|
||||
</button>
|
||||
<span class="hdr-ver-stat" id="ver-stat"></span>
|
||||
</span>
|
||||
</div>
|
||||
@@ -801,6 +804,17 @@
|
||||
|
||||
</div>
|
||||
|
||||
<span class="col-label" data-i18n="advanced_group_version"></span>
|
||||
|
||||
<div class="fg">
|
||||
<div class="fg-label-row">
|
||||
<label class="fg-label" data-i18n="version_check_url_label">版本检查地址</label>
|
||||
<i class="fa fa-info-circle fg-tip" data-i18n-title="version_check_url_doc" aria-hidden="true"></i>
|
||||
</div>
|
||||
<input class="fg-input" name="version_check_url" placeholder="https://github.com/honmashironeko/ProxyCat/releases.atom">
|
||||
<div class="fg-hint" data-i18n="version_check_url_hint">留空用内置的 GitHub 地址;填了只用这个,保存后下次检查生效</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user