机器人房

This commit is contained in:
ZuoZuo 2026-06-15 23:53:38 +08:00
parent e25258f233
commit 7c4bdf8d77
13 changed files with 1006 additions and 0 deletions

View File

@ -3138,6 +3138,84 @@
"x-permissions": ["room-pin:cancel"]
}
},
"/admin/rooms/robot-rooms": {
"get": {
"operationId": "listRobotRooms",
"responses": {
"200": {
"$ref": "#/components/responses/EmptyResponse"
}
},
"x-permission": "room-robot:view",
"x-permissions": ["room-robot:view"]
},
"post": {
"operationId": "createRobotRoom",
"responses": {
"200": {
"$ref": "#/components/responses/EmptyResponse"
}
},
"x-permission": "room-robot:create",
"x-permissions": ["room-robot:create"]
}
},
"/admin/rooms/robot-rooms/available-robots": {
"get": {
"operationId": "listAvailableRoomRobots",
"responses": {
"200": {
"$ref": "#/components/responses/EmptyResponse"
}
},
"x-permission": "room-robot:view",
"x-permissions": ["room-robot:view"]
}
},
"/admin/rooms/robot-rooms/{room_id}/start": {
"post": {
"operationId": "startRobotRoom",
"responses": {
"200": {
"$ref": "#/components/responses/EmptyResponse"
}
},
"parameters": [
{
"in": "path",
"name": "room_id",
"required": true,
"schema": {
"type": "string"
}
}
],
"x-permission": "room-robot:update",
"x-permissions": ["room-robot:update"]
}
},
"/admin/rooms/robot-rooms/{room_id}/stop": {
"post": {
"operationId": "stopRobotRoom",
"responses": {
"200": {
"$ref": "#/components/responses/EmptyResponse"
}
},
"parameters": [
{
"in": "path",
"name": "room_id",
"required": true,
"schema": {
"type": "string"
}
}
],
"x-permission": "room-robot:update",
"x-permissions": ["room-robot:update"]
}
},
"/admin/users/level-config": {
"get": {
"operationId": "listLevelConfig",

View File

@ -141,6 +141,7 @@ export const fallbackNavigation = [
routeNavItem("room-list", { icon: BedroomParentOutlined }),
routeNavItem("room-pins", { icon: PushPinOutlined }),
routeNavItem("room-config", { icon: SettingsApplicationsOutlined }),
routeNavItem("room-robots", { icon: GroupsOutlined }),
],
},
{

View File

@ -97,6 +97,9 @@ export const PERMISSIONS = {
roomPinCancel: "room-pin:cancel",
roomConfigView: "room-config:view",
roomConfigUpdate: "room-config:update",
roomRobotView: "room-robot:view",
roomRobotCreate: "room-robot:create",
roomRobotUpdate: "room-robot:update",
appConfigView: "app-config:view",
appConfigUpdate: "app-config:update",
appVersionView: "app-version:view",
@ -183,6 +186,7 @@ export const MENU_CODES = {
roomList: "room-list",
roomPins: "room-pins",
roomConfig: "room-config",
roomRobots: "room-robots",
appConfig: "app-config",
appConfigH5: "app-config-h5",
appConfigBanners: "app-config-banners",

View File

@ -9,6 +9,9 @@ import type {
RoomDto,
RoomPinDto,
RoomPinPayload,
AvailableRoomRobotDto,
RobotRoomDto,
RobotRoomPayload,
RoomUpdatePayload,
} from "@/shared/api/types";
@ -72,3 +75,40 @@ export function updateRoomConfig(payload: RoomConfigPayload): Promise<RoomConfig
method: endpoint.method,
});
}
export function listRobotRooms(query: PageQuery = {}): Promise<ApiPage<RobotRoomDto>> {
const endpoint = API_ENDPOINTS.listRobotRooms;
return apiRequest<ApiPage<RobotRoomDto>>(apiEndpointPath(API_OPERATIONS.listRobotRooms), {
method: endpoint.method,
query,
});
}
export function listAvailableRoomRobots(): Promise<AvailableRoomRobotDto[]> {
const endpoint = API_ENDPOINTS.listAvailableRoomRobots;
return apiRequest<AvailableRoomRobotDto[]>(apiEndpointPath(API_OPERATIONS.listAvailableRoomRobots), {
method: endpoint.method,
});
}
export function createRobotRoom(payload: RobotRoomPayload): Promise<RobotRoomDto> {
const endpoint = API_ENDPOINTS.createRobotRoom;
return apiRequest<RobotRoomDto, RobotRoomPayload>(apiEndpointPath(API_OPERATIONS.createRobotRoom), {
body: payload,
method: endpoint.method,
});
}
export function startRobotRoom(roomId: EntityId): Promise<RobotRoomDto> {
const endpoint = API_ENDPOINTS.startRobotRoom;
return apiRequest<RobotRoomDto>(apiEndpointPath(API_OPERATIONS.startRobotRoom, { room_id: roomId }), {
method: endpoint.method,
});
}
export function stopRobotRoom(roomId: EntityId): Promise<RobotRoomDto> {
const endpoint = API_ENDPOINTS.stopRobotRoom;
return apiRequest<RobotRoomDto>(apiEndpointPath(API_OPERATIONS.stopRobotRoom, { room_id: roomId }), {
method: endpoint.method,
});
}

View File

@ -0,0 +1,242 @@
import { useEffect, useMemo, useState } from "react";
import { listGifts } from "@/features/resources/api";
import {
createRobotRoom,
listAvailableRoomRobots,
listRobotRooms,
startRobotRoom,
stopRobotRoom,
} from "@/features/rooms/api";
import { useRoomAbilities } from "@/features/rooms/permissions.js";
import { robotRoomCreateSchema } from "@/features/rooms/schema";
import { parseForm } from "@/shared/forms/validation";
import { usePaginatedQuery } from "@/shared/hooks/usePaginatedQuery.js";
import { useToast } from "@/shared/ui/ToastProvider.jsx";
const pageSize = 50;
const emptyData = { items: [], page: 1, pageSize, total: 0 };
function emptyForm() {
return {
candidateRobotUserIds: [],
giftIds: [],
luckyComboMax: "10000",
luckyComboMin: "100",
luckyGiftIds: [],
luckyPauseMaxMs: "20000",
luckyPauseMinMs: "5000",
maxRobotCount: "8",
minRobotCount: "6",
normalGiftIntervalMs: "10000",
ownerRobotUserId: "",
roomName: "",
visibleRegionId: 0,
};
}
export function useRobotRoomsPage() {
const abilities = useRoomAbilities();
const { showToast } = useToast();
const [activeAction, setActiveAction] = useState("");
const [availableRobotsState, setAvailableRobotsState] = useState({ error: "", loading: false, options: [] });
const [candidatesTouched, setCandidatesTouched] = useState(false);
const [form, setForm] = useState(emptyForm);
const [giftOptionsState, setGiftOptionsState] = useState({ error: "", loading: false, options: [] });
const [loadingAction, setLoadingAction] = useState("");
const [page, setPage] = useState(1);
const [status, setStatus] = useState("active");
const filters = useMemo(() => ({ status }), [status]);
const {
data = emptyData,
error,
loading,
reload,
} = usePaginatedQuery({
errorMessage: "加载机器人房间列表失败",
fetcher: listRobotRooms,
filters,
page,
pageSize,
queryKey: ["robot-rooms", filters, page],
});
const loadAvailableRobots = async () => {
setAvailableRobotsState((current) => ({ ...current, error: "", loading: true }));
try {
const options = await listAvailableRoomRobots();
setAvailableRobotsState({ error: "", loading: false, options: options || [] });
} catch (err) {
setAvailableRobotsState({ error: err.message || "加载可用机器人失败", loading: false, options: [] });
}
};
const loadGiftOptions = async () => {
setGiftOptionsState((current) => ({ ...current, error: "", loading: true }));
try {
const result = await listGifts({ page: 1, page_size: 200, status: "active" });
setGiftOptionsState({ error: "", loading: false, options: result?.items || [] });
} catch (err) {
setGiftOptionsState({ error: err.message || "加载礼物列表失败", loading: false, options: [] });
}
};
useEffect(() => {
if (activeAction !== "create") {
return;
}
loadAvailableRobots();
loadGiftOptions();
}, [activeAction]);
useEffect(() => {
if (activeAction !== "create" || candidatesTouched) {
return;
}
const ids = availableRobotsState.options.map((item) => item.userId).filter(Boolean);
if (ids.length > 0) {
setForm((current) => ({ ...current, candidateRobotUserIds: ids }));
}
}, [activeAction, availableRobotsState.options, candidatesTouched]);
const changeStatus = (value) => {
setStatus(value || "active");
setPage(1);
};
const resetFilters = () => {
setStatus("active");
setPage(1);
};
const openCreate = () => {
setActiveAction("create");
setCandidatesTouched(false);
setForm(emptyForm());
setAvailableRobotsState({ error: "", loading: false, options: [] });
setGiftOptionsState({ error: "", loading: false, options: [] });
};
const closeAction = () => {
setActiveAction("");
};
const submitCreate = async (event) => {
event.preventDefault();
await runAction("create", "机器人房间已创建", async () => {
const payload = parseForm(robotRoomCreateSchema, form);
await createRobotRoom(payload);
closeAction();
await reload();
});
};
const setRoomStatus = async (room) => {
if (!room?.roomId || !abilities.canRobotUpdate) {
return;
}
const nextActive = room.status !== "active";
await runAction(`status-${room.roomId}`, nextActive ? "机器人房间已启动" : "机器人房间已停止", async () => {
if (nextActive) {
await startRobotRoom(room.roomId);
} else {
await stopRobotRoom(room.roomId);
}
await reload();
});
};
const selectOwnerRobot = (robot) => {
setForm((current) => ({ ...current, ownerRobotUserId: robot?.userId || "" }));
};
const selectCandidateRobots = (robots) => {
setCandidatesTouched(true);
setForm((current) => ({ ...current, candidateRobotUserIds: robots.map((robot) => robot.userId).filter(Boolean) }));
};
const selectGiftIds = (key, gifts) => {
setForm((current) => ({ ...current, [key]: gifts.map(giftId).filter(Boolean) }));
};
const runAction = async (action, successMessage, submitter) => {
setLoadingAction(action);
try {
await submitter();
showToast(successMessage, "success");
} catch (err) {
showToast(err.message || "操作失败", "error");
} finally {
setLoadingAction("");
}
};
const selectedOwnerRobot = useMemo(
() => availableRobotsState.options.find((item) => item.userId === String(form.ownerRobotUserId)) || null,
[availableRobotsState.options, form.ownerRobotUserId],
);
const selectedCandidateRobots = useMemo(() => {
const selected = new Set((form.candidateRobotUserIds || []).map(String));
return availableRobotsState.options.filter((item) => selected.has(String(item.userId)));
}, [availableRobotsState.options, form.candidateRobotUserIds]);
const selectedGiftOptions = useMemo(() => {
const selected = new Set((form.giftIds || []).map(String));
return giftOptionsState.options.filter((item) => selected.has(giftId(item)));
}, [form.giftIds, giftOptionsState.options]);
const selectedLuckyGiftOptions = useMemo(() => {
const selected = new Set((form.luckyGiftIds || []).map(String));
return giftOptionsState.options.filter((item) => selected.has(giftId(item)));
}, [form.luckyGiftIds, giftOptionsState.options]);
return {
abilities,
activeAction,
availableRobotsError: availableRobotsState.error,
availableRobotsLoading: availableRobotsState.loading,
availableRobots: availableRobotsState.options,
changeStatus,
closeAction,
data,
error,
form,
giftOptions: giftOptionsState.options,
giftOptionsError: giftOptionsState.error,
giftOptionsLoading: giftOptionsState.loading,
loading,
loadingAction,
openCreate,
page,
reload,
resetFilters,
selectCandidateRobots,
selectGiftIds,
selectOwnerRobot,
selectedCandidateRobots,
selectedGiftOptions,
selectedLuckyGiftOptions,
selectedOwnerRobot,
setForm,
setPage,
setRoomStatus,
status,
submitCreate,
};
}
export function robotOptionLabel(robot) {
if (!robot) {
return "";
}
const user = robot.user || {};
return [user.username, user.displayUserId, robot.userId].filter(Boolean).join(" / ");
}
export function giftId(gift) {
return String(gift?.giftId || "");
}
export function giftOptionLabel(gift) {
if (!gift) {
return "";
}
return [gift.name, gift.giftId].filter(Boolean).join(" / ");
}

View File

@ -0,0 +1,365 @@
import AddOutlined from "@mui/icons-material/AddOutlined";
import BedroomParentOutlined from "@mui/icons-material/BedroomParentOutlined";
import Autocomplete from "@mui/material/Autocomplete";
import TextField from "@mui/material/TextField";
import Tooltip from "@mui/material/Tooltip";
import { Button } from "@/shared/ui/Button.jsx";
import { AdminFormDialog } from "@/shared/ui/AdminFormDialog.jsx";
import { AdminUserIdentity } from "@/shared/ui/AdminUserIdentity.jsx";
import { DataState } from "@/shared/ui/DataState.jsx";
import { DataTable } from "@/shared/ui/DataTable.jsx";
import { AdminSwitch } from "@/shared/ui/AdminSwitch.jsx";
import { AdminFilterResetButton } from "@/shared/ui/AdminListLayout.jsx";
import { createOptionsColumnFilter } from "@/shared/ui/tableFilters.js";
import { formatMillis } from "@/shared/utils/time.js";
import {
giftId,
giftOptionLabel,
robotOptionLabel,
useRobotRoomsPage,
} from "@/features/rooms/hooks/useRobotRoomsPage.js";
import styles from "@/features/rooms/rooms.module.css";
const statusOptions = [
{ label: "运行中", value: "active" },
{ label: "已停止", value: "stopped" },
{ label: "全部状态", value: "all" },
];
const columns = [
{
key: "room",
label: "房间信息",
width: "minmax(280px, 1.4fr)",
render: (room) => <RobotRoomIdentity room={room} />,
},
{
key: "owner",
label: "房主机器人",
width: "minmax(220px, 1fr)",
render: (room) => <RobotIdentity user={room.owner} fallbackId={room.ownerRobotUserId} />,
},
{
key: "robots",
label: "进房机器人",
width: "minmax(120px, 0.5fr)",
render: (room) => Number(room.robotUserIds?.length || room.robots?.length || 0).toLocaleString("zh-CN"),
},
{
key: "normalRule",
label: "普通礼物",
width: "minmax(190px, 0.8fr)",
render: (room) => (
<RuleStack
rows={[
`${room.giftRule?.giftIds?.length || 0} 个礼物`,
`${formatSeconds(room.giftRule?.normalGiftIntervalMs)} / 次`,
]}
/>
),
},
{
key: "luckyRule",
label: "幸运礼物",
width: "minmax(220px, 0.9fr)",
render: (room) => (
<RuleStack
rows={[
`${room.giftRule?.luckyGiftIds?.length || 0} 个礼物`,
`${room.giftRule?.luckyComboMin || 0}-${room.giftRule?.luckyComboMax || 0} 连击`,
`${formatSeconds(room.giftRule?.luckyPauseMinMs)}-${formatSeconds(room.giftRule?.luckyPauseMaxMs)} 间隔`,
]}
/>
),
},
{
key: "createdAt",
label: "创建时间",
width: "minmax(170px, 0.8fr)",
render: (room) => formatMillis(room.createdAtMs),
},
];
export function RoomRobotPage() {
const page = useRobotRoomsPage();
const items = page.data.items || [];
const total = page.data.total || 0;
const tableColumns = [
...columns.map((column) =>
column.key === "createdAt"
? {
...column,
filter: createOptionsColumnFilter({
options: statusOptions,
placeholder: "房间状态",
value: page.status,
onChange: page.changeStatus,
}),
}
: column,
),
{
key: "status",
label: "状态",
width: "minmax(112px, 0.5fr)",
render: (room) => <RobotRoomStatus page={page} room={room} />,
},
];
return (
<section className={styles.root}>
<div className={styles.contentPanel}>
<div className={styles.toolbar}>
<section className={styles.filters}>
<AdminFilterResetButton disabled={page.status === "active"} onClick={page.resetFilters} />
</section>
{page.abilities.canRobotCreate ? (
<div className={styles.toolbarActions}>
<Button startIcon={<AddOutlined fontSize="small" />} variant="primary" onClick={page.openCreate}>
增加机器人房间
</Button>
</div>
) : null}
</div>
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
<div className={styles.listBlock}>
<DataTable
columns={tableColumns}
items={items}
minWidth="1320px"
rowKey={(room) => room.roomId}
pagination={
total > 0
? {
page: page.page,
pageSize: page.data.pageSize || 50,
total,
onPageChange: page.setPage,
}
: undefined
}
/>
</div>
</DataState>
</div>
<AdminFormDialog
loading={page.loadingAction === "create"}
open={page.activeAction === "create"}
size="wide"
submitDisabled={!page.abilities.canRobotCreate || page.loadingAction === "create"}
title="增加机器人房间"
onClose={page.closeAction}
onSubmit={page.submitCreate}
>
<TextField
label="房间名称"
value={page.form.roomName}
onChange={(event) => page.setForm({ ...page.form, roomName: event.target.value })}
/>
<Autocomplete
getOptionLabel={robotOptionLabel}
isOptionEqualToValue={(option, value) => option.userId === value.userId}
loading={page.availableRobotsLoading}
noOptionsText="当前无数据"
options={page.availableRobots}
renderInput={(params) => (
<TextField
{...params}
error={Boolean(page.availableRobotsError)}
helperText={page.availableRobotsError}
label="房主机器人"
required
/>
)}
renderOption={(props, option) => (
<li {...props} key={option.userId}>
<RobotIdentity user={option.user} fallbackId={option.userId} />
</li>
)}
size="small"
value={page.selectedOwnerRobot}
onChange={(_, option) => page.selectOwnerRobot(option)}
/>
<Autocomplete
multiple
disableCloseOnSelect
getOptionLabel={robotOptionLabel}
isOptionEqualToValue={(option, value) => option.userId === value.userId}
loading={page.availableRobotsLoading}
noOptionsText="当前无数据"
options={page.availableRobots}
renderInput={(params) => (
<TextField
{...params}
error={Boolean(page.availableRobotsError)}
helperText={page.availableRobotsError}
label="候选机器人"
required
/>
)}
renderOption={(props, option) => (
<li {...props} key={option.userId}>
<RobotIdentity user={option.user} fallbackId={option.userId} />
</li>
)}
size="small"
value={page.selectedCandidateRobots}
onChange={(_, options) => page.selectCandidateRobots(options)}
/>
<div className={styles.formGrid}>
<TextField
label="最少进房人数"
required
type="number"
value={page.form.minRobotCount}
onChange={(event) => page.setForm({ ...page.form, minRobotCount: event.target.value })}
/>
<TextField
label="最多进房人数"
required
type="number"
value={page.form.maxRobotCount}
onChange={(event) => page.setForm({ ...page.form, maxRobotCount: event.target.value })}
/>
</div>
<Autocomplete
multiple
disableCloseOnSelect
getOptionLabel={giftOptionLabel}
isOptionEqualToValue={(option, value) => giftId(option) === giftId(value)}
loading={page.giftOptionsLoading}
noOptionsText="当前无数据"
options={page.giftOptions}
renderInput={(params) => (
<TextField
{...params}
error={Boolean(page.giftOptionsError)}
helperText={page.giftOptionsError}
label="礼物合集"
required
/>
)}
size="small"
value={page.selectedGiftOptions}
onChange={(_, options) => page.selectGiftIds("giftIds", options)}
/>
<TextField
label="普通礼物频次(毫秒)"
required
type="number"
value={page.form.normalGiftIntervalMs}
onChange={(event) => page.setForm({ ...page.form, normalGiftIntervalMs: event.target.value })}
/>
<Autocomplete
multiple
disableCloseOnSelect
getOptionLabel={giftOptionLabel}
isOptionEqualToValue={(option, value) => giftId(option) === giftId(value)}
loading={page.giftOptionsLoading}
noOptionsText="当前无数据"
options={page.giftOptions}
renderInput={(params) => (
<TextField
{...params}
error={Boolean(page.giftOptionsError)}
helperText={page.giftOptionsError}
label="幸运礼物合集"
required
/>
)}
size="small"
value={page.selectedLuckyGiftOptions}
onChange={(_, options) => page.selectGiftIds("luckyGiftIds", options)}
/>
<div className={styles.formGrid}>
<TextField
label="最少幸运连击"
required
type="number"
value={page.form.luckyComboMin}
onChange={(event) => page.setForm({ ...page.form, luckyComboMin: event.target.value })}
/>
<TextField
label="最多幸运连击"
required
type="number"
value={page.form.luckyComboMax}
onChange={(event) => page.setForm({ ...page.form, luckyComboMax: event.target.value })}
/>
<TextField
label="最短幸运间隔(毫秒)"
required
type="number"
value={page.form.luckyPauseMinMs}
onChange={(event) => page.setForm({ ...page.form, luckyPauseMinMs: event.target.value })}
/>
<TextField
label="最长幸运间隔(毫秒)"
required
type="number"
value={page.form.luckyPauseMaxMs}
onChange={(event) => page.setForm({ ...page.form, luckyPauseMaxMs: event.target.value })}
/>
</div>
</AdminFormDialog>
</section>
);
}
function RobotRoomIdentity({ room }) {
return (
<div className={styles.identity}>
<span className={styles.cover}>
{room.coverUrl ? <img src={room.coverUrl} alt="" /> : <BedroomParentOutlined fontSize="small" />}
</span>
<div className={styles.identityText}>
<div className={styles.name}>{room.title || "-"}</div>
<div className={styles.meta}>长ID {room.roomId || "-"}</div>
<div className={styles.meta}>短ID {room.roomShortId || "-"}</div>
</div>
</div>
);
}
function RobotIdentity({ user = {}, fallbackId = "" }) {
return <AdminUserIdentity openInAppUserDetail rows={[user.displayUserId, user.userId || fallbackId]} user={user} />;
}
function RobotRoomStatus({ page, room }) {
const checked = room.status === "active";
return (
<div className={styles.statusCell}>
<Tooltip arrow title={checked ? "停止" : "启动"}>
<span>
<AdminSwitch
checked={checked}
checkedLabel="运行"
disabled={!page.abilities.canRobotUpdate || page.loadingAction === `status-${room.roomId}`}
label={checked ? "停止机器人房间" : "启动机器人房间"}
uncheckedLabel="停止"
onChange={() => page.setRoomStatus(room)}
/>
</span>
</Tooltip>
</div>
);
}
function RuleStack({ rows }) {
return (
<div className={styles.stack}>
{rows.map((row) => (
<span className={styles.meta} key={row}>
{row}
</span>
))}
</div>
);
}
function formatSeconds(ms) {
const seconds = Math.round(Number(ms || 0) / 1000);
return `${seconds.toLocaleString("zh-CN")}`;
}

View File

@ -11,6 +11,9 @@ export function useRoomAbilities() {
canPinCancel: can(PERMISSIONS.roomPinCancel),
canPinCreate: can(PERMISSIONS.roomPinCreate),
canPinView: can(PERMISSIONS.roomPinView),
canRobotCreate: can(PERMISSIONS.roomRobotCreate),
canRobotUpdate: can(PERMISSIONS.roomRobotUpdate),
canRobotView: can(PERMISSIONS.roomRobotView),
canUpdate: can(PERMISSIONS.roomUpdate),
canUpload: can(PERMISSIONS.uploadCreate),
canView: can(PERMISSIONS.roomView),

View File

@ -54,6 +54,18 @@
gap: var(--space-2);
}
.formGrid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: var(--space-3);
}
@media (max-width: 720px) {
.formGrid {
grid-template-columns: 1fr;
}
}
.search {
flex: 0 1 320px;
}

View File

@ -25,4 +25,12 @@ export const roomRoutes = [
path: "/rooms/config",
permission: PERMISSIONS.roomConfigView,
},
{
label: "机器人房间",
loader: () => import("./pages/RoomRobotPage.jsx").then((module) => module.RoomRobotPage),
menuCode: MENU_CODES.roomRobots,
pageKey: "room-robots",
path: "/rooms/robots",
permission: PERMISSIONS.roomRobotView,
},
];

View File

@ -70,7 +70,37 @@ export const roomPinCreateSchema = z
path: ["expiresAtMs"],
});
export const robotRoomCreateSchema = z
.object({
candidateRobotUserIds: z.array(z.coerce.number().int().positive()).min(1, "请选择候选机器人"),
giftIds: z.array(z.string().trim().min(1)).min(1, "请选择普通礼物合集"),
luckyComboMax: z.coerce.number().int().min(1, "幸运礼物连击范围不正确"),
luckyComboMin: z.coerce.number().int().min(1, "幸运礼物连击范围不正确"),
luckyGiftIds: z.array(z.string().trim().min(1)).min(1, "请选择幸运礼物合集"),
luckyPauseMaxMs: z.coerce.number().int().min(1000, "幸运礼物间隔范围不正确"),
luckyPauseMinMs: z.coerce.number().int().min(1000, "幸运礼物间隔范围不正确"),
maxRobotCount: z.coerce.number().int().min(1, "机器人数量范围不正确"),
minRobotCount: z.coerce.number().int().min(1, "机器人数量范围不正确"),
normalGiftIntervalMs: z.coerce.number().int().min(1000, "普通礼物频次不正确"),
ownerRobotUserId: z.coerce.number().int().positive("请选择房主机器人"),
roomName: z.string().trim().max(128, "房间名称不能超过 128 个字符").optional().default(""),
visibleRegionId: z.coerce.number().int().min(0, "区域 ID 不正确").default(0),
})
.refine((value) => value.maxRobotCount >= value.minRobotCount, {
message: "机器人数量范围不正确",
path: ["maxRobotCount"],
})
.refine((value) => value.luckyComboMax >= value.luckyComboMin, {
message: "幸运礼物连击范围不正确",
path: ["luckyComboMax"],
})
.refine((value) => value.luckyPauseMaxMs >= value.luckyPauseMinMs, {
message: "幸运礼物间隔范围不正确",
path: ["luckyPauseMaxMs"],
});
export type RoomUpdateForm = z.infer<typeof roomUpdateSchema>;
export type RoomStatusForm = z.infer<typeof roomStatusSchema>;
export type RoomConfigForm = z.infer<typeof roomConfigSchema>;
export type RoomPinCreateForm = z.infer<typeof roomPinCreateSchema>;
export type RobotRoomCreateForm = z.infer<typeof robotRoomCreateSchema>;

View File

@ -52,6 +52,7 @@ export const API_OPERATIONS = {
createRegion: "createRegion",
createResource: "createResource",
createResourceGroup: "createResourceGroup",
createRobotRoom: "createRobotRoom",
createRole: "createRole",
createRoomPin: "createRoomPin",
createSplashScreen: "createSplashScreen",
@ -128,6 +129,7 @@ export const API_OPERATIONS = {
listAgencies: "listAgencies",
listApps: "listApps",
listAppVersions: "listAppVersions",
listAvailableRoomRobots: "listAvailableRoomRobots",
listBanners: "listBanners",
listBDLeaders: "listBDLeaders",
listBDs: "listBDs",
@ -172,6 +174,7 @@ export const API_OPERATIONS = {
listResourceGroups: "listResourceGroups",
listResources: "listResources",
listResourceShopItems: "listResourceShopItems",
listRobotRooms: "listRobotRooms",
listRoles: "listRoles",
listRoomPins: "listRoomPins",
listRoomRpsChallenges: "listRoomRpsChallenges",
@ -218,6 +221,8 @@ export const API_OPERATIONS = {
setThirdPartyPaymentMethodStatus: "setThirdPartyPaymentMethodStatus",
setWeeklyStarCycleStatus: "setWeeklyStarCycleStatus",
sortMenus: "sortMenus",
startRobotRoom: "startRobotRoom",
stopRobotRoom: "stopRobotRoom",
syncPermissions: "syncPermissions",
syncThirdPartyPaymentRates: "syncThirdPartyPaymentRates",
updateAchievementDefinition: "updateAchievementDefinition",
@ -546,6 +551,13 @@ export const API_ENDPOINTS: Record<ApiOperationId, ApiEndpoint> = {
permission: "resource-group:create",
permissions: ["resource-group:create"]
},
createRobotRoom: {
method: "POST",
operationId: API_OPERATIONS.createRobotRoom,
path: "/v1/admin/rooms/robot-rooms",
permission: "room-robot:create",
permissions: ["room-robot:create"]
},
createRole: {
method: "POST",
operationId: API_OPERATIONS.createRole,
@ -1076,6 +1088,13 @@ export const API_ENDPOINTS: Record<ApiOperationId, ApiEndpoint> = {
permission: "app-version:view",
permissions: ["app-version:view"]
},
listAvailableRoomRobots: {
method: "GET",
operationId: API_OPERATIONS.listAvailableRoomRobots,
path: "/v1/admin/rooms/robot-rooms/available-robots",
permission: "room-robot:view",
permissions: ["room-robot:view"]
},
listBanners: {
method: "GET",
operationId: API_OPERATIONS.listBanners,
@ -1384,6 +1403,13 @@ export const API_ENDPOINTS: Record<ApiOperationId, ApiEndpoint> = {
permission: "resource-shop:view",
permissions: ["resource-shop:view"]
},
listRobotRooms: {
method: "GET",
operationId: API_OPERATIONS.listRobotRooms,
path: "/v1/admin/rooms/robot-rooms",
permission: "room-robot:view",
permissions: ["room-robot:view"]
},
listRoles: {
method: "GET",
operationId: API_OPERATIONS.listRoles,
@ -1694,6 +1720,20 @@ export const API_ENDPOINTS: Record<ApiOperationId, ApiEndpoint> = {
permission: "menu:sort",
permissions: ["menu:sort"]
},
startRobotRoom: {
method: "POST",
operationId: API_OPERATIONS.startRobotRoom,
path: "/v1/admin/rooms/robot-rooms/{room_id}/start",
permission: "room-robot:update",
permissions: ["room-robot:update"]
},
stopRobotRoom: {
method: "POST",
operationId: API_OPERATIONS.stopRobotRoom,
path: "/v1/admin/rooms/robot-rooms/{room_id}/stop",
permission: "room-robot:update",
permissions: ["room-robot:update"]
},
syncPermissions: {
method: "POST",
operationId: API_OPERATIONS.syncPermissions,

View File

@ -2164,6 +2164,70 @@ export interface paths {
patch?: never;
trace?: never;
};
"/admin/rooms/robot-rooms": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: operations["listRobotRooms"];
put?: never;
post: operations["createRobotRoom"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/admin/rooms/robot-rooms/available-robots": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: operations["listAvailableRoomRobots"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/admin/rooms/robot-rooms/{room_id}/start": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
post: operations["startRobotRoom"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/admin/rooms/robot-rooms/{room_id}/stop": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
post: operations["stopRobotRoom"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/admin/users/level-config": {
parameters: {
query?: never;
@ -5977,6 +6041,70 @@ export interface operations {
200: components["responses"]["EmptyResponse"];
};
};
listRobotRooms: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
200: components["responses"]["EmptyResponse"];
};
};
createRobotRoom: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
200: components["responses"]["EmptyResponse"];
};
};
listAvailableRoomRobots: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
200: components["responses"]["EmptyResponse"];
};
};
startRobotRoom: {
parameters: {
query?: never;
header?: never;
path: {
room_id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
200: components["responses"]["EmptyResponse"];
};
};
stopRobotRoom: {
parameters: {
query?: never;
header?: never;
path: {
room_id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
200: components["responses"]["EmptyResponse"];
};
};
listLevelConfig: {
parameters: {
query?: never;

View File

@ -938,6 +938,61 @@ export interface RoomPinPayload {
weight: number;
}
export interface RobotGiftRuleDto {
giftIds?: string[];
luckyGiftIds?: string[];
luckyComboMax?: number;
luckyComboMin?: number;
luckyPauseMaxMs?: number;
luckyPauseMinMs?: number;
normalGiftIntervalMs?: number;
}
export interface RobotRoomDto {
appCode?: string;
coverUrl?: string;
createdAtMs?: number;
createdByAdminId?: number;
giftRule?: RobotGiftRuleDto;
owner?: RoomOwnerDto;
ownerRobotUserId?: string;
regionName?: string;
robots?: RoomOwnerDto[];
robotUserIds?: string[];
roomId: string;
roomShortId?: string;
status?: string;
title?: string;
updatedAtMs?: number;
visibleRegionId?: number;
}
export interface AvailableRoomRobotDto {
lastUsedAtMs?: number;
roomScene?: string;
status?: string;
usedCount?: number;
user?: RoomOwnerDto;
userId: string;
}
export interface RobotRoomPayload {
candidateRobotUserIds: EntityId[];
giftIds: string[];
luckyComboMax: number;
luckyComboMin: number;
luckyGiftIds: string[];
luckyPauseMaxMs: number;
luckyPauseMinMs: number;
maxRobotCount: number;
minRobotCount: number;
normalGiftIntervalMs: number;
ownerRobotUserId: EntityId;
roomAvatar?: string;
roomName?: string;
visibleRegionId?: EntityId;
}
export interface RoomOwnerDto {
avatar?: string;
displayUserId?: string;