F50探针 CF-Server-Monitor 探针一键管理插件
把 CF-Server-Monitor 的探针(cf-probe)搬到了 KANO 面板里,
不用 SSH、不用敲命令,点几下就能装好并跑起来。
功能
· 安装/更新 cf-probe(arm64 / armv7 自动识别)
· 启动 / 停止 / 重启 / 刷新状态 / 卸载
· 开机自动启动(写进 ufi_tools_boot.sh,和 Komari Agent 共存)
· 状态栏直接看:安装状态、配置状态、CA 证书数、进程、面板连接
这个插件专门处理了 Android 上的三个坑
国内卡连不上 GitHub —— 内置三个加速代理,依次自动回退
Android 没有 Go 需要的那套 CA 证书路径 —— 自动把系统
cacerts 拼成 bundle 并通过 SSL_CERT_FILE 指给 Go,
否则必然报 "x509: certificate signed by unknown authority"
官方 checksums.txt 的文件名带 dist/ 前缀 —— 不剥掉就永远
匹配不到校验值
不能用官方 cf-probe install 的原因
Android 的 /etc 是只读的 /system/etc,也没有 systemd,
install 子命令必然失败。所以改成手写 config.conf +
cf-probe run -config=启动,配置放在可写的 /data/local 下。
使用前提
· 设备已 root 且装了 高级后台 面板
· 有自己的 CF-Server-Monitor 面板
· 知道面板后台的 API_SECRET
· 面板若开了「自动发现」可留空 SERVER_ID 自动建号,
没开就要先在面板里创建服务器并填入 UUID
使用方法
本文件全文粘到 高级 面板的自定义脚本区
刷新页面,ADVANCE 旁出现「CF探针」按钮
填 Worker URL(https://你的域名/update)、SECRET、服务器名称
点「安装/更新」,再点「启动」
状态栏变绿、显示「面板:已连接」即成功
已知限制
· cf-probe 的 -config= 参数未写进官方 --help,属上游内部用法,
版本更新若改名会导致配置读不到(插件里注释了兜底改法)
· 联通 IPv6 到 Cloudflare 偶有 i/o timeout,会自动重试
个人测速没问题
CF探针 F50
插件商店导入
<!--
========================================================================
CF-Server-Monitor 探针 · 小K F50 (KANO 面板) 管理插件
========================================================================
用途:在 KANO 面板里一键安装 / 启动 / 停止 / 重启 / 卸载 cf-probe
适配:小K F50 等已 root 的 Android 设备(通过 /api/root_shell 执行)
与 Komari Agent 插件完全共存,互不干扰
默认值(通用发布版):
Worker URL : 留空,使用者填自己的面板地址(例如 https://你的面板域名/update)
SECRET : 留空,使用者填自己面板后台的 API_SECRET
SERVER_ID : 留空 = 由面板自动发现建号;面板未开自动发现则需填服务器 UUID
服务器名称 : 留空 = 自动建号时用公网 IP 作名字
上报间隔 : 60 秒
GitHub代理 : https://ghfast.top/(国内卡必需,内置三个代理会自动回退)
使用者怎么用:
1. 把本文件粘到 KANO 面板的自定义脚本区
2. 刷新页面,ADVANCE 旁边出现「CF探针」按钮
3. 点开,填三样:Worker URL(自己的面板)、SECRET(面板的 API_SECRET)、服务器名称
4. 点「安装/更新」→ 完成后点「启动」
5. 状态栏变绿、显示「面板:已连接」即成功
要换成某套面板做默认值,改下面 DEFAULTS 那一处即可。
安装位置:
/data/local/cf-probe/config.conf 配置(key=value 大写,非 JSON)
/data/local/cf-probe/cf-probe 二进制
/data/local/cf-probe/start.sh 启动脚本
/data/local/cf-probe/update.sh 更新脚本(下载 + sha256 校验)
/data/local/cf-probe/probe.log 运行日志
/sdcard/ufi_tools_boot.sh 开机自启(独立标记段)
关键技术点:
1. 绝不调用 `cf-probe install` —— 它会写 /etc/config 并注册 systemd,
而 Android 的 /etc 是只读的 /system/etc 且没有 systemd,必然失败。
2. 改用 `cf-probe run -config=<路径>`,配置放在可写的 /data/local 下。
(-config= 是上游 systemd unit 内部使用的参数,未写进 --help,
属未文档化用法。若将来版本改名,表现是启动后日志报配置缺失,
兜底方案见 start.sh 里的注释。)
3. 二进制按 ABI 自动选择:
ro.product.cpu.abilist64 非空 + 有 linker64 → cf-probe-linux-arm64
否则 → cf-probe-linux-armv7
(cf-probe 没有通用 linux-arm 资产,只有 armv5/v6/v7)
GitHub 下载(国内卡必看):
F50 多插国内卡,直连 github.com 基本不通。update.sh 内置三个实测可用的
加速代理,会依次尝试,任一成功即停:
https://ghfast.top/
https://gh-proxy.com/
https://gh.llkk.cc/
弹窗里的「GitHub 下载代理前缀」默认已填 ghfast.top,可自行改成自己的。
三个都失败会输出 all-mirrors-failed,此时可手动把二进制塞到
/data/local/cf-probe/cf-probe 再点启动。
⚠ checksums 文件名带目录前缀(这是最容易踩的坑):
releases 的 checksums.txt 每行形如
7579b6f3... dist/cf-probe-linux-arm64
文件名前面有 dist/,若直接拿 cf-probe-linux-arm64 去比对第二列,
会永远匹配不到,报 checksum-not-found。update.sh 里已用
awk 先剥离 * 号和目录前缀再比较。
验证成功的标志(状态栏显示绿色):
安装:已安装 配置:已写入 CA:125张 进程:运行中 面板:已连接
对应日志:
[INFO] WSS connected url=wss://<你的面板域名>/update protocol=update
验证记录:
- 配置格式已在 x86_64 Linux 上实测连通真实面板(WSS connected)
- 三个加速代理均已实测能完整下载 7,405,728 字节的 arm64 二进制,
sha256 = 7579b6f35408be11753549e55fe250258353f2cff34652e0af786ed8869dbe9d
与官方 checksums.txt 一致
- Android 无 CA 的问题已用 mount namespace 复现并验证修复有效
- 真机(小K F50)实测通过:ca_certs=125、connected=1
========================================================================
-->
<script>
(function(){
"use strict";
if(window.__cfProbeF50Loaded)return;
window.__cfProbeF50Loaded=true;
/* ===== 路径常量 ===== */
var DIR="/data/local/cf-probe"; // 与 Komari 的 /data/local/komari 并列
var CFG=DIR+"/config.conf"; // key=value 大写格式,不是 JSON
var BIN=DIR+"/cf-probe";
var START=DIR+"/start.sh";
var UPDATE=DIR+"/update.sh";
var LOG=DIR+"/probe.log";
var PID=DIR+"/probe.pid";
var BOOT="/sdcard/ufi_tools_boot.sh";
var ENDPOINT_STORE="cfprobe_f50_worker"; // localStorage 键名
var SECRET_STORE="cfprobe_f50_secret";
var SID_STORE="cfprobe_f50_sid";
var NAME_STORE="cfprobe_f50_name";
var IVL_STORE="cfprobe_f50_interval";
var PROXY_STORE="cfprobe_f50_github_proxy";
/* ===== 发布默认值:换面板只改这一处 =====
只给默认值,不预填任何密钥 —— SECRET 与 SERVER_ID 一律留空,
由使用者自己填。发布版绝不能带私有密钥,否则所有人都往同一条记录上报。 */
var DEFAULTS={
worker:"", // 留空:使用者填自己的面板地址
secret:"", // 留空:使用者填自己的 API_SECRET
sid:"", // 留空:服务端自动发现建号
name:"", // 留空:自动建号时用公网 IP 作名字
interval:"60", // 上报间隔(秒)
proxy:"https://ghfast.top/" // 国内卡连不上 GitHub 时的加速前缀
};
function shellQuote(v){return "'"+String(v==null?"":v).replace(/'/g,"'\\''")+"'";}
function value(id){var e=document.getElementById(id);return e?e.value.trim():"";}
function status(t){var e=document.getElementById("cfp-result");if(e)e.textContent=String(t==null?"":t);}
function toast(t,c){if(typeof window.createToast==="function")window.createToast(t,c||"green");else status(t);}
/* ===== KANO 面板的 root_shell 通道(与 Komari 插件同一机制)===== */
function rootShell(command,timeout){
var base=(typeof KANO_baseURL!=="undefined"?KANO_baseURL:(window.KANO_baseURL||"/api")).replace(/\/$/,"");
var headers=typeof common_headers!=="undefined"?common_headers:{};
var options={method:"POST",headers:headers,body:JSON.stringify({command:String(command||"").trim(),timeout:timeout||100000})};
var request=typeof fetchWithTimeout==="function"?fetchWithTimeout(base+"/root_shell",options,timeout||100000):fetch(base+"/root_shell",options);
return request.then(function(response){
return response.text().then(function(raw){
if(!raw||!raw.trim())return {success:false,content:"root_shell 返回空响应,HTTP "+response.status};
var data;try{data=JSON.parse(raw);}catch(e){return {success:false,content:"非 JSON 响应,HTTP "+response.status+":"+raw.slice(0,240)};}
if(data.error)return {success:false,content:data.error};
if(!response.ok)return {success:false,content:"HTTP "+response.status+":"+raw.slice(0,240)};
return {success:true,content:data.result};
});
}).catch(function(e){return {success:false,content:e.message||String(e)};});
}
function setBusy(b){
["cfp-install","cfp-start","cfp-stop","cfp-status","cfp-restart","cfp-uninstall"].forEach(function(id){
var e=document.getElementById(id);if(e)e.disabled=b;
});
}
function wait(ms){return new Promise(function(r){setTimeout(r,ms);});}
function bindPersistence(id,key){
var e=document.getElementById(id);
if(e)e.addEventListener("input",function(){try{localStorage.setItem(key,e.value);}catch(ignore){}});
}
/* ===== start.sh:CA bundle + nohup 起,run -config=,绝不调用 install 子命令 =====
第三个坑(Android 特有):
cf-probe 是 GOOS=linux 编译的,二进制里只有 /etc/ssl/certs/ca-certificates.crt
这类桌面发行版路径,没有 Go 的 Android 分支目录 /system/etc/security/cacerts。
Android 上那几条路径统统不存在 → Go 加载到 0 个根证书 →
TLS 报 "x509: certificate signed by unknown authority"。
解决:把系统 CA 目录里的散装 .0 证书拼成 bundle,用 SSL_CERT_FILE 指给 Go,
同时用 SSL_CERT_DIR 兜底。
*/
function startScript(){
return [
"#!/system/bin/sh",
"# cf-probe 启动脚本(Android / 小K F50)",
"# 注意:不要改成 cf-probe install,Android 上会失败",
"# 若将来上游取消 -config 参数,把下面 nohup 那行改成:",
"# cd \"$DIR\" && nohup \"$BIN\" run >>\"$LOG\" 2>&1 </dev/null &",
"# 并把 config.conf 软链到程序默认路径 /etc/config/cf-probe/config.conf",
"DIR="+DIR,
"BIN=$DIR/cf-probe",
"CFG=$DIR/config.conf",
"PID=$DIR/probe.pid",
"LOG=$DIR/probe.log",
"CA=$DIR/ca-bundle.pem",
"mkdir -p \"$DIR\"",
"if [ ! -x \"$BIN\" ]; then echo binary-not-found; exit 2; fi",
"if [ ! -s \"$CFG\" ]; then echo config-not-found; exit 3; fi",
"# ---- Android 根证书修复:拼 CA bundle(只做一次,之后复用)----",
"if [ ! -s \"$CA\" ]; then",
" for d in /apex/com.android.conscrypt/cacerts /system/etc/security/cacerts /data/misc/keychain/cacerts-added; do",
" if [ -d \"$d\" ]; then cat \"$d\"/*.0 >> \"$CA\" 2>/dev/null; fi",
" done",
"fi",
"if [ -s \"$CA\" ]; then",
" export SSL_CERT_FILE=\"$CA\"",
" echo ca-bundle=$(grep -c 'BEGIN CERTIFICATE' \"$CA\" 2>/dev/null) certs",
"else",
" echo 'WARN: 未能生成 CA bundle,TLS 可能失败'",
"fi",
"# SSL_CERT_DIR 兜底:Go 在 certFiles 全空时会读这个目录",
"for d in /apex/com.android.conscrypt/cacerts /system/etc/security/cacerts; do",
" if [ -d \"$d\" ]; then export SSL_CERT_DIR=\"$d\"; break; fi",
"done",
"# ---- 已在运行就不重复启动 ----",
"if [ -f \"$PID\" ]; then OLD=$(cat \"$PID\" 2>/dev/null); if [ -n \"$OLD\" ] && kill -0 \"$OLD\" 2>/dev/null; then echo already-running pid=$OLD; exit 0; fi; fi",
"EXISTING=$(pidof cf-probe 2>/dev/null | awk '{print $1}')",
"if [ -z \"$EXISTING\" ]; then EXISTING=$(ps -A 2>/dev/null | awk '$NF==\"cf-probe\"{print $2; exit}'); fi",
"if [ -n \"$EXISTING\" ]; then echo \"$EXISTING\" >\"$PID\"; echo already-running pid=$EXISTING; exit 0; fi",
"cd \"$DIR\" || exit 4",
"nohup \"$BIN\" run -config=\"$CFG\" >>\"$LOG\" 2>&1 </dev/null &",
"NEW=$!",
"echo \"$NEW\" >\"$PID\"",
"sleep 2",
"if kill -0 \"$NEW\" 2>/dev/null; then echo started pid=$NEW; else echo agent-exited; exit 5; fi",
""
].join("\n");
}
/* ===== stop:先杀 PID 文件记录的,再用 pidof/ps 兜底 ===== */
function stopScript(){
return "P=$(cat "+shellQuote(PID)+" 2>/dev/null); if [ -n \"$P\" ]; then kill \"$P\" 2>/dev/null; fi; "+
"EXISTING=$(pidof cf-probe 2>/dev/null); if [ -z \"$EXISTING\" ]; then EXISTING=$(ps -A 2>/dev/null | awk '$NF==\"cf-probe\"{print $2}'); fi; "+
"for P in $EXISTING; do kill \"$P\" 2>/dev/null; done; rm -f "+shellQuote(PID)+"; echo stopped";
}
/* ===== update.sh:多代理自动回退 + 下载 + checksums 校验 + 原子替换 =====
两个坑都在这里修掉了:
1. 国内卡直连 GitHub 不通 → 内置三个实测可用的加速代理,依次尝试
2. checksums.txt 里文件名带 dist/ 前缀(dist/cf-probe-linux-arm64)
→ awk 里先剥离 * 和目录前缀再比对,否则永远 checksum-not-found
*/
function updateScript(){
return [
"#!/system/bin/sh",
"# cf-probe 更新:选 ABI → 多代理取 checksums → 多代理下载 → sha256 校验 → 原子替换",
"set -u",
"DIR="+DIR,
"BIN=$DIR/cf-probe",
"TMP=$DIR/cf-probe.tmp",
"SHA=/system/bin/sha256sum",
"BASE=https://github.com/huilang-me/cfsm-agent/releases/latest/download",
"# curl 路径兜底(KANO 自带优先,其次系统 curl)",
"CURL=/data/data/com.minikano.f50_sms/files/curl",
"if [ ! -x \"$CURL\" ]; then for c in /system/bin/curl /system/xbin/curl /data/data/com.minikano.f50_sms/files/curl; do if [ -x \"$c\" ]; then CURL=\"$c\"; break; fi; done; fi",
"if [ ! -x \"$CURL\" ]; then echo curl-not-found; exit 9; fi",
"# 用户填的代理优先",
"PROXY=$(cat $DIR/github_proxy 2>/dev/null | tr -d '\\r\\n')",
"case \"$PROXY\" in \"\") ;; */) ;; *) PROXY=\"$PROXY/\" ;; esac",
"# 64 位 ABI 且有 linker64 才用 arm64,否则 armv7(cf-probe 无通用 linux-arm)",
"ABI64=$(getprop ro.product.cpu.abilist64 2>/dev/null | tr -d '\\r\\n')",
"if [ -n \"$ABI64\" ] && [ -x /system/bin/linker64 ]; then ASSET=cf-probe-linux-arm64; else ASSET=cf-probe-linux-armv7; fi",
"echo asset=$ASSET",
"# 候选列表:空串代表直连,其余为代理前缀",
"CANDS=\"$PROXY https://ghfast.top/ https://gh-proxy.com/ https://gh.llkk.cc/ \"",
"SUMS=\"\"",
"for p in $CANDS; do",
" echo \"[checksums] ${p:-直接}\"",
" SUMS=$($CURL -fsSL --max-time 25 \"${p}${BASE}/checksums.txt\" 2>/dev/null)",
" if [ -n \"$SUMS\" ]; then break; fi",
" SUMS=\"\"",
"done",
"[ -n \"$SUMS\" ] || { echo checksums-download-failed; exit 2; }",
"# checksums 行格式: <sha256> dist/cf-probe-linux-arm64 —— 必须剥掉 * 和目录前缀",
"DIGEST=$(printf '%s' \"$SUMS\" | awk -v a=\"$ASSET\" '{n=$2; sub(/^\\*/,\"\",n); sub(/^.*\\//,\"\",n); if(n==a){print $1; exit}}')",
"[ -n \"$DIGEST\" ] || { echo checksum-not-found; exit 3; }",
"echo expect-sha256=$DIGEST",
"for p in $CANDS; do",
" echo \"[download] ${p:-直接}\"",
" rm -f $TMP",
" if $CURL -fL --max-time 300 \"${p}${BASE}/${ASSET}\" -o $TMP 2>/dev/null; then",
" ACTUAL=$($SHA $TMP 2>/dev/null | awk '{print $1}')",
" if [ \"$ACTUAL\" = \"$DIGEST\" ]; then",
" chmod 755 $TMP && mv $TMP $BIN",
" echo \"installed $ASSET bytes=$(stat -c %s $BIN) sha256=$ACTUAL via ${p:-直接}\"",
" exit 0",
" fi",
" echo \" sha256 不匹配:$ACTUAL\"",
" rm -f $TMP",
" else",
" echo \" 下载失败\"",
" rm -f $TMP",
" fi",
"done",
"echo all-mirrors-failed",
"exit 4",
""
].join("\n");
}
/* ===== 开机自启用独立标记,和 Komari 的块并存不冲突 ===== */
function bootScript(){
return [
"BOOT="+BOOT,
"mkdir -p "+DIR,
"[ -f \"$BOOT\" ] || touch \"$BOOT\"",
"if ! grep -q 'CF_PROBE_F50_BEGIN' \"$BOOT\" 2>/dev/null; then printf '\\n# CF_PROBE_F50_BEGIN\\nsh /data/local/cf-probe/start.sh >/dev/null 2>&1\\n# CF_PROBE_F50_END\\n' >> \"$BOOT\"; fi",
"chmod 755 \"$BOOT\"",
""
].join("\n");
}
/* ===== config.conf 生成:严格对齐官方 install 生成的格式 =====
官方格式是 key="value"(值带双引号),实测不带引号也能跑,
但发布版统一按官方格式写,避免上游将来收紧解析时出问题。
字段清单来源:`cf-probe install` 实际写出的 config.conf */
function configText(o){
function q(v){return '"'+String(v==null?"":v).replace(/"/g,"")+'"';}
var lines=[
"SERVER_ID="+q(o.serverId), // 空串 = 服务端自动发现建号
"SECRET="+q(o.secret),
"WORKER_URL="+q(o.workerUrl),
"NAME="+q(o.name), // 自动建号时用的服务器名
"REPORT_INTERVAL="+q(o.interval||"60"),
"RESET_DAY="+q(o.resetDay||"1"),
"PING_MODE="+q(o.pingMode||"tcp"),
"CONNECTION_MODE="+q(o.connMode||"auto"),
"AUTO_UPDATE="+q("0"), // 保持手动更新,不被上游静默换二进制
"UPDATE_PROXY="+q(o.proxy||""), // 代理同时写进运行时配置,供自动更新用
"CONFIG_MD5="+q("none")
];
return lines.join("\n")+"\n";
}
function writeFiles(cfg,proxy){
var cfgCmd="mkdir -p "+shellQuote(DIR)+
"; printf '%s' "+shellQuote(cfg)+" > "+shellQuote(CFG)+
"; chmod 600 "+shellQuote(CFG)+
"; printf '%s' "+shellQuote(proxy||"")+" > "+shellQuote(DIR+"/github_proxy")+
"; chmod 600 "+shellQuote(DIR+"/github_proxy")+"; echo config-written";
var startCmd="printf '%s' "+shellQuote(startScript())+" > "+shellQuote(START)+"; chmod 700 "+shellQuote(START)+"; echo start-written";
var updCmd="printf '%s' "+shellQuote(updateScript())+" > "+shellQuote(UPDATE)+"; chmod 700 "+shellQuote(UPDATE)+"; echo update-written";
var bootCmd=bootScript()+" echo boot-written";
return rootShell(cfgCmd,30000).then(function(r){
if(!r.success)return r;
return rootShell(startCmd,30000);
}).then(function(r){
if(!r.success)return r;
return rootShell(updCmd,30000);
}).then(function(r){
if(!r.success)return r;
return rootShell(bootCmd,30000);
});
}
/* ===== 安装/更新:写配置 → 停旧 → 下载校验 → 保持停止状态 ===== */
function installOrUpdate(){
var worker=value("cfp-endpoint");
var secret=value("cfp-secret");
var sid=value("cfp-sid");
var name=value("cfp-name");
var ivl=value("cfp-interval")||"60";
var resetDay=value("cfp-reset-day");
var pingMode=value("cfp-ping-mode");
var proxy=value("cfp-proxy");
if(!worker){toast("请填写 Worker URL","red");return;}
if(!secret){toast("请填写 SECRET(面板后台的 API_SECRET)","red");return;}
if(!/^[0-9]+$/.test(ivl)){toast("上报间隔必须是数字","red");return;}
if(resetDay&&(!/^[0-9]+$/.test(resetDay)||Number(resetDay)<1||Number(resetDay)>31)){toast("流量重置日填 1-31 或留空","red");return;}
try{
localStorage.setItem(ENDPOINT_STORE,worker);
localStorage.setItem(SECRET_STORE,secret);
localStorage.setItem(SID_STORE,sid);
localStorage.setItem(NAME_STORE,name);
localStorage.setItem(IVL_STORE,ivl);
localStorage.setItem(PROXY_STORE,proxy);
}catch(e){}
var cfg=configText({serverId:sid,secret:secret,workerUrl:worker,name:name,
interval:ivl,resetDay:resetDay,pingMode:pingMode,proxy:proxy});
setBusy(true);status("正在写入配置(不会自动启动)...");
writeFiles(cfg,proxy).then(function(r){
if(!r.success){status("写入失败:\n"+r.content);setBusy(false);return;}
status("正在停止旧进程...");
return rootShell(stopScript(),30000);
}).then(function(r){
if(!r)return;
status("正在下载 cf-probe 并校验 sha256...");
return rootShell("sh "+shellQuote(UPDATE),120000);
}).then(function(r){
if(!r)return;
if(!r.success){status("下载失败:\n"+r.content);setBusy(false);return;}
status("安装完成,当前保持停止状态。\n"+String(r.content||""));
setBusy(false);toast("cf-probe 安装/更新完成","green");
runStatus();
}).catch(function(e){status("执行失败:\n"+(e.message||String(e)));setBusy(false);});
}
/* ===== 状态检测:cf-probe 没有 --version,只能靠文件 + 日志判断 ===== */
function statusCommand(){
return [
"DIR="+shellQuote(DIR),
"BIN="+shellQuote(BIN),
"CFG="+shellQuote(CFG),
"PID="+shellQuote(PID),
"LOG="+shellQuote(LOG),
"if [ -x \"$BIN\" ]; then echo installed=1 bytes=$(stat -c %s \"$BIN\" 2>/dev/null); else echo installed=0; fi",
"if [ -s \"$CFG\" ] && grep -q '^SECRET=' \"$CFG\" 2>/dev/null; then echo configured=1; else echo configured=0; fi",
"echo worker=$(grep '^WORKER_URL=' \"$CFG\" 2>/dev/null | cut -d= -f2-)",
"# CA bundle 是 Android 上 TLS 能否成功的关键,单独统计证书数量",
"if [ -s \"$DIR/ca-bundle.pem\" ]; then echo ca_certs=$(grep -c 'BEGIN CERTIFICATE' \"$DIR/ca-bundle.pem\" 2>/dev/null); else echo ca_certs=0; fi",
"P=$(cat \"$PID\" 2>/dev/null)",
"if [ -n \"$P\" ] && kill -0 \"$P\" 2>/dev/null; then echo running=1 pid=$P; else P=$(pidof cf-probe 2>/dev/null | awk '{print $1}'); if [ -n \"$P\" ] && kill -0 \"$P\" 2>/dev/null; then echo running=1 pid=$P; else echo running=0; fi; fi",
"LAST=$(tail -n 240 \"$LOG\" 2>/dev/null | awk '/WSS connected/{s=\"connected\"} /probe stopped|WSS retry delayed|POST fallback delayed|WSS error|WSS close|agent-exited/{s=\"disconnected\"} END{print s}')",
"if [ \"$LAST\" = connected ]; then echo connected=1; else echo connected=0; fi",
"# 把最近一条失败原因单独提出来,方便一眼看出卡在哪",
"REASON=$(tail -n 60 \"$LOG\" 2>/dev/null | grep -oE 'reason=[^\\\"]*' | tail -1 | cut -c1-120)",
"[ -n \"$REASON\" ] && echo \"last_reason=$REASON\"",
"echo '--- recent log ---'",
"tail -n 60 \"$LOG\" 2>/dev/null || echo '(暂无日志)'"
].join("\n");
}
function renderState(content){
var e=document.getElementById("cfp-state");
if(!e)return;
var installed=/^installed=1/m.test(content);
var configured=/^configured=1/m.test(content);
var running=/^running=1/m.test(content);
var connected=/^connected=1/m.test(content);
var caMatch=content.match(/^ca_certs=(\d+)/m);
var caN=caMatch?parseInt(caMatch[1],10):0;
var state=(running&&connected)?"面板:已连接":(running?"面板:未确认(进程在跑)":"面板:未连接");
var caTxt=(caN>0?"CA:"+caN+"张":'CA:<span style="color:#ff8e8e">未生成</span>');
e.innerHTML="安装:"+(installed?"已安装":"未安装")+" 配置:"+(configured?"已写入":"未写入")+
" "+caTxt+" 进程:"+(running?"运行中":"未运行")+" "+state;
e.style.color=(running&&connected)?"#78d991":(running?"#f5c76b":"#ff8e8e");
}
function runStatus(){
setBusy(true);
rootShell(statusCommand(),100000).then(function(r){
if(r.success){var c=String(r.content||"");renderState(c);status(c||"完成");}
else{renderState("");status("检测失败:\n"+r.content);}
setBusy(false);
});
}
function startAgent(){
setBusy(true);status("正在启动 cf-probe...");
rootShell("sh "+shellQuote(START),30000).then(function(r){
if(!r.success){status("启动失败:\n"+r.content);setBusy(false);return null;}
status("启动命令已执行,等 4 秒查连接状态...\n"+String(r.content||""));
return wait(4000);
}).then(function(r){
if(!r)return;
return rootShell(statusCommand(),100000);
}).then(function(r){
if(!r)return;
if(r.success){var c=String(r.content||"");renderState(c);status(c||"启动后检测完成");}
else status("启动后检测失败:\n"+r.content);
setBusy(false);
}).catch(function(e){status("启动失败:\n"+(e.message||String(e)));setBusy(false);});
}
function stopAgent(){
setBusy(true);status("正在停止...");
rootShell(stopScript(),30000).then(function(r){
if(!r.success){status("停止失败:\n"+r.content);setBusy(false);return null;}
return wait(1500).then(function(){return rootShell(statusCommand(),100000);});
}).then(function(r){
if(!r)return;
if(r.success){var c=String(r.content||"");renderState(c);status(c||"已停止");}
setBusy(false);
}).catch(function(e){status("停止失败:\n"+(e.message||String(e)));setBusy(false);});
}
function restart(){
setBusy(true);status("正在重启...");
rootShell(stopScript()+"; sleep 2; sh "+shellQuote(START),30000).then(function(r){
if(!r.success){status("重启失败:\n"+r.content);setBusy(false);return null;}
status("重启命令已执行,等 4 秒复查...");
return wait(4000).then(function(){return rootShell(statusCommand(),100000);});
}).then(function(r){
if(!r)return;
if(r.success){var c=String(r.content||"");renderState(c);status(c||"重启后检测完成");}
else status("重启后检测失败:\n"+r.content);
setBusy(false);
}).catch(function(e){status("重启失败:\n"+(e.message||String(e)));setBusy(false);});
}
/* ===== 卸载:只删自己那块 boot 标记,不动 Komari 的 ===== */
function uninstall(){
if(!confirm("确认停止并卸载 cf-probe?(不会影响 Komari Agent)"))return;
setBusy(true);status("正在卸载...");
var cmd=stopScript()+
"; sed -i '/# CF_PROBE_F50_BEGIN/,/# CF_PROBE_F50_END/d' "+shellQuote(BOOT)+" 2>/dev/null"+
"; rm -rf "+shellQuote(DIR)+"; echo uninstalled";
rootShell(cmd,30000).then(function(r){
if(r.success){renderState("");status("已卸载 cf-probe");}
else status("卸载失败:\n"+r.content);
setBusy(false);
});
}
function showModal(){
if(typeof window.showModal==="function")window.showModal("#CfProbeF50Modal");
else document.getElementById("CfProbeF50Modal").style.display="flex";
runStatus();
}
function hideModal(){
if(typeof window.closeModal==="function")window.closeModal("#CfProbeF50Modal");
else document.getElementById("CfProbeF50Modal").style.display="none";
}
function addButton(){
if(document.getElementById("CFPROBE_F50_BTN"))return;
var list=document.querySelector(".actions-buttons");
if(!list)return;
var btn=document.createElement("button");
btn.id="CFPROBE_F50_BTN";
btn.className="btn";
btn.textContent="CF探针";
btn.onclick=showModal;
var adv=document.getElementById("ADVANCE");
if(adv&&adv.parentNode===list)list.insertBefore(btn,adv.nextSibling);else list.appendChild(btn);
}
function addModal(){
if(document.getElementById("CfProbeF50Modal"))return;
var style=document.createElement("style");
style.textContent=[
"#CfProbeF50Modal{top:0!important;left:0!important;right:0!important;bottom:0!important;width:100vw!important;height:100vh!important;display:flex!important;align-items:center;justify-content:center;overflow:auto;padding:14px;z-index:10000}",
"#CfProbeF50Modal[style*='display: none']{display:none!important}",
"#CfProbeF50Modal>.modal{position:relative!important;top:auto!important;left:auto!important;transform:none!important;width:90%;max-width:520px;max-height:90vh;overflow:auto}",
"#CfProbeF50Modal .action{background:transparent!important;padding:0!important;border-radius:0!important}",
"#CfProbeF50Modal .action input{width:100%;height:34px;padding:7px 9px;margin:4px 0 0;background:rgba(0,0,0,.32)!important;color:var(--dark-text-color)!important;border:1px solid var(--dark-title-color)!important;border-radius:6px;backdrop-filter:none!important;box-sizing:border-box}",
"#CfProbeF50Modal .action input::placeholder{color:var(--dark-text-color);opacity:.6}",
"#CfProbeF50Modal .btn{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:5px}",
"#CfProbeF50Modal .btn button{margin:0}"
].join("");
document.head.appendChild(style);
var modal=document.createElement("div");
modal.className="mask";modal.id="CfProbeF50Modal";modal.style.display="none";
modal.innerHTML=[
'<div class="modal" style="width:90%;max-width:520px;display:flex;flex-direction:column;">',
'<div class="title" style="margin-bottom:10px;">CF-Server-Monitor 探针</div>',
'<div class="content" style="text-align:left;max-height:500px;">',
'<div class="action" style="margin-bottom:8px;"><label style="width:100%;font-size:12px;">Worker URL(必填)</label><input id="cfp-endpoint" type="url" placeholder="https://你的面板域名/update"></div>',
'<div class="action" style="margin-bottom:8px;"><label style="width:100%;font-size:12px;">SECRET(必填)</label><input id="cfp-secret" type="password" autocomplete="off" placeholder="面板 API_SECRET"></div>',
'<div class="action" style="margin-bottom:8px;"><label style="width:100%;font-size:12px;">SERVER_ID(留空 = 自动建号)</label><input id="cfp-sid" type="text" placeholder="留空,由面板自动发现并创建"></div>',
'<div class="action" style="margin-bottom:8px;"><label style="width:100%;font-size:12px;">服务器名称(自动建号时用)</label><input id="cfp-name" type="text" placeholder="例如 小K-F50;留空则用公网 IP"></div>',
'<div class="action" style="margin-bottom:8px;"><label style="width:100%;font-size:12px;">上报间隔(秒)</label><input id="cfp-interval" type="number" min="30" step="10" placeholder="60"></div>',
'<div class="action" style="margin-bottom:8px;"><label style="width:100%;font-size:12px;">流量重置日(可选 1-31)</label><input id="cfp-reset-day" type="number" min="1" max="31" placeholder="留空表示不设置"></div>',
'<div class="action" style="margin-bottom:8px;"><label style="width:100%;font-size:12px;">Ping 模式(tcp / icmp)</label><input id="cfp-ping-mode" type="text" placeholder="留空默认 tcp"></div>',
'<div class="action" style="margin-bottom:8px;"><label style="width:100%;font-size:12px;">GitHub 下载代理前缀(可选)</label><input id="cfp-proxy" type="url" placeholder="例如 https://你的代理前缀/"></div>',
'<div class="title" style="display:block;font-size:11px;line-height:1.6;">SECRET 是你自己面板后台的 API_SECRET;用默认的这个面板则需要对方提供。<br>SERVER_ID 留空时要求面板已开启「自动发现」,否则请填面板里创建的服务器 UUID。<br>固定行为:不走 cf-probe install(Android 的 /etc 只读、无 systemd),改为手写 config.conf + run -config;开机自启写入 ufi_tools_boot.sh 的独立标记段,与 Komari Agent 共存。</div>',
'<div id="cfp-state" style="margin-top:9px;padding:7px 8px;font-size:11px;line-height:1.5;background:#0004;border-left:4px solid var(--dark-title-color);text-align:left;">正在检测...</div>',
'<pre id="cfp-result" style="margin:12px 0 0;padding:8px;min-height:80px;max-height:180px;overflow:auto;white-space:pre-wrap;word-break:break-word;font-size:11px;text-align:left;background:#0006;border-radius:6px;"></pre>',
'</div>',
'<div class="btn" style="text-align:right;margin-top:8px;">',
'<button id="cfp-install" type="button">安装/更新</button>',
'<button id="cfp-start" type="button">启动</button>',
'<button id="cfp-stop" type="button">停止</button>',
'<button id="cfp-status" type="button">刷新状态</button>',
'<button id="cfp-restart" type="button">重启</button>',
'<button id="cfp-uninstall" type="button">卸载</button>',
'<button id="cfp-close" type="button">关闭</button>',
'</div></div>'
].join("");
document.body.appendChild(modal);
document.getElementById("cfp-install").onclick=installOrUpdate;
document.getElementById("cfp-start").onclick=startAgent;
document.getElementById("cfp-stop").onclick=stopAgent;
document.getElementById("cfp-status").onclick=runStatus;
document.getElementById("cfp-restart").onclick=restart;
document.getElementById("cfp-uninstall").onclick=uninstall;
document.getElementById("cfp-close").onclick=hideModal;
/* 预填:优先用使用者上次填过的值(localStorage),否则用 DEFAULTS
注意 SECRET / SERVER_ID 的 DEFAULTS 是空串,首次打开一定是空白输入框 */
function prefill(id,store,fallback){
var el=document.getElementById(id);
if(!el)return;
var saved="";
try{saved=localStorage.getItem(store)||"";}catch(e){}
el.value=saved||fallback||"";
}
prefill("cfp-endpoint",ENDPOINT_STORE,DEFAULTS.worker);
prefill("cfp-secret",SECRET_STORE,DEFAULTS.secret);
prefill("cfp-sid",SID_STORE,DEFAULTS.sid);
prefill("cfp-name",NAME_STORE,DEFAULTS.name);
prefill("cfp-interval",IVL_STORE,DEFAULTS.interval);
prefill("cfp-proxy",PROXY_STORE,DEFAULTS.proxy);
bindPersistence("cfp-endpoint",ENDPOINT_STORE);
bindPersistence("cfp-secret",SECRET_STORE);
bindPersistence("cfp-sid",SID_STORE);
bindPersistence("cfp-name",NAME_STORE);
bindPersistence("cfp-interval",IVL_STORE);
bindPersistence("cfp-proxy",PROXY_STORE);
}
function boot(){
if(!document.body){setTimeout(boot,100);return;}
addButton();addModal();
if(!document.getElementById("CFPROBE_F50_BTN"))setTimeout(boot,300);
}
boot();
})();
</script>
借鉴于
Komari探针 F50扎针