mirror of
https://github.com/qaiu/netdisk-fast-download.git
synced 2026-02-24 14:15:24 +00:00
Compare commits
333 Commits
copilot/ad
...
copilot/ad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b74c3f31c4 | ||
|
|
f23b97e22c | ||
|
|
0560989e77 | ||
|
|
f2c9c34324 | ||
|
|
a97268c702 | ||
|
|
2654b550fb | ||
|
|
12a5a17a30 | ||
|
|
e346812c0a | ||
|
|
6b2e391af9 | ||
|
|
199456cb11 | ||
|
|
636994387f | ||
|
|
90c79f7bac | ||
|
|
79601b36a5 | ||
|
|
96cef89f08 | ||
|
|
e057825b25 | ||
|
|
ebe848dfe8 | ||
|
|
e259a0989e | ||
|
|
f750aa68e8 | ||
|
|
49b8501e86 | ||
|
|
fc2e2a4697 | ||
|
|
b4b1d7f923 | ||
|
|
df646b8c43 | ||
|
|
8e790f6b22 | ||
|
|
2e76af980e | ||
|
|
80ccbe5b62 | ||
|
|
aa0cd68f7f | ||
|
|
51833148b1 | ||
|
|
0fa77ebf21 | ||
|
|
584c075930 | ||
|
|
9e7a3718a4 | ||
|
|
0e2ca2f1ca | ||
|
|
52e889333b | ||
|
|
4745440079 | ||
|
|
b5628eac17 | ||
|
|
d23b11577e | ||
|
|
f1dd9fc0ee | ||
|
|
0877fadcfb | ||
|
|
733059dc8e | ||
|
|
321380c2b9 | ||
|
|
deb121a51b | ||
|
|
b6aef7c239 | ||
|
|
b13a7a5ee1 | ||
|
|
fff6a00690 | ||
|
|
b4da3cee20 | ||
|
|
0a650996a1 | ||
|
|
37b91cd388 | ||
|
|
42b721eabf | ||
|
|
231d5c3fb9 | ||
|
|
064efdf3f3 | ||
|
|
7b364a0f90 | ||
|
|
c8a4ca7f16 | ||
|
|
97627b824c | ||
|
|
6dbdc9bd90 | ||
|
|
4166ea10af | ||
|
|
fa12ab2c51 | ||
|
|
4fc4ed8640 | ||
|
|
48172f2769 | ||
|
|
c7e6d68fbd | ||
|
|
e6672a51c5 | ||
|
|
abde7841ac | ||
|
|
8e661ed1c5 | ||
|
|
217cb3a776 | ||
|
|
b8c1bca900 | ||
|
|
5e09b8e92a | ||
|
|
c16bde6bb8 | ||
|
|
eb06eb9f3d | ||
|
|
0c49088098 | ||
|
|
b970241a64 | ||
|
|
6c5aafc11e | ||
|
|
ca0846f4a7 | ||
|
|
14f7fcc5ad | ||
|
|
23a18aba5c | ||
|
|
2d5a79bb16 | ||
|
|
51e1bbefbb | ||
|
|
6647fc5371 | ||
|
|
b67544f0cd | ||
|
|
ef5826a73b | ||
|
|
a48adbd0df | ||
|
|
5c60493a24 | ||
|
|
55e6227de0 | ||
|
|
24a7395004 | ||
|
|
b2a7187fc5 | ||
|
|
ace7cdc88e | ||
|
|
2e909b5868 | ||
|
|
de78bcbc98 | ||
|
|
c560f0e902 | ||
|
|
88860c9302 | ||
|
|
ef65d0e095 | ||
|
|
6438505f4a | ||
|
|
1be5030dd1 | ||
|
|
421b2f4a42 | ||
|
|
a66bf84381 | ||
|
|
0c4d366d6d | ||
|
|
a1d0a921fa | ||
|
|
2092230a61 | ||
|
|
6e5ae6eff3 | ||
|
|
4f8259d772 | ||
|
|
8b987d9824 | ||
|
|
e8ba451d18 | ||
|
|
77758db463 | ||
|
|
6c58598a8e | ||
|
|
3ac35230a3 | ||
|
|
ca91302d28 | ||
|
|
e07272a5dc | ||
|
|
461305e1df | ||
|
|
8e8ab10a0f | ||
|
|
e754326925 | ||
|
|
4c92994c6f | ||
|
|
66c57f47ac | ||
|
|
ec689eadd8 | ||
|
|
c1e15709a7 | ||
|
|
2848937ce7 | ||
|
|
42ff0c21b2 | ||
|
|
3ed7e547e6 | ||
|
|
fad8e688df | ||
|
|
b2f2dcac4c | ||
|
|
fcba78e977 | ||
|
|
77c9d777a1 | ||
|
|
4460659210 | ||
|
|
8631524107 | ||
|
|
0579588814 | ||
|
|
df2bfb6ac7 | ||
|
|
517b6f8910 | ||
|
|
94a46d2833 | ||
|
|
1631a0faa1 | ||
|
|
06d5943cb6 | ||
|
|
3095e13676 | ||
|
|
482cbce7e8 | ||
|
|
ef2fc3ab98 | ||
|
|
5b57b05eae | ||
|
|
093579c6f5 | ||
|
|
c2d4990d7f | ||
|
|
40e8380738 | ||
|
|
b716e1e861 | ||
|
|
8432d4952c | ||
|
|
dd8f085f63 | ||
|
|
161ff8d8a3 | ||
|
|
1390cd0104 | ||
|
|
7a02b1e97f | ||
|
|
036f107c90 | ||
|
|
5652383450 | ||
|
|
9a047a5da0 | ||
|
|
8975743a37 | ||
|
|
0e30eafe49 | ||
|
|
7facb62f21 | ||
|
|
30d43cb961 | ||
|
|
c505b17e35 | ||
|
|
080c4c753d | ||
|
|
ade0d34d91 | ||
|
|
56d082eb0b | ||
|
|
795c4529ba | ||
|
|
0f5cfe22ea | ||
|
|
925ad2c3a5 | ||
|
|
f3e96907fe | ||
|
|
75a1e58a7d | ||
|
|
379e889f71 | ||
|
|
40c06f397b | ||
|
|
9e9302436e | ||
|
|
6d816d4193 | ||
|
|
438eda9c08 | ||
|
|
ace39e4633 | ||
|
|
7712391f29 | ||
|
|
65f08dcb02 | ||
|
|
1d332aa6f4 | ||
|
|
ba81641517 | ||
|
|
fb30bdb879 | ||
|
|
fc451d3b41 | ||
|
|
ffee1f3462 | ||
|
|
f30027dd13 | ||
|
|
8b6aad17f4 | ||
|
|
b77930adfb | ||
|
|
aff8f88076 | ||
|
|
4e6582e24c | ||
|
|
fa9acaccfd | ||
|
|
0414f85f12 | ||
|
|
527dd0eeb4 | ||
|
|
74ed7475c9 | ||
|
|
54dc3dba96 | ||
|
|
9980159090 | ||
|
|
0b193ebb00 | ||
|
|
f5fc9843b2 | ||
|
|
df1f67dd26 | ||
|
|
b069a5f576 | ||
|
|
7686763a03 | ||
|
|
635a6eac37 | ||
|
|
877edc535f | ||
|
|
01d59e3c1e | ||
|
|
fece2799e3 | ||
|
|
de9756ee86 | ||
|
|
51f047a51b | ||
|
|
04b66e82b7 | ||
|
|
df89253647 | ||
|
|
45dbca794e | ||
|
|
857bf28f99 | ||
|
|
e07ce15228 | ||
|
|
0637bcfd8e | ||
|
|
23db0563ac | ||
|
|
ccba71aa4e | ||
|
|
fee4bf2ad6 | ||
|
|
5052fea9ef | ||
|
|
e85215fca1 | ||
|
|
e42fe45329 | ||
|
|
4240815bd1 | ||
|
|
6f0c5305e2 | ||
|
|
757005cad8 | ||
|
|
81651ad97c | ||
|
|
f3763b6058 | ||
|
|
82478dc485 | ||
|
|
703fd05d43 | ||
|
|
ff868b6e2a | ||
|
|
051a74b37b | ||
|
|
a0a1085623 | ||
|
|
2612d3919c | ||
|
|
6f123a236f | ||
|
|
71e57e6a08 | ||
|
|
7cb18d8186 | ||
|
|
cdbf670ece | ||
|
|
e0dafee617 | ||
|
|
c37bce1563 | ||
|
|
0b3c77d644 | ||
|
|
2cf85caf86 | ||
|
|
594010ba88 | ||
|
|
d91460d2e2 | ||
|
|
89713e6ac9 | ||
|
|
17c9b2538c | ||
|
|
d337b003cb | ||
|
|
8f1485656b | ||
|
|
f0c4ec3031 | ||
|
|
458be84aca | ||
|
|
c7716aad34 | ||
|
|
4a3e734408 | ||
|
|
54cc212753 | ||
|
|
f4ae1eaa51 | ||
|
|
d2537282c9 | ||
|
|
87527688c3 | ||
|
|
2be0b6505a | ||
|
|
672f100c7c | ||
|
|
5af402c0c5 | ||
|
|
693a4f0f63 | ||
|
|
f8d2426ff6 | ||
|
|
973a9bedcd | ||
|
|
a583733400 | ||
|
|
78eb51b3ca | ||
|
|
a2606be9d8 | ||
|
|
a4975c72ce | ||
|
|
58f96822a4 | ||
|
|
96b0d94986 | ||
|
|
70b38db8c5 | ||
|
|
b6a9c2d3a0 | ||
|
|
a01df6c7db | ||
|
|
4455bee570 | ||
|
|
cd0adef2ed | ||
|
|
4aa24a65fb | ||
|
|
760dca8772 | ||
|
|
8269673619 | ||
|
|
82ec586554 | ||
|
|
ca98cc8708 | ||
|
|
f07800985d | ||
|
|
b042df93b7 | ||
|
|
ecf4441946 | ||
|
|
39b2612840 | ||
|
|
218f486e6b | ||
|
|
cfcc25f175 | ||
|
|
155e88223c | ||
|
|
05039ece51 | ||
|
|
1c673f2b46 | ||
|
|
2232a70228 | ||
|
|
e661b1d817 | ||
|
|
5a6a65f580 | ||
|
|
5cdd3bcd30 | ||
|
|
1233a885b8 | ||
|
|
adf56cd768 | ||
|
|
cd4b208be9 | ||
|
|
502de1a5d0 | ||
|
|
4158f869a3 | ||
|
|
ff569d339c | ||
|
|
10eec323dd | ||
|
|
0a3db51c7d | ||
|
|
229aee0b30 | ||
|
|
44714aa981 | ||
|
|
2b6138a889 | ||
|
|
5e424f7bf4 | ||
|
|
294e47deed | ||
|
|
dc42547b73 | ||
|
|
7ef7f0706b | ||
|
|
a59b98a7c9 | ||
|
|
088fee9a4d | ||
|
|
d8666acfe8 | ||
|
|
209e9c2866 | ||
|
|
6c3195dea4 | ||
|
|
7d774a7433 | ||
|
|
f1ec4433cf | ||
|
|
1f825db261 | ||
|
|
1019f24f1d | ||
|
|
f5c5b99579 | ||
|
|
e002d19f1b | ||
|
|
0d5c9651f0 | ||
|
|
53fc13b95c | ||
|
|
694c3b0ddc | ||
|
|
9b3d4577cc | ||
|
|
77783915dd | ||
|
|
b67ac21a79 | ||
|
|
603afed2f2 | ||
|
|
c2a7c34496 | ||
|
|
edd40f48ba | ||
|
|
cca3d6b8b9 | ||
|
|
f004512903 | ||
|
|
6407bb6730 | ||
|
|
b914eeadec | ||
|
|
dcadc6783e | ||
|
|
bc9f43634f | ||
|
|
4778f0164c | ||
|
|
9904754a07 | ||
|
|
1b79077c9e | ||
|
|
c13afb05b3 | ||
|
|
03e320efb8 | ||
|
|
7846332476 | ||
|
|
2d5d3b86e0 | ||
|
|
7c9ba890af | ||
|
|
0d609daffa | ||
|
|
c12e56d402 | ||
|
|
c7b38c07d5 | ||
|
|
dc51066cea | ||
|
|
59d2fb3010 | ||
|
|
a0fe702c10 | ||
|
|
f886f7e366 | ||
|
|
1d475d88ed | ||
|
|
e64c901912 | ||
|
|
5fce02e623 | ||
|
|
13997bc543 | ||
|
|
3e05b0d6f9 | ||
|
|
966417f867 | ||
|
|
601a0d1b91 |
@@ -1,166 +0,0 @@
|
||||
# Playground 密码保护功能
|
||||
|
||||
## 概述
|
||||
|
||||
JS解析器演练场现在支持密码保护功能,可以通过配置文件控制是否需要密码才能访问。
|
||||
|
||||
## 配置说明
|
||||
|
||||
在 `web-service/src/main/resources/app-dev.yml` 文件中添加以下配置:
|
||||
|
||||
```yaml
|
||||
# JS演练场配置
|
||||
playground:
|
||||
# 公开模式,默认false需要密码访问,设为true则无需密码
|
||||
public: false
|
||||
# 访问密码,建议修改默认密码!
|
||||
password: 'nfd_playground_2024'
|
||||
```
|
||||
|
||||
### 配置项说明
|
||||
|
||||
- `public`: 布尔值,默认为 `false`
|
||||
- `false`: 需要输入密码才能访问演练场(推荐)
|
||||
- `true`: 公开访问,无需密码
|
||||
|
||||
- `password`: 字符串,访问密码
|
||||
- 默认密码:`nfd_playground_2024`
|
||||
- **强烈建议在生产环境中修改为自定义密码!**
|
||||
|
||||
## 功能特点
|
||||
|
||||
### 1. 密码保护模式 (public: false)
|
||||
|
||||
当 `public` 设置为 `false` 时:
|
||||
|
||||
- 访问 `/playground` 页面时会显示密码输入界面
|
||||
- 必须输入正确的密码才能使用演练场功能
|
||||
- 密码验证通过后,会话保持登录状态
|
||||
- 所有演练场相关的 API 接口都受到保护
|
||||
|
||||
### 2. 公开模式 (public: true)
|
||||
|
||||
当 `public` 设置为 `true` 时:
|
||||
|
||||
- 无需输入密码即可访问演练场
|
||||
- 适用于内网环境或开发测试环境
|
||||
|
||||
### 3. 加载动画与进度条
|
||||
|
||||
页面加载过程会显示进度条,包括以下阶段:
|
||||
|
||||
1. 初始化Vue组件 (0-20%)
|
||||
2. 加载配置和本地数据 (20-40%)
|
||||
3. 准备TypeScript编译器 (40-50%)
|
||||
4. 初始化Monaco Editor (50-80%)
|
||||
5. 加载完成 (80-100%)
|
||||
|
||||
### 4. 移动端适配
|
||||
|
||||
- 桌面端:左右分栏布局,可拖拽调整宽度
|
||||
- 移动端(屏幕宽度 ≤ 768px):自动切换为上下分栏布局,可拖拽调整高度
|
||||
|
||||
## 安全建议
|
||||
|
||||
⚠️ **重要安全提示:**
|
||||
|
||||
1. **修改默认密码**:在生产环境中,务必修改 `playground.password` 为自定义的强密码
|
||||
2. **使用密码保护**:建议保持 `public: false`,避免未授权访问
|
||||
3. **定期更换密码**:定期更换访问密码以提高安全性
|
||||
4. **配置文件保护**:确保配置文件的访问权限受到保护
|
||||
|
||||
## 系统启动提示
|
||||
|
||||
当系统启动时,会在日志中显示当前配置:
|
||||
|
||||
```
|
||||
INFO - Playground配置已加载: public=false, password=已设置
|
||||
```
|
||||
|
||||
如果使用默认密码,会显示警告:
|
||||
|
||||
```
|
||||
WARN - ⚠️ 警告:您正在使用默认密码,建议修改配置文件中的 playground.password 以确保安全!
|
||||
```
|
||||
|
||||
## API 端点
|
||||
|
||||
### 1. 获取状态
|
||||
|
||||
```
|
||||
GET /v2/playground/status
|
||||
```
|
||||
|
||||
返回:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"public": false,
|
||||
"authed": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 登录
|
||||
|
||||
```
|
||||
POST /v2/playground/login
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"password": "your_password"
|
||||
}
|
||||
```
|
||||
|
||||
成功响应:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "登录成功",
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
失败响应:
|
||||
```json
|
||||
{
|
||||
"code": 500,
|
||||
"msg": "密码错误",
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 如何禁用密码保护?
|
||||
|
||||
A: 在配置文件中设置 `playground.public: true`
|
||||
|
||||
### Q: 忘记密码怎么办?
|
||||
|
||||
A: 修改配置文件中的 `playground.password` 为新密码,然后重启服务
|
||||
|
||||
### Q: 密码是否加密存储?
|
||||
|
||||
A: 当前版本密码以明文形式存储在配置文件中,请确保配置文件的访问权限受到保护
|
||||
|
||||
### Q: Session 有效期多久?
|
||||
|
||||
A: Session 由 Vert.x 管理,默认在浏览器会话期间有效,关闭浏览器后失效
|
||||
|
||||
## 后续版本计划
|
||||
|
||||
未来版本可能会添加以下功能:
|
||||
|
||||
- [ ] 支持环境变量配置密码
|
||||
- [ ] 支持加密存储密码
|
||||
- [ ] 支持多用户账户系统
|
||||
- [ ] 支持 Token 认证方式
|
||||
- [ ] 支持 Session 超时配置
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [Playground 使用指南](PLAYGROUND_GUIDE.md)
|
||||
- [JavaScript 解析器开发指南](parser/doc/JAVASCRIPT_PARSER_GUIDE.md)
|
||||
- [TypeScript 实现总结](TYPESCRIPT_IMPLEMENTATION_SUMMARY_CN.md)
|
||||
@@ -40,8 +40,6 @@ https://nfd-parser.github.io/nfd-preview/preview.html?src=https%3A%2F%2Flz.qaiu.
|
||||
|
||||
**JavaScript解析器文档:** [JavaScript解析器开发指南](parser/doc/JAVASCRIPT_PARSER_GUIDE.md) | [自定义解析器扩展指南](parser/doc/CUSTOM_PARSER_GUIDE.md) | [快速开始](parser/doc/CUSTOM_PARSER_QUICKSTART.md)
|
||||
|
||||
**Playground功能:** [JS解析器演练场密码保护说明](PLAYGROUND_PASSWORD_PROTECTION.md)
|
||||
|
||||
## 预览地址
|
||||
[预览地址1](https://lz.qaiu.top)
|
||||
[预览地址2](https://lzzz.qaiu.top)
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
package cn.qaiu.vx.core.verticle.conf;
|
||||
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import io.vertx.core.json.JsonArray;
|
||||
import io.vertx.core.json.impl.JsonUtil;
|
||||
import java.time.Instant;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* Converter and mapper for {@link cn.qaiu.vx.core.verticle.conf.HttpProxyConf}.
|
||||
* NOTE: This class has been automatically generated from the {@link cn.qaiu.vx.core.verticle.conf.HttpProxyConf} original class using Vert.x codegen.
|
||||
*/
|
||||
public class HttpProxyConfConverter {
|
||||
|
||||
|
||||
private static final Base64.Decoder BASE64_DECODER = JsonUtil.BASE64_DECODER;
|
||||
private static final Base64.Encoder BASE64_ENCODER = JsonUtil.BASE64_ENCODER;
|
||||
|
||||
static void fromJson(Iterable<java.util.Map.Entry<String, Object>> json, HttpProxyConf obj) {
|
||||
for (java.util.Map.Entry<String, Object> member : json) {
|
||||
switch (member.getKey()) {
|
||||
case "password":
|
||||
if (member.getValue() instanceof String) {
|
||||
obj.setPassword((String)member.getValue());
|
||||
}
|
||||
break;
|
||||
case "port":
|
||||
if (member.getValue() instanceof Number) {
|
||||
obj.setPort(((Number)member.getValue()).intValue());
|
||||
}
|
||||
break;
|
||||
case "preProxyOptions":
|
||||
if (member.getValue() instanceof JsonObject) {
|
||||
obj.setPreProxyOptions(new io.vertx.core.net.ProxyOptions((io.vertx.core.json.JsonObject)member.getValue()));
|
||||
}
|
||||
break;
|
||||
case "timeout":
|
||||
if (member.getValue() instanceof Number) {
|
||||
obj.setTimeout(((Number)member.getValue()).intValue());
|
||||
}
|
||||
break;
|
||||
case "username":
|
||||
if (member.getValue() instanceof String) {
|
||||
obj.setUsername((String)member.getValue());
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void toJson(HttpProxyConf obj, JsonObject json) {
|
||||
toJson(obj, json.getMap());
|
||||
}
|
||||
|
||||
static void toJson(HttpProxyConf obj, java.util.Map<String, Object> json) {
|
||||
if (obj.getPassword() != null) {
|
||||
json.put("password", obj.getPassword());
|
||||
}
|
||||
if (obj.getPort() != null) {
|
||||
json.put("port", obj.getPort());
|
||||
}
|
||||
if (obj.getPreProxyOptions() != null) {
|
||||
json.put("preProxyOptions", obj.getPreProxyOptions().toJson());
|
||||
}
|
||||
if (obj.getTimeout() != null) {
|
||||
json.put("timeout", obj.getTimeout());
|
||||
}
|
||||
if (obj.getUsername() != null) {
|
||||
json.put("username", obj.getUsername());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,14 +36,6 @@ 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;
|
||||
@@ -169,34 +161,23 @@ public class JsPlaygroundExecutor {
|
||||
}
|
||||
}, INDEPENDENT_EXECUTOR);
|
||||
|
||||
// 创建超时任务,强制取消执行
|
||||
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));
|
||||
// 添加超时处理
|
||||
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);
|
||||
}
|
||||
} else {
|
||||
Throwable cause = error.getCause();
|
||||
promise.fail(cause != null ? cause : error);
|
||||
promise.complete(result);
|
||||
}
|
||||
} else {
|
||||
promise.complete(result);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
@@ -244,34 +225,23 @@ public class JsPlaygroundExecutor {
|
||||
}
|
||||
}, INDEPENDENT_EXECUTOR);
|
||||
|
||||
// 创建超时任务,强制取消执行
|
||||
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));
|
||||
// 添加超时处理
|
||||
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);
|
||||
}
|
||||
} else {
|
||||
Throwable cause = error.getCause();
|
||||
promise.fail(cause != null ? cause : error);
|
||||
promise.complete(result);
|
||||
}
|
||||
} else {
|
||||
promise.complete(result);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
@@ -318,34 +288,23 @@ public class JsPlaygroundExecutor {
|
||||
}
|
||||
}, INDEPENDENT_EXECUTOR);
|
||||
|
||||
// 创建超时任务,强制取消执行
|
||||
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));
|
||||
// 添加超时处理
|
||||
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);
|
||||
}
|
||||
} else {
|
||||
Throwable cause = error.getCause();
|
||||
promise.fail(cause != null ? cause : error);
|
||||
promise.complete(result);
|
||||
}
|
||||
} else {
|
||||
promise.complete(result);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
@@ -1,362 +0,0 @@
|
||||
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()
|
||||
@@ -1,174 +0,0 @@
|
||||
# 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` 并检查构建产物
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"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",
|
||||
|
||||
@@ -4,33 +4,6 @@ import axios from 'axios';
|
||||
* 演练场API服务
|
||||
*/
|
||||
export const playgroundApi = {
|
||||
/**
|
||||
* 获取Playground状态(是否需要认证)
|
||||
* @returns {Promise} 状态信息
|
||||
*/
|
||||
async getStatus() {
|
||||
try {
|
||||
const response = await axios.get('/v2/playground/status');
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(error.response?.data?.error || error.message || '获取状态失败');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Playground登录
|
||||
* @param {string} password - 访问密码
|
||||
* @returns {Promise} 登录结果
|
||||
*/
|
||||
async login(password) {
|
||||
try {
|
||||
const response = await axios.post('/v2/playground/login', { password });
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(error.response?.data?.error || error.message || '登录失败');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 测试执行JavaScript代码
|
||||
* @param {string} jsCode - JavaScript代码
|
||||
@@ -170,4 +143,54 @@ 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代码失败');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
167
web-front/src/utils/tsCompiler.js
Normal file
167
web-front/src/utils/tsCompiler.js
Normal file
@@ -0,0 +1,167 @@
|
||||
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
|
||||
};
|
||||
@@ -1,66 +1,15 @@
|
||||
<template>
|
||||
<div ref="playgroundContainer" class="playground-container" :class="{ 'dark-theme': isDarkMode, 'fullscreen': isFullscreen, 'is-mobile': isMobile }">
|
||||
<!-- 加载动画 + 进度条 -->
|
||||
<div v-if="loading" class="playground-loading-overlay">
|
||||
<div class="playground-loading-card">
|
||||
<div class="loading-icon">
|
||||
<el-icon class="is-loading" :size="40"><Loading /></el-icon>
|
||||
</div>
|
||||
<div class="loading-text">正在加载编辑器和编译器...</div>
|
||||
<div class="loading-bar">
|
||||
<div class="loading-bar-inner" :style="{ width: loadProgress + '%' }"></div>
|
||||
</div>
|
||||
<div class="loading-percent">{{ loadProgress }}%</div>
|
||||
<div class="loading-details">{{ loadingMessage }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 密码验证界面 -->
|
||||
<div v-if="!loading && authChecking" class="playground-auth-loading">
|
||||
<el-icon class="is-loading" :size="30"><Loading /></el-icon>
|
||||
<span style="margin-left: 10px;">正在检查访问权限...</span>
|
||||
</div>
|
||||
|
||||
<div v-if="shouldShowAuthUI" class="playground-auth-overlay">
|
||||
<div class="playground-auth-card">
|
||||
<div class="auth-icon">
|
||||
<el-icon :size="50"><Lock /></el-icon>
|
||||
</div>
|
||||
<div class="auth-title">JS解析器演练场</div>
|
||||
<div class="auth-subtitle">请输入访问密码</div>
|
||||
<el-input
|
||||
v-model="inputPassword"
|
||||
type="password"
|
||||
placeholder="请输入访问密码"
|
||||
size="large"
|
||||
@keyup.enter="submitPassword"
|
||||
class="auth-input"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Lock /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<div v-if="authError" class="auth-error">
|
||||
<el-icon><WarningFilled /></el-icon>
|
||||
<span>{{ authError }}</span>
|
||||
</div>
|
||||
<el-button type="primary" size="large" @click="submitPassword" :loading="authLoading" class="auth-button">
|
||||
<el-icon v-if="!authLoading"><Unlock /></el-icon>
|
||||
<span>确认登录</span>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 原有内容 - 只在已认证时显示 -->
|
||||
<el-card v-if="authed && !loading" class="playground-card">
|
||||
<div ref="playgroundContainer" class="playground-container" :class="{ 'dark-theme': isDarkMode, 'fullscreen': isFullscreen }">
|
||||
<el-card class="playground-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div class="header-left">
|
||||
<span class="title">JS解析器演练场</span>
|
||||
<!-- 语言显示(仅支持JavaScript) -->
|
||||
<span style="margin-left: 15px; color: var(--el-text-color-secondary); font-size: 12px;">
|
||||
JavaScript (ES5)
|
||||
</span>
|
||||
<!-- 语言选择器 -->
|
||||
<el-radio-group v-model="codeLanguage" size="small" style="margin-left: 15px;" @change="onLanguageChange">
|
||||
<el-radio-button :label="LANGUAGE.JAVASCRIPT">JavaScript</el-radio-button>
|
||||
<el-radio-button :label="LANGUAGE.TYPESCRIPT">TypeScript</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<!-- 主要操作 -->
|
||||
@@ -118,8 +67,8 @@
|
||||
<el-tabs v-model="activeTab" @tab-change="handleTabChange">
|
||||
<!-- 代码编辑标签页 -->
|
||||
<el-tab-pane label="代码编辑" name="editor">
|
||||
<Splitpanes :class="['default-theme', isMobile ? 'mobile-vertical' : '']" :horizontal="isMobile" @resized="handleResize">
|
||||
<!-- 编辑器区域 (PC: 左侧, Mobile: 上方) -->
|
||||
<Splitpanes class="default-theme" @resized="handleResize">
|
||||
<!-- 左侧:代码编辑区 -->
|
||||
<Pane :size="collapsedPanels.rightPanel ? 100 : splitSizes[0]" min-size="30" class="editor-pane">
|
||||
<div class="editor-section">
|
||||
<MonacoEditor
|
||||
@@ -133,9 +82,9 @@
|
||||
</div>
|
||||
</Pane>
|
||||
|
||||
<!-- 测试参数和结果区域 (PC: 右侧, Mobile: 下方) -->
|
||||
<!-- 右侧:测试参数和结果 -->
|
||||
<Pane v-if="!collapsedPanels.rightPanel"
|
||||
:size="splitSizes[1]" min-size="20" class="test-pane" :style="isMobile ? 'margin-top: 10px;' : 'margin-left: 10px;'">
|
||||
:size="splitSizes[1]" min-size="20" class="test-pane" style="margin-left: 10px;">
|
||||
<div class="test-section">
|
||||
<!-- 优化的折叠按钮 -->
|
||||
<el-tooltip content="折叠测试面板" placement="left">
|
||||
@@ -535,6 +484,7 @@ 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 {
|
||||
@@ -548,25 +498,15 @@ export default {
|
||||
setup() {
|
||||
// 语言常量
|
||||
const LANGUAGE = {
|
||||
JAVASCRIPT: 'JavaScript'
|
||||
JAVASCRIPT: 'JavaScript',
|
||||
TYPESCRIPT: 'TypeScript'
|
||||
};
|
||||
|
||||
const editorRef = ref(null);
|
||||
const jsCode = ref('');
|
||||
|
||||
// ===== 加载和认证状态 =====
|
||||
const loading = ref(true);
|
||||
const loadProgress = ref(0);
|
||||
const loadingMessage = ref('初始化...');
|
||||
const authChecking = ref(true);
|
||||
const authed = ref(false);
|
||||
const inputPassword = ref('');
|
||||
const authError = ref('');
|
||||
const authLoading = ref(false);
|
||||
|
||||
// ===== 移动端检测 =====
|
||||
const isMobile = ref(false);
|
||||
|
||||
const codeLanguage = ref(LANGUAGE.JAVASCRIPT); // 新增:代码语言选择
|
||||
const compiledES5Code = ref(''); // 新增:编译后的ES5代码
|
||||
const compileStatus = ref({ success: true, errors: [] }); // 新增:编译状态
|
||||
const testParams = ref({
|
||||
shareUrl: 'https://lanzoui.com/i7Aq12ab3cd',
|
||||
pwd: '',
|
||||
@@ -693,15 +633,94 @@ 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/(?<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(() => {
|
||||
return isDarkMode.value ? 'vs-dark' : 'vs';
|
||||
});
|
||||
|
||||
// 计算属性:是否需要显示密码输入界面
|
||||
const shouldShowAuthUI = computed(() => {
|
||||
return !loading.value && !authChecking.value && !authed.value;
|
||||
});
|
||||
|
||||
// 编辑器配置
|
||||
const editorOptions = {
|
||||
@@ -713,127 +732,6 @@ function parseById(shareLinkInfo, http, logger) {
|
||||
formatOnType: true,
|
||||
tabSize: 2
|
||||
};
|
||||
|
||||
// ===== 移动端检测 =====
|
||||
const updateIsMobile = () => {
|
||||
isMobile.value = window.innerWidth <= 768;
|
||||
};
|
||||
|
||||
// ===== 进度设置函数 =====
|
||||
const setProgress = (progress, message = '') => {
|
||||
if (progress > loadProgress.value) {
|
||||
loadProgress.value = progress;
|
||||
}
|
||||
if (message) {
|
||||
loadingMessage.value = message;
|
||||
}
|
||||
};
|
||||
|
||||
// ===== 认证相关函数 =====
|
||||
const checkAuthStatus = async () => {
|
||||
try {
|
||||
const res = await playgroundApi.getStatus();
|
||||
if (res.code === 200 && res.data) {
|
||||
authed.value = res.data.authed || res.data.public;
|
||||
return res.data.authed || res.data.public;
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('检查认证状态失败:', error);
|
||||
ElMessage.error('检查访问权限失败: ' + error.message);
|
||||
return false;
|
||||
} finally {
|
||||
authChecking.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const submitPassword = async () => {
|
||||
if (!inputPassword.value.trim()) {
|
||||
authError.value = '请输入密码';
|
||||
return;
|
||||
}
|
||||
|
||||
authError.value = '';
|
||||
authLoading.value = true;
|
||||
|
||||
try {
|
||||
const res = await playgroundApi.login(inputPassword.value);
|
||||
if (res.code === 200 || res.success) {
|
||||
authed.value = true;
|
||||
ElMessage.success('登录成功');
|
||||
await initPlayground();
|
||||
} else {
|
||||
authError.value = res.msg || res.message || '密码错误';
|
||||
}
|
||||
} catch (error) {
|
||||
authError.value = error.message || '登录失败,请重试';
|
||||
} finally {
|
||||
authLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// ===== Playground 初始化 =====
|
||||
const initPlayground = async () => {
|
||||
loading.value = true;
|
||||
loadProgress.value = 0;
|
||||
|
||||
try {
|
||||
setProgress(10, '初始化Vue组件...');
|
||||
await nextTick();
|
||||
|
||||
setProgress(20, '加载配置和本地数据...');
|
||||
// 加载保存的代码
|
||||
const saved = localStorage.getItem('playground_code');
|
||||
if (saved) {
|
||||
jsCode.value = saved;
|
||||
} else {
|
||||
jsCode.value = exampleCode;
|
||||
}
|
||||
|
||||
setProgress(50, '初始化Monaco Editor类型定义...');
|
||||
await initMonacoTypes();
|
||||
|
||||
setProgress(80, '加载完成...');
|
||||
|
||||
// 加载保存的主题
|
||||
const savedTheme = localStorage.getItem('playground_theme');
|
||||
if (savedTheme) {
|
||||
currentTheme.value = savedTheme;
|
||||
const theme = themes.find(t => t.name === savedTheme);
|
||||
if (theme && document.documentElement && document.body) {
|
||||
await nextTick();
|
||||
if (theme.page === 'dark') {
|
||||
document.documentElement.classList.add('dark');
|
||||
document.body.classList.add('dark-theme');
|
||||
document.body.style.backgroundColor = '#0a0a0a';
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
document.body.classList.remove('dark-theme');
|
||||
document.body.style.backgroundColor = '#f0f2f5';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 加载保存的折叠状态
|
||||
const savedCollapsed = localStorage.getItem('playground_collapsed_panels');
|
||||
if (savedCollapsed) {
|
||||
try {
|
||||
collapsedPanels.value = JSON.parse(savedCollapsed);
|
||||
} catch (e) {
|
||||
console.warn('加载折叠状态失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
setProgress(100, '初始化完成!');
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
|
||||
} catch (error) {
|
||||
console.error('初始化失败:', error);
|
||||
ElMessage.error('初始化失败: ' + error.message);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化Monaco Editor类型定义
|
||||
const initMonacoTypes = async () => {
|
||||
@@ -866,8 +764,8 @@ function parseById(shareLinkInfo, http, logger) {
|
||||
|
||||
// 加载示例代码
|
||||
const loadTemplate = () => {
|
||||
jsCode.value = exampleCode;
|
||||
ElMessage.success('已加载JavaScript示例代码');
|
||||
jsCode.value = codeLanguage.value === LANGUAGE.TYPESCRIPT ? exampleTypeScriptCode : exampleCode;
|
||||
ElMessage.success(`已加载${codeLanguage.value}示例代码`);
|
||||
};
|
||||
|
||||
// 格式化代码
|
||||
@@ -906,6 +804,59 @@ function parseById(shareLinkInfo, http, logger) {
|
||||
};
|
||||
|
||||
// 语言切换处理
|
||||
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()) {
|
||||
@@ -949,9 +900,67 @@ function parseById(shareLinkInfo, http, logger) {
|
||||
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(
|
||||
jsCode.value, // 直接使用JavaScript代码
|
||||
codeToExecute, // 使用编译后的代码或原始JS代码
|
||||
testParams.value.shareUrl,
|
||||
testParams.value.pwd,
|
||||
testParams.value.method
|
||||
@@ -1359,23 +1368,53 @@ curl "${baseUrl}/json/parser?url=${encodeURIComponent(exampleUrl)}"</pre>
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
// 初始化移动端检测
|
||||
updateIsMobile();
|
||||
window.addEventListener('resize', updateIsMobile);
|
||||
|
||||
// 检查认证状态
|
||||
const isAuthed = await checkAuthStatus();
|
||||
|
||||
// 如果已认证,初始化playground
|
||||
if (isAuthed) {
|
||||
await initPlayground();
|
||||
} else {
|
||||
// 未认证,停止加载动画,显示密码输入
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
await nextTick();
|
||||
checkDarkMode();
|
||||
await initMonacoTypes();
|
||||
|
||||
// 加载保存的代码
|
||||
const saved = localStorage.getItem('playground_code');
|
||||
if (saved) {
|
||||
jsCode.value = saved;
|
||||
} else {
|
||||
jsCode.value = exampleCode;
|
||||
}
|
||||
|
||||
// 加载保存的主题
|
||||
await nextTick();
|
||||
const savedTheme = localStorage.getItem('playground_theme');
|
||||
if (savedTheme) {
|
||||
currentTheme.value = savedTheme;
|
||||
const theme = themes.find(t => t.name === savedTheme);
|
||||
if (theme && document.documentElement && document.body) {
|
||||
await nextTick();
|
||||
if (theme.page === 'dark') {
|
||||
document.documentElement.classList.add('dark');
|
||||
document.body.classList.add('dark-theme');
|
||||
document.body.style.backgroundColor = '#0a0a0a';
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
document.body.classList.remove('dark-theme');
|
||||
document.body.style.backgroundColor = '#f0f2f5';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 加载保存的折叠状态
|
||||
const savedCollapsed = localStorage.getItem('playground_collapsed_panels');
|
||||
if (savedCollapsed) {
|
||||
try {
|
||||
collapsedPanels.value = JSON.parse(savedCollapsed);
|
||||
} catch (e) {
|
||||
console.warn('加载折叠状态失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 加载保存的语言选择
|
||||
const savedLanguage = localStorage.getItem('playground_language');
|
||||
if (savedLanguage) {
|
||||
codeLanguage.value = savedLanguage;
|
||||
}
|
||||
|
||||
// 监听主题变化
|
||||
if (document.documentElement) {
|
||||
@@ -1391,37 +1430,23 @@ curl "${baseUrl}/json/parser?url=${encodeURIComponent(exampleUrl)}"</pre>
|
||||
// 初始化splitpanes样式
|
||||
updateSplitpanesStyle();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', updateIsMobile);
|
||||
});
|
||||
|
||||
return {
|
||||
LANGUAGE,
|
||||
editorRef,
|
||||
jsCode,
|
||||
codeLanguage,
|
||||
compiledES5Code,
|
||||
compileStatus,
|
||||
testParams,
|
||||
testResult,
|
||||
testing,
|
||||
isDarkMode,
|
||||
editorTheme,
|
||||
shouldShowAuthUI,
|
||||
editorOptions,
|
||||
// 加载和认证
|
||||
loading,
|
||||
loadProgress,
|
||||
loadingMessage,
|
||||
authChecking,
|
||||
authed,
|
||||
inputPassword,
|
||||
authError,
|
||||
authLoading,
|
||||
checkAuthStatus,
|
||||
submitPassword,
|
||||
// 移动端
|
||||
isMobile,
|
||||
updateIsMobile,
|
||||
onCodeChange,
|
||||
onLanguageChange,
|
||||
compileTypeScriptCode,
|
||||
loadTemplate,
|
||||
formatCode,
|
||||
saveCode,
|
||||
@@ -1525,154 +1550,6 @@ body.dark-theme .splitpanes__splitter:hover,
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
/* ===== 加载动画和进度条 ===== */
|
||||
.playground-loading-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(255, 255, 255, 0.98);
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
.dark-theme .playground-loading-overlay {
|
||||
background: rgba(10, 10, 10, 0.98);
|
||||
}
|
||||
|
||||
.playground-loading-card {
|
||||
width: 320px;
|
||||
padding: 30px 40px;
|
||||
background: #fff;
|
||||
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.12);
|
||||
border-radius: 12px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.dark-theme .playground-loading-card {
|
||||
background: #1f1f1f;
|
||||
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.loading-icon {
|
||||
margin-bottom: 20px;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-primary);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.loading-bar {
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
background: var(--el-fill-color-light);
|
||||
border-radius: 3px;
|
||||
margin: 12px 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.loading-bar-inner {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--el-color-primary) 0%, var(--el-color-primary-light-3) 100%);
|
||||
transition: width 0.3s ease;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.loading-percent {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--el-color-primary);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.loading-details {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
/* ===== 认证界面 ===== */
|
||||
.playground-auth-loading {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: var(--el-bg-color);
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.playground-auth-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.playground-auth-card {
|
||||
width: 400px;
|
||||
padding: 40px;
|
||||
background: #fff;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
|
||||
border-radius: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dark-theme .playground-auth-card {
|
||||
background: #1f1f1f;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.auth-icon {
|
||||
margin-bottom: 24px;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.auth-title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.auth-subtitle {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.auth-input {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.auth-error {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
color: var(--el-color-danger);
|
||||
font-size: 13px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.auth-button {
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ===== 容器布局 ===== */
|
||||
.playground-container {
|
||||
padding: 10px 20px;
|
||||
@@ -2636,40 +2513,6 @@ html.dark .playground-container .splitpanes__splitter:hover {
|
||||
}
|
||||
|
||||
/* ===== 响应式布局 ===== */
|
||||
/* 移动端纵向布局 */
|
||||
.playground-container.is-mobile .splitpanes.mobile-vertical {
|
||||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
.playground-container.is-mobile .splitpanes--horizontal > .splitpanes__splitter {
|
||||
height: 6px;
|
||||
width: 100%;
|
||||
cursor: row-resize;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.playground-container.is-mobile .editor-pane,
|
||||
.playground-container.is-mobile .test-pane {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.playground-container.is-mobile .test-pane {
|
||||
margin-left: 0 !important;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.playground-container.is-mobile .playground-loading-card {
|
||||
width: 90%;
|
||||
max-width: 320px;
|
||||
padding: 24px 30px;
|
||||
}
|
||||
|
||||
.playground-container.is-mobile .playground-auth-card {
|
||||
width: 90%;
|
||||
max-width: 400px;
|
||||
padding: 30px 24px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1200px) {
|
||||
.splitpanes {
|
||||
min-height: 400px;
|
||||
@@ -2712,10 +2555,6 @@ html.dark .playground-container .splitpanes__splitter:hover {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.splitpanes {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== 改进的滚动条样式 ===== */
|
||||
|
||||
@@ -4,13 +4,7 @@ import cn.qaiu.WebClientVertxInit;
|
||||
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;
|
||||
@@ -18,7 +12,6 @@ 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;
|
||||
@@ -32,7 +25,6 @@ import static cn.qaiu.vx.core.util.ConfigConstant.LOCAL;
|
||||
* <br>Create date 2021-05-08 13:00:01
|
||||
* @author qaiu yyzy
|
||||
*/
|
||||
@Slf4j
|
||||
public class AppMain {
|
||||
|
||||
public static void main(String[] args) {
|
||||
@@ -62,10 +54,6 @@ 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);
|
||||
});
|
||||
@@ -100,44 +88,5 @@ public class AppMain {
|
||||
JsonObject auths = jsonObject.getJsonObject(ConfigConstant.AUTHS);
|
||||
localMap.put(ConfigConstant.AUTHS, auths);
|
||||
}
|
||||
|
||||
// 演练场配置
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
package cn.qaiu.lz.web.config;
|
||||
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import lombok.Data;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* JS演练场配置
|
||||
*
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
*/
|
||||
@Data
|
||||
@Slf4j
|
||||
public class PlaygroundConfig {
|
||||
|
||||
/**
|
||||
* 单例实例
|
||||
*/
|
||||
private static PlaygroundConfig instance;
|
||||
|
||||
/**
|
||||
* 是否公开模式(不需要密码)
|
||||
* 默认false,需要密码访问
|
||||
*/
|
||||
private boolean isPublic = false;
|
||||
|
||||
/**
|
||||
* 访问密码
|
||||
* 默认密码:nfd_playground_2024
|
||||
*/
|
||||
private String password = "nfd_playground_2024";
|
||||
|
||||
/**
|
||||
* 私有构造函数
|
||||
*/
|
||||
private PlaygroundConfig() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单例实例
|
||||
*/
|
||||
public static PlaygroundConfig getInstance() {
|
||||
if (instance == null) {
|
||||
synchronized (PlaygroundConfig.class) {
|
||||
if (instance == null) {
|
||||
instance = new PlaygroundConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从JsonObject加载配置
|
||||
*/
|
||||
public static void loadFromJson(JsonObject config) {
|
||||
PlaygroundConfig cfg = getInstance();
|
||||
if (config != null && config.containsKey("playground")) {
|
||||
JsonObject playgroundConfig = config.getJsonObject("playground");
|
||||
cfg.isPublic = playgroundConfig.getBoolean("public", false);
|
||||
cfg.password = playgroundConfig.getString("password", "nfd_playground_2024");
|
||||
|
||||
log.info("Playground配置已加载: public={}, password={}",
|
||||
cfg.isPublic, cfg.isPublic ? "N/A" : "已设置");
|
||||
|
||||
if (!cfg.isPublic && "nfd_playground_2024".equals(cfg.password)) {
|
||||
log.warn("⚠️ 警告:您正在使用默认密码,建议修改配置文件中的 playground.password 以确保安全!");
|
||||
}
|
||||
} else {
|
||||
log.info("未找到playground配置,使用默认值: public=false");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,9 @@
|
||||
package cn.qaiu.lz.web.controller;
|
||||
|
||||
import cn.qaiu.entity.ShareLinkInfo;
|
||||
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;
|
||||
@@ -22,7 +19,6 @@ import io.vertx.core.http.HttpServerRequest;
|
||||
import io.vertx.core.http.HttpServerResponse;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import io.vertx.ext.web.RoutingContext;
|
||||
import io.vertx.ext.web.Session;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
@@ -32,8 +28,6 @@ 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;
|
||||
|
||||
/**
|
||||
@@ -48,92 +42,7 @@ public class PlaygroundApi {
|
||||
|
||||
private static final int MAX_PARSER_COUNT = 100;
|
||||
private static final int MAX_CODE_LENGTH = 128 * 1024; // 128KB 代码长度限制
|
||||
private static final String SESSION_AUTH_KEY = "playgroundAuthed";
|
||||
private final DbService dbService = AsyncServiceUtil.getAsyncServiceInstance(DbService.class);
|
||||
|
||||
/**
|
||||
* 检查Playground访问权限
|
||||
*/
|
||||
private boolean checkAuth(RoutingContext ctx) {
|
||||
PlaygroundConfig config = PlaygroundConfig.getInstance();
|
||||
|
||||
// 如果是公开模式,直接允许访问
|
||||
if (config.isPublic()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 否则检查Session中的认证状态
|
||||
Session session = ctx.session();
|
||||
if (session == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Boolean authed = session.get(SESSION_AUTH_KEY);
|
||||
return authed != null && authed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Playground状态(是否需要认证)
|
||||
*/
|
||||
@RouteMapping(value = "/status", method = RouteMethod.GET)
|
||||
public Future<JsonObject> getStatus(RoutingContext ctx) {
|
||||
PlaygroundConfig config = PlaygroundConfig.getInstance();
|
||||
boolean authed = checkAuth(ctx);
|
||||
|
||||
JsonObject result = new JsonObject()
|
||||
.put("public", config.isPublic())
|
||||
.put("authed", authed);
|
||||
|
||||
return Future.succeededFuture(JsonResult.data(result).toJsonObject());
|
||||
}
|
||||
|
||||
/**
|
||||
* Playground登录
|
||||
*/
|
||||
@RouteMapping(value = "/login", method = RouteMethod.POST)
|
||||
public Future<JsonObject> login(RoutingContext ctx) {
|
||||
Promise<JsonObject> promise = Promise.promise();
|
||||
|
||||
try {
|
||||
PlaygroundConfig config = PlaygroundConfig.getInstance();
|
||||
|
||||
// 如果是公开模式,直接成功
|
||||
if (config.isPublic()) {
|
||||
Session session = ctx.session();
|
||||
if (session != null) {
|
||||
session.put(SESSION_AUTH_KEY, true);
|
||||
}
|
||||
promise.complete(JsonResult.success("公开模式,无需密码").toJsonObject());
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
// 获取密码
|
||||
JsonObject body = ctx.body().asJsonObject();
|
||||
String password = body.getString("password");
|
||||
|
||||
if (StringUtils.isBlank(password)) {
|
||||
promise.complete(JsonResult.error("密码不能为空").toJsonObject());
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
if (config.getPassword().equals(password)) {
|
||||
Session session = ctx.session();
|
||||
if (session != null) {
|
||||
session.put(SESSION_AUTH_KEY, true);
|
||||
}
|
||||
promise.complete(JsonResult.success("登录成功").toJsonObject());
|
||||
} else {
|
||||
promise.complete(JsonResult.error("密码错误").toJsonObject());
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("登录失败", e);
|
||||
promise.complete(JsonResult.error("登录失败: " + e.getMessage()).toJsonObject());
|
||||
}
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试执行JavaScript代码
|
||||
@@ -143,11 +52,6 @@ public class PlaygroundApi {
|
||||
*/
|
||||
@RouteMapping(value = "/test", method = RouteMethod.POST)
|
||||
public Future<JsonObject> test(RoutingContext ctx) {
|
||||
// 权限检查
|
||||
if (!checkAuth(ctx)) {
|
||||
return Future.succeededFuture(JsonResult.error("未授权访问").toJsonObject());
|
||||
}
|
||||
|
||||
Promise<JsonObject> promise = Promise.promise();
|
||||
|
||||
try {
|
||||
@@ -182,32 +86,6 @@ 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)) {
|
||||
@@ -341,17 +219,10 @@ public class PlaygroundApi {
|
||||
/**
|
||||
* 获取types.js文件内容
|
||||
*
|
||||
* @param ctx 路由上下文
|
||||
* @param response HTTP响应
|
||||
*/
|
||||
@RouteMapping(value = "/types.js", method = RouteMethod.GET)
|
||||
public void getTypesJs(RoutingContext ctx, HttpServerResponse response) {
|
||||
// 权限检查
|
||||
if (!checkAuth(ctx)) {
|
||||
ResponseUtil.fireJsonResultResponse(response, JsonResult.error("未授权访问"));
|
||||
return;
|
||||
}
|
||||
|
||||
public void getTypesJs(HttpServerResponse response) {
|
||||
try (InputStream inputStream = getClass().getClassLoader()
|
||||
.getResourceAsStream("custom-parsers/types.js")) {
|
||||
|
||||
@@ -377,11 +248,7 @@ public class PlaygroundApi {
|
||||
* 获取解析器列表
|
||||
*/
|
||||
@RouteMapping(value = "/parsers", method = RouteMethod.GET)
|
||||
public Future<JsonObject> getParserList(RoutingContext ctx) {
|
||||
// 权限检查
|
||||
if (!checkAuth(ctx)) {
|
||||
return Future.succeededFuture(JsonResult.error("未授权访问").toJsonObject());
|
||||
}
|
||||
public Future<JsonObject> getParserList() {
|
||||
return dbService.getPlaygroundParserList();
|
||||
}
|
||||
|
||||
@@ -390,11 +257,6 @@ public class PlaygroundApi {
|
||||
*/
|
||||
@RouteMapping(value = "/parsers", method = RouteMethod.POST)
|
||||
public Future<JsonObject> saveParser(RoutingContext ctx) {
|
||||
// 权限检查
|
||||
if (!checkAuth(ctx)) {
|
||||
return Future.succeededFuture(JsonResult.error("未授权访问").toJsonObject());
|
||||
}
|
||||
|
||||
Promise<JsonObject> promise = Promise.promise();
|
||||
|
||||
try {
|
||||
@@ -463,18 +325,7 @@ public class PlaygroundApi {
|
||||
parser.put("enabled", true);
|
||||
|
||||
dbService.savePlaygroundParser(parser).onSuccess(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());
|
||||
}
|
||||
promise.complete(result);
|
||||
}).onFailure(e -> {
|
||||
log.error("保存解析器失败", e);
|
||||
promise.complete(JsonResult.error("保存失败: " + e.getMessage()).toJsonObject());
|
||||
@@ -505,11 +356,6 @@ public class PlaygroundApi {
|
||||
*/
|
||||
@RouteMapping(value = "/parsers/:id", method = RouteMethod.PUT)
|
||||
public Future<JsonObject> updateParser(RoutingContext ctx, Long id) {
|
||||
// 权限检查
|
||||
if (!checkAuth(ctx)) {
|
||||
return Future.succeededFuture(JsonResult.error("未授权访问").toJsonObject());
|
||||
}
|
||||
|
||||
Promise<JsonObject> promise = Promise.promise();
|
||||
|
||||
try {
|
||||
@@ -524,14 +370,12 @@ 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);
|
||||
@@ -541,29 +385,10 @@ public class PlaygroundApi {
|
||||
parser.put("version", version);
|
||||
parser.put("matchPattern", matchPattern);
|
||||
parser.put("jsCode", jsCode);
|
||||
parser.put("enabled", enabled);
|
||||
parser.put("enabled", body.getBoolean("enabled", true));
|
||||
|
||||
dbService.updatePlaygroundParser(id, parser).onSuccess(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());
|
||||
}
|
||||
promise.complete(result);
|
||||
}).onFailure(e -> {
|
||||
log.error("更新解析器失败", e);
|
||||
promise.complete(JsonResult.error("更新失败: " + e.getMessage()).toJsonObject());
|
||||
@@ -585,54 +410,15 @@ public class PlaygroundApi {
|
||||
* 删除解析器
|
||||
*/
|
||||
@RouteMapping(value = "/parsers/:id", method = RouteMethod.DELETE)
|
||||
public Future<JsonObject> deleteParser(RoutingContext ctx, Long id) {
|
||||
// 权限检查
|
||||
if (!checkAuth(ctx)) {
|
||||
return Future.succeededFuture(JsonResult.error("未授权访问").toJsonObject());
|
||||
}
|
||||
|
||||
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();
|
||||
public Future<JsonObject> deleteParser(Long id) {
|
||||
return dbService.deletePlaygroundParser(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取解析器
|
||||
*/
|
||||
@RouteMapping(value = "/parsers/:id", method = RouteMethod.GET)
|
||||
public Future<JsonObject> getParserById(RoutingContext ctx, Long id) {
|
||||
// 权限检查
|
||||
if (!checkAuth(ctx)) {
|
||||
return Future.succeededFuture(JsonResult.error("未授权访问").toJsonObject());
|
||||
}
|
||||
public Future<JsonObject> getParserById(Long id) {
|
||||
return dbService.getPlaygroundParserById(id);
|
||||
}
|
||||
|
||||
@@ -641,11 +427,6 @@ public class PlaygroundApi {
|
||||
*/
|
||||
@RouteMapping(value = "/typescript", method = RouteMethod.POST)
|
||||
public Future<JsonObject> saveTypeScriptCode(RoutingContext ctx) {
|
||||
// 权限检查
|
||||
if (!checkAuth(ctx)) {
|
||||
return Future.succeededFuture(JsonResult.error("未授权访问").toJsonObject());
|
||||
}
|
||||
|
||||
Promise<JsonObject> promise = Promise.promise();
|
||||
|
||||
try {
|
||||
@@ -708,11 +489,7 @@ public class PlaygroundApi {
|
||||
* 根据parserId获取TypeScript代码
|
||||
*/
|
||||
@RouteMapping(value = "/typescript/:parserId", method = RouteMethod.GET)
|
||||
public Future<JsonObject> getTypeScriptCode(RoutingContext ctx, Long parserId) {
|
||||
// 权限检查
|
||||
if (!checkAuth(ctx)) {
|
||||
return Future.succeededFuture(JsonResult.error("未授权访问").toJsonObject());
|
||||
}
|
||||
public Future<JsonObject> getTypeScriptCode(Long parserId) {
|
||||
return dbService.getTypeScriptCodeByParserId(parserId);
|
||||
}
|
||||
|
||||
@@ -721,11 +498,6 @@ public class PlaygroundApi {
|
||||
*/
|
||||
@RouteMapping(value = "/typescript/:parserId", method = RouteMethod.PUT)
|
||||
public Future<JsonObject> updateTypeScriptCode(RoutingContext ctx, Long parserId) {
|
||||
// 权限检查
|
||||
if (!checkAuth(ctx)) {
|
||||
return Future.succeededFuture(JsonResult.error("未授权访问").toJsonObject());
|
||||
}
|
||||
|
||||
Promise<JsonObject> promise = Promise.promise();
|
||||
|
||||
try {
|
||||
|
||||
@@ -13,13 +13,6 @@ server:
|
||||
# 反向代理服务器配置路径(不用加后缀)
|
||||
proxyConf: server-proxy
|
||||
|
||||
# JS演练场配置
|
||||
playground:
|
||||
# 公开模式,默认false需要密码访问,设为true则无需密码
|
||||
public: false
|
||||
# 访问密码,建议修改默认密码!
|
||||
password: 'nfd_playground_2024'
|
||||
|
||||
# vertx核心线程配置(一般无需改的), 为0表示eventLoopPoolSize将会采用默认配置(CPU核心*2) workerPoolSize将会采用默认20
|
||||
vertx:
|
||||
eventLoopPoolSize: 0
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
### 安全漏洞修复测试 - 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)
|
||||
|
||||
Reference in New Issue
Block a user