mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-21 20:29:03 +08:00
feat: 添加SECTL云服务集成功能
实现SECTL云服务的全面集成,包括: 1. 添加OAuth认证服务,支持PKCE流程 2. 实现云存储管理功能 3. 添加积分数据同步服务 4. 实现通知服务和KV存储服务 5. 添加多语言支持 6. 创建相关React组件和上下文 7. 更新依赖项添加crypto-js 8. 优化HTTP客户端重用
This commit is contained in:
@@ -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()
|
||||
@@ -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"
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user