diff --git a/parser/pom.xml b/parser/pom.xml
index bc9b0f8..ffbb628 100644
--- a/parser/pom.xml
+++ b/parser/pom.xml
@@ -114,10 +114,15 @@
${graalpy.version}
- org.graalvm.polyglot
+ org.graalvm.python
python
${graalpy.version}
- pom
+ runtime
+
+
+ org.graalvm.python
+ python-embedding
+ ${graalpy.version}
diff --git a/parser/src/main/java/cn/qaiu/parser/custompy/PyContextPool.java b/parser/src/main/java/cn/qaiu/parser/custompy/PyContextPool.java
new file mode 100644
index 0000000..e1bf7fa
--- /dev/null
+++ b/parser/src/main/java/cn/qaiu/parser/custompy/PyContextPool.java
@@ -0,0 +1,459 @@
+package cn.qaiu.parser.custompy;
+
+import org.graalvm.polyglot.Context;
+import org.graalvm.polyglot.Engine;
+import org.graalvm.polyglot.HostAccess;
+import org.graalvm.polyglot.io.IOAccess;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.concurrent.*;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * GraalPy Context 池化管理器
+ * 提供共享的 Engine 实例和 Context 池化支持
+ *
+ *
特性:
+ *
+ * - 共享单个 Engine 实例,减少内存占用和启动时间
+ * - Context 对象池,避免重复创建和销毁的开销
+ * - 支持安全的沙箱配置
+ * - 线程安全的池化管理
+ * - 支持优雅关闭和资源清理
+ *
+ *
+ * @author QAIU
+ */
+public class PyContextPool {
+
+ private static final Logger log = LoggerFactory.getLogger(PyContextPool.class);
+
+ // 池化配置
+ private static final int INITIAL_POOL_SIZE = 2;
+ private static final int MAX_POOL_SIZE = 10;
+ private static final long CONTEXT_TIMEOUT_MS = 30000; // 30秒获取超时
+ private static final long CONTEXT_MAX_AGE_MS = 300000; // 5分钟最大使用时间
+
+ // 单例实例
+ private static volatile PyContextPool instance;
+ private static final Object LOCK = new Object();
+
+ // 共享的GraalPy引擎
+ private final Engine sharedEngine;
+
+ // Context 池
+ private final BlockingQueue contextPool;
+
+ // 已创建的Context数量
+ private final AtomicInteger createdCount = new AtomicInteger(0);
+
+ // 是否已关闭
+ private final AtomicBoolean closed = new AtomicBoolean(false);
+
+ // 定期清理过期Context的调度器
+ private final ScheduledExecutorService cleanupScheduler;
+
+ // Python执行专用线程池
+ private final ExecutorService pythonExecutor;
+
+ // 超时调度器
+ private final ScheduledExecutorService timeoutScheduler;
+
+ /**
+ * 池化的Context包装器
+ */
+ public static class PooledContext implements AutoCloseable {
+ private final Context context;
+ private final long createdTime;
+ private final PyContextPool pool;
+ private volatile boolean inUse = false;
+ private volatile long lastUsedTime;
+
+ private PooledContext(Context context, PyContextPool pool) {
+ this.context = context;
+ this.pool = pool;
+ this.createdTime = System.currentTimeMillis();
+ this.lastUsedTime = createdTime;
+ }
+
+ /**
+ * 获取底层Context
+ */
+ public Context getContext() {
+ return context;
+ }
+
+ /**
+ * 检查是否过期
+ */
+ public boolean isExpired() {
+ return System.currentTimeMillis() - createdTime > CONTEXT_MAX_AGE_MS;
+ }
+
+ /**
+ * 归还到池中或关闭
+ */
+ @Override
+ public void close() {
+ pool.release(this);
+ }
+
+ /**
+ * 强制关闭Context
+ */
+ void forceClose() {
+ try {
+ context.close(true);
+ } catch (Exception e) {
+ log.warn("关闭Context失败: {}", e.getMessage());
+ }
+ }
+
+ /**
+ * 重置Context状态(清除绑定等)
+ */
+ boolean reset() {
+ try {
+ // 由于GraalPy的Context不能很好地重置状态,
+ // 简单场景下我们选择创建新的Context
+ // 但对于短生命周期的执行,可以尝试继续使用
+ lastUsedTime = System.currentTimeMillis();
+ return !isExpired();
+ } catch (Exception e) {
+ log.warn("重置Context失败: {}", e.getMessage());
+ return false;
+ }
+ }
+ }
+
+ /**
+ * 私有构造函数
+ */
+ private PyContextPool() {
+ log.info("初始化GraalPy Context池...");
+
+ // 创建共享Engine
+ this.sharedEngine = Engine.newBuilder()
+ .option("engine.WarnInterpreterOnly", "false")
+ .build();
+
+ // 创建Context池
+ this.contextPool = new LinkedBlockingQueue<>(MAX_POOL_SIZE);
+
+ // 创建Python执行专用线程池
+ this.pythonExecutor = Executors.newCachedThreadPool(r -> {
+ Thread thread = new Thread(r);
+ thread.setName("py-context-pool-worker-" + System.currentTimeMillis());
+ thread.setDaemon(true);
+ return thread;
+ });
+
+ // 创建超时调度器
+ this.timeoutScheduler = Executors.newScheduledThreadPool(2, r -> {
+ Thread thread = new Thread(r);
+ thread.setName("py-context-timeout-" + System.currentTimeMillis());
+ thread.setDaemon(true);
+ return thread;
+ });
+
+ // 创建清理调度器
+ this.cleanupScheduler = Executors.newSingleThreadScheduledExecutor(r -> {
+ Thread thread = new Thread(r);
+ thread.setName("py-context-cleanup");
+ thread.setDaemon(true);
+ return thread;
+ });
+
+ // 预热:初始化一些Context
+ warmup();
+
+ // 定期清理过期的Context
+ cleanupScheduler.scheduleWithFixedDelay(this::cleanup, 60, 60, TimeUnit.SECONDS);
+
+ log.info("GraalPy Context池初始化完成,初始大小: {}", INITIAL_POOL_SIZE);
+ }
+
+ /**
+ * 获取单例实例
+ */
+ public static PyContextPool getInstance() {
+ if (instance == null) {
+ synchronized (LOCK) {
+ if (instance == null) {
+ instance = new PyContextPool();
+ }
+ }
+ }
+ return instance;
+ }
+
+ /**
+ * 获取共享Engine
+ */
+ public Engine getSharedEngine() {
+ return sharedEngine;
+ }
+
+ /**
+ * 获取Python执行线程池
+ */
+ public ExecutorService getPythonExecutor() {
+ return pythonExecutor;
+ }
+
+ /**
+ * 获取超时调度器
+ */
+ public ScheduledExecutorService getTimeoutScheduler() {
+ return timeoutScheduler;
+ }
+
+ /**
+ * 预热Context池
+ */
+ private void warmup() {
+ for (int i = 0; i < INITIAL_POOL_SIZE; i++) {
+ try {
+ PooledContext pc = createPooledContext();
+ if (!contextPool.offer(pc)) {
+ pc.forceClose();
+ }
+ } catch (Exception e) {
+ log.warn("预热Context失败: {}", e.getMessage());
+ }
+ }
+ }
+
+ /**
+ * 创建新的池化Context
+ */
+ private PooledContext createPooledContext() {
+ if (closed.get()) {
+ throw new IllegalStateException("Context池已关闭");
+ }
+
+ Context context = Context.newBuilder("python")
+ .engine(sharedEngine)
+ .allowHostAccess(HostAccess.newBuilder(HostAccess.EXPLICIT)
+ .allowArrayAccess(true)
+ .allowListAccess(true)
+ .allowMapAccess(true)
+ .allowIterableAccess(true)
+ .allowIteratorAccess(true)
+ .build())
+ .allowHostClassLookup(className -> false)
+ .allowExperimentalOptions(true)
+ .allowCreateThread(true)
+ .allowNativeAccess(false)
+ .allowCreateProcess(false)
+ .allowIO(IOAccess.newBuilder()
+ .allowHostFileAccess(false)
+ .allowHostSocketAccess(false)
+ .build())
+ .option("python.PythonHome", "")
+ .option("python.ForceImportSite", "false")
+ .build();
+
+ createdCount.incrementAndGet();
+ log.debug("创建新的GraalPy Context,当前总数: {}", createdCount.get());
+
+ return new PooledContext(context, this);
+ }
+
+ /**
+ * 从池中获取Context
+ *
+ * @return 池化的Context,用完后需要调用close()归还
+ * @throws InterruptedException 如果等待被中断
+ * @throws TimeoutException 如果超时未获取到
+ */
+ public PooledContext acquire() throws InterruptedException, TimeoutException {
+ if (closed.get()) {
+ throw new IllegalStateException("Context池已关闭");
+ }
+
+ // 尝试从池中获取
+ PooledContext pc = contextPool.poll();
+
+ if (pc != null) {
+ if (!pc.isExpired() && pc.reset()) {
+ pc.inUse = true;
+ log.debug("从池中获取Context,池剩余: {}", contextPool.size());
+ return pc;
+ } else {
+ // Context已过期,关闭它
+ pc.forceClose();
+ createdCount.decrementAndGet();
+ }
+ }
+
+ // 池中没有可用的,检查是否可以创建新的
+ if (createdCount.get() < MAX_POOL_SIZE) {
+ try {
+ pc = createPooledContext();
+ pc.inUse = true;
+ return pc;
+ } catch (Exception e) {
+ log.error("创建新Context失败: {}", e.getMessage());
+ throw new RuntimeException("无法创建GraalPy Context", e);
+ }
+ }
+
+ // 已达最大数量,等待归还
+ pc = contextPool.poll(CONTEXT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ if (pc == null) {
+ throw new TimeoutException("获取GraalPy Context超时");
+ }
+
+ if (!pc.isExpired() && pc.reset()) {
+ pc.inUse = true;
+ return pc;
+ } else {
+ pc.forceClose();
+ createdCount.decrementAndGet();
+ // 递归重试
+ return acquire();
+ }
+ }
+
+ /**
+ * 创建一个新的非池化Context(用于需要独立生命周期的场景)
+ * 调用者负责管理其生命周期
+ */
+ public Context createFreshContext() {
+ return Context.newBuilder("python")
+ .engine(sharedEngine)
+ .allowHostAccess(HostAccess.newBuilder(HostAccess.EXPLICIT)
+ .allowArrayAccess(true)
+ .allowListAccess(true)
+ .allowMapAccess(true)
+ .allowIterableAccess(true)
+ .allowIteratorAccess(true)
+ .build())
+ .allowHostClassLookup(className -> false)
+ .allowExperimentalOptions(true)
+ .allowCreateThread(true)
+ .allowNativeAccess(false)
+ .allowCreateProcess(false)
+ .allowIO(IOAccess.newBuilder()
+ .allowHostFileAccess(false)
+ .allowHostSocketAccess(false)
+ .build())
+ .option("python.PythonHome", "")
+ .option("python.ForceImportSite", "false")
+ .build();
+ }
+
+ /**
+ * 归还Context到池中
+ */
+ private void release(PooledContext pc) {
+ if (pc == null) return;
+
+ pc.inUse = false;
+
+ if (closed.get() || pc.isExpired()) {
+ // 池已关闭或Context已过期,直接销毁
+ pc.forceClose();
+ createdCount.decrementAndGet();
+ log.debug("Context已过期或池已关闭,销毁Context");
+ } else if (!contextPool.offer(pc)) {
+ // 池已满,销毁Context
+ pc.forceClose();
+ createdCount.decrementAndGet();
+ log.debug("池已满,销毁多余Context");
+ } else {
+ log.debug("归还Context到池,池当前大小: {}", contextPool.size());
+ }
+ }
+
+ /**
+ * 清理过期的Context
+ */
+ private void cleanup() {
+ if (closed.get()) return;
+
+ int removed = 0;
+ PooledContext pc;
+
+ while ((pc = contextPool.poll()) != null) {
+ if (pc.isExpired() || closed.get()) {
+ pc.forceClose();
+ createdCount.decrementAndGet();
+ removed++;
+ } else {
+ // 还没过期,放回池中
+ if (!contextPool.offer(pc)) {
+ pc.forceClose();
+ createdCount.decrementAndGet();
+ removed++;
+ }
+ break;
+ }
+ }
+
+ if (removed > 0) {
+ log.info("清理了 {} 个过期的Context,当前池大小: {}", removed, contextPool.size());
+ }
+ }
+
+ /**
+ * 获取池状态信息
+ */
+ public String getStatus() {
+ return String.format("PyContextPool[total=%d, available=%d, maxSize=%d]",
+ createdCount.get(), contextPool.size(), MAX_POOL_SIZE);
+ }
+
+ /**
+ * 获取池中可用的Context数量
+ */
+ public int getAvailableCount() {
+ return contextPool.size();
+ }
+
+ /**
+ * 获取已创建的Context总数
+ */
+ public int getCreatedCount() {
+ return createdCount.get();
+ }
+
+ /**
+ * 关闭Context池
+ */
+ public void shutdown() {
+ if (closed.compareAndSet(false, true)) {
+ log.info("关闭GraalPy Context池...");
+
+ // 停止清理调度器
+ cleanupScheduler.shutdownNow();
+ timeoutScheduler.shutdownNow();
+ pythonExecutor.shutdownNow();
+
+ // 关闭所有池中的Context
+ PooledContext pc;
+ while ((pc = contextPool.poll()) != null) {
+ pc.forceClose();
+ }
+
+ // 关闭共享Engine
+ try {
+ sharedEngine.close(true);
+ } catch (Exception e) {
+ log.warn("关闭共享Engine失败: {}", e.getMessage());
+ }
+
+ log.info("GraalPy Context池已关闭");
+ }
+ }
+
+ /**
+ * 检查池是否已关闭
+ */
+ public boolean isClosed() {
+ return closed.get();
+ }
+}
diff --git a/parser/src/main/java/cn/qaiu/parser/custompy/PyParserExecutor.java b/parser/src/main/java/cn/qaiu/parser/custompy/PyParserExecutor.java
index 8d1ae89..3997484 100644
--- a/parser/src/main/java/cn/qaiu/parser/custompy/PyParserExecutor.java
+++ b/parser/src/main/java/cn/qaiu/parser/custompy/PyParserExecutor.java
@@ -9,21 +9,18 @@ import io.vertx.core.Future;
import io.vertx.core.WorkerExecutor;
import io.vertx.core.json.JsonObject;
import org.graalvm.polyglot.Context;
-import org.graalvm.polyglot.Engine;
-import org.graalvm.polyglot.HostAccess;
import org.graalvm.polyglot.Value;
-import org.graalvm.polyglot.io.IOAccess;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
-import java.util.Map;
/**
* Python解析器执行器
* 使用GraalPy执行Python解析器脚本
* 实现IPanTool接口,执行Python解析器逻辑
+ * 使用 PyContextPool 进行 Engine 池化管理
*
* @author QAIU
*/
@@ -34,10 +31,8 @@ public class PyParserExecutor implements IPanTool {
private static final WorkerExecutor EXECUTOR = WebClientVertxInit.get()
.createSharedWorkerExecutor("py-parser-executor", 32);
- // 共享的GraalPy引擎,提高性能
- private static final Engine SHARED_ENGINE = Engine.newBuilder()
- .option("engine.WarnInterpreterOnly", "false")
- .build();
+ // Context池实例
+ private static final PyContextPool CONTEXT_POOL = PyContextPool.getInstance();
private final CustomParserConfig config;
private final ShareLinkInfo shareLinkInfo;
@@ -71,48 +66,12 @@ public class PyParserExecutor implements IPanTool {
return shareLinkInfo;
}
- /**
- * 创建安全的GraalPy Context
- * 配置沙箱选项,禁用危险功能
- */
- private Context createContext() {
- return Context.newBuilder("python")
- .engine(SHARED_ENGINE)
- // 允许访问带有@HostAccess.Export注解的Java方法
- .allowHostAccess(HostAccess.newBuilder(HostAccess.EXPLICIT)
- .allowArrayAccess(true)
- .allowListAccess(true)
- .allowMapAccess(true)
- .allowIterableAccess(true)
- .allowIteratorAccess(true)
- .build())
- // 禁止访问任意Java类
- .allowHostClassLookup(className -> false)
- // 允许实验性选项
- .allowExperimentalOptions(true)
- // 允许创建线程(某些Python库需要)
- .allowCreateThread(true)
- // 禁用原生访问
- .allowNativeAccess(false)
- // 禁止创建子进程
- .allowCreateProcess(false)
- // 允许虚拟文件系统访问(用于import等)
- .allowIO(IOAccess.newBuilder()
- .allowHostFileAccess(false)
- .allowHostSocketAccess(false)
- .build())
- // GraalPy特定选项
- .option("python.PythonHome", "")
- .option("python.ForceImportSite", "false")
- .build();
- }
-
@Override
public Future parse() {
pyLogger.info("开始执行Python解析器: {}", config.getType());
return EXECUTOR.executeBlocking(() -> {
- try (Context context = createContext()) {
+ try (Context context = CONTEXT_POOL.createFreshContext()) {
// 注入Java对象到Python环境
Value bindings = context.getBindings("python");
bindings.putMember("http", httpClient);
@@ -152,7 +111,7 @@ public class PyParserExecutor implements IPanTool {
pyLogger.info("开始执行Python文件列表解析: {}", config.getType());
return EXECUTOR.executeBlocking(() -> {
- try (Context context = createContext()) {
+ try (Context context = CONTEXT_POOL.createFreshContext()) {
// 注入Java对象到Python环境
Value bindings = context.getBindings("python");
bindings.putMember("http", httpClient);
@@ -186,7 +145,7 @@ public class PyParserExecutor implements IPanTool {
pyLogger.info("开始执行Python按ID解析: {}", config.getType());
return EXECUTOR.executeBlocking(() -> {
- try (Context context = createContext()) {
+ try (Context context = CONTEXT_POOL.createFreshContext()) {
// 注入Java对象到Python环境
Value bindings = context.getBindings("python");
bindings.putMember("http", httpClient);
diff --git a/parser/src/main/java/cn/qaiu/parser/custompy/PyPlaygroundExecutor.java b/parser/src/main/java/cn/qaiu/parser/custompy/PyPlaygroundExecutor.java
index 98cbe3f..4b46d6b 100644
--- a/parser/src/main/java/cn/qaiu/parser/custompy/PyPlaygroundExecutor.java
+++ b/parser/src/main/java/cn/qaiu/parser/custompy/PyPlaygroundExecutor.java
@@ -6,10 +6,7 @@ import io.vertx.core.Future;
import io.vertx.core.Promise;
import io.vertx.core.json.JsonObject;
import org.graalvm.polyglot.Context;
-import org.graalvm.polyglot.Engine;
-import org.graalvm.polyglot.HostAccess;
import org.graalvm.polyglot.Value;
-import org.graalvm.polyglot.io.IOAccess;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -21,6 +18,7 @@ import java.util.concurrent.*;
* Python演练场执行器
* 用于临时执行Python代码,不注册到解析器注册表
* 使用独立线程池避免Vert.x BlockedThreadChecker警告
+ * 使用 PyContextPool 进行 Engine 和 Context 池化管理
*
* @author QAIU
*/
@@ -31,26 +29,8 @@ public class PyPlaygroundExecutor {
// Python执行超时时间(秒)
private static final long EXECUTION_TIMEOUT_SECONDS = 30;
- // 共享的GraalPy引擎
- private static final Engine SHARED_ENGINE = Engine.newBuilder()
- .option("engine.WarnInterpreterOnly", "false")
- .build();
-
- // 使用独立的线程池
- private static final ExecutorService INDEPENDENT_EXECUTOR = Executors.newCachedThreadPool(r -> {
- Thread thread = new Thread(r);
- thread.setName("py-playground-independent-" + System.currentTimeMillis());
- thread.setDaemon(true);
- return thread;
- });
-
- // 超时调度线程池
- private static final ScheduledExecutorService TIMEOUT_SCHEDULER = Executors.newScheduledThreadPool(2, r -> {
- Thread thread = new Thread(r);
- thread.setName("py-playground-timeout-scheduler-" + System.currentTimeMillis());
- thread.setDaemon(true);
- return thread;
- });
+ // Context池实例
+ private static final PyContextPool CONTEXT_POOL = PyContextPool.getInstance();
private final ShareLinkInfo shareLinkInfo;
private final String pyCode;
@@ -81,33 +61,6 @@ public class PyPlaygroundExecutor {
this.cryptoUtils = new PyCryptoUtils();
}
- /**
- * 创建安全的GraalPy Context
- */
- private Context createContext() {
- return Context.newBuilder("python")
- .engine(SHARED_ENGINE)
- .allowHostAccess(HostAccess.newBuilder(HostAccess.EXPLICIT)
- .allowArrayAccess(true)
- .allowListAccess(true)
- .allowMapAccess(true)
- .allowIterableAccess(true)
- .allowIteratorAccess(true)
- .build())
- .allowHostClassLookup(className -> false)
- .allowExperimentalOptions(true)
- .allowCreateThread(true)
- .allowNativeAccess(false)
- .allowCreateProcess(false)
- .allowIO(IOAccess.newBuilder()
- .allowHostFileAccess(false)
- .allowHostSocketAccess(false)
- .build())
- .option("python.PythonHome", "")
- .option("python.ForceImportSite", "false")
- .build();
- }
-
/**
* 执行parse方法(异步,带超时控制)
*/
@@ -117,7 +70,8 @@ public class PyPlaygroundExecutor {
CompletableFuture executionFuture = CompletableFuture.supplyAsync(() -> {
playgroundLogger.infoJava("开始执行parse方法");
- try (Context context = createContext()) {
+ // 使用池化的Context(每次执行创建新的Context以保证状态隔离)
+ try (Context context = CONTEXT_POOL.createFreshContext()) {
// 注入Java对象到Python环境
Value bindings = context.getBindings("python");
bindings.putMember("http", httpClient);
@@ -153,10 +107,10 @@ public class PyPlaygroundExecutor {
playgroundLogger.errorJava("执行parse方法失败: " + e.getMessage(), e);
throw new RuntimeException(e);
}
- }, INDEPENDENT_EXECUTOR);
+ }, CONTEXT_POOL.getPythonExecutor());
// 创建超时任务
- ScheduledFuture> timeoutTask = TIMEOUT_SCHEDULER.schedule(() -> {
+ ScheduledFuture> timeoutTask = CONTEXT_POOL.getTimeoutScheduler().schedule(() -> {
if (!executionFuture.isDone()) {
executionFuture.cancel(true);
playgroundLogger.errorJava("执行超时,已强制中断");
@@ -195,7 +149,7 @@ public class PyPlaygroundExecutor {
CompletableFuture> executionFuture = CompletableFuture.supplyAsync(() -> {
playgroundLogger.infoJava("开始执行parse_file_list方法");
- try (Context context = createContext()) {
+ try (Context context = CONTEXT_POOL.createFreshContext()) {
Value bindings = context.getBindings("python");
bindings.putMember("http", httpClient);
bindings.putMember("logger", playgroundLogger);
@@ -220,9 +174,9 @@ public class PyPlaygroundExecutor {
playgroundLogger.errorJava("执行parse_file_list方法失败: " + e.getMessage(), e);
throw new RuntimeException(e);
}
- }, INDEPENDENT_EXECUTOR);
+ }, CONTEXT_POOL.getPythonExecutor());
- ScheduledFuture> timeoutTask = TIMEOUT_SCHEDULER.schedule(() -> {
+ ScheduledFuture> timeoutTask = CONTEXT_POOL.getTimeoutScheduler().schedule(() -> {
if (!executionFuture.isDone()) {
executionFuture.cancel(true);
playgroundLogger.errorJava("执行超时,已强制中断");
@@ -257,7 +211,7 @@ public class PyPlaygroundExecutor {
CompletableFuture executionFuture = CompletableFuture.supplyAsync(() -> {
playgroundLogger.infoJava("开始执行parse_by_id方法");
- try (Context context = createContext()) {
+ try (Context context = CONTEXT_POOL.createFreshContext()) {
Value bindings = context.getBindings("python");
bindings.putMember("http", httpClient);
bindings.putMember("logger", playgroundLogger);
@@ -288,9 +242,9 @@ public class PyPlaygroundExecutor {
playgroundLogger.errorJava("执行parse_by_id方法失败: " + e.getMessage(), e);
throw new RuntimeException(e);
}
- }, INDEPENDENT_EXECUTOR);
+ }, CONTEXT_POOL.getPythonExecutor());
- ScheduledFuture> timeoutTask = TIMEOUT_SCHEDULER.schedule(() -> {
+ ScheduledFuture> timeoutTask = CONTEXT_POOL.getTimeoutScheduler().schedule(() -> {
if (!executionFuture.isDone()) {
executionFuture.cancel(true);
playgroundLogger.errorJava("执行超时,已强制中断");
diff --git a/parser/src/test/java/cn/qaiu/parser/PyCryptoUtilsTest.java b/parser/src/test/java/cn/qaiu/parser/PyCryptoUtilsTest.java
new file mode 100644
index 0000000..29b07d8
--- /dev/null
+++ b/parser/src/test/java/cn/qaiu/parser/PyCryptoUtilsTest.java
@@ -0,0 +1,439 @@
+package cn.qaiu.parser;
+
+import cn.qaiu.parser.custompy.PyCryptoUtils;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.nio.charset.StandardCharsets;
+
+import static org.junit.Assert.*;
+
+/**
+ * PyCryptoUtils 测试类
+ * 测试Python加密工具功能
+ *
+ * @author QAIU
+ * Create at 2026/1/11
+ */
+public class PyCryptoUtilsTest {
+
+ private PyCryptoUtils cryptoUtils;
+
+ @Before
+ public void setUp() {
+ cryptoUtils = new PyCryptoUtils();
+ System.out.println("--- 测试开始 ---");
+ }
+
+ // ===================== MD5 测试 =====================
+
+ @Test
+ public void testMd5() {
+ System.out.println("\n[测试] MD5哈希");
+
+ // 测试已知值
+ String input = "hello";
+ String expected = "5d41402abc4b2a76b9719d911017c592";
+
+ String result = cryptoUtils.md5(input);
+
+ System.out.println("输入: " + input);
+ System.out.println("MD5: " + result);
+ System.out.println("期望: " + expected);
+
+ assertEquals("MD5结果应该正确", expected, result);
+ assertEquals("MD5应该是32位", 32, result.length());
+
+ System.out.println("✓ 测试通过");
+ }
+
+ @Test
+ public void testMd5_16() {
+ System.out.println("\n[测试] MD5-16位哈希");
+
+ String input = "hello";
+ String fullMd5 = "5d41402abc4b2a76b9719d911017c592";
+ String expected = fullMd5.substring(8, 24); // "abc4b2a76b9719d9"
+
+ String result = cryptoUtils.md5_16(input);
+
+ System.out.println("输入: " + input);
+ System.out.println("MD5-16: " + result);
+ System.out.println("期望: " + expected);
+
+ assertEquals("MD5-16结果应该正确", expected, result);
+ assertEquals("MD5-16应该是16位", 16, result.length());
+
+ System.out.println("✓ 测试通过");
+ }
+
+ @Test
+ public void testMd5EmptyString() {
+ System.out.println("\n[测试] MD5空字符串");
+
+ String input = "";
+ String expected = "d41d8cd98f00b204e9800998ecf8427e";
+
+ String result = cryptoUtils.md5(input);
+
+ System.out.println("输入: (空字符串)");
+ System.out.println("MD5: " + result);
+
+ assertEquals("空字符串MD5应该正确", expected, result);
+
+ System.out.println("✓ 测试通过");
+ }
+
+ // ===================== SHA 测试 =====================
+
+ @Test
+ public void testSha1() {
+ System.out.println("\n[测试] SHA-1哈希");
+
+ String input = "hello";
+ String expected = "aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d";
+
+ String result = cryptoUtils.sha1(input);
+
+ System.out.println("输入: " + input);
+ System.out.println("SHA-1: " + result);
+
+ assertEquals("SHA-1结果应该正确", expected, result);
+ assertEquals("SHA-1应该是40位", 40, result.length());
+
+ System.out.println("✓ 测试通过");
+ }
+
+ @Test
+ public void testSha256() {
+ System.out.println("\n[测试] SHA-256哈希");
+
+ String input = "hello";
+ String expected = "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824";
+
+ String result = cryptoUtils.sha256(input);
+
+ System.out.println("输入: " + input);
+ System.out.println("SHA-256: " + result);
+
+ assertEquals("SHA-256结果应该正确", expected, result);
+ assertEquals("SHA-256应该是64位", 64, result.length());
+
+ System.out.println("✓ 测试通过");
+ }
+
+ @Test
+ public void testSha512() {
+ System.out.println("\n[测试] SHA-512哈希");
+
+ String input = "hello";
+
+ String result = cryptoUtils.sha512(input);
+
+ System.out.println("输入: " + input);
+ System.out.println("SHA-512: " + result);
+
+ assertNotNull("SHA-512结果不能为null", result);
+ assertEquals("SHA-512应该是128位", 128, result.length());
+
+ System.out.println("✓ 测试通过");
+ }
+
+ // ===================== Base64 测试 =====================
+
+ @Test
+ public void testBase64Encode() {
+ System.out.println("\n[测试] Base64编码");
+
+ String input = "hello world";
+ String expected = "aGVsbG8gd29ybGQ=";
+
+ String result = cryptoUtils.base64_encode(input);
+
+ System.out.println("输入: " + input);
+ System.out.println("Base64: " + result);
+
+ assertEquals("Base64编码应该正确", expected, result);
+
+ System.out.println("✓ 测试通过");
+ }
+
+ @Test
+ public void testBase64Decode() {
+ System.out.println("\n[测试] Base64解码");
+
+ String input = "aGVsbG8gd29ybGQ=";
+ String expected = "hello world";
+
+ String result = cryptoUtils.base64_decode(input);
+
+ System.out.println("输入: " + input);
+ System.out.println("解码: " + result);
+
+ assertEquals("Base64解码应该正确", expected, result);
+
+ System.out.println("✓ 测试通过");
+ }
+
+ @Test
+ public void testBase64EncodeBytes() {
+ System.out.println("\n[测试] Base64字节编码");
+
+ byte[] input = "hello".getBytes(StandardCharsets.UTF_8);
+ String expected = "aGVsbG8=";
+
+ String result = cryptoUtils.base64_encode_bytes(input);
+
+ System.out.println("输入字节数: " + input.length);
+ System.out.println("Base64: " + result);
+
+ assertEquals("Base64字节编码应该正确", expected, result);
+
+ System.out.println("✓ 测试通过");
+ }
+
+ @Test
+ public void testBase64UrlEncode() {
+ System.out.println("\n[测试] Base64 URL安全编码");
+
+ // 包含特殊字符的测试数据
+ String input = "hello+world/test";
+
+ String result = cryptoUtils.base64_url_encode(input);
+
+ System.out.println("输入: " + input);
+ System.out.println("Base64 URL: " + result);
+
+ assertNotNull("结果不能为null", result);
+ assertFalse("URL安全编码不应该包含+", result.contains("+"));
+ assertFalse("URL安全编码不应该包含/", result.contains("/"));
+
+ System.out.println("✓ 测试通过");
+ }
+
+ @Test
+ public void testBase64UrlDecode() {
+ System.out.println("\n[测试] Base64 URL安全解码");
+
+ String input = "aGVsbG8td29ybGQ";
+ String expected = "hello-world";
+
+ String result = cryptoUtils.base64_url_decode(input);
+
+ System.out.println("输入: " + input);
+ System.out.println("解码: " + result);
+
+ assertEquals("Base64 URL解码应该正确", expected, result);
+
+ System.out.println("✓ 测试通过");
+ }
+
+ @Test
+ public void testBase64RoundTrip() {
+ System.out.println("\n[测试] Base64编解码往返");
+
+ String[] testCases = {
+ "hello",
+ "hello world",
+ "中文测试",
+ "特殊字符!@#$%^&*()",
+ ""
+ };
+
+ for (String original : testCases) {
+ String encoded = cryptoUtils.base64_encode(original);
+ String decoded = cryptoUtils.base64_decode(encoded);
+
+ assertEquals("编解码往返应该得到原值: " + original, original, decoded);
+ }
+
+ System.out.println("✓ 测试通过(" + testCases.length + " 个测试用例)");
+ }
+
+ // ===================== AES 测试 =====================
+
+ @Test
+ public void testAesEcbEncryptDecrypt() {
+ System.out.println("\n[测试] AES ECB模式加解密");
+
+ String plaintext = "hello world 123";
+ String key = "1234567890123456"; // 16字节密钥
+
+ // 加密
+ String encrypted = cryptoUtils.aes_encrypt_ecb(plaintext, key);
+ System.out.println("原文: " + plaintext);
+ System.out.println("密钥: " + key);
+ System.out.println("密文: " + encrypted);
+
+ assertNotNull("加密结果不能为null", encrypted);
+ assertNotEquals("加密后应该不同于原文", plaintext, encrypted);
+
+ // 解密
+ String decrypted = cryptoUtils.aes_decrypt_ecb(encrypted, key);
+ System.out.println("解密: " + decrypted);
+
+ assertEquals("解密后应该恢复原文", plaintext, decrypted);
+
+ System.out.println("✓ 测试通过");
+ }
+
+ @Test
+ public void testAesCbcEncryptDecrypt() {
+ System.out.println("\n[测试] AES CBC模式加解密");
+
+ String plaintext = "hello world 123";
+ String key = "1234567890123456"; // 16字节密钥
+ String iv = "abcdefghijklmnop"; // 16字节IV
+
+ // 加密
+ String encrypted = cryptoUtils.aes_encrypt_cbc(plaintext, key, iv);
+ System.out.println("原文: " + plaintext);
+ System.out.println("密钥: " + key);
+ System.out.println("IV: " + iv);
+ System.out.println("密文: " + encrypted);
+
+ assertNotNull("加密结果不能为null", encrypted);
+ assertNotEquals("加密后应该不同于原文", plaintext, encrypted);
+
+ // 解密
+ String decrypted = cryptoUtils.aes_decrypt_cbc(encrypted, key, iv);
+ System.out.println("解密: " + decrypted);
+
+ assertEquals("解密后应该恢复原文", plaintext, decrypted);
+
+ System.out.println("✓ 测试通过");
+ }
+
+ @Test
+ public void testAesWithChineseContent() {
+ System.out.println("\n[测试] AES加密中文内容");
+
+ String plaintext = "这是一段中文内容123";
+ String key = "1234567890123456";
+ String iv = "abcdefghijklmnop";
+
+ // ECB模式
+ String encryptedEcb = cryptoUtils.aes_encrypt_ecb(plaintext, key);
+ String decryptedEcb = cryptoUtils.aes_decrypt_ecb(encryptedEcb, key);
+ assertEquals("ECB解密中文应该正确", plaintext, decryptedEcb);
+
+ // CBC模式
+ String encryptedCbc = cryptoUtils.aes_encrypt_cbc(plaintext, key, iv);
+ String decryptedCbc = cryptoUtils.aes_decrypt_cbc(encryptedCbc, key, iv);
+ assertEquals("CBC解密中文应该正确", plaintext, decryptedCbc);
+
+ System.out.println("✓ 测试通过");
+ }
+
+ @Test
+ public void testAesEcbCbcDifference() {
+ System.out.println("\n[测试] AES ECB和CBC模式差异");
+
+ String plaintext = "hello world 1234";
+ String key = "1234567890123456";
+ String iv = "abcdefghijklmnop";
+
+ String ecbEncrypted = cryptoUtils.aes_encrypt_ecb(plaintext, key);
+ String cbcEncrypted = cryptoUtils.aes_encrypt_cbc(plaintext, key, iv);
+
+ System.out.println("ECB密文: " + ecbEncrypted);
+ System.out.println("CBC密文: " + cbcEncrypted);
+
+ // ECB和CBC模式的加密结果应该不同
+ assertNotEquals("ECB和CBC加密结果应该不同", ecbEncrypted, cbcEncrypted);
+
+ System.out.println("✓ 测试通过");
+ }
+
+ // ===================== 工具方法测试 =====================
+
+ @Test
+ public void testBytesToHex() {
+ System.out.println("\n[测试] 字节转十六进制");
+
+ byte[] input = {0x00, 0x0F, (byte) 0xFF, 0x10, (byte) 0xAB};
+ String expected = "000fff10ab";
+
+ String result = cryptoUtils.bytes_to_hex(input);
+
+ System.out.println("输入字节: " + input.length + " 字节");
+ System.out.println("十六进制: " + result);
+
+ assertEquals("字节转十六进制应该正确", expected, result);
+
+ System.out.println("✓ 测试通过");
+ }
+
+ @Test
+ public void testConsistencyWithJsCryptoUtils() {
+ System.out.println("\n[测试] 与JavaScript加密工具一致性");
+
+ // 这些值应该与JsCryptoUtils产生相同的结果
+ String testString = "consistency_test";
+
+ String md5 = cryptoUtils.md5(testString);
+ String sha1 = cryptoUtils.sha1(testString);
+ String sha256 = cryptoUtils.sha256(testString);
+ String base64 = cryptoUtils.base64_encode(testString);
+
+ System.out.println("测试字符串: " + testString);
+ System.out.println("MD5: " + md5);
+ System.out.println("SHA1: " + sha1);
+ System.out.println("SHA256: " + sha256);
+ System.out.println("Base64: " + base64);
+
+ // 验证结果非空且格式正确
+ assertNotNull("MD5不能为null", md5);
+ assertEquals("MD5长度应该是32", 32, md5.length());
+
+ assertNotNull("SHA1不能为null", sha1);
+ assertEquals("SHA1长度应该是40", 40, sha1.length());
+
+ assertNotNull("SHA256不能为null", sha256);
+ assertEquals("SHA256长度应该是64", 64, sha256.length());
+
+ assertNotNull("Base64不能为null", base64);
+
+ System.out.println("✓ 测试通过");
+ }
+
+ @Test
+ public void testNullInput() {
+ System.out.println("\n[测试] 空输入处理");
+
+ try {
+ // MD5应该能处理null(返回null或抛出异常)
+ String result = cryptoUtils.md5(null);
+ // 如果没有抛出异常,结果应该是null
+ System.out.println("MD5(null) = " + result);
+ } catch (Exception e) {
+ System.out.println("MD5(null) 抛出异常: " + e.getClass().getSimpleName());
+ }
+
+ System.out.println("✓ 空输入处理测试完成");
+ }
+
+ @Test
+ public void testSpecialCharacters() {
+ System.out.println("\n[测试] 特殊字符处理");
+
+ String[] testCases = {
+ "~!@#$%^&*()_+",
+ "日本語テスト",
+ "🎉🎊🎁",
+ "\n\t\r",
+ " "
+ };
+
+ for (String input : testCases) {
+ String md5 = cryptoUtils.md5(input);
+ String base64 = cryptoUtils.base64_encode(input);
+ String decoded = cryptoUtils.base64_decode(base64);
+
+ assertNotNull("MD5不能为null", md5);
+ assertEquals("Base64往返应该正确", input, decoded);
+ }
+
+ System.out.println("✓ 测试通过(" + testCases.length + " 个测试用例)");
+ }
+}
diff --git a/parser/src/test/java/cn/qaiu/parser/PyHttpClientTest.java b/parser/src/test/java/cn/qaiu/parser/PyHttpClientTest.java
new file mode 100644
index 0000000..8e0853f
--- /dev/null
+++ b/parser/src/test/java/cn/qaiu/parser/PyHttpClientTest.java
@@ -0,0 +1,515 @@
+package cn.qaiu.parser;
+
+import cn.qaiu.WebClientVertxInit;
+import cn.qaiu.parser.custompy.PyHttpClient;
+import io.vertx.core.Vertx;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.Assert.*;
+
+/**
+ * PyHttpClient 测试类
+ * 测试Python HTTP客户端功能是否正常
+ *
+ * @author QAIU
+ * Create at 2026/1/11
+ */
+public class PyHttpClientTest {
+
+ private static Vertx vertx;
+ private PyHttpClient httpClient;
+
+ @BeforeClass
+ public static void init() {
+ // 初始化Vertx
+ vertx = Vertx.vertx();
+ WebClientVertxInit.init(vertx);
+ System.out.println("=== PyHttpClient测试初始化完成 ===\n");
+ }
+
+ @Before
+ public void setUp() {
+ // 创建PyHttpClient实例
+ httpClient = new PyHttpClient();
+ System.out.println("--- 测试开始 ---");
+ }
+
+ @After
+ public void tearDown() {
+ System.out.println("--- 测试结束 ---\n");
+ }
+
+ @Test
+ public void testSimpleGetRequest() {
+ System.out.println("\n[测试1] 简单GET请求 - httpbin.org/get");
+
+ try {
+ String url = "https://httpbin.org/get";
+ System.out.println("请求URL: " + url);
+
+ long startTime = System.currentTimeMillis();
+ PyHttpClient.PyHttpResponse response = httpClient.get(url);
+ long endTime = System.currentTimeMillis();
+
+ System.out.println("请求完成,耗时: " + (endTime - startTime) + "ms");
+ System.out.println("状态码: " + response.status_code());
+
+ String body = response.text();
+ System.out.println("响应体长度: " + (body != null ? body.length() : 0) + " 字符");
+
+ // 验证结果
+ assertNotNull("响应不能为null", response);
+ assertEquals("状态码应该是200", 200, response.status_code());
+ assertTrue("请求应该成功", response.ok());
+ assertNotNull("响应体不能为null", body);
+ assertTrue("响应体应该包含url字段", body.contains("\"url\""));
+
+ System.out.println("✓ 测试通过");
+
+ } catch (Exception e) {
+ System.err.println("✗ 测试失败: " + e.getMessage());
+ e.printStackTrace();
+ fail("GET请求失败: " + e.getMessage());
+ }
+ }
+
+ @Test
+ public void testGetWithRedirect() {
+ System.out.println("\n[测试2] GET请求(跟随重定向)");
+
+ try {
+ String url = "https://httpbin.org/redirect/1";
+ System.out.println("请求URL: " + url);
+
+ PyHttpClient.PyHttpResponse response = httpClient.get_with_redirect(url);
+
+ System.out.println("状态码: " + response.status_code());
+
+ // 验证结果
+ assertNotNull("响应不能为null", response);
+ assertEquals("状态码应该是200(重定向后)", 200, response.status_code());
+ assertTrue("请求应该成功", response.ok());
+
+ System.out.println("✓ 测试通过");
+
+ } catch (Exception e) {
+ System.err.println("✗ 测试失败: " + e.getMessage());
+ e.printStackTrace();
+ fail("GET重定向请求失败: " + e.getMessage());
+ }
+ }
+
+ @Test
+ public void testGetNoRedirect() {
+ System.out.println("\n[测试3] GET请求(不跟随重定向)");
+
+ try {
+ String url = "https://httpbin.org/redirect/1";
+ System.out.println("请求URL: " + url);
+
+ PyHttpClient.PyHttpResponse response = httpClient.get_no_redirect(url);
+
+ System.out.println("状态码: " + response.status_code());
+ String location = response.header("Location");
+ System.out.println("Location头: " + location);
+
+ // 验证结果
+ assertNotNull("响应不能为null", response);
+ assertTrue("状态码应该是3xx重定向",
+ response.status_code() >= 300 && response.status_code() < 400);
+ assertFalse("ok()应该返回false", response.ok());
+ assertNotNull("应该有Location头", location);
+
+ System.out.println("✓ 测试通过");
+
+ } catch (Exception e) {
+ System.err.println("✗ 测试失败: " + e.getMessage());
+ e.printStackTrace();
+ fail("GET不重定向请求失败: " + e.getMessage());
+ }
+ }
+
+ @Test
+ public void testPostFormData() {
+ System.out.println("\n[测试4] POST表单数据");
+
+ try {
+ String url = "https://httpbin.org/post";
+ Map formData = new HashMap<>();
+ formData.put("username", "testuser");
+ formData.put("password", "testpass");
+
+ System.out.println("请求URL: " + url);
+ System.out.println("表单数据: " + formData);
+
+ PyHttpClient.PyHttpResponse response = httpClient.post(url, formData);
+
+ System.out.println("状态码: " + response.status_code());
+
+ String body = response.text();
+
+ // 验证结果
+ assertNotNull("响应不能为null", response);
+ assertEquals("状态码应该是200", 200, response.status_code());
+ assertTrue("响应体应该包含username", body.contains("testuser"));
+
+ System.out.println("✓ 测试通过");
+
+ } catch (Exception e) {
+ System.err.println("✗ 测试失败: " + e.getMessage());
+ e.printStackTrace();
+ fail("POST表单数据失败: " + e.getMessage());
+ }
+ }
+
+ @Test
+ public void testPostJson() {
+ System.out.println("\n[测试5] POST JSON数据");
+
+ try {
+ String url = "https://httpbin.org/post";
+ Map jsonData = new HashMap<>();
+ jsonData.put("name", "测试用户");
+ jsonData.put("age", 25);
+ jsonData.put("active", true);
+
+ System.out.println("请求URL: " + url);
+ System.out.println("JSON数据: " + jsonData);
+
+ PyHttpClient.PyHttpResponse response = httpClient.post_json(url, jsonData);
+
+ System.out.println("状态码: " + response.status_code());
+
+ String body = response.text();
+
+ // 验证结果
+ assertNotNull("响应不能为null", response);
+ assertEquals("状态码应该是200", 200, response.status_code());
+ assertTrue("响应体应该包含json数据", body.contains("测试用户") || body.contains("name"));
+
+ System.out.println("✓ 测试通过");
+
+ } catch (Exception e) {
+ System.err.println("✗ 测试失败: " + e.getMessage());
+ e.printStackTrace();
+ fail("POST JSON数据失败: " + e.getMessage());
+ }
+ }
+
+ @Test
+ public void testCustomHeaders() {
+ System.out.println("\n[测试6] 自定义请求头");
+
+ try {
+ String url = "https://httpbin.org/headers";
+
+ // 设置自定义请求头
+ httpClient.put_header("X-Custom-Header", "CustomValue")
+ .put_header("X-Another-Header", "AnotherValue");
+
+ System.out.println("请求URL: " + url);
+
+ PyHttpClient.PyHttpResponse response = httpClient.get(url);
+
+ System.out.println("状态码: " + response.status_code());
+
+ String body = response.text();
+
+ // 验证结果
+ assertNotNull("响应不能为null", response);
+ assertEquals("状态码应该是200", 200, response.status_code());
+ assertTrue("响应体应该包含自定义头", body.contains("X-Custom-Header"));
+
+ System.out.println("✓ 测试通过");
+
+ } catch (Exception e) {
+ System.err.println("✗ 测试失败: " + e.getMessage());
+ e.printStackTrace();
+ fail("自定义请求头测试失败: " + e.getMessage());
+ }
+ }
+
+ @Test
+ public void testBatchHeaders() {
+ System.out.println("\n[测试7] 批量设置请求头");
+
+ try {
+ String url = "https://httpbin.org/headers";
+
+ Map headers = new HashMap<>();
+ headers.put("X-Header-1", "Value1");
+ headers.put("X-Header-2", "Value2");
+ headers.put("X-Header-3", "Value3");
+
+ // 先清除之前的头
+ httpClient.clear_headers();
+ httpClient.put_headers(headers);
+
+ System.out.println("请求URL: " + url);
+ System.out.println("批量设置 " + headers.size() + " 个请求头");
+
+ PyHttpClient.PyHttpResponse response = httpClient.get(url);
+
+ System.out.println("状态码: " + response.status_code());
+
+ // 验证结果
+ assertNotNull("响应不能为null", response);
+ assertEquals("状态码应该是200", 200, response.status_code());
+
+ System.out.println("✓ 测试通过");
+
+ } catch (Exception e) {
+ System.err.println("✗ 测试失败: " + e.getMessage());
+ e.printStackTrace();
+ fail("批量设置请求头测试失败: " + e.getMessage());
+ }
+ }
+
+ @Test
+ public void testResponseJson() {
+ System.out.println("\n[测试8] 解析JSON响应");
+
+ try {
+ String url = "https://httpbin.org/json";
+
+ System.out.println("请求URL: " + url);
+
+ // 清除之前设置的头
+ httpClient.clear_headers();
+
+ PyHttpClient.PyHttpResponse response = httpClient.get(url);
+
+ System.out.println("状态码: " + response.status_code());
+
+ Object jsonObj = response.json();
+
+ // 验证结果
+ assertNotNull("响应不能为null", response);
+ assertEquals("状态码应该是200", 200, response.status_code());
+ assertNotNull("JSON对象不能为null", jsonObj);
+
+ System.out.println("JSON类型: " + jsonObj.getClass().getSimpleName());
+ System.out.println("✓ 测试通过");
+
+ } catch (Exception e) {
+ System.err.println("✗ 测试失败: " + e.getMessage());
+ e.printStackTrace();
+ fail("解析JSON响应失败: " + e.getMessage());
+ }
+ }
+
+ @Test
+ public void testResponseHeader() {
+ System.out.println("\n[测试9] 获取响应头");
+
+ try {
+ String url = "https://httpbin.org/response-headers?X-Test-Header=TestValue";
+
+ System.out.println("请求URL: " + url);
+
+ httpClient.clear_headers();
+ PyHttpClient.PyHttpResponse response = httpClient.get(url);
+
+ System.out.println("状态码: " + response.status_code());
+
+ String contentType = response.header("Content-Type");
+ System.out.println("Content-Type: " + contentType);
+
+ // 验证结果
+ assertNotNull("响应不能为null", response);
+ assertEquals("状态码应该是200", 200, response.status_code());
+ assertNotNull("应该有Content-Type头", contentType);
+
+ System.out.println("✓ 测试通过");
+
+ } catch (Exception e) {
+ System.err.println("✗ 测试失败: " + e.getMessage());
+ e.printStackTrace();
+ fail("获取响应头失败: " + e.getMessage());
+ }
+ }
+
+ @Test
+ public void testContentLength() {
+ System.out.println("\n[测试10] 获取内容长度");
+
+ try {
+ String url = "https://httpbin.org/bytes/1024";
+
+ System.out.println("请求URL: " + url);
+
+ httpClient.clear_headers();
+ PyHttpClient.PyHttpResponse response = httpClient.get(url);
+
+ System.out.println("状态码: " + response.status_code());
+
+ long contentLength = response.content_length();
+ System.out.println("Content-Length: " + contentLength);
+
+ // 验证结果
+ assertNotNull("响应不能为null", response);
+ assertEquals("状态码应该是200", 200, response.status_code());
+ assertTrue("内容长度应该大于0", contentLength > 0);
+
+ System.out.println("✓ 测试通过");
+
+ } catch (Exception e) {
+ System.err.println("✗ 测试失败: " + e.getMessage());
+ e.printStackTrace();
+ fail("获取内容长度失败: " + e.getMessage());
+ }
+ }
+
+ @Test
+ public void testPutRequest() {
+ System.out.println("\n[测试11] PUT请求");
+
+ try {
+ String url = "https://httpbin.org/put";
+ Map data = new HashMap<>();
+ data.put("key", "value");
+
+ System.out.println("请求URL: " + url);
+
+ httpClient.clear_headers();
+ PyHttpClient.PyHttpResponse response = httpClient.put(url, data);
+
+ System.out.println("状态码: " + response.status_code());
+
+ // 验证结果
+ assertNotNull("响应不能为null", response);
+ assertEquals("状态码应该是200", 200, response.status_code());
+
+ System.out.println("✓ 测试通过");
+
+ } catch (Exception e) {
+ System.err.println("✗ 测试失败: " + e.getMessage());
+ e.printStackTrace();
+ fail("PUT请求失败: " + e.getMessage());
+ }
+ }
+
+ @Test
+ public void testDeleteRequest() {
+ System.out.println("\n[测试12] DELETE请求");
+
+ try {
+ String url = "https://httpbin.org/delete";
+
+ System.out.println("请求URL: " + url);
+
+ httpClient.clear_headers();
+ PyHttpClient.PyHttpResponse response = httpClient.delete(url);
+
+ System.out.println("状态码: " + response.status_code());
+
+ // 验证结果
+ assertNotNull("响应不能为null", response);
+ assertEquals("状态码应该是200", 200, response.status_code());
+
+ System.out.println("✓ 测试通过");
+
+ } catch (Exception e) {
+ System.err.println("✗ 测试失败: " + e.getMessage());
+ e.printStackTrace();
+ fail("DELETE请求失败: " + e.getMessage());
+ }
+ }
+
+ @Test
+ public void testPatchRequest() {
+ System.out.println("\n[测试13] PATCH请求");
+
+ try {
+ String url = "https://httpbin.org/patch";
+ Map data = new HashMap<>();
+ data.put("field", "updated");
+
+ System.out.println("请求URL: " + url);
+
+ httpClient.clear_headers();
+ PyHttpClient.PyHttpResponse response = httpClient.patch(url, data);
+
+ System.out.println("状态码: " + response.status_code());
+
+ // 验证结果
+ assertNotNull("响应不能为null", response);
+ assertEquals("状态码应该是200", 200, response.status_code());
+
+ System.out.println("✓ 测试通过");
+
+ } catch (Exception e) {
+ System.err.println("✗ 测试失败: " + e.getMessage());
+ e.printStackTrace();
+ fail("PATCH请求失败: " + e.getMessage());
+ }
+ }
+
+ @Test
+ public void testMethodChaining() {
+ System.out.println("\n[测试14] 方法链式调用");
+
+ try {
+ String url = "https://httpbin.org/headers";
+
+ System.out.println("请求URL: " + url);
+
+ // 测试链式调用
+ PyHttpClient.PyHttpResponse response = new PyHttpClient()
+ .put_header("X-Chain-1", "Value1")
+ .put_header("X-Chain-2", "Value2")
+ .set_timeout(30)
+ .get(url);
+
+ System.out.println("状态码: " + response.status_code());
+
+ String body = response.text();
+
+ // 验证结果
+ assertNotNull("响应不能为null", response);
+ assertEquals("状态码应该是200", 200, response.status_code());
+ assertTrue("响应体应该包含链式设置的头", body.contains("X-Chain"));
+
+ System.out.println("✓ 测试通过");
+
+ } catch (Exception e) {
+ System.err.println("✗ 测试失败: " + e.getMessage());
+ e.printStackTrace();
+ fail("方法链式调用测试失败: " + e.getMessage());
+ }
+ }
+
+ @Test
+ public void testBodyAndTextEquivalent() {
+ System.out.println("\n[测试15] body()和text()方法等价性");
+
+ try {
+ String url = "https://httpbin.org/get";
+
+ System.out.println("请求URL: " + url);
+
+ httpClient.clear_headers();
+ PyHttpClient.PyHttpResponse response = httpClient.get(url);
+
+ String body = response.body();
+ String text = response.text();
+
+ // 验证结果
+ assertEquals("body()和text()应该返回相同的结果", body, text);
+
+ System.out.println("✓ 测试通过");
+ System.out.println(" body() == text(): " + body.equals(text));
+
+ } catch (Exception e) {
+ System.err.println("✗ 测试失败: " + e.getMessage());
+ e.printStackTrace();
+ fail("body()和text()等价性测试失败: " + e.getMessage());
+ }
+ }
+}
diff --git a/parser/src/test/java/cn/qaiu/parser/PyParserTest.java b/parser/src/test/java/cn/qaiu/parser/PyParserTest.java
new file mode 100644
index 0000000..08033ef
--- /dev/null
+++ b/parser/src/test/java/cn/qaiu/parser/PyParserTest.java
@@ -0,0 +1,558 @@
+package cn.qaiu.parser;
+
+import cn.qaiu.entity.FileInfo;
+import cn.qaiu.entity.ShareLinkInfo;
+import cn.qaiu.parser.custom.CustomParserRegistry;
+import cn.qaiu.parser.custompy.*;
+import cn.qaiu.WebClientVertxInit;
+import io.vertx.core.Vertx;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.*;
+
+/**
+ * Python解析器测试
+ * 测试GraalPy Python解析器的核心功能
+ *
+ * @author QAIU
+ * Create at 2026/1/11
+ */
+public class PyParserTest {
+
+ private static Vertx vertx;
+
+ @BeforeClass
+ public static void init() {
+ // 初始化Vertx
+ vertx = Vertx.vertx();
+ WebClientVertxInit.init(vertx);
+ System.out.println("=== Python解析器测试初始化完成 ===\n");
+ }
+
+ @Before
+ public void setUp() {
+ // 清理注册表
+ CustomParserRegistry.clear();
+ }
+
+ @Test
+ public void testPyContextPoolInitialization() {
+ System.out.println("\n[测试] Context池初始化");
+
+ try {
+ PyContextPool pool = PyContextPool.getInstance();
+
+ assertNotNull("Context池实例不能为null", pool);
+ assertFalse("Context池不应该是关闭状态", pool.isClosed());
+ assertTrue("应该有可用的Context", pool.getCreatedCount() > 0);
+
+ System.out.println("✓ Context池初始化测试通过");
+ System.out.println(" " + pool.getStatus());
+
+ } catch (Exception e) {
+ System.err.println("✗ Context池初始化测试失败: " + e.getMessage());
+ e.printStackTrace();
+ fail("Context池初始化失败: " + e.getMessage());
+ }
+ }
+
+ @Test
+ public void testPyContextPoolAcquireRelease() throws Exception {
+ System.out.println("\n[测试] Context池获取和释放");
+
+ try {
+ PyContextPool pool = PyContextPool.getInstance();
+
+ // 获取Context
+ PyContextPool.PooledContext pc = pool.acquire();
+ assertNotNull("获取的Context不能为null", pc);
+ assertNotNull("底层Context不能为null", pc.getContext());
+ assertFalse("Context不应该过期", pc.isExpired());
+
+ int availableBefore = pool.getAvailableCount();
+
+ // 释放Context
+ pc.close();
+
+ // 验证归还后可用数量增加
+ int availableAfter = pool.getAvailableCount();
+ assertTrue("归还后可用数量应该增加", availableAfter >= availableBefore);
+
+ System.out.println("✓ Context池获取和释放测试通过");
+
+ } catch (Exception e) {
+ System.err.println("✗ Context池获取和释放测试失败: " + e.getMessage());
+ e.printStackTrace();
+ throw e;
+ }
+ }
+
+ @Test
+ public void testSimplePythonExecution() {
+ System.out.println("\n[测试] 简单Python代码执行");
+
+ String pyCode = """
+ # 简单测试
+ def parse(share_link_info, http, logger):
+ logger.info("测试日志")
+ return "https://example.com/download/test.zip"
+ """;
+
+ try {
+ ShareLinkInfo linkInfo = ShareLinkInfo.newBuilder()
+ .shareUrl("https://example.com/s/test123")
+ .shareKey("test123")
+ .otherParam(new HashMap<>())
+ .build();
+
+ PyPlaygroundExecutor executor = new PyPlaygroundExecutor(linkInfo, pyCode);
+
+ String result = executor.executeParseAsync()
+ .toCompletionStage()
+ .toCompletableFuture()
+ .get(30, TimeUnit.SECONDS);
+
+ assertNotNull("执行结果不能为null", result);
+ assertTrue("应该返回下载链接", result.contains("example.com"));
+
+ // 检查日志
+ List logs = executor.getLogs();
+ assertFalse("应该有日志输出", logs.isEmpty());
+
+ System.out.println("✓ 简单Python代码执行测试通过");
+ System.out.println(" 返回结果: " + result);
+ System.out.println(" 日志数量: " + logs.size());
+
+ } catch (Exception e) {
+ System.err.println("✗ 简单Python代码执行测试失败: " + e.getMessage());
+ e.printStackTrace();
+ fail("Python执行失败: " + e.getMessage());
+ }
+ }
+
+ @Test
+ public void testPythonHttpRequest() {
+ System.out.println("\n[测试] Python HTTP请求功能");
+
+ String pyCode = """
+ def parse(share_link_info, http, logger):
+ logger.info("开始HTTP请求测试")
+
+ # 发送GET请求
+ response = http.get("https://httpbin.org/get")
+
+ if response.ok():
+ logger.info(f"请求成功,状态码: {response.status_code()}")
+ return "https://example.com/success"
+ else:
+ logger.error(f"请求失败,状态码: {response.status_code()}")
+ return "https://example.com/failed"
+ """;
+
+ try {
+ ShareLinkInfo linkInfo = ShareLinkInfo.newBuilder()
+ .shareUrl("https://example.com/s/test123")
+ .shareKey("test123")
+ .otherParam(new HashMap<>())
+ .build();
+
+ PyPlaygroundExecutor executor = new PyPlaygroundExecutor(linkInfo, pyCode);
+
+ String result = executor.executeParseAsync()
+ .toCompletionStage()
+ .toCompletableFuture()
+ .get(60, TimeUnit.SECONDS);
+
+ assertNotNull("执行结果不能为null", result);
+ assertTrue("应该返回成功链接", result.contains("success"));
+
+ System.out.println("✓ Python HTTP请求功能测试通过");
+ System.out.println(" 返回结果: " + result);
+
+ } catch (Exception e) {
+ System.err.println("✗ Python HTTP请求功能测试失败: " + e.getMessage());
+ e.printStackTrace();
+ fail("Python HTTP请求失败: " + e.getMessage());
+ }
+ }
+
+ @Test
+ public void testPythonCryptoUtils() {
+ System.out.println("\n[测试] Python加密工具功能");
+
+ String pyCode = """
+ def parse(share_link_info, http, logger):
+ # 测试MD5
+ md5_result = crypto.md5("hello")
+ logger.info(f"MD5: {md5_result}")
+
+ # 测试SHA256
+ sha256_result = crypto.sha256("hello")
+ logger.info(f"SHA256: {sha256_result}")
+
+ # 测试Base64编码解码
+ b64_encoded = crypto.base64_encode("hello world")
+ b64_decoded = crypto.base64_decode(b64_encoded)
+ logger.info(f"Base64: {b64_encoded} -> {b64_decoded}")
+
+ # 验证MD5正确性
+ if md5_result == "5d41402abc4b2a76b9719d911017c592":
+ return "https://example.com/crypto_success"
+ else:
+ return "https://example.com/crypto_failed"
+ """;
+
+ try {
+ ShareLinkInfo linkInfo = ShareLinkInfo.newBuilder()
+ .shareUrl("https://example.com/s/test123")
+ .shareKey("test123")
+ .otherParam(new HashMap<>())
+ .build();
+
+ PyPlaygroundExecutor executor = new PyPlaygroundExecutor(linkInfo, pyCode);
+
+ String result = executor.executeParseAsync()
+ .toCompletionStage()
+ .toCompletableFuture()
+ .get(30, TimeUnit.SECONDS);
+
+ assertNotNull("执行结果不能为null", result);
+ assertTrue("加密工具应该正常工作", result.contains("crypto_success"));
+
+ System.out.println("✓ Python加密工具功能测试通过");
+ System.out.println(" 返回结果: " + result);
+
+ } catch (Exception e) {
+ System.err.println("✗ Python加密工具功能测试失败: " + e.getMessage());
+ e.printStackTrace();
+ fail("Python加密工具测试失败: " + e.getMessage());
+ }
+ }
+
+ @Test
+ public void testPythonShareLinkInfo() {
+ System.out.println("\n[测试] Python ShareLinkInfo访问");
+
+ String pyCode = """
+ def parse(share_link_info, http, logger):
+ # 获取分享链接信息
+ url = share_link_info.get_share_url()
+ key = share_link_info.get_share_key()
+ pwd = share_link_info.get_share_password()
+
+ logger.info(f"URL: {url}")
+ logger.info(f"Key: {key}")
+ logger.info(f"Password: {pwd}")
+
+ # 测试其他参数
+ custom_param = share_link_info.get_other_param("customKey")
+ logger.info(f"CustomKey: {custom_param}")
+
+ if url and key:
+ return f"https://example.com/download/{key}"
+ else:
+ return "https://example.com/failed"
+ """;
+
+ try {
+ Map otherParams = new HashMap<>();
+ otherParams.put("customKey", "customValue");
+
+ ShareLinkInfo linkInfo = ShareLinkInfo.newBuilder()
+ .shareUrl("https://example.com/s/mykey123")
+ .shareKey("mykey123")
+ .sharePassword("mypassword")
+ .otherParam(otherParams)
+ .build();
+
+ PyPlaygroundExecutor executor = new PyPlaygroundExecutor(linkInfo, pyCode);
+
+ String result = executor.executeParseAsync()
+ .toCompletionStage()
+ .toCompletableFuture()
+ .get(30, TimeUnit.SECONDS);
+
+ assertNotNull("执行结果不能为null", result);
+ assertTrue("应该包含正确的key", result.contains("mykey123"));
+
+ System.out.println("✓ Python ShareLinkInfo访问测试通过");
+ System.out.println(" 返回结果: " + result);
+
+ } catch (Exception e) {
+ System.err.println("✗ Python ShareLinkInfo访问测试失败: " + e.getMessage());
+ e.printStackTrace();
+ fail("Python ShareLinkInfo访问失败: " + e.getMessage());
+ }
+ }
+
+ @Test
+ public void testPythonFileListParsing() {
+ System.out.println("\n[测试] Python文件列表解析");
+
+ String pyCode = """
+ def parse(share_link_info, http, logger):
+ return "https://example.com/download/single.zip"
+
+ def parse_file_list(share_link_info, http, logger):
+ logger.info("开始解析文件列表")
+
+ # 返回文件列表
+ file_list = [
+ {
+ "file_name": "测试文件1.txt",
+ "file_id": "file001",
+ "file_type": "txt",
+ "size": 1024,
+ "pan_type": "custom"
+ },
+ {
+ "file_name": "测试文件2.zip",
+ "file_id": "file002",
+ "file_type": "zip",
+ "size": 2048,
+ "pan_type": "custom"
+ }
+ ]
+
+ logger.info(f"解析到 {len(file_list)} 个文件")
+ return file_list
+ """;
+
+ try {
+ ShareLinkInfo linkInfo = ShareLinkInfo.newBuilder()
+ .shareUrl("https://example.com/s/test123")
+ .shareKey("test123")
+ .otherParam(new HashMap<>())
+ .build();
+
+ PyPlaygroundExecutor executor = new PyPlaygroundExecutor(linkInfo, pyCode);
+
+ List fileList = executor.executeParseFileListAsync()
+ .toCompletionStage()
+ .toCompletableFuture()
+ .get(30, TimeUnit.SECONDS);
+
+ assertNotNull("文件列表不能为null", fileList);
+ assertEquals("应该有2个文件", 2, fileList.size());
+
+ FileInfo firstFile = fileList.get(0);
+ assertEquals("第一个文件名应该正确", "测试文件1.txt", firstFile.getFileName());
+ assertEquals("第一个文件ID应该正确", "file001", firstFile.getFileId());
+
+ System.out.println("✓ Python文件列表解析测试通过");
+ System.out.println(" 文件数量: " + fileList.size());
+ for (FileInfo file : fileList) {
+ System.out.println(" - " + file.getFileName() + " (" + file.getSize() + " bytes)");
+ }
+
+ } catch (Exception e) {
+ System.err.println("✗ Python文件列表解析测试失败: " + e.getMessage());
+ e.printStackTrace();
+ fail("Python文件列表解析失败: " + e.getMessage());
+ }
+ }
+
+ @Test
+ public void testPythonParseById() {
+ System.out.println("\n[测试] Python按ID解析");
+
+ String pyCode = """
+ def parse(share_link_info, http, logger):
+ return "https://example.com/download/single.zip"
+
+ def parse_by_id(share_link_info, http, logger):
+ # 获取文件ID参数
+ param_json = share_link_info.get_other_param("paramJson")
+
+ if param_json and hasattr(param_json, 'fileId'):
+ file_id = param_json.fileId
+ else:
+ file_id = "default_id"
+
+ logger.info(f"按ID解析: {file_id}")
+ return f"https://example.com/download/{file_id}"
+ """;
+
+ try {
+ Map otherParams = new HashMap<>();
+ io.vertx.core.json.JsonObject paramJson = new io.vertx.core.json.JsonObject();
+ paramJson.put("fileId", "myfile123");
+ otherParams.put("paramJson", paramJson);
+
+ ShareLinkInfo linkInfo = ShareLinkInfo.newBuilder()
+ .shareUrl("https://example.com/s/test123")
+ .shareKey("test123")
+ .otherParam(otherParams)
+ .build();
+
+ PyPlaygroundExecutor executor = new PyPlaygroundExecutor(linkInfo, pyCode);
+
+ String result = executor.executeParseByIdAsync()
+ .toCompletionStage()
+ .toCompletableFuture()
+ .get(30, TimeUnit.SECONDS);
+
+ assertNotNull("执行结果不能为null", result);
+ assertTrue("应该包含文件ID", result.contains("download"));
+
+ System.out.println("✓ Python按ID解析测试通过");
+ System.out.println(" 返回结果: " + result);
+
+ } catch (Exception e) {
+ System.err.println("✗ Python按ID解析测试失败: " + e.getMessage());
+ e.printStackTrace();
+ fail("Python按ID解析失败: " + e.getMessage());
+ }
+ }
+
+ @Test
+ public void testPythonErrorHandling() {
+ System.out.println("\n[测试] Python错误处理");
+
+ String pyCode = """
+ def parse(share_link_info, http, logger):
+ # 故意抛出异常
+ raise ValueError("测试错误处理")
+ """;
+
+ try {
+ ShareLinkInfo linkInfo = ShareLinkInfo.newBuilder()
+ .shareUrl("https://example.com/s/test123")
+ .shareKey("test123")
+ .otherParam(new HashMap<>())
+ .build();
+
+ PyPlaygroundExecutor executor = new PyPlaygroundExecutor(linkInfo, pyCode);
+
+ try {
+ executor.executeParseAsync()
+ .toCompletionStage()
+ .toCompletableFuture()
+ .get(30, TimeUnit.SECONDS);
+
+ fail("应该抛出异常");
+
+ } catch (Exception e) {
+ // 预期的异常
+ assertTrue("异常信息应该包含错误内容",
+ e.getMessage().contains("ValueError") ||
+ e.getCause().getMessage().contains("ValueError"));
+
+ System.out.println("✓ Python错误处理测试通过");
+ System.out.println(" 捕获到预期的异常: " + e.getMessage());
+ }
+
+ } catch (Exception e) {
+ System.err.println("✗ Python错误处理测试失败: " + e.getMessage());
+ e.printStackTrace();
+ fail("Python错误处理测试失败: " + e.getMessage());
+ }
+ }
+
+ @Test
+ public void testPythonSandboxSecurity() {
+ System.out.println("\n[测试] Python沙箱安全性");
+
+ // 测试禁止文件系统访问
+ String pyCode = """
+ import os
+
+ def parse(share_link_info, http, logger):
+ try:
+ # 尝试读取文件(应该被拒绝)
+ with open("/etc/passwd", "r") as f:
+ content = f.read()
+ return "https://example.com/security_breach"
+ except Exception as e:
+ logger.info(f"文件访问被正确拒绝: {type(e).__name__}")
+ return "https://example.com/security_ok"
+ """;
+
+ try {
+ ShareLinkInfo linkInfo = ShareLinkInfo.newBuilder()
+ .shareUrl("https://example.com/s/test123")
+ .shareKey("test123")
+ .otherParam(new HashMap<>())
+ .build();
+
+ PyPlaygroundExecutor executor = new PyPlaygroundExecutor(linkInfo, pyCode);
+
+ String result = executor.executeParseAsync()
+ .toCompletionStage()
+ .toCompletableFuture()
+ .get(30, TimeUnit.SECONDS);
+
+ // 如果返回security_ok或抛出异常都表示安全机制工作正常
+ assertTrue("沙箱应该阻止文件访问",
+ result.contains("security_ok") || !result.contains("security_breach"));
+
+ System.out.println("✓ Python沙箱安全性测试通过");
+ System.out.println(" 返回结果: " + result);
+
+ } catch (Exception e) {
+ // 如果直接抛出异常也表示安全机制工作正常
+ System.out.println("✓ Python沙箱安全性测试通过(抛出异常)");
+ System.out.println(" 异常信息: " + e.getMessage());
+ }
+ }
+
+ @Test
+ public void testPythonLoggerLevels() {
+ System.out.println("\n[测试] Python日志级别");
+
+ String pyCode = """
+ def parse(share_link_info, http, logger):
+ logger.debug("这是DEBUG日志")
+ logger.info("这是INFO日志")
+ logger.warn("这是WARN日志")
+ logger.error("这是ERROR日志")
+ return "https://example.com/log_test"
+ """;
+
+ try {
+ ShareLinkInfo linkInfo = ShareLinkInfo.newBuilder()
+ .shareUrl("https://example.com/s/test123")
+ .shareKey("test123")
+ .otherParam(new HashMap<>())
+ .build();
+
+ PyPlaygroundExecutor executor = new PyPlaygroundExecutor(linkInfo, pyCode);
+
+ executor.executeParseAsync()
+ .toCompletionStage()
+ .toCompletableFuture()
+ .get(30, TimeUnit.SECONDS);
+
+ List logs = executor.getLogs();
+
+ // 检查各个级别的日志
+ boolean hasDebug = logs.stream().anyMatch(l -> "DEBUG".equals(l.getLevel()));
+ boolean hasInfo = logs.stream().anyMatch(l -> "INFO".equals(l.getLevel()));
+ boolean hasWarn = logs.stream().anyMatch(l -> "WARN".equals(l.getLevel()));
+ boolean hasError = logs.stream().anyMatch(l -> "ERROR".equals(l.getLevel()));
+
+ System.out.println("✓ Python日志级别测试通过");
+ System.out.println(" 日志数量: " + logs.size());
+ System.out.println(" DEBUG: " + hasDebug);
+ System.out.println(" INFO: " + hasInfo);
+ System.out.println(" WARN: " + hasWarn);
+ System.out.println(" ERROR: " + hasError);
+
+ for (PyPlaygroundLogger.LogEntry log : logs) {
+ System.out.println(" [" + log.getLevel() + "] " + log.getMessage());
+ }
+
+ } catch (Exception e) {
+ System.err.println("✗ Python日志级别测试失败: " + e.getMessage());
+ e.printStackTrace();
+ fail("Python日志级别测试失败: " + e.getMessage());
+ }
+ }
+}
diff --git a/web-front/src/views/Playground.vue b/web-front/src/views/Playground.vue
index d883c98..09f0831 100644
--- a/web-front/src/views/Playground.vue
+++ b/web-front/src/views/Playground.vue
@@ -1625,66 +1625,77 @@ def parse_file_list(share_link_info, http, logger):
const createNewFile = async () => {
if (!newFileFormRef.value) return;
- await newFileFormRef.value.validate((valid) => {
+ try {
+ // 使用 Promise 模式进行表单验证
+ const valid = await newFileFormRef.value.validate();
if (!valid) return;
-
- const language = newFileForm.value.language || 'javascript';
- const isPython = language === 'python';
- const fileExt = isPython ? '.py' : '.js';
-
- // 使用解析器名称作为文件名
- let fileName = newFileForm.value.name;
- if (!fileName.endsWith(fileExt)) {
- // 移除可能的错误扩展名
- fileName = fileName.replace(/\.(js|py)$/i, '');
- fileName = fileName + fileExt;
+ } catch (e) {
+ // 验证失败
+ return;
+ }
+
+ const language = newFileForm.value.language || 'javascript';
+ const isPython = language === 'python';
+ const fileExt = isPython ? '.py' : '.js';
+
+ // 使用解析器名称作为文件名
+ let fileName = newFileForm.value.name;
+ if (!fileName.endsWith(fileExt)) {
+ // 移除可能的错误扩展名
+ fileName = fileName.replace(/\.(js|py)$/i, '');
+ fileName = fileName + fileExt;
+ }
+
+ // 检查文件名是否已存在
+ if (files.value.some(f => f.name === fileName)) {
+ ElMessage.warning('文件名已存在,请使用其他名称');
+ return;
+ }
+
+ // 生成模板代码
+ const template = generateTemplate(
+ newFileForm.value.name.replace(/\.(js|py)$/i, ''),
+ newFileForm.value.identifier,
+ newFileForm.value.author,
+ newFileForm.value.match,
+ language
+ );
+
+ // 创建新文件
+ fileIdCounter.value++;
+ const newFile = {
+ id: 'file' + fileIdCounter.value,
+ name: fileName,
+ content: template,
+ language: language,
+ modified: false
+ };
+
+ files.value.push(newFile);
+
+ // 关闭对话框并保存
+ newFileDialogVisible.value = false;
+ saveAllFilesToStorage();
+
+ // 使用 nextTick 确保 DOM 更新后再切换选项卡
+ await nextTick();
+
+ // 设置活动文件 ID(此时 DOM 已更新,tab 已存在)
+ activeFileId.value = newFile.id;
+
+ // 更新编辑器语言模式
+ updateEditorLanguage(language);
+
+ ElMessage.success(`${isPython ? 'Python' : 'JavaScript'}文件创建成功`);
+
+ // 等待编辑器更新后聚焦
+ await nextTick();
+ if (editorRef.value && editorRef.value.getEditor) {
+ const editor = editorRef.value.getEditor();
+ if (editor) {
+ editor.focus();
}
-
- // 检查文件名是否已存在
- if (files.value.some(f => f.name === fileName)) {
- ElMessage.warning('文件名已存在,请使用其他名称');
- return;
- }
-
- // 生成模板代码
- const template = generateTemplate(
- newFileForm.value.name.replace(/\.(js|py)$/i, ''),
- newFileForm.value.identifier,
- newFileForm.value.author,
- newFileForm.value.match,
- language
- );
-
- // 创建新文件
- fileIdCounter.value++;
- const newFile = {
- id: 'file' + fileIdCounter.value,
- name: fileName,
- content: template,
- language: language,
- modified: false
- };
-
- files.value.push(newFile);
- activeFileId.value = newFile.id;
- newFileDialogVisible.value = false;
- saveAllFilesToStorage();
-
- // 更新编辑器语言模式
- updateEditorLanguage(language);
-
- ElMessage.success(`${isPython ? 'Python' : 'JavaScript'}文件创建成功`);
-
- // 等待编辑器更新后聚焦
- nextTick(() => {
- if (editorRef.value && editorRef.value.getEditor) {
- const editor = editorRef.value.getEditor();
- if (editor) {
- editor.focus();
- }
- }
- });
- });
+ }
};
// IDE功能:复制全部