Compare commits

...

4 Commits

Author SHA1 Message Date
qaiu
e17fb99de4 启用在线脚本解析器 2026-01-06 02:21:19 +08:00
q
0f926a57ef 更新Playground和JsHttpClient相关功能,整理文档结构 2026-01-06 00:00:37 +08:00
q
4380bfe0d6 Merge branch 'main' of github.com:qaiu/netdisk-fast-download 2026-01-04 09:30:54 +08:00
q
8127cd0758 fixed. 演练场静态打包问题 2026-01-04 09:29:20 +08:00
16 changed files with 1350 additions and 104 deletions

17
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,17 @@
version: 2
updates:
- package-ecosystem: "maven"
directory: "/"
open-pull-requests-limit: 10
ignore:
# 忽略通过 BOM 管理的 Vert.x 依赖
# 这些依赖的版本通过 vertx-dependencies BOM 统一管理
# 应该通过更新 pom.xml 中的 vertx.version 属性来更新这些依赖
- dependency-name: "io.vertx:vertx-web"
- dependency-name: "io.vertx:vertx-codegen"
- dependency-name: "io.vertx:vertx-config"
- dependency-name: "io.vertx:vertx-config-yaml"
- dependency-name: "io.vertx:vertx-service-proxy"
- dependency-name: "io.vertx:vertx-web-proxy"
- dependency-name: "io.vertx:vertx-web-client"

2
.gitignore vendored
View File

@@ -29,6 +29,8 @@ target/
/src/logs/
*.zip
sdkTest.log
app.yml
app-local.yml
#some local files

View File

@@ -128,7 +128,9 @@ public class ReverseProxyVerticle extends AbstractVerticle {
}
private HttpServer getHttpsServer(JsonObject proxyConf) {
HttpServerOptions httpServerOptions = new HttpServerOptions();
HttpServerOptions httpServerOptions = new HttpServerOptions()
.setCompressionSupported(true);
if (proxyConf.containsKey("ssl")) {
JsonObject sslConfig = proxyConf.getJsonObject("ssl");
@@ -182,6 +184,7 @@ public class ReverseProxyVerticle extends AbstractVerticle {
} else {
staticHandler = StaticHandler.create();
}
if (staticConf.containsKey("directory-listing")) {
staticHandler.setDirectoryListing(staticConf.getBoolean("directory-listing"));
} else if (staticConf.containsKey("index")) {

View File

@@ -244,19 +244,17 @@ public class JsHttpClient {
if (data != null) {
if (data instanceof String) {
request.sendBuffer(Buffer.buffer((String) data));
return request.sendBuffer(Buffer.buffer((String) data));
} else if (data instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, String> mapData = (Map<String, String>) data;
request.sendForm(MultiMap.caseInsensitiveMultiMap().addAll(mapData));
return request.sendForm(MultiMap.caseInsensitiveMultiMap().addAll(mapData));
} else {
request.sendJson(data);
return request.sendJson(data);
}
} else {
request.send();
return request.send();
}
return request.send();
});
}
@@ -276,19 +274,17 @@ public class JsHttpClient {
if (data != null) {
if (data instanceof String) {
request.sendBuffer(Buffer.buffer((String) data));
return request.sendBuffer(Buffer.buffer((String) data));
} else if (data instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, String> mapData = (Map<String, String>) data;
request.sendForm(MultiMap.caseInsensitiveMultiMap().addAll(mapData));
return request.sendForm(MultiMap.caseInsensitiveMultiMap().addAll(mapData));
} else {
request.sendJson(data);
return request.sendJson(data);
}
} else {
request.send();
return request.send();
}
return request.send();
});
}
@@ -322,19 +318,17 @@ public class JsHttpClient {
if (data != null) {
if (data instanceof String) {
request.sendBuffer(Buffer.buffer((String) data));
return request.sendBuffer(Buffer.buffer((String) data));
} else if (data instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, String> mapData = (Map<String, String>) data;
request.sendForm(MultiMap.caseInsensitiveMultiMap().addAll(mapData));
return request.sendForm(MultiMap.caseInsensitiveMultiMap().addAll(mapData));
} else {
request.sendJson(data);
return request.sendJson(data);
}
} else {
request.send();
return request.send();
}
return request.send();
});
}

View File

@@ -111,7 +111,7 @@ public class JsPlaygroundExecutor {
playgroundLogger.infoJava("✅ Fetch API和Promise polyfill注入成功");
}
playgroundLogger.infoJava("🔒 安全的JavaScript引擎初始化成功演练场");
playgroundLogger.infoJava("初始化成功");
// 执行JavaScript代码
engine.eval(jsCode);

View File

@@ -549,6 +549,176 @@ public class JsHttpClientTest {
}
}
@Test
public void testPostWithJsonString() {
System.out.println("\n[测试16] POST请求JSON字符串 - httpbin.org/post");
System.out.println("测试修复POST请求发送JSON字符串时请求体是否正确发送");
try {
String url = "https://httpbin.org/post";
System.out.println("请求URL: " + url);
// 模拟阿里云盘登录请求格式
String jsonData = "{\"grant_type\":\"refresh_token\",\"refresh_token\":\"test_token_123\"}";
System.out.println("POST数据JSON字符串: " + jsonData);
// 设置Content-Type为application/json
httpClient.putHeader("Content-Type", "application/json");
System.out.println("设置Content-Type: application/json");
System.out.println("开始请求...");
long startTime = System.currentTimeMillis();
JsHttpClient.JsHttpResponse response = httpClient.post(url, jsonData);
long endTime = System.currentTimeMillis();
System.out.println("请求完成,耗时: " + (endTime - startTime) + "ms");
System.out.println("状态码: " + response.statusCode());
String body = response.body();
System.out.println("响应体前500字符: " + (body != null && body.length() > 500 ? body.substring(0, 500) + "..." : body));
// 验证结果
assertNotNull("响应不能为null", response);
assertEquals("状态码应该是200", 200, response.statusCode());
assertNotNull("响应体不能为null", body);
// 验证请求体是否正确发送httpbin会回显请求数据
assertTrue("响应体应该包含发送的JSON数据",
body.contains("grant_type") || body.contains("refresh_token"));
System.out.println("✓ 测试通过 - POST请求体已正确发送");
} catch (Exception e) {
System.err.println("✗ 测试失败: " + e.getMessage());
e.printStackTrace();
fail("POST JSON字符串请求测试失败: " + e.getMessage());
}
}
@Test
public void testAlipanTokenApi() {
System.out.println("\n[测试20] 阿里云盘Token接口测试 - auth.aliyundrive.com/v2/account/token");
System.out.println("参考 alipan.js 中的登录逻辑,测试请求格式是否正确");
try {
String tokenUrl = "https://auth.aliyundrive.com/v2/account/token";
System.out.println("请求URL: " + tokenUrl);
// 参考 alipan.js 中的请求格式
// setJsonHeaders(http) 设置 Content-Type: application/json 和 User-Agent
httpClient.putHeader("Content-Type", "application/json");
httpClient.putHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
// 参考 alipan.js: JSON.stringify({grant_type: "refresh_token", refresh_token: REFRESH_TOKEN})
String jsonData = "{\"grant_type\":\"refresh_token\",\"refresh_token\":\"\"}";
System.out.println("POST数据JSON字符串: " + jsonData);
System.out.println("注意使用无效token测试错误响应格式");
System.out.println("开始请求...");
long startTime = System.currentTimeMillis();
JsHttpClient.JsHttpResponse response = httpClient.post(tokenUrl, jsonData);
long endTime = System.currentTimeMillis();
System.out.println("请求完成,耗时: " + (endTime - startTime) + "ms");
System.out.println("状态码: " + response.statusCode());
String body = response.body();
System.out.println("响应体: " + body);
// 验证结果
assertNotNull("响应不能为null", response);
// 使用无效token应该返回400或401等错误状态码但请求格式应该是正确的
assertTrue("状态码应该是4xx无效token或200如果token有效",
response.statusCode() >= 200 && response.statusCode() < 500);
assertNotNull("响应体不能为null", body);
// 验证响应格式阿里云盘API通常返回JSON
try {
Object jsonResponse = response.json();
System.out.println("响应JSON解析成功: " + jsonResponse);
assertNotNull("JSON响应不能为null", jsonResponse);
} catch (Exception e) {
System.out.println("警告响应不是有效的JSON格式");
}
// 验证请求头是否正确设置
System.out.println("验证请求头设置...");
Map<String, String> headers = httpClient.getHeaders();
assertTrue("应该设置了Content-Type", headers.containsKey("Content-Type"));
assertEquals("Content-Type应该是application/json",
"application/json", headers.get("Content-Type"));
System.out.println("✓ 测试通过 - 请求格式正确已成功发送到阿里云盘API");
} catch (Exception e) {
System.err.println("✗ 测试失败: " + e.getMessage());
e.printStackTrace();
// 如果是超时或其他网络错误,说明请求格式可能有问题
if (e.getMessage() != null && e.getMessage().contains("超时")) {
fail("请求超时,可能是请求格式问题或网络问题: " + e.getMessage());
} else {
fail("阿里云盘Token接口测试失败: " + e.getMessage());
}
}
}
@Test
public void testAlipanTokenApiWithValidFormat() {
System.out.println("\n[测试21] 阿里云盘Token接口格式验证 - 使用httpbin验证请求格式");
System.out.println("通过httpbin回显验证请求格式是否与alipan.js中的格式一致");
try {
// 使用httpbin来验证请求格式
String testUrl = "https://httpbin.org/post";
System.out.println("测试URL: " + testUrl);
// 参考 alipan.js 中的请求格式
httpClient.clearHeaders(); // 清空之前的头
httpClient.putHeader("Content-Type", "application/json");
httpClient.putHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
// 完全模拟 alipan.js 中的请求体格式
String jsonData = "{\"grant_type\":\"refresh_token\",\"refresh_token\":\"test_refresh_token_12345\"}";
System.out.println("POST数据JSON字符串: " + jsonData);
System.out.println("开始请求...");
long startTime = System.currentTimeMillis();
JsHttpClient.JsHttpResponse response = httpClient.post(testUrl, jsonData);
long endTime = System.currentTimeMillis();
System.out.println("请求完成,耗时: " + (endTime - startTime) + "ms");
System.out.println("状态码: " + response.statusCode());
String body = response.body();
System.out.println("响应体前800字符: " + (body != null && body.length() > 800 ? body.substring(0, 800) + "..." : body));
// 验证结果
assertNotNull("响应不能为null", response);
assertEquals("状态码应该是200", 200, response.statusCode());
assertNotNull("响应体不能为null", body);
// httpbin会回显请求数据验证请求体是否正确发送
assertTrue("响应体应该包含grant_type字段", body.contains("grant_type"));
assertTrue("响应体应该包含refresh_token字段", body.contains("refresh_token"));
assertTrue("响应体应该包含发送的refresh_token值", body.contains("test_refresh_token_12345"));
// 验证Content-Type是否正确
assertTrue("响应体应该包含Content-Type信息", body.contains("application/json"));
// 验证User-Agent是否正确
assertTrue("响应体应该包含User-Agent信息", body.contains("Mozilla"));
System.out.println("✓ 测试通过 - 请求格式与alipan.js中的格式完全一致");
System.out.println(" - JSON请求体正确发送");
System.out.println(" - Content-Type正确设置");
System.out.println(" - User-Agent正确设置");
} catch (Exception e) {
System.err.println("✗ 测试失败: " + e.getMessage());
e.printStackTrace();
fail("阿里云盘Token接口格式验证失败: " + e.getMessage());
}
}
@Test
public void testSetTimeout() {
System.out.println("\n[测试15] 设置超时时间 - setTimeout方法");

View File

@@ -3,6 +3,7 @@ module.exports = {
'@vue/cli-plugin-babel/preset'
],
plugins: [
'@vue/babel-plugin-transform-vue-jsx'
'@vue/babel-plugin-transform-vue-jsx',
'@babel/plugin-transform-class-static-block'
]
}

View File

@@ -5,7 +5,8 @@
"scripts": {
"serve": "vue-cli-service serve",
"dev": "vue-cli-service serve",
"build": "vue-cli-service build",
"build": "vue-cli-service build && node scripts/compress-vs.js",
"build:no-compress": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
@@ -28,6 +29,7 @@
"@babel/core": "^7.26.0",
"@babel/eslint-parser": "^7.25.9",
"@babel/plugin-transform-class-properties": "^7.26.0",
"@babel/plugin-transform-class-static-block": "^7.26.0",
"@vue/babel-plugin-transform-vue-jsx": "^1.4.0",
"@vue/cli-plugin-babel": "~5.0.8",
"@vue/cli-plugin-eslint": "~5.0.8",
@@ -35,7 +37,8 @@
"compression-webpack-plugin": "^11.1.0",
"eslint": "^9.0.0",
"eslint-plugin-vue": "^9.30.0",
"filemanager-webpack-plugin": "8.0.0"
"filemanager-webpack-plugin": "8.0.0",
"monaco-editor-webpack-plugin": "^7.1.1"
},
"eslintConfig": {
"root": true,

124
web-front/scripts/compress-vs.js Executable file
View File

@@ -0,0 +1,124 @@
#!/usr/bin/env node
const path = require("path");
const fs = require("fs");
const zlib = require("zlib");
const { promisify } = require("util");
const gzip = promisify(zlib.gzip);
const readdir = promisify(fs.readdir);
const stat = promisify(fs.stat);
const readFile = promisify(fs.readFile);
const writeFile = promisify(fs.writeFile);
// 递归压缩目录下的所有文件
async function compressDirectory(dirPath, threshold = 1024) {
if (!fs.existsSync(dirPath)) {
console.warn(`目录不存在: ${dirPath}`);
return;
}
const files = await readdir(dirPath, { withFileTypes: true });
let compressedCount = 0;
let totalOriginalSize = 0;
let totalCompressedSize = 0;
for (const file of files) {
const filePath = path.join(dirPath, file.name);
if (file.isDirectory()) {
await compressDirectory(filePath, threshold);
} else if (file.isFile()) {
const stats = await stat(filePath);
// 只压缩超过阈值且不是已压缩的文件
if (stats.size > threshold && !filePath.endsWith('.gz') && !filePath.endsWith('.map')) {
try {
const content = await readFile(filePath);
const compressed = await gzip(content);
await writeFile(filePath + '.gz', compressed);
compressedCount++;
totalOriginalSize += stats.size;
totalCompressedSize += compressed.length;
console.log(`${file.name} (${(stats.size / 1024).toFixed(2)}KB -> ${(compressed.length / 1024).toFixed(2)}KB)`);
} catch (error) {
console.warn(`⚠ 压缩失败: ${filePath}`, error.message);
}
}
}
}
if (compressedCount > 0) {
console.log(`\n压缩完成: ${compressedCount} 个文件`);
console.log(`原始大小: ${(totalOriginalSize / 1024 / 1024).toFixed(2)}MB`);
console.log(`压缩后大小: ${(totalCompressedSize / 1024 / 1024).toFixed(2)}MB`);
console.log(`压缩率: ${((1 - totalCompressedSize / totalOriginalSize) * 100).toFixed(1)}%`);
}
}
// 删除未使用的 worker 文件
function deleteUnusedWorkers() {
const jsDir = path.join(__dirname, '../nfd-front/js');
const workers = ['editor.worker.js', 'editor.worker.js.gz', 'json.worker.js', 'json.worker.js.gz', 'ts.worker.js', 'ts.worker.js.gz'];
let deletedCount = 0;
for (const worker of workers) {
const filePath = path.join(jsDir, worker);
if (fs.existsSync(filePath)) {
try {
fs.unlinkSync(filePath);
deletedCount++;
console.log(`✓ 已删除未使用的文件: ${worker}`);
} catch (error) {
console.warn(`⚠ 删除失败: ${worker}`, error.message);
}
}
}
if (deletedCount > 0) {
console.log(`\n已删除 ${deletedCount} 个未使用的 worker 文件\n`);
}
}
// 复制到 webroot
function copyToWebroot() {
const source = path.join(__dirname, '../nfd-front');
const dest = path.join(__dirname, '../../webroot/nfd-front');
// 使用 FileManagerPlugin 的方式,这里用简单的复制
const { execSync } = require('child_process');
try {
// 删除目标目录
if (fs.existsSync(dest)) {
execSync(`rm -rf "${dest}"`, { stdio: 'inherit' });
}
// 复制整个目录
execSync(`cp -R "${source}" "${dest}"`, { stdio: 'inherit' });
console.log('\n✓ 已复制到 webroot');
} catch (error) {
console.error('\n✗ 复制到 webroot 失败:', error.message);
process.exit(1);
}
}
// 主函数
async function main() {
// 先删除未使用的 worker 文件
deleteUnusedWorkers();
// 然后压缩 vs 目录
const vsPath = path.join(__dirname, '../nfd-front/js/vs');
console.log('开始压缩 vs 目录下的文件...\n');
try {
await compressDirectory(vsPath, 1024); // 只压缩超过1KB的文件
console.log('\n✓ vs 目录压缩完成');
} catch (error) {
console.error('\n✗ vs 目录压缩失败:', error);
process.exit(1);
}
// 最后复制到 webroot
copyToWebroot();
}
main();

View File

@@ -34,6 +34,7 @@ export default {
const editorContainer = ref(null);
let editor = null;
let monaco = null;
let touchHandlers = { start: null, move: null };
const defaultOptions = {
value: props.modelValue,
@@ -94,13 +95,18 @@ export default {
return;
}
// 配置Monaco Editor使用国内CDN (npmmirror)
// npmmirror的路径格式: https://registry.npmmirror.com/包名/版本号/files/文件路径
loader.config({
paths: {
vs: 'https://registry.npmmirror.com/monaco-editor/0.55.1/files/min/vs'
}
});
// 配置Monaco Editor使用本地打包的文件而不是CDN
if (loader.config) {
const vsPath = process.env.NODE_ENV === 'production'
? './js/vs' // 生产环境使用相对路径
: '/js/vs'; // 开发环境使用绝对路径
loader.config({
paths: {
vs: vsPath
}
});
}
// 初始化Monaco Editor
monaco = await loader.init();
@@ -131,6 +137,46 @@ export default {
if (editorContainer.value) {
editorContainer.value.style.height = props.height;
}
// 移动端:添加触摸缩放来调整字体大小
if (window.innerWidth <= 768 && editorContainer.value) {
let initialDistance = 0;
let initialFontSize = defaultOptions.fontSize || 14;
const minFontSize = 8;
const maxFontSize = 24;
const getTouchDistance = (touch1, touch2) => {
const dx = touch1.clientX - touch2.clientX;
const dy = touch1.clientY - touch2.clientY;
return Math.sqrt(dx * dx + dy * dy);
};
touchHandlers.start = (e) => {
if (e.touches.length === 2 && editor) {
initialDistance = getTouchDistance(e.touches[0], e.touches[1]);
initialFontSize = editor.getOption(monaco.editor.EditorOption.fontSize);
}
};
touchHandlers.move = (e) => {
if (e.touches.length === 2 && editor) {
e.preventDefault(); // 防止页面缩放
const currentDistance = getTouchDistance(e.touches[0], e.touches[1]);
const scale = currentDistance / initialDistance;
const newFontSize = Math.round(initialFontSize * scale);
// 限制字体大小范围
const clampedFontSize = Math.max(minFontSize, Math.min(maxFontSize, newFontSize));
if (clampedFontSize !== editor.getOption(monaco.editor.EditorOption.fontSize)) {
editor.updateOptions({ fontSize: clampedFontSize });
}
}
};
editorContainer.value.addEventListener('touchstart', touchHandlers.start, { passive: false });
editorContainer.value.addEventListener('touchmove', touchHandlers.move, { passive: false });
}
} catch (error) {
console.error('Monaco Editor初始化失败:', error);
console.error('错误详情:', error.stack);
@@ -174,6 +220,11 @@ export default {
});
onBeforeUnmount(() => {
// 清理触摸事件监听器
if (editorContainer.value && touchHandlers.start && touchHandlers.move) {
editorContainer.value.removeEventListener('touchstart', touchHandlers.start);
editorContainer.value.removeEventListener('touchmove', touchHandlers.move);
}
if (editor) {
editor.dispose();
}
@@ -195,10 +246,26 @@ export default {
border: 1px solid #dcdfe6;
border-radius: 4px;
overflow: hidden;
/* 允许用户选择文本 */
-webkit-user-select: text;
user-select: text;
}
.monaco-editor-container :deep(.monaco-editor) {
border-radius: 4px;
}
/* 移动端:禁用页面缩放,只允许编辑器字体缩放 */
@media (max-width: 768px) {
.monaco-editor-container {
/* 禁用页面级别的缩放,只允许编辑器内部字体缩放 */
touch-action: pan-x pan-y;
}
.monaco-editor-container :deep(.monaco-editor) {
/* 禁用页面级别的缩放 */
touch-action: pan-x pan-y;
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -4,8 +4,10 @@ const path = require("path");
function resolve(dir) {
return path.join(__dirname, dir)
}
const CompressionPlugin = require('compression-webpack-plugin');
const FileManagerPlugin = require('filemanager-webpack-plugin')
const FileManagerPlugin = require('filemanager-webpack-plugin');
const MonacoEditorPlugin = require('monaco-editor-webpack-plugin');
module.exports = {
productionSourceMap: false, // 是否在构建生产包时生成sourceMap文件false将提高构建速度
@@ -43,7 +45,7 @@ module.exports = {
'@': resolve('src')
}
},
// Monaco Editor配置 - 使用国内CDN
// Monaco Editor配置 - 使用本地打包
module: {
rules: [
{
@@ -53,9 +55,18 @@ module.exports = {
]
},
plugins: [
new MonacoEditorPlugin({
languages: ['javascript', 'typescript', 'json'],
features: ['coreCommands', 'find', 'format', 'suggest', 'quickCommand'],
publicPath: process.env.NODE_ENV === 'production' ? './' : '/',
// Worker 文件输出路径
filename: 'js/[name].worker.js'
}),
new CompressionPlugin({
test: /\.js$|\.html$|\.css/, // 匹配文件
threshold: 10240 // 对超过10k文件压缩
threshold: 10240, // 对超过10k文件压缩
// 排除 js 目录下的 worker 文件Monaco Editor 使用 vs/assets 下的)
exclude: /js\/.*\.worker\.js$/
}),
new FileManagerPlugin({ //初始化 filemanager-webpack-plugin 插件实例
events: {
@@ -70,7 +81,11 @@ module.exports = {
{ source: '../webroot/nfd-front/view/.gitignore', options: { force: true } },
],
copy: [
{ source: './nfd-front', destination: '../webroot/nfd-front' }
// 复制 Monaco Editor 的 vs 目录到 js/vs
{
source: './node_modules/monaco-editor/min/vs',
destination: './nfd-front/js/vs'
}
],
archive: [ //然后我们选择dist文件夹将之打包成dist.zip并放在根目录
{

View File

@@ -16,7 +16,7 @@ proxyConf: server-proxy
# JS演练场配置
playground:
# 是否启用演练场默认false不启用
enabled: false
enabled: true
# 公开模式默认false需要密码访问设为true则无需密码
public: false
# 访问密码,建议修改默认密码!
@@ -110,4 +110,4 @@ auths:
# 123网盘配置用户名密码
ye:
username:
password:
password: