This commit is contained in:
commit
d19d4ef44b
|
|
@ -0,0 +1,5 @@
|
||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/FloatingBotExtension.iml" filepath="$PROJECT_DIR$/.idea/FloatingBotExtension.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,122 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>语音录制与播放</title>
|
||||||
|
<style>
|
||||||
|
.button-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-size: 16px;
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="button-row">
|
||||||
|
<button id="startRecord">1-开始录制语音</button>
|
||||||
|
<button id="stopRecord">2-停止录制语音</button>
|
||||||
|
<button id="playRecord">3-播放录制的声音</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="button-row">
|
||||||
|
<button id="startScreenRecord">4-获取系统扬声器声音</button>
|
||||||
|
<button id="stopScreenRecord">5-停止获取系统扬声器声音</button>
|
||||||
|
<button id="playScreenRecord">6-播放系统扬声器声音</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<audio id="audioPlayback" controls style="display:none;"></audio>
|
||||||
|
<audio id="audioPlayback2" controls style="display:none;"></audio>
|
||||||
|
<script>
|
||||||
|
let mediaRecorder;
|
||||||
|
let screenMediaRecorder;
|
||||||
|
let audioChunks = [];
|
||||||
|
|
||||||
|
document.getElementById('startRecord').addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||||
|
mediaRecorder = new MediaRecorder(stream);
|
||||||
|
mediaRecorder.ondataavailable = event => {
|
||||||
|
audioChunks.push(event.data);
|
||||||
|
};
|
||||||
|
mediaRecorder.onstop = () => {
|
||||||
|
const audioBlob = new Blob(audioChunks, { 'type': 'audio/wav' });
|
||||||
|
const audioUrl = URL.createObjectURL(audioBlob);
|
||||||
|
document.getElementById('audioPlayback').src = audioUrl;
|
||||||
|
document.getElementById('audioPlayback').style.display = 'block';
|
||||||
|
audioChunks = []; // Reset for next recording
|
||||||
|
mediaRecorder = null;
|
||||||
|
stream.getTracks().forEach(track => {
|
||||||
|
track.stop();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
mediaRecorder.start();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("获取用户语音失败: ", err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('stopRecord').addEventListener('click', () => {
|
||||||
|
if (mediaRecorder) {
|
||||||
|
mediaRecorder.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('playRecord').addEventListener('click', () => {
|
||||||
|
const audioPlayback = document.getElementById('audioPlayback');
|
||||||
|
if (audioPlayback.paused) {
|
||||||
|
audioPlayback.play();
|
||||||
|
} else {
|
||||||
|
audioPlayback.pause();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
document.getElementById('startScreenRecord').addEventListener('click', async()=> {
|
||||||
|
try {
|
||||||
|
// 请求访问用户的屏幕媒体流
|
||||||
|
const stream = await navigator.mediaDevices.getDisplayMedia({audio: true});
|
||||||
|
screenMediaRecorder = new MediaRecorder(stream);
|
||||||
|
screenMediaRecorder.ondataavailable = event => {
|
||||||
|
audioChunks.push(event.data);
|
||||||
|
};
|
||||||
|
screenMediaRecorder.onstop = () => {
|
||||||
|
const audioBlob = new Blob(audioChunks, { 'type': 'audio/wav' });
|
||||||
|
const audioUrl = URL.createObjectURL(audioBlob);
|
||||||
|
document.getElementById('audioPlayback2').src = audioUrl;
|
||||||
|
document.getElementById('audioPlayback2').style.display = 'block';
|
||||||
|
audioChunks = []; // Reset for next recording
|
||||||
|
screenMediaRecorder = null;
|
||||||
|
stream.getTracks().forEach(track => {
|
||||||
|
track.stop();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
screenMediaRecorder.start();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("获取用户语音失败: ", err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.getElementById('stopScreenRecord').addEventListener('click', () => {
|
||||||
|
if (screenMediaRecorder) {
|
||||||
|
screenMediaRecorder.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.getElementById('playScreenRecord').addEventListener('click', () => {
|
||||||
|
const audioPlayback = document.getElementById('audioPlayback2');
|
||||||
|
if (audioPlayback.paused) {
|
||||||
|
audioPlayback.play();
|
||||||
|
} else {
|
||||||
|
audioPlayback.pause();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
var crypto = require('crypto');
|
||||||
|
var WebSocketClient = require('websocket').client;
|
||||||
|
var fs = require('fs');
|
||||||
|
|
||||||
|
// AIUI websocket服务地址
|
||||||
|
var BASE_URL = "wss://wsapi.xfyun.cn/v1/aiui";
|
||||||
|
var ORIGIN = "http://wsapi.xfyun.cn";
|
||||||
|
// 应用ID,在AIUI开放平台创建并设置
|
||||||
|
var APPID = "xxxx";
|
||||||
|
// 接口密钥,在AIUI开放平台查看
|
||||||
|
var APIKEY = "xxxx";
|
||||||
|
// 业务参数
|
||||||
|
var PARAM = "{\"auth_id\":\"f8948af1d2d6547eaf09bc2f20ebfcc6\",\"data_type\":\"audio\",\"scene\":\"main_box\",\"sample_rate\":\"16000\",\"aue\":\"raw\",\"result_level\":\"plain\",\"context\":\"{\\\"sdk_support\\\":[\\\"tts\\\"]}\"}";
|
||||||
|
|
||||||
|
// 计算握手参数
|
||||||
|
function getHandshakeParams(){
|
||||||
|
var paramBase64 = new Buffer(PARAM).toString('base64');
|
||||||
|
var curtime = Math.floor(Date.now()/1000);
|
||||||
|
var originStr = APIKEY + curtime + paramBase64;
|
||||||
|
var checksum = crypto.createHash('md5').update(originStr).digest("hex");
|
||||||
|
var handshakeParams = "?appid="+APPID+"&checksum="+checksum+"&curtime="+curtime+"¶m="+paramBase64;
|
||||||
|
console.log(handshakeParams);
|
||||||
|
return handshakeParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定义websocket client
|
||||||
|
var client = new WebSocketClient();
|
||||||
|
|
||||||
|
client.on('connectFailed', function(error) {
|
||||||
|
console.log('Connect Error: ' + error.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('connect', function(connection) {
|
||||||
|
console.log('WebSocket client connected');
|
||||||
|
connection.on('error', function(error) {
|
||||||
|
console.log("Connection Error: " + error.toString());
|
||||||
|
});
|
||||||
|
connection.on('close', function() {
|
||||||
|
console.log('echo-protocol Connection Closed');
|
||||||
|
});
|
||||||
|
connection.on('message', function(message) {
|
||||||
|
if (message.type === 'utf8') {
|
||||||
|
console.log("Received: '" + message.utf8Data + "'");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function sendMsg() {
|
||||||
|
if (connection.connected) {
|
||||||
|
|
||||||
|
let audioFile = fs.createReadStream('./weather.pcm');
|
||||||
|
let idx = 0;
|
||||||
|
|
||||||
|
audioFile.on("data", function(data) {
|
||||||
|
console.log("发送音频块 ", idx++);
|
||||||
|
|
||||||
|
connection.sendBytes(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
audioFile.on("close", function() {
|
||||||
|
connection.sendUTF("--end--");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 发送数据
|
||||||
|
sendMsg();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 建立连接
|
||||||
|
client.connect(BASE_URL+getHandshakeParams(), "", ORIGIN);
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
// background.js - 监听扩展安装
|
||||||
|
chrome.runtime.onInstalled.addListener(() => {
|
||||||
|
console.log("Floating Bot Extension 已安装");
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
(function () {
|
||||||
|
let botWindow = document.createElement("div");
|
||||||
|
botWindow.id = "floating-bot";
|
||||||
|
botWindow.innerHTML = `
|
||||||
|
<div id="bot-header">🤖 小机器人</div>
|
||||||
|
<div id="bot-body">你好!我是你的网页助手。</div>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(botWindow);
|
||||||
|
|
||||||
|
let style = document.createElement("style");
|
||||||
|
style.innerHTML = `
|
||||||
|
#floating-bot {
|
||||||
|
position: fixed;
|
||||||
|
width: 200px;
|
||||||
|
height: 150px;
|
||||||
|
bottom: 50px;
|
||||||
|
right: 50px;
|
||||||
|
background: white;
|
||||||
|
border: 2px solid #ccc;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 2px 2px 10px rgba(0,0,0,0.2);
|
||||||
|
z-index: 9999;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
}
|
||||||
|
#bot-header {
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
padding: 10px;
|
||||||
|
cursor: move;
|
||||||
|
border-top-left-radius: 10px;
|
||||||
|
border-top-right-radius: 10px;
|
||||||
|
}
|
||||||
|
#bot-body {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
|
||||||
|
let isDragging = false, offsetX, offsetY;
|
||||||
|
botWindow.querySelector("#bot-header").addEventListener("mousedown", (e) => {
|
||||||
|
isDragging = true;
|
||||||
|
offsetX = e.clientX - botWindow.offsetLeft;
|
||||||
|
offsetY = e.clientY - botWindow.offsetTop;
|
||||||
|
});
|
||||||
|
document.addEventListener("mousemove", (e) => {
|
||||||
|
if (isDragging) {
|
||||||
|
botWindow.style.left = `${e.clientX - offsetX}px`;
|
||||||
|
botWindow.style.top = `${e.clientY - offsetY}px`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.addEventListener("mouseup", () => {
|
||||||
|
isDragging = false;
|
||||||
|
});
|
||||||
|
})();
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 80 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 80 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 80 KiB |
|
|
@ -0,0 +1,121 @@
|
||||||
|
function transcode(audioData) {
|
||||||
|
// 将音频数据转换为16kHz采样率
|
||||||
|
let output = to16kHz(audioData);
|
||||||
|
// 将音频数据转换为16位PCM格式
|
||||||
|
output = to16BitPCM(output);
|
||||||
|
// 将DataView对象转换为字节数组
|
||||||
|
output = Array.from(new Uint8Array(output.buffer));
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
function to16kHz(audioData) {
|
||||||
|
// 创建一个新的Float32Array以避免修改原始数据
|
||||||
|
let data = new Float32Array(audioData);
|
||||||
|
// 计算目标采样率下的数据点数量
|
||||||
|
let fitCount = Math.round(data.length * (16000 / 44100));
|
||||||
|
// 创建一个新的Float32Array用于存储转换后的数据
|
||||||
|
let newData = new Float32Array(fitCount);
|
||||||
|
// 计算插值的步长,用于将44100Hz的数据压缩到16000Hz
|
||||||
|
let springFactor = (data.length - 1) / (fitCount - 1);
|
||||||
|
// 设置第一帧数据
|
||||||
|
newData[0] = data[0];
|
||||||
|
for (let i = 1; i < fitCount - 1; i++) {
|
||||||
|
// 计算映射到原始数据的浮点索引
|
||||||
|
let tmp = i * springFactor;
|
||||||
|
// 计算索引的左右整数位置
|
||||||
|
let before = Math.floor(tmp);
|
||||||
|
let after = Math.ceil(tmp);
|
||||||
|
// 计算线性插值的权重
|
||||||
|
let atPoint = tmp - before;
|
||||||
|
// 使用线性插值法计算采样点
|
||||||
|
newData[i] = data[before] + (data[after] - data[before]) * atPoint;
|
||||||
|
}
|
||||||
|
// 设置最后一帧数据
|
||||||
|
newData[fitCount - 1] = data[data.length - 1];
|
||||||
|
return newData;
|
||||||
|
}
|
||||||
|
|
||||||
|
function to16BitPCM(input) {
|
||||||
|
// 计算输出数据所需的字节数,每个样本占2个字节
|
||||||
|
let dataLength = input.length * 2;
|
||||||
|
// 创建ArrayBuffer以存储二进制数据
|
||||||
|
let dataBuffer = new ArrayBuffer(dataLength);
|
||||||
|
// 使用DataView操作二进制数据
|
||||||
|
let dataView = new DataView(dataBuffer);
|
||||||
|
let offset = 0;
|
||||||
|
for (let i = 0; i < input.length; i++, offset += 2) {
|
||||||
|
// 将浮点数限制在[-1, 1]范围内
|
||||||
|
let s = Math.max(-1, Math.min(1, input[i]));
|
||||||
|
// 将浮点数转换为16位有符号整数,并按小端字节序写入DataView
|
||||||
|
dataView.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true);
|
||||||
|
}
|
||||||
|
return dataView;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function transToAudioData(
|
||||||
|
audioDataStr,
|
||||||
|
fromRate = 16000,
|
||||||
|
toRate = 22505,
|
||||||
|
) {
|
||||||
|
// 将Base64音频数据解码为Int16Array
|
||||||
|
let outputS16 = base64ToS16(audioDataStr);
|
||||||
|
// 将Int16Array数据转换为Float32Array
|
||||||
|
let output = transS16ToF32(outputS16);
|
||||||
|
// 进行采样率转换,将音频数据从fromRate转换到toRate
|
||||||
|
output = transSamplingRate(output, fromRate, toRate);
|
||||||
|
// 转换为普通数组格式并返回
|
||||||
|
output = Array.from(output);
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
function transSamplingRate(
|
||||||
|
data,
|
||||||
|
fromRate = 44100,
|
||||||
|
toRate = 16000,
|
||||||
|
) {
|
||||||
|
// 计算目标采样点数
|
||||||
|
let fitCount = Math.round(data.length * (toRate / fromRate));
|
||||||
|
// 创建新的Float32Array以存储采样率转换后的数据
|
||||||
|
let newData = new Float32Array(fitCount);
|
||||||
|
// 计算数据点之间的缩放因子
|
||||||
|
let springFactor = (data.length - 1) / (fitCount - 1);
|
||||||
|
// 设置起始点
|
||||||
|
newData[0] = data[0];
|
||||||
|
for (let i = 1; i < fitCount - 1; i++) {
|
||||||
|
// 计算缩放后的位置
|
||||||
|
let tmp = i * springFactor;
|
||||||
|
let before = Math.floor(tmp);
|
||||||
|
let after = Math.ceil(tmp);
|
||||||
|
// 计算线性插值
|
||||||
|
let atPoint = tmp - before;
|
||||||
|
newData[i] = data[before] + (data[after] - data[before]) * atPoint;
|
||||||
|
}
|
||||||
|
// 设置终止点
|
||||||
|
newData[fitCount - 1] = data[data.length - 1];
|
||||||
|
return newData;
|
||||||
|
}
|
||||||
|
|
||||||
|
function transS16ToF32(input) {
|
||||||
|
// 创建临时数组存储转换后的数据
|
||||||
|
let tmpData = [];
|
||||||
|
for (let i = 0; i < input.length; i++) {
|
||||||
|
// 将Int16数据标准化到[-1, 1]区间
|
||||||
|
let d = input[i] < 0 ? input[i] / 0x8000 : input[i] / 0x7fff;
|
||||||
|
tmpData.push(d);
|
||||||
|
}
|
||||||
|
// 转换为Float32Array
|
||||||
|
return new Float32Array(tmpData);
|
||||||
|
}
|
||||||
|
|
||||||
|
function base64ToS16(base64AudioData) {
|
||||||
|
// 使用atob解码Base64字符串为二进制字符串
|
||||||
|
base64AudioData = atob(base64AudioData);
|
||||||
|
// 创建Uint8Array以存储字节数据
|
||||||
|
const outputArray = new Uint8Array(base64AudioData.length);
|
||||||
|
for (let i = 0; i < base64AudioData.length; ++i) {
|
||||||
|
outputArray[i] = base64AudioData.charCodeAt(i);
|
||||||
|
}
|
||||||
|
// 将Uint8Array视为Int16Array
|
||||||
|
return new Int16Array(new DataView(outputArray.buffer).buffer);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
// 枚举定义:InteractMode
|
||||||
|
const InteractMode = {
|
||||||
|
/**
|
||||||
|
* @description 交互类型为音频模式
|
||||||
|
* @returns {string} 'audio' - 表示当前交互模式是音频模式
|
||||||
|
* @example usage:
|
||||||
|
* const interactType = getInteractType('AUDIO'); // 返回 'audio'
|
||||||
|
*/
|
||||||
|
AUDIO: 'audio',
|
||||||
|
/**
|
||||||
|
* @description 交互类型为文字模式
|
||||||
|
* @returns {string} 'text' - 表示当前交互模式是文本模式
|
||||||
|
* @example usage:
|
||||||
|
* const interactType = getInteractType('TEXT'); // 返回 'text'
|
||||||
|
*/
|
||||||
|
TEXT: 'text',
|
||||||
|
};
|
||||||
|
|
||||||
|
// 枚举定义:FORMAL_ENVIRONMENT
|
||||||
|
const FormalEnvironment = {
|
||||||
|
/**
|
||||||
|
* @description 测试环境
|
||||||
|
* @returns {string} '0' - 表示当前环境为测试环境
|
||||||
|
* @example usage:
|
||||||
|
* console.log(FORMAL_ENVIRONMENT.TEST); // 输出 '0'
|
||||||
|
*/
|
||||||
|
TEST: '0',
|
||||||
|
/**
|
||||||
|
* @description 生产环境
|
||||||
|
* @returns {string} '1' - 表示当前环境为生产环境
|
||||||
|
* @example usage:
|
||||||
|
* console.log(FORMAL_ENVIRONMENT.PROD); // 输出 '1'
|
||||||
|
*/
|
||||||
|
PROD: '1'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 常量类Constant,包含各项配置信息
|
||||||
|
const Constant = {
|
||||||
|
/**
|
||||||
|
* @description 交互云地址 WebSocket URL
|
||||||
|
* @returns {string} WebSocket连接地址 - 获取交互服务的 WebSocket 地址
|
||||||
|
* @example usage:
|
||||||
|
* console.log(Constant.INTERACT_SOCKET_URL); // 输出 'ws://103.8.34.136:26002/createRec'
|
||||||
|
*/
|
||||||
|
INTERACT_SOCKET_URL: 'ws://103.8.34.136:26003/createRec',
|
||||||
|
/**
|
||||||
|
* @description 机器人ID标识符
|
||||||
|
* @returns {string} 机器人的唯一标识符 - 获取用户的机器人身份信息
|
||||||
|
* @note 请确保这个 ID 在你的系统中是有效且唯一的
|
||||||
|
* @example usage:
|
||||||
|
* console.log(Constant.INTERACT_BOT_ID); // 输出你的机器人 ID '2557854368452906'
|
||||||
|
*/
|
||||||
|
INTERACT_BOT_ID: '2557854368452906',
|
||||||
|
/**
|
||||||
|
* @description API 请求头中的用户认证 token
|
||||||
|
* @returns {string} 用户身份认证 token - 获取或设置用户认证信息,建议保密
|
||||||
|
* @note 请在实际使用中替换为你的真实组织代码
|
||||||
|
* @example usage:
|
||||||
|
* console.log(Constant.INTERACT_ORG_CODE); // 输出 '6b9fe858-1efc-43e7-abf1-ab085a086ebf'
|
||||||
|
*/
|
||||||
|
INTERACT_ORG_CODE: '6b9fe858-1efc-43e7-abf1-ab085a086ebf',
|
||||||
|
/**
|
||||||
|
* @description 应用 ID
|
||||||
|
* @returns {string} 应用的唯一标识符 - 获取应用的基本信息,建议与组织代码关联
|
||||||
|
* @example usage:
|
||||||
|
* console.log(Constant.INTERACT_APP_ID); // 输出 '12345'
|
||||||
|
*/
|
||||||
|
INTERACT_APP_ID: 'ef014ded',
|
||||||
|
/**
|
||||||
|
* @description 当前场景名称
|
||||||
|
* @returns {string} 当前交互场景的名称 - 用于标识当前操作或情境
|
||||||
|
* @example usage:
|
||||||
|
* console.log(Constant.INTERACT_SCENE); // 输出 'test'
|
||||||
|
*/
|
||||||
|
INTERACT_SCENE: 'main_box',
|
||||||
|
/**
|
||||||
|
* @description 用户所在的地理位置信息
|
||||||
|
* @returns {string} 地理位置描述 - 获取用户的位置信息,例如场所名称或代码
|
||||||
|
* @example usage:
|
||||||
|
* console.log(Constant.INTERACT_LOCATION); // 输出 '钟楼站'
|
||||||
|
*/
|
||||||
|
INTERACT_LOCATION: '合肥',
|
||||||
|
/**
|
||||||
|
* @description 当前正式环境配置
|
||||||
|
* @returns {string} 正常化处理后的环境参数 - 获取当前使用的正式环境状态
|
||||||
|
* @example usage:
|
||||||
|
* console.log(Constant.INTERACT_FORMAL_ENVIRONMENT); // 输出当前正式环境状态,例如 '1'表示生产环境
|
||||||
|
*/
|
||||||
|
INTERACT_FORMAL_ENVIRONMENT: FormalEnvironment.PROD,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 使用InteractMode:
|
||||||
|
function getInteractType(type) {
|
||||||
|
switch (type) {
|
||||||
|
case 'AUDIO':
|
||||||
|
return Constant.INTERACT_SOCKET_URL;
|
||||||
|
default:
|
||||||
|
throw new Error('未知的交互类型');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,167 @@
|
||||||
|
|
||||||
|
function buildAudioPayload(data, sid, endFlag, languageType) {
|
||||||
|
let customParams = {
|
||||||
|
botId: Constant.INTERACT_BOT_ID,
|
||||||
|
orgCode: Constant.INTERACT_ORG_CODE,
|
||||||
|
sid: sid,
|
||||||
|
deviceId: '123',
|
||||||
|
location: Constant.INTERACT_LOCATION,
|
||||||
|
languageType: languageType === '2' ? 'cn' : languageType === '3' ? 'en' : 'cn',
|
||||||
|
};
|
||||||
|
|
||||||
|
let iatParams = {
|
||||||
|
sid: sid,
|
||||||
|
aue: 'raw',
|
||||||
|
dwa: 'wpgs',
|
||||||
|
pgs: 'apd',
|
||||||
|
endFlag: true,
|
||||||
|
vad_switch: 'false',
|
||||||
|
eos: '60000',
|
||||||
|
rse: 'utf-8',
|
||||||
|
rst: 'json',
|
||||||
|
engine_param: `pproc_param_puncproc=false;wdec_param_LanguageTypeChoice=${languageType || '1'}`,
|
||||||
|
};
|
||||||
|
let payload = {
|
||||||
|
data: toBase64(data),
|
||||||
|
appid: Constant.INTERACT_APP_ID,
|
||||||
|
scene: Constant.INTERACT_SCENE,
|
||||||
|
debug: true,
|
||||||
|
sessionParams: {
|
||||||
|
id: sid,
|
||||||
|
traceId: sid,
|
||||||
|
reset: false,
|
||||||
|
abilityList: [
|
||||||
|
{
|
||||||
|
abilityCode: 'iat',
|
||||||
|
serviceName: 'iat',
|
||||||
|
param: JSON.stringify(iatParams),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return JSON.stringify(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTextPayload(data, sid) {
|
||||||
|
let customParams = {
|
||||||
|
botId: 'INTERACT_BOT_ID',
|
||||||
|
orgCode: 'INTERACT_ORG_CODE',
|
||||||
|
sid: sid,
|
||||||
|
location: '双龙站',
|
||||||
|
deviceId: '123',
|
||||||
|
};
|
||||||
|
let ttsParams = {
|
||||||
|
audio_coding: 'raw',
|
||||||
|
sample_rate: '16000',
|
||||||
|
sid: sid,
|
||||||
|
volume: '20',
|
||||||
|
};
|
||||||
|
let payload = {
|
||||||
|
data: btoa(data),
|
||||||
|
appid: 'INTERACT_APPID',
|
||||||
|
scene: 'INTERACT_SCENE',
|
||||||
|
debug: true,
|
||||||
|
sessionParams: {
|
||||||
|
id: sid,
|
||||||
|
traceId: sid,
|
||||||
|
reset: false,
|
||||||
|
abilityList: [
|
||||||
|
{
|
||||||
|
abilityCode: 'dics',
|
||||||
|
param: JSON.stringify(customParams),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
abilityCode: 'tts',
|
||||||
|
param: JSON.stringify(ttsParams),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return JSON.stringify(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toBase64(buffer) {
|
||||||
|
var binary = '';
|
||||||
|
var bytes = new Uint8Array(buffer);
|
||||||
|
var len = bytes.byteLength;
|
||||||
|
for (var i = 0; i < len; i++) {
|
||||||
|
binary += String.fromCharCode(bytes[i]);
|
||||||
|
}
|
||||||
|
return btoa(binary);
|
||||||
|
}
|
||||||
|
|
||||||
|
let iatText = '';
|
||||||
|
let iatAppendText = '';
|
||||||
|
|
||||||
|
function decodeMessage(messageData) {
|
||||||
|
let aiuiResult = new AIUIResult(AIUI_TYPE.ERROR, messageData);
|
||||||
|
try {
|
||||||
|
const result = JSON.parse(messageData);
|
||||||
|
if (result) {
|
||||||
|
const json = JSON.parse(result.result);
|
||||||
|
let sub = json[0].sub;
|
||||||
|
let data = json[0].data;
|
||||||
|
if (sub === 'iat' && data && data !== '') {
|
||||||
|
let iat = JSON.parse(data);
|
||||||
|
let pgs = iat.pgs;
|
||||||
|
let ls = iat.ls;
|
||||||
|
if (ls) {
|
||||||
|
aiuiResult = new AIUIResult(AIUI_TYPE.IAT_FINAL, messageData);
|
||||||
|
aiuiResult.setFinalText(iatText);
|
||||||
|
iatText = '';
|
||||||
|
return aiuiResult;
|
||||||
|
}
|
||||||
|
if (pgs === 'rpl') {
|
||||||
|
iatText = '';
|
||||||
|
iatText += iatAppendText;
|
||||||
|
} else if (pgs === 'apd') {
|
||||||
|
iatAppendText = '';
|
||||||
|
if (iatText != null) {
|
||||||
|
iatAppendText += iatText;
|
||||||
|
}
|
||||||
|
iatText = '';
|
||||||
|
}
|
||||||
|
if (iat.ws && iat.ws.length > 0) {
|
||||||
|
iat.ws.forEach((item) => {
|
||||||
|
iatText += item.cw[0].w;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
aiuiResult = new AIUIResult(AIUI_TYPE.IAT_REALTIME, messageData);
|
||||||
|
aiuiResult.setRealtimeText(iatText);
|
||||||
|
return aiuiResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
return aiuiResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AIUI_TYPE = {
|
||||||
|
IAT_REALTIME: 'iat_realtime',
|
||||||
|
IAT_FINAL: 'iat_final',
|
||||||
|
ERROR: 'error',
|
||||||
|
};
|
||||||
|
|
||||||
|
class AIUIResult {
|
||||||
|
constructor(type, json) {
|
||||||
|
this.type = type;
|
||||||
|
this.json = json;
|
||||||
|
}
|
||||||
|
setRealtimeText(text) {
|
||||||
|
this.realtimeText = text;
|
||||||
|
}
|
||||||
|
setFinalText(text) {
|
||||||
|
this.finalText = text;
|
||||||
|
}
|
||||||
|
getReal() {
|
||||||
|
return this.realtimeText;
|
||||||
|
}
|
||||||
|
getFinal() {
|
||||||
|
return this.finalText;
|
||||||
|
}
|
||||||
|
getJSON() {
|
||||||
|
return this.json;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
const APPID = "6a744d1b";
|
||||||
|
const APIKEY = "27d1b3d2f5d1bd93e7c94330b1675ee2";
|
||||||
|
|
||||||
|
const PARAM = {
|
||||||
|
auth_id: "f8948af1d2d6547eaf09bc2f20ebfcc6",
|
||||||
|
data_type: "audio",
|
||||||
|
scene: "main_box",
|
||||||
|
aue: "raw",
|
||||||
|
sample_rate: "16000",
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算握手参数
|
||||||
|
* @returns {string} WebSocket 连接参数字符串
|
||||||
|
*/
|
||||||
|
function getHandshakeParams() {
|
||||||
|
const paramBase64 = btoa(JSON.stringify(PARAM)); // 将参数转换为 Base64
|
||||||
|
const curTime = Math.floor(Date.now() / 1000); // 当前时间戳(秒)
|
||||||
|
const originStr = APIKEY + curTime + paramBase64; // 拼接字符串
|
||||||
|
const checksum = CryptoJS.MD5(originStr).toString(); // MD5 哈希计算
|
||||||
|
return `?appid=${APPID}&checksum=${checksum}&curtime=${curTime}¶m=${paramBase64}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 Int16Array 转换为 Base64 编码字符串
|
||||||
|
* @param {Int16Array} buffer PCM 数据
|
||||||
|
* @returns {string} Base64 字符串
|
||||||
|
*/
|
||||||
|
function pcmToBase64(buffer) {
|
||||||
|
let binary = "";
|
||||||
|
buffer.forEach((val) => {
|
||||||
|
binary += String.fromCharCode(val & 0xff, (val >> 8) & 0xff);
|
||||||
|
});
|
||||||
|
return btoa(binary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 录音 + WebSocket 发送的完整流程
|
||||||
|
*/
|
||||||
|
async function startMicrophoneStreaming() {
|
||||||
|
const wsUtil = new WebSocketUtil(Constant.INTERACT_SOCKET_URL + getHandshakeParams());
|
||||||
|
const audioProcessor = new WebRTCAudioProcessor();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 启动音频处理
|
||||||
|
audioProcessor.start((pcmData)=>{
|
||||||
|
|
||||||
|
wsUtil.send(buildAudioPayload(transcode(pcmData),"","","cn"))
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
console.log('Audio processor started successfully.');
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to start audio processor:', error);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 连接 WebSocket
|
||||||
|
wsUtil.onOpen(() => {
|
||||||
|
console.log("✅ WebSocket 连接成功");
|
||||||
|
/*mic.startRecording((pcmData) =>{
|
||||||
|
console.log("📤 发送音频数据...");
|
||||||
|
console.log(pcmData)
|
||||||
|
})*/
|
||||||
|
|
||||||
|
});
|
||||||
|
wsUtil.onMessage((event) =>{});
|
||||||
|
wsUtil.onError((event) => console.error("❌ WebSocket 错误:", event));
|
||||||
|
wsUtil.onClose(() => console.log("🔴 WebSocket 连接已关闭"));
|
||||||
|
wsUtil.connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动录音流媒体传输
|
||||||
|
startMicrophoneStreaming();
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
// time.js (假设 getDateTime 函数在这个文件中)
|
||||||
|
function getDateTime() {
|
||||||
|
const date = new Date();
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
const hour = String(date.getHours()).padStart(2, '0');
|
||||||
|
const minute = String(date.getMinutes()).padStart(2, '0');
|
||||||
|
const second = String(date.getSeconds()).padStart(2, '0');
|
||||||
|
const ms = String(date.getMilliseconds()).padStart(3, '0');
|
||||||
|
return `${year}-${month}-${day} ${hour}:${minute}:${second}:${ms}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// StaticLogger 类,用于在浏览器控制台中打印格式化的日志
|
||||||
|
class StaticLogger {
|
||||||
|
// 静态属性,用于控制日志是否启用
|
||||||
|
static enabled = true;
|
||||||
|
|
||||||
|
// 静态方法,用于格式化日志消息
|
||||||
|
// tag: 日志标签,message: 日志内容
|
||||||
|
static formatMessage(tag, message) {
|
||||||
|
// 获取当前时间戳
|
||||||
|
const timeStamp = getDateTime();
|
||||||
|
// 使用 %c 占位符为时间戳添加蓝色样式
|
||||||
|
const coloredTimeStamp = `[${timeStamp}]`;
|
||||||
|
// 使用 %c 占位符为标签添加黄色样式
|
||||||
|
const coloredTag = `🚀 [${tag}]`;
|
||||||
|
// 如果有消息内容,则返回格式化后的完整日志
|
||||||
|
if (message) {
|
||||||
|
return `${coloredTag} ${coloredTimeStamp} ${message}`;
|
||||||
|
} else {
|
||||||
|
// 如果没有消息内容,则只返回时间戳和标签
|
||||||
|
return `${coloredTimeStamp} ${coloredTag}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 静态方法,用于打印普通日志
|
||||||
|
static log(tag, message) {
|
||||||
|
// 检查日志是否启用
|
||||||
|
if (this.enabled) {
|
||||||
|
// 如果消息是一个对象,则直接打印对象
|
||||||
|
if (message instanceof Object) {
|
||||||
|
console.log(this.formatMessage(tag), message);
|
||||||
|
} else {
|
||||||
|
// 否则,打印格式化后的日志,并应用样式
|
||||||
|
console.log(this.formatMessage(tag, message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 静态方法,用于打印信息日志
|
||||||
|
static info(tag, message) {
|
||||||
|
if (this.enabled) {
|
||||||
|
if (message instanceof Object) {
|
||||||
|
console.info(this.formatMessage(tag), message);
|
||||||
|
} else {
|
||||||
|
console.info(this.formatMessage(tag, message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 静态方法,用于打印警告日志
|
||||||
|
static warn(tag, message) {
|
||||||
|
if (this.enabled) {
|
||||||
|
if (message instanceof Object) {
|
||||||
|
console.warn(this.formatMessage(tag), message);
|
||||||
|
} else {
|
||||||
|
console.warn(this.formatMessage(tag, message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 静态方法,用于打印错误日志
|
||||||
|
static error(tag, message) {
|
||||||
|
if (this.enabled) {
|
||||||
|
if (message instanceof Object) {
|
||||||
|
console.error(this.formatMessage(tag), message);
|
||||||
|
} else {
|
||||||
|
console.error(this.formatMessage(tag, message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 静态方法,用于启用或禁用日志
|
||||||
|
static setEnabled(enabled) {
|
||||||
|
this.enabled = enabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将 StaticLogger 类暴露到全局作用域,以便在 HTML 中直接使用
|
||||||
|
window.StaticLogger = StaticLogger;
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
class WebRTCAudioProcessor {
|
||||||
|
constructor() {
|
||||||
|
this.audioContext = null;
|
||||||
|
this.scriptProcessor = null;
|
||||||
|
this.source = null;
|
||||||
|
this.onProcessAudio = null; // 用于外部传入的音频处理回调
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化并开始捕获音频流
|
||||||
|
* @param {Function} onProcessAudio - 处理 PCM 数据的回调函数
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async start(onProcessAudio) {
|
||||||
|
if (this.audioContext) {
|
||||||
|
console.warn('Audio processor is already running.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof onProcessAudio !== 'function') {
|
||||||
|
throw new Error('onProcessAudio must be a function');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.onProcessAudio = onProcessAudio;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取用户音频流
|
||||||
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
|
||||||
|
this._setupAudioContext(stream);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error accessing microphone:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置 AudioContext 和音频处理节点
|
||||||
|
* @param {MediaStream} stream - 音频流
|
||||||
|
*/
|
||||||
|
_setupAudioContext(stream) {
|
||||||
|
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||||
|
this.source = this.audioContext.createMediaStreamSource(stream);
|
||||||
|
|
||||||
|
// 创建 ScriptProcessorNode 处理音频数据
|
||||||
|
this.scriptProcessor = this.audioContext.createScriptProcessor(4096, 1, 1);
|
||||||
|
this.source.connect(this.scriptProcessor);
|
||||||
|
this.scriptProcessor.connect(this.audioContext.destination);
|
||||||
|
|
||||||
|
// 处理音频数据
|
||||||
|
this.scriptProcessor.onaudioprocess = (event) => {
|
||||||
|
const inputBuffer = event.inputBuffer;
|
||||||
|
const pcmData = inputBuffer.getChannelData(0); // 获取 PCM 数据
|
||||||
|
this.onProcessAudio(pcmData); // 调用外部传入的回调函数
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止音频处理并释放资源
|
||||||
|
*/
|
||||||
|
stop() {
|
||||||
|
if (this.scriptProcessor) {
|
||||||
|
this.scriptProcessor.disconnect();
|
||||||
|
this.scriptProcessor = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.source) {
|
||||||
|
this.source.disconnect();
|
||||||
|
this.source = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.audioContext) {
|
||||||
|
this.audioContext.close().then(() => {
|
||||||
|
this.audioContext = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.onProcessAudio = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,105 @@
|
||||||
|
class WebSocketUtil {
|
||||||
|
/**
|
||||||
|
* 构造函数,初始化 WebSocket 连接参数
|
||||||
|
* @param {string} url WebSocket 服务器地址
|
||||||
|
*/
|
||||||
|
constructor(url) {
|
||||||
|
this.url = url;
|
||||||
|
this.websocket = null;
|
||||||
|
this.onOpenCallback = null;
|
||||||
|
this.onMessageCallback = null;
|
||||||
|
this.onErrorCallback = null;
|
||||||
|
this.onCloseCallback = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 连接 WebSocket 服务器
|
||||||
|
*/
|
||||||
|
connect() {
|
||||||
|
if (this.websocket) {
|
||||||
|
StaticLogger.info("WebSocket connect","WebSocket 已经初始化。")
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.websocket = new WebSocket(this.url);
|
||||||
|
|
||||||
|
// 监听 WebSocket 连接成功事件
|
||||||
|
this.websocket.onopen = (event) => {
|
||||||
|
StaticLogger.info("WebSocket onopen","WebSocket 连接已打开。")
|
||||||
|
if (this.onOpenCallback) this.onOpenCallback(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听 WebSocket 接收到消息事件
|
||||||
|
this.websocket.onmessage = (event) => {
|
||||||
|
StaticLogger.info("WebSocket onmessage","WebSocket 接收到消息事件:"+ decodeMessage(event.data).realtimeText);
|
||||||
|
if (this.onMessageCallback) this.onMessageCallback(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听 WebSocket 发生错误事件
|
||||||
|
this.websocket.onerror = (event) => {
|
||||||
|
StaticLogger.error("WebSocket onerror","WebSocket 发生错误:")
|
||||||
|
if (this.onErrorCallback) this.onErrorCallback(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听 WebSocket 连接关闭事件
|
||||||
|
this.websocket.onclose = (event) => {
|
||||||
|
StaticLogger.info("WebSocket onclose","WebSocket 连接已关闭:")
|
||||||
|
if (this.onCloseCallback) this.onCloseCallback(event);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送消息到 WebSocket 服务器
|
||||||
|
* @param {string} message 需要发送的消息
|
||||||
|
*/
|
||||||
|
send(message) {
|
||||||
|
StaticLogger.info("WebSocket send","WebSocket 发送消息")
|
||||||
|
if (this.websocket && this.websocket.readyState === WebSocket.OPEN) {
|
||||||
|
this.websocket.send(message);
|
||||||
|
} else {
|
||||||
|
StaticLogger.warn("WebSocket send","WebSocket 连接未打开,无法发送消息。")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭 WebSocket 连接
|
||||||
|
*/
|
||||||
|
close() {
|
||||||
|
if (this.websocket) {
|
||||||
|
this.websocket.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置 WebSocket 连接成功时的回调函数
|
||||||
|
* @param {function} callback 连接成功的回调函数
|
||||||
|
*/
|
||||||
|
onOpen(callback) {
|
||||||
|
this.onOpenCallback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置 WebSocket 接收到消息时的回调函数
|
||||||
|
* @param {function} callback 消息接收的回调函数
|
||||||
|
*/
|
||||||
|
onMessage(callback) {
|
||||||
|
this.onMessageCallback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置 WebSocket 发生错误时的回调函数
|
||||||
|
* @param {function} callback 发生错误的回调函数
|
||||||
|
*/
|
||||||
|
onError(callback) {
|
||||||
|
this.onErrorCallback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置 WebSocket 连接关闭时的回调函数
|
||||||
|
* @param {function} callback 连接关闭的回调函数
|
||||||
|
*/
|
||||||
|
onClose(callback) {
|
||||||
|
this.onCloseCallback = callback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"manifest_version": 3,
|
||||||
|
"name": "Floating Bot Extension",
|
||||||
|
"version": "1.0",
|
||||||
|
"description": "一个可拖动的网页悬浮机器人窗口",
|
||||||
|
"permissions": ["activeTab", "storage"],
|
||||||
|
"host_permissions": ["<all_urls>"],
|
||||||
|
"background": {
|
||||||
|
"service_worker": "background.js"
|
||||||
|
},
|
||||||
|
"content_scripts": [
|
||||||
|
{
|
||||||
|
"matches": ["<all_urls>"],
|
||||||
|
"js": ["content.js"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"action": {
|
||||||
|
"default_popup": "popup.html",
|
||||||
|
"default_icon": {
|
||||||
|
"16": "icons/icon16.png",
|
||||||
|
"48": "icons/icon48.png",
|
||||||
|
"128": "icons/icon128.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Floating Bot</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial, sans-serif; padding: 10px; }
|
||||||
|
button { padding: 10px; cursor: pointer; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>浮动机器人</h2>
|
||||||
|
<button id="button1">开启麦克风</button>
|
||||||
|
<button id="button2">关闭麦克风</button>
|
||||||
|
<script src="js/crypto-js.min.js"></script>
|
||||||
|
<script src="js/constant.js"></script>
|
||||||
|
<script src="js/log.js"></script>
|
||||||
|
<script src="js/audio.js"></script>
|
||||||
|
<script src="js/webRTCAudioProcessor.js"></script>
|
||||||
|
<script src="js/handleMessage.js"></script>
|
||||||
|
<script src="js/webSocketUtil.js"></script>
|
||||||
|
<script src="js/iat.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
document.getElementById("toggleBot").addEventListener("click", () => {
|
||||||
|
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
|
||||||
|
chrome.scripting.executeScript({
|
||||||
|
target: { tabId: tabs[0].id },
|
||||||
|
function: toggleBot
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function toggleBot() {
|
||||||
|
let bot = document.getElementById("floating-bot");
|
||||||
|
if (bot) {
|
||||||
|
bot.style.display = bot.style.display === "none" ? "block" : "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
Loading…
Reference in New Issue