Vercel AI SDK 的工具审批(Approval)流程详解

概述

在 AI 应用中,当 AI 助手需要调用本地文件系统或其他敏感工具时,出于安全考虑,需要在执行前获得用户的明确批准。Vercel AI SDK(文中使用 v6 版本,这狗东西 API 一直变来变去的)提供了工具审批(Tool Approval)机制,允许开发者在 AI 执行工具前拦截并请求用户确认。

注意这个参数主要是为了配合stopWhen(也就是工具自动调用),也就是:

1
2
3
4
5
6
const result = streamText({
model: newapi.chat(config.model.model),
messages,
tools,
stopWhen: stepCountIs(5), // 这里 自动工具调用
});

同时也注意这东西的调用方式灵活的很,毕竟他不是应用级别的封装,不用这套自己也可以搓一个出来。

同时在 AI SDK UI 中的调用流程是完全不一样的,这里只针对 Core 的 API。(但是其实也八九不离十了)

Read More

Astro 项目集成 Better Auth 指南(使用drizzle+PG)

本文档说明如何在 Astro 项目中集成 Better Auth,使用 Drizzle 作为数据库适配器,PostgreSQL 作为数据库。

emmmm,文档是在实践完之后写的,不是边实践边写的,可能会有所遗漏,但是扫了几遍源码,应该八九不离十。

注意这个需要 server 支持,所以需要 server adapter,比如 node 的@astrojs/node

我使用的是默认 static,需要后端运行时渲染的地方用export const prerender = false;,如果你的 output 设置为了 server,那么就不需要 prerender=false。

📖 官方文档参考


步骤 1:安装依赖

首先,安装 Better Auth 及相关依赖:

1
2
3
4
5
# 安装核心依赖
pnpm add better-auth drizzle-orm pg dotenv

# 安装开发依赖
pnpm add -D drizzle-kit tsx @types/pg

依赖说明

  • better-auth: 认证核心库
  • drizzle-orm: ORM 库,用于数据库操作
  • pg: PostgreSQL 驱动
  • drizzle-kit: Drizzle 开发工具(用于数据库迁移)
  • dotenv: 环境变量加载(drizzle.config.ts 需要

Read More

字符编码详解:UTF-8、Unicode、GBK 等

编辑说明:本文初稿完成后,通过 Claude Code 进行了技术审阅和优化润色,修正了 UTF-16 编码方式的描述,补充了 Unicode 平面结构、代理对机制、MySQL utf8mb4 等深度内容,使文章在保持通俗易懂的同时更加准确严谨。


字符编码详解:UTF-8、Unicode、GBK 等

一、核心概念:Unicode vs UTF-8

用快递系统来理解

Unicode(统一字符集)

  • 相当于:全国统一的地址编号系统
  • 作用:给世界上每个文字、符号分配唯一编号
  • 例子:
    • ‘A’ → U+0041(编号 65)
    • ‘中’ → U+4E2D(编号 20013)
    • ‘😊’ → U+1F60A(编号 128522)
  • 特点:只定义编号,不管怎么存储

UTF-8(编码方式)

UTF-8 根据 Unicode 码点的大小,使用不同长度的字节:

Unicode 范围 字节数 UTF-8 格式
U+0000 ~ U+007F 1 字节 0xxxxxxx
U+0080 ~ U+07FF 2 字节 110xxxxx 10xxxxxx
U+0800 ~ U+FFFF 3 字节 1110xxxx 10xxxxxx 10xxxxxx
U+10000 ~ U+10FFFF 4 字节 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
  • 相当于:快递打包和运输方式
  • 作用:把 Unicode 编号转换成实际的字节数据
  • 例子:
    • ‘A’ (U+0041) → 01000001(1 字节)
    • ‘中’ (U+4E2D) → 11100100 10111000 10101101(3 字节)
    • ‘😊’ (U+1F60A) → 11110000 10011111 10011000 10001010(4 字节)
  • 特点:变长编码,节省空间

Read More

深入理解 SSR/CSR 与 SPA/MPA:现代前端渲染模式详解

前言

在学习现代前端框架时,你可能会发现它们的 SSR(服务端渲染)与传统后端框架的 SSR 似乎不太一样。这篇文章将详细解释它们的本质区别,以及 SSR/CSR 和 SPA/MPA 这两个维度的概念关系。

传统后端 SSR vs 现代前端 SSR

传统后端 SSR(PHP、JSP、Rails)

工作流程:

1
2
3
浏览器 → 请求页面 → 服务器生成完整HTML → 返回HTML → 浏览器显示

每次交互都重复这个过程

特点:

  • 纯服务器渲染,返回的是完整的静态 HTML
  • 每次交互都需要刷新整个页面
  • JavaScript 只是点缀(表单验证、简单动效)
  • 模板引擎在服务器端运行(如 PHP、EJS、Jinja2)

现代前端框架 SSR(Next.js、Nuxt、Remix)

工作流程:

1
2
3
4
5
首次访问:
浏览器 → 服务器SSR → 返回HTML + JS Bundle → Hydration → 变成SPA

后续导航:
客户端路由 → 仅获取数据 → 客户端渲染 → 无刷新更新

Read More

6GB显存跑Z-Image-Turbo完全指南

前言

我的拖拉机又开动了(指显卡风扇的噪音)。

这次是阿里开源的 Z-Image-Turbo,6GB 显存也能跑,而且效果还挺好。这篇博客就是纯纯的配置教程,不整那些虚的,直接告诉你怎么让小显存的卡也能愉快生图。

准备工作

首先,你需要:

  • ComfyUI(应该已经装好了吧,注意需要更新到最新版本,显卡驱动也要最新)
  • 6-12GB 显存(我用的 2060 6GB)
  • 16 以上内存(我用的是 16GB 内存,其实已经 swap 了)
  • 足够的硬盘空间下模型(量化版本所有东西最好保证 15GB 左右的空余空间)

第一步:拿官方工作流

官方已经给你做好工作流了,直接拖进 ComfyUI 的 web 界面就能用:

👉 官方工作流https://comfyanonymous.github.io/ComfyUI_examples/z_image/

拖进去之后你会发现缺模型,别慌,继续往下看。

Read More

自行部署 Next.js Docker 的挑战:臃肿、平台绑定与隐藏的“坑”

Next.js 作为目前最受欢迎的 React 全栈框架之一,凭借其强大的功能和“开箱即用”的体验赢得了大量开发者。然而,这种便利性很大程度上依赖于其创造者 Vercel 提供的原生托管平台。但是一旦开发者选择通过 Docker 自行部署,便会发现这条路并非坦途,充满了各种不易察觉的“坑”,并且会逐渐感受到框架本身的臃肿与对特定平台的深度绑定。

这里就总结了一下我这一年实践遇到的问题,建议没有 SEO 需求还是不要碰他了。

路由与请求类型的“黑盒”

在自行部署的环境中,Next.js 的一些内部机制会变得异常棘手。以 React Server Components (RSC) 的请求为例,它与页面的请求路径一致,但会附加一个 ?_rsc=... 参数以区分(也会增加一些 header)。问题在于,这个参数(以及额外的 header)在到达我们自己的应用逻辑之前,往往已被 Next.js 框架内部“消化”。这意味着,proxy.ts(16 之前的middleware.ts),无法捕获到 _rsc 这个参数。因此,从代码层面来看,开发者无法轻易判断一个传入的请求究竟是完整的页面加载请求,还是一个用于更新部分 UI 的 RSC 请求。

这在一些重定向场景中会造成一些问题。

Read More

Next.js 16 Cache Components 完全指南

基于 Next.js 16.0.1 官方文档整理

目录

  1. 什么是 Cache Components
  2. 核心工作原理
  3. 使用 Suspense 边界
  4. 使用 use cache
  5. 启用 Cache Components
  6. 从旧版本迁移
  7. 实战示例
  8. 最佳实践
  9. 和 Next-intl 结合
  10. 常见错误与解决方案
  11. FAQ
  12. 参考资料

这边文档主要是 AI 总结+我补充实际遇到的问题,大部分是 AI 写的。

注意,文档里不包含use cache: private的内容,在写的时候本来官方文档里说依赖unstable_prefetch,但是后来一看这个内容又被移除了,不知道后续会不会再改,先不写了,以官方文档为准。API Reference > Directives > use cache: private

注意,现在的 16.0 还在不断变化中,还是等 16.1,16.2 再用吧。这东西太不稳了。

Cache components除了方便的 PPR+显式缓存,另外一个就是在框架层面(主要是 dev 和构建的时候),防止用户写出动态内容卡住整个页面加载的事,这在之前很容易写出来,网上也有很多批评的文章和视频,一看连loading.tsxSuspense都不会用。😂
现在官方强制了,也是件好事吧。

什么是 Cache Components

Cache Components 是 Next.js 16 中一种新的渲染和缓存方法,通过 Partial Prerendering (PPR) 提供细粒度的缓存控制,同时确保出色的用户体验。

核心关系

1
Cache Components = PPR + use cache
  • PPR 提供静态外壳和流式传输基础设施
  • use cache 让你在外壳中包含优化的动态输出

解决的问题

在开发动态应用时,你需要在两种方式之间权衡:

  • 完全静态页面:加载快,但无法显示个性化或实时数据
  • 完全动态页面:可显示最新数据,但每次请求都需要渲染所有内容,导致初始加载慢

Read More

在容器中运行的mysql数据库定时备份脚本

单次备份直接跑:

1
docker exec -it mysql容器名/id mysqldump --all-databases -u用户 -p > ./mysql-backup/backup_$(date +%Y%m%d_%H%M).sql

命令行提示输入密码

注意使用--all-databases会连 mysql 本身内部数据库也导出。

批量定时让 AI 写了个脚本,改了几个版本,测试过没啥太大问题。

不过这个版本会把密码留在命令行里,我这里是单独建了个 localhost 能用的密码,不放心可以写临时文件/放环境变量等方法。

会创建当前时间的文件夹,里面按照每个数据库存放对应名字的 sql,设置了过期时间。

这个只能针对比较小的数据库备份,几 G 的无所谓,几十 G 上百 G 的就很慢了(这种业务量还是买云上的数据库吧)。

定时部分直接用 crontab 调这个脚本就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
#!/bin/bash

# =================================================================
# Docker MySQL 数据库备份脚本
# - 为每次备份创建独立的、带时间戳的文件夹
# - 备份为纯 .sql 文件,不压缩
# - 仅在所有备份成功后,才清理旧的备份文件夹
# - 清理前,在日志中一次性记录所有将被删除的目录列表
# =================================================================

# --- 自定义配置 ---

# MySQL 容器名称
CONTAINER_NAME="your_mysql_container_name"

# MySQL 用户名 (推荐使用下面创建的 'backup' 用户)
MYSQL_USER="backup"

# MySQL 密码 (您为 'backup' 用户设置的密码)
MYSQL_PASSWORD="your_strong_password_here"

# 需要备份的数据库列表,多个数据库用空格隔开
# 例如: "db1 db2 db3"
DATABASE_NAMES="your_db_name1 your_db_name2"

# 备份文件存放的根目录
BACKUP_ROOT_DIR="/data/mysql_backups"

# 备份文件保留天数
RETENTION_DAYS=30

# 日志文件路径
LOG_FILE="${BACKUP_ROOT_DIR}/backup.log"

# --- 脚本主体 ---

# 1. 创建本次备份的专属文件夹
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
CURRENT_BACKUP_DIR="${BACKUP_ROOT_DIR}/${TIMESTAMP}"
mkdir -p ${CURRENT_BACKUP_DIR}

# --- 函数定义 ---
log() {
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> ${LOG_FILE}
}

# 2. 执行备份
log "开始备份任务,备份至目录: ${CURRENT_BACKUP_DIR}"
if [ -z "${DATABASE_NAMES}" ]; then
log "错误: 没有指定要备份的数据库 (DATABASE_NAMES 为空)。"
exit 1
fi

ALL_BACKUPS_SUCCEEDED=true
for DB_NAME in ${DATABASE_NAMES}
do
log "正在备份数据库: ${DB_NAME}"
BACKUP_FILE="${CURRENT_BACKUP_DIR}/${DB_NAME}.sql"
docker exec ${CONTAINER_NAME} /usr/bin/mysqldump -u${MYSQL_USER} -p${MYSQL_PASSWORD} --databases ${DB_NAME} --single-transaction > ${BACKUP_FILE}
if [ $? -eq 0 ]; then
log "数据库 ${DB_NAME} 备份成功, 文件: ${BACKUP_FILE}"
else
log "错误: 数据库 ${DB_NAME} 备份失败。"
ALL_BACKUPS_SUCCEEDED=false
rm -f ${BACKUP_FILE}
fi
done
log "所有数据库备份操作执行完毕"

# 3. 清理旧备份 (仅在所有备份都成功时执行)
if [ "${ALL_BACKUPS_SUCCEEDED}" = true ]; then
log "本次所有备份任务均成功,开始清理 ${RETENTION_DAYS} 天前的旧备份文件夹"

# --- 这里是修改的核心部分 ---
# 先找到所有要删除的目录,存入变量
DIRS_TO_DELETE=$(find ${BACKUP_ROOT_DIR} -type d -name "20[0-9][0-9]*_*" -mtime +${RETENTION_DAYS})

if [ -n "${DIRS_TO_DELETE}" ]; then
# 如果找到了需要删除的目录,则先记录日志,然后执行删除
log "将要删除以下旧备份目录:"
log "${DIRS_TO_DELETE}"
# 使用 xargs 来处理目录列表并删除
echo "${DIRS_TO_DELETE}" | xargs rm -rf
log "旧备份文件夹清理完成"
else
# 如果没有找到,也记录一下
log "没有找到需要清理的旧备份目录。"
fi
else
log "警告: 本次备份任务中存在失败项,将跳过清理旧备份的操作以确保数据安全。"
log "由于备份失败,本次创建的文件夹 ${CURRENT_BACKUP_DIR} 可能包含不完整的备份。"
fi

log "=========================================================="

timestamp存取差几小时? mysql timestamp的timezone问题以及如何在mysql2设置

在 Node.js 应用中使用 MySQL 时,时间戳(TIMESTAMP)字段出现的“8 小时差异”是一个经典难题。这个问题的根源并非单一因素,而是由 MySQL 自身的时区机制、mysql2 驱动的特定行为,以及一个极具迷惑性的默认配置共同造成的。

本文将澄清 MySQL TIMESTAMP 的存储与转换原理,并深入剖析 mysql2 驱动中 timezone 配置项的真正含义及其默认值'local'所带来的陷阱,最终提供两套清晰的最佳实践方案。

Read More

drizzle如何处理mysql的bit类型

mysql 的 boolean 类型其实是 tinyint(1)的别名,一些 ORM 工具无法识别把他识别到对应语言的 boolean 类型,日常实践有时候会选择用bit(1)这个类型,比如javaJOOQ,boolean 类型会变为 java 的Byte,而bit(1)则是 java 的Boolean

但是 drizzle 的 mysql 内置类型中没有bit(1)。你用他的drizzle-kit pull之后会返回这个结果:

1
2
// Warning: Can't parse bit(1) from database
// bit(1)Type: bit(1)("deleted").notNull(),

但是如果是已经建好的表,其他逻辑在用的话,把bit(1)改成boolean并不方便。

需要我们自己定义一个类型。

bit(1)这个类型不同驱动返回不同,有些可能是作为整数,有些可能是作为 Buffer。所以扩展一下就行:

Read More