mirror of
https://github.com/qaiu/netdisk-fast-download.git
synced 2026-01-13 18:04:13 +00:00
Compare commits
7 Commits
v0.1.9b13
...
copilot/up
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf93f0302a | ||
|
|
a004becc95 | ||
|
|
8c92150c81 | ||
|
|
f673a4914e | ||
|
|
367c7f35ec | ||
|
|
32beb4f2f2 | ||
|
|
442ae2c2af |
@@ -1,148 +0,0 @@
|
|||||||
# 脚本演练场功能测试报告
|
|
||||||
|
|
||||||
## 测试时间
|
|
||||||
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分支
|
|
||||||
|
|
||||||
服务已成功启动,可以进行浏览器环境下的完整功能测试。
|
|
||||||
|
|
||||||
@@ -1,275 +0,0 @@
|
|||||||
# Implementation Summary
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Successfully implemented the backend portion of a browser-based TypeScript compilation solution for the netdisk-fast-download project. This implementation provides standard `fetch` API and `Promise` polyfills for the ES5 JavaScript engine (Nashorn), enabling modern JavaScript patterns in a legacy execution environment.
|
|
||||||
|
|
||||||
## What Was Implemented
|
|
||||||
|
|
||||||
### 1. Promise Polyfill (ES5 Compatible)
|
|
||||||
|
|
||||||
**File:** `parser/src/main/resources/fetch-runtime.js`
|
|
||||||
|
|
||||||
A complete Promise/A+ implementation that runs in ES5 environments:
|
|
||||||
|
|
||||||
- ✅ `new Promise(executor)` constructor
|
|
||||||
- ✅ `promise.then(onFulfilled, onRejected)` with chaining
|
|
||||||
- ✅ `promise.catch(onRejected)` error handling
|
|
||||||
- ✅ `promise.finally(onFinally)` cleanup
|
|
||||||
- ✅ `Promise.resolve(value)` static method
|
|
||||||
- ✅ `Promise.reject(reason)` static method
|
|
||||||
- ✅ `Promise.all(promises)` parallel execution
|
|
||||||
- ✅ `Promise.race(promises)` with correct edge case handling
|
|
||||||
|
|
||||||
**Key Features:**
|
|
||||||
- Pure ES5 syntax (no ES6+ features)
|
|
||||||
- Uses `setTimeout(fn, 0)` for async execution
|
|
||||||
- Handles Promise chaining and nesting
|
|
||||||
- Proper error propagation
|
|
||||||
|
|
||||||
### 2. Fetch API Polyfill
|
|
||||||
|
|
||||||
**File:** `parser/src/main/resources/fetch-runtime.js`
|
|
||||||
|
|
||||||
Standard fetch API implementation that bridges to JsHttpClient:
|
|
||||||
|
|
||||||
- ✅ All HTTP methods: GET, POST, PUT, DELETE, PATCH, HEAD
|
|
||||||
- ✅ Request options: method, headers, body
|
|
||||||
- ✅ Response object with:
|
|
||||||
- `text()` - returns Promise<string>
|
|
||||||
- `json()` - returns Promise<object>
|
|
||||||
- `arrayBuffer()` - returns Promise<ArrayBuffer>
|
|
||||||
- `status` - HTTP status code
|
|
||||||
- `ok` - boolean (2xx = true)
|
|
||||||
- `statusText` - proper HTTP status text mapping
|
|
||||||
- `headers` - response headers access
|
|
||||||
|
|
||||||
**Standards Compliance:**
|
|
||||||
- Follows Fetch API specification
|
|
||||||
- Proper HTTP status text for common codes (200, 404, 500, etc.)
|
|
||||||
- Handles request/response conversion correctly
|
|
||||||
|
|
||||||
### 3. Java Bridge Layer
|
|
||||||
|
|
||||||
**File:** `parser/src/main/java/cn/qaiu/parser/customjs/JsFetchBridge.java`
|
|
||||||
|
|
||||||
Java class that connects fetch API calls to the existing JsHttpClient:
|
|
||||||
|
|
||||||
- ✅ Receives fetch options (method, headers, body)
|
|
||||||
- ✅ Converts to JsHttpClient calls
|
|
||||||
- ✅ Returns JsHttpResponse objects
|
|
||||||
- ✅ Inherits SSRF protection
|
|
||||||
- ✅ Supports proxy configuration
|
|
||||||
|
|
||||||
**Integration:**
|
|
||||||
- Seamless with existing infrastructure
|
|
||||||
- No breaking changes to current code
|
|
||||||
- Extends functionality without modification
|
|
||||||
|
|
||||||
### 4. Auto-Injection System
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- `parser/src/main/java/cn/qaiu/parser/customjs/JsParserExecutor.java`
|
|
||||||
- `parser/src/main/java/cn/qaiu/parser/customjs/JsPlaygroundExecutor.java`
|
|
||||||
|
|
||||||
Automatic injection of fetch runtime into JavaScript engines:
|
|
||||||
|
|
||||||
- ✅ Loads fetch-runtime.js on engine initialization
|
|
||||||
- ✅ Injects `JavaFetch` bridge object
|
|
||||||
- ✅ Lazy-loaded and cached for performance
|
|
||||||
- ✅ Works in both parser and playground contexts
|
|
||||||
|
|
||||||
**Benefits:**
|
|
||||||
- Zero configuration required
|
|
||||||
- Transparent to end users
|
|
||||||
- Coexists with existing `http` object
|
|
||||||
|
|
||||||
### 5. Documentation and Examples
|
|
||||||
|
|
||||||
**Documentation Files:**
|
|
||||||
- `parser/doc/TYPESCRIPT_ES5_IMPLEMENTATION.md` - Implementation overview
|
|
||||||
- `parser/doc/TYPESCRIPT_FETCH_GUIDE.md` - Detailed usage guide
|
|
||||||
|
|
||||||
**Example Files:**
|
|
||||||
- `parser/src/main/resources/custom-parsers/fetch-demo.js` - Working example
|
|
||||||
|
|
||||||
**Test Files:**
|
|
||||||
- `parser/src/test/java/cn/qaiu/parser/customjs/JsFetchBridgeTest.java` - Unit tests
|
|
||||||
|
|
||||||
## What Can Users Do Now
|
|
||||||
|
|
||||||
### Current Capabilities
|
|
||||||
|
|
||||||
Users can write ES5 JavaScript with modern async patterns:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
function parse(shareLinkInfo, http, logger) {
|
|
||||||
// Use Promise
|
|
||||||
var promise = new Promise(function(resolve, reject) {
|
|
||||||
resolve("data");
|
|
||||||
});
|
|
||||||
|
|
||||||
promise.then(function(data) {
|
|
||||||
logger.info("Got: " + data);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Use fetch
|
|
||||||
fetch("https://api.example.com/data")
|
|
||||||
.then(function(response) {
|
|
||||||
return response.json();
|
|
||||||
})
|
|
||||||
.then(function(data) {
|
|
||||||
logger.info("Downloaded: " + data.url);
|
|
||||||
})
|
|
||||||
.catch(function(error) {
|
|
||||||
logger.error("Error: " + error.message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Future Capabilities (with Frontend Implementation)
|
|
||||||
|
|
||||||
Once TypeScript compilation is added to the frontend:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
async function parse(
|
|
||||||
shareLinkInfo: ShareLinkInfo,
|
|
||||||
http: JsHttpClient,
|
|
||||||
logger: JsLogger
|
|
||||||
): Promise<string> {
|
|
||||||
try {
|
|
||||||
const response = await fetch("https://api.example.com/data");
|
|
||||||
const data = await response.json();
|
|
||||||
return data.url;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`Error: ${error.message}`);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The frontend would compile this to ES5, which would then execute using the fetch polyfill.
|
|
||||||
|
|
||||||
## What Remains To Be Done
|
|
||||||
|
|
||||||
### Frontend TypeScript Compilation (Not Implemented)
|
|
||||||
|
|
||||||
To complete the full solution, the frontend needs:
|
|
||||||
|
|
||||||
1. **Add TypeScript Compiler**
|
|
||||||
```bash
|
|
||||||
cd web-front
|
|
||||||
npm install typescript
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Create Compilation Utility**
|
|
||||||
```javascript
|
|
||||||
// web-front/src/utils/tsCompiler.js
|
|
||||||
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']
|
|
||||||
},
|
|
||||||
fileName
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Update Playground UI**
|
|
||||||
- Add language selector (JavaScript / TypeScript)
|
|
||||||
- Pre-compile TypeScript before sending to backend
|
|
||||||
- Display compilation errors
|
|
||||||
- Optionally show compiled ES5 code
|
|
||||||
|
|
||||||
## Technical Details
|
|
||||||
|
|
||||||
### Architecture
|
|
||||||
|
|
||||||
```
|
|
||||||
Browser Backend
|
|
||||||
-------- -------
|
|
||||||
TypeScript Code (future) -->
|
|
||||||
↓ tsc compile (future)
|
|
||||||
ES5 + fetch() calls --> Nashorn Engine
|
|
||||||
↓ fetch-runtime.js loaded
|
|
||||||
↓ JavaFetch injected
|
|
||||||
fetch() call
|
|
||||||
↓
|
|
||||||
JavaFetch bridge
|
|
||||||
↓
|
|
||||||
JsHttpClient
|
|
||||||
↓
|
|
||||||
Vert.x HTTP Client
|
|
||||||
```
|
|
||||||
|
|
||||||
### Performance
|
|
||||||
|
|
||||||
- **Fetch runtime caching:** Loaded once, cached in static variable
|
|
||||||
- **Promise async execution:** Non-blocking via setTimeout(0)
|
|
||||||
- **Worker thread pools:** Prevents blocking Event Loop
|
|
||||||
- **Lazy loading:** Only loads when needed
|
|
||||||
|
|
||||||
### Security
|
|
||||||
|
|
||||||
- ✅ **SSRF Protection:** Inherited from JsHttpClient
|
|
||||||
- Blocks internal IPs (127.0.0.1, 10.x.x.x, 192.168.x.x)
|
|
||||||
- Blocks cloud metadata APIs (169.254.169.254)
|
|
||||||
- DNS resolution checks
|
|
||||||
- ✅ **Sandbox Isolation:** SecurityClassFilter restricts class access
|
|
||||||
- ✅ **No New Vulnerabilities:** CodeQL scan clean (0 alerts)
|
|
||||||
|
|
||||||
### Testing
|
|
||||||
|
|
||||||
- ✅ All existing tests pass
|
|
||||||
- ✅ New unit tests for Promise and fetch
|
|
||||||
- ✅ Example parser demonstrates real-world usage
|
|
||||||
- ✅ Build succeeds without errors
|
|
||||||
|
|
||||||
## Files Changed
|
|
||||||
|
|
||||||
### New Files (8)
|
|
||||||
1. `parser/src/main/resources/fetch-runtime.js` - Promise & Fetch polyfill
|
|
||||||
2. `parser/src/main/java/cn/qaiu/parser/customjs/JsFetchBridge.java` - Java bridge
|
|
||||||
3. `parser/src/main/resources/custom-parsers/fetch-demo.js` - Example
|
|
||||||
4. `parser/src/test/java/cn/qaiu/parser/customjs/JsFetchBridgeTest.java` - Tests
|
|
||||||
5. `parser/doc/TYPESCRIPT_FETCH_GUIDE.md` - Usage guide
|
|
||||||
6. `parser/doc/TYPESCRIPT_ES5_IMPLEMENTATION.md` - Implementation guide
|
|
||||||
7. `parser/doc/TYPESCRIPT_ES5_IMPLEMENTATION_SUMMARY.md` - This file
|
|
||||||
8. `.gitignore` updates (if any)
|
|
||||||
|
|
||||||
### Modified Files (2)
|
|
||||||
1. `parser/src/main/java/cn/qaiu/parser/customjs/JsParserExecutor.java` - Auto-inject
|
|
||||||
2. `parser/src/main/java/cn/qaiu/parser/customjs/JsPlaygroundExecutor.java` - Auto-inject
|
|
||||||
|
|
||||||
## Benefits
|
|
||||||
|
|
||||||
### For Users
|
|
||||||
- ✅ Write modern JavaScript patterns in ES5 environment
|
|
||||||
- ✅ Use familiar fetch API instead of custom http object
|
|
||||||
- ✅ Better error handling with Promise.catch()
|
|
||||||
- ✅ Cleaner async code (no callbacks hell)
|
|
||||||
|
|
||||||
### For Maintainers
|
|
||||||
- ✅ No breaking changes to existing code
|
|
||||||
- ✅ Backward compatible (http object still works)
|
|
||||||
- ✅ Well documented and tested
|
|
||||||
- ✅ Clear upgrade path to TypeScript
|
|
||||||
|
|
||||||
### For the Project
|
|
||||||
- ✅ Modern JavaScript support without Node.js
|
|
||||||
- ✅ Standards-compliant APIs
|
|
||||||
- ✅ Better developer experience
|
|
||||||
- ✅ Future-proof architecture
|
|
||||||
|
|
||||||
## Conclusion
|
|
||||||
|
|
||||||
This implementation successfully delivers the backend infrastructure for browser-based TypeScript compilation. The fetch API and Promise polyfills are production-ready, well-tested, and secure. Users can immediately start using modern async patterns in their ES5 parsers.
|
|
||||||
|
|
||||||
The frontend TypeScript compilation component is well-documented and ready for implementation when resources become available. The architecture is sound, the code is clean, and the solution is backward compatible with existing parsers.
|
|
||||||
|
|
||||||
**Status:** ✅ Backend Complete | ⏳ Frontend Planned | 🎯 Ready for Review
|
|
||||||
@@ -1,166 +0,0 @@
|
|||||||
# Playground 密码保护功能
|
|
||||||
|
|
||||||
## 概述
|
|
||||||
|
|
||||||
JS解析器演练场现在支持密码保护功能,可以通过配置文件控制是否需要密码才能访问。
|
|
||||||
|
|
||||||
## 配置说明
|
|
||||||
|
|
||||||
在 `web-service/src/main/resources/app-dev.yml` 文件中添加以下配置:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# JS演练场配置
|
|
||||||
playground:
|
|
||||||
# 公开模式,默认false需要密码访问,设为true则无需密码
|
|
||||||
public: false
|
|
||||||
# 访问密码,建议修改默认密码!
|
|
||||||
password: 'nfd_playground_2024'
|
|
||||||
```
|
|
||||||
|
|
||||||
### 配置项说明
|
|
||||||
|
|
||||||
- `public`: 布尔值,默认为 `false`
|
|
||||||
- `false`: 需要输入密码才能访问演练场(推荐)
|
|
||||||
- `true`: 公开访问,无需密码
|
|
||||||
|
|
||||||
- `password`: 字符串,访问密码
|
|
||||||
- 默认密码:`nfd_playground_2024`
|
|
||||||
- **强烈建议在生产环境中修改为自定义密码!**
|
|
||||||
|
|
||||||
## 功能特点
|
|
||||||
|
|
||||||
### 1. 密码保护模式 (public: false)
|
|
||||||
|
|
||||||
当 `public` 设置为 `false` 时:
|
|
||||||
|
|
||||||
- 访问 `/playground` 页面时会显示密码输入界面
|
|
||||||
- 必须输入正确的密码才能使用演练场功能
|
|
||||||
- 密码验证通过后,会话保持登录状态
|
|
||||||
- 所有演练场相关的 API 接口都受到保护
|
|
||||||
|
|
||||||
### 2. 公开模式 (public: true)
|
|
||||||
|
|
||||||
当 `public` 设置为 `true` 时:
|
|
||||||
|
|
||||||
- 无需输入密码即可访问演练场
|
|
||||||
- 适用于内网环境或开发测试环境
|
|
||||||
|
|
||||||
### 3. 加载动画与进度条
|
|
||||||
|
|
||||||
页面加载过程会显示进度条,包括以下阶段:
|
|
||||||
|
|
||||||
1. 初始化Vue组件 (0-20%)
|
|
||||||
2. 加载配置和本地数据 (20-40%)
|
|
||||||
3. 准备TypeScript编译器 (40-50%)
|
|
||||||
4. 初始化Monaco Editor (50-80%)
|
|
||||||
5. 加载完成 (80-100%)
|
|
||||||
|
|
||||||
### 4. 移动端适配
|
|
||||||
|
|
||||||
- 桌面端:左右分栏布局,可拖拽调整宽度
|
|
||||||
- 移动端(屏幕宽度 ≤ 768px):自动切换为上下分栏布局,可拖拽调整高度
|
|
||||||
|
|
||||||
## 安全建议
|
|
||||||
|
|
||||||
⚠️ **重要安全提示:**
|
|
||||||
|
|
||||||
1. **修改默认密码**:在生产环境中,务必修改 `playground.password` 为自定义的强密码
|
|
||||||
2. **使用密码保护**:建议保持 `public: false`,避免未授权访问
|
|
||||||
3. **定期更换密码**:定期更换访问密码以提高安全性
|
|
||||||
4. **配置文件保护**:确保配置文件的访问权限受到保护
|
|
||||||
|
|
||||||
## 系统启动提示
|
|
||||||
|
|
||||||
当系统启动时,会在日志中显示当前配置:
|
|
||||||
|
|
||||||
```
|
|
||||||
INFO - Playground配置已加载: public=false, password=已设置
|
|
||||||
```
|
|
||||||
|
|
||||||
如果使用默认密码,会显示警告:
|
|
||||||
|
|
||||||
```
|
|
||||||
WARN - ⚠️ 警告:您正在使用默认密码,建议修改配置文件中的 playground.password 以确保安全!
|
|
||||||
```
|
|
||||||
|
|
||||||
## API 端点
|
|
||||||
|
|
||||||
### 1. 获取状态
|
|
||||||
|
|
||||||
```
|
|
||||||
GET /v2/playground/status
|
|
||||||
```
|
|
||||||
|
|
||||||
返回:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"data": {
|
|
||||||
"public": false,
|
|
||||||
"authed": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 登录
|
|
||||||
|
|
||||||
```
|
|
||||||
POST /v2/playground/login
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"password": "your_password"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
成功响应:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"msg": "登录成功",
|
|
||||||
"success": true
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
失败响应:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"code": 500,
|
|
||||||
"msg": "密码错误",
|
|
||||||
"success": false
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 常见问题
|
|
||||||
|
|
||||||
### Q: 如何禁用密码保护?
|
|
||||||
|
|
||||||
A: 在配置文件中设置 `playground.public: true`
|
|
||||||
|
|
||||||
### Q: 忘记密码怎么办?
|
|
||||||
|
|
||||||
A: 修改配置文件中的 `playground.password` 为新密码,然后重启服务
|
|
||||||
|
|
||||||
### Q: 密码是否加密存储?
|
|
||||||
|
|
||||||
A: 当前版本密码以明文形式存储在配置文件中,请确保配置文件的访问权限受到保护
|
|
||||||
|
|
||||||
### Q: Session 有效期多久?
|
|
||||||
|
|
||||||
A: Session 由 Vert.x 管理,默认在浏览器会话期间有效,关闭浏览器后失效
|
|
||||||
|
|
||||||
## 后续版本计划
|
|
||||||
|
|
||||||
未来版本可能会添加以下功能:
|
|
||||||
|
|
||||||
- [ ] 支持环境变量配置密码
|
|
||||||
- [ ] 支持加密存储密码
|
|
||||||
- [ ] 支持多用户账户系统
|
|
||||||
- [ ] 支持 Token 认证方式
|
|
||||||
- [ ] 支持 Session 超时配置
|
|
||||||
|
|
||||||
## 相关文档
|
|
||||||
|
|
||||||
- [Playground 使用指南](PLAYGROUND_GUIDE.md)
|
|
||||||
- [JavaScript 解析器开发指南](parser/doc/JAVASCRIPT_PARSER_GUIDE.md)
|
|
||||||
- [TypeScript 实现总结](TYPESCRIPT_IMPLEMENTATION_SUMMARY_CN.md)
|
|
||||||
13
README.md
13
README.md
@@ -40,7 +40,7 @@ https://nfd-parser.github.io/nfd-preview/preview.html?src=https%3A%2F%2Flz.qaiu.
|
|||||||
|
|
||||||
**JavaScript解析器文档:** [JavaScript解析器开发指南](parser/doc/JAVASCRIPT_PARSER_GUIDE.md) | [自定义解析器扩展指南](parser/doc/CUSTOM_PARSER_GUIDE.md) | [快速开始](parser/doc/CUSTOM_PARSER_QUICKSTART.md)
|
**JavaScript解析器文档:** [JavaScript解析器开发指南](parser/doc/JAVASCRIPT_PARSER_GUIDE.md) | [自定义解析器扩展指南](parser/doc/CUSTOM_PARSER_GUIDE.md) | [快速开始](parser/doc/CUSTOM_PARSER_QUICKSTART.md)
|
||||||
|
|
||||||
**Playground功能:** [JS解析器演练场密码保护说明](PLAYGROUND_PASSWORD_PROTECTION.md)
|
**Playground访问控制:** [配置指南](web-service/doc/PLAYGROUND_ACCESS_CONTROL.md) - 了解如何配置演练场的访问权限
|
||||||
|
|
||||||
## 预览地址
|
## 预览地址
|
||||||
[预览地址1](https://lz.qaiu.top)
|
[预览地址1](https://lz.qaiu.top)
|
||||||
@@ -61,7 +61,7 @@ main分支依赖JDK17, 提供了JDK11分支[main-jdk11](https://github.com/qaiu/
|
|||||||
|
|
||||||
- [蓝奏云-lz](https://pc.woozooo.com/)
|
- [蓝奏云-lz](https://pc.woozooo.com/)
|
||||||
- [蓝奏云优享-iz](https://www.ilanzou.com/)
|
- [蓝奏云优享-iz](https://www.ilanzou.com/)
|
||||||
- [奶牛快传-cow](https://cowtransfer.com/)
|
- ~[奶牛快传-cow(即将停服)](https://cowtransfer.com/)~
|
||||||
- [移动云云空间-ec](https://www.ecpan.cn/web)
|
- [移动云云空间-ec](https://www.ecpan.cn/web)
|
||||||
- [小飞机网盘-fj](https://www.feijipan.com/)
|
- [小飞机网盘-fj](https://www.feijipan.com/)
|
||||||
- [亿方云-fc](https://www.fangcloud.com/)
|
- [亿方云-fc](https://www.fangcloud.com/)
|
||||||
@@ -473,11 +473,10 @@ Core模块集成Vert.x实现类似spring的注解式路由API
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
||||||
### 关于赞助定制专属版
|
### 关于专属版
|
||||||
1. 专属版提供对小飞机,蓝奏优享大文件解析的支持, 提供天翼云盘/移动云盘/联通云盘的解析支持。
|
99元, 提供对小飞机,蓝奏优享大文件解析的支持, 提供天翼云盘,移动云盘,联通云盘的解析支持
|
||||||
2. 可提供托管服务:包含部署服务和云服务器环境。
|
199元, 包含部署服务, 需提供宝塔环境
|
||||||
3. 可提供功能定制开发。
|
可以提供功能定制开发, 添加以下任意一个联系方式详谈:
|
||||||
您可能需要提供一定的资金赞助支持定制专属版, 请添加以下任意一个联系方式详谈赞助模式:
|
|
||||||
<p>qq: 197575894</p>
|
<p>qq: 197575894</p>
|
||||||
<p>wechat: imcoding_</p>
|
<p>wechat: imcoding_</p>
|
||||||
|
|
||||||
|
|||||||
270
TESTING_GUIDE.md
Normal file
270
TESTING_GUIDE.md
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
# Playground Access Control - Testing Guide
|
||||||
|
|
||||||
|
## Quick Test Scenarios
|
||||||
|
|
||||||
|
### Scenario 1: Disabled Mode (Default)
|
||||||
|
**Configuration:**
|
||||||
|
```yaml
|
||||||
|
playground:
|
||||||
|
enabled: false
|
||||||
|
password: ""
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected Behavior:**
|
||||||
|
1. Navigate to `/playground`
|
||||||
|
2. Should see: "Playground未开启,请联系管理员在配置中启用此功能"
|
||||||
|
3. All API endpoints (`/v2/playground/*`) should return error
|
||||||
|
|
||||||
|
**API Test:**
|
||||||
|
```bash
|
||||||
|
curl http://localhost:6400/v2/playground/status
|
||||||
|
# Expected: {"code":200,"msg":"success","success":true,"data":{"enabled":false,"needPassword":false,"authed":false}}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Scenario 2: Password-Protected Mode
|
||||||
|
**Configuration:**
|
||||||
|
```yaml
|
||||||
|
playground:
|
||||||
|
enabled: true
|
||||||
|
password: "test123"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected Behavior:**
|
||||||
|
1. Navigate to `/playground`
|
||||||
|
2. Should see password input form with lock icon
|
||||||
|
3. Enter wrong password → Error message: "密码错误"
|
||||||
|
4. Enter correct password "test123" → Success, editor loads
|
||||||
|
5. Refresh page → Should remain authenticated
|
||||||
|
|
||||||
|
**API Tests:**
|
||||||
|
```bash
|
||||||
|
# Check status
|
||||||
|
curl http://localhost:6400/v2/playground/status
|
||||||
|
# Expected: {"enabled":true,"needPassword":true,"authed":false}
|
||||||
|
|
||||||
|
# Login with wrong password
|
||||||
|
curl -X POST http://localhost:6400/v2/playground/login \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"password":"wrong"}'
|
||||||
|
# Expected: {"code":500,"msg":"密码错误","success":false}
|
||||||
|
|
||||||
|
# Login with correct password
|
||||||
|
curl -X POST http://localhost:6400/v2/playground/login \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"password":"test123"}'
|
||||||
|
# Expected: {"code":200,"msg":"登录成功","success":true}
|
||||||
|
|
||||||
|
# Try to access without login (should fail)
|
||||||
|
curl http://localhost:6400/v2/playground/test \
|
||||||
|
-X POST \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"jsCode":"function parse(){return \"test\";}","shareUrl":"http://test.com"}'
|
||||||
|
# Expected: Error response
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Scenario 3: Public Access Mode
|
||||||
|
**Configuration:**
|
||||||
|
```yaml
|
||||||
|
playground:
|
||||||
|
enabled: true
|
||||||
|
password: ""
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected Behavior:**
|
||||||
|
1. Navigate to `/playground`
|
||||||
|
2. Should directly load the editor (no password prompt)
|
||||||
|
3. All features work immediately
|
||||||
|
|
||||||
|
**API Test:**
|
||||||
|
```bash
|
||||||
|
curl http://localhost:6400/v2/playground/status
|
||||||
|
# Expected: {"enabled":true,"needPassword":false,"authed":true}
|
||||||
|
```
|
||||||
|
|
||||||
|
⚠️ **Warning**: Only use this mode in localhost or secure internal network!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Full Feature Tests
|
||||||
|
|
||||||
|
### 1. Status Endpoint
|
||||||
|
```bash
|
||||||
|
curl http://localhost:6400/v2/playground/status
|
||||||
|
```
|
||||||
|
|
||||||
|
Should return JSON with:
|
||||||
|
- `enabled`: boolean
|
||||||
|
- `needPassword`: boolean
|
||||||
|
- `authed`: boolean
|
||||||
|
|
||||||
|
### 2. Login Endpoint (when password is set)
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:6400/v2/playground/login \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"password":"YOUR_PASSWORD"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Test Script Execution (after authentication)
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:6400/v2/playground/test \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"jsCode": "function parse(shareLinkInfo, http, logger) { return \"http://example.com/file.zip\"; }",
|
||||||
|
"shareUrl": "https://example.com/share/123",
|
||||||
|
"pwd": "",
|
||||||
|
"method": "parse"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Get Types Definition
|
||||||
|
```bash
|
||||||
|
curl http://localhost:6400/v2/playground/types.js
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Parser Management (after authentication)
|
||||||
|
```bash
|
||||||
|
# List parsers
|
||||||
|
curl http://localhost:6400/v2/playground/parsers
|
||||||
|
|
||||||
|
# Get parser by ID
|
||||||
|
curl http://localhost:6400/v2/playground/parsers/1
|
||||||
|
|
||||||
|
# Delete parser
|
||||||
|
curl -X DELETE http://localhost:6400/v2/playground/parsers/1
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## UI Testing Checklist
|
||||||
|
|
||||||
|
### When Disabled
|
||||||
|
- [ ] Page shows "Playground未开启" message
|
||||||
|
- [ ] No editor visible
|
||||||
|
- [ ] Clean, centered layout
|
||||||
|
|
||||||
|
### When Password Protected (Not Authenticated)
|
||||||
|
- [ ] Password input form visible
|
||||||
|
- [ ] Lock icon displayed
|
||||||
|
- [ ] Can toggle password visibility
|
||||||
|
- [ ] Enter key submits form
|
||||||
|
- [ ] Error message shows for wrong password
|
||||||
|
- [ ] Success message and editor loads on correct password
|
||||||
|
|
||||||
|
### When Password Protected (Authenticated)
|
||||||
|
- [ ] Editor loads immediately on page refresh
|
||||||
|
- [ ] All features work (run, save, format, etc.)
|
||||||
|
- [ ] Can execute tests
|
||||||
|
- [ ] Can save/load parsers
|
||||||
|
|
||||||
|
### When Public Access
|
||||||
|
- [ ] Editor loads immediately
|
||||||
|
- [ ] All features work without authentication
|
||||||
|
- [ ] No password prompt visible
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration Examples
|
||||||
|
|
||||||
|
### Production (Recommended)
|
||||||
|
```yaml
|
||||||
|
playground:
|
||||||
|
enabled: false
|
||||||
|
password: ""
|
||||||
|
```
|
||||||
|
|
||||||
|
### Development Team (Public Network)
|
||||||
|
```yaml
|
||||||
|
playground:
|
||||||
|
enabled: true
|
||||||
|
password: "SecureP@ssw0rd2024!"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Local Development
|
||||||
|
```yaml
|
||||||
|
playground:
|
||||||
|
enabled: true
|
||||||
|
password: ""
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Issues
|
||||||
|
|
||||||
|
### Issue: "Failed to extract session ID from cookie"
|
||||||
|
**Cause**: Cookie parsing error
|
||||||
|
**Solution**: This is logged as a warning and falls back to IP-based identification
|
||||||
|
|
||||||
|
### Issue: Editor doesn't load after correct password
|
||||||
|
**Cause**: Frontend state not updated
|
||||||
|
**Solution**: Check browser console for errors, ensure initPlayground() is called
|
||||||
|
|
||||||
|
### Issue: Authentication lost on page refresh
|
||||||
|
**Cause**: Server restarted (in-memory session storage)
|
||||||
|
**Solution**: Expected behavior - re-enter password after server restart
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security Verification
|
||||||
|
|
||||||
|
### 1. Default Security
|
||||||
|
- [ ] Default config has `enabled: false`
|
||||||
|
- [ ] Cannot access playground without enabling
|
||||||
|
- [ ] No unintended API exposure
|
||||||
|
|
||||||
|
### 2. Password Protection
|
||||||
|
- [ ] Wrong password rejected
|
||||||
|
- [ ] Session persists across requests
|
||||||
|
- [ ] Different clients have independent sessions
|
||||||
|
|
||||||
|
### 3. API Protection
|
||||||
|
- [ ] All playground endpoints check authentication
|
||||||
|
- [ ] Status endpoint accessible without auth (returns state only)
|
||||||
|
- [ ] Login endpoint accessible without auth (for authentication)
|
||||||
|
- [ ] All other endpoints require authentication when password is set
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Performance Testing
|
||||||
|
|
||||||
|
### Load Test
|
||||||
|
```bash
|
||||||
|
# Test status endpoint
|
||||||
|
ab -n 1000 -c 10 http://localhost:6400/v2/playground/status
|
||||||
|
```
|
||||||
|
|
||||||
|
### Session Management Test
|
||||||
|
```bash
|
||||||
|
# Create multiple concurrent sessions
|
||||||
|
for i in {1..10}; do
|
||||||
|
curl -X POST http://localhost:6400/v2/playground/login \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"password":"test123"}' &
|
||||||
|
done
|
||||||
|
wait
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cleanup
|
||||||
|
|
||||||
|
After testing, remember to:
|
||||||
|
1. Set `enabled: false` in production
|
||||||
|
2. Use strong passwords if enabling in public networks
|
||||||
|
3. Monitor access logs
|
||||||
|
4. Regularly review created parsers
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Documentation References
|
||||||
|
|
||||||
|
- Full documentation: `web-service/doc/PLAYGROUND_ACCESS_CONTROL.md`
|
||||||
|
- Main README: `README.md` (Playground Access Control section)
|
||||||
|
- Configuration file: `web-service/src/main/resources/app-dev.yml`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Last Updated: 2025-12-07
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
package cn.qaiu.parser.customjs;
|
|
||||||
|
|
||||||
import cn.qaiu.parser.customjs.JsHttpClient.JsHttpResponse;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JavaScript Fetch API桥接类
|
|
||||||
* 将标准的fetch API调用桥接到现有的JsHttpClient实现
|
|
||||||
*
|
|
||||||
* @author <a href="https://qaiu.top">QAIU</a>
|
|
||||||
* Create at 2025/12/06
|
|
||||||
*/
|
|
||||||
public class JsFetchBridge {
|
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(JsFetchBridge.class);
|
|
||||||
|
|
||||||
private final JsHttpClient httpClient;
|
|
||||||
|
|
||||||
public JsFetchBridge(JsHttpClient httpClient) {
|
|
||||||
this.httpClient = httpClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch API实现
|
|
||||||
* 接收fetch API调用并转换为JsHttpClient调用
|
|
||||||
*
|
|
||||||
* @param url 请求URL
|
|
||||||
* @param options 请求选项(包含method、headers、body等)
|
|
||||||
* @return JsHttpResponse响应对象
|
|
||||||
*/
|
|
||||||
public JsHttpResponse fetch(String url, Map<String, Object> options) {
|
|
||||||
try {
|
|
||||||
// 解析请求方法
|
|
||||||
String method = "GET";
|
|
||||||
if (options != null && options.containsKey("method")) {
|
|
||||||
method = options.get("method").toString().toUpperCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析并设置请求头
|
|
||||||
if (options != null && options.containsKey("headers")) {
|
|
||||||
Object headersObj = options.get("headers");
|
|
||||||
if (headersObj instanceof Map) {
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
Map<String, Object> headersMap = (Map<String, Object>) headersObj;
|
|
||||||
for (Map.Entry<String, Object> entry : headersMap.entrySet()) {
|
|
||||||
if (entry.getValue() != null) {
|
|
||||||
httpClient.putHeader(entry.getKey(), entry.getValue().toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析请求体
|
|
||||||
Object body = null;
|
|
||||||
if (options != null && options.containsKey("body")) {
|
|
||||||
body = options.get("body");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据方法执行请求
|
|
||||||
JsHttpResponse response;
|
|
||||||
switch (method) {
|
|
||||||
case "GET":
|
|
||||||
response = httpClient.get(url);
|
|
||||||
break;
|
|
||||||
case "POST":
|
|
||||||
response = httpClient.post(url, body);
|
|
||||||
break;
|
|
||||||
case "PUT":
|
|
||||||
response = httpClient.put(url, body);
|
|
||||||
break;
|
|
||||||
case "DELETE":
|
|
||||||
response = httpClient.delete(url);
|
|
||||||
break;
|
|
||||||
case "PATCH":
|
|
||||||
response = httpClient.patch(url, body);
|
|
||||||
break;
|
|
||||||
case "HEAD":
|
|
||||||
response = httpClient.getNoRedirect(url);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new IllegalArgumentException("Unsupported HTTP method: " + method);
|
|
||||||
}
|
|
||||||
|
|
||||||
log.debug("Fetch请求完成: {} {} - 状态码: {}", method, url, response.statusCode());
|
|
||||||
return response;
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Fetch请求失败: {} - {}", url, e.getMessage());
|
|
||||||
throw new RuntimeException("Fetch请求失败: " + e.getMessage(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -14,13 +14,8 @@ import org.slf4j.Logger;
|
|||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import javax.script.ScriptEngine;
|
import javax.script.ScriptEngine;
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JavaScript解析器执行器
|
* JavaScript解析器执行器
|
||||||
@@ -35,19 +30,17 @@ public class JsParserExecutor implements IPanTool {
|
|||||||
|
|
||||||
private static final WorkerExecutor EXECUTOR = WebClientVertxInit.get().createSharedWorkerExecutor("parser-executor", 32);
|
private static final WorkerExecutor EXECUTOR = WebClientVertxInit.get().createSharedWorkerExecutor("parser-executor", 32);
|
||||||
|
|
||||||
private static String FETCH_RUNTIME_JS = null;
|
|
||||||
|
|
||||||
private final CustomParserConfig config;
|
private final CustomParserConfig config;
|
||||||
private final ShareLinkInfo shareLinkInfo;
|
private final ShareLinkInfo shareLinkInfo;
|
||||||
private final ScriptEngine engine;
|
private final ScriptEngine engine;
|
||||||
private final JsHttpClient httpClient;
|
private final JsHttpClient httpClient;
|
||||||
private final JsLogger jsLogger;
|
private final JsLogger jsLogger;
|
||||||
private final JsShareLinkInfoWrapper shareLinkInfoWrapper;
|
private final JsShareLinkInfoWrapper shareLinkInfoWrapper;
|
||||||
private final JsFetchBridge fetchBridge;
|
|
||||||
|
|
||||||
public JsParserExecutor(ShareLinkInfo shareLinkInfo, CustomParserConfig config) {
|
public JsParserExecutor(ShareLinkInfo shareLinkInfo, CustomParserConfig config) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.shareLinkInfo = shareLinkInfo;
|
this.shareLinkInfo = shareLinkInfo;
|
||||||
|
this.engine = initEngine();
|
||||||
|
|
||||||
// 检查是否有代理配置
|
// 检查是否有代理配置
|
||||||
JsonObject proxyConfig = null;
|
JsonObject proxyConfig = null;
|
||||||
@@ -58,34 +51,6 @@ public class JsParserExecutor implements IPanTool {
|
|||||||
this.httpClient = new JsHttpClient(proxyConfig);
|
this.httpClient = new JsHttpClient(proxyConfig);
|
||||||
this.jsLogger = new JsLogger("JsParser-" + config.getType());
|
this.jsLogger = new JsLogger("JsParser-" + config.getType());
|
||||||
this.shareLinkInfoWrapper = new JsShareLinkInfoWrapper(shareLinkInfo);
|
this.shareLinkInfoWrapper = new JsShareLinkInfoWrapper(shareLinkInfo);
|
||||||
this.fetchBridge = new JsFetchBridge(httpClient);
|
|
||||||
this.engine = initEngine();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 加载fetch运行时JS代码
|
|
||||||
* @return fetch运行时代码
|
|
||||||
*/
|
|
||||||
static String loadFetchRuntime() {
|
|
||||||
if (FETCH_RUNTIME_JS != null) {
|
|
||||||
return FETCH_RUNTIME_JS;
|
|
||||||
}
|
|
||||||
|
|
||||||
try (InputStream is = JsParserExecutor.class.getClassLoader().getResourceAsStream("fetch-runtime.js")) {
|
|
||||||
if (is == null) {
|
|
||||||
log.warn("未找到fetch-runtime.js文件,fetch API将不可用");
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
|
|
||||||
FETCH_RUNTIME_JS = reader.lines().collect(Collectors.joining("\n"));
|
|
||||||
log.debug("Fetch运行时加载成功,大小: {} 字符", FETCH_RUNTIME_JS.length());
|
|
||||||
return FETCH_RUNTIME_JS;
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("加载fetch-runtime.js失败", e);
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -116,7 +81,6 @@ public class JsParserExecutor implements IPanTool {
|
|||||||
engine.put("http", httpClient);
|
engine.put("http", httpClient);
|
||||||
engine.put("logger", jsLogger);
|
engine.put("logger", jsLogger);
|
||||||
engine.put("shareLinkInfo", shareLinkInfoWrapper);
|
engine.put("shareLinkInfo", shareLinkInfoWrapper);
|
||||||
engine.put("JavaFetch", fetchBridge);
|
|
||||||
|
|
||||||
// 禁用Java对象访问
|
// 禁用Java对象访问
|
||||||
engine.eval("var Java = undefined;");
|
engine.eval("var Java = undefined;");
|
||||||
@@ -126,13 +90,6 @@ public class JsParserExecutor implements IPanTool {
|
|||||||
engine.eval("var org = undefined;");
|
engine.eval("var org = undefined;");
|
||||||
engine.eval("var com = undefined;");
|
engine.eval("var com = undefined;");
|
||||||
|
|
||||||
// 加载fetch运行时(Promise和fetch API polyfill)
|
|
||||||
String fetchRuntime = loadFetchRuntime();
|
|
||||||
if (!fetchRuntime.isEmpty()) {
|
|
||||||
engine.eval(fetchRuntime);
|
|
||||||
log.debug("✅ Fetch API和Promise polyfill注入成功");
|
|
||||||
}
|
|
||||||
|
|
||||||
log.debug("🔒 安全的JavaScript引擎初始化成功,解析器类型: {}", config.getType());
|
log.debug("🔒 安全的JavaScript引擎初始化成功,解析器类型: {}", config.getType());
|
||||||
|
|
||||||
// 执行JavaScript代码
|
// 执行JavaScript代码
|
||||||
|
|||||||
@@ -36,21 +36,12 @@ public class JsPlaygroundExecutor {
|
|||||||
return thread;
|
return thread;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 超时调度线程池,用于处理超时中断
|
|
||||||
private static final ScheduledExecutorService TIMEOUT_SCHEDULER = Executors.newScheduledThreadPool(2, r -> {
|
|
||||||
Thread thread = new Thread(r);
|
|
||||||
thread.setName("playground-timeout-scheduler-" + System.currentTimeMillis());
|
|
||||||
thread.setDaemon(true);
|
|
||||||
return thread;
|
|
||||||
});
|
|
||||||
|
|
||||||
private final ShareLinkInfo shareLinkInfo;
|
private final ShareLinkInfo shareLinkInfo;
|
||||||
private final String jsCode;
|
private final String jsCode;
|
||||||
private final ScriptEngine engine;
|
private final ScriptEngine engine;
|
||||||
private final JsHttpClient httpClient;
|
private final JsHttpClient httpClient;
|
||||||
private final JsPlaygroundLogger playgroundLogger;
|
private final JsPlaygroundLogger playgroundLogger;
|
||||||
private final JsShareLinkInfoWrapper shareLinkInfoWrapper;
|
private final JsShareLinkInfoWrapper shareLinkInfoWrapper;
|
||||||
private final JsFetchBridge fetchBridge;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建演练场执行器
|
* 创建演练场执行器
|
||||||
@@ -71,7 +62,6 @@ public class JsPlaygroundExecutor {
|
|||||||
this.httpClient = new JsHttpClient(proxyConfig);
|
this.httpClient = new JsHttpClient(proxyConfig);
|
||||||
this.playgroundLogger = new JsPlaygroundLogger();
|
this.playgroundLogger = new JsPlaygroundLogger();
|
||||||
this.shareLinkInfoWrapper = new JsShareLinkInfoWrapper(shareLinkInfo);
|
this.shareLinkInfoWrapper = new JsShareLinkInfoWrapper(shareLinkInfo);
|
||||||
this.fetchBridge = new JsFetchBridge(httpClient);
|
|
||||||
this.engine = initEngine();
|
this.engine = initEngine();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +84,6 @@ public class JsPlaygroundExecutor {
|
|||||||
engine.put("http", httpClient);
|
engine.put("http", httpClient);
|
||||||
engine.put("logger", playgroundLogger);
|
engine.put("logger", playgroundLogger);
|
||||||
engine.put("shareLinkInfo", shareLinkInfoWrapper);
|
engine.put("shareLinkInfo", shareLinkInfoWrapper);
|
||||||
engine.put("JavaFetch", fetchBridge);
|
|
||||||
|
|
||||||
// 禁用Java对象访问
|
// 禁用Java对象访问
|
||||||
engine.eval("var Java = undefined;");
|
engine.eval("var Java = undefined;");
|
||||||
@@ -104,13 +93,6 @@ public class JsPlaygroundExecutor {
|
|||||||
engine.eval("var org = undefined;");
|
engine.eval("var org = undefined;");
|
||||||
engine.eval("var com = undefined;");
|
engine.eval("var com = undefined;");
|
||||||
|
|
||||||
// 加载fetch运行时(Promise和fetch API polyfill)
|
|
||||||
String fetchRuntime = JsParserExecutor.loadFetchRuntime();
|
|
||||||
if (!fetchRuntime.isEmpty()) {
|
|
||||||
engine.eval(fetchRuntime);
|
|
||||||
playgroundLogger.infoJava("✅ Fetch API和Promise polyfill注入成功");
|
|
||||||
}
|
|
||||||
|
|
||||||
playgroundLogger.infoJava("🔒 安全的JavaScript引擎初始化成功(演练场)");
|
playgroundLogger.infoJava("🔒 安全的JavaScript引擎初始化成功(演练场)");
|
||||||
|
|
||||||
// 执行JavaScript代码
|
// 执行JavaScript代码
|
||||||
@@ -169,23 +151,12 @@ public class JsPlaygroundExecutor {
|
|||||||
}
|
}
|
||||||
}, INDEPENDENT_EXECUTOR);
|
}, INDEPENDENT_EXECUTOR);
|
||||||
|
|
||||||
// 创建超时任务,强制取消执行
|
// 添加超时处理
|
||||||
ScheduledFuture<?> timeoutTask = TIMEOUT_SCHEDULER.schedule(() -> {
|
executionFuture.orTimeout(EXECUTION_TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
||||||
if (!executionFuture.isDone()) {
|
.whenComplete((result, error) -> {
|
||||||
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 != null) {
|
||||||
if (error instanceof CancellationException) {
|
if (error instanceof TimeoutException) {
|
||||||
String timeoutMsg = "JavaScript执行超时(超过" + EXECUTION_TIMEOUT_SECONDS + "秒),已强制中断";
|
String timeoutMsg = "JavaScript执行超时(超过" + EXECUTION_TIMEOUT_SECONDS + "秒),可能存在无限循环";
|
||||||
playgroundLogger.errorJava(timeoutMsg);
|
playgroundLogger.errorJava(timeoutMsg);
|
||||||
log.error(timeoutMsg);
|
log.error(timeoutMsg);
|
||||||
promise.fail(new RuntimeException(timeoutMsg));
|
promise.fail(new RuntimeException(timeoutMsg));
|
||||||
@@ -244,23 +215,12 @@ public class JsPlaygroundExecutor {
|
|||||||
}
|
}
|
||||||
}, INDEPENDENT_EXECUTOR);
|
}, INDEPENDENT_EXECUTOR);
|
||||||
|
|
||||||
// 创建超时任务,强制取消执行
|
// 添加超时处理
|
||||||
ScheduledFuture<?> timeoutTask = TIMEOUT_SCHEDULER.schedule(() -> {
|
executionFuture.orTimeout(EXECUTION_TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
||||||
if (!executionFuture.isDone()) {
|
.whenComplete((result, error) -> {
|
||||||
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 != null) {
|
||||||
if (error instanceof CancellationException) {
|
if (error instanceof TimeoutException) {
|
||||||
String timeoutMsg = "JavaScript执行超时(超过" + EXECUTION_TIMEOUT_SECONDS + "秒),已强制中断";
|
String timeoutMsg = "JavaScript执行超时(超过" + EXECUTION_TIMEOUT_SECONDS + "秒),可能存在无限循环";
|
||||||
playgroundLogger.errorJava(timeoutMsg);
|
playgroundLogger.errorJava(timeoutMsg);
|
||||||
log.error(timeoutMsg);
|
log.error(timeoutMsg);
|
||||||
promise.fail(new RuntimeException(timeoutMsg));
|
promise.fail(new RuntimeException(timeoutMsg));
|
||||||
@@ -318,23 +278,12 @@ public class JsPlaygroundExecutor {
|
|||||||
}
|
}
|
||||||
}, INDEPENDENT_EXECUTOR);
|
}, INDEPENDENT_EXECUTOR);
|
||||||
|
|
||||||
// 创建超时任务,强制取消执行
|
// 添加超时处理
|
||||||
ScheduledFuture<?> timeoutTask = TIMEOUT_SCHEDULER.schedule(() -> {
|
executionFuture.orTimeout(EXECUTION_TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
||||||
if (!executionFuture.isDone()) {
|
.whenComplete((result, error) -> {
|
||||||
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 != null) {
|
||||||
if (error instanceof CancellationException) {
|
if (error instanceof TimeoutException) {
|
||||||
String timeoutMsg = "JavaScript执行超时(超过" + EXECUTION_TIMEOUT_SECONDS + "秒),已强制中断";
|
String timeoutMsg = "JavaScript执行超时(超过" + EXECUTION_TIMEOUT_SECONDS + "秒),可能存在无限循环";
|
||||||
playgroundLogger.errorJava(timeoutMsg);
|
playgroundLogger.errorJava(timeoutMsg);
|
||||||
log.error(timeoutMsg);
|
log.error(timeoutMsg);
|
||||||
promise.fail(new RuntimeException(timeoutMsg));
|
promise.fail(new RuntimeException(timeoutMsg));
|
||||||
|
|||||||
@@ -1,105 +0,0 @@
|
|||||||
// ==UserScript==
|
|
||||||
// @name Fetch API示例解析器
|
|
||||||
// @type fetch_demo
|
|
||||||
// @displayName Fetch演示
|
|
||||||
// @description 演示如何在ES5环境中使用fetch API和async/await
|
|
||||||
// @match https?://example\.com/s/(?<KEY>\w+)
|
|
||||||
// @author QAIU
|
|
||||||
// @version 1.0.0
|
|
||||||
// ==/UserScript==
|
|
||||||
|
|
||||||
// 使用require导入类型定义(仅用于IDE类型提示)
|
|
||||||
var types = require('./types');
|
|
||||||
/** @typedef {types.ShareLinkInfo} ShareLinkInfo */
|
|
||||||
/** @typedef {types.JsHttpClient} JsHttpClient */
|
|
||||||
/** @typedef {types.JsLogger} JsLogger */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 演示使用fetch API的解析器
|
|
||||||
* 注意:虽然源码中使用了ES6+语法(async/await),但在浏览器中会被编译为ES5
|
|
||||||
*
|
|
||||||
* @param {ShareLinkInfo} shareLinkInfo - 分享链接信息
|
|
||||||
* @param {JsHttpClient} http - HTTP客户端(传统方式)
|
|
||||||
* @param {JsLogger} logger - 日志对象
|
|
||||||
* @returns {string} 下载链接
|
|
||||||
*/
|
|
||||||
function parse(shareLinkInfo, http, logger) {
|
|
||||||
logger.info("=== Fetch API Demo ===");
|
|
||||||
|
|
||||||
// 方式1:使用传统的http对象(同步)
|
|
||||||
logger.info("方式1: 使用传统http对象");
|
|
||||||
var response1 = http.get("https://httpbin.org/get");
|
|
||||||
logger.info("状态码: " + response1.statusCode());
|
|
||||||
|
|
||||||
// 方式2:使用fetch API(基于Promise)
|
|
||||||
logger.info("方式2: 使用fetch API");
|
|
||||||
|
|
||||||
// 注意:在ES5环境中,我们需要手动处理Promise
|
|
||||||
// 这个示例展示了如何在ES5中使用fetch
|
|
||||||
var fetchPromise = fetch("https://httpbin.org/get");
|
|
||||||
|
|
||||||
// 等待Promise完成(同步等待模拟)
|
|
||||||
var result = null;
|
|
||||||
var error = null;
|
|
||||||
|
|
||||||
fetchPromise
|
|
||||||
.then(function(response) {
|
|
||||||
logger.info("Fetch响应状态: " + response.status);
|
|
||||||
return response.text();
|
|
||||||
})
|
|
||||||
.then(function(text) {
|
|
||||||
logger.info("Fetch响应内容: " + text.substring(0, 100) + "...");
|
|
||||||
result = "https://example.com/download/demo.file";
|
|
||||||
})
|
|
||||||
['catch'](function(err) {
|
|
||||||
logger.error("Fetch失败: " + err.message);
|
|
||||||
error = err;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 简单的等待循环(实际场景中不推荐,这里仅作演示)
|
|
||||||
var timeout = 5000; // 5秒超时
|
|
||||||
var start = Date.now();
|
|
||||||
while (result === null && error === null && (Date.now() - start) < timeout) {
|
|
||||||
// 等待Promise完成
|
|
||||||
java.lang.Thread.sleep(10);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error !== null) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result === null) {
|
|
||||||
throw new Error("Fetch超时");
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 演示POST请求
|
|
||||||
*/
|
|
||||||
function demonstratePost(logger) {
|
|
||||||
logger.info("=== 演示POST请求 ===");
|
|
||||||
|
|
||||||
var postPromise = fetch("https://httpbin.org/post", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
key: "value",
|
|
||||||
demo: true
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
postPromise
|
|
||||||
.then(function(response) {
|
|
||||||
return response.json();
|
|
||||||
})
|
|
||||||
.then(function(data) {
|
|
||||||
logger.info("POST响应: " + JSON.stringify(data));
|
|
||||||
})
|
|
||||||
['catch'](function(err) {
|
|
||||||
logger.error("POST失败: " + err.message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,329 +0,0 @@
|
|||||||
// ==FetchRuntime==
|
|
||||||
// @name Fetch API Polyfill for ES5
|
|
||||||
// @description Fetch API and Promise implementation for ES5 JavaScript engines
|
|
||||||
// @version 1.0.0
|
|
||||||
// @author QAIU
|
|
||||||
// ==============
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Simple Promise implementation compatible with ES5
|
|
||||||
* Supports basic Promise functionality needed for fetch API
|
|
||||||
*/
|
|
||||||
function SimplePromise(executor) {
|
|
||||||
var state = 'pending';
|
|
||||||
var value;
|
|
||||||
var handlers = [];
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
function resolve(result) {
|
|
||||||
if (state !== 'pending') return;
|
|
||||||
state = 'fulfilled';
|
|
||||||
value = result;
|
|
||||||
handlers.forEach(handle);
|
|
||||||
handlers = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
function reject(err) {
|
|
||||||
if (state !== 'pending') return;
|
|
||||||
state = 'rejected';
|
|
||||||
value = err;
|
|
||||||
handlers.forEach(handle);
|
|
||||||
handlers = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
function handle(handler) {
|
|
||||||
if (state === 'pending') {
|
|
||||||
handlers.push(handler);
|
|
||||||
} else {
|
|
||||||
setTimeout(function() {
|
|
||||||
if (state === 'fulfilled' && typeof handler.onFulfilled === 'function') {
|
|
||||||
try {
|
|
||||||
var result = handler.onFulfilled(value);
|
|
||||||
if (result && typeof result.then === 'function') {
|
|
||||||
result.then(handler.resolve, handler.reject);
|
|
||||||
} else {
|
|
||||||
handler.resolve(result);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
handler.reject(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (state === 'rejected' && typeof handler.onRejected === 'function') {
|
|
||||||
try {
|
|
||||||
var result = handler.onRejected(value);
|
|
||||||
if (result && typeof result.then === 'function') {
|
|
||||||
result.then(handler.resolve, handler.reject);
|
|
||||||
} else {
|
|
||||||
handler.resolve(result);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
handler.reject(e);
|
|
||||||
}
|
|
||||||
} else if (state === 'rejected' && !handler.onRejected) {
|
|
||||||
handler.reject(value);
|
|
||||||
}
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.then = function(onFulfilled, onRejected) {
|
|
||||||
return new SimplePromise(function(resolveNext, rejectNext) {
|
|
||||||
handle({
|
|
||||||
onFulfilled: onFulfilled,
|
|
||||||
onRejected: onRejected,
|
|
||||||
resolve: resolveNext,
|
|
||||||
reject: rejectNext
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
this['catch'] = function(onRejected) {
|
|
||||||
return this.then(null, onRejected);
|
|
||||||
};
|
|
||||||
|
|
||||||
this['finally'] = function(onFinally) {
|
|
||||||
return this.then(
|
|
||||||
function(value) {
|
|
||||||
return SimplePromise.resolve(onFinally()).then(function() {
|
|
||||||
return value;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
function(reason) {
|
|
||||||
return SimplePromise.resolve(onFinally()).then(function() {
|
|
||||||
throw reason;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
executor(resolve, reject);
|
|
||||||
} catch (e) {
|
|
||||||
reject(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Static methods
|
|
||||||
SimplePromise.resolve = function(value) {
|
|
||||||
if (value && typeof value.then === 'function') {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
return new SimplePromise(function(resolve) {
|
|
||||||
resolve(value);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
SimplePromise.reject = function(reason) {
|
|
||||||
return new SimplePromise(function(resolve, reject) {
|
|
||||||
reject(reason);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
SimplePromise.all = function(promises) {
|
|
||||||
return new SimplePromise(function(resolve, reject) {
|
|
||||||
var results = [];
|
|
||||||
var remaining = promises.length;
|
|
||||||
|
|
||||||
if (remaining === 0) {
|
|
||||||
resolve(results);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleResult(index, value) {
|
|
||||||
results[index] = value;
|
|
||||||
remaining--;
|
|
||||||
if (remaining === 0) {
|
|
||||||
resolve(results);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var i = 0; i < promises.length; i++) {
|
|
||||||
(function(index) {
|
|
||||||
var promise = promises[index];
|
|
||||||
if (promise && typeof promise.then === 'function') {
|
|
||||||
promise.then(
|
|
||||||
function(value) { handleResult(index, value); },
|
|
||||||
reject
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
handleResult(index, promise);
|
|
||||||
}
|
|
||||||
})(i);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
SimplePromise.race = function(promises) {
|
|
||||||
return new SimplePromise(function(resolve, reject) {
|
|
||||||
if (promises.length === 0) {
|
|
||||||
// Per spec, Promise.race with empty array stays pending forever
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var i = 0; i < promises.length; i++) {
|
|
||||||
var promise = promises[i];
|
|
||||||
if (promise && typeof promise.then === 'function') {
|
|
||||||
promise.then(resolve, reject);
|
|
||||||
} else {
|
|
||||||
resolve(promise);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Make Promise global if not already defined
|
|
||||||
if (typeof Promise === 'undefined') {
|
|
||||||
var Promise = SimplePromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Response object that mimics the Fetch API Response
|
|
||||||
*/
|
|
||||||
function FetchResponse(jsHttpResponse) {
|
|
||||||
this._jsResponse = jsHttpResponse;
|
|
||||||
this.status = jsHttpResponse.statusCode();
|
|
||||||
this.ok = this.status >= 200 && this.status < 300;
|
|
||||||
|
|
||||||
// Map HTTP status codes to standard status text
|
|
||||||
var statusTexts = {
|
|
||||||
200: 'OK',
|
|
||||||
201: 'Created',
|
|
||||||
204: 'No Content',
|
|
||||||
301: 'Moved Permanently',
|
|
||||||
302: 'Found',
|
|
||||||
304: 'Not Modified',
|
|
||||||
400: 'Bad Request',
|
|
||||||
401: 'Unauthorized',
|
|
||||||
403: 'Forbidden',
|
|
||||||
404: 'Not Found',
|
|
||||||
405: 'Method Not Allowed',
|
|
||||||
408: 'Request Timeout',
|
|
||||||
409: 'Conflict',
|
|
||||||
410: 'Gone',
|
|
||||||
500: 'Internal Server Error',
|
|
||||||
501: 'Not Implemented',
|
|
||||||
502: 'Bad Gateway',
|
|
||||||
503: 'Service Unavailable',
|
|
||||||
504: 'Gateway Timeout'
|
|
||||||
};
|
|
||||||
|
|
||||||
this.statusText = statusTexts[this.status] || (this.ok ? 'OK' : 'Error');
|
|
||||||
this.headers = {
|
|
||||||
get: function(name) {
|
|
||||||
return jsHttpResponse.header(name);
|
|
||||||
},
|
|
||||||
has: function(name) {
|
|
||||||
return jsHttpResponse.header(name) !== null;
|
|
||||||
},
|
|
||||||
entries: function() {
|
|
||||||
var headerMap = jsHttpResponse.headers();
|
|
||||||
var entries = [];
|
|
||||||
for (var key in headerMap) {
|
|
||||||
if (headerMap.hasOwnProperty(key)) {
|
|
||||||
entries.push([key, headerMap[key]]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return entries;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
FetchResponse.prototype.text = function() {
|
|
||||||
var body = this._jsResponse.body();
|
|
||||||
return SimplePromise.resolve(body || '');
|
|
||||||
};
|
|
||||||
|
|
||||||
FetchResponse.prototype.json = function() {
|
|
||||||
var self = this;
|
|
||||||
return this.text().then(function(text) {
|
|
||||||
try {
|
|
||||||
return JSON.parse(text);
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error('Invalid JSON: ' + e.message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
FetchResponse.prototype.arrayBuffer = function() {
|
|
||||||
var bytes = this._jsResponse.bodyBytes();
|
|
||||||
return SimplePromise.resolve(bytes);
|
|
||||||
};
|
|
||||||
|
|
||||||
FetchResponse.prototype.blob = function() {
|
|
||||||
// Blob not supported in ES5, return bytes
|
|
||||||
return this.arrayBuffer();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch API implementation using JavaFetch bridge
|
|
||||||
* @param {string} url - Request URL
|
|
||||||
* @param {Object} options - Fetch options (method, headers, body, etc.)
|
|
||||||
* @returns {Promise<FetchResponse>}
|
|
||||||
*/
|
|
||||||
function fetch(url, options) {
|
|
||||||
return new SimplePromise(function(resolve, reject) {
|
|
||||||
try {
|
|
||||||
// Parse options
|
|
||||||
options = options || {};
|
|
||||||
var method = (options.method || 'GET').toUpperCase();
|
|
||||||
var headers = options.headers || {};
|
|
||||||
var body = options.body;
|
|
||||||
|
|
||||||
// Prepare request options for JavaFetch
|
|
||||||
var requestOptions = {
|
|
||||||
method: method,
|
|
||||||
headers: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Convert headers to simple object
|
|
||||||
if (headers) {
|
|
||||||
if (typeof headers.forEach === 'function') {
|
|
||||||
// Headers object
|
|
||||||
headers.forEach(function(value, key) {
|
|
||||||
requestOptions.headers[key] = value;
|
|
||||||
});
|
|
||||||
} else if (typeof headers === 'object') {
|
|
||||||
// Plain object
|
|
||||||
for (var key in headers) {
|
|
||||||
if (headers.hasOwnProperty(key)) {
|
|
||||||
requestOptions.headers[key] = headers[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add body if present
|
|
||||||
if (body !== undefined && body !== null) {
|
|
||||||
if (typeof body === 'string') {
|
|
||||||
requestOptions.body = body;
|
|
||||||
} else if (typeof body === 'object') {
|
|
||||||
// Assume JSON
|
|
||||||
requestOptions.body = JSON.stringify(body);
|
|
||||||
if (!requestOptions.headers['Content-Type'] && !requestOptions.headers['content-type']) {
|
|
||||||
requestOptions.headers['Content-Type'] = 'application/json';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call JavaFetch bridge
|
|
||||||
var jsHttpResponse = JavaFetch.fetch(url, requestOptions);
|
|
||||||
|
|
||||||
// Create Response object
|
|
||||||
var response = new FetchResponse(jsHttpResponse);
|
|
||||||
resolve(response);
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
reject(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Export for global use
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
window.fetch = fetch;
|
|
||||||
window.Promise = Promise;
|
|
||||||
} else if (typeof global !== 'undefined') {
|
|
||||||
global.fetch = fetch;
|
|
||||||
global.Promise = Promise;
|
|
||||||
}
|
|
||||||
@@ -1,362 +0,0 @@
|
|||||||
import requests
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
import json
|
|
||||||
import time
|
|
||||||
import random
|
|
||||||
import zlib
|
|
||||||
|
|
||||||
def get_timestamp():
|
|
||||||
"""获取当前时间戳(毫秒)"""
|
|
||||||
return str(int(time.time() * 1000))
|
|
||||||
|
|
||||||
def crc32(data):
|
|
||||||
"""计算CRC32并转换为16进制"""
|
|
||||||
crc = zlib.crc32(data.encode()) & 0xffffffff
|
|
||||||
return format(crc, '08x')
|
|
||||||
|
|
||||||
def hex_to_int(hex_str):
|
|
||||||
"""16进制转10进制"""
|
|
||||||
return int(hex_str, 16)
|
|
||||||
|
|
||||||
def encode123(url, way, version, timestamp):
|
|
||||||
"""
|
|
||||||
123盘的URL加密算法
|
|
||||||
参考C++代码中的encode123函数
|
|
||||||
"""
|
|
||||||
# 生成随机数
|
|
||||||
a = int(10000000 * random.randint(1, 10000000) / 10000)
|
|
||||||
|
|
||||||
# 字符映射表
|
|
||||||
u = "adefghlmyijnopkqrstubcvwsz"
|
|
||||||
|
|
||||||
# 将时间戳转换为时间格式
|
|
||||||
time_long = int(timestamp) // 1000
|
|
||||||
time_struct = time.localtime(time_long)
|
|
||||||
time_str = time.strftime("%Y%m%d%H%M", time_struct)
|
|
||||||
|
|
||||||
# 根据时间字符串生成g
|
|
||||||
g = ""
|
|
||||||
for char in time_str:
|
|
||||||
digit = int(char)
|
|
||||||
if digit == 0:
|
|
||||||
g += u[0]
|
|
||||||
else:
|
|
||||||
# 修正:数字1对应索引0,数字2对应索引1,以此类推
|
|
||||||
g += u[digit - 1]
|
|
||||||
|
|
||||||
# 计算y值(CRC32的十进制)
|
|
||||||
y = str(hex_to_int(crc32(g)))
|
|
||||||
|
|
||||||
# 计算最终的CRC32
|
|
||||||
final_crc_input = f"{time_long}|{a}|{url}|{way}|{version}|{y}"
|
|
||||||
final_crc = str(hex_to_int(crc32(final_crc_input)))
|
|
||||||
|
|
||||||
# 返回加密后的URL参数
|
|
||||||
return f"?{y}={time_long}-{a}-{final_crc}"
|
|
||||||
|
|
||||||
def login_123pan(username, password):
|
|
||||||
"""登录123盘获取token"""
|
|
||||||
print(f"🔐 正在登录账号: {username}")
|
|
||||||
|
|
||||||
login_data = {
|
|
||||||
"passport": username,
|
|
||||||
"password": password,
|
|
||||||
"remember": True
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = requests.post(
|
|
||||||
"https://login.123pan.com/api/user/sign_in",
|
|
||||||
json=login_data,
|
|
||||||
timeout=30
|
|
||||||
)
|
|
||||||
result = response.json()
|
|
||||||
|
|
||||||
if result.get('code') == 200:
|
|
||||||
token = result.get('data', {}).get('token', '')
|
|
||||||
print(f"✅ 登录成功!")
|
|
||||||
return token
|
|
||||||
else:
|
|
||||||
error_msg = result.get('message', '未知错误')
|
|
||||||
print(f"❌ 登录失败: {error_msg}")
|
|
||||||
return None
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ 登录请求失败: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_share_info(share_key, password=''):
|
|
||||||
"""获取分享信息(不需要登录)"""
|
|
||||||
headers = {
|
|
||||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
|
||||||
'Referer': 'https://www.123pan.com/',
|
|
||||||
'Origin': 'https://www.123pan.com',
|
|
||||||
}
|
|
||||||
|
|
||||||
api_url = f"https://www.123pan.com/b/api/share/get?limit=100&next=1&orderBy=share_id&orderDirection=desc&shareKey={share_key}&SharePwd={password}&ParentFileId=0&Page=1"
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = requests.get(api_url, headers=headers, timeout=30)
|
|
||||||
return response.json()
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ 获取分享信息失败: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_download_url_android(file_info, token):
|
|
||||||
"""
|
|
||||||
使用Android平台API获取下载链接(关键方法)
|
|
||||||
参考C++代码中的逻辑
|
|
||||||
"""
|
|
||||||
# 🔥 关键:使用Android平台的请求头
|
|
||||||
headers = {
|
|
||||||
'App-Version': '55',
|
|
||||||
'platform': 'android',
|
|
||||||
'Authorization': f'Bearer {token}',
|
|
||||||
'User-Agent': 'Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36',
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}
|
|
||||||
|
|
||||||
# 构建请求数据
|
|
||||||
post_data = {
|
|
||||||
'driveId': 0,
|
|
||||||
'etag': file_info.get('Etag', ''),
|
|
||||||
'fileId': file_info.get('FileId'),
|
|
||||||
'fileName': file_info.get('FileName', ''),
|
|
||||||
's3keyFlag': file_info.get('S3KeyFlag', ''),
|
|
||||||
'size': file_info.get('Size'),
|
|
||||||
'type': 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# 🔥 关键:使用encode123加密URL参数
|
|
||||||
timestamp = get_timestamp()
|
|
||||||
encrypted_params = encode123('/b/api/file/download_info', 'android', '55', timestamp)
|
|
||||||
api_url = f"https://www.123pan.com/b/api/file/download_info{encrypted_params}"
|
|
||||||
|
|
||||||
print(f" 📡 API URL: {api_url[:80]}...")
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = requests.post(api_url, json=post_data, headers=headers, timeout=30)
|
|
||||||
result = response.json()
|
|
||||||
|
|
||||||
print(f" 📥 API响应: code={result.get('code')}, message={result.get('message', 'N/A')}")
|
|
||||||
|
|
||||||
if result.get('code') == 0 and 'data' in result:
|
|
||||||
download_url = result['data'].get('DownloadUrl') or result['data'].get('DownloadURL')
|
|
||||||
return download_url
|
|
||||||
else:
|
|
||||||
error_msg = result.get('message', '未知错误')
|
|
||||||
print(f" ✗ API返回错误: {error_msg}")
|
|
||||||
return None
|
|
||||||
except Exception as e:
|
|
||||||
print(f" ✗ 请求失败: {e}")
|
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
|
||||||
return None
|
|
||||||
|
|
||||||
def start(link, password='', username='', user_password=''):
|
|
||||||
"""主函数:解析123盘分享链接"""
|
|
||||||
result = {
|
|
||||||
'code': 200,
|
|
||||||
'data': [],
|
|
||||||
'need_login': False
|
|
||||||
}
|
|
||||||
|
|
||||||
# 提取 Share_Key
|
|
||||||
patterns = [
|
|
||||||
r'/s/(.*?)\.html',
|
|
||||||
r'/s/([^/\s]+)',
|
|
||||||
]
|
|
||||||
|
|
||||||
share_key = None
|
|
||||||
for pattern in patterns:
|
|
||||||
matches = re.findall(pattern, link)
|
|
||||||
if matches:
|
|
||||||
share_key = matches[0]
|
|
||||||
break
|
|
||||||
|
|
||||||
if not share_key:
|
|
||||||
return {
|
|
||||||
"code": 201,
|
|
||||||
"message": "分享地址错误,无法提取分享密钥"
|
|
||||||
}
|
|
||||||
|
|
||||||
print(f"📌 分享密钥: {share_key}")
|
|
||||||
|
|
||||||
# 如果提供了账号密码,先登录
|
|
||||||
token = None
|
|
||||||
if username and user_password:
|
|
||||||
token = login_123pan(username, user_password)
|
|
||||||
if not token:
|
|
||||||
return {
|
|
||||||
"code": 201,
|
|
||||||
"message": "登录失败"
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
print("⚠️ 未提供登录信息,某些文件可能无法下载")
|
|
||||||
|
|
||||||
# 获取分享信息
|
|
||||||
print(f"\n📂 正在获取文件列表...")
|
|
||||||
share_data = get_share_info(share_key, password)
|
|
||||||
|
|
||||||
if not share_data or share_data.get('code') != 0:
|
|
||||||
error_msg = share_data.get('message', '未知错误') if share_data else '请求失败'
|
|
||||||
return {
|
|
||||||
"code": 201,
|
|
||||||
"message": f"获取分享信息失败: {error_msg}"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 获取文件列表
|
|
||||||
if 'data' not in share_data or 'InfoList' not in share_data['data']:
|
|
||||||
return {
|
|
||||||
"code": 201,
|
|
||||||
"message": "返回数据格式错误"
|
|
||||||
}
|
|
||||||
|
|
||||||
info_list = share_data['data']['InfoList']
|
|
||||||
length = len(info_list)
|
|
||||||
|
|
||||||
print(f"📁 找到 {length} 个项目\n")
|
|
||||||
|
|
||||||
# 遍历文件列表
|
|
||||||
for i, file_info in enumerate(info_list):
|
|
||||||
file_type = file_info.get('Type', 0)
|
|
||||||
file_name = file_info.get('FileName', '')
|
|
||||||
|
|
||||||
# 跳过文件夹
|
|
||||||
if file_type != 0:
|
|
||||||
print(f"[{i+1}/{length}] 跳过文件夹: {file_name}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
print(f"[{i+1}/{length}] 正在解析: {file_name}")
|
|
||||||
|
|
||||||
if not token:
|
|
||||||
print(f" ⚠️ 需要登录才能获取下载链接")
|
|
||||||
result['need_login'] = True
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 🔥 使用Android平台API获取下载链接
|
|
||||||
print(f" 🤖 使用Android平台API...")
|
|
||||||
download_url = get_download_url_android(file_info, token)
|
|
||||||
|
|
||||||
if download_url:
|
|
||||||
result['data'].append({
|
|
||||||
"Name": file_name,
|
|
||||||
"Size": file_info.get('Size', 0),
|
|
||||||
"DownloadURL": download_url
|
|
||||||
})
|
|
||||||
print(f" ✓ 成功获取直链\n")
|
|
||||||
else:
|
|
||||||
print(f" ✗ 获取失败\n")
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
def format_size(size_bytes):
|
|
||||||
"""格式化文件大小"""
|
|
||||||
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
|
|
||||||
if size_bytes < 1024.0:
|
|
||||||
return f"{size_bytes:.2f} {unit}"
|
|
||||||
size_bytes /= 1024.0
|
|
||||||
return f"{size_bytes:.2f} PB"
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""主程序入口"""
|
|
||||||
if len(sys.argv) < 2:
|
|
||||||
print("=" * 80)
|
|
||||||
print(" 123盘直链解析工具 v3.0")
|
|
||||||
print("=" * 80)
|
|
||||||
print("\n📖 使用方法:")
|
|
||||||
print(" python 123.py <分享链接> [选项]")
|
|
||||||
print("\n⚙️ 选项:")
|
|
||||||
print(" --pwd <密码> 分享密码(如果有)")
|
|
||||||
print(" --user <账号> 123盘账号")
|
|
||||||
print(" --pass <密码> 123盘密码")
|
|
||||||
print("\n💡 示例:")
|
|
||||||
print(' # 需要登录的分享(推荐)')
|
|
||||||
print(' python 123.py "https://www.123pan.com/s/xxxxx" --user "账号" --pass "密码"')
|
|
||||||
print()
|
|
||||||
print(' # 有分享密码')
|
|
||||||
print(' python 123.py "https://www.123pan.com/s/xxxxx" --pwd "分享密码" --user "账号" --pass "密码"')
|
|
||||||
print("\n✨ 特性:")
|
|
||||||
print(" • 使用Android平台API(完全绕过限制)")
|
|
||||||
print(" • 使用123盘加密算法(encode123)")
|
|
||||||
print(" • 支持账号密码登录")
|
|
||||||
print(" • 无地区限制,无流量限制")
|
|
||||||
print("=" * 80)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
link = sys.argv[1]
|
|
||||||
password = ''
|
|
||||||
username = ''
|
|
||||||
user_password = ''
|
|
||||||
|
|
||||||
# 解析参数
|
|
||||||
i = 2
|
|
||||||
while i < len(sys.argv):
|
|
||||||
if sys.argv[i] == '--pwd' and i + 1 < len(sys.argv):
|
|
||||||
password = sys.argv[i + 1]
|
|
||||||
i += 2
|
|
||||||
elif sys.argv[i] == '--user' and i + 1 < len(sys.argv):
|
|
||||||
username = sys.argv[i + 1]
|
|
||||||
i += 2
|
|
||||||
elif sys.argv[i] == '--pass' and i + 1 < len(sys.argv):
|
|
||||||
user_password = sys.argv[i + 1]
|
|
||||||
i += 2
|
|
||||||
else:
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
print("\n" + "=" * 80)
|
|
||||||
print(" 开始解析分享链接")
|
|
||||||
print("=" * 80)
|
|
||||||
print(f"🔗 链接: {link}")
|
|
||||||
if password:
|
|
||||||
print(f"🔐 分享密码: {password}")
|
|
||||||
if username:
|
|
||||||
print(f"👤 登录账号: {username}")
|
|
||||||
print("=" * 80)
|
|
||||||
print()
|
|
||||||
|
|
||||||
result = start(link, password, username, user_password)
|
|
||||||
|
|
||||||
if result['code'] != 200:
|
|
||||||
print(f"\n❌ 错误: {result['message']}")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
if not result['data']:
|
|
||||||
print("\n⚠️ 没有成功获取到任何文件的直链")
|
|
||||||
|
|
||||||
if result.get('need_login'):
|
|
||||||
print("\n🔒 该分享需要登录才能下载")
|
|
||||||
print("\n请使用以下命令:")
|
|
||||||
print(f' python 123.py "{link}" --user "你的账号" --pass "你的密码"')
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
print("\n" + "=" * 80)
|
|
||||||
print(" ✅ 解析成功!")
|
|
||||||
print("=" * 80)
|
|
||||||
|
|
||||||
for idx, file in enumerate(result['data'], 1):
|
|
||||||
print(f"\n📄 文件 {idx}:")
|
|
||||||
print(f" 名称: {file['Name']}")
|
|
||||||
print(f" 大小: {format_size(file['Size'])} ({file['Size']:,} 字节)")
|
|
||||||
print(f" 直链: {file['DownloadURL']}")
|
|
||||||
print("-" * 80)
|
|
||||||
|
|
||||||
print("\n💾 下载方法:")
|
|
||||||
print("\n 使用curl命令:")
|
|
||||||
for file in result['data']:
|
|
||||||
safe_name = file['Name'].replace('"', '\\"')
|
|
||||||
print(f' curl -L -o "{safe_name}" "{file["DownloadURL"]}"')
|
|
||||||
|
|
||||||
print("\n 使用aria2c命令(推荐,多线程):")
|
|
||||||
for file in result['data']:
|
|
||||||
safe_name = file['Name'].replace('"', '\\"')
|
|
||||||
print(f' aria2c -x 16 -s 16 -o "{safe_name}" "{file["DownloadURL"]}"')
|
|
||||||
|
|
||||||
print("\n💡 提示:")
|
|
||||||
print(" • 使用Android平台API,无地区限制")
|
|
||||||
print(" • 直链有效期通常为几小时")
|
|
||||||
print(" • 推荐使用 aria2c 下载(速度最快)")
|
|
||||||
print()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
package cn.qaiu.parser.customjs;
|
|
||||||
|
|
||||||
import cn.qaiu.WebClientVertxInit;
|
|
||||||
import cn.qaiu.entity.ShareLinkInfo;
|
|
||||||
import cn.qaiu.parser.IPanTool;
|
|
||||||
import cn.qaiu.parser.ParserCreate;
|
|
||||||
import cn.qaiu.parser.custom.CustomParserConfig;
|
|
||||||
import cn.qaiu.parser.custom.CustomParserRegistry;
|
|
||||||
import io.vertx.core.Vertx;
|
|
||||||
import org.junit.Test;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch Bridge测试
|
|
||||||
* 测试fetch API和Promise polyfill功能
|
|
||||||
*/
|
|
||||||
public class JsFetchBridgeTest {
|
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(JsFetchBridgeTest.class);
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testFetchPolyfillLoaded() {
|
|
||||||
// 初始化Vertx
|
|
||||||
Vertx vertx = Vertx.vertx();
|
|
||||||
WebClientVertxInit.init(vertx);
|
|
||||||
|
|
||||||
// 清理注册表
|
|
||||||
CustomParserRegistry.clear();
|
|
||||||
|
|
||||||
// 创建一个简单的解析器配置
|
|
||||||
String jsCode = """
|
|
||||||
// 测试Promise是否可用
|
|
||||||
function parse(shareLinkInfo, http, logger) {
|
|
||||||
logger.info("测试开始");
|
|
||||||
|
|
||||||
// 检查Promise是否存在
|
|
||||||
if (typeof Promise === 'undefined') {
|
|
||||||
throw new Error("Promise未定义");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查fetch是否存在
|
|
||||||
if (typeof fetch === 'undefined') {
|
|
||||||
throw new Error("fetch未定义");
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info("✓ Promise已定义");
|
|
||||||
logger.info("✓ fetch已定义");
|
|
||||||
|
|
||||||
return "https://example.com/success";
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
CustomParserConfig config = CustomParserConfig.builder()
|
|
||||||
.type("test_fetch")
|
|
||||||
.displayName("Fetch测试")
|
|
||||||
.matchPattern("https://example.com/s/(?<KEY>\\w+)")
|
|
||||||
.jsCode(jsCode)
|
|
||||||
.isJsParser(true)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
// 注册到注册表
|
|
||||||
CustomParserRegistry.register(config);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 使用ParserCreate创建工具
|
|
||||||
IPanTool tool = ParserCreate.fromType("test_fetch")
|
|
||||||
.shareKey("test123")
|
|
||||||
.createTool();
|
|
||||||
|
|
||||||
String result = tool.parseSync();
|
|
||||||
|
|
||||||
log.info("测试结果: {}", result);
|
|
||||||
assert "https://example.com/success".equals(result) : "结果不匹配";
|
|
||||||
|
|
||||||
System.out.println("✓ Fetch polyfill加载测试通过");
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("测试失败", e);
|
|
||||||
throw new RuntimeException("Fetch polyfill加载失败: " + e.getMessage(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testPromiseBasicUsage() {
|
|
||||||
// 初始化Vertx
|
|
||||||
Vertx vertx = Vertx.vertx();
|
|
||||||
WebClientVertxInit.init(vertx);
|
|
||||||
|
|
||||||
// 清理注册表
|
|
||||||
CustomParserRegistry.clear();
|
|
||||||
|
|
||||||
String jsCode = """
|
|
||||||
function parse(shareLinkInfo, http, logger) {
|
|
||||||
logger.info("测试Promise基本用法");
|
|
||||||
|
|
||||||
// 创建一个Promise
|
|
||||||
var testPromise = new Promise(function(resolve, reject) {
|
|
||||||
resolve("Promise成功");
|
|
||||||
});
|
|
||||||
|
|
||||||
var result = null;
|
|
||||||
testPromise.then(function(value) {
|
|
||||||
logger.info("Promise结果: " + value);
|
|
||||||
result = value;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 等待Promise完成(简单同步等待)
|
|
||||||
var timeout = 1000;
|
|
||||||
var start = Date.now();
|
|
||||||
while (result === null && (Date.now() - start) < timeout) {
|
|
||||||
java.lang.Thread.sleep(10);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result === null) {
|
|
||||||
throw new Error("Promise未完成");
|
|
||||||
}
|
|
||||||
|
|
||||||
return "https://example.com/" + result;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
CustomParserConfig config = CustomParserConfig.builder()
|
|
||||||
.type("test_promise")
|
|
||||||
.displayName("Promise测试")
|
|
||||||
.matchPattern("https://example.com/s/(?<KEY>\\w+)")
|
|
||||||
.jsCode(jsCode)
|
|
||||||
.isJsParser(true)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
// 注册到注册表
|
|
||||||
CustomParserRegistry.register(config);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 使用ParserCreate创建工具
|
|
||||||
IPanTool tool = ParserCreate.fromType("test_promise")
|
|
||||||
.shareKey("test456")
|
|
||||||
.createTool();
|
|
||||||
|
|
||||||
String result = tool.parseSync();
|
|
||||||
|
|
||||||
log.info("测试结果: {}", result);
|
|
||||||
assert result.contains("Promise成功") : "结果不包含'Promise成功'";
|
|
||||||
|
|
||||||
System.out.println("✓ Promise测试通过");
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("测试失败", e);
|
|
||||||
throw new RuntimeException("Promise测试失败: " + e.getMessage(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,174 +0,0 @@
|
|||||||
# Monaco Editor NPM包配置说明
|
|
||||||
|
|
||||||
## ✅ 已完成的配置
|
|
||||||
|
|
||||||
### 1. NPM包安装
|
|
||||||
已在 `package.json` 中安装:
|
|
||||||
- `monaco-editor`: ^0.45.0 - Monaco Editor核心包
|
|
||||||
- `@monaco-editor/loader`: ^1.4.0 - Monaco Editor加载器
|
|
||||||
- `monaco-editor-webpack-plugin`: ^7.1.1 - Webpack打包插件(devDependencies)
|
|
||||||
|
|
||||||
### 2. Webpack配置
|
|
||||||
在 `vue.config.js` 中已配置:
|
|
||||||
```javascript
|
|
||||||
new MonacoEditorPlugin({
|
|
||||||
languages: ['javascript', 'typescript', 'json'],
|
|
||||||
features: ['coreCommands', 'find'],
|
|
||||||
publicPath: process.env.NODE_ENV === 'production' ? './' : '/'
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 组件配置
|
|
||||||
在 `MonacoEditor.vue` 和 `Playground.vue` 中已配置:
|
|
||||||
```javascript
|
|
||||||
// 配置loader使用本地打包的文件,而不是CDN
|
|
||||||
if (loader.config) {
|
|
||||||
const vsPath = process.env.NODE_ENV === 'production'
|
|
||||||
? './js/vs' // 生产环境使用相对路径
|
|
||||||
: '/js/vs'; // 开发环境使用绝对路径
|
|
||||||
|
|
||||||
loader.config({
|
|
||||||
paths: {
|
|
||||||
vs: vsPath
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔍 工作原理
|
|
||||||
|
|
||||||
### 打包流程
|
|
||||||
1. `monaco-editor-webpack-plugin` 在构建时将 Monaco Editor 文件打包到 `js/vs/` 目录
|
|
||||||
2. `@monaco-editor/loader` 通过配置的路径加载本地文件
|
|
||||||
3. 不再从 CDN(如 `https://cdn.jsdelivr.net`)加载
|
|
||||||
|
|
||||||
### 文件结构(构建后)
|
|
||||||
```
|
|
||||||
nfd-front/
|
|
||||||
├── js/
|
|
||||||
│ └── vs/
|
|
||||||
│ ├── editor/
|
|
||||||
│ ├── loader/
|
|
||||||
│ ├── base/
|
|
||||||
│ └── ...
|
|
||||||
└── index.html
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🧪 验证方法
|
|
||||||
|
|
||||||
### 1. 检查网络请求
|
|
||||||
打开浏览器开发者工具 → Network标签:
|
|
||||||
- ✅ 应该看到请求 `/js/vs/...` 或 `./js/vs/...`
|
|
||||||
- ❌ 不应该看到请求 `cdn.jsdelivr.net` 或其他CDN域名
|
|
||||||
|
|
||||||
### 2. 检查构建产物
|
|
||||||
```bash
|
|
||||||
cd web-front
|
|
||||||
npm run build
|
|
||||||
ls -la nfd-front/js/vs/
|
|
||||||
```
|
|
||||||
应该看到 Monaco Editor 的文件被打包到本地。
|
|
||||||
|
|
||||||
### 3. 离线测试
|
|
||||||
1. 断开网络连接
|
|
||||||
2. 访问 Playground 页面
|
|
||||||
3. ✅ 编辑器应该正常加载(因为使用本地文件)
|
|
||||||
4. ❌ 如果使用CDN,编辑器会加载失败
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📝 修改的文件
|
|
||||||
|
|
||||||
1. ✅ `web-front/src/components/MonacoEditor.vue`
|
|
||||||
- 添加了 `loader.config()` 配置,明确使用本地路径
|
|
||||||
|
|
||||||
2. ✅ `web-front/src/views/Playground.vue`
|
|
||||||
- 在 `initMonacoTypes()` 中添加了相同的配置
|
|
||||||
|
|
||||||
3. ✅ `web-front/vue.config.js`
|
|
||||||
- 添加了 `publicPath` 配置,确保路径正确
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 部署
|
|
||||||
|
|
||||||
### 开发环境
|
|
||||||
```bash
|
|
||||||
cd web-front
|
|
||||||
npm install # 确保依赖已安装
|
|
||||||
npm run serve
|
|
||||||
```
|
|
||||||
访问 `http://127.0.0.1:6444/playground`,编辑器应该从本地加载。
|
|
||||||
|
|
||||||
### 生产环境
|
|
||||||
```bash
|
|
||||||
cd web-front
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
构建后,Monaco Editor 文件会打包到 `nfd-front/js/vs/` 目录。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ⚠️ 注意事项
|
|
||||||
|
|
||||||
### 1. 文件大小
|
|
||||||
Monaco Editor 打包后会增加构建产物大小(约2-3MB),但这是正常的。
|
|
||||||
|
|
||||||
### 2. 首次加载
|
|
||||||
- 开发环境:文件从 webpack dev server 加载
|
|
||||||
- 生产环境:文件从本地 `js/vs/` 目录加载
|
|
||||||
|
|
||||||
### 3. 缓存
|
|
||||||
浏览器会缓存 Monaco Editor 文件,更新后可能需要清除缓存。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔧 故障排查
|
|
||||||
|
|
||||||
### 问题:编辑器无法加载
|
|
||||||
**检查**:
|
|
||||||
1. 确认 `npm install` 已执行
|
|
||||||
2. 检查浏览器控制台是否有错误
|
|
||||||
3. 检查 Network 标签,确认文件路径是否正确
|
|
||||||
|
|
||||||
### 问题:仍然从CDN加载
|
|
||||||
**解决**:
|
|
||||||
1. 清除浏览器缓存
|
|
||||||
2. 确认 `loader.config()` 已正确配置
|
|
||||||
3. 检查 `vue.config.js` 中的 `publicPath` 配置
|
|
||||||
|
|
||||||
### 问题:构建后路径错误
|
|
||||||
**解决**:
|
|
||||||
- 检查 `publicPath` 配置
|
|
||||||
- 确认生产环境的相对路径 `./js/vs` 正确
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✅ 优势
|
|
||||||
|
|
||||||
1. **离线可用** - 不依赖外部CDN
|
|
||||||
2. **加载速度** - 本地文件通常比CDN更快
|
|
||||||
3. **版本控制** - 使用固定版本的Monaco Editor
|
|
||||||
4. **安全性** - 不依赖第三方CDN服务
|
|
||||||
5. **稳定性** - CDN故障不影响使用
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**配置状态**: ✅ 已完成
|
|
||||||
**验证状态**: ⚠️ 待测试
|
|
||||||
**建议**: 运行 `npm run build` 并检查构建产物
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -5,21 +5,24 @@ import axios from 'axios';
|
|||||||
*/
|
*/
|
||||||
export const playgroundApi = {
|
export const playgroundApi = {
|
||||||
/**
|
/**
|
||||||
* 获取Playground状态(是否需要认证)
|
* 获取Playground状态
|
||||||
* @returns {Promise} 状态信息
|
* @returns {Promise} 状态信息 {enabled, needPassword, authed}
|
||||||
*/
|
*/
|
||||||
async getStatus() {
|
async getStatus() {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get('/v2/playground/status');
|
const response = await axios.get('/v2/playground/status');
|
||||||
|
if (response.data && response.data.data) {
|
||||||
|
return response.data.data;
|
||||||
|
}
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(error.response?.data?.error || error.message || '获取状态失败');
|
throw new Error(error.response?.data?.msg || error.message || '获取状态失败');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Playground登录
|
* Playground登录
|
||||||
* @param {string} password - 访问密码
|
* @param {string} password - 密码
|
||||||
* @returns {Promise} 登录结果
|
* @returns {Promise} 登录结果
|
||||||
*/
|
*/
|
||||||
async login(password) {
|
async login(password) {
|
||||||
@@ -27,7 +30,7 @@ export const playgroundApi = {
|
|||||||
const response = await axios.post('/v2/playground/login', { password });
|
const response = await axios.post('/v2/playground/login', { password });
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(error.response?.data?.error || error.message || '登录失败');
|
throw new Error(error.response?.data?.msg || error.message || '登录失败');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -168,6 +171,6 @@ export const playgroundApi = {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(error.response?.data?.error || error.response?.data?.msg || error.message || '获取解析器失败');
|
throw new Error(error.response?.data?.error || error.response?.data?.msg || error.message || '获取解析器失败');
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,66 +1,68 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="playgroundContainer" class="playground-container" :class="{ 'dark-theme': isDarkMode, 'fullscreen': isFullscreen, 'is-mobile': isMobile }">
|
<div ref="playgroundContainer" class="playground-container" :class="{ 'dark-theme': isDarkMode, 'fullscreen': isFullscreen }">
|
||||||
<!-- 加载动画 + 进度条 -->
|
<!-- 加载状态 -->
|
||||||
<div v-if="loading" class="playground-loading-overlay">
|
<el-card v-if="statusLoading" class="playground-card" v-loading="true" element-loading-text="正在加载...">
|
||||||
<div class="playground-loading-card">
|
<div style="height: 400px;"></div>
|
||||||
<div class="loading-icon">
|
</el-card>
|
||||||
<el-icon class="is-loading" :size="40"><Loading /></el-icon>
|
|
||||||
</div>
|
|
||||||
<div class="loading-text">正在加载编辑器和编译器...</div>
|
|
||||||
<div class="loading-bar">
|
|
||||||
<div class="loading-bar-inner" :style="{ width: loadProgress + '%' }"></div>
|
|
||||||
</div>
|
|
||||||
<div class="loading-percent">{{ loadProgress }}%</div>
|
|
||||||
<div class="loading-details">{{ loadingMessage }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 密码验证界面 -->
|
<!-- Playground未开启 -->
|
||||||
<div v-if="!loading && authChecking" class="playground-auth-loading">
|
<el-card v-else-if="!enabled" class="playground-card">
|
||||||
<el-icon class="is-loading" :size="30"><Loading /></el-icon>
|
<el-empty description="Playground未开启">
|
||||||
<span style="margin-left: 10px;">正在检查访问权限...</span>
|
<template #extra>
|
||||||
</div>
|
<p style="color: #909399; font-size: 14px; margin-top: 10px;">
|
||||||
|
Playground功能目前未启用,请联系管理员在配置中开启此功能。
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
</el-empty>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
<div v-if="shouldShowAuthUI" class="playground-auth-overlay">
|
<!-- 需要密码但未认证 -->
|
||||||
<div class="playground-auth-card">
|
<el-card v-else-if="needPassword && !authed" class="playground-card">
|
||||||
<div class="auth-icon">
|
<div class="password-container">
|
||||||
<el-icon :size="50"><Lock /></el-icon>
|
<h2>🔒 Playground访问认证</h2>
|
||||||
</div>
|
<p style="color: #909399; margin: 20px 0;">
|
||||||
<div class="auth-title">JS解析器演练场</div>
|
此Playground需要密码访问,请输入密码后继续使用。
|
||||||
<div class="auth-subtitle">请输入访问密码</div>
|
</p>
|
||||||
|
<el-form @submit.prevent="submitPassword" style="max-width: 400px; margin: 0 auto;">
|
||||||
|
<el-form-item>
|
||||||
<el-input
|
<el-input
|
||||||
v-model="inputPassword"
|
v-model="password"
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="请输入访问密码"
|
placeholder="请输入访问密码"
|
||||||
size="large"
|
size="large"
|
||||||
|
show-password
|
||||||
|
clearable
|
||||||
@keyup.enter="submitPassword"
|
@keyup.enter="submitPassword"
|
||||||
class="auth-input"
|
|
||||||
>
|
>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<el-icon><Lock /></el-icon>
|
<el-icon><Lock /></el-icon>
|
||||||
</template>
|
</template>
|
||||||
</el-input>
|
</el-input>
|
||||||
<div v-if="authError" class="auth-error">
|
</el-form-item>
|
||||||
<el-icon><WarningFilled /></el-icon>
|
<el-form-item v-if="authError" style="margin-bottom: 10px;">
|
||||||
<span>{{ authError }}</span>
|
<el-alert type="error" :title="authError" :closable="false" />
|
||||||
</div>
|
</el-form-item>
|
||||||
<el-button type="primary" size="large" @click="submitPassword" :loading="authLoading" class="auth-button">
|
<el-form-item>
|
||||||
<el-icon v-if="!authLoading"><Unlock /></el-icon>
|
<el-button
|
||||||
<span>确认登录</span>
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
style="width: 100%;"
|
||||||
|
:loading="authenticating"
|
||||||
|
@click="submitPassword"
|
||||||
|
>
|
||||||
|
{{ authenticating ? '验证中...' : '验证并进入' }}
|
||||||
</el-button>
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</el-card>
|
||||||
|
|
||||||
<!-- 原有内容 - 只在已认证时显示 -->
|
<!-- 已启用且已认证(或公开模式) -->
|
||||||
<el-card v-if="authed && !loading" class="playground-card">
|
<el-card v-else class="playground-card">
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
<span class="title">JS解析器演练场</span>
|
<span class="title">JS解析器演练场</span>
|
||||||
<!-- 语言显示(仅支持JavaScript) -->
|
|
||||||
<span style="margin-left: 15px; color: var(--el-text-color-secondary); font-size: 12px;">
|
|
||||||
JavaScript (ES5)
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
<!-- 主要操作 -->
|
<!-- 主要操作 -->
|
||||||
@@ -118,8 +120,8 @@
|
|||||||
<el-tabs v-model="activeTab" @tab-change="handleTabChange">
|
<el-tabs v-model="activeTab" @tab-change="handleTabChange">
|
||||||
<!-- 代码编辑标签页 -->
|
<!-- 代码编辑标签页 -->
|
||||||
<el-tab-pane label="代码编辑" name="editor">
|
<el-tab-pane label="代码编辑" name="editor">
|
||||||
<Splitpanes :class="['default-theme', isMobile ? 'mobile-vertical' : '']" :horizontal="isMobile" @resized="handleResize">
|
<Splitpanes class="default-theme" @resized="handleResize">
|
||||||
<!-- 编辑器区域 (PC: 左侧, Mobile: 上方) -->
|
<!-- 左侧:代码编辑区 -->
|
||||||
<Pane :size="collapsedPanels.rightPanel ? 100 : splitSizes[0]" min-size="30" class="editor-pane">
|
<Pane :size="collapsedPanels.rightPanel ? 100 : splitSizes[0]" min-size="30" class="editor-pane">
|
||||||
<div class="editor-section">
|
<div class="editor-section">
|
||||||
<MonacoEditor
|
<MonacoEditor
|
||||||
@@ -133,9 +135,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</Pane>
|
</Pane>
|
||||||
|
|
||||||
<!-- 测试参数和结果区域 (PC: 右侧, Mobile: 下方) -->
|
<!-- 右侧:测试参数和结果 -->
|
||||||
<Pane v-if="!collapsedPanels.rightPanel"
|
<Pane v-if="!collapsedPanels.rightPanel"
|
||||||
:size="splitSizes[1]" min-size="20" class="test-pane" :style="isMobile ? 'margin-top: 10px;' : 'margin-left: 10px;'">
|
:size="splitSizes[1]" min-size="20" class="test-pane" style="margin-left: 10px;">
|
||||||
<div class="test-section">
|
<div class="test-section">
|
||||||
<!-- 优化的折叠按钮 -->
|
<!-- 优化的折叠按钮 -->
|
||||||
<el-tooltip content="折叠测试面板" placement="left">
|
<el-tooltip content="折叠测试面板" placement="left">
|
||||||
@@ -529,6 +531,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { ref, computed, onMounted, onUnmounted, nextTick, watch } from 'vue';
|
import { ref, computed, onMounted, onUnmounted, nextTick, watch } from 'vue';
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
|
import { Lock } from '@element-plus/icons-vue';
|
||||||
import { useMagicKeys, useFullscreen, useEventListener } from '@vueuse/core';
|
import { useMagicKeys, useFullscreen, useEventListener } from '@vueuse/core';
|
||||||
import { Splitpanes, Pane } from 'splitpanes';
|
import { Splitpanes, Pane } from 'splitpanes';
|
||||||
import 'splitpanes/dist/splitpanes.css';
|
import 'splitpanes/dist/splitpanes.css';
|
||||||
@@ -543,30 +546,12 @@ export default {
|
|||||||
MonacoEditor,
|
MonacoEditor,
|
||||||
JsonViewer,
|
JsonViewer,
|
||||||
Splitpanes,
|
Splitpanes,
|
||||||
Pane
|
Pane,
|
||||||
|
Lock
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
// 语言常量
|
|
||||||
const LANGUAGE = {
|
|
||||||
JAVASCRIPT: 'JavaScript'
|
|
||||||
};
|
|
||||||
|
|
||||||
const editorRef = ref(null);
|
const editorRef = ref(null);
|
||||||
const jsCode = ref('');
|
const jsCode = ref('');
|
||||||
|
|
||||||
// ===== 加载和认证状态 =====
|
|
||||||
const loading = ref(true);
|
|
||||||
const loadProgress = ref(0);
|
|
||||||
const loadingMessage = ref('初始化...');
|
|
||||||
const authChecking = ref(true);
|
|
||||||
const authed = ref(false);
|
|
||||||
const inputPassword = ref('');
|
|
||||||
const authError = ref('');
|
|
||||||
const authLoading = ref(false);
|
|
||||||
|
|
||||||
// ===== 移动端检测 =====
|
|
||||||
const isMobile = ref(false);
|
|
||||||
|
|
||||||
const testParams = ref({
|
const testParams = ref({
|
||||||
shareUrl: 'https://lanzoui.com/i7Aq12ab3cd',
|
shareUrl: 'https://lanzoui.com/i7Aq12ab3cd',
|
||||||
pwd: '',
|
pwd: '',
|
||||||
@@ -580,6 +565,15 @@ export default {
|
|||||||
const loadingList = ref(false);
|
const loadingList = ref(false);
|
||||||
const publishDialogVisible = ref(false);
|
const publishDialogVisible = ref(false);
|
||||||
const publishing = ref(false);
|
const publishing = ref(false);
|
||||||
|
|
||||||
|
// Playground状态相关
|
||||||
|
const statusLoading = ref(true);
|
||||||
|
const enabled = ref(false);
|
||||||
|
const needPassword = ref(false);
|
||||||
|
const authed = ref(false);
|
||||||
|
const password = ref('');
|
||||||
|
const authError = ref('');
|
||||||
|
const authenticating = ref(false);
|
||||||
const publishForm = ref({
|
const publishForm = ref({
|
||||||
jsCode: ''
|
jsCode: ''
|
||||||
});
|
});
|
||||||
@@ -698,11 +692,6 @@ function parseById(shareLinkInfo, http, logger) {
|
|||||||
return isDarkMode.value ? 'vs-dark' : 'vs';
|
return isDarkMode.value ? 'vs-dark' : 'vs';
|
||||||
});
|
});
|
||||||
|
|
||||||
// 计算属性:是否需要显示密码输入界面
|
|
||||||
const shouldShowAuthUI = computed(() => {
|
|
||||||
return !loading.value && !authChecking.value && !authed.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 编辑器配置
|
// 编辑器配置
|
||||||
const editorOptions = {
|
const editorOptions = {
|
||||||
minimap: { enabled: true },
|
minimap: { enabled: true },
|
||||||
@@ -714,127 +703,6 @@ function parseById(shareLinkInfo, http, logger) {
|
|||||||
tabSize: 2
|
tabSize: 2
|
||||||
};
|
};
|
||||||
|
|
||||||
// ===== 移动端检测 =====
|
|
||||||
const updateIsMobile = () => {
|
|
||||||
isMobile.value = window.innerWidth <= 768;
|
|
||||||
};
|
|
||||||
|
|
||||||
// ===== 进度设置函数 =====
|
|
||||||
const setProgress = (progress, message = '') => {
|
|
||||||
if (progress > loadProgress.value) {
|
|
||||||
loadProgress.value = progress;
|
|
||||||
}
|
|
||||||
if (message) {
|
|
||||||
loadingMessage.value = message;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ===== 认证相关函数 =====
|
|
||||||
const checkAuthStatus = async () => {
|
|
||||||
try {
|
|
||||||
const res = await playgroundApi.getStatus();
|
|
||||||
if (res.code === 200 && res.data) {
|
|
||||||
authed.value = res.data.authed || res.data.public;
|
|
||||||
return res.data.authed || res.data.public;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('检查认证状态失败:', error);
|
|
||||||
ElMessage.error('检查访问权限失败: ' + error.message);
|
|
||||||
return false;
|
|
||||||
} finally {
|
|
||||||
authChecking.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const submitPassword = async () => {
|
|
||||||
if (!inputPassword.value.trim()) {
|
|
||||||
authError.value = '请输入密码';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
authError.value = '';
|
|
||||||
authLoading.value = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await playgroundApi.login(inputPassword.value);
|
|
||||||
if (res.code === 200 || res.success) {
|
|
||||||
authed.value = true;
|
|
||||||
ElMessage.success('登录成功');
|
|
||||||
await initPlayground();
|
|
||||||
} else {
|
|
||||||
authError.value = res.msg || res.message || '密码错误';
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
authError.value = error.message || '登录失败,请重试';
|
|
||||||
} finally {
|
|
||||||
authLoading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ===== Playground 初始化 =====
|
|
||||||
const initPlayground = async () => {
|
|
||||||
loading.value = true;
|
|
||||||
loadProgress.value = 0;
|
|
||||||
|
|
||||||
try {
|
|
||||||
setProgress(10, '初始化Vue组件...');
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
setProgress(20, '加载配置和本地数据...');
|
|
||||||
// 加载保存的代码
|
|
||||||
const saved = localStorage.getItem('playground_code');
|
|
||||||
if (saved) {
|
|
||||||
jsCode.value = saved;
|
|
||||||
} else {
|
|
||||||
jsCode.value = exampleCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
setProgress(50, '初始化Monaco Editor类型定义...');
|
|
||||||
await initMonacoTypes();
|
|
||||||
|
|
||||||
setProgress(80, '加载完成...');
|
|
||||||
|
|
||||||
// 加载保存的主题
|
|
||||||
const savedTheme = localStorage.getItem('playground_theme');
|
|
||||||
if (savedTheme) {
|
|
||||||
currentTheme.value = savedTheme;
|
|
||||||
const theme = themes.find(t => t.name === savedTheme);
|
|
||||||
if (theme && document.documentElement && document.body) {
|
|
||||||
await nextTick();
|
|
||||||
if (theme.page === 'dark') {
|
|
||||||
document.documentElement.classList.add('dark');
|
|
||||||
document.body.classList.add('dark-theme');
|
|
||||||
document.body.style.backgroundColor = '#0a0a0a';
|
|
||||||
} else {
|
|
||||||
document.documentElement.classList.remove('dark');
|
|
||||||
document.body.classList.remove('dark-theme');
|
|
||||||
document.body.style.backgroundColor = '#f0f2f5';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载保存的折叠状态
|
|
||||||
const savedCollapsed = localStorage.getItem('playground_collapsed_panels');
|
|
||||||
if (savedCollapsed) {
|
|
||||||
try {
|
|
||||||
collapsedPanels.value = JSON.parse(savedCollapsed);
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('加载折叠状态失败', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setProgress(100, '初始化完成!');
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 300));
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('初始化失败:', error);
|
|
||||||
ElMessage.error('初始化失败: ' + error.message);
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 初始化Monaco Editor类型定义
|
// 初始化Monaco Editor类型定义
|
||||||
const initMonacoTypes = async () => {
|
const initMonacoTypes = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -857,6 +725,63 @@ function parseById(shareLinkInfo, http, logger) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 获取Playground状态
|
||||||
|
const fetchStatus = async () => {
|
||||||
|
try {
|
||||||
|
const result = await playgroundApi.getStatus();
|
||||||
|
enabled.value = result.enabled;
|
||||||
|
needPassword.value = result.needPassword;
|
||||||
|
authed.value = result.authed;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取Playground状态失败:', error);
|
||||||
|
ElMessage.error('获取Playground状态失败: ' + error.message);
|
||||||
|
// 默认为未启用
|
||||||
|
enabled.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 提交密码
|
||||||
|
const submitPassword = async () => {
|
||||||
|
if (!password.value) {
|
||||||
|
authError.value = '请输入密码';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
authError.value = '';
|
||||||
|
authenticating.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await playgroundApi.login(password.value);
|
||||||
|
if (result.success || result.code === 200) {
|
||||||
|
authed.value = true;
|
||||||
|
ElMessage.success('认证成功');
|
||||||
|
// 初始化Playground
|
||||||
|
await nextTick();
|
||||||
|
await initPlayground();
|
||||||
|
} else {
|
||||||
|
authError.value = result.msg || '密码错误';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Playground登录失败:', error);
|
||||||
|
authError.value = error.message || '登录失败';
|
||||||
|
} finally {
|
||||||
|
authenticating.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化Playground(加载编辑器等)
|
||||||
|
const initPlayground = async () => {
|
||||||
|
await initMonacoTypes();
|
||||||
|
|
||||||
|
// 加载保存的代码
|
||||||
|
const saved = localStorage.getItem('playground_code');
|
||||||
|
if (saved) {
|
||||||
|
jsCode.value = saved;
|
||||||
|
} else {
|
||||||
|
jsCode.value = exampleCode;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 代码变化处理
|
// 代码变化处理
|
||||||
const onCodeChange = (value) => {
|
const onCodeChange = (value) => {
|
||||||
jsCode.value = value;
|
jsCode.value = value;
|
||||||
@@ -867,7 +792,6 @@ function parseById(shareLinkInfo, http, logger) {
|
|||||||
// 加载示例代码
|
// 加载示例代码
|
||||||
const loadTemplate = () => {
|
const loadTemplate = () => {
|
||||||
jsCode.value = exampleCode;
|
jsCode.value = exampleCode;
|
||||||
ElMessage.success('已加载JavaScript示例代码');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 格式化代码
|
// 格式化代码
|
||||||
@@ -901,11 +825,8 @@ function parseById(shareLinkInfo, http, logger) {
|
|||||||
const clearCode = () => {
|
const clearCode = () => {
|
||||||
jsCode.value = '';
|
jsCode.value = '';
|
||||||
testResult.value = null;
|
testResult.value = null;
|
||||||
compiledES5Code.value = '';
|
|
||||||
compileStatus.value = { success: true, errors: [] };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 语言切换处理
|
|
||||||
// 执行测试
|
// 执行测试
|
||||||
const executeTest = async () => {
|
const executeTest = async () => {
|
||||||
if (!jsCode.value.trim()) {
|
if (!jsCode.value.trim()) {
|
||||||
@@ -951,7 +872,7 @@ function parseById(shareLinkInfo, http, logger) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await playgroundApi.testScript(
|
const result = await playgroundApi.testScript(
|
||||||
jsCode.value, // 直接使用JavaScript代码
|
jsCode.value,
|
||||||
testParams.value.shareUrl,
|
testParams.value.shareUrl,
|
||||||
testParams.value.pwd,
|
testParams.value.pwd,
|
||||||
testParams.value.method
|
testParams.value.method
|
||||||
@@ -1359,24 +1280,53 @@ curl "${baseUrl}/json/parser?url=${encodeURIComponent(exampleUrl)}"</pre>
|
|||||||
};
|
};
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// 初始化移动端检测
|
|
||||||
updateIsMobile();
|
|
||||||
window.addEventListener('resize', updateIsMobile);
|
|
||||||
|
|
||||||
// 检查认证状态
|
|
||||||
const isAuthed = await checkAuthStatus();
|
|
||||||
|
|
||||||
// 如果已认证,初始化playground
|
|
||||||
if (isAuthed) {
|
|
||||||
await initPlayground();
|
|
||||||
} else {
|
|
||||||
// 未认证,停止加载动画,显示密码输入
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
await nextTick();
|
await nextTick();
|
||||||
checkDarkMode();
|
checkDarkMode();
|
||||||
|
|
||||||
|
// 首先获取Playground状态
|
||||||
|
await fetchStatus();
|
||||||
|
statusLoading.value = false;
|
||||||
|
|
||||||
|
// 如果未启用,直接返回
|
||||||
|
if (!enabled.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果不需要密码或已认证,初始化Playground
|
||||||
|
if (!needPassword.value || authed.value) {
|
||||||
|
await initPlayground();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载保存的主题
|
||||||
|
await nextTick();
|
||||||
|
const savedTheme = localStorage.getItem('playground_theme');
|
||||||
|
if (savedTheme) {
|
||||||
|
currentTheme.value = savedTheme;
|
||||||
|
const theme = themes.find(t => t.name === savedTheme);
|
||||||
|
if (theme && document.documentElement && document.body) {
|
||||||
|
await nextTick();
|
||||||
|
if (theme.page === 'dark') {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
document.body.classList.add('dark-theme');
|
||||||
|
document.body.style.backgroundColor = '#0a0a0a';
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('dark');
|
||||||
|
document.body.classList.remove('dark-theme');
|
||||||
|
document.body.style.backgroundColor = '#f0f2f5';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载保存的折叠状态
|
||||||
|
const savedCollapsed = localStorage.getItem('playground_collapsed_panels');
|
||||||
|
if (savedCollapsed) {
|
||||||
|
try {
|
||||||
|
collapsedPanels.value = JSON.parse(savedCollapsed);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('加载折叠状态失败', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 监听主题变化
|
// 监听主题变化
|
||||||
if (document.documentElement) {
|
if (document.documentElement) {
|
||||||
const observer = new MutationObserver(() => {
|
const observer = new MutationObserver(() => {
|
||||||
@@ -1392,12 +1342,7 @@ curl "${baseUrl}/json/parser?url=${encodeURIComponent(exampleUrl)}"</pre>
|
|||||||
updateSplitpanesStyle();
|
updateSplitpanesStyle();
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
window.removeEventListener('resize', updateIsMobile);
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
LANGUAGE,
|
|
||||||
editorRef,
|
editorRef,
|
||||||
jsCode,
|
jsCode,
|
||||||
testParams,
|
testParams,
|
||||||
@@ -1405,22 +1350,7 @@ curl "${baseUrl}/json/parser?url=${encodeURIComponent(exampleUrl)}"</pre>
|
|||||||
testing,
|
testing,
|
||||||
isDarkMode,
|
isDarkMode,
|
||||||
editorTheme,
|
editorTheme,
|
||||||
shouldShowAuthUI,
|
|
||||||
editorOptions,
|
editorOptions,
|
||||||
// 加载和认证
|
|
||||||
loading,
|
|
||||||
loadProgress,
|
|
||||||
loadingMessage,
|
|
||||||
authChecking,
|
|
||||||
authed,
|
|
||||||
inputPassword,
|
|
||||||
authError,
|
|
||||||
authLoading,
|
|
||||||
checkAuthStatus,
|
|
||||||
submitPassword,
|
|
||||||
// 移动端
|
|
||||||
isMobile,
|
|
||||||
updateIsMobile,
|
|
||||||
onCodeChange,
|
onCodeChange,
|
||||||
loadTemplate,
|
loadTemplate,
|
||||||
formatCode,
|
formatCode,
|
||||||
@@ -1445,6 +1375,17 @@ curl "${baseUrl}/json/parser?url=${encodeURIComponent(exampleUrl)}"</pre>
|
|||||||
helpCollapseActive,
|
helpCollapseActive,
|
||||||
consoleLogs,
|
consoleLogs,
|
||||||
clearConsoleLogs,
|
clearConsoleLogs,
|
||||||
|
// Playground状态相关
|
||||||
|
statusLoading,
|
||||||
|
enabled,
|
||||||
|
needPassword,
|
||||||
|
authed,
|
||||||
|
password,
|
||||||
|
authError,
|
||||||
|
authenticating,
|
||||||
|
fetchStatus,
|
||||||
|
submitPassword,
|
||||||
|
initPlayground,
|
||||||
// 新增功能
|
// 新增功能
|
||||||
collapsedPanels,
|
collapsedPanels,
|
||||||
togglePanel,
|
togglePanel,
|
||||||
@@ -1467,6 +1408,27 @@ curl "${baseUrl}/json/parser?url=${encodeURIComponent(exampleUrl)}"</pre>
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
/* Password container styles */
|
||||||
|
.password-container {
|
||||||
|
text-align: center;
|
||||||
|
padding: 60px 20px;
|
||||||
|
min-height: 400px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-container h2 {
|
||||||
|
font-size: 28px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .password-container {
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
}
|
||||||
|
|
||||||
/* API示例对话框样式 */
|
/* API示例对话框样式 */
|
||||||
.api-example-dialog {
|
.api-example-dialog {
|
||||||
width: 80%;
|
width: 80%;
|
||||||
@@ -1525,154 +1487,6 @@ body.dark-theme .splitpanes__splitter:hover,
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* ===== 加载动画和进度条 ===== */
|
|
||||||
.playground-loading-overlay {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
background: rgba(255, 255, 255, 0.98);
|
|
||||||
z-index: 10000;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
backdrop-filter: blur(5px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark-theme .playground-loading-overlay {
|
|
||||||
background: rgba(10, 10, 10, 0.98);
|
|
||||||
}
|
|
||||||
|
|
||||||
.playground-loading-card {
|
|
||||||
width: 320px;
|
|
||||||
padding: 30px 40px;
|
|
||||||
background: #fff;
|
|
||||||
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.12);
|
|
||||||
border-radius: 12px;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark-theme .playground-loading-card {
|
|
||||||
background: #1f1f1f;
|
|
||||||
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-icon {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
color: var(--el-color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-text {
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--el-text-color-primary);
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-bar {
|
|
||||||
width: 100%;
|
|
||||||
height: 6px;
|
|
||||||
background: var(--el-fill-color-light);
|
|
||||||
border-radius: 3px;
|
|
||||||
margin: 12px 0;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-bar-inner {
|
|
||||||
height: 100%;
|
|
||||||
background: linear-gradient(90deg, var(--el-color-primary) 0%, var(--el-color-primary-light-3) 100%);
|
|
||||||
transition: width 0.3s ease;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-percent {
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--el-color-primary);
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-details {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
min-height: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== 认证界面 ===== */
|
|
||||||
.playground-auth-loading {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
background: var(--el-bg-color);
|
|
||||||
z-index: 10000;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 16px;
|
|
||||||
color: var(--el-text-color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.playground-auth-overlay {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
z-index: 10000;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.playground-auth-card {
|
|
||||||
width: 400px;
|
|
||||||
padding: 40px;
|
|
||||||
background: #fff;
|
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
|
|
||||||
border-radius: 16px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark-theme .playground-auth-card {
|
|
||||||
background: #1f1f1f;
|
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-icon {
|
|
||||||
margin-bottom: 24px;
|
|
||||||
color: var(--el-color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-title {
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--el-text-color-primary);
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-subtitle {
|
|
||||||
font-size: 14px;
|
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-input {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-error {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 6px;
|
|
||||||
color: var(--el-color-danger);
|
|
||||||
font-size: 13px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-button {
|
|
||||||
width: 100%;
|
|
||||||
height: 44px;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== 容器布局 ===== */
|
/* ===== 容器布局 ===== */
|
||||||
.playground-container {
|
.playground-container {
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
@@ -2636,40 +2450,6 @@ html.dark .playground-container .splitpanes__splitter:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ===== 响应式布局 ===== */
|
/* ===== 响应式布局 ===== */
|
||||||
/* 移动端纵向布局 */
|
|
||||||
.playground-container.is-mobile .splitpanes.mobile-vertical {
|
|
||||||
flex-direction: column !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.playground-container.is-mobile .splitpanes--horizontal > .splitpanes__splitter {
|
|
||||||
height: 6px;
|
|
||||||
width: 100%;
|
|
||||||
cursor: row-resize;
|
|
||||||
margin: 5px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.playground-container.is-mobile .editor-pane,
|
|
||||||
.playground-container.is-mobile .test-pane {
|
|
||||||
width: 100% !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.playground-container.is-mobile .test-pane {
|
|
||||||
margin-left: 0 !important;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.playground-container.is-mobile .playground-loading-card {
|
|
||||||
width: 90%;
|
|
||||||
max-width: 320px;
|
|
||||||
padding: 24px 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.playground-container.is-mobile .playground-auth-card {
|
|
||||||
width: 90%;
|
|
||||||
max-width: 400px;
|
|
||||||
padding: 30px 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 1200px) {
|
@media screen and (max-width: 1200px) {
|
||||||
.splitpanes {
|
.splitpanes {
|
||||||
min-height: 400px;
|
min-height: 400px;
|
||||||
@@ -2712,10 +2492,6 @@ html.dark .playground-container .splitpanes__splitter:hover {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.splitpanes {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== 改进的滚动条样式 ===== */
|
/* ===== 改进的滚动条样式 ===== */
|
||||||
|
|||||||
293
web-service/doc/PLAYGROUND_ACCESS_CONTROL.md
Normal file
293
web-service/doc/PLAYGROUND_ACCESS_CONTROL.md
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
# Playground 访问控制配置指南
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
Playground 演练场是一个用于编写、测试和发布 JavaScript 解析脚本的在线开发环境。为了提升安全性,现在支持灵活的访问控制配置。
|
||||||
|
|
||||||
|
## 配置说明
|
||||||
|
|
||||||
|
在 `web-service/src/main/resources/app-dev.yml` 文件中添加以下配置:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Playground演练场配置
|
||||||
|
playground:
|
||||||
|
# 是否启用Playground,默认关闭
|
||||||
|
enabled: false
|
||||||
|
# 访问密码,可选。仅在enabled=true时生效
|
||||||
|
# 为空时表示公开访问,不需要密码
|
||||||
|
password: ""
|
||||||
|
```
|
||||||
|
|
||||||
|
### 配置参数
|
||||||
|
|
||||||
|
#### `enabled`
|
||||||
|
- **类型**: `boolean`
|
||||||
|
- **默认值**: `false`
|
||||||
|
- **说明**: 控制 Playground 功能是否启用
|
||||||
|
- `false`: Playground 完全关闭,页面和所有相关 API 均不可访问
|
||||||
|
- `true`: Playground 启用,可以正常使用
|
||||||
|
|
||||||
|
#### `password`
|
||||||
|
- **类型**: `string`
|
||||||
|
- **默认值**: `""` (空字符串)
|
||||||
|
- **说明**: 访问密码(仅在 `enabled = true` 时生效)
|
||||||
|
- 空字符串或 `null`: 公开访问模式,无需密码
|
||||||
|
- 非空字符串: 需要输入正确密码才能访问
|
||||||
|
|
||||||
|
## 访问模式
|
||||||
|
|
||||||
|
### 1. 完全禁用模式 (enabled = false)
|
||||||
|
|
||||||
|
这是**默认且推荐的生产环境配置**。
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
playground:
|
||||||
|
enabled: false
|
||||||
|
password: ""
|
||||||
|
```
|
||||||
|
|
||||||
|
**行为**:
|
||||||
|
- `/playground` 页面显示"Playground未开启"提示
|
||||||
|
- 所有 Playground 相关 API(`/v2/playground/**`)返回错误提示
|
||||||
|
- 最安全的模式,适合生产环境
|
||||||
|
|
||||||
|
**适用场景**:
|
||||||
|
- 生产环境部署
|
||||||
|
- 不需要使用 Playground 功能的情况
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. 密码保护模式 (enabled = true, password 非空)
|
||||||
|
|
||||||
|
这是**公网环境的推荐配置**。
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
playground:
|
||||||
|
enabled: true
|
||||||
|
password: "your_strong_password_here"
|
||||||
|
```
|
||||||
|
|
||||||
|
**行为**:
|
||||||
|
- 访问 `/playground` 页面时会显示密码输入框
|
||||||
|
- 需要输入正确密码才能进入编辑器
|
||||||
|
- 密码验证通过后,在当前会话中保持已登录状态
|
||||||
|
- 会话基于客户端 IP 或 Cookie 进行识别
|
||||||
|
|
||||||
|
**适用场景**:
|
||||||
|
- 需要在公网环境使用 Playground
|
||||||
|
- 多人共享访问,但需要访问控制
|
||||||
|
- 团队协作开发环境
|
||||||
|
|
||||||
|
**示例**:
|
||||||
|
```yaml
|
||||||
|
playground:
|
||||||
|
enabled: true
|
||||||
|
password: "MySecure@Password123"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. 公开访问模式 (enabled = true, password 为空)
|
||||||
|
|
||||||
|
⚠️ **仅建议在本地开发或内网环境使用**。
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
playground:
|
||||||
|
enabled: true
|
||||||
|
password: ""
|
||||||
|
```
|
||||||
|
|
||||||
|
**行为**:
|
||||||
|
- Playground 对所有访问者开放
|
||||||
|
- 无需输入密码即可使用所有功能
|
||||||
|
- 页面加载后直接显示编辑器
|
||||||
|
|
||||||
|
**适用场景**:
|
||||||
|
- 本地开发环境(`localhost`)
|
||||||
|
- 完全隔离的内网环境
|
||||||
|
- 个人使用且不暴露在公网
|
||||||
|
|
||||||
|
**⚠️ 安全警告**:
|
||||||
|
> **强烈不建议在公网环境下使用此配置!**
|
||||||
|
>
|
||||||
|
> 公开访问模式允许任何人:
|
||||||
|
> - 执行任意 JavaScript 代码(虽然有沙箱限制)
|
||||||
|
> - 发布解析器脚本到数据库
|
||||||
|
> - 查看、修改、删除已有的解析器
|
||||||
|
> - 可能导致服务器资源被滥用
|
||||||
|
>
|
||||||
|
> 如果必须在公网环境开启 Playground,请务必:
|
||||||
|
> 1. 设置一个足够复杂的密码
|
||||||
|
> 2. 定期更换密码
|
||||||
|
> 3. 通过防火墙或网关限制访问来源(IP 白名单)
|
||||||
|
> 4. 启用访问日志监控
|
||||||
|
> 5. 考虑使用 HTTPS 加密传输
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 技术实现
|
||||||
|
|
||||||
|
### 后端实现
|
||||||
|
|
||||||
|
#### 状态检查 API
|
||||||
|
```
|
||||||
|
GET /v2/playground/status
|
||||||
|
```
|
||||||
|
|
||||||
|
返回:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"needPassword": true,
|
||||||
|
"authed": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 登录 API
|
||||||
|
```
|
||||||
|
POST /v2/playground/login
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"password": "your_password"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 认证机制
|
||||||
|
- 使用 Vert.x 的 `SharedData` 存储认证状态
|
||||||
|
- 基于客户端 IP 或 Cookie 中的 session ID 识别用户
|
||||||
|
- 密码验证通过后在 `playground_auth` Map 中记录
|
||||||
|
|
||||||
|
#### 受保护的端点
|
||||||
|
所有以下端点都需要通过访问控制检查:
|
||||||
|
- `POST /v2/playground/test` - 执行测试
|
||||||
|
- `GET /v2/playground/types.js` - 获取类型定义
|
||||||
|
- `GET /v2/playground/parsers` - 获取解析器列表
|
||||||
|
- `POST /v2/playground/parsers` - 保存解析器
|
||||||
|
- `PUT /v2/playground/parsers/:id` - 更新解析器
|
||||||
|
- `DELETE /v2/playground/parsers/:id` - 删除解析器
|
||||||
|
- `GET /v2/playground/parsers/:id` - 获取解析器详情
|
||||||
|
|
||||||
|
### 前端实现
|
||||||
|
|
||||||
|
#### 状态检查流程
|
||||||
|
1. 页面加载时调用 `/v2/playground/status`
|
||||||
|
2. 根据返回的状态显示不同界面:
|
||||||
|
- `enabled = false`: 显示"未开启"提示
|
||||||
|
- `enabled = true & needPassword = true & !authed`: 显示密码输入框
|
||||||
|
- `enabled = true & (!needPassword || authed)`: 加载编辑器
|
||||||
|
|
||||||
|
#### 密码输入界面
|
||||||
|
- 密码输入框(支持显示/隐藏密码)
|
||||||
|
- 验证按钮
|
||||||
|
- 错误提示
|
||||||
|
- 支持回车键提交
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 配置示例
|
||||||
|
|
||||||
|
### 示例 1: 生产环境(推荐)
|
||||||
|
```yaml
|
||||||
|
playground:
|
||||||
|
enabled: false
|
||||||
|
password: ""
|
||||||
|
```
|
||||||
|
|
||||||
|
### 示例 2: 公网开发环境
|
||||||
|
```yaml
|
||||||
|
playground:
|
||||||
|
enabled: true
|
||||||
|
password: "Str0ng!P@ssw0rd#2024"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 示例 3: 本地开发
|
||||||
|
```yaml
|
||||||
|
playground:
|
||||||
|
enabled: true
|
||||||
|
password: ""
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### Q1: 忘记密码怎么办?
|
||||||
|
**A**: 直接修改 `app-dev.yml` 配置文件中的 `password` 值,然后重启服务即可。
|
||||||
|
|
||||||
|
### Q2: 可以动态修改密码吗?
|
||||||
|
**A**: 目前需要修改配置文件并重启服务。密码在服务启动时加载。
|
||||||
|
|
||||||
|
### Q3: 多个用户可以同时使用吗?
|
||||||
|
**A**: 可以。密码验证通过后,每个客户端都会保持独立的认证状态。
|
||||||
|
|
||||||
|
### Q4: 公开模式下有什么安全限制吗?
|
||||||
|
**A**:
|
||||||
|
- 代码执行有大小限制(128KB)
|
||||||
|
- JavaScript 在 Nashorn 沙箱中运行
|
||||||
|
- 最多创建 100 个解析器
|
||||||
|
- 但仍然建议在内网或本地使用
|
||||||
|
|
||||||
|
### Q5: 密码会明文传输吗?
|
||||||
|
**A**: 如果使用 HTTP,是的。强烈建议配置 HTTPS 以加密传输。
|
||||||
|
|
||||||
|
### Q6: 会话会过期吗?
|
||||||
|
**A**: 当前实现基于内存存储,服务重启后需要重新登录。单个会话不会主动过期。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 安全建议
|
||||||
|
|
||||||
|
### 🔒 强密码要求
|
||||||
|
如果启用密码保护,请确保密码符合以下要求:
|
||||||
|
- 长度至少 12 位
|
||||||
|
- 包含大小写字母、数字和特殊字符
|
||||||
|
- 避免使用常见单词或生日等个人信息
|
||||||
|
- 定期更换密码(建议每季度一次)
|
||||||
|
|
||||||
|
### 🌐 网络安全措施
|
||||||
|
- 在生产环境使用 HTTPS
|
||||||
|
- 配置防火墙或网关限制访问来源
|
||||||
|
- 使用反向代理(如 Nginx)添加额外的安全层
|
||||||
|
- 启用访问日志和监控
|
||||||
|
|
||||||
|
### 📝 最佳实践
|
||||||
|
1. **默认禁用**: 除非必要,保持 `enabled: false`
|
||||||
|
2. **密码保护**: 公网环境务必设置密码
|
||||||
|
3. **访问控制**: 结合 IP 白名单限制访问
|
||||||
|
4. **定期审计**: 检查已创建的解析器脚本
|
||||||
|
5. **监控告警**: 设置异常访问告警机制
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 迁移指南
|
||||||
|
|
||||||
|
### 从旧版本升级
|
||||||
|
|
||||||
|
如果你的系统之前没有 Playground 访问控制,升级后:
|
||||||
|
|
||||||
|
1. **默认行为**: Playground 将被禁用(`enabled: false`)
|
||||||
|
2. **如需启用**: 在配置文件中添加上述配置
|
||||||
|
3. **兼容性**: 完全向后兼容,不影响其他功能
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 更新日志
|
||||||
|
|
||||||
|
### v0.1.8
|
||||||
|
- 添加 Playground 访问控制功能
|
||||||
|
- 支持三种访问模式:禁用、密码保护、公开访问
|
||||||
|
- 前端添加密码输入界面
|
||||||
|
- 所有 Playground API 受保护
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 技术支持
|
||||||
|
|
||||||
|
如有问题,请访问:
|
||||||
|
- GitHub Issues: https://github.com/qaiu/netdisk-fast-download/issues
|
||||||
|
- 项目文档: https://github.com/qaiu/netdisk-fast-download
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**最后更新**: 2025-12-07
|
||||||
@@ -3,14 +3,9 @@ package cn.qaiu.lz;
|
|||||||
import cn.qaiu.WebClientVertxInit;
|
import cn.qaiu.WebClientVertxInit;
|
||||||
import cn.qaiu.db.pool.JDBCPoolInit;
|
import cn.qaiu.db.pool.JDBCPoolInit;
|
||||||
import cn.qaiu.lz.common.cache.CacheConfigLoader;
|
import cn.qaiu.lz.common.cache.CacheConfigLoader;
|
||||||
|
import cn.qaiu.lz.common.config.PlaygroundConfig;
|
||||||
import cn.qaiu.lz.common.interceptorImpl.RateLimiter;
|
import cn.qaiu.lz.common.interceptorImpl.RateLimiter;
|
||||||
import cn.qaiu.lz.web.config.PlaygroundConfig;
|
|
||||||
import cn.qaiu.lz.web.service.DbService;
|
|
||||||
import cn.qaiu.parser.custom.CustomParserConfig;
|
|
||||||
import cn.qaiu.parser.custom.CustomParserRegistry;
|
|
||||||
import cn.qaiu.parser.customjs.JsScriptMetadataParser;
|
|
||||||
import cn.qaiu.vx.core.Deploy;
|
import cn.qaiu.vx.core.Deploy;
|
||||||
import cn.qaiu.vx.core.util.AsyncServiceUtil;
|
|
||||||
import cn.qaiu.vx.core.util.ConfigConstant;
|
import cn.qaiu.vx.core.util.ConfigConstant;
|
||||||
import cn.qaiu.vx.core.util.VertxHolder;
|
import cn.qaiu.vx.core.util.VertxHolder;
|
||||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||||
@@ -18,7 +13,6 @@ import io.vertx.core.json.JsonArray;
|
|||||||
import io.vertx.core.json.JsonObject;
|
import io.vertx.core.json.JsonObject;
|
||||||
import io.vertx.core.json.jackson.DatabindCodec;
|
import io.vertx.core.json.jackson.DatabindCodec;
|
||||||
import io.vertx.core.shareddata.LocalMap;
|
import io.vertx.core.shareddata.LocalMap;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.apache.commons.lang3.time.DateFormatUtils;
|
import org.apache.commons.lang3.time.DateFormatUtils;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
@@ -32,7 +26,6 @@ import static cn.qaiu.vx.core.util.ConfigConstant.LOCAL;
|
|||||||
* <br>Create date 2021-05-08 13:00:01
|
* <br>Create date 2021-05-08 13:00:01
|
||||||
* @author qaiu yyzy
|
* @author qaiu yyzy
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
|
||||||
public class AppMain {
|
public class AppMain {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
@@ -62,10 +55,6 @@ public class AppMain {
|
|||||||
VertxHolder.getVertxInstance().setTimer(1000, id -> {
|
VertxHolder.getVertxInstance().setTimer(1000, id -> {
|
||||||
System.out.println(DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss.SSS"));
|
System.out.println(DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss.SSS"));
|
||||||
System.out.println("数据库连接成功");
|
System.out.println("数据库连接成功");
|
||||||
|
|
||||||
// 加载演练场解析器
|
|
||||||
loadPlaygroundParsers();
|
|
||||||
|
|
||||||
String addr = jsonObject.getJsonObject(ConfigConstant.SERVER).getString("domainName");
|
String addr = jsonObject.getJsonObject(ConfigConstant.SERVER).getString("domainName");
|
||||||
System.out.println("启动成功: \n本地服务地址: " + addr);
|
System.out.println("启动成功: \n本地服务地址: " + addr);
|
||||||
});
|
});
|
||||||
@@ -101,43 +90,9 @@ public class AppMain {
|
|||||||
localMap.put(ConfigConstant.AUTHS, auths);
|
localMap.put(ConfigConstant.AUTHS, auths);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 演练场配置
|
// Playground配置
|
||||||
PlaygroundConfig.loadFromJson(jsonObject);
|
if (jsonObject.containsKey("playground")) {
|
||||||
}
|
PlaygroundConfig.init(jsonObject.getJsonObject("playground"));
|
||||||
|
|
||||||
/**
|
|
||||||
* 在启动时加载所有已发布的演练场解析器
|
|
||||||
*/
|
|
||||||
private static void loadPlaygroundParsers() {
|
|
||||||
DbService dbService = AsyncServiceUtil.getAsyncServiceInstance(DbService.class);
|
|
||||||
|
|
||||||
dbService.getPlaygroundParserList().onSuccess(result -> {
|
|
||||||
JsonArray parsers = result.getJsonArray("data");
|
|
||||||
if (parsers != null) {
|
|
||||||
int loadedCount = 0;
|
|
||||||
for (int i = 0; i < parsers.size(); i++) {
|
|
||||||
JsonObject parser = parsers.getJsonObject(i);
|
|
||||||
|
|
||||||
// 只注册已启用的解析器
|
|
||||||
if (parser.getBoolean("enabled", false)) {
|
|
||||||
try {
|
|
||||||
String jsCode = parser.getString("jsCode");
|
|
||||||
CustomParserConfig config = JsScriptMetadataParser.parseScript(jsCode);
|
|
||||||
CustomParserRegistry.register(config);
|
|
||||||
loadedCount++;
|
|
||||||
log.info("已加载演练场解析器: {} ({})",
|
|
||||||
config.getDisplayName(), config.getType());
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("加载演练场解析器失败: {}", parser.getString("name"), e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.info("演练场解析器加载完成,共加载 {} 个解析器", loadedCount);
|
|
||||||
} else {
|
|
||||||
log.info("未找到已发布的演练场解析器");
|
|
||||||
}
|
|
||||||
}).onFailure(e -> {
|
|
||||||
log.error("加载演练场解析器列表失败", e);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package cn.qaiu.lz.common.config;
|
||||||
|
|
||||||
|
import io.vertx.core.json.JsonObject;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Playground配置加载器
|
||||||
|
*
|
||||||
|
* @author <a href="https://qaiu.top">QAIU</a>
|
||||||
|
*/
|
||||||
|
public class PlaygroundConfig {
|
||||||
|
private static boolean enabled = false;
|
||||||
|
private static String password = "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化配置
|
||||||
|
* @param config 配置对象
|
||||||
|
*/
|
||||||
|
public static void init(JsonObject config) {
|
||||||
|
if (config == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
enabled = config.getBoolean("enabled", false);
|
||||||
|
password = config.getString("password", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用Playground
|
||||||
|
*/
|
||||||
|
public static boolean isEnabled() {
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取密码
|
||||||
|
*/
|
||||||
|
public static String getPassword() {
|
||||||
|
return password;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否需要密码
|
||||||
|
*/
|
||||||
|
public static boolean hasPassword() {
|
||||||
|
return StringUtils.isNotBlank(password);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
package cn.qaiu.lz.web.config;
|
|
||||||
|
|
||||||
import io.vertx.core.json.JsonObject;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JS演练场配置
|
|
||||||
*
|
|
||||||
* @author <a href="https://qaiu.top">QAIU</a>
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
@Slf4j
|
|
||||||
public class PlaygroundConfig {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 单例实例
|
|
||||||
*/
|
|
||||||
private static PlaygroundConfig instance;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否公开模式(不需要密码)
|
|
||||||
* 默认false,需要密码访问
|
|
||||||
*/
|
|
||||||
private boolean isPublic = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 访问密码
|
|
||||||
* 默认密码:nfd_playground_2024
|
|
||||||
*/
|
|
||||||
private String password = "nfd_playground_2024";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 私有构造函数
|
|
||||||
*/
|
|
||||||
private PlaygroundConfig() {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取单例实例
|
|
||||||
*/
|
|
||||||
public static PlaygroundConfig getInstance() {
|
|
||||||
if (instance == null) {
|
|
||||||
synchronized (PlaygroundConfig.class) {
|
|
||||||
if (instance == null) {
|
|
||||||
instance = new PlaygroundConfig();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从JsonObject加载配置
|
|
||||||
*/
|
|
||||||
public static void loadFromJson(JsonObject config) {
|
|
||||||
PlaygroundConfig cfg = getInstance();
|
|
||||||
if (config != null && config.containsKey("playground")) {
|
|
||||||
JsonObject playgroundConfig = config.getJsonObject("playground");
|
|
||||||
cfg.isPublic = playgroundConfig.getBoolean("public", false);
|
|
||||||
cfg.password = playgroundConfig.getString("password", "nfd_playground_2024");
|
|
||||||
|
|
||||||
log.info("Playground配置已加载: public={}, password={}",
|
|
||||||
cfg.isPublic, cfg.isPublic ? "N/A" : "已设置");
|
|
||||||
|
|
||||||
if (!cfg.isPublic && "nfd_playground_2024".equals(cfg.password)) {
|
|
||||||
log.warn("⚠️ 警告:您正在使用默认密码,建议修改配置文件中的 playground.password 以确保安全!");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.info("未找到playground配置,使用默认值: public=false");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
package cn.qaiu.lz.web.controller;
|
package cn.qaiu.lz.web.controller;
|
||||||
|
|
||||||
import cn.qaiu.entity.ShareLinkInfo;
|
import cn.qaiu.entity.ShareLinkInfo;
|
||||||
import cn.qaiu.lz.web.config.PlaygroundConfig;
|
import cn.qaiu.lz.common.config.PlaygroundConfig;
|
||||||
import cn.qaiu.lz.web.model.PlaygroundTestResp;
|
import cn.qaiu.lz.web.model.PlaygroundTestResp;
|
||||||
import cn.qaiu.lz.web.service.DbService;
|
import cn.qaiu.lz.web.service.DbService;
|
||||||
import cn.qaiu.parser.ParserCreate;
|
import cn.qaiu.parser.ParserCreate;
|
||||||
import cn.qaiu.parser.custom.CustomParserRegistry;
|
|
||||||
import cn.qaiu.parser.customjs.JsPlaygroundExecutor;
|
import cn.qaiu.parser.customjs.JsPlaygroundExecutor;
|
||||||
import cn.qaiu.parser.customjs.JsPlaygroundLogger;
|
import cn.qaiu.parser.customjs.JsPlaygroundLogger;
|
||||||
import cn.qaiu.parser.customjs.JsScriptMetadataParser;
|
import cn.qaiu.parser.customjs.JsScriptMetadataParser;
|
||||||
@@ -15,13 +14,13 @@ import cn.qaiu.vx.core.enums.RouteMethod;
|
|||||||
import cn.qaiu.vx.core.model.JsonResult;
|
import cn.qaiu.vx.core.model.JsonResult;
|
||||||
import cn.qaiu.vx.core.util.AsyncServiceUtil;
|
import cn.qaiu.vx.core.util.AsyncServiceUtil;
|
||||||
import cn.qaiu.vx.core.util.ResponseUtil;
|
import cn.qaiu.vx.core.util.ResponseUtil;
|
||||||
|
import cn.qaiu.vx.core.util.VertxHolder;
|
||||||
import io.vertx.core.Future;
|
import io.vertx.core.Future;
|
||||||
import io.vertx.core.Promise;
|
import io.vertx.core.Promise;
|
||||||
import io.vertx.core.http.HttpServerRequest;
|
import io.vertx.core.http.HttpServerRequest;
|
||||||
import io.vertx.core.http.HttpServerResponse;
|
import io.vertx.core.http.HttpServerResponse;
|
||||||
import io.vertx.core.json.JsonObject;
|
import io.vertx.core.json.JsonObject;
|
||||||
import io.vertx.ext.web.RoutingContext;
|
import io.vertx.ext.web.RoutingContext;
|
||||||
import io.vertx.ext.web.Session;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
@@ -31,8 +30,6 @@ import java.io.InputStreamReader;
|
|||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -47,93 +44,179 @@ public class PlaygroundApi {
|
|||||||
|
|
||||||
private static final int MAX_PARSER_COUNT = 100;
|
private static final int MAX_PARSER_COUNT = 100;
|
||||||
private static final int MAX_CODE_LENGTH = 128 * 1024; // 128KB 代码长度限制
|
private static final int MAX_CODE_LENGTH = 128 * 1024; // 128KB 代码长度限制
|
||||||
private static final String SESSION_AUTH_KEY = "playgroundAuthed";
|
private static final String AUTH_SESSION_KEY = "playground_authed_";
|
||||||
private final DbService dbService = AsyncServiceUtil.getAsyncServiceInstance(DbService.class);
|
private final DbService dbService = AsyncServiceUtil.getAsyncServiceInstance(DbService.class);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查Playground访问权限
|
* 获取Playground状态
|
||||||
*/
|
*
|
||||||
private boolean checkAuth(RoutingContext ctx) {
|
* @param ctx 路由上下文
|
||||||
PlaygroundConfig config = PlaygroundConfig.getInstance();
|
* @return 状态信息
|
||||||
|
|
||||||
// 如果是公开模式,直接允许访问
|
|
||||||
if (config.isPublic()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 否则检查Session中的认证状态
|
|
||||||
Session session = ctx.session();
|
|
||||||
if (session == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Boolean authed = session.get(SESSION_AUTH_KEY);
|
|
||||||
return authed != null && authed;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取Playground状态(是否需要认证)
|
|
||||||
*/
|
*/
|
||||||
@RouteMapping(value = "/status", method = RouteMethod.GET)
|
@RouteMapping(value = "/status", method = RouteMethod.GET)
|
||||||
public Future<JsonObject> getStatus(RoutingContext ctx) {
|
public Future<JsonObject> getStatus(RoutingContext ctx) {
|
||||||
PlaygroundConfig config = PlaygroundConfig.getInstance();
|
Promise<JsonObject> promise = Promise.promise();
|
||||||
boolean authed = checkAuth(ctx);
|
|
||||||
|
try {
|
||||||
|
boolean enabled = PlaygroundConfig.isEnabled();
|
||||||
|
boolean needPassword = enabled && PlaygroundConfig.hasPassword();
|
||||||
|
boolean authed = false;
|
||||||
|
|
||||||
|
if (enabled) {
|
||||||
|
if (!needPassword) {
|
||||||
|
// 公开模式,无需认证
|
||||||
|
authed = true;
|
||||||
|
} else {
|
||||||
|
// 检查是否已认证
|
||||||
|
String clientId = getClientId(ctx.request());
|
||||||
|
authed = isAuthenticated(clientId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
JsonObject result = new JsonObject()
|
JsonObject result = new JsonObject()
|
||||||
.put("public", config.isPublic())
|
.put("enabled", enabled)
|
||||||
|
.put("needPassword", needPassword)
|
||||||
.put("authed", authed);
|
.put("authed", authed);
|
||||||
|
|
||||||
return Future.succeededFuture(JsonResult.data(result).toJsonObject());
|
promise.complete(JsonResult.data(result).toJsonObject());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("获取Playground状态失败", e);
|
||||||
|
promise.complete(JsonResult.error("获取状态失败: " + e.getMessage()).toJsonObject());
|
||||||
|
}
|
||||||
|
|
||||||
|
return promise.future();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Playground登录
|
* Playground登录
|
||||||
|
*
|
||||||
|
* @param ctx 路由上下文
|
||||||
|
* @return 登录结果
|
||||||
*/
|
*/
|
||||||
@RouteMapping(value = "/login", method = RouteMethod.POST)
|
@RouteMapping(value = "/login", method = RouteMethod.POST)
|
||||||
public Future<JsonObject> login(RoutingContext ctx) {
|
public Future<JsonObject> login(RoutingContext ctx) {
|
||||||
Promise<JsonObject> promise = Promise.promise();
|
Promise<JsonObject> promise = Promise.promise();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
PlaygroundConfig config = PlaygroundConfig.getInstance();
|
if (!PlaygroundConfig.isEnabled()) {
|
||||||
|
promise.complete(JsonResult.error("Playground未开启").toJsonObject());
|
||||||
// 如果是公开模式,直接成功
|
return promise.future();
|
||||||
if (config.isPublic()) {
|
}
|
||||||
Session session = ctx.session();
|
|
||||||
if (session != null) {
|
if (!PlaygroundConfig.hasPassword()) {
|
||||||
session.put(SESSION_AUTH_KEY, true);
|
// 无密码模式,直接标记为已认证
|
||||||
}
|
String clientId = getClientId(ctx.request());
|
||||||
promise.complete(JsonResult.success("公开模式,无需密码").toJsonObject());
|
setAuthenticated(clientId, true);
|
||||||
|
promise.complete(JsonResult.success("登录成功").toJsonObject());
|
||||||
return promise.future();
|
return promise.future();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取密码
|
|
||||||
JsonObject body = ctx.body().asJsonObject();
|
JsonObject body = ctx.body().asJsonObject();
|
||||||
String password = body.getString("password");
|
String password = body.getString("password");
|
||||||
|
|
||||||
if (StringUtils.isBlank(password)) {
|
if (StringUtils.equals(password, PlaygroundConfig.getPassword())) {
|
||||||
promise.complete(JsonResult.error("密码不能为空").toJsonObject());
|
String clientId = getClientId(ctx.request());
|
||||||
return promise.future();
|
setAuthenticated(clientId, true);
|
||||||
}
|
|
||||||
|
|
||||||
// 验证密码
|
|
||||||
if (config.getPassword().equals(password)) {
|
|
||||||
Session session = ctx.session();
|
|
||||||
if (session != null) {
|
|
||||||
session.put(SESSION_AUTH_KEY, true);
|
|
||||||
}
|
|
||||||
promise.complete(JsonResult.success("登录成功").toJsonObject());
|
promise.complete(JsonResult.success("登录成功").toJsonObject());
|
||||||
} else {
|
} else {
|
||||||
promise.complete(JsonResult.error("密码错误").toJsonObject());
|
promise.complete(JsonResult.error("密码错误").toJsonObject());
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("登录失败", e);
|
log.error("Playground登录失败", e);
|
||||||
promise.complete(JsonResult.error("登录失败: " + e.getMessage()).toJsonObject());
|
promise.complete(JsonResult.error("登录失败: " + e.getMessage()).toJsonObject());
|
||||||
}
|
}
|
||||||
|
|
||||||
return promise.future();
|
return promise.future();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查Playground是否启用和已认证
|
||||||
|
*/
|
||||||
|
private void ensurePlaygroundAccess(RoutingContext ctx) {
|
||||||
|
if (!PlaygroundConfig.isEnabled()) {
|
||||||
|
throw new IllegalStateException("Playground未开启");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PlaygroundConfig.hasPassword()) {
|
||||||
|
String clientId = getClientId(ctx.request());
|
||||||
|
if (!isAuthenticated(clientId)) {
|
||||||
|
throw new IllegalStateException("未授权访问Playground");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取客户端唯一标识
|
||||||
|
*/
|
||||||
|
private String getClientId(HttpServerRequest request) {
|
||||||
|
// 优先使用Cookie中的session id,否则使用IP
|
||||||
|
String sessionId = extractSessionIdFromCookie(request);
|
||||||
|
if (sessionId != null) {
|
||||||
|
return sessionId;
|
||||||
|
}
|
||||||
|
return getClientIp(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从Cookie中提取session ID
|
||||||
|
*/
|
||||||
|
private String extractSessionIdFromCookie(HttpServerRequest request) {
|
||||||
|
String cookie = request.getHeader("Cookie");
|
||||||
|
if (cookie == null || !cookie.contains("playground_session=")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final String SESSION_KEY = "playground_session=";
|
||||||
|
int startIndex = cookie.indexOf(SESSION_KEY);
|
||||||
|
if (startIndex == -1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
startIndex += SESSION_KEY.length();
|
||||||
|
int endIndex = cookie.indexOf(";", startIndex);
|
||||||
|
|
||||||
|
if (endIndex != -1) {
|
||||||
|
return cookie.substring(startIndex, endIndex).trim();
|
||||||
|
} else {
|
||||||
|
return cookie.substring(startIndex).trim();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Failed to extract session ID from cookie", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否已认证
|
||||||
|
*/
|
||||||
|
private boolean isAuthenticated(String clientId) {
|
||||||
|
try {
|
||||||
|
Boolean authed = (Boolean) VertxHolder.getVertxInstance()
|
||||||
|
.sharedData()
|
||||||
|
.getLocalMap("playground_auth")
|
||||||
|
.get(AUTH_SESSION_KEY + clientId);
|
||||||
|
return authed != null && authed;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("检查认证状态失败", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置认证状态
|
||||||
|
*/
|
||||||
|
private void setAuthenticated(String clientId, boolean authed) {
|
||||||
|
try {
|
||||||
|
VertxHolder.getVertxInstance()
|
||||||
|
.sharedData()
|
||||||
|
.getLocalMap("playground_auth")
|
||||||
|
.put(AUTH_SESSION_KEY + clientId, authed);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("设置认证状态失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 测试执行JavaScript代码
|
* 测试执行JavaScript代码
|
||||||
*
|
*
|
||||||
@@ -142,14 +225,12 @@ public class PlaygroundApi {
|
|||||||
*/
|
*/
|
||||||
@RouteMapping(value = "/test", method = RouteMethod.POST)
|
@RouteMapping(value = "/test", method = RouteMethod.POST)
|
||||||
public Future<JsonObject> test(RoutingContext ctx) {
|
public Future<JsonObject> test(RoutingContext ctx) {
|
||||||
// 权限检查
|
|
||||||
if (!checkAuth(ctx)) {
|
|
||||||
return Future.succeededFuture(JsonResult.error("未授权访问").toJsonObject());
|
|
||||||
}
|
|
||||||
|
|
||||||
Promise<JsonObject> promise = Promise.promise();
|
Promise<JsonObject> promise = Promise.promise();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 检查访问权限
|
||||||
|
ensurePlaygroundAccess(ctx);
|
||||||
|
|
||||||
JsonObject body = ctx.body().asJsonObject();
|
JsonObject body = ctx.body().asJsonObject();
|
||||||
String jsCode = body.getString("jsCode");
|
String jsCode = body.getString("jsCode");
|
||||||
String shareUrl = body.getString("shareUrl");
|
String shareUrl = body.getString("shareUrl");
|
||||||
@@ -182,32 +263,6 @@ public class PlaygroundApi {
|
|||||||
return promise.future();
|
return promise.future();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== 新增:验证URL匹配 =====
|
|
||||||
try {
|
|
||||||
var config = JsScriptMetadataParser.parseScript(jsCode);
|
|
||||||
Pattern matchPattern = config.getMatchPattern();
|
|
||||||
|
|
||||||
if (matchPattern != null) {
|
|
||||||
Matcher matcher = matchPattern.matcher(shareUrl);
|
|
||||||
if (!matcher.matches()) {
|
|
||||||
promise.complete(JsonObject.mapFrom(PlaygroundTestResp.builder()
|
|
||||||
.success(false)
|
|
||||||
.error("分享链接与脚本的@match规则不匹配\n" +
|
|
||||||
"规则: " + matchPattern.pattern() + "\n" +
|
|
||||||
"链接: " + shareUrl)
|
|
||||||
.build()));
|
|
||||||
return promise.future();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
promise.complete(JsonObject.mapFrom(PlaygroundTestResp.builder()
|
|
||||||
.success(false)
|
|
||||||
.error("解析脚本元数据失败: " + e.getMessage())
|
|
||||||
.build()));
|
|
||||||
return promise.future();
|
|
||||||
}
|
|
||||||
// ===== 验证结束 =====
|
|
||||||
|
|
||||||
// 验证方法类型
|
// 验证方法类型
|
||||||
if (!"parse".equals(method) && !"parseFileList".equals(method) && !"parseById".equals(method)) {
|
if (!"parse".equals(method) && !"parseFileList".equals(method) && !"parseById".equals(method)) {
|
||||||
promise.complete(JsonObject.mapFrom(PlaygroundTestResp.builder()
|
promise.complete(JsonObject.mapFrom(PlaygroundTestResp.builder()
|
||||||
@@ -340,32 +395,34 @@ public class PlaygroundApi {
|
|||||||
/**
|
/**
|
||||||
* 获取types.js文件内容
|
* 获取types.js文件内容
|
||||||
*
|
*
|
||||||
* @param ctx 路由上下文
|
|
||||||
* @param response HTTP响应
|
* @param response HTTP响应
|
||||||
|
* @param ctx 路由上下文
|
||||||
*/
|
*/
|
||||||
@RouteMapping(value = "/types.js", method = RouteMethod.GET)
|
@RouteMapping(value = "/types.js", method = RouteMethod.GET)
|
||||||
public void getTypesJs(RoutingContext ctx, HttpServerResponse response) {
|
public void getTypesJs(HttpServerResponse response, RoutingContext ctx) {
|
||||||
// 权限检查
|
try {
|
||||||
if (!checkAuth(ctx)) {
|
// 检查访问权限
|
||||||
ResponseUtil.fireJsonResultResponse(response, JsonResult.error("未授权访问"));
|
ensurePlaygroundAccess(ctx);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try (InputStream inputStream = getClass().getClassLoader()
|
InputStream inputStream = getClass().getClassLoader()
|
||||||
.getResourceAsStream("custom-parsers/types.js")) {
|
.getResourceAsStream("custom-parsers/types.js");
|
||||||
|
|
||||||
if (inputStream == null) {
|
if (inputStream == null) {
|
||||||
ResponseUtil.fireJsonResultResponse(response, JsonResult.error("types.js文件不存在"));
|
ResponseUtil.fireJsonResultResponse(response, JsonResult.error("types.js文件不存在"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try (inputStream) {
|
||||||
String content = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))
|
String content = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))
|
||||||
.lines()
|
.lines()
|
||||||
.collect(Collectors.joining("\n"));
|
.collect(Collectors.joining("\n"));
|
||||||
|
|
||||||
response.putHeader("Content-Type", "text/javascript; charset=utf-8")
|
response.putHeader("Content-Type", "text/javascript; charset=utf-8")
|
||||||
.end(content);
|
.end(content);
|
||||||
|
}
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
log.error("访问Playground失败", e);
|
||||||
|
ResponseUtil.fireJsonResultResponse(response, JsonResult.error(e.getMessage()));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("读取types.js失败", e);
|
log.error("读取types.js失败", e);
|
||||||
ResponseUtil.fireJsonResultResponse(response, JsonResult.error("读取types.js失败: " + e.getMessage()));
|
ResponseUtil.fireJsonResultResponse(response, JsonResult.error("读取types.js失败: " + e.getMessage()));
|
||||||
@@ -377,11 +434,14 @@ public class PlaygroundApi {
|
|||||||
*/
|
*/
|
||||||
@RouteMapping(value = "/parsers", method = RouteMethod.GET)
|
@RouteMapping(value = "/parsers", method = RouteMethod.GET)
|
||||||
public Future<JsonObject> getParserList(RoutingContext ctx) {
|
public Future<JsonObject> getParserList(RoutingContext ctx) {
|
||||||
// 权限检查
|
try {
|
||||||
if (!checkAuth(ctx)) {
|
// 检查访问权限
|
||||||
return Future.succeededFuture(JsonResult.error("未授权访问").toJsonObject());
|
ensurePlaygroundAccess(ctx);
|
||||||
}
|
|
||||||
return dbService.getPlaygroundParserList();
|
return dbService.getPlaygroundParserList();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("获取解析器列表失败", e);
|
||||||
|
return Future.succeededFuture(JsonResult.error(e.getMessage()).toJsonObject());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -389,14 +449,12 @@ public class PlaygroundApi {
|
|||||||
*/
|
*/
|
||||||
@RouteMapping(value = "/parsers", method = RouteMethod.POST)
|
@RouteMapping(value = "/parsers", method = RouteMethod.POST)
|
||||||
public Future<JsonObject> saveParser(RoutingContext ctx) {
|
public Future<JsonObject> saveParser(RoutingContext ctx) {
|
||||||
// 权限检查
|
|
||||||
if (!checkAuth(ctx)) {
|
|
||||||
return Future.succeededFuture(JsonResult.error("未授权访问").toJsonObject());
|
|
||||||
}
|
|
||||||
|
|
||||||
Promise<JsonObject> promise = Promise.promise();
|
Promise<JsonObject> promise = Promise.promise();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 检查访问权限
|
||||||
|
ensurePlaygroundAccess(ctx);
|
||||||
|
|
||||||
JsonObject body = ctx.body().asJsonObject();
|
JsonObject body = ctx.body().asJsonObject();
|
||||||
String jsCode = body.getString("jsCode");
|
String jsCode = body.getString("jsCode");
|
||||||
|
|
||||||
@@ -462,18 +520,7 @@ public class PlaygroundApi {
|
|||||||
parser.put("enabled", true);
|
parser.put("enabled", true);
|
||||||
|
|
||||||
dbService.savePlaygroundParser(parser).onSuccess(result -> {
|
dbService.savePlaygroundParser(parser).onSuccess(result -> {
|
||||||
// 保存成功后,立即注册到解析器系统
|
promise.complete(result);
|
||||||
try {
|
|
||||||
CustomParserRegistry.register(config);
|
|
||||||
log.info("已注册演练场解析器: {} ({})", displayName, type);
|
|
||||||
promise.complete(JsonResult.success("保存并注册成功").toJsonObject());
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("注册解析器失败", e);
|
|
||||||
// 虽然注册失败,但保存成功了,返回警告
|
|
||||||
promise.complete(JsonResult.success(
|
|
||||||
"保存成功,但注册失败(重启服务后会自动加载): " + e.getMessage()
|
|
||||||
).toJsonObject());
|
|
||||||
}
|
|
||||||
}).onFailure(e -> {
|
}).onFailure(e -> {
|
||||||
log.error("保存解析器失败", e);
|
log.error("保存解析器失败", e);
|
||||||
promise.complete(JsonResult.error("保存失败: " + e.getMessage()).toJsonObject());
|
promise.complete(JsonResult.error("保存失败: " + e.getMessage()).toJsonObject());
|
||||||
@@ -504,14 +551,12 @@ public class PlaygroundApi {
|
|||||||
*/
|
*/
|
||||||
@RouteMapping(value = "/parsers/:id", method = RouteMethod.PUT)
|
@RouteMapping(value = "/parsers/:id", method = RouteMethod.PUT)
|
||||||
public Future<JsonObject> updateParser(RoutingContext ctx, Long id) {
|
public Future<JsonObject> updateParser(RoutingContext ctx, Long id) {
|
||||||
// 权限检查
|
|
||||||
if (!checkAuth(ctx)) {
|
|
||||||
return Future.succeededFuture(JsonResult.error("未授权访问").toJsonObject());
|
|
||||||
}
|
|
||||||
|
|
||||||
Promise<JsonObject> promise = Promise.promise();
|
Promise<JsonObject> promise = Promise.promise();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 检查访问权限
|
||||||
|
ensurePlaygroundAccess(ctx);
|
||||||
|
|
||||||
JsonObject body = ctx.body().asJsonObject();
|
JsonObject body = ctx.body().asJsonObject();
|
||||||
String jsCode = body.getString("jsCode");
|
String jsCode = body.getString("jsCode");
|
||||||
|
|
||||||
@@ -523,14 +568,12 @@ public class PlaygroundApi {
|
|||||||
// 解析元数据
|
// 解析元数据
|
||||||
try {
|
try {
|
||||||
var config = JsScriptMetadataParser.parseScript(jsCode);
|
var config = JsScriptMetadataParser.parseScript(jsCode);
|
||||||
String type = config.getType();
|
|
||||||
String displayName = config.getDisplayName();
|
String displayName = config.getDisplayName();
|
||||||
String name = config.getMetadata().get("name");
|
String name = config.getMetadata().get("name");
|
||||||
String description = config.getMetadata().get("description");
|
String description = config.getMetadata().get("description");
|
||||||
String author = config.getMetadata().get("author");
|
String author = config.getMetadata().get("author");
|
||||||
String version = config.getMetadata().get("version");
|
String version = config.getMetadata().get("version");
|
||||||
String matchPattern = config.getMatchPattern() != null ? config.getMatchPattern().pattern() : null;
|
String matchPattern = config.getMatchPattern() != null ? config.getMatchPattern().pattern() : null;
|
||||||
boolean enabled = body.getBoolean("enabled", true);
|
|
||||||
|
|
||||||
JsonObject parser = new JsonObject();
|
JsonObject parser = new JsonObject();
|
||||||
parser.put("name", name);
|
parser.put("name", name);
|
||||||
@@ -540,29 +583,10 @@ public class PlaygroundApi {
|
|||||||
parser.put("version", version);
|
parser.put("version", version);
|
||||||
parser.put("matchPattern", matchPattern);
|
parser.put("matchPattern", matchPattern);
|
||||||
parser.put("jsCode", jsCode);
|
parser.put("jsCode", jsCode);
|
||||||
parser.put("enabled", enabled);
|
parser.put("enabled", body.getBoolean("enabled", true));
|
||||||
|
|
||||||
dbService.updatePlaygroundParser(id, parser).onSuccess(result -> {
|
dbService.updatePlaygroundParser(id, parser).onSuccess(result -> {
|
||||||
// 更新成功后,重新注册解析器
|
promise.complete(result);
|
||||||
try {
|
|
||||||
if (enabled) {
|
|
||||||
// 先注销旧的(如果存在)
|
|
||||||
CustomParserRegistry.unregister(type);
|
|
||||||
// 重新注册新的
|
|
||||||
CustomParserRegistry.register(config);
|
|
||||||
log.info("已重新注册演练场解析器: {} ({})", displayName, type);
|
|
||||||
} else {
|
|
||||||
// 禁用时注销
|
|
||||||
CustomParserRegistry.unregister(type);
|
|
||||||
log.info("已注销演练场解析器: {}", type);
|
|
||||||
}
|
|
||||||
promise.complete(JsonResult.success("更新并重新注册成功").toJsonObject());
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("重新注册解析器失败", e);
|
|
||||||
promise.complete(JsonResult.success(
|
|
||||||
"更新成功,但注册失败(重启服务后会自动加载): " + e.getMessage()
|
|
||||||
).toJsonObject());
|
|
||||||
}
|
|
||||||
}).onFailure(e -> {
|
}).onFailure(e -> {
|
||||||
log.error("更新解析器失败", e);
|
log.error("更新解析器失败", e);
|
||||||
promise.complete(JsonResult.error("更新失败: " + e.getMessage()).toJsonObject());
|
promise.complete(JsonResult.error("更新失败: " + e.getMessage()).toJsonObject());
|
||||||
@@ -585,42 +609,14 @@ public class PlaygroundApi {
|
|||||||
*/
|
*/
|
||||||
@RouteMapping(value = "/parsers/:id", method = RouteMethod.DELETE)
|
@RouteMapping(value = "/parsers/:id", method = RouteMethod.DELETE)
|
||||||
public Future<JsonObject> deleteParser(RoutingContext ctx, Long id) {
|
public Future<JsonObject> deleteParser(RoutingContext ctx, Long id) {
|
||||||
// 权限检查
|
|
||||||
if (!checkAuth(ctx)) {
|
|
||||||
return Future.succeededFuture(JsonResult.error("未授权访问").toJsonObject());
|
|
||||||
}
|
|
||||||
|
|
||||||
Promise<JsonObject> promise = Promise.promise();
|
|
||||||
|
|
||||||
// 先获取解析器信息,用于注销
|
|
||||||
dbService.getPlaygroundParserById(id).onSuccess(getResult -> {
|
|
||||||
if (getResult.getBoolean("success", false)) {
|
|
||||||
JsonObject parser = getResult.getJsonObject("data");
|
|
||||||
String type = parser.getString("type");
|
|
||||||
|
|
||||||
// 删除数据库记录
|
|
||||||
dbService.deletePlaygroundParser(id).onSuccess(deleteResult -> {
|
|
||||||
// 从注册表中注销
|
|
||||||
try {
|
try {
|
||||||
CustomParserRegistry.unregister(type);
|
// 检查访问权限
|
||||||
log.info("已注销演练场解析器: {}", type);
|
ensurePlaygroundAccess(ctx);
|
||||||
|
return dbService.deletePlaygroundParser(id);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("注销解析器失败(可能未注册): {}", type, e);
|
|
||||||
}
|
|
||||||
promise.complete(deleteResult);
|
|
||||||
}).onFailure(e -> {
|
|
||||||
log.error("删除解析器失败", e);
|
log.error("删除解析器失败", e);
|
||||||
promise.complete(JsonResult.error("删除失败: " + e.getMessage()).toJsonObject());
|
return Future.succeededFuture(JsonResult.error(e.getMessage()).toJsonObject());
|
||||||
});
|
|
||||||
} else {
|
|
||||||
promise.complete(getResult);
|
|
||||||
}
|
}
|
||||||
}).onFailure(e -> {
|
|
||||||
log.error("获取解析器信息失败", e);
|
|
||||||
promise.complete(JsonResult.error("获取解析器信息失败: " + e.getMessage()).toJsonObject());
|
|
||||||
});
|
|
||||||
|
|
||||||
return promise.future();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -628,11 +624,14 @@ public class PlaygroundApi {
|
|||||||
*/
|
*/
|
||||||
@RouteMapping(value = "/parsers/:id", method = RouteMethod.GET)
|
@RouteMapping(value = "/parsers/:id", method = RouteMethod.GET)
|
||||||
public Future<JsonObject> getParserById(RoutingContext ctx, Long id) {
|
public Future<JsonObject> getParserById(RoutingContext ctx, Long id) {
|
||||||
// 权限检查
|
try {
|
||||||
if (!checkAuth(ctx)) {
|
// 检查访问权限
|
||||||
return Future.succeededFuture(JsonResult.error("未授权访问").toJsonObject());
|
ensurePlaygroundAccess(ctx);
|
||||||
}
|
|
||||||
return dbService.getPlaygroundParserById(id);
|
return dbService.getPlaygroundParserById(id);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("获取解析器失败", e);
|
||||||
|
return Future.succeededFuture(JsonResult.error(e.getMessage()).toJsonObject());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -13,13 +13,6 @@ server:
|
|||||||
# 反向代理服务器配置路径(不用加后缀)
|
# 反向代理服务器配置路径(不用加后缀)
|
||||||
proxyConf: server-proxy
|
proxyConf: server-proxy
|
||||||
|
|
||||||
# JS演练场配置
|
|
||||||
playground:
|
|
||||||
# 公开模式,默认false需要密码访问,设为true则无需密码
|
|
||||||
public: false
|
|
||||||
# 访问密码,建议修改默认密码!
|
|
||||||
password: 'nfd_playground_2024'
|
|
||||||
|
|
||||||
# vertx核心线程配置(一般无需改的), 为0表示eventLoopPoolSize将会采用默认配置(CPU核心*2) workerPoolSize将会采用默认20
|
# vertx核心线程配置(一般无需改的), 为0表示eventLoopPoolSize将会采用默认配置(CPU核心*2) workerPoolSize将会采用默认20
|
||||||
vertx:
|
vertx:
|
||||||
eventLoopPoolSize: 0
|
eventLoopPoolSize: 0
|
||||||
@@ -109,3 +102,11 @@ auths:
|
|||||||
ye:
|
ye:
|
||||||
username:
|
username:
|
||||||
password:
|
password:
|
||||||
|
|
||||||
|
# Playground演练场配置
|
||||||
|
playground:
|
||||||
|
# 是否启用Playground,默认关闭
|
||||||
|
enabled: false
|
||||||
|
# 访问密码,可选。仅在enabled=true时生效
|
||||||
|
# 为空时表示公开访问,不需要密码
|
||||||
|
password: ""
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
### 安全漏洞修复测试 - DoS攻击防护
|
|
||||||
###
|
|
||||||
### 测试目标:
|
|
||||||
### 1. 验证代码长度限制(128KB)
|
|
||||||
### 2. 验证JavaScript执行超时(30秒)
|
|
||||||
###
|
|
||||||
|
|
||||||
### 测试1: 正常代码执行(应该成功)
|
|
||||||
POST http://127.0.0.1:6400/v2/playground/test
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"jsCode": "// ==UserScript==\n// @name 正常测试\n// @type normal_test\n// @displayName 正常\n// @match https://example\\.com/(?<KEY>\\w+)\n// @author test\n// @version 1.0.0\n// ==/UserScript==\n\nfunction parse(shareLinkInfo, http, logger) {\n logger.info('正常执行');\n return 'https://example.com/download/file.zip';\n}",
|
|
||||||
"shareUrl": "https://example.com/test123",
|
|
||||||
"pwd": "",
|
|
||||||
"method": "parse"
|
|
||||||
}
|
|
||||||
|
|
||||||
###
|
|
||||||
|
|
||||||
### 测试2: 代码长度超过限制(应该失败并提示)
|
|
||||||
### 这个测试会创建一个超过128KB的代码
|
|
||||||
POST http://127.0.0.1:6400/v2/playground/test
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"jsCode": "// ==UserScript==\n// @name 长度测试\n// @type length_test\n// @displayName 长度\n// @match https://example\\.com/(?<KEY>\\w+)\n// @author test\n// @version 1.0.0\n// ==/UserScript==\n\nfunction parse(shareLinkInfo, http, logger) {\n var data = 'x'.repeat(150000);\n return data;\n}",
|
|
||||||
"shareUrl": "https://example.com/test123",
|
|
||||||
"pwd": "",
|
|
||||||
"method": "parse"
|
|
||||||
}
|
|
||||||
|
|
||||||
###
|
|
||||||
|
|
||||||
### 测试3: 无限循环(应该在30秒后超时)
|
|
||||||
POST http://127.0.0.1:6400/v2/playground/test
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"jsCode": "// ==UserScript==\n// @name 无限循环测试\n// @type infinite_loop_test\n// @displayName 无限循环\n// @match https://example\\.com/(?<KEY>\\w+)\n// @author test\n// @version 1.0.0\n// ==/UserScript==\n\nfunction parse(shareLinkInfo, http, logger) {\n logger.info('开始无限循环...');\n while(true) {\n var x = 1 + 1;\n }\n return 'never reached';\n}",
|
|
||||||
"shareUrl": "https://example.com/test123",
|
|
||||||
"pwd": "",
|
|
||||||
"method": "parse"
|
|
||||||
}
|
|
||||||
|
|
||||||
###
|
|
||||||
|
|
||||||
### 测试4: 大数组内存炸弹(应该在30秒后超时或内存限制)
|
|
||||||
POST http://127.0.0.1:6400/v2/playground/test
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"jsCode": "// ==UserScript==\n// @name 内存炸弹测试\n// @type memory_bomb_test\n// @displayName 内存炸弹\n// @match https://example\\.com/(?<KEY>\\w+)\n// @author test\n// @version 1.0.0\n// ==/UserScript==\n\nfunction parse(shareLinkInfo, http, logger) {\n logger.info('创建大数组...');\n var arr = [];\n for(var i = 0; i < 10000000; i++) {\n arr.push('x'.repeat(1000));\n }\n logger.info('数组创建完成');\n return 'DONE';\n}",
|
|
||||||
"shareUrl": "https://example.com/test123",
|
|
||||||
"pwd": "",
|
|
||||||
"method": "parse"
|
|
||||||
}
|
|
||||||
|
|
||||||
###
|
|
||||||
|
|
||||||
### 测试5: 递归调用栈溢出
|
|
||||||
POST http://127.0.0.1:6400/v2/playground/test
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"jsCode": "// ==UserScript==\n// @name 栈溢出测试\n// @type stack_overflow_test\n// @displayName 栈溢出\n// @match https://example\\.com/(?<KEY>\\w+)\n// @author test\n// @version 1.0.0\n// ==/UserScript==\n\nfunction boom() {\n return boom();\n}\n\nfunction parse(shareLinkInfo, http, logger) {\n logger.info('开始递归炸弹...');\n boom();\n return 'never reached';\n}",
|
|
||||||
"shareUrl": "https://example.com/test123",
|
|
||||||
"pwd": "",
|
|
||||||
"method": "parse"
|
|
||||||
}
|
|
||||||
|
|
||||||
###
|
|
||||||
|
|
||||||
### 测试6: 保存解析器 - 验证代码长度限制
|
|
||||||
POST http://127.0.0.1:6400/v2/playground/parsers
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"jsCode": "// ==UserScript==\n// @name 正常解析器\n// @type normal_parser\n// @displayName 正常解析器\n// @match https://example\\.com/(?<KEY>\\w+)\n// @author test\n// @version 1.0.0\n// ==/UserScript==\n\nfunction parse(shareLinkInfo, http, logger) {\n return 'https://example.com/download/file.zip';\n}\n\nfunction parseFileList(shareLinkInfo, http, logger) {\n return [];\n}\n\nfunction parseById(shareLinkInfo, http, logger) {\n return 'https://example.com/download/file.zip';\n}"
|
|
||||||
}
|
|
||||||
|
|
||||||
###
|
|
||||||
|
|
||||||
### 测试结果期望:
|
|
||||||
### 1. 测试1 - 应该成功返回结果
|
|
||||||
### 2. 测试2 - 应该返回错误:"代码长度超过限制"
|
|
||||||
### 3. 测试3 - 应该在30秒后返回超时错误:"JavaScript执行超时"
|
|
||||||
### 4. 测试4 - 应该在30秒后返回超时错误或内存错误
|
|
||||||
### 5. 测试5 - 应该返回堆栈溢出错误
|
|
||||||
### 6. 测试6 - 应该成功保存(如果代码不超过128KB)
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user