├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .npmrc ├── .nvmrc ├── .prettierignore ├── .prettierrc.js ├── .stylelintrc.json ├── .vscode ├── extensions.json ├── import-sorter.json └── settings.json ├── LICENSE ├── README.md ├── README.zh-CN.md ├── babel.config.js ├── bk_manifest.json ├── package.json ├── pnpm-lock.yaml ├── public ├── chat.html ├── doc │ ├── install.md │ ├── install.zh-CN.md │ ├── network.md │ ├── use.md │ └── use.zh-CN.md ├── icons │ ├── icon128.png │ ├── icon16.png │ ├── icon24.png │ ├── icon32.png │ └── icon48.png ├── install.html ├── options.html ├── popup.html └── sidepanel.html ├── scripts └── syncTranslations.ts ├── server ├── build.ts ├── client │ ├── allTabClient.ts │ └── backgroundClient.ts ├── configs │ ├── proxy.ts │ ├── webpack.common.ts │ ├── webpack.dev.ts │ └── webpack.prod.ts ├── generateManifest.ts ├── middlewares │ ├── extensionAutoReload.ts │ ├── index.ts │ ├── proxyMiddleware.ts │ └── webpackMiddleware.ts ├── start.ts ├── tsconfig.json ├── typings │ ├── global.d.ts │ └── modules.d.ts ├── utils │ ├── args.ts │ ├── constants.ts │ ├── entry.ts │ ├── exec.ts │ ├── fs.ts │ ├── getPort.ts │ └── path.ts └── watcher.ts ├── source ├── READMD-zh.md └── README.md ├── src ├── assets │ └── providers │ │ ├── 360.png │ │ ├── DMXAPI.png │ │ ├── adept.png │ │ ├── ai21.png │ │ ├── aihubmix.png │ │ ├── aimass.png │ │ ├── aisingapore.png │ │ ├── alayanew.png │ │ ├── anthropic.png │ │ ├── baichuan.png │ │ ├── baidu-cloud.svg │ │ ├── bailian.png │ │ ├── bge.webp │ │ ├── bigcode.png │ │ ├── bytedance.png │ │ ├── chatglm.png │ │ ├── chatgpt.jpeg │ │ ├── claude.png │ │ ├── codegeex.png │ │ ├── codestral.png │ │ ├── cohere.png │ │ ├── cohere.webp │ │ ├── copilot.png │ │ ├── dalle.png │ │ ├── dashscope.png │ │ ├── dbrx.png │ │ ├── deepseek.png │ │ ├── dianxin.png │ │ ├── doubao.png │ │ ├── embedding.png │ │ ├── fireworks.png │ │ ├── flashaudio.png │ │ ├── flux.png │ │ ├── gemini.png │ │ ├── gemma.png │ │ ├── gitee-ai.png │ │ ├── github.png │ │ ├── google.png │ │ ├── gpt_3.5.png │ │ ├── gpt_4.png │ │ ├── gpt_o1.png │ │ ├── gpustack.svg │ │ ├── graph-rag.png │ │ ├── grok.png │ │ ├── groq.png │ │ ├── gryphe.png │ │ ├── hailuo.png │ │ ├── huggingface.png │ │ ├── hunyuan.png │ │ ├── hyperbolic.png │ │ ├── ibm.png │ │ ├── infini.png │ │ ├── internlm.png │ │ ├── internvl.png │ │ ├── jina.png │ │ ├── keling.png │ │ ├── lepton.png │ │ ├── llama.png │ │ ├── llava.png │ │ ├── lmstudio.png │ │ ├── luma.png │ │ ├── magic.png │ │ ├── mediatek.png │ │ ├── microsoft.png │ │ ├── midjourney.png │ │ ├── minicpm.webp │ │ ├── minimax.png │ │ ├── mistral.png │ │ ├── mixedbread.png │ │ ├── mixtral.png │ │ ├── modelscope.png │ │ ├── moonshot.png │ │ ├── nousresearch.png │ │ ├── nvidia.png │ │ ├── o3.png │ │ ├── ocoolai.png │ │ ├── ollama.png │ │ ├── openai.jpeg │ │ ├── openai.png │ │ ├── openrouter.png │ │ ├── palm.png │ │ ├── perplexity.png │ │ ├── pixtral.png │ │ ├── ppio.png │ │ ├── qiniu.png │ │ ├── qwen.png │ │ ├── qwenlm.png │ │ ├── rakutenai.png │ │ ├── silicon.png │ │ ├── sparkdesk.png │ │ ├── stability.png │ │ ├── step.png │ │ ├── suno.png │ │ ├── tele.png │ │ ├── tencent-cloud-ti.png │ │ ├── together.png │ │ ├── upstage.png │ │ ├── vidu.png │ │ ├── volcengine.png │ │ ├── voyageai.png │ │ ├── wenxin.png │ │ ├── xirang.png │ │ ├── yi.png │ │ ├── zero-one.png │ │ └── zhipu.png ├── background │ ├── index.ts │ └── search.ts ├── chat │ ├── App.scss │ ├── App.tsx │ ├── components │ │ ├── ChatBody │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── CodeBlockView │ │ │ ├── MermaidView.scss │ │ │ ├── MermaidView.tsx │ │ │ ├── ThinkingView.scss │ │ │ ├── ThinkingView.tsx │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── MessageGroup │ │ │ ├── MessageRenderer.tsx │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── MessageList │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── Robot │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ └── Topic │ │ │ ├── index.scss │ │ │ └── index.tsx │ ├── hooks │ │ ├── useMessageOperations.ts │ │ ├── useMessageRenderer.ts │ │ ├── useMessageSender.ts │ │ └── usePromptSuggestions.ts │ └── index.tsx ├── config │ ├── models.ts │ ├── providers.ts │ ├── robot.ts │ └── robotListData.json ├── contents │ └── all │ │ ├── components │ │ ├── ChatControls │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── ChatInterface │ │ │ ├── index.scss │ │ │ ├── index.tsx │ │ │ └── promptSuggestions.css │ │ ├── ChatWindow │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── Config │ │ │ └── index.tsx │ │ ├── IframeSidePanel.scss │ │ ├── IframeSidePanel.tsx │ │ └── Think │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── index.tsx │ │ ├── style.scss │ │ └── styles │ │ ├── animations.css │ │ ├── animations.scss │ │ └── highlight.css ├── contexts │ ├── LanguageContext.tsx │ └── PositionContext.tsx ├── db │ └── index.ts ├── hooks │ └── useChatMessages.ts ├── index.tsx ├── install │ ├── App.scss │ ├── App.tsx │ └── index.tsx ├── llmProviders │ ├── AiProvider.ts │ ├── BaseLlmProvider.ts │ ├── LlmProviderFactory.ts │ ├── OpenAiLlmProvider.ts │ └── utils.ts ├── locales │ ├── de.ts │ ├── en.ts │ ├── es.ts │ ├── fr.ts │ ├── i18n.ts │ ├── index.ts │ ├── ja.ts │ ├── ko.ts │ ├── ru.ts │ ├── translationKeys.ts │ ├── zh-CN.ts │ └── zh-TW.ts ├── main.tsx ├── manifest.ts ├── options │ ├── App.scss │ ├── App.tsx │ ├── components │ │ ├── About │ │ │ └── index.tsx │ │ ├── ApiSettings │ │ │ └── index.tsx │ │ ├── Footer │ │ │ └── index.tsx │ │ ├── Interface │ │ │ └── index.tsx │ │ ├── Logger │ │ │ └── index.tsx │ │ └── Search │ │ │ └── index.tsx │ └── index.tsx ├── popup │ ├── App.scss │ ├── App.tsx │ └── index.tsx ├── services │ ├── AiService.ts │ ├── MessageService.ts │ ├── ModelMessageService.ts │ ├── RobotService.ts │ ├── StreamProcessingService.ts │ ├── chatService.ts │ ├── index.ts │ └── localChatService.ts ├── sidepanel │ ├── SidePanel.scss │ ├── SidePanel.tsx │ └── index.tsx ├── store │ ├── MessageBlockStore.ts │ ├── MessageStore.ts │ ├── chromeStorageAdapter.ts │ ├── index.ts │ ├── llm.ts │ ├── messageThunk.ts │ └── robot.ts ├── styles │ ├── reset.scss │ └── sidepanel.scss ├── tsconfig.json ├── types │ ├── chunk.ts │ ├── images.d.ts │ ├── index.d.ts │ ├── index.ts │ ├── message.ts │ └── messageBlock.ts └── utils │ ├── constant.ts │ ├── error.ts │ ├── featureSettings.ts │ ├── index.ts │ ├── indexedDBStorage.ts │ ├── logger.ts │ ├── markdownRenderer.ts │ ├── memoryOptimization.ts │ ├── message │ ├── create.ts │ ├── filters.ts │ └── find.ts │ ├── messageUtils.ts │ ├── performance.ts │ ├── queue.ts │ ├── reactOptimizations.ts │ ├── robotUtils.ts │ ├── searchService.ts │ ├── storage.ts │ └── webContentExtractor.ts └── webpack.config.js /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | extension 3 | server/client/react-devtools.js -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const OFF = 0; 2 | 3 | module.exports = { 4 | extends: '@yutengjing/eslint-config-react', 5 | rules: { 6 | 'import/default': OFF, 7 | '@typescript-eslint/ban-ts-comment': OFF, 8 | 'unicorn/consistent-destructuring': OFF, 9 | '@typescript-eslint/no-use-before-define': OFF, 10 | 'import/no-extraneous-dependencies': OFF, 11 | 'import/no-unresolved': OFF, 12 | 'no-use-before-define': OFF, 13 | 'unicorn/prefer-query-selector': OFF, 14 | 'unicorn/prefer-node-remove': OFF, 15 | 'unicorn/consistent-function-scoping': OFF, 16 | 'jsx-a11y/no-static-element-interactions': OFF, 17 | 'jsx-a11y/click-events-have-key-events': OFF, 18 | '@typescript-eslint/prefer-ts-expect-error': OFF, 19 | 'unicorn/no-negated-condition': OFF, 20 | 'object-shorthand': OFF, 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | cache-and-install: 9 | runs-on: macos-latest 10 | 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v3 14 | 15 | - name: Install Node.js 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version: 16 19 | 20 | - uses: pnpm/action-setup@v2.0.1 21 | name: Install pnpm 22 | id: pnpm-install 23 | with: 24 | version: 7 25 | run_install: false 26 | 27 | - name: Get pnpm store directory 28 | id: pnpm-cache 29 | run: | 30 | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT 31 | 32 | - uses: actions/cache@v3 33 | name: Setup pnpm cache 34 | with: 35 | path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} 36 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 37 | restore-keys: | 38 | ${{ runner.os }}-pnpm-store- 39 | 40 | - name: Install dependencies 41 | run: pnpm install 42 | 43 | - name: Build in production mode 44 | run: pnpm build -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # custom 2 | extension 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | 12 | # Diagnostic reports (https://nodejs.org/api/report.html) 13 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 14 | 15 | # Runtime data 16 | pids 17 | *.pid 18 | *.seed 19 | *.pid.lock 20 | 21 | # Directory for instrumented libs generated by jscoverage/JSCover 22 | lib-cov 23 | 24 | # Coverage directory used by tools like istanbul 25 | coverage 26 | *.lcov 27 | 28 | # nyc test coverage 29 | .nyc_output 30 | 31 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 32 | .grunt 33 | 34 | # Bower dependency directory (https://bower.io/) 35 | bower_components 36 | 37 | # node-waf configuration 38 | .lock-wscript 39 | 40 | # Compiled binary addons (https://nodejs.org/api/addons.html) 41 | build/Release 42 | 43 | # Dependency directories 44 | node_modules/ 45 | jspm_packages/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # Windows thumbnail cache files 107 | Thumbs.db 108 | Thumbs.db:encryptable 109 | ehthumbs.db 110 | ehthumbs_vista.db 111 | 112 | # Dump file 113 | *.stackdump 114 | 115 | # Folder config file 116 | [Dd]esktop.ini 117 | 118 | # Recycle Bin used on file shares 119 | $RECYCLE.BIN/ 120 | 121 | # Windows Installer files 122 | *.cab 123 | *.msi 124 | *.msix 125 | *.msm 126 | *.msp 127 | 128 | # Windows shortcuts 129 | *.lnk 130 | 131 | # General 132 | .DS_Store 133 | .AppleDouble 134 | .LSOverride 135 | 136 | # Thumbnails 137 | ._* 138 | 139 | # Files that might appear in the root of a volume 140 | .DocumentRevisions-V100 141 | .fseventsd 142 | .Spotlight-V100 143 | .TemporaryItems 144 | .Trashes 145 | .VolumeIcon.icns 146 | .com.apple.timemachine.donotpresent 147 | 148 | # Directories potentially created on remote AFP share 149 | .AppleDB 150 | .AppleDesktop 151 | Network Trash Folder 152 | Temporary Items 153 | .apdisk 154 | 155 | # Swap 156 | [._]*.s[a-v][a-z] 157 | !*.svg # comment out if you don't need vector files 158 | [._]*.sw[a-p] 159 | [._]s[a-rt-v][a-z] 160 | [._]ss[a-gi-z] 161 | [._]sw[a-p] 162 | 163 | # Session 164 | Session.vim 165 | Sessionx.vim 166 | 167 | # Temporary 168 | .netrwhist 169 | *~ 170 | # Auto-generated tag files 171 | tags 172 | # Persistent undo 173 | [._]*.un~ 174 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact=true 2 | registry="https://registry.npmmirror.com/" 3 | sass_binary_site=https://registry.npmmirror.com/mirrors/node-sass/ 4 | ELECTRON_MIRROR=http://registry.npmmirror.com/mirrors/electron/ -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 18.12.1 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | extension 3 | server/client/react-devtools.js -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@yutengjing/prettier-config'); 2 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "stylelint-config-standard", 4 | "stylelint-config-prettier", 5 | "stylelint-config-recess-order" 6 | ], 7 | "plugins": ["stylelint-declaration-block-no-ignored-properties", "stylelint-scss"], 8 | "rules": { 9 | "comment-empty-line-before": null, 10 | "function-name-case": "lower", 11 | "no-invalid-double-slash-comments": null, 12 | "no-descending-specificity": null, 13 | "declaration-empty-line-before": null 14 | }, 15 | "overrides": [ 16 | { 17 | "files": ["**/*.scss"], 18 | "customSyntax": "postcss-scss" 19 | }, 20 | { 21 | "files": ["**/*.less"], 22 | "customSyntax": "postcss-less" 23 | } 24 | ], 25 | "ignoreFiles": ["node_modules/**/*", "extension/**/*", "public/**/*", "**/*.d.ts"] 26 | } 27 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | // https://gist.github.com/tjx666/daa6317cf80ab5f467c50b2693527875 2 | { 3 | "recommendations": [ 4 | "dbaeumer.vscode-eslint", 5 | "esbenp.prettier-vscode", 6 | "stylelint.vscode-stylelint", 7 | "mrmlnc.vscode-scss", 8 | "dozerg.tsimportsorter", 9 | "yutengjing.open-in-external-app", 10 | "yutengjing.vscode-fe-helper", 11 | "YuTengjing.vscode-archive" 12 | ], 13 | "unwantedRecommendations": [ 14 | "hookyqr.beautify", 15 | "ms-vscode.vscode-typescript-tslint-plugin", 16 | "dbaeumer.jshint" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.vscode/import-sorter.json: -------------------------------------------------------------------------------- 1 | { 2 | "wrappingStyle": "prettier", 3 | "nodeProtocol": "always", 4 | "groupRules": [ 5 | [{ "builtin": true }, "^[@][^/]", {}], 6 | [ 7 | "^@(/|$)", 8 | "^actions?(/|$)", 9 | "^apis?(/|$)", 10 | "^assets(/|$)", 11 | "^components?(/|$)", 12 | "^features?(/|$)", 13 | "^helpers?(/|$)", 14 | "^pages?(/|$)", 15 | "^sagas?(/|$)", 16 | "^selectors?(/|$)", 17 | "^styles?(/|$)", 18 | "^reducers?(/|$)", 19 | "^types?(/|$)", 20 | "^utils(/|$)" 21 | ], 22 | { "flags": "named", "regex": "^[.]" }, 23 | [{ "flags": "scripts" }, { "flags": "scripts", "regex": "[.]((css)|(less)|(scss))$" }] 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "./node_modules/typescript/lib", 3 | // stylelint 扩展自身的校验就够了 4 | "stylelint.validate": [ 5 | "css", 6 | "less", 7 | "scss" 8 | ], 9 | "css.validate": false, 10 | "less.validate": false, 11 | "scss.validate": false, 12 | "json.schemas": [ 13 | { 14 | "fileMatch": [ 15 | "/**/src/manifest.dev.json", 16 | "/**/src/manifest.prod.json" 17 | ], 18 | "url": "http://json.schemastore.org/chrome-manifest" 19 | } 20 | ], 21 | "search.exclude": { 22 | "**/node_modules": true, 23 | "extension": true, 24 | "yarn.lock": true 25 | }, 26 | "files.watcherExclude": { 27 | "**/.git/objects/**": true, 28 | "**/.git/subtree-cache/**": true, 29 | "**/node_modules/**": true, 30 | "**/extension/**": true 31 | }, 32 | "[javascript][javascriptreact][typescript][typescriptreact][json][jsonc]": { 33 | "editor.defaultFormatter": "esbenp.prettier-vscode" 34 | }, 35 | "[html][css][less][scss][markdown][xml][yaml][svg]": { 36 | "editor.defaultFormatter": "esbenp.prettier-vscode" 37 | }, 38 | "cSpell.words": [ 39 | "aihubmix", 40 | "alayanew", 41 | "Aliyun", 42 | "baichuan", 43 | "bailian", 44 | "Bytedance", 45 | "deepseek", 46 | "dexie", 47 | "DMXAPI", 48 | "gitee", 49 | "gpustack", 50 | "groq", 51 | "hoverable", 52 | "hunyuan", 53 | "infini", 54 | "jina", 55 | "lmstudio", 56 | "millsec", 57 | "modelscope", 58 | "Nemotron", 59 | "ocoolai", 60 | "Ollama", 61 | "openrouter", 62 | "Persistable", 63 | "ppio", 64 | "qiniu", 65 | "Qwen", 66 | "Tavily", 67 | "uuidv", 68 | "volcengine", 69 | "voyageai", 70 | "xirang", 71 | "Zhinao", 72 | "zhipu" 73 | ] 74 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 YuTengjing 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.zh-CN.md: -------------------------------------------------------------------------------- 1 | # DeepSeekAllSupports - DeepSeek 网页助手 2 | 3 |
4 | 5 | DeepSeekAllSupports 6 | 7 |
8 | 9 | ## 📖 简介 10 | 11 | DeepSeekAllSupports 是一款免费开源的浏览器扩展,支持 [DeepSeek](https://deepseek.com) 及其多平台服务,包括 DeepSeek 官方、硅基流动、腾讯云、百度云、阿里云、本地大模型等。无论您使用哪家服务商,DeepSeekAllSupports 都能帮助您 无缝集成,轻松调用 DeepSeek 强大的 AI 能力,为您的工作和研究提供高效支持。 12 | 13 | ## 开源插件支持服务商 14 | 15 | 该插件兼容多个 DeepSeek API 提供商,包括: 16 | 17 | > - [DeepSeek](https://deepseek.com) 官方 API 18 | > - [硅基流动](https://cloud.siliconflow.cn/i/lStn36vH) DeepSeek API 19 | > - [腾讯云](https://cloud.tencent.com/document/product/1772/115969) DeepSeek API 20 | > - [百度云](https://console.bce.baidu.com/iam/#/iam/apikey/list) DeepSeek API 21 | > - [阿里云](https://bailian.console.aliyun.com/?apiKey=1#/api-key) DeepSeek API 22 | > - [本地](https://ollama.com/) DeepSeek API 23 | 24 | 🔜 未来计划支持更多服务商:科大讯飞、OpenRoute、字节跳动火山引擎等。 25 | 26 | ## 开源插件核心特性 27 | 28 | ### 智能交互 29 | 30 | - **智能文本分析**: 可在网页任意位置选中文本,实时获取 AI 分析与回复。 31 | - **多轮对话**: 支持上下文连续对话,提供更自然的交互体验。 32 | - **流式响应**: AI 实时加载回复,增强交互流畅度。 33 | - **多模型支持**: 自由切换 DeepSeek V3、DeepSeek R1,个性化体验不同模型能力。 34 | - **多 API 提供商集成**: 兼容多家云服务 API,随时切换,确保稳定可靠。 35 | - **自由调整窗口**: 支持全局拖拽、大小调整(右下角边缘)、固定窗口位置,适配不同使用场景。 36 | - **支持本地部署模型**: 可连接本地 Ollama 模型,无需网络,流畅使用 AI。 37 | - **快捷键操作**: 一键唤起插件,提升使用效率。 38 | - **自定义快捷键**: 支持个性化快捷键配置,满足不同用户习惯。 39 | - **一键复制 & 重新生成**: 快速复制 AI 回复,支持内容重新生成。 40 | - **支持中途停止**: 可随时终止 AI 回答,灵活掌控交互节奏。 41 | - **本地大模型联网能力**: 支持本地的大模型进行联网能力,让需要隐私的同时,还能够通过联网获取最新的信息综合进行回答。 42 | - **Prompt 能力**: 常见的 prompt 能力快捷支持,进一步降低使用成本 43 | - **独立窗口能力**: 可以将大模型的页面独立窗口打开,方便使用 44 | 45 | ### 内容展示 46 | 47 | - **Markdown 渲染**: 支持代码块、列表、数学公式(MathJax)等格式,增强阅读体验。 48 | - **代码高亮**: 支持多种编程语言语法高亮。 49 | - **代码 & 公式复制**: 支持多种编程语言和公式的单独复制。 50 | 51 | ## 即将支持 52 | 53 | 54 | ## 接下来还将支持 55 | ### ✅ 一键总结页面 56 | 支持一键总结页面,生成页面的总结、摘要、思维导图等 57 | ### ✅ 一键翻译 58 | 支持右键一键翻译文章和文本### ✅ 一键多模型 59 | 支持将多个模型共同回答同一个问题 60 | ### ✅ 代码审查 61 | 支持在 gitlab、github 等平台进行代码审查 62 | ### ✅ 问题并发能力 63 | 一个问题可以问多个服务商和多个大模型,来综合进行比对,并且每个服务商和模型单独窗口,可以随意拖拽和关闭任何其中一个 64 | ### ✅ 更多功能 期待你的建议,欢迎联系我 65 | 66 | ## 如何安装 67 | 68 | [安装文档](./public/doc/install.zh-CN.md) 69 | 70 | ## 如何使用 71 | 72 | [使用文档](./public/doc/use.zh-CN.md) 73 | 74 | ## 贡献指南 75 | 76 | ## 贡献指南 77 | 78 | 欢迎所有形式的贡献,无论是新功能、bug 修复还是文档改进。 79 | 80 | 1. Fork 本仓库 81 | 2. 创建您的特性分支 (`git checkout -b feature/AmazingFeature`) 82 | 3. 提交您的更改 (`git commit -m 'Add some AmazingFeature'`) 83 | 4. 推送到分支 (`git push origin feature/AmazingFeature`) 84 | 5. 开启一个 Pull Request 85 | 86 | ## 许可证 87 | 88 | 本项目采用 MIT 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情 89 | 90 | ## 联系我 91 | 92 | - 项目问题: [GitHub Issues](https://github.com/wjszxli/DeepSeekAllSupports/issues) 93 | - 邮件联系: [wjszxli@gmail.com] 94 | 95 | --- 96 | 97 |
98 | 如果这个项目对您有帮助,请考虑给它一个 ⭐️ 99 |
100 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = (api) => { 2 | api.cache(true); 3 | 4 | const envPreset = [ 5 | '@babel/env', 6 | { 7 | modules: false, 8 | bugfixes: true, 9 | useBuiltIns: 'usage', 10 | corejs: { version: require('./package.json').devDependencies['core-js'] }, 11 | }, 12 | ]; 13 | 14 | const importPlugin = [ 15 | 'import', 16 | { 17 | libraryName: 'antd', 18 | libraryDirectory: 'es', 19 | style: true, 20 | }, 21 | ]; 22 | 23 | return { 24 | presets: ['@babel/preset-env', '@babel/preset-react', '@babel/preset-typescript'], 25 | plugins: [ 26 | '@babel/plugin-transform-class-properties', 27 | '@babel/plugin-transform-private-methods', 28 | '@babel/plugin-transform-private-property-in-object', 29 | ['@babel/plugin-transform-runtime', { regenerator: true }], 30 | ['@babel/plugin-proposal-decorators', { decoratorsBeforeExport: true }], 31 | 'lodash', 32 | importPlugin, 33 | ], 34 | env: { 35 | development: { 36 | presets: [['@babel/preset-react', { runtime: 'automatic', development: true }]], 37 | plugins: [require.resolve('react-refresh/babel')], 38 | }, 39 | production: { 40 | presets: [['@babel/preset-react', { runtime: 'automatic', development: false }]], 41 | plugins: ['@babel/plugin-transform-react-constant-elements'], 42 | }, 43 | }, 44 | }; 45 | }; 46 | -------------------------------------------------------------------------------- /bk_manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "DeepSeekAllSupports", 3 | "version": "1.0.1", 4 | "description": "suport all servers in deepseek", 5 | "manifest_version": 3, 6 | "minimum_chrome_version": "96", 7 | "permissions": ["storage", "declarativeNetRequest", "contextMenus", "commands"], 8 | "host_permissions": ["https://*/*", "http://*/*"], 9 | "content_security_policy": { 10 | "extension_pages": "script-src 'self' http://localhost; object-src 'self';" 11 | }, 12 | "web_accessible_resources": [ 13 | { 14 | "matches": [""], 15 | "resources": ["icons/*", "images/*", "fonts/*"] 16 | } 17 | ], 18 | "background": { 19 | "scripts": ["js/background.js"] 20 | }, 21 | "content_scripts": [ 22 | { 23 | "matches": [""], 24 | "js": ["js/vendor.js"] 25 | }, 26 | { 27 | "matches": [""], 28 | "css": ["css/all.css"], 29 | "js": ["js/all.js", "js/all.js"] 30 | } 31 | ], 32 | "action": { 33 | "default_popup": "popup.html", 34 | "default_icon": { 35 | "16": "icons/icon16.png", 36 | "32": "icons/icon32.png", 37 | "48": "icons/icon48.png", 38 | "128": "icons/icon128.png" 39 | } 40 | }, 41 | "options_ui": { 42 | "page": "options.html", 43 | "open_in_tab": true 44 | }, 45 | "icons": { 46 | "16": "icons/icon16.png", 47 | "32": "icons/icon32.png", 48 | "48": "icons/icon48.png", 49 | "128": "icons/icon128.png" 50 | }, 51 | "commands": { 52 | "open-chat": { 53 | "suggested_key": { 54 | "default": "Ctrl+Shift+Y", 55 | "mac": "Command+Shift+Y", 56 | "windows": "Ctrl+Shift+Y" 57 | }, 58 | "description": "打开 AI 聊天窗口." 59 | } 60 | }, 61 | "browser_specific_settings": { 62 | "gecko": { 63 | "id": "wjszxli@gmail.com", 64 | "strict_min_version": "58.0" 65 | }, 66 | "safari": { 67 | "strict_min_version": "14", 68 | "strict_max_version": "20" 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /public/chat.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /public/doc/install.md: -------------------------------------------------------------------------------- 1 | ## Installation Guide 2 | 3 | ## Chrome Web Store 4 | 5 | Click [here](https://chromewebstore.google.com/detail/deepseekallsupports/llogfbeeebfjkbmajodnjpljpfnaaplm?authuser=0&hl=zh-CN) to install directly or search for **"DeepSeekAllSupports"** in the [Chrome Web Store](https://chromewebstore.google.com/?hl=zh-CN&authuser=0) to install. 6 | 7 | ## Manual Installation (ZIP Package) 8 | 9 | 1. **Download the Plugin ZIP Package** 10 | 11 | - Visit [GitHub Releases](https://github.com/wjszxli/DeepSeekAllSupports/releases). 12 | - Download the latest version **DeepSeekAllSupports.v1.0.zip**, and extract it to a local folder (e.g., `D:\DeepSeekAllSupports`). 13 | - Ensure that the extracted folder structure is correct (refer to the image below). 14 | 15 | ![Directory Structure](https://files.mdnice.com/user/14956/906ec0b4-93e9-4f91-a5c5-3c3851f30ac0.png) 16 | 17 | 2. **Load the Plugin in Chrome** 18 | - Go to `chrome://extensions/` and enable **Developer mode**. 19 | - Click "**Load unpacked**" and select the extracted plugin folder. 20 | - Once installed, you can enable the extension in your browser. 21 | 22 | ## Install from Source Code 23 | 24 | If you want to compile the extension yourself, follow these steps: 25 | 26 | ```bash 27 | # Clone the repository 28 | git clone https://github.com/wjszxli/DeepSeekAllSupports.git 29 | 30 | # Install dependencies 31 | pnpm install 32 | 33 | # Build the project 34 | pnpm run build 35 | 36 | ``` 37 | Then, follow the **ZIP Package** Installation steps to manually load the extension from the extension directory in your browser. -------------------------------------------------------------------------------- /public/doc/install.zh-CN.md: -------------------------------------------------------------------------------- 1 | ## 安装指南 2 | 3 | ## Chrome 商店 4 | 5 | 点击 [这里](https://chromewebstore.google.com/detail/deepseekallsupports/llogfbeeebfjkbmajodnjpljpfnaaplm?authuser=0&hl=zh-CN) 直接安装或者在 [Chrome 扩展商店](https://chromewebstore.google.com/?hl=zh-CN&authuser=0)搜索 “DeepSeekAllSupports” 进行安装。 6 | 7 | ## 手动安装(zip 包) 8 | 9 | 1. **下载插件压缩包** 10 | 11 | - 访问 [GitHub Releases](https://github.com/wjszxli/DeepSeekAllSupports/releases)。 12 | - 下载最新版本的 DeepSeekAllSupports.v1.0.zip,解压至本地文件夹(如 D:\DeepSeekAllSupports)。 13 | - 请确保解压后的文件夹结构正确(参考下图)。 14 | 15 | ![目录结构](https://files.mdnice.com/user/14956/906ec0b4-93e9-4f91-a5c5-3c3851f30ac0.png) 16 | 17 | 2. **在 Chrome 浏览器中加载插件** 18 | - 访问 chrome://extensions/,启用**开发者模式**。 19 | - 点击 “**加载已解压的扩展程序**”,选择解压后的插件文件夹。 20 | - 安装完成后,即可在浏览器中启用插件。 21 | 22 | ## 源码安装 23 | 24 | 如果你希望自行编译插件,可以按照以下步骤进行: 25 | 26 | ```bash 27 | # 克隆项目 28 | git clone https://github.com/wjszxli/DeepSeekAllSupports.git 29 | 30 | # 安装依赖 31 | pnpm install 32 | 33 | # 构建项目 34 | pnpm run build 35 | ``` 36 | 37 | 然后按照 ZIP 包安装方式,在浏览器中手动加载 extension 目录中的扩展程序。 38 | -------------------------------------------------------------------------------- /public/doc/network.md: -------------------------------------------------------------------------------- 1 | # 大模型的网络搜索是如何进行 2 | -------------------------------------------------------------------------------- /public/doc/use.zh-CN.md: -------------------------------------------------------------------------------- 1 | ## 如何使用 2 | 3 | ### 确保插件已启用 4 | 5 | 1. Chrome 插件有个开关,如何关闭会无法使用插件 6 | 7 | - 在 Chrome 地址栏输入 chrome://extensions/,查找 DeepSeekAllSupports。 8 | - 确保插件处于 启用 状态,否则请手动开启, 参考下图。 9 | 10 | ![image](https://files.mdnice.com/user/14956/8254890c-6115-4444-a09b-7759693d3ce3.png) 11 | 12 | ### 选择 API 提供商 & 配置 API Key 13 | 14 | 1. **固定插件**:点击 Chrome 右上角 扩展程序按钮,找到 DeepSeekAllSupports 并固定到工具栏。 15 | 16 | ![image](https://files.mdnice.com/user/14956/38511b25-f47a-4d27-aac2-88b945f52a82.png) 17 | 18 | 2. **选择服务商**:点击插件图标,在弹出的界面选择 API 提供商。 19 | 3. **获取 API Key**: 20 | 21 | - 例如,服务商选择了“阿里云”,点击 “获取 API Key”,会跳转至 API Key 管理页面。 22 | 23 | ![image](https://files.mdnice.com/user/14956/54c3ee05-3a7c-42be-84c6-e7930468be4d.png) 24 | 25 | ![image](https://files.mdnice.com/user/14956/cc5bb0d6-9eba-4aad-b304-9afc25807fa6.png) 26 | 27 | - 在 API Key 页面点击 “创建 API Key”,填写必要信息后生成 API Key 28 | 29 | ![image](https://files.mdnice.com/user/14956/49bf383f-fcec-4a4a-ba38-d78b7c9a849b.png) 30 | 31 | 4. **填入 API Key**:复制 API Key 并粘贴到插件的 API Key 输入框 中,点击 “保存配置”。 32 | ![image](https://files.mdnice.com/user/14956/09fe006a-e53b-4baf-b0e7-887a588aee18.png) 33 | 5. 测试 API 连接:保存 API Key 后,插件会自动进行 API 连接测试,成功后即可使用。 34 | 35 | ![image](https://files.mdnice.com/user/14956/0808b080-157b-4631-a888-1b5627b8bc66.png) 36 | 37 | ![image](https://files.mdnice.com/user/14956/0c313ca4-5dbd-4141-874c-19614d18403d.png) 38 | 39 | ### 体验 AI 功能 40 | 41 | - **网页选中文本**:选中任意网页文本,点击 DeepSeek 图标,即可调起 AI 对话窗口。 42 | 43 | ![image](https://files.mdnice.com/user/14956/4201fc0e-3541-43fa-87b6-5a88cd4ffb64.png) 44 | 45 | ![image](https://files.mdnice.com/user/14956/3d6ac9bc-5d60-405e-abe0-967374ff367b.png) 46 | 47 | - **连续对话**:在对话窗口与 AI 进行多轮交流,获得流畅体验 48 | 49 | - **实时 AI 回复**:支持流式加载 AI 响应,提高交互效率。 50 | 51 | ![output](https://files.mdnice.com/user/14956/cbdf62b7-d3b2-4245-b801-49ccf267a946.gif) 52 | 53 | ### 🔍 体验 R1 模型 54 | 55 | 选择 DeepSeek R1 模型,可观察 AI 的推理过程,直观感受其思维逻辑。 56 | 57 | ![image](https://files.mdnice.com/user/14956/9219618d-ac17-4b86-8d83-54e1185c44f3.png) 58 | ![2](https://files.mdnice.com/user/14956/ee7dbbba-8e32-482a-a84a-117e24d77366.gif) 59 | 60 | ### 本地模型支持 61 | 62 | #### 跨平台安装指南 63 | 64 | 跨平台安装指南 Ollama 作为本地运行大模型的利器,支持三大主流操作系统: 65 | 66 | ``` 67 | # macOS一键安装 68 | # Windows用户 69 | 访问官网 https://ollama.com/download 下载安装包 70 | 71 | # Linux安装(Ubuntu/Debian为例) 72 | curl -fsSL https://ollama.com/install.sh | sudo bash 73 | sudo usermod -aG ollama $USER  # 添加用户权限 74 | sudo systemctl start ollama    # 启动服务 75 | ``` 76 | 77 | #### 服务验证 78 | 79 | ``` 80 | Ollama -v 81 | # 输出 ollama version is 0.5.11 82 | ``` 83 | 84 | 出现上述则表示安装成功,可浏览器访问 http://localhost:11434/验证 85 | 86 | #### 安装模型 87 | 88 | ``` 89 | # 安装模型,如 deepseek-r1:7b,具体可以参考:https://ollama.com/search 90 | ollama run deepseek-r1:7b 91 | ``` 92 | 93 | #### 配置本地模型 94 | 95 | 服务商选择本地 Ollama,模型选择你安装的模型,如下图所示 96 | 97 | ![image](https://files.mdnice.com/user/14956/aa56949a-ac4f-40c3-991d-6174e43b902a.png) 98 | 99 | ### 快捷键操作和自定义快捷键 100 | 101 | #### 全局打开聊天窗口 102 | 103 | 默认快捷 mac 上为 `Command+Shift+Y`, windows 为 `Ctrl+Shift+Y` 104 | 105 | #### 关闭聊天窗口 106 | 107 | 默认的快捷键为键盘右上角的 `Esc`,也可以点击聊天窗口右上角的 x 来关闭 108 | 109 | #### 自定义快捷键 110 | 111 | 可以点击设置的设置快捷,进入快捷键自定义设置 112 | ![image](https://files.mdnice.com/user/14956/3d87a401-7999-4cfb-9c90-ac50a005302b.png) 113 | 114 | ### 窗口可以自由调整 115 | 116 | 1. 鼠标浮到右下角,可以调整高度和宽度 117 | 2. 鼠标浮到顶部,可以拖拽窗口 118 | 3. 点击左上角 📍,可以固定窗口或者取消固定窗口 119 | ![2](https://files.mdnice.com/user/14956/b9fbcf60-9c91-4528-b292-c57252be62d1.gif) 120 | 121 | ## 总结 122 | 123 | DeepSeekAllSupports 是一款高效、便捷的 AI 插件,支持多个 DeepSeek API 提供商,让用户无需复杂的技术操作即可快速接入 AI 服务。无论是网页内容分析、多轮对话,还是代码高亮、Markdown 渲染,该插件都能提供一流的体验。 124 | 125 | **🚀 立即体验 DeepSeek 极速 AI,探索更多可能性!** 126 | -------------------------------------------------------------------------------- /public/icons/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/public/icons/icon128.png -------------------------------------------------------------------------------- /public/icons/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/public/icons/icon16.png -------------------------------------------------------------------------------- /public/icons/icon24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/public/icons/icon24.png -------------------------------------------------------------------------------- /public/icons/icon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/public/icons/icon32.png -------------------------------------------------------------------------------- /public/icons/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/public/icons/icon48.png -------------------------------------------------------------------------------- /public/install.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | <%= htmlWebpackPlugin.options.title %> 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /public/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /public/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | <%= htmlWebpackPlugin.options.title %> 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /public/sidepanel.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Chat Side Panel 7 | 8 | 9 |
10 | 11 | -------------------------------------------------------------------------------- /server/build.ts: -------------------------------------------------------------------------------- 1 | import console from 'consola'; 2 | import webpack from 'webpack'; 3 | 4 | import prodConfig from './configs/webpack.prod'; 5 | import generateManifest from './generateManifest'; 6 | import { ENABLE_ANALYZE } from './utils/constants'; 7 | 8 | function webpackBuild() { 9 | const compiler = webpack(prodConfig); 10 | return new Promise((resolve, reject) => { 11 | compiler.run((error, stats) => { 12 | if (error) { 13 | console.error(error); 14 | reject(error); 15 | return; 16 | } 17 | 18 | if (stats) { 19 | const { errors } = stats.toJson({ all: false, errors: true }); 20 | 21 | if (stats.hasErrors()) { 22 | console.error(errors); 23 | reject(new Error(JSON.stringify(errors, undefined, 4))); 24 | return; 25 | } 26 | 27 | const analyzeStatsOpts = { 28 | preset: 'normal', 29 | colors: true, 30 | }; 31 | console.log(stats.toString(ENABLE_ANALYZE ? analyzeStatsOpts : 'minimal')); 32 | } 33 | 34 | resolve(); 35 | }); 36 | }); 37 | } 38 | 39 | function main() { 40 | generateManifest(); 41 | webpackBuild(); 42 | } 43 | 44 | main(); 45 | -------------------------------------------------------------------------------- /server/client/allTabClient.ts: -------------------------------------------------------------------------------- 1 | chrome.runtime.onMessage.addListener((request, _sender, sendResp) => { 2 | const shouldRefresh = 3 | request.from === 'background' && request.action === 'refresh current page'; 4 | if (shouldRefresh) { 5 | sendResp({ from: 'content script', action: 'reload extension' }); 6 | // !: 等待扩展 reload 7 | setTimeout(() => window.location.reload(), 100); 8 | } 9 | }); 10 | 11 | export {}; 12 | -------------------------------------------------------------------------------- /server/client/backgroundClient.ts: -------------------------------------------------------------------------------- 1 | function logWithPrefix(info: string) { 2 | console.log(`[EAR] ${info}`); 3 | } 4 | 5 | // !: 如果服务器配置改了,这里也要做对应的修改 6 | const source = new EventSource('http://127.0.0.1:3600/__extension_auto_reload__'); 7 | 8 | source.addEventListener( 9 | 'open', 10 | () => { 11 | logWithPrefix('connected'); 12 | }, 13 | false, 14 | ); 15 | 16 | source.addEventListener( 17 | 'message', 18 | (event) => { 19 | logWithPrefix('received a no event name message, data:'); 20 | console.log(event.data); 21 | }, 22 | false, 23 | ); 24 | 25 | source.addEventListener( 26 | 'pause', 27 | () => { 28 | logWithPrefix('received pause message from server, ready to close connection!'); 29 | source.close(); 30 | }, 31 | false, 32 | ); 33 | 34 | source.addEventListener( 35 | 'compiled successfully', 36 | (event: EventSourceEvent) => { 37 | const shouldReload = 38 | JSON.parse(event.data).action === 'reload extension and refresh current page'; 39 | 40 | if (shouldReload) { 41 | logWithPrefix('received the signal to reload chrome extension'); 42 | chrome.tabs.query({}, (tabs) => { 43 | tabs.forEach((tab) => { 44 | if (tab.id) { 45 | let received = false; 46 | chrome.tabs.sendMessage( 47 | tab.id, 48 | { 49 | from: 'background', 50 | action: 'refresh current page', 51 | }, 52 | (res) => { 53 | if (chrome.runtime.lastError && !res) return; 54 | 55 | const { from, action } = res; 56 | if ( 57 | !received && 58 | from === 'content script' && 59 | action === 'reload extension' 60 | ) { 61 | received = true; 62 | source.close(); 63 | logWithPrefix('reload extension'); 64 | chrome.runtime.reload(); 65 | } 66 | }, 67 | ); 68 | } 69 | }); 70 | }); 71 | } 72 | }, 73 | false, 74 | ); 75 | 76 | source.addEventListener( 77 | 'error', 78 | (event: EventSourceEvent) => { 79 | if (event.target!.readyState === 0) { 80 | // ignore because too annoying 81 | // console.error('You need to open devServer first!'); 82 | } else { 83 | console.error(event); 84 | } 85 | }, 86 | false, 87 | ); 88 | 89 | export {}; 90 | -------------------------------------------------------------------------------- /server/configs/proxy.ts: -------------------------------------------------------------------------------- 1 | import type { Options } from 'http-proxy-middleware/dist/types'; 2 | 3 | export interface ProxyTable { 4 | [path: string]: Options; 5 | } 6 | 7 | const proxyTable: ProxyTable = { 8 | // '/path_to_be_proxy': { target: 'http://target.domain.com', changeOrigin: true }, 9 | }; 10 | 11 | export default proxyTable; 12 | -------------------------------------------------------------------------------- /server/configs/webpack.dev.ts: -------------------------------------------------------------------------------- 1 | import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin'; 2 | import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin'; 3 | import { HotModuleReplacementPlugin } from 'webpack'; 4 | import merge from 'webpack-merge'; 5 | 6 | import { resolveSrc } from '../utils/path'; 7 | import commonConfig from './webpack.common'; 8 | 9 | const devConfig = merge(commonConfig, { 10 | mode: 'development', 11 | devtool: 'source-map', 12 | plugins: [ 13 | new HotModuleReplacementPlugin(), 14 | new ReactRefreshWebpackPlugin({ 15 | overlay: { 16 | sockIntegration: 'whm', 17 | }, 18 | }), 19 | new ForkTsCheckerWebpackPlugin({ 20 | typescript: { 21 | memoryLimit: 1024, 22 | configFile: resolveSrc('tsconfig.json'), 23 | }, 24 | }), 25 | ], 26 | }); 27 | 28 | export default devConfig; 29 | -------------------------------------------------------------------------------- /server/configs/webpack.prod.ts: -------------------------------------------------------------------------------- 1 | import AntdDayjsWebpackPlugin from 'antd-dayjs-webpack-plugin'; 2 | import browserslist from 'browserslist'; 3 | import CssMinimizerPlugin from 'css-minimizer-webpack-plugin'; 4 | import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin'; 5 | import lightningCss from 'lightningcss'; 6 | import TerserPlugin from 'terser-webpack-plugin'; 7 | import webpack, { BannerPlugin } from 'webpack'; 8 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; 9 | import merge from 'webpack-merge'; 10 | 11 | import pkg from '../../package.json'; 12 | import { __DEV__, COPYRIGHT, ENABLE_ANALYZE } from '../utils/constants'; 13 | import { resolveSrc } from '../utils/path'; 14 | import commonConfig from './webpack.common'; 15 | 16 | const prodConfig = merge(commonConfig, { 17 | mode: 'production', 18 | plugins: [ 19 | new BannerPlugin({ 20 | banner: COPYRIGHT, 21 | raw: true, 22 | }), 23 | new ForkTsCheckerWebpackPlugin({ 24 | typescript: { 25 | memoryLimit: 1024 * 2, 26 | configFile: resolveSrc('tsconfig.json'), 27 | profile: ENABLE_ANALYZE, 28 | }, 29 | }), 30 | new webpack.ids.HashedModuleIdsPlugin({ 31 | hashFunction: 'sha256', 32 | hashDigest: 'hex', 33 | hashDigestLength: 20, 34 | }), 35 | new AntdDayjsWebpackPlugin(), 36 | ], 37 | optimization: { 38 | splitChunks: { 39 | cacheGroups: { 40 | vendor: { 41 | test: /[/\\]node_modules[/\\](react|react-dom)[/\\]/, 42 | name: 'vendor', 43 | chunks: 'all', 44 | }, 45 | }, 46 | }, 47 | minimize: true, 48 | minimizer: [ 49 | new TerserPlugin({ 50 | minify: TerserPlugin.swcMinify, 51 | parallel: true, 52 | extractComments: false, 53 | }), 54 | new CssMinimizerPlugin({ 55 | minify: CssMinimizerPlugin.lightningCssMinify, 56 | minimizerOptions: { 57 | // @ts-expect-error webpack type define wrong 58 | targets: lightningCss.browserslistToTargets(browserslist(pkg.browserslist)), 59 | preset: [ 60 | 'default', 61 | { 62 | discardComments: { removeAll: true }, 63 | }, 64 | ], 65 | }, 66 | }), 67 | ], 68 | }, 69 | }); 70 | 71 | if (ENABLE_ANALYZE) { 72 | prodConfig.plugins!.push(new BundleAnalyzerPlugin()); 73 | } 74 | 75 | export default prodConfig; 76 | -------------------------------------------------------------------------------- /server/generateManifest.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises'; 2 | import console from 'consola'; 3 | 4 | import manifest from '../src/manifest'; 5 | import { ensureDir } from './utils/fs'; 6 | import { resolveExtension } from './utils/path'; 7 | 8 | export default async function generateManifest() { 9 | console.info('updating manifest.json...'); 10 | await ensureDir(resolveExtension()); 11 | return fs.writeFile( 12 | resolveExtension('manifest.json'), 13 | JSON.stringify(manifest, null, 4), 14 | 'utf8', 15 | ); 16 | } 17 | 18 | if (module === require.main) { 19 | generateManifest(); 20 | } 21 | -------------------------------------------------------------------------------- /server/middlewares/extensionAutoReload.ts: -------------------------------------------------------------------------------- 1 | import console from 'consola'; 2 | import type { RequestHandler } from 'express'; 3 | import { debounce } from 'lodash'; 4 | import SSEStream from 'ssestream'; 5 | import type { Compiler, Stats } from 'webpack'; 6 | 7 | import { resolveSrc } from '../utils/path'; 8 | 9 | export default function extensionAutoReload(compiler: Compiler): RequestHandler { 10 | return (req, res, next) => { 11 | const sseStream = new SSEStream(req); 12 | sseStream.pipe(res); 13 | 14 | let closed = false; 15 | 16 | const compileDoneHook = debounce((stats: Stats) => { 17 | const { modules } = stats.toJson({ all: false, modules: true }); 18 | const updatedJsModules = modules?.filter( 19 | (module) => module.type === 'module' && module.moduleType === 'javascript/auto', 20 | ); 21 | const shouldReload = 22 | !stats.hasErrors() && 23 | updatedJsModules?.some((module) => 24 | module.nameForCondition?.startsWith(resolveSrc('contents')), 25 | ); 26 | if (shouldReload) { 27 | sseStream.write( 28 | { 29 | event: 'compiled successfully', 30 | data: { 31 | action: 'reload extension and refresh current page', 32 | }, 33 | }, 34 | 'utf8', 35 | (err) => { 36 | if (err) { 37 | console.error(err); 38 | } 39 | }, 40 | ); 41 | } 42 | }, 1000); 43 | 44 | // 断开链接后之前的 hook 就不要执行了 45 | const plugin = (stats: Stats) => { 46 | if (!closed) { 47 | compileDoneHook(stats); 48 | } 49 | }; 50 | compiler.hooks.done.tap('extension-auto-reload-plugin', plugin); 51 | 52 | res.on('close', () => { 53 | closed = true; 54 | sseStream.unpipe(res); 55 | }); 56 | 57 | next(); 58 | }; 59 | } 60 | -------------------------------------------------------------------------------- /server/middlewares/index.ts: -------------------------------------------------------------------------------- 1 | import cors from 'cors'; 2 | import type { Express } from 'express'; 3 | import type { Compiler } from 'webpack'; 4 | 5 | import { EXTENSION_AUTO_RELOAD_PATH } from '../utils/constants'; 6 | import extensionAutoReload from './extensionAutoReload'; 7 | import proxyMiddleware from './proxyMiddleware'; 8 | import webpackMiddleware from './webpackMiddleware'; 9 | 10 | export default function setupMiddlewares(devServer: Express, compiler: Compiler): void { 11 | proxyMiddleware(devServer); 12 | devServer.use(cors()); 13 | devServer.use(webpackMiddleware(compiler)); 14 | devServer.use(EXTENSION_AUTO_RELOAD_PATH, extensionAutoReload(compiler)); 15 | } 16 | -------------------------------------------------------------------------------- /server/middlewares/proxyMiddleware.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'ansi-colors'; 2 | import console from 'consola'; 3 | import type { Express } from 'express'; 4 | import { createProxyMiddleware } from 'http-proxy-middleware'; 5 | 6 | import proxyTable from '../configs/proxy'; 7 | 8 | function link(str: string) { 9 | return chalk.magenta.underline(str); 10 | } 11 | 12 | export default function proxyMiddleware(server: Express): void { 13 | Object.entries(proxyTable).forEach(([path, options]) => { 14 | const from = path; 15 | const to = options.target as string; 16 | console.info(`proxy ${link(from)} ${chalk.green('->')} ${link(to)}`); 17 | 18 | if (!options.logLevel) options.logLevel = 'warn'; 19 | server.use(path, createProxyMiddleware(options)); 20 | }); 21 | process.stdout.write('\n'); 22 | } 23 | -------------------------------------------------------------------------------- /server/middlewares/webpackMiddleware.ts: -------------------------------------------------------------------------------- 1 | import type { Compiler } from 'webpack'; 2 | import webpackDevMiddleware from 'webpack-dev-middleware'; 3 | import webpackHotMiddleware from 'webpack-hot-middleware'; 4 | 5 | import devConfig from '../configs/webpack.dev'; 6 | import { HRM_PATH } from '../utils/constants'; 7 | 8 | export default function (compiler: Compiler) { 9 | const publicPath = devConfig.output!.publicPath! as string; 10 | return [ 11 | webpackDevMiddleware(compiler, { 12 | publicPath, 13 | stats: 'minimal', 14 | writeToDisk: true, 15 | }), 16 | webpackHotMiddleware(compiler as any, { path: HRM_PATH }), 17 | ]; 18 | } 19 | -------------------------------------------------------------------------------- /server/start.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises'; 2 | import chalk from 'ansi-colors'; 3 | import console from 'consola'; 4 | import exitHook from 'exit-hook'; 5 | import express from 'express'; 6 | import waitOn from 'wait-on'; 7 | import webpack from 'webpack'; 8 | 9 | import devConfig from './configs/webpack.dev'; 10 | import setupMiddlewares from './middlewares'; 11 | import { ENABLE_DEVTOOLS, HOST, PORT as DEFAULT_PORT } from './utils/constants'; 12 | import exec from './utils/exec'; 13 | import getPort from './utils/getPort'; 14 | import { resolveExtension } from './utils/path'; 15 | 16 | import './watcher'; 17 | 18 | async function start() { 19 | if (ENABLE_DEVTOOLS) { 20 | exec('npx react-devtools').promise.catch((error) => { 21 | console.error('Startup react-devtools occur error'); 22 | console.error(error); 23 | }); 24 | const reactDevtoolsJSAddress = 'http://localhost:8097'; 25 | waitOn({ resources: [reactDevtoolsJSAddress, resolveExtension('js')] }).then(async () => { 26 | const resp = await fetch(reactDevtoolsJSAddress); 27 | const data = await resp.text(); 28 | fs.writeFile(resolveExtension('js/react-devtools.js'), data, 'utf8'); 29 | }); 30 | } 31 | 32 | const compiler = webpack(devConfig); 33 | const devServer = express(); 34 | 35 | setupMiddlewares(devServer, compiler); 36 | const PORT = await getPort(HOST, DEFAULT_PORT); 37 | const httpServer = devServer.listen(PORT, HOST, () => { 38 | const coloredAddress = chalk.magenta.underline(`http://${HOST}:${PORT}`); 39 | console.info(`DevServer is running at ${coloredAddress} ✔`); 40 | }); 41 | 42 | exitHook(() => { 43 | // 先关闭 devServer 44 | httpServer.close(); 45 | // 在 ctrl + c 的时候随机输出 'See you again' 和 'Goodbye' 46 | console.log( 47 | chalk.greenBright.bold(`\n${Math.random() > 0.5 ? 'See you again' : 'Goodbye'}!`), 48 | ); 49 | }); 50 | } 51 | start(); 52 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "ES2022", 5 | "module": "commonjs", 6 | 7 | /* Strict Type-Checking Options */ 8 | "strict": true, 9 | 10 | /* Additional Checks */ 11 | "noUnusedLocals": true, 12 | "noUnusedParameters": true, 13 | "noImplicitReturns": true, 14 | "noFallthroughCasesInSwitch": true, 15 | 16 | /* Module Resolution Options */ 17 | "moduleResolution": "node", 18 | "esModuleInterop": true, 19 | "resolveJsonModule": true, 20 | 21 | /* Experimental Options */ 22 | "experimentalDecorators": true, 23 | "emitDecoratorMetadata": true, 24 | 25 | /* Advanced Options */ 26 | "forceConsistentCasingInFileNames": true, 27 | "skipLibCheck": true 28 | }, 29 | "include": ["./**/**.ts", "../src/manifest.ts"], 30 | "ts-node": { 31 | "files": true, 32 | "swc": true 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /server/typings/global.d.ts: -------------------------------------------------------------------------------- 1 | type EventSourceEvent = Event & { 2 | data?: any; 3 | target: Record | null; 4 | }; 5 | -------------------------------------------------------------------------------- /server/typings/modules.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'speed-measure-webpack-plugin' { 2 | import { StringDecoder } from 'string_decoder'; 3 | import { Configuration, Plugin } from 'webpack'; 4 | 5 | interface SpeedMeasurePluginOptions { 6 | disable: boolean; 7 | outputFormat: 8 | | 'json' 9 | | 'human' 10 | | 'humanVerbose' 11 | | ((outputObj: Record) => void); 12 | outputTarget: string | ((outputObj: string) => void); 13 | pluginNames: Record; 14 | granularLoaderData: boolean; 15 | } 16 | 17 | class SpeedMeasurePlugin extends Plugin { 18 | constructor(options?: Partial); 19 | wrap(webpackConfig: Configuration): Configuration; 20 | } 21 | 22 | export = SpeedMeasurePlugin; 23 | } 24 | 25 | declare module 'antd-dayjs-webpack-plugin' { 26 | import { Plugin } from 'webpack'; 27 | 28 | class WebpackDayjsPlugin extends Plugin { 29 | apply(compiler: Compiler): void; 30 | } 31 | 32 | export = WebpackDayjsPlugin; 33 | } 34 | 35 | declare module 'ssestream' { 36 | import { Request } from 'express'; 37 | import { Transform } from 'stream'; 38 | 39 | class SSEStream extends Transform { 40 | constructor(req: Request); 41 | } 42 | 43 | export = SSEStream; 44 | } 45 | 46 | declare module '@nuxt/friendly-errors-webpack-plugin' { 47 | import type { Plugin, Compiler } from 'webpack'; 48 | 49 | declare class FriendlyErrorsWebpackPlugin extends Plugin { 50 | constructor(options?: FriendlyErrorsWebpackPlugin.Options); 51 | 52 | apply(compiler: Compiler): void; 53 | } 54 | 55 | declare namespace FriendlyErrorsWebpackPlugin { 56 | // eslint-disable-next-line no-shadow 57 | enum Severity { 58 | Error = 'error', 59 | Warning = 'warning', 60 | } 61 | 62 | interface WebpackError { 63 | message: string; 64 | file: string; 65 | origin: string; 66 | name: string; 67 | severity: Severity; 68 | webpackError: any; 69 | } 70 | 71 | interface Options { 72 | compilationSuccessInfo?: { 73 | messages: string[]; 74 | notes: string[]; 75 | }; 76 | onErrors?(severity: Severity, errors: string): void; 77 | clearConsole?: boolean; 78 | additionalFormatters?: Array<(errors: WebpackError[], type: Severity) => string[]>; 79 | additionalTransformers?: Array<(error: any) => any>; 80 | } 81 | } 82 | 83 | export = FriendlyErrorsWebpackPlugin; 84 | } 85 | 86 | declare module 'webpack-hot-middleware' { 87 | import webpackHotMiddleware from '@types/webpack-hot-middleware'; 88 | 89 | export = webpackHotMiddleware; 90 | } 91 | -------------------------------------------------------------------------------- /server/utils/args.ts: -------------------------------------------------------------------------------- 1 | import yargs = require('yargs/yargs'); 2 | 3 | interface Arguments { 4 | [x: string]: unknown; 5 | devtools: boolean; 6 | analyze: boolean; 7 | open: string | undefined; 8 | } 9 | const argv = yargs(process.argv.slice(2)).options({ 10 | devtools: { type: 'boolean', default: false }, 11 | analyze: { type: 'boolean', default: false }, 12 | open: { type: 'string' }, 13 | }).argv as Arguments; 14 | 15 | export default argv; 16 | -------------------------------------------------------------------------------- /server/utils/constants.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'node:path'; 2 | 3 | import argv from './args'; 4 | 5 | const HOST = '127.0.0.1'; 6 | const PORT = 3600; 7 | const PROJECT_ROOT = resolve(__dirname, '../../'); 8 | const COPYRIGHT = `/** 9 | * This chrome extension is powered by awesome-chrome-extension-boilerplate 10 | * 11 | * @see {@link https://github.com/tjx666/awesome-chrome-extension-boilerplate} 12 | * @preserve 13 | */`; 14 | const HRM_PATH = '/__webpack_HMR__'; 15 | const EXTENSION_AUTO_RELOAD_PATH = '/__extension_auto_reload__'; 16 | 17 | const ENABLE_DEVTOOLS = argv.devtools; 18 | const ENABLE_ANALYZE = argv.analyze; 19 | 20 | const __DEV__ = process.env.NODE_ENV !== 'production'; 21 | 22 | export { 23 | __DEV__, 24 | COPYRIGHT, 25 | ENABLE_ANALYZE, 26 | ENABLE_DEVTOOLS, 27 | EXTENSION_AUTO_RELOAD_PATH, 28 | HOST, 29 | HRM_PATH, 30 | PORT, 31 | PROJECT_ROOT, 32 | }; 33 | -------------------------------------------------------------------------------- /server/utils/entry.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | 3 | import { __DEV__, HOST, HRM_PATH, PORT } from './constants'; 4 | import { resolveServer, resolveSrc } from './path'; 5 | 6 | const HMR_URL = encodeURIComponent(`http://${HOST}:${PORT}${HRM_PATH}`); 7 | // !: 必须指定 path 为 devServer 的地址,不然的话热更新 client 会向 chrome://xxx 请求 8 | const HMRClientScript = `webpack-hot-middleware/client?path=${HMR_URL}&reload=true&overlay=true`; 9 | 10 | const backgroundPath = resolveSrc('background/index.ts'); 11 | const optionsPath = resolveSrc('options/index.tsx'); 12 | const popupPath = resolveSrc('popup/index.tsx'); 13 | const installPath = resolveSrc('install/index.tsx'); 14 | const chatPath = resolveSrc('chat/index.tsx'); 15 | const sidepanelPath = resolveSrc('sidepanel/index.tsx'); 16 | 17 | const devEntry: Record = { 18 | background: [backgroundPath], 19 | options: [HMRClientScript, optionsPath], 20 | popup: [HMRClientScript, popupPath], 21 | install: [HMRClientScript, installPath], 22 | chat: [HMRClientScript, chatPath], 23 | sidepanel: [HMRClientScript, sidepanelPath], 24 | }; 25 | const prodEntry: Record = { 26 | background: [backgroundPath], 27 | options: [optionsPath], 28 | popup: [popupPath], 29 | install: [installPath], 30 | chat: [chatPath], 31 | sidepanel: [sidepanelPath], 32 | }; 33 | const entry = __DEV__ ? devEntry : prodEntry; 34 | 35 | const contentsDirs = fs.readdirSync(resolveSrc('contents')); 36 | const validExtensions = ['tsx', 'ts']; 37 | contentsDirs.forEach((contentScriptDir) => { 38 | const hasValid = validExtensions.some((ext) => { 39 | const abs = resolveSrc(`contents/${contentScriptDir}/index.${ext}`); 40 | if (fs.existsSync(abs)) { 41 | entry[contentScriptDir] = [abs]; 42 | return true; 43 | } 44 | 45 | return false; 46 | }); 47 | 48 | if (!hasValid) { 49 | const dir = resolveSrc(`contents/${contentScriptDir}`); 50 | throw new Error(`You must put index.tsx or index.ts under directory: ${dir}`); 51 | } 52 | }); 53 | 54 | // NOTE: 有可能用户没打算开发 content script,所以 contents/all 这个文件夹可能不存在 55 | if (entry.all && __DEV__) { 56 | entry.all.unshift(resolveServer('client/allTabClient.ts')); 57 | entry.background.unshift(resolveServer('client/backgroundClient.ts')); 58 | } 59 | 60 | export default entry; 61 | -------------------------------------------------------------------------------- /server/utils/exec.ts: -------------------------------------------------------------------------------- 1 | import type { ChildProcess } from 'node:child_process'; 2 | import { exec as _exec } from 'node:child_process'; 3 | import exitHook from 'exit-hook'; 4 | 5 | import { PROJECT_ROOT } from './constants'; 6 | 7 | export default function exec(command: string) { 8 | let childProcess: ChildProcess; 9 | const promise = new Promise((resolve, reject) => { 10 | childProcess = _exec( 11 | command, 12 | { 13 | cwd: PROJECT_ROOT, 14 | }, 15 | (error) => { 16 | if (error) { 17 | reject(error); 18 | return; 19 | } 20 | resolve(); 21 | }, 22 | ); 23 | 24 | childProcess.stdout?.pipe(process.stdout); 25 | childProcess.stderr?.pipe(process.stderr); 26 | exitHook(() => { 27 | childProcess.kill(); 28 | }); 29 | }); 30 | 31 | return { 32 | // @ts-expect-error In fact, childProcess had been initialized in promise 33 | childProcess, 34 | promise, 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /server/utils/fs.ts: -------------------------------------------------------------------------------- 1 | import { constants as FS_CONSTANTS } from 'node:fs'; 2 | import fs from 'node:fs/promises'; 3 | 4 | export function pathExists(path: string) { 5 | return fs 6 | .access(path, FS_CONSTANTS.F_OK) 7 | .then(() => true) 8 | .catch(() => false); 9 | } 10 | 11 | export async function ensureDir(dir: string) { 12 | if (!(await pathExists(dir))) { 13 | await fs.mkdir(dir, { recursive: true }); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /server/utils/getPort.ts: -------------------------------------------------------------------------------- 1 | import _getPort from 'get-port'; 2 | 3 | /** 4 | * 获取可用端口,被占用后加一 5 | */ 6 | export default async function getPort(host: string, port: number): Promise { 7 | const result = await _getPort({ host, port }); 8 | 9 | // 没被占用就返回这个端口号 10 | if (result === port) { 11 | return result; 12 | } 13 | 14 | // 递归,端口号 +1 15 | return getPort(host, port + 1); 16 | } 17 | -------------------------------------------------------------------------------- /server/utils/path.ts: -------------------------------------------------------------------------------- 1 | import { resolve as _resolve } from 'node:path'; 2 | 3 | import { PROJECT_ROOT } from './constants'; 4 | 5 | export function resolveProject(...args: string[]) { 6 | return _resolve(PROJECT_ROOT, ...args); 7 | } 8 | 9 | export function resolveExtension(...args: string[]) { 10 | return resolveProject('extension', ...args); 11 | } 12 | 13 | export function resolvePublic(...args: string[]) { 14 | return resolveProject('public', ...args); 15 | } 16 | 17 | export function resolveServer(...args: string[]) { 18 | return resolveProject('server', ...args); 19 | } 20 | 21 | export function resolveSrc(...args: string[]) { 22 | return resolveProject('src', ...args); 23 | } 24 | -------------------------------------------------------------------------------- /server/watcher.ts: -------------------------------------------------------------------------------- 1 | import chokidar from 'chokidar'; 2 | 3 | import exec from './utils/exec'; 4 | import { resolveProject, resolveSrc } from './utils/path'; 5 | 6 | function generateManifest() { 7 | return exec('npx ts-node ./server/generateManifest.ts').promise.catch(() => { 8 | // ignore, mainly is ts compile error 9 | }); 10 | } 11 | // run once when start 12 | generateManifest(); 13 | chokidar.watch([resolveSrc('manifest.ts'), resolveProject('package.json')]).on('change', () => { 14 | generateManifest(); 15 | }); 16 | -------------------------------------------------------------------------------- /src/assets/providers/360.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/360.png -------------------------------------------------------------------------------- /src/assets/providers/DMXAPI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/DMXAPI.png -------------------------------------------------------------------------------- /src/assets/providers/adept.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/adept.png -------------------------------------------------------------------------------- /src/assets/providers/ai21.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/ai21.png -------------------------------------------------------------------------------- /src/assets/providers/aihubmix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/aihubmix.png -------------------------------------------------------------------------------- /src/assets/providers/aimass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/aimass.png -------------------------------------------------------------------------------- /src/assets/providers/aisingapore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/aisingapore.png -------------------------------------------------------------------------------- /src/assets/providers/alayanew.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/alayanew.png -------------------------------------------------------------------------------- /src/assets/providers/anthropic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/anthropic.png -------------------------------------------------------------------------------- /src/assets/providers/baichuan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/baichuan.png -------------------------------------------------------------------------------- /src/assets/providers/baidu-cloud.svg: -------------------------------------------------------------------------------- 1 | BaiduCloud -------------------------------------------------------------------------------- /src/assets/providers/bailian.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/bailian.png -------------------------------------------------------------------------------- /src/assets/providers/bge.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/bge.webp -------------------------------------------------------------------------------- /src/assets/providers/bigcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/bigcode.png -------------------------------------------------------------------------------- /src/assets/providers/bytedance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/bytedance.png -------------------------------------------------------------------------------- /src/assets/providers/chatglm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/chatglm.png -------------------------------------------------------------------------------- /src/assets/providers/chatgpt.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/chatgpt.jpeg -------------------------------------------------------------------------------- /src/assets/providers/claude.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/claude.png -------------------------------------------------------------------------------- /src/assets/providers/codegeex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/codegeex.png -------------------------------------------------------------------------------- /src/assets/providers/codestral.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/codestral.png -------------------------------------------------------------------------------- /src/assets/providers/cohere.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/cohere.png -------------------------------------------------------------------------------- /src/assets/providers/cohere.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/cohere.webp -------------------------------------------------------------------------------- /src/assets/providers/copilot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/copilot.png -------------------------------------------------------------------------------- /src/assets/providers/dalle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/dalle.png -------------------------------------------------------------------------------- /src/assets/providers/dashscope.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/dashscope.png -------------------------------------------------------------------------------- /src/assets/providers/dbrx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/dbrx.png -------------------------------------------------------------------------------- /src/assets/providers/deepseek.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/deepseek.png -------------------------------------------------------------------------------- /src/assets/providers/dianxin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/dianxin.png -------------------------------------------------------------------------------- /src/assets/providers/doubao.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/doubao.png -------------------------------------------------------------------------------- /src/assets/providers/embedding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/embedding.png -------------------------------------------------------------------------------- /src/assets/providers/fireworks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/fireworks.png -------------------------------------------------------------------------------- /src/assets/providers/flashaudio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/flashaudio.png -------------------------------------------------------------------------------- /src/assets/providers/flux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/flux.png -------------------------------------------------------------------------------- /src/assets/providers/gemini.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/gemini.png -------------------------------------------------------------------------------- /src/assets/providers/gemma.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/gemma.png -------------------------------------------------------------------------------- /src/assets/providers/gitee-ai.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/gitee-ai.png -------------------------------------------------------------------------------- /src/assets/providers/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/github.png -------------------------------------------------------------------------------- /src/assets/providers/google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/google.png -------------------------------------------------------------------------------- /src/assets/providers/gpt_3.5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/gpt_3.5.png -------------------------------------------------------------------------------- /src/assets/providers/gpt_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/gpt_4.png -------------------------------------------------------------------------------- /src/assets/providers/gpt_o1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/gpt_o1.png -------------------------------------------------------------------------------- /src/assets/providers/graph-rag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/graph-rag.png -------------------------------------------------------------------------------- /src/assets/providers/grok.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/grok.png -------------------------------------------------------------------------------- /src/assets/providers/groq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/groq.png -------------------------------------------------------------------------------- /src/assets/providers/gryphe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/gryphe.png -------------------------------------------------------------------------------- /src/assets/providers/hailuo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/hailuo.png -------------------------------------------------------------------------------- /src/assets/providers/huggingface.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/huggingface.png -------------------------------------------------------------------------------- /src/assets/providers/hunyuan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/hunyuan.png -------------------------------------------------------------------------------- /src/assets/providers/hyperbolic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/hyperbolic.png -------------------------------------------------------------------------------- /src/assets/providers/ibm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/ibm.png -------------------------------------------------------------------------------- /src/assets/providers/infini.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/infini.png -------------------------------------------------------------------------------- /src/assets/providers/internlm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/internlm.png -------------------------------------------------------------------------------- /src/assets/providers/internvl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/internvl.png -------------------------------------------------------------------------------- /src/assets/providers/jina.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/jina.png -------------------------------------------------------------------------------- /src/assets/providers/keling.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/keling.png -------------------------------------------------------------------------------- /src/assets/providers/lepton.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/lepton.png -------------------------------------------------------------------------------- /src/assets/providers/llama.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/llama.png -------------------------------------------------------------------------------- /src/assets/providers/llava.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/llava.png -------------------------------------------------------------------------------- /src/assets/providers/lmstudio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/lmstudio.png -------------------------------------------------------------------------------- /src/assets/providers/luma.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/luma.png -------------------------------------------------------------------------------- /src/assets/providers/magic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/magic.png -------------------------------------------------------------------------------- /src/assets/providers/mediatek.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/mediatek.png -------------------------------------------------------------------------------- /src/assets/providers/microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/microsoft.png -------------------------------------------------------------------------------- /src/assets/providers/midjourney.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/midjourney.png -------------------------------------------------------------------------------- /src/assets/providers/minicpm.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/minicpm.webp -------------------------------------------------------------------------------- /src/assets/providers/minimax.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/minimax.png -------------------------------------------------------------------------------- /src/assets/providers/mistral.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/mistral.png -------------------------------------------------------------------------------- /src/assets/providers/mixedbread.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/mixedbread.png -------------------------------------------------------------------------------- /src/assets/providers/mixtral.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/mixtral.png -------------------------------------------------------------------------------- /src/assets/providers/modelscope.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/modelscope.png -------------------------------------------------------------------------------- /src/assets/providers/moonshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/moonshot.png -------------------------------------------------------------------------------- /src/assets/providers/nousresearch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/nousresearch.png -------------------------------------------------------------------------------- /src/assets/providers/nvidia.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/nvidia.png -------------------------------------------------------------------------------- /src/assets/providers/o3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/o3.png -------------------------------------------------------------------------------- /src/assets/providers/ocoolai.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/ocoolai.png -------------------------------------------------------------------------------- /src/assets/providers/ollama.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/ollama.png -------------------------------------------------------------------------------- /src/assets/providers/openai.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/openai.jpeg -------------------------------------------------------------------------------- /src/assets/providers/openai.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/openai.png -------------------------------------------------------------------------------- /src/assets/providers/openrouter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/openrouter.png -------------------------------------------------------------------------------- /src/assets/providers/palm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/palm.png -------------------------------------------------------------------------------- /src/assets/providers/perplexity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/perplexity.png -------------------------------------------------------------------------------- /src/assets/providers/pixtral.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/pixtral.png -------------------------------------------------------------------------------- /src/assets/providers/ppio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/ppio.png -------------------------------------------------------------------------------- /src/assets/providers/qiniu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/qiniu.png -------------------------------------------------------------------------------- /src/assets/providers/qwen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/qwen.png -------------------------------------------------------------------------------- /src/assets/providers/qwenlm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/qwenlm.png -------------------------------------------------------------------------------- /src/assets/providers/rakutenai.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/rakutenai.png -------------------------------------------------------------------------------- /src/assets/providers/silicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/silicon.png -------------------------------------------------------------------------------- /src/assets/providers/sparkdesk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/sparkdesk.png -------------------------------------------------------------------------------- /src/assets/providers/stability.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/stability.png -------------------------------------------------------------------------------- /src/assets/providers/step.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/step.png -------------------------------------------------------------------------------- /src/assets/providers/suno.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/suno.png -------------------------------------------------------------------------------- /src/assets/providers/tele.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/tele.png -------------------------------------------------------------------------------- /src/assets/providers/tencent-cloud-ti.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/tencent-cloud-ti.png -------------------------------------------------------------------------------- /src/assets/providers/together.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/together.png -------------------------------------------------------------------------------- /src/assets/providers/upstage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/upstage.png -------------------------------------------------------------------------------- /src/assets/providers/vidu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/vidu.png -------------------------------------------------------------------------------- /src/assets/providers/volcengine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/volcengine.png -------------------------------------------------------------------------------- /src/assets/providers/voyageai.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/voyageai.png -------------------------------------------------------------------------------- /src/assets/providers/wenxin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/wenxin.png -------------------------------------------------------------------------------- /src/assets/providers/xirang.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/xirang.png -------------------------------------------------------------------------------- /src/assets/providers/yi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/yi.png -------------------------------------------------------------------------------- /src/assets/providers/zero-one.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/zero-one.png -------------------------------------------------------------------------------- /src/assets/providers/zhipu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjszxli/DeepSeekAllSupports/5e5a1a4d9f7ea11ca85d137d745687a6bba81e9f/src/assets/providers/zhipu.png -------------------------------------------------------------------------------- /src/chat/components/ChatBody/index.scss: -------------------------------------------------------------------------------- 1 | .chat-body { 2 | display: flex; 3 | flex: 1; 4 | flex-direction: column; 5 | overflow: hidden; 6 | background-color: #f8fafc; 7 | 8 | .chat-footer { 9 | padding: 20px 24px; 10 | background-color: #fff; 11 | border-top: 1px solid #e8e8e8; 12 | box-shadow: 0 -2px 8px rgb(0 0 0 / 5%); 13 | 14 | .input-container { 15 | display: flex; 16 | gap: 12px; 17 | align-items: flex-end; 18 | 19 | .ant-input-textarea { 20 | flex: 1; 21 | 22 | textarea { 23 | padding: 10px 14px; 24 | font-size: 14px; 25 | line-height: 1.5; 26 | color: #333; 27 | background-color: #f5f7fa; 28 | border: 1px solid #e1e4e8; 29 | border-radius: 8px; 30 | resize: none; 31 | transition: all 0.3s; 32 | 33 | &:focus { 34 | background-color: #fff; 35 | border-color: #4776e6; 36 | outline: none; 37 | box-shadow: 0 0 0 2px rgb(71 118 230 / 10%); 38 | } 39 | 40 | &::placeholder { 41 | color: #999; 42 | } 43 | 44 | &:disabled { 45 | cursor: not-allowed; 46 | background-color: #f0f0f0; 47 | } 48 | } 49 | } 50 | 51 | .input-actions { 52 | display: flex; 53 | gap: 8px; 54 | 55 | .send-button, 56 | .stop-button { 57 | min-width: 80px; 58 | height: 36px; 59 | padding: 0 16px; 60 | font-weight: 500; 61 | border-radius: 6px; 62 | transition: all 0.3s; 63 | 64 | &:disabled { 65 | cursor: not-allowed; 66 | opacity: 0.5; 67 | } 68 | } 69 | 70 | .send-button { 71 | color: white; 72 | background: linear-gradient(135deg, #4776e6 0%, #8e54e9 100%); 73 | border: none; 74 | box-shadow: 0 2px 8px rgb(71 118 230 / 25%); 75 | 76 | &:hover:not(:disabled) { 77 | box-shadow: 0 4px 12px rgb(71 118 230 / 35%); 78 | transform: translateY(-1px); 79 | } 80 | 81 | &:active:not(:disabled) { 82 | transform: translateY(0); 83 | } 84 | } 85 | 86 | .stop-button { 87 | color: white; 88 | background-color: #ff4d4f; 89 | border: none; 90 | box-shadow: 0 2px 8px rgb(255 77 79 / 25%); 91 | 92 | &:hover:not(:disabled) { 93 | background-color: #ff7875; 94 | box-shadow: 0 4px 12px rgb(255 77 79 / 35%); 95 | transform: translateY(-1px); 96 | } 97 | 98 | &:active:not(:disabled) { 99 | transform: translateY(0); 100 | } 101 | } 102 | } 103 | } 104 | } 105 | } 106 | 107 | // 动画定义 108 | @keyframes fade-in { 109 | from { 110 | opacity: 0; 111 | } 112 | 113 | to { 114 | opacity: 1; 115 | } 116 | } 117 | 118 | @keyframes slide-in-left { 119 | from { 120 | opacity: 0; 121 | transform: translateX(-20px); 122 | } 123 | 124 | to { 125 | opacity: 1; 126 | transform: translateX(0); 127 | } 128 | } 129 | 130 | @keyframes slide-in-right { 131 | from { 132 | opacity: 0; 133 | transform: translateX(20px); 134 | } 135 | 136 | to { 137 | opacity: 1; 138 | transform: translateX(0); 139 | } 140 | } 141 | 142 | @keyframes pulse { 143 | 0% { 144 | opacity: 1; 145 | transform: scale(1); 146 | } 147 | 148 | 50% { 149 | opacity: 0.5; 150 | transform: scale(0.8); 151 | } 152 | 153 | 100% { 154 | opacity: 1; 155 | transform: scale(1); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/chat/components/MessageList/index.scss: -------------------------------------------------------------------------------- 1 | .messages-container { 2 | display: flex; 3 | flex: 1; 4 | flex-direction: column; 5 | gap: 16px; 6 | padding: 20px; 7 | overflow-y: auto; 8 | 9 | .welcome-container { 10 | display: flex; 11 | flex-direction: column; 12 | align-items: center; 13 | justify-content: center; 14 | height: 100%; 15 | padding: 40px; 16 | 17 | .prompt-suggestions { 18 | max-width: 600px; 19 | margin-top: 40px; 20 | text-align: center; 21 | 22 | h5 { 23 | margin-bottom: 20px; 24 | color: #666; 25 | } 26 | 27 | .suggestion-items { 28 | display: flex; 29 | flex-wrap: wrap; 30 | gap: 12px; 31 | justify-content: center; 32 | 33 | .suggestion-item { 34 | padding: 8px 16px; 35 | background: #f5f5f5; 36 | border: 1px solid #e0e0e0; 37 | border-radius: 20px; 38 | transition: all 0.3s; 39 | 40 | &:hover { 41 | background: #e8e8e8; 42 | border-color: #d0d0d0; 43 | } 44 | } 45 | } 46 | } 47 | } 48 | 49 | .loading-indicator { 50 | display: flex; 51 | gap: 8px; 52 | align-items: center; 53 | justify-content: center; 54 | padding: 20px; 55 | color: #666; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/chat/components/MessageList/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from 'react'; 2 | import { Empty, Typography, Button, Spin } from 'antd'; 3 | import { RocketOutlined, BulbOutlined } from '@ant-design/icons'; 4 | import { Message } from '@/types/message'; 5 | import { t } from '@/locales/i18n'; 6 | import MessageGroup from '../MessageGroup'; 7 | import { getGroupedMessages } from '@/services/MessageService'; 8 | import { useMessageOperations } from '@/chat/hooks/useMessageOperations'; 9 | import { usePromptSuggestions } from '@/chat/hooks/usePromptSuggestions'; 10 | import rootStore from '@/store'; 11 | import { observer } from 'mobx-react-lite'; 12 | import './index.scss'; 13 | 14 | interface MessageListProps { 15 | messages: Message[]; 16 | selectedProvider: string; 17 | onEditMessage: (text: string) => void; 18 | } 19 | 20 | const MessageList: React.FC = observer( 21 | ({ messages, selectedProvider, onEditMessage }) => { 22 | const messagesWrapperRef = useRef(null); 23 | const streamingMessageId = rootStore.messageStore.streamingMessageId; 24 | 25 | const { suggestedPrompts, handleSelectPrompt } = usePromptSuggestions(); 26 | const groupedMessages = Object.entries(getGroupedMessages(messages)); 27 | 28 | // 自动滚动到底部 29 | useEffect(() => { 30 | if (messagesWrapperRef.current) { 31 | messagesWrapperRef.current.scrollTop = messagesWrapperRef.current.scrollHeight; 32 | } 33 | }, [messages]); 34 | 35 | return ( 36 |
37 | {groupedMessages.length === 0 ? ( 38 |
39 | 42 | } 43 | description={ 44 | {t('welcomeMessage')} 45 | } 46 | /> 47 |
48 | 49 | {t('tryAsking')} 50 | 51 |
52 | {suggestedPrompts.map((prompt, index) => ( 53 | 60 | ))} 61 |
62 |
63 |
64 | ) : ( 65 | groupedMessages.map(([key, groupMessages]) => ( 66 | 74 | )) 75 | )} 76 |
77 | ); 78 | }, 79 | ); 80 | 81 | export default MessageList; 82 | -------------------------------------------------------------------------------- /src/chat/hooks/useMessageSender.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | import { message as AntdMessage } from 'antd'; 3 | import robotStore from '@/store/robot'; 4 | import llmStore from '@/store/llm'; 5 | import rootStore from '@/store'; 6 | import { MessageThunkService } from '@/store/messageThunk'; 7 | import { InputMessage } from '@/types/message'; 8 | import { getUserMessage } from '@/services/MessageService'; 9 | 10 | export const useMessageSender = () => { 11 | const handleSendMessage = useCallback((userInput: string, onSuccess?: () => void) => { 12 | if (!userInput.trim()) return; 13 | 14 | const { selectedRobot } = robotStore; 15 | const { selectedTopicId } = selectedRobot; 16 | 17 | if (!selectedTopicId) { 18 | AntdMessage.error('请先选择一个话题'); 19 | return; 20 | } 21 | 22 | selectedRobot.model = llmStore.defaultModel; 23 | 24 | const topic = robotStore.selectedRobot.topics.find((topic) => topic.id === selectedTopicId); 25 | 26 | if (!topic) { 27 | AntdMessage.error('请先选择一个话题'); 28 | return; 29 | } 30 | 31 | const userMessage: InputMessage = { 32 | robot: selectedRobot, 33 | topic: topic, 34 | content: userInput, 35 | }; 36 | 37 | const { message, blocks } = getUserMessage(userMessage); 38 | console.log(message, blocks); 39 | 40 | const messageService = new MessageThunkService(rootStore); 41 | messageService.sendMessage(message, blocks, selectedRobot, selectedTopicId); 42 | 43 | // 调用成功回调 44 | if (onSuccess) { 45 | onSuccess(); 46 | } 47 | }, []); 48 | 49 | return { 50 | handleSendMessage, 51 | }; 52 | }; 53 | -------------------------------------------------------------------------------- /src/chat/hooks/usePromptSuggestions.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | 3 | export const usePromptSuggestions = () => { 4 | // 这里可以从配置或API获取建议提示词 5 | const defaultSuggestions = [ 6 | '你能做什么?', 7 | '帮我解释一下React的生命周期', 8 | '如何使用TypeScript?', 9 | '写一个简单的Todo应用', 10 | ]; 11 | 12 | const handleSelectPrompt = useCallback((prompt: string, onSelect: (text: string) => void) => { 13 | onSelect(prompt); 14 | }, []); 15 | 16 | return { 17 | suggestedPrompts: defaultSuggestions, 18 | handleSelectPrompt, 19 | }; 20 | }; 21 | -------------------------------------------------------------------------------- /src/chat/index.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client'; 2 | import { HashRouter } from 'react-router-dom'; 3 | import React, { createContext } from 'react'; 4 | 5 | import App from './App'; 6 | import { LanguageProvider } from '../contexts/LanguageContext'; 7 | 8 | import './App.scss'; 9 | import rootStore from '@/store'; 10 | 11 | const container = document.getElementById('root'); 12 | const root = createRoot(container!); 13 | const StoreContext = createContext(rootStore); 14 | 15 | root.render( 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | , 25 | ); 26 | -------------------------------------------------------------------------------- /src/config/robot.ts: -------------------------------------------------------------------------------- 1 | import robotListData from './robotListData.json'; 2 | 3 | // 助手接口定义 4 | export interface Robot { 5 | id: string; 6 | name: string; 7 | icon: string; 8 | group: string[]; 9 | prompt: string; 10 | description: string; 11 | } 12 | 13 | // 工具函数 14 | export function getRobotById(id: string): Robot | undefined { 15 | return robotList.find((robot) => robot.id === id); 16 | } 17 | 18 | export function getRobotsByGroup(group: string): Robot[] { 19 | return robotList.filter((robot) => robot.group.includes(group)); 20 | } 21 | 22 | export function getAllGroups(): string[] { 23 | const groups = new Set(); 24 | robotList.forEach((robot) => { 25 | robot.group.forEach((g) => groups.add(g)); 26 | }); 27 | return Array.from(groups); 28 | } 29 | 30 | export function searchRobots(keyword: string): Robot[] { 31 | const lowerKeyword = keyword.toLowerCase(); 32 | return robotList.filter( 33 | (robot) => 34 | robot.name.toLowerCase().includes(lowerKeyword) || 35 | robot.description.toLowerCase().includes(lowerKeyword) || 36 | robot.group.some((g) => g.toLowerCase().includes(lowerKeyword)), 37 | ); 38 | } 39 | 40 | // 助手列表数据 41 | export const robotList: Robot[] = robotListData.map((data) => ({ 42 | ...data, 43 | group: data.group, 44 | })); 45 | 46 | export default robotList; 47 | -------------------------------------------------------------------------------- /src/contents/all/components/ChatControls/index.scss: -------------------------------------------------------------------------------- 1 | .chat-controls { 2 | display: flex; 3 | justify-content: space-between; 4 | padding: 8px 16px; 5 | background-color: rgb(255 255 255 / 95%); 6 | border-bottom: 1px solid #eaeaea; 7 | 8 | .control-item { 9 | display: flex; 10 | align-items: center; 11 | padding: 4px 12px; 12 | cursor: pointer; 13 | background-color: #fff; 14 | border-radius: 8px; 15 | box-shadow: 0 1px 2px rgb(0 0 0 / 5%); 16 | transition: all 0.2s ease; 17 | 18 | &:hover { 19 | box-shadow: 0 2px 4px rgb(0 0 0 / 10%); 20 | } 21 | } 22 | 23 | .control-label { 24 | margin: 0 8px; 25 | font-size: 13px; 26 | color: #4b5563; 27 | } 28 | 29 | .icon-enabled { 30 | color: #4776e6; 31 | } 32 | 33 | .icon-disabled { 34 | color: #4b5563; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/contents/all/components/ChatControls/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useState } from 'react'; 2 | import { Switch, Tooltip } from 'antd'; 3 | import { GlobalOutlined, LinkOutlined } from '@ant-design/icons'; 4 | import storage from '@/utils/storage'; 5 | import { useLanguage } from '@/contexts/LanguageContext'; 6 | import { featureSettings } from '@/utils/featureSettings'; 7 | 8 | import './index.scss'; 9 | 10 | const ChatControls: React.FC = () => { 11 | const [webSearchEnabled, setWebSearchEnabled] = useState(false); 12 | const [useWebpageContext, setUseWebpageContext] = useState(true); 13 | const { t } = useLanguage(); 14 | 15 | useEffect(() => { 16 | const loadSettings = async () => { 17 | try { 18 | const [webSearch, webpageContext] = await Promise.all([ 19 | storage.getWebSearchEnabled(), 20 | storage.getUseWebpageContext(), 21 | ]); 22 | 23 | setWebSearchEnabled(webSearch ?? false); 24 | setUseWebpageContext(webpageContext ?? true); 25 | } catch (error) { 26 | console.error('Failed to load settings:', error); 27 | } 28 | }; 29 | 30 | loadSettings(); 31 | }, []); 32 | 33 | const handleWebSearchToggle = useCallback( 34 | async (checked: boolean) => { 35 | try { 36 | const newState = await featureSettings.toggleWebSearch(checked, t); 37 | setWebSearchEnabled(newState); 38 | } catch (error) { 39 | console.error('Failed to toggle web search:', error); 40 | } 41 | }, 42 | [t], 43 | ); 44 | 45 | const handleWebpageContextToggle = useCallback( 46 | async (checked: boolean) => { 47 | try { 48 | const newState = await featureSettings.toggleWebpageContext(checked, t); 49 | setUseWebpageContext(newState); 50 | } catch (error) { 51 | console.error('Failed to toggle webpage context:', error); 52 | } 53 | }, 54 | [t], 55 | ); 56 | 57 | return ( 58 |
59 | 60 |
61 | 64 | {t('includeWebpage' as any)} 65 | 72 |
73 |
74 | 75 |
76 | 79 | {t('webSearch' as any)} 80 | 87 |
88 |
89 |
90 | ); 91 | }; 92 | 93 | export default ChatControls; 94 | -------------------------------------------------------------------------------- /src/contents/all/components/ChatInterface/promptSuggestions.css: -------------------------------------------------------------------------------- 1 | /* Prompt Suggestions Overlay Styles */ 2 | 3 | .prompt-suggestions-overlay { 4 | position: fixed; 5 | bottom: 90px; /* Position above the input area */ 6 | left: 50%; 7 | transform: translateX(-50%); 8 | width: 80%; /* Take up 80% of the container width */ 9 | max-width: 500px; 10 | z-index: 9999; /* Very high z-index to ensure it's on top of everything */ 11 | } 12 | 13 | .prompt-suggestions { 14 | width: 100%; 15 | max-height: 300px; 16 | overflow-y: auto; 17 | background-color: #fff; 18 | border-radius: 12px; 19 | box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); 20 | border: 1px solid #e0e0e0; 21 | } 22 | 23 | .prompt-item { 24 | padding: 10px 16px; 25 | cursor: pointer; 26 | transition: background-color 0.2s; 27 | } 28 | 29 | .prompt-item:hover { 30 | background-color: #f5f7fa; 31 | } 32 | 33 | .prompt-item.selected { 34 | background-color: #f0f5ff; 35 | border-left: 3px solid #4776e6; 36 | } 37 | 38 | .prompt-name { 39 | font-weight: 600; 40 | font-size: 14px; 41 | margin-bottom: 4px; 42 | color: #333; 43 | } 44 | 45 | .prompt-preview { 46 | font-size: 12px; 47 | color: #666; 48 | white-space: nowrap; 49 | overflow: hidden; 50 | text-overflow: ellipsis; 51 | } -------------------------------------------------------------------------------- /src/contents/all/components/ChatWindow/index.scss: -------------------------------------------------------------------------------- 1 | .ai-chat-box { 2 | position: fixed; 3 | z-index: 9999; 4 | display: none; 5 | resize: both; 6 | background-color: rgb(255 255 255 / 75%); 7 | backdrop-filter: blur(15px); 8 | border: 1px solid rgb(229 231 235 / 50%); 9 | border-radius: 12px; 10 | box-shadow: 0 10px 30px rgb(0 0 0 / 8%), 0 6px 12px rgb(0 0 0 / 5%); 11 | transition: opacity 0.3s ease, transform 0.3s ease; 12 | transform: translateY(20px); 13 | will-change: transform, opacity; 14 | 15 | &.visible { 16 | display: flex; 17 | flex-direction: column; 18 | transform: translateY(0); 19 | } 20 | 21 | // 拖动时的透明效果 22 | &.dragging { 23 | background-color: rgb(255 255 255 / 60%); 24 | backdrop-filter: blur(5px); 25 | border-color: rgb(229 231 235 / 30%); 26 | box-shadow: 0 10px 30px rgb(0 0 0 / 5%), 0 6px 12px rgb(0 0 0 / 3%); 27 | transition: none; 28 | 29 | .chat-content-container, 30 | .chat-window-header, 31 | .provider-alert-container { 32 | opacity: 0.5; 33 | transition: none; 34 | } 35 | } 36 | 37 | &::after { 38 | position: absolute; 39 | right: 0; 40 | bottom: 0; 41 | width: 15px; 42 | height: 15px; 43 | cursor: nwse-resize; 44 | content: ''; 45 | background: linear-gradient(135deg, transparent 50%, rgb(71 118 230 / 30%) 50%); 46 | } 47 | } 48 | 49 | .chat-window-header { 50 | display: flex; 51 | align-items: center; 52 | justify-content: space-between; 53 | padding: 12px 16px; 54 | cursor: move; 55 | user-select: none; 56 | background-color: rgb(248 250 252 / 80%); 57 | backdrop-filter: blur(5px); 58 | border-bottom: 1px solid rgb(234 234 234 / 70%); 59 | border-radius: 12px 12px 0 0; 60 | transition: opacity 0.3s ease; 61 | will-change: opacity; 62 | } 63 | 64 | .chat-window-actions { 65 | display: flex; 66 | gap: 8px; 67 | align-items: center; 68 | 69 | .header-action-button { 70 | display: flex; 71 | align-items: center; 72 | justify-content: center; 73 | width: 28px; 74 | height: 28px; 75 | cursor: pointer; 76 | background-color: rgb(255 255 255 / 50%); 77 | border-radius: 50%; 78 | transition: all 0.2s ease; 79 | 80 | &:hover { 81 | background-color: rgb(0 0 0 / 5%); 82 | transform: scale(1.05); 83 | } 84 | 85 | &.pin-button { 86 | color: #4776e6; 87 | } 88 | 89 | &.feedback-button { 90 | color: #38a169; 91 | } 92 | 93 | &.close-button { 94 | color: #e53e3e; 95 | } 96 | } 97 | } 98 | 99 | .resize-handle { 100 | position: absolute; 101 | right: 0; 102 | bottom: 0; 103 | z-index: 10; 104 | width: 25px; 105 | height: 25px; 106 | cursor: nwse-resize; 107 | background: linear-gradient(135deg, transparent 50%, rgb(71 118 230 / 40%) 50%); 108 | border-radius: 0 0 12px; 109 | } 110 | 111 | @keyframes fade-in { 112 | from { 113 | opacity: 0; 114 | transform: translateY(20px); 115 | } 116 | 117 | to { 118 | opacity: 1; 119 | transform: translateY(0); 120 | } 121 | } 122 | 123 | .chat-content-container { 124 | display: flex; 125 | flex-direction: column; 126 | width: 100%; 127 | height: 100%; 128 | overflow: hidden; // Prevent scrolling at this level 129 | } 130 | 131 | // Add styles for the chat interface when inside the chat window 132 | .chat-content-container .chat-interface-container { 133 | display: flex; 134 | flex: 1; 135 | flex-direction: column; 136 | height: 100%; 137 | overflow: hidden; // Prevent scrolling at the container level 138 | 139 | .messages-wrapper { 140 | flex: 1; 141 | overflow-x: hidden; 142 | overflow-y: auto; // Enable scrolling at this level 143 | } 144 | } 145 | 146 | // Add a style for when resizing is active 147 | .ai-chat-box.resizing { 148 | user-select: none; 149 | transition: none; 150 | 151 | &::after { 152 | background: linear-gradient(135deg, transparent 50%, rgb(71 118 230 / 70%) 50%); 153 | } 154 | 155 | .chat-content-container { 156 | pointer-events: none; 157 | } 158 | } 159 | 160 | // Fix for tooltips being hidden behind the chat window 161 | .ant-tooltip { 162 | z-index: 10000 !important; // Higher than the chat window's z-index 163 | } 164 | -------------------------------------------------------------------------------- /src/contents/all/components/IframeSidePanel.scss: -------------------------------------------------------------------------------- 1 | .iframe-sidepanel-overlay { 2 | position: fixed; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | height: 100%; 7 | z-index: 9999; 8 | background-color: rgb(0 0 0 / 50%); 9 | display: flex; 10 | justify-content: flex-end; 11 | animation: fadein 0.2s ease-in-out; 12 | } 13 | 14 | .iframe-sidepanel-container { 15 | width: 400px; 16 | height: 100%; 17 | display: flex; 18 | flex-direction: column; 19 | background-color: #fff; 20 | box-shadow: -2px 0 8px rgb(0 0 0 / 15%); 21 | animation: slidein 0.3s ease-out; 22 | } 23 | 24 | .iframe-sidepanel-header { 25 | display: flex; 26 | align-items: center; 27 | justify-content: space-between; 28 | padding: 12px 16px; 29 | background-color: #f5f5f5; 30 | border-bottom: 1px solid #e8e8e8; 31 | } 32 | 33 | .iframe-sidepanel-title { 34 | font-size: 16px; 35 | font-weight: 600; 36 | } 37 | 38 | .iframe-sidepanel-close { 39 | background: none; 40 | border: none; 41 | color: #999; 42 | cursor: pointer; 43 | font-size: 22px; 44 | line-height: 18px; 45 | padding: 0; 46 | margin: 0; 47 | display: flex; 48 | width: 24px; 49 | height: 24px; 50 | align-items: center; 51 | justify-content: center; 52 | 53 | &:hover { 54 | color: #666; 55 | } 56 | } 57 | 58 | .iframe-sidepanel-content { 59 | position: relative; 60 | flex: 1; 61 | height: calc(100% - 45px); 62 | } 63 | 64 | .iframe-sidepanel-iframe { 65 | width: 100%; 66 | height: 100%; 67 | display: block; 68 | border: none; 69 | } 70 | 71 | .iframe-sidepanel-loading { 72 | position: absolute; 73 | top: 0; 74 | left: 0; 75 | width: 100%; 76 | height: 100%; 77 | display: flex; 78 | flex-direction: column; 79 | align-items: center; 80 | justify-content: center; 81 | z-index: 1; 82 | background-color: #f5f5f5; 83 | } 84 | 85 | .loading-spinner { 86 | width: 40px; 87 | height: 40px; 88 | border: 4px solid rgb(0 0 0 / 10%); 89 | border-radius: 50%; 90 | border-top-color: #1677ff; 91 | margin-bottom: 16px; 92 | animation: spin 1s linear infinite; 93 | } 94 | 95 | @keyframes fadein { 96 | from { 97 | opacity: 0; 98 | } 99 | 100 | to { 101 | opacity: 1; 102 | } 103 | } 104 | 105 | @keyframes slidein { 106 | from { 107 | transform: translateX(100%); 108 | } 109 | 110 | to { 111 | transform: translateX(0); 112 | } 113 | } 114 | 115 | @keyframes spin { 116 | 0% { 117 | transform: rotate(0deg); 118 | } 119 | 120 | 100% { 121 | transform: rotate(360deg); 122 | } 123 | } 124 | 125 | /* 响应式设计 */ 126 | @media (max-width: 600px) { 127 | .iframe-sidepanel-container { 128 | width: 85%; 129 | } 130 | } 131 | 132 | /* Arc 浏览器特殊处理 */ 133 | .arc-browser .iframe-sidepanel-container { 134 | max-height: 90vh; /* 在 Arc 浏览器中限制高度,避免出现全屏问题 */ 135 | border-radius: 8px; 136 | margin: auto 10px auto 0; 137 | } -------------------------------------------------------------------------------- /src/contents/all/components/IframeSidePanel.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import './IframeSidePanel.scss'; 4 | 5 | interface IframeSidePanelProps { 6 | onClose: () => void; 7 | } 8 | 9 | // 检测是否为 Arc 浏览器 10 | const isArcBrowser = navigator.userAgent.includes('Arc/') || navigator.userAgent.includes('Arc '); 11 | 12 | const IframeSidePanel: React.FC = ({ onClose }) => { 13 | const iframeRef = useRef(null); 14 | const [isLoading, setIsLoading] = useState(true); 15 | const sidePanelUrl = chrome.runtime.getURL('sidepanel.html'); 16 | 17 | useEffect(() => { 18 | // 添加键盘事件监听 19 | const handleKeyDown = (e: KeyboardEvent) => { 20 | if (e.key === 'Escape') { 21 | onClose(); 22 | } 23 | }; 24 | 25 | document.addEventListener('keydown', handleKeyDown); 26 | 27 | // 处理 iframe 加载完成事件 28 | const handleIframeLoad = () => { 29 | setIsLoading(false); 30 | }; 31 | 32 | const iframe = iframeRef.current; 33 | if (iframe) { 34 | iframe.addEventListener('load', handleIframeLoad); 35 | } 36 | 37 | return () => { 38 | document.removeEventListener('keydown', handleKeyDown); 39 | if (iframe) { 40 | iframe.removeEventListener('load', handleIframeLoad); 41 | } 42 | }; 43 | }, [onClose]); 44 | 45 | // 防止冒泡事件到父元素 46 | const handleContainerClick = (e: React.MouseEvent) => { 47 | e.stopPropagation(); 48 | }; 49 | 50 | return ( 51 |
52 |
56 |
57 |
AI 聊天助手
58 | 61 |
62 |
63 | {isLoading && ( 64 |
65 |
66 |
加载中...
67 |
68 | )} 69 | 75 |
76 |
77 |
78 | ); 79 | }; 80 | 81 | // 创建并管理 IframeSidePanel 实例 82 | export class IframeSidePanelManager { 83 | private static container: HTMLDivElement | null = null; 84 | private static isVisible = false; 85 | private static root: any = null; 86 | 87 | // 显示侧边栏 88 | public static show(): void { 89 | if (this.isVisible) return; 90 | 91 | if (!this.container) { 92 | this.container = document.createElement('div'); 93 | this.container.id = 'iframe-sidepanel-root'; 94 | document.body.appendChild(this.container); 95 | this.root = createRoot(this.container); 96 | } 97 | 98 | this.isVisible = true; 99 | 100 | this.root.render(); 101 | 102 | // 禁用页面滚动 103 | document.body.style.overflow = 'hidden'; 104 | } 105 | 106 | // 隐藏侧边栏 107 | public static hide(): void { 108 | if (!this.isVisible || !this.container) return; 109 | 110 | this.isVisible = false; 111 | this.root.render(null); 112 | 113 | // 恢复页面滚动 114 | document.body.style.overflow = ''; 115 | } 116 | 117 | // 切换侧边栏显示状态 118 | public static toggle(): void { 119 | if (this.isVisible) { 120 | this.hide(); 121 | } else { 122 | this.show(); 123 | } 124 | } 125 | } 126 | 127 | export default IframeSidePanel; 128 | -------------------------------------------------------------------------------- /src/contents/all/components/Think/index.scss: -------------------------------------------------------------------------------- 1 | .deepseek-popup { 2 | position: relative; 3 | padding: 0 0 0 13px; 4 | margin: 0; 5 | line-height: 26px; 6 | color: #8b8b8b; 7 | text-align: left; 8 | white-space: pre-wrap; 9 | border-left: 2px solid #e5e5e5; 10 | } 11 | 12 | .deepseek-header { 13 | padding-bottom: 8px; 14 | margin-bottom: 8px; 15 | font-weight: bold; 16 | text-align: left; 17 | border-bottom: 1px solid #eee; 18 | } 19 | -------------------------------------------------------------------------------- /src/contents/all/components/Think/index.tsx: -------------------------------------------------------------------------------- 1 | import './index.scss'; 2 | 3 | const Think = ({ context }: { context: string }) => { 4 | return ( 5 |
6 |
🧠 已深思熟虑
7 |

{context}

8 |
9 | ); 10 | }; 11 | 12 | export default Think; 13 | -------------------------------------------------------------------------------- /src/contents/all/style.scss: -------------------------------------------------------------------------------- 1 | // the style will be applied to all pages 2 | .js-header-wrapper > header { 3 | background-color: lightseagreen; 4 | } 5 | -------------------------------------------------------------------------------- /src/contents/all/styles/animations.scss: -------------------------------------------------------------------------------- 1 | 2 | @keyframes pulse { 3 | 0% { 4 | opacity: 0.6; 5 | } 6 | 7 | 50% { 8 | opacity: 1; 9 | } 10 | 11 | 100% { 12 | opacity: 0.6; 13 | } 14 | } 15 | 16 | @keyframes bounce { 17 | 0%, 80%, 100% { 18 | transform: scale(0); 19 | } 20 | 21 | 40% { 22 | transform: scale(1.0); 23 | } 24 | } -------------------------------------------------------------------------------- /src/contents/all/styles/highlight.css: -------------------------------------------------------------------------------- 1 | /* 2 | * highlight.js styles - GitHub theme 3 | * Based on GitHub's syntax highlighting 4 | */ 5 | 6 | .hljs { 7 | display: block; 8 | overflow-x: auto; 9 | color: #24292e; 10 | background: #f6f8fa; 11 | } 12 | 13 | .hljs-comment, 14 | .hljs-punctuation { 15 | color: #6a737d; 16 | } 17 | 18 | .hljs-keyword, 19 | .hljs-selector-tag, 20 | .hljs-subst { 21 | font-weight: bold; 22 | color: #d73a49; 23 | } 24 | 25 | .hljs-number, 26 | .hljs-literal, 27 | .hljs-variable, 28 | .hljs-template-variable, 29 | .hljs-tag .hljs-attr { 30 | color: #005cc5; 31 | } 32 | 33 | .hljs-string, 34 | .hljs-doctag { 35 | color: #032f62; 36 | } 37 | 38 | .hljs-title, 39 | .hljs-section, 40 | .hljs-selector-id { 41 | font-weight: bold; 42 | color: #6f42c1; 43 | } 44 | 45 | .hljs-subst { 46 | font-weight: normal; 47 | } 48 | 49 | .hljs-type, 50 | .hljs-class .hljs-title { 51 | font-weight: bold; 52 | color: #458; 53 | } 54 | 55 | .hljs-tag, 56 | .hljs-name, 57 | .hljs-attribute { 58 | font-weight: normal; 59 | color: #22863a; 60 | } 61 | 62 | .hljs-regexp, 63 | .hljs-link { 64 | color: #009926; 65 | } 66 | 67 | .hljs-symbol, 68 | .hljs-bullet { 69 | color: #a00; 70 | } 71 | 72 | .hljs-built-in, 73 | .hljs-builtin-name { 74 | color: #0086b3; 75 | } 76 | 77 | .hljs-meta { 78 | font-weight: bold; 79 | color: #999; 80 | } 81 | 82 | .hljs-deletion { 83 | background: #fdd; 84 | } 85 | 86 | .hljs-addition { 87 | background: #dfd; 88 | } 89 | 90 | .hljs-emphasis { 91 | font-style: italic; 92 | } 93 | 94 | .hljs-strong { 95 | font-weight: bold; 96 | } -------------------------------------------------------------------------------- /src/contexts/LanguageContext.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from 'react'; 2 | import React, { createContext, useContext, useState, useEffect } from 'react'; 3 | import { en } from '@/locales/en'; 4 | import { zhCN } from '@/locales/zh-CN'; 5 | import { zhTW } from '@/locales/zh-TW'; 6 | import { ja } from '@/locales/ja'; 7 | import { ko } from '@/locales/ko'; 8 | import { getLocale, setLocale as setI18nLocale, subscribeToLocaleChange } from '@/locales/i18n'; 9 | import type { LocaleType } from '@/locales'; 10 | 11 | // 支持的语言 12 | type SupportedLanguages = LocaleType; 13 | 14 | // 定义所有翻译键的类型 15 | export type TranslationKey = 16 | | keyof typeof en 17 | | keyof typeof zhCN 18 | | keyof typeof zhTW 19 | | keyof typeof ja 20 | | keyof typeof ko; 21 | 22 | // 语言上下文类型 23 | export interface LanguageContextType { 24 | t: (key: TranslationKey) => string; 25 | currentLanguage: SupportedLanguages; 26 | setLocale: (locale: SupportedLanguages) => Promise; 27 | } 28 | 29 | // 创建上下文 30 | const LanguageContext = createContext(undefined); 31 | 32 | // 翻译资源 33 | const resources = { 34 | 'en': en, 35 | 'zh-CN': zhCN, 36 | 'zh-TW': zhTW, 37 | 'ja': ja, 38 | 'ko': ko, 39 | }; 40 | 41 | // 语言提供者组件 42 | export const LanguageProvider: React.FC<{ children: ReactNode }> = ({ children }) => { 43 | // 使用i18n服务获取当前语言 44 | const [currentLanguage, setCurrentLanguage] = useState(getLocale()); 45 | 46 | // 订阅语言变更 47 | useEffect(() => { 48 | const unsubscribe = subscribeToLocaleChange((locale) => { 49 | setCurrentLanguage(locale); 50 | }); 51 | 52 | return () => { 53 | unsubscribe(); 54 | }; 55 | }, []); 56 | 57 | // 翻译函数 58 | const t = (key: TranslationKey): string => { 59 | // 键名映射,处理不同版本的键名差异 60 | const keyMap: Record = { 61 | includeWebpageContent: 'includeWebpage', 62 | }; 63 | 64 | // 获取实际的键名 65 | const actualKey = keyMap[key as string] || key; 66 | 67 | try { 68 | return ( 69 | resources[currentLanguage as keyof typeof resources][ 70 | actualKey as keyof typeof resources[keyof typeof resources] 71 | ] || 72 | resources.en[actualKey as keyof typeof resources['en']] || 73 | (key as string) 74 | ); 75 | } catch { 76 | console.warn(`Translation key not found: ${key}`); 77 | return key as string; 78 | } 79 | }; 80 | 81 | // 设置语言的函数 82 | const handleSetLocale = async (locale: SupportedLanguages): Promise => { 83 | await setI18nLocale(locale); 84 | setCurrentLanguage(locale); 85 | }; 86 | 87 | return ( 88 | 95 | {children} 96 | 97 | ); 98 | }; 99 | 100 | // 自定义钩子 101 | export const useLanguage = (): LanguageContextType => { 102 | const context = useContext(LanguageContext); 103 | if (context === undefined) { 104 | throw new Error('useLanguage must be used within a LanguageProvider'); 105 | } 106 | 107 | return context; 108 | }; 109 | -------------------------------------------------------------------------------- /src/contexts/PositionContext.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from 'react'; 2 | import React, { createContext, useContext, useState } from 'react'; 3 | 4 | interface PositionConfig { 5 | enabled: boolean; 6 | left?: number; 7 | top?: number; 8 | } 9 | 10 | interface PositionContextType { 11 | positionConfig: PositionConfig; 12 | togglePositionMemory: () => void; 13 | updatePosition: (left: number, top: number) => void; 14 | } 15 | 16 | const PositionContext = createContext(undefined); 17 | 18 | export const PositionProvider: React.FC<{ children: ReactNode }> = ({ children }) => { 19 | const [positionConfig, setPositionConfig] = useState({ 20 | enabled: false, 21 | }); 22 | 23 | const togglePositionMemory = () => { 24 | setPositionConfig(prev => ({ 25 | ...prev, 26 | enabled: !prev.enabled 27 | })); 28 | }; 29 | 30 | const updatePosition = (left: number, top: number) => { 31 | setPositionConfig(prev => ({ 32 | ...prev, 33 | left, 34 | top 35 | })); 36 | }; 37 | 38 | return ( 39 | 40 | {children} 41 | 42 | ); 43 | }; 44 | 45 | export const usePositionConfig = (): PositionContextType => { 46 | const context = useContext(PositionContext); 47 | if (context === undefined) { 48 | throw new Error('usePositionConfig must be used within a PositionProvider'); 49 | } 50 | return context; 51 | }; -------------------------------------------------------------------------------- /src/db/index.ts: -------------------------------------------------------------------------------- 1 | import { Message } from '@/types/message'; 2 | import { MessageBlock } from '@/types/messageBlock'; 3 | import { Dexie, type EntityTable } from 'dexie'; 4 | 5 | export const db = new Dexie('AiDb') as Dexie & { 6 | topics: EntityTable<{ id: string; messages: Message[] }, 'id'>; 7 | message_blocks: EntityTable; 8 | }; 9 | 10 | db.version(1).stores({ 11 | topics: '&id, messages', 12 | message_blocks: 'id, messageId', 13 | }); 14 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext } from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import App from './popup/App'; 4 | import { LanguageProvider } from './contexts/LanguageContext'; 5 | import rootStore from '@/store'; 6 | 7 | const container = document.getElementById('root'); 8 | const root = createRoot(container!); 9 | const StoreContext = createContext(rootStore); 10 | 11 | root.render( 12 | 13 | 14 | 15 | 16 | 17 | 18 | , 19 | ); 20 | -------------------------------------------------------------------------------- /src/install/App.scss: -------------------------------------------------------------------------------- 1 | /* stylelint-disable selector-pseudo-element-colon-notation */ 2 | /* markdown-container 包裹所有内容 */ 3 | .markdown-container { 4 | display: flex; 5 | flex-direction: row; 6 | padding: 20px; 7 | } 8 | 9 | /* Markdown 内容区域 */ 10 | .markdown-content { 11 | width: 75%; 12 | margin-right: 20px; 13 | } 14 | 15 | /* 大纲区域 */ 16 | .table-of-contents { 17 | position: sticky; 18 | top: 20px; 19 | width: 25%; 20 | padding: 20px; 21 | background-color: #f4f4f4; 22 | border: 1px solid #ddd; 23 | } 24 | 25 | .table-of-contents ul { 26 | padding: 0; 27 | list-style-type: none; 28 | } 29 | 30 | .table-of-contents li { 31 | margin-bottom: 5px; 32 | } 33 | 34 | .table-of-contents a { 35 | color: #007bff; 36 | text-decoration: none; 37 | } 38 | 39 | .table-of-contents a:hover { 40 | text-decoration: underline; 41 | } 42 | 43 | /* 每个标题的折叠 */ 44 | .markdown-content h1, 45 | .markdown-content h2, 46 | .markdown-content h3 { 47 | margin: 20px 0; 48 | color: #333; 49 | cursor: pointer; 50 | } 51 | 52 | .markdown-content h1:before, 53 | .markdown-content h2:before, 54 | .markdown-content h3:before { 55 | color: #007bff; 56 | content: '+ '; 57 | } 58 | 59 | .markdown-content h1.collapsed:before, 60 | .markdown-content h2.collapsed:before, 61 | .markdown-content h3.collapsed:before { 62 | content: '- '; 63 | } 64 | 65 | /* 对折叠部分进行隐藏 */ 66 | .markdown-body .collapsed + p { 67 | display: none; 68 | } 69 | -------------------------------------------------------------------------------- /src/install/App.tsx: -------------------------------------------------------------------------------- 1 | import MarkdownIt from 'markdown-it'; 2 | import { useEffect, useState } from 'react'; 3 | 4 | import './App.scss'; 5 | 6 | function MarkdownRenderer() { 7 | const [markdownContent, setMarkdownContent] = useState(''); 8 | const [collapsedSections, setCollapsedSections] = useState({}); 9 | 10 | useEffect(() => { 11 | fetch('/doc/use.md') 12 | .then((response) => response.text()) 13 | .then((data) => { 14 | setMarkdownContent(data); 15 | }) 16 | .catch((error) => console.error('Error loading markdown:', error)); 17 | }, []); 18 | 19 | const md = new MarkdownIt(); 20 | const htmlContent = md.render(markdownContent); 21 | 22 | const toggleCollapse = (id: string) => { 23 | setCollapsedSections((prevState) => ({ 24 | ...prevState, 25 | // @ts-ignore 26 | [id]: !prevState[id], 27 | })); 28 | }; 29 | 30 | const renderMarkdownWithCollapse = () => { 31 | const parser = new DOMParser(); 32 | const htmlDoc = parser.parseFromString(htmlContent, 'text/html'); 33 | 34 | const headings = htmlDoc.querySelectorAll('h1, h2, h3, h4, h5, h6'); 35 | headings.forEach((heading) => { 36 | const id = heading.textContent?.toLowerCase().replace(/\s+/g, '-') || ''; 37 | heading.id = id; 38 | 39 | heading.addEventListener('click', () => toggleCollapse(id)); 40 | 41 | // @ts-ignore 42 | if (collapsedSections[id]) { 43 | heading.classList.add('collapsed'); 44 | let nextElement = heading.nextElementSibling; 45 | while (nextElement && !nextElement.matches('h1, h2, h3, h4, h5, h6')) { 46 | // @ts-ignore 47 | nextElement.style.display = 'none'; 48 | nextElement = nextElement.nextElementSibling; 49 | } 50 | } else { 51 | heading.classList.remove('collapsed'); 52 | let nextElement = heading.nextElementSibling; 53 | while (nextElement && !nextElement.matches('h1, h2, h3, h4, h5, h6')) { 54 | // @ts-ignore 55 | nextElement.style.display = ''; 56 | nextElement = nextElement.nextElementSibling; 57 | } 58 | } 59 | }); 60 | 61 | return htmlDoc.body.innerHTML; 62 | }; 63 | 64 | const renderTableOfContents = () => { 65 | return markdownContent.split('\n').map((line, index) => { 66 | if (line.startsWith('#')) { 67 | // @ts-ignore 68 | const level = line.match(/^#*/)[0].length; 69 | const title = line.replace(/^#*/, '').trim(); 70 | const id = title.toLowerCase().replace(/\s+/g, '-'); 71 | 72 | return ( 73 |
  • 74 | handleTocClick(e, id)}> 75 | {title} 76 | 77 |
  • 78 | ); 79 | } 80 | return null; 81 | }); 82 | }; 83 | 84 | // @ts-ignore 85 | const handleTocClick = (e, id) => { 86 | e.preventDefault(); 87 | const section = document.getElementById(id); 88 | if (section) { 89 | section.scrollIntoView({ behavior: 'smooth' }); 90 | 91 | setCollapsedSections((prevState) => ({ 92 | ...prevState, 93 | [id]: false, 94 | })); 95 | } 96 | }; 97 | 98 | return ( 99 |
    100 |
    101 |
    105 |
    106 | 107 |
    108 |

    大纲内容

    109 |
      {renderTableOfContents()}
    110 |
    111 |
    112 | ); 113 | } 114 | 115 | export default MarkdownRenderer; 116 | -------------------------------------------------------------------------------- /src/install/index.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client'; 2 | import { HashRouter } from 'react-router-dom'; 3 | 4 | import App from './App'; 5 | 6 | import './App.scss'; 7 | 8 | const container = document.querySelector('#root'); 9 | 10 | const root = createRoot(container!); 11 | 12 | root.render( 13 | 14 | 15 | , 16 | ); 17 | -------------------------------------------------------------------------------- /src/llmProviders/AiProvider.ts: -------------------------------------------------------------------------------- 1 | import { CompletionsParams, Model, Provider } from '@/types'; 2 | import BaseLlmProvider from './BaseLlmProvider'; 3 | import LlmProviderFactory from './LlmProviderFactory'; 4 | 5 | export default class AiProvider { 6 | private sdk: BaseLlmProvider; 7 | 8 | constructor(provider: Provider) { 9 | this.sdk = LlmProviderFactory.create(provider); 10 | } 11 | 12 | public async check( 13 | model: Model, 14 | stream: boolean = false, 15 | ): Promise<{ valid: boolean; error: Error | null }> { 16 | return this.sdk.check(model, stream); 17 | } 18 | 19 | public async models(provider: Provider): Promise { 20 | return this.sdk.models(provider); 21 | } 22 | 23 | public async completions({ 24 | messages, 25 | onChunk, 26 | onFilterMessages, 27 | }: CompletionsParams): Promise { 28 | return this.sdk.completions({ 29 | messages, 30 | onChunk, 31 | onFilterMessages, 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/llmProviders/BaseLlmProvider.ts: -------------------------------------------------------------------------------- 1 | import { CompletionsParams, Model, Provider } from '@/types'; 2 | import { formatApiHost } from '@/utils'; 3 | 4 | export default abstract class BaseLlmProvider { 5 | private provider: Provider; 6 | protected host: string; 7 | protected apiKey: string; 8 | 9 | constructor(provider: Provider) { 10 | this.provider = provider; 11 | this.host = this.getBaseURL(); 12 | this.apiKey = this.getApiKey(); 13 | } 14 | 15 | public getBaseURL(): string { 16 | const host = this.provider.apiHost; 17 | return formatApiHost(host); 18 | } 19 | 20 | public getApiKey(): string { 21 | return this.provider.apiKey; 22 | } 23 | 24 | public defaultHeaders() { 25 | return { 26 | // 'HTTP-Referer': '*', 27 | 'X-Title': 'AiAllSupport', 28 | 'X-Api-Key': this.apiKey, 29 | }; 30 | } 31 | 32 | abstract check(model: Model, stream: boolean): Promise<{ valid: boolean; error: Error | null }>; 33 | abstract models(provider: Provider): Promise; 34 | abstract completions({ messages, onChunk, onFilterMessages }: CompletionsParams): Promise; 35 | } 36 | -------------------------------------------------------------------------------- /src/llmProviders/LlmProviderFactory.ts: -------------------------------------------------------------------------------- 1 | import { Provider } from '@/types'; 2 | import OpenAiLlmProvider from './OpenAiLlmProvider'; 3 | 4 | export default class LlmProviderFactory { 5 | static create(provider: Provider) { 6 | switch (provider.id) { 7 | case 'openai': 8 | return new OpenAiLlmProvider(provider); 9 | default: 10 | return new OpenAiLlmProvider(provider); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/locales/i18n.ts: -------------------------------------------------------------------------------- 1 | import type { LocaleType, LocaleKey } from '@/locales'; 2 | import { locales, DEFAULT_LOCALE } from '@/locales'; 3 | import storage from '@/utils/storage'; 4 | 5 | // 使用更简单的实现 6 | const i18nState = { 7 | currentLocale: DEFAULT_LOCALE, 8 | listeners: [] as Array<(locale: LocaleType) => void>, 9 | initialized: false, 10 | }; 11 | 12 | // 初始化函数,在应用启动时调用 13 | const initializeLocale = async () => { 14 | if (i18nState.initialized) return; 15 | 16 | try { 17 | const savedLocale = await storage.getLocale(); 18 | if (savedLocale && Object.keys(locales).includes(savedLocale)) { 19 | i18nState.currentLocale = savedLocale as LocaleType; 20 | console.log('i18n service initialized with locale:', savedLocale); 21 | 22 | // 通知监听器初始语言设置 23 | i18nState.listeners.forEach((listener) => listener(i18nState.currentLocale)); 24 | 25 | // 广播初始语言设置到其他上下文 26 | window.dispatchEvent( 27 | new CustomEvent('localeChange', { 28 | detail: { locale: i18nState.currentLocale }, 29 | }), 30 | ); 31 | } else { 32 | console.log('Using default locale:', DEFAULT_LOCALE); 33 | } 34 | } catch (error) { 35 | console.error('Failed to initialize locale from storage:', error); 36 | } finally { 37 | i18nState.initialized = true; 38 | } 39 | }; 40 | 41 | // 立即开始初始化 42 | initializeLocale().catch(console.error); 43 | 44 | export const i18n = { 45 | getLocale: () => i18nState.currentLocale, 46 | 47 | setLocale: async (locale: LocaleType) => { 48 | if (i18nState.currentLocale === locale) return; 49 | 50 | i18nState.currentLocale = locale; 51 | await storage.setLocale(locale); 52 | 53 | // 通知监听器 54 | i18nState.listeners.forEach((listener) => listener(locale)); 55 | 56 | // 广播到其他上下文 57 | window.dispatchEvent(new CustomEvent('localeChange', { detail: { locale } })); 58 | 59 | // 告诉内容脚本保持窗口位置并更新翻译 60 | window.dispatchEvent(new CustomEvent('maintainChatPosition')); 61 | 62 | // 为内容脚本添加更详细的语言变更事件 63 | window.dispatchEvent( 64 | new CustomEvent('translationUpdate', { 65 | detail: { 66 | locale, 67 | translations: locales[locale], 68 | }, 69 | }), 70 | ); 71 | 72 | // 通知其他上下文,例如 background, popup 等 73 | if (chrome && chrome.runtime) { 74 | chrome.runtime.sendMessage({ action: 'localeChanged', locale }).catch(() => { 75 | /* 忽略错误 */ 76 | }); 77 | } 78 | }, 79 | 80 | translate: (key: LocaleKey) => { 81 | try { 82 | // 添加键名映射 83 | const keyMap: Record = { 84 | includeWebpageContent: 'includeWebpage', 85 | }; 86 | 87 | const actualKey = keyMap[key as string] || key; 88 | 89 | if (!locales[i18nState.currentLocale]) { 90 | return ( 91 | locales[DEFAULT_LOCALE][ 92 | actualKey as keyof typeof locales[typeof DEFAULT_LOCALE] 93 | ] || (key as string) 94 | ); 95 | } 96 | 97 | return ( 98 | locales[i18nState.currentLocale][ 99 | actualKey as keyof typeof locales[typeof i18nState.currentLocale] 100 | ] || 101 | locales[DEFAULT_LOCALE][actualKey as keyof typeof locales[typeof DEFAULT_LOCALE]] || 102 | (key as string) 103 | ); 104 | } catch { 105 | console.warn(`Translation key not found: ${key}`); 106 | return key as string; 107 | } 108 | }, 109 | 110 | subscribe: (callback: (locale: LocaleType) => void) => { 111 | i18nState.listeners.push(callback); 112 | return () => { 113 | const index = i18nState.listeners.indexOf(callback); 114 | if (index !== -1) { 115 | i18nState.listeners.splice(index, 1); 116 | } 117 | }; 118 | }, 119 | }; 120 | 121 | // 辅助函数 122 | export const t = (key: string): string => { 123 | const translation = i18n.translate(key as LocaleKey); 124 | // 如果翻译缺失并返回键名本身 125 | if (translation === key) { 126 | // 为特定键提供备选项 127 | const fallbacks: Record = { 128 | pin: 'Pin', 129 | unpin: 'Unpin', 130 | // 根据需要添加其他备选项 131 | }; 132 | return fallbacks[key] || key; 133 | } 134 | return translation; 135 | }; 136 | 137 | export const getLocale = (): LocaleType => i18n.getLocale(); 138 | export const setLocale = (locale: LocaleType): Promise => i18n.setLocale(locale); 139 | export const subscribeToLocaleChange = (callback: (locale: LocaleType) => void): (() => void) => 140 | i18n.subscribe(callback); 141 | -------------------------------------------------------------------------------- /src/locales/index.ts: -------------------------------------------------------------------------------- 1 | import { en } from './en'; 2 | import { zhCN } from './zh-CN'; 3 | import { zhTW } from './zh-TW'; 4 | import { ja } from './ja'; 5 | import { ko } from './ko'; 6 | import { de } from './de'; 7 | import { fr } from './fr'; 8 | import { es } from './es'; 9 | import { ru } from './ru'; 10 | 11 | export type LocaleKey = keyof typeof en; 12 | 13 | export const locales = { 14 | 'en': en, 15 | 'zh-CN': zhCN, 16 | 'zh-TW': zhTW, 17 | 'ja': ja, 18 | 'ko': ko, 19 | 'de': de, 20 | 'fr': fr, 21 | 'es': es, 22 | 'ru': ru, 23 | }; 24 | 25 | export type LocaleType = keyof typeof locales; 26 | 27 | export const DEFAULT_LOCALE: LocaleType = 'zh-CN'; 28 | -------------------------------------------------------------------------------- /src/locales/translationKeys.ts: -------------------------------------------------------------------------------- 1 | // 自动生成的翻译键类型定义,请勿直接修改 2 | // Generated on: 2025-03-12T03:10:00.168Z 3 | 4 | export type TranslationKey = 5 | | 'ok' 6 | | 'cancel' 7 | | 'save' 8 | | 'delete' 9 | | 'copy' 10 | | 'regenerate' 11 | | 'settings' 12 | | 'stop' 13 | | 'edit' 14 | | 'suggestedPrompt1' 15 | | 'suggestedPrompt2' 16 | | 'suggestedPrompt3' 17 | | 'suggestedPrompt4' 18 | | 'appTitle' 19 | | 'saveConfig' 20 | | 'savingConfig' 21 | | 'serviceProvider' 22 | | 'selectProvider' 23 | | 'apiKey' 24 | | 'enterApiKey' 25 | | 'getApiKey' 26 | | 'modelSelection' 27 | | 'selectModel' 28 | | 'showIcon' 29 | | 'setShortcuts' 30 | | 'starAuthor' 31 | | 'configSaved' 32 | | 'validatingApi' 33 | | 'apiValidSuccess' 34 | | 'savingConfigError' 35 | | 'aiAssistant' 36 | | 'askAnything' 37 | | 'exampleSummarize' 38 | | 'exampleMainPoints' 39 | | 'exampleHowToUse' 40 | | 'typeMessage' 41 | | 'send' 42 | | 'thinking' 43 | | 'think' 44 | | 'you' 45 | | 'assistant' 46 | | 'askWebpage' 47 | | 'sendMessage' 48 | | 'interfaceSettings' 49 | | 'errorProcessing' 50 | | 'errorRegenerating' 51 | | 'copied' 52 | | 'failedCopy' 53 | | 'codeCopied' 54 | | 'failedCodeCopy' 55 | | 'copyMessage' 56 | | 'selectProviderFirst' 57 | | 'unpinWindow' 58 | | 'pinWindow' 59 | | 'language' 60 | | 'languageEn' 61 | | 'languageZhCN' 62 | | 'languageZhTW' 63 | | 'languageJa' 64 | | 'languageKo' 65 | | 'languageChanged' 66 | | 'includeWebpage' 67 | | 'includeWebpageTooltip' 68 | | 'translate' 69 | | 'translatePrompt' 70 | | 'summarize' 71 | | 'summarizePrompt' 72 | | 'explain' 73 | | 'explainPrompt' 74 | | 'codeReview' 75 | | 'codeReviewPrompt' 76 | | 'rewrite' 77 | | 'rewritePrompt' 78 | | 'webSearch' 79 | | 'webSearchTooltip' 80 | | 'on' 81 | | 'off' 82 | | 'searchingWeb' 83 | | 'searchComplete' 84 | | 'noSearchResults' 85 | | 'exclusiveFeatureError' 86 | | 'exclusiveFeatureWarning' 87 | | 'webSearchResultsTips1' 88 | | 'webSearchResultsTips2' 89 | | 'Source' 90 | | 'close' 91 | | 'webpageContent' 92 | | 'webpagePrompt' 93 | | 'fetchWebpageContent' 94 | | 'fetchWebpageContentSuccess' 95 | | 'fetchWebpageContentFailed' 96 | | 'pleaseInputApiKey' 97 | | 'REFERENCE_PROMPT' 98 | | 'filteredDomains' 99 | | 'searchEngines' 100 | | 'openSettings' 101 | | 'openChat' 102 | | 'pressTip' 103 | | 'welcomeMessage' 104 | | 'tryAsking' 105 | | 'apiSettings' 106 | | 'interface' 107 | | 'search' 108 | | 'about' 109 | | 'tavilyApiKey' 110 | | 'enterTavilyApiKey' 111 | | 'getTavilyApiKey' 112 | | 'selectAtLeastOneSearchEngine' 113 | | 'noFilteredDomains' 114 | | 'enterDomainToFilter' 115 | | 'add' 116 | | 'enableWebSearchMessage' 117 | | 'aboutDescription' 118 | | 'autoSaving' 119 | | 'autoSaved' 120 | | 'autoSaveError' 121 | | 'validatingTavilyApi' 122 | | 'tavilyApiValidSuccess' 123 | | 'tavilyApiValidError' 124 | | 'feedback' 125 | | 'apiKeyNeeded' 126 | | 'enterQuestion' 127 | | 'systemPrompt' 128 | | 'modelListNotSupported' 129 | | 'pleaseSelectProvider' 130 | | 'pleaseEnterApiKey' 131 | | 'providerBaseUrlNotFound' 132 | | 'httpError' 133 | | 'invalidProviderData' 134 | | 'backgroundSearchFailed' 135 | | 'webContentFetchFailed' 136 | | 'baiduSearchFailed' 137 | | 'googleSearchFailed' 138 | | 'duckduckgoSearchFailed' 139 | | 'sogouSearchFailed' 140 | | 'braveSearchFailed' 141 | | 'searxngSearchFailed' 142 | | 'defaultTopicName'; 143 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import App from './chat/App'; 4 | import './index.css'; 5 | import './utils/debugDatabase'; // 导入调试工具 6 | 7 | ReactDOM.createRoot(document.getElementById('root')!).render( 8 | 9 | 10 | , 11 | ); 12 | -------------------------------------------------------------------------------- /src/manifest.ts: -------------------------------------------------------------------------------- 1 | import type { Manifest } from 'webextension-polyfill'; 2 | 3 | import pkg from '../package.json'; 4 | import { __DEV__ } from '../server/utils/constants'; 5 | 6 | const manifest: Manifest.WebExtensionManifest = { 7 | name: pkg.displayName, 8 | version: pkg.version, 9 | description: pkg.description, 10 | manifest_version: 3, 11 | minimum_chrome_version: pkg.browserslist.split(' ')[2], 12 | permissions: ['storage', 'declarativeNetRequest', 'contextMenus', 'commands', 'activeTab', 'scripting', 'sidePanel'], 13 | host_permissions: ['https://*/*', 'http://*/*'], 14 | content_security_policy: { 15 | extension_pages: "script-src 'self' http://localhost; object-src 'self';", 16 | }, 17 | web_accessible_resources: [ 18 | { 19 | matches: [''], 20 | resources: ['icons/*', 'images/*', 'fonts/*','*.html'], 21 | }, 22 | ], 23 | background: { 24 | service_worker: 'js/background.js', 25 | persistent: true, 26 | }, 27 | content_scripts: [ 28 | { 29 | matches: [''], 30 | css: ['css/all.css'], 31 | js: ['js/all.js', ...(__DEV__ ? [] : ['js/all.js'])], 32 | }, 33 | ], 34 | action: { 35 | default_popup: 'popup.html', 36 | default_icon: { 37 | '16': 'icons/icon16.png', 38 | '32': 'icons/icon32.png', 39 | '48': 'icons/icon48.png', 40 | '128': 'icons/icon128.png', 41 | }, 42 | }, 43 | options_ui: { 44 | page: 'options.html', 45 | open_in_tab: true, 46 | }, 47 | // @ts-ignore 48 | side_panel: { 49 | default_path: "sidepanel.html" 50 | }, 51 | icons: { 52 | '16': 'icons/icon16.png', 53 | '32': 'icons/icon32.png', 54 | '48': 'icons/icon48.png', 55 | '128': 'icons/icon128.png', 56 | }, 57 | commands: { 58 | 'open-chat': { 59 | suggested_key: { 60 | default: 'Ctrl+Shift+Y', 61 | mac: 'Command+Shift+Y', 62 | windows: 'Ctrl+Shift+Y', 63 | }, 64 | description: '打开 AI 聊天窗口.', 65 | }, 66 | }, 67 | }; 68 | if (!__DEV__) { 69 | manifest.content_scripts?.unshift({ 70 | matches: [''], 71 | js: ['js/vendor.js'], 72 | }); 73 | } 74 | 75 | export default manifest; 76 | -------------------------------------------------------------------------------- /src/options/components/About/index.tsx: -------------------------------------------------------------------------------- 1 | import { CommentOutlined, GithubOutlined, SettingOutlined } from '@ant-design/icons'; 2 | import { Typography } from 'antd'; 3 | import React from 'react'; 4 | 5 | import { t } from '@/locales/i18n'; 6 | import { GIT_URL } from '@/utils/constant'; 7 | 8 | interface AboutProps { 9 | onSetShortcuts: () => void; 10 | openFeedbackSurvey: () => void; 11 | } 12 | 13 | const About: React.FC = ({ onSetShortcuts, openFeedbackSurvey }) => { 14 | return ( 15 |
    16 | {t('appTitle')} 17 | {t('aboutDescription')} 18 |
    19 | 20 | {t('setShortcuts')} 21 | 22 | 23 | {t('starAuthor')} 24 | 25 | 26 | {t('feedback')} 27 | 28 |
    29 |
    30 | ); 31 | }; 32 | 33 | export default About; 34 | -------------------------------------------------------------------------------- /src/options/components/Footer/index.tsx: -------------------------------------------------------------------------------- 1 | import { CommentOutlined, GithubOutlined, SettingOutlined } from '@ant-design/icons'; 2 | import { Divider, Space, Typography } from 'antd'; 3 | 4 | import { t } from '@/locales/i18n'; 5 | import { GIT_URL } from '@/utils/constant'; 6 | 7 | export default function Footer({ 8 | onSetShortcuts, 9 | openFeedbackSurvey, 10 | }: { 11 | onSetShortcuts: () => void; 12 | openFeedbackSurvey: () => void; 13 | }) { 14 | return ( 15 |
    16 | }> 17 | 18 | {t('setShortcuts')} 19 | 20 | 21 | {t('starAuthor')} 22 | 23 | 24 | {t('feedback')} 25 | 26 | 27 |
    28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/options/components/Interface/index.tsx: -------------------------------------------------------------------------------- 1 | import { Form, Switch } from 'antd'; 2 | import React, { useEffect } from 'react'; 3 | 4 | import { t } from '@/locales/i18n'; 5 | import storage from '@/utils/storage'; 6 | 7 | interface InterfaceProps { 8 | form: any; 9 | } 10 | 11 | const Interface: React.FC = ({ form }) => { 12 | const onIsIconChange = (checked: boolean) => { 13 | storage.setIsChatBoxIcon(checked); 14 | }; 15 | 16 | const initData = async () => { 17 | const isChatBoxIcon = await storage.getIsChatBoxIcon(); 18 | const isUseWebpageContext = await storage.getUseWebpageContext(); 19 | form.setFieldsValue({ 20 | isIcon: isChatBoxIcon, 21 | useWebpageContext: isUseWebpageContext, 22 | }); 23 | }; 24 | 25 | useEffect(() => { 26 | initData(); 27 | }, []); 28 | 29 | return ( 30 |
    31 | 39 | onIsIconChange(checked)} /> 40 | 41 | 42 | 50 | { 52 | storage.setUseWebpageContext(checked); 53 | }} 54 | /> 55 | 56 |
    57 | ); 58 | }; 59 | 60 | export default Interface; 61 | -------------------------------------------------------------------------------- /src/options/components/Logger/index.tsx: -------------------------------------------------------------------------------- 1 | import { BugOutlined } from '@ant-design/icons'; 2 | import { Button, Card, Form, InputNumber, message, Radio, Space, Switch } from 'antd'; 3 | import React, { useState } from 'react'; 4 | 5 | import { t } from '@/locales/i18n'; 6 | import type { LoggerConfig } from '@/utils/logger'; 7 | import { clearLogs, getLoggerConfig, LogLevel, updateLoggerConfig } from '@/utils/logger'; 8 | 9 | const Logger: React.FC = () => { 10 | const [loggerConfig, setLoggerConfig] = useState(getLoggerConfig()); 11 | 12 | // Handler for clearing logs 13 | const handleClearLogs = async () => { 14 | try { 15 | await clearLogs(); 16 | message.success(t('options_logging_cleared')); 17 | } catch (error) { 18 | console.error('Failed to clear logs:', error); 19 | message.error(t('options_logging_clear_failed')); 20 | } 21 | }; 22 | 23 | // Handler for logging settings changes 24 | const handleLoggingSettingsChange = async (changedValues: any, _allValues: any) => { 25 | try { 26 | if (changedValues.logging) { 27 | const newConfig = await updateLoggerConfig(changedValues.logging); 28 | setLoggerConfig(newConfig); 29 | message.success(t('options_logging_settings_saved')); 30 | } 31 | } catch (error) { 32 | console.error('Failed to update logging settings:', error); 33 | message.error(t('options_logging_settings_save_failed')); 34 | } 35 | }; 36 | 37 | return ( 38 | 41 | 42 | {t('options_logging_settings')} 43 | 44 | } 45 | > 46 |
    51 | 56 | 57 | 58 | 59 | 60 | 61 | {t('options_logging_level_debug')} 62 | {t('options_logging_level_info')} 63 | {t('options_logging_level_warn')} 64 | {t('options_logging_level_error')} 65 | 66 | 67 | 68 | 73 | 74 | 75 | 76 | 81 | 82 | 83 | 84 | 89 | 90 | 91 | 92 | 96 | 97 | 98 | 99 | 100 | 103 | 104 |
    105 |
    106 | ); 107 | }; 108 | 109 | export default Logger; 110 | -------------------------------------------------------------------------------- /src/options/index.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client'; 2 | import { HashRouter } from 'react-router-dom'; 3 | import React, { createContext } from 'react'; 4 | 5 | import App from './App'; 6 | import { LanguageProvider } from '../contexts/LanguageContext'; 7 | 8 | import './App.scss'; 9 | import rootStore from '@/store'; 10 | 11 | const container = document.getElementById('root'); 12 | const root = createRoot(container!); 13 | const StoreContext = createContext(rootStore); 14 | 15 | root.render( 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | ; 25 | , 26 | ); 27 | -------------------------------------------------------------------------------- /src/popup/App.scss: -------------------------------------------------------------------------------- 1 | @import url('~@/styles/reset.scss'); 2 | 3 | .app { 4 | width: 500px; 5 | height: auto; 6 | padding: 20px; 7 | margin: 0 auto; 8 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 9 | sans-serif; 10 | opacity: 1; 11 | transition: opacity 300ms ease-out; 12 | 13 | &.fade-out { 14 | opacity: 0; 15 | } 16 | 17 | .app-container { 18 | padding: 20px; 19 | border-radius: 12px; 20 | box-shadow: 0 4px 12px rgb(0 0 0 / 8%); 21 | } 22 | 23 | .app-header { 24 | display: flex; 25 | align-items: center; 26 | justify-content: space-between; 27 | margin-bottom: 20px; 28 | 29 | .app-title { 30 | margin: 0; 31 | font-size: 24px; 32 | font-weight: 600; 33 | color: #1677ff; 34 | } 35 | 36 | .language-selector { 37 | min-width: 120px; 38 | 39 | &:hover { 40 | color: #1677ff; 41 | } 42 | } 43 | } 44 | 45 | .popup-content { 46 | display: flex; 47 | flex-direction: column; 48 | gap: 16px; 49 | padding: 20px 0; 50 | 51 | button { 52 | height: 50px; 53 | font-size: 16px; 54 | } 55 | } 56 | 57 | .app-footer { 58 | padding: 15px 0; 59 | margin-top: 20px; 60 | text-align: center; 61 | 62 | .footer-link { 63 | margin: 0 10px; 64 | font-size: 16px; 65 | color: #555; 66 | transition: all 0.3s; 67 | 68 | &:hover { 69 | color: #1677ff; 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/popup/index.tsx: -------------------------------------------------------------------------------- 1 | import './App.scss'; 2 | 3 | import { createRoot } from 'react-dom/client'; 4 | import React, { createContext } from 'react'; 5 | 6 | import App from './App'; 7 | import { LanguageProvider } from '../contexts/LanguageContext'; 8 | import rootStore from '@/store'; 9 | 10 | const container = document.getElementById('root'); 11 | 12 | const root = createRoot(container!); 13 | const StoreContext = createContext(rootStore); 14 | 15 | root.render( 16 | 17 | 18 | 19 | 20 | 21 | 22 | , 23 | ); 24 | -------------------------------------------------------------------------------- /src/services/MessageService.ts: -------------------------------------------------------------------------------- 1 | import { Model, Robot, RobotMessageStatus, Topic } from '@/types'; 2 | import { Message } from '@/types/message'; 3 | import { MessageBlock, MessageBlockStatus, MessageBlockType } from '@/types/messageBlock'; 4 | import llmStore from '@/store/llm'; 5 | import { v4 as uuidv4 } from 'uuid'; 6 | import { createBaseMessageBlock, createMessage } from '@/utils/message/create'; 7 | import rootStore from '@/store'; 8 | 9 | export function getUserMessage({ 10 | robot, 11 | topic, 12 | type, 13 | content, 14 | }: { 15 | robot: Robot; 16 | topic: Topic; 17 | type?: Message['type']; 18 | content?: string; 19 | }): { message: Message; blocks: MessageBlock[] } { 20 | const defaultModel = llmStore.defaultModel; 21 | const model = robot.model || defaultModel; 22 | const messageId = uuidv4(); 23 | const blocks: MessageBlock[] = []; 24 | const blockIds: string[] = []; 25 | 26 | if (content?.trim()) { 27 | const baseBlock = createBaseMessageBlock(messageId, MessageBlockType.MAIN_TEXT, { 28 | status: MessageBlockStatus.SUCCESS, 29 | }); 30 | const textBlock = { 31 | ...baseBlock, 32 | content, 33 | }; 34 | 35 | blocks.push(textBlock); 36 | blockIds.push(textBlock.id); 37 | } 38 | 39 | // 直接在createMessage中传入id 40 | const message = createMessage('user', topic.id, robot.id, { 41 | id: messageId, 42 | modelId: model?.id, 43 | model: model, 44 | blocks: blockIds, 45 | type, 46 | }); 47 | 48 | return { message, blocks }; 49 | } 50 | 51 | export function resetRobotMessage(message: Message, model?: Model): Message { 52 | const blockIdsToRemove = message.blocks; 53 | if (blockIdsToRemove.length > 0) { 54 | rootStore.messageBlockStore.removeManyBlocks(blockIdsToRemove); 55 | } 56 | 57 | return { 58 | ...message, 59 | model: model || message.model, 60 | modelId: model?.id || message.modelId, 61 | status: RobotMessageStatus.PENDING, 62 | useful: undefined, 63 | askId: undefined, 64 | blocks: [], 65 | createdAt: new Date().toISOString(), 66 | }; 67 | } 68 | 69 | export function getGroupedMessages(messages: Message[]): { 70 | [key: string]: (Message & { index: number })[]; 71 | } { 72 | const groups: { [key: string]: (Message & { index: number })[] } = {}; 73 | messages.forEach((message, index) => { 74 | // Use askId if available (should be on assistant messages), otherwise group user messages individually 75 | const key = 76 | message.role === 'assistant' && message.askId 77 | ? 'assistant' + message.askId 78 | : message.role + message.id; 79 | if (key && !groups[key]) { 80 | groups[key] = []; 81 | } 82 | groups[key].push({ ...message, index }); // Add message with its original index 83 | // Sort by index within group to maintain original order (ascending) 84 | groups[key].sort((a, b) => a.index - b.index); 85 | }); 86 | return groups; 87 | } 88 | -------------------------------------------------------------------------------- /src/services/ModelMessageService.ts: -------------------------------------------------------------------------------- 1 | import { Model } from '@/types'; 2 | import { ChatCompletionMessageParam } from 'openai/resources'; 3 | 4 | export function processReqMessages( 5 | model: Model, 6 | reqMessages: ChatCompletionMessageParam[], 7 | ): ChatCompletionMessageParam[] { 8 | if (!needStrictlyInterleaveUserAndAssistantMessages(model)) { 9 | return reqMessages; 10 | } 11 | 12 | return interleaveUserAndAssistantMessages(reqMessages); 13 | } 14 | 15 | function needStrictlyInterleaveUserAndAssistantMessages(model: Model) { 16 | return model.id === 'deepseek-reasoner'; 17 | } 18 | 19 | function interleaveUserAndAssistantMessages( 20 | messages: ChatCompletionMessageParam[], 21 | ): ChatCompletionMessageParam[] { 22 | if (!messages || messages.length === 0) { 23 | return []; 24 | } 25 | 26 | const processedMessages: ChatCompletionMessageParam[] = []; 27 | 28 | for (let i = 0; i < messages.length; i++) { 29 | const currentMessage = { ...messages[i] }; 30 | 31 | if (i > 0 && currentMessage.role === messages[i - 1].role) { 32 | // insert an empty message with the opposite role in between 33 | const emptyMessageRole = currentMessage.role === 'user' ? 'assistant' : 'user'; 34 | processedMessages.push({ 35 | role: emptyMessageRole, 36 | content: '', 37 | }); 38 | } 39 | 40 | processedMessages.push(currentMessage); 41 | } 42 | 43 | return processedMessages; 44 | } 45 | -------------------------------------------------------------------------------- /src/services/RobotService.ts: -------------------------------------------------------------------------------- 1 | import { Robot, Topic } from '@/types'; 2 | import { v4 as uuidv4 } from 'uuid'; 3 | import { robotList } from '../config/robot'; 4 | import { t } from '@/locales/i18n'; 5 | 6 | export function getDefaultTopic(assistantId: string): Topic { 7 | const now = new Date().toISOString(); 8 | return { 9 | id: uuidv4(), 10 | assistantId, 11 | createdAt: now, 12 | updatedAt: now, 13 | name: t('defaultTopicName'), 14 | messages: [], 15 | isNameManuallyEdited: false, 16 | }; 17 | } 18 | 19 | export function getDefaultRobot(): Robot { 20 | const robots = robotList; 21 | return { 22 | ...robots[0], 23 | type: 'assistant', 24 | topics: [getDefaultTopic(robots[0].id)], 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /src/services/StreamProcessingService.ts: -------------------------------------------------------------------------------- 1 | import { RobotMessageStatus } from '@/types'; 2 | import { Chunk, ChunkType } from '@/types/chunk'; 3 | 4 | export interface StreamProcessorCallbacks { 5 | // LLM response created 6 | onLLMResponseCreated?: () => void; 7 | // Text content chunk received 8 | onTextChunk?: (text: string) => void; 9 | // Full text content received 10 | onTextComplete?: (text: string) => void; 11 | // Thinking/reasoning content chunk received (e.g., from Claude) 12 | onThinkingChunk?: (text: string, thinking_millsec?: number) => void; 13 | onThinkingComplete?: (text: string, thinking_millsec?: number) => void; 14 | // Called when an error occurs during chunk processing 15 | onError?: (error: any) => void; 16 | // Called when the entire stream processing is signaled as complete (success or failure) 17 | onComplete?: (status: RobotMessageStatus, response?: Response) => void; 18 | } 19 | 20 | export function createStreamProcessor(callbacks: StreamProcessorCallbacks = {}) { 21 | // The returned function processes a single chunk or a final signal 22 | return (chunk: Chunk) => { 23 | try { 24 | // Logger.log(`[${new Date().toLocaleString()}] createStreamProcessor ${chunk.type}`, chunk) 25 | // 1. Handle the manual final signal first 26 | if (chunk?.type === ChunkType.BLOCK_COMPLETE) { 27 | callbacks.onComplete?.(RobotMessageStatus.SUCCESS, chunk?.response); 28 | return; 29 | } 30 | // 2. Process the actual ChunkCallbackData 31 | const data = chunk; // Cast after checking for 'final' 32 | // Invoke callbacks based on the fields present in the chunk data 33 | if (data.type === ChunkType.LLM_RESPONSE_CREATED && callbacks.onLLMResponseCreated) { 34 | callbacks.onLLMResponseCreated(); 35 | } 36 | if (data.type === ChunkType.TEXT_DELTA && callbacks.onTextChunk) { 37 | callbacks.onTextChunk(data.text); 38 | } 39 | if (data.type === ChunkType.TEXT_COMPLETE && callbacks.onTextComplete) { 40 | callbacks.onTextComplete(data.text); 41 | } 42 | if (data.type === ChunkType.THINKING_DELTA && callbacks.onThinkingChunk) { 43 | callbacks.onThinkingChunk(data.text); 44 | } 45 | if (data.type === ChunkType.THINKING_COMPLETE && callbacks.onThinkingComplete) { 46 | callbacks.onThinkingComplete(data.text, data.thinking_millsec); 47 | } 48 | } catch (error) { 49 | console.error('Error processing stream chunk:', error); 50 | callbacks.onError?.(error); 51 | } 52 | }; 53 | } 54 | 55 | export const createStreamCallback: StreamProcessorCallbacks = { 56 | onLLMResponseCreated: () => { 57 | // const baseBlock = createBaseMessageBlock(assistantMsgId, MessageBlockType.UNKNOWN, { 58 | // status: MessageBlockStatus.PROCESSING, 59 | // }); 60 | // handleBlockTransition(baseBlock as PlaceholderMessageBlock, MessageBlockType.UNKNOWN); 61 | }, 62 | onTextChunk: (text) => { 63 | // accumulatedContent += text; 64 | // if (lastBlockId) { 65 | // if (lastBlockType === MessageBlockType.UNKNOWN) { 66 | // const initialChanges: Partial = { 67 | // type: MessageBlockType.MAIN_TEXT, 68 | // content: accumulatedContent, 69 | // status: MessageBlockStatus.STREAMING, 70 | // citationReferences: citationBlockId ? [{ citationBlockId }] : [], 71 | // }; 72 | // mainTextBlockId = lastBlockId; 73 | // lastBlockType = MessageBlockType.MAIN_TEXT; 74 | // dispatch(updateOneBlock({ id: lastBlockId, changes: initialChanges })); 75 | // saveUpdatedBlockToDB(lastBlockId, assistantMsgId, topicId, getState); 76 | // } else if (lastBlockType === MessageBlockType.MAIN_TEXT) { 77 | // const blockChanges: Partial = { 78 | // content: accumulatedContent, 79 | // status: MessageBlockStatus.STREAMING, 80 | // }; 81 | // throttledBlockUpdate(lastBlockId, blockChanges); 82 | // // throttledBlockDbUpdate(lastBlockId, blockChanges) 83 | // } else { 84 | // const newBlock = createMainTextBlock(assistantMsgId, accumulatedContent, { 85 | // status: MessageBlockStatus.STREAMING, 86 | // citationReferences: citationBlockId ? [{ citationBlockId }] : [], 87 | // }); 88 | // handleBlockTransition(newBlock, MessageBlockType.MAIN_TEXT); 89 | // mainTextBlockId = newBlock.id; 90 | // } 91 | // } 92 | }, 93 | }; 94 | -------------------------------------------------------------------------------- /src/services/index.ts: -------------------------------------------------------------------------------- 1 | import { t } from '@/locales/i18n'; 2 | import type { IMessage } from '@/types'; 3 | import { requestAIStream, requestApi } from '@/utils'; 4 | import { SERVICE_MAP } from '@/utils/constant'; 5 | import storage from '@/utils/storage'; 6 | 7 | export const validateApiKey = async () => { 8 | const { selectedModel, selectedProvider } = await storage.getConfig(); 9 | 10 | if (!selectedProvider || !(selectedProvider in SERVICE_MAP)) { 11 | throw new Error(t('selectProvider')); 12 | } 13 | 14 | const url = SERVICE_MAP[selectedProvider as keyof typeof SERVICE_MAP].chat; 15 | const data = { 16 | model: selectedModel, 17 | messages: [{ role: 'user', content: 'test' }], 18 | stream: false, 19 | }; 20 | return requestApi(url, 'POST', data); 21 | }; 22 | 23 | export const chat = async (messages: IMessage[]) => { 24 | const { selectedModel, selectedProvider } = await storage.getConfig(); 25 | 26 | if (!selectedProvider || !(selectedProvider in SERVICE_MAP)) { 27 | throw new Error(t('selectProvider')); 28 | } 29 | const url = SERVICE_MAP[selectedProvider as keyof typeof SERVICE_MAP].chat; 30 | const data = { 31 | model: selectedModel, 32 | messages: [{ role: 'system', content: t('systemPrompt') }, ...messages], 33 | stream: true, 34 | }; 35 | return requestApi(url, 'POST', data); 36 | }; 37 | 38 | export const modelList = async (selectedProvider: string) => { 39 | if (!selectedProvider || !(selectedProvider in SERVICE_MAP)) { 40 | throw new Error(t('selectProvider')); 41 | } 42 | const service = SERVICE_MAP[selectedProvider as keyof typeof SERVICE_MAP]; 43 | if (!('modelList' in service)) { 44 | throw new Error(t('modelListNotSupported')); 45 | } 46 | const url = service.modelList; 47 | return requestApi(url); 48 | }; 49 | 50 | export const chatAIStream = async ( 51 | messages: IMessage[], 52 | onData: (chunk: { data: string; done: boolean }) => void, 53 | tabId?: string | null, 54 | ) => { 55 | const { selectedModel, selectedProvider } = await storage.getConfig(); 56 | 57 | if (!selectedProvider || !(selectedProvider in SERVICE_MAP)) { 58 | throw new Error(t('selectProvider')); 59 | } 60 | const url = SERVICE_MAP[selectedProvider as keyof typeof SERVICE_MAP].chat; 61 | console.log('urls', url); 62 | const data = { 63 | model: selectedModel, 64 | messages: [{ role: 'system', content: t('systemPrompt') }, ...messages], 65 | stream: true, 66 | }; 67 | return requestAIStream(url, 'POST', data, onData, tabId); 68 | }; 69 | -------------------------------------------------------------------------------- /src/sidepanel/SidePanel.scss: -------------------------------------------------------------------------------- 1 | .side-panel { 2 | display: flex; 3 | flex-direction: column; 4 | height: 100vh; 5 | width: 100%; 6 | background-color: #f5f5f5; 7 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 8 | 'Noto Sans', sans-serif; 9 | overflow: hidden; 10 | position: relative; 11 | 12 | .chat-header { 13 | display: flex; 14 | justify-content: space-between; 15 | align-items: center; 16 | padding: 12px 16px; 17 | background-color: white; 18 | border-bottom: 1px solid #e8e8e8; 19 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); 20 | z-index: 10; 21 | } 22 | 23 | .chat-messages { 24 | flex: 1; 25 | overflow-y: auto; 26 | padding: 16px; 27 | display: flex; 28 | flex-direction: column; 29 | gap: 16px; 30 | 31 | &::-webkit-scrollbar { 32 | width: 5px; 33 | } 34 | 35 | &::-webkit-scrollbar-thumb { 36 | background-color: rgba(0, 0, 0, 0.2); 37 | border-radius: 4px; 38 | } 39 | 40 | .empty-chat-message { 41 | display: flex; 42 | justify-content: center; 43 | align-items: center; 44 | height: 100%; 45 | text-align: center; 46 | opacity: 0.8; 47 | } 48 | 49 | .message { 50 | max-width: 85%; 51 | padding: 10px 14px; 52 | border-radius: 8px; 53 | word-break: break-word; 54 | 55 | &.user-message { 56 | align-self: flex-end; 57 | background-color: #e6f7ff; 58 | border: 1px solid #91caff; 59 | } 60 | 61 | &.ai-message { 62 | align-self: flex-start; 63 | background-color: white; 64 | border: 1px solid #d9d9d9; 65 | } 66 | 67 | .message-header { 68 | display: flex; 69 | justify-content: space-between; 70 | align-items: center; 71 | margin-bottom: 6px; 72 | } 73 | 74 | .message-actions { 75 | visibility: hidden; 76 | } 77 | 78 | &:hover .message-actions { 79 | visibility: visible; 80 | } 81 | 82 | .message-content { 83 | font-size: 14px; 84 | line-height: 1.5; 85 | 86 | .markdown-content { 87 | p { 88 | margin: 0.5em 0; 89 | 90 | &:first-child { 91 | margin-top: 0; 92 | } 93 | 94 | &:last-child { 95 | margin-bottom: 0; 96 | } 97 | } 98 | 99 | pre { 100 | background-color: #f5f5f5; 101 | border-radius: 4px; 102 | padding: 8px; 103 | overflow-x: auto; 104 | margin: 0.5em 0; 105 | } 106 | 107 | code { 108 | background-color: #f5f5f5; 109 | border-radius: 3px; 110 | padding: 0.2em 0.4em; 111 | font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; 112 | font-size: 0.9em; 113 | } 114 | } 115 | } 116 | } 117 | 118 | .loading-indicator { 119 | display: flex; 120 | align-items: center; 121 | gap: 8px; 122 | align-self: flex-start; 123 | padding: 8px 12px; 124 | background-color: white; 125 | border: 1px solid #d9d9d9; 126 | border-radius: 8px; 127 | } 128 | } 129 | 130 | .chat-input-container { 131 | padding: 12px 16px; 132 | background-color: white; 133 | border-top: 1px solid #e8e8e8; 134 | display: flex; 135 | flex-direction: column; 136 | gap: 8px; 137 | 138 | .stop-button, 139 | .regenerate-button { 140 | align-self: center; 141 | } 142 | 143 | .chat-form { 144 | display: flex; 145 | gap: 8px; 146 | 147 | .ant-input { 148 | resize: none; 149 | padding: 8px 12px; 150 | border-radius: 4px; 151 | border: 1px solid #d9d9d9; 152 | transition: all 0.3s; 153 | 154 | &:hover, 155 | &:focus { 156 | border-color: #40a9ff; 157 | } 158 | } 159 | 160 | button { 161 | align-self: flex-end; 162 | } 163 | } 164 | } 165 | } -------------------------------------------------------------------------------- /src/sidepanel/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import SidePanel from './SidePanel'; 4 | import '../styles/sidepanel.scss'; 5 | import { LanguageProvider } from '../contexts/LanguageContext'; 6 | 7 | const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); 8 | 9 | root.render( 10 | 11 | 12 | 13 | 14 | 15 | ); -------------------------------------------------------------------------------- /src/store/MessageBlockStore.ts: -------------------------------------------------------------------------------- 1 | import type { MessageBlock } from '@/types/messageBlock'; 2 | import { computed, makeAutoObservable } from 'mobx'; 3 | 4 | export class MessageBlockStore { 5 | // 可观察状态 6 | blocks = new Map(); 7 | 8 | constructor() { 9 | makeAutoObservable(this, { 10 | // 明确标记计算属性 11 | allBlocks: computed, 12 | blocksByMessage: computed, 13 | }); 14 | } 15 | 16 | // Actions - 状态修改方法 17 | upsertBlock(block: MessageBlock) { 18 | this.blocks.set(block.id, block); 19 | } 20 | 21 | upsertManyBlocks(blocks: MessageBlock[]) { 22 | blocks.forEach((block) => { 23 | this.blocks.set(block.id, block); 24 | }); 25 | } 26 | 27 | updateBlock(id: string, changes: Partial) { 28 | const block = this.blocks.get(id); 29 | if (block) { 30 | Object.assign(block, changes); 31 | } 32 | } 33 | 34 | removeBlock(id: string) { 35 | this.blocks.delete(id); 36 | } 37 | 38 | removeManyBlocks(ids: string[]) { 39 | ids.forEach((id) => { 40 | this.blocks.delete(id); 41 | }); 42 | } 43 | 44 | // Computed - 计算属性 45 | get allBlocks(): MessageBlock[] { 46 | return Array.from(this.blocks.values()); 47 | } 48 | 49 | get blocksByMessage(): Map { 50 | const result = new Map(); 51 | this.blocks.forEach((block) => { 52 | const messageId = block.messageId; 53 | if (!result.has(messageId)) { 54 | result.set(messageId, []); 55 | } 56 | result.get(messageId)!.push(block); 57 | }); 58 | return result; 59 | } 60 | 61 | // 获取指定消息的所有块 62 | getBlocksForMessage(messageId: string): MessageBlock[] { 63 | return this.blocksByMessage.get(messageId) || []; 64 | } 65 | 66 | // 根据ID获取块 67 | getBlockById(blockId: string): MessageBlock | undefined { 68 | return this.blocks.get(blockId); 69 | } 70 | 71 | // 获取所有块ID 72 | get allBlockIds(): string[] { 73 | return Array.from(this.blocks.keys()); 74 | } 75 | 76 | // 获取块实体字典 77 | get blockEntities(): Record { 78 | const entities: Record = {}; 79 | this.blocks.forEach((block, id) => { 80 | entities[id] = block; 81 | }); 82 | return entities; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/store/chromeStorageAdapter.ts: -------------------------------------------------------------------------------- 1 | type StorageKey = string | string[] | Record | null; 2 | type StorageResult = T extends string 3 | ? any 4 | : T extends string[] 5 | ? any[] 6 | : T extends Record 7 | ? Record 8 | : null; 9 | 10 | const chromeStorageAdapter = { 11 | getItem: async (key: T): Promise> => { 12 | return new Promise((resolve, reject) => { 13 | try { 14 | chrome.storage.local.get(key, (result) => { 15 | if (typeof key === 'string') { 16 | resolve(result[key] || null); 17 | } else if (Array.isArray(key)) { 18 | resolve(key.map((k) => result[k] || null) as StorageResult); 19 | } else if (key && typeof key === 'object') { 20 | resolve( 21 | Object.keys(key).reduce>((acc, k) => { 22 | acc[k] = result[k] || null; 23 | return acc; 24 | }, {}) as StorageResult, 25 | ); 26 | } else { 27 | resolve(null as StorageResult); 28 | } 29 | }); 30 | } catch (error) { 31 | reject(error); 32 | } 33 | }); 34 | }, 35 | setItem: async (key: T, value: any): Promise => { 36 | return new Promise((resolve, reject) => { 37 | try { 38 | if (typeof key === 'string') { 39 | chrome.storage.local.set({ [key]: value }, () => { 40 | if (chrome.runtime.lastError) { 41 | reject(chrome.runtime.lastError); 42 | return; 43 | } 44 | resolve(); 45 | }); 46 | } else if (Array.isArray(key)) { 47 | const stringKeys = key as string[]; 48 | const data = stringKeys.reduce>( 49 | (acc: Record, k: string) => ({ ...acc, [k]: value }), 50 | {}, 51 | ); 52 | chrome.storage.local.set(data, () => { 53 | if (chrome.runtime.lastError) { 54 | reject(chrome.runtime.lastError); 55 | return; 56 | } 57 | resolve(); 58 | }); 59 | } else if (key && typeof key === 'object') { 60 | chrome.storage.local.set(key, () => { 61 | if (chrome.runtime.lastError) { 62 | reject(chrome.runtime.lastError); 63 | return; 64 | } 65 | resolve(); 66 | }); 67 | } else { 68 | resolve(); 69 | } 70 | } catch (error) { 71 | reject(error); 72 | } 73 | }); 74 | }, 75 | removeItem: async (key: StorageKey): Promise => { 76 | return new Promise((resolve, reject) => { 77 | try { 78 | if (typeof key === 'string' || Array.isArray(key)) { 79 | chrome.storage.local.remove(key, () => { 80 | if (chrome.runtime.lastError) { 81 | reject(chrome.runtime.lastError); 82 | return; 83 | } 84 | resolve(); 85 | }); 86 | } else if (key && typeof key === 'object') { 87 | chrome.storage.local.remove(Object.keys(key), () => { 88 | if (chrome.runtime.lastError) { 89 | reject(chrome.runtime.lastError); 90 | return; 91 | } 92 | resolve(); 93 | }); 94 | } else { 95 | resolve(); 96 | } 97 | } catch (error) { 98 | reject(error); 99 | } 100 | }); 101 | }, 102 | }; 103 | 104 | export default chromeStorageAdapter; 105 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from 'react'; 2 | import llmStore from './llm'; 3 | import robotStore from './robot'; 4 | import { MessageStore } from './MessageStore'; 5 | import { MessageBlockStore } from './MessageBlockStore'; 6 | 7 | export class RootStore { 8 | llmStore = llmStore; 9 | robotStore = robotStore; 10 | messageStore: MessageStore; 11 | messageBlockStore: MessageBlockStore; 12 | 13 | constructor() { 14 | this.messageStore = new MessageStore(); 15 | this.messageBlockStore = new MessageBlockStore(); 16 | } 17 | } 18 | 19 | const rootStore = new RootStore(); 20 | 21 | const StoreContext = createContext(rootStore); 22 | export const useStore = () => useContext(StoreContext); 23 | 24 | export default rootStore; 25 | -------------------------------------------------------------------------------- /src/store/llm.ts: -------------------------------------------------------------------------------- 1 | // src/renderer/src/store/llmStore.ts 2 | import { makeAutoObservable } from 'mobx'; 3 | import { makePersistable } from 'mobx-persist-store'; 4 | // import { isLocalAi } from '@renderer/config/env'; 5 | import { uniqBy } from 'lodash'; 6 | import chromeStorageAdapter from './chromeStorageAdapter'; 7 | import { Model, Provider } from '@/types'; 8 | import { SYSTEM_MODELS } from '../config/models'; 9 | import { INITIAL_PROVIDERS } from '../config/providers'; 10 | 11 | class LlmStore { 12 | providers: Provider[] = INITIAL_PROVIDERS; 13 | defaultModel: Model; 14 | 15 | constructor() { 16 | makeAutoObservable(this); 17 | 18 | // 持久化数据存储 19 | makePersistable(this, { 20 | name: 'llm-store', 21 | properties: ['providers', 'defaultModel'], 22 | storage: chromeStorageAdapter as any, 23 | }); 24 | 25 | this.defaultModel = SYSTEM_MODELS.silicon[0]; 26 | } 27 | 28 | // 转换为动作方法 29 | updateProvider = (provider: Provider) => { 30 | this.providers = this.providers.map((p) => 31 | p.id === provider.id ? { ...p, ...provider } : p, 32 | ); 33 | }; 34 | 35 | updateProviders = (providers: Provider[]) => { 36 | this.providers = providers; 37 | }; 38 | 39 | addProvider = (provider: Provider) => { 40 | this.providers.unshift(provider); 41 | }; 42 | 43 | removeProvider = (provider: Provider) => { 44 | const index = this.providers.findIndex((p) => p.id === provider.id); 45 | if (index !== -1) { 46 | this.providers.splice(index, 1); 47 | } 48 | }; 49 | 50 | addModel = (providerId: string, model: Model) => { 51 | const provider = this.providers.find((p) => p.id === providerId); 52 | if (provider) { 53 | provider.models = uniqBy([...provider.models, model], 'id'); 54 | provider.enabled = true; 55 | } 56 | }; 57 | 58 | removeModel = (providerId: string, model: Model) => { 59 | const provider = this.providers.find((p) => p.id === providerId); 60 | if (provider) { 61 | provider.models = provider.models.filter((m: { id: any }) => m.id !== model.id); 62 | } 63 | }; 64 | 65 | setDefaultModel = (model: Model) => { 66 | this.defaultModel = model; 67 | }; 68 | 69 | updateModel = (providerId: string, model: Model) => { 70 | const provider = this.providers.find((p) => p.id === providerId); 71 | if (provider) { 72 | const modelIndex = provider.models.findIndex((m: { id: any }) => m.id === model.id); 73 | if (modelIndex !== -1) { 74 | provider.models[modelIndex] = model; 75 | } 76 | } 77 | }; 78 | 79 | moveProvider = (id: string, position: number) => { 80 | const index = this.providers.findIndex((p) => p.id === id); 81 | if (index === -1) return; 82 | 83 | const provider = this.providers[index]; 84 | this.providers.splice(index, 1); 85 | this.providers.splice(position - 1, 0, provider); 86 | }; 87 | } 88 | 89 | const llmStore = new LlmStore(); 90 | export default llmStore; 91 | -------------------------------------------------------------------------------- /src/styles/reset.scss: -------------------------------------------------------------------------------- 1 | @import url('../../node_modules/normalize.css/normalize.css'); 2 | 3 | html, 4 | body, 5 | p, 6 | ol, 7 | ul, 8 | li, 9 | dl, 10 | dt, 11 | dd, 12 | blockquote, 13 | figure, 14 | fieldset, 15 | legend, 16 | textarea, 17 | pre, 18 | iframe, 19 | hr, 20 | h1, 21 | h2, 22 | h3, 23 | h4, 24 | h5, 25 | h6 { 26 | padding: 0; 27 | margin: 0; 28 | } 29 | -------------------------------------------------------------------------------- /src/styles/sidepanel.scss: -------------------------------------------------------------------------------- 1 | @import 'normalize.css'; 2 | @import 'antd/dist/reset.css'; 3 | 4 | body { 5 | margin: 0; 6 | padding: 0; 7 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 8 | 'Noto Sans', sans-serif; 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | overflow: hidden; 12 | } 13 | 14 | #root { 15 | width: 100%; 16 | height: 100vh; 17 | } 18 | 19 | * { 20 | box-sizing: border-box; 21 | } -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "jsx": "react-jsx", 5 | "isolatedModules": true, 6 | /* Strict Type-Checking Options */ 7 | "strict": true, 8 | /* Additional Checks */ 9 | "noUnusedLocals": true, 10 | "noUnusedParameters": true, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | /* Module Resolution Options */ 14 | "moduleResolution": "node", 15 | "esModuleInterop": true, 16 | "resolveJsonModule": true, 17 | "baseUrl": ".", 18 | /* Experimental Options */ 19 | "experimentalDecorators": true, 20 | "emitDecoratorMetadata": true, 21 | /* Advanced Options */ 22 | "forceConsistentCasingInFileNames": true, 23 | "skipLibCheck": true, 24 | // 下面这些选项对 babel 编译 TypeScript 没有作用但是可以让 VSCode 等编辑器正确提示错误 25 | "target": "ES2022", 26 | "module": "ESNext", 27 | "paths": { 28 | "@/*": [ 29 | "./*" 30 | ] 31 | } 32 | }, 33 | "exclude": [ 34 | "manifest.ts" 35 | ] 36 | } -------------------------------------------------------------------------------- /src/types/chunk.ts: -------------------------------------------------------------------------------- 1 | export type ResponseError = Record; 2 | 3 | export enum ChunkType { 4 | BLOCK_CREATED = 'block_created', 5 | BLOCK_IN_PROGRESS = 'block_in_progress', 6 | LLM_RESPONSE_CREATED = 'llm_response_created', 7 | LLM_RESPONSE_IN_PROGRESS = 'llm_response_in_progress', 8 | TEXT_DELTA = 'text.delta', 9 | TEXT_COMPLETE = 'text.complete', 10 | AUDIO_DELTA = 'audio.delta', 11 | AUDIO_COMPLETE = 'audio.complete', 12 | IMAGE_CREATED = 'image.created', 13 | IMAGE_DELTA = 'image.delta', 14 | IMAGE_COMPLETE = 'image.complete', 15 | THINKING_DELTA = 'thinking.delta', 16 | THINKING_COMPLETE = 'thinking.complete', 17 | LLM_WEB_SEARCH_IN_PROGRESS = 'llm_websearch_in_progress', 18 | LLM_WEB_SEARCH_COMPLETE = 'llm_websearch_complete', 19 | LLM_RESPONSE_COMPLETE = 'llm_response_complete', 20 | BLOCK_COMPLETE = 'block_complete', 21 | ERROR = 'error', 22 | SEARCH_IN_PROGRESS_UNION = 'search_in_progress_union', 23 | SEARCH_COMPLETE_UNION = 'search_complete_union', 24 | } 25 | 26 | export interface BlockCreatedChunk { 27 | /** 28 | * The type of the chunk 29 | */ 30 | type: ChunkType.BLOCK_CREATED; 31 | } 32 | 33 | export interface BlockInProgressChunk { 34 | /** 35 | * The type of the chunk 36 | */ 37 | type: ChunkType.BLOCK_IN_PROGRESS; 38 | 39 | /** 40 | * The response 41 | */ 42 | response?: Response; 43 | } 44 | 45 | export interface LLMResponseCreatedChunk { 46 | /** 47 | * The response 48 | */ 49 | response?: Response; 50 | 51 | /** 52 | * The type of the chunk 53 | */ 54 | type: ChunkType.LLM_RESPONSE_CREATED; 55 | } 56 | 57 | export interface LLMResponseInProgressChunk { 58 | /** 59 | * The type of the chunk 60 | */ 61 | response?: Response; 62 | type: ChunkType.LLM_RESPONSE_IN_PROGRESS; 63 | } 64 | 65 | export interface TextDeltaChunk { 66 | /** 67 | * The text content of the chunk 68 | */ 69 | text: string; 70 | 71 | /** 72 | * The ID of the chunk 73 | */ 74 | chunk_id?: number; 75 | 76 | /** 77 | * The type of the chunk 78 | */ 79 | type: ChunkType.TEXT_DELTA; 80 | } 81 | 82 | export interface TextCompleteChunk { 83 | /** 84 | * The text content of the chunk 85 | */ 86 | text: string; 87 | 88 | /** 89 | * The ID of the chunk 90 | */ 91 | chunk_id?: number; 92 | 93 | /** 94 | * The type of the chunk 95 | */ 96 | type: ChunkType.TEXT_COMPLETE; 97 | } 98 | 99 | export interface ThinkingDeltaChunk { 100 | /** 101 | * The text content of the chunk 102 | */ 103 | text: string; 104 | 105 | /** 106 | * The thinking time of the chunk 107 | */ 108 | thinking_millsec?: number; 109 | 110 | /** 111 | * The type of the chunk 112 | */ 113 | type: ChunkType.THINKING_DELTA; 114 | } 115 | 116 | export interface ThinkingCompleteChunk { 117 | /** 118 | * The text content of the chunk 119 | */ 120 | text: string; 121 | 122 | /** 123 | * The thinking time of the chunk 124 | */ 125 | thinking_millsec?: number; 126 | 127 | /** 128 | * The type of the chunk 129 | */ 130 | type: ChunkType.THINKING_COMPLETE; 131 | } 132 | 133 | export interface BlockCompleteChunk { 134 | /** 135 | * The full response 136 | */ 137 | response?: Response; 138 | 139 | /** 140 | * The type of the chunk 141 | */ 142 | type: ChunkType.BLOCK_COMPLETE; 143 | 144 | /** 145 | * The error 146 | */ 147 | error?: ResponseError; 148 | } 149 | 150 | export interface ErrorChunk { 151 | error: ResponseError; 152 | 153 | type: ChunkType.ERROR; 154 | } 155 | 156 | export type Chunk = 157 | | BlockCreatedChunk // 消息块创建,无意义 158 | | BlockInProgressChunk // 消息块进行中,无意义 159 | | LLMResponseCreatedChunk // 大模型响应创建,返回即将创建的块类型 160 | | LLMResponseInProgressChunk // 大模型响应进行中 161 | | TextDeltaChunk // 文本内容生成中 162 | | TextCompleteChunk // 文本内容生成完成 163 | | ThinkingDeltaChunk // 思考内容生成中 164 | | ThinkingCompleteChunk // 思考内容生成完成 165 | | BlockCompleteChunk // 所有块创建完成,通常用于非流式处理;目前没有做区分 166 | | ErrorChunk; // 错误 167 | -------------------------------------------------------------------------------- /src/types/images.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.png' { 2 | const content: string; 3 | export default content; 4 | } 5 | 6 | declare module '*.webp' { 7 | const content: string; 8 | export default content; 9 | } 10 | 11 | declare module '*.svg' { 12 | const content: string; 13 | export default content; 14 | } 15 | -------------------------------------------------------------------------------- /src/types/index.d.ts: -------------------------------------------------------------------------------- 1 | declare type StyleSheetModule = { [key: string]: string }; 2 | 3 | declare module '*.scss' { 4 | const exports: StyleSheetModule; 5 | export default exports; 6 | } 7 | 8 | declare module '*.svg' { 9 | import * as React from 'react'; 10 | 11 | export const ReactComponent: React.FunctionComponent>; 12 | 13 | const src: string; 14 | export default src; 15 | } 16 | 17 | declare module '*.bmp' { 18 | const path: string; 19 | export default path; 20 | } 21 | 22 | declare module '*.gif' { 23 | const path: string; 24 | export default path; 25 | } 26 | 27 | declare module '*.jpg' { 28 | const path: string; 29 | export default path; 30 | } 31 | 32 | declare module '*.jpeg' { 33 | const path: string; 34 | export default path; 35 | } 36 | 37 | declare module '*.png' { 38 | const path: string; 39 | export default path; 40 | } 41 | -------------------------------------------------------------------------------- /src/types/message.ts: -------------------------------------------------------------------------------- 1 | import { Metrics, Usage, Model, RobotMessageStatus, UserMessageStatus, Robot, Topic } from '.'; 2 | 3 | import { MessageBlock } from './messageBlock'; 4 | 5 | export type Message = { 6 | id: string; 7 | role: 'user' | 'assistant' | 'system'; 8 | assistantId: string; 9 | topicId: string; 10 | createdAt: string; 11 | updatedAt?: string; 12 | status: UserMessageStatus | RobotMessageStatus; 13 | 14 | // 消息元数据 15 | modelId?: string; 16 | model?: Model; 17 | type?: 'clear'; 18 | isPreset?: boolean; 19 | useful?: boolean; 20 | askId?: string; // 关联的问题消息ID 21 | mentions?: Model[]; 22 | 23 | usage?: Usage; 24 | metrics?: Metrics; 25 | 26 | // UI相关 27 | multiModelMessageStyle?: 'horizontal' | 'vertical' | 'fold' | 'grid'; 28 | foldSelected?: boolean; 29 | 30 | // 块集合 31 | blocks: MessageBlock['id'][]; 32 | }; 33 | 34 | export interface InputMessage { 35 | robot: Robot; 36 | topic: Topic; 37 | content: string; 38 | } 39 | -------------------------------------------------------------------------------- /src/types/messageBlock.ts: -------------------------------------------------------------------------------- 1 | import { Model } from '.'; 2 | 3 | export enum MessageBlockType { 4 | UNKNOWN = 'unknown', // 未知类型,用于返回之前 5 | MAIN_TEXT = 'main_text', // 主要文本内容 6 | THINKING = 'thinking', // 思考过程(Claude、OpenAI-o系列等) 7 | TRANSLATION = 'translation', // Re-added 8 | CODE = 'code', // 代码块 9 | TOOL = 'tool', // Added unified tool block type 10 | ERROR = 'error', // 错误信息 11 | CITATION = 'citation', // 引用类型 (Now includes web search, grounding, etc.) 12 | } 13 | 14 | export enum MessageBlockStatus { 15 | PENDING = 'pending', // 等待处理 16 | PROCESSING = 'processing', // 正在处理,等待接收 17 | STREAMING = 'streaming', // 正在流式接收 18 | SUCCESS = 'success', // 处理成功 19 | ERROR = 'error', // 处理错误 20 | PAUSED = 'paused', // 处理暂停 21 | } 22 | 23 | export interface BaseMessageBlock { 24 | id: string; // 块ID 25 | messageId: string; // 所属消息ID 26 | type: MessageBlockType; // 块类型 27 | createdAt: string; // 创建时间 28 | updatedAt?: string; // 更新时间 29 | status: MessageBlockStatus; // 块状态 30 | model?: Model; // 使用的模型 31 | metadata?: Record; // 通用元数据 32 | error?: Record; // Added optional error field to base 33 | } 34 | 35 | export interface PlaceholderMessageBlock extends BaseMessageBlock { 36 | type: MessageBlockType.UNKNOWN; 37 | } 38 | 39 | export interface MainTextMessageBlock extends BaseMessageBlock { 40 | type: MessageBlockType.MAIN_TEXT; 41 | content: string; 42 | knowledgeBaseIds?: string[]; 43 | } 44 | 45 | export interface ThinkingMessageBlock extends BaseMessageBlock { 46 | type: MessageBlockType.THINKING; 47 | content: string; 48 | thinking_millsec?: number; 49 | } 50 | 51 | export interface CodeMessageBlock extends BaseMessageBlock { 52 | type: MessageBlockType.CODE; 53 | content: string; 54 | language: string; // 代码语言 55 | } 56 | 57 | export interface ErrorMessageBlock extends BaseMessageBlock { 58 | type: MessageBlockType.ERROR; 59 | } 60 | 61 | export type MessageBlock = 62 | | PlaceholderMessageBlock 63 | | MainTextMessageBlock 64 | | ThinkingMessageBlock 65 | | CodeMessageBlock 66 | | ErrorMessageBlock; 67 | -------------------------------------------------------------------------------- /src/utils/error.ts: -------------------------------------------------------------------------------- 1 | export const isAbortError = (error: any): boolean => { 2 | // 检查错误消息 3 | if (error?.message === 'Request was aborted.') { 4 | return true; 5 | } 6 | 7 | // 检查是否为 DOMException 类型的中止错误 8 | if (error instanceof DOMException && error.name === 'AbortError') { 9 | return true; 10 | } 11 | 12 | // 检查 OpenAI 特定的错误结构 13 | if ( 14 | error && 15 | typeof error === 'object' && 16 | (error.message === 'Request was aborted.' || 17 | error?.message?.includes('signal is aborted without reason')) 18 | ) { 19 | return true; 20 | } 21 | 22 | return false; 23 | }; 24 | -------------------------------------------------------------------------------- /src/utils/featureSettings.ts: -------------------------------------------------------------------------------- 1 | import { message } from 'antd'; 2 | import storage from './storage'; 3 | 4 | export const featureSettings = { 5 | /** 6 | * Toggle web search feature with mutual exclusivity check 7 | * @param checked New state 8 | * @param t Translation function for messages 9 | * @returns Promise resolving to the actual applied state 10 | */ 11 | toggleWebSearch: async (checked: boolean, t: Function): Promise => { 12 | if (checked) { 13 | const useWebpageContext = await storage.getUseWebpageContext(); 14 | if (useWebpageContext) { 15 | message.warning(t('exclusiveFeatureError')); 16 | return false; 17 | } 18 | } 19 | 20 | await storage.setWebSearchEnabled(checked); 21 | return checked; 22 | }, 23 | 24 | /** 25 | * Toggle webpage context feature with mutual exclusivity check 26 | * @param checked New state 27 | * @param t Translation function for messages 28 | * @returns Promise resolving to the actual applied state 29 | */ 30 | toggleWebpageContext: async (checked: boolean, t: Function): Promise => { 31 | if (checked) { 32 | const webSearchEnabled = await storage.getWebSearchEnabled(); 33 | if (webSearchEnabled) { 34 | message.warning(t('exclusiveFeatureError')); 35 | return false; 36 | } 37 | } 38 | 39 | await storage.setUseWebpageContext(checked); 40 | return checked; 41 | }, 42 | 43 | /** 44 | * Validate and submit form values with mutual exclusivity check for features 45 | * @param values Form values 46 | * @param t Translation function for messages 47 | * @returns Promise indicating if validation passed 48 | */ 49 | validateAndSubmitSettings: async (values: any, t: Function): Promise => { 50 | const { webSearchEnabled, useWebpageContext } = values; 51 | 52 | if (webSearchEnabled && useWebpageContext) { 53 | message.error(t('exclusiveFeatureError')); 54 | return false; 55 | } 56 | 57 | return true; 58 | }, 59 | }; 60 | -------------------------------------------------------------------------------- /src/utils/message/filters.ts: -------------------------------------------------------------------------------- 1 | import { Message } from '@/types/message'; 2 | import { remove } from 'lodash'; 3 | 4 | export function filterContextMessages(messages: Message[]): Message[] { 5 | const clearIndex = messages.findIndex((message: Message) => message.type === 'clear'); 6 | 7 | if (clearIndex === -1) { 8 | return messages; 9 | } 10 | 11 | return messages.slice(clearIndex + 1); 12 | } 13 | 14 | export function getGroupedMessages(messages: Message[]): { 15 | [key: string]: (Message & { index: number })[]; 16 | } { 17 | const groups: { [key: string]: (Message & { index: number })[] } = {}; 18 | messages.forEach((message, index) => { 19 | // Use askId if available (should be on assistant messages), otherwise group user messages individually 20 | const key = 21 | message.role === 'assistant' && message.askId 22 | ? 'assistant' + message.askId 23 | : message.role + message.id; 24 | if (key && !groups[key]) { 25 | groups[key] = []; 26 | } 27 | groups[key].push({ ...message, index }); // Add message with its original index 28 | // Sort by index within group to maintain original order 29 | groups[key].sort((a, b) => b.index - a.index); 30 | }); 31 | return groups; 32 | } 33 | 34 | export function filterUsefulMessages(messages: Message[]): Message[] { 35 | let _messages = [...messages]; 36 | const groupedMessages = getGroupedMessages(messages); 37 | 38 | Object.entries(groupedMessages).forEach(([key, groupedMsgs]) => { 39 | if (key.startsWith('assistant')) { 40 | const usefulMessage = groupedMsgs.find((m) => m.useful === true); 41 | if (usefulMessage) { 42 | // Remove all messages in the group except the useful one 43 | groupedMsgs.forEach((m) => { 44 | if (m.id !== usefulMessage.id) { 45 | remove(_messages, (o) => o.id === m.id); 46 | } 47 | }); 48 | } else if (groupedMsgs.length > 0) { 49 | // Keep only the last message if none are marked useful 50 | const messagesToRemove = groupedMsgs.slice(0, -1); 51 | messagesToRemove.forEach((m) => { 52 | remove(_messages, (o) => o.id === m.id); 53 | }); 54 | } 55 | } 56 | }); 57 | 58 | // Remove trailing assistant messages 59 | while (_messages.length > 0 && _messages[_messages.length - 1].role === 'assistant') { 60 | _messages.pop(); 61 | } 62 | 63 | // Filter adjacent user messages, keeping only the last one 64 | _messages = _messages.filter((message, index, origin) => { 65 | return !( 66 | message.role === 'user' && 67 | index + 1 < origin.length && 68 | origin[index + 1].role === 'user' 69 | ); 70 | }); 71 | 72 | return _messages; 73 | } 74 | -------------------------------------------------------------------------------- /src/utils/message/find.ts: -------------------------------------------------------------------------------- 1 | import rootStore from '@/store'; 2 | import { Message } from '@/types/message'; 3 | import { MainTextMessageBlock, MessageBlockType } from '@/types/messageBlock'; 4 | 5 | /** 6 | * 找出消息中的所有主文本块,不依赖 Redux 7 | * @param message - 消息对象 8 | * @returns 主文本块数组 9 | */ 10 | export const findMainTextBlocks = (message: Message): MainTextMessageBlock[] => { 11 | // 检查消息是否有效并包含块 12 | if (!message || !message.blocks || message.blocks.length === 0) { 13 | return []; 14 | } 15 | 16 | const textBlocks: MainTextMessageBlock[] = []; 17 | 18 | // Iterate through message blocks 19 | for (const blockId of message.blocks) { 20 | // Get block from store since blocks array contains IDs 21 | const block = rootStore.messageBlockStore.getBlockById(blockId); 22 | if (block && block.type === MessageBlockType.MAIN_TEXT) { 23 | textBlocks.push(block); 24 | } 25 | } 26 | 27 | return textBlocks; 28 | }; 29 | 30 | export const getMainTextContent = (message: Message): string => { 31 | const textBlocks = findMainTextBlocks(message); 32 | return textBlocks.map((block) => block.content).join('\n\n'); 33 | }; 34 | -------------------------------------------------------------------------------- /src/utils/messageUtils.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { ChatMessage } from '@/types'; 3 | import { flushSync } from 'react-dom'; 4 | 5 | /** 6 | * 更新消息列表中的特定消息 7 | * @param setMessages React 状态更新函数 8 | * @param messageId 需要更新的消息 ID 9 | * @param newMessage 新的消息对象 10 | */ 11 | export function updateMessage( 12 | setMessages: React.Dispatch>, 13 | messageId: number, 14 | newMessage: ChatMessage, 15 | ): void { 16 | // 使用 flushSync 确保每次更新后立即刷新 DOM 17 | flushSync(() => { 18 | setMessages((prev) => { 19 | const existingMessage = prev.find((msg) => msg.id === messageId); 20 | if (existingMessage) { 21 | return prev.map((msg) => (msg.id === messageId ? newMessage : msg)); 22 | } 23 | return [...prev, newMessage]; 24 | }); 25 | }); 26 | } 27 | 28 | /** 29 | * 创建一个系统消息对象 30 | * @param messageText 消息文本 31 | * @returns 新的系统消息对象 32 | */ 33 | export function createSystemMessage(messageText: string): ChatMessage { 34 | return { 35 | id: Date.now(), 36 | text: messageText, 37 | sender: 'system', 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /src/utils/queue.ts: -------------------------------------------------------------------------------- 1 | import PQueue from 'p-queue'; 2 | 3 | const requestQueues: { [topicId: string]: PQueue } = {}; 4 | 5 | export const getTopicQueue = (topicId: string, options = {}): PQueue => { 6 | if (!requestQueues[topicId]) requestQueues[topicId] = new PQueue(options); 7 | return requestQueues[topicId]; 8 | }; 9 | -------------------------------------------------------------------------------- /src/utils/robotUtils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 提取机器人名称的前半部分(用 - 分割) 3 | * @param fullName 完整的机器人名称 4 | * @returns 提取的前半部分名称 5 | */ 6 | export function getShortRobotName(fullName: string): string { 7 | if (!fullName) return ''; 8 | 9 | const parts = fullName.split(' - '); 10 | return parts[0] || fullName; 11 | } 12 | 13 | /** 14 | * 获取机器人名称的后半部分(英文部分) 15 | * @param fullName 完整的机器人名称 16 | * @returns 提取的后半部分名称 17 | */ 18 | export function getRobotEnglishName(fullName: string): string { 19 | if (!fullName) return ''; 20 | 21 | const parts = fullName.split(' - '); 22 | return parts[1] || ''; 23 | } 24 | 25 | /** 26 | * 获取机器人描述的前半部分(用 - 分割)并限制字数 27 | * @param description 完整的机器人描述 28 | * @param maxLength 最大字符数,默认20 29 | * @returns 处理后的描述 30 | */ 31 | export function getShortRobotDescription(description: string, maxLength: number = 20): string { 32 | if (!description) return ''; 33 | 34 | // 先提取 - 前的部分 35 | const parts = description.split(' - '); 36 | const shortDesc = parts[0] || description; 37 | 38 | // 限制字数 39 | if (shortDesc.length <= maxLength) { 40 | return shortDesc; 41 | } 42 | 43 | return shortDesc.substring(0, maxLength) + '...'; 44 | } 45 | -------------------------------------------------------------------------------- /src/utils/searchService.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { load } from 'cheerio'; 3 | 4 | // 搜索结果接口 5 | interface SearchResult { 6 | title: string; 7 | link: string; 8 | snippet: string; 9 | } 10 | 11 | // 使用免费搜索API或自建代理 12 | export async function performSearch(query: string): Promise { 13 | try { 14 | // 这里使用一个示例搜索API,实际实现可能需要替换 15 | const response = await axios.get('https://your-search-api.com/search', { 16 | params: { 17 | q: query, 18 | limit: 5, 19 | }, 20 | }); 21 | 22 | return response.data.results.map((result: any) => ({ 23 | title: result.title, 24 | link: result.link, 25 | snippet: result.snippet, 26 | })); 27 | } catch (error) { 28 | console.error('Search failed:', error); 29 | return []; 30 | } 31 | } 32 | 33 | // 抓取网页内容 34 | export async function fetchWebContent(url: string): Promise { 35 | try { 36 | const response = await axios.get(url); 37 | const $ = load(response.data); 38 | 39 | // 移除脚本、样式和不必要的元素 40 | $( 41 | 'script, style, nav, footer, header, aside, [role="banner"], [role="navigation"]', 42 | ).remove(); 43 | 44 | // 提取主要内容 45 | const title = $('title').text(); 46 | const mainContent = 47 | $('main, article, .content, #content, .main').text() || $('body').text(); 48 | 49 | // 清理文本 50 | const cleanedContent = mainContent.replace(/\s+/g, ' ').trim().substring(0, 8000); // 限制长度 51 | 52 | return `${title}\n\n${cleanedContent}`; 53 | } catch (error) { 54 | console.error('Failed to fetch content:', error); 55 | return ''; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/utils/webContentExtractor.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 从当前网页提取内容 3 | * @returns {Promise} 从网页提取的内容 4 | */ 5 | export async function extractWebpageContent(): Promise { 6 | try { 7 | // 获取页面标题 8 | const pageTitle = document.title; 9 | 10 | // 获取主要内容 11 | // 首先尝试查找主要内容区域 12 | const mainElements = document.querySelectorAll('main, article, [role="main"]'); 13 | let contentText = ''; 14 | 15 | if (mainElements.length > 0) { 16 | // 使用已识别的主要内容区域 17 | mainElements.forEach((element) => { 18 | // @ts-ignore 19 | contentText += `${element.innerText}\n\n`; 20 | }); 21 | } else { 22 | // 备选方案:获取正文文本但排除脚本、样式等 23 | const bodyText = document.body.innerText; 24 | contentText = bodyText; 25 | } 26 | 27 | // 获取当前URL 28 | const currentUrl = window.location.href; 29 | 30 | // 格式化提取的内容 31 | const extractedContent = ` 32 | URL: ${currentUrl} 33 | Title: ${pageTitle} 34 | Content:${contentText.slice(0, 15000)}${ 35 | contentText.length > 15000 ? '...(内容已截断)' : '' 36 | }`.trim(); 37 | 38 | return extractedContent; 39 | } catch (error) { 40 | console.error('提取网页内容时出错:', error); 41 | return '由于错误,无法提取网页内容。'; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | resolve: { 5 | alias: { 6 | '@': path.resolve(__dirname, 'src'), 7 | }, 8 | }, 9 | module: { 10 | rules: [ 11 | { 12 | test: /\.(js|jsx|ts|tsx)$/, 13 | exclude: /node_modules/, 14 | use: { 15 | loader: 'babel-loader', 16 | options: { 17 | // Remove any babel options from here if they're already in babel.config.js 18 | // Don't repeat @babel/plugin-transform-runtime here 19 | } 20 | } 21 | }, 22 | { 23 | test: /\.webp$/i, 24 | use: [ 25 | { 26 | loader: 'url-loader', 27 | options: { 28 | limit: 8192, 29 | fallback: 'file-loader', 30 | name: 'assets/[name].[ext]', 31 | }, 32 | }, 33 | ], 34 | }, 35 | { 36 | test: /\.(png|jpg|jpeg|gif|svg)$/i, 37 | type: 'asset/resource', 38 | generator: { 39 | filename: 'assets/[name][ext]' 40 | } 41 | }, 42 | // Other rules... 43 | ] 44 | } 45 | }; --------------------------------------------------------------------------------