@swimmingliu/autovpn 1.6.4 → 1.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -72,7 +72,7 @@ Current Node backend notes:
72
72
  - Add `--skip-deploy --skip-verify` when you want an offline Node pipeline check.
73
73
  - Plain Node foreground deploy/verify runs use Node for Wrangler deploy, primary blocked-project fallback, share-project sync/fallback, custom-domain binding, custom-domain DNS upsert, and verify.
74
74
  - Deploy and verify can be rolled back with `AUTOVPN_STAGE_BACKEND_DEPLOY=python` and `AUTOVPN_STAGE_BACKEND_VERIFY=python`.
75
- - `AUTOVPN_NO_PYTHON=1` disables implicit Python backend resolution and default Python runtime stage fallback. Use it as a v3 readiness gate. Empty offline runs now complete in Node, and Node has direct HTTP speedtest and availability runtimes. Node also has opt-in Mihomo-backed paths: set `AUTOVPN_SPEEDTEST_RUNTIME=mihomo` for controller delay probing and candidate downloads through the local Mihomo proxy, and set `AUTOVPN_AVAILABILITY_RUNTIME=mihomo` to check provider availability through the same per-node proxy runtime.
75
+ - `AUTOVPN_NO_PYTHON=1` disables implicit Python backend resolution and default Python runtime stage fallback. Use it as a v3 readiness gate. Empty offline runs now complete in Node. Speedtest and availability use the per-node Mihomo runtime by default so candidate measurements and provider checks traverse each candidate node. Set `AUTOVPN_SPEEDTEST_RUNTIME=direct` or `AUTOVPN_AVAILABILITY_RUNTIME=direct` only for direct-host diagnostic checks.
76
76
  - Project `.env` is loaded before resolving profile and artifact paths. Explicit process environment values still win over `.env`.
77
77
  - `autovpn serve` is Node-native and exposes the browser UI plus `/api/health`,
78
78
  `/api/state`, `/api/runs`, `/api/runs/current/stop`, and `/api/events`.
@@ -17,6 +17,9 @@ const CHALLENGE_PHRASES = [
17
17
  'verify you are human',
18
18
  'enable javascript and cookies'
19
19
  ];
20
+ const GEMINI_REGION_MARKERS = [',2,1,200,"', ',2,1,200,\\"'];
21
+ const CLAUDE_BLOCKED_CODES = new Set(['AF', 'BY', 'CN', 'CU', 'HK', 'IR', 'KP', 'MO', 'RU', 'SY']);
22
+ const GEMINI_BLOCKED_CODES = new Set(['CHN', 'RUS', 'BLR', 'CUB', 'IRN', 'PRK', 'SYR', 'HKG', 'MAC']);
20
23
  const PYTHON_AVAILABILITY_HELPER = `
21
24
  import json
22
25
  import sys
@@ -75,6 +78,30 @@ function hostnameFor(url) {
75
78
  return '';
76
79
  }
77
80
  }
81
+ function targetKey(target) {
82
+ return target.name.trim().toLowerCase().replaceAll('-', '_').replaceAll(' ', '_');
83
+ }
84
+ function extractTraceLocation(body) {
85
+ for (const line of body.split(/\r?\n/)) {
86
+ if (line.startsWith('loc=')) {
87
+ return line.slice(4).trim().toUpperCase();
88
+ }
89
+ }
90
+ return '';
91
+ }
92
+ function extractGeminiCountryCode(body) {
93
+ for (const marker of GEMINI_REGION_MARKERS) {
94
+ const index = body.indexOf(marker);
95
+ if (index < 0) {
96
+ continue;
97
+ }
98
+ const code = body.slice(index + marker.length, index + marker.length + 3);
99
+ if (/^[A-Z]{3}$/.test(code)) {
100
+ return code;
101
+ }
102
+ }
103
+ return '';
104
+ }
78
105
  export function normalizeProviderTargets(targets) {
79
106
  if (targets == null) {
80
107
  return PROVIDER_TARGETS.map((target) => ({ ...target, allowed_hosts: [...target.allowed_hosts], negative_phrases: [...target.negative_phrases] }));
@@ -126,6 +153,83 @@ export function evaluateProviderResponse(target, response) {
126
153
  matched_phrase: ''
127
154
  };
128
155
  }
156
+ const key = targetKey(target);
157
+ if (key === 'chatgpt_ios') {
158
+ const body = response.body.toLowerCase();
159
+ if (body.includes('you may be connected to a disallowed isp')) {
160
+ return {
161
+ provider: target.name,
162
+ passed: false,
163
+ reason: 'disallowed_isp',
164
+ status_code: statusCode,
165
+ final_url: finalUrl,
166
+ matched_phrase: ''
167
+ };
168
+ }
169
+ if (body.includes('request is not allowed. please try again later.')) {
170
+ return {
171
+ provider: target.name,
172
+ passed: true,
173
+ reason: 'ok',
174
+ status_code: statusCode,
175
+ final_url: finalUrl,
176
+ matched_phrase: ''
177
+ };
178
+ }
179
+ if (body.includes('sorry, you have been blocked')) {
180
+ return {
181
+ provider: target.name,
182
+ passed: false,
183
+ reason: 'blocked',
184
+ status_code: statusCode,
185
+ final_url: finalUrl,
186
+ matched_phrase: ''
187
+ };
188
+ }
189
+ return {
190
+ provider: target.name,
191
+ passed: false,
192
+ reason: 'unlock_failed',
193
+ status_code: statusCode,
194
+ final_url: finalUrl,
195
+ matched_phrase: ''
196
+ };
197
+ }
198
+ if (key === 'chatgpt' || key === 'chatgpt_web') {
199
+ const unsupported = response.body.toLowerCase().includes('unsupported_country');
200
+ return {
201
+ provider: target.name,
202
+ passed: !unsupported,
203
+ reason: unsupported ? 'unsupported_region' : 'ok',
204
+ status_code: statusCode,
205
+ final_url: finalUrl,
206
+ matched_phrase: unsupported ? 'unsupported_country' : ''
207
+ };
208
+ }
209
+ if (key === 'claude') {
210
+ const countryCode = extractTraceLocation(response.body);
211
+ const blocked = CLAUDE_BLOCKED_CODES.has(countryCode);
212
+ return {
213
+ provider: target.name,
214
+ passed: Boolean(countryCode) && !blocked,
215
+ reason: !countryCode ? 'unlock_failed' : blocked ? 'unsupported_region' : 'ok',
216
+ status_code: statusCode,
217
+ final_url: finalUrl,
218
+ matched_phrase: countryCode
219
+ };
220
+ }
221
+ if (key === 'gemini') {
222
+ const countryCode = extractGeminiCountryCode(response.body);
223
+ const blocked = GEMINI_BLOCKED_CODES.has(countryCode);
224
+ return {
225
+ provider: target.name,
226
+ passed: Boolean(countryCode) && !blocked,
227
+ reason: !countryCode ? 'unlock_failed' : blocked ? 'unsupported_region' : 'ok',
228
+ status_code: statusCode,
229
+ final_url: finalUrl,
230
+ matched_phrase: countryCode
231
+ };
232
+ }
129
233
  if (statusCode >= 400) {
130
234
  return {
131
235
  provider: target.name,
@@ -594,7 +698,8 @@ async function checkBatchInNode(input, options) {
594
698
  return [];
595
699
  }
596
700
  const targets = normalizeProviderTargets(input.targets);
597
- const useMihomoRuntime = String((options.env ?? process.env).AUTOVPN_AVAILABILITY_RUNTIME ?? '').trim().toLowerCase() === 'mihomo';
701
+ const requestedRuntime = String((options.env ?? process.env).AUTOVPN_AVAILABILITY_RUNTIME ?? '').trim().toLowerCase();
702
+ const useMihomoRuntime = requestedRuntime !== 'direct';
598
703
  const checkLinkAvailability = options.checkLinkAvailability ?? ((speedResult, config) => (useMihomoRuntime
599
704
  ? checkLinkAvailabilityMihomo(speedResult, config, {
600
705
  ...options,
@@ -5,7 +5,7 @@ const ZH_MESSAGES = {
5
5
  locale: 'zh-CN',
6
6
  appTitle: 'AutoVPN',
7
7
  sidebarTitle: 'AutoVPN',
8
- sidebarVersion: 'v.1.6.4',
8
+ sidebarVersion: 'v.1.6.5',
9
9
  brandSubtitle: '概览、运行、结果、订阅、日志、设置统一管理',
10
10
  languageLabel: '',
11
11
  saveButton: '保存配置',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swimmingliu/autovpn",
3
- "version": "1.6.4",
3
+ "version": "1.6.5",
4
4
  "description": "npm wrapper for the AutoVPN headless CLI",
5
5
  "type": "module",
6
6
  "repository": {