初始化
This commit is contained in:
@@ -14,7 +14,6 @@ dist
|
||||
# 编辑器/IDE
|
||||
.idea
|
||||
.vscode
|
||||
.kiro
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
---
|
||||
inclusion: manual
|
||||
---
|
||||
|
||||
# Plasmo 框架技术说明
|
||||
|
||||
## 1️⃣ 框架定位
|
||||
|
||||
Plasmo 是一个 Chrome 扩展开发框架,核心价值是:**让你用写 React 项目的方式来写 Chrome 插件**。
|
||||
|
||||
它帮你处理了:
|
||||
- manifest.json 自动生成(不用手写)
|
||||
- TypeScript/React 编译打包(内置,不用配 webpack)
|
||||
- Content Script 样式隔离(Shadow DOM)
|
||||
- 开发时热更新(改代码自动刷新扩展)
|
||||
|
||||
## 2️⃣ 约定式文件结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── popup.tsx # 点击插件图标弹出的小窗口
|
||||
├── options.tsx # 插件设置页(右键图标→选项)
|
||||
├── newtab.tsx # 覆盖浏览器新标签页
|
||||
├── sidepanel.tsx # 浏览器侧边栏面板
|
||||
│
|
||||
├── background/
|
||||
│ └── index.ts # 后台 Service Worker,监听事件、做中转
|
||||
│
|
||||
├── contents/
|
||||
│ └── xxx.tsx / xxx.ts # 注入到目标网页里的脚本(可以有多个)
|
||||
│
|
||||
├── components/ # 自己的 UI 组件(非框架约定)
|
||||
│ └── Button.tsx
|
||||
│
|
||||
└── lib/ # 工具函数(非框架约定)
|
||||
└── api.ts
|
||||
```
|
||||
|
||||
**核心规则:文件放对位置就自动生效,不需要任何注册或配置。**
|
||||
|
||||
| 文件 | 有就生效,没有就没这功能 |
|
||||
|------|------------------------|
|
||||
| `popup.tsx` | 有 → 点图标弹窗;没有 → 点图标触发 background 事件 |
|
||||
| `options.tsx` | 有 → 有设置页;没有 → 没设置页 |
|
||||
| `newtab.tsx` | 有 → 新标签页被接管;没有 → 正常新标签页 |
|
||||
| `sidepanel.tsx` | 有 → 有浏览器侧边栏;没有 → 没有 |
|
||||
| `background/index.ts` | 有 → 有后台服务;没有 → 没后台逻辑 |
|
||||
| `contents/任意名.tsx` | 有几个就注入几个脚本到网页里 |
|
||||
|
||||
## 3️⃣ 文件后缀规则
|
||||
|
||||
跟目录无关,跟内容有关:
|
||||
|
||||
| 后缀 | 能写 HTML 标签(JSX)吗 | 用途 |
|
||||
|------|------------------------|------|
|
||||
| `.ts` | ❌ 不能 | 纯逻辑、工具函数、类型定义 |
|
||||
| `.tsx` | ✅ 能 | 任何需要写 `<div>`、`<button>` 等标签的文件 |
|
||||
|
||||
**简单记:有尖括号标签的用 `.tsx`,没有的用 `.ts`。**
|
||||
|
||||
## 4️⃣ TSX vs Vue 文件对比
|
||||
|
||||
| | `.vue` | `.tsx` |
|
||||
|--|--------|--------|
|
||||
| 框架 | Vue | React |
|
||||
| 结构 | template / script / style 三段式 | 全部写在一个函数里 |
|
||||
| HTML 写法 | 写在 `<template>` 里,正常 HTML | 写在 JS 的 return 里,叫 JSX |
|
||||
| 变量绑定 | `{{ msg }}` 双花括号 | `{msg}` 单花括号 |
|
||||
| 思路 | HTML 为主,往里面插逻辑 | JS 为主,在逻辑里写 HTML |
|
||||
|
||||
**一句话:`.vue` 是"HTML 里面嵌 JS",`.tsx` 是"JS 里面嵌 HTML"。**
|
||||
|
||||
## 5️⃣ Background Service Worker
|
||||
|
||||
### 执行时机
|
||||
- **没有固定执行时机**,完全由事件驱动
|
||||
- 有事件触发时唤醒,没事干 30 秒左右自动休眠
|
||||
- 全局只有一个实例
|
||||
|
||||
### 能做什么
|
||||
- 监听插件图标点击
|
||||
- 接收/转发消息(content script ↔ background)
|
||||
- 调用 `chrome.cookies`、`chrome.tabs` 等完整 Chrome API
|
||||
- 定时任务(必须用 `chrome.alarms`,不能用 `setInterval`)
|
||||
|
||||
### 不能做什么
|
||||
- 不能访问 DOM(没有页面)
|
||||
- 不能写持续运行的逻辑(会被休眠打断)
|
||||
|
||||
### 代码结构
|
||||
里面基本全是事件监听器注册:
|
||||
```ts
|
||||
chrome.action.onClicked.addListener(...) // 图标被点击
|
||||
chrome.runtime.onMessage.addListener(...) // 收到消息
|
||||
chrome.runtime.onInstalled.addListener(...) // 安装/更新
|
||||
```
|
||||
|
||||
## 6️⃣ Content Script
|
||||
|
||||
### 执行时机
|
||||
- 页面加载时自动注入(匹配 `matches` 规则的 URL)
|
||||
- 默认在 `document_idle` 时机注入(DOM 解析完成后)
|
||||
- 每个匹配的标签页各一个独立实例(内存不共享)
|
||||
|
||||
### 能做什么
|
||||
- 读写目标网页的 DOM
|
||||
- 往网页里注入 UI 组件(通过 Shadow DOM 隔离样式)
|
||||
- 通过 `chrome.runtime.sendMessage` 与 background 通信
|
||||
- 调用部分 Chrome API(storage、runtime 等)
|
||||
|
||||
### 不能做什么
|
||||
- 不能直接调用 `chrome.cookies`(需要通过 background 中转)
|
||||
- 不能访问其他标签页
|
||||
|
||||
### 两种用法
|
||||
| 用法 | 文件后缀 | 例子 |
|
||||
|------|----------|------|
|
||||
| 往网页里塞 UI | `.tsx` | 侧边栏面板、悬浮按钮 |
|
||||
| 只跑逻辑不画界面 | `.ts` | 读取页面数据、监听事件 |
|
||||
|
||||
## 7️⃣ Content Script UI(Shadow DOM)
|
||||
|
||||
当 content script 需要往网页里渲染 UI 时,Plasmo 自动用 Shadow DOM 包裹:
|
||||
|
||||
```
|
||||
目标网页
|
||||
├── 网页自己的 HTML、CSS
|
||||
└── Shadow DOM(隔离墙)
|
||||
└── 你的 React 组件 + 你的样式
|
||||
```
|
||||
|
||||
- 网页的 CSS 影响不到你的组件
|
||||
- 你的 CSS 也不会搞乱网页
|
||||
- 通过 `getStyle` 导出函数将样式注入 Shadow DOM 内部
|
||||
|
||||
## 8️⃣ Manifest 配置
|
||||
|
||||
Plasmo 不需要手写 `manifest.json`,通过 `package.json` 的 `manifest` 字段声明权限:
|
||||
|
||||
```json
|
||||
{
|
||||
"manifest": {
|
||||
"permissions": ["activeTab", "storage", "tabs", "cookies"],
|
||||
"host_permissions": ["<all_urls>"],
|
||||
"action": {}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
框架会自动合并源码中的信息(如 content script 的 matches 配置)生成最终的 manifest.json。
|
||||
|
||||
## 9️⃣ 开发工作流
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
npm install
|
||||
|
||||
# 开发模式(热更新)
|
||||
npm run dev
|
||||
# 产物在 build/chrome-mv3-dev
|
||||
|
||||
# 生产构建
|
||||
npm run build
|
||||
# 产物在 build/chrome-mv3-prod
|
||||
|
||||
# 打包 zip(用于上架)
|
||||
npm run package
|
||||
```
|
||||
|
||||
### 加载到 Chrome
|
||||
1. 打开 `chrome://extensions/`
|
||||
2. 右上角开启「开发者模式」
|
||||
3. 点击「加载已解压的扩展程序」
|
||||
4. 选择 `build/chrome-mv3-dev` 目录
|
||||
|
||||
### 查看日志
|
||||
| 代码位置 | 在哪看日志 |
|
||||
|----------|-----------|
|
||||
| Background | 扩展管理页 → 点击 "Service Worker" 链接 → 打开 DevTools |
|
||||
| Content Script | 目标网页 → F12 → Console |
|
||||
| Popup / SidePanel | 右键插件图标 → 审查弹出内容 |
|
||||
|
||||
## 🔟 环境变量
|
||||
|
||||
Plasmo 支持 `.env` 文件,变量必须以 `PLASMO_PUBLIC_` 前缀命名才能在代码中访问:
|
||||
|
||||
```env
|
||||
PLASMO_PUBLIC_API_URL=http://localhost:8080/api
|
||||
```
|
||||
|
||||
代码中通过 `process.env.PLASMO_PUBLIC_API_URL` 读取。
|
||||
|
||||
> 本项目未使用此机制,而是通过 `src/config.ts` 手动管理多环境配置。
|
||||
@@ -0,0 +1,49 @@
|
||||
---
|
||||
inclusion: always
|
||||
---
|
||||
|
||||
# 项目开发规范与注意事项
|
||||
|
||||
## 技术栈
|
||||
|
||||
- Plasmo + React + TypeScript
|
||||
- 样式:SCSS
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── background/
|
||||
│ └── index.ts # 插件后台脚本(Service Worker)
|
||||
├── components/
|
||||
│ ├── SidebarPanel.tsx # 侧边栏面板组件(主操作界面,自动填写入口)
|
||||
│ └── SidebarPanel.scss # 侧边栏样式
|
||||
├── contents/
|
||||
│ └── sidebar.tsx # Content Script 入口(注入侧边栏到页面)
|
||||
├── lib/
|
||||
│ ├── types.ts # 所有共享类型定义(简历接口、表单标签、匹配字段、UI组件库配置)
|
||||
│ ├── constants.ts # 常量数据(JOB_FORM_LABELS标签数组、TEST_FILL_DATA测试数据、UI_LIB_PICKER_CONFIGS组件库配置、SPECIAL_LABEL_PLACEHOLDERS特殊标签修正配置)
|
||||
│ ├── dom.ts # DOM工具(extractDomStructure提取DOM树、detectPageLanguage语言检测、isJobApplicationForm表单页判断、buildSelector选择器生成)
|
||||
│ ├── formMatcher.ts # 表单字段匹配(matchFormFields在DOM中匹配标签对应的input、findNearestInput查找input、fixSpecialLabelInput特殊标签修正)
|
||||
│ ├── pickerDetector.ts # 选择器识别(detectPickerField检测字段是否为选择器类型,通过UI组件库类名/提示文字/主动点击DOM差异对比三种方式)
|
||||
│ ├── pickerFill.ts # 选择器选项匹配与点击(fuzzyMatchScore模糊匹配、clickBestOptionInDropdown弹出层选项点击、findAndClickOptionInVisiblePopups全局弹出层搜索)
|
||||
│ ├── datePicker.ts # 日期选择器填写(fillDatePicker日期填写、tryClickMonth月份点击、tryClickDay日期点击)
|
||||
│ ├── autofill.ts # 自动填写核心(delay/forceSetValue工具函数、fillMatchedField字段填写入口、fillPickerField选择器填写含DOM差异对比、closePopup弹窗关闭、fillAllFields批量填写)
|
||||
│ ├── resumeUpload.ts # 简历上传(detectAndUploadResume检测上传按钮并从OSS下载注入文件)
|
||||
│ └── api.ts # API接口模块
|
||||
└── assets/
|
||||
├── icon.png # 插件图标
|
||||
└── icon1.png # 备用图标
|
||||
```
|
||||
|
||||
## 编码规范
|
||||
|
||||
- 页面结构和ts的常量变量和方法都要加中文注释
|
||||
- 重要逻辑加【注意】标记防止误删(如fillPickerField的DOM差异对比逻辑)
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 项目接口有Java后端和Python AI后端两种,如果是联调加入新接口,一定要确认有没有说是哪个后端的
|
||||
- fillPickerField中的DOM差异对比弹出层检测是核心机制,不要简化或删除
|
||||
- detectPickerField的方式3(主动点击检测DOM变化)是自建组件选择器识别的关键,不要删除
|
||||
- UI_LIB_PICKER_CONFIGS里不要加容易误匹配的配置(如之前Beisen Phoenix被误匹配到新东方自建网站)
|
||||
@@ -0,0 +1,135 @@
|
||||
---
|
||||
inclusion: manual
|
||||
---
|
||||
|
||||
# OfferPie Chrome Extension 项目结构说明
|
||||
|
||||
## 1️⃣ 项目整体层次
|
||||
```
|
||||
offferpie_google_extension/
|
||||
│
|
||||
├─ package.json # 依赖 + manifest 权限配置
|
||||
├─ tsconfig.json # TypeScript 配置(继承 Plasmo 模板)
|
||||
├─ .prettierrc.cjs # 代码格式化配置
|
||||
│
|
||||
├─ assets/ # 静态资源(图标等)
|
||||
│ ├─ icon.png
|
||||
│ └─ icon1.png
|
||||
│
|
||||
└─ src/ # 源码主目录
|
||||
├─ config.ts # **环境配置**(多环境切换:dev/prod)
|
||||
├─ constants.ts # **全局常量**(消息类型、存储 Key、缓存时间等)
|
||||
│
|
||||
├─ background/ # **Background Service Worker**
|
||||
│ └─ index.ts # 后台服务(监听图标点击、处理 Cookie 读取消息、安装事件)
|
||||
│
|
||||
├─ contents/ # **Content Scripts**(注入到目标网页的脚本)
|
||||
│ └─ sidebar.tsx # 侧边栏入口(Shadow DOM 样式隔离,监听消息控制显隐)
|
||||
│
|
||||
├─ components/ # **UI 组件**
|
||||
│ ├─ SidebarPanel.tsx # 侧边栏面板组件
|
||||
│ └─ SidebarPanel.scss # 侧边栏样式
|
||||
│
|
||||
├─ api/ # **接口层**(REST API 请求封装)
|
||||
│ ├─ request.ts # 通用请求方法(自动带 Token、统一错误处理、支持路径/query/body 参数)
|
||||
│ ├─ dataApi.ts # Java 后端接口(业务 CRUD)
|
||||
│ └─ aiApi.ts # Python AI 后端接口(AI 能力)
|
||||
│
|
||||
├─ utils/ # **工具层**(无业务依赖的通用工具)
|
||||
│ ├─ cookie.ts # Cookie 读取(通过消息中转 Background 获取)
|
||||
│ ├─ storage.ts # Chrome Storage 封装(chrome.storage.local 读写删)
|
||||
│ └─ auth.ts # 登录状态检查(带缓存,全局共享)
|
||||
│
|
||||
└─ lib/ # **业务逻辑层**(具体业务功能实现)
|
||||
├─ autofill.ts # 自动填表逻辑
|
||||
├─ dom.ts # DOM 操作工具
|
||||
├─ formMatcher.ts # 表单匹配器
|
||||
├─ pickerDetector.ts # 选择器检测
|
||||
├─ pickerFill.ts # 选择器填充
|
||||
├─ datePicker.ts # 日期选择器处理
|
||||
├─ resumeDataHelper.ts # 简历数据辅助
|
||||
├─ resumeUpload.ts # 简历上传
|
||||
├─ constants.ts # 业务常量
|
||||
└─ types.ts # 业务类型定义
|
||||
```
|
||||
|
||||
## 2️⃣ 各层模块职责
|
||||
| 层级 | 主要职责 | 关键文件 |
|
||||
|------|----------|----------|
|
||||
| **config** | 环境配置管理,支持 dev/prod 切换 | `config.ts`(dataBaseApi、aiBaseApi、cookieSourceUrl) |
|
||||
| **constants** | 全局常量定义 | `constants.ts`(MSG_TYPES、STORAGE_KEYS、LOGIN_CHECK_INTERVAL) |
|
||||
| **background** | 后台事件处理:图标点击、Cookie 读取中转、安装事件 | `background/index.ts` |
|
||||
| **contents** | 注入目标网页的脚本,负责 UI 渲染和页面交互 | `contents/sidebar.tsx`(侧边栏面板注入) |
|
||||
| **components** | 可复用 React UI 组件 | `SidebarPanel.tsx` |
|
||||
| **api** | 接口请求封装,自动携带 Token,统一错误处理 | `request.ts`(通用封装)、`dataApi.ts`(Java 端)、`aiApi.ts`(Python 端) |
|
||||
| **utils** | 无业务依赖的通用工具 | `cookie.ts`(Cookie 获取)、`storage.ts`(持久化存储)、`auth.ts`(登录检查) |
|
||||
| **lib** | 具体业务功能实现(自动填表、DOM 操作、简历处理等) | `autofill.ts`、`formMatcher.ts`、`resumeUpload.ts` 等 |
|
||||
|
||||
## 3️⃣ 技术栈
|
||||
| 类别 | 技术选型 | 说明 |
|
||||
|------|----------|------|
|
||||
| **扩展框架** | Plasmo 0.90 | Chrome MV3 扩展开发框架,约定式文件结构 |
|
||||
| **UI 框架** | React 19 | 组件化 UI 开发 |
|
||||
| **UI 组件库** | Arco Design (web-react) | 字节跳动 React 组件库 |
|
||||
| **语言** | TypeScript 5.8 | 类型安全 |
|
||||
| **样式** | SCSS | 组件样式,通过 Shadow DOM 隔离 |
|
||||
|
||||
## 4️⃣ Plasmo 框架约定
|
||||
| 文件/目录 | 框架行为 |
|
||||
|-----------|----------|
|
||||
| `src/background/index.ts` | 自动注册为 Background Service Worker |
|
||||
| `src/contents/*.tsx` | 自动注册为 Content Script(带 UI,Shadow DOM 隔离) |
|
||||
| `src/contents/*.ts` | 自动注册为 Content Script(纯逻辑,无 UI) |
|
||||
| `popup.tsx`(未使用) | 点击图标弹出的小窗口 |
|
||||
| `options.tsx`(未使用) | 扩展设置页 |
|
||||
| `sidepanel.tsx`(未使用) | 浏览器侧边栏面板 |
|
||||
| `package.json → manifest` | 自动合并生成 manifest.json |
|
||||
|
||||
## 5️⃣ 通信架构
|
||||
```
|
||||
Content Script(页面内)
|
||||
│
|
||||
│ chrome.runtime.sendMessage
|
||||
│ ← GET_TOKEN / GET_COOKIES →
|
||||
▼
|
||||
Background Service Worker(后台)
|
||||
│
|
||||
│ chrome.cookies.get / getAll
|
||||
▼
|
||||
Chrome Cookie Store(公司网站域名下的 Cookie)
|
||||
```
|
||||
|
||||
- Content Script 不直接读 Cookie,统一通过 Background 中转
|
||||
- Background 按需唤醒,处理完消息后自动休眠
|
||||
- Token 从公司网站 Cookie 中获取,复用页面登录状态
|
||||
|
||||
## 6️⃣ 鉴权机制
|
||||
- **Token 来源**:通过 `chrome.cookies.get` 从公司网站域名(cookieSourceUrl)读取名为 `Token` 的 Cookie
|
||||
- **请求携带**:`api/request.ts` 每次请求前自动获取 Token,塞入请求头 `Token` 字段
|
||||
- **登录检查**:`utils/auth.ts` 提供 `ensureLogin()` 方法,调用后端 `checkLogin` 接口验证 Token 有效性
|
||||
- **缓存策略**:登录检查结果缓存到 `chrome.storage.local`,5 分钟内全局不重复请求;失败立即清缓存
|
||||
|
||||
## 7️⃣ 与后端的关系
|
||||
| 后端 | 基础地址 | 用途 |
|
||||
|------|----------|------|
|
||||
| **Java 端**(back-end) | `http://localhost:8080/api` | 业务 CRUD:用户、简历、岗位、投递等 |
|
||||
| **Python AI 端**(offerpie_python_ai) | `http://localhost:8000` | AI 能力:智能分析、简历解析、对话等 |
|
||||
|
||||
- 两个后端共享同一套 Token 鉴权体系
|
||||
- 插件通过读取公司网站 Cookie 复用登录状态,无需单独登录
|
||||
|
||||
## 8️⃣ Manifest 权限
|
||||
| 权限 | 用途 |
|
||||
|------|------|
|
||||
| `activeTab` | 访问当前活动标签页 |
|
||||
| `storage` | 使用 chrome.storage.local 持久化数据 |
|
||||
| `tabs` | 操作标签页(发送消息等) |
|
||||
| `cookies` | 读取公司网站域名下的 Cookie(获取 Token) |
|
||||
| `host_permissions: <all_urls>` | Content Script 注入所有页面 + 跨域请求 |
|
||||
|
||||
## 9️⃣ 构建与运行
|
||||
- **安装依赖**:`npm install`
|
||||
- **开发模式**:`npm run dev`(热更新,产物在 `build/chrome-mv3-dev`)
|
||||
- **生产构建**:`npm run build`(产物在 `build/chrome-mv3-prod`)
|
||||
- **打包发布**:`npm run package`(生成 zip)
|
||||
- **加载到 Chrome**:`chrome://extensions` → 开发者模式 → 加载已解压的扩展程序 → 选择 build 目录
|
||||
+2
-1
@@ -24,7 +24,8 @@
|
||||
"permissions": [
|
||||
"activeTab",
|
||||
"storage",
|
||||
"tabs"
|
||||
"tabs",
|
||||
"cookies"
|
||||
],
|
||||
"host_permissions": [
|
||||
"<all_urls>"
|
||||
|
||||
Generated
+6349
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,10 @@
|
||||
import { config } from "~config"
|
||||
import { createHttp } from "./request"
|
||||
|
||||
/** Python AI 后端接口 */
|
||||
const http = createHttp(config.aiBaseApi)
|
||||
|
||||
/** 健康检查 */
|
||||
export function healthCheck() {
|
||||
return http.get('/health/')
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { config } from "~config"
|
||||
import { createHttp } from "./request"
|
||||
|
||||
/** Java 后端接口 */
|
||||
const http = createHttp(config.dataBaseApi)
|
||||
|
||||
/** 根据岗位来源地址查询岗位信息 */
|
||||
export function findJobBySourceUrl(sourceUrl: string) {
|
||||
return http.get('/job/findByUrl', { params: { sourceUrl } })
|
||||
}
|
||||
|
||||
/** 校验登录状态 */
|
||||
export function checkLogin() {
|
||||
return http.get('/public/checkLogin')
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { getCookieValue } from "~utils/cookie"
|
||||
|
||||
interface ApiResult<T = unknown> {
|
||||
code: string | number
|
||||
msg: string
|
||||
data: T
|
||||
timestamp: string
|
||||
uuid: string
|
||||
}
|
||||
|
||||
interface RequestOptions {
|
||||
/** 路径参数,替换 url 中的 :key 占位符 */
|
||||
pathParams?: Record<string, string | number>
|
||||
/** query 参数,拼接到 url 后面 */
|
||||
params?: Record<string, string>
|
||||
/** 请求体 */
|
||||
body?: unknown
|
||||
/** 自定义请求头 */
|
||||
headers?: Record<string, string>
|
||||
}
|
||||
|
||||
/** 替换路径参数,如 /user/:id → /user/123 */
|
||||
function resolvePath(url: string, pathParams?: Record<string, string | number>): string {
|
||||
if (!pathParams) return url
|
||||
return Object.entries(pathParams).reduce(
|
||||
(path, [key, value]) => path.replace(`:${key}`, String(value)),
|
||||
url
|
||||
)
|
||||
}
|
||||
|
||||
async function request<T>(baseUrl: string, url: string, method: string, options: RequestOptions = {}): Promise<T> {
|
||||
const token = await getCookieValue('Token')
|
||||
const headers: HeadersInit = {
|
||||
'Content-Type': 'application/json',
|
||||
...(token ? { Token: token } : {}),
|
||||
...options.headers,
|
||||
}
|
||||
|
||||
const resolvedPath = resolvePath(url, options.pathParams)
|
||||
const fullUrl = options.params
|
||||
? `${baseUrl}${resolvedPath}?${new URLSearchParams(options.params)}`
|
||||
: `${baseUrl}${resolvedPath}`
|
||||
|
||||
const response = await fetch(fullUrl, {
|
||||
method,
|
||||
headers,
|
||||
body: options.body ? JSON.stringify(options.body) : undefined,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`)
|
||||
}
|
||||
|
||||
const result: ApiResult<T> = await response.json()
|
||||
// eslint-disable-next-line eqeqeq
|
||||
if (result.code != 0) throw new Error(result.msg || '请求失败')
|
||||
return result.data
|
||||
}
|
||||
|
||||
function createHttp(baseUrl: string) {
|
||||
return {
|
||||
get: <T>(url: string, options?: RequestOptions) =>
|
||||
request<T>(baseUrl, url, 'GET', options),
|
||||
post: <T>(url: string, options?: RequestOptions) =>
|
||||
request<T>(baseUrl, url, 'POST', options),
|
||||
put: <T>(url: string, options?: RequestOptions) =>
|
||||
request<T>(baseUrl, url, 'PUT', options),
|
||||
delete: <T>(url: string, options?: RequestOptions) =>
|
||||
request<T>(baseUrl, url, 'DELETE', options),
|
||||
}
|
||||
}
|
||||
|
||||
export { createHttp }
|
||||
+34
-11
@@ -1,19 +1,42 @@
|
||||
/**
|
||||
* Background Service Worker
|
||||
* 插件的后台服务,负责:
|
||||
* 1. 监听插件图标点击事件,通知 Content Script 切换侧边栏
|
||||
* 2. 后续可扩展:定时任务调度、跨页面状态管理、与后端的长连接等
|
||||
*/
|
||||
import { config } from "~config"
|
||||
import { MSG_TYPES } from "~constants"
|
||||
|
||||
export {}
|
||||
|
||||
/**
|
||||
* 监听插件图标点击事件
|
||||
* 由于 manifest 中 action 没有设置 default_popup,
|
||||
* 点击图标会触发此事件,向当前活动标签页发送切换侧边栏的消息
|
||||
*/
|
||||
/** 监听插件图标点击,切换侧边栏 */
|
||||
chrome.action.onClicked.addListener(async (tab) => {
|
||||
if (tab.id) {
|
||||
chrome.tabs.sendMessage(tab.id, { type: "TOGGLE_SIDEBAR" })
|
||||
}
|
||||
})
|
||||
|
||||
/** 监听消息:处理 Cookie 读取请求 */
|
||||
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
||||
// 获取所有 Cookie
|
||||
if (message.type === MSG_TYPES.GET_COOKIES) {
|
||||
const targetUrl = message.url || config.cookieSourceUrl
|
||||
const { hostname } = new URL(targetUrl)
|
||||
chrome.cookies.getAll({ domain: hostname, url: targetUrl }).then((cookies) => {
|
||||
sendResponse({ cookies })
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
// 获取单个 Cookie 值
|
||||
if (message.type === MSG_TYPES.GET_TOKEN) {
|
||||
const targetUrl = message.url || config.cookieSourceUrl
|
||||
chrome.cookies.get({ url: targetUrl, name: message.name }).then((cookie) => {
|
||||
sendResponse({ value: cookie?.value || null })
|
||||
})
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
/** 插件安装事件 */
|
||||
chrome.runtime.onInstalled.addListener((details) => {
|
||||
if (details.reason === "install") {
|
||||
console.log("[OfferPie] 插件安装成功")
|
||||
} else if (details.reason === "update") {
|
||||
console.log("[OfferPie] 插件已更新")
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* 环境配置
|
||||
*/
|
||||
|
||||
/** 当前环境,手动切换 */
|
||||
const ENV = 'dev'
|
||||
|
||||
/** 各环境配置 */
|
||||
const envConfigs: Record<string, {
|
||||
dataBaseApi: string
|
||||
aiBaseApi: string
|
||||
cookieSourceUrl: string
|
||||
}> = {
|
||||
dev: {
|
||||
dataBaseApi: 'http://localhost:8080/api',
|
||||
aiBaseApi: 'http://localhost:8000',
|
||||
cookieSourceUrl: 'http://192.168.31.135:5173',
|
||||
},
|
||||
prod: {
|
||||
dataBaseApi: 'https://your-domain.com/api',
|
||||
aiBaseApi: 'https://your-domain.com/ai',
|
||||
cookieSourceUrl: 'https://your-domain.com',
|
||||
},
|
||||
}
|
||||
|
||||
/** 根据当前环境返回配置 */
|
||||
export function getConfig() {
|
||||
return envConfigs[ENV] || envConfigs.dev
|
||||
}
|
||||
|
||||
export const config = getConfig()
|
||||
@@ -0,0 +1,18 @@
|
||||
/** 存储 key 定义 */
|
||||
export const STORAGE_KEYS = {
|
||||
/** 登录状态 */
|
||||
LOGIN_STATUS: 'loginStatus',
|
||||
/** 上次登录检查时间戳 */
|
||||
LAST_LOGIN_CHECK: 'lastLoginCheck',
|
||||
}
|
||||
|
||||
/** 登录状态缓存有效期(毫秒) */
|
||||
export const LOGIN_CHECK_INTERVAL = 5 * 60 * 1000
|
||||
|
||||
/** 消息事件类型 */
|
||||
export const MSG_TYPES = {
|
||||
/** 获取 Token */
|
||||
GET_TOKEN: 'GET_TOKEN',
|
||||
/** 获取 Cookie */
|
||||
GET_COOKIES: 'GET_COOKIES',
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
/**
|
||||
* 后端 API 通信模块
|
||||
* 封装了与 Java 后端和 Python AI 后端的 REST API 请求方法
|
||||
* 注意:项目有两个后端,新增接口时务必确认是哪个后端的
|
||||
*/
|
||||
|
||||
/** Java 后端基础地址 */
|
||||
const BASE_URL = "http://localhost:8080/api"
|
||||
|
||||
/** Python AI 后端基础地址 */
|
||||
const AI_BASE_URL = "http://localhost:5000/api"
|
||||
|
||||
/** 请求配置选项 */
|
||||
interface ApiOptions {
|
||||
/** 请求方法,默认 GET */
|
||||
method?: string
|
||||
/** 请求体数据 */
|
||||
body?: unknown
|
||||
/** 自定义请求头 */
|
||||
headers?: Record<string, string>
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用请求方法
|
||||
* @param baseUrl - 后端基础地址
|
||||
* @param path - 接口路径,如 /user/info
|
||||
* @param options - 请求配置
|
||||
* @returns 解析后的 JSON 响应数据
|
||||
* @throws 当响应状态码非 2xx 时抛出错误
|
||||
*/
|
||||
async function request<T>(baseUrl: string, path: string, options: ApiOptions = {}): Promise<T> {
|
||||
const { method = "GET", body, headers = {} } = options
|
||||
|
||||
const res = await fetch(`${baseUrl}${path}`, {
|
||||
method,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...headers
|
||||
},
|
||||
body: body ? JSON.stringify(body) : undefined
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`API Error: ${res.status} ${res.statusText}`)
|
||||
}
|
||||
|
||||
return res.json()
|
||||
}
|
||||
|
||||
/**
|
||||
* Java 后端接口
|
||||
* 用于用户管理、简历数据、投递记录等业务接口
|
||||
*/
|
||||
export const javaApi = {
|
||||
/** 发送 GET 请求到 Java 后端 */
|
||||
get: <T>(path: string) => request<T>(BASE_URL, path),
|
||||
/** 发送 POST 请求到 Java 后端 */
|
||||
post: <T>(path: string, body: unknown) => request<T>(BASE_URL, path, { method: "POST", body })
|
||||
}
|
||||
|
||||
/**
|
||||
* Python AI 后端接口
|
||||
* 用于页面结构分析、智能填表、简历优化等 AI 功能接口
|
||||
*/
|
||||
export const aiApi = {
|
||||
/** 发送 GET 请求到 Python AI 后端 */
|
||||
get: <T>(path: string) => request<T>(AI_BASE_URL, path),
|
||||
/** 发送 POST 请求到 Python AI 后端 */
|
||||
post: <T>(path: string, body: unknown) => request<T>(AI_BASE_URL, path, { method: "POST", body })
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { get, set } from "~utils/storage"
|
||||
import { checkLogin } from "~api/dataApi"
|
||||
import { STORAGE_KEYS, LOGIN_CHECK_INTERVAL } from "~constants"
|
||||
|
||||
/**
|
||||
* 检查登录状态(带缓存)
|
||||
* 成功后缓存结果,有效期内不重复请求
|
||||
* 失败则清除缓存,下次必定重新检查
|
||||
*/
|
||||
export async function ensureLogin(): Promise<boolean> {
|
||||
const lastCheck = await get<number>(STORAGE_KEYS.LAST_LOGIN_CHECK)
|
||||
|
||||
if (lastCheck && Date.now() - lastCheck < LOGIN_CHECK_INTERVAL) {
|
||||
return true
|
||||
}
|
||||
|
||||
try {
|
||||
await checkLogin()
|
||||
await set(STORAGE_KEYS.LAST_LOGIN_CHECK, Date.now())
|
||||
return true
|
||||
} catch {
|
||||
await set(STORAGE_KEYS.LAST_LOGIN_CHECK, 0)
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { MSG_TYPES } from "~constants"
|
||||
|
||||
/** 获取指定地址下的所有 Cookie,默认取 cookieSourceUrl */
|
||||
export async function getCookies(url?: string) {
|
||||
const res = await chrome.runtime.sendMessage({ type: MSG_TYPES.GET_COOKIES, url })
|
||||
return res?.cookies || []
|
||||
}
|
||||
|
||||
/** 获取指定地址下某个 Cookie 的值,默认取 cookieSourceUrl */
|
||||
export async function getCookieValue(name: string, url?: string) {
|
||||
const res = await chrome.runtime.sendMessage({ type: MSG_TYPES.GET_TOKEN, name, url })
|
||||
return res?.value || null
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Chrome Storage 封装
|
||||
* 统一使用 chrome.storage.local 进行数据持久化
|
||||
*/
|
||||
|
||||
/** 读取存储值 */
|
||||
export async function get<T = unknown>(key: string): Promise<T | null> {
|
||||
const result = await chrome.storage.local.get(key)
|
||||
return (result[key] as T) ?? null
|
||||
}
|
||||
|
||||
/** 写入存储值 */
|
||||
export async function set(key: string, value: unknown): Promise<void> {
|
||||
await chrome.storage.local.set({ [key]: value })
|
||||
}
|
||||
|
||||
/** 删除存储值 */
|
||||
export async function remove(key: string): Promise<void> {
|
||||
await chrome.storage.local.remove(key)
|
||||
}
|
||||
Reference in New Issue
Block a user