Skip to content

Commit 58a1cdb

Browse files
committed
feat(app): 备好时间码深链的 ms 载体与复位接口
1 parent 39f74e4 commit 58a1cdb

5 files changed

Lines changed: 58 additions & 5 deletions

File tree

‎app/src/App.tsx‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
* `ShellFallback` 首访加载态(此前懒 chunk 失败会一路抛到最外层 AppErrorBoundary ⇒
2020
* 整个导航壳被卸载、已访问页状态一起丢)。边界在 `shell/ShellFallback.tsx`。
2121
*/
22-
import { lazy, Suspense, useEffect, useState } from "react";
22+
import { lazy, Suspense, useEffect, useRef, useState } from "react";
2323
import { listen } from "@tauri-apps/api/event";
2424
import { invoke } from "@tauri-apps/api/core";
2525
import { confirm } from "@tauri-apps/plugin-dialog";
@@ -46,6 +46,8 @@ import { ShellFallback, SlotErrorBoundary } from "./shell/ShellFallback";
4646
// 两件**签名与语义逐字不变**(只搬不改,C9.19 第 1 条):文档段随代码搬走,本文件只剩装配与调用点。
4747
import { PageSlot } from "./shell/PageSlot";
4848
import { AiToast } from "./shell/aiToast";
49+
// 批 7 T1:`[[ts:ms]]` 深链的目标规格(**纯数据 + 纯函数**)—— 状态与 setter 仍住本文件(源码文本判据)。
50+
import { seekKeyOf, type FocusSeek } from "./shell/focusRouting";
4951
// REQ-274(v0.19.4):全局 AI 对话面板(丙案——按需唤起 + 内容保活)
5052
// 批 2 包体治理:从静态 import 改为按需 import,并在它之前加一道「首开挂载」闸门。
5153
// @ai-context: 原语义(见文件末尾 dock 渲染处的注释)是「常驻挂载——开合仅切 display,
@@ -159,6 +161,10 @@ function MainShell() {
159161
const [focusNoteId, setFocusNoteId] = useState<number | null>(null);
160162
// v0.19.1(REQ-260):引用跳笔记 + 命中词高亮(key 递增——同笔记重复引用可重触发)
161163
const [focusNoteSearch, setFocusNoteSearch] = useState<{ noteId: number; search: string; key: number } | null>(null);
164+
// 批 7 T1(R4/R9.1 的最小形态 · C9.2):`[[ts:ms]]` 深链的 **ms 载体**(`goSessions` 的第二个实参写它;`undefined` ⇒ 不 seek)。
165+
// @ai-context 为什么是 `{ms,key}` 而不是裸 number:裸值 setState 同值不触发 ⇒ 会话页已打开时再点同一时间码不会重新定位(其余 5 个粘滞字段的形态改造越权,登记批 8)。
166+
const [focusSeekMs, setFocusSeekMs] = useState<FocusSeek | null>(null);
167+
const focusSeekKeyRef = useRef(0);
162168

163169
// v0.19.1 审查 H1 修复:普通打开统一清带词态——focusNoteSearch 只写不清时,
164170
// NotesPage 合并 effect 会优先取残留的旧引用笔记,后续普通跨页跳转被重定向
@@ -175,7 +181,10 @@ function MainShell() {
175181
// 状态机、字段类型、各页消费逻辑一个字节未动(删字段 / 改路由参数是批 5 的视图层重构)。
176182
// 入口映射:⌘K 侧能独立发起的是页面命令(9 个)· 建体系向导 · 检索结果(带 ID 的笔记深链);
177183
// 其余带 ID 的深链(会话/体系/复习组/工作台/对话)仍由**页内入口**发起 —— ID 只有页内有。
178-
const goSessions = (sessionId: number) => {
184+
const goSessions = (sessionId: number, ms?: number) => {
185+
const key = seekKeyOf(Date.now(), focusSeekKeyRef.current);
186+
focusSeekKeyRef.current = key;
187+
setFocusSeekMs(ms === undefined ? null : { ms, key });
179188
setFocusSessionId(sessionId);
180189
setPage("sessions");
181190
};
@@ -399,6 +408,9 @@ function MainShell() {
399408
onRefineTaskStarted={(_sessionId, taskId) => goChatTask(taskId)}
400409
active={page === "sessions"}
401410
onOpenNote={(id) => openNotePlain(id)}
411+
// 批 7 T1(C9.2:接口在 7a 备好、行为在 7b 落地):ms 载体下传 + 消费后复位
412+
focusSeekMs={focusSeekMs}
413+
onFocusSeekConsumed={() => setFocusSeekMs(null)}
402414
/>
403415
</PageSlot>
404416
<PageSlot show={page === "notes"} mounted={mountedPages.has("notes")}>

‎app/src/pages/SessionsPage.tsx‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import type { BatchNoteResult, BatchSessionDeleteResult, CourseGroup, SessionDet
2828
import { columnSpec } from "../shell/columnRegistry";
2929
import { viewsFor } from "../views/registry"; // 批 5 T10(C1①):视图清单在本层注入(面板/宿主只收注入)
3030
import { Text } from "../ui/primitives";
31+
import type { FocusSeek } from "../shell/focusRouting";
3132

3233
interface Props {
3334
focusSessionId?: number | null;
@@ -43,6 +44,12 @@ interface Props {
4344
active: boolean;
4445
/** 查看笔记 → 笔记页直达(App 层切页 + focusNoteId) */
4546
onOpenNote: (noteId: number) => void;
47+
/** 批 7 T1(C9.2):`[[ts:ms]]` 深链的 ms 载体(接口在 7a 备好;**消费与透传归 7b 的 T17**)。
48+
* ⚠️ 本任务**不解构**它 —— 解构而不读会触发 `noUnusedLocals`(TS6133),而透传给
49+
* `<SessionDetailPanel>` 需要改该面板的 props 面(属 T17 的写者域)⇒ 只备类型。 */
50+
focusSeekMs?: FocusSeek | null;
51+
/** 批 7 T1:`focusSeekMs` 消费完成回调(App 清空——防陈旧 ms 跨导航复触发);消费逻辑归 T17。 */
52+
onFocusSeekConsumed?: () => void;
4653
}
4754

4855
export default function SessionsPage({ focusSessionId, onFocusSessionConsumed, focusRefineTaskId, onFocusRefineTaskConsumed, onRefineTaskStarted, active, onOpenNote }: Props) {

‎app/src/shell/CommandPalette.kb.test.tsx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,9 @@ describe("⑤ App.tsx 侧的接线与入口收敛(**静态**判据;运行期
172172

173173
it("10 个 focus* 字段一个不少(只收敛入口、不删状态机),跳转入口已具名化", () => {
174174
const fields = ["focusSessionId", "focusNoteId", "focusNoteSearch", "focusSystemId", "createSystemSignal",
175-
"focusGroupId", "focusReviewGroupId", "focusRefineTaskId", "focusChatTaskId", "focusChatId"];
175+
"focusGroupId", "focusReviewGroupId", "focusRefineTaskId", "focusChatTaskId", "focusChatId",
176+
// 批 7 T1(C9.9 授权的「只增」):`[[ts:ms]]` 深链的 ms 载体 —— 原 10 个字段一个不少。
177+
"focusSeekMs"];
176178
const missing = fields.filter((f) => !new RegExp(`const \\[${f}, set`).test(APP_CODE));
177179
expect(missing, `这些 focus* 字段被删/改名了(本批不动状态机):${missing.join(" / ")}`).toEqual([]);
178180
const entries = ["goSessions", "openNoteHighlight", "goSystem", "goGroup", "goReviewGroup",

‎app/src/shell/focusRouting.ts‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* @ai-context 批 7 T1(C9.2 / R4 / R9.1):`focus*` 家族的**目标规格**(纯数据)与跳转请求的判别键。
3+
* Why 只抽「类型 + 纯函数」:`CommandPalette.kb.test.tsx` 与 `useNotesDeepLink.test.ts` 都把
4+
* `App.tsx` 当源码文本读(前者判 `const [<字段>, set` 的声明形态),故状态与 setter 必须留在那里。
5+
* 副作用:无(纯函数,不读 store / 不发请求 / 不写磁盘)。
6+
* 边界:本文件**不得** import `views/**` / `pages/**`(`shell/**` 属首屏面,见 architecture.guard)。
7+
*/
8+
9+
/** `[[ts:ms]]` 深链的 **ms 载体**:`key` 是单调判别键(同值重复跳转必须能重新触发)。 */
10+
export interface FocusSeek {
11+
readonly ms: number;
12+
readonly key: number;
13+
}
14+
15+
/** 会话深链目标:`ms` 缺省 = 只定位会话、不 seek(`null` 与 `0` 语义不同)。 */
16+
export interface SessionJump {
17+
readonly sessionId: number;
18+
readonly ms: number | null;
19+
}
20+
21+
/** 由会话 id(+ 可选 ms)构造深链目标;`undefined` ⇒ `ms: null`(不 seek)。 */
22+
export function sessionJumpOf(sessionId: number, ms?: number): SessionJump {
23+
return { sessionId, ms: ms === undefined ? null : ms };
24+
}
25+
26+
/**
27+
* 跳转请求的**单调判别键**:裸 `number` 不够 —— `setState` 同值不触发重渲染,
28+
* 会话页已打开时再点同一个时间码就不会重新定位。`prev` 由调用方的 ref 持有。
29+
*/
30+
export function seekKeyOf(now: number, prev: number): number {
31+
return now > prev ? now : prev + 1;
32+
}

‎docs/standards/line-limit-exemptions.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020

2121
| 文件 | 行数 | 豁免理由 | 拆分计划 |
2222
|---|---|---|---|
23-
| app/src/App.tsx | 537 | 根装配(页面切换/焦点跨页直达状态机/全局事件监听)+ v0.20.5 行动页 + v0.20.10 批 5 复习页 Tab/深链/保活挂载(f702d876)——登记值 363 过期,实测纠偏 | 若再增长:焦点跨页直达 state 族拆至 useFocusRouting.ts |
23+
| app/src/App.tsx | 549 | 根装配(页面切换/焦点跨页直达状态机/全局事件监听)+ v0.20.5 行动页 + v0.20.10 批 5 复习页 Tab/深链/保活挂载(f702d876)——登记值 363 过期,实测纠偏 | 若再增长:焦点跨页直达 state 族拆至 useFocusRouting.ts |
2424
| app/src/pages/ChatPage.tsx | 579 | AI 对话页编排;批 1(REQ-306/307)active 门控/终态订阅接线净增——2026-09-09 实测纠偏(登记值 529 过期) | 若再增长:任务工具条与发起流拆至 ChatTasksToolbar.tsx |
2525
| app/src-tauri/src/db_migrations.rs | 573 | v0.20.11 批 6(REQ-315)再增:note_groups.pin ensure_column + note_group_orders 建表(+21,实测 573——登记值 492 过期;schema 单点收敛理由同左) | 若再增长:kb_* 与 chat_* 表 DDL 拆至 db_migrations_kb.rs |
2626
| app/src-tauri/src/lib.rs | 572 | crate 根 **322 `mod` + 15 `#[cfg]` = 337 行地板**(Task 1 评审实测更正:原写「321 `mod` + 16 `#[cfg]`」,总数 337 不变;322 含本任务新增的 `mod app_commands;`);注册清单已移至 `app_commands.rs`(**数据文件**,同属 300–600 豁免带)—— 结构性下界,非欠账 | 已完成(批 0-C3 Task 1,2026-09-11);余下 161 行模块理由注释 + 装配逻辑,无进一步拆分标的 |
@@ -106,7 +106,7 @@
106106
| app/src-tauri/src/commands_asr_pass2.rs | 333 | 实时链路只有端点句 SenseVoice 重打分;本命令把"导入同级的全窗离线质量"带给已结束会话:读取 S4 落盘音频(data_dir/session-audio/{id}.wav,16k PCM16)后台分窗重跑SenseVoice → 逐窗与现网轴比对 → 产 session_refine_drafts(pending),用户经 second_pass_list/decide 预览采纳/回退。(自动摘取,待细化) | 若再增长:按职责拆分 |
107107
| app/src-tauri/src/commands_after.rs | 332 | v0.20.3(REQ-294/295/299/300)收尾命令域(批决议/导出/练习/问题)内聚;2026-09-06 实测登记(TD-2026-09-06-G) | 若再增长:批决议核心拆至 weekly_resolve.rs |
108108
| app/src-tauri/src/fusion.rs | 332 | 纯规则融合(无 LLM,本地优先降级路径)。规则按优先级:1) 字幕权威——字幕段覆盖时间窗内以字幕为准(准确率近 100%)2) ASR 补缝——字幕段之间 gap > gap_ms 的空隙用 ASR 填补3) 重叠校对——REQ-062 升级为概率加权:编辑距离高相似(sim ≥阈值,≈旧 ≤2 规则)一律字幕胜;相似度不足时比较P(字幕胜出)=conf_sub×(0.6+0.4×sim) 与 P(ASR 保留)=conf_asr×(0.4+0.6×sim);双源低置信 → 输出低置信核对段(B3 落库标记);置信度缺失(None=旧数据)→ 回退旧硬规则(距离>2 保留核对段)4) 时间轴对齐——重叠部分归属字幕,ASR 段被裁剪到空隙5) 空窗丢弃——两端无内容的静默窗不产出段@ai-context: 输入字幕段需已含 end_ms(编排按下一字幕出现时刻补齐)。(自动摘取,待细化) | 若再增长:按职责拆分 |
109-
| app/src/pages/SessionsPage.tsx | 332 | 本层为状态宿主与数据编排:会话列表/详情状态、事件驱动刷新(live:status/session:fused/切页 active)、转化与删除操作;左栏列表 UI 拆至 SessionListPanel、右栏详情拆至 SessionDetailPanel(豁免清单拆分计划落地,本文件 ≤300 行)。(自动摘取,待细化) | 若再增长:按职责拆分 |
109+
| app/src/pages/SessionsPage.tsx | 339 | 本层为状态宿主与数据编排:会话列表/详情状态、事件驱动刷新(live:status/session:fused/切页 active)、转化与删除操作;左栏列表 UI 拆至 SessionListPanel、右栏详情拆至 SessionDetailPanel(豁免清单拆分计划落地,本文件 ≤300 行)。(自动摘取,待细化) | 若再增长:按职责拆分 |
110110
| app/src-tauri/src/capture/dxgi_capture.rs | 331 | 主路径用 DXGI 桌面复制(GPU 直取,性能最优);new 或运行时捕获失败(远程桌面/锁屏/设备丢失)自动降级 GDI BitBlt(gdi_capture.rs),调用方无感知。帧输出 BGRA8,支持按窗口矩形裁剪与底部字幕区裁剪。(自动摘取,待细化) | 若再增长:按职责拆分 |
111111
| app/src-tauri/src/commands_knowledge_decisions.rs | 331 | 本层只做参数校验、调用数据层、错误映射(AGENTS.md §6);编排 `fn xxx_inner(db, …)`为纯函数(:memory: 可测),薄 `#[tauri::command]` 壳只取 state.db 调 inner。一表两面:kind 区分 decision(思辨面)/application(学习面·记一次使用),不双表双记、只记"我的决策"。(自动摘取,待细化) | 若再增长:按职责拆分 |
112112
| app/src-tauri/src/structure_capture_tests.rs | 331 | 纯函数(网格换算/裁剪钳制/过滤上下文组装)+ 端到端集成(合成表格帧+字幕帧参考图集 → 直扫分析 → 只收表格拒字幕 + 幂等重跑+ 降级跳过);tempfile + 内存库隔离(不触碰真实数据)。(自动摘取,待细化) | 若再增长:按职责拆分 |

0 commit comments

Comments
 (0)