diff --git a/.env.example b/.env.example index 251858b..23120e8 100755 --- a/.env.example +++ b/.env.example @@ -1,4 +1,5 @@ APP_NAME=XBoard +APP_FULL_NAME="Deep Link" APP_ENV=production APP_KEY=base64:PZXk5vTuTinfeEVG5FpYv2l6WEhLsyvGpiWK7IgJJ60= APP_DEBUG=false @@ -38,4 +39,4 @@ GOOGLE_CLOUD_KEY_FILE=config/googleCloudStorageKey.json GOOGLE_CLOUD_STORAGE_BUCKET= # Prevent reinstallation -INSTALLED=false \ No newline at end of file +INSTALLED=false diff --git a/.gitignore b/.gitignore index c51b1df..74ee413 100755 --- a/.gitignore +++ b/.gitignore @@ -31,4 +31,19 @@ bun.lockb compose.yaml compose.host.yaml compose.split.yaml -.scribe \ No newline at end of file +.scribe +# Generated tutorial previews and publication exports + +# Local Vite dev-server opt-in (never publish) +/storage/framework/stellar-vite.hot + +# Separate frontend repository (commit it independently) +/stellar/ +# Local reports, backups, screenshots and private operational records +/codex/output/ +/deliverables/ +# Local native-client build products +/macos-client/build/ +/macos-client/.build/ + +/macos-client/.swiftpm/ diff --git a/app/Http/Controllers/V1/Guest/CommController.php b/app/Http/Controllers/V1/Guest/CommController.php index 00c05c7..ae3c827 100644 --- a/app/Http/Controllers/V1/Guest/CommController.php +++ b/app/Http/Controllers/V1/Guest/CommController.php @@ -13,6 +13,8 @@ class CommController extends Controller public function config() { $data = [ + 'app_name' => config('app.name'), + 'app_full_name' => config('app.full_name'), 'tos_url' => admin_setting('tos_url'), 'is_email_verify' => (int) admin_setting('email_verify', 0) ? 1 : 0, 'is_invite_force' => (int) admin_setting('invite_force', 0) ? 1 : 0, diff --git a/app/Http/Controllers/V2/Admin/PlanController.php b/app/Http/Controllers/V2/Admin/PlanController.php index 6a39f9f..be74653 100644 --- a/app/Http/Controllers/V2/Admin/PlanController.php +++ b/app/Http/Controllers/V2/Admin/PlanController.php @@ -90,7 +90,8 @@ class PlanController extends Controller $updateData = $request->only([ 'show', 'renew', - 'sell' + 'sell', + 'display_device_limit' ]); $plan = Plan::find($request->input('id')); @@ -98,6 +99,10 @@ class PlanController extends Controller return $this->fail([400202, '该订阅不存在']); } + $request->validate([ + 'display_device_limit' => 'sometimes|nullable|integer|min:0|max:4294967295', + ]); + try { $plan->update($updateData); } catch (\Exception $e) { diff --git a/app/Http/Requests/Admin/PlanSave.php b/app/Http/Requests/Admin/PlanSave.php index c35e4a7..34e5c37 100755 --- a/app/Http/Requests/Admin/PlanSave.php +++ b/app/Http/Requests/Admin/PlanSave.php @@ -33,6 +33,7 @@ class PlanSave extends FormRequest 'group_id' => 'integer|nullable', 'speed_limit' => 'integer|nullable|min:0', 'device_limit' => 'integer|nullable|min:0', + 'display_device_limit' => 'nullable|integer|min:0|max:4294967295', 'capacity_limit' => 'integer|nullable|min:0', 'tags' => 'array|nullable', ]; @@ -133,6 +134,9 @@ class PlanSave extends FormRequest 'group_id.integer' => '权限组ID必须是整数', 'speed_limit.integer' => '速度限制必须是整数', 'speed_limit.min' => '速度限制不能为负数', + 'display_device_limit.integer' => '展示设备数必须是整数', + 'display_device_limit.min' => '展示设备数不能为负数', + 'display_device_limit.max' => '展示设备数过大', 'device_limit.integer' => '设备限制必须是整数', 'device_limit.min' => '设备限制不能为负数', 'capacity_limit.integer' => '容量限制必须是整数', diff --git a/app/Http/Resources/PlanResource.php b/app/Http/Resources/PlanResource.php index b78583a..50cd873 100644 --- a/app/Http/Resources/PlanResource.php +++ b/app/Http/Resources/PlanResource.php @@ -30,6 +30,7 @@ class PlanResource extends JsonResource 'transfer_enable' => $this->resource['transfer_enable'], 'speed_limit' => $this->resource['speed_limit'], 'device_limit' => $this->resource['device_limit'], + 'display_device_limit' => $this->resource['display_device_limit'], 'show' => (bool) $this->resource['show'], 'sell' => (bool) $this->resource['sell'], 'renew' => (bool) $this->resource['renew'], @@ -84,7 +85,9 @@ class PlanResource extends JsonResource { $content = $this->resource['content'] ?? ''; + $displayDevices = $this->resource->getDisplayDeviceLimit(); $replacements = [ + '{{display_devices}}' => $displayDevices === null || $displayDevices === 0 ? __('No Limit') : $displayDevices, '{{transfer}}' => $this->resource['transfer_enable'], '{{speed}}' => $this->resource['speed_limit'] === NULL ? __('No Limit') : $this->resource['speed_limit'], '{{devices}}' => $this->resource['device_limit'] === NULL ? __('No Limit') : $this->resource['device_limit'], diff --git a/app/Models/Plan.php b/app/Models/Plan.php index 13d66f2..ccbd0a5 100755 --- a/app/Models/Plan.php +++ b/app/Models/Plan.php @@ -87,6 +87,7 @@ class Plan extends Model 'capacity_limit', 'sell', 'device_limit', + 'display_device_limit', 'tags' ]; @@ -96,6 +97,7 @@ class Plan extends Model 'created_at' => 'timestamp', 'updated_at' => 'timestamp', 'group_id' => 'integer', + 'display_device_limit' => 'integer', 'prices' => 'array', 'tags' => 'array', 'reset_traffic_method' => 'integer', @@ -302,6 +304,12 @@ class Plan extends Model return array_key_exists($period, self::getAvailablePeriods()); } + public function getDisplayDeviceLimit(): ?int + { + $value = $this->display_device_limit ?? $this->device_limit; + return $value === null ? null : (int) $value; + } + public function users(): HasMany { return $this->hasMany(User::class); diff --git a/app/Support/StellarDevAssets.php b/app/Support/StellarDevAssets.php new file mode 100644 index 0000000..c674785 --- /dev/null +++ b/app/Support/StellarDevAssets.php @@ -0,0 +1,36 @@ +getHost(), ['127.0.0.1', 'localhost', '::1'], true) + || !is_file($marker) || trim(file_get_contents($marker)) !== $origin) { + return $html; + } + // A stale marker after an interrupted process falls back to built assets. + $socket = @fsockopen('127.0.0.1', 5173, $error, $message, 0.1); + if (!$socket) return $html; + fclose($socket); + + $html = preg_replace('#]*src="/theme/Stellar/assets/[^"]+"[^>]*>#', '', $html); + $html = preg_replace('#]*href="/theme/Stellar/assets/[^"]+"[^>]*>#', '', $html); + $html = str_replace('src="/theme/Stellar/env.js"', 'src="'.$origin.'/env.js"', $html); + // Allow only this loopback dev origin; production HTML is left unchanged. + $html = str_replace("script-src 'self'", "script-src 'self' ".$origin, $html); + $html = str_replace("style-src 'self'", "style-src 'self' ".$origin, $html); + $html = str_replace("connect-src 'self'", "connect-src 'self' ".$origin." ws://127.0.0.1:5173", $html); + $html = str_replace("img-src 'self'", "img-src 'self' ".$origin, $html); + $html = str_replace("font-src 'self'", "font-src 'self' ".$origin, $html); + $scripts = '' + .''; + return str_replace('', $scripts.'', $html); + } +} diff --git a/config/app.php b/config/app.php index 3ed7fe4..b71a9c5 100755 --- a/config/app.php +++ b/config/app.php @@ -15,6 +15,9 @@ return [ 'name' => env('APP_NAME', 'Laravel'), + // Human-readable full brand name; the site name remains APP_NAME. + 'full_name' => env('APP_FULL_NAME', env('APP_NAME', 'Laravel')), + /* |-------------------------------------------------------------------------- | Application Environment diff --git a/content/tutorials/README.txt b/content/tutorials/README.txt new file mode 100644 index 0000000..65f2b03 --- /dev/null +++ b/content/tutorials/README.txt @@ -0,0 +1,11 @@ +教程为独立页面,Windows 教程按钮用普通链接打开新标签页。 +content/tutorials/<平台>/<客户端>/meta.json:标题、语言和平台信息。 +同目录 zh-CN.html:教程正文,只维护内容,不复制公共页脚和样式。 +同目录 images/:该教程配图,正文引用 ./images/图片名。 +resources/tutorials/layout.html:公共页面结构和页脚。 +resources/tutorials/tutorial.css:公共样式。 +scripts/tutorials/build.mjs:验证图片,生成 resources/views/tutorials 页面、public/tutorials 配图及前端入口索引。 +stellar 的 npm run build 自动构建教程。新增平台目录后构建即可连接对应按钮。 +生成文件不要直接编辑;从正文、模板、图片源文件修改。 + +网站名称统一使用外层 .env 的 APP_NAME;模板使用 {{brand}},由后端在请求时填入。修改 APP_NAME 后刷新 Laravel 配置缓存,无需重新编译教程。 diff --git a/content/tutorials/android/flclash/images/01-download.png b/content/tutorials/android/flclash/images/01-download.png new file mode 100644 index 0000000..3552035 Binary files /dev/null and b/content/tutorials/android/flclash/images/01-download.png differ diff --git a/content/tutorials/android/flclash/images/02-install.svg b/content/tutorials/android/flclash/images/02-install.svg new file mode 100644 index 0000000..72a7f33 --- /dev/null +++ b/content/tutorials/android/flclash/images/02-install.svg @@ -0,0 +1 @@ +下载 APK,安装 FlClash网站提供 ARM64 安装包,适用于多数安卓手机1打开安装包在下载记录中打开 APK先确认文件名为 FlClash2允许此次安装按系统提示确认安装来源无需开启全局安装权限3完成安装点击安装,再打开 FlClash从应用列表启动客户端操作示意 · 实际界面可能因客户端版本或手机系统有所不同 \ No newline at end of file diff --git a/content/tutorials/android/flclash/images/03-import.png b/content/tutorials/android/flclash/images/03-import.png new file mode 100644 index 0000000..57cacb1 Binary files /dev/null and b/content/tutorials/android/flclash/images/03-import.png differ diff --git a/content/tutorials/android/flclash/images/04-manual-import.svg b/content/tutorials/android/flclash/images/04-manual-import.svg new file mode 100644 index 0000000..f4ceedf --- /dev/null +++ b/content/tutorials/android/flclash/images/04-manual-import.svg @@ -0,0 +1 @@ +复制地址,导入配置不必手动输入服务器信息1复制配置地址在网站我的订阅中复制地址不要公开分享2配置 → + → URL打开 FlClash 的配置页面选择通过 URL 获取配置3粘贴并提交在 URL 栏粘贴完整地址导入后选中这份配置操作示意 · 实际界面可能因客户端版本或手机系统有所不同 \ No newline at end of file diff --git a/content/tutorials/android/flclash/images/05-proxy.svg b/content/tutorials/android/flclash/images/05-proxy.svg new file mode 100644 index 0000000..107d307 --- /dev/null +++ b/content/tutorials/android/flclash/images/05-proxy.svg @@ -0,0 +1 @@ +规则模式,选择可用线路线路名称与可选代理组以你的订阅为准1选中配置先确认订阅已导入没有节点时尝试更新2模式选「规则」在仪表盘的模式选项切换按订阅规则进行分流3选择线路打开代理组选择可用节点也可使用自动选择组操作示意 · 实际界面可能因客户端版本或手机系统有所不同 \ No newline at end of file diff --git a/content/tutorials/android/flclash/images/06-connect.svg b/content/tutorials/android/flclash/images/06-connect.svg new file mode 100644 index 0000000..71fcf27 --- /dev/null +++ b/content/tutorials/android/flclash/images/06-connect.svg @@ -0,0 +1 @@ +启动连接,确认 VPN 授权Android 使用系统 VPN 接管连接1点击启动返回 FlClash 仪表盘点击启动或播放按钮2确认连接请求首次启动出现系统提示确认应用为 FlClash3验证连接查看 VPN 状态并访问网页停止时回到客户端暂停操作示意 · 实际界面可能因客户端版本或手机系统有所不同 \ No newline at end of file diff --git a/content/tutorials/android/flclash/images/import-buttons.png b/content/tutorials/android/flclash/images/import-buttons.png new file mode 100644 index 0000000..87a50d5 Binary files /dev/null and b/content/tutorials/android/flclash/images/import-buttons.png differ diff --git a/content/tutorials/android/flclash/meta.json b/content/tutorials/android/flclash/meta.json new file mode 100644 index 0000000..8f8943c --- /dev/null +++ b/content/tutorials/android/flclash/meta.json @@ -0,0 +1,11 @@ +{ + "platform": "android", + "client": "flclash", + "title": "Android 使用教程", + "language": "zh-CN", + "sources": [ + "https://github.com/chen08209/FlClash/tree/v0.8.99", + "https://developer.android.com/develop/connectivity/vpn" + ], + "illustrations": "网站实际截图;客户端操作示意根据 v0.8.99 官方源码整理,非手机实拍" +} diff --git a/content/tutorials/android/flclash/zh-CN.html b/content/tutorials/android/flclash/zh-CN.html new file mode 100644 index 0000000..f11de0a --- /dev/null +++ b/content/tutorials/android/flclash/zh-CN.html @@ -0,0 +1,32 @@ +
{{brand}}{{brand}}

Android 使用教程

FlClash · 从下载安装到开始连接

+ +
开始前:请先登录网站,确认已有可用套餐。本文使用网站提供的 FlClash 安卓客户端,默认下载 ARM64 安装包。网站操作图为实际截图;客户端配图为根据官方版本整理的操作示意,按钮与布局以实际版本为准。
+

1下载并安装客户端

+

在手机浏览器登录网站,进入 仪表盘 → 客户端下载,找到 Android,点击右侧 下载。

+
网站 Android 客户端下载入口
点击 Android 行右侧的「下载」;手机端排列可能略有不同
+

下载完成后,从浏览器下载记录或文件管理器打开 FlClash 的 .apk 安装包。如果系统提示当前来源不允许安装应用,按提示进入设置,只允许当前用于打开安装包的浏览器或文件管理器安装,再返回完成安装。

+
打开 APK、允许此次来源安装、安装并打开 FlClash 的操作示意
安装操作示意:系统提示名称可能因手机品牌不同而变化
+

安装后打开 FlClash。网站当前提供的 arm64-v8a 安装包适用于多数安卓手机;如果提示安装包不兼容,可到 官方发布页选择适合设备架构的 Android APK。

+

2把网站配置导入客户端

+

回到网站仪表盘,在 我的订阅 里点击 一键导入。

+
一键导入与复制配置地址按钮
「一键导入」入口
+

弹窗中选择 Android,再点击 FlClash。浏览器询问是否打开应用时,确认打开已安装的 FlClash,随后按客户端提示导入配置,并在 配置 页面选中这份订阅。

+
网站一键导入弹窗的 Android 平台与 FlClash 选项
Android → FlClash;手机端排列可能略有不同
+
如果一键导入不成功,可以用复制配置地址的方式导入

点击网站上的 复制配置地址,打开 FlClash 的 配置 页面,点击 + 添加配置,选择 URL(通过 URL 获取配置文件)。将完整地址粘贴到 URL 栏,按需填写名称,点击 提交;导入完成后选中这份配置。

+
网站复制地址、FlClash 配置页面添加 URL、提交并选中配置的操作示意
手动导入操作示意
+

配置地址属于你的个人订阅,请勿发到公开群聊或给他人使用。

+

3选择线路与规则模式

+

确认订阅已导入并选中后,在 FlClash 的 仪表盘 中找到模式选项,选择 规则。再打开 代理组,选一条可用线路;也可以使用订阅提供的自动选择组。

+
选中配置、选择规则模式、在代理组中选择线路的操作示意
规则模式与线路选择示意,实际代理组以订阅为准
+

规则模式会按订阅配置分流。节点列表为空时,先回到配置页面,确认已选中刚导入的配置,再尝试更新订阅。

+

4启动连接,允许 VPN 请求

+

回到 FlClash 的 仪表盘,点击 启动 / 播放按钮。首次连接时 Android 会弹出 VPN 连接请求,确认请求来自 FlClash 后,点击 确定 / 允许。

+
启动 FlClash、确认 Android VPN 连接请求并验证连接的操作示意
连接与 VPN 授权示意;系统弹窗文字以你的手机为准
+

连接后,系统状态栏或 VPN 设置中通常会显示 VPN 标识。用浏览器访问你需要的网站,确认能正常加载。停止使用时,回到 FlClash 点击 暂停 / 停止按钮。

+
完成标志:配置已导入并选中、线路可用、VPN 已启动,浏览器访问正常。Android 通过系统 VPN 接管连接,不需要寻找 Windows/macOS 的系统代理开关。
+

遇到问题

+
安装失败或提示安装包不兼容

确认下载已完成,检查 APK 是否适合设备架构。默认包为 ARM64;其他设备到官方发布页选择对应版本。若安装已存在的应用时失败,先核对现有应用的来源,不要直接卸载,以免丢失配置。

+
一键导入没有反应

先确认 FlClash 已安装并打开。若浏览器未唤起应用,使用第 2 步里的复制地址导入。

+
导入后看不到节点

确认套餐可用,检查地址是否完整,更新订阅并选中导入的配置。

+
启动后没有连接,或网页仍打不开

确认已允许 FlClash 的 VPN 连接请求,并先停止其他正在使用的 VPN。尝试更换线路;仍有问题可在网站工单中附上客户端错误信息。

+
切到后台后连接中断

检查手机是否把 FlClash 的后台运行限制为禁止或严格省电;根据实际需要允许该应用后台运行,再重新连接。

diff --git a/content/tutorials/ios/hiddify/images/01-store.png b/content/tutorials/ios/hiddify/images/01-store.png new file mode 100644 index 0000000..27ae383 Binary files /dev/null and b/content/tutorials/ios/hiddify/images/01-store.png differ diff --git a/content/tutorials/ios/hiddify/images/02-install.svg b/content/tutorials/ios/hiddify/images/02-install.svg new file mode 100644 index 0000000..8324584 --- /dev/null +++ b/content/tutorials/ios/hiddify/images/02-install.svg @@ -0,0 +1 @@ +从 App Store 获取 Hiddify先在 App Store 登录美区 Apple ID,再获取 Hiddify1打开商店网站 iOS 行点「商店」打开 Hiddify 商店页面2点击获取按系统提示确认下载安装 Hiddify3安装后打开从桌面启动 Hiddify准备导入网站配置Hiddify 操作示意 \ No newline at end of file diff --git a/content/tutorials/ios/hiddify/images/03-import.png b/content/tutorials/ios/hiddify/images/03-import.png new file mode 100644 index 0000000..f0b2894 Binary files /dev/null and b/content/tutorials/ios/hiddify/images/03-import.png differ diff --git a/content/tutorials/ios/hiddify/images/04-manual-import.svg b/content/tutorials/ios/hiddify/images/04-manual-import.svg new file mode 100644 index 0000000..240e635 --- /dev/null +++ b/content/tutorials/ios/hiddify/images/04-manual-import.svg @@ -0,0 +1 @@ +复制配置地址,添加到 Hiddify从剪贴板添加,或手动填写订阅 URL1复制配置地址在网站点击对应按钮保留完整个人订阅地址2点击首页 +从剪贴板添加配置若提示粘贴,请确认允许3确认当前配置或手动填名称与 URL保存后选中刚导入的配置Hiddify 操作示意 \ No newline at end of file diff --git a/content/tutorials/ios/hiddify/images/05-connect.svg b/content/tutorials/ios/hiddify/images/05-connect.svg new file mode 100644 index 0000000..8526111 --- /dev/null +++ b/content/tutorials/ios/hiddify/images/05-connect.svg @@ -0,0 +1 @@ +允许 VPN 配置,开始连接首次使用按 iOS 系统提示完成授权1选中网站配置打开首页的连接按钮确认使用刚导入的订阅2允许 VPN 配置确认请求来自 Hiddify按系统提示验证身份3确认已连接等待连接状态更新访问网站检验实际连接Hiddify 操作示意 \ No newline at end of file diff --git a/content/tutorials/ios/hiddify/images/06-proxy.svg b/content/tutorials/ios/hiddify/images/06-proxy.svg new file mode 100644 index 0000000..17aaab3 --- /dev/null +++ b/content/tutorials/ios/hiddify/images/06-proxy.svg @@ -0,0 +1 @@ +选择自动或手动线路连接后打开「代理 / Proxies」,按订阅提供的选项选择1打开代理页面查看当前配置中的线路列表为空先更新配置2自动选择可保留可用的自动组按测试结果选择线路3手动切换也可选一条可用节点更换后再次检查访问Hiddify 操作示意 \ No newline at end of file diff --git a/content/tutorials/ios/hiddify/images/import-buttons.png b/content/tutorials/ios/hiddify/images/import-buttons.png new file mode 100644 index 0000000..87a50d5 Binary files /dev/null and b/content/tutorials/ios/hiddify/images/import-buttons.png differ diff --git a/content/tutorials/ios/hiddify/meta.json b/content/tutorials/ios/hiddify/meta.json new file mode 100644 index 0000000..c22dd8f --- /dev/null +++ b/content/tutorials/ios/hiddify/meta.json @@ -0,0 +1,16 @@ +{ + "platform": "ios", + "client": "hiddify", + "title": "iOS 使用教程", + "language": "zh-CN", + "sources": [ + "https://apps.apple.com/us/app/hiddify-proxy-vpn/id6596777532", + "https://hiddify.com/app/How-to-use-Hiddify-app/", + "https://github.com/hiddify/hiddify-app/blob/main/lib/features/profile/add/add_profile_modal.dart", + "https://github.com/hiddify/hiddify-app/blob/main/ios/Runner/VPN/VPNManager.swift", + "https://support.apple.com/zh-cn/108647", + "https://www.aiai1888.com/product/d36b6e3f-cd8b-4959-b57f-e8a512b5876f" + ], + "illustrations": "网站部分为实际截图;客户端部分为依据官方文档与源码制作的操作示意,并非 iPhone 实机截图。", + "verifiedAt": "2026-10-08" +} diff --git a/content/tutorials/ios/hiddify/zh-CN.html b/content/tutorials/ios/hiddify/zh-CN.html new file mode 100644 index 0000000..eaf066d --- /dev/null +++ b/content/tutorials/ios/hiddify/zh-CN.html @@ -0,0 +1,34 @@ +
{{brand}}{{brand}}

iOS 使用教程

Hiddify · 从下载安装到开始连接

+ +
开始前:登录网站,确认套餐可用。设备需运行 iOS 15.0 或以上。
+

1从 App Store 获取客户端

+
首先需要在 App Store 登录海外 Apple ID,如果没有海外 Apple ID 且自己不会注册,可以从直接这里购买。
+

在手机浏览器登录网站,进入 仪表盘 → 客户端下载,找到 iOS,点击右侧 商店。

+
网站 iOS 客户端商店入口
点击 iOS 行右侧的「商店」
+

在 App Store 点击 获取,安装 Hiddify Proxy & VPN,然后打开应用。

+
从商店获取并打开 Hiddify 的操作示意
商店获取与安装操作示意
+
+

2把网站配置导入客户端

+

回到网站仪表盘,在 我的订阅 里点击 一键导入。

+
网站一键导入与复制配置地址按钮
「一键导入」入口
+

弹窗中选择 iOS,再点击 Hiddify。浏览器询问是否打开应用时,确认打开已安装的 Hiddify,并按客户端提示确认添加配置。

+
网站一键导入面板中选中 iOS,并显示 Hiddify 选项
iOS → Hiddify
+

导入后,在 Hiddify 首页选中这份配置。

+
如果一键导入不成功,可以用复制配置地址的方式导入

点击网站的 复制配置地址,打开 Hiddify 首页,点击 + 添加配置,选择从剪贴板添加(Add from clipboard);若系统询问是否允许粘贴,确认允许。也可以选择手动添加(Add manually),填写名称和完整的 URL,再点击保存(Save)。添加后选中这份配置。

+
复制地址、从剪贴板或手动添加、选中当前配置的操作示意
手动导入操作示意
+

配置地址属于你的个人订阅,请勿发到公开群聊或给他人使用。

+

3允许 VPN 配置,开始连接

+

点击 Hiddify 首页的连接按钮。系统提示添加 VPN 配置时,点击允许,按提示完成身份验证。

+
点击连接、允许 Hiddify 添加 VPN 配置、检查连接状态的操作示意
iOS VPN 授权操作示意
+

显示已连接后,用浏览器访问网站检查连接。停止使用时,点击断开连接。

+
+

4查看线路,按需切换

+

连接后打开代理(Proxies)页面,使用自动选择组,或手动选择一条可用线路。

+
打开代理页面、使用自动选择或手动切换线路的操作示意
线路选择操作示意
+

切换后再次访问网站确认可用。若列表为空,先确认当前配置已选中,并更新订阅。

+
+

遇到问题

+
商店提示应用不可用,或无法安装

检查 App Store 地区是否提供 Hiddify,以及设备是否满足 iOS 15.0 或以上要求。

+
一键导入没有反应

先确认 Hiddify 已安装并打开。回到浏览器再次尝试,仍不成功时使用第 2 步的复制配置地址导入。

+
导入后看不到线路

确认套餐可用,检查地址是否完整,更新订阅并选中刚导入的配置。

+
没有允许 VPN,或连接后仍打不开网页

回到 Hiddify 再次点击连接,按系统提示完成 VPN 授权。确认当前配置正确,先断开其他正在使用的 VPN,再尝试更换线路。仍有问题可在网站工单中附上客户端错误信息。

diff --git a/content/tutorials/linux/clash-verge/images/01-download.png b/content/tutorials/linux/clash-verge/images/01-download.png new file mode 100644 index 0000000..ea441e5 Binary files /dev/null and b/content/tutorials/linux/clash-verge/images/01-download.png differ diff --git a/content/tutorials/linux/clash-verge/images/02-install.svg b/content/tutorials/linux/clash-verge/images/02-install.svg new file mode 100644 index 0000000..77f3c46 --- /dev/null +++ b/content/tutorials/linux/clash-verge/images/02-install.svg @@ -0,0 +1 @@ +选择适合系统的安装包先看发行版,再看处理器架构1Debian / Ubuntu.deb 安装包使用 apt 安装2Fedora / RHEL.rpm 安装包使用 dnf 安装3选择架构x86_64 → amd64 / x64aarch64 → arm64Linux 操作示意 · Clash Verge Rev \ No newline at end of file diff --git a/content/tutorials/linux/clash-verge/images/03-import.svg b/content/tutorials/linux/clash-verge/images/03-import.svg new file mode 100644 index 0000000..2023b57 --- /dev/null +++ b/content/tutorials/linux/clash-verge/images/03-import.svg @@ -0,0 +1 @@ +复制地址,导入订阅配置地址直接从网站复制,不用填写服务器参数1复制配置地址网站 → 我的订阅复制完整的订阅地址2订阅 → 导入打开 Clash Verge Rev粘贴地址,点击导入3选中订阅点击刚导入的订阅卡片使其成为当前配置Linux 操作示意 · Clash Verge Rev \ No newline at end of file diff --git a/content/tutorials/linux/clash-verge/images/04-proxy.svg b/content/tutorials/linux/clash-verge/images/04-proxy.svg new file mode 100644 index 0000000..45a38e6 --- /dev/null +++ b/content/tutorials/linux/clash-verge/images/04-proxy.svg @@ -0,0 +1 @@ +选择模式与线路使用订阅中提供的代理组1打开代理页面确认订阅已经选中节点列表应显示线路2选择规则模式按配置自动分流不必一直使用全局模式3选择可用线路在代理组内选择节点也可选自动选择组Linux 操作示意 · Clash Verge Rev \ No newline at end of file diff --git a/content/tutorials/linux/clash-verge/images/05-connect.svg b/content/tutorials/linux/clash-verge/images/05-connect.svg new file mode 100644 index 0000000..d043541 --- /dev/null +++ b/content/tutorials/linux/clash-verge/images/05-connect.svg @@ -0,0 +1 @@ +开启系统代理,开始使用普通浏览器访问先使用系统代理1系统代理 → 开在客户端打开开关让支持系统代理的应用使用2浏览器访问打开需要访问的网站无法加载时换一条线路3停止使用先关闭系统代理再退出客户端Linux 操作示意 · Clash Verge Rev \ No newline at end of file diff --git a/content/tutorials/linux/clash-verge/images/import-buttons.png b/content/tutorials/linux/clash-verge/images/import-buttons.png new file mode 100644 index 0000000..87a50d5 Binary files /dev/null and b/content/tutorials/linux/clash-verge/images/import-buttons.png differ diff --git a/content/tutorials/linux/clash-verge/meta.json b/content/tutorials/linux/clash-verge/meta.json new file mode 100644 index 0000000..9287174 --- /dev/null +++ b/content/tutorials/linux/clash-verge/meta.json @@ -0,0 +1,6 @@ +{ + "platform": "linux", + "client": "clash-verge", + "title": "Linux 使用教程", + "language": "zh-CN" +} diff --git a/content/tutorials/linux/clash-verge/zh-CN.html b/content/tutorials/linux/clash-verge/zh-CN.html new file mode 100644 index 0000000..a9b178a --- /dev/null +++ b/content/tutorials/linux/clash-verge/zh-CN.html @@ -0,0 +1,35 @@ +
{{brand}}{{brand}}

Linux 使用教程

Clash Verge Rev · 从下载安装到开始连接

+ +
适用于带图形界面的 Linux 桌面,Ubuntu 建议使用 24.04 或更新版本。开始前请登录网站,并准备好可用套餐。网站配图为实际截图,客户端配图为操作示意。
+

1下载并安装客户端

+

进入网站的 仪表盘 → 客户端下载,找到 Linux,点击 下载,打开 Clash Verge Rev 官方发布页。在页面的 Assets 中选择安装包。

+
网站 Linux 客户端下载入口
Linux 行右侧的「下载」
+

Ubuntu / Debian / Deepin 选择 .deb;Fedora / RHEL 系 选择 .rpm。在终端运行 uname -m 查看架构:x86_64 选 amd64 或 x86_64,aarch64 选 arm64 或 aarch64,armv7l 选 armhf 或 armhfp。

+
Linux 发行版与安装包架构选择示意
先选安装包格式,再选处理器架构
+

下载完成后,在文件管理器打开安装包所在文件夹,右键选择 在终端中打开。以下以网站当前提供的 2.5.7、x86_64 安装包为例:

+

Ubuntu / Debian:

sudo apt install ./Clash.Verge_2.5.7_amd64.deb
+

Fedora / RHEL 系:

sudo dnf install ./Clash.Verge-2.5.7-1.x86_64.rpm
+

输入系统密码并按提示完成安装;终端输入密码时不显示字符是正常现象。其他架构请将命令中的文件名替换为实际下载的文件名。安装后,从应用菜单打开 Clash Verge Rev。

+
Arch Linux / Manjaro 安装方式

已有 AUR 助手 yay 时,可运行 yay -S clash-verge-rev-bin 安装正式版。其他安装方式参见 官方安装说明。

+
+

2把网站配置导入客户端

+

回到仪表盘,在 我的订阅 中点击 复制配置地址。

+
网站我的订阅中的复制配置地址按钮
点击「复制配置地址」
+

打开 Clash Verge Rev 的 订阅 页面,将地址粘贴到顶部的导入栏,点击 导入。导入完成后,点击这份订阅卡片,选中它作为当前配置。

+
复制配置地址、导入 Clash Verge Rev 订阅并选中的操作示意
导入后记得选中这份订阅
+
配置地址是你的个人订阅,请勿公开分享。
+

3选择线路与规则模式

+

打开客户端的 代理 页面,选择 规则 模式,再在代理组中选择一条可用线路;也可以使用订阅提供的自动选择组。

+
Clash Verge Rev 规则模式和线路选择操作示意
规则模式与线路选择
+

节点列表为空时,返回订阅页面更新订阅,并确认已选中这份配置。

+

4开启系统代理,开始使用

+

在客户端打开 系统代理 开关,再用浏览器访问你需要的网站。停止使用时,先关闭系统代理,再退出客户端。

+
开启系统代理、浏览器验证和停止连接的操作示意
完成导入、选好线路后开启系统代理
+
系统代理主要作用于遵循系统代理设置的应用。终端工具或部分应用不会自动使用它;需要接管更多应用时,可安装客户端服务并开启 TUN 模式,按提示授权。
+

遇到问题

+
安装时提示架构不支持或缺少依赖

确认安装包架构与 uname -m 的结果一致。使用 apt 或 dnf 安装,让包管理器处理依赖;Ubuntu 20.04 等旧系统可能缺少所需库,建议升级到较新版本。

+
应用打不开、黑屏或界面卡顿

Linux 桌面环境与显卡驱动可能影响界面启动。可参照 Linux 启动问题说明,根据自己的报错处理;向网站工单提供发行版、桌面环境与错误信息,有助于定位问题。

+
导入失败或没有节点

重新复制完整配置地址,确认套餐可用,更新订阅并选中导入的配置。

+
开启后网页仍打不开

确认系统代理已开启,并停止其他代理客户端。尝试换一条线路;若浏览器使用独立代理设置,将其改为使用系统代理。仍有问题可提交工单。

+
没有桌面的 Linux 服务器能用吗?

本教程的 Clash Verge Rev 需要图形界面,不适用于仅通过 SSH 使用的服务器。

+
diff --git a/content/tutorials/macos/clash-verge/images/01-download.png b/content/tutorials/macos/clash-verge/images/01-download.png new file mode 100644 index 0000000..5610a16 Binary files /dev/null and b/content/tutorials/macos/clash-verge/images/01-download.png differ diff --git a/content/tutorials/macos/clash-verge/images/02-chip.png b/content/tutorials/macos/clash-verge/images/02-chip.png new file mode 100644 index 0000000..6732ce1 Binary files /dev/null and b/content/tutorials/macos/clash-verge/images/02-chip.png differ diff --git a/content/tutorials/macos/clash-verge/images/03-install.png b/content/tutorials/macos/clash-verge/images/03-install.png new file mode 100644 index 0000000..fa2a322 Binary files /dev/null and b/content/tutorials/macos/clash-verge/images/03-install.png differ diff --git a/content/tutorials/macos/clash-verge/images/04-macos-import.png b/content/tutorials/macos/clash-verge/images/04-macos-import.png new file mode 100644 index 0000000..d84553f Binary files /dev/null and b/content/tutorials/macos/clash-verge/images/04-macos-import.png differ diff --git a/content/tutorials/macos/clash-verge/images/05-manual-import.png b/content/tutorials/macos/clash-verge/images/05-manual-import.png new file mode 100644 index 0000000..17c271c Binary files /dev/null and b/content/tutorials/macos/clash-verge/images/05-manual-import.png differ diff --git a/content/tutorials/macos/clash-verge/images/06-proxy.png b/content/tutorials/macos/clash-verge/images/06-proxy.png new file mode 100644 index 0000000..324b1a9 Binary files /dev/null and b/content/tutorials/macos/clash-verge/images/06-proxy.png differ diff --git a/content/tutorials/macos/clash-verge/images/07-system-proxy.png b/content/tutorials/macos/clash-verge/images/07-system-proxy.png new file mode 100644 index 0000000..f015eb2 Binary files /dev/null and b/content/tutorials/macos/clash-verge/images/07-system-proxy.png differ diff --git a/content/tutorials/macos/clash-verge/images/import-buttons.png b/content/tutorials/macos/clash-verge/images/import-buttons.png new file mode 100644 index 0000000..87a50d5 Binary files /dev/null and b/content/tutorials/macos/clash-verge/images/import-buttons.png differ diff --git a/content/tutorials/macos/clash-verge/meta.json b/content/tutorials/macos/clash-verge/meta.json new file mode 100644 index 0000000..b506428 --- /dev/null +++ b/content/tutorials/macos/clash-verge/meta.json @@ -0,0 +1,10 @@ +{ + "platform": "macos", + "client": "clash-verge", + "title": "macOS 使用教程", + "language": "zh-CN", + "sources": [ + "https://www.clashverge.dev/install.html", + "https://www.clashverge.dev/guide/quickstart.html" + ] +} diff --git a/content/tutorials/macos/clash-verge/zh-CN.html b/content/tutorials/macos/clash-verge/zh-CN.html new file mode 100644 index 0000000..7c06840 --- /dev/null +++ b/content/tutorials/macos/clash-verge/zh-CN.html @@ -0,0 +1,33 @@ +
{{brand}}{{brand}}

macOS 使用教程

Clash Verge Rev · 从下载安装到开始连接

+ +
开始前:请先登录网站,确认已有可用套餐。本文适用于 macOS 12 及以上系统,支持 Apple 芯片和 Intel 芯片。网站操作图为实际截图;安装及客户端操作图参考官方文档,线路选择图为 {{brand}} 教程示意,实际线路以你的订阅为准。
+

1下载并安装客户端

+

进入网站的 仪表盘 → 客户端下载,找到 macOS,点击右侧 下载。

+
仪表盘 macOS 客户端下载入口
点击 macOS 行右侧的「下载」
+

在 苹果菜单 → 关于本机 中查看电脑的芯片类型,再在下载弹窗中选择对应版本:显示 Apple M 系列芯片时选择 Apple 芯片(M 系列 / ARM64);显示 Intel 处理器时选择 Intel 芯片(x64)。

+
macOS 下载弹窗中的 Apple 芯片和 Intel 芯片选项
根据「关于本机」中的芯片类型选择版本
+

下载完成后双击 .dmg 文件,把 Clash Verge 图标拖到 Applications(应用程序) 文件夹。复制完成后,从「应用程序」打开 Clash Verge Rev。

+
把 Clash Verge 图标拖入 Applications 文件夹安装
官方 macOS 安装示意:将应用拖入 Applications 文件夹
+
请从「应用程序」启动已经安装的客户端,不要一直在 DMG 安装窗口中运行。安装包下载失败时,可到 官方发布页选择对应架构的 .dmg 文件:Apple 芯片为 aarch64,Intel 芯片为 x64。
+

2把网站配置导入客户端

+

回到仪表盘,在 我的订阅 里点击 一键导入。

+
网站一键导入与复制配置地址按钮
「一键导入」入口
+

弹窗中选择 macOS,再点击 Clash Verge。浏览器询问是否打开应用时,确认打开已安装的 Clash Verge Rev,然后在客户端确认导入。

+
一键导入弹窗中的 macOS 平台及 Clash Verge 选项
macOS → Clash Verge
+
如果一键导入不成功,可以用复制配置地址的方式导入

点击网站上的 复制配置地址,打开 Clash Verge Rev 的 订阅 页面,将地址粘贴到导入栏并点击 导入。导入后选中这份订阅,使它成为当前配置。

+
Clash Verge Rev 订阅页面的导入栏
官方客户端示意:订阅页面粘贴地址并导入
+

配置地址属于你的个人订阅,请勿发到公开群聊或给他人使用。

+

3选择线路与规则模式

+

打开客户端的 代理 页面,选择 规则 模式,再在代理组里选一条可用线路;也可以使用配置中提供的自动选择组。

+
Clash Verge Rev 代理页面的规则模式与线路选择
{{brand}} 教程示意:代理页面与规则模式,实际线路以订阅为准
+

规则模式会按订阅配置分流。节点列表为空时,先回到订阅页面,确认已选中刚导入的配置,再尝试更新订阅。

+

4开启系统代理,开始使用

+

在客户端找到 系统代理 开关并打开,随后用浏览器访问你需要的网站,确认能正常加载。

+
Clash Verge Rev 系统代理开关
官方客户端示意:开启系统代理;新版本开关位置可能不同
+

普通网页使用先开启系统代理即可。停止使用时关闭系统代理,再退出客户端。

+
完成标志:订阅已导入并选中、线路可用、系统代理已开启,浏览器访问正常。
+

遇到问题

+
不确定应该下载哪个版本

打开苹果菜单 → 关于本机。Apple M 系列选择 ARM64,Intel 处理器选择 x64;同时确认系统为 macOS 12 或更新版本。

+
一键导入没有反应

先确认 Clash Verge Rev 已放入「应用程序」并打开;浏览器询问打开应用时确认打开。仍不成功时,使用第 2 步里的复制地址导入。

+
导入后看不到节点

确认套餐可用,检查地址是否完整,更新订阅并选中导入的配置。

+
已经导入,网页仍打不开

确认系统代理已开启,尝试换一条线路;同时避免多个代理客户端一起运行。仍有问题可在网站工单中附上错误信息。

diff --git a/content/tutorials/router/openclash/images/01-download.png b/content/tutorials/router/openclash/images/01-download.png new file mode 100644 index 0000000..a9bb9c7 Binary files /dev/null and b/content/tutorials/router/openclash/images/01-download.png differ diff --git a/content/tutorials/router/openclash/images/02-install.svg b/content/tutorials/router/openclash/images/02-install.svg new file mode 100644 index 0000000..8b9cca8 --- /dev/null +++ b/content/tutorials/router/openclash/images/02-install.svg @@ -0,0 +1 @@ +安装到路由器后台已安装 OpenClash 的用户可直接进入下一步1登录路由器电脑连接路由器网络进入 OpenWrt 管理后台2安装插件系统 → 软件包上传固件支持的安装包3准备内核OpenClash → 版本更新选择匹配设备的内核架构操作示意 · OpenWrt / OpenClash \ No newline at end of file diff --git a/content/tutorials/router/openclash/images/03-subscribe.svg b/content/tutorials/router/openclash/images/03-subscribe.svg new file mode 100644 index 0000000..8b10655 --- /dev/null +++ b/content/tutorials/router/openclash/images/03-subscribe.svg @@ -0,0 +1 @@ +添加网站订阅直接使用 Clash 格式配置,无需在线转换1复制配置地址网站 → 我的订阅复制完整订阅地址2添加配置订阅配置文件名:自定义订阅地址:粘贴网站地址3保存并更新开启该订阅条目保存配置 → 更新配置操作示意 · OpenWrt / OpenClash \ No newline at end of file diff --git a/content/tutorials/router/openclash/images/04-start.svg b/content/tutorials/router/openclash/images/04-start.svg new file mode 100644 index 0000000..76f3192 --- /dev/null +++ b/content/tutorials/router/openclash/images/04-start.svg @@ -0,0 +1 @@ +选中配置并启动配置已下载,内核已就绪1配置管理找到下载后的配置点击切换,设为当前配置2启用 OpenClash回到运行状态页面等待插件和内核运行3选择线路打开控制面板 → 代理选择规则模式与线路操作示意 · OpenWrt / OpenClash \ No newline at end of file diff --git a/content/tutorials/router/openclash/images/05-network.svg b/content/tutorials/router/openclash/images/05-network.svg new file mode 100644 index 0000000..a9ff08d --- /dev/null +++ b/content/tutorials/router/openclash/images/05-network.svg @@ -0,0 +1 @@ +让设备经过软路由只有流量经过软路由,代理才会生效1主路由模式设备连接软路由网络网关与 DNS 自动获取2旁路由模式设备网关指向旁路由DNS 也指向旁路由地址3浏览器测试先停止设备上的其他代理打开网站,确认连接正常操作示意 · OpenWrt / OpenClash \ No newline at end of file diff --git a/content/tutorials/router/openclash/images/import-buttons.png b/content/tutorials/router/openclash/images/import-buttons.png new file mode 100644 index 0000000..87a50d5 Binary files /dev/null and b/content/tutorials/router/openclash/images/import-buttons.png differ diff --git a/content/tutorials/router/openclash/meta.json b/content/tutorials/router/openclash/meta.json new file mode 100644 index 0000000..7c6c6bf --- /dev/null +++ b/content/tutorials/router/openclash/meta.json @@ -0,0 +1,6 @@ +{ + "platform": "router", + "client": "openclash", + "title": "软路由使用教程", + "language": "zh-CN" +} diff --git a/content/tutorials/router/openclash/zh-CN.html b/content/tutorials/router/openclash/zh-CN.html new file mode 100644 index 0000000..3147e33 --- /dev/null +++ b/content/tutorials/router/openclash/zh-CN.html @@ -0,0 +1,40 @@ +
{{brand}}{{brand}}

软路由使用教程

OpenWrt / OpenClash · 从安装插件到全家设备连接

+ +
适用于 OpenWrt 或兼容固件的软路由,需要管理后台权限。已装好 OpenClash 可直接从第 2 步开始。网站配图为实际截图,路由器配图为操作示意。
+

1安装 OpenClash,准备内核

+

电脑连接软路由的网络,用浏览器打开它的管理地址并登录。安装前,在 系统 → 备份 / 升级 中保存一份当前配置备份。

+

在网站 仪表盘 → 客户端下载 中找到 软路由,点击 下载,打开 OpenClash 官方发布页。在 Assets 中下载 luci-app-openclash 安装包。

+
网站软路由下载入口
软路由行右侧的「下载」
+

使用 opkg 的固件选择 .ipk,使用 apk 的固件选择 .apk;这里的 APK 是 OpenWrt 软件包,不是安卓手机安装包。

+

进入路由器 系统 → 软件包,先更新软件包列表,再使用 上传软件包 安装下载的文件。若固件内置 OpenClash 安装入口,也可以直接从该入口安装。

+
登录软路由后台、安装 OpenClash 并准备内核的操作示意
插件安装在路由器上,不是在电脑上
+

安装完成后,刷新路由器后台,进入 服务 → OpenClash。在 插件设置 → 版本更新 中选择与路由器匹配的内核架构,下载或更新 Meta / Mihomo 内核,等下载完成后继续。

+
没有上传入口,或安装提示缺少依赖

使用固件的软件包管理器安装 OpenClash 所需依赖,具体清单见 OpenClash 官方依赖说明。不同固件的防火墙和软件源不同,内核模块必须与固件匹配,不要强制忽略依赖或覆盖系统库。

+
+

2添加网站订阅

+

登录网站,在仪表盘 我的订阅 中点击 复制配置地址。

+
网站复制配置地址按钮
点击「复制配置地址」
+

回到路由器的 服务 → OpenClash → 配置订阅,添加一条订阅,填写:

+
  • 配置文件名:填写一个好认的名字,例如 {{brand}}。
  • 订阅地址:粘贴从网站复制的完整地址。
  • User-Agent:选择 Clash / clash.meta 类型,以获取 Clash 格式配置。
  • 在线订阅转换:关闭,直接使用网站提供的配置。
+

保存这条订阅,保持条目 启用,再点击 保存配置 → 更新配置,等待订阅下载完成。

+
复制地址、添加 OpenClash 配置订阅、保存并更新的操作示意
保存订阅后更新配置
+
订阅地址是你的个人配置,请勿公开分享。
+ +
+

3切换配置,启动并选择线路

+

进入 配置管理,找到刚下载的配置文件,点击 切换。回到 运行状态 页面启用 OpenClash,等待插件与内核运行。

+

打开 OpenClash 提供的 控制面板,进入 代理 页面,选择 规则 模式,并在代理组中选择可用线路;也可以使用订阅提供的自动选择组。

+
切换配置、启用 OpenClash 和在控制面板选择线路的操作示意
订阅下载后,切换配置再启动
+

节点为空时,先回到配置管理确认当前配置,再更新订阅。

+

4让设备经过软路由

+

软路由作为主路由:手机、电脑连接它的 Wi-Fi 或 LAN 网络,通常保持自动获取 IP、网关和 DNS 即可。

+

软路由作为旁路由:需要让使用代理的设备把 默认网关和 DNS 指向旁路由的 LAN 地址。旁路由的上游网关应指向主路由,不能指向自己;旁路由也需要已配置好转发和 NAT。

+
主路由与旁路由的设备接入区别示意
流量必须经过软路由,只有安装插件还不够
+

测试时,先停止手机或电脑上其他代理客户端,再用浏览器访问你需要的网站。停止使用时,在 OpenClash 的运行状态页面关闭插件。

+
旁路由网络结构因设备而异。先在一台设备上测试网关和 DNS,成功后再调整其他设备;请保留原网络设置,便于恢复。
+

遇到问题

+
后台没有 OpenClash 菜单

先刷新或重新登录路由器后台。仍没有菜单时,查看软件包安装结果,确认插件与所需依赖安装成功。

+
提示内核不存在或不能启动

到版本更新页面检查内核是否下载完成、架构是否匹配。仍不能启动时,查看运行日志中的错误。

+
订阅更新失败或配置格式不对

重新复制完整地址,检查路由器本身能否访问订阅网站。订阅的 User-Agent 使用 Clash 类型,关闭在线订阅转换,再更新配置。

+
插件显示运行,但设备无法访问

确认设备流量经过软路由,旁路由场景尤其要检查网关和 DNS。先停止设备上的其他代理,换一条线路再试;若启用后原有网络也不通,先停用 OpenClash,并恢复设备原来的网关与 DNS。

+
仍然无法解决

在网站提交工单,说明固件版本、主路由或旁路由模式及错误信息。OpenClash 的运行日志和调试日志有助于排查;发送前去掉其中的订阅地址、密码等个人配置。

diff --git a/content/tutorials/windows/clash-verge/images/01-download.png b/content/tutorials/windows/clash-verge/images/01-download.png new file mode 100644 index 0000000..a65ebdf Binary files /dev/null and b/content/tutorials/windows/clash-verge/images/01-download.png differ diff --git a/content/tutorials/windows/clash-verge/images/02-import-buttons.png b/content/tutorials/windows/clash-verge/images/02-import-buttons.png new file mode 100644 index 0000000..87a50d5 Binary files /dev/null and b/content/tutorials/windows/clash-verge/images/02-import-buttons.png differ diff --git a/content/tutorials/windows/clash-verge/images/03-windows-import.png b/content/tutorials/windows/clash-verge/images/03-windows-import.png new file mode 100644 index 0000000..135156e Binary files /dev/null and b/content/tutorials/windows/clash-verge/images/03-windows-import.png differ diff --git a/content/tutorials/windows/clash-verge/images/04-manual-import.png b/content/tutorials/windows/clash-verge/images/04-manual-import.png new file mode 100644 index 0000000..17c271c Binary files /dev/null and b/content/tutorials/windows/clash-verge/images/04-manual-import.png differ diff --git a/content/tutorials/windows/clash-verge/images/05-proxy.png b/content/tutorials/windows/clash-verge/images/05-proxy.png new file mode 100644 index 0000000..324b1a9 Binary files /dev/null and b/content/tutorials/windows/clash-verge/images/05-proxy.png differ diff --git a/content/tutorials/windows/clash-verge/images/06-system-proxy.png b/content/tutorials/windows/clash-verge/images/06-system-proxy.png new file mode 100644 index 0000000..f015eb2 Binary files /dev/null and b/content/tutorials/windows/clash-verge/images/06-system-proxy.png differ diff --git a/content/tutorials/windows/clash-verge/meta.json b/content/tutorials/windows/clash-verge/meta.json new file mode 100644 index 0000000..0f97cc0 --- /dev/null +++ b/content/tutorials/windows/clash-verge/meta.json @@ -0,0 +1,6 @@ +{ + "platform": "windows", + "client": "clash-verge", + "title": "Windows 使用教程", + "language": "zh-CN" +} diff --git a/content/tutorials/windows/clash-verge/zh-CN.html b/content/tutorials/windows/clash-verge/zh-CN.html new file mode 100644 index 0000000..d1b3591 --- /dev/null +++ b/content/tutorials/windows/clash-verge/zh-CN.html @@ -0,0 +1,15 @@ +
{{brand}}{{brand}}

Windows 使用教程

Clash Verge Rev · 从下载安装到开始连接

开始前:请先登录网站,确认已有可用套餐。本文适用于 x64 电脑。网站操作图为实际截图;客户端操作图参考官方文档,线路选择图为 {{brand}} 教程示意,实际线路以你的订阅为准。

1下载并安装客户端

进入网站的 仪表盘 → 客户端下载,找到 Windows,点击右侧 下载。

+
Windows 客户端下载入口
点击 Windows 行右侧的「下载」

下载完成后打开安装包,按安装向导完成安装,然后启动 Clash Verge Rev。

+
当前提供的是 x64 安装包。Windows ARM 电脑请到 官方发布页选择对应架构的 Windows 安装包。
+

2把网站配置导入客户端

回到仪表盘,在 我的订阅 里点击 一键导入。

+
一键导入按钮
「一键导入」入口

+

弹窗中选择 Windows,再点击 Clash Verge。浏览器询问是否打开应用时,确认打开已安装的 Clash Verge Rev,然后在客户端确认导入。

+
Windows 客户端选择
Windows → Clash Verge
如果一键导入不成功,可以用复制配置地址的方式导入

点击网站上的复制配置地址,打开 Clash Verge Rev 的订阅页面,将地址粘贴到导入栏并点击导入。导入后选中这份订阅,使它成为当前配置。

订阅地址导入
官方客户端示意:订阅页面粘贴地址并导入

配置地址属于你的个人订阅,请勿发到公开群聊或给他人使用。

+

3选择线路与规则模式

打开客户端的 代理 页面,选择 规则 模式,再在代理组里选一条可用线路;也可以使用配置中提供的自动选择组。

+
选择规则模式与线路
{{brand}} 教程示意:代理页面与规则模式,实际线路以订阅为准

规则模式会按订阅配置分流。节点列表为空时,先回到订阅页面,确认已选中刚导入的配置,再尝试更新订阅。

+
+

4开启系统代理,开始使用

在客户端找到 系统代理 开关并打开,随后用浏览器访问你需要的网站,确认能正常加载。

+
系统代理开关
官方客户端示意:开启系统代理;新版本开关位置可能不同

普通网页使用先开启系统代理即可。停止使用时关闭系统代理,再退出客户端。

+
完成标志:订阅已导入并选中、线路可用、系统代理已开启,浏览器访问正常。
+

遇到问题

一键导入没有反应

先确认客户端已经安装并打开;仍不成功时,使用第 2 步里的复制地址导入。

导入后看不到节点

确认套餐可用,检查地址是否完整,更新订阅并选中导入的配置。

已经导入,网页仍打不开

确认系统代理已开启,尝试换一条线路;同时避免多个代理客户端一起运行。仍有问题可在网站工单中附上错误信息。

+
\ No newline at end of file diff --git a/database/migrations/2026_10_08_140000_add_display_device_limit_to_plans.php b/database/migrations/2026_10_08_140000_add_display_device_limit_to_plans.php new file mode 100644 index 0000000..8f7630e --- /dev/null +++ b/database/migrations/2026_10_08_140000_add_display_device_limit_to_plans.php @@ -0,0 +1,16 @@ +unsignedInteger('display_device_limit')->nullable()->after('device_limit'); + }); + } + public function down(): void { + Schema::table('v2_plan', function (Blueprint $table) { + $table->dropColumn('display_device_limit'); + }); + } +}; diff --git a/macos-client/Package.swift b/macos-client/Package.swift new file mode 100644 index 0000000..53bbf18 --- /dev/null +++ b/macos-client/Package.swift @@ -0,0 +1,3 @@ +// swift-tools-version: 5.9 +import PackageDescription +let package = Package(name: "DPLink", platforms: [.macOS(.v13)], products: [.executable(name: "DPLink", targets: ["DPLink"])], targets: [.executableTarget(name: "DPLink")]) diff --git a/macos-client/README.md b/macos-client/README.md new file mode 100644 index 0000000..d88e1d6 --- /dev/null +++ b/macos-client/README.md @@ -0,0 +1,32 @@ +# DP-LINK macOS 开发预览 + +原生 SwiftUI 客户端,macOS 13+,无需 npm。 + +## 运行 + +```bash +cd /private/var/www/wwwroot/dpl/macos-client +swift run +``` + +后端开发环境先在项目根目录运行 `php artisan serve --host=127.0.0.1 --port=8000`。 +客户端填入 XBoard 地址和已有账户。远程服务器要求 HTTPS。 + +登录成功后选择官方 Mihomo 的可执行文件;也可以选择本机已经安装的 Clash Verge 中的 +`/Applications/Clash Verge.app/Contents/MacOS/verge-mihomo`。 +本项目没有复制或分发第三方内核。分发产品前应按所选内核的许可证处理授权和源码义务。 + +## 已实现 + +- XBoard 登录和订阅获取,密码与认证信息只存内存。 +- Mihomo HTTP 订阅 provider 获取节点,认证控制接口仅监听本机。 +- 本地 HTTP/SOCKS5 代理 `127.0.0.1:17899`,选择节点、断开连接。 +- 临时配置目录权限 0700、配置文件权限 0600,断开后清理。 + +## 尚未完成 + +- 系统代理自动接管与旧设置恢复、TUN、权限 helper。 +- 内核打包、签名与公证、断线恢复、自动更新。 +- 真实账号与真实节点的端到端连接验证。 + +目前只是开发预览,启动代理后需要在使用的应用中配置本地代理;不能视作完成的一键 VPN 客户端。 diff --git a/macos-client/Sources/DPLink/App.swift b/macos-client/Sources/DPLink/App.swift new file mode 100644 index 0000000..500d755 --- /dev/null +++ b/macos-client/Sources/DPLink/App.swift @@ -0,0 +1,184 @@ +import SwiftUI +import Foundation + +struct ClientError: LocalizedError { + let message: String + var errorDescription: String? { message } +} + +@MainActor final class Client: ObservableObject { + @Published var server = "http://127.0.0.1:8000" + @Published var email = "" + @Published var password = "" + @Published var corePath = "" + @Published var status = "请登录 XBoard 账户" + @Published var busy = false + @Published var loggedIn = false + @Published var connected = false + @Published var nodes: [String] = [] + @Published var selected = "" + private var authorization = "" + private var subscription = "" + private var process: Process? + private var secret = UUID().uuidString + private var directory: URL? + + func request(_ path: String, method: String = "GET", body: [String: String]? = nil, controller: Bool = false) async throws -> [String: Any] { + let base = controller ? "http://127.0.0.1:19091" : server.trimmingCharacters(in: CharacterSet(charactersIn: "/")) + guard let url = URL(string: base + path), let scheme = url.scheme, ["https", "http"].contains(scheme) else { throw ClientError(message: "服务器地址无效") } + if !controller && scheme == "http" && !["localhost", "127.0.0.1", "::1"].contains(url.host ?? "") { throw ClientError(message: "远程服务器请使用 HTTPS") } + var req = URLRequest(url: url) + req.timeoutInterval = 15 + req.httpMethod = method + if controller { req.setValue("Bearer " + secret, forHTTPHeaderField: "Authorization") } + else if !authorization.isEmpty { req.setValue(authorization, forHTTPHeaderField: "Authorization") } + if let body { req.httpBody = try JSONSerialization.data(withJSONObject: body); req.setValue("application/json", forHTTPHeaderField: "Content-Type") } + let (data, response) = try await URLSession.shared.data(for: req) + guard let response = response as? HTTPURLResponse else { throw ClientError(message: "服务器未响应") } + let object = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any] ?? [:] + guard (200..<300).contains(response.statusCode) else { throw ClientError(message: object["message"] as? String ?? "请求失败(\(response.statusCode))") } + return object + } + + func login() async { + busy = true + defer { busy = false } + do { + let response = try await request("/api/v1/passport/auth/login", method: "POST", body: ["email": email, "password": password]) + guard let data = response["data"] as? [String: Any], let auth = data["auth_data"] as? String else { throw ClientError(message: "登录响应缺少认证信息") } + authorization = auth + password = "" + let sub = try await request("/api/v1/user/getSubscribe") + guard let data = sub["data"] as? [String: Any], let url = data["subscribe_url"] as? String else { throw ClientError(message: "账户没有订阅地址") } + subscription = url + loggedIn = true + status = "登录成功,请选择 Mihomo 内核后连接" + } catch { authorization = ""; status = error.localizedDescription } + } + + func connect() async { + busy = true + defer { busy = false } + do { + guard FileManager.default.isExecutableFile(atPath: corePath) else { throw ClientError(message: "请选择可执行的 Mihomo 内核") } + guard var components = URLComponents(string: subscription) else { throw ClientError(message: "订阅地址无效") } + var query = components.queryItems ?? [] + query.removeAll { $0.name == "flag" } + query.append(URLQueryItem(name: "flag", value: "clash")) + components.queryItems = query + guard let subURL = components.url, subURL.scheme == "https" || ["127.0.0.1", "localhost"].contains(subURL.host ?? "") else { throw ClientError(message: "订阅地址必须使用 HTTPS 或本机地址") } + let dir = FileManager.default.temporaryDirectory.appendingPathComponent("dplink-" + UUID().uuidString) + try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true, attributes: [.posixPermissions: 0o700]) + directory = dir + let quotedURL = String(data: try JSONSerialization.data(withJSONObject: [subURL.absoluteString]), encoding: .utf8)!.dropFirst().dropLast() + let config = """ + mixed-port: 17899 + allow-lan: false + bind-address: 127.0.0.1 + mode: rule + log-level: warning + external-controller: 127.0.0.1:19091 + secret: "\(secret)" + proxy-providers: + account: + type: http + url: \(quotedURL) + path: ./subscription.yaml + interval: 3600 + proxy-groups: + - name: DP-LINK + type: select + use: [account] + rules: + - MATCH,DP-LINK + """ + let file = dir.appendingPathComponent("config.yaml") + try config.write(to: file, atomically: true, encoding: .utf8) + try FileManager.default.setAttributes([.posixPermissions: 0o600], ofItemAtPath: file.path) + let task = Process() + task.executableURL = URL(fileURLWithPath: corePath) + task.arguments = ["-d", dir.path, "-f", file.path] + task.standardOutput = FileHandle.nullDevice + task.standardError = FileHandle.nullDevice + try task.run() + process = task + var ready = false + for _ in 0..<30 { + try await Task.sleep(nanoseconds: 500_000_000) + guard task.isRunning else { throw ClientError(message: "内核退出,请检查端口占用、订阅或内核文件") } + if let result = try? await request("/proxies", controller: true), let proxies = result["proxies"] as? [String: Any], let group = proxies["DP-LINK"] as? [String: Any], let all = group["all"] as? [String], !all.isEmpty { + nodes = all; selected = group["now"] as? String ?? all[0]; ready = true; break + } + } + guard ready else { throw ClientError(message: "订阅加载超时,请检查服务器和网络") } + connected = true + status = "本地代理已启动:127.0.0.1:17899(HTTP / SOCKS5)" + } catch { disconnect(); status = error.localizedDescription } + } + + func chooseNode() async { + do { _ = try await request("/proxies/DP-LINK", method: "PUT", body: ["name": selected], controller: true); status = "当前节点:" + selected } + catch { status = error.localizedDescription } + } + + func disconnect() { + if let process, process.isRunning { process.terminate(); process.waitUntilExit() } + process = nil + connected = false + nodes = [] + if let directory { try? FileManager.default.removeItem(at: directory) } + directory = nil + status = "已断开" + } + func logout() { disconnect(); authorization = ""; subscription = ""; loggedIn = false } +} + +struct ContentView: View { + @ObservedObject var client: Client + var body: some View { + VStack(alignment: .leading, spacing: 22) { + HStack { + Image(systemName: "network").font(.system(size: 32)).foregroundStyle(.blue) + VStack(alignment: .leading) { Text("DP-LINK").font(.title.bold()); Text("macOS 客户端 · 开发预览").foregroundStyle(.secondary) } + Spacer() + if client.loggedIn { Button("退出登录") { client.logout() }.disabled(client.busy) } + } + Divider() + if !client.loggedIn { + TextField("XBoard 地址", text: $client.server) + TextField("邮箱", text: $client.email) + SecureField("密码", text: $client.password) + Button("登录并获取订阅") { Task { await client.login() } }.buttonStyle(.borderedProminent).disabled(client.busy || client.email.isEmpty || client.password.isEmpty) + } else { + Text(client.email).foregroundStyle(.secondary) + HStack { + TextField("Mihomo 可执行文件路径", text: $client.corePath).disabled(client.connected) + Button("选择内核") { + let panel = NSOpenPanel(); panel.canChooseDirectories = false + if panel.runModal() == .OK { client.corePath = panel.url?.path ?? "" } + }.disabled(client.busy || client.connected) + } + if !client.nodes.isEmpty { + Picker("节点", selection: $client.selected) { ForEach(client.nodes, id: \.self) { Text($0).tag($0) } } + .onChange(of: client.selected) { _ in if client.connected { Task { await client.chooseNode() } } } + } + Button(client.connected ? "断开连接" : "启动代理") { + if client.connected { client.disconnect() } else { Task { await client.connect() } } + }.buttonStyle(.borderedProminent).disabled(client.busy) + Text("当前预览版提供本地代理。系统代理和 TUN 接管尚未实现;应用需配置为使用 127.0.0.1:17899。") + .font(.caption).foregroundStyle(.secondary) + } + if client.busy { ProgressView().controlSize(.small) } + Text(client.status).font(.callout).textSelection(.enabled) + Spacer() + Text("凭据仅保留在内存中,退出后需重新登录。").font(.caption).foregroundStyle(.secondary) + }.textFieldStyle(.roundedBorder).padding(30).frame(minWidth: 560, minHeight: 460) + } +} + +@main struct DPLinkApp: App { + @StateObject private var client = Client() + var body: some Scene { + WindowGroup { ContentView(client: client).onReceive(NotificationCenter.default.publisher(for: NSApplication.willTerminateNotification)) { _ in client.disconnect() } } + } +} diff --git a/macos-client/build-app.sh b/macos-client/build-app.sh new file mode 100755 index 0000000..9ff0fcc --- /dev/null +++ b/macos-client/build-app.sh @@ -0,0 +1,21 @@ +#!/bin/sh +set -eu +cd "$(dirname "$0")" +swift build -c release +APP_DIR="$PWD/build/DP-Link.app" +mkdir -p "$APP_DIR/Contents/MacOS" +cp .build/release/DPLink "$APP_DIR/Contents/MacOS/DPLink" +cat > "$APP_DIR/Contents/Info.plist" <<'PLIST' + + + +CFBundleExecutableDPLink +CFBundleIdentifiernet.dp-link.macos +CFBundleNameDP-Link +CFBundlePackageTypeAPPL +CFBundleShortVersionString0.1.0 +LSMinimumSystemVersion13.0 +NSHighResolutionCapable + +PLIST +printf '%s\n' "$APP_DIR" diff --git a/public/admin-custom/plan-device-display.js b/public/admin-custom/plan-device-display.js new file mode 100644 index 0000000..1e4f081 --- /dev/null +++ b/public/admin-custom/plan-device-display.js @@ -0,0 +1,69 @@ +// Independent admin extension. No hooks into React or enforcement data. +const host = document.createElement('div'); +host.id = 'plan-device-display-tools'; +const style = document.createElement('style'); +style.textContent = ` +#plan-device-display-tools .entry { position:fixed; right:24px; bottom:24px; z-index:40; border:1px solid #cbd5e1; border-radius:8px; padding:9px 14px; color:#0f172a; background:#fff; cursor:pointer; box-shadow:0 2px 8px #0001; } +#plan-device-display-dialog { width:min(680px,calc(100vw - 40px)); max-height:80vh; overflow:auto; border:1px solid #cbd5e1; border-radius:12px; padding:24px; background:#fff; color:#0f172a; } +#plan-device-display-dialog::backdrop {background:#0007} +#plan-device-display-dialog h2 {font-size:18px;font-weight:600;margin:0 0 8px} +#plan-device-display-dialog .hint {font-size:13px;color:#64748b;margin:8px 0 18px} +#plan-device-display-dialog fieldset {border:1px solid #e2e8f0;border-radius:8px;padding:12px;margin:12px 0} +#plan-device-display-dialog legend {padding:0 6px;font-weight:600} +#plan-device-display-dialog label {font-size:13px;display:block;margin-bottom:6px} +#plan-device-display-dialog input {border:1px solid #cbd5e1;border-radius:6px;padding:8px;width:160px;margin-right:12px} +#plan-device-display-dialog button {border:1px solid #cbd5e1;border-radius:6px;padding:7px 12px;cursor:pointer;background:#f8fafc} +#plan-device-display-dialog .message {font-size:13px;min-height:20px;margin-top:6px} +`; +host.append(style); +const entry = document.createElement('button'); +entry.className='entry'; entry.type='button'; entry.textContent='展示设备数设置'; host.append(entry); +const dialog = document.createElement('dialog'); dialog.id='plan-device-display-dialog'; +const heading=document.createElement('h2'); heading.textContent='套餐展示设备数'; dialog.append(heading); +const hint=document.createElement('p'); hint.className='hint'; +hint.textContent='仅用于页面展示。留空沿用实际限制,0 展示“不限制”。保存不会修改实际设备限制。'; dialog.append(hint); +const rows=document.createElement('div'); dialog.append(rows); +const close=document.createElement('button'); close.type='button'; close.textContent='关闭'; close.addEventListener('click',()=>dialog.close()); dialog.append(close); +host.append(dialog); document.body.append(host); +function syncVisibility() { + const allowed=location.hash.split('?')[0]==='#/finance/plan' && document.querySelector('#root h2'); + entry.hidden=!allowed; + if (!allowed && dialog.open) dialog.close(); +} +window.addEventListener('hashchange',syncVisibility); syncVisibility(); +new MutationObserver(syncVisibility).observe(document.getElementById('root'),{childList:true,subtree:true}); +async function api(action,payload) { + let stored; try {stored=JSON.parse(localStorage.getItem('XBOARD_ACCESS_TOKEN')||'null');} catch {throw new Error('请重新登录后台');} + if (!stored?.value || (stored.expire!=null && stored.expire<=Date.now())) throw new Error('请重新登录后台'); + const base=new URL(window.settings?.base_url||'/',location.origin); + if (base.origin!==location.origin) throw new Error('后台接口必须同源'); + const prefix=base.pathname.replace(/\/$/,'')+'/api/v2/'+encodeURIComponent(window.settings.secure_path)+'/plan/'; + const response=await fetch(prefix+action,{method:payload?'POST':'GET',credentials:'same-origin',headers:{Authorization:stored.value,'Content-Type':'application/json','Content-Language':'zh-CN'},...(payload?{body:JSON.stringify(payload)}:{})}); + const data=await response.json(); + if (!response.ok || data.status==='fail' || data.data===false) throw new Error(data.message||'保存失败'); + return data.data; +} +entry.addEventListener('click',async()=>{ + rows.textContent='正在读取套餐…'; dialog.showModal(); entry.disabled=true; + try { + const plans=await api('fetch'); rows.replaceChildren(); + for (const plan of plans) { + const fieldset=document.createElement('fieldset'); const legend=document.createElement('legend'); legend.textContent=plan.name; fieldset.append(legend); + const actual=document.createElement('p'); actual.className='hint'; actual.textContent='实际设备限制:'+(plan.device_limit==null || Number(plan.device_limit)<=0?'不限制':plan.device_limit); fieldset.append(actual); + const label=document.createElement('label'); label.textContent='展示设备数'; label.htmlFor='display-device-'+plan.id; fieldset.append(label); + const input=document.createElement('input'); input.id=label.htmlFor; input.type='number'; input.min='0'; input.max='4294967295'; input.step='1'; input.placeholder='留空沿用实际限制'; input.value=plan.display_device_limit==null?'':String(plan.display_device_limit); fieldset.append(input); + const save=document.createElement('button'); save.type='button'; save.textContent='保存 '+plan.name; fieldset.append(save); + const message=document.createElement('p'); message.className='message'; message.setAttribute('role','status'); fieldset.append(message); + save.addEventListener('click',async()=>{ + const value=input.value.trim(); const number=value===''?null:Number(value); + if (number!==null && (!Number.isInteger(number)||number<0||number>4294967295)) {message.textContent='请填写非负整数,或留空';return;} + save.disabled=true; message.textContent=''; + try {await api('update',{id:plan.id,display_device_limit:number}); message.textContent='已保存;实际设备限制未修改。';} + catch(error){message.textContent=error.message||'保存失败';} + finally{save.disabled=false;} + }); + rows.append(fieldset); + } + } catch(error){rows.textContent=error.message||'读取失败';} + finally{entry.disabled=false;} +}); diff --git a/public/tutorials/android/flclash/images/01-download.png b/public/tutorials/android/flclash/images/01-download.png new file mode 100644 index 0000000..3552035 Binary files /dev/null and b/public/tutorials/android/flclash/images/01-download.png differ diff --git a/public/tutorials/android/flclash/images/02-install.svg b/public/tutorials/android/flclash/images/02-install.svg new file mode 100644 index 0000000..72a7f33 --- /dev/null +++ b/public/tutorials/android/flclash/images/02-install.svg @@ -0,0 +1 @@ +下载 APK,安装 FlClash网站提供 ARM64 安装包,适用于多数安卓手机1打开安装包在下载记录中打开 APK先确认文件名为 FlClash2允许此次安装按系统提示确认安装来源无需开启全局安装权限3完成安装点击安装,再打开 FlClash从应用列表启动客户端操作示意 · 实际界面可能因客户端版本或手机系统有所不同 \ No newline at end of file diff --git a/public/tutorials/android/flclash/images/03-import.png b/public/tutorials/android/flclash/images/03-import.png new file mode 100644 index 0000000..57cacb1 Binary files /dev/null and b/public/tutorials/android/flclash/images/03-import.png differ diff --git a/public/tutorials/android/flclash/images/04-manual-import.svg b/public/tutorials/android/flclash/images/04-manual-import.svg new file mode 100644 index 0000000..f4ceedf --- /dev/null +++ b/public/tutorials/android/flclash/images/04-manual-import.svg @@ -0,0 +1 @@ +复制地址,导入配置不必手动输入服务器信息1复制配置地址在网站我的订阅中复制地址不要公开分享2配置 → + → URL打开 FlClash 的配置页面选择通过 URL 获取配置3粘贴并提交在 URL 栏粘贴完整地址导入后选中这份配置操作示意 · 实际界面可能因客户端版本或手机系统有所不同 \ No newline at end of file diff --git a/public/tutorials/android/flclash/images/05-proxy.svg b/public/tutorials/android/flclash/images/05-proxy.svg new file mode 100644 index 0000000..107d307 --- /dev/null +++ b/public/tutorials/android/flclash/images/05-proxy.svg @@ -0,0 +1 @@ +规则模式,选择可用线路线路名称与可选代理组以你的订阅为准1选中配置先确认订阅已导入没有节点时尝试更新2模式选「规则」在仪表盘的模式选项切换按订阅规则进行分流3选择线路打开代理组选择可用节点也可使用自动选择组操作示意 · 实际界面可能因客户端版本或手机系统有所不同 \ No newline at end of file diff --git a/public/tutorials/android/flclash/images/06-connect.svg b/public/tutorials/android/flclash/images/06-connect.svg new file mode 100644 index 0000000..71fcf27 --- /dev/null +++ b/public/tutorials/android/flclash/images/06-connect.svg @@ -0,0 +1 @@ +启动连接,确认 VPN 授权Android 使用系统 VPN 接管连接1点击启动返回 FlClash 仪表盘点击启动或播放按钮2确认连接请求首次启动出现系统提示确认应用为 FlClash3验证连接查看 VPN 状态并访问网页停止时回到客户端暂停操作示意 · 实际界面可能因客户端版本或手机系统有所不同 \ No newline at end of file diff --git a/public/tutorials/android/flclash/images/import-buttons.png b/public/tutorials/android/flclash/images/import-buttons.png new file mode 100644 index 0000000..87a50d5 Binary files /dev/null and b/public/tutorials/android/flclash/images/import-buttons.png differ diff --git a/public/tutorials/assets/live-reload.js b/public/tutorials/assets/live-reload.js new file mode 100644 index 0000000..2fba508 --- /dev/null +++ b/public/tutorials/assets/live-reload.js @@ -0,0 +1,12 @@ +(() => { + if (!['localhost', '127.0.0.1'].includes(location.hostname)) return; + const events = new EventSource('http://127.0.0.1:8588/events'); + let revision; + const refresh = (event) => { + const next = JSON.parse(event.data).revision; + if (revision && revision !== next) location.reload(); + revision = next; + }; + events.addEventListener('ready', refresh); + events.addEventListener('update', refresh); +})(); diff --git a/public/tutorials/assets/tutorial.css b/public/tutorials/assets/tutorial.css new file mode 100644 index 0000000..b035948 --- /dev/null +++ b/public/tutorials/assets/tutorial.css @@ -0,0 +1 @@ +*{box-sizing:border-box}body{margin:0;background:#f1f5fb;color:#17253f;font:16px/1.8 -apple-system,BlinkMacSystemFont,"Segoe UI","Microsoft YaHei",sans-serif}main{max-width:960px;margin:40px auto 0;padding:0 28px 32px}header{padding:36px;background:#142542;color:white;border-radius:22px}header img{width:42px;height:42px;object-fit:contain;vertical-align:middle;margin-right:12px}.brand{font-weight:750;letter-spacing:.04em}h1{font-size:36px;margin:18px 0 8px;line-height:1.25}header p{color:#becde2;margin:0}nav{display:flex;flex-wrap:wrap;gap:8px;margin:24px 0}nav a{background:white;border-radius:8px;padding:8px 14px;text-decoration:none;color:#305277;font-size:14px}section{background:white;border:1px solid #e1e8f2;border-radius:18px;margin:22px 0;padding:30px}h2{font-size:24px;line-height:1.5;margin:0 0 14px}.num{display:inline-flex;align-items:center;justify-content:center;width:34px;height:34px;color:white;background:#3278e9;border-radius:10px;margin-right:12px;font-size:18px}p{margin:12px 0}figure{margin:20px 0 8px}figure img{display:block;max-width:100%;height:auto;border-radius:12px;border:1px solid #e3e9f2;margin:auto}figcaption{font-size:13px;color:#697d99;margin-top:10px;text-align:center}.note{padding:12px 16px;border-left:3px solid #5087db;background:#eef5ff;border-radius:6px;color:#3a567c;font-size:14px}.smallpic{max-width:560px;margin-left:auto;margin-right:auto}.pair{display:grid;grid-template-columns:1fr 1fr;gap:24px}a{color:#236bd3}code{background:#edf2fa;padding:2px 5px;border-radius:4px}details{border-top:1px solid #e7ecf3;padding:14px 0}summary{font-weight:650;cursor:pointer}footer{color:#6a7b93;font-size:13px;padding:0 10px}ul{padding-left:24px}b{font-weight:650}@media(max-width:640px){main{padding:0 16px;margin:20px auto}header,section{padding:22px}h1{font-size:28px}h2{font-size:21px}.pair{grid-template-columns:1fr}}@media print{body{background:white}main{margin:0;max-width:none;padding:0}nav{display:none}header{print-color-adjust:exact}section{break-inside:avoid;border-radius:0}details{display:block}details p{display:block}}#help h2{margin-bottom:20px}#help details{border:0;padding:0}#help details+details{margin-top:22px}#help details p{margin:8px 0 0}footer{line-height:1.6}@media(max-width:640px){footer{padding-left:10px!important;padding-right:10px!important}} \ No newline at end of file diff --git a/public/tutorials/ios/hiddify/images/01-store.png b/public/tutorials/ios/hiddify/images/01-store.png new file mode 100644 index 0000000..27ae383 Binary files /dev/null and b/public/tutorials/ios/hiddify/images/01-store.png differ diff --git a/public/tutorials/ios/hiddify/images/02-install.svg b/public/tutorials/ios/hiddify/images/02-install.svg new file mode 100644 index 0000000..8324584 --- /dev/null +++ b/public/tutorials/ios/hiddify/images/02-install.svg @@ -0,0 +1 @@ +从 App Store 获取 Hiddify先在 App Store 登录美区 Apple ID,再获取 Hiddify1打开商店网站 iOS 行点「商店」打开 Hiddify 商店页面2点击获取按系统提示确认下载安装 Hiddify3安装后打开从桌面启动 Hiddify准备导入网站配置Hiddify 操作示意 \ No newline at end of file diff --git a/public/tutorials/ios/hiddify/images/03-import.png b/public/tutorials/ios/hiddify/images/03-import.png new file mode 100644 index 0000000..f0b2894 Binary files /dev/null and b/public/tutorials/ios/hiddify/images/03-import.png differ diff --git a/public/tutorials/ios/hiddify/images/04-manual-import.svg b/public/tutorials/ios/hiddify/images/04-manual-import.svg new file mode 100644 index 0000000..240e635 --- /dev/null +++ b/public/tutorials/ios/hiddify/images/04-manual-import.svg @@ -0,0 +1 @@ +复制配置地址,添加到 Hiddify从剪贴板添加,或手动填写订阅 URL1复制配置地址在网站点击对应按钮保留完整个人订阅地址2点击首页 +从剪贴板添加配置若提示粘贴,请确认允许3确认当前配置或手动填名称与 URL保存后选中刚导入的配置Hiddify 操作示意 \ No newline at end of file diff --git a/public/tutorials/ios/hiddify/images/05-connect.svg b/public/tutorials/ios/hiddify/images/05-connect.svg new file mode 100644 index 0000000..8526111 --- /dev/null +++ b/public/tutorials/ios/hiddify/images/05-connect.svg @@ -0,0 +1 @@ +允许 VPN 配置,开始连接首次使用按 iOS 系统提示完成授权1选中网站配置打开首页的连接按钮确认使用刚导入的订阅2允许 VPN 配置确认请求来自 Hiddify按系统提示验证身份3确认已连接等待连接状态更新访问网站检验实际连接Hiddify 操作示意 \ No newline at end of file diff --git a/public/tutorials/ios/hiddify/images/06-proxy.svg b/public/tutorials/ios/hiddify/images/06-proxy.svg new file mode 100644 index 0000000..17aaab3 --- /dev/null +++ b/public/tutorials/ios/hiddify/images/06-proxy.svg @@ -0,0 +1 @@ +选择自动或手动线路连接后打开「代理 / Proxies」,按订阅提供的选项选择1打开代理页面查看当前配置中的线路列表为空先更新配置2自动选择可保留可用的自动组按测试结果选择线路3手动切换也可选一条可用节点更换后再次检查访问Hiddify 操作示意 \ No newline at end of file diff --git a/public/tutorials/ios/hiddify/images/import-buttons.png b/public/tutorials/ios/hiddify/images/import-buttons.png new file mode 100644 index 0000000..87a50d5 Binary files /dev/null and b/public/tutorials/ios/hiddify/images/import-buttons.png differ diff --git a/public/tutorials/linux/clash-verge/images/01-download.png b/public/tutorials/linux/clash-verge/images/01-download.png new file mode 100644 index 0000000..ea441e5 Binary files /dev/null and b/public/tutorials/linux/clash-verge/images/01-download.png differ diff --git a/public/tutorials/linux/clash-verge/images/02-install.svg b/public/tutorials/linux/clash-verge/images/02-install.svg new file mode 100644 index 0000000..77f3c46 --- /dev/null +++ b/public/tutorials/linux/clash-verge/images/02-install.svg @@ -0,0 +1 @@ +选择适合系统的安装包先看发行版,再看处理器架构1Debian / Ubuntu.deb 安装包使用 apt 安装2Fedora / RHEL.rpm 安装包使用 dnf 安装3选择架构x86_64 → amd64 / x64aarch64 → arm64Linux 操作示意 · Clash Verge Rev \ No newline at end of file diff --git a/public/tutorials/linux/clash-verge/images/03-import.svg b/public/tutorials/linux/clash-verge/images/03-import.svg new file mode 100644 index 0000000..2023b57 --- /dev/null +++ b/public/tutorials/linux/clash-verge/images/03-import.svg @@ -0,0 +1 @@ +复制地址,导入订阅配置地址直接从网站复制,不用填写服务器参数1复制配置地址网站 → 我的订阅复制完整的订阅地址2订阅 → 导入打开 Clash Verge Rev粘贴地址,点击导入3选中订阅点击刚导入的订阅卡片使其成为当前配置Linux 操作示意 · Clash Verge Rev \ No newline at end of file diff --git a/public/tutorials/linux/clash-verge/images/04-proxy.svg b/public/tutorials/linux/clash-verge/images/04-proxy.svg new file mode 100644 index 0000000..45a38e6 --- /dev/null +++ b/public/tutorials/linux/clash-verge/images/04-proxy.svg @@ -0,0 +1 @@ +选择模式与线路使用订阅中提供的代理组1打开代理页面确认订阅已经选中节点列表应显示线路2选择规则模式按配置自动分流不必一直使用全局模式3选择可用线路在代理组内选择节点也可选自动选择组Linux 操作示意 · Clash Verge Rev \ No newline at end of file diff --git a/public/tutorials/linux/clash-verge/images/05-connect.svg b/public/tutorials/linux/clash-verge/images/05-connect.svg new file mode 100644 index 0000000..d043541 --- /dev/null +++ b/public/tutorials/linux/clash-verge/images/05-connect.svg @@ -0,0 +1 @@ +开启系统代理,开始使用普通浏览器访问先使用系统代理1系统代理 → 开在客户端打开开关让支持系统代理的应用使用2浏览器访问打开需要访问的网站无法加载时换一条线路3停止使用先关闭系统代理再退出客户端Linux 操作示意 · Clash Verge Rev \ No newline at end of file diff --git a/public/tutorials/linux/clash-verge/images/import-buttons.png b/public/tutorials/linux/clash-verge/images/import-buttons.png new file mode 100644 index 0000000..87a50d5 Binary files /dev/null and b/public/tutorials/linux/clash-verge/images/import-buttons.png differ diff --git a/public/tutorials/macos/clash-verge/images/01-download.png b/public/tutorials/macos/clash-verge/images/01-download.png new file mode 100644 index 0000000..5610a16 Binary files /dev/null and b/public/tutorials/macos/clash-verge/images/01-download.png differ diff --git a/public/tutorials/macos/clash-verge/images/02-chip.png b/public/tutorials/macos/clash-verge/images/02-chip.png new file mode 100644 index 0000000..6732ce1 Binary files /dev/null and b/public/tutorials/macos/clash-verge/images/02-chip.png differ diff --git a/public/tutorials/macos/clash-verge/images/03-install.png b/public/tutorials/macos/clash-verge/images/03-install.png new file mode 100644 index 0000000..fa2a322 Binary files /dev/null and b/public/tutorials/macos/clash-verge/images/03-install.png differ diff --git a/public/tutorials/macos/clash-verge/images/04-macos-import.png b/public/tutorials/macos/clash-verge/images/04-macos-import.png new file mode 100644 index 0000000..d84553f Binary files /dev/null and b/public/tutorials/macos/clash-verge/images/04-macos-import.png differ diff --git a/public/tutorials/macos/clash-verge/images/05-manual-import.png b/public/tutorials/macos/clash-verge/images/05-manual-import.png new file mode 100644 index 0000000..17c271c Binary files /dev/null and b/public/tutorials/macos/clash-verge/images/05-manual-import.png differ diff --git a/public/tutorials/macos/clash-verge/images/06-proxy.png b/public/tutorials/macos/clash-verge/images/06-proxy.png new file mode 100644 index 0000000..324b1a9 Binary files /dev/null and b/public/tutorials/macos/clash-verge/images/06-proxy.png differ diff --git a/public/tutorials/macos/clash-verge/images/07-system-proxy.png b/public/tutorials/macos/clash-verge/images/07-system-proxy.png new file mode 100644 index 0000000..f015eb2 Binary files /dev/null and b/public/tutorials/macos/clash-verge/images/07-system-proxy.png differ diff --git a/public/tutorials/macos/clash-verge/images/import-buttons.png b/public/tutorials/macos/clash-verge/images/import-buttons.png new file mode 100644 index 0000000..87a50d5 Binary files /dev/null and b/public/tutorials/macos/clash-verge/images/import-buttons.png differ diff --git a/public/tutorials/router/openclash/images/01-download.png b/public/tutorials/router/openclash/images/01-download.png new file mode 100644 index 0000000..a9bb9c7 Binary files /dev/null and b/public/tutorials/router/openclash/images/01-download.png differ diff --git a/public/tutorials/router/openclash/images/02-install.svg b/public/tutorials/router/openclash/images/02-install.svg new file mode 100644 index 0000000..8b9cca8 --- /dev/null +++ b/public/tutorials/router/openclash/images/02-install.svg @@ -0,0 +1 @@ +安装到路由器后台已安装 OpenClash 的用户可直接进入下一步1登录路由器电脑连接路由器网络进入 OpenWrt 管理后台2安装插件系统 → 软件包上传固件支持的安装包3准备内核OpenClash → 版本更新选择匹配设备的内核架构操作示意 · OpenWrt / OpenClash \ No newline at end of file diff --git a/public/tutorials/router/openclash/images/03-subscribe.svg b/public/tutorials/router/openclash/images/03-subscribe.svg new file mode 100644 index 0000000..8b10655 --- /dev/null +++ b/public/tutorials/router/openclash/images/03-subscribe.svg @@ -0,0 +1 @@ +添加网站订阅直接使用 Clash 格式配置,无需在线转换1复制配置地址网站 → 我的订阅复制完整订阅地址2添加配置订阅配置文件名:自定义订阅地址:粘贴网站地址3保存并更新开启该订阅条目保存配置 → 更新配置操作示意 · OpenWrt / OpenClash \ No newline at end of file diff --git a/public/tutorials/router/openclash/images/04-start.svg b/public/tutorials/router/openclash/images/04-start.svg new file mode 100644 index 0000000..76f3192 --- /dev/null +++ b/public/tutorials/router/openclash/images/04-start.svg @@ -0,0 +1 @@ +选中配置并启动配置已下载,内核已就绪1配置管理找到下载后的配置点击切换,设为当前配置2启用 OpenClash回到运行状态页面等待插件和内核运行3选择线路打开控制面板 → 代理选择规则模式与线路操作示意 · OpenWrt / OpenClash \ No newline at end of file diff --git a/public/tutorials/router/openclash/images/05-network.svg b/public/tutorials/router/openclash/images/05-network.svg new file mode 100644 index 0000000..a9ff08d --- /dev/null +++ b/public/tutorials/router/openclash/images/05-network.svg @@ -0,0 +1 @@ +让设备经过软路由只有流量经过软路由,代理才会生效1主路由模式设备连接软路由网络网关与 DNS 自动获取2旁路由模式设备网关指向旁路由DNS 也指向旁路由地址3浏览器测试先停止设备上的其他代理打开网站,确认连接正常操作示意 · OpenWrt / OpenClash \ No newline at end of file diff --git a/public/tutorials/router/openclash/images/import-buttons.png b/public/tutorials/router/openclash/images/import-buttons.png new file mode 100644 index 0000000..87a50d5 Binary files /dev/null and b/public/tutorials/router/openclash/images/import-buttons.png differ diff --git a/public/tutorials/windows/clash-verge/images/01-download.png b/public/tutorials/windows/clash-verge/images/01-download.png new file mode 100644 index 0000000..a65ebdf Binary files /dev/null and b/public/tutorials/windows/clash-verge/images/01-download.png differ diff --git a/public/tutorials/windows/clash-verge/images/02-import-buttons.png b/public/tutorials/windows/clash-verge/images/02-import-buttons.png new file mode 100644 index 0000000..87a50d5 Binary files /dev/null and b/public/tutorials/windows/clash-verge/images/02-import-buttons.png differ diff --git a/public/tutorials/windows/clash-verge/images/03-windows-import.png b/public/tutorials/windows/clash-verge/images/03-windows-import.png new file mode 100644 index 0000000..135156e Binary files /dev/null and b/public/tutorials/windows/clash-verge/images/03-windows-import.png differ diff --git a/public/tutorials/windows/clash-verge/images/04-manual-import.png b/public/tutorials/windows/clash-verge/images/04-manual-import.png new file mode 100644 index 0000000..17c271c Binary files /dev/null and b/public/tutorials/windows/clash-verge/images/04-manual-import.png differ diff --git a/public/tutorials/windows/clash-verge/images/05-proxy.png b/public/tutorials/windows/clash-verge/images/05-proxy.png new file mode 100644 index 0000000..324b1a9 Binary files /dev/null and b/public/tutorials/windows/clash-verge/images/05-proxy.png differ diff --git a/public/tutorials/windows/clash-verge/images/06-system-proxy.png b/public/tutorials/windows/clash-verge/images/06-system-proxy.png new file mode 100644 index 0000000..f015eb2 Binary files /dev/null and b/public/tutorials/windows/clash-verge/images/06-system-proxy.png differ diff --git a/resources/js/admin/plan-device-display.js b/resources/js/admin/plan-device-display.js new file mode 100644 index 0000000..1e4f081 --- /dev/null +++ b/resources/js/admin/plan-device-display.js @@ -0,0 +1,69 @@ +// Independent admin extension. No hooks into React or enforcement data. +const host = document.createElement('div'); +host.id = 'plan-device-display-tools'; +const style = document.createElement('style'); +style.textContent = ` +#plan-device-display-tools .entry { position:fixed; right:24px; bottom:24px; z-index:40; border:1px solid #cbd5e1; border-radius:8px; padding:9px 14px; color:#0f172a; background:#fff; cursor:pointer; box-shadow:0 2px 8px #0001; } +#plan-device-display-dialog { width:min(680px,calc(100vw - 40px)); max-height:80vh; overflow:auto; border:1px solid #cbd5e1; border-radius:12px; padding:24px; background:#fff; color:#0f172a; } +#plan-device-display-dialog::backdrop {background:#0007} +#plan-device-display-dialog h2 {font-size:18px;font-weight:600;margin:0 0 8px} +#plan-device-display-dialog .hint {font-size:13px;color:#64748b;margin:8px 0 18px} +#plan-device-display-dialog fieldset {border:1px solid #e2e8f0;border-radius:8px;padding:12px;margin:12px 0} +#plan-device-display-dialog legend {padding:0 6px;font-weight:600} +#plan-device-display-dialog label {font-size:13px;display:block;margin-bottom:6px} +#plan-device-display-dialog input {border:1px solid #cbd5e1;border-radius:6px;padding:8px;width:160px;margin-right:12px} +#plan-device-display-dialog button {border:1px solid #cbd5e1;border-radius:6px;padding:7px 12px;cursor:pointer;background:#f8fafc} +#plan-device-display-dialog .message {font-size:13px;min-height:20px;margin-top:6px} +`; +host.append(style); +const entry = document.createElement('button'); +entry.className='entry'; entry.type='button'; entry.textContent='展示设备数设置'; host.append(entry); +const dialog = document.createElement('dialog'); dialog.id='plan-device-display-dialog'; +const heading=document.createElement('h2'); heading.textContent='套餐展示设备数'; dialog.append(heading); +const hint=document.createElement('p'); hint.className='hint'; +hint.textContent='仅用于页面展示。留空沿用实际限制,0 展示“不限制”。保存不会修改实际设备限制。'; dialog.append(hint); +const rows=document.createElement('div'); dialog.append(rows); +const close=document.createElement('button'); close.type='button'; close.textContent='关闭'; close.addEventListener('click',()=>dialog.close()); dialog.append(close); +host.append(dialog); document.body.append(host); +function syncVisibility() { + const allowed=location.hash.split('?')[0]==='#/finance/plan' && document.querySelector('#root h2'); + entry.hidden=!allowed; + if (!allowed && dialog.open) dialog.close(); +} +window.addEventListener('hashchange',syncVisibility); syncVisibility(); +new MutationObserver(syncVisibility).observe(document.getElementById('root'),{childList:true,subtree:true}); +async function api(action,payload) { + let stored; try {stored=JSON.parse(localStorage.getItem('XBOARD_ACCESS_TOKEN')||'null');} catch {throw new Error('请重新登录后台');} + if (!stored?.value || (stored.expire!=null && stored.expire<=Date.now())) throw new Error('请重新登录后台'); + const base=new URL(window.settings?.base_url||'/',location.origin); + if (base.origin!==location.origin) throw new Error('后台接口必须同源'); + const prefix=base.pathname.replace(/\/$/,'')+'/api/v2/'+encodeURIComponent(window.settings.secure_path)+'/plan/'; + const response=await fetch(prefix+action,{method:payload?'POST':'GET',credentials:'same-origin',headers:{Authorization:stored.value,'Content-Type':'application/json','Content-Language':'zh-CN'},...(payload?{body:JSON.stringify(payload)}:{})}); + const data=await response.json(); + if (!response.ok || data.status==='fail' || data.data===false) throw new Error(data.message||'保存失败'); + return data.data; +} +entry.addEventListener('click',async()=>{ + rows.textContent='正在读取套餐…'; dialog.showModal(); entry.disabled=true; + try { + const plans=await api('fetch'); rows.replaceChildren(); + for (const plan of plans) { + const fieldset=document.createElement('fieldset'); const legend=document.createElement('legend'); legend.textContent=plan.name; fieldset.append(legend); + const actual=document.createElement('p'); actual.className='hint'; actual.textContent='实际设备限制:'+(plan.device_limit==null || Number(plan.device_limit)<=0?'不限制':plan.device_limit); fieldset.append(actual); + const label=document.createElement('label'); label.textContent='展示设备数'; label.htmlFor='display-device-'+plan.id; fieldset.append(label); + const input=document.createElement('input'); input.id=label.htmlFor; input.type='number'; input.min='0'; input.max='4294967295'; input.step='1'; input.placeholder='留空沿用实际限制'; input.value=plan.display_device_limit==null?'':String(plan.display_device_limit); fieldset.append(input); + const save=document.createElement('button'); save.type='button'; save.textContent='保存 '+plan.name; fieldset.append(save); + const message=document.createElement('p'); message.className='message'; message.setAttribute('role','status'); fieldset.append(message); + save.addEventListener('click',async()=>{ + const value=input.value.trim(); const number=value===''?null:Number(value); + if (number!==null && (!Number.isInteger(number)||number<0||number>4294967295)) {message.textContent='请填写非负整数,或留空';return;} + save.disabled=true; message.textContent=''; + try {await api('update',{id:plan.id,display_device_limit:number}); message.textContent='已保存;实际设备限制未修改。';} + catch(error){message.textContent=error.message||'保存失败';} + finally{save.disabled=false;} + }); + rows.append(fieldset); + } + } catch(error){rows.textContent=error.message||'读取失败';} + finally{entry.disabled=false;} +}); diff --git a/resources/tutorials/layout.html b/resources/tutorials/layout.html new file mode 100644 index 0000000..5aa17ee --- /dev/null +++ b/resources/tutorials/layout.html @@ -0,0 +1 @@ +{{title}} · {{brand}}
{{body}}
{{brand}}如果连接遇到问题,请前往网站工单寻求帮助。
\ No newline at end of file diff --git a/resources/tutorials/live-reload.js b/resources/tutorials/live-reload.js new file mode 100644 index 0000000..2fba508 --- /dev/null +++ b/resources/tutorials/live-reload.js @@ -0,0 +1,12 @@ +(() => { + if (!['localhost', '127.0.0.1'].includes(location.hostname)) return; + const events = new EventSource('http://127.0.0.1:8588/events'); + let revision; + const refresh = (event) => { + const next = JSON.parse(event.data).revision; + if (revision && revision !== next) location.reload(); + revision = next; + }; + events.addEventListener('ready', refresh); + events.addEventListener('update', refresh); +})(); diff --git a/resources/tutorials/tutorial.css b/resources/tutorials/tutorial.css new file mode 100644 index 0000000..b035948 --- /dev/null +++ b/resources/tutorials/tutorial.css @@ -0,0 +1 @@ +*{box-sizing:border-box}body{margin:0;background:#f1f5fb;color:#17253f;font:16px/1.8 -apple-system,BlinkMacSystemFont,"Segoe UI","Microsoft YaHei",sans-serif}main{max-width:960px;margin:40px auto 0;padding:0 28px 32px}header{padding:36px;background:#142542;color:white;border-radius:22px}header img{width:42px;height:42px;object-fit:contain;vertical-align:middle;margin-right:12px}.brand{font-weight:750;letter-spacing:.04em}h1{font-size:36px;margin:18px 0 8px;line-height:1.25}header p{color:#becde2;margin:0}nav{display:flex;flex-wrap:wrap;gap:8px;margin:24px 0}nav a{background:white;border-radius:8px;padding:8px 14px;text-decoration:none;color:#305277;font-size:14px}section{background:white;border:1px solid #e1e8f2;border-radius:18px;margin:22px 0;padding:30px}h2{font-size:24px;line-height:1.5;margin:0 0 14px}.num{display:inline-flex;align-items:center;justify-content:center;width:34px;height:34px;color:white;background:#3278e9;border-radius:10px;margin-right:12px;font-size:18px}p{margin:12px 0}figure{margin:20px 0 8px}figure img{display:block;max-width:100%;height:auto;border-radius:12px;border:1px solid #e3e9f2;margin:auto}figcaption{font-size:13px;color:#697d99;margin-top:10px;text-align:center}.note{padding:12px 16px;border-left:3px solid #5087db;background:#eef5ff;border-radius:6px;color:#3a567c;font-size:14px}.smallpic{max-width:560px;margin-left:auto;margin-right:auto}.pair{display:grid;grid-template-columns:1fr 1fr;gap:24px}a{color:#236bd3}code{background:#edf2fa;padding:2px 5px;border-radius:4px}details{border-top:1px solid #e7ecf3;padding:14px 0}summary{font-weight:650;cursor:pointer}footer{color:#6a7b93;font-size:13px;padding:0 10px}ul{padding-left:24px}b{font-weight:650}@media(max-width:640px){main{padding:0 16px;margin:20px auto}header,section{padding:22px}h1{font-size:28px}h2{font-size:21px}.pair{grid-template-columns:1fr}}@media print{body{background:white}main{margin:0;max-width:none;padding:0}nav{display:none}header{print-color-adjust:exact}section{break-inside:avoid;border-radius:0}details{display:block}details p{display:block}}#help h2{margin-bottom:20px}#help details{border:0;padding:0}#help details+details{margin-top:22px}#help details p{margin:8px 0 0}footer{line-height:1.6}@media(max-width:640px){footer{padding-left:10px!important;padding-right:10px!important}} \ No newline at end of file diff --git a/resources/views/admin.blade.php b/resources/views/admin.blade.php index 8733d36..cfd3d09 100644 --- a/resources/views/admin.blade.php +++ b/resources/views/admin.blade.php @@ -81,6 +81,7 @@
+ diff --git a/resources/views/tutorials/android/flclash.blade.php b/resources/views/tutorials/android/flclash.blade.php new file mode 100644 index 0000000..b71630a --- /dev/null +++ b/resources/views/tutorials/android/flclash.blade.php @@ -0,0 +1,36 @@ +Android 使用教程 · {{ $brand }}
{{ $brand }}{{ $brand }}

Android 使用教程

FlClash · 从下载安装到开始连接

+ +
开始前:请先登录网站,确认已有可用套餐。本文使用网站提供的 FlClash 安卓客户端,默认下载 ARM64 安装包。网站操作图为实际截图;客户端配图为根据官方版本整理的操作示意,按钮与布局以实际版本为准。
+

1下载并安装客户端

+

在手机浏览器登录网站,进入 仪表盘 → 客户端下载,找到 Android,点击右侧 下载。

+
网站 Android 客户端下载入口
点击 Android 行右侧的「下载」;手机端排列可能略有不同
+

下载完成后,从浏览器下载记录或文件管理器打开 FlClash 的 .apk 安装包。如果系统提示当前来源不允许安装应用,按提示进入设置,只允许当前用于打开安装包的浏览器或文件管理器安装,再返回完成安装。

+
打开 APK、允许此次来源安装、安装并打开 FlClash 的操作示意
安装操作示意:系统提示名称可能因手机品牌不同而变化
+

安装后打开 FlClash。网站当前提供的 arm64-v8a 安装包适用于多数安卓手机;如果提示安装包不兼容,可到 官方发布页选择适合设备架构的 Android APK。

+

2把网站配置导入客户端

+

回到网站仪表盘,在 我的订阅 里点击 一键导入。

+
一键导入与复制配置地址按钮
「一键导入」入口
+

弹窗中选择 Android,再点击 FlClash。浏览器询问是否打开应用时,确认打开已安装的 FlClash,随后按客户端提示导入配置,并在 配置 页面选中这份订阅。

+
网站一键导入弹窗的 Android 平台与 FlClash 选项
Android → FlClash;手机端排列可能略有不同
+
如果一键导入不成功,可以用复制配置地址的方式导入

点击网站上的 复制配置地址,打开 FlClash 的 配置 页面,点击 + 添加配置,选择 URL(通过 URL 获取配置文件)。将完整地址粘贴到 URL 栏,按需填写名称,点击 提交;导入完成后选中这份配置。

+
网站复制地址、FlClash 配置页面添加 URL、提交并选中配置的操作示意
手动导入操作示意
+

配置地址属于你的个人订阅,请勿发到公开群聊或给他人使用。

+

3选择线路与规则模式

+

确认订阅已导入并选中后,在 FlClash 的 仪表盘 中找到模式选项,选择 规则。再打开 代理组,选一条可用线路;也可以使用订阅提供的自动选择组。

+
选中配置、选择规则模式、在代理组中选择线路的操作示意
规则模式与线路选择示意,实际代理组以订阅为准
+

规则模式会按订阅配置分流。节点列表为空时,先回到配置页面,确认已选中刚导入的配置,再尝试更新订阅。

+

4启动连接,允许 VPN 请求

+

回到 FlClash 的 仪表盘,点击 启动 / 播放按钮。首次连接时 Android 会弹出 VPN 连接请求,确认请求来自 FlClash 后,点击 确定 / 允许。

+
启动 FlClash、确认 Android VPN 连接请求并验证连接的操作示意
连接与 VPN 授权示意;系统弹窗文字以你的手机为准
+

连接后,系统状态栏或 VPN 设置中通常会显示 VPN 标识。用浏览器访问你需要的网站,确认能正常加载。停止使用时,回到 FlClash 点击 暂停 / 停止按钮。

+
完成标志:配置已导入并选中、线路可用、VPN 已启动,浏览器访问正常。Android 通过系统 VPN 接管连接,不需要寻找 Windows/macOS 的系统代理开关。
+

遇到问题

+
安装失败或提示安装包不兼容

确认下载已完成,检查 APK 是否适合设备架构。默认包为 ARM64;其他设备到官方发布页选择对应版本。若安装已存在的应用时失败,先核对现有应用的来源,不要直接卸载,以免丢失配置。

+
一键导入没有反应

先确认 FlClash 已安装并打开。若浏览器未唤起应用,使用第 2 步里的复制地址导入。

+
导入后看不到节点

确认套餐可用,检查地址是否完整,更新订阅并选中导入的配置。

+
启动后没有连接,或网页仍打不开

确认已允许 FlClash 的 VPN 连接请求,并先停止其他正在使用的 VPN。尝试更换线路;仍有问题可在网站工单中附上客户端错误信息。

+
切到后台后连接中断

检查手机是否把 FlClash 的后台运行限制为禁止或严格省电;根据实际需要允许该应用后台运行,再重新连接。

+
{{ $brand }}如果连接遇到问题,请前往网站工单寻求帮助。
@if(in_array(request()->getHost(), ['localhost', '127.0.0.1'])) + +@endif + \ No newline at end of file diff --git a/resources/views/tutorials/ios/hiddify.blade.php b/resources/views/tutorials/ios/hiddify.blade.php new file mode 100644 index 0000000..c1f5c52 --- /dev/null +++ b/resources/views/tutorials/ios/hiddify.blade.php @@ -0,0 +1,38 @@ +iOS 使用教程 · {{ $brand }}
{{ $brand }}{{ $brand }}

iOS 使用教程

Hiddify · 从下载安装到开始连接

+ +
开始前:登录网站,确认套餐可用。设备需运行 iOS 15.0 或以上。
+

1从 App Store 获取客户端

+
首先需要在 App Store 登录海外 Apple ID,如果没有海外 Apple ID 且自己不会注册,可以从直接这里购买。
+

在手机浏览器登录网站,进入 仪表盘 → 客户端下载,找到 iOS,点击右侧 商店。

+
网站 iOS 客户端商店入口
点击 iOS 行右侧的「商店」
+

在 App Store 点击 获取,安装 Hiddify Proxy & VPN,然后打开应用。

+
从商店获取并打开 Hiddify 的操作示意
商店获取与安装操作示意
+
+

2把网站配置导入客户端

+

回到网站仪表盘,在 我的订阅 里点击 一键导入。

+
网站一键导入与复制配置地址按钮
「一键导入」入口
+

弹窗中选择 iOS,再点击 Hiddify。浏览器询问是否打开应用时,确认打开已安装的 Hiddify,并按客户端提示确认添加配置。

+
网站一键导入面板中选中 iOS,并显示 Hiddify 选项
iOS → Hiddify
+

导入后,在 Hiddify 首页选中这份配置。

+
如果一键导入不成功,可以用复制配置地址的方式导入

点击网站的 复制配置地址,打开 Hiddify 首页,点击 + 添加配置,选择从剪贴板添加(Add from clipboard);若系统询问是否允许粘贴,确认允许。也可以选择手动添加(Add manually),填写名称和完整的 URL,再点击保存(Save)。添加后选中这份配置。

+
复制地址、从剪贴板或手动添加、选中当前配置的操作示意
手动导入操作示意
+

配置地址属于你的个人订阅,请勿发到公开群聊或给他人使用。

+

3允许 VPN 配置,开始连接

+

点击 Hiddify 首页的连接按钮。系统提示添加 VPN 配置时,点击允许,按提示完成身份验证。

+
点击连接、允许 Hiddify 添加 VPN 配置、检查连接状态的操作示意
iOS VPN 授权操作示意
+

显示已连接后,用浏览器访问网站检查连接。停止使用时,点击断开连接。

+
+

4查看线路,按需切换

+

连接后打开代理(Proxies)页面,使用自动选择组,或手动选择一条可用线路。

+
打开代理页面、使用自动选择或手动切换线路的操作示意
线路选择操作示意
+

切换后再次访问网站确认可用。若列表为空,先确认当前配置已选中,并更新订阅。

+
+

遇到问题

+
商店提示应用不可用,或无法安装

检查 App Store 地区是否提供 Hiddify,以及设备是否满足 iOS 15.0 或以上要求。

+
一键导入没有反应

先确认 Hiddify 已安装并打开。回到浏览器再次尝试,仍不成功时使用第 2 步的复制配置地址导入。

+
导入后看不到线路

确认套餐可用,检查地址是否完整,更新订阅并选中刚导入的配置。

+
没有允许 VPN,或连接后仍打不开网页

回到 Hiddify 再次点击连接,按系统提示完成 VPN 授权。确认当前配置正确,先断开其他正在使用的 VPN,再尝试更换线路。仍有问题可在网站工单中附上客户端错误信息。

+
{{ $brand }}如果连接遇到问题,请前往网站工单寻求帮助。
@if(in_array(request()->getHost(), ['localhost', '127.0.0.1'])) + +@endif + \ No newline at end of file diff --git a/resources/views/tutorials/linux/clash-verge.blade.php b/resources/views/tutorials/linux/clash-verge.blade.php new file mode 100644 index 0000000..aee6ff5 --- /dev/null +++ b/resources/views/tutorials/linux/clash-verge.blade.php @@ -0,0 +1,39 @@ +Linux 使用教程 · {{ $brand }}
{{ $brand }}{{ $brand }}

Linux 使用教程

Clash Verge Rev · 从下载安装到开始连接

+ +
适用于带图形界面的 Linux 桌面,Ubuntu 建议使用 24.04 或更新版本。开始前请登录网站,并准备好可用套餐。网站配图为实际截图,客户端配图为操作示意。
+

1下载并安装客户端

+

进入网站的 仪表盘 → 客户端下载,找到 Linux,点击 下载,打开 Clash Verge Rev 官方发布页。在页面的 Assets 中选择安装包。

+
网站 Linux 客户端下载入口
Linux 行右侧的「下载」
+

Ubuntu / Debian / Deepin 选择 .deb;Fedora / RHEL 系 选择 .rpm。在终端运行 uname -m 查看架构:x86_64 选 amd64 或 x86_64,aarch64 选 arm64 或 aarch64,armv7l 选 armhf 或 armhfp。

+
Linux 发行版与安装包架构选择示意
先选安装包格式,再选处理器架构
+

下载完成后,在文件管理器打开安装包所在文件夹,右键选择 在终端中打开。以下以网站当前提供的 2.5.7、x86_64 安装包为例:

+

Ubuntu / Debian:

sudo apt install ./Clash.Verge_2.5.7_amd64.deb
+

Fedora / RHEL 系:

sudo dnf install ./Clash.Verge-2.5.7-1.x86_64.rpm
+

输入系统密码并按提示完成安装;终端输入密码时不显示字符是正常现象。其他架构请将命令中的文件名替换为实际下载的文件名。安装后,从应用菜单打开 Clash Verge Rev。

+
Arch Linux / Manjaro 安装方式

已有 AUR 助手 yay 时,可运行 yay -S clash-verge-rev-bin 安装正式版。其他安装方式参见 官方安装说明。

+
+

2把网站配置导入客户端

+

回到仪表盘,在 我的订阅 中点击 复制配置地址。

+
网站我的订阅中的复制配置地址按钮
点击「复制配置地址」
+

打开 Clash Verge Rev 的 订阅 页面,将地址粘贴到顶部的导入栏,点击 导入。导入完成后,点击这份订阅卡片,选中它作为当前配置。

+
复制配置地址、导入 Clash Verge Rev 订阅并选中的操作示意
导入后记得选中这份订阅
+
配置地址是你的个人订阅,请勿公开分享。
+

3选择线路与规则模式

+

打开客户端的 代理 页面,选择 规则 模式,再在代理组中选择一条可用线路;也可以使用订阅提供的自动选择组。

+
Clash Verge Rev 规则模式和线路选择操作示意
规则模式与线路选择
+

节点列表为空时,返回订阅页面更新订阅,并确认已选中这份配置。

+

4开启系统代理,开始使用

+

在客户端打开 系统代理 开关,再用浏览器访问你需要的网站。停止使用时,先关闭系统代理,再退出客户端。

+
开启系统代理、浏览器验证和停止连接的操作示意
完成导入、选好线路后开启系统代理
+
系统代理主要作用于遵循系统代理设置的应用。终端工具或部分应用不会自动使用它;需要接管更多应用时,可安装客户端服务并开启 TUN 模式,按提示授权。
+

遇到问题

+
安装时提示架构不支持或缺少依赖

确认安装包架构与 uname -m 的结果一致。使用 apt 或 dnf 安装,让包管理器处理依赖;Ubuntu 20.04 等旧系统可能缺少所需库,建议升级到较新版本。

+
应用打不开、黑屏或界面卡顿

Linux 桌面环境与显卡驱动可能影响界面启动。可参照 Linux 启动问题说明,根据自己的报错处理;向网站工单提供发行版、桌面环境与错误信息,有助于定位问题。

+
导入失败或没有节点

重新复制完整配置地址,确认套餐可用,更新订阅并选中导入的配置。

+
开启后网页仍打不开

确认系统代理已开启,并停止其他代理客户端。尝试换一条线路;若浏览器使用独立代理设置,将其改为使用系统代理。仍有问题可提交工单。

+
没有桌面的 Linux 服务器能用吗?

本教程的 Clash Verge Rev 需要图形界面,不适用于仅通过 SSH 使用的服务器。

+
+
{{ $brand }}如果连接遇到问题,请前往网站工单寻求帮助。
@if(in_array(request()->getHost(), ['localhost', '127.0.0.1'])) + +@endif + \ No newline at end of file diff --git a/resources/views/tutorials/macos/clash-verge.blade.php b/resources/views/tutorials/macos/clash-verge.blade.php new file mode 100644 index 0000000..a4f5465 --- /dev/null +++ b/resources/views/tutorials/macos/clash-verge.blade.php @@ -0,0 +1,37 @@ +macOS 使用教程 · {{ $brand }}
{{ $brand }}{{ $brand }}

macOS 使用教程

Clash Verge Rev · 从下载安装到开始连接

+ +
开始前:请先登录网站,确认已有可用套餐。本文适用于 macOS 12 及以上系统,支持 Apple 芯片和 Intel 芯片。网站操作图为实际截图;安装及客户端操作图参考官方文档,线路选择图为 {{ $brand }} 教程示意,实际线路以你的订阅为准。
+

1下载并安装客户端

+

进入网站的 仪表盘 → 客户端下载,找到 macOS,点击右侧 下载。

+
仪表盘 macOS 客户端下载入口
点击 macOS 行右侧的「下载」
+

在 苹果菜单 → 关于本机 中查看电脑的芯片类型,再在下载弹窗中选择对应版本:显示 Apple M 系列芯片时选择 Apple 芯片(M 系列 / ARM64);显示 Intel 处理器时选择 Intel 芯片(x64)。

+
macOS 下载弹窗中的 Apple 芯片和 Intel 芯片选项
根据「关于本机」中的芯片类型选择版本
+

下载完成后双击 .dmg 文件,把 Clash Verge 图标拖到 Applications(应用程序) 文件夹。复制完成后,从「应用程序」打开 Clash Verge Rev。

+
把 Clash Verge 图标拖入 Applications 文件夹安装
官方 macOS 安装示意:将应用拖入 Applications 文件夹
+
请从「应用程序」启动已经安装的客户端,不要一直在 DMG 安装窗口中运行。安装包下载失败时,可到 官方发布页选择对应架构的 .dmg 文件:Apple 芯片为 aarch64,Intel 芯片为 x64。
+

2把网站配置导入客户端

+

回到仪表盘,在 我的订阅 里点击 一键导入。

+
网站一键导入与复制配置地址按钮
「一键导入」入口
+

弹窗中选择 macOS,再点击 Clash Verge。浏览器询问是否打开应用时,确认打开已安装的 Clash Verge Rev,然后在客户端确认导入。

+
一键导入弹窗中的 macOS 平台及 Clash Verge 选项
macOS → Clash Verge
+
如果一键导入不成功,可以用复制配置地址的方式导入

点击网站上的 复制配置地址,打开 Clash Verge Rev 的 订阅 页面,将地址粘贴到导入栏并点击 导入。导入后选中这份订阅,使它成为当前配置。

+
Clash Verge Rev 订阅页面的导入栏
官方客户端示意:订阅页面粘贴地址并导入
+

配置地址属于你的个人订阅,请勿发到公开群聊或给他人使用。

+

3选择线路与规则模式

+

打开客户端的 代理 页面,选择 规则 模式,再在代理组里选一条可用线路;也可以使用配置中提供的自动选择组。

+
Clash Verge Rev 代理页面的规则模式与线路选择
{{ $brand }} 教程示意:代理页面与规则模式,实际线路以订阅为准
+

规则模式会按订阅配置分流。节点列表为空时,先回到订阅页面,确认已选中刚导入的配置,再尝试更新订阅。

+

4开启系统代理,开始使用

+

在客户端找到 系统代理 开关并打开,随后用浏览器访问你需要的网站,确认能正常加载。

+
Clash Verge Rev 系统代理开关
官方客户端示意:开启系统代理;新版本开关位置可能不同
+

普通网页使用先开启系统代理即可。停止使用时关闭系统代理,再退出客户端。

+
完成标志:订阅已导入并选中、线路可用、系统代理已开启,浏览器访问正常。
+

遇到问题

+
不确定应该下载哪个版本

打开苹果菜单 → 关于本机。Apple M 系列选择 ARM64,Intel 处理器选择 x64;同时确认系统为 macOS 12 或更新版本。

+
一键导入没有反应

先确认 Clash Verge Rev 已放入「应用程序」并打开;浏览器询问打开应用时确认打开。仍不成功时,使用第 2 步里的复制地址导入。

+
导入后看不到节点

确认套餐可用,检查地址是否完整,更新订阅并选中导入的配置。

+
已经导入,网页仍打不开

确认系统代理已开启,尝试换一条线路;同时避免多个代理客户端一起运行。仍有问题可在网站工单中附上错误信息。

+
{{ $brand }}如果连接遇到问题,请前往网站工单寻求帮助。
@if(in_array(request()->getHost(), ['localhost', '127.0.0.1'])) + +@endif + \ No newline at end of file diff --git a/resources/views/tutorials/router/openclash.blade.php b/resources/views/tutorials/router/openclash.blade.php new file mode 100644 index 0000000..a4a6483 --- /dev/null +++ b/resources/views/tutorials/router/openclash.blade.php @@ -0,0 +1,44 @@ +软路由使用教程 · {{ $brand }}
{{ $brand }}{{ $brand }}

软路由使用教程

OpenWrt / OpenClash · 从安装插件到全家设备连接

+ +
适用于 OpenWrt 或兼容固件的软路由,需要管理后台权限。已装好 OpenClash 可直接从第 2 步开始。网站配图为实际截图,路由器配图为操作示意。
+

1安装 OpenClash,准备内核

+

电脑连接软路由的网络,用浏览器打开它的管理地址并登录。安装前,在 系统 → 备份 / 升级 中保存一份当前配置备份。

+

在网站 仪表盘 → 客户端下载 中找到 软路由,点击 下载,打开 OpenClash 官方发布页。在 Assets 中下载 luci-app-openclash 安装包。

+
网站软路由下载入口
软路由行右侧的「下载」
+

使用 opkg 的固件选择 .ipk,使用 apk 的固件选择 .apk;这里的 APK 是 OpenWrt 软件包,不是安卓手机安装包。

+

进入路由器 系统 → 软件包,先更新软件包列表,再使用 上传软件包 安装下载的文件。若固件内置 OpenClash 安装入口,也可以直接从该入口安装。

+
登录软路由后台、安装 OpenClash 并准备内核的操作示意
插件安装在路由器上,不是在电脑上
+

安装完成后,刷新路由器后台,进入 服务 → OpenClash。在 插件设置 → 版本更新 中选择与路由器匹配的内核架构,下载或更新 Meta / Mihomo 内核,等下载完成后继续。

+
没有上传入口,或安装提示缺少依赖

使用固件的软件包管理器安装 OpenClash 所需依赖,具体清单见 OpenClash 官方依赖说明。不同固件的防火墙和软件源不同,内核模块必须与固件匹配,不要强制忽略依赖或覆盖系统库。

+
+

2添加网站订阅

+

登录网站,在仪表盘 我的订阅 中点击 复制配置地址。

+
网站复制配置地址按钮
点击「复制配置地址」
+

回到路由器的 服务 → OpenClash → 配置订阅,添加一条订阅,填写:

+
  • 配置文件名:填写一个好认的名字,例如 {{ $brand }}。
  • 订阅地址:粘贴从网站复制的完整地址。
  • User-Agent:选择 Clash / clash.meta 类型,以获取 Clash 格式配置。
  • 在线订阅转换:关闭,直接使用网站提供的配置。
+

保存这条订阅,保持条目 启用,再点击 保存配置 → 更新配置,等待订阅下载完成。

+
复制地址、添加 OpenClash 配置订阅、保存并更新的操作示意
保存订阅后更新配置
+
订阅地址是你的个人配置,请勿公开分享。
+ +
+

3切换配置,启动并选择线路

+

进入 配置管理,找到刚下载的配置文件,点击 切换。回到 运行状态 页面启用 OpenClash,等待插件与内核运行。

+

打开 OpenClash 提供的 控制面板,进入 代理 页面,选择 规则 模式,并在代理组中选择可用线路;也可以使用订阅提供的自动选择组。

+
切换配置、启用 OpenClash 和在控制面板选择线路的操作示意
订阅下载后,切换配置再启动
+

节点为空时,先回到配置管理确认当前配置,再更新订阅。

+

4让设备经过软路由

+

软路由作为主路由:手机、电脑连接它的 Wi-Fi 或 LAN 网络,通常保持自动获取 IP、网关和 DNS 即可。

+

软路由作为旁路由:需要让使用代理的设备把 默认网关和 DNS 指向旁路由的 LAN 地址。旁路由的上游网关应指向主路由,不能指向自己;旁路由也需要已配置好转发和 NAT。

+
主路由与旁路由的设备接入区别示意
流量必须经过软路由,只有安装插件还不够
+

测试时,先停止手机或电脑上其他代理客户端,再用浏览器访问你需要的网站。停止使用时,在 OpenClash 的运行状态页面关闭插件。

+
旁路由网络结构因设备而异。先在一台设备上测试网关和 DNS,成功后再调整其他设备;请保留原网络设置,便于恢复。
+

遇到问题

+
后台没有 OpenClash 菜单

先刷新或重新登录路由器后台。仍没有菜单时,查看软件包安装结果,确认插件与所需依赖安装成功。

+
提示内核不存在或不能启动

到版本更新页面检查内核是否下载完成、架构是否匹配。仍不能启动时,查看运行日志中的错误。

+
订阅更新失败或配置格式不对

重新复制完整地址,检查路由器本身能否访问订阅网站。订阅的 User-Agent 使用 Clash 类型,关闭在线订阅转换,再更新配置。

+
插件显示运行,但设备无法访问

确认设备流量经过软路由,旁路由场景尤其要检查网关和 DNS。先停止设备上的其他代理,换一条线路再试;若启用后原有网络也不通,先停用 OpenClash,并恢复设备原来的网关与 DNS。

+
仍然无法解决

在网站提交工单,说明固件版本、主路由或旁路由模式及错误信息。OpenClash 的运行日志和调试日志有助于排查;发送前去掉其中的订阅地址、密码等个人配置。

+
{{ $brand }}如果连接遇到问题,请前往网站工单寻求帮助。
@if(in_array(request()->getHost(), ['localhost', '127.0.0.1'])) + +@endif + \ No newline at end of file diff --git a/resources/views/tutorials/windows/clash-verge.blade.php b/resources/views/tutorials/windows/clash-verge.blade.php new file mode 100644 index 0000000..1f1f9fa --- /dev/null +++ b/resources/views/tutorials/windows/clash-verge.blade.php @@ -0,0 +1,18 @@ +Windows 使用教程 · {{ $brand }}
{{ $brand }}{{ $brand }}

Windows 使用教程

Clash Verge Rev · 从下载安装到开始连接

开始前:请先登录网站,确认已有可用套餐。本文适用于 x64 电脑。网站操作图为实际截图;客户端操作图参考官方文档,线路选择图为 {{ $brand }} 教程示意,实际线路以你的订阅为准。

1下载并安装客户端

进入网站的 仪表盘 → 客户端下载,找到 Windows,点击右侧 下载。

+
Windows 客户端下载入口
点击 Windows 行右侧的「下载」

下载完成后打开安装包,按安装向导完成安装,然后启动 Clash Verge Rev。

+
当前提供的是 x64 安装包。Windows ARM 电脑请到 官方发布页选择对应架构的 Windows 安装包。
+

2把网站配置导入客户端

回到仪表盘,在 我的订阅 里点击 一键导入。

+
一键导入按钮
「一键导入」入口

+

弹窗中选择 Windows,再点击 Clash Verge。浏览器询问是否打开应用时,确认打开已安装的 Clash Verge Rev,然后在客户端确认导入。

+
Windows 客户端选择
Windows → Clash Verge
如果一键导入不成功,可以用复制配置地址的方式导入

点击网站上的复制配置地址,打开 Clash Verge Rev 的订阅页面,将地址粘贴到导入栏并点击导入。导入后选中这份订阅,使它成为当前配置。

订阅地址导入
官方客户端示意:订阅页面粘贴地址并导入

配置地址属于你的个人订阅,请勿发到公开群聊或给他人使用。

+

3选择线路与规则模式

打开客户端的 代理 页面,选择 规则 模式,再在代理组里选一条可用线路;也可以使用配置中提供的自动选择组。

+
选择规则模式与线路
{{ $brand }} 教程示意:代理页面与规则模式,实际线路以订阅为准

规则模式会按订阅配置分流。节点列表为空时,先回到订阅页面,确认已选中刚导入的配置,再尝试更新订阅。

+
+

4开启系统代理,开始使用

在客户端找到 系统代理 开关并打开,随后用浏览器访问你需要的网站,确认能正常加载。

+
系统代理开关
官方客户端示意:开启系统代理;新版本开关位置可能不同

普通网页使用先开启系统代理即可。停止使用时关闭系统代理,再退出客户端。

+
完成标志:订阅已导入并选中、线路可用、系统代理已开启,浏览器访问正常。
+

遇到问题

一键导入没有反应

先确认客户端已经安装并打开;仍不成功时,使用第 2 步里的复制地址导入。

导入后看不到节点

确认套餐可用,检查地址是否完整,更新订阅并选中导入的配置。

已经导入,网页仍打不开

确认系统代理已开启,尝试换一条线路;同时避免多个代理客户端一起运行。仍有问题可在网站工单中附上错误信息。

+
{{ $brand }}如果连接遇到问题,请前往网站工单寻求帮助。
@if(in_array(request()->getHost(), ['localhost', '127.0.0.1'])) + +@endif + \ No newline at end of file diff --git a/routes/web.php b/routes/web.php index abdc1e9..f9574a7 100755 --- a/routes/web.php +++ b/routes/web.php @@ -19,7 +19,7 @@ use Illuminate\Support\Facades\File; */ -Route::get('/', function (Request $request) { +$renderFrontend = function (Request $request) { if (admin_setting('app_url') && admin_setting('safe_mode_enable', 0)) { $requestHost = $request->getHost(); $configHost = parse_url(admin_setting('app_url'), PHP_URL_HOST); @@ -56,14 +56,22 @@ Route::get('/', function (Request $request) { } $renderParams = [ - 'title' => admin_setting('app_name', 'Xboard'), + 'title' => config('app.name'), 'theme' => $theme, 'version' => app(UpdateService::class)->getCurrentVersion(), 'description' => admin_setting('app_description', 'Xboard is best'), 'logo' => admin_setting('logo'), 'theme_config' => $themeService->getConfig($theme) ]; - return view('theme::' . $theme . '.dashboard', $renderParams); + // Supply only the public application name; environment credentials stay server-side. + $brand = json_encode((string) config('app.name'), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT); + $fullBrand = json_encode((string) config('app.full_name'), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT); + $brandScript = ''; + $html = view('theme::' . $theme . '.dashboard', $renderParams)->render(); + if ($theme === 'Stellar') { + $html = \App\Support\StellarDevAssets::apply($request, $html); + } + return response(str_replace('', $brandScript . '', $html)); } catch (Exception $e) { Log::error('Theme rendering failed', [ 'theme' => $theme, @@ -71,12 +79,21 @@ Route::get('/', function (Request $request) { ]); abort(500, '主题加载失败'); } -}); +}; + +// Tutorials are independent pages, with their brand supplied from application configuration. +Route::get('/tutorials/{platform}/{client}.html', function (string $platform, string $client) { + $view = 'tutorials.' . $platform . '.' . $client; + abort_unless(view()->exists($view), 404); + return view($view, ['brand' => (string) config('app.name')]); +})->where(['platform' => '[a-z0-9-]+', 'client' => '[a-z0-9-]+']); + +Route::get('/', $renderFrontend); //TODO:: 兼容 Route::get('/' . admin_setting('secure_path', admin_setting('frontend_admin_path', hash('crc32b', config('app.key')))), function () { return view('admin', [ - 'title' => admin_setting('app_name', 'XBoard'), + 'title' => config('app.name'), 'theme_sidebar' => admin_setting('frontend_theme_sidebar', 'light'), 'theme_header' => admin_setting('frontend_theme_header', 'dark'), 'theme_color' => admin_setting('frontend_theme_color', 'default'), @@ -89,4 +106,16 @@ Route::get('/' . admin_setting('secure_path', admin_setting('frontend_admin_path Route::get('/' . (admin_setting('subscribe_path', 's')) . '/{token}', [\App\Http\Controllers\V1\Client\ClientController::class, 'subscribe']) ->middleware('client') - ->name('client.subscribe'); \ No newline at end of file + ->name('client.subscribe'); +// Stellar uses Vue history routes; unmatched API and asset requests remain 404s. +Route::fallback(function (Request $request) use ($renderFrontend) { + if ( + admin_setting('frontend_theme', 'Xboard') !== 'Stellar' || + $request->is('api', 'api/*', 'theme', 'theme/*', 'assets', 'assets/*') || + pathinfo($request->path(), PATHINFO_EXTENSION) !== '' + ) { + abort(404); + } + + return $renderFrontend($request); +}); diff --git a/scripts/build-admin-display.mjs b/scripts/build-admin-display.mjs new file mode 100644 index 0000000..1d1b97c --- /dev/null +++ b/scripts/build-admin-display.mjs @@ -0,0 +1,2 @@ +import { copyFile } from 'node:fs/promises'; +await copyFile(new URL('../resources/js/admin/plan-device-display.js', import.meta.url), new URL('../public/admin-custom/plan-device-display.js', import.meta.url)); diff --git a/scripts/tutorials/build.mjs b/scripts/tutorials/build.mjs new file mode 100644 index 0000000..74ba792 --- /dev/null +++ b/scripts/tutorials/build.mjs @@ -0,0 +1,44 @@ +import fs from 'node:fs/promises' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..') +const source = path.join(root, 'content/tutorials') +const output = path.join(root, 'public/tutorials') +let layout = await fs.readFile(path.join(root, 'resources/tutorials/layout.html'), 'utf8') +if (process.argv.includes('--live-reload')) { + layout = layout.replace('', `@if(in_array(request()->getHost(), ['localhost', '127.0.0.1'])) + +@endif +`) +} +const registry = {} +const views = path.join(root, 'resources/views/tutorials') +await fs.mkdir(path.join(output, 'assets'), {recursive: true}) +await fs.copyFile(path.join(root, 'resources/tutorials/tutorial.css'), path.join(output, 'assets/tutorial.css')) +if (process.argv.includes('--live-reload')) await fs.copyFile(path.join(root, 'resources/tutorials/live-reload.js'), path.join(output, 'assets/live-reload.js')) +for (const platform of await fs.readdir(source, {withFileTypes: true})) { + if (!platform.isDirectory()) continue + for (const client of await fs.readdir(path.join(source, platform.name), {withFileTypes: true})) { + if (!client.isDirectory()) continue + const folder = path.join(source, platform.name, client.name) + const meta = JSON.parse(await fs.readFile(path.join(folder, 'meta.json'), 'utf8')) + if (meta.platform !== platform.name || meta.client !== client.name || !/^[a-z0-9-]+$/.test(platform.name) || !/^[a-z0-9-]+$/.test(client.name)) throw new Error('Invalid tutorial location') + const prefix = `/tutorials/${platform.name}/${client.name}` + let body = await fs.readFile(path.join(folder, `${meta.language}.html`), 'utf8') + for (const match of body.matchAll(/src="\.\/images\/([^"]+)"/g)) await fs.access(path.join(folder, 'images', match[1])) + body = body.replaceAll('src="./images/', `src="${prefix}/images/`) + const escape = value => value.replaceAll('&','&').replaceAll('<','<').replaceAll('"','"') + const html = layout.replace('{{language}}', escape(meta.language)).replace('{{title}}', escape(meta.title)).replace('{{body}}', body).replaceAll('{{brand}}', '{{ $brand }}') + await fs.mkdir(path.join(output, platform.name), {recursive:true}) + await fs.mkdir(path.join(views, platform.name), {recursive:true}) + await fs.writeFile(path.join(views, platform.name, `${client.name}.blade.php`), html) + // Remove legacy static HTML so Laravel can inject the current configured name. + await fs.rm(path.join(output, platform.name, `${client.name}.html`), {force:true}) + await fs.cp(path.join(folder,'images'), path.join(output,platform.name,client.name,'images'),{recursive:true}) + registry[platform.name] = prefix + '.html' + } +} +const generated = path.join(root, 'stellar/src/generated') +await fs.mkdir(generated,{recursive:true}) +await fs.writeFile(path.join(generated,'tutorials.ts'), '// Generated by scripts/tutorials/build.mjs\nexport const tutorialPages: Record = '+JSON.stringify(registry,null,2)+'\n') +console.log(`Built ${Object.keys(registry).length} independent tutorial page(s).`) diff --git a/scripts/tutorials/watch.mjs b/scripts/tutorials/watch.mjs new file mode 100644 index 0000000..a0867c8 --- /dev/null +++ b/scripts/tutorials/watch.mjs @@ -0,0 +1,75 @@ +import { watch } from 'node:fs' +import http from 'node:http' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import { execFile } from 'node:child_process' +import { promisify } from 'node:util' + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..') +const run = promisify(execFile) +const clients = new Set() +const allowedOrigins = new Set(['http://127.0.0.1:8587', 'http://localhost:8587']) +let revision = Date.now().toString() +let dirty = false +let building = false +let timer + +function send(response, event) { + response.write(`event: ${event}\ndata: ${JSON.stringify({ revision })}\n\n`) +} + +async function build() { + if (building) { dirty = true; return } + building = true + do { + dirty = false + try { + const { stdout } = await run(process.execPath, ['scripts/tutorials/build.mjs', '--live-reload'], { cwd: root }) + process.stdout.write(stdout) + revision = Date.now().toString() + for (const response of clients) send(response, 'update') + } catch (error) { + console.error('Tutorial build failed; keeping the current preview.', error.stderr || error.message) + } + } while (dirty) + building = false +} + +const watchers = ['content/tutorials', 'resources/tutorials'].map(folder => { + const watcher = watch(path.join(root, folder), { recursive: true }, (_, filename) => { + if (!filename || /(^|[\/])\./.test(filename)) return + clearTimeout(timer) + timer = setTimeout(build, 180) + }) + watcher.on('error', error => console.error('Tutorial watcher error:', error.message)) + return watcher +}) + +await build() +const server = http.createServer((request, response) => { + if (request.url !== '/events') { response.writeHead(404).end(); return } + const origin = request.headers.origin + if (origin && !allowedOrigins.has(origin)) { response.writeHead(403).end(); return } + response.writeHead(200, { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + ...(origin ? { 'Access-Control-Allow-Origin': origin, 'Vary': 'Origin' } : {}), + }) + response.flushHeaders() + clients.add(response) + send(response, 'ready') + request.on('close', () => clients.delete(response)) +}) +server.on('error', error => { console.error('Live reload server:', error.message); process.exit(1) }) +server.listen(8588, '127.0.0.1', () => console.log('Tutorial watch enabled: save content, images or shared styles to refresh the local preview.')) +const heartbeat = setInterval(() => { for (const response of clients) response.write(': keepalive\n\n') }, 20000) +function shutdown() { + clearInterval(heartbeat) + clearTimeout(timer) + for (const watcher of watchers) watcher.close() + for (const response of clients) response.end() + server.close(() => process.exit(0)) +} +process.on('SIGINT', shutdown) +process.on('SIGTERM', shutdown) diff --git a/tests/Unit/PlanDisplayDeviceLimitTest.php b/tests/Unit/PlanDisplayDeviceLimitTest.php new file mode 100644 index 0000000..6c75b73 --- /dev/null +++ b/tests/Unit/PlanDisplayDeviceLimitTest.php @@ -0,0 +1,40 @@ + $actual, 'display_device_limit' => $display, 'reset_traffic_method' => 1]); + $this->assertSame($expected, $plan->getDisplayDeviceLimit()); + $data = (new PlanResource($plan))->toArray(new Request()); + $this->assertSame($actual, $data['device_limit']); + $this->assertSame($display, $data['display_device_limit']); + } + } + + public function test_existing_template_variable_keeps_actual_limit(): void + { + $plan = new Plan(['device_limit' => 5, 'display_device_limit' => 18, 'reset_traffic_method' => 1, 'content' => '{{devices}} / {{display_devices}}']); + $this->assertSame('5 / 18', (new PlanResource($plan))->toArray(new Request())['content']); + } + + public function test_display_count_rejects_negative_fraction_and_overflow(): void + { + $rule = (new PlanSave())->rules()['display_device_limit']; + foreach ([null, 0, 5, 200, 4294967295] as $value) { + $this->assertFalse(validator(['display_device_limit' => $value], ['display_device_limit' => $rule])->fails()); + } + foreach ([-1, 1.5, 'abc', 4294967296] as $value) { + $this->assertTrue(validator(['display_device_limit' => $value], ['display_device_limit' => $rule])->fails()); + } + } +}