mirror of
https://github.com/xszyou/Fay.git
synced 2026-11-03 12:51:28 +08:00
519 lines
20 KiB
JavaScript
519 lines
20 KiB
JavaScript
class FayInterface {
|
|
constructor(baseWsUrl, baseApiUrl, vueInstance) {
|
|
this.baseWsUrl = baseWsUrl;
|
|
this.baseApiUrl = baseApiUrl;
|
|
this.websocket = null;
|
|
this.vueInstance = vueInstance;
|
|
}
|
|
|
|
connectWebSocket() {
|
|
if (this.websocket) {
|
|
this.websocket.onopen = null;
|
|
this.websocket.onmessage = null;
|
|
this.websocket.onclose = null;
|
|
this.websocket.onerror = null;
|
|
}
|
|
|
|
this.websocket = new WebSocket(this.baseWsUrl);
|
|
|
|
this.websocket.onopen = () => {
|
|
console.log('WebSocket connection opened');
|
|
};
|
|
|
|
this.websocket.onmessage = (event) => {
|
|
const data = JSON.parse(event.data);
|
|
this.handleIncomingMessage(data);
|
|
};
|
|
|
|
this.websocket.onclose = () => {
|
|
console.log('WebSocket connection closed. Attempting to reconnect...');
|
|
setTimeout(() => this.connectWebSocket(), 5000);
|
|
};
|
|
|
|
this.websocket.onerror = (error) => {
|
|
console.error('WebSocket error:', error);
|
|
};
|
|
}
|
|
|
|
async fetchData(url, options = {}) {
|
|
try {
|
|
const response = await fetch(url, options);
|
|
if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`);
|
|
return await response.json();
|
|
} catch (error) {
|
|
console.error('Error fetching data:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
getData() {
|
|
return this.fetchData(`${this.baseApiUrl}/api/get-data`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
});
|
|
}
|
|
|
|
submitConfig(config) {
|
|
return this.fetchData(`${this.baseApiUrl}/api/submit`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ config }),
|
|
});
|
|
}
|
|
|
|
startLive() {
|
|
return this.fetchData(`${this.baseApiUrl}/api/start-live`, {
|
|
method: 'POST',
|
|
});
|
|
}
|
|
|
|
stopLive() {
|
|
return this.fetchData(`${this.baseApiUrl}/api/stop-live`, {
|
|
method: 'POST',
|
|
});
|
|
}
|
|
|
|
getRunStatus() {
|
|
return this.fetchData(`${this.baseApiUrl}/api/get-run-status`, {
|
|
method: 'POST'
|
|
});
|
|
}
|
|
|
|
|
|
handleIncomingMessage(data) {
|
|
const vueInstance = this.vueInstance;
|
|
console.log('Incoming message:', data);
|
|
if (data.liveState !== undefined) {
|
|
vueInstance.liveState = data.liveState;
|
|
if (data.liveState === 1) {
|
|
vueInstance.configEditable = false;
|
|
} else if (data.liveState === 0) {
|
|
vueInstance.configEditable = true;
|
|
}
|
|
}
|
|
|
|
if (data.voiceList !== undefined) {
|
|
vueInstance.voiceList = data.voiceList.map((voice) => ({
|
|
value: voice.id,
|
|
label: voice.name,
|
|
}));
|
|
}
|
|
if (data.robot) {
|
|
console.log(data.robot);
|
|
vueInstance.$set(vueInstance, 'robot', data.robot);
|
|
}
|
|
|
|
if (data.is_connect !== undefined) {
|
|
vueInstance.isConnected = data.is_connect;
|
|
}
|
|
|
|
if (data.remote_audio_connect !== undefined) {
|
|
vueInstance.remoteAudioConnected = data.remote_audio_connect;
|
|
}
|
|
}
|
|
}
|
|
|
|
new Vue({
|
|
el: '#app',
|
|
delimiters: ['[[', ']]'],
|
|
data() {
|
|
return {
|
|
hostname: window.location.hostname,
|
|
base_url: window.location.origin,
|
|
messages: [],
|
|
newMessage: '',
|
|
fayService: null,
|
|
liveState: 0,
|
|
isConnected: false,
|
|
remoteAudioConnected: false,
|
|
userList: [],
|
|
selectedUser: null,
|
|
loading: false,
|
|
chatMessages: {},
|
|
panelMsg: '',
|
|
panelReply: '',
|
|
robot: 'images/emoji.png',
|
|
configEditable: true,
|
|
source_liveRoom_url: '',
|
|
play_sound_enabled: false,
|
|
mcpOnlineStatus: false,
|
|
mcpCheckTimer: null,
|
|
visualization_detection_enabled: false,
|
|
source_record_enabled: false,
|
|
source_record_device: '',
|
|
attribute_name: "",
|
|
attribute_gender: "",
|
|
attribute_age: "",
|
|
attribute_birth: "",
|
|
attribute_zodiac: "",
|
|
attribute_constellation: "",
|
|
attribute_job: "",
|
|
attribute_additional: "",
|
|
attribute_contact: "",
|
|
attribute_voice: "",
|
|
attribute_position: "",
|
|
attribute_goal: "",
|
|
QnA:"",
|
|
interact_perception_gift: 0,
|
|
interact_perception_follow: 0,
|
|
interact_perception_join: 0,
|
|
interact_perception_chat: 0,
|
|
interact_perception_indifferent: 0,
|
|
interact_maxInteractTime: 15,
|
|
voiceList: [],
|
|
deviceList: [],
|
|
wake_word_enabled:false,
|
|
wake_word: '',
|
|
loading: false,
|
|
remote_audio_connect: false,
|
|
wake_word_type: 'common',
|
|
wake_word_type_options: [{
|
|
value: 'common',
|
|
label: '普通'
|
|
}, {
|
|
value: 'front',
|
|
label: '前置词'
|
|
}],
|
|
automatic_player_status: false,
|
|
automatic_player_url: "",
|
|
host_url: window.location.protocol + '//' + window.location.hostname + ':' + window.location.port,
|
|
memory_isolate_by_user: false,
|
|
use_bionic_memory: false,
|
|
};
|
|
},
|
|
created() {
|
|
this.initFayService();
|
|
this.getData();
|
|
this.checkMcpStatus();
|
|
this.startMcpStatusTimer();
|
|
},
|
|
methods: {
|
|
initFayService() {
|
|
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws';
|
|
const wsHost = window.location.hostname;
|
|
this.fayService = new FayInterface(`${wsProtocol}://${wsHost}:10003`, this.host_url, this);
|
|
this.fayService.connectWebSocket();
|
|
},
|
|
getData() {
|
|
this.fayService.getRunStatus().then((data) => {
|
|
if (data) {
|
|
if(data.status){
|
|
this.liveState = 1;
|
|
this.configEditable = false;
|
|
}else{
|
|
this.liveState = 0;
|
|
this.configEditable = true;
|
|
}
|
|
|
|
}
|
|
});
|
|
this.fayService.getData().then((data) => {
|
|
if (data) {
|
|
this.voiceList = data.voice_list.map((voice) => ({
|
|
value: voice.id,
|
|
label: voice.name,
|
|
}));
|
|
this.updateConfigFromData(data.config);
|
|
}
|
|
});
|
|
},
|
|
updateConfigFromData(config) {
|
|
|
|
if (config.interact) {
|
|
this.play_sound_enabled = config.interact.playSound;
|
|
this.visualization_detection_enabled = config.interact.visualization;
|
|
this.QnA = config.interact.QnA;
|
|
}
|
|
if (config.source && config.source.record) {
|
|
this.source_record_enabled = config.source.record.enabled;
|
|
this.source_record_device = config.source.record.device;
|
|
this.wake_word = config.source.wake_word;
|
|
this.wake_word_type = config.source.wake_word_type;
|
|
this.wake_word_enabled = config.source.wake_word_enabled;
|
|
this.automatic_player_status = config.source.automatic_player_status;
|
|
this.automatic_player_url = config.source.automatic_player_url;
|
|
|
|
}
|
|
if (config.attribute) {
|
|
this.attribute_name = config.attribute.name;
|
|
this.attribute_gender = config.attribute.gender;
|
|
this.attribute_age = config.attribute.age;
|
|
this.attribute_name = config.attribute.name;
|
|
this.attribute_gender = config.attribute.gender;
|
|
this.attribute_birth = config.attribute.birth;
|
|
this.attribute_zodiac = config.attribute.zodiac;
|
|
this.attribute_constellation = config.attribute.constellation;
|
|
this.attribute_job = config.attribute.job;
|
|
this.attribute_additional = config.attribute.additional;
|
|
this.attribute_contact = config.attribute.contact;
|
|
this.attribute_voice = config.attribute.voice;
|
|
this.attribute_position = config.attribute.position || "客服";
|
|
this.attribute_goal = config.attribute.goal || "解决问题";
|
|
}
|
|
if (config.interact.perception) {
|
|
this.interact_perception_follow = config.interact.perception.follow;
|
|
}
|
|
if (config.memory) {
|
|
this.memory_isolate_by_user = config.memory.isolate_by_user || false;
|
|
this.use_bionic_memory = config.memory.use_bionic_memory || false;
|
|
}
|
|
},
|
|
saveConfig() {
|
|
let url = `${this.host_url}/api/submit`;
|
|
let send_data = {
|
|
"config": {
|
|
"source": {
|
|
"liveRoom": {
|
|
"enabled": this.configEditable,
|
|
"url": this.source_liveRoom_url
|
|
},
|
|
"record": {
|
|
"enabled": this.source_record_enabled,
|
|
"device": this.source_record_device
|
|
},
|
|
"wake_word_enabled": this.wake_word_enabled,
|
|
"wake_word": this.wake_word,
|
|
"wake_word_type": this.wake_word_type,
|
|
"automatic_player_status": this.automatic_player_status,
|
|
"automatic_player_url": this.automatic_player_url
|
|
},
|
|
"attribute": {
|
|
"voice": this.attribute_voice,
|
|
"name": this.attribute_name,
|
|
"gender": this.attribute_gender,
|
|
"age": this.attribute_age,
|
|
"birth": this.attribute_birth,
|
|
"zodiac": this.attribute_zodiac,
|
|
"constellation": this.attribute_constellation,
|
|
"job": this.attribute_job,
|
|
"additional": this.attribute_additional,
|
|
"contact": this.attribute_contact,
|
|
"position": this.attribute_position,
|
|
"goal": this.attribute_goal,
|
|
},
|
|
"interact": {
|
|
"playSound": this.play_sound_enabled,
|
|
"visualization": this.visualization_detection_enabled,
|
|
"QnA": this.QnA,
|
|
"perception": {
|
|
"follow": this.interact_perception_follow
|
|
},
|
|
"maxInteractTime": this.interact_maxInteractTime
|
|
},
|
|
"memory": {
|
|
"isolate_by_user": this.memory_isolate_by_user,
|
|
"use_bionic_memory": this.use_bionic_memory
|
|
},
|
|
"items": []
|
|
}
|
|
};
|
|
|
|
let xhr = new XMLHttpRequest()
|
|
xhr.open("post", url)
|
|
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded")
|
|
xhr.send('data=' + JSON.stringify(send_data))
|
|
let executed = false
|
|
xhr.onreadystatechange = async function () {
|
|
if (!executed && xhr.status === 200) {
|
|
try {
|
|
let data = await eval('(' + xhr.responseText + ')')
|
|
console.log("data: " + data['result'])
|
|
executed = true
|
|
} catch (e) {
|
|
}
|
|
}
|
|
}
|
|
this.sendSuccessMsg("配置已保存!");
|
|
},
|
|
startLive() {
|
|
this.liveState = 2
|
|
this.fayService.startLive().then(() => {
|
|
this.configEditable = false;
|
|
this.sendSuccessMsg('已开启!');
|
|
});
|
|
},
|
|
stopLive() {
|
|
this.liveState = 3
|
|
this.fayService.stopLive().then(() => {
|
|
this.configEditable = true;
|
|
this.sendSuccessMsg('已关闭!');
|
|
});
|
|
},
|
|
sendSuccessMsg(message) {
|
|
this.$notify({
|
|
title: '成功',
|
|
message,
|
|
type: 'success',
|
|
});
|
|
},
|
|
clearMemory() {
|
|
this.$confirm('清除记忆操作将删除Fay的所有对话记忆,清除后需要重启应用才能生效,确认继续吗?', '提示', {
|
|
confirmButtonText: '确定',
|
|
cancelButtonText: '取消',
|
|
type: 'warning'
|
|
}).then(() => {
|
|
// 发送清除记忆请求
|
|
fetch(`${this.host_url}/api/clear-memory`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' }
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
this.sendSuccessMsg(data.message || "记忆已清除,请重启应用使更改生效");
|
|
} else {
|
|
this.$notify({
|
|
title: '错误',
|
|
message: data.message || '清除记忆失败',
|
|
type: 'error'
|
|
});
|
|
}
|
|
})
|
|
.catch(error => {
|
|
this.$notify({
|
|
title: '错误',
|
|
message: '清除记忆请求失败',
|
|
type: 'error'
|
|
});
|
|
});
|
|
}).catch(() => {
|
|
// 用户取消操作
|
|
});
|
|
},
|
|
clonePersonality() {
|
|
// 检查是否启用了仿生记忆
|
|
if (this.use_bionic_memory) {
|
|
this.$notify({
|
|
title: '提示',
|
|
message: '仿生记忆模式下不支持人格克隆功能,请在设置中关闭仿生记忆后重试',
|
|
type: 'warning'
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (this.liveState === 1) {
|
|
this.$prompt('请输入克隆要求', '克隆人格', {
|
|
confirmButtonText: '确定',
|
|
cancelButtonText: '取消',
|
|
inputPlaceholder: '请输入克隆要求,例如:你现在是一个活泼开朗的助手...'
|
|
}).then(({ value }) => {
|
|
if (!value) {
|
|
this.$notify({
|
|
title: '提示',
|
|
message: '克隆要求不能为空',
|
|
type: 'warning'
|
|
});
|
|
return;
|
|
}
|
|
|
|
// 直接启动genagents_flask.py并打开decision_interview.html页面
|
|
fetch(`${this.host_url}/api/start-genagents`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ instruction: value })
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
// 弹出提示,显示克隆地址,不自动打开
|
|
this.$alert(`决策分析页面已启动,请复制以下链接在新窗口中打开:<br><br><code style="background-color: #f5f5f5; padding: 5px; border-radius: 3px;">${data.url}</code>`, '克隆人格', {
|
|
confirmButtonText: '确定',
|
|
dangerouslyUseHTMLString: true
|
|
});
|
|
} else {
|
|
this.$notify({
|
|
title: '错误',
|
|
message: data.message || '启动决策分析页面失败',
|
|
type: 'error'
|
|
});
|
|
}
|
|
})
|
|
.catch(error => {
|
|
this.$notify({
|
|
title: '错误',
|
|
message: '启动决策分析页面请求失败',
|
|
type: 'error'
|
|
});
|
|
});
|
|
});
|
|
} else {
|
|
this.$notify({
|
|
title: '提示',
|
|
message: '请先开Fay后再执行此操作',
|
|
type: 'warning'
|
|
});
|
|
}
|
|
},
|
|
|
|
// 检查MCP服务器状态
|
|
checkMcpStatus() {
|
|
const mcpUrl = `http://${this.hostname}:5010/api/mcp/servers`;
|
|
|
|
// 使用超时设置的fetch请求
|
|
const controller = new AbortController();
|
|
const timeoutId = setTimeout(() => controller.abort(), 3000); // 3秒超时
|
|
|
|
fetch(mcpUrl, { signal: controller.signal })
|
|
.then(response => {
|
|
clearTimeout(timeoutId);
|
|
if (!response.ok) {
|
|
throw new Error('MCP服务器响应不正常');
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
if (Array.isArray(data)) {
|
|
// 检查是否有任何一个MCP服务器在线
|
|
const hasOnlineServer = data.some(server => server.status === 'online');
|
|
this.mcpOnlineStatus = hasOnlineServer;
|
|
} else {
|
|
console.warn('MCP服务器返回的数据格式不正确');
|
|
this.mcpOnlineStatus = false;
|
|
}
|
|
})
|
|
.catch(error => {
|
|
clearTimeout(timeoutId);
|
|
// 如果是超时错误,不输出详细错误信息
|
|
if (error.name === 'AbortError') {
|
|
console.warn('MCP服务器请求超时');
|
|
} else {
|
|
console.warn('检查MCP状态出错:', error.message);
|
|
}
|
|
this.mcpOnlineStatus = false;
|
|
});
|
|
},
|
|
|
|
// 启动MCP状态检查定时器
|
|
startMcpStatusTimer() {
|
|
// 清除可能存在的旧定时器
|
|
if (this.mcpCheckTimer) {
|
|
clearInterval(this.mcpCheckTimer);
|
|
}
|
|
// 设置新的定时器,每30秒检查一次MCP状态
|
|
this.mcpCheckTimer = setInterval(() => {
|
|
this.checkMcpStatus();
|
|
}, 30000);
|
|
},
|
|
|
|
// 仿生记忆开关变化事件处理
|
|
onBionicMemoryChange(value) {
|
|
if (value) {
|
|
this.$confirm('开启仿生记忆后将使用不同的记忆系统,人格克隆功能和认知隔离功能将不可用。确认开启吗?', '提示', {
|
|
confirmButtonText: '确定',
|
|
cancelButtonText: '取消',
|
|
type: 'warning'
|
|
}).then(() => {
|
|
// 用户确认,保存配置
|
|
this.saveConfig();
|
|
}).catch(() => {
|
|
// 用户取消,恢复开关状态
|
|
this.use_bionic_memory = false;
|
|
});
|
|
} else {
|
|
// 关闭仿生记忆,直接保存配置
|
|
this.saveConfig();
|
|
}
|
|
},
|
|
},
|
|
});
|