自定义供应商保存永久失败:updated_at 为非数值时 Math.max 产生 NaN
- Dominant language
- TypeScript
- Stars
- 2.7k
- Forks
- 395
- Avg merge
- 21h 48m
- Merged PRs (30d)
- 776
Description
**提交人**: 用户1135
**客户端版本**: 0.1.57
---
## 现象
编辑自定义供应商并保存时,界面只提示「保存失败」,没有可操作的原因。受影响的供应商此后**永久无法保存任何修改**(包括修正模型上下文窗口等字段)。
主进程日志中的实际错误:
```
[ERROR] [console] Error occurred in handler for 'maker:provider:custom:update':
SqliteError: NOT NULL constraint failed: custom_providers.updated_at
code: 'SQLITE_CONSTRAINT_NOTNULL'
```
## 原因
`custom_providers.updated_at` 声明为 INTEGER(epoch ms)。两处写入表达式对读到的旧值不做数值转换:
```js
// updateCustomProvider
.set({ ..., updatedAt: Math.max(n, s.updatedAt + 1) })
// updateCustomProviderIfUnchanged
.set({ ..., updatedAt: Math.max(i, a.updatedAt + 1) })
```
若该行的 `updated_at` 实际存的是字符串(例如 ISO 日期),`s.updatedAt + 1` 变成字符串拼接,`Math.max(...)` 得到 `NaN`,写入 NOT NULL 整数列即违反约束。
建表语句未使用 SQLite `STRICT` 模式,因此 INTEGER 列可以存放文本,类型声明不构成实际约束:
```sql
CREATE TABLE `custom_providers` (
`id` text PRIMARY KEY NOT NULL,
`name` text NOT NULL,
`runtimes` text DEFAULT '{}' NOT NULL,
`sort_order` integer DEFAULT 0 NOT NULL,
`created_at` integer NOT NULL,
`updated_at` integer NOT NULL
, auth text)
```
读取路径 `rowToConfig` 只取 `id / name / runtimes / auth`,不读也不校验 `updated_at`,所以异常值在写入前不会被发现。
同一构建中其它表使用的是不含 `+1` 的写法(如 `Math.max(a.updatedAt, b.updatedAt)`),不受影响。
## 复现步骤
不依赖任何本地状态,用上述建表语句即可复现(Node 22+):
```js
import { DatabaseSync } from 'node:sqlite';
const db = new DatabaseSync(':memory:');
db.exec("CREATE TABLE `custom_providers` (`id` text PRIMARY KEY NOT NULL, `name` text NOT NULL, `runtimes` text DEFAULT '{}' NOT NULL, `sort_order` integer DEFAULT 0 NOT NULL, `created_at` integer NOT NULL, `updated_at` integer NOT NULL, auth text)");
const ins = db.prepare('INSERT INTO custom_providers (id,name,runtimes,sort_order,created_at,updated_at) VALUES (?,?,?,?,?,?)');
ins.run('a', 'n', '{}', 0, 1786000000000, 1786500000000); // integer
ins.run('b', 'n', '{}', 0, 1786000000000, '2026-08-19T01:45:07Z'); // text — 因无 STRICT 而被接受
const upd = db.prepare('UPDATE custom_providers SET updated_at = ? WHERE id = ?');
for (const r of db.prepare('SELECT id, updated_at FROM custom_providers').all()) {
const value = Math.max(Date.now(), r.updated_at + 1); // 与发布版一致的表达式
try { upd.run(value, r.id); console.log(r.id, 'OK'); }
catch (e) { console.log(r.id, e.code, e.message); }
}
```
输出:
```
a OK
b ERR_SQLITE_ERROR NOT NULL constraint failed: custom_providers.updated_at
```
加一层 `Number()` 转换后两行均写入成功。
## 期望行为
保存成功;若确实无法保存,应给出可定位的原因而不是只显示「保存失败」。
## 实际行为
保存被拒绝,数据库未更新,界面只显示「保存失败」,用户无从判断原因,也无法自行恢复。
## 复现频率
受影响的行 100% 失败,每次保存都失败,重启和升级均不缓解。
## 已尝试
- 重启应用:无效,仍然保存失败
- 升级客户端版本:无效,新版本中上述两处表达式未变
- 唯一有效的办法是把该列的值改回整数(数据修复)。修好后界面即可正常保存,但若将来再次写入非数值,问题会重现。
## 建议
1. 写入前对 `updated_at` 做数值转换或校验,非数值时回退为 `Date.now()`,避免产生 `NaN`。
2. 保存失败时把可定位的原因返回到界面。
3. 可考虑对该列做一次性数据修正,并给表加 `STRICT`,防止异常值继续写入。
## 触发来源未知
没能确定字符串值最初是怎么写进去的,不做推测。已排除:
- 迁移:出问题的时间点 `localDb.migrate` 报告已是最新版本,无待执行迁移
- hotfix:该时间点无 hotfix 应用记录
- 应用自身写入路径:`custom_providers` 只有三处写入,均为整数表达式,无裸 SQL
- 无供应商导入/恢复类功能会写这张表
观察到的现象是多行在同一秒内变成 ISO 字符串格式;前一天的数据库备份中这些行仍为整数。
---
**版本区域**: CN
**OS**: win32 x64 (10.0.26200)
**界面语言**: zh-CN
Contributor guide
Research direction
Start with the updateCustomProvider and updateCustomProviderIfUnchanged write paths, then inspect rowToConfig and the maker:provider:custom:update handler. Run the provided Node 22+ SQLite reproduction with both numeric and ISO-string updated_at values. Done means affected providers save successfully without a NOT NULL failure and the UI exposes a locatable error when saving still fails.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- sqlite, typescript
- Domain
- backend, database
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 68/100