From e00b4964e9cb8f8f41d9839737ca56a3197b62cf Mon Sep 17 00:00:00 2001 From: bowen628 Date: Sat, 21 Mar 2026 11:25:01 +0800 Subject: [PATCH 1/3] update remote control ico --- .../NavPanel/components/PersistentFooterActions.tsx | 4 ++-- src/web-ui/src/locales/en-US/common.json | 4 ++-- src/web-ui/src/locales/zh-CN/common.json | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/web-ui/src/app/components/NavPanel/components/PersistentFooterActions.tsx b/src/web-ui/src/app/components/NavPanel/components/PersistentFooterActions.tsx index 5589bc1f..3e7f7b70 100644 --- a/src/web-ui/src/app/components/NavPanel/components/PersistentFooterActions.tsx +++ b/src/web-ui/src/app/components/NavPanel/components/PersistentFooterActions.tsx @@ -1,5 +1,5 @@ import React, { useState, useCallback } from 'react'; -import { Settings, Info, MoreVertical, PictureInPicture2, SquareTerminal, Wifi, Globe } from 'lucide-react'; +import { Settings, Info, MoreVertical, PictureInPicture2, SquareTerminal, Smartphone, Globe } from 'lucide-react'; import { Tooltip, Modal } from '@/component-library'; import { useI18n } from '@/infrastructure/i18n/hooks/useI18n'; import { useSceneManager } from '../../../hooks/useSceneManager'; @@ -162,7 +162,7 @@ const PersistentFooterActions: React.FC = () => { aria-disabled={!hasWorkspace} onClick={handleRemoteConnect} > - + {t('header.remoteConnect')} diff --git a/src/web-ui/src/locales/en-US/common.json b/src/web-ui/src/locales/en-US/common.json index cbbfeacd..82441d72 100644 --- a/src/web-ui/src/locales/en-US/common.json +++ b/src/web-ui/src/locales/en-US/common.json @@ -52,7 +52,7 @@ "showChatPanel": "Show Chat Panel", "hideChatPanel": "Hide Chat Panel", "switchToToolbar": "Switch to Toolbar Mode", - "remoteConnect": "Remote Connect (Beta)", + "remoteConnect": "Remote Control (Beta)", "modeSwitchAriaLabel": "View mode switch", "modeCowork": "Cowork", "modeCoder": "Coder", @@ -344,7 +344,7 @@ "errorCreateFailed": "Failed to create project" }, "remoteConnect": { - "title": "Remote Connect (Beta)", + "title": "Remote Control (Beta)", "tabLan": "LAN", "tabBitfunServer": "BitFun Server", "tabNgrok": "NAT Traversal", diff --git a/src/web-ui/src/locales/zh-CN/common.json b/src/web-ui/src/locales/zh-CN/common.json index edba2f2b..b7bbe2da 100644 --- a/src/web-ui/src/locales/zh-CN/common.json +++ b/src/web-ui/src/locales/zh-CN/common.json @@ -52,7 +52,7 @@ "showChatPanel": "显示聊天面板", "hideChatPanel": "隐藏聊天面板", "switchToToolbar": "切换到工具栏模式", - "remoteConnect": "远程连接 (Beta)", + "remoteConnect": "远程控制 (Beta)", "modeSwitchAriaLabel": "视图模式切换", "modeCowork": "Cowork", "modeCoder": "Coder", @@ -344,7 +344,7 @@ "errorCreateFailed": "创建工程失败" }, "remoteConnect": { - "title": "远程连接 (Beta)", + "title": "远程控制 (Beta)", "tabLan": "局域网", "tabBitfunServer": "BitFun服务器", "tabNgrok": "内网穿透", From ea2238ad18b2430683673c72e004c04a6e68e122 Mon Sep 17 00:00:00 2001 From: bowen628 Date: Sat, 21 Mar 2026 12:50:10 +0800 Subject: [PATCH 2/3] fix(mobile-web): opening new tab for url --- package-lock.json | 248 +++++++++++++++++- package.json | 3 +- src/mobile-web/src/App.tsx | 25 ++ src/mobile-web/src/pages/ChatPage.tsx | 26 +- .../src/services/RemoteSessionManager.ts | 26 +- 5 files changed, 308 insertions(+), 20 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6ae9e48c..7118fd0d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,10 +8,15 @@ "name": "BitFun", "version": "0.2.0", "hasInstallScript": true, + "dependencies": { + "pnpm": "^10.32.1", + "pptxgenjs": "^4.0.1" + }, "devDependencies": { "@tauri-apps/cli": "^2.10.0", "@vitejs/plugin-react": "^4.6.0", "copyfiles": "^2.4.1", + "cross-env": "^10.1.0", "sass": "^1.93.2", "sharp": "^0.34.3", "typescript": "~5.8.3", @@ -314,6 +319,13 @@ "tslib": "^2.4.0" } }, + "node_modules/@epic-web/invariant": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", + "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==", + "dev": true, + "license": "MIT" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", @@ -2232,6 +2244,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "22.19.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", + "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, "node_modules/@vitejs/plugin-react": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", @@ -2451,9 +2472,41 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true, "license": "MIT" }, + "node_modules/cross-env": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", + "integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@epic-web/invariant": "^1.0.0", + "cross-spawn": "^7.0.6" + }, + "bin": { + "cross-env": "dist/bin/cross-env.js", + "cross-env-shell": "dist/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -2630,6 +2683,33 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/https": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https/-/https-1.0.0.tgz", + "integrity": "sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg==", + "license": "ISC" + }, + "node_modules/image-size": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", + "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", + "license": "MIT", + "dependencies": { + "queue": "6.0.2" + }, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=16.x" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, "node_modules/immutable": { "version": "5.1.5", "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz", @@ -2653,7 +2733,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, "license": "ISC" }, "node_modules/is-extglob": { @@ -2698,6 +2777,13 @@ "dev": true, "license": "MIT" }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -2731,6 +2817,57 @@ "node": ">=6" } }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -2829,6 +2966,12 @@ "wrappy": "1" } }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -2839,6 +2982,16 @@ "node": ">=0.10.0" } }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -2859,6 +3012,22 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pnpm": { + "version": "10.32.1", + "resolved": "https://registry.npmjs.org/pnpm/-/pnpm-10.32.1.tgz", + "integrity": "sha512-pwaTjw6JrBRWtlY+q07fHR+vM2jRGR/FxZeQ6W3JGORFarLmfWE94QQ9LoyB+HMD5rQNT/7KnfFe8a1Wc0jyvg==", + "license": "MIT", + "bin": { + "pnpm": "bin/pnpm.cjs", + "pnpx": "bin/pnpx.cjs" + }, + "engines": { + "node": ">=18.12" + }, + "funding": { + "url": "https://opencollective.com/pnpm" + } + }, "node_modules/postcss": { "version": "8.5.8", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", @@ -2888,13 +3057,33 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/pptxgenjs": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pptxgenjs/-/pptxgenjs-4.0.1.tgz", + "integrity": "sha512-TeJISr8wouAuXw4C1F/mC33xbZs/FuEG6nH9FG1Zj+nuPcGMP5YRHl6X+j3HSUnS1f3at6k75ZZXPMZlA5Lj9A==", + "license": "MIT", + "dependencies": { + "@types/node": "^22.8.1", + "https": "^1.0.0", + "image-size": "^1.2.1", + "jszip": "^3.10.1" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true, "license": "MIT" }, + "node_modules/queue": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", + "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", + "license": "MIT", + "dependencies": { + "inherits": "~2.0.3" + } + }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", @@ -2991,7 +3180,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, "license": "MIT" }, "node_modules/sass": { @@ -3025,6 +3213,12 @@ "semver": "bin/semver.js" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, "node_modules/sharp": { "version": "0.34.5", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", @@ -3083,6 +3277,29 @@ "node": ">=10" } }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -3211,6 +3428,12 @@ "node": ">=14.17" } }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, "node_modules/untildify": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", @@ -3256,7 +3479,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, "license": "MIT" }, "node_modules/vite": { @@ -3334,6 +3556,22 @@ } } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", diff --git a/package.json b/package.json index 3bdf60c6..4e259be4 100644 --- a/package.json +++ b/package.json @@ -74,6 +74,7 @@ }, "packageManager": "pnpm@10.15.0+sha512.486ebc259d3e999a4e8691ce03b5cac4a71cbeca39372a9b762cb500cfdf0873e2cb16abe3d951b1ee2cf012503f027b98b6584e4df22524e0c7450d9ec7aa7b", "dependencies": { - "pnpm": "^10.32.1" + "pnpm": "^10.32.1", + "pptxgenjs": "^4.0.1" } } diff --git a/src/mobile-web/src/App.tsx b/src/mobile-web/src/App.tsx index 22efe185..086aa3ca 100644 --- a/src/mobile-web/src/App.tsx +++ b/src/mobile-web/src/App.tsx @@ -73,6 +73,31 @@ const AppContent: React.FC = () => { useEffect(() => () => clearTimeout(timerRef.current), []); + // 全局链接点击处理 - 确保所有外部链接在新标签页打开 + useEffect(() => { + const handleLinkClick = (e: MouseEvent) => { + const target = e.target as HTMLElement; + const link = target.closest('a') as HTMLAnchorElement | null; + + if (link && link.href) { + const href = link.href; + // 检查是否是外部链接 (http/https 且不是当前域名) + if (href.startsWith('http://') || href.startsWith('https://')) { + e.preventDefault(); + e.stopPropagation(); + window.open(href, '_blank', 'noopener,noreferrer'); + } + } + }; + + // 添加全局点击监听 + document.addEventListener('click', handleLinkClick, true); + + return () => { + document.removeEventListener('click', handleLinkClick, true); + }; + }, []); + const handlePaired = useCallback( (client: RelayHttpClient, sessionMgr: RemoteSessionManager) => { clientRef.current = client; diff --git a/src/mobile-web/src/pages/ChatPage.tsx b/src/mobile-web/src/pages/ChatPage.tsx index de09603a..df3f5f42 100644 --- a/src/mobile-web/src/pages/ChatPage.tsx +++ b/src/mobile-web/src/pages/ChatPage.tsx @@ -466,17 +466,21 @@ const MarkdownContent: React.FC = ({ content, onFileDownlo // Fallback: render as plain text for computer:// links without handler, // or as a regular link for http(s) links. - if (typeof href === 'string' && (href.startsWith('http://') || href.startsWith('https://'))) { - return ( - - {children} - - ); + if (typeof href === 'string') { + // 所有外部链接都在新标签页打开 + const isExternalLink = href.startsWith('http://') || href.startsWith('https://'); + if (isExternalLink) { + return ( + + {children} + + ); + } } return {children}; diff --git a/src/mobile-web/src/services/RemoteSessionManager.ts b/src/mobile-web/src/services/RemoteSessionManager.ts index 2183a0fd..c29a2a0f 100644 --- a/src/mobile-web/src/services/RemoteSessionManager.ts +++ b/src/mobile-web/src/services/RemoteSessionManager.ts @@ -445,6 +445,8 @@ export class SessionPoller { private stopped = false; private hasActiveTurn = false; private knownModelCatalogVersion = 0; + private turnJustEndedAt: number | null = null; + private readonly TURN_JUST_ENDED_GRACE_PERIOD_MS = 5000; constructor( sessionMgr: RemoteSessionManager, @@ -492,6 +494,13 @@ export class SessionPoller { private getInterval(): number { if (document.visibilityState !== 'visible') return 5000; + + // 如果在宽限期内(turn 刚刚结束),继续保持快速轮询 + const now = Date.now(); + if (this.turnJustEndedAt != null && (now - this.turnJustEndedAt) < this.TURN_JUST_ENDED_GRACE_PERIOD_MS) { + return 1000; + } + return this.hasActiveTurn ? 1000 : 10000; } @@ -528,9 +537,20 @@ export class SessionPoller { // When changed=false the backend omits active_turn, so we must // preserve the previous value to keep 1-second fast polling alive. if (resp.changed) { - this.hasActiveTurn = resp.active_turn != null && resp.active_turn.status === 'active'; - } - if (resp.changed) { + const wasActive = this.hasActiveTurn; + const isActiveNow = resp.active_turn != null && resp.active_turn.status === 'active'; + this.hasActiveTurn = isActiveNow; + + // 检测到 active_turn 刚刚结束,设置宽限期 + if (wasActive && !isActiveNow) { + this.turnJustEndedAt = Date.now(); + } + + // 如果有新消息或者 active_turn 仍然活跃,重置宽限期 + if (resp.new_messages && resp.new_messages.length > 0) { + this.turnJustEndedAt = null; + } + this.sinceVersion = resp.version; if (resp.total_msg_count != null) { this.knownMsgCount = resp.total_msg_count; From 92da39cfaeadda3d0d037a92ab376fa0c631abc4 Mon Sep 17 00:00:00 2001 From: bowen628 Date: Sat, 21 Mar 2026 14:03:13 +0800 Subject: [PATCH 3/3] feat: Native Multimodal API Calling Support --- pnpm-lock.yaml | 34 ++++++++++++++ .../core/src/agentic/agents/agentic_mode.rs | 1 - .../core/src/agentic/agents/claw_mode.rs | 1 - .../src/agentic/execution/execution_engine.rs | 23 +++------- .../src/agentic/execution/round_executor.rs | 44 +------------------ .../image_analysis/image_processing.rs | 33 ++++++++++++++ src/crates/core/src/agentic/tools/registry.rs | 4 +- .../config/components/AIModelConfig.tsx | 3 ++ .../src/infrastructure/config/types/index.ts | 9 ++-- 9 files changed, 85 insertions(+), 67 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 98c0d8b3..30d6b006 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: pnpm: specifier: ^10.32.1 version: 10.32.1 + pptxgenjs: + specifier: ^4.0.1 + version: 4.0.1 devDependencies: '@tauri-apps/cli': specifier: ^2.10.0 @@ -2915,6 +2918,9 @@ packages: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} + https@1.0.0: + resolution: {integrity: sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg==} + human-signals@8.0.1: resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} engines: {node: '>=18.18.0'} @@ -2938,6 +2944,11 @@ packages: ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + image-size@1.2.1: + resolution: {integrity: sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==} + engines: {node: '>=16.x'} + hasBin: true + immediate@3.0.6: resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} @@ -3716,6 +3727,9 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + pptxgenjs@4.0.1: + resolution: {integrity: sha512-TeJISr8wouAuXw4C1F/mC33xbZs/FuEG6nH9FG1Zj+nuPcGMP5YRHl6X+j3HSUnS1f3at6k75ZZXPMZlA5Lj9A==} + preact@10.28.4: resolution: {integrity: sha512-uKFfOHWuSNpRFVTnljsCluEFq57OKT+0QdOiQo8XWnQ/pSvg7OpX5eNOejELXJMWy+BwM2nobz0FkvzmnpCNsQ==} @@ -3784,6 +3798,9 @@ packages: query-selector-shadow-dom@1.0.1: resolution: {integrity: sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==} + queue@6.0.2: + resolution: {integrity: sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==} + quick-format-unescaped@4.0.4: resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} @@ -7669,6 +7686,8 @@ snapshots: transitivePeerDependencies: - supports-color + https@1.0.0: {} + human-signals@8.0.1: {} i18next@25.8.0(typescript@5.8.3): @@ -7687,6 +7706,10 @@ snapshots: ieee754@1.2.1: {} + image-size@1.2.1: + dependencies: + queue: 6.0.2 + immediate@3.0.6: {} immer@11.1.4: {} @@ -8707,6 +8730,13 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + pptxgenjs@4.0.1: + dependencies: + '@types/node': 22.19.7 + https: 1.0.0 + image-size: 1.2.1 + jszip: 3.10.1 + preact@10.28.4: {} pretty-format@30.3.0: @@ -8795,6 +8825,10 @@ snapshots: query-selector-shadow-dom@1.0.1: {} + queue@6.0.2: + dependencies: + inherits: 2.0.4 + quick-format-unescaped@4.0.4: {} randombytes@2.1.0: diff --git a/src/crates/core/src/agentic/agents/agentic_mode.rs b/src/crates/core/src/agentic/agents/agentic_mode.rs index b9870b8e..da296fe6 100644 --- a/src/crates/core/src/agentic/agents/agentic_mode.rs +++ b/src/crates/core/src/agentic/agents/agentic_mode.rs @@ -23,7 +23,6 @@ impl AgenticMode { "IdeControl".to_string(), "MermaidInteractive".to_string(), "ReadLints".to_string(), - "view_image".to_string(), "Skill".to_string(), "AskUserQuestion".to_string(), "Git".to_string(), diff --git a/src/crates/core/src/agentic/agents/claw_mode.rs b/src/crates/core/src/agentic/agents/claw_mode.rs index 71f371b9..8ca14129 100644 --- a/src/crates/core/src/agentic/agents/claw_mode.rs +++ b/src/crates/core/src/agentic/agents/claw_mode.rs @@ -21,7 +21,6 @@ impl ClawMode { "WebSearch".to_string(), "IdeControl".to_string(), "MermaidInteractive".to_string(), - "view_image".to_string(), "Skill".to_string(), "Git".to_string(), "TerminalControl".to_string(), diff --git a/src/crates/core/src/agentic/execution/execution_engine.rs b/src/crates/core/src/agentic/execution/execution_engine.rs index 92df5474..81bfb2c2 100644 --- a/src/crates/core/src/agentic/execution/execution_engine.rs +++ b/src/crates/core/src/agentic/execution/execution_engine.rs @@ -16,7 +16,7 @@ use crate::agentic::tools::{get_all_registered_tools, SubagentParentInfo}; use crate::agentic::WorkspaceBinding; use crate::infrastructure::ai::get_global_ai_client_factory; use crate::service::config::get_global_config_service; -use crate::service::config::types::ModelCapability; +use crate::service::config::types::{ModelCapability, ModelCategory}; use crate::util::errors::{BitFunError, BitFunResult}; use crate::util::token_counter::TokenCounter; use crate::util::types::Message as AIMessage; @@ -292,7 +292,6 @@ impl ExecutionEngine { fn render_multimodal_as_text( text: &str, images: &[ImageContextData], - can_use_view_image: bool, ) -> String { let mut content = text.to_string(); @@ -312,8 +311,6 @@ impl ExecutionEngine { .or_else(|| image.image_path.as_ref().filter(|s| !s.is_empty()).cloned()) .unwrap_or_else(|| image.id.clone()); - // Keep the raw image payload out of text-only models. - // Provide `image_id` so the primary model can choose to call `view_image` when needed. content.push_str(&format!( "- {} ({}, image_id={})\n", name, image.mime_type, image.id @@ -321,15 +318,7 @@ impl ExecutionEngine { } content.push_str("]\n"); - if can_use_view_image { - content.push_str( - "If you need to inspect an image, call the `view_image` tool with `image_id`.\n", - ); - } else { - content.push_str( - "Note: image inspection is not available for this session.\n", - ); - } + content.push_str("Note: image inspection is not available for this session.\n"); content } @@ -701,6 +690,7 @@ impl ExecutionEngine { m.capabilities .iter() .any(|cap| matches!(cap, ModelCapability::ImageUnderstanding)) + || matches!(m.category, ModelCategory::Multimodal) }); (resolved_id, supports) @@ -732,11 +722,8 @@ impl ExecutionEngine { execution_context_vars.insert("turn_index".to_string(), context.turn_index.to_string()); // If the primary model is text-only, do not send image payloads to the provider. - // Instead, keep a text-only placeholder (including `image_id`) so the model can decide - // whether it wants to call `view_image` explicitly. + // Instead, keep a text-only placeholder (including `image_id`). if !primary_supports_image_understanding { - let can_use_view_image = available_tools.iter().any(|t| t == "view_image"); - for msg in messages.iter_mut() { let MessageContent::Multimodal { text, images } = &msg.content else { continue; @@ -747,7 +734,7 @@ impl ExecutionEngine { // Replace multimodal messages with text-only versions to avoid provider errors. let next_text = - Self::render_multimodal_as_text(&original_text, &original_images, can_use_view_image); + Self::render_multimodal_as_text(&original_text, &original_images); msg.content = MessageContent::Text(next_text); msg.metadata.tokens = None; diff --git a/src/crates/core/src/agentic/execution/round_executor.rs b/src/crates/core/src/agentic/execution/round_executor.rs index 6bdbcb14..3326355a 100644 --- a/src/crates/core/src/agentic/execution/round_executor.rs +++ b/src/crates/core/src/agentic/execution/round_executor.rs @@ -4,9 +4,8 @@ use super::stream_processor::StreamProcessor; use super::types::{FinishReason, RoundContext, RoundResult}; -use crate::agentic::core::{render_system_reminder, Message, MessageSemanticKind}; +use crate::agentic::core::Message; use crate::agentic::events::{AgenticEvent, EventPriority, EventQueue}; -use crate::agentic::image_analysis::ImageContextData as ModelImageContextData; use crate::agentic::tools::pipeline::{ToolExecutionContext, ToolExecutionOptions, ToolPipeline}; use crate::agentic::tools::registry::get_global_tool_registry; use crate::agentic::MessageContent; @@ -17,7 +16,6 @@ use crate::util::types::Message as AIMessage; use crate::util::types::ToolDefinition; use dashmap::DashMap; use log::{debug, error, warn}; -use serde_json::Value as JsonValue; use std::sync::Arc; use std::time::Duration; use tokio_util::sync::CancellationToken; @@ -439,32 +437,7 @@ impl RoundExecutor { // Create tool result messages (also need to set turn_id and round_id) let dialog_turn_id = context.dialog_turn_id.clone(); let round_id_clone = round_id.clone(); - let primary_supports_images = context - .context_vars - .get("primary_model_supports_image_understanding") - .and_then(|v| v.parse::().ok()) - .unwrap_or(false); - let extract_attached_image = |result: &JsonValue| -> Option { - if !primary_supports_images { - return None; - } - let mode = result.get("mode").and_then(|v| v.as_str())?; - if mode != "attached_to_primary_model" { - return None; - } - let image_value = result.get("image")?; - serde_json::from_value::(image_value.clone()).ok() - }; - let mut injected_images = Vec::new(); - for result in &tool_results { - if result.tool_name == "view_image" && !result.is_error { - if let Some(image_ctx) = extract_attached_image(&result.result) { - injected_images.push(image_ctx); - } - } - } - - let mut tool_result_messages: Vec = tool_results + let tool_result_messages: Vec = tool_results .into_iter() .map(|result| { Message::tool_result(result) @@ -473,19 +446,6 @@ impl RoundExecutor { }) .collect(); - if !injected_images.is_empty() { - let reminder_text = render_system_reminder(&format!( - "Attached {} image(s) from view_image tool.", - injected_images.len() - )); - tool_result_messages.push( - Message::user_multimodal(reminder_text, injected_images) - .with_semantic_kind(MessageSemanticKind::InternalReminder) - .with_turn_id(dialog_turn_id.clone()) - .with_round_id(round_id_clone.clone()), - ); - } - let has_more_rounds = !tool_result_messages.is_empty(); debug!( diff --git a/src/crates/core/src/agentic/image_analysis/image_processing.rs b/src/crates/core/src/agentic/image_analysis/image_processing.rs index d5c5bbea..1e12ed51 100644 --- a/src/crates/core/src/agentic/image_analysis/image_processing.rs +++ b/src/crates/core/src/agentic/image_analysis/image_processing.rs @@ -273,6 +273,26 @@ pub fn build_multimodal_message( tool_call_id: None, name: None, } + } else if provider_lower.contains("gemini") || provider_lower.contains("google") { + Message { + role: "user".to_string(), + content: Some(serde_json::to_string(&json!([ + { + "inline_data": { + "mime_type": mime_type, + "data": base64_data + } + }, + { + "text": prompt + } + ]))?), + reasoning_content: None, + thinking_signature: None, + tool_calls: None, + tool_call_id: None, + name: None, + } } else { // Default to OpenAI-compatible payload shape for OpenAI and most OpenAI-compatible providers. Message { @@ -370,6 +390,19 @@ pub fn build_multimodal_message_with_images( "text": prompt })); json!(blocks) + } else if provider_lower.contains("gemini") || provider_lower.contains("google") { + let mut parts = Vec::with_capacity(images.len() + 1); + for img in images { + let base64_data = BASE64.encode(&img.data); + parts.push(json!({ + "inline_data": { + "mime_type": img.mime_type, + "data": base64_data + } + })); + } + parts.push(json!({ "text": prompt })); + json!(parts) } else { let mut blocks = Vec::with_capacity(images.len() + 1); for img in images { diff --git a/src/crates/core/src/agentic/tools/registry.rs b/src/crates/core/src/agentic/tools/registry.rs index 14a52b26..9b4caa49 100644 --- a/src/crates/core/src/agentic/tools/registry.rs +++ b/src/crates/core/src/agentic/tools/registry.rs @@ -128,8 +128,8 @@ impl ToolRegistry { // Linter tool (LSP diagnosis) self.register_tool(Arc::new(ReadLintsTool::new())); - // Image analysis / viewing tool - self.register_tool(Arc::new(ViewImageTool::new())); + // Image analysis / viewing tool (deprecated - use native multimodal support) + // self.register_tool(Arc::new(ViewImageTool::new())); // Git version control tool self.register_tool(Arc::new(GitTool::new())); diff --git a/src/web-ui/src/infrastructure/config/components/AIModelConfig.tsx b/src/web-ui/src/infrastructure/config/components/AIModelConfig.tsx index d69ff438..e4ad7494 100644 --- a/src/web-ui/src/infrastructure/config/components/AIModelConfig.tsx +++ b/src/web-ui/src/infrastructure/config/components/AIModelConfig.tsx @@ -70,6 +70,7 @@ function uniqModelNames(modelNames: string[]): string[] { function getCapabilitiesByCategory(category: ModelCategory): ModelCapability[] { switch (category) { case 'general_chat': + case 'multimodal': default: return ['text_chat', 'function_calling']; } @@ -193,6 +194,7 @@ const AIModelConfig: React.FC = () => { const categoryOptions = useMemo( () => [ { label: t('category.general_chat'), value: 'general_chat' }, + { label: t('category.multimodal'), value: 'multimodal' }, ], [t] ); @@ -200,6 +202,7 @@ const AIModelConfig: React.FC = () => { const categoryCompactLabels = useMemo>( () => ({ general_chat: t('categoryIcons.general_chat'), + multimodal: t('categoryIcons.multimodal'), }), [t] ); diff --git a/src/web-ui/src/infrastructure/config/types/index.ts b/src/web-ui/src/infrastructure/config/types/index.ts index 9eaa67d8..21962872 100644 --- a/src/web-ui/src/infrastructure/config/types/index.ts +++ b/src/web-ui/src/infrastructure/config/types/index.ts @@ -71,7 +71,8 @@ export type ModelCapability = | 'function_calling'; export type ModelCategory = - | 'general_chat'; + | 'general_chat' + | 'multimodal'; export interface ModelMetadata { category: ModelCategory; @@ -82,12 +83,14 @@ export interface ModelMetadata { export const CATEGORY_LABELS: Record = { - general_chat: t('settings/ai-model:category.general_chat') + general_chat: t('settings/ai-model:category.general_chat'), + multimodal: t('settings/ai-model:category.multimodal') }; export const CATEGORY_ICONS: Record = { - general_chat: t('settings/ai-model:categoryIcons.general_chat') + general_chat: t('settings/ai-model:categoryIcons.general_chat'), + multimodal: t('settings/ai-model:categoryIcons.multimodal') };