Fix playground bugs and remove TypeScript compiler

- Fix BUG1: JavaScript timeout with proper thread interruption using ScheduledExecutorService
- Fix BUG2: Add URL regex validation before execution in playground test API
- Fix BUG3: Register published parsers to CustomParserRegistry on save/update/delete
- Remove TypeScript compiler functionality (tsCompiler.js, dependencies, UI)
- Add password authentication for playground access
- Add mobile responsive layout support
- Load playground parsers on application startup
This commit is contained in:
q
2026-01-02 19:24:47 +08:00
parent c79702eba8
commit 66b9bcc53a
10 changed files with 865 additions and 490 deletions

View File

@@ -36,6 +36,14 @@ public class JsPlaygroundExecutor {
return thread; 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 ShareLinkInfo shareLinkInfo;
private final String jsCode; private final String jsCode;
private final ScriptEngine engine; private final ScriptEngine engine;
@@ -161,23 +169,34 @@ public class JsPlaygroundExecutor {
} }
}, INDEPENDENT_EXECUTOR); }, INDEPENDENT_EXECUTOR);
// 添加超时处理 // 创建超时任务,强制取消执行
executionFuture.orTimeout(EXECUTION_TIMEOUT_SECONDS, TimeUnit.SECONDS) ScheduledFuture<?> timeoutTask = TIMEOUT_SCHEDULER.schedule(() -> {
.whenComplete((result, error) -> { if (!executionFuture.isDone()) {
if (error != null) { executionFuture.cancel(true); // 强制中断执行线程
if (error instanceof TimeoutException) { playgroundLogger.errorJava("执行超时,已强制中断");
String timeoutMsg = "JavaScript执行超时超过" + EXECUTION_TIMEOUT_SECONDS + "秒),可能存在无限循环"; log.warn("JavaScript执行超时已强制取消");
playgroundLogger.errorJava(timeoutMsg); }
log.error(timeoutMsg); }, EXECUTION_TIMEOUT_SECONDS, TimeUnit.SECONDS);
promise.fail(new RuntimeException(timeoutMsg));
} else { // 处理执行结果
Throwable cause = error.getCause(); executionFuture.whenComplete((result, error) -> {
promise.fail(cause != null ? cause : 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 { } else {
promise.complete(result); Throwable cause = error.getCause();
promise.fail(cause != null ? cause : error);
} }
}); } else {
promise.complete(result);
}
});
return promise.future(); return promise.future();
} }
@@ -225,23 +244,34 @@ public class JsPlaygroundExecutor {
} }
}, INDEPENDENT_EXECUTOR); }, INDEPENDENT_EXECUTOR);
// 添加超时处理 // 创建超时任务,强制取消执行
executionFuture.orTimeout(EXECUTION_TIMEOUT_SECONDS, TimeUnit.SECONDS) ScheduledFuture<?> timeoutTask = TIMEOUT_SCHEDULER.schedule(() -> {
.whenComplete((result, error) -> { if (!executionFuture.isDone()) {
if (error != null) { executionFuture.cancel(true); // 强制中断执行线程
if (error instanceof TimeoutException) { playgroundLogger.errorJava("执行超时,已强制中断");
String timeoutMsg = "JavaScript执行超时超过" + EXECUTION_TIMEOUT_SECONDS + "秒),可能存在无限循环"; log.warn("JavaScript执行超时已强制取消");
playgroundLogger.errorJava(timeoutMsg); }
log.error(timeoutMsg); }, EXECUTION_TIMEOUT_SECONDS, TimeUnit.SECONDS);
promise.fail(new RuntimeException(timeoutMsg));
} else { // 处理执行结果
Throwable cause = error.getCause(); executionFuture.whenComplete((result, error) -> {
promise.fail(cause != null ? cause : 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 { } else {
promise.complete(result); Throwable cause = error.getCause();
promise.fail(cause != null ? cause : error);
} }
}); } else {
promise.complete(result);
}
});
return promise.future(); return promise.future();
} }
@@ -288,23 +318,34 @@ public class JsPlaygroundExecutor {
} }
}, INDEPENDENT_EXECUTOR); }, INDEPENDENT_EXECUTOR);
// 添加超时处理 // 创建超时任务,强制取消执行
executionFuture.orTimeout(EXECUTION_TIMEOUT_SECONDS, TimeUnit.SECONDS) ScheduledFuture<?> timeoutTask = TIMEOUT_SCHEDULER.schedule(() -> {
.whenComplete((result, error) -> { if (!executionFuture.isDone()) {
if (error != null) { executionFuture.cancel(true); // 强制中断执行线程
if (error instanceof TimeoutException) { playgroundLogger.errorJava("执行超时,已强制中断");
String timeoutMsg = "JavaScript执行超时超过" + EXECUTION_TIMEOUT_SECONDS + "秒),可能存在无限循环"; log.warn("JavaScript执行超时已强制取消");
playgroundLogger.errorJava(timeoutMsg); }
log.error(timeoutMsg); }, EXECUTION_TIMEOUT_SECONDS, TimeUnit.SECONDS);
promise.fail(new RuntimeException(timeoutMsg));
} else { // 处理执行结果
Throwable cause = error.getCause(); executionFuture.whenComplete((result, error) -> {
promise.fail(cause != null ? cause : 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 { } else {
promise.complete(result); Throwable cause = error.getCause();
promise.fail(cause != null ? cause : error);
} }
}); } else {
promise.complete(result);
}
});
return promise.future(); return promise.future();
} }

View File

@@ -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()

View File

@@ -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` 并检查构建产物

View File

@@ -19,7 +19,6 @@
"monaco-editor": "^0.45.0", "monaco-editor": "^0.45.0",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"splitpanes": "^4.0.4", "splitpanes": "^4.0.4",
"typescript": "^5.9.3",
"vue": "^3.5.12", "vue": "^3.5.12",
"vue-clipboard3": "^2.0.0", "vue-clipboard3": "^2.0.0",
"vue-router": "^4.5.1", "vue-router": "^4.5.1",

View File

@@ -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代码失败');
}
}
}; };

View File

@@ -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
};

View File

@@ -57,11 +57,10 @@
<div class="card-header"> <div class="card-header">
<div class="header-left"> <div class="header-left">
<span class="title">JS解析器演练场</span> <span class="title">JS解析器演练场</span>
<!-- 语言选择器 --> <!-- 语言显示仅支持JavaScript -->
<el-radio-group v-model="codeLanguage" size="small" style="margin-left: 15px;" @change="onLanguageChange"> <span style="margin-left: 15px; color: var(--el-text-color-secondary); font-size: 12px;">
<el-radio-button :label="LANGUAGE.JAVASCRIPT">JavaScript</el-radio-button> JavaScript (ES5)
<el-radio-button :label="LANGUAGE.TYPESCRIPT">TypeScript</el-radio-button> </span>
</el-radio-group>
</div> </div>
<div class="header-actions"> <div class="header-actions">
<!-- 主要操作 --> <!-- 主要操作 -->
@@ -536,7 +535,6 @@ import 'splitpanes/dist/splitpanes.css';
import MonacoEditor from '@/components/MonacoEditor.vue'; import MonacoEditor from '@/components/MonacoEditor.vue';
import { playgroundApi } from '@/utils/playgroundApi'; import { playgroundApi } from '@/utils/playgroundApi';
import { configureMonacoTypes, loadTypesFromApi } from '@/utils/monacoTypes'; import { configureMonacoTypes, loadTypesFromApi } from '@/utils/monacoTypes';
import { compileToES5, isTypeScriptCode, formatCompileErrors } from '@/utils/tsCompiler';
import JsonViewer from 'vue3-json-viewer'; import JsonViewer from 'vue3-json-viewer';
export default { export default {
@@ -550,15 +548,11 @@ export default {
setup() { setup() {
// 语言常量 // 语言常量
const LANGUAGE = { const LANGUAGE = {
JAVASCRIPT: 'JavaScript', JAVASCRIPT: 'JavaScript'
TYPESCRIPT: 'TypeScript'
}; };
const editorRef = ref(null); const editorRef = ref(null);
const jsCode = ref(''); const jsCode = ref('');
const codeLanguage = ref(LANGUAGE.JAVASCRIPT); // 新增:代码语言选择
const compiledES5Code = ref(''); // 新增编译后的ES5代码
const compileStatus = ref({ success: true, errors: [] }); // 新增:编译状态
// ===== 加载和认证状态 ===== // ===== 加载和认证状态 =====
const loading = ref(true); const loading = ref(true);
@@ -699,90 +693,6 @@ function parseById(shareLinkInfo, http, logger) {
return "https://example.com/download?id=" + fileId; 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/(?<KEY>\\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<string> {
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<any[]> {
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<string> {
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(() => { const editorTheme = computed(() => {
return isDarkMode.value ? 'vs-dark' : 'vs'; return isDarkMode.value ? 'vs-dark' : 'vs';
@@ -880,15 +790,6 @@ async function parseById(
jsCode.value = exampleCode; 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类型定义...'); setProgress(50, '初始化Monaco Editor类型定义...');
await initMonacoTypes(); await initMonacoTypes();
@@ -965,8 +866,8 @@ async function parseById(
// 加载示例代码 // 加载示例代码
const loadTemplate = () => { const loadTemplate = () => {
jsCode.value = codeLanguage.value === LANGUAGE.TYPESCRIPT ? exampleTypeScriptCode : exampleCode; jsCode.value = exampleCode;
ElMessage.success(`已加载${codeLanguage.value}示例代码`); 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 () => { const executeTest = async () => {
if (!jsCode.value.trim()) { if (!jsCode.value.trim()) {
@@ -1101,67 +949,9 @@ async function parseById(
testResult.value = null; testResult.value = null;
consoleLogs.value = []; // 清空控制台 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 { try {
const result = await playgroundApi.testScript( const result = await playgroundApi.testScript(
codeToExecute, // 使用编译后的代码或原始JS代码 jsCode.value, // 直接使用JavaScript代码
testParams.value.shareUrl, testParams.value.shareUrl,
testParams.value.pwd, testParams.value.pwd,
testParams.value.method testParams.value.method
@@ -1610,9 +1400,6 @@ curl "${baseUrl}/json/parser?url=${encodeURIComponent(exampleUrl)}"</pre>
LANGUAGE, LANGUAGE,
editorRef, editorRef,
jsCode, jsCode,
codeLanguage,
compiledES5Code,
compileStatus,
testParams, testParams,
testResult, testResult,
testing, testing,
@@ -1635,8 +1422,6 @@ curl "${baseUrl}/json/parser?url=${encodeURIComponent(exampleUrl)}"</pre>
isMobile, isMobile,
updateIsMobile, updateIsMobile,
onCodeChange, onCodeChange,
onLanguageChange,
compileTypeScriptCode,
loadTemplate, loadTemplate,
formatCode, formatCode,
saveCode, saveCode,

View File

@@ -5,7 +5,12 @@ import cn.qaiu.db.pool.JDBCPoolInit;
import cn.qaiu.lz.common.cache.CacheConfigLoader; import cn.qaiu.lz.common.cache.CacheConfigLoader;
import cn.qaiu.lz.common.interceptorImpl.RateLimiter; import cn.qaiu.lz.common.interceptorImpl.RateLimiter;
import cn.qaiu.lz.web.config.PlaygroundConfig; 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.Deploy;
import cn.qaiu.vx.core.util.AsyncServiceUtil;
import cn.qaiu.vx.core.util.ConfigConstant; import cn.qaiu.vx.core.util.ConfigConstant;
import cn.qaiu.vx.core.util.VertxHolder; import cn.qaiu.vx.core.util.VertxHolder;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; 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.JsonObject;
import io.vertx.core.json.jackson.DatabindCodec; import io.vertx.core.json.jackson.DatabindCodec;
import io.vertx.core.shareddata.LocalMap; import io.vertx.core.shareddata.LocalMap;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.time.DateFormatUtils; import org.apache.commons.lang3.time.DateFormatUtils;
import java.util.Date; import java.util.Date;
@@ -26,6 +32,7 @@ import static cn.qaiu.vx.core.util.ConfigConstant.LOCAL;
* <br>Create date 2021-05-08 13:00:01 * <br>Create date 2021-05-08 13:00:01
* @author qaiu yyzy * @author qaiu yyzy
*/ */
@Slf4j
public class AppMain { public class AppMain {
public static void main(String[] args) { public static void main(String[] args) {
@@ -55,6 +62,10 @@ public class AppMain {
VertxHolder.getVertxInstance().setTimer(1000, id -> { VertxHolder.getVertxInstance().setTimer(1000, id -> {
System.out.println(DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss.SSS")); System.out.println(DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss.SSS"));
System.out.println("数据库连接成功"); System.out.println("数据库连接成功");
// 加载演练场解析器
loadPlaygroundParsers();
String addr = jsonObject.getJsonObject(ConfigConstant.SERVER).getString("domainName"); String addr = jsonObject.getJsonObject(ConfigConstant.SERVER).getString("domainName");
System.out.println("启动成功: \n本地服务地址: " + addr); System.out.println("启动成功: \n本地服务地址: " + addr);
}); });
@@ -93,4 +104,40 @@ public class AppMain {
// 演练场配置 // 演练场配置
PlaygroundConfig.loadFromJson(jsonObject); 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);
});
}
} }

View File

@@ -5,6 +5,8 @@ import cn.qaiu.lz.web.config.PlaygroundConfig;
import cn.qaiu.lz.web.model.PlaygroundTestResp; import cn.qaiu.lz.web.model.PlaygroundTestResp;
import cn.qaiu.lz.web.service.DbService; import cn.qaiu.lz.web.service.DbService;
import cn.qaiu.parser.ParserCreate; 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.JsPlaygroundExecutor;
import cn.qaiu.parser.customjs.JsPlaygroundLogger; import cn.qaiu.parser.customjs.JsPlaygroundLogger;
import cn.qaiu.parser.customjs.JsScriptMetadataParser; import cn.qaiu.parser.customjs.JsScriptMetadataParser;
@@ -30,6 +32,8 @@ import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
@@ -178,6 +182,32 @@ public class PlaygroundApi {
.build())); .build()));
return promise.future(); 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)) { if (!"parse".equals(method) && !"parseFileList".equals(method) && !"parseById".equals(method)) {
@@ -433,7 +463,18 @@ public class PlaygroundApi {
parser.put("enabled", true); parser.put("enabled", true);
dbService.savePlaygroundParser(parser).onSuccess(result -> { 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 -> { }).onFailure(e -> {
log.error("保存解析器失败", e); log.error("保存解析器失败", e);
promise.complete(JsonResult.error("保存失败: " + e.getMessage()).toJsonObject()); promise.complete(JsonResult.error("保存失败: " + e.getMessage()).toJsonObject());
@@ -483,12 +524,14 @@ public class PlaygroundApi {
// 解析元数据 // 解析元数据
try { try {
var config = JsScriptMetadataParser.parseScript(jsCode); var config = JsScriptMetadataParser.parseScript(jsCode);
String type = config.getType();
String displayName = config.getDisplayName(); String displayName = config.getDisplayName();
String name = config.getMetadata().get("name"); String name = config.getMetadata().get("name");
String description = config.getMetadata().get("description"); String description = config.getMetadata().get("description");
String author = config.getMetadata().get("author"); String author = config.getMetadata().get("author");
String version = config.getMetadata().get("version"); String version = config.getMetadata().get("version");
String matchPattern = config.getMatchPattern() != null ? config.getMatchPattern().pattern() : null; String matchPattern = config.getMatchPattern() != null ? config.getMatchPattern().pattern() : null;
boolean enabled = body.getBoolean("enabled", true);
JsonObject parser = new JsonObject(); JsonObject parser = new JsonObject();
parser.put("name", name); parser.put("name", name);
@@ -498,10 +541,29 @@ public class PlaygroundApi {
parser.put("version", version); parser.put("version", version);
parser.put("matchPattern", matchPattern); parser.put("matchPattern", matchPattern);
parser.put("jsCode", jsCode); parser.put("jsCode", jsCode);
parser.put("enabled", body.getBoolean("enabled", true)); parser.put("enabled", enabled);
dbService.updatePlaygroundParser(id, parser).onSuccess(result -> { 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 -> { }).onFailure(e -> {
log.error("更新解析器失败", e); log.error("更新解析器失败", e);
promise.complete(JsonResult.error("更新失败: " + e.getMessage()).toJsonObject()); promise.complete(JsonResult.error("更新失败: " + e.getMessage()).toJsonObject());
@@ -528,7 +590,38 @@ public class PlaygroundApi {
if (!checkAuth(ctx)) { if (!checkAuth(ctx)) {
return Future.succeededFuture(JsonResult.error("未授权访问").toJsonObject()); return Future.succeededFuture(JsonResult.error("未授权访问").toJsonObject());
} }
return dbService.deletePlaygroundParser(id);
Promise<JsonObject> 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();
} }
/** /**

View File

@@ -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/(?<KEY>\\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/(?<KEY>\\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/(?<KEY>\\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/(?<KEY>\\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/(?<KEY>\\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/(?<KEY>\\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