Files
yangxiangyuan 212dbc4e1d refactor: 重构禅音工具路径与配置,新增私人剪贴板技能
这是一次大型重构和功能新增:
1. 将原 zen_box 工具重命名为 web_zen_box,调整所有相关路径与配置
2. 新增 web_order_box 原生Web应用的完整资源与部署脚本
3. 新增私人剪贴板后端技能与配套测试脚本
4. 修复订单盒子APP的API地址配置,避免路径重复拼接
5. 新增本地开发跨域白名单支持localhost
6. 清理旧版zen_box的冗余文件
2026-07-26 21:03:28 +08:00

365 lines
11 KiB
TypeScript

import axios from 'axios';
import NetInfo from '@react-native-community/netinfo';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { Track, UserConfig, SortMode } from './types';
const API_BASE_URL = 'https://traesite.umersoft.com:8975';
const PLAYLIST_LIST_PATH = '/api/zen_box/playlists';
const DEFAULT_PLAYLIST_FILE = 'playlist.json';
const PLAYLIST_SNAPSHOT_KEY_PREFIX = '@zen_box_playlist_snapshot:';
const SELECTED_PLAYLIST_KEY = '@zen_box_selected_playlist';
const RESUME_ENABLED_KEY = '@zen_box_resume_enabled';
const PLAYBACK_POSITION_KEY_PREFIX = '@zen_box_playback_position:';
const LOGGED_IN_USER_KEY = '@zen_box_logged_in_user';
const CLOUD_CONFIG_KEY = '@zen_box_cloud_config';
const SORT_PREF_KEY = '@zen_box_sort_pref';
const api = axios.create({
baseURL: API_BASE_URL,
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});
// 网络状态检测
export const isNetworkConnected = async (): Promise<boolean> => {
try {
const state = await NetInfo.fetch();
return state.isConnected ?? false;
} catch {
return false;
}
};
export const formatError = (error: any): string => {
if (error.response) {
return `服务器响应 ${error.response.status}: ${error.response.statusText}`;
} else if (error.request) {
return `服务器无响应: 无法连接到 ${API_BASE_URL}`;
} else if (error.message) {
return `请求错误: ${error.message}`;
} else {
return `未知错误`;
}
};
const normalizePlaylistFile = (fileName?: string): string => {
return String(fileName || DEFAULT_PLAYLIST_FILE).trim() || DEFAULT_PLAYLIST_FILE;
};
const buildPlaylistPath = (fileName?: string): string => {
return `/tools/web_zen_box/${encodeURIComponent(normalizePlaylistFile(fileName))}`;
};
const getPlaylistSnapshotKey = (fileName?: string): string => {
return `${PLAYLIST_SNAPSHOT_KEY_PREFIX}${normalizePlaylistFile(fileName)}`;
};
const getPlaybackPositionKey = (playlistFile?: string, trackSrc?: string): string => {
return `${PLAYBACK_POSITION_KEY_PREFIX}${normalizePlaylistFile(playlistFile)}::${String(trackSrc || '').trim()}`;
};
// ========== 登录相关 ==========
export const login = async (username: string, password: string): Promise<{ ok: boolean; username?: string; error?: string }> => {
try {
const response = await api.post('/api/zen_box/auth/login', { username, password });
if (response.data?.ok) {
await AsyncStorage.setItem(LOGGED_IN_USER_KEY, String(response.data.username || username));
return { ok: true, username: response.data.username || username };
}
return { ok: false, error: '登录失败' };
} catch (error: any) {
const msg = error.response?.data?.error || formatError(error);
return { ok: false, error: msg };
}
};
export const logout = async (): Promise<void> => {
try {
await AsyncStorage.removeItem(LOGGED_IN_USER_KEY);
await AsyncStorage.removeItem(CLOUD_CONFIG_KEY);
} catch (error) {
console.error('登出失败:', error);
}
};
export const getLoggedInUser = async (): Promise<string | null> => {
try {
return await AsyncStorage.getItem(LOGGED_IN_USER_KEY);
} catch {
return null;
}
};
// ========== 云端配置同步 ==========
export const pushCloudConfig = async (config: UserConfig): Promise<void> => {
try {
const user = await getLoggedInUser();
if (!user) return;
const connected = await isNetworkConnected();
if (!connected) {
await AsyncStorage.setItem(CLOUD_CONFIG_KEY, JSON.stringify(config));
return;
}
await api.post('/api/zen_box/config', { username: user, config });
} catch (error) {
console.error('上传云端配置失败:', error);
}
};
export const pullCloudConfig = async (): Promise<UserConfig | null> => {
try {
const user = await getLoggedInUser();
if (!user) return null;
const connected = await isNetworkConnected();
if (!connected) {
const cached = await AsyncStorage.getItem(CLOUD_CONFIG_KEY);
return cached ? JSON.parse(cached) : null;
}
const response = await api.get('/api/zen_box/config', { params: { username: user } });
if (response.data?.ok && response.data?.config) {
await AsyncStorage.setItem(CLOUD_CONFIG_KEY, JSON.stringify(response.data.config));
return response.data.config as UserConfig;
}
return null;
} catch (error) {
console.error('拉取云端配置失败:', error);
const cached = await AsyncStorage.getItem(CLOUD_CONFIG_KEY);
return cached ? JSON.parse(cached) : null;
}
};
// ========== 歌单相关 ==========
export const getPlaylistFiles = async (): Promise<string[]> => {
try {
const connected = await isNetworkConnected();
if (!connected) {
return await getLocalPlaylistFiles();
}
const response = await api.get<{ ok?: boolean; playlists?: string[] }>(PLAYLIST_LIST_PATH);
const raw = Array.isArray(response.data?.playlists) ? response.data.playlists : [];
const files = raw.map(item => String(item || '')).filter(Boolean);
if (!files.includes(DEFAULT_PLAYLIST_FILE)) {
files.unshift(DEFAULT_PLAYLIST_FILE);
}
return files;
} catch (error) {
console.error('读取歌单列表失败:', error);
return await getLocalPlaylistFiles();
}
};
const getLocalPlaylistFiles = async (): Promise<string[]> => {
try {
const allKeys = await AsyncStorage.getAllKeys();
const snapshotKeys = allKeys.filter(k => k.startsWith(PLAYLIST_SNAPSHOT_KEY_PREFIX));
const files = snapshotKeys.map(k => k.replace(PLAYLIST_SNAPSHOT_KEY_PREFIX, ''));
if (!files.includes(DEFAULT_PLAYLIST_FILE)) {
files.unshift(DEFAULT_PLAYLIST_FILE);
}
return files;
} catch {
return [DEFAULT_PLAYLIST_FILE];
}
};
export const getSavedSelectedPlaylist = async (): Promise<string> => {
try {
const value = await AsyncStorage.getItem(SELECTED_PLAYLIST_KEY);
return normalizePlaylistFile(value || DEFAULT_PLAYLIST_FILE);
} catch (error) {
console.error('读取当前歌单失败:', error);
return DEFAULT_PLAYLIST_FILE;
}
};
export const saveSelectedPlaylist = async (fileName: string): Promise<void> => {
try {
await AsyncStorage.setItem(SELECTED_PLAYLIST_KEY, normalizePlaylistFile(fileName));
} catch (error) {
console.error('保存当前歌单失败:', error);
}
};
export const getResumePlaybackEnabled = async (): Promise<boolean> => {
try {
const value = await AsyncStorage.getItem(RESUME_ENABLED_KEY);
if (value === null) {
return true;
}
return value === '1';
} catch (error) {
console.error('读取断点续播设置失败:', error);
return true;
}
};
export const saveResumePlaybackEnabled = async (enabled: boolean): Promise<void> => {
try {
await AsyncStorage.setItem(RESUME_ENABLED_KEY, enabled ? '1' : '0');
} catch (error) {
console.error('保存断点续播设置失败:', error);
}
};
export const getSavedPlaybackPosition = async (playlistFile?: string, trackSrc?: string): Promise<number> => {
try {
const key = getPlaybackPositionKey(playlistFile, trackSrc);
const value = await AsyncStorage.getItem(key);
const seconds = Number(value || 0);
return Number.isFinite(seconds) && seconds > 0 ? seconds : 0;
} catch (error) {
console.error('读取播放进度失败:', error);
return 0;
}
};
export const savePlaybackPosition = async (
playlistFile: string,
trackSrc: string,
positionSeconds: number
): Promise<void> => {
try {
const key = getPlaybackPositionKey(playlistFile, trackSrc);
const safeSeconds = Math.max(0, Math.floor(positionSeconds || 0));
await AsyncStorage.setItem(key, String(safeSeconds));
} catch (error) {
console.error('保存播放进度失败:', error);
}
};
export const clearPlaybackPosition = async (playlistFile: string, trackSrc: string): Promise<void> => {
try {
const key = getPlaybackPositionKey(playlistFile, trackSrc);
await AsyncStorage.removeItem(key);
} catch (error) {
console.error('清除播放进度失败:', error);
}
};
export const getPlaylist = async (fileName?: string): Promise<Track[]> => {
try {
const connected = await isNetworkConnected();
if (!connected) {
throw new Error('网络不可用');
}
const response = await api.get<Track[]>(buildPlaylistPath(fileName));
return response.data;
} catch (error) {
throw new Error(formatError(error));
}
};
const isValidPlaylist = (data: unknown): data is Track[] => {
return Array.isArray(data) && data.every((item) => {
if (!item || typeof item !== 'object') {
return false;
}
const track = item as Track;
return typeof track.title === 'string'
&& typeof track.artist === 'string'
&& typeof track.src === 'string';
});
};
export const savePlaylistSnapshot = async (tracks: Track[], fileName?: string): Promise<void> => {
try {
await AsyncStorage.setItem(getPlaylistSnapshotKey(fileName), JSON.stringify(tracks));
} catch (error) {
console.error('保存歌单快照失败:', error);
}
};
export const getPlaylistSnapshot = async (fileName?: string): Promise<Track[]> => {
try {
const raw = await AsyncStorage.getItem(getPlaylistSnapshotKey(fileName));
if (!raw) {
return [];
}
const parsed: unknown = JSON.parse(raw);
return isValidPlaylist(parsed) ? parsed : [];
} catch (error) {
console.error('读取歌单快照失败:', error);
return [];
}
};
export const getPlaylistWithFallback = async (fileName?: string): Promise<{
tracks: Track[];
source: 'remote' | 'snapshot';
fallbackReason?: string;
playlistFile: string;
}> => {
const normalizedFile = normalizePlaylistFile(fileName);
try {
const tracks = await getPlaylist(normalizedFile);
await savePlaylistSnapshot(tracks, normalizedFile);
return { tracks, source: 'remote', playlistFile: normalizedFile };
} catch (error) {
const fallbackTracks = await getPlaylistSnapshot(normalizedFile);
if (fallbackTracks.length > 0) {
return {
tracks: fallbackTracks,
source: 'snapshot',
fallbackReason: error instanceof Error ? error.message : '远程歌单加载失败',
playlistFile: normalizedFile,
};
}
throw error;
}
};
// ========== 离线歌单反推 ==========
export const getOfflineAvailablePlaylists = async (
isCached: (src: string) => boolean
): Promise<string[]> => {
const allKeys = await AsyncStorage.getAllKeys();
const snapshotKeys = allKeys.filter(k => k.startsWith(PLAYLIST_SNAPSHOT_KEY_PREFIX));
const available: string[] = [];
for (const key of snapshotKeys) {
const fileName = key.replace(PLAYLIST_SNAPSHOT_KEY_PREFIX, '');
const snapshot = await getPlaylistSnapshot(fileName);
if (!snapshot.length) continue;
const cachedCount = snapshot.filter(track => isCached(track.src)).length;
if (cachedCount > 0) {
available.push(fileName);
}
}
if (!available.includes(DEFAULT_PLAYLIST_FILE)) {
available.unshift(DEFAULT_PLAYLIST_FILE);
}
return available;
};
// ========== 排序偏好 ==========
export const getSortPreference = async (): Promise<SortMode> => {
try {
const saved = await AsyncStorage.getItem(SORT_PREF_KEY);
if (saved && ['original', 'asc', 'desc'].includes(saved)) {
return saved as SortMode;
}
return 'original';
} catch {
return 'original';
}
};
export const saveSortPreference = async (mode: SortMode): Promise<void> => {
try {
await AsyncStorage.setItem(SORT_PREF_KEY, mode);
} catch (error) {
console.error('保存排序偏好失败:', error);
}
};