feat(browser): 新增 BrowserWindowJDialog 类并优化浏览器功能

- 新增 BrowserWindowJDialog 类,用于创建 JDialog 类型的浏览器窗口
- 优化 BrowserWindow 类,添加复制、粘贴功能- 新增 CefAppManager 类,用于管理 CefApp 实例
- 更新 build.gradle,添加新依赖项
This commit is contained in:
tzdwindows 7
2025-04-30 17:50:20 +08:00
parent d53fe66e37
commit 4fa079a753
14 changed files with 1316 additions and 791 deletions

View File

@@ -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 目录

View File

@@ -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;

View File

@@ -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);
});
}
}

View File

@@ -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;
}
}
}

View File

@@ -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>

View File

@@ -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);
});
}

View File

@@ -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) {
}

View File

@@ -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());
}
}
}

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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);
}
}));

View File

@@ -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);
}
}
}

View 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"));
}
}

View File

@@ -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
);
}