@rexvane/piweb 0.3.20 → 0.3.22
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 +6 -4
- package/.next/build-manifest.json +5 -5
- package/.next/next-minimal-server.js.nft.json +1 -1
- package/.next/next-server.js.nft.json +1 -1
- package/.next/prerender-manifest.json +7 -7
- package/.next/react-loadable-manifest.json +12 -12
- package/.next/required-server-files.js +18 -2
- package/.next/required-server-files.json +18 -2
- package/.next/routes-manifest.json +12 -0
- package/.next/server/app/_global-error/page.js +2 -2
- package/.next/server/app/_global-error/page.js.nft.json +1 -1
- 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.meta +8 -0
- package/.next/server/app/_global-error.rsc +15 -11
- package/.next/server/app/_global-error.segments/_full.segment.rsc +15 -11
- package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +20 -16
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +3 -1
- package/.next/server/app/_not-found/page.js +1 -1
- package/.next/server/app/_not-found/page.js.nft.json +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.meta +8 -0
- package/.next/server/app/_not-found.rsc +17 -13
- package/.next/server/app/_not-found.segments/_full.segment.rsc +17 -13
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +24 -20
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +4 -2
- package/.next/server/app/api/agent/[id]/events/route.js +3 -3
- 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/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 +5 -0
- package/.next/server/app/api/github/route.js.nft.json +1 -0
- package/.next/server/app/api/github/route_client-reference-manifest.js +1 -0
- 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/health/route.js.nft.json +1 -1
- package/.next/server/app/api/models/defaults/route.js +1 -0
- package/.next/server/app/api/models/defaults/route.js.nft.json +1 -0
- package/.next/server/app/api/models/defaults/route_client-reference-manifest.js +1 -0
- 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 +3 -3
- package/.next/server/app/api/remote/route.js.nft.json +1 -1
- 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 +1 -1
- package/.next/server/app/api/web-auth/route.js.nft.json +1 -1
- 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/icon.svg/route.js.nft.json +1 -1
- package/.next/server/app/icon.svg.meta +1 -1
- package/.next/server/app/index.html +1 -1
- package/.next/server/app/index.meta +8 -0
- package/.next/server/app/index.rsc +19 -13
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +25 -19
- package/.next/server/app/index.segments/_full.segment.rsc +19 -13
- package/.next/server/app/index.segments/_tree.segment.rsc +3 -2
- package/.next/server/app/login/page.js +1 -1
- package/.next/server/app/login/page.js.nft.json +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.meta +8 -0
- package/.next/server/app/login.rsc +23 -17
- package/.next/server/app/login.segments/_full.segment.rsc +23 -17
- package/.next/server/app/login.segments/_tree.segment.rsc +4 -2
- package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +30 -24
- 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 +6 -4
- package/.next/server/chunks/229.js +1 -1
- package/.next/server/chunks/421.js +1 -0
- package/.next/server/chunks/430.js +215 -31
- package/.next/server/chunks/496.js +1 -1
- package/.next/server/chunks/538.js +1 -1
- package/.next/server/chunks/662.js +2 -0
- package/.next/server/chunks/686.js +1 -1
- package/.next/server/chunks/807.js +1 -1
- package/.next/server/chunks/813.js +23 -1
- package/.next/server/chunks/920.js +2 -2
- package/.next/server/chunks/924.js +1 -1
- package/.next/server/chunks/970.js +305 -0
- package/.next/server/instrumentation.js +1 -1
- 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/middleware.js +185 -23
- package/.next/server/middleware.js.nft.json +1 -1
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/preview-props.json +5 -0
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/WMazYvD-gbeLsCUfUxvvM/_buildManifest.js +1 -0
- package/.next/static/chunks/316-698d10eb465bd8ef.js +1 -0
- package/.next/static/chunks/3794-0978865cda773008.js +52 -0
- package/.next/static/chunks/4673.4cf66cbdd8364c57.js +1 -0
- package/.next/static/chunks/4686-09fcd0028079006c.js +1 -0
- package/.next/static/chunks/4778.eddd3a5813f85824.js +1 -0
- package/.next/static/chunks/4bd1b696-e199e788e58acf79.js +1 -0
- package/.next/static/chunks/{5173.91ddb803785009f8.js → 5173.dfd1cbb795b9a21c.js} +1 -1
- package/.next/static/chunks/618.949981da99be7182.js +4 -0
- package/.next/static/chunks/7411.67366dc5032a141d.js +1 -0
- package/.next/static/chunks/8552.25c39ef4c995eba0.js +1 -0
- package/.next/static/chunks/app/_global-error/page-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/events/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/tree/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/agent/new/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/config/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/files/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/fs/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/git/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/github/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/growth/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/health/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/models/defaults/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/models/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/pi-settings/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/plugins/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/prompts/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/remote/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/security/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/export/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/sessions/import/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/sessions/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/skills/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/update/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/version/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/web-auth/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/workspaces/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/page-fc4ed2ff29b6235e.js +1 -0
- package/.next/static/chunks/framework-47b723d27f4a4963.js +1 -0
- package/.next/static/chunks/main-5403f0764579e5fe.js +5 -0
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/webpack-22ebffc20adb44ae.js +1 -0
- package/.next/static/css/6671d0d1589fdc0e.css +1 -0
- package/.next/static/css/{2959bd6b626b7ffe.css → f15a853af8b5127f.css} +1 -1
- package/.next/types/app/api/agent/[id]/events/route.ts +3 -1
- package/.next/types/app/api/agent/[id]/route.ts +3 -1
- package/.next/types/app/api/agent/[id]/tree/route.ts +3 -1
- package/.next/types/app/api/agent/new/route.ts +3 -1
- package/.next/types/app/api/config/route.ts +3 -1
- package/.next/types/app/api/files/route.ts +3 -1
- package/.next/types/app/api/fs/route.ts +3 -1
- package/.next/types/app/api/git/route.ts +3 -1
- package/.next/types/app/api/github/route.ts +355 -0
- package/.next/types/app/api/growth/route.ts +3 -1
- package/.next/types/app/api/health/route.ts +3 -1
- package/.next/types/app/api/models/defaults/route.ts +355 -0
- package/.next/types/app/api/models/route.ts +3 -1
- package/.next/types/app/api/pi-settings/route.ts +3 -1
- package/.next/types/app/api/plugins/route.ts +3 -1
- package/.next/types/app/api/prompts/route.ts +3 -1
- package/.next/types/app/api/remote/route.ts +3 -1
- package/.next/types/app/api/security/route.ts +3 -1
- package/.next/types/app/api/sessions/[id]/export/route.ts +3 -1
- package/.next/types/app/api/sessions/[id]/route.ts +3 -1
- package/.next/types/app/api/sessions/import/route.ts +3 -1
- package/.next/types/app/api/sessions/route.ts +3 -1
- package/.next/types/app/api/skills/route.ts +3 -1
- package/.next/types/app/api/update/route.ts +3 -1
- package/.next/types/app/api/version/route.ts +3 -1
- package/.next/types/app/api/web-auth/route.ts +3 -1
- package/.next/types/app/api/workspaces/route.ts +3 -1
- package/.next/types/app/layout.ts +4 -1
- package/.next/types/app/login/page.ts +4 -1
- package/.next/types/app/page.ts +4 -1
- package/.next/types/cache-life.d.ts +11 -43
- package/.next/types/routes.d.ts +3 -1
- package/.next/types/validator.ts +76 -2
- package/README.md +14 -5
- package/next.config.ts +4 -2
- package/package.json +2 -2
- package/scripts/release.mjs +1 -1
- package/src/app/api/agent/[id]/events/route.ts +42 -13
- package/src/app/api/files/route.ts +5 -2
- package/src/app/api/github/route.ts +26 -0
- package/src/app/api/models/defaults/route.ts +18 -0
- package/src/app/api/models/route.ts +24 -1
- package/src/app/api/update/route.ts +18 -2
- package/src/app/globals.css +275 -0
- package/src/components/AppShell.tsx +89 -33
- package/src/components/ChatInput.tsx +245 -63
- package/src/components/ChatWindow.tsx +123 -108
- package/src/components/ContextMeter.tsx +204 -123
- package/src/components/DrawerPanel.tsx +19 -26
- package/src/components/ExtensionUI.tsx +8 -2
- package/src/components/GitPanel.tsx +110 -40
- package/src/components/MarkdownBlock.tsx +333 -37
- package/src/components/ProjectPanel.tsx +55 -30
- package/src/components/PromptPanel.tsx +2 -4
- package/src/components/ProviderSetupModal.module.css +20 -0
- package/src/components/ProviderSetupModal.tsx +134 -29
- package/src/components/SettingsPanel.tsx +365 -80
- package/src/components/TrajectoryView.tsx +4 -6
- package/src/components/chat/chat-scroll.ts +18 -0
- package/src/components/composer/ComposerImages.tsx +306 -0
- package/src/components/update/CopyCommand.tsx +34 -0
- package/src/components/update/UpdateNotice.tsx +130 -0
- package/src/components/update/update-store.ts +155 -0
- package/src/hooks/useGrowth.ts +3 -0
- package/src/hooks/usePiWeb.ts +54 -27
- package/src/hooks/useWorkspaceFiles.ts +93 -0
- package/src/i18n.tsx +182 -6
- package/src/instrumentation-node.ts +3 -0
- package/src/lib/agent-manager.ts +193 -119
- package/src/lib/chat/event-stream.ts +113 -0
- package/src/lib/chat/stream-settings.ts +192 -0
- package/src/lib/files-service.ts +100 -1
- package/src/lib/format.ts +56 -0
- package/src/lib/git-service.ts +45 -13
- package/src/lib/github-identity.ts +245 -0
- package/src/lib/growth-service.ts +6 -2
- package/src/lib/hero-model-memory.ts +28 -0
- package/src/lib/model-defaults.ts +105 -0
- package/src/lib/model-draft.ts +257 -9
- package/src/lib/model-endpoint.ts +23 -0
- package/src/lib/model-files.ts +33 -0
- package/src/lib/models-service.ts +240 -78
- package/src/lib/pi.ts +27 -2
- package/src/lib/process-format.ts +3 -2
- package/src/lib/session/metrics.ts +31 -0
- package/src/lib/thinking-level.ts +39 -0
- package/src/lib/trajectory.ts +17 -3
- package/src/lib/types.ts +19 -1
- package/src/lib/update/startup-check.ts +117 -0
- package/src/lib/update-service.ts +23 -3
- package/src/lib/workspace-index.ts +270 -0
- package/.next/server/chunks/131.js +0 -1
- package/.next/server/chunks/243.js +0 -1
- package/.next/server/chunks/741.js +0 -165
- package/.next/static/_Y3Lq0lI5RJ1tFSOnDi0L/_buildManifest.js +0 -1
- package/.next/static/chunks/316-5ea443ee6b512aa9.js +0 -1
- package/.next/static/chunks/3794-3679f1c05160977c.js +0 -52
- package/.next/static/chunks/4673.8a5a9e6855350e34.js +0 -1
- package/.next/static/chunks/4686-b1e9fa876d0ea671.js +0 -1
- package/.next/static/chunks/4778.e4c9ed093a413bc4.js +0 -1
- package/.next/static/chunks/4bd1b696-93bf506b236d6248.js +0 -1
- package/.next/static/chunks/618.ee5d5bf1c647e483.js +0 -4
- package/.next/static/chunks/839.d3fe337a9117c325.js +0 -1
- package/.next/static/chunks/8552.6bfe3e101250b20f.js +0 -1
- package/.next/static/chunks/app/_global-error/page-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/events/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/tree/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/agent/new/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/config/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/files/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/fs/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/git/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/growth/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/health/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/models/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/pi-settings/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/plugins/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/prompts/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/remote/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/security/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/export/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/sessions/import/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/sessions/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/skills/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/update/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/version/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/web-auth/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/workspaces/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/page-2e425ef83fba0782.js +0 -1
- package/.next/static/chunks/framework-c4e651fe89edfa91.js +0 -1
- package/.next/static/chunks/main-6159b22a8cb0b7c5.js +0 -5
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/webpack-3614a8337843456b.js +0 -1
- package/.next/static/css/7391c42e1ae617f9.css +0 -1
- /package/.next/static/{_Y3Lq0lI5RJ1tFSOnDi0L → WMazYvD-gbeLsCUfUxvvM}/_ssgManifest.js +0 -0
package/src/lib/model-draft.ts
CHANGED
|
@@ -5,22 +5,83 @@ export interface ModelDraft {
|
|
|
5
5
|
name: string;
|
|
6
6
|
contextWindow?: number;
|
|
7
7
|
maxTokens?: number;
|
|
8
|
+
/** 输入模态里有 image(models.json 的 `input`)。undefined = 源里没写:pi 会当成只文本、把贴图换成占位文字 */
|
|
9
|
+
vision?: boolean;
|
|
8
10
|
source?: Readonly<Record<string, unknown>>;
|
|
11
|
+
/** 来自 modelOverrides:只覆盖内置模型的部分字段。缺省 = models[] 里的定义或新加的行 */
|
|
12
|
+
origin?: "override";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* models.json 里模型的 `input` 字段(pi 用它判断能不能收图)。
|
|
17
|
+
* 没写 → pi 按只文本处理,而且提示语会说是「模型不支持图像」,很容易被当成模型能力问题。
|
|
18
|
+
*/
|
|
19
|
+
export function modelInputFor(vision: boolean): string[] {
|
|
20
|
+
return vision ? ["text", "image"] : ["text"];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* pi 在 models[] 条目没写这些字段时用的值(provider-composer.js 的 modelFromJson:
|
|
25
|
+
* `contextWindow ?? 128000`、`maxTokens ?? 16384`、`input ?? ["text"]`)。只用来向用户解释「不填会被当成什么」。
|
|
26
|
+
*
|
|
27
|
+
* PiWeb 不替用户编容量:maxTokens 会原样当成请求的 max_tokens 发出去(pi-ai simple-options),
|
|
28
|
+
* contextWindow 决定 max_tokens 的钳制和自动压缩的时机——往大了猜,上游会直接拒绝请求;
|
|
29
|
+
* 往小了猜只是提前压缩。所以只写端点报的或用户填的值,缺的交给体检卡片提醒。
|
|
30
|
+
*/
|
|
31
|
+
export const PI_MODEL_FALLBACKS = { contextWindow: 128_000, maxTokens: 16_384 } as const;
|
|
32
|
+
|
|
33
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
34
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
9
35
|
}
|
|
10
36
|
|
|
11
37
|
export function modelDraftFromConfig(value: unknown): ModelDraft {
|
|
12
|
-
const source = value
|
|
13
|
-
|
|
14
|
-
:
|
|
38
|
+
const source = isRecord(value) ? value : { id: typeof value === "string" ? value : "" };
|
|
39
|
+
const input = Array.isArray(source.input)
|
|
40
|
+
? source.input.filter((item): item is string => typeof item === "string")
|
|
41
|
+
: undefined;
|
|
15
42
|
return {
|
|
16
43
|
id: String(source.id ?? ""),
|
|
17
44
|
name: typeof source.name === "string" ? source.name : "",
|
|
18
45
|
contextWindow: typeof source.contextWindow === "number" ? source.contextWindow : undefined,
|
|
19
46
|
maxTokens: typeof source.maxTokens === "number" ? source.maxTokens : undefined,
|
|
47
|
+
// 只有源里写了 input 才算「声明过」:没写 ≠ 只文本,而是「会被 pi 当只文本」
|
|
48
|
+
vision: input ? input.includes("image") : undefined,
|
|
20
49
|
source,
|
|
21
50
|
};
|
|
22
51
|
}
|
|
23
52
|
|
|
53
|
+
function positiveInt(value: unknown): number | undefined {
|
|
54
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** 「获取可用模型」返回的一条 → 表单行:端点报了容量与视觉就带上,没报的留空(不猜) */
|
|
58
|
+
export function modelDraftFromCatalog(entry: { id: string; name?: string; contextWindow?: number; maxTokens?: number; vision?: boolean }): ModelDraft {
|
|
59
|
+
const contextWindow = positiveInt(entry.contextWindow);
|
|
60
|
+
const maxTokens = positiveInt(entry.maxTokens);
|
|
61
|
+
return {
|
|
62
|
+
id: entry.id.trim(),
|
|
63
|
+
name: entry.name ?? "",
|
|
64
|
+
...(contextWindow === undefined ? {} : { contextWindow }),
|
|
65
|
+
...(maxTokens === undefined ? {} : { maxTokens }),
|
|
66
|
+
...(typeof entry.vision === "boolean" ? { vision: entry.vision } : {}),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** modelOverrides 的一条:键是模型 ID,值只含要覆盖的字段 */
|
|
71
|
+
export function modelDraftFromOverride(id: string, value: unknown): ModelDraft {
|
|
72
|
+
return { ...modelDraftFromConfig({ ...(isRecord(value) ? value : {}), id }), origin: "override" };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** 内置 provider 的 models.json 条目 → 表单行:models[] 的定义在前,modelOverrides 的覆盖在后 */
|
|
76
|
+
export function modelDraftsFromProviderConfig(config: unknown): ModelDraft[] {
|
|
77
|
+
const source = isRecord(config) ? config : {};
|
|
78
|
+
const definitions = Array.isArray(source.models) ? source.models.map(modelDraftFromConfig) : [];
|
|
79
|
+
const overrides = isRecord(source.modelOverrides)
|
|
80
|
+
? Object.entries(source.modelOverrides).map(([id, value]) => modelDraftFromOverride(id, value))
|
|
81
|
+
: [];
|
|
82
|
+
return [...definitions, ...overrides];
|
|
83
|
+
}
|
|
84
|
+
|
|
24
85
|
/** K/M use decimal units. Empty means the SDK default; NaN means invalid input. */
|
|
25
86
|
export function parseCapacity(text: string): number | undefined {
|
|
26
87
|
const value = text.trim().toLowerCase();
|
|
@@ -52,13 +113,16 @@ export function validateModelDrafts(models: ModelDraft[]): { index: number; reas
|
|
|
52
113
|
return undefined;
|
|
53
114
|
}
|
|
54
115
|
|
|
55
|
-
/**
|
|
116
|
+
/**
|
|
117
|
+
* Only edited form fields replace the source; unknown/nested configuration is opaque.
|
|
118
|
+
* 容量留空就不写(交给 pi 的缺省,见 PI_MODEL_FALLBACKS),不替用户编值。
|
|
119
|
+
*/
|
|
56
120
|
export function serializeModelDraft(model: Omit<ModelDraft, "name"> & { name?: string }): Record<string, unknown> {
|
|
57
121
|
const result: Record<string, unknown> = { ...model.source };
|
|
58
122
|
const original = model.source ? modelDraftFromConfig(model.source) : undefined;
|
|
59
123
|
for (const field of ["id", "name", "contextWindow", "maxTokens"] as const) {
|
|
60
|
-
if (original && Object.is(model[field], original[field])) continue;
|
|
61
124
|
const value = model[field];
|
|
125
|
+
if (original && Object.is(value, original[field])) continue;
|
|
62
126
|
if (field === "id" || field === "name") {
|
|
63
127
|
const text = typeof value === "string" ? value.trim() : "";
|
|
64
128
|
if (field === "id" || text) result[field] = text;
|
|
@@ -66,6 +130,15 @@ export function serializeModelDraft(model: Omit<ModelDraft, "name"> & { name?: s
|
|
|
66
130
|
} else if (typeof value === "number" && Number.isSafeInteger(value) && value > 0) result[field] = value;
|
|
67
131
|
else delete result[field];
|
|
68
132
|
}
|
|
133
|
+
// input 是数组:只在明确表态时写(端点报的或用户选的),不默认猜视觉
|
|
134
|
+
// (猜错会把图片发给不收图的上游;留空只是被 pi 当纯文本,体检卡片会提醒)
|
|
135
|
+
const vision = model.vision;
|
|
136
|
+
if (vision === undefined) {
|
|
137
|
+
// 源里声明过、现在选「未声明」= 明确取消这个声明(否则保留源值会让人取消不掉)
|
|
138
|
+
if (original?.vision !== undefined) delete result.input;
|
|
139
|
+
} else if (!Object.is(vision, original?.vision)) {
|
|
140
|
+
result.input = modelInputFor(vision);
|
|
141
|
+
}
|
|
69
142
|
return result;
|
|
70
143
|
}
|
|
71
144
|
|
|
@@ -82,6 +155,8 @@ export function serializeProviderDraft(
|
|
|
82
155
|
source: Readonly<Record<string, unknown>>,
|
|
83
156
|
draft: ProviderDraft,
|
|
84
157
|
defaultApi?: string,
|
|
158
|
+
/** 内置 provider 的目录模型 ID:给了就按 splitBuiltinModels 把覆盖已有模型的行写进 modelOverrides */
|
|
159
|
+
catalogIds?: ReadonlySet<string>,
|
|
85
160
|
): Record<string, unknown> {
|
|
86
161
|
const result: Record<string, unknown> = { ...source };
|
|
87
162
|
for (const field of ["baseUrl", "name", "api"] as const) {
|
|
@@ -91,13 +166,186 @@ export function serializeProviderDraft(
|
|
|
91
166
|
else delete result[field];
|
|
92
167
|
}
|
|
93
168
|
if (draft.apiKey?.trim()) result.apiKey = draft.apiKey.trim();
|
|
169
|
+
const { definitions, overrides } = catalogIds
|
|
170
|
+
? splitBuiltinModels(draft.models, catalogIds)
|
|
171
|
+
: { definitions: draft.models.map((model) => serializeModelDraft(model)), overrides: undefined };
|
|
94
172
|
// An explicit empty array removes the overrides. Do not merge rows back by ID.
|
|
95
|
-
if (
|
|
96
|
-
result.models =
|
|
173
|
+
if (definitions.length || Object.prototype.hasOwnProperty.call(source, "models")) {
|
|
174
|
+
result.models = definitions;
|
|
175
|
+
}
|
|
176
|
+
// 表单载入了全部 modelOverrides,这里整体替换;删光了就去掉这个键
|
|
177
|
+
if (overrides) {
|
|
178
|
+
if (Object.keys(overrides).length) result.modelOverrides = overrides;
|
|
179
|
+
else delete result.modelOverrides;
|
|
97
180
|
}
|
|
98
|
-
// Supply an API only for newly introduced builtin model
|
|
99
|
-
if (
|
|
181
|
+
// Supply an API only for newly introduced builtin model definitions, not existing rows.
|
|
182
|
+
if (definitions.length && source.models === undefined && result.api === undefined && defaultApi) {
|
|
100
183
|
result.api = defaultApi;
|
|
101
184
|
}
|
|
102
185
|
return result;
|
|
103
186
|
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* 内置 provider 的表单行分流。models[] 里与内置模型同 ID 的条目会被 SDK 当成新定义整条重建
|
|
190
|
+
* (reasoning / 上下文 / 价格 / 思考档位回到默认值,provider 级 api 还会顶掉模型自己的协议),
|
|
191
|
+
* 所以覆盖已有模型要写 modelOverrides:只写填了的字段,其余沿用内置值。
|
|
192
|
+
* - 自带 api / baseUrl 的 models[] 条目是手写的完整定义,原样留在 models[];
|
|
193
|
+
* - ID 在目录里,或来自 modelOverrides 且 ID 没改 → modelOverrides(没有要覆盖的字段就不写);
|
|
194
|
+
* - 其余是新模型 → models[]。
|
|
195
|
+
*/
|
|
196
|
+
function splitBuiltinModels(models: ModelDraft[], catalogIds: ReadonlySet<string>): {
|
|
197
|
+
definitions: Record<string, unknown>[];
|
|
198
|
+
overrides: Record<string, unknown>;
|
|
199
|
+
} {
|
|
200
|
+
const definitions: Record<string, unknown>[] = [];
|
|
201
|
+
const overrides: Array<[string, Record<string, unknown>]> = [];
|
|
202
|
+
for (const model of models) {
|
|
203
|
+
const id = model.id.trim();
|
|
204
|
+
const source = isRecord(model.source) ? model.source : undefined;
|
|
205
|
+
// 自带 api / baseUrl 的 models[] 条目是手写的完整定义,原样留在 models[]
|
|
206
|
+
const fullDefinition = model.origin !== "override" && (source?.api !== undefined || source?.baseUrl !== undefined);
|
|
207
|
+
const keptOverride = model.origin === "override" && source !== undefined && modelDraftFromConfig(source).id === id;
|
|
208
|
+
const asOverride = !fullDefinition && (catalogIds.has(id) || keptOverride);
|
|
209
|
+
const serialized = serializeModelDraft(model);
|
|
210
|
+
if (!asOverride) {
|
|
211
|
+
definitions.push(serialized);
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
const fields = { ...serialized };
|
|
215
|
+
delete fields.id;
|
|
216
|
+
if (Object.keys(fields).length) overrides.push([id, fields]);
|
|
217
|
+
}
|
|
218
|
+
// fromEntries 按数据属性写入,"__proto__" 这类 ID 也不会改原型
|
|
219
|
+
return { definitions, overrides: Object.fromEntries(overrides) };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** 条目里只剩这些字段时,SDK 不认它(见 isInertProviderConfig) */
|
|
223
|
+
const INERT_PROVIDER_FIELDS = new Set(["api", "name", "models", "modelOverrides"]);
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* 内置 provider 的 models.json 条目是否「什么都没覆盖」。SDK 要求条目至少有非空的 models / modelOverrides、
|
|
227
|
+
* baseUrl、headers、compat、apiKey、oauth、authHeader 之一,否则报错、整份 models.json 存不进去;
|
|
228
|
+
* 只剩 api / name 这类只对 models[] 有用的字段时,删掉条目才等于「全用内置默认」。
|
|
229
|
+
* 有不认识的字段就交给 SDK 校验,不替用户删。注意前端拿到的配置已去掉 apiKey,调用方要另外排除存了密钥的条目。
|
|
230
|
+
*/
|
|
231
|
+
export function isInertProviderConfig(config: Readonly<Record<string, unknown>>): boolean {
|
|
232
|
+
if (Array.isArray(config.models) && config.models.length) return false;
|
|
233
|
+
if (isRecord(config.modelOverrides) && Object.keys(config.modelOverrides).length) return false;
|
|
234
|
+
return Object.keys(config).every((field) => INERT_PROVIDER_FIELDS.has(field));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export type CustomModelField = "contextWindow" | "maxTokens" | "input";
|
|
238
|
+
|
|
239
|
+
/** 体检的一条:models[] 里有字段没写的模型(已写的值一并带上,界面要显示) */
|
|
240
|
+
export interface CustomModelGap {
|
|
241
|
+
provider: string;
|
|
242
|
+
/** 在 models[] 里的位置:手写的文件可能有重复 ID,按位置定位才准 */
|
|
243
|
+
index: number;
|
|
244
|
+
id: string;
|
|
245
|
+
name: string | null;
|
|
246
|
+
contextWindow?: number;
|
|
247
|
+
maxTokens?: number;
|
|
248
|
+
vision?: boolean;
|
|
249
|
+
missing: CustomModelField[];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* 体检:models.json 里哪些模型有字段没写、会落到 pi 的缺省上(见 PI_MODEL_FALLBACKS)。
|
|
254
|
+
* 只看 providers[].models[]——modelOverrides 覆盖的是内置模型,那些模型 pi 的目录里有真值。
|
|
255
|
+
* 读的是设置页已经载入的那份配置(密钥已去掉也不影响),保存后重新载入就自动刷新。
|
|
256
|
+
*/
|
|
257
|
+
export function customModelGaps(providers: Readonly<Record<string, unknown>>): CustomModelGap[] {
|
|
258
|
+
const gaps: CustomModelGap[] = [];
|
|
259
|
+
for (const [provider, config] of Object.entries(providers)) {
|
|
260
|
+
const models = isRecord(config) && Array.isArray(config.models) ? config.models : [];
|
|
261
|
+
models.forEach((model, index) => {
|
|
262
|
+
if (!isRecord(model)) return;
|
|
263
|
+
const id = typeof model.id === "string" ? model.id.trim() : "";
|
|
264
|
+
if (!id) return;
|
|
265
|
+
const missing: CustomModelField[] = [];
|
|
266
|
+
if (model.contextWindow === undefined) missing.push("contextWindow");
|
|
267
|
+
if (model.maxTokens === undefined) missing.push("maxTokens");
|
|
268
|
+
if (!Array.isArray(model.input)) missing.push("input");
|
|
269
|
+
if (!missing.length) return;
|
|
270
|
+
const declared = modelDraftFromConfig(model);
|
|
271
|
+
gaps.push({
|
|
272
|
+
provider,
|
|
273
|
+
index,
|
|
274
|
+
id,
|
|
275
|
+
name: typeof model.name === "string" && model.name.trim() ? model.name.trim() : null,
|
|
276
|
+
...(declared.contextWindow === undefined ? {} : { contextWindow: declared.contextWindow }),
|
|
277
|
+
...(declared.maxTokens === undefined ? {} : { maxTokens: declared.maxTokens }),
|
|
278
|
+
...(declared.vision === undefined ? {} : { vision: declared.vision }),
|
|
279
|
+
missing,
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
return gaps;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** 用户在体检卡片里给某个模型填的值;没填的字段不带 */
|
|
287
|
+
export interface ModelFieldFill {
|
|
288
|
+
provider: string;
|
|
289
|
+
index: number;
|
|
290
|
+
id: string;
|
|
291
|
+
contextWindow?: number;
|
|
292
|
+
maxTokens?: number;
|
|
293
|
+
vision?: boolean;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* 按用户填的值补字段:只补空着的(已写的值不改),按 provider + 位置定位并核对 ID,对不上就跳过。
|
|
298
|
+
* 不改入参;返回新的 providers 与实际补了几个模型。
|
|
299
|
+
*/
|
|
300
|
+
export function fillModelFields(providers: Readonly<Record<string, unknown>>, fills: readonly ModelFieldFill[]): { providers: Record<string, unknown>; filled: number } {
|
|
301
|
+
const next = JSON.parse(JSON.stringify(providers)) as Record<string, unknown>;
|
|
302
|
+
let filled = 0;
|
|
303
|
+
for (const fill of fills) {
|
|
304
|
+
const config = Object.prototype.hasOwnProperty.call(next, fill.provider) ? next[fill.provider] : undefined;
|
|
305
|
+
const model = isRecord(config) && Array.isArray(config.models) ? config.models[fill.index] : undefined;
|
|
306
|
+
if (!isRecord(model) || typeof model.id !== "string" || model.id.trim() !== fill.id) continue;
|
|
307
|
+
let touched = false;
|
|
308
|
+
for (const key of ["contextWindow", "maxTokens"] as const) {
|
|
309
|
+
const value = positiveInt(fill[key]);
|
|
310
|
+
if (model[key] === undefined && value !== undefined) {
|
|
311
|
+
model[key] = value;
|
|
312
|
+
touched = true;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if (typeof fill.vision === "boolean" && !Array.isArray(model.input)) {
|
|
316
|
+
model.input = modelInputFor(fill.vision);
|
|
317
|
+
touched = true;
|
|
318
|
+
}
|
|
319
|
+
if (touched) filled += 1;
|
|
320
|
+
}
|
|
321
|
+
return { providers: next, filled };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* 改一个模型的上下文窗口,写在 pi 实际取值的那一层(modelOverrides 最后套用,压过 models[]):
|
|
326
|
+
* - 已有 modelOverrides.<id>.contextWindow → 改它;
|
|
327
|
+
* - 否则 models[] 里定义了这个 ID → 改那几条(设置页编辑表单显示的就是它们);
|
|
328
|
+
* - 都没有(内置目录、远程目录、扩展补的模型)→ 写 modelOverrides.<id>.contextWindow,同一条覆盖的其它字段保留。
|
|
329
|
+
* 不改入参。
|
|
330
|
+
*/
|
|
331
|
+
export function withContextWindow(providers: Readonly<Record<string, unknown>>, provider: string, id: string, contextWindow: number): Record<string, unknown> {
|
|
332
|
+
const next = JSON.parse(JSON.stringify(providers)) as Record<string, unknown>;
|
|
333
|
+
const existing = Object.prototype.hasOwnProperty.call(next, provider) ? next[provider] : undefined;
|
|
334
|
+
const config: Record<string, unknown> = isRecord(existing) ? existing : {};
|
|
335
|
+
next[provider] = config;
|
|
336
|
+
const overrides = isRecord(config.modelOverrides) ? config.modelOverrides : undefined;
|
|
337
|
+
const override = overrides && Object.prototype.hasOwnProperty.call(overrides, id) ? overrides[id] : undefined;
|
|
338
|
+
if (isRecord(override) && override.contextWindow !== undefined) {
|
|
339
|
+
override.contextWindow = contextWindow;
|
|
340
|
+
return next;
|
|
341
|
+
}
|
|
342
|
+
const defined = Array.isArray(config.models) ? config.models.filter((model) => isRecord(model) && typeof model.id === "string" && model.id.trim() === id) : [];
|
|
343
|
+
if (defined.length) {
|
|
344
|
+
for (const model of defined as Record<string, unknown>[]) model.contextWindow = contextWindow;
|
|
345
|
+
return next;
|
|
346
|
+
}
|
|
347
|
+
const table = overrides ?? {};
|
|
348
|
+
config.modelOverrides = table;
|
|
349
|
+
table[id] = { ...(isRecord(override) ? override : {}), contextWindow };
|
|
350
|
+
return next;
|
|
351
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模型目录地址判断(客户端安全,纯函数)。
|
|
3
|
+
*
|
|
4
|
+
* ⚠️ 只用于界面提示:进了本机/私网地址就自动勾上「允许访问本机/私有网络地址」,
|
|
5
|
+
* 免得用户对着 127.0.0.1 的网关反复点「获取可用模型」都被服务端挡回来。
|
|
6
|
+
* 真正的闸门在服务端 requestModelCatalog 里(allowPrivate / PI_WEB_ALLOW_PRIVATE_MODEL_DISCOVERY),
|
|
7
|
+
* 这里判错也不影响安全。
|
|
8
|
+
*/
|
|
9
|
+
export function looksPrivateEndpoint(url: string): boolean {
|
|
10
|
+
try {
|
|
11
|
+
const host = new URL(url.trim()).hostname.replace(/^\[|\]$/g, "").toLowerCase();
|
|
12
|
+
if (!host) return false;
|
|
13
|
+
if (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local")) return true;
|
|
14
|
+
if (host.includes(":")) return host === "::1" || /^(fc|fd|fe80)/.test(host);
|
|
15
|
+
const v4 = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
|
|
16
|
+
if (!v4) return false;
|
|
17
|
+
const a = Number(v4[1]);
|
|
18
|
+
const b = Number(v4[2]);
|
|
19
|
+
return a === 0 || a === 10 || a === 127 || (a === 192 && b === 168) || (a === 172 && b >= 16 && b <= 31) || (a === 169 && b === 254);
|
|
20
|
+
} catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* models.json / auth.json 的外部改动感知。不依赖 SDK(运行时作为 unknown 传入),方便直接测。
|
|
3
|
+
*/
|
|
4
|
+
import { statSync } from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
/** 两个文件的指纹(大小 + 修改时间,不读内容):指纹变了才需要重新加载模型目录 */
|
|
8
|
+
export function modelFilesFingerprint(agentDir: string): string {
|
|
9
|
+
return ["models.json", "auth.json"]
|
|
10
|
+
.map((name) => {
|
|
11
|
+
try {
|
|
12
|
+
const stat = statSync(path.join(agentDir, name));
|
|
13
|
+
return `${name}:${stat.size}:${stat.mtimeMs}`;
|
|
14
|
+
} catch {
|
|
15
|
+
return `${name}:-`;
|
|
16
|
+
}
|
|
17
|
+
})
|
|
18
|
+
.join("|");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* SDK 的 auth 存储第一次读 auth.json 后就缓存在内存里,手改之后要让它重读。
|
|
23
|
+
* SDK 没有公开这个入口:运行时把存储包在 RuntimeCredentials.store 里,这里走这条路径调 reload()。
|
|
24
|
+
* 路径不存在(SDK 版本变了)就什么都不做,只剩 models.json 的刷新;tests/model-files.test.ts 会用真实 SDK 验证这条路径。
|
|
25
|
+
*/
|
|
26
|
+
export function reloadStoredCredentials(runtime: unknown): void {
|
|
27
|
+
const store = (runtime as { credentials?: { store?: { reload?: () => void } } }).credentials?.store;
|
|
28
|
+
try {
|
|
29
|
+
store?.reload?.();
|
|
30
|
+
} catch {
|
|
31
|
+
// auth.json 写坏了:保留旧缓存,错误由下一次读取报出来
|
|
32
|
+
}
|
|
33
|
+
}
|