feat(web): add peripheral card quick controls
This commit is contained in:
@@ -44,7 +44,7 @@
|
||||
<script type="module" src="/gesture-control.js?v=android-pad-9"></script>
|
||||
<script src="/pwa.js?v=android-pad-3" defer></script>
|
||||
<script src="/console-v2.js?v=local-first-79-cinema-status" defer></script>
|
||||
<script src="/self-extension.js?v=nfc-session-58" defer></script>
|
||||
<script src="/self-extension.js?v=peripheral-cards-59" defer></script>
|
||||
<script src="/assistant-chat.js?v=codex-assistant-39-video-media" defer></script>
|
||||
<script src="/energy-monitor.js?v=energy-monitor-5-performance" defer></script>
|
||||
<script src="/firmware-workbench-entry.js?v=3" defer></script>
|
||||
|
||||
@@ -2698,6 +2698,146 @@ form > .v2-extension-primary {
|
||||
font-size: 7px;
|
||||
}
|
||||
|
||||
.v2-extension-synced-device {
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
border: 1px solid rgba(107, 214, 203, 0.12);
|
||||
transition: border-color 160ms ease, background 160ms ease;
|
||||
}
|
||||
|
||||
.v2-extension-synced-device[data-expanded="true"] {
|
||||
border-color: rgba(107, 214, 203, 0.3);
|
||||
background: rgba(87, 191, 180, 0.075);
|
||||
}
|
||||
|
||||
.v2-extension-synced-device > header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 7px 8px;
|
||||
}
|
||||
|
||||
.v2-extension-device-toggle {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
display: grid;
|
||||
grid-template-columns: 28px minmax(0, 1fr) auto 14px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 0;
|
||||
color: inherit;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.v2-extension-device-toggle > span {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: #76cfc6;
|
||||
background: rgba(87, 191, 180, 0.1);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.v2-extension-device-toggle > span svg,
|
||||
.v2-extension-device-toggle > svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.v2-extension-device-toggle > div {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.v2-extension-device-toggle > div strong {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.v2-extension-device-toggle > b {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.v2-extension-device-toggle > svg {
|
||||
color: #8da7a5;
|
||||
transition: transform 160ms ease;
|
||||
}
|
||||
|
||||
.v2-extension-synced-device[data-expanded="true"] .v2-extension-device-toggle > svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.v2-extension-device-hide {
|
||||
width: 27px;
|
||||
height: 27px;
|
||||
flex: none;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 0;
|
||||
color: #8da7a5;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 7px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.v2-extension-device-hide:is(:hover, :focus-visible) {
|
||||
color: #eaa89c;
|
||||
background: rgba(216, 99, 82, 0.12);
|
||||
}
|
||||
|
||||
.v2-extension-device-hide svg {
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
}
|
||||
|
||||
.v2-extension-synced-device > .v2-extension-quick-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
margin: 0;
|
||||
padding: 0 8px 8px 44px;
|
||||
border-top: 1px solid rgba(107, 214, 203, 0.09);
|
||||
}
|
||||
|
||||
.v2-extension-quick-controls > button {
|
||||
min-height: 25px;
|
||||
padding: 4px 8px;
|
||||
color: #b9d8d4;
|
||||
background: rgba(95, 190, 180, 0.1);
|
||||
border: 1px solid rgba(107, 214, 203, 0.13);
|
||||
border-radius: 999px;
|
||||
font-size: 7px;
|
||||
font-weight: 800;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.v2-extension-quick-controls > button:is(:hover, :focus-visible) {
|
||||
color: #effaf8;
|
||||
background: rgba(95, 190, 180, 0.2);
|
||||
border-color: rgba(107, 214, 203, 0.3);
|
||||
}
|
||||
|
||||
.v2-extension-quick-controls > button:disabled {
|
||||
opacity: 0.55;
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
.v2-extension-quick-controls > button[data-state="success"] {
|
||||
color: #bdf2d5;
|
||||
background: rgba(61, 173, 112, 0.16);
|
||||
}
|
||||
|
||||
.v2-extension-quick-controls > button[data-state="error"] {
|
||||
color: #f0b4aa;
|
||||
background: rgba(216, 99, 82, 0.14);
|
||||
}
|
||||
|
||||
.v2-extension-sync-mount .v2-extension-empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -3547,6 +3687,37 @@ form > .v2-extension-primary {
|
||||
}
|
||||
}
|
||||
|
||||
.console-v2 #v2ExtensionPanel .v2-extension-device-toggle > span {
|
||||
color: var(--primary);
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
.console-v2 #v2ExtensionPanel .v2-extension-device-toggle > svg,
|
||||
.console-v2 #v2ExtensionPanel .v2-extension-device-hide {
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
.console-v2 #v2ExtensionPanel .v2-extension-synced-device[data-expanded="true"] {
|
||||
background: color-mix(in oklch, var(--accent) 64%, var(--card));
|
||||
border-color: color-mix(in oklch, var(--primary) 30%, var(--border));
|
||||
}
|
||||
|
||||
.console-v2 #v2ExtensionPanel .v2-extension-synced-device > .v2-extension-quick-controls {
|
||||
border-top-color: var(--border);
|
||||
}
|
||||
|
||||
.console-v2 #v2ExtensionPanel .v2-extension-quick-controls > button {
|
||||
color: var(--primary);
|
||||
background: var(--accent);
|
||||
border-color: color-mix(in oklch, var(--primary) 18%, var(--border));
|
||||
}
|
||||
|
||||
.console-v2 #v2ExtensionPanel .v2-extension-quick-controls > button:is(:hover, :focus-visible) {
|
||||
color: var(--primary-foreground);
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
/* Final semantic theme layer for extension surfaces and nested dialogs --- */
|
||||
.console-v2 #v2ExtensionPanel .v2-extension-sync-mount,
|
||||
.console-v2 #v2ExtensionPanel .v2-extension-gateway,
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
const SLOT_CATALOG_MIGRATION_KEY = "cognitive-agent.gscu-slot-catalog-migrated.v1";
|
||||
const MODE_KEY = "cognitive-agent.extension-mode";
|
||||
const CONVERSATION_KEY = "cognitive-agent.conversation-id";
|
||||
const PERIPHERAL_CARD_UI_KEY = "cognitive-agent.extension-peripheral-cards.v1";
|
||||
const SLOT_MIN = 1;
|
||||
const SLOT_MAX = 248;
|
||||
const ACTION_PAGE_SIZE = 6;
|
||||
@@ -375,10 +376,13 @@
|
||||
};
|
||||
let extensionToySnapshot = {
|
||||
revision: -1,
|
||||
session_id: null,
|
||||
state: "stopped",
|
||||
topic: "extension/toy",
|
||||
burst_published_count: 0,
|
||||
detected_nfc_ids: [],
|
||||
detection_count: 0,
|
||||
active_nfc_id: null,
|
||||
last_error: null,
|
||||
};
|
||||
let peripheralCatalogRevision = null;
|
||||
@@ -410,6 +414,10 @@
|
||||
let legacyMigrationCompleted = false;
|
||||
const legacyBridgeAttempts = new Set();
|
||||
let renderUnifiedExtensionView = null;
|
||||
const hiddenPeripheralDevices = new Map();
|
||||
let expandedPeripheralDeviceId = null;
|
||||
let peripheralCardSessionId = "";
|
||||
let runningPeripheralQuickAction = false;
|
||||
let latestVehicleStateTurn = -1;
|
||||
let sharedTwinBand = null;
|
||||
let sharedTwinHome = null;
|
||||
@@ -1494,6 +1502,59 @@
|
||||
return String(value || "").replace(/[^0-9a-f]/gi, "").toUpperCase();
|
||||
}
|
||||
|
||||
function persistPeripheralCardUi() {
|
||||
try {
|
||||
sessionStorage.setItem(PERIPHERAL_CARD_UI_KEY, JSON.stringify({
|
||||
sessionId: peripheralCardSessionId,
|
||||
hidden: [...hiddenPeripheralDevices].map(([deviceId, value]) => ({
|
||||
deviceId,
|
||||
cardUid: value.cardUid,
|
||||
detectionCount: value.detectionCount,
|
||||
})),
|
||||
}));
|
||||
} catch (_error) {
|
||||
// The in-memory state remains authoritative when storage is unavailable.
|
||||
}
|
||||
}
|
||||
|
||||
function restorePeripheralCardUi(sessionId) {
|
||||
hiddenPeripheralDevices.clear();
|
||||
expandedPeripheralDeviceId = null;
|
||||
peripheralCardSessionId = String(sessionId || "");
|
||||
try {
|
||||
const saved = JSON.parse(sessionStorage.getItem(PERIPHERAL_CARD_UI_KEY) || "null");
|
||||
if (!saved || saved.sessionId !== peripheralCardSessionId) {
|
||||
persistPeripheralCardUi();
|
||||
return;
|
||||
}
|
||||
(Array.isArray(saved.hidden) ? saved.hidden : []).forEach((item) => {
|
||||
const deviceId = String(item?.deviceId || "").trim();
|
||||
const cardUid = normalizeCardUid(item?.cardUid);
|
||||
const detectionCount = Number(item?.detectionCount ?? -1);
|
||||
if (deviceId && cardUid && Number.isInteger(detectionCount)) {
|
||||
hiddenPeripheralDevices.set(deviceId, { cardUid, detectionCount });
|
||||
}
|
||||
});
|
||||
} catch (_error) {
|
||||
persistPeripheralCardUi();
|
||||
}
|
||||
}
|
||||
|
||||
function revealRedetectedPeripheral(value) {
|
||||
const cardUid = normalizeCardUid(value?.active_nfc_id);
|
||||
const detectionCount = Number(value?.detection_count ?? -1);
|
||||
if (!cardUid || !Number.isInteger(detectionCount)) return false;
|
||||
let changed = false;
|
||||
hiddenPeripheralDevices.forEach((hidden, deviceId) => {
|
||||
if (hidden.cardUid === cardUid && hidden.detectionCount !== detectionCount) {
|
||||
hiddenPeripheralDevices.delete(deviceId);
|
||||
changed = true;
|
||||
}
|
||||
});
|
||||
if (changed) persistPeripheralCardUi();
|
||||
return changed;
|
||||
}
|
||||
|
||||
function renderMetrics() {
|
||||
$("#extensionHardwareCount").textContent = String(state.devices.length);
|
||||
$("#extensionCapabilityCount").textContent = String(learnedCapabilities().length);
|
||||
@@ -4007,9 +4068,16 @@
|
||||
function applyExtensionToySnapshot(value, { announce = true } = {}) {
|
||||
if (!value || typeof value !== "object") return false;
|
||||
const revision = Number(value.revision ?? -1);
|
||||
if (revision === extensionToySnapshot.revision) return false;
|
||||
const sessionId = String(value.session_id || "");
|
||||
const sessionChanged = sessionId !== peripheralCardSessionId;
|
||||
if (sessionChanged) restorePeripheralCardUi(sessionId);
|
||||
const cardRevealed = revealRedetectedPeripheral(value);
|
||||
if (revision === extensionToySnapshot.revision
|
||||
&& sessionId === String(extensionToySnapshot.session_id || "")
|
||||
&& !cardRevealed) return false;
|
||||
extensionToySnapshot = {
|
||||
revision,
|
||||
session_id: sessionId || null,
|
||||
state: String(value.state || "stopped"),
|
||||
topic: String(value.topic || "extension/toy"),
|
||||
burst_published_count: Number(value.burst_published_count || 0),
|
||||
@@ -4019,9 +4087,12 @@
|
||||
detected_nfc_ids: Array.isArray(value.detected_nfc_ids)
|
||||
? value.detected_nfc_ids.map(normalizeCardUid).filter(Boolean)
|
||||
: [],
|
||||
detection_count: Number(value.detection_count || 0),
|
||||
active_nfc_id: normalizeCardUid(value.active_nfc_id) || null,
|
||||
last_delivery_status: value.last_delivery_status || null,
|
||||
last_error: value.last_error || null,
|
||||
};
|
||||
if (sessionChanged || cardRevealed) unifiedExtensionDirty = true;
|
||||
if (announce && extensionToySnapshot.state === "burst_complete") {
|
||||
showToast(
|
||||
`已向 ${extensionToySnapshot.topic} 发送 ${extensionToySnapshot.burst_published_count} 帧设备能力消息。`,
|
||||
@@ -4119,8 +4190,107 @@
|
||||
void refreshNfcStatus();
|
||||
}
|
||||
|
||||
function hidePeripheralCard(deviceId, cardUid) {
|
||||
const normalizedDeviceId = String(deviceId || "").trim();
|
||||
const normalizedCardUid = normalizeCardUid(cardUid);
|
||||
if (!normalizedDeviceId || !normalizedCardUid) return;
|
||||
hiddenPeripheralDevices.set(normalizedDeviceId, {
|
||||
cardUid: normalizedCardUid,
|
||||
detectionCount: Number(extensionToySnapshot.detection_count || 0),
|
||||
});
|
||||
if (expandedPeripheralDeviceId === normalizedDeviceId) {
|
||||
expandedPeripheralDeviceId = null;
|
||||
}
|
||||
persistPeripheralCardUi();
|
||||
unifiedExtensionDirty = true;
|
||||
renderUnifiedExtensionView?.({ force: true });
|
||||
showToast("外设卡片已隐藏;再次感应对应 NFC 后将重新显示。", "success");
|
||||
}
|
||||
|
||||
function togglePeripheralCard(deviceId) {
|
||||
const normalizedDeviceId = String(deviceId || "").trim();
|
||||
if (!normalizedDeviceId) return;
|
||||
expandedPeripheralDeviceId = expandedPeripheralDeviceId === normalizedDeviceId
|
||||
? null
|
||||
: normalizedDeviceId;
|
||||
unifiedExtensionDirty = true;
|
||||
renderUnifiedExtensionView?.({ force: true });
|
||||
}
|
||||
|
||||
async function runPeripheralQuickAction(button) {
|
||||
const capabilityId = String(button?.dataset.runPeripheralQuickAction || "").trim();
|
||||
if (!capabilityId || button.disabled) return;
|
||||
if (runningPeripheralQuickAction) {
|
||||
showToast("上一条外设快捷指令仍在处理,请稍候。", "error");
|
||||
return;
|
||||
}
|
||||
const capability = durablePeripheralCatalog.find(
|
||||
(item) => String(item.id || "") === capabilityId,
|
||||
);
|
||||
const deviceName = String(
|
||||
capability?.device_name || capability?.device_id || "外设",
|
||||
);
|
||||
const actionName = String(capability?.action || capability?.name || "动作");
|
||||
if ((button.dataset.risk || capability?.risk) === "high" && !window.confirm(
|
||||
`“${deviceName} · ${actionName}”可能让外设移动,请确认周围安全。`,
|
||||
)) return;
|
||||
const previousText = button.textContent;
|
||||
runningPeripheralQuickAction = true;
|
||||
button.disabled = true;
|
||||
button.dataset.state = "running";
|
||||
button.textContent = "下发中…";
|
||||
try {
|
||||
await api("peripherals/execute", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
capability_id: capabilityId,
|
||||
conversation_id: sharedConversationId(),
|
||||
}),
|
||||
});
|
||||
button.dataset.state = "success";
|
||||
button.textContent = "已下发";
|
||||
showToast(`${deviceName} · ${actionName} 已下发。`, "success");
|
||||
window.setTimeout(() => {
|
||||
if (!button.isConnected) return;
|
||||
button.dataset.state = "";
|
||||
button.textContent = previousText;
|
||||
button.disabled = false;
|
||||
}, 1200);
|
||||
} catch (error) {
|
||||
button.dataset.state = "error";
|
||||
button.textContent = "下发失败";
|
||||
showToast(error.message, "error");
|
||||
window.setTimeout(() => {
|
||||
if (!button.isConnected) return;
|
||||
button.dataset.state = "";
|
||||
button.textContent = previousText;
|
||||
button.disabled = false;
|
||||
}, 1600);
|
||||
} finally {
|
||||
runningPeripheralQuickAction = false;
|
||||
}
|
||||
}
|
||||
|
||||
function bindUnifiedNfcEvents(mount) {
|
||||
mount.addEventListener("click", (event) => {
|
||||
const quickAction = event.target.closest("[data-run-peripheral-quick-action]");
|
||||
if (quickAction) {
|
||||
void runPeripheralQuickAction(quickAction);
|
||||
return;
|
||||
}
|
||||
const hideCard = event.target.closest("[data-hide-peripheral-card]");
|
||||
if (hideCard) {
|
||||
hidePeripheralCard(
|
||||
hideCard.dataset.hidePeripheralCard,
|
||||
hideCard.dataset.cardUid,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const toggleCard = event.target.closest("[data-toggle-peripheral-card]");
|
||||
if (toggleCard) {
|
||||
togglePeripheralCard(toggleCard.dataset.togglePeripheralCard);
|
||||
return;
|
||||
}
|
||||
if (event.target.closest("[data-open-pet-settings]")) openPetSettingsDialog();
|
||||
else if (event.target.closest("[data-open-nfc-binding]")) openNfcDialog();
|
||||
else if (event.target.closest("[data-open-gscu-tcp]")) openGscuTcpDialog();
|
||||
@@ -4237,6 +4407,11 @@
|
||||
card: nfcSnapshot.card,
|
||||
},
|
||||
detectedExtensionNfcIds: extensionToySnapshot.detected_nfc_ids,
|
||||
peripheralCardUi: {
|
||||
sessionId: peripheralCardSessionId,
|
||||
hiddenDeviceIds: [...hiddenPeripheralDevices.keys()].sort(),
|
||||
expandedDeviceId: expandedPeripheralDeviceId,
|
||||
},
|
||||
pets: petCardSnapshot,
|
||||
localDevices: state.devices.map((device) => ({
|
||||
id: device.id,
|
||||
@@ -4335,14 +4510,23 @@
|
||||
actions: [],
|
||||
});
|
||||
const actionName = item.action || item.name || "已学习能力";
|
||||
grouped.get(deviceId).actions.push(
|
||||
item.enabled === false ? `${actionName}(已停用)` : actionName,
|
||||
);
|
||||
grouped.get(deviceId).actions.push({
|
||||
id: String(item.id || ""),
|
||||
name: actionName,
|
||||
enabled: item.enabled !== false,
|
||||
risk: item.risk || "low",
|
||||
slot: item.slot,
|
||||
});
|
||||
});
|
||||
const detectedNfcIds = new Set(extensionToySnapshot.detected_nfc_ids);
|
||||
const devices = [...grouped.values()].filter(
|
||||
(device) => detectedNfcIds.has(normalizeCardUid(device.cardUid)),
|
||||
(device) => detectedNfcIds.has(normalizeCardUid(device.cardUid))
|
||||
&& !hiddenPeripheralDevices.has(device.id),
|
||||
);
|
||||
if (expandedPeripheralDeviceId
|
||||
&& !devices.some((device) => device.id === expandedPeripheralDeviceId)) {
|
||||
expandedPeripheralDeviceId = null;
|
||||
}
|
||||
const lastAction = snapshot?.extension?.last_action;
|
||||
const lastResult = snapshot?.extension?.result;
|
||||
const nfcCapabilities = nfcDevice
|
||||
@@ -4419,11 +4603,24 @@
|
||||
</div>
|
||||
<div class="v2-extension-device-list">
|
||||
${nfcPet ? renderDetectedPetCard(nfcPet) : ""}
|
||||
${devices.length ? devices.map((device) => `
|
||||
<article class="v2-extension-synced-device">
|
||||
<header><span><i data-lucide="${device.cardUid ? "badge-check" : "cpu"}"></i></span><div><strong title="${escapeHtml(device.type || device.name)}">${escapeHtml(device.name)}</strong></div><b>${device.actions.length} 项能力</b></header>
|
||||
<div>${device.actions.slice(0, 8).map((action) => `<span>${escapeHtml(action)}</span>`).join("")}</div>
|
||||
</article>`).join("") : nfcPet ? "" : `
|
||||
${devices.length ? devices.map((device) => {
|
||||
const expanded = expandedPeripheralDeviceId === device.id;
|
||||
return `
|
||||
<article class="v2-extension-synced-device" data-expanded="${String(expanded)}" data-extension-synced-device="${escapeHtml(device.id)}">
|
||||
<header>
|
||||
<button class="v2-extension-device-toggle" type="button" data-toggle-peripheral-card="${escapeHtml(device.id)}" aria-expanded="${String(expanded)}">
|
||||
<span><i data-lucide="${device.cardUid ? "badge-check" : "cpu"}"></i></span>
|
||||
<div><strong title="${escapeHtml(device.type || device.name)}">${escapeHtml(device.name)}</strong></div>
|
||||
<b>${device.actions.length} 项能力</b>
|
||||
<i data-lucide="chevron-down"></i>
|
||||
</button>
|
||||
<button class="v2-extension-device-hide" type="button" data-hide-peripheral-card="${escapeHtml(device.id)}" data-card-uid="${escapeHtml(device.cardUid)}" title="隐藏${escapeHtml(device.name)}" aria-label="隐藏${escapeHtml(device.name)}"><i data-lucide="eye-off"></i></button>
|
||||
</header>
|
||||
${expanded ? `<div class="v2-extension-quick-controls" aria-label="${escapeHtml(device.name)}快捷控制">
|
||||
${device.actions.map((action) => `<button type="button" data-run-peripheral-quick-action="${escapeHtml(action.id)}" data-risk="${escapeHtml(action.risk)}" ${action.enabled && action.id ? "" : "disabled"}>${escapeHtml(action.name)}${action.enabled ? "" : "(已停用)"}</button>`).join("")}
|
||||
</div>` : ""}
|
||||
</article>`;
|
||||
}).join("") : nfcPet ? "" : `
|
||||
<div class="v2-extension-empty">
|
||||
<i data-lucide="scan-line"></i><div><strong>等待 NFC 外设靠近</strong><span>检测到已绑定的 NFC 后,外设将在本次运行期间持续显示</span></div>
|
||||
</div>`}
|
||||
|
||||
@@ -11,8 +11,8 @@ const APP_SHELL = [
|
||||
"/pwa.js?v=android-pad-3",
|
||||
"/console-v2.css?v=delivery-state-69",
|
||||
"/console-v2.js?v=local-first-79-cinema-status",
|
||||
"/self-extension.css?v=pet-settings-54-dialog-size",
|
||||
"/self-extension.js?v=nfc-session-58",
|
||||
"/self-extension.css?v=peripheral-cards-55",
|
||||
"/self-extension.js?v=peripheral-cards-59",
|
||||
"/speech-input.css?v=sensevoice-7",
|
||||
"/speech-input.js?v=sensevoice-13",
|
||||
"/speech-audio-worklet.js?v=sensevoice-5",
|
||||
|
||||
Reference in New Issue
Block a user