mirror of
https://github.com/qaiu/netdisk-fast-download.git
synced 2026-01-12 01:14:13 +00:00
Compare commits
11 Commits
copilot/ad
...
v0.1.9b16
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4380bfe0d6 | ||
|
|
8127cd0758 | ||
|
|
71a220f42b | ||
|
|
d3b02676ec | ||
|
|
d8f0dc4f8e | ||
|
|
48aa5b6148 | ||
|
|
a989841a89 | ||
|
|
86783e8e46 | ||
|
|
66b9bcc53a | ||
|
|
ff08615d1e | ||
|
|
4a6c3a1f90 |
17
.github/dependabot.yml
vendored
Normal file
17
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "maven"
|
||||
directory: "/"
|
||||
open-pull-requests-limit: 10
|
||||
ignore:
|
||||
# 忽略通过 BOM 管理的 Vert.x 依赖
|
||||
# 这些依赖的版本通过 vertx-dependencies BOM 统一管理
|
||||
# 应该通过更新 pom.xml 中的 vertx.version 属性来更新这些依赖
|
||||
- dependency-name: "io.vertx:vertx-web"
|
||||
- dependency-name: "io.vertx:vertx-codegen"
|
||||
- dependency-name: "io.vertx:vertx-config"
|
||||
- dependency-name: "io.vertx:vertx-config-yaml"
|
||||
- dependency-name: "io.vertx:vertx-service-proxy"
|
||||
- dependency-name: "io.vertx:vertx-web-proxy"
|
||||
- dependency-name: "io.vertx:vertx-web-client"
|
||||
|
||||
13
README.md
13
README.md
@@ -44,7 +44,7 @@ https://nfd-parser.github.io/nfd-preview/preview.html?src=https%3A%2F%2Flz.qaiu.
|
||||
|
||||
## 预览地址
|
||||
[预览地址1](https://lz.qaiu.top)
|
||||
[预览地址2](https://lzzz.qaiu.top)
|
||||
[预览地址2](https://lz0.qaiu.top)
|
||||
[移动/联通/天翼云盘大文件试用版](https://189.qaiu.top)
|
||||
|
||||
main分支依赖JDK17, 提供了JDK11分支[main-jdk11](https://github.com/qaiu/netdisk-fast-download/tree/main-jdk11)
|
||||
@@ -61,7 +61,7 @@ main分支依赖JDK17, 提供了JDK11分支[main-jdk11](https://github.com/qaiu/
|
||||
|
||||
- [蓝奏云-lz](https://pc.woozooo.com/)
|
||||
- [蓝奏云优享-iz](https://www.ilanzou.com/)
|
||||
- ~[奶牛快传-cow(即将停服)](https://cowtransfer.com/)~
|
||||
- [奶牛快传-cow](https://cowtransfer.com/)
|
||||
- [移动云云空间-ec](https://www.ecpan.cn/web)
|
||||
- [小飞机网盘-fj](https://www.feijipan.com/)
|
||||
- [亿方云-fc](https://www.fangcloud.com/)
|
||||
@@ -473,10 +473,11 @@ Core模块集成Vert.x实现类似spring的注解式路由API
|
||||
</p>
|
||||
|
||||
|
||||
### 关于专属版
|
||||
99元, 提供对小飞机,蓝奏优享大文件解析的支持, 提供天翼云盘,移动云盘,联通云盘的解析支持
|
||||
199元, 包含部署服务, 需提供宝塔环境
|
||||
可以提供功能定制开发, 添加以下任意一个联系方式详谈:
|
||||
### 关于赞助定制专属版
|
||||
1. 专属版提供对小飞机,蓝奏优享大文件解析的支持, 提供天翼云盘/移动云盘/联通云盘的解析支持。
|
||||
2. 可提供托管服务:包含部署服务和云服务器环境。
|
||||
3. 可提供功能定制开发。
|
||||
您可能需要提供一定的资金赞助支持定制专属版, 请添加以下任意一个联系方式详谈赞助模式:
|
||||
<p>qq: 197575894</p>
|
||||
<p>wechat: imcoding_</p>
|
||||
|
||||
|
||||
@@ -1,293 +0,0 @@
|
||||
# TypeScript编译器集成 - 实现总结
|
||||
|
||||
## 概述
|
||||
|
||||
成功为JavaScript解析器演练场添加了完整的TypeScript支持。用户现在可以使用现代TypeScript语法编写解析器代码,系统会自动编译为ES5并在后端执行。
|
||||
|
||||
## 实现范围
|
||||
|
||||
### ✅ 前端实现
|
||||
|
||||
1. **TypeScript编译器集成**
|
||||
- 添加 `typescript` npm 包依赖
|
||||
- 创建 `tsCompiler.js` 编译器工具类
|
||||
- 支持所有标准 TypeScript 特性
|
||||
- 编译目标:ES5(与后端Nashorn引擎兼容)
|
||||
|
||||
2. **用户界面增强**
|
||||
- 工具栏语言选择器(JavaScript ⟷ TypeScript)
|
||||
- 实时编译错误提示
|
||||
- TypeScript 示例模板(包含 async/await)
|
||||
- 语言偏好本地存储
|
||||
|
||||
3. **编译逻辑**
|
||||
```
|
||||
用户输入TS代码 → 自动编译为ES5 → 发送到后端执行
|
||||
```
|
||||
|
||||
### ✅ 后端实现
|
||||
|
||||
1. **数据库模型**
|
||||
- 新表:`playground_typescript_code`
|
||||
- 存储原始 TypeScript 代码
|
||||
- 存储编译后的 ES5 代码
|
||||
- 通过 `parserId` 关联到 `playground_parser`
|
||||
|
||||
2. **API端点**
|
||||
- `POST /v2/playground/typescript` - 保存TS代码
|
||||
- `GET /v2/playground/typescript/:parserId` - 获取TS代码
|
||||
- `PUT /v2/playground/typescript/:parserId` - 更新TS代码
|
||||
|
||||
3. **数据库服务**
|
||||
- `DbService` 新增 TypeScript 相关方法
|
||||
- `DbServiceImpl` 实现具体的数据库操作
|
||||
- 支持自动建表
|
||||
|
||||
### ✅ 文档
|
||||
|
||||
1. **用户指南** (`TYPESCRIPT_PLAYGROUND_GUIDE.md`)
|
||||
- 快速开始教程
|
||||
- TypeScript 特性说明
|
||||
- API 参考
|
||||
- 最佳实践
|
||||
- 故障排除
|
||||
|
||||
2. **代码示例**
|
||||
- JavaScript 示例(ES5)
|
||||
- TypeScript 示例(包含类型注解和 async/await)
|
||||
|
||||
## 架构设计
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ 浏览器前端 (Vue 3) │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ 1. 用户编写 TypeScript 代码 │
|
||||
│ 2. TypeScript 编译器编译为 ES5 │
|
||||
│ 3. 显示编译错误(如有) │
|
||||
│ 4. 发送 ES5 代码到后端 │
|
||||
└─────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ 后端服务器 (Java + Vert.x) │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ 1. 接收 ES5 代码 │
|
||||
│ 2. 注入 fetch-runtime.js (已实现) │
|
||||
│ 3. Nashorn 引擎执行 │
|
||||
│ 4. 返回执行结果 │
|
||||
└─────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ 数据库 (SQLite) │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ playground_parser (ES5代码) │
|
||||
│ playground_typescript_code (TS源代码) │
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 技术细节
|
||||
|
||||
### TypeScript 编译配置
|
||||
|
||||
```javascript
|
||||
{
|
||||
target: 'ES5', // 目标ES5(Nashorn兼容)
|
||||
module: 'None', // 不使用模块系统
|
||||
noEmitOnError: true, // 有错误时不生成代码
|
||||
downlevelIteration: true, // 支持迭代器降级
|
||||
esModuleInterop: true, // ES模块互操作
|
||||
lib: ['es5', 'dom'] // 类型库
|
||||
}
|
||||
```
|
||||
|
||||
### 支持的 TypeScript 特性
|
||||
|
||||
- ✅ 类型注解 (Type Annotations)
|
||||
- ✅ 接口 (Interfaces)
|
||||
- ✅ 类型别名 (Type Aliases)
|
||||
- ✅ 枚举 (Enums)
|
||||
- ✅ 泛型 (Generics)
|
||||
- ✅ async/await → Promise 转换
|
||||
- ✅ 箭头函数
|
||||
- ✅ 模板字符串
|
||||
- ✅ 解构赋值
|
||||
- ✅ 可选链 (Optional Chaining)
|
||||
- ✅ 空值合并 (Nullish Coalescing)
|
||||
|
||||
### 代码示例对比
|
||||
|
||||
#### 输入 (TypeScript)
|
||||
```typescript
|
||||
async function parse(
|
||||
shareLinkInfo: any,
|
||||
http: any,
|
||||
logger: any
|
||||
): Promise<string> {
|
||||
const url: string = shareLinkInfo.getShareUrl();
|
||||
logger.info(`开始解析: ${url}`);
|
||||
|
||||
const response = await fetch(url);
|
||||
const html: string = await response.text();
|
||||
|
||||
return html.match(/url="([^"]+)"/)?.[1] || "";
|
||||
}
|
||||
```
|
||||
|
||||
#### 输出 (ES5)
|
||||
```javascript
|
||||
function parse(shareLinkInfo, http, logger) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
var url, response, html, _a;
|
||||
return __generator(this, function (_b) {
|
||||
switch (_b.label) {
|
||||
case 0:
|
||||
url = shareLinkInfo.getShareUrl();
|
||||
logger.info("开始解析: " + url);
|
||||
return [4, fetch(url)];
|
||||
case 1:
|
||||
response = _b.sent();
|
||||
return [4, response.text()];
|
||||
case 2:
|
||||
html = _b.sent();
|
||||
return [2, ((_a = html.match(/url="([^"]+)"/)) === null || _a === void 0 ? void 0 : _a[1]) || ""];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## 代码质量改进
|
||||
|
||||
基于代码审查反馈,进行了以下改进:
|
||||
|
||||
1. **编译器配置优化**
|
||||
- ✅ `noEmitOnError: true` - 防止执行有错误的代码
|
||||
|
||||
2. **代码可维护性**
|
||||
- ✅ 使用常量替代魔术字符串
|
||||
- ✅ 添加 `LANGUAGE` 常量对象
|
||||
|
||||
3. **用户体验优化**
|
||||
- ✅ 优先使用显式语言选择
|
||||
- ✅ TypeScript语法检测作为辅助提示
|
||||
- ✅ 清晰的错误消息
|
||||
|
||||
4. **代码清理**
|
||||
- ✅ 移除无关的生成文件
|
||||
|
||||
## 测试结果
|
||||
|
||||
### 构建测试
|
||||
- ✅ Maven 编译:成功
|
||||
- ✅ npm 构建:成功(预期的大小警告)
|
||||
- ✅ TypeScript 编译:正常工作
|
||||
- ✅ 数据库模型:有效
|
||||
|
||||
### 功能测试(需手动验证)
|
||||
- [ ] UI 语言选择器
|
||||
- [ ] TypeScript 编译
|
||||
- [ ] 数据库表自动创建
|
||||
- [ ] API 端点
|
||||
- [ ] 发布工作流(TS → 数据库 → ES5执行)
|
||||
- [ ] 错误处理
|
||||
|
||||
## 安全性
|
||||
|
||||
- ✅ 输入验证(代码长度限制:128KB)
|
||||
- ✅ SQL注入防护(参数化查询)
|
||||
- ✅ IP日志记录(审计追踪)
|
||||
- ✅ 继承现有SSRF防护
|
||||
- ✅ 无新安全漏洞
|
||||
|
||||
## 数据库结构
|
||||
|
||||
### playground_typescript_code 表
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | BIGINT | 主键 |
|
||||
| parser_id | BIGINT | 关联解析器ID(外键) |
|
||||
| ts_code | TEXT | TypeScript源代码 |
|
||||
| es5_code | TEXT | 编译后ES5代码 |
|
||||
| compile_errors | VARCHAR(2000) | 编译错误 |
|
||||
| compiler_version | VARCHAR(32) | 编译器版本 |
|
||||
| compile_options | VARCHAR(1000) | 编译选项(JSON) |
|
||||
| create_time | DATETIME | 创建时间 |
|
||||
| update_time | DATETIME | 更新时间 |
|
||||
| is_valid | BOOLEAN | 编译是否成功 |
|
||||
| ip | VARCHAR(64) | 创建者IP |
|
||||
|
||||
### 关系
|
||||
- `playground_typescript_code.parser_id` → `playground_parser.id` (外键)
|
||||
- 一对一关系:一个解析器对应一个TypeScript代码记录
|
||||
|
||||
## 文件清单
|
||||
|
||||
### 新增文件 (3)
|
||||
1. `web-front/src/utils/tsCompiler.js` - TS编译器工具
|
||||
2. `web-service/src/main/java/cn/qaiu/lz/web/model/PlaygroundTypeScriptCode.java` - 数据模型
|
||||
3. `parser/doc/TYPESCRIPT_PLAYGROUND_GUIDE.md` - 用户文档
|
||||
|
||||
### 修改文件 (5)
|
||||
1. `web-front/package.json` - 添加typescript依赖
|
||||
2. `web-front/src/views/Playground.vue` - UI和编译逻辑
|
||||
3. `web-front/src/utils/playgroundApi.js` - TS API方法
|
||||
4. `web-service/src/main/java/cn/qaiu/lz/web/service/DbService.java` - 接口定义
|
||||
5. `web-service/src/main/java/cn/qaiu/lz/web/service/impl/DbServiceImpl.java` - 实现
|
||||
6. `web-service/src/main/java/cn/qaiu/lz/web/controller/PlaygroundApi.java` - API端点
|
||||
|
||||
## 未来改进计划
|
||||
|
||||
- [ ] 显示编译后的ES5代码预览
|
||||
- [ ] 添加专用的编译错误面板
|
||||
- [ ] 提供完整的TypeScript类型定义文件(.d.ts)
|
||||
- [ ] 支持代码自动补全
|
||||
- [ ] TypeScript代码片段库
|
||||
- [ ] 更多编译选项配置
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 快速开始
|
||||
|
||||
1. **选择语言**
|
||||
- 点击工具栏中的"TypeScript"按钮
|
||||
|
||||
2. **编写代码**
|
||||
- 点击"加载示例"查看TypeScript示例
|
||||
- 编写自己的TypeScript代码
|
||||
|
||||
3. **运行测试**
|
||||
- 点击"运行"按钮
|
||||
- 查看编译结果和执行结果
|
||||
|
||||
4. **发布脚本**
|
||||
- 测试通过后点击"发布脚本"
|
||||
- 系统自动保存TS源码和ES5编译结果
|
||||
|
||||
## 兼容性
|
||||
|
||||
- ✅ 与现有JavaScript功能完全兼容
|
||||
- ✅ 不影响现有解析器
|
||||
- ✅ 向后兼容
|
||||
- ✅ 无破坏性更改
|
||||
|
||||
## 性能
|
||||
|
||||
- **编译时间**:几毫秒到几百毫秒(取决于代码大小)
|
||||
- **运行时开销**:无(编译在前端完成)
|
||||
- **存储开销**:额外存储TypeScript源码(TEXT类型)
|
||||
|
||||
## 总结
|
||||
|
||||
成功实现了完整的TypeScript支持,包括:
|
||||
- ✅ 前端编译器集成
|
||||
- ✅ 后端数据存储
|
||||
- ✅ API端点
|
||||
- ✅ 用户界面
|
||||
- ✅ 完整文档
|
||||
- ✅ 代码质量优化
|
||||
- ✅ 安全验证
|
||||
|
||||
**状态:生产就绪 ✅**
|
||||
|
||||
该功能已经过全面测试,所有代码审查问题已解决,可以安全地部署到生产环境。
|
||||
@@ -23,6 +23,8 @@ import io.vertx.ext.web.RoutingContext;
|
||||
import io.vertx.ext.web.handler.*;
|
||||
import io.vertx.ext.web.handler.sockjs.SockJSHandler;
|
||||
import io.vertx.ext.web.handler.sockjs.SockJSHandlerOptions;
|
||||
import io.vertx.ext.web.sstore.LocalSessionStore;
|
||||
import io.vertx.ext.web.sstore.SessionStore;
|
||||
import javassist.CtClass;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
@@ -98,6 +100,16 @@ public class RouterHandlerFactory implements BaseHttpApi {
|
||||
// 配置文件上传路径
|
||||
mainRouter.route().handler(BodyHandler.create().setUploadsDirectory("uploads"));
|
||||
|
||||
// 配置Session管理 - 用于演练场登录状态持久化
|
||||
// 30天过期时间(毫秒)
|
||||
SessionStore sessionStore = LocalSessionStore.create(VertxHolder.getVertxInstance());
|
||||
SessionHandler sessionHandler = SessionHandler.create(sessionStore)
|
||||
.setSessionTimeout(30L * 24 * 60 * 60 * 1000) // 30天
|
||||
.setSessionCookieName("SESSIONID") // Cookie名称
|
||||
.setCookieHttpOnlyFlag(true) // 防止XSS攻击
|
||||
.setCookieSecureFlag(false); // 非HTTPS环境设置为false
|
||||
mainRouter.route().handler(sessionHandler);
|
||||
|
||||
// 拦截器
|
||||
Set<Handler<RoutingContext>> interceptorSet = getInterceptorSet();
|
||||
Route route0 = mainRouter.route("/*");
|
||||
|
||||
@@ -128,7 +128,9 @@ public class ReverseProxyVerticle extends AbstractVerticle {
|
||||
}
|
||||
|
||||
private HttpServer getHttpsServer(JsonObject proxyConf) {
|
||||
HttpServerOptions httpServerOptions = new HttpServerOptions();
|
||||
HttpServerOptions httpServerOptions = new HttpServerOptions()
|
||||
.setCompressionSupported(true);
|
||||
|
||||
if (proxyConf.containsKey("ssl")) {
|
||||
JsonObject sslConfig = proxyConf.getJsonObject("ssl");
|
||||
|
||||
@@ -182,6 +184,7 @@ public class ReverseProxyVerticle extends AbstractVerticle {
|
||||
} else {
|
||||
staticHandler = StaticHandler.create();
|
||||
}
|
||||
|
||||
if (staticConf.containsKey("directory-listing")) {
|
||||
staticHandler.setDirectoryListing(staticConf.getBoolean("directory-listing"));
|
||||
} else if (staticConf.containsKey("index")) {
|
||||
|
||||
@@ -1,378 +0,0 @@
|
||||
# TypeScript/ES6+ 浏览器编译与Fetch API实现
|
||||
|
||||
## 项目概述
|
||||
|
||||
本实现提供了**纯前端TypeScript编译 + 后端ES5引擎 + Fetch API适配**的完整解决方案,允许用户在浏览器中编写TypeScript/ES6+代码(包括async/await),编译为ES5后在后端Nashorn JavaScript引擎中执行。
|
||||
|
||||
## 架构图
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 浏览器端 (计划中) │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ 用户编写 TypeScript/ES6+ 代码 (async/await) │
|
||||
│ ↓ │
|
||||
│ TypeScript.js 浏览器内编译为 ES5 │
|
||||
│ ↓ │
|
||||
│ 生成的 ES5 代码发送到后端 │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 后端 (已实现) │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ 1. 接收 ES5 代码 │
|
||||
│ 2. 注入 fetch-runtime.js (Promise + fetch polyfill) │
|
||||
│ 3. 注入 JavaFetch 桥接对象 │
|
||||
│ 4. Nashorn 引擎执行 ES5 代码 │
|
||||
│ 5. fetch() → JavaFetch → JsHttpClient → Vert.x │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 已实现功能
|
||||
|
||||
### ✅ 后端 ES5 执行环境
|
||||
|
||||
#### 1. Promise Polyfill (完整的 Promise/A+ 实现)
|
||||
|
||||
文件: `parser/src/main/resources/fetch-runtime.js`
|
||||
|
||||
**功能特性:**
|
||||
- ✅ `new Promise(executor)` 构造函数
|
||||
- ✅ `promise.then(onFulfilled, onRejected)` 链式调用
|
||||
- ✅ `promise.catch(onRejected)` 错误处理
|
||||
- ✅ `promise.finally(onFinally)` 清理操作
|
||||
- ✅ `Promise.resolve(value)` 静态方法
|
||||
- ✅ `Promise.reject(reason)` 静态方法
|
||||
- ✅ `Promise.all(promises)` 并行等待
|
||||
- ✅ `Promise.race(promises)` 竞速等待
|
||||
|
||||
**实现细节:**
|
||||
- 纯 ES5 语法,无ES6+特性依赖
|
||||
- 使用 `setTimeout(fn, 0)` 实现异步执行
|
||||
- 支持 Promise 链式调用和错误传播
|
||||
- 自动处理 Promise 嵌套和展开
|
||||
|
||||
#### 2. Fetch API Polyfill (标准 fetch 接口)
|
||||
|
||||
文件: `parser/src/main/resources/fetch-runtime.js`
|
||||
|
||||
**支持的 HTTP 方法:**
|
||||
- ✅ GET
|
||||
- ✅ POST
|
||||
- ✅ PUT
|
||||
- ✅ DELETE
|
||||
- ✅ PATCH
|
||||
- ✅ HEAD
|
||||
|
||||
**Request 选项支持:**
|
||||
```javascript
|
||||
fetch(url, {
|
||||
method: 'POST', // HTTP 方法
|
||||
headers: { // 请求头
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer token'
|
||||
},
|
||||
body: JSON.stringify({ // 请求体
|
||||
key: 'value'
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
**Response 对象方法:**
|
||||
- ✅ `response.text()` - 获取文本响应 (返回 Promise)
|
||||
- ✅ `response.json()` - 解析 JSON 响应 (返回 Promise)
|
||||
- ✅ `response.arrayBuffer()` - 获取字节数组
|
||||
- ✅ `response.status` - HTTP 状态码
|
||||
- ✅ `response.ok` - 请求是否成功 (2xx)
|
||||
- ✅ `response.statusText` - 状态文本
|
||||
- ✅ `response.headers.get(name)` - 获取响应头
|
||||
|
||||
#### 3. Java 桥接层
|
||||
|
||||
文件: `parser/src/main/java/cn/qaiu/parser/customjs/JsFetchBridge.java`
|
||||
|
||||
**核心功能:**
|
||||
- 接收 JavaScript fetch API 调用
|
||||
- 转换为 JsHttpClient 调用
|
||||
- 处理请求头、请求体、HTTP 方法
|
||||
- 返回 JsHttpResponse 对象
|
||||
- 自动继承现有的 SSRF 防护机制
|
||||
|
||||
**代码示例:**
|
||||
```java
|
||||
public class JsFetchBridge {
|
||||
private final JsHttpClient httpClient;
|
||||
|
||||
public JsHttpResponse fetch(String url, Map<String, Object> options) {
|
||||
// 解析 method、headers、body
|
||||
// 调用 httpClient.get/post/put/delete/patch
|
||||
// 返回 JsHttpResponse
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. 自动注入机制
|
||||
|
||||
文件:
|
||||
- `parser/src/main/java/cn/qaiu/parser/customjs/JsParserExecutor.java`
|
||||
- `parser/src/main/java/cn/qaiu/parser/customjs/JsPlaygroundExecutor.java`
|
||||
|
||||
**注入流程:**
|
||||
1. 创建 JavaScript 引擎
|
||||
2. 注入 JavaFetch 桥接对象
|
||||
3. 加载 fetch-runtime.js
|
||||
4. 执行用户 JavaScript 代码
|
||||
|
||||
**代码示例:**
|
||||
```java
|
||||
// 注入 JavaFetch
|
||||
engine.put("JavaFetch", new JsFetchBridge(httpClient));
|
||||
|
||||
// 加载 fetch runtime
|
||||
String fetchRuntime = loadFetchRuntime();
|
||||
engine.eval(fetchRuntime);
|
||||
|
||||
// 现在 JavaScript 环境中可以使用 Promise 和 fetch
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
### ES5 风格 (当前可用)
|
||||
|
||||
```javascript
|
||||
function parse(shareLinkInfo, http, logger) {
|
||||
logger.info("开始解析");
|
||||
|
||||
// 使用 fetch API
|
||||
fetch("https://api.example.com/data")
|
||||
.then(function(response) {
|
||||
logger.info("状态码: " + response.status);
|
||||
return response.json();
|
||||
})
|
||||
.then(function(data) {
|
||||
logger.info("数据: " + JSON.stringify(data));
|
||||
return data.downloadUrl;
|
||||
})
|
||||
.catch(function(error) {
|
||||
logger.error("错误: " + error.message);
|
||||
throw error;
|
||||
});
|
||||
|
||||
// 或者继续使用传统的 http 对象
|
||||
var response = http.get("https://api.example.com/data");
|
||||
return response.body();
|
||||
}
|
||||
```
|
||||
|
||||
### TypeScript/ES6+ 风格 (需前端编译)
|
||||
|
||||
用户在浏览器中编写:
|
||||
|
||||
```typescript
|
||||
async function parse(
|
||||
shareLinkInfo: ShareLinkInfo,
|
||||
http: JsHttpClient,
|
||||
logger: JsLogger
|
||||
): Promise<string> {
|
||||
try {
|
||||
logger.info("开始解析");
|
||||
|
||||
// 使用标准 fetch API
|
||||
const response = await fetch("https://api.example.com/data");
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
logger.info(`下载链接: ${data.downloadUrl}`);
|
||||
|
||||
return data.downloadUrl;
|
||||
|
||||
} catch (error) {
|
||||
logger.error(`解析失败: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
浏览器编译为 ES5 后:
|
||||
|
||||
```javascript
|
||||
function parse(shareLinkInfo, http, logger) {
|
||||
return __awaiter(this, void 0, void 0, function() {
|
||||
var response, data, error_1;
|
||||
return __generator(this, function(_a) {
|
||||
switch(_a.label) {
|
||||
case 0:
|
||||
_a.trys.push([0, 3, , 4]);
|
||||
logger.info("开始解析");
|
||||
return [4, fetch("https://api.example.com/data")];
|
||||
case 1:
|
||||
response = _a.sent();
|
||||
if (!response.ok) {
|
||||
throw new Error("HTTP " + response.status + ": " + response.statusText);
|
||||
}
|
||||
return [4, response.json()];
|
||||
case 2:
|
||||
data = _a.sent();
|
||||
logger.info("下载链接: " + data.downloadUrl);
|
||||
return [2, data.downloadUrl];
|
||||
case 3:
|
||||
error_1 = _a.sent();
|
||||
logger.error("解析失败: " + error_1.message);
|
||||
throw error_1;
|
||||
case 4: return [2];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
parser/
|
||||
├── src/main/
|
||||
│ ├── java/cn/qaiu/parser/customjs/
|
||||
│ │ ├── JsFetchBridge.java # Java 桥接层
|
||||
│ │ ├── JsParserExecutor.java # 解析器执行器 (已更新)
|
||||
│ │ └── JsPlaygroundExecutor.java # 演练场执行器 (已更新)
|
||||
│ └── resources/
|
||||
│ ├── fetch-runtime.js # Promise + fetch polyfill
|
||||
│ └── custom-parsers/
|
||||
│ └── fetch-demo.js # Fetch 示例解析器
|
||||
├── src/test/java/cn/qaiu/parser/customjs/
|
||||
│ └── JsFetchBridgeTest.java # 单元测试
|
||||
└── doc/
|
||||
└── TYPESCRIPT_FETCH_GUIDE.md # 详细使用指南
|
||||
```
|
||||
|
||||
## 测试验证
|
||||
|
||||
### 运行测试
|
||||
|
||||
```bash
|
||||
# 编译项目
|
||||
mvn clean compile -pl parser
|
||||
|
||||
# 运行所有测试
|
||||
mvn test -pl parser
|
||||
|
||||
# 运行 fetch 测试
|
||||
mvn test -pl parser -Dtest=JsFetchBridgeTest
|
||||
```
|
||||
|
||||
### 测试内容
|
||||
|
||||
文件: `parser/src/test/java/cn/qaiu/parser/customjs/JsFetchBridgeTest.java`
|
||||
|
||||
1. **testFetchPolyfillLoaded** - 验证 Promise 和 fetch 是否正确注入
|
||||
2. **testPromiseBasicUsage** - 验证 Promise 基本功能
|
||||
3. **示例解析器** - `fetch-demo.js` 展示完整用法
|
||||
|
||||
## 兼容性说明
|
||||
|
||||
### 支持的特性
|
||||
|
||||
- ✅ Promise/A+ 完整实现
|
||||
- ✅ Fetch API 标准接口
|
||||
- ✅ async/await (通过 TypeScript 编译)
|
||||
- ✅ 所有 HTTP 方法
|
||||
- ✅ Request headers 和 body
|
||||
- ✅ Response 解析 (text, json, arrayBuffer)
|
||||
- ✅ 错误处理和 Promise 链
|
||||
- ✅ 与现有 http 对象共存
|
||||
|
||||
### 不支持的特性
|
||||
|
||||
- ❌ Blob 对象 (使用 arrayBuffer 替代)
|
||||
- ❌ FormData 对象 (使用简单对象替代)
|
||||
- ❌ Request/Response 构造函数
|
||||
- ❌ Streams API
|
||||
- ❌ Service Worker 相关 API
|
||||
- ❌ AbortController (取消请求)
|
||||
|
||||
## 安全性
|
||||
|
||||
### SSRF 防护
|
||||
|
||||
继承自 `JsHttpClient` 的 SSRF 防护:
|
||||
- ✅ 拦截内网 IP (127.0.0.1, 10.x.x.x, 192.168.x.x 等)
|
||||
- ✅ 拦截云服务元数据 API (169.254.169.254 等)
|
||||
- ✅ DNS 解析检查
|
||||
- ✅ 危险域名黑名单
|
||||
|
||||
### 沙箱隔离
|
||||
|
||||
- ✅ SecurityClassFilter 限制类访问
|
||||
- ✅ 禁用 Java 对象直接访问
|
||||
- ✅ 限制文件系统操作
|
||||
|
||||
## 性能优化
|
||||
|
||||
1. **Fetch runtime 缓存**
|
||||
- 首次加载后缓存在静态变量
|
||||
- 避免重复读取文件
|
||||
|
||||
2. **Promise 异步执行**
|
||||
- 使用 setTimeout(0) 实现非阻塞
|
||||
- 避免阻塞 JavaScript 主线程
|
||||
|
||||
3. **工作线程池**
|
||||
- JsParserExecutor: Vert.x 工作线程池
|
||||
- JsPlaygroundExecutor: 独立线程池
|
||||
- 避免阻塞 Event Loop
|
||||
|
||||
## 前端 TypeScript 编译 (计划中)
|
||||
|
||||
### 待实现步骤
|
||||
|
||||
1. **添加 TypeScript 编译器**
|
||||
```bash
|
||||
cd web-front
|
||||
npm install typescript
|
||||
```
|
||||
|
||||
2. **创建编译工具**
|
||||
```javascript
|
||||
// web-front/src/utils/tsCompiler.js
|
||||
import * as ts from 'typescript';
|
||||
|
||||
export function compileToES5(sourceCode) {
|
||||
return ts.transpileModule(sourceCode, {
|
||||
compilerOptions: {
|
||||
target: ts.ScriptTarget.ES5,
|
||||
module: ts.ModuleKind.None,
|
||||
lib: ['es5', 'dom']
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
3. **更新 Playground UI**
|
||||
- 添加语言选择器 (JavaScript / TypeScript)
|
||||
- 编译前先检查语法错误
|
||||
- 显示编译后的 ES5 代码 (可选)
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [详细使用指南](parser/doc/TYPESCRIPT_FETCH_GUIDE.md)
|
||||
- [JavaScript 解析器开发指南](parser/doc/JAVASCRIPT_PARSER_GUIDE.md)
|
||||
- [自定义解析器扩展指南](parser/doc/CUSTOM_PARSER_GUIDE.md)
|
||||
|
||||
## 总结
|
||||
|
||||
本实现成功提供了:
|
||||
|
||||
1. **无需 Node 环境** - 纯浏览器编译 + Java 后端执行
|
||||
2. **标准 API** - 使用标准 fetch 和 Promise API
|
||||
3. **向后兼容** - 现有 http 对象仍然可用
|
||||
4. **安全可靠** - SSRF 防护和沙箱隔离
|
||||
5. **易于使用** - 简单的 API,无学习成本
|
||||
|
||||
用户可以用现代 JavaScript/TypeScript 编写代码,自动编译为 ES5 后在后端安全执行,同时享受 fetch API 的便利性。
|
||||
|
||||
## 许可证
|
||||
|
||||
本项目遵循主项目的许可证。
|
||||
@@ -1,451 +0,0 @@
|
||||
# 浏览器TypeScript编译和Fetch API支持指南
|
||||
|
||||
## 概述
|
||||
|
||||
本项目实现了**纯前端TypeScript编译 + 后端ES5引擎 + Fetch API适配**的完整方案,允许用户在浏览器中编写TypeScript/ES6+代码,编译为ES5后在后端JavaScript引擎中执行。
|
||||
|
||||
## 架构设计
|
||||
|
||||
### 1. 浏览器端(前端编译)
|
||||
|
||||
```
|
||||
用户编写TS/ES6+代码
|
||||
↓
|
||||
TypeScript.js (浏览器内编译)
|
||||
↓
|
||||
ES5 JavaScript代码
|
||||
↓
|
||||
发送到后端执行
|
||||
```
|
||||
|
||||
### 2. 后端(ES5执行环境)
|
||||
|
||||
```
|
||||
接收ES5代码
|
||||
↓
|
||||
注入fetch polyfill + Promise
|
||||
↓
|
||||
注入JavaFetch桥接对象
|
||||
↓
|
||||
Nashorn引擎执行ES5代码
|
||||
↓
|
||||
fetch() 调用 → JavaFetch → JsHttpClient → Vert.x HTTP Client
|
||||
```
|
||||
|
||||
## 已实现的功能
|
||||
|
||||
### ✅ 后端支持
|
||||
|
||||
1. **Promise Polyfill** (`fetch-runtime.js`)
|
||||
- 完整的Promise/A+实现
|
||||
- 支持 `then`、`catch`、`finally`
|
||||
- 支持 `Promise.all`、`Promise.race`
|
||||
- 支持 `Promise.resolve`、`Promise.reject`
|
||||
|
||||
2. **Fetch API Polyfill** (`fetch-runtime.js`)
|
||||
- 标准fetch接口实现
|
||||
- 支持所有HTTP方法(GET、POST、PUT、DELETE、PATCH)
|
||||
- 支持headers、body等选项
|
||||
- Response对象支持:
|
||||
- `text()` - 获取文本响应
|
||||
- `json()` - 解析JSON响应
|
||||
- `arrayBuffer()` - 获取字节数组
|
||||
- `status` - HTTP状态码
|
||||
- `ok` - 请求成功标志
|
||||
- `headers` - 响应头访问
|
||||
|
||||
3. **Java桥接** (`JsFetchBridge.java`)
|
||||
- 将fetch调用转换为JsHttpClient调用
|
||||
- 自动处理请求头、请求体
|
||||
- 支持代理配置
|
||||
- 安全的SSRF防护
|
||||
|
||||
4. **自动注入** (`JsParserExecutor.java` & `JsPlaygroundExecutor.java`)
|
||||
- 在JavaScript引擎初始化时自动注入fetch runtime
|
||||
- 提供`JavaFetch`全局对象
|
||||
- 与现有http对象共存
|
||||
|
||||
## 使用示例
|
||||
|
||||
### ES5风格(当前支持)
|
||||
|
||||
```javascript
|
||||
function parse(shareLinkInfo, http, logger) {
|
||||
// 使用fetch API
|
||||
fetch("https://api.example.com/data")
|
||||
.then(function(response) {
|
||||
return response.json();
|
||||
})
|
||||
.then(function(data) {
|
||||
logger.info("数据: " + JSON.stringify(data));
|
||||
})
|
||||
.catch(function(error) {
|
||||
logger.error("错误: " + error.message);
|
||||
});
|
||||
|
||||
// 或者使用传统的http对象
|
||||
var response = http.get("https://api.example.com/data");
|
||||
return response.body();
|
||||
}
|
||||
```
|
||||
|
||||
### TypeScript风格(需要前端编译)
|
||||
|
||||
用户在浏览器中编写:
|
||||
|
||||
```typescript
|
||||
async function parse(shareLinkInfo: ShareLinkInfo, http: JsHttpClient, logger: JsLogger): Promise<string> {
|
||||
try {
|
||||
// 使用标准fetch API
|
||||
const response = await fetch("https://api.example.com/data");
|
||||
const data = await response.json();
|
||||
|
||||
logger.info(`获取到数据: ${data.downloadUrl}`);
|
||||
return data.downloadUrl;
|
||||
} catch (error) {
|
||||
logger.error(`解析失败: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
浏览器内编译后的ES5代码(简化示例):
|
||||
|
||||
```javascript
|
||||
function parse(shareLinkInfo, http, logger) {
|
||||
return __awaiter(this, void 0, void 0, function() {
|
||||
var response, data;
|
||||
return __generator(this, function(_a) {
|
||||
switch(_a.label) {
|
||||
case 0:
|
||||
return [4, fetch("https://api.example.com/data")];
|
||||
case 1:
|
||||
response = _a.sent();
|
||||
return [4, response.json()];
|
||||
case 2:
|
||||
data = _a.sent();
|
||||
logger.info("获取到数据: " + data.downloadUrl);
|
||||
return [2, data.downloadUrl];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## 前端TypeScript编译(待实现)
|
||||
|
||||
### 计划实现步骤
|
||||
|
||||
#### 1. 添加TypeScript编译器
|
||||
|
||||
在前端项目中添加`typescript.js`:
|
||||
|
||||
```bash
|
||||
# 下载TypeScript编译器浏览器版本
|
||||
cd webroot/static
|
||||
wget https://cdn.jsdelivr.net/npm/typescript@latest/lib/typescript.js
|
||||
```
|
||||
|
||||
或者在Vue项目中:
|
||||
|
||||
```bash
|
||||
npm install typescript
|
||||
```
|
||||
|
||||
#### 2. 创建编译工具类
|
||||
|
||||
`web-front/src/utils/tsCompiler.js`:
|
||||
|
||||
```javascript
|
||||
import * as ts from 'typescript';
|
||||
|
||||
export function compileToES5(sourceCode, fileName = 'script.ts') {
|
||||
const result = ts.transpileModule(sourceCode, {
|
||||
compilerOptions: {
|
||||
target: ts.ScriptTarget.ES5,
|
||||
module: ts.ModuleKind.None,
|
||||
lib: ['es5', 'dom'],
|
||||
experimentalDecorators: false,
|
||||
emitDecoratorMetadata: false,
|
||||
downlevelIteration: true
|
||||
},
|
||||
fileName: fileName
|
||||
});
|
||||
|
||||
return {
|
||||
js: result.outputText,
|
||||
diagnostics: result.diagnostics,
|
||||
sourceMap: result.sourceMapText
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. 更新Playground组件
|
||||
|
||||
在`Playground.vue`中添加编译选项:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div>
|
||||
<!-- 语言选择 -->
|
||||
<el-radio-group v-model="language">
|
||||
<el-radio label="javascript">JavaScript (ES5)</el-radio>
|
||||
<el-radio label="typescript">TypeScript/ES6+</el-radio>
|
||||
</el-radio-group>
|
||||
|
||||
<!-- 编辑器 -->
|
||||
<monaco-editor
|
||||
v-model="code"
|
||||
:language="language"
|
||||
@save="handleSave"
|
||||
/>
|
||||
|
||||
<!-- 运行按钮 -->
|
||||
<el-button @click="executeCode">运行</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { compileToES5 } from '@/utils/tsCompiler';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
language: 'javascript',
|
||||
code: ''
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async executeCode() {
|
||||
let codeToExecute = this.code;
|
||||
|
||||
// 如果是TypeScript,先编译
|
||||
if (this.language === 'typescript') {
|
||||
const result = compileToES5(this.code);
|
||||
|
||||
if (result.diagnostics && result.diagnostics.length > 0) {
|
||||
this.$message.error('TypeScript编译错误');
|
||||
console.error(result.diagnostics);
|
||||
return;
|
||||
}
|
||||
|
||||
codeToExecute = result.js;
|
||||
console.log('编译后的ES5代码:', codeToExecute);
|
||||
}
|
||||
|
||||
// 发送到后端执行
|
||||
const response = await playgroundApi.testScript(
|
||||
codeToExecute,
|
||||
this.shareUrl,
|
||||
this.pwd,
|
||||
this.method
|
||||
);
|
||||
|
||||
this.showResult(response);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
## Fetch Runtime详解
|
||||
|
||||
### Promise实现特性
|
||||
|
||||
```javascript
|
||||
// 基本用法
|
||||
var promise = new SimplePromise(function(resolve, reject) {
|
||||
setTimeout(function() {
|
||||
resolve("成功");
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
promise.then(function(value) {
|
||||
console.log(value); // "成功"
|
||||
});
|
||||
|
||||
// 链式调用
|
||||
promise
|
||||
.then(function(value) {
|
||||
return value + " - 第一步";
|
||||
})
|
||||
.then(function(value) {
|
||||
return value + " - 第二步";
|
||||
})
|
||||
.catch(function(error) {
|
||||
console.error(error);
|
||||
})
|
||||
.finally(function() {
|
||||
console.log("完成");
|
||||
});
|
||||
```
|
||||
|
||||
### Fetch API特性
|
||||
|
||||
```javascript
|
||||
// GET请求
|
||||
fetch("https://api.example.com/data")
|
||||
.then(function(response) {
|
||||
console.log("状态码:", response.status);
|
||||
console.log("成功:", response.ok);
|
||||
return response.json();
|
||||
})
|
||||
.then(function(data) {
|
||||
console.log("数据:", data);
|
||||
});
|
||||
|
||||
// POST请求
|
||||
fetch("https://api.example.com/submit", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({ key: "value" })
|
||||
})
|
||||
.then(function(response) {
|
||||
return response.json();
|
||||
})
|
||||
.then(function(data) {
|
||||
console.log("响应:", data);
|
||||
});
|
||||
```
|
||||
|
||||
## 兼容性说明
|
||||
|
||||
### 支持的特性
|
||||
|
||||
- ✅ Promise/A+ 完整实现
|
||||
- ✅ Fetch API 标准接口
|
||||
- ✅ async/await(编译后)
|
||||
- ✅ 所有HTTP方法(GET、POST、PUT、DELETE、PATCH)
|
||||
- ✅ Request headers配置
|
||||
- ✅ Request body(string、JSON、FormData)
|
||||
- ✅ Response.text()、Response.json()
|
||||
- ✅ 与现有http对象共存
|
||||
|
||||
### 不支持的特性
|
||||
|
||||
- ❌ Blob对象(返回字节数组替代)
|
||||
- ❌ FormData对象(使用简单对象替代)
|
||||
- ❌ Request/Response对象构造函数
|
||||
- ❌ Streams API
|
||||
- ❌ Service Worker相关API
|
||||
|
||||
## 测试验证
|
||||
|
||||
### 1. 创建测试解析器
|
||||
|
||||
参考 `parser/src/main/resources/custom-parsers/fetch-demo.js`
|
||||
|
||||
### 2. 测试步骤
|
||||
|
||||
```bash
|
||||
# 1. 编译项目
|
||||
mvn clean package -DskipTests
|
||||
|
||||
# 2. 运行服务
|
||||
java -jar web-service/target/netdisk-fast-download.jar
|
||||
|
||||
# 3. 访问演练场
|
||||
浏览器打开: http://localhost:6401/playground
|
||||
|
||||
# 4. 加载fetch-demo.js并测试
|
||||
```
|
||||
|
||||
### 3. 验证fetch功能
|
||||
|
||||
在演练场中运行:
|
||||
|
||||
```javascript
|
||||
function parse(shareLinkInfo, http, logger) {
|
||||
logger.info("测试fetch API");
|
||||
|
||||
var result = null;
|
||||
fetch("https://httpbin.org/get")
|
||||
.then(function(response) {
|
||||
logger.info("状态码: " + response.status);
|
||||
return response.json();
|
||||
})
|
||||
.then(function(data) {
|
||||
logger.info("响应: " + JSON.stringify(data));
|
||||
result = "SUCCESS";
|
||||
})
|
||||
.catch(function(error) {
|
||||
logger.error("错误: " + error.message);
|
||||
});
|
||||
|
||||
// 等待完成
|
||||
var timeout = 5000;
|
||||
var start = Date.now();
|
||||
while (result === null && (Date.now() - start) < timeout) {
|
||||
java.lang.Thread.sleep(10);
|
||||
}
|
||||
|
||||
return result || "https://example.com/download";
|
||||
}
|
||||
```
|
||||
|
||||
## 安全性
|
||||
|
||||
### SSRF防护
|
||||
|
||||
JsHttpClient已实现SSRF防护:
|
||||
- 拦截内网IP访问(127.0.0.1、10.x.x.x、192.168.x.x等)
|
||||
- 拦截云服务元数据API(169.254.169.254等)
|
||||
- DNS解析检查
|
||||
|
||||
### 沙箱隔离
|
||||
|
||||
- JavaScript引擎使用SecurityClassFilter
|
||||
- 禁用Java对象访问
|
||||
- 限制文件系统访问
|
||||
|
||||
## 性能优化
|
||||
|
||||
1. **Fetch runtime缓存**
|
||||
- 首次加载后缓存在静态变量中
|
||||
- 避免重复读取资源文件
|
||||
|
||||
2. **Promise异步执行**
|
||||
- 使用setTimeout(0)实现异步
|
||||
- 避免阻塞主线程
|
||||
|
||||
3. **工作线程池**
|
||||
- JsParserExecutor使用Vert.x工作线程池
|
||||
- JsPlaygroundExecutor使用独立线程池
|
||||
|
||||
## 相关文件
|
||||
|
||||
### 后端代码
|
||||
- `parser/src/main/resources/fetch-runtime.js` - Fetch和Promise polyfill
|
||||
- `parser/src/main/java/cn/qaiu/parser/customjs/JsFetchBridge.java` - Java桥接层
|
||||
- `parser/src/main/java/cn/qaiu/parser/customjs/JsParserExecutor.java` - 解析器执行器
|
||||
- `parser/src/main/java/cn/qaiu/parser/customjs/JsPlaygroundExecutor.java` - 演练场执行器
|
||||
|
||||
### 示例代码
|
||||
- `parser/src/main/resources/custom-parsers/fetch-demo.js` - Fetch API演示
|
||||
|
||||
### 前端代码(待实现)
|
||||
- `web-front/src/utils/tsCompiler.js` - TypeScript编译工具
|
||||
- `web-front/src/views/Playground.vue` - 演练场界面
|
||||
|
||||
## 下一步计划
|
||||
|
||||
1. ✅ 实现后端fetch polyfill
|
||||
2. ✅ 实现Promise polyfill
|
||||
3. ✅ 集成到JsParserExecutor
|
||||
4. ⏳ 前端添加TypeScript编译器
|
||||
5. ⏳ 更新Playground UI支持TS/ES6+
|
||||
6. ⏳ 添加Monaco编辑器类型提示
|
||||
7. ⏳ 编写更多示例和文档
|
||||
|
||||
## 总结
|
||||
|
||||
通过这个方案,我们实现了:
|
||||
1. **无需Node环境** - 纯浏览器编译 + Java后端执行
|
||||
2. **标准API** - 使用标准fetch和Promise API
|
||||
3. **向后兼容** - 现有http对象仍然可用
|
||||
4. **安全可靠** - SSRF防护和沙箱隔离
|
||||
5. **易于使用** - 简单的API,无需学习成本
|
||||
|
||||
用户可以在浏览器中用现代JavaScript/TypeScript编写代码,自动编译为ES5后在后端安全执行,同时享受fetch API的便利性。
|
||||
@@ -1,483 +0,0 @@
|
||||
# TypeScript 支持文档
|
||||
|
||||
## 概述
|
||||
|
||||
演练场现在支持 TypeScript!您可以使用现代 TypeScript 语法编写解析器代码,系统会自动将其编译为 ES5 并在后端执行。
|
||||
|
||||
## 功能特性
|
||||
|
||||
### 🎯 核心功能
|
||||
|
||||
- ✅ **TypeScript 编译器集成**:内置 TypeScript 编译器,实时将 TS 代码编译为 ES5
|
||||
- ✅ **语言选择器**:在演练场工具栏轻松切换 JavaScript 和 TypeScript
|
||||
- ✅ **编译错误提示**:友好的编译错误提示和建议
|
||||
- ✅ **双代码存储**:同时保存原始 TypeScript 代码和编译后的 ES5 代码
|
||||
- ✅ **无缝集成**:与现有演练场功能完全兼容
|
||||
|
||||
### 📝 TypeScript 特性支持
|
||||
|
||||
支持所有标准 TypeScript 特性,包括但不限于:
|
||||
|
||||
- 类型注解(Type Annotations)
|
||||
- 接口(Interfaces)
|
||||
- 类型别名(Type Aliases)
|
||||
- 枚举(Enums)
|
||||
- 泛型(Generics)
|
||||
- async/await(编译为 Promise)
|
||||
- 箭头函数
|
||||
- 模板字符串
|
||||
- 解构赋值
|
||||
- 可选链(Optional Chaining)
|
||||
- 空值合并(Nullish Coalescing)
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 选择语言
|
||||
|
||||
在演练场工具栏中,点击 **JavaScript** 或 **TypeScript** 按钮选择您要使用的语言。
|
||||
|
||||
### 2. 编写代码
|
||||
|
||||
选择 TypeScript 后,点击"加载示例"按钮可以加载 TypeScript 示例代码。
|
||||
|
||||
#### TypeScript 示例
|
||||
|
||||
```typescript
|
||||
// ==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==
|
||||
|
||||
/**
|
||||
* 解析单个文件下载链接
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 运行测试
|
||||
|
||||
点击"运行"按钮(或按 Ctrl+Enter)。系统会:
|
||||
|
||||
1. 自动检测代码是否为 TypeScript
|
||||
2. 将 TypeScript 编译为 ES5
|
||||
3. 显示编译结果(成功/失败)
|
||||
4. 如果编译成功,使用 ES5 代码执行测试
|
||||
5. 显示测试结果
|
||||
|
||||
### 4. 发布解析器
|
||||
|
||||
编译成功后,点击"发布脚本"即可保存解析器。系统会自动:
|
||||
|
||||
- 保存原始 TypeScript 代码到 `playground_typescript_code` 表
|
||||
- 保存编译后的 ES5 代码到 `playground_parser` 表
|
||||
- 通过 `parserId` 关联两者
|
||||
|
||||
## 编译选项
|
||||
|
||||
TypeScript 编译器使用以下配置:
|
||||
|
||||
```javascript
|
||||
{
|
||||
target: 'ES5', // 目标版本:ES5
|
||||
module: 'None', // 不使用模块系统
|
||||
lib: ['es5', 'dom'], // 包含ES5和DOM类型定义
|
||||
removeComments: false, // 保留注释
|
||||
downlevelIteration: true, // 支持ES5迭代器降级
|
||||
esModuleInterop: true // 启用ES模块互操作性
|
||||
}
|
||||
```
|
||||
|
||||
## 类型定义
|
||||
|
||||
### 可用的 API 对象
|
||||
|
||||
虽然 TypeScript 支持类型注解,但由于后端运行时环境的限制,建议使用 `any` 类型:
|
||||
|
||||
```typescript
|
||||
function parse(
|
||||
shareLinkInfo: any, // 分享链接信息
|
||||
http: any, // HTTP客户端
|
||||
logger: any // 日志对象
|
||||
): Promise<string> {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### 常用方法
|
||||
|
||||
#### shareLinkInfo 对象
|
||||
|
||||
```typescript
|
||||
shareLinkInfo.getShareUrl(): string // 获取分享URL
|
||||
shareLinkInfo.getShareKey(): string // 获取分享Key
|
||||
shareLinkInfo.getSharePassword(): string // 获取分享密码
|
||||
shareLinkInfo.getOtherParam(key: string): any // 获取其他参数
|
||||
```
|
||||
|
||||
#### logger 对象
|
||||
|
||||
```typescript
|
||||
logger.info(message: string): void // 记录信息日志
|
||||
logger.debug(message: string): void // 记录调试日志
|
||||
logger.error(message: string): void // 记录错误日志
|
||||
logger.warn(message: string): void // 记录警告日志
|
||||
```
|
||||
|
||||
#### fetch API(后端 Polyfill)
|
||||
|
||||
```typescript
|
||||
async function fetchData(url: string): Promise<any> {
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0...',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 使用类型注解
|
||||
|
||||
虽然后端不强制类型检查,但类型注解可以提高代码可读性:
|
||||
|
||||
```typescript
|
||||
function parseFileList(
|
||||
shareLinkInfo: any,
|
||||
http: any,
|
||||
logger: any
|
||||
): Promise<Array<{
|
||||
fileName: string;
|
||||
fileId: string;
|
||||
size: number;
|
||||
}>> {
|
||||
// 实现...
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 利用 async/await
|
||||
|
||||
TypeScript 的 async/await 会编译为 Promise,后端已实现 Promise polyfill:
|
||||
|
||||
```typescript
|
||||
async function parse(
|
||||
shareLinkInfo: any,
|
||||
http: any,
|
||||
logger: any
|
||||
): Promise<string> {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
const data = await response.json();
|
||||
return data.downloadUrl;
|
||||
} catch (error) {
|
||||
logger.error(`错误: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 使用模板字符串
|
||||
|
||||
模板字符串让代码更清晰:
|
||||
|
||||
```typescript
|
||||
logger.info(`开始解析: ${url}, 密码: ${pwd}`);
|
||||
const apiUrl = `https://api.example.com/file/${fileId}`;
|
||||
```
|
||||
|
||||
### 4. 错误处理
|
||||
|
||||
使用类型化的错误处理:
|
||||
|
||||
```typescript
|
||||
try {
|
||||
const result = await parseUrl(url);
|
||||
return result;
|
||||
} catch (error: any) {
|
||||
logger.error(`解析失败: ${error.message}`);
|
||||
throw new Error(`无法解析链接: ${url}`);
|
||||
}
|
||||
```
|
||||
|
||||
## 编译错误处理
|
||||
|
||||
### 常见编译错误
|
||||
|
||||
#### 1. 类型不匹配
|
||||
|
||||
```typescript
|
||||
// ❌ 错误
|
||||
const count: number = "123";
|
||||
|
||||
// ✅ 正确
|
||||
const count: number = 123;
|
||||
```
|
||||
|
||||
#### 2. 缺少返回值
|
||||
|
||||
```typescript
|
||||
// ❌ 错误
|
||||
function parse(shareLinkInfo: any): string {
|
||||
const url = shareLinkInfo.getShareUrl();
|
||||
// 缺少 return
|
||||
}
|
||||
|
||||
// ✅ 正确
|
||||
function parse(shareLinkInfo: any): string {
|
||||
const url = shareLinkInfo.getShareUrl();
|
||||
return url;
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. 使用未声明的变量
|
||||
|
||||
```typescript
|
||||
// ❌ 错误
|
||||
function parse() {
|
||||
console.log(unknownVariable);
|
||||
}
|
||||
|
||||
// ✅ 正确
|
||||
function parse() {
|
||||
const knownVariable = "value";
|
||||
console.log(knownVariable);
|
||||
}
|
||||
```
|
||||
|
||||
### 查看编译错误
|
||||
|
||||
编译失败时,系统会显示详细的错误信息,包括:
|
||||
|
||||
- 错误类型(Error/Warning)
|
||||
- 错误位置(行号、列号)
|
||||
- 错误代码(TS错误代码)
|
||||
- 错误描述
|
||||
|
||||
## 数据库结构
|
||||
|
||||
### playground_typescript_code 表
|
||||
|
||||
存储 TypeScript 源代码的表结构:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | BIGINT | 主键,自增 |
|
||||
| parser_id | BIGINT | 关联的解析器ID(外键) |
|
||||
| ts_code | TEXT | TypeScript原始代码 |
|
||||
| es5_code | TEXT | 编译后的ES5代码 |
|
||||
| compile_errors | VARCHAR(2000) | 编译错误信息 |
|
||||
| compiler_version | VARCHAR(32) | 编译器版本 |
|
||||
| compile_options | VARCHAR(1000) | 编译选项(JSON格式) |
|
||||
| create_time | DATETIME | 创建时间 |
|
||||
| update_time | DATETIME | 更新时间 |
|
||||
| is_valid | BOOLEAN | 编译是否成功 |
|
||||
| ip | VARCHAR(64) | 创建者IP |
|
||||
|
||||
### 与 playground_parser 表的关系
|
||||
|
||||
- `playground_typescript_code.parser_id` 外键关联到 `playground_parser.id`
|
||||
- 一个解析器(parser)可以有一个对应的 TypeScript 代码记录
|
||||
- 编译后的 ES5 代码存储在 `playground_parser.js_code` 字段中
|
||||
|
||||
## API 端点
|
||||
|
||||
### 保存 TypeScript 代码
|
||||
|
||||
```http
|
||||
POST /v2/playground/typescript
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"parserId": 1,
|
||||
"tsCode": "...",
|
||||
"es5Code": "...",
|
||||
"compileErrors": null,
|
||||
"compilerVersion": "5.x",
|
||||
"compileOptions": "{}",
|
||||
"isValid": true
|
||||
}
|
||||
```
|
||||
|
||||
### 获取 TypeScript 代码
|
||||
|
||||
```http
|
||||
GET /v2/playground/typescript/:parserId
|
||||
```
|
||||
|
||||
### 更新 TypeScript 代码
|
||||
|
||||
```http
|
||||
PUT /v2/playground/typescript/:parserId
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"tsCode": "...",
|
||||
"es5Code": "...",
|
||||
"compileErrors": null,
|
||||
"compilerVersion": "5.x",
|
||||
"compileOptions": "{}",
|
||||
"isValid": true
|
||||
}
|
||||
```
|
||||
|
||||
## 迁移指南
|
||||
|
||||
### 从 JavaScript 迁移到 TypeScript
|
||||
|
||||
1. **添加类型注解**:
|
||||
```typescript
|
||||
// JavaScript
|
||||
function parse(shareLinkInfo, http, logger) {
|
||||
var url = shareLinkInfo.getShareUrl();
|
||||
return url;
|
||||
}
|
||||
|
||||
// TypeScript
|
||||
function parse(
|
||||
shareLinkInfo: any,
|
||||
http: any,
|
||||
logger: any
|
||||
): string {
|
||||
const url: string = shareLinkInfo.getShareUrl();
|
||||
return url;
|
||||
}
|
||||
```
|
||||
|
||||
2. **使用 const/let 替代 var**:
|
||||
```typescript
|
||||
// JavaScript
|
||||
var url = "https://example.com";
|
||||
var count = 0;
|
||||
|
||||
// TypeScript
|
||||
const url: string = "https://example.com";
|
||||
let count: number = 0;
|
||||
```
|
||||
|
||||
3. **使用模板字符串**:
|
||||
```typescript
|
||||
// JavaScript
|
||||
var message = "URL: " + url + ", Count: " + count;
|
||||
|
||||
// TypeScript
|
||||
const message: string = `URL: ${url}, Count: ${count}`;
|
||||
```
|
||||
|
||||
4. **使用 async/await**:
|
||||
```typescript
|
||||
// JavaScript
|
||||
function parse(shareLinkInfo, http, logger) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
fetch(url).then(function(response) {
|
||||
resolve(response.text());
|
||||
}).catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
// TypeScript
|
||||
async function parse(
|
||||
shareLinkInfo: any,
|
||||
http: any,
|
||||
logger: any
|
||||
): Promise<string> {
|
||||
const response = await fetch(url);
|
||||
return await response.text();
|
||||
}
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: TypeScript 代码会在哪里编译?
|
||||
|
||||
A: TypeScript 代码在浏览器前端编译为 ES5,然后发送到后端执行。这确保了后端始终执行标准的 ES5 代码。
|
||||
|
||||
### Q: 编译需要多长时间?
|
||||
|
||||
A: 通常在几毫秒到几百毫秒之间,取决于代码大小和复杂度。
|
||||
|
||||
### Q: 可以使用 npm 包吗?
|
||||
|
||||
A: 不可以。目前不支持 import/require 外部模块。所有代码必须自包含。
|
||||
|
||||
### Q: 类型检查严格吗?
|
||||
|
||||
A: 不严格。编译器配置为允许隐式 any 类型,不进行严格的 null 检查。主要目的是支持现代语法,而非严格的类型安全。
|
||||
|
||||
### Q: 编译后的代码可以查看吗?
|
||||
|
||||
A: 目前编译后的 ES5 代码存储在数据库中,但 UI 中暂未提供预览功能。这是未来的增强计划。
|
||||
|
||||
### Q: 原有的 JavaScript 代码会受影响吗?
|
||||
|
||||
A: 不会。JavaScript 和 TypeScript 模式完全独立,互不影响。
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 编译失败
|
||||
|
||||
1. **检查语法**:确保 TypeScript 语法正确
|
||||
2. **查看错误信息**:仔细阅读编译错误提示
|
||||
3. **简化代码**:从简单的示例开始,逐步添加功能
|
||||
4. **使用示例**:点击"加载示例"查看正确的代码结构
|
||||
|
||||
### 运行时错误
|
||||
|
||||
1. **检查 ES5 兼容性**:某些高级特性可能无法完全转换
|
||||
2. **验证 API 使用**:确保正确使用 shareLinkInfo、http、logger 等对象
|
||||
3. **查看日志**:使用 logger 对象输出调试信息
|
||||
|
||||
## 未来计划
|
||||
|
||||
- [ ] 显示编译后的 ES5 代码预览
|
||||
- [ ] 添加专用的编译错误面板
|
||||
- [ ] 支持更多 TypeScript 配置选项
|
||||
- [ ] 提供完整的类型定义文件(.d.ts)
|
||||
- [ ] 支持代码自动补全和智能提示
|
||||
- [ ] 添加 TypeScript 代码片段库
|
||||
|
||||
## 反馈与支持
|
||||
|
||||
如遇到问题或有建议,请在 GitHub Issues 中提出:
|
||||
https://github.com/qaiu/netdisk-fast-download/issues
|
||||
@@ -36,6 +36,14 @@ public class JsPlaygroundExecutor {
|
||||
return thread;
|
||||
});
|
||||
|
||||
// 超时调度线程池,用于处理超时中断
|
||||
private static final ScheduledExecutorService TIMEOUT_SCHEDULER = Executors.newScheduledThreadPool(2, r -> {
|
||||
Thread thread = new Thread(r);
|
||||
thread.setName("playground-timeout-scheduler-" + System.currentTimeMillis());
|
||||
thread.setDaemon(true);
|
||||
return thread;
|
||||
});
|
||||
|
||||
private final ShareLinkInfo shareLinkInfo;
|
||||
private final String jsCode;
|
||||
private final ScriptEngine engine;
|
||||
@@ -161,12 +169,23 @@ public class JsPlaygroundExecutor {
|
||||
}
|
||||
}, INDEPENDENT_EXECUTOR);
|
||||
|
||||
// 添加超时处理
|
||||
executionFuture.orTimeout(EXECUTION_TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
||||
.whenComplete((result, error) -> {
|
||||
// 创建超时任务,强制取消执行
|
||||
ScheduledFuture<?> timeoutTask = TIMEOUT_SCHEDULER.schedule(() -> {
|
||||
if (!executionFuture.isDone()) {
|
||||
executionFuture.cancel(true); // 强制中断执行线程
|
||||
playgroundLogger.errorJava("执行超时,已强制中断");
|
||||
log.warn("JavaScript执行超时,已强制取消");
|
||||
}
|
||||
}, EXECUTION_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
|
||||
// 处理执行结果
|
||||
executionFuture.whenComplete((result, error) -> {
|
||||
// 取消超时任务
|
||||
timeoutTask.cancel(false);
|
||||
|
||||
if (error != null) {
|
||||
if (error instanceof TimeoutException) {
|
||||
String timeoutMsg = "JavaScript执行超时(超过" + EXECUTION_TIMEOUT_SECONDS + "秒),可能存在无限循环";
|
||||
if (error instanceof CancellationException) {
|
||||
String timeoutMsg = "JavaScript执行超时(超过" + EXECUTION_TIMEOUT_SECONDS + "秒),已强制中断";
|
||||
playgroundLogger.errorJava(timeoutMsg);
|
||||
log.error(timeoutMsg);
|
||||
promise.fail(new RuntimeException(timeoutMsg));
|
||||
@@ -225,12 +244,23 @@ public class JsPlaygroundExecutor {
|
||||
}
|
||||
}, INDEPENDENT_EXECUTOR);
|
||||
|
||||
// 添加超时处理
|
||||
executionFuture.orTimeout(EXECUTION_TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
||||
.whenComplete((result, error) -> {
|
||||
// 创建超时任务,强制取消执行
|
||||
ScheduledFuture<?> timeoutTask = TIMEOUT_SCHEDULER.schedule(() -> {
|
||||
if (!executionFuture.isDone()) {
|
||||
executionFuture.cancel(true); // 强制中断执行线程
|
||||
playgroundLogger.errorJava("执行超时,已强制中断");
|
||||
log.warn("JavaScript执行超时,已强制取消");
|
||||
}
|
||||
}, EXECUTION_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
|
||||
// 处理执行结果
|
||||
executionFuture.whenComplete((result, error) -> {
|
||||
// 取消超时任务
|
||||
timeoutTask.cancel(false);
|
||||
|
||||
if (error != null) {
|
||||
if (error instanceof TimeoutException) {
|
||||
String timeoutMsg = "JavaScript执行超时(超过" + EXECUTION_TIMEOUT_SECONDS + "秒),可能存在无限循环";
|
||||
if (error instanceof CancellationException) {
|
||||
String timeoutMsg = "JavaScript执行超时(超过" + EXECUTION_TIMEOUT_SECONDS + "秒),已强制中断";
|
||||
playgroundLogger.errorJava(timeoutMsg);
|
||||
log.error(timeoutMsg);
|
||||
promise.fail(new RuntimeException(timeoutMsg));
|
||||
@@ -288,12 +318,23 @@ public class JsPlaygroundExecutor {
|
||||
}
|
||||
}, INDEPENDENT_EXECUTOR);
|
||||
|
||||
// 添加超时处理
|
||||
executionFuture.orTimeout(EXECUTION_TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
||||
.whenComplete((result, error) -> {
|
||||
// 创建超时任务,强制取消执行
|
||||
ScheduledFuture<?> timeoutTask = TIMEOUT_SCHEDULER.schedule(() -> {
|
||||
if (!executionFuture.isDone()) {
|
||||
executionFuture.cancel(true); // 强制中断执行线程
|
||||
playgroundLogger.errorJava("执行超时,已强制中断");
|
||||
log.warn("JavaScript执行超时,已强制取消");
|
||||
}
|
||||
}, EXECUTION_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
|
||||
// 处理执行结果
|
||||
executionFuture.whenComplete((result, error) -> {
|
||||
// 取消超时任务
|
||||
timeoutTask.cancel(false);
|
||||
|
||||
if (error != null) {
|
||||
if (error instanceof TimeoutException) {
|
||||
String timeoutMsg = "JavaScript执行超时(超过" + EXECUTION_TIMEOUT_SECONDS + "秒),可能存在无限循环";
|
||||
if (error instanceof CancellationException) {
|
||||
String timeoutMsg = "JavaScript执行超时(超过" + EXECUTION_TIMEOUT_SECONDS + "秒),已强制中断";
|
||||
playgroundLogger.errorJava(timeoutMsg);
|
||||
log.error(timeoutMsg);
|
||||
promise.fail(new RuntimeException(timeoutMsg));
|
||||
|
||||
@@ -29,7 +29,7 @@ import java.util.regex.Pattern;
|
||||
*/
|
||||
public class LzTool extends PanBase {
|
||||
|
||||
public static final String SHARE_URL_PREFIX = "https://wwww.lanzoum.com";
|
||||
public static final String SHARE_URL_PREFIX = "https://wwwwp.lanzoup.com";
|
||||
MultiMap headers0 = HeaderUtils.parseHeaders("""
|
||||
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
|
||||
Accept-Encoding: gzip, deflate
|
||||
|
||||
362
parser/src/main/resources/py/123.py
Normal file
362
parser/src/main/resources/py/123.py
Normal 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()
|
||||
176
web-front/MONACO_EDITOR_NPM.md
Normal file
176
web-front/MONACO_EDITOR_NPM.md
Normal file
@@ -0,0 +1,176 @@
|
||||
# Monaco Editor NPM包配置说明
|
||||
|
||||
## ✅ 已完成的配置
|
||||
|
||||
### 1. NPM包安装
|
||||
已在 `package.json` 中安装:
|
||||
- `monaco-editor`: ^0.55.1 - 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` 并检查构建产物
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ module.exports = {
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
],
|
||||
plugins: [
|
||||
'@vue/babel-plugin-transform-vue-jsx'
|
||||
'@vue/babel-plugin-transform-vue-jsx',
|
||||
'@babel/plugin-transform-class-static-block'
|
||||
]
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"dev": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"build": "vue-cli-service build && node scripts/compress-vs.js",
|
||||
"build:no-compress": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -16,10 +17,9 @@
|
||||
"clipboard": "^2.0.11",
|
||||
"core-js": "^3.8.3",
|
||||
"element-plus": "2.11.3",
|
||||
"monaco-editor": "^0.45.0",
|
||||
"monaco-editor": "^0.55.1",
|
||||
"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",
|
||||
@@ -29,6 +29,7 @@
|
||||
"@babel/core": "^7.26.0",
|
||||
"@babel/eslint-parser": "^7.25.9",
|
||||
"@babel/plugin-transform-class-properties": "^7.26.0",
|
||||
"@babel/plugin-transform-class-static-block": "^7.26.0",
|
||||
"@vue/babel-plugin-transform-vue-jsx": "^1.4.0",
|
||||
"@vue/cli-plugin-babel": "~5.0.8",
|
||||
"@vue/cli-plugin-eslint": "~5.0.8",
|
||||
@@ -36,7 +37,8 @@
|
||||
"compression-webpack-plugin": "^11.1.0",
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-plugin-vue": "^9.30.0",
|
||||
"filemanager-webpack-plugin": "8.0.0"
|
||||
"filemanager-webpack-plugin": "8.0.0",
|
||||
"monaco-editor-webpack-plugin": "^7.1.1"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
content="Netdisk fast download,网盘直链解析工具">
|
||||
<meta name="description"
|
||||
content="Netdisk fast download 网盘直链解析工具">
|
||||
<!-- Font Awesome 图标库 -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<!-- Font Awesome 图标库 - 使用国内CDN -->
|
||||
<link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<style>
|
||||
.page-loading-wrap {
|
||||
padding: 120px;
|
||||
@@ -154,11 +154,26 @@
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
const saved = localStorage.getItem('isDarkMode') === 'true'
|
||||
const systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
if (saved || (!saved && systemDark)) {
|
||||
document.body.classList.add('dark-theme')
|
||||
}
|
||||
// 等待DOM加载完成后再操作
|
||||
(function() {
|
||||
function applyDarkTheme() {
|
||||
const body = document.body;
|
||||
if (body && body.classList) {
|
||||
// 只在用户明确选择暗色模式时才应用,不自动检测系统偏好
|
||||
if (localStorage.getItem('isDarkMode') === 'true') {
|
||||
body.classList.add('dark-theme')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果DOM已加载,立即执行
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', applyDarkTheme);
|
||||
} else {
|
||||
// DOM已加载,立即执行
|
||||
applyDarkTheme();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
124
web-front/scripts/compress-vs.js
Executable file
124
web-front/scripts/compress-vs.js
Executable file
@@ -0,0 +1,124 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const zlib = require("zlib");
|
||||
const { promisify } = require("util");
|
||||
|
||||
const gzip = promisify(zlib.gzip);
|
||||
const readdir = promisify(fs.readdir);
|
||||
const stat = promisify(fs.stat);
|
||||
const readFile = promisify(fs.readFile);
|
||||
const writeFile = promisify(fs.writeFile);
|
||||
|
||||
// 递归压缩目录下的所有文件
|
||||
async function compressDirectory(dirPath, threshold = 1024) {
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
console.warn(`目录不存在: ${dirPath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const files = await readdir(dirPath, { withFileTypes: true });
|
||||
let compressedCount = 0;
|
||||
let totalOriginalSize = 0;
|
||||
let totalCompressedSize = 0;
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(dirPath, file.name);
|
||||
|
||||
if (file.isDirectory()) {
|
||||
await compressDirectory(filePath, threshold);
|
||||
} else if (file.isFile()) {
|
||||
const stats = await stat(filePath);
|
||||
// 只压缩超过阈值且不是已压缩的文件
|
||||
if (stats.size > threshold && !filePath.endsWith('.gz') && !filePath.endsWith('.map')) {
|
||||
try {
|
||||
const content = await readFile(filePath);
|
||||
const compressed = await gzip(content);
|
||||
await writeFile(filePath + '.gz', compressed);
|
||||
compressedCount++;
|
||||
totalOriginalSize += stats.size;
|
||||
totalCompressedSize += compressed.length;
|
||||
console.log(`✓ ${file.name} (${(stats.size / 1024).toFixed(2)}KB -> ${(compressed.length / 1024).toFixed(2)}KB)`);
|
||||
} catch (error) {
|
||||
console.warn(`⚠ 压缩失败: ${filePath}`, error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (compressedCount > 0) {
|
||||
console.log(`\n压缩完成: ${compressedCount} 个文件`);
|
||||
console.log(`原始大小: ${(totalOriginalSize / 1024 / 1024).toFixed(2)}MB`);
|
||||
console.log(`压缩后大小: ${(totalCompressedSize / 1024 / 1024).toFixed(2)}MB`);
|
||||
console.log(`压缩率: ${((1 - totalCompressedSize / totalOriginalSize) * 100).toFixed(1)}%`);
|
||||
}
|
||||
}
|
||||
|
||||
// 删除未使用的 worker 文件
|
||||
function deleteUnusedWorkers() {
|
||||
const jsDir = path.join(__dirname, '../nfd-front/js');
|
||||
const workers = ['editor.worker.js', 'editor.worker.js.gz', 'json.worker.js', 'json.worker.js.gz', 'ts.worker.js', 'ts.worker.js.gz'];
|
||||
|
||||
let deletedCount = 0;
|
||||
for (const worker of workers) {
|
||||
const filePath = path.join(jsDir, worker);
|
||||
if (fs.existsSync(filePath)) {
|
||||
try {
|
||||
fs.unlinkSync(filePath);
|
||||
deletedCount++;
|
||||
console.log(`✓ 已删除未使用的文件: ${worker}`);
|
||||
} catch (error) {
|
||||
console.warn(`⚠ 删除失败: ${worker}`, error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (deletedCount > 0) {
|
||||
console.log(`\n已删除 ${deletedCount} 个未使用的 worker 文件\n`);
|
||||
}
|
||||
}
|
||||
|
||||
// 复制到 webroot
|
||||
function copyToWebroot() {
|
||||
const source = path.join(__dirname, '../nfd-front');
|
||||
const dest = path.join(__dirname, '../../webroot/nfd-front');
|
||||
|
||||
// 使用 FileManagerPlugin 的方式,这里用简单的复制
|
||||
const { execSync } = require('child_process');
|
||||
try {
|
||||
// 删除目标目录
|
||||
if (fs.existsSync(dest)) {
|
||||
execSync(`rm -rf "${dest}"`, { stdio: 'inherit' });
|
||||
}
|
||||
// 复制整个目录
|
||||
execSync(`cp -R "${source}" "${dest}"`, { stdio: 'inherit' });
|
||||
console.log('\n✓ 已复制到 webroot');
|
||||
} catch (error) {
|
||||
console.error('\n✗ 复制到 webroot 失败:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 主函数
|
||||
async function main() {
|
||||
// 先删除未使用的 worker 文件
|
||||
deleteUnusedWorkers();
|
||||
|
||||
// 然后压缩 vs 目录
|
||||
const vsPath = path.join(__dirname, '../nfd-front/js/vs');
|
||||
console.log('开始压缩 vs 目录下的文件...\n');
|
||||
try {
|
||||
await compressDirectory(vsPath, 1024); // 只压缩超过1KB的文件
|
||||
console.log('\n✓ vs 目录压缩完成');
|
||||
} catch (error) {
|
||||
console.error('\n✗ vs 目录压缩失败:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 最后复制到 webroot
|
||||
copyToWebroot();
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
@@ -43,12 +43,16 @@ watch(darkMode, (newValue) => {
|
||||
emit('theme-change', newValue)
|
||||
|
||||
// 应用主题到body
|
||||
if (newValue) {
|
||||
document.body.classList.add('dark-theme')
|
||||
document.documentElement.classList.add('dark-theme')
|
||||
} else {
|
||||
document.body.classList.remove('dark-theme')
|
||||
document.documentElement.classList.remove('dark-theme')
|
||||
const html = document.documentElement;
|
||||
const body = document.body;
|
||||
if (html && body && html.classList && body.classList) {
|
||||
if (newValue) {
|
||||
body.classList.add('dark-theme')
|
||||
html.classList.add('dark-theme')
|
||||
} else {
|
||||
body.classList.remove('dark-theme')
|
||||
html.classList.remove('dark-theme')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -57,9 +61,11 @@ onMounted(() => {
|
||||
emit('theme-change', darkMode.value)
|
||||
|
||||
// 应用初始主题
|
||||
if (darkMode.value) {
|
||||
document.body.classList.add('dark-theme')
|
||||
document.documentElement.classList.add('dark-theme')
|
||||
const html = document.documentElement;
|
||||
const body = document.body;
|
||||
if (html && body && html.classList && body.classList && darkMode.value) {
|
||||
body.classList.add('dark-theme')
|
||||
html.classList.add('dark-theme')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -388,8 +388,14 @@ export default {
|
||||
return date.toLocaleString('zh-CN')
|
||||
},
|
||||
checkTheme() {
|
||||
this.isDarkTheme = document.body.classList.contains('dark-theme') ||
|
||||
document.documentElement.classList.contains('dark-theme')
|
||||
const html = document.documentElement;
|
||||
const body = document.body;
|
||||
if (html && body && html.classList && body.classList) {
|
||||
this.isDarkTheme = body.classList.contains('dark-theme') ||
|
||||
html.classList.contains('dark-theme')
|
||||
} else {
|
||||
this.isDarkTheme = false;
|
||||
}
|
||||
},
|
||||
renderContent(h, { node, data, store }) {
|
||||
const isFolder = data.fileType === 'folder'
|
||||
|
||||
@@ -94,6 +94,19 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
// 配置Monaco Editor使用本地打包的文件,而不是CDN
|
||||
if (loader.config) {
|
||||
const vsPath = process.env.NODE_ENV === 'production'
|
||||
? './js/vs' // 生产环境使用相对路径
|
||||
: '/js/vs'; // 开发环境使用绝对路径
|
||||
|
||||
loader.config({
|
||||
paths: {
|
||||
vs: vsPath
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化Monaco Editor
|
||||
monaco = await loader.init();
|
||||
|
||||
|
||||
@@ -10,7 +10,13 @@ const routes = [
|
||||
{ path: '/showFile', component: ShowFile },
|
||||
{ path: '/showList', component: ShowList },
|
||||
{ path: '/clientLinks', component: ClientLinks },
|
||||
{ path: '/playground', component: Playground }
|
||||
{ path: '/playground', component: Playground },
|
||||
// 404页面 - 必须放在最后
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
name: 'NotFound',
|
||||
component: () => import('@/views/NotFound.vue')
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import axios from 'axios';
|
||||
|
||||
// 创建axios实例,配置携带cookie
|
||||
const axiosInstance = axios.create({
|
||||
withCredentials: true // 重要:允许跨域请求携带cookie
|
||||
});
|
||||
|
||||
/**
|
||||
* 演练场API服务
|
||||
*/
|
||||
@@ -10,7 +15,7 @@ export const playgroundApi = {
|
||||
*/
|
||||
async getStatus() {
|
||||
try {
|
||||
const response = await axios.get('/v2/playground/status');
|
||||
const response = await axiosInstance.get('/v2/playground/status');
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(error.response?.data?.error || error.message || '获取状态失败');
|
||||
@@ -24,7 +29,7 @@ export const playgroundApi = {
|
||||
*/
|
||||
async login(password) {
|
||||
try {
|
||||
const response = await axios.post('/v2/playground/login', { password });
|
||||
const response = await axiosInstance.post('/v2/playground/login', { password });
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(error.response?.data?.error || error.message || '登录失败');
|
||||
@@ -41,7 +46,7 @@ export const playgroundApi = {
|
||||
*/
|
||||
async testScript(jsCode, shareUrl, pwd = '', method = 'parse') {
|
||||
try {
|
||||
const response = await axios.post('/v2/playground/test', {
|
||||
const response = await axiosInstance.post('/v2/playground/test', {
|
||||
jsCode,
|
||||
shareUrl,
|
||||
pwd,
|
||||
@@ -69,7 +74,7 @@ export const playgroundApi = {
|
||||
*/
|
||||
async getTypesJs() {
|
||||
try {
|
||||
const response = await axios.get('/v2/playground/types.js', {
|
||||
const response = await axiosInstance.get('/v2/playground/types.js', {
|
||||
responseType: 'text'
|
||||
});
|
||||
return response.data;
|
||||
@@ -83,7 +88,7 @@ export const playgroundApi = {
|
||||
*/
|
||||
async getParserList() {
|
||||
try {
|
||||
const response = await axios.get('/v2/playground/parsers');
|
||||
const response = await axiosInstance.get('/v2/playground/parsers');
|
||||
// 框架会自动包装成JsonResult,需要从data字段获取
|
||||
if (response.data && response.data.data) {
|
||||
return {
|
||||
@@ -104,7 +109,7 @@ export const playgroundApi = {
|
||||
*/
|
||||
async saveParser(jsCode) {
|
||||
try {
|
||||
const response = await axios.post('/v2/playground/parsers', { jsCode });
|
||||
const response = await axiosInstance.post('/v2/playground/parsers', { jsCode });
|
||||
// 框架会自动包装成JsonResult
|
||||
if (response.data && response.data.data) {
|
||||
return {
|
||||
@@ -130,7 +135,7 @@ export const playgroundApi = {
|
||||
*/
|
||||
async updateParser(id, jsCode, enabled = true) {
|
||||
try {
|
||||
const response = await axios.put(`/v2/playground/parsers/${id}`, { jsCode, enabled });
|
||||
const response = await axiosInstance.put(`/v2/playground/parsers/${id}`, { jsCode, enabled });
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(error.response?.data?.error || error.message || '更新解析器失败');
|
||||
@@ -142,7 +147,7 @@ export const playgroundApi = {
|
||||
*/
|
||||
async deleteParser(id) {
|
||||
try {
|
||||
const response = await axios.delete(`/v2/playground/parsers/${id}`);
|
||||
const response = await axiosInstance.delete(`/v2/playground/parsers/${id}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(error.response?.data?.error || error.message || '删除解析器失败');
|
||||
@@ -154,7 +159,7 @@ export const playgroundApi = {
|
||||
*/
|
||||
async getParserById(id) {
|
||||
try {
|
||||
const response = await axios.get(`/v2/playground/parsers/${id}`);
|
||||
const response = await axiosInstance.get(`/v2/playground/parsers/${id}`);
|
||||
// 框架会自动包装成JsonResult
|
||||
if (response.data && response.data.data) {
|
||||
return {
|
||||
@@ -170,54 +175,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代码失败');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
@@ -48,7 +48,7 @@
|
||||
</div>
|
||||
<!-- 项目简介移到卡片内 -->
|
||||
<div class="project-intro">
|
||||
<div class="intro-title">NFD网盘直链解析0.1.9_b12</div>
|
||||
<div class="intro-title">NFD网盘直链解析0.1.9_b15</div>
|
||||
<div class="intro-desc">
|
||||
<div>支持网盘:蓝奏云、蓝奏云优享、小飞机盘、123云盘、奶牛快传、移动云空间、QQ邮箱云盘、QQ闪传等 <el-link style="color:#606cf5" href="https://github.com/qaiu/netdisk-fast-download?tab=readme-ov-file#%E7%BD%91%E7%9B%98%E6%94%AF%E6%8C%81%E6%83%85%E5%86%B5" target="_blank"> >> </el-link></div>
|
||||
<div>文件夹解析支持:蓝奏云、蓝奏云优享、小飞机盘、123云盘</div>
|
||||
@@ -218,7 +218,7 @@
|
||||
<!-- 版本号显示 -->
|
||||
<div class="version-info">
|
||||
<span class="version-text">内部版本: {{ buildVersion }}</span>
|
||||
<!-- <el-link :href="'/playground'" class="playground-link">JS演练场</el-link>-->
|
||||
<el-link v-if="playgroundEnabled" :href="'/playground'" class="playground-link">脚本演练场</el-link>
|
||||
</div>
|
||||
|
||||
<!-- 文件解析结果区下方加分享按钮 -->
|
||||
@@ -248,6 +248,7 @@ import DirectoryTree from '@/components/DirectoryTree'
|
||||
import parserUrl from '../parserUrl1'
|
||||
import fileTypeUtils from '@/utils/fileTypeUtils'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { playgroundApi } from '@/utils/playgroundApi'
|
||||
|
||||
export const previewBaseUrl = 'https://nfd-parser.github.io/nfd-preview/preview.html?src=';
|
||||
|
||||
@@ -297,7 +298,10 @@ export default {
|
||||
errorButtonVisible: false,
|
||||
|
||||
// 版本信息
|
||||
buildVersion: ''
|
||||
buildVersion: '',
|
||||
|
||||
// 演练场启用状态
|
||||
playgroundEnabled: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -316,7 +320,9 @@ export default {
|
||||
// 主题切换
|
||||
handleThemeChange(isDark) {
|
||||
this.isDarkMode = isDark
|
||||
document.body.classList.toggle('dark-theme', isDark)
|
||||
if (document.body && document.body.classList) {
|
||||
document.body.classList.toggle('dark-theme', isDark)
|
||||
}
|
||||
window.localStorage.setItem('isDarkMode', isDark)
|
||||
|
||||
},
|
||||
@@ -552,6 +558,19 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
// 检查演练场是否启用
|
||||
async checkPlaygroundEnabled() {
|
||||
try {
|
||||
const result = await playgroundApi.getStatus()
|
||||
if (result && result.data) {
|
||||
this.playgroundEnabled = result.data.enabled === true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('检查演练场状态失败:', error)
|
||||
this.playgroundEnabled = false
|
||||
}
|
||||
},
|
||||
|
||||
// 新增切换目录树展示模式方法
|
||||
setDirectoryViewMode(mode) {
|
||||
this.directoryViewMode = mode
|
||||
@@ -656,6 +675,9 @@ export default {
|
||||
// 获取版本号
|
||||
this.getBuildVersion()
|
||||
|
||||
// 检查演练场是否启用
|
||||
this.checkPlaygroundEnabled()
|
||||
|
||||
// 自动读取剪切板
|
||||
if (this.autoReadClipboard) {
|
||||
this.getPaste()
|
||||
|
||||
135
web-front/src/views/NotFound.vue
Normal file
135
web-front/src/views/NotFound.vue
Normal file
@@ -0,0 +1,135 @@
|
||||
<template>
|
||||
<div class="not-found-container">
|
||||
<div class="not-found-content">
|
||||
<div class="not-found-icon">
|
||||
<el-icon :size="120"><DocumentDelete /></el-icon>
|
||||
</div>
|
||||
<h1 class="not-found-title">404</h1>
|
||||
<p class="not-found-message">抱歉,您访问的页面不存在</p>
|
||||
<div class="not-found-actions">
|
||||
<el-button type="primary" @click="goHome">返回首页</el-button>
|
||||
<el-button @click="goBack">返回上一页</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useRouter } from 'vue-router'
|
||||
import { DocumentDelete } from '@element-plus/icons-vue'
|
||||
|
||||
export default {
|
||||
name: 'NotFound',
|
||||
components: {
|
||||
DocumentDelete
|
||||
},
|
||||
setup() {
|
||||
const router = useRouter()
|
||||
|
||||
const goHome = () => {
|
||||
router.push('/')
|
||||
}
|
||||
|
||||
const goBack = () => {
|
||||
if (window.history.length > 1) {
|
||||
router.go(-1)
|
||||
} else {
|
||||
router.push('/')
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
goHome,
|
||||
goBack
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.not-found-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
/* background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); */
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.not-found-content {
|
||||
text-align: center;
|
||||
background: white;
|
||||
padding: 60px 40px;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.not-found-icon {
|
||||
color: #909399;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.not-found-title {
|
||||
font-size: 72px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
margin: 0 0 20px 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.not-found-message {
|
||||
font-size: 18px;
|
||||
color: #606266;
|
||||
margin: 0 0 40px 0;
|
||||
}
|
||||
|
||||
.not-found-actions {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 暗色主题支持 */
|
||||
.dark-theme .not-found-content {
|
||||
background: #1d1e1f;
|
||||
}
|
||||
|
||||
.dark-theme .not-found-title {
|
||||
color: #e5eaf3;
|
||||
}
|
||||
|
||||
.dark-theme .not-found-message {
|
||||
color: #a3a6ad;
|
||||
}
|
||||
|
||||
.dark-theme .not-found-icon {
|
||||
color: #6c6e72;
|
||||
}
|
||||
|
||||
/* 移动端适配 */
|
||||
@media (max-width: 768px) {
|
||||
.not-found-content {
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.not-found-title {
|
||||
font-size: 48px;
|
||||
}
|
||||
|
||||
.not-found-message {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.not-found-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.not-found-actions .el-button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -61,12 +61,16 @@ export default {
|
||||
}
|
||||
},
|
||||
toggleTheme(isDark) {
|
||||
if (isDark) {
|
||||
document.body.classList.add('dark-theme')
|
||||
document.documentElement.classList.add('dark-theme')
|
||||
} else {
|
||||
document.body.classList.remove('dark-theme')
|
||||
document.documentElement.classList.remove('dark-theme')
|
||||
const html = document.documentElement;
|
||||
const body = document.body;
|
||||
if (html && body && html.classList && body.classList) {
|
||||
if (isDark) {
|
||||
body.classList.add('dark-theme')
|
||||
html.classList.add('dark-theme')
|
||||
} else {
|
||||
body.classList.remove('dark-theme')
|
||||
html.classList.remove('dark-theme')
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -4,8 +4,10 @@ const path = require("path");
|
||||
function resolve(dir) {
|
||||
return path.join(__dirname, dir)
|
||||
}
|
||||
|
||||
const CompressionPlugin = require('compression-webpack-plugin');
|
||||
const FileManagerPlugin = require('filemanager-webpack-plugin')
|
||||
const FileManagerPlugin = require('filemanager-webpack-plugin');
|
||||
const MonacoEditorPlugin = require('monaco-editor-webpack-plugin');
|
||||
|
||||
module.exports = {
|
||||
productionSourceMap: false, // 是否在构建生产包时生成sourceMap文件,false将提高构建速度
|
||||
@@ -43,7 +45,7 @@ module.exports = {
|
||||
'@': resolve('src')
|
||||
}
|
||||
},
|
||||
// Monaco Editor配置
|
||||
// Monaco Editor配置 - 使用本地打包
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
@@ -53,9 +55,18 @@ module.exports = {
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new MonacoEditorPlugin({
|
||||
languages: ['javascript', 'typescript', 'json'],
|
||||
features: ['coreCommands', 'find', 'format', 'suggest', 'quickCommand'],
|
||||
publicPath: process.env.NODE_ENV === 'production' ? './' : '/',
|
||||
// Worker 文件输出路径
|
||||
filename: 'js/[name].worker.js'
|
||||
}),
|
||||
new CompressionPlugin({
|
||||
test: /\.js$|\.html$|\.css/, // 匹配文件
|
||||
threshold: 10240 // 对超过10k文件压缩
|
||||
threshold: 10240, // 对超过10k文件压缩
|
||||
// 排除 js 目录下的 worker 文件(Monaco Editor 使用 vs/assets 下的)
|
||||
exclude: /js\/.*\.worker\.js$/
|
||||
}),
|
||||
new FileManagerPlugin({ //初始化 filemanager-webpack-plugin 插件实例
|
||||
events: {
|
||||
@@ -70,7 +81,11 @@ module.exports = {
|
||||
{ source: '../webroot/nfd-front/view/.gitignore', options: { force: true } },
|
||||
],
|
||||
copy: [
|
||||
{ source: './nfd-front', destination: '../webroot/nfd-front' }
|
||||
// 复制 Monaco Editor 的 vs 目录到 js/vs
|
||||
{
|
||||
source: './node_modules/monaco-editor/min/vs',
|
||||
destination: './nfd-front/js/vs'
|
||||
}
|
||||
],
|
||||
archive: [ //然后我们选择dist文件夹将之打包成dist.zip并放在根目录
|
||||
{
|
||||
|
||||
148
web-service/doc/FUNCTIONAL_TEST_REPORT.md
Normal file
148
web-service/doc/FUNCTIONAL_TEST_REPORT.md
Normal file
@@ -0,0 +1,148 @@
|
||||
# 脚本演练场功能测试报告
|
||||
|
||||
## 测试时间
|
||||
2026-01-02 19:29
|
||||
|
||||
## 测试环境
|
||||
- 服务地址: http://localhost:6401
|
||||
- 后端版本: 0.1.8
|
||||
- 前端版本: 0.1.9
|
||||
|
||||
## 测试结果总结
|
||||
|
||||
### ✅ 1. 服务启动测试
|
||||
- **状态**: 通过
|
||||
- **结果**: 服务成功启动,监听端口6401
|
||||
- **日志**:
|
||||
```
|
||||
演练场解析器加载完成,共加载 0 个解析器
|
||||
数据库连接成功
|
||||
启动成功: 本地服务地址: http://127.0.0.1:6401
|
||||
```
|
||||
|
||||
### ✅ 2. 密码认证功能测试
|
||||
- **状态**: 通过
|
||||
- **测试项**:
|
||||
- ✅ `/v2/playground/status` API正常响应
|
||||
- ✅ `/v2/playground/login` 登录API正常响应
|
||||
- ✅ 密码验证机制正常工作
|
||||
- **结果**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "登录成功",
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
### ✅ 3. BUG1修复验证:JS超时机制
|
||||
- **状态**: 已修复
|
||||
- **修复内容**:
|
||||
- 在`JsPlaygroundExecutor`中实现了线程中断机制
|
||||
- 使用`ScheduledExecutorService`和`Future.cancel(true)`确保超时后强制中断
|
||||
- 超时时间设置为30秒
|
||||
- **代码位置**: `parser/src/main/java/cn/qaiu/parser/customjs/JsPlaygroundExecutor.java`
|
||||
- **验证**: 代码已编译通过,超时机制已实现
|
||||
|
||||
### ✅ 4. BUG2修复验证:URL正则匹配验证
|
||||
- **状态**: 已修复
|
||||
- **修复内容**:
|
||||
- 在`PlaygroundApi.test()`方法中添加了URL匹配验证
|
||||
- 执行前检查分享链接是否匹配脚本的`@match`规则
|
||||
- 不匹配时返回明确的错误提示
|
||||
- **代码位置**: `web-service/src/main/java/cn/qaiu/lz/web/controller/PlaygroundApi.java:185-209`
|
||||
- **验证**: 代码已编译通过,验证逻辑已实现
|
||||
|
||||
### ✅ 5. BUG3修复验证:脚本注册功能
|
||||
- **状态**: 已修复
|
||||
- **修复内容**:
|
||||
- 在`PlaygroundApi.saveParser()`中保存后立即注册到`CustomParserRegistry`
|
||||
- 在`PlaygroundApi.updateParser()`中更新后重新注册
|
||||
- 在`PlaygroundApi.deleteParser()`中删除时注销
|
||||
- 在`AppMain`启动时加载所有已发布的解析器
|
||||
- **代码位置**:
|
||||
- `web-service/src/main/java/cn/qaiu/lz/web/controller/PlaygroundApi.java`
|
||||
- `web-service/src/main/java/cn/qaiu/lz/AppMain.java`
|
||||
- **验证**: 代码已编译通过,注册机制已实现
|
||||
|
||||
### ✅ 6. TypeScript功能移除
|
||||
- **状态**: 已完成
|
||||
- **移除内容**:
|
||||
- ✅ 删除`web-front/src/utils/tsCompiler.js`
|
||||
- ✅ 从`package.json`移除`typescript`依赖
|
||||
- ✅ 从`Playground.vue`移除TypeScript相关UI和逻辑
|
||||
- ✅ 删除后端TypeScript API端点
|
||||
- ✅ 删除`PlaygroundTypeScriptCode`模型类
|
||||
- ✅ 删除TypeScript相关文档文件
|
||||
- **验证**: 代码已编译通过,无TypeScript相关代码残留
|
||||
|
||||
### ✅ 7. 文本更新:JS演练场 → 脚本演练场
|
||||
- **状态**: 已完成
|
||||
- **更新位置**:
|
||||
- ✅ `Home.vue`: "JS演练场" → "脚本演练场"
|
||||
- ✅ `Playground.vue`: "JS解析器演练场" → "脚本解析器演练场" (3处)
|
||||
- **验证**: 前端已重新编译并部署到webroot
|
||||
|
||||
### ✅ 8. 移动端布局优化
|
||||
- **状态**: 已保留
|
||||
- **说明**: 移动端布局优化功能已从`copilot/add-playground-enhancements`分支合并,代码已保留
|
||||
- **文档**: `web-front/PLAYGROUND_UI_UPGRADE.md`
|
||||
|
||||
## 编译验证
|
||||
|
||||
### 后端编译
|
||||
```bash
|
||||
mvn clean package -DskipTests -pl web-service -am
|
||||
```
|
||||
- **结果**: ✅ BUILD SUCCESS
|
||||
- **时间**: 5.614秒
|
||||
|
||||
### 前端编译
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
- **结果**: ✅ Build complete
|
||||
- **输出**: `nfd-front`目录已自动复制到`../webroot/nfd-front`
|
||||
|
||||
## 待浏览器环境测试项
|
||||
|
||||
以下测试项需要在浏览器环境中进行完整验证(需要session支持):
|
||||
|
||||
1. **密码认证流程**
|
||||
- 访问演练场页面
|
||||
- 输入密码登录
|
||||
- 验证登录后的访问权限
|
||||
|
||||
2. **BUG2完整测试**
|
||||
- 在演练场输入脚本(带@match规则)
|
||||
- 输入不匹配的分享链接
|
||||
- 验证是否显示匹配错误提示
|
||||
|
||||
3. **BUG3完整测试**
|
||||
- 发布一个脚本
|
||||
- 验证脚本是否立即可用
|
||||
- 通过分享链接调用验证
|
||||
|
||||
4. **移动端布局测试**
|
||||
- 使用移动设备或浏览器开发者工具
|
||||
- 验证响应式布局是否正常
|
||||
|
||||
## 代码质量
|
||||
|
||||
- ✅ 无编译错误
|
||||
- ✅ 无Linter错误
|
||||
- ✅ 所有TODO任务已完成
|
||||
- ✅ 代码已合并到main分支
|
||||
|
||||
## 总结
|
||||
|
||||
所有核心功能修复已完成并通过编译验证:
|
||||
- ✅ BUG1: JS超时机制已实现
|
||||
- ✅ BUG2: URL正则匹配验证已实现
|
||||
- ✅ BUG3: 脚本注册功能已实现
|
||||
- ✅ TypeScript功能已移除
|
||||
- ✅ 文本更新已完成
|
||||
- ✅ 代码已合并到main分支
|
||||
|
||||
服务已成功启动,可以进行浏览器环境下的完整功能测试。
|
||||
|
||||
0
web-service/doc/PLAYGROUND_GUIDE.md
Normal file
0
web-service/doc/PLAYGROUND_GUIDE.md
Normal file
@@ -5,7 +5,12 @@ import cn.qaiu.db.pool.JDBCPoolInit;
|
||||
import cn.qaiu.lz.common.cache.CacheConfigLoader;
|
||||
import cn.qaiu.lz.common.interceptorImpl.RateLimiter;
|
||||
import cn.qaiu.lz.web.config.PlaygroundConfig;
|
||||
import cn.qaiu.lz.web.service.DbService;
|
||||
import cn.qaiu.parser.custom.CustomParserConfig;
|
||||
import cn.qaiu.parser.custom.CustomParserRegistry;
|
||||
import cn.qaiu.parser.customjs.JsScriptMetadataParser;
|
||||
import cn.qaiu.vx.core.Deploy;
|
||||
import cn.qaiu.vx.core.util.AsyncServiceUtil;
|
||||
import cn.qaiu.vx.core.util.ConfigConstant;
|
||||
import cn.qaiu.vx.core.util.VertxHolder;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
@@ -13,6 +18,7 @@ import io.vertx.core.json.JsonArray;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import io.vertx.core.json.jackson.DatabindCodec;
|
||||
import io.vertx.core.shareddata.LocalMap;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.time.DateFormatUtils;
|
||||
|
||||
import java.util.Date;
|
||||
@@ -26,6 +32,7 @@ import static cn.qaiu.vx.core.util.ConfigConstant.LOCAL;
|
||||
* <br>Create date 2021-05-08 13:00:01
|
||||
* @author qaiu yyzy
|
||||
*/
|
||||
@Slf4j
|
||||
public class AppMain {
|
||||
|
||||
public static void main(String[] args) {
|
||||
@@ -55,6 +62,10 @@ public class AppMain {
|
||||
VertxHolder.getVertxInstance().setTimer(1000, id -> {
|
||||
System.out.println(DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss.SSS"));
|
||||
System.out.println("数据库连接成功");
|
||||
|
||||
// 加载演练场解析器
|
||||
loadPlaygroundParsers();
|
||||
|
||||
String addr = jsonObject.getJsonObject(ConfigConstant.SERVER).getString("domainName");
|
||||
System.out.println("启动成功: \n本地服务地址: " + addr);
|
||||
});
|
||||
@@ -93,4 +104,50 @@ public class AppMain {
|
||||
// 演练场配置
|
||||
PlaygroundConfig.loadFromJson(jsonObject);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在启动时加载所有已发布的演练场解析器
|
||||
*/
|
||||
private static void loadPlaygroundParsers() {
|
||||
DbService dbService = AsyncServiceUtil.getAsyncServiceInstance(DbService.class);
|
||||
|
||||
dbService.getPlaygroundParserList().onSuccess(result -> {
|
||||
JsonArray parsers = result.getJsonArray("data");
|
||||
if (parsers != null) {
|
||||
int loadedCount = 0;
|
||||
for (int i = 0; i < parsers.size(); i++) {
|
||||
JsonObject parser = parsers.getJsonObject(i);
|
||||
|
||||
// 只注册已启用的解析器
|
||||
if (parser.getBoolean("enabled", false)) {
|
||||
try {
|
||||
String jsCode = parser.getString("jsCode");
|
||||
if (jsCode == null || jsCode.trim().isEmpty()) {
|
||||
log.error("加载演练场解析器失败: {} - JavaScript代码为空", parser.getString("name"));
|
||||
continue;
|
||||
}
|
||||
CustomParserConfig config = JsScriptMetadataParser.parseScript(jsCode);
|
||||
CustomParserRegistry.register(config);
|
||||
loadedCount++;
|
||||
log.info("已加载演练场解析器: {} ({})",
|
||||
config.getDisplayName(), config.getType());
|
||||
} catch (Exception e) {
|
||||
String parserName = parser.getString("name");
|
||||
String errorMsg = e.getMessage();
|
||||
log.error("加载演练场解析器失败: {} - {}", parserName, errorMsg, e);
|
||||
// 如果是require相关错误,提供更详细的提示
|
||||
if (errorMsg != null && errorMsg.contains("require")) {
|
||||
log.error("提示:演练场解析器不支持CommonJS模块系统(require),请确保代码使用ES5.1语法");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
log.info("演练场解析器加载完成,共加载 {} 个解析器", loadedCount);
|
||||
} else {
|
||||
log.info("未找到已发布的演练场解析器");
|
||||
}
|
||||
}).onFailure(e -> {
|
||||
log.error("加载演练场解析器列表失败", e);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,12 @@ public class PlaygroundConfig {
|
||||
*/
|
||||
private static PlaygroundConfig instance;
|
||||
|
||||
/**
|
||||
* 是否启用演练场
|
||||
* 默认false,不启用
|
||||
*/
|
||||
private boolean enabled = false;
|
||||
|
||||
/**
|
||||
* 是否公开模式(不需要密码)
|
||||
* 默认false,需要密码访问
|
||||
@@ -57,17 +63,20 @@ public class PlaygroundConfig {
|
||||
PlaygroundConfig cfg = getInstance();
|
||||
if (config != null && config.containsKey("playground")) {
|
||||
JsonObject playgroundConfig = config.getJsonObject("playground");
|
||||
cfg.enabled = playgroundConfig.getBoolean("enabled", false);
|
||||
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" : "已设置");
|
||||
log.info("Playground配置已加载: enabled={}, public={}, password={}",
|
||||
cfg.enabled, cfg.isPublic, cfg.isPublic ? "N/A" : "已设置");
|
||||
|
||||
if (!cfg.isPublic && "nfd_playground_2024".equals(cfg.password)) {
|
||||
if (!cfg.enabled) {
|
||||
log.info("演练场功能已禁用");
|
||||
} else if (!cfg.isPublic && "nfd_playground_2024".equals(cfg.password)) {
|
||||
log.warn("⚠️ 警告:您正在使用默认密码,建议修改配置文件中的 playground.password 以确保安全!");
|
||||
}
|
||||
} else {
|
||||
log.info("未找到playground配置,使用默认值: public=false");
|
||||
log.info("未找到playground配置,使用默认值: enabled=false, public=false");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ 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.CustomParserRegistry;
|
||||
import cn.qaiu.parser.customjs.JsPlaygroundExecutor;
|
||||
import cn.qaiu.parser.customjs.JsPlaygroundLogger;
|
||||
import cn.qaiu.parser.customjs.JsScriptMetadataParser;
|
||||
@@ -30,6 +31,8 @@ import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
@@ -47,10 +50,23 @@ public class PlaygroundApi {
|
||||
private static final String SESSION_AUTH_KEY = "playgroundAuthed";
|
||||
private final DbService dbService = AsyncServiceUtil.getAsyncServiceInstance(DbService.class);
|
||||
|
||||
/**
|
||||
* 检查Playground是否启用
|
||||
*/
|
||||
private boolean checkEnabled() {
|
||||
PlaygroundConfig config = PlaygroundConfig.getInstance();
|
||||
return config.isEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查Playground访问权限
|
||||
*/
|
||||
private boolean checkAuth(RoutingContext ctx) {
|
||||
// 首先检查是否启用
|
||||
if (!checkEnabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
PlaygroundConfig config = PlaygroundConfig.getInstance();
|
||||
|
||||
// 如果是公开模式,直接允许访问
|
||||
@@ -74,9 +90,11 @@ public class PlaygroundApi {
|
||||
@RouteMapping(value = "/status", method = RouteMethod.GET)
|
||||
public Future<JsonObject> getStatus(RoutingContext ctx) {
|
||||
PlaygroundConfig config = PlaygroundConfig.getInstance();
|
||||
boolean authed = checkAuth(ctx);
|
||||
boolean enabled = config.isEnabled();
|
||||
boolean authed = enabled && checkAuth(ctx);
|
||||
|
||||
JsonObject result = new JsonObject()
|
||||
.put("enabled", enabled)
|
||||
.put("public", config.isPublic())
|
||||
.put("authed", authed);
|
||||
|
||||
@@ -88,6 +106,11 @@ public class PlaygroundApi {
|
||||
*/
|
||||
@RouteMapping(value = "/login", method = RouteMethod.POST)
|
||||
public Future<JsonObject> login(RoutingContext ctx) {
|
||||
// 检查是否启用
|
||||
if (!checkEnabled()) {
|
||||
return Future.succeededFuture(JsonResult.error("演练场功能已禁用").toJsonObject());
|
||||
}
|
||||
|
||||
Promise<JsonObject> promise = Promise.promise();
|
||||
|
||||
try {
|
||||
@@ -139,6 +162,11 @@ public class PlaygroundApi {
|
||||
*/
|
||||
@RouteMapping(value = "/test", method = RouteMethod.POST)
|
||||
public Future<JsonObject> test(RoutingContext ctx) {
|
||||
// 检查是否启用
|
||||
if (!checkEnabled()) {
|
||||
return Future.succeededFuture(JsonResult.error("演练场功能已禁用").toJsonObject());
|
||||
}
|
||||
|
||||
// 权限检查
|
||||
if (!checkAuth(ctx)) {
|
||||
return Future.succeededFuture(JsonResult.error("未授权访问").toJsonObject());
|
||||
@@ -179,6 +207,32 @@ public class PlaygroundApi {
|
||||
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)) {
|
||||
promise.complete(JsonObject.mapFrom(PlaygroundTestResp.builder()
|
||||
@@ -316,6 +370,12 @@ public class PlaygroundApi {
|
||||
*/
|
||||
@RouteMapping(value = "/types.js", method = RouteMethod.GET)
|
||||
public void getTypesJs(RoutingContext ctx, HttpServerResponse response) {
|
||||
// 检查是否启用
|
||||
if (!checkEnabled()) {
|
||||
ResponseUtil.fireJsonResultResponse(response, JsonResult.error("演练场功能已禁用"));
|
||||
return;
|
||||
}
|
||||
|
||||
// 权限检查
|
||||
if (!checkAuth(ctx)) {
|
||||
ResponseUtil.fireJsonResultResponse(response, JsonResult.error("未授权访问"));
|
||||
@@ -348,6 +408,11 @@ public class PlaygroundApi {
|
||||
*/
|
||||
@RouteMapping(value = "/parsers", method = RouteMethod.GET)
|
||||
public Future<JsonObject> getParserList(RoutingContext ctx) {
|
||||
// 检查是否启用
|
||||
if (!checkEnabled()) {
|
||||
return Future.succeededFuture(JsonResult.error("演练场功能已禁用").toJsonObject());
|
||||
}
|
||||
|
||||
// 权限检查
|
||||
if (!checkAuth(ctx)) {
|
||||
return Future.succeededFuture(JsonResult.error("未授权访问").toJsonObject());
|
||||
@@ -360,6 +425,11 @@ public class PlaygroundApi {
|
||||
*/
|
||||
@RouteMapping(value = "/parsers", method = RouteMethod.POST)
|
||||
public Future<JsonObject> saveParser(RoutingContext ctx) {
|
||||
// 检查是否启用
|
||||
if (!checkEnabled()) {
|
||||
return Future.succeededFuture(JsonResult.error("演练场功能已禁用").toJsonObject());
|
||||
}
|
||||
|
||||
// 权限检查
|
||||
if (!checkAuth(ctx)) {
|
||||
return Future.succeededFuture(JsonResult.error("未授权访问").toJsonObject());
|
||||
@@ -433,7 +503,18 @@ public class PlaygroundApi {
|
||||
parser.put("enabled", true);
|
||||
|
||||
dbService.savePlaygroundParser(parser).onSuccess(result -> {
|
||||
promise.complete(result);
|
||||
// 保存成功后,立即注册到解析器系统
|
||||
try {
|
||||
CustomParserRegistry.register(config);
|
||||
log.info("已注册演练场解析器: {} ({})", displayName, type);
|
||||
promise.complete(JsonResult.success("保存并注册成功").toJsonObject());
|
||||
} catch (Exception e) {
|
||||
log.error("注册解析器失败", e);
|
||||
// 虽然注册失败,但保存成功了,返回警告
|
||||
promise.complete(JsonResult.success(
|
||||
"保存成功,但注册失败(重启服务后会自动加载): " + e.getMessage()
|
||||
).toJsonObject());
|
||||
}
|
||||
}).onFailure(e -> {
|
||||
log.error("保存解析器失败", e);
|
||||
promise.complete(JsonResult.error("保存失败: " + e.getMessage()).toJsonObject());
|
||||
@@ -464,6 +545,11 @@ public class PlaygroundApi {
|
||||
*/
|
||||
@RouteMapping(value = "/parsers/:id", method = RouteMethod.PUT)
|
||||
public Future<JsonObject> updateParser(RoutingContext ctx, Long id) {
|
||||
// 检查是否启用
|
||||
if (!checkEnabled()) {
|
||||
return Future.succeededFuture(JsonResult.error("演练场功能已禁用").toJsonObject());
|
||||
}
|
||||
|
||||
// 权限检查
|
||||
if (!checkAuth(ctx)) {
|
||||
return Future.succeededFuture(JsonResult.error("未授权访问").toJsonObject());
|
||||
@@ -483,12 +569,14 @@ public class PlaygroundApi {
|
||||
// 解析元数据
|
||||
try {
|
||||
var config = JsScriptMetadataParser.parseScript(jsCode);
|
||||
String type = config.getType();
|
||||
String displayName = config.getDisplayName();
|
||||
String name = config.getMetadata().get("name");
|
||||
String description = config.getMetadata().get("description");
|
||||
String author = config.getMetadata().get("author");
|
||||
String version = config.getMetadata().get("version");
|
||||
String matchPattern = config.getMatchPattern() != null ? config.getMatchPattern().pattern() : null;
|
||||
boolean enabled = body.getBoolean("enabled", true);
|
||||
|
||||
JsonObject parser = new JsonObject();
|
||||
parser.put("name", name);
|
||||
@@ -498,10 +586,29 @@ public class PlaygroundApi {
|
||||
parser.put("version", version);
|
||||
parser.put("matchPattern", matchPattern);
|
||||
parser.put("jsCode", jsCode);
|
||||
parser.put("enabled", body.getBoolean("enabled", true));
|
||||
parser.put("enabled", enabled);
|
||||
|
||||
dbService.updatePlaygroundParser(id, parser).onSuccess(result -> {
|
||||
promise.complete(result);
|
||||
// 更新成功后,重新注册解析器
|
||||
try {
|
||||
if (enabled) {
|
||||
// 先注销旧的(如果存在)
|
||||
CustomParserRegistry.unregister(type);
|
||||
// 重新注册新的
|
||||
CustomParserRegistry.register(config);
|
||||
log.info("已重新注册演练场解析器: {} ({})", displayName, type);
|
||||
} else {
|
||||
// 禁用时注销
|
||||
CustomParserRegistry.unregister(type);
|
||||
log.info("已注销演练场解析器: {}", type);
|
||||
}
|
||||
promise.complete(JsonResult.success("更新并重新注册成功").toJsonObject());
|
||||
} catch (Exception e) {
|
||||
log.error("重新注册解析器失败", e);
|
||||
promise.complete(JsonResult.success(
|
||||
"更新成功,但注册失败(重启服务后会自动加载): " + e.getMessage()
|
||||
).toJsonObject());
|
||||
}
|
||||
}).onFailure(e -> {
|
||||
log.error("更新解析器失败", e);
|
||||
promise.complete(JsonResult.error("更新失败: " + e.getMessage()).toJsonObject());
|
||||
@@ -524,11 +631,47 @@ public class PlaygroundApi {
|
||||
*/
|
||||
@RouteMapping(value = "/parsers/:id", method = RouteMethod.DELETE)
|
||||
public Future<JsonObject> deleteParser(RoutingContext ctx, Long id) {
|
||||
// 检查是否启用
|
||||
if (!checkEnabled()) {
|
||||
return Future.succeededFuture(JsonResult.error("演练场功能已禁用").toJsonObject());
|
||||
}
|
||||
|
||||
// 权限检查
|
||||
if (!checkAuth(ctx)) {
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -536,6 +679,11 @@ public class PlaygroundApi {
|
||||
*/
|
||||
@RouteMapping(value = "/parsers/:id", method = RouteMethod.GET)
|
||||
public Future<JsonObject> getParserById(RoutingContext ctx, Long id) {
|
||||
// 检查是否启用
|
||||
if (!checkEnabled()) {
|
||||
return Future.succeededFuture(JsonResult.error("演练场功能已禁用").toJsonObject());
|
||||
}
|
||||
|
||||
// 权限检查
|
||||
if (!checkAuth(ctx)) {
|
||||
return Future.succeededFuture(JsonResult.error("未授权访问").toJsonObject());
|
||||
@@ -543,146 +691,6 @@ public class PlaygroundApi {
|
||||
return dbService.getPlaygroundParserById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存TypeScript代码及其编译结果
|
||||
*/
|
||||
@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 {
|
||||
JsonObject body = ctx.body().asJsonObject();
|
||||
Long parserId = body.getLong("parserId");
|
||||
String tsCode = body.getString("tsCode");
|
||||
String es5Code = body.getString("es5Code");
|
||||
String compileErrors = body.getString("compileErrors");
|
||||
String compilerVersion = body.getString("compilerVersion");
|
||||
String compileOptions = body.getString("compileOptions");
|
||||
Boolean isValid = body.getBoolean("isValid", true);
|
||||
|
||||
if (parserId == null) {
|
||||
promise.complete(JsonResult.error("解析器ID不能为空").toJsonObject());
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
if (StringUtils.isBlank(tsCode)) {
|
||||
promise.complete(JsonResult.error("TypeScript代码不能为空").toJsonObject());
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
if (StringUtils.isBlank(es5Code)) {
|
||||
promise.complete(JsonResult.error("编译后的ES5代码不能为空").toJsonObject());
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
// 代码长度验证
|
||||
if (tsCode.length() > MAX_CODE_LENGTH || es5Code.length() > MAX_CODE_LENGTH) {
|
||||
promise.complete(JsonResult.error("代码长度超过限制(最大128KB)").toJsonObject());
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
JsonObject tsCodeInfo = new JsonObject();
|
||||
tsCodeInfo.put("parserId", parserId);
|
||||
tsCodeInfo.put("tsCode", tsCode);
|
||||
tsCodeInfo.put("es5Code", es5Code);
|
||||
tsCodeInfo.put("compileErrors", compileErrors);
|
||||
tsCodeInfo.put("compilerVersion", compilerVersion);
|
||||
tsCodeInfo.put("compileOptions", compileOptions);
|
||||
tsCodeInfo.put("isValid", isValid);
|
||||
tsCodeInfo.put("ip", getClientIp(ctx.request()));
|
||||
|
||||
dbService.saveTypeScriptCode(tsCodeInfo).onSuccess(result -> {
|
||||
promise.complete(result);
|
||||
}).onFailure(e -> {
|
||||
log.error("保存TypeScript代码失败", e);
|
||||
promise.complete(JsonResult.error("保存失败: " + e.getMessage()).toJsonObject());
|
||||
});
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("解析请求参数失败", e);
|
||||
promise.complete(JsonResult.error("解析请求参数失败: " + e.getMessage()).toJsonObject());
|
||||
}
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据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());
|
||||
}
|
||||
return dbService.getTypeScriptCodeByParserId(parserId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新TypeScript代码
|
||||
*/
|
||||
@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 {
|
||||
JsonObject body = ctx.body().asJsonObject();
|
||||
String tsCode = body.getString("tsCode");
|
||||
String es5Code = body.getString("es5Code");
|
||||
String compileErrors = body.getString("compileErrors");
|
||||
String compilerVersion = body.getString("compilerVersion");
|
||||
String compileOptions = body.getString("compileOptions");
|
||||
Boolean isValid = body.getBoolean("isValid", true);
|
||||
|
||||
if (StringUtils.isBlank(tsCode)) {
|
||||
promise.complete(JsonResult.error("TypeScript代码不能为空").toJsonObject());
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
if (StringUtils.isBlank(es5Code)) {
|
||||
promise.complete(JsonResult.error("编译后的ES5代码不能为空").toJsonObject());
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
// 代码长度验证
|
||||
if (tsCode.length() > MAX_CODE_LENGTH || es5Code.length() > MAX_CODE_LENGTH) {
|
||||
promise.complete(JsonResult.error("代码长度超过限制(最大128KB)").toJsonObject());
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
JsonObject tsCodeInfo = new JsonObject();
|
||||
tsCodeInfo.put("tsCode", tsCode);
|
||||
tsCodeInfo.put("es5Code", es5Code);
|
||||
tsCodeInfo.put("compileErrors", compileErrors);
|
||||
tsCodeInfo.put("compilerVersion", compilerVersion);
|
||||
tsCodeInfo.put("compileOptions", compileOptions);
|
||||
tsCodeInfo.put("isValid", isValid);
|
||||
|
||||
dbService.updateTypeScriptCode(parserId, tsCodeInfo).onSuccess(result -> {
|
||||
promise.complete(result);
|
||||
}).onFailure(e -> {
|
||||
log.error("更新TypeScript代码失败", e);
|
||||
promise.complete(JsonResult.error("更新失败: " + e.getMessage()).toJsonObject());
|
||||
});
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("解析请求参数失败", e);
|
||||
promise.complete(JsonResult.error("解析请求参数失败: " + e.getMessage()).toJsonObject());
|
||||
}
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户端IP
|
||||
*/
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
package cn.qaiu.lz.web.model;
|
||||
|
||||
import cn.qaiu.db.ddl.Constraint;
|
||||
import cn.qaiu.db.ddl.Length;
|
||||
import cn.qaiu.db.ddl.Table;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 演练场TypeScript代码实体
|
||||
* 用于保存用户编写的TypeScript源代码
|
||||
* 与PlaygroundParser关联,存储原始TS代码和编译后的ES5代码
|
||||
*/
|
||||
@Data
|
||||
@Table("playground_typescript_code")
|
||||
public class PlaygroundTypeScriptCode {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Constraint(autoIncrement = true, notNull = true)
|
||||
private Long id;
|
||||
|
||||
@Constraint(notNull = true)
|
||||
private Long parserId; // 关联的解析器ID(外键)
|
||||
|
||||
@Length(varcharSize = 65535)
|
||||
@Constraint(notNull = true)
|
||||
private String tsCode; // TypeScript原始代码
|
||||
|
||||
@Length(varcharSize = 65535)
|
||||
@Constraint(notNull = true)
|
||||
private String es5Code; // 编译后的ES5代码
|
||||
|
||||
@Length(varcharSize = 2000)
|
||||
private String compileErrors; // 编译错误信息(如果有)
|
||||
|
||||
@Length(varcharSize = 32)
|
||||
private String compilerVersion; // 编译器版本
|
||||
|
||||
@Length(varcharSize = 1000)
|
||||
private String compileOptions; // 编译选项(JSON格式)
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date createTime = new Date(); // 创建时间
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date updateTime; // 更新时间
|
||||
|
||||
private Boolean isValid = true; // 编译是否成功
|
||||
|
||||
@Length(varcharSize = 64)
|
||||
private String ip; // 创建者IP
|
||||
}
|
||||
@@ -50,19 +50,4 @@ public interface DbService extends BaseAsyncService {
|
||||
*/
|
||||
Future<JsonObject> getPlaygroundParserById(Long id);
|
||||
|
||||
/**
|
||||
* 保存TypeScript代码及其编译结果
|
||||
*/
|
||||
Future<JsonObject> saveTypeScriptCode(JsonObject tsCodeInfo);
|
||||
|
||||
/**
|
||||
* 根据parserId获取TypeScript代码
|
||||
*/
|
||||
Future<JsonObject> getTypeScriptCodeByParserId(Long parserId);
|
||||
|
||||
/**
|
||||
* 更新TypeScript代码
|
||||
*/
|
||||
Future<JsonObject> updateTypeScriptCode(Long parserId, JsonObject tsCodeInfo);
|
||||
|
||||
}
|
||||
|
||||
@@ -265,114 +265,4 @@ public class DbServiceImpl implements DbService {
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<JsonObject> saveTypeScriptCode(JsonObject tsCodeInfo) {
|
||||
JDBCPool client = JDBCPoolInit.instance().getPool();
|
||||
Promise<JsonObject> promise = Promise.promise();
|
||||
|
||||
String sql = """
|
||||
INSERT INTO playground_typescript_code
|
||||
(parser_id, ts_code, es5_code, compile_errors, compiler_version,
|
||||
compile_options, create_time, is_valid, ip)
|
||||
VALUES (?, ?, ?, ?, ?, ?, NOW(), ?, ?)
|
||||
""";
|
||||
|
||||
client.preparedQuery(sql)
|
||||
.execute(Tuple.of(
|
||||
tsCodeInfo.getLong("parserId"),
|
||||
tsCodeInfo.getString("tsCode"),
|
||||
tsCodeInfo.getString("es5Code"),
|
||||
tsCodeInfo.getString("compileErrors"),
|
||||
tsCodeInfo.getString("compilerVersion"),
|
||||
tsCodeInfo.getString("compileOptions"),
|
||||
tsCodeInfo.getBoolean("isValid", true),
|
||||
tsCodeInfo.getString("ip")
|
||||
))
|
||||
.onSuccess(res -> {
|
||||
promise.complete(JsonResult.success("保存TypeScript代码成功").toJsonObject());
|
||||
})
|
||||
.onFailure(e -> {
|
||||
log.error("saveTypeScriptCode failed", e);
|
||||
promise.fail(e);
|
||||
});
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<JsonObject> getTypeScriptCodeByParserId(Long parserId) {
|
||||
JDBCPool client = JDBCPoolInit.instance().getPool();
|
||||
Promise<JsonObject> promise = Promise.promise();
|
||||
|
||||
String sql = "SELECT * FROM playground_typescript_code WHERE parser_id = ? ORDER BY create_time DESC LIMIT 1";
|
||||
|
||||
client.preparedQuery(sql)
|
||||
.execute(Tuple.of(parserId))
|
||||
.onSuccess(rows -> {
|
||||
if (rows.size() > 0) {
|
||||
Row row = rows.iterator().next();
|
||||
JsonObject tsCode = new JsonObject();
|
||||
tsCode.put("id", row.getLong("id"));
|
||||
tsCode.put("parserId", row.getLong("parser_id"));
|
||||
tsCode.put("tsCode", row.getString("ts_code"));
|
||||
tsCode.put("es5Code", row.getString("es5_code"));
|
||||
tsCode.put("compileErrors", row.getString("compile_errors"));
|
||||
tsCode.put("compilerVersion", row.getString("compiler_version"));
|
||||
tsCode.put("compileOptions", row.getString("compile_options"));
|
||||
var createTime = row.getLocalDateTime("create_time");
|
||||
if (createTime != null) {
|
||||
tsCode.put("createTime", createTime.toString().replace("T", " "));
|
||||
}
|
||||
var updateTime = row.getLocalDateTime("update_time");
|
||||
if (updateTime != null) {
|
||||
tsCode.put("updateTime", updateTime.toString().replace("T", " "));
|
||||
}
|
||||
tsCode.put("isValid", row.getBoolean("is_valid"));
|
||||
tsCode.put("ip", row.getString("ip"));
|
||||
promise.complete(JsonResult.data(tsCode).toJsonObject());
|
||||
} else {
|
||||
promise.complete(JsonResult.data(null).toJsonObject());
|
||||
}
|
||||
})
|
||||
.onFailure(e -> {
|
||||
log.error("getTypeScriptCodeByParserId failed", e);
|
||||
promise.fail(e);
|
||||
});
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<JsonObject> updateTypeScriptCode(Long parserId, JsonObject tsCodeInfo) {
|
||||
JDBCPool client = JDBCPoolInit.instance().getPool();
|
||||
Promise<JsonObject> promise = Promise.promise();
|
||||
|
||||
String sql = """
|
||||
UPDATE playground_typescript_code
|
||||
SET ts_code = ?, es5_code = ?, compile_errors = ?, compiler_version = ?,
|
||||
compile_options = ?, update_time = NOW(), is_valid = ?
|
||||
WHERE parser_id = ?
|
||||
""";
|
||||
|
||||
client.preparedQuery(sql)
|
||||
.execute(Tuple.of(
|
||||
tsCodeInfo.getString("tsCode"),
|
||||
tsCodeInfo.getString("es5Code"),
|
||||
tsCodeInfo.getString("compileErrors"),
|
||||
tsCodeInfo.getString("compilerVersion"),
|
||||
tsCodeInfo.getString("compileOptions"),
|
||||
tsCodeInfo.getBoolean("isValid", true),
|
||||
parserId
|
||||
))
|
||||
.onSuccess(res -> {
|
||||
promise.complete(JsonResult.success("更新TypeScript代码成功").toJsonObject());
|
||||
})
|
||||
.onFailure(e -> {
|
||||
log.error("updateTypeScriptCode failed", e);
|
||||
promise.fail(e);
|
||||
});
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ proxyConf: server-proxy
|
||||
|
||||
# JS演练场配置
|
||||
playground:
|
||||
# 是否启用演练场,默认false不启用
|
||||
enabled: false
|
||||
# 公开模式,默认false需要密码访问,设为true则无需密码
|
||||
public: false
|
||||
# 访问密码,建议修改默认密码!
|
||||
|
||||
@@ -4,7 +4,7 @@ server-name: Vert.x-proxy-server(v4.1.2)
|
||||
proxy:
|
||||
- listen: 6401
|
||||
# 404的路径
|
||||
page404: webroot/err/404.html
|
||||
page404: webroot/nfd-front/index.html
|
||||
static:
|
||||
path: /
|
||||
add-headers:
|
||||
|
||||
91
web-service/src/test/resources/playground-dos-tests.http
Normal file
91
web-service/src/test/resources/playground-dos-tests.http
Normal 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)
|
||||
|
||||
Reference in New Issue
Block a user