feat: 添加SECTL云服务集成功能

实现SECTL云服务的全面集成,包括:
1. 添加OAuth认证服务,支持PKCE流程
2. 实现云存储管理功能
3. 添加积分数据同步服务
4. 实现通知服务和KV存储服务
5. 添加多语言支持
6. 创建相关React组件和上下文
7. 更新依赖项添加crypto-js
8. 优化HTTP客户端重用
This commit is contained in:
Yukino_fox
2026-04-10 21:47:24 +08:00
parent 448df1a40b
commit bf9cb9af41
27 changed files with 8447 additions and 8 deletions
+300
View File
@@ -0,0 +1,300 @@
/**
* SECTL 积分数据同步服务
* 使用 KV 存储同步 SecScore 的积分数据
*/
import { sectlKVStorage } from "./sectlKVStorage"
import { sectlAuth } from "./sectlAuth"
// 数据结构定义
export interface ScoreData {
studentId: string
studentName: string
currentScore: number
lastUpdated: string
}
export interface ScoreEvent {
id: string
studentId: string
scoreChange: number
reason: string
createdAt: string
}
export interface SyncedData {
version: string
lastSyncTime: string
students: ScoreData[]
events: ScoreEvent[]
settings: {
reasons: any[]
tags: any[]
}
}
// 数据键名
const DATA_KEYS = {
STUDENTS: "secscore_students",
EVENTS: "secscore_events",
SETTINGS: "secscore_settings",
SYNC_META: "secscore_sync_metadata",
}
class ScoreSyncService {
/**
* 同步学生数据到云端
*/
async syncStudents(students: any[]): Promise<void> {
if (!sectlAuth.isAuthenticated()) {
throw new Error("未授权,请先登录")
}
try {
// 转换数据格式
const scoreData: ScoreData[] = students.map((student) => ({
studentId: student.id,
studentName: student.name,
currentScore: student.score || 0,
lastUpdated: new Date().toISOString(),
}))
// 保存到云端
await sectlKVStorage.setKV(DATA_KEYS.STUDENTS, scoreData, {
is_json: true,
})
console.log("学生数据同步成功")
} catch (error: any) {
console.error("同步学生数据失败:", error)
throw new Error(`同步失败:${error.message}`)
}
}
/**
* 从云端加载学生数据
*/
async loadStudents(): Promise<ScoreData[]> {
if (!sectlAuth.isAuthenticated()) {
throw new Error("未授权,请先登录")
}
try {
const result = await sectlKVStorage.getKV<ScoreData[]>(DATA_KEYS.STUDENTS)
return result.value || []
} catch (error: any) {
if (error.message.includes("不存在")) {
return []
}
throw error
}
}
/**
* 同步积分事件到云端
*/
async syncEvents(events: any[]): Promise<void> {
if (!sectlAuth.isAuthenticated()) {
throw new Error("未授权,请先登录")
}
try {
const scoreEvents: ScoreEvent[] = events.map((event) => ({
id: event.id,
studentId: event.student_id,
scoreChange: event.score_change,
reason: event.reason,
createdAt: event.created_at,
}))
await sectlKVStorage.setKV(DATA_KEYS.EVENTS, scoreEvents, {
is_json: true,
})
console.log("积分事件同步成功")
} catch (error: any) {
console.error("同步积分事件失败:", error)
throw new Error(`同步失败:${error.message}`)
}
}
/**
* 从云端加载积分事件
*/
async loadEvents(): Promise<ScoreEvent[]> {
if (!sectlAuth.isAuthenticated()) {
throw new Error("未授权,请先登录")
}
try {
const result = await sectlKVStorage.getKV<ScoreEvent[]>(DATA_KEYS.EVENTS)
return result.value || []
} catch (error: any) {
if (error.message.includes("不存在")) {
return []
}
throw error
}
}
/**
* 同步设置数据(理由、标签等)
*/
async syncSettings(settings: { reasons: any[]; tags: any[] }): Promise<void> {
if (!sectlAuth.isAuthenticated()) {
throw new Error("未授权,请先登录")
}
try {
await sectlKVStorage.setKV(DATA_KEYS.SETTINGS, settings, {
is_json: true,
})
console.log("设置数据同步成功")
} catch (error: any) {
console.error("同步设置数据失败:", error)
throw new Error(`同步失败:${error.message}`)
}
}
/**
* 从云端加载设置数据
*/
async loadSettings(): Promise<{ reasons: any[]; tags: any[] }> {
if (!sectlAuth.isAuthenticated()) {
throw new Error("未授权,请先登录")
}
try {
const result = await sectlKVStorage.getKV(DATA_KEYS.SETTINGS)
return result.value || { reasons: [], tags: [] }
} catch (error: any) {
if (error.message.includes("不存在")) {
return { reasons: [], tags: [] }
}
throw error
}
}
/**
* 更新同步元数据
*/
async updateSyncMetadata(): Promise<void> {
const metadata = {
version: "1.0",
lastSyncTime: new Date().toISOString(),
platform: "secscore",
}
await sectlKVStorage.setKV(DATA_KEYS.SYNC_META, metadata, {
is_json: true,
})
}
/**
* 获取同步元数据
*/
async getSyncMetadata(): Promise<{
version: string
lastSyncTime: string
platform: string
} | null> {
try {
const result = await sectlKVStorage.getKV(DATA_KEYS.SYNC_META)
return result.value
} catch {
return null
}
}
/**
* 完整同步(所有数据)
*/
async fullSync(data: {
students: any[]
events: any[]
settings: { reasons: any[]; tags: any[] }
}): Promise<void> {
if (!sectlAuth.isAuthenticated()) {
throw new Error("未授权,请先登录")
}
try {
await this.syncStudents(data.students)
await this.syncEvents(data.events)
await this.syncSettings(data.settings)
await this.updateSyncMetadata()
console.log("完整数据同步成功")
} catch (error: any) {
console.error("完整数据同步失败:", error)
throw error
}
}
/**
* 从云端恢复所有数据
*/
async restoreFromCloud(): Promise<{
students: ScoreData[]
events: ScoreEvent[]
settings: { reasons: any[]; tags: any[] }
metadata: any
}> {
if (!sectlAuth.isAuthenticated()) {
throw new Error("未授权,请先登录")
}
try {
const [students, events, settings, metadata] = await Promise.all([
this.loadStudents(),
this.loadEvents(),
this.loadSettings(),
this.getSyncMetadata(),
])
return { students, events, settings, metadata }
} catch (error: any) {
console.error("从云端恢复数据失败:", error)
throw error
}
}
/**
* 检查云端是否有数据
*/
async hasCloudData(): Promise<boolean> {
try {
const students = await this.loadStudents()
return students.length > 0
} catch {
return false
}
}
/**
* 清除云端数据
*/
async clearCloudData(): Promise<void> {
if (!sectlAuth.isAuthenticated()) {
throw new Error("未授权,请先登录")
}
try {
await Promise.all([
sectlKVStorage.deleteKV(DATA_KEYS.STUDENTS),
sectlKVStorage.deleteKV(DATA_KEYS.EVENTS),
sectlKVStorage.deleteKV(DATA_KEYS.SETTINGS),
sectlKVStorage.deleteKV(DATA_KEYS.SYNC_META),
])
console.log("云端数据已清除")
} catch (error: any) {
console.error("清除云端数据失败:", error)
throw error
}
}
}
// 导出单例
export const scoreSyncService = new ScoreSyncService()
+24
View File
@@ -0,0 +1,24 @@
/**
* SECTL 服务统一导出
*/
export { sectlAuth, SECTL_CONFIG } from "./sectlAuth"
export type { TokenData, UserInfo } from "./sectlAuth"
export { sectlCloudStorage } from "./sectlCloudStorage"
export type { CloudFile, ShareLink, StorageUsage } from "./sectlCloudStorage"
export { sectlKVStorage } from "./sectlKVStorage"
export type { KVData } from "./sectlCloudStorage"
export { sectlNotification } from "./sectlNotification"
export type { Notification, SendNotificationParams } from "./sectlNotification"
// 积分数据同步服务
export { scoreSyncService } from "./scoreSyncService"
export type { ScoreData, ScoreEvent, SyncedData } from "./scoreSyncService"
+388
View File
@@ -0,0 +1,388 @@
/**
* SECTL OAuth 2.0 认证服务
* 基于 SECTL-One-Stop 项目的 OAuth API 实现
* 支持 PKCE (Proof Key for Code Exchange) 流程
*/
import { Client, Account } from "appwrite"
import CryptoJS from "crypto-js"
// SECTL API 配置
export const SECTL_CONFIG = {
baseUrl: "https://appwrite.sectl.top",
authUrl: "https://sectl.top",
platformId: "", // 需要在设置中配置
callbackUrl: "http://localhost:5173/auth/callback",
callbackPort: 5173,
}
// Token 数据类型
export interface TokenData {
access_token: string
refresh_token: string
token_type: string
expires_in: number
user_id: string
}
// 用户信息类型
export interface UserInfo {
user_id: string
email: string
name: string
github_username?: string
permission: string
role: string
avatar_url?: string
background_url?: string
bio: string
tags: string[]
platform_id?: string
login_time?: string
}
// PKCE 相关工具函数
function generateCodeVerifier(): string {
const array = new Uint8Array(32)
crypto.getRandomValues(array)
return base64URLEncode(array)
}
function generateCodeChallenge(verifier: string): string {
const sha256 = CryptoJS.SHA256(verifier)
return base64URLEncode(CryptoJS.enc.Base64.parse(sha256.toString(CryptoJS.enc.Base64)))
}
function base64URLEncode(buffer: Uint8Array | CryptoJS.lib.WordArray): string {
let base64: string
if (buffer instanceof Uint8Array) {
base64 = btoa(String.fromCharCode(...buffer))
} else {
base64 = buffer.toString(CryptoJS.enc.Base64)
}
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "")
}
class SectlAuthService {
private client: Client
private tokenData: TokenData | null = null
private codeVerifier: string = ""
constructor() {
this.client = new Client()
new Account(this.client)
this.loadToken()
}
/**
* 初始化客户端配置
*/
initialize(platformId: string, callbackUrl?: string) {
SECTL_CONFIG.platformId = platformId
if (callbackUrl) {
SECTL_CONFIG.callbackUrl = callbackUrl
}
this.client.setEndpoint(SECTL_CONFIG.baseUrl).setProject("sectl-cloud") // 项目 ID
return this
}
/**
* 生成授权 URL
*/
getAuthorizationUrl(scope: string[] = ["user:read"]): string {
this.codeVerifier = generateCodeVerifier()
const codeChallenge = generateCodeChallenge(this.codeVerifier)
const params = new URLSearchParams({
client_id: SECTL_CONFIG.platformId,
redirect_uri: SECTL_CONFIG.callbackUrl,
response_type: "code",
code_challenge: codeChallenge,
code_challenge_method: "S256",
scope: scope.join(" "),
})
return `${SECTL_CONFIG.authUrl}/oauth/authorize?${params.toString()}`
}
/**
* 执行完整的 OAuth 授权流程
*/
async authorize(scope: string[] = ["user:read"]): Promise<TokenData> {
// 生成 PKCE 参数
this.codeVerifier = generateCodeVerifier()
// codeChallenge 在 getAuthorizationUrl 中生成
// 构建授权 URL
const authUrl = this.getAuthorizationUrl(scope)
// 在新窗口中打开授权页面
const authWindow = window.open(authUrl, "_blank", "width=600,height=700")
// 等待回调
return new Promise((resolve, reject) => {
// 监听授权回调
const checkCallback = async () => {
try {
// 检查是否有授权码(通过 postMessage 或 URL 参数)
window.addEventListener("message", async (event) => {
if (event.data.type === "oauth-callback" && event.data.code) {
try {
const token = await this.exchangeCodeForToken(event.data.code, scope)
authWindow?.close()
resolve(token)
} catch (error) {
reject(error)
}
}
})
// 轮询检查窗口关闭
const checkInterval = setInterval(() => {
if (authWindow?.closed) {
clearInterval(checkInterval)
reject(new Error("授权窗口已关闭"))
}
}, 1000)
} catch (error) {
reject(error)
}
}
checkCallback()
})
}
/**
* 使用授权码换取 Token
*/
async exchangeCodeForToken(code: string, scope: string[] = ["user:read"]): Promise<TokenData> {
const url = `${SECTL_CONFIG.baseUrl}/api/oauth/token`
const payload = {
grant_type: "authorization_code",
code,
client_id: SECTL_CONFIG.platformId,
redirect_uri: SECTL_CONFIG.callbackUrl,
code_verifier: this.codeVerifier,
scope: scope.join(" "),
}
try {
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error_description || "Token 交换失败")
}
const data: TokenData = await response.json()
this.tokenData = data
this.saveToken()
return data
} catch (error) {
console.error("Token 交换失败:", error)
throw error
}
}
/**
* 获取用户信息
*/
async getUserInfo(): Promise<UserInfo> {
if (!this.tokenData?.access_token) {
throw new Error("未授权,请先登录")
}
const url = `${SECTL_CONFIG.baseUrl}/api/oauth/userinfo`
try {
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${this.tokenData.access_token}`,
},
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error_description || "获取用户信息失败")
}
return await response.json()
} catch (error) {
console.error("获取用户信息失败:", error)
throw error
}
}
/**
* 验证 Token
*/
async introspectToken(token?: string): Promise<{ active: boolean; user_id?: string }> {
const tokenToCheck = token || this.tokenData?.access_token
if (!tokenToCheck) {
return { active: false }
}
const url = `${SECTL_CONFIG.baseUrl}/api/oauth/introspect`
try {
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
token: tokenToCheck,
client_id: SECTL_CONFIG.platformId,
}),
})
if (!response.ok) {
return { active: false }
}
return await response.json()
} catch (error) {
console.error("Token 验证失败:", error)
return { active: false }
}
}
/**
* 刷新 Token
*/
async refreshAccessToken(): Promise<TokenData> {
if (!this.tokenData?.refresh_token) {
throw new Error("没有刷新令牌")
}
const url = `${SECTL_CONFIG.baseUrl}/api/oauth/token`
const payload = {
grant_type: "refresh_token",
refresh_token: this.tokenData.refresh_token,
client_id: SECTL_CONFIG.platformId,
}
try {
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error_description || "Token 刷新失败")
}
const data: TokenData = await response.json()
this.tokenData = data
this.saveToken()
return data
} catch (error) {
console.error("Token 刷新失败:", error)
throw error
}
}
/**
* 登出(撤销 Token
*/
async logout(): Promise<void> {
if (!this.tokenData?.access_token) {
return
}
const url = `${SECTL_CONFIG.baseUrl}/api/oauth/logout`
try {
await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${this.tokenData.access_token}`,
},
})
} catch (error) {
console.error("登出失败:", error)
} finally {
this.tokenData = null
this.clearToken()
}
}
/**
* 获取当前 Token
*/
getToken(): TokenData | null {
return this.tokenData
}
/**
* 获取 Access Token
*/
getAccessToken(): string | null {
return this.tokenData?.access_token || null
}
/**
* 检查是否已授权
*/
isAuthenticated(): boolean {
return !!this.tokenData?.access_token
}
/**
* 保存 Token 到本地存储
*/
private saveToken(): void {
if (this.tokenData) {
localStorage.setItem("sectl_token", JSON.stringify(this.tokenData))
localStorage.setItem("sectl_code_verifier", this.codeVerifier)
}
}
/**
* 从本地存储加载 Token
*/
private loadToken(): void {
try {
const tokenStr = localStorage.getItem("sectl_token")
const codeVerifier = localStorage.getItem("sectl_code_verifier")
if (tokenStr) {
this.tokenData = JSON.parse(tokenStr)
this.codeVerifier = codeVerifier || ""
}
} catch (error) {
console.error("加载 Token 失败:", error)
}
}
/**
* 清除 Token
*/
private clearToken(): void {
localStorage.removeItem("sectl_token")
localStorage.removeItem("sectl_code_verifier")
this.tokenData = null
this.codeVerifier = ""
}
}
// 导出单例
export const sectlAuth = new SectlAuthService()
+596
View File
@@ -0,0 +1,596 @@
/**
* SECTL 云存储服务
* 提供文件上传、下载、管理、分享等功能
*/
import { SECTL_CONFIG, sectlAuth } from "./sectlAuth"
// 文件信息类型
export interface CloudFile {
file_id: string
filename: string
extension: string
size: number
size_formatted: string
created_at: string
updated_at: string
url: string
thumbnail_url?: string
}
// 分享信息类型
export interface ShareLink {
share_id: string
share_url: string
file_id: string
filename: string
expires_at?: string
has_password: boolean
view_count?: number
download_count?: number
status: "active" | "disabled" | "expired"
}
// KV 存储类型
export interface KVData {
kv_id: string
key: string
value: any
is_json: boolean
size: number
created_at: string
updated_at: string
}
// 存储使用情况
export interface StorageUsage {
used_storage: number
used_storage_formatted: string
total_storage: number
total_storage_formatted: string
available_storage: number
available_storage_formatted: string
percentage: number
file_count: number
}
class SectlCloudStorageService {
/**
* 获取文件列表
*/
async listFiles(options?: {
folder_id?: string
limit?: number
offset?: number
}): Promise<{ files: CloudFile[]; total: number; has_more: boolean }> {
const accessToken = sectlAuth.getAccessToken()
if (!accessToken) {
throw new Error("未授权,请先登录")
}
const params = new URLSearchParams({
client_id: SECTL_CONFIG.platformId,
user_id: sectlAuth.getToken()?.user_id || "",
limit: String(options?.limit || 100),
offset: String(options?.offset || 0),
})
if (options?.folder_id) {
params.append("folder_id", options.folder_id)
}
const url = `${SECTL_CONFIG.baseUrl}/api/cloud/files?${params.toString()}`
try {
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error_description || "获取文件列表失败")
}
return await response.json()
} catch (error) {
console.error("获取文件列表失败:", error)
throw error
}
}
/**
* 上传文件
*/
async uploadFile(
file: File | Blob,
options?: {
folder_id?: string
filename?: string
}
): Promise<{
success: boolean
file_id: string
filename: string
size: number
size_formatted: string
}> {
const accessToken = sectlAuth.getAccessToken()
if (!accessToken) {
throw new Error("未授权,请先登录")
}
const formData = new FormData()
formData.append("client_id", SECTL_CONFIG.platformId)
formData.append("user_id", sectlAuth.getToken()?.user_id || "")
if (options?.folder_id) {
formData.append("folder_id", options.folder_id)
}
// 如果是 File 对象,直接使用;否则需要指定文件名
if (file instanceof File) {
formData.append("file", file)
} else {
const filename = options?.filename || "upload_" + Date.now()
formData.append("file", file, filename)
}
const url = `${SECTL_CONFIG.baseUrl}/api/cloud/upload`
try {
const response = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
},
body: formData,
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error_description || "文件上传失败")
}
return await response.json()
} catch (error) {
console.error("文件上传失败:", error)
throw error
}
}
/**
* 上传文件(Base64 格式)
*/
async uploadFileBase64(
base64Data: string,
filename: string,
mimeType: string = "application/octet-stream",
options?: {
folder_id?: string
}
): Promise<{
success: boolean
file_id: string
filename: string
size: number
}> {
const accessToken = sectlAuth.getAccessToken()
if (!accessToken) {
throw new Error("未授权,请先登录")
}
const url = `${SECTL_CONFIG.baseUrl}/api/cloud/upload`
const payload = {
client_id: SECTL_CONFIG.platformId,
user_id: sectlAuth.getToken()?.user_id || "",
filename,
mime_type: mimeType,
file: base64Data,
folder_id: options?.folder_id,
}
try {
const response = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error_description || "文件上传失败")
}
return await response.json()
} catch (error) {
console.error("文件上传失败:", error)
throw error
}
}
/**
* 获取文件下载链接
*/
async downloadFile(fileId: string): Promise<{ download_url: string; expires_at: string }> {
const accessToken = sectlAuth.getAccessToken()
if (!accessToken) {
throw new Error("未授权,请先登录")
}
const params = new URLSearchParams({
client_id: SECTL_CONFIG.platformId,
})
const url = `${SECTL_CONFIG.baseUrl}/api/cloud/files/${fileId}/download?${params.toString()}`
try {
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error_description || "获取下载链接失败")
}
return await response.json()
} catch (error) {
console.error("获取下载链接失败:", error)
throw error
}
}
/**
* 获取文件预览链接
*/
async previewFile(fileId: string): Promise<{ view_url: string; expires_at: string }> {
const accessToken = sectlAuth.getAccessToken()
if (!accessToken) {
throw new Error("未授权,请先登录")
}
const params = new URLSearchParams({
client_id: SECTL_CONFIG.platformId,
})
const url = `${SECTL_CONFIG.baseUrl}/api/cloud/files/${fileId}/preview?${params.toString()}`
try {
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error_description || "获取预览链接失败")
}
return await response.json()
} catch (error) {
console.error("获取预览链接失败:", error)
throw error
}
}
/**
* 删除文件
*/
async deleteFile(fileId: string): Promise<{ success: boolean; message: string }> {
const accessToken = sectlAuth.getAccessToken()
if (!accessToken) {
throw new Error("未授权,请先登录")
}
const url = `${SECTL_CONFIG.baseUrl}/api/cloud/files/${fileId}`
try {
const response = await fetch(url, {
method: "DELETE",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
client_id: SECTL_CONFIG.platformId,
user_id: sectlAuth.getToken()?.user_id || "",
}),
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error_description || "删除文件失败")
}
return await response.json()
} catch (error) {
console.error("删除文件失败:", error)
throw error
}
}
/**
* 重命名文件
*/
async renameFile(
fileId: string,
newFilename: string
): Promise<{
file_id: string
filename: string
updated_at: string
}> {
const accessToken = sectlAuth.getAccessToken()
if (!accessToken) {
throw new Error("未授权,请先登录")
}
const url = `${SECTL_CONFIG.baseUrl}/api/cloud/files/${fileId}`
try {
const response = await fetch(url, {
method: "PUT",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
client_id: SECTL_CONFIG.platformId,
user_id: sectlAuth.getToken()?.user_id || "",
filename: newFilename,
}),
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error_description || "重命名文件失败")
}
return await response.json()
} catch (error) {
console.error("重命名文件失败:", error)
throw error
}
}
/**
* 创建分享链接
*/
async createShare(
fileId: string,
options?: {
expires_in?: number // 秒,0 表示永久
password?: string
}
): Promise<{
share_id: string
share_url: string
file_id: string
filename: string
expires_at?: string
has_password: boolean
}> {
const accessToken = sectlAuth.getAccessToken()
if (!accessToken) {
throw new Error("未授权,请先登录")
}
const url = `${SECTL_CONFIG.baseUrl}/api/cloud/share`
try {
const response = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
client_id: SECTL_CONFIG.platformId,
user_id: sectlAuth.getToken()?.user_id || "",
file_id: fileId,
expires_in: options?.expires_in || 86400, // 默认 1 天
password: options?.password,
}),
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error_description || "创建分享失败")
}
return await response.json()
} catch (error) {
console.error("创建分享失败:", error)
throw error
}
}
/**
* 获取分享列表
*/
async listShares(): Promise<{ shares: ShareLink[]; total: number }> {
const accessToken = sectlAuth.getAccessToken()
if (!accessToken) {
throw new Error("未授权,请先登录")
}
const params = new URLSearchParams({
client_id: SECTL_CONFIG.platformId,
user_id: sectlAuth.getToken()?.user_id || "",
})
const url = `${SECTL_CONFIG.baseUrl}/api/cloud/shares?${params.toString()}`
try {
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error_description || "获取分享列表失败")
}
return await response.json()
} catch (error) {
console.error("获取分享列表失败:", error)
throw error
}
}
/**
* 禁用分享
*/
async disableShare(shareId: string): Promise<{ success: boolean; message: string }> {
const accessToken = sectlAuth.getAccessToken()
if (!accessToken) {
throw new Error("未授权,请先登录")
}
const url = `${SECTL_CONFIG.baseUrl}/api/cloud/shares/${shareId}/disable`
try {
const response = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
client_id: SECTL_CONFIG.platformId,
user_id: sectlAuth.getToken()?.user_id || "",
}),
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error_description || "禁用分享失败")
}
return await response.json()
} catch (error) {
console.error("禁用分享失败:", error)
throw error
}
}
/**
* 启用分享
*/
async enableShare(shareId: string): Promise<{ success: boolean; message: string }> {
const accessToken = sectlAuth.getAccessToken()
if (!accessToken) {
throw new Error("未授权,请先登录")
}
const url = `${SECTL_CONFIG.baseUrl}/api/cloud/shares/${shareId}/enable`
try {
const response = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
client_id: SECTL_CONFIG.platformId,
user_id: sectlAuth.getToken()?.user_id || "",
}),
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error_description || "启用分享失败")
}
return await response.json()
} catch (error) {
console.error("启用分享失败:", error)
throw error
}
}
/**
* 删除分享
*/
async deleteShare(shareId: string): Promise<{ success: boolean; message: string }> {
const accessToken = sectlAuth.getAccessToken()
if (!accessToken) {
throw new Error("未授权,请先登录")
}
const url = `${SECTL_CONFIG.baseUrl}/api/cloud/shares/${shareId}`
try {
const response = await fetch(url, {
method: "DELETE",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
client_id: SECTL_CONFIG.platformId,
user_id: sectlAuth.getToken()?.user_id || "",
}),
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error_description || "删除分享失败")
}
return await response.json()
} catch (error) {
console.error("删除分享失败:", error)
throw error
}
}
/**
* 获取存储使用情况
*/
async getStorageUsage(): Promise<StorageUsage> {
const accessToken = sectlAuth.getAccessToken()
if (!accessToken) {
throw new Error("未授权,请先登录")
}
const params = new URLSearchParams({
client_id: SECTL_CONFIG.platformId,
user_id: sectlAuth.getToken()?.user_id || "",
})
const url = `${SECTL_CONFIG.baseUrl}/api/cloud/storage/usage?${params.toString()}`
try {
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error_description || "获取存储使用情况失败")
}
return await response.json()
} catch (error) {
console.error("获取存储使用情况失败:", error)
throw error
}
}
}
// 导出单例
export const sectlCloudStorage = new SectlCloudStorageService()
+256
View File
@@ -0,0 +1,256 @@
/**
* SECTL KV 键值对存储服务
* 提供键值对存储功能,支持 JSON 格式和字段级操作
*/
import { SECTL_CONFIG, sectlAuth } from "./sectlAuth"
import { KVData } from "./sectlCloudStorage"
class SectlKVStorageService {
/**
* 创建或更新键值对
*/
async setKV(
key: string,
value: any,
options?: {
is_json?: boolean
}
): Promise<{
success: boolean
kv_id: string
key: string
value: any
is_json: boolean
size: number
created_at: string
updated_at: string
}> {
const accessToken = sectlAuth.getAccessToken()
if (!accessToken) {
throw new Error("未授权,请先登录")
}
const url = `${SECTL_CONFIG.baseUrl}/api/cloud/kv`
// 自动检测是否为 JSON 格式
let valueStr: string
let isJson = options?.is_json
if (typeof value === "object") {
valueStr = JSON.stringify(value)
if (isJson === undefined) {
isJson = true
}
} else {
valueStr = String(value)
if (isJson === undefined) {
// 尝试检测是否为 JSON 字符串
try {
JSON.parse(valueStr)
isJson = true
} catch {
isJson = false
}
}
}
try {
const response = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
client_id: SECTL_CONFIG.platformId,
user_id: sectlAuth.getToken()?.user_id || "",
key,
value: valueStr,
is_json: isJson,
}),
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error_description || "设置键值对失败")
}
return await response.json()
} catch (error) {
console.error("设置键值对失败:", error)
throw error
}
}
/**
* 获取键值对
*/
async getKV<T = any>(
key: string,
options?: {
field?: string // 获取 JSON 中的特定字段
}
): Promise<KVData | { key: string; field: string; value: T }> {
const accessToken = sectlAuth.getAccessToken()
if (!accessToken) {
throw new Error("未授权,请先登录")
}
const params = new URLSearchParams({
client_id: SECTL_CONFIG.platformId,
user_id: sectlAuth.getToken()?.user_id || "",
})
if (options?.field) {
params.append("field", options.field)
}
const url = `${SECTL_CONFIG.baseUrl}/api/cloud/kv/${encodeURIComponent(key)}?${params.toString()}`
try {
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error_description || "获取键值对失败")
}
return await response.json()
} catch (error) {
console.error("获取键值对失败:", error)
throw error
}
}
/**
* 获取键值对列表
*/
async listKV(options?: {
limit?: number
offset?: number
}): Promise<{ kvs: KVData[]; total: number; has_more: boolean }> {
const accessToken = sectlAuth.getAccessToken()
if (!accessToken) {
throw new Error("未授权,请先登录")
}
const params = new URLSearchParams({
client_id: SECTL_CONFIG.platformId,
user_id: sectlAuth.getToken()?.user_id || "",
limit: String(options?.limit || 100),
offset: String(options?.offset || 0),
})
const url = `${SECTL_CONFIG.baseUrl}/api/cloud/kv?${params.toString()}`
try {
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error_description || "获取键值对列表失败")
}
return await response.json()
} catch (error) {
console.error("获取键值对列表失败:", error)
throw error
}
}
/**
* 更新 JSON 字段
*/
async updateKVField(
key: string,
field: string,
value: any
): Promise<{
success: boolean
key: string
field: string
value: any
updated_at: string
}> {
const accessToken = sectlAuth.getAccessToken()
if (!accessToken) {
throw new Error("未授权,请先登录")
}
const url = `${SECTL_CONFIG.baseUrl}/api/cloud/kv/${encodeURIComponent(key)}`
try {
const response = await fetch(url, {
method: "PATCH",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
client_id: SECTL_CONFIG.platformId,
user_id: sectlAuth.getToken()?.user_id || "",
field,
value,
}),
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error_description || "更新字段失败")
}
return await response.json()
} catch (error) {
console.error("更新字段失败:", error)
throw error
}
}
/**
* 删除键值对
*/
async deleteKV(key: string): Promise<{ success: boolean; message: string }> {
const accessToken = sectlAuth.getAccessToken()
if (!accessToken) {
throw new Error("未授权,请先登录")
}
const url = `${SECTL_CONFIG.baseUrl}/api/cloud/kv/${encodeURIComponent(key)}`
try {
const response = await fetch(url, {
method: "DELETE",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
client_id: SECTL_CONFIG.platformId,
user_id: sectlAuth.getToken()?.user_id || "",
}),
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error_description || "删除键值对失败")
}
return await response.json()
} catch (error) {
console.error("删除键值对失败:", error)
throw error
}
}
}
// 导出单例
export const sectlKVStorage = new SectlKVStorageService()
+225
View File
@@ -0,0 +1,225 @@
/**
* SECTL 通知服务
* 提供通知的获取、发送、管理等功能
*/
import { SECTL_CONFIG, sectlAuth } from "./sectlAuth"
// 通知类型
export interface Notification {
$id: string
user_id: string
title: string
content?: string
type: string
source?: string
source_id?: string
is_read: boolean
created_at: string
read_at?: string
action_url?: string
icon?: string
priority: "low" | "normal" | "high"
expires_at?: string
}
// 发送通知参数
export interface SendNotificationParams {
user_id: string
title: string
content?: string
type?: string
source_id?: string
action_url?: string
icon?: string
priority?: "low" | "normal" | "high"
expires_at?: string
}
class SectlNotificationService {
/**
* 获取通知列表
*/
async getNotifications(options?: {
limit?: number
offset?: number
unread_only?: boolean
}): Promise<{ notifications: Notification[]; total: number }> {
const accessToken = sectlAuth.getAccessToken()
if (!accessToken) {
throw new Error("未授权,请先登录")
}
const params = new URLSearchParams({
limit: String(options?.limit || 20),
offset: String(options?.offset || 0),
})
if (options?.unread_only) {
params.append("unread_only", "true")
}
const url = `${SECTL_CONFIG.baseUrl}/api/notifications?${params.toString()}`
try {
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error_description || "获取通知列表失败")
}
return await response.json()
} catch (error) {
console.error("获取通知列表失败:", error)
throw error
}
}
/**
* 标记通知为已读
*/
async markNotificationRead(notificationId: string): Promise<{
success: boolean
notification: {
$id: string
is_read: boolean
read_at: string
}
}> {
const accessToken = sectlAuth.getAccessToken()
if (!accessToken) {
throw new Error("未授权,请先登录")
}
const url = `${SECTL_CONFIG.baseUrl}/api/notifications/${notificationId}/read`
try {
const response = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
},
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error_description || "标记通知已读失败")
}
return await response.json()
} catch (error) {
console.error("标记通知已读失败:", error)
throw error
}
}
/**
* 标记所有通知为已读
*/
async markAllNotificationsRead(): Promise<{ success: boolean; marked_count: number }> {
const accessToken = sectlAuth.getAccessToken()
if (!accessToken) {
throw new Error("未授权,请先登录")
}
const url = `${SECTL_CONFIG.baseUrl}/api/notifications/read-all`
try {
const response = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
},
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error_description || "标记所有通知已读失败")
}
return await response.json()
} catch (error) {
console.error("标记所有通知已读失败:", error)
throw error
}
}
/**
* 删除通知
*/
async deleteNotification(notificationId: string): Promise<{ success: boolean; message: string }> {
const accessToken = sectlAuth.getAccessToken()
if (!accessToken) {
throw new Error("未授权,请先登录")
}
const url = `${SECTL_CONFIG.baseUrl}/api/notifications/${notificationId}`
try {
const response = await fetch(url, {
method: "DELETE",
headers: {
Authorization: `Bearer ${accessToken}`,
},
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error_description || "删除通知失败")
}
return await response.json()
} catch (error) {
console.error("删除通知失败:", error)
throw error
}
}
/**
* 发送通知(需要平台权限)
*/
async sendNotification(params: SendNotificationParams): Promise<{
success: boolean
notification_id: string
message: string
}> {
const accessToken = sectlAuth.getAccessToken()
if (!accessToken) {
throw new Error("未授权,请先登录")
}
const url = `${SECTL_CONFIG.baseUrl}/api/notifications/send`
try {
const response = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
...params,
client_id: SECTL_CONFIG.platformId,
}),
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error_description || "发送通知失败")
}
return await response.json()
} catch (error) {
console.error("发送通知失败:", error)
throw error
}
}
}
// 导出单例
export const sectlNotification = new SectlNotificationService()