diff --git a/parser/src/main/java/cn/qaiu/parser/customjs/JsPlaygroundExecutor.java b/parser/src/main/java/cn/qaiu/parser/customjs/JsPlaygroundExecutor.java index 79e5fda..a579972 100644 --- a/parser/src/main/java/cn/qaiu/parser/customjs/JsPlaygroundExecutor.java +++ b/parser/src/main/java/cn/qaiu/parser/customjs/JsPlaygroundExecutor.java @@ -36,6 +36,14 @@ public class JsPlaygroundExecutor { return thread; }); + // 超时调度线程池,用于处理超时中断 + private static final ScheduledExecutorService TIMEOUT_SCHEDULER = Executors.newScheduledThreadPool(2, r -> { + Thread thread = new Thread(r); + thread.setName("playground-timeout-scheduler-" + System.currentTimeMillis()); + thread.setDaemon(true); + return thread; + }); + private final ShareLinkInfo shareLinkInfo; private final String jsCode; private final ScriptEngine engine; @@ -161,23 +169,34 @@ public class JsPlaygroundExecutor { } }, INDEPENDENT_EXECUTOR); - // 添加超时处理 - executionFuture.orTimeout(EXECUTION_TIMEOUT_SECONDS, TimeUnit.SECONDS) - .whenComplete((result, error) -> { - if (error != null) { - if (error instanceof TimeoutException) { - String timeoutMsg = "JavaScript执行超时(超过" + EXECUTION_TIMEOUT_SECONDS + "秒),可能存在无限循环"; - playgroundLogger.errorJava(timeoutMsg); - log.error(timeoutMsg); - promise.fail(new RuntimeException(timeoutMsg)); - } else { - Throwable cause = error.getCause(); - promise.fail(cause != null ? cause : error); - } + // 创建超时任务,强制取消执行 + ScheduledFuture timeoutTask = TIMEOUT_SCHEDULER.schedule(() -> { + if (!executionFuture.isDone()) { + executionFuture.cancel(true); // 强制中断执行线程 + playgroundLogger.errorJava("执行超时,已强制中断"); + log.warn("JavaScript执行超时,已强制取消"); + } + }, EXECUTION_TIMEOUT_SECONDS, TimeUnit.SECONDS); + + // 处理执行结果 + executionFuture.whenComplete((result, error) -> { + // 取消超时任务 + timeoutTask.cancel(false); + + if (error != null) { + if (error instanceof CancellationException) { + String timeoutMsg = "JavaScript执行超时(超过" + EXECUTION_TIMEOUT_SECONDS + "秒),已强制中断"; + playgroundLogger.errorJava(timeoutMsg); + log.error(timeoutMsg); + promise.fail(new RuntimeException(timeoutMsg)); } else { - promise.complete(result); + Throwable cause = error.getCause(); + promise.fail(cause != null ? cause : error); } - }); + } else { + promise.complete(result); + } + }); return promise.future(); } @@ -225,23 +244,34 @@ public class JsPlaygroundExecutor { } }, INDEPENDENT_EXECUTOR); - // 添加超时处理 - executionFuture.orTimeout(EXECUTION_TIMEOUT_SECONDS, TimeUnit.SECONDS) - .whenComplete((result, error) -> { - if (error != null) { - if (error instanceof TimeoutException) { - String timeoutMsg = "JavaScript执行超时(超过" + EXECUTION_TIMEOUT_SECONDS + "秒),可能存在无限循环"; - playgroundLogger.errorJava(timeoutMsg); - log.error(timeoutMsg); - promise.fail(new RuntimeException(timeoutMsg)); - } else { - Throwable cause = error.getCause(); - promise.fail(cause != null ? cause : error); - } + // 创建超时任务,强制取消执行 + ScheduledFuture timeoutTask = TIMEOUT_SCHEDULER.schedule(() -> { + if (!executionFuture.isDone()) { + executionFuture.cancel(true); // 强制中断执行线程 + playgroundLogger.errorJava("执行超时,已强制中断"); + log.warn("JavaScript执行超时,已强制取消"); + } + }, EXECUTION_TIMEOUT_SECONDS, TimeUnit.SECONDS); + + // 处理执行结果 + executionFuture.whenComplete((result, error) -> { + // 取消超时任务 + timeoutTask.cancel(false); + + if (error != null) { + if (error instanceof CancellationException) { + String timeoutMsg = "JavaScript执行超时(超过" + EXECUTION_TIMEOUT_SECONDS + "秒),已强制中断"; + playgroundLogger.errorJava(timeoutMsg); + log.error(timeoutMsg); + promise.fail(new RuntimeException(timeoutMsg)); } else { - promise.complete(result); + Throwable cause = error.getCause(); + promise.fail(cause != null ? cause : error); } - }); + } else { + promise.complete(result); + } + }); return promise.future(); } @@ -288,23 +318,34 @@ public class JsPlaygroundExecutor { } }, INDEPENDENT_EXECUTOR); - // 添加超时处理 - executionFuture.orTimeout(EXECUTION_TIMEOUT_SECONDS, TimeUnit.SECONDS) - .whenComplete((result, error) -> { - if (error != null) { - if (error instanceof TimeoutException) { - String timeoutMsg = "JavaScript执行超时(超过" + EXECUTION_TIMEOUT_SECONDS + "秒),可能存在无限循环"; - playgroundLogger.errorJava(timeoutMsg); - log.error(timeoutMsg); - promise.fail(new RuntimeException(timeoutMsg)); - } else { - Throwable cause = error.getCause(); - promise.fail(cause != null ? cause : error); - } + // 创建超时任务,强制取消执行 + ScheduledFuture timeoutTask = TIMEOUT_SCHEDULER.schedule(() -> { + if (!executionFuture.isDone()) { + executionFuture.cancel(true); // 强制中断执行线程 + playgroundLogger.errorJava("执行超时,已强制中断"); + log.warn("JavaScript执行超时,已强制取消"); + } + }, EXECUTION_TIMEOUT_SECONDS, TimeUnit.SECONDS); + + // 处理执行结果 + executionFuture.whenComplete((result, error) -> { + // 取消超时任务 + timeoutTask.cancel(false); + + if (error != null) { + if (error instanceof CancellationException) { + String timeoutMsg = "JavaScript执行超时(超过" + EXECUTION_TIMEOUT_SECONDS + "秒),已强制中断"; + playgroundLogger.errorJava(timeoutMsg); + log.error(timeoutMsg); + promise.fail(new RuntimeException(timeoutMsg)); } else { - promise.complete(result); + Throwable cause = error.getCause(); + promise.fail(cause != null ? cause : error); } - }); + } else { + promise.complete(result); + } + }); return promise.future(); } diff --git a/parser/src/main/resources/py/123.py b/parser/src/main/resources/py/123.py new file mode 100644 index 0000000..8102a88 --- /dev/null +++ b/parser/src/main/resources/py/123.py @@ -0,0 +1,362 @@ +import requests +import re +import sys +import json +import time +import random +import zlib + +def get_timestamp(): + """获取当前时间戳(毫秒)""" + return str(int(time.time() * 1000)) + +def crc32(data): + """计算CRC32并转换为16进制""" + crc = zlib.crc32(data.encode()) & 0xffffffff + return format(crc, '08x') + +def hex_to_int(hex_str): + """16进制转10进制""" + return int(hex_str, 16) + +def encode123(url, way, version, timestamp): + """ + 123盘的URL加密算法 + 参考C++代码中的encode123函数 + """ + # 生成随机数 + a = int(10000000 * random.randint(1, 10000000) / 10000) + + # 字符映射表 + u = "adefghlmyijnopkqrstubcvwsz" + + # 将时间戳转换为时间格式 + time_long = int(timestamp) // 1000 + time_struct = time.localtime(time_long) + time_str = time.strftime("%Y%m%d%H%M", time_struct) + + # 根据时间字符串生成g + g = "" + for char in time_str: + digit = int(char) + if digit == 0: + g += u[0] + else: + # 修正:数字1对应索引0,数字2对应索引1,以此类推 + g += u[digit - 1] + + # 计算y值(CRC32的十进制) + y = str(hex_to_int(crc32(g))) + + # 计算最终的CRC32 + final_crc_input = f"{time_long}|{a}|{url}|{way}|{version}|{y}" + final_crc = str(hex_to_int(crc32(final_crc_input))) + + # 返回加密后的URL参数 + return f"?{y}={time_long}-{a}-{final_crc}" + +def login_123pan(username, password): + """登录123盘获取token""" + print(f"🔐 正在登录账号: {username}") + + login_data = { + "passport": username, + "password": password, + "remember": True + } + + try: + response = requests.post( + "https://login.123pan.com/api/user/sign_in", + json=login_data, + timeout=30 + ) + result = response.json() + + if result.get('code') == 200: + token = result.get('data', {}).get('token', '') + print(f"✅ 登录成功!") + return token + else: + error_msg = result.get('message', '未知错误') + print(f"❌ 登录失败: {error_msg}") + return None + except Exception as e: + print(f"❌ 登录请求失败: {e}") + return None + +def get_share_info(share_key, password=''): + """获取分享信息(不需要登录)""" + headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', + 'Referer': 'https://www.123pan.com/', + 'Origin': 'https://www.123pan.com', + } + + api_url = f"https://www.123pan.com/b/api/share/get?limit=100&next=1&orderBy=share_id&orderDirection=desc&shareKey={share_key}&SharePwd={password}&ParentFileId=0&Page=1" + + try: + response = requests.get(api_url, headers=headers, timeout=30) + return response.json() + except Exception as e: + print(f"❌ 获取分享信息失败: {e}") + return None + +def get_download_url_android(file_info, token): + """ + 使用Android平台API获取下载链接(关键方法) + 参考C++代码中的逻辑 + """ + # 🔥 关键:使用Android平台的请求头 + headers = { + 'App-Version': '55', + 'platform': 'android', + 'Authorization': f'Bearer {token}', + 'User-Agent': 'Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36', + 'Content-Type': 'application/json', + } + + # 构建请求数据 + post_data = { + 'driveId': 0, + 'etag': file_info.get('Etag', ''), + 'fileId': file_info.get('FileId'), + 'fileName': file_info.get('FileName', ''), + 's3keyFlag': file_info.get('S3KeyFlag', ''), + 'size': file_info.get('Size'), + 'type': 0 + } + + # 🔥 关键:使用encode123加密URL参数 + timestamp = get_timestamp() + encrypted_params = encode123('/b/api/file/download_info', 'android', '55', timestamp) + api_url = f"https://www.123pan.com/b/api/file/download_info{encrypted_params}" + + print(f" 📡 API URL: {api_url[:80]}...") + + try: + response = requests.post(api_url, json=post_data, headers=headers, timeout=30) + result = response.json() + + print(f" 📥 API响应: code={result.get('code')}, message={result.get('message', 'N/A')}") + + if result.get('code') == 0 and 'data' in result: + download_url = result['data'].get('DownloadUrl') or result['data'].get('DownloadURL') + return download_url + else: + error_msg = result.get('message', '未知错误') + print(f" ✗ API返回错误: {error_msg}") + return None + except Exception as e: + print(f" ✗ 请求失败: {e}") + import traceback + traceback.print_exc() + return None + +def start(link, password='', username='', user_password=''): + """主函数:解析123盘分享链接""" + result = { + 'code': 200, + 'data': [], + 'need_login': False + } + + # 提取 Share_Key + patterns = [ + r'/s/(.*?)\.html', + r'/s/([^/\s]+)', + ] + + share_key = None + for pattern in patterns: + matches = re.findall(pattern, link) + if matches: + share_key = matches[0] + break + + if not share_key: + return { + "code": 201, + "message": "分享地址错误,无法提取分享密钥" + } + + print(f"📌 分享密钥: {share_key}") + + # 如果提供了账号密码,先登录 + token = None + if username and user_password: + token = login_123pan(username, user_password) + if not token: + return { + "code": 201, + "message": "登录失败" + } + else: + print("⚠️ 未提供登录信息,某些文件可能无法下载") + + # 获取分享信息 + print(f"\n📂 正在获取文件列表...") + share_data = get_share_info(share_key, password) + + if not share_data or share_data.get('code') != 0: + error_msg = share_data.get('message', '未知错误') if share_data else '请求失败' + return { + "code": 201, + "message": f"获取分享信息失败: {error_msg}" + } + + # 获取文件列表 + if 'data' not in share_data or 'InfoList' not in share_data['data']: + return { + "code": 201, + "message": "返回数据格式错误" + } + + info_list = share_data['data']['InfoList'] + length = len(info_list) + + print(f"📁 找到 {length} 个项目\n") + + # 遍历文件列表 + for i, file_info in enumerate(info_list): + file_type = file_info.get('Type', 0) + file_name = file_info.get('FileName', '') + + # 跳过文件夹 + if file_type != 0: + print(f"[{i+1}/{length}] 跳过文件夹: {file_name}") + continue + + print(f"[{i+1}/{length}] 正在解析: {file_name}") + + if not token: + print(f" ⚠️ 需要登录才能获取下载链接") + result['need_login'] = True + continue + + # 🔥 使用Android平台API获取下载链接 + print(f" 🤖 使用Android平台API...") + download_url = get_download_url_android(file_info, token) + + if download_url: + result['data'].append({ + "Name": file_name, + "Size": file_info.get('Size', 0), + "DownloadURL": download_url + }) + print(f" ✓ 成功获取直链\n") + else: + print(f" ✗ 获取失败\n") + + return result + +def format_size(size_bytes): + """格式化文件大小""" + for unit in ['B', 'KB', 'MB', 'GB', 'TB']: + if size_bytes < 1024.0: + return f"{size_bytes:.2f} {unit}" + size_bytes /= 1024.0 + return f"{size_bytes:.2f} PB" + +def main(): + """主程序入口""" + if len(sys.argv) < 2: + print("=" * 80) + print(" 123盘直链解析工具 v3.0") + print("=" * 80) + print("\n📖 使用方法:") + print(" python 123.py <分享链接> [选项]") + print("\n⚙️ 选项:") + print(" --pwd <密码> 分享密码(如果有)") + print(" --user <账号> 123盘账号") + print(" --pass <密码> 123盘密码") + print("\n💡 示例:") + print(' # 需要登录的分享(推荐)') + print(' python 123.py "https://www.123pan.com/s/xxxxx" --user "账号" --pass "密码"') + print() + print(' # 有分享密码') + print(' python 123.py "https://www.123pan.com/s/xxxxx" --pwd "分享密码" --user "账号" --pass "密码"') + print("\n✨ 特性:") + print(" • 使用Android平台API(完全绕过限制)") + print(" • 使用123盘加密算法(encode123)") + print(" • 支持账号密码登录") + print(" • 无地区限制,无流量限制") + print("=" * 80) + sys.exit(1) + + link = sys.argv[1] + password = '' + username = '' + user_password = '' + + # 解析参数 + i = 2 + while i < len(sys.argv): + if sys.argv[i] == '--pwd' and i + 1 < len(sys.argv): + password = sys.argv[i + 1] + i += 2 + elif sys.argv[i] == '--user' and i + 1 < len(sys.argv): + username = sys.argv[i + 1] + i += 2 + elif sys.argv[i] == '--pass' and i + 1 < len(sys.argv): + user_password = sys.argv[i + 1] + i += 2 + else: + i += 1 + + print("\n" + "=" * 80) + print(" 开始解析分享链接") + print("=" * 80) + print(f"🔗 链接: {link}") + if password: + print(f"🔐 分享密码: {password}") + if username: + print(f"👤 登录账号: {username}") + print("=" * 80) + print() + + result = start(link, password, username, user_password) + + if result['code'] != 200: + print(f"\n❌ 错误: {result['message']}") + sys.exit(1) + + if not result['data']: + print("\n⚠️ 没有成功获取到任何文件的直链") + + if result.get('need_login'): + print("\n🔒 该分享需要登录才能下载") + print("\n请使用以下命令:") + print(f' python 123.py "{link}" --user "你的账号" --pass "你的密码"') + sys.exit(1) + + print("\n" + "=" * 80) + print(" ✅ 解析成功!") + print("=" * 80) + + for idx, file in enumerate(result['data'], 1): + print(f"\n📄 文件 {idx}:") + print(f" 名称: {file['Name']}") + print(f" 大小: {format_size(file['Size'])} ({file['Size']:,} 字节)") + print(f" 直链: {file['DownloadURL']}") + print("-" * 80) + + print("\n💾 下载方法:") + print("\n 使用curl命令:") + for file in result['data']: + safe_name = file['Name'].replace('"', '\\"') + print(f' curl -L -o "{safe_name}" "{file["DownloadURL"]}"') + + print("\n 使用aria2c命令(推荐,多线程):") + for file in result['data']: + safe_name = file['Name'].replace('"', '\\"') + print(f' aria2c -x 16 -s 16 -o "{safe_name}" "{file["DownloadURL"]}"') + + print("\n💡 提示:") + print(" • 使用Android平台API,无地区限制") + print(" • 直链有效期通常为几小时") + print(" • 推荐使用 aria2c 下载(速度最快)") + print() + +if __name__ == "__main__": + main() diff --git a/web-front/MONACO_EDITOR_NPM.md b/web-front/MONACO_EDITOR_NPM.md new file mode 100644 index 0000000..b3fa91d --- /dev/null +++ b/web-front/MONACO_EDITOR_NPM.md @@ -0,0 +1,174 @@ +# Monaco Editor NPM包配置说明 + +## ✅ 已完成的配置 + +### 1. NPM包安装 +已在 `package.json` 中安装: +- `monaco-editor`: ^0.45.0 - Monaco Editor核心包 +- `@monaco-editor/loader`: ^1.4.0 - Monaco Editor加载器 +- `monaco-editor-webpack-plugin`: ^7.1.1 - Webpack打包插件(devDependencies) + +### 2. Webpack配置 +在 `vue.config.js` 中已配置: +```javascript +new MonacoEditorPlugin({ + languages: ['javascript', 'typescript', 'json'], + features: ['coreCommands', 'find'], + publicPath: process.env.NODE_ENV === 'production' ? './' : '/' +}) +``` + +### 3. 组件配置 +在 `MonacoEditor.vue` 和 `Playground.vue` 中已配置: +```javascript +// 配置loader使用本地打包的文件,而不是CDN +if (loader.config) { + const vsPath = process.env.NODE_ENV === 'production' + ? './js/vs' // 生产环境使用相对路径 + : '/js/vs'; // 开发环境使用绝对路径 + + loader.config({ + paths: { + vs: vsPath + } + }); +} +``` + +--- + +## 🔍 工作原理 + +### 打包流程 +1. `monaco-editor-webpack-plugin` 在构建时将 Monaco Editor 文件打包到 `js/vs/` 目录 +2. `@monaco-editor/loader` 通过配置的路径加载本地文件 +3. 不再从 CDN(如 `https://cdn.jsdelivr.net`)加载 + +### 文件结构(构建后) +``` +nfd-front/ + ├── js/ + │ └── vs/ + │ ├── editor/ + │ ├── loader/ + │ ├── base/ + │ └── ... + └── index.html +``` + +--- + +## 🧪 验证方法 + +### 1. 检查网络请求 +打开浏览器开发者工具 → Network标签: +- ✅ 应该看到请求 `/js/vs/...` 或 `./js/vs/...` +- ❌ 不应该看到请求 `cdn.jsdelivr.net` 或其他CDN域名 + +### 2. 检查构建产物 +```bash +cd web-front +npm run build +ls -la nfd-front/js/vs/ +``` +应该看到 Monaco Editor 的文件被打包到本地。 + +### 3. 离线测试 +1. 断开网络连接 +2. 访问 Playground 页面 +3. ✅ 编辑器应该正常加载(因为使用本地文件) +4. ❌ 如果使用CDN,编辑器会加载失败 + +--- + +## 📝 修改的文件 + +1. ✅ `web-front/src/components/MonacoEditor.vue` + - 添加了 `loader.config()` 配置,明确使用本地路径 + +2. ✅ `web-front/src/views/Playground.vue` + - 在 `initMonacoTypes()` 中添加了相同的配置 + +3. ✅ `web-front/vue.config.js` + - 添加了 `publicPath` 配置,确保路径正确 + +--- + +## 🚀 部署 + +### 开发环境 +```bash +cd web-front +npm install # 确保依赖已安装 +npm run serve +``` +访问 `http://127.0.0.1:6444/playground`,编辑器应该从本地加载。 + +### 生产环境 +```bash +cd web-front +npm run build +``` +构建后,Monaco Editor 文件会打包到 `nfd-front/js/vs/` 目录。 + +--- + +## ⚠️ 注意事项 + +### 1. 文件大小 +Monaco Editor 打包后会增加构建产物大小(约2-3MB),但这是正常的。 + +### 2. 首次加载 +- 开发环境:文件从 webpack dev server 加载 +- 生产环境:文件从本地 `js/vs/` 目录加载 + +### 3. 缓存 +浏览器会缓存 Monaco Editor 文件,更新后可能需要清除缓存。 + +--- + +## 🔧 故障排查 + +### 问题:编辑器无法加载 +**检查**: +1. 确认 `npm install` 已执行 +2. 检查浏览器控制台是否有错误 +3. 检查 Network 标签,确认文件路径是否正确 + +### 问题:仍然从CDN加载 +**解决**: +1. 清除浏览器缓存 +2. 确认 `loader.config()` 已正确配置 +3. 检查 `vue.config.js` 中的 `publicPath` 配置 + +### 问题:构建后路径错误 +**解决**: +- 检查 `publicPath` 配置 +- 确认生产环境的相对路径 `./js/vs` 正确 + +--- + +## ✅ 优势 + +1. **离线可用** - 不依赖外部CDN +2. **加载速度** - 本地文件通常比CDN更快 +3. **版本控制** - 使用固定版本的Monaco Editor +4. **安全性** - 不依赖第三方CDN服务 +5. **稳定性** - CDN故障不影响使用 + +--- + +**配置状态**: ✅ 已完成 +**验证状态**: ⚠️ 待测试 +**建议**: 运行 `npm run build` 并检查构建产物 + + + + + + + + + + + diff --git a/web-front/package.json b/web-front/package.json index 1881029..b26be00 100644 --- a/web-front/package.json +++ b/web-front/package.json @@ -19,7 +19,6 @@ "monaco-editor": "^0.45.0", "qrcode": "^1.5.4", "splitpanes": "^4.0.4", - "typescript": "^5.9.3", "vue": "^3.5.12", "vue-clipboard3": "^2.0.0", "vue-router": "^4.5.1", diff --git a/web-front/src/utils/playgroundApi.js b/web-front/src/utils/playgroundApi.js index 869d0bb..44130b2 100644 --- a/web-front/src/utils/playgroundApi.js +++ b/web-front/src/utils/playgroundApi.js @@ -170,54 +170,4 @@ export const playgroundApi = { } }, - /** - * 保存TypeScript代码及其编译结果 - */ - async saveTypeScriptCode(parserId, tsCode, es5Code, compileErrors, compilerVersion, compileOptions, isValid) { - try { - const response = await axios.post('/v2/playground/typescript', { - parserId, - tsCode, - es5Code, - compileErrors, - compilerVersion, - compileOptions, - isValid - }); - return response.data; - } catch (error) { - throw new Error(error.response?.data?.error || error.response?.data?.msg || error.message || '保存TypeScript代码失败'); - } - }, - - /** - * 根据parserId获取TypeScript代码 - */ - async getTypeScriptCode(parserId) { - try { - const response = await axios.get(`/v2/playground/typescript/${parserId}`); - return response.data; - } catch (error) { - throw new Error(error.response?.data?.error || error.response?.data?.msg || error.message || '获取TypeScript代码失败'); - } - }, - - /** - * 更新TypeScript代码 - */ - async updateTypeScriptCode(parserId, tsCode, es5Code, compileErrors, compilerVersion, compileOptions, isValid) { - try { - const response = await axios.put(`/v2/playground/typescript/${parserId}`, { - tsCode, - es5Code, - compileErrors, - compilerVersion, - compileOptions, - isValid - }); - return response.data; - } catch (error) { - throw new Error(error.response?.data?.error || error.response?.data?.msg || error.message || '更新TypeScript代码失败'); - } - } }; diff --git a/web-front/src/utils/tsCompiler.js b/web-front/src/utils/tsCompiler.js deleted file mode 100644 index f117947..0000000 --- a/web-front/src/utils/tsCompiler.js +++ /dev/null @@ -1,167 +0,0 @@ -import * as ts from 'typescript'; - -/** - * TypeScript编译器工具类 - * 用于在浏览器中将TypeScript代码编译为ES5 JavaScript - */ - -/** - * 编译TypeScript代码为ES5 JavaScript - * @param {string} sourceCode - TypeScript源代码 - * @param {string} fileName - 文件名(默认为script.ts) - * @returns {Object} 编译结果 { success: boolean, code: string, errors: Array } - */ -export function compileToES5(sourceCode, fileName = 'script.ts') { - try { - // 编译选项 - const compilerOptions = { - target: ts.ScriptTarget.ES5, // 目标版本:ES5 - module: ts.ModuleKind.None, // 不使用模块系统 - lib: ['lib.es5.d.ts', 'lib.dom.d.ts'], // 包含ES5和DOM类型定义 - removeComments: false, // 保留注释 - noEmitOnError: true, // 有错误时不生成代码 - noImplicitAny: false, // 允许隐式any类型 - strictNullChecks: false, // 不进行严格的null检查 - suppressImplicitAnyIndexErrors: true, // 抑制隐式any索引错误 - downlevelIteration: true, // 支持ES5迭代器降级 - esModuleInterop: true, // 启用ES模块互操作性 - allowJs: true, // 允许编译JavaScript文件 - checkJs: false // 不检查JavaScript文件 - }; - - // 执行编译 - const result = ts.transpileModule(sourceCode, { - compilerOptions, - fileName, - reportDiagnostics: true - }); - - // 检查是否有诊断信息(错误/警告) - const diagnostics = result.diagnostics || []; - const errors = diagnostics.map(diagnostic => { - const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'); - let location = ''; - if (diagnostic.file && diagnostic.start !== undefined) { - const { line, character } = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start); - location = `(${line + 1},${character + 1})`; - } - return { - message, - location, - category: ts.DiagnosticCategory[diagnostic.category], - code: diagnostic.code - }; - }); - - // 过滤出真正的错误(不包括警告) - const realErrors = errors.filter(e => e.category === 'Error'); - - return { - success: realErrors.length === 0, - code: result.outputText || '', - errors: errors, - hasWarnings: errors.some(e => e.category === 'Warning'), - sourceMap: result.sourceMapText - }; - } catch (error) { - return { - success: false, - code: '', - errors: [{ - message: error.message || '编译失败', - location: '', - category: 'Error', - code: 0 - }] - }; - } -} - -/** - * 检查代码是否为TypeScript代码 - * 简单的启发式检查,看是否包含TypeScript特有的语法 - * @param {string} code - 代码字符串 - * @returns {boolean} 是否为TypeScript代码 - */ -export function isTypeScriptCode(code) { - if (!code || typeof code !== 'string') { - return false; - } - - // TypeScript特有的语法模式 - const tsPatterns = [ - /:\s*(string|number|boolean|any|void|never|unknown|object)\b/, // 类型注解 - /interface\s+\w+/, // interface声明 - /type\s+\w+\s*=/, // type别名 - /enum\s+\w+/, // enum声明 - /<\w+>/, // 泛型 - /implements\s+\w+/, // implements关键字 - /as\s+(string|number|boolean|any|const)/, // as类型断言 - /public|private|protected|readonly/, // 访问修饰符 - /:\s*\w+\[\]/, // 数组类型注解 - /\?\s*:/ // 可选属性 - ]; - - // 如果匹配任何TypeScript特有模式,则认为是TypeScript代码 - return tsPatterns.some(pattern => pattern.test(code)); -} - -/** - * 格式化编译错误信息 - * @param {Array} errors - 错误数组 - * @returns {string} 格式化后的错误信息 - */ -export function formatCompileErrors(errors) { - if (!errors || errors.length === 0) { - return ''; - } - - return errors.map((error, index) => { - const prefix = `[${error.category}]`; - const location = error.location ? ` ${error.location}` : ''; - const code = error.code ? ` (TS${error.code})` : ''; - return `${index + 1}. ${prefix}${location}${code}: ${error.message}`; - }).join('\n'); -} - -/** - * 验证编译后的代码是否为有效的ES5 - * @param {string} code - 编译后的代码 - * @returns {Object} { valid: boolean, error: string } - */ -export function validateES5Code(code) { - try { - // 尝试使用Function构造函数验证语法 - // eslint-disable-next-line no-new-func - new Function(code); - return { valid: true, error: null }; - } catch (error) { - return { valid: false, error: error.message }; - } -} - -/** - * 提取代码中的元数据注释 - * @param {string} code - 代码字符串 - * @returns {Object} 元数据对象 - */ -export function extractMetadata(code) { - const metadata = {}; - const metaRegex = /\/\/\s*@(\w+)\s+(.+)/g; - let match; - - while ((match = metaRegex.exec(code)) !== null) { - const [, key, value] = match; - metadata[key] = value.trim(); - } - - return metadata; -} - -export default { - compileToES5, - isTypeScriptCode, - formatCompileErrors, - validateES5Code, - extractMetadata -}; diff --git a/web-front/src/views/Playground.vue b/web-front/src/views/Playground.vue index 5fe85b4..fb1211f 100644 --- a/web-front/src/views/Playground.vue +++ b/web-front/src/views/Playground.vue @@ -57,11 +57,10 @@
JS解析器演练场 - - - JavaScript - TypeScript - + + + JavaScript (ES5) +
@@ -536,7 +535,6 @@ import 'splitpanes/dist/splitpanes.css'; import MonacoEditor from '@/components/MonacoEditor.vue'; import { playgroundApi } from '@/utils/playgroundApi'; import { configureMonacoTypes, loadTypesFromApi } from '@/utils/monacoTypes'; -import { compileToES5, isTypeScriptCode, formatCompileErrors } from '@/utils/tsCompiler'; import JsonViewer from 'vue3-json-viewer'; export default { @@ -550,15 +548,11 @@ export default { setup() { // 语言常量 const LANGUAGE = { - JAVASCRIPT: 'JavaScript', - TYPESCRIPT: 'TypeScript' + JAVASCRIPT: 'JavaScript' }; const editorRef = ref(null); const jsCode = ref(''); - const codeLanguage = ref(LANGUAGE.JAVASCRIPT); // 新增:代码语言选择 - const compiledES5Code = ref(''); // 新增:编译后的ES5代码 - const compileStatus = ref({ success: true, errors: [] }); // 新增:编译状态 // ===== 加载和认证状态 ===== const loading = ref(true); @@ -699,90 +693,6 @@ function parseById(shareLinkInfo, http, logger) { return "https://example.com/download?id=" + fileId; }`; - // TypeScript示例代码模板 - const exampleTypeScriptCode = `// ==UserScript== -// @name TypeScript示例解析器 -// @type ts_example_parser -// @displayName TypeScript示例网盘 -// @description 使用TypeScript实现的示例解析器 -// @match https?://example\.com/s/(?\\w+) -// @author yourname -// @version 1.0.0 -// ==/UserScript== - -/** - * 解析单个文件下载链接 - * @param shareLinkInfo - 分享链接信息 - * @param http - HTTP客户端 - * @param logger - 日志对象 - * @returns 下载链接 - */ -async function parse( - shareLinkInfo: any, - http: any, - logger: any -): Promise { - const url: string = shareLinkInfo.getShareUrl(); - logger.info(\`开始解析: \${url}\`); - - // 使用fetch API (已在后端实现polyfill) - try { - const response = await fetch(url); - if (!response.ok) { - throw new Error(\`请求失败: \${response.status}\`); - } - - const html: string = await response.text(); - - // 这里添加你的解析逻辑 - // 例如:使用正则表达式提取下载链接 - const match = html.match(/download-url="([^"]+)"/); - if (match) { - return match[1]; - } - - return "https://example.com/download/file.zip"; - } catch (error: any) { - logger.error(\`解析失败: \${error.message}\`); - throw error; - } -} - -/** - * 解析文件列表(可选) - */ -async function parseFileList( - shareLinkInfo: any, - http: any, - logger: any -): Promise { - const dirId: string = shareLinkInfo.getOtherParam("dirId") || "0"; - logger.info(\`解析文件列表,目录ID: \${dirId}\`); - - const fileList: any[] = []; - - // 这里添加你的文件列表解析逻辑 - - return fileList; -} - -/** - * 根据文件ID获取下载链接(可选) - */ -async function parseById( - shareLinkInfo: any, - http: any, - logger: any -): Promise { - const paramJson = shareLinkInfo.getOtherParam("paramJson"); - const fileId: string = paramJson.fileId; - logger.info(\`根据ID解析: \${fileId}\`); - - // 这里添加你的按ID解析逻辑 - - return \`https://example.com/download?id=\${fileId}\`; -}`; - // 编辑器主题 const editorTheme = computed(() => { return isDarkMode.value ? 'vs-dark' : 'vs'; @@ -880,15 +790,6 @@ async function parseById( jsCode.value = exampleCode; } - // 加载保存的语言选择 - const savedLanguage = localStorage.getItem('playground_language'); - if (savedLanguage) { - codeLanguage.value = savedLanguage; - } - - setProgress(40, '准备加载TypeScript编译器...'); - await new Promise(resolve => setTimeout(resolve, 100)); - setProgress(50, '初始化Monaco Editor类型定义...'); await initMonacoTypes(); @@ -965,8 +866,8 @@ async function parseById( // 加载示例代码 const loadTemplate = () => { - jsCode.value = codeLanguage.value === LANGUAGE.TYPESCRIPT ? exampleTypeScriptCode : exampleCode; - ElMessage.success(`已加载${codeLanguage.value}示例代码`); + jsCode.value = exampleCode; + ElMessage.success('已加载JavaScript示例代码'); }; // 格式化代码 @@ -1005,59 +906,6 @@ async function parseById( }; // 语言切换处理 - const onLanguageChange = (newLanguage) => { - console.log('切换语言:', newLanguage); - // 保存当前语言选择 - localStorage.setItem('playground_language', newLanguage); - - // 如果切换到TypeScript,尝试编译当前代码 - if (newLanguage === 'TypeScript' && jsCode.value.trim()) { - compileTypeScriptCode(); - } - }; - - // 编译TypeScript代码 - const compileTypeScriptCode = () => { - if (!jsCode.value.trim()) { - compiledES5Code.value = ''; - compileStatus.value = { success: true, errors: [] }; - return; - } - - try { - const result = compileToES5(jsCode.value); - compiledES5Code.value = result.code; - compileStatus.value = { - success: result.success, - errors: result.errors || [], - hasWarnings: result.hasWarnings - }; - - if (!result.success) { - const errorMsg = formatCompileErrors(result.errors); - ElMessage.error({ - message: '编译失败,请检查代码语法\n' + errorMsg, - duration: 5000, - showClose: true - }); - } else if (result.hasWarnings) { - ElMessage.warning({ - message: '编译成功,但存在警告', - duration: 3000 - }); - } else { - ElMessage.success('编译成功'); - } - } catch (error) { - console.error('编译错误:', error); - compileStatus.value = { - success: false, - errors: [{ message: error.message || '编译失败' }] - }; - ElMessage.error('编译失败: ' + error.message); - } - }; - // 执行测试 const executeTest = async () => { if (!jsCode.value.trim()) { @@ -1101,67 +949,9 @@ async function parseById( testResult.value = null; consoleLogs.value = []; // 清空控制台 - // 确定要执行的代码(TypeScript需要先编译) - let codeToExecute = jsCode.value; - - // 优先使用显式语言选择,如果是JavaScript模式但代码是TS,给出提示 - if (codeLanguage.value === LANGUAGE.TYPESCRIPT) { - // TypeScript模式:始终编译 - try { - const compileResult = compileToES5(jsCode.value); - - if (!compileResult.success) { - testing.value = false; - const errorMsg = formatCompileErrors(compileResult.errors); - ElMessage.error({ - message: 'TypeScript编译失败,请修复错误后再试\n' + errorMsg, - duration: 5000, - showClose: true - }); - testResult.value = { - success: false, - error: 'TypeScript编译失败', - stackTrace: errorMsg, - logs: [], - executionTime: 0 - }; - return; - } - - // 使用编译后的ES5代码 - codeToExecute = compileResult.code; - compiledES5Code.value = compileResult.code; - compileStatus.value = { - success: true, - errors: compileResult.errors || [], - hasWarnings: compileResult.hasWarnings - }; - - if (compileResult.hasWarnings) { - ElMessage.warning('编译成功,但存在警告'); - } - } catch (error) { - testing.value = false; - ElMessage.error('TypeScript编译失败: ' + error.message); - testResult.value = { - success: false, - error: 'TypeScript编译失败: ' + error.message, - logs: [], - executionTime: 0 - }; - return; - } - } else if (isTypeScriptCode(jsCode.value)) { - // JavaScript模式但检测到TypeScript语法:给出提示 - ElMessage.warning({ - message: '检测到TypeScript语法,建议切换到TypeScript模式', - duration: 3000 - }); - } - try { const result = await playgroundApi.testScript( - codeToExecute, // 使用编译后的代码或原始JS代码 + jsCode.value, // 直接使用JavaScript代码 testParams.value.shareUrl, testParams.value.pwd, testParams.value.method @@ -1610,9 +1400,6 @@ curl "${baseUrl}/json/parser?url=${encodeURIComponent(exampleUrl)}" LANGUAGE, editorRef, jsCode, - codeLanguage, - compiledES5Code, - compileStatus, testParams, testResult, testing, @@ -1635,8 +1422,6 @@ curl "${baseUrl}/json/parser?url=${encodeURIComponent(exampleUrl)}" isMobile, updateIsMobile, onCodeChange, - onLanguageChange, - compileTypeScriptCode, loadTemplate, formatCode, saveCode, diff --git a/web-service/src/main/java/cn/qaiu/lz/AppMain.java b/web-service/src/main/java/cn/qaiu/lz/AppMain.java index 859ee4c..b049ac8 100644 --- a/web-service/src/main/java/cn/qaiu/lz/AppMain.java +++ b/web-service/src/main/java/cn/qaiu/lz/AppMain.java @@ -5,7 +5,12 @@ import cn.qaiu.db.pool.JDBCPoolInit; import cn.qaiu.lz.common.cache.CacheConfigLoader; import cn.qaiu.lz.common.interceptorImpl.RateLimiter; import cn.qaiu.lz.web.config.PlaygroundConfig; +import cn.qaiu.lz.web.service.DbService; +import cn.qaiu.parser.custom.CustomParserConfig; +import cn.qaiu.parser.custom.CustomParserRegistry; +import cn.qaiu.parser.customjs.JsScriptMetadataParser; import cn.qaiu.vx.core.Deploy; +import cn.qaiu.vx.core.util.AsyncServiceUtil; import cn.qaiu.vx.core.util.ConfigConstant; import cn.qaiu.vx.core.util.VertxHolder; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; @@ -13,6 +18,7 @@ import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; import io.vertx.core.json.jackson.DatabindCodec; import io.vertx.core.shareddata.LocalMap; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.time.DateFormatUtils; import java.util.Date; @@ -26,6 +32,7 @@ import static cn.qaiu.vx.core.util.ConfigConstant.LOCAL; *
Create date 2021-05-08 13:00:01 * @author qaiu yyzy */ +@Slf4j public class AppMain { public static void main(String[] args) { @@ -55,6 +62,10 @@ public class AppMain { VertxHolder.getVertxInstance().setTimer(1000, id -> { System.out.println(DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss.SSS")); System.out.println("数据库连接成功"); + + // 加载演练场解析器 + loadPlaygroundParsers(); + String addr = jsonObject.getJsonObject(ConfigConstant.SERVER).getString("domainName"); System.out.println("启动成功: \n本地服务地址: " + addr); }); @@ -93,4 +104,40 @@ public class AppMain { // 演练场配置 PlaygroundConfig.loadFromJson(jsonObject); } + + /** + * 在启动时加载所有已发布的演练场解析器 + */ + private static void loadPlaygroundParsers() { + DbService dbService = AsyncServiceUtil.getAsyncServiceInstance(DbService.class); + + dbService.getPlaygroundParserList().onSuccess(result -> { + JsonArray parsers = result.getJsonArray("data"); + if (parsers != null) { + int loadedCount = 0; + for (int i = 0; i < parsers.size(); i++) { + JsonObject parser = parsers.getJsonObject(i); + + // 只注册已启用的解析器 + if (parser.getBoolean("enabled", false)) { + try { + String jsCode = parser.getString("jsCode"); + CustomParserConfig config = JsScriptMetadataParser.parseScript(jsCode); + CustomParserRegistry.register(config); + loadedCount++; + log.info("已加载演练场解析器: {} ({})", + config.getDisplayName(), config.getType()); + } catch (Exception e) { + log.error("加载演练场解析器失败: {}", parser.getString("name"), e); + } + } + } + log.info("演练场解析器加载完成,共加载 {} 个解析器", loadedCount); + } else { + log.info("未找到已发布的演练场解析器"); + } + }).onFailure(e -> { + log.error("加载演练场解析器列表失败", e); + }); + } } diff --git a/web-service/src/main/java/cn/qaiu/lz/web/controller/PlaygroundApi.java b/web-service/src/main/java/cn/qaiu/lz/web/controller/PlaygroundApi.java index 3abdebd..51c8e8e 100644 --- a/web-service/src/main/java/cn/qaiu/lz/web/controller/PlaygroundApi.java +++ b/web-service/src/main/java/cn/qaiu/lz/web/controller/PlaygroundApi.java @@ -5,6 +5,8 @@ import cn.qaiu.lz.web.config.PlaygroundConfig; import cn.qaiu.lz.web.model.PlaygroundTestResp; import cn.qaiu.lz.web.service.DbService; import cn.qaiu.parser.ParserCreate; +import cn.qaiu.parser.custom.CustomParserConfig; +import cn.qaiu.parser.custom.CustomParserRegistry; import cn.qaiu.parser.customjs.JsPlaygroundExecutor; import cn.qaiu.parser.customjs.JsPlaygroundLogger; import cn.qaiu.parser.customjs.JsScriptMetadataParser; @@ -30,6 +32,8 @@ import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.stream.Collectors; /** @@ -178,6 +182,32 @@ public class PlaygroundApi { .build())); return promise.future(); } + + // ===== 新增:验证URL匹配 ===== + try { + var config = JsScriptMetadataParser.parseScript(jsCode); + Pattern matchPattern = config.getMatchPattern(); + + if (matchPattern != null) { + Matcher matcher = matchPattern.matcher(shareUrl); + if (!matcher.matches()) { + promise.complete(JsonObject.mapFrom(PlaygroundTestResp.builder() + .success(false) + .error("分享链接与脚本的@match规则不匹配\n" + + "规则: " + matchPattern.pattern() + "\n" + + "链接: " + shareUrl) + .build())); + return promise.future(); + } + } + } catch (IllegalArgumentException e) { + promise.complete(JsonObject.mapFrom(PlaygroundTestResp.builder() + .success(false) + .error("解析脚本元数据失败: " + e.getMessage()) + .build())); + return promise.future(); + } + // ===== 验证结束 ===== // 验证方法类型 if (!"parse".equals(method) && !"parseFileList".equals(method) && !"parseById".equals(method)) { @@ -433,7 +463,18 @@ public class PlaygroundApi { parser.put("enabled", true); dbService.savePlaygroundParser(parser).onSuccess(result -> { - promise.complete(result); + // 保存成功后,立即注册到解析器系统 + try { + CustomParserRegistry.register(config); + log.info("已注册演练场解析器: {} ({})", displayName, type); + promise.complete(JsonResult.success("保存并注册成功").toJsonObject()); + } catch (Exception e) { + log.error("注册解析器失败", e); + // 虽然注册失败,但保存成功了,返回警告 + promise.complete(JsonResult.success( + "保存成功,但注册失败(重启服务后会自动加载): " + e.getMessage() + ).toJsonObject()); + } }).onFailure(e -> { log.error("保存解析器失败", e); promise.complete(JsonResult.error("保存失败: " + e.getMessage()).toJsonObject()); @@ -483,12 +524,14 @@ public class PlaygroundApi { // 解析元数据 try { var config = JsScriptMetadataParser.parseScript(jsCode); + String type = config.getType(); String displayName = config.getDisplayName(); String name = config.getMetadata().get("name"); String description = config.getMetadata().get("description"); String author = config.getMetadata().get("author"); String version = config.getMetadata().get("version"); String matchPattern = config.getMatchPattern() != null ? config.getMatchPattern().pattern() : null; + boolean enabled = body.getBoolean("enabled", true); JsonObject parser = new JsonObject(); parser.put("name", name); @@ -498,10 +541,29 @@ public class PlaygroundApi { parser.put("version", version); parser.put("matchPattern", matchPattern); parser.put("jsCode", jsCode); - parser.put("enabled", body.getBoolean("enabled", true)); + parser.put("enabled", enabled); dbService.updatePlaygroundParser(id, parser).onSuccess(result -> { - promise.complete(result); + // 更新成功后,重新注册解析器 + try { + if (enabled) { + // 先注销旧的(如果存在) + CustomParserRegistry.unregister(type); + // 重新注册新的 + CustomParserRegistry.register(config); + log.info("已重新注册演练场解析器: {} ({})", displayName, type); + } else { + // 禁用时注销 + CustomParserRegistry.unregister(type); + log.info("已注销演练场解析器: {}", type); + } + promise.complete(JsonResult.success("更新并重新注册成功").toJsonObject()); + } catch (Exception e) { + log.error("重新注册解析器失败", e); + promise.complete(JsonResult.success( + "更新成功,但注册失败(重启服务后会自动加载): " + e.getMessage() + ).toJsonObject()); + } }).onFailure(e -> { log.error("更新解析器失败", e); promise.complete(JsonResult.error("更新失败: " + e.getMessage()).toJsonObject()); @@ -528,7 +590,38 @@ public class PlaygroundApi { if (!checkAuth(ctx)) { return Future.succeededFuture(JsonResult.error("未授权访问").toJsonObject()); } - return dbService.deletePlaygroundParser(id); + + Promise promise = Promise.promise(); + + // 先获取解析器信息,用于注销 + dbService.getPlaygroundParserById(id).onSuccess(getResult -> { + if (getResult.getBoolean("success", false)) { + JsonObject parser = getResult.getJsonObject("data"); + String type = parser.getString("type"); + + // 删除数据库记录 + dbService.deletePlaygroundParser(id).onSuccess(deleteResult -> { + // 从注册表中注销 + try { + CustomParserRegistry.unregister(type); + log.info("已注销演练场解析器: {}", type); + } catch (Exception e) { + log.warn("注销解析器失败(可能未注册): {}", type, e); + } + promise.complete(deleteResult); + }).onFailure(e -> { + log.error("删除解析器失败", e); + promise.complete(JsonResult.error("删除失败: " + e.getMessage()).toJsonObject()); + }); + } else { + promise.complete(getResult); + } + }).onFailure(e -> { + log.error("获取解析器信息失败", e); + promise.complete(JsonResult.error("获取解析器信息失败: " + e.getMessage()).toJsonObject()); + }); + + return promise.future(); } /** diff --git a/web-service/src/test/resources/playground-dos-tests.http b/web-service/src/test/resources/playground-dos-tests.http new file mode 100644 index 0000000..57cc6e8 --- /dev/null +++ b/web-service/src/test/resources/playground-dos-tests.http @@ -0,0 +1,91 @@ +### 安全漏洞修复测试 - DoS攻击防护 +### +### 测试目标: +### 1. 验证代码长度限制(128KB) +### 2. 验证JavaScript执行超时(30秒) +### + +### 测试1: 正常代码执行(应该成功) +POST http://127.0.0.1:6400/v2/playground/test +Content-Type: application/json + +{ + "jsCode": "// ==UserScript==\n// @name 正常测试\n// @type normal_test\n// @displayName 正常\n// @match https://example\\.com/(?\\w+)\n// @author test\n// @version 1.0.0\n// ==/UserScript==\n\nfunction parse(shareLinkInfo, http, logger) {\n logger.info('正常执行');\n return 'https://example.com/download/file.zip';\n}", + "shareUrl": "https://example.com/test123", + "pwd": "", + "method": "parse" +} + +### + +### 测试2: 代码长度超过限制(应该失败并提示) +### 这个测试会创建一个超过128KB的代码 +POST http://127.0.0.1:6400/v2/playground/test +Content-Type: application/json + +{ + "jsCode": "// ==UserScript==\n// @name 长度测试\n// @type length_test\n// @displayName 长度\n// @match https://example\\.com/(?\\w+)\n// @author test\n// @version 1.0.0\n// ==/UserScript==\n\nfunction parse(shareLinkInfo, http, logger) {\n var data = 'x'.repeat(150000);\n return data;\n}", + "shareUrl": "https://example.com/test123", + "pwd": "", + "method": "parse" +} + +### + +### 测试3: 无限循环(应该在30秒后超时) +POST http://127.0.0.1:6400/v2/playground/test +Content-Type: application/json + +{ + "jsCode": "// ==UserScript==\n// @name 无限循环测试\n// @type infinite_loop_test\n// @displayName 无限循环\n// @match https://example\\.com/(?\\w+)\n// @author test\n// @version 1.0.0\n// ==/UserScript==\n\nfunction parse(shareLinkInfo, http, logger) {\n logger.info('开始无限循环...');\n while(true) {\n var x = 1 + 1;\n }\n return 'never reached';\n}", + "shareUrl": "https://example.com/test123", + "pwd": "", + "method": "parse" +} + +### + +### 测试4: 大数组内存炸弹(应该在30秒后超时或内存限制) +POST http://127.0.0.1:6400/v2/playground/test +Content-Type: application/json + +{ + "jsCode": "// ==UserScript==\n// @name 内存炸弹测试\n// @type memory_bomb_test\n// @displayName 内存炸弹\n// @match https://example\\.com/(?\\w+)\n// @author test\n// @version 1.0.0\n// ==/UserScript==\n\nfunction parse(shareLinkInfo, http, logger) {\n logger.info('创建大数组...');\n var arr = [];\n for(var i = 0; i < 10000000; i++) {\n arr.push('x'.repeat(1000));\n }\n logger.info('数组创建完成');\n return 'DONE';\n}", + "shareUrl": "https://example.com/test123", + "pwd": "", + "method": "parse" +} + +### + +### 测试5: 递归调用栈溢出 +POST http://127.0.0.1:6400/v2/playground/test +Content-Type: application/json + +{ + "jsCode": "// ==UserScript==\n// @name 栈溢出测试\n// @type stack_overflow_test\n// @displayName 栈溢出\n// @match https://example\\.com/(?\\w+)\n// @author test\n// @version 1.0.0\n// ==/UserScript==\n\nfunction boom() {\n return boom();\n}\n\nfunction parse(shareLinkInfo, http, logger) {\n logger.info('开始递归炸弹...');\n boom();\n return 'never reached';\n}", + "shareUrl": "https://example.com/test123", + "pwd": "", + "method": "parse" +} + +### + +### 测试6: 保存解析器 - 验证代码长度限制 +POST http://127.0.0.1:6400/v2/playground/parsers +Content-Type: application/json + +{ + "jsCode": "// ==UserScript==\n// @name 正常解析器\n// @type normal_parser\n// @displayName 正常解析器\n// @match https://example\\.com/(?\\w+)\n// @author test\n// @version 1.0.0\n// ==/UserScript==\n\nfunction parse(shareLinkInfo, http, logger) {\n return 'https://example.com/download/file.zip';\n}\n\nfunction parseFileList(shareLinkInfo, http, logger) {\n return [];\n}\n\nfunction parseById(shareLinkInfo, http, logger) {\n return 'https://example.com/download/file.zip';\n}" +} + +### + +### 测试结果期望: +### 1. 测试1 - 应该成功返回结果 +### 2. 测试2 - 应该返回错误:"代码长度超过限制" +### 3. 测试3 - 应该在30秒后返回超时错误:"JavaScript执行超时" +### 4. 测试4 - 应该在30秒后返回超时错误或内存错误 +### 5. 测试5 - 应该返回堆栈溢出错误 +### 6. 测试6 - 应该成功保存(如果代码不超过128KB) +