From 16d8bb5053beb004cfdc5a9e064fbb0e241d00ab Mon Sep 17 00:00:00 2001 From: coso Date: Thu, 14 May 2026 01:25:52 +0800 Subject: [PATCH] chore: release v1.38.0 --- RELEASE_NOTES.md | 58 +- docs/roadmap/i18n/prd.md | 37 +- .../voice/sensevoice-small-integration.md | 4 +- package-lock.json | 4 +- package.json | 2 +- packages/lime-cli-npm/package.json | 2 +- playwright-active-history-snapshot.json | 1 + ...wright-after-reload-new-chat-boundary.json | 6 + playwright-current-session-debug.json | 183 ++++ playwright-history-buttons.json | 62 ++ playwright-history-first-image-check.json | 32 + playwright-image-flow-state-after-wait.json | 31 + playwright-image-flow-state.json | 30 + playwright-new-chat-no-leak.json | 7 + ...right-second-image-after-reload-final.json | 29 + ...right-second-image-after-reload-start.json | 28 + playwright-second-image-start.json | 8 + src-tauri/Cargo.lock | 36 +- src-tauri/Cargo.toml | 4 +- src-tauri/capabilities/default.json | 2 +- src-tauri/crates/agent/src/skill_execution.rs | 105 +- .../crates/aster/src/agents/agent.rs | 256 ++++- .../crates/aster/src/session/runtime_store.rs | 2 + src-tauri/crates/core/src/config/mod.rs | 18 +- src-tauri/crates/core/src/config/types.rs | 53 +- src-tauri/crates/services/src/lib.rs | 4 - .../src/screenshot_capture_service.rs | 244 ----- .../services/src/screenshot_image_service.rs | 31 - src-tauri/src/app/runner.rs | 33 +- .../aster_agent_cmd/image_skill_launch.rs | 151 ++- src-tauri/src/commands/aster_agent_cmd/mod.rs | 5 +- .../request_model_resolution.rs | 20 +- .../request_model_resolution/tests.rs | 24 + .../commands/aster_agent_cmd/runtime_turn.rs | 237 ++++- .../src/commands/aster_agent_cmd/tests.rs | 134 ++- .../commands/aster_agent_cmd/tool_runtime.rs | 4 +- .../tool_runtime/creation_tools.rs | 28 +- .../tool_runtime/media_cli_bridge.rs | 3 + src-tauri/src/commands/experimental_cmd.rs | 23 + src-tauri/src/commands/hotkey_cmd.rs | 46 + src-tauri/src/commands/mod.rs | 3 +- src-tauri/src/commands/screenshot_cmd.rs | 360 ------- src-tauri/src/dev_bridge/dispatcher.rs | 2 +- src-tauri/src/dev_bridge/dispatcher/voice.rs | 19 - src-tauri/src/lib.rs | 1 - src-tauri/src/screenshot/README.md | 69 -- src-tauri/src/screenshot/capture.rs | 8 - src-tauri/src/screenshot/config.rs | 134 --- src-tauri/src/screenshot/mod.rs | 69 -- src-tauri/src/screenshot/shortcut.rs | 361 ------- src-tauri/src/screenshot/window.rs | 487 ---------- src-tauri/src/services/README.md | 4 - src-tauri/src/services/companion_service.rs | 6 +- src-tauri/src/skills/execution.rs | 11 +- src-tauri/src/voice/commands.rs | 69 -- src-tauri/src/voice/events.rs | 47 + src-tauri/src/voice/fn_shortcut.rs | 7 +- src-tauri/src/voice/mod.rs | 27 +- src-tauri/src/voice/shortcut.rs | 198 +--- src-tauri/src/voice/window.rs | 170 ---- src-tauri/tauri.conf.headless.json | 2 +- src-tauri/tauri.conf.json | 18 +- src/App.tsx | 25 - src/RootRouter.test.tsx | 12 - src/RootRouter.tsx | 11 - src/components/README.md | 1 - .../agent/chat/AgentChatWorkspace.tsx | 64 +- .../ChatModelSelector.integration.test.tsx | 7 +- .../agent/chat/components/EmptyState.test.tsx | 118 ++- .../agent/chat/components/EmptyState.tsx | 219 ++--- .../EmptyStateComposerPanel.test.tsx | 102 +- .../components/EmptyStateComposerPanel.tsx | 59 +- .../ImageWorkbenchMessagePreview.test.tsx | 55 ++ .../ImageWorkbenchMessagePreview.tsx | 107 ++- .../components/InputbarComposerSection.tsx | 46 +- .../Inputbar/components/InputbarCore.test.tsx | 180 +++- .../Inputbar/components/InputbarCore.tsx | 76 +- .../components/InputbarTools.test.tsx | 105 ++ .../Inputbar/components/InputbarTools.tsx | 34 +- .../components/QueuedTurnsPanel.test.tsx | 34 +- .../Inputbar/components/QueuedTurnsPanel.tsx | 30 +- .../components/inputbarComposerSectionCopy.ts | 73 ++ .../Inputbar/components/inputbarCoreCopy.ts | 149 +++ .../components/inputbarQueuedTurnsCopy.ts | 60 ++ .../Inputbar/components/inputbarToolsCopy.ts | 123 +++ .../Inputbar/hooks/inputbarControllerCopy.ts | 25 + .../Inputbar/hooks/inputbarDictationCopy.ts | 44 + .../hooks/inputbarImageAttachmentsCopy.ts | 36 + .../hooks/useImageAttachments.test.tsx | 22 +- .../Inputbar/hooks/useImageAttachments.ts | 28 +- .../Inputbar/hooks/useInputbarController.ts | 15 +- .../Inputbar/hooks/useInputbarDictation.ts | 129 ++- .../Inputbar/hooks/useInputbarToolState.ts | 28 +- .../chat/components/Inputbar/index.test.tsx | 221 ++++- .../agent/chat/components/Inputbar/index.tsx | 17 + .../chat/components/MessageList.test.tsx | 593 ++++++++++-- .../agent/chat/components/MessageList.tsx | 359 ++++--- .../agent/chat/home/HomeGuideCards.tsx | 6 +- .../chat/home/HomeMoreSkillsDrawer.test.tsx | 6 + .../agent/chat/home/HomeMoreSkillsDrawer.tsx | 8 +- .../home/HomeSceneSkillManagerDialog.test.tsx | 33 +- .../chat/home/HomeSceneSkillManagerDialog.tsx | 268 +++++- .../agent/chat/home/HomeSkillGallery.test.tsx | 17 +- .../agent/chat/home/HomeSkillGallery.tsx | 9 +- .../agent/chat/home/HomeStartSurface.test.tsx | 15 + .../agent/chat/home/HomeStartSurface.tsx | 6 + .../agent/chat/home/HomeStarterChips.test.tsx | 17 +- .../agent/chat/home/HomeStarterChips.tsx | 19 +- .../chat/home/buildHomeSkillSurface.test.ts | 90 +- .../agent/chat/home/buildHomeSkillSurface.ts | 51 +- .../chat/home/homeSkillSurfaceAdapters.ts | 21 +- .../agent/chat/home/homeSurfaceCopy.ts | 772 ++++++++++----- .../agent/chat/hooks/agentChatHistory.test.ts | 375 +++++++- .../agent/chat/hooks/agentChatHistory.ts | 181 +++- .../agent/chat/hooks/agentChatStorage.test.ts | 13 +- .../agent/chat/hooks/agentChatStorage.ts | 4 +- .../hooks/agentSessionScopedStorage.test.ts | 247 +++++ .../chat/hooks/agentSessionScopedStorage.ts | 77 +- .../chat/hooks/agentSessionState.test.ts | 294 ++++++ .../agent/chat/hooks/agentSessionState.ts | 58 +- .../agentStreamPreparedSendDispatch.test.ts | 6 + .../hooks/agentStreamRuntimeHandler.test.ts | 116 ++- .../chat/hooks/agentStreamRuntimeHandler.ts | 140 ++- .../agentStreamSlashSkillPreflight.test.ts | 59 ++ .../hooks/agentStreamSlashSkillPreflight.ts | 131 ++- .../agentStreamSubmissionLifecycle.test.ts | 96 ++ .../hooks/agentStreamSubmissionLifecycle.ts | 32 +- .../chat/hooks/agentStreamSubmitDraft.ts | 33 +- .../chat/hooks/agentStreamTurnEventBinding.ts | 8 + ...gentStreamUserInputSendPreparation.test.ts | 39 + .../agentStreamUserInputSendPreparation.ts | 4 +- .../agentStreamUserInputSubmission.test.ts | 6 + .../hooks/agentStreamUserInputSubmission.ts | 2 + .../agent/chat/hooks/skillCommand.test.ts | 217 +++++ .../agent/chat/hooks/skillCommand.ts | 153 ++- .../agent/chat/hooks/useAgentSession.ts | 41 +- .../agent/chat/hooks/useAgentStream.ts | 2 + .../hooks/useAgentStreamController.test.tsx | 35 + .../chat/hooks/useAgentStreamController.ts | 9 + .../chat/hooks/useAsterAgentChat.test.tsx | 210 ++++ .../agent/chat/hooks/useAsterAgentChat.ts | 29 +- src/components/agent/chat/index.test.tsx | 152 +++ .../messageTimelineRenderProjection.test.ts | 40 + .../messageTimelineRenderProjection.ts | 123 ++- .../chat/protocol-fact-source-guard.test.ts | 7 + .../agent/chat/service-skills/homeCopy.ts | 40 + .../service-skills/useServiceSkills.test.tsx | 4 +- .../chat/service-skills/useServiceSkills.ts | 120 ++- src/components/agent/chat/types.ts | 2 + .../chat/utils/imageWorkbenchPresentation.ts | 38 +- .../chat/utils/skillInlineProcessRetention.ts | 54 ++ ...eWorkspaceImageTaskPreviewRuntime.test.tsx | 56 ++ .../useWorkspaceImageTaskPreviewRuntime.ts | 102 +- .../image-gen/useImageGen.live.test.ts | 68 ++ src/components/image-gen/useImageGen.test.ts | 247 +++++ src/components/image-gen/useImageGen.ts | 476 +++++++++- .../layout/CrashRecoveryPanel.test.tsx | 8 +- .../onboarding/OnboardingWizard.tsx | 242 ----- src/components/onboarding/constants.test.ts | 11 - src/components/onboarding/constants.ts | 23 - .../onboarding/hooks/useOnboarding.ts | 84 -- src/components/onboarding/index.ts | 7 - .../onboarding/steps/CompleteStep.tsx | 75 -- .../onboarding/steps/MicrophoneTestStep.tsx | 276 ------ .../onboarding/steps/VoiceDemoStep.tsx | 307 ------ .../steps/VoiceShortcutTestStep.test.tsx | 136 --- .../steps/VoiceShortcutTestStep.tsx | 253 ----- .../onboarding/steps/WelcomeStep.tsx | 179 ---- .../steps/onboardingStepsCopy.test.tsx | 81 -- .../settings-v2/agent/voice/index.test.tsx | 13 +- .../settings-v2/agent/voice/index.tsx | 88 +- .../general/appearance/index.test.tsx | 15 +- .../settings-v2/general/appearance/index.tsx | 94 -- .../general/hotkeys/hotkeyCatalog.test.ts | 56 +- .../general/hotkeys/hotkeyCatalog.ts | 318 +------ .../general/hotkeys/index.test.tsx | 82 +- .../settings-v2/general/hotkeys/index.tsx | 28 +- .../shared}/ShortcutSettings.test.tsx | 0 .../shared}/ShortcutSettings.tsx | 6 +- .../system/experimental/index.test.tsx | 22 +- .../settings-v2/system/experimental/index.tsx | 159 ---- .../voice/InstructionEditor.test.tsx | 111 +++ src/components/voice/InstructionEditor.tsx | 9 +- src/hooks/useGlobalTrayModelSync.ts | 4 + src/i18n/__tests__/loadNamespace.test.ts | 143 ++- src/i18n/__tests__/types.test.ts | 256 ++++- src/i18n/resources/en-US/agent.json | 265 ++++++ src/i18n/resources/en-US/common.json | 17 - src/i18n/resources/en-US/settings.json | 52 - src/i18n/resources/ja-JP/agent.json | 271 +++++- src/i18n/resources/ja-JP/common.json | 17 - src/i18n/resources/ja-JP/settings.json | 52 - src/i18n/resources/ko-KR/agent.json | 271 +++++- src/i18n/resources/ko-KR/common.json | 17 - src/i18n/resources/ko-KR/settings.json | 52 - src/i18n/resources/zh-CN/agent.json | 271 +++++- src/i18n/resources/zh-CN/common.json | 17 - src/i18n/resources/zh-CN/settings.json | 52 - src/i18n/resources/zh-TW/agent.json | 271 +++++- src/i18n/resources/zh-TW/common.json | 17 - src/i18n/resources/zh-TW/settings.json | 52 - src/lib/api/asrProvider.test.ts | 26 +- src/lib/api/asrProvider.ts | 25 - src/lib/api/experimentalFeatureTypes.ts | 10 - src/lib/api/experimentalFeatures.test.ts | 18 +- src/lib/api/experimentalFeatures.ts | 11 - src/lib/api/hotkeys.test.ts | 76 +- src/lib/api/hotkeys.ts | 25 +- src/lib/api/screenshotChat.test.ts | 39 - src/lib/api/screenshotChat.ts | 19 - src/lib/governance/legacySurfaceCatalog.json | 93 +- .../governance/legacySurfaceCatalog.test.ts | 60 +- src/lib/tauri-mock/core.ts | 19 +- src/lib/voiceModelSettingsNavigation.test.ts | 12 +- src/lib/voiceModelSettingsNavigation.ts | 2 +- src/pages/README.md | 2 - src/pages/smart-input.css | 533 ----------- src/pages/smart-input.test.tsx | 351 ------- src/pages/smart-input.tsx | 899 ------------------ 219 files changed, 11257 insertions(+), 8493 deletions(-) create mode 100644 playwright-active-history-snapshot.json create mode 100644 playwright-after-reload-new-chat-boundary.json create mode 100644 playwright-current-session-debug.json create mode 100644 playwright-history-buttons.json create mode 100644 playwright-history-first-image-check.json create mode 100644 playwright-image-flow-state-after-wait.json create mode 100644 playwright-image-flow-state.json create mode 100644 playwright-new-chat-no-leak.json create mode 100644 playwright-second-image-after-reload-final.json create mode 100644 playwright-second-image-after-reload-start.json create mode 100644 playwright-second-image-start.json delete mode 100644 src-tauri/crates/services/src/screenshot_capture_service.rs delete mode 100644 src-tauri/crates/services/src/screenshot_image_service.rs create mode 100644 src-tauri/src/commands/experimental_cmd.rs create mode 100644 src-tauri/src/commands/hotkey_cmd.rs delete mode 100644 src-tauri/src/commands/screenshot_cmd.rs delete mode 100644 src-tauri/src/screenshot/README.md delete mode 100644 src-tauri/src/screenshot/capture.rs delete mode 100644 src-tauri/src/screenshot/config.rs delete mode 100644 src-tauri/src/screenshot/mod.rs delete mode 100644 src-tauri/src/screenshot/shortcut.rs delete mode 100644 src-tauri/src/screenshot/window.rs create mode 100644 src-tauri/src/voice/events.rs delete mode 100644 src-tauri/src/voice/window.rs create mode 100644 src/components/agent/chat/components/Inputbar/components/InputbarTools.test.tsx create mode 100644 src/components/agent/chat/components/Inputbar/components/inputbarComposerSectionCopy.ts create mode 100644 src/components/agent/chat/components/Inputbar/components/inputbarCoreCopy.ts create mode 100644 src/components/agent/chat/components/Inputbar/components/inputbarQueuedTurnsCopy.ts create mode 100644 src/components/agent/chat/components/Inputbar/components/inputbarToolsCopy.ts create mode 100644 src/components/agent/chat/components/Inputbar/hooks/inputbarControllerCopy.ts create mode 100644 src/components/agent/chat/components/Inputbar/hooks/inputbarDictationCopy.ts create mode 100644 src/components/agent/chat/components/Inputbar/hooks/inputbarImageAttachmentsCopy.ts create mode 100644 src/components/agent/chat/service-skills/homeCopy.ts create mode 100644 src/components/agent/chat/utils/skillInlineProcessRetention.ts create mode 100644 src/components/image-gen/useImageGen.live.test.ts delete mode 100644 src/components/onboarding/OnboardingWizard.tsx delete mode 100644 src/components/onboarding/constants.test.ts delete mode 100644 src/components/onboarding/constants.ts delete mode 100644 src/components/onboarding/hooks/useOnboarding.ts delete mode 100644 src/components/onboarding/index.ts delete mode 100644 src/components/onboarding/steps/CompleteStep.tsx delete mode 100644 src/components/onboarding/steps/MicrophoneTestStep.tsx delete mode 100644 src/components/onboarding/steps/VoiceDemoStep.tsx delete mode 100644 src/components/onboarding/steps/VoiceShortcutTestStep.test.tsx delete mode 100644 src/components/onboarding/steps/VoiceShortcutTestStep.tsx delete mode 100644 src/components/onboarding/steps/WelcomeStep.tsx delete mode 100644 src/components/onboarding/steps/onboardingStepsCopy.test.tsx rename src/components/{smart-input => settings-v2/shared}/ShortcutSettings.test.tsx (100%) rename src/components/{smart-input => settings-v2/shared}/ShortcutSettings.tsx (97%) create mode 100644 src/components/voice/InstructionEditor.test.tsx delete mode 100644 src/lib/api/screenshotChat.test.ts delete mode 100644 src/lib/api/screenshotChat.ts delete mode 100644 src/pages/smart-input.css delete mode 100644 src/pages/smart-input.test.tsx delete mode 100644 src/pages/smart-input.tsx diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index f3a1440cd..d3c0a5f88 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,49 +1,49 @@ -## Lime v1.37.0 +## Lime v1.38.0 -发布日期:`2026-05-12` -递交范围:当前完整 worktree,包含 tracked、deleted 与新增文件;本次按发布要求完成版本号、release note、校验,并在获得明确确认后执行提交。 +发布日期:`2026-05-14` +递交范围:当前完整 worktree,包含 tracked、deleted 与新增文件;本次按发布要求完成版本号、release note 与发布前校验,提交和推送将在获得明确确认后执行。 -> 发布说明:上一版 release tag 为 `v1.36.0`。本版升级到 `v1.37.0`,并继续清理旧 `RELEASE_NOTES.md` 历史堆叠内容,只保留当前版本说明;旧 v1.36.0 发布说明不再作为当前 release note 事实源保留。 +> 发布说明:上一版 release tag 为 `v1.37.0`。本版升级到 `v1.38.0`,并继续清理旧 `RELEASE_NOTES.md` 历史堆叠内容,只保留当前版本说明;旧 v1.37.0 发布说明不再作为当前 release note 事实源保留。 ### 发布概览 -- 应用版本从 `1.36.0` 升级到 `1.37.0`,同步 `package.json`、`package-lock.json`、`packages/lime-cli-npm/package.json`、`src-tauri/Cargo.toml`、`src-tauri/Cargo.lock`、`src-tauri/tauri.conf.json` 与 `src-tauri/tauri.conf.headless.json`。 -- AgentUI / Team Workbench 继续收敛到 current 投影主链:补齐 team workspace canvas、formation、activity preview、selected session detail、team operations 与 live runtime projector 的结构化 selectors 和回归。 -- Agent Chat 工作区强化图片生成与视觉任务预览链路,新增图片 workbench 状态文案、普通视觉 brief 确认、service skill entry 操作与消息列表展示回归。 -- Settings v2 与 Skills 工作台删除旧入口残留,继续减少过时 surface 对 current 页面导航和技能注册投影的干扰。 -- i18n 资源与类型测试扩展到 Agent 工作区新增 presentation keys,维持 `zh-CN`、`zh-TW`、`en-US`、`ja-JP`、`ko-KR` 五语言事实源一致。 +- 应用版本从 `1.37.0` 升级到 `1.38.0`,同步 `package.json`、`package-lock.json`、`packages/lime-cli-npm/package.json`、`src-tauri/Cargo.toml`、`src-tauri/Cargo.lock`、`src-tauri/tauri.conf.json` 与 `src-tauri/tauri.conf.headless.json`。 +- `smart-input` 独立浮窗整体下线,删除旧页面、组件、窗口配置、前端 API 与治理回流入口,不再保留可关闭、拖动、圆角、旧 Logo 等旧浮窗问题面。 +- 旧 `voice-input` 独立语音窗口下线,`Fn` / 语音快捷键改为进入首页 / Claw 输入框录音按钮同一状态机,通过主窗口事件 `voice-start-recording` / `voice-stop-recording` 驱动输入栏录音。 +- 翻译快捷键、截图对话快捷键与截图实验入口清理,保留真正仍属于 current 主链的 `@翻译` skill、`translate_instruction_id` 与首页 `voice-input` guide 语义。 +- 命令边界同步清理:移除旧 `open_voice_window` / `close_voice_window`、截图命令与 smart-input 调用面,补齐 Rust 注册、前端网关、DevBridge mock、governance catalog 与 contracts 守卫。 ### 用户可见更新 -- Team Workbench 增加更完整的队伍概览、画布分层、会话 inline detail、活动预览、协作状态与操作面板展示。 -- 图片生成体验补齐生成状态、素材预览、消息列表承接、workspace image task preview 与发送动作防护。 -- App Sidebar / 页面内容 / 设置首页导航继续向 current 页面集合收敛,移除旧 settings 分类和页面类型残留。 -- Capability Draft 与 Workspace Registered Skills 面板更新注册技能投影与工作区技能可用性展示。 -- `@` command registry smoke 和 TTFT 采样脚本补齐响应式聊天 / stream latency 证据,相关截图作为本轮 GUI 验证证据随 worktree 保留。 +- 首页 / Claw 输入框成为语音识别唯一 current 入口,快捷键不再打开单独快捷输入框,也不会再残留无法关闭或无法停止识别的旧浮窗。 +- 设置页快捷键列表移除已过时、未实现或已下线的翻译 / 截图 / smart-input 入口,避免用户看到无效快捷键。 +- 输入栏录音按钮、语音润色失败兜底、图片附件、排队回合与工具按钮补齐回归测试,保证快捷键进入同一输入栏状态后仍能稳定发送。 +- 历史消息合并修复:只有在 assistant 历史中存在工具、任务预览或过程轨迹缺用户 turn 时才补回用户消息,避免普通分页历史重复补用户消息。 +- 图片任务缓存恢复回归补齐:空历史从图片工作台缓存恢复时保留原始用户输入与完成卡,新草稿不再回灌上一条会话的图片轻卡。 ### 开发者与治理更新 -- `docs/roadmap/agentui` 增加 completion audit、responsive chat TTFT sample matrix 与 stream latency map 更新,记录 AgentUI 主线完成状态和剩余风险。 -- `docs/roadmap/i18n/prd.md` 更新本地化主线进度,明确新增 Agent 工作区文案与测试覆盖。 -- Rust media / image skill launch 链路整理生成任务参数、CLI bridge 与 media task 命令测试,避免图片工具运行时继续依赖旧参数假设。 -- Team Workspace runtime selectors、presentation hooks、operation state、activity previews 与 live runtime projector 增加定向单测,降低 UI 组件直接拼装 runtime facts 的风险。 -- Release note 继续按单版本事实源维护,旧版本说明不再堆叠在当前发布文件内。 +- 新语音快捷键事实源固定为 `src-tauri/src/voice/events.rs`、`voice/shortcut.rs`、`voice/fn_shortcut.rs` 与前端 `src/lib/api/voiceShortcutEvents.ts` / `useInputbarDictation`。 +- 旧 `smart-input` / 旧语音窗口 / 截图对话链路均归类为 `dead`,不再新增 compat 包装层。 +- `ShortcutSettings` 收敛到 Settings v2 shared 目录,旧 `src/components/smart-input/ShortcutSettings*` 删除。 +- 五语言 i18n 资源清理旧快捷键与旧入口文案,继续保持 `zh-CN`、`zh-TW`、`en-US`、`ja-JP`、`ko-KR` 覆盖一致。 +- `protocol-fact-source-guard`、`legacySurfaceCatalog` 与 hotkey catalog 回归补齐旧入口防回流断言。 ### 校验状态 -- `npm run verify:app-version`:通过,目标版本 `1.37.0`。 +- `npm run verify:app-version`:通过,目标版本 `1.38.0`。 - `cargo fmt --manifest-path "src-tauri/Cargo.toml" --all`:通过。 -- `cargo fmt --manifest-path "src-tauri/Cargo.toml" --all -- --check`:通过。 -- `npm run format`:通过。 +- `CARGO_TARGET_DIR="/tmp/lime-cargo-target-v138" cargo test --manifest-path "src-tauri/Cargo.toml"`:通过,主库 `1340 passed; 0 failed; 1 ignored`,集成测试通过。 +- `CARGO_TARGET_DIR="/tmp/lime-cargo-target-v138" cargo clippy --manifest-path "src-tauri/Cargo.toml" --workspace --all-targets -- -D warnings`:通过。 - `npm run lint`:通过。 +- `npm test -- src/components/agent/chat/hooks/agentChatHistory.test.ts src/components/agent/chat/hooks/sessionHistoryMergeController.test.ts`:通过。 +- `npm test -- src/components/agent/chat/workspace/useWorkspaceImageTaskPreviewRuntime.test.tsx`:通过。 - `npm test`:通过,55/55 批次通过。 -- `npm run test:contracts`:通过。 -- `cargo test --manifest-path "src-tauri/Cargo.toml" --workspace --locked`:通过。 -- `cargo clippy --manifest-path "src-tauri/Cargo.toml" --workspace --all-targets -- -D warnings`:通过。 -- `npm run governance:legacy-report`:通过,零引用候选 `0`、边界违规 `0`。 -- `LIME_AGENT_QC_PROVIDER="custom-f74b38b5-6d3f-44ef-aa0f-99d70c3482ed" LIME_AGENT_QC_MODEL="sensenova-6.7-flash-lite" npm run verify:gui-smoke`:通过,覆盖 GUI 主路径发布风险。 -- `LIME_AGENT_QC_PROVIDER="custom-f74b38b5-6d3f-44ef-aa0f-99d70c3482ed" LIME_AGENT_QC_MODEL="sensenova-6.7-flash-lite" npm run verify:local`:通过,包含版本一致性、前端 lint/typecheck/test、contracts、Rust test 与 GUI smoke。 +- `npm run test:contracts`:通过,命令契约、Harness 契约、modality contracts 与 cleanup report contract 均通过。 +- `npm run governance:legacy-report`:通过,边界违规 `0`;本轮 smart-input / screenshot / onboarding dead surface 作为已删除或零引用清理项受控记录。 +- `git diff --check`:通过。 +- `npm run verify:local`:通过,包含版本一致性、前端 lint/typecheck/Vitest/contracts、Rust test 与 GUI smoke。 --- -**完整变更**: `v1.36.0` -> `v1.37.0` +**完整变更**: `v1.37.0` -> `v1.38.0` diff --git a/docs/roadmap/i18n/prd.md b/docs/roadmap/i18n/prd.md index f256d58b0..5a974052f 100644 --- a/docs/roadmap/i18n/prd.md +++ b/docs/roadmap/i18n/prd.md @@ -1279,9 +1279,42 @@ Patch Layer 不能无限期作为事实源。建议退出条件: 291. 2026-05-12 Team Workspace live runtime projector / runtime subscriptions source fallback 收口续测:`src/components/agent/chat/team-workspace-runtime/liveRuntimeProjector.ts` 新增 `LiveRuntimeProjectorCopy` / `buildLiveRuntimeProjectorCopy` 与 source resource 派生默认 copy,把状态切换、文本 / thinking draft、runtime status fallback title / status、tool processing / completed fallback、warning / error title / status,以及 queue / turn lifecycle title / detail / status 从 projector 内建中文 literal 改为 `agentChat.teamWorkspace.liveRuntime.*` 与既有 `agentChat.teamWorkspace.runtimeStatus.*` key-based 读取;`src/components/agent/chat/team-workspace-runtime/runtimeEventSubscriptions.ts` 支持 live runtime copy 注入,`src/components/agent/chat/hooks/useTeamWorkspaceRuntime.ts` 通过 `useTranslation("agent")` 构建并传给 status / stream subscriptions。新增 5 locale `agentChat.teamWorkspace.liveRuntime.*` 27 个正式资源;runtime status event title / detail / checkpoints、tool display label(例如 `页面截图`)、tool output / error、turn error message、sourceType / sourceLabel、queue metadata、session / thread / patch facts 继续作为 runtime / API 数据边界保留,不误迁为静态资源。`src/components/agent/chat/team-workspace-runtime/liveRuntimeProjector.test.ts` 新增真实 en-US copy 注入回归,验证 Status changed、Running、Processing、Queued 与 lifecycle 文案来自资源,同时保留中文工具展示名 `页面截图`;`src/components/agent/chat/hooks/useTeamWorkspaceRuntime.test.tsx` 初始化真实 zh-CN i18n,覆盖 hook 通过 subscriptions 传递 copy 后的 live activity 投影;`src/i18n/__tests__/loadNamespace.test.ts` / `types.test.ts` 补充覆盖 liveRuntime title / detail / lifecycle / status 主键。验证:`npm test -- "src/components/agent/chat/team-workspace-runtime/liveRuntimeProjector.test.ts" "src/components/agent/chat/team-workspace-runtime/runtimeEventSubscriptions.test.ts" "src/components/agent/chat/hooks/useTeamWorkspaceRuntime.test.tsx" "src/components/agent/chat/team-workspace-runtime/activityPreviewSelectors.test.ts" "src/components/agent/chat/team-workspace-runtime/canvasLaneSelectors.test.ts" "src/i18n/__tests__/loadNamespace.test.ts" "src/i18n/__tests__/types.test.ts"` 通过,覆盖 7 个文件、31 个用例;`npx eslint "src/components/agent/chat/team-workspace-runtime/liveRuntimeProjector.ts" "src/components/agent/chat/team-workspace-runtime/liveRuntimeProjector.test.ts" "src/components/agent/chat/team-workspace-runtime/runtimeEventSubscriptions.ts" "src/components/agent/chat/team-workspace-runtime/runtimeEventSubscriptions.test.ts" "src/components/agent/chat/hooks/useTeamWorkspaceRuntime.ts" "src/components/agent/chat/hooks/useTeamWorkspaceRuntime.test.tsx" "src/i18n/__tests__/loadNamespace.test.ts" "src/i18n/__tests__/types.test.ts" --max-warnings 0`、`npm run typecheck`、`npm run detect-translations -- --verbose`、5 个 `agent.json` 的 JSON 解析、liveRuntime 关键 key 的 5 locale 存在性检查、`liveRuntimeProjector.ts` / `runtimeEventSubscriptions.ts` / `useTeamWorkspaceRuntime.ts` 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描与定向 `git diff --check` 通过,当前资源口径在 i18n worktree 更新为 5 个 locale、6 个 namespace、6196 个 source key;合并到 main 后保留主干已有 `agentChat.imageWorkbenchPreview.tool.*` 3 个资源,主干检测口径为 5 个 locale、6 个 namespace、6199 个 source key。本条属于 Team Workspace live runtime projector 普通前端 i18n helper / hook 收口,不触碰 Tauri 命令、Bridge、DevBridge mock、Team runtime API、runtime event payload、activity preview entry payload 或 `teamWorkspaceRuntime.ts` 的 thread item presentation 生成;未重复执行 `npm run verify:local` / GUI smoke,继续沿用记录 281 的非本轮 `src/lib/teamMemorySync.test.ts` `crypto.subtle.digest` undefined 统一入口 blocker 口径,未作为完整绿色统一入口。后续仍需继续审计 `teamWorkspaceRuntime.ts` 生成 runtime activity entry 的 presentation fallback,以及 Agent Chat 更深路径;用户本轮要求合并到 main 并暂停,因此本记录后不继续下一刀。 +292. 2026-05-13 Skills Workspace / official Skill Marketplace / Agent Chat 图片任务 source fallback 与命令边界综合收口续测:`src/components/skills/SkillsWorkspacePage.tsx` 在记录 258 基础上二次清理页面内 `t(key, "中文 fallback")` / `defaultValue` 风险,当前源码中文扫描仅剩“偏好保存失败不应阻断用户继续使用技能”的内部注释;`src/lib/api/officialSkillMarketplace.ts` 把 request failed / skillName required / invalid bundle 错误提示改为 `skills.workspace.marketplace.error.*` key-based copy,并补齐 5 locale `agent.json`;`src/components/agent/chat/utils/plainVisualBriefConfirmation.ts` 把图片任务 plain visual brief intent rules 拆为 `agentChat.imageGeneration.plainVisualBrief.intentRules.includeAny` / `requireAny` / `excludeAny` 三个 string key,运行时按 ` | ` split,避免 typed `agent.json` 出现 object resource 污染;`src/components/agent/chat/utils/imageWorkbenchCommand.ts`、`src/components/agent/chat/workspace/useWorkspaceSendActions.ts` 与 `src/components/agent/chat/workspace/useWorkspaceImageWorkbenchActionRuntime.test.tsx` 补齐 `ImageWorkbenchCommandMode | undefined` 与 `commandKey: "poster_generate"`,`resolveImageMentionCommandKey()` 回到 parsed command 事实源,确保 `@海报` / 图片工作台 poster bridge 继续沿 `image_skill_launch -> Skill(image_generate) -> lime_create_image_generation_task` current 主链,不新增平行 poster 命令或 compat 执行面;`src/components/agent/chat/AgentChatWorkspace.tsx` 将 pending preview helper 拆到 `src/components/agent/chat/homePendingPreview.ts`,`src/components/agent/chat/hooks/agentChatHistory.test.ts` 补齐 `runtimeStatus.detail`,清退本轮 lint / typecheck blocker。验证:`src/lib/api/officialSkillMarketplace.test.ts`、`src/components/skills/SkillsWorkspacePage.test.tsx`、`src/components/agent/chat/utils/plainVisualBriefConfirmation.test.ts`、图片工作台 / 发送链路相关测试与 `src/components/agent/chat/AgentChatWorkspace.homePendingPreview.test.ts` 均通过;核心触达文件 eslint 通过;`npm run detect-translations -- --verbose` 通过,当前口径为 5 个 locale、6 个 namespace、6278 个 source key;`npm run typecheck` 与 `npm run test:contracts` 通过,契约统计为 frontend commands 409、rust registered commands 567、mock priority commands 42、default mock commands 401;Rust 定向 `install_marketplace_skill_bundle_into_root`、`install_skill_zip_bytes_into_root`、`image_skill_launch`、`media_task` 与全量 `cargo test --manifest-path "src-tauri/Cargo.toml"` 通过(1346 个 lib tests + 4 个 integration tests,真实联网测试 ignored);最终 `npm run verify:gui-smoke` 与 `npm run verify:local` 均通过,GUI smoke 覆盖 workspace-ready / browser-runtime / site-adapters / agent-service-skill-entry / agent-runtime-tool-surface / agent-runtime-tool-surface-page / at-command-registry / claw-chat-ready-streaming / knowledge-gui / design-canvas,i18n Patch metrics 仍为 `no-hit`,退出候选 yes;收尾 `git diff --check` 通过。本条属于 Skills Workspace / Agent Chat 图片任务 / command boundary 组合收口;命令侧仅修正现有 parsedCommand / metadata 类型事实源并通过 contracts 验证,未新增 Tauri 命令、Rust 注册、agentCommandCatalog、mockPriorityCommands 或 defaultMocks,也未把 legacy CLI / poster 兼容面接回 current 主链。后续仍需继续 Agent Chat 更深路径、Workspace / Browser Runtime 长尾、P3 自动化治理和 legacy Patch 删除门禁。 + +293. 2026-05-13 Agent Chat MessageList 历史 / 操作 / 产物 chrome source fallback 收口续测:`src/components/agent/chat/components/MessageList.tsx` 把历史助手长消息预览 notice / 展开按钮、历史执行过程折叠摘要(工具步骤 / 思路 / 产物 / 分隔符 / deferred meta / 标题 / 展开)、首 token runtime status 与 streaming inline indicator 的 `defaultValue` fallback、复制成功 / 失败 toast、引用 / 复制 / 保存为 Skill / 保存到灵感库 / 保存到项目资料 action aria/title、Document 产物卡 badge / 可保存提示 / streaming 文案 / 保存按钮、持久化历史窗口 / 尾部历史窗口、恢复生成会话占位、任务中心空态与默认空态,从组件内中文 literal / fallback 改为 `agentChat.messageList.*` key-based 读取;`formatContentLength()` 改用 `formatNumber()`,使历史消息长度、步骤数和历史窗口数量跟随当前 locale。消息正文、用户输入、Artifact title / path / content / preview、底层 runtime error、status enum 映射结果和 session history error 继续作为 runtime / 用户数据或既有下游 presentation 边界保留,不在本条误迁为静态资源。同步 5 locale `agent.json` 新增 `agentChat.messageList.*` 39 个正式资源,`src/i18n/__tests__/loadNamespace.test.ts` / `src/i18n/__tests__/types.test.ts` 补充 key 存在性与 typed interpolation 覆盖;`src/components/agent/chat/components/MessageList.test.tsx` 改为真实 en-US 资源断言,覆盖 restoring、history window、historical preview、task center empty、actions 与 Document artifact card。验证:`npm test -- "src/components/agent/chat/components/MessageList.test.tsx" "src/components/agent/chat/projection/messageTimelineRenderProjection.test.ts" "src/i18n/__tests__/loadNamespace.test.ts" "src/i18n/__tests__/types.test.ts"` 通过,覆盖 4 个文件、130 个用例;`npx eslint "src/components/agent/chat/components/MessageList.tsx" "src/components/agent/chat/components/MessageList.test.tsx" "src/components/agent/chat/projection/messageTimelineRenderProjection.ts" "src/components/agent/chat/projection/messageTimelineRenderProjection.test.ts" "src/i18n/__tests__/loadNamespace.test.ts" "src/i18n/__tests__/types.test.ts" --max-warnings 0`、`npm run detect-translations -- --verbose`、`npm run typecheck`、定向 `git diff --check` 与 `MessageList.tsx` 的 `defaultValue` / `t(key, "中文")` fallback 扫描均通过,当前资源口径为 5 个 locale、6 个 namespace、6320 个 source key。GUI 验证方面,首次 `npm run verify:gui-smoke` 在 `smoke:claw-chat-ready-streaming` 阶段因 `等待长 turn submit 超时` 且 DevBridge 一度 `fetch failed` 失败;随后 `npm run bridge:health -- --timeout-ms 180000` 恢复为 status=ok,并完整重跑 `npm run verify:gui-smoke` 通过,覆盖 workspace-ready / browser-runtime / site-adapters / agent-service-skill-entry / agent-runtime-tool-surface / agent-runtime-tool-surface-page / at-command-registry / claw-chat-ready-streaming / knowledge-gui / design-canvas,i18n Patch metrics 仍为 `no-hit`、退出候选 yes。本条属于 Agent Chat `MessageList` 普通前端 i18n chrome 收口,不新增或修改 Tauri 命令、Bridge、DevBridge mock、`agentCommandCatalog`、`mockPriorityCommands` 或 `defaultMocks`;本轮未重复执行 `npm run verify:local`,不把记录 293 记为新的完整统一入口,完整统一入口仍沿用记录 292 的绿色口径。`src/components/agent/chat/projection/messageTimelineRenderProjection*` 与若干 Agent Chat hook / Rust 文件的既有脏改不属于本条 i18n source fallback 事实源,本条不接手、不回滚。后续仍需继续 Agent Chat 更深路径、Workspace / Browser Runtime 长尾、P3 自动化治理和 legacy Patch 删除门禁。 + +294. 2026-05-13 Agent Chat Home service skill badge / catalog sourceLabel source fallback 收口续测:`src/components/agent/chat/service-skills/homeCopy.ts` 新增 `ServiceSkillHomeCopy` / `buildServiceSkillHomeCopy` 作为首页服务技能文案事实源,`src/components/agent/chat/service-skills/useServiceSkills.ts` 通过 `useTranslation("agent")` 构建 copy,把服务技能首页 badge(最近使用 / 浏览器继续 / 现成做法)与 `catalogMeta.sourceLabel`(起步做法 / 已同步做法)从 hook 内中文 literal 改为 `agentChat.serviceSkills.*` key-based 读取;`src/components/agent/chat/home/homeSkillSurfaceAdapters.ts` 与 `buildHomeSkillSurface.ts` 通过同一 copy 注入,把 slash 最近使用覆盖、已安装 Skill badge 与 skill catalog scene 自定义场景 badge 改为资源文案;`src/components/agent/chat/components/EmptyState.tsx` 复用已有 `translateAgentCopyKey`,确保 Agent Chat 首页 / 第二屏任务库的 service skill / installed skill / catalog scene badge 使用同一 current i18n 入口。技能标题、摘要、runner 文案、`SceneAppEntryCardItem.businessLabel`、用户 replayText 与 catalog runtime 数据继续作为 API / 用户数据或既有下游 presentation 边界保留。同步 5 locale `agent.json` 新增 `agentChat.serviceSkills.badge.*` / `agentChat.serviceSkills.catalogSource.*` 7 个正式资源,`src/i18n/__tests__/loadNamespace.test.ts` / `types.test.ts` 补充 key 存在性与 typed key 覆盖;`src/components/agent/chat/home/buildHomeSkillSurface.test.ts` 与 `useServiceSkills.test.tsx` 补齐 copy 注入 / zh-CN i18n 初始化回归。验证:`npm test -- "src/components/agent/chat/service-skills/useServiceSkills.test.tsx" "src/components/agent/chat/home/buildHomeSkillSurface.test.ts" "src/components/agent/chat/components/EmptyState.test.tsx" "src/i18n/__tests__/loadNamespace.test.ts" "src/i18n/__tests__/types.test.ts"` 通过,覆盖 5 个文件、70 个用例;`npx eslint "src/components/agent/chat/service-skills/homeCopy.ts" "src/components/agent/chat/service-skills/useServiceSkills.ts" "src/components/agent/chat/service-skills/useServiceSkills.test.tsx" "src/components/agent/chat/home/homeSkillSurfaceAdapters.ts" "src/components/agent/chat/home/buildHomeSkillSurface.ts" "src/components/agent/chat/home/buildHomeSkillSurface.test.ts" "src/components/agent/chat/components/EmptyState.tsx" "src/components/agent/chat/components/EmptyState.test.tsx" "src/i18n/__tests__/loadNamespace.test.ts" "src/i18n/__tests__/types.test.ts" --max-warnings 0`、`npm run detect-translations -- --verbose`(5 locale、6 namespace、6327 source keys)、`npm run typecheck`、目标源文件 `defaultValue` / `t(key, "中文")` / 本轮中文 source fallback 扫描与定向 `git diff --check` 均通过。GUI 验证方面,本轮首次 `npm run verify:gui-smoke` 在 `smoke:agent-runtime-tool-surface-page` 等待 Runtime 能力摘要超时;随后单独复跑同一 smoke 通过;完整重跑 `npm run verify:gui-smoke` 推进到 `smoke:claw-chat-ready-streaming` 后因 `等待恢复 turn 完成 超时` 失败,单独复跑该 smoke 仍同样失败,因此记录 294 不宣称完整 GUI smoke 或 `verify:local` 绿色。本条属于 Agent Chat Home / service skill badge 普通前端 i18n 收口,不新增或修改 Tauri 命令、Bridge、DevBridge mock、`agentCommandCatalog`、`mockPriorityCommands` 或 `defaultMocks`;`HomeSurfaceCopy` 里其它 starter / category / guide 文案和 Agent UI projection 更深 helper 仍留给后续刀。 + +295. 2026-05-13 Agent Chat HomeSurfaceCopy starter / guide / gallery chrome source fallback 收口续测:`src/components/agent/chat/home/homeSurfaceCopy.ts` 继续作为 Agent Chat 首页默认 copy 事实源,新增 `HomeSurfaceCopyTranslate` / `HomeSurfaceChromeCopy`,把首页 composer 默认占位、站点技能自然句占位模板、guide help context label、starter chips、本地 category labels、Tab input suggestions、guide cards,以及 starter row / manager button / guide cards / more skills drawer / 第二屏 gallery / scroll cue / 最近会话与最近 SceneApp 补充入口 chrome,从源码中文 literal 改为 `agentChat.home.*` key-based 读取;`HomeStartSurface`、`HomeStarterChips`、`HomeGuideCards`、`HomeMoreSkillsDrawer`、`HomeSkillGallery` 通过 copy prop 只消费 presentation copy,`EmptyState.tsx` 复用同一 `homeSurfaceCopy` 注入首页主路径。同步 5 locale `agent.json`,`agentChat.home.*` 当前覆盖 64 个正式资源;本轮同时把 `ja-JP` / `ko-KR` 中此前沿用英文的 starter prompt、input suggestion、guide title / summary / prompt 替换为对应日 / 韩资源,避免五语言覆盖停留在英文 fallback。`src/components/agent/chat/home/buildHomeSkillSurface.test.ts` 增加 en-US resource 派生回归,验证本地兜底 starter / input suggestion / guide / category 不泄露中文 source copy;`src/components/agent/chat/components/EmptyState.test.tsx` 增加真实 en-US 首页起手 / guide 回归,并补站点技能占位模板断言;Home starter / drawer / gallery 组件测试同步注入 chrome copy;`src/i18n/__tests__/loadNamespace.test.ts` / `types.test.ts` 覆盖 `agentChat.home.composer.autoLaunchExample`、`guideHelp.contextLabelWithStarter`、starter / guide / gallery / category / supplemental 主键。验证:`npm test -- "src/components/agent/chat/home/buildHomeSkillSurface.test.ts" "src/components/agent/chat/home/HomeStartSurface.test.tsx" "src/components/agent/chat/home/HomeStarterChips.test.tsx" "src/components/agent/chat/home/HomeMoreSkillsDrawer.test.tsx" "src/components/agent/chat/home/HomeSkillGallery.test.tsx" "src/components/agent/chat/components/EmptyState.test.tsx" "src/i18n/__tests__/loadNamespace.test.ts" "src/i18n/__tests__/types.test.ts"` 通过,覆盖 8 个文件、78 个用例;目标文件 `npx eslint ... --max-warnings 0`、`npm run detect-translations -- --verbose`(5 locale、6 namespace、6391 source keys)、`npm run typecheck`、5 locale `agentChat.home.*` key 存在性检查、目标 Home copy / chrome 文件 `defaultValue` / `t(key, 中文 fallback)` / 中文 presentation fallback 扫描均通过,扫描仅剩 `buildHomeSkillSurface.ts` 内部中文注释;`EmptyState.tsx` 仍保留本刀外的 hero slogan、quick actions、toast、文件拖拽、curated task review hint 等旧长尾中文,`HomeSceneSkillManagerDialog.tsx` 的场景管理 chrome 也未纳入本刀。GUI 验证方面,`npm run verify:gui-smoke` 本轮完整通过,覆盖 workspace-ready / browser-runtime / site-adapters / agent-service-skill-entry / agent-runtime-tool-surface / agent-runtime-tool-surface-page / at-command-registry / claw-chat-ready-streaming / knowledge-gui / design-canvas,i18n Patch metrics 为 `no-hit`、退出候选 yes;随后尝试 `npm run verify:local`,已通过 app-version / lint / typecheck,并推进 smart vitest 至第 6 批,但被非本轮并行脏改 `src/components/agent/chat/hooks/agentStreamRuntimeHandler.ts:999` 的 `Unexpected "case"` 语法错误阻断,因此本条不宣称完整 `verify:local` 绿色。本条属于 Agent Chat Home 普通前端 i18n chrome 收口,不新增或修改 Tauri 命令、Bridge、DevBridge mock、`agentCommandCatalog`、`mockPriorityCommands` 或 `defaultMocks`;并行 Rust / agent stream / hook 脏改不属于本条事实源,本条不接手、不回滚。 + 仍未完成: -1. `common` namespace 已覆盖 App 根壳层、SplashScreen 启动画面(source fallback 已在记录 229 二次收口)、StartupLoadingScreen 启动加载屏(source fallback 已在记录 226 二次收口)、ConfirmDialog 共享确认弹窗(source fallback 已在记录 228 二次收口)、UpdateNotification 独立更新提醒窗口(source fallback 已在记录 218 二次收口)、SmartInput 独立悬浮输入窗(source fallback 已在记录 219 二次收口)、ShortcutSettings 共享快捷键设置组件(source fallback 已在记录 224 二次收口)、LatestRunStatusBadge 执行状态徽章(source fallback 已在记录 218 二次收口)、项目 / 工作区切换器(ProjectSelector source fallback 已在记录 216 二次收口)、新建项目弹窗(source fallback 已在记录 235 二次收口)、useProjects 重命名空名称错误(source fallback 已在记录 236 二次收口)、Deep Link 邀请 / 登录 / 支付 toast(source fallback 已在记录 237 二次收口)、App Startup Windows 启动自检 toast(source fallback 已在记录 238 二次收口)、OEM Cloud Access 登录 / OAuth / 支付反馈 / 会话刷新退出 / 邮箱验证码 / 密码登录 / Provider 默认服务 / API Key / 状态标签 / 浏览器预打开页与外部浏览器错误反馈(source fallback 已在记录 239 二次收口),以及 OEM Lime Hub provider sync 托管 Key 名称(source fallback 已在记录 239 二次收口;旧 `Lime 云端模型` 仅作为 legacy 本地 Key alias 数据匹配保留);`navigation` namespace 已覆盖侧边栏主入口、搜索弹窗、会话列表起步路径、会话 meta 格式化、会话重命名 / 删除的 prompt / confirm / toast、账号菜单核心未登录 / 已登录操作文案、邀请入口 / 邀请弹窗 / 复制邀请 toast,以及外观快捷面板;账号套餐 usage 已确认属于云端运行时字段边界,不再作为侧栏静态 i18n 缺口;`workspace` namespace 已覆盖 ArtifactToolbar 工具栏 chrome、ArtifactRenderer 统一入口空内容态 / 流式状态 / 未知类型回退、CanvasAdapter 图层设计预览 chrome、CanvasBreadcrumbHeader 返回按钮(source fallback 已在记录 230 收口)、Resource Manager PDF 预览器(source fallback 已在记录 231 收口)、Resource Manager 媒体播放预览器(source fallback 已在记录 232 收口)、DocumentRenderer 文档工具栏 chrome、ArtifactDocumentRenderer 结构化阅读面 chrome / block fallback、BrowserAssistRenderer 状态说明 / browserActionIndex 复盘、A2UI 提交完成提示 / 待处理表单状态 chrome(WorkspacePendingA2UIPanel source fallback 已在记录 276 二次收口)、图片素材 `ImageGallery`、Runtime AGENTS 引导 toast、视频 PromptInput(source fallback 已在记录 218 二次收口)、视频 VideoCanvas 插图反馈 / 侧栏按钮、视频 VideoSidebar 参数栏概览 / 创作 Tips / 参考图上传区 / 基础控制区 / 模型面板、视频 VideoWorkspace 首屏 / 摘要卡 / 最近任务标题 / 预览操作 / 任务状态 / 同步提示 / 会话主预览 / 聚焦任务面板 / 生成入口反馈 / 参考图处理、Browser Runtime 工作台外壳、环境预设子面板、资料管理子面板、Debug 面板外壳 / 状态 / 最小控制起步路径、Debug 审计与附着 Chrome 子面板、Debug CDP 托管会话高级区、existing session hook feedback / presentation 与 Site Adapter 面板 chrome / 执行反馈,`agent` namespace 已覆盖 Skills / CuratedTask / 能力草案等起步路径、Skills 仓库管理面板(已在记录 210 收口)、SkillCard 卡片 chrome(已在记录 212 收口)、内容检查弹窗(已在记录 213 收口)、WorkflowProgress 进度组件(已在记录 215 收口)与 SkillsPage 高级入口(已在记录 217 收口),`errors` namespace 已覆盖 Crash Recovery 外壳、Artifact ErrorFallbackRenderer 错误回退面板、Artifact SvgRenderer 预览控制 / 错误态、Artifact CodeRenderer 代码工具栏 / HTML 预览控制 / 错误态、Artifact HtmlRenderer 预览控制 / 错误态、Artifact ReactRenderer 预览 chrome / 编译错误态、Artifact MermaidRenderer 预览控制 / 导出控制 / 错误态与 crashDiagnostic 共享诊断摘要 / 复制失败 / 下载目录 fallback;Workspace 其他媒体 / Browser Runtime 其他子面板 / 工作台路径长尾与 Agent Chat 更深路径仍需 P1/P2 逐步迁移;Settings 共享壳层(侧边栏浮动导航 chrome 已在记录 144 收口,useSettingsCategory 导航分组 fallback 已在记录 187 收口,主布局 / Sidebar 壳层 fallback 已在记录 196 收口)、home(首屏总览 fallback 已在记录 197 收口)、developer-lab(合并入口 fallback 已在记录 188 收口)、system/developer(壳层与目录联调 fallback 已在记录 204 收口)、system/shared Clipboard 权限指引(ClipboardPermissionGuideCard fallback 已在记录 193 收口)、CrashRecoveryPanel 恢复模式外壳(已在记录 205 二次收口 UI fallback 与真实资源回归)、system/environment(主体 fallback 已在记录 202 收口)、system/experimental(主体 fallback 已在记录 201 收口,更新检查子面板二次 fallback 已在记录 208 收口)、system/web-search(壳层 / summary / tabs / sticky footer source fallback 与测试 mock fallback 已在记录 243 二次收口,Provider 凭证区 source fallback 已在记录 244 二次收口,MSE 聚合配置区 source fallback 已在记录 245 二次收口,图片搜索区 source fallback 已在记录 246 二次收口,观测面板配置区 source fallback 已在记录 247 二次收口,搜索链路字段 source fallback 已在记录 248 二次收口,页面用户可见 source fallback 已清零;console 诊断中文仅内部日志边界)、system/channels 工作台壳层、网关与隧道表单、Telegram / Feishu / 微信运行控制、微信兼容扫码排障 source fallback 与测试 mock 已在记录 257 二次收口(日志 Tail 面板 defaultValue fallback 已在记录 186 收口)、system/chrome-relay 独立连接引导窗口(已在记录 200 收口) / 主设置页全量 fallback(已在记录 203 收口)、system/shared Workspace 自愈记录共享卡片(WorkspaceRepairHistoryCard fallback 已在记录 195 收口)、system/automation 当前焦点条(AutomationJobFocusStrip fallback 已在记录 189 收口) / 概览焦点卡(AutomationOverviewFocusCard fallback 已在记录 191 收口) / HealthPanel(已在记录 192 收口) / AutomationJobDialog 弹窗(已在记录 211 收口) / AutomationJobDetailsDialog 详情弹窗与内嵌运行历史(已在记录 214 收口) / 主页面壳层与调度器设置 source fallback 二次收口已在记录 249(测试侧 legacy `react-i18next` mock 已在记录 256 收口为真实资源回归) / 开始模板与任务列表 source fallback 二次收口已在记录 250(测试侧 legacy `react-i18next` mock 已在记录 256 收口为真实资源回归) / overview tab raw literals 已在记录 251 收口 / 主页面运行历史区刷新失败 toast 已在记录 252 收口 / serviceSkill context helper 已在记录 253 二次收口 / presentation details helper 已在记录 254 二次收口 / SceneApp 动作 toast 与结果材料 missingPathMessage 已在记录 255 收口、general/memory(记忆总页 fallback 已在记录 209 收口,MemoryCuratedTaskSuggestionPanel source fallback 已在记录 266 收口,MemoryPage 首页 CuratedTask 推荐区 source fallback 已在记录 267 收口)、general/hotkeys(已审计快捷键页 fallback 已在记录 199 收口)、account/profile(字段卡 / 编辑动作 / 保存反馈已在记录 241 二次收口,hero / avatar / tags / usage / 上传错误长尾已在记录 242 二次收口,source fallback 与测试 mock fallback 已清零)、account/stats(统计页 defaultValue fallback 已在记录 184 收口,仍待更广设置页统一门禁)、user-center-session(已在记录 194 收口)、about(版本与更新区 fallback 已在记录 198 收口)、agent/skills 高级入口(SkillsPage 已在记录 217 收口,SkillsWorkspacePage source fallback 与测试 mock 已在记录 258 二次收口;installedSkillPresentation helper 独立中文默认 copy 已在记录 259 收口;CharacterMentionPanel / 当前 input capability 面板调用方 source fallback 与测试 mock 已在记录 260 二次收口;inputCapabilitySections.ts helper 内建默认 copy 已在记录 261 收口;service-skill helper 默认 copy 已在记录 264 收口;ServiceSkill launch prefill / creation replay 无 copy 调用面已在记录 269 收口;Runtime Scene Gate A2UI chrome / toast source fallback 已在记录 270 收口;sceneAppCuratedTaskReference helper UI copy 已在记录 271 收口(existing_results marker / baseline prompt / project_goal fallback 仍属 runtime 语义边界);curatedTaskTemplates.ts 定义层默认 copy 已在记录 263 收口,CuratedTask presentation helper 无 copy 调用面已在记录 265 收口)、agent/image-gen(媒体生成默认模型 fallback 已在记录 190 收口)、agent/video-gen(媒体生成默认模型 fallback 已在记录 190 收口)、agent/media-services(服务模型总页 fallback 已在记录 206 收口)、agent/voice(语音设置页 fallback 已在记录 207 收口)、agent/providers 的顶部切换器 / 云端反馈 / 左侧启用模型列表 / 导入导出弹窗(ImportExportDialog source fallback 已在记录 223 二次收口) / 详情配置外壳入口态 / 详情配置主体静态表单 / 删除确认与连接测试反馈 / 接口获取模型动态反馈 / 添加流程静态 chrome / 添加流程校验错误与动态反馈 / 添加流程目录辅助文案 / 添加流程内置模板名称与描述 / 添加流程 provider type label / 添加流程接口获取 helper 调用面审计 / 添加流程 runtime 数据边界审计 / 能力草案面板(CapabilityDraftPanel source fallback 已在记录 221 二次收口) / Workspace 已注册能力面板与 Agent envelope / Managed Job presentation / 桌宠能力偏好卡(source fallback 已在记录 240 二次收口) / Companion Bridge 诊断区已完成起步迁移,CuratedTask 模板定义与 Agent Chat / Skills / Home 结果模板主调用面已完成起步迁移;Agent Chat 会话过程索引面板(CanvasSessionOverviewPanel 已在记录 225 收口)、搜索结果预览列表(SearchResultPreviewList 已在记录 227 收口)、验证结果展示(HarnessVerificationSummarySection 已在记录 233 收口)、活跃 incident 面板(AgentIncidentPanel 已在记录 234 收口)、Agent Chat 输入能力区 section heading / banner / helper chrome、Skills 执行对话框 chrome / source fallback(chrome 已在记录 222 二次收口,CuratedTaskLauncherDialog source fallback 与测试 mock 已在记录 262 二次收口)、Skill 脚手架启动弹窗 chrome / source fallback(已在记录 220 二次收口)、Browser Runtime existing session hook feedback / presentation、Browser Runtime Debug 审计与附着 Chrome 子面板、Browser Runtime Debug CDP 托管会话高级区、Browser Site Adapter 面板 chrome / 执行反馈、SceneApp 最近判断建议横幅 runtime suggestion 与 General Workbench 最近判断建议横幅 runtime suggestion 已完成起步迁移,SceneAppExecutionSummaryCard UI chrome 已在记录 272 收口;GeneralWorkbenchContextPanel 上下文搜索 / 列表 / 详情 / 添加弹窗 chrome 已在记录 277 收口;ImageWorkbenchMessagePreview 图片任务消息卡 UI copy 已在记录 278 收口;Team Workspace 画布工具栏 / action buttons 已在记录 279 收口,session control toast 已在记录 280 收口,operation panel / selectors 已在记录 281 收口,formation selector / dock formation chrome 已在记录 282 收口,selected session inline chrome 已在记录 283 收口,board presentation / overview chrome 已在记录 284 收口,canvas lane selector / runtime hook 已在记录 285 收口,CanvasStage chrome 已在记录 286 收口,Dock current progress / concurrency chrome 已在记录 287 收口,selected session detail selector 已在记录 288 收口,board chrome selector 已在记录 289 收口,activity preview selector / sync hook 已在记录 290 收口,live runtime projector / runtime subscription 已在记录 291 收口;GeneralWorkbenchWorkflowPanel 渲染层 UI chrome 已在记录 273 收口,outputs / activity / runDetail helper 已在记录 274 收口,branch / handoff / follow-up source fallback 已在记录 275 清零;其他启动弹窗 runtime suggestion、其他模型执行提示正文和运行时 suggestion 长尾仍需继续;CuratedTaskBadge review / sceneapp prefix 已在记录 268 收口;sceneAppCuratedTaskReference helper UI copy 已在记录 271 收口,SceneAppExecutionSummaryCard UI chrome 已在记录 272 收口,prompt / marker 语义保留为 compat/runtime 边界;Workspace Skill runtime enable 首发提示正文已在条目 102 收口,Skills 执行对话框 chrome 已在条目 103 收口,source fallback 已在记录 222 二次收口,Skill 脚手架启动弹窗 chrome 已在条目 105 收口,source fallback 已在记录 220 二次收口,Browser Runtime existing session hook feedback 已在条目 106 收口,Browser Runtime existing session presentation 已在条目 112 收口,SceneApp 最近判断建议横幅 runtime suggestion 已在条目 107 收口,Browser Site Adapter 面板 chrome / 执行反馈已在条目 108 收口,General Workbench 最近判断建议横幅 runtime suggestion 已在条目 109 收口,Browser Runtime Debug 审计与附着 Chrome 子面板已在条目 110 收口,Browser Runtime Debug CDP 托管会话高级区已在条目 111 收口。 +1. `common` namespace 已覆盖 App 根壳层、SplashScreen 启动画面(source fallback 已在记录 229 二次收口)、StartupLoadingScreen 启动加载屏(source fallback 已在记录 226 二次收口)、ConfirmDialog 共享确认弹窗(source fallback 已在记录 228 二次收口)、UpdateNotification 独立更新提醒窗口(source fallback 已在记录 218 二次收口)、SmartInput 独立悬浮输入窗(source fallback 已在记录 219 二次收口)、ShortcutSettings 共享快捷键设置组件(source fallback 已在记录 224 二次收口)、LatestRunStatusBadge 执行状态徽章(source fallback 已在记录 218 二次收口)、项目 / 工作区切换器(ProjectSelector source fallback 已在记录 216 二次收口)、新建项目弹窗(source fallback 已在记录 235 二次收口)、useProjects 重命名空名称错误(source fallback 已在记录 236 二次收口)、Deep Link 邀请 / 登录 / 支付 toast(source fallback 已在记录 237 二次收口)、App Startup Windows 启动自检 toast(source fallback 已在记录 238 二次收口)、OEM Cloud Access 登录 / OAuth / 支付反馈 / 会话刷新退出 / 邮箱验证码 / 密码登录 / Provider 默认服务 / API Key / 状态标签 / 浏览器预打开页与外部浏览器错误反馈(source fallback 已在记录 239 二次收口),以及 OEM Lime Hub provider sync 托管 Key 名称(source fallback 已在记录 239 二次收口;旧 `Lime 云端模型` 仅作为 legacy 本地 Key alias 数据匹配保留);`navigation` namespace 已覆盖侧边栏主入口、搜索弹窗、会话列表起步路径、会话 meta 格式化、会话重命名 / 删除的 prompt / confirm / toast、账号菜单核心未登录 / 已登录操作文案、邀请入口 / 邀请弹窗 / 复制邀请 toast,以及外观快捷面板;账号套餐 usage 已确认属于云端运行时字段边界,不再作为侧栏静态 i18n 缺口;`workspace` namespace 已覆盖 ArtifactToolbar 工具栏 chrome、ArtifactRenderer 统一入口空内容态 / 流式状态 / 未知类型回退、CanvasAdapter 图层设计预览 chrome、CanvasBreadcrumbHeader 返回按钮(source fallback 已在记录 230 收口)、Resource Manager PDF 预览器(source fallback 已在记录 231 收口)、Resource Manager 媒体播放预览器(source fallback 已在记录 232 收口)、DocumentRenderer 文档工具栏 chrome、ArtifactDocumentRenderer 结构化阅读面 chrome / block fallback、BrowserAssistRenderer 状态说明 / browserActionIndex 复盘、A2UI 提交完成提示 / 待处理表单状态 chrome(WorkspacePendingA2UIPanel source fallback 已在记录 276 二次收口)、图片素材 `ImageGallery`、Runtime AGENTS 引导 toast、视频 PromptInput(source fallback 已在记录 218 二次收口)、视频 VideoCanvas 插图反馈 / 侧栏按钮、视频 VideoSidebar 参数栏概览 / 创作 Tips / 参考图上传区 / 基础控制区 / 模型面板、视频 VideoWorkspace 首屏 / 摘要卡 / 最近任务标题 / 预览操作 / 任务状态 / 同步提示 / 会话主预览 / 聚焦任务面板 / 生成入口反馈 / 参考图处理、Browser Runtime 工作台外壳、环境预设子面板、资料管理子面板、Debug 面板外壳 / 状态 / 最小控制起步路径、Debug 审计与附着 Chrome 子面板、Debug CDP 托管会话高级区、existing session hook feedback / presentation 与 Site Adapter 面板 chrome / 执行反馈,`agent` namespace 已覆盖 Skills / CuratedTask / 能力草案等起步路径、Skills 仓库管理面板(已在记录 210 收口)、SkillCard 卡片 chrome(已在记录 212 收口)、内容检查弹窗(已在记录 213 收口)、WorkflowProgress 进度组件(已在记录 215 收口)与 SkillsPage 高级入口(已在记录 217 收口),`errors` namespace 已覆盖 Crash Recovery 外壳、Artifact ErrorFallbackRenderer 错误回退面板、Artifact SvgRenderer 预览控制 / 错误态、Artifact CodeRenderer 代码工具栏 / HTML 预览控制 / 错误态、Artifact HtmlRenderer 预览控制 / 错误态、Artifact ReactRenderer 预览 chrome / 编译错误态、Artifact MermaidRenderer 预览控制 / 导出控制 / 错误态与 crashDiagnostic 共享诊断摘要 / 复制失败 / 下载目录 fallback;Workspace 其他媒体 / Browser Runtime 其他子面板 / 工作台路径长尾与 Agent Chat 更深路径仍需 P1/P2 逐步迁移;Settings 共享壳层(侧边栏浮动导航 chrome 已在记录 144 收口,useSettingsCategory 导航分组 fallback 已在记录 187 收口,主布局 / Sidebar 壳层 fallback 已在记录 196 收口)、home(首屏总览 fallback 已在记录 197 收口)、developer-lab(合并入口 fallback 已在记录 188 收口)、system/developer(壳层与目录联调 fallback 已在记录 204 收口)、system/shared Clipboard 权限指引(ClipboardPermissionGuideCard fallback 已在记录 193 收口)、CrashRecoveryPanel 恢复模式外壳(已在记录 205 二次收口 UI fallback 与真实资源回归)、system/environment(主体 fallback 已在记录 202 收口)、system/experimental(主体 fallback 已在记录 201 收口,更新检查子面板二次 fallback 已在记录 208 收口)、system/web-search(壳层 / summary / tabs / sticky footer source fallback 与测试 mock fallback 已在记录 243 二次收口,Provider 凭证区 source fallback 已在记录 244 二次收口,MSE 聚合配置区 source fallback 已在记录 245 二次收口,图片搜索区 source fallback 已在记录 246 二次收口,观测面板配置区 source fallback 已在记录 247 二次收口,搜索链路字段 source fallback 已在记录 248 二次收口,页面用户可见 source fallback 已清零;console 诊断中文仅内部日志边界)、system/channels 工作台壳层、网关与隧道表单、Telegram / Feishu / 微信运行控制、微信兼容扫码排障 source fallback 与测试 mock 已在记录 257 二次收口(日志 Tail 面板 defaultValue fallback 已在记录 186 收口)、system/chrome-relay 独立连接引导窗口(已在记录 200 收口) / 主设置页全量 fallback(已在记录 203 收口)、system/shared Workspace 自愈记录共享卡片(WorkspaceRepairHistoryCard fallback 已在记录 195 收口)、system/automation 当前焦点条(AutomationJobFocusStrip fallback 已在记录 189 收口) / 概览焦点卡(AutomationOverviewFocusCard fallback 已在记录 191 收口) / HealthPanel(已在记录 192 收口) / AutomationJobDialog 弹窗(已在记录 211 收口) / AutomationJobDetailsDialog 详情弹窗与内嵌运行历史(已在记录 214 收口) / 主页面壳层与调度器设置 source fallback 二次收口已在记录 249(测试侧 legacy `react-i18next` mock 已在记录 256 收口为真实资源回归) / 开始模板与任务列表 source fallback 二次收口已在记录 250(测试侧 legacy `react-i18next` mock 已在记录 256 收口为真实资源回归) / overview tab raw literals 已在记录 251 收口 / 主页面运行历史区刷新失败 toast 已在记录 252 收口 / serviceSkill context helper 已在记录 253 二次收口 / presentation details helper 已在记录 254 二次收口 / SceneApp 动作 toast 与结果材料 missingPathMessage 已在记录 255 收口、general/memory(记忆总页 fallback 已在记录 209 收口,MemoryCuratedTaskSuggestionPanel source fallback 已在记录 266 收口,MemoryPage 首页 CuratedTask 推荐区 source fallback 已在记录 267 收口)、general/hotkeys(已审计快捷键页 fallback 已在记录 199 收口)、account/profile(字段卡 / 编辑动作 / 保存反馈已在记录 241 二次收口,hero / avatar / tags / usage / 上传错误长尾已在记录 242 二次收口,source fallback 与测试 mock fallback 已清零)、account/stats(统计页 defaultValue fallback 已在记录 184 收口,仍待更广设置页统一门禁)、user-center-session(已在记录 194 收口)、about(版本与更新区 fallback 已在记录 198 收口)、agent/skills 高级入口(SkillsPage 已在记录 217 收口,SkillsWorkspacePage source fallback 与测试 mock 已在记录 258 二次收口,页面 source fallback 二次清理、official Skill Marketplace 错误 copy、plain visual brief intent rules string key 与图片 poster bridge 类型边界已在记录 292 综合收口;installedSkillPresentation helper 独立中文默认 copy 已在记录 259 收口;CharacterMentionPanel / 当前 input capability 面板调用方 source fallback 与测试 mock 已在记录 260 二次收口;inputCapabilitySections.ts helper 内建默认 copy 已在记录 261 收口;service-skill helper 默认 copy 已在记录 264 收口;ServiceSkill launch prefill / creation replay 无 copy 调用面已在记录 269 收口;Runtime Scene Gate A2UI chrome / toast source fallback 已在记录 270 收口;sceneAppCuratedTaskReference helper UI copy 已在记录 271 收口(existing_results marker / baseline prompt / project_goal fallback 仍属 runtime 语义边界);curatedTaskTemplates.ts 定义层默认 copy 已在记录 263 收口,CuratedTask presentation helper 无 copy 调用面已在记录 265 收口)、agent/image-gen(媒体生成默认模型 fallback 已在记录 190 收口)、agent/video-gen(媒体生成默认模型 fallback 已在记录 190 收口)、agent/media-services(服务模型总页 fallback 已在记录 206 收口)、agent/voice(语音设置页 fallback 已在记录 207 收口)、agent/providers 的顶部切换器 / 云端反馈 / 左侧启用模型列表 / 导入导出弹窗(ImportExportDialog source fallback 已在记录 223 二次收口) / 详情配置外壳入口态 / 详情配置主体静态表单 / 删除确认与连接测试反馈 / 接口获取模型动态反馈 / 添加流程静态 chrome / 添加流程校验错误与动态反馈 / 添加流程目录辅助文案 / 添加流程内置模板名称与描述 / 添加流程 provider type label / 添加流程接口获取 helper 调用面审计 / 添加流程 runtime 数据边界审计 / 能力草案面板(CapabilityDraftPanel source fallback 已在记录 221 二次收口) / Workspace 已注册能力面板与 Agent envelope / Managed Job presentation / 桌宠能力偏好卡(source fallback 已在记录 240 二次收口) / Companion Bridge 诊断区已完成起步迁移,CuratedTask 模板定义与 Agent Chat / Skills / Home 结果模板主调用面已完成起步迁移;Agent Chat 会话过程索引面板(CanvasSessionOverviewPanel 已在记录 225 收口)、搜索结果预览列表(SearchResultPreviewList 已在记录 227 收口)、验证结果展示(HarnessVerificationSummarySection 已在记录 233 收口)、活跃 incident 面板(AgentIncidentPanel 已在记录 234 收口)、Agent Chat 输入能力区 section heading / banner / helper chrome、Skills 执行对话框 chrome / source fallback(chrome 已在记录 222 二次收口,CuratedTaskLauncherDialog source fallback 与测试 mock 已在记录 262 二次收口)、Skill 脚手架启动弹窗 chrome / source fallback(已在记录 220 二次收口)、Browser Runtime existing session hook feedback / presentation、Browser Runtime Debug 审计与附着 Chrome 子面板、Browser Runtime Debug CDP 托管会话高级区、Browser Site Adapter 面板 chrome / 执行反馈、SceneApp 最近判断建议横幅 runtime suggestion 与 General Workbench 最近判断建议横幅 runtime suggestion 已完成起步迁移,SceneAppExecutionSummaryCard UI chrome 已在记录 272 收口;GeneralWorkbenchContextPanel 上下文搜索 / 列表 / 详情 / 添加弹窗 chrome 已在记录 277 收口;ImageWorkbenchMessagePreview 图片任务消息卡 UI copy 已在记录 278 收口;Team Workspace 画布工具栏 / action buttons 已在记录 279 收口,session control toast 已在记录 280 收口,operation panel / selectors 已在记录 281 收口,formation selector / dock formation chrome 已在记录 282 收口,selected session inline chrome 已在记录 283 收口,board presentation / overview chrome 已在记录 284 收口,canvas lane selector / runtime hook 已在记录 285 收口,CanvasStage chrome 已在记录 286 收口,Dock current progress / concurrency chrome 已在记录 287 收口,selected session detail selector 已在记录 288 收口,board chrome selector 已在记录 289 收口,activity preview selector / sync hook 已在记录 290 收口,live runtime projector / runtime subscription 已在记录 291 收口,MessageList 历史消息预览 / 历史执行过程折叠 / 消息操作按钮 / Document 产物卡 / 历史窗口 / 恢复占位 / 任务中心空态 / 默认空态已在记录 293 收口,Agent Chat Home service skill badge / catalog sourceLabel / installed 与 custom scene badge 已在记录 294 收口,HomeSurfaceCopy starter / category / input suggestion / guide / gallery chrome 已在记录 295 收口,HomeSceneSkillManagerDialog 场景管理弹窗 chrome / error / form 已在记录 297 收口,EmptyState hero / quick actions / toast / 文件拖拽 / 路径引用空发送 fallback / curated task review hint 已在记录 298 收口;GeneralWorkbenchWorkflowPanel 渲染层 UI chrome 已在记录 273 收口,outputs / activity / runDetail helper 已在记录 274 收口,branch / handoff / follow-up source fallback 已在记录 275 清零;其他启动弹窗 runtime suggestion、其他模型执行提示正文和运行时 suggestion 长尾仍需继续;CuratedTaskBadge review / sceneapp prefix 已在记录 268 收口;sceneAppCuratedTaskReference helper UI copy 已在记录 271 收口,SceneAppExecutionSummaryCard UI chrome 已在记录 272 收口,prompt / marker 语义保留为 compat/runtime 边界;Workspace Skill runtime enable 首发提示正文已在条目 102 收口,Skills 执行对话框 chrome 已在条目 103 收口,source fallback 已在记录 222 二次收口,Skill 脚手架启动弹窗 chrome 已在条目 105 收口,source fallback 已在记录 220 二次收口,Browser Runtime existing session hook feedback 已在条目 106 收口,Browser Runtime existing session presentation 已在条目 112 收口,SceneApp 最近判断建议横幅 runtime suggestion 已在条目 107 收口,Browser Site Adapter 面板 chrome / 执行反馈已在条目 108 收口,General Workbench 最近判断建议横幅 runtime suggestion 已在条目 109 收口,Browser Runtime Debug 审计与附着 Chrome 子面板已在条目 110 收口,Browser Runtime Debug CDP 托管会话高级区已在条目 111 收口。 2. `legacy-patch/` 已完成物理隔离,并已有运行时命中量指标 API、GUI smoke metrics 导出链与离线 text / JSON 报告脚本;但 CI 尚未把 `i18n:patch-report --check` 接入删除门禁,也尚未把 `no-hit` 报告和 current 主路径依赖审计绑定为 DOM replacer 删除条件。退出条件仍是 P3 后按命中量和 current 主路径依赖清零逐步拆除。 3. 类型绑定已覆盖当前 source resource,`AGENTS.md` / 质量工作流也已封住新增功能必须走 current i18n 的人工规则;但尚未接入官方 `i18next-cli` 的抽取 / hardcoded string lint / type generation,也尚未评估 selector API 或 `@i18next-selector/codemod` 是否适合资源规模扩大后的 P3 阶段。 -4. 记录 291 已取得 JSON 解析、定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check`、5 locale 关键 key 存在性检查与 Team Workspace live runtime projector / subscriptions 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描证据;本轮未重复执行 `npm run verify:local` / GUI smoke,继续沿用记录 281 的非本轮 `src/lib/teamMemorySync.test.ts` `crypto.subtle.digest` undefined 统一入口 blocker 口径,未作为完整绿色统一入口。记录 290 已取得 JSON 解析、定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check`、5 locale 关键 key 存在性检查与 Team Workspace activity preview selector / sync hook 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描证据;本轮未重复执行 `npm run verify:local` / GUI smoke,继续沿用记录 281 的非本轮 `src/lib/teamMemorySync.test.ts` `crypto.subtle.digest` undefined 统一入口 blocker 口径,未作为完整绿色统一入口。记录 289 已取得 JSON 解析、定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check`、5 locale 关键 key 存在性检查与 Team Workspace board chrome selector 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描证据;本轮未重复执行 `npm run verify:local` / GUI smoke,继续沿用记录 281 的非本轮 `src/lib/teamMemorySync.test.ts` `crypto.subtle.digest` undefined 统一入口 blocker 口径,未作为完整绿色统一入口。记录 288 已取得 JSON 解析、定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check`、5 locale 关键 key 存在性检查与 Team Workspace selected session detail selector 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描证据;本轮未重复执行 `npm run verify:local` / GUI smoke,继续沿用记录 281 的非本轮 `src/lib/teamMemorySync.test.ts` `crypto.subtle.digest` undefined 统一入口 blocker 口径,未作为完整绿色统一入口。记录 287 已取得 JSON 解析、定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check`、5 locale 关键 key 存在性检查与 Team Workspace Dock current progress / concurrency chrome 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描证据;本轮未重复执行 `npm run verify:local` / GUI smoke,继续沿用记录 281 的非本轮 `src/lib/teamMemorySync.test.ts` `crypto.subtle.digest` undefined 统一入口 blocker 口径,未作为完整绿色统一入口。记录 286 已取得 JSON 解析、定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check`、5 locale 关键 key 存在性检查与 Team Workspace CanvasStage chrome 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描证据;本轮未重复执行 `npm run verify:local` / GUI smoke,继续沿用记录 281 的非本轮 `src/lib/teamMemorySync.test.ts` `crypto.subtle.digest` undefined 统一入口 blocker 口径,未作为完整绿色统一入口。记录 285 已取得 JSON 解析、定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check`、5 locale 关键 key 存在性检查与 Team Workspace canvas lane selector / runtime hook 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描证据;本轮未重复执行 `npm run verify:local` / GUI smoke,继续沿用记录 281 的非本轮 `src/lib/teamMemorySync.test.ts` `crypto.subtle.digest` undefined 统一入口 blocker 口径,未作为完整绿色统一入口。记录 284 已取得 JSON 解析、定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check`、5 locale 关键 key 存在性检查与 Team Workspace board presentation / overview chrome 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描证据;本轮未重复执行 `npm run verify:local` / GUI smoke,继续沿用记录 281 的非本轮 `src/lib/teamMemorySync.test.ts` `crypto.subtle.digest` undefined 统一入口 blocker 口径,未作为完整绿色统一入口。记录 283 已取得 JSON 解析、定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check`、5 locale 关键 key 存在性检查与 Team Workspace selected session inline header / collaboration / activity / detail 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描证据;本轮未重复执行 `npm run verify:local` / GUI smoke,继续沿用记录 281 的非本轮 `src/lib/teamMemorySync.test.ts` `crypto.subtle.digest` undefined 统一入口 blocker 口径,未作为完整绿色统一入口。记录 282 已取得 JSON 解析、定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check`、5 locale 关键 key 存在性检查与 Team Workspace formation selector / board formation hook 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描证据,并确认 Dock / SummaryPanel 新增 `t()` 调用无第二参数 fallback、formation 相关目标硬编码清零;本轮未重复执行 `npm run verify:local` / GUI smoke,继续沿用记录 281 的非本轮 `src/lib/teamMemorySync.test.ts` `crypto.subtle.digest` undefined 统一入口 blocker 口径,未作为完整绿色统一入口。记录 281 已取得 JSON 解析、定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check`、5 locale 关键 key 存在性检查与 Team Workspace operation selectors / panel / operation state hook 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描证据;本轮额外重跑 `npm run verify:local`,已通过 `verify:app-version`、`lint`、`typecheck` 与 smart vitest 1-10 批,在 11/55 批同样因非本轮 `src/lib/teamMemorySync.test.ts` 的 `crypto.subtle.digest` undefined 失败,未作为完整绿色统一入口。记录 280 已取得 JSON 解析、定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check` 与 Team Workspace session control hook 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描证据;本轮 `npm run verify:local` 只推进到 smart vitest 12/55,因非本轮 `src/lib/teamMemorySync.test.ts` 的 `crypto.subtle.digest` 环境错误失败,未作为完整绿色统一入口。记录 279 已取得 JSON 解析、定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check` 与 Team Workspace toolbar / action buttons 两个目标组件中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描证据;本轮未重复执行 `npm run verify:gui-smoke`,继续沿用记录 274 的非 i18n Rust 脏改编译 blocker。记录 278 已取得 JSON 解析、定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check` 与 `ImageWorkbenchMessagePreview.tsx` 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描证据;本轮未重复执行 `npm run verify:gui-smoke`,继续沿用记录 274 的非 i18n Rust 脏改编译 blocker。记录 277 已取得 JSON 解析、定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check` 与 `GeneralWorkbenchContextPanel.tsx` 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描证据;本轮未重复执行 `npm run verify:gui-smoke`,继续沿用记录 274 的非 i18n Rust 脏改编译 blocker。记录 276 已取得定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check` 与 `WorkspacePendingA2UIPanel.tsx` 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描证据;本轮未重复执行 `npm run verify:gui-smoke`,继续沿用记录 274 的非 i18n Rust 脏改编译 blocker。记录 275 已取得 JSON 解析、定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check` 与 `GeneralWorkbenchWorkflowPanel.tsx` 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描证据;本轮未重复执行 `npm run verify:gui-smoke`,因为记录 274 已确认 GUI smoke 会被非 i18n Rust 脏改编译错误阻断。记录 274 已取得 JSON 解析、定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check` 通过证据;本轮 `npm run verify:gui-smoke` 被非 i18n Rust 脏改编译错误阻断,具体为 `src-tauri/src/services/runtime_evidence_pack_service.rs:694` 调用 `build_agent_runtime_profile_spine_json` 少传 `&[AgentRun]` 参数,签名变更位于 `src-tauri/src/services/runtime_agent_profile_projection_service.rs:15`,因此记录 274 不宣称 GUI smoke 通过。记录 273 已取得定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check` 与单独 `npm run verify:gui-smoke` 通过证据;`npm run verify:local` 本轮已通过 `verify:app-version`、lint、typecheck、全量 smart vitest 55 批与 Rust tests,但统一入口内 GUI smoke 的 `smoke:browser-runtime` 阶段曾因 `launch_browser_session 请求失败: fetch failed` 失败,因此不把记录 273 记为新的完整绿色统一入口。随后单独补跑的 GUI smoke 已覆盖 workspace-ready / browser-runtime / site-adapters / agent-service-skill-entry / agent-runtime-tool-surface / agent-runtime-tool-surface-page / claw-chat-ready-streaming / knowledge-gui / design-canvas,i18n Patch metrics 仍为 `no-hit`。记录 272 已重新取得最新 `npm run verify:local` 完整绿色统一入口,通过 `verify:app-version`、lint、typecheck、全量 smart vitest 55 批与 GUI smoke;GUI smoke 覆盖 workspace-ready / browser-runtime / site-adapters / agent-service-skill-entry / agent-runtime-tool-surface / agent-runtime-tool-surface-page / claw-chat-ready-streaming / knowledge-gui / design-canvas,i18n Patch metrics 仍为 `no-hit`,因此记录 125 的 `smoke:claw-chat-ready-streaming` blocker 不再代表当前最新状态。记录 105 已取得 `npm run verify:local` 与 GUI smoke 通过证据(日志 `.lime/qc/verify-local-2026-05-11-i18n-managed-job-initial-values.log`),并覆盖此前 Agent streaming runtime blocker;记录 125 已复跑 `npm run verify:local`,并通过 `verify:app-version`、`lint`、`typecheck`、全量 smart vitest、`test:contracts` 与 GUI smoke 的 workspace-ready / browser-runtime / site-adapters / agent-service-skill-entry / agent-runtime-tool-surface / agent-runtime-tool-surface-page 阶段;但当时 `smoke:claw-chat-ready-streaming` 仍以 `长 turn 在首个流式增量前结束: status=completed` 失败,单独复跑同样失败,因此在记录 125 当时,记录 109 / 110 / 111 / 112 / 113 / 114 / 115 / 116 / 117 / 118 / 119 / 120 / 121 / 122 / 123 / 124 / 125 之后仍没有完整绿色统一入口;当时最新工作区涉及 CuratedTask 模板定义 / Agent Chat 输入能力区、Skills runtime enable 首发提示正文、Skills 执行对话框 chrome、Skill 脚手架启动弹窗 chrome、SceneApp / General Workbench 最近判断建议横幅 runtime suggestion / 执行摘要卡、Workspace 图片素材、Browser Runtime 工作台外壳、环境预设子面板、资料管理子面板、Debug 面板外壳、Debug 审计与附着 Chrome 子面板、Debug CDP 托管会话高级区、existing session hook feedback / presentation 与 Site Adapter 面板迁移,当时主要取得定向测试、eslint、i18n、JSON 解析、typecheck、contracts 与大部分 GUI smoke 证据;记录 117 已补齐 `workspace.browserRuntimeDebug.*` typed resource 并重新取得 `npm run typecheck` 通过证据,记录 118 初次 typecheck 阻断已由记录 119 的 `workspace.browserExistingSession.feedback.*` 资源补齐与 `npm run typecheck` 通过证据覆盖,记录 120 的 `npm run typecheck` 被并行 Browser Site Adapter `workspace.browserSiteAdapter.*` typed key / defaultValue 错误阻断,该阻断已由记录 121 的 `workspace.browserSiteAdapter.*` 资源补齐与 `npm run typecheck` 通过证据覆盖;记录 122 的 `npm run typecheck` 被并行 `src/features/browser-runtime/BrowserRuntimeDebugPanel.tsx(582,33)` 的 `TS2554` 阻断,未作为 General Workbench 本刀通过证据;记录 123 已由 Browser Runtime Debug 审计与附着 Chrome 子面板迁移重新取得 `npm run typecheck` 通过证据;记录 124 继续取得 Browser Runtime Debug CDP 托管会话高级区的 `npm run typecheck` 通过证据;记录 131 提到的 `ExistingSessionAttachPanelCopy.presentation` 缺失已由记录 132 的 Browser Runtime existing session presentation 迁移与 `npm run typecheck` 通过证据覆盖;记录 269、270、271 与 272 已连续补齐 `npm run verify:local` / GUI smoke 当前证据,后续继续叠加改动时仍需基于最新 diff 复跑统一入口。`ChannelLogTailPanel.test.tsx` 的 `Timeout` 类型不匹配已在记录 49 收口,当前不再作为 i18n PRD 的 typecheck blocker;记录 50 / 51 / 52 / 53 / 54 / 55 / 56 / 57 / 58 的 typecheck 因并行重验证负载或本轮未单独启动而未取得通过证据,后续收口时需复跑;记录 59 / 60 / 61 已重新取得 `npm run typecheck` 通过证据;记录 65 / 66 因隔壁 `verify:local` 长跑占用未重复启动 typecheck;记录 67 / 68 已重新取得 `npm run typecheck` 通过证据;记录 69 / 71 / 72 / 73 / 74 未单独启动 typecheck / verify:local;记录 75 已尝试 `npm run typecheck` 但当时被 AutomationJobDialog 并行脏改阻塞,且隔壁 `verify:local` 在 `smoke:claw-chat-ready-streaming` 超时失败;记录 76 已重新取得 `npm run typecheck` 通过证据;记录 77 已尝试 `npm run typecheck` 但被 Memory 来源链并行脏改阻塞;记录 78 / 79 / 80 / 81 / 82 / 83 / 84 / 85 / 86 / 87 / 88 / 89 / 90 / 91 已重新取得 `npm run typecheck` 通过证据;记录 92 / 93 因并行改动下的 `MemoryPage.test.tsx` / `WorkspaceRegisteredSkillsPanel.tsx` 类型 blocker 未取得本轮 typecheck 通过证据;记录 94 / 95 / 96 / 97 / 98 / 99 / 101 / 103 / 105 已重新取得 `npm run typecheck` 通过证据;记录 70 已重新取得 `npm run typecheck` 通过证据;记录 173 已重新取得 `npm run typecheck` 通过证据,并覆盖记录 172 提到的 ArtifactToolbar typed key / children blocker;记录 174 已重新取得 `npm run typecheck` 通过证据;记录 176 / 177 / 178 已重新取得 `npm run typecheck` 通过证据;记录 179 的 `npm run typecheck` 被并行 `src/components/AppSidebar.test.tsx` / `src/components/settings-v2/account/profile/index.tsx` 脏改阻断,未作为本条通过证据;记录 180 / 181 / 182 已重新取得 `npm run typecheck` 通过证据。 +4. 记录 298 已取得定向测试 / eslint / i18n 结构检测 / 先行 `npm run typecheck` / 单独 `npm run verify:gui-smoke` / EmptyState source fallback 扫描通过证据;GUI smoke 覆盖 workspace-ready / browser-runtime / site-adapters / agent-service-skill-entry / agent-runtime-tool-surface / agent-runtime-tool-surface-page / at-command-registry / claw-chat-ready-streaming / knowledge-gui / design-canvas,i18n Patch metrics 为 `no-hit`、退出候选 yes;随后 `npm run verify:local` 在 typecheck 阶段被非本轮 dirty `src/components/agent/chat/hooks/skillCommand.ts:1044` / `:1070` 的 `contentParts` 类型错误阻断,未作为完整统一入口绿色口径;本条未触碰命令边界。记录 297 已取得定向测试 / eslint / i18n 结构检测 / typecheck / 单独 `npm run verify:gui-smoke` / `npm run verify:local` / 目标场景管理弹窗 source fallback 扫描 / 定向 `git diff --check` 完整绿色证据;GUI smoke 覆盖 workspace-ready / browser-runtime / site-adapters / agent-service-skill-entry / agent-runtime-tool-surface / agent-runtime-tool-surface-page / at-command-registry / claw-chat-ready-streaming / knowledge-gui / design-canvas,i18n Patch metrics 为 `no-hit`、退出候选 yes;本条未触碰命令边界。记录 296 已取得图片轻卡本地化、真实图片 E2E、contracts 与 GUI smoke 通过证据,但未执行 `npm run verify:local`。记录 295 已取得定向测试 / eslint / i18n 结构检测 / typecheck / `npm run verify:gui-smoke` / 目标 Home copy 与 chrome fallback 扫描通过证据;GUI smoke 完整通过且 i18n Patch metrics 为 `no-hit`,但随后 `npm run verify:local` 在已通过 app-version / lint / typecheck 后被非本轮并行脏改 `src/components/agent/chat/hooks/agentStreamRuntimeHandler.ts:999` 的 `Unexpected "case"` 语法错误阻断,未作为完整统一入口绿色口径;本条未触碰命令边界。记录 294 已取得定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check` 与目标源文件 fallback 扫描通过证据;`npm run verify:gui-smoke` 本轮首次在 `smoke:agent-runtime-tool-surface-page` 等待 Runtime 能力摘要超时,单独复跑该 smoke 通过,随后完整重跑推进到 `smoke:claw-chat-ready-streaming` 后因 `等待恢复 turn 完成 超时` 失败,单独复跑同样失败,因此记录 294 不作为完整 GUI smoke 或 `verify:local` 绿色口径;本条未触碰命令边界。记录 293 已取得定向测试 / eslint / i18n 结构检测 / typecheck / `npm run verify:gui-smoke` / `git diff --check` 与 `MessageList.tsx` fallback 扫描通过证据;首次 GUI smoke 在 `smoke:claw-chat-ready-streaming` 因 `等待长 turn submit 超时` 与 DevBridge `fetch failed` 失败,随后健康检查恢复并完整重跑 `npm run verify:gui-smoke` 通过,覆盖 workspace-ready / browser-runtime / site-adapters / agent-service-skill-entry / agent-runtime-tool-surface / agent-runtime-tool-surface-page / at-command-registry / claw-chat-ready-streaming / knowledge-gui / design-canvas,i18n Patch metrics 为 `no-hit`;本条未触碰命令边界且未重复执行 `npm run verify:local`,不作为新的完整绿色统一入口。记录 292 已取得定向测试 / eslint / i18n 结构检测 / typecheck / `npm run test:contracts` / Rust 定向与全量 / `npm run verify:gui-smoke` / `npm run verify:local` / `git diff --check` 完整绿色证据;GUI smoke 覆盖 workspace-ready / browser-runtime / site-adapters / agent-service-skill-entry / agent-runtime-tool-surface / agent-runtime-tool-surface-page / at-command-registry / claw-chat-ready-streaming / knowledge-gui / design-canvas,i18n Patch metrics 为 `no-hit`,并覆盖记录 281 以来的 `src/lib/teamMemorySync.test.ts` `crypto.subtle.digest` 统一入口 blocker 旧口径。记录 291 已取得 JSON 解析、定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check`、5 locale 关键 key 存在性检查与 Team Workspace live runtime projector / subscriptions 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描证据;本轮未重复执行 `npm run verify:local` / GUI smoke,继续沿用记录 281 的非本轮 `src/lib/teamMemorySync.test.ts` `crypto.subtle.digest` undefined 统一入口 blocker 口径,未作为完整绿色统一入口。记录 290 已取得 JSON 解析、定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check`、5 locale 关键 key 存在性检查与 Team Workspace activity preview selector / sync hook 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描证据;本轮未重复执行 `npm run verify:local` / GUI smoke,继续沿用记录 281 的非本轮 `src/lib/teamMemorySync.test.ts` `crypto.subtle.digest` undefined 统一入口 blocker 口径,未作为完整绿色统一入口。记录 289 已取得 JSON 解析、定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check`、5 locale 关键 key 存在性检查与 Team Workspace board chrome selector 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描证据;本轮未重复执行 `npm run verify:local` / GUI smoke,继续沿用记录 281 的非本轮 `src/lib/teamMemorySync.test.ts` `crypto.subtle.digest` undefined 统一入口 blocker 口径,未作为完整绿色统一入口。记录 288 已取得 JSON 解析、定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check`、5 locale 关键 key 存在性检查与 Team Workspace selected session detail selector 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描证据;本轮未重复执行 `npm run verify:local` / GUI smoke,继续沿用记录 281 的非本轮 `src/lib/teamMemorySync.test.ts` `crypto.subtle.digest` undefined 统一入口 blocker 口径,未作为完整绿色统一入口。记录 287 已取得 JSON 解析、定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check`、5 locale 关键 key 存在性检查与 Team Workspace Dock current progress / concurrency chrome 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描证据;本轮未重复执行 `npm run verify:local` / GUI smoke,继续沿用记录 281 的非本轮 `src/lib/teamMemorySync.test.ts` `crypto.subtle.digest` undefined 统一入口 blocker 口径,未作为完整绿色统一入口。记录 286 已取得 JSON 解析、定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check`、5 locale 关键 key 存在性检查与 Team Workspace CanvasStage chrome 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描证据;本轮未重复执行 `npm run verify:local` / GUI smoke,继续沿用记录 281 的非本轮 `src/lib/teamMemorySync.test.ts` `crypto.subtle.digest` undefined 统一入口 blocker 口径,未作为完整绿色统一入口。记录 285 已取得 JSON 解析、定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check`、5 locale 关键 key 存在性检查与 Team Workspace canvas lane selector / runtime hook 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描证据;本轮未重复执行 `npm run verify:local` / GUI smoke,继续沿用记录 281 的非本轮 `src/lib/teamMemorySync.test.ts` `crypto.subtle.digest` undefined 统一入口 blocker 口径,未作为完整绿色统一入口。记录 284 已取得 JSON 解析、定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check`、5 locale 关键 key 存在性检查与 Team Workspace board presentation / overview chrome 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描证据;本轮未重复执行 `npm run verify:local` / GUI smoke,继续沿用记录 281 的非本轮 `src/lib/teamMemorySync.test.ts` `crypto.subtle.digest` undefined 统一入口 blocker 口径,未作为完整绿色统一入口。记录 283 已取得 JSON 解析、定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check`、5 locale 关键 key 存在性检查与 Team Workspace selected session inline header / collaboration / activity / detail 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描证据;本轮未重复执行 `npm run verify:local` / GUI smoke,继续沿用记录 281 的非本轮 `src/lib/teamMemorySync.test.ts` `crypto.subtle.digest` undefined 统一入口 blocker 口径,未作为完整绿色统一入口。记录 282 已取得 JSON 解析、定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check`、5 locale 关键 key 存在性检查与 Team Workspace formation selector / board formation hook 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描证据,并确认 Dock / SummaryPanel 新增 `t()` 调用无第二参数 fallback、formation 相关目标硬编码清零;本轮未重复执行 `npm run verify:local` / GUI smoke,继续沿用记录 281 的非本轮 `src/lib/teamMemorySync.test.ts` `crypto.subtle.digest` undefined 统一入口 blocker 口径,未作为完整绿色统一入口。记录 281 已取得 JSON 解析、定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check`、5 locale 关键 key 存在性检查与 Team Workspace operation selectors / panel / operation state hook 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描证据;本轮额外重跑 `npm run verify:local`,已通过 `verify:app-version`、`lint`、`typecheck` 与 smart vitest 1-10 批,在 11/55 批同样因非本轮 `src/lib/teamMemorySync.test.ts` 的 `crypto.subtle.digest` undefined 失败,未作为完整绿色统一入口。记录 280 已取得 JSON 解析、定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check` 与 Team Workspace session control hook 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描证据;本轮 `npm run verify:local` 只推进到 smart vitest 12/55,因非本轮 `src/lib/teamMemorySync.test.ts` 的 `crypto.subtle.digest` 环境错误失败,未作为完整绿色统一入口。记录 279 已取得 JSON 解析、定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check` 与 Team Workspace toolbar / action buttons 两个目标组件中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描证据;本轮未重复执行 `npm run verify:gui-smoke`,继续沿用记录 274 的非 i18n Rust 脏改编译 blocker。记录 278 已取得 JSON 解析、定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check` 与 `ImageWorkbenchMessagePreview.tsx` 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描证据;本轮未重复执行 `npm run verify:gui-smoke`,继续沿用记录 274 的非 i18n Rust 脏改编译 blocker。记录 277 已取得 JSON 解析、定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check` 与 `GeneralWorkbenchContextPanel.tsx` 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描证据;本轮未重复执行 `npm run verify:gui-smoke`,继续沿用记录 274 的非 i18n Rust 脏改编译 blocker。记录 276 已取得定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check` 与 `WorkspacePendingA2UIPanel.tsx` 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描证据;本轮未重复执行 `npm run verify:gui-smoke`,继续沿用记录 274 的非 i18n Rust 脏改编译 blocker。记录 275 已取得 JSON 解析、定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check` 与 `GeneralWorkbenchWorkflowPanel.tsx` 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描证据;本轮未重复执行 `npm run verify:gui-smoke`,因为记录 274 已确认 GUI smoke 会被非 i18n Rust 脏改编译错误阻断。记录 274 已取得 JSON 解析、定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check` 通过证据;本轮 `npm run verify:gui-smoke` 被非 i18n Rust 脏改编译错误阻断,具体为 `src-tauri/src/services/runtime_evidence_pack_service.rs:694` 调用 `build_agent_runtime_profile_spine_json` 少传 `&[AgentRun]` 参数,签名变更位于 `src-tauri/src/services/runtime_agent_profile_projection_service.rs:15`,因此记录 274 不宣称 GUI smoke 通过。记录 273 已取得定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check` 与单独 `npm run verify:gui-smoke` 通过证据;`npm run verify:local` 本轮已通过 `verify:app-version`、lint、typecheck、全量 smart vitest 55 批与 Rust tests,但统一入口内 GUI smoke 的 `smoke:browser-runtime` 阶段曾因 `launch_browser_session 请求失败: fetch failed` 失败,因此不把记录 273 记为新的完整绿色统一入口。随后单独补跑的 GUI smoke 已覆盖 workspace-ready / browser-runtime / site-adapters / agent-service-skill-entry / agent-runtime-tool-surface / agent-runtime-tool-surface-page / claw-chat-ready-streaming / knowledge-gui / design-canvas,i18n Patch metrics 仍为 `no-hit`。记录 272 已重新取得最新 `npm run verify:local` 完整绿色统一入口,通过 `verify:app-version`、lint、typecheck、全量 smart vitest 55 批与 GUI smoke;GUI smoke 覆盖 workspace-ready / browser-runtime / site-adapters / agent-service-skill-entry / agent-runtime-tool-surface / agent-runtime-tool-surface-page / claw-chat-ready-streaming / knowledge-gui / design-canvas,i18n Patch metrics 仍为 `no-hit`,因此记录 125 的 `smoke:claw-chat-ready-streaming` blocker 不再代表当前最新状态。记录 105 已取得 `npm run verify:local` 与 GUI smoke 通过证据(日志 `.lime/qc/verify-local-2026-05-11-i18n-managed-job-initial-values.log`),并覆盖此前 Agent streaming runtime blocker;记录 125 已复跑 `npm run verify:local`,并通过 `verify:app-version`、`lint`、`typecheck`、全量 smart vitest、`test:contracts` 与 GUI smoke 的 workspace-ready / browser-runtime / site-adapters / agent-service-skill-entry / agent-runtime-tool-surface / agent-runtime-tool-surface-page 阶段;但当时 `smoke:claw-chat-ready-streaming` 仍以 `长 turn 在首个流式增量前结束: status=completed` 失败,单独复跑同样失败,因此在记录 125 当时,记录 109 / 110 / 111 / 112 / 113 / 114 / 115 / 116 / 117 / 118 / 119 / 120 / 121 / 122 / 123 / 124 / 125 之后仍没有完整绿色统一入口;当时最新工作区涉及 CuratedTask 模板定义 / Agent Chat 输入能力区、Skills runtime enable 首发提示正文、Skills 执行对话框 chrome、Skill 脚手架启动弹窗 chrome、SceneApp / General Workbench 最近判断建议横幅 runtime suggestion / 执行摘要卡、Workspace 图片素材、Browser Runtime 工作台外壳、环境预设子面板、资料管理子面板、Debug 面板外壳、Debug 审计与附着 Chrome 子面板、Debug CDP 托管会话高级区、existing session hook feedback / presentation 与 Site Adapter 面板迁移,当时主要取得定向测试、eslint、i18n、JSON 解析、typecheck、contracts 与大部分 GUI smoke 证据;记录 117 已补齐 `workspace.browserRuntimeDebug.*` typed resource 并重新取得 `npm run typecheck` 通过证据,记录 118 初次 typecheck 阻断已由记录 119 的 `workspace.browserExistingSession.feedback.*` 资源补齐与 `npm run typecheck` 通过证据覆盖,记录 120 的 `npm run typecheck` 被并行 Browser Site Adapter `workspace.browserSiteAdapter.*` typed key / defaultValue 错误阻断,该阻断已由记录 121 的 `workspace.browserSiteAdapter.*` 资源补齐与 `npm run typecheck` 通过证据覆盖;记录 122 的 `npm run typecheck` 被并行 `src/features/browser-runtime/BrowserRuntimeDebugPanel.tsx(582,33)` 的 `TS2554` 阻断,未作为 General Workbench 本刀通过证据;记录 123 已由 Browser Runtime Debug 审计与附着 Chrome 子面板迁移重新取得 `npm run typecheck` 通过证据;记录 124 继续取得 Browser Runtime Debug CDP 托管会话高级区的 `npm run typecheck` 通过证据;记录 131 提到的 `ExistingSessionAttachPanelCopy.presentation` 缺失已由记录 132 的 Browser Runtime existing session presentation 迁移与 `npm run typecheck` 通过证据覆盖;记录 269、270、271 与 272 已连续补齐 `npm run verify:local` / GUI smoke 当前证据,后续继续叠加改动时仍需基于最新 diff 复跑统一入口。`ChannelLogTailPanel.test.tsx` 的 `Timeout` 类型不匹配已在记录 49 收口,当前不再作为 i18n PRD 的 typecheck blocker;记录 50 / 51 / 52 / 53 / 54 / 55 / 56 / 57 / 58 的 typecheck 因并行重验证负载或本轮未单独启动而未取得通过证据,后续收口时需复跑;记录 59 / 60 / 61 已重新取得 `npm run typecheck` 通过证据;记录 65 / 66 因隔壁 `verify:local` 长跑占用未重复启动 typecheck;记录 67 / 68 已重新取得 `npm run typecheck` 通过证据;记录 69 / 71 / 72 / 73 / 74 未单独启动 typecheck / verify:local;记录 75 已尝试 `npm run typecheck` 但当时被 AutomationJobDialog 并行脏改阻塞,且隔壁 `verify:local` 在 `smoke:claw-chat-ready-streaming` 超时失败;记录 76 已重新取得 `npm run typecheck` 通过证据;记录 77 已尝试 `npm run typecheck` 但被 Memory 来源链并行脏改阻塞;记录 78 / 79 / 80 / 81 / 82 / 83 / 84 / 85 / 86 / 87 / 88 / 89 / 90 / 91 已重新取得 `npm run typecheck` 通过证据;记录 92 / 93 因并行改动下的 `MemoryPage.test.tsx` / `WorkspaceRegisteredSkillsPanel.tsx` 类型 blocker 未取得本轮 typecheck 通过证据;记录 94 / 95 / 96 / 97 / 98 / 99 / 101 / 103 / 105 已重新取得 `npm run typecheck` 通过证据;记录 70 已重新取得 `npm run typecheck` 通过证据;记录 173 已重新取得 `npm run typecheck` 通过证据,并覆盖记录 172 提到的 ArtifactToolbar typed key / children blocker;记录 174 已重新取得 `npm run typecheck` 通过证据;记录 176 / 177 / 178 已重新取得 `npm run typecheck` 通过证据;记录 179 的 `npm run typecheck` 被并行 `src/components/AppSidebar.test.tsx` / `src/components/settings-v2/account/profile/index.tsx` 脏改阻断,未作为本条通过证据;记录 180 / 181 / 182 已重新取得 `npm run typecheck` 通过证据。 + +296. 2026-05-13 Agent Chat 图片轻卡本地化与真实图片回流复测:在 `src/i18n/resources/{zh-CN,zh-TW,ja-JP,ko-KR}/agent.json` 将 `agentChat.imageWorkbenchPreview.tool.generation/editing/redraw` 从英文展示值补齐为对应 locale 文案,保留 `en-US` 英文;同步更新 `src/components/agent/chat/components/MessageList.test.tsx` 与 `.lime/e2e/image-chat-natural/run.mjs`,让中文 GUI 不再在图片轻卡暴露 `Image Generation` 英文工具名,同时 E2E 断言接受五语言本地化工具条。复测证明 `@Nanobanana Pro 生成一张城市春日照片` 仍沿 `agent_runtime_submit_turn -> harness.image_skill_launch -> Skill(image_generate) -> lime_create_image_generation_task -> image_generate task artifact + worker` 主链,同一 assistant 回复内显示三行自然文案、单张图片轻卡与最终图片,未泄露任务 ID、任务文件、内部工具名、旧 Image Workbench 文案、参考站名称或长 prompt。验证:`npm exec vitest run "src/components/agent/chat/utils/imageWorkbenchPresentation.test.ts" "src/components/agent/chat/workspace/imageSkillLaunch.test.ts" "src/components/agent/chat/hooks/agentStreamSubmissionLifecycle.test.ts" "src/components/agent/chat/hooks/agentStreamSubmitDraft.test.ts" "src/components/agent/chat/hooks/agentStreamUserInputSendPreparation.test.ts" "src/components/agent/chat/workspace/useWorkspaceImageTaskPreviewRuntime.test.tsx" "src/components/agent/chat/components/ImageWorkbenchMessagePreview.test.tsx" "src/components/agent/chat/components/MessageList.test.tsx"` 通过(8 文件 / 178 用例);`cargo test --manifest-path "src-tauri/Cargo.toml" image_skill_launch --lib` 通过(8 用例);`npm run test:contracts` 通过;`LIME_IMAGE_E2E_WAIT_FOR_COMPLETION=1 node ".lime/e2e/image-chat-natural/run.mjs"` 通过,`previewCardCount=1`、`previewImageCount=1`、`completionWait=image-returned`、`consoleErrors=[]`,证据见 `.lime/e2e/image-chat-natural/summary.json` 与 `.lime/e2e/image-chat-natural/02-final-chat.png`;`npm run verify:gui-smoke -- --reuse-running` 通过,覆盖 workspace-ready / browser-runtime / site-adapters / agent-service-skill-entry / runtime tool surface / at-command-registry / claw-chat-ready-streaming / knowledge-gui / design-canvas,i18n Patch metrics 仍为 `no-hit`。本条只收口图片轻卡 presentation 本地化和图片生成 current 主链复测,不新增 Tauri 命令、Bridge、mock 或 legacy CLI 入口;随后补跑 `npm run verify:local` 并完整通过,覆盖 `verify:app-version`、`lint`、`typecheck`、smart Vitest 55/55、全量 Rust tests 与 GUI smoke,统一入口当前为绿色;`npm run governance:legacy-report` 通过,边界违规 0,并确认 `plainVisualBriefConfirmation.ts` 属已删除 `dead-candidate` 且无实际引用。 + +297. 2026-05-13 Agent Chat HomeSceneSkillManagerDialog 场景管理弹窗 source fallback 收口续测:`src/components/agent/chat/home/HomeSceneSkillManagerDialog.tsx` 接入 `useTranslation("agent")`,新增场景管理 copy 聚合,把“管理做法”入口打开的场景管理弹窗标题 / 描述 / 关闭、无云端会话与同步登录错误、加载提示、列表 aria、场景 / Skill 类型、上移 / 下移 aria、显示开关、创建表单 label / placeholder / 必填错误、返回 / 加入列表 / 恢复默认 / 取消 / 完成 / 同步中,以及自定义场景默认模板标题 / 空摘要兜底,从源码中文 literal 改为 `agentChat.home.sceneManager.*` key-based 读取;运行时 SkillCatalog 标题 / 摘要、自定义场景用户输入、底层 API 错误 message 与 slug 规则继续作为 runtime / API / 用户数据边界保留。同步 5 locale `agent.json` 新增 `agentChat.home.sceneManager.*` 35 个正式资源,`src/i18n/__tests__/loadNamespace.test.ts` / `types.test.ts` 覆盖 title / action / error / row aria / field placeholder 主键;`src/components/agent/chat/home/HomeSceneSkillManagerDialog.test.tsx` 改用真实 i18n 初始化,并新增 en-US 回归验证 Scene management、Add scene、Visible、Done、Add custom scene、Scene name、Example: daily account review 与 Add to list 均来自资源。验证:`npm test -- "src/components/agent/chat/home/HomeSceneSkillManagerDialog.test.tsx" "src/components/agent/chat/home/HomeStartSurface.test.tsx" "src/components/agent/chat/components/EmptyState.test.tsx" "src/i18n/__tests__/loadNamespace.test.ts" "src/i18n/__tests__/types.test.ts"` 通过,覆盖 5 个文件、71 个用例;`npx eslint --max-warnings 0 "src/components/agent/chat/home/HomeSceneSkillManagerDialog.tsx" "src/components/agent/chat/home/HomeSceneSkillManagerDialog.test.tsx" "src/i18n/__tests__/loadNamespace.test.ts" "src/i18n/__tests__/types.test.ts"`、`npm run detect-translations -- --verbose`(5 locale、6 namespace、6426 source keys)、`npm run typecheck`、场景管理弹窗 / Home 目标 fallback 扫描、单独 `npm run verify:gui-smoke`、`npm run verify:local` 与定向 `git diff --check` 均通过;`verify:local` 覆盖 app-version、lint、typecheck、55 批 smart vitest、Rust tests 与 GUI smoke,GUI smoke 覆盖 workspace-ready / browser-runtime / site-adapters / agent-service-skill-entry / agent-runtime-tool-surface / agent-runtime-tool-surface-page / at-command-registry / claw-chat-ready-streaming / knowledge-gui / design-canvas,i18n Patch metrics 为 `no-hit` 且退出候选 yes。本条属于 Agent Chat Home 场景管理弹窗普通前端 i18n chrome 收口,不新增或修改 Tauri 命令、Bridge、DevBridge mock、`agentCommandCatalog`、`mockPriorityCommands` 或 `defaultMocks`;后续仍需继续 `EmptyState.tsx` hero slogan / quick actions / toast / 文件拖拽 / curated task review hint,以及 Agent Chat 更深路径、Workspace / Browser Runtime 长尾、P3 自动化治理和 legacy Patch 删除门禁。 + +298. 2026-05-13 Agent Chat EmptyState 首页首屏 / 快速启动 / 输入区 source fallback 收口续测:`src/components/agent/chat/home/homeSurfaceCopy.ts` 继续作为 Home copy 事实源,新增 hero、toast、quickActions、路径引用空发送 fallback 与 curated task review hint copy;`src/components/agent/chat/components/EmptyState.tsx` 删除内建 `THEME_WORKBENCH_COPY` 和快速启动 preset 中文 literal,把首页 eyebrow / slogan / description / supporting description、非通用首页快速启动标题 / 描述 / preset label / prompt、推荐卡 badge、图片读取失败 / 粘贴 / 添加 / 系统路径拖拽失败 toast、仅带路径引用发送时的兜底 prompt,以及 CuratedTask 最近判断模板切换提示统一迁移到 `agentChat.home.*` key-based 资源。同步 5 locale `agent.json` 新增 28 个正式资源,`src/i18n/__tests__/loadNamespace.test.ts` / `types.test.ts` 覆盖 hero、toast、quickActions、path fallback 与 review hint 主键;`src/components/agent/chat/components/EmptyState.test.tsx` 增加真实 en-US 回归,验证首页 hero、非通用快速启动面板和仅路径引用发送 fallback 均来自资源。Contextual recommendation 的 shortLabel / fullPrompt、SkillCatalog / SceneApp 标题摘要、用户文件名、用户路径和输入内容继续作为 runtime / API / 用户数据边界保留;`EmptyState.tsx` 目标扫描剩余中文仅为接口注释与 `console.error` 内部诊断日志,不作为用户可见 presentation copy。验证:`npm test -- "src/components/agent/chat/components/EmptyState.test.tsx" "src/components/agent/chat/home/HomeStartSurface.test.tsx" "src/components/agent/chat/home/HomeSceneSkillManagerDialog.test.tsx" "src/i18n/__tests__/loadNamespace.test.ts" "src/i18n/__tests__/types.test.ts"` 通过,覆盖 5 个文件、73 个用例;`npx eslint --max-warnings 0 "src/components/agent/chat/components/EmptyState.tsx" "src/components/agent/chat/components/EmptyState.test.tsx" "src/components/agent/chat/home/homeSurfaceCopy.ts" "src/i18n/__tests__/loadNamespace.test.ts" "src/i18n/__tests__/types.test.ts"`、`npm run detect-translations -- --verbose`(5 locale、6 namespace、6454 source keys)、先行 `npm run typecheck`、`EmptyState.tsx` 目标 fallback 扫描与单独 `npm run verify:gui-smoke` 均通过;GUI smoke 覆盖 workspace-ready / browser-runtime / site-adapters / agent-service-skill-entry / agent-runtime-tool-surface / agent-runtime-tool-surface-page / at-command-registry / claw-chat-ready-streaming / knowledge-gui / design-canvas,i18n Patch metrics 为 `no-hit`、退出候选 yes。随后尝试 `npm run verify:local`,已通过 app-version / lint,但在 typecheck 阶段被非本轮 dirty `src/components/agent/chat/hooks/skillCommand.ts:1044` / `:1070` 的 `contentParts` 类型错误阻断;该文件属于既有 Agent hook 脏区,本条不接手、不回滚,因此记录 298 不宣称完整 `verify:local` 绿色。本条属于 Agent Chat Home / EmptyState 普通前端 i18n chrome 收口,不新增或修改 Tauri 命令、Bridge、DevBridge mock、`agentCommandCatalog`、`mockPriorityCommands` 或 `defaultMocks`;后续仍需继续 Agent Chat 更深 runtime/projection/helper 路径、Workspace / Browser Runtime 长尾、P3 自动化治理和 legacy Patch 删除门禁。 + +299. 2026-05-13 Agent Chat EmptyStateComposerPanel 输入区 chrome source fallback 收口续测:`src/components/agent/chat/home/homeSurfaceCopy.ts` 在 Home copy 事实源中新增 `HomeSurfaceComposerCopy` 与 `agentChat.home.composer.*` 12 个 key,把首页输入区直接子组件的引导帮助默认 label / close aria 与 title、高级设置展开 / 收起 aria 和按钮 label、折叠态当前模型 label / title、左侧文件管理器打开 / 关闭 aria 和 title、创作模式分组 label、通用任务上下文 badge 从 `EmptyStateComposerPanel.tsx` 内建中文 literal 改为 key-based copy;`src/components/agent/chat/components/EmptyStateComposerPanel.tsx` 改为必传 `copy` prop 并移除 `guideHelpLabel = "Lime 引导帮助"` source fallback,`src/components/agent/chat/components/EmptyState.tsx` 通过 `homeSurfaceCopy.composer` 注入生产路径。同步 5 locale `agent.json` 新增 12 个正式资源,`src/i18n/__tests__/loadNamespace.test.ts` / `types.test.ts` 覆盖 guideHelp / advancedSettings / currentModel / fileManager / creationMode / generalContext 主键;`src/components/agent/chat/components/EmptyStateComposerPanel.test.tsx` 改用真实 zh-CN / en-US resource 构建 copy,并新增 en-US 回归验证 Close guide help、Expand / Collapse advanced settings、Current model、Open file manager sidebar 与 General task context 来自资源。Creation mode option 名称、InputbarCore 按钮 / path chip / 图片预览 / 语音输入、CharacterMention / SkillSelector / TeamSelector 等更深子组件 copy 仍属于后续输入区深层路径,不在本刀范围;provider / model 值、SkillCatalog / SceneApp 标题摘要、用户路径 / 文件名与输入内容继续作为 runtime / API / 用户数据边界保留。验证:`npm test -- "src/components/agent/chat/components/EmptyStateComposerPanel.test.tsx" "src/components/agent/chat/components/EmptyState.test.tsx" "src/components/agent/chat/home/HomeStartSurface.test.tsx" "src/i18n/__tests__/loadNamespace.test.ts" "src/i18n/__tests__/types.test.ts"` 通过,覆盖 5 个文件、103 个用例;`npx eslint --max-warnings 0 "src/components/agent/chat/components/EmptyStateComposerPanel.tsx" "src/components/agent/chat/components/EmptyStateComposerPanel.test.tsx" "src/components/agent/chat/components/EmptyState.tsx" "src/components/agent/chat/home/homeSurfaceCopy.ts" "src/i18n/__tests__/loadNamespace.test.ts" "src/i18n/__tests__/types.test.ts"`、`npm run detect-translations -- --verbose`(5 locale、6 namespace、6466 source keys)、`npm run typecheck`、`EmptyStateComposerPanel.tsx` 中文 / `defaultValue` / `t(key, "中文")` fallback 扫描与单独 `npm run verify:gui-smoke` 均通过;GUI smoke 覆盖 workspace-ready / browser-runtime / site-adapters / agent-service-skill-entry / agent-runtime-tool-surface / agent-runtime-tool-surface-page / at-command-registry / claw-chat-ready-streaming / knowledge-gui / design-canvas,i18n Patch metrics 为 `no-hit`、退出候选 yes。随后重跑 `npm run verify:local` 已完整通过,覆盖 `verify:app-version`、`lint`、`typecheck`、smart Vitest 55/55、全量 Rust tests 与 GUI smoke,因此记录 298 中 `skillCommand.ts` 的非本轮 typecheck blocker 已不再代表当前最新统一入口状态;本条未接手或回滚既有 Agent hook / Rust 脏区。本条属于 Agent Chat Home 输入区直接 chrome i18n 收口,不新增或修改 Tauri 命令、Bridge、DevBridge mock、`agentCommandCatalog`、`mockPriorityCommands` 或 `defaultMocks`;后续仍需继续 Agent Chat 更深 Inputbar / runtime / projection / helper 路径、Workspace / Browser Runtime 长尾、P3 自动化治理和 legacy Patch 删除门禁。 + +300. 2026-05-13 Agent Chat InputbarCore 输入壳 chrome source fallback 收口续测:`src/components/agent/chat/components/Inputbar/components/inputbarCoreCopy.ts` 新增 `InputbarCoreCopy` / `InputbarCoreCopyKey` / `buildInputbarCoreCopy()` 作为输入壳文案事实源,`src/components/agent/chat/components/Inputbar/components/InputbarCore.tsx` 改为必传 `copy`,把语音输入状态(录音中 / 识别中 / 润饰中 / 实时识别 / 停止录音 title / 未启用提示)、全屏 / 默认 placeholder、图片预览 alt / 删除 / 添加、路径引用容器 label / 本机文件与资料夹标签 / 设为资料动作 / 删除 aria、起手建议 Tab 提示、输入框展开 / 收起,以及稍后处理 / 停止 / 发送 action 从组件内中文 literal / fallback 改为 `agentChat.inputbar.core.*` key-based 读取;`src/components/agent/chat/components/Inputbar/index.tsx` 通过 `useTranslation("agent")` 构建 `inputbarCopy`,经 `src/components/agent/chat/components/Inputbar/components/InputbarComposerSection.tsx` 透传给 `InputbarCore.tsx`,`src/components/agent/chat/components/EmptyState.tsx` 同步构建 `inputbarCoreCopy` 并经 `src/components/agent/chat/components/EmptyStateComposerPanel.tsx` 注入首页输入壳,保证常规对话输入链与首页 `EmptyStateComposerPanel` 共用同一 current i18n 入口。同步 5 locale `agent.json` 新增 `agentChat.inputbar.core.*` 27 个正式资源,`src/i18n/__tests__/loadNamespace.test.ts` / `types.test.ts` 补充 dictation / placeholder / image / path / suggestion / textarea / action 主键与插值覆盖;`src/components/agent/chat/components/Inputbar/components/InputbarCore.test.tsx`、`src/components/agent/chat/components/EmptyStateComposerPanel.test.tsx`、`src/components/agent/chat/components/EmptyState.test.tsx` 改用真实资源回归,补齐 en-US 下 Send / preview / path reference / placeholder 等断言。`InputbarComposerSection.tsx` 里当前模型 title、高级设置、文件管理器与 3 条 workspace placeholder 长尾仍保留到后续刀,本条只收 `InputbarCore.tsx` 直接 chrome,不把 Inputbar 全链误记为已完成;provider / model 值、用户路径 / 文件名、用户输入、dictation duration 格式化与 runtime source data 继续作为 runtime / 用户数据边界保留。验证:`npm test -- "src/components/agent/chat/components/Inputbar/components/InputbarCore.test.tsx" "src/components/agent/chat/components/Inputbar/index.test.tsx" "src/components/agent/chat/components/EmptyStateComposerPanel.test.tsx" "src/components/agent/chat/components/EmptyState.test.tsx" "src/i18n/__tests__/loadNamespace.test.ts" "src/i18n/__tests__/types.test.ts"` 通过,覆盖 6 个文件、174 个用例;`npx eslint --max-warnings 0 "src/components/agent/chat/components/Inputbar/components/inputbarCoreCopy.ts" "src/components/agent/chat/components/Inputbar/components/InputbarCore.tsx" "src/components/agent/chat/components/Inputbar/components/InputbarCore.test.tsx" "src/components/agent/chat/components/Inputbar/components/InputbarComposerSection.tsx" "src/components/agent/chat/components/Inputbar/index.tsx" "src/components/agent/chat/components/EmptyState.tsx" "src/components/agent/chat/components/EmptyStateComposerPanel.tsx" "src/components/agent/chat/components/EmptyStateComposerPanel.test.tsx" "src/components/agent/chat/components/EmptyState.test.tsx" "src/i18n/__tests__/loadNamespace.test.ts" "src/i18n/__tests__/types.test.ts"`、`npm run detect-translations -- --verbose`(5 locale、6 namespace、6493 source keys)、`npm run typecheck`、目标文件中文 / `defaultValue` / `t(key, "中文")` fallback 扫描与定向 `git diff --check` 均通过。GUI 验证方面,首次冷启动 `npm run verify:gui-smoke` 因隔离 Cargo target `/var/folders/87/s6cpr7hd1_v43cs833x4s_900000gn/T/lime-gui-smoke-target` 在 `openssl-sys` vendor build 的 `install_dev` 阶段命中 `No space left on device`,未作为本条通过证据;随后基于当前仓库已运行的 `npm run tauri:dev:headless` 环境补跑 `npm run verify:gui-smoke -- --reuse-running` 通过,覆盖 workspace-ready / browser-runtime / site-adapters / agent-service-skill-entry / agent-runtime-tool-surface / agent-runtime-tool-surface-page / at-command-registry / claw-chat-ready-streaming / knowledge-gui / design-canvas,i18n Patch metrics 仍为 `no-hit`、退出候选 yes。统一入口方面,进一步尝试 `npm run verify:local`,已通过 `verify:app-version` 后在 `npm run lint` 被并行 dirty `src/components/agent/chat/hooks/useAsterAgentChat.ts` 的未使用导入 `DEFAULT_AGENT_MODEL` / `DEFAULT_AGENT_PROVIDER` 阻断;该文件不属于本条 InputbarCore i18n 收口范围,本条不接手、不回滚,因此记录 300 不宣称新的完整绿色统一入口。本条属于 Agent Chat Home / 常规聊天共用输入壳普通前端 i18n chrome 收口,不新增或修改 Tauri 命令、Bridge、DevBridge mock、`agentCommandCatalog`、`mockPriorityCommands` 或 `defaultMocks`;后续仍需继续 Inputbar 邻接层 / runtime / projection / helper 路径、Workspace / Browser Runtime 长尾、P3 自动化治理和 legacy Patch 删除门禁。 + +301. 2026-05-13 Agent Chat InputbarComposerSection 输入区邻接 chrome source fallback 收口续测:`src/components/agent/chat/components/Inputbar/components/inputbarComposerSectionCopy.ts` 新增 `InputbarComposerSectionCopy` / `InputbarComposerSectionCopyKey` / `buildInputbarComposerSectionCopy()` 作为常规输入区邻接层文案事实源,`src/components/agent/chat/components/Inputbar/components/InputbarComposerSection.tsx` 改为必传 `copy`,把当前模型 label / title、高级设置展开 / 收起 aria 与按钮 label、左侧文件管理器打开 / 关闭 aria,以及 workspace 输入 placeholder 三态从组件内中文 literal / fallback 改为 `agentChat.inputbar.composer.*` key-based 读取;`src/components/agent/chat/components/Inputbar/index.tsx` 通过 `useTranslation("agent")` 构建并注入 composer copy,避免继续复用首页专属 `agentChat.home.composer.*`,使常规对话输入区与首页输入区各自拥有清晰的 current 资源边界。同步 5 locale `agent.json` 新增 `agentChat.inputbar.composer.*` 10 个正式资源,`src/i18n/__tests__/loadNamespace.test.ts` / `types.test.ts` 补充 advancedSettings / currentModel / fileManager / workspacePlaceholder 主键与 `currentModel.title` 插值覆盖;`src/components/agent/chat/components/Inputbar/index.test.tsx` 改为真实 locale 回归,新增 en-US 断言覆盖 Model、Advanced settings、Current model: OpenAI / gpt-4.1、Expand / Collapse advanced settings、Open file manager sidebar,以及 default / task-center / waiting 三态 workspace placeholder。provider / model 值、workspace mode、用户输入、文件管理器展开状态与 `InputbarCore` 更深输入壳数据继续作为 runtime / UI 状态边界保留;TeamSelectorPanel、InputbarModelExtra、AccessMode、ExecutionStrategy、KnowledgeControl、runtime / projection / helper 等更深子组件长尾仍属于后续收口,不把本条误记为 Inputbar 全链完成。验证:目标 fallback 扫描 `rg -n "当前模型:|模型\\b|收起高级设置|展开高级设置|高级设置|关闭左侧文件管理器|打开左侧文件管理器|说说你的选择|继续补充这轮生成|试着输入任何指令|defaultValue|t\\([^\\n]*,\\s*[\\\"']" "src/components/agent/chat/components/Inputbar/components/InputbarComposerSection.tsx"` 无命中;`npm test -- "src/components/agent/chat/components/Inputbar/index.test.tsx" "src/components/agent/chat/components/EmptyStateComposerPanel.test.tsx" "src/i18n/__tests__/loadNamespace.test.ts" "src/i18n/__tests__/types.test.ts"` 通过,覆盖 4 个文件、102 个用例;`npx eslint --max-warnings 0 "src/components/agent/chat/components/Inputbar/components/inputbarComposerSectionCopy.ts" "src/components/agent/chat/components/Inputbar/components/InputbarComposerSection.tsx" "src/components/agent/chat/components/Inputbar/index.tsx" "src/components/agent/chat/components/Inputbar/index.test.tsx" "src/i18n/__tests__/loadNamespace.test.ts" "src/i18n/__tests__/types.test.ts"`、`npm run detect-translations -- --verbose`(5 locale、6 namespace、6504 source keys)、`npm run typecheck` 与定向 `git diff --check` 均通过。GUI 验证方面,`npm run verify:gui-smoke -- --cargo-target-dir "src-tauri/target"` 已确认 DevBridge 可用,并完成 workspace-ready / browser-runtime / site-adapters 与 agent-service-skill-entry 前端 metadata、多个 Skill Forge Rust 定向测试,但在 `smoke:agent-service-skill-entry` 继续执行 `tools::skill_tool_gate::tests::allowlisted_session_should_preserve_workspace_skill_source_metadata` 阶段超过 `1830000ms` 被脚本超时终止,期间多次等待 Cargo package cache / artifact directory file lock;该阻断来自 GUI smoke 的 Rust Skill Forge 长链与当前脏工作区并发构建环境,不属于本条前端 i18n 文案改动本身,因此本条不宣称完整 GUI smoke 绿色。统一入口方面,`npm run verify:local` 已通过 `verify:app-version`,随后在 `npm run lint` 被非本轮 dirty `src/components/agent/chat/hooks/useAsterAgentChat.ts` 未使用导入 `DEFAULT_AGENT_MODEL` / `DEFAULT_AGENT_PROVIDER` 阻断;该文件不属于本条 InputbarComposerSection 收口范围,本条不接手、不回滚,因此记录 301 不宣称新的完整绿色统一入口。本条属于 Agent Chat 常规聊天输入区邻接 chrome 普通前端 i18n 收口,不新增或修改 Tauri 命令、Bridge、DevBridge mock、`agentCommandCatalog`、`mockPriorityCommands` 或 `defaultMocks`;后续仍需继续 Inputbar 更深子组件、Agent Chat runtime / projection / helper 路径、Workspace / Browser Runtime 长尾、P3 自动化治理和 legacy Patch 删除门禁。 + +302. 2026-05-13 Agent Chat InputbarTools 工具开关 chrome source fallback 收口续测:`src/components/agent/chat/components/Inputbar/components/inputbarToolsCopy.ts` 新增 `InputbarToolsCopy` / `InputbarToolsCopyKey` / `buildInputbarToolsCopy()` 作为常规输入区工具开关事实源,`src/components/agent/chat/components/Inputbar/components/InputbarTools.tsx` 通过 `useTranslation("agent")` 把深度思考、联网搜索、任务拆分的按钮 label 与 title 从源码中文 literal 改为 `agentChat.inputbar.tools.*` key-based 读取;`src/components/agent/chat/components/Inputbar/hooks/useInputbarToolState.ts` 同步把工具开关 toast(深度思考 / 联网搜索 / 任务拆分 / 全屏编辑进入退出)改为同一组资源,确保按钮 chrome 与操作反馈不再各自拼中文。同步 5 locale `agent.json` 新增 `agentChat.inputbar.tools.*` 17 个正式资源,`src/i18n/__tests__/loadNamespace.test.ts` / `types.test.ts` 补充 thinking / webSearch / subagent / fullscreen toast 主键覆盖;新增 `src/components/agent/chat/components/Inputbar/components/InputbarTools.test.tsx`,用真实 en-US locale 回归 Thinking、Search、Task split、Deep thinking is off、Web search is on、Task splitting preference is off,并验证点击仍透传 `thinking` / `web_search` / `subagent_mode`;`src/components/agent/chat/components/Inputbar/index.test.tsx` 补充 zh-CN 与 en-US 下 `toast.info` 分别为“联网搜索已开启”和 “Web search is on”。activeTools 状态、tool id、activeTheme 是否显示任务拆分、用户输入和业务执行参数继续作为 runtime / UI 状态边界保留;TeamSelectorPanel、InputbarModelExtra、AccessMode、ExecutionStrategy、KnowledgeControl 与语音 / 图片 / 资料更深 hook 长尾仍属后续收口。验证:目标 fallback 扫描 `rg -n "深度思考|联网搜索|任务拆分|已进入全屏|已退出全屏|思考|搜索|defaultValue|t\\([^\\n]*,\\s*[\\\"']" "src/components/agent/chat/components/Inputbar/components/InputbarTools.tsx" "src/components/agent/chat/components/Inputbar/hooks/useInputbarToolState.ts" "src/components/agent/chat/components/Inputbar/components/inputbarToolsCopy.ts"` 无命中;`npm test -- "src/components/agent/chat/components/Inputbar/components/InputbarTools.test.tsx" "src/components/agent/chat/components/Inputbar/index.test.tsx" "src/i18n/__tests__/loadNamespace.test.ts" "src/i18n/__tests__/types.test.ts"` 通过,覆盖 4 个文件、72 个用例;`npx eslint --max-warnings 0 "src/components/agent/chat/components/Inputbar/components/inputbarToolsCopy.ts" "src/components/agent/chat/components/Inputbar/components/InputbarTools.tsx" "src/components/agent/chat/components/Inputbar/components/InputbarTools.test.tsx" "src/components/agent/chat/components/Inputbar/hooks/useInputbarToolState.ts" "src/components/agent/chat/components/Inputbar/index.test.tsx" "src/i18n/__tests__/loadNamespace.test.ts" "src/i18n/__tests__/types.test.ts"` 与 `npm run detect-translations -- --verbose`(5 locale、6 namespace、6521 source keys)通过。`npm run typecheck` 被非本轮 dirty `src/components/image-gen/useImageGen.ts:1773` 的 `input` 类型错误阻断;`npm run verify:local` 已通过 `verify:app-version`,随后仍在 `npm run lint` 被非本轮 dirty `src/components/agent/chat/hooks/useAsterAgentChat.ts` 未使用导入 `DEFAULT_AGENT_MODEL` / `DEFAULT_AGENT_PROVIDER` 阻断;本条不接手、不回滚这些并行脏改,因此不宣称新的完整 typecheck / verify:local / GUI smoke 绿色。本条属于 Agent Chat 常规输入区工具开关普通前端 i18n chrome 收口,不新增或修改 Tauri 命令、Bridge、DevBridge mock、`agentCommandCatalog`、`mockPriorityCommands` 或 `defaultMocks`;后续仍需继续 Inputbar 更深子组件与 hook(如图片附件、语音输入、知识资料中枢、TeamSelectorPanel)、Agent Chat runtime / projection / helper 路径、Workspace / Browser Runtime 长尾、P3 自动化治理和 legacy Patch 删除门禁。 + +303. 2026-05-13 Agent Chat useImageAttachments 图片附件 toast source fallback 收口续测:`src/components/agent/chat/components/Inputbar/hooks/inputbarImageAttachmentsCopy.ts` 新增 `InputbarImageAttachmentsCopy` / `InputbarImageAttachmentsCopyKey` / `buildInputbarImageAttachmentsCopy()` 作为常规输入区图片附件反馈事实源,`src/components/agent/chat/components/Inputbar/hooks/useImageAttachments.ts` 通过 `useTranslation("agent")` 把图片读取失败、添加图片、粘贴图片与未命名图片 fallback 从 hook 内中文 literal 改为 `agentChat.inputbar.imageAttachments.*` key-based 读取;图片 File name、mediaType、FileReader 读取结果、单轮图片上限与 pendingImages 状态继续作为 runtime / 浏览器数据边界保留,不迁入资源。同步 5 locale `agent.json` 新增 `agentChat.inputbar.imageAttachments.*` 4 个正式资源,`src/i18n/__tests__/loadNamespace.test.ts` / `types.test.ts` 补充 unnamed / readFailed / added / pasted 主键与 `fileName` 插值覆盖;`src/components/agent/chat/components/Inputbar/hooks/useImageAttachments.test.tsx` 接入真实 locale 初始化,并新增 en-US 回归验证粘贴反馈为 `Image pasted`,保留 zh-CN 粘贴、clipboard items 回退识别、删除与 20 张上限行为断言。验证:目标 fallback 扫描 `rg -n "图片读取失败|已添加图片|已粘贴图片|未命名图片|defaultValue|t\\([^\\n]*,\\s*[\\\"']" "src/components/agent/chat/components/Inputbar/hooks/useImageAttachments.ts" "src/components/agent/chat/components/Inputbar/hooks/inputbarImageAttachmentsCopy.ts"` 无命中;`npm test -- "src/components/agent/chat/components/Inputbar/hooks/useImageAttachments.test.tsx" "src/i18n/__tests__/loadNamespace.test.ts" "src/i18n/__tests__/types.test.ts"` 通过,覆盖 3 个文件、11 个用例;`npx eslint --max-warnings 0 "src/components/agent/chat/components/Inputbar/hooks/inputbarImageAttachmentsCopy.ts" "src/components/agent/chat/components/Inputbar/hooks/useImageAttachments.ts" "src/components/agent/chat/components/Inputbar/hooks/useImageAttachments.test.tsx" "src/i18n/__tests__/loadNamespace.test.ts" "src/i18n/__tests__/types.test.ts"` 与 `npm run detect-translations -- --verbose`(5 locale、6 namespace、6525 source keys)通过。`npm run typecheck` 仍被非本轮 dirty `src/components/image-gen/useImageGen.ts:1773` 的 `input` 类型错误阻断;`npm run verify:local` 已通过 `verify:app-version`,随后仍在 `npm run lint` 被非本轮 dirty `src/components/agent/chat/hooks/useAsterAgentChat.ts` 未使用导入 `DEFAULT_AGENT_MODEL` / `DEFAULT_AGENT_PROVIDER` 阻断;本条不接手、不回滚这些并行脏改,因此不宣称新的完整 typecheck / verify:local / GUI smoke 绿色。本条属于 Agent Chat 常规输入区图片附件 hook 普通前端 i18n toast 收口,不新增或修改 Tauri 命令、Bridge、DevBridge mock、`agentCommandCatalog`、`mockPriorityCommands` 或 `defaultMocks`;后续仍需继续语音输入、系统路径拖拽 / curated task hint、知识资料中枢、TeamSelectorPanel、InputbarModelExtra、Agent Chat runtime / projection / helper 路径、Workspace / Browser Runtime 长尾、P3 自动化治理和 legacy Patch 删除门禁。 + + +304. 2026-05-13 Agent Chat useInputbarDictation 语音输入 toast / 错误 fallback source fallback 收口续测:`src/components/agent/chat/components/Inputbar/hooks/inputbarDictationCopy.ts` 新增 `InputbarDictationCopy` / `InputbarDictationCopyKey` / `buildInputbarDictationCopy()` 作为常规输入区语音输入反馈事实源,`src/components/agent/chat/components/Inputbar/hooks/useInputbarDictation.ts` 通过 `useTranslation("agent")` 把语音输入不可用、设置未启用、本地模型未下载、开始录音失败、录音过短、未识别到内容与识别失败 fallback 从 hook 内中文 literal 改为 `agentChat.inputbar.dictation.feedback.*` key-based 读取;`readiness.message` 与运行时 error string / `error.message` 继续作为后端 / runtime 返回的优先展示内容保留,仅 fallback 进入资源。同步 5 locale `agent.json` 新增 `agentChat.inputbar.dictation.feedback.*` 7 个正式资源,`src/i18n/__tests__/loadNamespace.test.ts` / `types.test.ts` 补充 unavailable / enableInSettings / downloadVoiceModel / startFailed / tooShort / emptyTranscript / recognitionFailed 主键覆盖;`src/components/agent/chat/components/Inputbar/components/InputbarCore.test.tsx` 接入真实 locale 初始化,补充本地模型未安装 toast 断言,并新增 en-US 回归验证语音输入未启用时反馈为 `Enable voice input in Settings first` 且不进入模型 readiness / startRecording。录音状态、实时识别片段、识别文本、润色文本、设备 ID、duration 与用户输入仍作为 runtime / 用户数据边界保留;console 诊断日志继续作为开发诊断,不纳入 presentation 资源。验证:目标 fallback 扫描 `rg -n "语音输入暂不可用|请先在设置里启用语音输入|先下载语音模型|无法开始录音|录音时间太短|未识别到语音内容|语音识别失败|defaultValue|t\([^\n]*,\s*[\"']" "src/components/agent/chat/components/Inputbar/hooks/useInputbarDictation.ts" "src/components/agent/chat/components/Inputbar/hooks/inputbarDictationCopy.ts"` 无命中;5 个 `agent.json` 的 `python3 -m json.tool` 解析通过;`npm test -- "src/components/agent/chat/components/Inputbar/components/InputbarCore.test.tsx" "src/i18n/__tests__/loadNamespace.test.ts" "src/i18n/__tests__/types.test.ts"` 通过,覆盖 3 个文件、23 个用例;`npx eslint --max-warnings 0 "src/components/agent/chat/components/Inputbar/hooks/inputbarDictationCopy.ts" "src/components/agent/chat/components/Inputbar/hooks/useInputbarDictation.ts" "src/components/agent/chat/components/Inputbar/components/InputbarCore.test.tsx" "src/i18n/__tests__/loadNamespace.test.ts" "src/i18n/__tests__/types.test.ts"` 与 `npm run detect-translations -- --verbose`(5 locale、6 namespace、6532 source keys)通过。`npm run typecheck` 被非本轮 dirty `src/components/image-gen/useImageGen.ts:1773` 的 `input` 类型错误,以及已删除但仍被 `src/RootRouter.tsx:8` 引用的 `./pages/smart-input` 阻断;`npm run verify:local` 已通过 `verify:app-version`,随后仍在 `npm run lint` 被非本轮 dirty `src/components/agent/chat/hooks/useAsterAgentChat.ts` 未使用导入 `DEFAULT_AGENT_MODEL` / `DEFAULT_AGENT_PROVIDER` 阻断,因此本条不宣称新的完整 typecheck / verify:local / GUI smoke 绿色。本条属于 Agent Chat 常规输入区语音输入 hook 普通前端 i18n toast 收口,不新增或修改 Tauri 命令、Bridge、DevBridge mock、`agentCommandCatalog`、`mockPriorityCommands` 或 `defaultMocks`;后续仍需继续系统路径拖拽 / curated task hint、知识资料中枢、TeamSelectorPanel、InputbarModelExtra、Agent Chat runtime / projection / helper 路径、Workspace / Browser Runtime 长尾、P3 自动化治理和 legacy Patch 删除门禁。 + + +305. 2026-05-13 Agent Chat useInputbarController 系统路径拖拽 / curated task 编辑提示 source fallback 收口续测:`src/components/agent/chat/components/Inputbar/hooks/inputbarControllerCopy.ts` 新增 `InputbarControllerCopy` / `InputbarControllerCopyKey` / `buildInputbarControllerCopy()` 作为常规输入区 controller 层反馈事实源,`src/components/agent/chat/components/Inputbar/hooks/useInputbarController.ts` 通过 `useTranslation("agent")` 把系统文件路径无法读取 toast 与编辑态 launcher 按最近判断切模板 prefill hint 从 hook 内中文 literal 改为 `agentChat.inputbar.controller.*` key-based 读取。同步 5 locale `agent.json` 新增 `agentChat.inputbar.controller.toast.systemPathDropUnsupported` 与 `agentChat.inputbar.controller.curatedTask.reviewSuggestionPrefillHint` 2 个正式资源,`src/i18n/__tests__/loadNamespace.test.ts` / `types.test.ts` 补充主键覆盖;`src/components/agent/chat/components/Inputbar/index.test.tsx` 新增 en-US 回归验证非图片系统文件 drop 时 toast 为 `Cannot read system file paths. Drag files in from the built-in file manager.`,并把编辑态 curated task review suggestion 测试改为 en-US 资源断言 `Switched to a better-fit result template based on the latest review. You can keep editing before sending.`。DataTransfer 里的真实系统 path、File name / type、custom path references、curated task task/inputValues/reference selection 与用户输入继续作为 runtime / 浏览器 / 用户数据边界保留;首页 `agentChat.home.*` 对应 copy 仍服务 EmptyState/Home surface,不与本条常规输入区 controller copy 混用。验证:目标 fallback 扫描 `rg -n "无法读取系统文件路径|已按最近判断切到更适合的结果模板|defaultValue|t\([^\n]*,\s*[\"']" "src/components/agent/chat/components/Inputbar/hooks/useInputbarController.ts" "src/components/agent/chat/components/Inputbar/hooks/inputbarControllerCopy.ts"` 无命中;5 个 `agent.json` 的 `python3 -m json.tool` 解析通过;`npm test -- "src/components/agent/chat/components/Inputbar/index.test.tsx" "src/i18n/__tests__/loadNamespace.test.ts" "src/i18n/__tests__/types.test.ts"` 通过,覆盖 3 个文件、71 个用例;`npx eslint --max-warnings 0 "src/components/agent/chat/components/Inputbar/hooks/inputbarControllerCopy.ts" "src/components/agent/chat/components/Inputbar/hooks/useInputbarController.ts" "src/components/agent/chat/components/Inputbar/index.test.tsx" "src/i18n/__tests__/loadNamespace.test.ts" "src/i18n/__tests__/types.test.ts"`、`npm run detect-translations -- --verbose`(5 locale、6 namespace、6534 source keys)与定向 `git diff --check` / 新文件 `git diff --check --no-index` 均通过。`npm run typecheck` 被非本轮 dirty `src/components/image-gen/useImageGen.ts:1773` 的 `input` 类型错误,以及非本轮 `src/lib/api/asrProvider.test.ts` 引用已不存在导出 `closeVoiceWindow` / `openInputWithText` / `openVoiceWindow` 阻断;`npm run verify:local` 已通过 `verify:app-version`,随后仍在 `npm run lint` 被非本轮 dirty `src/components/agent/chat/hooks/useAsterAgentChat.ts` 未使用导入 `DEFAULT_AGENT_MODEL` / `DEFAULT_AGENT_PROVIDER` 阻断,因此本条不宣称新的完整 typecheck / verify:local / GUI smoke 绿色。本条属于 Agent Chat 常规输入区 controller 层普通前端 i18n toast / hint 收口,不新增或修改 Tauri 命令、Bridge、DevBridge mock、`agentCommandCatalog`、`mockPriorityCommands` 或 `defaultMocks`;后续仍需继续知识资料中枢、TeamSelectorPanel、InputbarModelExtra、QueuedTurnsPanel / workflow status、Agent Chat runtime / projection / helper 路径、Workspace / Browser Runtime 长尾、P3 自动化治理和 legacy Patch 删除门禁。 + + +306. 2026-05-13 Agent Chat QueuedTurnsPanel 排队消息 chrome source fallback 收口续测:`src/components/agent/chat/components/Inputbar/components/inputbarQueuedTurnsCopy.ts` 新增 `InputbarQueuedTurnsCopy` / `InputbarQueuedTurnsCopyKey` / `buildInputbarQueuedTurnsCopy()` 作为常规输入区排队消息面板文案事实源,`src/components/agent/chat/components/Inputbar/components/QueuedTurnsPanel.tsx` 通过 `useTranslation("agent")` 把稍后处理数量、依次开始提示、空白输入 fallback、查看 / 收起、附图数量、纯文本请求、立即执行 / 切换中、移除 / 正在移除等按钮与 aria chrome 从组件内中文 literal / template literal 改为 `agentChat.inputbar.queuedTurns.*` key-based 读取。同步 5 locale `agent.json` 新增 12 个 `agentChat.inputbar.queuedTurns.*` 正式资源,`src/i18n/__tests__/loadNamespace.test.ts` / `types.test.ts` 补充 header / action / meta 主键与 `count` 插值覆盖;`src/components/agent/chat/components/Inputbar/components/QueuedTurnsPanel.test.tsx` 接入真实 locale 初始化,并新增 en-US 回归验证 `Handle later 1`、`Starts in order`、`Blank input`、`2 images attached`、`Run queued message now` 与 `Remove queued message` 均来自资源,同时保留 promote / remove / loading 状态行为断言。queued turn 的 message preview/text、position、image_count 与 queued_turn_id 继续作为 runtime 数据保留,不迁入资源。验证:目标 fallback 扫描 `rg -n "稍后处理|会依次开始|空白输入|收起|查看|附图|纯文本请求|插队立即执行|切换中|立即执行|移除排队消息|正在移除排队消息|defaultValue|t\([^\n]*,\s*[\"']" "src/components/agent/chat/components/Inputbar/components/QueuedTurnsPanel.tsx" "src/components/agent/chat/components/Inputbar/components/inputbarQueuedTurnsCopy.ts"` 无命中;5 个 `agent.json` 的 `python3 -m json.tool` 解析通过;`npm test -- "src/components/agent/chat/components/Inputbar/components/QueuedTurnsPanel.test.tsx" "src/i18n/__tests__/loadNamespace.test.ts" "src/i18n/__tests__/types.test.ts"` 通过,覆盖 3 个文件、11 个用例;`npx eslint --max-warnings 0 "src/components/agent/chat/components/Inputbar/components/inputbarQueuedTurnsCopy.ts" "src/components/agent/chat/components/Inputbar/components/QueuedTurnsPanel.tsx" "src/components/agent/chat/components/Inputbar/components/QueuedTurnsPanel.test.tsx" "src/i18n/__tests__/loadNamespace.test.ts" "src/i18n/__tests__/types.test.ts"` 与 `npm run detect-translations -- --verbose`(5 locale、6 namespace、6546 source keys)通过。`npm run typecheck` 仍被非本轮 dirty `src/components/image-gen/useImageGen.ts:1773` 的 `input` 类型错误,以及非本轮 `src/lib/api/asrProvider.test.ts` 引用已不存在导出 `closeVoiceWindow` / `openInputWithText` / `openVoiceWindow` 阻断;`npm run verify:local` 已通过 `verify:app-version`,随后仍在 `npm run lint` 被非本轮 dirty `src/components/agent/chat/hooks/useAsterAgentChat.ts` 未使用导入 `DEFAULT_AGENT_MODEL` / `DEFAULT_AGENT_PROVIDER` 阻断,因此本条不宣称新的完整 typecheck / verify:local / GUI smoke 绿色。本条属于 Agent Chat 常规输入区排队消息面板普通前端 i18n chrome 收口,不新增或修改 Tauri 命令、Bridge、DevBridge mock、`agentCommandCatalog`、`mockPriorityCommands` 或 `defaultMocks`;后续仍需继续知识资料中枢、TeamSelectorPanel、InputbarModelExtra、InputbarWorkflowStatusPanel / ExecutionStrategySelect、Agent Chat runtime / projection / helper 路径、Workspace / Browser Runtime 长尾、P3 自动化治理和 legacy Patch 删除门禁。 diff --git a/docs/roadmap/voice/sensevoice-small-integration.md b/docs/roadmap/voice/sensevoice-small-integration.md index 4952f3ce9..7e62f66dd 100644 --- a/docs/roadmap/voice/sensevoice-small-integration.md +++ b/docs/roadmap/voice/sensevoice-small-integration.md @@ -386,7 +386,7 @@ npm run verify:gui-smoke 2026-05-01 实时录音预览验证: 1. `DevBridge` 真实链路:`voice_models_get_install_state -> start_recording -> get_recording_segment -> stop_recording -> transcribe_audio` 通过;录音中返回增量 PCM,最终转写 provider 为 `SenseVoice Small 本地`。 -2. 前端定向回归:`npm test -- src/lib/api/asrProvider.test.ts src/components/agent/chat/components/Inputbar/components/InputbarCore.test.tsx src/pages/smart-input.test.tsx src/lib/voiceModelSettingsNavigation.test.ts src/components/settings-v2/agent/voice/index.test.tsx` 通过。 +2. 前端定向回归:`npm test -- src/lib/api/asrProvider.test.ts src/components/agent/chat/components/Inputbar/components/InputbarCore.test.tsx src/lib/voiceModelSettingsNavigation.test.ts src/components/settings-v2/agent/voice/index.test.tsx` 通过。 3. 契约与 GUI 主路径:`npm run typecheck`、`npm run test:contracts`、`npm run verify:gui-smoke` 通过。 收口: @@ -401,7 +401,7 @@ npm run verify:local 2. 用户能从设置页显式下载模型。 3. 断网但已安装模型时,语音转写可工作。 4. 删除模型后,转写不再假装可用,并提示重新下载。 -5. 输入栏听写和悬浮语音窗消费同一条 ASR 主链。 +5. 首页 / 工作区输入栏听写与 Fn / 语音全局快捷键消费同一条 ASR 主链。 6. 已安装模型后,可以用本机 16-bit PCM WAV 文件做测试转写,证明模型目录和本地推理链路可用。 7. 开启语音输入后,macOS Fn 监听进入 registered 状态;无法捕获时明确回退到普通语音快捷键。 8. 日志能区分下载失败、模型损坏、运行时加载失败和识别失败。 diff --git a/package-lock.json b/package-lock.json index 2c889d81e..f9deee19f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "lime", - "version": "1.37.0", + "version": "1.38.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "lime", - "version": "1.37.0", + "version": "1.38.0", "license": "GPL-3.0-only", "dependencies": { "@babel/standalone": "^7.29.0", diff --git a/package.json b/package.json index e9f772693..bdcf22244 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "lime", "private": true, - "version": "1.37.0", + "version": "1.38.0", "description": "AI content workspace for Chinese creators: desktop writing, research, prompt management, knowledge base, and multi-model workflows.", "keywords": [ "ai", diff --git a/packages/lime-cli-npm/package.json b/packages/lime-cli-npm/package.json index f4626a905..d66fb3056 100644 --- a/packages/lime-cli-npm/package.json +++ b/packages/lime-cli-npm/package.json @@ -1,6 +1,6 @@ { "name": "@limecloud/lime-cli", - "version": "1.37.0", + "version": "1.38.0", "description": "Lime 官方任务 CLI", "bin": { "lime": "scripts/run.js" diff --git a/playwright-active-history-snapshot.json b/playwright-active-history-snapshot.json new file mode 100644 index 000000000..ec747fa47 --- /dev/null +++ b/playwright-active-history-snapshot.json @@ -0,0 +1 @@ +null \ No newline at end of file diff --git a/playwright-after-reload-new-chat-boundary.json b/playwright-after-reload-new-chat-boundary.json new file mode 100644 index 000000000..fbe8efd92 --- /dev/null +++ b/playwright-after-reload-new-chat-boundary.json @@ -0,0 +1,6 @@ +{ + "oldPrompt": false, + "oldLimePrompt": false, + "imagePreviewCount": 0, + "errorsText": false +} \ No newline at end of file diff --git a/playwright-current-session-debug.json b/playwright-current-session-debug.json new file mode 100644 index 000000000..7a6002450 --- /dev/null +++ b/playwright-current-session-debug.json @@ -0,0 +1,183 @@ +{ + "url": "http://127.0.0.1:1420/?e2e=1&ts=1778691100", + "body": "Lime\n邀请好友\n搜索任务\n新建任务\nSkills\n灵感\n项目资料\n最近对话\n好的,我来生成一张青柠极简插画。\n2分钟前\n新对话\n5分钟前\n新对话\n8分钟前\n国际形势分析无法执行\n23分钟前\n新对话\n45分钟前\n新对话\n46分钟前\n新对话\n50分钟前\n新对话\n51分钟前\nsmoke harness\n52分钟前\n\n1小时前\n查看更多对话\n归档\n开\n开源使用\n本地可用\nSkill Forge E2E 1778592514150\nHarness\n新对话\n工作台\n好啊,用 GPT Images 2 生成:一张广州塔,从花城汇看过去的春天照片\n先获取下工具参数\n马上生成\n图片生成\n|\nGPT Images 2\n搞定,图已经生成好了\n要调整的话直接说,我继续改\n添加资料\n模型\ndeepseek-v4-pro\n高级设置", + "sessionIds": [ + [ + "agent_session_workspace_8f16920f-b0f5-4198-9e20-bc643910be73", + "\"240ed157-3e7a-456c-a2c2-a05bd852b71c\"" + ], + [ + "agent_session_workspace_a4828a71-abed-4e31-9e43-791c58d644e1", + "\"240ed157-3e7a-456c-a2c2-a05bd852b71c\"" + ], + [ + "aster_session_access_mode_240ed157-3e7a-456c-a2c2-a05bd852b71c_bd1f156d-5d37-41f5-9d59-d80363da7928", + "\"full-access\"" + ], + [ + "agent_session_workspace_eed7439e-273e-4b49-97a2-df6c96be5fa3", + "\"240ed157-3e7a-456c-a2c2-a05bd852b71c\"" + ], + [ + "agent_session_workspace_3239e159-dacd-4a27-b5fa-cfd0aea11d28", + "\"240ed157-3e7a-456c-a2c2-a05bd852b71c\"" + ], + [ + "agent_session_workspace_8d383c48-80e2-42e7-9bf3-c2bf5c4ef864", + "\"240ed157-3e7a-456c-a2c2-a05bd852b71c\"" + ], + [ + "agent_session_workspace_4d6b14fa-d4cb-4fe5-b3fa-6f1827b19f5e", + "\"240ed157-3e7a-456c-a2c2-a05bd852b71c\"" + ], + [ + "agent_session_workspace_43e5ac59-422c-465a-9312-d86ce678e317", + "\"240ed157-3e7a-456c-a2c2-a05bd852b71c\"" + ], + [ + "aster_session_access_mode_240ed157-3e7a-456c-a2c2-a05bd852b71c_3fe77ab3-e1d3-4f47-9515-02404d70d1e5", + "\"full-access\"" + ], + [ + "agent_session_workspace_700543e4-9b1f-42d3-811a-0b1f0601fad2", + "\"240ed157-3e7a-456c-a2c2-a05bd852b71c\"" + ], + [ + "aster_session_access_mode_240ed157-3e7a-456c-a2c2-a05bd852b71c_8d383c48-80e2-42e7-9bf3-c2bf5c4ef864", + "\"full-access\"" + ], + [ + "agent_session_workspace_e2545a39-417c-4ba4-a1d7-d5a7b5517b28", + "\"240ed157-3e7a-456c-a2c2-a05bd852b71c\"" + ], + [ + "agent_session_workspace_3fe77ab3-e1d3-4f47-9515-02404d70d1e5", + "\"240ed157-3e7a-456c-a2c2-a05bd852b71c\"" + ], + [ + "aster_session_access_mode_240ed157-3e7a-456c-a2c2-a05bd852b71c_d9fd2dca-4f1c-4fc9-9ab1-57a8584a365b", + "\"full-access\"" + ], + [ + "aster_session_access_mode_240ed157-3e7a-456c-a2c2-a05bd852b71c_8f16920f-b0f5-4198-9e20-bc643910be73", + "\"full-access\"" + ], + [ + "aster_last_sessionId_global", + "null" + ], + [ + "agent_session_workspace_9ec7e2c6-01c3-4654-9881-cead648710a1", + "\"240ed157-3e7a-456c-a2c2-a05bd852b71c\"" + ], + [ + "agent_session_workspace_31cc9300-7e26-4c85-8a0d-3f00e33168c2", + "\"240ed157-3e7a-456c-a2c2-a05bd852b71c\"" + ], + [ + "agent_session_workspace_a2738669-04c1-40db-9c88-251e1328771b", + "\"240ed157-3e7a-456c-a2c2-a05bd852b71c\"" + ], + [ + "agent_session_workspace_1b215641-00bc-41fa-8bb6-0ee64bd87819", + "\"240ed157-3e7a-456c-a2c2-a05bd852b71c\"" + ], + [ + "agent_session_workspace_2123a0bd-445f-4ded-84ee-0bdff0c86284", + "\"240ed157-3e7a-456c-a2c2-a05bd852b71c\"" + ], + [ + "aster_session_access_mode_240ed157-3e7a-456c-a2c2-a05bd852b71c_31cc9300-7e26-4c85-8a0d-3f00e33168c2", + "\"full-access\"" + ], + [ + "agent_session_workspace_2b64278a-aa4a-442e-846c-6113dd1d4153", + "\"240ed157-3e7a-456c-a2c2-a05bd852b71c\"" + ], + [ + "agent_session_workspace_a0e782ef-a050-4126-8e8f-6bc258e13de9", + "\"240ed157-3e7a-456c-a2c2-a05bd852b71c\"" + ], + [ + "agent_session_workspace_d9fd2dca-4f1c-4fc9-9ab1-57a8584a365b", + "\"240ed157-3e7a-456c-a2c2-a05bd852b71c\"" + ], + [ + "aster_session_access_mode_240ed157-3e7a-456c-a2c2-a05bd852b71c_9ec7e2c6-01c3-4654-9881-cead648710a1", + "\"full-access\"" + ], + [ + "aster_session_access_mode_240ed157-3e7a-456c-a2c2-a05bd852b71c_69c81ba7-782a-47e1-92d5-cfec94ea7760", + "\"full-access\"" + ], + [ + "aster_last_sessionId_240ed157-3e7a-456c-a2c2-a05bd852b71c", + "\"8f16920f-b0f5-4198-9e20-bc643910be73\"" + ], + [ + "agent_session_workspace_6a7d0ff5-0cae-4edd-a396-9411cfede225", + "\"240ed157-3e7a-456c-a2c2-a05bd852b71c\"" + ], + [ + "aster_session_access_mode_240ed157-3e7a-456c-a2c2-a05bd852b71c_6a7d0ff5-0cae-4edd-a396-9411cfede225", + "\"full-access\"" + ], + [ + "agent_session_workspace_614c5e60-9b49-4a53-aba6-1c7b7301cb26", + "\"240ed157-3e7a-456c-a2c2-a05bd852b71c\"" + ], + [ + "agent_session_workspace_dc7fba43-1c2f-4251-8d9b-05c93943307a", + "\"240ed157-3e7a-456c-a2c2-a05bd852b71c\"" + ], + [ + "agent_session_workspace_69c81ba7-782a-47e1-92d5-cfec94ea7760", + "\"240ed157-3e7a-456c-a2c2-a05bd852b71c\"" + ], + [ + "agent_session_workspace_bd1f156d-5d37-41f5-9d59-d80363da7928", + "\"240ed157-3e7a-456c-a2c2-a05bd852b71c\"" + ], + [ + "aster_session_snapshots_persisted_240ed157-3e7a-456c-a2c2-a05bd852b71c", + "{\"8d383c48-80e2-42e7-9bf3-c2bf5c4ef864\":{\"messages\":[{\"id\":\"2b28ee8f-2588-4721-94f8-d9f9f31ffe24\",\"role\":\"user\",\"content\":\"@analysis 帮我分析一下今天的国际形势\",\"timestamp\":\"2026-05-13T16:31:22.532Z\"},{\"id\":\"9ddd7" + ] + ], + "activeButtons": [ + { + "tag": "DIV", + "text": "新对话8分钟前", + "testid": null, + "active": "true", + "ariaCurrent": null + }, + { + "tag": "BUTTON", + "text": "新对话8分钟前", + "testid": null, + "active": null, + "ariaCurrent": "page" + }, + { + "tag": "BUTTON", + "text": "Skill Forge E2E 1778592514150", + "testid": null, + "active": null, + "ariaCurrent": null + }, + { + "tag": "DIV", + "text": "新对话", + "testid": "task-center-tab-8f16920f-b0f5-4198-9e20-bc643910be73", + "active": "true", + "ariaCurrent": null + }, + { + "tag": "BUTTON", + "text": "新对话", + "testid": null, + "active": null, + "ariaCurrent": "page" + } + ] +} \ No newline at end of file diff --git a/playwright-history-buttons.json b/playwright-history-buttons.json new file mode 100644 index 000000000..2b86f8102 --- /dev/null +++ b/playwright-history-buttons.json @@ -0,0 +1,62 @@ +[ + { + "i": 8, + "text": "", + "aria": "新建对话", + "testid": null + }, + { + "i": 9, + "text": "新对话3分钟前", + "aria": null, + "testid": null + }, + { + "i": 11, + "text": "新对话5分钟前", + "aria": null, + "testid": null + }, + { + "i": 15, + "text": "新对话42分钟前", + "aria": null, + "testid": null + }, + { + "i": 17, + "text": "新对话43分钟前", + "aria": null, + "testid": null + }, + { + "i": 19, + "text": "新对话47分钟前", + "aria": null, + "testid": null + }, + { + "i": 21, + "text": "新对话49分钟前", + "aria": null, + "testid": null + }, + { + "i": 27, + "text": "新对话1小时前", + "aria": null, + "testid": null + }, + { + "i": 38, + "text": "新对话", + "aria": null, + "testid": null + }, + { + "i": 39, + "text": "", + "aria": "新建对话", + "testid": "task-center-tab-create-button" + } +] \ No newline at end of file diff --git a/playwright-history-first-image-check.json b/playwright-history-first-image-check.json new file mode 100644 index 000000000..21a1122d6 --- /dev/null +++ b/playwright-history-first-image-check.json @@ -0,0 +1,32 @@ +{ + "text": "Lime\n邀请好友\n搜索任务\n新建任务\nSkills\n灵感\n项目资料\n最近对话\n新对话\n5分钟前\n新对话\n8分钟前\n国际形势分析无法执行\n23分钟前\n新对话\n44分钟前\n新对话\n45分钟前\n新对话\n49分钟前\n新对话\n51分钟前\nsmoke harness\n52分钟前\n\n1小时前\n新对话\n1小时前\n查看更多对话\n归档\n开\n开源使用\n本地可用\nSkill Forge E2E 1778592514150\nHarness\n新对话\n工作台\n好啊,用 GPT Images 2 生成:一张广州塔,从花城汇看过去的春天照片\n先获取下工具参数\n马上生成\n图片生成\n|\nGPT Images 2\n搞定,图已经生成好了\n要调整的话直接说,我继续改\n添加资料\n模型\ndeepseek-v4-pro\n高级设置", + "guangzhou": true, + "lime": false, + "tags": [], + "intros": [ + "好啊,用 GPT Images 2 生成:一张广州塔,从花城汇看过去的春天照片\n先获取下工具参数\n马上生成" + ], + "imageCards": [ + { + "testid": "image-workbench-message-preview-e05d557e-60b5-4d47-b9e4-f825b3150df5", + "text": "图片生成|GPT Images 2搞定,图已经生成好了\n要调整的话直接说,我继续改", + "imgCount": 1 + }, + { + "testid": "image-workbench-message-preview-toolbar-e05d557e-60b5-4d47-b9e4-f825b3150df5", + "text": "图片生成|GPT Images 2", + "imgCount": 0 + }, + { + "testid": "image-workbench-message-preview-single-media-e05d557e-60b5-4d47-b9e4-f825b3150df5", + "text": "", + "imgCount": 1 + }, + { + "testid": "image-workbench-message-preview-caption-e05d557e-60b5-4d47-b9e4-f825b3150df5", + "text": "搞定,图已经生成好了\n要调整的话直接说,我继续改", + "imgCount": 0 + } + ], + "failureText": false +} \ No newline at end of file diff --git a/playwright-image-flow-state-after-wait.json b/playwright-image-flow-state-after-wait.json new file mode 100644 index 000000000..5045cb4c8 --- /dev/null +++ b/playwright-image-flow-state-after-wait.json @@ -0,0 +1,31 @@ +{ + "text": "Lime\n邀请好友\n搜索任务\n新建任务\nSkills\n灵感\n项目资料\n最近对话\n国际形势分析无法执行\n17分钟前\n新对话\n38分钟前\n新对话\n39分钟前\n新对话\n43分钟前\n新对话\n45分钟前\nsmoke harness\n46分钟前\n\n54分钟前\n新对话\n1小时前\n新对话\n2小时前\n新对话\n3小时前\n查看更多对话\n归档\n开\n开源使用\n本地可用\nSkill Think Keep Audit 20260513\nHarness\n新对话\n@配图\n生成一张广州塔,从花城汇看过去的春天照片\n好啊,用 GPT Images 2 生成:一张广州塔,从花城汇看过去的春天照片\n先获取下工具参数\n马上生成\n图片生成\n|\nGPT Images 2\n搞定,图已经生成好了\n要调整的话直接说,我继续改\n添加资料\n模型\ndeepseek-v4-pro\n高级设置", + "imageCards": [ + { + "testid": "image-workbench-message-preview-e05d557e-60b5-4d47-b9e4-f825b3150df5", + "text": "图片生成|GPT Images 2搞定,图已经生成好了\n要调整的话直接说,我继续改", + "imgCount": 1, + "retryCount": 0 + }, + { + "testid": "image-workbench-message-preview-toolbar-e05d557e-60b5-4d47-b9e4-f825b3150df5", + "text": "图片生成|GPT Images 2", + "imgCount": 0, + "retryCount": 0 + }, + { + "testid": "image-workbench-message-preview-single-media-e05d557e-60b5-4d47-b9e4-f825b3150df5", + "text": "", + "imgCount": 1, + "retryCount": 0 + }, + { + "testid": "image-workbench-message-preview-caption-e05d557e-60b5-4d47-b9e4-f825b3150df5", + "text": "搞定,图已经生成好了\n要调整的话直接说,我继续改", + "imgCount": 0, + "retryCount": 0 + } + ], + "retryButtons": [], + "messageCount": 2 +} \ No newline at end of file diff --git a/playwright-image-flow-state.json b/playwright-image-flow-state.json new file mode 100644 index 000000000..4961303ef --- /dev/null +++ b/playwright-image-flow-state.json @@ -0,0 +1,30 @@ +{ + "text": "Lime\n邀请好友\n搜索任务\n新建任务\nSkills\n灵感\n项目资料\n最近对话\n国际形势分析无法执行\n16分钟前\n新对话\n37分钟前\n新对话\n38分钟前\n新对话\n43分钟前\n新对话\n44分钟前\nsmoke harness\n45分钟前\n\n54分钟前\n新对话\n1小时前\n新对话\n2小时前\n新对话\n2小时前\n查看更多对话\n归档\n开\n开源使用\n本地可用\nSkill Think Keep Audit 20260513\nHarness\n新对话\n@配图\n生成一张广州塔,从花城汇看过去的春天照片\n好啊,用 GPT Images 2 生成:一张广州塔,从花城汇看过去的春天照片\n先获取下工具参数\n马上生成\n图片生成\n|\nGPT Images 2\n正在生成图片\n添加资料\n模型\ndeepseek-v4-pro\n高级设置", + "imageCards": [ + { + "testid": "image-workbench-message-preview-e05d557e-60b5-4d47-b9e4-f825b3150df5", + "text": "图片生成|GPT Images 2正在生成图片", + "imgCount": 0, + "buttons": [] + }, + { + "testid": "image-workbench-message-preview-toolbar-e05d557e-60b5-4d47-b9e4-f825b3150df5", + "text": "图片生成|GPT Images 2", + "imgCount": 0, + "buttons": [] + }, + { + "testid": "image-workbench-message-preview-single-media-e05d557e-60b5-4d47-b9e4-f825b3150df5", + "text": "正在生成图片", + "imgCount": 0, + "buttons": [] + } + ], + "retryButtons": [], + "userTags": [ + "@配图" + ], + "intros": [ + "好啊,用 GPT Images 2 生成:一张广州塔,从花城汇看过去的春天照片\n先获取下工具参数\n马上生成" + ] +} \ No newline at end of file diff --git a/playwright-new-chat-no-leak.json b/playwright-new-chat-no-leak.json new file mode 100644 index 000000000..7426583f9 --- /dev/null +++ b/playwright-new-chat-no-leak.json @@ -0,0 +1,7 @@ +{ + "text": "Lime\n邀请好友\n搜索任务\n新建任务\nSkills\n灵感\n项目资料\n最近对话\n国际形势分析无法执行\n17分钟前\n新对话\n39分钟前\n新对话\n40分钟前\n新对话\n44分钟前\n新对话\n45分钟前\nsmoke harness\n46分钟前\n\n55分钟前\n新对话\n1小时前\n新对话\n2小时前\n新对话\n3小时前\n查看更多对话\n归档\n开\n开源使用\n本地可用\nSkill Think Keep Audit 20260513\n新对话\n创作\n\n青柠一下,灵感即来\n\n说一句目标,Lime 就接着帮你做。\n\n文案、图片、视频、搜索和网页任务围绕同一目标持续推进,并沉淀上下文、偏好和做法。\n\n帮我梳理一下需求分析\ntab\n添加资料\n高级设置\n当前模型\ndeepseek-v4-pro\n引导帮助\n写作\n添加资料\nPPT\n调研报告\n需求分析\n视频\n设计\nExcel\n编程\n更多做法\n回看最近结果 · 国际形势分析无法执行\n向下滑,看看 Lime 可以帮你做什么\n↓\n你可以从这些任务开始\n\n往下看更多任务样例;真正执行仍会回到生成里继续补充。\n\n拆解一条爆款内容\n把一条高表现内容拆成标题钩子、结构节奏、素材手法和可复用套路,方便快速复刻。\n复盘这个账号/项目\n围绕目标、已有结果和下一步动作判断当前该怎么推进,适合内容账号、项目推进和运营回看。\n脚本转口播/字幕稿\n把现有脚本整理成更适合口播和字幕的版本,补齐停顿、节奏和适合出镜表达的语气。\n每日趋势摘要\n先收一版内容趋势、热点方向和值得继续跟进的切口,适合每天快速开工前拉一遍。\n内容主稿生成\n围绕目标受众、表达结构和关键信息,先生成一版可继续迭代的内容首稿。\n长文转多平台发布稿\n把长文、资料或文章整理成多平台可直接发布的版本,不用再自己拆标题和结构。\n复制轮播帖\n拆解参考轮播帖的结构、文风和卖点,再输出一版可继续改写的轮播内容。\n视频配音\n围绕视频素材、语言要求与旁白意图,先在本地工作区整理一版可继续加工的配音稿。\n视频配音成其他语言\n先整理配音脚本和语言要求,输出一版可继续进入配音流程的执行稿。\n文章转 Slide 视频提纲\n把文章拆成镜头化结构,先生成一版适合做 Slide 视频的提纲。\n账号增长跟踪\n围绕参考账号和目标平台先整理一版增长打法,再持续跟踪内容节奏、关键指标和提醒条件。", + "oldPrompt": false, + "generating": false, + "imagePreviewCount": 0, + "userTags": [] +} \ No newline at end of file diff --git a/playwright-second-image-after-reload-final.json b/playwright-second-image-after-reload-final.json new file mode 100644 index 000000000..6e4a93c14 --- /dev/null +++ b/playwright-second-image-after-reload-final.json @@ -0,0 +1,29 @@ +{ + "text": "Lime\n邀请好友\n搜索任务\n新建任务\nSkills\n灵感\n项目资料\n最近对话\n新对话\n3分钟前\n新对话\n5分钟前\n国际形势分析无法执行\n20分钟前\n新对话\n42分钟前\n新对话\n43分钟前\n新对话\n47分钟前\n新对话\n49分钟前\nsmoke harness\n49分钟前\n\n58分钟前\n新对话\n1小时前\n查看更多对话\n归档\n开\n开源使用\n本地可用\nSkill Think Keep Audit 20260513\nHarness\n新对话\n@配图\n再生成一张青柠极简插画\n好啊,用 GPT Images 2 生成:再生成一张青柠极简插画\n先获取下工具参数\n马上生成\n图片生成\n|\nGPT Images 2\n搞定,图已经生成好了\n要调整的话直接说,我继续改\n添加资料\n模型\ndeepseek-v4-pro\n高级设置", + "oldPrompt": false, + "newPrompt": true, + "imageCards": [ + { + "testid": "image-workbench-message-preview-968e9568-1dc4-4fba-be89-d3df274a4adb", + "text": "图片生成|GPT Images 2搞定,图已经生成好了\n要调整的话直接说,我继续改", + "imgCount": 1 + }, + { + "testid": "image-workbench-message-preview-toolbar-968e9568-1dc4-4fba-be89-d3df274a4adb", + "text": "图片生成|GPT Images 2", + "imgCount": 0 + }, + { + "testid": "image-workbench-message-preview-single-media-968e9568-1dc4-4fba-be89-d3df274a4adb", + "text": "", + "imgCount": 1 + }, + { + "testid": "image-workbench-message-preview-caption-968e9568-1dc4-4fba-be89-d3df274a4adb", + "text": "搞定,图已经生成好了\n要调整的话直接说,我继续改", + "imgCount": 0 + } + ], + "retryButtons": [], + "forbidden": [] +} \ No newline at end of file diff --git a/playwright-second-image-after-reload-start.json b/playwright-second-image-after-reload-start.json new file mode 100644 index 000000000..e03f9fff2 --- /dev/null +++ b/playwright-second-image-after-reload-start.json @@ -0,0 +1,28 @@ +{ + "text": "Lime\n邀请好友\n搜索任务\n新建任务\nSkills\n灵感\n项目资料\n最近对话\n新对话\n3分钟前\n新对话\n5分钟前\n国际形势分析无法执行\n20分钟前\n新对话\n42分钟前\n新对话\n43分钟前\n新对话\n47分钟前\n新对话\n49分钟前\nsmoke harness\n49分钟前\n\n58分钟前\n新对话\n1小时前\n查看更多对话\n归档\n开\n开源使用\n本地可用\nSkill Think Keep Audit 20260513\nHarness\n新对话\n@配图\n再生成一张青柠极简插画\n好啊,用 GPT Images 2 生成:再生成一张青柠极简插画\n先获取下工具参数\n马上生成\n图片生成\n|\nGPT Images 2\n正在生成图片\n添加资料\n模型\ndeepseek-v4-pro\n高级设置", + "oldPrompt": false, + "newPrompt": true, + "tags": [ + "@配图" + ], + "intros": [ + "好啊,用 GPT Images 2 生成:再生成一张青柠极简插画\n先获取下工具参数\n马上生成" + ], + "imageCards": [ + { + "testid": "image-workbench-message-preview-968e9568-1dc4-4fba-be89-d3df274a4adb", + "text": "图片生成|GPT Images 2正在生成图片", + "imgCount": 0 + }, + { + "testid": "image-workbench-message-preview-toolbar-968e9568-1dc4-4fba-be89-d3df274a4adb", + "text": "图片生成|GPT Images 2", + "imgCount": 0 + }, + { + "testid": "image-workbench-message-preview-single-media-968e9568-1dc4-4fba-be89-d3df274a4adb", + "text": "正在生成图片", + "imgCount": 0 + } + ] +} \ No newline at end of file diff --git a/playwright-second-image-start.json b/playwright-second-image-start.json new file mode 100644 index 000000000..18a24c319 --- /dev/null +++ b/playwright-second-image-start.json @@ -0,0 +1,8 @@ +{ + "text": "Lime\n邀请好友\n搜索任务\n新建任务\nSkills\n灵感\n项目资料\n最近对话\n国际形势分析无法执行\n18分钟前\n新对话\n39分钟前\n新对话\n40分钟前\n新对话\n45分钟前\n新对话\n46分钟前\nsmoke harness\n47分钟前\n\n56分钟前\n新对话\n1小时前\n新对话\n2小时前\n新对话\n3小时前\n查看更多对话\n归档\n开\n开源使用\n本地可用\nSkill Think Keep Audit 20260513\n新对话\n创作\n\n青柠一下,灵感即来\n\n说一句目标,Lime 就接着帮你做。\n\n文案、图片、视频、搜索和网页任务围绕同一目标持续推进,并沉淀上下文、偏好和做法。\n\n帮我整理项目资料\ntab\n添加资料\n高级设置\n当前模型\ndeepseek-v4-pro\n引导帮助\n写作\n添加资料\nPPT\n调研报告\n需求分析\n视频\n设计\nExcel\n编程\n更多做法\n回看最近结果 · 国际形势分析无法执行\n向下滑,看看 Lime 可以帮你做什么\n↓\n你可以从这些任务开始\n\n往下看更多任务样例;真正执行仍会回到生成里继续补充。\n\n拆解一条爆款内容\n把一条高表现内容拆成标题钩子、结构节奏、素材手法和可复用套路,方便快速复刻。\n复盘这个账号/项目\n围绕目标、已有结果和下一步动作判断当前该怎么推进,适合内容账号、项目推进和运营回看。\n脚本转口播/字幕稿\n把现有脚本整理成更适合口播和字幕的版本,补齐停顿、节奏和适合出镜表达的语气。\n每日趋势摘要\n先收一版内容趋势、热点方向和值得继续跟进的切口,适合每天快速开工前拉一遍。\n内容主稿生成\n围绕目标受众、表达结构和关键信息,先生成一版可继续迭代的内容首稿。\n长文转多平台发布稿\n把长文、资料或文章整理成多平台可直接发布的版本,不用再自己拆标题和结构。\n短视频编排\n把文本、线框图、配乐、剧本和短视频草稿收口成一条多模态结果链。\n复制轮播帖\n拆解参考轮播帖的结构、文风和卖点,再输出一版可继续改写的轮播内容。\n视频配音\n围绕视频素材、语言要求与旁白意图,先在本地工作区整理一版可继续加工的配音稿。\n视频配音成其他语言\n先整理配音脚本和语言要求,输出一版可继续进入配音流程的执行稿。\n文章转 Slide 视频提纲\n把文章拆成镜头化结构,先生成一版适合做 Slide 视频的提纲。\n我来读取已导出的 Markdown 文件,然后翻...\n沉淀自一次成功结果:我来读取已导出的 Markdown 文件,然后翻译成中文。 文件已读取完毕。现在我将正文翻译为中文,保留所有 Markdown 结构、代码块、链...\n加载对话历史失败: normalizeUserVisibleContent is not defined", + "oldPrompt": false, + "newPrompt": false, + "tags": [], + "intros": [], + "imagePreviewCount": 0 +} \ No newline at end of file diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index c4f9ed060..1b2909c44 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -5131,7 +5131,7 @@ dependencies = [ [[package]] name = "lime" -version = "1.37.0" +version = "1.38.0" dependencies = [ "anyhow", "arboard", @@ -5238,7 +5238,7 @@ dependencies = [ [[package]] name = "lime-agent" -version = "1.37.0" +version = "1.38.0" dependencies = [ "anyhow", "aster-core", @@ -5267,7 +5267,7 @@ dependencies = [ [[package]] name = "lime-browser-runtime" -version = "1.37.0" +version = "1.38.0" dependencies = [ "chrono", "futures", @@ -5284,7 +5284,7 @@ dependencies = [ [[package]] name = "lime-cli" -version = "1.37.0" +version = "1.38.0" dependencies = [ "clap", "lime-core", @@ -5296,7 +5296,7 @@ dependencies = [ [[package]] name = "lime-config" -version = "1.37.0" +version = "1.38.0" dependencies = [ "async-trait", "lime-core", @@ -5312,7 +5312,7 @@ dependencies = [ [[package]] name = "lime-core" -version = "1.37.0" +version = "1.38.0" dependencies = [ "aster-models", "async-trait", @@ -5365,7 +5365,7 @@ dependencies = [ [[package]] name = "lime-gateway" -version = "1.37.0" +version = "1.38.0" dependencies = [ "aes", "axum 0.7.9", @@ -5395,7 +5395,7 @@ dependencies = [ [[package]] name = "lime-infra" -version = "1.37.0" +version = "1.38.0" dependencies = [ "chrono", "dashmap 5.5.3", @@ -5415,7 +5415,7 @@ dependencies = [ [[package]] name = "lime-knowledge" -version = "1.37.0" +version = "1.38.0" dependencies = [ "chrono", "hex", @@ -5428,7 +5428,7 @@ dependencies = [ [[package]] name = "lime-mcp" -version = "1.37.0" +version = "1.38.0" dependencies = [ "async-trait", "dirs 5.0.1", @@ -5444,7 +5444,7 @@ dependencies = [ [[package]] name = "lime-media-runtime" -version = "1.37.0" +version = "1.38.0" dependencies = [ "axum 0.7.9", "base64 0.22.1", @@ -5477,7 +5477,7 @@ dependencies = [ [[package]] name = "lime-processor" -version = "1.37.0" +version = "1.38.0" dependencies = [ "async-trait", "lime-core", @@ -5496,7 +5496,7 @@ dependencies = [ [[package]] name = "lime-providers" -version = "1.37.0" +version = "1.38.0" dependencies = [ "anyhow", "async-stream", @@ -5551,7 +5551,7 @@ dependencies = [ [[package]] name = "lime-server" -version = "1.37.0" +version = "1.38.0" dependencies = [ "aster-core", "async-stream", @@ -5595,7 +5595,7 @@ dependencies = [ [[package]] name = "lime-server-utils" -version = "1.37.0" +version = "1.38.0" dependencies = [ "axum 0.7.9", "futures", @@ -5610,7 +5610,7 @@ dependencies = [ [[package]] name = "lime-services" -version = "1.37.0" +version = "1.38.0" dependencies = [ "anyhow", "aster-core", @@ -5655,7 +5655,7 @@ dependencies = [ [[package]] name = "lime-skills" -version = "1.37.0" +version = "1.38.0" dependencies = [ "async-trait", "dirs 5.0.1", @@ -5673,7 +5673,7 @@ dependencies = [ [[package]] name = "lime-websocket" -version = "1.37.0" +version = "1.38.0" dependencies = [ "axum 0.7.9", "chrono", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 53932d98f..41f213f76 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -9,7 +9,7 @@ exclude = [ resolver = "2" [workspace.package] -version = "1.37.0" +version = "1.38.0" edition = "2021" authors = ["coso"] repository = "https://github.com/aiclientproxy/lime" @@ -198,7 +198,7 @@ version = "2.4.9" [package] name = "lime" -version = "1.37.0" +version = "1.38.0" description = "AI API Proxy Desktop App" authors = ["you"] edition = "2021" diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 3e1286090..4242d0fe5 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -2,7 +2,7 @@ "$schema": "../gen/schemas/desktop-schema.json", "identifier": "default", "description": "Lime 默认权限配置", - "windows": ["main", "smart-input", "browser-connector-guide-*"], + "windows": ["main", "browser-connector-guide-*"], "permissions": [ "core:default", "core:event:default", diff --git a/src-tauri/crates/agent/src/skill_execution.rs b/src-tauri/crates/agent/src/skill_execution.rs index 11f633ca4..6c6698c13 100644 --- a/src-tauri/crates/agent/src/skill_execution.rs +++ b/src-tauri/crates/agent/src/skill_execution.rs @@ -48,6 +48,7 @@ pub struct SkillWorkflowExecution<'a> { pub aster_state: &'a AsterAgentState, pub skill: &'a LoadedSkillDefinition, pub user_input: &'a str, + pub user_visible_input: Option<&'a str>, pub images: &'a [SkillInputImage], pub execution_id: &'a str, pub session_id: &'a str, @@ -60,6 +61,7 @@ pub struct SkillPromptExecution<'a> { pub aster_state: &'a AsterAgentState, pub skill: &'a LoadedSkillDefinition, pub user_input: &'a str, + pub user_visible_input: Option<&'a str>, pub images: &'a [SkillInputImage], pub execution_id: &'a str, pub session_id: &'a str, @@ -132,29 +134,61 @@ fn build_prompt_system_prompt(skill_markdown: &str, memory_prompt: Option<&str>) } } -fn build_user_message(user_input: &str, images: &[SkillInputImage]) -> Message { +fn should_hide_execution_input_from_user( + execution_input: &str, + user_visible_input: Option<&str>, +) -> bool { + user_visible_input + .map(|input| { + let visible = input.trim(); + !visible.is_empty() && visible != execution_input.trim() + }) + .unwrap_or(false) +} + +fn build_user_message( + user_input: &str, + user_visible_input: Option<&str>, + images: &[SkillInputImage], +) -> Message { let mut user_message = Message::user().with_text(user_input); for image in images { user_message = user_message.with_image(image.data.clone(), image.media_type.clone()); } + if should_hide_execution_input_from_user(user_input, user_visible_input) { + user_message = user_message.agent_only(); + } user_message } -fn build_skill_turn_context(skill: &LoadedSkillDefinition) -> Option { +fn build_skill_turn_context( + skill: &LoadedSkillDefinition, + user_visible_input: Option<&str>, +) -> Option { let allowed_tools = skill .allowed_tools .as_ref() - .filter(|tools| !tools.is_empty())?; + .filter(|tools| !tools.is_empty()); + let user_visible_input_text = user_visible_input.and_then(|input| { + (!input.trim().is_empty()).then(|| input.trim().to_string()) + }); + + if allowed_tools.is_none() && user_visible_input_text.is_none() { + return None; + } let mut metadata = HashMap::new(); - metadata.insert( - "subagent".to_string(), - json!({ - "allowed_tools": allowed_tools, - }), - ); + if let Some(allowed_tools) = allowed_tools { + metadata.insert( + "subagent".to_string(), + json!({ + "allowed_tools": allowed_tools, + }), + ); + } Some(TurnContextOverride { + user_visible_input_text, metadata, ..TurnContextOverride::default() }) @@ -232,6 +266,7 @@ pub async fn execute_skill_workflow( aster_state, skill, user_input, + user_visible_input, images, execution_id, session_id, @@ -246,7 +281,7 @@ pub async fn execute_skill_workflow( let mut artifact_paths = Vec::new(); let mut accumulated_context = user_input.to_string(); let mut final_output = String::new(); - let skill_turn_context = build_skill_turn_context(skill); + let skill_turn_context = build_skill_turn_context(skill, user_visible_input); tracing::info!( "[execute_skill_workflow] 开始 workflow 执行: steps={}, skill={}", @@ -283,7 +318,7 @@ pub async fn execute_skill_workflow( } let session_config = session_config_builder.build(); let step_input = build_step_input(user_input, &accumulated_context, idx == 0); - let user_message = build_user_message(&step_input, images); + let user_message = build_user_message(&step_input, user_visible_input, images); let reply = stream_skill_session( aster_state, @@ -364,6 +399,7 @@ pub async fn execute_skill_prompt( aster_state, skill, user_input, + user_visible_input, images, execution_id, session_id, @@ -377,11 +413,11 @@ pub async fn execute_skill_prompt( memory_prompt, )) .include_context_trace(true); - if let Some(turn_context) = build_skill_turn_context(skill) { + if let Some(turn_context) = build_skill_turn_context(skill, user_visible_input) { session_config_builder = session_config_builder.turn_context(turn_context); } let session_config = session_config_builder.build(); - let user_message = build_user_message(user_input, images); + let user_message = build_user_message(user_input, user_visible_input, images); let reply = stream_skill_session( aster_state, session_id, @@ -425,7 +461,7 @@ pub async fn execute_skill_prompt( #[cfg(test)] mod tests { - use super::build_skill_turn_context; + use super::{build_skill_turn_context, build_user_message}; use lime_skills::LoadedSkillDefinition; use std::collections::HashMap; @@ -464,7 +500,7 @@ mod tests { "social_generate_cover_image", ])); - let turn_context = build_skill_turn_context(&skill).expect("turn context"); + let turn_context = build_skill_turn_context(&skill, None).expect("turn context"); assert_eq!( turn_context.metadata["subagent"]["allowed_tools"], serde_json::json!([ @@ -477,9 +513,44 @@ mod tests { #[test] fn build_skill_turn_context_skips_empty_allowed_tools() { let skill = build_loaded_skill(Some(Vec::new())); - assert!(build_skill_turn_context(&skill).is_none()); + assert!(build_skill_turn_context(&skill, None).is_none()); let skill = build_loaded_skill(None); - assert!(build_skill_turn_context(&skill).is_none()); + assert!(build_skill_turn_context(&skill, None).is_none()); + } + + #[test] + fn build_skill_turn_context_keeps_user_visible_input_without_allowed_tools() { + let skill = build_loaded_skill(None); + + let turn_context = + build_skill_turn_context(&skill, Some(" @analysis 帮我分析一下 ")) + .expect("turn context"); + + assert_eq!( + turn_context.user_visible_input_text.as_deref(), + Some("@analysis 帮我分析一下") + ); + assert!(turn_context.metadata.is_empty()); + } + + #[test] + fn build_user_message_hides_execution_input_when_visible_input_differs() { + let message = build_user_message( + "结构化执行输入", + Some("@analysis 帮我分析一下今天的国际形势"), + &[], + ); + + assert!(!message.is_user_visible()); + assert!(message.is_agent_visible()); + } + + #[test] + fn build_user_message_keeps_plain_input_user_visible() { + let message = build_user_message("普通用户输入", Some("普通用户输入"), &[]); + + assert!(message.is_user_visible()); + assert!(message.is_agent_visible()); } } diff --git a/src-tauri/crates/aster-rust/crates/aster/src/agents/agent.rs b/src-tauri/crates/aster-rust/crates/aster/src/agents/agent.rs index c7fc0532f..6336325fd 100644 --- a/src-tauri/crates/aster-rust/crates/aster/src/agents/agent.rs +++ b/src-tauri/crates/aster-rust/crates/aster/src/agents/agent.rs @@ -600,6 +600,128 @@ fn extract_tool_result_metadata(result: &T) -> Option) -> Vec { + match value { + Some(Value::Array(items)) => items + .iter() + .filter_map(Value::as_str) + .map(str::trim) + .filter(|item| !item.is_empty()) + .map(str::to_string) + .collect(), + Some(Value::String(item)) => item + .split(',') + .map(str::trim) + .filter(|item| !item.is_empty()) + .map(str::to_string) + .collect(), + _ => Vec::new(), + } +} + +fn metadata_string_matches(actual: Option<&Value>, expected: &str) -> bool { + actual + .and_then(Value::as_str) + .map(str::trim) + .is_some_and(|actual| actual.eq_ignore_ascii_case(expected.trim())) +} + +fn tool_result_metadata_matches_stop_rule(metadata: &Value, rule: &Value) -> bool { + let Some(rule) = rule.as_object() else { + return false; + }; + + if let Some(expected) = rule + .get("metadata_equals") + .or_else(|| rule.get("metadataEquals")) + .and_then(Value::as_object) + { + let all_matched = expected.iter().all(|(key, expected_value)| { + expected_value + .as_str() + .map(|expected_text| metadata_string_matches(metadata.get(key), expected_text)) + .unwrap_or_else(|| metadata.get(key) == Some(expected_value)) + }); + if !all_matched { + return false; + } + } + + let required_keys = string_list_from_metadata( + rule.get("require_any") + .or_else(|| rule.get("requireAny")) + .or_else(|| rule.get("required_any")) + .or_else(|| rule.get("requiredAny")), + ); + if !required_keys.is_empty() { + let has_required = required_keys.iter().any(|key| { + metadata.get(key).is_some_and(|value| match value { + Value::Null => false, + Value::String(text) => !text.trim().is_empty(), + _ => true, + }) + }); + if !has_required { + return false; + } + } + + let statuses = string_list_from_metadata( + rule.get("statuses") + .or_else(|| rule.get("status_values")) + .or_else(|| rule.get("statusValues")), + ); + if !statuses.is_empty() { + let Some(actual_status) = metadata + .get("status") + .and_then(Value::as_str) + .map(str::trim) + else { + return false; + }; + if !statuses + .iter() + .any(|expected| actual_status.eq_ignore_ascii_case(expected)) + { + return false; + } + } + + true +} + +fn should_stop_after_tool_result( + turn_context: Option<&TurnContextOverride>, + messages_to_add: &Conversation, +) -> bool { + let Some(stop_rule) = turn_context + .and_then(|context| context.metadata.get("runtime_control")) + .or_else(|| turn_context.and_then(|context| context.metadata.get("runtimeControl"))) + .and_then(|control| { + control + .get("stop_after_tool_result") + .or_else(|| control.get("stopAfterToolResult")) + }) + else { + return false; + }; + + messages_to_add.messages().iter().any(|message| { + message.content.iter().any(|content| { + let MessageContent::ToolResponse(tool_response) = content else { + return false; + }; + let Ok(result) = tool_response.tool_result.as_ref() else { + return false; + }; + let Some(metadata) = extract_tool_result_metadata(result) else { + return false; + }; + tool_result_metadata_matches_stop_rule(&metadata, stop_rule) + }) + }) +} + fn native_tool_metadata_to_value( metadata: std::collections::HashMap, ) -> Option { @@ -1061,7 +1183,12 @@ impl TurnItemRuntimeProjector { } fn project_user_input(&mut self, turn: &TurnRuntime) -> Option { - let content = turn.input_text.as_ref()?.trim(); + let content = turn + .context_override + .as_ref() + .and_then(|context| context.user_visible_input_text.as_deref()) + .or(turn.input_text.as_deref())? + .trim(); if content.is_empty() { return None; } @@ -3421,7 +3548,15 @@ impl Agent { let scope_session_config = session_config.clone(); let stream_session_config = session_config.clone(); let scoped_session_config = session_config.clone(); - let input_text_for_turn = (!message_text.trim().is_empty()).then_some(message_text.clone()); + let input_text_for_turn = scoped_session_config + .turn_context + .as_ref() + .and_then(|context| context.user_visible_input_text.as_deref()) + .and_then(|input_text| { + let value = input_text.trim(); + (!value.is_empty()).then(|| value.to_string()) + }) + .or_else(|| (!message_text.trim().is_empty()).then_some(message_text.clone())); Ok(Self::scope_reply_stream( &scope_session_config, @@ -4133,6 +4268,11 @@ impl Agent { } } } + } else if should_stop_after_tool_result( + session_config.turn_context.as_ref(), + &messages_to_add, + ) { + exit_chat = true; } for msg in &messages_to_add { @@ -4964,6 +5104,38 @@ mod tests { ); } + #[test] + fn test_project_user_input_prefers_user_visible_turn_context() { + let turn = TurnRuntime::new( + "turn-skill-visible", + "session-1", + "thread-1", + Some("内部结构化执行输入".to_string()), + Some(TurnContextOverride { + user_visible_input_text: Some( + "@analysis 帮我分析一下今天的国际形势".to_string(), + ), + ..TurnContextOverride::default() + }), + ); + let mut projector = TurnItemRuntimeProjector::new(&turn); + + let event = projector + .project_user_input(&turn) + .expect("user input event"); + + assert!(matches!( + event, + AgentEvent::ItemStarted { item } + if item.id == "user_input:turn-skill-visible" + && matches!( + item.payload, + ItemRuntimePayload::UserInput { ref text } + if text == "@analysis 帮我分析一下今天的国际形势" + ) + )); + } + #[test] fn test_project_tool_response_emits_file_artifact_runtime_item() { let turn = TurnRuntime::new( @@ -6851,6 +7023,86 @@ mod tests { assert!(tool_surface_updated_from_call_tool_result(&call_result)); } + #[test] + fn test_should_stop_after_tool_result_matches_forwarded_image_task_metadata() { + let turn_context = TurnContextOverride { + metadata: HashMap::from([( + "runtime_control".to_string(), + serde_json::json!({ + "stop_after_tool_result": { + "metadata_equals": { + "skill_forwarded_tool_name": "lime_create_image_generation_task", + "task_type": "image_generate" + }, + "require_any": ["task_id", "artifact_path", "path"], + "statuses": ["pending_submit", "queued", "running", "partial", "succeeded"] + } + }), + )]), + ..TurnContextOverride::default() + }; + let mut messages = Conversation::default(); + messages.push(Message::user().with_tool_response( + "call-image-skill", + Ok(CallToolResult { + content: vec![Content::text("图片任务已创建")], + structured_content: Some(serde_json::json!({ + "skill_forwarded_tool_name": "lime_create_image_generation_task", + "task_type": "image_generate", + "task_id": "task-image-1", + "status": "pending_submit" + })), + is_error: Some(false), + meta: None, + }), + )); + + assert!(should_stop_after_tool_result( + Some(&turn_context), + &messages + )); + } + + #[test] + fn test_should_stop_after_tool_result_ignores_unmatched_metadata() { + let turn_context = TurnContextOverride { + metadata: HashMap::from([( + "runtime_control".to_string(), + serde_json::json!({ + "stop_after_tool_result": { + "metadata_equals": { + "skill_forwarded_tool_name": "lime_create_image_generation_task", + "task_type": "image_generate" + }, + "require_any": ["task_id"], + "statuses": ["pending_submit"] + } + }), + )]), + ..TurnContextOverride::default() + }; + let mut messages = Conversation::default(); + messages.push(Message::user().with_tool_response( + "call-other", + Ok(CallToolResult { + content: vec![Content::text("其它工具完成")], + structured_content: Some(serde_json::json!({ + "skill_forwarded_tool_name": "lime_create_cover_generation_task", + "task_type": "cover_generate", + "task_id": "task-cover-1", + "status": "pending_submit" + })), + is_error: Some(false), + meta: None, + }), + )); + + assert!(!should_stop_after_tool_result( + Some(&turn_context), + &messages + )); + } + #[tokio::test] async fn test_agent_register_mcp_tool() -> Result<()> { let agent = Agent::new(); diff --git a/src-tauri/crates/aster-rust/crates/aster/src/session/runtime_store.rs b/src-tauri/crates/aster-rust/crates/aster/src/session/runtime_store.rs index 3a5a3717f..14ba14863 100644 --- a/src-tauri/crates/aster-rust/crates/aster/src/session/runtime_store.rs +++ b/src-tauri/crates/aster-rust/crates/aster/src/session/runtime_store.rs @@ -39,6 +39,8 @@ pub struct TurnContextOverride { #[serde(skip_serializing_if = "Option::is_none")] pub collaboration_mode: Option, #[serde(skip_serializing_if = "Option::is_none")] + pub user_visible_input_text: Option, + #[serde(skip_serializing_if = "Option::is_none")] #[schema(value_type = Object)] pub output_schema: Option, #[serde(skip, default)] diff --git a/src-tauri/crates/core/src/config/mod.rs b/src-tauri/crates/core/src/config/mod.rs index 8bcbf5d0b..dcf354a67 100644 --- a/src-tauri/crates/core/src/config/mod.rs +++ b/src-tauri/crates/core/src/config/mod.rs @@ -37,14 +37,14 @@ pub use types::{ MultiSearchEngineEntryConfig, NativeAgentConfig, NavigationConfig, OpenAIAsrConfig, PairingSettings, ProviderConfig, ProviderModelsConfig, ProvidersConfig, QuotaExceededConfig, RateLimitSettings, RemoteManagementConfig, ResponseCacheSettings, RetrySettings, RoutingConfig, - ScreenshotChatConfig, SearchEngine, SenseVoiceLocalConfig, ServerConfig, - ShellEnvironmentImportConfig, TaskSchedule, TelegramAccountConfig, TelegramBotConfig, - TelegramGroupConfig, TelegramTopicConfig, TlsConfig, ToolCallingConfig, - ToolExecutionOverrideConfig, ToolExecutionPolicyConfig, ToolExecutionRestrictionProfileConfig, - ToolExecutionSandboxProfileConfig, ToolExecutionWarningPolicyConfig, UpdateCheckConfig, - UserProfile, VertexApiKeyEntry, VertexModelAlias, VoiceInputConfig, VoiceInstruction, - VoiceOutputConfig, VoiceOutputMode, VoiceProcessorConfig, WebSearchConfig, WebSearchProvider, - WechatAccountConfig, WechatBotConfig, WechatGroupConfig, WhisperLocalConfig, WhisperModelSize, - WorkspacePreferencesConfig, WorkspaceSandboxConfig, XunfeiConfig, DEFAULT_API_KEY, + SearchEngine, SenseVoiceLocalConfig, ServerConfig, ShellEnvironmentImportConfig, TaskSchedule, + TelegramAccountConfig, TelegramBotConfig, TelegramGroupConfig, TelegramTopicConfig, TlsConfig, + ToolCallingConfig, ToolExecutionOverrideConfig, ToolExecutionPolicyConfig, + ToolExecutionRestrictionProfileConfig, ToolExecutionSandboxProfileConfig, + ToolExecutionWarningPolicyConfig, UpdateCheckConfig, UserProfile, VertexApiKeyEntry, + VertexModelAlias, VoiceInputConfig, VoiceInstruction, VoiceOutputConfig, VoiceOutputMode, + VoiceProcessorConfig, WebSearchConfig, WebSearchProvider, WechatAccountConfig, WechatBotConfig, + WechatGroupConfig, WhisperLocalConfig, WhisperModelSize, WorkspacePreferencesConfig, + WorkspaceSandboxConfig, XunfeiConfig, DEFAULT_API_KEY, }; pub use yaml::{load_config, save_config, ConfigError, ConfigManager, YamlService}; diff --git a/src-tauri/crates/core/src/config/types.rs b/src-tauri/crates/core/src/config/types.rs index eda1a29be..81a60e24a 100644 --- a/src-tauri/crates/core/src/config/types.rs +++ b/src-tauri/crates/core/src/config/types.rs @@ -895,23 +895,6 @@ fn normalize_navigation_enabled_items(items: &[String]) -> Vec { // ============ 实验室功能配置类型 ============ -/// 截图对话功能配置 -/// -/// 配置截图对话功能的开关和快捷键 -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct ScreenshotChatConfig { - /// 是否启用截图对话功能 - #[serde(default)] - pub enabled: bool, - /// 触发截图的全局快捷键 - #[serde(default = "default_screenshot_shortcut")] - pub shortcut: String, -} - -fn default_screenshot_shortcut() -> String { - "CommandOrControl+Alt+Q".to_string() -} - /// 自动更新检查配置 /// /// 配置自动检查更新的行为,符合 macOS/Windows 平台规范 @@ -998,15 +981,6 @@ impl Default for UpdateCheckConfig { } } -impl Default for ScreenshotChatConfig { - fn default() -> Self { - Self { - enabled: false, - shortcut: default_screenshot_shortcut(), - } - } -} - /// WebMCP 预留配置 /// /// 当前仅作为实验开关预留,不参与实际执行链。 @@ -1022,9 +996,6 @@ pub struct WebMcpConfig { /// 管理所有实验性功能的开关和配置 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] pub struct ExperimentalFeatures { - /// 截图对话功能配置 - #[serde(default)] - pub screenshot_chat: ScreenshotChatConfig, /// WebMCP 预留配置 #[serde(default)] pub webmcp: WebMcpConfig, @@ -1099,9 +1070,6 @@ pub struct VoiceInputConfig { /// 是否启用交互音效 #[serde(default = "default_sound_enabled")] pub sound_enabled: bool, - /// 翻译模式快捷键(可选) - #[serde(default, skip_serializing_if = "Option::is_none")] - pub translate_shortcut: Option, /// 翻译模式使用的指令 ID #[serde(default = "default_translate_instruction_id")] pub translate_instruction_id: String, @@ -1130,7 +1098,6 @@ impl Default for VoiceInputConfig { asr_credentials: Vec::new(), selected_device_id: None, sound_enabled: default_sound_enabled(), - translate_shortcut: None, translate_instruction_id: default_translate_instruction_id(), } } @@ -3359,13 +3326,6 @@ mod unit_tests { assert_eq!(parsed, config); } - #[test] - fn test_screenshot_chat_config_default() { - let config = ScreenshotChatConfig::default(); - assert!(!config.enabled); - assert_eq!(config.shortcut, "CommandOrControl+Alt+Q"); - } - #[test] fn test_webmcp_config_default() { let config = WebMcpConfig::default(); @@ -3375,18 +3335,12 @@ mod unit_tests { #[test] fn test_experimental_features_default() { let config = ExperimentalFeatures::default(); - assert!(!config.screenshot_chat.enabled); - assert_eq!(config.screenshot_chat.shortcut, "CommandOrControl+Alt+Q"); assert!(!config.webmcp.enabled); } #[test] fn test_experimental_features_serialization() { let config = ExperimentalFeatures { - screenshot_chat: ScreenshotChatConfig { - enabled: true, - shortcut: "CommandOrControl+Alt+X".to_string(), - }, webmcp: WebMcpConfig { enabled: true }, ..Default::default() }; @@ -3394,7 +3348,7 @@ mod unit_tests { let yaml = serde_yaml::to_string(&config).unwrap(); assert!(yaml.contains("webmcp")); assert!(yaml.contains("enabled: true")); - assert!(yaml.contains("shortcut: CommandOrControl+Alt+X")); + assert!(!yaml.contains("screenshot_chat")); let parsed: ExperimentalFeatures = serde_yaml::from_str(&yaml).unwrap(); assert_eq!(parsed, config); @@ -3434,11 +3388,6 @@ mod unit_tests { #[test] fn test_config_with_experimental() { let config = Config::default(); - assert!(!config.experimental.screenshot_chat.enabled); - assert_eq!( - config.experimental.screenshot_chat.shortcut, - "CommandOrControl+Alt+Q" - ); assert!(!config.experimental.webmcp.enabled); // 语音输入测试 assert!(!config.experimental.voice_input.enabled); diff --git a/src-tauri/crates/services/src/lib.rs b/src-tauri/crates/services/src/lib.rs index e95821884..0ced21634 100644 --- a/src-tauri/crates/services/src/lib.rs +++ b/src-tauri/crates/services/src/lib.rs @@ -21,8 +21,6 @@ //! - `voice_asr_service` - ASR 识别服务 //! - `voice_command_service` - 语音命令业务服务 //! - `voice_recording_service` - 录音状态与设备服务 -//! - `screenshot_capture_service` - 跨平台截图服务 -//! - `screenshot_image_service` - 截图图片编码服务 //! - `machine_id_service` - 机器 ID 服务 //! - `live_sync` - 实时同步 //! - `mcp_sync` - MCP 同步 @@ -43,8 +41,6 @@ // 无外部依赖的服务 pub mod context_memory_service; pub mod file_browser_service; -pub mod screenshot_capture_service; -pub mod screenshot_image_service; pub mod sysinfo_service; pub mod update_check_service; pub mod voice_asr_service; diff --git a/src-tauri/crates/services/src/screenshot_capture_service.rs b/src-tauri/crates/services/src/screenshot_capture_service.rs deleted file mode 100644 index 1c1d1ffb1..000000000 --- a/src-tauri/crates/services/src/screenshot_capture_service.rs +++ /dev/null @@ -1,244 +0,0 @@ -//! 屏幕截图服务 -//! -//! 提供跨平台的屏幕截图功能,支持交互式区域选择 - -use std::path::PathBuf; -use tracing::{debug, error, info}; - -#[cfg(target_os = "macos")] -use std::path::Path; - -#[cfg(target_os = "macos")] -use tracing::warn; - -/// 截图错误类型 -#[derive(Debug, thiserror::Error)] -pub enum CaptureError { - #[error("用户取消截图")] - Cancelled, - #[error("权限不足: {0}")] - PermissionDenied(String), - #[error("系统错误: {0}")] - SystemError(String), - #[error("IO 错误: {0}")] - IoError(#[from] std::io::Error), - #[error("临时文件创建失败: {0}")] - TempFileError(String), -} - -/// 截图结果 -#[derive(Debug)] -pub enum CaptureResult { - /// 截图成功,包含图片路径 - Success(PathBuf), - /// 用户取消截图 - Cancelled, -} - -/// 启动交互式截图 -/// -/// 调用系统截图工具进行交互式区域选择 -/// -/// # 返回 -/// 成功返回图片路径,用户取消返回 Cancelled 错误,其他情况返回相应错误 -/// -/// # 平台支持 -/// - macOS: 使用 `screencapture -i -x` 命令 -/// - Windows: 使用 Windows API 或系统截图工具 -/// - Linux: 使用 `gnome-screenshot` 或 `scrot` -pub async fn start_capture() -> Result { - info!("启动交互式截图"); - - // 生成临时文件路径 - let temp_dir = std::env::temp_dir(); - let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S_%3f"); - let filename = format!("lime_screenshot_{timestamp}.png"); - let temp_path = temp_dir.join(&filename); - - debug!("截图临时文件路径: {:?}", temp_path); - - // 根据平台调用不同的截图命令 - #[cfg(target_os = "macos")] - { - capture_macos(&temp_path).await?; - } - - #[cfg(target_os = "windows")] - { - capture_windows(&temp_path).await?; - } - - #[cfg(target_os = "linux")] - { - capture_linux(&temp_path).await?; - } - - // 检查文件是否存在(用户可能取消了截图) - if !temp_path.exists() { - info!("截图文件不存在,用户可能取消了截图"); - return Err(CaptureError::Cancelled); - } - - // 检查文件大小(空文件表示取消) - let metadata = std::fs::metadata(&temp_path)?; - if metadata.len() == 0 { - info!("截图文件为空,用户取消了截图"); - std::fs::remove_file(&temp_path)?; - return Err(CaptureError::Cancelled); - } - - info!("截图成功: {:?}", temp_path); - Ok(temp_path) -} - -/// macOS 截图实现 -#[cfg(target_os = "macos")] -async fn capture_macos(output_path: &Path) -> Result<(), CaptureError> { - use std::process::Command; - - debug!("使用 macOS screencapture 命令"); - - // 先检查屏幕录制权限 - // 通过尝试执行一个快速的全屏截图到 /dev/null 来检测权限 - let permission_check = Command::new("screencapture") - .args(["-x", "-c"]) // -c 截图到剪贴板,快速检测权限 - .output(); - - if let Ok(output) = permission_check { - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - if stderr.contains("cannot") || stderr.contains("permission") { - warn!("屏幕录制权限不足: {}", stderr); - return Err(CaptureError::PermissionDenied( - "请在系统设置 → 隐私与安全性 → 录屏与系统录音 中授权 Lime".to_string(), - )); - } - } - } - - // 使用 screencapture 命令 - // -i: 交互式选择区域 - // -x: 不播放截图声音 - let output = Command::new("screencapture") - .args(["-i", "-x", output_path.to_str().unwrap()]) - .output() - .map_err(|e| CaptureError::SystemError(format!("执行 screencapture 失败: {e}")))?; - - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - // 用户按 ESC 取消时,screencapture 返回非零状态但不创建文件 - if !output_path.exists() { - return Err(CaptureError::Cancelled); - } - error!("screencapture 命令失败: {}", stderr); - return Err(CaptureError::SystemError(format!( - "screencapture 失败: {stderr}" - ))); - } - - Ok(()) -} - -/// Windows 截图实现 -#[cfg(target_os = "windows")] -async fn capture_windows(output_path: &PathBuf) -> Result<(), CaptureError> { - use std::process::Command; - - debug!("使用 Windows 截图工具"); - - // 使用 PowerShell 调用截图功能 - // 注意:这是一个简化实现,实际可能需要使用 Windows API - let script = format!( - r#" - Add-Type -AssemblyName System.Windows.Forms - $screen = [System.Windows.Forms.Screen]::PrimaryScreen - $bitmap = New-Object System.Drawing.Bitmap($screen.Bounds.Width, $screen.Bounds.Height) - $graphics = [System.Drawing.Graphics]::FromImage($bitmap) - $graphics.CopyFromScreen($screen.Bounds.Location, [System.Drawing.Point]::Empty, $screen.Bounds.Size) - $bitmap.Save('{}') - "#, - output_path.to_str().unwrap().replace("\\", "\\\\") - ); - - let output = Command::new("powershell") - .args(["-Command", &script]) - .output() - .map_err(|e| CaptureError::SystemError(format!("执行 PowerShell 失败: {}", e)))?; - - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - error!("PowerShell 截图失败: {}", stderr); - return Err(CaptureError::SystemError(format!("截图失败: {}", stderr))); - } - - Ok(()) -} - -/// Linux 截图实现 -#[cfg(target_os = "linux")] -async fn capture_linux(output_path: &PathBuf) -> Result<(), CaptureError> { - use std::process::Command; - - debug!("使用 Linux 截图工具"); - - // 尝试使用 gnome-screenshot - let gnome_result = Command::new("gnome-screenshot") - .args(["-a", "-f", output_path.to_str().unwrap()]) - .output(); - - if let Ok(output) = gnome_result { - if output.status.success() { - return Ok(()); - } - } - - // 回退到 scrot - let scrot_result = Command::new("scrot") - .args(["-s", output_path.to_str().unwrap()]) - .output() - .map_err(|e| { - CaptureError::SystemError(format!( - "无法找到截图工具 (gnome-screenshot 或 scrot): {}", - e - )) - })?; - - if !scrot_result.status.success() { - let stderr = String::from_utf8_lossy(&scrot_result.stderr); - if !output_path.exists() { - return Err(CaptureError::Cancelled); - } - return Err(CaptureError::SystemError(format!("scrot 失败: {}", stderr))); - } - - Ok(()) -} - -/// 清理临时截图文件 -/// -/// # 参数 -/// - `path`: 要删除的文件路径 -pub fn cleanup_temp_file(path: &PathBuf) { - if path.exists() { - if let Err(e) = std::fs::remove_file(path) { - error!("删除临时截图文件失败: {}", e); - } else { - debug!("已删除临时截图文件: {:?}", path); - } - } -} - -#[cfg(test)] -mod tests { - - #[test] - fn test_temp_path_generation() { - let temp_dir = std::env::temp_dir(); - let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S_%3f"); - let filename = format!("lime_screenshot_{timestamp}.png"); - let temp_path = temp_dir.join(&filename); - - assert!(temp_path.to_str().unwrap().contains("lime_screenshot_")); - assert!(temp_path.to_str().unwrap().ends_with(".png")); - } -} diff --git a/src-tauri/crates/services/src/screenshot_image_service.rs b/src-tauri/crates/services/src/screenshot_image_service.rs deleted file mode 100644 index 59c8c1cb9..000000000 --- a/src-tauri/crates/services/src/screenshot_image_service.rs +++ /dev/null @@ -1,31 +0,0 @@ -//! 截图图片处理服务 -//! -//! 提供截图文件读取与 Base64 编码能力。 - -use base64::{engine::general_purpose::STANDARD, Engine}; -use std::path::Path; -use tokio::fs; - -/// 读取图片文件并转换为 Base64 -pub async fn read_image_as_base64(path: &str) -> Result { - tracing::debug!("读取图片为 Base64: {}", path); - - let file_path = Path::new(path); - - if !file_path.exists() { - return Err(format!("文件不存在: {}", file_path.display())); - } - - let bytes = fs::read(file_path) - .await - .map_err(|e| format!("读取文件失败: {e}"))?; - - if bytes.is_empty() { - return Err("文件为空".to_string()); - } - - let base64 = STANDARD.encode(&bytes); - - tracing::debug!("图片读取成功,大小: {} 字节", bytes.len()); - Ok(base64) -} diff --git a/src-tauri/src/app/runner.rs b/src-tauri/src/app/runner.rs index 521bc5954..b9ea1179d 100644 --- a/src-tauri/src/app/runner.rs +++ b/src-tauri/src/app/runner.rs @@ -531,21 +531,6 @@ pub fn run() { }); } - // 初始化截图对话模块 - // _Requirements: 7.3_ - { - let app_handle = app.handle(); - match crate::screenshot::init(app_handle) { - Ok(()) => { - tracing::info!("[启动] 截图对话模块初始化成功"); - } - Err(e) => { - tracing::error!("[启动] 截图对话模块初始化失败: {}", e); - // 截图模块初始化失败不影响应用运行 - } - } - } - // 初始化语音输入模块 { let app_handle = app.handle(); @@ -1355,18 +1340,10 @@ pub fn run() { commands::webview_cmd::get_browser_event_buffer, commands::webview_cmd::browser_execute_action, commands::webview_cmd::get_browser_action_audit_logs, - // Screenshot Chat commands - // _Requirements: 1.1, 1.4, 1.5, 2.2, 2.4, 3.1, 5.1_ - commands::screenshot_cmd::get_experimental_config, - commands::screenshot_cmd::get_screenshot_shortcut_runtime_status, - commands::screenshot_cmd::save_experimental_config, - commands::screenshot_cmd::start_screenshot, - commands::screenshot_cmd::validate_shortcut, - commands::screenshot_cmd::update_screenshot_shortcut, - commands::screenshot_cmd::close_screenshot_chat_window, - commands::screenshot_cmd::open_input_with_text, - commands::screenshot_cmd::read_image_as_base64, - commands::screenshot_cmd::send_screenshot_chat, + // Experimental settings commands + commands::experimental_cmd::get_experimental_config, + commands::experimental_cmd::save_experimental_config, + commands::hotkey_cmd::validate_shortcut, // Update Check commands commands::update_cmd::check_update, commands::update_cmd::check_for_updates, @@ -1562,8 +1539,6 @@ pub fn run() { crate::voice::commands::get_voice_instructions, crate::voice::commands::save_voice_instruction, crate::voice::commands::delete_voice_instruction, - crate::voice::commands::open_voice_window, - crate::voice::commands::close_voice_window, crate::voice::commands::transcribe_audio, crate::voice::commands::polish_voice_text, crate::voice::commands::output_voice_text, diff --git a/src-tauri/src/commands/aster_agent_cmd/image_skill_launch.rs b/src-tauri/src/commands/aster_agent_cmd/image_skill_launch.rs index cd69a8f29..5247c7b3c 100644 --- a/src-tauri/src/commands/aster_agent_cmd/image_skill_launch.rs +++ b/src-tauri/src/commands/aster_agent_cmd/image_skill_launch.rs @@ -33,6 +33,18 @@ const IMAGE_SKILL_LAUNCH_DETOUR_DENY_PATTERNS: &[&str] = &[ "mcp__playwright__*", "playwright*", ]; +const IMAGE_SKILL_LAUNCH_MAIN_ALLOWED_TOOLS: &[&str] = &["Skill"]; +const IMAGE_SKILL_LAUNCH_RUNTIME_METADATA_KEY: &str = "lime_runtime"; +const IMAGE_SKILL_LAUNCH_TOOL_SURFACE_KEY: &str = "tool_surface"; +const IMAGE_SKILL_LAUNCH_RUNTIME_CONTROL_KEY: &str = "runtime_control"; +const IMAGE_SKILL_LAUNCH_STOP_AFTER_TOOL_RESULT_KEY: &str = "stop_after_tool_result"; +const IMAGE_SKILL_LAUNCH_DEFAULT_SKILL_NAME: &str = "image_generate"; + +#[derive(Debug, Clone)] +pub(crate) struct ImageSkillLaunchDirectTask { + pub tool_params: serde_json::Value, + pub skill_tool_arguments: String, +} fn extract_object_string( object: &serde_json::Map, @@ -46,6 +58,37 @@ fn extract_object_string( .map(str::to_string) } +fn insert_non_empty_string_if_missing( + record: &mut serde_json::Map, + key: &str, + value: Option<&str>, +) { + if record + .get(key) + .and_then(serde_json::Value::as_str) + .map(str::trim) + .filter(|item| !item.is_empty()) + .is_some() + { + return; + } + let Some(value) = value.map(str::trim).filter(|item| !item.is_empty()) else { + return; + }; + record.insert( + key.to_string(), + serde_json::Value::String(value.to_string()), + ); +} + +fn build_image_skill_launch_direct_skill_arguments(skill_name: &str) -> String { + serde_json::to_string(&serde_json::json!({ + "skill": skill_name, + "source": "image_skill_launch" + })) + .unwrap_or_else(|_| "{\"skill\":\"image_generate\"}".to_string()) +} + fn build_image_skill_input_ref(index: usize) -> String { format!("{IMAGE_SKILL_INPUT_REF_PREFIX}{}", index + 1) } @@ -215,6 +258,55 @@ fn ensure_image_skill_launch_workbench_chat_mode(value: &mut serde_json::Value) ); } +fn ensure_image_skill_launch_turn_tool_scope(value: &mut serde_json::Value) { + let Some(root) = value.as_object_mut() else { + return; + }; + let launch_container = root + .get("harness") + .and_then(serde_json::Value::as_object) + .unwrap_or(root); + let has_launch = ["image_skill_launch", "imageSkillLaunch"] + .iter() + .any(|key| { + launch_container + .get(*key) + .and_then(serde_json::Value::as_object) + .is_some() + }); + if !has_launch { + return; + } + + root.insert( + "tool_scope".to_string(), + serde_json::json!({ + "allowed_tools": IMAGE_SKILL_LAUNCH_MAIN_ALLOWED_TOOLS, + "source": "image_skill_launch" + }), + ); + root.insert( + IMAGE_SKILL_LAUNCH_RUNTIME_CONTROL_KEY.to_string(), + serde_json::json!({ + IMAGE_SKILL_LAUNCH_STOP_AFTER_TOOL_RESULT_KEY: { + "source": "image_skill_launch", + "metadata_equals": { + "skill_forwarded_tool_name": "lime_create_image_generation_task", + "task_type": "image_generate" + }, + "require_any": ["task_id", "artifact_path", "path"], + "statuses": ["pending_submit", "queued", "running", "partial", "succeeded"] + } + }), + ); + if let Some(runtime) = root + .get_mut(IMAGE_SKILL_LAUNCH_RUNTIME_METADATA_KEY) + .and_then(serde_json::Value::as_object_mut) + { + runtime.remove(IMAGE_SKILL_LAUNCH_TOOL_SURFACE_KEY); + } +} + fn ensure_image_generation_contract_metadata( launch: &mut serde_json::Map, ) { @@ -255,6 +347,7 @@ pub(crate) fn prepare_image_skill_launch_request_metadata( ) -> Option { let mut metadata = request_metadata.cloned()?; ensure_image_skill_launch_workbench_chat_mode(&mut metadata); + ensure_image_skill_launch_turn_tool_scope(&mut metadata); let Some(launch) = extract_harness_nested_object_mut( &mut metadata, @@ -284,6 +377,44 @@ pub(crate) fn prepare_image_skill_launch_request_metadata( Some(metadata) } +pub(crate) fn build_image_skill_launch_direct_task( + request_metadata: Option<&serde_json::Value>, + session_id: &str, + thread_id: &str, + turn_id: &str, + project_id: Option<&str>, +) -> Result, String> { + let Some(launch) = extract_harness_nested_object( + request_metadata, + &["image_skill_launch", "imageSkillLaunch"], + ) else { + return Ok(None); + }; + let kind = extract_object_string(launch, &["kind"]).unwrap_or_else(|| "image_task".to_string()); + if kind != "image_task" { + return Ok(None); + } + + let skill_name = extract_object_string(launch, &["skill_name", "skillName"]) + .unwrap_or_else(|| IMAGE_SKILL_LAUNCH_DEFAULT_SKILL_NAME.to_string()); + let mut image_task = launch + .get("image_task") + .and_then(serde_json::Value::as_object) + .cloned() + .ok_or_else(|| "图片技能启动缺少 image_task 上下文".to_string())?; + + insert_image_generation_contract_fields(&mut image_task); + insert_non_empty_string_if_missing(&mut image_task, "session_id", Some(session_id)); + insert_non_empty_string_if_missing(&mut image_task, "thread_id", Some(thread_id)); + insert_non_empty_string_if_missing(&mut image_task, "turn_id", Some(turn_id)); + insert_non_empty_string_if_missing(&mut image_task, "project_id", project_id); + + Ok(Some(ImageSkillLaunchDirectTask { + tool_params: serde_json::Value::Object(image_task), + skill_tool_arguments: build_image_skill_launch_direct_skill_arguments(&skill_name), + })) +} + pub(crate) fn merge_system_prompt_with_image_skill_launch( base_prompt: Option, request_metadata: Option<&serde_json::Value>, @@ -447,9 +578,9 @@ fn build_image_skill_launch_system_prompt( let presentation_json = truncate_json_value(image_task.get("presentation"), 2_000); let taste_context_json = truncate_json_value(image_task.get("taste_context"), 2_000); let args_payload = serde_json::json!({ - "user_input": raw_text + "user_input": prompt .clone() - .or(prompt.clone()) + .or(raw_text.clone()) .unwrap_or_else(|| "请根据当前要求执行图片任务".to_string()), "image_task": serde_json::Value::Object(image_task.clone()), }); @@ -477,18 +608,18 @@ fn build_image_skill_launch_system_prompt( serde_json::to_string(&args_json).unwrap_or_else(|_| "\"{}\"".to_string()) ), "- 当前回合已经显式知道要走图片技能主链,不要为了确认技能名、工具名或命令名再去调用 ToolSearch。".to_string(), - "- 当前主会话第一刀必须先调用 Skill(image_generate),但这不等于任务已经创建。".to_string(), + "- 当前主会话第一刀必须先调用 Skill(image_generate),且用户文本里的 @命令 / 模型标签不是 Skill 名称;不要把 @ 后面的展示名当成 Skill.skill 参数。".to_string(), "- 在 Skill(image_generate) 真正执行前,不要先走 ToolSearch / WebSearch / lime_search_web_images / Bash / Read / Write / Edit / Glob / Grep / 浏览器 MCP / Playwright 等通用工具发现、检索、联网搜图、脚本、文件或页面链路。".to_string(), - "- `lime_search_web_images` 只服务 @素材 的联网图片候选检索,不是 @配图/@Nanobanana Pro 的图片生成入口;当前回合禁止调用它。".to_string(), + "- `lime_search_web_images` 只服务 @素材 的联网图片候选检索,不是当前 @图片生成入口;当前回合禁止调用它。".to_string(), "- 不要搜索 “Skill image_generate”、“lime media image generate --json”、“lime_create_image_generation_task” 之类目录信息;当前 image_task 已经提供了足够上下文。".to_string(), "- 如果某个通用搜索/读文件工具因为 session policy 被拒绝,不要重复同类调用;应立即改为直调 Skill(image_generate)。".to_string(), - "- 如果 Skill(image_generate) 返回的 Lime 工具元数据里只有 allowed_tools=[\"lime_create_image_generation_task\"],而没有 task_id/path/status,说明任务尚未创建;当前主会话必须立刻继续调用 lime_create_image_generation_task。".to_string(), - "- 不要把 Skill(image_generate) success=true 误判成“任务已提交”;只有拿到 task_id、path,或 status=pending_submit/queued/running/partial/succeeded,才算真实提交成功。".to_string(), - "- 当前图片主链是 Skill(image_generate) -> lime_create_image_generation_task -> 标准 image task artifact + worker,不要停在 Skill 返回后直接写总结。".to_string(), + "- 不要把 Skill(image_generate) success=true 单独误判成任务已提交;但只要 Skill 返回的 Lime 工具元数据里出现 skill_forwarded_tool_name=lime_create_image_generation_task,或拿到 task_id/path/status,就表示真实图片任务已创建。".to_string(), + "- status=pending_submit 是标准 image task artifact 已创建、等待 worker 回流的合法状态;看到它后必须停止本回合工具调用,不要再次调用 Skill(image_generate),也不要改用“查看结果/提交/状态”等新参数重复创建第二个任务。".to_string(), + "- 当前图片主链是 Skill(image_generate) -> lime_create_image_generation_task -> 标准 image task artifact + worker;拿到任务元数据后立即收口,让同一条 assistant 消息里的任务轻卡继续展示进度与结果。".to_string(), "- 不要再通过 Bash 拼接 CLI 命令或临时 /tmp 任务文件替代 lime_create_image_generation_task。".to_string(), - "- 调用 lime_create_image_generation_task 时,必须把 image_task 对象本身直接作为工具参数提交;不要再包一层 {\"image_task\": ...},更不要把整个对象再次序列化成字符串。".to_string(), - "- 调用 lime_create_image_generation_task 时,统一使用 snake_case 字段名;不要把 anchorHint / providerId / projectId 这类 camelCase 同义字段与 snake_case 一起重复提交。".to_string(), - "- 调用 lime_create_image_generation_task 时,必须只提交标准 image task 参数;不要传 outputPath,不要把任务写成 markdown 文稿。".to_string(), + "- Skill 内部调用 lime_create_image_generation_task 时,必须把 image_task 对象本身直接作为工具参数提交;不要再包一层 {\"image_task\": ...},更不要把整个对象再次序列化成字符串。".to_string(), + "- Skill 内部调用 lime_create_image_generation_task 时,统一使用 snake_case 字段名;不要把 anchorHint / providerId / projectId 这类 camelCase 同义字段与 snake_case 一起重复提交。".to_string(), + "- Skill 内部调用 lime_create_image_generation_task 时,必须只提交标准 image task 参数;不要传 outputPath,不要把任务写成 markdown 文稿。".to_string(), "- 不要伪造“图片已生成完成”;在 task file 真正返回结果前,只能让工具轨迹展示任务已提交、排队或执行中,不要额外输出递交模板。".to_string(), "- 如果当前回合已经拿到任何图片任务结果,且结果里含 task_id、path,或 status=pending_submit/queued/running/partial/succeeded,说明任务已提交;不要再次调用 Skill(image_generate) 或重复创建第二个图片任务。".to_string(), "- 拿到上述任务结果后,不要再输出“任务类型 / 任务 ID / 任务文件 / 状态”这类提交摘要;让同一条 assistant 消息内的工具调用和图片任务轻卡继续展示进度与结果。".to_string(), diff --git a/src-tauri/src/commands/aster_agent_cmd/mod.rs b/src-tauri/src/commands/aster_agent_cmd/mod.rs index ef5ea5a60..5dfa1d535 100644 --- a/src-tauri/src/commands/aster_agent_cmd/mod.rs +++ b/src-tauri/src/commands/aster_agent_cmd/mod.rs @@ -484,8 +484,8 @@ pub(crate) use form_skill_launch::{ prepare_form_skill_launch_request_metadata, prune_form_skill_launch_detour_tools_from_registry, }; pub(crate) use image_skill_launch::{ - append_image_skill_launch_session_permissions, merge_system_prompt_with_image_skill_launch, - prepare_image_skill_launch_request_metadata, + append_image_skill_launch_session_permissions, build_image_skill_launch_direct_task, + merge_system_prompt_with_image_skill_launch, prepare_image_skill_launch_request_metadata, prune_image_skill_launch_detour_tools_from_registry, }; pub(crate) use mcp_bridge::{ensure_lime_mcp_servers_running, inject_mcp_extensions}; @@ -597,6 +597,7 @@ pub(crate) use tool_runtime::{ pub(crate) use tool_runtime::{ ensure_browser_mcp_tools_registered, ensure_creation_task_tools_registered, ensure_runtime_support_tools_registered, ensure_social_image_tool_registered, + submit_image_generation_task_value, }; #[cfg(test)] #[allow(unused_imports)] diff --git a/src-tauri/src/commands/aster_agent_cmd/request_model_resolution.rs b/src-tauri/src/commands/aster_agent_cmd/request_model_resolution.rs index 34882186f..05de45330 100644 --- a/src-tauri/src/commands/aster_agent_cmd/request_model_resolution.rs +++ b/src-tauri/src/commands/aster_agent_cmd/request_model_resolution.rs @@ -2620,6 +2620,14 @@ fn honors_explicit_model_lock_with_capability_check( ) && matches!(model_preference_source, RequestPreferenceSource::Request) } +fn should_allow_cross_provider_runtime_fallback( + provider_preference_source: RequestPreferenceSource, + model_preference_source: RequestPreferenceSource, +) -> bool { + !matches!(provider_preference_source, RequestPreferenceSource::Request) + && !matches!(model_preference_source, RequestPreferenceSource::Request) +} + fn should_reselect_for_runtime_capability_gap( resolved_model: Option<&EnhancedModelMetadata>, compatible_candidate_count: u32, @@ -3550,6 +3558,10 @@ pub(super) async fn resolve_runtime_request_provider_resolution( &provider_selector, session_context.as_ref(), )?; + let allow_runtime_fallback = should_allow_cross_provider_runtime_fallback( + provider_preference_source, + model_preference_source, + ); if matches!(provider_preference_source, RequestPreferenceSource::Session) { tracing::info!( @@ -3577,7 +3589,7 @@ pub(super) async fn resolve_runtime_request_provider_resolution( &provider_selector, &model_preference, model_preference_source, - matches!(model_preference_source, RequestPreferenceSource::Session), + allow_runtime_fallback, ) .await?; let limit_state = build_limit_state( @@ -3589,6 +3601,12 @@ pub(super) async fn resolve_runtime_request_provider_resolution( selection.capability_gap.clone(), { let mut notes = vec!["当前回合显式指定了 provider/model 偏好。".to_string()]; + if !allow_runtime_fallback { + notes.push( + "当前回合含显式 provider/model 选择,运行时不会静默切换到其他 Provider。" + .to_string(), + ); + } if selection.capability_gap_source.as_deref() == Some("explicit_model_lock") { notes.push( "显式用户模型锁定不满足当前 execution profile 的 routing slot,模型执行前必须阻断。" diff --git a/src-tauri/src/commands/aster_agent_cmd/request_model_resolution/tests.rs b/src-tauri/src/commands/aster_agent_cmd/request_model_resolution/tests.rs index 88d4c0b04..189c543fe 100644 --- a/src-tauri/src/commands/aster_agent_cmd/request_model_resolution/tests.rs +++ b/src-tauri/src/commands/aster_agent_cmd/request_model_resolution/tests.rs @@ -2126,6 +2126,30 @@ fn explicit_provider_preference_wins_over_session_fallback() { ); } +#[test] +fn explicit_request_preference_disables_cross_provider_runtime_fallback() { + assert!(!should_allow_cross_provider_runtime_fallback( + RequestPreferenceSource::Request, + RequestPreferenceSource::Session, + )); + assert!(!should_allow_cross_provider_runtime_fallback( + RequestPreferenceSource::Session, + RequestPreferenceSource::Request, + )); + assert!(!should_allow_cross_provider_runtime_fallback( + RequestPreferenceSource::Request, + RequestPreferenceSource::Request, + )); +} + +#[test] +fn session_recovered_preference_can_keep_runtime_fallback() { + assert!(should_allow_cross_provider_runtime_fallback( + RequestPreferenceSource::Session, + RequestPreferenceSource::Session, + )); +} + #[test] fn service_scene_model_preference_reads_complete_preference() { let metadata = serde_json::json!({ diff --git a/src-tauri/src/commands/aster_agent_cmd/runtime_turn.rs b/src-tauri/src/commands/aster_agent_cmd/runtime_turn.rs index 839dcbc09..69dcd732b 100644 --- a/src-tauri/src/commands/aster_agent_cmd/runtime_turn.rs +++ b/src-tauri/src/commands/aster_agent_cmd/runtime_turn.rs @@ -26,7 +26,7 @@ use crate::commands::modality_runtime_contracts::hydrate_limecore_policy_hits_fr use aster::agents::extension::PlatformExtensionContext; use aster::hooks::{CompactTrigger, SessionSource}; use aster::session::TurnContextOverride; -use aster::tools::{ConfigTool, SkillTool}; +use aster::tools::{ConfigTool, SkillTool, ToolContext}; use lime_agent::{build_diagnostics_runtime_status_metadata, AgentEvent as RuntimeAgentEvent}; use lime_core::database::dao::agent_timeline::{AgentThreadItemPayload, AgentThreadItemStatus}; use lime_core::workspace::WorkspaceSettings; @@ -264,10 +264,13 @@ async fn sync_runtime_skill_source_agent(session_id: &str, agent: &Agent) -> Res extension_manager: Some(Arc::downgrade(&donor.extension_manager)), }) .await; - donor - .inherit_runtime_tool_surface_from(agent) - .await - .map_err(|error| format!("继承 runtime tool surface 失败: {error}"))?; + if let Err(error) = donor.inherit_runtime_tool_surface_from(agent).await { + tracing::warn!( + "[AsterAgent] 继承 runtime extension 工具面失败,保留 native 工具面供 Skill 使用: session={}, error={}", + session_id, + error + ); + } SkillTool::register_source_agent_for_session(session_id.to_string(), Arc::new(donor)).await; Ok(()) } @@ -877,6 +880,177 @@ fn emit_service_skill_preload_runtime_events( ); } +fn build_image_skill_launch_tool_context( + workspace_root: &str, + session_id: &str, + thread_id: &str, + turn_id: &str, + project_id: Option<&str>, + content_id: Option<&str>, +) -> ToolContext { + let mut environment = HashMap::new(); + environment.insert("LIME_THREAD_ID".to_string(), thread_id.to_string()); + environment.insert("PROXYCAST_THREAD_ID".to_string(), thread_id.to_string()); + environment.insert("LIME_TURN_ID".to_string(), turn_id.to_string()); + environment.insert("PROXYCAST_TURN_ID".to_string(), turn_id.to_string()); + if let Some(project_id) = project_id.map(str::trim).filter(|value| !value.is_empty()) { + environment.insert("LIME_PROJECT_ID".to_string(), project_id.to_string()); + environment.insert("PROXYCAST_PROJECT_ID".to_string(), project_id.to_string()); + } + if let Some(content_id) = content_id.map(str::trim).filter(|value| !value.is_empty()) { + environment.insert("LIME_CONTENT_ID".to_string(), content_id.to_string()); + environment.insert("PROXYCAST_CONTENT_ID".to_string(), content_id.to_string()); + } + + ToolContext::new(Path::new(workspace_root).to_path_buf()) + .with_session_id(session_id.to_string()) + .with_environment(environment) +} + +fn image_skill_launch_agent_tool_result( + tool_result: aster::tools::ToolResult, +) -> lime_agent::AgentToolResult { + let success = tool_result.success; + let error = tool_result.error; + let mut metadata = tool_result.metadata; + let forwarded_metadata = metadata.clone(); + metadata.insert( + "skill_forwarded_tool_name".to_string(), + serde_json::json!(LIME_CREATE_IMAGE_TASK_TOOL_NAME), + ); + metadata.insert( + "skill_forwarded_tool_metadata".to_string(), + serde_json::json!(forwarded_metadata), + ); + metadata.insert( + "allowed_tools".to_string(), + serde_json::json!([LIME_CREATE_IMAGE_TASK_TOOL_NAME]), + ); + metadata.insert( + "command_name".to_string(), + serde_json::json!("user:image_generate"), + ); + metadata.insert( + "skill".to_string(), + serde_json::json!({ + "success": success, + "error": error.clone(), + "stepsCompleted": [], + }), + ); + + lime_agent::AgentToolResult { + success, + output: String::new(), + error, + images: None, + metadata: Some(metadata), + } +} + +fn image_skill_launch_failed_tool_result(error: &str) -> lime_agent::AgentToolResult { + lime_agent::AgentToolResult { + success: false, + output: String::new(), + error: Some(error.to_string()), + images: None, + metadata: Some(HashMap::from([ + ( + "skill_forwarded_tool_name".to_string(), + serde_json::json!(LIME_CREATE_IMAGE_TASK_TOOL_NAME), + ), + ( + "source".to_string(), + serde_json::json!("image_skill_launch"), + ), + ("task_type".to_string(), serde_json::json!("image_generate")), + ])), + } +} + +fn emit_image_skill_launch_tool_event( + app: &AppHandle, + event_name: &str, + timeline_recorder: &Arc>, + workspace_root: &str, + event: RuntimeAgentEvent, +) { + emit_runtime_side_event(app, event_name, timeline_recorder, workspace_root, event); +} + +fn execute_image_skill_launch_direct_task( + app: &AppHandle, + request: &AsterChatRequest, + timeline_recorder: &Arc>, + workspace_root: &str, + session_id: &str, + thread_id: &str, + turn_id: &str, + request_metadata: Option<&serde_json::Value>, +) -> Result { + let Some(task) = build_image_skill_launch_direct_task( + request_metadata, + session_id, + thread_id, + turn_id, + request.project_id.as_deref(), + )? + else { + return Ok(false); + }; + + let tool_id = format!("image-skill-launch:{turn_id}"); + emit_image_skill_launch_tool_event( + app, + &request.event_name, + timeline_recorder, + workspace_root, + RuntimeAgentEvent::ToolStart { + tool_name: "Skill".to_string(), + tool_id: tool_id.clone(), + arguments: Some(task.skill_tool_arguments), + }, + ); + + let context = build_image_skill_launch_tool_context( + workspace_root, + session_id, + thread_id, + turn_id, + request.project_id.as_deref(), + None, + ); + match submit_image_generation_task_value(app, &context, task.tool_params) { + Ok(tool_result) => { + emit_image_skill_launch_tool_event( + app, + &request.event_name, + timeline_recorder, + workspace_root, + RuntimeAgentEvent::ToolEnd { + tool_id, + result: image_skill_launch_agent_tool_result(tool_result), + }, + ); + Ok(true) + } + Err(error) => { + let message = format!("创建图片任务失败: {error}"); + emit_image_skill_launch_tool_event( + app, + &request.event_name, + timeline_recorder, + workspace_root, + RuntimeAgentEvent::ToolEnd { + tool_id, + result: image_skill_launch_failed_tool_result(&message), + }, + ); + Err(message) + } + } +} + fn build_artifact_document_warning_message( status: &str, fallback_used: bool, @@ -1522,6 +1696,59 @@ impl RuntimeTurnPreparedExecution { } } + match execute_image_skill_launch_direct_task( + app, + request, + &self.runtime_turn_execution_context.timeline_recorder, + workspace_root, + session_id, + self.thread_id(), + self.turn_id(), + request_metadata, + ) { + Ok(true) => { + return finalize_runtime_turn_result( + agent, + app, + state, + db, + &request.event_name, + &self.runtime_turn_execution_context.timeline_recorder, + workspace_root, + &self + .runtime_turn_execution_context + .runtime_status_session_config, + &self.runtime_turn_execution_context.profile_stream, + &self.runtime_turn_execution_context.task_profile_refs, + session_id, + request_metadata, + Ok(String::new()), + ) + .await; + } + Ok(false) => {} + Err(error) => { + return finalize_runtime_turn_result( + agent, + app, + state, + db, + &request.event_name, + &self.runtime_turn_execution_context.timeline_recorder, + workspace_root, + &self + .runtime_turn_execution_context + .runtime_status_session_config, + &self.runtime_turn_execution_context.profile_stream, + &self.runtime_turn_execution_context.task_profile_refs, + session_id, + request_metadata, + Err(error), + ) + .await; + } + } + self.runtime_turn_execution_context .execute_and_finalize( tracker, diff --git a/src-tauri/src/commands/aster_agent_cmd/tests.rs b/src-tauri/src/commands/aster_agent_cmd/tests.rs index 62a1c8e7e..50e34df17 100644 --- a/src-tauri/src/commands/aster_agent_cmd/tests.rs +++ b/src-tauri/src/commands/aster_agent_cmd/tests.rs @@ -5102,9 +5102,9 @@ mod tests { assert!(merged.contains("`lime_search_web_images` 只服务 @素材")); assert!(merged.contains("应立即改为直调 Skill(image_generate)")); assert!(merged.contains( - "如果 Skill(image_generate) 返回的 Lime 工具元数据里只有 allowed_tools=[\"lime_create_image_generation_task\"]" + "skill_forwarded_tool_name=lime_create_image_generation_task" )); - assert!(merged.contains("不要把 Skill(image_generate) success=true 误判成“任务已提交”")); + assert!(merged.contains("status=pending_submit 是标准 image task artifact 已创建")); assert!(merged.contains("Skill(image_generate) -> lime_create_image_generation_task -> 标准 image task artifact + worker")); assert!(merged.contains("不要再通过 Bash 拼接 CLI 命令或临时 /tmp 任务文件")); assert!(merged.contains( @@ -6757,6 +6757,20 @@ mod tests { harness.get("chat_mode").and_then(serde_json::Value::as_str), Some("workbench") ); + assert_eq!( + prepared + .pointer("/tool_scope/allowed_tools/0") + .and_then(serde_json::Value::as_str), + Some("Skill") + ); + assert_eq!( + prepared + .pointer( + "/runtime_control/stop_after_tool_result/metadata_equals/skill_forwarded_tool_name" + ) + .and_then(serde_json::Value::as_str), + Some("lime_create_image_generation_task") + ); let launch = harness .get("image_skill_launch") .and_then(serde_json::Value::as_object) @@ -6810,6 +6824,9 @@ mod tests { #[test] fn test_prepare_image_skill_launch_request_metadata_sets_workbench_chat_mode_without_images() { let metadata = serde_json::json!({ + "lime_runtime": { + "tool_surface": "compact_tools" + }, "harness": { "image_skill_launch": { "skill_name": "image_generate", @@ -6838,6 +6855,37 @@ mod tests { harness.get("chat_mode").and_then(serde_json::Value::as_str), Some("workbench") ); + assert_eq!( + prepared + .pointer("/tool_scope/source") + .and_then(serde_json::Value::as_str), + Some("image_skill_launch") + ); + assert_eq!( + prepared + .pointer("/tool_scope/allowed_tools/0") + .and_then(serde_json::Value::as_str), + Some("Skill") + ); + assert!( + prepared.pointer("/lime_runtime/tool_surface").is_none(), + "image launch must not keep compact tool surface because it hides Skill" + ); + assert_eq!( + prepared + .pointer("/runtime_control/stop_after_tool_result/source") + .and_then(serde_json::Value::as_str), + Some("image_skill_launch") + ); + assert!( + prepared + .pointer("/runtime_control/stop_after_tool_result/statuses") + .and_then(serde_json::Value::as_array) + .is_some_and(|items| items + .iter() + .any(|item| item.as_str() == Some("pending_submit"))), + "image launch must stop the provider loop once the task artifact is created" + ); let launch = harness .get("image_skill_launch") .and_then(serde_json::Value::as_object) @@ -6860,6 +6908,88 @@ mod tests { ); } + #[test] + fn test_build_image_skill_launch_direct_task_keeps_current_task_contract() { + let metadata = serde_json::json!({ + "harness": { + "image_skill_launch": { + "skill_name": "image_generate", + "kind": "image_task", + "image_task": { + "prompt": "一张城市春日照片", + "provider_id": "fal", + "model": "fal-ai/nano-banana-pro", + "presentation": { + "assistant_intro": "好啊\n先获取下工具参数\n马上生成" + }, + "taste_context": { + "version": "lime-image-taste-v1" + } + } + } + } + }); + + let direct = build_image_skill_launch_direct_task( + Some(&metadata), + "session-image", + "thread-image", + "turn-image", + Some("project-image"), + ) + .expect("direct task result") + .expect("direct image task"); + let params = direct + .tool_params + .as_object() + .expect("tool params should be flat object"); + + assert_eq!( + params.get("prompt").and_then(serde_json::Value::as_str), + Some("一张城市春日照片") + ); + assert_eq!( + params.get("provider_id").and_then(serde_json::Value::as_str), + Some("fal") + ); + assert_eq!( + params.get("model").and_then(serde_json::Value::as_str), + Some("fal-ai/nano-banana-pro") + ); + assert_eq!( + params.get("session_id").and_then(serde_json::Value::as_str), + Some("session-image") + ); + assert_eq!( + params.get("thread_id").and_then(serde_json::Value::as_str), + Some("thread-image") + ); + assert_eq!( + params.get("turn_id").and_then(serde_json::Value::as_str), + Some("turn-image") + ); + assert_eq!( + params.get("project_id").and_then(serde_json::Value::as_str), + Some("project-image") + ); + assert_eq!( + params + .get("modality_contract_key") + .and_then(serde_json::Value::as_str), + Some("image_generation") + ); + let skill_args: serde_json::Value = + serde_json::from_str(&direct.skill_tool_arguments).expect("skill args json"); + assert_eq!( + skill_args.get("skill").and_then(serde_json::Value::as_str), + Some("image_generate") + ); + assert!( + !direct.skill_tool_arguments.contains("一张城市春日照片"), + "Skill trace arguments should not expose the full prompt in chat chrome" + ); + } + #[test] fn test_prepare_service_scene_launch_request_metadata_sets_workbench_chat_mode() { let metadata = serde_json::json!({ diff --git a/src-tauri/src/commands/aster_agent_cmd/tool_runtime.rs b/src-tauri/src/commands/aster_agent_cmd/tool_runtime.rs index 65cb98fd1..d3a51ee03 100644 --- a/src-tauri/src/commands/aster_agent_cmd/tool_runtime.rs +++ b/src-tauri/src/commands/aster_agent_cmd/tool_runtime.rs @@ -28,7 +28,9 @@ mod workspace_tools; pub(crate) use browser_tools::ensure_browser_mcp_tools_registered; #[allow(unused_imports)] pub(crate) use browser_tools::LimeBrowserMcpTool; -pub(crate) use creation_tools::ensure_creation_task_tools_registered; +pub(crate) use creation_tools::{ + ensure_creation_task_tools_registered, submit_image_generation_task_value, +}; pub(crate) use mcp_resource_tools::ensure_mcp_resource_tools_registered; #[allow(unused_imports)] pub(crate) use mcp_resource_tools::{ListMcpResourcesBridgeTool, ReadMcpResourceBridgeTool}; diff --git a/src-tauri/src/commands/aster_agent_cmd/tool_runtime/creation_tools.rs b/src-tauri/src/commands/aster_agent_cmd/tool_runtime/creation_tools.rs index 30a9f91cc..46aba1407 100644 --- a/src-tauri/src/commands/aster_agent_cmd/tool_runtime/creation_tools.rs +++ b/src-tauri/src/commands/aster_agent_cmd/tool_runtime/creation_tools.rs @@ -1350,6 +1350,23 @@ fn submit_image_generation_task_record( )) } +pub(crate) fn submit_image_generation_task_value( + app_handle: &AppHandle, + context: &ToolContext, + params: serde_json::Value, +) -> Result { + let normalized_params = + normalize_flat_image_task_tool_params(normalize_image_task_tool_params(params)?)?; + let input: ImageTaskInput = serde_json::from_value(normalized_params) + .map_err(|error| ToolError::invalid_params(format!("参数解析失败: {error}")))?; + if input.prompt.trim().is_empty() { + return Err(ToolError::invalid_params( + "prompt 不能为空字符串".to_string(), + )); + } + submit_image_generation_task_record(app_handle, context, input) +} + #[async_trait] impl Tool for LimeCreateImageTaskTool { fn name(&self) -> &str { @@ -1369,16 +1386,7 @@ impl Tool for LimeCreateImageTaskTool { params: serde_json::Value, context: &ToolContext, ) -> Result { - let normalized_params = - normalize_flat_image_task_tool_params(normalize_image_task_tool_params(params)?)?; - let input: ImageTaskInput = serde_json::from_value(normalized_params) - .map_err(|error| ToolError::invalid_params(format!("参数解析失败: {error}")))?; - if input.prompt.trim().is_empty() { - return Err(ToolError::invalid_params( - "prompt 不能为空字符串".to_string(), - )); - } - submit_image_generation_task_record(&self.app_handle, context, input) + submit_image_generation_task_value(&self.app_handle, context, params) } } diff --git a/src-tauri/src/commands/aster_agent_cmd/tool_runtime/media_cli_bridge.rs b/src-tauri/src/commands/aster_agent_cmd/tool_runtime/media_cli_bridge.rs index df750af34..59ba06c2b 100644 --- a/src-tauri/src/commands/aster_agent_cmd/tool_runtime/media_cli_bridge.rs +++ b/src-tauri/src/commands/aster_agent_cmd/tool_runtime/media_cli_bridge.rs @@ -92,6 +92,9 @@ pub(crate) fn emit_media_creation_task_event(app_handle: &AppHandle, output: &Me "persona_context": payload_record.get("persona_context").cloned(), "presentation": payload_record.get("presentation").cloned(), "taste_context": payload_record.get("taste_context").cloned(), + "provider": read_payload_string(payload_record, &["provider", "providerName", "provider_name"]), + "provider_id": read_payload_string(payload_record, &["provider_id", "providerId"]), + "model": read_payload_string(payload_record, &["model", "modelName", "model_name"]), "session_id": read_payload_string(payload_record, &["session_id", "sessionId"]), "thread_id": read_payload_string(payload_record, &["thread_id", "threadId"]), "turn_id": read_payload_string(payload_record, &["turn_id", "turnId"]), diff --git a/src-tauri/src/commands/experimental_cmd.rs b/src-tauri/src/commands/experimental_cmd.rs new file mode 100644 index 000000000..977808cf9 --- /dev/null +++ b/src-tauri/src/commands/experimental_cmd.rs @@ -0,0 +1,23 @@ +use crate::config::{ExperimentalFeatures, GlobalConfigManagerState}; +use tauri::{command, State}; + +#[command] +pub async fn get_experimental_config( + config_manager: State<'_, GlobalConfigManagerState>, +) -> Result { + Ok(config_manager.config().experimental.clone()) +} + +#[command] +pub async fn save_experimental_config( + config_manager: State<'_, GlobalConfigManagerState>, + experimental_config: ExperimentalFeatures, +) -> Result<(), String> { + let mut config = config_manager.config(); + config.experimental = experimental_config; + + config_manager + .save_config(&config) + .await + .map_err(|error| format!("保存实验室配置失败: {error}")) +} diff --git a/src-tauri/src/commands/hotkey_cmd.rs b/src-tauri/src/commands/hotkey_cmd.rs new file mode 100644 index 000000000..14a8f479e --- /dev/null +++ b/src-tauri/src/commands/hotkey_cmd.rs @@ -0,0 +1,46 @@ +use tauri::command; +use tauri_plugin_global_shortcut::Shortcut; + +#[command] +pub fn validate_shortcut(shortcut_str: String) -> Result { + if shortcut_str.trim().is_empty() { + return Err("快捷键不能为空".to_string()); + } + + shortcut_str + .parse::() + .map_err(|error| format!("无法解析快捷键 '{shortcut_str}': {error}"))?; + + if let Some(reason) = + crate::global_shortcut_guard::reserved_system_shortcut_reason(&shortcut_str) + { + return Err(reason.to_string()); + } + + Ok(true) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn validate_shortcut_accepts_common_global_shortcuts() { + assert!(validate_shortcut("CommandOrControl+Shift+S".to_string()).is_ok()); + assert!(validate_shortcut("Alt+F4".to_string()).is_ok()); + assert!(validate_shortcut("Ctrl+C".to_string()).is_ok()); + } + + #[test] + fn validate_shortcut_rejects_invalid_values() { + assert!(validate_shortcut("".to_string()).is_err()); + assert!(validate_shortcut("InvalidKey".to_string()).is_err()); + } + + #[test] + fn validate_shortcut_rejects_input_method_reserved_shortcuts() { + let result = validate_shortcut("CommandOrControl+Space".to_string()); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("输入法切换")); + } +} diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index b54deed7b..2add5eaa3 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -18,11 +18,13 @@ pub mod context_memory; pub mod document_import_cmd; pub mod ecommerce_review_reply_cmd; pub mod execution_run_cmd; +pub mod experimental_cmd; pub mod external_tools_cmd; pub mod file_upload_cmd; pub mod gallery_material_cmd; pub mod gateway_channel_cmd; pub mod gateway_tunnel_cmd; +pub mod hotkey_cmd; pub mod image_search_cmd; pub mod image_upload_cmd; pub mod injection_cmd; @@ -42,7 +44,6 @@ pub mod models_cmd; pub mod persona_cmd; pub mod prompt_cmd; pub mod sceneapp_cmd; -pub mod screenshot_cmd; pub mod security_perf_cmd; pub mod session_files_cmd; pub mod site_capability_cmd; diff --git a/src-tauri/src/commands/screenshot_cmd.rs b/src-tauri/src/commands/screenshot_cmd.rs deleted file mode 100644 index 0539b8795..000000000 --- a/src-tauri/src/commands/screenshot_cmd.rs +++ /dev/null @@ -1,360 +0,0 @@ -//! 截图对话命令模块 -//! -//! 提供截图对话功能的 Tauri 命令接口,包括: -//! - 获取和保存实验室功能配置 -//! - 启动截图 -//! - 验证和更新快捷键 -//! - 读取图片为 Base64 -//! -//! _需求: 1.1, 1.4, 1.5, 2.2, 2.4, 3.1, 5.1_ - -use crate::config::{ExperimentalFeatures, GlobalConfigManagerState}; -use crate::screenshot::{capture, shortcut}; -use tauri::{AppHandle, Emitter, Manager, State}; -use tracing::{debug, error, info}; - -#[derive(Debug, Clone, serde::Serialize)] -pub struct ScreenshotShortcutRuntimeStatus { - pub shortcut_registered: bool, - pub registered_shortcut: Option, -} - -/// 获取实验室功能配置 -/// -/// 从应用状态中获取当前的实验室功能配置 -/// -/// # 返回 -/// 成功返回 ExperimentalFeatures,失败返回错误信息 -/// -/// _需求: 1.1_ -#[tauri::command] -pub async fn get_experimental_config( - config_manager: State<'_, GlobalConfigManagerState>, -) -> Result { - debug!("获取实验室功能配置"); - - let config = config_manager.config(); - Ok(config.experimental.clone()) -} - -/// 获取截图快捷键运行时状态 -#[tauri::command] -pub async fn get_screenshot_shortcut_runtime_status( -) -> Result { - Ok(ScreenshotShortcutRuntimeStatus { - shortcut_registered: shortcut::is_registered(), - registered_shortcut: shortcut::get_current(), - }) -} - -/// 保存实验室功能配置 -/// -/// 将实验室功能配置保存到应用状态和配置文件 -/// 同时根据配置状态注册或注销快捷键 -/// -/// # 参数 -/// - `app`: Tauri 应用句柄 -/// - `config_manager`: 全局配置管理器状态 -/// - `experimental_config`: 要保存的实验室功能配置 -/// -/// # 返回 -/// 成功返回 Ok(()), 失败返回错误信息 -/// -/// _需求: 1.4, 1.5_ -#[tauri::command] -pub async fn save_experimental_config( - app: AppHandle, - config_manager: State<'_, GlobalConfigManagerState>, - experimental_config: ExperimentalFeatures, -) -> Result<(), String> { - info!( - "保存实验室功能配置: enabled={}, shortcut={}", - experimental_config.screenshot_chat.enabled, experimental_config.screenshot_chat.shortcut - ); - - // 获取旧配置以比较变化 - let old_config = config_manager.config(); - let was_enabled = old_config.experimental.screenshot_chat.enabled; - let is_enabled = experimental_config.screenshot_chat.enabled; - - debug!( - "配置变化: was_enabled={}, is_enabled={}", - was_enabled, is_enabled - ); - - // 更新配置 - let mut new_config = old_config.clone(); - new_config.experimental = experimental_config.clone(); - - // 保存配置到文件 - debug!("开始保存配置到文件..."); - if let Err(e) = config_manager.save_config(&new_config).await { - error!("保存配置失败: {}", e); - return Err(format!("保存配置失败: {e}")); - } - info!("配置文件保存成功"); - - // 根据功能开关状态注册或注销快捷键 - if was_enabled != is_enabled { - if is_enabled { - info!( - "截图对话功能已启用,注册快捷键: {}", - experimental_config.screenshot_chat.shortcut - ); - if let Err(e) = shortcut::register(&app, &experimental_config.screenshot_chat.shortcut) - { - error!("注册快捷键失败: {}", e); - return Err(format!("注册快捷键失败: {e}")); - } - info!("快捷键注册成功"); - } else { - info!("截图对话功能已禁用,注销快捷键"); - if let Err(e) = shortcut::unregister(&app) { - error!("注销快捷键失败: {}", e); - return Err(format!("注销快捷键失败: {e}")); - } - info!("快捷键注销成功"); - } - } - - info!("实验室功能配置保存完成"); - Ok(()) -} - -/// 启动截图 -/// -/// 启动交互式截图,返回截图文件路径 -/// -/// # 返回 -/// 成功返回截图文件路径,用户取消返回空字符串,失败返回错误信息 -/// -/// _需求: 3.1_ -#[tauri::command] -pub async fn start_screenshot() -> Result { - info!("启动截图命令"); - - match capture::start_capture().await { - Ok(path) => { - info!("截图成功: {:?}", path); - Ok(path.to_string_lossy().to_string()) - } - Err(capture::CaptureError::Cancelled) => { - info!("用户取消了截图"); - Ok(String::new()) - } - Err(e) => { - error!("截图失败: {}", e); - Err(format!("截图失败: {e}")) - } - } -} - -/// 验证快捷键格式 -/// -/// 检查快捷键字符串是否符合 Tauri 快捷键格式要求 -/// -/// # 参数 -/// - `shortcut_str`: 快捷键字符串,如 "CommandOrControl+Shift+S" -/// -/// # 返回 -/// 如果格式有效返回 true,否则返回错误信息 -/// -/// _需求: 2.2_ -#[tauri::command] -pub fn validate_shortcut(shortcut_str: String) -> Result { - debug!("验证快捷键格式: {}", shortcut_str); - - match shortcut::validate(&shortcut_str) { - Ok(()) => Ok(true), - Err(e) => Err(format!("{e}")), - } -} - -/// 更新截图快捷键 -/// -/// 原子性地更新快捷键:先注销旧快捷键,再注册新快捷键 -/// 同时更新配置文件 -/// -/// # 参数 -/// - `app`: Tauri 应用句柄 -/// - `config_manager`: 全局配置管理器状态 -/// - `new_shortcut`: 新的快捷键字符串 -/// -/// # 返回 -/// 成功返回 Ok(()), 失败返回错误信息 -/// -/// _需求: 2.4_ -#[tauri::command] -pub async fn update_screenshot_shortcut( - app: AppHandle, - config_manager: State<'_, GlobalConfigManagerState>, - new_shortcut: String, -) -> Result<(), String> { - info!("更新截图快捷键: {}", new_shortcut); - - // 验证新快捷键格式 - shortcut::validate(&new_shortcut).map_err(|e| format!("快捷键格式无效: {e}"))?; - - // 获取当前配置 - let mut config = config_manager.config(); - - // 检查功能是否启用 - if config.experimental.screenshot_chat.enabled { - // 更新快捷键(原子操作) - shortcut::update(&app, &new_shortcut).map_err(|e| format!("更新快捷键失败: {e}"))?; - } - - // 更新配置 - config.experimental.screenshot_chat.shortcut = new_shortcut; - - // 保存配置到文件 - config_manager - .save_config(&config) - .await - .map_err(|e| format!("保存配置失败: {e}"))?; - - info!("截图快捷键更新成功"); - Ok(()) -} - -/// 关闭截图对话窗口 -/// -/// 关闭当前打开的截图对话悬浮窗口 -/// -/// # 返回 -/// 成功返回 Ok(()), 失败返回错误信息 -#[tauri::command] -pub fn close_screenshot_chat_window(app: AppHandle) -> Result<(), String> { - info!("关闭截图对话窗口"); - - crate::screenshot::window::close_floating_window(&app).map_err(|e| format!("关闭窗口失败: {e}")) -} - -/// 打开带预填文本的输入框 -/// -/// 用于语音识别完成后,将识别结果填入输入框 -/// -/// # 参数 -/// - `app`: Tauri 应用句柄 -/// - `text`: 预填文本 -/// -/// # 返回 -/// 成功返回 Ok(()), 失败返回错误信息 -#[tauri::command] -pub fn open_input_with_text(app: AppHandle, text: String) -> Result<(), String> { - info!("打开带预填文本的输入框: {} 字符", text.len()); - - crate::screenshot::window::open_floating_window_with_text(&app, &text) - .map_err(|e| format!("打开窗口失败: {e}")) -} - -/// 读取图片文件并转换为 Base64 -/// -/// 读取指定路径的图片文件,并将其内容编码为 Base64 字符串 -/// -/// # 参数 -/// - `path`: 图片文件路径 -/// -/// # 返回 -/// 成功返回 Base64 编码的图片数据,失败返回错误信息 -/// -/// _需求: 5.1_ -#[tauri::command] -pub async fn read_image_as_base64(path: String) -> Result { - lime_services::screenshot_image_service::read_image_as_base64(&path).await -} - -/// 截图对话消息结构 -#[derive(Debug, Clone, serde::Serialize)] -pub struct ScreenshotChatMessage { - pub message: String, - pub image_path: Option, - pub image_base64: Option, -} - -/// 发送截图对话消息到主应用 -/// -/// 将用户输入的消息和截图发送到主应用的 Agent 聊天界面 -/// -/// # 参数 -/// - `app`: Tauri 应用句柄 -/// - `message`: 用户输入的消息 -/// - `image_path`: 截图文件路径(可选) -/// -/// # 返回 -/// 成功返回 Ok(()), 失败返回错误信息 -#[tauri::command] -pub async fn send_screenshot_chat( - app: AppHandle, - message: String, - image_path: Option, -) -> Result<(), String> { - info!( - "发送截图对话: message={}, image_path={:?}", - message, image_path - ); - - // 如果有图片,读取为 Base64 - let image_base64 = if let Some(ref path) = image_path { - match read_image_as_base64(path.clone()).await { - Ok(base64) => Some(base64), - Err(e) => { - error!("读取图片失败: {}", e); - None - } - } - } else { - None - }; - - // 构建消息 - let chat_message = ScreenshotChatMessage { - message, - image_path, - image_base64, - }; - - // 发送事件到主窗口 - if let Some(main_window) = app.get_webview_window("main") { - main_window - .emit("smart-input-message", &chat_message) - .map_err(|e| format!("发送事件失败: {e}"))?; - - // 恢复并聚焦主窗口(主窗口在截图时被最小化) - let _ = main_window.unminimize(); - let _ = main_window.show(); - let _ = main_window.set_focus(); - } else { - // 尝试发送到所有窗口 - app.emit("smart-input-message", &chat_message) - .map_err(|e| format!("发送事件失败: {e}"))?; - } - - info!("截图对话消息已发送"); - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_validate_shortcut_valid() { - assert!(validate_shortcut("CommandOrControl+Shift+S".to_string()).is_ok()); - assert!(validate_shortcut("Alt+F4".to_string()).is_ok()); - assert!(validate_shortcut("Ctrl+C".to_string()).is_ok()); - } - - #[test] - fn test_validate_shortcut_invalid() { - assert!(validate_shortcut("".to_string()).is_err()); - assert!(validate_shortcut("InvalidKey".to_string()).is_err()); - } - - #[test] - fn test_validate_shortcut_rejects_input_method_reserved_shortcuts() { - let result = validate_shortcut("CommandOrControl+Space".to_string()); - assert!(result.is_err()); - assert!(result.err().unwrap().contains("输入法切换")); - } -} diff --git a/src-tauri/src/dev_bridge/dispatcher.rs b/src-tauri/src/dev_bridge/dispatcher.rs index 80a93d15b..226ce3f3c 100644 --- a/src-tauri/src/dev_bridge/dispatcher.rs +++ b/src-tauri/src/dev_bridge/dispatcher.rs @@ -492,7 +492,7 @@ mod tests { .unwrap(); assert!(status_value["shortcut_registered"].is_boolean()); - assert!(status_value["translate_shortcut_registered"].is_boolean()); + assert!(status_value["fn_registered"].is_boolean()); } #[tokio::test] diff --git a/src-tauri/src/dev_bridge/dispatcher/voice.rs b/src-tauri/src/dev_bridge/dispatcher/voice.rs index b83076b55..cdf702aee 100644 --- a/src-tauri/src/dev_bridge/dispatcher/voice.rs +++ b/src-tauri/src/dev_bridge/dispatcher/voice.rs @@ -164,18 +164,6 @@ pub(super) async fn try_handle( crate::voice::commands::delete_voice_instruction(id).await?; JsonValue::Null } - "open_voice_window" => { - let args = args_or_default(args); - let target = get_optional_string_arg(&args, "target", "target"); - let app_handle = require_app_handle(state)?; - crate::voice::commands::open_voice_window(app_handle, target).await?; - JsonValue::Null - } - "close_voice_window" => { - let app_handle = require_app_handle(state)?; - crate::voice::commands::close_voice_window(app_handle).await?; - JsonValue::Null - } "list_audio_devices" => { serde_json::to_value(crate::voice::recording_service::list_audio_devices()?)? } @@ -277,13 +265,6 @@ pub(super) async fn try_handle( duration: service.get_duration(), })? } - "open_input_with_text" => { - let args = args_or_default(args); - let text = get_string_arg(&args, "text", "text")?; - let app_handle = require_app_handle(state)?; - crate::commands::screenshot_cmd::open_input_with_text(app_handle, text)?; - JsonValue::Null - } _ => return Ok(None), }; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 791d3d69d..65ac46b52 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -42,7 +42,6 @@ pub mod agent; pub mod agent_tools; pub mod app; pub mod sceneapp; -pub mod screenshot; pub mod services; pub mod skills; pub mod tray; diff --git a/src-tauri/src/screenshot/README.md b/src-tauri/src/screenshot/README.md deleted file mode 100644 index aa15a7e5e..000000000 --- a/src-tauri/src/screenshot/README.md +++ /dev/null @@ -1,69 +0,0 @@ -# 截图对话模块 (screenshot/) - -截图对话功能的 Rust 后端实现,提供全局快捷键、屏幕截图和悬浮窗口管理。 - -## 模块结构 - -``` -screenshot/ -├── mod.rs # 模块入口,导出公共接口和初始化函数 -├── capture.rs # 截图桥接层(纯逻辑在 crates/services/src/screenshot_capture_service.rs) -├── config.rs # 实验室功能配置管理 -├── shortcut.rs # 全局快捷键注册/注销/更新 -├── window.rs # 悬浮对话窗口管理 -└── README.md # 本文档 -``` - -## 核心功能 - -### 1. 模块初始化 (mod.rs) - -- `init(app)`: 初始化截图对话模块,根据配置注册快捷键 -- `cleanup(app)`: 清理模块资源,注销快捷键 - -### 2. 配置管理 (config.rs) - -- `load_experimental_config(app)`: 加载实验室功能配置 -- `is_screenshot_chat_enabled(config)`: 检查功能是否启用 -- `save_experimental_config(app, config)`: 保存配置到文件 - -### 3. 快捷键管理 (shortcut.rs) - -- `register(app, shortcut)`: 注册全局快捷键 -- `unregister(app)`: 注销当前快捷键 -- `update(app, new_shortcut)`: 原子性更新快捷键 -- `validate(shortcut)`: 验证快捷键格式 - -### 4. 截图服务 (capture.rs) - -- `start_capture()`: 启动交互式截图 -- 支持 macOS (`screencapture`)、Windows (PowerShell)、Linux (`gnome-screenshot` / `scrot`) - -### 5. 窗口管理 (window.rs) - -- `open_floating_window(app, image_path)`: 打开悬浮对话窗口 -- `close_floating_window(app)`: 关闭悬浮窗口 -- `is_floating_window_open(app)`: 检查窗口状态 - -## 依赖 - -- `tauri-plugin-global-shortcut`: 全局快捷键支持 -- `chrono`: 时间戳生成 -- `urlencoding`: URL 参数编码 - -## 配置示例 - -```yaml -experimental: - screenshot_chat: - enabled: true - shortcut: "CommandOrControl+Shift+S" -``` - -## 相关需求 - -- 需求 1: 实验室功能开关 -- 需求 2: 快捷键配置 -- 需求 3: 屏幕截图 -- 需求 4: 悬浮对话窗口 -- 需求 7: 模块化代码组织 diff --git a/src-tauri/src/screenshot/capture.rs b/src-tauri/src/screenshot/capture.rs deleted file mode 100644 index 6b3613c7c..000000000 --- a/src-tauri/src/screenshot/capture.rs +++ /dev/null @@ -1,8 +0,0 @@ -//! 屏幕截图服务(桥接层) -//! -//! 纯逻辑已迁移到 `lime-services` crate, -//! 本模块保留兼容导出。 - -pub use lime_services::screenshot_capture_service::{ - cleanup_temp_file, start_capture, CaptureError, CaptureResult, -}; diff --git a/src-tauri/src/screenshot/config.rs b/src-tauri/src/screenshot/config.rs deleted file mode 100644 index 9ebb66d0e..000000000 --- a/src-tauri/src/screenshot/config.rs +++ /dev/null @@ -1,134 +0,0 @@ -//! 截图对话配置管理 -//! -//! 提供实验室功能配置的加载和检查功能 - -use crate::config::GlobalConfigManagerState; -use lime_core::config::{ExperimentalFeatures, ScreenshotChatConfig}; -use tauri::{AppHandle, Manager}; -use tracing::debug; - -/// 配置错误类型 -#[derive(Debug, thiserror::Error)] -pub enum ConfigError { - #[error("无法获取应用状态")] - StateNotFound, - #[error("配置加载失败: {0}")] - LoadFailed(String), -} - -/// 加载实验室功能配置 -/// -/// 从应用状态中获取当前的实验室功能配置 -/// -/// # 参数 -/// - `app`: Tauri 应用句柄 -/// -/// # 返回 -/// 成功返回 ExperimentalFeatures,失败返回错误 -pub fn load_experimental_config(app: &AppHandle) -> Result { - debug!("加载实验室功能配置"); - - // 获取全局配置管理器 - let config_manager = app - .try_state::() - .ok_or(ConfigError::StateNotFound)?; - - // 从配置中获取实验室功能配置 - let config = config_manager.config(); - let experimental = config.experimental.clone(); - - debug!( - "实验室功能配置: screenshot_chat.enabled={}, shortcut={}", - experimental.screenshot_chat.enabled, experimental.screenshot_chat.shortcut - ); - - Ok(experimental) -} - -/// 检查截图对话功能是否启用 -/// -/// # 参数 -/// - `config`: 实验室功能配置 -/// -/// # 返回 -/// 如果截图对话功能启用返回 true,否则返回 false -pub fn is_screenshot_chat_enabled(config: &ExperimentalFeatures) -> bool { - config.screenshot_chat.enabled -} - -/// 获取截图对话配置 -/// -/// # 参数 -/// - `app`: Tauri 应用句柄 -/// -/// # 返回 -/// 成功返回 ScreenshotChatConfig,失败返回错误 -pub fn get_screenshot_chat_config(app: &AppHandle) -> Result { - let experimental = load_experimental_config(app)?; - Ok(experimental.screenshot_chat) -} - -/// 保存实验室功能配置 -/// -/// 将实验室功能配置保存到应用状态和配置文件 -/// -/// # 参数 -/// - `app`: Tauri 应用句柄 -/// - `experimental_config`: 要保存的实验室功能配置 -/// -/// # 返回 -/// 成功返回 Ok(()), 失败返回错误 -pub async fn save_experimental_config( - app: &AppHandle, - experimental_config: ExperimentalFeatures, -) -> Result<(), ConfigError> { - debug!("保存实验室功能配置"); - - // 获取全局配置管理器 - let config_manager = app - .try_state::() - .ok_or(ConfigError::StateNotFound)?; - - // 获取当前配置并更新实验室功能部分 - let mut config = config_manager.config(); - config.experimental = experimental_config; - - // 保存配置 - config_manager - .save_config(&config) - .await - .map_err(ConfigError::LoadFailed)?; - - debug!("实验室功能配置已保存"); - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_is_screenshot_chat_enabled_default() { - let config = ExperimentalFeatures::default(); - assert!(!is_screenshot_chat_enabled(&config)); - } - - #[test] - fn test_is_screenshot_chat_enabled_true() { - let config = ExperimentalFeatures { - screenshot_chat: ScreenshotChatConfig { - enabled: true, - shortcut: "CommandOrControl+Shift+S".to_string(), - }, - ..Default::default() - }; - assert!(is_screenshot_chat_enabled(&config)); - } - - #[test] - fn test_default_shortcut() { - let config = ScreenshotChatConfig::default(); - assert_eq!(config.shortcut, "CommandOrControl+Alt+Q"); - assert!(!config.enabled); - } -} diff --git a/src-tauri/src/screenshot/mod.rs b/src-tauri/src/screenshot/mod.rs deleted file mode 100644 index 9e131064f..000000000 --- a/src-tauri/src/screenshot/mod.rs +++ /dev/null @@ -1,69 +0,0 @@ -//! 截图对话模块 -//! -//! 提供截图对话功能的核心实现,包括: -//! - 屏幕截图服务 -//! - 全局快捷键管理 -//! - 悬浮窗口管理 -//! - 实验室功能配置 - -pub mod capture; -pub mod config; -pub mod shortcut; -pub mod window; - -use tauri::AppHandle; -use tracing::{error, info}; - -/// 截图模块错误类型 -#[derive(Debug, thiserror::Error)] -pub enum ScreenshotError { - #[error("配置错误: {0}")] - Config(String), - #[error("快捷键错误: {0}")] - Shortcut(#[from] shortcut::ShortcutError), - #[error("截图错误: {0}")] - Capture(#[from] capture::CaptureError), - #[error("窗口错误: {0}")] - Window(#[from] window::WindowError), -} - -/// 初始化截图对话模块 -/// -/// 根据配置决定是否注册全局快捷键 -/// -/// # 参数 -/// - `app`: Tauri 应用句柄 -/// -/// # 返回 -/// 成功返回 Ok(()), 失败返回错误 -pub fn init(app: &AppHandle) -> Result<(), ScreenshotError> { - info!("初始化截图对话模块"); - - // 加载实验室功能配置 - let experimental_config = config::load_experimental_config(app) - .map_err(|e| ScreenshotError::Config(e.to_string()))?; - - // 检查截图对话功能是否启用 - if config::is_screenshot_chat_enabled(&experimental_config) { - info!("截图对话功能已启用,注册快捷键"); - shortcut::register(app, &experimental_config.screenshot_chat.shortcut)?; - } else { - info!("截图对话功能未启用,跳过快捷键注册"); - } - - Ok(()) -} - -/// 清理截图对话模块资源 -/// -/// 注销快捷键并清理临时文件 -pub fn cleanup(app: &AppHandle) -> Result<(), ScreenshotError> { - info!("清理截图对话模块资源"); - - // 尝试注销快捷键(忽略错误,因为可能未注册) - if let Err(e) = shortcut::unregister(app) { - error!("注销快捷键失败: {}", e); - } - - Ok(()) -} diff --git a/src-tauri/src/screenshot/shortcut.rs b/src-tauri/src/screenshot/shortcut.rs deleted file mode 100644 index cee8751bf..000000000 --- a/src-tauri/src/screenshot/shortcut.rs +++ /dev/null @@ -1,361 +0,0 @@ -//! 全局快捷键管理 -//! -//! 提供截图对话功能的全局快捷键注册、注销和更新功能 - -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::OnceLock; -use tauri::{AppHandle, Manager}; -use tauri_plugin_global_shortcut::{GlobalShortcutExt, Shortcut, ShortcutState}; -use tracing::{debug, error, info, warn}; - -use super::capture; -use super::window; - -/// 快捷键错误类型 -#[derive(Debug, thiserror::Error)] -pub enum ShortcutError { - #[error("快捷键格式无效: {0}")] - InvalidFormat(String), - #[error("{0}")] - ReservedSystemShortcut(String), - #[error("快捷键注册失败: {0}")] - RegisterFailed(String), - #[error("快捷键注销失败: {0}")] - UnregisterFailed(String), - #[error("快捷键已被占用: {0}")] - AlreadyInUse(String), - #[error("快捷键解析失败: {0}")] - ParseFailed(String), -} - -/// 当前注册的快捷键(用于更新时注销旧快捷键) -static CURRENT_SHORTCUT: OnceLock>> = OnceLock::new(); - -/// 快捷键是否已注册 -static IS_REGISTERED: AtomicBool = AtomicBool::new(false); - -fn get_current_shortcut() -> &'static parking_lot::RwLock> { - CURRENT_SHORTCUT.get_or_init(|| parking_lot::RwLock::new(None)) -} - -/// 验证快捷键格式 -/// -/// 检查快捷键字符串是否符合 Tauri 快捷键格式要求 -/// -/// # 参数 -/// - `shortcut`: 快捷键字符串,如 "CommandOrControl+Shift+S" -/// -/// # 返回 -/// 如果格式有效返回 Ok(()), 否则返回错误 -/// -/// # 有效格式示例 -/// - "CommandOrControl+Shift+S" -/// - "Alt+F4" -/// - "Ctrl+Alt+Delete" -pub fn validate(shortcut: &str) -> Result<(), ShortcutError> { - if shortcut.is_empty() { - return Err(ShortcutError::InvalidFormat("快捷键不能为空".to_string())); - } - - // 尝试解析快捷键以验证格式 - shortcut - .parse::() - .map_err(|e| ShortcutError::InvalidFormat(format!("无法解析快捷键 '{shortcut}': {e}")))?; - - if let Some(reason) = crate::global_shortcut_guard::reserved_system_shortcut_reason(shortcut) { - return Err(ShortcutError::ReservedSystemShortcut(reason.to_string())); - } - - debug!("快捷键格式验证通过: {}", shortcut); - Ok(()) -} - -/// 注册全局快捷键 -/// -/// 注册截图对话功能的全局快捷键 -/// -/// # 参数 -/// - `app`: Tauri 应用句柄 -/// - `shortcut_str`: 快捷键字符串 -/// -/// # 返回 -/// 成功返回 Ok(()), 失败返回错误 -pub fn register(app: &AppHandle, shortcut_str: &str) -> Result<(), ShortcutError> { - info!("注册全局快捷键: {}", shortcut_str); - - // 验证快捷键格式 - validate(shortcut_str)?; - - // 解析快捷键 - let shortcut: Shortcut = shortcut_str - .parse() - .map_err(|e| ShortcutError::ParseFailed(format!("{e}")))?; - - // 获取全局快捷键管理器 - let global_shortcut = app.global_shortcut(); - - // 检查快捷键是否已被注册 - let is_already_registered = global_shortcut.is_registered(shortcut); - info!( - "快捷键 {} 是否已注册: {}", - shortcut_str, is_already_registered - ); - - if is_already_registered { - warn!("快捷键已被注册: {}", shortcut_str); - // 如果是我们自己注册的,先注销 - if IS_REGISTERED.load(Ordering::SeqCst) { - info!("尝试注销已有的快捷键"); - if let Err(e) = global_shortcut.unregister(shortcut) { - error!("注销已有快捷键失败: {}", e); - } - } else { - return Err(ShortcutError::AlreadyInUse(shortcut_str.to_string())); - } - } - - // 克隆 app handle 用于回调 - let app_clone = app.clone(); - - // 注册快捷键 - info!("开始注册快捷键回调..."); - global_shortcut - .on_shortcut(shortcut, move |_app, _shortcut, event| { - if event.state == ShortcutState::Pressed { - info!("截图快捷键被触发"); - handle_shortcut_triggered(&app_clone); - } - }) - .map_err(|e| { - error!("注册快捷键失败: {}", e); - ShortcutError::RegisterFailed(format!("{e}")) - })?; - - // 更新状态 - IS_REGISTERED.store(true, Ordering::SeqCst); - *get_current_shortcut().write() = Some(shortcut_str.to_string()); - - info!("全局快捷键注册成功: {}", shortcut_str); - Ok(()) -} - -/// 注销全局快捷键 -/// -/// 注销当前注册的截图对话快捷键 -/// -/// # 参数 -/// - `app`: Tauri 应用句柄 -/// -/// # 返回 -/// 成功返回 Ok(()), 失败返回错误 -pub fn unregister(app: &AppHandle) -> Result<(), ShortcutError> { - let current = get_current_shortcut().read().clone(); - - if let Some(shortcut_str) = current { - info!("注销全局快捷键: {}", shortcut_str); - - let shortcut: Shortcut = shortcut_str - .parse() - .map_err(|e| ShortcutError::ParseFailed(format!("{e}")))?; - - let global_shortcut = app.global_shortcut(); - - if global_shortcut.is_registered(shortcut) { - global_shortcut - .unregister(shortcut) - .map_err(|e| ShortcutError::UnregisterFailed(format!("{e}")))?; - } - - // 更新状态 - IS_REGISTERED.store(false, Ordering::SeqCst); - *get_current_shortcut().write() = None; - - info!("全局快捷键注销成功"); - } else { - debug!("没有已注册的快捷键需要注销"); - } - - Ok(()) -} - -/// 更新全局快捷键 -/// -/// 原子性地更新快捷键:先注销旧快捷键,再注册新快捷键 -/// -/// # 参数 -/// - `app`: Tauri 应用句柄 -/// - `new_shortcut`: 新的快捷键字符串 -/// -/// # 返回 -/// 成功返回 Ok(()), 失败返回错误 -/// -/// # 注意 -/// 此操作是原子性的:如果新快捷键注册失败,会尝试恢复旧快捷键 -pub fn update(app: &AppHandle, new_shortcut: &str) -> Result<(), ShortcutError> { - info!("更新全局快捷键: {}", new_shortcut); - - // 验证新快捷键格式 - validate(new_shortcut)?; - - // 保存旧快捷键以便恢复 - let old_shortcut = get_current_shortcut().read().clone(); - - // 注销旧快捷键 - if let Err(e) = unregister(app) { - warn!("注销旧快捷键失败: {}", e); - // 继续尝试注册新快捷键 - } - - // 注册新快捷键 - match register(app, new_shortcut) { - Ok(()) => { - info!("快捷键更新成功: {}", new_shortcut); - Ok(()) - } - Err(e) => { - error!("注册新快捷键失败: {}", e); - - // 尝试恢复旧快捷键 - if let Some(old) = old_shortcut { - warn!("尝试恢复旧快捷键: {}", old); - if let Err(restore_err) = register(app, &old) { - error!("恢复旧快捷键失败: {}", restore_err); - } - } - - Err(e) - } - } -} - -/// 检查快捷键是否已注册 -pub fn is_registered() -> bool { - IS_REGISTERED.load(Ordering::SeqCst) -} - -/// 获取当前注册的快捷键 -pub fn get_current() -> Option { - get_current_shortcut().read().clone() -} - -/// 处理快捷键触发事件 -/// -/// 当用户按下截图快捷键时调用此函数 -fn handle_shortcut_triggered(app: &AppHandle) { - info!("处理截图快捷键触发"); - - // 在后台线程中执行截图操作 - let app_clone = app.clone(); - tauri::async_runtime::spawn(async move { - // 截图前最小化主窗口(而非隐藏),避免视觉干扰但保持可恢复 - let main_window = app_clone.get_webview_window("main"); - if let Some(win) = main_window.as_ref() { - debug!("最小化主窗口"); - let _ = win.minimize(); - } - - // 短暂延迟,确保窗口完全最小化 - tokio::time::sleep(std::time::Duration::from_millis(100)).await; - - match capture::start_capture().await { - Ok(image_path) => { - info!("截图成功: {:?}", image_path); - // 打开悬浮窗口 - if let Err(e) = window::open_floating_window(&app_clone, &image_path) { - error!("打开悬浮窗口失败: {}", e); - // 如果悬浮窗口打开失败,恢复主窗口 - if let Some(win) = main_window.as_ref() { - let _ = win.unminimize(); - let _ = win.set_focus(); - } - } - } - Err(capture::CaptureError::Cancelled) => { - info!("用户取消了截图"); - // 用户取消截图,恢复主窗口显示 - if let Some(win) = main_window.as_ref() { - debug!("恢复主窗口显示"); - let _ = win.unminimize(); - let _ = win.set_focus(); - } - } - Err(e) => { - error!("截图失败: {}", e); - // 截图失败,恢复主窗口显示 - if let Some(win) = main_window.as_ref() { - let _ = win.unminimize(); - let _ = win.set_focus(); - } - } - } - }); -} - -#[cfg(test)] -mod tests { - use super::*; - - fn reset_runtime_state() { - IS_REGISTERED.store(false, Ordering::SeqCst); - *get_current_shortcut().write() = None; - } - - #[test] - fn test_validate_valid_shortcuts() { - // 有效的快捷键格式 - assert!(validate("CommandOrControl+Shift+S").is_ok()); - assert!(validate("Alt+F4").is_ok()); - assert!(validate("Ctrl+C").is_ok()); - assert!(validate("Shift+A").is_ok()); - } - - #[test] - fn test_validate_empty_shortcut() { - let result = validate(""); - assert!(result.is_err()); - match result { - Err(ShortcutError::InvalidFormat(msg)) => { - assert!(msg.contains("不能为空")); - } - _ => panic!("Expected InvalidFormat error"), - } - } - - #[test] - fn test_validate_invalid_shortcuts() { - // 无效的快捷键格式 - assert!(validate("InvalidKey").is_err()); - assert!(validate("+++").is_err()); - assert!(validate("Ctrl++").is_err()); - } - - #[test] - fn test_validate_rejects_input_method_reserved_shortcuts() { - for shortcut in ["CommandOrControl+Space", "Ctrl+Space", "Super+Space"] { - let result = validate(shortcut); - assert!(result.is_err()); - match result { - Err(ShortcutError::ReservedSystemShortcut(message)) => { - assert!(message.contains("输入法切换")); - } - _ => panic!("Expected ReservedSystemShortcut error for {shortcut}"), - } - } - } - - #[test] - fn test_runtime_status_accessors_reflect_state() { - reset_runtime_state(); - - assert!(!is_registered()); - assert_eq!(get_current(), None); - - IS_REGISTERED.store(true, Ordering::SeqCst); - *get_current_shortcut().write() = Some("CommandOrControl+Shift+4".to_string()); - - assert!(is_registered()); - assert_eq!(get_current().as_deref(), Some("CommandOrControl+Shift+4")); - - reset_runtime_state(); - } -} diff --git a/src-tauri/src/screenshot/window.rs b/src-tauri/src/screenshot/window.rs deleted file mode 100644 index 704f1bfe2..000000000 --- a/src-tauri/src/screenshot/window.rs +++ /dev/null @@ -1,487 +0,0 @@ -//! 悬浮窗口管理 -//! -//! 提供截图对话悬浮窗口的创建、显示和关闭功能 - -use mouse_position::mouse_position::Mouse; -use std::path::Path; -use tauri::{AppHandle, Manager, WebviewUrl, WebviewWindowBuilder}; -use tracing::{debug, info}; - -#[cfg(target_os = "macos")] -#[allow(deprecated)] -use cocoa::appkit::{NSColor, NSWindow}; -#[cfg(target_os = "macos")] -#[allow(deprecated)] -use cocoa::base::{id, nil}; - -/// 窗口错误类型 -#[derive(Debug, thiserror::Error)] -pub enum WindowError { - #[error("窗口创建失败: {0}")] - CreateFailed(String), - #[error("窗口未找到: {0}")] - NotFound(String), - #[error("窗口操作失败: {0}")] - OperationFailed(String), -} - -/// 悬浮窗口标签 -const FLOATING_WINDOW_LABEL: &str = "smart-input"; - -/// 窗口尺寸(包含 padding 用于阴影) -const WINDOW_WIDTH: f64 = 645.0; -const WINDOW_HEIGHT: f64 = 70.0; -/// 距离屏幕底部的距离 -const BOTTOM_MARGIN: f64 = 200.0; - -/// 获取鼠标所在的显示器 -/// -/// 使用 mouse_position crate 获取鼠标位置,然后遍历所有显示器找到鼠标所在的显示器 -fn get_monitor_at_cursor(app: &AppHandle) -> Option { - // 使用 mouse_position crate 获取鼠标位置 - let (cursor_x, cursor_y) = match Mouse::get_mouse_position() { - Mouse::Position { x, y } => { - debug!("鼠标位置 (mouse_position crate): ({}, {})", x, y); - (x as f64, y as f64) - } - Mouse::Error => { - debug!("无法获取鼠标位置"); - return None; - } - }; - - // 获取所有显示器 - let monitors = match app.available_monitors() { - Ok(monitors) => monitors, - Err(e) => { - debug!("无法获取显示器列表: {}", e); - return None; - } - }; - - // 查找鼠标所在的显示器 - for monitor in monitors { - let pos = monitor.position(); - let size = monitor.size(); - - let left = pos.x as f64; - let top = pos.y as f64; - let right = left + size.width as f64; - let bottom = top + size.height as f64; - - if cursor_x >= left && cursor_x < right && cursor_y >= top && cursor_y < bottom { - debug!( - "鼠标在显示器: {:?}, 位置: ({}, {}), 尺寸: {}x{}", - monitor.name(), - pos.x, - pos.y, - size.width, - size.height - ); - return Some(monitor); - } - } - - debug!( - "未找到鼠标所在的显示器,鼠标位置: ({}, {})", - cursor_x, cursor_y - ); - None -} - -/// 计算窗口位置(屏幕底部居中) -/// -/// 优先使用鼠标所在的显示器,否则使用主显示器 -/// 返回逻辑坐标(考虑 DPI 缩放) -fn calculate_window_position(app: &AppHandle) -> (f64, f64) { - // 优先获取鼠标所在的显示器 - let monitor = get_monitor_at_cursor(app).or_else(|| app.primary_monitor().ok().flatten()); - - if let Some(monitor) = monitor { - let screen_pos = monitor.position(); - let screen_size = monitor.size(); - let scale_factor = monitor.scale_factor(); - - // 物理像素转换为逻辑像素 - let screen_width = screen_size.width as f64 / scale_factor; - let screen_height = screen_size.height as f64 / scale_factor; - let screen_x = screen_pos.x as f64 / scale_factor; - let screen_y = screen_pos.y as f64 / scale_factor; - - // 在该显示器上水平居中(使用逻辑坐标) - let x = screen_x + (screen_width - WINDOW_WIDTH) / 2.0; - // 距离该显示器底部 BOTTOM_MARGIN(使用逻辑坐标) - let y = screen_y + screen_height - WINDOW_HEIGHT - BOTTOM_MARGIN; - - debug!( - "显示器: scale_factor={}, 物理位置: ({}, {}), 物理尺寸: {}x{}", - scale_factor, screen_pos.x, screen_pos.y, screen_size.width, screen_size.height - ); - debug!( - "逻辑坐标: 屏幕({}, {}), 尺寸: {}x{}, 窗口位置: ({}, {})", - screen_x, screen_y, screen_width, screen_height, x, y - ); - return (x, y); - } - - // 默认位置(如果无法获取屏幕尺寸) - debug!("无法获取显示器信息,使用默认位置"); - (400.0, 600.0) -} - -/// 打开悬浮对话窗口 -/// -/// 如果窗口已在 tauri.conf.json 中预定义,则显示并导航到新 URL -/// 否则动态创建一个全屏透明的悬浮窗口 -/// -/// # 参数 -/// - `app`: Tauri 应用句柄 -/// - `image_path`: 截图文件路径 -/// -/// # 返回 -/// 成功返回 Ok(()), 失败返回错误 -pub fn open_floating_window(app: &AppHandle, image_path: &Path) -> Result<(), WindowError> { - info!("打开悬浮对话窗口"); - - // 构建窗口 URL,包含图片路径参数 - let image_path_str = image_path.to_str().unwrap_or(""); - let encoded_path = urlencoding::encode(image_path_str); - let url = format!("/smart-input?image={encoded_path}"); - - debug!("悬浮窗口 URL: {}", url); - - // 检查窗口是否已存在(可能是预定义的或之前创建的) - if let Some(window) = app.get_webview_window(FLOATING_WINDOW_LABEL) { - info!("悬浮窗口已存在,导航到新 URL 并显示"); - - // 计算窗口位置(返回逻辑坐标) - let (x, y) = calculate_window_position(app); - - // 设置窗口位置(使用逻辑坐标) - use tauri::LogicalPosition; - let _ = window.set_position(LogicalPosition::new(x, y)); - - // macOS: 设置窗口和 webview 背景透明 - #[cfg(target_os = "macos")] - { - use objc::{msg_send, sel, sel_impl}; - if let Ok(ns_win) = window.ns_window() { - #[allow(deprecated, unexpected_cfgs)] - unsafe { - let ns_window = ns_win as id; - // 设置窗口背景透明 - let clear_color = NSColor::clearColor(nil); - ns_window.setBackgroundColor_(clear_color); - let _: () = msg_send![ns_window, setOpaque: false]; - // 禁用窗口阴影 - let _: () = msg_send![ns_window, setHasShadow: false]; - } - } - } - - // 使用 JavaScript 导航到新的 URL(更新图片路径) - let js = format!("window.location.href = '{url}';"); - window - .eval(&js) - .map_err(|e| WindowError::OperationFailed(format!("导航失败: {e}")))?; - - // 显示窗口 - window - .show() - .map_err(|e| WindowError::OperationFailed(format!("显示窗口失败: {e}")))?; - - // 聚焦窗口 - window - .set_focus() - .map_err(|e| WindowError::OperationFailed(format!("聚焦窗口失败: {e}")))?; - - return Ok(()); - } - - // 窗口不存在,动态创建 - info!("动态创建悬浮窗口"); - - // 计算窗口位置 - let (x, y) = calculate_window_position(app); - - // 创建悬浮窗口(启用透明) - #[cfg_attr(not(target_os = "macos"), allow(unused_variables))] - let window = WebviewWindowBuilder::new(app, FLOATING_WINDOW_LABEL, WebviewUrl::App(url.into())) - .inner_size(WINDOW_WIDTH, WINDOW_HEIGHT) - .position(x, y) - .decorations(false) - .always_on_top(true) - .skip_taskbar(true) - .visible(true) - .focused(true) - .transparent(true) - .build() - .map_err(|e| WindowError::CreateFailed(format!("{e}")))?; - - // macOS: 设置窗口和 webview 背景透明 - #[cfg(target_os = "macos")] - { - use objc::{msg_send, sel, sel_impl}; - if let Ok(ns_win) = window.ns_window() { - #[allow(deprecated, unexpected_cfgs)] - unsafe { - let ns_window = ns_win as id; - // 设置窗口背景透明 - let clear_color = NSColor::clearColor(nil); - ns_window.setBackgroundColor_(clear_color); - let _: () = msg_send![ns_window, setOpaque: false]; - // 禁用窗口阴影 - let _: () = msg_send![ns_window, setHasShadow: false]; - } - } - } - - info!("悬浮窗口创建成功: {}", FLOATING_WINDOW_LABEL); - - Ok(()) -} - -/// 关闭悬浮对话窗口 -/// -/// # 参数 -/// - `app`: Tauri 应用句柄 -/// -/// # 返回 -/// 成功返回 Ok(()), 失败返回错误 -pub fn close_floating_window(app: &AppHandle) -> Result<(), WindowError> { - info!("关闭悬浮对话窗口"); - - if let Some(window) = app.get_webview_window(FLOATING_WINDOW_LABEL) { - window - .close() - .map_err(|e| WindowError::OperationFailed(format!("关闭窗口失败: {e}")))?; - info!("悬浮窗口已关闭"); - } else { - debug!("悬浮窗口不存在,无需关闭"); - } - - Ok(()) -} - -/// 检查悬浮窗口是否打开 -/// -/// # 参数 -/// - `app`: Tauri 应用句柄 -/// -/// # 返回 -/// 如果窗口存在且可见返回 true,否则返回 false -pub fn is_floating_window_open(app: &AppHandle) -> bool { - app.get_webview_window(FLOATING_WINDOW_LABEL) - .map(|w| w.is_visible().unwrap_or(false)) - .unwrap_or(false) -} - -/// 聚焦悬浮窗口 -/// -/// # 参数 -/// - `app`: Tauri 应用句柄 -/// -/// # 返回 -/// 成功返回 Ok(()), 失败返回错误 -pub fn focus_floating_window(app: &AppHandle) -> Result<(), WindowError> { - if let Some(window) = app.get_webview_window(FLOATING_WINDOW_LABEL) { - window - .set_focus() - .map_err(|e| WindowError::OperationFailed(format!("聚焦窗口失败: {e}")))?; - Ok(()) - } else { - Err(WindowError::NotFound(FLOATING_WINDOW_LABEL.to_string())) - } -} - -/// 打开带预填文本的悬浮输入框 -/// -/// 用于语音识别完成后,将识别结果填入输入框 -/// -/// # 参数 -/// - `app`: Tauri 应用句柄 -/// - `text`: 预填文本 -/// -/// # 返回 -/// 成功返回 Ok(()), 失败返回错误 -pub fn open_floating_window_with_text(app: &AppHandle, text: &str) -> Result<(), WindowError> { - info!("打开带预填文本的悬浮输入框"); - - // 构建窗口 URL,包含文本参数 - let encoded_text = urlencoding::encode(text); - let url = format!("/smart-input?text={encoded_text}"); - - open_floating_window_with_url(app, &url) -} - -/// 打开语音模式的悬浮输入框 -/// -/// 自动开始录音,录音完成后填入文本 -/// -/// # 参数 -/// - `app`: Tauri 应用句柄 -/// -/// # 返回 -/// 成功返回 Ok(()), 失败返回错误 -pub fn open_floating_window_voice_mode(app: &AppHandle) -> Result<(), WindowError> { - info!("打开语音模式的悬浮输入框"); - let url = "/smart-input?voice=true"; - open_floating_window_with_url(app, url) -} - -/// 内部函数:打开带指定 URL 的悬浮窗口 -fn open_floating_window_with_url(app: &AppHandle, url: &str) -> Result<(), WindowError> { - debug!("悬浮窗口 URL: {}", url); - - // 检查是否是语音模式 - let is_voice_mode = url.contains("voice=true"); - - // 检查窗口是否已存在 - if let Some(window) = app.get_webview_window(FLOATING_WINDOW_LABEL) { - info!("悬浮窗口已存在,导航到新 URL 并显示"); - - // 计算窗口位置 - let (x, y) = calculate_window_position(app); - - // 设置窗口位置 - use tauri::LogicalPosition; - let _ = window.set_position(LogicalPosition::new(x, y)); - - // macOS: 设置窗口背景透明 - #[cfg(target_os = "macos")] - { - use objc::{msg_send, sel, sel_impl}; - if let Ok(ns_win) = window.ns_window() { - #[allow(deprecated, unexpected_cfgs)] - unsafe { - let ns_window = ns_win as id; - let clear_color = NSColor::clearColor(nil); - ns_window.setBackgroundColor_(clear_color); - let _: () = msg_send![ns_window, setOpaque: false]; - let _: () = msg_send![ns_window, setHasShadow: false]; - } - } - } - - // 导航到新 URL(强制刷新) - let js = format!("window.location.replace('{url}');"); - window - .eval(&js) - .map_err(|e| WindowError::OperationFailed(format!("导航失败: {e}")))?; - - window - .show() - .map_err(|e| WindowError::OperationFailed(format!("显示窗口失败: {e}")))?; - - window - .set_focus() - .map_err(|e| WindowError::OperationFailed(format!("聚焦窗口失败: {e}")))?; - - // 如果是语音模式,额外发送事件确保前端收到 - if is_voice_mode { - use tauri::Emitter; - // 延迟发送事件,等待页面加载 - let app_clone = app.clone(); - std::thread::spawn(move || { - std::thread::sleep(std::time::Duration::from_millis(200)); - let _ = app_clone.emit("voice-start-recording", ()); - info!("[语音输入] 已发送开始录音事件"); - }); - } - - return Ok(()); - } - - // 窗口不存在,动态创建 - info!("动态创建悬浮窗口"); - - let (x, y) = calculate_window_position(app); - - #[cfg_attr(not(target_os = "macos"), allow(unused_variables))] - let window = WebviewWindowBuilder::new(app, FLOATING_WINDOW_LABEL, WebviewUrl::App(url.into())) - .inner_size(WINDOW_WIDTH, WINDOW_HEIGHT) - .position(x, y) - .decorations(false) - .always_on_top(true) - .skip_taskbar(true) - .visible(true) - .focused(true) - .transparent(true) - .build() - .map_err(|e| WindowError::CreateFailed(format!("{e}")))?; - - // macOS: 设置窗口背景透明 - #[cfg(target_os = "macos")] - { - use objc::{msg_send, sel, sel_impl}; - if let Ok(ns_win) = window.ns_window() { - #[allow(deprecated, unexpected_cfgs)] - unsafe { - let ns_window = ns_win as id; - let clear_color = NSColor::clearColor(nil); - ns_window.setBackgroundColor_(clear_color); - let _: () = msg_send![ns_window, setOpaque: false]; - let _: () = msg_send![ns_window, setHasShadow: false]; - } - } - } - - info!("悬浮窗口创建成功: {}", FLOATING_WINDOW_LABEL); - - if is_voice_mode { - use tauri::Emitter; - let app_clone = app.clone(); - std::thread::spawn(move || { - std::thread::sleep(std::time::Duration::from_millis(200)); - let _ = app_clone.emit("voice-start-recording", ()); - info!("[语音输入] 已广播开始录音事件"); - }); - } - - Ok(()) -} - -/// 打开语音模式的悬浮输入框(别名,供语音模块调用) -pub fn open_floating_window_with_voice(app: &AppHandle) -> Result<(), WindowError> { - open_floating_window_voice_mode(app) -} - -/// 发送语音停止录音事件到截图输入框 -pub fn send_voice_stop_event(app: &AppHandle) -> Result<(), WindowError> { - use tauri::Emitter; - - app.emit("voice-stop-recording", ()) - .map_err(|e| WindowError::OperationFailed(format!("发送停止录音事件失败: {e}")))?; - info!("[语音输入] 已广播停止录音事件"); - Ok(()) -} - -/// 打开翻译模式的悬浮输入框 -/// -/// 自动开始录音,录音完成后使用指定的翻译指令处理 -/// -/// # 参数 -/// - `app`: Tauri 应用句柄 -/// - `instruction_id`: 翻译指令 ID -/// -/// # 返回 -/// 成功返回 Ok(()), 失败返回错误 -pub fn open_floating_window_with_translate( - app: &AppHandle, - instruction_id: &str, -) -> Result<(), WindowError> { - info!("打开翻译模式的悬浮输入框,指令: {}", instruction_id); - let encoded_instruction = urlencoding::encode(instruction_id); - let url = format!("/smart-input?voice=true&translate=true&instruction={encoded_instruction}"); - open_floating_window_with_url(app, &url) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_window_label() { - assert_eq!(FLOATING_WINDOW_LABEL, "smart-input"); - } -} diff --git a/src-tauri/src/services/README.md b/src-tauri/src/services/README.md index 3a8916486..8261505c1 100644 --- a/src-tauri/src/services/README.md +++ b/src-tauri/src/services/README.md @@ -42,10 +42,6 @@ - `voice_command_service.rs` - 转写/润色/输出业务流程 - `voice_recording_service.rs` - 录音状态封装与设备查询 -以下截图相关纯逻辑已迁移到 `crates/services/src/`: -- `screenshot_capture_service.rs` - 跨平台截图与临时文件清理 -- `screenshot_image_service.rs` - 图片读取与 Base64 编码 - ## Aster Agent 集成 Aster Agent 集成位于 `src-tauri/src/agent/` 目录: diff --git a/src-tauri/src/services/companion_service.rs b/src-tauri/src/services/companion_service.rs index 2389c141b..5107faba4 100644 --- a/src-tauri/src/services/companion_service.rs +++ b/src-tauri/src/services/companion_service.rs @@ -677,10 +677,8 @@ impl CompanionServiceState { let _ = self.send_pet_bubble("你说吧,我在认真听", 1600).await; - if let Err(error) = - crate::voice::window::open_voice_window(app_handle, Some("companion-pet")) - { - tracing::warn!("[Companion] 打开桌宠语音窗口失败: {}", error); + if let Err(error) = crate::voice::events::request_start_recording(app_handle) { + tracing::warn!("[Companion] 启动主输入框语音录音失败: {}", error); let _ = self .send_pet_bubble("语音入口暂时没打开,你先打字和我聊吧", 2200) .await; diff --git a/src-tauri/src/skills/execution.rs b/src-tauri/src/skills/execution.rs index f9bde4017..24160d875 100644 --- a/src-tauri/src/skills/execution.rs +++ b/src-tauri/src/skills/execution.rs @@ -341,6 +341,7 @@ pub async fn execute_named_skill( let execution_id_for_run = execution_id.clone(); let session_id_for_run = session_id.clone(); let user_input_for_run = effective_user_input.clone(); + let user_visible_input_for_run = user_input.clone(); let skill_images_for_run = skill_images.clone(); let provider_override_for_run = provider_override.clone(); let model_override_for_run = model_override.clone(); @@ -398,6 +399,7 @@ pub async fn execute_named_skill( &aster_state, &skill, &user_input_for_run, + Some(user_visible_input_for_run.as_str()), &skill_images_for_run, &execution_id_for_run, &session_id_for_run, @@ -429,6 +431,7 @@ pub async fn execute_skill_prompt( aster_state: &AsterAgentState, skill: &LoadedSkillDefinition, user_input: &str, + user_visible_input: Option<&str>, images: &[SkillInputImage], execution_id: &str, session_id: &str, @@ -443,6 +446,7 @@ pub async fn execute_skill_prompt( aster_state, skill, user_input, + user_visible_input, images, execution_id, session_id, @@ -467,7 +471,7 @@ pub async fn execute_skill_prompt( let finalized = finalize_skill_output( app_handle, &skill.skill_name, - user_input, + user_visible_input.unwrap_or(user_input), execution_id, result.output.as_deref().unwrap_or(""), ); @@ -488,6 +492,7 @@ pub async fn execute_skill_workflow( aster_state: &AsterAgentState, skill: &LoadedSkillDefinition, user_input: &str, + user_visible_input: Option<&str>, images: &[SkillInputImage], execution_id: &str, session_id: &str, @@ -499,6 +504,7 @@ pub async fn execute_skill_workflow( aster_state, skill, user_input, + user_visible_input, images, execution_id, session_id, @@ -516,6 +522,7 @@ pub async fn execute_skill_definition( aster_state: &AsterAgentState, skill: &LoadedSkillDefinition, user_input: &str, + user_visible_input: Option<&str>, images: &[SkillInputImage], execution_id: &str, session_id: &str, @@ -528,6 +535,7 @@ pub async fn execute_skill_definition( aster_state, skill, user_input, + user_visible_input, images, execution_id, session_id, @@ -541,6 +549,7 @@ pub async fn execute_skill_definition( aster_state, skill, user_input, + user_visible_input, images, execution_id, session_id, diff --git a/src-tauri/src/voice/commands.rs b/src-tauri/src/voice/commands.rs index 0447bc898..2dcb95dee 100644 --- a/src-tauri/src/voice/commands.rs +++ b/src-tauri/src/voice/commands.rs @@ -10,23 +10,10 @@ use super::config; use super::recording_service::{AudioDeviceInfo, RecordingServiceState}; use tauri::State; -fn normalize_shortcut(value: Option) -> Option { - value.and_then(|raw| { - let trimmed = raw.trim(); - if trimmed.is_empty() { - None - } else { - Some(trimmed.to_string()) - } - }) -} - #[derive(Debug, Clone, serde::Serialize)] pub struct VoiceShortcutRuntimeStatus { pub shortcut_registered: bool, pub registered_shortcut: Option, - pub translate_shortcut_registered: bool, - pub registered_translate_shortcut: Option, pub fn_supported: bool, pub fn_registered: bool, pub fn_fallback_shortcut: Option, @@ -52,8 +39,6 @@ pub async fn get_voice_shortcut_runtime_status() -> Result { - super::shortcut::update_translate( - &app, - new_shortcut, - &voice_config.translate_instruction_id, - )?; - } - (None, Some(new_shortcut)) => { - super::shortcut::register_translate( - &app, - new_shortcut, - &voice_config.translate_instruction_id, - )?; - } - (Some(_), None) => { - super::shortcut::unregister_translate(&app)?; - } - (None, None) => {} - } - } } else if old_enabled && !new_enabled { super::shortcut::unregister(&app)?; - let _ = super::shortcut::unregister_translate(&app); let _ = super::fn_shortcut::unregister(); } else if !old_enabled && new_enabled { super::shortcut::register(&app, &voice_config.shortcut)?; super::register_fn_shortcut_if_supported(&app); - - if let Some(translate_shortcut) = - normalize_shortcut(voice_config.translate_shortcut.clone()) - { - super::shortcut::register_translate( - &app, - &translate_shortcut, - &voice_config.translate_instruction_id, - )?; - } } config::save_voice_config(voice_config) @@ -149,18 +92,6 @@ pub async fn delete_voice_instruction(id: String) -> Result<(), String> { config::delete_voice_instruction(&id) } -/// 打开语音输入窗口 -#[command] -pub async fn open_voice_window(app: AppHandle, target: Option) -> Result<(), String> { - super::window::open_voice_window(&app, target.as_deref()) -} - -/// 关闭语音输入窗口 -#[command] -pub async fn close_voice_window(app: AppHandle) -> Result<(), String> { - super::window::close_voice_window(&app) -} - pub use lime_services::voice_command_service::{PolishResult, TranscribeResult}; /// 执行语音识别 diff --git a/src-tauri/src/voice/events.rs b/src-tauri/src/voice/events.rs new file mode 100644 index 000000000..71bc0f8a9 --- /dev/null +++ b/src-tauri/src/voice/events.rs @@ -0,0 +1,47 @@ +use tauri::{AppHandle, Emitter, Manager}; + +pub const VOICE_START_RECORDING_EVENT: &str = "voice-start-recording"; +pub const VOICE_STOP_RECORDING_EVENT: &str = "voice-stop-recording"; + +fn focus_main_window(app: &AppHandle) { + if let Some(window) = app.get_webview_window("main") { + if let Err(error) = window.unminimize() { + tracing::debug!("[语音输入] 恢复主窗口失败: {}", error); + } + if let Err(error) = window.show() { + tracing::debug!("[语音输入] 显示主窗口失败: {}", error); + } + if let Err(error) = window.set_focus() { + tracing::debug!("[语音输入] 聚焦主窗口失败: {}", error); + } + } else { + tracing::warn!("[语音输入] 未找到主窗口,仍广播录音快捷键事件"); + } +} + +fn emit_recording_event(app: &AppHandle, event_name: &str) -> Result<(), String> { + focus_main_window(app); + app.emit(event_name, ()) + .map_err(|error| format!("广播语音快捷键事件失败: {error}"))?; + tracing::info!("[语音输入] 已广播快捷键事件: {}", event_name); + Ok(()) +} + +pub fn request_start_recording(app: &AppHandle) -> Result<(), String> { + emit_recording_event(app, VOICE_START_RECORDING_EVENT) +} + +pub fn request_stop_recording(app: &AppHandle) -> Result<(), String> { + emit_recording_event(app, VOICE_STOP_RECORDING_EVENT) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn voice_shortcut_event_names_match_frontend_contract() { + assert_eq!(VOICE_START_RECORDING_EVENT, "voice-start-recording"); + assert_eq!(VOICE_STOP_RECORDING_EVENT, "voice-stop-recording"); + } +} diff --git a/src-tauri/src/voice/fn_shortcut.rs b/src-tauri/src/voice/fn_shortcut.rs index 9ac60db7a..d0210c217 100644 --- a/src-tauri/src/voice/fn_shortcut.rs +++ b/src-tauri/src/voice/fn_shortcut.rs @@ -95,14 +95,13 @@ mod platform { match (was_down, fn_down) { (false, true) => { info!("[语音输入] Fn 按下"); - if let Err(error) = crate::screenshot::window::open_floating_window_with_voice(app) - { - error!("[语音输入] Fn 打开语音窗口失败: {}", error); + if let Err(error) = crate::voice::events::request_start_recording(app) { + error!("[语音输入] Fn 发送开始录音事件失败: {}", error); } } (true, false) => { info!("[语音输入] Fn 释放,发送停止录音事件"); - if let Err(error) = crate::screenshot::window::send_voice_stop_event(app) { + if let Err(error) = crate::voice::events::request_stop_recording(app) { error!("[语音输入] Fn 发送停止录音事件失败: {}", error); } } diff --git a/src-tauri/src/voice/mod.rs b/src-tauri/src/voice/mod.rs index 5af0c1f96..384854da9 100644 --- a/src-tauri/src/voice/mod.rs +++ b/src-tauri/src/voice/mod.rs @@ -9,12 +9,12 @@ pub mod asr_service; pub mod commands; pub mod config; +pub mod events; pub mod fn_shortcut; pub mod output_service; pub mod processor; pub mod recording_service; pub mod shortcut; -pub mod window; use tauri::AppHandle; @@ -44,25 +44,6 @@ pub fn init(app: &AppHandle) -> Result<(), String> { shortcut::register(app, &config.shortcut)?; register_fn_shortcut_if_supported(app); - // 注册翻译快捷键(如果配置了) - if let Some(translate_shortcut) = &config.translate_shortcut { - if !translate_shortcut.is_empty() { - match shortcut::register_translate( - app, - translate_shortcut, - &config.translate_instruction_id, - ) { - Ok(()) => { - tracing::info!("[语音输入] 翻译快捷键已注册: {}", translate_shortcut); - } - Err(e) => { - tracing::warn!("[语音输入] 翻译快捷键注册失败: {}", e); - // 翻译快捷键注册失败不影响主功能 - } - } - } - } - tracing::info!("[语音输入] 模块初始化完成"); Ok(()) } @@ -72,15 +53,9 @@ pub fn cleanup(app: &AppHandle) -> Result<(), String> { // 注销快捷键 shortcut::unregister(app)?; - // 注销翻译快捷键 - let _ = shortcut::unregister_translate(app); - // 注销 Fn 监听 let _ = fn_shortcut::unregister(); - // 关闭悬浮窗口 - window::close_voice_window(app)?; - tracing::info!("[语音输入] 模块已清理"); Ok(()) } diff --git a/src-tauri/src/voice/shortcut.rs b/src-tauri/src/voice/shortcut.rs index e8b2ec2f3..9360b122c 100644 --- a/src-tauri/src/voice/shortcut.rs +++ b/src-tauri/src/voice/shortcut.rs @@ -11,23 +11,13 @@ use tracing::{debug, error, info, warn}; /// 当前注册的快捷键 static CURRENT_SHORTCUT: OnceLock>> = OnceLock::new(); -/// 当前注册的翻译快捷键 -static TRANSLATE_SHORTCUT: OnceLock>> = OnceLock::new(); - /// 快捷键是否已注册 static IS_REGISTERED: AtomicBool = AtomicBool::new(false); -/// 翻译快捷键是否已注册 -static IS_TRANSLATE_REGISTERED: AtomicBool = AtomicBool::new(false); - fn get_current_shortcut() -> &'static parking_lot::RwLock> { CURRENT_SHORTCUT.get_or_init(|| parking_lot::RwLock::new(None)) } -fn get_translate_shortcut() -> &'static parking_lot::RwLock> { - TRANSLATE_SHORTCUT.get_or_init(|| parking_lot::RwLock::new(None)) -} - fn parse_user_shortcut(shortcut_str: &str) -> Result { if let Some(reason) = crate::global_shortcut_guard::reserved_system_shortcut_reason(shortcut_str) @@ -79,16 +69,12 @@ pub fn register(app: &AppHandle, shortcut_str: &str) -> Result<(), String> { .on_shortcut(shortcut, move |_app, _shortcut, event| { if event.state == ShortcutState::Pressed { info!("[语音输入] 快捷键按下"); - // 打开截图输入框(语音模式) - if let Err(e) = - crate::screenshot::window::open_floating_window_with_voice(&app_clone) - { - error!("[语音输入] 打开窗口失败: {}", e); + if let Err(e) = super::events::request_start_recording(&app_clone) { + error!("[语音输入] 发送开始录音事件失败: {}", e); } } else { info!("[语音输入] 快捷键释放,发送停止录音事件"); - // 发送停止录音事件到前端 - if let Err(e) = crate::screenshot::window::send_voice_stop_event(&app_clone) { + if let Err(e) = super::events::request_stop_recording(&app_clone) { error!("[语音输入] 发送停止录音事件失败: {}", e); } } @@ -181,160 +167,6 @@ pub fn get_current() -> Option { get_current_shortcut().read().clone() } -/// 注册翻译模式快捷键 -pub fn register_translate( - app: &AppHandle, - shortcut_str: &str, - instruction_id: &str, -) -> Result<(), String> { - info!( - "[语音输入] 注册翻译快捷键: {}, 指令: {}", - shortcut_str, instruction_id - ); - - // 解析快捷键 - let shortcut = parse_user_shortcut(shortcut_str)?; - - // 获取全局快捷键管理器 - let global_shortcut = app.global_shortcut(); - - // 检查快捷键是否已被注册 - let is_already_registered = global_shortcut.is_registered(shortcut); - debug!( - "[语音输入] 翻译快捷键 {} 是否已注册: {}", - shortcut_str, is_already_registered - ); - - if is_already_registered { - warn!("[语音输入] 翻译快捷键已被注册: {}", shortcut_str); - // 如果是我们自己注册的,先注销 - if IS_TRANSLATE_REGISTERED.load(Ordering::SeqCst) { - info!("[语音输入] 尝试注销已有的翻译快捷键"); - if let Err(e) = global_shortcut.unregister(shortcut) { - error!("[语音输入] 注销已有翻译快捷键失败: {}", e); - } - } else { - return Err(format!("快捷键已被占用: {shortcut_str}")); - } - } - - // 克隆 app handle 和 instruction_id 用于回调 - let app_clone = app.clone(); - let instruction_id_owned = instruction_id.to_string(); - - // 注册快捷键 - info!("[语音输入] 开始注册翻译快捷键回调..."); - global_shortcut - .on_shortcut(shortcut, move |_app, _shortcut, event| { - if event.state == ShortcutState::Pressed { - info!("[语音输入] 翻译快捷键按下"); - // 打开截图输入框(翻译模式) - if let Err(e) = crate::screenshot::window::open_floating_window_with_translate( - &app_clone, - &instruction_id_owned, - ) { - error!("[语音输入] 打开翻译窗口失败: {}", e); - } - } else { - info!("[语音输入] 翻译快捷键释放,发送停止录音事件"); - // 发送停止录音事件到前端 - if let Err(e) = crate::screenshot::window::send_voice_stop_event(&app_clone) { - error!("[语音输入] 发送停止录音事件失败: {}", e); - } - } - }) - .map_err(|e| { - error!("[语音输入] 注册翻译快捷键失败: {}", e); - format!("注册翻译快捷键失败: {e}") - })?; - - // 更新状态 - IS_TRANSLATE_REGISTERED.store(true, Ordering::SeqCst); - *get_translate_shortcut().write() = Some(shortcut_str.to_string()); - - info!("[语音输入] 翻译快捷键已注册: {}", shortcut_str); - Ok(()) -} - -/// 注销翻译快捷键 -pub fn unregister_translate(app: &AppHandle) -> Result<(), String> { - let current = get_translate_shortcut().read().clone(); - - if let Some(shortcut_str) = current { - info!("[语音输入] 注销翻译快捷键: {}", shortcut_str); - - let shortcut: Shortcut = shortcut_str - .parse() - .map_err(|e| format!("解析快捷键失败: {e}"))?; - - let global_shortcut = app.global_shortcut(); - - if global_shortcut.is_registered(shortcut) { - global_shortcut - .unregister(shortcut) - .map_err(|e| format!("注销翻译快捷键失败: {e}"))?; - } - - // 更新状态 - IS_TRANSLATE_REGISTERED.store(false, Ordering::SeqCst); - *get_translate_shortcut().write() = None; - - info!("[语音输入] 翻译快捷键已注销"); - } else { - debug!("[语音输入] 没有已注册的翻译快捷键需要注销"); - } - - Ok(()) -} - -/// 检查翻译快捷键是否已注册 -pub fn is_translate_registered() -> bool { - IS_TRANSLATE_REGISTERED.load(Ordering::SeqCst) -} - -/// 获取当前已注册的翻译快捷键 -pub fn get_current_translate() -> Option { - get_translate_shortcut().read().clone() -} - -/// 更新翻译快捷键 -pub fn update_translate( - app: &AppHandle, - new_shortcut: &str, - instruction_id: &str, -) -> Result<(), String> { - info!("[语音输入] 更新翻译快捷键: {}", new_shortcut); - - // 保存旧快捷键以便恢复 - let old_shortcut = get_translate_shortcut().read().clone(); - - // 注销旧快捷键 - if let Err(e) = unregister_translate(app) { - warn!("[语音输入] 注销旧翻译快捷键失败: {}", e); - } - - // 注册新快捷键 - match register_translate(app, new_shortcut, instruction_id) { - Ok(()) => { - info!("[语音输入] 翻译快捷键更新成功: {}", new_shortcut); - Ok(()) - } - Err(e) => { - error!("[语音输入] 注册新翻译快捷键失败: {}", e); - - // 尝试恢复旧快捷键 - if let Some(old) = old_shortcut { - warn!("[语音输入] 尝试恢复旧翻译快捷键: {}", old); - if let Err(restore_err) = register_translate(app, &old, instruction_id) { - error!("[语音输入] 恢复旧翻译快捷键失败: {}", restore_err); - } - } - - Err(e) - } - } -} - #[cfg(test)] mod tests { use super::*; @@ -344,11 +176,6 @@ mod tests { *get_current_shortcut().write() = None; } - fn reset_translate_shortcut_state() { - IS_TRANSLATE_REGISTERED.store(false, Ordering::SeqCst); - *get_translate_shortcut().write() = None; - } - #[test] fn test_main_shortcut_runtime_accessors_reflect_state() { reset_main_shortcut_state(); @@ -365,25 +192,6 @@ mod tests { reset_main_shortcut_state(); } - #[test] - fn test_translate_shortcut_runtime_accessors_reflect_state() { - reset_translate_shortcut_state(); - - assert!(!is_translate_registered()); - assert_eq!(get_current_translate(), None); - - IS_TRANSLATE_REGISTERED.store(true, Ordering::SeqCst); - *get_translate_shortcut().write() = Some("CommandOrControl+Shift+T".to_string()); - - assert!(is_translate_registered()); - assert_eq!( - get_current_translate().as_deref(), - Some("CommandOrControl+Shift+T") - ); - - reset_translate_shortcut_state(); - } - #[test] fn test_parse_user_shortcut_rejects_input_method_reserved_shortcuts() { for shortcut in ["CommandOrControl+Space", "Ctrl+Space", "Super+Space"] { diff --git a/src-tauri/src/voice/window.rs b/src-tauri/src/voice/window.rs deleted file mode 100644 index 942488f84..000000000 --- a/src-tauri/src/voice/window.rs +++ /dev/null @@ -1,170 +0,0 @@ -//! 语音输入悬浮窗口管理 -//! -//! 创建和管理语音输入的悬浮窗口 - -use tauri::{AppHandle, Emitter, Manager, WebviewUrl, WebviewWindowBuilder}; - -const VOICE_WINDOW_LABEL: &str = "voice-input"; -const VOICE_WINDOW_WIDTH: f64 = 500.0; -const VOICE_WINDOW_HEIGHT: f64 = 80.0; - -#[derive(Clone, serde::Serialize)] -struct VoiceWindowResetPayload { - target: Option, -} - -/// 打开语音输入窗口 -pub fn open_voice_window(app: &AppHandle, target: Option<&str>) -> Result<(), String> { - let target = normalize_voice_window_target(target); - - // 检查窗口是否已存在 - if let Some(window) = app.get_webview_window(VOICE_WINDOW_LABEL) { - // 发送重置事件,让前端重新开始录音 - window - .emit( - "voice-reset", - VoiceWindowResetPayload { - target: target.clone(), - }, - ) - .map_err(|e| format!("发送重置事件失败: {e}"))?; - - // 移动到鼠标所在屏幕 - position_window_on_cursor_screen(&window)?; - - window.show().map_err(|e| e.to_string())?; - window.set_focus().map_err(|e| e.to_string())?; - tracing::info!("[语音输入] 窗口已存在,发送重置事件"); - return Ok(()); - } - - // 创建新窗口 - 使用 /smart-input 路由并带上 voice=true 参数 - let window = WebviewWindowBuilder::new( - app, - VOICE_WINDOW_LABEL, - WebviewUrl::App(build_voice_window_route(target.as_deref()).into()), - ) - .title("语音输入") - .inner_size(VOICE_WINDOW_WIDTH, VOICE_WINDOW_HEIGHT) - .resizable(false) - .decorations(false) - .always_on_top(true) - .transparent(false) // 关闭透明,避免 macOS 上的渲染问题 - .skip_taskbar(true) - .build() - .map_err(|e| format!("创建窗口失败: {e}"))?; - - // 移动到鼠标所在屏幕 - position_window_on_cursor_screen(&window)?; - - window.show().map_err(|e| e.to_string())?; - window.set_focus().map_err(|e| e.to_string())?; - - tracing::info!("[语音输入] 窗口已打开"); - Ok(()) -} - -fn normalize_voice_window_target(target: Option<&str>) -> Option { - target - .map(str::trim) - .filter(|value| !value.is_empty()) - .map(ToString::to_string) -} - -fn build_voice_window_route(target: Option<&str>) -> String { - match normalize_voice_window_target(target).as_deref() { - Some(target) => format!("/smart-input?voice=true&target={target}"), - None => "/smart-input?voice=true".to_string(), - } -} - -/// 将窗口定位到鼠标所在屏幕的中央 -fn position_window_on_cursor_screen(window: &tauri::WebviewWindow) -> Result<(), String> { - use tauri::PhysicalPosition; - - // 获取鼠标位置 - let cursor_pos = match window.cursor_position() { - Ok(pos) => pos, - Err(e) => { - tracing::warn!("[语音输入] 获取鼠标位置失败: {},使用默认居中", e); - window.center().map_err(|e| e.to_string())?; - return Ok(()); - } - }; - - // 获取所有显示器 - let monitors = window.available_monitors().map_err(|e| e.to_string())?; - - // 找到鼠标所在的显示器 - let target_monitor = monitors.iter().find(|m| { - let pos = m.position(); - let size = m.size(); - cursor_pos.x >= pos.x as f64 - && cursor_pos.x < (pos.x + size.width as i32) as f64 - && cursor_pos.y >= pos.y as f64 - && cursor_pos.y < (pos.y + size.height as i32) as f64 - }); - - if let Some(monitor) = target_monitor { - let monitor_pos = monitor.position(); - let monitor_size = monitor.size(); - let scale_factor = monitor.scale_factor(); - - // 计算窗口在该显示器上的居中位置 - let window_width = (VOICE_WINDOW_WIDTH * scale_factor) as i32; - let window_height = (VOICE_WINDOW_HEIGHT * scale_factor) as i32; - - let x = monitor_pos.x + (monitor_size.width as i32 - window_width) / 2; - let y = monitor_pos.y + (monitor_size.height as i32 - window_height) / 2; - - window - .set_position(PhysicalPosition::new(x, y)) - .map_err(|e| e.to_string())?; - - tracing::info!( - "[语音输入] 窗口定位到显示器 ({}, {}) 尺寸 {}x{}, 窗口位置 ({}, {})", - monitor_pos.x, - monitor_pos.y, - monitor_size.width, - monitor_size.height, - x, - y - ); - } else { - // 没找到对应显示器,使用默认居中 - tracing::warn!("[语音输入] 未找到鼠标所在显示器,使用默认居中"); - window.center().map_err(|e| e.to_string())?; - } - - Ok(()) -} - -/// 关闭语音输入窗口 -pub fn close_voice_window(app: &AppHandle) -> Result<(), String> { - if let Some(window) = app.get_webview_window(VOICE_WINDOW_LABEL) { - window.close().map_err(|e| e.to_string())?; - tracing::info!("[语音输入] 窗口已关闭"); - } - Ok(()) -} - -/// 更新窗口状态(发送事件到前端) -pub fn update_window_state(app: &AppHandle, state: &str) -> Result<(), String> { - if let Some(window) = app.get_webview_window(VOICE_WINDOW_LABEL) { - window - .emit("voice-state-change", state) - .map_err(|e| e.to_string())?; - } - Ok(()) -} - -/// 发送停止录音事件到前端 -pub fn send_stop_recording_event(app: &AppHandle) -> Result<(), String> { - if let Some(window) = app.get_webview_window(VOICE_WINDOW_LABEL) { - window - .emit("voice-stop-recording", ()) - .map_err(|e| format!("发送停止录音事件失败: {e}"))?; - tracing::info!("[语音输入] 已发送停止录音事件"); - } - Ok(()) -} diff --git a/src-tauri/tauri.conf.headless.json b/src-tauri/tauri.conf.headless.json index b18ef0e29..a3b38636d 100644 --- a/src-tauri/tauri.conf.headless.json +++ b/src-tauri/tauri.conf.headless.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Lime", - "version": "1.37.0", + "version": "1.38.0", "identifier": "com.limecloud.lime.headless", "build": { "beforeDevCommand": "npm run dev:web-bridge", diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 54af60a91..c9cde7f04 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Lime", - "version": "1.37.0", + "version": "1.38.0", "identifier": "com.limecloud.lime", "build": { "beforeDevCommand": "node scripts/start-tauri-dev-server.mjs", @@ -26,22 +26,6 @@ "titleBarStyle": "Overlay", "hiddenTitle": true, "dragDropEnabled": false - }, - { - "label": "smart-input", - "title": "", - "url": "/smart-input", - "width": 500, - "height": 70, - "resizable": false, - "decorations": false, - "alwaysOnTop": true, - "center": true, - "visible": false, - "focus": true, - "skipTaskbar": true, - "transparent": true, - "shadow": false } ], "security": { diff --git a/src/App.tsx b/src/App.tsx index 8fa30d326..878356310 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -23,7 +23,6 @@ import { isUserProjectType, resolveProjectRootPath, } from "./lib/api/project"; -import { useOnboardingState } from "./components/onboarding"; import { useDeepLink } from "./hooks/useDeepLink"; import { useRelayRegistry } from "./hooks/useRelayRegistry"; import { useSkillCatalogBootstrap } from "./hooks/useSkillCatalogBootstrap"; @@ -128,11 +127,6 @@ const CreateProjectDialog = lazy(() => default: module.CreateProjectDialog, })), ); -const OnboardingWizard = lazy(() => - import("./components/onboarding").then((module) => ({ - default: module.OnboardingWizard, - })), -); const ConnectConfirmDialog = lazy(() => import("./components/connect").then((module) => ({ default: module.ConnectConfirmDialog, @@ -154,7 +148,6 @@ function AppContent() { handleNavigate, } = useAppNavigation(); const [agentHasMessages, setAgentHasMessages] = useState(false); - const { needsOnboarding, completeOnboarding } = useOnboardingState(); const [projectDialogOpen, setProjectDialogOpen] = useState(false); const [pendingRecommendation, setPendingRecommendation] = useState<{ @@ -401,10 +394,6 @@ function AppContent() { preloadDefaultProject(); }, []); - const handleOnboardingComplete = useCallback(() => { - completeOnboarding(); - }, [completeOnboarding]); - const handleWindowDragStart = useCallback( (event: React.MouseEvent) => { void startWindowDragFromMouseEvent(event, { source: "app_shell" }); @@ -417,20 +406,6 @@ function AppContent() { return ; } - if (needsOnboarding === null) { - startupTracker.mark("AppContent: checking onboarding"); - return null; - } - - if (needsOnboarding) { - startupTracker.mark("AppContent: showing onboarding"); - return ( - - - - ); - } - startupTracker.mark("AppContent: rendering main app"); return ( diff --git a/src/RootRouter.test.tsx b/src/RootRouter.test.tsx index a6c9607d3..07dbb3517 100644 --- a/src/RootRouter.test.tsx +++ b/src/RootRouter.test.tsx @@ -18,10 +18,6 @@ vi.mock("./App", () => ({ default: () =>
主应用
, })); -vi.mock("./pages/smart-input", () => ({ - SmartInputPage: () =>
, -})); - vi.mock("./pages/update-notification", () => ({ UpdateNotificationPage: () =>
, })); @@ -102,14 +98,6 @@ describe("RootRouter", () => { expect(container.textContent).toContain("主应用"); }); - it("独立工具窗口不应触发主应用登录流程", async () => { - const { container } = await renderRootRouter("/smart-input"); - - expect( - container.querySelector('[data-testid="smart-input-page"]'), - ).not.toBeNull(); - }); - it("Windows 独立窗口从 index.html 入口启动时应映射到连接器引导页", async () => { const { container } = await renderRootRouter( "/index.html?lime_window=browser-connector-guide&mode=cdp", diff --git a/src/RootRouter.tsx b/src/RootRouter.tsx index 5951f0771..546ad5ed0 100644 --- a/src/RootRouter.tsx +++ b/src/RootRouter.tsx @@ -5,7 +5,6 @@ import { lazy, Suspense, useEffect } from "react"; import App from "./App"; -import { SmartInputPage } from "./pages/smart-input"; import { UpdateNotificationPage } from "./pages/update-notification"; import { BrowserRuntimeDebuggerPage } from "./pages"; import { ResourceManagerPage } from "./features/resource-manager"; @@ -43,7 +42,6 @@ function getEffectivePathname(location: Location): string { /** * 根据 URL 路径渲染对应的组件 * - * - /smart-input: 截图对话悬浮窗口(独立 Tauri 窗口,支持语音模式) * - /update-notification: 更新提醒悬浮窗口(独立 Tauri 窗口) * - /browser-runtime-debugger: 浏览器运行时独立调试窗口 * - /resource-manager: 独立资源管理器窗口 @@ -66,15 +64,6 @@ export function RootRouter() { ); }, [pathname]); - // 截图对话悬浮窗口路由(也用于语音输入) - if (pathname === "/smart-input") { - return ( - - - - ); - } - // 更新提醒悬浮窗口路由 if (pathname === "/update-notification") { return ( diff --git a/src/components/README.md b/src/components/README.md index 7bf174c8c..80be4281b 100644 --- a/src/components/README.md +++ b/src/components/README.md @@ -15,7 +15,6 @@ React 组件层,包含 UI 组件和业务组件。 - `mcp/` - MCP 服务器管理组件(配置管理、运行时控制、工具/提示词/资源浏览与调用) - `api-key-provider/` - API Key Provider 设置组件 - `routing/` - 路由规则配置组件 -- `smart-input/` - 截图/语音浮窗共享组件(当前仅保留快捷键设置) - `settings-v2/` - 设置页面组件(当前主实现) - `skills/` - 技能管理组件 - `widgets/` - 右侧小部件栏组件(移植自 Waveterm) diff --git a/src/components/agent/chat/AgentChatWorkspace.tsx b/src/components/agent/chat/AgentChatWorkspace.tsx index 198e808e5..4d0c050c6 100644 --- a/src/components/agent/chat/AgentChatWorkspace.tsx +++ b/src/components/agent/chat/AgentChatWorkspace.tsx @@ -434,6 +434,14 @@ function createTaskCenterDraftTabId(): string { return `${TASK_CENTER_DRAFT_TAB_PREFIX}-${Date.now().toString(36)}-${random}`; } +function createLocalImageWorkbenchSessionKey(): string { + const random = + typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" + ? crypto.randomUUID().slice(0, 8) + : Math.random().toString(36).slice(2, 10); + return `__local_image_workbench__:${Date.now().toString(36)}:${random}`; +} + function isTaskCenterDraftTabId(value: string): boolean { return value.startsWith(`${TASK_CENTER_DRAFT_TAB_PREFIX}-`); } @@ -2492,13 +2500,27 @@ export function AgentChatWorkspace({ clearPreparedRuntimeTeamState(); }, [clearPreparedRuntimeTeamState]); const localImageWorkbenchSessionKeyRef = useRef( - `__local_image_workbench__:${Date.now().toString(36)}:${Math.random() - .toString(36) - .slice(2, 8)}`, + createLocalImageWorkbenchSessionKey(), ); + const [localImageWorkbenchSessionKey, setLocalImageWorkbenchSessionKey] = + useState(() => localImageWorkbenchSessionKeyRef.current); + const resetLocalImageWorkbenchSessionScope = useCallback(() => { + const previousLocalSessionKey = localImageWorkbenchSessionKeyRef.current; + const nextLocalSessionKey = createLocalImageWorkbenchSessionKey(); + localImageWorkbenchSessionKeyRef.current = nextLocalSessionKey; + setLocalImageWorkbenchSessionKey(nextLocalSessionKey); + setImageWorkbenchBySessionId((previous) => { + if (!previous[previousLocalSessionKey]) { + return previous; + } + const next = { ...previous }; + delete next[previousLocalSessionKey]; + return next; + }); + }, []); const imageWorkbenchSessionKey = useMemo( - () => sessionId?.trim() || localImageWorkbenchSessionKeyRef.current, - [sessionId], + () => sessionId?.trim() || localImageWorkbenchSessionKey, + [localImageWorkbenchSessionKey, sessionId], ); const cachedImageWorkbenchState = useMemo(() => { const normalizedProjectId = normalizeProjectId(projectId); @@ -2593,7 +2615,7 @@ export function AgentChatWorkspace({ ); useEffect(() => { const normalizedSessionId = sessionId?.trim(); - const localSessionKey = localImageWorkbenchSessionKeyRef.current; + const localSessionKey = localImageWorkbenchSessionKey; if (!normalizedSessionId || normalizedSessionId === localSessionKey) { return; } @@ -2613,6 +2635,7 @@ export function AgentChatWorkspace({ ); }, [ imageWorkbenchBySessionId, + localImageWorkbenchSessionKey, sessionId, updateImageWorkbenchStateForSession, ]); @@ -4446,9 +4469,7 @@ export function AgentChatWorkspace({ const isTaskCenterDraftSendInFlight = Boolean( agentEntry === "claw" && activeTaskCenterDraftTab && - (taskCenterDraftSendRequest?.draftTabId === activeTaskCenterDraftTab.id || - isPreparingSend || - isSending), + taskCenterDraftSendRequest?.draftTabId === activeTaskCenterDraftTab.id, ); const shouldSuppressTaskCenterDraftContent = isTaskCenterDraftTabActive && !isTaskCenterDraftSendInFlight; @@ -4717,6 +4738,8 @@ export function AgentChatWorkspace({ status: "draft", }; + resetLocalImageWorkbenchSessionScope(); + clearMessages({ showToast: false }); startTransition(() => { setTaskCenterTransitionTopicId(null); setTaskCenterDetachedTopicId(null); @@ -4742,6 +4765,8 @@ export function AgentChatWorkspace({ return draftTab.id; }, [ + clearMessages, + resetLocalImageWorkbenchSessionScope, resetTopicLocalState, setInput, setMentionedCharacters, @@ -4987,6 +5012,7 @@ export function AgentChatWorkspace({ ); }; + resetLocalImageWorkbenchSessionScope(); setTaskCenterTransitionTopicId(topicId); setTaskCenterDetachedTopicId(null); setActiveTaskCenterDraftTabId(null); @@ -5036,6 +5062,7 @@ export function AgentChatWorkspace({ clearEntryPendingA2UI, markTaskCenterLocalSessionOverride, replaceTaskCenterOpenTabs, + resetLocalImageWorkbenchSessionScope, setActiveTaskCenterDraftTabId, switchTopic, taskCenterWorkspaceId, @@ -5048,6 +5075,7 @@ export function AgentChatWorkspace({ const handleOpenArchivedTaskTopic = useCallback( async (topicId: string) => { + resetLocalImageWorkbenchSessionScope(); setActiveTaskCenterDraftTabId(null); setTaskCenterDetachedTopicId(topicId); setTaskCenterTransitionTopicId(topicId); @@ -5071,7 +5099,12 @@ export function AgentChatWorkspace({ current === topicId ? null : current, ); }, - [clearEntryPendingA2UI, markTaskCenterLocalSessionOverride, switchTopic], + [ + clearEntryPendingA2UI, + markTaskCenterLocalSessionOverride, + resetLocalImageWorkbenchSessionScope, + switchTopic, + ], ); const handleSelectTaskCenterDraftTab = useCallback( @@ -5083,6 +5116,8 @@ export function AgentChatWorkspace({ return; } + resetLocalImageWorkbenchSessionScope(); + clearMessages({ showToast: false }); startTransition(() => { setTaskCenterTransitionTopicId(null); setTaskCenterDetachedTopicId(null); @@ -5095,7 +5130,14 @@ export function AgentChatWorkspace({ setMentionedCharacters([]); }); }, - [resetTopicLocalState, setInput, setMentionedCharacters, setSelectedText], + [ + clearMessages, + resetLocalImageWorkbenchSessionScope, + resetTopicLocalState, + setInput, + setMentionedCharacters, + setSelectedText, + ], ); const handleSwitchTaskTopic = useCallback( diff --git a/src/components/agent/chat/components/ChatModelSelector.integration.test.tsx b/src/components/agent/chat/components/ChatModelSelector.integration.test.tsx index 7efbe6b23..4583e4426 100644 --- a/src/components/agent/chat/components/ChatModelSelector.integration.test.tsx +++ b/src/components/agent/chat/components/ChatModelSelector.integration.test.tsx @@ -527,7 +527,12 @@ describe("ChatModelSelector + useAsterAgentChat 集成", () => { }); it("关闭后台预加载时,应在打开选择器后再加载 Provider 和模型", async () => { - const { container } = mount("ws-model-selector-lazy-provider-load", { + const workspaceId = "ws-model-selector-lazy-provider-load"; + localStorage.setItem(`agent_pref_provider_${workspaceId}`, "gemini"); + localStorage.setItem(`agent_pref_model_${workspaceId}`, "gemini-2.5-pro"); + localStorage.setItem(`agent_pref_migrated_${workspaceId}`, "true"); + + const { container } = mount(workspaceId, { chatModelSelectorProps: { backgroundPreload: "disabled", }, diff --git a/src/components/agent/chat/components/EmptyState.test.tsx b/src/components/agent/chat/components/EmptyState.test.tsx index 33f129468..ccd479d70 100644 --- a/src/components/agent/chat/components/EmptyState.test.tsx +++ b/src/components/agent/chat/components/EmptyState.test.tsx @@ -584,6 +584,102 @@ describe("EmptyState", () => { ).toBeNull(); }); + it("通用首页静态起手与引导文案应跟随 en-US 资源", async () => { + await changeLimeLocale("en-US"); + const container = renderEmptyState({ + activeTheme: "general", + serviceSkills: [], + }); + + await act(async () => { + await Promise.resolve(); + }); + + expect(container.textContent).toContain("Creation"); + expect(container.textContent).toContain("Ask Lime, spark the idea"); + expect(container.textContent).toContain( + "Say the goal and Lime will keep going with you.", + ); + expect(container.textContent).toContain("Guide help"); + expect(container.textContent).toContain("Writing"); + expect(container.textContent).toContain("Add knowledge"); + expect(container.textContent).toContain("More methods"); + expect(container.textContent).toContain("Start from these tasks"); + expect( + container.querySelector('[data-testid="home-scroll-cue"]')?.textContent, + ).toContain("Scroll down to see what Lime can help you do"); + + const guideTrigger = container.querySelector( + '[data-testid="home-guide-help-trigger"]', + ) as HTMLButtonElement | null; + act(() => { + guideTrigger?.click(); + }); + + expect(container.textContent).toContain( + "How do I add and use project knowledge?", + ); + expect(container.querySelector("textarea")?.placeholder).toContain( + "What would you like to learn?", + ); + }); + + it("非通用首页快速启动面板应跟随 en-US 资源", async () => { + await changeLimeLocale("en-US"); + const container = renderEmptyState({ + activeTheme: "image", + serviceSkills: [], + }); + + await act(async () => { + await Promise.resolve(); + }); + + expect(container.textContent).toContain("Quick start"); + expect(container.textContent).toContain( + "Choose a task template first, then keep adding details and follow-ups in this session.", + ); + expect(container.textContent).toContain("Generate image"); + expect(container.textContent).toContain("Organize as Notebook"); + expect(container.textContent).toContain("Enter research mode"); + }); + + it("仅发送路径引用时应使用当前 locale 的兜底 prompt", async () => { + await changeLimeLocale("en-US"); + const onSend = vi.fn(); + const container = renderEmptyState({ + activeTheme: "general", + input: "", + onSend, + pathReferences: [ + { + id: "path-1", + path: "/tmp/report.md", + name: "report.md", + isDir: false, + source: "file_manager", + }, + ], + }); + + await act(async () => { + await Promise.resolve(); + }); + + const sendButton = container.querySelector( + 'button[aria-label="Send"]', + ) as HTMLButtonElement | null; + expect(sendButton).toBeTruthy(); + + act(() => { + sendButton?.click(); + }); + + expect(onSend.mock.calls[0]?.[0]).toBe( + "Please review these files or folders.", + ); + }); + it("从 Skills 页带回的技能应显示在首页输入框内的 @ 标签", async () => { const onSend = vi.fn(); const skill = { @@ -628,14 +724,19 @@ describe("EmptyState", () => { sendButton?.click(); }); - expect(onSend).toHaveBeenCalledWith("整理最近发布计划", "react", undefined, { - capabilityRoute: { - kind: "installed_skill", - skillKey: "writer", - skillName: "写作助手", + expect(onSend).toHaveBeenCalledWith( + "整理最近发布计划", + "react", + undefined, + { + capabilityRoute: { + kind: "installed_skill", + skillKey: "writer", + skillName: "写作助手", + }, + displayContent: "整理最近发布计划", }, - displayContent: "整理最近发布计划", - }); + ); }); it("首页添加资料入口应打开输入框资料中枢,而不是预填一段说明", async () => { @@ -2635,6 +2736,9 @@ describe("EmptyState", () => { }); const textarea = container.querySelector("textarea"); + expect(textarea?.getAttribute("placeholder")).toContain( + "直接说一句话,例如:", + ); expect(textarea?.getAttribute("placeholder")).toContain( "帮我用 GitHub 查一下 AI Agent 项目", ); diff --git a/src/components/agent/chat/components/EmptyState.tsx b/src/components/agent/chat/components/EmptyState.tsx index 689e873ca..31d967c3a 100644 --- a/src/components/agent/chat/components/EmptyState.tsx +++ b/src/components/agent/chat/components/EmptyState.tsx @@ -92,6 +92,7 @@ import { getSiteSkillAutoLaunchExample, hasAutoLaunchableSiteSkill, } from "../service-skills/siteSkillExamplePrompts"; +import { buildServiceSkillHomeCopy } from "../service-skills/homeCopy"; import { listFeaturedHomeServiceSkills } from "../service-skills/homeEntrySkills"; import type { SceneAppEntryCardItem } from "../sceneappEntryTypes"; import type { RuntimeToolAvailability } from "../utils/runtimeToolAvailability"; @@ -111,11 +112,8 @@ import { buildHomeSkillSections, buildHomeStarterChips, } from "../home/buildHomeSkillSurface"; -import { - HOME_COMPOSER_PLACEHOLDER, - HOME_GUIDE_HELP_CONTEXT_LABEL, - HOME_GUIDE_HELP_PLACEHOLDER, -} from "../home/homeSurfaceCopy"; +import { buildHomeSurfaceCopy } from "../home/homeSurfaceCopy"; +import { buildInputbarCoreCopy } from "./Inputbar/components/inputbarCoreCopy"; import type { HomeGuideCard, HomeSkillSurfaceItem, @@ -439,22 +437,6 @@ const THEME_ICONS: Record = { general: "✨", }; -const THEME_WORKBENCH_COPY: Record< - string, - { - title: string; - description: string; - supportingDescription?: string; - } -> = { - general: { - title: "", - description: "说一句目标,Lime 就接着帮你做。", - supportingDescription: - "文案、图片、视频、搜索和网页任务围绕同一目标持续推进,并沉淀上下文、偏好和做法。", - }, -}; - function truncatePrompt(value: string, maxLength = 92) { const normalized = value.trim().replace(/\s+/g, " "); if (normalized.length <= maxLength) { @@ -511,7 +493,7 @@ export const EmptyState: React.FC = ({ onResumeRecentSceneApp, recentSessionTitle = null, recentSessionSummary = null, - recentSessionActionLabel = "继续最近会话", + recentSessionActionLabel, onResumeRecentSession, onOpenSceneAppsDirectory, projectId = null, @@ -538,13 +520,27 @@ export const EmptyState: React.FC = ({ onToggleFileManager, }) => { const { t } = useTranslation("agent"); - const curatedTaskTemplateCopy = useMemo( - () => - buildCuratedTaskTemplateCopy((key, values) => - t(key as AgentI18nKey, values ?? {}), - ), + const translateAgentCopyKey = useCallback( + (key: string, values?: Record) => + t(key as AgentI18nKey, values ?? {}), [t], ); + const curatedTaskTemplateCopy = useMemo( + () => buildCuratedTaskTemplateCopy(translateAgentCopyKey), + [translateAgentCopyKey], + ); + const serviceSkillHomeCopy = useMemo( + () => buildServiceSkillHomeCopy(translateAgentCopyKey), + [translateAgentCopyKey], + ); + const homeSurfaceCopy = useMemo( + () => buildHomeSurfaceCopy(translateAgentCopyKey), + [translateAgentCopyKey], + ); + const inputbarCoreCopy = useMemo( + () => buildInputbarCoreCopy(translateAgentCopyKey), + [translateAgentCopyKey], + ); const pageContainerRef = useRef(null); const handledInitialInputCapabilitySignatureRef = useRef(""); const [activeCapability, setActiveCapability] = @@ -859,7 +855,11 @@ export const EmptyState: React.FC = ({ setPendingImages((prev) => [...prev, image]); }) .catch(() => { - toast.error(`图片读取失败: ${file.name || "未命名图片"}`); + toast.error( + homeSurfaceCopy.toast.imageReadFailed( + file.name || homeSurfaceCopy.toast.unnamedImage, + ), + ); }); }); @@ -878,11 +878,15 @@ export const EmptyState: React.FC = ({ .then((image) => { setPendingImages((prev) => [...prev, image]); if (index === 0) { - toast.success("已粘贴图片"); + toast.success(homeSurfaceCopy.toast.imagePasted); } }) .catch(() => { - toast.error(`图片读取失败: ${file.name || "未命名图片"}`); + toast.error( + homeSurfaceCopy.toast.imageReadFailed( + file.name || homeSurfaceCopy.toast.unnamedImage, + ), + ); }); }); }; @@ -922,11 +926,15 @@ export const EmptyState: React.FC = ({ .then((image) => { setPendingImages((prev) => [...prev, image]); if (index === 0) { - toast.success("已添加图片"); + toast.success(homeSurfaceCopy.toast.imageAdded); } }) .catch(() => { - toast.error(`图片读取失败: ${file.name || "未命名图片"}`); + toast.error( + homeSurfaceCopy.toast.imageReadFailed( + file.name || homeSurfaceCopy.toast.unnamedImage, + ), + ); }); }); return; @@ -935,7 +943,7 @@ export const EmptyState: React.FC = ({ if (files && files.length > 0) { event.preventDefault(); event.stopPropagation(); - toast.error("无法读取系统文件路径,请从内置文件管理器拖入。"); + toast.error(homeSurfaceCopy.toast.systemPathDropUnsupported); } }; @@ -981,7 +989,7 @@ export const EmptyState: React.FC = ({ const effectiveInput = inputOverride.trim() ? inputOverride : hasPathReferences - ? "请查看这些文件或文件夹。" + ? homeSurfaceCopy.composerPathReferenceFallbackPrompt : inputOverride; const sendOptions = capabilityDispatch.capabilityRoute || @@ -1008,14 +1016,13 @@ export const EmptyState: React.FC = ({ clearSelectedSkill?.(); }; - const workbenchCopy = - THEME_WORKBENCH_COPY[activeTheme] || THEME_WORKBENCH_COPY.general; - // Dynamic Placeholder const getPlaceholder = () => { return hasAutoLaunchSiteSkill - ? `直接说一句话,例如:${siteSkillAutoLaunchExample}` - : HOME_COMPOSER_PLACEHOLDER; + ? homeSurfaceCopy.composerAutoLaunchPlaceholder( + siteSkillAutoLaunchExample, + ) + : homeSurfaceCopy.composerPlaceholder; }; const handleApplyRecommendation = useCallback( @@ -1105,10 +1112,10 @@ export const EmptyState: React.FC = ({ options.referenceSelection.referenceEntries, ); setCuratedTaskLauncherPrefillHint( - `已按最近判断切到更适合的结果模板,你可以继续改后再进入生成。`, + homeSurfaceCopy.curatedTaskReviewSuggestionPrefillHint, ); }, - [], + [homeSurfaceCopy.curatedTaskReviewSuggestionPrefillHint], ); const handleApplyCuratedTaskTemplate = useCallback( @@ -1201,67 +1208,15 @@ export const EmptyState: React.FC = ({ key: `${activeTheme}-${shortLabel}`, title: shortLabel, description: truncatePrompt(fullPrompt), - badge: `${THEME_ICONS[activeTheme] || "✨"} 快速启动`, + badge: homeSurfaceCopy.quickActions.badge( + THEME_ICONS[activeTheme] || "✨", + ), prompt: fullPrompt, })), - [activeTheme, currentRecommendations], + [activeTheme, currentRecommendations, homeSurfaceCopy.quickActions], ); - const quickStartPresets = useMemo(() => { - const presets = [ - { - key: "generate-image", - label: "生成配图", - icon: "✨", - prompt: - "请帮我生成一张适合当前主题的高质量图片,并先帮我整理一版可直接用于生图模型的详细 Prompt。", - }, - { - key: "join-notebook", - label: "整理为 Notebook", - icon: "📒", - prompt: - "请把这个主题整理成 notebook 工作方式:背景、资料、思路、草稿、待办分栏组织。", - }, - { - key: "create-skill", - label: "设计 Skill", - icon: "🧩", - prompt: - "请帮我设计一个可复用的 Skill,先定义适用场景、输入输出、执行步骤和失败回退策略。", - }, - { - key: "create-slides", - label: "生成演示稿", - icon: "🖥️", - prompt: - "请基于当前主题生成一份演示文稿结构,包含封面、目录、核心论点、案例页和结论页。", - }, - { - key: "frontend-design", - label: "前端界面方案", - icon: "🌐", - prompt: - "请帮我设计一个前端界面方案,先给出信息架构、关键模块、视觉方向和组件层级。", - }, - { - key: "copymail-skill", - label: "专业邮件草稿", - icon: "✉️", - prompt: - "请帮我起草一封专业邮件,先确认收件对象、语气、目标和希望对方采取的下一步动作。", - }, - { - key: "research-skills", - label: "进入研究模式", - icon: "🔎", - prompt: - "请先进入研究模式,帮我围绕当前主题做信息收集、观点归纳、风险点识别和结论总结。", - }, - ]; - - return presets; - }, []); + const quickStartPresets = homeSurfaceCopy.quickActions.presets; const [homeCatalogEntries, setHomeCatalogEntries] = useState< SkillCatalogEntry[] @@ -1318,16 +1273,16 @@ export const EmptyState: React.FC = ({ [serviceSkills], ); const homeStarterChips = useMemo( - () => buildHomeStarterChips(homeCatalogEntries), - [homeCatalogEntries], + () => buildHomeStarterChips(homeCatalogEntries, homeSurfaceCopy), + [homeCatalogEntries, homeSurfaceCopy], ); const homeInputSuggestions = useMemo( - () => buildHomeInputSuggestions(homeCatalogEntries), - [homeCatalogEntries], + () => buildHomeInputSuggestions(homeCatalogEntries, homeSurfaceCopy), + [homeCatalogEntries, homeSurfaceCopy], ); const homeGuideCards = useMemo( - () => buildHomeGuideCards(homeCatalogEntries), - [homeCatalogEntries], + () => buildHomeGuideCards(homeCatalogEntries, homeSurfaceCopy), + [homeCatalogEntries, homeSurfaceCopy], ); const guideHelpStarterLabel = useMemo( () => @@ -1336,13 +1291,14 @@ export const EmptyState: React.FC = ({ [homeStarterChips], ); const guideHelpLabel = guideHelpStarterLabel - ? `Lime ${guideHelpStarterLabel}` - : HOME_GUIDE_HELP_CONTEXT_LABEL; + ? homeSurfaceCopy.guideHelpContextLabelWithStarter(guideHelpStarterLabel) + : homeSurfaceCopy.guideHelpContextLabel; const homeSkillItems = useMemo(() => { void slashEntryUsageVersion; return buildHomeSkillItems({ curatedTasks: curatedTaskTemplates, + serviceSkillHomeCopy, catalogSceneEntries: homeCatalogSceneEntries, serviceSkills: homeServiceSkillItems, installedSkills: skillSelection.skills ?? [], @@ -1354,13 +1310,14 @@ export const EmptyState: React.FC = ({ featuredSceneApps, homeCatalogSceneEntries, homeServiceSkillItems, + serviceSkillHomeCopy, skillSelection.skills, slashEntryUsageVersion, ]); const homeSkillSections = useMemo( - () => buildHomeSkillSections(homeSkillItems), - [homeSkillItems], + () => buildHomeSkillSections(homeSkillItems, homeSurfaceCopy), + [homeSkillItems, homeSurfaceCopy], ); const homeGalleryItems = useMemo( () => buildHomeGalleryItems(homeSkillItems, "all"), @@ -1535,11 +1492,18 @@ export const EmptyState: React.FC = ({ const recentSessionLinkLabel = useMemo(() => { const normalizedTitle = truncatePrompt(recentSessionTitle || "", 18); + const effectiveRecentSessionActionLabel = + recentSessionActionLabel ?? + homeSurfaceCopy.chrome.recentSessionDefaultActionLabel; if (!normalizedTitle) { - return recentSessionActionLabel; + return effectiveRecentSessionActionLabel; } - return `${recentSessionActionLabel} · ${normalizedTitle}`; - }, [recentSessionActionLabel, recentSessionTitle]); + return `${effectiveRecentSessionActionLabel} · ${normalizedTitle}`; + }, [ + homeSurfaceCopy.chrome.recentSessionDefaultActionLabel, + recentSessionActionLabel, + recentSessionTitle, + ]); const recentSessionLinkTitle = useMemo( () => [recentSessionTitle, recentSessionSummary] @@ -1565,7 +1529,7 @@ export const EmptyState: React.FC = ({ if (canResumeRecentSceneApp && onResumeRecentSceneApp) { actions.push({ id: "recent-sceneapp", - label: "继续最近做法", + label: homeSurfaceCopy.chrome.recentSceneAppActionLabel, testId: "entry-sceneapp-resume", onSelect: onResumeRecentSceneApp, }); @@ -1574,6 +1538,7 @@ export const EmptyState: React.FC = ({ return actions; }, [ canResumeRecentSceneApp, + homeSurfaceCopy.chrome.recentSceneAppActionLabel, onResumeRecentSceneApp, onResumeRecentSession, recentSessionLinkLabel, @@ -1586,7 +1551,9 @@ export const EmptyState: React.FC = ({ = ({ effectiveDefaultCuratedTaskReferenceMemoryIds, activeCuratedTaskReferenceEntries || effectiveDefaultCuratedTaskReferenceEntries, - "已按最近判断切到更适合的结果模板,你可以继续改后再进入生成。", + homeSurfaceCopy.curatedTaskReviewSuggestionPrefillHint, ) : undefined } @@ -1651,6 +1618,8 @@ export const EmptyState: React.FC = ({ onToggleKnowledgeCompanionPack={onToggleKnowledgeCompanionPack} onStartKnowledgeOrganize={onStartKnowledgeOrganize} onManageKnowledgePacks={onManageKnowledgePacks} + copy={homeSurfaceCopy.composer} + inputbarCopy={inputbarCoreCopy} showCreationModeSelector={showCreationModeSelector} creationMode={creationMode} onCreationModeChange={onCreationModeChange} @@ -1688,8 +1657,8 @@ export const EmptyState: React.FC = ({ const defaultQuickActionsPanel = ( = ({ const homeStartSurfacePanel = ( = ({ = ({ - 向下滑,看看 Lime 可以帮你做什么 + {homeSurfaceCopy.chrome.scrollCueLabel} ↓ @@ -1759,13 +1729,14 @@ export const EmptyState: React.FC = ({ {isGeneralTheme && homeGalleryItems.length > 0 ? ( diff --git a/src/components/agent/chat/components/EmptyStateComposerPanel.test.tsx b/src/components/agent/chat/components/EmptyStateComposerPanel.test.tsx index 10e20977d..ade4c7a33 100644 --- a/src/components/agent/chat/components/EmptyStateComposerPanel.test.tsx +++ b/src/components/agent/chat/components/EmptyStateComposerPanel.test.tsx @@ -7,6 +7,17 @@ import { createSkillSelectionProps, type SkillSelectionProps, } from "../skill-selection/skillSelectionBindings"; +import agentResource from "@/i18n/resources/zh-CN/agent.json"; +import enAgentResource from "@/i18n/resources/en-US/agent.json"; +import { + buildHomeSurfaceCopy, + type HomeSurfaceCopyKey, +} from "../home/homeSurfaceCopy"; +import { + buildInputbarCoreCopy, + type InputbarCoreCopyKey, +} from "./Inputbar/components/inputbarCoreCopy"; +import { changeLimeLocale } from "@/i18n/createI18n"; vi.mock("./ChatModelSelector", () => ({ ChatModelSelector: () =>
, @@ -45,6 +56,34 @@ vi.mock("./Inputbar/components/TeamSelector", () => ({ ), })); +function translateResource( + resource: Partial>, + key: HomeSurfaceCopyKey | InputbarCoreCopyKey, + values?: Record, +) { + return Object.entries(values ?? {}).reduce( + (text, [name, value]) => + text.split(`{{${name}}}`).join(String(value)), + resource[key] ?? key, + ); +} + +const TEST_COMPOSER_COPY = buildHomeSurfaceCopy((key, values) => + translateResource(agentResource, key, values), +).composer; + +const TEST_EN_COMPOSER_COPY = buildHomeSurfaceCopy((key, values) => + translateResource(enAgentResource, key, values), +).composer; + +const TEST_INPUTBAR_CORE_COPY = buildInputbarCoreCopy((key, values) => + translateResource(agentResource, key, values), +); + +const TEST_EN_INPUTBAR_CORE_COPY = buildInputbarCoreCopy((key, values) => + translateResource(enAgentResource, key, values), +); + const mockSelectedTeam = { id: "frontend-triage-team", source: "builtin" as const, @@ -105,12 +144,13 @@ function createGithubSearchServiceSkill() { const mountedRoots: Array<{ root: Root; container: HTMLDivElement }> = []; -beforeEach(() => { +beforeEach(async () => { ( globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean; } ).IS_REACT_ACT_ENVIRONMENT = true; + await changeLimeLocale("zh-CN"); }); afterEach(() => { @@ -162,6 +202,8 @@ function renderPanel( isGeneralTheme: false, characters: [], skillSelection: createSkillSelection(), + copy: TEST_COMPOSER_COPY, + inputbarCopy: TEST_INPUTBAR_CORE_COPY, showCreationModeSelector: false, creationMode: "guided", onCreationModeChange: vi.fn(), @@ -218,6 +260,8 @@ function renderStatefulPanel( isGeneralTheme characters={[]} skillSelection={createSkillSelection()} + copy={TEST_COMPOSER_COPY} + inputbarCopy={TEST_INPUTBAR_CORE_COPY} showCreationModeSelector={false} creationMode="guided" onCreationModeChange={vi.fn()} @@ -360,6 +404,62 @@ describe("EmptyStateComposerPanel", () => { expect(onToggleFileManager).toHaveBeenCalledTimes(1); }); + it("首页空态输入区 chrome 文案应支持 en-US copy", () => { + const container = renderPanel({ + copy: TEST_EN_COMPOSER_COPY, + inputbarCopy: TEST_EN_INPUTBAR_CORE_COPY, + isGeneralTheme: true, + guideHelpActive: true, + onClearGuideHelp: vi.fn(), + onToggleFileManager: vi.fn(), + showCreationModeSelector: true, + }); + + const guideBadge = container.querySelector( + '[data-testid="home-guide-help-active-badge"]', + ); + expect(guideBadge?.textContent).toContain("Lime guide help"); + + const guideCloseButton = guideBadge?.querySelector("button"); + expect(guideCloseButton?.getAttribute("aria-label")).toBe( + "Close Lime guide help", + ); + expect(guideCloseButton?.getAttribute("title")).toBe("Close guide help"); + expect( + container + .querySelector('[data-testid="home-guide-help-toolbar-badge"]') + ?.getAttribute("title"), + ).toBe("Close guide help"); + + const advancedToggle = container.querySelector( + '[data-testid="empty-state-advanced-toggle"]', + ) as HTMLButtonElement | null; + expect(advancedToggle?.getAttribute("aria-label")).toBe( + "Expand advanced settings", + ); + expect(advancedToggle?.getAttribute("title")).toBe( + "Expand advanced settings", + ); + expect(advancedToggle?.textContent).toContain("Advanced settings"); + expect(container.textContent).toContain("Current model"); + + const fileManagerToggle = container.querySelector( + '[data-testid="inputbar-file-manager-toggle"]', + ) as HTMLButtonElement | null; + expect(fileManagerToggle?.getAttribute("aria-label")).toBe( + "Open file manager sidebar", + ); + + expandAdvancedControls(container); + + expect( + container + .querySelector('[data-testid="empty-state-advanced-toggle"]') + ?.getAttribute("aria-label"), + ).toBe("Collapse advanced settings"); + expect(container.textContent).toContain("General task context"); + }); + it("首页空态输入区应把项目资料作为底栏主入口", () => { const container = renderPanel({ knowledgePackSelection: { diff --git a/src/components/agent/chat/components/EmptyStateComposerPanel.tsx b/src/components/agent/chat/components/EmptyStateComposerPanel.tsx index 9649fa2de..5a6e3d96e 100644 --- a/src/components/agent/chat/components/EmptyStateComposerPanel.tsx +++ b/src/components/agent/chat/components/EmptyStateComposerPanel.tsx @@ -50,6 +50,7 @@ import { buildSkillSelectionBindings, type SkillSelectionProps, } from "../skill-selection/skillSelectionBindings"; +import type { HomeSurfaceComposerCopy } from "../home/homeSurfaceCopy"; import type { AgentAccessMode } from "../hooks/agentChatStorage"; import type { InputCapabilitySelection, @@ -64,6 +65,7 @@ import type { InputbarKnowledgePackOption, InputbarKnowledgePackSelection, } from "./Inputbar/types"; +import type { InputbarCoreCopy } from "./Inputbar/components/inputbarCoreCopy"; interface EmptyStateComposerPanelProps { input: string; @@ -104,6 +106,8 @@ interface EmptyStateComposerPanelProps { onToggleKnowledgeCompanionPack?: (packName: string, enabled: boolean) => void; onStartKnowledgeOrganize?: () => void; onManageKnowledgePacks?: () => void; + copy: HomeSurfaceComposerCopy; + inputbarCopy: InputbarCoreCopy; showCreationModeSelector: boolean; creationMode: CreationMode; onCreationModeChange?: (mode: CreationMode) => void; @@ -137,9 +141,13 @@ interface EmptyStateComposerPanelProps { function GuideHelpBadge({ label, + closeLabel, + closeTitle, onClear, }: { label: string; + closeLabel: string; + closeTitle: string; onClear: () => void; }) { return ( @@ -154,8 +162,8 @@ function GuideHelpBadge({ type="button" onClick={onClear} className="ml-0.5 text-emerald-800/70 hover:opacity-70" - aria-label={`关闭${label}`} - title="关闭引导帮助" + aria-label={closeLabel} + title={closeTitle} > @@ -165,9 +173,11 @@ function GuideHelpBadge({ function GuideHelpToolbarBadge({ label, + title, onClear, }: { label: string; + title: string; onClear: () => void; }) { return ( @@ -175,7 +185,7 @@ function GuideHelpToolbarBadge({ type="button" data-testid="home-guide-help-toolbar-badge" className="inline-flex min-h-8 max-w-full items-center gap-1.5 rounded-full border border-emerald-200/80 bg-emerald-50/90 px-3 text-xs font-semibold text-emerald-900 shadow-sm shadow-emerald-950/5 transition hover:bg-white hover:text-emerald-950" - title="关闭引导帮助" + title={title} onClick={onClear} > @@ -222,6 +232,8 @@ export function EmptyStateComposerPanel({ onToggleKnowledgeCompanionPack, onStartKnowledgeOrganize, onManageKnowledgePacks, + copy, + inputbarCopy, showCreationModeSelector, creationMode, onCreationModeChange, @@ -249,7 +261,7 @@ export function EmptyStateComposerPanel({ onToggleFileManager, inputSuggestions = [], guideHelpActive = false, - guideHelpLabel = "Lime 引导帮助", + guideHelpLabel, onClearGuideHelp, }: EmptyStateComposerPanelProps) { const [draftInput, setDraftInput] = useState(input); @@ -407,6 +419,8 @@ export function EmptyStateComposerPanel({ } }; + const effectiveGuideHelpLabel = + guideHelpLabel ?? copy.guideHelpDefaultLabel; const topExtra = guideHelpActive || activeBuiltinCommand || @@ -418,7 +432,9 @@ export function EmptyStateComposerPanel({ <> {guideHelpActive ? ( undefined)} /> ) : null} @@ -524,6 +540,12 @@ export function EmptyStateComposerPanel({ Boolean(onToggleFileManager); const shouldShowLeftExtra = Boolean(knowledgePackControl) || shouldShowAdvancedToggle; + const advancedToggleLabel = showAdvancedControls + ? copy.advancedSettings.collapse + : copy.advancedSettings.expand; + const fileManagerToggleLabel = fileManagerOpen + ? copy.fileManager.close + : copy.fileManager.open; const leftExtra = shouldShowLeftExtra ? ( <> {knowledgePackControl} @@ -532,10 +554,10 @@ export function EmptyStateComposerPanel({ setShowAdvancedControls((previous) => !previous)} > - 高级设置 + {copy.advancedSettings.label} {showAdvancedControls ? ( ) : ( @@ -556,7 +578,8 @@ export function EmptyStateComposerPanel({ {guideHelpActive ? ( undefined)} /> ) : null} @@ -565,9 +588,11 @@ export function EmptyStateComposerPanel({ - 当前模型 + + {copy.currentModel.label} + {trimmedModel} ) : null} @@ -576,10 +601,8 @@ export function EmptyStateComposerPanel({ @@ -639,7 +662,7 @@ export function EmptyStateComposerPanel({
- 选择创作模式 + {copy.creationMode.label}
{( Object.entries(CREATION_MODE_CONFIG) as [ @@ -664,7 +687,7 @@ export function EmptyStateComposerPanel({ className={EMPTY_STATE_PASSIVE_BADGE_CLASSNAME} > - 通用任务上下文 + {copy.generalContext} ) : null} @@ -706,6 +729,7 @@ export function EmptyStateComposerPanel({ /> ); diff --git a/src/components/agent/chat/components/ImageWorkbenchMessagePreview.test.tsx b/src/components/agent/chat/components/ImageWorkbenchMessagePreview.test.tsx index 1e0c300a7..0893c9328 100644 --- a/src/components/agent/chat/components/ImageWorkbenchMessagePreview.test.tsx +++ b/src/components/agent/chat/components/ImageWorkbenchMessagePreview.test.tsx @@ -6,6 +6,7 @@ import type { MessageImageWorkbenchPreview } from "../types"; import { ImageWorkbenchMessagePreview } from "./ImageWorkbenchMessagePreview"; const mountedRoots: Array<{ root: Root; container: HTMLDivElement }> = []; +const IMAGE_WORKBENCH_TASK_ACTION_EVENT = "lime:image-workbench-task-action"; function renderPreview(preview: MessageImageWorkbenchPreview) { const container = document.createElement("div"); @@ -60,6 +61,13 @@ describe("ImageWorkbenchMessagePreview", () => { expect(container.textContent).toContain("Image Generation"); expect(container.textContent).toContain("Nanobanana Pro"); expect(container.textContent).toContain("搞定,已生成这张图。"); + expect( + container + .querySelector( + '[data-testid="image-workbench-message-preview-toolbar-image-preview-complete"]', + ) + ?.className.toString(), + ).not.toContain("border"); expect(container.querySelector("img")?.getAttribute("src")).toBe( "data:image/png;base64,aW1hZ2U=", ); @@ -101,4 +109,51 @@ describe("ImageWorkbenchMessagePreview", () => { expect(container.textContent).not.toContain("参考图"); expect(container.textContent).not.toContain("部分完成"); }); + + it("failed preview exposes a dedicated retry action without replacing the open card", () => { + const { container } = renderPreview({ + taskId: "image-preview-failed", + prompt: "Create a lime poster", + mode: "generate", + status: "failed", + projectId: "project-1", + contentId: "content-1", + statusMessage: "Provider timeout", + }); + + let actionDetail: Record | null = null; + const handleAction = (event: Event) => { + if (!(event instanceof CustomEvent)) { + return; + } + actionDetail = event.detail as Record; + }; + window.addEventListener(IMAGE_WORKBENCH_TASK_ACTION_EVENT, handleAction); + + const openButton = container.querySelector( + '[data-testid="image-workbench-message-preview-image-preview-failed"]', + ) as HTMLButtonElement | null; + const retryButton = container.querySelector( + '[data-testid="image-workbench-message-preview-action-image-preview-failed-retry"]', + ) as HTMLButtonElement | null; + + expect(openButton?.getAttribute("aria-label")).toBe("Open image result"); + expect(retryButton?.textContent).toContain("Retry"); + + act(() => { + retryButton?.click(); + }); + + expect(actionDetail).toEqual({ + action: "retry", + taskId: "image-preview-failed", + projectId: "project-1", + contentId: "content-1", + }); + + window.removeEventListener( + IMAGE_WORKBENCH_TASK_ACTION_EVENT, + handleAction, + ); + }); }); diff --git a/src/components/agent/chat/components/ImageWorkbenchMessagePreview.tsx b/src/components/agent/chat/components/ImageWorkbenchMessagePreview.tsx index a946953f3..2249ecf44 100644 --- a/src/components/agent/chat/components/ImageWorkbenchMessagePreview.tsx +++ b/src/components/agent/chat/components/ImageWorkbenchMessagePreview.tsx @@ -1,8 +1,11 @@ import React from "react"; import type { TFunction } from "i18next"; import { useTranslation } from "react-i18next"; -import { Leaf, LoaderCircle } from "lucide-react"; -import { emitImageWorkbenchFocus } from "@/lib/imageWorkbenchEvents"; +import { Leaf, LoaderCircle, RotateCcw } from "lucide-react"; +import { + emitImageWorkbenchFocus, + emitImageWorkbenchTaskAction, +} from "@/lib/imageWorkbenchEvents"; import { cn } from "@/lib/utils"; import type { MessageImageWorkbenchPreview } from "../types"; import { resolveImageWorkbenchPreviewModelLabel } from "../utils/imageWorkbenchPresentation"; @@ -206,48 +209,76 @@ export const ImageWorkbenchMessagePreview: React.FC< const toolLabel = resolveToolLabel(preview, t); const modelLabel = resolveImageWorkbenchPreviewModelLabel(preview); const caption = preview.caption?.trim(); + const showRetryAction = preview.status === "failed"; + + const openPreview = () => { + if (onOpen) { + onOpen(preview); + return; + } + emitImageWorkbenchFocus({ + projectId: preview.projectId ?? null, + contentId: preview.contentId ?? null, + }); + }; + + const handleRetry = (event: React.MouseEvent) => { + event.stopPropagation(); + emitImageWorkbenchTaskAction({ + action: "retry", + taskId: preview.taskId, + projectId: preview.projectId ?? null, + contentId: preview.contentId ?? null, + }); + }; return ( -
-
- {renderPreviewMedia(preview, t)} -
- {caption ? (
- {caption} + + {toolLabel} + {modelLabel ? ( + <> + | + {modelLabel} + + ) : null} +
+
+ {renderPreviewMedia(preview, t)} +
+ {caption ? ( +
+ {caption} +
+ ) : null} + + {showRetryAction ? ( +
+
) : null} - +
); }; diff --git a/src/components/agent/chat/components/Inputbar/components/InputbarComposerSection.tsx b/src/components/agent/chat/components/Inputbar/components/InputbarComposerSection.tsx index 694677f49..bb71b0d46 100644 --- a/src/components/agent/chat/components/Inputbar/components/InputbarComposerSection.tsx +++ b/src/components/agent/chat/components/Inputbar/components/InputbarComposerSection.tsx @@ -34,6 +34,8 @@ import type { InputbarKnowledgePackSelection, } from "../types"; import { InputbarKnowledgeControl } from "../knowledge/InputbarKnowledgeControl"; +import type { InputbarComposerSectionCopy } from "./inputbarComposerSectionCopy"; +import type { InputbarCoreCopy } from "./inputbarCoreCopy"; import type { WorkflowGateState, WorkflowQuickAction, @@ -112,6 +114,8 @@ interface InputbarComposerSectionProps { projectId?: string | null; sessionId?: string | null; inputCompletionEnabled?: boolean; + copy: InputbarComposerSectionCopy; + inputbarCopy: InputbarCoreCopy; } export const InputbarComposerSection: React.FC< @@ -177,6 +181,8 @@ export const InputbarComposerSection: React.FC< projectId = null, sessionId = null, inputCompletionEnabled = true, + copy, + inputbarCopy, }) => { const [teamSelectorAutoOpenToken, setTeamSelectorAutoOpenToken] = useState< number | null @@ -262,6 +268,18 @@ export const InputbarComposerSection: React.FC< Boolean(onToggleFileManager); const shouldShowLeftExtra = Boolean(knowledgePackControl) || shouldShowAdvancedToggle; + const advancedSettingsLabel = showAdvancedControls + ? copy.advancedSettings.collapse + : copy.advancedSettings.expand; + const fileManagerLabel = fileManagerOpen + ? copy.fileManager.close + : copy.fileManager.open; + const workspacePlaceholder = + workflowGate?.status === "waiting" + ? copy.workspacePlaceholder.waiting + : contextVariant === "task-center" + ? copy.workspacePlaceholder.taskCenter + : copy.workspacePlaceholder.default; const leftExtra = shouldShowLeftExtra ? ( <> {knowledgePackControl} @@ -270,9 +288,11 @@ export const InputbarComposerSection: React.FC< - 模型 + + {copy.currentModel.label} + {trimmedModel} ) : null} @@ -296,10 +316,10 @@ export const InputbarComposerSection: React.FC< setShowAdvancedControls((previous) => !previous)} > - 高级设置 + {copy.advancedSettings.label} {showAdvancedControls ? ( ) : ( @@ -322,10 +342,8 @@ export const InputbarComposerSection: React.FC< @@ -434,6 +452,7 @@ export const InputbarComposerSection: React.FC< inputCompletionEnabled={inputCompletionEnabled} /> ); diff --git a/src/components/agent/chat/components/Inputbar/components/InputbarCore.test.tsx b/src/components/agent/chat/components/Inputbar/components/InputbarCore.test.tsx index c2e0da0c1..9a746ff7f 100644 --- a/src/components/agent/chat/components/Inputbar/components/InputbarCore.test.tsx +++ b/src/components/agent/chat/components/Inputbar/components/InputbarCore.test.tsx @@ -3,7 +3,14 @@ import { act } from "react"; import { createRoot, type Root } from "react-dom/client"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { OPEN_VOICE_MODEL_SETTINGS_EVENT } from "@/lib/voiceModelSettingsNavigation"; +import { changeLimeLocale } from "@/i18n/createI18n"; +import agentResource from "@/i18n/resources/zh-CN/agent.json"; +import enAgentResource from "@/i18n/resources/en-US/agent.json"; import { InputbarCore } from "./InputbarCore"; +import { + buildInputbarCoreCopy, + type InputbarCoreCopyKey, +} from "./inputbarCoreCopy"; const { mockGetVoiceInputConfig, @@ -15,7 +22,10 @@ const { mockGetRecordingSegment, mockGetRecordingStatus, mockGetDefaultLocalVoiceModelReadiness, + mockOnVoiceStartRecording, + mockOnVoiceStopRecording, mockToastError, + mockToastInfo, } = vi.hoisted(() => ({ mockGetVoiceInputConfig: vi.fn(async () => ({ enabled: true, @@ -63,7 +73,10 @@ const { mockGetDefaultLocalVoiceModelReadiness: vi.fn(async () => ({ ready: true, })), + mockOnVoiceStartRecording: vi.fn(), + mockOnVoiceStopRecording: vi.fn(), mockToastError: vi.fn(), + mockToastInfo: vi.fn(), })); vi.mock("./InputbarTools", () => ({ @@ -85,10 +98,15 @@ vi.mock("@/lib/api/voiceModels", () => ({ getDefaultLocalVoiceModelReadiness: mockGetDefaultLocalVoiceModelReadiness, })); +vi.mock("@/lib/api/voiceShortcutEvents", () => ({ + onVoiceStartRecording: mockOnVoiceStartRecording, + onVoiceStopRecording: mockOnVoiceStopRecording, +})); + vi.mock("sonner", () => ({ toast: { error: mockToastError, - info: vi.fn(), + info: mockToastInfo, }, })); @@ -101,12 +119,35 @@ vi.mock("@/hooks/useVoiceSound", () => ({ const mountedRoots: Array<{ root: Root; container: HTMLDivElement }> = []; -beforeEach(() => { +function translateResource( + resource: Partial>, + key: InputbarCoreCopyKey, + values?: Record, +) { + return Object.entries(values ?? {}).reduce( + (text, [name, value]) => + text.split(`{{${name}}}`).join(String(value)), + resource[key] ?? key, + ); +} + +const TEST_INPUTBAR_CORE_COPY = buildInputbarCoreCopy((key, values) => + translateResource(agentResource, key, values), +); + +const TEST_EN_INPUTBAR_CORE_COPY = buildInputbarCoreCopy((key, values) => + translateResource(enAgentResource, key, values), +); + +beforeEach(async () => { ( globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean; } ).IS_REACT_ACT_ENVIRONMENT = true; + await changeLimeLocale("zh-CN"); + mockOnVoiceStartRecording.mockImplementation(async () => vi.fn()); + mockOnVoiceStopRecording.mockImplementation(async () => vi.fn()); }); afterEach(() => { @@ -123,7 +164,11 @@ afterEach(() => { }); const renderInputbarCore = async ( - props?: Partial>, + props?: Partial< + Omit, "uiCopy"> + > & { + uiCopy?: React.ComponentProps["uiCopy"]; + }, ) => { const container = document.createElement("div"); document.body.appendChild(container); @@ -132,6 +177,7 @@ const renderInputbarCore = async ( act(() => { root.render( { ).toBeTruthy(); }); + it("应支持使用 en-US copy 渲染输入区 chrome 文案", async () => { + const container = await renderInputbarCore({ + uiCopy: TEST_EN_INPUTBAR_CORE_COPY, + visualVariant: "default", + toolMode: "default", + isLoading: true, + queuedTurns: [ + { + queued_turn_id: "queued-en-1", + message_preview: "Follow-up brief", + message_text: "Queue body", + created_at: 1700000000000, + image_count: 0, + position: 1, + }, + ], + }); + + expect( + container.querySelector('button[aria-label="Add image"]'), + ).toBeTruthy(); + expect( + container.querySelector('button[aria-label="Expand input"]'), + ).toBeTruthy(); + expect( + container.querySelector('button[aria-label="Send"]'), + ).toBeNull(); + expect(container.textContent).toContain("Handle later"); + expect( + container.querySelector('button[aria-label="Stop"]'), + ).toBeTruthy(); + }); + it("添加路径引用时应显示 chip 并允许移除", async () => { const onRemovePathReference = vi.fn(); const container = await renderInputbarCore({ @@ -545,6 +624,7 @@ describe("InputbarCore", () => { expect(mockGetDefaultLocalVoiceModelReadiness).toHaveBeenCalledTimes(1); expect(mockStartRecording).not.toHaveBeenCalled(); + expect(mockToastInfo).toHaveBeenCalledWith("先下载语音模型"); expect(container.querySelector('[aria-live="polite"]')).toBeNull(); expect(navigationRequests).toEqual([ expect.objectContaining({ @@ -555,6 +635,100 @@ describe("InputbarCore", () => { ]); }); + it("语音输入未启用反馈应跟随 en-US 资源", async () => { + await changeLimeLocale("en-US"); + mockGetVoiceInputConfig.mockResolvedValueOnce({ + enabled: false, + shortcut: "Alt+Space", + processor: { + polish_enabled: true, + default_instruction_id: "default", + }, + output: { + mode: "type", + type_delay_ms: 0, + }, + instructions: [], + sound_enabled: false, + translate_instruction_id: "", + }); + const container = await renderInputbarCore({ + uiCopy: TEST_EN_INPUTBAR_CORE_COPY, + visualVariant: "default", + toolMode: "default", + }); + + const micButton = container.querySelector( + 'button[aria-label="Start voice input"]', + ) as HTMLButtonElement | null; + expect(micButton).toBeTruthy(); + + await act(async () => { + micButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + await Promise.resolve(); + await Promise.resolve(); + }); + + expect(mockToastInfo).toHaveBeenCalledWith( + "Enable voice input in Settings first", + ); + expect(mockGetDefaultLocalVoiceModelReadiness).not.toHaveBeenCalled(); + expect(mockStartRecording).not.toHaveBeenCalled(); + }); + + it("启用快捷键监听后应复用输入框录音状态机完成开始和停止", async () => { + let startHandler: (() => void | Promise) | null = null; + let stopHandler: (() => void | Promise) | null = null; + const setText = vi.fn(); + + mockOnVoiceStartRecording.mockImplementationOnce(async (handler) => { + startHandler = handler; + return vi.fn(); + }); + mockOnVoiceStopRecording.mockImplementationOnce(async (handler) => { + stopHandler = handler; + return vi.fn(); + }); + + await renderInputbarCore({ + setText, + listenForVoiceShortcut: true, + visualVariant: "default", + toolMode: "default", + }); + + await act(async () => { + await startHandler?.(); + await Promise.resolve(); + await Promise.resolve(); + }); + + expect(mockStartRecording).toHaveBeenCalledTimes(1); + + await act(async () => { + await stopHandler?.(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + }); + + expect(mockStopRecording).toHaveBeenCalledTimes(1); + expect(mockTranscribeAudio).toHaveBeenCalled(); + expect(mockPolishVoiceText).toHaveBeenCalledWith("原始识别文本"); + expect(setText).toHaveBeenLastCalledWith("润色后的文本"); + }); + + it("未启用快捷键监听时不应注册语音快捷键事件", async () => { + await renderInputbarCore({ + listenForVoiceShortcut: false, + visualVariant: "default", + toolMode: "default", + }); + + expect(mockOnVoiceStartRecording).not.toHaveBeenCalled(); + expect(mockOnVoiceStopRecording).not.toHaveBeenCalled(); + }); + it("生成中应显示稍后处理与停止按钮,并渲染待处理列表", async () => { const onSend = vi.fn(); const onStop = vi.fn(); diff --git a/src/components/agent/chat/components/Inputbar/components/InputbarCore.tsx b/src/components/agent/chat/components/Inputbar/components/InputbarCore.tsx index 111b560ca..ab60c783e 100644 --- a/src/components/agent/chat/components/Inputbar/components/InputbarCore.tsx +++ b/src/components/agent/chat/components/Inputbar/components/InputbarCore.tsx @@ -50,6 +50,7 @@ import type { MessageImage, MessagePathReference } from "../../../types"; import type { QueuedTurnSnapshot } from "@/lib/api/agentRuntime"; import { QueuedTurnsPanel } from "./QueuedTurnsPanel"; import { useInputbarDictation } from "../hooks/useInputbarDictation"; +import type { InputbarCoreCopy } from "./inputbarCoreCopy"; const INTERACTIVE_TARGET_SELECTOR = "button, a, input, textarea, select, option, [role='button'], [contenteditable=''], [contenteditable='true'], [contenteditable='plaintext-only']"; @@ -63,15 +64,16 @@ function formatDictationDuration(duration = 0): string { function buildDictationStatusText( state: "idle" | "listening" | "transcribing" | "polishing", + copy: InputbarCoreCopy, duration = 0, ): string { switch (state) { case "listening": - return `录音中 ${formatDictationDuration(duration)}`; + return copy.dictation.recording(formatDictationDuration(duration)); case "transcribing": - return "识别中"; + return copy.dictation.transcribing; case "polishing": - return "润色中"; + return copy.dictation.polishing; case "idle": default: return ""; @@ -86,6 +88,7 @@ function shouldFocusComposerTextarea(target: EventTarget | null): boolean { } interface InputbarCoreProps { + uiCopy: InputbarCoreCopy; text: string; setText: (text: string) => void; onSend: () => void; @@ -135,9 +138,11 @@ interface InputbarCoreProps { prompt: string; testId?: string; }) => void; + listenForVoiceShortcut?: boolean; } export const InputbarCore: React.FC = ({ + uiCopy, text, setText, onSend, @@ -170,6 +175,7 @@ export const InputbarCore: React.FC = ({ showMetaTools = true, inputSuggestion = null, onAcceptInputSuggestion, + listenForVoiceShortcut = false, }) => { const [isTextareaExpanded, setIsTextareaExpanded] = useState(false); const inputBarContainerRef = useRef(null); @@ -191,6 +197,7 @@ export const InputbarCore: React.FC = ({ setText, textareaRef: resolvedTextareaRef, disabled, + listenForVoiceShortcut, }); const hasInlineComposerContent = text.trim().length > 0 || @@ -247,21 +254,24 @@ export const InputbarCore: React.FC = ({ Boolean(inputSuggestion) && text.trim().length === 0 && !disabled; const dictationStatusText = buildDictationStatusText( dictationState, + uiCopy, recordingStatus?.duration, ); const dictationStatusLabel = dictationState === "listening" && liveTranscript - ? `${dictationStatusText} · 实时识别` + ? `${dictationStatusText} · ${uiCopy.dictation.liveTranscript}` : dictationStatusText; const dictationButtonTitle = isDictationProcessing ? dictationState === "polishing" - ? "语音润色中" - : "语音识别中" + ? uiCopy.dictation.polishingTitle + : uiCopy.dictation.transcribingTitle : isDictating - ? `${dictationStatusLabel || "录音中"},点击停止` + ? uiCopy.dictation.stopRecording( + dictationStatusLabel || uiCopy.dictation.recordingLabel, + ) : dictationEnabled || !voiceConfigLoaded - ? "开始语音输入" - : "语音输入未启用"; + ? uiCopy.dictation.start + : uiCopy.dictation.disabled; const handleInputSuggestionKeyDown = useCallback( (event: React.KeyboardEvent) => { @@ -394,8 +404,8 @@ export const InputbarCore: React.FC = ({ ? "" : placeholder || (isFullscreen - ? "全屏编辑模式,按 ESC 退出,Enter 发送" - : "在这里输入消息, 按 Enter 发送") + ? uiCopy.placeholder.fullscreen + : uiCopy.placeholder.default) } > {({ textareaProps, textareaRef, isPrimaryDisabled, onPrimaryAction }) => { @@ -432,11 +442,11 @@ export const InputbarCore: React.FC = ({ handleRemoveImageClick(event, index) @@ -450,7 +460,7 @@ export const InputbarCore: React.FC = ({ )} {pathReferences.length > 0 ? ( - + {pathReferences.map((reference) => { const ReferenceIcon = reference.isDir ? Folder : FileText; return ( @@ -467,26 +477,30 @@ export const InputbarCore: React.FC = ({ {reference.name} - {reference.isDir ? "本地文件夹" : "本地文件"} + {reference.isDir + ? uiCopy.path.localFolder + : uiCopy.path.localFile} {onImportPathReferenceAsKnowledge && isKnowledgeTextSourceCandidate(reference) ? ( handleImportPathReferenceClick(event, reference) } > - 设为资料 + {uiCopy.path.importAction} ) : null} handleRemovePathReferenceClick(event, reference.id) @@ -511,8 +525,8 @@ export const InputbarCore: React.FC = ({ onToolClick("attach")} - aria-label="添加图片" - title="添加图片" + aria-label={uiCopy.image.add} + title={uiCopy.image.add} > @@ -521,7 +535,7 @@ export const InputbarCore: React.FC = ({ {inputSuggestion.label} @@ -542,9 +556,15 @@ export const InputbarCore: React.FC = ({ disabled={disabled} className={isTextareaExpanded ? "is-active" : ""} aria-label={ - isTextareaExpanded ? "收起输入框" : "展开输入框" + isTextareaExpanded + ? uiCopy.textarea.collapse + : uiCopy.textarea.expand + } + title={ + isTextareaExpanded + ? uiCopy.textarea.collapse + : uiCopy.textarea.expand } - title={isTextareaExpanded ? "收起输入框" : "展开输入框"} > {isTextareaExpanded ? ( @@ -585,7 +605,7 @@ export const InputbarCore: React.FC = ({ onClick={onPrimaryAction} disabled={isPrimaryDisabled} > - 稍后处理 + {uiCopy.action.defer} ) : null} {isLoading ? ( @@ -594,8 +614,8 @@ export const InputbarCore: React.FC = ({ onClick={onStop} disabled={!onStop} $destructive - aria-label="停止" - title="停止" + aria-label={uiCopy.action.stop} + title={uiCopy.action.stop} > @@ -605,8 +625,8 @@ export const InputbarCore: React.FC = ({ type="button" onClick={onPrimaryAction} disabled={isPrimaryDisabled} - aria-label="发送" - title="发送" + aria-label={uiCopy.action.send} + title={uiCopy.action.send} > diff --git a/src/components/agent/chat/components/Inputbar/components/InputbarTools.test.tsx b/src/components/agent/chat/components/Inputbar/components/InputbarTools.test.tsx new file mode 100644 index 000000000..a547b72f1 --- /dev/null +++ b/src/components/agent/chat/components/Inputbar/components/InputbarTools.test.tsx @@ -0,0 +1,105 @@ +import React from "react"; +import { act } from "react"; +import { createRoot, type Root } from "react-dom/client"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { changeLimeLocale } from "@/i18n/createI18n"; +import { InputbarTools } from "./InputbarTools"; + +const mountedRoots: Array<{ root: Root; container: HTMLDivElement }> = []; + +beforeEach(async () => { + ( + globalThis as typeof globalThis & { + IS_REACT_ACT_ENVIRONMENT?: boolean; + } + ).IS_REACT_ACT_ENVIRONMENT = true; + await changeLimeLocale("zh-CN"); +}); + +afterEach(() => { + while (mountedRoots.length > 0) { + const mounted = mountedRoots.pop(); + if (!mounted) break; + act(() => { + mounted.root.unmount(); + }); + mounted.container.remove(); + } + vi.clearAllMocks(); +}); + +function renderInputbarTools( + props?: Partial>, +) { + const container = document.createElement("div"); + document.body.appendChild(container); + const root = createRoot(container); + const defaultProps: React.ComponentProps = { + onToolClick: vi.fn(), + activeTools: {}, + }; + + act(() => { + root.render(); + }); + + mountedRoots.push({ root, container }); + return { + container, + onToolClick: props?.onToolClick ?? defaultProps.onToolClick, + }; +} + +describe("InputbarTools", () => { + it("工具开关 chrome 文案应跟随 en-US 资源", async () => { + await changeLimeLocale("en-US"); + const onToolClick = vi.fn(); + const { container } = renderInputbarTools({ + onToolClick, + activeTheme: "general", + activeTools: { + thinking: false, + web_search: true, + subagent_mode: false, + }, + }); + + expect(container.textContent).toContain("Thinking"); + expect(container.textContent).toContain("Search"); + expect(container.textContent).toContain("Task split"); + expect(container.textContent).not.toContain("思考"); + expect(container.textContent).not.toContain("任务拆分"); + + const buttons = Array.from(container.querySelectorAll("button")); + expect(buttons.map((button) => button.getAttribute("title"))).toEqual([ + "Deep thinking is off", + "Web search is on", + "Task splitting preference is off", + ]); + + act(() => { + buttons[0]?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + buttons[1]?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + buttons[2]?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + + expect(onToolClick).toHaveBeenNthCalledWith(1, "thinking"); + expect(onToolClick).toHaveBeenNthCalledWith(2, "web_search"); + expect(onToolClick).toHaveBeenNthCalledWith(3, "subagent_mode"); + }); + + it("attach-only 模式不渲染工具开关", () => { + const { container } = renderInputbarTools({ + toolMode: "attach-only", + activeTheme: "general", + activeTools: { + thinking: true, + web_search: true, + subagent_mode: true, + }, + }); + + expect(container.querySelectorAll("button")).toHaveLength(0); + expect(container.textContent).toBe(""); + }); +}); diff --git a/src/components/agent/chat/components/Inputbar/components/InputbarTools.tsx b/src/components/agent/chat/components/Inputbar/components/InputbarTools.tsx index 450de9930..55439d720 100644 --- a/src/components/agent/chat/components/Inputbar/components/InputbarTools.tsx +++ b/src/components/agent/chat/components/Inputbar/components/InputbarTools.tsx @@ -1,7 +1,9 @@ import React from "react"; +import { useTranslation } from "react-i18next"; import { Lightbulb, Globe, Workflow } from "lucide-react"; import { ToolButton } from "../styles"; import { isGeneralResearchTheme } from "../../../utils/generalAgentPrompt"; +import { buildInputbarToolsCopy } from "./inputbarToolsCopy"; interface InputbarToolsProps { onToolClick: (tool: string) => void; @@ -16,7 +18,15 @@ export const InputbarTools: React.FC = ({ toolMode = "default", activeTheme, }) => { + const { t } = useTranslation("agent"); + const copy = React.useMemo( + () => buildInputbarToolsCopy((key) => t(key, {})), + [t], + ); const isGeneralTheme = isGeneralResearchTheme(activeTheme); + const thinkingEnabled = Boolean(activeTools["thinking"]); + const webSearchEnabled = Boolean(activeTools["web_search"]); + const subagentEnabled = Boolean(activeTools["subagent_mode"]); return (
@@ -25,24 +35,24 @@ export const InputbarTools: React.FC = ({ onToolClick("thinking")} - className={activeTools["thinking"] ? "active" : ""} - aria-pressed={activeTools["thinking"]} - title={`深度思考${activeTools["thinking"] ? "已开启" : "已关闭"}`} + className={thinkingEnabled ? "active" : ""} + aria-pressed={thinkingEnabled} + title={copy.thinking.title(thinkingEnabled)} > - 思考 + {copy.thinking.label} onToolClick("web_search")} - className={activeTools["web_search"] ? "active" : ""} - aria-pressed={activeTools["web_search"]} - title={`联网搜索${activeTools["web_search"] ? "已开启" : "已关闭"}`} + className={webSearchEnabled ? "active" : ""} + aria-pressed={webSearchEnabled} + title={copy.webSearch.title(webSearchEnabled)} data-testid="toggle-web-search" > - 搜索 + {copy.webSearch.label} {isGeneralTheme ? ( @@ -50,13 +60,13 @@ export const InputbarTools: React.FC = ({ onToolClick("subagent_mode")} - className={activeTools["subagent_mode"] ? "active" : ""} - aria-pressed={activeTools["subagent_mode"]} - title={`任务拆分偏好${activeTools["subagent_mode"] ? "已开启" : "已关闭"}`} + className={subagentEnabled ? "active" : ""} + aria-pressed={subagentEnabled} + title={copy.subagent.title(subagentEnabled)} data-testid="toggle-subagent-mode" > - 任务拆分 + {copy.subagent.label} ) : null} diff --git a/src/components/agent/chat/components/Inputbar/components/QueuedTurnsPanel.test.tsx b/src/components/agent/chat/components/Inputbar/components/QueuedTurnsPanel.test.tsx index 6201f570c..edcd2dc5b 100644 --- a/src/components/agent/chat/components/Inputbar/components/QueuedTurnsPanel.test.tsx +++ b/src/components/agent/chat/components/Inputbar/components/QueuedTurnsPanel.test.tsx @@ -2,16 +2,18 @@ import React from "react"; import { act } from "react"; import { createRoot, type Root } from "react-dom/client"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { changeLimeLocale } from "@/i18n/createI18n"; import { QueuedTurnsPanel } from "./QueuedTurnsPanel"; const mountedRoots: Array<{ root: Root; container: HTMLDivElement }> = []; -beforeEach(() => { +beforeEach(async () => { ( globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean; } ).IS_REACT_ACT_ENVIRONMENT = true; + await changeLimeLocale("zh-CN"); }); afterEach(() => { @@ -56,6 +58,36 @@ function renderQueuedTurnsPanel( } describe("QueuedTurnsPanel", () => { + it("排队消息 chrome 文案应跟随 en-US 资源", async () => { + await changeLimeLocale("en-US"); + const container = renderQueuedTurnsPanel({ + queuedTurns: [ + { + queued_turn_id: "queued-en-1", + message_preview: "", + message_text: "", + created_at: 1_700_000_000_000, + image_count: 2, + position: 1, + }, + ], + }); + + expect(container.textContent).toContain("Handle later 1"); + expect(container.textContent).toContain("Starts in order"); + expect(container.textContent).toContain("Blank input"); + expect(container.textContent).toContain("2 images attached"); + expect(container.textContent).toContain("View"); + expect( + container.querySelector('button[aria-label="Run queued message now"]'), + ).toBeTruthy(); + expect( + container.querySelector('button[aria-label="Remove queued message"]'), + ).toBeTruthy(); + expect(container.textContent).not.toContain("稍后处理"); + expect(container.textContent).not.toContain("附图"); + }); + it("应展示立即执行按钮,并触发 promote 回调", async () => { const onPromoteQueuedTurn = vi.fn().mockResolvedValue(true); const container = renderQueuedTurnsPanel({ onPromoteQueuedTurn }); diff --git a/src/components/agent/chat/components/Inputbar/components/QueuedTurnsPanel.tsx b/src/components/agent/chat/components/Inputbar/components/QueuedTurnsPanel.tsx index 21c5a83f7..cb9dca597 100644 --- a/src/components/agent/chat/components/Inputbar/components/QueuedTurnsPanel.tsx +++ b/src/components/agent/chat/components/Inputbar/components/QueuedTurnsPanel.tsx @@ -1,6 +1,8 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; import { Loader2, Play, X } from "lucide-react"; import type { QueuedTurnSnapshot } from "@/lib/api/agentRuntime"; +import { buildInputbarQueuedTurnsCopy } from "./inputbarQueuedTurnsCopy"; interface QueuedTurnsPanelProps { queuedTurns: QueuedTurnSnapshot[]; @@ -13,6 +15,14 @@ export const QueuedTurnsPanel: React.FC = ({ onPromoteQueuedTurn, onRemoveQueuedTurn, }) => { + const { t } = useTranslation("agent"); + const copy = useMemo( + () => + buildInputbarQueuedTurnsCopy((key, values) => + t(key, values ?? {}), + ), + [t], + ); const [expandedTurnId, setExpandedTurnId] = useState(null); const [pendingAction, setPendingAction] = useState<{ queuedTurnId: string; @@ -57,14 +67,14 @@ export const QueuedTurnsPanel: React.FC = ({ return (
- 稍后处理 {queuedTurns.length} - 会依次开始 + {copy.queuedCount(queuedTurns.length)} + {copy.sequenceHint}
{queuedTurns.map((item) => { const messageText = item.message_text.trim() ? item.message_text - : item.message_preview || "空白输入"; + : item.message_preview || copy.emptyInput; const title = item.message_preview.trim() ? item.message_preview : messageText; @@ -104,13 +114,13 @@ export const QueuedTurnsPanel: React.FC = ({ {title}
- {isExpanded ? "收起" : "查看"} + {isExpanded ? copy.collapse : copy.expand}
{item.image_count > 0 - ? `附图 ${item.image_count} 张` - : "纯文本请求"} + ? copy.imageCount(item.image_count) + : copy.textOnly}
{isExpanded ? (
= ({ void runQueuedAction(item.queued_turn_id, "promote") } disabled={isBusy} - aria-label="插队立即执行" + aria-label={copy.promoteAria} > {isPromoting ? ( ) : ( )} - {isPromoting ? "切换中" : "立即执行"} + {isPromoting ? copy.promoting : copy.promote}
@@ -407,9 +414,22 @@ const HistoricalMarkdownHydrationPreview: React.FC<{ content: string }> = ({
); +const ImageWorkbenchAssistantIntro: React.FC<{ content: string }> = ({ + content, +}) => ( +
+ {content} +
+); + function summarizeHistoricalTimelineItems(items: AgentThreadItem[]): { stepsCount: number; - metaText: string; + toolStepsCount: number; + thinkingStepsCount: number; + artifactStepsCount: number; } { const visibleItems = items.filter((item) => { if (item.type === "user_message" || item.type === "agent_message") { @@ -437,15 +457,11 @@ function summarizeHistoricalTimelineItems(items: AgentThreadItem[]): { const artifactStepsCount = visibleItems.filter( (item) => item.type === "file_artifact", ).length; - const metaParts = [ - toolStepsCount > 0 ? `${toolStepsCount} 个工具步骤` : null, - thinkingStepsCount > 0 ? `${thinkingStepsCount} 条思路` : null, - artifactStepsCount > 0 ? `${artifactStepsCount} 份产物` : null, - ].filter((part): part is string => Boolean(part)); - return { stepsCount: visibleItems.length, - metaText: metaParts.length > 0 ? metaParts.join(",") : "执行细节已折叠", + toolStepsCount, + thinkingStepsCount, + artifactStepsCount, }; } @@ -455,6 +471,7 @@ const HistoricalTimelinePreview: React.FC<{ detailsDeferred?: boolean; onExpand: () => void; }> = ({ items, placement, detailsDeferred = false, onExpand }) => { + const { t } = useTranslation("agent"); const summary = useMemo( () => summarizeHistoricalTimelineItems(items), [items], @@ -463,10 +480,34 @@ const HistoricalTimelinePreview: React.FC<{ if (summary.stepsCount <= 0 && !detailsDeferred) { return null; } + const metaParts = [ + summary.toolStepsCount > 0 + ? t("agentChat.messageList.historicalTimeline.toolSteps", { + countLabel: formatContentLength(summary.toolStepsCount), + }) + : null, + summary.thinkingStepsCount > 0 + ? t("agentChat.messageList.historicalTimeline.thinkingSteps", { + countLabel: formatContentLength(summary.thinkingStepsCount), + }) + : null, + summary.artifactStepsCount > 0 + ? t("agentChat.messageList.historicalTimeline.artifactSteps", { + countLabel: formatContentLength(summary.artifactStepsCount), + }) + : null, + ].filter((part): part is string => Boolean(part)); + const summaryMetaText = + metaParts.length > 0 + ? metaParts.join(t("agentChat.messageList.historicalTimeline.separator")) + : t("agentChat.messageList.historicalTimeline.foldedMeta"); const metaText = summary.stepsCount > 0 - ? `${summary.stepsCount} 步 · ${summary.metaText}` - : "点击展开后加载执行细节"; + ? t("agentChat.messageList.historicalTimeline.meta", { + stepCountLabel: formatContentLength(summary.stepsCount), + meta: summaryMetaText, + }) + : t("agentChat.messageList.historicalTimeline.deferredMeta"); return ( ); @@ -577,22 +620,12 @@ const AssistantFirstTokenRuntimeStatus: React.FC<{ }> = ({ status }) => { const { t } = useTranslation("agent"); const phase = status?.phase || "submitted"; - const submittedTitle = t( - "agentChat.messageList.firstTokenStatus.submitted.title", - ); - const submittedDetail = t( - "agentChat.messageList.firstTokenStatus.submitted.detail", - ); const title = truncateRuntimeStatusText( - t(`agentChat.messageList.firstTokenStatus.${phase}.title`, { - defaultValue: submittedTitle, - }), + t(`agentChat.messageList.firstTokenStatus.${phase}.title`), 48, ); const detail = truncateRuntimeStatusText( - t(`agentChat.messageList.firstTokenStatus.${phase}.detail`, { - defaultValue: submittedDetail, - }), + t(`agentChat.messageList.firstTokenStatus.${phase}.detail`), 120, ); @@ -641,90 +674,12 @@ const AssistantStreamingInlineIndicator: React.FC<{ aria-hidden /> - {t(`agentChat.messageList.streamingInline.${status}`, { - defaultValue: isQueued ? "等待输出" : "正在输出", - })} + {t(`agentChat.messageList.streamingInline.${status}`)} ); }; -const ImageWorkbenchAssistantIntro: React.FC<{ content: string }> = ({ - content, -}) => { - const lines = content - .split(/\r?\n/u) - .map((line) => line.trim()) - .filter(Boolean); - - if (lines.length === 0) { - return null; - } - - return ( -
- {lines.map((line, index) => ( -

- {line} -

- ))} -
- ); -}; - -const ImageWorkbenchAssistantHeader: React.FC<{ label: string }> = ({ - label, -}) => ( -
- - - - - - - - - - - {label} - -
-); - const USER_COMMAND_TAG_CANDIDATES = INPUTBAR_BUILTIN_COMMANDS.map((command) => command.commandPrefix.trim(), ) @@ -749,11 +704,16 @@ function parseLeadingUserCommandTag( route?: Message["inputCapabilityRoute"], ): { tag: string; body: string } | null { const trimmed = content.trimStart(); + const routePrefix = resolveUserCommandRoutePrefix(route); if (!trimmed.startsWith("@")) { - return null; + return routePrefix + ? { + tag: routePrefix, + body: content.trim(), + } + : null; } - const routePrefix = resolveUserCommandRoutePrefix(route); const candidates = routePrefix ? [ routePrefix, @@ -789,11 +749,14 @@ const UserCommandMessageContent: React.FC<{ if (!command) { return null; } + const ariaLabel = command.body + ? `${command.tag} ${command.body}`.trim() + : command.tag; return (
[number], + { type: "thinking"; text: string } + > => part.type === "thinking" && part.text.trim().length > 0, + ) + .map((part) => part.text) + .join(""); + + return thinkingText.trim() ? thinkingText : undefined; +} + function hasNonTextInlineProcessPart(message: Message): boolean { return Boolean( message.contentParts?.some( @@ -1278,6 +1264,7 @@ const MessageListInner: React.FC = ({ activePendingA2UISource = null, providerType, }) => { + const { t } = useTranslation("agent"); const scrollRef = useRef(null); const containerRef = useRef(null); const previousVisibleMessageCountRef = useRef(null); @@ -2029,6 +2016,13 @@ const MessageListInner: React.FC = ({ return true; } + if ( + isRetainedSkillProcessMessage(message) && + hasInlineThinkingContent(message) + ) { + return true; + } + if (!isConversationTailAssistant) { return false; } @@ -2041,6 +2035,14 @@ const MessageListInner: React.FC = ({ return true; } + if ( + !hasTurnContext && + message.runtimeTurnId?.trim() && + hasInlineThinkingContent(message) + ) { + return true; + } + const hasRunningToolCall = (message.toolCalls || []).some( (toolCall) => toolCall.status === "running", @@ -2166,10 +2168,10 @@ const MessageListInner: React.FC = ({ try { await navigator.clipboard.writeText(content); setCopiedId(id); - toast.success("已复制到剪贴板"); + toast.success(t("agentChat.messageList.toast.copySuccess")); setTimeout(() => setCopiedId(null), 2000); } catch { - toast.error("复制失败"); + toast.error(t("agentChat.messageList.toast.copyFailed")); } }; @@ -2211,9 +2213,14 @@ const MessageListInner: React.FC = ({ msg.imageWorkbenchPreview || shouldSuppressStandaloneImageWorkbenchProcess, ); + const shouldSuppressImageWorkbenchAssistantText = + msg.role === "assistant" && + Boolean(msg.imageWorkbenchPreview) && + shouldSuppressImageWorkbenchStatusText(rawDisplayContent); const visibleRawDisplayContent = msg.role === "assistant" && - shouldSuppressImageWorkbenchStatusText(rawDisplayContent) + (shouldSuppressImageWorkbenchAssistantText || + shouldSuppressStandaloneImageWorkbenchProcess) ? "" : rawDisplayContent; const displayContent = sanitizeMessageTextForDisplay( @@ -2271,6 +2278,10 @@ const MessageListInner: React.FC = ({ msg.role === "assistant" && includeInlineProcessFlow ? msg.thinkingContent : undefined; + const imageWorkbenchThinkingContent = + msg.role === "assistant" && msg.imageWorkbenchPreview + ? resolveInlineThinkingContent(msg) + : undefined; const conversationToolCalls = msg.role === "assistant" && includeInlineProcessFlow ? msg.toolCalls @@ -2785,9 +2796,6 @@ const MessageListInner: React.FC = ({ > {msg.role === "assistant" ? ( <> - {msg.imageWorkbenchPreview ? ( - - ) : null} {shouldRenderPrimaryTimelineOutsideBubble ? null : primaryTimelineNode} @@ -2812,8 +2820,27 @@ const MessageListInner: React.FC = ({ - ) : msg.imageWorkbenchPreview && hasVisibleAssistantText ? ( - + ) : msg.imageWorkbenchPreview ? ( + <> + {imageWorkbenchThinkingContent ? ( + + ) : null} + {hasVisibleAssistantText ? ( + + ) : null} + ) : ( = ({ size="icon" className="h-7 w-7 rounded-full border border-slate-200/90 bg-white/92 text-slate-400 shadow-sm shadow-slate-950/5 hover:bg-slate-50 hover:text-slate-700" onClick={() => onQuoteMessage?.(actionContent, msg.id)} - aria-label="引用消息" - title="引用消息" + aria-label={t("agentChat.messageList.actions.quote")} + title={t("agentChat.messageList.actions.quote")} > @@ -3040,8 +3067,8 @@ const MessageListInner: React.FC = ({ size="icon" className="h-7 w-7 rounded-full border border-slate-200/90 bg-white/92 text-slate-400 shadow-sm shadow-slate-950/5 hover:bg-slate-50 hover:text-slate-700" onClick={() => handleCopy(actionContent, msg.id)} - aria-label="复制消息" - title="复制消息" + aria-label={t("agentChat.messageList.actions.copy")} + title={t("agentChat.messageList.actions.copy")} > {copiedId === msg.id ? ( @@ -3061,8 +3088,8 @@ const MessageListInner: React.FC = ({ content: actionContent, }) } - aria-label="保存为技能" - title="保存为技能" + aria-label={t("agentChat.messageList.actions.saveAsSkill")} + title={t("agentChat.messageList.actions.saveAsSkill")} > @@ -3078,8 +3105,12 @@ const MessageListInner: React.FC = ({ content: actionContent, }) } - aria-label="保存到灵感库" - title="保存到灵感库" + aria-label={t( + "agentChat.messageList.actions.saveToInspiration", + )} + title={t( + "agentChat.messageList.actions.saveToInspiration", + )} > @@ -3097,11 +3128,17 @@ const MessageListInner: React.FC = ({ description: knowledgeArtifactSource?.description, }) } - aria-label="保存到项目资料" - title="保存到项目资料" + aria-label={t( + "agentChat.messageList.actions.saveToKnowledge", + )} + title={t( + "agentChat.messageList.actions.saveToKnowledge", + )} > - 保存到项目资料 + + {t("agentChat.messageList.actions.saveToKnowledge")} + ) : null} @@ -3166,11 +3203,11 @@ const MessageListInner: React.FC = ({
- Document 产物 + {t("agentChat.messageList.artifact.documentBadge")} {knowledgeSource ? ( - 可保存到项目资料 + {t("agentChat.messageList.artifact.saveableBadge")} ) : null}
@@ -3190,7 +3227,7 @@ const MessageListInner: React.FC = ({ ) : artifact.status === "streaming" ? ( - 正在准备文件内容... + {t("agentChat.messageList.artifact.streaming")} ) : null}
@@ -3210,7 +3247,7 @@ const MessageListInner: React.FC = ({ } className="flex shrink-0 items-center justify-center rounded-lg border border-sky-200 bg-white px-3 py-2 text-xs font-semibold text-sky-700 transition-colors hover:border-sky-300 hover:bg-sky-100 sm:py-0" > - 保存这份文档 + {t("agentChat.messageList.artifact.saveDocument")} ) : null}
@@ -3242,10 +3279,16 @@ const MessageListInner: React.FC = ({ className="flex flex-wrap items-center justify-between gap-3 rounded-2xl border border-slate-200/80 bg-slate-50/80 px-4 py-3 text-sm text-slate-600" >
- 为了更快打开旧对话,当前先展示最近{" "} - {sessionHistoryWindow?.loadedMessages ?? renderedMessages.length}{" "} - / {sessionHistoryWindow?.totalMessages ?? renderedMessages.length}{" "} - 条消息。 + {t("agentChat.messageList.history.persistedSummary", { + loaded: formatContentLength( + sessionHistoryWindow?.loadedMessages ?? + renderedMessages.length, + ), + total: formatContentLength( + sessionHistoryWindow?.totalMessages ?? + renderedMessages.length, + ), + })} {sessionHistoryWindow?.error ? ( {sessionHistoryWindow.error} @@ -3265,8 +3308,8 @@ const MessageListInner: React.FC = ({ }} > {sessionHistoryWindow?.isLoadingFull - ? "正在加载更多历史" - : "加载更多历史"} + ? t("agentChat.messageList.history.loadingMore") + : t("agentChat.messageList.history.loadMore")}
) : null} @@ -3276,11 +3319,15 @@ const MessageListInner: React.FC = ({ className="flex flex-wrap items-center justify-between gap-3 rounded-2xl border border-slate-200/80 bg-slate-50/80 px-4 py-3 text-sm text-slate-600" >
- 为了更快打开对话,当前先展示最近 {renderedMessages.length}{" "} - 条消息, - {isRestoredHistoryWindow - ? `更早的 ${hiddenHistoryCount} 条可按需展开。` - : `更早的 ${hiddenHistoryCount} 条会在空闲时继续补齐。`} + {t( + isRestoredHistoryWindow + ? "agentChat.messageList.history.windowSummaryRestored" + : "agentChat.messageList.history.windowSummaryDeferred", + { + loaded: formatContentLength(renderedMessages.length), + hidden: formatContentLength(hiddenHistoryCount), + }, + )}
) : null} @@ -3305,10 +3352,10 @@ const MessageListInner: React.FC = ({

- 正在恢复生成会话... + {t("agentChat.messageList.restoring.title")}

- 正在同步最近一次生成会话,请稍候。 + {t("agentChat.messageList.restoring.description")}

@@ -3328,18 +3375,18 @@ const MessageListInner: React.FC = ({
- 对话 + {t("agentChat.messageList.taskCenterEmpty.badge")}

- 最近对话 + {t("agentChat.messageList.taskCenterEmpty.title")}

- 这里集中展示最近对话、待继续会话和更早归档,方便你随时回到上一次工作现场。 + {t("agentChat.messageList.taskCenterEmpty.description")}

- 还没有对话时,可以先从“新建对话”开始;后续的结果、素材和中间过程都会继续留在这里。 + {t("agentChat.messageList.taskCenterEmpty.helper")}

@@ -3347,13 +3394,13 @@ const MessageListInner: React.FC = ({
- 左侧会优先显示待继续的对话 + {t("agentChat.messageList.taskCenterEmpty.chip.pending")} - 最近对话和归档会按时间自动整理 + {t("agentChat.messageList.taskCenterEmpty.chip.organized")} - 恢复中的会话会自动回到这里继续 + {t("agentChat.messageList.taskCenterEmpty.chip.restore")}
@@ -3365,7 +3412,9 @@ const MessageListInner: React.FC = ({ alt={LIME_BRAND_NAME} className="w-12 h-12 mb-4 opacity-20" /> -

开始一段新的对话吧

+

+ {t("agentChat.messageList.empty.defaultTitle")} +

))} diff --git a/src/components/agent/chat/home/HomeGuideCards.tsx b/src/components/agent/chat/home/HomeGuideCards.tsx index d281b2233..eef5fe1c1 100644 --- a/src/components/agent/chat/home/HomeGuideCards.tsx +++ b/src/components/agent/chat/home/HomeGuideCards.tsx @@ -1,5 +1,6 @@ import styled from "styled-components"; import type { HomeGuideCard } from "./homeSurfaceTypes"; +import type { HomeSurfaceChromeCopy } from "./homeSurfaceCopy"; const GuideGrid = styled.div` display: grid; @@ -83,16 +84,17 @@ const GuideSummary = styled.span` interface HomeGuideCardsProps { cards: HomeGuideCard[]; + copy: Pick; onSelect: (card: HomeGuideCard) => void; } -export function HomeGuideCards({ cards, onSelect }: HomeGuideCardsProps) { +export function HomeGuideCards({ cards, copy, onSelect }: HomeGuideCardsProps) { if (cards.length === 0) { return null; } return ( - + {cards.map((card) => ( = []; +const TEST_CHROME_COPY: Pick = { + moreSkillsDrawerLabel: "更多做法", +}; + beforeEach(() => { ( globalThis as typeof globalThis & { @@ -48,6 +53,7 @@ function renderDrawer( root.render( , diff --git a/src/components/agent/chat/home/HomeMoreSkillsDrawer.tsx b/src/components/agent/chat/home/HomeMoreSkillsDrawer.tsx index c95ff5536..db197ddc9 100644 --- a/src/components/agent/chat/home/HomeMoreSkillsDrawer.tsx +++ b/src/components/agent/chat/home/HomeMoreSkillsDrawer.tsx @@ -3,6 +3,7 @@ import type { HomeSkillSection, HomeSkillSurfaceItem, } from "./homeSurfaceTypes"; +import type { HomeSurfaceChromeCopy } from "./homeSurfaceCopy"; import { resolveHomeCoverAsset } from "./homeCoverAssets"; const Drawer = styled.div` @@ -131,12 +132,14 @@ function resolveCoverGradient(token: string): string { interface HomeMoreSkillsDrawerProps { open: boolean; + copy: Pick; sections: HomeSkillSection[]; onSelectItem: (item: HomeSkillSurfaceItem) => void; } export function HomeMoreSkillsDrawer({ open, + copy, sections, onSelectItem, }: HomeMoreSkillsDrawerProps) { @@ -145,7 +148,10 @@ export function HomeMoreSkillsDrawer({ } return ( - + {sections.map((section) => (
{section.title} diff --git a/src/components/agent/chat/home/HomeSceneSkillManagerDialog.test.tsx b/src/components/agent/chat/home/HomeSceneSkillManagerDialog.test.tsx index cc7b88dcd..d21832715 100644 --- a/src/components/agent/chat/home/HomeSceneSkillManagerDialog.test.tsx +++ b/src/components/agent/chat/home/HomeSceneSkillManagerDialog.test.tsx @@ -2,6 +2,7 @@ import React from "react"; import { act } from "react"; import { createRoot, type Root } from "react-dom/client"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { changeLimeLocale } from "@/i18n/createI18n"; import { HomeSceneSkillManagerDialog } from "./HomeSceneSkillManagerDialog"; import type { SkillCatalog } from "@/lib/api/skillCatalog"; @@ -114,7 +115,8 @@ function setSelectValue(element: HTMLSelectElement, value: string) { element.dispatchEvent(new Event("change", { bubbles: true })); } -beforeEach(() => { +beforeEach(async () => { + await changeLimeLocale("zh-CN"); ( globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean; @@ -220,4 +222,33 @@ describe("HomeSceneSkillManagerDialog", () => { expect(container.textContent).toContain("每日复盘"); }); + + it("场景管理弹窗 chrome 与表单文案应跟随 en-US 资源", async () => { + await changeLimeLocale("en-US"); + const container = renderDialog(); + + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + + expect(container.textContent).toContain("Scene management"); + expect(container.textContent).toContain("Add scene"); + expect(container.textContent).toContain("Visible"); + expect(container.textContent).toContain("Done"); + + const add = Array.from(container.querySelectorAll("button")).find( + (button) => button.textContent === "Add scene", + ); + act(() => { + add?.click(); + }); + + expect(container.textContent).toContain("Add custom scene"); + expect(container.textContent).toContain("Scene name"); + expect(container.querySelector("input")?.placeholder).toBe( + "Example: daily account review", + ); + expect(container.textContent).toContain("Add to list"); + }); }); diff --git a/src/components/agent/chat/home/HomeSceneSkillManagerDialog.tsx b/src/components/agent/chat/home/HomeSceneSkillManagerDialog.tsx index 0abd740e1..084219ad0 100644 --- a/src/components/agent/chat/home/HomeSceneSkillManagerDialog.tsx +++ b/src/components/agent/chat/home/HomeSceneSkillManagerDialog.tsx @@ -1,5 +1,7 @@ import { useEffect, useMemo, useState, type FormEvent } from "react"; +import { useTranslation } from "react-i18next"; import styled from "styled-components"; +import type agentResource from "@/i18n/resources/zh-CN/agent.json"; import { getClientSceneSkillPreferences, updateClientSceneSkillPreferences, @@ -280,6 +282,46 @@ interface DraftScene { templatePrompt: string; } +type AgentI18nKey = keyof typeof agentResource; + +interface SceneManagerCopy { + defaultTemplateTitle: string; + customSceneSummaryFallback: string; + syncSignInRequiredError: string; + createRequiredError: string; + titleList: string; + titleCreate: string; + descriptionList: string; + descriptionCreate: string; + closeAction: string; + noSessionNotice: string; + addSceneAction: string; + loadingNotice: string; + listLabel: string; + kindScene: string; + kindSkill: string; + moveUpLabel: (title: string) => string; + moveDownLabel: (title: string) => string; + visibleLabel: string; + sceneNameLabel: string; + sceneNamePlaceholder: string; + linkedSkillLabel: string; + linkedSkillEmptyOption: string; + placeholderLabel: string; + placeholderPlaceholder: string; + templateTitleLabel: string; + templateDescriptionLabel: string; + templateDescriptionPlaceholder: string; + templatePromptLabel: string; + templatePromptPlaceholder: string; + backAction: string; + addToListAction: string; + restoreDefaultAction: string; + cancelAction: string; + doneAction: string; + savingAction: string; +} + const emptyPreference: OemCloudSceneSkillPreference = { tenantId: "", userId: "", @@ -288,15 +330,17 @@ const emptyPreference: OemCloudSceneSkillPreference = { customScenes: [], }; -const defaultDraft: DraftScene = { - title: "", - summary: "", - linkedEntryId: "", - placeholder: "", - templateTitle: "开始", - templateDescription: "", - templatePrompt: "", -}; +function createDefaultDraft(templateTitle: string): DraftScene { + return { + title: "", + summary: "", + linkedEntryId: "", + placeholder: "", + templateTitle, + templateDescription: "", + templatePrompt: "", + }; +} function canManageEntry(entry: SkillCatalogEntry): boolean { return entry.kind === "skill" || entry.kind === "scene"; @@ -338,7 +382,10 @@ function toRows( return [...ordered, ...baseRows.filter((row) => !orderedIds.has(row.id))]; } -function buildCustomScene(draft: DraftScene): OemCloudCustomScene { +function buildCustomScene( + draft: DraftScene, + defaultTemplateTitle: string, +): OemCloudCustomScene { const idSuffix = draft.title .trim() @@ -356,7 +403,7 @@ function buildCustomScene(draft: DraftScene): OemCloudCustomScene { templates: [ { id: "default", - title: draft.templateTitle.trim() || "开始", + title: draft.templateTitle.trim() || defaultTemplateTitle, description: draft.templateDescription.trim(), prompt: draft.templatePrompt.trim(), }, @@ -373,13 +420,114 @@ export function HomeSceneSkillManagerDialog({ open, onClose, }: HomeSceneSkillManagerDialogProps) { + const { t } = useTranslation("agent"); + const sceneManagerCopy = useMemo( + () => ({ + defaultTemplateTitle: t( + "agentChat.home.sceneManager.defaultTemplateTitle" as AgentI18nKey, + ), + customSceneSummaryFallback: t( + "agentChat.home.sceneManager.customSceneSummaryFallback" as AgentI18nKey, + ), + syncSignInRequiredError: t( + "agentChat.home.sceneManager.error.syncSignInRequired" as AgentI18nKey, + ), + createRequiredError: t( + "agentChat.home.sceneManager.error.createRequired" as AgentI18nKey, + ), + titleList: t("agentChat.home.sceneManager.title.list" as AgentI18nKey), + titleCreate: t( + "agentChat.home.sceneManager.title.create" as AgentI18nKey, + ), + descriptionList: t( + "agentChat.home.sceneManager.description.list" as AgentI18nKey, + ), + descriptionCreate: t( + "agentChat.home.sceneManager.description.create" as AgentI18nKey, + ), + closeAction: t( + "agentChat.home.sceneManager.action.close" as AgentI18nKey, + ), + noSessionNotice: t( + "agentChat.home.sceneManager.notice.noSession" as AgentI18nKey, + ), + addSceneAction: t( + "agentChat.home.sceneManager.action.addScene" as AgentI18nKey, + ), + loadingNotice: t( + "agentChat.home.sceneManager.notice.loading" as AgentI18nKey, + ), + listLabel: t("agentChat.home.sceneManager.list.label" as AgentI18nKey), + kindScene: t("agentChat.home.sceneManager.kind.scene" as AgentI18nKey), + kindSkill: t("agentChat.home.sceneManager.kind.skill" as AgentI18nKey), + moveUpLabel: (title) => + t("agentChat.home.sceneManager.row.moveUp" as AgentI18nKey, { title }), + moveDownLabel: (title) => + t("agentChat.home.sceneManager.row.moveDown" as AgentI18nKey, { + title, + }), + visibleLabel: t( + "agentChat.home.sceneManager.row.visible" as AgentI18nKey, + ), + sceneNameLabel: t( + "agentChat.home.sceneManager.field.sceneName.label" as AgentI18nKey, + ), + sceneNamePlaceholder: t( + "agentChat.home.sceneManager.field.sceneName.placeholder" as AgentI18nKey, + ), + linkedSkillLabel: t( + "agentChat.home.sceneManager.field.linkedSkill.label" as AgentI18nKey, + ), + linkedSkillEmptyOption: t( + "agentChat.home.sceneManager.field.linkedSkill.emptyOption" as AgentI18nKey, + ), + placeholderLabel: t( + "agentChat.home.sceneManager.field.placeholder.label" as AgentI18nKey, + ), + placeholderPlaceholder: t( + "agentChat.home.sceneManager.field.placeholder.placeholder" as AgentI18nKey, + ), + templateTitleLabel: t( + "agentChat.home.sceneManager.field.templateTitle.label" as AgentI18nKey, + ), + templateDescriptionLabel: t( + "agentChat.home.sceneManager.field.templateDescription.label" as AgentI18nKey, + ), + templateDescriptionPlaceholder: t( + "agentChat.home.sceneManager.field.templateDescription.placeholder" as AgentI18nKey, + ), + templatePromptLabel: t( + "agentChat.home.sceneManager.field.templatePrompt.label" as AgentI18nKey, + ), + templatePromptPlaceholder: t( + "agentChat.home.sceneManager.field.templatePrompt.placeholder" as AgentI18nKey, + ), + backAction: t("agentChat.home.sceneManager.action.back" as AgentI18nKey), + addToListAction: t( + "agentChat.home.sceneManager.action.addToList" as AgentI18nKey, + ), + restoreDefaultAction: t( + "agentChat.home.sceneManager.action.restoreDefault" as AgentI18nKey, + ), + cancelAction: t( + "agentChat.home.sceneManager.action.cancel" as AgentI18nKey, + ), + doneAction: t("agentChat.home.sceneManager.action.done" as AgentI18nKey), + savingAction: t( + "agentChat.home.sceneManager.action.saving" as AgentI18nKey, + ), + }), + [t], + ); const [view, setView] = useState<"list" | "create">("list"); const [entries, setEntries] = useState([]); const [preference, setPreference] = useState(emptyPreference); const [rows, setRows] = useState([]); const [customScenes, setCustomScenes] = useState([]); - const [draft, setDraft] = useState(defaultDraft); + const [draft, setDraft] = useState(() => + createDefaultDraft(sceneManagerCopy.defaultTemplateTitle), + ); const [draggingId, setDraggingId] = useState(null); const [loading, setLoading] = useState(false); const [saving, setSaving] = useState(false); @@ -411,7 +559,7 @@ export function HomeSceneSkillManagerDialog({ runtime.tenantId, ); } else { - setError("需要登录云端账号后,才能同步场景 Skills 管理结果。"); + setError(sceneManagerCopy.syncSignInRequiredError); } if (cancelled) { return; @@ -436,7 +584,7 @@ export function HomeSceneSkillManagerDialog({ return () => { cancelled = true; }; - }, [open]); + }, [open, sceneManagerCopy.syncSignInRequiredError]); const linkableEntries = useMemo( () => @@ -481,23 +629,29 @@ export function HomeSceneSkillManagerDialog({ const linkedEntryId = draft.linkedEntryId.trim(); const prompt = draft.templatePrompt.trim(); if (!title || !linkedEntryId || !prompt) { - setError("请填写场景名称、关联技能和模板提示词。"); + setError(sceneManagerCopy.createRequiredError); return; } - const scene = buildCustomScene(draft); + const scene = buildCustomScene( + draft, + sceneManagerCopy.defaultTemplateTitle, + ); setCustomScenes((current) => [...current, scene]); setRows((current) => [ ...current, { id: scene.id ?? `custom_scene:${Date.now()}`, title: scene.title, - summary: scene.summary || scene.templates[0]?.prompt || "自定义场景", + summary: + scene.summary || + scene.templates[0]?.prompt || + sceneManagerCopy.customSceneSummaryFallback, kind: "scene", visible: true, }, ]); - setDraft(defaultDraft); + setDraft(createDefaultDraft(sceneManagerCopy.defaultTemplateTitle)); setView("list"); setError(null); }; @@ -507,7 +661,7 @@ export function HomeSceneSkillManagerDialog({ const syncAvailable = hasOemCloudSession(runtime); if (!runtime || !syncAvailable) { setCanSync(false); - setError("需要登录云端账号后,才能同步场景 Skills 管理结果。"); + setError(sceneManagerCopy.syncSignInRequiredError); return; } setSaving(true); @@ -541,36 +695,38 @@ export function HomeSceneSkillManagerDialog({
- {view === "list" ? "场景管理" : "新增自定义场景"} + {view === "list" + ? sceneManagerCopy.titleList + : sceneManagerCopy.titleCreate} {view === "list" - ? "调整首页场景和 Skills 的顺序、显隐;新增入口会同步到云端。" - : "把已有 SkillCatalog 里的技能包装成更贴近你工作流的起手场景。"} + ? sceneManagerCopy.descriptionList + : sceneManagerCopy.descriptionCreate}
{error ? {error} : null} {!error && !canSync ? ( - - 当前未检测到云端会话,列表可以预览,但保存需要先登录。 - + {sceneManagerCopy.noSessionNotice} ) : null} {view === "list" ? ( <> - {loading ? 正在载入场景 Skills… : null} - + {loading ? ( + {sceneManagerCopy.loadingNotice} + ) : null} + {rows.map((row, index) => ( {row.title} - {row.kind === "scene" ? "场景" : "Skill"} ·{" "} - {row.summary} + {row.kind === "scene" + ? sceneManagerCopy.kindScene + : sceneManagerCopy.kindSkill}{" "} + · {row.summary} setRows((current) => moveRow(current, row.id, -1)) @@ -600,7 +758,7 @@ export function HomeSceneSkillManagerDialog({ setRows((current) => moveRow(current, row.id, 1)) @@ -623,7 +781,7 @@ export function HomeSceneSkillManagerDialog({ ); }} /> - 显示 + {sceneManagerCopy.visibleLabel} @@ -633,10 +791,10 @@ export function HomeSceneSkillManagerDialog({ ) : ( - 场景名称 + {sceneManagerCopy.sceneNameLabel} setDraft((current) => ({ ...current, @@ -646,7 +804,7 @@ export function HomeSceneSkillManagerDialog({ /> - 关联技能 + {sceneManagerCopy.linkedSkillLabel} - 输入框占位提示 + {sceneManagerCopy.placeholderLabel} setDraft((current) => ({ ...current, @@ -679,7 +841,7 @@ export function HomeSceneSkillManagerDialog({ /> - 快捷模板标题 + {sceneManagerCopy.templateTitleLabel} @@ -691,10 +853,10 @@ export function HomeSceneSkillManagerDialog({ /> - 快捷模板描述 + {sceneManagerCopy.templateDescriptionLabel} setDraft((current) => ({ ...current, @@ -704,10 +866,10 @@ export function HomeSceneSkillManagerDialog({ /> - 模板提示词 + {sceneManagerCopy.templatePromptLabel}