chore: 初始化仓库基线(AGENTS.md、git 规范、敏感文件排除)

This commit is contained in:
weijuesen
2026-08-10 22:30:53 +08:00
commit 84abf4454c
358 changed files with 75993 additions and 0 deletions
+42
View File
@@ -0,0 +1,42 @@
import { get, post } from '../api/http';
export interface Alert {
id: string;
level?: 'info' | 'warn' | 'danger';
severity?: string;
title: string;
content?: string;
message?: string;
houseId?: string;
deviceId?: string;
deviceKey?: string;
read?: boolean;
acknowledged?: boolean;
open?: boolean;
createdAt?: string;
triggeredAt?: string;
}
export interface AlarmClip {
id: string;
playbackUrl?: string;
cameraId?: string;
startAt?: string;
endAt?: string;
mock?: boolean;
}
const toList = (data: Alert[] | { items: Alert[]; total?: number }) => {
const items = Array.isArray(data) ? data : data.items;
return { items, total: Array.isArray(data) ? data.length : data.total ?? items.length };
};
export const listAlerts = async (params?: any) =>
toList(await get<Alert[] | { items: Alert[]; total?: number }>('/alarms', { params }));
export const markAlertRead = (id: string) => post(`/alarms/${id}/ack`);
export const markAllRead = async () => {
const res = await listAlerts({ openOnly: true });
await Promise.all(res.items.filter((item) => !item.acknowledged).map((item) => markAlertRead(item.id)));
return { ok: true };
};
export const getAlertClip = (id: string) => get<AlarmClip>(`/alarms/${id}/clip`);
+35
View File
@@ -0,0 +1,35 @@
import { get, post } from '../api/http';
export interface LoginPayload {
username: string;
password: string;
}
export interface LoginResp {
accessToken: string;
refreshToken?: string;
token?: string;
user: { id: string; username?: string; name?: string; fullName?: string; role?: string; permissions?: string[] };
}
export interface ChangePasswordPayload {
oldPassword: string;
newPassword: string;
}
export const loginApi = (payload: LoginPayload) =>
post<LoginResp>('/auth/login', payload, { silent: true });
export const logoutApi = () => post('/auth/logout', {});
export const changePasswordApi = (payload: ChangePasswordPayload) =>
post('/auth/change-password', payload);
export interface MeResp {
sub: string;
username: string;
role: string;
permissions: string[];
}
export const meApi = () => get<MeResp>('/auth/me');
+148
View File
@@ -0,0 +1,148 @@
import { get } from '../api/http';
export interface Room {
id: string;
name: string;
code?: string;
location?: string;
status?: 'active' | 'inactive' | 'running' | 'idle' | 'alarm';
}
export interface RealtimeMetric {
key: string;
name: string;
value: number;
unit: string;
status: 'normal' | 'warn' | 'danger';
}
export interface TrendPoint {
time: string;
temp?: number;
humidity?: number;
co2?: number;
}
export interface DeviceSummary {
total: number;
online: number;
offline: number;
alarm: number;
/** 蚕房运行中数量(status=running/active */
running: number;
/** 蚕房停用数量(status=inactive */
inactive: number;
}
interface TelemetryRecord {
id?: string;
deviceKey: string;
metric: string;
value: number;
timestamp: string;
}
const METRIC_NAME: Record<string, string> = {
temperature: '温度',
temp: '温度',
humidity: '湿度',
co2: 'CO₂',
light: '光照',
ph: 'PH',
};
const METRIC_UNIT: Record<string, string> = {
temperature: '℃',
temp: '℃',
humidity: '%',
co2: 'ppm',
light: 'lux',
ph: '',
};
const toArray = <T>(data: T[] | { items?: T[] }) => (Array.isArray(data) ? data : data.items ?? []);
const normalizeMetricKey = (metric: string) => (metric === 'temperature' ? 'temp' : metric);
const metricStatus = (key: string, value: number): RealtimeMetric['status'] => {
if (key === 'temp') return value < 20 || value > 30 ? 'danger' : value < 22 || value > 28 ? 'warn' : 'normal';
if (key === 'humidity') return value < 55 || value > 85 ? 'danger' : value < 60 || value > 80 ? 'warn' : 'normal';
if (key === 'co2') return value > 1500 ? 'danger' : value > 1000 ? 'warn' : 'normal';
return 'normal';
};
const normalizeRealtime = (records: TelemetryRecord[]): RealtimeMetric[] => {
const latestByMetric = new Map<string, TelemetryRecord>();
for (const record of records) {
const key = normalizeMetricKey(record.metric);
const prev = latestByMetric.get(key);
if (!prev || new Date(record.timestamp).getTime() > new Date(prev.timestamp).getTime()) {
latestByMetric.set(key, record);
}
}
return Array.from(latestByMetric.entries()).map(([key, record]) => ({
key,
name: METRIC_NAME[key] || record.metric,
value: Number(record.value),
unit: METRIC_UNIT[key] || '',
status: metricStatus(key, Number(record.value)),
}));
};
export const fetchRooms = async () => toArray(await get<Room[] | { items: Room[] }>('/rooms', { silent: true }));
export const fetchRealtime = async () => {
const records = await get<TelemetryRecord[]>('/telemetry', {
params: { limit: 100 },
silent: true,
});
return normalizeRealtime(records);
};
export const fetchRoomRealtime = async (roomId: string) => {
try {
const data = await get<TelemetryRecord[] | RealtimeMetric[]>(`/rooms/${roomId}/telemetry/latest`, { silent: true });
const arr = toArray(data as TelemetryRecord[] | { items: TelemetryRecord[] });
if (arr.length > 0 && 'metric' in arr[0]) return normalizeRealtime(arr as TelemetryRecord[]);
return arr as unknown as RealtimeMetric[];
} catch {
return fetchRealtime();
}
};
export const fetchTrend = async (hours = 24) => {
const to = new Date();
const from = new Date(to.getTime() - hours * 3600 * 1000);
const records = await get<TelemetryRecord[]>('/telemetry', {
params: { from: from.toISOString(), to: to.toISOString(), limit: 1000 },
silent: true,
});
return records
.slice()
.reverse()
.reduce<TrendPoint[]>((acc, record) => {
const key = normalizeMetricKey(record.metric);
if (!['temp', 'humidity', 'co2'].includes(key)) return acc;
const time = new Date(record.timestamp).toLocaleTimeString().slice(0, 5);
const point = acc.find((item) => item.time === time) ?? { time };
(point as unknown as Record<string, string | number | undefined>)[key] = Number(record.value);
if (!acc.includes(point)) acc.push(point);
return acc;
}, []);
};
export const fetchDeviceSummary = async (): Promise<DeviceSummary> => {
const [rooms, alarms] = await Promise.all([
fetchRooms().catch(() => []),
get<any[]>('/alarms', { params: { openOnly: true }, silent: true }).catch(() => []),
]);
return {
total: rooms.length,
online: rooms.filter((room) => room.status !== 'inactive').length,
offline: rooms.filter((room) => room.status === 'inactive').length,
running: rooms.filter((room) => room.status === 'running' || room.status === 'active').length,
inactive: rooms.filter((room) => room.status === 'inactive').length,
alarm: alarms.length,
};
};
+37
View File
@@ -0,0 +1,37 @@
import { get, post, patch, del } from '../api/http';
export interface Device {
id: string;
deviceKey?: string;
name: string;
type?: 'sensor' | 'actuator' | 'gateway';
kind?: 'sensor' | 'actuator' | 'gateway';
houseId?: string;
roomId?: string;
status?: 'online' | 'offline' | 'unknown';
onlineStatus?: 'online' | 'offline' | 'unknown';
lastSeen?: string;
meta?: Record<string, any>;
}
const toList = (data: Device[] | { items: Device[]; total?: number }) => {
const items = Array.isArray(data) ? data : data.items;
return { items, total: Array.isArray(data) ? data.length : data.total ?? items.length };
};
const toBackendDevice = (data: Partial<Device>) => ({
...data,
kind: data.kind || data.type,
roomId: data.roomId || data.houseId,
});
export const listDevices = async (params?: any) =>
toList(await get<Device[] | { items: Device[]; total?: number }>('/devices', { params }));
export const getDevice = (id: string) => get<Device>(`/devices/${id}`);
export const createDevice = (data: Partial<Device>) =>
post<Device>('/devices', toBackendDevice(data));
export const updateDevice = (id: string, data: Partial<Device>) =>
patch<Device>(`/devices/${id}`, toBackendDevice(data));
export const deleteDevice = (id: string) => del(`/devices/${id}`);
export const sendDeviceCommand = (deviceKey: string, action: string, payload?: any) =>
post<{ ok?: boolean; success?: boolean }>('/control/send', { deviceKey, action, payload });
+14
View File
@@ -0,0 +1,14 @@
import { get } from '../api/http';
export interface ControlLog {
id: string;
deviceId?: string;
deviceName?: string;
command: string;
operator?: string;
success?: boolean;
createdAt: string;
}
export const listLogs = (params?: any) =>
get<{ items: ControlLog[]; total: number }>('/logs/control', { params });
+27
View File
@@ -0,0 +1,27 @@
import { get, post, patch, del } from '../api/http';
export interface SilkwormHouse {
id: string;
name: string;
code?: string;
location?: string;
description?: string;
status?: 'active' | 'inactive' | 'running' | 'idle' | 'alarm';
capacity?: number;
stage?: string;
createdAt?: string;
}
const toList = (data: SilkwormHouse[] | { items: SilkwormHouse[]; total?: number }) => {
const items = Array.isArray(data) ? data : data.items;
return { items, total: Array.isArray(data) ? data.length : data.total ?? items.length };
};
export const listHouses = async (params?: any) =>
toList(await get<SilkwormHouse[] | { items: SilkwormHouse[]; total?: number }>('/rooms', { params }));
export const getHouse = (id: string) => get<SilkwormHouse>(`/rooms/${id}`);
export const createHouse = (data: Partial<SilkwormHouse>) =>
post<SilkwormHouse>('/rooms', data);
export const updateHouse = (id: string, data: Partial<SilkwormHouse>) =>
patch<SilkwormHouse>(`/rooms/${id}`, data);
export const deleteHouse = (id: string) => del(`/rooms/${id}`);
+38
View File
@@ -0,0 +1,38 @@
import { get, post, patch, del } from '../api/http';
export interface Threshold {
id: string;
name?: string;
metric?: string;
houseId?: string;
roomId?: string;
sensorId?: string;
min?: number;
max?: number;
minValue?: number;
maxValue?: number;
debounceSeconds?: number;
severity?: number;
unit?: string;
enabled?: boolean;
sensor?: { id: string; name?: string; metric?: string; unit?: string };
}
const toList = (data: Threshold[] | { items: Threshold[]; total?: number }) => {
const items = Array.isArray(data) ? data : data.items;
return { items, total: Array.isArray(data) ? data.length : data.total ?? items.length };
};
const toBackendThreshold = (data: Partial<Threshold>) => ({
...data,
minValue: data.minValue ?? data.min,
maxValue: data.maxValue ?? data.max,
});
export const listThresholds = async (params?: any) =>
toList(await get<Threshold[] | { items: Threshold[]; total?: number }>('/thresholds', { params }));
export const createThreshold = (data: Partial<Threshold>) =>
post<Threshold>('/thresholds', toBackendThreshold(data));
export const updateThreshold = (id: string, data: Partial<Threshold>) =>
patch<Threshold>(`/thresholds/${id}`, toBackendThreshold(data));
export const deleteThreshold = (id: string) => del(`/thresholds/${id}`);
+217
View File
@@ -0,0 +1,217 @@
import { del, get, patch, post } from '../api/http';
export interface Camera {
id: string;
name: string;
online?: boolean;
isOnline?: boolean;
enabled?: boolean;
roomId?: string;
streamUrl?: string;
rtspUrl?: string;
httpUrl?: string;
hlsUrl?: string;
flvUrl?: string;
webrtcUrl?: string;
snapshotUrl?: string;
username?: string;
passwordEnc?: string;
position?: string;
resolution?: string;
fps?: number;
gbDeviceId?: string;
gbChannelId?: string;
gbAuthId?: string;
gbAuthPassword?: string;
gbStreamType?: string;
gbTransport?: string;
gbAlarmChannelId?: string;
gbVoiceChannelId?: string;
gbManufacturer?: string;
manufacturerId?: string;
}
export interface WvpSipConfig {
sipId: string;
sipDomain: string;
sipPassword: string;
sipPort: number;
sipShowIp: string;
}
export interface PlayInfo {
cameraId: string;
code?: string;
format: 'hls' | 'flv' | 'webrtc';
url: string;
expiresAt?: string;
mock?: boolean;
gbDeviceId?: string | null;
gbChannelId?: string | null;
}
export interface LiveInfo {
cameraId: string;
gbDeviceId: string | null;
gbChannelId: string | null;
format: 'hls' | 'flv' | 'webrtc';
url: string;
expiresAt?: string;
mock?: boolean;
}
export interface VideoClip {
id: string;
cameraId: string;
trigger: 'alarm' | 'manual' | 'schedule' | 'motion';
format: 'hls' | 'flv' | 'webrtc' | 'mp4';
startAt: string;
endAt?: string;
durationSec: number;
resolution?: string;
sizeBytes?: string | number;
playbackUrl?: string;
mock?: boolean;
}
export interface ClipListResponse {
items: VideoClip[];
total: number;
}
export interface ClipPlayInfo {
clipId: string;
url: string;
format: string;
expiresAt?: string;
}
export interface ClipQuery {
cameraId?: string;
from?: string;
to?: string;
limit?: number;
}
const toArray = <T>(data: T[] | { items?: T[] }) => (Array.isArray(data) ? data : data.items ?? []);
export const listCameras = async () => toArray(await get<Camera[] | { items: Camera[] }>('/video/cameras'));
export const playCamera = (id: string, format: PlayInfo['format'] = 'hls') =>
post<PlayInfo>(`/video/cameras/${id}/play`, { format });
export const getLiveUrl = (id: string, format: 'hls' | 'flv' | 'webrtc' = 'hls') =>
post<LiveInfo>(`/video/cameras/${id}/live`, { format });
export const listClips = (query: ClipQuery = {}) => {
const params = new URLSearchParams();
if (query.cameraId) params.set('cameraId', query.cameraId);
if (query.from) params.set('from', query.from);
if (query.to) params.set('to', query.to);
if (query.limit) params.set('limit', String(query.limit));
const qs = params.toString();
return get<ClipListResponse>(`/video/clips${qs ? `?${qs}` : ''}`);
};
export const getClipPlayUrl = (clipId: string) =>
get<ClipPlayInfo>(`/video/clips/${clipId}/play`);
export const createCamera = (data: Partial<Camera>) =>
post<Camera>('/video/cameras', data);
export const updateCamera = (id: string, data: Partial<Camera>) =>
patch<Camera>(`/video/cameras/${id}`, data);
export const deleteCamera = (id: string) =>
del<{ id: string }>(`/video/cameras/${id}`);
export const getWvpConfig = () =>
get<WvpSipConfig>('/video/wvp-config');
export interface WvpDevice {
id: number;
deviceId: string;
name: string;
manufacturer: string;
model: string;
onLine: boolean;
transport: string;
streamMode: string;
hostAddress: string;
}
export interface WvpChannel {
id: number;
deviceId: string;
channelId: string;
name: string;
onLine: boolean;
}
export const listWvpDevices = (query?: string) =>
get<WvpDevice[]>(`/video/wvp/devices${query ? `?query=${encodeURIComponent(query)}` : ''}`);
export const listWvpChannels = (deviceId: string) =>
get<WvpChannel[]>(`/video/wvp/devices/${encodeURIComponent(deviceId)}/channels`);
export const syncWvpDevice = (deviceId: string) =>
post<{ ok: boolean }>(`/video/wvp/devices/${encodeURIComponent(deviceId)}/sync`);
export interface ActiveRecording {
cameraId: string;
deviceId: string;
channelId: string;
stream: string;
app: string;
startedAt: string;
}
/** 开始录制 */
export const startRecording = (cameraId: string) =>
post<{ ok: boolean }>(`/video/cameras/${cameraId}/record/start`);
/** 停止录制并归档 */
export const stopRecording = (cameraId: string) =>
post<VideoClip>(`/video/cameras/${cameraId}/record/stop`);
/** 获取正在录制的摄像头 */
export const getActiveRecordings = () =>
get<ActiveRecording[]>(`/video/recordings/active`);
// ===== 存储状态 =====
export interface CephStorageData {
df: {
stats: {
total_bytes: number;
total_used_bytes: number;
total_avail_bytes: number;
};
pools: Array<{
name: string;
id: number;
stats: {
stored: number;
objects: number;
max_avail: number;
percent_used: number;
};
}>;
};
osdTree: {
nodes: Array<{
id: number;
name: string;
type: string;
status: string;
weight: number;
class?: string;
}>;
};
health: {
status: string;
checks?: Record<string, { severity: string; summary: { message: string; count: number } }>;
};
}
export const getCephStorage = () =>
get<CephStorageData>('/storage/ceph');