About

关于项目

面溯是一个面向求职者的面试复盘工具。核心命题只有一个: 让一份录音同时喂饱记录、复盘、仪表盘三个模块, 把「感觉面得不好」翻译成可追踪的字段。

技术栈

前端
原生 HTML / CSS / JavaScript,零框架、零构建、零运行时依赖
数据层
localStorage,按 miansu: 命名空间隔离(jobs / reviews / settings)
LLM
DeepSeek(stream: true SSE 流式,解析 reasoning_content 思考链)
ASR
浏览器本地 Whisper(transformers.js,默认 whisper-small,可切 base / tiny),转写全程不出浏览器;另有直接粘贴逐字稿旁路
可视化
漏斗与转化率纯 CSS + DOM 手绘,未引入图表库
设计系统
CSS 变量驱动主题,玻璃拟态(backdrop-filter)+ 深浅双主题

关键架构决策

1为什么不直连 DeepSeek 官方接口?
两个硬性原因:官方不返回 CORS 响应头,浏览器 fetch 会被拦截; Key 会明文暴露在前端代码里。因此采用可插拔代理层: 设置页填 endpoint + Key,前端只认代理,业务层零分支。
代价:依赖一个自建代理,换取了「真实可用」而非「纯演示」。
2为什么 AI 不可用时页面依然能走完?
纯前端项目最大的风险是「没配置就打不开」。业务层只认一套接口签名 (LLM.chat(messages, {onThink, onToken})), 代理不可达时静默降级到本地占位数据,全流程照样可点可填,不弹错误、不中断; 接口恢复后自动回到真实模型——用户永远看不到状态牌,只看到功能可用。
代价:需要维护一份占位数据,换来「任何时候都能演示、任何时刻都不白屏」。
3为什么复盘表是 6 个模块?
单块的「面试总结」会退化成情绪日记。拆成 6 模块后, 每个模块对应不同的数据来源:基础信息(记录带入)、作答(转写带入)、 深度复盘 / 知识漏洞 / 下一步(AI 映射)。 AI 只负责它能推断的部分,意向信号这类必须人工判断的不交给模型。
代价:填写成本变高,所以做了折叠面板 + AI 预填,把成本压到「校对」而非「从零写」。
4为什么多页面而不是单页路由?
这个产品的信息架构本身是流水线(投递 → 转写 → 复盘 → 仪表盘), 不是并列的功能 Tab。多页面让每一步有独立 URL,可直接深链跳转 (如从记录页带 ?job=xxx 进入复盘),也便于逐个页面调试。
代价:导航与主题状态需跨页同步,通过统一注入 + localStorage 解决。
5既然有本地转写,为什么还要做「直接粘贴逐字稿」?
本地 Whisper 的价值在隐私——录音与逐字稿完全不出浏览器,也不消耗任何云端额度。 但代价很实在:需下载 80–250MB 模型、纯 WASM 推理偏慢、中文专有名词识别不准, 小模型在静音段还会生成与音频无关的幻觉文本。 而现实是:手机录音 App 已自带转写,质量普遍高于本地小模型。 因此做成双路径——粘贴逐字稿为默认主路径(零等待、零流量、质量最高), 本地转写作为隐私优先的备选,并可在 small / base / tiny 间切换; 转写结果上另叠一层幻觉清洗(剔除循环重复句与字幕套话)。
代价:两条路径必须归一到同一份「可编辑逐字稿」结构,解析器要兼容多种导出格式;本地模型仍不区分说话人,需人工点击标签校对。

踩过的坑

SSE 分包
流式响应会拆成多个 chunk,一行可能被截断。必须维护 buffer,按 \n 切分后留最后一段到下一轮,否则 JSON.parse 随机报错。
思考链顺序
reasoning_content 与 content 在同一条 delta 里可能只出现一个,回调要分开处理,不能假设成对出现。
玻璃拟态可读性
浅色主题下 rgba(255,255,255,.72) 叠加高饱和渐变会导致文字发灰,最终用 CSS 变量分别控制浅/深两套卡片透明度与描边。
localStorage 隔离
所有 key 统一加命名空间前缀,避免与同域下其他页面串数据;清空功能必须逐个 removeItem 而非 clear。
本地 ASR 幻觉
先用 whisper-base,中文转写出的文本与录音内容完全不符——这是小模型在静音段的典型幻觉,不是代码 bug。换 whisper-small 后明显改善,并补了一层清洗(折叠重复句、剔除字幕套话)。
逐字稿格式碎片化
手机录音 App、讯飞、飞书妙记导出的格式各不相同。解析器只认「已知说话人别名 + 方/圆括号标签」,避免把「注意:…」这类普通冒号句误判成说话人。
!能力边界(如实说明)
开始使用 → 配置代理与 Key
面溯 · 纯静态 · 数据不出浏览器 v2.0.8