Skip to content

Commit 23ea61f

Browse files
committed
fix(ai): v0.17.1 笔记页两报障——轨迹 NULL 列崩溃(Option 化)+ 笔记级精修卡住(hook taskIdRef 接线与取回防御)
1 parent bf49c55 commit 23ea61f

5 files changed

Lines changed: 43 additions & 5 deletions

File tree

‎app/src-tauri/src/db_ai_chat_tests.rs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,4 +142,8 @@ fn trajectory_column_roundtrip_and_migration_idempotent() {
142142
assert_eq!(got.as_deref(), Some(r#"[{"turn":1,"system":"s","user":"u","response":"r"}]"#));
143143
// 无轨迹任务 → None(旧任务诚实降级)
144144
assert!(db.get_ai_task_trajectory(2).unwrap().is_none());
145+
// v0.17.1 回归:任务存在但列为 NULL(旧任务/未写入轨迹)→ Ok(None)——
146+
// 此前 get::<String> 对 NULL 列抛 "Invalid column type Null"(用户报障)
147+
db.insert_ai_task(&task_rec(3, "succeeded")).unwrap();
148+
assert!(db.get_ai_task_trajectory(3).unwrap().is_none());
145149
}

‎app/src-tauri/src/db_ai_tasks.rs‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,14 +186,21 @@ impl Db {
186186
}
187187

188188
/// 轨迹读取(任务对话视图数据源;NULL/无轨迹 → None——升级前旧任务)。
189+
///
190+
/// @ai-context: v0.17.1 修复:行存在但列为 NULL 时 `get::<String>` 抛
191+
/// "Invalid column type Null at index: 0"(rusqlite 类型
192+
/// 严格)——须 Option<String>(NULL=无轨迹=诚实 None;旧
193+
/// 任务/失败任务/轨迹未写入均合法)。
189194
pub fn get_ai_task_trajectory(&self, task_id: u64) -> Result<Option<String>> {
190195
let conn = self.conn.lock().unwrap_or_else(|e| e.into_inner());
196+
// 双层 Option 拍平:列 NULL(无轨迹/旧任务)→ 外层 None(与"无行"同语义)
191197
conn.query_row(
192198
"SELECT trajectory_json FROM ai_tasks WHERE task_id=?1",
193199
params![task_id as i64],
194-
|row| row.get::<_, String>(0),
200+
|row| row.get::<_, Option<String>>(0),
195201
)
196202
.optional()
203+
.map(|v| v.flatten())
197204
.map_err(Into::into)
198205
}
199206

‎app/src/components/NoteAiDialog.tsx‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
* 状态机);重生成沿用本次策略档位(overrideFromInfo——首版与
1111
* 重生成同档位,不回退全局默认)。
1212
*/
13-
import { useCallback, useRef, useState } from "react";
13+
import { useCallback, useState } from "react";
1414
import { invoke } from "@tauri-apps/api/core";
1515
import { useAiTaskPolling } from "../hooks/useAiTaskPolling";
1616
import type { AiRefineResult, AiTaskState } from "../types";
@@ -49,7 +49,6 @@ export default function NoteAiDialog({
4949
const [result, setResult] = useState<AiRefineResult | null>(null);
5050
const [showWorkbench, setShowWorkbench] = useState(false);
5151
const [msg, setMsg] = useState("");
52-
const taskIdRef = useRef<number | null>(null);
5352

5453
const handleState = useCallback(async (st: AiTaskState, tid: number | null) => {
5554
if (st === "Succeeded") {
@@ -58,6 +57,10 @@ export default function NoteAiDialog({
5857
setResult(r);
5958
setRunning(false);
6059
setShowWorkbench(true);
60+
} else {
61+
// v0.17.1 防御:结果取回失败不得静默卡住(提示可重试)
62+
setMsg("任务已完成但结果取回失败——可关闭后到 AI 对话页「AI 任务」查看或重试");
63+
setRunning(false);
6164
}
6265
} else if (typeof st === "object" && st !== null && "Failed" in st) {
6366
setMsg(`精修失败:${JSON.stringify(st.Failed.reason ?? {})}`);
@@ -67,8 +70,11 @@ export default function NoteAiDialog({
6770
}
6871
}, []);
6972

70-
const { startPolling, stopPolling } = useAiTaskPolling(handleState, () => {
71-
setMsg("任务 30 秒无进展(可能未启动或后台卡住)——请查看日志后重试");
73+
// v0.17.1 修复:hook 的 taskIdRef 由调用方设置(事件通道过滤 + 派发参数
74+
// 数据源)——此前误用本地自建 ref → 轮询以 taskId=null 派发 → 结果取回
75+
// 失败被吞 → running 永不翻转(用户报障"长时间卡在处理页面")
76+
const { taskIdRef, startPolling, stopPolling } = useAiTaskPolling(handleState, () => {
77+
setMsg("任务 30 秒无进展(可能未启动或后台卡住)——请查看 tauri 终端 [refine-task] 日志后重试");
7278
setRunning(false);
7379
});
7480

‎docs/versions/README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
| v0.16.0 | [v0.16.0.md](./v0.16.0.md) — 内嵌 AI 对话(纯聊天 · DSH 蓝本):AI 对话页 + 流式/停止/重发 + 会话管理 + 每会话模型 + 授权/成本/审计 + 任务对话视图(REQ-224~230) | 版本文档(代码已交付 2026-08-30) |
4646
| v0.16.1 | [v0.16.1.md](./v0.16.1.md) — 用户反馈批·体验修复与 AI 对话闭环:图片插入 RangeError 修复 + 正文多色 `==[色]文本==`(含 hName 缺陷修复)+ 对话转笔记双入口 + 任务对话化(线程卡/追问/`/`命令/精修自动跳转)+ 工作台深链 + 右键菜单完整性 + 浏览器原生右键菜单去除(REQ-238~244) | 版本文档(代码已交付 2026-08-31) |
4747
| v0.17.0 | [v0.17.0.md](./v0.17.0.md) — AI 精修可到达度:策略化提示词(JSON v3 声明/四档阶梯/六旋钮/书面目标 chips/预览所见即所发)+ 笔记级 AI 能力(编辑态 🤖 AI 统一入口/手写档案/内容传参/工作台笔记级)+ 精修流式会话化(片级解析流事件/对话卡流式正文/完成双入口)(REQ-245~247,ADR-026) | 版本文档(代码已交付 2026-09-01) |
48+
| v0.17.1 | [v0.17.1.md](./v0.17.1.md) — bug 修复:知识补充轨迹 NULL 列崩溃(get_ai_task_trajectory Option 化)+ 笔记级精修卡住(NoteAiDialog hook taskIdRef 接线 + 结果取回防御) | 版本文档(代码已交付 2026-09-01) |
4849

4950
## 沉淀规则
5051

‎docs/versions/v0.17.1.md‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# v0.17.1:bug 修复小版本——知识补充轨迹报错 + 笔记级精修卡住
2+
3+
> 状态:已交付(2026-09-01)
4+
> 依据:用户报障——①笔记页知识扩展报错 `Invalid column type Null at index: 0, name: trajectory_json`;②笔记页使用 AI 精修长时间卡在处理页面
5+
6+
## 根因与修复
7+
8+
| # | 位置 | 根因 | 影响 | 修复 |
9+
|---|------|------|------|------|
10+
| 1 | `db_ai_tasks.rs` `get_ai_task_trajectory` | 行存在但 `trajectory_json` 列值为 NULL(旧任务/失败任务/轨迹未写入)时,`row.get::<String>` 因 rusqlite 严格类型检查抛 `Invalid column type Null`(v0.16.0 预存缺陷;测试仅覆盖"无行→None",漏"行存在列 NULL"分支) | AI 对话页「AI 任务」查看轨迹(含知识补充任务)即报错,任务视图不可用 | 改 `Option<String>` 读取 + 双层 Option 拍平(列 NULL 与无行同语义→诚实 None);补回归测试(行存在列为 NULL → Ok(None)) |
11+
| 2 | `components/NoteAiDialog.tsx` | 误用**本地自建 `taskIdRef`**,从未设置 `useAiTaskPolling` 内部的 `taskIdRef.current`(事件通道过滤与派发参数的数据源)→ 轮询/事件均以 `taskId=null` 派发 → `ai_refine_result` 反序列化失败被 `.catch(() => null)` 吞掉 → `running` 永不翻转;且任务推进到 Running 会不断刷新"30s 卡住检测"时间戳 → 卡住提示永不触发 | 笔记级精修(手写笔记)完成后无限"精修中/处理中",工作台不可达 | 改用 hook 返回的 `taskIdRef`(发起/重生成统一赋值——与 AiRefineCard/EnrichPanel 同模式);防御:结果取回失败不再静默(提示到 AI 对话页查看) |
12+
13+
## 验证
14+
15+
- Rust:新增回归测试(NULL 列 → None)通过;全量 1880/1882 通过(仅 2 例 note_filter 预存失败 TD-30-B)
16+
- 前端:vitest 64 文件 496 用例全绿;tsc 零错误
17+
18+
## 关联
19+
20+
- 此前交付:v0.17.0(REQ-245~247,精修可到达度)

0 commit comments

Comments
 (0)