More XToys

Takes over XToys's custom serial port toy functionality, replacing it with custom Bluetooth toys (currently supporting Roussan).

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да инсталирате разширение, като например Tampermonkey .

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

// ==UserScript==
// @name          More XToys
// @name:zh-CN    XToys 玩具多多
// @namespace     https://github.com/forest-devil/
// @version       1.46
// @description   Takes over XToys's custom serial port toy functionality, replacing it with custom Bluetooth toys (currently supporting Roussan).
// @description:zh-CN 接管XToys的自定义串口功能,替换为通过蓝牙控制玩具(当前支持若仙)。
// @author        forest-devil & Gemini
// @license       Apache
// @match         https://xtoys.app/*
// @grant         unsafeWindow
// @grant         GM_log
// @grant         GM_registerMenuCommand
// @grant         GM_unregisterMenuCommand
// @grant         GM_getValue
// @grant         GM_setValue
// @run-at        document-start
// @icon          https://xtoys.app/icons/favicon-96x96.png
// ==/UserScript==

(() => {
    'use strict';

    // --- 配置区域 ---
    /**
     * @type {boolean} 调试模式开关
     * 设置为 true: 启用无设备调试模式。脚本将不会连接真实蓝牙设备,而是将数据流打印到控制台。
     * 设置为 false: 正常模式,脚本将尝试连接真实的蓝牙设备。
     */
    let DEBUG_MODE = GM_getValue("DEBUG_MODE", false);
    // 用于调试模式下生成友好设备名的计数器
    let debugDeviceCounter = 0;

    // --- 模拟 USB ID 常量 ---
    // 这些常量用于为模拟串口设备创建唯一的 USB ID
    const MOCK_USB_VENDOR_ID = 0x1A86;
    // MOCK_USB_PRODUCT_ID_BASE 提供了产品 ID 的起始基数,用于生成唯一 ID
    const MOCK_USB_PRODUCT_ID_BASE = 0x7523;

    /**
     * 切换调试模式并刷新页面。
     */
    function toggleDebugMode() {
        DEBUG_MODE = !DEBUG_MODE;
        GM_setValue("DEBUG_MODE", DEBUG_MODE);
        console.log(`[Xtoys 玩具多多] 调试模式: ${DEBUG_MODE ? '已开启' : '已关闭'}`);
        location.reload();
    }

    // 注册菜单命令来切换调试模式
    GM_registerMenuCommand(
        `切换调试模式 (当前: ${DEBUG_MODE ? '开启' : '关闭'})`, // 菜单项显示的文本
        toggleDebugMode
    );

    // 在这里定义所有支持的蓝牙协议
    const PROTOCOLS = {
        "Roussan": {
            serviceUUID: "fe400001-b5a3-f393-e0a9-e50e24dcca9e",
            writeUUID: "fe400002-b5a3-f393-e0a9-e50e24dcca9e",
            notifyUUID: "fe400003-b5a3-f393-e0a9-e50e24dcca9e",
            /**
             * 将输入的指令转换为蓝牙数据包
             * @param {object} command - 从模拟串口接收到的JSON对象
             * @returns {Uint8Array | null} - 转换后的数据包或在无效输入时返回null
             */
            transform: command => {
                // 优先使用 vibrate,其次是 Speed
                let speed = command.vibrate ?? command.speed ?? null;
                if (speed === null) {
                    console.warn('[Xtoys 玩具多多] 指令对象中不包含 "vibrate" 或 "speed" 键。', command);
                    return null;
                }
                // 确保速度值在 0-100 范围内
                speed = Math.max(0, Math.min(100, parseInt(speed, 10)));
                // 若仙协议只支持30档速度,需要从0~100转换为0~30
                // 将速度值从 0-100 映射到 0-30
                speed = Math.round(speed * 0.3);
                // 封装数据包: 55 AA 03 01 XX 00
                return new Uint8Array([0x55, 0xAA, 0x03, 0x01, speed, 0x00]);
            }
        }
        // 未来可以在这里添加更多协议...
    };

    // --- 脚本核心逻辑 ---

    console.info('[Xtoys 玩具多多] 脚本已加载。正在等待拦截串口请求...');
    if (DEBUG_MODE) {
        console.warn('[Xtoys 玩具多多] 调试模式已启用。将不使用真实蓝牙设备。');
    }

    /**
     * @typedef {Object} BleConnectionState
     * @property {BluetoothDevice} device - 蓝牙设备对象
     * @property {BluetoothRemoteGATTServer} server - GATT 服务器对象
     * @property {BluetoothRemoteGATTCharacteristic} writeCharacteristic - 写入特性
     * @property {BluetoothRemoteGATTCharacteristic | null} notifyCharacteristic - 通知特性 (可能没有)
     * @property {Object} activeProtocol - 当前设备使用的协议配置
     * @property {MockSerialPort} mockPortInstance - 关联的模拟 SerialPort 实例
     * @property {string | null} xtoysDeviceId - XToys 为此设备提供的 'id' 字段 (如果有)。
     * @property {number} usbVendorId - 此设备的模拟 USB 供应商 ID。
     * @property {number} usbProductId - 此设备的设备的模拟 USB 产品 ID。
     */
    const activeConnections = new Map(); // 使用 Map 存储,键可以是 device.id

    /**
     * 重置并移除某个设备的连接状态
     * @param {string} deviceId -设备的ID
     */
    function removeConnectionState(deviceId) {
        if (!activeConnections.has(deviceId)) {
            return;
        }

        const state = activeConnections.get(deviceId);
        // 获取更友好的设备名称,用于日志
        const deviceFriendlyName = state.device?.name || `ID: ${state.device?.id}`;

        if (state.notifyCharacteristic) {
            try {
                state.notifyCharacteristic.removeEventListener('characteristicvaluechanged', handleNotifications);
                state.notifyCharacteristic.stopNotifications();
            } catch (e) {
                // 优化日志信息:根据设备是否已断开来判断是否为预期错误
                if (!state.device?.gatt?.connected) {
                    console.info(`[Xtoys 玩具多多] 设备 ${deviceFriendlyName} 已断开,停止通知时出现预期错误: ${e.message}`);
                } else {
                    console.error(`[Xtoys 玩具多多] 设备 ${deviceFriendlyName} 停止通知时出现未知错误: ${e.message}`);
                }
            }
        }
        // 检查 state.device 和 state.device.gatt 是否存在
        if (state.device?.gatt?.connected) {
            // 捕获 disconnect() Promise 的拒绝,避免 Uncaught (in promise) 错误
            state.device.gatt.disconnect().catch(e => {
                console.warn(`[Xtoys 玩具多多] 断开设备 ${deviceFriendlyName} GATT 连接时发生错误 (可能已断开):`, e);
            });
        }

        activeConnections.delete(deviceId);
        console.info(`[Xtoys 玩具多多] 已断开并移除设备 ${deviceFriendlyName} 的连接状态。当前活跃连接数: ${activeConnections.size}`);
    }

    /**
     * 处理来自蓝牙设备的通知
     * @param {Event} event - 特性值改变事件
     */
    function handleNotifications(event) {
        const { value, target: characteristic } = event;
        const { service } = characteristic;
        const { id: deviceId } = service.device;
        const deviceFriendlyName = activeConnections.get(deviceId)?.device?.name || `ID: ${deviceId}`;
        console.debug(`[Xtoys 玩具多多] 收到来自设备 ${deviceFriendlyName} 的通知:`, value);
        // 在这里可以添加对通知数据的处理逻辑,例如推送到对应的 readable stream
        // 由于Xtoys主要使用 writable stream,此处暂时只做日志输出
    }

    /**
     * 伪造的 requestPort 函数,用于替代原始的 navigator.serial.requestPort
     * 每次调用都会尝试连接一个新设备
     */
    async function mockRequestPort() {
        console.info('[Xtoys 玩具多多] 已拦截 navigator.serial.requestPort() 调用。等待设备选择...');

        // 调试模式逻辑
        if (DEBUG_MODE) {
            // 在调试模式下,每次请求都创建一个新的模拟端口
            const defaultProtocolName = Object.keys(PROTOCOLS)[0];
            if (!defaultProtocolName) {
                console.error('[Xtoys 玩具多多 调试模式] 未定义任何协议。无法在调试模式下运行。');
                return Promise.reject(new Error("未定义任何协议。"));
            }
            const activeProtocol = PROTOCOLS?.[defaultProtocolName];

            // 递增计数器,用于生成调试设备的唯一 ID
            debugDeviceCounter++;

            // 为调试设备生成唯一的友好名称和模拟 USB ID
            const debugDeviceId = `debug-device-${Date.now()}-${debugDeviceCounter}`;
            const debugDeviceName = `调试设备 #${debugDeviceCounter}`;
            const mockUsbProductId = MOCK_USB_PRODUCT_ID_BASE + debugDeviceCounter; // 为每个调试设备生成唯一的 Product ID

            console.info(`[Xtoys 玩具多多 调试模式] 正在使用协议 "${defaultProtocolName}" 模拟设备 "${debugDeviceName}"。`);
            console.info(`[Xtoys 玩具多多 调试模式] 生成的设备ID: ${debugDeviceId}, 模拟 USB 产品ID: 0x${mockUsbProductId.toString(16)}`);

            const debugConnectionState = {
                device: { id: debugDeviceId, name: debugDeviceName }, // 模拟 device 对象,包含友好名称
                server: null,
                writeCharacteristic: null, // 在调试模式下不实际使用
                notifyCharacteristic: null, // 不实际使用
                activeProtocol,
                mockPortInstance: null, // 将在 createMockSerialPort 中赋值
                xtoysDeviceId: null, // 调试设备初始没有 XToys Device ID
                usbVendorId: MOCK_USB_VENDOR_ID,
                usbProductId: mockUsbProductId
            };
            activeConnections.set(debugDeviceId, debugConnectionState);
            const mockPort = createMockSerialPort(debugConnectionState);
            debugConnectionState.mockPortInstance = mockPort;

            return mockPort;
        }

        // 真实蓝牙设备连接逻辑
        let device;
        try {
            //let filters = Object.values(PROTOCOLS).map(p => ({ services: [p.serviceUUID] }));
            // 获取所有协议的服务UUID,用于 optionalServices
            let optionalServices = Object.values(PROTOCOLS).map(p => p.serviceUUID);

            console.info('[Xtoys 玩具多多] 正在请求蓝牙设备,搜索条件:', /*filters*/ '所有设备');
            console.info('[Xtoys 玩具多多] 可选服务:', optionalServices); // 新增日志

            device = await navigator.bluetooth.requestDevice({
                acceptAllDevices: true,
                optionalServices: optionalServices // 添加 optionalServices
            });
            const deviceFriendlyName = device.name || `ID: ${device.id}`;
            console.info('[Xtoys 玩具多多] 已选择设备: %s', deviceFriendlyName);

            // 检查该设备是否已连接,如果已连接则复用其模拟端口
            if (activeConnections.has(device.id) && activeConnections.get(device.id).device?.gatt?.connected) {
                console.info(`[Xtoys 玩具多多] 设备 ${deviceFriendlyName} 已连接。正在复用现有模拟端口。`);
                return activeConnections.get(device.id).mockPortInstance;
            }

            // 添加 GATT 断开连接事件监听器
            device.addEventListener('gattserverdisconnected', function() {
                console.warn(`[Xtoys 玩具多多] 蓝牙设备 ${deviceFriendlyName} 已断开连接。`);
                removeConnectionState(device.id); // 移除该设备的连接状态
            });

            console.info(`[Xtoys 玩具多多] 正在连接设备 ${deviceFriendlyName} 的 GATT 服务器...`);
            const server = await device.gatt.connect();

            let activeProtocol = null;
            let writeCharacteristic = null;
            let notifyCharacteristic = null;

            for (const protocolName in PROTOCOLS) {
                const protocol = PROTOCOLS?.[protocolName];
                try {
                    const service = await server.getPrimaryService(protocol.serviceUUID);
                    if (service) {
                        console.info(`[Xtoys 玩具多多] 在设备 ${deviceFriendlyName} 上找到匹配的协议服务: "${protocolName}"`);
                        activeProtocol = protocol;
                        writeCharacteristic = await service.getCharacteristic(protocol.writeUUID);
                        try {
                            notifyCharacteristic = await service.getCharacteristic(protocol.notifyUUID);
                            await notifyCharacteristic.startNotifications();
                            console.info('[Xtoys 玩具多多] 已订阅通知。');
                            notifyCharacteristic.addEventListener('characteristicvaluechanged', handleNotifications);
                        } catch (notifyError) {
                            console.warn('[Xtoys 玩具多多] 无法获取或订阅通知特性。', notifyError.message);
                        }
                        break; // 找到一个匹配的协议就停止
                    }
                } catch (serviceError) {
                    // 直接在 serviceError 捕获块中给出用户提示
                    const userMessage = `尝试连接设备 ${deviceFriendlyName} 上的控制服务失败。请检查设备是否已开启且在范围内,并尝试重新连接。\n详细错误: ${serviceError.message}`;
                    showUserMessage('[Xtoys 玩具多多] 连接失败', userMessage); // 显示UI提示
                    console.error(`[Xtoys 玩具多多] ${userMessage}`); // 同时输出到控制台
                    throw serviceError; // 重新抛出错误,让外层catch捕获
                }
            }

            if (!activeProtocol || !writeCharacteristic) {
                // 如果代码执行到这里,说明没有找到匹配的协议或写入特性,
                // 且之前的 try-catch 并没有捕获到错误(例如,getPrimaryService返回null但未抛出错误)。
                // 此时抛出更通用的错误,外层catch会处理。
                const userMessage = `在所选设备 ${deviceFriendlyName} 上找不到匹配的协议或写入特性。请确保设备支持所选协议。`;
                showUserMessage('[Xtoys 玩具多多] 连接失败', userMessage); // 显示UI提示
                console.error(`[Xtoys 玩具多多] ${userMessage}`); // 同时输出到控制台
                throw new Error(userMessage); // 仍然抛出错误以保持Promise链的拒绝
            }

            // 为真实蓝牙设备生成一个唯一的模拟 USB 产品 ID
            // 使用 Date.now() 的一部分,以确保每次连接的 Product ID 都是不同的
            const mockUsbProductId = MOCK_USB_PRODUCT_ID_BASE + (Date.now() % 10000);

            // 存储新的连接状态
            const newConnectionState = {
                device,
                server,
                writeCharacteristic,
                notifyCharacteristic,
                activeProtocol,
                mockPortInstance: null, // 将在 createMockSerialPort 中赋值
                xtoysDeviceId: null, // 真实设备初始没有 XToys Device ID
                usbVendorId: MOCK_USB_VENDOR_ID,
                usbProductId: mockUsbProductId
            };
            activeConnections.set(device.id, newConnectionState); // 将新连接存储到 Map 中

            console.info(`[Xtoys 玩具多多] 设备 ${deviceFriendlyName} 蓝牙连接成功并准备就绪。模拟 USB 产品ID: 0x${mockUsbProductId.toString(16)}。当前活跃连接数: ${activeConnections.size}`);
            const mockPort = createMockSerialPort(newConnectionState);
            newConnectionState.mockPortInstance = mockPort; // 将模拟端口实例也保存到状态中

            return mockPort;

        } catch (error) {
            const deviceFriendlyName = device?.name || (device?.id ? `ID: ${device.id}` : '未知设备');
            let userMessageTitle = '[Xtoys 玩具多多] 连接失败';
            let userMessage = `尝试连接蓝牙设备 ${deviceFriendlyName} 时发生错误。`;
            let suppressUiMessage = false; // 标记是否抑制UI消息

            // 检查是否是用户取消选择 (NotFoundError)
            if (error.name === 'NotFoundError') {
                userMessage = '蓝牙设备选择已取消。'; // 更精确的控制台消息
                console.info(`[Xtoys 玩具多多] ${userMessage}`); // 记录为 info
                suppressUiMessage = true; // 抑制UI对话框
            }
            // 检查是否是连接或通信失败 (NetworkError 或特定消息)
            else if (error.name === 'NetworkError' || error.message.includes('GATT operation failed') || error.message.includes('Failed to connect')) {
                userMessage = `设备 ${deviceFriendlyName} 连接或通信失败。请检查设备是否已开启且在范围内,并尝试重新连接。`;
                console.error(`[Xtoys 玩具多多] ${userMessageTitle}: ${userMessage}\n原始错误:`, error);
            }
            // 对于 Origin is not allowed to access any service 错误
            else if (error.name === 'SecurityError' && error.message.includes('Origin is not allowed to access any service')) {
                userMessage = `连接设备 ${deviceFriendlyName} 失败:浏览器安全策略限制了对蓝牙服务的访问。请确保所有要访问的服务 UUID 都已添加到 'optionalServices' 中。`;
                console.error(`[Xtoys 玩具多多] ${userMessageTitle}: ${userMessage}\n原始错误:`, error);
            }
            // 对于其他未被内层 serviceError 捕获的通用错误,或内层 serviceError 重新抛出的错误,使用通用提示
            else {
                 userMessage = `连接过程中发生未知错误: ${error.message}。请尝试重新连接。`;
                 console.error(`[Xtoys 玩具多多] ${userMessageTitle}: ${userMessage}\n原始错误:`, error);
            }

            // 只有在不抑制UI消息的情况下才显示
            if (!suppressUiMessage) {
                showUserMessage(userMessageTitle, userMessage);
            }

            // 如果连接失败,并且设备ID已经存在于 Map 中(可能是在选择设备后但在连接前失败),则将其移除
            if (device && activeConnections.has(device.id)) {
                removeConnectionState(device.id);
            }
            return Promise.reject(error); // 仍然拒绝 Promise
        }
    }

    /**
     * 创建一个模拟的 SerialPort 对象,并将其绑定到特定的连接状态
     * @param {BleConnectionState} connectionState - 当前设备的连接状态
     * @returns {object} 一个包含可写流的模拟 Port 对象
     */
    function createMockSerialPort(connectionState) {
        // 获取更友好的设备名称,用于日志和 getInfo().deviceName
        // 优先使用 XToys Device ID,然后是连接状态中的设备名称,最后是通用的蓝牙 ID 字符串
        const deviceFriendlyName = connectionState.xtoysDeviceId || connectionState.device?.name || `蓝牙设备 ${connectionState.device?.id?.substring(0, 8)}`;

        const mockPort = {
            // 这个可写流将作为所有来自 XToys 的传入命令的路由器
            writable: new WritableStream({
                async write(chunk) {
                    try {
                        const commandStr = new TextDecoder().decode(chunk);
                        const command = JSON.parse(commandStr);
                        console.debug(`[Xtoys 玩具多多] 收到原始命令 (来自 ${deviceFriendlyName}):`, command);

                        const incomingCommandId = command.id;

                        // 根据 XToys 的保证:在一次连接中,给设备传输的所有命令的id是一致的。
                        // 因此,如果命令中包含 ID,并且它与当前连接的 xtoysDeviceId 不同,
                        // 我们就更新 xtoysDeviceId。这确保了 xtoysDeviceId 始终是 XToys 正在使用的 ID。
                        if (incomingCommandId && connectionState.xtoysDeviceId !== incomingCommandId) {
                            connectionState.xtoysDeviceId = incomingCommandId;
                            console.info(`[Xtoys 玩具多多] 设备 '${connectionState.device.name}' (内部ID: ${connectionState.device.id}) 现在映射到 XToys Device ID '${incomingCommandId}'。`);
                        }

                        // 使用当前 connectionState 的协议转换命令
                        const dataPacket = connectionState.activeProtocol?.transform(command);

                        if (dataPacket) {
                            const hexString = `${Array.from(dataPacket).map(b => b.toString(16).padStart(2, '0')).join(' ')}`;

                            if (DEBUG_MODE) {
                                // 使用最合适的名称进行调试日志
                                const logDeviceName = connectionState.xtoysDeviceId || connectionState.device?.name || connectionState.device?.id;
                                console.debug(`[Xtoys 玩具多多 调试模式] 正在路由到设备 '${logDeviceName}' (XToys Device ID: ${incomingCommandId || 'N/A'})。发送 HEX 数据: ${hexString}`);
                            } else {
                                if (!connectionState.writeCharacteristic) {
                                    console.error(`[Xtoys 玩具多多] 设备 '${connectionState.device?.name || connectionState.device?.id}' 写入失败: 此端口的蓝牙特性不可用。`);
                                    throw new Error("此端口的蓝牙特性不可用。");
                                }
                                const logDeviceName = connectionState.xtoysDeviceId || connectionState.device?.name || connectionState.device?.id;
                                console.debug(`[Xtoys 玩具多多] 正在路由到设备 '${logDeviceName}' (XToys Device ID: ${incomingCommandId || 'N/A'})。已转换为 HEX ${hexString} 并发送到蓝牙设备...`);
                                await connectionState.writeCharacteristic.writeValueWithoutResponse(dataPacket);
                            }
                        } else {
                            console.warn(`[Xtoys 玩具多多] 设备 '${connectionState.device?.name || connectionState.device?.id}' 未生成数据包,跳过写入操作。`);
                        }
                    } catch (error) {
                        console.error(`[Xtoys 玩具多多] 设备 ${deviceFriendlyName} 处理和写入数据失败:`, error);
                        // 如果是 JSON 解析错误,则特别记录
                        if (error instanceof SyntaxError && error.message.includes("JSON.parse")) {
                            console.error(`[Xtoys 玩具多多] 可能收到非 JSON 数据: ${new TextDecoder().decode(chunk).substring(0, 100)}...`);
                        }
                        throw error;
                    }
                },
                close: () => {
                    console.info(`[Xtoys 玩具多多] 设备 ${deviceFriendlyName} 的可写流已关闭。`);
                    if (!DEBUG_MODE && connectionState.device?.gatt?.connected) {
                        removeConnectionState(connectionState.device.id);
                    }
                },
                abort: (err) => {
                    console.error(`[Xtoys 玩具多多] 设备 ${deviceFriendlyName} 的可写流已中止:`, err);
                    if (!DEBUG_MODE && connectionState.device?.gatt?.connected) {
                        removeConnectionState(connectionState.device.id);
                    }
                }
            }),
            readable: new ReadableStream({
                start: (controller) => { console.info(`[Xtoys 玩具多多] 设备 ${deviceFriendlyName} 的模拟可读流已启动。`); }
                // TODO: 如果需要从蓝牙设备接收数据并推送到 ReadableStream,需要在这里实现
                // 例如:在 handleNotifications 中将数据 enqueue 到 controller
            }),
            getInfo: () => ({
                usbVendorId: connectionState.usbVendorId,
                usbProductId: connectionState.usbProductId,
                bluetoothServiceClassId: connectionState.activeProtocol?.serviceUUID || '',
                deviceId: connectionState.device?.id,
                // 优先使用 XToys Device ID 作为显示名称,然后是内部设备名称,最后是通用蓝牙 ID
                deviceName: connectionState.xtoysDeviceId || connectionState.device?.name || `蓝牙设备 ${connectionState.device?.id?.substring(0, 8)}`
            }),
            open: async (options) => {
                console.info(`[Xtoys 玩具多多] 设备 ${deviceFriendlyName} 的模拟端口已使用选项打开:`, options);
                return Promise.resolve();
            },
            close: async () => {
                console.info(`[Xtoys 玩具多多] 设备 ${deviceFriendlyName} 的模拟端口 close() 方法被调用。`);
                try {
                    if (this.writable && !this.writable.locked) {
                        await this.writable.close();
                    }
                } catch (e) {
                    // 优化:处理预期的 'Cannot close a ERRORED writable stream' 错误
                    if (e.message.includes("Cannot close a ERRORED writable stream")) {
                        console.warn(`[Xtoys 玩具多多] 关闭设备 ${deviceFriendlyName} 的可写流时出错 (预期的错误,流可能已中止): ${e.message}`);
                    } else {
                        // 对于其他非预期错误,仍然作为警告或错误处理
                        console.warn(`[Xtoys 玩具多多] 关闭设备 ${deviceFriendlyName} 的可写流时出错: ${e.message}`);
                    }
                }
            },
            setSignals: async () => {},
            getSignals: async () => ({}),
            forget: async () => {
                console.info(`[Xtoys 玩具多多] 设备 ${deviceFriendlyName} 的 forget 方法被调用。正在断开连接。`);
                await mockPort.close();
            }
        };

        console.info(`[Xtoys 玩具多多] 已为设备 ${deviceFriendlyName} 创建模拟 SerialPort 对象。`, mockPort);
        return mockPort;
    }

    // --- 最终覆盖 ---
    if (unsafeWindow.navigator?.serial) {
        unsafeWindow.navigator.serial.requestPort = mockRequestPort;
        // 覆盖 getPorts 以返回所有当前活跃的模拟端口
        unsafeWindow.navigator.serial.getPorts = async () => {
            return Array.from(activeConnections.values()).map(conn => conn.mockPortInstance).filter(Boolean);
        };
        console.info('[Xtoys 玩具多多] 已成功拦截 navigator.serial.requestPort 和 getPorts。');
    } else {
        // 如果 navigator.serial 不存在,则创建完整的模拟 API
        Object.defineProperty(unsafeWindow.navigator, 'serial', {
            value: {
                requestPort: mockRequestPort,
                getPorts: async () => {
                    return Array.from(activeConnections.values()).map(conn => conn.mockPortInstance).filter(Boolean);
                }
            },
            writable: true
        });
        console.warn('[Xtoys 玩具多多] 未找到 navigator.serial,已创建模拟 API。');
    }

    // --- 前端UI提示框相关变量和函数 ---
    const customDialog = {};

    // 创建并添加对话框UI到DOM
    function createDialogUI() {
        // 创建一个临时的div来解析HTML字符串
        const tempDiv = document.createElement('div');
        tempDiv.innerHTML = `
            <div id="more-xtoys-message-dialog" role="dialog" aria-modal="true" class="q-dialog fullscreen no-pointer-events q-dialog--modal hidden" style="--q-transition-duration: 300ms;">
                <div class="q-dialog__backdrop fixed-full" aria-hidden="true" tabindex="-1" style="--q-transition-duration: 300ms;"></div>
                <div class="q-dialog__inner flex no-pointer-events q-dialog__inner--minimized q-dialog__inner--standard fixed-full flex-center" tabindex="-1" style="--q-transition-duration: 300ms;">
                    <div class="q-card q-card--dark q-dark column no-wrap" style="max-width: 900px; min-width: 400px;">
                        <div class="q-toolbar row no-wrap items-center text-white bg-primary-7" role="toolbar">
                            <div id="more-xtoys-dialog-title" class="q-toolbar__title ellipsis"></div>
                            <button id="more-xtoys-dialog-close-btn" class="q-btn q-btn-item non-selectable no-outline q-btn--flat q-btn--rectangle q-btn--actionable q-focusable q-hoverable q-btn--dense" tabindex="0" type="button">
                                <span class="q-focus-helper"></span>
                                <span class="q-btn__content text-center col items-center q-anchor--skip justify-center row">
                                    <i class="q-icon fas fa-times" aria-hidden="true" role="img"> </i>
                                </span>
                            </button>
                        </div>
                        <div class="q-card__section q-card__section--vert scroll q-pa-md" style="max-height: 85vh;">
                            <p id="more-xtoys-dialog-message" class="text-white"></p>
                        </div>
                        <div class="q-card__actions justify-end q-card__actions--horiz row">
                            <button id="more-xtoys-dialog-confirm-btn" class="q-btn q-btn-item non-selectable no-outline q-btn--standard q-btn--rectangle bg-green text-white q-btn--actionable q-focusable q-hoverable" tabindex="0" type="button">
                                <span class="q-focus-helper"></span>
                                <span class="q-btn__content text-center col items-center q-anchor--skip justify-center row">
                                    <span class="block">确认</span>
                                </span>
                            </button>
                        </div>
                    </div>
                </div>
            </div>
        `;

        // 从临时div中获取根对话框元素
        customDialog.messageDialog = tempDiv.firstElementChild;
        document.body.appendChild(customDialog.messageDialog);

        // 重新获取所有内部元素的引用
        customDialog.backdrop = customDialog.messageDialog.querySelector('.q-dialog__backdrop');
        customDialog.titleElement = customDialog.messageDialog.querySelector('#more-xtoys-dialog-title');
        customDialog.messageElement = customDialog.messageDialog.querySelector('#more-xtoys-dialog-message');
        customDialog.closeButton = customDialog.messageDialog.querySelector('#more-xtoys-dialog-close-btn');
        customDialog.confirmButton = customDialog.messageDialog.querySelector('#more-xtoys-dialog-confirm-btn');

        // 重新绑定事件监听器
        customDialog.messageDialog.addEventListener('click', (event) => {
            // Check if the click occurred on the backdrop or the close/confirm buttons
            if (event.target === customDialog.backdrop ||
                event.target === customDialog.closeButton ||
                event.target === customDialog.confirmButton ||
                customDialog.closeButton.contains(event.target) ||
                customDialog.confirmButton.contains(event.target)) {
                customDialog.messageDialog.classList.add('hidden');
                customDialog.messageDialog.classList.add('no-pointer-events'); // 重新添加
            }
        });
    }

    // 显示用户消息的函数
    function showUserMessage(title, message) {
        if (!customDialog.messageDialog || !document.body.contains(customDialog.messageDialog)) {
            createDialogUI(); // 确保对话框存在并已添加到DOM
        }
        customDialog.titleElement.textContent = title;
        customDialog.messageElement.textContent = message;
        // 显示模态对话框
        customDialog.messageDialog.classList.remove('hidden');
        customDialog.messageDialog.classList.remove('no-pointer-events'); // 移除以允许交互
    }

    // 在DOM内容加载完毕后创建UI
    document.addEventListener('DOMContentLoaded', createDialogUI);

})();