Files
2026-01-09 23:18:00 +00:00

522 lines
20 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
// 设置默认时区(可选,防止警告)
date_default_timezone_set('Asia/Shanghai');
// ===== 辅助函数:vmess 解析与构建 =====
// 解析一行 vmess 链接,返回 [bool 成功, array 数据]
function parse_vmess_line(string $line): array {
$line = trim($line);
if ($line === '' || stripos($line, 'vmess://') !== 0) {
return [false, null];
}
$b64 = substr($line, strlen('vmess://'));
$b64 = trim($b64);
if ($b64 === '') {
return [false, null];
}
// 补齐 Base64 的 '=' 填充
$mod = strlen($b64) % 4;
if ($mod > 0) {
$b64 .= str_repeat('=', 4 - $mod);
}
$json_str = base64_decode($b64, true);
if ($json_str === false) {
return [false, null];
}
$data = json_decode($json_str, true);
if (!is_array($data)) {
return [false, null];
}
return [true, $data];
}
// 将配置重新编码为 vmess 行
function build_vmess_line(array $data): string {
$json = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$b64 = base64_encode($json);
return 'vmess://' . $b64;
}
// 标准 vmess 字段定义(显示顺序 + 中文说明)
$vmess_fields = [
'ps' => '备注(ps)',
'add' => '地址(add)',
'port' => '端口(port)',
'id' => 'UUID(id)',
'aid' => '额外ID(aid)',
'scy' => '加密方式(scy)',
'net' => '网络(net)',
'type' => '伪装类型(type)',
'host' => 'Host(host)',
'path' => '路径(path)',
'tls' => 'TLS(tls)',
'sni' => 'SNI(sni)',
'alpn' => 'ALPN(alpn)',
'insecure' => 'insecure',
];
// ===== 控制流程变量 =====
$mode = 'input'; // input | edit | result
$original_text = '';
$result_text = '';
$gost_text = '';
$vmess_rows = []; // 仅合法 vmess 行的解析结果
$other_lines = []; // 非 vmess 行或解析失败的行
$target_ip = '';
$batch_from = '';
$batch_to = '';
$batch_ip_from = '';
$batch_ip_to = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$step = $_POST['step'] ?? 'input';
$target_ip = trim($_POST['target_ip'] ?? '');
$batch_from = trim($_POST['batch_from'] ?? '');
$batch_to = trim($_POST['batch_to'] ?? '');
$batch_ip_from = trim($_POST['batch_ip_from'] ?? '');
$batch_ip_to = trim($_POST['batch_ip_to'] ?? '');
if ($step === 'upload') {
// 第一步:用户粘贴 vmess 列表,进入“表格编辑模式”
$original_text = trim($_POST['content'] ?? '');
if ($original_text !== '') {
$lines = preg_split("/\r\n|\n|\r/", $original_text);
foreach ($lines as $line) {
$raw = trim($line);
if ($raw === '') {
continue;
}
[$ok, $data] = parse_vmess_line($raw);
if (!$ok) {
$other_lines[] = $raw;
continue;
}
// 拆分成标准字段 + 额外字段
$fields = [];
foreach ($vmess_fields as $key => $_label) {
$fields[$key] = isset($data[$key]) ? (string)$data[$key] : '';
}
$extra = array_diff_key($data, $vmess_fields);
$vmess_rows[] = [
'fields' => $fields,
'extra' => !empty($extra)
? json_encode($extra, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
: '',
];
}
}
$mode = 'edit';
} elseif ($step === 'save') {
// 第二步:从表格接收编辑后的数据,生成最终 vmess 列表
$rows_post = $_POST['rows'] ?? [];
$other_lines = [];
$other_raw = trim($_POST['other_lines'] ?? '');
if ($other_raw !== '') {
$other_lines = preg_split("/\r\n|\n|\r/", $other_raw);
}
$lines_out = [];
$gost_lines_out = [];
// 从表格重建 JSON
foreach ($rows_post as $row) {
$fields = $row['fields'] ?? [];
$extra_json = $row['extra'] ?? '';
$data = [];
foreach ($vmess_fields as $key => $_label) {
if (isset($fields[$key]) && $fields[$key] !== '') {
$data[$key] = $fields[$key];
}
}
if ($extra_json !== '') {
$extra = json_decode($extra_json, true);
if (is_array($extra)) {
$data = array_merge($data, $extra);
}
}
// 批量替换(提交阶段再兜底跑一遍)
if ($target_ip !== '') {
$data['add'] = $target_ip;
}
if ($batch_from !== '' && $batch_to !== '') {
array_walk_recursive($data, function (&$value) use ($batch_from, $batch_to) {
if (is_string($value) && $value !== '') {
$value = str_replace($batch_from, $batch_to, $value);
}
});
}
if ($batch_ip_from !== '' && $batch_ip_to !== '' && isset($data['add'])) {
$data['add'] = str_replace($batch_ip_from, $batch_ip_to, (string)$data['add']);
}
// 构建 vmess 行
$lines_out[] = build_vmess_line($data);
// 构建对应的 Gost 配置行(端口使用当前记录的 port 字段)
$ip_for_gost = isset($data['add']) ? (string)$data['add'] : '';
$port_for_gost = isset($data['port']) ? (string)$data['port'] : '';
if ($ip_for_gost !== '' && $port_for_gost !== '') {
if (strpos($ip_for_gost, ':') === false) {
// IPv4
$gost_lines_out[] = 'nonencrypt/' . $port_for_gost . '#' . $ip_for_gost . '#' . $port_for_gost;
} else {
// IPv6,添加方括号
$ip = $ip_for_gost;
if ($ip[0] !== '[') {
$ip = '[' . $ip . ']';
}
$gost_lines_out[] = 'nonencrypt/' . $port_for_gost . '#' . $ip . '#' . $port_for_gost;
}
} else {
$gost_lines_out[] = '';
}
}
// 把无法解析的其它行也保留在结果中
foreach ($other_lines as $raw) {
$raw = trim($raw);
if ($raw !== '') {
$lines_out[] = $raw;
}
}
$result_text = implode(PHP_EOL, $lines_out);
$gost_text = implode(PHP_EOL, $gost_lines_out);
$mode = 'result';
}
} else {
// 默认示例文本
$original_text = "vmess://ew0KICAidiI6ICIyIiwNCiAgInBzIjogIkFsaVBDLUdHWS1TaGFyZS1CZXN0dm0tU0ciLA0KICAiYWRkIjogIjEuMS4xLjEiLA0KICAicG9ydCI6ICIzMzg4OCIsDQogICJpZCI6ICI3N2MwYWFmZC03MTNhLTQwZDUtYmI2MS1mZWMyN2JiMTI0MTEiLA0KICAiYWlkIjogIjAiLA0KICAic2N5IjogImF1dG8iLA0KICAibmV0IjogInRjcCIsDQogICJ0eXBlIjogIm5vbmUiLA0KICAiaG9zdCI6ICIiLA0KICAicGF0aCI6ICIiLA0KICAidGxzIjogIiIsDQogICJzbmkiOiAiIiwNCiAgImFscG4iOiAiIiwNCiAgImZwIjogIiIsDQogICJpbnNlY3VyZSI6ICIwIg0KfQ==";
$mode = 'input';
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>vmess 批量编辑与替换工具</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f4f6f9;
color: #333;
margin: 0;
padding: 20px;
display: flex;
justify-content: center;
}
.container {
background-color: #fff;
width: 100%;
max-width: 1100px;
padding: 24px 28px 32px;
border-radius: 8px;
box-shadow: 0 4px 15px rgba(0,0,0,0.08);
}
h1 {
text-align: center;
color: #2c3e50;
margin: 0 0 20px;
font-size: 22px;
}
h2 {
font-size: 16px;
margin: 18px 0 10px;
color: #2c3e50;
}
.form-group {
margin-bottom: 16px;
}
label {
display: block;
margin-bottom: 6px;
font-weight: 600;
color: #555;
font-size: 13px;
}
input[type="text"] {
width: 100%;
padding: 7px 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 13px;
box-sizing: border-box;
}
textarea {
width: 100%;
height: 260px;
padding: 8px 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-family: monospace;
resize: vertical;
box-sizing: border-box;
background-color: #fafafa;
font-size: 12px;
}
.btn-primary {
background-color: #007bff;
color: white;
border: none;
padding: 10px 20px;
font-size: 14px;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.25s;
}
.btn-primary:hover {
background-color: #0056b3;
}
.btn-secondary {
background-color: #6c757d;
color: #fff;
border: none;
padding: 6px 10px;
font-size: 12px;
border-radius: 4px;
cursor: pointer;
margin-right: 6px;
}
.btn-secondary:hover {
background-color: #5a6268;
}
.grid-layout {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-top: 12px;
}
@media (max-width: 900px) {
.grid-layout {
grid-template-columns: 1fr;
}
}
.table-wrapper {
max-height: 460px;
overflow: auto;
border: 1px solid #e0e0e0;
border-radius: 4px;
background-color: #fafafa;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 12px;
}
thead {
position: sticky;
top: 0;
background-color: #f0f2f5;
z-index: 1;
}
th, td {
border: 1px solid #e1e1e1;
padding: 4px 6px;
text-align: left;
white-space: nowrap;
}
th {
font-weight: 600;
font-size: 11px;
}
td input[type="text"] {
width: 140px;
padding: 3px 5px;
font-size: 11px;
}
td input.cell-short {
width: 80px;
}
td input.cell-long {
width: 200px;
}
.small-hint {
font-size: 11px;
color: #888;
margin-top: 4px;
}
.section {
margin-bottom: 18px;
}
</style>
</head>
<body>
<div class="container">
<h1>vmess 批量编辑与替换工具</h1>
<?php if ($mode === 'input'): ?>
<form method="post" action="">
<input type="hidden" name="step" value="upload">
<div class="section">
<h2>① 粘贴 vmess 分享地址(每行一条)</h2>
<textarea name="content" placeholder="在此粘贴 vmess:// 开头的分享地址,每行一条"><?php echo htmlspecialchars($original_text); ?></textarea>
<div class="small-hint">提交后会进入类似 Excel 的表格界面,可以逐行编辑字段。</div>
</div>
<button type="submit" class="btn-primary">下一步:进入表格编辑</button>
</form>
<?php elseif ($mode === 'edit'): ?>
<form method="post" action="">
<input type="hidden" name="step" value="save">
<div class="section">
<h2>② 全局批量替换设置(可选)</h2>
<div class="grid-layout">
<div>
<div class="form-group">
<label for="target_ip">统一替换为的新 IP(写入 add 字段)</label>
<input type="text" id="target_ip" name="target_ip" value="<?php echo htmlspecialchars($target_ip); ?>" placeholder="例如:192.168.1.1">
<div class="small-hint">留空则不修改 IP。</div>
</div>
<div class="form-group">
<label>字符批量替换(作用于所有字段,如备注、路径等)</label>
<input type="text" id="batch_from" name="batch_from" value="<?php echo htmlspecialchars($batch_from); ?>" placeholder="原始字符,如 AliPC-GGY">
<div style="height:4px;"></div>
<input type="text" id="batch_to" name="batch_to" value="<?php echo htmlspecialchars($batch_to); ?>" placeholder="目标字符,如 TX-Haha">
<div class="small-hint">
点击下方按钮可在当前表格中直接替换,不提交到服务器。
</div>
<button type="button" class="btn-secondary" onclick="applyBatchReplaceAll()">在表格中应用字符替换</button>
</div>
</div>
<div>
<div class="form-group">
<label>IP 批量替换(仅作用于地址 add 字段)</label>
<input type="text" id="batch_ip_from" name="batch_ip_from" value="<?php echo htmlspecialchars($batch_ip_from); ?>" placeholder="原始 IP 片段,例如 1.1.1.1">
<div style="height:4px;"></div>
<input type="text" id="batch_ip_to" name="batch_ip_to" value="<?php echo htmlspecialchars($batch_ip_to); ?>" placeholder="目标 IP 片段,例如 8.8.8.8">
<div class="small-hint">
点击按钮仅在 IP 列(add)中替换,方便从一台机器迁移到另一台。
</div>
<button type="button" class="btn-secondary" onclick="applyBatchReplaceIP()">在表格中应用 IP 替换</button>
</div>
</div>
</div>
</div>
<div class="section">
<h2>③ 表格编辑(类似 Excel,可逐行修改任意字段)</h2>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>#</th>
<?php foreach ($vmess_fields as $key => $label): ?>
<th><?php echo htmlspecialchars($label); ?></th>
<?php endforeach; ?>
</tr>
</thead>
<tbody>
<?php foreach ($vmess_rows as $idx => $row): ?>
<tr>
<td><?php echo $idx + 1; ?></td>
<?php foreach ($vmess_fields as $key => $label): ?>
<?php
$value = $row['fields'][$key] ?? '';
$classes = 'cell-string';
if (in_array($key, ['port', 'aid'], true)) {
$classes .= ' cell-short';
} elseif (in_array($key, ['ps', 'host', 'path', 'id'], true)) {
$classes .= ' cell-long';
}
if ($key === 'add') {
$classes .= ' cell-add';
}
?>
<td>
<input type="text" name="rows[<?php echo $idx; ?>][fields][<?php echo htmlspecialchars($key); ?>]"
value="<?php echo htmlspecialchars($value); ?>"
class="<?php echo $classes; ?>">
</td>
<?php endforeach; ?>
<input type="hidden" name="rows[<?php echo $idx; ?>][extra]" value="<?php echo htmlspecialchars($row['extra']); ?>">
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php if (!empty($other_lines)): ?>
<div class="small-hint" style="margin-top:6px;">
下列行无法识别为 vmess,将原样保留在结果中(如需要可自行编辑后粘贴回上一步):
</div>
<textarea readonly><?php echo htmlspecialchars(implode("\n", $other_lines)); ?></textarea>
<input type="hidden" name="other_lines" value="<?php echo htmlspecialchars(implode("\n", $other_lines)); ?>">
<?php else: ?>
<input type="hidden" name="other_lines" value="">
<?php endif; ?>
</div>
<div class="section" style="margin-top: 16px;">
<button type="submit" class="btn-primary">确认提交并生成 vmess 结果</button>
</div>
</form>
<script>
function applyBatchReplaceAll() {
var from = document.getElementById('batch_from').value;
var to = document.getElementById('batch_to').value;
if (!from) return;
var inputs = document.querySelectorAll('input.cell-string');
inputs.forEach(function (input) {
if (input.value) {
input.value = input.value.split(from).join(to);
}
});
}
function applyBatchReplaceIP() {
var from = document.getElementById('batch_ip_from').value;
var to = document.getElementById('batch_ip_to').value;
if (!from || !to) return;
var inputs = document.querySelectorAll('input.cell-add');
inputs.forEach(function (input) {
if (input.value) {
input.value = input.value.split(from).join(to);
}
});
}
</script>
<?php elseif ($mode === 'result'): ?>
<div class="section">
<h2>④ 生成结果</h2>
<div class="grid-layout">
<div>
<label>生成后的 vmess 链接(每行一条,可直接复制使用)</label>
<textarea readonly onclick="this.select()" placeholder="结果会显示在这里"><?php echo htmlspecialchars($result_text); ?></textarea>
</div>
<div>
<label>对应的 Gost 配置(每条记录一行,端口使用当前记录的 port 值)</label>
<textarea readonly onclick="this.select()" placeholder="Gost 配置会显示在这里"><?php echo htmlspecialchars($gost_text); ?></textarea>
<div class="small-hint">示例:IPv4 => nonencrypt/33888#1.1.1.1#33888IPv6 => nonencrypt/22777#[2403:2c80::1]#22777(端口都取自各自 vmess 的 port)。</div>
</div>
</div>
<div class="section" style="margin-top: 16px;">
<form method="post" action="">
<input type="hidden" name="step" value="input">
<button type="submit" class="btn-primary">返回第①步继续处理其他链接</button>
</form>
</div>
</div>
<?php endif; ?>
</div>
</body>
</html>