bug处理,插件版本同步
This commit is contained in:
@@ -20,6 +20,7 @@
|
|||||||
"vuex": "^4.1.0"
|
"vuex": "^4.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/chrome": "^0.2.2",
|
||||||
"@types/markdown-it": "^14.1.2",
|
"@types/markdown-it": "^14.1.2",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
"@vitejs/plugin-vue": "^6.0.2",
|
"@vitejs/plugin-vue": "^6.0.2",
|
||||||
|
|||||||
Generated
+28
@@ -36,6 +36,9 @@ importers:
|
|||||||
specifier: ^4.1.0
|
specifier: ^4.1.0
|
||||||
version: 4.1.0(vue@3.5.29(typescript@5.9.3))
|
version: 4.1.0(vue@3.5.29(typescript@5.9.3))
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@types/chrome':
|
||||||
|
specifier: ^0.2.2
|
||||||
|
version: 0.2.2
|
||||||
'@types/markdown-it':
|
'@types/markdown-it':
|
||||||
specifier: ^14.1.2
|
specifier: ^14.1.2
|
||||||
version: 14.1.2
|
version: 14.1.2
|
||||||
@@ -490,9 +493,21 @@ packages:
|
|||||||
'@sxzz/popperjs-es@2.11.8':
|
'@sxzz/popperjs-es@2.11.8':
|
||||||
resolution: {integrity: sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==}
|
resolution: {integrity: sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==}
|
||||||
|
|
||||||
|
'@types/chrome@0.2.2':
|
||||||
|
resolution: {integrity: sha512-8rSMZ4cvo2xmaSyQg0sN5yRL7oiDkntLoiHxUhfwQnv1mvnkrdoZ25SlNrKWmYKaeP50WvrfWj1pmc02+U9KKw==}
|
||||||
|
|
||||||
'@types/estree@1.0.8':
|
'@types/estree@1.0.8':
|
||||||
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
||||||
|
|
||||||
|
'@types/filesystem@0.0.36':
|
||||||
|
resolution: {integrity: sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA==}
|
||||||
|
|
||||||
|
'@types/filewriter@0.0.33':
|
||||||
|
resolution: {integrity: sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g==}
|
||||||
|
|
||||||
|
'@types/har-format@1.2.16':
|
||||||
|
resolution: {integrity: sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A==}
|
||||||
|
|
||||||
'@types/linkify-it@5.0.0':
|
'@types/linkify-it@5.0.0':
|
||||||
resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==}
|
resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==}
|
||||||
|
|
||||||
@@ -1365,8 +1380,21 @@ snapshots:
|
|||||||
|
|
||||||
'@sxzz/popperjs-es@2.11.8': {}
|
'@sxzz/popperjs-es@2.11.8': {}
|
||||||
|
|
||||||
|
'@types/chrome@0.2.2':
|
||||||
|
dependencies:
|
||||||
|
'@types/filesystem': 0.0.36
|
||||||
|
'@types/har-format': 1.2.16
|
||||||
|
|
||||||
'@types/estree@1.0.8': {}
|
'@types/estree@1.0.8': {}
|
||||||
|
|
||||||
|
'@types/filesystem@0.0.36':
|
||||||
|
dependencies:
|
||||||
|
'@types/filewriter': 0.0.33
|
||||||
|
|
||||||
|
'@types/filewriter@0.0.33': {}
|
||||||
|
|
||||||
|
'@types/har-format@1.2.16': {}
|
||||||
|
|
||||||
'@types/linkify-it@5.0.0': {}
|
'@types/linkify-it@5.0.0': {}
|
||||||
|
|
||||||
'@types/lodash-es@4.17.12':
|
'@types/lodash-es@4.17.12':
|
||||||
|
|||||||
@@ -279,7 +279,6 @@
|
|||||||
border-radius: 0.12rem;
|
border-radius: 0.12rem;
|
||||||
max-height: calc(var(--app-height, 100vh) - 1.4rem);
|
max-height: calc(var(--app-height, 100vh) - 1.4rem);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 0.24rem;
|
|
||||||
scrollbar-width: none; // Firefox
|
scrollbar-width: none; // Firefox
|
||||||
-ms-overflow-style: none; // IE/Edge
|
-ms-overflow-style: none; // IE/Edge
|
||||||
|
|
||||||
|
|||||||
@@ -548,7 +548,10 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 0.08rem 0.16rem;
|
text-align: center;
|
||||||
|
width: 0.88rem;
|
||||||
|
height: 0.37rem;
|
||||||
|
line-height: 0.37rem;
|
||||||
border: 1.23px solid $accent;
|
border: 1.23px solid $accent;
|
||||||
border-radius: 0.08rem;
|
border-radius: 0.08rem;
|
||||||
background: none;
|
background: none;
|
||||||
@@ -557,7 +560,6 @@
|
|||||||
color: $accent;
|
color: $accent;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
line-height: 0.19rem;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
@@ -569,7 +571,9 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 0.08rem 0.16rem;
|
width: 0.88rem;
|
||||||
|
height: 0.37rem;
|
||||||
|
line-height: 0.35rem;
|
||||||
background: $accent;
|
background: $accent;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 0.08rem;
|
border-radius: 0.08rem;
|
||||||
@@ -578,7 +582,6 @@
|
|||||||
color: #FFFFFF;
|
color: #FFFFFF;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
line-height: 0.19rem;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
|||||||
@@ -155,8 +155,12 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
import { useStore } from 'vuex'
|
||||||
|
import { createChannelBridge } from '@/utils/channelBridge'
|
||||||
import type { JobListItem } from '@/api/jobs'
|
import type { JobListItem } from '@/api/jobs'
|
||||||
|
|
||||||
|
const store = useStore()
|
||||||
|
|
||||||
// ==================== Props ====================
|
// ==================== Props ====================
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -286,8 +290,29 @@ function handleContinueApply() {
|
|||||||
emit('continueApply')
|
emit('continueApply')
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 前往投递 */
|
/** 前往投递 — 跨域 put 投递链接给插件 */
|
||||||
function handleGoApply() {
|
async function handleGoApply() {
|
||||||
|
const link = props.jobInfo?.sourceUrl
|
||||||
|
if (link) {
|
||||||
|
const bridge = createChannelBridge({ domain: store.state.bridgeDomain })
|
||||||
|
try {
|
||||||
|
// 先 get 拿到已有的链接列表
|
||||||
|
const existing = await bridge.get<{ linkList: string[] }>(store.state.bridgeDomain, 'offerpaiDeliveryLinkList', true)
|
||||||
|
const linkList: string[] = existing?.linkList || []
|
||||||
|
|
||||||
|
// 如果链接不在列表中则 push
|
||||||
|
if (!linkList.includes(link)) {
|
||||||
|
linkList.push(link)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 跨域 put 数据给插件
|
||||||
|
await bridge.put<{ linkList: string[] }>(store.state.bridgeDomain, 'offerpaiDeliveryLinkList', { linkList }, true)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[AgentApplyProgress] 跨域 put 投递链接失败', e)
|
||||||
|
} finally {
|
||||||
|
bridge.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
emit('goApply')
|
emit('goApply')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -317,6 +317,24 @@ onMounted(async () => {
|
|||||||
sessionStorage.removeItem('wizardStep')
|
sessionStorage.removeItem('wizardStep')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 刷新后检测插件是否在线
|
||||||
|
const needCheck = sessionStorage.getItem('wizardCheckPlug')
|
||||||
|
if (needCheck) {
|
||||||
|
sessionStorage.removeItem('wizardCheckPlug')
|
||||||
|
// 立即打开弹窗
|
||||||
|
showInstallDialog.value = true
|
||||||
|
// 等待心跳检测完成(最多等 3.5 秒)
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 3500))
|
||||||
|
if (store.state.plugIsOnline) {
|
||||||
|
// 插件在线,关闭弹窗并直接进入下一步
|
||||||
|
showInstallDialog.value = false
|
||||||
|
handleNext()
|
||||||
|
} else {
|
||||||
|
// 插件离线,弹窗保持展示 + 提示
|
||||||
|
ElMessage.warning('未检测到插件安装')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!store.state.regions.length) store.dispatch('loadCommonData')
|
if (!store.state.regions.length) store.dispatch('loadCommonData')
|
||||||
store.dispatch('loadJobIntention')
|
store.dispatch('loadJobIntention')
|
||||||
await loadProfile()
|
await loadProfile()
|
||||||
@@ -665,9 +683,10 @@ function handleInstallConfirm() {
|
|||||||
showInstallDialog.value = true
|
showInstallDialog.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 点击"已安装,继续":保存步骤 + 刷新页面 */
|
/** 点击"已安装,继续":保存步骤 + 标记需要检测 + 刷新页面 */
|
||||||
function handleInstallConfirmReload() {
|
function handleInstallConfirmReload() {
|
||||||
sessionStorage.setItem('wizardStep', String(currentStep.value))
|
sessionStorage.setItem('wizardStep', String(currentStep.value))
|
||||||
|
sessionStorage.setItem('wizardCheckPlug', '1')
|
||||||
location.reload()
|
location.reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -159,11 +159,15 @@ function onRegionChange(codes: string[]) {
|
|||||||
/** 保存表单数据 */
|
/** 保存表单数据 */
|
||||||
async function handleSave() {
|
async function handleSave() {
|
||||||
try {
|
try {
|
||||||
|
const intention = store.state.jobIntention
|
||||||
await store.dispatch('saveJobIntention', {
|
await store.dispatch('saveJobIntention', {
|
||||||
categoryIds: [...selectedCategoryIds.value],
|
categoryIds: [...selectedCategoryIds.value],
|
||||||
industryIds: [...selectedIndustryIds.value],
|
industryIds: [...selectedIndustryIds.value],
|
||||||
regionCodes: [...selectedRegionCodes.value],
|
regionCodes: [...selectedRegionCodes.value],
|
||||||
recruitCategory: JOB_TYPE_OPTIONS.find(o => o.label === selectedJobType.value)?.value ?? 0,
|
recruitCategory: selectedJobType.value === '全部' ? null : (JOB_TYPE_OPTIONS.find(o => o.label === selectedJobType.value)?.value ?? null),
|
||||||
|
employmentType: intention.employmentType ?? null,
|
||||||
|
aiCategoryIds: intention.aiCategoryIds || [],
|
||||||
|
aiIndustryIds: intention.aiIndustryIds || [],
|
||||||
})
|
})
|
||||||
visible.value = false
|
visible.value = false
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
+32
-9
@@ -171,6 +171,15 @@ export interface RootState {
|
|||||||
|
|
||||||
/** 浏览器插件是否在线(3秒内有 BroadcastChannel 心跳则为 true) */
|
/** 浏览器插件是否在线(3秒内有 BroadcastChannel 心跳则为 true) */
|
||||||
plugIsOnline: boolean
|
plugIsOnline: boolean
|
||||||
|
|
||||||
|
/** 插件当前使用版本(从通信数据 offerpieBrowserPlugUsage.version 获取) */
|
||||||
|
plugUseVersion: string
|
||||||
|
|
||||||
|
/** 插件目标版本(手动设置,用于判断是否需要更新) */
|
||||||
|
plugTargetVersion: string
|
||||||
|
|
||||||
|
/** 插件是否是最新版本(plugUseVersion === plugTargetVersion) */
|
||||||
|
isLatestPlugin: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export default createStore<RootState>({
|
export default createStore<RootState>({
|
||||||
@@ -206,6 +215,12 @@ export default createStore<RootState>({
|
|||||||
bridgeDomain: 'test.offerpai.com.cn',
|
bridgeDomain: 'test.offerpai.com.cn',
|
||||||
/** 浏览器插件是否在线 */
|
/** 浏览器插件是否在线 */
|
||||||
plugIsOnline: false,
|
plugIsOnline: false,
|
||||||
|
/** 插件当前使用版本 */
|
||||||
|
plugUseVersion: '',
|
||||||
|
/** 插件目标版本 */
|
||||||
|
plugTargetVersion: '0.1.0',
|
||||||
|
/** 插件是否是最新版本 */
|
||||||
|
isLatestPlugin: true,
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
getAppName: (state) => state.appName,
|
getAppName: (state) => state.appName,
|
||||||
@@ -284,6 +299,11 @@ export default createStore<RootState>({
|
|||||||
SET_PLUG_IS_ONLINE(state, online: boolean) {
|
SET_PLUG_IS_ONLINE(state, online: boolean) {
|
||||||
state.plugIsOnline = online
|
state.plugIsOnline = online
|
||||||
},
|
},
|
||||||
|
SET_PLUG_USE_VERSION(state, version: string) {
|
||||||
|
state.plugUseVersion = version
|
||||||
|
// 实时计算是否最新版本
|
||||||
|
state.isLatestPlugin = version === state.plugTargetVersion
|
||||||
|
},
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
updateAppName({ commit }, name: string) {
|
updateAppName({ commit }, name: string) {
|
||||||
@@ -480,31 +500,35 @@ export default createStore<RootState>({
|
|||||||
return new Date(timeStr.replace(' ', 'T')).getTime()
|
return new Date(timeStr.replace(' ', 'T')).getTime()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 根据 updateTime 判断是否在线并更新状态 */
|
/** 根据 updateTime 判断是否在线并更新状态,同时提取 version */
|
||||||
function checkOnline(updateTime: string) {
|
function checkOnline(updateTime: string, version?: string) {
|
||||||
const elapsed = Date.now() - parseTime(updateTime)
|
const elapsed = Date.now() - parseTime(updateTime)
|
||||||
if (elapsed <= 3000) {
|
if (elapsed <= 3000) {
|
||||||
lastHeartbeat = Date.now()
|
lastHeartbeat = Date.now()
|
||||||
if (!state.plugIsOnline) {
|
if (!state.plugIsOnline) {
|
||||||
commit('SET_PLUG_IS_ONLINE', true)
|
commit('SET_PLUG_IS_ONLINE', true)
|
||||||
}
|
}
|
||||||
|
// 更新插件版本号
|
||||||
|
if (version && version !== state.plugUseVersion) {
|
||||||
|
commit('SET_PLUG_USE_VERSION', version)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 启动时立即 get 一次,根据 updateTime 判断初始在线状态
|
// 启动时立即 get 一次,根据 updateTime 判断初始在线状态
|
||||||
bridge.get<{ usage: string }>(state.bridgeDomain, 'offerpieBrowserPlugUsage').then((data) => {
|
bridge.get<{ usage: string; version?: string }>(state.bridgeDomain, 'offerpieBrowserPlugUsage').then((data) => {
|
||||||
if (data && data.updateTime) {
|
if (data && data.updateTime) {
|
||||||
checkOnline(data.updateTime)
|
checkOnline(data.updateTime, data.version)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 方式1:BroadcastChannel watch — 实时收心跳
|
// 方式1:BroadcastChannel watch — 实时收心跳
|
||||||
const unwatch = bridge.watch<{ usage: string }>(
|
const unwatch = bridge.watch<{ usage: string; version?: string }>(
|
||||||
state.bridgeDomain,
|
state.bridgeDomain,
|
||||||
'offerpieBrowserPlugUsage',
|
'offerpieBrowserPlugUsage',
|
||||||
(data) => {
|
(data) => {
|
||||||
if (data && data.updateTime) {
|
if (data && data.updateTime) {
|
||||||
checkOnline(data.updateTime)
|
checkOnline(data.updateTime, data.version)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -512,10 +536,9 @@ export default createStore<RootState>({
|
|||||||
// 方式2:定时轮询 get IndexedDB — 兜底(覆盖 BroadcastChannel 断连场景)
|
// 方式2:定时轮询 get IndexedDB — 兜底(覆盖 BroadcastChannel 断连场景)
|
||||||
const pollTimer = setInterval(async () => {
|
const pollTimer = setInterval(async () => {
|
||||||
try {
|
try {
|
||||||
const data = await bridge.get<{ usage: string }>(state.bridgeDomain, 'offerpieBrowserPlugUsage')
|
const data = await bridge.get<{ usage: string; version?: string }>(state.bridgeDomain, 'offerpieBrowserPlugUsage')
|
||||||
// console.log('[plugHeartbeat] poll get:', data)
|
|
||||||
if (data && data.updateTime) {
|
if (data && data.updateTime) {
|
||||||
checkOnline(data.updateTime)
|
checkOnline(data.updateTime, data.version)
|
||||||
}
|
}
|
||||||
} catch { /* 忽略读取异常 */ }
|
} catch { /* 忽略读取异常 */ }
|
||||||
}, 3000)
|
}, 3000)
|
||||||
|
|||||||
+547
-277
@@ -21,6 +21,12 @@
|
|||||||
* // 取消监听
|
* // 取消监听
|
||||||
* unwatch()
|
* unwatch()
|
||||||
*
|
*
|
||||||
|
* 【跨域模式 useCsl】
|
||||||
|
* 在 put/emit/get/watch 最后一个参数传 true,数据存储走 chrome.storage.local(通过 Background 中转)
|
||||||
|
* 适用于网页与插件在不同域名标签页之间的通信场景
|
||||||
|
* 示例:bridge.put<MyData>('offerpai.com', 'someData', { ... }, true)
|
||||||
|
* 注意:useCsl 模式依赖插件 Background Service Worker 在线
|
||||||
|
*
|
||||||
* 6. 销毁实例(页面卸载时调用):
|
* 6. 销毁实例(页面卸载时调用):
|
||||||
* bridge.destroy()
|
* bridge.destroy()
|
||||||
*
|
*
|
||||||
@@ -47,10 +53,10 @@
|
|||||||
|
|
||||||
/** 组件自动附加的时间戳字段 */
|
/** 组件自动附加的时间戳字段 */
|
||||||
export interface BridgeTimestamp {
|
export interface BridgeTimestamp {
|
||||||
/** 数据首次创建时间(格式:yyyy-MM-dd HH:mm:ss) */
|
/** 数据首次创建时间(格式:yyyy-MM-dd HH:mm:ss) */
|
||||||
createTime: string
|
createTime: string
|
||||||
/** 数据最近更新时间(格式:yyyy-MM-dd HH:mm:ss) */
|
/** 数据最近更新时间(格式:yyyy-MM-dd HH:mm:ss) */
|
||||||
updateTime: string
|
updateTime: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 带时间戳的完整数据包装 */
|
/** 带时间戳的完整数据包装 */
|
||||||
@@ -58,30 +64,32 @@ export type BridgeData<T extends object> = T & BridgeTimestamp
|
|||||||
|
|
||||||
/** IndexedDB 存储记录结构 */
|
/** IndexedDB 存储记录结构 */
|
||||||
interface DBRecord {
|
interface DBRecord {
|
||||||
/** 数据名称(唯一索引) */
|
/** 数据名称(唯一索引) */
|
||||||
name: string
|
name: string
|
||||||
/** JSON 数据本体 */
|
/** JSON 数据本体 */
|
||||||
data: object
|
data: object
|
||||||
/** 创建时间 */
|
/** 创建时间 */
|
||||||
createTime: string
|
createTime: string
|
||||||
/** 更新时间 */
|
/** 更新时间 */
|
||||||
updateTime: string
|
updateTime: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** BroadcastChannel 消息结构 */
|
/** BroadcastChannel 消息结构 */
|
||||||
interface ChannelMessage {
|
interface ChannelMessage {
|
||||||
/** 消息类型 */
|
/** 消息类型 */
|
||||||
type: 'put' | 'emit' | 'get-request' | 'get-response'
|
type: 'put' | 'emit' | 'get-request' | 'get-response' | 'csl-put-request' | 'csl-put-response' | 'csl-get-request' | 'csl-get-response'
|
||||||
/** 数据名称 */
|
/** 数据名称 */
|
||||||
name: string
|
name: string
|
||||||
/** 请求唯一ID(用于 get 请求-响应匹配) */
|
/** 请求唯一ID(用于 get 请求-响应匹配) */
|
||||||
id?: string
|
id?: string
|
||||||
/** 数据本体(put/emit/get-response 携带) */
|
/** 数据本体(put/emit/get-response 携带) */
|
||||||
data?: object
|
data?: object
|
||||||
/** 创建时间 */
|
/** 创建时间 */
|
||||||
createTime?: string
|
createTime?: string
|
||||||
/** 更新时间 */
|
/** 更新时间 */
|
||||||
updateTime?: string
|
updateTime?: string
|
||||||
|
/** 目标域名(csl 中转消息使用) */
|
||||||
|
domain?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** watch 回调函数类型 */
|
/** watch 回调函数类型 */
|
||||||
@@ -89,10 +97,10 @@ type WatchCallback<T extends object> = (data: BridgeData<T> | null) => void
|
|||||||
|
|
||||||
/** 创建实例的配置参数 */
|
/** 创建实例的配置参数 */
|
||||||
export interface ChannelBridgeOptions {
|
export interface ChannelBridgeOptions {
|
||||||
/** 默认域名(可在方法调用时覆盖) */
|
/** 默认域名(可在方法调用时覆盖) */
|
||||||
domain: string
|
domain: string
|
||||||
/** 是否为插件环境,默认 false。非插件时启用 Web Lock 保活 */
|
/** 是否为插件环境,默认 false。非插件时启用 Web Lock 保活 */
|
||||||
isPlugin?: boolean
|
isPlugin?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ 常量 ============
|
// ============ 常量 ============
|
||||||
@@ -105,313 +113,575 @@ const DB_VERSION = 1
|
|||||||
const GET_TIMEOUT_MS = 60
|
const GET_TIMEOUT_MS = 60
|
||||||
/** emit 方法节流写库间隔(毫秒) */
|
/** emit 方法节流写库间隔(毫秒) */
|
||||||
const EMIT_THROTTLE_MS = 500
|
const EMIT_THROTTLE_MS = 500
|
||||||
|
/** chrome.storage.local 存储总 key */
|
||||||
|
const CSL_STORAGE_KEY = 'offerpie_comm_7394028156183472'
|
||||||
|
/** Background 消息类型前缀(用于 useCsl 模式) */
|
||||||
|
const CSL_MSG_PREFIX = 'OFFERPIE_CSL'
|
||||||
|
|
||||||
// ============ 工具函数 ============
|
// ============ 工具函数 ============
|
||||||
|
|
||||||
/** 获取当前时间的 localDateTime 格式字符串:yyyy-MM-dd HH:mm:ss */
|
/** 获取当前时间的 localDateTime 格式字符串:yyyy-MM-dd HH:mm:ss */
|
||||||
function nowLocalDateTime(): string {
|
function nowLocalDateTime(): string {
|
||||||
const d = new Date()
|
const d = new Date()
|
||||||
const pad = (n: number) => String(n).padStart(2, '0')
|
const pad = (n: number) => String(n).padStart(2, '0')
|
||||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 生成简单唯一ID(用于 get 请求匹配) */
|
/** 生成简单唯一ID(用于 get 请求匹配) */
|
||||||
function uid(): string {
|
function uid(): string {
|
||||||
return Date.now().toString(36) + Math.random().toString(36).slice(2, 8)
|
return Date.now().toString(36) + Math.random().toString(36).slice(2, 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ chrome.storage.local 操作(useCsl 模式) ============
|
||||||
|
|
||||||
|
/** 检测 chrome.runtime.sendMessage 是否可用(插件环境) */
|
||||||
|
function isChromeRuntimeAvailable(): boolean {
|
||||||
|
return typeof chrome !== 'undefined' && !!chrome.runtime && !!chrome.runtime.sendMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过 Background 写入 chrome.storage.local
|
||||||
|
* 插件环境直接发 chrome.runtime.sendMessage
|
||||||
|
* 网页环境自动走 BroadcastChannel 中转给同域 Content Script 代理
|
||||||
|
*/
|
||||||
|
function cslPut(domain: string, name: string, data: object, createTime: string, updateTime: string): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (isChromeRuntimeAvailable()) {
|
||||||
|
// 插件环境:直接发给 Background
|
||||||
|
chrome.runtime.sendMessage(
|
||||||
|
{ type: `${CSL_MSG_PREFIX}_PUT`, domain, name, data, createTime, updateTime },
|
||||||
|
(response) => {
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
reject(new Error(chrome.runtime.lastError.message))
|
||||||
|
} else {
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// 网页环境:通过 BroadcastChannel 请求同域 Content Script 代理
|
||||||
|
const requestId = uid()
|
||||||
|
const ch = _getCslProxyChannel(domain)
|
||||||
|
const handler = (event: MessageEvent<ChannelMessage>) => {
|
||||||
|
if (event.data.type === 'csl-put-response' && event.data.id === requestId) {
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ch.addEventListener('message', handler)
|
||||||
|
ch.postMessage({ type: 'csl-put-request', id: requestId, name, data, createTime, updateTime, domain } as ChannelMessage)
|
||||||
|
// 超时兜底
|
||||||
|
setTimeout(() => {
|
||||||
|
ch.removeEventListener('message', handler)
|
||||||
|
resolve() // 即使超时也不阻塞
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过 Background 从 chrome.storage.local 读取数据
|
||||||
|
* 插件环境直接发 chrome.runtime.sendMessage
|
||||||
|
* 网页环境自动走 BroadcastChannel 中转给同域 Content Script 代理
|
||||||
|
*/
|
||||||
|
function cslGet(domain: string, name: string): Promise<DBRecord | null> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (isChromeRuntimeAvailable()) {
|
||||||
|
// 插件环境:直接发给 Background
|
||||||
|
chrome.runtime.sendMessage(
|
||||||
|
{ type: `${CSL_MSG_PREFIX}_GET`, domain, name },
|
||||||
|
(response) => {
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
reject(new Error(chrome.runtime.lastError.message))
|
||||||
|
} else {
|
||||||
|
resolve(response?.record || null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// 网页环境:通过 BroadcastChannel 请求同域 Content Script 代理
|
||||||
|
const requestId = uid()
|
||||||
|
const ch = _getCslProxyChannel(domain)
|
||||||
|
const handler = (event: MessageEvent<ChannelMessage>) => {
|
||||||
|
if (event.data.type === 'csl-get-response' && event.data.id === requestId) {
|
||||||
|
ch.removeEventListener('message', handler)
|
||||||
|
const record = event.data.data && event.data.createTime && event.data.updateTime
|
||||||
|
? { name: event.data.name, data: event.data.data, createTime: event.data.createTime, updateTime: event.data.updateTime }
|
||||||
|
: null
|
||||||
|
resolve(record)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ch.addEventListener('message', handler)
|
||||||
|
ch.postMessage({ type: 'csl-get-request', id: requestId, name, domain } as ChannelMessage)
|
||||||
|
// 超时兜底
|
||||||
|
setTimeout(() => {
|
||||||
|
ch.removeEventListener('message', handler)
|
||||||
|
resolve(null)
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** CSL 代理频道缓存 */
|
||||||
|
const _cslProxyChannels = new Map<string, BroadcastChannel>()
|
||||||
|
|
||||||
|
/** 获取 CSL 代理用的 BroadcastChannel(与主频道分开,避免消息混淆) */
|
||||||
|
function _getCslProxyChannel(domain: string): BroadcastChannel {
|
||||||
|
if (_cslProxyChannels.has(domain)) return _cslProxyChannels.get(domain)!
|
||||||
|
const ch = new BroadcastChannel(`offerpie-bridge-csl-${domain}`)
|
||||||
|
_cslProxyChannels.set(domain, ch)
|
||||||
|
return ch
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ IndexedDB 操作 ============
|
// ============ IndexedDB 操作 ============
|
||||||
|
|
||||||
/** 打开/创建 IndexedDB 数据库,按 domain 动态创建 ObjectStore */
|
/** 打开/创建 IndexedDB 数据库,按 domain 动态创建 ObjectStore */
|
||||||
function openDB(storeName: string): Promise<IDBDatabase> {
|
function openDB(storeName: string): Promise<IDBDatabase> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const request = indexedDB.open(DB_NAME, DB_VERSION)
|
const request = indexedDB.open(DB_NAME, DB_VERSION)
|
||||||
request.onupgradeneeded = () => {
|
request.onupgradeneeded = () => {
|
||||||
const db = request.result
|
const db = request.result
|
||||||
if (!db.objectStoreNames.contains(storeName)) {
|
if (!db.objectStoreNames.contains(storeName)) {
|
||||||
const store = db.createObjectStore(storeName, { keyPath: 'name' })
|
const store = db.createObjectStore(storeName, { keyPath: 'name' })
|
||||||
store.createIndex('name', 'name', { unique: true })
|
store.createIndex('name', 'name', { unique: true })
|
||||||
}
|
}
|
||||||
}
|
|
||||||
request.onsuccess = () => {
|
|
||||||
const db = request.result
|
|
||||||
// 如果 store 不存在(版本没升级的情况),关闭后升版本重建
|
|
||||||
if (!db.objectStoreNames.contains(storeName)) {
|
|
||||||
db.close()
|
|
||||||
const version = db.version + 1
|
|
||||||
const req2 = indexedDB.open(DB_NAME, version)
|
|
||||||
req2.onupgradeneeded = () => {
|
|
||||||
const db2 = req2.result
|
|
||||||
if (!db2.objectStoreNames.contains(storeName)) {
|
|
||||||
const store = db2.createObjectStore(storeName, { keyPath: 'name' })
|
|
||||||
store.createIndex('name', 'name', { unique: true })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
req2.onsuccess = () => resolve(req2.result)
|
request.onsuccess = () => {
|
||||||
req2.onerror = () => reject(req2.error)
|
const db = request.result
|
||||||
} else {
|
// 如果 store 不存在(版本没升级的情况),关闭后升版本重建
|
||||||
resolve(db)
|
if (!db.objectStoreNames.contains(storeName)) {
|
||||||
}
|
db.close()
|
||||||
}
|
const version = db.version + 1
|
||||||
request.onerror = () => reject(request.error)
|
const req2 = indexedDB.open(DB_NAME, version)
|
||||||
})
|
req2.onupgradeneeded = () => {
|
||||||
|
const db2 = req2.result
|
||||||
|
if (!db2.objectStoreNames.contains(storeName)) {
|
||||||
|
const store = db2.createObjectStore(storeName, { keyPath: 'name' })
|
||||||
|
store.createIndex('name', 'name', { unique: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
req2.onsuccess = () => resolve(req2.result)
|
||||||
|
req2.onerror = () => reject(req2.error)
|
||||||
|
} else {
|
||||||
|
resolve(db)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
request.onerror = () => reject(request.error)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 写入或更新一条记录到 IndexedDB */
|
/** 写入或更新一条记录到 IndexedDB */
|
||||||
async function dbPut(storeName: string, record: DBRecord): Promise<void> {
|
async function dbPut(storeName: string, record: DBRecord): Promise<void> {
|
||||||
const db = await openDB(storeName)
|
const db = await openDB(storeName)
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const tx = db.transaction(storeName, 'readwrite')
|
const tx = db.transaction(storeName, 'readwrite')
|
||||||
const store = tx.objectStore(storeName)
|
const store = tx.objectStore(storeName)
|
||||||
store.put(record)
|
store.put(record)
|
||||||
tx.oncomplete = () => { db.close(); resolve() }
|
tx.oncomplete = () => { db.close(); resolve() }
|
||||||
tx.onerror = () => { db.close(); reject(tx.error) }
|
tx.onerror = () => { db.close(); reject(tx.error) }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 从 IndexedDB 读取一条记录 */
|
/** 从 IndexedDB 读取一条记录 */
|
||||||
async function dbGet(storeName: string, name: string): Promise<DBRecord | null> {
|
async function dbGet(storeName: string, name: string): Promise<DBRecord | null> {
|
||||||
const db = await openDB(storeName)
|
const db = await openDB(storeName)
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const tx = db.transaction(storeName, 'readonly')
|
const tx = db.transaction(storeName, 'readonly')
|
||||||
const store = tx.objectStore(storeName)
|
const store = tx.objectStore(storeName)
|
||||||
const req = store.get(name)
|
const req = store.get(name)
|
||||||
req.onsuccess = () => { db.close(); resolve(req.result || null) }
|
req.onsuccess = () => { db.close(); resolve(req.result || null) }
|
||||||
req.onerror = () => { db.close(); reject(req.error) }
|
req.onerror = () => { db.close(); reject(req.error) }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ 核心:创建通信桥接实例 ============
|
// ============ 核心:创建通信桥接实例 ============
|
||||||
|
|
||||||
export function createChannelBridge(options: ChannelBridgeOptions) {
|
export function createChannelBridge(options: ChannelBridgeOptions) {
|
||||||
const { domain, isPlugin = false } = options
|
const { domain, isPlugin = false } = options
|
||||||
|
|
||||||
/** BroadcastChannel 实例缓存(按 domain 隔离频道) */
|
/** BroadcastChannel 实例缓存(按 domain 隔离频道) */
|
||||||
const channels = new Map<string, BroadcastChannel>()
|
const channels = new Map<string, BroadcastChannel>()
|
||||||
|
|
||||||
/** watch 订阅回调注册表:key = `${domain}::${name}` */
|
/** watch 订阅回调注册表:key = `${domain}::${name}` */
|
||||||
const watchers = new Map<string, Set<WatchCallback<any>>>()
|
const watchers = new Map<string, Set<WatchCallback<any>>>()
|
||||||
|
|
||||||
/** emit 节流定时器:key = `${domain}::${name}` */
|
/** emit 节流定时器:key = `${domain}::${name}` */
|
||||||
const emitTimers = new Map<string, ReturnType<typeof setTimeout>>()
|
const emitTimers = new Map<string, ReturnType<typeof setTimeout>>()
|
||||||
|
|
||||||
/** emit 节流暂存最新数据:key = `${domain}::${name}` */
|
/** emit 节流暂存最新数据:key = `${domain}::${name}` */
|
||||||
const emitPending = new Map<string, DBRecord>()
|
const emitPending = new Map<string, DBRecord>()
|
||||||
|
|
||||||
/** get 请求等待队列:key = requestId */
|
/** get 请求等待队列:key = requestId */
|
||||||
const getResolvers = new Map<string, (data: BridgeData<any> | null) => void>()
|
const getResolvers = new Map<string, (data: BridgeData<any> | null) => void>()
|
||||||
|
|
||||||
// --- Web Lock 保活(非插件环境) ---
|
// --- Web Lock 保活(非插件环境) ---
|
||||||
if (!isPlugin && typeof navigator !== 'undefined' && navigator.locks) {
|
if (!isPlugin && typeof navigator !== 'undefined' && navigator.locks) {
|
||||||
navigator.locks.request('offerpie-keep-alive', () => new Promise(() => {}))
|
navigator.locks.request('offerpie-keep-alive', () => new Promise(() => {}))
|
||||||
}
|
|
||||||
|
|
||||||
/** 获取或创建指定 domain 的 BroadcastChannel */
|
|
||||||
function getChannel(channelDomain: string): BroadcastChannel {
|
|
||||||
if (channels.has(channelDomain)) return channels.get(channelDomain)!
|
|
||||||
const ch = new BroadcastChannel(`offerpie-bridge-${channelDomain}`)
|
|
||||||
ch.onmessage = (event: MessageEvent<ChannelMessage>) => handleMessage(channelDomain, event.data)
|
|
||||||
channels.set(channelDomain, ch)
|
|
||||||
return ch
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 处理收到的 BroadcastChannel 消息 */
|
|
||||||
async function handleMessage(channelDomain: string, msg: ChannelMessage) {
|
|
||||||
const watchKey = `${channelDomain}::${msg.name}`
|
|
||||||
|
|
||||||
if (msg.type === 'put' || msg.type === 'emit') {
|
|
||||||
// 收到对方的数据推送,触发本地 watch 回调
|
|
||||||
const wrapped = msg.data && msg.createTime && msg.updateTime
|
|
||||||
? { ...msg.data, createTime: msg.createTime, updateTime: msg.updateTime } as BridgeData<any>
|
|
||||||
: null
|
|
||||||
const callbacks = watchers.get(watchKey)
|
|
||||||
if (callbacks) {
|
|
||||||
callbacks.forEach(cb => cb(wrapped))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (msg.type === 'get-request' && msg.id) {
|
/** 获取或创建指定 domain 的 BroadcastChannel */
|
||||||
// 收到对方的 get 请求,从本地 IndexedDB 读取数据并回复
|
function getChannel(channelDomain: string): BroadcastChannel {
|
||||||
const record = await dbGet(channelDomain, msg.name)
|
if (channels.has(channelDomain)) return channels.get(channelDomain)!
|
||||||
const ch = getChannel(channelDomain)
|
const ch = new BroadcastChannel(`offerpie-bridge-${channelDomain}`)
|
||||||
const response: ChannelMessage = {
|
ch.onmessage = (event: MessageEvent<ChannelMessage>) => handleMessage(channelDomain, event.data)
|
||||||
type: 'get-response',
|
channels.set(channelDomain, ch)
|
||||||
name: msg.name,
|
return ch
|
||||||
id: msg.id,
|
|
||||||
data: record?.data,
|
|
||||||
createTime: record?.createTime,
|
|
||||||
updateTime: record?.updateTime,
|
|
||||||
}
|
|
||||||
ch.postMessage(response)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (msg.type === 'get-response' && msg.id) {
|
/** 处理收到的 BroadcastChannel 消息 */
|
||||||
// 收到对方对 get 请求的回复
|
async function handleMessage(channelDomain: string, msg: ChannelMessage) {
|
||||||
const resolver = getResolvers.get(msg.id)
|
const watchKey = `${channelDomain}::${msg.name}`
|
||||||
if (resolver) {
|
|
||||||
getResolvers.delete(msg.id)
|
if (msg.type === 'put' || msg.type === 'emit') {
|
||||||
if (msg.data && msg.createTime && msg.updateTime) {
|
// 收到对方的数据推送,触发本地 watch 回调
|
||||||
resolver({ ...msg.data, createTime: msg.createTime, updateTime: msg.updateTime } as BridgeData<any>)
|
const wrapped = msg.data && msg.createTime && msg.updateTime
|
||||||
|
? { ...msg.data, createTime: msg.createTime, updateTime: msg.updateTime } as BridgeData<any>
|
||||||
|
: null
|
||||||
|
const callbacks = watchers.get(watchKey)
|
||||||
|
if (callbacks) {
|
||||||
|
callbacks.forEach(cb => cb(wrapped))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.type === 'get-request' && msg.id) {
|
||||||
|
// 收到对方的 get 请求,从本地 IndexedDB 读取数据并回复
|
||||||
|
const record = await dbGet(channelDomain, msg.name)
|
||||||
|
const ch = getChannel(channelDomain)
|
||||||
|
const response: ChannelMessage = {
|
||||||
|
type: 'get-response',
|
||||||
|
name: msg.name,
|
||||||
|
id: msg.id,
|
||||||
|
data: record?.data,
|
||||||
|
createTime: record?.createTime,
|
||||||
|
updateTime: record?.updateTime,
|
||||||
|
}
|
||||||
|
ch.postMessage(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.type === 'get-response' && msg.id) {
|
||||||
|
// 收到对方对 get 请求的回复
|
||||||
|
const resolver = getResolvers.get(msg.id)
|
||||||
|
if (resolver) {
|
||||||
|
getResolvers.delete(msg.id)
|
||||||
|
if (msg.data && msg.createTime && msg.updateTime) {
|
||||||
|
resolver({ ...msg.data, createTime: msg.createTime, updateTime: msg.updateTime } as BridgeData<any>)
|
||||||
|
} else {
|
||||||
|
resolver(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化默认频道监听
|
||||||
|
getChannel(domain)
|
||||||
|
|
||||||
|
// --- CSL 代理监听(插件环境下,监听网页端的 CSL 中转请求) ---
|
||||||
|
if (isPlugin && isChromeRuntimeAvailable()) {
|
||||||
|
const cslProxyCh = new BroadcastChannel(`offerpie-bridge-csl-${domain}`)
|
||||||
|
cslProxyCh.onmessage = async (event: MessageEvent<ChannelMessage>) => {
|
||||||
|
const msg = event.data
|
||||||
|
|
||||||
|
if (msg.type === 'csl-put-request' && msg.id) {
|
||||||
|
// 代理写入 chrome.storage.local
|
||||||
|
const targetDomain = msg.domain || domain
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
chrome.runtime.sendMessage(
|
||||||
|
{ type: `${CSL_MSG_PREFIX}_PUT`, domain: targetDomain, name: msg.name, data: msg.data, createTime: msg.createTime, updateTime: msg.updateTime },
|
||||||
|
() => resolve()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
cslProxyCh.postMessage({ type: 'csl-put-response', id: msg.id, name: msg.name } as ChannelMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.type === 'csl-get-request' && msg.id) {
|
||||||
|
// 代理读取 chrome.storage.local
|
||||||
|
const targetDomain = msg.domain || domain
|
||||||
|
const response = await new Promise<any>((resolve) => {
|
||||||
|
chrome.runtime.sendMessage(
|
||||||
|
{ type: `${CSL_MSG_PREFIX}_GET`, domain: targetDomain, name: msg.name },
|
||||||
|
(res) => resolve(res)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
const record = response?.record
|
||||||
|
cslProxyCh.postMessage({
|
||||||
|
type: 'csl-get-response',
|
||||||
|
id: msg.id,
|
||||||
|
name: msg.name,
|
||||||
|
data: record?.data,
|
||||||
|
createTime: record?.createTime,
|
||||||
|
updateTime: record?.updateTime,
|
||||||
|
} as ChannelMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 公开方法 ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* put - 单次发送数据(写入 IndexedDB + BroadcastChannel 广播)
|
||||||
|
* @param targetDomain 目标域名频道
|
||||||
|
* @param name 数据名称
|
||||||
|
* @param data 数据本体(需有 TS 类型定义)
|
||||||
|
* @param useCsl 是否使用 chrome.storage.local 跨域模式(通过 Background 中转)
|
||||||
|
*/
|
||||||
|
async function put<T extends object>(targetDomain: string, name: string, data: T, useCsl?: boolean): Promise<void> {
|
||||||
|
const now = nowLocalDateTime()
|
||||||
|
|
||||||
|
if (useCsl) {
|
||||||
|
// 跨域模式:通过 Background 读写 chrome.storage.local
|
||||||
|
const existing = await cslGet(targetDomain, name).catch(() => null)
|
||||||
|
const createTime = existing?.createTime || now
|
||||||
|
const updateTime = now
|
||||||
|
await cslPut(targetDomain, name, data, createTime, updateTime)
|
||||||
|
// 同时广播(同域标签页也能收到)
|
||||||
|
const ch = getChannel(targetDomain)
|
||||||
|
const msg: ChannelMessage = { type: 'put', name, data, createTime, updateTime }
|
||||||
|
ch.postMessage(msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认模式:IndexedDB
|
||||||
|
const existing = await dbGet(targetDomain, name)
|
||||||
|
const createTime = existing?.createTime || now
|
||||||
|
const updateTime = now
|
||||||
|
|
||||||
|
const record: DBRecord = { name, data, createTime, updateTime }
|
||||||
|
// 写入 IndexedDB
|
||||||
|
await dbPut(targetDomain, record)
|
||||||
|
// BroadcastChannel 广播
|
||||||
|
const ch = getChannel(targetDomain)
|
||||||
|
const msg: ChannelMessage = { type: 'put', name, data, createTime, updateTime }
|
||||||
|
ch.postMessage(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* emit - 高频发送数据(每次都广播,节流 500ms 写一次 IndexedDB)
|
||||||
|
* @param targetDomain 目标域名频道
|
||||||
|
* @param name 数据名称
|
||||||
|
* @param data 数据本体(需有 TS 类型定义)
|
||||||
|
* @param useCsl 是否使用 chrome.storage.local 跨域模式(通过 Background 中转)
|
||||||
|
*/
|
||||||
|
async function emit<T extends object>(targetDomain: string, name: string, data: T, useCsl?: boolean): Promise<void> {
|
||||||
|
const now = nowLocalDateTime()
|
||||||
|
const key = `${targetDomain}::${name}`
|
||||||
|
|
||||||
|
if (useCsl) {
|
||||||
|
// 跨域模式:每次广播,节流写 chrome.storage.local
|
||||||
|
let createTime: string
|
||||||
|
const pending = emitPending.get(key)
|
||||||
|
if (pending) {
|
||||||
|
createTime = pending.createTime
|
||||||
|
} else {
|
||||||
|
const existing = await cslGet(targetDomain, name).catch(() => null)
|
||||||
|
createTime = existing?.createTime || now
|
||||||
|
}
|
||||||
|
const updateTime = now
|
||||||
|
const record: DBRecord = { name, data, createTime, updateTime }
|
||||||
|
emitPending.set(key, record)
|
||||||
|
|
||||||
|
// BroadcastChannel 每次都广播
|
||||||
|
const ch = getChannel(targetDomain)
|
||||||
|
const msg: ChannelMessage = { type: 'emit', name, data, createTime, updateTime }
|
||||||
|
ch.postMessage(msg)
|
||||||
|
|
||||||
|
// 节流写 chrome.storage.local
|
||||||
|
if (!emitTimers.has(key)) {
|
||||||
|
emitTimers.set(key, setTimeout(async () => {
|
||||||
|
emitTimers.delete(key)
|
||||||
|
const latestRecord = emitPending.get(key)
|
||||||
|
if (latestRecord) {
|
||||||
|
emitPending.delete(key)
|
||||||
|
await cslPut(targetDomain, latestRecord.name, latestRecord.data, latestRecord.createTime, latestRecord.updateTime).catch(() => {})
|
||||||
|
}
|
||||||
|
}, EMIT_THROTTLE_MS))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认模式:IndexedDB
|
||||||
|
// 先尝试获取 createTime(从暂存或数据库)
|
||||||
|
let createTime: string
|
||||||
|
const pending = emitPending.get(key)
|
||||||
|
if (pending) {
|
||||||
|
createTime = pending.createTime
|
||||||
} else {
|
} else {
|
||||||
resolver(null)
|
const existing = await dbGet(targetDomain, name)
|
||||||
|
createTime = existing?.createTime || now
|
||||||
}
|
}
|
||||||
}
|
const updateTime = now
|
||||||
}
|
const record: DBRecord = { name, data, createTime, updateTime }
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化默认频道监听
|
// 暂存最新数据
|
||||||
getChannel(domain)
|
emitPending.set(key, record)
|
||||||
|
|
||||||
// --- 公开方法 ---
|
// BroadcastChannel 每次都广播(保证 watch 实时性)
|
||||||
|
const ch = getChannel(targetDomain)
|
||||||
|
const msg: ChannelMessage = { type: 'emit', name, data, createTime, updateTime }
|
||||||
|
ch.postMessage(msg)
|
||||||
|
|
||||||
/**
|
// 节流写库
|
||||||
* put - 单次发送数据(写入 IndexedDB + BroadcastChannel 广播)
|
if (!emitTimers.has(key)) {
|
||||||
* @param targetDomain 目标域名频道
|
emitTimers.set(key, setTimeout(async () => {
|
||||||
* @param name 数据名称
|
emitTimers.delete(key)
|
||||||
* @param data 数据本体(需有 TS 类型定义)
|
const latestRecord = emitPending.get(key)
|
||||||
*/
|
if (latestRecord) {
|
||||||
async function put<T extends object>(targetDomain: string, name: string, data: T): Promise<void> {
|
emitPending.delete(key)
|
||||||
const now = nowLocalDateTime()
|
await dbPut(targetDomain, latestRecord)
|
||||||
// 读取已有记录判断是否为新建
|
}
|
||||||
const existing = await dbGet(targetDomain, name)
|
}, EMIT_THROTTLE_MS))
|
||||||
const createTime = existing?.createTime || now
|
|
||||||
const updateTime = now
|
|
||||||
|
|
||||||
const record: DBRecord = { name, data, createTime, updateTime }
|
|
||||||
// 写入 IndexedDB
|
|
||||||
await dbPut(targetDomain, record)
|
|
||||||
// BroadcastChannel 广播
|
|
||||||
const ch = getChannel(targetDomain)
|
|
||||||
const msg: ChannelMessage = { type: 'put', name, data, createTime, updateTime }
|
|
||||||
ch.postMessage(msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* emit - 高频发送数据(每次都广播,节流 500ms 写一次 IndexedDB)
|
|
||||||
* @param targetDomain 目标域名频道
|
|
||||||
* @param name 数据名称
|
|
||||||
* @param data 数据本体(需有 TS 类型定义)
|
|
||||||
*/
|
|
||||||
async function emit<T extends object>(targetDomain: string, name: string, data: T): Promise<void> {
|
|
||||||
const now = nowLocalDateTime()
|
|
||||||
const key = `${targetDomain}::${name}`
|
|
||||||
|
|
||||||
// 先尝试获取 createTime(从暂存或数据库)
|
|
||||||
let createTime: string
|
|
||||||
const pending = emitPending.get(key)
|
|
||||||
if (pending) {
|
|
||||||
createTime = pending.createTime
|
|
||||||
} else {
|
|
||||||
const existing = await dbGet(targetDomain, name)
|
|
||||||
createTime = existing?.createTime || now
|
|
||||||
}
|
|
||||||
const updateTime = now
|
|
||||||
const record: DBRecord = { name, data, createTime, updateTime }
|
|
||||||
|
|
||||||
// 暂存最新数据
|
|
||||||
emitPending.set(key, record)
|
|
||||||
|
|
||||||
// BroadcastChannel 每次都广播(保证 watch 实时性)
|
|
||||||
const ch = getChannel(targetDomain)
|
|
||||||
const msg: ChannelMessage = { type: 'emit', name, data, createTime, updateTime }
|
|
||||||
ch.postMessage(msg)
|
|
||||||
|
|
||||||
// 节流写库
|
|
||||||
if (!emitTimers.has(key)) {
|
|
||||||
emitTimers.set(key, setTimeout(async () => {
|
|
||||||
emitTimers.delete(key)
|
|
||||||
const latestRecord = emitPending.get(key)
|
|
||||||
if (latestRecord) {
|
|
||||||
emitPending.delete(key)
|
|
||||||
await dbPut(targetDomain, latestRecord)
|
|
||||||
}
|
}
|
||||||
}, EMIT_THROTTLE_MS))
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get - 主动获取一次数据(先 BroadcastChannel 请求,60ms 超时转 IndexedDB)
|
* get - 主动获取一次数据(先 BroadcastChannel 请求,60ms 超时转 IndexedDB)
|
||||||
* @param targetDomain 目标域名频道
|
* @param targetDomain 目标域名频道
|
||||||
* @param name 数据名称
|
* @param name 数据名称
|
||||||
* @returns 数据本体(含 createTime/updateTime),无数据返回 null
|
* @param useCsl 是否使用 chrome.storage.local 跨域模式(通过 Background 中转)
|
||||||
*/
|
* @returns 数据本体(含 createTime/updateTime),无数据返回 null
|
||||||
async function get<T extends object>(targetDomain: string, name: string): Promise<BridgeData<T> | null> {
|
*/
|
||||||
const ch = getChannel(targetDomain)
|
async function get<T extends object>(targetDomain: string, name: string, useCsl?: boolean): Promise<BridgeData<T> | null> {
|
||||||
const requestId = uid()
|
if (useCsl) {
|
||||||
|
// 跨域模式:先广播请求,超时读 chrome.storage.local
|
||||||
|
const ch = getChannel(targetDomain)
|
||||||
|
const requestId = uid()
|
||||||
|
const msg: ChannelMessage = { type: 'get-request', name, id: requestId }
|
||||||
|
ch.postMessage(msg)
|
||||||
|
|
||||||
// 发起广播请求
|
const result = await new Promise<BridgeData<T> | null>((resolve) => {
|
||||||
const msg: ChannelMessage = { type: 'get-request', name, id: requestId }
|
getResolvers.set(requestId, resolve as any)
|
||||||
ch.postMessage(msg)
|
setTimeout(() => {
|
||||||
|
if (getResolvers.has(requestId)) {
|
||||||
|
getResolvers.delete(requestId)
|
||||||
|
resolve(null)
|
||||||
|
}
|
||||||
|
}, GET_TIMEOUT_MS)
|
||||||
|
})
|
||||||
|
|
||||||
// 等待响应或超时
|
if (result) return result
|
||||||
const result = await new Promise<BridgeData<T> | null>((resolve) => {
|
|
||||||
getResolvers.set(requestId, resolve as any)
|
// 超时降级读 chrome.storage.local
|
||||||
setTimeout(() => {
|
const record = await cslGet(targetDomain, name).catch(() => null)
|
||||||
if (getResolvers.has(requestId)) {
|
if (record) {
|
||||||
getResolvers.delete(requestId)
|
return { ...record.data, createTime: record.createTime, updateTime: record.updateTime } as BridgeData<T>
|
||||||
resolve(null) // 超时,标记为未收到响应
|
}
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
}, GET_TIMEOUT_MS)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 如果广播拿到了数据直接返回
|
// 默认模式:IndexedDB
|
||||||
if (result) return result
|
const ch = getChannel(targetDomain)
|
||||||
|
const requestId = uid()
|
||||||
|
|
||||||
// 超时降级读 IndexedDB
|
// 发起广播请求
|
||||||
const record = await dbGet(targetDomain, name)
|
const msg: ChannelMessage = { type: 'get-request', name, id: requestId }
|
||||||
if (record) {
|
ch.postMessage(msg)
|
||||||
return { ...record.data, createTime: record.createTime, updateTime: record.updateTime } as BridgeData<T>
|
|
||||||
|
// 等待响应或超时
|
||||||
|
const result = await new Promise<BridgeData<T> | null>((resolve) => {
|
||||||
|
getResolvers.set(requestId, resolve as any)
|
||||||
|
setTimeout(() => {
|
||||||
|
if (getResolvers.has(requestId)) {
|
||||||
|
getResolvers.delete(requestId)
|
||||||
|
resolve(null) // 超时,标记为未收到响应
|
||||||
|
}
|
||||||
|
}, GET_TIMEOUT_MS)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 如果广播拿到了数据直接返回
|
||||||
|
if (result) return result
|
||||||
|
|
||||||
|
// 超时降级读 IndexedDB
|
||||||
|
const record = await dbGet(targetDomain, name)
|
||||||
|
if (record) {
|
||||||
|
return { ...record.data, createTime: record.createTime, updateTime: record.updateTime } as BridgeData<T>
|
||||||
|
}
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* watch - 持续监听数据变化(初始读一次缓存,后续实时接收 put/emit 推送)
|
* watch - 持续监听数据变化(初始读一次缓存,后续实时接收 put/emit 推送)
|
||||||
* @param targetDomain 目标域名频道
|
* @param targetDomain 目标域名频道
|
||||||
* @param name 数据名称
|
* @param name 数据名称
|
||||||
* @param callback 数据变化回调,参数为最新数据或 null
|
* @param callback 数据变化回调,参数为最新数据或 null
|
||||||
* @returns 取消监听的函数
|
* @param useCsl 是否使用 chrome.storage.local 跨域模式(通过 Background 中转)
|
||||||
*/
|
* @returns 取消监听的函数
|
||||||
function watch<T extends object>(targetDomain: string, name: string, callback: WatchCallback<T>): () => void {
|
*/
|
||||||
const key = `${targetDomain}::${name}`
|
function watch<T extends object>(targetDomain: string, name: string, callback: WatchCallback<T>, useCsl?: boolean): () => void {
|
||||||
// 确保频道已初始化
|
const key = `${targetDomain}::${name}`
|
||||||
getChannel(targetDomain)
|
// 确保频道已初始化
|
||||||
|
getChannel(targetDomain)
|
||||||
|
|
||||||
// 注册回调
|
// 注册回调(BroadcastChannel 广播也能触发,不管 useCsl 与否)
|
||||||
if (!watchers.has(key)) watchers.set(key, new Set())
|
if (!watchers.has(key)) watchers.set(key, new Set())
|
||||||
watchers.get(key)!.add(callback)
|
watchers.get(key)!.add(callback)
|
||||||
|
|
||||||
// 初始读取 IndexedDB
|
if (useCsl) {
|
||||||
dbGet(targetDomain, name).then((record) => {
|
// 跨域模式:初始读 chrome.storage.local
|
||||||
if (record) {
|
cslGet(targetDomain, name).then((record) => {
|
||||||
callback({ ...record.data, createTime: record.createTime, updateTime: record.updateTime } as BridgeData<T>)
|
if (record) {
|
||||||
} else {
|
callback({ ...record.data, createTime: record.createTime, updateTime: record.updateTime } as BridgeData<T>)
|
||||||
callback(null)
|
} else {
|
||||||
}
|
callback(null)
|
||||||
})
|
}
|
||||||
|
}).catch(() => callback(null))
|
||||||
|
|
||||||
// 返回取消监听函数
|
// 监听 chrome.storage.onChanged 事件
|
||||||
return () => {
|
const storageListener = (changes: { [key: string]: chrome.storage.StorageChange }, areaName: string) => {
|
||||||
const set = watchers.get(key)
|
if (areaName !== 'local' || !changes[CSL_STORAGE_KEY]) return
|
||||||
if (set) {
|
const newValue = changes[CSL_STORAGE_KEY].newValue as Array<{ domain: string; value: DBRecord }> | undefined
|
||||||
set.delete(callback)
|
if (!newValue) return
|
||||||
if (set.size === 0) watchers.delete(key)
|
const item = newValue.find(i => i.domain === targetDomain && i.value?.name === name)
|
||||||
}
|
if (item && item.value) {
|
||||||
|
callback({ ...item.value.data, createTime: item.value.createTime, updateTime: item.value.updateTime } as BridgeData<T>)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.onChanged) {
|
||||||
|
chrome.storage.onChanged.addListener(storageListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回取消监听函数
|
||||||
|
return () => {
|
||||||
|
const set = watchers.get(key)
|
||||||
|
if (set) {
|
||||||
|
set.delete(callback)
|
||||||
|
if (set.size === 0) watchers.delete(key)
|
||||||
|
}
|
||||||
|
if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.onChanged) {
|
||||||
|
chrome.storage.onChanged.removeListener(storageListener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认模式:初始读取 IndexedDB
|
||||||
|
dbGet(targetDomain, name).then((record) => {
|
||||||
|
if (record) {
|
||||||
|
callback({ ...record.data, createTime: record.createTime, updateTime: record.updateTime } as BridgeData<T>)
|
||||||
|
} else {
|
||||||
|
callback(null)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 返回取消监听函数
|
||||||
|
return () => {
|
||||||
|
const set = watchers.get(key)
|
||||||
|
if (set) {
|
||||||
|
set.delete(callback)
|
||||||
|
if (set.size === 0) watchers.delete(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* destroy - 销毁实例,关闭所有频道,清除定时器
|
* destroy - 销毁实例,关闭所有频道,清除定时器
|
||||||
*/
|
*/
|
||||||
function destroy(): void {
|
function destroy(): void {
|
||||||
channels.forEach(ch => ch.close())
|
channels.forEach(ch => ch.close())
|
||||||
channels.clear()
|
channels.clear()
|
||||||
watchers.clear()
|
watchers.clear()
|
||||||
emitTimers.forEach(timer => clearTimeout(timer))
|
emitTimers.forEach(timer => clearTimeout(timer))
|
||||||
emitTimers.clear()
|
emitTimers.clear()
|
||||||
emitPending.clear()
|
emitPending.clear()
|
||||||
getResolvers.clear()
|
getResolvers.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
return { put, emit, get, watch, destroy }
|
return { put, emit, get, watch, destroy }
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-7
@@ -7,18 +7,18 @@
|
|||||||
<!-- 主内容区域 -->
|
<!-- 主内容区域 -->
|
||||||
<div class="agent-page__content" v-loading="pageLoading" element-loading-text="加载中...">
|
<div class="agent-page__content" v-loading="pageLoading" element-loading-text="加载中...">
|
||||||
<!-- 准备向导 — 第1步到第4步-->
|
<!-- 准备向导 — 第1步到第4步-->
|
||||||
<!-- <AgentSetupWizard
|
<AgentSetupWizard
|
||||||
v-if="!pageLoading && showSetupWizard"
|
v-if="!pageLoading && showSetupWizard"
|
||||||
:initial-config="agentConfig"
|
:initial-config="agentConfig"
|
||||||
@complete="handleSetupComplete"
|
@complete="handleSetupComplete"
|
||||||
@launch="handleLaunch"
|
@launch="handleLaunch"
|
||||||
/> -->
|
|
||||||
<AgentSetupWizard
|
|
||||||
v-if="true"
|
|
||||||
:initial-config="agentConfig"
|
|
||||||
@complete="handleSetupComplete"
|
|
||||||
@launch="handleLaunch"
|
|
||||||
/>
|
/>
|
||||||
|
<!-- <AgentSetupWizard-->
|
||||||
|
<!-- v-if="true"-->
|
||||||
|
<!-- :initial-config="agentConfig"-->
|
||||||
|
<!-- @complete="handleSetupComplete"-->
|
||||||
|
<!-- @launch="handleLaunch"-->
|
||||||
|
<!-- />-->
|
||||||
|
|
||||||
|
|
||||||
<!-- 求职助手正式内容区域 -->
|
<!-- 求职助手正式内容区域 -->
|
||||||
@@ -418,6 +418,11 @@ onMounted(async () => {
|
|||||||
loadProgressFromStorage()
|
loadProgressFromStorage()
|
||||||
loadCompletedFromStorage()
|
loadCompletedFromStorage()
|
||||||
await loadAgentConfig()
|
await loadAgentConfig()
|
||||||
|
|
||||||
|
// 检查插件版本是否最新
|
||||||
|
if (store.state.plugIsOnline && !store.state.isLatestPlugin) {
|
||||||
|
ElMessage.warning(`当前安装的浏览器插件不是最新${store.state.plugTargetVersion}版,请去智能投递里下载最新版安装`)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
/** 查询求职助手配置 */
|
/** 查询求职助手配置 */
|
||||||
|
|||||||
+16
-1
@@ -366,6 +366,7 @@
|
|||||||
import { ref, watch, onMounted, onBeforeUnmount, nextTick, computed } from 'vue'
|
import { ref, watch, onMounted, onBeforeUnmount, nextTick, computed } from 'vue'
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import { useStore } from 'vuex'
|
import { useStore } from 'vuex'
|
||||||
|
import { createChannelBridge } from '@/utils/channelBridge'
|
||||||
import { JOB_TYPE_OPTIONS, formatEmploymentType, formatEducation } from '@/stores/index'
|
import { JOB_TYPE_OPTIONS, formatEmploymentType, formatEducation } from '@/stores/index'
|
||||||
import SideNav from '@/components/SideNav.vue'
|
import SideNav from '@/components/SideNav.vue'
|
||||||
import AiChat from '@/components/AiChat.vue'
|
import AiChat from '@/components/AiChat.vue'
|
||||||
@@ -903,9 +904,23 @@ async function handleApplyMethodAction() {
|
|||||||
if (!applyMethodChoice.value) return
|
if (!applyMethodChoice.value) return
|
||||||
|
|
||||||
if (applyMethodChoice.value === 'direct') {
|
if (applyMethodChoice.value === 'direct') {
|
||||||
// 直接前往官网
|
// 直接前往官网 — 同时跨域 put 链接给插件
|
||||||
showAgentRemindDialog.value = false
|
showAgentRemindDialog.value = false
|
||||||
if (pendingApplyUrl.value) {
|
if (pendingApplyUrl.value) {
|
||||||
|
// 跨域推送投递链接到插件
|
||||||
|
const bridge = createChannelBridge({ domain: store.state.bridgeDomain })
|
||||||
|
try {
|
||||||
|
const existing = await bridge.get<{ linkList: string[] }>(store.state.bridgeDomain, 'offerpaiDeliveryLinkList', true)
|
||||||
|
const linkList: string[] = existing?.linkList || []
|
||||||
|
if (!linkList.includes(pendingApplyUrl.value)) {
|
||||||
|
linkList.push(pendingApplyUrl.value)
|
||||||
|
}
|
||||||
|
await bridge.put<{ linkList: string[] }>(store.state.bridgeDomain, 'offerpaiDeliveryLinkList', { linkList }, true)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[Jobs] 跨域 put 投递链接失败', e)
|
||||||
|
} finally {
|
||||||
|
bridge.destroy()
|
||||||
|
}
|
||||||
window.open(pendingApplyUrl.value, '_blank')
|
window.open(pendingApplyUrl.value, '_blank')
|
||||||
}
|
}
|
||||||
pendingApplyUrl.value = ''
|
pendingApplyUrl.value = ''
|
||||||
|
|||||||
@@ -261,6 +261,7 @@ import { useRouter, useRoute } from 'vue-router'
|
|||||||
import { sendSmsCode, smsLogin } from '@/api/auth'
|
import { sendSmsCode, smsLogin } from '@/api/auth'
|
||||||
import { fetchProfile, syncProfileFromResume } from '@/api/profile'
|
import { fetchProfile, syncProfileFromResume } from '@/api/profile'
|
||||||
import { uploadResume } from '@/utils/aiRequest'
|
import { uploadResume } from '@/utils/aiRequest'
|
||||||
|
import { fetchJobIntention, saveJobIntention } from '@/api/jobs'
|
||||||
import AgreementPreviewDialog from '@/components/tools/AgreementPreviewDialog.vue'
|
import AgreementPreviewDialog from '@/components/tools/AgreementPreviewDialog.vue'
|
||||||
|
|
||||||
// 左侧动画面板图片资源
|
// 左侧动画面板图片资源
|
||||||
@@ -606,6 +607,17 @@ async function startUpload(file: File) {
|
|||||||
if (res.code === 0 && res.data?.resumeId) {
|
if (res.code === 0 && res.data?.resumeId) {
|
||||||
// 上传成功,同步个人资料
|
// 上传成功,同步个人资料
|
||||||
await syncProfileFromResume(String(res.data.resumeId))
|
await syncProfileFromResume(String(res.data.resumeId))
|
||||||
|
// 等待1秒后同步AI推荐岗位到求职意向
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||||
|
const intentionRes = await fetchJobIntention()
|
||||||
|
if (intentionRes.code === '0' && intentionRes.data) {
|
||||||
|
const intention = intentionRes.data
|
||||||
|
// 把 aiCategoryIds 复制到 categoryIds 后保存
|
||||||
|
if (intention.aiCategoryIds && intention.aiCategoryIds.length) {
|
||||||
|
intention.categoryIds = intention.aiCategoryIds.map(Number)
|
||||||
|
await saveJobIntention(intention)
|
||||||
|
}
|
||||||
|
}
|
||||||
// 标记接口已返回
|
// 标记接口已返回
|
||||||
uploadDone = true
|
uploadDone = true
|
||||||
// 完成所有步骤的动画后会自动过渡到 success 状态
|
// 完成所有步骤的动画后会自动过渡到 success 状态
|
||||||
|
|||||||
Reference in New Issue
Block a user