| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370 |
- // 主应用入口
- import { checkOpusLoaded, initOpusEncoder } from './core/audio/opus-codec.js?v=0205';
- import { getAudioPlayer } from './core/audio/player.js?v=0205';
- import { checkMicrophoneAvailability, isHttpNonLocalhost } from './core/audio/recorder.js?v=0205';
- import { initMcpTools } from './core/mcp/tools.js?v=0205';
- import { uiController } from './ui/controller.js?v=0205';
- import { log } from './utils/logger.js?v=0205';
- // 辅助函数:将Base64数据转换为Blob
- function dataURItoBlob(dataURI) {
- const byteString = atob(dataURI.split(',')[1]);
- const mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
- const ab = new ArrayBuffer(byteString.length);
- const ia = new Uint8Array(ab);
- for (let i = 0; i < byteString.length; i++) {
- ia[i] = byteString.charCodeAt(i);
- }
- return new Blob([ab], { type: mimeString });
- }
- // 应用类
- class App {
- constructor() {
- this.uiController = null;
- this.audioPlayer = null;
- this.live2dManager = null;
- this.cameraStream = null;
- this.currentFacingMode = 'user';
- }
- // 初始化应用
- async init() {
- log('正在初始化应用...', 'info');
- // 初始化UI控制器
- this.uiController = uiController;
- this.uiController.init();
- // 检查Opus库
- checkOpusLoaded();
- // 初始化Opus编码器
- initOpusEncoder();
- // 初始化音频播放器
- this.audioPlayer = getAudioPlayer();
- await this.audioPlayer.start();
- // 初始化MCP工具
- initMcpTools();
- // 检查麦克风可用性
- await this.checkMicrophoneAvailability();
- // 检查摄像头可用性
- this.checkCameraAvailability();
- // 初始化Live2D
- await this.initLive2D();
- // 初始化摄像头
- this.initCamera();
- // 关闭加载loading
- this.setModelLoadingStatus(false);
- log('应用初始化完成', 'success');
- }
- // 初始化Live2D
- async initLive2D() {
- try {
- // 检查Live2DManager是否已加载
- if (typeof window.Live2DManager === 'undefined') {
- throw new Error('Live2DManager未加载,请检查脚本引入顺序');
- }
- this.live2dManager = new window.Live2DManager();
- await this.live2dManager.initializeLive2D();
- // 更新UI状态
- const live2dStatus = document.getElementById('live2dStatus');
- if (live2dStatus) {
- live2dStatus.textContent = '● 已加载';
- live2dStatus.className = 'status loaded';
- }
- log('Live2D初始化完成', 'success');
- } catch (error) {
- log(`Live2D初始化失败: ${error.message}`, 'error');
- // 更新UI状态
- const live2dStatus = document.getElementById('live2dStatus');
- if (live2dStatus) {
- live2dStatus.textContent = '● 加载失败';
- live2dStatus.className = 'status error';
- }
- }
- }
- // 设置model加载状态
- setModelLoadingStatus(isLoading) {
- const modelLoading = document.getElementById('modelLoading');
- if (modelLoading) {
- modelLoading.style.display = isLoading ? 'flex' : 'none';
- }
- }
- /**
- * 检查麦克风可用性
- * 在应用初始化时调用,检查麦克风是否可用并更新UI状态
- */
- async checkMicrophoneAvailability() {
- try {
- const isAvailable = await checkMicrophoneAvailability();
- const isHttp = isHttpNonLocalhost();
- // 保存可用性状态到全局变量
- window.microphoneAvailable = isAvailable;
- window.isHttpNonLocalhost = isHttp;
- // 更新UI
- if (this.uiController) {
- this.uiController.updateMicrophoneAvailability(isAvailable, isHttp);
- }
- log(`麦克风可用性检查完成: ${isAvailable ? '可用' : '不可用'}`, isAvailable ? 'success' : 'warning');
- } catch (error) {
- log(`检查麦克风可用性失败: ${error.message}`, 'error');
- // 默认设置为不可用
- window.microphoneAvailable = false;
- window.isHttpNonLocalhost = isHttpNonLocalhost();
- if (this.uiController) {
- this.uiController.updateMicrophoneAvailability(false, window.isHttpNonLocalhost);
- }
- }
- }
- // 检查摄像头可用性
- checkCameraAvailability() {
- window.cameraAvailable = true;
- log('摄像头可用性检查完成: 默认已绑定验证码', 'success');
- }
- // 初始化摄像头
- async initCamera() {
- const cameraContainer = document.getElementById('cameraContainer');
- const cameraVideo = document.getElementById('cameraVideo');
- const cameraSwitch = document.getElementById('cameraSwitch');
- const cameraSwitchMask = document.getElementById('cameraSwitchMask');
- const dialBtn = document.getElementById('dialBtn');
- if (!cameraContainer || !cameraVideo) {
- log('摄像头元素未找到,跳过初始化', 'warning');
- return Promise.resolve(false);
- }
- let isDragging = false;
- let currentX, currentY, initialX, initialY;
- let xOffset = 0, yOffset = 0;
- cameraContainer.addEventListener('mousedown', dragStart);
- document.addEventListener('mousemove', drag);
- document.addEventListener('mouseup', dragEnd);
- cameraContainer.addEventListener('touchstart', dragStart, { passive: false });
- document.addEventListener('touchmove', drag, { passive: false });
- document.addEventListener('touchend', dragEnd);
- function dragStart(e) {
- if (e.type === 'touchstart') {
- initialX = e.touches[0].clientX - xOffset;
- initialY = e.touches[0].clientY - yOffset;
- } else {
- initialX = e.clientX - xOffset;
- initialY = e.clientY - yOffset;
- }
- isDragging = true;
- cameraContainer.classList.add('dragging');
- }
- function drag(e) {
- if (isDragging) {
- e.preventDefault();
- if (e.type === 'touchmove') {
- currentX = e.touches[0].clientX - initialX;
- currentY = e.touches[0].clientY - initialY;
- } else {
- currentX = e.clientX - initialX;
- currentY = e.clientY - initialY;
- }
- xOffset = currentX;
- yOffset = currentY;
- cameraContainer.style.transform = `translate3d(${currentX}px, ${currentY}px, 0)`;
- }
- }
- function dragEnd() {
- initialX = currentX;
- initialY = currentY;
- isDragging = false;
- cameraContainer.classList.remove('dragging');
- }
- return new Promise((resolve) => {
- window.startCamera = async () => {
- try {
- if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
- log('浏览器不支持摄像头API', 'warning');
- return false;
- }
- log('正在请求摄像头权限...', 'info');
- this.cameraStream = await navigator.mediaDevices.getUserMedia({
- video: { width: 180, height: 240, facingMode: this.currentFacingMode },
- audio: false
- });
- cameraVideo.srcObject = this.cameraStream;
- const devices = await navigator.mediaDevices.enumerateDevices();
- const videoDevices = devices.filter(device => device.kind === 'videoinput');
- if (videoDevices.length > 1) {
- if (cameraSwitch) cameraSwitch.classList.add('active');
- }
- cameraContainer.classList.add('active');
- // 切换时挂断情况
- const hasActive = dialBtn.classList.contains('dial-active');
- if (!hasActive) {
- cameraContainer.classList.remove('active');
- cameraSwitch.classList.remove('active');
- window.stopCamera();
- }
- log('摄像头已启动', 'success');
- return true;
- } catch (error) {
- log(`启动摄像头失败: ${error.name} - ${error.message}`, 'error');
- if (error.name === 'NotAllowedError') {
- log('摄像头权限被拒绝,请检查浏览器设置', 'warning');
- } else if (error.name === 'NotFoundError') {
- log('未找到摄像头设备', 'warning');
- } else if (error.name === 'NotReadableError') {
- log('摄像头已被其他程序占用', 'warning');
- }
- return false;
- }
- };
- window.stopCamera = () => {
- if (this.cameraStream) {
- this.cameraStream.getTracks().forEach(track => track.stop());
- this.cameraStream = null;
- cameraVideo.srcObject = null;
- log('摄像头已关闭', 'info');
- }
- };
- window.switchCamera = async() => {
- if (window.switchCameraTimer) return;
- if (this.cameraStream) {
- const currentTransform = window.getComputedStyle(cameraContainer).transform;
- const originalTransform = currentTransform === 'none' ? 'translate(0px, 0px)' : currentTransform;
- cameraContainer.style.setProperty('--original-transform', originalTransform);
- cameraContainer.classList.add('flip');
- if (cameraSwitchMask) cameraSwitchMask.style.opacity = 0;
- this.currentFacingMode = this.currentFacingMode === 'user' ? 'environment' : 'user';
- window.stopCamera();
- window.startCamera();
-
- window.switchCameraTimer = setTimeout(() => {
- if (this.currentFacingMode === 'user') {
- cameraVideo.style.transform = 'scaleX(-1)';
- } else {
- cameraVideo.style.transform = 'scaleX(1)';
- }
- window.switchCameraTimer = null;
- cameraContainer.classList.remove('flip');
- cameraContainer.style.removeProperty('--original-transform');
- if (cameraSwitchMask) cameraSwitchMask.style.opacity = 1;
- }, 500);
- }
- };
- window.takePhoto = (question = '描述一下看到的物品') => {
- return new Promise(async (resolve) => {
- const canvas = document.createElement('canvas');
- const video = cameraVideo;
- if (!video || video.readyState !== video.HAVE_ENOUGH_DATA) {
- log('无法拍照:摄像头未就绪', 'warning');
- resolve({
- success: false,
- error: '摄像头未就绪,请确保已连接且摄像头已启动'
- });
- return;
- }
- canvas.width = video.videoWidth || 180;
- canvas.height = video.videoHeight || 240;
- const ctx = canvas.getContext('2d');
- ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
- const photoData = canvas.toDataURL('image/jpeg', 0.8);
- log(`拍照成功,图像数据长度: ${photoData.length}`, 'success');
- try {
- const xz_tester_vision = localStorage.getItem('xz_tester_vision');
- if (xz_tester_vision) {
- let visionInfo = null;
- try {
- visionInfo = JSON.parse(xz_tester_vision);
- } catch (err) {
- throw new Error(`视觉配置解析失败`);
- }
- const { url, token } = visionInfo || {};
- if (!url || !token) {
- throw new Error('视觉分析失败:配置缺少接口地址(url)或令牌(token)');
- }
- log(`正在发送图片到视觉分析接口: ${url}`, 'info');
- const deviceId = document.getElementById('deviceMac')?.value || '';
- const clientId = document.getElementById('clientId')?.value || 'web_test_client';
- const formData = new FormData();
- formData.append('question', question);
- formData.append('image', dataURItoBlob(photoData), 'photo.jpg');
- const response = await fetch(url, {
- method: 'POST',
- body: formData,
- headers: {
- 'Device-Id': deviceId,
- 'Client-Id': clientId,
- 'Authorization': `Bearer ${token}`
- }
- });
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
- }
- const analysisResult = await response.json();
- log(`视觉分析完成: ${JSON.stringify(analysisResult).substring(0, 200)}...`, 'success');
- resolve({
- success: true,
- message: question,
- photo_data: photoData,
- photo_width: canvas.width,
- photo_height: canvas.height,
- vision_analysis: analysisResult
- });
- } else {
- log('未配置视觉分析服务', 'warning');
- }
- } catch (error) {
- log(`视觉分析失败: ${error.message}`, 'error');
- resolve({
- success: true,
- message: question,
- photo_data: photoData,
- photo_width: canvas.width,
- photo_height: canvas.height,
- vision_analysis: {
- success: false,
- error: error.message,
- fallback: '无法连接到视觉分析服务'
- }
- });
- }
- });
- };
- log('摄像头初始化完成', 'success');
- resolve(true);
- });
- }
- }
- // 创建并启动应用
- const app = new App();
- // 将应用实例暴露到全局,供其他模块访问
- window.chatApp = app;
- document.addEventListener('DOMContentLoaded', () => {
- // 初始化应用
- app.init();
- });
- export default app;
|