mirror of
https://github.com/MengMengCode/VoCat.git
synced 2026-08-18 13:53:42 +08:00
Enforce hard limits and hide unavailable task paths
This commit is contained in:
@@ -27,9 +27,10 @@ export function DeviceOverviewTab(props: DeviceOverviewTabProps) {
|
||||
const [operatorOpen, setOperatorOpen] = useState(false);
|
||||
const { device } = props;
|
||||
const wifiCallingOnly = device.deviceType === "usb_sim_reader";
|
||||
const showNetworkDetails = !!device.developerEnabled && !wifiCallingOnly;
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className={`grid grid-cols-1 gap-4 ${wifiCallingOnly ? "lg:grid-cols-2" : "lg:grid-cols-3"}`}>
|
||||
<div className={`grid grid-cols-1 gap-4 ${showNetworkDetails ? "lg:grid-cols-3" : "lg:grid-cols-2"}`}>
|
||||
<div className="ui-panel-muted p-4">
|
||||
<div className="mb-3 text-xs font-bold uppercase tracking-wider text-gray-500">{t("运行状态")}</div>
|
||||
{isVoWiFiInUse(device) && !(device.modem?.imei && device.modem?.simInserted === false) ? (
|
||||
@@ -45,7 +46,7 @@ export function DeviceOverviewTab(props: DeviceOverviewTabProps) {
|
||||
e911Starting={props.e911Starting}
|
||||
onSetupE911={props.onSetupE911}
|
||||
/>
|
||||
{!wifiCallingOnly ? <OverviewNetworkPanel
|
||||
{showNetworkDetails ? <OverviewNetworkPanel
|
||||
device={device}
|
||||
trafficMinuteRx={props.trafficMinuteRx}
|
||||
trafficMinuteTx={props.trafficMinuteTx}
|
||||
|
||||
@@ -77,11 +77,7 @@ export function OverviewNetworkPanel({ device, trafficMinuteRx, trafficMinuteTx,
|
||||
: "";
|
||||
|
||||
if (!developerActive) {
|
||||
return (
|
||||
<div className="ui-panel-muted p-4">
|
||||
<div className="text-xs font-bold uppercase tracking-wider text-gray-500">{t("网络")}</div>
|
||||
</div>
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -38,7 +38,7 @@ export function DeviceQuotaCard({
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={value?.maxDeviceLimit ?? 128}
|
||||
max={value?.maxDeviceLimit ?? 10}
|
||||
value={Number.isFinite(limit) ? limit : ""}
|
||||
disabled={loading || saving}
|
||||
onChange={(event) => onLimitChange(Number(event.target.value))}
|
||||
|
||||
@@ -38,7 +38,7 @@ export function SMSRateLimitCard({
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={value?.maxSmsHourlyLimit ?? 1000}
|
||||
max={value?.maxSmsHourlyLimit ?? 20}
|
||||
value={Number.isFinite(limit) ? limit : ""}
|
||||
disabled={loading || saving}
|
||||
onChange={(event) => onLimitChange(Number(event.target.value))}
|
||||
@@ -46,8 +46,8 @@ export function SMSRateLimitCard({
|
||||
/>
|
||||
<p className="text-xs leading-5 text-gray-500 dark:text-gray-400">
|
||||
{zh
|
||||
? `采用滚动一小时窗口,网页、TG Bot、自动任务、API、VoWiFi 与基站发送全部计入;接收短信不受限制。关闭开发者模式后恢复为 ${value?.defaultSmsHourlyLimit ?? 10} 条/小时。`
|
||||
: `Uses a rolling one-hour window across the web UI, Telegram bot, automatic tasks, API, VoWiFi, and cellular sending. Receiving is unlimited. Disabling developer mode restores ${value?.defaultSmsHourlyLimit ?? 10} messages/hour.`}
|
||||
? "采用滚动一小时窗口,网页、TG Bot、自动任务、API、VoWiFi 与基站发送全部计入;接收短信不受限制。"
|
||||
: "Uses a rolling one-hour window across the web UI, Telegram bot, automatic tasks, API, VoWiFi, and cellular sending. Receiving is unlimited."}
|
||||
</p>
|
||||
<Button variant="primary" loading={saving} disabled={loading} onClick={onSave} className="w-full !border-0">
|
||||
{zh ? "保存短信速率限制" : "Save SMS rate limit"}
|
||||
|
||||
@@ -157,6 +157,7 @@ export const EN_DICT: Record<string, string> = {
|
||||
短信检测: "SMS Test",
|
||||
自动任务: "Automatic Tasks",
|
||||
"按周期切换指定 eSIM Profile,并在设备串行队列中执行短信、通话或漫游公网 IP 任务": "Switch to a selected eSIM profile on schedule, then run SMS, call, or roaming public-IP jobs in a per-device queue",
|
||||
"按周期切换指定 eSIM Profile,并在设备串行队列中执行短信或通话任务": "Switch to a selected eSIM profile on schedule, then run SMS or call jobs in a per-device queue",
|
||||
添加任务: "Add Task",
|
||||
"设备 / Profile": "Device / Profile",
|
||||
执行环境: "Environment",
|
||||
@@ -193,7 +194,7 @@ export const EN_DICT: Record<string, string> = {
|
||||
任务类型: "Task Type",
|
||||
开启漫游流量并获取一次公网IP: "Enable roaming data and get the public IP once",
|
||||
"基站直连(自动选网)": "Cellular (automatic network selection)",
|
||||
"该任务固定使用基站直连和自动选网;执行时会开启漫游数据,并通过模块接口访问 ipinfo.io。需要开启开发者模式。": "This task always uses cellular direct mode with automatic network selection. It enables roaming data and accesses ipinfo.io through the modem interface. Developer mode is required.",
|
||||
"该任务固定使用基站直连和自动选网;执行时会开启漫游数据,并通过模块接口访问 ipinfo.io。": "This task always uses cellular direct mode with automatic network selection. It enables roaming data and accesses ipinfo.io through the modem interface.",
|
||||
首次执行日期: "First Run Date",
|
||||
执行时间: "Run Time",
|
||||
执行周期: "Interval",
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
SendClockRegular,
|
||||
} from "@fluentui/react-icons";
|
||||
import { api, apiMessage } from "../api";
|
||||
import type { DeviceListItem, DevicesResponse } from "../types";
|
||||
import type { DeviceListItem, DevicesResponse, SystemInfo } from "../types";
|
||||
import type { EsimProfileGroup } from "../components/devices/types";
|
||||
import {
|
||||
Button,
|
||||
@@ -140,6 +140,7 @@ export default function AutomaticTasksPage() {
|
||||
const [runsPage, setRunsPage] = useState(1);
|
||||
const [runsPageSize, setRunsPageSize] = useState(20);
|
||||
const [devices, setDevices] = useState<DeviceListItem[]>([]);
|
||||
const [advancedTasksAvailable, setAdvancedTasksAvailable] = useState(false);
|
||||
const [profiles, setProfiles] = useState<ProfileOption[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [profileLoading, setProfileLoading] = useState(false);
|
||||
@@ -155,12 +156,14 @@ export default function AutomaticTasksPage() {
|
||||
const load = useCallback(async (initial = false) => {
|
||||
if (initial) setLoading(true);
|
||||
try {
|
||||
const [taskData, deviceData] = await Promise.all([
|
||||
const [taskData, deviceData, systemInfo] = await Promise.all([
|
||||
api<{ tasks?: AutomaticTask[] }>("/automatic-tasks"),
|
||||
api<DevicesResponse>("/devices"),
|
||||
api<SystemInfo>("/system/info"),
|
||||
]);
|
||||
setTasks(taskData.tasks || []);
|
||||
setDevices(deviceData.devices || []);
|
||||
setAdvancedTasksAvailable(!!systemInfo.developer);
|
||||
} catch (error) {
|
||||
message.error(apiMessage(error));
|
||||
} finally {
|
||||
@@ -272,6 +275,9 @@ export default function AutomaticTasksPage() {
|
||||
if (devices.find((device) => device.id === deviceId)?.deviceType === "usb_sim_reader") {
|
||||
next = { ...next, taskType: next.taskType === "public_ip" ? "sms" : next.taskType, environment: "vowifi" };
|
||||
}
|
||||
if (!advancedTasksAvailable && (next.taskType === "public_ip" || next.environment === "cellular")) {
|
||||
next = { ...next, taskType: "sms", environment: "vowifi" };
|
||||
}
|
||||
setForm(next);
|
||||
setOpen(true);
|
||||
void loadProfiles(deviceId, next.profileIccid);
|
||||
@@ -282,7 +288,7 @@ export default function AutomaticTasksPage() {
|
||||
setForm((current) => ({
|
||||
...current, deviceId, profileIccid: "", profileAid: "",
|
||||
taskType: reader && current.taskType === "public_ip" ? "sms" : current.taskType,
|
||||
environment: reader ? "vowifi" : current.environment,
|
||||
environment: reader || !advancedTasksAvailable ? "vowifi" : current.environment,
|
||||
}));
|
||||
void loadProfiles(deviceId);
|
||||
}
|
||||
@@ -293,7 +299,7 @@ export default function AutomaticTasksPage() {
|
||||
}
|
||||
|
||||
function chooseTaskType(taskType: TaskType) {
|
||||
if (deviceByID.get(form.deviceId)?.deviceType === "usb_sim_reader" && taskType === "public_ip") return;
|
||||
if ((!advancedTasksAvailable || deviceByID.get(form.deviceId)?.deviceType === "usb_sim_reader") && taskType === "public_ip") return;
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
taskType,
|
||||
@@ -308,6 +314,7 @@ export default function AutomaticTasksPage() {
|
||||
if (deviceByID.get(form.deviceId)?.deviceType === "usb_sim_reader" && (form.environment !== "vowifi" || form.taskType === "public_ip")) {
|
||||
return message.warning(t("USB SIM读卡器仅支持VoWiFi短信和通话任务"));
|
||||
}
|
||||
if (!advancedTasksAvailable && (form.environment !== "vowifi" || form.taskType === "public_ip")) return;
|
||||
if (form.taskType !== "public_ip" && !form.phone.trim()) return message.warning(t("请输入号码"));
|
||||
if (form.taskType === "sms" && !form.message.trim()) return message.warning(t("请输入短信内容"));
|
||||
setSaving(true);
|
||||
@@ -393,9 +400,9 @@ export default function AutomaticTasksPage() {
|
||||
const taskTypeOptions = [
|
||||
{ value: "sms", label: t("发送短信") },
|
||||
{ value: "call", label: t("拨打电话并自动挂断") },
|
||||
...(!selectedTaskDeviceIsReader ? [{ value: "public_ip", label: t("开启漫游流量并获取一次公网 IP") }] : []),
|
||||
...(advancedTasksAvailable && !selectedTaskDeviceIsReader ? [{ value: "public_ip", label: t("开启漫游流量并获取一次公网 IP") }] : []),
|
||||
];
|
||||
const environmentOptions = selectedTaskDeviceIsReader
|
||||
const environmentOptions = selectedTaskDeviceIsReader || !advancedTasksAvailable
|
||||
? [{ value: "vowifi", label: "VoWiFi" }]
|
||||
: [{ value: "vowifi", label: "VoWiFi" }, { value: "cellular", label: t("基站直连(自动选网)") }];
|
||||
|
||||
@@ -403,7 +410,9 @@ export default function AutomaticTasksPage() {
|
||||
<div className="mx-auto max-w-7xl">
|
||||
<PageHeader
|
||||
title={t("自动任务")}
|
||||
subtitle={t("按周期切换指定 eSIM Profile,并在设备串行队列中执行短信、通话或漫游公网 IP 任务")}
|
||||
subtitle={advancedTasksAvailable
|
||||
? t("按周期切换指定 eSIM Profile,并在设备串行队列中执行短信、通话或漫游公网 IP 任务")
|
||||
: t("按周期切换指定 eSIM Profile,并在设备串行队列中执行短信或通话任务")}
|
||||
actions={<Button variant="primary" icon={<AddRegular />} onClick={() => edit()} disabled={!devices.length}>{t("添加任务")}</Button>}
|
||||
/>
|
||||
|
||||
@@ -475,8 +484,8 @@ export default function AutomaticTasksPage() {
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{!runs.length ? <div className="p-8 text-center text-sm text-gray-400">{t("暂无执行记录")}</div> : null}
|
||||
{runsTotal > 0 ? (
|
||||
{!runs.length ? <div className="p-8 text-center text-sm text-gray-400">{t("暂无执行记录")}</div> : null}
|
||||
{runsTotal > 0 ? (
|
||||
<div className="border-t border-gray-100 px-5 py-3 dark:border-white/10">
|
||||
<Pagination
|
||||
page={runsPage}
|
||||
@@ -501,7 +510,7 @@ export default function AutomaticTasksPage() {
|
||||
{form.taskType !== "public_ip" ? <div><label className={fieldLabel}>{t("号码")}</label><Input value={form.phone} onChange={(event) => setForm({ ...form, phone: event.target.value })} placeholder="+447700900123" /></div> : null}
|
||||
{form.taskType === "call" ? <div><label className={fieldLabel}>{t("自动挂断")}</label><Input type="number" min={1} max={600} value={form.durationSeconds} suffix="s" onChange={(event) => setForm({ ...form, durationSeconds: Number(event.target.value) })} /></div> : null}
|
||||
{form.taskType === "sms" ? <div className="md:col-span-2"><label className={fieldLabel}>{t("短信内容")}</label><Textarea rows={4} value={form.message} onChange={(event) => setForm({ ...form, message: event.target.value })} /></div> : null}
|
||||
{form.taskType === "public_ip" ? <div className="md:col-span-2 rounded-lg border border-amber-200 bg-amber-50 p-3 text-sm text-amber-700 dark:border-amber-500/20 dark:bg-amber-500/10 dark:text-amber-300">{t("该任务固定使用基站直连和自动选网;执行时会开启漫游数据,并通过模块接口访问 ipinfo.io。需要开启开发者模式。")}</div> : null}
|
||||
{advancedTasksAvailable && form.taskType === "public_ip" ? <div className="md:col-span-2 rounded-lg border border-amber-200 bg-amber-50 p-3 text-sm text-amber-700 dark:border-amber-500/20 dark:bg-amber-500/10 dark:text-amber-300">{t("该任务固定使用基站直连和自动选网;执行时会开启漫游数据,并通过模块接口访问 ipinfo.io。")}</div> : null}
|
||||
|
||||
<div><label className={fieldLabel}>{t("首次执行日期")}</label><Input type="date" value={form.startDate} onChange={(event) => setForm({ ...form, startDate: event.target.value })} /></div>
|
||||
<div><label className={fieldLabel}>{t("执行时间")}</label><Input type="time" value={form.runTime} onChange={(event) => setForm({ ...form, runTime: event.target.value })} /></div>
|
||||
|
||||
@@ -177,7 +177,7 @@ export default function SettingsPage() {
|
||||
}, [lang]);
|
||||
|
||||
const onSaveDeviceLimit = useCallback(async () => {
|
||||
const maximum = developerSettings?.maxDeviceLimit ?? 128;
|
||||
const maximum = developerSettings?.maxDeviceLimit ?? 10;
|
||||
if (!Number.isInteger(deviceLimit) || deviceLimit < 1 || deviceLimit > maximum) {
|
||||
message.error(lang === "zh" ? `设备配额必须是 1 到 ${maximum} 的整数` : `Device quota must be an integer between 1 and ${maximum}`);
|
||||
return;
|
||||
@@ -196,7 +196,7 @@ export default function SettingsPage() {
|
||||
}, [developerSettings, deviceLimit, lang]);
|
||||
|
||||
const onSaveSMSHourlyLimit = useCallback(async () => {
|
||||
const maximum = developerSettings?.maxSmsHourlyLimit ?? 1000;
|
||||
const maximum = developerSettings?.maxSmsHourlyLimit ?? 20;
|
||||
if (!Number.isInteger(smsHourlyLimit) || smsHourlyLimit < 1 || smsHourlyLimit > maximum) {
|
||||
message.error(lang === "zh" ? `短信发送限制必须是 1 到 ${maximum} 的整数` : `SMS limit must be an integer between 1 and ${maximum}`);
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user