├── .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 |

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 | 
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 | 
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 | 
11 |
12 | ### 选择 API 提供商 & 配置 API Key
13 |
14 | 1. **固定插件**:点击 Chrome 右上角 扩展程序按钮,找到 DeepSeekAllSupports 并固定到工具栏。
15 |
16 | 
17 |
18 | 2. **选择服务商**:点击插件图标,在弹出的界面选择 API 提供商。
19 | 3. **获取 API Key**:
20 |
21 | - 例如,服务商选择了“阿里云”,点击 “获取 API Key”,会跳转至 API Key 管理页面。
22 |
23 | 
24 |
25 | 
26 |
27 | - 在 API Key 页面点击 “创建 API Key”,填写必要信息后生成 API Key
28 |
29 | 
30 |
31 | 4. **填入 API Key**:复制 API Key 并粘贴到插件的 API Key 输入框 中,点击 “保存配置”。
32 | 
33 | 5. 测试 API 连接:保存 API Key 后,插件会自动进行 API 连接测试,成功后即可使用。
34 |
35 | 
36 |
37 | 
38 |
39 | ### 体验 AI 功能
40 |
41 | - **网页选中文本**:选中任意网页文本,点击 DeepSeek 图标,即可调起 AI 对话窗口。
42 |
43 | 
44 |
45 | 
46 |
47 | - **连续对话**:在对话窗口与 AI 进行多轮交流,获得流畅体验
48 |
49 | - **实时 AI 回复**:支持流式加载 AI 响应,提高交互效率。
50 |
51 | 
52 |
53 | ### 🔍 体验 R1 模型
54 |
55 | 选择 DeepSeek R1 模型,可观察 AI 的推理过程,直观感受其思维逻辑。
56 |
57 | 
58 | 
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 | 
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 | 
113 |
114 | ### 窗口可以自由调整
115 |
116 | 1. 鼠标浮到右下角,可以调整高度和宽度
117 | 2. 鼠标浮到顶部,可以拖拽窗口
118 | 3. 点击左上角 📍,可以固定窗口或者取消固定窗口
119 | 
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 |
--------------------------------------------------------------------------------
/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 |
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 |
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 |
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 |
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 | };
--------------------------------------------------------------------------------