├── .env.example ├── src ├── constants │ └── index.ts ├── app │ ├── favicon.ico │ ├── globals.css │ ├── api │ │ ├── update-servers │ │ │ └── route.ts │ │ ├── detect │ │ │ └── route.ts │ │ └── generate │ │ │ └── route.ts │ └── [locale] │ │ ├── layout.tsx │ │ └── page.tsx ├── config.ts ├── types │ └── index.ts ├── navigation.ts ├── middleware.ts ├── types.ts ├── i18n │ ├── request.ts │ └── locales │ │ ├── zh.json │ │ └── en.json ├── scripts │ └── tsconfig.json ├── components │ ├── Modal.tsx │ ├── LanguageSwitch.tsx │ ├── Footer.tsx │ ├── ModelFilter.tsx │ ├── ModelTestModal.tsx │ ├── ServiceList.tsx │ └── Header.tsx └── lib │ ├── detect.ts │ └── ollama-utils.ts ├── public ├── vercel.svg ├── window.svg ├── file.svg ├── globe.svg └── next.svg ├── postcss.config.mjs ├── vercel.json ├── next.config.ts ├── eslint.config.mjs ├── .dockerignore ├── tailwind.config.ts ├── .gitignore ├── docker-compose.yml ├── Dockerfile ├── tsconfig.json ├── package.json ├── .github └── workflows │ └── monitor.yml ├── scripts ├── upload-data.ts ├── fofa-scan.mjs └── monitor.ts ├── README.md └── README.EN.md /.env.example: -------------------------------------------------------------------------------- 1 | UPSTASH_REDIS_URL= 2 | UPSTASH_REDIS_TOKEN= -------------------------------------------------------------------------------- /src/constants/index.ts: -------------------------------------------------------------------------------- 1 | export const PAGE_SIZE_OPTIONS = [10, 20, 50, 100]; -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forrany/Awesome-Ollama-Server/HEAD/src/app/favicon.ico -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | export const locales = ['en', 'zh'] as const; 2 | export type Locale = typeof locales[number]; 3 | export const defaultLocale: Locale = 'zh'; -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "functions": { 3 | "src/app/api/detect/route.ts": { 4 | "maxDuration": 50 5 | }, 6 | "src/app/api/update-servers/route.ts": { 7 | "maxDuration": 50 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface OllamaService { 2 | server: string; 3 | models: string[]; 4 | tps: number; 5 | lastUpdate: string; 6 | } 7 | 8 | export type SortField = 'tps' | 'lastUpdate'; 9 | export type SortOrder = 'asc' | 'desc'; -------------------------------------------------------------------------------- /src/navigation.ts: -------------------------------------------------------------------------------- 1 | import { createSharedPathnamesNavigation } from 'next-intl/navigation'; 2 | import { locales } from './config'; 3 | 4 | export const { Link, redirect, usePathname, useRouter } = createSharedPathnamesNavigation({ 5 | locales, 6 | localePrefix: 'always' 7 | }); 8 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import createNextIntlPlugin from 'next-intl/plugin'; 2 | import type { NextConfig } from "next"; 3 | 4 | const withNextIntl = createNextIntlPlugin(); 5 | 6 | const nextConfig: NextConfig = { 7 | /* config options here */ 8 | }; 9 | 10 | export default withNextIntl(nextConfig); 11 | -------------------------------------------------------------------------------- /src/middleware.ts: -------------------------------------------------------------------------------- 1 | import createMiddleware from 'next-intl/middleware'; 2 | import { defaultLocale, locales } from './config'; 3 | 4 | export default createMiddleware({ 5 | defaultLocale, 6 | locales, 7 | localePrefix: 'always' 8 | }); 9 | 10 | export const config = { 11 | matcher: ['/((?!api|_next|.*\\..*).*)'] 12 | }; -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface OllamaService { 2 | server: string; 3 | models: string[]; 4 | tps: number; 5 | lastUpdate: string; 6 | loading?: boolean; 7 | status: 'success' | 'error' | 'loading' | 'fake'; 8 | isFake?: boolean; 9 | } 10 | 11 | export type SortField = 'tps' | 'lastUpdate'; 12 | export type SortOrder = 'asc' | 'desc'; -------------------------------------------------------------------------------- /src/i18n/request.ts: -------------------------------------------------------------------------------- 1 | import { getRequestConfig } from 'next-intl/server'; 2 | import { defaultLocale } from '../config'; 3 | 4 | export default getRequestConfig(async ({ locale }) => { 5 | return { 6 | messages: (await import(`./locales/${locale || defaultLocale}.json`)).default, 7 | timeZone: 'UTC', 8 | now: new Date() 9 | }; 10 | }); -------------------------------------------------------------------------------- /public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/scripts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "CommonJS", 5 | "moduleResolution": "node", 6 | "esModuleInterop": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "outDir": "../../dist", 11 | "rootDir": ".." 12 | }, 13 | "include": ["./**/*", "../lib/**/*"], 14 | "exclude": ["node_modules"] 15 | } -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { dirname } from "path"; 2 | import { fileURLToPath } from "url"; 3 | import { FlatCompat } from "@eslint/eslintrc"; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname, 10 | }); 11 | 12 | const eslintConfig = [ 13 | ...compat.extends("next/core-web-vitals", "next/typescript"), 14 | ]; 15 | 16 | export default eslintConfig; 17 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Git 2 | .git 3 | .gitignore 4 | .github 5 | 6 | # 构建输出 7 | .next 8 | dist 9 | out 10 | 11 | # 依赖 12 | node_modules 13 | 14 | # 日志 15 | npm-debug.log* 16 | yarn-debug.log* 17 | yarn-error.log* 18 | 19 | # 本地环境 20 | .env.local 21 | .env.development.local 22 | .env.test.local 23 | .env.production.local 24 | 25 | # 编辑器配置 26 | .idea 27 | .vscode 28 | *.swp 29 | *.swo 30 | 31 | # 其他 32 | .DS_Store 33 | *.pem 34 | .vercel 35 | 36 | # 调试工具 37 | .aider* 38 | .aider-desk -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | export default { 4 | content: [ 5 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}", 8 | ], 9 | theme: { 10 | extend: { 11 | colors: { 12 | background: "var(--background)", 13 | foreground: "var(--foreground)", 14 | }, 15 | }, 16 | }, 17 | plugins: [], 18 | } satisfies Config; 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | /dist 23 | 24 | # misc 25 | .DS_Store 26 | *.pem 27 | 28 | # debug 29 | npm-debug.log* 30 | yarn-debug.log* 31 | yarn-error.log* 32 | .pnpm-debug.log* 33 | 34 | # env files (can opt-in for committing if needed) 35 | .env 36 | 37 | # vercel 38 | .vercel 39 | 40 | # typescript 41 | *.tsbuildinfo 42 | next-env.d.ts 43 | .aider* 44 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | ollama-monitor: 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | target: production 9 | ports: 10 | - "3000:3000" 11 | environment: 12 | - NODE_ENV=production 13 | - UPSTASH_REDIS_URL=${UPSTASH_REDIS_URL} 14 | - UPSTASH_REDIS_TOKEN=${UPSTASH_REDIS_TOKEN} 15 | restart: unless-stopped 16 | 17 | # 添加定期监控任务服务(可选) 18 | monitor-service: 19 | build: 20 | context: . 21 | dockerfile: Dockerfile 22 | target: production 23 | command: npm run monitor-and-upload 24 | environment: 25 | - NODE_ENV=production 26 | - UPSTASH_REDIS_URL=${UPSTASH_REDIS_URL} 27 | - UPSTASH_REDIS_TOKEN=${UPSTASH_REDIS_TOKEN} 28 | restart: unless-stopped 29 | depends_on: 30 | - ollama-monitor -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # 使用Node.js官方镜像作为基础镜像 2 | FROM node:20-alpine AS base 3 | 4 | # 设置工作目录 5 | WORKDIR /app 6 | 7 | # 安装pnpm 8 | RUN npm install -g pnpm 9 | 10 | # 安装依赖 11 | COPY package.json package-lock.json* ./ 12 | RUN npm ci 13 | 14 | # 复制所有文件 15 | COPY . . 16 | 17 | # 生成环境变量文件(如果不存在) 18 | RUN [ -f .env ] || cp .env.example .env 19 | 20 | # 构建应用 21 | RUN npm run build 22 | 23 | # 生产环境 24 | FROM node:20-alpine AS production 25 | 26 | WORKDIR /app 27 | 28 | # 复制依赖和构建文件 29 | COPY --from=base /app/node_modules ./node_modules 30 | COPY --from=base /app/.next ./.next 31 | COPY --from=base /app/public ./public 32 | COPY --from=base /app/package.json ./package.json 33 | COPY --from=base /app/.env ./.env 34 | COPY --from=base /app/next.config.ts ./next.config.ts 35 | COPY --from=base /app/scripts ./scripts 36 | 37 | # 暴露端口 38 | EXPOSE 3000 39 | 40 | # 启动应用 41 | CMD ["npm", "start"] -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "bundler", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve", 20 | "incremental": true, 21 | "plugins": [ 22 | { 23 | "name": "next" 24 | } 25 | ], 26 | "paths": { 27 | "@/*": [ 28 | "./src/*" 29 | ] 30 | } 31 | }, 32 | "include": [ 33 | "next-env.d.ts", 34 | "src/**/*.ts", 35 | "src/**/*.tsx", 36 | ".next/types/**/*.ts", 37 | "scripts/monitor.ts" 38 | ], 39 | "exclude": [ 40 | "node_modules" 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer utilities { 6 | .custom-scrollbar::-webkit-scrollbar { 7 | width: 6px; 8 | height: 6px; 9 | } 10 | 11 | .custom-scrollbar::-webkit-scrollbar-track { 12 | background: rgba(75, 85, 99, 0.1); 13 | border-radius: 3px; 14 | } 15 | 16 | .custom-scrollbar::-webkit-scrollbar-thumb { 17 | background: rgba(75, 85, 99, 0.5); 18 | border-radius: 3px; 19 | } 20 | 21 | .custom-scrollbar::-webkit-scrollbar-thumb:hover { 22 | background: rgba(75, 85, 99, 0.7); 23 | } 24 | } 25 | 26 | :root { 27 | --background: #ffffff; 28 | --foreground: #171717; 29 | } 30 | 31 | @media (prefers-color-scheme: dark) { 32 | :root { 33 | --background: #0a0a0a; 34 | --foreground: #ededed; 35 | } 36 | } 37 | 38 | body { 39 | color: var(--foreground); 40 | background: var(--background); 41 | font-family: Arial, Helvetica, sans-serif; 42 | } 43 | -------------------------------------------------------------------------------- /public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/api/update-servers/route.ts: -------------------------------------------------------------------------------- 1 | import { Redis } from '@upstash/redis' 2 | import { NextResponse } from 'next/server' 3 | 4 | const redis = new Redis({ 5 | url: process.env.UPSTASH_REDIS_URL!, 6 | token: process.env.UPSTASH_REDIS_TOKEN!, 7 | }) 8 | 9 | export async function POST(request: Request) { 10 | try { 11 | const { server } = await request.json() 12 | const encodedServer = encodeURIComponent(server) 13 | 14 | // 先检查服务器是否已存在 15 | const exists = await redis.sismember('ollama:servers', encodedServer) 16 | if (exists) { 17 | console.log(`服务器已存在: ${server}`) 18 | return NextResponse.json({ success: true, exists: true }) 19 | } 20 | 21 | // 如果不存在,则添加 22 | await redis.sadd('ollama:servers', encodedServer) 23 | console.log(`新增服务器: ${server}`) 24 | return NextResponse.json({ success: true, exists: false }) 25 | } catch (error) { 26 | console.error('更新服务器列表失败:', error) 27 | return NextResponse.json({ error: 'Failed to update servers' }, { status: 500 }) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ollama-monitor-service", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --turbopack", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "upload": "ts-node scripts/upload-data.ts", 11 | "monitor": "tsx scripts/monitor.ts", 12 | "monitor-and-upload": "npm run monitor && npm run upload" 13 | }, 14 | "dependencies": { 15 | "@headlessui/react": "^2.2.0", 16 | "@heroicons/react": "^2.2.0", 17 | "@types/axios": "^0.9.36", 18 | "axios": "^1.7.9", 19 | "date-fns": "^4.1.0", 20 | "next": "15.1.7", 21 | "next-intl": "^3.26.4", 22 | "react": "^19.0.0", 23 | "react-dom": "^19.0.0" 24 | }, 25 | "devDependencies": { 26 | "@eslint/eslintrc": "^3", 27 | "@types/node": "^20", 28 | "@types/react": "^19", 29 | "@types/react-dom": "^19", 30 | "@upstash/redis": "^1.34.4", 31 | "dotenv": "^16.4.7", 32 | "eslint": "^9", 33 | "eslint-config-next": "15.1.7", 34 | "postcss": "^8", 35 | "tailwindcss": "^3.4.1", 36 | "ts-node": "^10.9.2", 37 | "tsx": "^4.19.3", 38 | "typescript": "^5" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.github/workflows/monitor.yml: -------------------------------------------------------------------------------- 1 | name: Update Services Monitor 2 | 3 | on: 4 | schedule: 5 | - cron: '0 */10 * * *' # 每10小时执行一次 6 | workflow_dispatch: # 允许手动触发 7 | 8 | jobs: 9 | update-services: 10 | runs-on: ubuntu-latest 11 | 12 | env: 13 | UPSTASH_REDIS_URL: ${{ secrets.UPSTASH_REDIS_URL }} 14 | UPSTASH_REDIS_TOKEN: ${{ secrets.UPSTASH_REDIS_TOKEN }} 15 | COUNTRYS: US,CN,JP,KR,SG,TW 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | 22 | - name: Setup Node.js 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: '20' 26 | 27 | - name: Install dependencies 28 | run: npm install 29 | 30 | - name: Run update Services 31 | run: npm run monitor 32 | 33 | - name: Commit and push if changed 34 | run: | 35 | git config --global user.name 'GitHub Action' 36 | git config --global user.email 'action@github.com' 37 | git add public/data.json 38 | git diff --quiet && git diff --staged --quiet || (git commit -m "Update services data" && git push) 39 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /scripts/upload-data.ts: -------------------------------------------------------------------------------- 1 | const { Redis } = require('@upstash/redis') 2 | const fs = require('fs') 3 | const path = require('path') 4 | require('dotenv').config({ path: '.env' }) 5 | 6 | const redis = new Redis({ 7 | url: process.env.UPSTASH_REDIS_URL!, 8 | token: process.env.UPSTASH_REDIS_TOKEN!, 9 | }) 10 | 11 | interface OllamaService { 12 | server: string 13 | models: string[] 14 | tps: number 15 | lastUpdate: string 16 | } 17 | 18 | async function uploadData() { 19 | try { 20 | // 读取data.json文件 21 | const dataPath = path.join(process.cwd(), 'public', 'data.json') 22 | const services: OllamaService[] = JSON.parse(fs.readFileSync(dataPath, 'utf8')) 23 | 24 | // 使用pipeline批量操作提高性能 25 | const pipeline = redis.pipeline() 26 | 27 | // 先清除已有数据 28 | pipeline.del('ollama:servers') 29 | 30 | // 将每个服务存储为hash 31 | for (const service of services) { 32 | const encodedServer = encodeURIComponent(service.server) 33 | // const key = `ollama:server:${encodedServer}` 34 | // pipeline.hset(key, { 35 | // models: JSON.stringify(service.models), 36 | // tps: service.tps, 37 | // lastUpdate: service.lastUpdate 38 | // }) 39 | // 将编码后的server URL添加到服务器集合中 40 | pipeline.sadd('ollama:servers', encodedServer) 41 | } 42 | 43 | await pipeline.exec() 44 | console.log('数据上传成功!') 45 | } catch (error) { 46 | console.error('上传数据时出错:', error) 47 | } 48 | } 49 | 50 | uploadData() -------------------------------------------------------------------------------- /src/components/Modal.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, useEffect } from 'react'; 2 | import { XMarkIcon } from '@heroicons/react/24/outline'; 3 | 4 | interface ModalProps { 5 | isOpen: boolean; 6 | onClose: () => void; 7 | title: string; 8 | children: ReactNode; 9 | } 10 | 11 | export function Modal({ isOpen, onClose, title, children }: ModalProps) { 12 | // 处理 ESC 键关闭 13 | useEffect(() => { 14 | const handleEsc = (e: KeyboardEvent) => { 15 | if (e.key === 'Escape') onClose(); 16 | }; 17 | 18 | if (isOpen) { 19 | window.addEventListener('keydown', handleEsc); 20 | return () => window.removeEventListener('keydown', handleEsc); 21 | } 22 | }, [isOpen, onClose]); 23 | 24 | if (!isOpen) return null; 25 | 26 | return ( 27 |
28 |
29 |
33 | 34 |
36 |
37 |

38 | {title} 39 |

40 | 46 |
47 | 48 |
49 | {children} 50 |
51 |
52 |
53 |
54 | ); 55 | } -------------------------------------------------------------------------------- /src/app/api/detect/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import { checkService, measureTPS } from '@/lib/detect'; 3 | 4 | export const maxDuration = 50; // 设置最大执行时间为 50 秒 5 | 6 | export async function POST(request: Request) { 7 | let url = ''; 8 | try { 9 | const { url: requestUrl } = await request.json(); 10 | url = requestUrl; 11 | 12 | if (!url) { 13 | return NextResponse.json({ error: 'URL is required' }, { status: 400 }); 14 | } 15 | 16 | console.log('检测服务:', url); 17 | 18 | // 检查服务并获取可用模型 19 | const models = await checkService(url); 20 | 21 | // 如果服务不可用,返回空结果 22 | if (!models) { 23 | return NextResponse.json({ 24 | server: url, 25 | models: [], 26 | tps: 0, 27 | lastUpdate: new Date().toISOString(), 28 | status: 'error' 29 | }); 30 | } 31 | 32 | // 如果有可用模型,测试性能 33 | let tps = 0; 34 | let isFake = false; 35 | 36 | if (models.length > 0) { 37 | try { 38 | const tpsResult = await measureTPS(url, models[0]); 39 | 40 | // 检查是否为 fake-ollama 41 | if (typeof tpsResult === 'object' && 'isFake' in tpsResult) { 42 | isFake = true; 43 | tps = 0; 44 | } else { 45 | tps = tpsResult as number; 46 | } 47 | } catch (error) { 48 | console.error('性能测试失败:', error); 49 | } 50 | } 51 | 52 | // 返回结果 53 | return NextResponse.json({ 54 | server: url, 55 | models: models.map(model => model.name), 56 | tps, 57 | lastUpdate: new Date().toISOString(), 58 | status: isFake ? 'fake' : 'success', 59 | isFake 60 | }); 61 | 62 | } catch (error) { 63 | console.error('检测出错:', error); 64 | return NextResponse.json({ 65 | server: url, 66 | models: [], 67 | tps: 0, 68 | lastUpdate: new Date().toISOString(), 69 | status: 'error' 70 | }); 71 | } 72 | } -------------------------------------------------------------------------------- /scripts/fofa-scan.mjs: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'fs'; 2 | import axios from 'axios'; 3 | 4 | export async function fofaScan(country = 'RU') { 5 | const searchQuery = `app="Ollama" && country="${country}"`; 6 | const encodedQuery = Buffer.from(searchQuery).toString('base64'); 7 | const baseUrl = `https://fofa.info/result?qbase64=${encodedQuery}`; 8 | const userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"; 9 | 10 | try { 11 | const response = await axios.get(baseUrl, { 12 | headers: { 13 | 'User-Agent': userAgent 14 | } 15 | }); 16 | 17 | const HTML_START_TAG = 'hsxa-host"> console.log(host)); 37 | console.log("已保存:result.txt", hosts.length, hosts); 38 | 39 | return { hosts }; 40 | 41 | } catch (error) { 42 | const errorMessage = error.isAxiosError 43 | ? `请求失败: ${error.message}` 44 | : `未知错误: ${error}`; 45 | 46 | console.error(errorMessage); 47 | return { 48 | hosts: [], 49 | errorMessage 50 | }; 51 | } 52 | } 53 | 54 | // 自执行异步函数 55 | (async () => { 56 | await fofaScan(); 57 | })(); -------------------------------------------------------------------------------- /src/components/LanguageSwitch.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { Locale, locales } from '@/config'; 3 | import { GlobeAltIcon } from '@heroicons/react/24/outline'; 4 | import { useRouter, usePathname } from '@/navigation'; 5 | 6 | interface LanguageSwitchProps { 7 | currentLocale: Locale; 8 | } 9 | 10 | export function LanguageSwitch({ currentLocale }: LanguageSwitchProps) { 11 | const [isOpen, setIsOpen] = useState(false); 12 | const router = useRouter(); 13 | const pathname = usePathname(); 14 | 15 | const handleLanguageChange = (locale: Locale) => { 16 | setIsOpen(false); 17 | router.replace(pathname, { locale }); 18 | }; 19 | 20 | const languageNames: Record = { 21 | en: 'English', 22 | zh: '中文', 23 | }; 24 | 25 | return ( 26 |
27 | 35 | 36 | {isOpen && ( 37 |
38 |
39 | {locales.map((locale) => ( 40 | 52 | ))} 53 |
54 |
55 | )} 56 |
57 | ); 58 | } -------------------------------------------------------------------------------- /src/app/[locale]/layout.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | import { notFound } from 'next/navigation'; 3 | import { NextIntlClientProvider } from 'next-intl'; 4 | import { locales } from '@/config'; 5 | import { Geist, Geist_Mono } from "next/font/google"; 6 | import "../globals.css"; 7 | import type { Metadata } from "next"; 8 | import Script from 'next/script'; 9 | import * as React from 'react' 10 | const geistSans = Geist({ 11 | variable: "--font-geist-sans", 12 | subsets: ["latin"], 13 | }); 14 | 15 | const geistMono = Geist_Mono({ 16 | variable: "--font-geist-mono", 17 | subsets: ["latin"], 18 | }); 19 | 20 | export const metadata: Metadata = { 21 | title: "Ollama Monitor", 22 | description: "Ollama Monitor", 23 | }; 24 | 25 | interface RootLayoutProps { 26 | children: ReactNode; 27 | params: Promise<{ 28 | locale: string; 29 | }>; 30 | } 31 | 32 | export function generateStaticParams() { 33 | return locales.map((locale) => ({ locale })); 34 | } 35 | 36 | async function getMessages(locale: string) { 37 | try { 38 | return (await import(`@/i18n/locales/${locale}.json`)).default; 39 | } catch (error) { 40 | notFound(); 41 | console.error(error); 42 | } 43 | } 44 | 45 | export default async function RootLayout(props: RootLayoutProps) { 46 | const params = await props.params; 47 | 48 | const { 49 | children 50 | } = props; 51 | 52 | const messages = await getMessages(params.locale); // 异步获取 messages 53 | const now = new Date(); 54 | 55 | return ( 56 | 57 | 58 | {/* Google Analytics */} 59 | 71 | 72 | 78 | {children} 79 | 80 | 81 | 82 | ); 83 | } 84 | -------------------------------------------------------------------------------- /src/i18n/locales/zh.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Ollama 可用服务", 3 | "header": { 4 | "refresh": "刷新数据", 5 | "refreshing": "刷新中...", 6 | "refreshCountdown": "{countdown}秒后可刷新", 7 | "detect": "检测服务", 8 | "detecting": "检测中...", 9 | "detectCountdown": "{countdown}秒后可检测" 10 | }, 11 | "detect": { 12 | "title": "检测 Ollama 服务", 13 | "description": "请输入要检测的 Ollama 服务地址,每行一个地址。\n例如:http://localhost:11434", 14 | "placeholder": "http://localhost:11434\nhttp://192.168.1.100:11434", 15 | "cancel": "取消", 16 | "confirm": "开始检测", 17 | "newDetection": "新的检测", 18 | "downloading": "正在下载数据...", 19 | "download": "下载检测结果", 20 | "error": "检测失败", 21 | "success": "检测成功", 22 | "unavailable": "服务不可用", 23 | "fake": "伪装服务" 24 | }, 25 | "modelTest": { 26 | "title": "测试模型", 27 | "selectModel": "选择模型", 28 | "prompt": "输入提示词", 29 | "promptPlaceholder": "请输入您想要模型回答的问题或完成的任务...", 30 | "response": "模型回复", 31 | "responseEmpty": "等待生成...", 32 | "error": "生成失败,请重试", 33 | "close": "关闭", 34 | "test": "开始测试", 35 | "generating": "生成中...", 36 | "stop": "停止生成" 37 | }, 38 | "filter": { 39 | "title": "模型过滤", 40 | "search": "搜索模型...", 41 | "selectedModels": "已选模型:", 42 | "clearSelection": "清空选择", 43 | "sort": { 44 | "tps": "TPS 排序", 45 | "lastUpdate": "更新时间排序" 46 | } 47 | }, 48 | "service": { 49 | "server": "服务地址", 50 | "models": "可用模型", 51 | "tps": "{value} TPS", 52 | "lastUpdate": "更新时间", 53 | "lastUpdateValue": "更新于:{value}", 54 | "noServices": "暂无可用服务", 55 | "total": "共 {count} 个服务", 56 | "filtered": "已过滤模型: {models}", 57 | "availableModels": "可用模型:", 58 | "actions": "操作", 59 | "test": "测试" 60 | }, 61 | "pagination": { 62 | "perPage": "每页显示:", 63 | "showing": "显示 {from} - {to} 条,共 {total} 条", 64 | "first": "首页", 65 | "prev": "上一页", 66 | "next": "下一页", 67 | "last": "末页" 68 | }, 69 | "footer": { 70 | "about": { 71 | "title": "关于作者", 72 | "author": "作者:", 73 | "license": "本项目基于 MIT 协议开源,欢迎贡献和使用。" 74 | }, 75 | "disclaimer": { 76 | "title": "免责声明", 77 | "items": [ 78 | "1. 本网站仅用于安全研究和教育目的,展示的所有服务信息均来自互联网公开数据。", 79 | "2. 如果您发现自己的服务出现在列表中,建议及时加强网络安全防护措施。", 80 | "3. 本站不对任何因使用本站信息而导致的直接或间接损失负责。", 81 | "4. 请遵守当地法律法规,不得利用本站信息从事任何违法活动。" 82 | ] 83 | }, 84 | "copyright": "© {year} Ollama Monitor Service. All rights reserved." 85 | } 86 | } -------------------------------------------------------------------------------- /src/app/api/generate/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | 3 | export const maxDuration = 50; // 设置最大执行时间为 50 秒 4 | 5 | export async function POST(request: Request) { 6 | const encoder = new TextEncoder(); 7 | const { server, model, prompt } = await request.json(); 8 | 9 | try { 10 | const response = await fetch(`${server}/api/generate`, { 11 | method: 'POST', 12 | headers: { 13 | 'Content-Type': 'application/json', 14 | }, 15 | body: JSON.stringify({ 16 | model, 17 | prompt, 18 | stream: true, 19 | options: { 20 | temperature: 0.7, 21 | top_p: 0.9, 22 | } 23 | }), 24 | }); 25 | 26 | if (!response.ok) { 27 | return NextResponse.json({ error: 'Generation failed' }, { status: response.status }); 28 | } 29 | 30 | // 创建转换流来处理数据 31 | const transformStream = new TransformStream({ 32 | async transform(chunk, controller) { 33 | try { 34 | // 将二进制数据转换为文本 35 | const text = new TextDecoder().decode(chunk); 36 | // 分割成行并过滤掉空行 37 | const lines = text.split('\n').filter(line => line.trim()); 38 | 39 | for (const line of lines) { 40 | try { 41 | // 尝试解析每一行 JSON 42 | const data = JSON.parse(line.replace(/^data: /, '')); 43 | // 只处理包含 response 的数据 44 | if (data.response) { 45 | controller.enqueue(encoder.encode(data.response)); 46 | } 47 | // 如果是最后一条消息,可以处理其他信息 48 | if (data.done) { 49 | break; 50 | } 51 | } catch (e) { 52 | // 如果解析失败,尝试直接发送内容 53 | if (line.includes('response')) { 54 | const match = line.match(/"response":"([^"]*?)"/); 55 | if (match && match[1]) { 56 | controller.enqueue(encoder.encode(match[1])); 57 | } 58 | } 59 | console.error('处理数据块错误:', e); 60 | continue; 61 | } 62 | } 63 | } catch (error) { 64 | console.error('处理数据块错误:', error); 65 | } 66 | } 67 | }); 68 | 69 | // 将响应通过转换流处理 70 | const readableStream = response.body?.pipeThrough(transformStream); 71 | if (!readableStream) { 72 | return NextResponse.json({ error: 'No response body' }, { status: 500 }); 73 | } 74 | 75 | return new Response(readableStream); 76 | } catch (error) { 77 | console.error('生成错误:', error); 78 | return NextResponse.json({ error: 'Generation failed' }, { status: 500 }); 79 | } 80 | } -------------------------------------------------------------------------------- /src/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslations } from 'next-intl'; 2 | 3 | export function Footer() { 4 | const t = useTranslations(); 5 | const disclaimerItems = t.raw('footer.disclaimer.items') as string[]; 6 | 7 | return ( 8 |
72 | ); 73 | } -------------------------------------------------------------------------------- /src/lib/detect.ts: -------------------------------------------------------------------------------- 1 | import { 2 | TEST_PROMPTS, 3 | ModelInfo, 4 | fetchWithTimeout, 5 | checkService as checkServiceUtil, 6 | isFakeOllama, 7 | estimateTokens, 8 | generateRequestBody, 9 | isValidTPS 10 | } from './ollama-utils'; 11 | 12 | const TEST_ROUNDS = 3; // 测试轮数 13 | 14 | // 导出 checkService 函数 15 | export const checkService = checkServiceUtil; 16 | 17 | // 测量服务性能 18 | export async function measureTPS(url: string, model: ModelInfo): Promise { 19 | try { 20 | let totalTokens = 0; 21 | let totalTime = 0; 22 | let isFake = false; 23 | let abnormalTpsDetected = false; 24 | 25 | // 多轮测试 26 | for (let i = 0; i < TEST_ROUNDS; i++) { 27 | const prompt = TEST_PROMPTS[i % TEST_PROMPTS.length]; 28 | 29 | const response = await fetchWithTimeout(`${url}/api/generate`, { 30 | method: 'POST', 31 | headers: { 32 | 'Content-Type': 'application/json', 33 | }, 34 | body: JSON.stringify(generateRequestBody(model.name, prompt, false)), 35 | }); 36 | 37 | if (!response.ok) { 38 | console.error(`第 ${i + 1} 轮测试失败:`, await response.text()); 39 | continue; 40 | } 41 | 42 | const data = await response.json(); 43 | 44 | // 检查是否为 fake-ollama 45 | if (isFakeOllama(data.response)) { 46 | console.log(`检测到 fake-ollama: ${url}`); 47 | isFake = true; 48 | break; 49 | } 50 | 51 | // 使用 API 返回的 eval_count 和 eval_duration 计算 TPS 52 | if (data.eval_count && data.eval_duration) { 53 | const rawTps = (data.eval_count / data.eval_duration) * 1e9; 54 | 55 | // 检查 TPS 是否异常 56 | if (!isValidTPS(rawTps)) { 57 | console.warn(`检测到异常 TPS 值: ${rawTps.toFixed(2)} 来自服务器: ${url}`); 58 | abnormalTpsDetected = true; 59 | // 继续测试其他轮次,收集更多数据 60 | } 61 | 62 | totalTokens += data.eval_count; 63 | totalTime += data.eval_duration / 1e9; // 转换为秒 64 | } else { 65 | // 如果 API 没有返回这些字段,则使用估算方法 66 | const inputTokens = estimateTokens(prompt); 67 | const outputTokens = estimateTokens(data.response); 68 | const timeInSeconds = data.total_duration ? data.total_duration / 1e9 : 1; // 如果有 total_duration 则使用它 69 | 70 | totalTokens += (inputTokens + outputTokens); 71 | totalTime += timeInSeconds; 72 | } 73 | 74 | // 等待一小段时间再进行下一轮测试 75 | await new Promise(resolve => setTimeout(resolve, 1000)); 76 | } 77 | 78 | // 如果是假的或者检测到异常 TPS,返回特殊标记 79 | if (isFake || abnormalTpsDetected) { 80 | return { isFake: true }; 81 | } 82 | 83 | // 计算平均 TPS 84 | const averageTps = totalTime > 0 ? totalTokens / totalTime : 0; 85 | 86 | // 最后再检查一次平均 TPS 是否合理 87 | if (!isValidTPS(averageTps)) { 88 | console.warn(`最终计算的平均 TPS 异常: ${averageTps.toFixed(2)} 来自服务器: ${url}`); 89 | return { isFake: true }; 90 | } 91 | 92 | return averageTps; 93 | } catch (error) { 94 | console.error('测量 TPS 失败:', error); 95 | return 0; 96 | } 97 | } -------------------------------------------------------------------------------- /src/i18n/locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Ollama Available Services", 3 | "header": { 4 | "refresh": "Refresh Data", 5 | "refreshing": "Refreshing...", 6 | "refreshCountdown": "Refresh in {countdown}s", 7 | "detect": "Detect Services", 8 | "detecting": "Detecting...", 9 | "detectCountdown": "Detect in {countdown}s" 10 | }, 11 | "detect": { 12 | "title": "Detect Ollama Services", 13 | "description": "Please enter the Ollama service URLs to detect, one per line.\nExample: http://localhost:11434", 14 | "placeholder": "http://localhost:11434\nhttp://192.168.1.100:11434", 15 | "cancel": "Cancel", 16 | "confirm": "Start Detection", 17 | "newDetection": "New Detection", 18 | "downloading": "Downloading data...", 19 | "download": "Download Results", 20 | "error": "Detection Failed", 21 | "success": "Detection Success", 22 | "unavailable": "Service Unavailable", 23 | "fake": "Fake Service" 24 | }, 25 | "modelTest": { 26 | "title": "Test Model", 27 | "selectModel": "Select Model", 28 | "prompt": "Enter Prompt", 29 | "promptPlaceholder": "Enter your question or task for the model...", 30 | "response": "Model Response", 31 | "responseEmpty": "Waiting for generation...", 32 | "error": "Generation failed, please try again", 33 | "close": "Close", 34 | "test": "Start Test", 35 | "generating": "Generating...", 36 | "stop": "Stop Generation" 37 | }, 38 | "filter": { 39 | "title": "Model Filter", 40 | "search": "Search models...", 41 | "selectedModels": "Selected Models:", 42 | "clearSelection": "Clear Selection", 43 | "sort": { 44 | "tps": "Sort by TPS", 45 | "lastUpdate": "Sort by Update Time" 46 | } 47 | }, 48 | "service": { 49 | "server": "Server", 50 | "models": "Available Models", 51 | "tps": "{value} TPS", 52 | "lastUpdate": "Last Update", 53 | "lastUpdateValue": "Updated: {value}", 54 | "noServices": "No services available", 55 | "total": "{count} Services Total", 56 | "actions": "Actions", 57 | "test": "Test", 58 | "filtered": "Filtered Models: {models}", 59 | "availableModels": "Available Models:" 60 | }, 61 | "pagination": { 62 | "perPage": "Per page:", 63 | "showing": "Showing {from} - {to} of {total}", 64 | "first": "First", 65 | "prev": "Previous", 66 | "next": "Next", 67 | "last": "Last" 68 | }, 69 | "footer": { 70 | "about": { 71 | "title": "About Author", 72 | "author": "Author:", 73 | "license": "This project is open source under the MIT License. Contributions are welcome." 74 | }, 75 | "disclaimer": { 76 | "title": "Disclaimer", 77 | "items": [ 78 | "1. This website is for security research and educational purposes only. All service information is from public data.", 79 | "2. If you find your service listed here, it's recommended to enhance your network security measures.", 80 | "3. We are not responsible for any direct or indirect losses caused by using the information on this site.", 81 | "4. Please comply with local laws and regulations. Do not use this information for any illegal activities." 82 | ] 83 | }, 84 | "copyright": "© {year} Ollama Monitor Service. All rights reserved." 85 | } 86 | } -------------------------------------------------------------------------------- /src/lib/ollama-utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Ollama 工具函数库 3 | * 提供与 Ollama API 交互的通用函数 4 | */ 5 | 6 | // 超时设置 7 | export const TIMEOUT_MS = 30000; // 30秒超时 8 | 9 | // 测试提示词 10 | export const TEST_PROMPTS = [ 11 | "Tell me a short story about a robot who learns to love.", 12 | "Explain the concept of recursion in programming.", 13 | "What are the main differences between classical and quantum computing?" 14 | ]; 15 | 16 | // 定义模型信息的接口 17 | export interface ModelInfo { 18 | name: string; 19 | model: string; 20 | modified_at: string; 21 | size: number; 22 | digest: string; 23 | details: { 24 | parent_model: string; 25 | format: string; 26 | family: string; 27 | families: string[]; 28 | parameter_size: string; 29 | quantization_level: string; 30 | }; 31 | } 32 | 33 | // 创建带超时的 fetch 函数 34 | export async function fetchWithTimeout(url: string, options: RequestInit = {}, timeout = TIMEOUT_MS) { 35 | const controller = new AbortController(); 36 | const timeoutId = setTimeout(() => controller.abort(), timeout); 37 | 38 | try { 39 | const response = await fetch(url, { 40 | ...options, 41 | signal: controller.signal, 42 | }); 43 | clearTimeout(timeoutId); 44 | return response; 45 | } catch (error) { 46 | clearTimeout(timeoutId); 47 | throw error; 48 | } 49 | } 50 | 51 | // 检查服务可用性并获取模型列表 52 | export async function checkService(url: string): Promise { 53 | try { 54 | const response = await fetchWithTimeout(`${url}/api/tags`, { 55 | method: 'GET', 56 | headers: { 57 | 'Accept': 'application/json', 58 | }, 59 | }); 60 | 61 | if (!response.ok) { 62 | console.log(`服务返回非 200 状态码: ${url}, 状态码: ${response.status}`); 63 | return null; 64 | } 65 | 66 | const data = await response.json(); 67 | return data.models || []; 68 | } catch (error) { 69 | console.error('检查服务失败:', error); 70 | return null; 71 | } 72 | } 73 | 74 | // 检测 TPS 是否在合理范围内 75 | export function isValidTPS(tps: number): boolean { 76 | // 正常的 Ollama 服务 TPS 通常在 0.1 到 100 之间 77 | // 高性能服务器可能达到 200-300 TPS 78 | // 超过 1000 的 TPS 值通常是不合理的 79 | const MIN_VALID_TPS = 0.01; // 最小有效 TPS 80 | const MAX_VALID_TPS = 1000; // 最大有效 TPS 81 | 82 | return tps >= MIN_VALID_TPS && tps <= MAX_VALID_TPS; 83 | } 84 | 85 | // 检测是否为 fake-ollama 86 | export function isFakeOllama(response: string): boolean { 87 | return response.includes('fake-ollama') || 88 | response.includes('这是一条来自') || 89 | response.includes('固定回复'); 90 | } 91 | 92 | // 估算文本的 token 数量 93 | export function estimateTokens(text: string): number { 94 | // 这是一个简单的估算,实际的 token 数量可能会有所不同 95 | // 1. 按空格分词 96 | const words = text.split(/\s+/); 97 | // 2. 考虑标点符号 98 | const punctuation = text.match(/[.,!?;:'"()\[\]{}]/g)?.length || 0; 99 | // 3. 考虑数字 100 | const numbers = text.match(/\d+/g)?.length || 0; 101 | 102 | return words.length + punctuation + numbers; 103 | } 104 | 105 | // 生成测试请求体 106 | export function generateRequestBody(model: string, prompt: string, stream = false) { 107 | return { 108 | model, 109 | prompt, 110 | stream, 111 | options: { 112 | temperature: 0.7, 113 | top_p: 0.9, 114 | top_k: 40, 115 | } 116 | }; 117 | } 118 | 119 | // 计算 TPS (Tokens Per Second) 120 | export function calculateTPS(data: { eval_count: number, eval_duration: number }): number { 121 | // 使用 API 返回的 eval_count 和 eval_duration 计算 TPS 122 | if (data.eval_count && data.eval_duration) { 123 | // eval_duration 是纳秒单位,计算: eval_count / eval_duration * 10^9 124 | const tps = (data.eval_count / data.eval_duration) * 1e9; 125 | 126 | // 检查 TPS 是否在合理范围内 127 | if (!isValidTPS(tps)) { 128 | console.warn(`检测到异常 TPS 值: ${tps.toFixed(2)}`); 129 | // 如果 TPS 不合理,返回一个合理的默认值 130 | return 0; 131 | } 132 | 133 | return tps; 134 | } 135 | 136 | // 如果没有这些字段,返回 0 137 | return 0; 138 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ollama 服务监控系统 2 | 3 | [English Version](https://github.com/forrany/Awesome-Ollama-Server/blob/main/README.EN.md) 4 | 5 | 这是一个用于监控和检测 Ollama 服务可用性和性能的系统。它提供了一个现代化的 Web 界面,支持多语言(中文/英文),并具有实时检测和数据展示功能。 6 | 7 | [在线体验](https://ollama.vincentko.top/) 8 | 9 | --- 10 | 11 | ### ❤️ Sponsors 12 | CDN acceleration and security protection for this project are sponsored by Tencent EdgeOne. 13 | 14 | [Tencent EdgeOne](https://edgeone.ai/?from=github) 15 | 16 | --- 17 | 18 | ## 功能特点 19 | 20 | - 🔍 服务检测 21 | - 支持批量检测 Ollama 服务 22 | - 实时显示检测状态和结果 23 | - 支持检测结果导出 24 | - 支持自动 FOFA 扫描 25 | - 📊 性能监控 26 | - 测试服务响应时间和 TPS 27 | - 展示可用模型列表 28 | - 性能数据可视化 29 | - 🌐 多语言支持 30 | - 中文界面 31 | - 英文界面 32 | - 一键切换语言 33 | - 🎯 高级筛选 34 | - 模型过滤 35 | - TPS/更新时间排序 36 | - 分页显示 37 | 38 | ## 技术栈 39 | 40 | - ⚡️ Next.js 14 - React 框架 41 | - 🔥 TypeScript - 类型安全 42 | - 🎨 Tailwind CSS - 样式框架 43 | - 🌍 next-intl - 国际化 44 | - 🔄 Server Components - 服务端组件 45 | - 📱 响应式设计 - 移动端适配 46 | 47 | ## 快速开始 48 | 49 | ### 前置要求 50 | 51 | - Node.js 18.0 或更高版本 52 | - npm 或 yarn 包管理器 53 | 54 | ### 安装 55 | 56 | ```bash 57 | # 克隆项目 58 | git clone https://github.com/forrany/Awesome-Ollama-Server.git 59 | cd Awesome-Ollama-Server 60 | 61 | # 安装依赖 62 | npm install 63 | # 或 64 | yarn install 65 | ``` 66 | 67 | ### 开发环境 68 | 69 | ```bash 70 | # 启动开发服务器 71 | npm run dev 72 | # 或 73 | yarn dev 74 | ``` 75 | 76 | 访问 [http://localhost:3000](http://localhost:3000/) 查看应用。 77 | 78 | ### 生产环境 79 | 80 | ```bash 81 | # 构建项目 82 | npm run build 83 | # 或 84 | yarn build 85 | 86 | # 启动服务 87 | npm start 88 | # 或 89 | yarn start 90 | ``` 91 | 92 | ## 使用说明 93 | 94 | ### 检测服务 95 | 96 | 1. 点击"检测服务"按钮 97 | 2. 在弹出的对话框中输入 Ollama 服务地址(每行一个) 98 | 3. 点击"开始检测" 99 | 4. 等待检测完成,查看结果 100 | 5. 可选择下载检测结果 101 | 102 | ### 筛选和排序 103 | 104 | - 使用模型过滤器选择特定模型 105 | - 点击 TPS 或更新时间进行排序 106 | - 使用搜索框快速查找模型 107 | 108 | ### 语言切换 109 | 110 | - 点击右上角的语言切换按钮 111 | - 选择中文或英文 112 | 113 | ## 项目结构 114 | 115 | ``` 116 | src/ 117 | ├── app/ # Next.js 应用目录 118 | ├── components/ # React 组件 119 | ├── i18n/ # 国际化文件 120 | ├── lib/ # 工具函数 121 | ├── types/ # TypeScript 类型定义 122 | └── config/ # 配置文件 123 | ``` 124 | 125 | ## 环境变量 126 | 127 | 创建 `.env` 文件并设置以下变量,填写后 Github Actions 会自动执行监控和上传 128 | 129 | ``` 130 | # 可选:Redis 配置(如果使用) 131 | UPSTASH_REDIS_URL=your-redis-url 132 | UPSTASH_REDIS_TOKEN=your-redis-token 133 | 134 | # 可选:FOFA扫描国家列表(如果使用) 135 | COUNTRYS=US,CN,RU 136 | ``` 137 | 138 | ## 贡献指南 139 | 140 | 1. Fork 项目 141 | 2. 创建特性分支 (`git checkout -b feature/AmazingFeature`) 142 | 3. 提交更改 (`git commit -m 'Add some AmazingFeature'`) 143 | 4. 推送到分支 (`git push origin feature/AmazingFeature`) 144 | 5. 打开 Pull Request 145 | 146 | ## 许可证 147 | 148 | 本项目基于 MIT 协议开源 - 详见 [LICENSE](https://github.com/forrany/Awesome-Ollama-Server/blob/main/LICENSE) 文件 149 | 150 | ## 作者 151 | 152 | VincentKo (@forrany) - [GitHub](https://github.com/forrany) 153 | 154 | ## 免责声明 155 | 156 | 1. 本项目仅用于安全研究和教育目的 157 | 2. 不得将本项目用于任何非法用途 158 | 3. 作者不对使用本项目造成的任何损失负责 159 | 4. 数据来源于网络,如有侵权,请联系作者删除 160 | 161 | ## Star History 162 | 163 | [![Star History Chart](https://api.star-history.com/svg?repos=forrany/Awesome-Ollama-Server&type=Date)](https://star-history.com/#forrany/Awesome-Ollama-Server&Date) 164 | 165 | ## Docker 部署 166 | 167 | 项目支持 Docker 部署,方便在各种环境中快速搭建。 168 | 169 | ### 使用 Docker Compose 部署(推荐) 170 | 171 | 1. 确保已安装 [Docker](https://docs.docker.com/get-docker/) 和 [Docker Compose](https://docs.docker.com/compose/install/) 172 | 173 | 2. 克隆仓库并进入项目目录 174 | 175 | ```bash 176 | git clone https://github.com/forrany/Awesome-Ollama-Server.git 177 | cd Awesome-Ollama-Server 178 | ``` 179 | 180 | 3. 创建环境变量文件(如果需要 Upstash Redis 数据存储) 181 | 182 | ```bash 183 | cp .env.example .env 184 | ``` 185 | 186 | 然后编辑 `.env` 文件,填入 Upstash Redis 的凭据: 187 | 188 | ``` 189 | UPSTASH_REDIS_URL=your_redis_url 190 | UPSTASH_REDIS_TOKEN=your_redis_token 191 | ``` 192 | 193 | 4. 启动服务 194 | 195 | ```bash 196 | docker-compose up -d 197 | ``` 198 | 199 | 这将启动两个服务: 200 | - `ollama-monitor`: Web 应用,访问 http://localhost:3000 查看 201 | - `monitor-service`: 后台监控服务,自动收集 Ollama 服务数据 202 | 203 | ### 仅使用 Docker 部署 204 | 205 | 如果只需要部署 Web 应用而不需要后台监控服务: 206 | 207 | ```bash 208 | # 构建镜像 209 | docker build -t ollama-monitor . 210 | 211 | # 运行容器 212 | docker run -d -p 3000:3000 --name ollama-monitor \ 213 | -e UPSTASH_REDIS_URL=your_redis_url \ 214 | -e UPSTASH_REDIS_TOKEN=your_redis_token \ 215 | ollama-monitor 216 | ``` 217 | 218 | 访问 [http://localhost:3000](http://localhost:3000/) 查看应用。 219 | -------------------------------------------------------------------------------- /src/components/ModelFilter.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslations } from 'next-intl'; 2 | import { ArrowUpIcon, ArrowDownIcon, XMarkIcon } from '@heroicons/react/24/outline'; 3 | 4 | interface ModelFilterProps { 5 | selectedModels: string[]; 6 | availableModels: string[]; 7 | searchTerm: string; 8 | sortField: 'tps' | 'lastUpdate'; 9 | sortOrder: 'asc' | 'desc'; 10 | onSearchChange: (term: string) => void; 11 | onToggleModel: (model: string) => void; 12 | onRemoveModel: (model: string) => void; 13 | onClearModels: () => void; 14 | onToggleSort: (field: 'tps' | 'lastUpdate') => void; 15 | } 16 | 17 | export function ModelFilter({ 18 | selectedModels, 19 | availableModels, 20 | searchTerm, 21 | sortField, 22 | sortOrder, 23 | onSearchChange, 24 | onToggleModel, 25 | onRemoveModel, 26 | onClearModels, 27 | onToggleSort, 28 | }: ModelFilterProps) { 29 | const t = useTranslations(); 30 | 31 | // 获取过滤后的模型列表 32 | const filteredModels = availableModels.filter(model => 33 | model.toLowerCase().includes(searchTerm.toLowerCase()) 34 | ); 35 | 36 | // 排序图标组件 37 | const SortIcon = ({ field }: { field: 'tps' | 'lastUpdate' }) => { 38 | if (sortField !== field) return null; 39 | return sortOrder === 'asc' ? 40 | : 41 | ; 42 | }; 43 | 44 | return ( 45 |
46 |
47 |
48 |

{t('filter.title')}

49 |
50 | 59 | 68 |
69 |
70 | 71 | {/* 搜索输入框 */} 72 |
73 | onSearchChange(e.target.value)} 77 | placeholder={t('filter.search')} 78 | className="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-md text-gray-200 79 | placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" 80 | /> 81 |
82 | 83 | {/* 已选模型 */} 84 | {selectedModels.length > 0 && ( 85 |
86 |
87 | {t('filter.selectedModels')} 88 | 94 |
95 |
96 | {selectedModels.map(model => ( 97 | 102 | {model} 103 | 109 | 110 | ))} 111 |
112 |
113 | )} 114 | 115 | {/* 模型列表 */} 116 |
117 | {filteredModels.map(model => ( 118 | 129 | ))} 130 |
131 |
132 |
133 | ); 134 | } -------------------------------------------------------------------------------- /README.EN.md: -------------------------------------------------------------------------------- 1 | # Ollama Service Monitoring System 2 | 3 | [中文版](README.md) 4 | 5 | This is a system for monitoring and detecting the availability and performance of Ollama services. It provides a modern web interface, supports multiple languages (Chinese/English), and features real-time detection and data visualization. 6 | 7 | [Online Demo](https://ollama.vincentko.top) 8 | 9 | Support online model testing 10 | 14 | 15 | ## Features 16 | 17 | - 🔍 Service Detection 18 | - Supports batch detection of Ollama services 19 | - Real-time display of detection status and results 20 | - Supports exporting detection results 21 | - Supports automatic FOFA scanning 22 | 23 | - 📊 Performance Monitoring 24 | - Tests service response time and TPS 25 | - Displays a list of available models 26 | - Visualizes performance data 27 | 28 | - 🌐 Multi-language Support 29 | - Chinese interface 30 | - English interface 31 | - One-click language switching 32 | 33 | - 🎯 Advanced Filtering 34 | - Model filtering 35 | - TPS/Update Time sorting 36 | - Paginated display 37 | 38 | ## Technology Stack 39 | 40 | - ⚡️ Next.js 14 - React Framework 41 | - 🔥 TypeScript - Type Safety 42 | - 🎨 Tailwind CSS - CSS Framework 43 | - 🌍 next-intl - Internationalization 44 | - 🔄 Server Components - Server Components 45 | - 📱 Responsive Design - Mobile Adaptation 46 | 47 | ## Quick Start 48 | 49 | ### Prerequisites 50 | 51 | - Node.js 18.0 or higher 52 | - npm or yarn package manager 53 | 54 | ### Installation 55 | 56 | ```bash 57 | # Clone the project 58 | git clone git@github.com:forrany/Awesome-Ollama-Server.git 59 | cd Awesome-Ollama-Server 60 | 61 | # Install dependencies 62 | npm install 63 | # or 64 | yarn install 65 | ``` 66 | 67 | ### Development Environment 68 | 69 | ```bash 70 | # Start the development server 71 | npm run dev 72 | # or 73 | yarn dev 74 | ``` 75 | 76 | Visit http://localhost:3000 to view the application. 77 | 78 | ### Production Environment 79 | 80 | ```bash 81 | # Build the project 82 | npm run build 83 | # or 84 | yarn build 85 | 86 | # Start the server 87 | npm start 88 | # or 89 | yarn start 90 | ``` 91 | 92 | ## Usage Instructions 93 | 94 | ### Detect Services 95 | 96 | 1. Click the "Service Detection" button 97 | 2. Enter Ollama service addresses in the pop-up dialog (one per line) 98 | 3. Click "Start Detection" 99 | 4. Wait for the detection to complete and view the results 100 | 5. Option to download detection results 101 | 102 | ### Filter and Sort 103 | 104 | - Use the model filter to select specific models 105 | - Click TPS or Update Time to sort 106 | - Use the search box to quickly find models 107 | 108 | ### Language Switching 109 | 110 | - Click the language switch button in the upper right corner 111 | - Select Chinese or English 112 | 113 | ## Project Structure 114 | 115 | ``` 116 | src/ 117 | ├── app/ # Next.js Application Directory 118 | ├── components/ # React Components 119 | ├── i18n/ # Internationalization Files 120 | ├── lib/ # Utility Functions 121 | ├── types/ # TypeScript Type Definitions 122 | └── config/ # Configuration Files 123 | ``` 124 | 125 | ## Environment Variables 126 | 127 | Create a `.env` file and set the following variables. Github Actions will automatically execute monitoring and upload after filling in. 128 | 129 | ```env 130 | # Optional: Redis Configuration (if used) 131 | UPSTASH_REDIS_URL=your-redis-url 132 | UPSTASH_REDIS_TOKEN=your-redis-token 133 | 134 | # Optional: FOFA scanning country list (if used) 135 | COUNTRYS=US,CN,RU 136 | ``` 137 | 138 | ## Docker Deployment 139 | 140 | This project supports Docker deployment for easy setup in various environments. 141 | 142 | ### Using Docker Compose (Recommended) 143 | 144 | 1. Make sure you have [Docker](https://docs.docker.com/get-docker/) and [Docker Compose](https://docs.docker.com/compose/install/) installed 145 | 146 | 2. Clone the repository and navigate to the project directory 147 | ```bash 148 | git clone https://github.com/vincexiv/ollama-monitor-service.git 149 | cd ollama-monitor-service 150 | ``` 151 | 152 | 3. Create an environment variables file (if you need Upstash Redis for data storage) 153 | ```bash 154 | cp .env.example .env 155 | ``` 156 | 157 | Then edit the `.env` file and fill in your Upstash Redis credentials: 158 | ``` 159 | UPSTASH_REDIS_URL=your_redis_url 160 | UPSTASH_REDIS_TOKEN=your_redis_token 161 | ``` 162 | 163 | 4. Start the services 164 | ```bash 165 | docker-compose up -d 166 | ``` 167 | 168 | This will start two services: 169 | - `ollama-monitor`: Web application, accessible at http://localhost:3000 170 | - `monitor-service`: Background monitoring service that automatically collects Ollama service data 171 | 172 | ### Using Docker Only 173 | 174 | If you only need to deploy the web application without the background monitoring service: 175 | 176 | ```bash 177 | # Build the image 178 | docker build -t ollama-monitor . 179 | 180 | # Run the container 181 | docker run -d -p 3000:3000 --name ollama-monitor \ 182 | -e UPSTASH_REDIS_URL=your_redis_url \ 183 | -e UPSTASH_REDIS_TOKEN=your_redis_token \ 184 | ollama-monitor 185 | ``` 186 | 187 | Access the application at http://localhost:3000. 188 | 189 | ## Contribution Guidelines 190 | 191 | 1. Fork the project 192 | 2. Create a feature branch (`git checkout -b feature/AmazingFeature`) 193 | 3. Commit your changes (`git commit -m 'Add some AmazingFeature'`) 194 | 4. Push to the branch (`git push origin feature/AmazingFeature`) 195 | 5. Open a Pull Request 196 | 197 | ## License 198 | 199 | This project is open-sourced under the MIT License - see the [LICENSE](LICENSE) file for details. 200 | 201 | ## Author 202 | 203 | VincentKo (@forrany) - [GitHub](https://github.com/forrany) 204 | 205 | ## Disclaimer 206 | 207 | 1. This project is for security research and educational purposes only. 208 | 2. This project must not be used for any illegal purposes. 209 | 3. The author is not responsible for any loss caused by the use of this project. 210 | 4. Data is sourced from the internet. If there is any infringement, please contact the author to delete it. 211 | 212 | 213 | ## Star History 214 | 215 | [![Star History Chart](https://api.star-history.com/svg?repos=forrany/Awesome-Ollama-Server&type=Date)](https://star-history.com/#forrany/Awesome-Ollama-Server&Date) 216 | -------------------------------------------------------------------------------- /src/components/ModelTestModal.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useRef } from 'react'; 2 | import { useTranslations } from 'next-intl'; 3 | import { Modal } from './Modal'; 4 | import { BeakerIcon, StopIcon } from '@heroicons/react/24/outline'; 5 | 6 | interface ModelTestModalProps { 7 | isOpen: boolean; 8 | onClose: () => void; 9 | server: string; 10 | models: string[]; 11 | } 12 | 13 | export function ModelTestModal({ isOpen, onClose, server, models }: ModelTestModalProps) { 14 | const t = useTranslations(); 15 | const [selectedModel, setSelectedModel] = useState(models[0] || ''); 16 | const [prompt, setPrompt] = useState(''); 17 | const [response, setResponse] = useState(''); 18 | const [isGenerating, setIsGenerating] = useState(false); 19 | const abortControllerRef = useRef(null); 20 | const responseEndRef = useRef(null); 21 | const [isWating, setIsWating] = useState(false); 22 | 23 | // 自动滚动到底部 24 | const scrollToBottom = () => { 25 | if (responseEndRef.current) { 26 | responseEndRef.current.scrollIntoView({ behavior: 'smooth' }); 27 | } 28 | }; 29 | 30 | const handleTest = async () => { 31 | if (!selectedModel || !prompt) return; 32 | setIsWating(true); 33 | 34 | setIsGenerating(true); 35 | // 保留之前的响应,添加分隔符 36 | setResponse(''); 37 | 38 | try { 39 | abortControllerRef.current = new AbortController(); 40 | 41 | const response = await fetch('/api/generate', { 42 | method: 'POST', 43 | headers: { 44 | 'Content-Type': 'application/json', 45 | }, 46 | body: JSON.stringify({ 47 | server, 48 | model: selectedModel, 49 | prompt, 50 | }), 51 | signal: abortControllerRef.current.signal, 52 | }); 53 | 54 | if (!response.ok) { 55 | throw new Error('生成失败'); 56 | } 57 | 58 | const reader = response.body?.getReader(); 59 | if (!reader) return; 60 | 61 | while (true) { 62 | const { done, value } = await reader.read(); 63 | if (done) break; 64 | 65 | const text = new TextDecoder().decode(value); 66 | setIsWating(false); 67 | setResponse(prev => prev + text); 68 | scrollToBottom(); 69 | } 70 | } catch (error) { 71 | if (error instanceof Error && error.name === 'AbortError') { 72 | setResponse(prev => prev + '\n[已停止生成]'); 73 | return; 74 | } 75 | console.error('生成出错:', error); 76 | setResponse(prev => prev + '\n' + t('modelTest.error')); 77 | } finally { 78 | setIsGenerating(false); 79 | abortControllerRef.current = null; 80 | scrollToBottom(); 81 | } 82 | }; 83 | 84 | const handleStopGeneration = () => { 85 | if (abortControllerRef.current) { 86 | abortControllerRef.current.abort(); 87 | } 88 | }; 89 | 90 | return ( 91 | 96 |
97 |
98 | 101 | 111 |
112 | 113 |
114 | 117 |