UI #3
Conversation
Independent settings webview window restored from settings.html prototype.
Phase 1 is presentational only — all controls are local refs; no backend
persistence wired up yet (DB settings table + REST routes belong to phase 2).
- new "settings" Tauri window (1080x760, decorated, hide-on-close like panel)
- new SettingsView with 6 sections: general / assistant / persona / model /
privacy / hooks (1:1 content with prototype)
- useSettings composable aggregates state + actions
- styles/settings.css scoped to [data-view="settings"] so the prototypes
light-glass + blue-accent visual does not pollute the global tokens
- bg image baked into .settings-root background-image (light/dark via class)
to avoid the negative z-index trick that only works on <body>
- panel header gets a "设置" button to open the window via
WebviewWindow.getByLabel("settings")
- App.vue skips refresh() / WS connect for the settings view
There was a problem hiding this comment.
代码审查总结
此 PR 引入了 StatusBoard / SettingsPanel 的 UI 架构,整体设计方向清晰,组件拆分合理,v-if 逐页挂载的模式也避免了多余的 GPU 合成层。以下列出需要在合并前处理的问题,以及若干维护性建议。
需优先修复
WeeklyFocusCard柱状图数据合并错误 —bars数组将blueBars和greyBars拼接成了 14 项,导致图表显示 14 根柱子而非 7 组对比柱。正确做法是将每天的两个值配对成一个对象数组。CreateFocusCard时长选择无效 — 卡片展示了25 · 45 · 90 min供用户选择,但点击整个卡片只会 emitstartFocus(25),durations数组从未被用于实际选择逻辑,应拆分为独立按钮或移除虚假的选择提示。quickStartFocus移除了activeTaskId的空值守卫 — 旧版在tasks.activeTaskId.value为 null 时跳过调用;此 PR 直接将null传给focus.startFocus,需确认startFocus能安全处理null,否则需恢复守卫。FloatingControls的事件未接入 —toggleTts/toggleImmersive在App.vue中无监听器,按钮对用户表现为可用,但实际不影响useSettings的任何状态。focusStartTime硬编码为"18:42"—BottomBar显示专注开始时间,但始终固定为"18:42",需从useFocus或onStartFocus记录真实时间。
安全问题
AssistantStatusCard使用v-html渲染quote— 当前为硬编码字符串,但注释中明确计划 Phase 2 对接后端 WebSocket。tauri.conf.json中 CSP 为null,一旦quote来自动态数据(AI 回复、persona 文件、WS 消息),v-html将带来 XSS 风险,攻击者可借此调用窗口 Tauri IPC 能力。建议将<div v-html="quote">替换为静态模板(文字 +<em>标签),在 Phase 2 到来前消除这一模式。settings窗口继承了全量默认能力 —capabilities/default.json将"settings"加入与main/panel相同的能力集,包含allow-internal-toggle-devtools和opener:default(允许打开外部 URL)。建议为设置窗口创建单独的最小权限能力集,移除不需要的opener和 DevTools 权限。- 自定义 API 端点无验证 —
ModelSection的自定义端点字段接受任意字符串;Phase 2 用此 URL 发送含 API Key 的请求时,需在 Rust 侧校验 URL scheme 为 https 且非 loopback/私有地址,防止 SSRF 将认证请求转发至本地 Hook API(同在 10103 端口)。
性能建议
- 多层
backdrop-filter同时存在的 GPU 合成开销 — 顶部导航、各卡片、底部栏等超过 10 个元素同时持有backdrop-filter,在 1400×900 的 Tauri WebView 中叠加的合成层会显著增加每帧 GPU 开销,尤其在 hover 动画期间。建议收敛到关键区域(如顶部导航容器),其余区域改用半透明纯色背景。 - SVG
feTurbulence噪点滤镜对每张卡片::after单独计算 —filter: url(#panel-noise-filter)无法复用缓存,每个元素都会触发一次湍流光栅化。视觉效果在opacity: 0.045下可忽略,建议改用一张静态 PNG 噪点纹理 +mix-blend-mode: overlay,在 CSS 层面无运行时开销。 - 全屏
position: fixed伪元素 +mix-blend-mode: multiply—panel-shell::before/after跨越整个视口并使用混合模式,阻止了浏览器的不透明层快速路径优化;任何子元素的动画都会导致整个合成栈重绘。建议在.panel-shell上添加contain: paint或将背景效果移至独立的非fixed层。
维护性建议
TopNav接收petState但模板中从未引用该 prop,头像为完全静态的硬编码内联样式;建议在实现响应式头像前移除该 prop 声明,避免误导。TopNav定义了searchemit 但App.vue未监听,AiChatCard.vue输入框内联了 6 条样式声明,WeeklyFocusCard多处统计数据(12h 40min、+1h 50min)及DiaryCard中的日期(06·12)均为硬编码,建议在静态占位处统一添加// TODO: 接入真实数据注释,与ClawGatewayCard、AssistantStatusCard等已有注释的文件保持一致。apiKeyMasked默认值"sk-••••••••3f7a"会让未配置 Key 的用户误以为已有 Key,建议默认为空字符串并在 UI 侧条件渲染占位提示。- Phase 2 实现 API Key IPC 时,应确保明文 Key 仅在 Rust 侧完成掩码处理后再跨 IPC 传入渲染进程,避免明文 Key 驻留在 JS 堆中。
| const blueBars = [35, 45, 30, 55, 40, 65, 50]; | ||
| const greyBars = [45, 70, 60, 75, 55, 65, 50]; | ||
| const bars = [ | ||
| ...blueBars.map((height) => ({ height, color: "#3b82f6" })), |
There was a problem hiding this comment.
柱状图数据合并错误:当前将 blueBars 和 greyBars 直接拼接为 14 项数组,模板迭代后会渲染 14 根柱子,而 weekDays 只有 7 个标签。正确方式是将两组数据按索引配对:
const bars = blueBars.map((blue, i) => ({ blue, grey: greyBars[i] }));然后在模板中为每天渲染一对蓝/灰柱子。
| <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | ||
| <path d="M12 5v14M5 12h14" /> | ||
| </svg> | ||
| <span style="font-weight: 500">开始新一段专注</span> |
There was a problem hiding this comment.
durations 数组为死代码,时长选择无效:整张卡片是一个按钮,点击后固定 emit startFocus(25),用户无法选择 45 或 90 分钟。durations 仅用于展示字符串。建议拆分为三个独立按钮,或移除展示的时长列表并在注释中说明这是单次快速开始操作。
| @@ -184,10 +209,8 @@ function closeContextMenu() { | |||
|
|
|||
| async function quickStartFocus() { | |||
| closeContextMenu(); | |||
There was a problem hiding this comment.
移除了 activeTaskId 的空值守卫:旧版 quickStartFocus 在 tasks.activeTaskId.value 为 null 时会跳过调用。现在直接将可能为 null 的值传入 focus.startFocus,请确认 startFocus 能安全处理 null 参数,否则需恢复守卫:
if (tasks.activeTaskId.value) {
await focus.startFocus(tasks.activeTaskId.value);
}| @send-chat="onSendChat" | ||
| @send-message="handleSendMessage" | ||
| /> | ||
|
|
There was a problem hiding this comment.
FloatingControls 事件未接入:<FloatingControls /> 没有任何 @toggle-tts / @toggle-immersive 监听器,用户点击 TTS / 沉浸模式按钮时 emit 会丢失,不会影响 useSettings 中的对应状态。
| <AssistantStatusCard v-else-if="activePanelPage === 'assistant'" :pet-state="pet.petState.value" /> | ||
|
|
||
| <HookNotificationsCard v-else-if="activePanelPage === 'hooks'" /> | ||
|
|
There was a problem hiding this comment.
focusStartTime 硬编码:focus-start-time="18:42" 是字面字符串,专注开始时间始终显示为 18:42。建议在 useFocus 中暴露会话开始时间戳,或在 onStartFocusFromCard / quickStartFocus 调用时记录 new Date() 并格式化后传入。
| </div> | ||
|
|
||
| <div class="nc-mood"> | ||
| <div class="orb"></div> |
There was a problem hiding this comment.
v-html + 无 CSP 存在 XSS 升级路径:当前 quote 是硬编码字符串,但 useSettings.ts 中的注释明确计划 Phase 2 接入后端 WS 消息。tauri.conf.json 的 CSP 为 null,如果届时 quote 来自 AI 回复、persona 文件或 WS 推送,v-html 将允许任意 HTML/脚本在渲染进程中执行,进而调用窗口的 Tauri IPC 能力。
建议在 Phase 2 前替换为静态模板:
<div class="quote">「你正在做<em>重要的事</em>,不要分心……」</div>| "identifier": "default", | ||
| "description": "Default desktop capability for NeoCompanion v1", | ||
| "windows": ["main", "panel", "wallpaper"], | ||
| "windows": ["main", "panel", "wallpaper", "settings"], |
There was a problem hiding this comment.
设置窗口继承了过多默认能力:"settings" 窗口与 main/panel 共享同一能力集,包含 allow-internal-toggle-devtools(允许从脚本打开 DevTools)和 opener:default(允许打开外部 URL / shell 操作)。建议为设置窗口单独创建一个最小权限能力文件,仅授予实际所需权限(如配置读写的路径访问),移除不必要的 opener 和 DevTools 权限。
| }>(); | ||
|
|
||
| const emit = defineEmits<{ | ||
| toggleTheme: []; |
There was a problem hiding this comment.
petState prop 未被模板使用:petState: CompanionState 已在 defineProps 中声明,但模板中的头像完全由硬编码内联样式绘制,prop 值从未被读取。建议在实现响应式头像之前移除该 prop 声明,或添加注释说明头像样式将在后续迭代中依据 petState 动态变化。
fix: address PR #3 review feedback — bugs, security, and maintenance - fix WeeklyFocusCard bar chart: pair blue/grey bars per weekday (14→7 groups) - fix CreateFocusCard: make durations (25/45/90) interactively selectable - fix quickStartFocus: restore null guard on activeTaskId - fix onStartFocusFromCard: add null guard + record real focusStartTime - fix FloatingControls: wire toggleTts/toggleImmersive events to useSettings - fix AssistantStatusCard: remove v-html XSS vector, use static template - fix apiKeyMasked: default to empty string instead of misleading placeholder - chore TopNav: mark unused petState/search with Phase 2 TODO comments - chore: add TODO markers on all hardcoded static data - chore ModelSection: add TODO for Phase 2 URL validation (SSRF prevention) - style AiChatCard: extract inline styles to .chat-input-native class @
|
感谢详细的 review!以下是逐条回复:\n\n### ✅ 已修复\n\n1. WeeklyFocusCard 柱状图数据合并错误 — 已将 bars 从 14 项扁平拼接改为按天配对,模板中每天渲染两根并排对比柱。\n2. CreateFocusCard 时长选择无效 — 已拆分为三个可互动的时长 chip (25/45/90),点击选择高亮,开始按钮 emit 选中的时长。\n3. quickStartFocus 空值守卫 — 已恢复 activeTaskId null 检查,null 时直接 return。onStartFocusFromCard 也已添加同款守卫。\n4. FloatingControls 事件接入 — 已在 App.vue 接入 toggleTts / toggleImmersive,连接 useSettings 状态。\n5. focusStartTime 硬编码 — 已改为在专注开始时记录真实时间,通过 ref 传入 BottomBar。\n6. v-html XSS 风险 — 已移除 v-html,改为静态模板。Phase 2 也不会再用 v-html 渲染动态内容。\n\n### ✅ 维护性修复\n\n- apiKeyMasked 默认值已改为空字符串\n- TopNav 未使用的 petState prop 和 search emit 已加 TODO: Phase 2 注释\n- 全部硬编码数据处已添加 TODO: 接入真实数据 注释\n- AiChatCard 内联样式已提取为 .chat-input-native CSS class\n- ModelSection 自定义端点字段旁已添加 Phase 2 SSRF 防范 TODO 注释\n\n### 📝 后续迭代处理\n\n7. Settings 窗口权限 — 当前 Phase 1 settings 为纯静态页面,Phase 2 接入真实配置时会创建独立最小权限 capability。\n8. API 端点验证 — Phase 2 会在 Rust 侧校验 URL scheme 为 https 并禁止 loopback/私有地址。\n9-11. 性能优化 — 噪音纹理改 PNG、收敛 backdrop-filter、contain:paint 等已记录 backlog。\nPhase 2 API Key 安全 — 同意,明文 Key 仅在 Rust 侧掩码后跨 IPC 传入渲染进程。 |
…up read from package.json
tauri-plugin-wallpaper depends on the windows crate which pulls in windows-future. This crate fails to compile on macOS and Linux because windows-future v0.2.1 references IMarshal/marshaler APIs that are unavailable outside Windows targets. Move the dependency to target cfg windows dependencies since it is already gated with cfg target_os windows in lib.rs
The wallpaper:default permission in capabilities/default.json caused tauri-build to fail on macOS/Linux because tauri-plugin-wallpaper is now gated to Windows-only in Cargo.toml. Split into a separate capability file with platforms: [windows] so non-Windows CI passes.
macos-latest runners are Apple Silicon and only have aarch64 target installed. Building universal-apple-darwin requires both architectures. Add rustup target add step for macOS in ci-build and release workflows.
add settting board,status board and test window feat