vmess批量编辑器

This commit is contained in:
2026-01-09 23:18:00 +00:00
parent 9bf2edede9
commit 4240f550d9
+521
View File
@@ -0,0 +1,521 @@
<?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>