@rexvane/piweb 0.3.24 → 0.3.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/BUILD_ID +1 -1
- package/.next/app-path-routes-manifest.json +11 -11
- package/.next/build-manifest.json +3 -3
- package/.next/prerender-manifest.json +5 -5
- package/.next/react-loadable-manifest.json +13 -20
- package/.next/required-server-files.js +2 -1
- package/.next/required-server-files.json +2 -1
- package/.next/routes-manifest.json +6 -8
- package/.next/server/app/_global-error/page.js +2 -2
- package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page.js +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +2 -2
- package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/api/agent/[id]/events/route.js +2 -2
- package/.next/server/app/api/agent/[id]/events/route.js.nft.json +1 -1
- package/.next/server/app/api/agent/[id]/route.js +1 -1
- package/.next/server/app/api/agent/[id]/route.js.nft.json +1 -1
- package/.next/server/app/api/agent/[id]/tree/route.js +1 -1
- package/.next/server/app/api/agent/[id]/tree/route.js.nft.json +1 -1
- package/.next/server/app/api/agent/new/route.js +1 -1
- package/.next/server/app/api/agent/new/route.js.nft.json +1 -1
- package/.next/server/app/api/agent/resources/route.js +1 -0
- package/.next/server/app/api/agent/resources/route.js.nft.json +1 -0
- package/.next/server/app/api/agent/resources/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/config/route.js +1 -1
- package/.next/server/app/api/config/route.js.nft.json +1 -1
- package/.next/server/app/api/files/route.js +1 -1
- package/.next/server/app/api/files/route.js.nft.json +1 -1
- package/.next/server/app/api/fs/route.js +1 -1
- package/.next/server/app/api/fs/route.js.nft.json +1 -1
- package/.next/server/app/api/git/route.js +4 -4
- package/.next/server/app/api/git/route.js.nft.json +1 -1
- package/.next/server/app/api/github/route.js +4 -4
- package/.next/server/app/api/github/route.js.nft.json +1 -1
- package/.next/server/app/api/growth/route.js +1 -1
- package/.next/server/app/api/growth/route.js.nft.json +1 -1
- package/.next/server/app/api/health/route.js +1 -1
- package/.next/server/app/api/models/defaults/route.js +1 -1
- package/.next/server/app/api/models/defaults/route.js.nft.json +1 -1
- package/.next/server/app/api/models/route.js +1 -1
- package/.next/server/app/api/models/route.js.nft.json +1 -1
- package/.next/server/app/api/pi-settings/route.js +1 -1
- package/.next/server/app/api/pi-settings/route.js.nft.json +1 -1
- package/.next/server/app/api/plugins/route.js +1 -1
- package/.next/server/app/api/plugins/route.js.nft.json +1 -1
- package/.next/server/app/api/prompts/route.js +1 -1
- package/.next/server/app/api/prompts/route.js.nft.json +1 -1
- package/.next/server/app/api/remote/route.js +4 -3
- package/.next/server/app/api/security/route.js +1 -1
- package/.next/server/app/api/security/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/[id]/export/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/export/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/[id]/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/import/route.js +2 -2
- package/.next/server/app/api/sessions/import/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/route.js +1 -1
- package/.next/server/app/api/sessions/route.js.nft.json +1 -1
- package/.next/server/app/api/skills/route.js +2 -2
- package/.next/server/app/api/skills/route.js.nft.json +1 -1
- package/.next/server/app/api/update/route.js +2 -2
- package/.next/server/app/api/update/route.js.nft.json +1 -1
- package/.next/server/app/api/version/route.js +1 -1
- package/.next/server/app/api/version/route.js.nft.json +1 -1
- package/.next/server/app/api/web-auth/route.js +2 -2
- package/.next/server/app/api/workspaces/route.js +1 -1
- package/.next/server/app/api/workspaces/route.js.nft.json +1 -1
- package/.next/server/app/icon.svg/route.js +1 -1
- package/.next/server/app/index.html +1 -1
- package/.next/server/app/index.rsc +6 -7
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +6 -7
- package/.next/server/app/index.segments/_full.segment.rsc +6 -7
- package/.next/server/app/index.segments/_tree.segment.rsc +2 -3
- package/.next/server/app/login/page.js +1 -1
- package/.next/server/app/login/page_client-reference-manifest.js +1 -1
- package/.next/server/app/login.html +1 -1
- package/.next/server/app/login.rsc +3 -3
- package/.next/server/app/login.segments/_full.segment.rsc +3 -3
- package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +3 -3
- package/.next/server/app/page.js +1 -1
- package/.next/server/app/page.js.nft.json +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +11 -11
- package/.next/server/chunks/3924.js +3 -3
- package/.next/server/chunks/538.js +1 -1
- package/.next/server/chunks/620.js +1 -0
- package/.next/server/chunks/6229.js +1 -1
- package/.next/server/chunks/7496.js +1 -1
- package/.next/server/chunks/7662.js +2 -2
- package/.next/server/chunks/8686.js +1 -1
- package/.next/server/functions-config-manifest.json +1 -2
- package/.next/server/instrumentation.js.nft.json +1 -1
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/preview-props.json +3 -3
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/QJCtqgDKzGS2BdKABd9ch/_buildManifest.js +1 -0
- package/.next/static/chunks/2577.86c8538fbf05a8b2.js +1 -0
- package/.next/static/chunks/2716.fa561519675a3ff1.js +1 -0
- package/.next/static/chunks/2906.8a0e09bb47605cf1.js +1 -0
- package/.next/static/chunks/4673.ea36e55005c0f82f.js +1 -0
- package/.next/static/chunks/4686-4dc565817a160351.js +1 -0
- package/.next/static/chunks/4778.0d2c5d75b791d352.js +1 -0
- package/.next/static/chunks/{618.949981da99be7182.js → 618.9a22656b863f242b.js} +2 -2
- package/.next/static/chunks/app/_global-error/page-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/events/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/tree/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/agent/new/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/agent/resources/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/config/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/files/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/fs/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/git/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/github/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/growth/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/health/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/models/defaults/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/models/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/pi-settings/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/plugins/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/prompts/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/remote/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/security/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/export/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/sessions/import/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/sessions/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/skills/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/update/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/version/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/web-auth/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/workspaces/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/page-71f2d3b3ed864b3d.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/webpack-ffc09b5fcc8aaa6d.js +1 -0
- package/.next/static/css/014eb1f21414e615.css +1 -0
- package/.next/static/css/56b129a13637fcfd.css +1 -0
- package/.next/static/css/7e3caf797e4cb19a.css +1 -0
- package/.next/types/app/api/agent/{[id]/side-chat → resources}/route.ts +3 -3
- package/.next/types/routes.d.ts +2 -2
- package/.next/types/validator.ts +9 -9
- package/README.md +1 -1
- package/next.config.ts +1 -1
- package/package.json +9 -9
- package/scripts/apply-global.mjs +87 -0
- package/scripts/launcher.mjs +21 -0
- package/scripts/testing/prepare-sdk-tools.mjs +19 -0
- package/src/app/api/agent/new/route.ts +2 -1
- package/src/app/api/agent/resources/route.ts +18 -0
- package/src/app/api/models/route.ts +22 -1
- package/src/app/api/plugins/route.ts +3 -2
- package/src/app/api/prompts/route.ts +3 -2
- package/src/app/api/remote/route.ts +22 -4
- package/src/app/api/sessions/[id]/export/route.ts +3 -1
- package/src/app/api/skills/route.ts +3 -2
- package/src/app/globals.css +1002 -1765
- package/src/components/AppShell.tsx +128 -93
- package/src/components/ChatInput.tsx +177 -98
- package/src/components/ChatWindow.tsx +189 -56
- package/src/components/ContextMeter.tsx +33 -9
- package/src/components/CustomSelect.module.css +142 -0
- package/src/components/CustomSelect.tsx +157 -0
- package/src/components/DrawerPanel.tsx +1 -1
- package/src/components/ExtensionUI.tsx +9 -3
- package/src/components/FileViewer.tsx +24 -21
- package/src/components/GitPanel.tsx +35 -13
- package/src/components/GrowthTree.tsx +6 -2
- package/src/components/ImageViewer.tsx +72 -0
- package/src/components/MarkdownBlock.tsx +67 -96
- package/src/components/ModelSelector.tsx +36 -39
- package/src/components/PiMark.tsx +76 -41
- package/src/components/PreviewImage.tsx +33 -0
- package/src/components/ProjectPanel.tsx +8 -5
- package/src/components/PromptPanel.tsx +85 -20
- package/src/components/ProviderSetupModal.module.css +12 -11
- package/src/components/ProviderSetupModal.tsx +107 -36
- package/src/components/RemoteSection.tsx +53 -2
- package/src/components/SessionImportSection.tsx +30 -15
- package/src/components/SessionSidebar.tsx +72 -62
- package/src/components/SettingsPanel.tsx +49 -151
- package/src/components/TrajectoryAnalyzer.module.css +26 -11
- package/src/components/TrajectoryView.tsx +11 -3
- package/src/components/chat/markdown-actions.tsx +23 -0
- package/src/components/composer/ComposerFiles.tsx +224 -0
- package/src/components/composer/ComposerImages.tsx +59 -127
- package/src/hooks/usePiWeb.ts +47 -2
- package/src/i18n.tsx +62 -80
- package/src/lib/agent-manager.ts +365 -33
- package/src/lib/chat/file-path.ts +54 -0
- package/src/lib/chat/skill-expansion.ts +51 -0
- package/src/lib/extension-ui.ts +25 -1
- package/src/lib/font.ts +37 -39
- package/src/lib/git-service.ts +4 -2
- package/src/lib/growth-turns.ts +3 -1
- package/src/lib/model-draft.ts +69 -2
- package/src/lib/models-service.ts +53 -3
- package/src/lib/pi.ts +2 -2
- package/src/lib/remote/assets.json +1 -1
- package/src/lib/remote/supervisor.ts +48 -5
- package/src/lib/security-service.ts +2 -1
- package/src/lib/session-reader.ts +25 -13
- package/src/lib/session-retention.ts +11 -8
- package/src/lib/skills-service.ts +2 -1
- package/src/lib/slash-command-token.ts +14 -0
- package/src/lib/trajectory.ts +111 -15
- package/src/lib/types.ts +28 -3
- package/src/lib/update-service.ts +29 -9
- package/src/lib/workspace-store.ts +36 -1
- package/.next/server/app/api/agent/[id]/side-chat/route.js +0 -5
- package/.next/server/app/api/agent/[id]/side-chat/route.js.nft.json +0 -1
- package/.next/server/app/api/agent/[id]/side-chat/route_client-reference-manifest.js +0 -1
- package/.next/server/chunks/3421.js +0 -1
- package/.next/static/chunks/2716.44fdf1b21454b961.js +0 -1
- package/.next/static/chunks/4673.e261707dce391be7.js +0 -1
- package/.next/static/chunks/4686-0cd87513f9ea3538.js +0 -1
- package/.next/static/chunks/4778.eddd3a5813f85824.js +0 -1
- package/.next/static/chunks/7411.5f79671208209b13.js +0 -1
- package/.next/static/chunks/8552.25c39ef4c995eba0.js +0 -1
- package/.next/static/chunks/app/_global-error/page-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/events/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/side-chat/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/tree/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/agent/new/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/config/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/files/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/fs/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/git/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/github/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/growth/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/health/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/models/defaults/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/models/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/pi-settings/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/plugins/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/prompts/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/remote/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/security/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/export/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/sessions/import/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/sessions/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/skills/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/update/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/version/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/web-auth/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/workspaces/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/page-dc3cb41d91c7432e.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/webpack-2ac3d76b81895f5b.js +0 -1
- package/.next/static/css/08815507594e8b8d.css +0 -1
- package/.next/static/css/9fc559c2c4ad1d0f.css +0 -1
- package/.next/static/css/c6e2227c36ec2fd6.css +0 -1
- package/.next/static/css/f15a853af8b5127f.css +0 -1
- package/.next/static/lvKCJmjFj8LuqSojRVLRF/_buildManifest.js +0 -1
- package/src/app/api/agent/[id]/side-chat/route.ts +0 -77
- package/src/components/side-chat/SideChatPanel.module.css +0 -131
- package/src/components/side-chat/SideChatPanel.tsx +0 -189
- package/src/components/side-chat/icons.tsx +0 -8
- package/src/components/side-chat/request-history.ts +0 -29
- package/src/components/side-chat/store.ts +0 -248
- package/src/lib/chat/stream-settings.ts +0 -192
- package/src/lib/side-chat/context.ts +0 -123
- package/src/lib/side-chat/service.ts +0 -191
- package/src/lib/side-chat/types.ts +0 -61
- /package/.next/static/{lvKCJmjFj8LuqSojRVLRF → QJCtqgDKzGS2BdKABd9ch}/_ssgManifest.js +0 -0
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* 技能 / 提示模板,含扩展与 npm 包提供的那些);热会话额外给出"组装后的系统提示词"
|
|
7
7
|
* 与每个工具的定义,它们不是文件、没有路径,直接在本面板内展开查看。
|
|
8
8
|
*/
|
|
9
|
-
import { useCallback, useEffect, useState } from "react";
|
|
9
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
10
10
|
import { useI18n } from "@/i18n";
|
|
11
11
|
import { fmtBytes } from "@/lib/format";
|
|
12
12
|
import { IconRefreshOutline14, IconCloseOutline14 } from "@/components/icons";
|
|
@@ -69,8 +69,11 @@ export function PromptPanel({
|
|
|
69
69
|
const [error, setError] = useState<string | null>(null);
|
|
70
70
|
const [busy, setBusy] = useState(false);
|
|
71
71
|
const [opening, setOpening] = useState<string | null>(null);
|
|
72
|
+
/** 只认最后一次请求:快速切会话时,先发的请求后回来会把上一个会话的提示词盖到当前会话上 */
|
|
73
|
+
const latestRequest = useRef(0);
|
|
72
74
|
|
|
73
75
|
const load = useCallback(async () => {
|
|
76
|
+
const request = ++latestRequest.current;
|
|
74
77
|
setBusy(true);
|
|
75
78
|
setError(null);
|
|
76
79
|
try {
|
|
@@ -78,15 +81,21 @@ export function PromptPanel({
|
|
|
78
81
|
if (sessionId) query.set("session", sessionId);
|
|
79
82
|
const response = await fetch(`/api/prompts?${query.toString()}`);
|
|
80
83
|
const body = await response.json();
|
|
84
|
+
if (request !== latestRequest.current) return;
|
|
81
85
|
if (!response.ok || !body.success) throw new Error(body.error || `request failed (${response.status})`);
|
|
82
86
|
setData(body.data as PromptSourcesData);
|
|
83
87
|
} catch (err) {
|
|
84
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
88
|
+
if (request === latestRequest.current) setError(err instanceof Error ? err.message : String(err));
|
|
85
89
|
} finally {
|
|
86
|
-
setBusy(false);
|
|
90
|
+
if (request === latestRequest.current) setBusy(false);
|
|
87
91
|
}
|
|
88
92
|
}, [cwd, sessionId]);
|
|
89
93
|
|
|
94
|
+
// 换了工作区/会话:先清掉上一个的内容,别在加载期间把旧会话的提示词当成当前的
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
setData(null);
|
|
97
|
+
}, [cwd, sessionId]);
|
|
98
|
+
|
|
90
99
|
useEffect(() => {
|
|
91
100
|
void load();
|
|
92
101
|
}, [load, refreshKey]);
|
|
@@ -165,19 +174,33 @@ export function PromptPanel({
|
|
|
165
174
|
{groups.map((group) => (
|
|
166
175
|
<section key={group.kind} className="mb-3">
|
|
167
176
|
<div className="mb-1 px-1" style={{ fontSize: 11.5, color: "var(--dsw-label-caption)" }}>{kindLabel(group.kind)}</div>
|
|
168
|
-
<div className="flex flex-col gap-1">
|
|
177
|
+
<div className="flex flex-col gap-1.5">
|
|
169
178
|
{group.items.map((source) => (
|
|
170
179
|
<button
|
|
171
180
|
key={`${source.kind}:${source.path ?? source.name}`}
|
|
172
181
|
type="button"
|
|
173
|
-
className="flex w-full items-center gap-2 rounded-
|
|
174
|
-
style={{
|
|
182
|
+
className="flex w-full items-center gap-2 rounded-xl px-2.5 py-2 text-left transition-all"
|
|
183
|
+
style={{
|
|
184
|
+
background: opening === source.path ? "var(--dsw-active)" : "var(--dsw-bg-elevated)",
|
|
185
|
+
border: "1px solid var(--dsw-border-l2)",
|
|
186
|
+
transition: "all 0.2s var(--isx-ease)",
|
|
187
|
+
}}
|
|
175
188
|
title={source.path ?? source.name}
|
|
189
|
+
onMouseEnter={(e) => {
|
|
190
|
+
e.currentTarget.style.borderColor = "var(--dsw-border-l3)";
|
|
191
|
+
e.currentTarget.style.background = "var(--dsw-hover)";
|
|
192
|
+
e.currentTarget.style.transform = "translateY(-1px)";
|
|
193
|
+
}}
|
|
194
|
+
onMouseLeave={(e) => {
|
|
195
|
+
e.currentTarget.style.borderColor = "var(--dsw-border-l2)";
|
|
196
|
+
e.currentTarget.style.background = opening === source.path ? "var(--dsw-active)" : "var(--dsw-bg-elevated)";
|
|
197
|
+
e.currentTarget.style.transform = "none";
|
|
198
|
+
}}
|
|
176
199
|
onClick={() => void openFile(source)}
|
|
177
200
|
>
|
|
178
|
-
<span className="min-w-0 flex-1 truncate" style={{ color: "var(--dsw-label-primary)" }}>{source.name}</span>
|
|
179
|
-
<span className="flex-none" style={{ fontSize: 10.5, color: "var(--dsw-label-tertiary)" }}>{formatBytes(source.bytes)}</span>
|
|
180
|
-
<span className="flex-none rounded-
|
|
201
|
+
<span className="min-w-0 flex-1 truncate font-medium" style={{ color: "var(--dsw-label-primary)" }}>{source.name}</span>
|
|
202
|
+
<span className="flex-none font-mono" style={{ fontSize: 10.5, color: "var(--dsw-label-tertiary)" }}>{formatBytes(source.bytes)}</span>
|
|
203
|
+
<span className="flex-none rounded-full px-2 py-0.5" style={{ fontSize: 10.5, border: "1px solid var(--dsw-border-l2)", color: "var(--dsw-label-tertiary)", background: "var(--dsw-bg-base)" }}>
|
|
181
204
|
{originLabel(source.origin)}
|
|
182
205
|
</span>
|
|
183
206
|
</button>
|
|
@@ -189,14 +212,28 @@ export function PromptPanel({
|
|
|
189
212
|
{hasTools && (
|
|
190
213
|
<section className="mb-3">
|
|
191
214
|
<div className="mb-1 px-1" style={{ fontSize: 11.5, color: "var(--dsw-label-caption)" }}>{t.promptKindTool}</div>
|
|
192
|
-
<div className="flex flex-col gap-1">
|
|
215
|
+
<div className="flex flex-col gap-1.5">
|
|
193
216
|
{data!.tools!.map((tool) => (
|
|
194
217
|
<button
|
|
195
218
|
key={`tool:${tool.name}`}
|
|
196
219
|
type="button"
|
|
197
|
-
className="flex w-full items-center gap-2 rounded-
|
|
198
|
-
style={{
|
|
220
|
+
className="flex w-full items-center gap-2 rounded-xl px-2.5 py-2 text-left transition-all"
|
|
221
|
+
style={{
|
|
222
|
+
border: "1px solid var(--dsw-border-l2)",
|
|
223
|
+
background: "var(--dsw-bg-elevated)",
|
|
224
|
+
transition: "all 0.2s var(--isx-ease)",
|
|
225
|
+
}}
|
|
199
226
|
title={tool.name}
|
|
227
|
+
onMouseEnter={(e) => {
|
|
228
|
+
e.currentTarget.style.borderColor = "var(--dsw-border-l3)";
|
|
229
|
+
e.currentTarget.style.background = "var(--dsw-hover)";
|
|
230
|
+
e.currentTarget.style.transform = "translateY(-1px)";
|
|
231
|
+
}}
|
|
232
|
+
onMouseLeave={(e) => {
|
|
233
|
+
e.currentTarget.style.borderColor = "var(--dsw-border-l2)";
|
|
234
|
+
e.currentTarget.style.background = "var(--dsw-bg-elevated)";
|
|
235
|
+
e.currentTarget.style.transform = "none";
|
|
236
|
+
}}
|
|
200
237
|
onClick={() =>
|
|
201
238
|
onOpenContent?.(
|
|
202
239
|
`tool-${tool.name}.md`,
|
|
@@ -212,8 +249,8 @@ export function PromptPanel({
|
|
|
212
249
|
)
|
|
213
250
|
}
|
|
214
251
|
>
|
|
215
|
-
<span className="min-w-0 flex-1 truncate" style={{ color: "var(--dsw-label-primary)" }}>{tool.name}</span>
|
|
216
|
-
<span className="flex-none rounded-
|
|
252
|
+
<span className="min-w-0 flex-1 truncate font-mono text-[12px] font-semibold" style={{ color: "var(--dsw-label-primary)" }}>{tool.name}</span>
|
|
253
|
+
<span className="flex-none rounded-full px-2 py-0.5" style={{ fontSize: 10.5, border: "1px solid var(--dsw-border-l2)", color: "var(--dsw-label-tertiary)", background: "var(--dsw-bg-base)" }}>
|
|
217
254
|
{tool.source === "builtin" ? t.promptToolBuiltin : t.promptToolExtension}
|
|
218
255
|
</span>
|
|
219
256
|
</button>
|
|
@@ -227,12 +264,26 @@ export function PromptPanel({
|
|
|
227
264
|
<div className="mb-1 px-1" style={{ fontSize: 11.5, color: "var(--dsw-label-caption)" }}>{t.promptKindAssembled}</div>
|
|
228
265
|
<button
|
|
229
266
|
type="button"
|
|
230
|
-
className="w-full rounded-
|
|
231
|
-
style={{
|
|
267
|
+
className="w-full rounded-xl px-2.5 py-2 text-left transition-all"
|
|
268
|
+
style={{
|
|
269
|
+
border: "1px solid var(--dsw-border-l2)",
|
|
270
|
+
background: "var(--dsw-bg-elevated)",
|
|
271
|
+
transition: "all 0.2s var(--isx-ease)",
|
|
272
|
+
}}
|
|
232
273
|
title={t.promptAssembledKey}
|
|
274
|
+
onMouseEnter={(e) => {
|
|
275
|
+
e.currentTarget.style.borderColor = "var(--dsw-border-l3)";
|
|
276
|
+
e.currentTarget.style.background = "var(--dsw-hover)";
|
|
277
|
+
e.currentTarget.style.transform = "translateY(-1px)";
|
|
278
|
+
}}
|
|
279
|
+
onMouseLeave={(e) => {
|
|
280
|
+
e.currentTarget.style.borderColor = "var(--dsw-border-l2)";
|
|
281
|
+
e.currentTarget.style.background = "var(--dsw-bg-elevated)";
|
|
282
|
+
e.currentTarget.style.transform = "none";
|
|
283
|
+
}}
|
|
233
284
|
onClick={() => onOpenContent?.("system-prompt.md", data.assembledSystemPrompt ?? "")}
|
|
234
285
|
>
|
|
235
|
-
<span className="truncate" style={{ color: "var(--dsw-label-primary)" }}>{t.promptAssembledKey}</span>
|
|
286
|
+
<span className="truncate font-medium" style={{ color: "var(--dsw-label-primary)" }}>{t.promptAssembledKey}</span>
|
|
236
287
|
</button>
|
|
237
288
|
</section>
|
|
238
289
|
)}
|
|
@@ -242,12 +293,26 @@ export function PromptPanel({
|
|
|
242
293
|
<div className="mb-1 px-1" style={{ fontSize: 11.5, color: "var(--dsw-label-caption)" }}>{t.promptKindCompacted}</div>
|
|
243
294
|
<button
|
|
244
295
|
type="button"
|
|
245
|
-
className="w-full rounded-
|
|
246
|
-
style={{
|
|
296
|
+
className="w-full rounded-xl px-2.5 py-2 text-left transition-all"
|
|
297
|
+
style={{
|
|
298
|
+
border: "1px solid var(--dsw-border-l2)",
|
|
299
|
+
background: "var(--dsw-bg-elevated)",
|
|
300
|
+
transition: "all 0.2s var(--isx-ease)",
|
|
301
|
+
}}
|
|
247
302
|
title={t.promptCompactedKey}
|
|
303
|
+
onMouseEnter={(e) => {
|
|
304
|
+
e.currentTarget.style.borderColor = "var(--dsw-border-l3)";
|
|
305
|
+
e.currentTarget.style.background = "var(--dsw-hover)";
|
|
306
|
+
e.currentTarget.style.transform = "translateY(-1px)";
|
|
307
|
+
}}
|
|
308
|
+
onMouseLeave={(e) => {
|
|
309
|
+
e.currentTarget.style.borderColor = "var(--dsw-border-l2)";
|
|
310
|
+
e.currentTarget.style.background = "var(--dsw-bg-elevated)";
|
|
311
|
+
e.currentTarget.style.transform = "none";
|
|
312
|
+
}}
|
|
248
313
|
onClick={() => onOpenContent?.("compacted-summary.md", data.compactedSummary ?? "")}
|
|
249
314
|
>
|
|
250
|
-
<span className="truncate" style={{ color: "var(--dsw-label-primary)" }}>{t.promptCompactedKey}</span>
|
|
315
|
+
<span className="truncate font-medium" style={{ color: "var(--dsw-label-primary)" }}>{t.promptCompactedKey}</span>
|
|
251
316
|
</button>
|
|
252
317
|
</section>
|
|
253
318
|
)}
|
|
@@ -278,17 +278,6 @@
|
|
|
278
278
|
gap: 12px;
|
|
279
279
|
}
|
|
280
280
|
|
|
281
|
-
.allowPrivate {
|
|
282
|
-
display: flex;
|
|
283
|
-
align-items: center;
|
|
284
|
-
gap: 4px;
|
|
285
|
-
font-size: 11.5px;
|
|
286
|
-
color: var(--dsw-label-caption);
|
|
287
|
-
cursor: pointer;
|
|
288
|
-
user-select: none;
|
|
289
|
-
white-space: nowrap;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
281
|
.modelEmpty {
|
|
293
282
|
margin-top: 13px;
|
|
294
283
|
padding: 17px;
|
|
@@ -307,6 +296,18 @@
|
|
|
307
296
|
margin-top: 13px;
|
|
308
297
|
}
|
|
309
298
|
|
|
299
|
+
/* 自定义请求头:名字 | 值 | 删除 */
|
|
300
|
+
.headerRow {
|
|
301
|
+
display: grid;
|
|
302
|
+
grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr) 34px;
|
|
303
|
+
gap: 8px;
|
|
304
|
+
align-items: center;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
.headerRow + .headerRow {
|
|
308
|
+
margin-top: 8px;
|
|
309
|
+
}
|
|
310
|
+
|
|
310
311
|
.modelRow {
|
|
311
312
|
display: grid;
|
|
312
313
|
/* 折叠箭头 | 模型 ID | 名称 | 删除 */
|
|
@@ -10,10 +10,11 @@ import {
|
|
|
10
10
|
IconTrashOutline16,
|
|
11
11
|
} from "@/components/icons";
|
|
12
12
|
import { ProviderBrand } from "@/components/ProviderBrand";
|
|
13
|
+
import { CustomSelect } from "@/components/CustomSelect";
|
|
13
14
|
import { useI18n } from "@/i18n";
|
|
14
15
|
import { looksPrivateEndpoint } from "@/lib/model-endpoint";
|
|
15
16
|
import { CUSTOM_PROVIDER_ID_PATTERN, customApiOptions } from "@/lib/provider-display";
|
|
16
|
-
import { formatCapacity, modelDraftFromCatalog, parseCapacity, serializeModelDraft, serializeProviderDraft, validateModelDrafts, type ModelDraft } from "@/lib/model-draft";
|
|
17
|
+
import { formatCapacity, modelDraftFromCatalog, parseCapacity, serializeModelDraft, serializeProviderDraft, validateHeaderEntries, validateModelDrafts, type ModelDraft } from "@/lib/model-draft";
|
|
17
18
|
import type { ProviderView } from "@/lib/models-service";
|
|
18
19
|
import styles from "./ProviderSetupModal.module.css";
|
|
19
20
|
|
|
@@ -66,6 +67,10 @@ export function ProviderSetupModal({
|
|
|
66
67
|
const allApis = customApiOptions(providers.flatMap((provider) => provider.apis));
|
|
67
68
|
const [api, setApi] = useState(allApis.includes("openai-completions") ? "openai-completions" : (allApis[0] ?? "openai-completions"));
|
|
68
69
|
const [models, setModels] = useState<ModelDraft[]>([]);
|
|
70
|
+
// 自定义请求头(models.json 的 headers):只对自定义 provider 开放。
|
|
71
|
+
// null = 尚未载入(不表态,保存时不碰原文件里的 headers);对象 = 用户编辑中。
|
|
72
|
+
// 载入时把源里的头读进表单,保存时整体替换。
|
|
73
|
+
const [headers, setHeaders] = useState<Array<{ name: string; value: string }> | null>(null);
|
|
69
74
|
// 自定义 provider 的自动识别只在地址 / 密钥输入完(失焦)或换协议时触发:
|
|
70
75
|
// 还没有任何模型行,且 URL(+ 密钥,本机网关可省)都填了,这次失焦才算数
|
|
71
76
|
const autoReady = models.length === 0 && Boolean(baseUrl.trim()) && Boolean(apiKey.trim() || looksPrivateEndpoint(baseUrl));
|
|
@@ -131,6 +136,8 @@ export function ProviderSetupModal({
|
|
|
131
136
|
result = await onSaveBuiltin({ providerId: selected.id, apiKey: apiKey.trim(), config });
|
|
132
137
|
}
|
|
133
138
|
} else {
|
|
139
|
+
// 表单里非空的头才写下去;一条都没有就不带这个键,免得留个空对象在文件里
|
|
140
|
+
const headerEntries = (headers ?? []).map(({ name, value }) => [name.trim(), value.trim()] as [string, string]).filter(([name, value]) => name && value);
|
|
134
141
|
result = await onSaveCustom({
|
|
135
142
|
providerId: normalizedId,
|
|
136
143
|
config: {
|
|
@@ -138,6 +145,7 @@ export function ProviderSetupModal({
|
|
|
138
145
|
baseUrl: baseUrl.trim(),
|
|
139
146
|
api,
|
|
140
147
|
...(apiKey.trim() ? { apiKey: apiKey.trim() } : {}),
|
|
148
|
+
...(headerEntries.length ? { headers: Object.fromEntries(headerEntries) } : {}),
|
|
141
149
|
models: models.map((model) => serializeModelDraft(model)),
|
|
142
150
|
},
|
|
143
151
|
});
|
|
@@ -155,11 +163,23 @@ export function ProviderSetupModal({
|
|
|
155
163
|
/** 供 ModelCatalog 的候选对话框调用:用表单当前值(含未保存的 key)探测远端目录 */
|
|
156
164
|
const discoverAvailableModels = async (allowPrivate: boolean): Promise<ModelDraft[]> => {
|
|
157
165
|
if (!baseUrl.trim()) throw new Error(t.apiAddress);
|
|
166
|
+
// 自定义头要跟着发现请求一起发:网关要求自定义头时,不带就永远拿不到模型目录
|
|
167
|
+
const headerEntries = (headers ?? []).map(({ name, value }) => [name.trim(), value.trim()] as [string, string]).filter(([name, value]) => name && value);
|
|
168
|
+
const headerError = validateHeaderEntries(headerEntries);
|
|
169
|
+
if (headerError) throw new Error(headerError);
|
|
158
170
|
const response = await fetch("/api/models", {
|
|
159
171
|
method: "POST",
|
|
160
172
|
headers: { "Content-Type": "application/json" },
|
|
161
173
|
// builtin 模式带 providerId:表单 key 为空时后端回退到已存密钥 / OAuth 令牌
|
|
162
|
-
body: JSON.stringify({
|
|
174
|
+
body: JSON.stringify({
|
|
175
|
+
action: "discoverModels",
|
|
176
|
+
baseUrl: baseUrl.trim(),
|
|
177
|
+
api: mode === "builtin" ? selected?.apis[0] : api,
|
|
178
|
+
apiKey: apiKey.trim(),
|
|
179
|
+
providerId: mode === "builtin" ? selected?.id : undefined,
|
|
180
|
+
allowPrivate,
|
|
181
|
+
...(headerEntries.length && mode !== "builtin" ? { headers: Object.fromEntries(headerEntries) } : {}),
|
|
182
|
+
}),
|
|
163
183
|
});
|
|
164
184
|
const result = await response.json();
|
|
165
185
|
if (!response.ok || !result.success) throw new Error(result.error || t.toastError);
|
|
@@ -238,10 +258,15 @@ export function ProviderSetupModal({
|
|
|
238
258
|
{selected?.authTypes.includes("oauth") && onOAuthLogin ? (
|
|
239
259
|
<div className={styles.field}>
|
|
240
260
|
<label className={styles.label}>{t.authMethod}</label>
|
|
241
|
-
<
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
261
|
+
<CustomSelect
|
|
262
|
+
value={authMethod}
|
|
263
|
+
onChange={(val) => setAuthMethod(val as "api_key" | "oauth")}
|
|
264
|
+
options={[
|
|
265
|
+
{ value: "api_key", label: t.authMethodApiKey },
|
|
266
|
+
{ value: "oauth", label: t.oauthLogin },
|
|
267
|
+
]}
|
|
268
|
+
aria-label={t.authMethod}
|
|
269
|
+
/>
|
|
245
270
|
</div>
|
|
246
271
|
) : null}
|
|
247
272
|
|
|
@@ -287,22 +312,58 @@ export function ProviderSetupModal({
|
|
|
287
312
|
</div>
|
|
288
313
|
<div className={styles.field}>
|
|
289
314
|
<label className={styles.label}>{t.apiProtocol}</label>
|
|
290
|
-
<
|
|
291
|
-
|
|
292
|
-
|
|
315
|
+
<CustomSelect
|
|
316
|
+
value={api}
|
|
317
|
+
onChange={(val) => { setApi(val); requestAutoDiscover(); }}
|
|
318
|
+
options={allApis.map((value) => ({ value, label: value }))}
|
|
319
|
+
aria-label={t.apiProtocol}
|
|
320
|
+
/>
|
|
293
321
|
</div>
|
|
294
322
|
<div className={styles.field}>
|
|
295
323
|
<label className={styles.label}>{t.apiKey}</label>
|
|
296
324
|
<input className={styles.input} type="password" value={apiKey} onChange={(event) => setApiKey(event.target.value)} onBlur={requestAutoDiscover} placeholder={t.apiKeyOptional} />
|
|
297
325
|
<div className={styles.hint}>{t.customApiKeyHint}</div>
|
|
298
326
|
</div>
|
|
327
|
+
{/* 自定义请求头:网关常要求额外认证头,探测模型目录与发消息都会带上 */}
|
|
328
|
+
<div className={styles.field}>
|
|
329
|
+
<label className={styles.label}>{t.customHeaders}</label>
|
|
330
|
+
{(headers ?? []).map((entry, index) => (
|
|
331
|
+
<div key={index} className={styles.headerRow}>
|
|
332
|
+
<input
|
|
333
|
+
className={styles.input}
|
|
334
|
+
value={entry.name}
|
|
335
|
+
placeholder={t.customHeaderName}
|
|
336
|
+
onChange={(event) => setHeaders((current) => (current ?? []).map((item, i) => (i === index ? { ...item, name: event.target.value } : item)))}
|
|
337
|
+
/>
|
|
338
|
+
<input
|
|
339
|
+
className={styles.input}
|
|
340
|
+
value={entry.value}
|
|
341
|
+
placeholder={t.customHeaderValue}
|
|
342
|
+
onChange={(event) => setHeaders((current) => (current ?? []).map((item, i) => (i === index ? { ...item, value: event.target.value } : item)))}
|
|
343
|
+
/>
|
|
344
|
+
<button
|
|
345
|
+
type="button"
|
|
346
|
+
className={styles.removeModel}
|
|
347
|
+
title={t.delete}
|
|
348
|
+
aria-label={`${t.delete} ${entry.name || index + 1}`}
|
|
349
|
+
onClick={() => setHeaders((current) => (current ?? []).filter((_, i) => i !== index))}
|
|
350
|
+
>
|
|
351
|
+
<IconTrashOutline16 size={15} />
|
|
352
|
+
</button>
|
|
353
|
+
</div>
|
|
354
|
+
))}
|
|
355
|
+
<button type="button" className={styles.addModel} onClick={() => setHeaders((current) => [...(current ?? []), { name: "", value: "" }])}>
|
|
356
|
+
<IconPlusOutline16 size={14} /> {t.customHeaderAdd}
|
|
357
|
+
</button>
|
|
358
|
+
<div className={styles.hint}>{t.customHeadersHint}</div>
|
|
359
|
+
</div>
|
|
299
360
|
<ModelCatalog
|
|
300
361
|
models={models}
|
|
301
362
|
setModels={setModels}
|
|
302
363
|
updateModel={updateModel}
|
|
303
364
|
discover={discoverAvailableModels}
|
|
304
365
|
discoverDisabled={!baseUrl.trim()}
|
|
305
|
-
autoDiscover={autoReady ? { signature: `${baseUrl.trim()}|${api}|${apiKey.trim()}`,
|
|
366
|
+
autoDiscover={autoReady ? { signature: `${baseUrl.trim()}|${api}|${apiKey.trim()}`, trigger: autoTrigger } : undefined}
|
|
306
367
|
/>
|
|
307
368
|
</>
|
|
308
369
|
)}
|
|
@@ -345,21 +406,19 @@ export function ModelCatalog({
|
|
|
345
406
|
inheritedCount?: number;
|
|
346
407
|
/** 内置 provider 的目录模型 ID:候选里已在目录中的不能勾选(本来就能选),同 ID 的行只覆盖填写的字段 */
|
|
347
408
|
catalogIds?: ReadonlySet<string>;
|
|
348
|
-
/**
|
|
409
|
+
/** 用表单当前值探测远端目录;本机 / 私网地址一律放行(服务端仍会自己再判一次);抛错时错误显示在目录内 */
|
|
349
410
|
discover?: (allowPrivate: boolean) => Promise<ModelDraft[]>;
|
|
350
411
|
discoverDisabled?: boolean;
|
|
351
412
|
/**
|
|
352
413
|
* 自动识别:trigger 变了(地址 / 密钥失焦、换协议)就按当前签名(URL + 协议 + 密钥)拉一次模型目录。
|
|
353
414
|
* 只在「还没有任何模型行」时给出;同一签名只跑一次,清空模型行后不会自己又加回来。
|
|
354
415
|
*/
|
|
355
|
-
autoDiscover?: { signature: string;
|
|
416
|
+
autoDiscover?: { signature: string; trigger: number };
|
|
356
417
|
}) {
|
|
357
418
|
const { t } = useI18n();
|
|
358
419
|
const [expanded, setExpanded] = useState<ReadonlySet<number>>(new Set());
|
|
359
420
|
const [discovering, setDiscovering] = useState(false);
|
|
360
421
|
const [discoverError, setDiscoverError] = useState("");
|
|
361
|
-
// 本地网关(Ollama 等)在私网/回环地址上,探测需要用户显式授权
|
|
362
|
-
const [allowPrivate, setAllowPrivate] = useState(false);
|
|
363
422
|
// 候选对话框:null = 关闭
|
|
364
423
|
const [candidates, setCandidates] = useState<ModelDraft[] | null>(null);
|
|
365
424
|
const [picked, setPicked] = useState<ReadonlySet<string>>(new Set());
|
|
@@ -434,8 +493,8 @@ export function ModelCatalog({
|
|
|
434
493
|
setAutoStatus(status);
|
|
435
494
|
return true;
|
|
436
495
|
};
|
|
437
|
-
//
|
|
438
|
-
discover(
|
|
496
|
+
// 本机/私网地址一并放行:探测模型目录本来就是本机使用,服务端仍会自己再判一次
|
|
497
|
+
discover(true).then(
|
|
439
498
|
(found) => {
|
|
440
499
|
if (!settle(found.length ? "done" : "empty")) return;
|
|
441
500
|
setAutoFound(found.length);
|
|
@@ -460,7 +519,7 @@ export function ModelCatalog({
|
|
|
460
519
|
setDiscovering(true);
|
|
461
520
|
setDiscoverError("");
|
|
462
521
|
try {
|
|
463
|
-
const found = await discover(
|
|
522
|
+
const found = await discover(true);
|
|
464
523
|
if (!found.length) {
|
|
465
524
|
setDiscoverError(t.discoverEmpty);
|
|
466
525
|
return;
|
|
@@ -527,14 +586,6 @@ export function ModelCatalog({
|
|
|
527
586
|
</div>
|
|
528
587
|
{discover ? (
|
|
529
588
|
<div className={styles.discoverControls}>
|
|
530
|
-
<label className={styles.allowPrivate} title={t.allowPrivateHint}>
|
|
531
|
-
<input
|
|
532
|
-
type="checkbox"
|
|
533
|
-
checked={allowPrivate}
|
|
534
|
-
onChange={(event) => setAllowPrivate(event.target.checked)}
|
|
535
|
-
/>
|
|
536
|
-
{t.allowPrivateDiscovery}
|
|
537
|
-
</label>
|
|
538
589
|
<button className={styles.discoverButton} disabled={discoverDisabled || discovering} onClick={() => void startDiscover()}>
|
|
539
590
|
{discovering ? t.discoveringModels : t.discoverModels}
|
|
540
591
|
</button>
|
|
@@ -592,22 +643,42 @@ export function ModelCatalog({
|
|
|
592
643
|
placeholder="16K"
|
|
593
644
|
/>
|
|
594
645
|
</label>
|
|
595
|
-
<
|
|
646
|
+
<div className={styles.capacityField} title={t.visionHint}>
|
|
596
647
|
<span>{t.visionLabel}</span>
|
|
597
|
-
<
|
|
598
|
-
|
|
648
|
+
<CustomSelect
|
|
649
|
+
size="sm"
|
|
650
|
+
style={{ width: 104 }}
|
|
599
651
|
value={model.vision === undefined ? "unset" : model.vision ? "image" : "text"}
|
|
600
|
-
onChange={(
|
|
601
|
-
const choice = event.target.value;
|
|
652
|
+
onChange={(choice) => {
|
|
602
653
|
// 未声明 = 不写 input:pi 按纯文本处理,体检卡片会列出来提醒
|
|
603
654
|
updateModel(index, { vision: choice === "unset" ? undefined : choice === "image" });
|
|
604
655
|
}}
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
656
|
+
options={[
|
|
657
|
+
{ value: "unset", label: t.visionUnset },
|
|
658
|
+
{ value: "image", label: t.visionOn },
|
|
659
|
+
{ value: "text", label: t.visionOff },
|
|
660
|
+
]}
|
|
661
|
+
aria-label={t.visionLabel}
|
|
662
|
+
/>
|
|
663
|
+
</div>
|
|
664
|
+
<div className={styles.capacityField} title={t.reasoningHint}>
|
|
665
|
+
<span>{t.reasoningLabel}</span>
|
|
666
|
+
<CustomSelect
|
|
667
|
+
size="sm"
|
|
668
|
+
style={{ width: 104 }}
|
|
669
|
+
value={model.reasoning === undefined ? "unset" : model.reasoning ? "on" : "off"}
|
|
670
|
+
onChange={(choice) => {
|
|
671
|
+
// 未声明 = 不写 reasoning:pi 按 false 处理,思考档位会消失,体检卡片会提醒
|
|
672
|
+
updateModel(index, { reasoning: choice === "unset" ? undefined : choice === "on" });
|
|
673
|
+
}}
|
|
674
|
+
options={[
|
|
675
|
+
{ value: "unset", label: t.reasoningUnset },
|
|
676
|
+
{ value: "on", label: t.reasoningOn },
|
|
677
|
+
{ value: "off", label: t.reasoningOff },
|
|
678
|
+
]}
|
|
679
|
+
aria-label={t.reasoningLabel}
|
|
680
|
+
/>
|
|
681
|
+
</div>
|
|
611
682
|
<span className={styles.capacityHint}>{t.capacityHint}</span>
|
|
612
683
|
</div>
|
|
613
684
|
) : null}
|
|
@@ -21,6 +21,8 @@ interface LanState {
|
|
|
21
21
|
running: boolean;
|
|
22
22
|
port: number | null;
|
|
23
23
|
urls: string[];
|
|
24
|
+
/** 是否在注入手机排版;插件关闭时为 false。旧服务端的响应里可能没有这个字段 */
|
|
25
|
+
styled?: boolean;
|
|
24
26
|
error: string | null;
|
|
25
27
|
}
|
|
26
28
|
|
|
@@ -35,6 +37,9 @@ interface PublicState {
|
|
|
35
37
|
|
|
36
38
|
interface RemoteStatus {
|
|
37
39
|
plugin: string;
|
|
40
|
+
/** 内置插件总开关;关闭后局域网入口仍在(纯转发),但没有手机排版与公网入口。
|
|
41
|
+
* 旧服务端的响应里可能没有这个字段,界面按「已启用」处理 */
|
|
42
|
+
pluginEnabled?: boolean;
|
|
38
43
|
targetPort: number;
|
|
39
44
|
/** 非空时服务端拒绝开任何入口(没开认证 / 开发模式) */
|
|
40
45
|
blocked: "auth" | "dev" | null;
|
|
@@ -181,12 +186,35 @@ export function RemoteSection() {
|
|
|
181
186
|
const lan = status?.lan;
|
|
182
187
|
const pub = status?.public;
|
|
183
188
|
const blocked = status?.blocked ?? null;
|
|
189
|
+
// 开关未取到状态前按「已启用」显示,避免首帧闪一下「已关闭」
|
|
190
|
+
const pluginOn = status?.pluginEnabled ?? true;
|
|
184
191
|
const lanTone: "on" | "off" = lan?.running ? "on" : "off";
|
|
185
192
|
const pubTone: "on" | "pending" | "off" = pub?.running ? (pub.ready ? "on" : "pending") : "off";
|
|
186
193
|
|
|
194
|
+
/** 切换内置插件总开关;局域网入口照旧(关掉后退化成纯转发),公网入口会一并停掉 */
|
|
195
|
+
const togglePlugin = async () => {
|
|
196
|
+
if (busy) return;
|
|
197
|
+
setBusy(true);
|
|
198
|
+
setError("");
|
|
199
|
+
try {
|
|
200
|
+
const response = await fetch("/api/remote", {
|
|
201
|
+
method: "POST",
|
|
202
|
+
headers: { "Content-Type": "application/json" },
|
|
203
|
+
body: JSON.stringify({ action: "set-plugin", enabled: !pluginOn }),
|
|
204
|
+
});
|
|
205
|
+
const result = await response.json();
|
|
206
|
+
if (result?.success) setStatus(result.data as RemoteStatus);
|
|
207
|
+
else setError(result?.error ?? t.remoteFailed);
|
|
208
|
+
} catch {
|
|
209
|
+
setError(t.remoteFailed);
|
|
210
|
+
} finally {
|
|
211
|
+
if (alive.current) setBusy(false);
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
|
|
187
215
|
return (
|
|
188
216
|
<div className="flex flex-col gap-6">
|
|
189
|
-
{/* 插件抬头 */}
|
|
217
|
+
{/* 插件抬头 + 总开关 */}
|
|
190
218
|
<div className="flex items-center gap-2.5">
|
|
191
219
|
<div style={{ fontSize: 15, fontWeight: 500 }}>{t.setRemote}</div>
|
|
192
220
|
<span
|
|
@@ -195,7 +223,24 @@ export function RemoteSection() {
|
|
|
195
223
|
>
|
|
196
224
|
{t.remoteBuiltin}
|
|
197
225
|
</span>
|
|
226
|
+
<div className="flex-1" />
|
|
227
|
+
<span style={{ fontSize: 12, color: "var(--dsw-label-caption)" }}>{pluginOn ? t.remotePluginOn : t.remotePluginOff}</span>
|
|
228
|
+
<button
|
|
229
|
+
type="button"
|
|
230
|
+
className="relative h-5 w-9 flex-none rounded-full transition-colors"
|
|
231
|
+
style={{ background: pluginOn ? "var(--dsw-accent)" : "var(--dsw-border-l3)" }}
|
|
232
|
+
role="switch"
|
|
233
|
+
aria-checked={pluginOn}
|
|
234
|
+
aria-label={t.remotePluginToggle}
|
|
235
|
+
title={t.remotePluginHint}
|
|
236
|
+
// 被拦住(没开认证 / 开发模式)时入口一律开不出来,开关拨了也没有意义
|
|
237
|
+
disabled={busy || !status || blocked !== null}
|
|
238
|
+
onClick={() => void togglePlugin()}
|
|
239
|
+
>
|
|
240
|
+
<span className="absolute top-0.5 h-4 w-4 rounded-full bg-white transition-all" style={{ left: pluginOn ? 18 : 2 }} />
|
|
241
|
+
</button>
|
|
198
242
|
</div>
|
|
243
|
+
<div style={{ fontSize: 12, color: "var(--dsw-label-secondary)", lineHeight: 1.6, marginTop: -12 }}>{t.remotePluginHint}</div>
|
|
199
244
|
|
|
200
245
|
{/* 局域网入口 */}
|
|
201
246
|
<Row title={t.remoteLanTitle} hint={t.remoteLanHint}>
|
|
@@ -205,6 +250,11 @@ export function RemoteSection() {
|
|
|
205
250
|
<span style={{ color: lan?.running ? "var(--dsw-label-primary)" : "var(--dsw-label-caption)" }}>
|
|
206
251
|
{lan?.running ? t.remoteRunning : t.remoteStopped}
|
|
207
252
|
</span>
|
|
253
|
+
{/* 只在服务端明确回了 styled:false 时才说「纯转发」:
|
|
254
|
+
字段缺失(旧服务端、缓存的响应)不能当成关闭,否则会误报 */}
|
|
255
|
+
{lan?.running && lan.styled === false ? (
|
|
256
|
+
<span style={{ fontSize: 11.5, color: "var(--dsw-label-caption)" }}>· {t.remoteLanPlain}</span>
|
|
257
|
+
) : null}
|
|
208
258
|
</div>
|
|
209
259
|
{(lan?.urls ?? []).map((url) => (
|
|
210
260
|
<EntryLink key={url} url={url} token={status?.loginToken} />
|
|
@@ -229,11 +279,12 @@ export function RemoteSection() {
|
|
|
229
279
|
{t.remoteStop}
|
|
230
280
|
</button>
|
|
231
281
|
) : (
|
|
232
|
-
<button className="btn-primary-white" style={{ height: 30, fontSize: 13 }} disabled={busy || blocked !== null} onClick={() => void act("start")}>
|
|
282
|
+
<button className="btn-primary-white" style={{ height: 30, fontSize: 13 }} disabled={busy || blocked !== null || !pluginOn} onClick={() => void act("start")}>
|
|
233
283
|
{busy ? t.remoteWorking : t.remoteStart}
|
|
234
284
|
</button>
|
|
235
285
|
)}
|
|
236
286
|
</div>
|
|
287
|
+
{!pluginOn ? <div style={{ fontSize: 11.5, color: "var(--dsw-label-caption)", textAlign: "right" }}>{t.remotePublicNeedsPlugin}</div> : null}
|
|
237
288
|
</div>
|
|
238
289
|
</Row>
|
|
239
290
|
|