mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-21 20:29:03 +08:00
feat(URL协议): 增强URL协议注册健壮性
- 新增check_url_protocol_status命令检测注册状态 - 新增unregister_url_protocol命令取消注册 - 改进register_url_protocol命令增加验证逻辑 - 前端增加状态检测、自动重试和操作日志功能 - 更新多语言翻译支持新功能
This commit is contained in:
@@ -160,7 +160,7 @@ class ScoreSyncService {
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await sectlKVStorage.getKV(DATA_KEYS.SETTINGS)
|
||||
const result = await sectlKVStorage.getKV<{ reasons: any[]; tags: any[] }>(DATA_KEYS.SETTINGS)
|
||||
return result.value || { reasons: [], tags: [] }
|
||||
} catch (error: any) {
|
||||
if (error.message.includes("不存在")) {
|
||||
@@ -192,8 +192,12 @@ class ScoreSyncService {
|
||||
platform: string
|
||||
} | null> {
|
||||
try {
|
||||
const result = await sectlKVStorage.getKV(DATA_KEYS.SYNC_META)
|
||||
return result.value
|
||||
const result = await sectlKVStorage.getKV<{
|
||||
version: string
|
||||
lastSyncTime: string
|
||||
platform: string
|
||||
}>(DATA_KEYS.SYNC_META)
|
||||
return result.value ?? null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -0,0 +1,507 @@
|
||||
import { sectlKVStorage } from "./sectlKVStorage"
|
||||
import { sectlAuth } from "./sectlAuth"
|
||||
|
||||
const SYNC_PREFIX = "ss"
|
||||
|
||||
const SYNC_TABLES = [
|
||||
"students",
|
||||
"reasons",
|
||||
"score_events",
|
||||
"tags",
|
||||
"student_tags",
|
||||
"reward_settings",
|
||||
"reward_redemptions",
|
||||
] as const
|
||||
|
||||
type SyncTable = (typeof SYNC_TABLES)[number]
|
||||
|
||||
interface SyncMetadata {
|
||||
last_sync_at: string
|
||||
table_versions: Record<SyncTable, string>
|
||||
device_id: string
|
||||
}
|
||||
|
||||
interface TableSyncResult {
|
||||
table: SyncTable
|
||||
uploaded: number
|
||||
downloaded: number
|
||||
errors: string[]
|
||||
}
|
||||
|
||||
export interface CloudSyncResult {
|
||||
success: boolean
|
||||
message: string
|
||||
tables: TableSyncResult[]
|
||||
synced_at: string
|
||||
}
|
||||
|
||||
export interface CloudSyncStatus {
|
||||
is_configured: boolean
|
||||
last_sync_at: string | null
|
||||
last_error: string | null
|
||||
table_count: number
|
||||
is_syncing: boolean
|
||||
}
|
||||
|
||||
type SyncDirection = "push" | "pull" | "bidirectional"
|
||||
|
||||
const generateDeviceId = (): string => {
|
||||
const stored = localStorage.getItem("ss_cloud_device_id")
|
||||
if (stored) return stored
|
||||
const id = `dev_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`
|
||||
localStorage.setItem("ss_cloud_device_id", id)
|
||||
return id
|
||||
}
|
||||
|
||||
const buildKey = (table: SyncTable): string => `${SYNC_PREFIX}:${table}`
|
||||
|
||||
const buildMetadataKey = (): string => `${SYNC_PREFIX}:__metadata`
|
||||
|
||||
const safeJsonParse = <T>(value: unknown): T | null => {
|
||||
if (typeof value === "string") {
|
||||
try {
|
||||
return JSON.parse(value) as T
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
if (value && typeof value === "object") return value as T
|
||||
return null
|
||||
}
|
||||
|
||||
class SectlCloudSyncService {
|
||||
private deviceId: string
|
||||
private isSyncing: boolean = false
|
||||
private lastSyncAt: string | null = null
|
||||
private lastError: string | null = null
|
||||
|
||||
constructor() {
|
||||
this.deviceId = generateDeviceId()
|
||||
}
|
||||
|
||||
getStatus(): CloudSyncStatus {
|
||||
const cached = localStorage.getItem("ss_cloud_sync_status")
|
||||
let lastSyncAt = this.lastSyncAt
|
||||
if (!lastSyncAt && cached) {
|
||||
try {
|
||||
const parsed = JSON.parse(cached)
|
||||
lastSyncAt = parsed.last_sync_at || null
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
return {
|
||||
is_configured: sectlAuth.isAuthenticated(),
|
||||
last_sync_at: lastSyncAt,
|
||||
last_error: this.lastError,
|
||||
table_count: SYNC_TABLES.length,
|
||||
is_syncing: this.isSyncing,
|
||||
}
|
||||
}
|
||||
|
||||
private saveStatus() {
|
||||
localStorage.setItem(
|
||||
"ss_cloud_sync_status",
|
||||
JSON.stringify({
|
||||
last_sync_at: this.lastSyncAt,
|
||||
last_error: this.lastError,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
async fetchCloudData(table: SyncTable): Promise<unknown[] | null> {
|
||||
try {
|
||||
const key = buildKey(table)
|
||||
const result = await sectlKVStorage.getKV(key)
|
||||
if (result && "value" in result) {
|
||||
const parsed = safeJsonParse<unknown[]>(result.value)
|
||||
return Array.isArray(parsed) ? parsed : null
|
||||
}
|
||||
return null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async pushTableData(table: SyncTable, data: unknown[]): Promise<number> {
|
||||
try {
|
||||
const key = buildKey(table)
|
||||
const jsonValue = JSON.stringify(data)
|
||||
const result = await sectlKVStorage.setKV(key, jsonValue)
|
||||
return result.success ? data.length : 0
|
||||
} catch {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
async fetchMetadata(): Promise<SyncMetadata | null> {
|
||||
try {
|
||||
const key = buildMetadataKey()
|
||||
const result = await sectlKVStorage.getKV(key)
|
||||
if (result && "value" in result) {
|
||||
return safeJsonParse<SyncMetadata>(result.value)
|
||||
}
|
||||
return null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async pushMetadata(metadata: SyncMetadata): Promise<boolean> {
|
||||
try {
|
||||
const key = buildMetadataKey()
|
||||
const result = await sectlKVStorage.setKV(key, JSON.stringify(metadata))
|
||||
return result.success
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private async getLocalData(table: SyncTable): Promise<unknown[]> {
|
||||
const api = (window as any).api
|
||||
if (!api) return []
|
||||
try {
|
||||
const apiMap: Record<SyncTable, string> = {
|
||||
students: "getStudents",
|
||||
reasons: "getReasons",
|
||||
score_events: "getScoreEvents",
|
||||
tags: "getTags",
|
||||
student_tags: "getStudentTags",
|
||||
reward_settings: "getRewardSettings",
|
||||
reward_redemptions: "getRewardRedemptions",
|
||||
}
|
||||
const methodName = apiMap[table]
|
||||
if (!methodName || !api[methodName]) return []
|
||||
const res = await api[methodName]()
|
||||
return res.success && Array.isArray(res.data) ? res.data : []
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
private async setLocalData(table: SyncTable, data: unknown[]): Promise<boolean> {
|
||||
const api = (window as any).api
|
||||
if (!api) return false
|
||||
try {
|
||||
const methodName = `import${table.charAt(0).toUpperCase() + table.slice(1)}Data`
|
||||
if (api[methodName]) {
|
||||
const res = await api[methodName](JSON.stringify(data))
|
||||
return res.success
|
||||
}
|
||||
const res = await api.importTableData(table, JSON.stringify(data))
|
||||
return res.success
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async syncToCloud(localDataMap?: Map<SyncTable, unknown[]>): Promise<CloudSyncResult> {
|
||||
if (this.isSyncing) {
|
||||
return {
|
||||
success: false,
|
||||
message: "同步正在进行中",
|
||||
tables: [],
|
||||
synced_at: new Date().toISOString(),
|
||||
}
|
||||
}
|
||||
|
||||
if (!sectlAuth.isAuthenticated()) {
|
||||
return {
|
||||
success: false,
|
||||
message: "请先登录 SECTL Auth 账户",
|
||||
tables: [],
|
||||
synced_at: new Date().toISOString(),
|
||||
}
|
||||
}
|
||||
|
||||
this.isSyncing = true
|
||||
const results: TableSyncResult[] = []
|
||||
const syncedAt = new Date().toISOString()
|
||||
|
||||
try {
|
||||
for (const table of SYNC_TABLES) {
|
||||
const tableResult: TableSyncResult = {
|
||||
table,
|
||||
uploaded: 0,
|
||||
downloaded: 0,
|
||||
errors: [],
|
||||
}
|
||||
|
||||
try {
|
||||
const localData = localDataMap?.get(table) ?? (await this.getLocalData(table))
|
||||
const uploaded = await this.pushTableData(table, localData)
|
||||
tableResult.uploaded = uploaded
|
||||
} catch (err: unknown) {
|
||||
tableResult.errors.push(err instanceof Error ? err.message : String(err))
|
||||
}
|
||||
|
||||
results.push(tableResult)
|
||||
}
|
||||
|
||||
const metadata: SyncMetadata = {
|
||||
last_sync_at: syncedAt,
|
||||
table_versions: Object.fromEntries(SYNC_TABLES.map((t) => [t, syncedAt])) as Record<
|
||||
SyncTable,
|
||||
string
|
||||
>,
|
||||
device_id: this.deviceId,
|
||||
}
|
||||
await this.pushMetadata(metadata)
|
||||
|
||||
this.lastSyncAt = syncedAt
|
||||
this.lastError = null
|
||||
this.saveStatus()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `已上传 ${results.reduce((s, r) => s + r.uploaded, 0)} 条记录到云端`,
|
||||
tables: results,
|
||||
synced_at: syncedAt,
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : String(err)
|
||||
this.lastError = msg
|
||||
this.saveStatus()
|
||||
return {
|
||||
success: false,
|
||||
message: `同步失败:${msg}`,
|
||||
tables: results,
|
||||
synced_at: syncedAt,
|
||||
}
|
||||
} finally {
|
||||
this.isSyncing = false
|
||||
}
|
||||
}
|
||||
|
||||
async syncFromCloud(): Promise<CloudSyncResult> {
|
||||
if (this.isSyncing) {
|
||||
return {
|
||||
success: false,
|
||||
message: "同步正在进行中",
|
||||
tables: [],
|
||||
synced_at: new Date().toISOString(),
|
||||
}
|
||||
}
|
||||
|
||||
if (!sectlAuth.isAuthenticated()) {
|
||||
return {
|
||||
success: false,
|
||||
message: "请先登录 SECTL Auth 账户",
|
||||
tables: [],
|
||||
synced_at: new Date().toISOString(),
|
||||
}
|
||||
}
|
||||
|
||||
this.isSyncing = true
|
||||
const results: TableSyncResult[] = []
|
||||
const syncedAt = new Date().toISOString()
|
||||
|
||||
try {
|
||||
for (const table of SYNC_TABLES) {
|
||||
const tableResult: TableSyncResult = {
|
||||
table,
|
||||
uploaded: 0,
|
||||
downloaded: 0,
|
||||
errors: [],
|
||||
}
|
||||
|
||||
try {
|
||||
const cloudData = await this.fetchCloudData(table)
|
||||
if (cloudData) {
|
||||
const applied = await this.setLocalData(table, cloudData)
|
||||
if (applied) {
|
||||
tableResult.downloaded = cloudData.length
|
||||
} else {
|
||||
tableResult.errors.push("应用云端数据失败")
|
||||
}
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
tableResult.errors.push(err instanceof Error ? err.message : String(err))
|
||||
}
|
||||
|
||||
results.push(tableResult)
|
||||
}
|
||||
|
||||
this.lastSyncAt = syncedAt
|
||||
this.lastError = null
|
||||
this.saveStatus()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `已从云端下载 ${results.reduce((s, r) => s + r.downloaded, 0)} 条记录`,
|
||||
tables: results,
|
||||
synced_at: syncedAt,
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : String(err)
|
||||
this.lastError = msg
|
||||
this.saveStatus()
|
||||
return {
|
||||
success: false,
|
||||
message: `同步失败:${msg}`,
|
||||
tables: results,
|
||||
synced_at: syncedAt,
|
||||
}
|
||||
} finally {
|
||||
this.isSyncing = false
|
||||
}
|
||||
}
|
||||
|
||||
async fullSync(direction: SyncDirection = "bidirectional"): Promise<CloudSyncResult> {
|
||||
if (this.isSyncing) {
|
||||
return {
|
||||
success: false,
|
||||
message: "同步正在进行中",
|
||||
tables: [],
|
||||
synced_at: new Date().toISOString(),
|
||||
}
|
||||
}
|
||||
|
||||
if (!sectlAuth.isAuthenticated()) {
|
||||
return {
|
||||
success: false,
|
||||
message: "请先登录 SECTL Auth 账户",
|
||||
tables: [],
|
||||
synced_at: new Date().toISOString(),
|
||||
}
|
||||
}
|
||||
|
||||
this.isSyncing = true
|
||||
const results: TableSyncResult[] = []
|
||||
const syncedAt = new Date().toISOString()
|
||||
|
||||
try {
|
||||
const metadata = await this.fetchMetadata()
|
||||
const isRemoteDevice = metadata?.device_id !== this.deviceId
|
||||
|
||||
for (const table of SYNC_TABLES) {
|
||||
const tableResult: TableSyncResult = {
|
||||
table,
|
||||
uploaded: 0,
|
||||
downloaded: 0,
|
||||
errors: [],
|
||||
}
|
||||
|
||||
try {
|
||||
if (direction === "push") {
|
||||
const localData = await this.getLocalData(table)
|
||||
const uploaded = await this.pushTableData(table, localData)
|
||||
tableResult.uploaded = uploaded
|
||||
} else if (direction === "pull") {
|
||||
const cloudData = await this.fetchCloudData(table)
|
||||
if (cloudData) {
|
||||
const applied = await this.setLocalData(table, cloudData)
|
||||
if (applied) tableResult.downloaded = cloudData.length
|
||||
else tableResult.errors.push("应用云端数据失败")
|
||||
}
|
||||
} else {
|
||||
const cloudData = await this.fetchCloudData(table)
|
||||
const localData = await this.getLocalData(table)
|
||||
|
||||
if (!cloudData || cloudData.length === 0) {
|
||||
const uploaded = await this.pushTableData(table, localData)
|
||||
tableResult.uploaded = uploaded
|
||||
} else if (localData.length === 0) {
|
||||
const applied = await this.setLocalData(table, cloudData)
|
||||
if (applied) tableResult.downloaded = cloudData.length
|
||||
else tableResult.errors.push("应用云端数据失败")
|
||||
} else {
|
||||
const cloudTs = metadata?.table_versions?.[table]
|
||||
const cloudTime = cloudTs ? new Date(cloudTs).getTime() : 0
|
||||
const now = Date.now()
|
||||
|
||||
if (isRemoteDevice || cloudTime < now - 60_000) {
|
||||
const uploaded = await this.pushTableData(table, localData)
|
||||
tableResult.uploaded = uploaded
|
||||
} else {
|
||||
const applied = await this.setLocalData(table, cloudData)
|
||||
if (applied) tableResult.downloaded = cloudData.length
|
||||
else tableResult.errors.push("应用云端数据失败")
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
tableResult.errors.push(err instanceof Error ? err.message : String(err))
|
||||
}
|
||||
|
||||
results.push(tableResult)
|
||||
}
|
||||
|
||||
const newMetadata: SyncMetadata = {
|
||||
last_sync_at: syncedAt,
|
||||
table_versions: Object.fromEntries(SYNC_TABLES.map((t) => [t, syncedAt])) as Record<
|
||||
SyncTable,
|
||||
string
|
||||
>,
|
||||
device_id: this.deviceId,
|
||||
}
|
||||
await this.pushMetadata(newMetadata)
|
||||
|
||||
this.lastSyncAt = syncedAt
|
||||
this.lastError = null
|
||||
this.saveStatus()
|
||||
|
||||
const totalUploaded = results.reduce((s, r) => s + r.uploaded, 0)
|
||||
const totalDownloaded = results.reduce((s, r) => s + r.downloaded, 0)
|
||||
return {
|
||||
success: true,
|
||||
message: `同步完成:上传 ${totalUploaded} 条,下载 ${totalDownloaded} 条`,
|
||||
tables: results,
|
||||
synced_at: syncedAt,
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : String(err)
|
||||
this.lastError = msg
|
||||
this.saveStatus()
|
||||
return {
|
||||
success: false,
|
||||
message: `同步失败:${msg}`,
|
||||
tables: results,
|
||||
synced_at: syncedAt,
|
||||
}
|
||||
} finally {
|
||||
this.isSyncing = false
|
||||
}
|
||||
}
|
||||
|
||||
async getCloudStorageUsage(): Promise<{
|
||||
used: number
|
||||
total: number
|
||||
percentage: number
|
||||
table_stats: Array<{ table: SyncTable; count: number; size: number }>
|
||||
} | null> {
|
||||
try {
|
||||
const tableStats: Array<{ table: SyncTable; count: number; size: number }> = []
|
||||
let totalSize = 0
|
||||
|
||||
for (const table of SYNC_TABLES) {
|
||||
const key = buildKey(table)
|
||||
const result = await sectlKVStorage.getKV(key)
|
||||
if (result && "value" in result) {
|
||||
const size = (result as any).size || 0
|
||||
const parsed = safeJsonParse<unknown[]>(result.value)
|
||||
tableStats.push({
|
||||
table,
|
||||
count: Array.isArray(parsed) ? parsed.length : 0,
|
||||
size,
|
||||
})
|
||||
totalSize += size
|
||||
} else {
|
||||
tableStats.push({ table, count: 0, size: 0 })
|
||||
}
|
||||
}
|
||||
|
||||
const maxStorage = 64 * 1024 * SYNC_TABLES.length
|
||||
return {
|
||||
used: totalSize,
|
||||
total: maxStorage,
|
||||
percentage: maxStorage > 0 ? Math.round((totalSize / maxStorage) * 100) : 0,
|
||||
table_stats: tableStats,
|
||||
}
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const sectlCloudSync = new SectlCloudSyncService()
|
||||
+80
-200
@@ -1,22 +1,14 @@
|
||||
/**
|
||||
* SECTL KV 键值对存储服务
|
||||
* 基于 SECTL-One-Stop SDK 规范实现
|
||||
* 提供键值对存储功能,支持 JSON 格式和字段级操作
|
||||
*/
|
||||
|
||||
import { SECTL_CONFIG, sectlAuth } from "./sectlAuth"
|
||||
|
||||
// KV 数据类型
|
||||
export interface KVItem {
|
||||
key: string
|
||||
value: any
|
||||
value: unknown
|
||||
size: number
|
||||
is_json: boolean
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
// KV 列表选项
|
||||
export interface ListKVOptions {
|
||||
prefix?: string
|
||||
limit?: number
|
||||
@@ -24,12 +16,60 @@ export interface ListKVOptions {
|
||||
}
|
||||
|
||||
class SectlKVStorageService {
|
||||
/**
|
||||
* 设置 KV 键值对
|
||||
* @param key 键名(最大 255 字符)
|
||||
* @param value 值(任意 JSON 可序列化数据,最大 64KB)
|
||||
* @param ttl 过期时间(秒),可选
|
||||
*/
|
||||
private get platformId(): string {
|
||||
return SECTL_CONFIG.platformId
|
||||
}
|
||||
|
||||
private getAuthHeaders(): Record<string, string> {
|
||||
const token = sectlAuth.getAccessToken()
|
||||
if (!token) throw new Error("未授权,请先登录")
|
||||
return { Authorization: `Bearer ${token}` }
|
||||
}
|
||||
|
||||
private async makeRequest<T>(
|
||||
method: string,
|
||||
endpoint: string,
|
||||
options: { jsonData?: Record<string, unknown>; params?: Record<string, unknown> } = {}
|
||||
): Promise<T> {
|
||||
const headers: Record<string, string> = {
|
||||
...this.getAuthHeaders(),
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
let url = `${SECTL_CONFIG.baseUrl}${endpoint}`
|
||||
if (options.params) {
|
||||
const searchParams = new URLSearchParams()
|
||||
for (const [k, v] of Object.entries(options.params)) {
|
||||
if (v !== undefined && v !== null) searchParams.set(k, String(v))
|
||||
}
|
||||
const qs = searchParams.toString()
|
||||
if (qs) url += `?${qs}`
|
||||
}
|
||||
|
||||
const init: RequestInit = { method, headers }
|
||||
if (options.jsonData && method !== "GET") {
|
||||
init.body = JSON.stringify(options.jsonData)
|
||||
}
|
||||
|
||||
const response = await fetch(url, init)
|
||||
|
||||
if (!response.ok) {
|
||||
let errorData: { error?: string; error_description?: string; message?: string } = {}
|
||||
try {
|
||||
errorData = await response.json()
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
const desc = errorData.error_description || errorData.message || `HTTP ${response.status}`
|
||||
const err = new Error(desc) as Error & { status?: number; error?: string }
|
||||
err.status = response.status
|
||||
err.error = errorData.error
|
||||
throw err
|
||||
}
|
||||
|
||||
return response.json() as Promise<T>
|
||||
}
|
||||
|
||||
async setKV(
|
||||
key: string,
|
||||
value: unknown,
|
||||
@@ -43,51 +83,16 @@ class SectlKVStorageService {
|
||||
updated_at?: string
|
||||
message: string
|
||||
}> {
|
||||
const accessToken = sectlAuth.getAccessToken()
|
||||
if (!accessToken) {
|
||||
throw new Error("未授权,请先登录")
|
||||
}
|
||||
|
||||
const url = `${SECTL_CONFIG.baseUrl}/api/cloud/kv`
|
||||
|
||||
const body: Record<string, unknown> = {
|
||||
client_id: SECTL_CONFIG.platformId,
|
||||
user_id: sectlAuth.getToken()?.user_id || "",
|
||||
const jsonData: Record<string, unknown> = {
|
||||
client_id: this.platformId,
|
||||
key,
|
||||
value,
|
||||
}
|
||||
if (ttl !== undefined) jsonData.ttl = ttl
|
||||
|
||||
if (ttl !== undefined) {
|
||||
body.ttl = ttl
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
|
||||
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
|
||||
}
|
||||
return this.makeRequest("POST", "/api/cloud/kv", { jsonData })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 KV 键值对
|
||||
* @param key 键名
|
||||
* @param field JSON 字段路径,如 "theme" 或 "user.name",可选
|
||||
*/
|
||||
async getKV<T = unknown>(
|
||||
key: string,
|
||||
field?: string
|
||||
@@ -95,93 +100,25 @@ class SectlKVStorageService {
|
||||
| (KVItem & { key: string; value: T })
|
||||
| { key: string; field: string; value: T; is_json: boolean }
|
||||
> {
|
||||
const accessToken = sectlAuth.getAccessToken()
|
||||
if (!accessToken) {
|
||||
throw new Error("未授权,请先登录")
|
||||
}
|
||||
const params: Record<string, string> = { client_id: this.platformId }
|
||||
if (field) params.field = field
|
||||
|
||||
const params = new URLSearchParams({
|
||||
client_id: SECTL_CONFIG.platformId,
|
||||
user_id: sectlAuth.getToken()?.user_id || "",
|
||||
})
|
||||
|
||||
if (field) {
|
||||
params.append("field", 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
|
||||
}
|
||||
return this.makeRequest("GET", `/api/cloud/kv/${encodeURIComponent(key)}`, { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 KV 键值对列表
|
||||
* @param options 列表选项
|
||||
*/
|
||||
async listKV(options: ListKVOptions = {}): Promise<{
|
||||
kv_list: KVItem[]
|
||||
total: number
|
||||
has_more: boolean
|
||||
}> {
|
||||
const accessToken = sectlAuth.getAccessToken()
|
||||
if (!accessToken) {
|
||||
throw new Error("未授权,请先登录")
|
||||
async listKV(
|
||||
options: ListKVOptions = {}
|
||||
): Promise<{ kv_list: KVItem[]; total: number; has_more: boolean }> {
|
||||
const params: Record<string, unknown> = {
|
||||
client_id: this.platformId,
|
||||
limit: Math.min(options.limit || 100, 1000),
|
||||
offset: options.offset || 0,
|
||||
}
|
||||
if (options.prefix) params.prefix = options.prefix
|
||||
|
||||
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.prefix) {
|
||||
params.append("prefix", options.prefix)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
return this.makeRequest("GET", "/api/cloud/kv", { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 KV JSON 字段
|
||||
* @param key 键名
|
||||
* @param field JSON 字段路径,支持嵌套如 "user.name" 或数组索引 "items.0.id"
|
||||
* @param value 要设置的值
|
||||
*/
|
||||
async updateKVField(
|
||||
key: string,
|
||||
field: string,
|
||||
@@ -194,77 +131,20 @@ class SectlKVStorageService {
|
||||
updated_at: string
|
||||
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: "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
|
||||
}
|
||||
return this.makeRequest("PATCH", `/api/cloud/kv/${encodeURIComponent(key)}`, {
|
||||
jsonData: { client_id: this.platformId, field, value },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除 KV 键值对
|
||||
* @param key 键名
|
||||
*/
|
||||
async deleteKV(key: string): Promise<{ success: boolean; message: string }> {
|
||||
const accessToken = sectlAuth.getAccessToken()
|
||||
if (!accessToken) {
|
||||
throw new Error("未授权,请先登录")
|
||||
}
|
||||
return this.makeRequest("DELETE", `/api/cloud/kv/${encodeURIComponent(key)}`, {
|
||||
jsonData: { client_id: this.platformId },
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
getPlatformId(): string {
|
||||
return this.platformId
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例
|
||||
export const sectlKVStorage = new SectlKVStorageService()
|
||||
|
||||
Reference in New Issue
Block a user