第35批:机翻无条件执行;IPv4/IPv6未加白即拦;通用resources模板同步补缺
This commit is contained in:
@@ -1122,6 +1122,108 @@ public class ChatFilterManager {
|
||||
return containsIllegalDomain(probe);
|
||||
}
|
||||
|
||||
// ==================== 35批修订:IPv4/IPv6 地址检测(加白豁免,其余无条件拦) ====================
|
||||
|
||||
/** IPv4:四段 0-255,前后用非数字边界包住("1.21.4" 三段版本号不匹配,"1.2.3.4.5" 不匹配) */
|
||||
private static final Pattern IPV4_PATTERN = Pattern.compile(
|
||||
"(?<![0-9.])(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)"
|
||||
+ "(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}(?![0-9]|\\.\\d)");
|
||||
|
||||
/** IPv6 候选:>=1 个冒号的十六进制/冒号/点串(粗取后 isRealIpv6 严格校验,防 12:34:56 时间误伤) */
|
||||
private static final Pattern IPV6_CAND = Pattern.compile(
|
||||
"(?i)(?:[0-9a-f]{0,4}:){1,}[0-9a-f:.]*");
|
||||
|
||||
/**
|
||||
* 提取消息里全部 IPv4/IPv6 地址(严格校验、按出现顺序去重)。
|
||||
* IPv4 按 0-255 校验;IPv6 先粗取候选再去尾点后校验组数/::唯一性/组格式,
|
||||
* 时间 "12:34:56"、三段冒号串不会误伤;句末句号("…cbd5.")不吞。
|
||||
*/
|
||||
public java.util.List<String> extractIpAddresses(String msg) {
|
||||
java.util.List<String> out = new ArrayList<>();
|
||||
if (msg == null || msg.isEmpty()) return out;
|
||||
Matcher m4 = IPV4_PATTERN.matcher(msg);
|
||||
while (m4.find()) {
|
||||
String ip = m4.group();
|
||||
if (!out.contains(ip)) out.add(ip);
|
||||
}
|
||||
Matcher m6 = IPV6_CAND.matcher(msg);
|
||||
while (m6.find()) {
|
||||
String cand = m6.group();
|
||||
// 去尾部句点(英文句号紧跟 IP 时不能因此漏判)
|
||||
int end = cand.length();
|
||||
while (end > 0 && cand.charAt(end - 1) == '.') end--;
|
||||
cand = cand.substring(0, end);
|
||||
if (cand.isEmpty() || !isRealIpv6(cand)) continue;
|
||||
if (!out.contains(cand)) out.add(cand);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息是否包含「未加白」的 IPv4/IPv6(35批修订口径,用户原话:
|
||||
* 「对加白的ip和玩家不受影响,其它玩家ipv6、ipv4全部无条件拦下来」)。
|
||||
* 加白 IP = chat.txt「白名单」节条目(与域名共用同一列表,支持完整 IP 与 *.x.x.x 后缀);
|
||||
* 消息里的 IP 全部加白才放行,出现任何一个未加白 IP 即返回 true。
|
||||
* 加白玩家在 onChat 更早的「白名单玩家」早退里豁免,走不到 IP 检测。
|
||||
*/
|
||||
public boolean containsNonWhitelistedIp(String msg) {
|
||||
for (String ip : extractIpAddresses(msg)) {
|
||||
if (!isWhitelistedIp(ip)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加白 IP 判定。不复用 isWhitelisted:它按第一个冒号剥「端口」,
|
||||
* 对 IPv6(2408:8352::1)会截断成 "2408" 误判未加白 —— 这里不剥端口,
|
||||
* 提取出来的 IP 本就不含端口;条目支持完整等值与 *.段 后缀通配。
|
||||
*/
|
||||
private boolean isWhitelistedIp(String ip) {
|
||||
if (ip == null || ip.isEmpty()) return false;
|
||||
String clean = ip.trim().toLowerCase(java.util.Locale.ROOT);
|
||||
for (String entry : whitelistUrls) {
|
||||
String e = entry.trim().toLowerCase(java.util.Locale.ROOT);
|
||||
if (e.isEmpty()) continue;
|
||||
if (e.startsWith("*.")) {
|
||||
if (clean.endsWith(e.substring(1))) return true;
|
||||
} else if (clean.equals(e)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 候选串是否为合法 IPv6(:: 压缩、末段 IPv4-mapped、%scope 尾巴) */
|
||||
private static boolean isRealIpv6(String t) {
|
||||
if (t == null || t.isEmpty()) return false;
|
||||
String s = t;
|
||||
int pct = s.indexOf('%');
|
||||
if (pct >= 0) s = s.substring(0, pct); // fe80::1%eth0
|
||||
int slash = s.indexOf('/');
|
||||
if (slash >= 0) s = s.substring(0, slash); // CIDR 尾巴
|
||||
if (s.indexOf(':') < 0) return false;
|
||||
int dc = s.indexOf("::");
|
||||
if (dc >= 0 && s.indexOf("::", dc + 1) >= 0) return false; // 两个 :: 非法
|
||||
String body = dc >= 0
|
||||
? s.substring(0, dc) + ":" + s.substring(dc + 2) : s;
|
||||
int groups = 0;
|
||||
for (String part : body.split(":", -1)) {
|
||||
if (part.isEmpty()) continue;
|
||||
if (part.indexOf('.') >= 0) {
|
||||
// 末段 IPv4 形态(::ffff:192.168.1.1)→ 按 IPv4 校验,计 2 组
|
||||
if (!part.matches("(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)"
|
||||
+ "(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}")) return false;
|
||||
groups += 2;
|
||||
} else {
|
||||
if (!part.matches("[0-9a-fA-F]{1,4}")) return false;
|
||||
groups++;
|
||||
}
|
||||
if (groups > 8) return false;
|
||||
}
|
||||
if (dc >= 0) return groups < 8; // :: 至少压缩 1 组 -> 显式组 <=7
|
||||
return groups == 8; // 无 :: 必须恰好 8 组
|
||||
}
|
||||
|
||||
public boolean isLikelyDomain(String url) {
|
||||
int lastDot = url.lastIndexOf('.');
|
||||
if (lastDot < 0
|
||||
|
||||
@@ -958,7 +958,7 @@ public class ConfigManager {
|
||||
if (t.startsWith("```")) { origHasCfgBlock = true; continue; }
|
||||
if (!t.startsWith("|")) continue;
|
||||
String[] cols = t.split("\\|", -1);
|
||||
if (cols.length < 4) continue;
|
||||
if (cols.length < 3) continue;
|
||||
String key = cols[1].trim();
|
||||
if (key.isEmpty() || key.matches("[-:\\s]+")) continue; // 表头分隔行
|
||||
origKeys.add(key);
|
||||
@@ -988,7 +988,7 @@ public class ConfigManager {
|
||||
}
|
||||
if (!t.startsWith("|")) continue;
|
||||
String[] cols = t.split("\\|", -1);
|
||||
if (cols.length < 4) continue;
|
||||
if (cols.length < 3) continue;
|
||||
String key = cols[1].trim();
|
||||
if (key.isEmpty() || key.matches("[-:\\s]+")) continue;
|
||||
tplKeys.add(key);
|
||||
@@ -1028,7 +1028,7 @@ public class ConfigManager {
|
||||
if (inFence) continue;
|
||||
if (t.startsWith("|")) {
|
||||
String[] cols = t.split("\\|", -1);
|
||||
String key = cols.length >= 4 ? cols[1].trim() : "";
|
||||
String key = cols.length >= 3 ? cols[1].trim() : "";
|
||||
boolean isData = !key.isEmpty() && !key.matches("[-:\\s]+");
|
||||
if (tableEndIdx < 0) {
|
||||
if (isData || key.matches("[-:\\s]+")) tableEndIdx = i;
|
||||
@@ -1129,6 +1129,280 @@ public class ConfigManager {
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 35批:resources 模板通用同步(全量扫描,缺啥补啥) ====================
|
||||
|
||||
/**
|
||||
* 通用模板同步(35批,用户口径「处理所有我们插件 resources 里的模板文件,如果插件数据目录
|
||||
* 里的文件内容缺了模板里的一些配置值,立刻备份,提取内容出来,安全释放新的。只插入对应的
|
||||
* 配置,不能误伤其它内容」):启动时扫描 jar 内全部资源模板,与插件数据目录逐一对照 ——
|
||||
* ① 数据目录缺该文件 → 直接安全释放新版;
|
||||
* ② 文件存在但缺模板里的配置值 → 先备份 .bak(逐字节校验)→ 只把缺失项插入,
|
||||
* 用户已有配置值、自定义键、说明文档一律原样保留(绝不整文件覆盖);
|
||||
* ③ 内容与模板一致 → 安静跳过(幂等,不产生 .bak)。
|
||||
* 按扩展名分派:.md → safeUpgradeDoc(只补配置区表格行,说明文档不动);
|
||||
* .json → 递归缺键补入(Gson 解析失败=用户加了注释/格式特殊 → 跳过不动);
|
||||
* 纯文本(.txt/.yml 等)→ 缺失数据行追加;未识别类型只记录不改动。
|
||||
* 任何异常只打日志,绝不影响启动。必须在任何 Manager 读配置文件之前调用(onEnable 早期)。
|
||||
*/
|
||||
public void syncAllResourceTemplates() {
|
||||
try {
|
||||
java.net.URL loc = ConfigManager.class.getProtectionDomain()
|
||||
.getCodeSource().getLocation();
|
||||
if (loc == null) return;
|
||||
File jar = new File(loc.toURI());
|
||||
if (!jar.isFile()) return; // 开发态(class 目录)无 jar 可扫,跳过
|
||||
int released = 0, upgraded = 0, skipped = 0;
|
||||
try (java.util.jar.JarFile jf = new java.util.jar.JarFile(jar)) {
|
||||
java.util.Enumeration<java.util.jar.JarEntry> en = jf.entries();
|
||||
while (en.hasMoreElements()) {
|
||||
java.util.jar.JarEntry je = en.nextElement();
|
||||
if (je.isDirectory()) continue;
|
||||
String name = je.getName();
|
||||
if (name.startsWith("META-INF/")) continue;
|
||||
if (name.endsWith(".class")) continue;
|
||||
if (name.equals("plugin.yml")) continue; // 插件描述文件,不是配置模板
|
||||
try {
|
||||
byte[] tpl;
|
||||
try (InputStream in = jf.getInputStream(je)) {
|
||||
tpl = readAllBytes(in);
|
||||
}
|
||||
if (tpl == null || tpl.length == 0) continue;
|
||||
File target = new File(dataFolder, name);
|
||||
if (!target.exists()) {
|
||||
File parent = target.getParentFile();
|
||||
if (parent != null) parent.mkdirs();
|
||||
java.nio.file.Files.write(target.toPath(), tpl);
|
||||
released++;
|
||||
continue;
|
||||
}
|
||||
byte[] cur = java.nio.file.Files.readAllBytes(target.toPath());
|
||||
if (Arrays.equals(cur, tpl)) continue; // 与模板一致 → 安静
|
||||
String label = "通用模板:" + name;
|
||||
String lower = name.toLowerCase(Locale.ROOT);
|
||||
boolean ok;
|
||||
if (lower.endsWith(".md")) {
|
||||
ok = safeUpgradeDoc(target, readUtf8Lines(tpl), label);
|
||||
} else if (lower.endsWith(".json")) {
|
||||
ok = upgradeJsonMissing(target, tpl, label);
|
||||
} else if (lower.endsWith(".txt") || lower.endsWith(".yml")
|
||||
|| lower.endsWith(".yaml") || lower.endsWith(".cfg")
|
||||
|| lower.endsWith(".conf") || lower.endsWith(".properties")
|
||||
|| lower.endsWith(".csv")) {
|
||||
ok = upgradeLineList(target, readUtf8Lines(tpl), label);
|
||||
} else {
|
||||
logger.info("[" + label + "] 未识别的资源类型,跳过(不改动)");
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
if (ok) upgraded++;
|
||||
} catch (Throwable inner) {
|
||||
skipped++;
|
||||
logger.warning("[通用模板:" + name + "] 处理失败(已跳过,不影响启动): "
|
||||
+ inner.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (released > 0 || upgraded > 0) {
|
||||
logger.info("[通用模板] 扫描完成:首装释放 " + released + " 个文件,安全升级 "
|
||||
+ upgraded + " 个文件(只补缺失配置,其余原样保留)"
|
||||
+ (skipped > 0 ? ",跳过 " + skipped + " 个" : ""));
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
// 兜底:模板同步任何异常都不允许影响插件启动
|
||||
logger.warning("[通用模板] 同步异常(已跳过,不影响启动): " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/** 读 InputStream → 字节数组(jar 资源读取用) */
|
||||
private static byte[] readAllBytes(InputStream in) throws IOException {
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
byte[] buf = new byte[8192];
|
||||
int n;
|
||||
while ((n = in.read(buf)) > 0) bos.write(buf, 0, n);
|
||||
return bos.toByteArray();
|
||||
}
|
||||
|
||||
/** 字节数组 → 行列表(UTF-8,去 BOM,去 CR,去文件末空行) */
|
||||
private static List<String> readUtf8Lines(byte[] data) {
|
||||
String s = new String(data, StandardCharsets.UTF_8);
|
||||
if (!s.isEmpty() && s.charAt(0) == '\uFEFF') s = s.substring(1);
|
||||
List<String> L = new ArrayList<>();
|
||||
for (String line : s.split("\n", -1)) {
|
||||
if (line.endsWith("\r")) line = line.substring(0, line.length() - 1);
|
||||
L.add(line);
|
||||
}
|
||||
if (!L.isEmpty() && L.get(L.size() - 1).isEmpty()) L.remove(L.size() - 1);
|
||||
return L;
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON 配置缺键补入(35批):以原文件为基底,递归把模板里 target 缺失的键插进去 ——
|
||||
* 只插入、绝不覆盖已有值(数组/标量/类型不同一律不碰,用户改过的值原样保留);
|
||||
* 键名以 "#" 开头的视为模板示例说明(opwl.json「以下是模板文件」节),不回插。
|
||||
* 原文件 JSON 解析失败(用户加了注释/格式特殊)→ 跳过不动。
|
||||
* 动手前备份 .bak 并逐字节校验(失败=取消);写后重新解析校验,失败从 .bak 还原。
|
||||
*
|
||||
* @return true=本次执行了升级写入
|
||||
*/
|
||||
private boolean upgradeJsonMissing(File file, byte[] tplBytes, String label) {
|
||||
try {
|
||||
String tplText = new String(tplBytes, StandardCharsets.UTF_8);
|
||||
if (!tplText.isEmpty() && tplText.charAt(0) == '\uFEFF') tplText = tplText.substring(1);
|
||||
byte[] cur = java.nio.file.Files.readAllBytes(file.toPath());
|
||||
String oldText = new String(cur, StandardCharsets.UTF_8);
|
||||
if (!oldText.isEmpty() && oldText.charAt(0) == '\uFEFF') oldText = oldText.substring(1);
|
||||
com.google.gson.JsonElement tplEl;
|
||||
try {
|
||||
tplEl = com.google.gson.JsonParser.parseString(tplText);
|
||||
} catch (Exception e) {
|
||||
logger.warning("[" + label + "] 模板 JSON 解析失败,跳过");
|
||||
return false;
|
||||
}
|
||||
if (!tplEl.isJsonObject()) return false;
|
||||
com.google.gson.JsonElement oldEl;
|
||||
try {
|
||||
oldEl = com.google.gson.JsonParser.parseString(oldText);
|
||||
} catch (Exception e) {
|
||||
logger.warning("[" + label + "] 原文件 JSON 解析失败(可能含注释或被改坏),跳过不动");
|
||||
return false;
|
||||
}
|
||||
if (!oldEl.isJsonObject()) {
|
||||
logger.warning("[" + label + "] 原文件 JSON 顶层不是对象,跳过不动");
|
||||
return false;
|
||||
}
|
||||
com.google.gson.JsonObject root = oldEl.getAsJsonObject();
|
||||
List<String> added = new ArrayList<>();
|
||||
if (!mergeJsonMissing(root, tplEl.getAsJsonObject(), "", added)) {
|
||||
return false; // 无缺失键,幂等
|
||||
}
|
||||
// ① 备份原文件 → .bak 并逐字节校验(失败=取消,原文件不动)
|
||||
File bak = new File(file.getParentFile(), file.getName() + ".bak");
|
||||
java.nio.file.Files.copy(file.toPath(), bak.toPath(),
|
||||
java.nio.file.StandardCopyOption.REPLACE_EXISTING);
|
||||
boolean bakOk = bak.exists() && bak.length() == file.length();
|
||||
if (bakOk) {
|
||||
try {
|
||||
bakOk = Arrays.equals(cur, java.nio.file.Files.readAllBytes(bak.toPath()));
|
||||
} catch (Exception e) { bakOk = false; }
|
||||
}
|
||||
if (!bakOk) {
|
||||
bak.delete();
|
||||
logger.warning("[" + label + "] ★升级取消:备份 .bak 校验不通过,原文件未改动");
|
||||
return false;
|
||||
}
|
||||
// ② 写回(pretty + 不转义 HTML,与 OpWhiteListManager 写回同款格式),写后复解析校验
|
||||
com.google.gson.Gson g = new com.google.gson.GsonBuilder()
|
||||
.setPrettyPrinting().disableHtmlEscaping().create();
|
||||
try {
|
||||
byte[] out = g.toJson(root).getBytes(StandardCharsets.UTF_8);
|
||||
java.nio.file.Files.write(file.toPath(), out);
|
||||
com.google.gson.JsonParser.parseString(
|
||||
new String(out, StandardCharsets.UTF_8)).getAsJsonObject();
|
||||
} catch (Exception e) {
|
||||
try {
|
||||
java.nio.file.Files.copy(bak.toPath(), file.toPath(),
|
||||
java.nio.file.StandardCopyOption.REPLACE_EXISTING);
|
||||
logger.warning("[" + label + "] ★升级失败:写入/校验出错,已从 "
|
||||
+ bak.getName() + " 还原原文件");
|
||||
} catch (Exception e2) {
|
||||
logger.severe("[" + label + "] ★升级失败且还原失败!完整备份仍在: "
|
||||
+ bak.getAbsolutePath());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
logger.info("[" + label + "] JSON 配置安全交换完成:补入 " + added.size()
|
||||
+ " 个缺失键 " + added + ",已有配置值与其它内容原样保留;备份: " + bak.getName());
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
logger.warning("[" + label + "] JSON 配置升级异常(已跳过,保持原文件): " + e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 递归把模板里 target 缺失的键补进去(只插入不覆盖;数组/标量不递归合并;# 说明键跳过) */
|
||||
private boolean mergeJsonMissing(com.google.gson.JsonObject target,
|
||||
com.google.gson.JsonObject tpl, String path, List<String> added) {
|
||||
boolean changed = false;
|
||||
for (Map.Entry<String, com.google.gson.JsonElement> en : tpl.entrySet()) {
|
||||
String k = en.getKey();
|
||||
if (k.startsWith("#")) continue; // 模板示例说明键,不算配置,不回插
|
||||
com.google.gson.JsonElement have = target.get(k);
|
||||
if (have == null || have.isJsonNull()) {
|
||||
target.add(k, en.getValue().deepCopy());
|
||||
added.add(path + k);
|
||||
changed = true;
|
||||
} else if (have.isJsonObject() && en.getValue().isJsonObject()) {
|
||||
changed |= mergeJsonMissing(have.getAsJsonObject(),
|
||||
en.getValue().getAsJsonObject(), path + k + ".", added);
|
||||
}
|
||||
// 已存在的键(含数组、标量、类型不同)一律不碰 → 用户值与自定义内容原样保留
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 纯文本行列表缺行补入(35批,illegal_domains.txt 等):原文件没有的模板数据行
|
||||
* (非空非注释)追加到文件末尾;已有行、用户自加行原样保留。
|
||||
* 备份/校验/写失败还原与 safeUpgrade 同一套安全约定。
|
||||
*
|
||||
* @return true=本次执行了升级写入
|
||||
*/
|
||||
private boolean upgradeLineList(File file, List<String> tpl, String label) {
|
||||
try {
|
||||
List<String> orig = readUtf8Lines(java.nio.file.Files.readAllBytes(file.toPath()));
|
||||
Set<String> have = new HashSet<>();
|
||||
for (String raw : orig) {
|
||||
String t = raw.trim();
|
||||
if (!t.isEmpty()) have.add(t);
|
||||
}
|
||||
List<String> missing = new ArrayList<>();
|
||||
for (String raw : tpl) {
|
||||
String t = raw.trim();
|
||||
if (t.isEmpty() || t.startsWith("#")) continue;
|
||||
if (!have.contains(t)) missing.add(raw);
|
||||
}
|
||||
if (missing.isEmpty()) return false; // 幂等
|
||||
File bak = new File(file.getParentFile(), file.getName() + ".bak");
|
||||
java.nio.file.Files.copy(file.toPath(), bak.toPath(),
|
||||
java.nio.file.StandardCopyOption.REPLACE_EXISTING);
|
||||
boolean bakOk = bak.exists() && bak.length() == file.length();
|
||||
if (bakOk) {
|
||||
try {
|
||||
bakOk = Arrays.equals(
|
||||
java.nio.file.Files.readAllBytes(file.toPath()),
|
||||
java.nio.file.Files.readAllBytes(bak.toPath()));
|
||||
} catch (Exception e) { bakOk = false; }
|
||||
}
|
||||
if (!bakOk) {
|
||||
bak.delete();
|
||||
logger.warning("[" + label + "] ★升级取消:备份 .bak 校验不通过,原文件未改动");
|
||||
return false;
|
||||
}
|
||||
List<String> out = new ArrayList<>(orig);
|
||||
out.add("");
|
||||
out.add("# ===== 以下为模板补充行(通用模板同步自动补入,可自行删改) =====");
|
||||
out.addAll(missing);
|
||||
if (!writeLines(file, out)) {
|
||||
try {
|
||||
java.nio.file.Files.copy(bak.toPath(), file.toPath(),
|
||||
java.nio.file.StandardCopyOption.REPLACE_EXISTING);
|
||||
logger.warning("[" + label + "] ★升级失败:写入出错,已从 "
|
||||
+ bak.getName() + " 还原原文件");
|
||||
} catch (Exception e2) {
|
||||
logger.severe("[" + label + "] ★升级失败且还原失败!完整备份仍在: "
|
||||
+ bak.getAbsolutePath());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
logger.info("[" + label + "] 行列表安全交换完成:补入 " + missing.size()
|
||||
+ " 行,已有内容原样保留;备份: " + bak.getName());
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
logger.warning("[" + label + "] 行列表升级异常(已跳过,保持原文件): " + e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private double parseDouble(String s, double def) { try { return Double.parseDouble(s.trim()); } catch (Exception e) { return def; } }
|
||||
private int parseInt(String s, int def) { try { return Integer.parseInt(s.trim()); } catch (Exception e) { return def; } }
|
||||
|
||||
|
||||
@@ -420,6 +420,11 @@ public class Main extends JavaPlugin
|
||||
|
||||
// ===== 1. 基础配置 =====
|
||||
config = new ConfigManager(getDataFolder());
|
||||
// ★ 35批:通用模板同步——启动即扫描 jar 全部 resources 模板:数据目录缺文件=安全释放,
|
||||
// 缺模板里的配置值=先备份 .bak 再只插缺失项(说明文档与用户改过的值原样保留)。
|
||||
// 必须在任何 Manager 读配置之前(illegal_domains.txt / chest/cfg.json /
|
||||
// OP白名单/opwl.json / 各 md 配置都由它先对齐)。
|
||||
config.syncAllResourceTemplates();
|
||||
config.loadMessages();
|
||||
config.loadSmtp();
|
||||
config.loadSettings();
|
||||
@@ -6074,6 +6079,8 @@ public class Main extends JavaPlugin
|
||||
// ★ 答对了 → 释放所有缓存消息 + 当前消息
|
||||
chatFilter.broadcastCachedMessages(p.getName());
|
||||
Bukkit.broadcastMessage(p.getDisplayName() + ": " + msg);
|
||||
// 35批:机翻无条件执行——验证通过手动广播的这条也照常追加机翻
|
||||
tryScheduleChatTranslation(p, msg);
|
||||
p.sendMessage("§a§l[验证码] §a验证通过!");
|
||||
e.setCancelled(true);
|
||||
return;
|
||||
@@ -6192,11 +6199,19 @@ public class Main extends JavaPlugin
|
||||
}
|
||||
|
||||
// ===== 聊天过滤 - 开关检查 =====
|
||||
if (!chatFilter.isEnabled()) return;
|
||||
if (!chatFilter.isEnabled()) {
|
||||
// 35批:机翻无条件执行——过滤关闭也照常追加英文机翻(机翻是展示功能,不随开关走)
|
||||
tryScheduleChatTranslation(p, msg);
|
||||
return;
|
||||
}
|
||||
|
||||
// ===== 白名单玩家 =====
|
||||
if (chatFilter.isPlayerWhitelisted(
|
||||
p.getName())) return;
|
||||
p.getName())) {
|
||||
// 35批:机翻无条件执行——加白玩家不过滤,但英文机翻照常(机翻没有任何豁免)
|
||||
tryScheduleChatTranslation(p, msg);
|
||||
return;
|
||||
}
|
||||
|
||||
// ===== 禁言检查 =====
|
||||
if (chatFilter.isMuted(p.getName())) {
|
||||
@@ -6273,6 +6288,24 @@ public class Main extends JavaPlugin
|
||||
}
|
||||
}
|
||||
|
||||
// ===== IPv4/IPv6 地址检测(35批修订口径:加白 IP / 加白玩家不受影响,
|
||||
// 其它玩家消息含 IPv4/IPv6 一律无条件拦截)=====
|
||||
// 加白玩家:上面「白名单玩家」已早退,根本走不到这里;
|
||||
// 加白 IP:复用 chat.txt「白名单」节条目(isWhitelisted,支持完整 IP 与 *.x.x.x 后缀),
|
||||
// 消息里的 IP 全部加白才放行,出现任何一个未加白的 IP 即拦。
|
||||
if (chatFilter != null && chatFilter.containsNonWhitelistedIp(msg)) {
|
||||
e.setCancelled(true);
|
||||
p.sendMessage("§c§l[聊天过滤] §7消息包含未加白的 IP 地址,已被拦截");
|
||||
chatFilter.incrementViolation(p.getName());
|
||||
int ipCount = chatFilter.getViolationCount(p.getName());
|
||||
p.sendMessage(chatFilter.msg(
|
||||
"chat_url_violation",
|
||||
"count", String.valueOf(ipCount)));
|
||||
chatFilter.applyPunishment(p, ipCount,
|
||||
"发送IP地址", "系统");
|
||||
return;
|
||||
}
|
||||
|
||||
// ===== URL检测 =====
|
||||
List<String> urls =
|
||||
chatFilter.extractUrls(msg);
|
||||
@@ -6373,9 +6406,7 @@ public class Main extends JavaPlugin
|
||||
|
||||
// ==================== 聊天过滤辅助方法 ====================
|
||||
|
||||
// ===== 英文聊天机翻(28批新增) =====
|
||||
/** 同一玩家机翻冷却(毫秒),防刷屏触发大量 API 请求 */
|
||||
private final java.util.Map<String, Long> chatTransCd = new java.util.concurrent.ConcurrentHashMap<>();
|
||||
// ===== 英文聊天机翻(28批新增,35批去豁免) =====
|
||||
/** 机翻专用线程池(HTTP 阻塞最多 ~9s,不能占用 ForkJoinPool.commonPool) */
|
||||
private final java.util.concurrent.ExecutorService chatTransPool =
|
||||
java.util.concurrent.Executors.newFixedThreadPool(2, r -> {
|
||||
@@ -6385,12 +6416,14 @@ public class Main extends JavaPlugin
|
||||
});
|
||||
|
||||
/**
|
||||
* 英文聊天机翻调度(28批,用户定稿口径):
|
||||
* 英文聊天机翻调度(28批用户定稿口径,35批修正「机翻无条件执行,没有豁免」):
|
||||
* - 所有消息一律不拦截、原样放行(交 MC 自行处理),本方法只做「追加双语展示」;
|
||||
* - 仅「整条消息不含任何中文字符」的英文消息(如 what are you doing?)才机翻,
|
||||
* - 触发口径不变:整条消息不含任何中文字符(如 what are you doing?)才机翻,
|
||||
* 原始消息由 MC 正常广播之后,再追加一条「[机翻] 原文 → 译文」;
|
||||
* - 中文消息或中文夹杂英文(如「你需要用water来免伤」)一律原样输出、不做机翻
|
||||
* 中文/中英夹杂(如「你需要用water来免伤」)一律原样输出、不做机翻
|
||||
* —— 脱离第三方模型无法判定英文单词在语境中的真实含义,强行翻反而出错。
|
||||
* - ★ 35批:无条件执行、没有任何豁免 —— 10秒/人冷却与 200 字符上限已删除,
|
||||
* 每条英文消息都触发机翻(白名单玩家/过滤关闭/验证通过等早退路径也补了调度)。
|
||||
* 异步:HTTP 翻译在线程池执行(每源3s超时,YOUDAO→JIANXIN→GOOGLE 降级),
|
||||
* 结果回主线程广播;任何异常静默吞掉,绝不影响聊天主流程。
|
||||
*/
|
||||
@@ -6398,26 +6431,21 @@ public class Main extends JavaPlugin
|
||||
try {
|
||||
if (p == null || msg == null) return;
|
||||
String text = msg.trim();
|
||||
if (text.isEmpty() || text.length() > 200) return;
|
||||
// 整条消息必须「不含任何中文字符」才机翻(含中文=原样输出)
|
||||
if (text.isEmpty()) return;
|
||||
// 触发口径:整条消息必须「不含任何中文字符」才机翻(含中文=原样输出)
|
||||
boolean hasChinese = false;
|
||||
for (int i = 0; i < text.length(); i++) {
|
||||
char c = text.charAt(i);
|
||||
if (c >= '\u4e00' && c <= '\u9fff') { hasChinese = true; break; }
|
||||
}
|
||||
if (hasChinese) return;
|
||||
// 至少要有一个英文字母(纯数字/符号无翻译意义)
|
||||
// 触发口径:至少一个英文字母(纯数字/符号不算英文消息)
|
||||
boolean hasLetter = false;
|
||||
for (int i = 0; i < text.length(); i++) {
|
||||
char c = text.charAt(i);
|
||||
if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) { hasLetter = true; break; }
|
||||
}
|
||||
if (!hasLetter) return;
|
||||
// 冷却防刷(10秒/人)
|
||||
long now = System.currentTimeMillis();
|
||||
Long last = chatTransCd.get(p.getName());
|
||||
if (last != null && now - last < 10_000L) return;
|
||||
chatTransCd.put(p.getName(), now);
|
||||
|
||||
final String original = text;
|
||||
final String sender = p.getName();
|
||||
@@ -6443,7 +6471,13 @@ public class Main extends JavaPlugin
|
||||
if (content == null || content.isEmpty()) return false;
|
||||
if (chatFilter == null || !chatFilter.isEnabled()) return false;
|
||||
if (chatFilter.isPlayerWhitelisted(p.getName())) return false;
|
||||
|
||||
// 35批修订:私信/告示牌/书本内容同样拦 IP —— 加白 IP 放行,其余 IPv4/IPv6 无条件拦
|
||||
// (加白玩家已在上一行豁免)
|
||||
if (chatFilter.containsNonWhitelistedIp(content)) {
|
||||
p.sendMessage("§c§l[聊天过滤] §7内容包含未加白的 IP 地址,已被拦截");
|
||||
return true;
|
||||
}
|
||||
|
||||
List<String> urls = chatFilter.extractUrls(content);
|
||||
if (urls.isEmpty()) return false;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user