From a9bc4bc102a77c2e008cd4297ac6bc2ba69f3409 Mon Sep 17 00:00:00 2001 From: wizardchen Date: Fri, 13 Mar 2026 16:43:56 +0800 Subject: [PATCH] feat: enhance Quick Notes 5.0 with API integration and UI improvements - Implemented API communication for WeKnora, allowing users to authenticate and interact with knowledge bases. - Added functionality for image uploads and previews in chat, enhancing multimodal interaction capabilities. - Refactored the popup and sidepanel interfaces to improve user experience and accessibility, including new toolbar elements for mode selection. - Introduced session management features, enabling users to manage chat sessions and knowledge base selections more effectively. - Enhanced styling and layout for better responsiveness and visual appeal across various components. These changes significantly improve the functionality and user experience of the Quick Notes extension, providing a more robust platform for managing web content and interactions. --- .../快速笔记 5.0/background.js | 346 +++- .../chrome-extension/快速笔记 5.0/popup.html | 239 ++- .../chrome-extension/快速笔记 5.0/popup.js | 787 +++++++-- .../快速笔记 5.0/sidepanel.html | 295 +++- .../快速笔记 5.0/sidepanel.js | 1429 ++++++++++++++++- .../快速笔记 5.0/weknora.html | 128 +- .../chrome-extension/快速笔记 5.0/weknora.js | 1187 +++++++++++++- 7 files changed, 4145 insertions(+), 266 deletions(-) diff --git a/frontend/chrome-extension/快速笔记 5.0/background.js b/frontend/chrome-extension/快速笔记 5.0/background.js index d0631c68..6ca60774 100644 --- a/frontend/chrome-extension/快速笔记 5.0/background.js +++ b/frontend/chrome-extension/快速笔记 5.0/background.js @@ -1,5 +1,94 @@ // background.js — Service Worker -// 存储管理 + 消息路由 + 右键菜单 +// 存储管理 + 消息路由 + 右键菜单 + API 通信 + +// === WeKnora API Helper === +// 构建请求头:API Key 使用 X-API-Key header,Bearer token 使用 Authorization header +async function buildHeaders(config) { + var headers = { + 'Content-Type': 'application/json', + 'X-Request-ID': Date.now().toString(36) + Math.random().toString(36).slice(2, 8) + }; + if (config.apiKey) { + // API Key (sk- 开头) 使用 X-API-Key header 进行租户级认证 + headers['X-API-Key'] = config.apiKey; + } + if (config.bearerToken) { + // Bearer token (通过用户名密码登录获取) 使用 Authorization header + headers['Authorization'] = 'Bearer ' + config.bearerToken; + } + return headers; +} + +async function apiRequest(method, path, body, options) { + var config = await getConfigData(); + if (!config || !config.baseUrl) { + return { success: false, error: '未配置服务地址,请先在设置中配置' }; + } + var baseUrl = config.baseUrl.replace(/\/+$/, ''); + var url = baseUrl + path; + var headers = await buildHeaders(config); + try { + var fetchOpts = { method: method, headers: headers }; + if (body && method !== 'GET') { + fetchOpts.body = JSON.stringify(body); + } + if (options && options.signal) { + fetchOpts.signal = options.signal; + } + var resp = await fetch(url, fetchOpts); + if (!resp.ok) { + var errText = ''; + try { var errJson = await resp.json(); errText = errJson.error?.message || errJson.message || resp.statusText; } catch (e) { errText = resp.statusText; } + return { success: false, error: errText, status: resp.status }; + } + var data = await resp.json(); + return data; + } catch (err) { + return { success: false, error: err.message || '网络请求失败' }; + } +} + +// SSE streaming chat request — returns ReadableStream +async function apiChatStream(path, body) { + var config = await getConfigData(); + if (!config || !config.baseUrl) { + return { success: false, error: '未配置服务地址' }; + } + var baseUrl = config.baseUrl.replace(/\/+$/, ''); + var url = baseUrl + path; + var headers = await buildHeaders(config); + headers['Accept'] = 'text/event-stream'; + try { + var resp = await fetch(url, { + method: 'POST', + headers: headers, + body: JSON.stringify(body), + cache: 'no-store' // 避免浏览器缓存导致 SSE 流被缓冲 + }); + if (!resp.ok) { + var errText = ''; + try { var errJson = await resp.json(); errText = errJson.error?.message || errJson.message || resp.statusText; } catch (e) { errText = resp.statusText; } + return { success: false, error: errText }; + } + return { success: true, response: resp }; + } catch (err) { + return { success: false, error: err.message || '网络请求失败' }; + } +} + +// 流式推送消息到 sidepanel / popup 等前端页面 +function notifyStream(msg) { + chrome.runtime.sendMessage(msg).catch(function () {}); +} + +// Helper to get raw config data +async function getConfigData() { + var data = await chrome.storage.local.get('ka_config'); + return data.ka_config || null; +} + +// Session cache per tab for sidepanel chat +var chatSessions = {}; // === 右键菜单 === // 防止并发注册 @@ -58,9 +147,9 @@ async function updateContextMenuTitle() { } else if (auth && auth.type === 'ka') { askTitle = '使用知识助理提问'; } - try { - chrome.contextMenus.update('ka-ask-selection', { title: askTitle }); - } catch (e) { /* 菜单还未创建时忽略 */ } + chrome.contextMenus.update('ka-ask-selection', { title: askTitle }, function () { + void chrome.runtime.lastError; // 菜单还未创建时静默忽略 + }); } chrome.contextMenus.onClicked.addListener(function (info, tab) { @@ -182,6 +271,255 @@ async function handleMessage(msg, sender) { } catch (err) { return { success: false, error: err.message || '截图失败' }; } + + // === WeKnora API 相关 === + case 'VALIDATE_CONFIG': + // API Key 没有专门的验证端点,通过调用知识库列表接口来验证连通性和认证 + return apiRequest('GET', '/knowledge-bases'); + + case 'LOGIN': { + // 用户名密码登录,获取 Bearer token + var loginPayload = msg.payload || {}; + var loginResp = await apiRequest('POST', '/auth/login', { + email: loginPayload.email, + password: loginPayload.password + }); + // 如果登录成功,将 token 存入 config + if (loginResp && loginResp.success && loginResp.token) { + var curConfig = await getConfigData() || {}; + curConfig.bearerToken = loginResp.token; + curConfig.refreshToken = loginResp.refresh_token; + await chrome.storage.local.set({ ka_config: curConfig }); + } + return loginResp; + } + + case 'LIST_KNOWLEDGE_BASES': + // agent_id 参数仅用于共享智能体(跨租户),本地/内置智能体不传 + var agentFilter = (msg.payload && msg.payload.sharedAgentId) ? '?agent_id=' + msg.payload.sharedAgentId : ''; + return apiRequest('GET', '/knowledge-bases' + agentFilter); + + case 'LIST_AGENTS': + return apiRequest('GET', '/agents'); + + case 'CREATE_KNOWLEDGE_BASE': + return apiRequest('POST', '/knowledge-bases', msg.payload); + + case 'CREATE_SESSION': + return apiRequest('POST', '/sessions', msg.payload || {}); + + case 'LIST_SESSIONS': + var p = msg.payload || {}; + return apiRequest('GET', '/sessions?page=' + (p.page || 1) + '&page_size=' + (p.page_size || 20)); + + case 'CLEAR_SESSION_MESSAGES': { + var sid = (msg.payload || {}).sessionId; + if (!sid) return { success: false, error: '缺少 sessionId' }; + return apiRequest('DELETE', '/sessions/' + sid + '/messages'); + } + + case 'CHAT_QUERY': { + // 真正的知识库问答 — 使用 SSE 流式输出 + var payload = msg.payload || {}; + var query = payload.query; + if (!query) return { success: false, error: '请输入问题' }; + + // 获取或创建会话 + var sessionId = payload.sessionId; + if (!sessionId) { + var sessionResp = await apiRequest('POST', '/sessions', {}); + if (sessionResp && sessionResp.success && sessionResp.data) { + sessionId = sessionResp.data.id; + } else if (sessionResp && sessionResp.id) { + sessionId = sessionResp.id; + } + if (!sessionId) { + return { success: false, error: '创建会话失败: ' + (sessionResp.error || '未知错误') }; + } + await chrome.storage.local.set({ ka_current_session: sessionId }); + } + + // 确定使用知识库问答还是智能体问答 + var kbIds = payload.knowledgeBaseIds || []; + var agentId = payload.agentId || ''; + var useAgent = payload.agentEnabled || false; + var chatPath = useAgent + ? '/agent-chat/' + sessionId + : '/knowledge-chat/' + sessionId; + + // 构建完整请求体,参考 CreateKnowledgeQARequest + var chatBody = { query: query }; + if (kbIds.length > 0) { + chatBody.knowledge_base_ids = kbIds; + } + if (agentId) { + chatBody.agent_id = agentId; + } + if (useAgent) { + chatBody.agent_enabled = true; + } + if (payload.webSearchEnabled) { + chatBody.web_search_enabled = true; + } + if (payload.mentionedItems) { + chatBody.mentioned_items = payload.mentionedItems; + } + if (payload.images && payload.images.length > 0) { + chatBody.images = payload.images; + } + + // 使用请求 ID 区分不同来源的流式推送 + var chatRequestId = payload._requestId || (Date.now().toString(36) + Math.random().toString(36).slice(2, 6)); + + // SSE 流式请求 + var streamResult = await apiChatStream(chatPath, chatBody); + if (!streamResult.success) { + return { success: false, error: streamResult.error }; + } + + // 读取 SSE 流,逐块推送到前端 + try { + var reader = streamResult.response.body.getReader(); + var decoder = new TextDecoder(); + var fullText = ''; + var buffer = ''; + + while (true) { + var readResult = await reader.read(); + if (readResult.done) break; + buffer += decoder.decode(readResult.value, { stream: true }); + + // SSE 格式: "event: message\ndata: {json}\n\n" + // 按双换行分割完整事件块 + var eventBlocks = buffer.split('\n\n'); + buffer = eventBlocks.pop() || ''; + + for (var bi = 0; bi < eventBlocks.length; bi++) { + var block = eventBlocks[bi].trim(); + if (!block) continue; + + // 从事件块中提取 data 行 + var dataLine = ''; + var blockLines = block.split('\n'); + for (var li = 0; li < blockLines.length; li++) { + var bline = blockLines[li]; + if (bline.startsWith('data:')) { + dataLine = bline.substring(5).trim(); + } + } + if (!dataLine || dataLine === '[DONE]') continue; + + try { + var evt = JSON.parse(dataLine); + var responseType = evt.response_type || ''; + + // 根据 response_type 处理不同事件 + if (responseType === 'answer') { + var chunk = evt.content || ''; + if (chunk) { + fullText += chunk; + notifyStream({ + type: 'CHAT_STREAM_CHUNK', + payload: { requestId: chatRequestId, sessionId: sessionId, responseType: 'answer', content: chunk, done: !!evt.done } + }); + } + } else if (responseType === 'thinking') { + notifyStream({ + type: 'CHAT_STREAM_CHUNK', + payload: { requestId: chatRequestId, sessionId: sessionId, responseType: 'thinking', content: evt.content || '', eventId: evt.data && evt.data.event_id } + }); + } else if (responseType === 'tool_call') { + notifyStream({ + type: 'CHAT_STREAM_CHUNK', + payload: { requestId: chatRequestId, sessionId: sessionId, responseType: 'tool_call', content: evt.content || '', toolName: evt.data && evt.data.tool_name, eventId: evt.data && (evt.data.event_id || evt.data.tool_call_id) } + }); + } else if (responseType === 'tool_result') { + notifyStream({ + type: 'CHAT_STREAM_CHUNK', + payload: { requestId: chatRequestId, sessionId: sessionId, responseType: 'tool_result', content: evt.content || '', toolName: evt.data && evt.data.tool_name, eventId: evt.data && (evt.data.event_id || evt.data.tool_call_id), success: evt.data && evt.data.success } + }); + } else if (responseType === 'references') { + var kRefs = evt.knowledge_references; + if (Array.isArray(kRefs) && kRefs.length > 0) { + notifyStream({ + type: 'CHAT_STREAM_CHUNK', + payload: { requestId: chatRequestId, sessionId: sessionId, responseType: 'references', references: kRefs } + }); + } + } else if (responseType === 'error') { + notifyStream({ + type: 'CHAT_STREAM_CHUNK', + payload: { requestId: chatRequestId, sessionId: sessionId, responseType: 'error', content: evt.content || '请求出错' } + }); + } else if (responseType === 'complete') { + notifyStream({ + type: 'CHAT_STREAM_CHUNK', + payload: { requestId: chatRequestId, sessionId: sessionId, responseType: 'complete', done: true } + }); + } + if (responseType === 'session_title') { + notifyStream({ + type: 'CHAT_STREAM_CHUNK', + payload: { requestId: chatRequestId, sessionId: sessionId, responseType: 'session_title', content: evt.content || '' } + }); + } + // agent_query 等事件静默忽略 + } catch (e) { + // 非 JSON data 行,忽略 + } + } + } + + return { success: true, data: fullText || '未获取到回复内容', sessionId: sessionId, requestId: chatRequestId }; + } catch (streamErr) { + return { success: false, error: '读取回复流失败: ' + streamErr.message }; + } + } + + case 'SAVE_CLIP_TO_KB': { + // 保存剪藏内容到知识库(作为手动知识条目) + var pl = msg.payload || {}; + if (!pl.kbId || !pl.content) return { success: false, error: '缺少知识库 ID 或内容' }; + var contentWithMeta = pl.content; + if (pl.url) { + contentWithMeta = '> 来源: ' + pl.url + '\n\n' + pl.content; + } + return apiRequest('POST', '/knowledge-bases/' + pl.kbId + '/knowledge/manual', { + title: pl.title || '快速笔记剪藏', + content: contentWithMeta, + status: 'publish' + }); + } + + case 'FETCH_FILE': { + // 通过 background 代理带认证头请求文件(图片等),返回 data URL + var filePath = (msg.payload || {}).filePath; + if (!filePath) return { success: false, error: '缺少 filePath' }; + var cfg = await getConfigData(); + if (!cfg || !cfg.baseUrl) return { success: false, error: '未配置服务地址' }; + var fileBaseUrl = cfg.baseUrl.replace(/\/+$/, '').replace(/\/api\/v\d+$/, ''); + var fileUrl = fileBaseUrl + '/files?file_path=' + encodeURIComponent(filePath); + var fileHeaders = await buildHeaders(cfg); + try { + var fileResp = await fetch(fileUrl, { method: 'GET', headers: fileHeaders }); + if (!fileResp.ok) return { success: false, error: 'HTTP ' + fileResp.status }; + var blob = await fileResp.blob(); + // 转为 data URL 以便跨上下文传递 + var reader2 = new FileReader(); + var dataUrl = await new Promise(function (resolve, reject) { + reader2.onload = function () { resolve(reader2.result); }; + reader2.onerror = function () { reject(new Error('FileReader error')); }; + reader2.readAsDataURL(blob); + }); + return { success: true, dataUrl: dataUrl }; + } catch (err) { + return { success: false, error: err.message || '文件请求失败' }; + } + } + + case 'GET_USER_INFO': + return apiRequest('GET', '/auth/me'); + default: return { success: false, error: '未知消息类型' }; } diff --git a/frontend/chrome-extension/快速笔记 5.0/popup.html b/frontend/chrome-extension/快速笔记 5.0/popup.html index bb5f2425..9d38898c 100644 --- a/frontend/chrome-extension/快速笔记 5.0/popup.html +++ b/frontend/chrome-extension/快速笔记 5.0/popup.html @@ -385,22 +385,35 @@ body { width: 380px; max-height: 680px; font-family: -apple-system, "PingFang SC .note-save svg { width: 14px; height: 14px; } /* ===== 知识问答 — 单行输入栏 ===== */ -.chat-bar { - display: flex; - align-items: center; +/* 底部输入区域整体包裹 */ +.chat-bar-wrapper { background: var(--white); - border-radius: 50px; + border-radius: 16px; box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06), 0 0 1px rgba(0, 0, 0, 0.04); - padding: 5px 5px 5px 6px; - gap: 0; + margin-top: 4px; + overflow: visible; position: relative; transition: box-shadow 0.2s; - margin-top: 4px; } -.chat-bar:focus-within { +.chat-bar-wrapper:focus-within { box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08), 0 0 0 1.5px rgba(7, 193, 96, 0.15); } +/* 顶部工具栏:知识库 + 模式选择 */ +.chat-bar-toolbar { + display: flex; + align-items: center; + gap: 4px; + padding: 6px 8px 0; +} + +.chat-bar { + display: flex; + align-items: center; + padding: 4px 5px 5px 8px; + gap: 4px; + position: relative; +} /* 知识库选择按钮 */ .chat-bar-kb { display: flex; @@ -422,6 +435,21 @@ body { width: 380px; max-height: 680px; font-family: -apple-system, "PingFang SC .chat-bar-kb svg { width: 13px; height: 13px; flex-shrink: 0; } .chat-bar-arrow { width: 10px !important; height: 10px !important; opacity: 0.5; } +/* 模式选择按钮 */ +.chat-bar-mode { + display: flex; align-items: center; gap: 3px; + border: none; background: #f3f4f5; border-radius: 50px; + padding: 6px 8px 6px 10px; font-size: 11px; font-weight: 500; + color: var(--text2); cursor: pointer; white-space: nowrap; + transition: all 0.15s; flex-shrink: 0; +} +.chat-bar-mode:hover { background: #eaebed; color: var(--text); } +.chat-bar-mode svg { width: 13px; height: 13px; flex-shrink: 0; } + +/* 模式下拉菜单 */ +.popup-mode-menu { position: fixed; background: var(--white); border-radius: 12px; box-shadow: 0 8px 32px rgba(0,0,0,0.12), 0 0 1px rgba(0,0,0,0.06); padding: 6px 0; min-width: 160px; display: none; z-index: 100; } +.popup-mode-menu.show { display: block; } + /* 分隔竖线 */ .chat-bar-divider { width: 1px; height: 16px; background: #e5e5e5; margin: 0 8px; flex-shrink: 0; } @@ -434,9 +462,13 @@ body { width: 380px; max-height: 680px; font-family: -apple-system, "PingFang SC font-family: inherit; outline: none; color: var(--text); - padding: 0; + padding: 6px 4px; min-width: 0; height: 30px; + max-height: 100px; + resize: none; + overflow-y: auto; + line-height: 18px; } .chat-bar-input::placeholder { color: #c0c0c0; } @@ -491,28 +523,79 @@ body { width: 380px; max-height: 680px; font-family: -apple-system, "PingFang SC .kb-mode-item svg { width: 14px; height: 14px; } /* 附件选项 */ -.kb-attach-item { padding: 8px 14px; font-size: 13px; color: var(--text2); cursor: pointer; display: flex; align-items: center; gap: 8px; transition: all 0.15s; } -.kb-attach-item:hover { background: rgba(7,193,96,0.06); color: var(--primary); } -.kb-attach-item svg { width: 14px; height: 14px; } +/* 图片上传按钮 */ +.popup-image-btn { + display: flex; align-items: center; justify-content: center; + border: none; background: none; color: #999; cursor: pointer; + width: 28px; height: 28px; padding: 0; flex-shrink: 0; border-radius: 6px; transition: all 0.15s; +} +.popup-image-btn:hover { color: var(--primary); background: rgba(7,193,96,0.06); } +.popup-image-btn svg { width: 16px; height: 16px; } + +/* 图片预览条 */ +.popup-image-previews { + display: flex; gap: 6px; padding: 4px 12px; overflow-x: auto; flex-shrink: 0; +} +.popup-img-item { position: relative; flex-shrink: 0; } +.popup-img-thumb { width: 40px; height: 40px; border-radius: 6px; object-fit: cover; border: 1px solid #e8e8e8; } +.popup-img-remove { + position: absolute; top: -4px; right: -4px; + width: 14px; height: 14px; border-radius: 50%; background: rgba(0,0,0,0.5); color: #fff; + font-size: 10px; line-height: 14px; text-align: center; cursor: pointer; border: none; padding: 0; +} +.popup-img-remove:hover { background: #e44; } /* Settings Overlay */ .settings-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.3); display: none; z-index: 100; backdrop-filter: blur(2px); } .settings-overlay.show { display: flex; align-items: center; justify-content: center; } .settings-panel { background: var(--white); border-radius: 14px; padding: 18px 20px; width: 340px; box-shadow: 0 8px 32px rgba(0,0,0,0.15); } .settings-handle { display: none; } -.settings-title { font-size: 15px; font-weight: 700; margin-bottom: 12px; color: var(--text); } +.settings-title { font-size: 15px; font-weight: 700; margin-bottom: 12px; color: var(--text); display: flex; align-items: center; justify-content: space-between; } +.settings-close { border: none; background: none; color: var(--text3); cursor: pointer; width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; border-radius: 8px; } +.settings-close:hover { background: #f0f0f0; color: var(--text); } +.settings-close svg { width: 16px; height: 16px; } .settings-item { display: flex; justify-content: space-between; align-items: center; padding: 10px 0; border-bottom: 1px solid #f2f2f2; } .settings-item:last-child { border-bottom: none; } .settings-label { font-size: 13px; color: var(--text3); } .settings-value { font-size: 13px; color: var(--text); font-weight: 500; max-width: 200px; text-align: right; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .settings-logout { width: 100%; height: 40px; border: none; border-radius: 10px; background: #fff0f0; color: #e44; font-size: 13px; font-weight: 500; cursor: pointer; margin-top: 12px; transition: all 0.2s; } .settings-logout:hover { background: #ffe5e5; } +.settings-item-clickable { cursor: pointer; border-radius: 6px; margin: 0 -6px; padding: 10px 6px !important; transition: background 0.15s; } +.settings-item-clickable:hover { background: rgba(7,193,96,0.06); } +.settings-item-clickable .settings-value { color: var(--primary); } +.clip-kb-card { padding: 8px 10px; border: 1px solid #eee; border-radius: 8px; margin-bottom: 6px; cursor: pointer; transition: all 0.15s; } +.clip-kb-card:hover { border-color: var(--primary); background: rgba(7,193,96,0.03); } +.clip-kb-card.selected { border-color: var(--primary); background: rgba(7,193,96,0.06); } +.clip-kb-card-name { font-size: 12px; font-weight: 600; color: var(--text); margin-bottom: 1px; } +.clip-kb-card-desc { font-size: 10px; color: var(--text3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.clip-kb-card-meta { font-size: 10px; color: #ccc; margin-top: 2px; } /* Toast */ .toast { position: fixed; top: 20px; left: 50%; transform: translateX(-50%) translateY(-80px); background: var(--text); color: #fff; padding: 10px 22px; border-radius: 22px; font-size: 13px; z-index: 200; transition: transform 0.3s ease; pointer-events: none; box-shadow: 0 4px 12px rgba(0,0,0,0.15); } .toast.show { transform: translateX(-50%) translateY(0); } .toast.success { background: var(--primary); } .toast.error { background: #e44; } + +/* === Markdown 渲染样式 === */ +.md-pre { background: #1e1e2e; color: #cdd6f4; border-radius: 8px; padding: 10px 12px; overflow-x: auto; font-size: 12px; margin: 6px 0; line-height: 1.5; } +.md-pre code { font-family: "SF Mono", "Fira Code", "Consolas", monospace; white-space: pre; } +.md-code { background: #f3f4f6; padding: 1px 5px; border-radius: 4px; font-size: 12px; font-family: "SF Mono", "Fira Code", "Consolas", monospace; } +.md-h { margin: 8px 0 4px; line-height: 1.4; } +h1.md-h { font-size: 18px; font-weight: 700; } +h2.md-h { font-size: 16px; font-weight: 700; } +h3.md-h { font-size: 14px; font-weight: 600; } +h4.md-h, h5.md-h, h6.md-h { font-size: 13px; font-weight: 600; } +.md-bq { border-left: 3px solid var(--primary); padding: 4px 12px; margin: 6px 0; background: #f8faf8; color: #666; border-radius: 0 4px 4px 0; font-size: 13px; } +.md-list { margin: 4px 0; padding-left: 20px; font-size: 13px; line-height: 1.6; } +.md-list li { margin: 2px 0; } +.md-list .md-list { margin: 2px 0 2px 0; } +.md-p { margin: 4px 0; line-height: 1.6; font-size: 13px; } +.md-p a, .md-list a, .md-bq a { color: var(--primary); text-decoration: none; } +.md-p a:hover, .md-list a:hover { text-decoration: underline; } +.md-table { border-collapse: collapse; margin: 6px 0; font-size: 12px; width: 100%; } +.md-table th { border: 1px solid #e5e5e5; padding: 4px 8px; background: #f7f8fa; font-weight: 600; text-align: left; } +.md-table td { border: 1px solid #e5e5e5; padding: 4px 8px; } +.md-table tr:hover td { background: rgba(7,193,96,0.02); } @@ -634,14 +717,14 @@ body { width: 380px; max-height: 680px; font-family: -apple-system, "PingFang SC
- +
-
-
- +
+
+ + + 全部知识库 +
+
+ + + 快速笔记 +
+
+ + + 网页收藏 +
+
+ + + 产品文档 +
+
+ + + 技术规范 +
+
+
+ -
-
- - - 全部知识库 -
-
- - - 快速笔记 -
-
- - - 网页收藏 -
-
- - - 产品文档 -
-
- - - 技术规范 -
-
-
模式
+
-
- - +
+ + + +
+ @@ -826,7 +917,12 @@ body { width: 380px; max-height: 680px; font-family: -apple-system, "PingFang SC
-
设置
+
+ 设置 + +
账号类型 @@ -844,6 +940,10 @@ body { width: 380px; max-height: 680px; font-family: -apple-system, "PingFang SC API Key -
+
+ 剪藏知识库 + 未选择 › +
版本 v5.0.0 @@ -853,6 +953,23 @@ body { width: 380px; max-height: 680px; font-family: -apple-system, "PingFang SC
+ +
+
+
+ 选择剪藏知识库 + +
+
剪藏的网页内容将存储到此知识库
+
+ +
+
+
diff --git a/frontend/chrome-extension/快速笔记 5.0/popup.js b/frontend/chrome-extension/快速笔记 5.0/popup.js index f1116bda..58431a2d 100644 --- a/frontend/chrome-extension/快速笔记 5.0/popup.js +++ b/frontend/chrome-extension/快速笔记 5.0/popup.js @@ -3,6 +3,8 @@ // === State === var currentUser = null; // { type: 'ka'|'wk', name, avatar, badge } + var clipKbId = ''; // 剪藏目标知识库 ID + var clipKbName = ''; // 剪藏目标知识库名称 // === DOM Helpers === function $(id) { return document.getElementById(id); } @@ -167,8 +169,14 @@ msg.textContent = '正在验证…'; msg.className = 'ka-test-msg'; - setTimeout(function () { - sendMsg({ type: 'SET_CONFIG', payload: { baseUrl: url, apiKey: key } }).then(function () { + // 先保存配置 + sendMsg({ type: 'SET_CONFIG', payload: { baseUrl: url, apiKey: key } }).then(function () { + // 通过调用知识库列表来验证 API Key 和服务地址是否有效 + return sendMsg({ type: 'VALIDATE_CONFIG' }); + }).then(function (resp) { + // VALIDATE_CONFIG 现在实际调用 GET /knowledge-bases,成功说明连通且认证有效 + if (resp && resp.success) { + // API Key 认证成功,不需要调用 /auth/me(那个需要 Bearer token) sendMsg({ type: 'SET_AUTH', payload: { type: 'ka', name: '快速笔记用户', avatar: kaAvatarUrl } }); msg.textContent = '验证通过'; msg.className = 'ka-test-msg ok'; @@ -176,8 +184,11 @@ enterMain('ka', '快速笔记用户', kaAvatarUrl, '快速笔记 5.0'); toast('登录成功', 'success'); }, 500); - }); - }, 800); + } else { + msg.textContent = '验证未通过,请检查服务地址和 API Key'; + msg.className = 'ka-test-msg err'; + } + }); }); // WeKnora 配置页 @@ -215,27 +226,58 @@ $('wk-test-msg').className = 'wk-test-result'; // 保存配置 sendMsg({ type: 'SET_CONFIG', payload: { baseUrl: url, apiKey: key } }).then(function () { - sendMsg({ type: 'SET_AUTH', payload: { type: 'wk', name: 'WeKnora 用户', avatar: '' } }); - $('wk-test-msg').textContent = '配置已保存'; - $('wk-test-msg').className = 'wk-test-result ok'; - setTimeout(function () { - enterMain('wk', 'WeKnora 用户', '', 'WeKnora'); - }, 500); + // 验证连接 + return sendMsg({ type: 'VALIDATE_CONFIG' }); + }).then(function (resp) { + if (resp && resp.success) { + // API Key 验证成功 + sendMsg({ type: 'SET_AUTH', payload: { type: 'wk', name: 'WeKnora 用户', avatar: '' } }); + $('wk-test-msg').textContent = '验证通过,配置已保存'; + $('wk-test-msg').className = 'wk-test-result ok'; + setTimeout(function () { + enterMain('wk', 'WeKnora 用户', '', 'WeKnora'); + }, 500); + } else { + $('wk-test-msg').textContent = '验证未通过,请检查服务地址和 API Key'; + $('wk-test-msg').className = 'wk-test-result err'; + } }); }); + // === 更新用户信息显示 === + function updateUserDisplay(type, name, avatarUrl, badge) { + if (avatarUrl) { + $('main-avatar-img').src = avatarUrl; + $('main-avatar-img').style.display = 'block'; + $('main-letter').style.display = 'none'; + } else { + $('main-avatar-img').style.display = 'none'; + $('main-letter').style.display = 'flex'; + $('main-letter').textContent = (name || 'U').charAt(0).toUpperCase(); + $('main-letter').style.background = '#07C160'; + } + $('main-name').textContent = name; + $('main-badge').textContent = badge; + $('main-badge').className = 'main-badge ' + (type === 'ka' ? 'badge-ka' : 'badge-wk'); + } + // === 进入主界面 === function enterMain(type, name, avatarUrl, badge) { currentUser = { type: type, name: name, avatar: avatarUrl, badge: badge }; - // 统一使用字母头像:圆形绿色底 + 字母 S - $('main-avatar-img').style.display = 'none'; - $('main-letter').style.display = 'flex'; - $('main-letter').textContent = 'S'; - $('main-letter').style.background = '#07C160'; - // 设置名称和标签 - $('main-name').textContent = name; - $('main-badge').textContent = badge; - $('main-badge').className = 'main-badge ' + (type === 'ka' ? 'badge-ka' : 'badge-wk'); + updateUserDisplay(type, name, avatarUrl, badge); + // 从 API 获取真实用户信息 + sendMsg({ type: 'GET_USER_INFO' }).then(function (resp) { + if (resp && resp.success && resp.data) { + var user = resp.data.user || resp.data; + var realName = user.username || user.name || name; + var realAvatar = user.avatar || avatarUrl; + currentUser.name = realName; + currentUser.avatar = realAvatar; + // 同步更新 auth 存储 + sendMsg({ type: 'SET_AUTH', payload: { type: type, name: realName, avatar: realAvatar } }); + updateUserDisplay(type, realName, realAvatar, badge); + } + }); // 设置面板数据 $('set-type').textContent = badge; $('set-name').textContent = name; @@ -264,6 +306,166 @@ } goPage('pg-main'); loadLatestClip(); + loadAgentsForDropdown(); + } + + // === 从 API 加载知识库列表到下拉菜单 === + function loadKnowledgeBasesForDropdown() { + sendMsg({ type: 'LIST_KNOWLEDGE_BASES' }).then(function (resp) { + if (resp && resp.success && resp.data) { + var kbList = Array.isArray(resp.data) ? resp.data : (resp.data.items || []); + popupAllKbs = kbList; + filterAndRenderPopupKbs(); + // 检查剪藏知识库选择 + checkClipKbSelection(kbList); + } + }); + } + + var popupAllKbs = []; + var popupAgents = []; + + // === 根据当前选中智能体配置过滤知识库 === + function filterAndRenderPopupKbs() { + var filtered = popupAllKbs; + var agent = null; + for (var i = 0; i < popupAgents.length; i++) { + if (popupAgents[i].id === popupAgentId) { agent = popupAgents[i]; break; } + } + if (agent && agent.config) { + var mode = agent.config.kb_selection_mode || 'all'; + if (mode === 'none') { + filtered = []; + } else if (mode === 'selected' && agent.config.knowledge_bases) { + var allowedIds = agent.config.knowledge_bases; + filtered = popupAllKbs.filter(function (kb) { + return allowedIds.indexOf(kb.id) !== -1; + }); + } + } + renderKbDropdownItems(filtered); + } + + // === 从 API 加载智能体列表到模式下拉 === + function loadAgentsForDropdown() { + sendMsg({ type: 'LIST_AGENTS' }).then(function (resp) { + if (resp && resp.success && resp.data) { + popupAgents = Array.isArray(resp.data) ? resp.data : (resp.data.data || []); + if (popupAgents.length > 0) { + // 恢复持久化的模式选择 + chrome.storage.local.get('ka_selected_agent', function (stored) { + if (!popupAgentId && stored && stored.ka_selected_agent && stored.ka_selected_agent.agentId) { + var saved = stored.ka_selected_agent; + for (var i = 0; i < popupAgents.length; i++) { + if (popupAgents[i].id === saved.agentId) { + popupAgentId = saved.agentId; + popupAgentEnabled = !!saved.agentEnabled; + break; + } + } + } + if (!popupAgentId) { + popupAgentId = popupAgents[0].id; + var isQA = popupAgents[0].id === 'builtin-quick-answer' || (popupAgents[0].config && popupAgents[0].config.agent_mode === 'quick-answer'); + popupAgentEnabled = !isQA; + } + // 设置当前选中智能体的图片上传能力 + for (var j = 0; j < popupAgents.length; j++) { + if (popupAgents[j].id === popupAgentId) { + popupAgentImageUpload = !!(popupAgents[j].config && popupAgents[j].config.image_upload_enabled); + break; + } + } + popupUpdateImageUI(); + renderAgentModeItems(popupAgents); + // 加载全部知识库(前端根据智能体配置过滤) + loadKnowledgeBasesForDropdown(); + }); + } else { + loadKnowledgeBasesForDropdown(); + } + } else { + loadKnowledgeBasesForDropdown(); + } + }); + } + + function renderAgentModeItems(agentList) { + var modeMenu = $('popup-mode-menu'); + if (!modeMenu) return; + // 移除旧的模式选项 + var oldItems = modeMenu.querySelectorAll('.kb-mode-item'); + oldItems.forEach(function (item) { item.remove(); }); + + agentList.forEach(function (agent, idx) { + var item = document.createElement('div'); + var isQA = agent.id === 'builtin-quick-answer' || (agent.config && agent.config.agent_mode === 'quick-answer'); + var isSelected = popupAgentId ? (agent.id === popupAgentId) : (idx === 0); + item.className = 'kb-mode-item' + (isSelected ? ' selected' : ''); + item.setAttribute('data-agent-id', agent.id); + item.innerHTML = ' ' + (function (s) { + var d = document.createElement('div'); d.textContent = s; return d.innerHTML; + })(agent.name); + if (isSelected) { + $('mode-name').textContent = agent.name; + } + item.addEventListener('click', function (e) { + e.stopPropagation(); + popupAgentId = agent.id; + popupAgentEnabled = !isQA; + modeMenu.querySelectorAll('.kb-mode-item').forEach(function (i) { i.classList.remove('selected'); }); + item.classList.add('selected'); + $('mode-name').textContent = agent.name; + modeMenu.classList.remove('show'); + // 持久化模式选择,同步给 sidepanel / weknora + chrome.storage.local.set({ ka_selected_agent: { agentId: agent.id, agentEnabled: !isQA } }); + // 更新图片上传按钮可见性 + popupAgentImageUpload = !!(agent.config && agent.config.image_upload_enabled); + popupUpdateImageUI(); + if (!popupAgentImageUpload && popupPendingImages.length > 0) popupClearImages(); + // 切换智能体后根据配置过滤知识库 + filterAndRenderPopupKbs(); + }); + modeMenu.appendChild(item); + }); + } + + function renderKbDropdownItems(kbList) { + var kbMenu = $('kb-menu'); + if (!kbMenu) return; + // 移除旧的知识库选项(保留分隔线、模式等) + var oldItems = kbMenu.querySelectorAll('.kb-dropdown-item'); + oldItems.forEach(function (item) { item.remove(); }); + // 在第一个分隔线之前插入新选项 + var firstDivider = kbMenu.querySelector('.kb-dropdown-divider'); + // 插入 "全部" 选项 + var allItem = createKbDropdownItem('all', '全部知识库', true); + kbMenu.insertBefore(allItem, firstDivider); + // 插入真实知识库 + kbList.forEach(function (kb) { + var item = createKbDropdownItem(kb.id, kb.name, false); + kbMenu.insertBefore(item, firstDivider); + }); + } + + function createKbDropdownItem(kbId, name, isSelected) { + var div = document.createElement('div'); + div.className = 'kb-dropdown-item' + (isSelected ? ' selected' : ''); + div.setAttribute('data-kb', kbId); + div.innerHTML = ' ' + (function (s) { + var d = document.createElement('div'); + d.textContent = s; + return d.innerHTML; + })(name); + div.addEventListener('click', function (e) { + e.stopPropagation(); + selectedKb = kbId; + $('kb-name').textContent = name.length > 4 ? name.substring(0, 4) : name; + $('kb-menu').querySelectorAll('.kb-dropdown-item').forEach(function (i) { i.classList.remove('selected'); }); + div.classList.add('selected'); + $('kb-menu').classList.remove('show'); + }); + return div; } // === 加载并显示最新一条内容 === @@ -356,10 +558,114 @@ var chatInput = $('chat-input'); var sendBtn = $('btn-chat-send'); var selectedKb = 'all'; - var selectedMode = 'fast'; + var popupAgentId = ''; + var popupAgentEnabled = false; + var popupAgentImageUpload = false; + + // 图片上传 + var popupPendingImages = []; + var POPUP_MAX_IMAGES = 5; + var POPUP_ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']; + var POPUP_MAX_IMAGE_SIZE = 10 * 1024 * 1024; + var popupImageInput = $('popup-image-input'); + var popupImageBtn = $('popup-image-btn'); + var popupImagePreviews = $('popup-image-previews'); + + function popupAddImages(files) { + if (!popupAgentImageUpload) return; + for (var i = 0; i < files.length; i++) { + if (popupPendingImages.length >= POPUP_MAX_IMAGES) { break; } + var f = files[i]; + if (POPUP_ALLOWED_TYPES.indexOf(f.type) === -1 || f.size > POPUP_MAX_IMAGE_SIZE) continue; + popupPendingImages.push({ file: f, preview: URL.createObjectURL(f) }); + } + popupRenderPreviews(); + } + + function popupRemoveImage(idx) { + if (idx >= 0 && idx < popupPendingImages.length) { + URL.revokeObjectURL(popupPendingImages[idx].preview); + popupPendingImages.splice(idx, 1); + } + popupRenderPreviews(); + } + + function popupClearImages() { + popupPendingImages.forEach(function (img) { URL.revokeObjectURL(img.preview); }); + popupPendingImages = []; + popupRenderPreviews(); + } + + function popupRenderPreviews() { + if (!popupImagePreviews) return; + if (popupPendingImages.length === 0) { + popupImagePreviews.style.display = 'none'; + popupImagePreviews.innerHTML = ''; + return; + } + popupImagePreviews.style.display = 'flex'; + var html = ''; + for (var i = 0; i < popupPendingImages.length; i++) { + html += ''; + } + popupImagePreviews.innerHTML = html; + popupImagePreviews.querySelectorAll('.popup-img-remove').forEach(function (btn) { + btn.addEventListener('click', function () { + popupRemoveImage(parseInt(btn.parentElement.getAttribute('data-idx'))); + }); + }); + } + + function popupUpdateImageUI() { + if (popupImageBtn) popupImageBtn.style.display = popupAgentImageUpload ? '' : 'none'; + if (!popupAgentImageUpload && popupPendingImages.length > 0) popupClearImages(); + } + + function popupFileToBase64(file) { + return new Promise(function (resolve, reject) { + var reader = new FileReader(); + reader.onload = function () { resolve(reader.result); }; + reader.onerror = reject; + reader.readAsDataURL(file); + }); + } + + if (popupImageInput) { + popupImageInput.addEventListener('change', function () { + if (popupImageInput.files) popupAddImages(Array.from(popupImageInput.files)); + popupImageInput.value = ''; + }); + } + if (popupImageBtn) { + popupImageBtn.addEventListener('click', function () { + if (popupImageInput) popupImageInput.click(); + }); + } + + chatInput.addEventListener('paste', function (e) { + if (!popupAgentImageUpload) return; + var items = e.clipboardData && e.clipboardData.items; + if (!items) return; + var imageFiles = []; + for (var i = 0; i < items.length; i++) { + if (items[i].type.indexOf('image/') === 0) { + var f = items[i].getAsFile(); + if (f) imageFiles.push(f); + } + } + if (imageFiles.length > 0) { + e.preventDefault(); + popupAddImages(imageFiles); + } + }); chatInput.addEventListener('input', function () { sendBtn.classList.toggle('active', chatInput.value.trim().length > 0); + // 自动调整高度 + chatInput.style.height = 'auto'; + chatInput.style.height = Math.min(chatInput.scrollHeight, 100) + 'px'; }); chatInput.addEventListener('keydown', function (e) { @@ -367,6 +673,7 @@ e.preventDefault(); sendChat(); } + // Shift+Enter 默认行为即换行,无需额外处理 }); sendBtn.addEventListener('click', sendChat); @@ -374,32 +681,48 @@ function sendChat() { var text = chatInput.value.trim(); if (!text) return; - var queryData = { query: text, user: currentUser, kb: selectedKb, mode: selectedMode, ts: Date.now() }; + var queryData = { query: text, user: currentUser, kb: selectedKb, ts: Date.now(), agentId: popupAgentId, agentEnabled: popupAgentEnabled }; + // 如果选中了具体知识库,传递知识库 ID + if (selectedKb && selectedKb !== 'all') { + queryData.knowledgeBaseIds = [selectedKb]; + } - if (chrome && chrome.storage) { - chrome.storage.local.set({ ka_pending_query: queryData }); + function doPopupSend(data) { + if (chrome && chrome.storage) { + chrome.storage.local.set({ ka_pending_query: data }); + } + if (chrome && chrome.runtime && chrome.runtime.sendMessage) { + chrome.runtime.sendMessage({ type: 'CHAT_QUERY', payload: data }).catch(function () {}); + } + if (chrome && chrome.tabs) { + chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) { + if (tabs && tabs[0]) { + chrome.sidePanel.open({ tabId: tabs[0].id }).catch(function () {}); + } + window.close(); + }); + } } - if (chrome && chrome.runtime && chrome.runtime.sendMessage) { - chrome.runtime.sendMessage({ type: 'CHAT_QUERY', payload: queryData }).catch(function () {}); - } - if (chrome && chrome.tabs) { - chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) { - if (tabs && tabs[0]) { - chrome.sidePanel.open({ tabId: tabs[0].id }).catch(function () {}); - } - window.close(); + + if (popupPendingImages.length > 0) { + var promises = popupPendingImages.map(function (img) { return popupFileToBase64(img.file); }); + Promise.all(promises).then(function (dataURIs) { + queryData.images = dataURIs.map(function (d) { return { data: d }; }); + popupClearImages(); + doPopupSend(queryData); }); + } else { + doPopupSend(queryData); } } - // === 知识库下拉(合并模式选项) === + // === 知识库下拉 === var kbMenu = $('kb-menu'); var kbBtn = $('btn-kb-select'); function positionKbMenu() { var rect = kbBtn.getBoundingClientRect(); var menuH = kbMenu.offsetHeight; - // 优先向上弹出;空间不足则向下 var top = rect.top - menuH - 6; if (top < 4) top = rect.bottom + 6; kbMenu.style.left = rect.left + 'px'; @@ -408,10 +731,10 @@ kbBtn.addEventListener('click', function (e) { e.stopPropagation(); + popupModeMenu.classList.remove('show'); var willShow = !kbMenu.classList.contains('show'); kbMenu.classList.toggle('show'); if (willShow) { - // 先显示再计算高度 requestAnimationFrame(positionKbMenu); } }); @@ -427,20 +750,38 @@ kbMenu.classList.remove('show'); }); }); - // 模式选择(合并在知识库菜单内) - kbMenu.querySelectorAll('.kb-mode-item').forEach(function (item) { - item.addEventListener('click', function (e) { - e.stopPropagation(); - selectedMode = item.getAttribute('data-mode'); - kbMenu.querySelectorAll('.kb-mode-item').forEach(function (i) { i.classList.remove('selected'); }); - item.classList.add('selected'); - kbMenu.classList.remove('show'); - }); + + // === 模式下拉(独立) === + var popupModeMenu = $('popup-mode-menu'); + var modeBtn = $('btn-mode-select'); + + function positionModeMenu() { + var rect = modeBtn.getBoundingClientRect(); + var menuH = popupModeMenu.offsetHeight; + var top = rect.top - menuH - 6; + if (top < 4) top = rect.bottom + 6; + popupModeMenu.style.left = rect.left + 'px'; + popupModeMenu.style.top = top + 'px'; + } + + modeBtn.addEventListener('click', function (e) { + e.stopPropagation(); + kbMenu.classList.remove('show'); + var willShow = !popupModeMenu.classList.contains('show'); + popupModeMenu.classList.toggle('show'); + if (willShow) { + requestAnimationFrame(positionModeMenu); + } + }); + + popupModeMenu.addEventListener('click', function (e) { + e.stopPropagation(); }); // 点击其他地方关闭下拉 document.addEventListener('click', function () { kbMenu.classList.remove('show'); + popupModeMenu.classList.remove('show'); if (moreMenu) moreMenu.classList.remove('show'); }); @@ -586,61 +927,186 @@ function renderMarkdown(text) { if (!text) return ''; - var html = text; - html = html.replace(/```(\w*)\n([\s\S]*?)```/g, function (_, lang, code) { - return '
' + escapeHtml(code.replace(/\n$/, '')) + '
'; + + // 1. 提取代码块,替换为占位符 + var codeBlocks = []; + var processed = text.replace(/```(\w*)\n([\s\S]*?)```/g, function (_, lang, code) { + var idx = codeBlocks.length; + codeBlocks.push('
' + escapeHtml(code.replace(/\n$/, '')) + '
'); + return '\x00CODEBLOCK' + idx + '\x00'; }); - html = html.replace(/`([^`]+)`/g, '$1'); - html = html.replace(/((?:\|.+\|\n)+)/g, function (tableBlock) { + + // 2. 提取行内代码 + var inlineCodes = []; + processed = processed.replace(/`([^`]+)`/g, function (_, code) { + var idx = inlineCodes.length; + inlineCodes.push('' + escapeHtml(code) + ''); + return '\x00INLINE' + idx + '\x00'; + }); + + // 3. 表格处理 — 提取为占位符 + var tables = []; + processed = processed.replace(/((?:\|.+\|\n)+)/g, function (tableBlock) { var rows = tableBlock.trim().split('\n'); if (rows.length < 2) return tableBlock; - var out = ''; + var out = '
'; rows.forEach(function (row, i) { if (/^\|[\s\-:|]+\|$/.test(row)) return; var tag = i === 0 ? 'th' : 'td'; var cells = row.split('|').filter(function (c, ci, arr) { return ci > 0 && ci < arr.length - 1; }); out += ''; - cells.forEach(function (cell) { out += '<' + tag + '>' + cell.trim() + ''; }); + cells.forEach(function (cell) { + out += '<' + tag + '>' + cell.trim() + ''; + }); out += ''; }); out += '
'; - return out; + var idx = tables.length; + tables.push(out); + return '\n\x00TABLE' + idx + '\x00\n'; }); - var lines = html.split('\n'); - var result = []; - var inList = false; - var listType = ''; - for (var i = 0; i < lines.length; i++) { + + // 4. 逐行解析为块元素 + var lines = processed.split('\n'); + var html = ''; + var i = 0; + + while (i < lines.length) { var line = lines[i]; - if (line.indexOf('
') !== -1 || line.indexOf('') !== -1) {
-        if (inList) { result.push(''); inList = false; }
-        result.push(line); continue;
+
+      // 代码块占位符
+      var cbMatch = line.match(/^\x00CODEBLOCK(\d+)\x00$/);
+      if (cbMatch) {
+        html += codeBlocks[parseInt(cbMatch[1])];
+        i++; continue;
       }
-      if (/^####\s+(.+)/.test(line)) { if (inList) { result.push(''); inList = false; } result.push('

' + RegExp.$1 + '

'); continue; } - if (/^###\s+(.+)/.test(line)) { if (inList) { result.push(''); inList = false; } result.push('

' + RegExp.$1 + '

'); continue; } - if (/^##\s+(.+)/.test(line)) { if (inList) { result.push(''); inList = false; } result.push('

' + RegExp.$1 + '

'); continue; } - if (/^#\s+(.+)/.test(line)) { if (inList) { result.push(''); inList = false; } result.push('

' + RegExp.$1 + '

'); continue; } - if (/^---+$/.test(line.trim())) { if (inList) { result.push(''); inList = false; } result.push('
'); continue; } - if (/^>\s*(.*)/.test(line)) { if (inList) { result.push(''); inList = false; } result.push('
' + RegExp.$1 + '
'); continue; } - if (/^[\-\*]\s+(.+)/.test(line)) { - if (!inList || listType !== 'ul') { if (inList) result.push(''); result.push('
    '); inList = true; listType = 'ul'; } - result.push('
  • ' + RegExp.$1 + '
  • '); continue; + + // 表格占位符 + var tbMatch = line.match(/^\x00TABLE(\d+)\x00$/); + if (tbMatch) { + html += tables[parseInt(tbMatch[1])]; + i++; continue; } - if (/^\d+\.\s+(.+)/.test(line)) { - if (!inList || listType !== 'ol') { if (inList) result.push(''); result.push('
      '); inList = true; listType = 'ol'; } - result.push('
    1. ' + RegExp.$1 + '
    2. '); continue; + + // 标题 + if (/^(#{1,6})\s+(.+)/.test(line)) { + var level = RegExp.$1.length; + html += '' + inlineFormat(RegExp.$2) + ''; + i++; continue; } - if (inList) { result.push(''); inList = false; } - if (line.trim() === '') continue; - result.push('

      ' + line + '

      '); + + // 引用块 + if (/^>\s*(.*)/.test(line)) { + var bqLines = []; + while (i < lines.length && /^>\s*(.*)/.test(lines[i])) { + bqLines.push(RegExp.$1); + i++; + } + html += '
      ' + inlineFormat(bqLines.join('
      ')) + '
      '; + continue; + } + + // 无序列表 + if (/^(\s*)[*\-]\s+(.+)/.test(line)) { + html += parseList(lines, i, 'ul'); + while (i < lines.length && /^(\s*)[*\-]\s+/.test(lines[i])) i++; + continue; + } + + // 有序列表 + if (/^(\s*)\d+\.\s+(.+)/.test(line)) { + html += parseList(lines, i, 'ol'); + while (i < lines.length && /^(\s*)\d+\.\s+/.test(lines[i])) i++; + continue; + } + + // 水平线 + if (/^[-*_]{3,}\s*$/.test(line.trim())) { + html += '
      '; + i++; continue; + } + + // 空行 + if (line.trim() === '') { + i++; continue; + } + + // 普通段落 + var pLines = []; + while (i < lines.length) { + var pl = lines[i]; + if (pl.trim() === '' || /^#{1,6}\s/.test(pl) || /^>\s/.test(pl) || + /^(\s*)[*\-]\s+/.test(pl) || /^(\s*)\d+\.\s+/.test(pl) || + /^\x00CODEBLOCK/.test(pl) || /^\x00TABLE/.test(pl) || + /^[-*_]{3,}\s*$/.test(pl.trim())) break; + pLines.push(pl); + i++; + } + html += '

      ' + inlineFormat(pLines.join('
      ')) + '

      '; } - if (inList) result.push(''); - html = result.join('\n'); - html = html.replace(/\*\*(.+?)\*\*/g, '$1'); - html = html.replace(/\*(.+?)\*/g, '$1'); - html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '$1'); - html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); + + // 5. 恢复行内代码占位符 + html = html.replace(/\x00INLINE(\d+)\x00/g, function (_, idx) { + return inlineCodes[parseInt(idx)]; + }); + return html; + + // --- 列表解析(支持嵌套)--- + function parseList(allLines, startIdx, defaultType) { + var items = []; + var j = startIdx; + var baseIndent = -1; + var listTag = defaultType; + var re = listTag === 'ul' ? /^(\s*)[*\-]\s+(.+)/ : /^(\s*)\d+\.\s+(.+)/; + + while (j < allLines.length) { + var m = allLines[j].match(re); + if (!m) break; + var indent = m[1].length; + if (baseIndent < 0) baseIndent = indent; + if (indent > baseIndent) { + var subStart = j; + while (j < allLines.length) { + var sm = allLines[j].match(re); + if (!sm || sm[1].length < indent) break; + j++; + } + var subHtml = parseList(allLines, subStart, listTag); + if (items.length > 0) { + items[items.length - 1] += subHtml; + } + continue; + } + if (indent < baseIndent) break; + items.push(inlineFormat(m[2])); + j++; + } + + var out = '<' + listTag + ' class="md-list">'; + for (var k = 0; k < items.length; k++) { + out += '
    3. ' + items[k] + '
    4. '; + } + out += ''; + return out; + } + + // --- 行内格式化 --- + function inlineFormat(s) { + if (!s) return ''; + // 图片(必须在链接之前) + s = s.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '$1'); + // 链接 + s = s.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); + s = s.replace(/\*\*\*(.+?)\*\*\*/g, '$1'); + s = s.replace(/\*\*(.+?)\*\*/g, '$1'); + s = s.replace(/(?$1'); + s = s.replace(/~~(.+?)~~/g, '$1'); + s = s.replace(/\x00INLINE(\d+)\x00/g, function (_, idx) { + return inlineCodes[parseInt(idx)]; + }); + return s; + } } // === Markdown 速记 === @@ -795,31 +1261,160 @@ } }); - // === 主页按钮 === - $('btn-home').addEventListener('click', function () { - moreMenu.classList.remove('show'); - // 打开 WeKnora 管理页面,弹窗保留 - if (chrome && chrome.tabs) { - chrome.tabs.create({ url: chrome.runtime.getURL('weknora.html') }); + $('btn-settings-close').addEventListener('click', function () { + $('settings-overlay').classList.remove('show'); + }); + + // === 剪藏知识库选择 === + var clipKbOverlay = $('clip-kb-overlay'); + + // 检查是否需要引导用户选择剪藏知识库 + function checkClipKbSelection(kbList) { + chrome.storage.local.get(['clipKbId', 'clipKbName'], function (data) { + clipKbId = data.clipKbId || ''; + clipKbName = data.clipKbName || ''; + + // 验证保存的 KB 是否仍存在 + if (clipKbId) { + var found = kbList.some(function (kb) { return kb.id === clipKbId; }); + if (!found) { + clipKbId = ''; + clipKbName = ''; + chrome.storage.local.remove(['clipKbId', 'clipKbName']); + } + } + + // 更新设置面板显示 + updateClipKbDisplay(); + + // 首次使用 — 引导选择 + if (!clipKbId) { + showClipKbSelector(kbList); + } + }); + } + + function updateClipKbDisplay() { + var el = $('set-clip-kb'); + if (el) { + el.textContent = clipKbName ? (clipKbName + ' ›') : '未选择 ›'; + } + } + + function showClipKbSelector(kbList) { + var list = kbList || popupAllKbs; + var listEl = $('clip-kb-list'); + var emptyEl = $('clip-kb-empty'); + + if (!list || list.length === 0) { + listEl.style.display = 'none'; + emptyEl.style.display = 'block'; } else { - goPage('pg-login'); + listEl.style.display = 'block'; + emptyEl.style.display = 'none'; + var html = ''; + for (var i = 0; i < list.length; i++) { + var kb = list[i]; + var selected = kb.id === clipKbId ? ' selected' : ''; + html += '
      ' + + '
      ' + escapeHtml(kb.name) + '
      ' + + '
      ' + escapeHtml(kb.description || '暂无描述') + '
      ' + + '
      ' + (kb.document_count || 0) + ' 文档 · ' + (kb.chunk_count || 0) + ' 分段
      ' + + '
      '; + } + listEl.innerHTML = html; + + // 点击选择 + listEl.querySelectorAll('.clip-kb-card').forEach(function (card) { + card.addEventListener('click', function () { + clipKbId = card.getAttribute('data-kb-id'); + clipKbName = card.getAttribute('data-kb-name'); + chrome.storage.local.set({ clipKbId: clipKbId, clipKbName: clipKbName }); + updateClipKbDisplay(); + clipKbOverlay.classList.remove('show'); + toast('已选择知识库: ' + clipKbName, 'success'); + }); + }); + } + + clipKbOverlay.classList.add('show'); + } + + // 设置面板中点击"剪藏知识库" + $('set-clip-kb-row').addEventListener('click', function () { + $('settings-overlay').classList.remove('show'); + showClipKbSelector(); + }); + + // 关闭按钮 + $('clip-kb-close').addEventListener('click', function () { + clipKbOverlay.classList.remove('show'); + }); + + // 背景点击关闭 + clipKbOverlay.addEventListener('click', function (e) { + if (e.target === clipKbOverlay) { + clipKbOverlay.classList.remove('show'); } }); - // === 查看收藏按钮 === - $('btn-clips').addEventListener('click', function () { + // "前往创建"按钮 — 打开 WeKnora 主站 + $('clip-kb-goto').addEventListener('click', function () { + clipKbOverlay.classList.remove('show'); + sendMsg({ type: 'GET_CONFIG' }).then(function (resp) { + if (resp && resp.success && resp.data && resp.data.baseUrl) { + var baseUrl = resp.data.baseUrl.replace(/\/api\/v1\/?$/, ''); + if (chrome && chrome.tabs) { + chrome.tabs.create({ url: baseUrl }); + } + } else { + toast('请先配置服务地址', 'error'); + } + }); + }); + + // === 打开对话面板(sidebar)=== + $('btn-sidebar').addEventListener('click', function () { moreMenu.classList.remove('show'); if (chrome && chrome.tabs) { - chrome.tabs.create({ url: chrome.runtime.getURL('clips.html') }); + chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) { + if (tabs && tabs[0]) { + chrome.sidePanel.open({ tabId: tabs[0].id }).catch(function () {}); + } + window.close(); + }); } }); + // === 打开笔记/知识库页面 === + function openNotesPage() { + if (!currentUser || !chrome || !chrome.tabs) return; + if (currentUser.type === 'wk') { + // 自部署 WeKnora — 用配置的 baseUrl + sendMsg({ type: 'GET_CONFIG' }).then(function (resp) { + if (resp && resp.success && resp.data && resp.data.baseUrl) { + var url = resp.data.baseUrl.replace(/\/api\/v1\/?$/, ''); + chrome.tabs.create({ url: url }); + } else { + toast('请先配置服务地址', 'error'); + } + }); + } else { + // 知识助手官网用户 + chrome.tabs.create({ url: 'https://weknora.weixin.qq.com' }); + } + } + + // === 查看笔记按钮 === + $('btn-clips').addEventListener('click', function () { + moreMenu.classList.remove('show'); + openNotesPage(); + }); + // === 最新内容卡片 — 查看全部 === $('btn-latest-more').addEventListener('click', function (e) { e.stopPropagation(); - if (chrome && chrome.tabs) { - chrome.tabs.create({ url: chrome.runtime.getURL('clips.html') }); - } + openNotesPage(); }); // 点击最新内容卡片本身也打开列表页;双击翻转进入速记编辑 @@ -837,9 +1432,7 @@ // 单击:延迟执行,等看有没有双击 clickTimer = setTimeout(function () { clickTimer = null; - if (chrome && chrome.tabs) { - chrome.tabs.create({ url: chrome.runtime.getURL('clips.html') }); - } + openNotesPage(); }, 250); } }); diff --git a/frontend/chrome-extension/快速笔记 5.0/sidepanel.html b/frontend/chrome-extension/快速笔记 5.0/sidepanel.html index a4d6edc7..b5777baf 100644 --- a/frontend/chrome-extension/快速笔记 5.0/sidepanel.html +++ b/frontend/chrome-extension/快速笔记 5.0/sidepanel.html @@ -23,7 +23,10 @@ body { font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; } .header-icon { width: 30px; height: 30px; background: var(--primary); border-radius: 10px; display: flex; align-items: center; justify-content: center; } .header-icon svg { width: 16px; height: 16px; color: #fff; } -.header h1 { font-size: 15px; font-weight: 600; flex: 1; } +.header h1 { font-size: 15px; font-weight: 600; flex: 1; min-width: 0; } +.header-title-wrap { flex: 1; min-width: 0; display: flex; flex-direction: column; justify-content: center; } +.header-session-title { font-size: 11px; font-weight: 400; color: var(--text3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: none; margin-top: 1px; } +.header-session-title.show { display: block; } .header-btn { width: 32px; height: 32px; border: none; background: #f5f6f7; border-radius: 10px; cursor: pointer; display: flex; align-items: center; justify-content: center; color: var(--text3); transition: all 0.2s; } .header-btn:hover { background: #eee; color: var(--text); } .header-btn svg { width: 16px; height: 16px; } @@ -38,6 +41,8 @@ body { font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; font-size: 14px; line-height: 1.6; word-wrap: break-word; + overflow: hidden; + flex-shrink: 0; } .msg-user { align-self: flex-end; @@ -100,27 +105,41 @@ body { font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; } .quick-btn:hover { background: rgba(7,193,96,0.08); color: var(--primary); box-shadow: 0 2px 8px rgba(7,193,96,0.12); } -/* ===== Input Area — 单行胶囊输入栏 ===== */ +/* ===== Input Area ===== */ .input-area { padding: 10px 14px 14px; flex-shrink: 0; background: transparent; } -.sp-chat-bar { - display: flex; - align-items: center; + +/* 整体包裹卡片 */ +.sp-chat-bar-wrapper { background: var(--white); - border-radius: 50px; + border-radius: 16px; box-shadow: 0 0 6px rgba(0,0,0,0.04), 0 4px 20px rgba(0,0,0,0.10); - padding: 5px 5px 5px 6px; - gap: 0; + overflow: visible; position: relative; transition: box-shadow 0.2s; } -.sp-chat-bar:focus-within { +.sp-chat-bar-wrapper:focus-within { box-shadow: 0 4px 24px rgba(0,0,0,0.10), 0 0 0 1.5px rgba(7,193,96,0.18); } +/* 顶部工具栏:知识库 + 模式 */ +.sp-chat-bar-toolbar { + display: flex; + align-items: center; + gap: 4px; + padding: 6px 8px 0; +} + +.sp-chat-bar { + display: flex; + align-items: center; + padding: 4px 5px 5px 8px; + gap: 4px; + position: relative; +} /* 知识库选择按钮 */ .sp-chat-bar-kb { display: flex; @@ -142,6 +161,25 @@ body { font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; .sp-chat-bar-kb svg { width: 13px; height: 13px; flex-shrink: 0; } .sp-chat-bar-arrow { width: 10px !important; height: 10px !important; opacity: 0.5; } +/* 模式选择按钮 */ +.sp-chat-bar-mode { + display: flex; align-items: center; gap: 3px; + border: none; background: #f3f4f5; border-radius: 50px; + padding: 6px 8px 6px 10px; font-size: 11px; font-weight: 500; + color: var(--text2); cursor: pointer; white-space: nowrap; + transition: all 0.15s; flex-shrink: 0; +} +.sp-chat-bar-mode:hover { background: #eaebed; color: var(--text); } +.sp-chat-bar-mode svg { width: 13px; height: 13px; flex-shrink: 0; } + +/* 模式下拉菜单 */ +.sp-mode-dropdown-menu { + position: fixed; background: var(--white); border-radius: 14px; + box-shadow: 0 0 6px rgba(0,0,0,0.04), 0 8px 32px rgba(0,0,0,0.14); + padding: 6px 0; min-width: 160px; display: none; z-index: 100; +} +.sp-mode-dropdown-menu.show { display: block; } + /* 分隔竖线 */ .sp-chat-bar-divider { width: 1px; height: 16px; background: #e5e5e5; margin: 0 8px; flex-shrink: 0; } @@ -154,9 +192,13 @@ body { font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; font-family: inherit; outline: none; color: var(--text); - padding: 0; + padding: 6px 4px; min-width: 0; height: 30px; + max-height: 120px; + resize: none; + overflow-y: auto; + line-height: 18px; } .sp-chat-bar-input::placeholder { color: #c0c0c0; } @@ -179,6 +221,33 @@ body { font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; .sp-chat-bar-send.active { background: var(--primary); color: #fff; box-shadow: 0 2px 8px rgba(7,193,96,0.25); } .sp-chat-bar-send.active:hover { background: var(--primary-dark); } +/* 图片上传按钮 */ +.sp-chat-bar-img { + display: flex; align-items: center; justify-content: center; + border: none; background: none; color: #999; cursor: pointer; + width: 28px; height: 28px; padding: 0; flex-shrink: 0; border-radius: 6px; transition: all 0.15s; +} +.sp-chat-bar-img:hover { color: var(--primary); background: rgba(7,193,96,0.06); } +.sp-chat-bar-img svg { width: 16px; height: 16px; } + +/* 图片预览条 */ +.sp-image-previews { + display: flex; gap: 6px; padding: 6px 12px 0; overflow-x: auto; flex-shrink: 0; +} +.sp-img-item { position: relative; flex-shrink: 0; } +.sp-img-thumb { width: 44px; height: 44px; border-radius: 6px; object-fit: cover; border: 1px solid #e8e8e8; } +.sp-img-remove { + position: absolute; top: -4px; right: -4px; + width: 16px; height: 16px; border-radius: 50%; background: rgba(0,0,0,0.5); color: #fff; + font-size: 12px; line-height: 16px; text-align: center; cursor: pointer; border: none; padding: 0; +} +.sp-img-remove:hover { background: #e44; } + +/* 用户消息中的图片 */ +.msg-user-images { display: flex; gap: 6px; margin-top: 6px; flex-wrap: wrap; } +.msg-user-img { width: 60px; height: 60px; border-radius: 8px; object-fit: cover; border: 1px solid rgba(255,255,255,0.3); cursor: pointer; } +.msg-user-img:hover { opacity: 0.85; } + /* ===== 合并下拉菜单 (fixed定位) ===== */ .sp-kb-dropdown-menu { position: fixed; @@ -293,6 +362,162 @@ body { font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; transition: all 0.2s; } .clip-action-btn:hover { background: rgba(7,193,96,0.08); color: var(--primary); } + +/* === Markdown 渲染样式 === */ +.md-pre { background: #1e1e2e; color: #cdd6f4; border-radius: 8px; padding: 10px 12px; overflow-x: auto; font-size: 12px; margin: 6px 0; line-height: 1.5; } +.md-pre code { font-family: "SF Mono", "Fira Code", "Consolas", monospace; white-space: pre; } +.md-code { background: #f3f4f6; padding: 1px 5px; border-radius: 4px; font-size: 12px; font-family: "SF Mono", "Fira Code", "Consolas", monospace; } +.md-h { margin: 8px 0 4px; line-height: 1.4; } +h1.md-h { font-size: 18px; font-weight: 700; } +h2.md-h { font-size: 16px; font-weight: 700; } +h3.md-h { font-size: 14px; font-weight: 600; } +h4.md-h, h5.md-h, h6.md-h { font-size: 13px; font-weight: 600; } +.md-bq { border-left: 3px solid var(--primary); padding: 4px 12px; margin: 6px 0; background: #f8faf8; color: #666; border-radius: 0 4px 4px 0; font-size: 13px; } +.md-list { margin: 4px 0; padding-left: 20px; font-size: 13px; line-height: 1.6; } +.md-list li { margin: 2px 0; } +.md-list .md-list { margin: 2px 0 2px 0; } +.md-p { margin: 4px 0; line-height: 1.6; font-size: 13px; } +.md-p a, .md-list a, .md-bq a { color: var(--primary); text-decoration: none; } +.md-p a:hover, .md-list a:hover { text-decoration: underline; } +.md-table { border-collapse: collapse; margin: 6px 0; font-size: 12px; } +.md-table th { border: 1px solid #e5e5e5; padding: 4px 8px; background: #f7f8fa; font-weight: 600; text-align: left; white-space: nowrap; } +.md-table td { border: 1px solid #e5e5e5; padding: 4px 8px; } +.md-table tr:hover td { background: rgba(7,193,96,0.02); } +.md-table-wrap { overflow-x: auto; margin: 6px 0; } +.md-img { max-width: 100%; max-height: 200px; border-radius: 6px; margin: 6px 0; display: block; object-fit: contain; cursor: pointer; } +.md-img:hover { opacity: 0.85; } + +/* 图片灯箱 */ +.sp-lightbox { + position: fixed; inset: 0; background: rgba(0,0,0,0.8); z-index: 9999; + display: flex; align-items: center; justify-content: center; cursor: zoom-out; +} +.sp-lightbox img { + max-width: 95%; max-height: 95%; object-fit: contain; border-radius: 8px; + box-shadow: 0 4px 32px rgba(0,0,0,0.4); +} + +/* === Agent 流式显示(参考 AgentStreamDisplay.vue)=== */ +.agent-stream { display: flex; flex-direction: column; gap: 0; width: 92%; align-self: flex-start; margin-bottom: 6px; } + +/* 折叠的中间步骤 — 树根 */ +.agent-tree-root { + display: flex; justify-content: space-between; align-items: center; + padding: 6px 14px; font-size: 12px; font-weight: 500; cursor: pointer; + border-radius: 8px; background: var(--white); border: 0.5px solid #e0e0e0; + box-shadow: 0 2px 4px rgba(7,192,95,0.08); color: var(--text); + transition: all 0.25s cubic-bezier(0.4,0,0.2,1); margin-bottom: 10px; user-select: none; +} +.agent-tree-root:hover { background: rgba(7,192,95,0.04); } +.agent-tree-root-title { display: flex; align-items: center; gap: 6px; } +.agent-tree-root-title svg { width: 16px; height: 16px; color: var(--primary); flex-shrink: 0; } +.agent-tree-root-toggle { font-size: 12px; color: var(--primary); transition: transform 0.2s; } +.agent-tree-root.expanded .agent-tree-root-toggle { transform: rotate(180deg); } + +/* 树子节点 */ +.agent-tree-children { display: none; padding-left: 12px; margin-top: -4px; margin-bottom: 10px; position: relative; } +.agent-tree-root.expanded + .agent-tree-children { display: block; } +.agent-tree-child { position: relative; padding-left: 20px; margin-bottom: 6px; } +.agent-tree-child::before { content: ''; position: absolute; left: 0; top: 0; bottom: 0; border-left: 1px dashed #e0e0e0; } +.agent-tree-child:last-child::before { bottom: auto; height: 15px; } +.agent-tree-branch { position: absolute; left: 0; top: 15px; width: 16px; border-top: 1px dashed #e0e0e0; } + +/* 通用动作卡片(思考/工具调用) */ +.agent-card { + background: var(--white); border-radius: 5px; border: 1px solid #e8e8e8; + overflow: hidden; transition: all 0.2s; box-shadow: 0 1px 2px rgba(0,0,0,0.02); + animation: agentFadeIn 0.25s ease-out; margin-bottom: 8px; position: relative; width: 100%; +} +.agent-card:hover { border-color: var(--primary); box-shadow: 0 1px 4px rgba(7,192,95,0.08); } +.agent-card.pending { border-color: rgba(7,192,95,0.15); overflow: hidden; } +.agent-card.pending::after { + content: ''; position: absolute; inset: 0; + background: linear-gradient(120deg, transparent 0%, rgba(7,192,95,0.06) 40%, rgba(7,192,95,0.08) 55%, transparent 85%); + transform: translateX(-100%); animation: agentShimmer 2.8s ease-in-out infinite; pointer-events: none; +} +.agent-card.error { border-left: 2px solid #e44; } +.agent-card-header { + display: flex; justify-content: space-between; align-items: center; + padding: 5px 10px; cursor: pointer; user-select: none; transition: background-color 0.15s; +} +.agent-card-header:hover { background: rgba(7,192,95,0.03); } +.agent-card-header.no-expand { cursor: default; } +.agent-card-header.no-expand:hover { background: transparent; } +.agent-card-title { display: flex; align-items: center; gap: 6px; flex: 1; min-width: 0; } +.agent-card-title svg { width: 14px; height: 14px; color: var(--primary); flex-shrink: 0; } +.agent-card-name { font-size: 12px; font-weight: 500; white-space: nowrap; color: var(--text); overflow: hidden; text-overflow: ellipsis; } +.agent-card-summary { color: var(--text3); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; margin-left: 4px; } +.agent-card-toggle { font-size: 12px; color: var(--text3); transition: transform 0.2s; padding: 0 2px; flex-shrink: 0; } +.agent-card.expanded .agent-card-toggle { transform: rotate(180deg); } +.agent-card-detail { + display: none; padding: 6px 12px; border-top: 1px solid #f0f0f0; + font-size: 12px; color: var(--text2); line-height: 1.6; max-height: 200px; + overflow-y: auto; background: #fafbfc; white-space: pre-wrap; word-break: break-word; +} +.agent-card.expanded .agent-card-detail { display: block; } +.agent-card-info { + padding: 5px 10px; border-top: 1px solid #f0f0f0; + font-size: 11px; font-weight: 500; color: var(--text); line-height: 1.5; +} +.agent-card-info strong { color: var(--primary); font-weight: 600; } + +/* 计划任务变更 */ +.agent-plan-change { + padding: 8px 12px; background: linear-gradient(135deg, rgba(7,192,95,0.05), rgba(7,192,95,0.02)); + border-radius: 6px; border: 1px solid rgba(7,192,95,0.2); + font-size: 12px; color: var(--text); margin-bottom: 8px; animation: agentFadeIn 0.25s ease-out; +} +.agent-plan-change strong { color: var(--primary); font-weight: 600; margin-right: 3px; } + +/* 回答区域 */ +.agent-answer { animation: agentFadeIn 0.25s ease-out; } +.agent-answer-content { + font-size: 14px; color: var(--text); line-height: 1.6; background: var(--white); + border-radius: 16px; border-bottom-left-radius: 6px; padding: 12px 16px; + box-shadow: 0 0 6px rgba(0,0,0,0.04), 0 4px 16px rgba(0,0,0,0.08); +} +.agent-answer-toolbar { + display: flex; gap: 6px; margin-top: 6px; padding-left: 2px; +} +.agent-answer-toolbar button { + display: flex; align-items: center; justify-content: center; + width: 28px; height: 28px; border: 1px solid #e8e8e8; border-radius: 50%; + background: var(--white); cursor: pointer; color: var(--text3); transition: all 0.2s; +} +.agent-answer-toolbar button:hover { border-color: var(--primary); color: var(--primary); background: rgba(7,192,95,0.04); } +.agent-answer-toolbar button svg { width: 14px; height: 14px; } + +/* 知识引用面板 */ +.agent-refs { + background: var(--white); border: 1px solid #e8e8e8; border-radius: 10px; + margin-top: 8px; overflow: hidden; font-size: 12px; +} +.agent-refs-header { + display: flex; align-items: center; gap: 6px; padding: 8px 12px; + cursor: pointer; color: var(--text2); font-weight: 500; user-select: none; +} +.agent-refs-header:hover { background: rgba(7,193,96,0.04); } +.agent-refs-header svg:first-child { color: var(--primary); flex-shrink: 0; } +.agent-refs-arrow { margin-left: auto; transition: transform 0.2s; color: var(--text3); } +.agent-refs.expanded .agent-refs-arrow { transform: rotate(180deg); } +.agent-refs-list { + display: none; border-top: 1px solid #f0f0f0; padding: 6px 12px 8px; + max-height: 200px; overflow-y: auto; +} +.agent-refs.expanded .agent-refs-list { display: block; } +.agent-ref-group { margin-bottom: 6px; } +.agent-ref-group-title { display: flex; align-items: center; gap: 4px; font-weight: 500; color: var(--text); padding: 2px 0; font-size: 12px; } +.agent-ref-group-title svg { color: var(--primary); flex-shrink: 0; } +.agent-ref-count { color: var(--text3); font-weight: 400; font-size: 11px; } +.agent-ref-chunk { color: var(--text3); padding: 2px 0 2px 18px; line-height: 1.5; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: default; font-size: 11px; } +.agent-ref-chunk:hover { color: var(--primary); background: rgba(7,193,96,0.04); border-radius: 4px; } +.agent-ref-idx { color: var(--primary); font-weight: 500; font-size: 10px; } + +/* 动画 */ +@keyframes agentFadeIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } } +@keyframes agentShimmer { 0% { transform: translateX(-90%); } 50% { transform: translateX(-5%); } 100% { transform: translateX(90%); } } +@keyframes agentSpin { to { transform: rotate(360deg); } } +.agent-spinner { width: 12px; height: 12px; border: 2px solid #e0e0e0; border-top-color: var(--primary); border-radius: 50%; animation: agentSpin 0.8s linear infinite; display: inline-block; } @@ -301,7 +526,10 @@ body { font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
      -

      快速笔记 5.0

      +
      +

      快速笔记 5.0

      +
      +
      @@ -328,18 +556,31 @@ body { font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
      -
      - -
      - - + +
      +
      + + +
      +
      + + + +
      +
      @@ -367,8 +608,9 @@ body { font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; 技术规范
      -
      -
      模式
      +
      + +
      @@ -384,11 +626,6 @@ body { font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; 精确
      -
      -
      - - 上传图片 -
      diff --git a/frontend/chrome-extension/快速笔记 5.0/sidepanel.js b/frontend/chrome-extension/快速笔记 5.0/sidepanel.js index 5dea2e93..297c3c66 100644 --- a/frontend/chrome-extension/快速笔记 5.0/sidepanel.js +++ b/frontend/chrome-extension/快速笔记 5.0/sidepanel.js @@ -8,10 +8,235 @@ var sendBtn = $('sp-send'); var welcomeEl = $('welcome'); var typingEl = $('typing'); + var sessionTitleEl = $('sp-session-title'); + + // === 状态管理 === + var currentSessionId = null; + var selectedKbId = 'all'; + var selectedAgentId = ''; // 当前选中的智能体 ID + var selectedAgentEnabled = false; // 是否启用 agent 模式(smart-reasoning 用 agent-chat) + var knowledgeBases = []; + var agents = []; // 从 API 加载的智能体列表 + var isSending = false; + var cachedBaseUrl = ''; // 缓存服务地址,用于文件 URL 转换 + + // 图片上传状态 + var pendingImages = []; // { file: File, preview: string (blob URL) } + var selectedAgentImageUpload = false; + var MAX_IMAGES = 5; + var ALLOWED_IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']; + var MAX_IMAGE_SIZE = 10 * 1024 * 1024; // 10MB + + // 流式状态 + var streamingText = ''; + var streamingBotDiv = null; + var currentRequestId = ''; + var streamingReferences = []; + var renderPending = false; + + // Agent 事件流状态(参考 AgentStreamDisplay.vue) + var agentEventStream = []; + var agentContainer = null; + var agentExpandedIds = {}; + var agentActiveThinkingIds = {}; + var agentHasAnswer = false; + var agentTreeExpanded = false; + var agentRefsExpanded = false; + var agentAnswerText = ''; + var agentAnswerDone = false; + + // === Session Title 展示 === + function updateSessionTitle(title) { + if (!sessionTitleEl) return; + if (title) { + sessionTitleEl.textContent = title; + sessionTitleEl.classList.add('show'); + } else { + sessionTitleEl.textContent = ''; + sessionTitleEl.classList.remove('show'); + } + } + + // === 加载智能体列表(优先加载,然后根据默认智能体过滤 KB)=== + function loadAgents() { + chrome.runtime.sendMessage({ type: 'LIST_AGENTS' }, function (resp) { + void chrome.runtime.lastError; + if (resp && resp.success && resp.data) { + agents = Array.isArray(resp.data) ? resp.data : (resp.data.data || []); + // 恢复持久化的模式选择(仅当没有被 applyPendingPayload 预设时) + chrome.storage.local.get('ka_selected_agent', function (stored) { + if (!selectedAgentId && stored && stored.ka_selected_agent && stored.ka_selected_agent.agentId) { + var saved = stored.ka_selected_agent; + for (var i = 0; i < agents.length; i++) { + if (agents[i].id === saved.agentId) { + selectedAgentId = saved.agentId; + selectedAgentEnabled = !!saved.agentEnabled; + break; + } + } + } + // 兜底:默认选中第一个智能体 + if (agents.length > 0 && !selectedAgentId) { + var defaultAgent = agents[0]; + selectedAgentId = defaultAgent.id; + var isQuickAnswer = defaultAgent.id === 'builtin-quick-answer' || (defaultAgent.config && defaultAgent.config.agent_mode === 'quick-answer'); + selectedAgentEnabled = !isQuickAnswer; + } + renderAgentDropdown(); + // 更新图片上传按钮状态 + var selAgent = agents.find(function (a) { return a.id === selectedAgentId; }); + selectedAgentImageUpload = !!(selAgent && selAgent.config && selAgent.config.image_upload_enabled); + updateImageUploadUI(); + // 加载全部知识库(然后在前端根据智能体配置过滤) + loadAllKnowledgeBases(); + }); + } else { + // 加载全部知识库(然后在前端根据智能体配置过滤) + loadAllKnowledgeBases(); + } + }); + } + + // === 全部知识库缓存 === + var allKnowledgeBases = []; + + // === 加载全部知识库 === + function loadAllKnowledgeBases() { + chrome.runtime.sendMessage({ type: 'LIST_KNOWLEDGE_BASES' }, function (resp) { + void chrome.runtime.lastError; + if (resp && resp.success && resp.data) { + allKnowledgeBases = Array.isArray(resp.data) ? resp.data : (resp.data.items || []); + } else { + allKnowledgeBases = []; + } + filterAndRenderKbs(); + }); + } + + // === 根据当前选中的智能体配置过滤知识库 === + function filterAndRenderKbs() { + var agent = findAgentById(selectedAgentId); + if (agent && agent.config) { + var mode = agent.config.kb_selection_mode || 'all'; + if (mode === 'none') { + knowledgeBases = []; + } else if (mode === 'selected' && agent.config.knowledge_bases) { + var allowedIds = agent.config.knowledge_bases; + knowledgeBases = allKnowledgeBases.filter(function (kb) { + return allowedIds.indexOf(kb.id) !== -1; + }); + } else { + // 'all' 或未设置:显示全部 + knowledgeBases = allKnowledgeBases.slice(); + } + } else { + knowledgeBases = allKnowledgeBases.slice(); + } + renderKbDropdown(); + } + + function findAgentById(id) { + for (var i = 0; i < agents.length; i++) { + if (agents[i].id === id) return agents[i]; + } + return null; + } + + function renderKbDropdown() { + var kbMenu = $('sp-kb-menu'); + if (!kbMenu) return; + + // 清除旧的知识库选项 + var items = kbMenu.querySelectorAll('.sp-dropdown-item'); + items.forEach(function (item) { item.remove(); }); + + var firstDivider = kbMenu.querySelector('.sp-dropdown-divider'); + + if (knowledgeBases.length === 0) { + // 没有知识库时只显示一个"全部"选项 + var allItem = createKbItem('all', '全部知识库', true); + kbMenu.insertBefore(allItem, firstDivider); + selectedKbId = 'all'; + $('sp-kb-name').textContent = '全部'; + return; + } + + // 插入 "全部知识库" 选项 + var allItem = createKbItem('all', '全部知识库', true); + kbMenu.insertBefore(allItem, firstDivider); + + // 插入真实知识库 + knowledgeBases.forEach(function (kb) { + var item = createKbItem(kb.id, kb.name, false); + kbMenu.insertBefore(item, firstDivider); + }); + + // 重置选择 + selectedKbId = 'all'; + $('sp-kb-name').textContent = '全部'; + } + + function createKbItem(kbId, name, isSelected) { + var div = document.createElement('div'); + div.className = 'sp-dropdown-item' + (isSelected ? ' selected' : ''); + div.setAttribute('data-kb', kbId); + div.innerHTML = ' ' + escapeHtml(name); + div.addEventListener('click', function (e) { + e.stopPropagation(); + selectedKbId = kbId; + $('sp-kb-name').textContent = name.length > 4 ? name.substring(0, 4) : name; + var allItems = $('sp-kb-menu').querySelectorAll('.sp-dropdown-item'); + allItems.forEach(function (i) { i.classList.remove('selected'); }); + div.classList.add('selected'); + $('sp-kb-menu').classList.remove('show'); + }); + return div; + } + + // === 渲染智能体下拉(替换静态模式选项)=== + function renderAgentDropdown() { + var modeMenu = $('sp-mode-menu'); + if (!modeMenu || agents.length === 0) return; + + // 清除旧的模式选项 + var modeItems = modeMenu.querySelectorAll('.sp-mode-item'); + modeItems.forEach(function (item) { item.remove(); }); + + agents.forEach(function (agent, idx) { + var item = document.createElement('div'); + var isQuickAnswer = agent.id === 'builtin-quick-answer' || (agent.config && agent.config.agent_mode === 'quick-answer'); + var isSelected = selectedAgentId ? (agent.id === selectedAgentId) : (idx === 0); + item.className = 'sp-mode-item' + (isSelected ? ' selected' : ''); + item.setAttribute('data-agent-id', agent.id); + item.innerHTML = ' ' + escapeHtml(agent.name); + if (isSelected) { + $('sp-mode-name').textContent = agent.name; + } + item.addEventListener('click', function (e) { + e.stopPropagation(); + selectedAgentId = agent.id; + selectedAgentEnabled = !isQuickAnswer; + selectedAgentImageUpload = !!(agent.config && agent.config.image_upload_enabled); + updateImageUploadUI(); + modeMenu.querySelectorAll('.sp-mode-item').forEach(function (i) { i.classList.remove('selected'); }); + item.classList.add('selected'); + $('sp-mode-name').textContent = agent.name; + modeMenu.classList.remove('show'); + // 持久化模式选择,同步给其他页面 + chrome.storage.local.set({ ka_selected_agent: { agentId: agent.id, agentEnabled: !isQuickAnswer } }); + // 切换智能体后根据配置过滤知识库 + filterAndRenderKbs(); + }); + modeMenu.appendChild(item); + }); + } // === 消息发送 === inputEl.addEventListener('input', function () { sendBtn.classList.toggle('active', inputEl.value.trim().length > 0); + // 自动调整高度 + inputEl.style.height = 'auto'; + inputEl.style.height = Math.min(inputEl.scrollHeight, 120) + 'px'; }); inputEl.addEventListener('keydown', function (e) { @@ -21,44 +246,737 @@ } }); - sendBtn.addEventListener('click', sendMessage); + sendBtn.addEventListener('click', function () { sendMessage(); }); - function sendMessage(text) { - var query = text || inputEl.value.trim(); - if (!query) return; + function sendMessage(text, externalImages) { + var query = (typeof text === 'string' && text) ? text : inputEl.value.trim(); + if (!query || isSending) return; if (welcomeEl) welcomeEl.style.display = 'none'; - appendMsg('user', query); + // 图片来源:本地待上传 or 外部传入(popup base64) + var msgImageUrls = pendingImages.map(function (img) { return img.preview; }); + var hasExternalImages = externalImages && externalImages.length > 0; + if (hasExternalImages) { + msgImageUrls = externalImages.map(function (img) { return img.data; }); + } + + appendMsg('user', query, false, msgImageUrls.length > 0 ? msgImageUrls : null); inputEl.value = ''; + inputEl.style.height = 'auto'; sendBtn.classList.remove('active'); showTyping(true); - var replied = false; + isSending = true; + streamingText = ''; + streamingBotDiv = null; + streamingReferences = []; + renderPending = false; + agentEventStream = []; + agentContainer = null; + agentExpandedIds = {}; + agentActiveThinkingIds = {}; + agentHasAnswer = false; + agentTreeExpanded = false; + agentRefsExpanded = false; + agentAnswerText = ''; + agentAnswerDone = false; + currentRequestId = Date.now().toString(36) + Math.random().toString(36).slice(2, 6); - // 发送到 background - chrome.runtime.sendMessage({ type: 'CHAT_QUERY', payload: { query: query } }, function (resp) { - if (replied) return; - if (resp && resp.success && resp.data) { - replied = true; + // 构建请求参数 + var payload = { + query: query, + sessionId: currentSessionId, + agentId: selectedAgentId, + agentEnabled: selectedAgentEnabled, + _requestId: currentRequestId + }; + if (selectedKbId && selectedKbId !== 'all') { + payload.knowledgeBaseIds = [selectedKbId]; + } + + // 外部传入的图片直接使用 + if (hasExternalImages) { + payload.images = externalImages; + doSendPayload(payload); + } else if (pendingImages.length > 0) { + // 本地待上传图片,先转 base64 再发送 + var promises = pendingImages.map(function (img) { return fileToBase64(img.file); }); + Promise.all(promises).then(function (dataURIs) { + payload.images = dataURIs.map(function (d) { return { data: d }; }); + clearImages(true); + doSendPayload(payload); + }).catch(function () { + showToast('图片读取失败'); + isSending = false; showTyping(false); - appendMsg('bot', resp.data); - } - }); - - // 超时兜底:3 秒无回复则显示模拟回复 - setTimeout(function () { - if (replied) return; - replied = true; - showTyping(false); - appendMsg('bot', '收到你的问题:「' + query + '」\n\n目前知识库正在配置中,稍后将为你提供准确回答。'); - }, 3000); + }); + } else { + doSendPayload(payload); + } } - function appendMsg(role, text) { + function doSendPayload(payload) { + // 发送到 background 进行真实 API 调用 + chrome.runtime.sendMessage({ type: 'CHAT_QUERY', payload: payload }, function (resp) { + void chrome.runtime.lastError; + isSending = false; + showTyping(false); + + if (resp && resp.success) { + if (resp.sessionId) { + currentSessionId = resp.sessionId; + } + if (agentContainer) { + agentAnswerDone = true; + renderAgentStream(); + } else if (streamingBotDiv && streamingText) { + streamingBotDiv.innerHTML = renderMarkdown(streamingText); + hydrateFileImages(streamingBotDiv); + messagesEl.scrollTop = messagesEl.scrollHeight; + } else if (resp.data && !streamingBotDiv && !agentContainer) { + appendMsg('bot', resp.data, true); + } + streamingBotDiv = null; + streamingText = ''; + } else { + if (agentContainer) { agentContainer.remove(); agentContainer = null; } + if (streamingBotDiv) { streamingBotDiv.remove(); streamingBotDiv = null; streamingText = ''; } + var errMsg = (resp && resp.error) || '请求失败,请检查服务配置'; + appendMsg('bot', errMsg); + } + }); + } + + // === 处理来自 background 的流式推送(参考 AgentStreamDisplay.vue)=== + function cleanThinkingContent(text) { + if (!text) return ''; + return text.replace(/Calling tool:\s*\w*/g, '').replace(/\n{3,}/g, '\n\n').trim(); + } + + function resolveAllPending() { + for (var k = 0; k < agentEventStream.length; k++) { + var e = agentEventStream[k]; + if (e._type === 'tool_call' && e.pending) { e.pending = false; e.success = true; e.output = e.output || ''; } + } + } + + function resolveOldPending() { + var foundLatest = false; + for (var k = agentEventStream.length - 1; k >= 0; k--) { + var e = agentEventStream[k]; + if (e._type === 'tool_call' && e.pending) { + if (!foundLatest) { foundLatest = true; continue; } + e.pending = false; e.success = true; e.output = e.output || ''; + } + } + } + + function handleStreamChunk(payload) { + if (!payload) return; + if (payload.requestId && payload.requestId !== currentRequestId) return; + var type = payload.responseType; + + if (payload.sessionId && !currentSessionId) { + currentSessionId = payload.sessionId; + } + + if (type === 'answer') { + agentAnswerText += payload.content || ''; + if (payload.done) agentAnswerDone = true; + if (!agentHasAnswer) { + agentHasAnswer = true; + showTyping(false); + collapseActiveThinking(); + collapseAllExpanded(); + resolveAllPending(); + agentContainer && (agentContainer._stepsChanged = true); + } + scheduleAgentRender(); + + } else if (type === 'thinking' || (type === 'tool_call' && payload.toolName === 'thinking')) { + if (agentEventStream.length > 0) setTypingText(''); + var thinkContent = cleanThinkingContent(payload.content); + if (!thinkContent || /^Thought process recorded/i.test(thinkContent)) return; + var lastEv = agentEventStream.length > 0 ? agentEventStream[agentEventStream.length - 1] : null; + if (lastEv && lastEv._type === 'thinking') { + lastEv.content += thinkContent; + } else { + collapseAllExpanded(); + var tid = 'think-' + Date.now() + '-' + Math.random().toString(36).slice(2, 5); + agentEventStream.push({ _type: 'thinking', id: tid, content: thinkContent }); + agentActiveThinkingIds[tid] = true; + agentContainer && (agentContainer._stepsChanged = true); + } + scheduleAgentRender(); + + } else if (type === 'tool_call') { + if (agentEventStream.length > 0) setTypingText(''); + // 按 eventId(tool_call_id) 去重:同一 tool_call_id 的第二次事件是参数补充,合并而非新建 + var existingTc = null; + if (payload.eventId) { + for (var ei = agentEventStream.length - 1; ei >= 0; ei--) { + if (agentEventStream[ei]._type === 'tool_call' && agentEventStream[ei].eventId === payload.eventId) { + existingTc = agentEventStream[ei]; break; + } + } + } + if (existingTc) { + if (payload.content) existingTc.content = payload.content; + } else { + collapseActiveThinking(); + collapseAllExpanded(); + resolveOldPending(); + var tcId = payload.eventId || ('tc-' + Date.now() + '-' + Math.random().toString(36).slice(2, 5)); + agentEventStream.push({ + _type: 'tool_call', id: tcId, eventId: payload.eventId || '', tool_name: payload.toolName || 'tool', + pending: true, success: null, content: payload.content || '', output: '' + }); + agentContainer && (agentContainer._stepsChanged = true); + } + scheduleAgentRender(); + + } else if (type === 'tool_result') { + if (payload.toolName === 'thinking') { + var thinkRes = cleanThinkingContent(payload.content); + if (thinkRes && !/^Thought process recorded/i.test(thinkRes)) { + var lastThink = agentEventStream.length > 0 ? agentEventStream[agentEventStream.length - 1] : null; + if (lastThink && lastThink._type === 'thinking') lastThink.content += thinkRes; + } + scheduleAgentRender(); + } else { + var matched = false; + for (var i = agentEventStream.length - 1; i >= 0; i--) { + var ev = agentEventStream[i]; + if (ev._type !== 'tool_call' || !ev.pending) continue; + var idMatch = payload.eventId && ev.eventId && ev.eventId === payload.eventId; + var nameMatch = payload.toolName && ev.tool_name === payload.toolName; + if (idMatch || nameMatch || !matched) { + ev.pending = false; ev.success = payload.success !== false; ev.output = payload.content || ''; + matched = true; break; + } + } + agentContainer && (agentContainer._stepsChanged = true); + scheduleAgentRender(); + } + + } else if (type === 'references') { + if (Array.isArray(payload.references) && payload.references.length > 0) { + streamingReferences = payload.references; + scheduleAgentRender(); + } + + } else if (type === 'complete') { + showTyping(false); + collapseActiveThinking(); + collapseAllExpanded(); + resolveAllPending(); + agentAnswerDone = true; + agentContainer && (agentContainer._stepsChanged = true); + scheduleAgentRender(); + + } else if (type === 'error') { + showTyping(false); + if (agentContainer) { agentContainer.remove(); agentContainer = null; } + if (streamingBotDiv) { streamingBotDiv.remove(); streamingBotDiv = null; streamingText = ''; } + appendMsg('bot', payload.content || '请求出错'); + + } else if (type === 'session_title') { + var title = payload.content || ''; + if (title) { + updateSessionTitle(title); + } + } + } + + function collapseActiveThinking() { + for (var key in agentActiveThinkingIds) { + delete agentExpandedIds[key]; + } + agentActiveThinkingIds = {}; + agentContainer && (agentContainer._stepsChanged = true); + } + + function collapseAllExpanded() { + agentExpandedIds = {}; + agentContainer && (agentContainer._stepsChanged = true); + } + + var _agentRenderTimer = null; + var _lastRenderTime = 0; + var RENDER_THROTTLE = 60; + function scheduleAgentRender() { + var now = Date.now(); + var elapsed = now - _lastRenderTime; + if (elapsed >= RENDER_THROTTLE) { + if (_agentRenderTimer) { clearTimeout(_agentRenderTimer); _agentRenderTimer = null; } + _lastRenderTime = now; + renderAgentStream(); + } else if (!_agentRenderTimer) { + _agentRenderTimer = setTimeout(function () { + _agentRenderTimer = null; + _lastRenderTime = Date.now(); + renderAgentStream(); + }, RENDER_THROTTLE - elapsed); + } + } + + // === 工具名称映射(参考 AgentStreamDisplay.vue)=== + var TOOL_NAME_DISPLAY = { + knowledge_search: '语义搜索', search_knowledge: '语义搜索', + grep_chunks: '文本搜索', list_knowledge_chunks: '阅读文档', + get_document_info: '获取文档信息', get_document_content: '获取文档内容', + get_related_documents: '查找关联文档', + query_knowledge_graph: '知识图谱查询', + web_search: '网络搜索', web_fetch: '网页抓取', + todo_write: '制定计划', final_answer: '生成回答', + thinking: '思考', read_skill: '读取技能', + execute_skill_script: '执行技能脚本', + data_analysis: '数据分析', data_schema: '数据结构', + database_query: '数据库查询', image_analysis: '查看图片内容', + knowledge_graph_extract: '知识图谱提取' + }; + + var TOOL_ICONS = { + '语义搜索': '', + '文本搜索': '', + '阅读文档': '', + '获取文档信息': '', + '网络搜索': '', + '网页抓取': '', + '制定计划': '', + '思考': '', + '数据分析': '', + '查看图片内容': '' + }; + var ICON_AGENT = ''; + var ICON_CHEVRON = ''; + var DEFAULT_TOOL_ICON = ''; + var ICON_COPY = ''; + + function getToolDisplayName(rawName) { + return TOOL_NAME_DISPLAY[rawName] || rawName; + } + + function getToolIcon(displayName) { + return TOOL_ICONS[displayName] || DEFAULT_TOOL_ICON; + } + + function getThinkingSummary(content) { + if (!content) return ''; + var cleaned = content.replace(/^#+\s+/gm, '').replace(/\*\*/g, '').replace(/\*/g, '') + .replace(/`/g, '').replace(/\n+/g, ' ').trim(); + return cleaned.length <= 50 ? cleaned : cleaned.slice(0, 50) + '…'; + } + + function extractToolSummary(ev) { + if (!ev.output) return ''; + try { + var parsed = JSON.parse(ev.output); + if (parsed.results && Array.isArray(parsed.results)) return '找到 ' + parsed.results.length + ' 条结果'; + if (parsed.total_matches !== undefined) return '匹配 ' + parsed.total_matches + ' 条'; + if (parsed.total !== undefined) return '找到 ' + parsed.total + ' 条结果'; + } catch (e) {} + var summary = ev.output.replace(/\n/g, ' ').trim(); + return summary.length > 60 ? summary.substring(0, 60) + '…' : summary; + } + + // === Agent 流式渲染 — 增量 DOM 更新(避免 innerHTML 全量重建导致图片重刷)=== + function renderAgentStream() { + if (agentEventStream.length === 0 && !agentAnswerText) return; + + if (!agentContainer) { + agentContainer = document.createElement('div'); + agentContainer.className = 'agent-stream'; + messagesEl.insertBefore(agentContainer, typingEl); + agentContainer._stepsEl = document.createElement('div'); + agentContainer.appendChild(agentContainer._stepsEl); + } + + var changed = false; + + // --- 1. 渲染中间步骤(仅当事件变化时更新)--- + if (!agentContainer._lastStepCount || agentContainer._lastStepCount !== agentEventStream.length || + agentContainer._stepsChanged || (agentHasAnswer && !agentContainer._treeMode)) { + renderAgentSteps(); + if (agentHasAnswer) agentContainer._treeMode = true; + changed = true; + } else { + var lastEv = agentEventStream.length > 0 ? agentEventStream[agentEventStream.length - 1] : null; + if (lastEv && lastEv._type === 'thinking' && agentActiveThinkingIds[lastEv.id]) { + renderAgentSteps(); + changed = true; + } + } + + // --- 2. 增量渲染回答内容(有图片,必须保持 DOM 稳定)--- + if (renderAgentAnswer()) changed = true; + + if (changed) messagesEl.scrollTop = messagesEl.scrollHeight; + } + + function renderAgentSteps() { + var stepsEl = agentContainer._stepsEl; + var events = agentEventStream; + var lastEv = events.length > 0 ? events[events.length - 1] : null; + + // 如果只是活跃的思考内容在更新(事件数量没变、无状态切换),就地更新而非重建 + if (lastEv && lastEv._type === 'thinking' && + agentContainer._lastStepCount === events.length && + !agentContainer._stepsChanged && !agentHasAnswer) { + var activeCard = stepsEl.querySelector('.agent-card[data-eid="' + lastEv.id + '"]'); + if (activeCard) { + var detailEl = activeCard.querySelector('.agent-card-detail'); + if (detailEl) { + detailEl.textContent = lastEv.content; + detailEl.scrollTop = detailEl.scrollHeight; + } + return; + } + } + + // 需要全量重建 + agentContainer._lastStepCount = events.length; + var wasCollapsed = agentContainer._stepsChanged; + agentContainer._stepsChanged = false; + + if (!wasCollapsed) { + stepsEl.querySelectorAll('.agent-card.expanded').forEach(function (el) { + var eid = el.getAttribute('data-eid'); + if (eid) agentExpandedIds[eid] = true; + }); + if (stepsEl.querySelector('.agent-tree-root.expanded')) agentTreeExpanded = true; + } + + var intermediateEvents = events.filter(function (e) { return e._type !== 'error'; }); + var stepCount = intermediateEvents.length; + var html = ''; + + if (agentHasAnswer && stepCount > 0) { + html += '
      '; + html += '' + ICON_AGENT + '智能体执行 ' + stepCount + ' 步'; + html += '' + ICON_CHEVRON + ''; + html += '
      '; + html += '
      '; + for (var ti = 0; ti < intermediateEvents.length; ti++) { + var isLast = ti === intermediateEvents.length - 1; + html += '
      '; + html += '
      '; + html += buildEventCardHTML(intermediateEvents[ti], true); + html += '
      '; + } + html += '
      '; + } else if (!agentHasAnswer) { + for (var fi = 0; fi < events.length; fi++) { + html += buildEventCardHTML(events[fi], false); + } + } + + stepsEl.innerHTML = html; + bindStepEvents(stepsEl); + + // 活跃思考卡片自动滚动到底部 + for (var aid in agentActiveThinkingIds) { + var thinkCard = stepsEl.querySelector('.agent-card[data-eid="' + aid + '"]'); + if (thinkCard) { + var det = thinkCard.querySelector('.agent-card-detail'); + if (det) det.scrollTop = det.scrollHeight; + } + } + } + + function bindStepEvents(stepsEl) { + var treeRoot = stepsEl.querySelector('.agent-tree-root'); + if (treeRoot) { + treeRoot.addEventListener('click', function () { + agentTreeExpanded = !agentTreeExpanded; + treeRoot.classList.toggle('expanded'); + var children = treeRoot.nextElementSibling; + if (children && children.classList.contains('agent-tree-children')) { + children.style.display = agentTreeExpanded ? 'block' : 'none'; + } + }); + } + stepsEl.querySelectorAll('.agent-card-header:not(.no-expand)').forEach(function (hdr) { + hdr.addEventListener('click', function () { + var card = hdr.closest('.agent-card'); + if (!card) return; + var eid = card.getAttribute('data-eid'); + card.classList.toggle('expanded'); + if (eid) { + if (card.classList.contains('expanded')) agentExpandedIds[eid] = true; + else delete agentExpandedIds[eid]; + } + }); + }); + } + + var ANSWER_RENDER_INTERVAL = 20; + function renderAgentAnswer() { + if (!agentAnswerText) return false; + + if (!agentContainer._answerWrapper) { + var wrapper = document.createElement('div'); + wrapper.className = 'agent-answer'; + var content = document.createElement('div'); + content.className = 'agent-answer-content'; + wrapper.appendChild(content); + agentContainer.appendChild(wrapper); + agentContainer._answerWrapper = wrapper; + agentContainer._answerContent = content; + agentContainer._lastAnswerLen = 0; + agentContainer._lastAnswerRenderTime = 0; + agentContainer._answerTimer = null; + } + + var newLen = agentAnswerText.length; + if (newLen === (agentContainer._lastAnswerLen || 0)) return false; + + var now = Date.now(); + var timeSince = now - (agentContainer._lastAnswerRenderTime || 0); + + if (agentAnswerDone || timeSince >= ANSWER_RENDER_INTERVAL) { + if (agentContainer._answerTimer) { clearTimeout(agentContainer._answerTimer); agentContainer._answerTimer = null; } + agentContainer._lastAnswerLen = newLen; + agentContainer._lastAnswerRenderTime = now; + agentContainer._answerContent.innerHTML = renderMarkdown(agentAnswerText); + hydrateFileImages(agentContainer._answerContent); + + // 回答完成后添加工具栏(只添加一次) + if (agentAnswerDone && !agentContainer._toolbarEl) { + var toolbar = document.createElement('div'); + toolbar.className = 'agent-answer-toolbar'; + toolbar.innerHTML = ''; + toolbar.querySelector('.btn-copy-answer').addEventListener('click', function () { + copyToClipboard(agentAnswerText || ''); + }); + agentContainer._answerWrapper.appendChild(toolbar); + agentContainer._toolbarEl = toolbar; + } + + return true; + } + if (!agentContainer._answerTimer) { + agentContainer._answerTimer = setTimeout(function () { + agentContainer._answerTimer = null; + if (renderAgentAnswer()) messagesEl.scrollTop = messagesEl.scrollHeight; + }, ANSWER_RENDER_INTERVAL - timeSince); + } + return false; + } + + function buildEventCardHTML(ev, inTree) { + var isActive = !!agentActiveThinkingIds[ev.id]; + var isExpanded = isActive || !!agentExpandedIds[ev.id]; + var cls = 'agent-card'; + if (isExpanded) cls += ' expanded'; + if (ev.pending) cls += ' pending'; + if (ev.success === false) cls += ' error'; + + var html = '
      '; + + if (ev._type === 'thinking') { + var thinkIcon = getToolIcon('思考'); + html += '
      '; + html += '
      ' + thinkIcon + '思考'; + if (isActive) html += ''; + html += '
      '; + if (ev.content) html += '' + ICON_CHEVRON + ''; + html += '
      '; + if (ev.content) { + html += '
      ' + escapeHtml(ev.content) + '
      '; + } + + } else if (ev._type === 'tool_call') { + var displayName = getToolDisplayName(ev.tool_name); + var icon = getToolIcon(displayName); + var hasDetail = false; + + if (ev.pending) { + html += '
      '; + html += '
      ' + icon + '正在调用 ' + escapeHtml(displayName) + '…
      '; + html += ''; + html += '
      '; + } else { + hasDetail = !!ev.output; + html += '
      '; + html += '
      ' + icon + '' + escapeHtml(displayName) + ''; + html += '
      '; + if (hasDetail) html += '' + ICON_CHEVRON + ''; + html += '
      '; + if (hasDetail) { + html += '
      ' + escapeHtml(ev.output).substring(0, 600) + '
      '; + } + } + } + + html += '
      '; + return html; + } + + // buildReferencesHTML 和 bindAgentEvents 已拆解到 renderAgentSteps / renderAgentAnswer 中 + + function copyToClipboard(text) { + if (!text) return; + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(text).then(function () { + showToast('已复制'); + }).catch(function () { fallbackCopy(text); }); + } else { + fallbackCopy(text); + } + } + + function fallbackCopy(text) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.style.cssText = 'position:fixed;left:-9999px;'; + document.body.appendChild(ta); + ta.select(); + try { document.execCommand('copy'); showToast('已复制'); } catch (e) {} + document.body.removeChild(ta); + } + + function showToast(msg) { + var toast = document.createElement('div'); + toast.textContent = msg; + toast.style.cssText = 'position:fixed;top:60px;left:50%;transform:translateX(-50%);background:#07C160;color:#fff;padding:6px 18px;border-radius:20px;font-size:12px;z-index:9999;animation:agentFadeIn 0.2s;'; + document.body.appendChild(toast); + setTimeout(function () { toast.remove(); }, 1500); + } + + // === 图片上传 === + var imageInput = $('sp-image-input'); + var imageBtn = $('sp-image-btn'); + var imagePreviews = $('sp-image-previews'); + + function addImageFiles(files) { + if (!selectedAgentImageUpload) return; + for (var i = 0; i < files.length; i++) { + if (pendingImages.length >= MAX_IMAGES) { + showToast('最多上传 ' + MAX_IMAGES + ' 张图片'); + break; + } + var f = files[i]; + if (ALLOWED_IMAGE_TYPES.indexOf(f.type) === -1) { + showToast('仅支持 JPG/PNG/GIF/WebP 格式'); + continue; + } + if (f.size > MAX_IMAGE_SIZE) { + showToast('图片大小不能超过 10MB'); + continue; + } + pendingImages.push({ file: f, preview: URL.createObjectURL(f) }); + } + renderImagePreviews(); + } + + function removeImage(index) { + if (index >= 0 && index < pendingImages.length) { + URL.revokeObjectURL(pendingImages[index].preview); + pendingImages.splice(index, 1); + } + renderImagePreviews(); + } + + function clearImages(skipRevoke) { + if (!skipRevoke) { + for (var i = 0; i < pendingImages.length; i++) { + URL.revokeObjectURL(pendingImages[i].preview); + } + } + pendingImages = []; + renderImagePreviews(); + } + + function renderImagePreviews() { + if (!imagePreviews) return; + if (pendingImages.length === 0) { + imagePreviews.style.display = 'none'; + imagePreviews.innerHTML = ''; + return; + } + imagePreviews.style.display = 'flex'; + var html = ''; + for (var i = 0; i < pendingImages.length; i++) { + html += '
      ' + + '' + + '×' + + '
      '; + } + imagePreviews.innerHTML = html; + imagePreviews.querySelectorAll('.sp-img-remove').forEach(function (btn) { + btn.addEventListener('click', function () { + var idx = parseInt(btn.parentElement.getAttribute('data-idx')); + removeImage(idx); + }); + }); + } + + function updateImageUploadUI() { + if (imageBtn) imageBtn.style.display = selectedAgentImageUpload ? '' : 'none'; + if (!selectedAgentImageUpload && pendingImages.length > 0) clearImages(); + } + + function fileToBase64(file) { + return new Promise(function (resolve, reject) { + var reader = new FileReader(); + reader.onload = function () { resolve(reader.result); }; + reader.onerror = reject; + reader.readAsDataURL(file); + }); + } + + if (imageInput) { + imageInput.addEventListener('change', function () { + if (imageInput.files) addImageFiles(Array.from(imageInput.files)); + imageInput.value = ''; + }); + } + if (imageBtn) { + imageBtn.addEventListener('click', function () { + if (imageInput) imageInput.click(); + }); + } + + // 粘贴图片 + inputEl.addEventListener('paste', function (e) { + if (!selectedAgentImageUpload) return; + var items = e.clipboardData && e.clipboardData.items; + if (!items) return; + var imageFiles = []; + for (var i = 0; i < items.length; i++) { + if (items[i].type.indexOf('image/') === 0) { + var f = items[i].getAsFile(); + if (f) imageFiles.push(f); + } + } + if (imageFiles.length > 0) { + e.preventDefault(); + addImageFiles(imageFiles); + } + }); + + function appendMsg(role, text, isMarkdown, imageUrls) { var div = document.createElement('div'); div.className = 'msg msg-' + (role === 'user' ? 'user' : 'bot'); - div.textContent = text; + if (role === 'bot' && isMarkdown) { + div.innerHTML = renderMarkdown(text); + } else { + div.textContent = text; + } + // 在用户消息中显示上传的图片 + if (role === 'user' && imageUrls && imageUrls.length > 0) { + var imgWrap = document.createElement('div'); + imgWrap.className = 'msg-user-images'; + imageUrls.forEach(function (url) { + var img = document.createElement('img'); + img.src = url; + img.className = 'msg-user-img'; + imgWrap.appendChild(img); + }); + div.appendChild(imgWrap); + } messagesEl.insertBefore(div, typingEl); messagesEl.scrollTop = messagesEl.scrollHeight; } @@ -73,37 +991,364 @@ + '
      '; card.querySelector('.save-clip-btn').addEventListener('click', function () { - chrome.runtime.sendMessage({ - type: 'SAVE_NOTE', - payload: { type: 'clip', content: '## ' + title + '\n\n' + content } - }, function (resp) { - if (resp && resp.success) { - this.textContent = '已保存'; - this.disabled = true; - } - }.bind(this)); + var btn = this; + // 优先用对话中选中的 KB,其次用剪藏设置的 KB + var targetKbId = (selectedKbId && selectedKbId !== 'all') ? selectedKbId : ''; + if (targetKbId) { + doSaveClip(btn, targetKbId, title, content); + } else { + // 从 storage 读取剪藏知识库 + chrome.storage.local.get(['clipKbId'], function (data) { + if (data.clipKbId) { + doSaveClip(btn, data.clipKbId, title, content); + } else { + saveToLocal(btn, title, content); + } + }); + } }); + function doSaveClip(btn, kbId, clipTitle, clipContent) { + // 获取当前页面 URL + chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) { + void chrome.runtime.lastError; + var pageUrl = (tabs && tabs[0]) ? tabs[0].url : ''; + chrome.runtime.sendMessage({ + type: 'SAVE_CLIP_TO_KB', + payload: { kbId: kbId, title: clipTitle, content: clipContent, url: pageUrl } + }, function (resp) { + void chrome.runtime.lastError; + if (resp && resp.success) { + btn.textContent = '已保存到知识库'; + btn.disabled = true; + } else { + saveToLocal(btn, clipTitle, clipContent); + } + }); + }); + } + messagesEl.insertBefore(card, typingEl); messagesEl.scrollTop = messagesEl.scrollHeight; } + function saveToLocal(btn, title, content) { + chrome.runtime.sendMessage({ + type: 'SAVE_NOTE', + payload: { type: 'clip', content: '## ' + title + '\n\n' + content } + }, function (resp) { + void chrome.runtime.lastError; + if (resp && resp.success) { + btn.textContent = '已保存'; + btn.disabled = true; + } + }); + } + function showTyping(show) { typingEl.classList.toggle('show', show); + if (!show) setTypingText(''); // 隐藏时重置文字 messagesEl.scrollTop = messagesEl.scrollHeight; } + function setTypingText(text) { + if (!typingEl) return; + // 找到或创建文字元素 + var textEl = typingEl.querySelector('.typing-text'); + if (!textEl && text) { + textEl = document.createElement('span'); + textEl.className = 'typing-text'; + textEl.style.cssText = 'font-size:12px;color:#999;margin-left:8px;'; + typingEl.appendChild(textEl); + } + if (textEl) { + textEl.textContent = text; + } + } + function escapeHtml(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; } + // === 文件 URL 转换:minio/cos/tos/local/s3 前缀走 /files 接口 === + var _fileBlobCache = {}; + var _fileFailedCache = {}; + + function hydrateFileImages(container) { + if (!container) return; + var imgs = container.querySelectorAll('img[data-file-src]'); + if (!imgs.length) return; + imgs.forEach(function (img) { + var fileSrc = img.getAttribute('data-file-src'); + if (!fileSrc) return; + if (_fileFailedCache[fileSrc]) { + img.removeAttribute('src'); + img.alt = '[图片加载失败]'; + img.style.display = 'none'; + return; + } + if (_fileBlobCache[fileSrc]) { + img.src = _fileBlobCache[fileSrc]; + return; + } + if (img.getAttribute('data-hydrated') === '1') return; + img.setAttribute('data-hydrated', '1'); + chrome.runtime.sendMessage({ type: 'FETCH_FILE', payload: { filePath: fileSrc } }, function (resp) { + void chrome.runtime.lastError; + if (resp && resp.success && resp.dataUrl) { + _fileBlobCache[fileSrc] = resp.dataUrl; + img.src = resp.dataUrl; + img.style.display = ''; + } else { + _fileFailedCache[fileSrc] = true; + img.alt = '[图片加载失败]'; + img.style.display = 'none'; + } + }); + }); + } + + // === Markdown 渲染(参考 botmsg.vue / AgentStreamDisplay.vue)=== + var MD_MAX_LEN = 100000; + function renderMarkdown(text) { + if (!text) return ''; + try { return _renderMarkdownInner(text); } catch (e) { + console.warn('renderMarkdown error:', e); + var safe = text.length > 2000 ? text.substring(0, 2000) + '\n…(渲染出错,已截断)' : text; + return '

      ' + escapeHtml(safe) + '

      '; + } + } + function _renderMarkdownInner(text) { + if (text.length > MD_MAX_LEN) text = text.substring(0, MD_MAX_LEN) + '\n\n…(内容过长,已截断)'; + + // 1. 提取代码块,替换为占位符(避免内部被解析) + var codeBlocks = []; + var processed = text.replace(/```(\w*)\n([\s\S]*?)```/g, function (_, lang, code) { + var idx = codeBlocks.length; + codeBlocks.push('
      ' + escapeHtml(code.replace(/\n$/, '')) + '
      '); + return '\x00CODEBLOCK' + idx + '\x00'; + }); + + // 2. 提取行内代码 + var inlineCodes = []; + processed = processed.replace(/`([^`]+)`/g, function (_, code) { + var idx = inlineCodes.length; + inlineCodes.push('' + escapeHtml(code) + ''); + return '\x00INLINE' + idx + '\x00'; + }); + + // 2.5 提取表格,替换为占位符 + var tables = []; + processed = processed.replace(/((?:\|.+\|\n)+)/g, function (tableBlock) { + var rows = tableBlock.trim().split('\n'); + if (rows.length < 2) return tableBlock; + var out = '
'; + rows.forEach(function (row, ri) { + if (/^\|[\s\-:|]+\|$/.test(row)) return; // 跳过分隔行 + var tag = ri === 0 ? 'th' : 'td'; + var cells = row.split('|').filter(function (c, ci, arr) { return ci > 0 && ci < arr.length - 1; }); + out += ''; + cells.forEach(function (cell) { out += '<' + tag + '>' + inlineFormat(cell.trim()) + ''; }); + out += ''; + }); + out += '
'; + var idx = tables.length; + tables.push(out); + return '\n\x00TABLE' + idx + '\x00\n'; + }); + + // 3. 逐行解析为块元素 + var lines = processed.split('\n'); + var html = ''; + var i = 0; + + while (i < lines.length) { + var line = lines[i]; + + // 代码块占位符 + var cbMatch = line.match(/^\x00CODEBLOCK(\d+)\x00$/); + if (cbMatch) { + html += codeBlocks[parseInt(cbMatch[1])]; + i++; continue; + } + + // 表格占位符 + var tbMatch = line.match(/^\x00TABLE(\d+)\x00$/); + if (tbMatch) { + html += '
' + tables[parseInt(tbMatch[1])] + '
'; + i++; continue; + } + + // 标题 + if (/^(#{1,6})\s+(.+)/.test(line)) { + var level = RegExp.$1.length; + html += '' + inlineFormat(RegExp.$2) + ''; + i++; continue; + } + + // 引用块(合并连续 > 行) + if (/^>\s*(.*)/.test(line)) { + var bqLines = []; + while (i < lines.length && /^>\s*(.*)/.test(lines[i])) { + bqLines.push(RegExp.$1); + i++; + } + html += '
' + inlineFormat(bqLines.join('
')) + '
'; + continue; + } + + // 无序列表(- 或 * 开头,需要和 *italic* 区分) + if (/^(\s*)[*\-]\s+(.+)/.test(line)) { + html += parseList(lines, i, 'ul'); + // 跳过已消费的行 + while (i < lines.length && /^(\s*)[*\-]\s+/.test(lines[i])) i++; + continue; + } + + // 有序列表 + if (/^(\s*)\d+\.\s+(.+)/.test(line)) { + html += parseList(lines, i, 'ol'); + while (i < lines.length && /^(\s*)\d+\.\s+/.test(lines[i])) i++; + continue; + } + + // 水平线 + if (/^[-*_]{3,}\s*$/.test(line.trim())) { + html += '
'; + i++; continue; + } + + // 空行 + if (line.trim() === '') { + i++; continue; + } + + // 普通段落(合并连续非空行,限制单段最大 500 行防溢出) + var pLines = []; + while (i < lines.length && pLines.length < 500) { + var pl = lines[i]; + if (pl.trim() === '' || /^#{1,6}\s/.test(pl) || /^>\s/.test(pl) || + /^(\s*)[*\-]\s+/.test(pl) || /^(\s*)\d+\.\s+/.test(pl) || + /^\x00CODEBLOCK/.test(pl) || /^\x00TABLE/.test(pl) || /^[-*_]{3,}\s*$/.test(pl.trim())) break; + pLines.push(pl); + i++; + } + try { + html += '

' + inlineFormat(pLines.join('
')) + '

'; + } catch (e) { + try { html += '

' + escapeHtml(pLines.slice(0, 5).join('\n')) + '…

'; } catch (_) { /* skip */ } + } + if (html.length > 2000000) { html += '

…(内容过长,已截断)

'; break; } + } + + // 4. 恢复行内代码占位符 + html = html.replace(/\x00INLINE(\d+)\x00/g, function (_, idx) { + return inlineCodes[parseInt(idx)]; + }); + + return html; + + // --- 列表解析(支持嵌套)--- + function parseList(allLines, startIdx, defaultType) { + var items = []; + var j = startIdx; + var baseIndent = -1; + var listTag = defaultType; + var re = listTag === 'ul' ? /^(\s*)[*\-]\s+(.+)/ : /^(\s*)\d+\.\s+(.+)/; + + while (j < allLines.length) { + var m = allLines[j].match(re); + if (!m) break; + var indent = m[1].length; + if (baseIndent < 0) baseIndent = indent; + if (indent > baseIndent) { + // 嵌套:找出子列表范围 + var subStart = j; + while (j < allLines.length) { + var sm = allLines[j].match(re); + if (!sm || sm[1].length < indent) break; + j++; + } + // 将子列表附加到上一个 li + var subHtml = parseList(allLines, subStart, listTag); + if (items.length > 0) { + items[items.length - 1] += subHtml; + } + continue; + } + if (indent < baseIndent) break; + items.push(inlineFormat(m[2])); + j++; + } + + var out = '<' + listTag + ' class="md-list">'; + for (var k = 0; k < items.length; k++) { + out += '
  • ' + items[k] + '
  • '; + } + out += ''; + return out; + } + + // --- 行内格式化 --- + function inlineFormat(s) { + if (!s) return ''; + s = s.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, function (_, alt, url) { + if (/^(minio|cos|tos|local|s3):\/\//.test(url)) { + if (_fileFailedCache[url]) return ''; + return '' + alt + ''; + } + return '' + alt + ''; + }); + // 链接 + s = s.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); + // 粗斜体 + s = s.replace(/\*\*\*(.+?)\*\*\*/g, '$1'); + // 粗体 + s = s.replace(/\*\*(.+?)\*\*/g, '$1'); + // 斜体(避免匹配列表 * ) + s = s.replace(/(?$1'); + // 删除线 + s = s.replace(/~~(.+?)~~/g, '$1'); + // 恢复行内代码占位符 + s = s.replace(/\x00INLINE(\d+)\x00/g, function (_, idx) { + return inlineCodes[parseInt(idx)]; + }); + return s; + } + } + // === 清空对话 === $('btn-clear').addEventListener('click', function () { - var msgs = messagesEl.querySelectorAll('.msg, .clip-card'); + // 先调用后端清空当前会话的消息 + if (currentSessionId) { + chrome.runtime.sendMessage({ + type: 'CLEAR_SESSION_MESSAGES', + payload: { sessionId: currentSessionId } + }, function (resp) { + if (resp && !resp.success) { + console.warn('清空会话消息失败:', resp.error); + } + }); + } + var msgs = messagesEl.querySelectorAll('.msg, .clip-card, .agent-stream'); msgs.forEach(function (m) { m.remove(); }); if (welcomeEl) welcomeEl.style.display = 'block'; + currentSessionId = null; + updateSessionTitle(''); + streamingReferences = []; + agentEventStream = []; + agentContainer = null; + agentExpandedIds = {}; + agentActiveThinkingIds = {}; + agentHasAnswer = false; + agentTreeExpanded = false; + agentRefsExpanded = false; + agentAnswerText = ''; + agentAnswerDone = false; + _fileFailedCache = {}; + chrome.storage.local.remove('ka_current_session'); }); // === 快捷问题 === @@ -117,13 +1362,9 @@ var kbBtn = $('sp-btn-kb'); var kbMenu = $('sp-kb-menu'); - // 知识库简称映射 - var kbShortNames = { 'all': '全部', 'quick-note': '笔记', 'web-collect': '收藏', 'product-doc': '文档', 'tech-spec': '规范' }; - // 定位下拉菜单 — 使用 fixed 定位避免 overflow 裁剪 function positionKbMenu() { var rect = kbBtn.getBoundingClientRect(); - kbMenu.style.display = 'block'; requestAnimationFrame(function () { var menuH = kbMenu.offsetHeight; var top = rect.top - menuH - 6; @@ -135,6 +1376,7 @@ kbBtn.addEventListener('click', function (e) { e.stopPropagation(); + modeMenu.classList.remove('show'); if (kbMenu.classList.contains('show')) { kbMenu.classList.remove('show'); } else { @@ -143,31 +1385,53 @@ } }); - // 知识库选项 + // 知识库选项(静态选项的兜底,动态加载后会覆盖) kbMenu.querySelectorAll('.sp-dropdown-item').forEach(function (item) { item.addEventListener('click', function (e) { e.stopPropagation(); var kb = item.getAttribute('data-kb'); - $('sp-kb-name').textContent = kbShortNames[kb] || item.textContent.trim(); + selectedKbId = kb; + $('sp-kb-name').textContent = item.textContent.trim().substring(0, 4); kbMenu.querySelectorAll('.sp-dropdown-item').forEach(function (i) { i.classList.remove('selected'); }); item.classList.add('selected'); kbMenu.classList.remove('show'); }); }); - // 模式选项 - kbMenu.querySelectorAll('.sp-mode-item').forEach(function (item) { - item.addEventListener('click', function (e) { - e.stopPropagation(); - kbMenu.querySelectorAll('.sp-mode-item').forEach(function (i) { i.classList.remove('selected'); }); - item.classList.add('selected'); - // 模式选中后不关闭菜单,让用户继续操作 + // 模式选项 — 静态选项的兜底,加载智能体后会被动态替换 + var modeBtn = $('sp-btn-mode'); + var modeMenu = $('sp-mode-menu'); + + function positionModeMenu() { + var rect = modeBtn.getBoundingClientRect(); + requestAnimationFrame(function () { + var menuH = modeMenu.offsetHeight; + var top = rect.top - menuH - 6; + if (top < 4) top = rect.bottom + 6; + modeMenu.style.left = rect.left + 'px'; + modeMenu.style.top = top + 'px'; }); + } + + modeBtn.addEventListener('click', function (e) { + e.stopPropagation(); + kbMenu.classList.remove('show'); + if (modeMenu.classList.contains('show')) { + modeMenu.classList.remove('show'); + } else { + modeMenu.classList.add('show'); + positionModeMenu(); + } + }); + + modeMenu.addEventListener('click', function (e) { + e.stopPropagation(); }); // 点击其他地方关闭下拉 document.addEventListener('click', function () { kbMenu.classList.remove('show'); + modeMenu.classList.remove('show'); }); // 菜单内部点击不冒泡关闭 @@ -175,35 +1439,96 @@ e.stopPropagation(); }); - // === 监听来自 popup / content 的消息 === + // === 同步 popup 传来的智能体/知识库选择到 sidepanel 状态 === + function applyPendingPayload(payload) { + if (payload.agentId) { + selectedAgentId = payload.agentId; + selectedAgentEnabled = !!payload.agentEnabled; + selectedAgentImageUpload = !!payload.agentImageUpload; + updateImageUploadUI(); + // 同步下拉 UI 选中状态 + var mItems = $('sp-mode-menu').querySelectorAll('.sp-mode-item'); + mItems.forEach(function (item) { + var isMatch = item.getAttribute('data-agent-id') === payload.agentId; + item.classList.toggle('selected', isMatch); + if (isMatch) { + $('sp-mode-name').textContent = item.textContent.trim(); + } + }); + } + if (payload.knowledgeBaseIds && payload.knowledgeBaseIds.length > 0) { + selectedKbId = payload.knowledgeBaseIds[0]; + } + } + + // === 监听来自 popup / content / background 的消息 === chrome.runtime.onMessage.addListener(function (msg, sender, sendResponse) { if (msg.type === 'CHAT_QUERY' && msg.payload) { - // 清除 storage 中的待处理标记(避免重复) chrome.storage.local.remove('ka_pending_query'); - sendMessage(msg.payload.query); + // 同步 popup 传来的智能体和知识库选择 + applyPendingPayload(msg.payload); + sendMessage(msg.payload.query, msg.payload.images); sendResponse({ success: true }); } if (msg.type === 'CLIP_RESULT' && msg.payload) { appendClipCard(msg.payload.title || '网页剪藏', msg.payload.content || ''); sendResponse({ success: true }); } + if (msg.type === 'CHAT_STREAM_CHUNK' && msg.payload) { + handleStreamChunk(msg.payload); + } return true; }); // === 初始化:检查是否有从 popup 传来的待处理问题 === - chrome.storage.local.get('ka_pending_query', function (data) { + chrome.storage.local.get(['ka_pending_query', 'ka_current_session'], function (data) { + // 恢复之前的会话 ID + if (data.ka_current_session) { + currentSessionId = data.ka_current_session; + } + if (data && data.ka_pending_query && data.ka_pending_query.query) { var pending = data.ka_pending_query; // 只处理 5 秒内的问题(防止旧数据) if (Date.now() - (pending.ts || 0) < 5000) { chrome.storage.local.remove('ka_pending_query'); + applyPendingPayload(pending); // 延迟一点让页面渲染完成 setTimeout(function () { - sendMessage(pending.query); + sendMessage(pending.query, pending.images); }, 200); } else { chrome.storage.local.remove('ka_pending_query'); } } }); + + // === 加载智能体列表(会自动链式加载知识库)=== + loadAgents(); + + // 缓存 baseUrl 用于文件 URL 转换 + chrome.runtime.sendMessage({ type: 'GET_CONFIG' }, function (resp) { + void chrome.runtime.lastError; + if (resp && resp.success && resp.data && resp.data.baseUrl) { + cachedBaseUrl = resp.data.baseUrl.replace(/\/+$/, ''); + } + }); + + // === 图片点击放大(灯箱)=== + messagesEl.addEventListener('click', function (e) { + var img = e.target; + if (img.tagName !== 'IMG') return; + if (!img.classList.contains('md-img') && !img.classList.contains('msg-user-img')) return; + var src = img.src; + if (!src) return; + var overlay = document.createElement('div'); + overlay.className = 'sp-lightbox'; + var bigImg = document.createElement('img'); + bigImg.src = src; + overlay.appendChild(bigImg); + document.body.appendChild(overlay); + overlay.addEventListener('click', function () { + overlay.remove(); + }); + }); })(); diff --git a/frontend/chrome-extension/快速笔记 5.0/weknora.html b/frontend/chrome-extension/快速笔记 5.0/weknora.html index ccf773e8..7f1e7cc1 100644 --- a/frontend/chrome-extension/快速笔记 5.0/weknora.html +++ b/frontend/chrome-extension/快速笔记 5.0/weknora.html @@ -106,6 +106,123 @@ body { font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; .chat-tool-btn svg { width: 12px; height: 12px; } .chat-tool-btn.mode-active { color: var(--primary); font-weight: 500; } .chat-tool-divider { width: 1px; height: 12px; background: #eee; margin: 0 2px; } + +/* 图片上传按钮 */ +.wk-image-upload-btn { + display: flex; align-items: center; justify-content: center; + border: none; background: none; color: #999; cursor: pointer; + width: 26px; height: 26px; padding: 0; border-radius: 6px; transition: all 0.15s; +} +.wk-image-upload-btn:hover { color: var(--primary); background: rgba(7,193,96,0.06); } +.wk-image-upload-btn svg { width: 14px; height: 14px; } + +/* 图片预览条 */ +.wk-image-previews { + display: flex; gap: 6px; padding: 6px 12px; overflow-x: auto; flex-shrink: 0; +} +.wk-img-item { position: relative; flex-shrink: 0; } +.wk-img-thumb { width: 44px; height: 44px; border-radius: 6px; object-fit: cover; border: 1px solid #e8e8e8; } +.wk-img-remove { + position: absolute; top: -4px; right: -4px; + width: 16px; height: 16px; border-radius: 50%; background: rgba(0,0,0,0.5); color: #fff; + font-size: 12px; line-height: 16px; text-align: center; cursor: pointer; border: none; padding: 0; +} +.wk-img-remove:hover { background: #e44; } + +/* === Markdown 渲染样式 === */ +.md-pre { background: #1e1e2e; color: #cdd6f4; border-radius: 8px; padding: 10px 12px; overflow-x: auto; font-size: 12px; margin: 6px 0; line-height: 1.5; } +.md-pre code { font-family: "SF Mono", "Fira Code", "Consolas", monospace; white-space: pre; } +.md-code { background: #f3f4f6; padding: 1px 5px; border-radius: 4px; font-size: 12px; font-family: "SF Mono", "Fira Code", "Consolas", monospace; } +.md-h { margin: 8px 0 4px; line-height: 1.4; } +h1.md-h { font-size: 18px; font-weight: 700; } +h2.md-h { font-size: 16px; font-weight: 700; } +h3.md-h { font-size: 14px; font-weight: 600; } +h4.md-h, h5.md-h, h6.md-h { font-size: 13px; font-weight: 600; } +.md-bq { border-left: 3px solid var(--primary); padding: 4px 12px; margin: 6px 0; background: #f8faf8; color: #666; border-radius: 0 4px 4px 0; font-size: 13px; } +.md-list { margin: 4px 0; padding-left: 20px; font-size: 13px; line-height: 1.6; } +.md-list li { margin: 2px 0; } +.md-list .md-list { margin: 2px 0 2px 0; } +.md-p { margin: 4px 0; line-height: 1.6; font-size: 13px; } +.md-p a, .md-list a, .md-bq a { color: var(--primary); text-decoration: none; } +.md-p a:hover, .md-list a:hover { text-decoration: underline; } +.md-table { border-collapse: collapse; margin: 6px 0; font-size: 12px; width: 100%; } +.md-table th { border: 1px solid #e5e5e5; padding: 4px 8px; background: #f7f8fa; font-weight: 600; text-align: left; } +.md-table td { border: 1px solid #e5e5e5; padding: 4px 8px; } +.md-table tr:hover td { background: rgba(7,193,96,0.02); } +.md-img { max-width: 100%; max-height: 200px; border-radius: 6px; margin: 6px 0; display: block; object-fit: contain; cursor: pointer; } + +/* === Agent 流式显示 === */ +.agent-stream { display: flex; flex-direction: column; gap: 0; max-width: 90%; margin-bottom: 6px; } +.agent-tree-root { + display: flex; justify-content: space-between; align-items: center; + padding: 6px 12px; font-size: 12px; font-weight: 500; cursor: pointer; + border-radius: 8px; background: #fff; border: 0.5px solid #e0e0e0; + box-shadow: 0 2px 4px rgba(7,192,95,0.08); color: var(--text); + transition: all 0.25s; margin-bottom: 8px; user-select: none; +} +.agent-tree-root:hover { background: rgba(7,192,95,0.04); } +.agent-tree-root-title { display: flex; align-items: center; gap: 6px; } +.agent-tree-root-title svg { width: 16px; height: 16px; color: var(--primary); flex-shrink: 0; } +.agent-tree-root-toggle { font-size: 12px; color: var(--primary); transition: transform 0.2s; } +.agent-tree-root.expanded .agent-tree-root-toggle { transform: rotate(180deg); } +.agent-tree-children { display: none; padding-left: 12px; margin-top: -4px; margin-bottom: 8px; } +.agent-tree-root.expanded + .agent-tree-children { display: block; } +.agent-tree-child { position: relative; padding-left: 20px; margin-bottom: 6px; } +.agent-tree-child::before { content: ''; position: absolute; left: 0; top: 0; bottom: 0; border-left: 1px dashed #e0e0e0; } +.agent-tree-child:last-child::before { bottom: auto; height: 15px; } +.agent-tree-branch { position: absolute; left: 0; top: 15px; width: 16px; border-top: 1px dashed #e0e0e0; } +.agent-card { + background: #fff; border-radius: 5px; border: 1px solid #e8e8e8; + overflow: hidden; transition: all 0.2s; box-shadow: 0 1px 2px rgba(0,0,0,0.02); + animation: agentFadeIn 0.25s ease-out; margin-bottom: 8px; position: relative; +} +.agent-card:hover { border-color: var(--primary); box-shadow: 0 1px 4px rgba(7,192,95,0.08); } +.agent-card.pending { border-color: rgba(7,192,95,0.15); } +.agent-card.pending::after { + content: ''; position: absolute; inset: 0; + background: linear-gradient(120deg, transparent 0%, rgba(7,192,95,0.06) 40%, rgba(7,192,95,0.08) 55%, transparent 85%); + transform: translateX(-100%); animation: agentShimmer 2.8s ease-in-out infinite; pointer-events: none; +} +.agent-card.error { border-left: 2px solid #e44; } +.agent-card-header { display: flex; justify-content: space-between; align-items: center; padding: 5px 10px; cursor: pointer; user-select: none; transition: background-color 0.15s; } +.agent-card-header:hover { background: rgba(7,192,95,0.03); } +.agent-card-header.no-expand { cursor: default; } +.agent-card-header.no-expand:hover { background: transparent; } +.agent-card-title { display: flex; align-items: center; gap: 6px; flex: 1; min-width: 0; } +.agent-card-title svg { width: 14px; height: 14px; color: var(--primary); flex-shrink: 0; } +.agent-card-name { font-size: 12px; font-weight: 500; white-space: nowrap; color: var(--text); overflow: hidden; text-overflow: ellipsis; } +.agent-card-summary { color: #999; font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; margin-left: 4px; } +.agent-card-toggle { font-size: 12px; color: #999; transition: transform 0.2s; padding: 0 2px; flex-shrink: 0; } +.agent-card.expanded .agent-card-toggle { transform: rotate(180deg); } +.agent-card-detail { display: none; padding: 6px 12px; border-top: 1px solid #f0f0f0; font-size: 12px; color: #555; line-height: 1.6; max-height: 200px; overflow-y: auto; background: #fafbfc; white-space: pre-wrap; word-break: break-word; } +.agent-card.expanded .agent-card-detail { display: block; } +.agent-card-info { padding: 5px 10px; border-top: 1px solid #f0f0f0; font-size: 11px; font-weight: 500; color: var(--text); } +.agent-card-info strong { color: var(--primary); font-weight: 600; } +.agent-answer { animation: agentFadeIn 0.25s ease-out; } +.agent-answer-content { font-size: 13px; color: #333; line-height: 1.6; background: #f7f8fa; padding: 10px 14px; border-radius: 12px; border-bottom-left-radius: 4px; } +.agent-answer-toolbar { display: flex; gap: 6px; margin-top: 6px; } +.agent-answer-toolbar button { display: flex; align-items: center; justify-content: center; width: 26px; height: 26px; border: 1px solid #e8e8e8; border-radius: 50%; background: #fff; cursor: pointer; color: #999; transition: all 0.2s; } +.agent-answer-toolbar button:hover { border-color: var(--primary); color: var(--primary); } +.agent-answer-toolbar button svg { width: 13px; height: 13px; } +.agent-refs { background: #fff; border: 1px solid #e8e8e8; border-radius: 10px; margin-top: 8px; overflow: hidden; font-size: 12px; } +.agent-refs-header { display: flex; align-items: center; gap: 6px; padding: 8px 12px; cursor: pointer; color: #555; font-weight: 500; user-select: none; } +.agent-refs-header:hover { background: rgba(7,193,96,0.04); } +.agent-refs-header svg:first-child { color: var(--primary); flex-shrink: 0; } +.agent-refs-arrow { margin-left: auto; transition: transform 0.2s; color: #999; } +.agent-refs.expanded .agent-refs-arrow { transform: rotate(180deg); } +.agent-refs-list { display: none; border-top: 1px solid #f0f0f0; padding: 6px 12px 8px; max-height: 200px; overflow-y: auto; } +.agent-refs.expanded .agent-refs-list { display: block; } +.agent-ref-group { margin-bottom: 6px; } +.agent-ref-group-title { display: flex; align-items: center; gap: 4px; font-weight: 500; color: var(--text); padding: 2px 0; font-size: 12px; } +.agent-ref-group-title svg { color: var(--primary); flex-shrink: 0; } +.agent-ref-count { color: #999; font-weight: 400; font-size: 11px; } +.agent-ref-chunk { color: #999; padding: 2px 0 2px 18px; line-height: 1.5; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: default; font-size: 11px; } +.agent-ref-chunk:hover { color: var(--primary); background: rgba(7,193,96,0.04); border-radius: 4px; } +.agent-ref-idx { color: var(--primary); font-weight: 500; font-size: 10px; } +@keyframes agentFadeIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } } +@keyframes agentShimmer { 0% { transform: translateX(-90%); } 50% { transform: translateX(-5%); } 100% { transform: translateX(90%); } } +@keyframes agentSpin { to { transform: rotate(360deg); } } +.agent-spinner { width: 12px; height: 12px; border: 2px solid #e0e0e0; border-top-color: var(--primary); border-radius: 50%; animation: agentSpin 0.8s linear infinite; display: inline-block; } @@ -146,10 +263,10 @@ body { font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
    @@ -236,9 +353,13 @@ body { font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;

    选择知识库后,输入问题开始智能问答

    +
    +
    + diff --git a/frontend/chrome-extension/快速笔记 5.0/weknora.js b/frontend/chrome-extension/快速笔记 5.0/weknora.js index a0229086..f2d5e270 100644 --- a/frontend/chrome-extension/快速笔记 5.0/weknora.js +++ b/frontend/chrome-extension/快速笔记 5.0/weknora.js @@ -5,6 +5,58 @@ function $(sel) { return document.querySelector(sel); } function $$(sel) { return document.querySelectorAll(sel); } + // === 状态 === + var knowledgeBases = []; + var agents = []; + var currentSessionId = null; + var chatMessages = []; + var wkSelectedAgentId = ''; + var wkSelectedAgentEnabled = false; + var wkSelectedAgentImageUpload = false; + + // 图片上传状态 + var wkPendingImages = []; + var WK_MAX_IMAGES = 5; + var WK_ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']; + var WK_MAX_IMAGE_SIZE = 10 * 1024 * 1024; + + // 流式消息状态 + var wkStreamingDiv = null; + var wkStreamingText = ''; + var wkRequestId = ''; + var wkRenderPending = false; + var wkStreamingReferences = []; + var wkCachedBaseUrl = ''; + + // Agent 事件流状态(参考 AgentStreamDisplay.vue) + var wkAgentEvents = []; + var wkAgentContainer = null; + var wkAgentExpandedIds = {}; + var wkAgentActiveThinkingIds = {}; + var wkAgentHasAnswer = false; + var wkAgentTreeExpanded = false; + var wkAgentRefsExpanded = false; + var wkAgentAnswerText = ''; + var wkAgentAnswerDone = false; + + // === Chrome 消息辅助 === + function sendMsg(data) { + return new Promise(function (resolve) { + if (chrome && chrome.runtime && chrome.runtime.sendMessage) { + try { + chrome.runtime.sendMessage(data, function (resp) { + void chrome.runtime.lastError; + resolve(resp || { success: false }); + }); + } catch (e) { + resolve({ success: false, error: e.message }); + } + } else { + resolve({ success: false, error: 'no chrome api' }); + } + }); + } + // === 左侧导航切换 === $$('.nav-item').forEach(function (item) { item.addEventListener('click', function () { @@ -18,27 +70,275 @@ tab.addEventListener('click', function () { $$('.content-tab').forEach(function (t) { t.classList.remove('active'); }); tab.classList.add('active'); + loadKnowledgeBases(); }); }); - // === 新建知识库按钮 === - var emptyBtn = $('.empty-btn'); - if (emptyBtn) { - emptyBtn.addEventListener('click', function () { - alert('新建知识库功能即将上线'); + // === 加载智能体列表 === + function loadAgentList() { + sendMsg({ type: 'LIST_AGENTS' }).then(function (resp) { + if (resp && resp.success && resp.data) { + agents = Array.isArray(resp.data) ? resp.data : (resp.data.data || []); + // 恢复持久化的模式选择 + chrome.storage.local.get('ka_selected_agent', function (data) { + if (data && data.ka_selected_agent && data.ka_selected_agent.agentId) { + var saved = data.ka_selected_agent; + // 验证保存的 agent 仍然存在 + for (var i = 0; i < agents.length; i++) { + if (agents[i].id === saved.agentId) { + wkSelectedAgentId = saved.agentId; + wkSelectedAgentEnabled = !!saved.agentEnabled; + break; + } + } + } + if (agents.length > 0 && !wkSelectedAgentId) { + wkSelectedAgentId = agents[0].id; + var isQA = agents[0].id === 'builtin-quick-answer' || (agents[0].config && agents[0].config.agent_mode === 'quick-answer'); + wkSelectedAgentEnabled = !isQA; + } + renderChatToolbar(); + // 更新图片上传按钮状态 + var selAgent = agents.find(function (a) { return a.id === wkSelectedAgentId; }); + wkSelectedAgentImageUpload = !!(selAgent && selAgent.config && selAgent.config.image_upload_enabled); + wkUpdateImageUploadUI(); + }); + } }); } - // === FAB 按钮 === - var fabBtn = $('.fab-btn'); - if (fabBtn) { - fabBtn.addEventListener('click', function () { - alert('新建知识库功能即将上线'); + // === 加载知识库列表 === + function loadKnowledgeBases() { + sendMsg({ type: 'LIST_KNOWLEDGE_BASES' }).then(function (resp) { + if (resp && resp.success && resp.data) { + knowledgeBases = Array.isArray(resp.data) ? resp.data : (resp.data.items || []); + renderKnowledgeBases(); + } else if (resp && resp.error) { + showEmptyState('加载失败: ' + resp.error); + } }); } + function renderKnowledgeBases() { + var body = $('.content-body'); + if (!body) return; + + if (knowledgeBases.length === 0) { + showEmptyState(); + return; + } + + // 构建知识库卡片网格 + body.innerHTML = '
    ' + + ''; + body.style.alignItems = 'stretch'; + body.style.justifyContent = 'flex-start'; + body.style.padding = '20px 24px'; + + var grid = body.querySelector('.kb-grid'); + grid.style.cssText = 'display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:16px;width:100%;'; + + knowledgeBases.forEach(function (kb) { + var card = document.createElement('div'); + card.style.cssText = 'background:#fff;border:1px solid #eaeaea;border-radius:12px;padding:16px;cursor:pointer;transition:all 0.2s;'; + card.innerHTML = '
    ' + escapeHtml(kb.name) + '
    ' + + '
    ' + escapeHtml(kb.description || '暂无描述') + '
    ' + + '
    ' + + '' + (kb.document_count || 0) + ' 文档' + + '' + (kb.chunk_count || 0) + ' 分段' + + '
    '; + card.addEventListener('mouseenter', function () { + card.style.borderColor = '#07C160'; + card.style.boxShadow = '0 4px 16px rgba(7,193,96,0.12)'; + }); + card.addEventListener('mouseleave', function () { + card.style.borderColor = '#eaeaea'; + card.style.boxShadow = 'none'; + }); + grid.appendChild(card); + }); + + // FAB 按钮 + var fab = body.querySelector('#fab-create'); + if (fab) { + fab.addEventListener('click', function () { + showCreateKbDialog(); + }); + } + } + + function showEmptyState(msg) { + var body = $('.content-body'); + if (!body) return; + body.style.alignItems = ''; + body.style.justifyContent = ''; + body.style.padding = ''; + body.innerHTML = '
    ' + + '' + + '' + + '' + + '' + + '' + + '
    ' + + '
    ' + (msg || '暂无知识库') + '
    ' + + '
    点击下方按钮创建第一个知识库
    ' + + '' + + ''; + + var createBtn = body.querySelector('#btn-create-kb'); + if (createBtn) createBtn.addEventListener('click', showCreateKbDialog); + var fab = body.querySelector('#fab-create2'); + if (fab) fab.addEventListener('click', showCreateKbDialog); + } + + function showCreateKbDialog() { + var name = prompt('请输入知识库名称:'); + if (!name || !name.trim()) return; + var desc = prompt('请输入知识库描述(可选):') || ''; + + sendMsg({ + type: 'CREATE_KNOWLEDGE_BASE', + payload: { name: name.trim(), description: desc.trim() } + }).then(function (resp) { + if (resp && resp.success) { + loadKnowledgeBases(); + } else { + alert('创建失败: ' + (resp.error || '未知错误')); + } + }); + } + + function escapeHtml(s) { + var d = document.createElement('div'); + d.textContent = s || ''; + return d.innerHTML; + } + // === 右侧对话面板 - 输入交互 === var chatTextarea = $('.chat-input-card textarea'); + var chatBody = $('.chat-panel-body'); + var wkImageInput = $('#wk-image-input'); + var wkImageBtn = $('#wk-image-btn'); + var wkImagePreviews = $('#wk-image-previews'); + + // === 图片上传 === + function wkShowToast(msg) { + var toast = document.createElement('div'); + toast.textContent = msg; + toast.style.cssText = 'position:fixed;top:60px;left:50%;transform:translateX(-50%);background:#07C160;color:#fff;padding:6px 18px;border-radius:20px;font-size:12px;z-index:9999;opacity:1;transition:opacity 0.3s;'; + document.body.appendChild(toast); + setTimeout(function () { toast.style.opacity = '0'; setTimeout(function () { toast.remove(); }, 300); }, 1500); + } + + function wkAddImageFiles(files) { + if (!wkSelectedAgentImageUpload) return; + for (var i = 0; i < files.length; i++) { + if (wkPendingImages.length >= WK_MAX_IMAGES) { + wkShowToast('最多上传 ' + WK_MAX_IMAGES + ' 张图片'); + break; + } + var f = files[i]; + if (WK_ALLOWED_TYPES.indexOf(f.type) === -1) { + wkShowToast('仅支持 JPG/PNG/GIF/WebP 格式'); + continue; + } + if (f.size > WK_MAX_IMAGE_SIZE) { + wkShowToast('图片大小不能超过 10MB'); + continue; + } + wkPendingImages.push({ file: f, preview: URL.createObjectURL(f) }); + } + wkRenderImagePreviews(); + } + + function wkRemoveImage(index) { + if (index >= 0 && index < wkPendingImages.length) { + URL.revokeObjectURL(wkPendingImages[index].preview); + wkPendingImages.splice(index, 1); + } + wkRenderImagePreviews(); + } + + function wkClearImages(skipRevoke) { + if (!skipRevoke) { + for (var i = 0; i < wkPendingImages.length; i++) { + URL.revokeObjectURL(wkPendingImages[i].preview); + } + } + wkPendingImages = []; + wkRenderImagePreviews(); + } + + function wkRenderImagePreviews() { + if (!wkImagePreviews) return; + if (wkPendingImages.length === 0) { + wkImagePreviews.style.display = 'none'; + wkImagePreviews.innerHTML = ''; + return; + } + wkImagePreviews.style.display = 'flex'; + var html = ''; + for (var i = 0; i < wkPendingImages.length; i++) { + html += '
    ' + + '' + + '×' + + '
    '; + } + wkImagePreviews.innerHTML = html; + wkImagePreviews.querySelectorAll('.wk-img-remove').forEach(function (btn) { + btn.addEventListener('click', function () { + var idx = parseInt(btn.parentElement.getAttribute('data-idx')); + wkRemoveImage(idx); + }); + }); + } + + function wkUpdateImageUploadUI() { + if (wkImageBtn) wkImageBtn.style.display = wkSelectedAgentImageUpload ? '' : 'none'; + if (!wkSelectedAgentImageUpload && wkPendingImages.length > 0) wkClearImages(); + } + + function wkFileToBase64(file) { + return new Promise(function (resolve, reject) { + var reader = new FileReader(); + reader.onload = function () { resolve(reader.result); }; + reader.onerror = reject; + reader.readAsDataURL(file); + }); + } + + if (wkImageInput) { + wkImageInput.addEventListener('change', function () { + if (wkImageInput.files) wkAddImageFiles(Array.from(wkImageInput.files)); + wkImageInput.value = ''; + }); + } + if (wkImageBtn) { + wkImageBtn.addEventListener('click', function () { + if (wkImageInput) wkImageInput.click(); + }); + } + + // 粘贴图片 + if (chatTextarea) { + chatTextarea.addEventListener('paste', function (e) { + if (!wkSelectedAgentImageUpload) return; + var items = e.clipboardData && e.clipboardData.items; + if (!items) return; + var imageFiles = []; + for (var i = 0; i < items.length; i++) { + if (items[i].type.indexOf('image/') === 0) { + var f = items[i].getAsFile(); + if (f) imageFiles.push(f); + } + } + if (imageFiles.length > 0) { + e.preventDefault(); + wkAddImageFiles(imageFiles); + } + }); + } + if (chatTextarea) { chatTextarea.addEventListener('input', function () { this.style.height = 'auto'; @@ -50,7 +350,7 @@ e.preventDefault(); var text = chatTextarea.value.trim(); if (text) { - alert('发送: ' + text); + sendChatMessage(text); chatTextarea.value = ''; chatTextarea.style.height = 'auto'; } @@ -58,6 +358,793 @@ }); } + // === 流式消息状态 (weknora 页面) — 已移至顶部状态区 === + + function sendChatMessage(text) { + if (!chatBody) return; + + // 首次发送时替换欢迎内容 + if (chatBody.querySelector('h3')) { + chatBody.innerHTML = ''; + chatBody.style.justifyContent = 'flex-start'; + chatBody.style.alignItems = 'stretch'; + chatBody.style.padding = '16px'; + chatBody.style.overflowY = 'auto'; + } + + // 捕获图片预览 URL(清除前) + var msgImageUrls = wkPendingImages.map(function (img) { return img.preview; }); + + // 显示用户消息(含图片) + appendChatMsg('user', text, msgImageUrls.length > 0 ? msgImageUrls : null); + + // 显示 loading + var loadingDiv = document.createElement('div'); + loadingDiv.className = 'chat-msg-bot wk-loading'; + loadingDiv.style.cssText = 'background:#f7f8fa;padding:10px 14px;border-radius:12px;margin-bottom:8px;font-size:13px;color:#999;'; + loadingDiv.textContent = '思考中…'; + chatBody.appendChild(loadingDiv); + chatBody.scrollTop = chatBody.scrollHeight; + + wkStreamingDiv = null; + wkStreamingText = ''; + wkStreamingReferences = []; + wkRenderPending = false; + wkAgentEvents = []; + wkAgentContainer = null; + wkAgentExpandedIds = {}; + wkAgentActiveThinkingIds = {}; + wkAgentHasAnswer = false; + wkAgentTreeExpanded = false; + wkAgentRefsExpanded = false; + wkAgentAnswerText = ''; + wkAgentAnswerDone = false; + _wkFileFailedCache = {}; + wkRequestId = Date.now().toString(36) + Math.random().toString(36).slice(2, 6); + + var chatPayload = { + query: text, + sessionId: currentSessionId, + agentId: wkSelectedAgentId, + agentEnabled: wkSelectedAgentEnabled, + _requestId: wkRequestId + }; + + function doWkSend() { + sendMsg({ type: 'CHAT_QUERY', payload: chatPayload }).then(function (resp) { + loadingDiv.remove(); + if (resp && resp.success) { + if (resp.sessionId) currentSessionId = resp.sessionId; + if (wkAgentContainer) { + wkAgentAnswerDone = true; + wkRenderAgentStream(); + } else if (wkStreamingDiv && wkStreamingText) { + wkStreamingDiv.innerHTML = renderMarkdown(wkStreamingText); + wkHydrateFileImages(wkStreamingDiv); + chatBody.scrollTop = chatBody.scrollHeight; + } else if (resp.data && !wkStreamingDiv && !wkAgentContainer) { + appendChatMsg('bot', resp.data); + } + wkStreamingDiv = null; + wkStreamingText = ''; + } else { + if (wkAgentContainer) { wkAgentContainer.remove(); wkAgentContainer = null; } + appendChatMsg('bot', '请求失败: ' + (resp.error || '请检查服务配置')); + } + }); + } + + // 如果有待上传图片,先转 base64 再发送 + if (wkPendingImages.length > 0) { + var promises = wkPendingImages.map(function (img) { return wkFileToBase64(img.file); }); + Promise.all(promises).then(function (dataURIs) { + chatPayload.images = dataURIs.map(function (d) { return { data: d }; }); + wkClearImages(true); + doWkSend(); + }).catch(function () { + wkShowToast('图片读取失败'); + loadingDiv.remove(); + }); + } else { + doWkSend(); + } + } + + // 移除 loading 指示器 + function removeWkLoading() { + if (!chatBody) return; + var loadings = chatBody.querySelectorAll('.wk-loading'); + loadings.forEach(function (el) { el.remove(); }); + } + + // === 工具名称/图标映射 === + var WK_TOOL_NAMES = { + knowledge_search: '语义搜索', search_knowledge: '语义搜索', + grep_chunks: '文本搜索', list_knowledge_chunks: '阅读文档', + get_document_info: '获取文档信息', get_document_content: '获取文档内容', + web_search: '网络搜索', web_fetch: '网页抓取', + todo_write: '制定计划', final_answer: '生成回答', + thinking: '思考', image_analysis: '查看图片内容', + data_analysis: '数据分析', database_query: '数据库查询' + }; + var WK_TOOL_ICONS = { + '语义搜索': '', + '文本搜索': '', + '阅读文档': '', + '网络搜索': '', + '网页抓取': '', + '制定计划': '', + '思考': '' + }; + var WK_ICON_AGENT = ''; + var WK_ICON_CHEVRON = ''; + var WK_DEFAULT_ICON = ''; + var WK_ICON_COPY = ''; + + function wkGetToolName(raw) { return WK_TOOL_NAMES[raw] || raw; } + function wkGetToolIcon(name) { return WK_TOOL_ICONS[name] || WK_DEFAULT_ICON; } + function wkGetThinkingSummary(content) { + if (!content) return ''; + var c = content.replace(/^#+\s+/gm, '').replace(/\*\*/g, '').replace(/`/g, '').replace(/\n+/g, ' ').trim(); + return c.length <= 50 ? c : c.slice(0, 50) + '…'; + } + function wkExtractSummary(ev) { + if (!ev.output) return ''; + try { + var p = JSON.parse(ev.output); + if (p.results && Array.isArray(p.results)) return '找到 ' + p.results.length + ' 条结果'; + if (p.total_matches !== undefined) return '匹配 ' + p.total_matches + ' 条'; + } catch (e) {} + var s = ev.output.replace(/\n/g, ' ').trim(); + return s.length > 60 ? s.substring(0, 60) + '…' : s; + } + + function wkCleanThinking(text) { + if (!text) return ''; + return text.replace(/Calling tool:\s*\w*/g, '').replace(/\n{3,}/g, '\n\n').trim(); + } + function wkResolveAllPending() { + for (var k = 0; k < wkAgentEvents.length; k++) { + var e = wkAgentEvents[k]; + if (e._type === 'tool_call' && e.pending) { e.pending = false; e.success = true; e.output = e.output || ''; } + } + } + function wkResolveOldPending() { + var found = false; + for (var k = wkAgentEvents.length - 1; k >= 0; k--) { + var e = wkAgentEvents[k]; + if (e._type === 'tool_call' && e.pending) { + if (!found) { found = true; continue; } + e.pending = false; e.success = true; e.output = e.output || ''; + } + } + } + function wkCollapseThinking() { + for (var k in wkAgentActiveThinkingIds) { delete wkAgentExpandedIds[k]; } + wkAgentActiveThinkingIds = {}; + wkAgentContainer && (wkAgentContainer._stepsChanged = true); + } + + function wkCollapseAllExpanded() { + wkAgentExpandedIds = {}; + for (var k in wkAgentActiveThinkingIds) { delete wkAgentActiveThinkingIds[k]; } + wkAgentContainer && (wkAgentContainer._stepsChanged = true); + } + + // 监听流式推送 + if (chrome && chrome.runtime && chrome.runtime.onMessage) { + chrome.runtime.onMessage.addListener(function (msg) { + if (msg.type === 'CHAT_STREAM_CHUNK' && msg.payload) { + var payload = msg.payload; + if (payload.requestId && payload.requestId !== wkRequestId) return; + + if (payload.responseType === 'answer' && chatBody) { + wkAgentAnswerText += payload.content || ''; + if (payload.done) wkAgentAnswerDone = true; + if (!wkAgentHasAnswer) { + wkAgentHasAnswer = true; + removeWkLoading(); + wkCollapseAllExpanded(); + wkResolveAllPending(); + wkAgentContainer && (wkAgentContainer._stepsChanged = true); + } + wkScheduleRender(); + + } else if ((payload.responseType === 'thinking' || (payload.responseType === 'tool_call' && payload.toolName === 'thinking')) && chatBody) { + var thinkContent = wkCleanThinking(payload.content); + if (!thinkContent || /^Thought process recorded/i.test(thinkContent)) return; + var lastEv = wkAgentEvents.length > 0 ? wkAgentEvents[wkAgentEvents.length - 1] : null; + if (lastEv && lastEv._type === 'thinking') { + lastEv.content += thinkContent; + } else { + wkCollapseAllExpanded(); + var tid = 'wkt-' + Date.now(); + wkAgentEvents.push({ _type: 'thinking', id: tid, content: thinkContent }); + wkAgentActiveThinkingIds[tid] = true; + wkAgentContainer && (wkAgentContainer._stepsChanged = true); + } + wkScheduleRender(); + + } else if (payload.responseType === 'tool_call' && chatBody) { + var existingWkTc = null; + if (payload.eventId) { + for (var wei = wkAgentEvents.length - 1; wei >= 0; wei--) { + if (wkAgentEvents[wei]._type === 'tool_call' && wkAgentEvents[wei].eventId === payload.eventId) { + existingWkTc = wkAgentEvents[wei]; break; + } + } + } + if (existingWkTc) { + if (payload.content) existingWkTc.content = payload.content; + } else { + wkCollapseThinking(); + wkCollapseAllExpanded(); + wkResolveOldPending(); + var tcId = payload.eventId || ('wktc-' + Date.now() + '-' + Math.random().toString(36).slice(2, 5)); + wkAgentEvents.push({ _type: 'tool_call', id: tcId, eventId: payload.eventId || '', tool_name: payload.toolName || 'tool', pending: true, success: null, content: payload.content || '', output: '' }); + wkAgentContainer && (wkAgentContainer._stepsChanged = true); + } + wkScheduleRender(); + + } else if (payload.responseType === 'tool_result' && chatBody) { + if (payload.toolName === 'thinking') { + var thinkRes = wkCleanThinking(payload.content); + if (thinkRes && !/^Thought process recorded/i.test(thinkRes)) { + var lastThink = wkAgentEvents.length > 0 ? wkAgentEvents[wkAgentEvents.length - 1] : null; + if (lastThink && lastThink._type === 'thinking') lastThink.content += thinkRes; + } + wkScheduleRender(); + } else { + var wkMatched = false; + for (var ri = wkAgentEvents.length - 1; ri >= 0; ri--) { + var rev = wkAgentEvents[ri]; + if (rev._type !== 'tool_call' || !rev.pending) continue; + var idMatch = payload.eventId && rev.eventId && rev.eventId === payload.eventId; + var nameMatch = payload.toolName && rev.tool_name === payload.toolName; + if (idMatch || nameMatch || !wkMatched) { + rev.pending = false; rev.success = payload.success !== false; rev.output = payload.content || ''; + wkMatched = true; break; + } + } + wkAgentContainer && (wkAgentContainer._stepsChanged = true); + wkScheduleRender(); + } + + } else if (payload.responseType === 'references' && chatBody) { + var refs = payload.references; + if (Array.isArray(refs) && refs.length > 0) { + wkStreamingReferences = refs; + wkScheduleRender(); + } + + } else if (payload.responseType === 'complete' && chatBody) { + removeWkLoading(); + wkCollapseAllExpanded(); + wkResolveAllPending(); + wkAgentAnswerDone = true; + wkAgentContainer && (wkAgentContainer._stepsChanged = true); + wkScheduleRender(); + + } else if (payload.responseType === 'error' && chatBody) { + removeWkLoading(); + if (wkAgentContainer) { wkAgentContainer.remove(); wkAgentContainer = null; } + if (wkStreamingDiv) { wkStreamingDiv.remove(); wkStreamingDiv = null; wkStreamingText = ''; } + appendChatMsg('bot', payload.content || '请求出错'); + } + } + }); + } + + var _wkRenderTimer = null; + var _wkLastRenderTime = 0; + var WK_RENDER_THROTTLE = 60; + function wkScheduleRender() { + var now = Date.now(); + var elapsed = now - _wkLastRenderTime; + if (elapsed >= WK_RENDER_THROTTLE) { + if (_wkRenderTimer) { clearTimeout(_wkRenderTimer); _wkRenderTimer = null; } + _wkLastRenderTime = now; + wkRenderAgentStream(); + } else if (!_wkRenderTimer) { + _wkRenderTimer = setTimeout(function () { + _wkRenderTimer = null; + _wkLastRenderTime = Date.now(); + wkRenderAgentStream(); + }, WK_RENDER_THROTTLE - elapsed); + } + } + + // === Agent 流式渲染 === + function wkRenderAgentStream() { + if (wkAgentEvents.length === 0 && !wkAgentAnswerText) return; + if (!chatBody) return; + + if (!wkAgentContainer) { + wkAgentContainer = document.createElement('div'); + wkAgentContainer.className = 'agent-stream'; + chatBody.appendChild(wkAgentContainer); + wkAgentContainer._stepsEl = document.createElement('div'); + wkAgentContainer.appendChild(wkAgentContainer._stepsEl); + } + + var changed = false; + + // --- 1. 渲染中间步骤(仅当事件变化时更新)--- + if (!wkAgentContainer._lastStepCount || wkAgentContainer._lastStepCount !== wkAgentEvents.length || + wkAgentContainer._stepsChanged || (wkAgentHasAnswer && !wkAgentContainer._treeMode)) { + wkRenderAgentSteps(); + if (wkAgentHasAnswer) wkAgentContainer._treeMode = true; + changed = true; + } else { + var lastEvWk = wkAgentEvents.length > 0 ? wkAgentEvents[wkAgentEvents.length - 1] : null; + if (lastEvWk && lastEvWk._type === 'thinking' && wkAgentActiveThinkingIds[lastEvWk.id]) { + wkRenderAgentSteps(); + changed = true; + } + } + // --- 2. 增量渲染回答内容(有图片,必须保持 DOM 稳定)--- + if (wkRenderAgentAnswer()) changed = true; + + if (changed) chatBody.scrollTop = chatBody.scrollHeight; + } + + function wkRenderAgentSteps() { + var stepsEl = wkAgentContainer._stepsEl; + var events = wkAgentEvents; + var lastEv = events.length > 0 ? events[events.length - 1] : null; + + if (lastEv && lastEv._type === 'thinking' && + wkAgentContainer._lastStepCount === events.length && + !wkAgentContainer._stepsChanged && !wkAgentHasAnswer) { + var activeCard = stepsEl.querySelector('.agent-card[data-eid="' + lastEv.id + '"]'); + if (activeCard) { + var summaryEl = activeCard.querySelector('.agent-card-summary'); + if (summaryEl) summaryEl.textContent = wkGetThinkingSummary(lastEv.content); + var detailEl = activeCard.querySelector('.agent-card-detail'); + if (detailEl) { + detailEl.textContent = lastEv.content; + detailEl.scrollTop = detailEl.scrollHeight; + } + return; + } + } + + wkAgentContainer._lastStepCount = events.length; + wkAgentContainer._stepsChanged = false; + + stepsEl.querySelectorAll('.agent-card.expanded').forEach(function (el) { + var eid = el.getAttribute('data-eid'); + if (eid) wkAgentExpandedIds[eid] = true; + }); + if (stepsEl.querySelector('.agent-tree-root.expanded')) wkAgentTreeExpanded = true; + + var intermediateEvents = events.filter(function (e) { return e._type !== 'error'; }); + var stepCount = intermediateEvents.length; + var html = ''; + + if (wkAgentHasAnswer && stepCount > 0) { + html += '
    '; + html += '' + WK_ICON_AGENT + '智能体执行 ' + stepCount + ' 步'; + html += '' + WK_ICON_CHEVRON + ''; + html += '
    '; + html += '
    '; + for (var ti = 0; ti < intermediateEvents.length; ti++) { + var isLast = ti === intermediateEvents.length - 1; + html += '
    '; + html += '
    '; + html += wkBuildCardHTML(intermediateEvents[ti]); + html += '
    '; + } + html += '
    '; + } else if (!wkAgentHasAnswer) { + for (var fi = 0; fi < events.length; fi++) { + html += wkBuildCardHTML(events[fi]); + } + } + + stepsEl.innerHTML = html; + wkBindStepEvents(stepsEl); + + for (var aid in wkAgentActiveThinkingIds) { + var thinkCard = stepsEl.querySelector('.agent-card[data-eid="' + aid + '"]'); + if (thinkCard) { + var det = thinkCard.querySelector('.agent-card-detail'); + if (det) det.scrollTop = det.scrollHeight; + } + } + } + + function wkBindStepEvents(stepsEl) { + var treeRoot = stepsEl.querySelector('.agent-tree-root'); + if (treeRoot) { + treeRoot.addEventListener('click', function () { + wkAgentTreeExpanded = !wkAgentTreeExpanded; + treeRoot.classList.toggle('expanded'); + var children = treeRoot.nextElementSibling; + if (children && children.classList.contains('agent-tree-children')) { + children.style.display = wkAgentTreeExpanded ? 'block' : 'none'; + } + }); + } + stepsEl.querySelectorAll('.agent-card-header:not(.no-expand)').forEach(function (hdr) { + hdr.addEventListener('click', function () { + var card = hdr.closest('.agent-card'); + if (!card) return; + var eid = card.getAttribute('data-eid'); + card.classList.toggle('expanded'); + if (eid) { + if (card.classList.contains('expanded')) wkAgentExpandedIds[eid] = true; + else delete wkAgentExpandedIds[eid]; + } + }); + }); + } + + var WK_ANSWER_RENDER_INTERVAL = 20; + function wkRenderAgentAnswer() { + if (!wkAgentAnswerText) return false; + + if (!wkAgentContainer._answerWrapper) { + var wrapper = document.createElement('div'); + wrapper.className = 'agent-answer'; + var content = document.createElement('div'); + content.className = 'agent-answer-content'; + wrapper.appendChild(content); + wkAgentContainer.appendChild(wrapper); + wkAgentContainer._answerWrapper = wrapper; + wkAgentContainer._answerContent = content; + wkAgentContainer._lastAnswerLen = 0; + wkAgentContainer._lastAnswerRenderTime = 0; + wkAgentContainer._answerTimer = null; + } + + var newLen = wkAgentAnswerText.length; + if (newLen === (wkAgentContainer._lastAnswerLen || 0)) return false; + + var now = Date.now(); + var timeSince = now - (wkAgentContainer._lastAnswerRenderTime || 0); + + if (wkAgentAnswerDone || timeSince >= WK_ANSWER_RENDER_INTERVAL) { + if (wkAgentContainer._answerTimer) { clearTimeout(wkAgentContainer._answerTimer); wkAgentContainer._answerTimer = null; } + wkAgentContainer._lastAnswerLen = newLen; + wkAgentContainer._lastAnswerRenderTime = now; + wkAgentContainer._answerContent.innerHTML = renderMarkdown(wkAgentAnswerText); + wkHydrateFileImages(wkAgentContainer._answerContent); + return true; + } + if (!wkAgentContainer._answerTimer) { + wkAgentContainer._answerTimer = setTimeout(function () { + wkAgentContainer._answerTimer = null; + if (wkRenderAgentAnswer()) chatBody.scrollTop = chatBody.scrollHeight; + }, WK_ANSWER_RENDER_INTERVAL - timeSince); + } + return false; + + if (wkAgentAnswerDone && !wkAgentContainer._toolbarEl) { + var toolbar = document.createElement('div'); + toolbar.className = 'agent-answer-toolbar'; + toolbar.innerHTML = ''; + toolbar.querySelector('.wk-btn-copy').addEventListener('click', function () { + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(wkAgentAnswerText || ''); + } else { + var ta = document.createElement('textarea'); ta.value = wkAgentAnswerText || ''; + ta.style.cssText = 'position:fixed;left:-9999px;'; document.body.appendChild(ta); + ta.select(); try { document.execCommand('copy'); } catch (e) {} document.body.removeChild(ta); + } + }); + wkAgentContainer._answerWrapper.appendChild(toolbar); + wkAgentContainer._toolbarEl = toolbar; + } + } + + function wkBuildCardHTML(ev) { + var isActive = !!wkAgentActiveThinkingIds[ev.id]; + var isExpanded = isActive || !!wkAgentExpandedIds[ev.id]; + var cls = 'agent-card'; + if (isExpanded) cls += ' expanded'; + if (ev.pending) cls += ' pending'; + if (ev.success === false) cls += ' error'; + var html = '
    '; + + if (ev._type === 'thinking') { + var summary = wkGetThinkingSummary(ev.content); + html += '
    ' + wkGetToolIcon('思考') + '思考'; + if (!isExpanded && summary) html += '' + escapeHtml(summary) + ''; + if (isActive) html += ''; + html += '
    '; + if (ev.content) html += '' + WK_ICON_CHEVRON + ''; + html += '
    '; + if (ev.content) html += '
    ' + escapeHtml(ev.content) + '
    '; + + } else if (ev._type === 'tool_call') { + var displayName = wkGetToolName(ev.tool_name); + var icon = wkGetToolIcon(displayName); + if (ev.pending) { + html += '
    ' + icon + '正在调用 ' + escapeHtml(displayName) + '…
    '; + } else { + var toolSummary = wkExtractSummary(ev); + var hasDetail = !!ev.output; + html += '
    ' + icon + '' + escapeHtml(displayName) + ''; + if (!isExpanded && toolSummary) html += '' + escapeHtml(toolSummary) + ''; + html += '
    '; + if (hasDetail) html += '' + WK_ICON_CHEVRON + ''; + html += '
    '; + if (toolSummary && !isExpanded) html += '
    ' + escapeHtml(toolSummary) + '
    '; + if (hasDetail) html += '
    ' + escapeHtml(ev.output).substring(0, 600) + '
    '; + } + } + html += '
    '; + return html; + } + + // wkBuildRefsHTML 和 wkBindAgentEvents 已拆解到 wkRenderAgentSteps / wkRenderAgentAnswer 中 + + function appendChatMsg(role, text, imageUrls) { + if (!chatBody) return; + var div = document.createElement('div'); + if (role === 'user') { + div.style.cssText = 'background:#07C160;color:#fff;padding:10px 14px;border-radius:12px;border-bottom-right-radius:4px;margin-bottom:8px;font-size:13px;align-self:flex-end;max-width:80%;word-break:break-word;'; + } else { + div.style.cssText = 'background:#f7f8fa;padding:10px 14px;border-radius:12px;border-bottom-left-radius:4px;margin-bottom:8px;font-size:13px;color:#333;max-width:85%;word-break:break-word;line-height:1.6;'; + } + if (role === 'bot') { + div.innerHTML = renderMarkdown(text); + wkHydrateFileImages(div); + } else { + div.textContent = text; + } + // 在用户消息中显示上传的图片 + if (role === 'user' && imageUrls && imageUrls.length > 0) { + var imgWrap = document.createElement('div'); + imgWrap.style.cssText = 'display:flex;gap:6px;margin-top:6px;flex-wrap:wrap;'; + imageUrls.forEach(function (url) { + var img = document.createElement('img'); + img.src = url; + img.style.cssText = 'width:60px;height:60px;border-radius:8px;object-fit:cover;border:1px solid rgba(255,255,255,0.3);'; + imgWrap.appendChild(img); + }); + div.appendChild(imgWrap); + } + chatBody.appendChild(div); + chatBody.scrollTop = chatBody.scrollHeight; + } + + // === 文件 URL 转换 === + var _wkFileBlobCache = {}; + var _wkFileFailedCache = {}; + + function wkHydrateFileImages(container) { + if (!container) return; + var imgs = container.querySelectorAll('img[data-file-src]'); + if (!imgs.length) return; + imgs.forEach(function (img) { + var fileSrc = img.getAttribute('data-file-src'); + if (!fileSrc) return; + if (_wkFileFailedCache[fileSrc]) { + img.removeAttribute('src'); + img.alt = '[图片加载失败]'; + img.style.display = 'none'; + return; + } + if (_wkFileBlobCache[fileSrc]) { + img.src = _wkFileBlobCache[fileSrc]; + return; + } + if (img.getAttribute('data-hydrated') === '1') return; + img.setAttribute('data-hydrated', '1'); + chrome.runtime.sendMessage({ type: 'FETCH_FILE', payload: { filePath: fileSrc } }, function (resp) { + void chrome.runtime.lastError; + if (resp && resp.success && resp.dataUrl) { + _wkFileBlobCache[fileSrc] = resp.dataUrl; + img.src = resp.dataUrl; + img.style.display = ''; + } else { + _wkFileFailedCache[fileSrc] = true; + img.alt = '[图片加载失败]'; + img.style.display = 'none'; + } + }); + }); + } + + // === Markdown 渲染(参考 botmsg.vue / AgentStreamDisplay.vue)=== + var WK_MD_MAX_LEN = 200000; + function renderMarkdown(text) { + if (!text) return ''; + if (text.length > WK_MD_MAX_LEN) text = text.substring(0, WK_MD_MAX_LEN) + '\n\n…(内容过长,已截断)'; + + // 1. 提取代码块,替换为占位符(避免内部被解析) + var codeBlocks = []; + var processed = text.replace(/```(\w*)\n([\s\S]*?)```/g, function (_, lang, code) { + var idx = codeBlocks.length; + codeBlocks.push('
    ' + escapeHtml(code.replace(/\n$/, '')) + '
    '); + return '\x00CODEBLOCK' + idx + '\x00'; + }); + + // 2. 提取行内代码 + var inlineCodes = []; + processed = processed.replace(/`([^`]+)`/g, function (_, code) { + var idx = inlineCodes.length; + inlineCodes.push('' + escapeHtml(code) + ''); + return '\x00INLINE' + idx + '\x00'; + }); + + // 2.5 提取表格,替换为占位符 + var tables = []; + processed = processed.replace(/((?:\|.+\|\n)+)/g, function (tableBlock) { + var rows = tableBlock.trim().split('\n'); + if (rows.length < 2) return tableBlock; + var out = ''; + rows.forEach(function (row, ri) { + if (/^\|[\s\-:|]+\|$/.test(row)) return; + var tag = ri === 0 ? 'th' : 'td'; + var cells = row.split('|').filter(function (c, ci, arr) { return ci > 0 && ci < arr.length - 1; }); + out += ''; + cells.forEach(function (cell) { out += '<' + tag + '>' + inlineFormat(cell.trim()) + ''; }); + out += ''; + }); + out += '
    '; + var idx = tables.length; + tables.push(out); + return '\n\x00TABLE' + idx + '\x00\n'; + }); + + // 3. 逐行解析为块元素 + var lines = processed.split('\n'); + var html = ''; + var i = 0; + + while (i < lines.length) { + var line = lines[i]; + + // 代码块占位符 + var cbMatch = line.match(/^\x00CODEBLOCK(\d+)\x00$/); + if (cbMatch) { + html += codeBlocks[parseInt(cbMatch[1])]; + i++; continue; + } + + // 表格占位符 + var tbMatch = line.match(/^\x00TABLE(\d+)\x00$/); + if (tbMatch) { + html += '
    ' + tables[parseInt(tbMatch[1])] + '
    '; + i++; continue; + } + + // 标题 + if (/^(#{1,6})\s+(.+)/.test(line)) { + var level = RegExp.$1.length; + html += '' + inlineFormat(RegExp.$2) + ''; + i++; continue; + } + + // 引用块(合并连续 > 行) + if (/^>\s*(.*)/.test(line)) { + var bqLines = []; + while (i < lines.length && /^>\s*(.*)/.test(lines[i])) { + bqLines.push(RegExp.$1); + i++; + } + html += '
    ' + inlineFormat(bqLines.join('
    ')) + '
    '; + continue; + } + + // 无序列表(- 或 * 开头,需要和 *italic* 区分) + if (/^(\s*)[*\-]\s+(.+)/.test(line)) { + html += parseList(lines, i, 'ul'); + while (i < lines.length && /^(\s*)[*\-]\s+/.test(lines[i])) i++; + continue; + } + + // 有序列表 + if (/^(\s*)\d+\.\s+(.+)/.test(line)) { + html += parseList(lines, i, 'ol'); + while (i < lines.length && /^(\s*)\d+\.\s+/.test(lines[i])) i++; + continue; + } + + // 水平线 + if (/^[-*_]{3,}\s*$/.test(line.trim())) { + html += '
    '; + i++; continue; + } + + // 空行 + if (line.trim() === '') { + i++; continue; + } + + var pLines = []; + while (i < lines.length && pLines.length < 500) { + var pl = lines[i]; + if (pl.trim() === '' || /^#{1,6}\s/.test(pl) || /^>\s/.test(pl) || + /^(\s*)[*\-]\s+/.test(pl) || /^(\s*)\d+\.\s+/.test(pl) || + /^\x00CODEBLOCK/.test(pl) || /^\x00TABLE/.test(pl) || /^[-*_]{3,}\s*$/.test(pl.trim())) break; + pLines.push(pl); + i++; + } + try { + html += '

    ' + inlineFormat(pLines.join('
    ')) + '

    '; + } catch (e) { + html += '

    ' + escapeHtml(pLines.slice(0, 20).join('\n')) + '…

    '; + } + } + + // 4. 恢复行内代码占位符 + html = html.replace(/\x00INLINE(\d+)\x00/g, function (_, idx) { + return inlineCodes[parseInt(idx)]; + }); + + return html; + + // --- 列表解析(支持嵌套)--- + function parseList(allLines, startIdx, defaultType) { + var items = []; + var j = startIdx; + var baseIndent = -1; + var listTag = defaultType; + var re = listTag === 'ul' ? /^(\s*)[*\-]\s+(.+)/ : /^(\s*)\d+\.\s+(.+)/; + + while (j < allLines.length) { + var m = allLines[j].match(re); + if (!m) break; + var indent = m[1].length; + if (baseIndent < 0) baseIndent = indent; + if (indent > baseIndent) { + var subStart = j; + while (j < allLines.length) { + var sm = allLines[j].match(re); + if (!sm || sm[1].length < indent) break; + j++; + } + var subHtml = parseList(allLines, subStart, listTag); + if (items.length > 0) { + items[items.length - 1] += subHtml; + } + continue; + } + if (indent < baseIndent) break; + items.push(inlineFormat(m[2])); + j++; + } + + var out = '<' + listTag + ' class="md-list">'; + for (var k = 0; k < items.length; k++) { + out += '
  • ' + items[k] + '
  • '; + } + out += ''; + return out; + } + + // --- 行内格式化 --- + function inlineFormat(s) { + if (!s) return ''; + s = s.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, function (_, alt, url) { + if (/^(minio|cos|tos|local|s3):\/\//.test(url)) { + if (_wkFileFailedCache[url]) return ''; + return '' + alt + ''; + } + return '' + alt + ''; + }); + // 链接 + s = s.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); + // 粗斜体 + s = s.replace(/\*\*\*(.+?)\*\*\*/g, '$1'); + // 粗体 + s = s.replace(/\*\*(.+?)\*\*/g, '$1'); + // 斜体(避免匹配列表 * ) + s = s.replace(/(?$1'); + // 删除线 + s = s.replace(/~~(.+?)~~/g, '$1'); + // 恢复行内代码占位符 + s = s.replace(/\x00INLINE(\d+)\x00/g, function (_, idx) { + return inlineCodes[parseInt(idx)]; + }); + return s; + } + } + // === 对话面板头部按钮 === $$('.chat-panel-btn').forEach(function (btn) { btn.addEventListener('click', function () { @@ -72,18 +1159,36 @@ }); }); - // === 工具栏按钮交互 === - $$('.chat-tool-btn').forEach(function (btn) { - btn.addEventListener('click', function () { - // 切换 mode-active 状态 - if (btn.classList.contains('mode-active')) { - btn.classList.remove('mode-active'); - } else { + // === 对话面板工具栏 — 动态渲染智能体列表 === + function renderChatToolbar() { + var toolbar = $('.chat-input-toolbar'); + if (!toolbar || agents.length === 0) return; + + // 清空现有工具栏按钮 + toolbar.innerHTML = ''; + + agents.forEach(function (agent, idx) { + var btn = document.createElement('button'); + var isSelected = wkSelectedAgentId ? (agent.id === wkSelectedAgentId) : (idx === 0); + btn.className = 'chat-tool-btn' + (isSelected ? ' mode-active' : ''); + btn.textContent = agent.name; + btn.setAttribute('data-agent-id', agent.id); + var isQuickAnswer = agent.id === 'builtin-quick-answer' || (agent.config && agent.config.agent_mode === 'quick-answer'); + + btn.addEventListener('click', function () { $$('.chat-tool-btn').forEach(function (b) { b.classList.remove('mode-active'); }); btn.classList.add('mode-active'); - } + wkSelectedAgentId = agent.id; + wkSelectedAgentEnabled = !isQuickAnswer; + wkSelectedAgentImageUpload = !!(agent.config && agent.config.image_upload_enabled); + wkUpdateImageUploadUI(); + // 持久化模式选择,同步给 sidepanel + chrome.storage.local.set({ ka_selected_agent: { agentId: agent.id, agentEnabled: !isQuickAnswer } }); + }); + + toolbar.appendChild(btn); }); - }); + } // === 侧边栏折叠按钮 === var logoBtn = $('.sidebar-logo-btn'); @@ -106,4 +1211,46 @@ }); } + // === 初始化:加载知识库和智能体 === + loadKnowledgeBases(); + loadAgentList(); + + // 缓存 baseUrl 用于文件 URL 转换 + sendMsg({ type: 'GET_CONFIG' }).then(function (resp) { + if (resp && resp.success && resp.data && resp.data.baseUrl) { + wkCachedBaseUrl = resp.data.baseUrl.replace(/\/+$/, ''); + } + }); + + // 加载用户信息 + sendMsg({ type: 'GET_USER_INFO' }).then(function (resp) { + if (resp && resp.success && resp.data) { + var user = resp.data.user || resp.data; + var nameEl = $('#wk-user-name'); + var emailEl = $('#wk-user-email'); + var letterEl = $('#wk-user-letter'); + if (nameEl) nameEl.textContent = user.username || user.name || '用户'; + if (emailEl) emailEl.textContent = user.email || ''; + if (letterEl) letterEl.textContent = (user.username || user.name || 'U').charAt(0).toUpperCase(); + } + }); + + // === 图片点击放大(灯箱)=== + if (chatBody) { + chatBody.addEventListener('click', function (e) { + var img = e.target; + if (img.tagName !== 'IMG') return; + var src = img.src; + if (!src) return; + var overlay = document.createElement('div'); + overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.8);z-index:9999;display:flex;align-items:center;justify-content:center;cursor:zoom-out;'; + var bigImg = document.createElement('img'); + bigImg.src = src; + bigImg.style.cssText = 'max-width:95%;max-height:95%;object-fit:contain;border-radius:8px;box-shadow:0 4px 32px rgba(0,0,0,0.4);'; + overlay.appendChild(bigImg); + document.body.appendChild(overlay); + overlay.addEventListener('click', function () { overlay.remove(); }); + }); + } + })();