feat(browser): 新增 BrowserWindowJDialog 类并优化浏览器功能
- 新增 BrowserWindowJDialog 类,用于创建 JDialog 类型的浏览器窗口 - 优化 BrowserWindow 类,添加复制、粘贴功能- 新增 CefAppManager 类,用于管理 CefApp 实例 - 更新 build.gradle,添加新依赖项
This commit is contained in:
22
build.gradle
22
build.gradle
@@ -30,10 +30,16 @@ repositories {
|
||||
ignoreGradleMetadataRedirection()
|
||||
}
|
||||
}
|
||||
maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' }
|
||||
maven { url 'https://maven.aliyun.com/repository/public' }
|
||||
maven { url 'https://jitpack.io' }
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
configurations.all {
|
||||
resolutionStrategy.cacheChangingModulesFor 0, 'seconds'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation platform('org.junit:junit-bom:5.10.0')
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter'
|
||||
@@ -123,6 +129,22 @@ dependencies {
|
||||
implementation 'org.openjfx:javafx-graphics:21'
|
||||
|
||||
implementation 'me.friwi:jcefmaven:122.1.10'
|
||||
|
||||
implementation 'com.alphacephei:vosk:0.3.45' // Java API
|
||||
implementation 'net.java.dev.jna:jna:5.12.1' // 本地库加载
|
||||
|
||||
// 高性能音频处理
|
||||
implementation 'org.apache.commons:commons-math3:3.6.1'
|
||||
implementation 'com.google.guava:guava:31.1-jre'
|
||||
|
||||
// 中文拼音处理
|
||||
implementation 'com.belerweb:pinyin4j:2.5.1'
|
||||
|
||||
// 音频I/O
|
||||
implementation 'commons-io:commons-io:2.18.0'
|
||||
implementation 'jflac:jflac:1.3' // FLAC支持
|
||||
|
||||
implementation 'com.github.axet:TarsosDSP:2.4'
|
||||
}
|
||||
|
||||
// 分离依赖项到 libs 目录
|
||||
|
||||
@@ -3,17 +3,29 @@ package com.axis.innovators.box.browser;
|
||||
import com.axis.innovators.box.tools.FolderCreator;
|
||||
import org.cef.*;
|
||||
import org.cef.browser.*;
|
||||
import org.cef.callback.CefContextMenuParams;
|
||||
import org.cef.callback.CefMenuModel;
|
||||
import org.cef.callback.CefQueryCallback;
|
||||
import org.cef.handler.*;
|
||||
import org.cef.network.CefRequest;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.datatransfer.Clipboard;
|
||||
import java.awt.datatransfer.DataFlavor;
|
||||
import java.awt.datatransfer.UnsupportedFlavorException;
|
||||
import java.awt.event.*;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.util.Arrays;
|
||||
import java.util.UUID;
|
||||
import java.net.URI;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.cef.callback.CefMenuModel.MenuId.MENU_ID_USER_FIRST;
|
||||
|
||||
/**
|
||||
* @author tzdwindows 7
|
||||
*/
|
||||
public class BrowserWindow extends JFrame {
|
||||
private final String windowId;
|
||||
private CefApp cefApp;
|
||||
@@ -25,6 +37,7 @@ public class BrowserWindow extends JFrame {
|
||||
private WindowOperationHandler operationHandler;
|
||||
private static Thread cefThread;
|
||||
private CefMessageRouter msgRouter;
|
||||
|
||||
public static class Builder {
|
||||
private String windowId;
|
||||
private String title = "JCEF Window";
|
||||
@@ -32,31 +45,70 @@ public class BrowserWindow extends JFrame {
|
||||
private WindowOperationHandler operationHandler;
|
||||
private String htmlPath;
|
||||
private Image icon;
|
||||
private boolean resizable = true; // 默认允许调整大小
|
||||
private boolean maximizable = true; // 默认允许最大化
|
||||
private boolean minimizable = true; // 默认允许最小化
|
||||
|
||||
|
||||
public Builder resizable(boolean resizable) {
|
||||
this.resizable = resizable;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder maximizable(boolean maximizable) {
|
||||
this.maximizable = maximizable;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder minimizable(boolean minimizable) {
|
||||
this.minimizable = minimizable;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder(String windowId) {
|
||||
this.windowId = windowId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置浏览器窗口标题
|
||||
* @param title 标题
|
||||
*/
|
||||
public Builder title(String title) {
|
||||
this.title = title;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置浏览器窗口大小
|
||||
* @param width 宽度
|
||||
* @param height 高度
|
||||
*/
|
||||
public Builder size(int width, int height) {
|
||||
this.size = new Dimension(width, height);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置浏览器触发事件
|
||||
* @param handler 事件处理器
|
||||
*/
|
||||
public Builder operationHandler(WindowOperationHandler handler) {
|
||||
this.operationHandler = handler;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置浏览器图标
|
||||
* @param icon 图标
|
||||
*/
|
||||
public Builder icon(Image icon) {
|
||||
this.icon = icon;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置HTML路径
|
||||
*/
|
||||
public BrowserWindow build() {
|
||||
if (this.htmlPath == null || this.htmlPath.isEmpty()) {
|
||||
throw new IllegalArgumentException("HTML paths cannot be empty");
|
||||
@@ -68,6 +120,10 @@ public class BrowserWindow extends JFrame {
|
||||
return new BrowserWindow(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置HTML路径
|
||||
* @param path HTML路径
|
||||
*/
|
||||
public Builder htmlPath(String path) {
|
||||
this.htmlPath = path;
|
||||
return this;
|
||||
@@ -79,9 +135,12 @@ public class BrowserWindow extends JFrame {
|
||||
this.htmlPath = builder.htmlPath;
|
||||
this.operationHandler = builder.operationHandler;
|
||||
|
||||
// 设置图标(如果存在)
|
||||
if (builder.icon != null) {
|
||||
setIconImage(builder.icon);
|
||||
}
|
||||
|
||||
// 初始化浏览器组件
|
||||
try {
|
||||
this.browserComponent = initializeCef(builder);
|
||||
if (operationHandler != null) {
|
||||
@@ -89,11 +148,11 @@ public class BrowserWindow extends JFrame {
|
||||
}
|
||||
} catch (Exception e) {
|
||||
JOptionPane.showMessageDialog(this, "初始化失败: " + e.getMessage());
|
||||
//System.exit(1);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private Component initializeCef(Builder builder) throws MalformedURLException {
|
||||
if (!isInitialized && CefApp.getState() != CefApp.CefAppState.INITIALIZED) {
|
||||
try {
|
||||
@@ -150,14 +209,16 @@ public class BrowserWindow extends JFrame {
|
||||
int line
|
||||
) {
|
||||
// 格式化输出到 Java 控制台
|
||||
String log = String.format(
|
||||
"[Browser Console] %s %s (Line %d) -> %s",
|
||||
getLogLevelSymbol(level),
|
||||
source,
|
||||
line,
|
||||
message
|
||||
);
|
||||
System.out.println(log);
|
||||
//if (level != CefSettings.LogSeverity.LOGSEVERITY_WARNING) {
|
||||
String log = String.format(
|
||||
"[Browser Console] %s %s (Line %d) -> %s",
|
||||
getLogLevelSymbol(level),
|
||||
source,
|
||||
line,
|
||||
message
|
||||
);
|
||||
System.out.println(log);
|
||||
//}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -176,6 +237,112 @@ public class BrowserWindow extends JFrame {
|
||||
}
|
||||
});
|
||||
|
||||
client.addLifeSpanHandler(new CefLifeSpanHandlerAdapter() {
|
||||
@Override
|
||||
public boolean onBeforePopup(CefBrowser browser, CefFrame frame,
|
||||
String targetUrl, String targetFrameName) {
|
||||
try {
|
||||
Desktop.getDesktop().browse(new URI(targetUrl));
|
||||
} catch (Exception e) {
|
||||
System.out.println("Failed to open external browser: " + e.getMessage());
|
||||
}
|
||||
return true; // 拦截弹窗
|
||||
}
|
||||
});
|
||||
|
||||
client.addRequestHandler(new CefRequestHandlerAdapter() {
|
||||
@Override
|
||||
public boolean onBeforeBrowse(CefBrowser browser, CefFrame frame,
|
||||
CefRequest request, boolean userGesture, boolean isRedirect) {
|
||||
if (userGesture) {
|
||||
try {
|
||||
Desktop.getDesktop().browse(new URI(request.getURL()));
|
||||
return true; // 取消内置浏览器导航
|
||||
} catch (Exception e) {
|
||||
System.out.println("Failed to open external browser: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
client.addContextMenuHandler(new CefContextMenuHandlerAdapter() {
|
||||
@Override
|
||||
public void onBeforeContextMenu(CefBrowser browser, CefFrame frame,
|
||||
CefContextMenuParams params, CefMenuModel model) {
|
||||
model.clear();
|
||||
if (!params.getSelectionText().isEmpty() || params.isEditable()) {
|
||||
model.addItem(MENU_ID_USER_FIRST, "复制");
|
||||
}
|
||||
|
||||
if (params.isEditable()) {
|
||||
model.addItem(MENU_ID_USER_FIRST + 1, "粘贴");
|
||||
model.addItem(MENU_ID_USER_FIRST + 2, "粘贴纯文本");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onContextMenuCommand(CefBrowser browser, CefFrame frame,
|
||||
CefContextMenuParams params, int commandId, int eventFlags) {
|
||||
if (commandId == MENU_ID_USER_FIRST) {
|
||||
if (params.isEditable()) {
|
||||
browser.executeJavaScript("document.execCommand('copy');", browser.getURL(), 0);
|
||||
} else {
|
||||
browser.executeJavaScript(
|
||||
"window.getSelection().toString();",
|
||||
browser.getURL(),
|
||||
0
|
||||
);
|
||||
}
|
||||
return true;
|
||||
} else if (commandId == MENU_ID_USER_FIRST + 1) {
|
||||
pasteContent(browser, false);
|
||||
return true;
|
||||
} else if (commandId == MENU_ID_USER_FIRST + 2) {
|
||||
pasteContent(browser, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理粘贴操作
|
||||
* @param plainText 是否去除格式(纯文本模式)
|
||||
*/
|
||||
private void pasteContent(CefBrowser browser, boolean plainText) {
|
||||
try {
|
||||
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
|
||||
if (clipboard.isDataFlavorAvailable(DataFlavor.stringFlavor)) {
|
||||
String text = (String) clipboard.getData(DataFlavor.stringFlavor);
|
||||
|
||||
if (plainText) {
|
||||
text = text.replaceAll("<[^>]+>", "");
|
||||
}
|
||||
|
||||
String escapedText = text
|
||||
.replace("\\", "\\\\")
|
||||
.replace("'", "\\'")
|
||||
.replace("\n", "\\n")
|
||||
.replace("\r", "\\r");
|
||||
|
||||
String script = String.format(
|
||||
"if (document.activeElement) {\n" +
|
||||
" document.activeElement.value += '%s';\n" + // 简单追加文本
|
||||
" document.dispatchEvent(new Event('input', { bubbles: true }));\n" + // 触发输入事件
|
||||
"}",
|
||||
escapedText
|
||||
);
|
||||
browser.executeJavaScript(script, browser.getURL(), 0);
|
||||
}
|
||||
} catch (UnsupportedFlavorException | IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
// 3. 拦截所有新窗口(关键修复点!)
|
||||
//client.addLifeSpanHandler(new CefLifeSpanHandlerAdapter() {
|
||||
// @Override
|
||||
@@ -205,7 +372,7 @@ public class BrowserWindow extends JFrame {
|
||||
|
||||
// 6. 配置窗口布局(确保只添加一次)
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
getContentPane().removeAll(); // 清空已有组件
|
||||
getContentPane().removeAll();
|
||||
getContentPane().setLayout(new BorderLayout());
|
||||
|
||||
// 透明拖拽层(仅顶部可拖拽)
|
||||
@@ -243,7 +410,7 @@ public class BrowserWindow extends JFrame {
|
||||
|
||||
// 7. 窗口属性设置
|
||||
setTitle(builder.title);
|
||||
setSize(builder.size); // 直接设置尺寸,避免pack()计算错误
|
||||
setSize(builder.size);
|
||||
setLocationRelativeTo(null);
|
||||
setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
|
||||
|
||||
@@ -251,7 +418,7 @@ public class BrowserWindow extends JFrame {
|
||||
addWindowListener(new WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosed(WindowEvent e) {
|
||||
browser.close(true); // 强制关闭浏览器
|
||||
browser.close(true);
|
||||
client.dispose();
|
||||
cefApp.dispose();
|
||||
isInitialized = false;
|
||||
|
||||
@@ -0,0 +1,532 @@
|
||||
package com.axis.innovators.box.browser;
|
||||
|
||||
import com.axis.innovators.box.tools.FolderCreator;
|
||||
import org.cef.*;
|
||||
import org.cef.browser.*;
|
||||
import org.cef.callback.CefContextMenuParams;
|
||||
import org.cef.callback.CefMenuModel;
|
||||
import org.cef.callback.CefQueryCallback;
|
||||
import org.cef.handler.*;
|
||||
import org.cef.network.CefRequest;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.datatransfer.Clipboard;
|
||||
import java.awt.datatransfer.DataFlavor;
|
||||
import java.awt.datatransfer.UnsupportedFlavorException;
|
||||
import java.awt.event.*;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URI;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.cef.callback.CefMenuModel.MenuId.MENU_ID_USER_FIRST;
|
||||
|
||||
/**
|
||||
* @author tzdwindows 7
|
||||
*/
|
||||
public class BrowserWindowJDialog extends JDialog {
|
||||
private final String windowId;
|
||||
private CefApp cefApp;
|
||||
private CefClient client;
|
||||
private CefBrowser browser;
|
||||
private final Component browserComponent;
|
||||
private final String htmlPath;
|
||||
//private boolean isInitialized = false;
|
||||
private WindowOperationHandler operationHandler;
|
||||
private static Thread cefThread;
|
||||
private CefMessageRouter msgRouter;
|
||||
|
||||
public static class Builder {
|
||||
private String windowId;
|
||||
private String title = "JCEF Window";
|
||||
private Dimension size = new Dimension(800, 600);
|
||||
private WindowOperationHandler operationHandler;
|
||||
private String htmlPath;
|
||||
private Image icon;
|
||||
private JFrame parentFrame;
|
||||
private boolean resizable = true; // 默认允许调整大小
|
||||
private boolean maximizable = true; // 默认允许最大化
|
||||
private boolean minimizable = true; // 默认允许最小化
|
||||
|
||||
|
||||
public Builder resizable(boolean resizable) {
|
||||
this.resizable = resizable;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder maximizable(boolean maximizable) {
|
||||
this.maximizable = maximizable;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder minimizable(boolean minimizable) {
|
||||
this.minimizable = minimizable;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder(String windowId) {
|
||||
this.windowId = windowId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置浏览器窗口标题
|
||||
* @param title 标题
|
||||
*/
|
||||
public Builder title(String title) {
|
||||
this.title = title;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置浏览器窗口大小
|
||||
* @param width 宽度
|
||||
* @param height 高度
|
||||
*/
|
||||
public Builder size(int width, int height) {
|
||||
this.size = new Dimension(width, height);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置浏览器窗口父窗口
|
||||
* @param parent 父窗口
|
||||
*/
|
||||
public Builder parentFrame(JFrame parent) {
|
||||
this.parentFrame = parent;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 设置浏览器触发事件
|
||||
* @param handler 事件处理器
|
||||
*/
|
||||
public Builder operationHandler(WindowOperationHandler handler) {
|
||||
this.operationHandler = handler;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置浏览器图标
|
||||
* @param icon 图标
|
||||
*/
|
||||
public Builder icon(Image icon) {
|
||||
this.icon = icon;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置HTML路径
|
||||
*/
|
||||
public BrowserWindowJDialog build() {
|
||||
if (this.htmlPath == null || this.htmlPath.isEmpty()) {
|
||||
throw new IllegalArgumentException("HTML paths cannot be empty");
|
||||
}
|
||||
File htmlFile = new File(this.htmlPath);
|
||||
if (!htmlFile.exists()) {
|
||||
throw new RuntimeException("The HTML file does not exist: " + htmlFile.getAbsolutePath());
|
||||
}
|
||||
return new BrowserWindowJDialog(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置HTML路径
|
||||
* @param path HTML路径
|
||||
*/
|
||||
public Builder htmlPath(String path) {
|
||||
this.htmlPath = path;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
private BrowserWindowJDialog(Builder builder) {
|
||||
// 根据父窗口是否存在,设置是否为模态对话框
|
||||
super(builder.parentFrame, builder.title, builder.parentFrame != null);
|
||||
this.windowId = builder.windowId;
|
||||
this.htmlPath = builder.htmlPath;
|
||||
this.operationHandler = builder.operationHandler;
|
||||
|
||||
// 设置图标(如果存在)
|
||||
if (builder.icon != null) {
|
||||
setIconImage(builder.icon);
|
||||
}
|
||||
|
||||
// 初始化浏览器组件
|
||||
try {
|
||||
this.browserComponent = initializeCef(builder);
|
||||
if (operationHandler != null) {
|
||||
setupMessageHandlers(operationHandler);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
JOptionPane.showMessageDialog(this, "初始化失败: " + e.getMessage());
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
private static boolean isInitialized = false;
|
||||
private Component initializeCef(Builder builder) throws MalformedURLException {
|
||||
if (!isInitialized) {
|
||||
isInitialized = true;
|
||||
try {
|
||||
this.cefApp = CefAppManager.getInstance();
|
||||
CefAppManager.incrementBrowserCount();
|
||||
client = cefApp.createClient();
|
||||
|
||||
client.addDisplayHandler(new CefDisplayHandler (){
|
||||
@Override
|
||||
public void onAddressChange(CefBrowser browser, CefFrame frame, String url) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTitleChange(CefBrowser browser, String title) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void OnFullscreenModeChange(CefBrowser browser, boolean fullscreen) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTooltip(CefBrowser browser, String text) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStatusMessage(CefBrowser browser, String value) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onConsoleMessage(
|
||||
CefBrowser browser,
|
||||
CefSettings.LogSeverity level,
|
||||
String message,
|
||||
String source,
|
||||
int line
|
||||
) {
|
||||
// 格式化输出到 Java 控制台
|
||||
//if (level != CefSettings.LogSeverity.LOGSEVERITY_WARNING) {
|
||||
String log = String.format(
|
||||
"[Browser Console] %s %s (Line %d) -> %s",
|
||||
getLogLevelSymbol(level),
|
||||
source,
|
||||
line,
|
||||
message
|
||||
);
|
||||
System.out.println(log);
|
||||
//}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCursorChange(CefBrowser browser, int cursorType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
private String getLogLevelSymbol(CefSettings.LogSeverity level) {
|
||||
switch (level) {
|
||||
case LOGSEVERITY_ERROR: return "⛔";
|
||||
case LOGSEVERITY_WARNING: return "⚠️";
|
||||
case LOGSEVERITY_DEFAULT: return "🐞";
|
||||
default: return "ℹ️";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
client.addLifeSpanHandler(new CefLifeSpanHandlerAdapter() {
|
||||
@Override
|
||||
public boolean onBeforePopup(CefBrowser browser, CefFrame frame,
|
||||
String targetUrl, String targetFrameName) {
|
||||
try {
|
||||
Desktop.getDesktop().browse(new URI(targetUrl));
|
||||
} catch (Exception e) {
|
||||
System.out.println("Failed to open external browser: " + e.getMessage());
|
||||
}
|
||||
return true; // 拦截弹窗
|
||||
}
|
||||
});
|
||||
|
||||
client.addRequestHandler(new CefRequestHandlerAdapter() {
|
||||
@Override
|
||||
public boolean onBeforeBrowse(CefBrowser browser, CefFrame frame,
|
||||
CefRequest request, boolean userGesture, boolean isRedirect) {
|
||||
if (userGesture) {
|
||||
try {
|
||||
Desktop.getDesktop().browse(new URI(request.getURL()));
|
||||
return true; // 取消内置浏览器导航
|
||||
} catch (Exception e) {
|
||||
System.out.println("Failed to open external browser: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
client.addContextMenuHandler(new CefContextMenuHandlerAdapter() {
|
||||
@Override
|
||||
public void onBeforeContextMenu(CefBrowser browser, CefFrame frame,
|
||||
CefContextMenuParams params, CefMenuModel model) {
|
||||
model.clear();
|
||||
if (!params.getSelectionText().isEmpty() || params.isEditable()) {
|
||||
model.addItem(MENU_ID_USER_FIRST, "复制");
|
||||
}
|
||||
|
||||
if (params.isEditable()) {
|
||||
model.addItem(MENU_ID_USER_FIRST + 1, "粘贴");
|
||||
model.addItem(MENU_ID_USER_FIRST + 2, "粘贴纯文本");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onContextMenuCommand(CefBrowser browser, CefFrame frame,
|
||||
CefContextMenuParams params, int commandId, int eventFlags) {
|
||||
if (commandId == MENU_ID_USER_FIRST) {
|
||||
if (params.isEditable()) {
|
||||
browser.executeJavaScript("document.execCommand('copy');", browser.getURL(), 0);
|
||||
} else {
|
||||
browser.executeJavaScript(
|
||||
"window.getSelection().toString();",
|
||||
browser.getURL(),
|
||||
0
|
||||
);
|
||||
}
|
||||
return true;
|
||||
} else if (commandId == MENU_ID_USER_FIRST + 1) {
|
||||
pasteContent(browser, false);
|
||||
return true;
|
||||
} else if (commandId == MENU_ID_USER_FIRST + 2) {
|
||||
pasteContent(browser, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理粘贴操作
|
||||
* @param plainText 是否去除格式(纯文本模式)
|
||||
*/
|
||||
private void pasteContent(CefBrowser browser, boolean plainText) {
|
||||
try {
|
||||
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
|
||||
if (clipboard.isDataFlavorAvailable(DataFlavor.stringFlavor)) {
|
||||
String text = (String) clipboard.getData(DataFlavor.stringFlavor);
|
||||
|
||||
if (plainText) {
|
||||
text = text.replaceAll("<[^>]+>", "");
|
||||
}
|
||||
|
||||
String escapedText = text
|
||||
.replace("\\", "\\\\")
|
||||
.replace("'", "\\'")
|
||||
.replace("\n", "\\n")
|
||||
.replace("\r", "\\r");
|
||||
|
||||
String script = String.format(
|
||||
"if (document.activeElement) {\n" +
|
||||
" document.activeElement.value += '%s';\n" + // 简单追加文本
|
||||
" document.dispatchEvent(new Event('input', { bubbles: true }));\n" + // 触发输入事件
|
||||
"}",
|
||||
escapedText
|
||||
);
|
||||
browser.executeJavaScript(script, browser.getURL(), 0);
|
||||
}
|
||||
} catch (UnsupportedFlavorException | IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
// 3. 拦截所有新窗口(关键修复点!)
|
||||
//client.addLifeSpanHandler(new CefLifeSpanHandlerAdapter() {
|
||||
// @Override
|
||||
// public boolean onBeforePopup(CefBrowser browser,
|
||||
// CefFrame frame, String target_url, String target_frame_name) {
|
||||
// return true; // 返回true表示拦截弹窗
|
||||
// }
|
||||
//});
|
||||
|
||||
|
||||
Thread.currentThread().setName("BrowserRenderThread");
|
||||
|
||||
// 4. 加载HTML
|
||||
String fileUrl = new File(htmlPath).toURI().toURL().toString();
|
||||
System.out.println("Loading HTML from: " + fileUrl);
|
||||
|
||||
// 5. 创建浏览器组件(直接添加到内容面板)
|
||||
browser = client.createBrowser(fileUrl, false, false);
|
||||
|
||||
Component browserComponent = browser.getUIComponent();
|
||||
browser.executeJavaScript("console.log('Java -> HTML 消息测试')",null,2);
|
||||
|
||||
CefMessageRouter.CefMessageRouterConfig config = new CefMessageRouter.CefMessageRouterConfig();
|
||||
config.jsQueryFunction = "javaQuery";// 定义方法
|
||||
config.jsCancelFunction = "javaQueryCancel";// 定义取消方法
|
||||
|
||||
|
||||
// 6. 配置窗口布局(确保只添加一次)
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
getContentPane().removeAll();
|
||||
getContentPane().setLayout(new BorderLayout());
|
||||
|
||||
// 透明拖拽层(仅顶部可拖拽)
|
||||
JPanel dragPanel = new JPanel(new BorderLayout());
|
||||
dragPanel.setOpaque(false);
|
||||
|
||||
JPanel titleBar = new JPanel();
|
||||
titleBar.setOpaque(false);
|
||||
titleBar.setPreferredSize(new Dimension(builder.size.width, 20));
|
||||
final Point[] dragStart = new Point[1];
|
||||
titleBar.addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mousePressed(MouseEvent e) {
|
||||
dragStart[0] = e.getPoint();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseReleased(MouseEvent e) {
|
||||
dragStart[0] = null;
|
||||
}
|
||||
});
|
||||
titleBar.addMouseMotionListener(new MouseMotionAdapter() {
|
||||
@Override
|
||||
public void mouseDragged(MouseEvent e) {
|
||||
if (dragStart[0] != null) {
|
||||
Point curr = e.getLocationOnScreen();
|
||||
setLocation(curr.x - dragStart[0].x, curr.y - dragStart[0].y);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
dragPanel.add(titleBar, BorderLayout.NORTH);
|
||||
getContentPane().add(dragPanel, BorderLayout.CENTER);
|
||||
getContentPane().add(browserComponent, BorderLayout.CENTER);
|
||||
|
||||
// 7. 窗口属性设置
|
||||
setTitle(builder.title);
|
||||
setSize(builder.size);
|
||||
setLocationRelativeTo(builder.parentFrame);
|
||||
setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
|
||||
|
||||
// 8. 资源释放
|
||||
addWindowListener(new WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosed(WindowEvent e) {
|
||||
browser.close(true);
|
||||
client.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
setVisible(true);
|
||||
|
||||
});
|
||||
return browserComponent;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
JOptionPane.showMessageDialog(null, "初始化失败: " + e.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
} else {
|
||||
isInitialized = false;
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
dispose();
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void printStackTrace() {
|
||||
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
|
||||
for (int i = 2; i < stackTrace.length; i++) {
|
||||
StackTraceElement element = stackTrace[i];
|
||||
System.out.println(element.getClassName() + "." + element.getMethodName() +
|
||||
"(" + (element.getFileName() != null ? element.getFileName() : "Unknown Source") +
|
||||
":" + element.getLineNumber() + ")");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVisible(boolean b) {
|
||||
super.setVisible(b);
|
||||
}
|
||||
|
||||
public Component getBrowserComponent() {
|
||||
return browserComponent;
|
||||
}
|
||||
|
||||
private void setupMessageHandlers(WindowOperationHandler handler) {
|
||||
if (client != null) {
|
||||
msgRouter = CefMessageRouter.create();
|
||||
msgRouter.addHandler(new CefMessageRouterHandlerAdapter() {
|
||||
@Override
|
||||
public boolean onQuery(CefBrowser browser,
|
||||
CefFrame frame,
|
||||
long queryId,
|
||||
String request,
|
||||
boolean persistent,
|
||||
CefQueryCallback callback) {
|
||||
if (request.startsWith("system:")) {
|
||||
String[] parts = request.split(":");
|
||||
String operation = parts.length >= 2 ? parts[1] : null;
|
||||
String targetWindow = parts.length > 2 ? parts[2] : null;
|
||||
handler.handleOperation(
|
||||
new WindowOperation(operation, targetWindow, callback) // [!code ++]
|
||||
);
|
||||
return true;
|
||||
}
|
||||
if (request.startsWith("java-response:")) {
|
||||
String[] parts = request.split(":");
|
||||
String requestId = parts[1];
|
||||
String responseData = parts.length > 2 ? parts[2] : "";
|
||||
Consumer<String> handler = WindowRegistry.getInstance().getCallback(requestId);
|
||||
if (handler != null) {
|
||||
handler.accept(responseData);
|
||||
callback.success("");
|
||||
} else {
|
||||
callback.failure(-1, "无效的请求ID");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}, true);
|
||||
client.addMessageRouter(msgRouter);
|
||||
}
|
||||
}
|
||||
|
||||
public String getWindowId() {
|
||||
return windowId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取消息路由器
|
||||
* @return 消息路由器
|
||||
*/
|
||||
public CefMessageRouter getMsgRouter() {
|
||||
return msgRouter;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取浏览器对象
|
||||
* @return 浏览器对象
|
||||
*/
|
||||
public CefBrowser getBrowser() {
|
||||
return browser;
|
||||
}
|
||||
|
||||
public void closeWindow() {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
if (browser != null) {
|
||||
browser.close(true);
|
||||
}
|
||||
dispose();
|
||||
cefApp.dispose();
|
||||
WindowRegistry.getInstance().unregisterWindow(windowId);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.axis.innovators.box.browser;
|
||||
|
||||
import com.axis.innovators.box.tools.FolderCreator;
|
||||
import org.cef.CefApp;
|
||||
import org.cef.CefSettings;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class CefAppManager {
|
||||
private static CefApp cefApp;
|
||||
private static int browserCount = 0;
|
||||
private static boolean isInitialized = false;
|
||||
|
||||
static {
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
||||
disposeCefApp();
|
||||
}));
|
||||
}
|
||||
public static synchronized CefApp getInstance() throws Exception {
|
||||
// 关键修改:仅在第一次初始化时设置参数
|
||||
if (cefApp == null && !isInitialized) {
|
||||
CefSettings settings = new CefSettings();
|
||||
settings.windowless_rendering_enabled = false;
|
||||
settings.javascript_flags = "--expose-gc";
|
||||
settings.log_severity = CefSettings.LogSeverity.LOGSEVERITY_VERBOSE;
|
||||
|
||||
String subprocessPath = FolderCreator.getLibraryFolder() + "/jcef/lib/win64/jcef_helper.exe";
|
||||
validateSubprocessPath(subprocessPath);
|
||||
settings.browser_subprocess_path = subprocessPath;
|
||||
|
||||
cefApp = CefApp.getInstance(settings);
|
||||
isInitialized = true;
|
||||
} else if (cefApp == null) {
|
||||
// 后续调用使用无参数版本
|
||||
cefApp = CefApp.getInstance();
|
||||
}
|
||||
return cefApp;
|
||||
}
|
||||
|
||||
private static void validateSubprocessPath(String path) {
|
||||
File exeFile = new File(path);
|
||||
if (!exeFile.exists()) {
|
||||
throw new IllegalStateException("jcef_helper.exe not found at: " + path);
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized void incrementBrowserCount() {
|
||||
browserCount++;
|
||||
}
|
||||
|
||||
public static synchronized void decrementBrowserCount() {
|
||||
if (--browserCount <= 0) {
|
||||
disposeCefApp();
|
||||
}
|
||||
}
|
||||
|
||||
private static void disposeCefApp() {
|
||||
if (cefApp != null) {
|
||||
cefApp.dispose();
|
||||
cefApp = null;
|
||||
isInitialized = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,688 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>DeepSeek - 智能助手</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/intellij-light.min.css">
|
||||
<!-- KaTeX 核心样式 -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
|
||||
<!-- KaTeX 核心库 -->
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
|
||||
<!-- 自动渲染扩展 -->
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: #2d8cf0;
|
||||
--primary-hover: #57a3f3;
|
||||
--bg-color: #f8f9fa;
|
||||
--card-bg: rgba(255, 255, 255, 0.97);
|
||||
--text-primary: #1f2d3d;
|
||||
--text-secondary: #666;
|
||||
--shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.katex {
|
||||
font-size: 1.1em;
|
||||
padding: 0 0.2em;
|
||||
}
|
||||
.math-block {
|
||||
margin: 1em 0;
|
||||
padding: 1em;
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.math-block::-webkit-scrollbar {
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.message.ai.response {
|
||||
background: white; /* 恢复白色背景 */
|
||||
color: var(--text-primary); /* 恢复正常文字颜色 */
|
||||
}
|
||||
|
||||
.message.ai.response.collapsed .bubble {
|
||||
max-height: 6em;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.message.ai.response .fold-btn {
|
||||
display: none;
|
||||
background: linear-gradient(180deg, rgba(255,255,255,0) 0%, rgba(255,255,255,0.95) 60%);
|
||||
padding: 6px 12px;
|
||||
right: 15px;
|
||||
bottom: 10px;
|
||||
}
|
||||
|
||||
.message.ai.response.collapsed .fold-btn {
|
||||
display: block;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 20px auto;
|
||||
flex: 1;
|
||||
width: 95%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chat-container {
|
||||
background: var(--card-bg);
|
||||
backdrop-filter: blur(12px);
|
||||
border-radius: 16px;
|
||||
box-shadow: var(--shadow);
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.messages {
|
||||
flex: 1;
|
||||
padding: 24px;
|
||||
overflow-y: auto;
|
||||
scroll-behavior: smooth;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.message {
|
||||
max-width: 85%;
|
||||
opacity: 0;
|
||||
animation: messageIn 0.3s ease-out forwards;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@keyframes messageIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.message.user {
|
||||
align-self: flex-end;
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
border-radius: 20px 20px 4px 20px;
|
||||
}
|
||||
|
||||
.message.ai {
|
||||
align-self: flex-start;
|
||||
background: white;
|
||||
color: var(--text-secondary);
|
||||
border-radius: 4px 20px 20px 20px;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.bubble {
|
||||
padding: 16px 24px;
|
||||
line-height: 1.7;
|
||||
position: relative;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.streaming-cursor {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 1em;
|
||||
background: var(--text-secondary);
|
||||
margin-left: 4px;
|
||||
vertical-align: middle;
|
||||
animation: cursorPulse 1.2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes cursorPulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.3; }
|
||||
}
|
||||
|
||||
.typing-indicator {
|
||||
display: none;
|
||||
padding: 16px 24px;
|
||||
background: white;
|
||||
border-radius: 24px;
|
||||
margin: 12px 0;
|
||||
box-shadow: var(--shadow);
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.dot-flashing {
|
||||
position: relative;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
background-color: var(--text-secondary);
|
||||
animation: dotFlashing 1s infinite linear;
|
||||
}
|
||||
|
||||
@keyframes dotFlashing {
|
||||
0% { background-color: var(--text-secondary); }
|
||||
50%, 100% { background-color: rgba(94, 108, 130, 0.2); }
|
||||
}
|
||||
|
||||
.input-area {
|
||||
padding: 24px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
input {
|
||||
flex: 1;
|
||||
padding: 16px 24px;
|
||||
border: none;
|
||||
border-radius: 30px;
|
||||
background: white;
|
||||
font-size: 16px;
|
||||
box-shadow: var(--shadow);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px rgba(45, 140, 240, 0.2);
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 16px 32px;
|
||||
border: none;
|
||||
border-radius: 30px;
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: var(--primary-hover);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
pre {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
position: relative;
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 12px;
|
||||
padding: 6px 12px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background: rgba(0, 0, 0, 0.08);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.copy-btn:hover {
|
||||
background: rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.fold-btn {
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
bottom: 10px;
|
||||
background: linear-gradient(180deg, rgba(255,255,255,0) 0%, rgba(255,255,255,0.95) 60%);
|
||||
padding: 6px 12px;
|
||||
border: none;
|
||||
color: var(--primary-color);
|
||||
font-size: 0.85em;
|
||||
cursor: pointer;
|
||||
display: none;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.message.collapsed .fold-btn,
|
||||
.message:not(.streaming):hover .fold-btn {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.message.collapsed .bubble {
|
||||
max-height: 6em;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.message.collapsed .fold-btn::after {
|
||||
content: "展开";
|
||||
}
|
||||
|
||||
.message:not(.collapsed) .fold-btn::after {
|
||||
content: "收起";
|
||||
}
|
||||
|
||||
.thinking-content {
|
||||
color: #888;
|
||||
font-style: italic;
|
||||
margin-bottom: 12px;
|
||||
padding: 8px 12px;
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.toast {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
color: white;
|
||||
padding: 12px 24px;
|
||||
border-radius: 30px;
|
||||
font-size: 0.9em;
|
||||
animation: toastIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes toastIn {
|
||||
from { opacity: 0; transform: translate(-50%, 10px); }
|
||||
to { opacity: 1; transform: translate(-50%, 0); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/java.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/python.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/javascript.min.js"></script>
|
||||
|
||||
<div class="container">
|
||||
<div class="chat-container">
|
||||
<div class="messages" id="messages">
|
||||
<div class="typing-indicator" id="typing">
|
||||
<div class="dot-flashing"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-area">
|
||||
<div class="input-wrapper">
|
||||
<input type="text"
|
||||
id="input"
|
||||
placeholder="输入您的问题..."
|
||||
autocomplete="off">
|
||||
<button onclick="sendMessage()">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z"/>
|
||||
</svg>
|
||||
发送
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 初始化配置
|
||||
marked.setOptions({
|
||||
breaks: true,
|
||||
highlight: code => hljs.highlightAuto(code).value
|
||||
});
|
||||
|
||||
marked.use({
|
||||
extensions: [
|
||||
{
|
||||
name: 'math',
|
||||
level: 'block',
|
||||
start(src) { return src.indexOf('$$') !== -1; },
|
||||
tokenizer(src) {
|
||||
const match = src.match(/^\$\$((?:\\\$|[\s\S])+?)\$\$/m); // 修正正则
|
||||
if (match) {
|
||||
return {
|
||||
type: 'math',
|
||||
raw: match[0],
|
||||
text: match[1].trim()
|
||||
};
|
||||
}
|
||||
},
|
||||
renderer(token) {
|
||||
return `<div class="math-block">${token.text}</div>`;
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'inlineMath',
|
||||
level: 'inline',
|
||||
start(src) { return src.indexOf('$') !== -1; },
|
||||
tokenizer(src) {
|
||||
const match = src.match(/^\$((?:\\\$|[^$])+?)\$/);
|
||||
if (match) {
|
||||
return {
|
||||
type: 'inlineMath',
|
||||
raw: match[0],
|
||||
text: match[1].trim()
|
||||
};
|
||||
}
|
||||
},
|
||||
renderer(token) {
|
||||
return `<span class="math-inline">${token.text}</span>`;
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
hljs.configure({
|
||||
languages: ['java', 'python', 'javascript', 'typescript'],
|
||||
cssSelector: 'pre code',
|
||||
ignoreUnescapedHTML: true
|
||||
});
|
||||
|
||||
// CEF通信桥接
|
||||
window.javaQuery = window.cefQuery ? (request, success, error) => {
|
||||
window.cefQuery({
|
||||
request,
|
||||
onSuccess: success,
|
||||
onFailure: (code, msg) => error?.(msg)
|
||||
});
|
||||
} : console.error;
|
||||
|
||||
// 流式响应处理器
|
||||
const streams = new Map();
|
||||
|
||||
window.updateResponse = (requestId, content) => {
|
||||
if (content === '[end]') {
|
||||
finalizeStream(requestId);
|
||||
return;
|
||||
}
|
||||
|
||||
let stream = streams.get(requestId);
|
||||
if (!stream) {
|
||||
stream = {
|
||||
buffer: "",
|
||||
element: createMessageElement(requestId),
|
||||
cursorTimer: null,
|
||||
isCompleted: false,
|
||||
isResponse: true
|
||||
};
|
||||
streams.set(requestId, stream);
|
||||
stream.element.classList.add('response');
|
||||
startCursorAnimation(requestId);
|
||||
hideTyping();
|
||||
}
|
||||
|
||||
// 累积内容到缓冲区
|
||||
stream.buffer += content;
|
||||
|
||||
// 更新 DOM 并触发排版
|
||||
renderContent(requestId);
|
||||
maintainScroll();
|
||||
};
|
||||
|
||||
function hideTyping() {
|
||||
document.getElementById('typing').style.display = 'none';
|
||||
}
|
||||
|
||||
function handleThinkingContent(stream, content) {
|
||||
const parts = content.split('</think>');
|
||||
if (parts[0]) {
|
||||
stream.hasThinking = true;
|
||||
appendThinkingContent(stream, parts[0]);
|
||||
}
|
||||
if (parts[1]) {
|
||||
stream.buffer += parts[1];
|
||||
}
|
||||
}
|
||||
|
||||
function appendThinkingContent(stream, content) {
|
||||
const thinkingDiv = document.createElement('div');
|
||||
thinkingDiv.className = 'thinking-content';
|
||||
thinkingDiv.textContent = content.replace('<think>', '').trim();
|
||||
stream.element.querySelector('.content').appendChild(thinkingDiv);
|
||||
}
|
||||
|
||||
function startCursorAnimation(requestId) {
|
||||
const stream = streams.get(requestId);
|
||||
if (!stream) return;
|
||||
|
||||
stream.cursorTimer = setInterval(() => {
|
||||
if (stream.isCompleted) {
|
||||
clearInterval(stream.cursorTimer);
|
||||
return;
|
||||
}
|
||||
const cursor = stream.element.querySelector('.streaming-cursor');
|
||||
if (cursor) {
|
||||
cursor.style.opacity = cursor.style.opacity === '1' ? '0.3' : '1';
|
||||
}
|
||||
}, 600);
|
||||
}
|
||||
|
||||
function renderContent(requestId) {
|
||||
const stream = streams.get(requestId);
|
||||
if (!stream) return;
|
||||
|
||||
const contentDiv = stream.element.querySelector('.content');
|
||||
const rawContent = stream.buffer;
|
||||
|
||||
// 使用 Marked 解析内容
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = marked.parse(rawContent);
|
||||
|
||||
// 高亮代码块
|
||||
tempDiv.querySelectorAll('pre code').forEach(block => {
|
||||
hljs.highlightElement(block);
|
||||
});
|
||||
|
||||
// 更新 DOM
|
||||
contentDiv.innerHTML = tempDiv.innerHTML;
|
||||
|
||||
// 手动触发 KaTeX 渲染(核心修正)
|
||||
if (window.renderMathInElement) {
|
||||
renderMathInElement(contentDiv, {
|
||||
delimiters: [
|
||||
{ left: '$$', right: '$$', display: true },
|
||||
{ left: '$', right: '$', display: false }
|
||||
],
|
||||
throwOnError: false, // 忽略错误,允许未闭合公式临时显示
|
||||
strict: false // 宽松模式,兼容不完整语法
|
||||
});
|
||||
} else {
|
||||
console.error('KaTeX 自动渲染扩展未加载');
|
||||
}
|
||||
|
||||
// 显示流式光标
|
||||
if (!stream.isCompleted) {
|
||||
contentDiv.innerHTML += '<div class="streaming-cursor"></div>';
|
||||
}
|
||||
|
||||
addCopyButtons(contentDiv);
|
||||
}
|
||||
|
||||
function createMessageElement(requestId) {
|
||||
const element = document.createElement('div');
|
||||
element.className = 'message ai response'; // 添加response类
|
||||
element.innerHTML = `
|
||||
<div class="bubble">
|
||||
<div class="content"></div>
|
||||
<div class="streaming-cursor"></div>
|
||||
<button class="fold-btn" onclick="toggleFold(event)">展开</button>
|
||||
</div>
|
||||
`;
|
||||
messages.insertBefore(element, typing);
|
||||
return element;
|
||||
}
|
||||
|
||||
function finalizeStream(requestId) {
|
||||
const stream = streams.get(requestId);
|
||||
if (stream) {
|
||||
clearInterval(stream.cursorTimer);
|
||||
stream.isCompleted = true;
|
||||
|
||||
// 移除光标
|
||||
const cursor = stream.element.querySelector('.streaming-cursor');
|
||||
if (cursor) cursor.remove();
|
||||
|
||||
// 自动折叠逻辑
|
||||
checkCollapsible(stream.element);
|
||||
addFoldButton(stream.element);
|
||||
|
||||
streams.delete(requestId);
|
||||
}
|
||||
}
|
||||
|
||||
function checkCollapsible(element) {
|
||||
const bubble = element.querySelector('.bubble');
|
||||
const lineHeight = parseInt(getComputedStyle(bubble).lineHeight);
|
||||
if (bubble.scrollHeight > lineHeight * 5) {
|
||||
element.classList.add('collapsed');
|
||||
}
|
||||
}
|
||||
|
||||
function addFoldButton(element) {
|
||||
const btn = element.querySelector('.fold-btn');
|
||||
btn.style.display = 'block';
|
||||
btn.textContent = element.classList.contains('collapsed') ? '展开' : '收起';
|
||||
}
|
||||
|
||||
function toggleFold(event) {
|
||||
const btn = event.target;
|
||||
const message = btn.closest('.message');
|
||||
const beforeHeight = messages.scrollHeight;
|
||||
|
||||
message.classList.toggle('collapsed');
|
||||
btn.textContent = message.classList.contains('collapsed') ? '展开' : '收起';
|
||||
|
||||
const heightDiff = messages.scrollHeight - beforeHeight;
|
||||
messages.scrollTop += heightDiff;
|
||||
}
|
||||
|
||||
function maintainScroll() {
|
||||
const threshold = 100;
|
||||
const container = document.getElementById('messages');
|
||||
const isNearBottom = container.scrollHeight - container.scrollTop - container.clientHeight <= threshold;
|
||||
|
||||
if (isNearBottom) {
|
||||
requestAnimationFrame(() => {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function addCopyButtons(container) {
|
||||
container.querySelectorAll('pre').forEach(pre => {
|
||||
if (!pre.querySelector('.copy-btn')) {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'copy-btn';
|
||||
btn.textContent = '复制';
|
||||
btn.onclick = () => copyCode(pre);
|
||||
pre.prepend(btn);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function copyCode(pre) {
|
||||
const code = pre.querySelector('code')?.textContent || '';
|
||||
navigator.clipboard.writeText(code).then(() => {
|
||||
showToast('代码已复制');
|
||||
});
|
||||
}
|
||||
|
||||
function showToast(message) {
|
||||
const toast = document.createElement('div');
|
||||
toast.className = 'toast';
|
||||
toast.textContent = message;
|
||||
document.body.appendChild(toast);
|
||||
setTimeout(() => toast.remove(), 2000);
|
||||
}
|
||||
|
||||
function sendMessage() {
|
||||
const input = document.getElementById('input');
|
||||
const prompt = input.value.trim();
|
||||
if (!prompt) return;
|
||||
|
||||
const requestId = `req_${Date.now()}_${Math.random().toString(36).substr(2, 4)}`;
|
||||
|
||||
const userMsg = document.createElement('div');
|
||||
userMsg.className = 'message user';
|
||||
userMsg.innerHTML = `
|
||||
<div class="bubble">${marked.parse(prompt)}</div>
|
||||
`;
|
||||
messages.insertBefore(userMsg, typing);
|
||||
messages.scrollTop = messages.scrollHeight;
|
||||
|
||||
showTyping(true);
|
||||
input.value = '';
|
||||
|
||||
window.javaQuery(
|
||||
`ai-inference:${requestId}:${prompt}`,
|
||||
response => {
|
||||
if (response.startsWith("COMPLETED:")) {
|
||||
finalizeStream(requestId);
|
||||
}
|
||||
},
|
||||
error => showError(requestId, error)
|
||||
);
|
||||
}
|
||||
|
||||
function showError(requestId, message) {
|
||||
const stream = streams.get(requestId);
|
||||
if (stream) {
|
||||
stream.element.innerHTML = `
|
||||
<div class="bubble error">
|
||||
<strong>⚠️ 请求失败:</strong> ${message}
|
||||
</div>
|
||||
`;
|
||||
streams.delete(requestId);
|
||||
}
|
||||
showTyping(false);
|
||||
}
|
||||
|
||||
function showTyping(show) {
|
||||
typing.style.display = show ? 'block' : 'none';
|
||||
if (show) messages.scrollTop = messages.scrollHeight;
|
||||
}
|
||||
|
||||
document.getElementById('input').addEventListener('keypress', e => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
sendMessage();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
renderMathInElement(document.body, {
|
||||
delimiters: [
|
||||
{ left: '$$', right: '$$', display: true }, // 块级公式
|
||||
{ left: '$', right: '$', display: false }, // 行内公式
|
||||
{ left: '\\[', right: '\\]', display: true }, // LaTeX 环境
|
||||
{ left: '\\(', right: '\\)', display: false }
|
||||
],
|
||||
throwOnError: false // 忽略渲染错误
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -8,14 +8,18 @@ import org.cef.handler.CefMessageRouterHandlerAdapter;
|
||||
import org.tzd.lm.LM;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.io.InputStream;
|
||||
import java.io.PrintStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
* 这是一个简单的示例程序,用于展示如何使用JCEF来创建一个简单的浏览器窗口。
|
||||
* @author tzdwindows 7
|
||||
*/
|
||||
public class MainApplication {
|
||||
private static final ExecutorService executor = Executors.newCachedThreadPool();
|
||||
@@ -27,12 +31,63 @@ public class MainApplication {
|
||||
modelHandle = LM.llamaLoadModelFromFile(LM.DEEP_SEEK);
|
||||
ctxHandle = LM.createContext(modelHandle);
|
||||
|
||||
AtomicReference<BrowserWindow> window = new AtomicReference<>();
|
||||
AtomicReference<BrowserWindowJDialog> window = new AtomicReference<>();
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
WindowRegistry.getInstance().createNewWindow("main", builder ->
|
||||
window.set(builder.title("Axis Innovators Box")
|
||||
WindowRegistry.getInstance().createNewChildWindow("main", builder ->
|
||||
window.set(builder.title("Axis Innovators Box AI 工具箱")
|
||||
.icon(new ImageIcon(Objects.requireNonNull(MainApplication.class.getClassLoader().getResource("icons/logo.png"))).getImage())
|
||||
.size(1280, 720)
|
||||
.htmlPath("C:\\Users\\Administrator\\MCreatorWorkspaces\\AxisInnovatorsBox\\src\\main\\java\\com\\axis\\innovators\\box\\browser\\DeepSeek - 探索未至之境.html")
|
||||
.htmlPath(Objects.requireNonNull(MainApplication.class.getClassLoader().getResource("javascript/AIaToolbox.html")).getFile())
|
||||
.operationHandler(createOperationHandler())
|
||||
.build())
|
||||
);
|
||||
|
||||
CefMessageRouter msgRouter = window.get().getMsgRouter();
|
||||
if (msgRouter != null) {
|
||||
msgRouter.addHandler(new CefMessageRouterHandlerAdapter() {
|
||||
@Override
|
||||
public boolean onQuery(CefBrowser browser, CefFrame frame, long queryId,
|
||||
String request, boolean persistent, CefQueryCallback callback) {
|
||||
// 处理浏览器请求
|
||||
handleBrowserQuery(browser, request, callback);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onQueryCanceled(CefBrowser browser, CefFrame frame, long queryId) {
|
||||
// 处理请求取消
|
||||
}
|
||||
}, true);
|
||||
}
|
||||
});
|
||||
|
||||
// 关闭钩子
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
||||
LM.llamaFreeContext(ctxHandle);
|
||||
LM.llamaFreeModel(modelHandle);
|
||||
executor.shutdown();
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 弹出AI窗口
|
||||
* @param parent 父窗口
|
||||
*/
|
||||
public static void popupAIWindow
|
||||
(JFrame parent) {
|
||||
LM.loadLibrary(LM.CUDA);
|
||||
modelHandle = LM.llamaLoadModelFromFile(LM.DEEP_SEEK);
|
||||
ctxHandle = LM.createContext(modelHandle);
|
||||
|
||||
AtomicReference<BrowserWindowJDialog> window = new AtomicReference<>();
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
WindowRegistry.getInstance().createNewChildWindow("main", builder ->
|
||||
window.set(builder.title("Axis Innovators Box AI 工具箱")
|
||||
.parentFrame(parent)
|
||||
.icon(new ImageIcon(Objects.requireNonNull(MainApplication.class.getClassLoader().getResource("icons/logo.png"))).getImage())
|
||||
.size(1280, 720)
|
||||
.htmlPath(Objects.requireNonNull(MainApplication.class.getClassLoader().getResource("javascript/AIaToolbox_dark.html")).getFile())
|
||||
.operationHandler(createOperationHandler())
|
||||
.build())
|
||||
);
|
||||
@@ -78,13 +133,9 @@ public class MainApplication {
|
||||
|
||||
if ("ai-inference".equals(operation)) {
|
||||
executor.execute(() -> {
|
||||
//String system;
|
||||
if (isSystem) {
|
||||
//system = ;
|
||||
isSystem = false;
|
||||
} //else {
|
||||
//system = null;
|
||||
//}
|
||||
}
|
||||
List<String> messageList = new java.util.ArrayList<>(List.of());
|
||||
// 修改后的推理回调处理
|
||||
String jsCode = String.format(
|
||||
@@ -96,27 +147,10 @@ public class MainApplication {
|
||||
"<summary>推理内容</summary><font face=\\\"黑体\\\" color=grey size=3>"
|
||||
);
|
||||
browser.executeJavaScript(jsCode, null, 0);
|
||||
LM.inference(modelHandle, ctxHandle, 0.6f, prompt + "<think>\n",
|
||||
"""
|
||||
# 角色设定
|
||||
你是一个严格遵循规则的AI助手,
|
||||
当遇到简单的问题时你可以直接回答(回答先添加</think>),在遇到复杂问题时请你思考后再给出答案,并且需满足以下要求:
|
||||
|
||||
## 核心指令
|
||||
1. **强制推理标记**:无论是否推理,回答**必须**以`<think>`开头,推理结束时闭合`</think>`。
|
||||
2. **推理流程**:
|
||||
- 使用固定句式(如“好的,用户现在需要...”)启动分析
|
||||
3. **回答规范**:
|
||||
- 使用Markdown标题、列表、加粗突出重点
|
||||
- LaTeX公式严格包裹在`$$...$$`中(示例:`$$E=mc^2$$`)
|
||||
- 不同可能性答案用列表呈现
|
||||
|
||||
## 违规惩罚
|
||||
- 若未包含`<think>`标签,需在回答开头添加 </think>
|
||||
""",
|
||||
LM.inference(modelHandle, ctxHandle, 0.6f, prompt + "<think>\n","",
|
||||
new LM.MessageCallback() {
|
||||
private boolean thinkingClosed = false;
|
||||
|
||||
//
|
||||
@Override
|
||||
public void onMessage(String message) {
|
||||
messageList.add(message);
|
||||
@@ -128,7 +162,7 @@ public class MainApplication {
|
||||
.replace("\"", "\\\"")
|
||||
.replace("\n", "\\n")
|
||||
.replace("\r", "\\r");
|
||||
|
||||
//
|
||||
if (messageList.contains("</think>") && !thinkingClosed) {
|
||||
String endJs = String.format(
|
||||
"if (typeof updateResponse === 'function') {" +
|
||||
@@ -139,8 +173,10 @@ public class MainApplication {
|
||||
browser.executeJavaScript(endJs, null, 0);
|
||||
thinkingClosed = true;
|
||||
}
|
||||
|
||||
//
|
||||
// 实时更新内容
|
||||
System.setOut(new PrintStream(System.out, true, StandardCharsets.UTF_8));
|
||||
//
|
||||
String jsCode = String.format(
|
||||
"if (typeof updateResponse === 'function') {" +
|
||||
" updateResponse('%s', '%s');" +
|
||||
@@ -152,6 +188,17 @@ public class MainApplication {
|
||||
}
|
||||
},isSystem);
|
||||
messageList.clear();
|
||||
//jsCode = String.format(
|
||||
// "if (typeof updateResponse === 'function') {" +
|
||||
// " updateResponse('%s', '%s');" +
|
||||
// "}",
|
||||
// requestId, "嗯,用户问的是“请直接告诉我傅里叶变换公式”。首先,我需要回忆一下傅里叶变换的基本知识。傅里叶变换是将一个时间域的信号转换为频率域的信号,它在工程和科学研究中有着广泛的应用。\n\n接下来,我要确定傅里叶变换的数学表达式。标准形式应该是$$F(\\omega) = \\int_{-\\infty}^{\\infty} f(t) e^{-i\\omega t} dt$$。这里,$f(t)$是原函数,$e^{-i\\omega t}$是指数函数,$\\omega$是频率变量。\n\n然后,我需要考虑是否有其他形式的傅里叶变换,比如离散形式或逆变换。通常,离散傅里叶变换(DFT)使用$$X[k] = \\sum_{n=0}^{N-1} x[n] e^{-i2\\pi kn/N}$$来表示,而逆变换则是$$x[n] = \\frac{1}{N} \\sum_{k=0}^{N-1} X[k] e^{i2\\pi kn/N}$$。不过,用户的问题比较直接,可能只关注基本的连续形式。\n\n最后,我要确保回答准确无误,并且按照用户的格式要求使用标准的 LaTeX æ式来呈现。\n</think>\n\n傅里叶变换的基本公式是:$$F(\\omega) = \\int_{-\\infty}^{\\infty} f(t) e^{-i\\omega t} dt$$".replace("\\", "\\\\")
|
||||
// .replace("'", "\\'")
|
||||
// .replace("\"", "\\\"")
|
||||
// .replace("\n", "\\n")
|
||||
// .replace("\r", "\\r")
|
||||
//);
|
||||
//browser.executeJavaScript(jsCode, null, 0);
|
||||
callback.success("COMPLETED:" + requestId);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,26 +5,5 @@ import org.cef.callback.CefQueryCallback;
|
||||
/**
|
||||
* @author tzdwindows 7
|
||||
*/
|
||||
public class WindowOperation {
|
||||
private final String type;
|
||||
private final String targetWindow;
|
||||
private final CefQueryCallback callback;
|
||||
|
||||
public WindowOperation(String type, String targetWindow, CefQueryCallback callback) {
|
||||
this.type = type;
|
||||
this.targetWindow = targetWindow;
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public String getTargetWindow() {
|
||||
return targetWindow;
|
||||
}
|
||||
|
||||
public CefQueryCallback getCallback() {
|
||||
return callback;
|
||||
}
|
||||
public record WindowOperation(String type, String targetWindow, CefQueryCallback callback) {
|
||||
}
|
||||
|
||||
@@ -83,12 +83,12 @@ public class WindowOperationHandler {
|
||||
}
|
||||
|
||||
public void handleOperation(WindowOperation operation) {
|
||||
Consumer<String> handler = operations.get(operation.getType());
|
||||
Consumer<String> handler = operations.get(operation.type());
|
||||
if (handler != null) {
|
||||
handler.accept(operation.getTargetWindow());
|
||||
operation.getCallback().success("操作成功: " + operation.getType());
|
||||
handler.accept(operation.targetWindow());
|
||||
operation.callback().success("操作成功: " + operation.type());
|
||||
} else {
|
||||
operation.getCallback().failure(-1, "未定义的操作: " + operation.getType());
|
||||
operation.callback().failure(-1, "未定义的操作: " + operation.type());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,8 @@ public class WindowRegistry {
|
||||
private static WindowRegistry instance;
|
||||
private final ConcurrentMap<String, BrowserWindow> windows =
|
||||
new ConcurrentHashMap<>();
|
||||
private final ConcurrentMap<String, BrowserWindowJDialog> childWindows =
|
||||
new ConcurrentHashMap<>();
|
||||
private final Map<String, Consumer<String>> callbacks = new ConcurrentHashMap<>();
|
||||
|
||||
private WindowRegistry() {}
|
||||
@@ -23,6 +25,10 @@ public class WindowRegistry {
|
||||
windows.put(window.getWindowId(), window);
|
||||
}
|
||||
|
||||
public void registerChildWindow(BrowserWindowJDialog window) {
|
||||
childWindows.put(window.getWindowId(), window);
|
||||
}
|
||||
|
||||
public void registerCallback(String requestId, Consumer<String> handler) {
|
||||
callbacks.put(requestId, handler);
|
||||
}
|
||||
@@ -42,10 +48,27 @@ public class WindowRegistry {
|
||||
return windows.get(windowId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个新的窗口
|
||||
* @param windowId 窗口ID
|
||||
* @param config 窗口配置
|
||||
*/
|
||||
public void createNewWindow(String windowId, Consumer<BrowserWindow.Builder> config) {
|
||||
BrowserWindow.Builder builder = new BrowserWindow.Builder(windowId);
|
||||
config.accept(builder);
|
||||
BrowserWindow window = builder.build();
|
||||
registerWindow(window);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个新的子窗口
|
||||
* @param windowId 窗口ID
|
||||
* @param config 窗口配置
|
||||
*/
|
||||
public void createNewChildWindow(String windowId, Consumer<BrowserWindowJDialog.Builder> config) {
|
||||
BrowserWindowJDialog.Builder builder = new BrowserWindowJDialog.Builder(windowId);
|
||||
config.accept(builder);
|
||||
BrowserWindowJDialog window = builder.build();
|
||||
registerChildWindow(window);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
<script>
|
||||
// 必须定义此函数以接收 Java 消息
|
||||
function javaMessageReceived(requestId, message) {
|
||||
console.log("[HTML] 收到 Java 消息:", requestId, message);
|
||||
// 示例:将消息转为大写并返回
|
||||
const response = message.toUpperCase();
|
||||
window.cefQuery({
|
||||
request: 'java-response:' + requestId + ':' + response,
|
||||
onSuccess: function() {},
|
||||
onFailure: function() {}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.axis.innovators.box.register;
|
||||
|
||||
import com.axis.innovators.box.AxisInnovatorsBox;
|
||||
import com.axis.innovators.box.browser.MainApplication;
|
||||
import com.axis.innovators.box.gui.FridaWindow;
|
||||
import com.axis.innovators.box.gui.LocalWindow;
|
||||
import com.axis.innovators.box.gui.MainWindow;
|
||||
@@ -65,8 +66,10 @@ public class RegistrationTool {
|
||||
}
|
||||
|
||||
Window owner = SwingUtilities.windowForComponent((Component) e.getSource());
|
||||
LocalWindow dialog = new LocalWindow(owner);
|
||||
main.popupWindow(dialog);
|
||||
// 这是被抛弃的界面,在后面的版本可能会删除
|
||||
//LocalWindow dialog = new LocalWindow(owner);
|
||||
//main.popupWindow(dialog);
|
||||
MainApplication.popupAIWindow((JFrame)owner);
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
@@ -0,0 +1,390 @@
|
||||
package com.axis.innovators.box.speech;
|
||||
|
||||
import be.tarsos.dsp.AudioDispatcher;
|
||||
import be.tarsos.dsp.AudioEvent;
|
||||
import be.tarsos.dsp.io.TarsosDSPAudioFormat;
|
||||
import be.tarsos.dsp.io.jvm.AudioDispatcherFactory;
|
||||
import be.tarsos.dsp.io.jvm.JVMAudioInputStream;
|
||||
import be.tarsos.dsp.io.jvm.WaveformWriter;
|
||||
import com.google.gson.*;
|
||||
import org.vosk.*;
|
||||
import org.apache.commons.math3.util.Precision;
|
||||
import net.sourceforge.pinyin4j.PinyinHelper;
|
||||
import javax.sound.sampled.*;
|
||||
import java.io.*;
|
||||
import java.lang.reflect.Field;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.file.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public class HighAccuracySpeechRecognition {
|
||||
private final Model model;
|
||||
private final String outputPath;
|
||||
private final int sampleRate = 44100;
|
||||
private final TarsosDSPAudioFormat audioFormat = new TarsosDSPAudioFormat(
|
||||
44100,
|
||||
16,
|
||||
1,
|
||||
true,
|
||||
false
|
||||
);
|
||||
|
||||
static {
|
||||
LibVosk.setLogLevel(LogLevel.WARNINGS);
|
||||
}
|
||||
|
||||
public HighAccuracySpeechRecognition(String modelPath, String outputDir) throws IOException {
|
||||
this.model = new Model(modelPath);
|
||||
|
||||
this.outputPath = outputDir;
|
||||
Files.createDirectories(Paths.get(outputDir));
|
||||
}
|
||||
|
||||
public void processAudio(Path audioFile) throws Exception {
|
||||
Path convertedFile = null;
|
||||
try {
|
||||
convertedFile = convertToPCM(audioFile);
|
||||
try (Recognizer recognizer = new Recognizer(model, sampleRate);
|
||||
AudioInputStream ais = AudioSystem.getAudioInputStream(convertedFile.toFile())) {
|
||||
|
||||
byte[] buffer = new byte[4096];
|
||||
List<WordInfo> wordList = new ArrayList<>();
|
||||
|
||||
while (ais.read(buffer) >= 0) {
|
||||
if (recognizer.acceptWaveForm(buffer, buffer.length)) {
|
||||
parseResult(recognizer.getResult(), wordList);
|
||||
}
|
||||
}
|
||||
|
||||
parseResult(recognizer.getFinalResult(), wordList);
|
||||
segmentAudio(convertedFile, wordList);
|
||||
}
|
||||
} finally {
|
||||
// 增强的文件删除逻辑
|
||||
if (convertedFile != null) {
|
||||
deleteWithRetry(convertedFile, 5, 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 新增重试删除方法
|
||||
private void deleteWithRetry(Path path, int maxRetries, long intervalMillis) throws IOException {
|
||||
int retryCount = 0;
|
||||
while (Files.exists(path)) {
|
||||
try {
|
||||
Files.delete(path);
|
||||
break;
|
||||
} catch (IOException ex) {
|
||||
if (++retryCount >= maxRetries) {
|
||||
throw ex;
|
||||
}
|
||||
try {
|
||||
// 强制释放文件句柄
|
||||
System.gc();
|
||||
Thread.sleep(intervalMillis);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IOException("删除中断", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void parseResult(String json, List<WordInfo> wordList) {
|
||||
if (json == null || json.isEmpty()) return;
|
||||
|
||||
System.out.println("Processing result: " + json);
|
||||
try {
|
||||
JsonObject result = JsonParser.parseString(json).getAsJsonObject();
|
||||
|
||||
if (result.has("text")) {
|
||||
String fullText = result.get("text").getAsString();
|
||||
processRawText(fullText, wordList);
|
||||
} else if (result.has("result") && result.get("result").isJsonArray()) {
|
||||
JsonArray words = result.getAsJsonArray("result");
|
||||
|
||||
words.forEach(w -> {
|
||||
if (w.isJsonObject()) {
|
||||
JsonObject word = w.getAsJsonObject();
|
||||
if (result.has("result") && result.get("result").isJsonArray()) {
|
||||
wordList.add(new WordInfo(
|
||||
word.get("word").getAsString(),
|
||||
Precision.round(word.get("start").getAsDouble(), 3),
|
||||
Precision.round(word.get("end").getAsDouble(), 3),
|
||||
(float) word.get("conf").getAsDouble()
|
||||
));
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
System.err.println("Warning: Invalid recognition result - " + json);
|
||||
}
|
||||
} catch (JsonSyntaxException e) {
|
||||
throw new AudioProcessingException("Invalid JSON result: " + json, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void processRawText(String rawText, List<WordInfo> wordList) {
|
||||
// 使用正则表达式过滤无效字符
|
||||
String cleanedText = rawText.replaceAll("[^\\u4e00-\\u9fa5a-zA-Z0-9]", " ")
|
||||
.replaceAll("\\s+", " ")
|
||||
.trim();
|
||||
|
||||
if (!cleanedText.isEmpty()) {
|
||||
// 对无法切分的时间信息进行估算(示例逻辑)
|
||||
double estimatedDuration = cleanedText.length() * 0.3; // 假设每个字0.3秒
|
||||
wordList.add(new WordInfo(
|
||||
cleanedText,
|
||||
0.0,
|
||||
estimatedDuration,
|
||||
0.5f // 默认置信度
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
private void segmentAudio(Path audioFile, List<WordInfo> words) throws Exception {
|
||||
// 确保每个片段独立处理
|
||||
for (WordInfo word : words) {
|
||||
AudioDispatcher dispatcher = AudioDispatcherFactory.fromFile(
|
||||
audioFile.toFile(),
|
||||
4096,
|
||||
0
|
||||
);
|
||||
|
||||
// 为每个片段创建独立处理器
|
||||
SegmentWriter writer = new SegmentWriter(
|
||||
Paths.get(outputPath, generateShortName(word.text) + ".wav"),
|
||||
(int)(word.start * sampleRate),
|
||||
(int)((word.end - word.start) * sampleRate),
|
||||
word.confidence
|
||||
);
|
||||
|
||||
dispatcher.addAudioProcessor(writer);
|
||||
dispatcher.run(); // 每个片段独立运行
|
||||
}
|
||||
}
|
||||
|
||||
private static String generateShortName(String text) {
|
||||
String clean = text.replaceAll("[^a-zA-Z0-9]", "");
|
||||
return clean.substring(0, Math.min(clean.length(), 12))
|
||||
+ "_" + System.currentTimeMillis() % 10000;
|
||||
}
|
||||
|
||||
private Path convertToPCM(Path input) throws Exception {
|
||||
Path output = Files.createTempFile("converted", ".wav");
|
||||
ProcessBuilder pb = new ProcessBuilder(
|
||||
"G:\\ffmpeg-N-102781-g05f9b3a0a5-win64-gpl\\bin\\ffmpeg.exe", "-y",
|
||||
"-i", input.toString(),
|
||||
"-acodec", "pcm_s16le",
|
||||
"-ac", "1",
|
||||
"-ar", String.valueOf(sampleRate),
|
||||
output.toString()
|
||||
);
|
||||
pb.redirectErrorStream(true);
|
||||
Process process = pb.start();
|
||||
if (process.waitFor() != 0) {
|
||||
throw new IOException("FFmpeg conversion failed");
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
private String convertToCleanPinyin(String chinese) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (char c : chinese.toCharArray()) {
|
||||
String[] pinyins = PinyinHelper.toHanyuPinyinStringArray(c);
|
||||
if (pinyins != null && pinyins.length > 0) {
|
||||
sb.append(pinyins[0].replaceAll("[^a-zA-Z]", ""));
|
||||
}
|
||||
}
|
||||
return sb.toString().toLowerCase();
|
||||
}
|
||||
|
||||
private static class WordInfo {
|
||||
final String text;
|
||||
final double start;
|
||||
final double end;
|
||||
final float confidence;
|
||||
|
||||
WordInfo(String text, double start, double end, float confidence) {
|
||||
this.text = text;
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
this.confidence = confidence;
|
||||
}
|
||||
}
|
||||
|
||||
static class AudioFadeUtil {
|
||||
public static void applyFade(Path audioPath, int fadeInMs, int fadeOutMs) throws Exception {
|
||||
AudioInputStream ais = AudioSystem.getAudioInputStream(audioPath.toFile());
|
||||
AudioFormat format = ais.getFormat();
|
||||
byte[] bytes = ais.readAllBytes();
|
||||
|
||||
// 转换到short数组处理
|
||||
short[] samples = new short[bytes.length / 2];
|
||||
for (int i = 0; i < samples.length; i++) {
|
||||
samples[i] = (short) ((bytes[2*i+1] << 8) | (bytes[2*i] & 0xFF));
|
||||
}
|
||||
|
||||
// 淡入处理
|
||||
int fadeInSamples = (int) (format.getSampleRate() * fadeInMs / 1000);
|
||||
fadeInSamples = Math.min(fadeInSamples, samples.length);
|
||||
for (int i = 0; i < fadeInSamples; i++) {
|
||||
double factor = (double) i / fadeInSamples;
|
||||
samples[i] = (short) (samples[i] * factor);
|
||||
}
|
||||
|
||||
// 淡出处理
|
||||
int fadeOutSamples = (int) (format.getSampleRate() * fadeOutMs / 1000);
|
||||
fadeOutSamples = Math.min(fadeOutSamples, samples.length);
|
||||
for (int i = samples.length - fadeOutSamples; i < samples.length; i++) {
|
||||
double factor = 1.0 - (double) (i - (samples.length - fadeOutSamples)) / fadeOutSamples;
|
||||
samples[i] = (short) (samples[i] * factor);
|
||||
}
|
||||
|
||||
// 转换回byte数组
|
||||
byte[] processedBytes = new byte[bytes.length];
|
||||
for (int i = 0; i < samples.length; i++) {
|
||||
processedBytes[2*i] = (byte) (samples[i] & 0xFF);
|
||||
processedBytes[2*i+1] = (byte) ((samples[i] >> 8) & 0xFF);
|
||||
}
|
||||
|
||||
try (AudioInputStream newAis = new AudioInputStream(
|
||||
new ByteArrayInputStream(processedBytes),
|
||||
format,
|
||||
processedBytes.length / format.getFrameSize())) {
|
||||
AudioSystem.write(newAis, AudioFileFormat.Type.WAVE, audioPath.toFile());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 修改后的SegmentWriter实现
|
||||
private class SegmentWriter extends WaveformWriter {
|
||||
private final Path finalOutputPath;
|
||||
private final int targetBytes;
|
||||
private int writtenBytes = 0;
|
||||
private final float gain;
|
||||
private final File tempRawFile;
|
||||
|
||||
SegmentWriter(Path output, int startFrame, int frameCount, float confidence) {
|
||||
// 使用独立控制的临时文件
|
||||
super(createTempFormat(), generateTempPath(output));
|
||||
this.finalOutputPath = output;
|
||||
this.gain = Math.min(1.0f, confidence * 2);
|
||||
this.targetBytes = frameCount * 2;
|
||||
this.tempRawFile = createManagedTempFile(output);
|
||||
|
||||
// 初始化文件通道
|
||||
initFileChannel(startFrame);
|
||||
}
|
||||
|
||||
private static TarsosDSPAudioFormat createTempFormat() {
|
||||
return new TarsosDSPAudioFormat(
|
||||
44100,
|
||||
16,
|
||||
1,
|
||||
true,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
private static String generateTempPath(Path output) {
|
||||
return output.getParent().toString() + File.separator +
|
||||
"tmp_" + System.nanoTime() + ".raw";
|
||||
}
|
||||
|
||||
private File createManagedTempFile(Path output) {
|
||||
try {
|
||||
File tempFile = File.createTempFile("seg_", ".raw", output.getParent().toFile());
|
||||
tempFile.deleteOnExit();
|
||||
return tempFile;
|
||||
} catch (IOException e) {
|
||||
throw new AudioProcessingException("无法创建临时文件", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void initFileChannel(int startFrame) {
|
||||
try (RandomAccessFile raf = new RandomAccessFile(tempRawFile, "rw")) {
|
||||
FileChannel channel = raf.getChannel();
|
||||
channel.position(startFrame * 2L);
|
||||
} catch (IOException e) {
|
||||
throw new AudioProcessingException("文件初始化失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean process(AudioEvent audioEvent) {
|
||||
byte[] buffer = audioEvent.getByteBuffer().clone();
|
||||
applyGain(buffer);
|
||||
writeToTemp(buffer);
|
||||
return writtenBytes < targetBytes;
|
||||
}
|
||||
|
||||
private void applyGain(byte[] buffer) {
|
||||
for (int i = 0; i < buffer.length; i += 2) {
|
||||
short sample = (short) ((buffer[i+1] << 8) | (buffer[i] & 0xFF));
|
||||
sample = (short) (sample * gain);
|
||||
buffer[i] = (byte) (sample & 0xFF);
|
||||
buffer[i+1] = (byte) ((sample >> 8) & 0xFF);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeToTemp(byte[] buffer) {
|
||||
try (FileOutputStream fos = new FileOutputStream(tempRawFile, true)) {
|
||||
fos.write(buffer);
|
||||
writtenBytes += buffer.length;
|
||||
} catch (IOException e) {
|
||||
throw new AudioProcessingException("写入临时文件失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processingFinished() {
|
||||
try {
|
||||
convertToWav();
|
||||
applyAudioEffects();
|
||||
} finally {
|
||||
cleanupResources();
|
||||
}
|
||||
}
|
||||
|
||||
private void convertToWav() {
|
||||
try (AudioInputStream rawStream = new AudioInputStream(
|
||||
new FileInputStream(tempRawFile),
|
||||
JVMAudioInputStream.toAudioFormat(audioFormat),
|
||||
tempRawFile.length() / audioFormat.getFrameSize())) {
|
||||
|
||||
AudioSystem.write(rawStream,
|
||||
AudioFileFormat.Type.WAVE,
|
||||
finalOutputPath.toFile());
|
||||
} catch (IOException e) {
|
||||
throw new AudioProcessingException("WAV转换失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void applyAudioEffects() {
|
||||
try {
|
||||
AudioFadeUtil.applyFade(finalOutputPath, 50, 50);
|
||||
} catch (Exception e) {
|
||||
System.err.println("音频效果处理失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void cleanupResources() {
|
||||
try {
|
||||
if (tempRawFile.exists() && !tempRawFile.delete()) {
|
||||
tempRawFile.deleteOnExit();
|
||||
}
|
||||
} catch (SecurityException e) {
|
||||
System.err.println("临时文件清理失败: " + tempRawFile.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class AudioProcessingException extends RuntimeException {
|
||||
public AudioProcessingException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
}
|
||||
15
src/main/java/com/axis/innovators/box/speech/Main.java
Normal file
15
src/main/java/com/axis/innovators/box/speech/Main.java
Normal file
@@ -0,0 +1,15 @@
|
||||
package com.axis.innovators.box.speech;
|
||||
|
||||
import java.nio.file.Paths;
|
||||
|
||||
public class Main {
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
HighAccuracySpeechRecognition recognizer = new HighAccuracySpeechRecognition(
|
||||
"C:\\Users\\Administrator\\Desktop\\声音识别模型\\vosk-model-cn-0.22",
|
||||
"output"
|
||||
);
|
||||
|
||||
recognizer.processAudio(Paths.get("G:\\鬼畜素材\\工作间\\哪吒-嗵嗵\\哪吒-嗵嗵1.wav"));
|
||||
}
|
||||
}
|
||||
@@ -136,22 +136,6 @@ public class LM {
|
||||
String prompt,
|
||||
String system,
|
||||
MessageCallback messageCallback, boolean isContinue){
|
||||
//if (isContinue){
|
||||
// return inference(modelHandle,
|
||||
// ctxHandle,
|
||||
// temperature,
|
||||
// 0.1f,
|
||||
// 100,
|
||||
// 0.9f,
|
||||
// 0,
|
||||
// 64,
|
||||
// 1.1f,
|
||||
// 0.0f,
|
||||
// 0.0f,
|
||||
// system + "用户:" + prompt + "\n请继续回答:",
|
||||
// messageCallback
|
||||
// );
|
||||
//}
|
||||
return inference(modelHandle,
|
||||
ctxHandle,
|
||||
temperature,
|
||||
@@ -163,7 +147,7 @@ public class LM {
|
||||
1.1f,
|
||||
0.0f,
|
||||
0.0f,
|
||||
"{问题}" + prompt,
|
||||
prompt,
|
||||
messageCallback
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user