├── .github └── workflows │ └── release.yml ├── .gitignore ├── LICENSE ├── README.md ├── assets ├── gmgn.ico └── react.svg ├── components.json ├── components ├── app │ ├── floating-menu.tsx │ ├── menu-item.tsx │ └── result-card.tsx └── ui │ ├── button.tsx │ ├── card.tsx │ ├── form.tsx │ ├── input.tsx │ ├── label.tsx │ ├── popover.tsx │ ├── scroll-area.tsx │ ├── select.tsx │ ├── separator.tsx │ ├── toast.tsx │ ├── toaster.tsx │ └── tooltip.tsx ├── entrypoints ├── background.ts ├── content │ ├── App.tsx │ └── index.tsx └── sidepanel │ ├── App.tsx │ ├── index.html │ └── main.tsx ├── globals.css ├── hooks ├── use-settings.ts ├── use-text-observer.tsx ├── use-text-selection.ts └── use-toast.ts ├── lib └── utils.ts ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── public ├── icon │ ├── 128.png │ ├── 16.png │ ├── 256.png │ ├── 32.png │ ├── 48.png │ ├── 64.png │ └── 96.png └── wxt.svg ├── services └── api.ts ├── tailwind.config.ts ├── tsconfig.json ├── types └── index.ts └── wxt.config.ts /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Build & Release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: '版本号 (例如: v1.0.0)' 8 | required: true 9 | type: string 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: write 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | with: 20 | token: ${{ secrets.GITHUB_TOKEN }} 21 | 22 | - name: Create Tag 23 | run: | 24 | git config --global user.email "github-actions[bot]@users.noreply.github.com" 25 | git config --global user.name "github-actions[bot]" 26 | git tag ${{ github.event.inputs.version }} 27 | git push origin ${{ github.event.inputs.version }} 28 | 29 | - name: Setup Node.js 30 | uses: actions/setup-node@v4 31 | with: 32 | node-version: '20' 33 | 34 | - name: Setup pnpm 35 | uses: pnpm/action-setup@v2 36 | with: 37 | version: latest 38 | 39 | - name: Install dependencies 40 | run: pnpm install 41 | 42 | - name: Build extension 43 | run: pnpm build 44 | 45 | - name: Create ZIP file 46 | run: | 47 | cd dist/chrome-mv3 48 | zip -r ../../pump-finder.zip * 49 | cd ../.. 50 | 51 | - name: Create Release 52 | id: create_release 53 | uses: softprops/action-gh-release@v1 54 | with: 55 | tag_name: ${{ github.event.inputs.version }} 56 | files: pump-finder.zip 57 | draft: false 58 | prerelease: false 59 | env: 60 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .output 12 | stats.html 13 | stats-*.json 14 | .wxt 15 | web-ext.config.ts 16 | 17 | # Editor directories and files 18 | .vscode/* 19 | !.vscode/extensions.json 20 | .idea 21 | .DS_Store 22 | *.suo 23 | *.ntvs* 24 | *.njsproj 25 | *.sln 26 | *.sw? 27 | dist 28 | @dist 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2015 hellokaton 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pump Finder - A Chrome Extension 🤖 2 | 3 | 一个基于 WXT 和 shadcn/ui 构建的 Chrome 扩展,帮助你在 Twitter 上快速检索相关pump代币。 4 | 5 | ## ✨ 功能 6 | 7 | - 🔍 自动识别 pump 地址,给出 GMGN 购买入口 8 | - 🔍 手动搜索关键词,给出 pump 相关代币 9 | 10 | ## 🛠️ 技术栈 11 | 12 | - **Framework**: React + TypeScript 13 | - **Extension Framework**: WXT (WebExtension Tools) 14 | - **UI Components**: shadcn/ui 15 | - **Styling**: Tailwind CSS 16 | - **Icons**: Lucide React 17 | - **State Management**: React Hooks 18 | - **API Integration**: OpenAI/Claude API 19 | 20 | ## 🚀 快速开始 21 | 22 | 1. Clone the repository: `git clone https://github.com/hellokaton/text-polish-chrome-extension.git` 23 | 2. Install dependencies: 24 | 25 | ```bash 26 | pnpm install 27 | ``` 28 | 29 | 3. Start development server: 30 | 31 | ```bash 32 | pnpm dev 33 | ``` 34 | 35 | ## 安装 36 | -------------------------------------------------------------------------------- /assets/gmgn.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chongqiangchen/pump-finder-extension/7c7679d6fb03e76060d2f003017498eb3ba4787f/assets/gmgn.ico -------------------------------------------------------------------------------- /assets/react.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } 22 | -------------------------------------------------------------------------------- /components/app/floating-menu.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Button } from "~/components/ui/button"; 3 | import { Wand2, Languages, Sparkles, Copy, Loader2 } from "lucide-react"; 4 | import type { Position } from "~/types"; 5 | import { MenuItem } from "./menu-item"; 6 | 7 | interface FloatingMenuProps { 8 | position: Position; 9 | showMenu: boolean; 10 | onButtonClick: (e: React.MouseEvent) => void; 11 | onPumpSearch: () => void; 12 | loading: boolean; 13 | } 14 | 15 | export const FloatingMenu: React.FC = ({ 16 | position, 17 | showMenu, 18 | onButtonClick, 19 | onPumpSearch, 20 | loading, 21 | }) => ( 22 |
31 |
32 | 40 | 41 | {showMenu && ( 42 |
43 | 44 | Pump 搜索 45 | 46 |
47 | )} 48 |
49 |
50 | ); 51 | -------------------------------------------------------------------------------- /components/app/menu-item.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { cn } from "~/lib/utils"; 3 | 4 | interface MenuItemProps { 5 | children: React.ReactNode; 6 | onClick: () => void; 7 | } 8 | 9 | export const MenuItem: React.FC = ({ children, onClick }) => ( 10 | 21 | ); 22 | -------------------------------------------------------------------------------- /components/app/result-card.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Card, 4 | CardContent, 5 | CardHeader, 6 | CardTitle, 7 | } from "~/components/ui/card.tsx"; 8 | import { Loader2 } from "lucide-react"; 9 | import { AdvancedSearchResponse, FuzzySearchResponse } from "~/types"; 10 | import { ScrollArea } from "~/components/ui/scroll-area"; 11 | import pumpIcon from "~/assets/gmgn.ico"; 12 | 13 | interface ResultCardProps { 14 | result?: Array<{ 15 | fuzzySearch?: FuzzySearchResponse; 16 | advancedSearch?: AdvancedSearchResponse["coin"]; 17 | }>; 18 | loading?: boolean; 19 | onClose: () => void; 20 | } 21 | 22 | const formatMarketCap = (value?: number | string) => { 23 | if (!value) return "N/A"; 24 | 25 | const num = typeof value === 'string' ? parseFloat(value) : value; 26 | 27 | if (num >= 1e9) { 28 | return `${(num / 1e9).toFixed(2)}B`; 29 | } else if (num >= 1e6) { 30 | return `${(num / 1e6).toFixed(2)}M`; 31 | } else if (num >= 1e3) { 32 | return `${(num / 1e3).toFixed(2)}K`; 33 | } 34 | 35 | return num.toFixed(2); 36 | }; 37 | 38 | export const ResultCard: React.FC = ({ 39 | result, 40 | loading = false, 41 | onClose, 42 | }) => { 43 | return ( 44 | 45 | 46 |
47 |
48 | 搜索结果 49 |
50 | 70 |
71 |
72 | 73 | {loading ? ( 74 |
75 | 76 |
77 | ) : ( 78 | 79 | {result?.map((item) => ( 80 |
84 |
85 | {item.fuzzySearch?.image_uri && ( 86 | {item.fuzzySearch?.name} 91 | )} 92 |
93 |

{item.fuzzySearch?.name}

94 |

95 | {item.fuzzySearch?.symbol} 96 |

97 |
98 |
99 | 100 |
101 |
102 | 市值 103 | 104 | ${formatMarketCap(item.fuzzySearch?.usd_market_cap)} 105 | 106 |
107 |
108 | 内盘状态 109 | {item.fuzzySearch?.complete ? ( 110 | 已完成 111 | ) : ( 112 | 113 | {item.advancedSearch?.progress || "未完成"} 114 | 115 | )} 116 |
117 |
118 | 119 | {item.fuzzySearch?.description && ( 120 |

121 | {item.fuzzySearch?.description} 122 |

123 | )} 124 | 125 |
126 | {item.fuzzySearch?.twitter && ( 127 | 132 | 140 | 144 | 145 | 146 | )} 147 | 152 | GMGN 153 | 154 |
155 |
156 | ))} 157 |
158 | )} 159 |
160 |
161 | ); 162 | }; 163 | -------------------------------------------------------------------------------- /components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "~/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2", 25 | sm: "h-8 rounded-md px-3 text-xs", 26 | lg: "h-10 rounded-md px-8", 27 | icon: "h-9 w-9", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | } 35 | ) 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button" 46 | return ( 47 | 52 | ) 53 | } 54 | ) 55 | Button.displayName = "Button" 56 | 57 | export { Button, buttonVariants } 58 | -------------------------------------------------------------------------------- /components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "~/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLDivElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |
41 | )) 42 | CardTitle.displayName = "CardTitle" 43 | 44 | const CardDescription = React.forwardRef< 45 | HTMLDivElement, 46 | React.HTMLAttributes 47 | >(({ className, ...props }, ref) => ( 48 |
53 | )) 54 | CardDescription.displayName = "CardDescription" 55 | 56 | const CardContent = React.forwardRef< 57 | HTMLDivElement, 58 | React.HTMLAttributes 59 | >(({ className, ...props }, ref) => ( 60 |
61 | )) 62 | CardContent.displayName = "CardContent" 63 | 64 | const CardFooter = React.forwardRef< 65 | HTMLDivElement, 66 | React.HTMLAttributes 67 | >(({ className, ...props }, ref) => ( 68 |
73 | )) 74 | CardFooter.displayName = "CardFooter" 75 | 76 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 77 | -------------------------------------------------------------------------------- /components/ui/form.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as LabelPrimitive from "@radix-ui/react-label" 3 | import { Slot } from "@radix-ui/react-slot" 4 | import { 5 | Controller, 6 | ControllerProps, 7 | FieldPath, 8 | FieldValues, 9 | FormProvider, 10 | useFormContext, 11 | } from "react-hook-form" 12 | 13 | import { cn } from "~/lib/utils" 14 | import { Label } from "~/components/ui/label" 15 | 16 | const Form = FormProvider 17 | 18 | type FormFieldContextValue< 19 | TFieldValues extends FieldValues = FieldValues, 20 | TName extends FieldPath = FieldPath 21 | > = { 22 | name: TName 23 | } 24 | 25 | const FormFieldContext = React.createContext( 26 | {} as FormFieldContextValue 27 | ) 28 | 29 | const FormField = < 30 | TFieldValues extends FieldValues = FieldValues, 31 | TName extends FieldPath = FieldPath 32 | >({ 33 | ...props 34 | }: ControllerProps) => { 35 | return ( 36 | 37 | 38 | 39 | ) 40 | } 41 | 42 | const useFormField = () => { 43 | const fieldContext = React.useContext(FormFieldContext) 44 | const itemContext = React.useContext(FormItemContext) 45 | const { getFieldState, formState } = useFormContext() 46 | 47 | const fieldState = getFieldState(fieldContext.name, formState) 48 | 49 | if (!fieldContext) { 50 | throw new Error("useFormField should be used within ") 51 | } 52 | 53 | const { id } = itemContext 54 | 55 | return { 56 | id, 57 | name: fieldContext.name, 58 | formItemId: `${id}-form-item`, 59 | formDescriptionId: `${id}-form-item-description`, 60 | formMessageId: `${id}-form-item-message`, 61 | ...fieldState, 62 | } 63 | } 64 | 65 | type FormItemContextValue = { 66 | id: string 67 | } 68 | 69 | const FormItemContext = React.createContext( 70 | {} as FormItemContextValue 71 | ) 72 | 73 | const FormItem = React.forwardRef< 74 | HTMLDivElement, 75 | React.HTMLAttributes 76 | >(({ className, ...props }, ref) => { 77 | const id = React.useId() 78 | 79 | return ( 80 | 81 |
82 | 83 | ) 84 | }) 85 | FormItem.displayName = "FormItem" 86 | 87 | const FormLabel = React.forwardRef< 88 | React.ElementRef, 89 | React.ComponentPropsWithoutRef 90 | >(({ className, ...props }, ref) => { 91 | const { error, formItemId } = useFormField() 92 | 93 | return ( 94 |