mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-19 06:04:22 +08:00
feat(kv-storage): 实现 KV 存储服务及管理界面
- 重构 KV 存储服务,遵循 SECTL-One-Stop SDK 规范 - 新增 KV 存储管理组件,支持增删改查操作 - 优化 setKV 接口,移除冗余参数 - 添加类型定义和列表查询选项
This commit is contained in:
+1
-1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,436 @@
|
||||
/**
|
||||
* SECTL KV 存储管理组件
|
||||
* 基于 SECTL-One-Stop SDK 规范实现
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from "react"
|
||||
import {
|
||||
Card,
|
||||
Table,
|
||||
Button,
|
||||
Space,
|
||||
Modal,
|
||||
Input,
|
||||
message,
|
||||
Popconfirm,
|
||||
Typography,
|
||||
Tag,
|
||||
Form,
|
||||
Switch,
|
||||
InputNumber,
|
||||
Tooltip,
|
||||
} from "antd"
|
||||
import {
|
||||
DatabaseOutlined,
|
||||
PlusOutlined,
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
ReloadOutlined,
|
||||
CodeOutlined,
|
||||
FieldStringOutlined,
|
||||
} from "@ant-design/icons"
|
||||
import { sectlKVStorage, KVItem, ListKVOptions } from "../services/sectlKVStorage"
|
||||
import { useSectl } from "../contexts/SectlContext"
|
||||
|
||||
const { Title, Text } = Typography
|
||||
const { TextArea } = Input
|
||||
|
||||
export const SectlKVStorageManager: React.FC = () => {
|
||||
const { isAuthenticated } = useSectl()
|
||||
const [kvList, setKvList] = useState<KVItem[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [total, setTotal] = useState(0)
|
||||
const [hasMore, setHasMore] = useState(false)
|
||||
const [offset, setOffset] = useState(0)
|
||||
const [prefix, setPrefix] = useState("")
|
||||
|
||||
// 编辑/创建对话框状态
|
||||
const [editModal, setEditModal] = useState<{
|
||||
visible: boolean
|
||||
isEdit: boolean
|
||||
key?: string
|
||||
}>({ visible: false, isEdit: false })
|
||||
|
||||
// 查看详情对话框状态
|
||||
const [detailModal, setDetailModal] = useState<{
|
||||
visible: boolean
|
||||
item?: KVItem
|
||||
}>({ visible: false })
|
||||
|
||||
// 表单
|
||||
const [form] = Form.useForm()
|
||||
|
||||
// 加载 KV 列表
|
||||
const loadKVList = async (resetOffset = false) => {
|
||||
if (!isAuthenticated) return
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
const currentOffset = resetOffset ? 0 : offset
|
||||
const options: ListKVOptions = {
|
||||
limit: 20,
|
||||
offset: currentOffset,
|
||||
}
|
||||
if (prefix) {
|
||||
options.prefix = prefix
|
||||
}
|
||||
|
||||
const result = await sectlKVStorage.listKV(options)
|
||||
setKvList(resetOffset ? result.kv_list : [...kvList, ...result.kv_list])
|
||||
setTotal(result.total)
|
||||
setHasMore(result.has_more)
|
||||
if (resetOffset) {
|
||||
setOffset(20)
|
||||
} else {
|
||||
setOffset(currentOffset + 20)
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(`加载键值对列表失败:${error.message}`)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 初始加载
|
||||
useEffect(() => {
|
||||
if (isAuthenticated) {
|
||||
loadKVList(true)
|
||||
}
|
||||
}, [isAuthenticated, prefix])
|
||||
|
||||
// 处理创建/更新
|
||||
const handleSave = async (values: {
|
||||
key: string
|
||||
value: string
|
||||
isJson: boolean
|
||||
ttl?: number
|
||||
}) => {
|
||||
try {
|
||||
let parsedValue: unknown
|
||||
|
||||
if (values.isJson) {
|
||||
try {
|
||||
parsedValue = JSON.parse(values.value)
|
||||
} catch {
|
||||
message.error("JSON 格式无效")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
parsedValue = values.value
|
||||
}
|
||||
|
||||
await sectlKVStorage.setKV(values.key, parsedValue, values.ttl)
|
||||
message.success(editModal.isEdit ? "键值对已更新" : "键值对已创建")
|
||||
setEditModal({ visible: false, isEdit: false })
|
||||
form.resetFields()
|
||||
loadKVList(true)
|
||||
} catch (error: any) {
|
||||
message.error(`保存失败:${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理删除
|
||||
const handleDelete = async (key: string) => {
|
||||
try {
|
||||
await sectlKVStorage.deleteKV(key)
|
||||
message.success("键值对已删除")
|
||||
loadKVList(true)
|
||||
} catch (error: any) {
|
||||
message.error(`删除失败:${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 打开编辑对话框
|
||||
const openEditModal = (item: KVItem) => {
|
||||
form.setFieldsValue({
|
||||
key: item.key,
|
||||
value: item.is_json ? JSON.stringify(item.value, null, 2) : String(item.value),
|
||||
isJson: item.is_json,
|
||||
})
|
||||
setEditModal({ visible: true, isEdit: true, key: item.key })
|
||||
}
|
||||
|
||||
// 打开创建对话框
|
||||
const openCreateModal = () => {
|
||||
form.resetFields()
|
||||
form.setFieldsValue({ isJson: true })
|
||||
setEditModal({ visible: true, isEdit: false })
|
||||
}
|
||||
|
||||
// 打开详情对话框
|
||||
const openDetailModal = (item: KVItem) => {
|
||||
setDetailModal({ visible: true, item })
|
||||
}
|
||||
|
||||
// 格式化值显示
|
||||
const formatValue = (value: unknown, isJson: boolean): string => {
|
||||
if (isJson) {
|
||||
return JSON.stringify(value).slice(0, 100) + (JSON.stringify(value).length > 100 ? "..." : "")
|
||||
}
|
||||
return String(value).slice(0, 100) + (String(value).length > 100 ? "..." : "")
|
||||
}
|
||||
|
||||
// 表格列定义
|
||||
const columns = [
|
||||
{
|
||||
title: "键名",
|
||||
dataIndex: "key",
|
||||
key: "key",
|
||||
render: (text: string, record: KVItem) => (
|
||||
<Space>
|
||||
<DatabaseOutlined />
|
||||
<a onClick={() => openDetailModal(record)}>{text}</a>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "类型",
|
||||
dataIndex: "is_json",
|
||||
key: "is_json",
|
||||
width: 100,
|
||||
render: (isJson: boolean) =>
|
||||
isJson ? (
|
||||
<Tag color="blue" icon={<CodeOutlined />}>
|
||||
JSON
|
||||
</Tag>
|
||||
) : (
|
||||
<Tag icon={<FieldStringOutlined />}>文本</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "大小",
|
||||
dataIndex: "size",
|
||||
key: "size",
|
||||
width: 100,
|
||||
render: (size: number) => {
|
||||
if (size < 1024) return `${size} B`
|
||||
if (size < 1024 * 1024) return `${(size / 1024).toFixed(2)} KB`
|
||||
return `${(size / (1024 * 1024)).toFixed(2)} MB`
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "值预览",
|
||||
dataIndex: "value",
|
||||
key: "value",
|
||||
ellipsis: true,
|
||||
render: (_: unknown, record: KVItem) => (
|
||||
<Text type="secondary" style={{ fontSize: "12px" }}>
|
||||
{formatValue(record.value, record.is_json)}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "更新时间",
|
||||
dataIndex: "updated_at",
|
||||
key: "updated_at",
|
||||
width: 180,
|
||||
render: (text: string) => new Date(text).toLocaleString("zh-CN"),
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "action",
|
||||
width: 150,
|
||||
render: (_: unknown, record: KVItem) => (
|
||||
<Space>
|
||||
<Tooltip title="编辑">
|
||||
<Button type="link" icon={<EditOutlined />} onClick={() => openEditModal(record)} />
|
||||
</Tooltip>
|
||||
<Popconfirm
|
||||
title="确定要删除这个键值对吗?"
|
||||
onConfirm={() => handleDelete(record.key)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button type="link" danger icon={<DeleteOutlined />} />
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return (
|
||||
<Card>
|
||||
<div style={{ textAlign: "center", padding: "40px" }}>
|
||||
<DatabaseOutlined style={{ fontSize: 48, color: "#999" }} />
|
||||
<Title level={4} style={{ marginTop: 16 }}>
|
||||
请先登录 SECTL 以使用 KV 存储服务
|
||||
</Title>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<DatabaseOutlined />
|
||||
<span>KV 存储管理</span>
|
||||
</Space>
|
||||
}
|
||||
extra={
|
||||
<Space>
|
||||
<Input
|
||||
placeholder="键名前缀过滤"
|
||||
value={prefix}
|
||||
onChange={(e) => setPrefix(e.target.value)}
|
||||
style={{ width: 200 }}
|
||||
allowClear
|
||||
/>
|
||||
<Button icon={<ReloadOutlined />} onClick={() => loadKVList(true)} loading={loading}>
|
||||
刷新
|
||||
</Button>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={openCreateModal}>
|
||||
新建键值对
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
{/* 统计信息 */}
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Text type="secondary">
|
||||
共 {total} 个键值对
|
||||
{hasMore && `(已加载 ${kvList.length} 个)`}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{/* KV 列表 */}
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={kvList}
|
||||
rowKey="key"
|
||||
loading={loading}
|
||||
pagination={false}
|
||||
/>
|
||||
|
||||
{/* 加载更多 */}
|
||||
{hasMore && (
|
||||
<div style={{ textAlign: "center", marginTop: 16 }}>
|
||||
<Button onClick={() => loadKVList(false)} loading={loading}>
|
||||
加载更多
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* 创建/编辑对话框 */}
|
||||
<Modal
|
||||
title={editModal.isEdit ? "编辑键值对" : "新建键值对"}
|
||||
open={editModal.visible}
|
||||
onOk={() => form.submit()}
|
||||
onCancel={() => {
|
||||
setEditModal({ visible: false, isEdit: false })
|
||||
form.resetFields()
|
||||
}}
|
||||
okText="保存"
|
||||
cancelText="取消"
|
||||
width={600}
|
||||
>
|
||||
<Form form={form} layout="vertical" onFinish={handleSave}>
|
||||
<Form.Item
|
||||
name="key"
|
||||
label="键名"
|
||||
rules={[
|
||||
{ required: true, message: "请输入键名" },
|
||||
{ max: 255, message: "键名最大 255 字符" },
|
||||
]}
|
||||
>
|
||||
<Input placeholder="请输入键名" disabled={editModal.isEdit} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="isJson" label="数据类型" valuePropName="checked">
|
||||
<Switch checkedChildren="JSON" unCheckedChildren="文本" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="value" label="值" rules={[{ required: true, message: "请输入值" }]}>
|
||||
<TextArea
|
||||
rows={8}
|
||||
placeholder={
|
||||
form.getFieldValue("isJson")
|
||||
? '请输入 JSON 格式数据,例如:\n{\n "theme": "dark",\n "language": "zh-CN"\n}'
|
||||
: "请输入文本值"
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="ttl" label="过期时间(秒)">
|
||||
<InputNumber style={{ width: "100%" }} placeholder="留空表示永不过期" min={1} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
{/* 详情对话框 */}
|
||||
<Modal
|
||||
title="键值对详情"
|
||||
open={detailModal.visible}
|
||||
onCancel={() => setDetailModal({ visible: false })}
|
||||
footer={[
|
||||
<Button key="close" onClick={() => setDetailModal({ visible: false })}>
|
||||
关闭
|
||||
</Button>,
|
||||
]}
|
||||
width={700}
|
||||
>
|
||||
{detailModal.item && (
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||
<div>
|
||||
<Text strong>键名:</Text>
|
||||
<Text copyable>{detailModal.item.key}</Text>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text strong>类型:</Text>
|
||||
{detailModal.item.is_json ? (
|
||||
<Tag color="blue" icon={<CodeOutlined />}>
|
||||
JSON
|
||||
</Tag>
|
||||
) : (
|
||||
<Tag icon={<FieldStringOutlined />}>文本</Tag>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text strong>大小:</Text>
|
||||
<Text>
|
||||
{detailModal.item.size < 1024
|
||||
? `${detailModal.item.size} B`
|
||||
: detailModal.item.size < 1024 * 1024
|
||||
? `${(detailModal.item.size / 1024).toFixed(2)} KB`
|
||||
: `${(detailModal.item.size / (1024 * 1024)).toFixed(2)} MB`}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text strong>创建时间:</Text>
|
||||
<Text>{new Date(detailModal.item.created_at).toLocaleString("zh-CN")}</Text>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text strong>更新时间:</Text>
|
||||
<Text>{new Date(detailModal.item.updated_at).toLocaleString("zh-CN")}</Text>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text strong>值:</Text>
|
||||
<pre
|
||||
style={{
|
||||
background: "#f5f5f5",
|
||||
padding: 16,
|
||||
borderRadius: 8,
|
||||
overflow: "auto",
|
||||
maxHeight: 400,
|
||||
}}
|
||||
>
|
||||
{detailModal.item.is_json
|
||||
? JSON.stringify(detailModal.item.value, null, 2)
|
||||
: String(detailModal.item.value)}
|
||||
</pre>
|
||||
</div>
|
||||
</Space>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -13,6 +13,8 @@ import {
|
||||
import { useSectl } from "../contexts/SectlContext"
|
||||
import { SectlLoginButton } from "./SectlLoginButton"
|
||||
import { ScoreSyncPanel } from "./ScoreSyncPanel"
|
||||
import { SectlCloudStorageManager } from "./SectlCloudStorageManager"
|
||||
import { SectlKVStorageManager } from "./SectlKVStorageManager"
|
||||
|
||||
export const SectlSettingsPanel: React.FC = () => {
|
||||
const { isAuthenticated, isLoading, userInfo, platformId, setPlatformId, refreshUserInfo } =
|
||||
@@ -152,6 +154,12 @@ export const SectlSettingsPanel: React.FC = () => {
|
||||
|
||||
{/* 数据同步面板 */}
|
||||
<ScoreSyncPanel />
|
||||
|
||||
{/* 云存储管理 */}
|
||||
<SectlCloudStorageManager />
|
||||
|
||||
{/* KV 存储管理 */}
|
||||
<SectlKVStorageManager />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -836,7 +836,7 @@ const api = {
|
||||
}> => invoke("plugin_get_runtime_modules"),
|
||||
|
||||
// Generic invoke wrapper for backward compatibility with callers using `api.invoke`
|
||||
invoke: async (channel: string, ..._args: any[]): Promise<any> => {
|
||||
invoke: async (channel: string): Promise<any> => {
|
||||
switch (channel) {
|
||||
default:
|
||||
throw new Error(`Unsupported legacy invoke channel: ${channel}`)
|
||||
|
||||
@@ -60,9 +60,7 @@ class ScoreSyncService {
|
||||
}))
|
||||
|
||||
// 保存到云端
|
||||
await sectlKVStorage.setKV(DATA_KEYS.STUDENTS, scoreData, {
|
||||
is_json: true,
|
||||
})
|
||||
await sectlKVStorage.setKV(DATA_KEYS.STUDENTS, scoreData)
|
||||
|
||||
console.log("学生数据同步成功")
|
||||
} catch (error: any) {
|
||||
@@ -107,9 +105,7 @@ class ScoreSyncService {
|
||||
createdAt: event.created_at,
|
||||
}))
|
||||
|
||||
await sectlKVStorage.setKV(DATA_KEYS.EVENTS, scoreEvents, {
|
||||
is_json: true,
|
||||
})
|
||||
await sectlKVStorage.setKV(DATA_KEYS.EVENTS, scoreEvents)
|
||||
|
||||
console.log("积分事件同步成功")
|
||||
} catch (error: any) {
|
||||
@@ -146,9 +142,7 @@ class ScoreSyncService {
|
||||
}
|
||||
|
||||
try {
|
||||
await sectlKVStorage.setKV(DATA_KEYS.SETTINGS, settings, {
|
||||
is_json: true,
|
||||
})
|
||||
await sectlKVStorage.setKV(DATA_KEYS.SETTINGS, settings)
|
||||
|
||||
console.log("设置数据同步成功")
|
||||
} catch (error: any) {
|
||||
@@ -186,9 +180,7 @@ class ScoreSyncService {
|
||||
platform: "secscore",
|
||||
}
|
||||
|
||||
await sectlKVStorage.setKV(DATA_KEYS.SYNC_META, metadata, {
|
||||
is_json: true,
|
||||
})
|
||||
await sectlKVStorage.setKV(DATA_KEYS.SYNC_META, metadata)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,30 +1,47 @@
|
||||
/**
|
||||
* SECTL KV 键值对存储服务
|
||||
* 基于 SECTL-One-Stop SDK 规范实现
|
||||
* 提供键值对存储功能,支持 JSON 格式和字段级操作
|
||||
*/
|
||||
|
||||
import { SECTL_CONFIG, sectlAuth } from "./sectlAuth"
|
||||
import { KVData } from "./sectlCloudStorage"
|
||||
|
||||
// KV 数据类型
|
||||
export interface KVItem {
|
||||
key: string
|
||||
value: any
|
||||
size: number
|
||||
is_json: boolean
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
// KV 列表选项
|
||||
export interface ListKVOptions {
|
||||
prefix?: string
|
||||
limit?: number
|
||||
offset?: number
|
||||
}
|
||||
|
||||
class SectlKVStorageService {
|
||||
/**
|
||||
* 创建或更新键值对
|
||||
* 设置 KV 键值对
|
||||
* @param key 键名(最大 255 字符)
|
||||
* @param value 值(任意 JSON 可序列化数据,最大 64KB)
|
||||
* @param ttl 过期时间(秒),可选
|
||||
*/
|
||||
async setKV(
|
||||
key: string,
|
||||
value: any,
|
||||
options?: {
|
||||
is_json?: boolean
|
||||
}
|
||||
value: unknown,
|
||||
ttl?: number
|
||||
): Promise<{
|
||||
success: boolean
|
||||
kv_id: string
|
||||
key: string
|
||||
value: any
|
||||
is_json: boolean
|
||||
size: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
is_json: boolean
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
message: string
|
||||
}> {
|
||||
const accessToken = sectlAuth.getAccessToken()
|
||||
if (!accessToken) {
|
||||
@@ -33,26 +50,15 @@ class SectlKVStorageService {
|
||||
|
||||
const url = `${SECTL_CONFIG.baseUrl}/api/cloud/kv`
|
||||
|
||||
// 自动检测是否为 JSON 格式
|
||||
let valueStr: string
|
||||
let isJson = options?.is_json
|
||||
const body: Record<string, unknown> = {
|
||||
client_id: SECTL_CONFIG.platformId,
|
||||
user_id: sectlAuth.getToken()?.user_id || "",
|
||||
key,
|
||||
value,
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
if (ttl !== undefined) {
|
||||
body.ttl = ttl
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -62,13 +68,7 @@ class SectlKVStorageService {
|
||||
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,
|
||||
}),
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -84,14 +84,17 @@ class SectlKVStorageService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取键值对
|
||||
* 获取 KV 键值对
|
||||
* @param key 键名
|
||||
* @param field JSON 字段路径,如 "theme" 或 "user.name",可选
|
||||
*/
|
||||
async getKV<T = any>(
|
||||
async getKV<T = unknown>(
|
||||
key: string,
|
||||
options?: {
|
||||
field?: string // 获取 JSON 中的特定字段
|
||||
}
|
||||
): Promise<KVData | { key: string; field: string; value: T }> {
|
||||
field?: string
|
||||
): Promise<
|
||||
| (KVItem & { key: string; value: T })
|
||||
| { key: string; field: string; value: T; is_json: boolean }
|
||||
> {
|
||||
const accessToken = sectlAuth.getAccessToken()
|
||||
if (!accessToken) {
|
||||
throw new Error("未授权,请先登录")
|
||||
@@ -102,8 +105,8 @@ class SectlKVStorageService {
|
||||
user_id: sectlAuth.getToken()?.user_id || "",
|
||||
})
|
||||
|
||||
if (options?.field) {
|
||||
params.append("field", options.field)
|
||||
if (field) {
|
||||
params.append("field", field)
|
||||
}
|
||||
|
||||
const url = `${SECTL_CONFIG.baseUrl}/api/cloud/kv/${encodeURIComponent(key)}?${params.toString()}`
|
||||
@@ -128,12 +131,14 @@ class SectlKVStorageService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取键值对列表
|
||||
* 获取 KV 键值对列表
|
||||
* @param options 列表选项
|
||||
*/
|
||||
async listKV(options?: {
|
||||
limit?: number
|
||||
offset?: number
|
||||
}): Promise<{ kvs: KVData[]; total: number; has_more: boolean }> {
|
||||
async listKV(options: ListKVOptions = {}): Promise<{
|
||||
kv_list: KVItem[]
|
||||
total: number
|
||||
has_more: boolean
|
||||
}> {
|
||||
const accessToken = sectlAuth.getAccessToken()
|
||||
if (!accessToken) {
|
||||
throw new Error("未授权,请先登录")
|
||||
@@ -142,10 +147,14 @@ class SectlKVStorageService {
|
||||
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),
|
||||
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 {
|
||||
@@ -168,18 +177,22 @@ class SectlKVStorageService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 JSON 字段
|
||||
* 更新 KV JSON 字段
|
||||
* @param key 键名
|
||||
* @param field JSON 字段路径,支持嵌套如 "user.name" 或数组索引 "items.0.id"
|
||||
* @param value 要设置的值
|
||||
*/
|
||||
async updateKVField(
|
||||
key: string,
|
||||
field: string,
|
||||
value: any
|
||||
value: unknown
|
||||
): Promise<{
|
||||
success: boolean
|
||||
key: string
|
||||
field: string
|
||||
value: any
|
||||
size: number
|
||||
updated_at: string
|
||||
message: string
|
||||
}> {
|
||||
const accessToken = sectlAuth.getAccessToken()
|
||||
if (!accessToken) {
|
||||
@@ -216,7 +229,8 @@ class SectlKVStorageService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除键值对
|
||||
* 删除 KV 键值对
|
||||
* @param key 键名
|
||||
*/
|
||||
async deleteKV(key: string): Promise<{ success: boolean; message: string }> {
|
||||
const accessToken = sectlAuth.getAccessToken()
|
||||
|
||||
Reference in New Issue
Block a user