|
@@ -0,0 +1,687 @@
|
|
|
|
|
+/**
|
|
|
|
|
+ * @fileoverview 一个 SharedWorker,用于处理 Web 应用程序的后台任务,例如 AI 内容生成、翻译和 SEO 优化。
|
|
|
|
|
+ */
|
|
|
|
|
+
|
|
|
|
|
+'use strict'
|
|
|
|
|
+
|
|
|
|
|
+// ===================================================================================
|
|
|
|
|
+// 1. 状态与配置
|
|
|
|
|
+// ===================================================================================
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Worker 的全局状态。
|
|
|
|
|
+ */
|
|
|
|
|
+const state = {
|
|
|
|
|
+ token: null,
|
|
|
|
|
+ mainPort: null, // 主“守护”页面的端口
|
|
|
|
|
+ helperPort: null, // AI 助手 UI 的端口
|
|
|
|
|
+ aiUrl: '',
|
|
|
|
|
+ baseURL: '',
|
|
|
|
|
+ settings: {
|
|
|
|
|
+ maxRetry: 5, // 默认全局重试次数限制
|
|
|
|
|
+ retryOne: 3, // 默认单个任务重试次数限制
|
|
|
|
|
+ debug: false
|
|
|
|
|
+ },
|
|
|
|
|
+ seoConfig: {},
|
|
|
|
|
+ workList: [],
|
|
|
|
|
+ isWorkLoopActive: false,
|
|
|
|
|
+ globalRetryCount: 0
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * API 端点定义。
|
|
|
|
|
+ * 使用 getter 确保 state 中的 baseURL 始终是最新的。
|
|
|
|
|
+ */
|
|
|
|
|
+const api = {
|
|
|
|
|
+ _constructUrl(path) { return state.baseURL + path },
|
|
|
|
|
+ get getBlog() { return this._constructUrl('api/blog/info') },
|
|
|
|
|
+ get getBlogTags() { return this._constructUrl('api/blog/get-tag-list') },
|
|
|
|
|
+ get getBlogTypes() { return this._constructUrl('api/blog/get-type-list') },
|
|
|
|
|
+ get getBlogPlates() { return this._constructUrl('api/blog/get-plate-list') },
|
|
|
|
|
+ get getBlogList() { return this._constructUrl('/api/blog/get-list') },
|
|
|
|
|
+ get saveBlog() { return this._constructUrl('api/blog/save') },
|
|
|
|
|
+ get getProduct() { return this._constructUrl('api/product/info') },
|
|
|
|
|
+ get getProductTags() { return this._constructUrl('api/product/get-tag-list') },
|
|
|
|
|
+ get getProductTypes() { return this._constructUrl('api/product/get-type-list') },
|
|
|
|
|
+ get saveProduct() { return this._constructUrl('api/product/save') },
|
|
|
|
|
+ get getMeeting() { return this._constructUrl('api/meeting/info') },
|
|
|
|
|
+ get getMeetingTypes() { return this._constructUrl('api/meeting/get-type-list') },
|
|
|
|
|
+ get getMeetingTags() { return this._constructUrl('api/meeting/get-tag-list') },
|
|
|
|
|
+ get saveMeeting() { return this._constructUrl('api/meeting/save') },
|
|
|
|
|
+ get getStaticPage() { return this._constructUrl('api/static-page/get-info') },
|
|
|
|
|
+ get saveStaticPage() { return this._constructUrl('api/static-page/save') },
|
|
|
|
|
+ get transStatus() { return this._constructUrl('api/web/page/update-trans-status') }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ===================================================================================
|
|
|
|
|
+// 2. WORKER 核心逻辑
|
|
|
|
|
+// ===================================================================================
|
|
|
|
|
+
|
|
|
|
|
+console.log('Worker: 脚本已加载,准备接收连接。')
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 处理到 SharedWorker 的新连接。
|
|
|
|
|
+ */
|
|
|
|
|
+onconnect = (event) => {
|
|
|
|
|
+ const port = event.ports[0]
|
|
|
|
|
+ console.log('Worker: 新连接已建立。')
|
|
|
|
|
+
|
|
|
|
|
+ port.onmessage = (e) => {
|
|
|
|
|
+ const { type, data, message } = e.data
|
|
|
|
|
+ console.log(`Worker: 接收到类型为 "${type}" 的消息`, e.data)
|
|
|
|
|
+
|
|
|
|
|
+ // 仅启动一次工作循环。
|
|
|
|
|
+ if (!state.isWorkLoopActive) {
|
|
|
|
|
+ state.isWorkLoopActive = true
|
|
|
|
|
+ startWorkLoop()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ switch (type) {
|
|
|
|
|
+ case 'init':
|
|
|
|
|
+ handleInit(port, e.data)
|
|
|
|
|
+ break
|
|
|
|
|
+ case 'helper':
|
|
|
|
|
+ state.helperPort = port
|
|
|
|
|
+ console.log('Worker: AI 助手页面已连接。')
|
|
|
|
|
+ break
|
|
|
|
|
+ case 'connect':
|
|
|
|
|
+ console.log('Worker: 普通页面已连接。')
|
|
|
|
|
+ if (state.mainPort) {
|
|
|
|
|
+ state.mainPort.postMessage(createMessage('connect', message, {}))
|
|
|
|
|
+ }
|
|
|
|
|
+ break
|
|
|
|
|
+ case 'translate':
|
|
|
|
|
+ case 'seo':
|
|
|
|
|
+ case 'write':
|
|
|
|
|
+ handleNewTask(type, data, port, message)
|
|
|
|
|
+ break
|
|
|
|
|
+ default:
|
|
|
|
|
+ console.warn(`Worker: 接收到未知消息类型 "${type}"`)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ port.postMessage(createMessage('message', '新页面已连接'))
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 使用主页面提供的必要数据初始化 Worker。
|
|
|
|
|
+ * @param {MessagePort} port - 连接页面的端口。
|
|
|
|
|
+ * @param {object} eventData - 消息事件中的数据对象。
|
|
|
|
|
+ */
|
|
|
|
|
+function handleInit(port, eventData) {
|
|
|
|
|
+ state.mainPort = port
|
|
|
|
|
+ const { token, aiUrl, setting, seoConfig, baseURL } = eventData.data
|
|
|
|
|
+ state.token = token
|
|
|
|
|
+ state.aiUrl = aiUrl
|
|
|
|
|
+ state.settings = { ...state.settings, ...setting }
|
|
|
|
|
+ state.seoConfig = seoConfig
|
|
|
|
|
+ state.baseURL = baseURL
|
|
|
|
|
+ console.log('Worker: 主页面已连接,状态已初始化。', { settings: state.settings, baseURL: state.baseURL })
|
|
|
|
|
+ port.postMessage(createMessage('init', eventData.message, {}))
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 处理新任务的提交。
|
|
|
|
|
+ * @param {string} type - 任务类型 ('translate', 'seo', 'write')。
|
|
|
|
|
+ * @param {object} data - 任务所需的数据。
|
|
|
|
|
+ * @param {MessagePort} port - 提交任务的端口。
|
|
|
|
|
+ * @param {string} message - 附带的消息。
|
|
|
|
|
+ */
|
|
|
|
|
+function handleNewTask(type, data, port, message) {
|
|
|
|
|
+ if (addTask(type, data, port)) {
|
|
|
|
|
+ const successMsg = createMessage('success', message)
|
|
|
|
|
+ // 通知所有相关端口任务已成功入队
|
|
|
|
|
+ broadcastMessage(createMessage(type, `新的 ${type} 任务已添加到队列。`))
|
|
|
|
|
+ port.postMessage(successMsg)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ port.postMessage(createMessage('error', '创建任务失败,任务可能已存在。'))
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 处理 workList 中任务的主循环。
|
|
|
|
|
+ */
|
|
|
|
|
+async function startWorkLoop() {
|
|
|
|
|
+ console.log('Worker: 工作循环已启动。')
|
|
|
|
|
+ while (true) {
|
|
|
|
|
+ const workItem = state.workList.find(w => w.state === 'waiting')
|
|
|
|
|
+
|
|
|
|
|
+ if (!workItem || !state.token) {
|
|
|
|
|
+ await new Promise(resolve => setTimeout(resolve, 1000))
|
|
|
|
|
+ continue
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ workItem.state = 'running'
|
|
|
|
|
+ console.log(`Worker: 开始执行任务 ${workItem.id},类型为 ${workItem.type}`)
|
|
|
|
|
+ broadcastMessage(createMessage('update', `开始任务: ${workItem.type} (ID: ${workItem.data.id || ''})`))
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ let taskResult
|
|
|
|
|
+ switch (workItem.type) {
|
|
|
|
|
+ case 'translate':
|
|
|
|
|
+ taskResult = await doTranslateTask(workItem)
|
|
|
|
|
+ break
|
|
|
|
|
+ case 'seo':
|
|
|
|
|
+ taskResult = await doSeoTask(workItem)
|
|
|
|
|
+ break
|
|
|
|
|
+ case 'write':
|
|
|
|
|
+ taskResult = await doWriteTask(workItem)
|
|
|
|
|
+ break
|
|
|
|
|
+ default:
|
|
|
|
|
+ throw new Error(`未知的任务类型: ${workItem.type}`)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 关键改动:先更新列表,再发送消息
|
|
|
|
|
+ workSuccess(workItem)
|
|
|
|
|
+
|
|
|
|
|
+ // 使用任务返回的结果来广播消息
|
|
|
|
|
+ broadcastMessage(createMessage('success', taskResult.message, {}, taskResult.response))
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error(`Worker: 任务 ${workItem.id} 执行失败。`, error)
|
|
|
|
|
+ broadcastMessage(createMessage('error', `类型为 ${workItem.type} 的任务失败。详情请查看控制台。`, {}, error.message))
|
|
|
|
|
+ workFail(workItem)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ===================================================================================
|
|
|
|
|
+// 3. 任务管理
|
|
|
|
|
+// ===================================================================================
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 如果不存在相似任务,则向工作队列中添加一个新任务。
|
|
|
|
|
+ * @param {string} type - 任务类型 ('translate', 'seo', 'write')。
|
|
|
|
|
+ * @param {object} data - 任务特定数据,必须包含 'id' 和 'type' 以进行唯一性检查。
|
|
|
|
|
+ * @param {MessagePort} port - 发起任务的端口。
|
|
|
|
|
+ * @returns {boolean} - 如果任务被添加则返回 true,否则返回 false。
|
|
|
|
|
+ */
|
|
|
|
|
+function addTask(type, data, port) {
|
|
|
|
|
+ const exists = state.workList.some(item =>
|
|
|
|
|
+ item.type === type &&
|
|
|
|
|
+ item.data.id === data.id &&
|
|
|
|
|
+ item.data.type === data.type // 内部类型,例如 'blog'
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ if (exists) {
|
|
|
|
|
+ console.warn(`类型为 "${type}" 且针对项目 ${data.id} 的任务已在队列中。`)
|
|
|
|
|
+ return false
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const workItem = {
|
|
|
|
|
+ id: `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
|
|
|
|
|
+ type: type,
|
|
|
|
|
+ data: data,
|
|
|
|
|
+ port: port,
|
|
|
|
|
+ state: 'waiting',
|
|
|
|
|
+ retry: 0
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ state.workList.push(workItem)
|
|
|
|
|
+ console.log(`任务 ${workItem.id} 已添加。队列长度: ${state.workList.length}`)
|
|
|
|
|
+ return true
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 从列表中移除已完成的任务,并重置全局重试计数器。
|
|
|
|
|
+ * @param {object} workItem - 成功完成的工作项。
|
|
|
|
|
+ */
|
|
|
|
|
+function workSuccess(workItem) {
|
|
|
|
|
+ state.workList = state.workList.filter(w => w.id !== workItem.id)
|
|
|
|
|
+ state.globalRetryCount = 0 // 任何任务成功后重置
|
|
|
|
|
+ console.log(`Worker: 任务 ${workItem.id} 已成功完成。`)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 处理失败的任务,并实现重试逻辑。
|
|
|
|
|
+ * @param {object} workItem - 失败的工作项。
|
|
|
|
|
+ */
|
|
|
|
|
+function workFail(workItem) {
|
|
|
|
|
+ // 从列表中移除,以便后续可能重新添加
|
|
|
|
|
+ state.workList = state.workList.filter(w => w.id !== workItem.id)
|
|
|
|
|
+ state.globalRetryCount++
|
|
|
|
|
+
|
|
|
|
|
+ if (state.globalRetryCount >= state.settings.maxRetry) {
|
|
|
|
|
+ broadcastMessage(createMessage('error', '已达到全局最大重试次数。所有任务已中止。'))
|
|
|
|
|
+ state.workList = [] // 清空所有任务
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ workItem.retry++
|
|
|
|
|
+ if (workItem.retry < state.settings.retryOne) {
|
|
|
|
|
+ workItem.state = 'waiting'
|
|
|
|
|
+ state.workList.push(workItem) // 添加回队列末尾
|
|
|
|
|
+ const msg = `任务失败,正在重新排队。尝试次数 ${workItem.retry}/${state.settings.retryOne}。`
|
|
|
|
|
+ console.warn(msg)
|
|
|
|
|
+ broadcastMessage(createMessage('warning', msg))
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const msg = `任务已达到其重试上限,已被丢弃。`
|
|
|
|
|
+ console.error(msg, workItem.data)
|
|
|
|
|
+ broadcastMessage(createMessage('error', msg, {}, workItem.data))
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ===================================================================================
|
|
|
|
|
+// 4. 异步任务处理器
|
|
|
|
|
+// ===================================================================================
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 执行一个 'write' 写作任务。
|
|
|
|
|
+ * @param {object} work - 写作任务的工作项。
|
|
|
|
|
+ */
|
|
|
|
|
+async function doWriteTask(work) {
|
|
|
|
|
+ const { writeInfo, typeList, tagList, template } = work.data
|
|
|
|
|
+ const titleList = []
|
|
|
|
|
+
|
|
|
|
|
+ // 获取现有博客标题以避免重复
|
|
|
|
|
+ const blogListResponse = await apiClient(api.getBlogList, 'GET', { page: 1, page_size: 999 })
|
|
|
|
|
+ blogListResponse.data.data.forEach(item => titleList.push(item.title))
|
|
|
|
|
+
|
|
|
|
|
+ // 构建 AI 提示
|
|
|
|
|
+ let templateCreatWay = ''
|
|
|
|
|
+ template.templateList.forEach((item, index) => {
|
|
|
|
|
+ let prompt = ''
|
|
|
|
|
+ if (item.type === 'title') {
|
|
|
|
|
+ prompt = `
|
|
|
|
|
+ 第${index}部分是一个标题,其内容${item.isAi ? '需要你根据以下提示生成:' + item.value : "值为'" + item.value + "'"}。
|
|
|
|
|
+ `
|
|
|
|
|
+ } else {
|
|
|
|
|
+ prompt = `
|
|
|
|
|
+ 第${index}部分是一个段落,其内容${item.isAi ? '需要你根据以下提示生成:' + item.value : "值为'" + item.value + "'"}。${item.canTitle ? '你可以在这个部分创建多个段落,以及次级标题,也就是创建的标题等级不能高于这个段落所属的那个标题' : '你只能生成一个段落,也就是只能创建一个p标签'}。
|
|
|
|
|
+ `
|
|
|
|
|
+ }
|
|
|
|
|
+ templateCreatWay += prompt
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ const requestJson = {
|
|
|
|
|
+ contents: [{
|
|
|
|
|
+ role: 'model',
|
|
|
|
|
+ parts: [{
|
|
|
|
|
+ text: `
|
|
|
|
|
+ 无论用户使用何种语言提问,返回给用户的数据一律使用${state.settings.language}回答,除非我直接告诉你这条数据值为多少。
|
|
|
|
|
+ 你需要按照以下规则生成指定条数的文章数据输出至blog_list数组,接下来我将告诉你单条文章数据内的每个属性该如何生成。
|
|
|
|
|
+ title:这是文章标题数据,内容${writeInfo.title.is_ai ? '根据文章具体内容生成' : "值为'" + writeInfo.title.text + "'"}。生成的标题不能与${JSON.stringify(titleList)}中的文章标题重复。
|
|
|
|
|
+ author:这是文章作者,值为${writeInfo.author}。
|
|
|
|
|
+ content:这是文章正文,${template.templatePrompt},使用html格式输出,你只可以使用'p','h1-h6','ul','ol','b','em','sub','sup','u','table','blockquote' 进行文章生成,${templateCreatWay}。
|
|
|
|
|
+ description:这是文章梗概,根据内容生成,50个字以内。
|
|
|
|
|
+ type_ids:这是文章类型,${writeInfo.types.is_ai ? '根据文章内容从' + JSON.stringify(typeList) + '挑选一个,将其id填入' : "值为'" + JSON.stringify(writeInfo.types.ids) + "'。这是所有可用类型数据,你可用根据id找到对应的数据,作为生成文章的参考:" + JSON.stringify(typeList) + '。'}
|
|
|
|
|
+ tag_ids:这是文章标签,${writeInfo.tags.is_ai ? '根据文章内容从' + JSON.stringify(tagList) + '挑选任意数量,将其id填入' : "值为'" + JSON.stringify(writeInfo.tags.ids) + "'。这是所有可用标签数据,你可用根据id找到对应的数据,作为生成文章的参考:" + JSON.stringify(tagList) + '。'}
|
|
|
|
|
+ `
|
|
|
|
|
+ }]
|
|
|
|
|
+ }, {
|
|
|
|
|
+ role: 'user',
|
|
|
|
|
+ parts: [{
|
|
|
|
|
+ text: `生成${writeInfo.number}条文章数据`
|
|
|
|
|
+ }]
|
|
|
|
|
+ }],
|
|
|
|
|
+ generationConfig: {
|
|
|
|
|
+ responseMimeType: 'application/json',
|
|
|
|
|
+ responseSchema: {
|
|
|
|
|
+ type: 'object',
|
|
|
|
|
+ properties: {
|
|
|
|
|
+ blog_list: {
|
|
|
|
|
+ type: 'array',
|
|
|
|
|
+ items: {
|
|
|
|
|
+ type: 'object',
|
|
|
|
|
+ properties: {
|
|
|
|
|
+ title: { type: 'string' },
|
|
|
|
|
+ author: { type: 'string' },
|
|
|
|
|
+ content: { type: 'string' },
|
|
|
|
|
+ description: { type: 'string' },
|
|
|
|
|
+ type_ids: { type: 'array', items: { type: 'number' }},
|
|
|
|
|
+ tag_ids: { type: 'array', items: { type: 'number' }}
|
|
|
|
|
+ },
|
|
|
|
|
+ required: ['title', 'author', 'content']
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 调用 AI 并处理结果
|
|
|
|
|
+ const aiResult = await callAI(requestJson)
|
|
|
|
|
+ const blogList = aiResult.blog_list
|
|
|
|
|
+
|
|
|
|
|
+ for (const blog of blogList) {
|
|
|
|
|
+ const saveData = {
|
|
|
|
|
+ ...blog,
|
|
|
|
|
+ image_url: writeInfo.cover.url,
|
|
|
|
|
+ image_alt: writeInfo.cover.url,
|
|
|
|
|
+ status: 1,
|
|
|
|
|
+ plate_id: 1
|
|
|
|
|
+ }
|
|
|
|
|
+ const saveResponse = await apiClient(api.saveBlog, 'POST', saveData)
|
|
|
|
|
+ const newBlogId = JSON.parse(saveResponse).data
|
|
|
|
|
+
|
|
|
|
|
+ // 为新创建的博客自动排队一个 SEO 任务
|
|
|
|
|
+ addTask('seo', { id: newBlogId, title: blog.title, type: 'blog' }, work.port)
|
|
|
|
|
+ broadcastMessage(createMessage('seo', `"${blog.title}" 已创建并加入 SEO 队列。`))
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 原来的代码:
|
|
|
|
|
+ // const successMsg = `${writeInfo.number} 篇文章已成功生成。`
|
|
|
|
|
+ // broadcastMessage(createMessage('write', successMsg, {}, blogList))
|
|
|
|
|
+ // work.port.postMessage(createMessage('success', '文章生成完成。'))
|
|
|
|
|
+
|
|
|
|
|
+ // 修改为:
|
|
|
|
|
+ const successMsg = `${writeInfo.number} 篇文章已成功生成。`
|
|
|
|
|
+ return {
|
|
|
|
|
+ type: 'write',
|
|
|
|
|
+ message: successMsg,
|
|
|
|
|
+ response: blogList,
|
|
|
|
|
+ port: work.port,
|
|
|
|
|
+ portMessage: createMessage('success', '文章生成完成。')
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 执行一个 'seo' 优化任务。
|
|
|
|
|
+ * @param {object} work - SEO 任务的工作项。
|
|
|
|
|
+ */
|
|
|
|
|
+async function doSeoTask(work) {
|
|
|
|
|
+ const { type, id } = work.data
|
|
|
|
|
+ console.log('调用接口',`get${capitalize(type)}`)
|
|
|
|
|
+ console.log('调用接口',api[`get${capitalize(type)}`])
|
|
|
|
|
+ const pageData = (await apiClient(api[`get${capitalize(type)}`], 'GET', { id })).data
|
|
|
|
|
+ const title = pageData.page_name || pageData.title
|
|
|
|
|
+
|
|
|
|
|
+ // 并行获取相关数据(标签、类型等)
|
|
|
|
|
+ const lists = {}
|
|
|
|
|
+ const promises = []
|
|
|
|
|
+ if (type !== 'staticPage') {
|
|
|
|
|
+ promises.push(apiClient(api[`get${capitalize(type)}Tags`], 'GET', { page: 1, page_size: 1000 }).then(r => {
|
|
|
|
|
+ lists.tags = r.data.data
|
|
|
|
|
+ }))
|
|
|
|
|
+ promises.push(apiClient(api[`get${capitalize(type)}Types`], 'GET', { page: 1, page_size: 1000 }).then(r => { lists.types = r.data.data }))
|
|
|
|
|
+ }
|
|
|
|
|
+ if (type === 'blog') {
|
|
|
|
|
+ promises.push(apiClient(api.getBlogPlates, 'GET', { page: 1, page_size: 1000 }).then(r => { lists.plates = r.data.data }))
|
|
|
|
|
+ }
|
|
|
|
|
+ await Promise.all(promises)
|
|
|
|
|
+
|
|
|
|
|
+ const seoData = state.seoConfig[`${type}Seo`]
|
|
|
|
|
+ if (!seoData) throw new Error(`未找到类型为 ${type} 的 SEO 配置`)
|
|
|
|
|
+
|
|
|
|
|
+ // 构建 AI 提示
|
|
|
|
|
+ const requestJson = {
|
|
|
|
|
+ contents: [{
|
|
|
|
|
+ role: 'model',
|
|
|
|
|
+ parts: [{
|
|
|
|
|
+ text: `
|
|
|
|
|
+ 无论用户使用何种语言提问,返回给用户的数据一律使用${state.settings.language}回答。
|
|
|
|
|
+ 你需要处理用户发送的数据,将数据中的'descData','keywordData','titleData','urlData' 四项根据用户的要求生成字符串后返回给用户。这些数据将被使用在用户的网站元信息中用于SEO优化。
|
|
|
|
|
+ descData的内容参考提示${seoData.descData[0].value}进行生成,
|
|
|
|
|
+ keywordData的内容参考提示${seoData.keywordData[0].value}进行生成,
|
|
|
|
|
+ titleData的内容参考提示${seoData.titleData[0].value}进行生成,
|
|
|
|
|
+ urlData的内容参考提示${seoData.urlData[0].value}进行生成,注意此项需要作为地址参数使用,不能使用特殊字符,只可使用小写字母与'-'。
|
|
|
|
|
+ 需要SEO优化的文章具体内容数据为${JSON.stringify(pageData)};
|
|
|
|
|
+ 类型和标签数据为id数组,其具体内容可在此处取得${JSON.stringify(lists)}。
|
|
|
|
|
+ `
|
|
|
|
|
+ }]
|
|
|
|
|
+ }, {
|
|
|
|
|
+ role: 'user',
|
|
|
|
|
+ parts: [{ text: '生成数据' }]
|
|
|
|
|
+ }],
|
|
|
|
|
+ generationConfig: {
|
|
|
|
|
+ responseMimeType: 'application/json',
|
|
|
|
|
+ responseSchema: {
|
|
|
|
|
+ type: 'object',
|
|
|
|
|
+ properties: {
|
|
|
|
|
+ descData: { type: 'string' },
|
|
|
|
|
+ urlData: { type: 'string' },
|
|
|
|
|
+ keywordData: { type: 'string' },
|
|
|
|
|
+ titleData: { type: 'string' }
|
|
|
|
|
+ },
|
|
|
|
|
+ required: ['descData', 'keywordData', 'titleData', 'urlData']
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 调用 AI 并保存结果
|
|
|
|
|
+ const aiResult = await callAI(requestJson)
|
|
|
|
|
+ pageData.seo_data.seo_describe = aiResult.descData
|
|
|
|
|
+ pageData.seo_data.seo_keyword = aiResult.keywordData
|
|
|
|
|
+ pageData.seo_data.seo_title = aiResult.titleData
|
|
|
|
|
+ pageData.seo_data.urla = aiResult.urlData
|
|
|
|
|
+
|
|
|
|
|
+ const saveResponse = await apiClient(api[`save${capitalize(type)}`], 'POST', pageData)
|
|
|
|
|
+
|
|
|
|
|
+ // 原来的代码:
|
|
|
|
|
+ // const successMsg = `"${title}" 的 SEO 优化已完成。`
|
|
|
|
|
+ // broadcastMessage(createMessage('seo', successMsg, {}, saveResponse))
|
|
|
|
|
+ // work.port.postMessage(createMessage('success', successMsg, {}, saveResponse))
|
|
|
|
|
+
|
|
|
|
|
+ // 修改为:
|
|
|
|
|
+ const successMsg = `"${title}" 的 SEO 优化已完成。`
|
|
|
|
|
+ return {
|
|
|
|
|
+ type: 'seo',
|
|
|
|
|
+ message: successMsg,
|
|
|
|
|
+ response: saveResponse,
|
|
|
|
|
+ port: work.port,
|
|
|
|
|
+ portMessage: createMessage('success', successMsg, {}, saveResponse)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 执行一个 'translate' 翻译任务。
|
|
|
|
|
+ * @param {object} work - 翻译任务的工作项。
|
|
|
|
|
+ */
|
|
|
|
|
+async function doTranslateTask(work) {
|
|
|
|
|
+ const { type, id } = work.data
|
|
|
|
|
+ const pageData = (await apiClient(api[`get${capitalize(type)}`], 'GET', { id })).data
|
|
|
|
|
+ const title = pageData.page_name || pageData.title
|
|
|
|
|
+
|
|
|
|
|
+ // 1. 更新状态为“翻译中”
|
|
|
|
|
+ await apiClient(api.transStatus, 'POST', { id: pageData.seo_id, trans_status: 1 })
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 构建 AI 提示并调用 AI
|
|
|
|
|
+ const requestJson = {
|
|
|
|
|
+ contents: [{
|
|
|
|
|
+ role: 'model',
|
|
|
|
|
+ parts: [{
|
|
|
|
|
+ text: `
|
|
|
|
|
+ 无论用户使用何种语言提问,返回给用户的数据一律使用${state.settings.language}回答。
|
|
|
|
|
+ 将用户提供的JSON数据翻译成${state.settings.language}并以纯json字符串的形式输出,不要输出任何markdown格式,输出纯字符串。
|
|
|
|
|
+ `
|
|
|
|
|
+ }]
|
|
|
|
|
+ }, {
|
|
|
|
|
+ role: 'user',
|
|
|
|
|
+ parts: [{
|
|
|
|
|
+ text: `翻译数据${JSON.stringify(pageData)}`
|
|
|
|
|
+ }]
|
|
|
|
|
+ }],
|
|
|
|
|
+ generationConfig: {
|
|
|
|
|
+ responseMimeType: 'text/plain' // AI 将返回一个字符串化的 JSON
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const translatedText = await callAI(requestJson, true) // rawText = true
|
|
|
|
|
+ const translatedData = JSON.parse(translatedText)
|
|
|
|
|
+ if (translatedData.content === '') {
|
|
|
|
|
+ translatedData.content = translatedData.title
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 保存翻译后的数据
|
|
|
|
|
+ const saveResponse = await apiClient(api[`save${capitalize(type)}`], 'POST', translatedData)
|
|
|
|
|
+
|
|
|
|
|
+ // 4. 更新状态为“已翻译”
|
|
|
|
|
+ await apiClient(api.transStatus, 'POST', { id: pageData.seo_id, trans_status: state.settings.language })
|
|
|
|
|
+
|
|
|
|
|
+ // 原来的代码:
|
|
|
|
|
+ // const successMsg = `"${title}" 翻译成功。`
|
|
|
|
|
+ // broadcastMessage(createMessage('translate', successMsg, {}, saveResponse))
|
|
|
|
|
+ // work.port.postMessage(createMessage('success', successMsg, { refresh: true, type: work.data.type }, work.data))
|
|
|
|
|
+
|
|
|
|
|
+ // 修改为:
|
|
|
|
|
+ const successMsg = `"${title}" 翻译成功。`
|
|
|
|
|
+ return {
|
|
|
|
|
+ type: 'translate',
|
|
|
|
|
+ message: successMsg,
|
|
|
|
|
+ response: saveResponse,
|
|
|
|
|
+ port: work.port,
|
|
|
|
|
+ portMessage: createMessage('success', successMsg, { refresh: true, type: work.data.type }, work.data)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ===================================================================================
|
|
|
|
|
+// 5. API 及 AI 客户端
|
|
|
|
|
+// ===================================================================================
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 一个健壮的、用于应用后端的异步 API 客户端。
|
|
|
|
|
+ * @param {string} url - 请求的完整 URL。
|
|
|
|
|
+ * @param {'GET'|'POST'} method - HTTP 方法。
|
|
|
|
|
+ * @param {object} [data] - 要发送的数据。对于 GET 请求会转换为查询参数,对于 POST 请求会作为 JSON 主体。
|
|
|
|
|
+ * @returns {Promise<any>} - 一个解析为响应数据的 Promise。
|
|
|
|
|
+ * @throws {Error} 如果请求失败或返回非 200 状态码。
|
|
|
|
|
+ */
|
|
|
|
|
+async function apiClient(url, method, data) {
|
|
|
|
|
+ if (!state.token) throw new Error('API 客户端调用失败:认证令牌未设置。')
|
|
|
|
|
+
|
|
|
|
|
+ const options = {
|
|
|
|
|
+ method: method.toUpperCase(),
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ token: state.token,
|
|
|
|
|
+ 'Content-Type': 'application/json',
|
|
|
|
|
+ 'X-Requested-With': 'XMLHttpRequest'
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let fullUrl = url
|
|
|
|
|
+ if (method.toUpperCase() === 'GET' && data) {
|
|
|
|
|
+ fullUrl += '?' + new URLSearchParams(data).toString()
|
|
|
|
|
+ } else if (method.toUpperCase() === 'POST' && data) {
|
|
|
|
|
+ options.body = JSON.stringify(data)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const response = await fetch(fullUrl, options)
|
|
|
|
|
+
|
|
|
|
|
+ if (!response.ok) {
|
|
|
|
|
+ const errorBody = await response.text()
|
|
|
|
|
+ throw new Error(`API 错误: ${response.status} ${response.statusText} on ${url}. Body: ${errorBody}`)
|
|
|
|
|
+ }
|
|
|
|
|
+ // 原始 API 在某些情况下似乎返回 JSON 字符串,因此我们返回原始响应的文本
|
|
|
|
|
+ // 以便进行灵活的解析 (.json() 或 .text())
|
|
|
|
|
+ return response.json()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 使用给定的负载调用 AI 服务。
|
|
|
|
|
+ * @param {object} payload - 发送给 AI 请求的 JSON 负载。
|
|
|
|
|
+ * @param {boolean} [expectRawText=false] - 如果为 true,则返回原始文本响应。如果为 false,则解析内部的 JSON。
|
|
|
|
|
+ * @returns {Promise<any>} - 一个解析为 AI 响应数据的 Promise。
|
|
|
|
|
+ * @throws {Error} 如果 AI 调用失败或响应格式错误。
|
|
|
|
|
+ */
|
|
|
|
|
+async function callAI(payload, expectRawText = false) {
|
|
|
|
|
+ const response = await fetch(state.aiUrl, {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
+ body: JSON.stringify(payload)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ if (!response.ok) {
|
|
|
|
|
+ const errorBody = await response.text()
|
|
|
|
|
+ throw new Error(`AI API 错误: ${response.status} ${response.statusText}. Body: ${errorBody}`)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const responseData = await response.json()
|
|
|
|
|
+ const textPart = responseData?.candidates?.[0]?.content?.parts?.[0]?.text
|
|
|
|
|
+ if (typeof textPart !== 'string') {
|
|
|
|
|
+ throw new Error('AI 响应格式无效:缺少文本部分或非字符串类型。')
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (expectRawText) {
|
|
|
|
|
+ return textPart
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // AI 有时会将其 JSON 输出包裹在 markdown 的 ```json ... ``` 中,因此我们需要清理它。
|
|
|
|
|
+ const cleanedText = textPart.replace(/^```json\s*|```\s*$/g, '')
|
|
|
|
|
+ return JSON.parse(cleanedText)
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.error('从 AI 响应中解析内部 JSON 失败:', textPart)
|
|
|
|
|
+ throw new Error('AI 返回了格式错误的 JSON 字符串。')
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ===================================================================================
|
|
|
|
|
+// 6. 工具函数
|
|
|
|
|
+// ===================================================================================
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 创建一个标准的消息对象,用于通过端口发送。
|
|
|
|
|
+ * @param {string} type - 消息类型。
|
|
|
|
|
+ * @param {string} message - 主要的消息文本。
|
|
|
|
|
+ * @param {object} data - 任何要发送的附加数据。
|
|
|
|
|
+ * @param {any} [debugCode] - 调试信息。
|
|
|
|
|
+ * @returns {object} 结构化的消息对象。
|
|
|
|
|
+ */
|
|
|
|
|
+function createMessage(type, message, data = {}, debugCode = {}) {
|
|
|
|
|
+ return {
|
|
|
|
|
+ type: state.settings.debug ? 'code' : type,
|
|
|
|
|
+ message: message,
|
|
|
|
|
+ time: Date.now(),
|
|
|
|
|
+ code: state.settings.debug ? debugCode : '',
|
|
|
|
|
+ data: {
|
|
|
|
|
+ ...data,
|
|
|
|
|
+ workLength: state.workList.length,
|
|
|
|
|
+ isInit: !!state.mainPort
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 如果主端口和助手端口存在,则向它们广播消息。
|
|
|
|
|
+ * @param {object} messageObject - 由 createMessage 创建的消息对象。
|
|
|
|
|
+ */
|
|
|
|
|
+function broadcastMessage(messageObject) {
|
|
|
|
|
+ if (state.mainPort) {
|
|
|
|
|
+ state.mainPort.postMessage(messageObject)
|
|
|
|
|
+ }
|
|
|
|
|
+ if (state.helperPort) {
|
|
|
|
|
+ // 向助手页面发送一个简化的成功/失败消息
|
|
|
|
|
+ const helperMsgType = (messageObject.type === 'error' || messageObject.type === 'warning') ? messageObject.type : 'success'
|
|
|
|
|
+ state.helperPort.postMessage(createMessage(helperMsgType, messageObject.message, {}, messageObject.code))
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 将字符串的第一个字母大写。
|
|
|
|
|
+ * 例如:'blog' -> 'Blog'
|
|
|
|
|
+ * @param {string} s - 要大写的字符串。
|
|
|
|
|
+ * @returns {string}
|
|
|
|
|
+ */
|
|
|
|
|
+function capitalize(s) {
|
|
|
|
|
+ if (typeof s !== 'string' || !s) return s
|
|
|
|
|
+ return s.charAt(0).toUpperCase() + s.slice(1)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 从模板生成封面图片 URL。
|
|
|
|
|
+ * 注意:此函数存在于原始文件中但未使用。
|
|
|
|
|
+ * 它依赖于一个此处未定义的 'md5' 函数。
|
|
|
|
|
+ * @param {string} title - 要嵌入图片中的标题。
|
|
|
|
|
+ * @param {string} image - 背景图片 URL。
|
|
|
|
|
+ * @returns {string} - 最终的模板化图片 URL。
|
|
|
|
|
+ */
|
|
|
|
|
+function getTemplateImage(title, image) {
|
|
|
|
|
+ // 此函数需要一个全局可用的 'md5' 函数。
|
|
|
|
|
+ if (typeof md5 !== 'function') {
|
|
|
|
|
+ console.error('getTemplateImage 需要一个 md5() 函数,但该函数未定义。')
|
|
|
|
|
+ return ''
|
|
|
|
|
+ }
|
|
|
|
|
+ const urlPrefix = 'https://image.edgeone.app'
|
|
|
|
|
+ const format = 'png'
|
|
|
|
|
+ const userId = '3e5b976c108b4febb15687047013beff'
|
|
|
|
|
+ const templateId = 'ep-uchZw5e0Lxe9'
|
|
|
|
|
+ const apiKey = 'rC5asgUt51is'
|
|
|
|
|
+ const params = { image, title }
|
|
|
|
|
+
|
|
|
|
|
+ const sortedKeys = Object.keys(params).sort()
|
|
|
|
|
+ const searchParams = sortedKeys.map(key => `${key}=${params[key]}`).join('&')
|
|
|
|
|
+ const signData = JSON.stringify({ apiKey, searchParams })
|
|
|
|
|
+ const sign = md5(signData)
|
|
|
|
|
+ const encodedSearchParams = sortedKeys.map(key => `${key}=${encodeURIComponent(params[key])}`).join('&')
|
|
|
|
|
+
|
|
|
|
|
+ return `${urlPrefix}/${sign}/${userId}/${templateId}.${format}?${encodedSearchParams}`
|
|
|
|
|
+}
|