feat: implement Cloudflare KV utility class and React hooks

- Add CloudflareKVClient class with full CRUD operations
- Support for batch operations, metadata, and key listing
- Implement useCloudflareKV hook for React components
- Add useKVValue hook for auto-loading specific keys
- Include comprehensive error handling and loading states
- Create demo component showing all functionality
- Add detailed documentation and usage examples
- Support for JSON parsing and custom configurations
- Based on jm_video_ui.md specifications
This commit is contained in:
root 2025-07-10 20:51:09 +08:00
parent 04e5990376
commit a5381e5305
7 changed files with 1347 additions and 569 deletions

View File

@ -1,568 +1,17 @@
import glob
import mimetypes
import os
import threading
import time
import tkinter as tk
from tkinter import ttk, filedialog, scrolledtext, messagebox
import requests
from loguru import logger
from qcloud_cos import CosConfig
from qcloud_cos import CosS3Client
cos_bucket_name = 'sucai-1324682537'
cos_secret_id = 'AKIDsrihIyjZOBsjimt8TsN8yvv1AMh5dB44'
cos_secret_key = 'CPZcxdk6W39Jd4cGY95wvupoyMd0YFqW'
cos_region = 'ap-shanghai'
api_key = '21575c22-14aa-40ca-8aa8-f00ca27a3a17'
default_prompt = [
"女人扭动身体向摄像机展示身材,一只手撩了一下头发,镜头从左向右移动并放大画面",
# "女人扭动身体向摄像机展示身材,一只手撩了一下头发后放在了裤子上,镜头从左向右移动",
"时尚模特抬头自信的展示身材,扭动身体,一只手放在了头上,镜头逐渐放大聚焦在了下衣上",
"女人扭动身体向摄像机展示身材,一只手撩了一下头发后放在了裤子上,镜头从左向右移动",
"自信步伐跟拍模特,模特步伐自信地同时行走,镜头紧紧跟随。抬起手捋一捋头发。传递出自信与时尚的气息。",
"女生两只手捏着拳头轻盈的左右摇摆跳舞动作幅度不大然后把手摊开放在胸口再做出像popping心脏跳动的动作左右身体都要非常协调",
"一个年轻女子自信地在相机前展示了她优美的身材,以自然的流体动作自由地摇摆,左手撩了一下头发之后停在了胸前。一个美女自拍跳舞扭动身体的视频,手从下到上最后放在胸前,妩媚的表情",
"美女向后退了一步站在那里展示服装,双手轻轻提了一下裤子两侧,镜头从上到下逐渐放大",
"女人低头看向裤子向镜头展示身材一只手放在了头上做pose动作",
"美女向后退了一步站在那里展示服装,低头并用手抚摸裤子,镜头从上到下逐渐放大",
"美女向后退了一步站在那里展示服装,双手从上到下整理衣服,自然扭动身体,自信的表情"
]
default_prompt_str = '\n'.join(default_prompt)
# pyinstaller -F -w --name seed_video jm_video_ui.py
class VideoUtils:
@staticmethod
def download_video(video_url: str, save_path: str) -> str:
try:
file_name = f'{int(time.time() * 1000)}.mp4'
response = requests.get(video_url, stream=True)
full_path = os.path.join(save_path, file_name)
with open(full_path, 'wb') as f:
for chunk in response.iter_content(chunk_size=1024 * 1024 * 5):
if chunk:
f.write(chunk)
return full_path
except Exception as e:
logger.error(e)
pass
@staticmethod
def upload_file_to_cos(file_path: str, remove_src_file: bool = False):
resp_data = {'status': True, 'data': '', 'msg': ''}
mime_type, _ = mimetypes.guess_type(file_path)
category = mime_type.split('/')[0]
f_name = os.path.basename(file_path)
suffix = f_name.split('.')[-1]
real_name = f'{int(time.time() * 1000)}.{suffix}'
try:
object_key = f'tk/{category}/{real_name}'
config = CosConfig(Region=cos_region, SecretId=cos_secret_id, SecretKey=cos_secret_key)
client = CosS3Client(config)
_ = client.upload_file(
Bucket=cos_bucket_name,
Key=object_key,
LocalFilePath=file_path,
EnableMD5=False,
progress_callback=None
)
url = f'https://{cos_bucket_name}.cos.ap-shanghai.myqcloud.com/{object_key}'
resp_data['data'] = url
resp_data['msg'] = '上传成功'
except Exception as e:
logger.error(e)
resp_data['status'] = False
resp_data['msg'] = str(e)
finally:
if remove_src_file:
os.remove(file_path)
return resp_data
@staticmethod
def submit_task(prompt: str, img_url: str, duration: str = '5', model_type:str='lite'):
"""
:param prompt: 生成视频的提示词
:param img_url:
:param duration:
:return:
"""
if model_type == 'lite':
model = 'doubao-seedance-1-0-lite-i2v-250428'
resolution = '720p'
else:
model = 'doubao-seedance-1-0-pro-250528'
resolution = '1080p'
if duration not in ('5', '10'):
logger.error('Duration must be either 5 or 10')
try:
headers = {
'Content-Type': 'application/json',
'Authorization': f'Bearer {api_key}',
}
json_data = {
'model': model,
'content': [
{
'type': 'text',
'text': f'{prompt} --resolution {resolution} --dur {duration} --camerafixed false',
},
{
'type': 'image_url',
'image_url': {
'url': img_url,
},
},
],
}
response = requests.post('https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks',
headers=headers,
json=json_data)
# 检查HTTP状态码
if response.status_code != 200:
error_msg = f"API请求失败状态码: {response.status_code}, 响应: {response.text}"
logger.error(error_msg)
return {"status": False, 'msg': error_msg}
resp_json = response.json()
# 检查响应中是否包含id字段
if 'id' not in resp_json:
error_msg = f"API响应缺少id字段响应内容: {resp_json}"
logger.error(error_msg)
return {"status": False, 'msg': error_msg}
job_id = resp_json['id']
return {"data": job_id, 'status': True}
except Exception as e:
logger.error(e)
return {"status": False, 'msg': str(e)}
@staticmethod
def query_task_result(job_id, timeout: int = 180, interval: int = 2, progress_callback=None):
def query_status(t_id: str):
resp_dict = {'status': False, 'data': None, 'msg': ''}
try:
headers = {
'Content-Type': 'application/json',
'Authorization': f'Bearer {api_key}',
}
response = requests.get(f'https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks/{t_id}',
headers=headers)
resp_json = response.json()
resp_dict['status'] = resp_json['status'] == 'succeeded'
resp_dict['msg'] = resp_json['status']
resp_dict['data'] = resp_json['content']['video_url'] if 'content' in resp_json else None
except Exception as e:
logger.error(e)
resp_dict['msg'] = str(e)
finally:
return resp_dict
end = time.time() + timeout
final_result = {"status": False, "data": None, "msg": ""}
success = False
wait_count = 0
# 添加开始查询的日志
if progress_callback:
progress_callback(f" 开始查询任务状态任务ID: {job_id}")
while time.time() < end:
tmp_data = query_status(job_id)
if tmp_data['status']:
final_result['status'] = True
final_result['data'] = tmp_data['data']
final_result['msg'] = 'succeeded'
success = True
if progress_callback:
progress_callback(f" ✓ 视频生成完成!")
break
elif tmp_data['msg'] == 'running':
wait_count += 1
elapsed = wait_count * interval
remaining = max(0, timeout - elapsed)
progress_msg = f" ⏳ 任务运行中,已等待{elapsed}秒,预计剩余{remaining}秒..."
logger.info(progress_msg)
if progress_callback:
progress_callback(progress_msg)
time.sleep(interval)
elif tmp_data['msg'] == 'failed':
final_result['msg'] = '任务执行失败'
if progress_callback:
progress_callback(f" ✗ 任务执行失败")
break
elif tmp_data['msg'] == 'pending' or tmp_data['msg'] == 'queued':
wait_count += 1
elapsed = wait_count * interval
remaining = max(0, timeout - elapsed)
progress_msg = f" ⏳ 任务排队中,已等待{elapsed}秒,预计剩余{remaining}秒..."
logger.info(progress_msg)
if progress_callback:
progress_callback(progress_msg)
time.sleep(interval)
else:
# 其他未知状态继续等待
wait_count += 1
logger.info(f" 未知状态: {tmp_data['msg']},继续等待...")
if progress_callback:
progress_callback(f" ❔ 状态: {tmp_data['msg']},继续等待...")
time.sleep(interval)
if not success and final_result['msg'] == '':
final_result['msg'] = '任务超时'
if progress_callback:
progress_callback(f" ⏰ 任务查询超时({timeout}秒)")
return final_result
@staticmethod
def local_file_to_video(file_path: str, prompt: str, duration: str = '5', model_type: str = 'lite',
timeout: int = 180, interval: int = 2, save_path: str = None, progress_callback=None):
"""
将本地图片文件转换为视频
:param file_path: 本地图片文件路径
:param prompt: 视频生成提示词
:param duration: 视频时长 ('5' 或 '10')
:param model_type: 模型类型 ('lite' 或 'pro')
:param timeout: 任务超时时间(秒)
:param interval: 查询间隔(秒)
:param save_path: 视频保存目录
:return: {'status': bool, 'video_path': str, 'msg': str}
"""
result = {'status': False, 'video_path': '', 'msg': ''}
try:
# 检查文件是否存在
if not os.path.exists(file_path):
result['msg'] = f'文件不存在: {file_path}'
logger.error(result['msg'])
return result
# 步骤1: 上传图片到COS
if progress_callback:
progress_callback(" 📤 正在上传图片到云存储...")
logger.info(f"正在上传图片到COS: {file_path}")
cos_dict = VideoUtils.upload_file_to_cos(file_path)
if not cos_dict['status']:
result['msg'] = f"上传图片失败: {cos_dict['msg']}"
logger.error(result['msg'])
return result
img_url = cos_dict['data']
if progress_callback:
progress_callback(" ✓ 图片上传成功")
logger.info(f"图片上传成功: {img_url}")
# 步骤2: 提交视频生成任务
if progress_callback:
progress_callback(" 🚀 正在提交视频生成任务...")
logger.info("正在提交视频生成任务...")
task_dict = VideoUtils.submit_task(prompt, img_url, duration, model_type)
if not task_dict['status']:
result['msg'] = f"提交任务失败: {task_dict['msg']}"
logger.error(result['msg'])
return result
task_id = task_dict['data']
if progress_callback:
progress_callback(f" ✓ 任务提交成功任务ID: {task_id}")
logger.info(f"任务提交成功任务ID: {task_id}")
# 步骤3: 查询任务结果
if progress_callback:
progress_callback(" ⏳ 正在等待视频生成完成...")
logger.info("正在等待视频生成完成...")
status_dict = VideoUtils.query_task_result(task_id, timeout=timeout, interval=interval,
progress_callback=progress_callback)
if not status_dict['status']:
result['msg'] = f"视频生成失败: {status_dict['msg']}"
logger.error(result['msg'])
return result
video_url = status_dict['data']
logger.info(f"视频生成成功: {video_url}")
# 步骤4: 下载视频到本地
if save_path:
if progress_callback:
progress_callback(" 📥 正在下载视频到本地...")
logger.info("正在下载视频到本地...")
video_path = VideoUtils.download_video(video_url, save_path)
if video_path and os.path.exists(video_path):
result['status'] = True
result['video_path'] = video_path
result['msg'] = '视频生成并下载成功'
if progress_callback:
progress_callback(f" ✓ 视频下载成功: {os.path.basename(video_path)}")
logger.info(f"视频下载成功: {video_path}")
else:
result['msg'] = '视频下载失败'
if progress_callback:
progress_callback(" ✗ 视频下载失败")
logger.error(result['msg'])
else:
result['status'] = True
result['video_path'] = video_url
result['msg'] = '视频生成成功'
except Exception as e:
result['msg'] = f'处理过程中发生异常: {str(e)}'
logger.error(result['msg'])
return result
def run_actual_script_logic(image_folder, prompt, output_video_dir, video_duration, model_type, add_log_entry_func,
other_params=None):
add_log_entry_func(f"开始处理任务...")
add_log_entry_func(f" 图片文件夹: {image_folder}")
add_log_entry_func(f" 提示词: \n{prompt[:100]}{'...' if len(prompt) > 100 else ''}")
add_log_entry_func(f" 视频保存目录: {output_video_dir}")
add_log_entry_func(f" 视频时长: {video_duration}秒")
add_log_entry_func(f" 模型类型: {model_type}")
if other_params:
for key, value in other_params.items():
add_log_entry_func(f" 额外参数 {key}: {value}")
try:
# 筛选图片文件
# img_extensions = ['.jpg', '.jpeg', '.png', '.bmp', '.gif', '.tiff', '.webp']
img_list = glob.glob(f'{image_folder}/*')
add_log_entry_func(f'目录下找到 {len(img_list)} 张图片')
if len(img_list) == 0:
add_log_entry_func("警告: 未找到任何图片文件")
return
# 解析提示词列表
prompt_lines = [line.strip() for line in prompt.split('\n') if line.strip()]
if not prompt_lines:
add_log_entry_func("错误: 提示词不能为空")
return
add_log_entry_func(f'解析到 {len(prompt_lines)} 个提示词,将循环使用')
# 确保输出目录存在
if not os.path.exists(output_video_dir):
os.makedirs(output_video_dir)
add_log_entry_func(f"创建输出目录: {output_video_dir}")
success_count = 0
failed_count = 0
# 处理每个图片文件
for i, img_path in enumerate(img_list):
img_name = os.path.basename(img_path)
# 使用循环提示词
current_prompt = prompt_lines[i % len(prompt_lines)]
add_log_entry_func(f"[{i + 1}/{len(img_list)}] 处理图片: {img_name}")
add_log_entry_func(
f" 使用提示词[{i % len(prompt_lines) + 1}]: {current_prompt[:50]}{'...' if len(current_prompt) > 50 else ''}")
try:
# 调用视频生成功能
result = VideoUtils.local_file_to_video(
file_path=img_path,
prompt=current_prompt,
duration=str(video_duration),
model_type=model_type,
timeout=300, # 5分钟超时
interval=3, # 3秒检查一次
save_path=output_video_dir,
progress_callback=add_log_entry_func
)
if result and result.get('status'):
success_count += 1
video_path = result.get('video_path', '')
add_log_entry_func(f" ✓ 视频生成成功: {os.path.basename(video_path)}")
else:
failed_count += 1
error_msg = result.get('msg', '未知错误') if result else '处理失败'
add_log_entry_func(f" ✗ 视频生成失败: {error_msg}")
except Exception as e:
failed_count += 1
add_log_entry_func(f" ✗ 处理图片时出错: {str(e)}")
# 输出最终统计
add_log_entry_func("=" * 50)
add_log_entry_func(f"处理完成!成功: {success_count}, 失败: {failed_count}")
if success_count > 0:
add_log_entry_func(f"生成的视频已保存到: {output_video_dir}")
except Exception as e:
add_log_entry_func(f"处理过程中发生错误: {str(e)}")
finally:
add_log_entry_func("任务处理结束。")
# --- 核心处理函数结束 ---
class App:
def __init__(self, root):
self.root = root
self.root.title("视频生成工具")
self.root.geometry("700x650")
self.root.resizable(False, False)
# --- 变量 ---
self.image_folder_var = tk.StringVar()
self.output_video_dir_var = tk.StringVar()
self.video_duration_var = tk.IntVar(value=5)
self.model_type_var = tk.StringVar(value='lite')
# --- 主框架 ---
main_frame = ttk.Frame(root, padding="10")
main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
root.grid_rowconfigure(0, weight=1)
root.grid_columnconfigure(0, weight=1)
# --- 组件 ---
row_idx = 0
general_pady = 5
# 1. 选择图片文件夹
ttk.Label(main_frame, text="选择图片文件夹:").grid(row=row_idx, column=0, sticky=tk.W, pady=(general_pady, 2))
self.image_folder_entry = ttk.Entry(main_frame, textvariable=self.image_folder_var, width=50)
self.image_folder_entry.grid(row=row_idx, column=1, sticky=(tk.W, tk.E), pady=(general_pady, 2), padx=5)
ttk.Button(main_frame, text="浏览...", command=self.select_image_folder).grid(row=row_idx, column=2,
sticky=tk.E,
pady=(general_pady, 2))
row_idx += 1
# 2. 保存视频存储目录
ttk.Label(main_frame, text="视频保存目录:").grid(row=row_idx, column=0, sticky=tk.W, pady=general_pady)
self.output_video_dir_entry = ttk.Entry(main_frame, textvariable=self.output_video_dir_var, width=50)
self.output_video_dir_entry.grid(row=row_idx, column=1, sticky=(tk.W, tk.E), pady=general_pady, padx=5)
ttk.Button(main_frame, text="浏览...", command=self.select_output_video_dir).grid(row=row_idx, column=2,
sticky=tk.E,
pady=general_pady)
row_idx += 1
# 3. 生图提示词
ttk.Label(main_frame, text="生成视频提示词:").grid(row=row_idx, column=0, sticky=(tk.W, tk.NW),
pady=(general_pady, 2))
self.prompt_text = scrolledtext.ScrolledText(main_frame, wrap=tk.WORD, width=60, height=8)
self.prompt_text.grid(row=row_idx, column=1, columnspan=2, sticky=(tk.W, tk.E), pady=(general_pady, 2), padx=5)
# 设置默认提示词
self.prompt_text.insert("1.0", default_prompt_str)
row_idx += 1
# 4. 视频时长
duration_frame = ttk.LabelFrame(main_frame, text="视频时长")
duration_frame.grid(row=row_idx, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=general_pady, padx=5)
ttk.Radiobutton(duration_frame, text="5秒", variable=self.video_duration_var, value=5).pack(side=tk.LEFT,
padx=10, pady=5)
ttk.Radiobutton(duration_frame, text="10秒", variable=self.video_duration_var, value=10).pack(side=tk.LEFT,
padx=10, pady=5)
row_idx += 1
# 5. 模型类型
model_frame = ttk.LabelFrame(main_frame, text="模型类型")
model_frame.grid(row=row_idx, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=general_pady, padx=5)
ttk.Radiobutton(model_frame, text="lite", variable=self.model_type_var, value='lite').pack(side=tk.LEFT,
padx=10, pady=5)
ttk.Radiobutton(model_frame, text="pro", variable=self.model_type_var, value='pro').pack(side=tk.LEFT,
padx=10, pady=5)
row_idx += 1
# 6. 运行按钮 (新位置)
self.run_button = ttk.Button(main_frame, text="运行", command=self.start_processing_thread)
# 增加上下边距,使其与上下组件有明显区隔
self.run_button.grid(row=row_idx, column=0, columnspan=3, pady=(general_pady + 10, general_pady + 5))
row_idx += 1
# 7. 运行日志 Label
ttk.Label(main_frame, text="运行日志:").grid(row=row_idx, column=0, sticky=tk.W,
pady=(general_pady, 0)) # 调整pady使其靠近下方的Text
row_idx += 1
# 8. 运行日志 ScrolledText
self.log_text = scrolledtext.ScrolledText(main_frame, wrap=tk.WORD, width=70, height=15, state='disabled')
self.log_text.grid(row=row_idx, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(2, general_pady),
padx=5)
main_frame.grid_rowconfigure(row_idx, weight=1) # 让日志区域可以扩展
# row_idx += 1 # 最后一个组件后面不需要再增加row_idx除非还要添加东西
# 配置列的伸缩
main_frame.grid_columnconfigure(1, weight=1)
def select_image_folder(self):
folder_selected = filedialog.askdirectory()
if folder_selected:
self.image_folder_var.set(folder_selected)
self.add_log_entry(f"选择图片文件夹: {folder_selected}")
def select_output_video_dir(self):
folder_selected = filedialog.askdirectory()
if folder_selected:
self.output_video_dir_var.set(folder_selected)
self.add_log_entry(f"选择视频保存目录: {folder_selected}")
def add_log_entry(self, message):
self.log_text.configure(state='normal')
self.log_text.insert(tk.END, message + "\n")
self.log_text.configure(state='disabled')
self.log_text.see(tk.END)
def start_processing_thread(self):
image_folder = self.image_folder_var.get()
prompt = self.prompt_text.get("1.0", tk.END).strip()
output_video_dir = self.output_video_dir_var.get()
video_duration = self.video_duration_var.get()
model_type = self.model_type_var.get()
other_params = {}
if not image_folder:
messagebox.showerror("错误", "请选择图片文件夹!")
return
if not prompt:
messagebox.showerror("错误", "请输入生图提示词!")
return
if not output_video_dir:
messagebox.showerror("错误", "请选择视频保存目录!")
return
self.add_log_entry("=" * 30)
self.run_button.config(state="disabled")
thread = threading.Thread(target=self.run_script_in_background,
args=(image_folder, prompt, output_video_dir, video_duration, model_type, other_params))
thread.daemon = True
thread.start()
def run_script_in_background(self, image_folder, prompt, output_video_dir, video_duration, model_type, other_params):
try:
run_actual_script_logic(
image_folder,
prompt,
output_video_dir,
video_duration,
model_type,
self.add_log_entry,
other_params
)
except Exception as e:
self.add_log_entry(f"线程中发生未捕获的错误: {e}")
self.root.after(0, lambda: messagebox.showerror("线程错误", f"处理过程中发生错误:\n{e}"))
finally:
self.root.after(0, self.enable_run_button)
def enable_run_button(self):
self.run_button.config(state="normal")
if __name__ == "__main__":
root = tk.Tk()
app = App(root)
root.mainloop()
config:
cloudflare_api_key: str = "dlGquMNiAX-S7SV9pXne7YGfdH_fEgq3TfIGgNcQ"
cloudflare_url: str = "https://api.cloudflare.com/client/v4/"
cloudflare_account_id: str = "67720b647ff2b55cf37ba3ef9e677083"
cloudflare_kv_id: str = "995d7eff088547d7a7d8fea53e56115b"
Authorization: `Bearer ${config.cloudflare_api_key}`
kv api:
- put
curl put ${config.cloudflare_url}$/accounts/${config.cloudflare_account_id}/storage/kv/namespaces/${config.cloudflare_kv_id}/values/$KEY_NAME \
-X PUT \
-H 'Content-Type: multipart/form-data' \
-d '{
"value": "Some Value"
}'
curl get ${config.cloudflare_url}$/accounts/${config.cloudflare_account_id}/storage/kv/namespaces/${config.cloudflare_kv_id}/values/$KEY_NAME

View File

@ -35,7 +35,11 @@ class Settings(BaseSettings):
default_fps: int = 30
default_video_codec: str = "libx264"
default_audio_codec: str = "aac"
# cloud flare
cloudflare_api_key: str = "dlGquMNiAX-S7SV9pXne7YGfdH_fEgq3TfIGgNcQ"
cloudflare_url: str = "https://api.cloudflare.com/client/v4/"
cloudflare_account_id: str = "67720b647ff2b55cf37ba3ef9e677083"
cloudflare_kv_id: str = "995d7eff088547d7a7d8fea53e56115b"
# Audio Processing
default_sample_rate: int = 44100
default_audio_bitrate: str = "192k"

View File

@ -0,0 +1,287 @@
/**
* Cloudflare KV Demo Component
*
* This component demonstrates how to use the Cloudflare KV utilities
* in a React application with proper loading states and error handling.
*/
import React, { useState } from 'react'
import { useCloudflareKV, useKVValue } from '../hooks/useCloudflareKV'
interface DemoData {
message: string
timestamp: string
counter: number
}
export const CloudflareKVDemo: React.FC = () => {
const [inputKey, setInputKey] = useState('demo:test')
const [inputValue, setInputValue] = useState('Hello, Cloudflare KV!')
const [searchPrefix, setSearchPrefix] = useState('demo:')
// Use the general KV hook
const kv = useCloudflareKV()
// Use the specific value hook for a counter
const {
value: counterValue,
loading: counterLoading,
error: counterError,
save: saveCounter,
refresh: refreshCounter
} = useKVValue<number>('demo:counter', true, true)
// Handle putting a value
const handlePutValue = async () => {
if (!inputKey || !inputValue) return
try {
const data: DemoData = {
message: inputValue,
timestamp: new Date().toISOString(),
counter: (typeof counterValue === 'number' ? counterValue : 0) + 1
}
await kv.put(inputKey, data)
alert('✅ Value stored successfully!')
} catch (error) {
alert('❌ Failed to store value: ' + (error as Error).message)
}
}
// Handle getting a value
const handleGetValue = async () => {
if (!inputKey) return
try {
const result = await kv.get<DemoData>(inputKey)
if (result) {
alert(`✅ Retrieved value: ${JSON.stringify(result, null, 2)}`)
} else {
alert(' Key not found')
}
} catch (error) {
alert('❌ Failed to get value: ' + (error as Error).message)
}
}
// Handle deleting a value
const handleDeleteValue = async () => {
if (!inputKey) return
try {
await kv.delete(inputKey)
alert('✅ Value deleted successfully!')
} catch (error) {
alert('❌ Failed to delete value: ' + (error as Error).message)
}
}
// Handle listing keys
const handleListKeys = async () => {
try {
const result = await kv.listKeys(searchPrefix, 50)
const keyNames = result.result.keys.map(k => k.name).join('\n')
alert(`✅ Found keys:\n${keyNames || 'No keys found'}`)
} catch (error) {
alert('❌ Failed to list keys: ' + (error as Error).message)
}
}
// Handle incrementing counter
const handleIncrementCounter = async () => {
try {
const newValue = (typeof counterValue === 'number' ? counterValue : 0) + 1
await saveCounter(newValue)
} catch (error) {
alert('❌ Failed to increment counter: ' + (error as Error).message)
}
}
// Handle batch operations
const handleBatchPut = async () => {
try {
const entries = [
{ key: 'batch:item1', value: { name: 'Item 1', price: 10.99 } },
{ key: 'batch:item2', value: { name: 'Item 2', price: 15.50 } },
{ key: 'batch:item3', value: { name: 'Item 3', price: 8.75 } }
]
await kv.putBatch(entries)
alert('✅ Batch operation completed!')
} catch (error) {
alert('❌ Batch operation failed: ' + (error as Error).message)
}
}
return (
<div className="max-w-4xl mx-auto p-6 space-y-8">
<div className="bg-white rounded-lg shadow-lg p-6">
<h1 className="text-3xl font-bold text-gray-900 mb-6">
Cloudflare KV Demo
</h1>
{/* Loading indicator */}
{kv.isLoading && (
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
<div className="flex items-center">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-600 mr-2"></div>
<span className="text-blue-800">Processing...</span>
</div>
</div>
)}
{/* Error display */}
{kv.hasError && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4 mb-6">
<h3 className="text-red-800 font-medium mb-2">Errors:</h3>
<ul className="text-red-700 text-sm space-y-1">
{kv.errors.get && <li>Get: {kv.errors.get}</li>}
{kv.errors.put && <li>Put: {kv.errors.put}</li>}
{kv.errors.delete && <li>Delete: {kv.errors.delete}</li>}
{kv.errors.list && <li>List: {kv.errors.list}</li>}
</ul>
</div>
)}
{/* Counter Section */}
<div className="bg-gray-50 rounded-lg p-4 mb-6">
<h2 className="text-xl font-semibold text-gray-900 mb-4">
Auto-loaded Counter
</h2>
<div className="flex items-center space-x-4">
<div className="text-lg">
Current value:
<span className="font-mono font-bold ml-2">
{counterLoading ? '...' : counterValue || 0}
</span>
</div>
<button
onClick={handleIncrementCounter}
disabled={counterLoading}
className="bg-blue-500 hover:bg-blue-600 disabled:bg-gray-400 text-white px-4 py-2 rounded-lg transition-colors"
>
Increment
</button>
<button
onClick={refreshCounter}
disabled={counterLoading}
className="bg-gray-500 hover:bg-gray-600 disabled:bg-gray-400 text-white px-4 py-2 rounded-lg transition-colors"
>
Refresh
</button>
</div>
{counterError && (
<p className="text-red-600 text-sm mt-2">{counterError}</p>
)}
</div>
{/* Manual Operations Section */}
<div className="space-y-6">
<h2 className="text-xl font-semibold text-gray-900">
Manual Operations
</h2>
{/* Key-Value Input */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Key
</label>
<input
type="text"
value={inputKey}
onChange={(e) => setInputKey(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="Enter key name"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Value
</label>
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="Enter value"
/>
</div>
</div>
{/* Action Buttons */}
<div className="flex flex-wrap gap-3">
<button
onClick={handlePutValue}
disabled={kv.isLoading || !inputKey || !inputValue}
className="bg-green-500 hover:bg-green-600 disabled:bg-gray-400 text-white px-4 py-2 rounded-lg transition-colors"
>
Put Value
</button>
<button
onClick={handleGetValue}
disabled={kv.isLoading || !inputKey}
className="bg-blue-500 hover:bg-blue-600 disabled:bg-gray-400 text-white px-4 py-2 rounded-lg transition-colors"
>
Get Value
</button>
<button
onClick={handleDeleteValue}
disabled={kv.isLoading || !inputKey}
className="bg-red-500 hover:bg-red-600 disabled:bg-gray-400 text-white px-4 py-2 rounded-lg transition-colors"
>
Delete Value
</button>
</div>
{/* List Keys Section */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Search Prefix
</label>
<div className="flex gap-3">
<input
type="text"
value={searchPrefix}
onChange={(e) => setSearchPrefix(e.target.value)}
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="Enter prefix to search"
/>
<button
onClick={handleListKeys}
disabled={kv.isLoading}
className="bg-purple-500 hover:bg-purple-600 disabled:bg-gray-400 text-white px-4 py-2 rounded-lg transition-colors"
>
List Keys
</button>
</div>
</div>
{/* Batch Operations */}
<div>
<button
onClick={handleBatchPut}
disabled={kv.isLoading}
className="bg-orange-500 hover:bg-orange-600 disabled:bg-gray-400 text-white px-4 py-2 rounded-lg transition-colors"
>
Batch Put Demo Items
</button>
</div>
{/* Clear States */}
<div>
<button
onClick={kv.clearStates}
className="bg-gray-500 hover:bg-gray-600 text-white px-4 py-2 rounded-lg transition-colors"
>
Clear All States
</button>
</div>
</div>
</div>
</div>
)
}
export default CloudflareKVDemo

View File

@ -0,0 +1,291 @@
/**
* React Hook for Cloudflare KV Operations
*
* This hook provides a convenient way to interact with Cloudflare KV
* from React components with loading states and error handling.
*/
import { useState, useCallback, useEffect } from 'react'
import { cloudflareKV, CloudflareKVClient } from '../utils/cloudflareKV'
interface UseCloudflareKVOptions {
client?: CloudflareKVClient
}
interface KVOperationState<T = any> {
data: T | null
loading: boolean
error: string | null
}
export function useCloudflareKV(options: UseCloudflareKVOptions = {}) {
const client = options.client || cloudflareKV
// State for get operations
const [getState, setGetState] = useState<KVOperationState>({
data: null,
loading: false,
error: null
})
// State for put operations
const [putState, setPutState] = useState<KVOperationState>({
data: null,
loading: false,
error: null
})
// State for delete operations
const [deleteState, setDeleteState] = useState<KVOperationState>({
data: null,
loading: false,
error: null
})
// State for list operations
const [listState, setListState] = useState<KVOperationState>({
data: null,
loading: false,
error: null
})
/**
* Get a value from KV store
*/
const get = useCallback(async <T = any>(key: string, parseJSON: boolean = true): Promise<T | string | null> => {
setGetState({ data: null, loading: true, error: null })
try {
const result = await client.get<T>(key, parseJSON)
setGetState({ data: result, loading: false, error: null })
return result
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
setGetState({ data: null, loading: false, error: errorMessage })
throw error
}
}, [client])
/**
* Put a value to KV store
*/
const put = useCallback(async (key: string, value: any, metadata?: Record<string, any>) => {
setPutState({ data: null, loading: true, error: null })
try {
const result = await client.put(key, value, metadata)
setPutState({ data: result, loading: false, error: null })
return result
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
setPutState({ data: null, loading: false, error: errorMessage })
throw error
}
}, [client])
/**
* Delete a key from KV store
*/
const deleteKey = useCallback(async (key: string) => {
setDeleteState({ data: null, loading: true, error: null })
try {
const result = await client.delete(key)
setDeleteState({ data: result, loading: false, error: null })
return result
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
setDeleteState({ data: null, loading: false, error: errorMessage })
throw error
}
}, [client])
/**
* List keys from KV store
*/
const listKeys = useCallback(async (prefix?: string, limit: number = 1000, cursor?: string) => {
setListState({ data: null, loading: true, error: null })
try {
const result = await client.listKeys(prefix, limit, cursor)
setListState({ data: result, loading: false, error: null })
return result
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
setListState({ data: null, loading: false, error: errorMessage })
throw error
}
}, [client])
/**
* Check if a key exists
*/
const exists = useCallback(async (key: string): Promise<boolean> => {
try {
return await client.exists(key)
} catch (error) {
console.error('Error checking key existence:', error)
return false
}
}, [client])
/**
* Put multiple values in batch
*/
const putBatch = useCallback(async (entries: Array<{ key: string; value: any; metadata?: Record<string, any> }>) => {
setPutState({ data: null, loading: true, error: null })
try {
const result = await client.putBatch(entries)
setPutState({ data: result, loading: false, error: null })
return result
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
setPutState({ data: null, loading: false, error: errorMessage })
throw error
}
}, [client])
/**
* Clear all operation states
*/
const clearStates = useCallback(() => {
setGetState({ data: null, loading: false, error: null })
setPutState({ data: null, loading: false, error: null })
setDeleteState({ data: null, loading: false, error: null })
setListState({ data: null, loading: false, error: null })
}, [])
return {
// Operations
get,
put,
delete: deleteKey,
listKeys,
exists,
putBatch,
clearStates,
// States
getState,
putState,
deleteState,
listState,
// Convenience getters
isLoading: getState.loading || putState.loading || deleteState.loading || listState.loading,
hasError: !!(getState.error || putState.error || deleteState.error || listState.error),
errors: {
get: getState.error,
put: putState.error,
delete: deleteState.error,
list: listState.error
}
}
}
/**
* Hook for managing a specific KV key with automatic loading and caching
*/
export function useKVValue<T = any>(key: string, parseJSON: boolean = true, autoLoad: boolean = true) {
const [value, setValue] = useState<T | string | null>(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [lastUpdated, setLastUpdated] = useState<Date | null>(null)
const kv = useCloudflareKV()
/**
* Load the value from KV
*/
const load = useCallback(async () => {
if (!key) return
setLoading(true)
setError(null)
try {
const result = await kv.get<T>(key, parseJSON)
setValue(result)
setLastUpdated(new Date())
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to load value'
setError(errorMessage)
} finally {
setLoading(false)
}
}, [key, parseJSON, kv])
/**
* Save a new value to KV
*/
const save = useCallback(async (newValue: T | string, metadata?: Record<string, any>) => {
if (!key) return
setLoading(true)
setError(null)
try {
await kv.put(key, newValue, metadata)
setValue(newValue)
setLastUpdated(new Date())
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to save value'
setError(errorMessage)
throw err
} finally {
setLoading(false)
}
}, [key, kv])
/**
* Delete the value from KV
*/
const remove = useCallback(async () => {
if (!key) return
setLoading(true)
setError(null)
try {
await kv.delete(key)
setValue(null)
setLastUpdated(new Date())
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to delete value'
setError(errorMessage)
throw err
} finally {
setLoading(false)
}
}, [key, kv])
/**
* Refresh the value from KV
*/
const refresh = useCallback(() => {
return load()
}, [load])
// Auto-load on mount if enabled
useEffect(() => {
if (autoLoad && key) {
load()
}
}, [autoLoad, key, load])
return {
value,
loading,
error,
lastUpdated,
load,
save,
remove,
refresh,
exists: value !== null
}
}
// Export types
export type { KVOperationState }

268
src/utils/README.md Normal file
View File

@ -0,0 +1,268 @@
# Cloudflare KV Utility
这是一个用于与 Cloudflare KV 存储交互的 TypeScript 工具类,提供了完整的 CRUD 操作和 React Hook 集成。
## 功能特性
- ✅ 完整的 CRUD 操作Create, Read, Update, Delete
- ✅ 批量操作支持
- ✅ 元数据支持
- ✅ React Hook 集成
- ✅ TypeScript 类型安全
- ✅ 错误处理和加载状态
- ✅ 自动 JSON 解析
- ✅ 可配置的客户端
## 文件结构
```
src/utils/
├── cloudflareKV.ts # 核心 KV 客户端类
├── cloudflareKV.example.ts # 使用示例
└── README.md # 文档
src/hooks/
└── useCloudflareKV.ts # React Hook
src/components/
└── CloudflareKVDemo.tsx # 演示组件
```
## 快速开始
### 1. 基本使用
```typescript
import { cloudflareKV } from '../utils/cloudflareKV'
// 存储数据
await cloudflareKV.put('user:123', { name: 'John Doe', email: 'john@example.com' })
// 获取数据
const user = await cloudflareKV.get('user:123')
console.log(user) // { name: 'John Doe', email: 'john@example.com' }
// 删除数据
await cloudflareKV.delete('user:123')
// 检查键是否存在
const exists = await cloudflareKV.exists('user:123')
console.log(exists) // false
```
### 2. 在 React 组件中使用
```typescript
import { useCloudflareKV } from '../hooks/useCloudflareKV'
function MyComponent() {
const kv = useCloudflareKV()
const handleSave = async () => {
try {
await kv.put('my-key', { message: 'Hello World' })
console.log('保存成功!')
} catch (error) {
console.error('保存失败:', error)
}
}
return (
<div>
{kv.isLoading && <p>加载中...</p>}
{kv.hasError && <p>错误: {kv.errors.put}</p>}
<button onClick={handleSave}>保存数据</button>
</div>
)
}
```
### 3. 使用自动加载的值
```typescript
import { useKVValue } from '../hooks/useCloudflareKV'
function CounterComponent() {
const {
value: counter,
loading,
error,
save,
refresh
} = useKVValue<number>('counter', true, true) // 自动加载
const increment = async () => {
await save((counter || 0) + 1)
}
if (loading) return <div>加载中...</div>
if (error) return <div>错误: {error}</div>
return (
<div>
<p>计数器: {counter || 0}</p>
<button onClick={increment}>增加</button>
<button onClick={refresh}>刷新</button>
</div>
)
}
```
## API 参考
### CloudflareKVClient
#### 构造函数
```typescript
new CloudflareKVClient(config?: Partial<CloudflareKVConfig>)
```
#### 方法
- `put(key: string, value: any, metadata?: Record<string, any>)` - 存储键值对
- `get<T>(key: string, parseJSON?: boolean)` - 获取值
- `delete(key: string)` - 删除键
- `listKeys(prefix?: string, limit?: number, cursor?: string)` - 列出键
- `exists(key: string)` - 检查键是否存在
- `putBatch(entries: Array<{key, value, metadata?}>)` - 批量存储
### useCloudflareKV Hook
返回对象包含:
- **操作方法**: `get`, `put`, `delete`, `listKeys`, `exists`, `putBatch`
- **状态**: `getState`, `putState`, `deleteState`, `listState`
- **便捷属性**: `isLoading`, `hasError`, `errors`
### useKVValue Hook
用于管理单个 KV 键的 Hook返回
- `value` - 当前值
- `loading` - 加载状态
- `error` - 错误信息
- `save(newValue)` - 保存新值
- `remove()` - 删除值
- `refresh()` - 刷新值
## 配置
默认配置基于 `jm_video_ui.md` 文档:
```typescript
{
apiKey: "dlGquMNiAX-S7SV9pXne7YGfdH_fEgq3TfIGgNcQ",
baseUrl: "https://api.cloudflare.com/client/v4",
accountId: "67720b647ff2b55cf37ba3ef9e677083",
kvNamespaceId: "995d7eff088547d7a7d8fea53e56115b"
}
```
### 自定义配置
```typescript
import { CloudflareKVClient } from '../utils/cloudflareKV'
const customKV = new CloudflareKVClient({
apiKey: 'your-custom-api-key',
// 其他配置使用默认值
})
```
## 使用场景
### 1. 用户偏好设置
```typescript
// 保存用户设置
await cloudflareKV.put('user:123:preferences', {
theme: 'dark',
language: 'zh-CN',
notifications: true
})
// 加载用户设置
const preferences = await cloudflareKV.get('user:123:preferences')
```
### 2. 视频模板存储
```typescript
// 存储视频模板
const template = {
id: 'template-001',
name: '现代介绍模板',
duration: 5000,
assets: [...],
settings: {...}
}
await cloudflareKV.put(`template:${template.id}`, template)
// 创建模板索引
await cloudflareKV.put(`template:index:${template.id}`, {
id: template.id,
name: template.name,
thumbnail: 'https://example.com/thumb.jpg'
})
```
### 3. 缓存数据
```typescript
// 缓存 API 响应
const cacheKey = `api:users:${userId}`
const cachedData = await cloudflareKV.get(cacheKey)
if (!cachedData) {
const freshData = await fetchUserData(userId)
await cloudflareKV.put(cacheKey, freshData, {
ttl: 3600 // 1小时过期
})
return freshData
}
return cachedData
```
## 错误处理
所有方法都会抛出错误,建议使用 try-catch 处理:
```typescript
try {
await cloudflareKV.put('key', 'value')
} catch (error) {
if (error.message.includes('HTTP 401')) {
console.error('API 密钥无效')
} else if (error.message.includes('HTTP 429')) {
console.error('请求频率限制')
} else {
console.error('未知错误:', error.message)
}
}
```
## 演示
运行演示组件查看完整功能:
```typescript
import CloudflareKVDemo from '../components/CloudflareKVDemo'
// 在你的应用中使用
<CloudflareKVDemo />
```
## 注意事项
1. **API 密钥安全**: 在生产环境中,请将 API 密钥存储在环境变量中
2. **请求限制**: Cloudflare KV 有请求频率限制,请合理使用
3. **数据大小**: 单个值最大 25MB
4. **键名限制**: 键名最大 512 字节
5. **一致性**: KV 是最终一致性存储,写入后可能需要时间同步
## 许可证
MIT License

View File

@ -0,0 +1,223 @@
/**
* Cloudflare KV Usage Examples
*
* This file demonstrates how to use the CloudflareKVClient utility class
*/
import { CloudflareKVClient, cloudflareKV } from './cloudflareKV'
// Example 1: Using the default instance
export async function basicUsageExample() {
try {
// Store a simple string value
await cloudflareKV.put('user:123', 'John Doe')
console.log('✅ Stored user name')
// Store a JSON object
const userData = {
id: 123,
name: 'John Doe',
email: 'john@example.com',
preferences: {
theme: 'dark',
language: 'en'
}
}
await cloudflareKV.put('user:123:profile', userData)
console.log('✅ Stored user profile')
// Retrieve the data
const name = await cloudflareKV.get('user:123', false) // Don't parse as JSON
const profile = await cloudflareKV.get('user:123:profile') // Parse as JSON
console.log('Retrieved name:', name)
console.log('Retrieved profile:', profile)
// Check if a key exists
const exists = await cloudflareKV.exists('user:123')
console.log('User exists:', exists)
// List keys with prefix
const keys = await cloudflareKV.listKeys('user:')
console.log('User keys:', keys.result.keys)
} catch (error) {
console.error('❌ Error in basic usage example:', error)
}
}
// Example 2: Using a custom configuration
export async function customConfigExample() {
const customKV = new CloudflareKVClient({
// You can override any configuration values
apiKey: 'your-custom-api-key',
// Other values will use defaults
})
try {
await customKV.put('custom:test', { message: 'Hello from custom config!' })
const result = await customKV.get('custom:test')
console.log('Custom config result:', result)
} catch (error) {
console.error('❌ Error in custom config example:', error)
}
}
// Example 3: Batch operations
export async function batchOperationsExample() {
try {
// Store multiple values at once
const entries = [
{ key: 'batch:item1', value: { name: 'Item 1', price: 10.99 } },
{ key: 'batch:item2', value: { name: 'Item 2', price: 15.50 } },
{ key: 'batch:item3', value: { name: 'Item 3', price: 8.75 } }
]
const results = await cloudflareKV.putBatch(entries)
console.log('Batch put results:', results)
// Retrieve all batch items
for (const entry of entries) {
const item = await cloudflareKV.get(entry.key)
console.log(`Retrieved ${entry.key}:`, item)
}
} catch (error) {
console.error('❌ Error in batch operations example:', error)
}
}
// Example 4: Working with metadata
export async function metadataExample() {
try {
const value = { content: 'Important data' }
const metadata = {
created_at: new Date().toISOString(),
created_by: 'user:123',
version: '1.0'
}
await cloudflareKV.put('data:important', value, metadata)
console.log('✅ Stored data with metadata')
// Note: To retrieve metadata, you'd need to use the Cloudflare API directly
// as the get method only returns the value, not metadata
} catch (error) {
console.error('❌ Error in metadata example:', error)
}
}
// Example 5: Error handling and edge cases
export async function errorHandlingExample() {
try {
// Try to get a non-existent key
const nonExistent = await cloudflareKV.get('does-not-exist')
console.log('Non-existent key result:', nonExistent) // Should be null
// Store and then delete a key
await cloudflareKV.put('temp:data', 'temporary value')
console.log('✅ Stored temporary data')
const beforeDelete = await cloudflareKV.get('temp:data')
console.log('Before delete:', beforeDelete)
await cloudflareKV.delete('temp:data')
console.log('✅ Deleted temporary data')
const afterDelete = await cloudflareKV.get('temp:data')
console.log('After delete:', afterDelete) // Should be null
} catch (error) {
console.error('❌ Error in error handling example:', error)
}
}
// Example 6: Video template storage use case
export async function videoTemplateStorageExample() {
try {
const templateData = {
id: 'template-001',
name: 'Modern Intro Template',
description: 'A sleek modern intro template with animations',
duration: 5000, // 5 seconds
assets: [
{ type: 'video', url: 'https://example.com/intro.mp4' },
{ type: 'audio', url: 'https://example.com/music.mp3' }
],
settings: {
resolution: '1920x1080',
fps: 30,
format: 'mp4'
},
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
}
// Store template
await cloudflareKV.put(`template:${templateData.id}`, templateData)
console.log('✅ Stored video template')
// Store template index for quick listing
const templateIndex = {
id: templateData.id,
name: templateData.name,
description: templateData.description,
thumbnail: 'https://example.com/thumbnail.jpg'
}
await cloudflareKV.put(`template:index:${templateData.id}`, templateIndex)
console.log('✅ Stored template index')
// Retrieve template
const retrievedTemplate = await cloudflareKV.get(`template:${templateData.id}`)
console.log('Retrieved template:', retrievedTemplate)
// List all templates
const templateKeys = await cloudflareKV.listKeys('template:index:', 100)
console.log('Template list:', templateKeys.result.keys)
} catch (error) {
console.error('❌ Error in video template storage example:', error)
}
}
// Run all examples
export async function runAllExamples() {
console.log('🚀 Running Cloudflare KV Examples...\n')
console.log('1. Basic Usage Example:')
await basicUsageExample()
console.log('\n')
console.log('2. Custom Config Example:')
await customConfigExample()
console.log('\n')
console.log('3. Batch Operations Example:')
await batchOperationsExample()
console.log('\n')
console.log('4. Metadata Example:')
await metadataExample()
console.log('\n')
console.log('5. Error Handling Example:')
await errorHandlingExample()
console.log('\n')
console.log('6. Video Template Storage Example:')
await videoTemplateStorageExample()
console.log('\n')
console.log('✅ All examples completed!')
}
// Export individual examples for selective testing
export {
basicUsageExample,
customConfigExample,
batchOperationsExample,
metadataExample,
errorHandlingExample,
videoTemplateStorageExample
}

256
src/utils/cloudflareKV.ts Normal file
View File

@ -0,0 +1,256 @@
/**
* Cloudflare KV Storage Utility Class
*
* This utility class provides methods to interact with Cloudflare KV storage
* for storing and retrieving key-value pairs.
*/
interface CloudflareKVConfig {
apiKey: string
baseUrl: string
accountId: string
kvNamespaceId: string
}
interface CloudflareKVResponse<T = any> {
success: boolean
errors: Array<{ code: number; message: string }>
messages: string[]
result: T
}
export class CloudflareKVClient {
private config: CloudflareKVConfig
constructor(config?: Partial<CloudflareKVConfig>) {
this.config = {
apiKey: config?.apiKey || "dlGquMNiAX-S7SV9pXne7YGfdH_fEgq3TfIGgNcQ",
baseUrl: config?.baseUrl || "https://api.cloudflare.com/client/v4",
accountId: config?.accountId || "67720b647ff2b55cf37ba3ef9e677083",
kvNamespaceId: config?.kvNamespaceId || "995d7eff088547d7a7d8fea53e56115b"
}
}
/**
* Get the base URL for KV operations
*/
private getKVBaseUrl(): string {
return `${this.config.baseUrl}/accounts/${this.config.accountId}/storage/kv/namespaces/${this.config.kvNamespaceId}`
}
/**
* Get common headers for API requests
*/
private getHeaders(): HeadersInit {
return {
'Authorization': `Bearer ${this.config.apiKey}`,
'Content-Type': 'application/json'
}
}
/**
* Store a value in Cloudflare KV
*
* @param key - The key to store the value under
* @param value - The value to store (will be JSON stringified if not a string)
* @param metadata - Optional metadata to associate with the key-value pair
* @returns Promise resolving to the API response
*/
async put(key: string, value: any, metadata?: Record<string, any>): Promise<CloudflareKVResponse> {
try {
const url = `${this.getKVBaseUrl()}/values/${encodeURIComponent(key)}`
const body: any = {
value: typeof value === 'string' ? value : JSON.stringify(value)
}
if (metadata) {
body.metadata = JSON.stringify(metadata)
}
const response = await fetch(url, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${this.config.apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
})
const result = await response.json()
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${result.errors?.[0]?.message || 'Unknown error'}`)
}
return result
} catch (error) {
console.error('Error putting value to Cloudflare KV:', error)
throw error
}
}
/**
* Retrieve a value from Cloudflare KV
*
* @param key - The key to retrieve
* @param parseJSON - Whether to attempt JSON parsing of the retrieved value
* @returns Promise resolving to the stored value
*/
async get<T = any>(key: string, parseJSON: boolean = true): Promise<T | string | null> {
try {
const url = `${this.getKVBaseUrl()}/values/${encodeURIComponent(key)}`
const response = await fetch(url, {
method: 'GET',
headers: {
'Authorization': `Bearer ${this.config.apiKey}`
}
})
if (response.status === 404) {
return null
}
if (!response.ok) {
const errorData = await response.json()
throw new Error(`HTTP ${response.status}: ${errorData.errors?.[0]?.message || 'Unknown error'}`)
}
const textValue = await response.text()
if (parseJSON) {
try {
return JSON.parse(textValue) as T
} catch {
// If JSON parsing fails, return as string
return textValue as any
}
}
return textValue as any
} catch (error) {
console.error('Error getting value from Cloudflare KV:', error)
throw error
}
}
/**
* Delete a key-value pair from Cloudflare KV
*
* @param key - The key to delete
* @returns Promise resolving to the API response
*/
async delete(key: string): Promise<CloudflareKVResponse> {
try {
const url = `${this.getKVBaseUrl()}/values/${encodeURIComponent(key)}`
const response = await fetch(url, {
method: 'DELETE',
headers: this.getHeaders()
})
const result = await response.json()
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${result.errors?.[0]?.message || 'Unknown error'}`)
}
return result
} catch (error) {
console.error('Error deleting value from Cloudflare KV:', error)
throw error
}
}
/**
* List keys in the KV namespace
*
* @param prefix - Optional prefix to filter keys
* @param limit - Maximum number of keys to return (default: 1000)
* @param cursor - Cursor for pagination
* @returns Promise resolving to the list of keys
*/
async listKeys(prefix?: string, limit: number = 1000, cursor?: string): Promise<CloudflareKVResponse<{
keys: Array<{
name: string
expiration?: number
metadata?: Record<string, any>
}>
list_complete: boolean
cursor?: string
}>> {
try {
const params = new URLSearchParams()
if (prefix) params.append('prefix', prefix)
if (limit) params.append('limit', limit.toString())
if (cursor) params.append('cursor', cursor)
const url = `${this.getKVBaseUrl()}/keys?${params.toString()}`
const response = await fetch(url, {
method: 'GET',
headers: this.getHeaders()
})
const result = await response.json()
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${result.errors?.[0]?.message || 'Unknown error'}`)
}
return result
} catch (error) {
console.error('Error listing keys from Cloudflare KV:', error)
throw error
}
}
/**
* Check if a key exists in the KV store
*
* @param key - The key to check
* @returns Promise resolving to boolean indicating if key exists
*/
async exists(key: string): Promise<boolean> {
try {
const value = await this.get(key, false)
return value !== null
} catch (error) {
console.error('Error checking key existence in Cloudflare KV:', error)
return false
}
}
/**
* Store multiple key-value pairs in batch
*
* @param entries - Array of key-value pairs to store
* @returns Promise resolving to array of results
*/
async putBatch(entries: Array<{ key: string; value: any; metadata?: Record<string, any> }>): Promise<CloudflareKVResponse[]> {
const results = await Promise.allSettled(
entries.map(entry => this.put(entry.key, entry.value, entry.metadata))
)
return results.map((result, index) => {
if (result.status === 'fulfilled') {
return result.value
} else {
console.error(`Failed to put key ${entries[index].key}:`, result.reason)
return {
success: false,
errors: [{ code: -1, message: result.reason.message || 'Unknown error' }],
messages: [],
result: null
}
}
})
}
}
// Export a default instance with the default configuration
export const cloudflareKV = new CloudflareKVClient()
// Export types for external use
export type { CloudflareKVConfig, CloudflareKVResponse }