From 96759eb34c4859b4e64eee843e3f44c6e094ea74 Mon Sep 17 00:00:00 2001 From: Nezumi-2711 Date: Sat, 15 Aug 2026 12:08:10 +0700 Subject: [PATCH] feat: add setup script for models --- .gitignore | 2 + README.md | 142 +++++------ ...297c-ae5b-4ac7-8e4d-2422896a7203.omp.cache | Bin 0 -> 2790 bytes bun.lock | 8 + omp.cache | Bin 0 -> 216 bytes package.json | 6 +- src/components/ConfigPreview.tsx | 174 ++++++++++++- src/data/mockModels.ts | 2 + src/data/tools.ts | 68 +++-- src/services/installEndpoint.test.ts | 91 +++++++ src/services/installEndpoint.ts | 76 ++++++ src/services/installLink.ts | 110 ++++++++ src/services/installScript.ts | 236 ++++++++++++++++++ src/services/modelService.ts | 47 +--- src/types.ts | 7 + tsconfig.app.json | 2 +- tsconfig.json | 3 +- vite.config.ts | 19 ++ worker/index.ts | 35 +++ worker/modelProxy.test.ts | 91 +++++++ worker/modelProxy.ts | 147 +++++++++++ worker/tsconfig.json | 14 ++ wrangler.jsonc | 5 +- 23 files changed, 1140 insertions(+), 145 deletions(-) create mode 100644 bash.cb00297c-ae5b-4ac7-8e4d-2422896a7203.omp.cache create mode 100644 omp.cache create mode 100644 src/services/installEndpoint.test.ts create mode 100644 src/services/installEndpoint.ts create mode 100644 src/services/installLink.ts create mode 100644 src/services/installScript.ts create mode 100644 worker/index.ts create mode 100644 worker/modelProxy.test.ts create mode 100644 worker/modelProxy.ts create mode 100644 worker/tsconfig.json diff --git a/.gitignore b/.gitignore index 2442cd4..ceb4ea0 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,8 @@ dist-ssr # Cloudflare Workers local dev/deploy state .wrangler +*.tsbuildinfo +worker/env.d.ts # Editor directories and files .vscode/* diff --git a/README.md b/README.md index a00d0dd..36ed67d 100644 --- a/README.md +++ b/README.md @@ -1,77 +1,65 @@ -# React + TypeScript + Vite - -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. - -Currently, two official plugins are available: - -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) - -## React Compiler - -The React Compiler is enabled on this template. See [this documentation](https://react.dev/learn/react-compiler) for more information. - -Note: This will impact Vite dev & build performances. - -## Expanding the ESLint configuration - -If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: - -```js -export default defineConfig([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - // Other configs... - - // Remove tseslint.configs.recommended and replace with this - tseslint.configs.recommendedTypeChecked, - // Alternatively, use this for stricter rules - tseslint.configs.strictTypeChecked, - // Optionally, add this for stylistic rules - tseslint.configs.stylisticTypeChecked, - - // Other configs... - ], - languageOptions: { - parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname, - }, - // other options... - }, - }, -]) - -``` - -You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: - -```js -// eslint.config.js -import reactX from 'eslint-plugin-react-x' -import reactDom from 'eslint-plugin-react-dom' - -export default defineConfig([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - // Other configs... - // Enable lint rules for React - reactX.configs['recommended-typescript'], - // Enable lint rules for React DOM - reactDom.configs.recommended, - ], - languageOptions: { - parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname, - }, - // other options... - }, - }, -]) - -``` +# 9router Config Generator + +Generate configuration files and one-command installers for 9router-compatible model gateways. + +## Local development + +```sh +bun install +bun run dev +``` + +## Quality checks + +```sh +bun run verify +``` + +`verify` runs the production build, all tests, and ESLint. `bun run deploy` runs this validation before publishing the Worker. + +## Deploy to Cloudflare Workers + +This project deploys a Cloudflare Worker together with the Vite `dist/` assets. It is not a Cloudflare Pages project. + +1. Enable **Workers** for the target Cloudflare account and configure its `workers.dev` subdomain. +2. Authenticate Wrangler locally, or create a least-privilege Cloudflare API token for CI with the **Edit Cloudflare Workers** template, restricted to the deployment account. +3. Run: + + ```sh + bun run deploy + ``` + +4. Open the deployed `https://9router-config-generate..workers.dev` URL and test both `/` and a generated `/i/.sh` install link. + +### Optional custom domain + +After the first deployment, add a hostname in **Workers & Pages → 9router-config-generate → Settings → Domains & Routes → Add Custom Domain**. The Cloudflare zone must be active and the chosen hostname cannot already have a CNAME record. Cloudflare creates the DNS record and TLS certificate. + +Do not protect the app or `/i/*` with Cloudflare Access if users must download install scripts via `curl` or PowerShell without signing in. + +## Production model proxy + +The browser first requests `/models` directly. If that gateway does not allow CORS, the app falls back to `/api/fetch-models` on the Worker. + +The Worker proxy is disabled by default and only accepts HTTPS gateway origins explicitly allowlisted through `MODEL_PROXY_ALLOWED_ORIGINS`. This prevents the public endpoint from becoming an open proxy. + +Configure it in **Workers & Pages → 9router-config-generate → Settings → Variables and Secrets** as a plaintext variable: + +```txt +MODEL_PROXY_ALLOWED_ORIGINS=https://router.example.com,https://backup-router.example.com +``` + +Each value must be an HTTPS origin only: no path, query string, credentials, or wildcard. Deploy again after changing the variable. + +> A Cloudflare Worker cannot reach `localhost` on a visitor's computer. For a local 9router gateway, configure that gateway's CORS policy to allow the deployed app origin instead. Use the Worker proxy only for publicly reachable HTTPS gateways. + +The proxy forwards the API key supplied by the user for the single `/models` request. It does not store, log, or cache the key. Do not configure a shared 9router API key as a Worker variable or secret. + +## Cloudflare resources used + +- Cloudflare Workers +- Workers Static Assets (`ASSETS` binding) +- Optional custom domain +- Optional plaintext variable: `MODEL_PROXY_ALLOWED_ORIGINS` + +No KV, D1, R2, Queue, Durable Object, or Worker secret is required. diff --git a/bash.cb00297c-ae5b-4ac7-8e4d-2422896a7203.omp.cache b/bash.cb00297c-ae5b-4ac7-8e4d-2422896a7203.omp.cache new file mode 100644 index 0000000000000000000000000000000000000000..ad0920a1425027c84cf9bd4d4717b23816712a17 GIT binary patch literal 2790 zcmb_e-ESL35Z|%SA4%NwLsdb%U?f1L$ab7I4K1ONIBAnMX``HzAeE|GpVz)??ryEU zb7??8zkn7>`TlN8Ti$r#f8a0Rfxmzk-dgbvS!Vanu~R7$kt$ib+1c6InfcBB#&5_= z2EYl^@melBy|3wncDq)zxK}zy{W`1jT8+7ki4wO9-fE%61G^Lmt7K8jX2mH#6sxBl zKV+goEtY@#oik@ZUM7$Od6AG8-%COQ1xP|_iMnkDI|%eu9ghVe^;%%yPPICVUqaU4 zWjvS|3>ij3Lv~i*0IzH@q9~DS>_lEk0t{}{fxJp!7i=?G(_?-ZR8%G)KfqX)sECC5 z@*L;&LU^p%ypNT2psOK-IP`+X<)n-aJL<@ z*m2^NsnnSsy;fM^Z9i0nNtx5LOR6+r9Otz_3l&~xN=Po7vyi}@1 zn$xkNzVvB%9nHJ@`lv1{$Qu~cUioq4Ze6F`RoR75S7!$M>jI{uuy^FO?EsL?`|Pc@ z=j2hDJm!Skx)I_^euD0l8stp^PeNvyixtOjg8U3?X@<1fS)>QH#$OYLQvnb@qE}0qMfO6jel;j6@-FOZ^nuc9q1?)7iN`Za)C9}cW{Be z`RJBuc_4p4AjfS+>k78M`F3Ehulm($g4vF*H`86QeZXhYAe0qxb^N z(y^qD07Z@>@LI?Hp{|&MQw%dDL;$ z4Wur=N2_E9=ycChNKQJMHnIz>#XCo{ofc6yZ%2>=*R&C+`aBJ71>9hTdmhqmCu@b* zT4F-pT!Y^bBVs(293%=Y)T4Q(JZ#YhcHrJv7&+J&rmS3VSTC`Wq0ym+HC!IG4h$cx z9VpkRHF5xwe_p=_nFp8eViGK3SFDKaXZ)K9W7 zJX$W74hfDdCJOi-8cws&WIhu})?jFV6?F01CTv_Ti`l_yer$H;S+Gm^8UO+ABrWLd#n^uQE8G}Z9OP9P5z0^ zsxrWZgPc@S#UaE{Fb{SaO)ce*s?Rk3@)jc6(?Z!nhmN8RTJ%zKP8Zzd(X-{gzS^I)quW(MAmKa?p2dcfTMU-dsu^C z<*hX^5uJ1S2ftj25o$OtJG`}WMTl%tvPV!-S3f>D3@{Y1kkSDoe?|yHt~UXs>2<_m zPw!5*69{Yy`4)SJB!HwN-rdpdj7TBxDFW+|@_n3GJ>7wNMSGgV=8!F8IW;8&k1&_N z%lo?#u2lZv#T$7@E8-Z!AA!eL(nP0(o$rp3;{M{Hz2z4Q0;ho*!3GXud&?N6+cl(~ xHe`km70biLAwY5^@XsIeuQB*H@iQU2TKezOSxr&#f5jyJ?=cc2-D?+F{|6XXMV$Zu literal 0 HcmV?d00001 diff --git a/bun.lock b/bun.lock index 507bb5d..f92e7e8 100644 --- a/bun.lock +++ b/bun.lock @@ -10,10 +10,12 @@ }, "devDependencies": { "@babel/core": "^7.29.7", + "@cloudflare/workers-types": "^5.20260815.1", "@eslint/js": "^10.0.1", "@rolldown/plugin-babel": "^0.2.3", "@tailwindcss/vite": "^4", "@types/babel__core": "^7.20.5", + "@types/bun": "^1.3.14", "@types/node": "^24.13.3", "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", @@ -79,6 +81,8 @@ "@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260811.1", "", { "os": "win32", "cpu": "x64" }, "sha512-g6VquwjASlYAibcNW/0E6Zszht4qLkmnXOGwIjjRHl2A0Qz48kVeMcGvyH6eA0G9U3OzZojjYFpP+YeyQmmdjw=="], + "@cloudflare/workers-types": ["@cloudflare/workers-types@5.20260815.1", "", {}, "sha512-btpW8rEgeJKcxDlBWKxzjLLUHxzUWqXZY8O+DmM6NcSHadUQN5I71z6Q/yAKCTJyGS7adBpXzlsZSFbosLnECQ=="], + "@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="], "@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="], @@ -307,6 +311,8 @@ "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], + "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], + "@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="], "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], @@ -359,6 +365,8 @@ "browserslist": ["browserslist@4.28.8", "", { "dependencies": { "baseline-browser-mapping": "^2.11.12", "caniuse-lite": "^1.0.30001809", "electron-to-chromium": "^1.5.402", "node-releases": "^2.0.53", "update-browserslist-db": "^1.3.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA=="], + "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], + "caniuse-lite": ["caniuse-lite@1.0.30001809", "", {}, "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ=="], "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], diff --git a/omp.cache b/omp.cache new file mode 100644 index 0000000000000000000000000000000000000000..9700e13a6d44f906b6fde9a97e83f7429eb87e7d GIT binary patch literal 216 zcmX|&ONzok6h+@lVo-44pcmlSMzab>iW5UmRHUkWCMER=^--j0x^<0i!CeUHj2ztq z=iF_3%Mcf9sT)O`NhL!a`Ak?{oR~S#!E#QD$Q8Dp>g%+zzAn5IgWX{rAqsfGQqgXgvr!n`zNmC8`kVhU(i0}R%i84qV literal 0 HcmV?d00001 diff --git a/package.json b/package.json index 6b124c6..901cca7 100644 --- a/package.json +++ b/package.json @@ -6,9 +6,11 @@ "scripts": { "dev": "vite", "build": "tsc -b && vite build", + "test": "bun test", "lint": "eslint .", "preview": "vite preview", - "deploy": "vite build && wrangler deploy", + "verify": "bun run build && bun run test && bun run lint", + "deploy": "bun run verify && wrangler deploy", "preview:cf": "vite build && wrangler dev" }, "dependencies": { @@ -17,10 +19,12 @@ }, "devDependencies": { "@babel/core": "^7.29.7", + "@cloudflare/workers-types": "^5.20260815.1", "@eslint/js": "^10.0.1", "@rolldown/plugin-babel": "^0.2.3", "@tailwindcss/vite": "^4", "@types/babel__core": "^7.20.5", + "@types/bun": "^1.3.14", "@types/node": "^24.13.3", "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", diff --git a/src/components/ConfigPreview.tsx b/src/components/ConfigPreview.tsx index 60d68f4..1ca4499 100644 --- a/src/components/ConfigPreview.tsx +++ b/src/components/ConfigPreview.tsx @@ -4,7 +4,9 @@ import { Card } from './ui/Card' import { Button } from './ui/Button' import { Badge } from './ui/Badge' import { CodeBlock } from './ui/CodeBlock' -import { Download, Copy, Check, FileCode, Info } from 'lucide-react' +import { buildInstallScript, type ScriptOS } from '../services/installScript' +import { buildOneClickCommand } from '../services/installLink' +import { Download, Copy, Check, FileCode, Info, Terminal, Key, Sparkles, ShieldAlert } from 'lucide-react' interface ConfigPreviewProps { tool: ToolMeta @@ -17,21 +19,39 @@ export const ConfigPreview: React.FC = ({ connection, selectedModels, }) => { - const [copied, setCopied] = useState(false) + const [copiedConfig, setCopiedConfig] = useState(false) + const [copiedCommand, setCopiedCommand] = useState(false) + const [selectedOS, setSelectedOS] = useState('unix') + + const origin = typeof window !== 'undefined' ? window.location.origin : 'http://localhost:5173' + const oneClickCommand = buildOneClickCommand(tool, connection, selectedModels, selectedOS, origin) const configContent = tool.sampleTemplate(connection, selectedModels) + const installScript = buildInstallScript(tool, connection, selectedModels, selectedOS, { + keySource: 'runtime-env', + }) - const handleCopy = async () => { + const handleCopyConfig = async () => { try { await navigator.clipboard.writeText(configContent) - setCopied(true) - setTimeout(() => setCopied(false), 2000) + setCopiedConfig(true) + setTimeout(() => setCopiedConfig(false), 2000) } catch { // ignore } } - const handleDownload = () => { + const handleCopyCommand = async (cmd: string) => { + try { + await navigator.clipboard.writeText(cmd) + setCopiedCommand(true) + setTimeout(() => setCopiedCommand(false), 2000) + } catch { + // ignore + } + } + + const handleDownloadConfig = () => { const blob = new Blob([configContent], { type: 'text/plain;charset=utf-8' }) const url = URL.createObjectURL(blob) const link = document.createElement('a') @@ -43,8 +63,138 @@ export const ConfigPreview: React.FC = ({ URL.revokeObjectURL(url) } + const handleDownloadScript = () => { + if (!installScript) return + const blob = new Blob([installScript.content], { type: 'text/plain;charset=utf-8' }) + const url = URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = url + link.download = installScript.filename + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + URL.revokeObjectURL(url) + } + return (
+ {/* 1. Install Script Section (if supported by tool) */} + {installScript && ( + +
+
+
+ +
+
+
+ + 1-Command Auto Install + + + + Automated + +
+ + Copy & run — script downloads & writes config + +
+
+ + {/* OS selector tabs */} +
+ + +
+
+ + {/* Run Command snippet */} +
+ + Run this command in your terminal: + +
+ + {oneClickCommand} + + +
+
+ + {/* Security / Notice */} +
+ + + The URL and command contain no API key. The installer prompts locally for it, then writes it directly into VS Code configuration. + +
+ +
+ + Need offline installer? + + +
+
+ )} + + {/* 2. API Key Helper Note for GitHub Copilot (when no key in form) */} + {tool.id === 'copilot' && !connection.apiKey && ( +
+
+ + Secure API Key Storage in VS Code +
+

+ No API key entered above. The generated config uses{' '} + + ${'{input:9router-api-key}'} + + . When VS Code prompts you for 9router-api-key, paste your key. +

+
+ )} + + {/* 3. Raw Config Preview */}
@@ -61,7 +211,7 @@ export const ConfigPreview: React.FC = ({
- Target for {tool.name} + Manual config for {tool.name}
@@ -70,15 +220,15 @@ export const ConfigPreview: React.FC = ({